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
+84
View File
@@ -0,0 +1,84 @@
using MAF1.Tools;
using MAF1.Utils;
using Microsoft.Extensions.Configuration;
namespace MAF1.Route;
/// <summary>
/// 命令行宿主:读配置和图 JSON,用 MAF WithHandoff 按方案 C 跑路由。
/// </summary>
internal static class CliHost
{
public static async Task RunAsync(string[] args)
{
if (args.Length > 0 && args[0] is "-h" or "--help")
{
PrintUsage();
return;
}
IConfiguration config = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false)
.AddEnvironmentVariables()
.Build();
string graphPath = args.Length > 0 ? args[0] : Path.Combine("Graphs", "city-weather.json");
string? resolvedGraph = ResolveExisting(graphPath);
if (resolvedGraph is null)
{
Console.WriteLine($"找不到路由图:{graphPath}");
PrintUsage();
return;
}
RouteGraph graph = RouteGraph.LoadFile(resolvedGraph);
Console.WriteLine($"路由图: {resolvedGraph}");
Console.WriteLine();
RouteUserInput input = RouteUserInput.ReadFromConsole(graph);
if (!input.HasRaw)
{
Console.WriteLine("没有输入,已取消。");
return;
}
Console.WriteLine($"本次输入: {input.Describe()}");
Console.WriteLine();
AgentFactory factory = new(AgentFactory.Load(config));
WeatherOptions weatherOptions = config.GetSection("Weather").Get<WeatherOptions>() ?? new WeatherOptions();
using HttpClient http = WeatherTools.CreateHttpClient();
WeatherTools weatherTools = new(weatherOptions, http);
Console.WriteLine($"天气数据源: {weatherOptions.Provider}");
Console.WriteLine();
await RouteOrchestrator.RunAsync(factory, weatherTools, graph, input);
}
private static string? ResolveExisting(string path)
=> RouteUserInput.ResolveExisting(path);
private static void PrintUsage()
{
Console.WriteLine(
"""
C + MAF WithHandoff:
dotnet run --project MAF1.Route
dotnet run --project MAF1.Route -- Graphs/city-weather.json
dotnet run --project MAF1.Route -- Graphs/nodes-only.json
Data/cities.txt
config filePath
city-weather.json
nodes-only.json
Handoff:
dotnet run --project MAF1 -- node Data/cities.txt
""");
}
}
+3
View File
@@ -0,0 +1,3 @@
成都
阿姆斯特丹
北京
+3
View File
@@ -0,0 +1,3 @@
香蕉
hello
不是城市
+22
View File
@@ -0,0 +1,22 @@
{
"nodes": [
{
"id": "n1",
"type": "fileCity-system",
"title": "抽城市",
"config": { "filePath": "Data/cities.txt" }
},
{
"id": "n2",
"type": "weather-system",
"title": "查天气"
}
],
"edges": [
{
"from": "n1",
"to": "n2",
"when": "hasValidCities"
}
]
}
+16
View File
@@ -0,0 +1,16 @@
{
"nodes": [
{
"id": "n1",
"type": "fileCity-system",
"title": "抽城市",
"config": { "filePath": "Data/cities.txt" }
},
{
"id": "n2",
"type": "weather-system",
"title": "查天气"
}
],
"edges": []
}
+177
View File
@@ -0,0 +1,177 @@
using MAF1.Agents;
using MAF1.Agents.FileCity;
using MAF1.Agents.Weather;
using MAF1.Tools;
using MAF1.Utils;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
namespace MAF1.Route;
/// <summary>
/// 把方案 C 的图编译成 MAF Handoff:节点 = 白名单(主管可第一跳交给其中任何一个);
/// 边 = 专家之间允许的交接。主管根据用户原话自己选第一跳,不必先经过拓扑入口。
/// </summary>
public static class HandoffGraphBuilder
{
public static Workflow Build(AgentFactory factory, WeatherTools weatherTools, RouteGraph graph, RouteUserInput input)
{
AIAgent router = factory.CreateAgent(
name: "router",
description: "路由主管:根据用户任务把对话交接给图上允许的专家,自己不读文件、不查天气。",
instructions: BuildRouterInstructions(graph, input));
Dictionary<string, AIAgent> specialists = new(StringComparer.OrdinalIgnoreCase);
foreach (RouteNode node in graph.Nodes)
{
specialists[node.Id] = CreateSpecialist(factory, weatherTools, node, graph, input);
}
HandoffWorkflowBuilder builder = AgentWorkflowBuilder.CreateHandoffBuilderWith(router);
foreach (RouteNode node in graph.Nodes)
{
builder.WithHandoff(router, specialists[node.Id], HandoffReason(node, fromRouter: true, when: null));
}
foreach (RouteEdge edge in graph.Edges)
{
builder.WithHandoff(
specialists[edge.From],
specialists[edge.To],
HandoffReason(graph.Find(edge.To)!, fromRouter: false, when: edge.When));
}
List<AIAgent> mustHandoff = [router];
foreach (RouteNode node in graph.Nodes)
{
if (graph.HasOutgoing(node.Id))
{
mustHandoff.Add(specialists[node.Id]);
}
}
return builder
.WithName("Route-Handoff")
.EmitAgentResponseUpdateEvents()
.EmitAgentResponseEvents()
.WithAutonomousMode(
turnLimit: 4,
continuationPrompt: "你还没有完成交接。请立即调用 handoff 工具把对话交给下一位专家,不要自己结束本轮。",
agents: mustHandoff)
.Build();
}
private static AIAgent CreateSpecialist(
AgentFactory factory,
WeatherTools weatherTools,
RouteNode node,
RouteGraph graph,
RouteUserInput input)
{
string title = string.IsNullOrWhiteSpace(node.Title) ? node.Type : node.Title;
string extra = BuildSpecialistExtra(node, graph, input);
return node.Type switch
{
SystemNodeTypes.FileCity => FileCityAgent.Create(
factory,
name: node.Id,
description: $"{title}{FileCityAgent.DefaultDescription}",
extraInstructions: extra),
SystemNodeTypes.Weather => WeatherAgent.Create(
factory,
weatherTools,
name: node.Id,
description: $"{title}{WeatherAgent.DefaultDescription}",
extraInstructions: extra),
_ => throw new InvalidOperationException($"未知节点类型 {node.Type}"),
};
}
private static string BuildRouterInstructions(RouteGraph graph, RouteUserInput input)
{
string entries = string.Join("、", graph.Nodes.Select(n => $"{n.Id}({n.Type})"));
string edges = graph.HasEdges
? string.Join("", graph.Edges.Select(e => $"{e.From}->{e.To}" + (string.IsNullOrWhiteSpace(e.When) ? "" : $"[{e.When}]")))
: "专家之间无边";
return
$"""
你是交接图上的主管。禁止自己读文件或查询天气。
白名单专家:{entries}
收到用户任务后,必须立刻 handoff 给最合适的一位,不要经过用不上的专家。
判断规则:
- 用户已经给出明确城市名、或只要查天气(如「拉斯维加斯」「帮我查成都天气」)→ 直接交给天气专家,不要先抽城市。
- 用户给的是文件路径、或一段需要从中识别城市的长文本 → 先交给抽城市专家。
专家之间的边:{edges}
不要向用户叙述交接过程,也不要在未交接时直接结束。
""";
}
private static string BuildSpecialistExtra(RouteNode node, RouteGraph graph, RouteUserInput input)
{
List<string> lines =
[
$"你是交接图上的节点 `{node.Id}`。",
"完成自己的工作后:若有下游专家,必须调用框架注入的 handoff 工具把对话交出去;不要只输出结果就结束。",
"若没有有效输入(例如抽不到城市),不要交接,用中文说明原因。",
"不要向用户叙述交接过程。",
];
if (node.Type == SystemNodeTypes.FileCity)
{
lines.Add(
"""
//
- ReadTextFile
-
""");
}
List<RouteEdge> outgoing = graph.Edges
.Where(e => e.From.Equals(node.Id, StringComparison.OrdinalIgnoreCase))
.ToList();
if (outgoing.Count == 0)
{
lines.Add("你没有下游专家。做完后直接回答用户,不要再交接。");
}
else
{
lines.Add("下游:" + string.Join("", outgoing.Select(e =>
{
RouteNode to = graph.Find(e.To)!;
string when = string.IsNullOrWhiteSpace(e.When) ? "" : $",条件 {e.When}";
return $"{to.Id}{to.Type}{when}";
})));
}
return string.Join("\n", lines);
}
private static string HandoffReason(RouteNode target, bool fromRouter, string? when)
{
string title = string.IsNullOrWhiteSpace(target.Title) ? target.Type : target.Title;
string role = target.Type switch
{
SystemNodeTypes.FileCity => FileCityAgent.DefaultDescription,
SystemNodeTypes.Weather => WeatherAgent.DefaultDescription,
_ => target.Type,
};
string prefix = fromRouter
? target.Type switch
{
SystemNodeTypes.FileCity => "用户给了文件或需要从文本里识别城市时,交给这位专家。已经是明确城市名、只要查天气时不要交给他。",
SystemNodeTypes.Weather => "用户已经给出城市名或明确要查天气时,直接交给这位专家,不必先抽城市。",
_ => "把用户任务交给这位专家。",
}
: "把对话交给下一位专家。";
string condition = string.IsNullOrWhiteSpace(when)
? ""
: when.Equals("hasValidCities", StringComparison.OrdinalIgnoreCase)
? " 仅当已经抽出有效城市时才交接。"
: when.Equals("!hasValidCities", StringComparison.OrdinalIgnoreCase)
? " 仅当没有有效城市时才交接。"
: $" 条件:{when}。";
return $"{prefix} 节点 {target.Id}{title}):{role}{condition}";
}
}
+165
View File
@@ -0,0 +1,165 @@
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
namespace MAF1.Route;
/// <summary>
/// 跑 Handoff。工具日志按 callId 去重:同一调用会同时出现在流式事件、完整回复、结束输出里,只打一遍。
/// </summary>
public static class HandoffRun
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
public static async Task RunAsync(Workflow workflow, string task)
{
HashSet<string> printedCalls = new(StringComparer.Ordinal);
List<ChatMessage> messages = [new(ChatRole.User, task)];
await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, messages);
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
string? lastExecutorId = null;
string? lastSummary = null;
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
switch (evt)
{
case ExecutorFailedEvent failed:
Console.WriteLine();
Console.WriteLine($"[失败] {failed.ExecutorId}: {failed.Data}");
break;
case AgentResponseUpdateEvent update:
if (update.ExecutorId != lastExecutorId)
{
Console.WriteLine();
Console.Write($"{Friendly(update.ExecutorId)}: ");
lastExecutorId = update.ExecutorId;
}
if (!string.IsNullOrEmpty(update.Update.Text))
{
Console.Write(update.Update.Text);
}
WriteNewTools(update.ExecutorId, update.Update.Contents, printedCalls);
break;
case WorkflowOutputEvent output:
string? summary = ReadSummary(output);
if (!string.IsNullOrWhiteSpace(summary) && summary != lastSummary)
{
lastSummary = summary;
Console.WriteLine();
Console.WriteLine();
Console.WriteLine($"[最终输出] {Trim(summary)}");
}
break;
}
}
Console.WriteLine();
Console.WriteLine("======== Handoff 本轮结束 ========");
}
private static void WriteNewTools(string executorId, IList<AIContent> contents, HashSet<string> printedCalls)
{
foreach (AIContent content in contents)
{
if (content is FunctionCallContent call)
{
string key = "call:" + (string.IsNullOrWhiteSpace(call.CallId) ? call.Name + FormatArguments(call.Arguments) : call.CallId);
if (!printedCalls.Add(key))
{
continue;
}
Console.WriteLine();
Console.WriteLine($"[工具调用] {Friendly(executorId)} → {call.Name}");
Console.WriteLine($" 参数: {FormatArguments(call.Arguments)}");
continue;
}
if (content is FunctionResultContent result)
{
string key = "result:" + result.CallId;
if (!printedCalls.Add(key))
{
continue;
}
Console.WriteLine();
Console.WriteLine($"[工具返回] {Friendly(executorId)} → {result.CallId}");
Console.WriteLine($" 结果: {Trim(FormatResult(result.Result))}");
}
}
}
private static string? ReadSummary(WorkflowOutputEvent output)
{
if (output.As<List<ChatMessage>>() is List<ChatMessage> chat)
{
return LastAssistantText(chat);
}
return output.As<string>();
}
private static string FormatArguments(IDictionary<string, object?>? arguments)
{
if (arguments is null || arguments.Count == 0)
{
return "{}";
}
try
{
return JsonSerializer.Serialize(arguments, JsonOptions);
}
catch
{
return string.Join(", ", arguments.Select(kv => $"{kv.Key}={kv.Value}"));
}
}
private static string FormatResult(object? result)
=> result is null ? "(空)" : result.ToString() ?? "(空)";
private static string LastAssistantText(List<ChatMessage> chat)
{
string text = "";
foreach (ChatMessage message in chat)
{
if (message.Role == ChatRole.Assistant && !string.IsNullOrWhiteSpace(message.Text))
{
text = message.Text;
}
}
return text;
}
private static string Friendly(string executorId)
{
int us = executorId.IndexOf('_');
if (us > 0 && executorId[(us + 1)..] == executorId[..us])
{
return executorId[..us];
}
return executorId;
}
private static string Trim(string text)
{
string oneLine = text.Replace("\r\n", " ").Replace('\n', ' ').Trim();
return oneLine.Length <= 500 ? oneLine : oneLine[..500] + "…";
}
}
+35
View File
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>MAF1.Route</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Workflows" Version="1.19.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MAF1.Core\MAF1.Core.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Data\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Graphs\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
+5
View File
@@ -0,0 +1,5 @@
using MAF1.Route;
using MAF1.Utils;
WindowsConsole.EnableUtf8();
await CliHost.RunAsync(args);
+22
View File
@@ -0,0 +1,22 @@
{
"profiles": {
"route": {
"commandName": "Project",
"commandLineArgs": "Graphs/city-weather.json",
"environmentVariables": {
"OPENAI_ENDPOINT": "https://api.deepseek.com",
"OPENAI_CHAT_MODEL": "deepseek-v4-flash",
"OPENAI_API_KEY": "sk-b31330cf3a414d86ab614b9dfb3650c3"
}
},
"nodes-only": {
"commandName": "Project",
"commandLineArgs": "Graphs/nodes-only.json 请查询成都的天气",
"environmentVariables": {
"OPENAI_ENDPOINT": "https://api.deepseek.com",
"OPENAI_CHAT_MODEL": "deepseek-v4-flash",
"OPENAI_API_KEY": "sk-b31330cf3a414d86ab614b9dfb3650c3"
}
}
}
}
+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; }
}
+35
View File
@@ -0,0 +1,35 @@
using MAF1.Tools;
using MAF1.Utils;
using Microsoft.Agents.AI.Workflows;
namespace MAF1.Route;
/// <summary>把图编译成 WithHandoff 工作流并跑一轮。</summary>
public static class RouteOrchestrator
{
public static async Task RunAsync(
AgentFactory factory,
WeatherTools weatherTools,
RouteGraph graph,
RouteUserInput input)
{
string task = input.ToTask();
Console.WriteLine("模式 route:方案 C + MAF WithHandoff(图 = 白名单,边 = 允许交接)");
Console.WriteLine($"图节点: {string.Join("", graph.Nodes.Select(DescribeNode))}");
Console.WriteLine(graph.HasEdges
? $"专家之间的边: {string.Join("", graph.Edges.Select(e => $"{e.From}->{e.To}" + (string.IsNullOrWhiteSpace(e.When) ? "" : $" [{e.When}]")))}"
: "专家之间无边");
Console.WriteLine($"主管可第一跳: {string.Join("", graph.Nodes.Select(n => n.Id))}(按用户原话选择,不必先抽城市)");
Console.WriteLine($"输入: {input.Describe()}");
Console.WriteLine();
Workflow workflow = HandoffGraphBuilder.Build(factory, weatherTools, graph, input);
await HandoffRun.RunAsync(workflow, task);
}
private static string DescribeNode(RouteNode node)
{
string title = string.IsNullOrWhiteSpace(node.Title) ? node.Type : node.Title;
return $"{node.Id}:{title}";
}
}
+97
View File
@@ -0,0 +1,97 @@
using MAF1.Agents;
namespace MAF1.Route;
/// <summary>控制台里用户原话。路径还是描述,交给专家自己判断,不在 C# 里写死。</summary>
public sealed class RouteUserInput
{
public string Raw { get; init; } = "";
public string? ResolvedFilePath { get; init; }
public bool HasRaw => !string.IsNullOrWhiteSpace(Raw);
public string Describe()
{
if (!string.IsNullOrWhiteSpace(ResolvedFilePath))
{
return $"已解析到文件 {ResolvedFilePath}";
}
string preview = Raw.Replace("\r\n", " ").Replace('\n', ' ').Trim();
if (preview.Length > 80)
{
preview = preview[..80] + "…";
}
return $"用户原话「{preview}」";
}
public string ToTask()
=> $"""
请根据用户下面这句话完成任务:提取有效城市,并在图允许时查询天气。
若这句话是本地文件路径,就读文件;若是描述、问句或城市名单,就直接从这句话提取,不要读文件。
用户输入:
{Raw}
""";
public static RouteUserInput ReadFromConsole(RouteGraph graph)
{
string? fallback = graph.Nodes
.Select(n => n.Type == SystemNodeTypes.FileCity && n.Config.TryGetValue("filePath", out string? path) ? path : null)
.FirstOrDefault(p => !string.IsNullOrWhiteSpace(p));
Console.WriteLine("请输入:本地文件路径,或一段描述/城市名单(例如:帮我查成都和杭州的天气)。");
if (!string.IsNullOrWhiteSpace(fallback))
{
Console.WriteLine($"直接回车则使用图里的默认文件:{fallback}");
}
Console.Write("> ");
string? line = Console.ReadLine();
string trimmed = line?.Trim() ?? "";
if (trimmed.Length == 0)
{
trimmed = fallback ?? "";
}
return new RouteUserInput
{
Raw = trimmed,
ResolvedFilePath = ResolveExisting(trimmed),
};
}
public static string? ResolveExisting(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
return null;
}
try
{
if (path.IndexOfAny(Path.GetInvalidPathChars()) >= 0)
{
return null;
}
}
catch (ArgumentException)
{
return null;
}
if (File.Exists(path))
{
return Path.GetFullPath(path);
}
string fromBase = Path.Combine(AppContext.BaseDirectory, path);
if (File.Exists(fromBase))
{
return Path.GetFullPath(fromBase);
}
return null;
}
}
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="MAF1.Route"/>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
</windowsSettings>
</application>
</assembly>
+21
View File
@@ -0,0 +1,21 @@
{
"Llm": {
"ApiKey": "",
"Endpoint": "https://api.deepseek.com",
"Model": "deepseek-v4-flash"
},
"Workflow": {
"DecisionTimeoutSeconds": 30
},
"Weather": {
"Provider": "Wttr",
"Language": "zh",
"Wttr": {
"UrlTemplate": "https://wttr.in/{location}?lang={lang}&format=3"
},
"OpenWeather": {
"UrlTemplate": "https://api.openweathermap.org/data/2.5/weather?q={location}&appid={apiKey}&units=metric&lang={lang}",
"ApiKey": ""
}
}
}