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
1ac5825c
Commit
1ac5825c
authored
Aug 26, 2026
by
段廷瑞
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
pref: 行业知识,SQL及问题排查能力内置
parent
5cdb8136
Changes
53
Hide whitespace changes
Inline
Side-by-side
Showing
53 changed files
with
3844 additions
and
73 deletions
+3844
-73
.gitignore
.gitignore
+3
-1
AgentTest.csproj
AgentTest/AgentTest.csproj
+14
-0
DialectServiceTests.cs
AgentTest/DialectServiceTests.cs
+258
-0
TextEncodingTests.cs
AgentTest/TextEncodingTests.cs
+477
-0
INDEX.md
AppHome/Agent/dialect/INDEX.md
+38
-0
aspnet.md
AppHome/Agent/dialect/erp/aspnet.md
+120
-0
hotspots.md
AppHome/Agent/dialect/erp/cases/hotspots.md
+59
-0
grid-filter.md
AppHome/Agent/dialect/erp/grid-filter.md
+128
-0
parent-provider.md
AppHome/Agent/dialect/erp/parent-provider.md
+73
-0
special-modules.md
AppHome/Agent/dialect/erp/special-modules.md
+135
-0
sql-script-location.md
AppHome/Agent/dialect/erp/sql-script-location.md
+316
-0
verification-flow.md
AppHome/Agent/dialect/erp/verification-flow.md
+238
-0
winforms.md
AppHome/Agent/dialect/erp/winforms.md
+94
-0
hotspots.md
AppHome/Agent/dialect/mes/cases/hotspots.md
+12
-0
sql-templates-mes.md
AppHome/Agent/dialect/mes/sql-templates-mes.md
+51
-0
hotspots.md
AppHome/Agent/dialect/pdm/cases/hotspots.md
+20
-0
sql-scripts.md
AppHome/Agent/dialect/sql-core/sql-scripts.md
+38
-0
sql-style.md
AppHome/Agent/dialect/sql-core/sql-style.md
+30
-0
sql-templates.md
AppHome/Agent/dialect/sql-core/sql-templates.md
+52
-0
five-steps.md
AppHome/Agent/dialect/troubleshoot-core/five-steps.md
+44
-0
output-spec.md
AppHome/Agent/dialect/troubleshoot-core/output-spec.md
+21
-0
凉鞋与拖鞋.md
AppHome/Agent/industry/鞋业/01-品类认知/凉鞋与拖鞋.md
+20
-0
劳保鞋.md
AppHome/Agent/industry/鞋业/01-品类认知/劳保鞋.md
+28
-0
皮鞋与休闲鞋.md
AppHome/Agent/industry/鞋业/01-品类认知/皮鞋与休闲鞋.md
+25
-0
童鞋.md
AppHome/Agent/industry/鞋业/01-品类认知/童鞋.md
+26
-0
运动鞋.md
AppHome/Agent/industry/鞋业/01-品类认知/运动鞋.md
+20
-0
包装与入库.md
AppHome/Agent/industry/鞋业/02-生产工艺/包装与入库.md
+19
-0
成型工艺.md
AppHome/Agent/industry/鞋业/02-生产工艺/成型工艺.md
+33
-0
裁断与针车.md
AppHome/Agent/industry/鞋业/02-生产工艺/裁断与针车.md
+21
-0
BOM与单耗.md
AppHome/Agent/industry/鞋业/03-材料与BOM/BOM与单耗.md
+23
-0
鞋底材料.md
AppHome/Agent/industry/鞋业/03-材料与BOM/鞋底材料.md
+20
-0
鞋面材料.md
AppHome/Agent/industry/鞋业/03-材料与BOM/鞋面材料.md
+24
-0
报价与成本.md
AppHome/Agent/industry/鞋业/04-业务与贸易/报价与成本.md
+24
-0
接单与订单评审.md
AppHome/Agent/industry/鞋业/04-业务与贸易/接单与订单评审.md
+20
-0
翻单与交期.md
AppHome/Agent/industry/鞋业/04-业务与贸易/翻单与交期.md
+24
-0
检验体系.md
AppHome/Agent/industry/鞋业/05-质量管理/检验体系.md
+24
-0
物性测试.md
AppHome/Agent/industry/鞋业/05-质量管理/物性测试.md
+25
-0
术语表.md
AppHome/Agent/industry/鞋业/06-专业术语/术语表.md
+44
-0
README.md
AppHome/Agent/industry/鞋业/07-常见问题/README.md
+7
-0
INDEX.md
AppHome/Agent/industry/鞋业/INDEX.md
+42
-0
Project.cs
Ftt.PatchPack/Project.cs
+19
-0
AgentEngine.cs
Tools/MyAgent/Agent/AgentEngine.cs
+5
-0
PromptBuilder.cs
Tools/MyAgent/Agent/PromptBuilder.cs
+22
-0
DbQueryTool.cs
Tools/MyAgent/Agent/Tools/DbQueryTool.cs
+2
-1
DbSchemaTool.cs
Tools/MyAgent/Agent/Tools/DbSchemaTool.cs
+194
-0
DialectTools.cs
Tools/MyAgent/Agent/Tools/DialectTools.cs
+202
-0
ITool.cs
Tools/MyAgent/Agent/Tools/ITool.cs
+8
-0
AgentSettingsForm.Designer.cs
Tools/MyAgent/Forms/AgentSettingsForm.Designer.cs
+105
-34
AgentSettingsForm.cs
Tools/MyAgent/Forms/AgentSettingsForm.cs
+9
-9
DialectService.cs
Tools/MyAgent/Kb/DialectService.cs
+250
-0
KbService.cs
Tools/MyAgent/Kb/KbService.cs
+24
-28
TextEncoding.cs
Tools/MyUtils/TextEncoding.cs
+310
-0
Tools.csproj
Tools/Tools.csproj
+4
-0
No files found.
.gitignore
View file @
1ac5825c
...
@@ -65,5 +65,7 @@ bin
...
@@ -65,5 +65,7 @@ bin
*.zip
*.zip
/AppHome/AgentChat
/AppHome/AgentChat
/AppHome/Agent/history
/AppHome/Agent/history
/AppHome/Agent
/AppHome/Agent/*
!/AppHome/Agent/dialect/
!/AppHome/Agent/industry/
/AppHome/Agent/config/AgentConfig.json
/AppHome/Agent/config/AgentConfig.json
AgentTest/AgentTest.csproj
View file @
1ac5825c
...
@@ -45,6 +45,18 @@
...
@@ -45,6 +45,18 @@
<Reference
Include=
"System.Xml.Linq"
/>
<Reference
Include=
"System.Xml.Linq"
/>
<Reference
Include=
"System.Data"
/>
<Reference
Include=
"System.Data"
/>
<Reference
Include=
"System.Xml"
/>
<Reference
Include=
"System.Xml"
/>
<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>
<Reference
Include=
"NPOI.OOXML, Version=2.5.1.0, Culture=neutral, PublicKeyToken=0df73ec7942b34e1, processorArchitecture=MSIL"
>
<HintPath>
..\packages\NPOI.2.5.1\lib\net45\NPOI.OOXML.dll
</HintPath>
</Reference>
<Reference
Include=
"NPOI.OpenXml4Net, Version=2.5.1.0, Culture=neutral, PublicKeyToken=0df73ec7942b34e1, processorArchitecture=MSIL"
>
<HintPath>
..\packages\NPOI.2.5.1\lib\net45\NPOI.OpenXml4Net.dll
</HintPath>
</Reference>
<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>
</ItemGroup>
</ItemGroup>
<ItemGroup>
<ItemGroup>
<Compile
Include=
"Program.cs"
/>
<Compile
Include=
"Program.cs"
/>
...
@@ -54,8 +66,10 @@
...
@@ -54,8 +66,10 @@
<Compile
Include=
"AgentEngineLogicTests.cs"
/>
<Compile
Include=
"AgentEngineLogicTests.cs"
/>
<Compile
Include=
"FailoverPolicyTests.cs"
/>
<Compile
Include=
"FailoverPolicyTests.cs"
/>
<Compile
Include=
"KbServiceSandboxTests.cs"
/>
<Compile
Include=
"KbServiceSandboxTests.cs"
/>
<Compile
Include=
"DialectServiceTests.cs"
/>
<Compile
Include=
"EmbeddingClientTests.cs"
/>
<Compile
Include=
"EmbeddingClientTests.cs"
/>
<Compile
Include=
"ToolPermissionTests.cs"
/>
<Compile
Include=
"ToolPermissionTests.cs"
/>
<Compile
Include=
"TextEncodingTests.cs"
/>
</ItemGroup>
</ItemGroup>
<ItemGroup>
<ItemGroup>
<ProjectReference
Include=
"..\Tools\Tools.csproj"
>
<ProjectReference
Include=
"..\Tools\Tools.csproj"
>
...
...
AgentTest/DialectServiceTests.cs
0 → 100644
View file @
1ac5825c
using
System
;
using
System.IO
;
using
System.Linq
;
using
Newtonsoft.Json.Linq
;
using
Tools.MyAgent.Agent
;
using
Tools.MyAgent.Agent.Tools
;
using
Tools.MyAgent.Kb
;
namespace
AgentTest
{
/// <summary>
/// 方言迁移 P2 测试:DialectService(INDEX 解析/摘要预算/沙箱)+ d_* 工具(d_read/d_grep/d_list)+
/// PromptBuilder 方言层注入。覆盖迁移方案 §8.1 用例。
/// </summary>
public
sealed
class
DialectServiceTests
:
TestRunner
{
private
string
_root
;
// 临时 agentRoot(含 dialect/ + industry/)
private
DialectService
_ds
;
public
override
void
SetUp
()
{
_root
=
Path
.
Combine
(
Path
.
GetTempPath
(),
"agenttest_dialect_"
+
Guid
.
NewGuid
().
ToString
(
"N"
));
string
dialect
=
Path
.
Combine
(
_root
,
"dialect"
);
string
industry
=
Path
.
Combine
(
_root
,
"industry"
);
string
shoe
=
Path
.
Combine
(
industry
,
"鞋业"
);
Directory
.
CreateDirectory
(
Path
.
Combine
(
dialect
,
"erp"
,
"cases"
));
Directory
.
CreateDirectory
(
Path
.
Combine
(
dialect
,
"sql-core"
));
Directory
.
CreateDirectory
(
Path
.
Combine
(
shoe
,
"01-品类认知"
));
Directory
.
CreateDirectory
(
Path
.
Combine
(
shoe
,
"06-专业术语"
));
File
.
WriteAllText
(
Path
.
Combine
(
dialect
,
"INDEX.md"
),
"# 产品排查方言\n行业简介:产品线排查方言。\n"
+
"## 01-排查方法论\n"
+
"- troubleshoot-core/five-steps.md:排查五步法(触发词:排查、报错、根因)\n"
+
"## 02-SQL 规范\n"
+
"- sql-core/sql-templates.md:四库查询模板(触发词:加列、执行计划)\n"
);
File
.
WriteAllText
(
Path
.
Combine
(
dialect
,
"erp"
,
"winforms.md"
),
"# WinForms 排查\n库存不对先查 view_MaterialStockDocFlow 流水。\n"
);
File
.
WriteAllText
(
Path
.
Combine
(
dialect
,
"sql-core"
,
"sql-templates.md"
),
"# 模板\nSELECT 1;\n"
);
File
.
WriteAllText
(
Path
.
Combine
(
shoe
,
"INDEX.md"
),
"# 鞋业(别名:制鞋、鞋类)\n行业简介:制鞋行业基础知识。\n"
+
"## 01-品类认知\n"
+
"- 运动鞋.md:跑鞋/篮球鞋(触发词:运动鞋、跑鞋)\n"
+
"## 06-专业术语\n"
+
"- 术语表.md:裁片/针数(触发词:术语、唛架)\n"
);
File
.
WriteAllText
(
Path
.
Combine
(
shoe
,
"01-品类认知"
,
"运动鞋.md"
),
"# 运动鞋\n飞织鞋面,EVA 中底。\n"
);
File
.
WriteAllText
(
Path
.
Combine
(
shoe
,
"06-专业术语"
,
"术语表.md"
),
"# 术语表\n唛架:裁剪排版图。\n"
);
_ds
=
new
DialectService
(
_root
);
}
public
override
void
TearDown
()
{
try
{
Directory
.
Delete
(
_root
,
true
);
}
catch
{
}
}
// ── DialectService ──
[
Test
]
public
void
ScanDialect_ParsesIndex
()
{
var
d
=
_ds
.
ScanDialect
();
Assert
.
Equal
(
"产品排查方言"
,
d
.
Name
);
Assert
.
Equal
(
2
,
d
.
Topics
.
Count
,
"方言主题数"
);
var
t
=
d
.
Topics
[
0
];
Assert
.
Equal
(
"01-排查方法论"
,
t
.
Category
);
Assert
.
Equal
(
"troubleshoot-core/five-steps.md"
,
t
.
File
);
Assert
.
Equal
(
"five-steps"
,
t
.
Title
);
Assert
.
Equal
(
"排查五步法"
,
t
.
Description
);
Assert
.
True
(
t
.
Triggers
.
Contains
(
"报错"
),
"触发词应解析"
);
Assert
.
True
(
t
.
Triggers
.
Contains
(
"根因"
));
}
[
Test
]
public
void
ScanIndustries_ParsesAliases
()
{
var
inds
=
_ds
.
ScanIndustries
().
ToList
();
Assert
.
Equal
(
1
,
inds
.
Count
,
"行业数"
);
var
ind
=
inds
[
0
];
Assert
.
Equal
(
"鞋业"
,
ind
.
Name
);
Assert
.
Equal
(
2
,
ind
.
Aliases
.
Length
,
"别名数"
);
Assert
.
Equal
(
"制鞋"
,
ind
.
Aliases
[
0
]);
Assert
.
Equal
(
"鞋类"
,
ind
.
Aliases
[
1
]);
Assert
.
True
(
ind
.
Topics
.
Any
(
x
=>
x
.
File
==
"01-品类认知/运动鞋.md"
),
"行业主题路径应补全大类前缀"
);
}
[
Test
]
public
void
BuildSummary_UnderBudget_AndHasHeader
()
{
string
summary
=
_ds
.
BuildSummary
();
Assert
.
True
(
summary
.
Contains
(
"产品方言与行业知识"
),
"摘要应有路由头"
);
Assert
.
True
(
summary
.
Contains
(
"产品排查方言"
),
"摘要应含方言导航"
);
Assert
.
True
(
summary
.
Contains
(
"鞋业"
),
"摘要应含行业导航"
);
Assert
.
True
(
summary
.
Contains
(
"别名:制鞋"
),
"摘要应含别名"
);
Assert
.
True
(
summary
.
Contains
(
"触发词"
),
"摘要应含触发词提示"
);
Assert
.
True
(
System
.
Text
.
Encoding
.
UTF8
.
GetByteCount
(
summary
)
<=
4096
,
"摘要预算(宽松校验,实际 2KB 预算内)"
);
}
[
Test
]
public
void
BuildSummary_MissingIndex_NoThrow
()
{
var
ds2
=
new
DialectService
(
Path
.
Combine
(
Path
.
GetTempPath
(),
"agenttest_missing_"
+
Guid
.
NewGuid
().
ToString
(
"N"
)));
Assert
.
False
(
ds2
.
IsAvailable
,
"目录缺失应不可用"
);
Assert
.
Equal
(
""
,
ds2
.
BuildSummary
(),
"无内置知识时应返回空(不注入空层)"
);
}
[
Test
]
public
void
ResolveSafe_ValidAndBoundary
()
{
string
abs
=
_ds
.
ResolveSafe
(
"dialect/erp/winforms.md"
);
Assert
.
NotNull
(
abs
,
"合法路径应解析"
);
Assert
.
True
(
abs
.
EndsWith
(
"winforms.md"
,
StringComparison
.
OrdinalIgnoreCase
));
string
abs2
=
_ds
.
ResolveSafe
(
"industry/鞋业/01-品类认知/运动鞋.md"
);
Assert
.
NotNull
(
abs2
,
"中文行业路径应解析"
);
Assert
.
Null
(
_ds
.
ResolveSafe
(
"erp/winforms.md"
),
"缺少前缀应拒绝"
);
Assert
.
Null
(
_ds
.
ResolveSafe
(
"dialect/../../secret.txt"
),
"../ 穿越应拒绝"
);
Assert
.
Null
(
_ds
.
ResolveSafe
(
"dialect/"
+
_root
.
Replace
(
'\\'
,
'/'
)
+
"/x"
),
"绝对路径应拒绝"
);
Assert
.
Null
(
_ds
.
ResolveSafe
(
"dialect"
),
"目录非文件也返回 null(ResolveSafe 仅文件语义)"
);
}
[
Test
]
public
void
EnumerateFiles_CoversBothRoots
()
{
var
files
=
_ds
.
EnumerateFiles
().
ToList
();
Assert
.
True
(
files
.
Any
(
f
=>
f
==
"dialect/erp/winforms.md"
),
"应枚举方言文件"
);
Assert
.
True
(
files
.
Any
(
f
=>
f
.
StartsWith
(
"industry/鞋业/"
,
StringComparison
.
Ordinal
)),
"应枚举行业文件"
);
Assert
.
False
(
files
.
Any
(
f
=>
f
.
StartsWith
(
"dialect/../"
,
StringComparison
.
Ordinal
)),
"路径应规范化"
);
}
// ── d_* 工具 ──
private
static
ToolContext
MakeCtx
(
DialectService
ds
)
{
var
kb
=
new
KbService
();
return
new
ToolContext
(
kb
,
null
)
{
Dialect
=
ds
};
}
[
Test
]
public
void
DRead_ValidFile
()
{
var
tool
=
new
DialectReadTool
();
string
result
=
tool
.
Execute
(
JObject
.
Parse
(
"{'path':'dialect/erp/winforms.md'}"
),
MakeCtx
(
_ds
));
Assert
.
True
(
result
.
Contains
(
"view_MaterialStockDocFlow"
),
"应读到文件内容"
);
Assert
.
True
(
result
.
Contains
(
"== dialect/erp/winforms.md"
),
"应带路径头"
);
}
[
Test
]
public
void
DRead_MissingFile_GivesCandidates
()
{
var
tool
=
new
DialectReadTool
();
string
result
=
tool
.
Execute
(
JObject
.
Parse
(
"{'path':'dialect/erp/winforms'}"
),
MakeCtx
(
_ds
));
Assert
.
True
(
result
.
Contains
(
"winforms.md"
),
"缺扩展名应给出候选"
);
}
[
Test
]
public
void
DRead_Traversal_Rejected
()
{
var
tool
=
new
DialectReadTool
();
string
result
=
tool
.
Execute
(
JObject
.
Parse
(
"{'path':'dialect/../../secret'}"
),
MakeCtx
(
_ds
));
Assert
.
True
(
result
.
Contains
(
"不存在"
)
||
result
.
Contains
(
"非法"
),
"越界应拒绝"
);
}
[
Test
]
public
void
DGrep_HitsAndMiss
()
{
var
tool
=
new
DialectGrepTool
();
string
hit
=
tool
.
Execute
(
JObject
.
Parse
(
"{'query':'库存不对'}"
),
MakeCtx
(
_ds
));
Assert
.
True
(
hit
.
Contains
(
"dialect/erp/winforms.md"
),
"应命中方言文件"
);
string
miss
=
tool
.
Execute
(
JObject
.
Parse
(
"{'query':'不存在的关键字xyz'}"
),
MakeCtx
(
_ds
));
Assert
.
True
(
miss
.
Contains
(
"未找到"
),
"未命中应明确说明"
);
}
[
Test
]
public
void
DGrep_GlobScope
()
{
var
tool
=
new
DialectGrepTool
();
string
hit
=
tool
.
Execute
(
JObject
.
Parse
(
"{'query':'飞织','glob':'industry/鞋业'}"
),
MakeCtx
(
_ds
));
Assert
.
True
(
hit
.
Contains
(
"运动鞋.md"
),
"限定行业目录应命中行业文件"
);
}
[
Test
]
public
void
DList_Roots
()
{
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
(
"鞋业"
),
"应含行业目录"
);
}
[
Test
]
public
void
DList_Subdir
()
{
var
tool
=
new
DialectListTool
();
string
result
=
tool
.
Execute
(
JObject
.
Parse
(
"{'path':'dialect/erp'}"
),
MakeCtx
(
_ds
));
Assert
.
True
(
result
.
Contains
(
"winforms.md"
),
"子目录应列出文件"
);
}
// ── PromptBuilder 方言层 ──
[
Test
]
public
void
PromptBuilder_InjectsDialectLayer
()
{
var
kb
=
new
KbService
();
var
pb
=
new
PromptBuilder
{
Dialect
=
_ds
};
string
msg
=
pb
.
BuildSystemMessage
(
kb
);
Assert
.
True
(
msg
.
Contains
(
"产品方言与行业知识"
),
"应注入方言层"
);
Assert
.
True
(
msg
.
Contains
(
"d_read"
),
"应提示 d_read 精读"
);
Assert
.
True
(
msg
.
Contains
(
"安全规则"
),
"安全层仍在"
);
}
[
Test
]
public
void
PromptBuilder_NoDialect_NoLayer
()
{
var
kb
=
new
KbService
();
var
pb
=
new
PromptBuilder
();
// Dialect = null
string
msg
=
pb
.
BuildSystemMessage
(
kb
);
Assert
.
False
(
msg
.
Contains
(
"产品方言与行业知识"
),
"未配置方言服务不应注入"
);
}
// ── 真实内置知识集成(AppHome 随仓库存在时)──
[
Test
]
public
void
RealAppHome_DialectAndIndustry_Available
()
{
string
agentRoot
=
Path
.
Combine
(
AppDomain
.
CurrentDomain
.
BaseDirectory
,
@"..\..\..\AppHome\Agent"
);
if
(!
Directory
.
Exists
(
agentRoot
))
return
;
// 仓库外运行则跳过(不视为失败)
var
ds
=
new
DialectService
(
agentRoot
);
Assert
.
True
(
ds
.
IsAvailable
,
"仓库内 AppHome/Agent 应含 dialect 或 industry"
);
var
d
=
ds
.
ScanDialect
();
Assert
.
True
(
d
.
Topics
.
Count
>=
10
,
"真实方言 INDEX 应有 ≥10 主题(实际 "
+
d
.
Topics
.
Count
+
")"
);
Assert
.
True
(
d
.
Topics
.
Any
(
t
=>
t
.
File
==
"erp/winforms.md"
),
"应含 erp/winforms.md"
);
var
inds
=
ds
.
ScanIndustries
().
ToList
();
Assert
.
Equal
(
1
,
inds
.
Count
,
"行业应只有鞋业一个"
);
Assert
.
Equal
(
"鞋业"
,
inds
[
0
].
Name
);
Assert
.
True
(
inds
[
0
].
Topics
.
Count
>=
15
,
"鞋业 INDEX 应有 ≥15 主题(实际 "
+
inds
[
0
].
Topics
.
Count
+
")"
);
Assert
.
True
(
inds
[
0
].
Topics
.
Any
(
t
=>
t
.
File
==
"01-品类认知/运动鞋.md"
),
"应含运动鞋主题(路径补全)"
);
Assert
.
True
(
inds
[
0
].
Topics
.
Any
(
t
=>
t
.
File
==
"01-品类认知/劳保鞋.md"
),
"应含劳保鞋主题(路径补全)"
);
Assert
.
True
(
inds
[
0
].
Topics
.
Any
(
t
=>
t
.
File
==
"01-品类认知/童鞋.md"
),
"应含童鞋主题(路径补全)"
);
string
summary
=
ds
.
BuildSummary
();
Assert
.
True
(
System
.
Text
.
Encoding
.
UTF8
.
GetByteCount
(
summary
)
<=
2048
,
"真实摘要应在 2KB 预算内"
);
// 真实内容精读
var
tool
=
new
DialectReadTool
();
string
r
=
tool
.
Execute
(
JObject
.
Parse
(
"{'path':'industry/鞋业/01-品类认知/劳保鞋.md'}"
),
MakeCtx
(
ds
));
Assert
.
True
(
r
.
Contains
(
"GB 21148"
),
"劳保鞋应含国标知识"
);
r
=
tool
.
Execute
(
JObject
.
Parse
(
"{'path':'dialect/erp/winforms.md'}"
),
MakeCtx
(
ds
));
Assert
.
True
(
r
.
Contains
(
"SysBusinessModel"
),
"winforms 方言应含链路查询"
);
}
}
}
AgentTest/TextEncodingTests.cs
0 → 100644
View file @
1ac5825c
using
System
;
using
System.IO
;
using
System.Linq
;
using
System.Text
;
using
Tools.MyAgent.Kb
;
using
Tools.MyUtils
;
namespace
AgentTest
{
/// <summary>
/// 编码识别与文件读取增强测试(迁移自源 AgentTests.ReadTextFile_Utf8Bom / ReadTextFile_Gb2312Fallback,
/// 并扩展:BOM/无 BOM 多编码判定、magic bytes 类型识别、编解码往返、Read 大小闸门、SplitLines、工作表前缀)。
/// </summary>
public
sealed
class
TextEncodingTests
:
TestRunner
{
private
string
_dir
;
public
override
void
SetUp
()
{
_dir
=
Path
.
Combine
(
Path
.
GetTempPath
(),
"agenttest_enc_"
+
Guid
.
NewGuid
().
ToString
(
"N"
));
Directory
.
CreateDirectory
(
_dir
);
}
public
override
void
TearDown
()
{
try
{
Directory
.
Delete
(
_dir
,
true
);
}
catch
{
}
}
private
string
TempFile
(
string
name
)
{
return
Path
.
Combine
(
_dir
,
name
);
}
// ── ReadTextFile 迁移用例(源 AgentTests:128/140)──
[
Test
]
public
void
ReadTextFile_Utf8Bom
()
{
string
f
=
TempFile
(
"u8.txt"
);
try
{
File
.
WriteAllText
(
f
,
"中文内容abc"
,
new
UTF8Encoding
(
true
));
Assert
.
Equal
(
"中文内容abc"
,
KbService
.
ReadTextFile
(
f
));
}
finally
{
File
.
Delete
(
f
);
}
}
[
Test
]
public
void
ReadTextFile_Gb2312Fallback
()
{
string
f
=
TempFile
(
"gb.txt"
);
try
{
// 用 GB2312 编码写入(无 BOM 的 UTF-8 解码将失败 → 回退 GB18030/GB2312)
var
gb
=
Encoding
.
GetEncoding
(
"GB2312"
);
File
.
WriteAllBytes
(
f
,
gb
.
GetBytes
(
"报销流程说明123"
));
string
text
=
KbService
.
ReadTextFile
(
f
);
Assert
.
True
(
text
.
Contains
(
"报销流程说明"
),
"GB2312 文件应被正确识别解码,实际:"
+
text
);
}
finally
{
File
.
Delete
(
f
);
}
}
// ── DetectEncoding:BOM 判定 ──
[
Test
]
public
void
DetectEncoding_Utf8Bom
()
{
var
enc
=
TextEncoding
.
DetectEncoding
(
Encoding
.
UTF8
.
GetPreamble
().
Concat
(
Encoding
.
UTF8
.
GetBytes
(
"中文"
)).
ToArray
());
Assert
.
Equal
(
65001
,
enc
.
CodePage
,
"UTF-8 BOM 应判为 UTF-8"
);
}
[
Test
]
public
void
DetectEncoding_Utf8NoBom
()
{
var
enc
=
TextEncoding
.
DetectEncoding
(
Encoding
.
UTF8
.
GetBytes
(
"中文内容abc"
));
Assert
.
Equal
(
65001
,
enc
.
CodePage
,
"无 BOM 合法 UTF-8 应判为 UTF-8"
);
}
[
Test
]
public
void
DetectEncoding_Utf16LeBom
()
{
byte
[]
bytes
=
{
0xFF
,
0xFE
,
0x2D
,
0x4E
};
Assert
.
Equal
(
1200
,
TextEncoding
.
DetectEncoding
(
bytes
).
CodePage
,
"FF FE 应判为 UTF-16 LE"
);
}
[
Test
]
public
void
DetectEncoding_Utf16BeBom
()
{
byte
[]
bytes
=
{
0xFE
,
0xFF
,
0x4E
,
0x2D
};
Assert
.
Equal
(
1201
,
TextEncoding
.
DetectEncoding
(
bytes
).
CodePage
,
"FE FF 应判为 UTF-16 BE"
);
}
[
Test
]
public
void
DetectEncoding_Utf32LeBom
()
{
byte
[]
bytes
=
{
0xFF
,
0xFE
,
0x00
,
0x00
,
0x2D
,
0x4E
,
0x00
,
0x00
};
Assert
.
Equal
(
12000
,
TextEncoding
.
DetectEncoding
(
bytes
).
CodePage
,
"FF FE 00 00 应判为 UTF-32 LE(先于 UTF-16 LE)"
);
}
[
Test
]
public
void
DetectEncoding_Utf32BeBom
()
{
byte
[]
bytes
=
{
0x00
,
0x00
,
0xFE
,
0xFF
,
0x00
,
0x00
,
0x4E
,
0x2D
};
Assert
.
Equal
(
12001
,
TextEncoding
.
DetectEncoding
(
bytes
).
CodePage
,
"00 00 FE FF 应判为 UTF-32 BE"
);
}
// ── DetectEncoding:无 BOM UTF-16 启发式 ──
[
Test
]
public
void
DetectEncoding_Utf16LeNoBom
()
{
byte
[]
bytes
=
Encoding
.
Unicode
.
GetBytes
(
"中文内容abc"
);
// 无 BOM 的 UTF-16 LE(中文为主)
Assert
.
Equal
(
1200
,
TextEncoding
.
DetectEncoding
(
bytes
).
CodePage
,
"无 BOM UTF-16 LE(中文内容)应启发式判定"
);
}
[
Test
]
public
void
DetectEncoding_Utf16BeNoBom
()
{
byte
[]
bytes
=
Encoding
.
BigEndianUnicode
.
GetBytes
(
"中文内容abc"
);
Assert
.
Equal
(
1201
,
TextEncoding
.
DetectEncoding
(
bytes
).
CodePage
,
"无 BOM UTF-16 BE(中文内容)应启发式判定"
);
}
[
Test
]
public
void
DetectEncoding_Ascii_DoesNotTriggerUtf16
()
{
// 纯 ASCII(UTF-8 单字节,无 NUL):不应误判为 UTF-16,也不应判为 GB18030 以外编码
byte
[]
bytes
=
Encoding
.
ASCII
.
GetBytes
(
"hello world 123"
);
var
enc
=
TextEncoding
.
DetectEncoding
(
bytes
);
Assert
.
Equal
(
65001
,
enc
.
CodePage
,
"纯 ASCII 应判为 UTF-8(GB18030 解码结果一致)"
);
}
// ── DetectEncoding:严格 UTF-8 校验拒绝 ──
[
Test
]
public
void
DetectEncoding_Utf8_Overlong_Rejected
()
{
byte
[]
bytes
=
{
0xC0
,
0xAF
};
// overlong 2 字节(/ 的 2 字节编码)
var
enc
=
TextEncoding
.
DetectEncoding
(
bytes
);
Assert
.
False
(
enc
.
CodePage
==
65001
,
"overlong 序列不应判为 UTF-8"
);
}
[
Test
]
public
void
DetectEncoding_Utf8_BadContinuation_Rejected
()
{
byte
[]
bytes
=
{
0xE4
,
0xB8
,
0x41
};
// 中 的首字节 + 非法续字节 0x41
var
enc
=
TextEncoding
.
DetectEncoding
(
bytes
);
Assert
.
False
(
enc
.
CodePage
==
65001
,
"非法续字节不应判为 UTF-8"
);
}
[
Test
]
public
void
DetectEncoding_Utf8_Surrogate_Rejected
()
{
byte
[]
bytes
=
{
0xED
,
0xA0
,
0x80
};
// UTF-16 代理区(D800)的 UTF-8 编码
var
enc
=
TextEncoding
.
DetectEncoding
(
bytes
);
Assert
.
False
(
enc
.
CodePage
==
65001
,
"代理区编码不应判为 UTF-8"
);
}
[
Test
]
public
void
DetectEncoding_Utf8_Truncated_Rejected
()
{
byte
[]
bytes
=
{
0xE4
,
0xB8
};
// 截断的 3 字节序列
var
enc
=
TextEncoding
.
DetectEncoding
(
bytes
);
Assert
.
False
(
enc
.
CodePage
==
65001
,
"截断序列不应判为 UTF-8"
);
}
// ── DetectEncoding:GB18030 / 回退 ──
[
Test
]
public
void
DetectEncoding_Gb2312
()
{
var
gb
=
Encoding
.
GetEncoding
(
"GB2312"
);
byte
[]
bytes
=
gb
.
GetBytes
(
"报销流程说明123"
);
Assert
.
Equal
(
54936
,
TextEncoding
.
DetectEncoding
(
bytes
).
CodePage
,
"GB2312 中文应回退判为 GB18030(超集)"
);
}
[
Test
]
public
void
DetectEncoding_Gb18030
()
{
// 生僻字 U+20000(𠀀):GB18030 四字节 95 32 82 36,GB2312 无法编码
byte
[]
bytes
=
{
0x95
,
0x32
,
0x82
,
0x36
};
var
enc
=
TextEncoding
.
DetectEncoding
(
bytes
);
Assert
.
Equal
(
54936
,
enc
.
CodePage
,
"GB18030 生僻字四字节序列应判为 GB18030"
);
Assert
.
Equal
(
"\uD840\uDC00"
,
enc
.
GetString
(
bytes
),
"GB18030 解码生僻字应正确"
);
}
[
Test
]
public
void
DetectEncoding_Binary_FallsBackToDefault
()
{
// 0xFF 在 GB18030 中非法、无 NUL(不触发 UTF-16 启发式)→ 最终回退系统 ANSI
byte
[]
bytes
=
{
0xFF
,
0xFF
,
0x11
,
0x22
};
Assert
.
Equal
(
Encoding
.
Default
.
CodePage
,
TextEncoding
.
DetectEncoding
(
bytes
).
CodePage
,
"二进制内容应回退系统 ANSI"
);
}
[
Test
]
public
void
DetectEncoding_Empty
()
{
Assert
.
Equal
(
65001
,
TextEncoding
.
DetectEncoding
(
new
byte
[
0
]).
CodePage
,
"空字节应返回 UTF-8 无 BOM"
);
Assert
.
Equal
(
65001
,
TextEncoding
.
DetectEncoding
(
null
).
CodePage
,
"null 应返回 UTF-8 无 BOM"
);
}
// ── ReadAllTextAuto / ReadTextFile:多编码整读 ──
[
Test
]
public
void
ReadAllTextAuto_RoundTrip
()
{
string
text
=
"中文内容 abc 123。"
;
var
cases
=
new
System
.
Collections
.
Generic
.
Dictionary
<
string
,
Encoding
>
{
{
"rt_u8bom.txt"
,
new
UTF8Encoding
(
true
)
},
{
"rt_u8nobom.txt"
,
new
UTF8Encoding
(
false
)
},
{
"rt_u16le.txt"
,
new
UnicodeEncoding
(
false
,
true
)
},
{
"rt_u16be.txt"
,
new
UnicodeEncoding
(
true
,
true
)
},
{
"rt_u32le.txt"
,
new
UTF32Encoding
(
false
,
true
)
},
{
"rt_u32be.txt"
,
new
UTF32Encoding
(
true
,
true
)
},
{
"rt_gb.txt"
,
Encoding
.
GetEncoding
(
"GB2312"
)
},
};
foreach
(
var
kv
in
cases
)
{
string
f
=
TempFile
(
kv
.
Key
);
try
{
File
.
WriteAllBytes
(
f
,
kv
.
Value
.
GetPreamble
().
Concat
(
kv
.
Value
.
GetBytes
(
text
)).
ToArray
());
string
actual
=
TextEncoding
.
ReadAllTextAuto
(
f
);
Assert
.
Equal
(
text
,
actual
,
kv
.
Key
+
" 往返读取应一致"
);
Assert
.
Equal
(
text
,
KbService
.
ReadTextFile
(
f
),
kv
.
Key
+
" KbService.ReadTextFile 应一致"
);
}
finally
{
File
.
Delete
(
f
);
}
}
}
[
Test
]
public
void
ReadAllTextAuto_Utf16LeNoBom
()
{
string
f
=
TempFile
(
"u16lenobom.txt"
);
try
{
File
.
WriteAllBytes
(
f
,
Encoding
.
Unicode
.
GetBytes
(
"中文内容abc"
));
Assert
.
Equal
(
"中文内容abc"
,
TextEncoding
.
ReadAllTextAuto
(
f
),
"无 BOM UTF-16 LE 应正确读取"
);
Assert
.
Equal
(
"中文内容abc"
,
KbService
.
ReadTextFile
(
f
),
"KbService 无 BOM UTF-16 LE 应正确读取"
);
}
finally
{
File
.
Delete
(
f
);
}
}
[
Test
]
public
void
ReadAllTextAuto_EmptyFile
()
{
string
f
=
TempFile
(
"empty.txt"
);
try
{
File
.
WriteAllBytes
(
f
,
new
byte
[
0
]);
Assert
.
Equal
(
""
,
TextEncoding
.
ReadAllTextAuto
(
f
),
"空文件应返回空串"
);
}
finally
{
File
.
Delete
(
f
);
}
}
[
Test
]
public
void
ReadAllTextAuto_MissingFile_ReturnsNull
()
{
Assert
.
Null
(
TextEncoding
.
ReadAllTextAuto
(
Path
.
Combine
(
_dir
,
"not_exist.txt"
)),
"文件不存在应返回 null"
);
Assert
.
Null
(
KbService
.
ReadTextFile
(
Path
.
Combine
(
_dir
,
"not_exist.txt"
)),
"KbService 文件不存在应返回 null"
);
}
// ── GetFileType:magic bytes ──
[
Test
]
public
void
GetFileType_MagicBytes
()
{
// 无扩展名文件 → 强制走 magic bytes 分支(扩展名非空时 GetFileType 直接返回扩展名,见 GetFileType_WithExtension)
var
cases
=
new
System
.
Collections
.
Generic
.
Dictionary
<
byte
[],
string
>
{
{
TextEncoding
.
FromHex
(
"89504E470D0A1A0A0000000D"
),
"png"
},
// PNG
{
TextEncoding
.
FromHex
(
"FFD8FFE000104A4649460001"
),
"jpg"
},
// JPEG
{
TextEncoding
.
FromHex
(
"474946383961000000"
),
"gif"
},
// GIF89a
{
TextEncoding
.
FromHex
(
"424D363400000000"
),
"bmp"
},
// BM
{
TextEncoding
.
FromHex
(
"49492A0008000000"
),
"tiff"
},
// II*
{
TextEncoding
.
FromHex
(
"3842505301000000"
),
"psd"
},
// 8BPS
{
TextEncoding
.
FromHex
(
"255044462D312E340A25E2E3CF"
),
"pdf"
},
// %PDF-1.4
{
TextEncoding
.
FromHex
(
"504B0304140006000800"
),
"zip/xlsx/docx"
},
// PK zip
{
TextEncoding
.
FromHex
(
"D0CF11E0A1B11AE100"
),
"xls/doc"
},
// OLE2
{
TextEncoding
.
FromHex
(
"526172211A0700CF9073"
),
"rar"
},
// RAR
{
TextEncoding
.
FromHex
(
"7B5C727466315C616E7369"
),
"rtf"
},
// {\rtf1\ansi
{
TextEncoding
.
FromHex
(
"3C3F786D6C2076657273696F6E"
),
"xml"
},
// <?xml version
{
TextEncoding
.
FromHex
(
"68746D6C3E0D0A3C68656164"
),
"html"
},
// <html>\r\n<head
{
TextEncoding
.
FromHex
(
"4D5A9000030000000400"
),
"exe/dll/drv/vxd/sys/ocx/vbx"
},
// MZ
};
int
i
=
0
;
foreach
(
var
kv
in
cases
)
{
string
f
=
TempFile
(
"noext_"
+
(
i
++));
try
{
File
.
WriteAllBytes
(
f
,
kv
.
Key
);
Assert
.
Equal
(
kv
.
Value
,
TextEncoding
.
GetFileType
(
f
),
"文件头 "
+
TextEncoding
.
ToHex
(
kv
.
Key
,
4
)
+
" 应判为 "
+
kv
.
Value
);
}
finally
{
File
.
Delete
(
f
);
}
}
}
[
Test
]
public
void
GetFileType_NoExtension_UsesMagicBytes
()
{
string
f
=
TempFile
(
"noext_png"
);
try
{
File
.
WriteAllBytes
(
f
,
TextEncoding
.
FromHex
(
"89504E470D0A1A0A0000000D"
));
Assert
.
Equal
(
"png"
,
TextEncoding
.
GetFileType
(
f
),
"无扩展名应通过 magic bytes 判为 png"
);
}
finally
{
File
.
Delete
(
f
);
}
}
[
Test
]
public
void
GetFileType_WithExtension_ReturnsExtension
()
{
string
f
=
TempFile
(
"data.TXT"
);
try
{
File
.
WriteAllText
(
f
,
"hello"
);
Assert
.
Equal
(
"txt"
,
TextEncoding
.
GetFileType
(
f
),
"扩展名非空应直接返回扩展名"
);
}
finally
{
File
.
Delete
(
f
);
}
}
[
Test
]
public
void
GetFileType_UnknownNoExt
()
{
string
f
=
TempFile
(
"noext_unknown"
);
try
{
File
.
WriteAllBytes
(
f
,
new
byte
[]
{
0x11
,
0x22
,
0x33
,
0x44
,
0x55
,
0x66
,
0x77
});
Assert
.
Equal
(
"unknown"
,
TextEncoding
.
GetFileType
(
f
));
}
finally
{
File
.
Delete
(
f
);
}
}
[
Test
]
public
void
GetFileType_MissingFile
()
{
Assert
.
Equal
(
"unknown"
,
TextEncoding
.
GetFileType
(
Path
.
Combine
(
_dir
,
"missing"
)),
"文件不存在应返回 unknown"
);
}
// ── 编解码工具 ──
[
Test
]
public
void
Codec_Utf8_Unicode_Gb2312_RoundTrip
()
{
string
s
=
"编码工具测试 123"
;
Assert
.
Equal
(
s
,
TextEncoding
.
UTF8
(
TextEncoding
.
UTF8
(
s
)));
Assert
.
Equal
(
s
,
TextEncoding
.
Unicode
(
TextEncoding
.
Unicode
(
s
)));
Assert
.
Equal
(
s
,
TextEncoding
.
GB2312
(
TextEncoding
.
GB2312
(
s
)));
// 中文 Windows ANSI = GBK:GB2312 编码字节应与 GetEncoding("gb2312") 一致(byte[] 用 ToHex 比较,避免 ToString 恒等的无效断言)
Assert
.
Equal
(
TextEncoding
.
ToHex
(
Encoding
.
GetEncoding
(
"gb2312"
).
GetBytes
(
s
)),
TextEncoding
.
ToHex
(
TextEncoding
.
GB2312
(
s
)),
"GB2312 编码字节应与 GetEncoding(gb2312) 一致"
);
}
[
Test
]
public
void
Hex_RoundTrip
()
{
byte
[]
data
=
{
0x00
,
0x01
,
0x0A
,
0xFF
,
0x80
,
0x2E
};
Assert
.
Equal
(
"00010AFF802E"
,
TextEncoding
.
ToHex
(
data
));
// byte[] 比较用 ToHex 归一,避免 Assert.Equal 的 ToString 恒等("System.Byte[]") 造成无效断言
Assert
.
Equal
(
TextEncoding
.
ToHex
(
data
),
TextEncoding
.
ToHex
(
TextEncoding
.
FromHex
(
"00010AFF802E"
)),
"FromHex→ToHex 往返应一致"
);
Assert
.
Equal
(
TextEncoding
.
ToHex
(
data
),
TextEncoding
.
ToHex
(
TextEncoding
.
FromHex
(
" 00010AFF802E "
)),
"应忽略首尾空白"
);
Assert
.
Equal
(
""
,
TextEncoding
.
ToHex
(
null
),
"null 应返回空串"
);
Assert
.
Equal
(
0
,
TextEncoding
.
FromHex
(
""
).
Length
,
"空串应返回空数组"
);
Assert
.
Throws
<
FormatException
>(()
=>
TextEncoding
.
FromHex
(
"ABC"
),
"奇数长度应抛 FormatException"
);
}
// ── SplitLines 统一行切分 ──
[
Test
]
public
void
SplitLines_AllNewlineStyles
()
{
string
[]
a
=
KbService
.
SplitLines
(
"a\r\nb\rc\nd"
);
Assert
.
Equal
(
4
,
a
.
Length
,
"CRLF/CR/LF 应归一为 4 行"
);
Assert
.
Equal
(
"a"
,
a
[
0
]);
Assert
.
Equal
(
"b"
,
a
[
1
]);
Assert
.
Equal
(
"c"
,
a
[
2
]);
Assert
.
Equal
(
"d"
,
a
[
3
]);
Assert
.
Equal
(
1
,
KbService
.
SplitLines
(
null
).
Length
,
"null 应归一为单行空串"
);
Assert
.
Equal
(
1
,
KbService
.
SplitLines
(
""
).
Length
);
Assert
.
Equal
(
3
,
KbService
.
SplitLines
(
"\r\n\r\n"
).
Length
,
"连续空行应保留"
);
}
// ── ReadExcel 工作表名证据前缀 ──
[
Test
]
public
void
ReadExcel_SheetNamePrefix
()
{
string
f
=
TempFile
(
"book.xlsx"
);
try
{
var
wb
=
new
NPOI
.
XSSF
.
UserModel
.
XSSFWorkbook
();
var
sh
=
wb
.
CreateSheet
(
"销售明细"
);
sh
.
CreateRow
(
0
).
CreateCell
(
0
).
SetCellValue
(
"日期"
);
sh
.
CreateRow
(
1
).
CreateCell
(
0
).
SetCellValue
(
"2024-01-01"
);
using
(
var
fs
=
File
.
Create
(
f
))
wb
.
Write
(
fs
);
wb
.
Close
();
string
text
=
KbService
.
ReadExcel
(
f
);
Assert
.
True
(
text
.
StartsWith
(
"【工作表:销售明细】"
),
"输出应带工作表名证据前缀,实际:"
+
(
text
??
"(null)"
).
Substring
(
0
,
Math
.
Min
(
30
,
(
text
??
""
).
Length
)));
Assert
.
True
(
text
.
Contains
(
"日期"
),
"单元格内容应正常提取"
);
}
finally
{
try
{
File
.
Delete
(
f
);
}
catch
{
}
}
}
// ── Read 文件大小闸门(> 30MB 拒绝整读,稀疏文件不占真实磁盘)──
[
Test
]
public
void
Read_SizeGate_RejectsOversize
()
{
string
f
=
TempFile
(
"big.txt"
);
try
{
using
(
var
fs
=
new
FileStream
(
f
,
FileMode
.
CreateNew
,
FileAccess
.
Write
))
fs
.
SetLength
(
31L
*
1024
*
1024
);
// 31MB 稀疏文件 > 30MB 闸门
var
kb
=
new
KbService
();
kb
.
SetRoot
(
_dir
);
string
result
=
kb
.
Read
(
"big.txt"
,
1
,
10
);
Assert
.
True
(
result
.
Contains
(
"文件过大"
),
"超限文件应拒绝整读并给出提示,实际:"
+
result
);
Assert
.
True
(
result
.
Contains
(
"kb_grep"
),
"提示应给出替代路径 kb_grep"
);
}
finally
{
try
{
File
.
Delete
(
f
);
}
catch
{
}
}
}
[
Test
]
public
void
Read_SmallFile_ReadsNormally
()
{
string
f
=
TempFile
(
"small.txt"
);
try
{
File
.
WriteAllText
(
f
,
"第一行\n第二行\n第三行"
);
var
kb
=
new
KbService
();
kb
.
SetRoot
(
_dir
);
string
result
=
kb
.
Read
(
"small.txt"
,
1
,
2
);
Assert
.
True
(
result
.
Contains
(
"第一行"
),
"小文件应正常读取,实际:"
+
result
);
Assert
.
True
(
result
.
Contains
(
"第二行"
));
Assert
.
False
(
result
.
Contains
(
"第三行"
),
"limit=2 不应读到第三行"
);
}
finally
{
File
.
Delete
(
f
);
}
}
// ── Read 提示语(图片 / 解析失败 → kb_view_image)──
[
Test
]
public
void
Read_ImageFile_HintsKbViewImage
()
{
string
f
=
TempFile
(
"pic.png"
);
try
{
File
.
WriteAllBytes
(
f
,
TextEncoding
.
FromHex
(
"89504E470D0A1A0A0000000D"
));
var
kb
=
new
KbService
();
kb
.
SetRoot
(
_dir
);
string
result
=
kb
.
Read
(
"pic.png"
,
1
,
10
);
Assert
.
True
(
result
.
Contains
(
"kb_view_image"
),
"图片文件应提示 kb_view_image,实际:"
+
result
);
}
finally
{
File
.
Delete
(
f
);
}
}
[
Test
]
public
void
Read_UnparsableFile_HintsKbViewImage
()
{
string
f
=
TempFile
(
"broken.docx"
);
try
{
File
.
WriteAllBytes
(
f
,
new
byte
[]
{
0x00
,
0xFF
,
0x11
,
0x00
});
// 损坏的 docx(非 zip):解析器返回 null
var
kb
=
new
KbService
();
kb
.
SetRoot
(
_dir
);
string
result
=
kb
.
Read
(
"broken.docx"
,
1
,
10
);
Assert
.
True
(
result
.
Contains
(
"kb_view_image"
),
"不可解析文件应提示 kb_view_image,实际:"
+
result
);
}
finally
{
File
.
Delete
(
f
);
}
}
}
}
AppHome/Agent/dialect/INDEX.md
0 → 100644
View file @
1ac5825c
# 产品排查方言(内化版)
行业简介: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-常见问题
> 本地沉淀区:排查确认的典型问题经维护者整理追加到本大类(内置目录只读,禁止智能体直接写入)。
AppHome/Agent/dialect/erp/aspnet.md
0 → 100644
View file @
1ac5825c
# 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 天)
-
日志写入失败不影响主业务(静默忽略)
AppHome/Agent/dialect/erp/cases/hotspots.md
0 → 100644
View file @
1ac5825c
# 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 兼容、后缀归一化)与页签覆盖范围。
AppHome/Agent/dialect/erp/grid-filter.md
0 → 100644
View file @
1ac5825c
# 网格过滤联动机制(GridView / PivotGrid)
> WinForms 客户端有两种分析表格,均支持复选框过滤。但普通表格有**联动效果**(如部门过滤后,工艺过滤框只显示该部门的工艺),多维表格原生无联动,需额外实现。
## 普通分析表格(GridView)— GridViewFilterHelper
**入口**
:
`U.WinCtl.OpCtl.UP4XtraGrid.AddToGrid(grdXtrMainGrid, dsDetail, busObj)`
→ 自动附加
`GridViewFilterHelper`
**核心文件**
:
| 文件 | 位置 | 作用 |
|------|------|------|
|
`GridViewFilterHelper.cs`
|
`U.WinCtl\OpCtl\` | 复选框过滤 + 联动核心类 |
| `
UP4XtraGrid.cs
` | `
U.WinCtl
\O
pCtl
\`
| 在
`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
\P
ivotGrid
\`
| U10 版本(
**未实现联动**
) |
|
`PivotGridEx.cs`
|
`XWinLib\PivotGrid\` | PivotGrid 控件包装器(继承 DevExpress PivotGridControl) |
| `
GridViewFilterHelper.cs
` | `
U.WinCtl
\O
pCtl
\`
| 参考实现(仅 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)
```
AppHome/Agent/dialect/erp/parent-provider.md
0 → 100644
View file @
1ac5825c
# 父子单据联动机制(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 返回值能否精确匹配。
AppHome/Agent/dialect/erp/special-modules.md
0 → 100644
View file @
1ac5825c
# 报表类模块 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)
```
AppHome/Agent/dialect/erp/sql-script-location.md
0 → 100644
View file @
1ac5825c
# SQL 脚本存放位置与项目规范(公司方言)
> 通用规范(格式化 CRLF、脚本命名、可重复执行模板、落盘 `Agent/output/sql/`)见 `dialect/sql-core`(sql-style.md / sql-scripts.md)。
> 本文件只讲**公司/项目级**约定:脚本在项目内的存放位置、命名、数据库说明、变更注释、编码。
## 职责分工(与 dialect/sql-core 的 sql-scripts.md 区分)
-
`dialect/sql-core/sql-scripts.md`
=
**生成规范**
:agent 生成脚本时的命名/幂等/模板,落盘
`Agent/output/sql/`
(kb_write 沙箱内,agent 可直接写)。
-
本文件 =
**交付位置规范**
:脚本最终应放入项目仓库的
`{项目}/开发文档/数据库脚本/`
。agent 无该项目目录写权限,
**交付脚本时向用户说明应存放的位置与命名,由用户放置**
(或用户确认后自行复制)。
-
两者不冲突:agent 先按 sql-scripts.md 生成到沙箱,交付时按本文件告知用户归档位置。
## 一、脚本存放位置(核心原则)
所有 SQL 脚本统一存放在对应项目的开发脚本目录下:
```
{项目根目录}/开发文档/数据库脚本/
```
> 路径定位:按当前分析的项目确认路径。如排查 `U6_FD` 的问题,脚本应放到 `d:\working\source\U6_FD\开发文档\数据库脚本\`。
### 1.1 普通脚本(建表、加列、加模块、DML 等)
按
**日期创建文件夹**
,按
**序号 + 功能**
命名:
```
{项目}/开发文档/数据库脚本/
├── yyyy-MM-dd/ ← 日期文件夹(脚本执行日期)
│ ├── 01.AddTable_xxx.sql ← 建表
│ ├── 02.AddTableColumn_xxx.sql ← 加列
│ ├── 03.AddModule_xxx.sql ← 添加模块/窗口注册
│ ├── 04.SP_xxx.sql ← 新增存储过程(非改动的存储过程)
│ ├── 05.Data_xxx.sql ← 数据修复/迁移脚本
│ └── ...
```
**规则**
:
-
日期格式统一用
`yyyy-MM-dd`
(如
`2026-07-30`
)
-
**序号从 `01` 开始**
,两位数字,按执行顺序递增
-
功能名用英文 + 下划线,简洁描述脚本用途
-
同一批次(同一天)的脚本放在同一个日期文件夹下
-
序号决定执行顺序:先建表 → 加列 → 加模块 → 存储过程 → 数据修复
**示例**
:
```
2026-07-30/
├── 01.AddTable_AIQC_Inspector.sql
├── 02.AddTableColumn_SysFunction_MapXMLName.sql
├── 03.AddModule_行为引导报表.sql
└── 04.Data_Fix_DuplicateRecords.sql
```
### 1.2 视图 / 存储过程 / 函数(可复用对象)
存放在
`2099-01-01`
或
`2999-01-01`
文件夹下(表示「永不过期」的持久化对象),按类型分子目录:
```
{项目}/开发文档/数据库脚本/
├── 2099-01-01/ (或 2999-01-01/)
│ ├── 20.VW/ ← 视图(View)
│ ├── 30.SP/ ← 存储过程(Stored Procedure)
│ └── 30.FN/ ← 函数(Function)
```
> **2099 vs 2999 选择**:优先与该项目**已有**的特殊日期文件夹保持一致。
| 项目 | 使用 |
|------|------|
| cloud |
`2999-01-01`
|
| PDM_LN |
`2999-01-01`
|
| LNCenterApi |
`2999-01-01`
|
| U6_HY |
`2999-01-01`
|
| MES_OneMix |
`2099-01-01`
|
| mes_geshiwei |
`2099-01-01`
|
| u6_huachuan |
`2099-01-01`
|
**若该项目尚无此目录**
:新建
`2999-01-01`
(默认)。
### 1.3 视图 / 存储过程命名
```
格式:{yyyyMMdd}{序号}_{对象名称}.sql
其中:
yyyyMMdd = 修改日期(8位,如 20260730)
序号 = 当天序号(3位从001开始,如超过999用4位)
对象名称 = VW_ViewName / SP_ProcName / FN_FuncName
```
**示例**
:
```
2099-01-01/30.SP/20260730001.SP_MES_GetPartQRInfoForSubmit.sql
2099-01-01/20.VW/20260730001.VW_MX_SoleItemDetail.sql
2099-01-01/30.FN/20260730001.FN_ExtractDecimalNumber.sql
```
### 1.4 命名冲突处理
如果该对象名称(如
`VW_MX_SoleItemDetail`
)在目标文件夹下已
**存在文件**
:
1.
**更新前缀**
:将旧文件名中的
`yyyyMMddNNN`
前缀改为本次修改日期和最新序号
2.
**更新内容**
:将文件内容更新为本次修改后的最新版本
3.
**旧文件不保留**
:每次一个对象只保留
**最新一个**
版本文件(Git 历史可追溯旧版本)
**示例**
:
```
旧文件:20260303001.VW_MX_SoleItemDetail.sql
新序号:20260730003
旧文件 → 重命名为 → 20260730003.VW_MX_SoleItemDetail.sql,同时更新文件内容
```
**注意**
:重命名前需检查旧文件的
`yyyyMMddNNN`
前缀序号是否已被当天其他文件占用,跳过已用序号。
---
## 二、脚本内容规范
### 2.1 可重复执行(强制规则)
**所有 SQL 脚本必须能够无条件重复执行,不会因重复运行而报错。**
#### 普通脚本(DDL / DML)
```
sql
/* ✅ 建表:先判断再创建 */
IF
OBJECT_ID
(
'dbo.TableName'
,
'U'
)
IS
NULL
BEGIN
CREATE
TABLE
[
dbo
].[
TableName
]
(
...
);
END
GO
/* ✅ 加列:先判断再添加 */
IF
NOT
EXISTS
(
SELECT
*
FROM
sys
.
columns
WHERE
object_id
=
OBJECT_ID
(
'dbo.TableName'
)
AND
name
=
'ColumnName'
)
BEGIN
ALTER
TABLE
[
dbo
].[
TableName
]
ADD
[
ColumnName
]
INT
NULL
;
END
GO
/* ✅ 加模块:先判断再插入 */
IF
NOT
EXISTS
(
SELECT
*
FROM
dbo
.
App_SysBusinessModel
WHERE
Name
=
'模块名称'
)
BEGIN
INSERT
INTO
dbo
.
App_SysBusinessModel
(...)
VALUES
(...);
END
GO
```
**完整示例**
:
```
sql
/* 建表:可重复执行 */
IF
OBJECT_ID
(
'dbo.AIQC_Inspector'
,
'U'
)
IS
NULL
BEGIN
CREATE
TABLE
[
dbo
].[
AIQC_Inspector
]
(
[
Id
]
INT
IDENTITY
(
1
,
1
)
PRIMARY
KEY
,
[
LineName
]
NVARCHAR
(
50
)
NOT
NULL
,
[
DeviceCode
]
NVARCHAR
(
50
)
NOT
NULL
,
[
UserID
]
NVARCHAR
(
50
)
NOT
NULL
,
[
UserName
]
NVARCHAR
(
100
)
NOT
NULL
,
[
Remark
]
NVARCHAR
(
500
)
NULL
);
/* 表说明 */
EXEC
dbo
.
sp_tools_AddTableDescription
@
TableName
=
N
'AIQC_Inspector'
,
@
Desctription
=
N
'流水线与质检员映射关系表'
;
/* 列说明 */
EXEC
dbo
.
sp_tools_AddTableClumnDescription
@
TableName
=
N
'AIQC_Inspector'
,
@
ClumnName
=
N
'Id'
,
@
Desctription
=
N
'自增主键'
;
EXEC
dbo
.
sp_tools_AddTableClumnDescription
@
TableName
=
N
'AIQC_Inspector'
,
@
ClumnName
=
N
'LineName'
,
@
Desctription
=
N
'流水线名称'
;
END
ELSE
BEGIN
PRINT
'表 [dbo].[AIQC_Inspector] 已存在,跳过建表。'
;
END
GO
```
#### 视图 / 存储过程 / 函数
```
sql
/* ✅ 存储过程:先删除再创建 */
IF
OBJECT_ID
(
'dbo.SP_ProcName'
)
IS
NOT
NULL
DROP
PROCEDURE
[
dbo
].[
SP_ProcName
];
GO
CREATE
PROCEDURE
[
dbo
].[
SP_ProcName
]
...
GO
/* ✅ 视图:先删除再创建 */
IF
OBJECT_ID
(
'dbo.VW_ViewName'
)
IS
NOT
NULL
DROP
VIEW
[
dbo
].[
VW_ViewName
];
GO
CREATE
VIEW
[
dbo
].[
VW_ViewName
]
...
GO
```
### 2.2 建表 / 加列必须同步数据库说明(强制规则)
**建表时**
必须调用
`sp_tools_AddTableDescription`
添加表说明,调用
`sp_tools_AddTableClumnDescription`
添加每一列的说明。
**加列时**
必须调用
`sp_tools_AddTableClumnDescription`
添加列说明:
```
sql
/* 加列(使用项目提供的存储过程,已内置重复检查) */
EXEC
dbo
.
sp_tools_AddTableClumn
@
TableName
=
'SysFunction'
,
@
ClumnName
=
'MapXMLName'
,
@
TypeDefinition
=
'nvarchar(500)'
,
@
Desctription
=
'归属XML'
;
GO
/* 或手动写法——自行检查时也需添加说明 */
IF
NOT
EXISTS
(
SELECT
*
FROM
sys
.
columns
WHERE
object_id
=
OBJECT_ID
(
'dbo.SysFunction'
)
AND
name
=
'MapXMLName'
)
BEGIN
ALTER
TABLE
[
dbo
].[
SysFunction
]
ADD
[
MapXMLName
]
NVARCHAR
(
500
)
NULL
;
EXEC
dbo
.
sp_tools_AddTableClumnDescription
@
TableName
=
N
'SysFunction'
,
@
ClumnName
=
N
'MapXMLName'
,
@
Desctription
=
N
'归属XML'
;
END
GO
```
> **注意**:`sp_tools_AddTableClumn` 是项目自定义存储过程,内部已判断列是否存在,可安全重复执行。若项目无此 SP,需自行用 `IF NOT EXISTS` 包装。
### 2.3 存储过程 / 视图:最小化改动 + 注释规范
**原则**
:只改需要改的部分,保持原有逻辑不变。在文件顶部添加变更说明注释块。
#### 文件头部注释块(顶部)
> 模板对标 SSMS 生成脚本格式,使用 `/* ===== */` 风格的可视分隔块。
```
sql
/* ===== CREATE PROCEDURE: dbo.SP_MES_GetPartQRInfoForSubmit ===== */
/* 变更说明:[任务编号]变更内容摘要 */
/* 负责人:duan | 2026-07-30 14:25:30 */
/* ===== */
IF
OBJECT_ID
(
'dbo.SP_MES_GetPartQRInfoForSubmit'
)
IS
NOT
NULL
DROP
PROCEDURE
[
dbo
].[
SP_MES_GetPartQRInfoForSubmit
];
GO
SET
ANSI_NULLS
ON
;
GO
SET
QUOTED_IDENTIFIER
ON
;
GO
-- =============================================
-- Author: 原始作者
-- Create date: 原始创建日期
-- Description: 原始功能描述
-- 变更:[任务编号]变更内容摘要 2026-07-30 14:25:30 duan
-- =============================================
CREATE
PROCEDURE
[
dbo
].[
SP_MES_GetPartQRInfoForSubmit
]
...
GO
```
#### 变更注释格式
| 元素 | 格式 | 示例 |
|------|------|------|
|
**有任务编号**
|
`-- 变更:{任务编号}{描述} {时间} {负责人}`
|
`-- 变更:#24680 修正颜色匹配逻辑 2026-07-30 14:25:30 duan`
|
|
**无任务编号**
|
`-- 变更:{描述} {时间} {负责人}`
|
`-- 变更:修正颜色匹配逻辑,改为模糊匹配 2026-07-30 14:25:30 duan`
|
|
**时间精度**
|
`yyyy-MM-dd HH:mm:ss`
(精确到秒) |
`2026-07-30 14:25:30`
|
#### 视图注释模板
```
sql
/* ===== CREATE VIEW: dbo.VW_MX_SoleItemDetail ===== */
/* 变更说明:#24680 修正鞋底物料明细视图,新增 MaterialHardness 字段 */
/* 负责人:duan | 2026-07-30 14:25:30 */
/* ===== */
IF
OBJECT_ID
(
'dbo.VW_MX_SoleItemDetail'
)
IS
NOT
NULL
DROP
VIEW
[
dbo
].[
VW_MX_SoleItemDetail
];
GO
SET
ANSI_NULLS
ON
;
GO
SET
QUOTED_IDENTIFIER
ON
;
GO
-- =============================================
-- Author: lxj
-- Create date: 2025-04-22 17:00:06
-- Description: 鞋底物料明细信息(合并配方之后)
-- 变更:#24680 新增 MaterialHardness 字段 2026-07-30 14:25:30 duan
-- =============================================
CREATE
VIEW
[
dbo
].[
VW_MX_SoleItemDetail
]
AS
...
GO
```
### 2.4 格式规范继承
-
脚本中的 SQL 语句格式遵循
`dialect/sql-core`
的 sql-style.md(关键字大写/缩进/CRLF)
-
**注释规范**
:顶部声明注释块(
`/* ===== */`
块和
`-- =====`
分隔线)用
`--`
单行注释;SQL 语句内部注释用
`/* */`
块注释
-
**XML 修改注释**
:ConfigFile XML 或任何 .xml 文件内的修改,注释统一使用
`<!-- -->`
XML 标准注释(不是
`/* */`
);禁止在 XML 中使用 SQL 注释风格
-
保持与项目现有脚本风格一致
### 2.5 编码:GB2312(强制)
**所有项目内落盘的 `.sql` 文件必须使用 GB2312(GBK)编码**
,保持与 SQL Server 默认排序规则兼容。
> 桌面端注意:kb_write / d_write 默认写 UTF-8。生成 `.sql` 后需转换为 GB2312:
> - 有 Python 环境:读 UTF-8 → 写 GBK(`open(path, 'w', encoding='gbk')`)
> - 无 Python:请用户用文本编辑器(Notepad++/VS)将文件另存为「ANSI」编码
> - 交付脚本时明确提示用户核对编码
### 2.6 常见脚本清单速查
| 脚本类型 | 存放位置 | 命名格式 | 可重复执行策略 |
|----------|----------|----------|---------------|
| 建表 |
`{日期}/0X.AddTable_xxx.sql`
| 序号 + AddTable + 表名 |
`IF OBJECT_ID IS NULL`
→
`CREATE`
|
| 加列 |
`{日期}/0X.AddTableColumn_xxx.sql`
| 序号 + AddTableColumn + 描述 |
`IF NOT EXISTS sys.columns`
→
`ALTER`
|
| 加模块/窗口 |
`{日期}/0X.AddModule_xxx.sql`
| 序号 + AddModule + 模块名 |
`IF NOT EXISTS App_SysBusinessModel`
→
`INSERT`
|
| 数据修复 |
`{日期}/0X.Data_xxx.sql`
| 序号 + Data + 描述 | 按具体情况用临时表 + 条件判断 |
| 建存储过程 |
`{2999}/30.SP/{yyyymmdd}{NNN}.SP_Name.sql`
| 日期序号 + SP + 名称 |
`IF EXISTS`
→
`DROP`
→
`CREATE`
|
| 建视图 |
`{2999}/20.VW/{yyyymmdd}{NNN}.VW_Name.sql`
| 日期序号 + VW + 名称 |
`IF EXISTS`
→
`DROP`
→
`CREATE`
|
| 建函数 |
`{2999}/30.FN/{yyyymmdd}{NNN}.FN_Name.sql`
| 日期序号 + FN + 名称 |
`IF EXISTS`
→
`DROP`
→
`CREATE`
|
AppHome/Agent/dialect/erp/verification-flow.md
0 → 100644
View file @
1ac5825c
# 审批流程引擎(VerificationFlow)
> ERP/PDM/MES 通用。每个需要审批的单据类型,通过枚举 `VerificationFlowType` 注册两个自定义特性:`[DataEditForm]` 定义编辑窗口和业务类,`[VerificationFlow]` 定义审批数据元信息。
## 核心组件
| 组件 | 作用 | 位置 |
|------|------|------|
|
`VerificationFlowType`
枚举 | 注册所有审批类型,每个枚举值绑定两个特性 |
`U.Model/Enum/VerificationFlowType.cs`
|
|
`[DataEditForm]`
特性 | 定义编辑窗口(全限定名)、业务类(全限定名)、DLL 名 |
`U.Model/Att/DataEditFormAttribute.cs`
|
|
`[VerificationFlow]`
特性 | 定义审批名称、DocType、主表名、取数函数、主键名 |
`U.Model/Att/VerificationFlowAttribute.cs`
|
|
`ORMappingByAttHelper`
| 反射读取特性的路由方法 |
`U.Rule/Share/ORMappingByAttHelper.cs`
|
|
`FlowMapHelper`
| 审批流程提交/审批/回退的核心引擎 |
`U.Rule/VF/FlowMapHelper.cs`
|
|
`IVerificationFlow`
接口 | 业务类必须实现此接口才能进入审批流 |
`U.IFace/IVerificationFlow.cs`
|
|
`IVerificationFlowEx`
接口 | 扩展接口(审核前后业务逻辑校验) |
`U.IFace/IVerificationFlow.cs`
|
|
`frmViewDataInVFlow`
| 审批主界面("数据在审"列表 + "已审列表") |
`U.WinVerificationFlow/frmViewDataInVFlow.cs`
|
|
`frmEditAuditingText`
| 审批意见录入弹窗 |
`U.WinVerificationFlow/frmEditAuditingText.cs`
|
|
`frmEditTaskNode`
| 审批流程节点编辑 |
`U.WinVerificationFlow/frmEditTaskNode.cs`
|
|
`EditHelper.Audit()`
| 业务编辑窗口内嵌审批入口 |
`U.WinBaseLib/EditHelper.cs`
|
---
## 一、VerificationFlowType 枚举(审批注册中心)
每个审批单据类型在
`VerificationFlowType`
枚举中占一个值,同时绑定两个特性:
```
csharp
[DataEditForm("窗口类全限定名", "业务类全限定名", "所在的DLL名")]
[
VerificationFlow
(
"审批名称"
,
DocType
=
DocType
.
StockIn
,
DocTableName
=
"StockIn"
,
DocSqlFunName
=
"fu_VF_StockIn(61)"
,
DocKeyName
=
"ID"
)]
StockInFabric
=
51
,
// 枚举值(存入数据库 VerificationFlowType 列)
```
### [DataEditForm] 特性参数
| 参数 | 说明 | 示例 |
|------|------|------|
|
`FormFullName`
| 编辑窗口全限定名 |
`"U.WinClientWH.frmEditStockIn"`
|
|
`BusRuleFullName`
| 业务类全限定名 |
`"U.Rule.WH.UStockIn"`
|
|
`FormAssemblyName`
| 窗口所在 DLL |
`"U.WinClientWH.dll"`
|
|
`RuleCreateParams`
| 业务类构造函数参数(可选) |
`new object[] { "61" }`
|
|
`RuleAssemblyName`
| 业务类所在 DLL(可选,默认
`"U.Rule.DLL"`
) |
`"U.Rule.DLL"`
|
|
`IsLinkEditForm`
| 是否联动编辑窗口(默认
`true`
) |
`true`
/
`false`
|
### [VerificationFlow] 特性参数
| 参数 | 说明 | 示例 |
|------|------|------|
|
`Description`
| 审批模块名称(中文显示) |
`"面料入库单"`
|
|
`DocType`
| 单据类型枚举值 |
`DocType.StockIn`
|
|
`DocTableName`
| 单据主表名 |
`"StockIn"`
|
|
`DocSqlFunName`
| 取数表值函数(带参数) |
`"fu_VF_StockIn(61)"`
|
|
`DocKeyName`
| 单据主键列名(默认
`"ID"`
) |
`"ID"`
/
`"UID"`
|
|
`MapXmlFileName`
| 自定义 XML 配置(默认等于
`DocTableName`
) |
`"StockIn"`
|
---
## 二、运行时链路
### 2.1 单据提交 → 进入审批
```
业务编辑窗口保存
→ FrmEditBaseLink → ObjectModefiedHelper
→ IVerificationFlow 接口已实现 (U.IFace)
→ FlowMapHelper.SubmitData()
→ 从 VerificationFlowType 枚举反射取 [VerificationFlow] 特性 → DocType/DocTableName/DocKeyName/DocSqlFunName
→ 查 VF_FlowMap 表确认该类型启用了审批流程
→ 查 VF_FlowMapNode 找起始节点 (PrevID = -1)
→ 写入 VF_DataInFlow 表(审批实例记录)
→ 存储过程 up_VF_SubmitDataToFlow
```
### 2.2 审批主界面 → 查看待审/已审数据
```
frmViewDataInVFlow 打开
→ 左树加载:FlowMapHelper.GetWaitAuditingVFlow()
→ fu_VF_GetWaitAuditingVFlow(userID) 返回当前用户有权限的审批类型
→ 选中审批类型枚举值 → 反射取 [VerificationFlow] 特性
→ 待审列表:FlowMapHelper.GetAllowVFDocData(flowType)
→ 拼接 SQL = fu_VF_GetAllowAuditingDoc(flowType, docType, userID)
+ DocSqlFunName(取数函数,如 fu_VF_StockIn(61))
→ JOIN ON DocKeyName = DocInstanceKey
→ 已审列表:FlowMapHelper.GetHasAuditingDocData(flowType, beginDate, endDate)
→ fu_VF_GetHasAuditingDoc(docType, userID, beginDate, endDate)
+ DocSqlFunName
```
### 2.3 审批界面 → 打开单据查看详情
```
审批列表双击/点"详情"按钮
→ showDocDataDetail()
→ 反射取 [DataEditForm] 特性 → FormFullName + BusRuleFullName + FormAssemblyName
→ DllFactory.CreateInstance(BusRuleFullName, RuleCreateParams, RuleAssemblyName) → 创建业务类实例
→ IsLinkEditForm ?
true → DllFactory.DispLinkEditForm(OptType, FormFullName, FormAssemblyName, rule, grdCtl)
false → DllFactory.DispEditFormEx(OptType, FormFullName, FormAssemblyName, rule, grdCtl)
```
### 2.4 审批决议
```
frmEditAuditingText → 审批意见(通过/不通过)
→ FlowMapHelper.SubmitFlowNodeAuditing()
→ 加载 VF_VersionCompare 条件约束
→ 检查是否有下一级审批节点 → 若有则流转,若无可结束审批
→ 存储过程 up_VF_NodeAuditing
```
---
## 三、数据库表结构
### 核心表
| 表名 | 说明 | 关键列 |
|------|------|--------|
|
`VerificationFlowType`
| 审批类型主数据表(与枚举对应) |
`ID`
,
`Name`
,
`Code`
,
`DocType`
,
`DocTableName`
,
`DocSqlFunName`
,
`DocKeyName`
|
|
`VF_FlowMap`
| 审批流程模板(一个审批类型可有多个版本) |
`ID`
,
`VerificationFlowType`
,
`IsRuning`
(当前启用哪个),
`StartEndAuditing`
,
`EndAuditingPassword`
,
`IsReverseAudit`
|
|
`VF_FlowMapNode`
| 审批流程节点(节点链 = 审批链路) |
`ID`
,
`FlowMapID`
,
`Name`
,
`FlowNodeType`
(1=起始/2=业务/3=结束),
`PrevID`
,
`NextID`
,
`FilterName`
(条件约束名),
`LimitDepartment`
|
|
`VF_FlowMapNodeUser`
| 节点审批人 |
`ID`
,
`FlowMapNodeID`
,
`UserID`
,
`LimitDepartment`
|
|
`VF_DataInFlow`
| 审批实例(每个进入审批的单据一条记录) |
`ID`
,
`DocType`
,
`DocInstanceKey`
,
`DocSubmitUserID`
,
`FlowCheckState`
,
`EndCheckDate`
|
|
`VF_AuditingText`
| 审批决议记录 |
`ID`
,
`DataInFlowID`
,
`FlowMapNodeID`
,
`AuditingText`
,
`AuditingDate`
,
`AuditingUserID`
,
`FlowAuditingState`
(0=未审/1=通过/2=不通过) |
|
`VF_VersionCompare`
| 审批条件约束(版本对比/条件分支审批) |
`ID`
,
`VerificationFlowType`
,
`Name`
,
`FilterString`
(SQL WHERE 子句),
`TableAlias`
,
`Description`
|
### 关键存储过程/函数
| 对象 | 说明 |
|------|------|
|
`up_VF_SubmitDataToFlow`
| 单据提交时写入审批流 |
|
`up_VF_NodeAuditing`
| 执行审批决议(通过/不通过) |
|
`up_GetVFFlowMapID`
| 获取指定审批类型的 FlowMapID |
|
`up_VF_ClearInFlowData`
| 清除在审数据 |
|
`fu_VF_GetWaitAuditingVFlow(userID)`
| 当前用户有权限的审批类型列表 |
|
`fu_VF_GetAllowAuditingDoc(flowType, docType, userID)`
| 当前用户待审单据列表 |
|
`fu_VF_GetHasAuditingDoc(docType, userID, begin, end)`
| 已审核单据列表 |
|
`fu_VF_GetAllowAuditingUser(auditingTextID)`
| 当前节点有权审批的用户 |
|
`fu_VF_DataInFlowProcessing(docType, instanceKey)`
| 单据当前审批进程(节点链+决议状态) |
|
`fu_VF_*`
(取数表值函数) | 各审批类型的取数函数(如
`fu_VF_StockIn(61)`
),返回该类型单据的完整数据 |
|
`fu_CheckAllowCancelFlow(flowType, docType, instanceKey)`
| 判断是否允许撤回审批 |
---
## 四、审批条件分流(VersionCompare)
通过
`VF_VersionCompare`
表配置条件约束,实现同一审批类型内的分支路由:
```
VF_FlowMapNode 节点
→ FlowNodeType = 2(业务节点)
→ FilterName 非空 → 查 VF_VersionCompare 中 Name 匹配的记录
→ FilterString = SQL WHERE 子句(如 "MaterialTypeID = 61")
→ 用 DocSqlFunName + FilterString 检查当前单据是否满足条件
→ 满足 → 进入该节点的审批人审核
→ 不满足 → 递归找下一节点(existsNextFlowMap)
```
---
## 五、打印控制(VerificationPrintControl)
审批引擎还提供打印控制功能:
| 组件 | 对应 |
|------|------|
|
`VerificationPrintControl`
枚举 | 打印控制类型(结构与
`VerificationFlowType`
类似) |
|
`frmViewPrintControl`
| 打印控制界面 |
---
## 六、排查步骤
当遇到「审批不触发」或「审批流程不对」时:
```
1. 确认单据是否注册审批:
→ 检查业务类是否实现 IVerificationFlow 接口
→ 检查 VerificationFlowType get 属性返回的枚举值是否正确
→ 检查 VerificationFlowType 枚举值上是否有 [DataEditForm] + [VerificationFlow] 特性
2. 确认审批流程是否启用:
→ 查 VF_FlowMap 表:IsRuning = 1 且 VerificationFlowType 匹配(db_query)
→ 无记录 → 审批未启用
→ 有记录但有节点 → 进入步骤 3
3. 确认审批节点配置:
→ VF_FlowMapNode 必须有 FlowNodeType = 2(业务节点)且有 FlowNodeType = 3(结束节点)
→ 每个业务节点必须在 VF_FlowMapNodeUser 中配置审批人
→ 起始节点(PrevID = -1)必须存在
4. 确认审批提交逻辑:
→ FlowMapHelper.SubmitData() 中 CheckFlowMapIsRuning
→ 检查 VF_VersionCompare 中的条件约束(FilterString)是否过滤掉了当前单据
→ 若条件约束过滤掉所有节点,且 isFirstFlow = false → 审批不触发
5. 确认取数 SQL:
→ [VerificationFlow] 特性的 DocSqlFunName = 表值函数名 + 参数
→ 用 db_query 执行该函数(无连接则给用户 SSMS 执行),确认返回正确的结果集
→ 检查 DocKeyName 是否与函数返回的列名匹配
6. 审批界面打不开单据:
→ 检查 [DataEditForm] 特性的三个参数正确性
→ 检查 DLL 是否已部署到运行目录
→ 检查业务类构造函数参数是否匹配 RuleCreateParams
→ 检查 IsLinkEditForm 是否正确(大部分为 true,少数为 false)
7. 审批分支/条件不对:
→ 查 VF_VersionCompare 中 FilterString 是否正确
→ 查 VF_FlowMapNode.FilterName 是否与 VF_VersionCompare.Name 匹配
→ 用 db_query 以 DocSqlFunName + FilterString 验证数据匹配
```
## 常见问题模式
| 模式 | 症状 | 根因 | 修复 |
|------|------|------|------|
| 未实现接口 | 保存后不进审批 | 业务类未实现
`IVerificationFlow`
| 实现接口 + 返回正确 VerificationFlowType |
| 审批流程未启用 | VF_FlowMap 中该类型 IsRuning=0 | 管理员未启用该审批流 | 审批管理 → 启用该类型的流程模板 |
| 节点无审批人 | 流程中某节点无人可审 | VF_FlowMapNodeUser 中未配置 | 为节点添加审批用户 |
| 条件约束过严 | 单据进不了审批 | VF_VersionCompare.FilterString 过滤掉数据 | 检查 FilterString 与数据是否匹配 |
| 取数函数不对 | 审批列表显示数据为空/不全 | DocSqlFunName 写错或函数不存在 | 检查函数的正确名称和参数 |
| IsLinkEditForm 设错 | 审批界面打开单据白屏/null | IsLinkEditForm 与实际窗口类型不符 | 标准编辑窗口=true,树形/属性窗口=false |
| 枚举值 hardcode | 新增审批类型但编译后不生效 | 枚举值与数据库不一致 | 确保 VerificationFlowType 表中有对应 ID |
## 关键文件(修改时需谨慎)
| 文件 | 说明 |
|------|------|
|
`U.Model/Enum/VerificationFlowType.cs`
| 审批注册中心,新增类型在此添加枚举值+两个特性 |
|
`U.Model/Att/DataEditFormAttribute.cs`
| DataEditForm 特性定义(禁止随意修改) |
|
`U.Model/Att/VerificationFlowAttribute.cs`
| VerificationFlow 特性定义(禁止随意修改) |
|
`U.IFace/IVerificationFlow.cs`
| 审批接口(禁止修改) |
|
`U.Rule/Share/ORMappingByAttHelper.cs`
|
**反射路由方法**
(修改影响所有审批类型) |
|
`U.Rule/VF/FlowMapHelper.cs`
|
**审批核心引擎**
(修改影响所有审批提交/审批行为) |
|
`U.WinVerificationFlow/frmViewDataInVFlow.cs`
|
**审批主界面**
(修改影响所有审批类型的交互) |
|
`U.WinBaseLib/EditHelper.cs`
| 业务窗口内嵌审批入口(
`Audit()`
) |
AppHome/Agent/dialect/erp/winforms.md
0 → 100644
View file @
1ac5825c
# 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)/ 外键约束
```
AppHome/Agent/dialect/mes/cases/hotspots.md
0 → 100644
View file @
1ac5825c
# 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/折叠吞数。排查「合计<明细」先核对聚合键的唯一性域。
AppHome/Agent/dialect/mes/sql-templates-mes.md
0 → 100644
View file @
1ac5825c
# 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`
),先确认当前连接库。
-
跨库查询需确认账号有对应库权限;无权限时分别查询后在结果侧关联。
AppHome/Agent/dialect/pdm/cases/hotspots.md
0 → 100644
View file @
1ac5825c
# 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 分支)。
AppHome/Agent/dialect/sql-core/sql-scripts.md
0 → 100644
View file @
1ac5825c
# 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:类型/引号差异按库适配。
AppHome/Agent/dialect/sql-core/sql-style.md
0 → 100644
View file @
1ac5825c
# 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(先查影响行)。
AppHome/Agent/dialect/sql-core/sql-templates.md
0 → 100644
View file @
1ac5825c
# 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 验证数据分布)
```
AppHome/Agent/dialect/troubleshoot-core/five-steps.md
0 → 100644
View file @
1ac5825c
# 排查五步法
> 对齐系统化调试方法论(根因先行,禁止症状修复)。
## 第 1 步:复现与证据
-
完整读报错信息:堆栈全文、行号、文件路径、错误码(错误信息常含直接答案)。
-
稳定复现:能否稳定触发?触发步骤是什么?每次都出现吗?
-
不能复现 → 收集更多数据(日志/截图/现场),
**不猜**
。
-
检查近期变更:代码提交、配置、数据、环境差异。
## 第 2 步:链路定位(桌面端业务链路)
```
窗口/界面 → 业务类/Controller → SQL/SP → 数据库
```
-
沿链路逐层取证(
`db_query`
/
`db_schema`
/ 文件读取)。
-
**每一层边界记录**
:什么数据进入、什么数据出去、状态/配置是否传递。
-
一次取证找出
**在哪一层断**
,再深入那一层(不要全链路乱翻)。
## 第 3 步:模式对比
-
找可用的同类链路/相似功能作为对照(「什么能用、和坏的很像」)。
-
对照参考实现时
**完整读**
(不扫读),列出每一个差异(哪怕很小)。
-
不假设「那个差异不可能有关」。
## 第 4 步:单一假设 + 最小验证
-
明确写出假设:「我认为 X 是根因,因为 Y」。
-
用
**最小改动**
验证一个变量;一次只验证一个假设。
-
验证不通过 → 形成新假设,
**不要在失败修复上叠加修复**
。
## 第 5 步:根因修复
-
先造
**失败用例**
(自动化测试优先;无框架则一段复现脚本)——必须在修复前有。
-
一次只改一处(无「顺手优化」、无捆绑重构)。
-
验证:用例通过?其他用例没坏?问题真正消失?
-
**已试 3 次仍失败 → 停止并质疑架构**
:每个修复都暴露新耦合/新问题、修复需「大重构」、每修一个又出新的 → 与用户讨论是否重构架构,而非继续第 4 次硬试。
## 环境/外部因素
若系统排查确认是环境、时序或外部因素(95% 的「无根因」是调查不完整,先自查):
1.
记录已调查内容;2. 做合理处理(重试/超时/明确报错);3. 加监控便于后续定位。
AppHome/Agent/dialect/troubleshoot-core/output-spec.md
0 → 100644
View file @
1ac5825c
# 排查输出规范
回答排查类问题时,按以下结构输出:
```
markdown
【任务类型】数据错误 / 程序异常 / 性能 / 约束 / 模块分析
【追踪链路】窗口/接口 → 业务类/Controller → SQL/SP → 数据库
【根因定位】一句话根本原因(必须基于证据,禁止臆断)
【证据链】取证过程(哪一层、用了什么工具、得到什么)
【修复建议】具体修改方案(含文件/配置/SQL)
【影响范围】可能受影响的关联功能
【测试数据】验证用关键记录 / 测试 SQL
```
## 要求
-
**根因一句话**
:能说清「什么环节、因为什么、导致什么」。
-
**证据链可回溯**
:每步结论标注来源(文件路径/查询结果/计划),读者能复现。
-
**不确定要明说**
:证据不足时明确「尚未确认 X」,给下一步取证建议,不编造。
-
**修复建议可执行**
:具体到文件/字段/SQL;附验证方式(失败用例先造)。
-
排查完的典型案例可经用户确认后整理进 dialect 热点案例(内置只读,由维护者落盘)。
AppHome/Agent/industry/鞋业/01-品类认知/凉鞋与拖鞋.md
0 → 100644
View file @
1ac5825c
# 凉鞋与拖鞋
## 品类细分
-
**EVA 注塑拖鞋**
:一体注塑成型,轻量防水,居家/沙滩/酒店用。
-
**PVC 发泡拖鞋**
:成本低,冬季凉拖/浴室拖。
-
**凉鞋**
:帮面(织带/皮革/超纤)+ 大底(EVA/橡胶),分平底与坡跟。
-
**沙滩鞋/涉水鞋**
:速干面料+排水结构+防滑底。
## 工艺特点
-
**注塑成型**
:模具合模注射(EVA 交联发泡或 PVC),一双一双成型,模具成本高但单双成本低。
-
**冷粘**
:凉鞋帮面与大底胶粘,款式变化灵活。
-
大底防滑纹路设计(排水槽/人字纹)是卖点与质检点。
## 业务要点
-
季节性强(夏季爆量),产能集中在 Q1-Q2 备货。
-
注塑款模具管理重要(模具编号/寿命/存放)。
-
尺码跨度大(成人 34-46),码重成本差异小。
AppHome/Agent/industry/鞋业/01-品类认知/劳保鞋.md
0 → 100644
View file @
1ac5825c
# 劳保鞋
## 品类细分
-
**安全鞋**
:防砸(钢包头/复合包头,耐冲击≥200J 等级)、防刺穿(防刺中底)。
-
**绝缘鞋**
:电绝缘(6kV/10kV 等级),禁止金属部件外露。
-
**防静电鞋**
:表面电阻/体积电阻控制,用于电子/化工车间。
-
**耐油/耐酸碱鞋**
:特种材质鞋底与帮面。
-
**户外/矿山劳保靴**
:高帮、耐磨耐折。
## 标准与认证(国内)
-
**GB 21148**
《足部防护 安全鞋》:防砸/防刺穿/防静电/电绝缘等安全性能通用要求。
-
需第三方检测报告(SGS/CTI 等),出欧盟 CE 需 EN ISO 20345 系列。
-
钢包头:GB 21148 规定冲击能量 200J 与压缩 15kN 试验。
## 工艺特点
-
帮面常用:头层牛皮/二层牛皮(耐摩擦)、超纤、帆布(轻便款)。
-
大底:橡胶/聚氨酯(PU)双密度(中底缓震+外底耐磨防滑)。
-
成型:冷粘或硫化(橡胶大底硫化结合强度高,耐油款常用)。
-
中底防刺层:钢板/凯夫拉(Kevlar)复合中底。
## 业务要点
-
证书与检测报告是接单必要条件(客户验厂/验货重点)。
-
鞋码偏大(欧码),内长标注重要。
-
单量稳定但品类固定,翻单率高。
AppHome/Agent/industry/鞋业/01-品类认知/皮鞋与休闲鞋.md
0 → 100644
View file @
1ac5825c
# 皮鞋与休闲鞋
## 品类细分
-
**正装皮鞋**
:头层牛皮帮面、真皮/橡胶大底、固特异沿条工艺(可换底)。
-
**休闲皮鞋**
:胶粘工艺为主,超纤/二层皮帮面,EVA/橡胶底。
-
**商务休闲鞋**
:轻量软底,飞织+皮革拼接。
## 材料
-
帮面:头层牛皮(粒面/修面)、二层牛皮(剖层贴膜)、超纤(PU 涂层的仿皮,物性稳定)、PU 革。
-
内里:猪皮/羊皮里(透气)、超纤里、织物里。
-
大底:真皮底(固特异)、橡胶、PU、TPU(耐磨止滑)。
## 工艺特点
-
**固特异(Goodyear)**
:沿条+缝线+软木填充,可换底、耐穿,工艺链长(绷帮→沿条→缝线→上底)。
-
**胶粘(Cemented)**
:绷帮后直接胶粘大底,主流工艺,周期短。
-
**绷帮**
:鞋面套楦定型,决定鞋型美观与合脚度(绷帮不良→歪楦/开胶)。
## 业务要点
-
真皮采购有批差/色差(缸差),同款多批次需预留配色余量。
-
皮鞋尺码偏大,内长标注;半码需求多。
-
固特异款交期长(工艺链长),报价高。
AppHome/Agent/industry/鞋业/01-品类认知/童鞋.md
0 → 100644
View file @
1ac5825c
# 童鞋
## 品类细分
-
**学步鞋(1~3 岁)**
:软底、轻量、易弯折,鞋头宽敞保护脚趾。
-
**机能鞋(3~6 岁)**
:足弓支撑、后跟加固,纠正步态。
-
**中大童鞋(6~12 岁)**
:接近成人鞋结构,运动款为主(校园/日常)。
-
**婴幼鞋(<1 岁)**
:防掉跟、无鞋带,装饰件最少化。
## 安全标准(国内重点)
-
**GB 30585**
《儿童鞋安全技术规范》:物理安全(小附件抗拉、锐边)与化学安全(六价铬、可分解致癌芳香胺、甲醛、重金属限量)。
-
小附件(鞋带扣/装饰件)拉力 ≥70N 测试,防脱落误吞。
-
童鞋号型按 GB/T 3293(内长制),鞋内腔尺寸必须标注。
## 工艺特点
-
成型:注塑一体成型占比高(EVA/TPU/橡胶注塑),成本低、无胶水污染。
-
帮面:网布/飞织/超纤/帆布,避免小颗粒装饰。
-
大底:EVA(轻)或橡胶(防滑),学步鞋强调 1/3 处易弯折。
## 业务要点
-
检测报告(GB 30585 全套)是商超/电商平台入驻门槛。
-
款式多、单款量小、翻单快,对开发与排产柔性要求高。
-
尺码分段(段码)管理:婴幼段/小童段/中童段/大童段,BOM 按段码区分。
AppHome/Agent/industry/鞋业/01-品类认知/运动鞋.md
0 → 100644
View file @
1ac5825c
# 运动鞋
## 品类细分
-
**跑步鞋**
:缓震中底(EVA/TPU/超临界发泡),网布/飞织鞋面,轻量化。专业跑鞋分缓震/支撑/竞速。
-
**篮球鞋**
:高帮包裹、防侧翻(TPU 抗扭转片)、耐磨大底(橡胶/人字纹)。
-
**足球鞋**
:鞋钉(FG/SG/AG/TF 按场地)、触球面材料(袋鼠皮/超纤/合成革)。
-
**综训/休闲运动鞋**
:通用缓震+稳定结构,鞋面网布+合成革拼接。
## 工艺特点
-
主流成型路线:
**冷粘**
(占多数,鞋面+大底胶粘,工艺灵活)与
**注塑**
(PVC/TPU/EVA 一体成型,多用于童鞋/拖鞋)。
-
飞织鞋面:一体织造无裁片,减少针车工序,需热熔贴合支撑片。
-
大底常见组合:EVA 中底 + 橡胶外底(耐磨防滑)。
## 业务要点
-
开发节奏快(季度上新),样品确认(确认样/船头样)是关键节点。
-
尺码体系:欧码(EU)/美码(US)/英码(UK)换算,半码/整码分布影响 BOM 与排产。
-
配色多(每款多色),材料单耗按配色分别核算。
AppHome/Agent/industry/鞋业/02-生产工艺/包装与入库.md
0 → 100644
View file @
1ac5825c
# 包装与入库
## 包装流程
-
**配对**
:左右脚配对(同码同色,色差/号码核对),补单件。
-
**装盒**
:内衬/纸盒/防潮剂;盒标(款号/颜色/尺码/条码)。
-
**装箱**
:箱规(每箱双数,如 12/24 双)、外箱唛(毛净重/箱号/款号)。
-
**吊牌/三标**
:洗标/主标/尺码标齐备(三标错漏属外观不良)。
## 入库
-
成品仓按款/色/码/批次入库,批次追溯(生产批→原材料批)。
-
尾数管理:订单完成后未销完的零头数量(库存尾数/生产尾数),需单独标识与处理。
## 排查要点(数据不对时)
-
箱规与实际装箱不符 → 出货数量差异(装盒/装箱环节)。
-
配对错误(鸳鸯鞋)→ 质检漏检(OQC 抽检比例不足)。
-
批次混放 → 追溯断链(先入库先出库原则)。
AppHome/Agent/industry/鞋业/02-生产工艺/成型工艺.md
0 → 100644
View file @
1ac5825c
# 成型工艺
> 鞋类最后成型(贴合/上底),三大路线决定设备、胶水、交期与成本结构。
## 冷粘(Cemented)——主流
-
流程:鞋面套楦(绷帮)→ 刷处理剂/胶水(鞋面+大底)→ 烘箱活化 → 贴合 → 压合 → 冷定型。
-
特点:工艺灵活、款式切换快;胶水环保要求高(水性胶替代油性胶)。
-
常见:运动鞋、休闲鞋、凉鞋。
## 硫化(Vulcanized)
-
流程:生胶大底贴鞋面 → 硫化罐加温加压(硫磺交联)→ 橡胶熟化成型。
-
特点:橡胶大底与鞋面结合强度高、耐油耐磨;工艺链长、周期长。
-
常见:劳保鞋(耐油款)、帆布硫化鞋(经典款)。
## 注塑(Injection)
-
流程:模具合模,PVC/TPU/EVA 注射成型,大底与鞋面一体或直接成型。
-
特点:无胶水、效率高、成本低;模具投入大。
-
常见:童鞋、拖鞋、凉鞋、劳保鞋(PU 注塑底)。
## 绷帮(Lasting)
-
鞋面套楦定型工序(三大路线共用前置),决定鞋型与合脚度。
-
常见方式:手工绷帮/机器绷帮(绷帮机);后跟定型/包头定型。
## 排查要点(质量问题)
-
开胶:处理剂/胶水工艺参数(温湿度、活化温度)或大底材质污染。
-
歪楦/不对称:绷帮不正、楦头配对错误。
-
溢胶/缺胶:贴合对位、压合力与时间。
-
成型产能:成型线节拍 = 瓶颈,排产与交期核心。
AppHome/Agent/industry/鞋业/02-生产工艺/裁断与针车.md
0 → 100644
View file @
1ac5825c
# 裁断与针车
## 裁断
-
**裁片**
:按唛架排版裁剪出的面料片(鞋面/内里/衬片)。
-
**唛架(Marker)**
:裁剪排版图,决定面料利用率(利用率=裁片面积/用料面积)。
-
**裁断设备**
:液压裁断机(刀模冲裁)/电脑裁床(多层)。
-
要点:刀模管理(编号/寿命)、错裁/色差片隔离、余料利用。
## 针车(鞋面缝制)
-
**针数**
:每英寸针数(针织密度指标,影响手感与强度,常规 8~12 针/英寸)。
-
工序:合缝(帮片拼接)→ 压线 → 包边 → 装饰线 → 缝标(洗标/主标/尺码标)。
-
设备:单针/双针平缝机、花样机、包边机。
-
要点:针距均匀、线色匹配、跳针/断针记录(IPQC 巡检点)。
## 排查要点(数据不对时)
-
裁片损耗:实际裁断用量 vs BOM 单耗(损耗率是否漏加/多加,行业常见 3%~8%)。
-
针车产能:瓶颈工序负载(针车工位数量×效率 vs 排产)。
-
色差:不同缸次面料混裁混缝 → 同双色差(IQC 与仓库批次追溯)。
AppHome/Agent/industry/鞋业/03-材料与BOM/BOM与单耗.md
0 → 100644
View file @
1ac5825c
# BOM 与单耗
## 核心概念
-
**BOM(物料清单)**
:鞋款的结构化物料清单——面料/辅料/配件/大底/中底/鞋垫/包装,含单耗与用量单位。
-
**单耗**
:单双鞋的物料用量(如鞋面材料按平方英尺/码/米,大底按双,织带按米)。
-
**损耗率**
:裁断损耗/缝制损耗计入材料成本(行业常见 3%~8%,按面料弹性与排版;皮料按伤残率更高)。
-
**BOM 版本**
:样品 BOM → 确认样 BOM → 量产 BOM,版本与变更记录(ECN)管理。
## 核算链路
```
BOM(面料/辅料单耗×单价) + 工价(工序×工价) + 制费分摊 + 损耗加成
→ 制造成本 → + 管理/销售费用 → 完全成本 → 报价
```
## 排查要点(数据不对时)
-
**单耗 vs 实际用量**
:BOM 单耗与裁剪实际用量是否一致(损耗率是否漏加/多加)。
-
**单价来源**
:面料单价是否按最新采购价(价差未更新 → 成本虚高/虚低)。
-
**工价版本**
:工序工价表是否与当前工艺单版本一致。
-
**分摊口径**
:制费分摊基数(产量 vs 工时)是否与成本中心配置一致。
-
**BOM 缺失/错挂**
:配色多时按配色 BOM 区分,错挂导致材料计划偏差。
AppHome/Agent/industry/鞋业/03-材料与BOM/鞋底材料.md
0 → 100644
View file @
1ac5825c
# 鞋底材料
## 大底/中底材料
-
**RB(橡胶)**
:耐磨、止滑、耐油,外底常用(人字纹/水波纹);硫化工艺用生胶。
-
**EVA**
:轻量、缓震,中底主流(发泡密度决定软硬度);成本低,压缩永久变形偏大。
-
**PU(聚氨酯)**
:缓震回弹好、耐疲劳,中底/整底;怕水解(储存环境要求)。
-
**TPU**
:高回弹、耐折、耐磨,中底/外底/结构件(抗扭转片);成本高。
-
**MD(Phylon)**
:EVA 二次发泡(射坯发泡),轻量回弹好,运动鞋中底。
-
**PVC**
:低成本,拖鞋/凉鞋注塑;低温变硬。
## 结构件
-
抗扭转片(TPU/碳板)、防刺穿中底(钢板/凯夫拉——劳保鞋)、鞋垫(EVA/乳胶/ortholite)。
## 排查要点(数据不对时)
-
单耗:大底按双计(一双两件:左+右),模具腔数决定单位产出。
-
密度/硬度规格:发泡密度波动影响克重与缓震(物性测试项)。
-
橡胶 vs 发泡类物料管理差异:橡胶需防硫化时间超期,EVA 发泡片需熟化期。
AppHome/Agent/industry/鞋业/03-材料与BOM/鞋面材料.md
0 → 100644
View file @
1ac5825c
# 鞋面材料
## 织物类
-
**网布(Mesh)**
:透气,运动鞋鞋面主材;按纱线分涤纶网布/尼龙网布,强度与透气权衡。
-
**飞织(Flyknit)**
:一体织造鞋面,无裁片、轻量、贴合;需热熔丝/支撑片补强。
-
**帆布**
:棉/涤帆布,硫化鞋/休闲鞋经典材料,耐磨耐折。
## 皮革类
-
**头层牛皮**
:粒面/修面,正装皮鞋;需注意缸差与伤残。
-
**二层牛皮**
:剖层贴膜(PU 涂层),性价比。
-
**超纤(Microfiber)**
:PU 涂层的仿皮,物性稳定、色牢度好、环保,运动/休闲大量使用。
-
**PU 革/合成革**
:低成本,需关注耐磨与低温耐折。
## 辅料
-
织带/松紧带/鞋带、五金(鞋眼/扣具/装饰件——童鞋小附件拉力要求)、热熔胶膜(补强/贴合)、衬片(港宝:后跟定型片)。
## 排查要点(数据不对时)
-
单耗:鞋面材料单耗按唛架排版核算(利用率),实际裁断用量 vs BOM 差异定位损耗口径。
-
色差:不同缸次/批次混用(缸差),IQC 检验记录与仓库批次追溯。
-
缩水率:面料水洗/热缩参数影响裁片尺寸(工艺单参数核对)。
AppHome/Agent/industry/鞋业/04-业务与贸易/报价与成本.md
0 → 100644
View file @
1ac5825c
# 报价与成本
## 核心概念
-
**料(直接材料)**
:鞋面/内里/大底/中底/辅料/包装成本 = 单耗 × 单价。BOM 是核算基准。
-
**工(直接人工)**
:工价 × 标准工时(按工序:裁断/针车/成型/包装)。
-
**费(制造费用)**
:车间折旧/水电/管理分摊,按产量或工时摊入。
-
**损耗率**
:裁断损耗/缝制损耗计入材料成本(常见 3%~8%,皮料更高)。
-
**报价口径**
:FOB(离岸价)不含运费保险;CIF 含。报价 = 料 + 工 + 费 + 毛利 + 税费。
## 鞋业核算链路
```
BOM(面料/辅料/大底单耗×单价) + 工价(工序×工价,按成型路线差异) + 制费分摊 + 损耗加成
→ 制造成本 → + 管理/销售费用 → 完全成本 → 报价
```
## 排查要点(数据不对时)
-
**单耗 vs 实际用量**
:BOM 单耗与裁剪实际用量是否一致(损耗率是否漏加/多加)。
-
**单价来源**
:面料单价是否按最新采购价(价差未更新 → 成本虚高/虚低)。
-
**工价版本**
:工序工价表是否与当前工艺单版本一致(成型路线变化影响工价)。
-
**分摊口径**
:制费分摊基数(产量 vs 工时)是否与成本中心配置一致。
-
**汇率**
:外购件按采购币种折算是否使用正确汇率日期。
AppHome/Agent/industry/鞋业/04-业务与贸易/接单与订单评审.md
0 → 100644
View file @
1ac5825c
# 接单与订单评审
## 流程要点
1.
**接单**
:客户订单(款号/颜色/尺码/数量/单价/交期/贸易条款)。
2.
**评审**
:产能可接?物料齐套?报价覆盖成本?交期可行?(鞋业加评:材料到位周期、模具状态、检测证书齐备)
3.
**下单**
:订单转工单,生成 BOM 与工艺单(含缩水率/唛架/工序工价)。
## 鞋业评审要点
-
**模具**
:注塑款模具是否就绪(新模/修模状态);硫化款鞋底模具腔数。
-
**材料**
:特殊材料(真皮/飞织纱线/防刺中底)采购周期长,需提前备料确认。
-
**检测**
:劳保鞋/童鞋需检测报告(GB 21148 / GB 30585),出证周期 2~4 周,订单评审必须覆盖。
-
**配色**
:多配色订单按配色分别评审材料齐套。
## 排查要点
-
订单未流转:查评审状态字段(待评审/评审通过/已下单)与审批节点。
-
数量差异:接单数量 vs 工单数量 vs 出货数量核对(尾数/损耗加成口径)。
-
交期冲突:同款多订单产能重叠 → 排产冲突。
AppHome/Agent/industry/鞋业/04-业务与贸易/翻单与交期.md
0 → 100644
View file @
1ac5825c
# 翻单与交期
## 核心概念
-
**翻单**
:老款/老订单再次下单生产(材料与模具齐备,交期短)。
-
**交期承诺**
:下单日 → 打样 → 备料 → 生产 → 出货(制鞋典型周期 45~90 天;翻单可缩短至 30~45 天)。
-
**产能与排产**
:按产线/工序产能排产;瓶颈工序(针车/成型)决定节拍。
-
**齐套**
:面料/辅料/大底/配件齐套后才投产(缺料 = 排产延迟主因)。
-
**跟催**
:交期预警(逾期/临期清单)→ 催料/调产能/插单。
## 排查要点(交期延误)
-
**缺口定位**
:交期延误先定位「承诺交期 vs 实际」差在哪一环(打样/备料/生产/出货)。
-
**齐套率**
:物料齐套率不足 → 查采购到货计划与缺料清单。
-
**产能占用**
:插单/急单占用瓶颈产能 → 查排产甘特/产能负载。
-
**工序异常**
:返工/停线 → 查质量问题与设备故障记录。
-
**数据口径**
:交期承诺是否按「工作日 vs 自然日」计算(口径不一致是常见根因)。
## 关键术语
-
交期(Delivery Date)/ 承诺交期(Promise Date)
-
齐套率(Kit Rate):物料备齐订单比例
-
瓶颈工序:限制整线产能的工序(针车/成型)
-
插单:临时插入高优先订单
AppHome/Agent/industry/鞋业/05-质量管理/检验体系.md
0 → 100644
View file @
1ac5825c
# 检验体系
## 核心环节
-
**IQC(来料检验)**
:面料/辅料/大底进厂检验(色差、克重、缩水率、瑕疵、物性)。
-
**IPQC(制程检验)**
:裁断/针车/成型各工序巡检(针距、对称度、粘合、绷帮)。
-
**OQC(出货检验)**
:成品抽检(AQL 标准、功能/外观/包装)。
-
**返工率/不良率**
:按批次/款号统计,异常时定位到工序与班次。
-
**客诉**
:售后投诉按「批次 → 工序 → 原料」追溯。
## 排查要点(质量问题)
-
**不良集中度**
:按款/颜色/尺码/日期/班次分组,定位集中点(单一因素主导 → 该环节问题)。
-
**色差/克重差异**
:面料缸差(批次混用)→ 查 IQC 记录与仓库批次追溯。
-
**尺寸偏差**
:缩水率未按面料实测调整 → 查工艺单缩水参数。
-
**缝制瑕疵**
:针距/断针 → 查 IPQC 巡检记录与针车工位。
-
**开胶/脱底**
:成型工艺参数(活化温度/压合力)或大底材质污染 → 查成型巡检记录。
-
**追溯链**
:成品 → 出货单 → 生产批次 → 采购单 → 原料批(反向查证)。
## 关键术语
-
AQL:可接受质量水平(抽样标准,如 AQL 2.5/4.0)
-
疵点:面料瑕疵(竹节/色斑/断纱)
-
三标:洗标/主标/尺码标(错漏属外观不良)
AppHome/Agent/industry/鞋业/05-质量管理/物性测试.md
0 → 100644
View file @
1ac5825c
# 物性测试
> 鞋类材料与成品的关键物性测试项(实验室/第三方检测),影响接单、验货与客诉归因。
## 材料级
-
**耐折(耐弯折)**
:帮面/大底反复弯折(如 4 万次/6 万次),开裂/断裂判定。
-
**耐磨**
:大底/外底磨耗(DIN 磨耗/阿克隆磨耗),劳保鞋与运动鞋重点。
-
**剥离强度**
:鞋面与大底粘合牢度(开胶归因的核心指标)。
-
**拉伸/撕裂强度**
:面料/超纤/织带。
-
**止滑(防滑)**
:大底动/静摩擦系数,出口欧美需测。
## 成品级
-
**整鞋耐折**
:成品鞋动态弯折测试(屈挠次数)。
-
**耐黄变**
:浅色材料/大底光照黄变。
-
**低温耐折**
:-10℃/-20℃ 弯折(冬季鞋/劳保鞋)。
-
**安全性能**
(劳保鞋):防砸(200J 冲击)、防刺穿(1100N)、电绝缘(6kV/10kV)。
-
**化学安全**
(童鞋):六价铬、甲醛、可分解致癌芳香胺、重金属(GB 30585)。
## 排查要点
-
客诉「开胶/断底/掉色」→ 先查对应物性测试报告与生产批次测试记录。
-
材料批次差异:同供应商不同批次物性波动 → IQC 抽样测试频率不足。
-
测试标准口径:客户标准 vs 国标(GB)vs 国际(ISO/EN/ASTM)不一致是常见争议点。
AppHome/Agent/industry/鞋业/06-专业术语/术语表.md
0 → 100644
View file @
1ac5825c
# 鞋业专业术语
## 生产工艺
-
**裁片**
:按唛架排版裁剪出的面料片(鞋面/内里/衬片)。
-
**唛架(Marker)**
:裁剪排版图,决定面料利用率。
-
**针数**
:每英寸针数(针织密度指标,影响手感与强度)。
-
**针车**
:鞋面缝制工序(合缝/压线/包边)。
-
**绷帮(Lasting)**
:鞋面套楦定型工序,决定鞋型与合脚度。
-
**成型**
:鞋类最后成型(贴合/上底),分冷粘/硫化/注塑三路线。
-
**冷粘**
:鞋面与大底胶粘成型(主流工艺)。
-
**硫化**
:生胶大底加温加压熟化成型(耐油耐磨,劳保鞋常用)。
-
**注塑**
:PVC/TPU/EVA 模具注射成型(童鞋/拖鞋/凉鞋)。
-
**港宝**
:后跟定型片(热熔胶/皮革材质)。
-
**飞织(Flyknit)**
:一体织造鞋面技术。
## 材料
-
**网布**
:透气织造面料,运动鞋鞋面主材。
-
**超纤**
:PU 涂层的仿皮材料,物性稳定。
-
**RB**
:橡胶大底(耐磨止滑)。
-
**EVA / MD / TPU / PU**
:中底材料(发泡/二次发泡/高回弹/聚氨酯)。
-
**缸差**
:同色不同缸次的色差。
## 计划与贸易
-
**BOM**
:物料清单(面料/辅料/大底结构与用量)。
-
**单耗**
:单双鞋的物料用量。
-
**MOQ**
:最小起订量(Minimum Order Quantity)。
-
**FOB / CIF**
:贸易术语(离岸价/到岸价含运费保险)。
-
**在制**
:生产进行中的在产数量。
-
**尾数**
:订单完成后未销完的零头数量(库存尾数/生产尾数)。
-
**翻单**
:老款/老订单再次下单生产。
-
**齐套**
:物料备齐可投产。
-
**三标**
:洗标/主标/尺码标。
## 质量
-
**AQL**
:可接受质量水平(抽检标准)。
-
**IQC / IPQC / OQC**
:来料/制程/出货检验。
-
**疵点**
:材料瑕疵(色斑/断纱/竹节)。
-
**物性测试**
:耐折/耐磨/剥离/止滑等物理性能测试。
-
**GB 21148**
:劳保安全鞋国家标准。
-
**GB 30585**
:儿童鞋安全技术规范。
AppHome/Agent/industry/鞋业/07-常见问题/README.md
0 → 100644
View file @
1ac5825c
# 常见问题(本地沉淀区)
> 本目录为鞋业行业问答沉淀区:智能体在行业问答中确认的典型问题,由维护者整理成 Markdown 主题文件放入本目录,并在 `../INDEX.md` 的 07-常见问题 大类登记(文件名 + 一句话说明 + 触发词)。
>
> 当前为空:暂无沉淀条目。
>
> 注意:内置目录只读,智能体禁止直接写入;沉淀由维护者(人工)执行。
AppHome/Agent/industry/鞋业/INDEX.md
0 → 100644
View file @
1ac5825c
# 鞋业(别名:制鞋、鞋类、鞋服)
行业简介:制鞋行业基础知识——品类认知(运动鞋/劳保鞋/童鞋等)、生产工艺(裁断/针车/成型)、材料与 BOM、业务与贸易、质量管理与专业术语,供排查与需求理解对齐业务语境。适用:运动鞋、劳保鞋、童鞋、皮鞋、凉拖鞋等各类鞋生产制造企业。
## 01-品类认知
-
运动鞋.md:跑鞋/篮球鞋/足球鞋/综训鞋(触发词:运动鞋、跑鞋、篮球鞋、飞织、综训)
-
劳保鞋.md:安全鞋/防砸防刺/绝缘/钢包头(触发词:劳保鞋、安全鞋、钢包头、防刺穿、绝缘鞋)
-
童鞋.md:学步鞋/机能鞋/中大童(触发词:童鞋、学步鞋、机能鞋、小附件、GB 30585)
-
皮鞋与休闲鞋.md:真皮/超纤/固特异/胶粘(触发词:皮鞋、真皮、超纤、固特异、休闲鞋)
-
凉鞋与拖鞋.md:EVA 注塑/PVC 发泡(触发词:凉鞋、拖鞋、EVA、注塑、沙滩鞋)
## 02-生产工艺
-
裁断与针车.md:裁片/唛架/针数/鞋面针车(触发词:裁断、针车、裁片、唛架、针数、鞋面)
-
成型工艺.md:冷粘/硫化/注塑/绷帮(触发词:成型、冷粘、硫化、注塑、绷帮、上底)
-
包装与入库.md:配对/装盒/箱规(触发词:包装、配对、装盒、箱规、入库)
## 03-材料与BOM
-
鞋面材料.md:网布/飞织/皮革/超纤/PU(触发词:鞋面、网布、飞织、超纤、PU、皮革)
-
鞋底材料.md:RB/EVA/PU/TPU/橡胶/MD(触发词:鞋底、大底、EVA、TPU、RB、橡胶、MD)
-
BOM与单耗.md:单耗/损耗率/用量核算(触发词:BOM、单耗、损耗、用量、核算)
## 04-业务与贸易
-
接单与订单评审.md:订单评审/下单(触发词:接单、订单评审、报价、翻单、下单)
-
报价与成本.md:料工费/报价口径(触发词:成本、核算、料工费、报价、FOB、CIF)
-
翻单与交期.md:翻单/齐套/交期(触发词:交期、延误、跟催、齐套、尾数、翻单)
## 05-质量管理
-
检验体系.md:IQC/IPQC/OQC/AQL(触发词:质量、IQC、IPQC、OQC、返工、客诉、AQL)
-
物性测试.md:耐折/耐磨/剥离/止滑(触发词:物性、耐折、耐磨、剥离、止滑、拉力)
## 06-专业术语
-
术语表.md:裁片/针数/唛架/绷帮/翻单/齐套(触发词:术语、MOQ、FOB、唛架、绷帮、翻单)
## 07-常见问题
> 本地沉淀区:行业问答中确认的常见问题经维护者整理追加到本大类(内置目录只读,禁止智能体直接写入)。
Ftt.PatchPack/Project.cs
View file @
1ac5825c
...
@@ -196,6 +196,25 @@ namespace Ftt.PatchPack
...
@@ -196,6 +196,25 @@ namespace Ftt.PatchPack
#
endregion
#
endregion
#
region
内置方言
/
行业知识(
Agent
/
dialect
、
Agent
/
industry
,随程序发布)
// 轻量智能体内置知识:产品排查方言(dialect)+ 行业知识(industry),只读、随程序发布。
// 新增/修改的内置知识进入增量补丁,解包到客户端 {ExeName}\Agent\ 下(时间窗同 XML,默认 30 天)。
var
dialectPath
=
Path
.
Combine
(
AbsoluteRootDirectory
,
RelativeAppFilePath
,
"Agent"
,
"dialect"
);
if
(
System
.
IO
.
Directory
.
Exists
(
dialectPath
))
{
valid
.
AddRange
(
GetValidPatchFile
(
dialectPath
,
"*.*"
,
SearchOption
.
AllDirectories
,
dialectPath
,
$"
{
ExeName
}
\\Agent\\dialect"
,
PatchTimeFrame_Xml
));
}
var
industryPath
=
Path
.
Combine
(
AbsoluteRootDirectory
,
RelativeAppFilePath
,
"Agent"
,
"industry"
);
if
(
System
.
IO
.
Directory
.
Exists
(
industryPath
))
{
valid
.
AddRange
(
GetValidPatchFile
(
industryPath
,
"*.*"
,
SearchOption
.
AllDirectories
,
industryPath
,
$"
{
ExeName
}
\\Agent\\industry"
,
PatchTimeFrame_Xml
));
}
#
endregion
#
region
额外附加的文件
.
#
region
额外附加的文件
.
foreach
(
var
fileName
in
ContainFiles
)
foreach
(
var
fileName
in
ContainFiles
)
...
...
Tools/MyAgent/Agent/AgentEngine.cs
View file @
1ac5825c
...
@@ -23,6 +23,7 @@ namespace Tools.MyAgent.Agent
...
@@ -23,6 +23,7 @@ namespace Tools.MyAgent.Agent
private
IOpenAiChatClient
_client
=
new
OpenAiCompatibleClient
();
private
IOpenAiChatClient
_client
=
new
OpenAiCompatibleClient
();
private
readonly
KbService
_kb
;
private
readonly
KbService
_kb
;
private
readonly
PromptBuilder
_prompts
=
new
PromptBuilder
();
private
readonly
PromptBuilder
_prompts
=
new
PromptBuilder
();
private
readonly
DialectService
_dialect
;
// 内置方言/行业知识(程序目录 Agent/dialect + Agent/industry)
private
CancellationTokenSource
_cts
;
private
CancellationTokenSource
_cts
;
/// <summary>上下文 token 预算(由配置注入;默认 32000)</summary>
/// <summary>上下文 token 预算(由配置注入;默认 32000)</summary>
...
@@ -138,6 +139,9 @@ namespace Tools.MyAgent.Agent
...
@@ -138,6 +139,9 @@ namespace Tools.MyAgent.Agent
_kb
=
kb
;
_kb
=
kb
;
_backupClient
=
new
OpenAiCompatibleClient
();
_backupClient
=
new
OpenAiCompatibleClient
();
_failover
=
new
FailoverPolicy
(
_client
,
_backupClient
);
_failover
=
new
FailoverPolicy
(
_client
,
_backupClient
);
// 内置方言/行业知识:随程序发布(AppDomain.BaseDirectory + Agent),目录缺失时自动降级为空
_dialect
=
new
DialectService
(
Path
.
Combine
(
AppDomain
.
CurrentDomain
.
BaseDirectory
,
"Agent"
));
_prompts
.
Dialect
=
_dialect
;
}
}
/// <summary>应用配置(ApiKey/BaseUrl/Model/Temperature/MaxTokens/代理/上下文预算)</summary>
/// <summary>应用配置(ApiKey/BaseUrl/Model/Temperature/MaxTokens/代理/上下文预算)</summary>
...
@@ -920,6 +924,7 @@ namespace Tools.MyAgent.Agent
...
@@ -920,6 +924,7 @@ namespace Tools.MyAgent.Agent
{
{
CancelToken
=
ct
,
CancelToken
=
ct
,
DbConnections
=
DbConnections
,
DbConnections
=
DbConnections
,
Dialect
=
_dialect
,
};
};
string
result
=
tool
.
Execute
(
args
,
ctx
);
string
result
=
tool
.
Execute
(
args
,
ctx
);
sw
.
Stop
();
sw
.
Stop
();
...
...
Tools/MyAgent/Agent/PromptBuilder.cs
View file @
1ac5825c
...
@@ -18,6 +18,9 @@ namespace Tools.MyAgent.Agent
...
@@ -18,6 +18,9 @@ namespace Tools.MyAgent.Agent
/// <summary>系统提示词启用开关(由设置注入;仅启用后才注入 CustomLayer)</summary>
/// <summary>系统提示词启用开关(由设置注入;仅启用后才注入 CustomLayer)</summary>
public
bool
SystemPromptEnabled
{
get
;
set
;
}
public
bool
SystemPromptEnabled
{
get
;
set
;
}
/// <summary>内置方言/行业知识服务(产品排查方言 + 鞋业行业知识;由引擎注入,可空)</summary>
public
DialectService
Dialect
{
get
;
set
;
}
public
string
BuildSystemMessage
(
KbService
kb
)
public
string
BuildSystemMessage
(
KbService
kb
)
{
{
// B2 提示词外置:静态层优先读 输出目录/prompts/*.md 模板(用户可定制),
// B2 提示词外置:静态层优先读 输出目录/prompts/*.md 模板(用户可定制),
...
@@ -29,6 +32,8 @@ namespace Tools.MyAgent.Agent
...
@@ -29,6 +32,8 @@ namespace Tools.MyAgent.Agent
sb
.
AppendLine
();
sb
.
AppendLine
();
sb
.
AppendLine
(
BuildKbLayer
(
kb
));
sb
.
AppendLine
(
BuildKbLayer
(
kb
));
sb
.
AppendLine
();
sb
.
AppendLine
();
sb
.
AppendLine
(
BuildDialectLayer
());
sb
.
AppendLine
();
// 自定义系统提示词:仅启用开关打开时注入(关闭时即使输入框有内容也不注入)
// 自定义系统提示词:仅启用开关打开时注入(关闭时即使输入框有内容也不注入)
if
(
SystemPromptEnabled
&&
!
string
.
IsNullOrWhiteSpace
(
CustomLayer
))
if
(
SystemPromptEnabled
&&
!
string
.
IsNullOrWhiteSpace
(
CustomLayer
))
{
{
...
@@ -155,6 +160,23 @@ namespace Tools.MyAgent.Agent
...
@@ -155,6 +160,23 @@ namespace Tools.MyAgent.Agent
return
sb
.
ToString
();
return
sb
.
ToString
();
}
}
/// <summary>
/// §2.2b 方言层(动态,随内置知识注入):产品排查方言 + 鞋业行业知识导航摘要。
/// 触发词路由:排查/业务类问题先按摘要命中 → d_grep 定位 → d_read 精读(禁止全量读取)。
/// </summary>
private
string
BuildDialectLayer
()
{
if
(
Dialect
==
null
||
!
Dialect
.
IsAvailable
)
return
""
;
try
{
return
Dialect
.
BuildSummary
();
}
catch
{
return
""
;
// 内置知识损坏不影响智能体可用性
}
}
/// <summary>搜索范围描述(空 = 整个知识库根目录)</summary>
/// <summary>搜索范围描述(空 = 整个知识库根目录)</summary>
private
static
string
ScopeText
(
KbService
kb
)
private
static
string
ScopeText
(
KbService
kb
)
{
{
...
...
Tools/MyAgent/Agent/Tools/DbQueryTool.cs
View file @
1ac5825c
...
@@ -355,7 +355,8 @@ namespace Tools.MyAgent.Agent.Tools
...
@@ -355,7 +355,8 @@ namespace Tools.MyAgent.Agent.Tools
return
Truncate
(
sb
.
ToString
());
return
Truncate
(
sb
.
ToString
());
}
}
private
static
IDbConnection
OpenConnection
(
string
type
,
string
connection
)
/// <summary>打开非 sqlserver 数据库连接(sqlserver 走平台 SmpDB,见 ExecuteQuery);供 DbSchemaTool 复用</summary>
internal
static
IDbConnection
OpenConnection
(
string
type
,
string
connection
)
{
{
switch
(
type
)
switch
(
type
)
{
{
...
...
Tools/MyAgent/Agent/Tools/DbSchemaTool.cs
0 → 100644
View file @
1ac5825c
using
Newtonsoft.Json.Linq
;
using
System
;
using
System.Data
;
using
System.Text
;
using
Tools.MyData
;
namespace
Tools.MyAgent.Agent.Tools
{
/// <summary>
/// db_schema(轻量版,迁移自 Common.Agent):只读结构分析——表/列/索引/主键/视图定义。
/// <para>sqlserver 走平台 SmpDB 数据访问层;sqlite 用 sqlite_master + PRAGMA;mysql/postgres 用 information_schema。
/// 与 db_query 同一连接体系(connectionId 预存连接优先)。辅助 SQL 排查:表名/列名不确定时先查结构再写查询。</para>
/// </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(设置中配置,如 sqlserver_erp);指定后无需再传 connection,连接串不回显"
)
+
@"},
'connection':{'type':'string','description':"
+
ToolJson
.
Q
(
"连接字符串(用户提供,仅当未指定 connectionId 时使用);sqlite 可直接给数据库文件路径"
)
+
@"},
'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
{
return
type
==
"sqlserver"
?
SqlServerSchema
(
connection
,
obj
,
name
)
:
InfoSchema
(
type
,
connection
,
obj
,
name
);
}
catch
(
Exception
ex
)
{
return
$"错误:
{
ex
.
Message
}
"
;
}
}
/// <summary>sqlserver:平台 SmpDB 执行 INFORMATION_SCHEMA 查询(只读 SELECT)</summary>
private
static
string
SqlServerSchema
(
string
connection
,
string
obj
,
string
name
)
{
var
sb
=
new
StringBuilder
();
using
(
var
db
=
new
SmpDB
())
{
if
(
string
.
IsNullOrEmpty
(
name
))
{
string
sql
=
obj
==
"view"
?
"SELECT TABLE_NAME AS Name FROM INFORMATION_SCHEMA.VIEWS ORDER BY TABLE_NAME"
:
"SELECT TABLE_NAME AS Name FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE TABLE' ORDER BY TABLE_NAME"
;
var
dt
=
db
.
GetDataTable
(
connection
,
sql
,
"schema"
,
false
);
if
(
dt
.
Rows
.
Count
==
0
)
return
"(无"
+
(
obj
==
"view"
?
"视图"
:
"表"
)
+
")"
;
foreach
(
DataRow
r
in
dt
.
Rows
)
sb
.
AppendLine
(
r
[
"Name"
]?.
ToString
()
??
""
);
return
sb
.
ToString
();
}
// 指定对象:先确认存在,再列列定义
string
typeFilter
=
obj
==
"view"
?
"VIEW"
:
"BASE TABLE"
;
string
chk
=
"SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME='"
+
name
.
Replace
(
"'"
,
"''"
)
+
"' AND TABLE_TYPE='"
+
typeFilter
+
"'"
;
var
exist
=
db
.
GetDataTable
(
connection
,
chk
,
"schema"
,
false
);
if
(
exist
.
Rows
.
Count
==
0
)
return
$"未找到
{(
obj
==
"view"
?
"视图"
:
"表"
)}
:
{
name
}
(可先不带 name 列出全部表清单)"
;
string
cols
=
"SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, IS_NULLABLE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='"
+
name
.
Replace
(
"'"
,
"''"
)
+
"' ORDER BY ORDINAL_POSITION"
;
var
dtc
=
db
.
GetDataTable
(
connection
,
cols
,
"schema"
,
false
);
sb
.
AppendLine
(
$"==
{
name
}
(
{
dtc
.
Rows
.
Count
}
列)=="
);
foreach
(
DataRow
r
in
dtc
.
Rows
)
{
string
dtype
=
r
[
"DATA_TYPE"
]?.
ToString
()
??
""
;
if
(
r
[
"CHARACTER_MAXIMUM_LENGTH"
]
!=
null
&&
r
[
"CHARACTER_MAXIMUM_LENGTH"
]
!=
DBNull
.
Value
)
dtype
+=
"("
+
r
[
"CHARACTER_MAXIMUM_LENGTH"
]
+
")"
;
sb
.
AppendLine
(
$"
{
r
[
"COLUMN_NAME"
]}
{
dtype
}
{(
r
[
"IS_NULLABLE"
]?.
ToString
()
??
""
)}
"
);
}
// 索引/主键(sqlserver 视图无索引)
if
(
obj
!=
"view"
)
{
string
idx
=
"SELECT i.name AS IndexName, i.is_primary_key AS IsPK, "
+
"STUFF((SELECT ',' + c.name FROM sys.index_columns ic "
+
"JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id "
+
"WHERE ic.object_id = i.object_id AND ic.index_id = i.index_id ORDER BY ic.key_ordinal FOR XML PATH('')), 1, 1, '') AS Cols "
+
"FROM sys.indexes i WHERE i.object_id = OBJECT_ID('"
+
name
.
Replace
(
"'"
,
"''"
)
+
"') AND i.name IS NOT NULL ORDER BY i.index_id"
;
var
dti
=
db
.
GetDataTable
(
connection
,
idx
,
"schema"
,
false
);
if
(
dti
.
Rows
.
Count
>
0
)
{
sb
.
AppendLine
(
"索引:"
);
foreach
(
DataRow
r
in
dti
.
Rows
)
sb
.
AppendLine
(
$"
{
r
[
"IndexName"
]}{(
Convert
.
ToInt32
(
r
[
"IsPK"
])
==
1
?
"(主键)"
:
""
)}
:
{
r
[
"Cols"
]}
"
);
}
}
return
sb
.
ToString
();
}
}
/// <summary>sqlite:sqlite_master + PRAGMA;mysql/postgres:information_schema</summary>
private
static
string
InfoSchema
(
string
type
,
string
connection
,
string
obj
,
string
name
)
{
if
(
type
==
"sqlite"
)
return
SqliteSchema
(
connection
,
obj
,
name
);
var
sb
=
new
StringBuilder
();
using
(
var
conn
=
DbQueryTool
.
OpenConnection
(
type
,
connection
))
using
(
var
cmd
=
conn
.
CreateCommand
())
{
conn
.
Open
();
cmd
.
CommandText
=
string
.
IsNullOrEmpty
(
name
)
?
"SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, IS_NULLABLE FROM INFORMATION_SCHEMA.COLUMNS ORDER BY TABLE_NAME, ORDINAL_POSITION"
:
"SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, IS_NULLABLE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='"
+
name
.
Replace
(
"'"
,
"''"
)
+
"' ORDER BY TABLE_NAME, ORDINAL_POSITION"
;
string
last
=
null
;
bool
found
=
false
;
using
(
var
rd
=
cmd
.
ExecuteReader
())
{
while
(
rd
.
Read
())
{
string
t
=
rd
.
GetString
(
0
);
if
(
last
!=
t
)
{
last
=
t
;
sb
.
AppendLine
(
$"==
{
t
}
=="
);
found
=
true
;
}
sb
.
AppendLine
(
$"
{
rd
.
GetString
(
1
)}
{
rd
.
GetString
(
2
)}
{(
rd
.
IsDBNull
(
3
)
?
""
:
rd
.
GetString
(
3
))}
"
);
}
}
if
(!
found
)
return
$"未找到对象:
{
name
}
"
;
}
return
sb
.
ToString
();
}
private
static
string
SqliteSchema
(
string
connection
,
string
obj
,
string
name
)
{
var
sb
=
new
StringBuilder
();
using
(
var
conn
=
DbQueryTool
.
OpenConnection
(
"sqlite"
,
connection
))
using
(
var
cmd
=
conn
.
CreateCommand
())
{
conn
.
Open
();
string
typeFilter
=
obj
==
"view"
?
"view"
:
"table"
;
cmd
.
CommandText
=
$"SELECT name, sql FROM sqlite_master WHERE type='
{
typeFilter
}
' AND name NOT LIKE 'sqlite_%' ORDER BY name"
;
using
(
var
rd
=
cmd
.
ExecuteReader
())
{
bool
found
=
false
;
while
(
rd
.
Read
())
{
string
tname
=
rd
.
GetString
(
0
);
if
(!
string
.
IsNullOrEmpty
(
name
)
&&
!
tname
.
Equals
(
name
,
StringComparison
.
OrdinalIgnoreCase
))
continue
;
found
=
true
;
sb
.
AppendLine
(
$"==
{
tname
}
=="
);
if
(
obj
==
"view"
)
{
string
sql
=
rd
.
IsDBNull
(
1
)
?
""
:
rd
.
GetString
(
1
);
sb
.
AppendLine
(
Truncate
(
sql
,
1500
));
continue
;
}
using
(
var
cmd2
=
conn
.
CreateCommand
())
{
cmd2
.
CommandText
=
$"PRAGMA table_info(\"
{
tname
.
Replace
(
"\""
,
"\"\""
)}
\")"
;
using
(
var
rd2
=
cmd2
.
ExecuteReader
())
while
(
rd2
.
Read
())
sb
.
AppendLine
(
$"
{
rd2
.
GetString
(
1
)}
{
rd2
.
GetString
(
2
)}{(
rd2
.
GetInt32
(
5
)
==
1
?
" PK"
:
""
)}
"
);
}
using
(
var
cmd2
=
conn
.
CreateCommand
())
{
cmd2
.
CommandText
=
$"PRAGMA index_list(\"
{
tname
.
Replace
(
"\""
,
"\"\""
)}
\")"
;
using
(
var
rd2
=
cmd2
.
ExecuteReader
())
while
(
rd2
.
Read
())
sb
.
AppendLine
(
$" 索引:
{
rd2
.
GetString
(
1
)}
(
{(
rd2
.
GetInt32
(
2
)
==
1
?
"唯一"
:
"普通"
)}
)"
);
}
}
if
(!
found
)
return
$"未找到
{(
obj
==
"view"
?
"视图"
:
"表"
)}
:
{
name
}
(可先不带 name 列出全部)"
;
}
}
return
sb
.
Length
>
0
?
sb
.
ToString
()
:
"(无表)"
;
}
private
static
string
Truncate
(
string
s
,
int
max
)
=>
s
==
null
?
""
:
(
s
.
Length
<=
max
?
s
:
s
.
Substring
(
0
,
max
)
+
"\n…(已截断)"
);
}
}
Tools/MyAgent/Agent/Tools/DialectTools.cs
0 → 100644
View file @
1ac5825c
using
Newtonsoft.Json.Linq
;
using
System
;
using
System.Collections.Generic
;
using
System.IO
;
using
System.Linq
;
using
System.Text
;
namespace
Tools.MyAgent.Agent.Tools
{
/// <summary>
/// 内置方言读取工具(d_* 系列):只读访问程序目录 Agent/dialect/(产品排查方言)与 Agent/industry/(行业知识)。
/// <para>沙箱:路径必须以 "dialect/" 或 "industry/" 开头,经 DialectService.ResolveSafe 校验(越界/符号链接拒绝),
/// 与用户知识库(kb_*)完全隔离。默认启用(排查能力是核心目标)。</para>
/// </summary>
internal
sealed
class
DialectListTool
:
ITool
{
public
string
Name
=>
"d_list"
;
public
string
Description
=>
"列出内置产品方言与行业知识目录下的文件/子目录(路径前缀:dialect/ 或 industry/),用于了解内置知识组织结构。"
+
"dialect/ = 产品排查方言(sql-core/troubleshoot-core/erp/mes/pdm),industry/ = 鞋业行业知识。"
;
public
string
ParametersJson
=>
@"{'type':'object',
'properties':{
'path':{'type':'string','description':"
+
ToolJson
.
Q
(
"内置知识内相对路径,如 'dialect'、'dialect/erp'、'industry/鞋业';默认 '.'(列出 dialect 与 industry 两级)"
)
+
@"},
'recursive':{'type':'boolean','description':"
+
ToolJson
.
Q
(
"是否递归列出子目录,默认 false"
)
+
@"}
}}"
;
public
bool
RequiresKb
=>
false
;
public
bool
IsDefault
=>
true
;
public
string
Describe
(
JObject
args
)
=>
$"浏览内置知识:
{
args
[
"path"
]?.
Value
<
string
>()
??
"."
}
"
;
public
string
Execute
(
JObject
args
,
ToolContext
ctx
)
{
var
dialect
=
ctx
.
Dialect
;
if
(
dialect
==
null
||
!
dialect
.
IsAvailable
)
return
"错误:内置方言/行业知识目录不可用(程序目录下 Agent/dialect、Agent/industry)。"
;
string
path
=
args
[
"path"
]?.
Value
<
string
>()
??
"."
;
bool
recursive
=
args
[
"recursive"
]?.
Value
<
bool
>()
??
false
;
try
{
if
(
path
==
"."
||
path
==
"dialect"
||
path
==
"industry"
)
{
var
sb
=
new
StringBuilder
();
if
(
path
==
"."
||
path
==
"dialect"
)
sb
.
AppendLine
(
"dialect/(产品排查方言):"
).
Append
(
ListDir
(
dialect
.
DialectRoot
,
recursive
,
"dialect"
));
if
(
path
==
"."
||
path
==
"industry"
)
sb
.
AppendLine
(
"industry/(行业知识):"
).
Append
(
ListDir
(
dialect
.
IndustryRoot
,
recursive
,
"industry"
));
return
sb
.
ToString
();
}
string
abs
=
dialect
.
ResolveSafe
(
path
);
if
(
abs
==
null
||
!
Directory
.
Exists
(
abs
))
return
$"错误:路径不存在或非法:
{
path
}
(仅允许 dialect/ 与 industry/ 前缀,拒绝越界)。"
;
return
ListDir
(
abs
,
recursive
,
path
.
Split
(
'/'
)[
0
]);
}
catch
(
Exception
ex
)
{
return
$"错误:
{
ex
.
Message
}
"
;
}
}
private
static
string
ListDir
(
string
abs
,
bool
recursive
,
string
prefix
)
{
if
(
string
.
IsNullOrEmpty
(
abs
)
||
!
Directory
.
Exists
(
abs
))
return
"(目录不可用)\n"
;
var
sb
=
new
StringBuilder
();
foreach
(
var
dir
in
Directory
.
GetDirectories
(
abs
).
OrderBy
(
d
=>
d
,
StringComparer
.
Ordinal
))
{
sb
.
AppendLine
(
" [目录] "
+
Path
.
GetFileName
(
dir
));
if
(
recursive
)
{
string
rel
=
prefix
+
"/"
+
Path
.
GetFileName
(
dir
);
string
inner
=
ListDir
(
dir
,
true
,
rel
);
foreach
(
var
l
in
inner
.
Split
(
'\n'
).
Where
(
x
=>
x
.
Length
>
0
))
sb
.
AppendLine
(
" "
+
l
.
TrimEnd
());
}
}
foreach
(
var
f
in
Directory
.
GetFiles
(
abs
,
"*.*"
).
OrderBy
(
f
=>
f
,
StringComparer
.
Ordinal
))
sb
.
AppendLine
(
" "
+
Path
.
GetFileName
(
f
));
return
sb
.
ToString
();
}
}
/// <summary>d_grep:在内置方言/行业知识文件内容中搜索关键字,返回命中文件与上下文片段</summary>
internal
sealed
class
DialectGrepTool
:
ITool
{
public
string
Name
=>
"d_grep"
;
public
string
Description
=>
"在内置产品方言与行业知识文件内容中搜索关键字(限 *.md 文本),返回命中文件与上下文片段。"
+
"排查/业务类问题先按触发词用本工具定位,再用 d_read 精读。"
;
public
string
ParametersJson
=>
@"{'type':'object',
'properties':{
'query':{'type':'string','description':"
+
ToolJson
.
Q
(
"搜索关键字(支持正则,如 '审批不触发'、'库存(不对|多扣)')"
)
+
@"},
'glob':{'type':'string','description':"
+
ToolJson
.
Q
(
"限定路径前缀,如 'dialect/erp'、'industry/鞋业';不填搜索全部内置知识"
)
+
@"},
'maxResults':{'type':'integer','description':'最多返回命中条数','minimum':1,'maximum':30}
},
'required':['query']}"
;
public
bool
RequiresKb
=>
false
;
public
bool
IsDefault
=>
true
;
public
string
Describe
(
JObject
args
)
=>
$"内置知识检索:
{
args
[
"query"
]?.
Value
<
string
>()}
"
;
public
string
Execute
(
JObject
args
,
ToolContext
ctx
)
{
var
dialect
=
ctx
.
Dialect
;
if
(
dialect
==
null
||
!
dialect
.
IsAvailable
)
return
"错误:内置方言/行业知识目录不可用。"
;
string
query
=
args
[
"query"
]?.
Value
<
string
>()
??
""
;
if
(
string
.
IsNullOrEmpty
(
query
))
return
"错误:缺少 query 参数。"
;
string
glob
=
(
args
[
"glob"
]?.
Value
<
string
>()
??
""
).
Trim
().
TrimEnd
(
'/'
).
Replace
(
'\\'
,
'/'
);
int
max
=
args
[
"maxResults"
]?.
Value
<
int
>()
??
20
;
if
(
max
<
1
)
max
=
1
;
if
(
max
>
30
)
max
=
30
;
try
{
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
())
{
if
(
glob
.
Length
>
0
&&
!
rel
.
StartsWith
(
glob
+
"/"
,
StringComparison
.
OrdinalIgnoreCase
)
&&
!
rel
.
Equals
(
glob
,
StringComparison
.
OrdinalIgnoreCase
))
continue
;
string
abs
=
dialect
.
ResolveSafe
(
rel
);
if
(
abs
==
null
||
!
File
.
Exists
(
abs
))
continue
;
string
[]
lines
;
try
{
lines
=
File
.
ReadAllLines
(
abs
,
Encoding
.
UTF8
);
}
catch
{
continue
;
}
foreach
(
var
line
in
lines
)
{
if
(
line
.
Length
>
300
)
continue
;
// 跳过超长行(通常是代码块)
if
(
regex
.
IsMatch
(
line
))
{
hits
.
Add
(
$"-
{
rel
}
:
{
line
.
Trim
().
TrimStart
(
'#'
,
' '
,
'>'
,
'-'
,
'*'
)}
"
);
if
(
hits
.
Count
>=
max
)
break
;
}
}
if
(
hits
.
Count
>=
max
)
break
;
}
if
(
hits
.
Count
==
0
)
return
$"(内置知识中未找到「
{
query
}
」;可换关键词,或先 d_list 浏览目录结构)"
;
return
$"命中
{
hits
.
Count
}
条:\n"
+
string
.
Join
(
"\n"
,
hits
);
}
catch
(
Exception
ex
)
{
return
$"错误:
{
ex
.
Message
}
"
;
}
}
}
/// <summary>d_read:读取内置方言/行业知识文件内容(UTF-8 文本,≤300 行/次)</summary>
internal
sealed
class
DialectReadTool
:
ITool
{
public
string
Name
=>
"d_read"
;
public
string
Description
=>
"读取内置产品方言或行业知识文件的文本内容(只读)。"
+
"路径格式:dialect/<主题>/<文件>(如 dialect/erp/winforms.md)或 industry/<行业>/<大类>/<文件>(如 industry/鞋业/01-品类认知/运动鞋.md)。"
;
public
string
ParametersJson
=>
@"{'type':'object',
'properties':{
'path':{'type':'string','description':"
+
ToolJson
.
Q
(
"内置知识相对路径,如 'dialect/erp/winforms.md'、'industry/鞋业/06-专业术语/术语表.md'"
)
+
@"},
'offset':{'type':'integer','description':'起始行号,默认 1','minimum':1},
'limit':{'type':'integer','description':'最多读取行数,默认 200,最大 300','minimum':1,'maximum':300}
},
'required':['path']}"
;
public
bool
RequiresKb
=>
false
;
public
bool
IsDefault
=>
true
;
public
string
Describe
(
JObject
args
)
=>
$"读取内置知识:
{
args
[
"path"
]?.
Value
<
string
>()}
"
;
public
string
Execute
(
JObject
args
,
ToolContext
ctx
)
{
var
dialect
=
ctx
.
Dialect
;
if
(
dialect
==
null
||
!
dialect
.
IsAvailable
)
return
"错误:内置方言/行业知识目录不可用。"
;
string
path
=
args
[
"path"
]?.
Value
<
string
>()
??
""
;
if
(
string
.
IsNullOrEmpty
(
path
))
return
"错误:缺少 path 参数(如 'dialect/erp/winforms.md'、'industry/鞋业/术语表' 或 'industry/鞋业/06-专业术语/术语表.md')。"
;
int
offset
=
args
[
"offset"
]?.
Value
<
int
>()
??
1
;
int
limit
=
args
[
"limit"
]?.
Value
<
int
>()
??
200
;
if
(
offset
<
1
)
offset
=
1
;
if
(
limit
<
1
)
limit
=
1
;
if
(
limit
>
300
)
limit
=
300
;
string
abs
=
dialect
.
ResolveSafe
(
path
);
if
(
abs
==
null
||
!
File
.
Exists
(
abs
))
{
// #32 风格参数补全:给出候选文件名,便于模型二次确认
var
cands
=
dialect
.
EnumerateFiles
()
.
Where
(
f
=>
f
.
IndexOf
(
path
,
StringComparison
.
OrdinalIgnoreCase
)
>=
0
||
path
.
IndexOf
(
Path
.
GetFileNameWithoutExtension
(
f
),
StringComparison
.
OrdinalIgnoreCase
)
>=
0
)
.
Take
(
8
).
ToList
();
if
(
cands
.
Count
>
0
)
return
$"错误:文件不存在:
{
path
}
。相近的内置知识文件:\n- "
+
string
.
Join
(
"\n- "
,
cands
)
+
"\n(请从候选中选择或给出完整路径)"
;
return
$"错误:文件不存在或路径非法:
{
path
}
(仅允许 dialect/ 与 industry/ 前缀,拒绝越界)。"
;
}
try
{
var
lines
=
File
.
ReadAllLines
(
abs
,
Encoding
.
UTF8
);
int
start
=
Math
.
Min
(
offset
-
1
,
Math
.
Max
(
0
,
lines
.
Length
-
1
));
int
count
=
Math
.
Min
(
limit
,
lines
.
Length
-
start
);
var
sb
=
new
StringBuilder
();
sb
.
AppendLine
(
$"==
{
path
}
(共
{
lines
.
Length
}
行,显示
{
start
+
1
}
-
{
start
+
count
}
行)=="
);
for
(
int
i
=
start
;
i
<
start
+
count
;
i
++)
sb
.
AppendLine
(
lines
[
i
]);
if
(
start
+
count
<
lines
.
Length
)
sb
.
AppendLine
(
$"…(剩余
{
lines
.
Length
-
start
-
count
}
行未显示,可 offset=
{
start
+
count
+
1
}
续读)"
);
return
sb
.
ToString
();
}
catch
(
Exception
ex
)
{
return
$"错误:
{
ex
.
Message
}
"
;
}
}
}
}
Tools/MyAgent/Agent/Tools/ITool.cs
View file @
1ac5825c
...
@@ -26,6 +26,9 @@ namespace Tools.MyAgent.Agent.Tools
...
@@ -26,6 +26,9 @@ namespace Tools.MyAgent.Agent.Tools
/// <summary>预存数据库连接串(ID → 连接串,已解密明文;来自 AgentConfig.DbConnections)</summary>
/// <summary>预存数据库连接串(ID → 连接串,已解密明文;来自 AgentConfig.DbConnections)</summary>
public
IReadOnlyDictionary
<
string
,
string
>
DbConnections
{
get
;
set
;
}
public
IReadOnlyDictionary
<
string
,
string
>
DbConnections
{
get
;
set
;
}
/// <summary>内置方言/行业知识服务(d_* 工具沙箱;程序目录 Agent/dialect + Agent/industry)</summary>
public
DialectService
Dialect
{
get
;
set
;
}
public
ToolContext
(
KbService
kb
,
Action
<
string
>
onFileProduced
)
public
ToolContext
(
KbService
kb
,
Action
<
string
>
onFileProduced
)
{
{
Kb
=
kb
;
Kb
=
kb
;
...
@@ -155,9 +158,14 @@ namespace Tools.MyAgent.Agent.Tools
...
@@ -155,9 +158,14 @@ namespace Tools.MyAgent.Agent.Tools
Register
(
new
KbWriteTool
());
Register
(
new
KbWriteTool
());
Register
(
new
KbWriteKbTool
());
Register
(
new
KbWriteKbTool
());
Register
(
new
DbQueryTool
());
Register
(
new
DbQueryTool
());
Register
(
new
DbSchemaTool
());
Register
(
new
DocExportTool
());
Register
(
new
DocExportTool
());
Register
(
new
ImageSearchTool
());
Register
(
new
ImageSearchTool
());
Register
(
new
ModulePatchTool
());
Register
(
new
ModulePatchTool
());
// 内置方言/行业知识(d_* 系列,只读沙箱,默认启用)
Register
(
new
DialectListTool
());
Register
(
new
DialectGrepTool
());
Register
(
new
DialectReadTool
());
}
}
/// <summary>全部已注册工具(下发顺序)</summary>
/// <summary>全部已注册工具(下发顺序)</summary>
...
...
Tools/MyAgent/Forms/AgentSettingsForm.Designer.cs
View file @
1ac5825c
...
@@ -53,6 +53,12 @@
...
@@ -53,6 +53,12 @@
this
.
txtSharePassword
=
new
System
.
Windows
.
Forms
.
TextBox
();
this
.
txtSharePassword
=
new
System
.
Windows
.
Forms
.
TextBox
();
this
.
btnTestShare
=
new
System
.
Windows
.
Forms
.
Button
();
this
.
btnTestShare
=
new
System
.
Windows
.
Forms
.
Button
();
this
.
tabAdv
=
new
System
.
Windows
.
Forms
.
TabPage
();
this
.
tabAdv
=
new
System
.
Windows
.
Forms
.
TabPage
();
this
.
tabAdvSub
=
new
System
.
Windows
.
Forms
.
TabControl
();
this
.
tabSubGen
=
new
System
.
Windows
.
Forms
.
TabPage
();
this
.
tabSubStrategy
=
new
System
.
Windows
.
Forms
.
TabPage
();
this
.
tabSubSem
=
new
System
.
Windows
.
Forms
.
TabPage
();
this
.
tabSubFail
=
new
System
.
Windows
.
Forms
.
TabPage
();
this
.
tabSubOpt
=
new
System
.
Windows
.
Forms
.
TabPage
();
this
.
grpGen
=
new
System
.
Windows
.
Forms
.
GroupBox
();
this
.
grpGen
=
new
System
.
Windows
.
Forms
.
GroupBox
();
this
.
lblTemp
=
new
System
.
Windows
.
Forms
.
Label
();
this
.
lblTemp
=
new
System
.
Windows
.
Forms
.
Label
();
this
.
numTemp
=
new
System
.
Windows
.
Forms
.
NumericUpDown
();
this
.
numTemp
=
new
System
.
Windows
.
Forms
.
NumericUpDown
();
...
@@ -85,7 +91,6 @@
...
@@ -85,7 +91,6 @@
this
.
cmbRole
=
new
System
.
Windows
.
Forms
.
ComboBox
();
this
.
cmbRole
=
new
System
.
Windows
.
Forms
.
ComboBox
();
this
.
txtCustomPrompt
=
new
System
.
Windows
.
Forms
.
TextBox
();
this
.
txtCustomPrompt
=
new
System
.
Windows
.
Forms
.
TextBox
();
this
.
lblPromptHint
=
new
System
.
Windows
.
Forms
.
Label
();
this
.
lblPromptHint
=
new
System
.
Windows
.
Forms
.
Label
();
this
.
tabOther
=
new
System
.
Windows
.
Forms
.
TabPage
();
this
.
grpOpt
=
new
System
.
Windows
.
Forms
.
GroupBox
();
this
.
grpOpt
=
new
System
.
Windows
.
Forms
.
GroupBox
();
this
.
chkOpt
=
new
System
.
Windows
.
Forms
.
CheckBox
();
this
.
chkOpt
=
new
System
.
Windows
.
Forms
.
CheckBox
();
this
.
chkImageInput
=
new
System
.
Windows
.
Forms
.
CheckBox
();
this
.
chkImageInput
=
new
System
.
Windows
.
Forms
.
CheckBox
();
...
@@ -105,6 +110,12 @@
...
@@ -105,6 +110,12 @@
this
.
grpModel
.
SuspendLayout
();
this
.
grpModel
.
SuspendLayout
();
this
.
grpKb
.
SuspendLayout
();
this
.
grpKb
.
SuspendLayout
();
this
.
tabAdv
.
SuspendLayout
();
this
.
tabAdv
.
SuspendLayout
();
this
.
tabAdvSub
.
SuspendLayout
();
this
.
tabSubGen
.
SuspendLayout
();
this
.
tabSubStrategy
.
SuspendLayout
();
this
.
tabSubSem
.
SuspendLayout
();
this
.
tabSubFail
.
SuspendLayout
();
this
.
tabSubOpt
.
SuspendLayout
();
this
.
grpGen
.
SuspendLayout
();
this
.
grpGen
.
SuspendLayout
();
((
System
.
ComponentModel
.
ISupportInitialize
)(
this
.
numTemp
)).
BeginInit
();
((
System
.
ComponentModel
.
ISupportInitialize
)(
this
.
numTemp
)).
BeginInit
();
((
System
.
ComponentModel
.
ISupportInitialize
)(
this
.
numMaxTokens
)).
BeginInit
();
((
System
.
ComponentModel
.
ISupportInitialize
)(
this
.
numMaxTokens
)).
BeginInit
();
...
@@ -115,7 +126,6 @@
...
@@ -115,7 +126,6 @@
this
.
grpSem
.
SuspendLayout
();
this
.
grpSem
.
SuspendLayout
();
this
.
tabPrompt
.
SuspendLayout
();
this
.
tabPrompt
.
SuspendLayout
();
this
.
grpPrompt
.
SuspendLayout
();
this
.
grpPrompt
.
SuspendLayout
();
this
.
tabOther
.
SuspendLayout
();
this
.
grpOpt
.
SuspendLayout
();
this
.
grpOpt
.
SuspendLayout
();
this
.
grpFail
.
SuspendLayout
();
this
.
grpFail
.
SuspendLayout
();
this
.
SuspendLayout
();
this
.
SuspendLayout
();
...
@@ -125,7 +135,6 @@
...
@@ -125,7 +135,6 @@
this
.
tabMain
.
Controls
.
Add
(
this
.
tabBasic
);
this
.
tabMain
.
Controls
.
Add
(
this
.
tabBasic
);
this
.
tabMain
.
Controls
.
Add
(
this
.
tabAdv
);
this
.
tabMain
.
Controls
.
Add
(
this
.
tabAdv
);
this
.
tabMain
.
Controls
.
Add
(
this
.
tabPrompt
);
this
.
tabMain
.
Controls
.
Add
(
this
.
tabPrompt
);
this
.
tabMain
.
Controls
.
Add
(
this
.
tabOther
);
this
.
tabMain
.
Location
=
new
System
.
Drawing
.
Point
(
12
,
12
);
this
.
tabMain
.
Location
=
new
System
.
Drawing
.
Point
(
12
,
12
);
this
.
tabMain
.
Name
=
"tabMain"
;
this
.
tabMain
.
Name
=
"tabMain"
;
this
.
tabMain
.
SelectedIndex
=
0
;
this
.
tabMain
.
SelectedIndex
=
0
;
...
@@ -378,10 +387,7 @@
...
@@ -378,10 +387,7 @@
//
//
// tabAdv
// tabAdv
//
//
this
.
tabAdv
.
AutoScroll
=
true
;
this
.
tabAdv
.
Controls
.
Add
(
this
.
tabAdvSub
);
this
.
tabAdv
.
Controls
.
Add
(
this
.
grpGen
);
this
.
tabAdv
.
Controls
.
Add
(
this
.
grpStrategy
);
this
.
tabAdv
.
Controls
.
Add
(
this
.
grpSem
);
this
.
tabAdv
.
Location
=
new
System
.
Drawing
.
Point
(
4
,
26
);
this
.
tabAdv
.
Location
=
new
System
.
Drawing
.
Point
(
4
,
26
);
this
.
tabAdv
.
Name
=
"tabAdv"
;
this
.
tabAdv
.
Name
=
"tabAdv"
;
this
.
tabAdv
.
Padding
=
new
System
.
Windows
.
Forms
.
Padding
(
3
);
this
.
tabAdv
.
Padding
=
new
System
.
Windows
.
Forms
.
Padding
(
3
);
...
@@ -390,6 +396,74 @@
...
@@ -390,6 +396,74 @@
this
.
tabAdv
.
Text
=
"高级配置"
;
this
.
tabAdv
.
Text
=
"高级配置"
;
this
.
tabAdv
.
UseVisualStyleBackColor
=
true
;
this
.
tabAdv
.
UseVisualStyleBackColor
=
true
;
//
//
// tabAdvSub
//
this
.
tabAdvSub
.
Controls
.
Add
(
this
.
tabSubGen
);
this
.
tabAdvSub
.
Controls
.
Add
(
this
.
tabSubStrategy
);
this
.
tabAdvSub
.
Controls
.
Add
(
this
.
tabSubSem
);
this
.
tabAdvSub
.
Controls
.
Add
(
this
.
tabSubFail
);
this
.
tabAdvSub
.
Controls
.
Add
(
this
.
tabSubOpt
);
this
.
tabAdvSub
.
Location
=
new
System
.
Drawing
.
Point
(
3
,
3
);
this
.
tabAdvSub
.
Name
=
"tabAdvSub"
;
this
.
tabAdvSub
.
SelectedIndex
=
0
;
this
.
tabAdvSub
.
Size
=
new
System
.
Drawing
.
Size
(
606
,
354
);
this
.
tabAdvSub
.
TabIndex
=
0
;
//
// tabSubGen
//
this
.
tabSubGen
.
Controls
.
Add
(
this
.
grpGen
);
this
.
tabSubGen
.
Location
=
new
System
.
Drawing
.
Point
(
4
,
26
);
this
.
tabSubGen
.
Name
=
"tabSubGen"
;
this
.
tabSubGen
.
Padding
=
new
System
.
Windows
.
Forms
.
Padding
(
3
);
this
.
tabSubGen
.
Size
=
new
System
.
Drawing
.
Size
(
598
,
324
);
this
.
tabSubGen
.
TabIndex
=
0
;
this
.
tabSubGen
.
Text
=
"生成参数"
;
this
.
tabSubGen
.
UseVisualStyleBackColor
=
true
;
//
// tabSubStrategy
//
this
.
tabSubStrategy
.
Controls
.
Add
(
this
.
grpStrategy
);
this
.
tabSubStrategy
.
Location
=
new
System
.
Drawing
.
Point
(
4
,
26
);
this
.
tabSubStrategy
.
Name
=
"tabSubStrategy"
;
this
.
tabSubStrategy
.
Padding
=
new
System
.
Windows
.
Forms
.
Padding
(
3
);
this
.
tabSubStrategy
.
Size
=
new
System
.
Drawing
.
Size
(
598
,
324
);
this
.
tabSubStrategy
.
TabIndex
=
1
;
this
.
tabSubStrategy
.
Text
=
"策略参数"
;
this
.
tabSubStrategy
.
UseVisualStyleBackColor
=
true
;
//
// tabSubSem
//
this
.
tabSubSem
.
Controls
.
Add
(
this
.
grpSem
);
this
.
tabSubSem
.
Location
=
new
System
.
Drawing
.
Point
(
4
,
26
);
this
.
tabSubSem
.
Name
=
"tabSubSem"
;
this
.
tabSubSem
.
Padding
=
new
System
.
Windows
.
Forms
.
Padding
(
3
);
this
.
tabSubSem
.
Size
=
new
System
.
Drawing
.
Size
(
598
,
324
);
this
.
tabSubSem
.
TabIndex
=
2
;
this
.
tabSubSem
.
Text
=
"语义检索"
;
this
.
tabSubSem
.
UseVisualStyleBackColor
=
true
;
//
// tabSubFail
//
this
.
tabSubFail
.
Controls
.
Add
(
this
.
grpFail
);
this
.
tabSubFail
.
Location
=
new
System
.
Drawing
.
Point
(
4
,
26
);
this
.
tabSubFail
.
Name
=
"tabSubFail"
;
this
.
tabSubFail
.
Padding
=
new
System
.
Windows
.
Forms
.
Padding
(
3
);
this
.
tabSubFail
.
Size
=
new
System
.
Drawing
.
Size
(
598
,
324
);
this
.
tabSubFail
.
TabIndex
=
3
;
this
.
tabSubFail
.
Text
=
"故障转移"
;
this
.
tabSubFail
.
UseVisualStyleBackColor
=
true
;
//
// tabSubOpt
//
this
.
tabSubOpt
.
Controls
.
Add
(
this
.
grpOpt
);
this
.
tabSubOpt
.
Location
=
new
System
.
Drawing
.
Point
(
4
,
26
);
this
.
tabSubOpt
.
Name
=
"tabSubOpt"
;
this
.
tabSubOpt
.
Padding
=
new
System
.
Windows
.
Forms
.
Padding
(
3
);
this
.
tabSubOpt
.
Size
=
new
System
.
Drawing
.
Size
(
598
,
324
);
this
.
tabSubOpt
.
TabIndex
=
4
;
this
.
tabSubOpt
.
Text
=
"可选能力"
;
this
.
tabSubOpt
.
UseVisualStyleBackColor
=
true
;
//
// grpGen
// grpGen
//
//
this
.
grpGen
.
Controls
.
Add
(
this
.
lblTemp
);
this
.
grpGen
.
Controls
.
Add
(
this
.
lblTemp
);
...
@@ -401,9 +475,9 @@
...
@@ -401,9 +475,9 @@
this
.
grpGen
.
Controls
.
Add
(
this
.
numContext
);
this
.
grpGen
.
Controls
.
Add
(
this
.
numContext
);
this
.
grpGen
.
Controls
.
Add
(
this
.
lblContextHint
);
this
.
grpGen
.
Controls
.
Add
(
this
.
lblContextHint
);
this
.
grpGen
.
Controls
.
Add
(
this
.
chkProxy
);
this
.
grpGen
.
Controls
.
Add
(
this
.
chkProxy
);
this
.
grpGen
.
Location
=
new
System
.
Drawing
.
Point
(
8
,
12
);
this
.
grpGen
.
Location
=
new
System
.
Drawing
.
Point
(
4
,
8
);
this
.
grpGen
.
Name
=
"grpGen"
;
this
.
grpGen
.
Name
=
"grpGen"
;
this
.
grpGen
.
Size
=
new
System
.
Drawing
.
Size
(
59
6
,
128
);
this
.
grpGen
.
Size
=
new
System
.
Drawing
.
Size
(
59
0
,
128
);
this
.
grpGen
.
TabIndex
=
0
;
this
.
grpGen
.
TabIndex
=
0
;
this
.
grpGen
.
TabStop
=
false
;
this
.
grpGen
.
TabStop
=
false
;
this
.
grpGen
.
Text
=
"生成参数"
;
this
.
grpGen
.
Text
=
"生成参数"
;
...
@@ -552,9 +626,9 @@
...
@@ -552,9 +626,9 @@
this
.
grpSem
.
Controls
.
Add
(
this
.
lblEmbKey
);
this
.
grpSem
.
Controls
.
Add
(
this
.
lblEmbKey
);
this
.
grpSem
.
Controls
.
Add
(
this
.
txtEmbKey
);
this
.
grpSem
.
Controls
.
Add
(
this
.
txtEmbKey
);
this
.
grpSem
.
Controls
.
Add
(
this
.
lblEmbHint
);
this
.
grpSem
.
Controls
.
Add
(
this
.
lblEmbHint
);
this
.
grpSem
.
Location
=
new
System
.
Drawing
.
Point
(
8
,
14
8
);
this
.
grpSem
.
Location
=
new
System
.
Drawing
.
Point
(
4
,
8
);
this
.
grpSem
.
Name
=
"grpSem"
;
this
.
grpSem
.
Name
=
"grpSem"
;
this
.
grpSem
.
Size
=
new
System
.
Drawing
.
Size
(
59
6
,
128
);
this
.
grpSem
.
Size
=
new
System
.
Drawing
.
Size
(
59
0
,
128
);
this
.
grpSem
.
TabIndex
=
1
;
this
.
grpSem
.
TabIndex
=
1
;
this
.
grpSem
.
TabStop
=
false
;
this
.
grpSem
.
TabStop
=
false
;
this
.
grpSem
.
Text
=
"语义检索(向量)"
;
this
.
grpSem
.
Text
=
"语义检索(向量)"
;
...
@@ -641,9 +715,9 @@
...
@@ -641,9 +715,9 @@
this
.
grpStrategy
.
Controls
.
Add
(
this
.
numToolRounds
);
this
.
grpStrategy
.
Controls
.
Add
(
this
.
numToolRounds
);
this
.
grpStrategy
.
Controls
.
Add
(
this
.
lblHistory
);
this
.
grpStrategy
.
Controls
.
Add
(
this
.
lblHistory
);
this
.
grpStrategy
.
Controls
.
Add
(
this
.
numHistory
);
this
.
grpStrategy
.
Controls
.
Add
(
this
.
numHistory
);
this
.
grpStrategy
.
Location
=
new
System
.
Drawing
.
Point
(
8
,
284
);
this
.
grpStrategy
.
Location
=
new
System
.
Drawing
.
Point
(
4
,
8
);
this
.
grpStrategy
.
Name
=
"grpStrategy"
;
this
.
grpStrategy
.
Name
=
"grpStrategy"
;
this
.
grpStrategy
.
Size
=
new
System
.
Drawing
.
Size
(
59
6
,
92
);
this
.
grpStrategy
.
Size
=
new
System
.
Drawing
.
Size
(
59
0
,
92
);
this
.
grpStrategy
.
TabIndex
=
2
;
this
.
grpStrategy
.
TabIndex
=
2
;
this
.
grpStrategy
.
TabStop
=
false
;
this
.
grpStrategy
.
TabStop
=
false
;
this
.
grpStrategy
.
Text
=
"策略参数"
;
this
.
grpStrategy
.
Text
=
"策略参数"
;
...
@@ -801,27 +875,14 @@
...
@@ -801,27 +875,14 @@
this
.
lblPromptHint
.
TabIndex
=
1
;
this
.
lblPromptHint
.
TabIndex
=
1
;
this
.
lblPromptHint
.
Text
=
"启用开关开启后,下方提示词作为追加层注入(位于安全层之前),未启用则不注入;切换角色会用所选角色的模板覆盖输入框内容(已修改时会先确认)。角色模板库位于 Agen"
+
this
.
lblPromptHint
.
Text
=
"启用开关开启后,下方提示词作为追加层注入(位于安全层之前),未启用则不注入;切换角色会用所选角色的模板覆盖输入框内容(已修改时会先确认)。角色模板库位于 Agen"
+
"t/prompts/roles.json,可直接编辑新增角色;基础层提示词模板位于 Agent/prompts 目录(*.md)。"
;
"t/prompts/roles.json,可直接编辑新增角色;基础层提示词模板位于 Agent/prompts 目录(*.md)。"
;
//
// tabOther
//
this
.
tabOther
.
AutoScroll
=
true
;
this
.
tabOther
.
Controls
.
Add
(
this
.
grpOpt
);
this
.
tabOther
.
Controls
.
Add
(
this
.
grpFail
);
this
.
tabOther
.
Location
=
new
System
.
Drawing
.
Point
(
4
,
26
);
this
.
tabOther
.
Name
=
"tabOther"
;
this
.
tabOther
.
Padding
=
new
System
.
Windows
.
Forms
.
Padding
(
3
);
this
.
tabOther
.
Size
=
new
System
.
Drawing
.
Size
(
612
,
360
);
this
.
tabOther
.
TabIndex
=
3
;
this
.
tabOther
.
Text
=
"其他"
;
this
.
tabOther
.
UseVisualStyleBackColor
=
true
;
//
//
// grpOpt
// grpOpt
//
//
this
.
grpOpt
.
Controls
.
Add
(
this
.
chkOpt
);
this
.
grpOpt
.
Controls
.
Add
(
this
.
chkOpt
);
this
.
grpOpt
.
Controls
.
Add
(
this
.
chkImageInput
);
this
.
grpOpt
.
Controls
.
Add
(
this
.
chkImageInput
);
this
.
grpOpt
.
Location
=
new
System
.
Drawing
.
Point
(
8
,
12
);
this
.
grpOpt
.
Location
=
new
System
.
Drawing
.
Point
(
4
,
8
);
this
.
grpOpt
.
Name
=
"grpOpt"
;
this
.
grpOpt
.
Name
=
"grpOpt"
;
this
.
grpOpt
.
Size
=
new
System
.
Drawing
.
Size
(
59
6
,
92
);
this
.
grpOpt
.
Size
=
new
System
.
Drawing
.
Size
(
59
0
,
216
);
this
.
grpOpt
.
TabIndex
=
0
;
this
.
grpOpt
.
TabIndex
=
0
;
this
.
grpOpt
.
TabStop
=
false
;
this
.
grpOpt
.
TabStop
=
false
;
this
.
grpOpt
.
Text
=
"可选能力"
;
this
.
grpOpt
.
Text
=
"可选能力"
;
...
@@ -858,9 +919,9 @@
...
@@ -858,9 +919,9 @@
this
.
grpFail
.
Controls
.
Add
(
this
.
lblBKey
);
this
.
grpFail
.
Controls
.
Add
(
this
.
lblBKey
);
this
.
grpFail
.
Controls
.
Add
(
this
.
txtBKey
);
this
.
grpFail
.
Controls
.
Add
(
this
.
txtBKey
);
this
.
grpFail
.
Controls
.
Add
(
this
.
lblFailHint
);
this
.
grpFail
.
Controls
.
Add
(
this
.
lblFailHint
);
this
.
grpFail
.
Location
=
new
System
.
Drawing
.
Point
(
8
,
112
);
this
.
grpFail
.
Location
=
new
System
.
Drawing
.
Point
(
4
,
8
);
this
.
grpFail
.
Name
=
"grpFail"
;
this
.
grpFail
.
Name
=
"grpFail"
;
this
.
grpFail
.
Size
=
new
System
.
Drawing
.
Size
(
59
6
,
168
);
this
.
grpFail
.
Size
=
new
System
.
Drawing
.
Size
(
59
0
,
168
);
this
.
grpFail
.
TabIndex
=
2
;
this
.
grpFail
.
TabIndex
=
2
;
this
.
grpFail
.
TabStop
=
false
;
this
.
grpFail
.
TabStop
=
false
;
this
.
grpFail
.
Text
=
"故障转移(主模型连续失败自动切备用)"
;
this
.
grpFail
.
Text
=
"故障转移(主模型连续失败自动切备用)"
;
...
@@ -984,6 +1045,8 @@
...
@@ -984,6 +1045,8 @@
this
.
grpKb
.
ResumeLayout
(
false
);
this
.
grpKb
.
ResumeLayout
(
false
);
this
.
grpKb
.
PerformLayout
();
this
.
grpKb
.
PerformLayout
();
this
.
tabAdv
.
ResumeLayout
(
false
);
this
.
tabAdv
.
ResumeLayout
(
false
);
this
.
tabAdvSub
.
ResumeLayout
(
false
);
this
.
tabSubGen
.
ResumeLayout
(
false
);
this
.
grpGen
.
ResumeLayout
(
false
);
this
.
grpGen
.
ResumeLayout
(
false
);
this
.
grpGen
.
PerformLayout
();
this
.
grpGen
.
PerformLayout
();
((
System
.
ComponentModel
.
ISupportInitialize
)(
this
.
numTemp
)).
EndInit
();
((
System
.
ComponentModel
.
ISupportInitialize
)(
this
.
numTemp
)).
EndInit
();
...
@@ -991,19 +1054,22 @@
...
@@ -991,19 +1054,22 @@
((
System
.
ComponentModel
.
ISupportInitialize
)(
this
.
numContext
)).
EndInit
();
((
System
.
ComponentModel
.
ISupportInitialize
)(
this
.
numContext
)).
EndInit
();
((
System
.
ComponentModel
.
ISupportInitialize
)(
this
.
numToolRounds
)).
EndInit
();
((
System
.
ComponentModel
.
ISupportInitialize
)(
this
.
numToolRounds
)).
EndInit
();
((
System
.
ComponentModel
.
ISupportInitialize
)(
this
.
numHistory
)).
EndInit
();
((
System
.
ComponentModel
.
ISupportInitialize
)(
this
.
numHistory
)).
EndInit
();
this
.
tabSubStrategy
.
ResumeLayout
(
false
);
this
.
grpStrategy
.
ResumeLayout
(
false
);
this
.
grpStrategy
.
ResumeLayout
(
false
);
this
.
grpStrategy
.
PerformLayout
();
this
.
grpStrategy
.
PerformLayout
();
this
.
tabSubSem
.
ResumeLayout
(
false
);
this
.
grpSem
.
ResumeLayout
(
false
);
this
.
grpSem
.
ResumeLayout
(
false
);
this
.
grpSem
.
PerformLayout
();
this
.
grpSem
.
PerformLayout
();
this
.
tabSubFail
.
ResumeLayout
(
false
);
this
.
grpFail
.
ResumeLayout
(
false
);
this
.
grpFail
.
PerformLayout
();
this
.
tabPrompt
.
ResumeLayout
(
false
);
this
.
tabPrompt
.
ResumeLayout
(
false
);
this
.
tabPrompt
.
PerformLayout
();
this
.
tabPrompt
.
PerformLayout
();
this
.
grpPrompt
.
ResumeLayout
(
false
);
this
.
grpPrompt
.
ResumeLayout
(
false
);
this
.
grpPrompt
.
PerformLayout
();
this
.
grpPrompt
.
PerformLayout
();
this
.
tab
Other
.
ResumeLayout
(
false
);
this
.
tab
SubOpt
.
ResumeLayout
(
false
);
this
.
grpOpt
.
ResumeLayout
(
false
);
this
.
grpOpt
.
ResumeLayout
(
false
);
this
.
grpOpt
.
PerformLayout
();
this
.
grpOpt
.
PerformLayout
();
this
.
grpFail
.
ResumeLayout
(
false
);
this
.
grpFail
.
PerformLayout
();
this
.
ResumeLayout
(
false
);
this
.
ResumeLayout
(
false
);
}
}
...
@@ -1035,6 +1101,12 @@
...
@@ -1035,6 +1101,12 @@
private
System
.
Windows
.
Forms
.
TextBox
txtSharePassword
;
private
System
.
Windows
.
Forms
.
TextBox
txtSharePassword
;
private
System
.
Windows
.
Forms
.
Button
btnTestShare
;
private
System
.
Windows
.
Forms
.
Button
btnTestShare
;
private
System
.
Windows
.
Forms
.
TabPage
tabAdv
;
private
System
.
Windows
.
Forms
.
TabPage
tabAdv
;
private
System
.
Windows
.
Forms
.
TabControl
tabAdvSub
;
private
System
.
Windows
.
Forms
.
TabPage
tabSubGen
;
private
System
.
Windows
.
Forms
.
TabPage
tabSubStrategy
;
private
System
.
Windows
.
Forms
.
TabPage
tabSubSem
;
private
System
.
Windows
.
Forms
.
TabPage
tabSubFail
;
private
System
.
Windows
.
Forms
.
TabPage
tabSubOpt
;
private
System
.
Windows
.
Forms
.
GroupBox
grpGen
;
private
System
.
Windows
.
Forms
.
GroupBox
grpGen
;
private
System
.
Windows
.
Forms
.
Label
lblTemp
;
private
System
.
Windows
.
Forms
.
Label
lblTemp
;
private
System
.
Windows
.
Forms
.
NumericUpDown
numTemp
;
private
System
.
Windows
.
Forms
.
NumericUpDown
numTemp
;
...
@@ -1067,7 +1139,6 @@
...
@@ -1067,7 +1139,6 @@
private
System
.
Windows
.
Forms
.
ComboBox
cmbRole
;
private
System
.
Windows
.
Forms
.
ComboBox
cmbRole
;
private
System
.
Windows
.
Forms
.
TextBox
txtCustomPrompt
;
private
System
.
Windows
.
Forms
.
TextBox
txtCustomPrompt
;
private
System
.
Windows
.
Forms
.
Label
lblPromptHint
;
private
System
.
Windows
.
Forms
.
Label
lblPromptHint
;
private
System
.
Windows
.
Forms
.
TabPage
tabOther
;
private
System
.
Windows
.
Forms
.
GroupBox
grpOpt
;
private
System
.
Windows
.
Forms
.
GroupBox
grpOpt
;
private
System
.
Windows
.
Forms
.
CheckBox
chkOpt
;
private
System
.
Windows
.
Forms
.
CheckBox
chkOpt
;
private
System
.
Windows
.
Forms
.
CheckBox
chkImageInput
;
private
System
.
Windows
.
Forms
.
CheckBox
chkImageInput
;
...
...
Tools/MyAgent/Forms/AgentSettingsForm.cs
View file @
1ac5825c
...
@@ -13,8 +13,8 @@ namespace Tools.MyAgent.Forms
...
@@ -13,8 +13,8 @@ namespace Tools.MyAgent.Forms
{
{
/// <summary>
/// <summary>
/// 智能体设置弹窗(选项卡一体化,纯 UI):
/// 智能体设置弹窗(选项卡一体化,纯 UI):
/// <para>基础配置(模型/知识库)、高级配置(
生成参数/语义检索
)、
/// <para>基础配置(模型/知识库)、高级配置(
二级页:生成参数/策略参数/语义检索/故障转移/可选能力
)、
/// 提示词(启用开关 + 角色模板 + 自定义系统提示词)、
其他(可选能力/故障转移
),无二次弹窗。</para>
/// 提示词(启用开关 + 角色模板 + 自定义系统提示词)、
数据库连接(预存连接串
),无二次弹窗。</para>
/// <para>控件布局由 AgentSettingsForm.Designer.cs(设计器)实现;本文件仅含逻辑:
/// <para>控件布局由 AgentSettingsForm.Designer.cs(设计器)实现;本文件仅含逻辑:
/// 配置绑定(BindConfig)、布局联动(ApplyProviderMode / ApplyKbMode)、交互事件与保存回写。</para>
/// 配置绑定(BindConfig)、布局联动(ApplyProviderMode / ApplyKbMode)、交互事件与保存回写。</para>
/// <para>保存语义:「保存」一次性回写全部改动;「取消」放弃全部改动。</para>
/// <para>保存语义:「保存」一次性回写全部改动;「取消」放弃全部改动。</para>
...
@@ -139,7 +139,7 @@ namespace Tools.MyAgent.Forms
...
@@ -139,7 +139,7 @@ namespace Tools.MyAgent.Forms
private
TextBox
txtToolRoles
;
private
TextBox
txtToolRoles
;
/// <summary>在「
其他」页可选能力组
追加工具权限输入区(角色白名单,一行一规则)</summary>
/// <summary>在「
可选能力」页
追加工具权限输入区(角色白名单,一行一规则)</summary>
private
void
BuildToolPermissionUi
()
private
void
BuildToolPermissionUi
()
{
{
var
lbl
=
new
Label
var
lbl
=
new
Label
...
@@ -251,11 +251,11 @@ namespace Tools.MyAgent.Forms
...
@@ -251,11 +251,11 @@ namespace Tools.MyAgent.Forms
chkPromptEnabled
.
Checked
=
_config
.
SystemPromptEnabled
;
chkPromptEnabled
.
Checked
=
_config
.
SystemPromptEnabled
;
PopulateRoles
();
PopulateRoles
();
//
其他:可选能力
//
可选能力页:可选工具开关
chkOpt
.
Checked
=
_config
.
EnableOptionalTools
;
chkOpt
.
Checked
=
_config
.
EnableOptionalTools
;
chkImageInput
.
Checked
=
_config
.
EnableImageInput
;
// v2.2 多模态:图片提问开关
chkImageInput
.
Checked
=
_config
.
EnableImageInput
;
// v2.2 多模态:图片提问开关
//
其他
:工具级权限(P3,角色白名单)
//
可选能力页
:工具级权限(P3,角色白名单)
txtToolRoles
.
Lines
=
ToolRoleRulesToLines
(
_config
.
ToolRoleAllow
);
txtToolRoles
.
Lines
=
ToolRoleRulesToLines
(
_config
.
ToolRoleAllow
);
// 数据库连接(P0):预存连接串(内存明文态)回填表格
// 数据库连接(P0):预存连接串(内存明文态)回填表格
...
@@ -264,7 +264,7 @@ namespace Tools.MyAgent.Forms
...
@@ -264,7 +264,7 @@ namespace Tools.MyAgent.Forms
foreach
(
var
kv
in
_config
.
DbConnections
.
OrderBy
(
k
=>
k
.
Key
,
StringComparer
.
OrdinalIgnoreCase
))
foreach
(
var
kv
in
_config
.
DbConnections
.
OrderBy
(
k
=>
k
.
Key
,
StringComparer
.
OrdinalIgnoreCase
))
dgvDbConn
.
Rows
.
Add
(
kv
.
Key
,
kv
.
Value
);
dgvDbConn
.
Rows
.
Add
(
kv
.
Key
,
kv
.
Value
);
//
其他:故障转移
//
故障转移页
chkFail
.
Checked
=
_config
.
EnableFailover
;
chkFail
.
Checked
=
_config
.
EnableFailover
;
txtBUrl
.
Text
=
_config
.
BackupBaseUrl
;
txtBUrl
.
Text
=
_config
.
BackupBaseUrl
;
txtBModel
.
Text
=
_config
.
BackupModel
;
txtBModel
.
Text
=
_config
.
BackupModel
;
...
@@ -562,10 +562,10 @@ namespace Tools.MyAgent.Forms
...
@@ -562,10 +562,10 @@ namespace Tools.MyAgent.Forms
_config
.
SystemPromptEnabled
=
chkPromptEnabled
.
Checked
;
_config
.
SystemPromptEnabled
=
chkPromptEnabled
.
Checked
;
_config
.
RoleTemplateId
=
(
cmbRole
.
SelectedItem
as
RoleItem
)?.
Id
??
""
;
_config
.
RoleTemplateId
=
(
cmbRole
.
SelectedItem
as
RoleItem
)?.
Id
??
""
;
_config
.
CustomSystemPrompt
=
txtCustomPrompt
.
Text
.
Trim
();
_config
.
CustomSystemPrompt
=
txtCustomPrompt
.
Text
.
Trim
();
//
其他:可选能力
//
可选能力页:可选工具开关
_config
.
EnableOptionalTools
=
chkOpt
.
Checked
;
_config
.
EnableOptionalTools
=
chkOpt
.
Checked
;
_config
.
EnableImageInput
=
chkImageInput
.
Checked
;
// v2.2 多模态:图片提问开关
_config
.
EnableImageInput
=
chkImageInput
.
Checked
;
// v2.2 多模态:图片提问开关
//
其他
:工具级权限(P3)
//
可选能力页
:工具级权限(P3)
_config
.
ToolRoleAllow
=
ParseToolRoleRules
(
txtToolRoles
.
Text
);
_config
.
ToolRoleAllow
=
ParseToolRoleRules
(
txtToolRoles
.
Text
);
// 数据库连接(P0):表格 → 字典(ID 大小写不敏感,空格清理;空 ID 行忽略)
// 数据库连接(P0):表格 → 字典(ID 大小写不敏感,空格清理;空 ID 行忽略)
var
conns
=
new
Dictionary
<
string
,
string
>(
StringComparer
.
OrdinalIgnoreCase
);
var
conns
=
new
Dictionary
<
string
,
string
>(
StringComparer
.
OrdinalIgnoreCase
);
...
@@ -577,7 +577,7 @@ namespace Tools.MyAgent.Forms
...
@@ -577,7 +577,7 @@ namespace Tools.MyAgent.Forms
conns
[
id
]
=
(
r
.
Cells
[
1
].
Value
?.
ToString
()
??
""
).
Trim
();
conns
[
id
]
=
(
r
.
Cells
[
1
].
Value
?.
ToString
()
??
""
).
Trim
();
}
}
_config
.
DbConnections
=
conns
;
_config
.
DbConnections
=
conns
;
//
其他:故障转移
//
故障转移页
_config
.
EnableFailover
=
chkFail
.
Checked
;
_config
.
EnableFailover
=
chkFail
.
Checked
;
_config
.
BackupBaseUrl
=
txtBUrl
.
Text
.
Trim
();
_config
.
BackupBaseUrl
=
txtBUrl
.
Text
.
Trim
();
_config
.
BackupModel
=
string
.
IsNullOrEmpty
(
txtBModel
.
Text
.
Trim
())
?
"qwen2.5"
:
txtBModel
.
Text
.
Trim
();
_config
.
BackupModel
=
string
.
IsNullOrEmpty
(
txtBModel
.
Text
.
Trim
())
?
"qwen2.5"
:
txtBModel
.
Text
.
Trim
();
...
...
Tools/MyAgent/Kb/DialectService.cs
0 → 100644
View file @
1ac5825c
using
System
;
using
System.Collections.Generic
;
using
System.IO
;
using
System.Linq
;
using
System.Text
;
namespace
Tools.MyAgent.Kb
{
/// <summary>
/// 方言服务(轻量化内化版):扫描程序目录 Agent/dialect/(产品排查方言)与 Agent/industry/(行业知识),
/// 解析 INDEX.md 导航(行业名/别名/大类/主题/触发词),生成折叠摘要(触发词路由,≤2KB 预算),提供沙箱解析。
/// <para>相对源 Common.Agent 的 ModuleService/IndustryService 裁剪:去掉多公司层、module.json 动态开关、
/// 依赖/冲突/回滚、启用过滤与写入机制——单公司内化、内置只读。</para>
/// INDEX.md 格式约定(与源兼容):
/// <code>
/// # 鞋业(别名:制鞋、鞋类)
/// 行业简介:…
/// ## 01-品类认知
/// - 运动鞋.md:跑鞋/篮球鞋(触发词:运动鞋、跑鞋)
/// </code>
/// </summary>
public
sealed
class
DialectService
{
private
readonly
string
_dialectRoot
;
private
readonly
string
_industryRoot
;
private
DialectInfo
_dialectCache
;
private
List
<
DialectInfo
>
_industryCache
;
/// <summary>行业/方言根目录是否可用(任一存在即视为可用)</summary>
public
bool
IsAvailable
=>
(!
string
.
IsNullOrEmpty
(
_dialectRoot
)
&&
Directory
.
Exists
(
_dialectRoot
))
||
(!
string
.
IsNullOrEmpty
(
_industryRoot
)
&&
Directory
.
Exists
(
_industryRoot
));
/// <summary>产品方言根目录</summary>
public
string
DialectRoot
=>
_dialectRoot
;
/// <summary>行业知识根目录</summary>
public
string
IndustryRoot
=>
_industryRoot
;
public
DialectService
(
string
agentRoot
)
{
_dialectRoot
=
string
.
IsNullOrEmpty
(
agentRoot
)
?
""
:
Path
.
Combine
(
agentRoot
,
"dialect"
);
_industryRoot
=
string
.
IsNullOrEmpty
(
agentRoot
)
?
""
:
Path
.
Combine
(
agentRoot
,
"industry"
);
}
/// <summary>单个主题(大类 + 文件 + 说明 + 触发词)</summary>
public
sealed
class
DialectTopic
{
public
string
Category
=
""
;
// 大类名,如 01-品类认知
public
string
File
=
""
;
// 主题文件名(可含子路径),如 运动鞋.md、erp/winforms.md
public
string
Title
=
""
;
// 主题名(去 .md)
public
string
Description
=
""
;
public
string
[]
Triggers
=
Array
.
Empty
<
string
>();
}
/// <summary>单个方言/行业(目录 + INDEX.md 解析结果)</summary>
public
sealed
class
DialectInfo
{
public
string
Name
=
""
;
// 名称(行业名/方言名)
public
string
Intro
=
""
;
public
string
[]
Aliases
=
Array
.
Empty
<
string
>();
public
List
<
DialectTopic
>
Topics
=
new
List
<
DialectTopic
>();
public
string
Root
=
""
;
// 该导航对应的根目录
}
/// <summary>产品方言导航(dialect/INDEX.md)</summary>
public
DialectInfo
ScanDialect
()
{
if
(
_dialectCache
!=
null
)
return
_dialectCache
;
var
info
=
new
DialectInfo
{
Name
=
"产品排查方言"
,
Root
=
_dialectRoot
};
string
indexFile
=
Path
.
Combine
(
_dialectRoot
,
"INDEX.md"
);
if
(
File
.
Exists
(
indexFile
))
ParseIndex
(
indexFile
,
info
);
_dialectCache
=
info
;
return
info
;
}
/// <summary>全部行业导航(industry/<行业>/INDEX.md)</summary>
public
IEnumerable
<
DialectInfo
>
ScanIndustries
()
{
if
(
_industryCache
!=
null
)
return
_industryCache
;
var
list
=
new
List
<
DialectInfo
>();
if
(
Directory
.
Exists
(
_industryRoot
))
{
foreach
(
var
dir
in
Directory
.
GetDirectories
(
_industryRoot
))
{
string
indexFile
=
Path
.
Combine
(
dir
,
"INDEX.md"
);
if
(!
File
.
Exists
(
indexFile
))
continue
;
var
ind
=
new
DialectInfo
{
Name
=
Path
.
GetFileName
(
dir
),
Root
=
dir
};
ParseIndex
(
indexFile
,
ind
);
list
.
Add
(
ind
);
}
}
_industryCache
=
list
;
return
list
;
}
private
static
void
ParseIndex
(
string
indexFile
,
DialectInfo
ind
)
{
string
category
=
""
;
foreach
(
var
raw
in
File
.
ReadAllLines
(
indexFile
,
Encoding
.
UTF8
))
{
string
line
=
(
raw
??
""
).
Trim
();
if
(
line
.
StartsWith
(
"## "
,
StringComparison
.
Ordinal
))
{
category
=
line
.
Substring
(
3
).
Trim
();
continue
;
}
if
(
line
.
StartsWith
(
"# "
)
&&
!
line
.
StartsWith
(
"## "
))
{
ind
.
Intro
=
line
.
Substring
(
2
).
Trim
();
// 别名解析:# 行业名(别名:鞋服、服装、箱包)
int
ap
=
ind
.
Intro
.
IndexOf
(
"(别名:"
,
StringComparison
.
Ordinal
);
if
(
ap
>
0
)
{
string
rest
=
ind
.
Intro
.
Substring
(
ap
+
"(别名:"
.
Length
);
int
end
=
rest
.
IndexOf
(
')'
);
if
(
end
>
0
)
ind
.
Aliases
=
rest
.
Substring
(
0
,
end
)
.
Split
(
new
[]
{
'、'
,
','
,
','
},
StringSplitOptions
.
RemoveEmptyEntries
)
.
Select
(
s
=>
s
.
Trim
()).
Where
(
s
=>
s
.
Length
>
0
).
ToArray
();
}
continue
;
}
if
(
line
.
StartsWith
(
"- "
)
&&
line
.
IndexOf
(
".md"
,
StringComparison
.
OrdinalIgnoreCase
)
>
0
)
{
string
body
=
line
.
Substring
(
2
).
Trim
();
int
sep
=
body
.
IndexOfAny
(
new
[]
{
':'
,
':'
});
if
(
sep
<=
0
)
continue
;
string
file
=
body
.
Substring
(
0
,
sep
).
Trim
();
string
rest
=
body
.
Substring
(
sep
+
1
).
Trim
();
// 主题路径补全:File 含子路径(如 dialect 的 "erp/winforms.md")则原样保留;
// 纯文件名(行业 INDEX,文件在大类目录下)补全为 "{Category}/{File}",便于模型直接构造 d_read 路径
if
(
file
.
IndexOf
(
'/'
)
<
0
&&
file
.
IndexOf
(
'\\'
)
<
0
&&
category
.
Length
>
0
)
file
=
category
+
"/"
+
file
;
var
topic
=
new
DialectTopic
{
Category
=
category
,
File
=
file
,
Title
=
Path
.
GetFileNameWithoutExtension
(
file
)
};
int
tg
=
rest
.
IndexOf
(
"(触发词:"
,
StringComparison
.
Ordinal
);
if
(
tg
>
0
)
{
topic
.
Description
=
rest
.
Substring
(
0
,
tg
).
Trim
();
string
trigPart
=
rest
.
Substring
(
tg
+
"(触发词:"
.
Length
);
int
end
=
trigPart
.
IndexOf
(
')'
);
if
(
end
>
0
)
trigPart
=
trigPart
.
Substring
(
0
,
end
);
topic
.
Triggers
=
trigPart
.
Split
(
new
[]
{
'、'
,
','
,
','
},
StringSplitOptions
.
RemoveEmptyEntries
)
.
Select
(
s
=>
s
.
Trim
()).
Where
(
s
=>
s
.
Length
>
0
).
ToArray
();
}
else
topic
.
Description
=
rest
;
ind
.
Topics
.
Add
(
topic
);
}
}
}
/// <summary>
/// 折叠摘要(产品方言 + 全部行业,总计 ≤2KB):名称 + 别名 + 大类列表 + 汇总触发词(去重截断)。
/// 主题级详情不注入,模型需要时 d_read 精读。
/// </summary>
public
string
BuildSummary
()
{
var
parts
=
new
List
<
string
>();
var
dialect
=
ScanDialect
();
if
(
dialect
.
Topics
.
Count
>
0
)
parts
.
Add
(
BuildInfoLine
(
dialect
,
"产品排查方言"
));
foreach
(
var
ind
in
ScanIndustries
())
parts
.
Add
(
BuildInfoLine
(
ind
,
ind
.
Name
));
if
(
parts
.
Count
==
0
)
return
""
;
// 无任何内置知识导航:不注入空层
var
sb
=
new
StringBuilder
();
sb
.
AppendLine
(
"【产品方言与行业知识】(内置只读知识,随程序发布;业务语境问题先按触发词路由 → d_grep 定位 → d_read 精读,禁止一次性全量读取)"
);
const
int
totalBudget
=
2048
;
// 摘要总预算
int
used
=
Encoding
.
UTF8
.
GetByteCount
(
sb
.
ToString
());
foreach
(
var
line
in
parts
)
{
int
lineBytes
=
Encoding
.
UTF8
.
GetByteCount
(
line
+
"\n"
);
if
(
used
+
lineBytes
>
totalBudget
)
break
;
sb
.
AppendLine
(
line
);
used
+=
lineBytes
;
}
return
sb
.
ToString
();
}
private
static
string
BuildInfoLine
(
DialectInfo
ind
,
string
displayName
)
{
string
line
=
"- "
+
displayName
;
if
(
ind
.
Topics
.
Count
>
0
)
{
var
cats
=
ind
.
Topics
.
Select
(
t
=>
t
.
Category
).
Where
(
c
=>
c
.
Length
>
0
).
Distinct
();
if
(
cats
.
Any
())
line
+=
"("
+
string
.
Join
(
"/"
,
cats
)
+
")"
;
var
trigs
=
ind
.
Topics
.
SelectMany
(
t
=>
t
.
Triggers
).
Distinct
().
Take
(
12
).
ToList
();
if
(
trigs
.
Count
>
0
)
line
+=
":"
+
string
.
Join
(
"、"
,
trigs
);
}
if
(
ind
.
Aliases
.
Length
>
0
)
line
+=
"(别名:"
+
string
.
Join
(
"、"
,
ind
.
Aliases
)
+
")"
;
return
TruncateBytes
(
line
,
1024
);
}
private
static
string
TruncateBytes
(
string
s
,
int
maxBytes
)
{
var
sb
=
new
StringBuilder
();
foreach
(
char
c
in
s
)
{
if
(
Encoding
.
UTF8
.
GetByteCount
(
sb
.
ToString
()
+
c
)
>
maxBytes
)
break
;
sb
.
Append
(
c
);
}
return
sb
.
ToString
()
+
(
sb
.
Length
<
s
.
Length
?
"…"
:
""
);
}
/// <summary>
/// 沙箱解析 d_* 工具路径:"dialect/..." 或 "industry/..."(相对各自根目录);
/// 越界/符号链接/不存在返回 null。复用 KbService.ResolveIn(NormPath + IsWithinRoot + reparse point 防护)。
/// </summary>
public
string
ResolveSafe
(
string
path
)
{
if
(
string
.
IsNullOrEmpty
(
path
))
return
null
;
string
p
=
path
.
Replace
(
'/'
,
'\\'
);
if
(
p
.
StartsWith
(
"dialect\\"
,
StringComparison
.
OrdinalIgnoreCase
))
return
ResolveIn
(
_dialectRoot
,
p
.
Substring
(
"dialect\\"
.
Length
));
if
(
p
.
StartsWith
(
"industry\\"
,
StringComparison
.
OrdinalIgnoreCase
))
return
ResolveIn
(
_industryRoot
,
p
.
Substring
(
"industry\\"
.
Length
));
return
null
;
}
private
static
string
ResolveIn
(
string
root
,
string
relative
)
{
try
{
return
KbService
.
ResolveIn
(
root
,
relative
,
"内置方言目录不可用。"
);
}
catch
{
return
null
;
}
}
/// <summary>递归枚举内置知识文件(*.md),返回 "dialect/..." 或 "industry/..." 形式路径;供 d_list/d_grep 使用</summary>
public
IEnumerable
<
string
>
EnumerateFiles
()
{
foreach
(
var
f
in
EnumerateMd
(
_dialectRoot
,
"dialect"
))
yield
return
f
;
foreach
(
var
f
in
EnumerateMd
(
_industryRoot
,
"industry"
))
yield
return
f
;
}
private
static
IEnumerable
<
string
>
EnumerateMd
(
string
root
,
string
prefix
)
{
if
(
string
.
IsNullOrEmpty
(
root
)
||
!
Directory
.
Exists
(
root
))
yield
break
;
foreach
(
var
f
in
Directory
.
GetFiles
(
root
,
"*.md"
,
SearchOption
.
AllDirectories
))
{
string
rel
=
f
.
Substring
(
root
.
Length
).
TrimStart
(
'\\'
,
'/'
).
Replace
(
'\\'
,
'/'
);
yield
return
prefix
+
"/"
+
rel
;
}
}
/// <summary>缓存失效:目录外部变动后调用,下次扫描重扫</summary>
public
void
Invalidate
()
{
_dialectCache
=
null
;
_industryCache
=
null
;
}
}
}
Tools/MyAgent/Kb/KbService.cs
View file @
1ac5825c
...
@@ -36,6 +36,7 @@ namespace Tools.MyAgent.Kb
...
@@ -36,6 +36,7 @@ namespace Tools.MyAgent.Kb
private
const
int
MaxScanFileSize
=
4
*
1024
*
1024
;
// 内容检索跳过 > 4MB 的文件
private
const
int
MaxScanFileSize
=
4
*
1024
*
1024
;
// 内容检索跳过 > 4MB 的文件
private
const
int
MaxToolOutput
=
8000
;
// 工具返回文本上限(字符)
private
const
int
MaxToolOutput
=
8000
;
// 工具返回文本上限(字符)
private
const
int
MaxReadChars
=
12000
;
// kb_read 单次返回上限(字符)
private
const
int
MaxReadChars
=
12000
;
// kb_read 单次返回上限(字符)
internal
const
long
MaxReadFileSize
=
30L
*
1024
*
1024
;
// kb_read/file_read 单文件大小闸门:> 30MB 拒绝整读(防 OOM)
/// <summary>检索索引(SQLite,含 docx/xlsx 内容;未就绪时检索回退全盘扫描)</summary>
/// <summary>检索索引(SQLite,含 docx/xlsx 内容;未就绪时检索回退全盘扫描)</summary>
private
KbIndex
_index
;
private
KbIndex
_index
;
...
@@ -1323,13 +1324,20 @@ namespace Tools.MyAgent.Kb
...
@@ -1323,13 +1324,20 @@ namespace Tools.MyAgent.Kb
if
(
limit
<
1
)
limit
=
1
;
if
(
limit
<
1
)
limit
=
1
;
if
(
limit
>
500
)
limit
=
500
;
if
(
limit
>
500
)
limit
=
500
;
// 文件大小闸门:提取前先看大小,避免超大文件整读进内存(OOM/卡死)——
// 检索类已有 MaxScanFileSize 闸门,读取路径此前缺失(漏洞修复)
var
fi
=
new
FileInfo
(
fullPath
);
if
(
fi
.
Length
>
MaxReadFileSize
)
return
$"错误:文件过大(
{
fi
.
Length
/
1024
/
1024
}
MB,上限
{
MaxReadFileSize
/
1024
/
1024
}
MB),已跳过整读。"
+
$"请改用 kb_grep 检索定位相关内容,或把文件拆分成小块后再读。"
;
string
ext
=
Path
.
GetExtension
(
fullPath
).
ToLowerInvariant
();
string
ext
=
Path
.
GetExtension
(
fullPath
).
ToLowerInvariant
();
string
content
=
null
;
string
content
=
null
;
if
(
ext
==
".ppt"
)
if
(
ext
==
".ppt"
)
return
$"暂不支持旧版 .ppt 二进制格式:
{
relPath
}
。可另存为 .pptx 后使用。"
;
return
$"暂不支持旧版 .ppt 二进制格式:
{
relPath
}
。可另存为 .pptx 后使用。"
;
else
if
(
IsImageExtension
(
ext
))
else
if
(
IsImageExtension
(
ext
))
return
$"图片文件:
{
relPath
}
(当前不支持直接读取图片内容
,可描述图片或先查看文件名)。
"
;
return
$"图片文件:
{
relPath
}
(当前不支持直接读取图片内容
;可改用 kb_view_image 把图片作为附图发送给视觉模型识别(图片提问),或启用「图片提问」后手动发送)
"
;
else
else
{
{
// 统一走解析器注册表(X1):新格式只需注册 IFileParser,无需改本分支
// 统一走解析器注册表(X1):新格式只需注册 IFileParser,无需改本分支
...
@@ -1338,11 +1346,11 @@ namespace Tools.MyAgent.Kb
...
@@ -1338,11 +1346,11 @@ namespace Tools.MyAgent.Kb
}
}
if
(
content
==
null
)
if
(
content
==
null
)
return
$"二进制、扫描件(无文字层)或不可解析的文件:
{
relPath
}
。"
;
return
$"二进制、扫描件(无文字层)或不可解析的文件:
{
relPath
}
。
扫描件可改用 kb_view_image 把页面作为图片发送给视觉模型识别(图片提问,无需 OCR)。
"
;
if
(
content
.
StartsWith
(
"错误"
))
if
(
content
.
StartsWith
(
"错误"
))
return
content
;
// 提取层的业务错误(如工作表不存在)直接透传
return
content
;
// 提取层的业务错误(如工作表不存在)直接透传
var
lines
=
content
.
Replace
(
"\r\n"
,
"\n"
).
Replace
(
'\r'
,
'\n'
).
Split
(
'\n'
);
var
lines
=
SplitLines
(
content
);
if
(
offset
>
lines
.
Length
)
offset
=
lines
.
Length
;
if
(
offset
>
lines
.
Length
)
offset
=
lines
.
Length
;
int
start
=
offset
;
int
start
=
offset
;
int
end
=
Math
.
Min
(
lines
.
Length
,
offset
+
limit
-
1
);
int
end
=
Math
.
Min
(
lines
.
Length
,
offset
+
limit
-
1
);
...
@@ -1371,33 +1379,19 @@ namespace Tools.MyAgent.Kb
...
@@ -1371,33 +1379,19 @@ namespace Tools.MyAgent.Kb
}
}
/// <summary>
/// <summary>
/// 读取文本文件(自动识别 UTF-8/
GB2312 等编码,兼容中文 Windows
)
/// 读取文本文件(自动识别 UTF-8/
UTF-16/UTF-32/GB18030/系统 ANSI 等编码,兼容中文 Windows;多编码兼容见 TextEncoding
)
/// </summary>
/// </summary>
internal
static
string
ReadTextFile
(
string
path
)
internal
static
string
ReadTextFile
(
string
path
)
{
{
try
return
TextEncoding
.
ReadAllTextAuto
(
path
);
{
}
byte
[]
bytes
=
File
.
ReadAllBytes
(
path
);
if
(
bytes
.
Length
==
0
)
return
""
;
/// <summary>
if
(
bytes
.
Length
>=
3
&&
bytes
[
0
]
==
0xEF
&&
bytes
[
1
]
==
0xBB
&&
bytes
[
2
]
==
0xBF
)
/// 统一行切分:任意换行风格(CRLF/CR/LF)归一为 \n 后切行,供各读取工具(file_read/module_read/industry_read/kb_read)共用
return
new
UTF8Encoding
(
false
).
GetString
(
bytes
,
3
,
bytes
.
Length
-
3
);
/// </summary>
if
(
bytes
.
Length
>=
2
&&
bytes
[
0
]
==
0xFF
&&
bytes
[
1
]
==
0xFE
)
internal
static
string
[]
SplitLines
(
string
content
)
return
Encoding
.
Unicode
.
GetString
(
bytes
,
2
,
bytes
.
Length
-
2
);
{
if
(
bytes
.
Length
>=
2
&&
bytes
[
0
]
==
0xFE
&&
bytes
[
1
]
==
0xFF
)
return
(
content
??
""
).
Replace
(
"\r\n"
,
"\n"
).
Replace
(
'\r'
,
'\n'
).
Split
(
'\n'
);
return
Encoding
.
BigEndianUnicode
.
GetString
(
bytes
,
2
,
bytes
.
Length
-
2
);
try
{
return
new
UTF8Encoding
(
false
,
true
).
GetString
(
bytes
);
}
catch
(
DecoderFallbackException
)
{
return
Encoding
.
GetEncoding
(
"GB2312"
).
GetString
(
bytes
);
}
}
catch
{
return
null
;
}
}
}
/// <summary>提取 docx 文本(段落 + 表格单元格)</summary>
/// <summary>提取 docx 文本(段落 + 表格单元格)</summary>
...
@@ -1457,7 +1451,9 @@ namespace Tools.MyAgent.Kb
...
@@ -1457,7 +1451,9 @@ namespace Tools.MyAgent.Kb
NPOI
.
SS
.
UserModel
.
ISheet
sheetObj
=
ResolveSheet
(
wb
,
sheet
);
NPOI
.
SS
.
UserModel
.
ISheet
sheetObj
=
ResolveSheet
(
wb
,
sheet
);
if
(
sheetObj
==
null
)
if
(
sheetObj
==
null
)
return
$"错误:工作表不存在:
{
sheet
??
"0"
}
(本文件共
{
wb
.
NumberOfSheets
}
个工作表)"
;
return
$"错误:工作表不存在:
{
sheet
??
"0"
}
(本文件共
{
wb
.
NumberOfSheets
}
个工作表)"
;
return
ReadSheet
(
sheetObj
,
maxRow
:
500
,
maxCol
:
30
);
// P2 证据出处:输出头部标注工作表名(与索引的【表名】前缀一致,模型可据此引用)
string
body
=
ReadSheet
(
sheetObj
,
maxRow
:
500
,
maxCol
:
30
);
return
$"【工作表:
{
sheetObj
.
SheetName
}
】\n"
+
body
;
}
}
finally
finally
{
{
...
...
Tools/MyUtils/TextEncoding.cs
0 → 100644
View file @
1ac5825c
using
System
;
using
System.IO
;
using
System.Text
;
namespace
Tools.MyUtils
{
/// <summary>
/// 文本编码识别与编解码工具(纯静态、无状态,SRP:编码/类型识别独立成类,供 KbService 与 Tools 各模块复用)。
/// <para>能力:BOM/无 BOM 多编码自动识别(UTF-8/16/32、GB18030/GB2312、系统 ANSI)、自动编码读取、
/// magic bytes 文件类型识别、UTF8/Unicode/GB2312/Hex 编解码。</para>
/// <para>兼容 .NET Framework 4.8(GB18030 = codepage 54936 原生支持,零新增依赖)。</para>
/// </summary>
public
static
class
TextEncoding
{
/// <summary>UTF-8 无 BOM(严格校验失败回退用)</summary>
private
static
readonly
Encoding
Utf8NoBom
=
new
UTF8Encoding
(
false
);
/// <summary>UTF-8 带 BOM(BOM 由调用方按 BomLength 剥离)</summary>
private
static
readonly
Encoding
Utf8Bom
=
new
UTF8Encoding
(
true
);
/// <summary>UTF-32 LE(BOM 由调用方按 BomLength 剥离)</summary>
private
static
readonly
Encoding
Utf32Le
=
new
UTF32Encoding
(
false
,
true
);
/// <summary>UTF-32 BE</summary>
private
static
readonly
Encoding
Utf32Be
=
new
UTF32Encoding
(
true
,
true
);
/// <summary>GB18030(GB2312 超集:1:1 兼容 GB2312,并覆盖 GBK/GB18030 生僻字;codepage 54936)</summary>
private
static
readonly
Encoding
Gb18030
=
Encoding
.
GetEncoding
(
54936
);
/// <summary>无 BOM UTF-16 启发式检测的采样字节数</summary>
private
const
int
Utf16ProbeBytes
=
4096
;
/// <summary>无 BOM UTF-16 启发式判定阈值:一侧 NUL 占比 ≥ 高阈值且另一侧 ≤ 低阈值才判定(保守,防误判)</summary>
private
const
double
NulRatioHigh
=
0.30
;
private
const
double
NulRatioLow
=
0.01
;
/// <summary>GB18030 合法性采样字节数</summary>
private
const
int
Gb18030ProbeBytes
=
4096
;
// ────────────────────────── 编码识别 ──────────────────────────
/// <summary>
/// 自动识别字节流的文本编码(需求 #1:多编码兼容)。
/// <para>识别顺序(按可靠性递减):UTF-8 BOM → UTF-32 LE/BE BOM → UTF-16 LE/BE BOM →
/// 无 BOM 严格 UTF-8 序列校验(含 overlong/代理区/超范围拒绝)→ 无 BOM UTF-16 NUL 分布启发式 →
/// GB18030(GB2312 超集)→ 系统 ANSI(Encoding.Default)。</para>
/// </summary>
/// <param name="bytes">文件原始字节(null/空返回 UTF-8 无 BOM,与空文本语义一致)</param>
/// <returns>识别出的编码;UTF-16/32 带 BOM 时返回对应带 BOM 的编码实例(BOM 剥离见 BomLength)</returns>
public
static
Encoding
DetectEncoding
(
byte
[]
bytes
)
{
if
(
bytes
==
null
||
bytes
.
Length
==
0
)
return
Utf8NoBom
;
// 1. UTF-8 BOM:EF BB BF
if
(
bytes
.
Length
>=
3
&&
bytes
[
0
]
==
0xEF
&&
bytes
[
1
]
==
0xBB
&&
bytes
[
2
]
==
0xBF
)
return
Utf8Bom
;
// 2. UTF-32 LE BOM:FF FE 00 00(须先于 UTF-16 LE 判断:以其 FF FE 前缀开头)
if
(
bytes
.
Length
>=
4
&&
bytes
[
0
]
==
0xFF
&&
bytes
[
1
]
==
0xFE
&&
bytes
[
2
]
==
0x00
&&
bytes
[
3
]
==
0x00
)
return
Utf32Le
;
// 3. UTF-32 BE BOM:00 00 FE FF
if
(
bytes
.
Length
>=
4
&&
bytes
[
0
]
==
0x00
&&
bytes
[
1
]
==
0x00
&&
bytes
[
2
]
==
0xFE
&&
bytes
[
3
]
==
0xFF
)
return
Utf32Be
;
// 4. UTF-16 LE BOM:FF FE
if
(
bytes
.
Length
>=
2
&&
bytes
[
0
]
==
0xFF
&&
bytes
[
1
]
==
0xFE
)
return
Encoding
.
Unicode
;
// 5. UTF-16 BE BOM:FE FF
if
(
bytes
.
Length
>=
2
&&
bytes
[
0
]
==
0xFE
&&
bytes
[
1
]
==
0xFF
)
return
Encoding
.
BigEndianUnicode
;
// 6. 无 BOM:全量严格 UTF-8 序列校验(含 overlong/代理区/超 U+10FFFF 拒绝)
if
(
IsValidUtf8
(
bytes
))
return
Utf8NoBom
;
// 7. 无 BOM:UTF-16 NUL 分布启发式(ASCII 字符高字节为 0x00:LE 中 NUL 集中在奇数下标,BE 中在偶数下标)
Encoding
utf16
=
DetectUtf16ByNulDistribution
(
bytes
);
if
(
utf16
!=
null
)
return
utf16
;
// 8. 回退 GB18030(GB2312 超集,覆盖生僻字);采样不可解码(二进制)时最终回退系统 ANSI
return
IsValidGb18030Sample
(
bytes
)
?
Gb18030
:
Encoding
.
Default
;
}
/// <summary>
/// 自动识别编码并读取整个文本文件(返回 null 表示读取失败或文件不存在)。
/// <para>BOM 自动剥离;所有候选编码均以 ReplacementFallback 解码(DetectEncoding 已保证字节可解码),无解码异常路径。</para>
/// </summary>
public
static
string
ReadAllTextAuto
(
string
path
)
{
try
{
byte
[]
bytes
=
File
.
ReadAllBytes
(
path
);
if
(
bytes
.
Length
==
0
)
return
""
;
Encoding
enc
=
DetectEncoding
(
bytes
);
int
bomLen
=
BomLength
(
enc
,
bytes
);
return
enc
.
GetString
(
bytes
,
bomLen
,
bytes
.
Length
-
bomLen
);
}
catch
{
return
null
;
}
}
/// <summary>BOM 字节数(按 DetectEncoding 结果剥离;无 BOM 返回 0)</summary>
internal
static
int
BomLength
(
Encoding
enc
,
byte
[]
bytes
)
{
if
(
bytes
==
null
||
bytes
.
Length
<
2
)
return
0
;
int
cp
=
enc
!=
null
?
enc
.
CodePage
:
0
;
if
(
cp
==
65001
&&
bytes
.
Length
>=
3
&&
bytes
[
0
]
==
0xEF
&&
bytes
[
1
]
==
0xBB
&&
bytes
[
2
]
==
0xBF
)
return
3
;
// UTF-8
if
(
cp
==
12000
&&
bytes
.
Length
>=
4
&&
bytes
[
0
]
==
0xFF
&&
bytes
[
1
]
==
0xFE
&&
bytes
[
2
]
==
0x00
&&
bytes
[
3
]
==
0x00
)
return
4
;
// UTF-32 LE
if
(
cp
==
12001
&&
bytes
.
Length
>=
4
&&
bytes
[
0
]
==
0x00
&&
bytes
[
1
]
==
0x00
&&
bytes
[
2
]
==
0xFE
&&
bytes
[
3
]
==
0xFF
)
return
4
;
// UTF-32 BE
if
(
cp
==
1200
&&
bytes
.
Length
>=
2
&&
bytes
[
0
]
==
0xFF
&&
bytes
[
1
]
==
0xFE
)
return
2
;
// UTF-16 LE
if
(
cp
==
1201
&&
bytes
.
Length
>=
2
&&
bytes
[
0
]
==
0xFE
&&
bytes
[
1
]
==
0xFF
)
return
2
;
// UTF-16 BE
return
0
;
}
// ────────────────────────── 文件类型识别 ──────────────────────────
/// <summary>
/// magic bytes 文件类型识别(无扩展名/扩展名可疑时辅助判定)。
/// <para>扩展名非空时直接返回扩展名(源行为,保持兼容);扩展名为空时读取文件头 7 字节判定
/// (bmp/jpg/png/gif/tiff/psd/xls/zip/xlsx/docx/rar/rtf/xml/html/exe/pdf 共 16 类)。</para>
/// <para>说明:xls 与 doc(OLE2)、zip 与 xlsx/docx(PK)文件头相同,无法仅凭头部细分,返回复合标识。</para>
/// </summary>
/// <param name="path">文件路径</param>
/// <returns>类型标识;文件不存在或无法识别返回 "unknown"</returns>
public
static
string
GetFileType
(
string
path
)
{
if
(
string
.
IsNullOrEmpty
(
path
)
||
!
File
.
Exists
(
path
))
return
"unknown"
;
var
fileInfo
=
new
FileInfo
(
path
);
// 扩展名不为空,直接返回扩展名(去点,小写归一)
if
(!
string
.
IsNullOrEmpty
(
fileInfo
.
Extension
))
return
fileInfo
.
Extension
.
TrimStart
(
'.'
).
ToLowerInvariant
();
// 扩展名为空:按最长文件头(pdf 7 字节)读取判定
try
{
byte
[]
head
=
new
byte
[
7
];
int
n
;
using
(
var
fs
=
new
FileStream
(
path
,
FileMode
.
Open
,
FileAccess
.
Read
,
FileShare
.
ReadWrite
))
n
=
fs
.
Read
(
head
,
0
,
head
.
Length
);
string
hex
=
ToHex
(
head
,
n
);
if
(
hex
.
StartsWith
(
"424D"
))
return
"bmp"
;
// 2 字节
if
(
hex
.
StartsWith
(
"FFD8FF"
))
return
"jpg"
;
// 3 字节
if
(
hex
.
StartsWith
(
"89504E47"
))
return
"png"
;
// 4 字节
if
(
hex
.
StartsWith
(
"47494638"
))
return
"gif"
;
if
(
hex
.
StartsWith
(
"49492A00"
))
return
"tiff"
;
if
(
hex
.
StartsWith
(
"38425053"
))
return
"psd"
;
if
(
hex
.
StartsWith
(
"D0CF11E0"
))
return
"xls/doc"
;
// OLE2 复合文档
if
(
hex
.
StartsWith
(
"504B0304"
))
return
"zip/xlsx/docx"
;
// PK zip
if
(
hex
.
StartsWith
(
"52617221"
))
return
"rar"
;
if
(
hex
.
StartsWith
(
"7B5C727466"
))
return
"rtf"
;
// 5 字节
if
(
hex
.
StartsWith
(
"3C3F786D6C"
))
return
"xml"
;
if
(
hex
.
StartsWith
(
"68746D6C3E"
))
return
"html"
;
if
(
hex
.
StartsWith
(
"4D5A900003"
))
return
"exe/dll/drv/vxd/sys/ocx/vbx"
;
if
(
hex
.
StartsWith
(
"255044462D312E"
))
return
"pdf"
;
// 7 字节
return
"unknown"
;
}
catch
{
return
"unknown"
;
}
}
// ────────────────────────── 编解码工具 ──────────────────────────
/// <summary>byte[] 转 string:UTF-8</summary>
public
static
string
UTF8
(
byte
[]
bytes
)
{
return
Encoding
.
UTF8
.
GetString
(
bytes
??
new
byte
[
0
]);
}
/// <summary>string 转 byte[]:UTF-8</summary>
public
static
byte
[]
UTF8
(
string
s
)
{
return
Encoding
.
UTF8
.
GetBytes
(
s
??
""
);
}
/// <summary>byte[] 转 string:Unicode(UTF-16 LE)</summary>
public
static
string
Unicode
(
byte
[]
bytes
)
{
return
Encoding
.
Unicode
.
GetString
(
bytes
??
new
byte
[
0
]);
}
/// <summary>string 转 byte[]:Unicode(UTF-16 LE)</summary>
public
static
byte
[]
Unicode
(
string
s
)
{
return
Encoding
.
Unicode
.
GetBytes
(
s
??
""
);
}
/// <summary>byte[] 转 string:GB2312</summary>
public
static
string
GB2312
(
byte
[]
bytes
)
{
return
Encoding
.
GetEncoding
(
"gb2312"
).
GetString
(
bytes
??
new
byte
[
0
]);
}
/// <summary>string 转 byte[]:GB2312(超出 GB2312 字符集范围的字符由编码器替换)</summary>
public
static
byte
[]
GB2312
(
string
s
)
{
return
Encoding
.
GetEncoding
(
"gb2312"
).
GetBytes
(
s
??
""
);
}
/// <summary>byte[] 转 16 进制字符串(count 省略 = 全部字节;每字节两位大写)</summary>
public
static
string
ToHex
(
byte
[]
bytes
,
int
count
=
-
1
)
{
if
(
bytes
==
null
)
return
""
;
if
(
count
<
0
||
count
>
bytes
.
Length
)
count
=
bytes
.
Length
;
var
sb
=
new
StringBuilder
(
count
*
2
);
for
(
int
i
=
0
;
i
<
count
;
i
++)
sb
.
Append
(
bytes
[
i
].
ToString
(
"X2"
));
return
sb
.
ToString
();
}
/// <summary>16 进制字符串转 byte[](忽略首尾空白;非法字符/奇数长度抛 FormatException)</summary>
public
static
byte
[]
FromHex
(
string
hex
)
{
if
(
string
.
IsNullOrEmpty
(
hex
))
return
new
byte
[
0
];
hex
=
hex
.
Trim
();
if
(
hex
.
Length
%
2
!=
0
)
throw
new
FormatException
(
"十六进制字符串长度必须为偶数:"
+
hex
);
byte
[]
result
=
new
byte
[
hex
.
Length
/
2
];
for
(
int
i
=
0
;
i
<
result
.
Length
;
i
++)
result
[
i
]
=
Convert
.
ToByte
(
hex
.
Substring
(
i
*
2
,
2
),
16
);
return
result
;
}
// ────────────────────────── 内部判定 ──────────────────────────
/// <summary>
/// 全量严格 UTF-8 序列校验(无 BOM 判定核心)。
/// <para>拒绝:0x80-0xC1/0xF5-0xFF 非法首字节、截断序列、非 10xxxxxx 续字节、
/// overlong(C0/C1、E0 9x、F0 8x)、UTF-16 代理区(ED A0-BF)、超 U+10FFFF(F4 90+)。</para>
/// </summary>
private
static
bool
IsValidUtf8
(
byte
[]
bytes
)
{
int
i
=
0
,
n
=
bytes
.
Length
;
while
(
i
<
n
)
{
byte
b
=
bytes
[
i
];
if
(
b
<
0x80
)
{
i
++;
continue
;
}
int
need
;
if
(
b
>=
0xC2
&&
b
<=
0xDF
)
need
=
1
;
// 2 字节(0xC0/0xC1 = overlong,排除)
else
if
(
b
>=
0xE0
&&
b
<=
0xEF
)
need
=
2
;
// 3 字节
else
if
(
b
>=
0xF0
&&
b
<=
0xF4
)
need
=
3
;
// 4 字节(0xF5+ 超 U+10FFFF,排除)
else
return
false
;
// 0x80-0xC1 或 0xF5-0xFF
if
(
i
+
need
>=
n
)
return
false
;
// 序列截断
for
(
int
k
=
1
;
k
<=
need
;
k
++)
if
((
bytes
[
i
+
k
]
&
0xC0
)
!=
0x80
)
return
false
;
// 续字节必须 10xxxxxx
// overlong / 代理区 / 超范围(need == 1 时 b ∈ 0xC2-0xDF,overlong 0xC0/0xC1 已排除)
if
(
need
==
2
&&
b
==
0xE0
&&
bytes
[
i
+
1
]
<
0xA0
)
return
false
;
// E0 80-9F = overlong 3 字节
if
(
need
==
2
&&
b
==
0xED
&&
bytes
[
i
+
1
]
>=
0xA0
)
return
false
;
// ED A0-BF = UTF-16 代理区
if
(
need
==
3
&&
b
==
0xF0
&&
bytes
[
i
+
1
]
<
0x90
)
return
false
;
// F0 80-8F = overlong 4 字节
if
(
need
==
3
&&
b
==
0xF4
&&
bytes
[
i
+
1
]
>=
0x90
)
return
false
;
// F4 90+ = 超 U+10FFFF
i
+=
need
+
1
;
}
return
true
;
}
/// <summary>
/// 无 BOM UTF-16 启发式:采样前 4KB,统计偶数/奇数下标 NUL 占比。
/// <para>UTF-16 LE 中 ASCII 字符高字节(奇数下标)为 0x00;UTF-16 BE 中高字节(偶数下标)为 0x00。
/// 一侧占比 ≥ 30% 且另一侧 ≤ 1% 才判定(保守阈值:纯 ASCII/GBK 中文两侧均近 0,不触发)。</para>
/// </summary>
private
static
Encoding
DetectUtf16ByNulDistribution
(
byte
[]
bytes
)
{
int
n
=
Math
.
Min
(
bytes
.
Length
,
Utf16ProbeBytes
)
&
~
1
;
// 偶数对齐
if
(
n
<
4
)
return
null
;
int
evenNul
=
0
,
oddNul
=
0
,
total
=
n
/
2
;
for
(
int
i
=
0
;
i
<
n
;
i
+=
2
)
{
if
(
bytes
[
i
]
==
0
)
evenNul
++;
if
(
bytes
[
i
+
1
]
==
0
)
oddNul
++;
}
double
evenRatio
=
(
double
)
evenNul
/
total
;
double
oddRatio
=
(
double
)
oddNul
/
total
;
if
(
oddRatio
>=
NulRatioHigh
&&
evenRatio
<=
NulRatioLow
)
return
Encoding
.
Unicode
;
// LE:NUL 集中在奇数下标(高字节)
if
(
evenRatio
>=
NulRatioHigh
&&
oddRatio
<=
NulRatioLow
)
return
Encoding
.
BigEndianUnicode
;
// BE:NUL 集中在偶数下标(高字节)
return
null
;
}
/// <summary>
/// GB18030 合法性采样(前 4KB):单字节 0x00-0x7F;双字节 0x81-0xFE + 0x40-0xFE(≠0x7F);
/// 四字节 0x81-0xFE + 0x30-0x39 + 0x81-0xFE + 0x30-0x39(生僻字扩展区)。
/// <para>采样边界处不完整序列视为合法(避免误伤);0x80/0xFF 等非法字节判定为二进制。</para>
/// </summary>
private
static
bool
IsValidGb18030Sample
(
byte
[]
bytes
)
{
int
n
=
Math
.
Min
(
bytes
.
Length
,
Gb18030ProbeBytes
);
int
i
=
0
;
while
(
i
<
n
)
{
byte
b
=
bytes
[
i
];
if
(
b
<
0x80
)
{
i
++;
continue
;
}
// ASCII 单字节
if
(
b
<
0x81
||
b
>
0xFE
)
return
false
;
// 0x80 / 0xFF 非法
if
(
i
+
1
>=
n
)
return
true
;
// 采样边界截断:视为合法
byte
b2
=
bytes
[
i
+
1
];
if
(
b2
>=
0x40
&&
b2
<=
0xFE
&&
b2
!=
0x7F
)
{
i
+=
2
;
continue
;
}
// 双字节
if
(
b2
>=
0x30
&&
b2
<=
0x39
)
{
if
(
i
+
3
>=
n
)
return
true
;
// 采样边界截断
byte
b3
=
bytes
[
i
+
2
],
b4
=
bytes
[
i
+
3
];
if
(
b3
>=
0x81
&&
b3
<=
0xFE
&&
b4
>=
0x30
&&
b4
<=
0x39
)
{
i
+=
4
;
continue
;
}
// 四字节
}
return
false
;
// 非法序列
}
return
true
;
}
}
}
Tools/Tools.csproj
View file @
1ac5825c
...
@@ -173,6 +173,8 @@
...
@@ -173,6 +173,8 @@
<Compile
Include=
"MyAgent\Agent\Tools\ITool.cs"
/>
<Compile
Include=
"MyAgent\Agent\Tools\ITool.cs"
/>
<Compile
Include=
"MyAgent\Agent\Tools\KbTools.cs"
/>
<Compile
Include=
"MyAgent\Agent\Tools\KbTools.cs"
/>
<Compile
Include=
"MyAgent\Agent\Tools\DbQueryTool.cs"
/>
<Compile
Include=
"MyAgent\Agent\Tools\DbQueryTool.cs"
/>
<Compile
Include=
"MyAgent\Agent\Tools\DbSchemaTool.cs"
/>
<Compile
Include=
"MyAgent\Agent\Tools\DialectTools.cs"
/>
<Compile
Include=
"MyAgent\Agent\Tools\DocExportTool.cs"
/>
<Compile
Include=
"MyAgent\Agent\Tools\DocExportTool.cs"
/>
<Compile
Include=
"MyAgent\Agent\Tools\ImageSearchTool.cs"
/>
<Compile
Include=
"MyAgent\Agent\Tools\ImageSearchTool.cs"
/>
<Compile
Include=
"MyAgent\Config\AgentConfig.cs"
/>
<Compile
Include=
"MyAgent\Config\AgentConfig.cs"
/>
...
@@ -194,6 +196,7 @@
...
@@ -194,6 +196,7 @@
<SubType>
Form
</SubType>
<SubType>
Form
</SubType>
</Compile>
</Compile>
<Compile
Include=
"MyAgent\Kb\KbConfig.cs"
/>
<Compile
Include=
"MyAgent\Kb\KbConfig.cs"
/>
<Compile
Include=
"MyAgent\Kb\DialectService.cs"
/>
<Compile
Include=
"MyAgent\Kb\KbService.cs"
/>
<Compile
Include=
"MyAgent\Kb\KbService.cs"
/>
<Compile
Include=
"MyAgent\Kb\KbIndex.cs"
/>
<Compile
Include=
"MyAgent\Kb\KbIndex.cs"
/>
<Compile
Include=
"MyAgent\Kb\SynonymIndex.cs"
/>
<Compile
Include=
"MyAgent\Kb\SynonymIndex.cs"
/>
...
@@ -272,6 +275,7 @@
...
@@ -272,6 +275,7 @@
<Compile
Include=
"MyUtils\SegPicture.cs"
/>
<Compile
Include=
"MyUtils\SegPicture.cs"
/>
<Compile
Include=
"MyUtils\TextBoxPlaceholder.cs"
/>
<Compile
Include=
"MyUtils\TextBoxPlaceholder.cs"
/>
<Compile
Include=
"MyUtils\TimeWatch.cs"
/>
<Compile
Include=
"MyUtils\TimeWatch.cs"
/>
<Compile
Include=
"MyUtils\TextEncoding.cs"
/>
<Compile
Include=
"UI\Popups\wEditEvaluation.cs"
>
<Compile
Include=
"UI\Popups\wEditEvaluation.cs"
>
<SubType>
Form
</SubType>
<SubType>
Form
</SubType>
</Compile>
</Compile>
...
...
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