245 lines
10 KiB
C#
245 lines
10 KiB
C#
using ControlPlane.Api.Services;
|
|
using ControlPlane.Core.Models;
|
|
using LibGit2Sharp;
|
|
using System.Net.Http.Headers;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
|
|
namespace ControlPlane.Api.Endpoints;
|
|
|
|
public static class OpcEndpoints
|
|
{
|
|
private static readonly JsonSerializerOptions JsonOpts =
|
|
new(JsonSerializerDefaults.Web) { WriteIndented = false };
|
|
|
|
public static IEndpointRouteBuilder MapOpcEndpoints(this IEndpointRouteBuilder app)
|
|
{
|
|
var g = app.MapGroup("/api/opc").WithTags("OPC");
|
|
|
|
// ── OPC CRUD ──────────────────────────────────────────────────────────
|
|
g.MapGet ("", ListOpcs);
|
|
g.MapGet ("/next-number", GetNextNumber);
|
|
g.MapPost ("", CreateOpc);
|
|
g.MapGet ("/{id:guid}", GetOpc);
|
|
g.MapPatch ("/{id:guid}", UpdateOpc);
|
|
g.MapDelete("/{id:guid}", DeleteOpc);
|
|
|
|
// ── Notes ─────────────────────────────────────────────────────────────
|
|
g.MapGet ("/{id:guid}/notes", ListNotes);
|
|
g.MapPost ("/{id:guid}/notes", AddNote);
|
|
|
|
// ── Artifacts ─────────────────────────────────────────────────────────
|
|
g.MapGet ("/{id:guid}/artifacts", ListArtifacts);
|
|
g.MapPost ("/{id:guid}/artifacts", CreateArtifact);
|
|
g.MapPatch ("/artifacts/{artifactId:guid}", UpdateArtifact);
|
|
g.MapDelete("/artifacts/{artifactId:guid}", DeleteArtifact);
|
|
|
|
// ── Pinned commits ────────────────────────────────────────────────────
|
|
g.MapGet ("/{id:guid}/pinned-commits", ListPinnedCommits);
|
|
g.MapPost ("/{id:guid}/pinned-commits", PinCommit);
|
|
g.MapDelete("/{id:guid}/pinned-commits/{hash}", UnpinCommit);
|
|
|
|
// ── AI assist (proxies to OpenRouter, key stays on server) ────────────
|
|
g.MapPost ("/ai-assist", AiAssist);
|
|
|
|
return app;
|
|
}
|
|
|
|
// ── OPC handlers ──────────────────────────────────────────────────────────
|
|
|
|
private static async Task<IResult> ListOpcs(
|
|
OpcService svc,
|
|
string? type = null, string? status = null,
|
|
CancellationToken ct = default)
|
|
{
|
|
var list = await svc.ListAsync(type, status, ct);
|
|
return Results.Ok(list);
|
|
}
|
|
|
|
private static async Task<IResult> GetNextNumber(
|
|
OpcService svc, CancellationToken ct)
|
|
{
|
|
var number = await svc.NextNumberAsync(ct);
|
|
return Results.Ok(new { number });
|
|
}
|
|
|
|
private static async Task<IResult> CreateOpc(
|
|
CreateOpcRequest req, OpcService svc, CancellationToken ct)
|
|
{
|
|
var opc = await svc.CreateAsync(req, ct);
|
|
return Results.Created($"/api/opc/{opc.Id}", opc);
|
|
}
|
|
|
|
private static async Task<IResult> GetOpc(
|
|
Guid id, OpcService svc, CancellationToken ct)
|
|
{
|
|
var opc = await svc.GetAsync(id, ct);
|
|
return opc is null ? Results.NotFound() : Results.Ok(opc);
|
|
}
|
|
|
|
private static async Task<IResult> UpdateOpc(
|
|
Guid id, UpdateOpcRequest req, OpcService svc, CancellationToken ct)
|
|
{
|
|
var opc = await svc.UpdateAsync(id, req, ct);
|
|
return opc is null ? Results.NotFound() : Results.Ok(opc);
|
|
}
|
|
|
|
private static async Task<IResult> DeleteOpc(
|
|
Guid id, OpcService svc, CancellationToken ct)
|
|
{
|
|
return await svc.DeleteAsync(id, ct) ? Results.NoContent() : Results.NotFound();
|
|
}
|
|
|
|
// ── Note handlers ─────────────────────────────────────────────────────────
|
|
|
|
private static async Task<IResult> ListNotes(
|
|
Guid id, OpcService svc, CancellationToken ct)
|
|
{
|
|
var notes = await svc.ListNotesAsync(id, ct);
|
|
return Results.Ok(notes);
|
|
}
|
|
|
|
private static async Task<IResult> AddNote(
|
|
Guid id, AddNoteRequest req, OpcService svc, CancellationToken ct)
|
|
{
|
|
var note = await svc.AddNoteAsync(id, req, ct);
|
|
return Results.Created($"/api/opc/{id}/notes/{note.Id}", note);
|
|
}
|
|
|
|
// ── Artifact handlers ─────────────────────────────────────────────────────
|
|
|
|
private static async Task<IResult> ListArtifacts(
|
|
Guid id, OpcService svc,
|
|
string? type = null, CancellationToken ct = default)
|
|
{
|
|
var artifacts = await svc.ListArtifactsAsync(id, type, ct);
|
|
return Results.Ok(artifacts);
|
|
}
|
|
|
|
private static async Task<IResult> CreateArtifact(
|
|
Guid id, UpsertArtifactRequest req, OpcService svc, CancellationToken ct)
|
|
{
|
|
var artifact = await svc.UpsertArtifactAsync(id, req, ct);
|
|
return Results.Created($"/api/opc/{id}/artifacts/{artifact.Id}", artifact);
|
|
}
|
|
|
|
private static async Task<IResult> UpdateArtifact(
|
|
Guid artifactId, UpsertArtifactRequest req, OpcService svc, CancellationToken ct)
|
|
{
|
|
var artifact = await svc.UpdateArtifactAsync(artifactId, req, ct);
|
|
return artifact is null ? Results.NotFound() : Results.Ok(artifact);
|
|
}
|
|
|
|
private static async Task<IResult> DeleteArtifact(
|
|
Guid artifactId, OpcService svc, CancellationToken ct)
|
|
{
|
|
return await svc.DeleteArtifactAsync(artifactId, ct)
|
|
? Results.NoContent()
|
|
: Results.NotFound();
|
|
}
|
|
|
|
// ── Pinned commit handlers ────────────────────────────────────────────────
|
|
|
|
private static async Task<IResult> ListPinnedCommits(
|
|
Guid id, OpcService svc, CancellationToken ct)
|
|
{
|
|
var commits = await svc.ListPinnedCommitsAsync(id, ct);
|
|
return Results.Ok(commits);
|
|
}
|
|
|
|
private static async Task<IResult> PinCommit(
|
|
Guid id, PinCommitRequest req, OpcService svc, IConfiguration config, CancellationToken ct)
|
|
{
|
|
var repoPath = config["Docker:RepoRoot"];
|
|
string fullHash = req.Hash;
|
|
string shortHash = req.Hash.Length >= 7 ? req.Hash[..7] : req.Hash;
|
|
string subject = string.Empty;
|
|
string author = string.Empty;
|
|
|
|
if (!string.IsNullOrWhiteSpace(repoPath) && Directory.Exists(repoPath))
|
|
{
|
|
using var repo = new Repository(repoPath);
|
|
var commit = repo.Lookup<Commit>(req.Hash);
|
|
if (commit is null) return Results.NotFound("Commit not found in repository.");
|
|
fullHash = commit.Sha;
|
|
shortHash = commit.Sha[..7];
|
|
subject = commit.MessageShort;
|
|
author = commit.Author.Name;
|
|
}
|
|
|
|
var pinned = await svc.PinCommitAsync(id, fullHash, shortHash, subject, author, req.PinnedBy, ct);
|
|
return pinned is null
|
|
? Results.NotFound()
|
|
: Results.Created($"/api/opc/{id}/pinned-commits/{fullHash}", pinned);
|
|
}
|
|
|
|
private static async Task<IResult> UnpinCommit(
|
|
Guid id, string hash, OpcService svc, CancellationToken ct)
|
|
{
|
|
return await svc.UnpinCommitAsync(id, hash, ct) ? Results.NoContent() : Results.NotFound();
|
|
}
|
|
|
|
// ── AI assist ─────────────────────────────────────────────────────────────
|
|
|
|
private static async Task<IResult> AiAssist(
|
|
AiAssistRequest req,
|
|
IConfiguration config,
|
|
IHttpClientFactory http,
|
|
CancellationToken ct)
|
|
{
|
|
var apiKey = config["OpenRouter:ApiKey"];
|
|
if (string.IsNullOrWhiteSpace(apiKey))
|
|
return Results.Problem("OpenRouter API key not configured. Add OpenRouter:ApiKey to appsettings.");
|
|
|
|
var systemPrompt =
|
|
"You are an assistant helping a software engineering team write clear, concise " +
|
|
"OPC (Online Project Communication) content — requirements, change descriptions, " +
|
|
"QA test paths, and specifications. Be direct, structured, and professional. " +
|
|
"Respond with plain text only (no markdown wrapping).";
|
|
|
|
var messages = new List<object>
|
|
{
|
|
new { role = "system", content = systemPrompt },
|
|
};
|
|
|
|
if (!string.IsNullOrWhiteSpace(req.Context))
|
|
messages.Add(new { role = "user", content = $"Context:\n{req.Context}" });
|
|
|
|
messages.Add(new { role = "user", content = req.Prompt });
|
|
|
|
var body = new
|
|
{
|
|
model = "anthropic/claude-3.5-haiku",
|
|
messages,
|
|
max_tokens = 1024,
|
|
};
|
|
|
|
var client = http.CreateClient("openrouter");
|
|
client.DefaultRequestHeaders.Authorization =
|
|
new AuthenticationHeaderValue("Bearer", apiKey);
|
|
client.DefaultRequestHeaders.Add("HTTP-Referer", "https://controlplane.clarity.internal");
|
|
client.DefaultRequestHeaders.Add("X-Title", "Clarity ControlPlane OPC");
|
|
|
|
var response = await client.PostAsync(
|
|
"https://openrouter.ai/api/v1/chat/completions",
|
|
new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json"),
|
|
ct);
|
|
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
var err = await response.Content.ReadAsStringAsync(ct);
|
|
return Results.Problem($"OpenRouter error {response.StatusCode}: {err}");
|
|
}
|
|
|
|
var json = await response.Content.ReadAsStringAsync(ct);
|
|
using var doc = JsonDocument.Parse(json);
|
|
var text = doc.RootElement
|
|
.GetProperty("choices")[0]
|
|
.GetProperty("message")
|
|
.GetProperty("content")
|
|
.GetString() ?? string.Empty;
|
|
|
|
return Results.Ok(new { text });
|
|
}
|
|
}
|