OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
73
app/Domain/Projects/Tools/AddProjectTool.php
Normal file
73
app/Domain/Projects/Tools/AddProjectTool.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Projects\Tools;
|
||||
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
use Laravel\Mcp\Server\Tools\ToolInputSchema;
|
||||
use Laravel\Mcp\Server\Tools\ToolResult;
|
||||
use Leantime\Domain\Modulemanager\Services\Modulemanager;
|
||||
use Leantime\Domain\Projects\Services\Projects;
|
||||
|
||||
/**
|
||||
* Creates a new project with the specified details.
|
||||
*/
|
||||
class AddProjectTool extends Tool
|
||||
{
|
||||
public function __construct(
|
||||
private Projects $projectService,
|
||||
private Modulemanager $moduleManager,
|
||||
) {}
|
||||
|
||||
public function schema(ToolInputSchema $schema): ToolInputSchema
|
||||
{
|
||||
return $schema
|
||||
->string('name')->description('Name of the project.')
|
||||
->required()
|
||||
->integer('clientId')->description('ID of the client for this project.')
|
||||
->required()
|
||||
->string('details')->description('Project description.')
|
||||
->string('start')->description('Start date in ISO 8601 format.')
|
||||
->string('end')->description('End date in ISO 8601 format.')
|
||||
->integer('hourBudget')->description('Hour budget for the project.')
|
||||
->integer('parent')->description('ID of the parent program or plan (only works if PgmPro plugin is active).');
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'addProject';
|
||||
}
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return 'Creates a new project with the specified details.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the tool request.
|
||||
*/
|
||||
public function handle(array $arguments): ToolResult
|
||||
{
|
||||
$values = [
|
||||
'name' => $arguments['name'],
|
||||
'details' => ($arguments['details'] ?? ''),
|
||||
'clientId' => (int) ($arguments['clientId'] ?? 0),
|
||||
'hourBudget' => ($arguments['hourBudget'] ?? null),
|
||||
'start' => ($arguments['start'] ?? null),
|
||||
'end' => ($arguments['end'] ?? null),
|
||||
];
|
||||
|
||||
// Check if PgmPro plugin is active and parent is specified
|
||||
$parent = ($arguments['parent'] ?? null);
|
||||
if ($parent && $this->moduleManager->isModuleAvailable('pgmPro')) {
|
||||
$values['parent'] = $parent;
|
||||
}
|
||||
|
||||
$projectId = $this->projectService->addProject($values);
|
||||
|
||||
if ($projectId) {
|
||||
return ToolResult::text("Project created successfully with ID: $projectId");
|
||||
}
|
||||
|
||||
return ToolResult::error('Failed to create project. Please check the provided information.');
|
||||
}
|
||||
}
|
||||
61
app/Domain/Projects/Tools/DeleteProjectTool.php
Normal file
61
app/Domain/Projects/Tools/DeleteProjectTool.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Projects\Tools;
|
||||
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
use Laravel\Mcp\Server\Tools\ToolInputSchema;
|
||||
use Laravel\Mcp\Server\Tools\ToolResult;
|
||||
use Leantime\Domain\Projects\Services\Projects;
|
||||
|
||||
/**
|
||||
* 删除项目(高风险写操作)。
|
||||
*
|
||||
* 会删除项目本身 + 所有用户关系。调用方(AI)应先 getProject 列出项目信息
|
||||
* 并征得用户明确确认。
|
||||
*/
|
||||
class DeleteProjectTool extends Tool
|
||||
{
|
||||
public function __construct(
|
||||
private Projects $projectService,
|
||||
) {}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'deleteProject';
|
||||
}
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return '删除一个项目(高风险操作,不可恢复)。会删除项目及其用户关联。调用前必须先 getProject 列出项目信息并征得用户明确确认。';
|
||||
}
|
||||
|
||||
public function schema(ToolInputSchema $schema): ToolInputSchema
|
||||
{
|
||||
return $schema
|
||||
->integer('id')->description('要删除的项目 ID。')->required()
|
||||
->boolean('confirmed')->description('用户是否已明确确认删除。必须为 true 才执行。')->required();
|
||||
}
|
||||
|
||||
public function handle(array $arguments): ToolResult
|
||||
{
|
||||
$id = (int) ($arguments['id'] ?? 0);
|
||||
$confirmed = (bool) ($arguments['confirmed'] ?? false);
|
||||
|
||||
if (! $confirmed) {
|
||||
return ToolResult::error('未确认删除。请先 getProject 列出项目信息,征得用户确认后再传 confirmed=true。');
|
||||
}
|
||||
|
||||
$project = $this->projectService->getProject($id);
|
||||
if (! isset($project['id'])) {
|
||||
return ToolResult::error("项目不存在或无权访问:{$id}");
|
||||
}
|
||||
|
||||
$name = $project['name'] ?? ('项目 #'.$id);
|
||||
|
||||
if ($this->projectService->deleteProject($id)) {
|
||||
return ToolResult::text("项目已删除:{$name}(ID {$id})。");
|
||||
}
|
||||
|
||||
return ToolResult::error('删除失败(可能无权限或项目不存在)。');
|
||||
}
|
||||
}
|
||||
112
app/Domain/Projects/Tools/EditProjectTool.php
Normal file
112
app/Domain/Projects/Tools/EditProjectTool.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Projects\Tools;
|
||||
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
use Laravel\Mcp\Server\Tools\ToolInputSchema;
|
||||
use Laravel\Mcp\Server\Tools\ToolResult;
|
||||
use Leantime\Domain\Modulemanager\Services\Modulemanager;
|
||||
use Leantime\Domain\Projects\Services\Projects;
|
||||
|
||||
/**
|
||||
* Updates an existing project with the specified details.
|
||||
*/
|
||||
class EditProjectTool extends Tool
|
||||
{
|
||||
public function __construct(
|
||||
private Projects $projectService,
|
||||
private Modulemanager $moduleManager,
|
||||
) {}
|
||||
|
||||
public function schema(ToolInputSchema $schema): ToolInputSchema
|
||||
{
|
||||
return $schema
|
||||
->integer('id')->description('ID of the project to update.')
|
||||
->required()
|
||||
->string('name')->description('Name of the project.')
|
||||
->string('details')->description('Project description.')
|
||||
->integer('clientId')->description('ID of the client for this project.')
|
||||
->string('start')->description('Start date in ISO8601 format.')
|
||||
->string('end')->description('End date in ISO8601 format.')
|
||||
->integer('hourBudget')->description('Hour budget for the project.')
|
||||
->integer('state')->description('Project state (0=open, 1=closed).')
|
||||
->integer('parent')->description('ID of the parent program or plan (only works if PgmPro plugin is active).');
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'editProject';
|
||||
}
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return 'Updates an existing project with the specified details.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the tool request.
|
||||
*/
|
||||
public function handle(array $arguments): ToolResult
|
||||
{
|
||||
$id = (int) ($arguments['id'] ?? 0);
|
||||
|
||||
// Get current project to ensure it exists
|
||||
$currentProject = $this->projectService->getProject($id);
|
||||
if (! $currentProject) {
|
||||
return ToolResult::error('Project not found.');
|
||||
}
|
||||
|
||||
$values = [];
|
||||
|
||||
// Only include parameters that were actually provided
|
||||
$name = ($arguments['name'] ?? null);
|
||||
if ($name !== null) {
|
||||
$values['name'] = $name;
|
||||
}
|
||||
|
||||
$details = ($arguments['details'] ?? null);
|
||||
if ($details !== null) {
|
||||
$values['details'] = $details;
|
||||
}
|
||||
|
||||
$clientId = ($arguments['clientId'] ?? null);
|
||||
if ($clientId !== null) {
|
||||
$values['clientId'] = $clientId;
|
||||
}
|
||||
|
||||
$start = ($arguments['start'] ?? null);
|
||||
if ($start !== null) {
|
||||
$values['start'] = $start;
|
||||
}
|
||||
|
||||
$end = ($arguments['end'] ?? null);
|
||||
if ($end !== null) {
|
||||
$values['end'] = $end;
|
||||
}
|
||||
|
||||
$hourBudget = ($arguments['hourBudget'] ?? null);
|
||||
if ($hourBudget !== null) {
|
||||
$values['hourBudget'] = $hourBudget;
|
||||
}
|
||||
|
||||
$state = ($arguments['state'] ?? null);
|
||||
if ($state !== null) {
|
||||
$values['state'] = $state;
|
||||
}
|
||||
|
||||
// Check if PgmPro plugin is active and parent is specified
|
||||
$parent = ($arguments['parent'] ?? null);
|
||||
if ($parent !== null && $this->moduleManager->isModuleAvailable('pgmPro')) {
|
||||
$values['parent'] = $parent;
|
||||
}
|
||||
|
||||
// If no values were provided, return early
|
||||
if (empty($values)) {
|
||||
return ToolResult::text('No changes provided for the project.');
|
||||
}
|
||||
|
||||
$this->projectService->editProject($values, $id);
|
||||
|
||||
return ToolResult::text('Project updated successfully.');
|
||||
}
|
||||
}
|
||||
65
app/Domain/Projects/Tools/FindProjectTool.php
Normal file
65
app/Domain/Projects/Tools/FindProjectTool.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Projects\Tools;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
|
||||
use Laravel\Mcp\Server\Tools\ToolInputSchema;
|
||||
use Laravel\Mcp\Server\Tools\ToolResult;
|
||||
use Leantime\Domain\Projects\Services\Projects;
|
||||
|
||||
/**
|
||||
* Searches for projects by name.
|
||||
*/
|
||||
#[IsReadOnly]
|
||||
class FindProjectTool extends Tool
|
||||
{
|
||||
public function __construct(
|
||||
private Projects $projectService,
|
||||
) {}
|
||||
|
||||
public function schema(ToolInputSchema $schema): ToolInputSchema
|
||||
{
|
||||
return $schema
|
||||
->string('term')->description('Search term to find in project names.')
|
||||
->required();
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'findProject';
|
||||
}
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return 'Searches for projects by name.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the tool request.
|
||||
*/
|
||||
public function handle(array $arguments): ToolResult
|
||||
{
|
||||
$term = $arguments['term'];
|
||||
$projects = $this->projectService->findProject($term);
|
||||
|
||||
if (empty($projects)) {
|
||||
return ToolResult::text("No projects found matching: '$term'.");
|
||||
}
|
||||
|
||||
$response = "## Projects Matching: '$term'\n";
|
||||
foreach ($projects as $project) {
|
||||
$result = [
|
||||
'id' => $project['id'],
|
||||
'name' => Str::sanitizeForLLM($project['name']),
|
||||
'clientName' => Str::sanitizeForLLM($project['clientName']),
|
||||
'type' => $project['type'],
|
||||
'state' => $project['state'],
|
||||
];
|
||||
$response .= Str::toMarkdown($result)."\n";
|
||||
}
|
||||
|
||||
return ToolResult::text($response);
|
||||
}
|
||||
}
|
||||
118
app/Domain/Projects/Tools/GetAllProjectsTool.php
Normal file
118
app/Domain/Projects/Tools/GetAllProjectsTool.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Projects\Tools;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
|
||||
use Laravel\Mcp\Server\Tools\ToolInputSchema;
|
||||
use Laravel\Mcp\Server\Tools\ToolResult;
|
||||
use Leantime\Domain\Comments\Services\Comments;
|
||||
use Leantime\Domain\Projects\Services\Projects;
|
||||
|
||||
/**
|
||||
* Gets all projects the current user has access to with comprehensive progress information.
|
||||
*/
|
||||
#[IsReadOnly]
|
||||
class GetAllProjectsTool extends Tool
|
||||
{
|
||||
public function __construct(
|
||||
private Projects $projectService,
|
||||
private Comments $commentsService,
|
||||
) {}
|
||||
|
||||
public function schema(ToolInputSchema $schema): ToolInputSchema
|
||||
{
|
||||
return $schema
|
||||
->boolean('showClosedProjects')->description('Whether to include closed projects in the results.')
|
||||
->boolean('includeProgressDetails')->description('Whether to include detailed progress information (RAG status, completion dates, recent comments).');
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'getAllProjects';
|
||||
}
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return 'Gets all projects the current user has access to with comprehensive progress information.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the tool request.
|
||||
*/
|
||||
public function handle(array $arguments): ToolResult
|
||||
{
|
||||
$showClosedProjects = $arguments['showClosedProjects'] ?? false;
|
||||
$includeProgressDetails = $arguments['includeProgressDetails'] ?? true;
|
||||
|
||||
$projects = $this->projectService->getAll($showClosedProjects);
|
||||
|
||||
$response = "## All Projects Overview\n";
|
||||
|
||||
if (empty($projects)) {
|
||||
return ToolResult::text('No projects found.');
|
||||
}
|
||||
|
||||
foreach ($projects as $project) {
|
||||
$result = [
|
||||
'id' => $project['id'],
|
||||
'name' => Str::sanitizeForLLM($project['name']),
|
||||
'clientName' => Str::sanitizeForLLM($project['clientName']),
|
||||
'type' => $project['type'],
|
||||
'state' => $project['state'],
|
||||
'start' => $project['start'] ?? 'Not set',
|
||||
'end' => $project['end'] ?? 'Not set',
|
||||
'progress' => isset($project['progress']['percent']) ? round($project['progress']['percent']).'%' : 'Not calculated',
|
||||
];
|
||||
|
||||
// Add detailed progress information if requested
|
||||
if ($includeProgressDetails) {
|
||||
try {
|
||||
$progress = $this->projectService->getProjectProgress($project['id']);
|
||||
$projectComments = $this->commentsService->getComments('project', $project['id'], 1);
|
||||
|
||||
$result['progressPercent'] = isset($progress['percent']) ? round($progress['percent']).'%' : 'Not calculated';
|
||||
$result['estimatedCompletion'] = isset($progress['estimatedCompletionDate']) ? strip_tags($progress['estimatedCompletionDate']) : 'Not set';
|
||||
$result['plannedCompletion'] = $progress['plannedCompletionDate'] ?? 'Not set';
|
||||
|
||||
// Add RAG status and latest update
|
||||
if (! empty($projectComments)) {
|
||||
$latestComment = $projectComments[0];
|
||||
$result['ragStatus'] = $this->formatRagStatus($latestComment['status'] ?? '');
|
||||
$result['lastUpdate'] = [
|
||||
'date' => $latestComment['date'],
|
||||
'status' => $this->formatRagStatus($latestComment['status'] ?? ''),
|
||||
'message' => Str::sanitizeForLLM($latestComment['comment'] ?? ''),
|
||||
'author' => $latestComment['firstname'].' '.$latestComment['lastname'],
|
||||
];
|
||||
} else {
|
||||
$result['ragStatus'] = 'Not set';
|
||||
$result['lastUpdate'] = 'No updates available';
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Fallback to basic progress info if detailed fetch fails
|
||||
$result['ragStatus'] = 'Unable to fetch';
|
||||
$result['lastUpdate'] = 'Unable to fetch';
|
||||
}
|
||||
}
|
||||
|
||||
$response .= Str::toMarkdown($result)."\n\n";
|
||||
}
|
||||
|
||||
return ToolResult::text($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format RAG status with appropriate emoji.
|
||||
*/
|
||||
private function formatRagStatus(string $status): string
|
||||
{
|
||||
return match (strtolower($status)) {
|
||||
'green' => 'Green (On Track)',
|
||||
'yellow' => 'Yellow (At Risk)',
|
||||
'red' => 'Red (Critical)',
|
||||
default => $status ?: 'Not Set'
|
||||
};
|
||||
}
|
||||
}
|
||||
226
app/Domain/Projects/Tools/GetFullProjectOverviewTool.php
Normal file
226
app/Domain/Projects/Tools/GetFullProjectOverviewTool.php
Normal file
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Projects\Tools;
|
||||
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
|
||||
use Laravel\Mcp\Server\Tools\ToolInputSchema;
|
||||
use Laravel\Mcp\Server\Tools\ToolResult;
|
||||
use Leantime\Domain\Comments\Services\Comments;
|
||||
use Leantime\Domain\Projects\Services\Projects;
|
||||
use Leantime\Domain\Tickets\Services\Tickets;
|
||||
use Leantime\Domain\Timesheets\Services\Timesheets;
|
||||
|
||||
/**
|
||||
* Gets comprehensive project information in a single call.
|
||||
*/
|
||||
#[IsReadOnly]
|
||||
class GetFullProjectOverviewTool extends Tool
|
||||
{
|
||||
public function __construct(
|
||||
private Projects $projectService,
|
||||
private Comments $commentsService,
|
||||
private Tickets $ticketsService,
|
||||
) {}
|
||||
|
||||
public function schema(ToolInputSchema $schema): ToolInputSchema
|
||||
{
|
||||
return $schema
|
||||
->integer('projectId')->description('Project ID to get full overview for.')
|
||||
->required()
|
||||
->boolean('includeTimesheets')->description('Whether to include timesheet data in the overview. Default false.')
|
||||
->string('dateFrom')->description('Start date for timesheet data if included. ISO8601 format (example: 2024-04-30T15:00:00-04:00).')
|
||||
->string('dateTo')->description('End date for timesheet data if included. ISO8601 format (example: 2024-04-30T15:00:00-04:00).');
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'getFullProjectOverview';
|
||||
}
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return 'Gets comprehensive project information in a single call, combining project details, progress, comments, and optionally timesheets.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the tool request.
|
||||
*/
|
||||
public function handle(array $arguments): ToolResult
|
||||
{
|
||||
$projectId = (int) ($arguments['projectId'] ?? 0);
|
||||
$includeTimesheets = $arguments['includeTimesheets'] ?? false;
|
||||
$dateFrom = ($arguments['dateFrom'] ?? '');
|
||||
$dateTo = ($arguments['dateTo'] ?? '');
|
||||
|
||||
// This method consolidates what would normally be 3-4 separate tool calls
|
||||
$response = "# Complete Project Overview\n\n";
|
||||
|
||||
try {
|
||||
// Get project details (equivalent to getProject tool)
|
||||
$project = $this->projectService->getProject($projectId);
|
||||
if (! $project) {
|
||||
return ToolResult::error("Project with ID {$projectId} not found.");
|
||||
}
|
||||
|
||||
$response .= "## Project Details\n";
|
||||
$response .= "**Name:** {$project['name']}\n";
|
||||
$response .= '**Client:** '.($project['clientName'] ?? 'No client')."\n";
|
||||
$response .= "**Type:** {$project['type']}\n";
|
||||
$response .= '**Status:** '.($project['state'] ?? 'Not set')."\n";
|
||||
$response .= '**Start Date:** '.($project['start'] ?? 'Not set')."\n";
|
||||
$response .= '**End Date:** '.($project['end'] ?? 'Not set')."\n";
|
||||
$response .= "**Description:** {$project['details']}\n\n";
|
||||
|
||||
// Get project progress (equivalent to getProjectProgress tool)
|
||||
$progress = $this->projectService->getProjectProgress($projectId);
|
||||
|
||||
$response .= "## Progress Overview\n";
|
||||
$response .= '**Overall Progress:** '.($progress['percent'] ?? '0')."%\n";
|
||||
$response .= '**RAG Status:** '.$this->formatRagStatus($progress['ragStatus'] ?? '')."\n";
|
||||
$response .= '**Estimated Completion:** '.($progress['estimatedCompletionDate'] ?? 'Not calculated')."\n";
|
||||
$response .= '**Planned Completion:** '.($progress['plannedCompletionDate'] ?? 'Not set')."\n\n";
|
||||
|
||||
// Get recent status updates/comments (equivalent to getAllProjectComments tool)
|
||||
$comments = $this->commentsService->getComments('project', $projectId);
|
||||
|
||||
$response .= "## Recent Status Updates\n";
|
||||
if (empty($comments)) {
|
||||
$response .= "*No status updates found.*\n\n";
|
||||
} else {
|
||||
foreach ($comments as $comment) {
|
||||
$status = $this->formatRagStatus($comment['status'] ?? '');
|
||||
$response .= "**{$comment['date']}** - {$status}\n";
|
||||
$response .= "*{$comment['firstname']} {$comment['lastname']}:* {$comment['text']}\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Optionally include timesheet data
|
||||
if ($includeTimesheets) {
|
||||
$response .= "## Time Tracking Summary\n";
|
||||
|
||||
// Set default date range if not provided
|
||||
if (empty($dateFrom)) {
|
||||
$dateFrom = date('Y-m-01'); // First day of current month
|
||||
}
|
||||
if (empty($dateTo)) {
|
||||
$dateTo = date('Y-m-d'); // Today
|
||||
}
|
||||
|
||||
try {
|
||||
$timesheets = app(Timesheets::class)->getAll(
|
||||
dateFrom: dtHelper()->parseUserDateTime($dateFrom)->startOfDay(),
|
||||
dateTo: dtHelper()->parseUserDateTime($dateTo)->endOfDay(),
|
||||
projectId: $projectId
|
||||
);
|
||||
|
||||
if (empty($timesheets)) {
|
||||
$response .= "*No timesheet entries found for the specified period.*\n\n";
|
||||
} else {
|
||||
$totalHours = 0;
|
||||
$userHours = [];
|
||||
|
||||
foreach ($timesheets as $entry) {
|
||||
$hours = floatval($entry['hours'] ?? 0);
|
||||
$totalHours += $hours;
|
||||
$user = $entry['firstname'].' '.$entry['lastname'];
|
||||
$userHours[$user] = ($userHours[$user] ?? 0) + $hours;
|
||||
}
|
||||
|
||||
$response .= "**Total Hours Logged:** {$totalHours}h\n";
|
||||
$response .= "**Period:** {$dateFrom} to {$dateTo}\n";
|
||||
$response .= "**Team Breakdown:**\n";
|
||||
foreach ($userHours as $user => $hours) {
|
||||
$response .= "- {$user}: {$hours}h\n";
|
||||
}
|
||||
$response .= "\n";
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$response .= "*Could not retrieve timesheet data.*\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Add quick task summary
|
||||
$response .= "## Task Summary\n";
|
||||
try {
|
||||
$allTasks = $this->ticketsService->getAll(['currentProject' => $projectId], 100);
|
||||
$taskStats = $this->calculateTaskStats($allTasks);
|
||||
|
||||
$response .= "**Total Tasks:** {$taskStats['total']}\n";
|
||||
$response .= "**Completed:** {$taskStats['completed']} ({$taskStats['completedPercent']}%)\n";
|
||||
$response .= "**In Progress:** {$taskStats['inProgress']}\n";
|
||||
$response .= "**Not Started:** {$taskStats['notStarted']}\n";
|
||||
$response .= "**Overdue:** {$taskStats['overdue']}\n\n";
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$response .= "*Could not retrieve task statistics.*\n\n";
|
||||
}
|
||||
|
||||
return ToolResult::text($response);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return ToolResult::error('Error retrieving project overview: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate task statistics from a list of tasks.
|
||||
*
|
||||
* @param array $tasks Array of task data.
|
||||
* @return array<string, int|float> Computed statistics.
|
||||
*/
|
||||
private function calculateTaskStats(array $tasks): array
|
||||
{
|
||||
$stats = [
|
||||
'total' => count($tasks),
|
||||
'completed' => 0,
|
||||
'inProgress' => 0,
|
||||
'notStarted' => 0,
|
||||
'overdue' => 0,
|
||||
'completedPercent' => 0,
|
||||
];
|
||||
|
||||
$now = new \DateTime;
|
||||
|
||||
foreach ($tasks as $task) {
|
||||
$status = $task['status'] ?? '';
|
||||
$dueDate = $task['dateToFinish'] ?? '';
|
||||
|
||||
// Count by status type
|
||||
if (in_array($status, ['done', 'closed', 'completed'])) {
|
||||
$stats['completed']++;
|
||||
} elseif (in_array($status, ['inprogress', 'working', 'development'])) {
|
||||
$stats['inProgress']++;
|
||||
} else {
|
||||
$stats['notStarted']++;
|
||||
}
|
||||
|
||||
// Check for overdue tasks
|
||||
if (! empty($dueDate) && ! in_array($status, ['done', 'closed', 'completed'])) {
|
||||
$due = new \DateTime($dueDate);
|
||||
if ($due < $now) {
|
||||
$stats['overdue']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($stats['total'] > 0) {
|
||||
$stats['completedPercent'] = round(($stats['completed'] / $stats['total']) * 100, 1);
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format RAG status with appropriate label.
|
||||
*/
|
||||
private function formatRagStatus(string $status): string
|
||||
{
|
||||
return match (strtolower($status)) {
|
||||
'green' => 'Green (On Track)',
|
||||
'yellow' => 'Yellow (At Risk)',
|
||||
'red' => 'Red (Critical)',
|
||||
default => $status ?: 'Not Set'
|
||||
};
|
||||
}
|
||||
}
|
||||
86
app/Domain/Projects/Tools/GetProjectTool.php
Normal file
86
app/Domain/Projects/Tools/GetProjectTool.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Projects\Tools;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
|
||||
use Laravel\Mcp\Server\Tools\ToolInputSchema;
|
||||
use Laravel\Mcp\Server\Tools\ToolResult;
|
||||
use Leantime\Domain\Comments\Services\Comments;
|
||||
use Leantime\Domain\Projects\Services\Projects;
|
||||
|
||||
/**
|
||||
* Gets detailed information about a specific project by its ID.
|
||||
*/
|
||||
#[IsReadOnly]
|
||||
class GetProjectTool extends Tool
|
||||
{
|
||||
public function __construct(
|
||||
private Projects $projectService,
|
||||
private Comments $commentsService,
|
||||
) {}
|
||||
|
||||
public function schema(ToolInputSchema $schema): ToolInputSchema
|
||||
{
|
||||
return $schema
|
||||
->integer('projectId')->description('ID of the project to retrieve.')
|
||||
->required();
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'getProject';
|
||||
}
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return 'Gets detailed information about a specific project by its ID and project progress.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the tool request.
|
||||
*/
|
||||
public function handle(array $arguments): ToolResult
|
||||
{
|
||||
$projectId = (int) ($arguments['projectId'] ?? 0);
|
||||
$project = $this->projectService->getProject($projectId);
|
||||
|
||||
if (! $project) {
|
||||
return ToolResult::error('Project not found.');
|
||||
}
|
||||
|
||||
$progress = $this->projectService->getProjectProgress($projectId);
|
||||
$projectComment = $this->commentsService->getComments('project', $project['id']);
|
||||
$project['team'] = $this->projectService->getUsersAssignedToProject($project['id']);
|
||||
|
||||
if (is_array($projectComment) && count($projectComment) > 0) {
|
||||
$project['lastUpdate'] = $projectComment[0];
|
||||
$project['status'] = $projectComment[0]['status'];
|
||||
} else {
|
||||
$project['lastUpdate'] = false;
|
||||
$project['status'] = '';
|
||||
}
|
||||
|
||||
$project['progress'] = $progress;
|
||||
|
||||
$response = "## Project Details\n";
|
||||
$result = [
|
||||
'id' => $project['id'],
|
||||
'name' => Str::sanitizeForLLM($project['name']),
|
||||
'details' => Str::sanitizeForLLM($project['details']),
|
||||
'clientName' => Str::sanitizeForLLM($project['clientName'] ?? ''),
|
||||
'type' => $project['type'],
|
||||
'state' => $project['state'],
|
||||
'ragStatus' => $project['status'],
|
||||
'lastUpdateMessage' => $project['lastUpdate'],
|
||||
'start' => $project['start'] ?? 'Not set',
|
||||
'end' => $project['end'] ?? 'Not set',
|
||||
'progress' => isset($progress['percent']) ? round($progress['percent']).'%' : 'Not calculated',
|
||||
'estimatedCompletionDate' => isset($progress['estimatedCompletionDate']) ? strip_tags($progress['estimatedCompletionDate']) : 'Unknown',
|
||||
];
|
||||
$response .= Str::toMarkdown($result)."\n";
|
||||
|
||||
return ToolResult::text($response);
|
||||
}
|
||||
}
|
||||
78
app/Domain/Projects/Tools/GetProjectsAssignedToUserTool.php
Normal file
78
app/Domain/Projects/Tools/GetProjectsAssignedToUserTool.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Projects\Tools;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
|
||||
use Laravel\Mcp\Server\Tools\ToolInputSchema;
|
||||
use Laravel\Mcp\Server\Tools\ToolResult;
|
||||
use Leantime\Domain\Projects\Services\Projects;
|
||||
|
||||
/**
|
||||
* Gets all projects assigned to a specific user.
|
||||
*/
|
||||
#[IsReadOnly]
|
||||
class GetProjectsAssignedToUserTool extends Tool
|
||||
{
|
||||
public function __construct(
|
||||
private Projects $projectService,
|
||||
) {}
|
||||
|
||||
public function schema(ToolInputSchema $schema): ToolInputSchema
|
||||
{
|
||||
return $schema
|
||||
->integer('userId')->description('User ID to get projects for.')
|
||||
->required()
|
||||
->string('projectStatus')->description('Filter by project status (open, closed, all).')
|
||||
->integer('clientId')->description('Filter by client ID.')
|
||||
->string('projectTypes')->description('Filter by project types (comma-separated: project, program, etc.). Use "all" for all project types.');
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'getProjectsAssignedToUser';
|
||||
}
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return 'Gets all projects assigned to a specific user.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the tool request.
|
||||
*/
|
||||
public function handle(array $arguments): ToolResult
|
||||
{
|
||||
$userId = (int) ($arguments['userId'] ?? 0);
|
||||
$projectStatus = ($arguments['projectStatus'] ?? 'open');
|
||||
$clientId = ($arguments['clientId'] ?? null);
|
||||
$projectTypes = ($arguments['projectTypes'] ?? 'all');
|
||||
|
||||
if ($userId === 0) {
|
||||
$userId = session('userdata.id') ?? '';
|
||||
}
|
||||
|
||||
$projects = $this->projectService->getProjectsAssignedToUser($userId, $projectStatus, $clientId, $projectTypes);
|
||||
|
||||
$response = "## Projects Assigned to User\n";
|
||||
foreach ($projects as $project) {
|
||||
$result = [
|
||||
'id' => $project['id'],
|
||||
'name' => Str::sanitizeForLLM($project['name']),
|
||||
'clientName' => Str::sanitizeForLLM($project['clientName']),
|
||||
'type' => $project['type'],
|
||||
'state' => $project['state'],
|
||||
'start' => $project['start'] ?? 'Not set',
|
||||
'end' => $project['end'] ?? 'Not set',
|
||||
];
|
||||
$response .= Str::toMarkdown($result)."\n";
|
||||
}
|
||||
|
||||
if (empty($projects)) {
|
||||
return ToolResult::text('No projects found.');
|
||||
}
|
||||
|
||||
return ToolResult::text($response);
|
||||
}
|
||||
}
|
||||
68
app/Domain/Projects/Tools/GetUsersAssignedToProjectTool.php
Normal file
68
app/Domain/Projects/Tools/GetUsersAssignedToProjectTool.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Projects\Tools;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
|
||||
use Laravel\Mcp\Server\Tools\ToolInputSchema;
|
||||
use Laravel\Mcp\Server\Tools\ToolResult;
|
||||
use Leantime\Domain\Projects\Services\Projects;
|
||||
|
||||
/**
|
||||
* Gets all users assigned to a specific project.
|
||||
*/
|
||||
#[IsReadOnly]
|
||||
class GetUsersAssignedToProjectTool extends Tool
|
||||
{
|
||||
public function __construct(
|
||||
private Projects $projectService,
|
||||
) {}
|
||||
|
||||
public function schema(ToolInputSchema $schema): ToolInputSchema
|
||||
{
|
||||
return $schema
|
||||
->integer('projectId')->description('ID of the project to get users for.')
|
||||
->required()
|
||||
->boolean('teamOnly')->description('Whether to only include direct team members.');
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'getUsersAssignedToProject';
|
||||
}
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return 'Gets all users assigned to a specific project.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the tool request.
|
||||
*/
|
||||
public function handle(array $arguments): ToolResult
|
||||
{
|
||||
$projectId = (int) ($arguments['projectId'] ?? 0);
|
||||
$teamOnly = $arguments['teamOnly'] ?? false;
|
||||
|
||||
$users = $this->projectService->getUsersAssignedToProject($projectId, $teamOnly);
|
||||
|
||||
if (empty($users)) {
|
||||
return ToolResult::text('No users assigned to this project.');
|
||||
}
|
||||
|
||||
$response = "## Users Assigned to Project\n";
|
||||
foreach ($users as $user) {
|
||||
$result = [
|
||||
'id' => $user['id'],
|
||||
'name' => Str::sanitizeForLLM($user['firstname'].' '.$user['lastname']),
|
||||
'email' => $user['username'],
|
||||
'role' => $user['role'] ?? 'Not specified',
|
||||
'projectRole' => $user['projectRole'] ?? 'Not specified',
|
||||
];
|
||||
$response .= Str::toMarkdown($result)."\n";
|
||||
}
|
||||
|
||||
return ToolResult::text($response);
|
||||
}
|
||||
}
|
||||
75
app/Domain/Projects/Tools/PatchProjectTool.php
Normal file
75
app/Domain/Projects/Tools/PatchProjectTool.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Projects\Tools;
|
||||
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
use Laravel\Mcp\Server\Tools\ToolInputSchema;
|
||||
use Laravel\Mcp\Server\Tools\ToolResult;
|
||||
use Leantime\Domain\Modulemanager\Services\Modulemanager;
|
||||
use Leantime\Domain\Projects\Services\Projects;
|
||||
|
||||
/**
|
||||
* Updates specific fields of an existing project.
|
||||
*/
|
||||
class PatchProjectTool extends Tool
|
||||
{
|
||||
public function __construct(
|
||||
private Projects $projectService,
|
||||
private Modulemanager $moduleManager,
|
||||
) {}
|
||||
|
||||
public function schema(ToolInputSchema $schema): ToolInputSchema
|
||||
{
|
||||
return $schema
|
||||
->integer('id')->description('ID of the project to update.')
|
||||
->required()
|
||||
->raw('params', ['type' => 'object', 'description' => 'Key-value pairs of fields to update. Example: {"name": "New name", "state": 0}'])->required();
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'patchProject';
|
||||
}
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return 'Updates specific fields of an existing project.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the tool request.
|
||||
*/
|
||||
public function handle(array $arguments): ToolResult
|
||||
{
|
||||
$id = (int) ($arguments['id'] ?? 0);
|
||||
$params = ($arguments['params'] ?? null);
|
||||
|
||||
// Get current project to ensure it exists
|
||||
$currentProject = $this->projectService->getProject($id);
|
||||
if (! $currentProject) {
|
||||
return ToolResult::error('Project not found.');
|
||||
}
|
||||
|
||||
// Check if $params is array of arrays (AI sometimes does this)
|
||||
if (is_array($params) && ! empty($params) && isset($params[0]) && is_array($params[0])) {
|
||||
$params = $params[0];
|
||||
}
|
||||
|
||||
if (! is_array($params)) {
|
||||
return ToolResult::error('The params parameter is not a valid object. Please provide an object of key-value pairs.');
|
||||
}
|
||||
|
||||
// Handle parent field if PgmPro plugin is active
|
||||
if (isset($params['parent']) && ! $this->moduleManager->isModuleAvailable('pgmPro')) {
|
||||
unset($params['parent']);
|
||||
}
|
||||
|
||||
$result = $this->projectService->patch($id, $params);
|
||||
|
||||
if ($result) {
|
||||
return ToolResult::text('Project updated successfully.');
|
||||
}
|
||||
|
||||
return ToolResult::error('Failed to update project. Please check the provided information.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user