AX Agent ?? ?? ??? MCP ?? ??? ???? ??? ??? ??
- MCP ?? ?????? synthetic skill? ???? McpSkillCatalog? ???? ToolRegistry ?? snapshot ?? ??? ??? - managed/user/additional/project/plugin/mcp/legacy ?? source ??, plugin-only ??, source? inline shell trust boundary? SkillService/AppSettings/Settings UI? ??? - SlashCommandCatalog? ChatWindow?? builtin command? skill? ???? ???? ??? ?? ? ????? dedupe?? MCP ???? ? synthetic skill ?? ??? SkillGallery/AgentSettings? ??? - README.md? docs/DEVELOPMENT.md? 2026-04-14 19:13 (KST) ?? ?? ??? ?? ??? ??? - ??: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_phase4\\ -p:IntermediateOutputPath=obj\\verify_phase4\\ (?? 0, ?? 0) - ??: dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "SkillServiceRuntimePolicyTests|SlashCommandCatalogTests|McpSkillCatalogTests" -p:OutputPath=bin\\verify_phase4_tests\\ -p:IntermediateOutputPath=obj\\verify_phase4_tests\\ (?? 17, ?? WorkspaceContextGeneratorTests.cs nullable ?? 1? ??)
This commit is contained in:
257
src/AxCopilot/Services/Agent/McpSkillCatalog.cs
Normal file
257
src/AxCopilot/Services/Agent/McpSkillCatalog.cs
Normal file
@@ -0,0 +1,257 @@
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
internal static class McpSkillCatalog
|
||||
{
|
||||
private static readonly object s_gate = new();
|
||||
private static readonly Dictionary<string, McpServerSnapshot> s_snapshots = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public static void RefreshFromClients(IEnumerable<McpClientService> clients)
|
||||
{
|
||||
if (clients == null)
|
||||
return;
|
||||
|
||||
lock (s_gate)
|
||||
{
|
||||
foreach (var client in clients)
|
||||
{
|
||||
if (client == null || string.IsNullOrWhiteSpace(client.ServerName))
|
||||
continue;
|
||||
|
||||
var serverName = client.ServerName.Trim();
|
||||
s_snapshots[serverName] = new McpServerSnapshot(
|
||||
serverName,
|
||||
client.Tools
|
||||
.Select(tool => new McpToolDefinition
|
||||
{
|
||||
Name = tool.Name,
|
||||
Description = tool.Description,
|
||||
ServerName = tool.ServerName,
|
||||
Parameters = tool.Parameters.ToDictionary(
|
||||
pair => pair.Key,
|
||||
pair => new McpParameterDef
|
||||
{
|
||||
Type = pair.Value.Type,
|
||||
Description = pair.Value.Description,
|
||||
Required = pair.Value.Required,
|
||||
},
|
||||
StringComparer.OrdinalIgnoreCase),
|
||||
})
|
||||
.ToList()
|
||||
.AsReadOnly(),
|
||||
client.Resources
|
||||
.Select(resource => new McpResourceDefinition
|
||||
{
|
||||
Uri = resource.Uri,
|
||||
Name = resource.Name,
|
||||
Description = resource.Description,
|
||||
MimeType = resource.MimeType,
|
||||
ServerName = resource.ServerName,
|
||||
})
|
||||
.ToList()
|
||||
.AsReadOnly(),
|
||||
DateTime.UtcNow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static IReadOnlyList<SkillDefinition> BuildSyntheticSkills(IEnumerable<McpServerEntry>? servers)
|
||||
{
|
||||
if (servers == null)
|
||||
return [];
|
||||
|
||||
var snapshots = GetSnapshotMap();
|
||||
var skills = new List<SkillDefinition>();
|
||||
var usedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var server in servers
|
||||
.Where(server => server != null && server.Enabled)
|
||||
.OrderBy(server => server.Name, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var displayName = string.IsNullOrWhiteSpace(server.Name) ? "mcp-server" : server.Name.Trim();
|
||||
var skillName = BuildUniqueSkillName(displayName, usedNames);
|
||||
snapshots.TryGetValue(displayName, out var snapshot);
|
||||
|
||||
var allowedTools = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"mcp_list_resources",
|
||||
"mcp_read_resource"
|
||||
};
|
||||
|
||||
if (snapshot != null)
|
||||
{
|
||||
foreach (var tool in snapshot.Tools)
|
||||
allowedTools.Add($"mcp_{tool.ServerName}_{tool.Name}");
|
||||
}
|
||||
|
||||
skills.Add(new SkillDefinition
|
||||
{
|
||||
Id = skillName,
|
||||
Name = skillName,
|
||||
Label = $"{displayName} MCP",
|
||||
Description = BuildDescription(displayName, snapshot),
|
||||
SystemPrompt = BuildPromptBody(displayName, server, snapshot),
|
||||
FilePath = $"[mcp]/{displayName}",
|
||||
SkillRoot = "",
|
||||
SourceScope = "mcp",
|
||||
AllowedTools = string.Join(", ", allowedTools.OrderBy(tool => tool, StringComparer.OrdinalIgnoreCase)),
|
||||
WhenToUse = BuildWhenToUse(displayName, snapshot),
|
||||
Tabs = "all",
|
||||
UserInvocable = true,
|
||||
IsAvailable = true,
|
||||
});
|
||||
}
|
||||
|
||||
return skills;
|
||||
}
|
||||
|
||||
public static string ComputeSignature(IEnumerable<McpServerEntry>? servers)
|
||||
{
|
||||
var snapshots = GetSnapshotMap();
|
||||
var parts = new List<string>();
|
||||
var orderedServers = (servers ?? [])
|
||||
.Where(server => server != null)
|
||||
.OrderBy(server => server.Name, StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var server in orderedServers)
|
||||
{
|
||||
var name = string.IsNullOrWhiteSpace(server.Name) ? "(unnamed)" : server.Name.Trim();
|
||||
snapshots.TryGetValue(name, out var snapshot);
|
||||
parts.Add(string.Join(":",
|
||||
name,
|
||||
server.Enabled ? "1" : "0",
|
||||
server.Transport ?? "stdio",
|
||||
server.Command ?? "",
|
||||
server.Url ?? "",
|
||||
snapshot?.Tools.Count ?? 0,
|
||||
snapshot?.Resources.Count ?? 0,
|
||||
snapshot?.UpdatedAtUtc.Ticks ?? 0));
|
||||
}
|
||||
|
||||
return string.Join("|", parts);
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, McpServerSnapshot> GetSnapshotMap()
|
||||
{
|
||||
lock (s_gate)
|
||||
return new Dictionary<string, McpServerSnapshot>(s_snapshots, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string BuildDescription(string displayName, McpServerSnapshot? snapshot)
|
||||
{
|
||||
if (snapshot == null)
|
||||
return $"{displayName} MCP 서버의 도구와 리소스를 탐색하는 보조 스킬입니다.";
|
||||
|
||||
var toolCount = snapshot.Tools.Count;
|
||||
var resourceCount = snapshot.Resources.Count;
|
||||
if (toolCount == 0 && resourceCount == 0)
|
||||
return $"{displayName} MCP 서버에 연결되었지만 아직 노출된 도구나 리소스가 없습니다.";
|
||||
|
||||
return $"{displayName} MCP 서버의 도구 {toolCount}개와 리소스 {resourceCount}개를 우선 활용하도록 돕습니다.";
|
||||
}
|
||||
|
||||
private static string BuildWhenToUse(string displayName, McpServerSnapshot? snapshot)
|
||||
{
|
||||
if (snapshot == null || (snapshot.Tools.Count == 0 && snapshot.Resources.Count == 0))
|
||||
return $"{displayName} MCP 서버가 제공하는 리소스나 도구를 먼저 확인해야 하는 작업";
|
||||
|
||||
var keywords = snapshot.Resources
|
||||
.SelectMany(resource => new[] { resource.Name, resource.Description, resource.MimeType })
|
||||
.Concat(snapshot.Tools.SelectMany(tool => new[] { tool.Name, tool.Description }))
|
||||
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||
.Select(value => value!.Trim())
|
||||
.Take(4)
|
||||
.ToList();
|
||||
|
||||
return keywords.Count == 0
|
||||
? $"{displayName} MCP 서버의 도구와 리소스가 필요한 작업"
|
||||
: $"{displayName} MCP 서버의 {string.Join(", ", keywords)} 관련 정보를 찾거나 실행해야 하는 작업";
|
||||
}
|
||||
|
||||
private static string BuildPromptBody(string displayName, McpServerEntry server, McpServerSnapshot? snapshot)
|
||||
{
|
||||
var lines = new List<string>
|
||||
{
|
||||
$"Use the MCP server \"{displayName}\" as the first source of truth when it is relevant.",
|
||||
$"Transport: {NormalizeTransport(server.Transport)}",
|
||||
$"1. Start with `mcp_list_resources(server_name: \"{displayName}\")` to discover available resources.",
|
||||
"2. Read only the specific resources you need with `mcp_read_resource`.",
|
||||
"3. If server-specific MCP tools are available, prefer the smallest relevant call before broader exploration.",
|
||||
"4. Summarize the MCP evidence you used and clearly note any missing data."
|
||||
};
|
||||
|
||||
if (snapshot == null)
|
||||
{
|
||||
lines.Add(string.Empty);
|
||||
lines.Add("This session does not yet have a cached MCP metadata snapshot, so inspect the live resource list first.");
|
||||
return string.Join(Environment.NewLine, lines);
|
||||
}
|
||||
|
||||
if (snapshot.Tools.Count > 0)
|
||||
{
|
||||
lines.Add(string.Empty);
|
||||
lines.Add("Available server-specific MCP tools:");
|
||||
foreach (var tool in snapshot.Tools.Take(8))
|
||||
{
|
||||
var description = string.IsNullOrWhiteSpace(tool.Description) ? "" : $" - {tool.Description}";
|
||||
lines.Add($"- mcp_{tool.ServerName}_{tool.Name}{description}");
|
||||
}
|
||||
}
|
||||
|
||||
if (snapshot.Resources.Count > 0)
|
||||
{
|
||||
lines.Add(string.Empty);
|
||||
lines.Add("Known MCP resources:");
|
||||
foreach (var resource in snapshot.Resources.Take(8))
|
||||
{
|
||||
var descriptor = new StringBuilder();
|
||||
descriptor.Append($"- {resource.Name}");
|
||||
if (!string.IsNullOrWhiteSpace(resource.MimeType))
|
||||
descriptor.Append($" ({resource.MimeType})");
|
||||
if (!string.IsNullOrWhiteSpace(resource.Description))
|
||||
descriptor.Append($" - {resource.Description}");
|
||||
lines.Add(descriptor.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
return string.Join(Environment.NewLine, lines);
|
||||
}
|
||||
|
||||
private static string BuildUniqueSkillName(string displayName, ISet<string> usedNames)
|
||||
{
|
||||
var baseName = $"mcp:{NormalizeToken(displayName)}";
|
||||
var current = baseName;
|
||||
var suffix = 2;
|
||||
while (!usedNames.Add(current))
|
||||
{
|
||||
current = $"{baseName}-{suffix}";
|
||||
suffix++;
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
private static string NormalizeToken(string input)
|
||||
{
|
||||
var normalized = Regex.Replace((input ?? "").Trim().ToLowerInvariant(), @"[^a-z0-9]+", "-").Trim('-');
|
||||
return string.IsNullOrWhiteSpace(normalized) ? "server" : normalized;
|
||||
}
|
||||
|
||||
private static string NormalizeTransport(string? transport)
|
||||
{
|
||||
return (transport ?? "stdio").Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"sse" => "server-sent events",
|
||||
_ => "stdio",
|
||||
};
|
||||
}
|
||||
|
||||
private sealed record McpServerSnapshot(
|
||||
string ServerName,
|
||||
IReadOnlyList<McpToolDefinition> Tools,
|
||||
IReadOnlyList<McpResourceDefinition> Resources,
|
||||
DateTime UpdatedAtUtc);
|
||||
}
|
||||
@@ -60,7 +60,7 @@ public class SkillManagerTool : IAgentTool
|
||||
{
|
||||
var skills = SkillService.Skills;
|
||||
if (skills.Count == 0)
|
||||
return ToolResult.Ok("로드된 스킬이 없습니다. 번들 스킬, %APPDATA%\\AxCopilot\\skills\\, 추가 스킬 폴더, 프로젝트 .claude\\skills\\를 확인하세요.");
|
||||
return ToolResult.Ok("로드된 스킬이 없습니다. 번들 스킬, %APPDATA%\\AxCopilot\\skills\\, 추가 스킬 폴더, 프로젝트 .claude\\skills\\, MCP 스킬 소스를 확인하세요.");
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"사용 가능한 스킬 ({skills.Count}개):\n");
|
||||
|
||||
@@ -33,18 +33,25 @@ public static class SkillService
|
||||
/// <summary>스킬 폴더에서 *.skill.md / SKILL.md 파일을 로드합니다.</summary>
|
||||
public static void LoadSkills(string? customFolder = null, string? projectRoot = null, IEnumerable<string>? additionalFolders = null)
|
||||
{
|
||||
var app = System.Windows.Application.Current as App;
|
||||
var llm = app?.SettingsService?.Settings.Llm;
|
||||
var normalizedCustomFolder = NormalizeExistingDirectory(customFolder);
|
||||
var normalizedProjectRoot = NormalizeExistingDirectory(projectRoot);
|
||||
var normalizedAdditionalFolders = NormalizeDistinctDirectories(additionalFolders);
|
||||
var sources = BuildSkillSources(normalizedCustomFolder, normalizedProjectRoot, normalizedAdditionalFolders).ToList();
|
||||
var loadSignature = ComputeLoadSignature(sources);
|
||||
var mcpSkills = BuildMcpSkills(llm?.McpServers, llm);
|
||||
var loadSignature = ComputeLoadSignature(sources, llm, mcpSkills);
|
||||
if (_skills.Count > 0 && string.Equals(_lastLoadSignature, loadSignature, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
var allSkills = GetBundledSkills().ToList();
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var bundled in allSkills)
|
||||
seen.Add(bundled.Name);
|
||||
var candidates = new List<SkillLoadCandidate>();
|
||||
var order = 0;
|
||||
|
||||
foreach (var bundled in GetBundledSkills())
|
||||
candidates.Add(new SkillLoadCandidate(order++, bundled));
|
||||
|
||||
foreach (var mcpSkill in mcpSkills)
|
||||
candidates.Add(new SkillLoadCandidate(order++, mcpSkill));
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
@@ -57,8 +64,8 @@ public static class SkillService
|
||||
var skill = source.Kind == SkillSourceKind.LegacyCommand
|
||||
? ParseLegacyCommandFile(file, source.Directory, source.Scope)
|
||||
: ParseSkillFile(file, source.Directory, source.Scope);
|
||||
if (skill != null && seen.Add(skill.Name))
|
||||
allSkills.Add(skill);
|
||||
if (skill != null)
|
||||
candidates.Add(new SkillLoadCandidate(order++, skill));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -67,6 +74,17 @@ public static class SkillService
|
||||
}
|
||||
}
|
||||
|
||||
var allSkills = candidates
|
||||
.GroupBy(candidate => candidate.Skill.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(group => group
|
||||
.OrderByDescending(candidate => GetSkillSourcePriority(candidate.Skill.SourceScope))
|
||||
.ThenBy(candidate => candidate.LoadOrder)
|
||||
.Select(candidate => candidate.Skill)
|
||||
.First())
|
||||
.OrderByDescending(skill => GetSkillSourcePriority(skill.SourceScope))
|
||||
.ThenBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
// 런타임 의존성 검증
|
||||
foreach (var skill in allSkills)
|
||||
{
|
||||
@@ -338,6 +356,8 @@ public static class SkillService
|
||||
|
||||
void AddSource(string? directory, string scope, SkillSourceKind kind = SkillSourceKind.SkillFolder)
|
||||
{
|
||||
if (!IsSkillSourceEnabled(scope, llm))
|
||||
return;
|
||||
if (string.IsNullOrWhiteSpace(directory) || !Directory.Exists(directory))
|
||||
return;
|
||||
|
||||
@@ -350,8 +370,11 @@ public static class SkillService
|
||||
AddSource(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "skills"), "user");
|
||||
AddSource(customFolder, "custom");
|
||||
|
||||
foreach (var folder in additionalFolders)
|
||||
AddSource(folder, "additional");
|
||||
if (llm?.EnableAdditionalSkillDiscovery ?? true)
|
||||
{
|
||||
foreach (var folder in additionalFolders)
|
||||
AddSource(folder, "additional");
|
||||
}
|
||||
|
||||
if (llm?.EnablePluginSkillDiscovery ?? true)
|
||||
{
|
||||
@@ -374,7 +397,10 @@ public static class SkillService
|
||||
return sources;
|
||||
}
|
||||
|
||||
private static string ComputeLoadSignature(IEnumerable<SkillSourceDescriptor> sources)
|
||||
private static string ComputeLoadSignature(
|
||||
IEnumerable<SkillSourceDescriptor> sources,
|
||||
Models.LlmSettings? llm,
|
||||
IReadOnlyList<SkillDefinition> mcpSkills)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
foreach (var source in sources)
|
||||
@@ -388,9 +414,68 @@ public static class SkillService
|
||||
parts.Add($"{source.Kind}:{source.Scope}:{source.Directory}:{files.Count}:{latestWrite}");
|
||||
}
|
||||
|
||||
parts.Add($"skill-policy:{llm?.EnableManagedSkillSource ?? true}:{llm?.EnableUserSkillSource ?? true}:{llm?.EnableAdditionalSkillDiscovery ?? true}:{llm?.EnableProjectSkillDiscovery ?? true}:{llm?.EnablePluginSkillDiscovery ?? true}:{llm?.EnableMcpSkillDiscovery ?? true}:{llm?.EnablePluginOnlySkillMode ?? false}:{llm?.EnableLegacyCommandSkills ?? true}");
|
||||
parts.Add($"mcp:{McpSkillCatalog.ComputeSignature(llm?.McpServers)}:{mcpSkills.Count}");
|
||||
return string.Join("|", parts);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<SkillDefinition> BuildMcpSkills(
|
||||
IEnumerable<Models.McpServerEntry>? servers,
|
||||
Models.LlmSettings? llm)
|
||||
{
|
||||
if (!IsSkillSourceEnabled("mcp", llm))
|
||||
return [];
|
||||
|
||||
return McpSkillCatalog.BuildSyntheticSkills(servers);
|
||||
}
|
||||
|
||||
internal static bool IsSkillSourceEnabled(string scope, Models.LlmSettings? llm)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(scope))
|
||||
return true;
|
||||
|
||||
var normalized = scope.Trim().ToLowerInvariant();
|
||||
if (normalized == "bundled")
|
||||
return true;
|
||||
|
||||
if (llm == null)
|
||||
return true;
|
||||
|
||||
if (llm.EnablePluginOnlySkillMode)
|
||||
{
|
||||
return normalized is "managed" or "plugin";
|
||||
}
|
||||
|
||||
return normalized switch
|
||||
{
|
||||
"managed" => llm.EnableManagedSkillSource,
|
||||
"user" or "custom" => llm.EnableUserSkillSource,
|
||||
"additional" => llm.EnableAdditionalSkillDiscovery,
|
||||
"project" => llm.EnableProjectSkillDiscovery,
|
||||
"plugin" => llm.EnablePluginSkillDiscovery,
|
||||
"mcp" => llm.EnableMcpSkillDiscovery,
|
||||
"legacy" => llm.EnableLegacyCommandSkills,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
internal static int GetSkillSourcePriority(string? sourceScope)
|
||||
{
|
||||
return (sourceScope ?? "").Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"project" => 900,
|
||||
"mcp" => 850,
|
||||
"plugin" => 800,
|
||||
"additional" => 760,
|
||||
"custom" => 740,
|
||||
"user" => 700,
|
||||
"managed" => 650,
|
||||
"bundled" => 600,
|
||||
"legacy" => 550,
|
||||
_ => 500,
|
||||
};
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateProjectSkillFolders(string? projectRoot)
|
||||
{
|
||||
foreach (var root in EnumerateAncestorDirectories(projectRoot))
|
||||
@@ -527,7 +612,57 @@ public static class SkillService
|
||||
완료한 작업, 남은 이슈, 주의할 파일, 다음 권장 액션을 분리해서 작성하세요.
|
||||
""",
|
||||
whenToUse: "작업 마무리, 인수인계, 세션 요약, 다음 액션 정리가 필요할 때",
|
||||
allowedTools: "git_tool, file_read, task_list")
|
||||
allowedTools: "git_tool, file_read, task_list"),
|
||||
CreateBundledSkill(
|
||||
"stuck-recovery",
|
||||
"막힘 복구",
|
||||
"막힌 작업을 다시 전진시키기 위한 원인 정리와 우회 경로를 제안합니다.",
|
||||
"""
|
||||
현재 작업이 멈춘 이유를 입력, 환경, 도구, 정책, 검증 결과 관점에서 나눠 분석하세요.
|
||||
즉시 시도할 수 있는 우회 경로와, 사용자의 결정을 받아야 하는 항목을 분리해 정리하세요.
|
||||
""",
|
||||
whenToUse: "반복 실패, 권한 차단, 환경 문제, 막힘 복구가 필요한 작업",
|
||||
allowedTools: "file_read, grep, build_run, process, task_list"),
|
||||
CreateBundledSkill(
|
||||
"context-memory",
|
||||
"컨텍스트 메모리",
|
||||
"지속적으로 참고해야 할 사실과 후속 체크포인트를 짧게 정리합니다.",
|
||||
"""
|
||||
현재 작업에서 다음 반복이나 다음 세션에도 유지해야 할 핵심 사실만 추려 정리하세요.
|
||||
파일 경로, 결정 사항, 미해결 리스크, 다음 확인 지점을 짧은 메모 형식으로 요약하세요.
|
||||
""",
|
||||
whenToUse: "세션이 길어지거나, 다음 반복을 위해 핵심 사실을 압축해 남겨야 할 때",
|
||||
allowedTools: "memory, file_read, task_list"),
|
||||
CreateBundledSkill(
|
||||
"config-tune",
|
||||
"설정 튜닝",
|
||||
"현재 설정/정책 상태를 바탕으로 실행 품질과 안전성을 조정합니다.",
|
||||
"""
|
||||
현재 작업 목적에 맞춰 설정을 점검하고, 변경이 필요한 항목과 그대로 둬야 하는 항목을 구분하세요.
|
||||
성능, 권한, 스킬 source, 도구 노출 관점에서 추천 변경안을 우선순위로 제시하세요.
|
||||
""",
|
||||
whenToUse: "권한, 스킬 source, 도구 노출, 자동화 설정을 조정해야 할 때",
|
||||
allowedTools: "skill_manager, env_tool, mcp_list_resources"),
|
||||
CreateBundledSkill(
|
||||
"change-guard",
|
||||
"변경 가드",
|
||||
"수정 전에 영향 범위와 회귀 가능성을 빠르게 짚어줍니다.",
|
||||
"""
|
||||
변경 대상의 영향 범위, 연관 파일, 테스트 누락 가능성을 먼저 정리하세요.
|
||||
실제 수정 전에 어떤 확인이 필요한지와 수정 후 어떤 검증을 해야 하는지를 함께 제시하세요.
|
||||
""",
|
||||
whenToUse: "수정 전에 영향 범위와 회귀 리스크를 빠르게 훑어야 할 때",
|
||||
allowedTools: "file_read, grep, lsp_code_intel, folder_map"),
|
||||
CreateBundledSkill(
|
||||
"delivery-pack",
|
||||
"전달 패키지",
|
||||
"배포/공유 직전 필요한 결과물과 커뮤니케이션 패키지를 정리합니다.",
|
||||
"""
|
||||
배포 또는 공유 직전에 필요한 요약, 변경 포인트, 검증 상태, 사용자 전달 문구를 정리하세요.
|
||||
문서, PPT, 보고서가 함께 필요한 경우 어떤 산출물을 어떤 순서로 만들지 제안하세요.
|
||||
""",
|
||||
whenToUse: "배포 직전, 보고 직전, 문서/PPT와 함께 전달 패키지를 정리해야 할 때",
|
||||
allowedTools: "document_plan, document_assemble, pptx_create, docx_create, markdown_create")
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1172,13 +1307,7 @@ public static class SkillService
|
||||
score += 4;
|
||||
if (!skill.UserInvocable)
|
||||
score += 1;
|
||||
score += skill.SourceScope switch
|
||||
{
|
||||
"project" => 3,
|
||||
"plugin" => 2,
|
||||
"bundled" => 1,
|
||||
_ => 0,
|
||||
};
|
||||
score += Math.Max(0, GetSkillSourcePriority(skill.SourceScope) / 200);
|
||||
if (skill.DisableModelInvocation)
|
||||
score += 1;
|
||||
|
||||
@@ -1338,11 +1467,14 @@ public static class SkillService
|
||||
return input;
|
||||
|
||||
var result = input;
|
||||
var app = System.Windows.Application.Current as App;
|
||||
var llm = app?.SettingsService?.Settings.Llm;
|
||||
var blockedReason = GetInlineShellBlockedReason(skill, llm);
|
||||
foreach (Match match in matches.Cast<Match>().Reverse())
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var command = match.Groups[1].Value.Trim();
|
||||
var output = await ExecuteInlineShellAsync(command, shellKind, workFolder, ct).ConfigureAwait(false);
|
||||
var output = blockedReason ?? await ExecuteInlineShellAsync(command, shellKind, workFolder, ct).ConfigureAwait(false);
|
||||
result = result.Remove(match.Index, match.Length).Insert(match.Index, output);
|
||||
}
|
||||
|
||||
@@ -1425,6 +1557,19 @@ public static class SkillService
|
||||
}
|
||||
}
|
||||
|
||||
internal static string? GetInlineShellBlockedReason(SkillDefinition skill, Models.LlmSettings? llm)
|
||||
{
|
||||
if (llm is { EnableSkillInlineShell: false })
|
||||
return "[inline-shell disabled by settings]";
|
||||
|
||||
return (skill.SourceScope ?? "").Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"plugin" when !(llm?.AllowPluginSkillInlineShell ?? false) => "[inline-shell blocked for plugin skills]",
|
||||
"mcp" when !(llm?.AllowMcpSkillInlineShell ?? false) => "[inline-shell blocked for mcp skills]",
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
private static string NormalizeEffort(string raw)
|
||||
{
|
||||
var effort = raw.Trim().ToLowerInvariant();
|
||||
@@ -1734,6 +1879,8 @@ public sealed record CompiledSkillInvocation(
|
||||
string SystemPrompt,
|
||||
string DisplayText);
|
||||
|
||||
internal sealed record SkillLoadCandidate(int LoadOrder, SkillDefinition Skill);
|
||||
|
||||
internal sealed record SkillSourceDescriptor(string Directory, string Scope, SkillSourceKind Kind);
|
||||
|
||||
internal enum SkillSourceKind
|
||||
|
||||
@@ -53,6 +53,7 @@ public class ToolRegistry : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
McpSkillCatalog.RefreshFromClients(_mcpClients.Values);
|
||||
return registered;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user