需要你确认
diff --git a/MAF1.Web/wwwroot/js/app.js b/MAF1.Web/wwwroot/js/app.js
index b91114b..eaf9b36 100644
--- a/MAF1.Web/wwwroot/js/app.js
+++ b/MAF1.Web/wwwroot/js/app.js
@@ -1,3 +1,14 @@
+/**
+ * 可视化编排器前端(无框架)。
+ *
+ * 学习路径:
+ * 1. init → loadCatalog:拉系统节点和插件节点
+ * 2. 示例图 / 拖节点、点端口连线;图数据在 state.nodes / state.edges
+ * 3. runGraph POST /api/run;若 status=needsDecision 弹出确认框
+ * 4. 确认走 /api/run/{id}/decide;超时则 pollRun 等服务端自动采用默认方案
+ *
+ * selected / pending 只用于点击交互。发给后端的 JSON 不含 API Key,只有 credentialId。
+ */
const state = {
catalog: [],
system: [],
@@ -44,6 +55,7 @@ function pickNode(list, inputName) {
return (list || []).find((item) => item.inputs?.some((p) => p.name === inputName));
}
+/** 生成「抽城市 → 有城市才查天气」的示例图。连线 when=hasValidCities。 */
function exampleGraph() {
const fileInfo = pickNode(state.system, "filePath") || pickNode(state.catalog, "filePath");
const weatherInfo = pickNode(state.plugins, "cities") || pickNode(state.system, "cities") || pickNode(state.catalog, "cities");
@@ -110,6 +122,7 @@ function addNode(type) {
render();
}
+/** 根据 state 重画节点 DOM;连线是 SVG,在 drawWires。 */
function render() {
canvas.innerHTML = "";
for (const node of state.nodes) {
@@ -172,6 +185,7 @@ function startDrag(e, node) {
window.addEventListener("mouseup", up);
}
+/** 先点输出端口记下 pending,再点输入端口生成一条边。 */
function onPort(nodeId, port, dir) {
if (dir === "out") {
state.pending = { nodeId, port };
@@ -293,6 +307,7 @@ function renderInspector() {
};
}
+/** 把画布图交给后端。可能直接完成,也可能弹出 decisionModal。 */
async function runGraph() {
closeDecision();
logEl.textContent = "运行中…";
@@ -379,6 +394,7 @@ function syncDecisionText() {
}
}
+/** 倒计时到 0 后去 GET 运行结果,因为服务端会自己按默认方案继续。 */
function startDecisionTimer(deadline) {
if (decisionTimer) {
clearInterval(decisionTimer);
@@ -447,6 +463,7 @@ decisionSubmit.onclick = async () => {
}
};
+/** 拉节点目录和凭据列表,刷新左侧面板。 */
async function loadCatalog() {
const [catalog, credentials] = await Promise.all([
(await fetch("/api/catalog")).json(),
diff --git a/MAF1/CliHost.cs b/MAF1/CliHost.cs
index da1c598..51c32b7 100644
--- a/MAF1/CliHost.cs
+++ b/MAF1/CliHost.cs
@@ -10,6 +10,10 @@ using Microsoft.Extensions.Configuration;
namespace MAF1;
+///
+/// 命令行宿主:读配置、创建两个 Agent、按 node/edge 选一张工作流图。
+/// node = 判断写在 CityGate 节点里;edge = 判断写在 AddEdge 的 condition 上。两条路缺城市时都会等人确认。
+///
internal static class CliHost
{
public static async Task RunAsync(string[] args)
@@ -39,6 +43,7 @@ internal static class CliHost
Console.WriteLine($"决策等待: {decisionTimeoutSeconds} 秒(超时采用默认方案:结束查询)");
Console.WriteLine();
+ // 同一套 Agent,两张不同的图。对比源码时重点看 CityWeatherWorkflow 里 AddEdge 的差异。
Workflow workflow = mode == "edge"
? CityWeatherWorkflow.BuildEdgeCondition(fileCityAgent, weatherAgent, decisionTimeoutSeconds)
: CityWeatherWorkflow.BuildNodeCondition(fileCityAgent, weatherAgent, decisionTimeoutSeconds);
@@ -48,6 +53,7 @@ internal static class CliHost
await WorkflowOrchestration.RunAsync(workflow, label, filePath);
}
+ /// 无参数时默认 node + Data/cities.txt。--cli 是旧兼容前缀,可忽略。
private static bool TryParseArgs(string[] args, out string mode, out string filePath)
{
mode = "node";
diff --git a/MAF1/Orchestration/ConsoleDecisionPrompt.cs b/MAF1/Orchestration/ConsoleDecisionPrompt.cs
index 22cac8a..3d4e27c 100644
--- a/MAF1/Orchestration/ConsoleDecisionPrompt.cs
+++ b/MAF1/Orchestration/ConsoleDecisionPrompt.cs
@@ -2,6 +2,10 @@ using MAF1.Decisions;
namespace MAF1.Orchestration;
+///
+/// CLI 版确认框。回车 / 1 = 结束查询;2 = 再输入城市;也可以直接打「成都」。
+/// ReadLine 和超时赛跑:到期返回 TimedOut,工作流走默认方案。
+///
internal static class ConsoleDecisionPrompt
{
public static async Task WaitAsync(DecisionRequest request)
@@ -70,6 +74,7 @@ internal static class ConsoleDecisionPrompt
return new DecisionAnswer { OptionId = DecisionOptionIds.Stop };
}
+ /// 超时返回 null。注意:超时后后台的 ReadLine 仍可能阻塞,学习项目可接受。
private static async Task ReadLineOrTimeoutAsync(DateTimeOffset deadline)
{
TimeSpan remain = deadline - DateTimeOffset.UtcNow;
diff --git a/MAF1/Orchestration/WorkflowOrchestration.cs b/MAF1/Orchestration/WorkflowOrchestration.cs
index 4ca434d..e9ddb34 100644
--- a/MAF1/Orchestration/WorkflowOrchestration.cs
+++ b/MAF1/Orchestration/WorkflowOrchestration.cs
@@ -4,6 +4,10 @@ using Microsoft.Agents.AI.Workflows;
namespace MAF1.Orchestration;
+///
+/// 把 Workflow 事件打到控制台。核心循环:WatchStreamAsync。
+/// 遇到 RequestInfoEvent 说明图在 RequestPort 上等人,这里弹出 ConsoleDecisionPrompt 再 SendResponseAsync。
+///
public static class WorkflowOrchestration
{
public static async Task RunAsync(Workflow workflow, string modeLabel, string filePath)
@@ -13,6 +17,7 @@ public static class WorkflowOrchestration
string prompt = $"请读取这个文件并提取有效城市名:{filePath}";
await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, prompt);
+ // TurnToken 通知 Agent Executor「可以开始一轮」;emitEvents 才会推送流式文本。
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
string? lastExecutorId = null;
@@ -21,6 +26,7 @@ public static class WorkflowOrchestration
switch (evt)
{
case RequestInfoEvent requestEvent:
+ // 人机环:工作流挂起 → 控制台询问 → 把 DecisionAnswer 送回 RequestPort。
DecisionRequest decision = ReadDecision(requestEvent.Request);
DecisionAnswer answer = await ConsoleDecisionPrompt.WaitAsync(decision);
await run.SendResponseAsync(requestEvent.Request.CreateResponse(answer));
diff --git a/MAF1/Program.cs b/MAF1/Program.cs
index f0ce8ca..45da5f7 100644
--- a/MAF1/Program.cs
+++ b/MAF1/Program.cs
@@ -1,4 +1,6 @@
-using MAF1;
+// 控制台入口。学习时从这里开始:参数解析 → 组装 Agent → 跑 Microsoft Agents AI Workflow。
+// 可视化编排在另一个项目 MAF1.Web,本进程不听 HTTP。
+using MAF1;
using MAF1.Utils;
WindowsConsole.EnableUtf8();
diff --git a/MAF1/Workflows/AskCityDecisionExecutor.cs b/MAF1/Workflows/AskCityDecisionExecutor.cs
index 49ee289..9a6ece2 100644
--- a/MAF1/Workflows/AskCityDecisionExecutor.cs
+++ b/MAF1/Workflows/AskCityDecisionExecutor.cs
@@ -5,7 +5,7 @@ using Microsoft.Agents.AI.Workflows;
namespace MAF1.Workflows;
///
-/// 边条件模式:没有城市时不在边上直接结束,而是发出决策请求。
+/// 边条件模式:没有城市时不在边上直接结束,而是发出 DecisionRequest,进入 RequestPort 等人。
///
internal sealed class AskCityDecisionExecutor(int timeoutSeconds) : Executor("AskCityDecision")
{
diff --git a/MAF1/Workflows/CityGateExecutor.cs b/MAF1/Workflows/CityGateExecutor.cs
index a9d8a8f..9a9542e 100644
--- a/MAF1/Workflows/CityGateExecutor.cs
+++ b/MAF1/Workflows/CityGateExecutor.cs
@@ -5,6 +5,10 @@ using Microsoft.Extensions.AI;
namespace MAF1.Workflows;
+///
+/// node 模式的闸门:解析 FileCity 文本,有城市就改写成查天气的用户消息;没有就发出确认请求。
+/// 判断发生在这个类内部,所以叫「条件写在节点里」。
+///
internal sealed class CityGateExecutor(int decisionTimeoutSeconds) : ChatProtocolExecutor(
"CityGate",
new ChatProtocolExecutorOptions { AutoSendTurnToken = false })
diff --git a/MAF1/Workflows/CityParseExecutor.cs b/MAF1/Workflows/CityParseExecutor.cs
index 5005b57..09f2bb4 100644
--- a/MAF1/Workflows/CityParseExecutor.cs
+++ b/MAF1/Workflows/CityParseExecutor.cs
@@ -5,7 +5,7 @@ using Microsoft.Extensions.AI;
namespace MAF1.Workflows;
///
-/// 边条件模式:这里只解析,不做 if。走哪条边由 AddEdge 的 condition 决定。
+/// 边条件模式:这里只解析 FileCity 文本,不做 if。走哪条边由 AddEdge 的 condition 决定。
///
internal sealed class CityParseExecutor() : ChatProtocolExecutor(
"CityParse",
diff --git a/MAF1/Workflows/CityWeatherWorkflow.cs b/MAF1/Workflows/CityWeatherWorkflow.cs
index 2b736d6..6bb704e 100644
--- a/MAF1/Workflows/CityWeatherWorkflow.cs
+++ b/MAF1/Workflows/CityWeatherWorkflow.cs
@@ -5,8 +5,16 @@ using MAF1.Agents.FileCity;
namespace MAF1.Workflows;
+///
+/// 用 Microsoft.Agents.AI.Workflows 拼「抽城市 → 查天气」两张对照图。
+/// 建议对照 BuildNodeCondition / BuildEdgeCondition:前者 if 在 CityGate 里,后者 if 在边上。
+/// RequestPort 是官方的「向外要一次人工输入」端口,对应网页的 needsDecision。
+///
public static class CityWeatherWorkflow
{
+ ///
+ /// 节点内判断:FileCity → CityGate。有城市则 Gate 直接催 Weather;没有则发 DecisionRequest。
+ ///
public static Workflow BuildNodeCondition(AIAgent fileCityAgent, AIAgent weatherAgent, int decisionTimeoutSeconds)
{
(ExecutorBinding fileCity, ExecutorBinding weather) = BindAgents(fileCityAgent, weatherAgent);
@@ -27,6 +35,9 @@ public static class CityWeatherWorkflow
.Build();
}
+ ///
+ /// 边条件:Parse 之后两条边,condition 看 HasValidCities。没有城市走 Ask → RequestPort,而不是直接结束。
+ ///
public static Workflow BuildEdgeCondition(AIAgent fileCityAgent, AIAgent weatherAgent, int decisionTimeoutSeconds)
{
(ExecutorBinding fileCity, ExecutorBinding weather) = BindAgents(fileCityAgent, weatherAgent);
@@ -49,6 +60,7 @@ public static class CityWeatherWorkflow
.Build();
}
+ /// 把 AIAgent 嵌进 Workflow。ForwardIncomingMessages=false 避免把上游闲聊原样转给下游。
private static (ExecutorBinding FileCity, ExecutorBinding Weather) BindAgents(AIAgent fileCityAgent, AIAgent weatherAgent)
{
AIAgentHostOptions agentOptions = new()
diff --git a/Plugins/file-city/Program.cs b/Plugins/file-city/Program.cs
index 3fe660e..bd933bc 100644
--- a/Plugins/file-city/Program.cs
+++ b/Plugins/file-city/Program.cs
@@ -1,3 +1,5 @@
+// 进程外 FileCity 插件。宿主启动本 exe,stdin 给 JSON,stdout 只回输出对象。
+// 业务逻辑全部复用 MAF1.Core 里的 FileCityAgent,这里只做协议适配。
using MAF1.Agents.FileCity;
using MAF1.PluginContract;
using MAF1.Utils;
diff --git a/Plugins/weather/Program.cs b/Plugins/weather/Program.cs
index 009c736..bb3c97d 100644
--- a/Plugins/weather/Program.cs
+++ b/Plugins/weather/Program.cs
@@ -1,3 +1,4 @@
+// 进程外 Weather 插件。同样只做 stdio 适配,天气实现仍在 MAF1.Core。
using MAF1.Agents.Weather;
using MAF1.PluginContract;
using MAF1.Tools;