feat: 新增方案 C 的 Handoff 路由宿主,并让抽城市支持自然语言。

把图 JSON 编译成 CreateHandoffBuilderWith + WithHandoff;Agent 固定 Id 以免交接工具名错位;DeepSeek 关闭 thinking,避免多轮丢掉 reasoning_content 导致 400。
This commit is contained in:
2026-09-01 17:56:41 +08:00
parent 2c5c6fd118
commit b0e89b6092
25 changed files with 960 additions and 27 deletions
+108
View File
@@ -0,0 +1,108 @@
using System.Text.Json;
using MAF1.Agents;
namespace MAF1.Route;
/// <summary>
/// 方案 C 的图:节点 = 主管能调的专家白名单;边 = 允许的交接。
/// 没有边时,名单上的节点可以按任意顺序调用。
/// </summary>
public sealed class RouteGraph
{
public List<RouteNode> Nodes { get; set; } = [];
public List<RouteEdge> Edges { get; set; } = [];
public bool HasEdges => Edges.Count > 0;
/// <summary>主管第一跳只能到这些节点:无边时整张白名单;有边时取没有入边的入口。</summary>
public IReadOnlyList<RouteNode> EntryNodes()
{
if (!HasEdges)
{
return Nodes;
}
HashSet<string> targets = new(Edges.Select(e => e.To), StringComparer.OrdinalIgnoreCase);
List<RouteNode> sources = Nodes.Where(n => !targets.Contains(n.Id)).ToList();
return sources.Count > 0 ? sources : Nodes;
}
public bool HasOutgoing(string nodeId)
=> Edges.Any(e => e.From.Equals(nodeId, StringComparison.OrdinalIgnoreCase));
public RouteNode? Find(string nodeId)
=> Nodes.FirstOrDefault(n => n.Id.Equals(nodeId, StringComparison.OrdinalIgnoreCase));
public static JsonSerializerOptions JsonOptions { get; } = new()
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true,
};
public static RouteGraph LoadFile(string path)
{
string json = File.ReadAllText(path);
RouteGraph graph = JsonSerializer.Deserialize<RouteGraph>(json, JsonOptions)
?? throw new InvalidOperationException($"无法解析路由图:{path}");
graph.Validate();
return graph;
}
public void Validate()
{
if (Nodes.Count == 0)
{
throw new InvalidOperationException("图里没有任何节点。方案 C 至少要放一个专家到白名单。");
}
HashSet<string> ids = new(StringComparer.OrdinalIgnoreCase);
foreach (RouteNode node in Nodes)
{
if (string.IsNullOrWhiteSpace(node.Id))
{
throw new InvalidOperationException("节点缺少 id。");
}
if (!ids.Add(node.Id))
{
throw new InvalidOperationException($"节点 id 重复:{node.Id}");
}
if (!SystemNodeTypes.IsKnown(node.Type))
{
throw new InvalidOperationException(
$"节点 `{node.Id}` 的类型 `{node.Type}` 不是系统专家。当前支持 {SystemNodeTypes.FileCity} / {SystemNodeTypes.Weather}。");
}
}
foreach (RouteEdge edge in Edges)
{
if (Find(edge.From) is null)
{
throw new InvalidOperationException($"边的 from `{edge.From}` 不在图上。");
}
if (Find(edge.To) is null)
{
throw new InvalidOperationException($"边的 to `{edge.To}` 不在图上。");
}
}
}
}
public sealed class RouteNode
{
public string Id { get; set; } = "";
public string Type { get; set; } = "";
public string Title { get; set; } = "";
public Dictionary<string, string> Config { get; set; } = new(StringComparer.OrdinalIgnoreCase);
}
public sealed class RouteEdge
{
public string From { get; set; } = "";
public string To { get; set; } = "";
/// <summary>可选。hasValidCities / !hasValidCities / 空=只要拓扑允许就可以走。</summary>
public string? When { get; set; }
}