后端: 1. 工具执行结果协议升级为结构化 ToolExecutionResult——execute/tool_runtime、ToolRegistry、stream extra 与 timeline 持久化统一改为透传 observation_text / summary / argument_view / result_view,不再只回写纯文本结果;context_tools、upsert_task_class 与旧 schedule/web 工具通过兼容包装接入新协议 2. 日程写工具注册继续收口——place / move / swap / batch_move / unplace / queue_apply_head_move 从 registry 内联实现下沉为独立 handler,降低注册表内参数解析与业务逻辑混写 3. 工具结果展示基础能力补齐——新增 execution_result / schedule_operation_handlers 公共件,为日程操作结果、参数本地化展示、blocked/failed/done 状态统一建模 前端: 4. AssistantPanel 接入结构化工具卡片渲染——新增 ToolCardRenderer,tool_call / tool_result 支持 argument_view / result_view 展示;schedule_completed 恢复为时间线内的占位卡片块,避免排程卡片脱离原消息顺序 5. 时间线类型与渲染收敛——schedule_agent.ts 补齐 ToolView 协议,AssistantPanel 改为按块渲染 tool / schedule_card / business_card,并移除旧 demo/prototype 路由与页面,收束正式面板代码路径 仓库: 6. AGENTS.md 新增协作约束——禁止擅自回滚、覆盖或删除用户/其他代理产生的工作区改动
306 lines
8.9 KiB
Go
306 lines
8.9 KiB
Go
package newagenttools
|
||
|
||
import (
|
||
"encoding/json"
|
||
"strings"
|
||
|
||
"github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||
)
|
||
|
||
type contextToolsAddResult struct {
|
||
Tool string `json:"tool"`
|
||
Success bool `json:"success"`
|
||
Action string `json:"action"`
|
||
Domain string `json:"domain,omitempty"`
|
||
Packs []string `json:"packs,omitempty"`
|
||
Mode string `json:"mode,omitempty"`
|
||
Message string `json:"message,omitempty"`
|
||
Error string `json:"error,omitempty"`
|
||
ErrorCode string `json:"error_code,omitempty"`
|
||
}
|
||
|
||
type contextToolsRemoveResult struct {
|
||
Tool string `json:"tool"`
|
||
Success bool `json:"success"`
|
||
Action string `json:"action"`
|
||
Domain string `json:"domain,omitempty"`
|
||
Packs []string `json:"packs,omitempty"`
|
||
All bool `json:"all,omitempty"`
|
||
Message string `json:"message,omitempty"`
|
||
Error string `json:"error,omitempty"`
|
||
ErrorCode string `json:"error_code,omitempty"`
|
||
}
|
||
|
||
// NewContextToolsAddHandler 创建 context_tools_add 工具。
|
||
//
|
||
// 职责边界:
|
||
// 1. 仅负责校验 domain/mode/packs 并返回结构化结果,不直接修改流程状态;
|
||
// 2. 真正的“激活态写回”由 execute 节点根据工具结果回写 CommonState;
|
||
// 3. schedule 支持可选 packs,taskclass 当前不支持可选 packs。
|
||
func NewContextToolsAddHandler() ToolHandler {
|
||
return func(state *schedule.ScheduleState, args map[string]any) ToolExecutionResult {
|
||
_ = state
|
||
|
||
domain := NormalizeToolDomain(readContextToolString(args["domain"]))
|
||
if domain == "" {
|
||
return LegacyResult(ToolNameContextToolsAdd, args, marshalContextToolsAddResult(contextToolsAddResult{
|
||
Tool: ToolNameContextToolsAdd,
|
||
Success: false,
|
||
Action: "reject",
|
||
Error: "参数非法:domain 仅支持 schedule/taskclass",
|
||
ErrorCode: "invalid_domain",
|
||
}))
|
||
}
|
||
|
||
mode := strings.ToLower(strings.TrimSpace(readContextToolString(args["mode"])))
|
||
if mode == "" {
|
||
mode = "replace"
|
||
}
|
||
if mode != "replace" && mode != "merge" {
|
||
return LegacyResult(ToolNameContextToolsAdd, args, marshalContextToolsAddResult(contextToolsAddResult{
|
||
Tool: ToolNameContextToolsAdd,
|
||
Success: false,
|
||
Action: "reject",
|
||
Domain: domain,
|
||
Error: "参数非法:mode 仅支持 replace/merge",
|
||
ErrorCode: "invalid_mode",
|
||
}))
|
||
}
|
||
|
||
packsRaw := readContextToolStringSlice(args["packs"])
|
||
packs, errCode, errText := validateContextPacks(domain, packsRaw, false)
|
||
if errCode != "" {
|
||
return LegacyResult(ToolNameContextToolsAdd, args, marshalContextToolsAddResult(contextToolsAddResult{
|
||
Tool: ToolNameContextToolsAdd,
|
||
Success: false,
|
||
Action: "reject",
|
||
Domain: domain,
|
||
Error: errText,
|
||
ErrorCode: errCode,
|
||
}))
|
||
}
|
||
|
||
// schedule 未显式传 packs 时,默认启用最小可用包(mutation + analyze)。
|
||
if domain == ToolDomainSchedule && len(packsRaw) == 0 {
|
||
packs = ResolveEffectiveToolPacks(domain, nil)
|
||
}
|
||
|
||
return LegacyResult(ToolNameContextToolsAdd, args, marshalContextToolsAddResult(contextToolsAddResult{
|
||
Tool: ToolNameContextToolsAdd,
|
||
Success: true,
|
||
Action: "activate",
|
||
Domain: domain,
|
||
Packs: packs,
|
||
Mode: mode,
|
||
Message: "已激活工具域,可继续调用对应业务工具。",
|
||
}))
|
||
}
|
||
}
|
||
|
||
// NewContextToolsRemoveHandler 创建 context_tools_remove 工具。
|
||
//
|
||
// 职责边界:
|
||
// 1. 仅解析 domain/all/packs 语义并返回结构化结果,不直接触碰上下文存储;
|
||
// 2. all=true 表示清空动态区业务工具,domain+packs 表示移除该域下指定二级包;
|
||
// 3. 仅 schedule 支持按 packs 移除,且 core 不允许显式移除。
|
||
func NewContextToolsRemoveHandler() ToolHandler {
|
||
return func(state *schedule.ScheduleState, args map[string]any) ToolExecutionResult {
|
||
_ = state
|
||
|
||
all := readContextToolBool(args["all"])
|
||
domainRaw := strings.ToLower(strings.TrimSpace(readContextToolString(args["domain"])))
|
||
packsRaw := readContextToolStringSlice(args["packs"])
|
||
|
||
// 兼容写法:domain=all 视为清空全部。
|
||
if domainRaw == "all" {
|
||
all = true
|
||
}
|
||
if all {
|
||
return LegacyResult(ToolNameContextToolsRemove, args, marshalContextToolsRemoveResult(contextToolsRemoveResult{
|
||
Tool: ToolNameContextToolsRemove,
|
||
Success: true,
|
||
Action: "clear_all",
|
||
All: true,
|
||
Message: "已移除全部业务工具域,仅保留上下文管理工具。",
|
||
}))
|
||
}
|
||
|
||
domain := NormalizeToolDomain(domainRaw)
|
||
if domain == "" {
|
||
return LegacyResult(ToolNameContextToolsRemove, args, marshalContextToolsRemoveResult(contextToolsRemoveResult{
|
||
Tool: ToolNameContextToolsRemove,
|
||
Success: false,
|
||
Action: "reject",
|
||
Error: "参数非法:需提供 domain=schedule/taskclass 或 all=true",
|
||
ErrorCode: "invalid_domain",
|
||
}))
|
||
}
|
||
|
||
packs, errCode, errText := validateContextPacks(domain, packsRaw, true)
|
||
if errCode != "" {
|
||
return LegacyResult(ToolNameContextToolsRemove, args, marshalContextToolsRemoveResult(contextToolsRemoveResult{
|
||
Tool: ToolNameContextToolsRemove,
|
||
Success: false,
|
||
Action: "reject",
|
||
Domain: domain,
|
||
Error: errText,
|
||
ErrorCode: errCode,
|
||
}))
|
||
}
|
||
|
||
if len(packs) > 0 {
|
||
return LegacyResult(ToolNameContextToolsRemove, args, marshalContextToolsRemoveResult(contextToolsRemoveResult{
|
||
Tool: ToolNameContextToolsRemove,
|
||
Success: true,
|
||
Action: "deactivate_packs",
|
||
Domain: domain,
|
||
Packs: packs,
|
||
Message: "已移除指定工具包。",
|
||
}))
|
||
}
|
||
|
||
return LegacyResult(ToolNameContextToolsRemove, args, marshalContextToolsRemoveResult(contextToolsRemoveResult{
|
||
Tool: ToolNameContextToolsRemove,
|
||
Success: true,
|
||
Action: "deactivate",
|
||
Domain: domain,
|
||
Message: "已移除指定工具域。",
|
||
}))
|
||
}
|
||
}
|
||
|
||
func validateContextPacks(domain string, packs []string, forRemove bool) ([]string, string, string) {
|
||
normalizedDomain := NormalizeToolDomain(domain)
|
||
if normalizedDomain == "" {
|
||
return nil, "invalid_domain", "参数非法:domain 非法"
|
||
}
|
||
if len(packs) == 0 {
|
||
return nil, "", ""
|
||
}
|
||
|
||
if normalizedDomain == ToolDomainTaskClass {
|
||
return nil, "unsupported_packs_for_domain", "参数非法:taskclass 暂不支持 packs"
|
||
}
|
||
|
||
normalized := make([]string, 0, len(packs))
|
||
seen := make(map[string]struct{}, len(packs))
|
||
for _, raw := range packs {
|
||
trimmed := strings.TrimSpace(raw)
|
||
if trimmed == "" {
|
||
continue
|
||
}
|
||
pack := NormalizeToolPack(normalizedDomain, trimmed)
|
||
if pack == "" {
|
||
return nil, "invalid_pack", "参数非法:存在不支持的 pack"
|
||
}
|
||
if IsFixedToolPack(normalizedDomain, pack) {
|
||
if forRemove {
|
||
return nil, "fixed_pack_forbidden", "参数非法:core 为固定包,不允许 remove"
|
||
}
|
||
return nil, "fixed_pack_forbidden", "参数非法:core 为固定包,不允许 add"
|
||
}
|
||
if _, exists := seen[pack]; exists {
|
||
continue
|
||
}
|
||
seen[pack] = struct{}{}
|
||
normalized = append(normalized, pack)
|
||
}
|
||
if len(normalized) == 0 {
|
||
return nil, "invalid_pack", "参数非法:packs 为空或无效"
|
||
}
|
||
return normalized, "", ""
|
||
}
|
||
|
||
func readContextToolString(raw any) string {
|
||
text, _ := raw.(string)
|
||
return strings.TrimSpace(text)
|
||
}
|
||
|
||
func readContextToolStringSlice(raw any) []string {
|
||
switch typed := raw.(type) {
|
||
case []string:
|
||
out := make([]string, 0, len(typed))
|
||
for _, item := range typed {
|
||
text := strings.TrimSpace(item)
|
||
if text == "" {
|
||
continue
|
||
}
|
||
out = append(out, text)
|
||
}
|
||
return out
|
||
case []any:
|
||
out := make([]string, 0, len(typed))
|
||
for _, item := range typed {
|
||
text, ok := item.(string)
|
||
if !ok {
|
||
continue
|
||
}
|
||
text = strings.TrimSpace(text)
|
||
if text == "" {
|
||
continue
|
||
}
|
||
out = append(out, text)
|
||
}
|
||
return out
|
||
case string:
|
||
text := strings.TrimSpace(typed)
|
||
if text == "" {
|
||
return nil
|
||
}
|
||
parts := strings.Split(text, ",")
|
||
out := make([]string, 0, len(parts))
|
||
for _, part := range parts {
|
||
part = strings.TrimSpace(part)
|
||
if part == "" {
|
||
continue
|
||
}
|
||
out = append(out, part)
|
||
}
|
||
return out
|
||
default:
|
||
return nil
|
||
}
|
||
}
|
||
|
||
func readContextToolBool(raw any) bool {
|
||
switch v := raw.(type) {
|
||
case bool:
|
||
return v
|
||
case string:
|
||
value := strings.ToLower(strings.TrimSpace(v))
|
||
return value == "1" || value == "true" || value == "yes"
|
||
case float64:
|
||
return v != 0
|
||
case float32:
|
||
return v != 0
|
||
case int:
|
||
return v != 0
|
||
case int8:
|
||
return v != 0
|
||
case int16:
|
||
return v != 0
|
||
case int32:
|
||
return v != 0
|
||
case int64:
|
||
return v != 0
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
func marshalContextToolsAddResult(result contextToolsAddResult) string {
|
||
raw, err := json.Marshal(result)
|
||
if err != nil {
|
||
return `{"tool":"context_tools_add","success":false,"action":"reject","error":"result encode failed","error_code":"encode_failed"}`
|
||
}
|
||
return string(raw)
|
||
}
|
||
|
||
func marshalContextToolsRemoveResult(result contextToolsRemoveResult) string {
|
||
raw, err := json.Marshal(result)
|
||
if err != nil {
|
||
return `{"tool":"context_tools_remove","success":false,"action":"reject","error":"result encode failed","error_code":"encode_failed"}`
|
||
}
|
||
return string(raw)
|
||
}
|