feat(acp): support expectedFileCountByExtension constraint and dynamic chat timeout

- Add ExpectedFileCounts field to openClawArtifactContract to support per-extension file count validation
- Add normalizeOpenClawArtifactExtCountMap and openClawPositiveInt helpers
- Propagate expectedFileCountByExtension from contract/metadata/xworkmateArtifactConstraints
- Replace hard-coded 2min chat timeout with openClawAgentWaitTimeout for dynamic timeouts
- Add test coverage for normalize result and web contract
This commit is contained in:
Haitao Pan 2026-06-17 16:57:11 +08:00
parent e6ffdf3177
commit 861816738b
4 changed files with 90 additions and 5 deletions

View File

@ -342,12 +342,13 @@ func (o *SessionOrchestrator) startOpenClawGatewayTask(
return nil, rpcErr
}
applyOpenClawPreparedArtifactToChatParams(chatParams, preparedArtifact, sessionKey, turnID, artifactContract)
chatSendTimeout := openClawAgentWaitTimeout(params, chatParams)
sendStarted := time.Now()
sendResult := o.openClawGatewayRequestWithRetry(
gatewayProvider,
"chat.send",
chatParams,
2*time.Minute,
chatSendTimeout,
notifyWithCollection,
)
logOpenClawGatewayTiming(
@ -784,6 +785,7 @@ type openClawArtifactContract struct {
RequiresArtifactExport bool
ExpectedArtifactDirs []string
RequiredArtifactExts []string
ExpectedFileCounts map[string]int
SourceMessage string
}
@ -806,16 +808,66 @@ func openClawArtifactContractForParams(params map[string]any, chatParams map[str
if len(requiredExts) == 0 {
requiredExts = inferOpenClawRequiredArtifactExts(lowerMessage)
}
expectedFileCounts := normalizeOpenClawArtifactExtCountMap(shared.AsMap(contract["expectedFileCountByExtension"]))
if len(expectedFileCounts) == 0 {
expectedFileCounts = normalizeOpenClawArtifactExtCountMap(shared.AsMap(metadata["expectedFileCountByExtension"]))
}
if len(expectedFileCounts) == 0 {
expectedFileCounts = normalizeOpenClawArtifactExtCountMap(shared.AsMap(shared.AsMap(metadata["xworkmateArtifactConstraints"])["expectedFileCountByExtension"]))
}
return openClawArtifactContract{
TaskLoadClass: taskLoadClass,
ComplexLongChain: complex,
RequiresArtifactExport: requiresExport,
ExpectedArtifactDirs: expectedDirs,
RequiredArtifactExts: requiredExts,
ExpectedFileCounts: expectedFileCounts,
SourceMessage: message,
}
}
func normalizeOpenClawArtifactExtCountMap(values map[string]any) map[string]int {
if len(values) == 0 {
return nil
}
result := map[string]int{}
for key, raw := range values {
ext := strings.ToLower(strings.TrimSpace(key))
ext = strings.TrimPrefix(ext, ".")
if ext == "" || strings.Contains(ext, "/") || strings.Contains(ext, "\\") {
continue
}
count := openClawPositiveInt(raw)
if count <= 0 {
continue
}
result[ext] = count
}
if len(result) == 0 {
return nil
}
return result
}
func openClawPositiveInt(value any) int {
switch v := value.(type) {
case int:
return v
case int64:
return int(v)
case float64:
return int(v)
case float32:
return int(v)
case string:
var parsed int
if _, err := fmt.Sscanf(strings.TrimSpace(v), "%d", &parsed); err == nil {
return parsed
}
}
return 0
}
func normalizeOpenClawDirList(values []any) []string {
if len(values) == 0 {
return nil
@ -1429,6 +1481,13 @@ func (o *SessionOrchestrator) openClawArtifactExport(
if len(artifactContract.RequiredArtifactExts) > 0 {
exportParams["requiredArtifactExtensions"] = append([]string(nil), artifactContract.RequiredArtifactExts...)
}
if len(artifactContract.ExpectedFileCounts) > 0 {
counts := map[string]int{}
for ext, count := range artifactContract.ExpectedFileCounts {
counts[ext] = count
}
exportParams["expectedFileCountByExtension"] = counts
}
payload := o.openClawArtifactExportRequest(gatewayProvider, exportParams, notify)
return payload
}
@ -1497,6 +1556,9 @@ func mergeOpenClawArtifactPayload(result map[string]any, source map[string]any)
if _, ok := source["missingRequiredExtensions"]; ok {
result["missingRequiredExtensions"] = appendStringList(result["missingRequiredExtensions"], source["missingRequiredExtensions"])
}
if value, ok := source["missingRequiredFileCounts"]; ok {
result["missingRequiredFileCounts"] = value
}
}
func appendStringList(existing any, incoming any) []any {

View File

@ -263,6 +263,9 @@ func TestTaskGetArtifactExportReceivesRequiredArtifactExtensions(t *testing.T) {
"gatewayProviderId": shared.StringArg(start, "resolvedGatewayProviderId", ""),
"requiresArtifactExport": true,
"requiredArtifactExtensions": []any{"pdf"},
"expectedFileCountByExtension": map[string]any{
"pdf": 1,
},
},
}, nil)
if rpcErr != nil {
@ -275,4 +278,7 @@ func TestTaskGetArtifactExportReceivesRequiredArtifactExtensions(t *testing.T) {
if got := shared.ListArg(exportParams, "requiredArtifactExtensions"); len(got) != 1 || got[0] != "pdf" {
t.Fatalf("expected requiredArtifactExtensions to reach export, got %#v", exportParams)
}
if got := shared.AsMap(exportParams["expectedFileCountByExtension"]); openClawPositiveInt(got["pdf"]) != 1 {
t.Fatalf("expected expectedFileCountByExtension to reach export, got %#v", exportParams)
}
}

View File

@ -245,6 +245,9 @@ func (s *Server) mergeOpenClawTaskGetArtifactExport(payload map[string]any, para
if requiredExts := openClawTaskGetRequiredArtifactExtensions(params, payload); len(requiredExts) > 0 {
exportParams["requiredArtifactExtensions"] = append([]string(nil), requiredExts...)
}
if expectedCounts := openClawTaskGetExpectedFileCounts(params, payload); len(expectedCounts) > 0 {
exportParams["expectedFileCountByExtension"] = expectedCounts
}
exportPayload := s.orchestrator.openClawArtifactExportRequest(gatewayProvider, exportParams, notify)
if openClawArtifactExportPayloadAuthoritative(exportPayload) {
replaceOpenClawArtifactPayload(payload, exportPayload)
@ -491,6 +494,20 @@ func openClawTaskGetRequiredArtifactExtensions(params map[string]any, payload ma
return normalizeOpenClawArtifactExtList(openClawTaskGetMergedList(params, payload, "requiredArtifactExtensions"))
}
func openClawTaskGetExpectedFileCounts(params map[string]any, payload map[string]any) map[string]int {
result := normalizeOpenClawArtifactExtCountMap(shared.AsMap(payload["expectedFileCountByExtension"]))
for ext, count := range normalizeOpenClawArtifactExtCountMap(shared.AsMap(params["expectedFileCountByExtension"])) {
if result == nil {
result = map[string]int{}
}
result[ext] = count
}
if len(result) == 0 {
return nil
}
return result
}
func openClawTaskGetMergedList(params map[string]any, payload map[string]any, key string) []any {
seen := map[string]bool{}
result := []any{}

View File

@ -394,11 +394,11 @@ func TestHTTPHandlerGatewayOpenClawHandlesFiveConcurrentE2ECases(t *testing.T) {
defer httpServer.Close()
prompts := []string{
"采集最新AI资讯保存在md文件",
"围绕\n从单机权限 → 网络边界 → Web安全 → 云身份 → Zero Trust → AI Agent 身份 → AI模型与知识保护 演进 右侧是当下 \n测试制作视频附件带有图片",
"从单机权限 → 网络边界 → Web安全 → 云身份 → Zero Trust → AI Agent 身份 → AI模型与知识保护 演进 \n制作 使用codex 制作连续制作 7张的一些列图片",
"参考附件模版制作 ,围绕\n从单机权限 → 网络边界 → Web安全 → 云身份 → Zero Trust → AI Agent 身份 → AI模型与知识保护 演进 \n连续制作 7张的一些列图片",
"拆章节 -> 每章调用 Codex -> 每章 GPT images2 生成图 -> 汇总排版 -> 输出 PDF\n\n右侧 artifact栏 显示的陈旧文件 make artifact",
"围绕\n从单机权限 → 网络边界 → Web安全 → 云身份 → Zero Trust → AI Agent 身份 → AI模型与知识保护 演进 右侧是当下 \n测试制作视频",
"围绕\n\n从单机权限 → 网络边界 → Web安全 → 云身份 → Zero Trust → AI Agent 身份 → AI模型与知识保护 演进 \n\n拆章节 -> 每章调用 Codex -> 每章 GPT images2 生成图 -> 汇总排版 -> 制作视频",
"围绕\n从单机权限 → 网络边界 → Web安全 → 云身份 → Zero Trust → AI Agent 身份 → AI模型与知识保护 演进\n输出Markdown格式文件 微信公众号短图文 400-600字 插入关键词的软文\n输出Markdown格式文件 小红书风格 600-800字 插入钩子话题的软文\n输出Markdown格式文件 X文案串 小于144字的英语 鲜明的观点\n输出Markdown格式文件 微信公众号文章 800-1200字左右\n输出Markdown格式文件 头条号长文 800-1200字左右",
"围绕\n\n从单机权限 → 网络边界 → Web安全 → 云身份 → Zero Trust → AI Agent 身份 → AI模型与知识保护 演进 \n\n拆章节 -> 每章调用 Codex -> 每章 GPT images2 生成图 -> 汇总排版 -> 输出 PDF",
}
type result struct {
body string