Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
F
Ftt_Common
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
Ftt_Common
Commits
41a89411
Commit
41a89411
authored
Oct 22, 2026
by
段廷瑞
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 支持分发模式(服务端统一配置apikey和知识库,其他用户自动拉取)
parent
0020a375
Changes
18
Show whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
1756 additions
and
177 deletions
+1756
-177
.gitignore
.gitignore
+2
-0
Tools.Agent.dll
AppHome/Tools.Agent.dll
+0
-0
Tools.Agent.xml
AppHome/Tools.Agent.xml
+314
-109
Program.cs
Tools.Agent.Host/Program.cs
+30
-1
AgentEngine.cs
Tools.Agent/Agent/AgentEngine.cs
+1
-1
DbQueryTool.cs
Tools.Agent/Agent/Tools/DbQueryTool.cs
+1
-1
AgentRuntime.cs
Tools.Agent/AgentRuntime.cs
+37
-5
AgentAdminForm.cs
Tools.Agent/Forms/AgentAdminForm.cs
+1
-1
AgentRunLogForm.Designer.cs
Tools.Agent/Forms/AgentRunLogForm.Designer.cs
+0
-13
AgentRunLogForm.cs
Tools.Agent/Forms/AgentRunLogForm.cs
+4
-34
AgentSettingsForm.cs
Tools.Agent/Forms/AgentSettingsForm.cs
+43
-3
RemoteEntryKb.cs
Tools.Agent/Kb/Remote/RemoteEntryKb.cs
+1
-1
AgentCenterClient.cs
Tools.Agent/Support/AgentCenterClient.cs
+1100
-0
AgentHealth.cs
Tools.Agent/Support/AgentHealth.cs
+35
-1
AgentKbProbe.cs
Tools.Agent/Support/AgentKbProbe.cs
+82
-0
Tools.Agent.csproj
Tools.Agent/Tools.Agent.csproj
+3
-0
wMyAgent.cs
Tools.Agent/wMyAgent.cs
+102
-6
wExtend.cs
Tools/wExtend.cs
+0
-1
No files found.
.gitignore
View file @
41a89411
...
...
@@ -78,3 +78,5 @@ bin
/AppHome/_backup
/Tools.Agent.ProbeKbPref
/working
/Tools.Agent/Defaults/Agent/config
/Tools.Agent.ProbeCenter
AppHome/Tools.Agent.dll
View file @
41a89411
No preview for this file type
AppHome/Tools.Agent.xml
View file @
41a89411
...
...
@@ -19,6 +19,18 @@
<member
name=
"P:Tools.MyAgent.AgentRuntimeOptions.ProductContext"
>
<summary>
宿主固定的产品上下文;空值表示未绑定产品线。
</summary>
</member>
<member
name=
"P:Tools.MyAgent.AgentRuntimeOptions.Mode"
>
<summary>
运行模式(config_center_plan.md v2.2):Custom=自定义(现状零改动);Distribution=分发(配置中心)。
缺省 null = 按
<
程序目录
>
/Agent/config/center.json 是否存在自动判定;显式传入时以入参为准。
</summary>
</member>
<member
name=
"P:Tools.MyAgent.AgentRuntimeOptions.IsAdmin"
>
<summary>
是否管理员(config_center_plan.md v2.2):宿主登录体系已知时直接传入,优先于中心 AdminRoles 名单;
缺省 null = 分发模式下按中心名单比对 CurrentUser(自定义模式恒按可设置处理)。
</summary>
</member>
<member
name=
"T:Tools.MyAgent.AgentRuntime"
>
<summary>
其他 .NET Framework 项目引用 Tools.Agent 后使用的稳定入口。
</summary>
</member>
...
...
@@ -31,6 +43,15 @@
<member
name=
"P:Tools.MyAgent.AgentRuntime.ProductLineId"
>
<summary>
当前产品线稳定标识;未绑定时为空。
</summary>
</member>
<member
name=
"P:Tools.MyAgent.AgentRuntime.Center"
>
<summary>
配置中心解析结果(v2.2);自定义模式下仅承载 Mode/IsAdmin,其余字段为空。
</summary>
</member>
<member
name=
"P:Tools.MyAgent.AgentRuntime.CenterMode"
>
<summary>
生效模式(入参优先 / center.json 自动判定)。
</summary>
</member>
<member
name=
"P:Tools.MyAgent.AgentRuntime.CenterIsAdmin"
>
<summary>
当前用户是否管理员(宿主入参
>
中心名单;自定义模式恒 true = 现状人人可设置)。
</summary>
</member>
<member
name=
"P:Tools.MyAgent.AgentRuntime.DataRoot"
>
<summary>
实际数据根目录。
</summary>
</member>
...
...
@@ -1468,6 +1489,13 @@
<member
name=
"M:Tools.MyAgent.Forms.AgentSettingsForm.#ctor"
>
<summary>
仅设计器使用(WinForms 设计器要求无参构造);运行时请使用带参构造。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.Forms.AgentSettingsForm.#ctor(Tools.MyAgent.Config.AgentConfig,System.String,System.Boolean)"
>
<summary>
带参构造。
<para><paramref
name=
"focusRunDbTab"
/>
(2026-10-22):分发模式未配置数据库连接时,
管理员打开设置自动跳到「高级 → 对话上报」页,引导先完成配置。
</para>
</summary>
</member>
<member
name=
"M:Tools.MyAgent.Forms.AgentSettingsForm.BuildRemoteKbUi"
>
<summary>
远程知识库条目接口(next_pref_plan v4.1):追加到「高级 → 语义检索」二级页底部。
...
...
@@ -1489,6 +1517,12 @@
<member
name=
"M:Tools.MyAgent.Forms.AgentSettingsForm.OnTestRunDb(System.Object,System.EventArgs)"
>
<summary>
测试连接:Open + SELECT 1,成功再预演建表(无建表权限时提示具体错误,不落任何数据)。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.Forms.AgentSettingsForm.OnViewRunLog(System.Object,System.EventArgs)"
>
<summary>
查看上报日志(2026-10-22):用当前页面填写的连接串/表名直开
<see
cref=
"T:Tools.MyAgent.Forms.AgentRunLogForm"
/>
(未保存的修改也能即时预览);空连接串时提示先填写。窗口内只读,不再提供连接配置。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.Forms.AgentSettingsForm.BuildToolPermissionUi"
>
<summary>
在「可选能力」页追加工具权限输入区(角色白名单,一行一规则)
</summary>
</member>
...
...
@@ -1583,13 +1617,13 @@
<summary>
Agent 日志查看窗口(agent_analysis_plan.md v1.3 / Q5 决议):只读查询对话上报记录(Agent_RunLog),
左列表 + 右详情(两个 Tab:「问答」= 提问+回答聊天式同页渲染;「指标」= 运行明细),支持日期 / 用户 / 结果过滤与 CSV 导出。
<para>
**
独立窗口**:由宿主"实施工具"菜单直接打开(
<see
cref=
"M:Tools.MyAgent.AgentRuntime.CreateRunLogForm(System.String,System.String)"
/>
),
无任何聊天命令唤出;窗口侧永不写库
。
</para>
<para>
**
入口(2026-10-22)**:设置页「高级 → 对话上报 → 查看上报日志」按钮打开(连接取页面当前填写值);
原实施工具菜单入口与窗口内"连接配置"按钮已移除——连接统一在设置页管理
。
</para>
<para>
控件布局由 AgentRunLogForm.Designer.cs(设计器)实现;本文件仅含逻辑。
</para>
</summary>
</member>
<member
name=
"F:Tools.MyAgent.Forms.AgentRunLogForm._connection"
>
<summary>
当前上报连接(明文,
窗口内可经"连接配置"切换
)。
</summary>
<summary>
当前上报连接(明文,
由设置页「对话上报」传入,窗口内只读
)。
</summary>
</member>
<member
name=
"F:Tools.MyAgent.Forms.AgentRunLogForm._table"
>
<summary>
当前日志表名。
</summary>
...
...
@@ -1599,7 +1633,7 @@
</member>
<member
name=
"M:Tools.MyAgent.Forms.AgentRunLogForm.#ctor"
>
<summary>
模块系统入口(Activator 无参创建
,实施工具菜单直接打开
):自读本机默认 Agent 配置取上报连接;
模块系统入口(Activator 无参创建
兜底
):自读本机默认 Agent 配置取上报连接;
取不到 = 未配置(查询时提示先在设置页配置)。VS 设计器实例化时同样走此路径(读不到配置也不抛异常)。
</summary>
</member>
...
...
@@ -1623,12 +1657,6 @@
RichEdit 静默解析失败 ⇒ 空白)。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.Forms.AgentRunLogForm.OnConnConfigClick(System.Object,System.EventArgs)"
>
<summary>
"连接配置":IP → 账号 → 密码 → 库(下拉拉取 sys.databases);
确定后写回 AgentConfig.json(DPAPI 加密)并刷新本窗口的连接与列表。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.Forms.AgentRunLogForm.Q(System.String)"
>
<summary>
表名标识(白名单已在 Reporter/配置层校验;此处再防御)。
</summary>
</member>
...
...
@@ -2863,147 +2891,165 @@
<member
name=
"E:Tools.MyAgent.AgentTelemetry.OnToolSpan"
>
<summary>
一次工具调用完成(实时事件,替代
<c>
OnToolStatus
</c>
的字符串)。
</summary>
</member>
<member
name=
"T:Tools.MyAgent.
HealthItem
"
>
<summary>
健康检查结果项
。
</summary>
<member
name=
"T:Tools.MyAgent.
Support.AgentRunMode
"
>
<summary>
Agent 运行模式(config_center_plan.md v2.2)
。
</summary>
</member>
<member
name=
"F:Tools.MyAgent.
HealthItem.Id
"
>
<summary>
检查项标识(稳定,便于聚合与对比)
。
</summary>
<member
name=
"F:Tools.MyAgent.
Support.AgentRunMode.Custom
"
>
<summary>
自定义模式(现状):本地 AgentConfig.json,人人可设置,零改动
。
</summary>
</member>
<member
name=
"F:Tools.MyAgent.HealthItem.Name"
>
<summary>
展示名。
</summary>
</member>
<member
name=
"F:Tools.MyAgent.HealthItem.Status"
>
<summary>
ok / warn / fail。
</summary>
<member
name=
"F:Tools.MyAgent.Support.AgentRunMode.Distribution"
>
<summary>
分发模式:配置中心(SQL Server),管理员写透、普通用户拉取。
</summary>
</member>
<member
name=
"F:Tools.MyAgent.HealthItem.Detail"
>
<summary>
判定依据或处置建议。
</summary>
</member>
<member
name=
"T:Tools.MyAgent.AgentHealth"
>
<member
name=
"T:Tools.MyAgent.Support.CenterResolution"
>
<summary>
依赖健康检查(9 项)。按需执行,不定时轮询;单项异常只影响该项。
<para>
输出:
<c>
Logs/health.json
</c>
+ 聊天窗
<c>
/diag
</c>
的表格 + Admin 看板 ⑤Tab。
</para>
配置中心解析结果(config_center_plan.md v2.0–v2.3)。
<para>
承载中心库拉取/快照/缺省三级来源合并后的配置,由
<see
cref=
"M:Tools.MyAgent.Support.AgentCenterClient.Fetch(System.String,System.Nullable{Tools.MyAgent.Support.AgentRunMode},System.Nullable{System.Boolean},System.String,System.String)"
/>
产出;
通过
<see
cref=
"M:Tools.MyAgent.Support.CenterResolution.ApplyTo(Tools.MyAgent.Config.AgentConfig)"
/>
覆盖到 AgentConfig(字段级"非空才覆盖")。
</para>
</summary>
</member>
<member
name=
"
F:Tools.MyAgent.AgentHealth.LogsWarnBytes
"
>
<summary>
日志目录超该大小时告警(防止观测自身撑爆磁盘
)。
</summary>
<member
name=
"
P:Tools.MyAgent.Support.CenterResolution.Mode
"
>
<summary>
生效模式(Custom = 仅承载模式与角色,其余字段为空
)。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentHealth.Check(Tools.MyAgent.Config.AgentConfig,Tools.MyAgent.Kb.KbService,System.Collections.Generic.IReadOnlyList{Tools.MyAgent.Diagnostics.DiagnosticRoot})"
>
<member
name=
"P:Tools.MyAgent.Support.CenterResolution.IsAdmin"
>
<summary>
是否管理员:宿主入参优先,否则中心 AdminRoles 名单比对;Custom 模式恒 true(现状人人可设置)。
</summary>
</member>
<member
name=
"P:Tools.MyAgent.Support.CenterResolution.Source"
>
<summary>
配置来源:custom / db / snapshot / none。
</summary>
</member>
<member
name=
"P:Tools.MyAgent.Support.CenterResolution.MissingItems"
>
<summary>
缺项清单(仅分发模式计算):ApiKey / BaseUrl / Model。KB 缺省不进清单(v2.3:默认 {CLIENT_ROOT} 静默兜底)。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.Support.CenterResolution.ApplyTo(Tools.MyAgent.Config.AgentConfig)"
>
<summary>
把本分辨率结果覆盖到 AgentConfig(字段级"非空才覆盖";中心未配置项回落本地)。
</summary>
</member>
<member
name=
"T:Tools.MyAgent.Support.AgentCenterClient"
>
<summary>
执行检查(不含模型服务连通性——它是异步的,由调用方
<see
cref=
"M:Tools.MyAgent.AgentHealth.ModelService(System.Boolean,System.String)"
/>
单独追加)。
配置中心客户端(config_center_plan.md P0 读侧)。
<para>
职责:模式判定(宿主入参优先 / center.json 自动判定兜底)→ 拉取中心四表 →
{CLIENT_ROOT} 展开 → 角色判定 → 缺项计算 → 快照读写(离线降级)。
</para>
<para>
红线(与 AgentTelemetry 同规):本类所有 public 方法体首句必须是 try —— 任何异常不上抛、不阻塞启动。
</para>
<para>
安全:ApiKey 等敏感列 AES-256(密钥内嵌派生,防库备份肉眼泄露);快照文件只存密文。
</para>
</summary>
</member>
<member
name=
"
M:Tools.MyAgent.AgentHealth.ModelService(System.Boolean,System.String)
"
>
<summary>
模型服务连通性项(由调用方用
<c>
TestConnectionAsync
</c>
结果构造
)。
</summary>
<member
name=
"
F:Tools.MyAgent.Support.AgentCenterClient.ClientRootPlaceholder
"
>
<summary>
{CLIENT_ROOT} 占位符(v2.4 路径统一规范:仅本地文件夹生效;共享文件夹 UNC 原样存储
)。
</summary>
</member>
<member
name=
"
M:Tools.MyAgent.AgentHealth.CheckTelemetrySelf
"
>
<summary>
观测自身:日志目录是否膨胀(自查项,防止观测把磁盘撑爆)
。
</summary>
<member
name=
"
F:Tools.MyAgent.Support.AgentCenterClient.SecretPrefix
"
>
<summary>
AES 密文前缀
。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.
AgentHealth.ToJson(Tools.MyAgent.HealthItem[]
)"
>
<summary>
序列化为 JSON(
<c>
Logs/health.json
</c>
)。
</summary>
<member
name=
"M:Tools.MyAgent.
Support.AgentCenterClient.CenterJsonPath(System.String
)"
>
<summary>
center.json 固定位置:
<
程序目录
>
/Agent/config/center.json(随分发包内置
)。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentHealth.Save(Tools.MyAgent.HealthItem[])"
>
<summary>
落盘到
<c>
Logs/health.json
</c>
(覆盖写);失败不影响调用方。
</summary>
<member
name=
"M:Tools.MyAgent.Support.AgentCenterClient.DetectMode(System.Nullable{Tools.MyAgent.Support.AgentRunMode},System.String)"
>
<summary>
模式判定(v2.2):宿主入参显式传入时以入参为准(显式优于隐式);
未传时自动判定 center.json 是否存在(覆盖独立运行场景)。任何异常回退 Custom。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentHealth.RenderMarkdown(Tools.MyAgent.HealthItem[])"
>
<summary>
渲染为聊天窗 Markdown(分组表格;
<c>
/diag
</c>
用)。
</summary>
<member
name=
"M:Tools.MyAgent.Support.AgentCenterClient.Fetch(System.String,System.Nullable{Tools.MyAgent.Support.AgentRunMode},System.Nullable{System.Boolean},System.String,System.String)"
>
<summary>
拉取并构建配置中心解析结果。任何失败均不抛出;分发模式下按
中心库 → 本地快照 → 缺省 三级来源降级。
</summary>
</member>
<member
name=
"T:Tools.MyAgent.TelemetryStore"
>
<member
name=
"M:Tools.MyAgent.Support.AgentCenterClient.ExpandClientRoot(System.String,System.String)"
>
<summary>
把模板中的 {CLIENT_ROOT} 替换为程序根目录(去尾 \);不含占位符原样返回(UNC 共享路径天然走此分支);空模板返回空串。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.Support.AgentCenterClient.FoldClientRoot(System.String,System.String)"
>
<summary>
可观测性只读聚合层(Admin 看板数据源)。与写侧
<see
cref=
"T:Tools.MyAgent.AgentTelemetry"
/>
分离:
写侧不因看板需求膨胀,看板想怎么查是看板的事。
<para>
护栏:只读最近 N 天 / 单文件最多 N 行 / 坏行跳过并计数 / 全程不允许抛异常
。
</para>
把本机**本地**绝对路径反折回 {CLIENT_ROOT} 占位符(管理员保存用,P1)。
<para>
v2.4 例外:共享文件夹(UNC 路径 \\server\share\…)保持原有模式——各终端访问同一网络路径,
无需按本机展开,原样存储原样使用;程序目录本身在 UNC 上时同样不折叠
。
</para>
</summary>
</member>
<member
name=
"
T:Tools.MyAgent.TelemetryStore.RunRow
"
>
<summary>
一行运行记录(看板表格的行模型)
。
</summary>
<member
name=
"
M:Tools.MyAgent.Support.AgentCenterClient.ProtectSecret(System.String)
"
>
<summary>
AES-256-CBC 加密(随机 IV 前置,base64,"aes:" 前缀);空串原样返回;失败回退明文
。
</summary>
</member>
<member
name=
"
F:Tools.MyAgent.TelemetryStore.RunRow.Tools
"
>
<summary>
工具 span 明细(下钻用;受
<see
cref=
"P:Tools.MyAgent.TelemetryStore.AllowDetailed"
/>
约束
)。
</summary>
<member
name=
"
M:Tools.MyAgent.Support.AgentCenterClient.UnprotectSecret(System.String)
"
>
<summary>
解密 "aes:" 密文;非本前缀视为明文原样返回(兼容);解密失败返回空串(视为未配置
)。
</summary>
</member>
<member
name=
"
T:Tools.MyAgent.TelemetryStore.ToolSpanView
"
>
<summary>
一条工具 span(下钻展示
)。
</summary>
<member
name=
"
M:Tools.MyAgent.Support.AgentCenterClient.WithShortTimeout(System.String)
"
>
<summary>
连接串附加启动短超时(幂等;已有 Connect Timeout 则不动
)。
</summary>
</member>
<member
name=
"T:Tools.MyAgent.TelemetryStore.ToolAgg"
>
<summary>
按工具名聚合。
</summary>
<member
name=
"M:Tools.MyAgent.Support.AgentCenterClient.TestCenterConnection(System.String)"
>
<summary>
测试中心连接(设置页「测试连接」/ P2 /diag 用):Open + 幂等建表。
<para>
返回错误消息,成功返回 null(不抛异常——遵守本类 public 不上抛红线;不写任何数据行)。
</para>
</summary>
</member>
<member
name=
"T:Tools.MyAgent.TelemetryStore.DailyAgg"
>
<summary>
按天聚合。
</summary>
<member
name=
"M:Tools.MyAgent.Support.AgentCenterClient.SaveFromConfig(System.String,Tools.MyAgent.Config.AgentConfig)"
>
<summary>
管理员保存写透(P1):把 AgentConfig 的中心管辖区字段 Upsert 到中心库,路径反折 {CLIENT_ROOT}。
<para>
中心管辖区 = 模型(Provider/BaseUrl/Model/ApiKey/生成参数/远程KB)+
知识库(根/范围/方式/共享凭据)+ Global(AdminRoles/RunDb/遥测)。
</para>
<para>
返回错误消息,成功返回 null。
</para>
</summary>
</member>
<member
name=
"
T:Tools.MyAgent.TelemetryStore.ModeAgg
"
>
<summary>
召回通道分布(直答率来源)
。
</summary>
<member
name=
"
M:Tools.MyAgent.Support.AgentCenterClient.SaveCenter(System.String,Tools.MyAgent.Support.AgentCenterClient.CenterModelRow,Tools.MyAgent.Support.AgentCenterClient.CenterKbRow,Tools.MyAgent.Support.AgentCenterClient.CenterGlobalRow)
"
>
<summary>
Upsert 三表(模型/KB 写 IsDefault=1 行,Global 单行);缺表自动创建。返回错误消息,成功返回 null
。
</summary>
</member>
<member
name=
"
P:Tools.MyAgent.TelemetryStore.AllowDetailed
"
>
<summary>
是否允许展示详细字段(参数摘要等)。由宿主按角色判定后注入
。
</summary>
<member
name=
"
M:Tools.MyAgent.Support.AgentCenterClient.EnsureCenterTablesOn(System.Data.SqlClient.SqlConnection)
"
>
<summary>
幂等建表(四表;IF OBJECT_ID NULL 才建,管理员首次发布即完成中心库初始化)
。
</summary>
</member>
<member
name=
"
P:Tools.MyAgent.TelemetryStore.BadLines
"
>
<summary>
坏行计数(>5% 时看板顶部提示日志疑似损坏
)。
</summary>
<member
name=
"
M:Tools.MyAgent.Support.AgentCenterClient.ExecOrNull(System.Data.SqlClient.SqlConnection,System.String)
"
>
<summary>
对已打开连接执行单条 SQL,失败静默(建表/索引等幂等语句用
)。
</summary>
</member>
<member
name=
"
P:Tools.MyAgent.TelemetryStore.ReadLines
"
>
<summary>
已读取行数
。
</summary>
<member
name=
"
M:Tools.MyAgent.Support.AgentCenterClient.ReadBootstrap(System.String)
"
>
<summary>
读 center.json 的 Connection 字段;文件缺失/无连接串返回空
。
</summary>
</member>
<member
name=
"
M:Tools.MyAgent.TelemetryStore.LoadRuns
"
>
<summary>
读取最近 N 天的运行记录(倒序:最新在前
)。
</summary>
<member
name=
"
T:Tools.MyAgent.Support.AgentCenterClient.CenterPull
"
>
<summary>
中心库原始拉取结果(快照序列化载体
)。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.
TelemetryStore.ReadTailLines(System.String,System.Int32
)"
>
<summary>
读文件末尾 N 行(环形缓冲,避免大文件整体入内存)
。
</summary>
<member
name=
"M:Tools.MyAgent.
Support.AgentCenterClient.PullFromDb(System.String,System.String
)"
>
<summary>
拉取中心四表(分配 → 默认 → Global);库不可达/缺表返回 null(由快照兜底)。解析优先级:分配行
>
默认行
。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.
TelemetryStore.AggregateTools(Tools.MyAgent.TelemetryStore.RunRow[]
)"
>
<summary>
按工具名聚合(调用数 / 失败率 / P50 / P95 / 超时 / 截断)
。
</summary>
<member
name=
"M:Tools.MyAgent.
Support.AgentCenterClient.WriteSnapshot(System.String,Tools.MyAgent.Support.AgentCenterClient.CenterPull
)"
>
<summary>
写快照(只存原始行数据,敏感列保持密文);失败静默
。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.
TelemetryStore.AggregateDaily(Tools.MyAgent.TelemetryStore.RunRow[],System.Int32
)"
>
<summary>
按天聚合
。
</summary>
<member
name=
"M:Tools.MyAgent.
Support.AgentCenterClient.ReadSnapshot(System.String
)"
>
<summary>
读快照;文件缺失/损坏返回 null
。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.
TelemetryStore.AggregateModes(Tools.MyAgent.TelemetryStore.RunRow[]
)"
>
<summary>
召回通道分布
。
</summary>
<member
name=
"M:Tools.MyAgent.
Support.AgentCenterClient.Build(Tools.MyAgent.Support.AgentCenterClient.CenterPull,System.String,Tools.MyAgent.Support.AgentRunMode,System.Nullable{System.Boolean},System.String,System.String
)"
>
<summary>
由原始拉取结果构建分辨率:角色判定 + {CLIENT_ROOT} 展开 + 缺项计算(v2.3:KB 缺省不进缺项)
。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.
TelemetryStore.AggregateErrors(Tools.MyAgent.TelemetryStore.RunRow[],System.Int32
)"
>
<summary>
错误类型分布(Top N
)。
</summary>
<member
name=
"M:Tools.MyAgent.
Support.AgentCenterClient.MatchAdminRole(System.String,System.String
)"
>
<summary>
管理员名单比对(逗号/分号分隔,中英文皆可;忽略大小写与首尾空白
)。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.
TelemetryStore.RecentErrors(Tools.MyAgent.TelemetryStore.RunRow[],System.Int32
)"
>
<summary>
最近 N 条错误(看板 ⑤Tab)
。
</summary>
<member
name=
"M:Tools.MyAgent.
Support.AgentCenterClient.NormalizeRoot(System.String
)"
>
<summary>
程序根目录归一:去尾 \
。
</summary>
</member>
<member
name=
"
M:Tools.MyAgent.TelemetryStore.Hourly(Tools.MyAgent.TelemetryStore.RunRow[],System.Int64[]@)
"
>
<summary>
24 小时分布:按小时统计成功/失败(概览柱状图
)。
</summary>
<member
name=
"
T:Tools.MyAgent.Support.KbProbeResult
"
>
<summary>
知识库根目录探测结论(config_center_plan.md v2.3
)。
</summary>
</member>
<member
name=
"T:Tools.MyAgent.AgentUtils"
>
<summary>
智能体专用小工具(迁移自 Tools.MyUtils.Utils 中智能体依赖的方法)
</summary>
<member
name=
"P:Tools.MyAgent.Support.KbProbeResult.Template"
>
<summary>
原始模板(空 = 中心未下发 KB 配置)。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentUtils.AsyncUI(System.Windows.Forms.Control,System.Action)"
>
<summary>
异步UI扩展(替代原 Common.Extensions.AsyncUI):线程安全地回到控件所在 UI 线程执行
</summary>
<member
name=
"P:Tools.MyAgent.Support.KbProbeResult.Resolved"
>
<summary>
最终采用的绝对路径({CLIENT_ROOT} 已展开;UNC 共享路径原样)。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentUtils.SelectFolder"
>
<summary>
选择文件夹
</summary>
<returns></returns>
<member
name=
"P:Tools.MyAgent.Support.KbProbeResult.Exists"
>
<summary>
目录是否存在。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentUtils.PickFolder(System.String)"
>
<summary>
选择文件夹(支持直接输入网络共享 UNC 路径,如 \\服务器\共享名)。
</summary>
<param
name=
"initialPath"
>
预填路径
</param>
<returns>
选择的路径;取消时返回空串
</returns>
<member
name=
"P:Tools.MyAgent.Support.KbProbeResult.IsEmpty"
>
<summary>
存在但为空目录(允许,首次分发场景;/diag 呈现)。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentUtils.PromptShareCredentials(System.String)"
>
<summary>
弹出共享文件夹访问凭据输入框(账号/密码,可留空以使用当前 Windows 身份)。
</summary>
<member
name=
"P:Tools.MyAgent.Support.KbProbeResult.Conclusion"
>
<summary>
结论:default_ok(未配置→宿主程序根目录) / ok / fallback_ok(已回退 BaseDirectory\Agent) / unavailable。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentUtils.PromptText(System.String,System.String,System.String)"
>
<summary>
单行文本输入框(用于「存知识库」路径、会话重命名等场景)
</summary>
<member
name=
"P:Tools.MyAgent.Support.KbProbeResult.Usable"
>
<summary>
是否可用(Exists 且非 unavailable 类结论)。
</summary>
</member>
<member
name=
"
M:Tools.MyAgent.AgentUtils.Toast(System.String,System.Int32)
"
>
<member
name=
"
T:Tools.MyAgent.Support.AgentKbProbe
"
>
<summary>
Toast提示(自动关闭)
默认知识库探测器(config_center_plan.md v2.3/v2.4)。
<para>
规则:模板空(中心未下发)→ 默认
<c>
{CLIENT_ROOT}
</c>
(宿主程序根目录,静默兜底,不提示);
已配置但解析后不存在 → 回退
<c>
BaseDirectory\Agent
</c>
;仍不存在 → unavailable(提示"目录不可用")。
v2.4:占位符仅本地文件夹生效——UNC 共享模板不含占位符、原样解析。
</para>
<para>
红线:本类所有 public 方法体首句必须是 try —— 任何异常不上抛、不阻塞启动。
</para>
</summary>
<param
name=
"msg"
>
消息
</param>
<param
name=
"durationMs"
>
显示时长(毫秒),默认2秒
</param>
</member>
<member
name=
"M:Tools.MyAgent.Support.AgentKbProbe.Probe(System.String,System.String)"
>
<summary>
未配置模板时 KB 解析(v2.3):默认 {CLIENT_ROOT} = 宿主程序根目录。
</summary>
</member>
<member
name=
"T:Tools.MyAgent.Support.BackupHelper"
>
<summary>
...
...
@@ -3245,6 +3291,152 @@
<para>
采样边界处不完整序列视为合法(避免误伤);0x80/0xFF 等非法字节判定为二进制。
</para>
</summary>
</member>
<member
name=
"T:Tools.MyAgent.HealthItem"
>
<summary>
健康检查结果项。
</summary>
</member>
<member
name=
"F:Tools.MyAgent.HealthItem.Id"
>
<summary>
检查项标识(稳定,便于聚合与对比)。
</summary>
</member>
<member
name=
"F:Tools.MyAgent.HealthItem.Name"
>
<summary>
展示名。
</summary>
</member>
<member
name=
"F:Tools.MyAgent.HealthItem.Status"
>
<summary>
ok / warn / fail。
</summary>
</member>
<member
name=
"F:Tools.MyAgent.HealthItem.Detail"
>
<summary>
判定依据或处置建议。
</summary>
</member>
<member
name=
"T:Tools.MyAgent.AgentHealth"
>
<summary>
依赖健康检查(9 项)。按需执行,不定时轮询;单项异常只影响该项。
<para>
输出:
<c>
Logs/health.json
</c>
+ 聊天窗
<c>
/diag
</c>
的表格 + Admin 看板 ⑤Tab。
</para>
</summary>
</member>
<member
name=
"F:Tools.MyAgent.AgentHealth.LogsWarnBytes"
>
<summary>
日志目录超该大小时告警(防止观测自身撑爆磁盘)。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentHealth.Check(Tools.MyAgent.Config.AgentConfig,Tools.MyAgent.Kb.KbService,System.Collections.Generic.IReadOnlyList{Tools.MyAgent.Diagnostics.DiagnosticRoot},Tools.MyAgent.Support.CenterResolution)"
>
<summary>
执行检查(不含模型服务连通性——它是异步的,由调用方
<see
cref=
"M:Tools.MyAgent.AgentHealth.ModelService(System.Boolean,System.String)"
/>
单独追加)。
<para>
v2.2 新增第 10 项
<c>
config_center
</c>
:传入配置中心解析结果即输出该项(null = 自定义模式)。
</para>
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentHealth.CheckConfigCenter(Tools.MyAgent.Support.CenterResolution)"
>
<summary>
配置中心项(P2):模式 / 来源 / 角色 / KB 探测结论 / 缺项清单。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentHealth.ModelService(System.Boolean,System.String)"
>
<summary>
模型服务连通性项(由调用方用
<c>
TestConnectionAsync
</c>
结果构造)。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentHealth.CheckTelemetrySelf"
>
<summary>
观测自身:日志目录是否膨胀(自查项,防止观测把磁盘撑爆)。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentHealth.ToJson(Tools.MyAgent.HealthItem[])"
>
<summary>
序列化为 JSON(
<c>
Logs/health.json
</c>
)。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentHealth.Save(Tools.MyAgent.HealthItem[])"
>
<summary>
落盘到
<c>
Logs/health.json
</c>
(覆盖写);失败不影响调用方。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentHealth.RenderMarkdown(Tools.MyAgent.HealthItem[])"
>
<summary>
渲染为聊天窗 Markdown(分组表格;
<c>
/diag
</c>
用)。
</summary>
</member>
<member
name=
"T:Tools.MyAgent.TelemetryStore"
>
<summary>
可观测性只读聚合层(Admin 看板数据源)。与写侧
<see
cref=
"T:Tools.MyAgent.AgentTelemetry"
/>
分离:
写侧不因看板需求膨胀,看板想怎么查是看板的事。
<para>
护栏:只读最近 N 天 / 单文件最多 N 行 / 坏行跳过并计数 / 全程不允许抛异常。
</para>
</summary>
</member>
<member
name=
"T:Tools.MyAgent.TelemetryStore.RunRow"
>
<summary>
一行运行记录(看板表格的行模型)。
</summary>
</member>
<member
name=
"F:Tools.MyAgent.TelemetryStore.RunRow.Tools"
>
<summary>
工具 span 明细(下钻用;受
<see
cref=
"P:Tools.MyAgent.TelemetryStore.AllowDetailed"
/>
约束)。
</summary>
</member>
<member
name=
"T:Tools.MyAgent.TelemetryStore.ToolSpanView"
>
<summary>
一条工具 span(下钻展示)。
</summary>
</member>
<member
name=
"T:Tools.MyAgent.TelemetryStore.ToolAgg"
>
<summary>
按工具名聚合。
</summary>
</member>
<member
name=
"T:Tools.MyAgent.TelemetryStore.DailyAgg"
>
<summary>
按天聚合。
</summary>
</member>
<member
name=
"T:Tools.MyAgent.TelemetryStore.ModeAgg"
>
<summary>
召回通道分布(直答率来源)。
</summary>
</member>
<member
name=
"P:Tools.MyAgent.TelemetryStore.AllowDetailed"
>
<summary>
是否允许展示详细字段(参数摘要等)。由宿主按角色判定后注入。
</summary>
</member>
<member
name=
"P:Tools.MyAgent.TelemetryStore.BadLines"
>
<summary>
坏行计数(>5% 时看板顶部提示日志疑似损坏)。
</summary>
</member>
<member
name=
"P:Tools.MyAgent.TelemetryStore.ReadLines"
>
<summary>
已读取行数。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.TelemetryStore.LoadRuns"
>
<summary>
读取最近 N 天的运行记录(倒序:最新在前)。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.TelemetryStore.ReadTailLines(System.String,System.Int32)"
>
<summary>
读文件末尾 N 行(环形缓冲,避免大文件整体入内存)。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.TelemetryStore.AggregateTools(Tools.MyAgent.TelemetryStore.RunRow[])"
>
<summary>
按工具名聚合(调用数 / 失败率 / P50 / P95 / 超时 / 截断)。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.TelemetryStore.AggregateDaily(Tools.MyAgent.TelemetryStore.RunRow[],System.Int32)"
>
<summary>
按天聚合。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.TelemetryStore.AggregateModes(Tools.MyAgent.TelemetryStore.RunRow[])"
>
<summary>
召回通道分布。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.TelemetryStore.AggregateErrors(Tools.MyAgent.TelemetryStore.RunRow[],System.Int32)"
>
<summary>
错误类型分布(Top N)。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.TelemetryStore.RecentErrors(Tools.MyAgent.TelemetryStore.RunRow[],System.Int32)"
>
<summary>
最近 N 条错误(看板 ⑤Tab)。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.TelemetryStore.Hourly(Tools.MyAgent.TelemetryStore.RunRow[],System.Int64[]@)"
>
<summary>
24 小时分布:按小时统计成功/失败(概览柱状图)。
</summary>
</member>
<member
name=
"T:Tools.MyAgent.AgentUtils"
>
<summary>
智能体专用小工具(迁移自 Tools.MyUtils.Utils 中智能体依赖的方法)
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentUtils.AsyncUI(System.Windows.Forms.Control,System.Action)"
>
<summary>
异步UI扩展(替代原 Common.Extensions.AsyncUI):线程安全地回到控件所在 UI 线程执行
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentUtils.SelectFolder"
>
<summary>
选择文件夹
</summary>
<returns></returns>
</member>
<member
name=
"M:Tools.MyAgent.AgentUtils.PickFolder(System.String)"
>
<summary>
选择文件夹(支持直接输入网络共享 UNC 路径,如 \\服务器\共享名)。
</summary>
<param
name=
"initialPath"
>
预填路径
</param>
<returns>
选择的路径;取消时返回空串
</returns>
</member>
<member
name=
"M:Tools.MyAgent.AgentUtils.PromptShareCredentials(System.String)"
>
<summary>
弹出共享文件夹访问凭据输入框(账号/密码,可留空以使用当前 Windows 身份)。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentUtils.PromptText(System.String,System.String,System.String)"
>
<summary>
单行文本输入框(用于「存知识库」路径、会话重命名等场景)
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentUtils.Toast(System.String,System.Int32)"
>
<summary>
Toast提示(自动关闭)
</summary>
<param
name=
"msg"
>
消息
</param>
<param
name=
"durationMs"
>
显示时长(毫秒),默认2秒
</param>
</member>
<member
name=
"T:Tools.MyAgent.ChatMessage"
>
<summary>
对话消息(迁移自 Tools.MyCommon)
...
...
@@ -3683,6 +3875,12 @@
<member
name=
"M:Tools.MyAgent.wMyAgent.#ctor(System.String,System.String,Tools.MyAgent.AgentProductContext,System.Collections.Generic.IReadOnlyList{Tools.MyAgent.Diagnostics.DiagnosticRoot})"
>
<summary>
创建可嵌入的智能体窗体,并允许宿主指定独立诊断根目录。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.wMyAgent.#ctor(System.String,System.String,Tools.MyAgent.AgentProductContext,System.Collections.Generic.IReadOnlyList{Tools.MyAgent.Diagnostics.DiagnosticRoot},Tools.MyAgent.Support.CenterResolution)"
>
<summary>
创建可嵌入的智能体窗体(v2.2 完整形态):center 为宿主(AgentRuntime)已拉取的配置中心解析结果;
传 null(独立实例化)时按 center.json 是否存在自动判定模式并自行拉取。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.wMyAgent.CreateChatControl"
>
<summary>
创建聊天区控件:统一使用 RichBox(RichTextBox + RTF 渲染,轻量、启动快、无浏览器依赖)。
...
...
@@ -3725,8 +3923,15 @@
<member
name=
"P:Tools.MyAgent.wMyAgent.ProviderPresets"
>
<summary>
提供商预设(定义统一在设置窗体 AgentSettingsForm)
</summary>
</member>
<member
name=
"
P:Tools.MyAgent.wMyAgent.DefaultKbRoot
"
>
<member
name=
"
F:Tools.MyAgent.wMyAgent._center
"
>
<summary>
默认知识库路径:程序输出目录下的 Agent 文件夹(未设置知识库时生效)
</summary>
<summary>
配置中心解析结果(config_center_plan.md v2.2);Custom 模式仅承载模式与角色。保存成功后可被刷新(非只读)。
</summary>
</member>
<member
name=
"P:Tools.MyAgent.wMyAgent.IsDistribution"
>
<summary>
当前是否分发模式(配置中心生效)。
</summary>
</member>
<member
name=
"P:Tools.MyAgent.wMyAgent.CanOpenSettings"
>
<summary>
当前用户是否可打开设置:自定义模式恒可(现状);分发模式仅管理员。
</summary>
</member>
<member
name=
"P:Tools.MyAgent.wMyAgent.ProductFixedScope"
>
<summary>
产品上下文真正固定的搜索子目录;旧宿主把根目录作为范围时视为未固定。
</summary>
...
...
Tools.Agent.Host/Program.cs
View file @
41a89411
...
...
@@ -9,6 +9,7 @@ using System.Threading;
using
System.Windows.Forms
;
using
Tools.MyAgent
;
using
Tools.MyAgent.Diagnostics
;
using
Tools.MyAgent.Support
;
namespace
Tools.MyAgent.Host
{
...
...
@@ -47,6 +48,8 @@ namespace Tools.MyAgent.Host
{
DataRoot
=
options
.
DataRoot
,
CurrentUser
=
options
.
User
,
Mode
=
options
.
Mode
,
IsAdmin
=
options
.
IsAdmin
,
ProductContext
=
product
,
};
...
...
@@ -197,6 +200,8 @@ namespace Tools.MyAgent.Host
public
string
LogRoot
=
""
;
public
string
ConfigRoot
=
""
;
public
string
DeploymentRoot
=
""
;
public
AgentRunMode
?
Mode
;
public
bool
?
IsAdmin
;
public
string
DataRoot
;
public
int
ParentPid
;
public
bool
ValidateOnly
;
...
...
@@ -206,7 +211,7 @@ namespace Tools.MyAgent.Host
"Tools.Agent.Host.exe --protocol 1 --product <pdm|erp|mes> --user <user> "
+
"--data-root <path> [--project-root <path>] [--kb-root <path>] "
+
"[--kb-scope <path>] [--log-root <path>] [--config-root <path>] [--deployment-root <path>] "
+
"[--parent-pid <pid>]"
;
+
"[--
mode <custom|distribution>] [--is-admin <true|false>] [--
parent-pid <pid>]"
;
public
static
bool
TryParse
(
string
[]
args
,
out
LaunchOptions
options
,
out
string
error
)
{
...
...
@@ -272,6 +277,30 @@ namespace Tools.MyAgent.Host
values
.
TryGetValue
(
"log-root"
,
out
options
.
LogRoot
);
values
.
TryGetValue
(
"config-root"
,
out
options
.
ConfigRoot
);
values
.
TryGetValue
(
"deployment-root"
,
out
options
.
DeploymentRoot
);
if
(
values
.
TryGetValue
(
"mode"
,
out
value
)
&&
!
string
.
IsNullOrWhiteSpace
(
value
))
{
if
(
value
.
Equals
(
"custom"
,
StringComparison
.
OrdinalIgnoreCase
))
options
.
Mode
=
AgentRunMode
.
Custom
;
else
if
(
value
.
Equals
(
"distribution"
,
StringComparison
.
OrdinalIgnoreCase
))
options
.
Mode
=
AgentRunMode
.
Distribution
;
else
{
error
=
"mode 必须是 custom 或 distribution。"
;
return
false
;
}
}
if
(
values
.
TryGetValue
(
"is-admin"
,
out
value
))
{
bool
isAdmin
;
if
(
value
==
"1"
)
isAdmin
=
true
;
else
if
(
value
==
"0"
)
isAdmin
=
false
;
else
if
(!
bool
.
TryParse
(
value
,
out
isAdmin
))
{
error
=
"is-admin 必须是 true、false、1 或 0。"
;
return
false
;
}
options
.
IsAdmin
=
isAdmin
;
}
if
(
values
.
TryGetValue
(
"parent-pid"
,
out
value
)
&&
!
int
.
TryParse
(
value
,
out
options
.
ParentPid
))
{
error
=
"parent-pid 不是有效整数。"
;
...
...
Tools.Agent/Agent/AgentEngine.cs
View file @
41a89411
...
...
@@ -237,7 +237,7 @@ namespace Tools.MyAgent.Agent
_backupClient
=
new
OpenAiCompatibleClient
();
_failover
=
new
FailoverPolicy
(
_client
,
_backupClient
);
string
root
=
string
.
IsNullOrEmpty
(
agentRoot
)
?
Path
.
Combine
(
AppDomain
.
CurrentDomain
.
BaseDirectory
,
"Agent"
)
?
global
::
Tools
.
MyAgent
.
Support
.
AgentCenterClient
.
AgentRootPath
(
AppDomain
.
CurrentDomain
.
BaseDirectory
)
:
Path
.
GetFullPath
(
agentRoot
);
_dialect
=
new
DialectService
(
root
);
_recall
=
new
KbRecall
(
_kb
);
// P3:本地融合召回器(构造无副作用,仅持有 KbService 引用)
...
...
Tools.Agent/Agent/Tools/DbQueryTool.cs
View file @
41a89411
...
...
@@ -150,7 +150,7 @@ namespace Tools.MyAgent.Agent.Tools
{
try
{
string
root
=
Path
.
Combine
(
AppDomain
.
CurrentDomain
.
BaseDirectory
,
"Agent"
);
string
root
=
global
::
Tools
.
MyAgent
.
Support
.
AgentCenterClient
.
AgentRootPath
(
AppDomain
.
CurrentDomain
.
BaseDirectory
);
string
cfgPath
=
Path
.
Combine
(
root
,
"config"
,
"AgentConfig.json"
);
if
(
File
.
Exists
(
cfgPath
))
{
...
...
Tools.Agent/AgentRuntime.cs
View file @
41a89411
...
...
@@ -23,6 +23,18 @@ namespace Tools.MyAgent
/// <summary>宿主固定的产品上下文;空值表示未绑定产品线。</summary>
public
AgentProductContext
ProductContext
{
get
;
set
;
}
/// <summary>
/// 运行模式(config_center_plan.md v2.2):Custom=自定义(现状零改动);Distribution=分发(配置中心)。
/// 缺省 null = 按 <程序目录>/Agent/config/center.json 是否存在自动判定;显式传入时以入参为准。
/// </summary>
public
Support
.
AgentRunMode
?
Mode
{
get
;
set
;
}
/// <summary>
/// 是否管理员(config_center_plan.md v2.2):宿主登录体系已知时直接传入,优先于中心 AdminRoles 名单;
/// 缺省 null = 分发模式下按中心名单比对 CurrentUser(自定义模式恒按可设置处理)。
/// </summary>
public
bool
?
IsAdmin
{
get
;
set
;
}
}
/// <summary>其他 .NET Framework 项目引用 Tools.Agent 后使用的稳定入口。</summary>
...
...
@@ -31,6 +43,7 @@ namespace Tools.MyAgent
private
readonly
AgentRuntimeOptions
_options
;
private
readonly
AgentProductContext
_productContext
;
private
readonly
IReadOnlyList
<
DiagnosticRoot
>
_diagnosticRoots
;
private
readonly
Support
.
CenterResolution
_center
;
/// <summary>当前运行时诊断根目录快照;返回独立副本,避免外部修改。</summary>
public
IReadOnlyList
<
DiagnosticRoot
>
DiagnosticRoots
...
...
@@ -62,18 +75,35 @@ namespace Tools.MyAgent
context
.
DiagnosticRoots
=
new
[]
{
new
DiagnosticRoot
{
Id
=
"source"
,
Path
=
context
.
ProjectRoot
,
Kind
=
"source"
}
};
}
_productContext
=
ProductContextValidator
.
CreateSnapshot
(
context
);
// 配置中心(config_center_plan.md P0):模式判定 + 分发模式拉取(内部三级降级,任何异常不上抛)
_center
=
Support
.
AgentCenterClient
.
Fetch
(
AppDomain
.
CurrentDomain
.
BaseDirectory
,
_options
.
Mode
,
_options
.
IsAdmin
,
_options
.
CurrentUser
,
ProductLineId
);
}
/// <summary>配置中心解析结果(v2.2);自定义模式下仅承载 Mode/IsAdmin,其余字段为空。</summary>
public
Support
.
CenterResolution
Center
=>
_center
;
/// <summary>生效模式(入参优先 / center.json 自动判定)。</summary>
public
Support
.
AgentRunMode
CenterMode
=>
_center
!=
null
?
_center
.
Mode
:
Support
.
AgentRunMode
.
Custom
;
/// <summary>当前用户是否管理员(宿主入参 > 中心名单;自定义模式恒 true = 现状人人可设置)。</summary>
public
bool
CenterIsAdmin
=>
_center
!=
null
?
_center
.
IsAdmin
:
true
;
/// <summary>实际数据根目录。</summary>
public
string
DataRoot
=>
string
.
IsNullOrEmpty
(
_options
.
DataRoot
)
?
Path
.
Combine
(
AppDomain
.
CurrentDomain
.
BaseDirectory
,
"Agent"
)
?
Support
.
AgentCenterClient
.
AgentRootPath
(
AppDomain
.
CurrentDomain
.
BaseDirectory
)
:
Path
.
GetFullPath
(
_options
.
DataRoot
);
/// <summary>创建完整聊天窗体,可作为顶级窗体显示或嵌入宿主容器。</summary>
public
wMyAgent
CreateChatForm
()
{
EnsureDefaultResources
();
return
new
wMyAgent
(
DataRoot
,
_options
.
CurrentUser
,
ProductContext
,
_diagnosticRoots
);
return
new
wMyAgent
(
DataRoot
,
_options
.
CurrentUser
,
ProductContext
,
_diagnosticRoots
,
_center
);
}
/// <summary>创建已配置输出目录的知识库服务,供无 UI 宿主自行设置知识库根目录。</summary>
...
...
@@ -124,7 +154,8 @@ namespace Tools.MyAgent
{
try
{
string
path
=
Path
.
Combine
(
AppDomain
.
CurrentDomain
.
BaseDirectory
,
"Agent"
,
"config"
,
"AgentConfig.json"
);
Support
.
AgentCenterClient
.
MigrateLegacyLayout
(
AppDomain
.
CurrentDomain
.
BaseDirectory
);
string
path
=
Path
.
Combine
(
Support
.
AgentCenterClient
.
AgentRootPath
(
AppDomain
.
CurrentDomain
.
BaseDirectory
),
"config"
,
"AgentConfig.json"
);
if
(!
File
.
Exists
(
path
))
return
null
;
var
cfg
=
Newtonsoft
.
Json
.
JsonConvert
.
DeserializeObject
<
Config
.
AgentConfig
>(
File
.
ReadAllText
(
path
,
System
.
Text
.
Encoding
.
UTF8
));
...
...
@@ -143,7 +174,8 @@ namespace Tools.MyAgent
{
try
{
string
path
=
Path
.
Combine
(
AppDomain
.
CurrentDomain
.
BaseDirectory
,
"Agent"
,
"config"
,
"AgentConfig.json"
);
Support
.
AgentCenterClient
.
MigrateLegacyLayout
(
AppDomain
.
CurrentDomain
.
BaseDirectory
);
string
path
=
Path
.
Combine
(
Support
.
AgentCenterClient
.
AgentRootPath
(
AppDomain
.
CurrentDomain
.
BaseDirectory
),
"config"
,
"AgentConfig.json"
);
var
root
=
File
.
Exists
(
path
)
?
Newtonsoft
.
Json
.
Linq
.
JObject
.
Parse
(
File
.
ReadAllText
(
path
,
System
.
Text
.
Encoding
.
UTF8
))
:
new
Newtonsoft
.
Json
.
Linq
.
JObject
();
...
...
@@ -160,7 +192,7 @@ namespace Tools.MyAgent
private
void
EnsureDefaultResources
()
{
Directory
.
CreateDirectory
(
DataRoot
);
string
defaults
=
Path
.
Combine
(
AppDomain
.
CurrentDomain
.
BaseDirectory
,
"Agent"
);
string
defaults
=
Support
.
AgentCenterClient
.
AgentRootPath
(
AppDomain
.
CurrentDomain
.
BaseDirectory
);
if
(
string
.
Equals
(
Path
.
GetFullPath
(
defaults
).
TrimEnd
(
'\\'
),
DataRoot
.
TrimEnd
(
'\\'
),
StringComparison
.
OrdinalIgnoreCase
)
||
!
Directory
.
Exists
(
defaults
))
return
;
CopyMissing
(
Path
.
Combine
(
defaults
,
"prompts"
),
Path
.
Combine
(
DataRoot
,
"prompts"
));
...
...
Tools.Agent/Forms/AgentAdminForm.cs
View file @
41a89411
...
...
@@ -521,7 +521,7 @@ namespace Tools.MyAgent.Forms
{
var
dlg
=
new
SaveFileDialog
{
Filter
=
"ZIP|*.zip"
,
FileName
=
"agent_diag.zip"
};
if
(
dlg
.
ShowDialog
(
this
)
!=
DialogResult
.
OK
)
return
;
string
agentRoot
=
Path
.
Combine
(
AppDomain
.
CurrentDomain
.
BaseDirectory
,
"Agent"
);
string
agentRoot
=
global
::
Tools
.
MyAgent
.
Support
.
AgentCenterClient
.
AgentRootPath
(
AppDomain
.
CurrentDomain
.
BaseDirectory
);
string
path
=
BackupHelper
.
BackupAgent
(
agentRoot
,
null
,
dlg
.
FileName
);
MessageBox
.
Show
(
this
,
string
.
IsNullOrEmpty
(
path
)
?
"导出失败"
:
"已导出:"
+
path
,
"诊断包"
,
MessageBoxButtons
.
OK
,
MessageBoxIcon
.
Information
);
...
...
Tools.Agent/Forms/AgentRunLogForm.Designer.cs
View file @
41a89411
...
...
@@ -39,7 +39,6 @@
this
.
cmbOutcome
=
new
System
.
Windows
.
Forms
.
ComboBox
();
this
.
btnRefresh
=
new
System
.
Windows
.
Forms
.
Button
();
this
.
btnExport
=
new
System
.
Windows
.
Forms
.
Button
();
this
.
btnConn
=
new
System
.
Windows
.
Forms
.
Button
();
this
.
splitMain
=
new
System
.
Windows
.
Forms
.
SplitContainer
();
this
.
dgv
=
new
System
.
Windows
.
Forms
.
DataGridView
();
this
.
tabDetails
=
new
System
.
Windows
.
Forms
.
TabControl
();
...
...
@@ -75,7 +74,6 @@
this
.
panelTop
.
Controls
.
Add
(
this
.
cmbOutcome
);
this
.
panelTop
.
Controls
.
Add
(
this
.
btnRefresh
);
this
.
panelTop
.
Controls
.
Add
(
this
.
btnExport
);
this
.
panelTop
.
Controls
.
Add
(
this
.
btnConn
);
this
.
panelTop
.
Dock
=
System
.
Windows
.
Forms
.
DockStyle
.
Top
;
this
.
panelTop
.
Location
=
new
System
.
Drawing
.
Point
(
0
,
0
);
this
.
panelTop
.
Name
=
"panelTop"
;
...
...
@@ -167,16 +165,6 @@
this
.
btnExport
.
UseVisualStyleBackColor
=
true
;
this
.
btnExport
.
Click
+=
new
System
.
EventHandler
(
this
.
OnExportCsvClick
);
//
// btnConn(窗口内数据库连接配置:IP/账号/密码/库下拉)
//
this
.
btnConn
.
FlatStyle
=
System
.
Windows
.
Forms
.
FlatStyle
.
Flat
;
this
.
btnConn
.
Margin
=
new
System
.
Windows
.
Forms
.
Padding
(
6
,
4
,
3
,
0
);
this
.
btnConn
.
Name
=
"btnConn"
;
this
.
btnConn
.
Size
=
new
System
.
Drawing
.
Size
(
88
,
28
);
this
.
btnConn
.
Text
=
"连接配置"
;
this
.
btnConn
.
UseVisualStyleBackColor
=
true
;
this
.
btnConn
.
Click
+=
new
System
.
EventHandler
(
this
.
OnConnConfigClick
);
//
// splitMain
//
this
.
splitMain
.
Dock
=
System
.
Windows
.
Forms
.
DockStyle
.
Fill
;
...
...
@@ -333,7 +321,6 @@
private
System
.
Windows
.
Forms
.
ComboBox
cmbOutcome
;
private
System
.
Windows
.
Forms
.
Button
btnRefresh
;
private
System
.
Windows
.
Forms
.
Button
btnExport
;
private
System
.
Windows
.
Forms
.
Button
btnConn
;
private
System
.
Windows
.
Forms
.
SplitContainer
splitMain
;
private
System
.
Windows
.
Forms
.
DataGridView
dgv
;
private
System
.
Windows
.
Forms
.
TabControl
tabDetails
;
...
...
Tools.Agent/Forms/AgentRunLogForm.cs
View file @
41a89411
...
...
@@ -13,15 +13,15 @@ namespace Tools.MyAgent.Forms
/// <summary>
/// Agent 日志查看窗口(agent_analysis_plan.md v1.3 / Q5 决议):只读查询对话上报记录(Agent_RunLog),
/// 左列表 + 右详情(两个 Tab:「问答」= 提问+回答聊天式同页渲染;「指标」= 运行明细),支持日期 / 用户 / 结果过滤与 CSV 导出。
/// <para>**
独立窗口**:由宿主"实施工具"菜单直接打开(<see cref="AgentRuntime.CreateRunLogForm"/>),
///
无任何聊天命令唤出;窗口侧永不写库
。</para>
/// <para>**
入口(2026-10-22)**:设置页「高级 → 对话上报 → 查看上报日志」按钮打开(连接取页面当前填写值);
///
原实施工具菜单入口与窗口内"连接配置"按钮已移除——连接统一在设置页管理
。</para>
/// <para>控件布局由 AgentRunLogForm.Designer.cs(设计器)实现;本文件仅含逻辑。</para>
/// </summary>
public
partial
class
AgentRunLogForm
:
Form
{
private
const
int
MaxRows
=
500
;
/// <summary>当前上报连接(明文,
窗口内可经"连接配置"切换
)。</summary>
/// <summary>当前上报连接(明文,
由设置页「对话上报」传入,窗口内只读
)。</summary>
private
string
_connection
;
/// <summary>当前日志表名。</summary>
private
string
_table
;
...
...
@@ -30,7 +30,7 @@ namespace Tools.MyAgent.Forms
private
string
_currentRunId
;
/// <summary>
/// 模块系统入口(Activator 无参创建
,实施工具菜单直接打开
):自读本机默认 Agent 配置取上报连接;
/// 模块系统入口(Activator 无参创建
兜底
):自读本机默认 Agent 配置取上报连接;
/// 取不到 = 未配置(查询时提示先在设置页配置)。VS 设计器实例化时同样走此路径(读不到配置也不抛异常)。
/// </summary>
public
AgentRunLogForm
()
...
...
@@ -297,36 +297,6 @@ namespace Tools.MyAgent.Forms
txtMetrics
.
Text
=
metrics
;
}
// ══════════════ 连接配置(窗口内直配,2026-10-19) ══════════════
/// <summary>
/// "连接配置":IP → 账号 → 密码 → 库(下拉拉取 sys.databases);
/// 确定后写回 AgentConfig.json(DPAPI 加密)并刷新本窗口的连接与列表。
/// </summary>
private
void
OnConnConfigClick
(
object
sender
,
EventArgs
e
)
{
using
(
var
dlg
=
new
AgentDbConfigForm
(
_connection
,
_table
))
{
if
(
dlg
.
ShowDialog
(
this
)
!=
DialogResult
.
OK
)
return
;
string
plain
=
dlg
.
BuildConnectionString
();
string
table
=
dlg
.
Table
;
if
(!
AgentRuntime
.
SaveLocalRunDbConfig
(
plain
,
table
,
dlg
.
EnableReporting
))
{
AgentUtils
.
Toast
(
"保存到配置文件失败(AgentConfig.json 不可写)"
,
4000
);
return
;
}
_connection
=
plain
;
// 本窗口立即生效,无需重开
_table
=
table
;
_currentRunId
=
null
;
SetDetailText
(
"(已切换连接,请选择记录)"
,
"(已切换连接)"
);
LoadUsers
();
RefreshList
();
AgentUtils
.
Toast
(
dlg
.
EnableReporting
?
"已保存并启用对话上报;引擎侧在下次应用配置(重启客户端或设置页确定)后生效"
:
"已保存连接;对话上报保持关闭(仅本窗口查询用)"
,
4000
);
}
}
// ══════════════ 导出 ══════════════
private
void
OnExportCsvClick
(
object
sender
,
EventArgs
e
)
...
...
Tools.Agent/Forms/AgentSettingsForm.cs
View file @
41a89411
...
...
@@ -110,11 +110,16 @@ namespace Tools.MyAgent.Forms
{
}
public
AgentSettingsForm
(
AgentConfig
config
,
string
agentRoot
)
/// <summary>
/// 带参构造。
/// <para><paramref name="focusRunDbTab"/>(2026-10-22):分发模式未配置数据库连接时,
/// 管理员打开设置自动跳到「高级 → 对话上报」页,引导先完成配置。</para>
/// </summary>
public
AgentSettingsForm
(
AgentConfig
config
,
string
agentRoot
,
bool
focusRunDbTab
=
false
)
{
_config
=
config
??
new
AgentConfig
();
_agentRoot
=
string
.
IsNullOrEmpty
(
agentRoot
)
?
Path
.
Combine
(
AppDomain
.
CurrentDomain
.
BaseDirectory
,
"Agent"
)
?
global
::
Tools
.
MyAgent
.
Support
.
AgentCenterClient
.
AgentRootPath
(
AppDomain
.
CurrentDomain
.
BaseDirectory
)
:
Path
.
GetFullPath
(
agentRoot
);
InitializeComponent
();
...
...
@@ -127,6 +132,11 @@ namespace Tools.MyAgent.Forms
BindConfig
();
// 配置值回填控件(控件创建见 Designer.cs)
ApplyProviderMode
();
// 布局联动:BaseUrl 行显隐
ApplyKbMode
();
// 布局联动:共享 账号/密码/测试连接 行显隐
if
(
focusRunDbTab
&&
_tabRunDb
!=
null
)
{
tabMain
.
SelectedTab
=
tabAdv
;
// 外层先定位「高级」页
tabAdvSub
.
SelectedTab
=
_tabRunDb
;
// 内层定位「对话上报」
}
}
// ══ v4.1:远程知识库条目接口(程序化创建,避免侵入设计器) ══
...
...
@@ -217,6 +227,8 @@ namespace Tools.MyAgent.Forms
private
TextBox
txtRunDbConn
;
private
TextBox
txtRunDbTable
;
private
Button
btnTestRunDb
;
private
Button
btnViewRunLog
;
private
TabPage
_tabRunDb
;
// 分发模式未配置连接时自动跳转目标(2026-10-22)
/// <summary>
/// 高级配置新增二级 Tab「对话上报」:每轮对话完成后自动上报一条记录到 SQL Server
...
...
@@ -255,9 +267,16 @@ namespace Tools.MyAgent.Forms
Size
=
new
Size
(
96
,
26
),
};
btnTestRunDb
.
Click
+=
new
EventHandler
(
OnTestRunDb
);
btnViewRunLog
=
new
Button
{
Text
=
"查看上报日志"
,
Location
=
new
Point
(
372
,
136
),
Size
=
new
Size
(
110
,
26
),
};
btnViewRunLog
.
Click
+=
new
EventHandler
(
OnViewRunLog
);
var
hint
=
new
Label
{
Text
=
"提问与回答原文会存入该库,用于统计分析与体验优化。
查看入口:实施工具 → Agent日志
。\r\n示例:Server=主机;Database=库名;User Id=账号;Password=密码;Trusted_Connection=False"
,
Text
=
"提问与回答原文会存入该库,用于统计分析与体验优化。
点击「查看上报日志」可查询已上报记录
。\r\n示例:Server=主机;Database=库名;User Id=账号;Password=密码;Trusted_Connection=False"
,
Location
=
new
Point
(
12
,
172
),
AutoSize
=
false
,
Size
=
new
Size
(
556
,
40
),
...
...
@@ -270,7 +289,9 @@ namespace Tools.MyAgent.Forms
tabRunDb
.
Controls
.
Add
(
lblTable
);
tabRunDb
.
Controls
.
Add
(
txtRunDbTable
);
tabRunDb
.
Controls
.
Add
(
btnTestRunDb
);
tabRunDb
.
Controls
.
Add
(
btnViewRunLog
);
tabRunDb
.
Controls
.
Add
(
hint
);
_tabRunDb
=
tabRunDb
;
tabAdvSub
.
TabPages
.
Add
(
tabRunDb
);
}
...
...
@@ -305,6 +326,25 @@ namespace Tools.MyAgent.Forms
}
}
/// <summary>
/// 查看上报日志(2026-10-22):用当前页面填写的连接串/表名直开 <see cref="AgentRunLogForm"/>
/// (未保存的修改也能即时预览);空连接串时提示先填写。窗口内只读,不再提供连接配置。
/// </summary>
private
void
OnViewRunLog
(
object
sender
,
EventArgs
e
)
{
string
conn
=
txtRunDbConn
.
Text
.
Trim
();
if
(
conn
.
Length
==
0
)
{
AgentUtils
.
Toast
(
"请先填写连接串"
);
return
;
}
string
table
=
string
.
IsNullOrWhiteSpace
(
txtRunDbTable
.
Text
)
?
"Agent_RunLog"
:
txtRunDbTable
.
Text
.
Trim
();
using
(
var
dlg
=
new
AgentRunLogForm
(
conn
,
table
))
{
dlg
.
ShowDialog
(
this
);
}
}
// ══ P3:工具级权限(按角色) ══
private
TextBox
txtToolRoles
;
...
...
Tools.Agent/Kb/Remote/RemoteEntryKb.cs
View file @
41a89411
...
...
@@ -64,7 +64,7 @@ namespace Tools.MyAgent.Kb.Remote
private
static
string
CacheDir
{
get
{
return
Path
.
Combine
(
AppDomain
.
CurrentDomain
.
BaseDirectory
,
"Agent"
,
"cache"
,
"remote_kb"
);
}
get
{
return
Path
.
Combine
(
global
::
Tools
.
MyAgent
.
Support
.
AgentCenterClient
.
AgentRootPath
(
AppDomain
.
CurrentDomain
.
BaseDirectory
)
,
"cache"
,
"remote_kb"
);
}
}
/// <summary>一个远程源(解析自 URL 模板;无效模板在解析期静默丢弃)</summary>
...
...
Tools.Agent/Support/AgentCenterClient.cs
0 → 100644
View file @
41a89411
using
System
;
using
System.Collections.Generic
;
using
System.Data.SqlClient
;
using
System.IO
;
using
System.Security.Cryptography
;
using
System.Text
;
using
Newtonsoft.Json
;
using
Newtonsoft.Json.Linq
;
using
Tools.MyAgent.Config
;
namespace
Tools.MyAgent.Support
{
/// <summary>Agent 运行模式(config_center_plan.md v2.2)。</summary>
public
enum
AgentRunMode
{
/// <summary>自定义模式(现状):本地 AgentConfig.json,人人可设置,零改动。</summary>
Custom
=
0
,
/// <summary>分发模式:配置中心(SQL Server),管理员写透、普通用户拉取。</summary>
Distribution
=
1
,
}
/// <summary>
/// 配置中心解析结果(config_center_plan.md v2.0–v2.3)。
/// <para>承载中心库拉取/快照/缺省三级来源合并后的配置,由 <see cref="AgentCenterClient.Fetch"/> 产出;
/// 通过 <see cref="ApplyTo"/> 覆盖到 AgentConfig(字段级"非空才覆盖")。</para>
/// </summary>
public
sealed
class
CenterResolution
{
/// <summary>生效模式(Custom = 仅承载模式与角色,其余字段为空)。</summary>
public
AgentRunMode
Mode
{
get
;
set
;
}
/// <summary>是否管理员:宿主入参优先,否则中心 AdminRoles 名单比对;Custom 模式恒 true(现状人人可设置)。</summary>
public
bool
IsAdmin
{
get
;
set
;
}
/// <summary>配置来源:custom / db / snapshot / none。</summary>
public
string
Source
{
get
;
set
;
}
=
"none"
;
// ── 模型(Agent_Center_Model) ──
public
string
Provider
{
get
;
set
;
}
public
string
BaseUrl
{
get
;
set
;
}
public
string
ModelName
{
get
;
set
;
}
public
string
ApiKey
{
get
;
set
;
}
public
double
?
Temperature
{
get
;
set
;
}
public
int
?
MaxTokens
{
get
;
set
;
}
public
int
?
MaxContextTokens
{
get
;
set
;
}
public
string
RemoteKbJson
{
get
;
set
;
}
// ── 知识库(Agent_Center_Kb,{CLIENT_ROOT} 已展开) ──
public
string
KbRoot
{
get
;
set
;
}
public
string
SearchScope
{
get
;
set
;
}
public
bool
KbConfigured
{
get
;
set
;
}
public
string
KbMode
{
get
;
set
;
}
public
string
ShareUser
{
get
;
set
;
}
public
string
SharePassword
{
get
;
set
;
}
public
KbProbeResult
KbProbe
{
get
;
set
;
}
// ── 全局(Agent_Center_Global) ──
public
string
AdminRoles
{
get
;
set
;
}
public
bool
?
RunDbEnabled
{
get
;
set
;
}
public
string
RunDbConnection
{
get
;
set
;
}
public
string
RunDbTable
{
get
;
set
;
}
public
bool
?
TelemetryEnabled
{
get
;
set
;
}
public
string
ObservabilityLevel
{
get
;
set
;
}
/// <summary>缺项清单(仅分发模式计算):ApiKey / BaseUrl / Model。KB 缺省不进清单(v2.3:默认 {CLIENT_ROOT} 静默兜底)。</summary>
public
List
<
string
>
MissingItems
{
get
;
}
=
new
List
<
string
>();
/// <summary>把本分辨率结果覆盖到 AgentConfig(字段级"非空才覆盖";中心未配置项回落本地)。</summary>
public
void
ApplyTo
(
AgentConfig
cfg
)
{
try
{
if
(
cfg
==
null
)
return
;
if
(
NotBlack
(
Provider
))
cfg
.
Provider
=
Provider
;
if
(
NotBlack
(
BaseUrl
))
cfg
.
BaseUrl
=
BaseUrl
;
if
(
NotBlack
(
ModelName
))
cfg
.
Model
=
ModelName
;
if
(
NotBlack
(
ApiKey
))
cfg
.
ApiKey
=
ApiKey
;
if
(
Temperature
.
HasValue
)
cfg
.
Temperature
=
Temperature
.
Value
;
if
(
MaxTokens
.
HasValue
&&
MaxTokens
.
Value
>
0
)
cfg
.
MaxTokens
=
MaxTokens
.
Value
;
if
(
MaxContextTokens
.
HasValue
&&
MaxContextTokens
.
Value
>
0
)
cfg
.
MaxContextTokens
=
MaxContextTokens
.
Value
;
if
(
NotBlack
(
RemoteKbJson
))
{
try
{
var
list
=
JsonConvert
.
DeserializeObject
<
List
<
string
>>(
RemoteKbJson
);
if
(
list
!=
null
)
cfg
.
RemoteKbEntries
=
list
;
}
catch
{
}
}
if
(
NotBlack
(
KbRoot
))
cfg
.
KnowledgeBasePath
=
KbRoot
;
if
(
NotBlack
(
SearchScope
))
cfg
.
SearchScope
=
SearchScope
;
if
(
NotBlack
(
KbMode
))
cfg
.
KbMode
=
KbMode
;
if
(
NotBlack
(
ShareUser
))
cfg
.
ShareUser
=
ShareUser
;
if
(
NotBlack
(
SharePassword
))
cfg
.
SharePassword
=
SharePassword
;
if
(
NotBlack
(
AdminRoles
))
cfg
.
AdminRoles
=
AdminRoles
;
if
(
RunDbEnabled
.
HasValue
)
cfg
.
RunDbEnabled
=
RunDbEnabled
.
Value
;
if
(
NotBlack
(
RunDbConnection
))
cfg
.
RunDbConnection
=
RunDbConnection
;
if
(
NotBlack
(
RunDbTable
))
cfg
.
RunDbTable
=
RunDbTable
;
if
(
TelemetryEnabled
.
HasValue
)
cfg
.
TelemetryEnabled
=
TelemetryEnabled
.
Value
;
if
(
NotBlack
(
ObservabilityLevel
))
cfg
.
ObservabilityLevel
=
ObservabilityLevel
;
}
catch
{
}
}
internal
static
bool
NotBlack
(
string
s
)
{
return
!
string
.
IsNullOrWhiteSpace
(
s
);
}
}
/// <summary>
/// 配置中心客户端(config_center_plan.md P0 读侧)。
/// <para>职责:模式判定(宿主入参优先 / center.json 自动判定兜底)→ 拉取中心四表 →
/// {CLIENT_ROOT} 展开 → 角色判定 → 缺项计算 → 快照读写(离线降级)。</para>
/// <para>红线(与 AgentTelemetry 同规):本类所有 public 方法体首句必须是 try —— 任何异常不上抛、不阻塞启动。</para>
/// <para>安全:ApiKey 等敏感列 AES-256(密钥内嵌派生,防库备份肉眼泄露);快照文件只存密文。</para>
/// </summary>
public
static
class
AgentCenterClient
{
// ══════════════ 常量 ══════════════
/// <summary>{CLIENT_ROOT} 占位符(v2.4 路径统一规范:仅本地文件夹生效;共享文件夹 UNC 原样存储)。</summary>
public
const
string
ClientRootPlaceholder
=
"{CLIENT_ROOT}"
;
/// <summary>AES 密文前缀。</summary>
public
const
string
SecretPrefix
=
"aes:"
;
private
const
string
SnapshotFileName
=
"center_cache.json"
;
private
const
string
ConnectTimeoutToken
=
"Connect Timeout"
;
// ══════════════ 模式判定 ══════════════
/// <summary>解析 Agent 数据根目录:程序目录本身已是 Agent 时不再重复追加 Agent。</summary>
public
static
string
AgentRootPath
(
string
baseDirectory
)
{
try
{
string
root
=
NormalizeRoot
(
baseDirectory
);
return
string
.
Equals
(
Path
.
GetFileName
(
root
),
"Agent"
,
StringComparison
.
OrdinalIgnoreCase
)
?
root
:
Path
.
Combine
(
root
,
"Agent"
);
}
catch
{
return
"Agent"
;
}
}
/// <summary>
/// 将旧版程序目录下的 Agent\Agent\config 配置迁移到外层 Agent\config。
/// <para>目标文件存在时保留目标配置,并将旧文件改名为 .legacy 备份;不会直接删除旧配置。</para>
/// </summary>
public
static
void
MigrateLegacyLayout
(
string
baseDirectory
)
{
try
{
string
root
=
AgentRootPath
(
baseDirectory
);
string
legacyRoot
=
Path
.
Combine
(
root
,
"Agent"
);
if
(
string
.
Equals
(
root
.
TrimEnd
(
'\\'
),
legacyRoot
.
TrimEnd
(
'\\'
),
StringComparison
.
OrdinalIgnoreCase
))
return
;
string
source
=
Path
.
Combine
(
legacyRoot
,
"config"
);
if
(!
Directory
.
Exists
(
source
))
return
;
string
target
=
Path
.
Combine
(
root
,
"config"
);
Directory
.
CreateDirectory
(
target
);
foreach
(
string
file
in
Directory
.
GetFiles
(
source
))
MoveOrPreserveLegacyFile
(
file
,
Path
.
Combine
(
target
,
Path
.
GetFileName
(
file
)));
}
catch
{
}
}
private
static
void
MoveOrPreserveLegacyFile
(
string
source
,
string
target
)
{
try
{
if
(!
File
.
Exists
(
source
))
return
;
if
(!
File
.
Exists
(
target
))
{
File
.
Move
(
source
,
target
);
return
;
}
string
backup
=
target
+
".legacy-"
+
DateTime
.
Now
.
ToString
(
"yyyyMMddHHmmssfff"
);
int
index
=
1
;
while
(
File
.
Exists
(
backup
))
backup
=
target
+
".legacy-"
+
DateTime
.
Now
.
ToString
(
"yyyyMMddHHmmssfff"
)
+
"-"
+
index
++;
File
.
Move
(
source
,
backup
);
}
catch
{
}
}
/// <summary>center.json 固定位置:Agent 数据根目录下的 config\center.json。</summary>
public
static
string
CenterJsonPath
(
string
baseDirectory
)
{
try
{
return
Path
.
Combine
(
AgentRootPath
(
baseDirectory
),
"config"
,
"center.json"
);
}
catch
{
return
Path
.
Combine
(
"Agent"
,
"config"
,
"center.json"
);
}
}
/// <summary>
/// 模式判定(v2.2):宿主入参显式传入时以入参为准(显式优于隐式);
/// 未传时自动判定 center.json 是否存在(覆盖独立运行场景)。任何异常回退 Custom。
/// </summary>
public
static
AgentRunMode
DetectMode
(
AgentRunMode
?
explicitMode
,
string
baseDirectory
)
{
try
{
if
(
explicitMode
.
HasValue
)
return
explicitMode
.
Value
;
return
File
.
Exists
(
CenterJsonPath
(
baseDirectory
))
?
AgentRunMode
.
Distribution
:
AgentRunMode
.
Custom
;
}
catch
{
return
explicitMode
.
HasValue
?
explicitMode
.
Value
:
AgentRunMode
.
Custom
;
}
}
// ══════════════ 主入口 ══════════════
/// <summary>
/// 拉取并构建配置中心解析结果。任何失败均不抛出;分发模式下按
/// 中心库 → 本地快照 → 缺省 三级来源降级。
/// <para>中心引导连接优先读取 center.json;缺失时兼容使用本机已保存的 RunDbConnection,
/// 以支持首次配置时用“对话上报”连接完成中心库引导。</para>
/// </summary>
public
static
CenterResolution
Fetch
(
string
baseDirectory
,
AgentRunMode
?
explicitMode
,
bool
?
explicitIsAdmin
,
string
currentUser
,
string
productLineId
)
{
try
{
MigrateLegacyLayout
(
baseDirectory
);
AgentRunMode
mode
=
DetectMode
(
explicitMode
,
baseDirectory
);
if
(
mode
==
AgentRunMode
.
Custom
)
{
// 自定义模式:现状行为,人人可设置;不读中心、不写快照
return
new
CenterResolution
{
Mode
=
AgentRunMode
.
Custom
,
IsAdmin
=
explicitIsAdmin
??
true
,
Source
=
"custom"
};
}
CenterPull
pull
=
null
;
string
source
=
"none"
;
string
connection
=
ReadBootstrap
(
baseDirectory
);
if
(!
NotBlack
(
connection
))
{
connection
=
ReadLocalRunDbConnection
(
baseDirectory
);
if
(
NotBlack
(
connection
))
EnsureBootstrapFile
(
baseDirectory
,
connection
);
}
if
(
NotBlack
(
connection
))
{
pull
=
PullFromDb
(
connection
,
productLineId
);
if
(
pull
!=
null
)
{
source
=
"db"
;
WriteSnapshot
(
baseDirectory
,
pull
);
}
}
if
(
pull
==
null
)
{
pull
=
ReadSnapshot
(
baseDirectory
);
if
(
pull
!=
null
)
source
=
"snapshot"
;
}
return
Build
(
pull
,
source
,
mode
,
explicitIsAdmin
,
currentUser
,
baseDirectory
);
}
catch
{
var
fb
=
new
CenterResolution
{
Mode
=
AgentRunMode
.
Distribution
,
Source
=
"none"
};
fb
.
IsAdmin
=
explicitIsAdmin
??
false
;
fb
.
MissingItems
.
Add
(
"ApiKey"
);
fb
.
MissingItems
.
Add
(
"BaseUrl"
);
fb
.
MissingItems
.
Add
(
"Model"
);
return
fb
;
}
}
// ══════════════ {CLIENT_ROOT} 展开 / 反折(v2.4:仅本地文件夹生效,UNC 共享保持原样) ══════════════
/// <summary>把模板中的 {CLIENT_ROOT} 替换为程序根目录(去尾 \);不含占位符原样返回(UNC 共享路径天然走此分支);空模板返回空串。</summary>
public
static
string
ExpandClientRoot
(
string
template
,
string
baseDirectory
)
{
try
{
if
(
string
.
IsNullOrEmpty
(
template
))
return
""
;
string
root
=
NormalizeRoot
(
baseDirectory
);
return
template
.
Replace
(
ClientRootPlaceholder
,
root
);
}
catch
{
return
template
??
""
;
}
}
/// <summary>
/// 把本机**本地**绝对路径反折回 {CLIENT_ROOT} 占位符(管理员保存用,P1)。
/// <para>v2.4 例外:共享文件夹(UNC 路径 \\server\share\…)保持原有模式——各终端访问同一网络路径,
/// 无需按本机展开,原样存储原样使用;程序目录本身在 UNC 上时同样不折叠。</para>
/// </summary>
public
static
string
FoldClientRoot
(
string
path
,
string
baseDirectory
)
{
try
{
if
(
string
.
IsNullOrEmpty
(
path
))
return
path
??
""
;
if
(
path
.
StartsWith
(
"\\\\"
)
||
path
.
StartsWith
(
"//"
))
return
path
;
// UNC 共享:原样
string
root
=
NormalizeRoot
(
baseDirectory
);
if
(
root
.
StartsWith
(
"\\\\"
)
||
root
.
StartsWith
(
"//"
))
return
path
;
// 程序目录在共享上:不折叠
if
(
path
.
StartsWith
(
root
,
StringComparison
.
OrdinalIgnoreCase
))
return
ClientRootPlaceholder
+
path
.
Substring
(
root
.
Length
);
return
path
;
}
catch
{
return
path
??
""
;
}
}
// ══════════════ 敏感列 AES-256 ══════════════
private
static
byte
[]
_aesKey
;
private
static
byte
[]
GetAesKey
()
{
if
(
_aesKey
==
null
)
{
using
(
var
sha
=
new
SHA256CryptoServiceProvider
())
_aesKey
=
sha
.
ComputeHash
(
Encoding
.
UTF8
.
GetBytes
(
"EasyWork.Agent.Center.v1|Ftt"
));
}
return
_aesKey
;
}
/// <summary>AES-256-CBC 加密(随机 IV 前置,base64,"aes:" 前缀);空串原样返回;失败回退明文。</summary>
public
static
string
ProtectSecret
(
string
plain
)
{
try
{
if
(
string
.
IsNullOrEmpty
(
plain
))
return
plain
??
""
;
using
(
var
aes
=
new
AesCryptoServiceProvider
())
{
aes
.
Key
=
GetAesKey
();
aes
.
GenerateIV
();
using
(
var
enc
=
aes
.
CreateEncryptor
())
{
byte
[]
data
=
Encoding
.
UTF8
.
GetBytes
(
plain
);
byte
[]
cipher
=
enc
.
TransformFinalBlock
(
data
,
0
,
data
.
Length
);
byte
[]
output
=
new
byte
[
aes
.
IV
.
Length
+
cipher
.
Length
];
Buffer
.
BlockCopy
(
aes
.
IV
,
0
,
output
,
0
,
aes
.
IV
.
Length
);
Buffer
.
BlockCopy
(
cipher
,
0
,
output
,
aes
.
IV
.
Length
,
cipher
.
Length
);
return
SecretPrefix
+
Convert
.
ToBase64String
(
output
);
}
}
}
catch
{
return
plain
??
""
;
}
}
/// <summary>解密 "aes:" 密文;非本前缀视为明文原样返回(兼容);解密失败返回空串(视为未配置)。</summary>
public
static
string
UnprotectSecret
(
string
stored
)
{
try
{
if
(
string
.
IsNullOrEmpty
(
stored
))
return
stored
??
""
;
if
(!
stored
.
StartsWith
(
SecretPrefix
,
StringComparison
.
Ordinal
))
return
stored
;
byte
[]
all
=
Convert
.
FromBase64String
(
stored
.
Substring
(
SecretPrefix
.
Length
));
using
(
var
aes
=
new
AesCryptoServiceProvider
())
{
aes
.
Key
=
GetAesKey
();
int
ivLen
=
aes
.
BlockSize
/
8
;
byte
[]
iv
=
new
byte
[
ivLen
];
byte
[]
cipher
=
new
byte
[
all
.
Length
-
ivLen
];
Buffer
.
BlockCopy
(
all
,
0
,
iv
,
0
,
ivLen
);
Buffer
.
BlockCopy
(
all
,
ivLen
,
cipher
,
0
,
cipher
.
Length
);
aes
.
IV
=
iv
;
using
(
var
dec
=
aes
.
CreateDecryptor
())
{
byte
[]
plain
=
dec
.
TransformFinalBlock
(
cipher
,
0
,
cipher
.
Length
);
return
Encoding
.
UTF8
.
GetString
(
plain
);
}
}
}
catch
{
return
""
;
}
}
// ══════════════ 写侧(P1:管理员保存写透) ══════════════
/// <summary>连接串附加启动短超时(幂等;已有 Connect Timeout 则不动)。</summary>
private
static
string
WithShortTimeout
(
string
connection
)
{
try
{
if
(
string
.
IsNullOrEmpty
(
connection
))
return
connection
;
if
(
connection
.
IndexOf
(
ConnectTimeoutToken
,
StringComparison
.
OrdinalIgnoreCase
)
>=
0
)
return
connection
;
return
connection
+
";Connect Timeout=3"
;
}
catch
{
return
connection
;
}
}
/// <summary>
/// 测试中心连接(设置页「测试连接」/ P2 /diag 用):Open + 幂等建表。
/// <para>返回错误消息,成功返回 null(不抛异常——遵守本类 public 不上抛红线;不写任何数据行)。</para>
/// </summary>
public
static
string
TestCenterConnection
(
string
connection
)
{
try
{
if
(!
NotBlack
(
connection
))
return
"连接串为空"
;
using
(
var
conn
=
new
SqlConnection
(
WithShortTimeout
(
connection
)))
{
conn
.
Open
();
EnsureCenterTablesOn
(
conn
);
}
return
null
;
}
catch
(
Exception
ex
)
{
return
ex
.
Message
;
}
}
/// <summary>
/// 管理员保存写透(P1):把 AgentConfig 的中心管辖区字段 Upsert 到中心库,路径反折 {CLIENT_ROOT}。
/// <para>中心管辖区 = 模型(Provider/BaseUrl/Model/ApiKey/生成参数/远程KB)+
/// 知识库(根/范围/方式/共享凭据)+ Global(AdminRoles/RunDb/遥测)。</para>
/// <para>返回错误消息,成功返回 null。</para>
/// </summary>
public
static
string
SaveFromConfig
(
string
baseDirectory
,
AgentConfig
cfg
)
{
try
{
if
(
cfg
==
null
)
return
"配置对象为空"
;
string
connection
=
ReadBootstrap
(
baseDirectory
);
if
(!
NotBlack
(
connection
))
connection
=
SecretProtector
.
Unprotect
(
cfg
.
RunDbConnection
);
if
(!
NotBlack
(
connection
))
connection
=
ReadLocalRunDbConnection
(
baseDirectory
);
if
(!
NotBlack
(
ReadBootstrap
(
baseDirectory
)))
EnsureBootstrapFile
(
baseDirectory
,
connection
);
if
(!
NotBlack
(
connection
))
return
"未找到中心库连接(Agent/config/center.json 或本地 RunDbConnection)"
;
var
model
=
new
CenterModelRow
{
Name
=
"默认"
,
IsDefault
=
true
,
Provider
=
cfg
.
Provider
??
""
,
BaseUrl
=
cfg
.
BaseUrl
??
""
,
Model
=
cfg
.
Model
??
""
,
ApiKeyEnc
=
ProtectSecret
(
cfg
.
ApiKey
??
""
),
Temperature
=
cfg
.
Temperature
,
MaxTokens
=
cfg
.
MaxTokens
,
MaxContextTokens
=
cfg
.
MaxContextTokens
,
RemoteKbJson
=
JsonConvert
.
SerializeObject
(
cfg
.
RemoteKbEntries
??
new
List
<
string
>()),
};
var
kb
=
new
CenterKbRow
{
Name
=
"默认"
,
IsDefault
=
true
,
KbRootTpl
=
FoldClientRoot
(
cfg
.
KnowledgeBasePath
??
""
,
baseDirectory
),
SearchScopeTpl
=
FoldClientRoot
(
cfg
.
SearchScope
??
""
,
baseDirectory
),
KbMode
=
cfg
.
KbMode
??
""
,
ShareUser
=
cfg
.
ShareUser
??
""
,
SharePwdEnc
=
ProtectSecret
(
cfg
.
SharePassword
??
""
),
};
var
global
=
new
CenterGlobalRow
{
AdminRoles
=
cfg
.
AdminRoles
??
""
,
RunDbEnabled
=
cfg
.
RunDbEnabled
,
RunDbConnection
=
ProtectSecret
(
cfg
.
RunDbConnection
??
""
),
RunDbTable
=
cfg
.
RunDbTable
??
"Agent_RunLog"
,
TelemetryEnabled
=
cfg
.
TelemetryEnabled
,
ObservabilityLevel
=
cfg
.
ObservabilityLevel
??
"basic"
,
};
return
SaveCenter
(
connection
,
model
,
kb
,
global
);
}
catch
(
Exception
ex
)
{
return
ex
.
Message
;
}
}
/// <summary>Upsert 三表(模型/KB 写 IsDefault=1 行,Global 单行);缺表自动创建。返回错误消息,成功返回 null。</summary>
public
static
string
SaveCenter
(
string
connection
,
CenterModelRow
model
,
CenterKbRow
kb
,
CenterGlobalRow
global
)
{
try
{
if
(!
NotBlack
(
connection
))
return
"连接串为空"
;
using
(
var
conn
=
new
SqlConnection
(
WithShortTimeout
(
connection
)))
{
conn
.
Open
();
EnsureCenterTablesOn
(
conn
);
string
updatedBy
=
(
Environment
.
UserName
??
""
)
+
"@"
+
(
Environment
.
MachineName
??
""
);
if
(
model
!=
null
)
UpsertModel
(
conn
,
model
,
updatedBy
);
if
(
kb
!=
null
)
UpsertKb
(
conn
,
kb
,
updatedBy
);
if
(
global
!=
null
)
UpsertGlobal
(
conn
,
global
,
updatedBy
);
}
return
null
;
}
catch
(
Exception
ex
)
{
return
ex
.
Message
;
}
}
/// <summary>幂等建表(四表;IF OBJECT_ID NULL 才建,管理员首次发布即完成中心库初始化)。</summary>
private
static
void
EnsureCenterTablesOn
(
SqlConnection
conn
)
{
ExecOrNull
(
conn
,
"IF OBJECT_ID(N'dbo."
+
TModel
+
"', N'U') IS NULL "
+
"CREATE TABLE dbo."
+
TModel
+
" ("
+
"Id INT IDENTITY(1,1) PRIMARY KEY, "
+
"Name NVARCHAR(64) NOT NULL UNIQUE, "
+
"IsDefault BIT NOT NULL DEFAULT 0, "
+
"Provider NVARCHAR(32) NOT NULL DEFAULT '', "
+
"BaseUrl NVARCHAR(256) NOT NULL DEFAULT '', "
+
"Model NVARCHAR(128) NOT NULL DEFAULT '', "
+
"ApiKeyEnc NVARCHAR(MAX) NULL, "
+
"Temperature FLOAT NOT NULL DEFAULT 0.3, "
+
"MaxTokens INT NOT NULL DEFAULT 16384, "
+
"MaxContextTokens INT NOT NULL DEFAULT 64000, "
+
"RemoteKbJson NVARCHAR(MAX) NULL, "
+
"UpdatedAt DATETIME2(3) NOT NULL DEFAULT SYSDATETIME(), "
+
"UpdatedBy NVARCHAR(64) NOT NULL DEFAULT '');"
);
ExecOrNull
(
conn
,
"IF OBJECT_ID(N'dbo."
+
TKb
+
"', N'U') IS NULL "
+
"CREATE TABLE dbo."
+
TKb
+
" ("
+
"Id INT IDENTITY(1,1) PRIMARY KEY, "
+
"Name NVARCHAR(64) NOT NULL UNIQUE, "
+
"IsDefault BIT NOT NULL DEFAULT 0, "
+
"KbRootTpl NVARCHAR(512) NOT NULL DEFAULT '', "
+
"SearchScopeTpl NVARCHAR(512) NULL, "
+
"KbMode VARCHAR(8) NOT NULL DEFAULT '', "
+
"ShareUser NVARCHAR(64) NOT NULL DEFAULT '', "
+
"SharePwdEnc NVARCHAR(MAX) NULL, "
+
"UpdatedAt DATETIME2(3) NOT NULL DEFAULT SYSDATETIME(), "
+
"UpdatedBy NVARCHAR(64) NOT NULL DEFAULT '');"
);
ExecOrNull
(
conn
,
"IF OBJECT_ID(N'dbo."
+
TAssign
+
"', N'U') IS NULL "
+
"CREATE TABLE dbo."
+
TAssign
+
" ("
+
"Id INT IDENTITY(1,1) PRIMARY KEY, "
+
"MachineName NVARCHAR(64) NOT NULL, "
+
"ProductLineId NVARCHAR(64) NOT NULL DEFAULT '', "
+
"ModelId INT NULL, "
+
"KbId INT NULL, "
+
"Remark NVARCHAR(256) NOT NULL DEFAULT '', "
+
"UpdatedAt DATETIME2(3) NOT NULL DEFAULT SYSDATETIME(), "
+
"CONSTRAINT UX_"
+
TAssign
+
" UNIQUE (MachineName, ProductLineId));"
);
ExecOrNull
(
conn
,
"IF OBJECT_ID(N'dbo."
+
TGlobal
+
"', N'U') IS NULL "
+
"CREATE TABLE dbo."
+
TGlobal
+
" ("
+
"Id INT IDENTITY(1,1) PRIMARY KEY, "
+
"AdminRoles NVARCHAR(256) NOT NULL DEFAULT '', "
+
"RunDbEnabled BIT NOT NULL DEFAULT 0, "
+
"RunDbConnection NVARCHAR(MAX) NULL, "
+
"RunDbTable NVARCHAR(64) NOT NULL DEFAULT 'Agent_RunLog', "
+
"TelemetryEnabled BIT NOT NULL DEFAULT 1, "
+
"ObservabilityLevel VARCHAR(8) NOT NULL DEFAULT 'basic', "
+
"UpdatedAt DATETIME2(3) NOT NULL DEFAULT SYSDATETIME(), "
+
"UpdatedBy NVARCHAR(64) NOT NULL DEFAULT '');"
);
}
/// <summary>对已打开连接执行单条 SQL,失败静默(建表/索引等幂等语句用)。</summary>
private
static
void
ExecOrNull
(
SqlConnection
conn
,
string
sql
)
{
try
{
if
(
string
.
IsNullOrEmpty
(
sql
))
return
;
using
(
var
cmd
=
new
SqlCommand
(
sql
,
conn
))
cmd
.
ExecuteNonQuery
();
}
catch
{
}
}
private
static
void
UpsertModel
(
SqlConnection
conn
,
CenterModelRow
m
,
string
updatedBy
)
{
try
{
using
(
var
cmd
=
new
SqlCommand
(
"UPDATE "
+
TModel
+
" SET "
+
"Provider = @Provider, BaseUrl = @BaseUrl, Model = @Model, ApiKeyEnc = @ApiKeyEnc, "
+
"Temperature = @Temperature, MaxTokens = @MaxTokens, MaxContextTokens = @MaxContextTokens, "
+
"RemoteKbJson = @RemoteKbJson, UpdatedAt = SYSDATETIME(), UpdatedBy = @UpdatedBy "
+
"WHERE IsDefault = 1; "
+
"IF @@ROWCOUNT = 0 INSERT INTO "
+
TModel
+
" "
+
"(Name, IsDefault, Provider, BaseUrl, Model, ApiKeyEnc, Temperature, MaxTokens, MaxContextTokens, RemoteKbJson, UpdatedBy) "
+
"VALUES (@Name, 1, @Provider, @BaseUrl, @Model, @ApiKeyEnc, @Temperature, @MaxTokens, @MaxContextTokens, @RemoteKbJson, @UpdatedBy);"
,
conn
))
{
BindModel
(
cmd
,
m
,
updatedBy
);
cmd
.
ExecuteNonQuery
();
}
}
catch
{
}
}
private
static
void
BindModel
(
SqlCommand
cmd
,
CenterModelRow
m
,
string
updatedBy
)
{
cmd
.
Parameters
.
AddWithValue
(
"@Name"
,
(
object
)(
m
.
Name
??
"默认"
)
??
"默认"
);
cmd
.
Parameters
.
AddWithValue
(
"@Provider"
,
m
.
Provider
??
""
);
cmd
.
Parameters
.
AddWithValue
(
"@BaseUrl"
,
m
.
BaseUrl
??
""
);
cmd
.
Parameters
.
AddWithValue
(
"@Model"
,
m
.
Model
??
""
);
cmd
.
Parameters
.
AddWithValue
(
"@ApiKeyEnc"
,
(
object
)(
m
.
ApiKeyEnc
??
""
)
??
""
);
cmd
.
Parameters
.
AddWithValue
(
"@Temperature"
,
m
.
Temperature
.
HasValue
?
m
.
Temperature
.
Value
:
0.3
);
cmd
.
Parameters
.
AddWithValue
(
"@MaxTokens"
,
m
.
MaxTokens
.
HasValue
?
m
.
MaxTokens
.
Value
:
16384
);
cmd
.
Parameters
.
AddWithValue
(
"@MaxContextTokens"
,
m
.
MaxContextTokens
.
HasValue
?
m
.
MaxContextTokens
.
Value
:
64000
);
cmd
.
Parameters
.
AddWithValue
(
"@RemoteKbJson"
,
(
object
)(
m
.
RemoteKbJson
??
""
)
??
""
);
cmd
.
Parameters
.
AddWithValue
(
"@UpdatedBy"
,
updatedBy
??
""
);
}
private
static
void
UpsertKb
(
SqlConnection
conn
,
CenterKbRow
k
,
string
updatedBy
)
{
try
{
using
(
var
cmd
=
new
SqlCommand
(
"UPDATE "
+
TKb
+
" SET "
+
"KbRootTpl = @KbRootTpl, SearchScopeTpl = @SearchScopeTpl, KbMode = @KbMode, "
+
"ShareUser = @ShareUser, SharePwdEnc = @SharePwdEnc, UpdatedAt = SYSDATETIME(), UpdatedBy = @UpdatedBy "
+
"WHERE IsDefault = 1; "
+
"IF @@ROWCOUNT = 0 INSERT INTO "
+
TKb
+
" "
+
"(Name, IsDefault, KbRootTpl, SearchScopeTpl, KbMode, ShareUser, SharePwdEnc, UpdatedBy) "
+
"VALUES (@Name, 1, @KbRootTpl, @SearchScopeTpl, @KbMode, @ShareUser, @SharePwdEnc, @UpdatedBy);"
,
conn
))
{
cmd
.
Parameters
.
AddWithValue
(
"@Name"
,
(
object
)(
k
.
Name
??
"默认"
)
??
"默认"
);
cmd
.
Parameters
.
AddWithValue
(
"@KbRootTpl"
,
k
.
KbRootTpl
??
""
);
cmd
.
Parameters
.
AddWithValue
(
"@SearchScopeTpl"
,
(
object
)(
k
.
SearchScopeTpl
??
""
)
??
""
);
cmd
.
Parameters
.
AddWithValue
(
"@KbMode"
,
(
object
)(
k
.
KbMode
??
""
)
??
""
);
cmd
.
Parameters
.
AddWithValue
(
"@ShareUser"
,
k
.
ShareUser
??
""
);
cmd
.
Parameters
.
AddWithValue
(
"@SharePwdEnc"
,
(
object
)(
k
.
SharePwdEnc
??
""
)
??
""
);
cmd
.
Parameters
.
AddWithValue
(
"@UpdatedBy"
,
updatedBy
??
""
);
cmd
.
ExecuteNonQuery
();
}
}
catch
{
}
}
private
static
void
UpsertGlobal
(
SqlConnection
conn
,
CenterGlobalRow
g
,
string
updatedBy
)
{
try
{
using
(
var
cmd
=
new
SqlCommand
(
"IF EXISTS (SELECT 1 FROM "
+
TGlobal
+
") "
+
"UPDATE "
+
TGlobal
+
" SET "
+
"AdminRoles = @AdminRoles, RunDbEnabled = @RunDbEnabled, RunDbConnection = @RunDbConnection, "
+
"RunDbTable = @RunDbTable, TelemetryEnabled = @TelemetryEnabled, ObservabilityLevel = @ObservabilityLevel, "
+
"UpdatedAt = SYSDATETIME(), UpdatedBy = @UpdatedBy; "
+
"ELSE INSERT INTO "
+
TGlobal
+
" "
+
"(AdminRoles, RunDbEnabled, RunDbConnection, RunDbTable, TelemetryEnabled, ObservabilityLevel, UpdatedBy) "
+
"VALUES (@AdminRoles, @RunDbEnabled, @RunDbConnection, @RunDbTable, @TelemetryEnabled, @ObservabilityLevel, @UpdatedBy);"
,
conn
))
{
cmd
.
Parameters
.
AddWithValue
(
"@AdminRoles"
,
g
.
AdminRoles
??
""
);
cmd
.
Parameters
.
AddWithValue
(
"@RunDbEnabled"
,
g
.
RunDbEnabled
??
false
);
cmd
.
Parameters
.
AddWithValue
(
"@RunDbConnection"
,
(
object
)(
g
.
RunDbConnection
??
""
)
??
""
);
cmd
.
Parameters
.
AddWithValue
(
"@RunDbTable"
,
g
.
RunDbTable
??
"Agent_RunLog"
);
cmd
.
Parameters
.
AddWithValue
(
"@TelemetryEnabled"
,
g
.
TelemetryEnabled
??
true
);
cmd
.
Parameters
.
AddWithValue
(
"@ObservabilityLevel"
,
g
.
ObservabilityLevel
??
"basic"
);
cmd
.
Parameters
.
AddWithValue
(
"@UpdatedBy"
,
updatedBy
??
""
);
cmd
.
ExecuteNonQuery
();
}
}
catch
{
}
}
// ══════════════ 引导配置 ══════════════
/// <summary>读 center.json 的 Connection 字段;文件缺失/无连接串返回空。</summary>
private
static
string
ReadBootstrap
(
string
baseDirectory
)
{
try
{
string
path
=
CenterJsonPath
(
baseDirectory
);
if
(!
File
.
Exists
(
path
))
return
""
;
var
obj
=
JObject
.
Parse
(
File
.
ReadAllText
(
path
,
Encoding
.
UTF8
));
string
conn
=
(
string
)
obj
[
"Connection"
];
return
(
conn
??
""
).
Trim
();
}
catch
{
return
""
;
}
}
/// <summary>
/// 读取本地 AgentConfig 中已保存的 RunDbConnection,作为中心库连接的首次引导回退。
/// <para>RunDbConnection 与配置中心按方案共用同一数据库;密文仅在当前用户上下文中解密。</para>
/// </summary>
private
static
string
ReadLocalRunDbConnection
(
string
baseDirectory
)
{
try
{
string
path
=
Path
.
Combine
(
AgentRootPath
(
baseDirectory
),
"config"
,
"AgentConfig.json"
);
if
(!
File
.
Exists
(
path
))
return
""
;
var
cfg
=
JsonConvert
.
DeserializeObject
<
AgentConfig
>(
File
.
ReadAllText
(
path
,
Encoding
.
UTF8
));
return
cfg
==
null
?
""
:
(
SecretProtector
.
Unprotect
(
cfg
.
RunDbConnection
)
??
""
).
Trim
();
}
catch
{
return
""
;
}
}
/// <summary>
/// center.json 缺失且已有中心库连接时创建引导文件;已有文件不覆盖。
/// <para>只有拿到非空连接串才创建,避免普通自定义实例因空文件被误判为分发模式。</para>
/// </summary>
private
static
void
EnsureBootstrapFile
(
string
baseDirectory
,
string
connection
)
{
try
{
if
(!
NotBlack
(
connection
))
return
;
string
path
=
CenterJsonPath
(
baseDirectory
);
if
(
File
.
Exists
(
path
))
return
;
string
directory
=
Path
.
GetDirectoryName
(
path
);
if
(!
string
.
IsNullOrEmpty
(
directory
))
Directory
.
CreateDirectory
(
directory
);
var
root
=
new
JObject
{
[
"Connection"
]
=
connection
.
Trim
(),
[
"TablePrefix"
]
=
"Agent_Center"
,
};
string
temp
=
path
+
".tmp"
;
File
.
WriteAllText
(
temp
,
root
.
ToString
(
Formatting
.
Indented
),
Encoding
.
UTF8
);
if
(
File
.
Exists
(
path
))
{
File
.
Delete
(
temp
);
return
;
}
File
.
Move
(
temp
,
path
);
}
catch
{
try
{
string
temp
=
CenterJsonPath
(
baseDirectory
)
+
".tmp"
;
if
(
File
.
Exists
(
temp
))
File
.
Delete
(
temp
);
}
catch
{
}
}
}
// ══════════════ 中心库拉取 ══════════════
private
const
string
TModel
=
"Agent_Center_Model"
;
private
const
string
TKb
=
"Agent_Center_Kb"
;
private
const
string
TAssign
=
"Agent_Center_Assign"
;
private
const
string
TGlobal
=
"Agent_Center_Global"
;
/// <summary>中心库原始拉取结果(快照序列化载体)。</summary>
public
sealed
class
CenterPull
{
public
string
FetchedAt
{
get
;
set
;
}
public
CenterModelRow
Model
{
get
;
set
;
}
public
CenterKbRow
Kb
{
get
;
set
;
}
public
CenterGlobalRow
Global
{
get
;
set
;
}
}
public
sealed
class
CenterModelRow
{
public
int
Id
{
get
;
set
;
}
public
string
Name
{
get
;
set
;
}
public
bool
IsDefault
{
get
;
set
;
}
public
string
Provider
{
get
;
set
;
}
public
string
BaseUrl
{
get
;
set
;
}
public
string
Model
{
get
;
set
;
}
public
string
ApiKeyEnc
{
get
;
set
;
}
public
double
?
Temperature
{
get
;
set
;
}
public
int
?
MaxTokens
{
get
;
set
;
}
public
int
?
MaxContextTokens
{
get
;
set
;
}
public
string
RemoteKbJson
{
get
;
set
;
}
}
public
sealed
class
CenterKbRow
{
public
int
Id
{
get
;
set
;
}
public
string
Name
{
get
;
set
;
}
public
bool
IsDefault
{
get
;
set
;
}
public
string
KbRootTpl
{
get
;
set
;
}
public
string
SearchScopeTpl
{
get
;
set
;
}
public
string
KbMode
{
get
;
set
;
}
public
string
ShareUser
{
get
;
set
;
}
public
string
SharePwdEnc
{
get
;
set
;
}
}
public
sealed
class
CenterGlobalRow
{
public
string
AdminRoles
{
get
;
set
;
}
public
bool
?
RunDbEnabled
{
get
;
set
;
}
public
string
RunDbConnection
{
get
;
set
;
}
public
string
RunDbTable
{
get
;
set
;
}
public
bool
?
TelemetryEnabled
{
get
;
set
;
}
public
string
ObservabilityLevel
{
get
;
set
;
}
}
/// <summary>拉取中心四表(分配 → 默认 → Global);库不可达/缺表返回 null(由快照兜底)。解析优先级:分配行 > 默认行。</summary>
private
static
CenterPull
PullFromDb
(
string
connection
,
string
productLineId
)
{
try
{
string
connStr
=
connection
;
if
(
connStr
.
IndexOf
(
ConnectTimeoutToken
,
StringComparison
.
OrdinalIgnoreCase
)
<
0
)
connStr
=
connStr
+
";Connect Timeout=3"
;
// 启动链路短超时(方案 §1:≤3s,不阻塞)
using
(
var
conn
=
new
SqlConnection
(
connStr
))
{
conn
.
Open
();
var
pull
=
new
CenterPull
{
FetchedAt
=
DateTime
.
UtcNow
.
ToString
(
"o"
)
};
string
assignModelId
=
null
,
assignKbId
=
null
;
try
{
using
(
var
cmd
=
new
SqlCommand
(
"SELECT ModelId, KbId FROM "
+
TAssign
+
" WHERE MachineName = @m AND ProductLineId = @p"
,
conn
))
{
cmd
.
Parameters
.
AddWithValue
(
"@m"
,
Environment
.
MachineName
??
""
);
cmd
.
Parameters
.
AddWithValue
(
"@p"
,
productLineId
??
""
);
using
(
var
r
=
cmd
.
ExecuteReader
())
{
if
(
r
.
Read
())
{
assignModelId
=
S
(
r
,
"ModelId"
);
assignKbId
=
S
(
r
,
"KbId"
);
}
}
}
}
catch
{
}
// 分配表缺失/无行 = 用默认行
pull
.
Model
=
ReadModelRow
(
conn
,
assignModelId
);
pull
.
Kb
=
ReadKbRow
(
conn
,
assignKbId
);
pull
.
Global
=
ReadGlobalRow
(
conn
);
return
pull
;
}
}
catch
{
return
null
;
}
}
private
static
CenterModelRow
ReadModelRow
(
SqlConnection
conn
,
string
assignId
)
{
try
{
if
(
NotBlack
(
assignId
))
{
var
row
=
QueryModelRow
(
conn
,
"SELECT * FROM "
+
TModel
+
" WHERE Id = @id"
,
assignId
);
if
(
row
!=
null
)
return
row
;
// 分配行命中
}
return
QueryModelRow
(
conn
,
"SELECT TOP 1 * FROM "
+
TModel
+
" WHERE IsDefault = 1"
,
null
);
}
catch
{
return
null
;
}
}
private
static
CenterModelRow
QueryModelRow
(
SqlConnection
conn
,
string
sql
,
string
id
)
{
try
{
using
(
var
cmd
=
new
SqlCommand
(
sql
,
conn
))
{
if
(
id
!=
null
)
cmd
.
Parameters
.
AddWithValue
(
"@id"
,
id
);
using
(
var
r
=
cmd
.
ExecuteReader
())
{
if
(!
r
.
Read
())
return
null
;
return
new
CenterModelRow
{
Id
=
I2
(
r
,
"Id"
),
Name
=
S
(
r
,
"Name"
),
IsDefault
=
B2
(
r
,
"IsDefault"
),
Provider
=
S
(
r
,
"Provider"
),
BaseUrl
=
S
(
r
,
"BaseUrl"
),
Model
=
S
(
r
,
"Model"
),
ApiKeyEnc
=
S
(
r
,
"ApiKeyEnc"
),
Temperature
=
D
(
r
,
"Temperature"
),
MaxTokens
=
I
(
r
,
"MaxTokens"
),
MaxContextTokens
=
I
(
r
,
"MaxContextTokens"
),
RemoteKbJson
=
S
(
r
,
"RemoteKbJson"
),
};
}
}
}
catch
{
return
null
;
}
}
private
static
CenterKbRow
ReadKbRow
(
SqlConnection
conn
,
string
assignId
)
{
try
{
if
(
NotBlack
(
assignId
))
{
var
row
=
QueryKbRow
(
conn
,
"SELECT * FROM "
+
TKb
+
" WHERE Id = @id"
,
assignId
);
if
(
row
!=
null
)
return
row
;
}
return
QueryKbRow
(
conn
,
"SELECT TOP 1 * FROM "
+
TKb
+
" WHERE IsDefault = 1"
,
null
);
}
catch
{
return
null
;
}
}
private
static
CenterKbRow
QueryKbRow
(
SqlConnection
conn
,
string
sql
,
string
id
)
{
try
{
using
(
var
cmd
=
new
SqlCommand
(
sql
,
conn
))
{
if
(
id
!=
null
)
cmd
.
Parameters
.
AddWithValue
(
"@id"
,
id
);
using
(
var
r
=
cmd
.
ExecuteReader
())
{
if
(!
r
.
Read
())
return
null
;
return
new
CenterKbRow
{
Id
=
I2
(
r
,
"Id"
),
Name
=
S
(
r
,
"Name"
),
IsDefault
=
B2
(
r
,
"IsDefault"
),
KbRootTpl
=
S
(
r
,
"KbRootTpl"
),
SearchScopeTpl
=
S
(
r
,
"SearchScopeTpl"
),
KbMode
=
S
(
r
,
"KbMode"
),
ShareUser
=
S
(
r
,
"ShareUser"
),
SharePwdEnc
=
S
(
r
,
"SharePwdEnc"
),
};
}
}
}
catch
{
return
null
;
}
}
private
static
CenterGlobalRow
ReadGlobalRow
(
SqlConnection
conn
)
{
try
{
using
(
var
cmd
=
new
SqlCommand
(
"SELECT TOP 1 * FROM "
+
TGlobal
,
conn
))
using
(
var
r
=
cmd
.
ExecuteReader
())
{
if
(!
r
.
Read
())
return
null
;
return
new
CenterGlobalRow
{
AdminRoles
=
S
(
r
,
"AdminRoles"
),
RunDbEnabled
=
B
(
r
,
"RunDbEnabled"
),
RunDbConnection
=
S
(
r
,
"RunDbConnection"
),
RunDbTable
=
S
(
r
,
"RunDbTable"
),
TelemetryEnabled
=
B
(
r
,
"TelemetryEnabled"
),
ObservabilityLevel
=
S
(
r
,
"ObservabilityLevel"
),
};
}
}
catch
{
return
null
;
}
}
// ══════════════ 快照(离线降级) ══════════════
private
static
string
SnapshotPath
(
string
baseDirectory
)
{
try
{
return
Path
.
Combine
(
AgentRootPath
(
baseDirectory
),
"config"
,
SnapshotFileName
);
}
catch
{
return
SnapshotFileName
;
}
}
/// <summary>写快照(只存原始行数据,敏感列保持密文);失败静默。</summary>
private
static
void
WriteSnapshot
(
string
baseDirectory
,
CenterPull
pull
)
{
try
{
if
(
pull
==
null
)
return
;
string
path
=
SnapshotPath
(
baseDirectory
);
Directory
.
CreateDirectory
(
Path
.
GetDirectoryName
(
path
));
File
.
WriteAllText
(
path
,
JsonConvert
.
SerializeObject
(
pull
,
Formatting
.
Indented
),
Encoding
.
UTF8
);
}
catch
{
}
}
/// <summary>读快照;文件缺失/损坏返回 null。</summary>
private
static
CenterPull
ReadSnapshot
(
string
baseDirectory
)
{
try
{
string
path
=
SnapshotPath
(
baseDirectory
);
if
(!
File
.
Exists
(
path
))
return
null
;
return
JsonConvert
.
DeserializeObject
<
CenterPull
>(
File
.
ReadAllText
(
path
,
Encoding
.
UTF8
));
}
catch
{
return
null
;
}
}
// ══════════════ 解析构建 ══════════════
/// <summary>由原始拉取结果构建分辨率:角色判定 + {CLIENT_ROOT} 展开 + 缺项计算(v2.3:KB 缺省不进缺项)。</summary>
private
static
CenterResolution
Build
(
CenterPull
pull
,
string
source
,
AgentRunMode
mode
,
bool
?
explicitIsAdmin
,
string
currentUser
,
string
baseDirectory
)
{
try
{
var
r
=
new
CenterResolution
{
Mode
=
mode
,
Source
=
source
};
var
g
=
pull
!=
null
?
pull
.
Global
:
null
;
// 角色判定:宿主入参 > 中心 AdminRoles 名单;Custom 模式恒 true(现状)
bool
isAdmin
;
if
(
explicitIsAdmin
.
HasValue
)
isAdmin
=
explicitIsAdmin
.
Value
;
else
isAdmin
=
MatchAdminRole
(
g
!=
null
?
g
.
AdminRoles
:
null
,
currentUser
);
if
(
mode
==
AgentRunMode
.
Custom
)
isAdmin
=
explicitIsAdmin
??
true
;
r
.
IsAdmin
=
isAdmin
;
r
.
AdminRoles
=
g
!=
null
?
g
.
AdminRoles
:
null
;
if
(
g
!=
null
)
{
r
.
RunDbEnabled
=
g
.
RunDbEnabled
;
r
.
RunDbConnection
=
UnprotectSecret
(
g
.
RunDbConnection
);
r
.
RunDbTable
=
g
.
RunDbTable
;
r
.
TelemetryEnabled
=
g
.
TelemetryEnabled
;
r
.
ObservabilityLevel
=
g
.
ObservabilityLevel
;
}
var
m
=
pull
!=
null
?
pull
.
Model
:
null
;
if
(
m
!=
null
)
{
r
.
Provider
=
m
.
Provider
;
r
.
BaseUrl
=
m
.
BaseUrl
;
r
.
ModelName
=
m
.
Model
;
r
.
ApiKey
=
UnprotectSecret
(
m
.
ApiKeyEnc
);
r
.
Temperature
=
m
.
Temperature
;
r
.
MaxTokens
=
m
.
MaxTokens
;
r
.
MaxContextTokens
=
m
.
MaxContextTokens
;
r
.
RemoteKbJson
=
m
.
RemoteKbJson
;
}
// 缺项计算(仅分发模式;v2.3 KB 缺省不进缺项清单)
if
(
mode
==
AgentRunMode
.
Distribution
)
{
AddMissing
(
r
.
MissingItems
,
"ApiKey"
,
m
!=
null
?
r
.
ApiKey
:
null
);
AddMissing
(
r
.
MissingItems
,
"BaseUrl"
,
m
!=
null
?
m
.
BaseUrl
:
null
);
AddMissing
(
r
.
MissingItems
,
"Model"
,
m
!=
null
?
m
.
Model
:
null
);
}
// 知识库:{CLIENT_ROOT} 展开 + 探测(缺省 → 宿主程序根目录,AgentKbProbe 内实现)
var
kb
=
pull
!=
null
?
pull
.
Kb
:
null
;
string
tpl
=
kb
!=
null
?
kb
.
KbRootTpl
:
null
;
r
.
KbConfigured
=
NotBlack
(
tpl
);
r
.
KbProbe
=
AgentKbProbe
.
Probe
(
tpl
,
baseDirectory
);
r
.
KbRoot
=
r
.
KbProbe
.
Resolved
;
if
(
kb
!=
null
)
{
r
.
SearchScope
=
AgentCenterClient
.
ExpandClientRoot
(
kb
.
SearchScopeTpl
,
baseDirectory
);
r
.
KbMode
=
kb
.
KbMode
;
r
.
ShareUser
=
kb
.
ShareUser
;
r
.
SharePassword
=
UnprotectSecret
(
kb
.
SharePwdEnc
);
}
return
r
;
}
catch
{
var
fb
=
new
CenterResolution
{
Mode
=
mode
,
Source
=
"none"
};
fb
.
IsAdmin
=
explicitIsAdmin
??
false
;
return
fb
;
}
}
/// <summary>管理员名单比对(逗号/分号分隔,中英文皆可;忽略大小写与首尾空白)。</summary>
private
static
bool
MatchAdminRole
(
string
roles
,
string
currentUser
)
{
try
{
if
(
string
.
IsNullOrWhiteSpace
(
roles
)
||
string
.
IsNullOrWhiteSpace
(
currentUser
))
return
false
;
foreach
(
string
part
in
roles
.
Split
(
new
[]
{
','
,
';'
,
','
,
';'
},
StringSplitOptions
.
RemoveEmptyEntries
))
{
if
(
string
.
Equals
(
part
.
Trim
(),
currentUser
.
Trim
(),
StringComparison
.
OrdinalIgnoreCase
))
return
true
;
}
return
false
;
}
catch
{
return
false
;
}
}
private
static
void
AddMissing
(
List
<
string
>
list
,
string
name
,
string
value
)
{
try
{
if
(
string
.
IsNullOrWhiteSpace
(
value
)
&&
!
list
.
Contains
(
name
))
list
.
Add
(
name
);
}
catch
{
}
}
// ══════════════ 工具 ══════════════
/// <summary>程序根目录归一:去尾 \。</summary>
public
static
string
NormalizeRoot
(
string
baseDirectory
)
{
string
root
=
string
.
IsNullOrEmpty
(
baseDirectory
)
?
AppDomain
.
CurrentDomain
.
BaseDirectory
:
baseDirectory
;
return
root
.
TrimEnd
(
'\\'
);
}
private
static
bool
NotBlack
(
string
s
)
{
return
!
string
.
IsNullOrWhiteSpace
(
s
);
}
// ── SqlDataReader 列安全读取(列缺失/类型不符返回空,兼容表结构演进) ──
private
static
string
S
(
SqlDataReader
r
,
string
col
)
{
try
{
int
i
=
r
.
GetOrdinal
(
col
);
if
(
r
.
IsDBNull
(
i
))
return
""
;
object
v
=
r
.
GetValue
(
i
);
return
v
==
null
?
""
:
Convert
.
ToString
(
v
,
System
.
Globalization
.
CultureInfo
.
InvariantCulture
);
}
catch
{
return
""
;
}
}
private
static
int
?
I
(
SqlDataReader
r
,
string
col
)
{
try
{
int
i
=
r
.
GetOrdinal
(
col
);
if
(
r
.
IsDBNull
(
i
))
return
null
;
return
Convert
.
ToInt32
(
r
.
GetValue
(
i
),
System
.
Globalization
.
CultureInfo
.
InvariantCulture
);
}
catch
{
return
null
;
}
}
private
static
int
I2
(
SqlDataReader
r
,
string
col
)
{
try
{
return
I
(
r
,
col
)
??
0
;
}
catch
{
return
0
;
}
}
private
static
double
?
D
(
SqlDataReader
r
,
string
col
)
{
try
{
int
i
=
r
.
GetOrdinal
(
col
);
if
(
r
.
IsDBNull
(
i
))
return
null
;
return
Convert
.
ToDouble
(
r
.
GetValue
(
i
),
System
.
Globalization
.
CultureInfo
.
InvariantCulture
);
}
catch
{
return
null
;
}
}
private
static
bool
?
B
(
SqlDataReader
r
,
string
col
)
{
try
{
int
i
=
r
.
GetOrdinal
(
col
);
if
(
r
.
IsDBNull
(
i
))
return
null
;
return
Convert
.
ToBoolean
(
r
.
GetValue
(
i
),
System
.
Globalization
.
CultureInfo
.
InvariantCulture
);
}
catch
{
return
null
;
}
}
private
static
bool
B2
(
SqlDataReader
r
,
string
col
)
{
try
{
return
B
(
r
,
col
)
??
false
;
}
catch
{
return
false
;
}
}
}
}
Tools.Agent/Support/AgentHealth.cs
View file @
41a89411
...
...
@@ -39,8 +39,10 @@ namespace Tools.MyAgent
/// <summary>
/// 执行检查(不含模型服务连通性——它是异步的,由调用方 <see cref="ModelService"/> 单独追加)。
/// <para>v2.2 新增第 10 项 <c>config_center</c>:传入配置中心解析结果即输出该项(null = 自定义模式)。</para>
/// </summary>
public
static
HealthItem
[]
Check
(
AgentConfig
cfg
,
KbService
kb
,
IReadOnlyList
<
DiagnosticRoot
>
roots
)
public
static
HealthItem
[]
Check
(
AgentConfig
cfg
,
KbService
kb
,
IReadOnlyList
<
DiagnosticRoot
>
roots
,
Support
.
CenterResolution
center
=
null
)
{
var
list
=
new
List
<
HealthItem
>();
try
...
...
@@ -53,11 +55,43 @@ namespace Tools.MyAgent
list
.
Add
(
CheckSemantic
(
cfg
));
list
.
Add
(
CheckDiagnosticRoots
(
roots
));
list
.
Add
(
CheckTelemetrySelf
());
list
.
Add
(
CheckConfigCenter
(
center
));
}
catch
{
}
return
list
.
ToArray
();
}
/// <summary>配置中心项(P2):模式 / 来源 / 角色 / KB 探测结论 / 缺项清单。</summary>
private
static
HealthItem
CheckConfigCenter
(
Support
.
CenterResolution
center
)
{
try
{
if
(
center
==
null
||
center
.
Mode
!=
Support
.
AgentRunMode
.
Distribution
)
return
Item
(
"config_center"
,
"配置中心"
,
"ok"
,
"自定义模式(本地配置)"
);
var
parts
=
new
List
<
string
>
{
"来源="
+
(
center
.
Source
==
"snapshot"
?
"本地快照"
:
center
.
Source
==
"db"
?
"中心库"
:
"缺省"
),
"角色="
+
(
center
.
IsAdmin
?
"管理员"
:
"普通用户"
),
};
if
(
center
.
KbProbe
!=
null
)
{
string
kb
=
"KB="
+
center
.
KbProbe
.
Conclusion
+
"("
+
center
.
KbProbe
.
Resolved
+
(
center
.
KbProbe
.
IsEmpty
?
",空库"
:
""
)
+
")"
;
parts
.
Add
(
kb
);
}
string
detail
=
string
.
Join
(
";"
,
parts
.
ToArray
());
if
(
center
.
MissingItems
!=
null
&&
center
.
MissingItems
.
Count
>
0
)
return
Item
(
"config_center"
,
"配置中心"
,
"fail"
,
detail
+
";缺项:"
+
string
.
Join
(
"、"
,
center
.
MissingItems
.
ToArray
())
+
"(请联系管理员)"
);
if
(
center
.
Source
==
"none"
)
return
Item
(
"config_center"
,
"配置中心"
,
"fail"
,
detail
+
"(中心库不可达且无本地快照)"
);
if
(
center
.
Source
==
"snapshot"
)
return
Item
(
"config_center"
,
"配置中心"
,
"warn"
,
detail
+
"(中心库不可达,使用本地快照)"
);
return
Item
(
"config_center"
,
"配置中心"
,
"ok"
,
detail
);
}
catch
(
Exception
ex
)
{
return
Item
(
"config_center"
,
"配置中心"
,
"warn"
,
ex
.
Message
);
}
}
/// <summary>模型服务连通性项(由调用方用 <c>TestConnectionAsync</c> 结果构造)。</summary>
public
static
HealthItem
ModelService
(
bool
ok
,
string
message
)
{
...
...
Tools.Agent/Support/AgentKbProbe.cs
0 → 100644
View file @
41a89411
using
System
;
using
System.IO
;
namespace
Tools.MyAgent.Support
{
/// <summary>知识库根目录探测结论(config_center_plan.md v2.3)。</summary>
public
sealed
class
KbProbeResult
{
/// <summary>原始模板(空 = 中心未下发 KB 配置)。</summary>
public
string
Template
{
get
;
set
;
}
/// <summary>最终采用的绝对路径({CLIENT_ROOT} 已展开;UNC 共享路径原样)。</summary>
public
string
Resolved
{
get
;
set
;
}
/// <summary>目录是否存在。</summary>
public
bool
Exists
{
get
;
set
;
}
/// <summary>存在但为空目录(允许,首次分发场景;/diag 呈现)。</summary>
public
bool
IsEmpty
{
get
;
set
;
}
/// <summary>结论:default_ok(未配置→宿主程序根目录) / ok / fallback_ok(已回退 BaseDirectory\Agent) / unavailable。</summary>
public
string
Conclusion
{
get
;
set
;
}
/// <summary>是否可用(Exists 且非 unavailable 类结论)。</summary>
public
bool
Usable
{
get
{
return
Exists
;
}
}
}
/// <summary>
/// 默认知识库探测器(config_center_plan.md v2.3/v2.4)。
/// <para>规则:模板空(中心未下发)→ 默认 <c>{CLIENT_ROOT}</c>(宿主程序根目录,静默兜底,不提示);
/// 已配置但解析后不存在 → 回退 <c>BaseDirectory\Agent</c>;仍不存在 → unavailable(提示"目录不可用")。
/// v2.4:占位符仅本地文件夹生效——UNC 共享模板不含占位符、原样解析。</para>
/// <para>红线:本类所有 public 方法体首句必须是 try —— 任何异常不上抛、不阻塞启动。</para>
/// </summary>
public
static
class
AgentKbProbe
{
/// <summary>未配置模板时 KB 解析(v2.3):默认 {CLIENT_ROOT} = 宿主程序根目录。</summary>
public
static
KbProbeResult
Probe
(
string
template
,
string
baseDirectory
)
{
try
{
string
root
=
AgentCenterClient
.
NormalizeRoot
(
baseDirectory
);
var
result
=
new
KbProbeResult
{
Template
=
template
??
""
};
string
requested
=
string
.
IsNullOrWhiteSpace
(
template
)
?
root
:
AgentCenterClient
.
ExpandClientRoot
(
template
,
root
);
result
.
Resolved
=
requested
;
if
(
Directory
.
Exists
(
requested
))
{
result
.
Exists
=
true
;
result
.
IsEmpty
=
new
DirectoryInfo
(
requested
).
GetFileSystemInfos
().
Length
==
0
;
result
.
Conclusion
=
string
.
IsNullOrWhiteSpace
(
template
)
?
"default_ok"
:
"ok"
;
return
result
;
}
// 已配置但不存在 → 回退 BaseDirectory\Agent(方案 §1.3)
string
fallback
=
Path
.
Combine
(
root
,
"Agent"
);
if
(
Directory
.
Exists
(
fallback
))
{
result
.
Exists
=
true
;
result
.
IsEmpty
=
new
DirectoryInfo
(
fallback
).
GetFileSystemInfos
().
Length
==
0
;
result
.
Resolved
=
fallback
;
result
.
Conclusion
=
"fallback_ok"
;
return
result
;
}
result
.
Conclusion
=
"unavailable"
;
return
result
;
}
catch
{
return
new
KbProbeResult
{
Template
=
template
??
""
,
Resolved
=
AgentCenterClient
.
NormalizeRoot
(
baseDirectory
),
Conclusion
=
"unavailable"
,
};
}
}
}
}
Tools.Agent/Tools.Agent.csproj
View file @
41a89411
...
...
@@ -174,7 +174,9 @@
<Compile
Include=
"ShareHelper.cs"
/>
<Compile
Include=
"Support\AgentLogger.cs"
/>
<Compile
Include=
"Support\AgentTelemetry.cs"
/>
<Compile
Include=
"Support\AgentCenterClient.cs"
/>
<Compile
Include=
"Support\AgentHealth.cs"
/>
<Compile
Include=
"Support\AgentKbProbe.cs"
/>
<Compile
Include=
"Support\TelemetryStore.cs"
/>
<Compile
Include=
"Support\AgentUtils.cs"
/>
<Compile
Include=
"Support\BackupHelper.cs"
/>
...
...
@@ -230,6 +232,7 @@
</ItemGroup>
<ItemGroup>
<None
Include=
"build\Ftt.Tools.Agent.targets"
/>
<None
Include=
"Defaults\Agent\config\center.sample.json"
/>
<None
Include=
"Ftt.Tools.Agent.nuspec"
/>
<None
Include=
"README.md"
/>
<None
Include=
"THIRD-PARTY-NOTICES.txt"
/>
...
...
Tools.Agent/wMyAgent.cs
View file @
41a89411
...
...
@@ -160,12 +160,26 @@ namespace Tools.MyAgent
/// <summary>创建可嵌入的智能体窗体,并允许宿主指定独立诊断根目录。</summary>
public
wMyAgent
(
string
agentRoot
,
string
currentUser
,
AgentProductContext
productContext
,
IReadOnlyList
<
DiagnosticRoot
>
diagnosticRoots
)
:
this
(
agentRoot
,
currentUser
,
productContext
,
diagnosticRoots
,
null
)
{
}
/// <summary>
/// 创建可嵌入的智能体窗体(v2.2 完整形态):center 为宿主(AgentRuntime)已拉取的配置中心解析结果;
/// 传 null(独立实例化)时按 center.json 是否存在自动判定模式并自行拉取。
/// </summary>
public
wMyAgent
(
string
agentRoot
,
string
currentUser
,
AgentProductContext
productContext
,
IReadOnlyList
<
DiagnosticRoot
>
diagnosticRoots
,
Support
.
CenterResolution
center
)
{
_productContext
=
ProductContextValidator
.
CreateSnapshot
(
productContext
);
_agentRoot
=
string
.
IsNullOrEmpty
(
agentRoot
)
?
Path
.
Combine
(
AppDomain
.
CurrentDomain
.
BaseDirectory
,
"Agent"
)
?
Support
.
AgentCenterClient
.
AgentRootPath
(
AppDomain
.
CurrentDomain
.
BaseDirectory
)
:
Path
.
GetFullPath
(
agentRoot
);
_currentUser
=
string
.
IsNullOrEmpty
(
currentUser
)
?
"访客"
:
currentUser
;
// 配置中心(v2.2):优先用宿主传入的解析结果;否则自行判定 + 拉取(内部三级降级,任何异常不上抛)
_center
=
center
??
Support
.
AgentCenterClient
.
Fetch
(
AppDomain
.
CurrentDomain
.
BaseDirectory
,
null
,
null
,
_currentUser
,
_productContext
!=
null
?
_productContext
.
ProductLineId
:
null
);
InitializeComponent
();
CreateChatControl
();
CreateCommandAutocomplete
();
...
...
@@ -174,6 +188,7 @@ namespace Tools.MyAgent
ApplyConfig
();
RunStartupHealthCheck
();
// HealthCheckOnStart=true 时后台自检落 health.json(P1-4:原为死配置)
WireEvents
();
lblSettings
.
Visible
=
CanOpenSettings
;
// 分发模式普通用户隐藏设置入口(v2.2)
RefreshChat
();
UpdateStatusIndicator
();
}
...
...
@@ -342,6 +357,15 @@ namespace Tools.MyAgent
private
static
(
string
Name
,
string
BaseUrl
,
string
Model
)[]
ProviderPresets
=>
Forms
.
AgentSettingsForm
.
ProviderPresets
;
/// <summary>默认知识库路径:程序输出目录下的 Agent 文件夹(未设置知识库时生效)</summary>
/// <summary>配置中心解析结果(config_center_plan.md v2.2);Custom 模式仅承载模式与角色。保存成功后可被刷新(非只读)。</summary>
private
Support
.
CenterResolution
_center
;
/// <summary>当前是否分发模式(配置中心生效)。</summary>
private
bool
IsDistribution
=>
_center
!=
null
&&
_center
.
Mode
==
Support
.
AgentRunMode
.
Distribution
;
/// <summary>当前用户是否可打开设置:自定义模式恒可(现状);分发模式仅管理员。</summary>
private
bool
CanOpenSettings
=>
!
IsDistribution
||
(
_center
!=
null
&&
_center
.
IsAdmin
);
private
string
DefaultKbRoot
=>
_agentRoot
;
/// <summary>产品上下文真正固定的搜索子目录;旧宿主把根目录作为范围时视为未固定。</summary>
...
...
@@ -505,6 +529,22 @@ namespace Tools.MyAgent
{
// 统一使用默认 Agent 目录下 config 文件夹的 AgentConfig.json(新位置)
_config
=
LoadAgentConfig
(
out
bool
migrated
)
??
new
AgentConfig
();
// 配置中心(v2.2):分发模式下本地退出中心管辖区——先清空再由中心覆盖;
// 宿主注入(下方 productContext 块)随后仍最高优先级。自定义模式零改动。
if
(
IsDistribution
)
{
_config
.
Provider
=
""
;
_config
.
BaseUrl
=
""
;
_config
.
Model
=
""
;
_config
.
ApiKey
=
""
;
_config
.
RemoteKbEntries
=
new
List
<
string
>();
_config
.
KnowledgeBasePath
=
""
;
_config
.
SearchScope
=
""
;
_config
.
KbMode
=
""
;
_config
.
ShareUser
=
""
;
_config
.
SharePassword
=
""
;
_center
.
ApplyTo
(
_config
);
// 字段级"非空才覆盖";KB 缺省时中心已解析为 {CLIENT_ROOT}
}
if
(
_productContext
!=
null
)
{
_config
.
ProductLineId
=
_productContext
.
ProductLineId
;
...
...
@@ -548,7 +588,7 @@ namespace Tools.MyAgent
var
roots
=
_engine
!=
null
?
_engine
.
DiagnosticRoots
:
null
;
System
.
Threading
.
Tasks
.
Task
.
Run
(()
=>
{
try
{
AgentHealth
.
Save
(
AgentHealth
.
Check
(
cfg
,
kb
,
roots
));
}
try
{
AgentHealth
.
Save
(
AgentHealth
.
Check
(
cfg
,
kb
,
roots
,
_center
));
}
catch
{
}
});
}
...
...
@@ -719,7 +759,9 @@ namespace Tools.MyAgent
$"知识库:
{
kbInfo
}
。输入问题即可检索回答;输入 / 呼出命令菜单。"
,
};
if
(
string
.
IsNullOrEmpty
(
_config
.
ApiKey
))
lines
.
Add
(
"API Key 未配置(点击「设置」填入后可开始回答)。"
);
lines
.
Add
(
IsDistribution
?
"API Key 未配置,请联系管理员!"
// 分发模式:无设置入口(v2.0)
:
"API Key 未配置(点击「设置」填入后可开始回答)。"
);
return
string
.
Join
(
"\n"
,
lines
);
}
...
...
@@ -1117,6 +1159,15 @@ namespace Tools.MyAgent
SendMessage
();
}
private
List
<
string
>
GetMissingEffectiveConfig
()
{
var
missing
=
new
List
<
string
>();
if
(
string
.
IsNullOrWhiteSpace
(
_config
?.
ApiKey
))
missing
.
Add
(
"ApiKey"
);
if
(
string
.
IsNullOrWhiteSpace
(
_config
?.
BaseUrl
))
missing
.
Add
(
"BaseUrl"
);
if
(
string
.
IsNullOrWhiteSpace
(
_config
?.
Model
))
missing
.
Add
(
"Model"
);
return
missing
;
}
private
async
void
SendMessage
()
{
string
text
=
txtInput
.
Text
.
Trim
();
...
...
@@ -1135,6 +1186,17 @@ namespace Tools.MyAgent
return
;
}
if
(
IsDistribution
)
{
// 以当前会话实际生效的配置判断;管理员刚保存但中心同步失败时,
// 本机配置仍应立即可用,不能继续使用启动时的旧中心缺项快照拦截。
var
missing
=
GetMissingEffectiveConfig
();
if
(
missing
.
Count
>
0
)
{
AddSystemMessage
(
"以下配置缺失,请联系管理员:"
+
string
.
Join
(
"、"
,
missing
.
ToArray
()));
return
;
}
}
if
(
string
.
IsNullOrEmpty
(
_config
.
ApiKey
))
{
AddSystemMessage
(
"请先在「设置」中配置 DeepSeek API Key。"
);
...
...
@@ -2888,7 +2950,15 @@ namespace Tools.MyAgent
private
void
ShowSettingsPopup
()
{
using
(
var
frm
=
new
Forms
.
AgentSettingsForm
(
_config
,
DefaultKbRoot
))
// 分发模式(v2.2):普通用户无设置入口(按钮已隐藏,此处兜底 /settings 命令)
if
(!
CanOpenSettings
)
{
AddSystemMessage
(
"当前为分发模式,配置由管理员统一管理;如需修改请联系管理员。"
);
return
;
}
// 分发模式且未配置数据库连接(2026-10-22):管理员打开设置自动跳到「对话上报」页引导配置
bool
focusRunDb
=
IsDistribution
&&
string
.
IsNullOrEmpty
(
_config
.
RunDbConnection
);
using
(
var
frm
=
new
Forms
.
AgentSettingsForm
(
_config
,
DefaultKbRoot
,
focusRunDb
))
{
if
(
frm
.
ShowDialog
(
this
)
!=
DialogResult
.
OK
)
return
;
...
...
@@ -2896,9 +2966,35 @@ namespace Tools.MyAgent
if
(!
string
.
IsNullOrEmpty
(
_config
.
SearchScope
)
&&
!
IsPathUnder
(
_config
.
KnowledgeBasePath
,
_config
.
SearchScope
))
_config
.
SearchScope
=
""
;
// 分发模式(P1):中心管辖区字段写透数据库(缺表自建、本地路径反折 {CLIENT_ROOT},UNC 共享原样),其余终端下次启动生效
if
(
IsDistribution
)
{
// 先落本地配置,使首次设置的 RunDbConnection 可作为中心库连接引导;
// 中心同步成功后仍会在下方再次保存最终生效配置。
SaveConfig
();
string
err
=
Support
.
AgentCenterClient
.
SaveFromConfig
(
AppDomain
.
CurrentDomain
.
BaseDirectory
,
_config
);
if
(
err
!=
null
)
{
// 中心写失败:本机已生效但不下发;不刷新快照(避免把失败态固化)
AddSystemMessage
(
"❌ 配置已在本机生效,但同步配置中心失败:"
+
err
+
"(其他终端不会收到)"
);
}
else
{
var
fresh
=
Support
.
AgentCenterClient
.
Fetch
(
AppDomain
.
CurrentDomain
.
BaseDirectory
,
Support
.
AgentRunMode
.
Distribution
,
_center
.
IsAdmin
,
_currentUser
,
_productContext
!=
null
?
_productContext
.
ProductLineId
:
null
);
if
(
fresh
!=
null
)
{
_center
=
fresh
;
// 刷新解析结果(同时更新本地快照)
_center
.
ApplyTo
(
_config
);
}
AddSystemMessage
(
"✅ 配置已保存并同步到配置中心(其余终端下次启动生效)。"
);
}
}
ApplyKbRoot
(
_config
.
KnowledgeBasePath
);
ApplyConfig
();
SaveConfig
();
SaveConfig
();
// 本地持久化(分发模式下中心管辖区字段以中心库为准,本地仅承载非管辖区/兜底)
RefreshScopeCombo
();
RefreshChat
();
UpdateStatusIndicator
();
...
...
@@ -3119,7 +3215,7 @@ namespace Tools.MyAgent
private
async
void
RunDiagnostics
()
{
var
items
=
new
System
.
Collections
.
Generic
.
List
<
HealthItem
>(
AgentHealth
.
Check
(
_config
,
_kb
,
_engine
.
DiagnosticRoots
));
// 诊断根由引擎持有(窗体未另存字段)
AgentHealth
.
Check
(
_config
,
_kb
,
_engine
.
DiagnosticRoots
,
_center
));
// 诊断根由引擎持有(窗体未另存字段)
bool
ok
=
false
;
string
msg
=
""
;
try
...
...
Tools/wExtend.cs
View file @
41a89411
...
...
@@ -49,7 +49,6 @@ namespace Tools
this
.
CreateModule
(
Tools_imp
,
typeof
(
UI
.
wEmptyTable
),
"清空表"
,
"tr:输入主表、自动清空关联表的工具"
),
this
.
CreateModule
(
Tools_imp
,
typeof
(
UI
.
wExcelExportSetting
),
"Excel导出设置"
,
"tr:Excel导出配置图形化版本"
),
this
.
CreateModule
(
Tools_imp
,
typeof
(
UI
.
wLogViewer
),
"日志查看器"
,
"tr:可以方便的查看系统日志"
),
this
.
CreateModule
(
Tools_imp
,
typeof
(
Tools
.
MyAgent
.
Forms
.
AgentRunLogForm
),
"Agent日志"
,
"tr:查看AI对话上报记录(按用户提问统计分析)"
),
this
.
CreateModule
(
Tools_imp
,
typeof
(
UI
.
wTextFormatter
),
"格式化工具"
,
"tr:SQL和JSON文本格式化工具"
),
this
.
CreateModule
(
Tools_imp
,
typeof
(
UI
.
wTextDiff
),
"文本对比工具"
,
"tr:行级文本差异对比,支持左右/上下布局"
),
this
.
CreateModule
(
Tools_imp
,
typeof
(
DotNetVersionChecker
.
FrmVersion
),
"系统检测"
,
"lxj:检测操作系统运行环境"
),
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment