Commit 954b2011 authored by 段廷瑞's avatar 段廷瑞

1、【编号:25240】Agent终端:添加鞋业和飞天兔等内置知识

2、【编号:25241】工具平台:减少数据库依赖
parent 0fff0a7b
......@@ -69,3 +69,4 @@ bin
!/AppHome/Agent/dialect/
!/AppHome/Agent/industry/
/AppHome/Agent/config/AgentConfig.json
/cc
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{A11393B8-85B1-46CA-A5E7-1741AE8067FA}</ProjectGuid>
<OutputType>Exe</OutputType>
<RootNamespace>AgentCompatibilityTest</RootNamespace>
<AssemblyName>AgentCompatibilityTest</AssemblyName>
<TargetFrameworkVersion Condition=" '$(TargetFrameworkVersion)' == '' ">v4.5</TargetFrameworkVersion>
<TargetFrameworkProfile />
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<LangVersion>7.3</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget><DebugSymbols>true</DebugSymbols><DebugType>full</DebugType><Optimize>false</Optimize>
<OutputPath Condition=" '$(OutputPath)' == '' ">bin\Debug\</OutputPath><DefineConstants>DEBUG;TRACE</DefineConstants><WarningLevel>4</WarningLevel><Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget><DebugType>pdbonly</DebugType><Optimize>true</Optimize>
<OutputPath Condition=" '$(OutputPath)' == '' ">bin\Release\</OutputPath><DefineConstants>TRACE</DefineConstants><WarningLevel>4</WarningLevel><Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<ItemGroup>
<Reference Include="ICSharpCode.SharpZipLib"><HintPath>..\packages\SharpZipLib.1.3.2\lib\net45\ICSharpCode.SharpZipLib.dll</HintPath></Reference>
<Reference Include="MySql.Data"><HintPath>..\packages\MySql.Data.6.9.12\lib\net45\MySql.Data.dll</HintPath></Reference>
<Reference Include="Newtonsoft.Json"><HintPath>..\packages\Newtonsoft.Json.12.0.1\lib\net45\Newtonsoft.Json.dll</HintPath></Reference>
<Reference Include="Npgsql"><HintPath>..\packages\Npgsql.3.2.7\lib\net45\Npgsql.dll</HintPath></Reference>
<Reference Include="NPOI"><HintPath>..\packages\NPOI.2.5.1\lib\net45\NPOI.dll</HintPath></Reference>
<Reference Include="NPOI.OOXML"><HintPath>..\packages\NPOI.2.5.1\lib\net45\NPOI.OOXML.dll</HintPath></Reference>
<Reference Include="NPOI.OpenXml4Net"><HintPath>..\packages\NPOI.2.5.1\lib\net45\NPOI.OpenXml4Net.dll</HintPath></Reference>
<Reference Include="NPOI.OpenXmlFormats"><HintPath>..\packages\NPOI.2.5.1\lib\net45\NPOI.OpenXmlFormats.dll</HintPath></Reference>
<Reference Include="UglyToad.PdfPig"><HintPath>..\packages\PdfPig.0.0.9\lib\net45\UglyToad.PdfPig.dll</HintPath></Reference>
<Reference Include="System" /><Reference Include="System.Core" /><Reference Include="System.Data" /><Reference Include="System.Drawing" />
<Reference Include="System.IO.Compression" /><Reference Include="System.IO.Compression.FileSystem" /><Reference Include="System.Net.Http" />
<Reference Include="System.Security" /><Reference Include="System.Web" /><Reference Include="System.Windows.Forms" /><Reference Include="System.Xml" /><Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.SQLite"><HintPath>..\packages\Stub.System.Data.SQLite.Core.NetFramework.1.0.115.5\lib\net45\System.Data.SQLite.dll</HintPath></Reference>
<Reference Include="System.ValueTuple"><HintPath>..\packages\System.ValueTuple.4.5.0\lib\portable-net40+sl4+win8+wp8\System.ValueTuple.dll</HintPath></Reference>
</ItemGroup>
<ItemGroup><Compile Include="Program.cs" /></ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Tools.Agent\Tools.Agent.csproj"><Project>{D2D9C734-0C5B-4B06-9E4E-1AE6C4DB3F24}</Project><Name>Tools.Agent</Name><GlobalPropertiesToRemove>TargetFrameworkVersion;OutputPath</GlobalPropertiesToRemove></ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="..\packages\Stub.System.Data.SQLite.Core.NetFramework.1.0.115.5\build\net45\Stub.System.Data.SQLite.Core.NetFramework.targets" Condition="Exists('..\packages\Stub.System.Data.SQLite.Core.NetFramework.1.0.115.5\build\net45\Stub.System.Data.SQLite.Core.NetFramework.targets')" />
</Project>
using System;
using System.IO;
using Tools.MyAgent;
namespace AgentCompatibilityTest
{
public static class Program
{
[STAThread]
public static int Main()
{
string root = Path.Combine(Path.GetTempPath(), "tools_agent_smoke_" + Guid.NewGuid().ToString("N"));
try
{
var runtime = new AgentRuntime(new AgentRuntimeOptions { DataRoot = root, CurrentUser = "smoke" });
var kb = runtime.CreateKnowledgeBase();
using (var engine = runtime.CreateEngine(kb))
using (var form = runtime.CreateChatForm())
{
if (!string.Equals(engine.CurrentUser, "smoke", StringComparison.Ordinal)) return 2;
if (!string.Equals(form.CurrentUser, "smoke", StringComparison.Ordinal)) return 3;
}
kb.ShutdownIndex();
Console.WriteLine("Tools.Agent compatibility smoke passed: " + Environment.Version);
return 0;
}
finally
{
try { Directory.Delete(root, true); } catch { }
}
}
}
}
......@@ -8,7 +8,7 @@
<OutputType>Exe</OutputType>
<RootNamespace>AgentTest</RootNamespace>
<AssemblyName>AgentTest</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>false</Deterministic>
......@@ -45,6 +45,12 @@
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
<Reference Include="System.ValueTuple">
<HintPath>..\packages\System.ValueTuple.4.5.0\lib\portable-net40+sl4+win8+wp8\System.ValueTuple.dll</HintPath>
</Reference>
<Reference Include="ICSharpCode.SharpZipLib, Version=1.3.2.10, Culture=neutral, PublicKeyToken=1b03e6acf1164f73, processorArchitecture=MSIL">
<HintPath>..\packages\SharpZipLib.1.3.2\lib\net45\ICSharpCode.SharpZipLib.dll</HintPath>
</Reference>
<Reference Include="NPOI, Version=2.5.1.0, Culture=neutral, PublicKeyToken=0df73ec7942b34e1, processorArchitecture=MSIL">
<HintPath>..\packages\NPOI.2.5.1\lib\net45\NPOI.dll</HintPath>
</Reference>
......@@ -57,6 +63,9 @@
<Reference Include="NPOI.OpenXmlFormats, Version=2.5.1.0, Culture=neutral, PublicKeyToken=0df73ec7942b34e1, processorArchitecture=MSIL">
<HintPath>..\packages\NPOI.2.5.1\lib\net45\NPOI.OpenXmlFormats.dll</HintPath>
</Reference>
<Reference Include="UglyToad.PdfPig">
<HintPath>..\packages\PdfPig.0.0.9\lib\net45\UglyToad.PdfPig.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Program.cs" />
......@@ -71,11 +80,12 @@
<Compile Include="ToolPermissionTests.cs" />
<Compile Include="TextEncodingTests.cs" />
<Compile Include="MarkdownRendererTests.cs" />
<Compile Include="ProductContextTests.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Tools\Tools.csproj">
<Project>{C5AB3CE9-C46A-467D-96E4-D80DD3BBB239}</Project>
<Name>Tools</Name>
<ProjectReference Include="..\Tools.Agent\Tools.Agent.csproj">
<Project>{D2D9C734-0C5B-4B06-9E4E-1AE6C4DB3F24}</Project>
<Name>Tools.Agent</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
......
......@@ -166,7 +166,7 @@ namespace AgentTest
public void DGrep_HitsAndMiss()
{
var tool = new DialectGrepTool();
string hit = tool.Execute(JObject.Parse("{'query':'库存不对'}"), MakeCtx(_ds));
string hit = tool.Execute(JObject.Parse("{'query':'库存不对','glob':'dialect/erp'}"), MakeCtx(_ds));
Assert.True(hit.Contains("dialect/erp/winforms.md"), "应命中方言文件");
string miss = tool.Execute(JObject.Parse("{'query':'不存在的关键字xyz'}"), MakeCtx(_ds));
......@@ -186,10 +186,9 @@ namespace AgentTest
{
var tool = new DialectListTool();
string result = tool.Execute(JObject.Parse("{}"), MakeCtx(_ds));
Assert.True(result.Contains("dialect/"), "应列出方言根");
Assert.True(result.Contains("industry/"), "应列出行业根");
Assert.True(result.Contains("erp"), "应含 erp 目录");
Assert.True(result.Contains("鞋业"), "应含行业目录");
Assert.True(result.Contains("troubleshoot-core"), "未绑定时默认应列出 troubleshoot-core");
Assert.False(result.Contains("erp"), "未绑定时默认不应列出具体产品目录");
Assert.False(result.Contains("鞋业"), "未绑定时默认不应列出行业目录");
}
[Test]
......
......@@ -137,5 +137,48 @@ namespace AgentTest
Assert.True(cancelled, "已取消的令牌应使枚举立即终止");
}
}
[Test]
public void SetScope_OutsideRoot_FallsBackToRoot()
{
_kb.SetScope(_outside);
Assert.Equal(KbService.NormPath(_root), _kb.SearchRoot,
"搜索范围越界时应回退到知识库根目录,不能访问外部目录");
}
[Test]
public void ConfiguredExcludedPaths_AreHiddenFromListAndWrites()
{
string secretDir = Path.Combine(_root, "secrets");
Directory.CreateDirectory(secretDir);
File.WriteAllText(Path.Combine(secretDir, "password.txt"), "secret");
File.WriteAllText(Path.Combine(_root, "kb-config.json"),
"{\"ExcludedPrefixes\":[\"secrets\"]}");
string listing = _kb.List(".", false);
Assert.NotContains(listing, "secrets", "排除目录不应出现在目录列表");
Assert.Throws<InvalidOperationException>(() => _kb.ResolveSearch("secrets"),
"排除目录不应被读取");
Assert.Contains(_kb.WriteToKb("secrets/new.md", "data", false), "错误",
"排除目录不应被写入");
}
[Test]
public void AllowedPrefixes_KeepAncestorVisibleButHideSiblings()
{
Directory.CreateDirectory(Path.Combine(_root, "allowed", "nested"));
Directory.CreateDirectory(Path.Combine(_root, "other"));
File.WriteAllText(Path.Combine(_root, "allowed", "nested", "ok.md"), "ok");
File.WriteAllText(Path.Combine(_root, "other", "no.md"), "no");
File.WriteAllText(Path.Combine(_root, "kb-config.json"),
"{\"AllowedPrefixes\":[\"allowed/nested\"]}");
string listing = _kb.List(".", false);
Assert.Contains(listing, "allowed", "允许目录的祖先应可浏览");
Assert.NotContains(listing, "other", "允许范围外的目录不应暴露");
Assert.Contains(_kb.List("allowed/nested", false), "ok.md", "允许范围内文件应可浏览");
}
}
}
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json.Linq;
using Tools.MyAgent;
using Tools.MyAgent.Agent;
using Tools.MyAgent.Kb;
namespace AgentTest
{
/// <summary>产品上下文、PDM 方言路由和提示词边界测试。</summary>
public sealed class ProductContextTests : TestRunner
{
private string _root;
public override void SetUp()
{
_root = Path.Combine(Path.GetTempPath(), "agenttest_product_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(Path.Combine(_root, "dialect", "pdm"));
Directory.CreateDirectory(Path.Combine(_root, "dialect", "erp"));
Directory.CreateDirectory(Path.Combine(_root, "dialect", "mes"));
Directory.CreateDirectory(Path.Combine(_root, "dialect", "troubleshoot-core"));
Directory.CreateDirectory(Path.Combine(_root, "industry", "鞋业"));
File.WriteAllText(Path.Combine(_root, "dialect", "INDEX.md"),
"# 产品排查方言\n" +
"## PDM\n" +
"- pdm/cases.md:PDM 案例(触发词:PDM、材料)\n" +
"## ERP\n" +
"- erp/winforms.md:ERP 桌面端(触发词:窗体)\n" +
"## MES\n" +
"- mes/mes.md:MES 排查(触发词:排程)\n" +
"## Troubleshoot\n" +
"- troubleshoot-core/five-steps.md:排查五步法(触发词:报错、根因)\n");
File.WriteAllText(Path.Combine(_root, "dialect", "pdm", "cases.md"), "PDM 材料汇总案例\n");
File.WriteAllText(Path.Combine(_root, "dialect", "erp", "winforms.md"), "ERP 窗体案例\n");
File.WriteAllText(Path.Combine(_root, "dialect", "mes", "mes.md"), "MES 排程案例\n");
File.WriteAllText(Path.Combine(_root, "dialect", "troubleshoot-core", "five-steps.md"), "复现证据 -> 链路定位 -> 单一假设\n");
File.WriteAllText(Path.Combine(_root, "industry", "鞋业", "INDEX.md"),
"# 鞋业(别名:制鞋)\n" +
"## 术语\n" +
"- 术语.md:鞋业术语(触发词:材料)\n");
File.WriteAllText(Path.Combine(_root, "industry", "鞋业", "术语.md"), "鞋业材料术语\n");
}
public override void TearDown()
{
try { Directory.Delete(_root, true); } catch { }
}
[Test]
public void ResolvePdm_ReturnsTrustedProfileAndClone()
{
var context = ProductContextCatalog.Resolve("pdm");
Assert.Equal("pdm", context.ProductLineId);
Assert.Equal("PDM_LN", context.ProjectId);
Assert.Equal("dialect/pdm", context.ProductDialectScope);
Assert.True(context.RelatedDialectScopes.Contains("dialect/erp"), "PDM 应允许按需使用 ERP 桌面端链路");
var aliases = context.Aliases;
context.Aliases[0] = "changed";
var another = ProductContextCatalog.Resolve("pdm");
Assert.NotContains(string.Join(",", another.Aliases), "changed", "注册表画像不能被调用方修改");
Assert.NotNull(aliases);
}
[Test]
public void Resolve_UnknownProduct_Throws()
{
Assert.Throws<ArgumentException>(() => ProductContextCatalog.Resolve("pdm_ln"), "项目标识不能作为产品线");
Assert.NotNull(ProductContextCatalog.Resolve("erp"));
Assert.NotNull(ProductContextCatalog.Resolve("mes"));
}
[Test]
public void Snapshot_RejectsScopeOutsideKnowledgeBase()
{
var context = ProductContextCatalog.Resolve("pdm");
context.KnowledgeBaseRoot = Path.Combine(_root, "kb");
context.KnowledgeBaseScope = Path.Combine(_root, "other");
Assert.Throws<ArgumentException>(() => ProductContextValidator.CreateSnapshot(context),
"知识库搜索范围必须位于知识库根目录内");
}
[Test]
public void Runtime_SnapshotsProductContext()
{
var original = ProductContextCatalog.Resolve("pdm");
var runtime = new AgentRuntime(new AgentRuntimeOptions { ProductContext = original });
original.ProjectId = "changed";
Assert.Equal("PDM_LN", runtime.ProductContext.ProjectId, "运行时不应读取外部上下文的后续修改");
Assert.Equal("pdm", runtime.ProductLineId);
}
[Test]
public void KnowledgeBase_ConfigFiltersProductFiles()
{
string kbRoot = Path.Combine(_root, "kb");
Directory.CreateDirectory(Path.Combine(kbRoot, "modules"));
Directory.CreateDirectory(Path.Combine(kbRoot, "secrets"));
File.WriteAllText(Path.Combine(kbRoot, "modules", "Pdm.cs"), "PDM source");
File.WriteAllText(Path.Combine(kbRoot, "secrets", "password.txt"), "secret");
File.WriteAllText(Path.Combine(kbRoot, "kb-config.json"),
"{\"ProductLineId\":\"pdm\",\"AllowedPrefixes\":[\"modules\"],\"ExcludedPrefixes\":[\"secrets\"]}");
var kb = new KbService();
kb.SetProductContext(ProductContextCatalog.Resolve("pdm"));
kb.SetRoot(kbRoot);
Assert.Equal(1, kb.CountFiles(), "项目知识库应按允许前缀过滤文件");
Assert.Contains(kb.Find("Pdm", 10), "modules/Pdm.cs", "允许目录内文件应可检索");
Assert.NotContains(kb.Find("password", 10), "secrets/password.txt", "排除目录内文件不得检索");
}
[Test]
public void History_PersistsProductMetadata()
{
string history = Path.Combine(_root, "history");
Directory.CreateDirectory(history);
var messages = new List<ChatMessage>
{
new ChatMessage { Role = "user", Content = "PDM 问题" },
new ChatMessage { Role = "assistant", Content = "答复" },
};
var context = ProductContextCatalog.Resolve("pdm");
HistoryManager.SaveSession(history, messages, "pdm_test", context);
var saved = HistoryManager.LoadSession(Path.Combine(history, "pdm_test.json"));
Assert.Equal("pdm", saved.ProductLineId);
Assert.Equal("PDM_LN", saved.ProjectId);
}
[Test]
public void PromptBuilder_UnboundUsesTroubleshootCore()
{
var prompts = new PromptBuilder { Dialect = new DialectService(_root) };
string message = prompts.BuildSystemMessage(new KbService());
Assert.Contains(message, "排查五步法", "未绑定时应注入 troubleshoot-core 规则");
Assert.Contains(message, "未绑定具体产品线", "应明确当前处于智能排查模式");
Assert.NotContains(message, "PDM 案例", "未绑定时不应默认注入具体产品案例");
}
[Test]
public void DialectGrep_UnboundDefaultsToTroubleshootCore()
{
var tool = new Tools.MyAgent.Agent.Tools.DialectGrepTool();
var ctx = new Tools.MyAgent.Agent.Tools.ToolContext(new KbService(), null)
{
Dialect = new DialectService(_root),
};
string defaultResult = tool.Execute(JObject.Parse("{'query':'MES 排程'}"), ctx);
string explicitResult = tool.Execute(JObject.Parse("{'query':'MES 排程','glob':'dialect/mes'}"), ctx);
Assert.Contains(defaultResult, "未找到", "未绑定默认检索应先使用 troubleshoot-core");
Assert.Contains(explicitResult, "MES 排程", "识别产品后显式范围仍应可检索");
}
[Test]
public void PromptBuilder_PdmContextDoesNotExposeMesByDefault()
{
var context = ProductContextCatalog.Resolve("pdm");
var dialect = new DialectService(_root);
var prompts = new PromptBuilder { Dialect = dialect, ProductContext = context };
string message = prompts.BuildSystemMessage(new KbService());
Assert.Contains(message, "PDM_LN", "系统提示词应锁定项目");
Assert.Contains(message, "pdm", "系统提示词应包含稳定产品标识");
Assert.Contains(message, "PDM 优先资料", "应声明 PDM 资料优先");
Assert.NotContains(message, "MES 排查", "PDM 默认摘要不应暴露 MES 主题");
}
[Test]
public void Dialect_PdmSummaryAndEnumerationAreScoped()
{
var context = ProductContextCatalog.Resolve("pdm");
var dialect = new DialectService(_root);
string summary = dialect.BuildSummary(context);
var defaultFiles = dialect.EnumerateFiles(context).ToList();
var erpFiles = dialect.EnumerateFiles(context, "dialect/erp").ToList();
Assert.Contains(summary, "产品排查方言", "应保留方言摘要头");
Assert.Contains(summary, "PDM", "摘要应包含 PDM 路由");
Assert.NotContains(summary, "ERP 桌面端", "摘要不应包含未选中的 ERP 主题正文");
Assert.False(defaultFiles.Any(f => f.StartsWith("dialect/erp/", StringComparison.OrdinalIgnoreCase)),
"默认枚举不应包含 ERP");
Assert.False(defaultFiles.Any(f => f.StartsWith("dialect/mes/", StringComparison.OrdinalIgnoreCase)),
"默认枚举不应包含 MES");
Assert.True(defaultFiles.Any(f => f == "dialect/pdm/cases.md"), "默认枚举应包含 PDM 文件");
Assert.True(erpFiles.Any(f => f == "dialect/erp/winforms.md"), "显式请求允许的相关 ERP 范围应可访问");
}
}
}
......@@ -43,8 +43,8 @@ namespace AgentTest
{
failed++;
var inner = ex.InnerException ?? ex;
failures.Add($"{type.Name}.{method.Name}: {inner.Message}");
Console.WriteLine($" ✗ {type.Name}.{method.Name} → {inner.Message}");
failures.Add($"{type.Name}.{method.Name}: {inner.GetType().Name}: {inner.Message}");
Console.WriteLine($" ✗ {type.Name}.{method.Name} → {inner.GetType().Name}: {inner.Message}");
}
catch (Exception ex)
{
......
......@@ -3,7 +3,7 @@ using System.IO;
using System.Linq;
using System.Text;
using Tools.MyAgent.Kb;
using Tools.MyUtils;
using Tools.MyAgent.Support;
namespace AgentTest
{
......@@ -405,6 +405,49 @@ namespace AgentTest
finally { try { File.Delete(f); } catch { } }
}
// ── PDF 文本层提取(net45 PdfPig)──
[Test]
public void ReadPdf_TextLayer_ExtractsText()
{
string file = TempFile("text.pdf");
try
{
WriteMinimalPdf(file, "Hello Agent");
string text = KbService.ReadPdf(file);
Assert.Contains(text, "Hello Agent", "PDF 文本层应在 net45 下正常提取");
}
finally { try { File.Delete(file); } catch { } }
}
private static void WriteMinimalPdf(string path, string text)
{
var objects = new[]
{
"<< /Type /Catalog /Pages 2 0 R >>",
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 200] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>",
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
null,
};
string stream = "BT /F1 24 Tf 40 100 Td (" + text.Replace("(", "\\(").Replace(")", "\\)") + ") Tj ET";
objects[4] = "<< /Length " + stream.Length + " >>\nstream\n" + stream + "\nendstream";
var pdf = new StringBuilder("%PDF-1.4\n");
var offsets = new System.Collections.Generic.List<int>();
for (int i = 0; i < objects.Length; i++)
{
offsets.Add(Encoding.ASCII.GetByteCount(pdf.ToString()));
pdf.Append(i + 1).Append(" 0 obj\n").Append(objects[i]).Append("\nendobj\n");
}
int xref = Encoding.ASCII.GetByteCount(pdf.ToString());
pdf.Append("xref\n0 ").Append(objects.Length + 1).Append("\n0000000000 65535 f \n");
foreach (int offset in offsets) pdf.Append(offset.ToString("D10")).Append(" 00000 n \n");
pdf.Append("trailer\n<< /Size ").Append(objects.Length + 1).Append(" /Root 1 0 R >>\nstartxref\n")
.Append(xref).Append("\n%%EOF\n");
File.WriteAllText(path, pdf.ToString(), Encoding.ASCII);
}
// ── Read 文件大小闸门(> 30MB 拒绝整读,稀疏文件不占真实磁盘)──
[Test]
......
No preview for this file type
No preview for this file type
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -27,8 +27,8 @@
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="BouncyCastle.Crypto" publicKeyToken="0e99375e54769942" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-1.8.6.0" newVersion="1.8.6.0" />
<assemblyIdentity name="System.Resources.Extensions" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.0.0" newVersion="4.0.0.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
......@@ -37,59 +37,5 @@
<bindingRedirect oldVersion="0.0.0.0-4.0.2.0" newVersion="4.0.2.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Buffers" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.4.0" newVersion="4.0.4.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Numerics.Vectors" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.1.5.0" newVersion="4.1.5.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-6.0.1.0" newVersion="6.0.1.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.Bcl.AsyncInterfaces" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Text.Json" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Threading.Channels" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-7.0.0.0" newVersion="7.0.0.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Threading.Tasks.Extensions" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.5.4.0" newVersion="4.5.4.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.ValueTuple" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.5.0.0" newVersion="4.5.0.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.Bcl.HashCode" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
\ No newline at end of file
This diff is collapsed.
......@@ -5,6 +5,8 @@ VisualStudioVersion = 17.1.32319.34
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UpdateExecuter", "UpdateExecuter\UpdateExecuter.csproj", "{5ABCD19A-7A28-479C-BCAE-0AD7F1A4FEB0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tools.Agent", "Tools.Agent\Tools.Agent.csproj", "{D2D9C734-0C5B-4B06-9E4E-1AE6C4DB3F24}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tools", "Tools\Tools.csproj", "{C5AB3CE9-C46A-467D-96E4-D80DD3BBB239}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ftt.PatchPack", "Ftt.PatchPack\Ftt.PatchPack.csproj", "{42E384C1-CC13-494D-90E2-33C399E13F07}"
......@@ -23,6 +25,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsSearch", "WindowsSea
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AgentTest", "AgentTest\AgentTest.csproj", "{B4E2F1A0-5C31-4A6B-9D1E-0A7F3C8B2D44}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AgentCompatibilityTest", "AgentCompatibilityTest\AgentCompatibilityTest.csproj", "{A11393B8-85B1-46CA-A5E7-1741AE8067FA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
......@@ -45,6 +49,18 @@ Global
{5ABCD19A-7A28-479C-BCAE-0AD7F1A4FEB0}.Release|x64.Build.0 = Release|x64
{5ABCD19A-7A28-479C-BCAE-0AD7F1A4FEB0}.Release|x86.ActiveCfg = Release|x86
{5ABCD19A-7A28-479C-BCAE-0AD7F1A4FEB0}.Release|x86.Build.0 = Release|x86
{D2D9C734-0C5B-4B06-9E4E-1AE6C4DB3F24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D2D9C734-0C5B-4B06-9E4E-1AE6C4DB3F24}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D2D9C734-0C5B-4B06-9E4E-1AE6C4DB3F24}.Debug|x64.ActiveCfg = Debug|Any CPU
{D2D9C734-0C5B-4B06-9E4E-1AE6C4DB3F24}.Debug|x64.Build.0 = Debug|Any CPU
{D2D9C734-0C5B-4B06-9E4E-1AE6C4DB3F24}.Debug|x86.ActiveCfg = Debug|Any CPU
{D2D9C734-0C5B-4B06-9E4E-1AE6C4DB3F24}.Debug|x86.Build.0 = Debug|Any CPU
{D2D9C734-0C5B-4B06-9E4E-1AE6C4DB3F24}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D2D9C734-0C5B-4B06-9E4E-1AE6C4DB3F24}.Release|Any CPU.Build.0 = Release|Any CPU
{D2D9C734-0C5B-4B06-9E4E-1AE6C4DB3F24}.Release|x64.ActiveCfg = Release|Any CPU
{D2D9C734-0C5B-4B06-9E4E-1AE6C4DB3F24}.Release|x86.ActiveCfg = Release|Any CPU
{D2D9C734-0C5B-4B06-9E4E-1AE6C4DB3F24}.Release|x64.Build.0 = Release|Any CPU
{D2D9C734-0C5B-4B06-9E4E-1AE6C4DB3F24}.Release|x86.Build.0 = Release|Any CPU
{C5AB3CE9-C46A-467D-96E4-D80DD3BBB239}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C5AB3CE9-C46A-467D-96E4-D80DD3BBB239}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C5AB3CE9-C46A-467D-96E4-D80DD3BBB239}.Debug|x64.ActiveCfg = Debug|Any CPU
......@@ -153,6 +169,14 @@ Global
{B4E2F1A0-5C31-4A6B-9D1E-0A7F3C8B2D44}.Release|x64.Build.0 = Release|Any CPU
{B4E2F1A0-5C31-4A6B-9D1E-0A7F3C8B2D44}.Release|x86.ActiveCfg = Release|Any CPU
{B4E2F1A0-5C31-4A6B-9D1E-0A7F3C8B2D44}.Release|x86.Build.0 = Release|Any CPU
{A11393B8-85B1-46CA-A5E7-1741AE8067FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A11393B8-85B1-46CA-A5E7-1741AE8067FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A11393B8-85B1-46CA-A5E7-1741AE8067FA}.Debug|x64.ActiveCfg = Debug|Any CPU
{A11393B8-85B1-46CA-A5E7-1741AE8067FA}.Debug|x86.ActiveCfg = Debug|Any CPU
{A11393B8-85B1-46CA-A5E7-1741AE8067FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A11393B8-85B1-46CA-A5E7-1741AE8067FA}.Release|Any CPU.Build.0 = Release|Any CPU
{A11393B8-85B1-46CA-A5E7-1741AE8067FA}.Release|x64.ActiveCfg = Release|Any CPU
{A11393B8-85B1-46CA-A5E7-1741AE8067FA}.Release|x86.ActiveCfg = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
......
......@@ -15,7 +15,8 @@ Ftt_Common 是一个基于 .NET Framework 4.x 的 WinForms 多文档界面(MDI
| 项目 | 说明 | 技术栈 |
|------|------|--------|
| **Tools** | 主程序,MDI 工具箱容器 | WinForms, .NET 4.8 |
| **Tools** | 主程序,MDI 工具箱容器,仅负责承载智能体 UI | WinForms, .NET 4.8 |
| **Tools.Agent** | 可独立分发的智能体完整实现 | WinForms, OpenAI API, SQLite, .NET 4.5+ |
| **UpdateExecuter** | SQL 脚本远程执行工具 | WinForms, .NET 4.8 |
| **WebKit** | 内嵌 Chromium 浏览器 | CefSharp 116, .NET 4.7.2 |
| **DIYForm** | 可视化表单设计器 | SunnyUI, .NET 4.7.2 |
......@@ -60,7 +61,7 @@ Ftt_Common 是一个基于 .NET Framework 4.x 的 WinForms 多文档界面(MDI
- 员工考核 — 绩效考核记录
### AI 集成
- **Claude Chat** — 内嵌 AI 对话助手,基于 Claude CLI 的本地交互
- **Tools.Agent** — 独立智能体类库,支持 OpenAI 兼容模型、本地/共享知识库、工具调用、会话管理和完整 WinForms 界面;Tools 仅引用并承载该项目。
## 技术栈
......
......@@ -24,6 +24,7 @@ namespace Tools.MyAgent.Agent
private readonly KbService _kb;
private readonly PromptBuilder _prompts = new PromptBuilder();
private readonly DialectService _dialect; // 内置方言/行业知识(程序目录 Agent/dialect + Agent/industry)
private AgentProductContext _productContext;
private CancellationTokenSource _cts;
/// <summary>上下文 token 预算(由配置注入;默认 32000)</summary>
......@@ -109,6 +110,12 @@ namespace Tools.MyAgent.Agent
/// <summary>工具级权限(P3):工具名 → 允许角色列表;空 = 全部可用(由 AgentConfig.ToolRoleAllow 注入)</summary>
private Dictionary<string, string> _toolRoleAllow = new Dictionary<string, string>();
/// <summary>当前宿主绑定的产品上下文;返回副本,避免运行期间被外部修改。</summary>
public AgentProductContext ProductContext => _productContext?.Clone();
/// <summary>当前产品线稳定标识;未绑定时为空。</summary>
public string ProductLineId => _productContext?.ProductLineId ?? "";
/// <summary>当前登录用户名(即角色,P3 工具权限判定用;由宿主注入)</summary>
public string CurrentUser { get; set; } = "";
......@@ -134,13 +141,34 @@ namespace Tools.MyAgent.Agent
/// 并在最终回答前拼接保留(修复:此前被回答正文全量替换而丢失)</summary>
private string _contextNotice = "";
public AgentEngine(KbService kb)
public AgentEngine(KbService kb) : this(kb, null, null)
{
}
public AgentEngine(KbService kb, string agentRoot) : this(kb, agentRoot, null)
{
_kb = kb;
}
/// <summary>创建引擎,并允许宿主指定内置提示词、方言、行业知识和产品上下文所在的 Agent 根目录。</summary>
public AgentEngine(KbService kb, string agentRoot, AgentProductContext productContext)
{
_kb = kb ?? throw new ArgumentNullException(nameof(kb));
_backupClient = new OpenAiCompatibleClient();
_failover = new FailoverPolicy(_client, _backupClient);
// 内置方言/行业知识:随程序发布(AppDomain.BaseDirectory + Agent),目录缺失时自动降级为空
_dialect = new DialectService(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Agent"));
string root = string.IsNullOrEmpty(agentRoot)
? Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Agent")
: Path.GetFullPath(agentRoot);
_dialect = new DialectService(root);
SetProductContext(productContext);
}
/// <summary>切换产品上下文。运行中的回答禁止切换,调用方应同时创建新会话。</summary>
public void SetProductContext(AgentProductContext context)
{
if (IsRunning) throw new InvalidOperationException("回答进行中不能切换产品上下文。请先取消当前回答。");
_productContext = ProductContextValidator.CreateSnapshot(context);
_prompts.ProductContext = _productContext?.Clone();
_kb.SetProductContext(_productContext);
_prompts.Dialect = _dialect;
}
......@@ -323,6 +351,7 @@ namespace Tools.MyAgent.Agent
{
try { _cts?.Cancel(); } catch { }
_client.Cancel();
_backupClient?.Cancel();
}
/// <summary>本轮补全是否被输出上限截断(finish_reason=length,回答残缺不全的直接标志)</summary>
......@@ -924,7 +953,9 @@ namespace Tools.MyAgent.Agent
{
CancelToken = ct,
DbConnections = DbConnections,
CurrentUser = CurrentUser,
Dialect = _dialect,
ProductContext = _productContext?.Clone(),
};
string result = tool.Execute(args, ctx);
sw.Stop();
......
......@@ -21,6 +21,9 @@ namespace Tools.MyAgent.Agent
/// <summary>内置方言/行业知识服务(产品排查方言 + 鞋业行业知识;由引擎注入,可空)</summary>
public DialectService Dialect { get; set; }
/// <summary>宿主绑定的产品上下文;由运行时注入,不接受聊天内容修改</summary>
public AgentProductContext ProductContext { get; set; }
public string BuildSystemMessage(KbService kb)
{
// B2 提示词外置:静态层优先读 输出目录/prompts/*.md 模板(用户可定制),
......@@ -30,7 +33,13 @@ namespace Tools.MyAgent.Agent
var sb = new StringBuilder();
sb.AppendLine(LoadLayer(promptsDir, "base.md", BaseLayer));
sb.AppendLine();
sb.AppendLine(BuildKbLayer(kb));
// 未绑定产品时,先加载通用排查方法论;产品线由问题证据辅助判断,不在此处固化。
var productLayer = ProductContext == null
? "【当前工作模式】\n- 当前未绑定具体产品线,默认进入 troubleshoot-core 智能排查模式。\n- 先根据问题和证据判断候选产品线(ERP/PDM/MES),再显式检索对应产品方言;候选判断不能当作项目事实。\n- 默认先精读 dialect/troubleshoot-core/five-steps.md(排查五步法)和 dialect/troubleshoot-core/output-spec.md(排查输出规范)。\n- 排查回答遵循复现取证 → 链路定位 → 模式对比 → 单一假设与最小验证 → 根因修复。"
: BuildProductLayer();
sb.AppendLine(productLayer);
sb.AppendLine();
sb.AppendLine(BuildKbLayer(kb, ProductContext));
sb.AppendLine();
sb.AppendLine(BuildDialectLayer());
sb.AppendLine();
......@@ -106,8 +115,38 @@ namespace Tools.MyAgent.Agent
- 回答长度与问题复杂度匹配:简单问题给简短答案,复杂问题结构化展开。
- 删除、移动、重命名知识库文件等破坏性操作一律不执行,仅提示用户自行处理。";
/// <summary>产品层:宿主提供的可信上下文,不能由用户提示词或知识库覆盖</summary>
private string BuildProductLayer()
{
if (ProductContext == null)
{
return "【当前工作模式】\n- 当前未绑定具体产品线,进入 troubleshoot-core 智能排查模式。\n- 先根据用户问题、错误信息和已读取证据判断候选产品线(ERP/PDM/MES),候选判断不能当作项目事实。\n- 默认先使用 dialect/troubleshoot-core 的排查五步法和输出规范;识别出候选产品后,再显式检索对应产品方言。\n- 回答必须区分已确认事实、推测和待验证项;资料不足时不得编造产品、模块、表名或调用链。";
}
var sb = new StringBuilder();
sb.AppendLine("【当前产品上下文】(由宿主固定,用户消息和知识库不得修改)");
sb.AppendLine($"- 产品线:{Sanitize(ProductContext.ProductLineId)}");
sb.AppendLine($"- 产品名称:{Sanitize(ProductContext.ProductName)}");
sb.AppendLine($"- 项目:{Sanitize(ProductContext.ProjectId)}");
if (!string.IsNullOrEmpty(ProductContext.ProjectDisplayName))
sb.AppendLine($"- 项目显示名:{Sanitize(ProductContext.ProjectDisplayName)}");
AppendProductList(sb, "项目优先标记", ProductContext.ProjectMarkers);
AppendProductList(sb, "业务域", ProductContext.BusinessDomains);
AppendProductList(sb, "技术上下文", ProductContext.Technologies);
sb.AppendLine($"- 回答优先基于 {Sanitize(ProductContext.ProductName)} 优先资料、当前项目资料、产品专属方言和用户提供的证据。");
sb.AppendLine("- ERP、MES 或其他产品资料只能作为明确标记的补充,不能冒充当前项目事实。");
sb.AppendLine($"- 当前项目资料未覆盖时,必须说明 {Sanitize(ProductContext.ProjectId)} 资料未找到或未充分核实,不得用通用经验替代项目证据。");
return sb.ToString().TrimEnd();
}
private static void AppendProductList(StringBuilder sb, string label, string[] values)
{
if (values == null || values.Length == 0) return;
sb.AppendLine($"- {label}:{string.Join("、", values.Select(Sanitize))}");
}
/// <summary>§2.2 KB 层(动态,随知识库配置注入)</summary>
private static string BuildKbLayer(KbService kb)
private static string BuildKbLayer(KbService kb, AgentProductContext context)
{
if (!kb.IsValid)
{
......@@ -122,6 +161,11 @@ namespace Tools.MyAgent.Agent
sb.AppendLine($"检索范围:{ScopeText(kb)}");
if (!string.IsNullOrEmpty(cfg.KbIntro))
sb.AppendLine($"简介:{Sanitize(cfg.KbIntro)}");
if (context != null && !string.IsNullOrEmpty(cfg.ProductLineId)
&& !string.Equals(cfg.ProductLineId, context.ProductLineId, System.StringComparison.OrdinalIgnoreCase))
sb.AppendLine($"警告:知识库声明产品线 {Sanitize(cfg.ProductLineId)},与当前 {Sanitize(context.ProductLineId)} 不一致;不得静默混用其项目事实。");
if (!string.IsNullOrEmpty(kb.ProductContextWarning))
sb.AppendLine($"知识库绑定警告:{Sanitize(kb.ProductContextWarning)}");
// 文件规模/格式走后台统计缓存,避免每次发送都在 UI 线程全量递归
kb.EnsureStats();
string formats = !string.IsNullOrEmpty(cfg.Formats) ? cfg.Formats : kb.FormatsSummary;
......@@ -154,6 +198,8 @@ namespace Tools.MyAgent.Agent
sb.AppendLine($"- 常用检索入口:{Sanitize(cfg.FaqHints)}");
if (!string.IsNullOrEmpty(cfg.PriorityHints))
sb.AppendLine($"- 重点资料位置:{Sanitize(cfg.PriorityHints)}");
if (cfg.SourcePriority != null && cfg.SourcePriority.Count > 0)
sb.AppendLine($"- 资料优先级:{Sanitize(string.Join(" > ", cfg.SourcePriority))}");
sb.AppendLine("- 检索/读取仅限当前搜索范围目录内;返回的路径以知识库根目录为基准表示(限定目录在知识库内时)。");
sb.AppendLine("- 检索时优先使用本知识库的业务术语作为关键词。");
sb.AppendLine("- 默认生成的文档用 kb_write 写入默认输出目录的 kb_write 子文件夹(程序目录下 Agent/output/kb_write/);表格/报表用 doc_export(Agent/output/doc_export/);用户明确要求存入知识库时用 kb_write_kb 写入本知识库。");
......@@ -169,7 +215,7 @@ namespace Tools.MyAgent.Agent
if (Dialect == null || !Dialect.IsAvailable) return "";
try
{
return Dialect.BuildSummary();
return Dialect.BuildSummary(ProductContext);
}
catch
{
......
using Newtonsoft.Json.Linq;
using System;
using System.Data;
using System.Text;
namespace Tools.MyAgent.Agent.Tools
{
/// <summary>db_schema:只读查看表、列、索引、主键和视图定义。</summary>
internal sealed class DbSchemaTool : ITool
{
public string Name => "db_schema";
public string Description =>
"查看数据库结构(表清单/表列/索引/主键/视图),用于了解表结构辅助写查询与排查(只读)。" +
"排查查询报错或不确定字段名时先用本工具确认结构。";
public string ParametersJson => @"{'type':'object',
'properties':{
'type':{'type':'string','enum':['sqlserver','mysql','postgres','sqlite'],'description':'数据库类型'},
'connectionId':{'type':'string','description':" + ToolJson.Q("预存连接 ID(设置中配置);指定后无需再传 connection") + @"},
'connection':{'type':'string','description':" + ToolJson.Q("连接字符串;sqlite 可直接给 .db 文件路径") + @"},
'object':{'type':'string','enum':['table','view'],'description':'对象类型,默认 table'},
'name':{'type':'string','description':" + ToolJson.Q("对象名(表/视图);缺省列出全部该类对象") + @"}
},
'required':['type']}";
public bool RequiresKb => false;
public bool IsDefault => false;
public string Describe(JObject args) => $"查看表结构:{args["name"]?.Value<string>() ?? "(全部)"}";
public string Execute(JObject args, ToolContext ctx)
{
string type = (args["type"]?.Value<string>() ?? "").Trim().ToLowerInvariant();
string obj = (args["object"]?.Value<string>() ?? "table").Trim().ToLowerInvariant();
string name = (args["name"]?.Value<string>() ?? "").Trim();
string connection = null;
string error = null;
if (!DbQueryTool.ResolveConnection(args, ctx, ref connection, ref error)) return error;
try
{
switch (type)
{
case "sqlite": return SqliteSchema(connection, obj, name);
case "sqlserver": return InformationSchema(type, connection, obj, name, true);
case "mysql":
case "postgres": return InformationSchema(type, connection, obj, name, false);
default: return $"错误:不支持的数据库类型:{type}";
}
}
catch (Exception ex) { return $"错误:{DbQueryTool.MaskSensitive(ex.Message)}"; }
}
private static string InformationSchema(string type, string connection, string obj, string name, bool sqlServer)
{
using (var conn = DbQueryTool.OpenConnection(type, connection))
using (var cmd = conn.CreateCommand())
{
conn.Open();
string objectType = obj == "view" ? "VIEW" : "BASE TABLE";
string sql = "SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, IS_NULLABLE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_TYPE='" + objectType + "'";
if (!string.IsNullOrEmpty(name)) sql += " AND TABLE_NAME='" + name.Replace("'", "''") + "'";
sql += " ORDER BY TABLE_NAME, ORDINAL_POSITION";
cmd.CommandText = sql;
var sb = new StringBuilder();
string last = null;
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
string table = Convert.ToString(reader.GetValue(0));
if (!string.Equals(last, table, StringComparison.OrdinalIgnoreCase))
{
last = table;
sb.AppendLine($"== {table} ==");
}
sb.AppendLine($" {reader.GetValue(1)} {reader.GetValue(2)} {reader.GetValue(3)}");
}
}
if (sb.Length == 0) return $"未找到 {(obj == "view" ? "视图" : "表")}:{name}";
if (sqlServer && obj != "view" && !string.IsNullOrEmpty(name))
AppendSqlServerIndexes(conn, sb, name);
return sb.ToString();
}
}
private static void AppendSqlServerIndexes(IDbConnection conn, StringBuilder sb, string name)
{
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = "SELECT i.name, i.is_primary_key FROM sys.indexes i WHERE i.object_id = OBJECT_ID('" + name.Replace("'", "''") + "') AND i.name IS NOT NULL ORDER BY i.index_id";
using (var reader = cmd.ExecuteReader())
{
bool wrote = false;
while (reader.Read())
{
if (!wrote) { sb.AppendLine("索引:"); wrote = true; }
sb.AppendLine($" {reader.GetValue(0)}{(Convert.ToInt32(reader.GetValue(1)) == 1 ? "(主键)" : "")}");
}
}
}
}
private static string SqliteSchema(string connection, string obj, string name)
{
using (var conn = DbQueryTool.OpenConnection("sqlite", connection))
{
conn.Open();
string typeFilter = obj == "view" ? "view" : "table";
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = $"SELECT name, sql FROM sqlite_master WHERE type='{typeFilter}' AND name NOT LIKE 'sqlite_%' ORDER BY name";
var sb = new StringBuilder();
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
string table = reader.GetString(0);
if (!string.IsNullOrEmpty(name) && !table.Equals(name, StringComparison.OrdinalIgnoreCase)) continue;
sb.AppendLine($"== {table} ==");
if (obj == "view") { sb.AppendLine(reader.IsDBNull(1) ? "" : reader.GetString(1)); continue; }
AppendSqliteColumns(conn, sb, table);
AppendSqliteIndexes(conn, sb, table);
}
}
return sb.Length == 0 ? $"未找到 {(obj == "view" ? "视图" : "表")}:{name}" : sb.ToString();
}
}
}
private static void AppendSqliteColumns(IDbConnection conn, StringBuilder sb, string table)
{
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = $"PRAGMA table_info(\"{table.Replace("\"", "\"\"")}\")";
using (var reader = cmd.ExecuteReader())
while (reader.Read()) sb.AppendLine($" {reader.GetString(1)} {reader.GetString(2)}{(reader.GetInt32(5) == 1 ? " PK" : "")}");
}
}
private static void AppendSqliteIndexes(IDbConnection conn, StringBuilder sb, string table)
{
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = $"PRAGMA index_list(\"{table.Replace("\"", "\"\"")}\")";
using (var reader = cmd.ExecuteReader())
while (reader.Read()) sb.AppendLine($" 索引:{reader.GetString(1)}({(reader.GetInt32(2) == 1 ? "唯一" : "普通")})");
}
}
}
}
......@@ -38,6 +38,18 @@ namespace Tools.MyAgent.Agent.Tools
{
if (path == "." || path == "dialect" || path == "industry")
{
if (ctx.ProductContext == null && (path == "." || path == "dialect"))
{
var defaultFiles = dialect.EnumerateFiles(null).ToList();
return "未绑定产品时默认使用 troubleshoot-core:\n- " + string.Join("\n- ", defaultFiles);
}
if (ctx.ProductContext != null)
{
var files = dialect.EnumerateFiles(ctx.ProductContext,
path == "." ? null : path).ToList();
return "当前产品允许的内置知识文件:\n- " + string.Join("\n- ", files);
}
var sb = new StringBuilder();
if (path == "." || path == "dialect")
sb.AppendLine("dialect/(产品排查方言):").Append(ListDir(dialect.DialectRoot, recursive, "dialect"));
......@@ -45,6 +57,8 @@ namespace Tools.MyAgent.Agent.Tools
sb.AppendLine("industry/(行业知识):").Append(ListDir(dialect.IndustryRoot, recursive, "industry"));
return sb.ToString();
}
if (ctx.ProductContext != null && !dialect.IsScopeAllowed(ctx.ProductContext, path, true))
return $"错误:路径不在当前产品允许范围内:{path}。";
string abs = dialect.ResolveSafe(path);
if (abs == null || !Directory.Exists(abs))
return $"错误:路径不存在或非法:{path}(仅允许 dialect/ 与 industry/ 前缀,拒绝越界)。";
......@@ -109,7 +123,7 @@ namespace Tools.MyAgent.Agent.Tools
var regex = new System.Text.RegularExpressions.Regex(query,
System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Compiled);
var hits = new List<string>();
foreach (var rel in dialect.EnumerateFiles())
foreach (var rel in dialect.EnumerateFiles(ctx.ProductContext, glob))
{
if (glob.Length > 0 && !rel.StartsWith(glob + "/", StringComparison.OrdinalIgnoreCase)
&& !rel.Equals(glob, StringComparison.OrdinalIgnoreCase)) continue;
......@@ -169,11 +183,23 @@ namespace Tools.MyAgent.Agent.Tools
if (limit < 1) limit = 1;
if (limit > 300) limit = 300;
if (ctx.ProductContext != null && !dialect.IsScopeAllowed(ctx.ProductContext, path, true))
return $"错误:路径不在当前产品允许范围内:{path}。";
string abs = dialect.ResolveSafe(path);
if (abs == null || !File.Exists(abs))
{
// #32 风格参数补全:给出候选文件名,便于模型二次确认
var cands = dialect.EnumerateFiles()
// #32 风格参数补全:给出当前产品主范围内的候选文件名,便于模型二次确认
var candidateFiles = dialect.EnumerateFiles(ctx.ProductContext).ToList();
if (path.StartsWith("dialect/", StringComparison.OrdinalIgnoreCase)
|| path.StartsWith("industry/", StringComparison.OrdinalIgnoreCase))
{
int slash = path.LastIndexOf('/');
string requestedScope = slash > 0 ? path.Substring(0, slash) : path;
var scopedFiles = dialect.EnumerateFiles(ctx.ProductContext, requestedScope).ToList();
if (scopedFiles.Count > 0) candidateFiles = scopedFiles;
}
var cands = candidateFiles
.Where(f => f.IndexOf(path, StringComparison.OrdinalIgnoreCase) >= 0
|| path.IndexOf(Path.GetFileNameWithoutExtension(f), StringComparison.OrdinalIgnoreCase) >= 0)
.Take(8).ToList();
......
......@@ -23,12 +23,18 @@ namespace Tools.MyAgent.Agent.Tools
/// 工具应在长循环(全盘扫描等)中检查并尽早终止。</summary>
public CancellationToken CancelToken { get; set; }
/// <summary>当前宿主用户/角色,用于审计和工具权限;不依赖宿主登录静态类</summary>
public string CurrentUser { get; set; } = "";
/// <summary>预存数据库连接串(ID → 连接串,已解密明文;来自 AgentConfig.DbConnections)</summary>
public IReadOnlyDictionary<string, string> DbConnections { get; set; }
/// <summary>内置方言/行业知识服务(d_* 工具沙箱;程序目录 Agent/dialect + Agent/industry)</summary>
public DialectService Dialect { get; set; }
/// <summary>宿主绑定的产品上下文,用于限制 d_* 工具检索范围</summary>
public AgentProductContext ProductContext { get; set; }
public ToolContext(KbService kb, Action<string> onFileProduced)
{
Kb = kb;
......@@ -46,7 +52,7 @@ namespace Tools.MyAgent.Agent.Tools
/// <summary>
/// 工具级权限(P3):工具名 → 允许角色列表;空规则 = 全部可用;值空串/"*" = 不限制。
/// <para>纯逻辑、无状态,独立可测;当前角色取登录用户名(LoginInfo.UserName),
/// <para>纯逻辑、无状态,独立可测;当前角色由宿主通过 AgentRuntimeOptions/AgentEngine.CurrentUser 注入,
/// 规则匹配不分大小写,支持逗号/分号/顿号分隔多个角色。</para>
/// </summary>
public static class ToolPermission
......
using System;
using System.IO;
using Tools.MyAgent.Agent;
using Tools.MyAgent.Kb;
namespace Tools.MyAgent
{
/// <summary>智能体运行时选项。所有路径均由宿主控制,不依赖 Tools 项目。</summary>
public sealed class AgentRuntimeOptions
{
/// <summary>配置、历史、索引、日志和输出的根目录。空值使用应用目录下的 Agent。</summary>
public string DataRoot { get; set; }
/// <summary>当前用户或角色,用于聊天展示、工具权限和数据库审计。</summary>
public string CurrentUser { get; set; } = "访客";
/// <summary>宿主固定的产品上下文;空值表示未绑定产品线。</summary>
public AgentProductContext ProductContext { get; set; }
}
/// <summary>其他 .NET Framework 项目引用 Tools.Agent 后使用的稳定入口。</summary>
public sealed class AgentRuntime
{
private readonly AgentRuntimeOptions _options;
private readonly AgentProductContext _productContext;
/// <summary>当前宿主绑定的产品上下文;返回副本,避免运行期间被外部对象修改。</summary>
public AgentProductContext ProductContext => _productContext?.Clone();
/// <summary>当前产品线稳定标识;未绑定时为空。</summary>
public string ProductLineId => _productContext?.ProductLineId ?? "";
public AgentRuntime(AgentRuntimeOptions options = null)
{
_options = options ?? new AgentRuntimeOptions();
_productContext = ProductContextValidator.CreateSnapshot(_options.ProductContext);
}
/// <summary>实际数据根目录。</summary>
public string DataRoot => string.IsNullOrEmpty(_options.DataRoot)
? Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Agent")
: Path.GetFullPath(_options.DataRoot);
/// <summary>创建完整聊天窗体,可作为顶级窗体显示或嵌入宿主容器。</summary>
public wMyAgent CreateChatForm()
{
EnsureDefaultResources();
return new wMyAgent(DataRoot, _options.CurrentUser, ProductContext);
}
/// <summary>创建已配置输出目录的知识库服务,供无 UI 宿主自行设置知识库根目录。</summary>
public KbService CreateKnowledgeBase()
{
EnsureDefaultResources();
Directory.CreateDirectory(DataRoot);
var kb = new KbService();
kb.SetOutputRoot(DataRoot);
kb.SetProductContext(ProductContext);
if (ProductContext != null && !string.IsNullOrEmpty(ProductContext.KnowledgeBaseRoot))
{
kb.SetRoot(ProductContext.KnowledgeBaseRoot);
kb.SetScope(ProductContext.KnowledgeBaseScope);
}
return kb;
}
/// <summary>创建无 UI Agent 引擎。调用方负责配置、订阅事件和释放。</summary>
public AgentEngine CreateEngine(KbService knowledgeBase)
{
if (knowledgeBase == null) throw new ArgumentNullException(nameof(knowledgeBase));
EnsureDefaultResources();
return new AgentEngine(knowledgeBase, DataRoot, ProductContext) { CurrentUser = _options.CurrentUser ?? "访客" };
}
private void EnsureDefaultResources()
{
Directory.CreateDirectory(DataRoot);
string defaults = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Agent");
if (string.Equals(Path.GetFullPath(defaults).TrimEnd('\\'), DataRoot.TrimEnd('\\'), StringComparison.OrdinalIgnoreCase)
|| !Directory.Exists(defaults)) return;
CopyMissing(Path.Combine(defaults, "prompts"), Path.Combine(DataRoot, "prompts"));
CopyMissing(Path.Combine(defaults, "dialect"), Path.Combine(DataRoot, "dialect"));
CopyMissing(Path.Combine(defaults, "industry"), Path.Combine(DataRoot, "industry"));
}
private static void CopyMissing(string source, string target)
{
if (!Directory.Exists(source)) return;
Directory.CreateDirectory(target);
foreach (string file in Directory.GetFiles(source))
{
string destination = Path.Combine(target, Path.GetFileName(file));
if (!File.Exists(destination)) File.Copy(file, destination, false);
}
foreach (string directory in Directory.GetDirectories(source))
CopyMissing(directory, Path.Combine(target, Path.GetFileName(directory)));
}
}
}
......@@ -14,6 +14,12 @@
/// <summary>【核心】提供商预设名(DeepSeek / 通义千问 / OpenAI / 本地 Ollama / 自定义)</summary>
public string Provider { get; set; } = "";
/// <summary>宿主绑定的产品线标识(由宿主注入,保存用于诊断,不作为权限边界)</summary>
public string ProductLineId { get; set; } = "";
/// <summary>宿主绑定的项目标识(由宿主注入,保存用于诊断)</summary>
public string ProjectId { get; set; } = "";
/// <summary>【核心】API Key(OpenAI 兼容服务)</summary>
public string ApiKey { get; set; } = "";
......
# 产品排查方言(内化版)
行业简介:Ftt 产品线排查方言——桌面端业务链路(窗口→业务类→SQL/SP→数据库)、审批引擎、SQL 规范与热点归因模式,供 SQL 排查与问题归因对齐产品事实。适用:ERP(U6_*、风行等)、PDM(PDM_LN、LNCenterApi)、MES(MES_*、mes_geshiwei)桌面端与 cloud Api 服务端。
## 01-排查方法论
- troubleshoot-core/five-steps.md:排查五步法:复现取证/链路定位/模式对比/单一假设/根因修复(触发词:排查、报错、数据不对、超时、闪退、定位问题、怎么查、根因、证据)
- troubleshoot-core/output-spec.md:排查输出规范:一句话根因+证据链+影响范围(触发词:输出规范、根因一句话、证据链、修复建议)
## 02-SQL 规范
- sql-core/sql-templates.md:四库通用查询模板(触发词:加列、索引、执行计划、分页、核对查询、幂等)
- sql-core/sql-style.md:SQL 格式规范(触发词:格式化、CRLF、别名、注释头)
- sql-core/sql-scripts.md:脚本生成规范(触发词:生成脚本、落盘、脚本命名)
## 03-ERP 排查链路
- erp/winforms.md:桌面端排查链路(触发词:库存不对、报表数据不对、单据状态、提交报错、程序异常、性能、约束、App.Run.Log)
- erp/verification-flow.md:审批流程引擎(触发词:审批不触发、审批流程、VF_FlowMap、在审、已审、审批节点)
- erp/sql-script-location.md:SQL 脚本存放规范(触发词:脚本存放、2999-01-01、GB2312、AddTable、存储过程命名)
- erp/special-modules.md:报表类模块 SQL 获取三途径(触发词:自定义报表、FrmRPTView、动态表结构、TableIdentic、报表列空)
- erp/grid-filter.md:网格过滤联动机制(触发词:过滤不联动、复选框过滤、PivotGrid、GridView)
- erp/parent-provider.md:父子单据联动(触发词:产生了任务、跟单、ParentProvider、A单据产生B任务)
- erp/aspnet.md:Api 服务端排查(触发词:Api、接口报错、ApiLog、LNCenterApi、FttData.Api、接口慢)
- erp/cases/hotspots.md:ERP 热点归因案例(触发词:数量变0、库存多扣、二次保存、缓存失效、提交报错、8114)
## 04-MES 排查方言
- mes/sql-templates-mes.md:MES 查询模板(触发词:MES、报工、检验、成型配套、排程、生产计划、尺码明细)
- mes/cases/hotspots.md:MES 热点归因案例(触发词:成型配套、合计行小于明细)
## 05-PDM 排查方言
- pdm/cases/hotspots.md:PDM 热点归因案例(触发词:PDM、材料汇总、报表慢、风险评估)
## 06-常见问题
> 本地沉淀区:排查确认的典型问题经维护者整理追加到本大类(内置目录只读,禁止智能体直接写入)。
# ASP.NET 排查(数据错误 / 程序异常 / 性能 / 约束 / SQL 日志)
> 适用:cloud(FttData.Api)、LNCenterApi。
> 项目事实(数据库连接、ModuleName 路由机制、App_Sys_Operation_Log 表结构)从知识库检索获取。
> 排查任一步因信息不足卡住 → 向用户索要关键信息(入参/日志文件),或建议补强(开启 SqlLog / 加完整异常日志)。
> 通用方法论见 dialect/troubleshoot-core(五步法 / 输出规范)。
## 取证工具
- 查库:`db_query`(SELECT-only)/ `db_schema`,需先在设置中预存连接(connectionId)。
- 无预置连接时:把 SQL 给用户在 SSMS 执行,取回结果再分析。
- 读代码:源码检索(知识库内代码分析文档)。
## ASP.NET 数据错误排查
```
1. 查操作日志:
SELECT TOP 10 * FROM App_Sys_Operation_Log
WHERE Action LIKE '%关键词%' ORDER BY OperateDate DESC
→ 获取 RequestParams(入参 JSON)+ ResponseParams(出参 JSON)
2. 定位源码 → Controller/{名称}Controller.cs → Action 方法
3. 找到 SP → Action 中 "Exec dbo.SP_xxx" 调用 → 提取 SP 名和参数
4. db_query 复现 → 用日志入参执行 SP → 比对结果 vs 日志出参
→ 一致但数据错 = SP 逻辑问题 / 不一致 = 参数绑定/序列化问题
```
## ASP.NET 程序异常排查
```
1. 查 App_Sys_Operation_Log WHERE ResponseMessage != '' → 获取错误
2. 分类:参数校验失败 / 500 内部错误 / 序列化失败 / 数据库连接失败
3. 卡住处理(ResponseMessage 为空 / ApiLog 缺失 / 现象模糊):
→ 向用户索要入参与 ApiLog 文件,建议开启 SqlLog / 加完整异常日志
```
## ASP.NET 性能问题排查
```
1. SELECT TOP 20 * FROM App_Sys_Operation_Log ORDER BY SpendTime DESC
2. 找到对应 SP → SSMS 执行计划 → 缺失索引/参数嗅探/锁等待/无分页
```
## 数据库约束异常排查(ASP.NET 侧)
```
1. 获取完整错误 → App_Sys_Operation_Log 的 ResponseMessage
2. 提取约束名和表名 → 找出违反约束的数据
3. 追踪数据来源 → 业务链路上溯到写入点
常见类型:违反唯一性约束(主键/唯一索引冲突)/ 值检查约束(CHECK)/ 外键约束
```
## ASP.NET SQL 日志排查(ApiLog)
```
API 请求日志位于:{程序目录}/ApiLog/versionXXX/Controller_yyyyMMdd.log
排查流程:
1. 确定时间范围和接口:
→ 让用户提供或检索 ApiLog 目录:ls ApiLog/versionPM/TaskController_20260701.log
→ 或用 grep "start:接口名" 搜索
2. 定位具体请求:
→ grep "traceId=xxx" ApiLog/versionXXX/*.log
→ 或 grep "user=用户名" ApiLog/versionXXX/*.log
→ 或 grep "-- start:Controller.Action" ApiLog/versionXXX/*.log
3. 分析请求链路:
→ 从 -- start 到 -- end 之间的所有 SQL 按执行时间排列
→ 每条 SQL 格式:SQL语句; -- 参数名=值 -- 耗时ms, 行数, 模块
4. 排查异常:
→ grep "失败" ApiLog/versionXXX/*.log
→ grep "错误:" ApiLog/versionXXX/*.log
5. 慢查询分析:
→ grep -E "\-\- [0-9]{4,}ms" ApiLog/versionXXX/*.log
```
日志格式:
```
-----------------------------------------------------
-- start:Controller.Action (request={...}) [时间] user=用户 ip=IP traceId=GUID
SELECT * FROM Table WHERE Col=@Param; -- @Param='Value' -- 45ms, 12 rows, Module
EXEC SP_Name @P1, @P2; -- @P1=1, @P2='text' -- 230ms, Module
-- end: Controller.Action 总耗时=Nms SQL数=N 成功/失败 [时间]
```
**配置**(`WebAppSettings.config`):
```xml
<add key="SqlLog:Enabled" value="true" /> <!-- 开关 -->
<add key="SqlLog:SlowQueryThresholdMs" value="0" /> <!-- 慢查询阈值,0=全部 -->
<add key="SqlLog:MaxSqlLength" value="4000" /> <!-- SQL截断长度 -->
<add key="SqlLog:RetentionDays" value="30" /> <!-- 保留天数 -->
```
**技术架构摘要**:
| 组件 | 作用 |
|------|------|
| `SqlTraceAttribute` | ActionFilter — 请求开始时创建缓冲区,结束时写文件 |
| `SqlTraceContext` | HttpContext.Items 中存储 TraceId + `List<string>` 缓冲区 |
| `SqlLogHelper.Log()` | 格式化 SQL 行并追加到缓冲区 |
| `SqlLogWriter` | ThreadPool 异步写入,按文件路径 lock 防交错 |
| `SqlLogContextWrapper` | 包装 `IDbContext`,拦截 `Sql()` 和 `MultiResultSql` |
| `SqlLogCommandWrapper` | 包装 `IDbCommand`,拦截所有 QueryMany/Execute 等方法 |
**文件位置**:`ApiLog/versionXXX/Controller_yyyyMMdd.log`,位于程序 bin 目录下,例如:
`FttData.Api\bin\ApiLog\versionPM\TaskController_20260701.log`
**排查技巧**:
- 请求块的 `-- start` 行包含完整请求参数(复杂对象以 JSON 序列化)
- 每个 SQL 行末尾 `-- @param=value` 显示实际参数值
- `traceId` 可在不同请求间唯一标识,关联同一请求的所有 SQL
- 文件按天滚动,过期自动清理(默认保留 30 天)
- 日志写入失败不影响主业务(静默忽略)
# ERP 产品线热点问题统计(归因模式库)
> 每次排查确认根因后由维护者追加;智能体遇到相似症状时优先比对本库模式,再按五步法取证。
> 通用桌面端排查链路(窗口→业务类→SQL/SP→数据库)见 dialect/erp/winforms.md。
> 最后更新:2026-08-24
## 采购单 / 辅料采购合同(frmEditPurchaseDoc / ucPurchaseShip / PurchaseShipMaterial.xml)
### 二次保存提示「采购数量大于未采数量(0)+可多采数量(0)」(已采统计自包含)
- **分类**:数据错误(校验口径)
- **根因**:`SelectByDocTree` 的 has 子查询未排除当前单据 → 首次保存后 AfterDataSave 重载把当前单自身计入 HasPurchaseCount → 未采=需求−已采(含自身)=0 → 二次保存 checkPurchaseCountInput 必拦
- **修复**:has 子查询加 `AND a.DocTreeID <> @DocTreeID`
- **归因模式**:**自包含统计**——校验口径的聚合查询把「当前正在处理的单据」计入了统计基数。排查时先核对统计 SQL 是否排除自身。
### 保存后提交,明细采购数量变 0 / 数量不守恒(155→4.3278)
- **分类**:数据错误
- **根因**:AfterDataSave 中「移除明细缓存 + 数据库重载 + 重建合并视图」,用新 DataRow 实例替换明细数据集,而 GridSpliceDetailMerge 分摊缓存(m_hasComputeTotal/m_hasSingleCol)按 DataRow 哈希键控且从不失效 → 重载后下次保存/提交以 0 为基数分摊/截断 → 采购数量清零
- **修复**:缓存失配检测重建(IsDetailCacheValid/RebuildDetailCache)+ SetTotalDetailMerge 清空三缓存
- **归因模式**:**按对象哈希键控的缓存不清理**——重载/重建行实例后缓存命中错误基线。排查「保存后数量变化」先找按 DataRow/对象实例做键的缓存。
### 保存检查错误:表头中不存在列 LastModifiedDate 或 LastModifiedMemo
- **分类**:程序异常(日志级,非致命)
- **根因**:Update 保存表头行取自列表查询 SelectObject,PurchaseDoc.xml 列表查询缺 LastModifiedMemo 列 → ObjectModefiedHelper 并发修改检查静默失效(多模块长期存在)
- **归因模式**:**XML 列表查询缺列导致框架特性静默失效**——报错指向框架机制(并发检查)时,先核对 ConfigFile XML 的列是否齐全。
### 保存异常:无法访问已释放的对象 ucRoundDeskShow
- **分类**:程序异常
- **根因**:消息提醒平台模板数≤3 时 Init() Remove 了承载 ucRoundDeskShow1 的 tabPage1(Remove 释放子控件),DbAsynClient 异步回调仍 Invoke 访问 → ObjectDisposedException
- **归因模式**:**异步回调访问已释放控件**——Remove/Dispose 控件后,异步回调未取消仍 Invoke。排查「无法访问已释放的对象」先找异步回调生命周期。
## 样品单提交(ucObjectSubmit / SP_Sync_NoticeToMesOnSubmit)
### 样品单提交报错:未将对象引用设置到对象的实例(MES 同步链路失败)
- **分类**:程序异常
- **根因**:① 真实错误 = 样品单的 EachExpend 非法数字文本,FTT_MES.SP_Sync_U6NoticeToStyleMaterial `Convert(Decimal(18,6), EachExpend)` 抛 SQL 8114(varchar→numeric);② 表象错误 = 事务失败后 DBCommand.Dispose() 仍调 GetCurrentTransactionLevel() 在失败 SqlTransaction 上执行 SELECT @@TRANCOUNT → SqlHelper.PrepareCommand 抛 NullReferenceException,**掩盖真实错误**
- **修复**:SP 加固(Convert→Try_Convert);DBCommand.Dispose 失败路径直接 Rollback(不再查 @@TRANCOUNT);日志增强(SqlException.Procedure/LineNumber 定位)
- **归因模式**:**表象错误掩盖真实错误**——提交报错先看堆栈是否是框架清理路径(Dispose/事务回滚)抛的异常,真实错误常在更早的 SQL 层(SqlException.Procedure/LineNumber 直接定位)。
## 风行正式订单简易版(UOrderDocImport)
### 数量修改同步到正式订单的触发时机(仅手动【更新订单信息】,提交不触发)
- **分类**:其他(功能改动方案/模块分析)
- **根因**:同步仅挂在【更新订单信息】链接(linkLabel3_LinkClicked → Update() 尾部 exec SP_ImportToOrderDoc_Change);保存走重写版 Save()、提交走 _EditForm.Save() 多态,均不触发同步;另 SP 依赖日志字段导致增删尺码不同步
- **归因模式**:**多入口保存路径不一致**——「保存能同步/提交不能」类问题,先枚举所有保存入口(保存/提交/审核/异步),比对同步钩子挂点。
## 仓库管理-出库/调拨
### 物料出库库存多扣(现存量与流水不符)
- **分类**:数据错误
- **根因**:出库/调拨 SP(up_StockOutSubmit/up_StockAssignSubmit)按 Factory+Warehouse+MaterialID+BatchNo 松条件扣减现存量,不按选料库存行(MCSID);BatchNo=NULL 时唯一约束失效可同键多行 → N 倍多扣;提交/撤回条件不对称;找不到库存行时插负行;调拨确认/取消到料入方不同步;StockOutDetail 无 MCSID 字段
- **归因模式**:**扣减键过松**——库存多扣先核对扣减条件是否含明细行唯一键(MCSID),再核对提交/撤回对称性。
## 样品单-物料表选择物料(frmDataImportFilter)
### 选择物料按款式+颜色筛选 + 颜色兼容匹配(去掉-x后缀)
- **分类**:数据错误(筛选漏数据)+ 功能优化
- **根因**:filter 只对 PDMSample 页签启用,样品单/正式订单页签不按款式+颜色过滤;颜色过滤精确等值导致 云雾白-3 与 云雾白 互不匹配
- **修复**:非 StyleBOM 调用方且 Filter_StyleCode/Filter_ColorName 非空时启用过滤;双方去掉末尾"-数字"后缀后比较
- **归因模式**:**筛选条件语义不一致**——「选不到数据/漏数据」先核对过滤口径(等值 vs 兼容、后缀归一化)与页签覆盖范围。
# 网格过滤联动机制(GridView / PivotGrid)
> WinForms 客户端有两种分析表格,均支持复选框过滤。但普通表格有**联动效果**(如部门过滤后,工艺过滤框只显示该部门的工艺),多维表格原生无联动,需额外实现。
## 普通分析表格(GridView)— GridViewFilterHelper
**入口**:`U.WinCtl.OpCtl.UP4XtraGrid.AddToGrid(grdXtrMainGrid, dsDetail, busObj)` → 自动附加 `GridViewFilterHelper`
**核心文件**:
| 文件 | 位置 | 作用 |
|------|------|------|
| `GridViewFilterHelper.cs` | `U.WinCtl\OpCtl\` | 复选框过滤 + 联动核心类 |
| `UP4XtraGrid.cs` | `U.WinCtl\OpCtl\` | 在 `createGridView` 中调用 `SetGridViewFilterCheckedListModel` |
**调用链**:
```
UP4XtraGrid.AddToGrid()
→ AddToGridEx()
→ createGridView()
→ GridViewFilterHelper.SetGridViewFilterCheckedListModel(gridView) // 第 226 行
→ new GridViewFilterHelper(gridView) // 构造即注册事件
```
**联动原理**:
```
用户点击列头过滤图标
→ ShowFilterPopupCheckedListBox 事件触发
→ 判断 FilterValues 是否为空:
├── 不为空(已有过滤):直接 return,让 DevExpress 用内置缓存值(已反映过滤)
└── 为空(首次或无过滤):
→ 遍历 gridView 中所有**可见行**(gridView.RowCount)
→ 对每行调用 GetRowCellValue / GetRowCellDisplayText
→ SortedDictionary 去重排序
→ 填充 e.CheckedComboBox.Items
```
**联动关键**:每次遍历的是 `gridView` 中**当前可见的行**(已被其他列过滤筛选后的结果),天然实现了级联过滤效果。
**其他调用点**(共 6 处,模式相同):
- `OPEditXtraGrid.cs:405`
- `DefaultXGridView.cs:559`
- `frmViewDocDataLink.linkData.cs:248`
- `frmViewDocDataLink.cs:1407`
- `frmViewDocDataLink.cs:1673`
## 多维分析表格(PivotGrid)— PivotGridHelper + CustomFilterPopupItems
**入口**:`XWinLib.PivotGrid.PivotGridHelper.LoadPivotGridData(pivGridCtl, dsDetail.Tables[0], busObj, _PivotColList);`
**核心文件**:
| 文件 | 位置 | 作用 |
|------|------|------|
| `PivotGridHelper.cs` | `XWinLib\PivotGrid\` | PivotGrid 数据加载 + 过滤联动(**已实现**) |
| `PivotGridHelper_U10.cs` | `XWinLib\PivotGrid\` | U10 版本(**未实现联动**) |
| `PivotGridEx.cs` | `XWinLib\PivotGrid\` | PivotGrid 控件包装器(继承 DevExpress PivotGridControl) |
| `GridViewFilterHelper.cs` | `U.WinCtl\OpCtl\` | 参考实现(仅 GridView,非 PivotGrid) |
**联动实现**(`PivotGridHelper.cs`,`addDataToCtl` 方法中注册):
```
用户点击 FilterArea 字段的过滤下拉框
→ DevExpress 触发 CustomFilterPopupItems 事件
→ GridCtl_CustomFilterPopupItems 事件处理器:
→ ① 检查其他字段是否有激活的过滤条件(FilterValues.HasFilter)
→ 无 → 保持默认行为(显示全部值)
→ 有 → 继续
→ ② 获取底层 DataTable(pivotGrid.DataSource → DataView → DataTable)
→ ③ 为每个其他字段构建 DataTable.Select 过滤表达式(BuildFilterExpression)
- Included 类型: [FieldName] IN (val1, val2, ...)
- Excluded 类型:[FieldName] NOT IN (val1, val2, ...)
- 值格式化:字符串加引号 + 转义、DateTime 用 #...#、数值直接用 ToString
→ ④ 组合过滤条件(AND 连接),dt.Select(combinedFilter) 交叉过滤
→ ⑤ 从过滤后行中提取当前字段去重值(HashSet<string>)
→ ⑥ 从 e.Items 中移除不在去重集合中的项(倒序遍历 RemoveAt)
```
**关键差异(GridView vs PivotGrid)**:
| 维度 | GridViewFilterHelper | PivotGrid CustomFilterPopupItems |
|------|----------------------|----------------------------------|
| 联动机制 | 遍历可见行(天然联动) | DataTable.Select 手动交叉过滤 |
| 事件 | `ShowFilterPopupCheckedListBox` | `CustomFilterPopupItems` |
| 数据源 | `gridView.GetRowCellValue()` | `pivotGrid.DataSource` → DataView → DataTable |
| 值获取 | 按行索引逐行读取 | DataTable.Select 批量过滤后取列值 |
| 勾选状态 | 手动跟踪 `checkedItems` 列表 | DevExpress 原生管理 |
**注意事项**:
- `field.FilterValues.Values` 返回 `object[]` 数组(非 IList),使用 `.Length` 而非 `.Count`
- `PivotGridHelper_U10.cs`(U10 版本)尚未实现联动过滤,如需为 U10 版本表单添加联动,参考主版本 `PivotGridHelper.cs` 的实现
- `FilterArea` 字段的过滤才是复选框形式,RowArea/ColumnArea/DataArea 字段不走此逻辑
**其他项目已有类似实现**:
- `PDM_LN` 和 `YB_FX` 的 `PivotGridHelper.cs` 中已订阅 `CustomFilterPopupItems` 事件,但过滤值操作被注释掉(仅配置了弹出选项)
## 排查步骤
当遇到「多维表格过滤不联动」或「过滤下拉框显示值不对」时:
```
1. 确认表格类型:
→ 普通表格(GridView)→ 检查 GridViewFilterHelper 是否已附加
→ 多维表格(PivotGrid)→ 检查 PivotGridHelper 中 CustomFilterPopupItems 是否已注册
2. 多维表格联动排查:
→ 确认 LoadPivotGridData 是否被调用
→ 确认 addDataToCtl 中是否有 gridCtl.CustomFilterPopupItems += GridCtl_CustomFilterPopupItems
→ 检查 BuildFilterExpression 中 field.FilterValues.Values 的数据类型
→ 检查 DataTable.Select 过滤表达式中值格式化是否正确(字符串需加引号并转义)
3. 性能排查:
→ 数据量大时 DataTable.Select 可能较慢(每次弹出下拉框都全表扫描)
→ 可考虑缓存过滤结果或使用 DataView.RowFilter 替代
```
## 相关文件速查
```
网格过滤功能涉及文件:
├── U.WinCtl\OpCtl\GridViewFilterHelper.cs ← 普通表格复选框过滤联动(完整实现)
├── U.WinCtl\OpCtl\UP4XtraGrid.cs ← 普通表格数据绑定入口(调用 GridViewFilterHelper)
├── XWinLib\PivotGrid\PivotGridHelper.cs ← 多维表格数据加载 + 过滤联动(已实现)
├── XWinLib\PivotGrid\PivotGridHelper_U10.cs ← 多维表格 U10 版本(联动未实现)
├── XWinLib\PivotGrid\PivotGridEx.cs ← 多维表格控件包装器
└── XWinLib\Share\XmlCfgHelper.cs ← PivotGrid 列配置读取(ColPivotList)
```
# 父子单据联动机制(ParentProvider)
> 排查"为什么A单据产生了B任务"。当遇到"单据A保存后异常产生了单据B的任务/通知"类问题时,这是 ParentProvider 跟单联动机制在起作用。
**核心组件**:
| 组件 | 作用 | 位置 |
|------|------|------|
| `[ParentProvider]` 特性 | 声明式定义"谁是当前类的父级" | 业务类上方(AllowMultiple=true) |
| `IDataLinkObjRule.ParentDocType` | 运行时决定"当前数据属于哪个父级" | 业务类的属性实现 |
| `ORMappingByAttHelper.GetParentProviderAttByType()` | 路由方法:DocType → 匹配的 ParentProvider 特性 | `U.Rule/Share/ORMappingByAttHelper.cs` |
| `ObjectModefiedHelper.AfterSaveDate()` | 执行UPDATE:更新父级表的修改时间和状态 | `U.Rule/Share/ObjectModefiedHelper.cs` |
**工作链路**:
```
UI 保存 → BusinessObject.SaveObjectData()
→ ModefiedHelper.AfterSaveDate(busObj, ...)
→ pDocType = busObj.ParentDocType // ① 获取运行时父级类型
→ att = GetParentProviderAttByType(busObj, pDocType) // ② 路由匹配
→ if (att != null)
UPDATE {att.ProviderTableName} // ③ 更新父级表
SET B_MODIFIED_DATE, B_MODIFIED_MEMO='FollowDoc'
WHERE {att.KeyFieldName} IN (keys)
→ 跟单系统检测到父级表变动 // ④ 级联通知
→ 触发父级单据的跟单逻辑 → 产生任务
```
**排查步骤**:
```
1. 确认当前单据的 ParentDocType 返回值
→ 查看业务类中 ParentDocType get 属性
→ 不同业务场景可能返回不同的 DocType(如 UStockAssignTwo 按 StockAssignType 分支)
2. 查看类上所有 [ParentProvider] 特性声明
→ 在源码中检索(知识库内代码分析文档;本地工程目录需用户协助定位)
→ 注意:可能有被注释掉的旧特性(如 //[ParentProvider(OrderSendProject, ...)])
3. 校验路由匹配逻辑
→ 打开 ORMappingByAttHelper.GetParentProviderAttByType()
→ 检查是否存在硬编码 fallback(else if att.DocType == OrderSendProject)
→ 预期:ParentDocType 返回值应精确匹配某个 [ParentProvider] 的 DocType
→ 异常:ParentDocType 返回的 DocType 无对应特性,但被 else fallback 捕获
4. 确认父级表 UPDATE 是否执行
→ 在 AfterSaveDate 中查看 att.ForeingKeyField 是否为 null(null 时直接 return)
→ 外键字段存在于子表明细中时才会收集 keys 并执行 UPDATE
5. 追踪跟单通知链
→ 父级表被 UPDATE 后,跟单系统检测到变更
→ 通知父级单据 → 触发父级自身的跟单 → 产生任务
```
**常见问题模式**:
| 模式 | 症状 | 根因 | 修复 |
|------|------|------|------|
| **硬编码 fallback** | ParentDocType 返回 A,但路由到了 B 父级 | GetParentProviderAttByType 中 else if 无条件匹配 | 改为精确匹配优先,仅单父级类启用降级 |
| **注释残留** | 代码注释掉了一个 [ParentProvider],但运行时仍生效 | DLL 未重新编译/部署 | 检查 bin 目录中的 DLL 是否最新 |
| **ParentDocType 分支遗漏** | 新业务场景走 default 分支返回了错误的 DocType | ParentDocType 的 if/switch 未覆盖新场景 | 新增业务场景分支 |
| **外键字段为 null** | AfterSaveDate 不执行 UPDATE,但跟单仍被触发 | 问题不在 ParentProvider 链路上 | 检查是否有其他触发跟单的路径 |
**关键文件(修改时需谨慎)**:
| 文件 | 说明 |
|------|------|
| `U.Model/Att/ParentProviderAttribute.cs` | 特性定义(禁止随意修改) |
| `U.IFace/IDataLinkObjRule.cs` | ParentDocType 接口定义(禁止修改) |
| `U.Rule/Share/ORMappingByAttHelper.cs:1297` | **核心路由方法**(修改影响所有业务类) |
| `U.Rule/Share/ObjectModefiedHelper.cs:324-380` | **执行UPDATE**(修改影响所有单据保存) |
> **修改 ORMappingByAttHelper.GetParentProviderAttByType() 必须做全量影响分析**:在源码中检索 `[ParentProvider.*OrderSendProject` 找出所有声明了该父级的类,逐一验证其 ParentDocType 返回值能否精确匹配。
# 报表类模块 SQL 获取(三途径 — 按 SysFunction.Winform 分流)
> 旧范式认为报表模块"不走标准窗口加载、SysFunction 无记录"——实际**全部模块在 SysFunction 中都有记录**。区别在于 Winform 类型不同,SQL 定义来源也因此不同。
## 前置查询(所有途径共用)
```sql
SELECT sm.Name AS ModuleName, sf.Name AS WindowType,
sf.BusinessObject, sf.Winform, sf.AssemblyName
FROM SysBusinessModel sm
JOIN SysFunction sf ON sf.ModelID = sm.ID
WHERE sm.Name LIKE '%关键词%'
```
取得 `Winform` 和 `BusinessObject` 后,按以下三途径分流:
```
Winform = U.WinClientDoc.FrmRPTView → 途径 A:自定义报表
Winform = U.WinClient.SysSetting.FrmEditPreviewView → 途径 B:动态表结构定义
其他 Winform(Name = Form 无 View / 标准 View+编辑窗口) → 途径 C:普通业务模块
```
---
## 途径 A:自定义报表(Winform = FrmRPTView)
**特征**:`Winform = U.WinClientDoc.FrmRPTView`,`BusinessObject = U.Rule.Doc.Schedule`。基于存储过程,支持列头合并和字段级权限控制。
**SQL 定位**:四个表均通过 `ReportCode` 关联,由 `Schedule` 引擎逻辑按模块名或 ID 匹配。
| 表 | 作用 | 关键字段 |
|----|------|----------|
| `Sys_RPT_TableDef` | 主表 | `ReportCode`(主键)、`ReportName`(报表名/菜单名)、`CommandType`(一般是 `SP`)、`CommandText`(一般是存储过程名称) |
| `Sys_RPT_GridColumn` | 列配置 | `ReportCode`、`Caption`(显示名称)、`FieldName`(字段名)、`DataType`(字段类型)、`BandCode`(合并列头 UID) |
| `Sys_RPT_GridBand` | 列头合并 | `BandCode`(关联 GridColumn)、`Caption`(合并列头显示名称) |
| `Sys_RPT_Auth` | 权限控制 | `ReportCode`、`FieldName`(字段名)、`AuthCode`(`Edit` 可编辑 / `Visible` 可见)、`UserID`(用户 ID,仅该用户有此权限) |
### 排查步骤
```
1. 确认 Winform = FrmRPTView → 此模块为自定义报表
2. 通过模块名反查 ReportCode(db_query,无连接则给用户 SSMS 执行):
SELECT ReportCode, ReportName, CommandType, CommandText
FROM Sys_RPT_TableDef
WHERE ReportName LIKE '%模块名%'
→ 得到 ReportCode + CommandText(SP 名称)
3. 分析列配置(含合并列头):
SELECT gc.Caption, gc.FieldName, gc.DataType, gc.BandCode,
gb.Caption AS BandCaption
FROM Sys_RPT_GridColumn gc
LEFT JOIN Sys_RPT_GridBand gb ON gb.BandCode = gc.BandCode
WHERE gc.ReportCode = @ReportCode
ORDER BY gc.SortNo
4. 检查权限:
SELECT FieldName, AuthCode, UserID
FROM Sys_RPT_Auth
WHERE ReportCode = @ReportCode
→ AuthCode = 'Edit' 可编辑,'Visible' 可见
→ UserID 非空表示仅该用户有此权限
5. 数据排查 → 提取 CommandText(SP 名)→ db_query 执行
→ 用界面实际参数调用 SP,比对结果
→ 检查:SP 内部逻辑 / 参数过滤 / 权限过滤
6. 常见问题:
- SP 逻辑缺陷(关联/过滤/聚合错误)
- GridColumn 中 FieldName 与 SP 返回列名不匹配 → 列显示空
- GridBand 中 BandCode 写错 → 列头合并失效
- Sys_RPT_Auth 中 AuthCode 配置错误 → 用户看不到列或无法编辑
- 权限冲突(同一字段多条 Auth 记录,UserID 条件互斥)
```
---
## 途径 B:动态表结构定义(Winform = FrmEditPreviewView)
**特征**:`Winform = U.WinClient.SysSetting.FrmEditPreviewView`,`BusinessObject = U.Rule.SysSetting.UPreviewView`(或 UP4 变体,如 `UP4.Rule.SysSetting.UP4PreviewView`)。基于 SQL 动态生成表格。
**SQL 定位**:`UPreviewView` 引擎通过菜单名(`SysBusinessModel.Name`)匹配 `TableIdentic.TableText`。
| 表 | 作用 | 关键字段 |
|----|------|----------|
| `TableIdentic` | 主表 | `TableText`(表描述/模块名)、`SqlString`(查询 SQL 语句) |
| `TableDef` | 列定义 | `FieldName`(字段名)、`FieldType`(字段类型)、`Description`(显示名称) |
### 排查步骤
```
1. 确认 Winform = FrmEditPreviewView → 此模块为动态表结构定义
2. 通过模块名查 TableIdentic(db_query):
SELECT * FROM TableIdentic WHERE TableText LIKE '%模块名%'
→ 得到 SqlString(查询 SQL)
3. 分析列定义:
SELECT FieldName, FieldType, Description
FROM TableDef
WHERE TableIdenticID = @ID
ORDER BY SortNo
4. 数据排查 → 提取 SqlString → db_query 执行
→ 检查:WHERE 过滤条件 / JOIN 关联逻辑 / 子查询 / 聚合计算
5. 常见问题:
- SqlString 中硬编码了错误的过滤条件
- 关联表字段名拼写错误
- TableDef 中 FieldType 与实际查询列类型不匹配
```
---
## 途径 C:普通业务模块(其他 Winform)
**特征**:`Winform` 不是 `FrmRPTView` 或 `FrmEditPreviewView`。不走报表引擎,SQL 定义在 `ConfigFile` XML 中。
又分两类:
- **标准列表窗口**:`Name = View` 存在 → 按常规 dialect/erp/winforms.md 排查链路
- **直接弹窗基础模块**:`Name = Form` 但无 `View` → 按 dialect/erp/winforms.md 通用链路(窗口 → 业务类 → ConfigFile XML Sqls)排查
---
## 三途径速查表
```
SysBusinessModel JOIN SysFunction
↓ 取得 Winform
├── FrmRPTView → 途径 A → Sys_RPT_TableDef.CommandText(SP)
├── FrmEditPreviewView → 途径 B → TableIdentic.SqlString(SQL)
└── 其他 → 途径 C → ConfigFile XML Sqls 节点
├── Name = View 存在 → 标准列表窗口(dialect/erp/winforms.md)
└── Name = Form 无 View → 直接弹窗基础模块(dialect/erp/winforms.md)
```
This diff is collapsed.
This diff is collapsed.
# WinForms 排查(数据错误 / 程序异常 / 性能 / 约束)
> 适用:ERP / PDM / MES 桌面端(F1, U6_*, PDM_LN, MES_*, mes_geshiwei, PDM_MES, Ftt_Common 等)。
> 项目事实(数据库连接、模块加载机制)从知识库检索获取(kb_find / kb_grep / kb_read)。
> 排查任一步因信息不足卡住 → 按 dialect/troubleshoot-core 五步法第 1 步收集证据:向用户索要关键信息(报错全文/操作步骤/日志文件),或建议补强(加异常堆栈日志 / 调试打点)。
## 取证工具
- 查库:`db_query`(SELECT-only)/ `db_schema`,需先在设置中预存连接(connectionId)。
- 无预置连接时:把 SQL 给用户在 SSMS 执行,取回结果再分析。
- 读代码:源码检索(知识库内代码分析文档;本地工程目录需用户协助定位)。
## WinForms 数据错误排查
### 快速分流
```
库存类问题(入库/出库/调拨/盘点/结存/进销存)
→ 先查 `view_MaterialStockDocFlow` 流水视图定界
→ 按 MaterialID + Warehouse + 日期范围查流水,比对各单据类型汇总数 vs 界面展示
→ 确认差异后再按单据类型追踪到具体源表:
入库 → StockIn + StockInDetail
出库 → StockOut + StockOutDetail
调拨 → StockAssign + StockAssignDetail
盘点 → StockBalance + StockCheck + StockCheckDetail
→ 仍无法定位 → 走下方通用流程步骤 1
```
### 通用排查流程
```
1. 确认问题窗口
→ 让用户提供:哪个模块的哪个窗口?
→ 查数据库:
SELECT sm.Name AS ModuleName, sf.Name AS WindowType,
sf.BusinessObject, sf.Winform, sf.AssemblyName
FROM SysBusinessModel sm
JOIN SysFunction sf ON sf.ModelID = sm.ID
WHERE sm.Name LIKE '%关键词%'
→ 得到:AssemblyName(项目), BusinessObject(业务类), Winform(窗口类)
→ 按 Winform 值分流:
- FrmRPTView → 途径 A:自定义报表(读 dialect/erp/special-modules.md)
- FrmEditPreviewView → 途径 B:动态表结构定义(读 dialect/erp/special-modules.md)
- 其他 → 途径 C:普通业务模块(继续下方步骤 2)
2. 定位源码
→ BusinessObject 全限定名 = 项目下文件夹路径 + 文件名
→ 如 U.WinClientDR.BO.DR_BO_Order → U.WinClientDR 项目下 BO/DR_BO_Order.cs
3. 找到配置 SQL
→ 查看业务类中 OBJECT_xx 常量 → 定位 ConfigFile XML
→ 找到 Sqls 节点中问题相关的 SQL(按 Name 匹配)
4. 数据库验证
→ 截取 SQL + 从 temp/(日期)App.Run.Log 获取实际参数
→ db_query 执行(无连接则给用户 SSMS 执行),比对结果与界面展示
→ 分析:过滤条件 / JOIN / GROUP BY / 子查询
5. 根因判定
→ SQL 逻辑缺陷 / 数据脏数据 / 并发覆盖 / XML 配置错误 / 复制后遗症
```
## WinForms 程序异常排查
```
1. 获取错误 → temp/(日期)App.Run.Log → Exception 段落 → 堆栈最上层
2. 分类处理:
空引用 → 构造方法/Load 事件/数据查询后未初始化 → 数据库返回 NULL / XML 缺节点
反射失败 → SysFunction 中信息是否正确 / DLL 是否部署 / 复制后是否重命名完整
类型转换 → ConfigFile Columns 节点类型 vs 数据库实际字段类型
3. 卡住处理(信息不足 / 无堆栈 / 现象模糊 / 日志缺失):
→ 向用户索要关键信息(报错全文/复现步骤/App.Run.Log)
→ 建议补强:异常堆栈日志 / 调试打点(先确认再改代码)
```
## WinForms 性能问题排查
```
1. 定位慢环节 → 列表打开/编辑加载/保存?→ 查看日志耗时
2. 列表慢 → 定位查询 SQL → SSMS 执行计划 → 缺失索引/全表扫描
3. 保存慢 → 检查存储过程/触发器/级联更新
```
## 数据库约束异常排查(WinForms 侧)
```
1. 获取完整错误 → temp/(日期)App.Run.Log 中的异常信息
2. 提取约束名和表名 → 找出违反约束的数据
3. 追踪数据来源 → 业务链路上溯到写入点
常见类型:违反唯一性约束(主键/唯一索引冲突)/ 值检查约束(CHECK)/ 外键约束
```
# MES 产品线热点问题统计(归因模式库)
> 涵盖:MES_*, mes_geshiwei, PDM_MES。每次排查确认根因后由维护者追加。
> 通用桌面端排查链路(窗口→业务类→SQL/SP→数据库)见 dialect/erp/winforms.md。
> 最后更新:2026-08-14
## 成型配套汇总查询(Summary_PartTaskReport / GetStyleIndexNo_CXPT)
### 合计行小于明细行之和
- **分类**:数据错误
- **根因**:合计 UNION 按 `GROUP BY SizeName, SizeSN` 生成 + 最终 `PIVOT (MAX(Qty))` 折叠,多指令查询时同尺码多行被 MAX 吞减(SN 为指令内序号,跨指令不唯一)
- **归因模式**:**聚合键跨上下文不唯一**——UNION 合计与明细用同一 GROUP BY 键,但键只在单指令内唯一;多指令场景被 MAX/折叠吞数。排查「合计<明细」先核对聚合键的唯一性域。
# MES 查询模板(产品线方言)
> 四库通用模板见 `dialect/sql-core/sql-templates.md`;本文件仅 MES 产品线专属查询。
> 表前缀速查:`MES_*` MES 制造执行 / `Order*` ERP 订单 / `Factory*` 工厂车间 / `OrderDocImport*` 订单导入。
## 生产订单-订单关联(查指令的款式/车间)
```sql
SELECT ppd.IndexNo, ppd.StyleCode, a.CustomStyleCode,
b.WorkSectionID, ws.Name AS SectionName
FROM MES_ProductionPlanDetail ppd
INNER JOIN OrderDocImportPlanDetail b ON b.ID = ppd.OrderDetailID
INNER JOIN OrderDocImportDetail a ON a.ID = b.OrderDocImportDetailID
LEFT JOIN FactoryWorkSection ws ON ws.ID = b.WorkSectionID
WHERE ppd.IndexNo LIKE '%关键词%'
AND (a.StyleCode LIKE '%关键词%' OR a.CustomStyleCode LIKE '%关键词%')
```
## 检验批次汇总(查不良明细)
```sql
SELECT ir.ID, ir.InspectionQty, qid.QIItemName,
ird.Qty AS DefectQty, wt.Name AS TeamName
FROM MES_InspectionReport ir
INNER JOIN MES_InspectionReportDetail ird ON ird.InspectionReportID = ir.ID
INNER JOIN MES_QIDetail qid ON qid.ID = ird.QIDetailID
LEFT JOIN FactoryWorkShopTeam wt ON wt.ID = ir.TeamID
WHERE ir.CreateDate >= @StartDate
```
## 尺码明细(查指令的尺码数量)
```sql
SELECT sd.SizeName, sd.Qty, ppd.IndexNo
FROM MES_ProductionPlanSizeDetail sd
INNER JOIN MES_ProductionPlanDetail ppd ON ppd.ID = sd.ProductionPlanDetailID
```
## 订单附加数据(预补数等)
```sql
SELECT ad.AddCount, tp.Name AS AddTypeName
FROM OrderDocImportDetailAddData ad
INNER JOIN FTT_U6.dbo.OrderAddDataType tp ON tp.ID = ad.OrderAddDataTypeID
INNER JOIN OrderDocImportDetail a ON a.ID = ad.OrderDocImportDetailID
```
## 注意
- MES 库与 ERP 库跨库关联时显式写库名(如 `FTT_U6.dbo.OrderAddDataType`),先确认当前连接库。
- 跨库查询需确认账号有对应库权限;无权限时分别查询后在结果侧关联。
# PDM 产品线热点问题统计(归因模式库)
> 涵盖:PDM_LN, LNCenterApi。每次排查确认根因后由维护者追加。
> 通用桌面端排查链路(窗口→业务类→SQL/SP→数据库)见 dialect/erp/winforms.md。
> 最后更新:2026-08-14
## 季节材料汇总表(RPT_SessionMaterialPrice)
### 报表查询慢(>3分钟)
- **分类**:性能
- **根因**:① SP 主查询 3 个相关子查询(STUFF+FOR XML PATH)对无索引临时表全表扫描拼接,工作量=分组数×行数×3(单季度约 8.9 亿次),单季查询数分钟;② SP 返回后客户端用 BeginInvoke+递归串行按批(每批 100 ID)查询 `Sys_StyleConditionDetail`/`Sys_ReportComment`,几十批串行,额外约 30 秒
- **修复**:① 临时表建覆盖索引 + 三个拼接列预去重,SP 提速约 120 倍(>5 分钟→2.5 秒);② 客户端改为一次性拆批 + 受限并发(最多 8 并发);③ 拼接索引键改 MD5 hash,拼接键 UPPER+RTRIM+全角转半角规范化对齐 collation,消除 nvarchar(8000) 索引键超长风险
- **归因模式**:**XML PATH 拼接全表扫描 + 串行批查询**——报表慢先看两个层面:SP 内拼接子查询是否打爆临时表;客户端回填是否串行按批查询。优化方向:覆盖索引/预去重/受限并发。
## 高危风险评估(RK_RiskAssessment)
### 更新模式追加明细保存时 @RiskAssessmentID=0(明细外键没拿到主单ID)
- **分类**:数据错误
- **根因**:明细外键由框架 `SaveDataByObjectDataType` 用主单 ID(mainID)回填,mainID 来自 `_ObjectDataList[BaseData]["ID"]`;更新模式下该行 ID 失效(re-fetch 失败 / 阶段切换 drNext 陈旧未复位 / OptType 误入新增分支),导致 mainID=0 透传成 @RiskAssessmentID=0
- **归因模式**:**主单 ID 回填失效**——「新增正常、更新/编辑模式保存外键为 0」先检查主单 ID 获取路径在更新模式下是否失效(re-fetch/阶段切换/OptType 分支)。
# SQL 脚本生成规范
## 生成与落盘
- 模型生成变更类 SQL 时,产出 `.sql` 脚本文件,落盘到 `Agent/output/sql/`(kb_write 沙箱)。
- **不直接对业务库执行**变更语句(SELECT 查询用 db_query / 结构分析用 db_schema / 计划用 SSMS)。
- 文件名规范:`<动作>_<对象>_<描述>.sql`,如 `AddColumn_Orders_Remark.sql`、`FixData_OrderStatus.sql`。
## 内容规范
1. 顶部注释头(见 sql-style.md:名称/用途/执行人/日期/可重复执行)。
2. 变更语句**幂等**(IF NOT EXISTS / 先查后改)。
3. 数据修正类脚本先给影响行数查询,再给变更语句,最后给验证查询。
4. 脚本用 CRLF 换行,UTF-8 编码。
## 模板
```sql
-- 脚本名称:FixData_OrderStatus
-- 用途:修正订单状态异常数据
-- 执行人:xxx 日期:2024-09-01
-- 可重复执行:是
-- 1. 影响范围核对
SELECT OrderNo, Status FROM dbo.Orders WHERE Status = 'X';
-- 2. 修正
UPDATE dbo.Orders SET Status = 'Y' WHERE Status = 'X';
-- 3. 验证
SELECT OrderNo, Status FROM dbo.Orders WHERE Status = 'X'; -- 期望 0 行
```
## 方言差异提示
- sqlserver:IDENTITY/新增列用 `ALTER TABLE ... ADD` + 幂等判断;NVARCHAR。
- sqlite:`ALTER TABLE ADD COLUMN` 有限制(不能加主键/非空默认复杂值);先 PRAGMA 检查。
- mysql/postgres:类型/引号差异按库适配。
# SQL 格式化规范
## 换行
- **默认以 CRLF 换行**(Windows 标准;与公司既有脚本一致)。
- 禁止在同一文件混用 LF/CRLF。
## 结构
- 关键字大写:`SELECT`、`FROM`、`WHERE`、`JOIN`、`GROUP BY`、`ORDER BY`、`INSERT`、`UPDATE`、`ALTER`。
- 单条语句内缩进 4 空格;子查询/JOIN 条件换行对齐。
- 每张表一个别名(可读性优先):`FROM dbo.Orders o`。
## 注释头(脚本必带)
```sql
-- 脚本名称:AddColumn_Orders_Remark
-- 用途:订单表增加备注列
-- 执行人:xxx 日期:2024-09-01
-- 可重复执行:是(IF NOT EXISTS 幂等)
```
- 变更类脚本(加列/改表/改数据)必须写注释头:名称/用途/执行人/日期/是否可重复执行。
- 查询类(SELECT)不强制,但复杂查询建议注明用途。
## 其它
- 行尾不留空格;文件末尾一个换行。
- 字符串用单引号;标识符含保留字时用方括号(sqlserver)或双引号(其他库)。
- 禁止 `SELECT *` 之外的无条件 UPDATE/DELETE(先查影响行)。
# SQL 查询模板(四库通用)
> 可重复执行原则:所有脚本/查询必须以幂等方式编写——重复执行结果一致、不报错、不产生重复数据。
## 加列(可重复执行)
```sql
-- sqlserver:先查再改(存在才加)
IF NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID('dbo.Orders') AND name = 'Remark')
ALTER TABLE dbo.Orders ADD Remark NVARCHAR(200) NULL;
```
```sql
-- sqlite:PRAGMA 查询列名,不存在才 ALTER
SELECT COUNT(*) FROM pragma_table_info('Orders') WHERE name = 'Remark';
-- 结果 = 0 时执行:
ALTER TABLE Orders ADD COLUMN Remark TEXT;
```
## 更新前先查(核对影响行)
```sql
SELECT COUNT(*) FROM dbo.Orders WHERE Status = 'X'; -- 先看影响范围
```
## 报表/核对查询
```sql
SELECT o.OrderNo, o.Amount, SUM(d.Qty) AS TotalQty
FROM dbo.Orders o
LEFT JOIN dbo.OrderDetails d ON d.OrderId = o.Id
WHERE o.OrderDate >= '2024-01-01'
GROUP BY o.OrderNo, o.Amount
HAVING o.Amount <> SUM(d.Qty) -- 核对金额与明细不一致
ORDER BY o.OrderNo;
```
## 分页(sqlserver OFFSET-FETCH / sqlite LIMIT)
```sql
SELECT * FROM dbo.Orders ORDER BY Id OFFSET 0 ROWS FETCH NEXT 50 ROWS ONLY; -- sqlserver
SELECT * FROM Orders ORDER BY Id LIMIT 50 OFFSET 0; -- sqlite/mysql/postgres
```
## 执行计划(定位慢查询)
```sql
EXPLAIN QUERY PLAN SELECT ... ; -- sqlite
EXPLAIN SELECT ...; -- mysql
EXPLAIN ANALYZE SELECT ...; -- postgres
-- sqlserver 用 SSMS 执行计划(或 db_query 验证数据分布)
```
# 排查五步法
> 对齐系统化调试方法论(根因先行,禁止症状修复)。
## 第 1 步:复现与证据
- 完整读报错信息:堆栈全文、行号、文件路径、错误码(错误信息常含直接答案)。
- 稳定复现:能否稳定触发?触发步骤是什么?每次都出现吗?
- 不能复现 → 收集更多数据(日志/截图/现场),**不猜**。
- 检查近期变更:代码提交、配置、数据、环境差异。
## 第 2 步:链路定位(桌面端业务链路)
```
窗口/界面 → 业务类/Controller → SQL/SP → 数据库
```
- 沿链路逐层取证(`db_query` / `db_schema` / 文件读取)。
- **每一层边界记录**:什么数据进入、什么数据出去、状态/配置是否传递。
- 一次取证找出**在哪一层断**,再深入那一层(不要全链路乱翻)。
## 第 3 步:模式对比
- 找可用的同类链路/相似功能作为对照(「什么能用、和坏的很像」)。
- 对照参考实现时**完整读**(不扫读),列出每一个差异(哪怕很小)。
- 不假设「那个差异不可能有关」。
## 第 4 步:单一假设 + 最小验证
- 明确写出假设:「我认为 X 是根因,因为 Y」。
- 用**最小改动**验证一个变量;一次只验证一个假设。
- 验证不通过 → 形成新假设,**不要在失败修复上叠加修复**。
## 第 5 步:根因修复
- 先造**失败用例**(自动化测试优先;无框架则一段复现脚本)——必须在修复前有。
- 一次只改一处(无「顺手优化」、无捆绑重构)。
- 验证:用例通过?其他用例没坏?问题真正消失?
- **已试 3 次仍失败 → 停止并质疑架构**:每个修复都暴露新耦合/新问题、修复需「大重构」、每修一个又出新的 → 与用户讨论是否重构架构,而非继续第 4 次硬试。
## 环境/外部因素
若系统排查确认是环境、时序或外部因素(95% 的「无根因」是调查不完整,先自查):
1. 记录已调查内容;2. 做合理处理(重试/超时/明确报错);3. 加监控便于后续定位。
# 排查输出规范
回答排查类问题时,按以下结构输出:
```markdown
【任务类型】数据错误 / 程序异常 / 性能 / 约束 / 模块分析
【追踪链路】窗口/接口 → 业务类/Controller → SQL/SP → 数据库
【根因定位】一句话根本原因(必须基于证据,禁止臆断)
【证据链】取证过程(哪一层、用了什么工具、得到什么)
【修复建议】具体修改方案(含文件/配置/SQL)
【影响范围】可能受影响的关联功能
【测试数据】验证用关键记录 / 测试 SQL
```
## 要求
- **根因一句话**:能说清「什么环节、因为什么、导致什么」。
- **证据链可回溯**:每步结论标注来源(文件路径/查询结果/计划),读者能复现。
- **不确定要明说**:证据不足时明确「尚未确认 X」,给下一步取证建议,不编造。
- **修复建议可执行**:具体到文件/字段/SQL;附验证方式(失败用例先造)。
- 排查完的典型案例可经用户确认后整理进 dialect 热点案例(内置只读,由维护者落盘)。
# 凉鞋与拖鞋
## 品类细分
- **EVA 注塑拖鞋**:一体注塑成型,轻量防水,居家/沙滩/酒店用。
- **PVC 发泡拖鞋**:成本低,冬季凉拖/浴室拖。
- **凉鞋**:帮面(织带/皮革/超纤)+ 大底(EVA/橡胶),分平底与坡跟。
- **沙滩鞋/涉水鞋**:速干面料+排水结构+防滑底。
## 工艺特点
- **注塑成型**:模具合模注射(EVA 交联发泡或 PVC),一双一双成型,模具成本高但单双成本低。
- **冷粘**:凉鞋帮面与大底胶粘,款式变化灵活。
- 大底防滑纹路设计(排水槽/人字纹)是卖点与质检点。
## 业务要点
- 季节性强(夏季爆量),产能集中在 Q1-Q2 备货。
- 注塑款模具管理重要(模具编号/寿命/存放)。
- 尺码跨度大(成人 34-46),码重成本差异小。
# 劳保鞋
## 品类细分
- **安全鞋**:防砸(钢包头/复合包头,耐冲击≥200J 等级)、防刺穿(防刺中底)。
- **绝缘鞋**:电绝缘(6kV/10kV 等级),禁止金属部件外露。
- **防静电鞋**:表面电阻/体积电阻控制,用于电子/化工车间。
- **耐油/耐酸碱鞋**:特种材质鞋底与帮面。
- **户外/矿山劳保靴**:高帮、耐磨耐折。
## 标准与认证(国内)
- **GB 21148**《足部防护 安全鞋》:防砸/防刺穿/防静电/电绝缘等安全性能通用要求。
- 需第三方检测报告(SGS/CTI 等),出欧盟 CE 需 EN ISO 20345 系列。
- 钢包头:GB 21148 规定冲击能量 200J 与压缩 15kN 试验。
## 工艺特点
- 帮面常用:头层牛皮/二层牛皮(耐摩擦)、超纤、帆布(轻便款)。
- 大底:橡胶/聚氨酯(PU)双密度(中底缓震+外底耐磨防滑)。
- 成型:冷粘或硫化(橡胶大底硫化结合强度高,耐油款常用)。
- 中底防刺层:钢板/凯夫拉(Kevlar)复合中底。
## 业务要点
- 证书与检测报告是接单必要条件(客户验厂/验货重点)。
- 鞋码偏大(欧码),内长标注重要。
- 单量稳定但品类固定,翻单率高。
# 皮鞋与休闲鞋
## 品类细分
- **正装皮鞋**:头层牛皮帮面、真皮/橡胶大底、固特异沿条工艺(可换底)。
- **休闲皮鞋**:胶粘工艺为主,超纤/二层皮帮面,EVA/橡胶底。
- **商务休闲鞋**:轻量软底,飞织+皮革拼接。
## 材料
- 帮面:头层牛皮(粒面/修面)、二层牛皮(剖层贴膜)、超纤(PU 涂层的仿皮,物性稳定)、PU 革。
- 内里:猪皮/羊皮里(透气)、超纤里、织物里。
- 大底:真皮底(固特异)、橡胶、PU、TPU(耐磨止滑)。
## 工艺特点
- **固特异(Goodyear)**:沿条+缝线+软木填充,可换底、耐穿,工艺链长(绷帮→沿条→缝线→上底)。
- **胶粘(Cemented)**:绷帮后直接胶粘大底,主流工艺,周期短。
- **绷帮**:鞋面套楦定型,决定鞋型美观与合脚度(绷帮不良→歪楦/开胶)。
## 业务要点
- 真皮采购有批差/色差(缸差),同款多批次需预留配色余量。
- 皮鞋尺码偏大,内长标注;半码需求多。
- 固特异款交期长(工艺链长),报价高。
# 童鞋
## 品类细分
- **学步鞋(1~3 岁)**:软底、轻量、易弯折,鞋头宽敞保护脚趾。
- **机能鞋(3~6 岁)**:足弓支撑、后跟加固,纠正步态。
- **中大童鞋(6~12 岁)**:接近成人鞋结构,运动款为主(校园/日常)。
- **婴幼鞋(<1 岁)**:防掉跟、无鞋带,装饰件最少化。
## 安全标准(国内重点)
- **GB 30585**《儿童鞋安全技术规范》:物理安全(小附件抗拉、锐边)与化学安全(六价铬、可分解致癌芳香胺、甲醛、重金属限量)。
- 小附件(鞋带扣/装饰件)拉力 ≥70N 测试,防脱落误吞。
- 童鞋号型按 GB/T 3293(内长制),鞋内腔尺寸必须标注。
## 工艺特点
- 成型:注塑一体成型占比高(EVA/TPU/橡胶注塑),成本低、无胶水污染。
- 帮面:网布/飞织/超纤/帆布,避免小颗粒装饰。
- 大底:EVA(轻)或橡胶(防滑),学步鞋强调 1/3 处易弯折。
## 业务要点
- 检测报告(GB 30585 全套)是商超/电商平台入驻门槛。
- 款式多、单款量小、翻单快,对开发与排产柔性要求高。
- 尺码分段(段码)管理:婴幼段/小童段/中童段/大童段,BOM 按段码区分。
# 运动鞋
## 品类细分
- **跑步鞋**:缓震中底(EVA/TPU/超临界发泡),网布/飞织鞋面,轻量化。专业跑鞋分缓震/支撑/竞速。
- **篮球鞋**:高帮包裹、防侧翻(TPU 抗扭转片)、耐磨大底(橡胶/人字纹)。
- **足球鞋**:鞋钉(FG/SG/AG/TF 按场地)、触球面材料(袋鼠皮/超纤/合成革)。
- **综训/休闲运动鞋**:通用缓震+稳定结构,鞋面网布+合成革拼接。
## 工艺特点
- 主流成型路线:**冷粘**(占多数,鞋面+大底胶粘,工艺灵活)与**注塑**(PVC/TPU/EVA 一体成型,多用于童鞋/拖鞋)。
- 飞织鞋面:一体织造无裁片,减少针车工序,需热熔贴合支撑片。
- 大底常见组合:EVA 中底 + 橡胶外底(耐磨防滑)。
## 业务要点
- 开发节奏快(季度上新),样品确认(确认样/船头样)是关键节点。
- 尺码体系:欧码(EU)/美码(US)/英码(UK)换算,半码/整码分布影响 BOM 与排产。
- 配色多(每款多色),材料单耗按配色分别核算。
# 包装与入库
## 包装流程
- **配对**:左右脚配对(同码同色,色差/号码核对),补单件。
- **装盒**:内衬/纸盒/防潮剂;盒标(款号/颜色/尺码/条码)。
- **装箱**:箱规(每箱双数,如 12/24 双)、外箱唛(毛净重/箱号/款号)。
- **吊牌/三标**:洗标/主标/尺码标齐备(三标错漏属外观不良)。
## 入库
- 成品仓按款/色/码/批次入库,批次追溯(生产批→原材料批)。
- 尾数管理:订单完成后未销完的零头数量(库存尾数/生产尾数),需单独标识与处理。
## 排查要点(数据不对时)
- 箱规与实际装箱不符 → 出货数量差异(装盒/装箱环节)。
- 配对错误(鸳鸯鞋)→ 质检漏检(OQC 抽检比例不足)。
- 批次混放 → 追溯断链(先入库先出库原则)。
# 成型工艺
> 鞋类最后成型(贴合/上底),三大路线决定设备、胶水、交期与成本结构。
## 冷粘(Cemented)——主流
- 流程:鞋面套楦(绷帮)→ 刷处理剂/胶水(鞋面+大底)→ 烘箱活化 → 贴合 → 压合 → 冷定型。
- 特点:工艺灵活、款式切换快;胶水环保要求高(水性胶替代油性胶)。
- 常见:运动鞋、休闲鞋、凉鞋。
## 硫化(Vulcanized)
- 流程:生胶大底贴鞋面 → 硫化罐加温加压(硫磺交联)→ 橡胶熟化成型。
- 特点:橡胶大底与鞋面结合强度高、耐油耐磨;工艺链长、周期长。
- 常见:劳保鞋(耐油款)、帆布硫化鞋(经典款)。
## 注塑(Injection)
- 流程:模具合模,PVC/TPU/EVA 注射成型,大底与鞋面一体或直接成型。
- 特点:无胶水、效率高、成本低;模具投入大。
- 常见:童鞋、拖鞋、凉鞋、劳保鞋(PU 注塑底)。
## 绷帮(Lasting)
- 鞋面套楦定型工序(三大路线共用前置),决定鞋型与合脚度。
- 常见方式:手工绷帮/机器绷帮(绷帮机);后跟定型/包头定型。
## 排查要点(质量问题)
- 开胶:处理剂/胶水工艺参数(温湿度、活化温度)或大底材质污染。
- 歪楦/不对称:绷帮不正、楦头配对错误。
- 溢胶/缺胶:贴合对位、压合力与时间。
- 成型产能:成型线节拍 = 瓶颈,排产与交期核心。
# 裁断与针车
## 裁断
- **裁片**:按唛架排版裁剪出的面料片(鞋面/内里/衬片)。
- **唛架(Marker)**:裁剪排版图,决定面料利用率(利用率=裁片面积/用料面积)。
- **裁断设备**:液压裁断机(刀模冲裁)/电脑裁床(多层)。
- 要点:刀模管理(编号/寿命)、错裁/色差片隔离、余料利用。
## 针车(鞋面缝制)
- **针数**:每英寸针数(针织密度指标,影响手感与强度,常规 8~12 针/英寸)。
- 工序:合缝(帮片拼接)→ 压线 → 包边 → 装饰线 → 缝标(洗标/主标/尺码标)。
- 设备:单针/双针平缝机、花样机、包边机。
- 要点:针距均匀、线色匹配、跳针/断针记录(IPQC 巡检点)。
## 排查要点(数据不对时)
- 裁片损耗:实际裁断用量 vs BOM 单耗(损耗率是否漏加/多加,行业常见 3%~8%)。
- 针车产能:瓶颈工序负载(针车工位数量×效率 vs 排产)。
- 色差:不同缸次面料混裁混缝 → 同双色差(IQC 与仓库批次追溯)。
# BOM 与单耗
## 核心概念
- **BOM(物料清单)**:鞋款的结构化物料清单——面料/辅料/配件/大底/中底/鞋垫/包装,含单耗与用量单位。
- **单耗**:单双鞋的物料用量(如鞋面材料按平方英尺/码/米,大底按双,织带按米)。
- **损耗率**:裁断损耗/缝制损耗计入材料成本(行业常见 3%~8%,按面料弹性与排版;皮料按伤残率更高)。
- **BOM 版本**:样品 BOM → 确认样 BOM → 量产 BOM,版本与变更记录(ECN)管理。
## 核算链路
```
BOM(面料/辅料单耗×单价) + 工价(工序×工价) + 制费分摊 + 损耗加成
→ 制造成本 → + 管理/销售费用 → 完全成本 → 报价
```
## 排查要点(数据不对时)
- **单耗 vs 实际用量**:BOM 单耗与裁剪实际用量是否一致(损耗率是否漏加/多加)。
- **单价来源**:面料单价是否按最新采购价(价差未更新 → 成本虚高/虚低)。
- **工价版本**:工序工价表是否与当前工艺单版本一致。
- **分摊口径**:制费分摊基数(产量 vs 工时)是否与成本中心配置一致。
- **BOM 缺失/错挂**:配色多时按配色 BOM 区分,错挂导致材料计划偏差。
# 鞋底材料
## 大底/中底材料
- **RB(橡胶)**:耐磨、止滑、耐油,外底常用(人字纹/水波纹);硫化工艺用生胶。
- **EVA**:轻量、缓震,中底主流(发泡密度决定软硬度);成本低,压缩永久变形偏大。
- **PU(聚氨酯)**:缓震回弹好、耐疲劳,中底/整底;怕水解(储存环境要求)。
- **TPU**:高回弹、耐折、耐磨,中底/外底/结构件(抗扭转片);成本高。
- **MD(Phylon)**:EVA 二次发泡(射坯发泡),轻量回弹好,运动鞋中底。
- **PVC**:低成本,拖鞋/凉鞋注塑;低温变硬。
## 结构件
- 抗扭转片(TPU/碳板)、防刺穿中底(钢板/凯夫拉——劳保鞋)、鞋垫(EVA/乳胶/ortholite)。
## 排查要点(数据不对时)
- 单耗:大底按双计(一双两件:左+右),模具腔数决定单位产出。
- 密度/硬度规格:发泡密度波动影响克重与缓震(物性测试项)。
- 橡胶 vs 发泡类物料管理差异:橡胶需防硫化时间超期,EVA 发泡片需熟化期。
# 鞋面材料
## 织物类
- **网布(Mesh)**:透气,运动鞋鞋面主材;按纱线分涤纶网布/尼龙网布,强度与透气权衡。
- **飞织(Flyknit)**:一体织造鞋面,无裁片、轻量、贴合;需热熔丝/支撑片补强。
- **帆布**:棉/涤帆布,硫化鞋/休闲鞋经典材料,耐磨耐折。
## 皮革类
- **头层牛皮**:粒面/修面,正装皮鞋;需注意缸差与伤残。
- **二层牛皮**:剖层贴膜(PU 涂层),性价比。
- **超纤(Microfiber)**:PU 涂层的仿皮,物性稳定、色牢度好、环保,运动/休闲大量使用。
- **PU 革/合成革**:低成本,需关注耐磨与低温耐折。
## 辅料
- 织带/松紧带/鞋带、五金(鞋眼/扣具/装饰件——童鞋小附件拉力要求)、热熔胶膜(补强/贴合)、衬片(港宝:后跟定型片)。
## 排查要点(数据不对时)
- 单耗:鞋面材料单耗按唛架排版核算(利用率),实际裁断用量 vs BOM 差异定位损耗口径。
- 色差:不同缸次/批次混用(缸差),IQC 检验记录与仓库批次追溯。
- 缩水率:面料水洗/热缩参数影响裁片尺寸(工艺单参数核对)。
# 常见问题(本地沉淀区)
> 本目录为鞋业行业问答沉淀区:智能体在行业问答中确认的典型问题,由维护者整理成 Markdown 主题文件放入本目录,并在 `../INDEX.md` 的 07-常见问题 大类登记(文件名 + 一句话说明 + 触发词)。
>
> 当前为空:暂无沉淀条目。
>
> 注意:内置目录只读,智能体禁止直接写入;沉淀由维护者(人工)执行。
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
【工具使用规则】
- 无依赖的多个信息需求应在同一轮一次性提交多个工具调用(如同时 kb_read 三个文件),避免逐个调用浪费轮次。
- kb_read 一次最多读 500 行;长文件先读主体部分,再用 offset 续读未读部分,不要重复读取已读区间。
- 工具结果过长会带截断提示:此时应用 offset 续读未读区间,不要调小 limit 从头重读。
- kb_write / kb_write_kb 一次只写一个文件;覆盖已有文件前先征得用户同意;追加请用 append=true。kb_write 写默认输出目录的 kb_write 子文件夹(Agent/output/kb_write/),kb_write_kb 仅在用户明确要求存入知识库时使用。
- 检索结果过多时,先用 kb_list/kb_find 缩小范围,再精读。
- 工具返回为空或报错时,如实说明,不要假装成功。
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment