Compare commits
14 Commits
86d7cd2fe1
...
6d8076674f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d8076674f | ||
|
|
c39ee1ce2a | ||
|
|
16865117de | ||
|
|
86ab2d2f85 | ||
|
|
16739d3cf8 | ||
|
|
cab064f83f | ||
|
|
e3a79166c2 | ||
|
|
f77955074e | ||
|
|
3df7ae78c9 | ||
|
|
4034e98dfb | ||
|
|
3faaff6220 | ||
|
|
1d48cdc973 | ||
|
|
8d144ac139 | ||
|
|
b413edccaf |
@ -15,6 +15,7 @@ fn shared_tools() -> Vec<String> {
|
||||
"git_file_content".into(),
|
||||
"git_blob_get".into(),
|
||||
"git_blob_content".into(),
|
||||
"git_status".into(),
|
||||
]
|
||||
}
|
||||
|
||||
@ -25,8 +26,10 @@ fn researcher_tools() -> Vec<String> {
|
||||
"git_search_commits".into(),
|
||||
"repo_search".into(),
|
||||
"repo_doc_search".into(),
|
||||
"project_bing_search".into(),
|
||||
"project_arxiv_search".into(),
|
||||
"project_curl".into(),
|
||||
"curl_exec".into(),
|
||||
// Index / overview
|
||||
"repo_overview".into(),
|
||||
"repo_readme".into(),
|
||||
@ -45,6 +48,10 @@ fn researcher_tools() -> Vec<String> {
|
||||
"git_graph".into(),
|
||||
"git_commit_info".into(),
|
||||
"repo_commit_log".into(),
|
||||
"git_ref_list".into(),
|
||||
"git_ref_info".into(),
|
||||
"git_lfs_summary".into(),
|
||||
"git_lfs_scan_tree".into(),
|
||||
]
|
||||
}
|
||||
|
||||
@ -65,6 +72,9 @@ fn analyst_tools() -> Vec<String> {
|
||||
"git_branch_list".into(),
|
||||
"git_branch_info".into(),
|
||||
"git_branch_diff".into(),
|
||||
"git_merge_analysis".into(),
|
||||
"git_ref_list".into(),
|
||||
"git_ref_info".into(),
|
||||
// Project data
|
||||
"project_list_issues".into(),
|
||||
"project_list_repos".into(),
|
||||
@ -81,6 +91,7 @@ fn reviewer_tools() -> Vec<String> {
|
||||
// Merge status
|
||||
"git_branches_merged".into(),
|
||||
"git_branch_info".into(),
|
||||
"git_merge_analysis".into(),
|
||||
// Tracking
|
||||
"project_list_issues".into(),
|
||||
"project_update_issue".into(),
|
||||
@ -90,6 +101,108 @@ fn reviewer_tools() -> Vec<String> {
|
||||
]
|
||||
}
|
||||
|
||||
/// Architect-specific tools (system design & dependency mapping).
|
||||
fn architect_tools() -> Vec<String> {
|
||||
vec![
|
||||
// Repository structure
|
||||
"repo_overview".into(),
|
||||
"repo_readme".into(),
|
||||
"repo_file_tree".into(),
|
||||
"repo_languages".into(),
|
||||
"repo_dependencies".into(),
|
||||
"repo_test_discovery".into(),
|
||||
// Change and branch context
|
||||
"repo_diff_summary".into(),
|
||||
"git_branch_list".into(),
|
||||
"git_branch_info".into(),
|
||||
"git_branch_diff".into(),
|
||||
"git_merge_analysis".into(),
|
||||
"git_ref_list".into(),
|
||||
// Project context
|
||||
"project_list_repos".into(),
|
||||
"project_list_issues".into(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Debugger-specific tools (root-cause analysis & history tracing).
|
||||
fn debugger_tools() -> Vec<String> {
|
||||
vec![
|
||||
// Failure and change inspection
|
||||
"git_show".into(),
|
||||
"git_diff".into(),
|
||||
"git_diff_stats".into(),
|
||||
"git_blame".into(),
|
||||
"git_log".into(),
|
||||
"git_commit_info".into(),
|
||||
"git_ref_info".into(),
|
||||
// Structural clues
|
||||
"repo_file_tree".into(),
|
||||
"repo_dependencies".into(),
|
||||
"repo_test_discovery".into(),
|
||||
"repo_doc_search".into(),
|
||||
// Issue context
|
||||
"project_list_issues".into(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Implementer-specific tools (implementation planning & code navigation).
|
||||
fn implementer_tools() -> Vec<String> {
|
||||
vec![
|
||||
// Code and documentation context
|
||||
"repo_overview".into(),
|
||||
"repo_readme".into(),
|
||||
"repo_file_tree".into(),
|
||||
"repo_doc_index".into(),
|
||||
"repo_doc_read".into(),
|
||||
"repo_dependencies".into(),
|
||||
"repo_test_discovery".into(),
|
||||
// Current change context
|
||||
"git_diff".into(),
|
||||
"git_diff_stats".into(),
|
||||
"git_show".into(),
|
||||
"git_branch_info".into(),
|
||||
// Project context
|
||||
"project_list_issues".into(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Tester-specific tools (coverage, regression, and validation planning).
|
||||
fn tester_tools() -> Vec<String> {
|
||||
vec![
|
||||
// Test discovery and changed surface
|
||||
"git_grep".into(),
|
||||
"git_diff".into(),
|
||||
"git_diff_stats".into(),
|
||||
"git_show".into(),
|
||||
"repo_file_tree".into(),
|
||||
"repo_dependencies".into(),
|
||||
"repo_test_discovery".into(),
|
||||
// Project context
|
||||
"project_list_issues".into(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Security-specific tools (threat modeling & sensitive-code review).
|
||||
fn security_tools() -> Vec<String> {
|
||||
vec![
|
||||
// Sensitive pattern discovery
|
||||
"git_grep".into(),
|
||||
"git_diff".into(),
|
||||
"git_diff_stats".into(),
|
||||
"git_blame".into(),
|
||||
"git_log".into(),
|
||||
// Dependency and surface review
|
||||
"repo_file_tree".into(),
|
||||
"repo_dependencies".into(),
|
||||
"repo_doc_search".into(),
|
||||
"git_lfs_summary".into(),
|
||||
"git_lfs_pointer_info".into(),
|
||||
"git_lfs_object_info".into(),
|
||||
// Issue context
|
||||
"project_list_issues".into(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Supervisor-specific tools (delegation & synthesis).
|
||||
fn supervisor_tools() -> Vec<String> {
|
||||
vec![
|
||||
@ -105,12 +218,31 @@ pub fn tools_for_role(role: &AgentRole) -> Vec<String> {
|
||||
AgentRole::Researcher => tools.extend(researcher_tools()),
|
||||
AgentRole::Analyst => tools.extend(analyst_tools()),
|
||||
AgentRole::Reviewer => tools.extend(reviewer_tools()),
|
||||
AgentRole::Architect => tools.extend(architect_tools()),
|
||||
AgentRole::Debugger => tools.extend(debugger_tools()),
|
||||
AgentRole::Implementer => tools.extend(implementer_tools()),
|
||||
AgentRole::Tester => tools.extend(tester_tools()),
|
||||
AgentRole::Security => tools.extend(security_tools()),
|
||||
AgentRole::Supervisor => tools.extend(supervisor_tools()),
|
||||
AgentRole::Default => {} // Default role gets only shared tools
|
||||
}
|
||||
tools
|
||||
}
|
||||
|
||||
pub fn profile_for_role_name(role: &str) -> AgentExecutionProfile {
|
||||
match role {
|
||||
"researcher" => researcher_profile(),
|
||||
"analyst" => analyst_profile(),
|
||||
"reviewer" => reviewer_profile(),
|
||||
"architect" => architect_profile(),
|
||||
"debugger" => debugger_profile(),
|
||||
"implementer" => implementer_profile(),
|
||||
"tester" => tester_profile(),
|
||||
"security" => security_profile(),
|
||||
_ => researcher_profile(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn supervisor_profile() -> AgentExecutionProfile {
|
||||
AgentExecutionProfile {
|
||||
role: AgentRole::Supervisor,
|
||||
@ -123,6 +255,11 @@ pub fn supervisor_profile() -> AgentExecutionProfile {
|
||||
- **researcher**: Gathers concrete facts, evidence, and data from tools and context. Best for finding information, searching code, and discovering evidence.\n\
|
||||
- **analyst**: Builds coherent explanations, highlights causal links, edge cases, and tradeoffs. Best for explaining findings and reasoning about implications.\n\
|
||||
- **reviewer**: Stress-tests proposals, identifies contradictions, missing assumptions, regressions, and risks. Best for quality checks and risk assessment.\n\
|
||||
- **architect**: Maps systems, boundaries, dependencies, and long-term design tradeoffs. Best for architecture decisions and refactor strategy.\n\
|
||||
- **debugger**: Finds likely root causes, reproduction gaps, and suspect changes. Best for failures, regressions, and confusing behavior.\n\
|
||||
- **implementer**: Turns requirements into concrete implementation steps, affected files, and integration concerns. Best for execution planning.\n\
|
||||
- **tester**: Designs validation strategy, regression coverage, and edge-case test plans. Best for test planning and release confidence.\n\
|
||||
- **security**: Reviews auth, data exposure, injection, dependency, and abuse risks. Best for threat modeling and sensitive changes.\n\
|
||||
- Provide a clear, focused task description for each sub-agent.\n\
|
||||
- You may call multiple sub-agents in sequence (call one, review its output, then decide to call another).\n\
|
||||
- You may also call the same role twice with different tasks if needed.\n\
|
||||
@ -130,7 +267,10 @@ pub fn supervisor_profile() -> AgentExecutionProfile {
|
||||
## Decision Guide\n\
|
||||
- Simple factual questions: call researcher only.\n\
|
||||
- Questions requiring explanation: call researcher then analyst.\n\
|
||||
- Design/architecture reviews: call researcher, analyst, then reviewer.\n\
|
||||
- Design/architecture reviews: call researcher, architect, then reviewer.\n\
|
||||
- Bug or regression diagnosis: call debugger, then tester if validation is needed.\n\
|
||||
- Implementation requests: call implementer, then reviewer for risk checks.\n\
|
||||
- Security-sensitive changes: call security, then reviewer.\n\
|
||||
- If a sub-agent's output is insufficient, call another sub-agent for clarification.\n\
|
||||
\n\
|
||||
## Output Rules\n\
|
||||
@ -201,8 +341,93 @@ pub fn reviewer_profile() -> AgentExecutionProfile {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn architect_profile() -> AgentExecutionProfile {
|
||||
AgentExecutionProfile {
|
||||
role: AgentRole::Architect,
|
||||
system_prompt: Some(
|
||||
"You are the architect agent. Map system boundaries, dependencies, data flow, and design tradeoffs. Prefer practical architecture guidance tied to repository evidence. Call out migration risks and long-term maintainability concerns.".to_string(),
|
||||
),
|
||||
temperature: Some(0.2),
|
||||
max_tokens: Some(2000),
|
||||
top_p: Some(1.0),
|
||||
frequency_penalty: Some(0.0),
|
||||
presence_penalty: Some(0.0),
|
||||
max_tool_depth: Some(5),
|
||||
allowed_tools: Some(tools_for_role(&AgentRole::Architect)),
|
||||
disable_orchestration: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn debugger_profile() -> AgentExecutionProfile {
|
||||
AgentExecutionProfile {
|
||||
role: AgentRole::Debugger,
|
||||
system_prompt: Some(
|
||||
"You are the debugger agent. Identify likely root causes, suspect files or commits, missing reproduction details, and the shortest validation path. Separate evidence from hypotheses and rank hypotheses by plausibility.".to_string(),
|
||||
),
|
||||
temperature: Some(0.1),
|
||||
max_tokens: Some(1800),
|
||||
top_p: Some(1.0),
|
||||
frequency_penalty: Some(0.0),
|
||||
presence_penalty: Some(0.0),
|
||||
max_tool_depth: Some(6),
|
||||
allowed_tools: Some(tools_for_role(&AgentRole::Debugger)),
|
||||
disable_orchestration: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn implementer_profile() -> AgentExecutionProfile {
|
||||
AgentExecutionProfile {
|
||||
role: AgentRole::Implementer,
|
||||
system_prompt: Some(
|
||||
"You are the implementer agent. Convert requirements into a concrete execution plan: files to touch, sequencing, integration points, and risks. Keep recommendations actionable and avoid broad rewrites unless justified.".to_string(),
|
||||
),
|
||||
temperature: Some(0.15),
|
||||
max_tokens: Some(1800),
|
||||
top_p: Some(1.0),
|
||||
frequency_penalty: Some(0.0),
|
||||
presence_penalty: Some(0.0),
|
||||
max_tool_depth: Some(5),
|
||||
allowed_tools: Some(tools_for_role(&AgentRole::Implementer)),
|
||||
disable_orchestration: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tester_profile() -> AgentExecutionProfile {
|
||||
AgentExecutionProfile {
|
||||
role: AgentRole::Tester,
|
||||
system_prompt: Some(
|
||||
"You are the tester agent. Design high-signal validation: unit, integration, regression, and edge-case coverage. Identify what must be tested, what can be skipped, and the fastest commands or checks to build confidence.".to_string(),
|
||||
),
|
||||
temperature: Some(0.1),
|
||||
max_tokens: Some(1600),
|
||||
top_p: Some(1.0),
|
||||
frequency_penalty: Some(0.0),
|
||||
presence_penalty: Some(0.0),
|
||||
max_tool_depth: Some(4),
|
||||
allowed_tools: Some(tools_for_role(&AgentRole::Tester)),
|
||||
disable_orchestration: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn security_profile() -> AgentExecutionProfile {
|
||||
AgentExecutionProfile {
|
||||
role: AgentRole::Security,
|
||||
system_prompt: Some(
|
||||
"You are the security agent. Review authentication, authorization, data exposure, injection, dependency, secret-handling, and abuse-case risks. Prioritize exploitable issues and concrete mitigations over generic advice.".to_string(),
|
||||
),
|
||||
temperature: Some(0.1),
|
||||
max_tokens: Some(1800),
|
||||
top_p: Some(1.0),
|
||||
frequency_penalty: Some(0.0),
|
||||
presence_penalty: Some(0.0),
|
||||
max_tool_depth: Some(5),
|
||||
allowed_tools: Some(tools_for_role(&AgentRole::Security)),
|
||||
disable_orchestration: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether to enable multi-agent delegation for this request.
|
||||
/// Simplified from keyword-based gating: delegation is enabled when tools are available.
|
||||
pub fn should_enable_delegation(_input: &str, tools_available: bool) -> bool {
|
||||
tools_available
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::agent_profile::{analyst_profile, researcher_profile, reviewer_profile};
|
||||
use super::agent_profile::profile_for_role_name;
|
||||
use crate::client::AiClientConfig;
|
||||
use crate::client::types::{ChatRequestMessage, ToolCall};
|
||||
use crate::client::{StreamChunk, StreamChunkType, StreamedToolCall, call_stream};
|
||||
@ -629,13 +629,13 @@ pub async fn execute_chat_stream(
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "call_sub_agent",
|
||||
"description": "Delegate a task to a specialist sub-agent and receive its output.\nAvailable roles:\n- researcher: Gathers facts, evidence, and data. Best for finding information and searching code.\n- analyst: Builds explanations, highlights causal links and tradeoffs. Best for reasoning about implications.\n- reviewer: Stress-tests proposals, identifies risks and contradictions. Best for quality checks.\nProvide a clear, focused task description so the sub-agent knows exactly what to investigate.",
|
||||
"description": "Delegate a task to a specialist sub-agent and receive its output.\nAvailable roles:\n- researcher: Gathers facts, evidence, and data. Best for finding information and searching code.\n- analyst: Builds explanations, highlights causal links and tradeoffs. Best for reasoning about implications.\n- reviewer: Stress-tests proposals, identifies risks and contradictions. Best for quality checks.\n- architect: Maps systems, dependencies, boundaries, and design tradeoffs. Best for architecture decisions.\n- debugger: Finds root causes, suspect changes, and validation paths. Best for bugs and regressions.\n- implementer: Converts requirements into concrete implementation steps. Best for execution planning.\n- tester: Designs validation and regression coverage. Best for test strategy.\n- security: Reviews auth, data exposure, injection, dependency, and abuse risks. Best for sensitive changes.\nProvide a clear, focused task description so the sub-agent knows exactly what to investigate.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"role": {
|
||||
"type": "string",
|
||||
"description": "The sub-agent role to delegate to: researcher, analyst, or reviewer."
|
||||
"description": "The sub-agent role to delegate to: researcher, analyst, reviewer, architect, debugger, implementer, tester, or security."
|
||||
},
|
||||
"task": {
|
||||
"type": "string",
|
||||
@ -805,11 +805,7 @@ pub async fn execute_chat_stream(
|
||||
.unwrap_or("researcher");
|
||||
let task = args.get("task").and_then(|v| v.as_str()).unwrap_or("");
|
||||
|
||||
let profile = match role {
|
||||
"analyst" => analyst_profile(),
|
||||
"reviewer" => reviewer_profile(),
|
||||
_ => researcher_profile(),
|
||||
};
|
||||
let profile = profile_for_role_name(role);
|
||||
|
||||
// Generate children_id BEFORE starting sub-agent execution
|
||||
let sub_agent_id = format!("sub-agent-{}", Uuid::now_v7());
|
||||
|
||||
@ -87,6 +87,11 @@ pub enum AgentRole {
|
||||
Researcher,
|
||||
Analyst,
|
||||
Reviewer,
|
||||
Architect,
|
||||
Debugger,
|
||||
Implementer,
|
||||
Tester,
|
||||
Security,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
|
||||
@ -1,9 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::agent_profile::{
|
||||
analyst_profile, researcher_profile, reviewer_profile, should_enable_delegation,
|
||||
supervisor_profile,
|
||||
};
|
||||
use super::agent_profile::{profile_for_role_name, should_enable_delegation, supervisor_profile};
|
||||
use super::message_builder::MessageBuilder;
|
||||
use super::nonstreaming_execution::execute_process;
|
||||
use super::service::{ProcessResult, StreamResult};
|
||||
@ -67,9 +64,8 @@ pub async fn execute_orchestrated_process(
|
||||
supervisor_request.frequency_penalty = profile
|
||||
.frequency_penalty
|
||||
.unwrap_or(request.frequency_penalty);
|
||||
supervisor_request.presence_penalty = profile
|
||||
.presence_penalty
|
||||
.unwrap_or(request.presence_penalty);
|
||||
supervisor_request.presence_penalty =
|
||||
profile.presence_penalty.unwrap_or(request.presence_penalty);
|
||||
|
||||
execute_process(
|
||||
supervisor_request,
|
||||
@ -138,9 +134,8 @@ pub async fn execute_orchestrated_stream(
|
||||
supervisor_request.frequency_penalty = profile
|
||||
.frequency_penalty
|
||||
.unwrap_or(request.frequency_penalty);
|
||||
supervisor_request.presence_penalty = profile
|
||||
.presence_penalty
|
||||
.unwrap_or(request.presence_penalty);
|
||||
supervisor_request.presence_penalty =
|
||||
profile.presence_penalty.unwrap_or(request.presence_penalty);
|
||||
|
||||
super::streaming_execution::execute_process_stream(
|
||||
supervisor_request,
|
||||
@ -175,6 +170,11 @@ fn register_call_sub_agent_tool(
|
||||
- researcher: Gathers facts, evidence, and data. Best for finding information and searching code.\n\
|
||||
- analyst: Builds explanations, highlights causal links and tradeoffs. Best for reasoning about implications.\n\
|
||||
- reviewer: Stress-tests proposals, identifies risks and contradictions. Best for quality checks.\n\
|
||||
- architect: Maps systems, dependencies, boundaries, and design tradeoffs. Best for architecture decisions.\n\
|
||||
- debugger: Finds root causes, suspect changes, and validation paths. Best for bugs and regressions.\n\
|
||||
- implementer: Converts requirements into concrete implementation steps. Best for execution planning.\n\
|
||||
- tester: Designs validation and regression coverage. Best for test strategy.\n\
|
||||
- security: Reviews auth, data exposure, injection, dependency, and abuse risks. Best for sensitive changes.\n\
|
||||
Provide a clear, focused task description so the sub-agent knows exactly what to investigate.",
|
||||
)
|
||||
.parameters(ToolSchema {
|
||||
@ -187,7 +187,7 @@ fn register_call_sub_agent_tool(
|
||||
name: "role".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some(
|
||||
"The sub-agent role to delegate to: researcher, analyst, or reviewer.".into(),
|
||||
"The sub-agent role to delegate to: researcher, analyst, reviewer, architect, debugger, implementer, tester, or security.".into(),
|
||||
),
|
||||
required: true,
|
||||
properties: None,
|
||||
@ -224,12 +224,7 @@ fn register_call_sub_agent_tool(
|
||||
.unwrap_or("")
|
||||
.to_owned();
|
||||
|
||||
let profile = match role.as_str() {
|
||||
"researcher" => researcher_profile(),
|
||||
"analyst" => analyst_profile(),
|
||||
"reviewer" => reviewer_profile(),
|
||||
_ => researcher_profile(),
|
||||
};
|
||||
let profile = profile_for_role_name(role.as_str());
|
||||
|
||||
let mut sub_request = captured_request.clone();
|
||||
sub_request.input = format!(
|
||||
@ -288,9 +283,12 @@ fn filter_tools_for_sub_agent(
|
||||
let Some(tools) = original_tools else {
|
||||
return Vec::new();
|
||||
};
|
||||
let allowed = allowed_tools
|
||||
.as_ref()
|
||||
.map(|list| list.iter().filter(|n| *n != "call_sub_agent").cloned().collect::<Vec<String>>());
|
||||
let allowed = allowed_tools.as_ref().map(|list| {
|
||||
list.iter()
|
||||
.filter(|n| *n != "call_sub_agent")
|
||||
.cloned()
|
||||
.collect::<Vec<String>>()
|
||||
});
|
||||
|
||||
match allowed {
|
||||
Some(allowed_list) if !allowed_list.is_empty() => tools
|
||||
@ -308,8 +306,7 @@ fn filter_tools_for_sub_agent(
|
||||
_ => tools
|
||||
.iter()
|
||||
.filter(|tool| {
|
||||
tool
|
||||
.get("function")
|
||||
tool.get("function")
|
||||
.and_then(|f| f.get("name"))
|
||||
.and_then(|v| v.as_str())
|
||||
.is_some_and(|name| name != "call_sub_agent")
|
||||
@ -317,4 +314,4 @@ fn filter_tools_for_sub_agent(
|
||||
.cloned()
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -165,10 +165,7 @@ impl ChatService {
|
||||
request: AiRequest,
|
||||
room_tools: ToolRegistry,
|
||||
) -> Result<ProcessResult> {
|
||||
let mut merged = self
|
||||
.tool_registry
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
let mut merged = self.tool_registry.clone().unwrap_or_default();
|
||||
merged.merge(room_tools);
|
||||
|
||||
super::nonstreaming_execution::execute_process(
|
||||
@ -191,10 +188,7 @@ impl ChatService {
|
||||
on_chunk: StreamCallback,
|
||||
room_tools: ToolRegistry,
|
||||
) -> Result<StreamResult> {
|
||||
let mut merged = self
|
||||
.tool_registry
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
let mut merged = self.tool_registry.clone().unwrap_or_default();
|
||||
merged.merge(room_tools);
|
||||
|
||||
super::streaming_execution::execute_process_stream(
|
||||
|
||||
@ -198,9 +198,7 @@ impl super::CompactService {
|
||||
.unwrap_or_else(|_| estimate_input.len() / 4);
|
||||
|
||||
let retain_count = Self::resolve_retain_count(config, estimated_tokens);
|
||||
if estimated_tokens >= config.token_threshold
|
||||
&& messages.len() > retain_count
|
||||
{
|
||||
if estimated_tokens >= config.token_threshold && messages.len() > retain_count {
|
||||
let split_index = messages.len().saturating_sub(retain_count);
|
||||
let (to_summarize, retained_messages) = messages.split_at(split_index);
|
||||
let from_seq = to_summarize
|
||||
|
||||
@ -2,11 +2,11 @@ use models::rooms::room_message::Model as RoomMessageModel;
|
||||
use models::users::user::{Column as UserCol, Entity as User};
|
||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
||||
|
||||
use crate::AgentError;
|
||||
use crate::client::call_with_params;
|
||||
use crate::client::types::ChatRequestMessage;
|
||||
use crate::compact::types::{CompactConfig, MessageSummary};
|
||||
use crate::tokent::{TokenUsage, count_message_text};
|
||||
use crate::AgentError;
|
||||
|
||||
const DEFAULT_MODEL_CONTEXT_LIMIT: usize = 128_000;
|
||||
const MODEL_INPUT_RATIO_NUMERATOR: usize = 85;
|
||||
@ -193,7 +193,10 @@ impl super::CompactService {
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok((final_summary, if has_usage { Some(total_usage) } else { None }))
|
||||
Ok((
|
||||
final_summary,
|
||||
if has_usage { Some(total_usage) } else { None },
|
||||
))
|
||||
}
|
||||
|
||||
async fn merge_summary_rounds(
|
||||
@ -230,11 +233,7 @@ impl super::CompactService {
|
||||
for pair_text in fitted_pairs {
|
||||
let prompt = self.build_prompt(kind, true, &pair_text, current_budget);
|
||||
let (summary, usage) = self
|
||||
.invoke_summary_prompt(
|
||||
&prompt,
|
||||
current_budget,
|
||||
Self::temperature_for(kind),
|
||||
)
|
||||
.invoke_summary_prompt(&prompt, current_budget, Self::temperature_for(kind))
|
||||
.await?;
|
||||
Self::accumulate_usage(total_usage, has_usage, usage);
|
||||
next_round.push(summary);
|
||||
@ -474,18 +473,16 @@ impl super::CompactService {
|
||||
}
|
||||
|
||||
fn safe_model_input_budget_from_limit(model_context_limit: Option<usize>) -> usize {
|
||||
let context_limit = model_context_limit.unwrap_or(DEFAULT_MODEL_CONTEXT_LIMIT).max(1);
|
||||
let context_limit = model_context_limit
|
||||
.unwrap_or(DEFAULT_MODEL_CONTEXT_LIMIT)
|
||||
.max(1);
|
||||
context_limit
|
||||
.saturating_mul(MODEL_INPUT_RATIO_NUMERATOR)
|
||||
.saturating_div(MODEL_INPUT_RATIO_DENOMINATOR)
|
||||
.max(1)
|
||||
}
|
||||
|
||||
fn accumulate_usage(
|
||||
total: &mut TokenUsage,
|
||||
has_usage: &mut bool,
|
||||
usage: Option<TokenUsage>,
|
||||
) {
|
||||
fn accumulate_usage(total: &mut TokenUsage, has_usage: &mut bool, usage: Option<TokenUsage>) {
|
||||
if let Some(usage) = usage {
|
||||
total.input_tokens += usage.input_tokens;
|
||||
total.output_tokens += usage.output_tokens;
|
||||
@ -500,7 +497,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn room_summary_uses_eighty_five_percent_input_budget() {
|
||||
assert_eq!(CompactService::safe_model_input_budget_from_limit(Some(1000)), 850);
|
||||
assert_eq!(
|
||||
CompactService::safe_model_input_budget_from_limit(Some(1000)),
|
||||
850
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -19,8 +19,8 @@ pub use billing::{
|
||||
initialize_user_billing, persist_billing_error, record_ai_usage, record_user_ai_usage,
|
||||
};
|
||||
pub use chat::{
|
||||
AgentExecutionProfile, AgentRole, AiContextSenderType, AiRequest, AiStreamChunk,
|
||||
ChatService, Mention, RoomMessageContext, StreamCallback,
|
||||
AgentExecutionProfile, AgentRole, AiContextSenderType, AiRequest, AiStreamChunk, ChatService,
|
||||
Mention, RoomMessageContext, StreamCallback,
|
||||
};
|
||||
pub use client::types::ChatRequestMessage;
|
||||
pub use client::{AiCallResponse, AiClientConfig, call_with_params, call_with_retry};
|
||||
|
||||
@ -177,12 +177,7 @@ impl ToolContext {
|
||||
return Err(existing);
|
||||
}
|
||||
|
||||
match current.compare_exchange(
|
||||
existing,
|
||||
next,
|
||||
Ordering::AcqRel,
|
||||
Ordering::Relaxed,
|
||||
) {
|
||||
match current.compare_exchange(existing, next, Ordering::AcqRel, Ordering::Relaxed) {
|
||||
Ok(_) => return Ok(()),
|
||||
Err(_) => continue,
|
||||
}
|
||||
|
||||
262
libs/fctool/src/git_tools/lfs.rs
Normal file
262
libs/fctool/src/git_tools/lfs.rs
Normal file
@ -0,0 +1,262 @@
|
||||
//! Git LFS query tools.
|
||||
|
||||
use super::ctx::GitToolCtx;
|
||||
use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema};
|
||||
use git::lfs::types::LfsOid;
|
||||
use std::collections::HashMap;
|
||||
|
||||
async fn git_lfs_summary_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p = parse_args(args)?;
|
||||
let project_name = required_str(&p, "project_name")?;
|
||||
let repo_name = required_str(&p, "repo_name")?;
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let objects = domain.lfs_object_list().map_err(|e| e.to_string())?;
|
||||
let cache_size = domain.lfs_cache_size().map_err(|e| e.to_string())?;
|
||||
let attributes = domain.lfs_gitattributes_list().map_err(|e| e.to_string())?;
|
||||
let config = domain.lfs_config().map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"object_count": objects.len(),
|
||||
"cache_size_bytes": cache_size,
|
||||
"objects": objects.into_iter().map(|oid| oid.to_string()).collect::<Vec<_>>(),
|
||||
"gitattributes": attributes,
|
||||
"config": {
|
||||
"endpoint": config.endpoint,
|
||||
"has_access_token": config.access_token.as_ref().is_some_and(|s| !s.is_empty()),
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
async fn git_lfs_scan_tree_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p = parse_args(args)?;
|
||||
let project_name = required_str(&p, "project_name")?;
|
||||
let repo_name = required_str(&p, "repo_name")?;
|
||||
let rev = p.get("rev").and_then(|v| v.as_str()).unwrap_or("HEAD");
|
||||
let recursive = p.get("recursive").and_then(|v| v.as_bool()).unwrap_or(true);
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let commit_oid = resolve_rev(&domain, rev)?;
|
||||
let commit = domain.commit_get(&commit_oid).map_err(|e| e.to_string())?;
|
||||
let entries = domain
|
||||
.lfs_scan_tree(&commit.tree_id, recursive)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"rev": rev,
|
||||
"commit_oid": commit_oid.to_string(),
|
||||
"count": entries.len(),
|
||||
"entries": entries.into_iter().map(|entry| {
|
||||
serde_json::json!({
|
||||
"path": entry.path,
|
||||
"oid": entry.pointer.oid.to_string(),
|
||||
"size": entry.pointer.size,
|
||||
"cached": domain.lfs_object_cached(&entry.pointer.oid),
|
||||
"extra": entry.pointer.extra,
|
||||
})
|
||||
}).collect::<Vec<_>>(),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn git_lfs_pointer_info_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p = parse_args(args)?;
|
||||
let project_name = required_str(&p, "project_name")?;
|
||||
let repo_name = required_str(&p, "repo_name")?;
|
||||
let rev = p.get("rev").and_then(|v| v.as_str()).unwrap_or("HEAD");
|
||||
let blob_oid = p.get("blob_oid").and_then(|v| v.as_str());
|
||||
let path = p.get("path").and_then(|v| v.as_str());
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let blob_oid = if let Some(blob_oid) = blob_oid {
|
||||
git::commit::types::CommitOid::new(blob_oid)
|
||||
} else if let Some(path) = path {
|
||||
let commit_oid = resolve_rev(&domain, rev)?;
|
||||
domain
|
||||
.tree_entry_by_path_from_commit(&commit_oid, path)
|
||||
.map_err(|e| e.to_string())?
|
||||
.oid
|
||||
} else {
|
||||
return Err("either blob_oid or path is required".into());
|
||||
};
|
||||
|
||||
let pointer = domain
|
||||
.lfs_pointer_from_blob(&blob_oid)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(match pointer {
|
||||
Some(pointer) => serde_json::json!({
|
||||
"is_lfs_pointer": true,
|
||||
"blob_oid": blob_oid.to_string(),
|
||||
"pointer": {
|
||||
"version": pointer.version,
|
||||
"oid": pointer.oid.to_string(),
|
||||
"size": pointer.size,
|
||||
"cached": domain.lfs_object_cached(&pointer.oid),
|
||||
"object_path": domain.lfs_object_path(&pointer.oid).ok().map(|p| p.to_string_lossy().to_string()),
|
||||
"extra": pointer.extra,
|
||||
}
|
||||
}),
|
||||
None => serde_json::json!({
|
||||
"is_lfs_pointer": false,
|
||||
"blob_oid": blob_oid.to_string(),
|
||||
"pointer": null,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
async fn git_lfs_object_info_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p = parse_args(args)?;
|
||||
let project_name = required_str(&p, "project_name")?;
|
||||
let repo_name = required_str(&p, "repo_name")?;
|
||||
let oid = LfsOid::new(required_str(&p, "oid")?);
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
if !oid.is_valid() {
|
||||
return Err(format!("invalid LFS oid: {}", oid));
|
||||
}
|
||||
let path = domain.lfs_object_path(&oid).map_err(|e| e.to_string())?;
|
||||
let cached = domain.lfs_object_cached(&oid);
|
||||
let size = if cached {
|
||||
std::fs::metadata(&path).ok().map(|m| m.len())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"oid": oid.to_string(),
|
||||
"cached": cached,
|
||||
"path": path.to_string_lossy().to_string(),
|
||||
"size_bytes": size,
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_rev(
|
||||
domain: &git::GitDomain,
|
||||
rev: &str,
|
||||
) -> Result<git::commit::types::CommitOid, String> {
|
||||
if rev.len() == 40 && rev.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
return Ok(git::commit::types::CommitOid::new(rev));
|
||||
}
|
||||
if let Ok(Some(oid)) = domain.ref_target(rev) {
|
||||
return Ok(oid);
|
||||
}
|
||||
domain
|
||||
.commit_get_prefix(rev)
|
||||
.map(|m| m.oid)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub fn register_git_tools(registry: &mut ToolRegistry) {
|
||||
registry.register(
|
||||
ToolDefinition::new("git_lfs_summary")
|
||||
.description("Summarize Git LFS state for a repository: local LFS objects, cache size, .gitattributes LFS patterns, and sanitized LFS config.")
|
||||
.parameters(base_schema(vec![])),
|
||||
handler(git_lfs_summary_exec),
|
||||
);
|
||||
|
||||
registry.register(
|
||||
ToolDefinition::new("git_lfs_scan_tree")
|
||||
.description("Scan a revision tree for Git LFS pointer files. Returns path, LFS object OID, declared size, and local cache status.")
|
||||
.parameters(base_schema(vec![
|
||||
param("rev", "string", "Revision to scan. Default HEAD.", false),
|
||||
param("recursive", "boolean", "Scan recursively. Default true.", false),
|
||||
])),
|
||||
handler(git_lfs_scan_tree_exec),
|
||||
);
|
||||
|
||||
registry.register(
|
||||
ToolDefinition::new("git_lfs_pointer_info")
|
||||
.description("Inspect whether a blob or file path is a Git LFS pointer. Provide either blob_oid or path; path is resolved at rev, default HEAD.")
|
||||
.parameters(base_schema(vec![
|
||||
param("blob_oid", "string", "Blob object ID to inspect.", false),
|
||||
param("path", "string", "File path to inspect at rev.", false),
|
||||
param("rev", "string", "Revision used when path is provided. Default HEAD.", false),
|
||||
])),
|
||||
handler(git_lfs_pointer_info_exec),
|
||||
);
|
||||
|
||||
registry.register(
|
||||
ToolDefinition::new("git_lfs_object_info")
|
||||
.description("Inspect one local Git LFS object by SHA-256 OID and report cache path and size if present.")
|
||||
.parameters(base_schema(vec![param(
|
||||
"oid",
|
||||
"string",
|
||||
"64-character Git LFS object SHA-256 OID.",
|
||||
true,
|
||||
)])),
|
||||
handler(git_lfs_object_info_exec),
|
||||
);
|
||||
}
|
||||
|
||||
fn handler<F, Fut>(f: F) -> ToolHandler
|
||||
where
|
||||
F: Fn(GitToolCtx, serde_json::Value) -> Fut + Send + Sync + 'static,
|
||||
Fut: std::future::Future<Output = Result<serde_json::Value, String>> + Send + 'static,
|
||||
{
|
||||
ToolHandler::new(move |ctx, args| {
|
||||
let gctx = super::ctx::GitToolCtx::new(ctx);
|
||||
let fut = f(gctx, args);
|
||||
Box::pin(async move { fut.await.map_err(agent::ToolError::ExecutionError) })
|
||||
})
|
||||
}
|
||||
|
||||
fn base_schema(extra: Vec<(String, ToolParam)>) -> ToolSchema {
|
||||
let mut properties = HashMap::from([
|
||||
param("project_name", "string", "Project name (slug)", true),
|
||||
param("repo_name", "string", "Repository name", true),
|
||||
]);
|
||||
let mut required = vec!["project_name".into(), "repo_name".into()];
|
||||
required.extend(
|
||||
extra
|
||||
.iter()
|
||||
.filter(|(_, param)| param.required)
|
||||
.map(|(name, _)| name.clone()),
|
||||
);
|
||||
properties.extend(extra);
|
||||
ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(properties),
|
||||
required: Some(required),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_args(
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Map<String, serde_json::Value>, String> {
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn required_str<'a>(
|
||||
p: &'a serde_json::Map<String, serde_json::Value>,
|
||||
name: &str,
|
||||
) -> Result<&'a str, String> {
|
||||
p.get(name)
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| format!("missing {}", name))
|
||||
}
|
||||
|
||||
fn param(name: &str, param_type: &str, description: &str, required: bool) -> (String, ToolParam) {
|
||||
(
|
||||
name.into(),
|
||||
ToolParam {
|
||||
name: name.into(),
|
||||
param_type: param_type.into(),
|
||||
description: Some(description.into()),
|
||||
required,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
)
|
||||
}
|
||||
109
libs/fctool/src/git_tools/merge.rs
Normal file
109
libs/fctool/src/git_tools/merge.rs
Normal file
@ -0,0 +1,109 @@
|
||||
//! Git merge analysis tools.
|
||||
|
||||
use super::ctx::GitToolCtx;
|
||||
use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema};
|
||||
use std::collections::HashMap;
|
||||
|
||||
async fn git_merge_analysis_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = required_str(&p, "project_name")?;
|
||||
let repo_name = required_str(&p, "repo_name")?;
|
||||
let target = required_str(&p, "target")?;
|
||||
let base_ref = p.get("base_ref").and_then(|v| v.as_str()).unwrap_or("HEAD");
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let target_oid = resolve_rev(&domain, target)?;
|
||||
let (analysis, preference) = if base_ref == "HEAD" {
|
||||
domain
|
||||
.merge_analysis(&target_oid)
|
||||
.map_err(|e| e.to_string())?
|
||||
} else {
|
||||
domain
|
||||
.merge_analysis_for_ref(base_ref, &target_oid)
|
||||
.map_err(|e| e.to_string())?
|
||||
};
|
||||
|
||||
let base_oid = domain.ref_target(base_ref).ok().flatten();
|
||||
let merge_base = base_oid
|
||||
.as_ref()
|
||||
.and_then(|base| domain.merge_base(base, &target_oid).ok())
|
||||
.map(|oid| oid.to_string());
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"base_ref": base_ref,
|
||||
"base_oid": base_oid.map(|oid| oid.to_string()),
|
||||
"target": target,
|
||||
"target_oid": target_oid.to_string(),
|
||||
"merge_base": merge_base,
|
||||
"analysis": analysis,
|
||||
"preference": preference,
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_rev(
|
||||
domain: &git::GitDomain,
|
||||
rev: &str,
|
||||
) -> Result<git::commit::types::CommitOid, String> {
|
||||
if rev.len() == 40 && rev.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
return Ok(git::commit::types::CommitOid::new(rev));
|
||||
}
|
||||
if let Ok(Some(oid)) = domain.ref_target(rev) {
|
||||
return Ok(oid);
|
||||
}
|
||||
domain
|
||||
.commit_get_prefix(rev)
|
||||
.map(|m| m.oid)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub fn register_git_tools(registry: &mut ToolRegistry) {
|
||||
registry.register(
|
||||
ToolDefinition::new("git_merge_analysis")
|
||||
.description("Analyze whether a target commit/ref can be merged into a base ref. Returns up-to-date, fast-forward, normal merge, unborn, merge preference, and merge-base information. This is read-only.")
|
||||
.parameters(ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(HashMap::from([
|
||||
param("project_name", "string", "Project name (slug)", true),
|
||||
param("repo_name", "string", "Repository name", true),
|
||||
param("target", "string", "Target revision/ref to merge, e.g. refs/heads/feature, feature, or a commit SHA.", true),
|
||||
param("base_ref", "string", "Base ref to merge into. Default HEAD.", false),
|
||||
])),
|
||||
required: Some(vec!["project_name".into(), "repo_name".into(), "target".into()]),
|
||||
}),
|
||||
ToolHandler::new(|ctx, args| {
|
||||
let gctx = super::ctx::GitToolCtx::new(ctx);
|
||||
Box::pin(async move {
|
||||
git_merge_analysis_exec(gctx, args)
|
||||
.await
|
||||
.map_err(agent::ToolError::ExecutionError)
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
fn required_str<'a>(
|
||||
p: &'a serde_json::Map<String, serde_json::Value>,
|
||||
name: &str,
|
||||
) -> Result<&'a str, String> {
|
||||
p.get(name)
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| format!("missing {}", name))
|
||||
}
|
||||
|
||||
fn param(name: &str, param_type: &str, description: &str, required: bool) -> (String, ToolParam) {
|
||||
(
|
||||
name.into(),
|
||||
ToolParam {
|
||||
name: name.into(),
|
||||
param_type: param_type.into(),
|
||||
description: Some(description.into()),
|
||||
required,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -9,8 +9,12 @@ pub mod commit;
|
||||
pub mod ctx;
|
||||
pub mod diff;
|
||||
pub mod kb;
|
||||
pub mod lfs;
|
||||
pub mod merge;
|
||||
pub mod reference;
|
||||
pub mod repo_analysis;
|
||||
pub mod repo_util;
|
||||
pub mod status;
|
||||
pub mod tag;
|
||||
pub mod tree;
|
||||
pub mod types;
|
||||
@ -18,11 +22,15 @@ pub mod types;
|
||||
/// Batch-register all git tools into a ToolRegistry.
|
||||
pub fn register_all(registry: &mut agent::ToolRegistry) {
|
||||
commit::register_git_tools(registry);
|
||||
status::register_git_tools(registry);
|
||||
branch::register_git_tools(registry);
|
||||
reference::register_git_tools(registry);
|
||||
merge::register_git_tools(registry);
|
||||
diff::register_git_tools(registry);
|
||||
blob::register_git_tools(registry);
|
||||
tree::register_git_tools(registry);
|
||||
tag::register_git_tools(registry);
|
||||
lfs::register_git_tools(registry);
|
||||
repo_analysis::register_git_tools(registry);
|
||||
kb::register_git_tools(registry);
|
||||
repo_util::register_git_tools(registry);
|
||||
|
||||
137
libs/fctool/src/git_tools/reference.rs
Normal file
137
libs/fctool/src/git_tools/reference.rs
Normal file
@ -0,0 +1,137 @@
|
||||
//! Git reference query tools.
|
||||
|
||||
use super::ctx::GitToolCtx;
|
||||
use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema};
|
||||
use std::collections::HashMap;
|
||||
|
||||
async fn git_ref_list_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p = parse_args(args)?;
|
||||
let project_name = required_str(&p, "project_name")?;
|
||||
let repo_name = required_str(&p, "repo_name")?;
|
||||
let pattern = p.get("pattern").and_then(|v| v.as_str());
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let refs = domain.ref_list(pattern).map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"count": refs.len(),
|
||||
"refs": refs.into_iter().map(ref_json).collect::<Vec<_>>(),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn git_ref_info_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p = parse_args(args)?;
|
||||
let project_name = required_str(&p, "project_name")?;
|
||||
let repo_name = required_str(&p, "repo_name")?;
|
||||
let name = required_str(&p, "name")?;
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let info = domain.ref_get(name).map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(ref_json(info))
|
||||
}
|
||||
|
||||
fn ref_json(info: git::reference::types::RefInfo) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"name": info.name,
|
||||
"oid": info.oid.map(|oid| oid.to_string()),
|
||||
"target": info.target.map(|oid| oid.to_string()),
|
||||
"is_symbolic": info.is_symbolic,
|
||||
"is_branch": info.is_branch,
|
||||
"is_remote": info.is_remote,
|
||||
"is_tag": info.is_tag,
|
||||
"is_note": info.is_note,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn register_git_tools(registry: &mut ToolRegistry) {
|
||||
registry.register(
|
||||
ToolDefinition::new("git_ref_list")
|
||||
.description("List Git references such as branches, remote-tracking refs, tags, and notes. Optional pattern supports exact names plus refs/heads/* or refs/tags/** style prefix matching.")
|
||||
.parameters(ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(HashMap::from([
|
||||
param("project_name", "string", "Project name (slug)", true),
|
||||
param("repo_name", "string", "Repository name", true),
|
||||
param("pattern", "string", "Optional ref pattern, e.g. refs/heads/*, refs/tags/**, or refs/heads/main.", false),
|
||||
])),
|
||||
required: Some(vec!["project_name".into(), "repo_name".into()]),
|
||||
}),
|
||||
ToolHandler::new(|ctx, args| {
|
||||
let gctx = super::ctx::GitToolCtx::new(ctx);
|
||||
Box::pin(async move {
|
||||
git_ref_list_exec(gctx, args)
|
||||
.await
|
||||
.map_err(agent::ToolError::ExecutionError)
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
registry.register(
|
||||
ToolDefinition::new("git_ref_info")
|
||||
.description(
|
||||
"Get one Git reference by full name, including peeled commit OID and target OID.",
|
||||
)
|
||||
.parameters(ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(HashMap::from([
|
||||
param("project_name", "string", "Project name (slug)", true),
|
||||
param("repo_name", "string", "Repository name", true),
|
||||
param(
|
||||
"name",
|
||||
"string",
|
||||
"Full ref name, e.g. refs/heads/main or refs/tags/v1.0.0.",
|
||||
true,
|
||||
),
|
||||
])),
|
||||
required: Some(vec![
|
||||
"project_name".into(),
|
||||
"repo_name".into(),
|
||||
"name".into(),
|
||||
]),
|
||||
}),
|
||||
ToolHandler::new(|ctx, args| {
|
||||
let gctx = super::ctx::GitToolCtx::new(ctx);
|
||||
Box::pin(async move {
|
||||
git_ref_info_exec(gctx, args)
|
||||
.await
|
||||
.map_err(agent::ToolError::ExecutionError)
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
fn parse_args(
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Map<String, serde_json::Value>, String> {
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn required_str<'a>(
|
||||
p: &'a serde_json::Map<String, serde_json::Value>,
|
||||
name: &str,
|
||||
) -> Result<&'a str, String> {
|
||||
p.get(name)
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| format!("missing {}", name))
|
||||
}
|
||||
|
||||
fn param(name: &str, param_type: &str, description: &str, required: bool) -> (String, ToolParam) {
|
||||
(
|
||||
name.into(),
|
||||
ToolParam {
|
||||
name: name.into(),
|
||||
param_type: param_type.into(),
|
||||
description: Some(description.into()),
|
||||
required,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -27,6 +27,23 @@ const DEPENDENCY_MANIFESTS: &[(&str, &str)] = &[
|
||||
("Makefile", "make"),
|
||||
];
|
||||
|
||||
const TEST_FILE_MARKERS: &[&str] = &[
|
||||
"_test.", ".test.", ".spec.", "_spec.", "test.", "tests.", "test_", ".feature",
|
||||
];
|
||||
|
||||
const TEST_DIR_MARKERS: &[&str] = &[
|
||||
"test",
|
||||
"tests",
|
||||
"__tests__",
|
||||
"spec",
|
||||
"specs",
|
||||
"e2e",
|
||||
"integration",
|
||||
"unit",
|
||||
"cypress",
|
||||
"playwright",
|
||||
];
|
||||
|
||||
/// Language detection by file extension (lowercase).
|
||||
fn ext_to_language(ext: &str) -> Option<&'static str> {
|
||||
match ext {
|
||||
@ -648,6 +665,271 @@ async fn repo_dependencies_exec(
|
||||
|
||||
// ── Registration ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// Tool: repo_test_discovery - discover likely tests and test commands.
|
||||
async fn repo_test_discovery_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let max_files = p.get("max_files").and_then(|v| v.as_u64()).unwrap_or(200) as usize;
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let tree = head_tree(&domain)?;
|
||||
let repo = domain.repo();
|
||||
let mut test_files = Vec::new();
|
||||
let mut manifests = Vec::new();
|
||||
let mut frameworks: HashMap<String, u64> = HashMap::new();
|
||||
let mut stack: Vec<(git2::Tree<'_>, String)> = vec![(tree, String::new())];
|
||||
|
||||
while let Some((current_tree, prefix)) = stack.pop() {
|
||||
for entry in current_tree.iter() {
|
||||
let name = match entry.name() {
|
||||
Some(n) => n,
|
||||
None => continue,
|
||||
};
|
||||
let entry_path = if prefix.is_empty() {
|
||||
name.to_string()
|
||||
} else {
|
||||
format!("{}/{}", prefix, name)
|
||||
};
|
||||
match entry.kind() {
|
||||
Some(git2::ObjectType::Tree) => {
|
||||
if !is_ignored_dir(name) {
|
||||
if let Ok(subtree) = entry.to_object(repo).and_then(|o| o.peel_to_tree()) {
|
||||
stack.push((subtree, entry_path));
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(git2::ObjectType::Blob) => {
|
||||
let lower_path = entry_path.to_lowercase();
|
||||
let lower_name = name.to_lowercase();
|
||||
if is_test_file(&lower_path, &lower_name) && test_files.len() < max_files {
|
||||
test_files.push(serde_json::json!({
|
||||
"path": entry_path,
|
||||
"kind": test_kind(&lower_path),
|
||||
}));
|
||||
}
|
||||
if is_test_manifest(name) {
|
||||
if let Ok(blob) = entry.to_object(repo).and_then(|o| o.peel_to_blob()) {
|
||||
let content = String::from_utf8_lossy(blob.content());
|
||||
for framework in detect_test_frameworks(name, &content) {
|
||||
*frameworks.entry(framework).or_insert(0) += 1;
|
||||
}
|
||||
manifests.push(serde_json::json!({ "path": entry_path, "name": name }));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut framework_list: Vec<_> = frameworks
|
||||
.into_iter()
|
||||
.map(|(name, evidence_count)| {
|
||||
serde_json::json!({ "name": name, "evidence_count": evidence_count })
|
||||
})
|
||||
.collect();
|
||||
framework_list.sort_by(|a, b| {
|
||||
b["evidence_count"]
|
||||
.as_u64()
|
||||
.unwrap_or(0)
|
||||
.cmp(&a["evidence_count"].as_u64().unwrap_or(0))
|
||||
});
|
||||
|
||||
let commands = infer_test_commands(&framework_list, &manifests);
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"test_file_count_returned": test_files.len(),
|
||||
"test_files": test_files,
|
||||
"test_manifests": manifests,
|
||||
"frameworks": framework_list,
|
||||
"suggested_commands": commands,
|
||||
}))
|
||||
}
|
||||
|
||||
fn is_test_file(lower_path: &str, lower_name: &str) -> bool {
|
||||
TEST_FILE_MARKERS
|
||||
.iter()
|
||||
.any(|marker| lower_name.contains(marker))
|
||||
|| lower_path
|
||||
.split('/')
|
||||
.any(|part| TEST_DIR_MARKERS.iter().any(|marker| part == *marker))
|
||||
}
|
||||
|
||||
fn test_kind(lower_path: &str) -> &'static str {
|
||||
if lower_path.contains("/e2e/")
|
||||
|| lower_path.contains("/cypress/")
|
||||
|| lower_path.contains("/playwright/")
|
||||
{
|
||||
"e2e"
|
||||
} else if lower_path.contains("/integration/") {
|
||||
"integration"
|
||||
} else if lower_path.contains("/unit/") {
|
||||
"unit"
|
||||
} else {
|
||||
"test"
|
||||
}
|
||||
}
|
||||
|
||||
fn is_test_manifest(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"Cargo.toml"
|
||||
| "package.json"
|
||||
| "go.mod"
|
||||
| "pyproject.toml"
|
||||
| "requirements.txt"
|
||||
| "pom.xml"
|
||||
| "build.gradle"
|
||||
| "build.gradle.kts"
|
||||
| "pytest.ini"
|
||||
| "tox.ini"
|
||||
| "vitest.config.ts"
|
||||
| "vitest.config.js"
|
||||
| "jest.config.js"
|
||||
| "jest.config.ts"
|
||||
| "playwright.config.ts"
|
||||
| "playwright.config.js"
|
||||
| "cypress.config.ts"
|
||||
| "cypress.config.js"
|
||||
)
|
||||
}
|
||||
|
||||
fn detect_test_frameworks(name: &str, content: &str) -> Vec<String> {
|
||||
let lower = content.to_lowercase();
|
||||
let mut found = Vec::new();
|
||||
let mut add = |framework: &str| {
|
||||
if !found.iter().any(|v| v == framework) {
|
||||
found.push(framework.to_string());
|
||||
}
|
||||
};
|
||||
|
||||
match name {
|
||||
"Cargo.toml" => {
|
||||
if lower.contains("[dev-dependencies]")
|
||||
|| lower.contains("tokio-test")
|
||||
|| lower.contains("rstest")
|
||||
{
|
||||
add("cargo test");
|
||||
}
|
||||
}
|
||||
"package.json" => {
|
||||
if lower.contains("\"test\"") {
|
||||
add("npm test");
|
||||
}
|
||||
if lower.contains("vitest") {
|
||||
add("vitest");
|
||||
}
|
||||
if lower.contains("jest") {
|
||||
add("jest");
|
||||
}
|
||||
if lower.contains("playwright") {
|
||||
add("playwright");
|
||||
}
|
||||
if lower.contains("cypress") {
|
||||
add("cypress");
|
||||
}
|
||||
}
|
||||
"go.mod" => add("go test"),
|
||||
"pyproject.toml" | "requirements.txt" | "pytest.ini" | "tox.ini" => {
|
||||
if lower.contains("pytest") {
|
||||
add("pytest");
|
||||
}
|
||||
if lower.contains("tox") {
|
||||
add("tox");
|
||||
}
|
||||
}
|
||||
"pom.xml" => add("maven test"),
|
||||
"build.gradle" | "build.gradle.kts" => add("gradle test"),
|
||||
_ => {
|
||||
if name.starts_with("vitest.config") {
|
||||
add("vitest");
|
||||
} else if name.starts_with("jest.config") {
|
||||
add("jest");
|
||||
} else if name.starts_with("playwright.config") {
|
||||
add("playwright");
|
||||
} else if name.starts_with("cypress.config") {
|
||||
add("cypress");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
found
|
||||
}
|
||||
|
||||
fn infer_test_commands(
|
||||
frameworks: &[serde_json::Value],
|
||||
manifests: &[serde_json::Value],
|
||||
) -> Vec<serde_json::Value> {
|
||||
let names: Vec<String> = frameworks
|
||||
.iter()
|
||||
.filter_map(|v| v.get("name").and_then(|n| n.as_str()).map(str::to_string))
|
||||
.collect();
|
||||
let manifest_names: Vec<String> = manifests
|
||||
.iter()
|
||||
.filter_map(|v| v.get("name").and_then(|n| n.as_str()).map(str::to_string))
|
||||
.collect();
|
||||
let mut commands = Vec::new();
|
||||
let mut add = |command: &str, reason: &str| {
|
||||
if !commands
|
||||
.iter()
|
||||
.any(|v: &serde_json::Value| v.get("command").and_then(|c| c.as_str()) == Some(command))
|
||||
{
|
||||
commands.push(serde_json::json!({ "command": command, "reason": reason }));
|
||||
}
|
||||
};
|
||||
|
||||
if names.iter().any(|n| n == "cargo test") || manifest_names.iter().any(|n| n == "Cargo.toml") {
|
||||
add("cargo test", "Rust Cargo manifest detected");
|
||||
}
|
||||
if names.iter().any(|n| n == "npm test") || manifest_names.iter().any(|n| n == "package.json") {
|
||||
add("npm test", "Node package.json detected");
|
||||
}
|
||||
if names.iter().any(|n| n == "vitest") {
|
||||
add("npx vitest run", "Vitest detected");
|
||||
}
|
||||
if names.iter().any(|n| n == "jest") {
|
||||
add("npx jest", "Jest detected");
|
||||
}
|
||||
if names.iter().any(|n| n == "playwright") {
|
||||
add("npx playwright test", "Playwright detected");
|
||||
}
|
||||
if names.iter().any(|n| n == "cypress") {
|
||||
add("npx cypress run", "Cypress detected");
|
||||
}
|
||||
if names.iter().any(|n| n == "go test") || manifest_names.iter().any(|n| n == "go.mod") {
|
||||
add("go test ./...", "Go module detected");
|
||||
}
|
||||
if names.iter().any(|n| n == "pytest") {
|
||||
add("pytest", "Pytest detected");
|
||||
}
|
||||
if names.iter().any(|n| n == "tox") {
|
||||
add("tox", "Tox detected");
|
||||
}
|
||||
if names.iter().any(|n| n == "maven test") || manifest_names.iter().any(|n| n == "pom.xml") {
|
||||
add("mvn test", "Maven project detected");
|
||||
}
|
||||
if names.iter().any(|n| n == "gradle test")
|
||||
|| manifest_names
|
||||
.iter()
|
||||
.any(|n| n == "build.gradle" || n == "build.gradle.kts")
|
||||
{
|
||||
add("./gradlew test", "Gradle project detected");
|
||||
}
|
||||
|
||||
commands
|
||||
}
|
||||
|
||||
macro_rules! param {
|
||||
($name:expr, $type:expr, $desc:expr, $required:expr) => {
|
||||
(
|
||||
@ -746,4 +1028,27 @@ pub fn register_git_tools(registry: &mut ToolRegistry) {
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
// repo_test_discovery
|
||||
registry.register(
|
||||
ToolDefinition::new("repo_test_discovery")
|
||||
.description("Discover likely test files, test frameworks, and suggested test commands from repository manifests and file layout. Useful before changing code or planning validation.")
|
||||
.parameters(ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(HashMap::from([
|
||||
param!("project_name", "string", "Project name (slug)", true),
|
||||
param!("repo_name", "string", "Repository name", true),
|
||||
param!("max_files", "integer", "Maximum test file entries to return (default: 200)", false),
|
||||
])),
|
||||
required: Some(vec!["project_name".into(), "repo_name".into()]),
|
||||
}),
|
||||
ToolHandler::new(|ctx, args| {
|
||||
let gctx = super::ctx::GitToolCtx::new(ctx);
|
||||
Box::pin(async move {
|
||||
repo_test_discovery_exec(gctx, args)
|
||||
.await
|
||||
.map_err(agent::ToolError::ExecutionError)
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
199
libs/fctool/src/git_tools/status.rs
Normal file
199
libs/fctool/src/git_tools/status.rs
Normal file
@ -0,0 +1,199 @@
|
||||
//! Git status tools.
|
||||
|
||||
use super::ctx::GitToolCtx;
|
||||
use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema};
|
||||
use std::collections::HashMap;
|
||||
|
||||
async fn git_status_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let include_ignored = p
|
||||
.get("include_ignored")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let repo = domain.repo();
|
||||
let is_bare = repo.is_bare();
|
||||
let head = repo
|
||||
.head()
|
||||
.ok()
|
||||
.and_then(|h| h.shorthand().map(str::to_string));
|
||||
|
||||
if is_bare {
|
||||
return Ok(serde_json::json!({
|
||||
"is_bare": true,
|
||||
"head": head,
|
||||
"is_dirty": false,
|
||||
"files": [],
|
||||
"summary": {
|
||||
"total": 0,
|
||||
"index": 0,
|
||||
"worktree": 0,
|
||||
"untracked": 0,
|
||||
"ignored": 0,
|
||||
"conflicted": 0,
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
let mut opts = git2::StatusOptions::new();
|
||||
opts.include_untracked(true)
|
||||
.renames_head_to_index(true)
|
||||
.renames_index_to_workdir(true)
|
||||
.recurse_untracked_dirs(true);
|
||||
if include_ignored {
|
||||
opts.include_ignored(true);
|
||||
}
|
||||
|
||||
let statuses = repo
|
||||
.statuses(Some(&mut opts))
|
||||
.map_err(|e| format!("git status failed: {}", e))?;
|
||||
|
||||
let mut files = Vec::new();
|
||||
let mut index = 0usize;
|
||||
let mut worktree = 0usize;
|
||||
let mut untracked = 0usize;
|
||||
let mut ignored = 0usize;
|
||||
let mut conflicted = 0usize;
|
||||
|
||||
for entry in statuses.iter() {
|
||||
let status = entry.status();
|
||||
let path = entry
|
||||
.head_to_index()
|
||||
.and_then(|d| d.new_file().path())
|
||||
.or_else(|| entry.index_to_workdir().and_then(|d| d.new_file().path()))
|
||||
.or_else(|| entry.path().map(std::path::Path::new))
|
||||
.map(|p| p.to_string_lossy().replace('\\', "/"))
|
||||
.unwrap_or_default();
|
||||
|
||||
let index_status = index_status_label(status);
|
||||
let worktree_status = worktree_status_label(status);
|
||||
let is_untracked = status.contains(git2::Status::WT_NEW);
|
||||
let is_ignored = status.contains(git2::Status::IGNORED);
|
||||
let is_conflicted = status.is_conflicted();
|
||||
|
||||
if index_status.is_some() {
|
||||
index += 1;
|
||||
}
|
||||
if worktree_status.is_some() {
|
||||
worktree += 1;
|
||||
}
|
||||
if is_untracked {
|
||||
untracked += 1;
|
||||
}
|
||||
if is_ignored {
|
||||
ignored += 1;
|
||||
}
|
||||
if is_conflicted {
|
||||
conflicted += 1;
|
||||
}
|
||||
|
||||
files.push(serde_json::json!({
|
||||
"path": path,
|
||||
"index_status": index_status,
|
||||
"worktree_status": worktree_status,
|
||||
"is_untracked": is_untracked,
|
||||
"is_ignored": is_ignored,
|
||||
"is_conflicted": is_conflicted,
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"is_bare": false,
|
||||
"head": head,
|
||||
"is_dirty": !files.is_empty(),
|
||||
"summary": {
|
||||
"total": files.len(),
|
||||
"index": index,
|
||||
"worktree": worktree,
|
||||
"untracked": untracked,
|
||||
"ignored": ignored,
|
||||
"conflicted": conflicted,
|
||||
},
|
||||
"files": files,
|
||||
}))
|
||||
}
|
||||
|
||||
fn index_status_label(status: git2::Status) -> Option<&'static str> {
|
||||
if status.contains(git2::Status::INDEX_NEW) {
|
||||
Some("added")
|
||||
} else if status.contains(git2::Status::INDEX_MODIFIED) {
|
||||
Some("modified")
|
||||
} else if status.contains(git2::Status::INDEX_DELETED) {
|
||||
Some("deleted")
|
||||
} else if status.contains(git2::Status::INDEX_RENAMED) {
|
||||
Some("renamed")
|
||||
} else if status.contains(git2::Status::INDEX_TYPECHANGE) {
|
||||
Some("typechange")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn worktree_status_label(status: git2::Status) -> Option<&'static str> {
|
||||
if status.contains(git2::Status::WT_NEW) {
|
||||
Some("untracked")
|
||||
} else if status.contains(git2::Status::WT_MODIFIED) {
|
||||
Some("modified")
|
||||
} else if status.contains(git2::Status::WT_DELETED) {
|
||||
Some("deleted")
|
||||
} else if status.contains(git2::Status::WT_RENAMED) {
|
||||
Some("renamed")
|
||||
} else if status.contains(git2::Status::WT_TYPECHANGE) {
|
||||
Some("typechange")
|
||||
} else if status.contains(git2::Status::IGNORED) {
|
||||
Some("ignored")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_git_tools(registry: &mut ToolRegistry) {
|
||||
registry.register(
|
||||
ToolDefinition::new("git_status")
|
||||
.description("Show repository working tree status: staged, unstaged, untracked, ignored, and conflicted files. Bare repositories return an empty clean status.")
|
||||
.parameters(ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(HashMap::from([
|
||||
param("project_name", "string", "Project name (slug)", true),
|
||||
param("repo_name", "string", "Repository name", true),
|
||||
param("include_ignored", "boolean", "Include ignored files. Default false.", false),
|
||||
])),
|
||||
required: Some(vec!["project_name".into(), "repo_name".into()]),
|
||||
}),
|
||||
ToolHandler::new(|ctx, args| {
|
||||
let gctx = super::ctx::GitToolCtx::new(ctx);
|
||||
Box::pin(async move {
|
||||
git_status_exec(gctx, args)
|
||||
.await
|
||||
.map_err(agent::ToolError::ExecutionError)
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
fn param(name: &str, param_type: &str, description: &str, required: bool) -> (String, ToolParam) {
|
||||
(
|
||||
name.into(),
|
||||
ToolParam {
|
||||
name: name.into(),
|
||||
param_type: param_type.into(),
|
||||
description: Some(description.into()),
|
||||
required,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
)
|
||||
}
|
||||
280
libs/fctool/src/project_tools/bing.rs
Normal file
280
libs/fctool/src/project_tools/bing.rs
Normal file
@ -0,0 +1,280 @@
|
||||
//! Tool: project_bing_search - search the web with Bing Web Search API.
|
||||
|
||||
use agent::{ToolContext, ToolDefinition, ToolError, ToolParam, ToolSchema};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
const DEFAULT_COUNT: u64 = 10;
|
||||
const MAX_COUNT: u64 = 50;
|
||||
const DEFAULT_ENDPOINT: &str = "https://api.bing.microsoft.com/v7.0/search";
|
||||
|
||||
static SHARED_CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
|
||||
|
||||
fn shared_client() -> &'static reqwest::Client {
|
||||
SHARED_CLIENT.get_or_init(|| {
|
||||
reqwest::Client::builder()
|
||||
.connect_timeout(std::time::Duration::from_secs(10))
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.build()
|
||||
.expect("reqwest client build should not fail")
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct BingSearchResponse {
|
||||
#[serde(default)]
|
||||
web_pages: Option<BingWebPages>,
|
||||
#[serde(default)]
|
||||
query_context: Option<BingQueryContext>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct BingWebPages {
|
||||
#[serde(default)]
|
||||
total_estimated_matches: Option<u64>,
|
||||
#[serde(default)]
|
||||
value: Vec<BingWebResult>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct BingWebResult {
|
||||
#[serde(default)]
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
url: String,
|
||||
#[serde(default)]
|
||||
display_url: String,
|
||||
#[serde(default)]
|
||||
snippet: String,
|
||||
#[serde(default)]
|
||||
date_last_crawled: Option<String>,
|
||||
#[serde(default)]
|
||||
language: Option<String>,
|
||||
#[serde(default)]
|
||||
is_family_friendly: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct BingQueryContext {
|
||||
#[serde(default)]
|
||||
original_query: String,
|
||||
#[serde(default)]
|
||||
altered_query: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn bing_search_exec(
|
||||
ctx: ToolContext,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, ToolError> {
|
||||
let query = args
|
||||
.get("query")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.ok_or_else(|| ToolError::ExecutionError("query is required".into()))?;
|
||||
|
||||
let count = args
|
||||
.get("count")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(DEFAULT_COUNT)
|
||||
.clamp(1, MAX_COUNT);
|
||||
let offset = args.get("offset").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||
let market = args
|
||||
.get("market")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("en-US");
|
||||
let safe_search = args
|
||||
.get("safe_search")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("Moderate");
|
||||
let freshness = args.get("freshness").and_then(|v| v.as_str());
|
||||
|
||||
let api_key = ctx
|
||||
.config()
|
||||
.env
|
||||
.get("APP_BING_SEARCH_API_KEY")
|
||||
.or_else(|| ctx.config().env.get("BING_SEARCH_API_KEY"))
|
||||
.map(String::as_str)
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.ok_or_else(|| {
|
||||
ToolError::ExecutionError(
|
||||
"Bing search API key is required: set APP_BING_SEARCH_API_KEY or BING_SEARCH_API_KEY"
|
||||
.into(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let endpoint = ctx
|
||||
.config()
|
||||
.env
|
||||
.get("APP_BING_SEARCH_ENDPOINT")
|
||||
.map(String::as_str)
|
||||
.unwrap_or(DEFAULT_ENDPOINT);
|
||||
|
||||
let mut url = reqwest::Url::parse(endpoint)
|
||||
.map_err(|e| ToolError::ExecutionError(format!("Invalid Bing endpoint: {}", e)))?;
|
||||
{
|
||||
let mut query_pairs = url.query_pairs_mut();
|
||||
query_pairs
|
||||
.append_pair("q", query)
|
||||
.append_pair("count", &count.to_string())
|
||||
.append_pair("offset", &offset.to_string())
|
||||
.append_pair("mkt", market)
|
||||
.append_pair("safeSearch", safe_search)
|
||||
.append_pair("responseFilter", "Webpages")
|
||||
.append_pair("textFormat", "Raw");
|
||||
if let Some(freshness) = freshness {
|
||||
query_pairs.append_pair("freshness", freshness);
|
||||
}
|
||||
}
|
||||
|
||||
let response = shared_client()
|
||||
.get(url)
|
||||
.header("Ocp-Apim-Subscription-Key", api_key)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ToolError::ExecutionError(format!("Bing search request failed: {}", e)))?;
|
||||
let status = response.status();
|
||||
let body = response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| ToolError::ExecutionError(format!("Failed to read Bing response: {}", e)))?;
|
||||
|
||||
if !status.is_success() {
|
||||
return Err(ToolError::ExecutionError(format!(
|
||||
"Bing search returned status {}: {}",
|
||||
status,
|
||||
truncate(&body, 500)
|
||||
)));
|
||||
}
|
||||
|
||||
let parsed: BingSearchResponse = serde_json::from_str(&body)
|
||||
.map_err(|e| ToolError::ExecutionError(format!("Failed to parse Bing response: {}", e)))?;
|
||||
|
||||
let results = parsed
|
||||
.web_pages
|
||||
.as_ref()
|
||||
.map(|pages| {
|
||||
pages
|
||||
.value
|
||||
.iter()
|
||||
.map(|item| {
|
||||
serde_json::json!({
|
||||
"title": item.name,
|
||||
"url": item.url,
|
||||
"display_url": item.display_url,
|
||||
"snippet": item.snippet,
|
||||
"date_last_crawled": item.date_last_crawled,
|
||||
"language": item.language,
|
||||
"is_family_friendly": item.is_family_friendly,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"query": query,
|
||||
"original_query": parsed.query_context.as_ref().map(|q| q.original_query.as_str()).unwrap_or(query),
|
||||
"altered_query": parsed.query_context.and_then(|q| q.altered_query),
|
||||
"count": results.len(),
|
||||
"total_estimated_matches": parsed.web_pages.and_then(|p| p.total_estimated_matches),
|
||||
"results": results,
|
||||
}))
|
||||
}
|
||||
|
||||
fn truncate(s: &str, max_chars: usize) -> String {
|
||||
let mut chars = s.chars();
|
||||
let truncated: String = chars.by_ref().take(max_chars).collect();
|
||||
if chars.next().is_some() {
|
||||
format!("{}...", truncated)
|
||||
} else {
|
||||
truncated
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tool_definition() -> ToolDefinition {
|
||||
let mut p = HashMap::new();
|
||||
p.insert(
|
||||
"query".into(),
|
||||
ToolParam {
|
||||
name: "query".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Web search query. Required.".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"count".into(),
|
||||
ToolParam {
|
||||
name: "count".into(),
|
||||
param_type: "integer".into(),
|
||||
description: Some("Number of results to return. Default 10, max 50.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"offset".into(),
|
||||
ToolParam {
|
||||
name: "offset".into(),
|
||||
param_type: "integer".into(),
|
||||
description: Some("Result offset for pagination. Default 0.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"market".into(),
|
||||
ToolParam {
|
||||
name: "market".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Market code such as en-US or zh-CN. Default en-US.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"safe_search".into(),
|
||||
ToolParam {
|
||||
name: "safe_search".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some(
|
||||
"Bing safe search level: Off, Moderate, or Strict. Default Moderate.".into(),
|
||||
),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"freshness".into(),
|
||||
ToolParam {
|
||||
name: "freshness".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Optional freshness filter: Day, Week, or Month.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
|
||||
ToolDefinition::new("project_bing_search")
|
||||
.description(
|
||||
"Search the public web with Bing Web Search API. Returns titles, URLs, snippets, crawl dates, and estimated match counts. Requires APP_BING_SEARCH_API_KEY or BING_SEARCH_API_KEY.",
|
||||
)
|
||||
.parameters(ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(p),
|
||||
required: Some(vec!["query".into()]),
|
||||
})
|
||||
}
|
||||
@ -272,7 +272,7 @@ pub async fn curl_exec(
|
||||
|
||||
// ─── tool definition ─────────────────────────────────────────────────────────
|
||||
|
||||
pub fn tool_definition() -> ToolDefinition {
|
||||
fn tool_definition_with_name(name: &str) -> ToolDefinition {
|
||||
let mut p = HashMap::new();
|
||||
p.insert(
|
||||
"url".into(),
|
||||
@ -332,7 +332,7 @@ pub fn tool_definition() -> ToolDefinition {
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
ToolDefinition::new("project_curl")
|
||||
ToolDefinition::new(name)
|
||||
.description(
|
||||
"Perform an HTTP request to any URL. Supports GET, POST, PUT, DELETE, PATCH, HEAD. \
|
||||
Returns status code, headers, and response body. \
|
||||
@ -345,3 +345,11 @@ pub fn tool_definition() -> ToolDefinition {
|
||||
required: Some(vec!["url".into()]),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn tool_definition() -> ToolDefinition {
|
||||
tool_definition_with_name("project_curl")
|
||||
}
|
||||
|
||||
pub fn alias_tool_definition() -> ToolDefinition {
|
||||
tool_definition_with_name("curl_exec")
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
//! - list / create / update boards and board cards
|
||||
|
||||
mod arxiv;
|
||||
mod bing;
|
||||
mod boards;
|
||||
mod curl;
|
||||
mod issues;
|
||||
@ -16,6 +17,7 @@ mod repos;
|
||||
use agent::{ToolHandler, ToolRegistry};
|
||||
|
||||
pub use arxiv::arxiv_search_exec;
|
||||
pub use bing::bing_search_exec;
|
||||
pub use boards::{
|
||||
create_board_card_exec, create_board_column_exec, create_board_exec, delete_board_card_exec,
|
||||
list_boards_exec, update_board_card_exec, update_board_exec,
|
||||
@ -34,10 +36,19 @@ pub fn register_all(registry: &mut ToolRegistry) {
|
||||
ToolHandler::new(|ctx, args| Box::pin(arxiv_search_exec(ctx, args))),
|
||||
);
|
||||
|
||||
registry.register(
|
||||
bing::tool_definition(),
|
||||
ToolHandler::new(|ctx, args| Box::pin(bing_search_exec(ctx, args))),
|
||||
);
|
||||
|
||||
registry.register(
|
||||
curl::tool_definition(),
|
||||
ToolHandler::new(|ctx, args| Box::pin(curl_exec(ctx, args))),
|
||||
);
|
||||
registry.register(
|
||||
curl::alias_tool_definition(),
|
||||
ToolHandler::new(|ctx, args| Box::pin(curl_exec(ctx, args))),
|
||||
);
|
||||
|
||||
registry.register(
|
||||
repos::list_tool_definition(),
|
||||
|
||||
@ -23,12 +23,16 @@ use crate::GitDomain;
|
||||
|
||||
use sha1::Digest;
|
||||
|
||||
const SKILL_ROOTS: &[(&str, &str)] = &[(".claude/skills", "claude"), (".codex/skills", "codex")];
|
||||
const ROOT_SKILL_SYSTEM: &str = "root";
|
||||
|
||||
fn should_descend_dir(name: &str) -> bool {
|
||||
name != ".git"
|
||||
}
|
||||
|
||||
/// Recursively scan `base` for files named `SKILL.md`.
|
||||
/// The skill slug is `{short_repo_id}/{parent_dir_name}` to ensure uniqueness across repos.
|
||||
/// Recursively scan supported skill locations for files named `SKILL.md`.
|
||||
/// Root-level skill packs keep the legacy slug `{short_repo_id}/{skill_dir}`.
|
||||
/// System skills use `{short_repo_id}/{system}/{relative_skill_dir}`.
|
||||
fn scan_skills_from_dir(
|
||||
base: &Path,
|
||||
repo_id: &RepoId,
|
||||
@ -36,8 +40,34 @@ fn scan_skills_from_dir(
|
||||
) -> Result<Vec<DiscoveredSkill>, std::io::Error> {
|
||||
let repo_id_prefix = &repo_id.to_string()[..8];
|
||||
let mut discovered = Vec::new();
|
||||
let mut stack = vec![base.to_path_buf()];
|
||||
|
||||
for (root, system) in SKILL_ROOTS {
|
||||
let root_path = base.join(root);
|
||||
if root_path.exists() {
|
||||
scan_skill_root_from_dir(
|
||||
&root_path,
|
||||
repo_id_prefix,
|
||||
system,
|
||||
root,
|
||||
commit_sha,
|
||||
&mut discovered,
|
||||
);
|
||||
}
|
||||
}
|
||||
scan_root_skill_pack_from_dir(base, repo_id_prefix, commit_sha, &mut discovered);
|
||||
|
||||
Ok(discovered)
|
||||
}
|
||||
|
||||
fn scan_skill_root_from_dir(
|
||||
root_path: &Path,
|
||||
repo_id_prefix: &str,
|
||||
system: &str,
|
||||
root: &str,
|
||||
commit_sha: &str,
|
||||
discovered: &mut Vec<DiscoveredSkill>,
|
||||
) {
|
||||
let mut stack = vec![root_path.to_path_buf()];
|
||||
while let Some(dir) = stack.pop() {
|
||||
let entries = match std::fs::read_dir(&dir) {
|
||||
Ok(e) => e,
|
||||
@ -47,30 +77,82 @@ fn scan_skills_from_dir(
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
stack.push(path);
|
||||
} else if path
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.map(|x| x.to_lowercase())
|
||||
== Some("skill.md".to_string())
|
||||
{
|
||||
if let Some(dir_name) = path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.and_then(|n| n.to_str())
|
||||
{
|
||||
let slug = format!("{}/{}", repo_id_prefix, dir_name);
|
||||
if let Ok(raw) = std::fs::read(&path) {
|
||||
let blob_hash = git_blob_hash(&raw);
|
||||
let mut skill = parse_skill_content(&slug, &raw);
|
||||
skill.commit_sha = Some(commit_sha.to_string());
|
||||
skill.blob_hash = Some(blob_hash);
|
||||
discovered.push(skill);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if !is_skill_file_name(&path) {
|
||||
continue;
|
||||
}
|
||||
let Some(parent) = path.parent() else {
|
||||
continue;
|
||||
};
|
||||
let relative_skill_dir = parent
|
||||
.strip_prefix(root_path)
|
||||
.ok()
|
||||
.and_then(path_to_slug)
|
||||
.filter(|s| !s.is_empty());
|
||||
let Some(relative_skill_dir) = relative_skill_dir else {
|
||||
continue;
|
||||
};
|
||||
let slug = format!("{}/{}/{}", repo_id_prefix, system, relative_skill_dir);
|
||||
if let Ok(raw) = std::fs::read(&path) {
|
||||
let blob_hash = git_blob_hash(&raw);
|
||||
let mut skill = parse_skill_content(&slug, &raw);
|
||||
skill.commit_sha = Some(commit_sha.to_string());
|
||||
skill.blob_hash = Some(blob_hash);
|
||||
skill.metadata = enrich_metadata(
|
||||
skill.metadata,
|
||||
system,
|
||||
Some(&format!("{}/{}/SKILL.md", root, relative_skill_dir)),
|
||||
);
|
||||
discovered.push(skill);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(discovered)
|
||||
}
|
||||
|
||||
fn scan_root_skill_pack_from_dir(
|
||||
base: &Path,
|
||||
repo_id_prefix: &str,
|
||||
commit_sha: &str,
|
||||
discovered: &mut Vec<DiscoveredSkill>,
|
||||
) {
|
||||
let entries = match std::fs::read_dir(base) {
|
||||
Ok(e) => e,
|
||||
Err(_) => return,
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if !path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
let Some(dir_name) = path.file_name().and_then(|n| n.to_str()) else {
|
||||
continue;
|
||||
};
|
||||
if dir_name == ".git" || dir_name == ".claude" || dir_name == ".codex" {
|
||||
continue;
|
||||
}
|
||||
let skill_file = path.join("SKILL.md");
|
||||
if !skill_file.exists() {
|
||||
continue;
|
||||
}
|
||||
let relative_skill_dir = slugify_segment(dir_name);
|
||||
if relative_skill_dir.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let slug = format!("{}/{}", repo_id_prefix, relative_skill_dir);
|
||||
if let Ok(raw) = std::fs::read(&skill_file) {
|
||||
let blob_hash = git_blob_hash(&raw);
|
||||
let mut skill = parse_skill_content(&slug, &raw);
|
||||
skill.commit_sha = Some(commit_sha.to_string());
|
||||
skill.blob_hash = Some(blob_hash);
|
||||
skill.metadata = enrich_metadata(
|
||||
skill.metadata,
|
||||
ROOT_SKILL_SYSTEM,
|
||||
Some(&format!("{}/SKILL.md", relative_skill_dir)),
|
||||
);
|
||||
discovered.push(skill);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn git_blob_hash(content: &[u8]) -> String {
|
||||
@ -126,6 +208,57 @@ struct DiscoveredSkill {
|
||||
blob_hash: Option<String>,
|
||||
}
|
||||
|
||||
fn is_skill_file_name(path: &Path) -> bool {
|
||||
path.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.is_some_and(|name| name.eq_ignore_ascii_case("SKILL.md"))
|
||||
}
|
||||
|
||||
fn path_to_slug(path: &Path) -> Option<String> {
|
||||
let parts: Vec<String> = path
|
||||
.components()
|
||||
.filter_map(|c| c.as_os_str().to_str())
|
||||
.map(slugify_segment)
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
(!parts.is_empty()).then(|| parts.join("/"))
|
||||
}
|
||||
|
||||
fn slugify_segment(input: &str) -> String {
|
||||
let mut out = String::with_capacity(input.len());
|
||||
let mut last_dash = false;
|
||||
for ch in input.chars() {
|
||||
let ch = ch.to_ascii_lowercase();
|
||||
if ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' {
|
||||
out.push(ch);
|
||||
last_dash = false;
|
||||
} else if !last_dash {
|
||||
out.push('-');
|
||||
last_dash = true;
|
||||
}
|
||||
}
|
||||
out.trim_matches('-').to_string()
|
||||
}
|
||||
|
||||
fn enrich_metadata(
|
||||
mut metadata: serde_json::Value,
|
||||
system: &str,
|
||||
relative_path: Option<&str>,
|
||||
) -> serde_json::Value {
|
||||
if !metadata.is_object() {
|
||||
metadata = serde_json::json!({});
|
||||
}
|
||||
if let Some(obj) = metadata.as_object_mut() {
|
||||
obj.entry("system")
|
||||
.or_insert_with(|| serde_json::Value::String(system.to_string()));
|
||||
if let Some(relative_path) = relative_path {
|
||||
obj.entry("path")
|
||||
.or_insert_with(|| serde_json::Value::String(relative_path.to_string()));
|
||||
}
|
||||
}
|
||||
metadata
|
||||
}
|
||||
|
||||
fn extract_frontmatter(raw: &str) -> (Option<&str>, &str) {
|
||||
let trimmed = raw.trim_start();
|
||||
if !trimmed.starts_with("---") {
|
||||
@ -175,20 +308,25 @@ fn scan_skills_from_tree(
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(git2::ObjectType::Blob) if name.to_lowercase() == "skill.md" => {
|
||||
let dir_name = std::path::Path::new(&entry_path)
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.and_then(|n| n.to_str());
|
||||
let Some(dir_name) = dir_name else { continue };
|
||||
Some(git2::ObjectType::Blob) if name.eq_ignore_ascii_case("SKILL.md") => {
|
||||
let Some((system, relative_skill_dir, legacy_slug)) =
|
||||
skill_location_from_path(&entry_path)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let slug = format!("{}/{}", repo_id_prefix, dir_name);
|
||||
let slug = if legacy_slug {
|
||||
format!("{}/{}", repo_id_prefix, relative_skill_dir)
|
||||
} else {
|
||||
format!("{}/{}/{}", repo_id_prefix, system, relative_skill_dir)
|
||||
};
|
||||
if let Ok(blob) = entry.to_object(git_repo).and_then(|o| o.peel_to_blob()) {
|
||||
let raw = blob.content();
|
||||
let blob_hash = git_blob_hash(raw);
|
||||
let mut skill = parse_skill_content(&slug, raw);
|
||||
skill.commit_sha = Some(commit_sha.to_string());
|
||||
skill.blob_hash = Some(blob_hash);
|
||||
skill.metadata = enrich_metadata(skill.metadata, system, Some(&entry_path));
|
||||
discovered.push(skill);
|
||||
}
|
||||
}
|
||||
@ -200,6 +338,39 @@ fn scan_skills_from_tree(
|
||||
Ok(discovered)
|
||||
}
|
||||
|
||||
fn skill_location_from_path(path: &str) -> Option<(&'static str, String, bool)> {
|
||||
let normalized = path.replace('\\', "/");
|
||||
for (root, system) in SKILL_ROOTS {
|
||||
let prefix = format!("{}/", root);
|
||||
let suffix = "/SKILL.md";
|
||||
if normalized.starts_with(&prefix) && normalized.ends_with(suffix) {
|
||||
let relative = &normalized[prefix.len()..normalized.len() - suffix.len()];
|
||||
let slug = relative
|
||||
.split('/')
|
||||
.map(slugify_segment)
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join("/");
|
||||
if !slug.is_empty() {
|
||||
return Some((*system, slug, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let suffix = "/SKILL.md";
|
||||
if normalized.ends_with(suffix) && !normalized.starts_with('.') {
|
||||
let relative = &normalized[..normalized.len() - suffix.len()];
|
||||
if !relative.contains('/') {
|
||||
let slug = slugify_segment(relative);
|
||||
if !slug.is_empty() {
|
||||
return Some((ROOT_SKILL_SYSTEM, slug, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HookMetaDataSync {
|
||||
pub db: AppDatabase,
|
||||
@ -414,40 +585,27 @@ impl HookMetaDataSync {
|
||||
}
|
||||
};
|
||||
|
||||
// Deduplicate by {repo_id}+{blob_hash}, keep latest by commit_sha
|
||||
// Deduplicate by stable slug. Blob hash changes when content changes and must not be the
|
||||
// upsert key because project_skill has a unique (project_uuid, slug) constraint.
|
||||
let mut deduped: std::collections::HashMap<String, DiscoveredSkill> =
|
||||
std::collections::HashMap::new();
|
||||
for skill in discovered {
|
||||
let key = if let Some(ref hash) = skill.blob_hash {
|
||||
format!("{}:{}", self.repo.id, hash)
|
||||
} else {
|
||||
format!("{}:{}:slug", self.repo.id, skill.slug)
|
||||
};
|
||||
match deduped.get(&key) {
|
||||
match deduped.get(&skill.slug) {
|
||||
Some(existing) => {
|
||||
if skill.commit_sha.as_ref().unwrap_or(&String::new())
|
||||
> existing.commit_sha.as_ref().unwrap_or(&String::new())
|
||||
{
|
||||
deduped.insert(key, skill);
|
||||
deduped.insert(skill.slug.clone(), skill);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
deduped.insert(key, skill);
|
||||
deduped.insert(skill.slug.clone(), skill);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let existing_by_hash: HashMap<_, _> = existing
|
||||
.into_iter()
|
||||
.map(|s| {
|
||||
let key = format!(
|
||||
"{}:{}",
|
||||
s.repo_id.unwrap_or_default(),
|
||||
s.blob_hash.clone().unwrap_or_default()
|
||||
);
|
||||
(key, s)
|
||||
})
|
||||
.collect();
|
||||
let existing_by_slug: HashMap<_, _> =
|
||||
existing.into_iter().map(|s| (s.slug.clone(), s)).collect();
|
||||
|
||||
let mut seen_keys = std::collections::HashSet::new();
|
||||
|
||||
@ -455,12 +613,17 @@ impl HookMetaDataSync {
|
||||
seen_keys.insert(key.clone());
|
||||
let json_meta = serde_json::to_value(&skill.metadata).unwrap_or_default();
|
||||
|
||||
if let Some(existing_skill) = existing_by_hash.get(&key) {
|
||||
if let Some(existing_skill) = existing_by_slug.get(&key) {
|
||||
if existing_skill.content != skill.content
|
||||
|| existing_skill.metadata != json_meta
|
||||
|| existing_skill.commit_sha != skill.commit_sha
|
||||
|| existing_skill.blob_hash != skill.blob_hash
|
||||
|| existing_skill.name != skill.name
|
||||
|| existing_skill.description != skill.description
|
||||
{
|
||||
let mut active: SkillActiveModel = existing_skill.clone().into();
|
||||
active.name = Set(skill.name);
|
||||
active.description = Set(skill.description);
|
||||
active.content = Set(skill.content);
|
||||
active.metadata = Set(json_meta);
|
||||
active.commit_sha = Set(skill.commit_sha);
|
||||
@ -494,7 +657,7 @@ impl HookMetaDataSync {
|
||||
}
|
||||
}
|
||||
|
||||
for (key, old_skill) in existing_by_hash {
|
||||
for (key, old_skill) in existing_by_slug {
|
||||
if !seen_keys.contains(&key) {
|
||||
if SkillEntity::delete_by_id(old_skill.id)
|
||||
.exec(&self.db)
|
||||
|
||||
@ -4,27 +4,24 @@ pub struct Migration;
|
||||
|
||||
impl MigrationName for Migration {
|
||||
fn name(&self) -> &str {
|
||||
"room_compact_summary"
|
||||
"bootstrap"
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let sql = include_str!("sql/room_compact_summary.sql");
|
||||
super::execute_sql(manager, sql).await?;
|
||||
manager
|
||||
.get_connection()
|
||||
.execute_unprepared(include_str!("sql/bootstrap/bootstrap_up_01.sql"))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.get_connection()
|
||||
.execute_unprepared(
|
||||
r#"
|
||||
DROP INDEX IF EXISTS idx_room_compact_summary_room_to_seq;
|
||||
DROP TABLE IF EXISTS room_compact_summary;
|
||||
"#,
|
||||
)
|
||||
.execute_unprepared(include_str!("sql/bootstrap/bootstrap_down_01.sql"))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@ -1,54 +0,0 @@
|
||||
//! SeaORM migration: create room_notifications table
|
||||
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
pub struct Migration;
|
||||
|
||||
impl MigrationName for Migration {
|
||||
fn name(&self) -> &str {
|
||||
"init"
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let sql = include_str!("sql/init.sql");
|
||||
super::execute_sql(manager, sql).await?;
|
||||
let e = "
|
||||
CREATE OR REPLACE FUNCTION room_message_tsv_trigger()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
BEGIN
|
||||
NEW.content_tsv := to_tsvector('english', NEW.content);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$function$;
|
||||
create or replace trigger room_message_tsv_update
|
||||
before insert or update
|
||||
on room_message
|
||||
for each row
|
||||
execute procedure room_message_tsv_trigger();
|
||||
DO
|
||||
$$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1
|
||||
FROM pg_type
|
||||
WHERE typname = 'notification_type'
|
||||
AND typtype = 'e') THEN
|
||||
CREATE TYPE notification_type AS ENUM (
|
||||
'mention',
|
||||
'invitation',
|
||||
'role_change',
|
||||
'room_created',
|
||||
'room_deleted',
|
||||
'system_announcement'
|
||||
);
|
||||
END IF;
|
||||
END
|
||||
$$;";
|
||||
manager.get_connection().execute_unprepared(e).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -35,23 +35,267 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Migrator;
|
||||
macro_rules! define_sql_migrations {
|
||||
($( $module:ident => { name: $name:literal, up: $up:literal, down: $down:literal } ),+ $(,)?) => {
|
||||
$(
|
||||
pub mod $module {
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigratorTrait for Migrator {
|
||||
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||
vec![
|
||||
Box::new(init::Migration),
|
||||
Box::new(room_compact_summary::Migration),
|
||||
Box::new(user_billing_history::Migration),
|
||||
Box::new(project_message_favorite::Migration),
|
||||
Box::new(ai_subagent_session::Migration),
|
||||
]
|
||||
}
|
||||
pub struct Migration;
|
||||
|
||||
impl MigrationName for Migration {
|
||||
fn name(&self) -> &str {
|
||||
$name
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let sql = include_str!($up);
|
||||
super::execute_sql(manager, sql).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let sql = include_str!($down);
|
||||
super::execute_sql(manager, sql).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
)+
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigratorTrait for Migrator {
|
||||
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||
vec![
|
||||
Box::new(user::Migration),
|
||||
Box::new(user_password::Migration),
|
||||
Box::new(user_email::Migration),
|
||||
Box::new(user_2fa::Migration),
|
||||
Box::new(user_notification::Migration),
|
||||
Box::new(user_preferences::Migration),
|
||||
Box::new(user_password_reset::Migration),
|
||||
Box::new(user_relation::Migration),
|
||||
Box::new(user_ssh_key::Migration),
|
||||
Box::new(user_token::Migration),
|
||||
Box::new(user_activity_log::Migration),
|
||||
Box::new(project_access_log::Migration),
|
||||
Box::new(project_audit_log::Migration),
|
||||
Box::new(project_billing::Migration),
|
||||
Box::new(project_billing_history::Migration),
|
||||
Box::new(project_follow::Migration),
|
||||
Box::new(project_history_name::Migration),
|
||||
Box::new(project_label::Migration),
|
||||
Box::new(project_like::Migration),
|
||||
Box::new(project_member_invitations::Migration),
|
||||
Box::new(project_member_join_answers::Migration),
|
||||
Box::new(project_member_join_request::Migration),
|
||||
Box::new(project_member_join_settings::Migration),
|
||||
Box::new(project_members::Migration),
|
||||
Box::new(project_watch::Migration),
|
||||
Box::new(repo::Migration),
|
||||
Box::new(repo_branch::Migration),
|
||||
Box::new(repo_branch_protect::Migration),
|
||||
Box::new(repo_collaborator::Migration),
|
||||
Box::new(repo_commit::Migration),
|
||||
Box::new(repo_fork::Migration),
|
||||
Box::new(repo_history_name::Migration),
|
||||
Box::new(repo_hook::Migration),
|
||||
Box::new(repo_lfs_lock::Migration),
|
||||
Box::new(repo_lfs_object::Migration),
|
||||
Box::new(repo_lock::Migration),
|
||||
Box::new(repo_star::Migration),
|
||||
Box::new(repo_tag::Migration),
|
||||
Box::new(repo_upstream::Migration),
|
||||
Box::new(repo_watch::Migration),
|
||||
Box::new(repo_webhook::Migration),
|
||||
Box::new(issue::Migration),
|
||||
Box::new(issue_assignee::Migration),
|
||||
Box::new(issue_comment::Migration),
|
||||
Box::new(issue_comment_reaction::Migration),
|
||||
Box::new(issue_label::Migration),
|
||||
Box::new(issue_pull_request::Migration),
|
||||
Box::new(issue_reaction::Migration),
|
||||
Box::new(issue_repo::Migration),
|
||||
Box::new(issue_subscriber::Migration),
|
||||
Box::new(pull_request::Migration),
|
||||
Box::new(pull_request_commit::Migration),
|
||||
Box::new(pull_request_review::Migration),
|
||||
Box::new(pull_request_review_comment::Migration),
|
||||
Box::new(room_category::Migration),
|
||||
Box::new(room::Migration),
|
||||
Box::new(room_ai::Migration),
|
||||
Box::new(room_message::Migration),
|
||||
Box::new(room_pin::Migration),
|
||||
Box::new(room_thread::Migration),
|
||||
Box::new(ai_model_provider::Migration),
|
||||
Box::new(ai_model::Migration),
|
||||
Box::new(ai_model_version::Migration),
|
||||
Box::new(ai_model_capability::Migration),
|
||||
Box::new(ai_model_parameter_profile::Migration),
|
||||
Box::new(ai_model_pricing::Migration),
|
||||
Box::new(ai_session::Migration),
|
||||
Box::new(ai_tool_call::Migration),
|
||||
Box::new(ai_tool_auth::Migration),
|
||||
Box::new(label::Migration),
|
||||
Box::new(notify::Migration),
|
||||
Box::new(room_notifications::Migration),
|
||||
Box::new(user_email_change::Migration),
|
||||
Box::new(project_activity::Migration),
|
||||
Box::new(room_message_reaction::Migration),
|
||||
Box::new(room_message_edit_history::Migration),
|
||||
Box::new(project_board::Migration),
|
||||
Box::new(project_board_column::Migration),
|
||||
Box::new(project_board_card::Migration),
|
||||
Box::new(pull_request_review_request::Migration),
|
||||
Box::new(workspace::Migration),
|
||||
Box::new(project::Migration),
|
||||
Box::new(workspace_membership::Migration),
|
||||
Box::new(workspace_billing::Migration),
|
||||
Box::new(workspace_billing_history::Migration),
|
||||
Box::new(project_skill::Migration),
|
||||
Box::new(agent_task::Migration),
|
||||
Box::new(admin_user::Migration),
|
||||
Box::new(admin_role::Migration),
|
||||
Box::new(admin_permission::Migration),
|
||||
Box::new(admin_user_role::Migration),
|
||||
Box::new(admin_role_permission::Migration),
|
||||
Box::new(admin_audit_log::Migration),
|
||||
Box::new(admin_api_token::Migration),
|
||||
Box::new(workspace_alert_config::Migration),
|
||||
Box::new(room_attachment::Migration),
|
||||
Box::new(room_access::Migration),
|
||||
Box::new(room_user_state::Migration),
|
||||
Box::new(project_role_priority::Migration),
|
||||
Box::new(ai_conversation::Migration),
|
||||
Box::new(ai_message::Migration),
|
||||
Box::new(ai_message_fork::Migration),
|
||||
Box::new(ai_shared_conversation::Migration),
|
||||
Box::new(ai_token_usage::Migration),
|
||||
Box::new(room_compact_summary::Migration),
|
||||
Box::new(user_billing_history::Migration),
|
||||
Box::new(project_message_favorite::Migration),
|
||||
Box::new(ai_subagent_session::Migration),
|
||||
Box::new(bootstrap::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub mod ai_subagent_session;
|
||||
pub mod init;
|
||||
pub mod project_message_favorite;
|
||||
pub mod room_compact_summary;
|
||||
pub mod user_billing_history;
|
||||
define_sql_migrations! {
|
||||
user => { name: "user", up: "sql/user/user_up_01.sql", down: "sql/user/user_down_01.sql" },
|
||||
user_password => { name: "user_password", up: "sql/user_password/user_password_up_01.sql", down: "sql/user_password/user_password_down_01.sql" },
|
||||
user_email => { name: "user_email", up: "sql/user_email/user_email_up_01.sql", down: "sql/user_email/user_email_down_01.sql" },
|
||||
user_2fa => { name: "user_2fa", up: "sql/user_2fa/user_2fa_up_01.sql", down: "sql/user_2fa/user_2fa_down_01.sql" },
|
||||
user_notification => { name: "user_notification", up: "sql/user_notification/user_notification_up_01.sql", down: "sql/user_notification/user_notification_down_01.sql" },
|
||||
user_preferences => { name: "user_preferences", up: "sql/user_preferences/user_preferences_up_01.sql", down: "sql/user_preferences/user_preferences_down_01.sql" },
|
||||
user_password_reset => { name: "user_password_reset", up: "sql/user_password_reset/user_password_reset_up_01.sql", down: "sql/user_password_reset/user_password_reset_down_01.sql" },
|
||||
user_relation => { name: "user_relation", up: "sql/user_relation/user_relation_up_01.sql", down: "sql/user_relation/user_relation_down_01.sql" },
|
||||
user_ssh_key => { name: "user_ssh_key", up: "sql/user_ssh_key/user_ssh_key_up_01.sql", down: "sql/user_ssh_key/user_ssh_key_down_01.sql" },
|
||||
user_token => { name: "user_token", up: "sql/user_token/user_token_up_01.sql", down: "sql/user_token/user_token_down_01.sql" },
|
||||
user_activity_log => { name: "user_activity_log", up: "sql/user_activity_log/user_activity_log_up_01.sql", down: "sql/user_activity_log/user_activity_log_down_01.sql" },
|
||||
project_access_log => { name: "project_access_log", up: "sql/project_access_log/project_access_log_up_01.sql", down: "sql/project_access_log/project_access_log_down_01.sql" },
|
||||
project_audit_log => { name: "project_audit_log", up: "sql/project_audit_log/project_audit_log_up_01.sql", down: "sql/project_audit_log/project_audit_log_down_01.sql" },
|
||||
project_billing => { name: "project_billing", up: "sql/project_billing/project_billing_up_01.sql", down: "sql/project_billing/project_billing_down_01.sql" },
|
||||
project_billing_history => { name: "project_billing_history", up: "sql/project_billing_history/project_billing_history_up_01.sql", down: "sql/project_billing_history/project_billing_history_down_01.sql" },
|
||||
project_follow => { name: "project_follow", up: "sql/project_follow/project_follow_up_01.sql", down: "sql/project_follow/project_follow_down_01.sql" },
|
||||
project_history_name => { name: "project_history_name", up: "sql/project_history_name/project_history_name_up_01.sql", down: "sql/project_history_name/project_history_name_down_01.sql" },
|
||||
project_label => { name: "project_label", up: "sql/project_label/project_label_up_01.sql", down: "sql/project_label/project_label_down_01.sql" },
|
||||
project_like => { name: "project_like", up: "sql/project_like/project_like_up_01.sql", down: "sql/project_like/project_like_down_01.sql" },
|
||||
project_member_invitations => { name: "project_member_invitations", up: "sql/project_member_invitations/project_member_invitations_up_01.sql", down: "sql/project_member_invitations/project_member_invitations_down_01.sql" },
|
||||
project_member_join_answers => { name: "project_member_join_answers", up: "sql/project_member_join_answers/project_member_join_answers_up_01.sql", down: "sql/project_member_join_answers/project_member_join_answers_down_01.sql" },
|
||||
project_member_join_request => { name: "project_member_join_request", up: "sql/project_member_join_request/project_member_join_request_up_01.sql", down: "sql/project_member_join_request/project_member_join_request_down_01.sql" },
|
||||
project_member_join_settings => { name: "project_member_join_settings", up: "sql/project_member_join_settings/project_member_join_settings_up_01.sql", down: "sql/project_member_join_settings/project_member_join_settings_down_01.sql" },
|
||||
project_members => { name: "project_members", up: "sql/project_members/project_members_up_01.sql", down: "sql/project_members/project_members_down_01.sql" },
|
||||
project_watch => { name: "project_watch", up: "sql/project_watch/project_watch_up_01.sql", down: "sql/project_watch/project_watch_down_01.sql" },
|
||||
repo => { name: "repo", up: "sql/repo/repo_up_01.sql", down: "sql/repo/repo_down_01.sql" },
|
||||
repo_branch => { name: "repo_branch", up: "sql/repo_branch/repo_branch_up_01.sql", down: "sql/repo_branch/repo_branch_down_01.sql" },
|
||||
repo_branch_protect => { name: "repo_branch_protect", up: "sql/repo_branch_protect/repo_branch_protect_up_01.sql", down: "sql/repo_branch_protect/repo_branch_protect_down_01.sql" },
|
||||
repo_collaborator => { name: "repo_collaborator", up: "sql/repo_collaborator/repo_collaborator_up_01.sql", down: "sql/repo_collaborator/repo_collaborator_down_01.sql" },
|
||||
repo_commit => { name: "repo_commit", up: "sql/repo_commit/repo_commit_up_01.sql", down: "sql/repo_commit/repo_commit_down_01.sql" },
|
||||
repo_fork => { name: "repo_fork", up: "sql/repo_fork/repo_fork_up_01.sql", down: "sql/repo_fork/repo_fork_down_01.sql" },
|
||||
repo_history_name => { name: "repo_history_name", up: "sql/repo_history_name/repo_history_name_up_01.sql", down: "sql/repo_history_name/repo_history_name_down_01.sql" },
|
||||
repo_hook => { name: "repo_hook", up: "sql/repo_hook/repo_hook_up_01.sql", down: "sql/repo_hook/repo_hook_down_01.sql" },
|
||||
repo_lfs_lock => { name: "repo_lfs_lock", up: "sql/repo_lfs_lock/repo_lfs_lock_up_01.sql", down: "sql/repo_lfs_lock/repo_lfs_lock_down_01.sql" },
|
||||
repo_lfs_object => { name: "repo_lfs_object", up: "sql/repo_lfs_object/repo_lfs_object_up_01.sql", down: "sql/repo_lfs_object/repo_lfs_object_down_01.sql" },
|
||||
repo_lock => { name: "repo_lock", up: "sql/repo_lock/repo_lock_up_01.sql", down: "sql/repo_lock/repo_lock_down_01.sql" },
|
||||
repo_star => { name: "repo_star", up: "sql/repo_star/repo_star_up_01.sql", down: "sql/repo_star/repo_star_down_01.sql" },
|
||||
repo_tag => { name: "repo_tag", up: "sql/repo_tag/repo_tag_up_01.sql", down: "sql/repo_tag/repo_tag_down_01.sql" },
|
||||
repo_upstream => { name: "repo_upstream", up: "sql/repo_upstream/repo_upstream_up_01.sql", down: "sql/repo_upstream/repo_upstream_down_01.sql" },
|
||||
repo_watch => { name: "repo_watch", up: "sql/repo_watch/repo_watch_up_01.sql", down: "sql/repo_watch/repo_watch_down_01.sql" },
|
||||
repo_webhook => { name: "repo_webhook", up: "sql/repo_webhook/repo_webhook_up_01.sql", down: "sql/repo_webhook/repo_webhook_down_01.sql" },
|
||||
issue => { name: "issue", up: "sql/issue/issue_up_01.sql", down: "sql/issue/issue_down_01.sql" },
|
||||
issue_assignee => { name: "issue_assignee", up: "sql/issue_assignee/issue_assignee_up_01.sql", down: "sql/issue_assignee/issue_assignee_down_01.sql" },
|
||||
issue_comment => { name: "issue_comment", up: "sql/issue_comment/issue_comment_up_01.sql", down: "sql/issue_comment/issue_comment_down_01.sql" },
|
||||
issue_comment_reaction => { name: "issue_comment_reaction", up: "sql/issue_comment_reaction/issue_comment_reaction_up_01.sql", down: "sql/issue_comment_reaction/issue_comment_reaction_down_01.sql" },
|
||||
issue_label => { name: "issue_label", up: "sql/issue_label/issue_label_up_01.sql", down: "sql/issue_label/issue_label_down_01.sql" },
|
||||
issue_pull_request => { name: "issue_pull_request", up: "sql/issue_pull_request/issue_pull_request_up_01.sql", down: "sql/issue_pull_request/issue_pull_request_down_01.sql" },
|
||||
issue_reaction => { name: "issue_reaction", up: "sql/issue_reaction/issue_reaction_up_01.sql", down: "sql/issue_reaction/issue_reaction_down_01.sql" },
|
||||
issue_repo => { name: "issue_repo", up: "sql/issue_repo/issue_repo_up_01.sql", down: "sql/issue_repo/issue_repo_down_01.sql" },
|
||||
issue_subscriber => { name: "issue_subscriber", up: "sql/issue_subscriber/issue_subscriber_up_01.sql", down: "sql/issue_subscriber/issue_subscriber_down_01.sql" },
|
||||
pull_request => { name: "pull_request", up: "sql/pull_request/pull_request_up_01.sql", down: "sql/pull_request/pull_request_down_01.sql" },
|
||||
pull_request_commit => { name: "pull_request_commit", up: "sql/pull_request_commit/pull_request_commit_up_01.sql", down: "sql/pull_request_commit/pull_request_commit_down_01.sql" },
|
||||
pull_request_review => { name: "pull_request_review", up: "sql/pull_request_review/pull_request_review_up_01.sql", down: "sql/pull_request_review/pull_request_review_down_01.sql" },
|
||||
pull_request_review_comment => { name: "pull_request_review_comment", up: "sql/pull_request_review_comment/pull_request_review_comment_up_01.sql", down: "sql/pull_request_review_comment/pull_request_review_comment_down_01.sql" },
|
||||
room_category => { name: "room_category", up: "sql/room_category/room_category_up_01.sql", down: "sql/room_category/room_category_down_01.sql" },
|
||||
room => { name: "room", up: "sql/room/room_up_01.sql", down: "sql/room/room_down_01.sql" },
|
||||
room_ai => { name: "room_ai", up: "sql/room_ai/room_ai_up_01.sql", down: "sql/room_ai/room_ai_down_01.sql" },
|
||||
room_message => { name: "room_message", up: "sql/room_message/room_message_up_01.sql", down: "sql/room_message/room_message_down_01.sql" },
|
||||
room_pin => { name: "room_pin", up: "sql/room_pin/room_pin_up_01.sql", down: "sql/room_pin/room_pin_down_01.sql" },
|
||||
room_thread => { name: "room_thread", up: "sql/room_thread/room_thread_up_01.sql", down: "sql/room_thread/room_thread_down_01.sql" },
|
||||
ai_model_provider => { name: "ai_model_provider", up: "sql/ai_model_provider/ai_model_provider_up_01.sql", down: "sql/ai_model_provider/ai_model_provider_down_01.sql" },
|
||||
ai_model => { name: "ai_model", up: "sql/ai_model/ai_model_up_01.sql", down: "sql/ai_model/ai_model_down_01.sql" },
|
||||
ai_model_version => { name: "ai_model_version", up: "sql/ai_model_version/ai_model_version_up_01.sql", down: "sql/ai_model_version/ai_model_version_down_01.sql" },
|
||||
ai_model_capability => { name: "ai_model_capability", up: "sql/ai_model_capability/ai_model_capability_up_01.sql", down: "sql/ai_model_capability/ai_model_capability_down_01.sql" },
|
||||
ai_model_parameter_profile => { name: "ai_model_parameter_profile", up: "sql/ai_model_parameter_profile/ai_model_parameter_profile_up_01.sql", down: "sql/ai_model_parameter_profile/ai_model_parameter_profile_down_01.sql" },
|
||||
ai_model_pricing => { name: "ai_model_pricing", up: "sql/ai_model_pricing/ai_model_pricing_up_01.sql", down: "sql/ai_model_pricing/ai_model_pricing_down_01.sql" },
|
||||
ai_session => { name: "ai_session", up: "sql/ai_session/ai_session_up_01.sql", down: "sql/ai_session/ai_session_down_01.sql" },
|
||||
ai_tool_call => { name: "ai_tool_call", up: "sql/ai_tool_call/ai_tool_call_up_01.sql", down: "sql/ai_tool_call/ai_tool_call_down_01.sql" },
|
||||
ai_tool_auth => { name: "ai_tool_auth", up: "sql/ai_tool_auth/ai_tool_auth_up_01.sql", down: "sql/ai_tool_auth/ai_tool_auth_down_01.sql" },
|
||||
label => { name: "label", up: "sql/label/label_up_01.sql", down: "sql/label/label_down_01.sql" },
|
||||
notify => { name: "notify", up: "sql/notify/notify_up_01.sql", down: "sql/notify/notify_down_01.sql" },
|
||||
room_notifications => { name: "room_notifications", up: "sql/room_notifications/room_notifications_up_01.sql", down: "sql/room_notifications/room_notifications_down_01.sql" },
|
||||
user_email_change => { name: "user_email_change", up: "sql/user_email_change/user_email_change_up_01.sql", down: "sql/user_email_change/user_email_change_down_01.sql" },
|
||||
project_activity => { name: "project_activity", up: "sql/project_activity/project_activity_up_01.sql", down: "sql/project_activity/project_activity_down_01.sql" },
|
||||
room_message_reaction => { name: "room_message_reaction", up: "sql/room_message_reaction/room_message_reaction_up_01.sql", down: "sql/room_message_reaction/room_message_reaction_down_01.sql" },
|
||||
room_message_edit_history => { name: "room_message_edit_history", up: "sql/room_message_edit_history/room_message_edit_history_up_01.sql", down: "sql/room_message_edit_history/room_message_edit_history_down_01.sql" },
|
||||
project_board => { name: "project_board", up: "sql/project_board/project_board_up_01.sql", down: "sql/project_board/project_board_down_01.sql" },
|
||||
project_board_column => { name: "project_board_column", up: "sql/project_board_column/project_board_column_up_01.sql", down: "sql/project_board_column/project_board_column_down_01.sql" },
|
||||
project_board_card => { name: "project_board_card", up: "sql/project_board_card/project_board_card_up_01.sql", down: "sql/project_board_card/project_board_card_down_01.sql" },
|
||||
pull_request_review_request => { name: "pull_request_review_request", up: "sql/pull_request_review_request/pull_request_review_request_up_01.sql", down: "sql/pull_request_review_request/pull_request_review_request_down_01.sql" },
|
||||
workspace => { name: "workspace", up: "sql/workspace/workspace_up_01.sql", down: "sql/workspace/workspace_down_01.sql" },
|
||||
project => { name: "project", up: "sql/project/project_up_01.sql", down: "sql/project/project_down_01.sql" },
|
||||
workspace_membership => { name: "workspace_membership", up: "sql/workspace_membership/workspace_membership_up_01.sql", down: "sql/workspace_membership/workspace_membership_down_01.sql" },
|
||||
workspace_billing => { name: "workspace_billing", up: "sql/workspace_billing/workspace_billing_up_01.sql", down: "sql/workspace_billing/workspace_billing_down_01.sql" },
|
||||
workspace_billing_history => { name: "workspace_billing_history", up: "sql/workspace_billing_history/workspace_billing_history_up_01.sql", down: "sql/workspace_billing_history/workspace_billing_history_down_01.sql" },
|
||||
project_skill => { name: "project_skill", up: "sql/project_skill/project_skill_up_01.sql", down: "sql/project_skill/project_skill_down_01.sql" },
|
||||
agent_task => { name: "agent_task", up: "sql/agent_task/agent_task_up_01.sql", down: "sql/agent_task/agent_task_down_01.sql" },
|
||||
admin_user => { name: "admin_user", up: "sql/admin_user/admin_user_up_01.sql", down: "sql/admin_user/admin_user_down_01.sql" },
|
||||
admin_role => { name: "admin_role", up: "sql/admin_role/admin_role_up_01.sql", down: "sql/admin_role/admin_role_down_01.sql" },
|
||||
admin_permission => { name: "admin_permission", up: "sql/admin_permission/admin_permission_up_01.sql", down: "sql/admin_permission/admin_permission_down_01.sql" },
|
||||
admin_user_role => { name: "admin_user_role", up: "sql/admin_user_role/admin_user_role_up_01.sql", down: "sql/admin_user_role/admin_user_role_down_01.sql" },
|
||||
admin_role_permission => { name: "admin_role_permission", up: "sql/admin_role_permission/admin_role_permission_up_01.sql", down: "sql/admin_role_permission/admin_role_permission_down_01.sql" },
|
||||
admin_audit_log => { name: "admin_audit_log", up: "sql/admin_audit_log/admin_audit_log_up_01.sql", down: "sql/admin_audit_log/admin_audit_log_down_01.sql" },
|
||||
admin_api_token => { name: "admin_api_token", up: "sql/admin_api_token/admin_api_token_up_01.sql", down: "sql/admin_api_token/admin_api_token_down_01.sql" },
|
||||
workspace_alert_config => { name: "workspace_alert_config", up: "sql/workspace_alert_config/workspace_alert_config_up_01.sql", down: "sql/workspace_alert_config/workspace_alert_config_down_01.sql" },
|
||||
room_attachment => { name: "room_attachment", up: "sql/room_attachment/room_attachment_up_01.sql", down: "sql/room_attachment/room_attachment_down_01.sql" },
|
||||
room_access => { name: "room_access", up: "sql/room_access/room_access_up_01.sql", down: "sql/room_access/room_access_down_01.sql" },
|
||||
room_user_state => { name: "room_user_state", up: "sql/room_user_state/room_user_state_up_01.sql", down: "sql/room_user_state/room_user_state_down_01.sql" },
|
||||
project_role_priority => { name: "project_role_priority", up: "sql/project_role_priority/project_role_priority_up_01.sql", down: "sql/project_role_priority/project_role_priority_down_01.sql" },
|
||||
ai_conversation => { name: "ai_conversation", up: "sql/ai_conversation/ai_conversation_up_01.sql", down: "sql/ai_conversation/ai_conversation_down_01.sql" },
|
||||
ai_message => { name: "ai_message", up: "sql/ai_message/ai_message_up_01.sql", down: "sql/ai_message/ai_message_down_01.sql" },
|
||||
ai_message_fork => { name: "ai_message_fork", up: "sql/ai_message_fork/ai_message_fork_up_01.sql", down: "sql/ai_message_fork/ai_message_fork_down_01.sql" },
|
||||
ai_shared_conversation => { name: "ai_shared_conversation", up: "sql/ai_shared_conversation/ai_shared_conversation_up_01.sql", down: "sql/ai_shared_conversation/ai_shared_conversation_down_01.sql" },
|
||||
ai_token_usage => { name: "ai_token_usage", up: "sql/ai_token_usage/ai_token_usage_up_01.sql", down: "sql/ai_token_usage/ai_token_usage_down_01.sql" },
|
||||
room_compact_summary => { name: "room_compact_summary", up: "sql/room_compact_summary/room_compact_summary_up_01.sql", down: "sql/room_compact_summary/room_compact_summary_down_01.sql" },
|
||||
user_billing_history => { name: "user_billing_history", up: "sql/user_billing_history/user_billing_history_up_01.sql", down: "sql/user_billing_history/user_billing_history_down_01.sql" },
|
||||
project_message_favorite => { name: "project_message_favorite", up: "sql/project_message_favorite/project_message_favorite_up_01.sql", down: "sql/project_message_favorite/project_message_favorite_down_01.sql" },
|
||||
ai_subagent_session => { name: "ai_subagent_session", up: "sql/ai_subagent_session/ai_subagent_session_up_01.sql", down: "sql/ai_subagent_session/ai_subagent_session_down_01.sql" },
|
||||
}
|
||||
|
||||
pub mod bootstrap;
|
||||
|
||||
@ -1,57 +0,0 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
pub struct Migration;
|
||||
|
||||
impl MigrationName for Migration {
|
||||
fn name(&self) -> &str {
|
||||
"project_message_favorite"
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.get_connection()
|
||||
.execute_unprepared(
|
||||
r#"
|
||||
create table if not exists project_message_favorite
|
||||
(
|
||||
uid uuid not null
|
||||
primary key,
|
||||
project uuid not null,
|
||||
room uuid not null,
|
||||
message uuid not null,
|
||||
user_uuid uuid not null,
|
||||
created_at timestamp with time zone not null
|
||||
);
|
||||
|
||||
create unique index if not exists idx_project_message_favorite_user_message
|
||||
on project_message_favorite (user_uuid, message);
|
||||
|
||||
create index if not exists idx_project_message_favorite_project_user
|
||||
on project_message_favorite (project, user_uuid, created_at desc);
|
||||
|
||||
create index if not exists idx_project_message_favorite_room
|
||||
on project_message_favorite (room);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.get_connection()
|
||||
.execute_unprepared(
|
||||
r#"
|
||||
drop index if exists idx_project_message_favorite_room;
|
||||
drop index if exists idx_project_message_favorite_project_user;
|
||||
drop index if exists idx_project_message_favorite_user_message;
|
||||
drop table if exists project_message_favorite;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
DROP INDEX IF EXISTS idx_admin_api_token_hash;
|
||||
DROP TABLE IF EXISTS admin_api_token;
|
||||
22
libs/migrate/sql/admin_api_token/admin_api_token_up_01.sql
Normal file
22
libs/migrate/sql/admin_api_token/admin_api_token_up_01.sql
Normal file
@ -0,0 +1,22 @@
|
||||
create table if not exists admin_api_token
|
||||
(
|
||||
id serial
|
||||
primary key,
|
||||
name varchar(255) not null,
|
||||
token_hash varchar(64) not null
|
||||
unique,
|
||||
token_prefix varchar(32) not null,
|
||||
permissions text[] default '{}'::text[] not null,
|
||||
created_by integer not null
|
||||
references admin_user
|
||||
on delete set null,
|
||||
created_at timestamp with time zone default now() not null,
|
||||
last_used_at timestamp with time zone,
|
||||
expires_at timestamp with time zone,
|
||||
is_active boolean default true not null
|
||||
);
|
||||
|
||||
comment on table admin_api_token is 'Admin API Token for programmatic access';
|
||||
|
||||
create index if not exists idx_admin_api_token_hash
|
||||
on admin_api_token (token_hash);
|
||||
@ -0,0 +1,5 @@
|
||||
DROP INDEX IF EXISTS idx_admin_audit_log_resource;
|
||||
DROP INDEX IF EXISTS idx_admin_audit_log_action;
|
||||
DROP INDEX IF EXISTS idx_admin_audit_log_created_at;
|
||||
DROP INDEX IF EXISTS idx_admin_audit_log_user_id;
|
||||
DROP TABLE IF EXISTS admin_audit_log;
|
||||
28
libs/migrate/sql/admin_audit_log/admin_audit_log_up_01.sql
Normal file
28
libs/migrate/sql/admin_audit_log/admin_audit_log_up_01.sql
Normal file
@ -0,0 +1,28 @@
|
||||
create table if not exists admin_audit_log
|
||||
(
|
||||
id bigserial
|
||||
primary key,
|
||||
user_id integer not null,
|
||||
username varchar(255) not null,
|
||||
action varchar(50) not null,
|
||||
resource varchar(255) not null,
|
||||
resource_id varchar(255),
|
||||
request_params jsonb,
|
||||
ip_address varchar(255),
|
||||
user_agent text,
|
||||
result varchar(20) default 'success'::character varying not null,
|
||||
error_message text,
|
||||
created_at timestamp with time zone default now() not null
|
||||
);
|
||||
|
||||
create index if not exists idx_admin_audit_log_user_id
|
||||
on admin_audit_log (user_id);
|
||||
|
||||
create index if not exists idx_admin_audit_log_created_at
|
||||
on admin_audit_log (created_at desc);
|
||||
|
||||
create index if not exists idx_admin_audit_log_action
|
||||
on admin_audit_log (action);
|
||||
|
||||
create index if not exists idx_admin_audit_log_resource
|
||||
on admin_audit_log (resource);
|
||||
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS admin_permission;
|
||||
10
libs/migrate/sql/admin_permission/admin_permission_up_01.sql
Normal file
10
libs/migrate/sql/admin_permission/admin_permission_up_01.sql
Normal file
@ -0,0 +1,10 @@
|
||||
create table if not exists admin_permission
|
||||
(
|
||||
id serial
|
||||
primary key,
|
||||
name varchar(255) not null,
|
||||
code varchar(255) not null
|
||||
unique,
|
||||
description text,
|
||||
created_at timestamp with time zone default now() not null
|
||||
);
|
||||
1
libs/migrate/sql/admin_role/admin_role_down_01.sql
Normal file
1
libs/migrate/sql/admin_role/admin_role_down_01.sql
Normal file
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS admin_role;
|
||||
9
libs/migrate/sql/admin_role/admin_role_up_01.sql
Normal file
9
libs/migrate/sql/admin_role/admin_role_up_01.sql
Normal file
@ -0,0 +1,9 @@
|
||||
create table if not exists admin_role
|
||||
(
|
||||
id serial
|
||||
primary key,
|
||||
name varchar(255) not null
|
||||
unique,
|
||||
description text,
|
||||
created_at timestamp with time zone default now() not null
|
||||
);
|
||||
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS admin_role_permission;
|
||||
@ -0,0 +1,10 @@
|
||||
create table if not exists admin_role_permission
|
||||
(
|
||||
role_id integer not null
|
||||
references admin_role
|
||||
on delete cascade,
|
||||
permission_id integer not null
|
||||
references admin_permission
|
||||
on delete cascade,
|
||||
primary key (role_id, permission_id)
|
||||
);
|
||||
2
libs/migrate/sql/admin_user/admin_user_down_01.sql
Normal file
2
libs/migrate/sql/admin_user/admin_user_down_01.sql
Normal file
@ -0,0 +1,2 @@
|
||||
DROP INDEX IF EXISTS idx_admin_user_username;
|
||||
DROP TABLE IF EXISTS admin_user;
|
||||
14
libs/migrate/sql/admin_user/admin_user_up_01.sql
Normal file
14
libs/migrate/sql/admin_user/admin_user_up_01.sql
Normal file
@ -0,0 +1,14 @@
|
||||
create table if not exists admin_user
|
||||
(
|
||||
id serial
|
||||
primary key,
|
||||
username varchar(255) not null
|
||||
unique,
|
||||
password_hash varchar(255) not null,
|
||||
is_active boolean default true not null,
|
||||
created_at timestamp with time zone default now() not null,
|
||||
updated_at timestamp with time zone default now() not null
|
||||
);
|
||||
|
||||
create index if not exists idx_admin_user_username
|
||||
on admin_user (username);
|
||||
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS admin_user_role;
|
||||
10
libs/migrate/sql/admin_user_role/admin_user_role_up_01.sql
Normal file
10
libs/migrate/sql/admin_user_role/admin_user_role_up_01.sql
Normal file
@ -0,0 +1,10 @@
|
||||
create table if not exists admin_user_role
|
||||
(
|
||||
user_id integer not null
|
||||
references admin_user
|
||||
on delete cascade,
|
||||
role_id integer not null
|
||||
references admin_role
|
||||
on delete cascade,
|
||||
primary key (user_id, role_id)
|
||||
);
|
||||
8
libs/migrate/sql/agent_task/agent_task_down_01.sql
Normal file
8
libs/migrate/sql/agent_task/agent_task_down_01.sql
Normal file
@ -0,0 +1,8 @@
|
||||
DROP INDEX IF EXISTS idx_agent_task_retry_count;
|
||||
DROP INDEX IF EXISTS idx_agent_task_issue;
|
||||
DROP INDEX IF EXISTS idx_agent_task_created_at;
|
||||
DROP INDEX IF EXISTS idx_agent_task_created_by;
|
||||
DROP INDEX IF EXISTS idx_agent_task_status;
|
||||
DROP INDEX IF EXISTS idx_agent_task_parent;
|
||||
DROP INDEX IF EXISTS idx_agent_task_project;
|
||||
DROP TABLE IF EXISTS agent_task;
|
||||
45
libs/migrate/sql/agent_task/agent_task_up_01.sql
Normal file
45
libs/migrate/sql/agent_task/agent_task_up_01.sql
Normal file
@ -0,0 +1,45 @@
|
||||
create table if not exists agent_task
|
||||
(
|
||||
id bigserial
|
||||
primary key,
|
||||
project_uuid uuid not null,
|
||||
parent_id bigint
|
||||
constraint fk_agent_task_parent
|
||||
references agent_task
|
||||
on delete set null,
|
||||
agent_type varchar(20) default 'react'::character varying not null,
|
||||
status varchar(20) default 'pending'::character varying not null,
|
||||
title varchar(255),
|
||||
input text not null,
|
||||
output text,
|
||||
error text,
|
||||
created_by uuid,
|
||||
created_at timestamp with time zone default now() not null,
|
||||
updated_at timestamp with time zone default now() not null,
|
||||
started_at timestamp with time zone,
|
||||
done_at timestamp with time zone,
|
||||
progress varchar(255),
|
||||
issue_id uuid,
|
||||
retry_count integer default 0
|
||||
);
|
||||
|
||||
create index if not exists idx_agent_task_project
|
||||
on agent_task (project_uuid);
|
||||
|
||||
create index if not exists idx_agent_task_parent
|
||||
on agent_task (parent_id);
|
||||
|
||||
create index if not exists idx_agent_task_status
|
||||
on agent_task (status);
|
||||
|
||||
create index if not exists idx_agent_task_created_by
|
||||
on agent_task (created_by);
|
||||
|
||||
create index if not exists idx_agent_task_created_at
|
||||
on agent_task (created_at);
|
||||
|
||||
create index if not exists idx_agent_task_issue
|
||||
on agent_task (issue_id);
|
||||
|
||||
create index if not exists idx_agent_task_retry_count
|
||||
on agent_task (retry_count);
|
||||
@ -0,0 +1,8 @@
|
||||
DROP INDEX IF EXISTS idx_ai_conv_project_uid;
|
||||
DROP INDEX IF EXISTS idx_ai_conv_access_vis;
|
||||
DROP INDEX IF EXISTS idx_ai_conv_project_created;
|
||||
DROP INDEX IF EXISTS idx_ai_conv_user_created;
|
||||
DROP INDEX IF EXISTS idx_ai_conv_scope;
|
||||
DROP INDEX IF EXISTS idx_ai_conv_project_id;
|
||||
DROP INDEX IF EXISTS idx_ai_conv_user_id;
|
||||
DROP TABLE IF EXISTS ai_conversation;
|
||||
49
libs/migrate/sql/ai_conversation/ai_conversation_up_01.sql
Normal file
49
libs/migrate/sql/ai_conversation/ai_conversation_up_01.sql
Normal file
@ -0,0 +1,49 @@
|
||||
create table if not exists ai_conversation
|
||||
(
|
||||
id uuid default gen_random_uuid() not null
|
||||
primary key,
|
||||
user_id uuid not null,
|
||||
project_id uuid
|
||||
constraint fk_ai_conv_project
|
||||
references project
|
||||
on delete cascade,
|
||||
scope varchar(16) not null,
|
||||
title varchar(512),
|
||||
model varchar(128) default 'gpt-4'::character varying not null,
|
||||
model_config jsonb,
|
||||
status varchar(32) default 'active'::character varying not null,
|
||||
root_message_id uuid,
|
||||
fork_count integer default 0 not null,
|
||||
is_shared boolean default false not null,
|
||||
message_count integer default 0 not null,
|
||||
token_usage_total integer,
|
||||
created_at timestamp with time zone default now() not null,
|
||||
updated_at timestamp with time zone default now() not null,
|
||||
access_visibility varchar(32) default 'owner'::character varying not null,
|
||||
can_ask varchar(32) default 'owner'::character varying not null,
|
||||
project_uid integer,
|
||||
model_uid uuid,
|
||||
model_name varchar(256)
|
||||
);
|
||||
|
||||
create index if not exists idx_ai_conv_user_id
|
||||
on ai_conversation (user_id);
|
||||
|
||||
create index if not exists idx_ai_conv_project_id
|
||||
on ai_conversation (project_id);
|
||||
|
||||
create index if not exists idx_ai_conv_scope
|
||||
on ai_conversation (scope);
|
||||
|
||||
create index if not exists idx_ai_conv_user_created
|
||||
on ai_conversation (user_id asc, created_at desc);
|
||||
|
||||
create index if not exists idx_ai_conv_project_created
|
||||
on ai_conversation (project_id asc, created_at desc);
|
||||
|
||||
create index if not exists idx_ai_conv_access_vis
|
||||
on ai_conversation (access_visibility);
|
||||
|
||||
create index if not exists idx_ai_conv_project_uid
|
||||
on ai_conversation (project_id, project_uid)
|
||||
where (project_uid IS NOT NULL);
|
||||
3
libs/migrate/sql/ai_message/ai_message_down_01.sql
Normal file
3
libs/migrate/sql/ai_message/ai_message_down_01.sql
Normal file
@ -0,0 +1,3 @@
|
||||
DROP INDEX IF EXISTS idx_ai_msg_parent;
|
||||
DROP INDEX IF EXISTS idx_ai_msg_conv;
|
||||
DROP TABLE IF EXISTS ai_message;
|
||||
30
libs/migrate/sql/ai_message/ai_message_up_01.sql
Normal file
30
libs/migrate/sql/ai_message/ai_message_up_01.sql
Normal file
@ -0,0 +1,30 @@
|
||||
create table if not exists ai_message
|
||||
(
|
||||
id uuid default gen_random_uuid() not null
|
||||
primary key,
|
||||
conversation_id uuid not null
|
||||
constraint fk_ai_msg_conv
|
||||
references ai_conversation
|
||||
on delete cascade,
|
||||
parent_message_id uuid
|
||||
constraint fk_ai_msg_parent
|
||||
references ai_message
|
||||
on delete set null,
|
||||
role varchar(16) not null,
|
||||
content jsonb not null,
|
||||
model varchar(128),
|
||||
is_fork_origin boolean default false not null,
|
||||
stop_reason varchar(32),
|
||||
input_tokens integer,
|
||||
output_tokens integer,
|
||||
latency_ms integer,
|
||||
metadata jsonb,
|
||||
room_id uuid,
|
||||
created_at timestamp with time zone default now() not null
|
||||
);
|
||||
|
||||
create index if not exists idx_ai_msg_conv
|
||||
on ai_message (conversation_id, created_at);
|
||||
|
||||
create index if not exists idx_ai_msg_parent
|
||||
on ai_message (parent_message_id);
|
||||
@ -0,0 +1,3 @@
|
||||
DROP INDEX IF EXISTS idx_ai_fork_fork;
|
||||
DROP INDEX IF EXISTS idx_ai_fork_source;
|
||||
DROP TABLE IF EXISTS ai_message_fork;
|
||||
20
libs/migrate/sql/ai_message_fork/ai_message_fork_up_01.sql
Normal file
20
libs/migrate/sql/ai_message_fork/ai_message_fork_up_01.sql
Normal file
@ -0,0 +1,20 @@
|
||||
create table if not exists ai_message_fork
|
||||
(
|
||||
id uuid default gen_random_uuid() not null
|
||||
primary key,
|
||||
source_message_id uuid not null
|
||||
constraint fk_ai_fork_source
|
||||
references ai_message
|
||||
on delete cascade,
|
||||
fork_message_id uuid not null
|
||||
constraint fk_ai_fork_fork
|
||||
references ai_message
|
||||
on delete cascade,
|
||||
created_at timestamp with time zone default now() not null
|
||||
);
|
||||
|
||||
create index if not exists idx_ai_fork_source
|
||||
on ai_message_fork (source_message_id);
|
||||
|
||||
create index if not exists idx_ai_fork_fork
|
||||
on ai_message_fork (fork_message_id);
|
||||
2
libs/migrate/sql/ai_model/ai_model_down_01.sql
Normal file
2
libs/migrate/sql/ai_model/ai_model_down_01.sql
Normal file
@ -0,0 +1,2 @@
|
||||
DROP INDEX IF EXISTS idx_ai_model_provider_id;
|
||||
DROP TABLE IF EXISTS ai_model;
|
||||
19
libs/migrate/sql/ai_model/ai_model_up_01.sql
Normal file
19
libs/migrate/sql/ai_model/ai_model_up_01.sql
Normal file
@ -0,0 +1,19 @@
|
||||
create table if not exists ai_model
|
||||
(
|
||||
id uuid not null
|
||||
primary key,
|
||||
provider_id uuid not null,
|
||||
name varchar(255) not null,
|
||||
modality varchar(255) not null,
|
||||
capability varchar(255) not null,
|
||||
context_length bigint not null,
|
||||
max_output_tokens bigint,
|
||||
training_cutoff timestamp with time zone,
|
||||
is_open_source boolean default false not null,
|
||||
status varchar(255) not null,
|
||||
created_at timestamp with time zone not null,
|
||||
updated_at timestamp with time zone not null
|
||||
);
|
||||
|
||||
create index if not exists idx_ai_model_provider_id
|
||||
on ai_model (provider_id);
|
||||
@ -0,0 +1,2 @@
|
||||
DROP INDEX IF EXISTS idx_ai_model_capability_model_version_id;
|
||||
DROP TABLE IF EXISTS ai_model_capability;
|
||||
@ -0,0 +1,12 @@
|
||||
create table if not exists ai_model_capability
|
||||
(
|
||||
id bigserial
|
||||
primary key,
|
||||
model_version_id uuid not null,
|
||||
capability varchar(255) not null,
|
||||
is_supported boolean default false not null,
|
||||
created_at timestamp with time zone not null
|
||||
);
|
||||
|
||||
create index if not exists idx_ai_model_capability_model_version_id
|
||||
on ai_model_capability (model_version_id);
|
||||
@ -0,0 +1,2 @@
|
||||
DROP INDEX IF EXISTS idx_ai_model_parameter_profile_model_version_id;
|
||||
DROP TABLE IF EXISTS ai_model_parameter_profile;
|
||||
@ -0,0 +1,16 @@
|
||||
create table if not exists ai_model_parameter_profile
|
||||
(
|
||||
id bigserial
|
||||
primary key,
|
||||
model_version_id uuid not null
|
||||
unique,
|
||||
temperature_min double precision not null,
|
||||
temperature_max double precision not null,
|
||||
top_p_min double precision not null,
|
||||
top_p_max double precision not null,
|
||||
frequency_penalty_supported boolean default false not null,
|
||||
presence_penalty_supported boolean default false not null
|
||||
);
|
||||
|
||||
create unique index if not exists idx_ai_model_parameter_profile_model_version_id
|
||||
on ai_model_parameter_profile (model_version_id);
|
||||
@ -0,0 +1,2 @@
|
||||
DROP INDEX IF EXISTS idx_ai_model_pricing_model_version_id;
|
||||
DROP TABLE IF EXISTS ai_model_pricing;
|
||||
13
libs/migrate/sql/ai_model_pricing/ai_model_pricing_up_01.sql
Normal file
13
libs/migrate/sql/ai_model_pricing/ai_model_pricing_up_01.sql
Normal file
@ -0,0 +1,13 @@
|
||||
create table if not exists ai_model_pricing
|
||||
(
|
||||
id bigserial
|
||||
primary key,
|
||||
model_version_id uuid not null,
|
||||
input_price_per_1k_tokens varchar(255) not null,
|
||||
output_price_per_1k_tokens varchar(255) not null,
|
||||
currency varchar(255) not null,
|
||||
effective_from timestamp with time zone not null
|
||||
);
|
||||
|
||||
create index if not exists idx_ai_model_pricing_model_version_id
|
||||
on ai_model_pricing (model_version_id);
|
||||
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS ai_model_provider;
|
||||
@ -0,0 +1,11 @@
|
||||
create table if not exists ai_model_provider
|
||||
(
|
||||
id uuid not null
|
||||
primary key,
|
||||
name varchar(255) not null,
|
||||
display_name varchar(255) not null,
|
||||
website varchar(255),
|
||||
status varchar(255) not null,
|
||||
created_at timestamp with time zone not null,
|
||||
updated_at timestamp with time zone not null
|
||||
);
|
||||
@ -0,0 +1,2 @@
|
||||
DROP INDEX IF EXISTS idx_ai_model_version_model_id;
|
||||
DROP TABLE IF EXISTS ai_model_version;
|
||||
15
libs/migrate/sql/ai_model_version/ai_model_version_up_01.sql
Normal file
15
libs/migrate/sql/ai_model_version/ai_model_version_up_01.sql
Normal file
@ -0,0 +1,15 @@
|
||||
create table if not exists ai_model_version
|
||||
(
|
||||
id uuid not null
|
||||
primary key,
|
||||
model_id uuid not null,
|
||||
version varchar(255) not null,
|
||||
release_date timestamp with time zone,
|
||||
change_log text,
|
||||
is_default boolean default false not null,
|
||||
status varchar(255) not null,
|
||||
created_at timestamp with time zone not null
|
||||
);
|
||||
|
||||
create index if not exists idx_ai_model_version_model_id
|
||||
on ai_model_version (model_id);
|
||||
2
libs/migrate/sql/ai_session/ai_session_down_01.sql
Normal file
2
libs/migrate/sql/ai_session/ai_session_down_01.sql
Normal file
@ -0,0 +1,2 @@
|
||||
DROP INDEX IF EXISTS idx_ai_session_room;
|
||||
DROP TABLE IF EXISTS ai_session;
|
||||
19
libs/migrate/sql/ai_session/ai_session_up_01.sql
Normal file
19
libs/migrate/sql/ai_session/ai_session_up_01.sql
Normal file
@ -0,0 +1,19 @@
|
||||
create table if not exists ai_session
|
||||
(
|
||||
id uuid not null
|
||||
primary key,
|
||||
room uuid not null,
|
||||
model uuid not null,
|
||||
version uuid not null,
|
||||
token_input bigint default 0 not null,
|
||||
token_output bigint default 0 not null,
|
||||
latency_ms bigint,
|
||||
cost double precision,
|
||||
currency varchar(255),
|
||||
error_message text,
|
||||
error_code varchar(255),
|
||||
created_at timestamp with time zone not null
|
||||
);
|
||||
|
||||
create index if not exists idx_ai_session_room
|
||||
on ai_session (room);
|
||||
@ -0,0 +1,3 @@
|
||||
DROP INDEX IF EXISTS idx_ai_share_token;
|
||||
DROP INDEX IF EXISTS idx_ai_share_conv;
|
||||
DROP TABLE IF EXISTS ai_shared_conversation;
|
||||
@ -0,0 +1,21 @@
|
||||
create table if not exists ai_shared_conversation
|
||||
(
|
||||
id uuid default gen_random_uuid() not null
|
||||
primary key,
|
||||
conversation_id uuid not null
|
||||
constraint fk_ai_share_conv
|
||||
references ai_conversation
|
||||
on delete cascade,
|
||||
share_token varchar(128) not null
|
||||
unique,
|
||||
created_by uuid not null,
|
||||
view_count integer default 0 not null,
|
||||
created_at timestamp with time zone default now() not null,
|
||||
expires_at timestamp with time zone
|
||||
);
|
||||
|
||||
create index if not exists idx_ai_share_conv
|
||||
on ai_shared_conversation (conversation_id);
|
||||
|
||||
create index if not exists idx_ai_share_token
|
||||
on ai_shared_conversation (share_token);
|
||||
@ -0,0 +1,4 @@
|
||||
DROP INDEX IF EXISTS idx_ai_subagent_session_message;
|
||||
DROP INDEX IF EXISTS idx_ai_subagent_session_children;
|
||||
DROP INDEX IF EXISTS idx_ai_subagent_session_conv;
|
||||
DROP TABLE IF EXISTS ai_subagent_session;
|
||||
@ -1,20 +1,3 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
pub struct Migration;
|
||||
|
||||
impl MigrationName for Migration {
|
||||
fn name(&self) -> &str {
|
||||
"ai_subagent_session"
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.get_connection()
|
||||
.execute_unprepared(
|
||||
r#"
|
||||
create table if not exists ai_subagent_session
|
||||
(
|
||||
id uuid not null primary key,
|
||||
@ -40,24 +23,3 @@ create index if not exists idx_ai_subagent_session_children
|
||||
|
||||
create index if not exists idx_ai_subagent_session_message
|
||||
on ai_subagent_session (message_id);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.get_connection()
|
||||
.execute_unprepared(
|
||||
r#"
|
||||
drop index if exists idx_ai_subagent_session_message;
|
||||
drop index if exists idx_ai_subagent_session_children;
|
||||
drop index if exists idx_ai_subagent_session_conv;
|
||||
drop table if exists ai_subagent_session;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
DROP INDEX IF EXISTS idx_ai_token_recorded;
|
||||
DROP INDEX IF EXISTS idx_ai_token_conv;
|
||||
DROP INDEX IF EXISTS idx_ai_token_user;
|
||||
DROP TABLE IF EXISTS ai_token_usage;
|
||||
21
libs/migrate/sql/ai_token_usage/ai_token_usage_up_01.sql
Normal file
21
libs/migrate/sql/ai_token_usage/ai_token_usage_up_01.sql
Normal file
@ -0,0 +1,21 @@
|
||||
create table if not exists ai_token_usage
|
||||
(
|
||||
id uuid default gen_random_uuid() not null
|
||||
primary key,
|
||||
user_id uuid not null,
|
||||
conversation_id uuid,
|
||||
model varchar(128) not null,
|
||||
input_tokens integer not null,
|
||||
output_tokens integer not null,
|
||||
cost_usd numeric(10, 6),
|
||||
recorded_at timestamp with time zone default now() not null
|
||||
);
|
||||
|
||||
create index if not exists idx_ai_token_user
|
||||
on ai_token_usage (user_id);
|
||||
|
||||
create index if not exists idx_ai_token_conv
|
||||
on ai_token_usage (conversation_id);
|
||||
|
||||
create index if not exists idx_ai_token_recorded
|
||||
on ai_token_usage (recorded_at);
|
||||
1
libs/migrate/sql/ai_tool_auth/ai_tool_auth_down_01.sql
Normal file
1
libs/migrate/sql/ai_tool_auth/ai_tool_auth_down_01.sql
Normal file
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS ai_tool_auth;
|
||||
17
libs/migrate/sql/ai_tool_auth/ai_tool_auth_up_01.sql
Normal file
17
libs/migrate/sql/ai_tool_auth/ai_tool_auth_up_01.sql
Normal file
@ -0,0 +1,17 @@
|
||||
create table if not exists ai_tool_auth
|
||||
(
|
||||
session uuid not null,
|
||||
tool_call_id varchar(255) not null,
|
||||
method varchar(255) not null,
|
||||
arguments text not null,
|
||||
decision boolean default false not null,
|
||||
reason varchar(255) not null,
|
||||
decision_by uuid not null,
|
||||
decision_comment text,
|
||||
logs jsonb not null,
|
||||
expires_at timestamp with time zone,
|
||||
authorized_at timestamp with time zone,
|
||||
created_at timestamp with time zone not null,
|
||||
updated_at timestamp with time zone not null,
|
||||
primary key (session, tool_call_id)
|
||||
);
|
||||
3
libs/migrate/sql/ai_tool_call/ai_tool_call_down_01.sql
Normal file
3
libs/migrate/sql/ai_tool_call/ai_tool_call_down_01.sql
Normal file
@ -0,0 +1,3 @@
|
||||
DROP INDEX IF EXISTS idx_ai_tool_call_status;
|
||||
DROP INDEX IF EXISTS idx_ai_tool_call_session;
|
||||
DROP TABLE IF EXISTS ai_tool_call;
|
||||
24
libs/migrate/sql/ai_tool_call/ai_tool_call_up_01.sql
Normal file
24
libs/migrate/sql/ai_tool_call/ai_tool_call_up_01.sql
Normal file
@ -0,0 +1,24 @@
|
||||
create table if not exists ai_tool_call
|
||||
(
|
||||
tool_call_id varchar(255) not null,
|
||||
session uuid not null,
|
||||
tool_name varchar(255) not null,
|
||||
caller uuid not null,
|
||||
arguments jsonb not null,
|
||||
result jsonb not null,
|
||||
status varchar(255) not null,
|
||||
execution_time_ms bigint,
|
||||
error_message text,
|
||||
error_stack text,
|
||||
retry_count integer default 0 not null,
|
||||
created_at timestamp with time zone not null,
|
||||
completed_at timestamp with time zone,
|
||||
updated_at timestamp with time zone not null,
|
||||
primary key (tool_call_id, session)
|
||||
);
|
||||
|
||||
create index if not exists idx_ai_tool_call_session
|
||||
on ai_tool_call (session);
|
||||
|
||||
create index if not exists idx_ai_tool_call_status
|
||||
on ai_tool_call (status);
|
||||
3
libs/migrate/sql/bootstrap/bootstrap_down_01.sql
Normal file
3
libs/migrate/sql/bootstrap/bootstrap_down_01.sql
Normal file
@ -0,0 +1,3 @@
|
||||
DROP TRIGGER IF EXISTS room_message_tsv_update ON room_message;
|
||||
DROP FUNCTION IF EXISTS room_message_tsv_trigger();
|
||||
DROP TYPE IF EXISTS notification_type;
|
||||
33
libs/migrate/sql/bootstrap/bootstrap_up_01.sql
Normal file
33
libs/migrate/sql/bootstrap/bootstrap_up_01.sql
Normal file
@ -0,0 +1,33 @@
|
||||
CREATE OR REPLACE FUNCTION room_message_tsv_trigger()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
BEGIN
|
||||
NEW.content_tsv := to_tsvector('english', NEW.content);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$function$;
|
||||
CREATE OR REPLACE TRIGGER room_message_tsv_update
|
||||
BEFORE INSERT OR UPDATE
|
||||
ON room_message
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE room_message_tsv_trigger();
|
||||
|
||||
DO
|
||||
$$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1
|
||||
FROM pg_type
|
||||
WHERE typname = 'notification_type'
|
||||
AND typtype = 'e') THEN
|
||||
CREATE TYPE notification_type AS ENUM (
|
||||
'mention',
|
||||
'invitation',
|
||||
'role_change',
|
||||
'room_created',
|
||||
'room_deleted',
|
||||
'system_announcement'
|
||||
);
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
File diff suppressed because it is too large
Load Diff
4
libs/migrate/sql/issue/issue_down_01.sql
Normal file
4
libs/migrate/sql/issue/issue_down_01.sql
Normal file
@ -0,0 +1,4 @@
|
||||
DROP INDEX IF EXISTS idx_issue_state;
|
||||
DROP INDEX IF EXISTS idx_issue_author;
|
||||
DROP INDEX IF EXISTS idx_issue_project;
|
||||
DROP TABLE IF EXISTS issue;
|
||||
25
libs/migrate/sql/issue/issue_up_01.sql
Normal file
25
libs/migrate/sql/issue/issue_up_01.sql
Normal file
@ -0,0 +1,25 @@
|
||||
create table if not exists issue
|
||||
(
|
||||
id uuid not null
|
||||
primary key,
|
||||
project uuid not null,
|
||||
number bigint not null,
|
||||
title varchar(255) not null,
|
||||
body text,
|
||||
state varchar(255) not null,
|
||||
author uuid not null,
|
||||
milestone varchar(255),
|
||||
created_at timestamp with time zone not null,
|
||||
updated_at timestamp with time zone not null,
|
||||
closed_at timestamp with time zone,
|
||||
created_by_ai boolean default false not null
|
||||
);
|
||||
|
||||
create index if not exists idx_issue_project
|
||||
on issue (project);
|
||||
|
||||
create index if not exists idx_issue_author
|
||||
on issue (author);
|
||||
|
||||
create index if not exists idx_issue_state
|
||||
on issue (state);
|
||||
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS issue_assignee;
|
||||
7
libs/migrate/sql/issue_assignee/issue_assignee_up_01.sql
Normal file
7
libs/migrate/sql/issue_assignee/issue_assignee_up_01.sql
Normal file
@ -0,0 +1,7 @@
|
||||
create table if not exists issue_assignee
|
||||
(
|
||||
issue uuid not null,
|
||||
"user" uuid not null,
|
||||
assigned_at timestamp with time zone not null,
|
||||
primary key (issue, "user")
|
||||
);
|
||||
2
libs/migrate/sql/issue_comment/issue_comment_down_01.sql
Normal file
2
libs/migrate/sql/issue_comment/issue_comment_down_01.sql
Normal file
@ -0,0 +1,2 @@
|
||||
DROP INDEX IF EXISTS idx_issue_comment_issue;
|
||||
DROP TABLE IF EXISTS issue_comment;
|
||||
13
libs/migrate/sql/issue_comment/issue_comment_up_01.sql
Normal file
13
libs/migrate/sql/issue_comment/issue_comment_up_01.sql
Normal file
@ -0,0 +1,13 @@
|
||||
create table if not exists issue_comment
|
||||
(
|
||||
id bigserial
|
||||
primary key,
|
||||
issue uuid not null,
|
||||
author uuid not null,
|
||||
body text not null,
|
||||
created_at timestamp with time zone not null,
|
||||
updated_at timestamp with time zone not null
|
||||
);
|
||||
|
||||
create index if not exists idx_issue_comment_issue
|
||||
on issue_comment (issue);
|
||||
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS issue_comment_reaction;
|
||||
@ -0,0 +1,8 @@
|
||||
create table if not exists issue_comment_reaction
|
||||
(
|
||||
comment_id bigint not null,
|
||||
user_uuid uuid not null,
|
||||
reaction varchar(255) not null,
|
||||
created_at timestamp with time zone not null,
|
||||
primary key (comment_id, user_uuid, reaction)
|
||||
);
|
||||
1
libs/migrate/sql/issue_label/issue_label_down_01.sql
Normal file
1
libs/migrate/sql/issue_label/issue_label_down_01.sql
Normal file
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS issue_label;
|
||||
7
libs/migrate/sql/issue_label/issue_label_up_01.sql
Normal file
7
libs/migrate/sql/issue_label/issue_label_up_01.sql
Normal file
@ -0,0 +1,7 @@
|
||||
create table if not exists issue_label
|
||||
(
|
||||
issue uuid not null,
|
||||
label bigint not null,
|
||||
relation_at timestamp with time zone not null,
|
||||
primary key (issue, label)
|
||||
);
|
||||
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS issue_pull_request;
|
||||
@ -0,0 +1,8 @@
|
||||
create table if not exists issue_pull_request
|
||||
(
|
||||
issue uuid not null,
|
||||
repo uuid not null,
|
||||
number bigint not null,
|
||||
relation_at timestamp with time zone not null,
|
||||
primary key (issue, repo, number)
|
||||
);
|
||||
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS issue_reaction;
|
||||
8
libs/migrate/sql/issue_reaction/issue_reaction_up_01.sql
Normal file
8
libs/migrate/sql/issue_reaction/issue_reaction_up_01.sql
Normal file
@ -0,0 +1,8 @@
|
||||
create table if not exists issue_reaction
|
||||
(
|
||||
issue_uuid uuid not null,
|
||||
user_uuid uuid not null,
|
||||
reaction varchar(255) not null,
|
||||
created_at timestamp with time zone not null,
|
||||
primary key (issue_uuid, user_uuid, reaction)
|
||||
);
|
||||
1
libs/migrate/sql/issue_repo/issue_repo_down_01.sql
Normal file
1
libs/migrate/sql/issue_repo/issue_repo_down_01.sql
Normal file
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS issue_repo;
|
||||
7
libs/migrate/sql/issue_repo/issue_repo_up_01.sql
Normal file
7
libs/migrate/sql/issue_repo/issue_repo_up_01.sql
Normal file
@ -0,0 +1,7 @@
|
||||
create table if not exists issue_repo
|
||||
(
|
||||
issue uuid not null,
|
||||
repo uuid not null,
|
||||
relation_at timestamp with time zone not null,
|
||||
primary key (issue, repo)
|
||||
);
|
||||
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS issue_subscriber;
|
||||
@ -0,0 +1,8 @@
|
||||
create table if not exists issue_subscriber
|
||||
(
|
||||
issue uuid not null,
|
||||
"user" uuid not null,
|
||||
subscribed boolean default true not null,
|
||||
created_at timestamp with time zone not null,
|
||||
primary key (issue, "user")
|
||||
);
|
||||
2
libs/migrate/sql/label/label_down_01.sql
Normal file
2
libs/migrate/sql/label/label_down_01.sql
Normal file
@ -0,0 +1,2 @@
|
||||
DROP INDEX IF EXISTS idx_label_project;
|
||||
DROP TABLE IF EXISTS label;
|
||||
11
libs/migrate/sql/label/label_up_01.sql
Normal file
11
libs/migrate/sql/label/label_up_01.sql
Normal file
@ -0,0 +1,11 @@
|
||||
create table if not exists label
|
||||
(
|
||||
id bigserial
|
||||
primary key,
|
||||
project_uuid uuid not null,
|
||||
name varchar(255) not null,
|
||||
color varchar(255) not null
|
||||
);
|
||||
|
||||
create index if not exists idx_label_project
|
||||
on label (project_uuid);
|
||||
3
libs/migrate/sql/notify/notify_down_01.sql
Normal file
3
libs/migrate/sql/notify/notify_down_01.sql
Normal file
@ -0,0 +1,3 @@
|
||||
DROP INDEX IF EXISTS idx_notify_created_at;
|
||||
DROP INDEX IF EXISTS idx_notify_user;
|
||||
DROP TABLE IF EXISTS notify;
|
||||
20
libs/migrate/sql/notify/notify_up_01.sql
Normal file
20
libs/migrate/sql/notify/notify_up_01.sql
Normal file
@ -0,0 +1,20 @@
|
||||
create table if not exists notify
|
||||
(
|
||||
id bigserial
|
||||
primary key,
|
||||
user_uuid uuid not null,
|
||||
title varchar(255) not null,
|
||||
description text,
|
||||
content text not null,
|
||||
url varchar(255),
|
||||
kind integer not null,
|
||||
read_at timestamp with time zone,
|
||||
deleted_at timestamp with time zone,
|
||||
created_at timestamp with time zone not null
|
||||
);
|
||||
|
||||
create index if not exists idx_notify_user
|
||||
on notify (user_uuid);
|
||||
|
||||
create index if not exists idx_notify_created_at
|
||||
on notify (created_at);
|
||||
6
libs/migrate/sql/project/project_down_01.sql
Normal file
6
libs/migrate/sql/project/project_down_01.sql
Normal file
@ -0,0 +1,6 @@
|
||||
DROP INDEX IF EXISTS idx_workspace_deleted_at;
|
||||
DROP INDEX IF EXISTS idx_workspace_slug;
|
||||
DROP INDEX IF EXISTS idx_project_workspace_id;
|
||||
DROP INDEX IF EXISTS idx_project_created_by;
|
||||
DROP INDEX IF EXISTS idx_project_name;
|
||||
DROP TABLE IF EXISTS project;
|
||||
32
libs/migrate/sql/project/project_up_01.sql
Normal file
32
libs/migrate/sql/project/project_up_01.sql
Normal file
@ -0,0 +1,32 @@
|
||||
create table if not exists project
|
||||
(
|
||||
id uuid not null
|
||||
primary key,
|
||||
name varchar(255) not null,
|
||||
display_name varchar(255) not null,
|
||||
avatar_url varchar(255),
|
||||
description text,
|
||||
is_public boolean default false not null,
|
||||
created_by uuid not null,
|
||||
created_at timestamp with time zone not null,
|
||||
updated_at timestamp with time zone not null,
|
||||
workspace_id uuid
|
||||
references workspace
|
||||
on delete set null
|
||||
);
|
||||
|
||||
create index if not exists idx_project_name
|
||||
on project (name);
|
||||
|
||||
create index if not exists idx_project_created_by
|
||||
on project (created_by);
|
||||
|
||||
create index if not exists idx_project_workspace_id
|
||||
on project (workspace_id)
|
||||
where (workspace_id IS NOT NULL);
|
||||
|
||||
create unique index if not exists idx_workspace_slug
|
||||
on workspace (slug);
|
||||
|
||||
create index if not exists idx_workspace_deleted_at
|
||||
on workspace (deleted_at);
|
||||
@ -0,0 +1,3 @@
|
||||
DROP INDEX IF EXISTS idx_project_access_log_created_at;
|
||||
DROP INDEX IF EXISTS idx_project_access_log_project;
|
||||
DROP TABLE IF EXISTS project_access_log;
|
||||
@ -0,0 +1,17 @@
|
||||
create table if not exists project_access_log
|
||||
(
|
||||
id bigserial
|
||||
primary key,
|
||||
project uuid not null,
|
||||
actor_uid uuid,
|
||||
action varchar(255) not null,
|
||||
ip_address varchar(255),
|
||||
user_agent varchar(255),
|
||||
created_at timestamp with time zone not null
|
||||
);
|
||||
|
||||
create index if not exists idx_project_access_log_project
|
||||
on project_access_log (project);
|
||||
|
||||
create index if not exists idx_project_access_log_created_at
|
||||
on project_access_log (created_at);
|
||||
@ -0,0 +1,4 @@
|
||||
DROP INDEX IF EXISTS idx_project_activity_event_type;
|
||||
DROP INDEX IF EXISTS idx_project_activity_created_at;
|
||||
DROP INDEX IF EXISTS idx_project_activity_project;
|
||||
DROP TABLE IF EXISTS project_activity;
|
||||
25
libs/migrate/sql/project_activity/project_activity_up_01.sql
Normal file
25
libs/migrate/sql/project_activity/project_activity_up_01.sql
Normal file
@ -0,0 +1,25 @@
|
||||
create table if not exists project_activity
|
||||
(
|
||||
id bigserial
|
||||
primary key,
|
||||
project uuid not null,
|
||||
repo uuid,
|
||||
actor uuid not null,
|
||||
event_type varchar(50) not null,
|
||||
event_id uuid,
|
||||
event_sub_id bigint,
|
||||
title varchar(500) not null,
|
||||
content text,
|
||||
metadata jsonb,
|
||||
is_private boolean default false not null,
|
||||
created_at timestamp with time zone not null
|
||||
);
|
||||
|
||||
create index if not exists idx_project_activity_project
|
||||
on project_activity (project);
|
||||
|
||||
create index if not exists idx_project_activity_created_at
|
||||
on project_activity (created_at desc);
|
||||
|
||||
create index if not exists idx_project_activity_event_type
|
||||
on project_activity (event_type);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user