mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-17 21:44:43 +02:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e5d52cdf85 | |||
| 65e48826ff | |||
| 0cff507272 | |||
| 30afd71c05 | |||
| d2b6a154de | |||
| 278d5aa25c |
+8
-1
@@ -10,7 +10,7 @@
|
|||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
# 前端显示的版本号(可选,不填则显示默认版本)
|
# 前端显示的版本号(可选,不填则显示默认版本)
|
||||||
version: "v1.6.7"
|
version: "v1.6.8"
|
||||||
# 服务器配置
|
# 服务器配置
|
||||||
server:
|
server:
|
||||||
host: 0.0.0.0 # 监听地址,0.0.0.0 表示监听所有网络接口
|
host: 0.0.0.0 # 监听地址,0.0.0.0 表示监听所有网络接口
|
||||||
@@ -41,6 +41,13 @@ openai:
|
|||||||
api_key: sk-xxxxxxx # API 密钥(必填)
|
api_key: sk-xxxxxxx # API 密钥(必填)
|
||||||
model: qwen3-max # 模型名称(必填)
|
model: qwen3-max # 模型名称(必填)
|
||||||
max_total_tokens: 120000 # LLM 相关上下文的最大 Token 数限制(内存压缩和攻击链构建会共用此配置)
|
max_total_tokens: 120000 # LLM 相关上下文的最大 Token 数限制(内存压缩和攻击链构建会共用此配置)
|
||||||
|
# Eino 路径模型推理:DeepSeek/OpenAI 为 thinking / reasoning_effort 等;provider 为 claude 时合并为 Anthropic 顶层 thinking(extended thinking),mode: off 关闭
|
||||||
|
reasoning:
|
||||||
|
mode: off # auto | on | off;off 时不附加任何推理扩展字段
|
||||||
|
effort: max # low | medium | high | max;空表示不指定(openai_compat 下 auto 且无强度时不发请求扩展)
|
||||||
|
allow_client_reasoning: true # false 时忽略对话请求体 reasoning,仅以下方为准
|
||||||
|
profile: openai_compat # auto | deepseek_compat | openai_compat | output_config_effort
|
||||||
|
# extra_request_fields: {} # 可选:管理员自定义根级 JSON 片段(高级)
|
||||||
# ============================================
|
# ============================================
|
||||||
# 信息收集(FOFA)配置(可选)
|
# 信息收集(FOFA)配置(可选)
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|||||||
@@ -195,6 +195,8 @@ type ChatMessage struct {
|
|||||||
ToolCallID string `json:"tool_call_id,omitempty"`
|
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||||
// ToolName 仅 tool 角色:从 Eino/轨迹 JSON 的 name 或 tool_name 恢复,供续跑构造 ToolMessage。
|
// ToolName 仅 tool 角色:从 Eino/轨迹 JSON 的 name 或 tool_name 恢复,供续跑构造 ToolMessage。
|
||||||
ToolName string `json:"tool_name,omitempty"`
|
ToolName string `json:"tool_name,omitempty"`
|
||||||
|
// ReasoningContent 对应 OpenAI/DeepSeek 的 reasoning_content;思考模式 + 工具调用后续跑须回传(见 DeepSeek 文档)。
|
||||||
|
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalJSON 自定义JSON序列化,将tool_calls中的arguments转换为JSON字符串
|
// MarshalJSON 自定义JSON序列化,将tool_calls中的arguments转换为JSON字符串
|
||||||
@@ -208,6 +210,9 @@ func (cm ChatMessage) MarshalJSON() ([]byte, error) {
|
|||||||
if cm.Content != "" {
|
if cm.Content != "" {
|
||||||
aux["content"] = cm.Content
|
aux["content"] = cm.Content
|
||||||
}
|
}
|
||||||
|
if cm.ReasoningContent != "" {
|
||||||
|
aux["reasoning_content"] = cm.ReasoningContent
|
||||||
|
}
|
||||||
|
|
||||||
// 添加tool_call_id(如果存在)
|
// 添加tool_call_id(如果存在)
|
||||||
if cm.ToolCallID != "" {
|
if cm.ToolCallID != "" {
|
||||||
@@ -663,8 +668,8 @@ func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, his
|
|||||||
|
|
||||||
// 检查是否有工具调用
|
// 检查是否有工具调用
|
||||||
if len(choice.Message.ToolCalls) > 0 {
|
if len(choice.Message.ToolCalls) > 0 {
|
||||||
// 思考内容:如果本轮启用了思考流式增量(thinking_stream_*),前端会去重;
|
// ReAct 助手正文流式增量(thinking_stream_*)在 UI 上归为「思考」;若与 streamId 重复则前端会去重。
|
||||||
// 同时也需要在该“思考阶段结束”时补一条可落库的 thinking(用于刷新后持久化展示)。
|
// 该条 thinking 用于刷新后持久化展示(与流式聚合一致)。
|
||||||
if choice.Message.Content != "" {
|
if choice.Message.Content != "" {
|
||||||
sendProgress("thinking", choice.Message.Content, map[string]interface{}{
|
sendProgress("thinking", choice.Message.Content, map[string]interface{}{
|
||||||
"iteration": i + 1,
|
"iteration": i + 1,
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ func (b *Builder) formatProcessDetailsForAttackChain(details []database.ProcessD
|
|||||||
// 目标:以主 agent(编排器)视角输出整轮迭代
|
// 目标:以主 agent(编排器)视角输出整轮迭代
|
||||||
// - 保留:编排器工具调用/结果、对子代理的 task 调度、子代理最终回复(不含推理)
|
// - 保留:编排器工具调用/结果、对子代理的 task 调度、子代理最终回复(不含推理)
|
||||||
// - 丢弃:thinking/planning/progress 等噪声、子代理的工具细节与推理过程
|
// - 丢弃:thinking/planning/progress 等噪声、子代理的工具细节与推理过程
|
||||||
if d.EventType == "progress" || d.EventType == "thinking" || d.EventType == "planning" {
|
if d.EventType == "progress" || d.EventType == "thinking" || d.EventType == "reasoning_chain" || d.EventType == "planning" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -346,6 +346,48 @@ type OpenAIConfig struct {
|
|||||||
BaseURL string `yaml:"base_url" json:"base_url"`
|
BaseURL string `yaml:"base_url" json:"base_url"`
|
||||||
Model string `yaml:"model" json:"model"`
|
Model string `yaml:"model" json:"model"`
|
||||||
MaxTotalTokens int `yaml:"max_total_tokens,omitempty" json:"max_total_tokens,omitempty"`
|
MaxTotalTokens int `yaml:"max_total_tokens,omitempty" json:"max_total_tokens,omitempty"`
|
||||||
|
// Reasoning 控制 Eino ChatModel 的 thinking / reasoning_effort / output_config 等(仅 Eino 路径生效;原生 ReAct 忽略)。
|
||||||
|
Reasoning OpenAIReasoningConfig `yaml:"reasoning,omitempty" json:"reasoning,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenAIReasoningConfig 全局默认与网关 profile(对话页可通过 ChatRequest.reasoning 覆盖,受 AllowClientReasoning 约束)。
|
||||||
|
type OpenAIReasoningConfig struct {
|
||||||
|
// Mode: auto(默认)| on | off | default(与 auto 相同)。off 时不向模型附加推理扩展字段。
|
||||||
|
Mode string `yaml:"mode,omitempty" json:"mode,omitempty"`
|
||||||
|
// Effort: low | medium | high | max;空表示不单独指定强度(各 profile 行为见 internal/reasoning)。
|
||||||
|
Effort string `yaml:"effort,omitempty" json:"effort,omitempty"`
|
||||||
|
// AllowClientReasoning 为 false 时忽略请求体 reasoning;nil 或未设置等同于 true。
|
||||||
|
AllowClientReasoning *bool `yaml:"allow_client_reasoning,omitempty" json:"allow_client_reasoning,omitempty"`
|
||||||
|
// Profile: auto | deepseek_compat | openai_compat | output_config_effort
|
||||||
|
Profile string `yaml:"profile,omitempty" json:"profile,omitempty"`
|
||||||
|
// ExtraRequestFields 合并进 Chat Completions 根 JSON(管理员用;与自动字段同名时后者覆盖)。
|
||||||
|
ExtraRequestFields map[string]interface{} `yaml:"extra_request_fields,omitempty" json:"extra_request_fields,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModeEffective returns auto when empty or default.
|
||||||
|
func (c OpenAIReasoningConfig) ModeEffective() string {
|
||||||
|
m := strings.ToLower(strings.TrimSpace(c.Mode))
|
||||||
|
if m == "" || m == "default" {
|
||||||
|
return "auto"
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProfileEffective returns auto when empty.
|
||||||
|
func (c OpenAIReasoningConfig) ProfileEffective() string {
|
||||||
|
p := strings.ToLower(strings.TrimSpace(c.Profile))
|
||||||
|
if p == "" {
|
||||||
|
return "auto"
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowClientReasoningEffective true when client may send ChatRequest.reasoning.
|
||||||
|
func (c OpenAIReasoningConfig) AllowClientReasoningEffective() bool {
|
||||||
|
if c.AllowClientReasoning == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return *c.AllowClientReasoning
|
||||||
}
|
}
|
||||||
|
|
||||||
type FofaConfig struct {
|
type FofaConfig struct {
|
||||||
|
|||||||
@@ -25,14 +25,15 @@ type Conversation struct {
|
|||||||
|
|
||||||
// Message 消息
|
// Message 消息
|
||||||
type Message struct {
|
type Message struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
ConversationID string `json:"conversationId"`
|
ConversationID string `json:"conversationId"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
MCPExecutionIDs []string `json:"mcpExecutionIds,omitempty"`
|
ReasoningContent string `json:"reasoningContent,omitempty"`
|
||||||
ProcessDetails []map[string]interface{} `json:"processDetails,omitempty"`
|
MCPExecutionIDs []string `json:"mcpExecutionIds,omitempty"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
ProcessDetails []map[string]interface{} `json:"processDetails,omitempty"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateConversation 创建新对话
|
// CreateConversation 创建新对话
|
||||||
@@ -498,8 +499,8 @@ func (db *DB) AddMessage(conversationID, role, content string, mcpExecutionIDs [
|
|||||||
}
|
}
|
||||||
|
|
||||||
_, err := db.Exec(
|
_, err := db.Exec(
|
||||||
"INSERT INTO messages (id, conversation_id, role, content, mcp_execution_ids, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
"INSERT INTO messages (id, conversation_id, role, content, reasoning_content, mcp_execution_ids, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
id, conversationID, role, content, mcpIDsJSON, now, now,
|
id, conversationID, role, content, "", mcpIDsJSON, now, now,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("添加消息失败: %w", err)
|
return nil, fmt.Errorf("添加消息失败: %w", err)
|
||||||
@@ -523,10 +524,30 @@ func (db *DB) AddMessage(conversationID, role, content string, mcpExecutionIDs [
|
|||||||
return message, nil
|
return message, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateAssistantMessageFinalize 更新助手消息终态(正文、MCP id、思考链聚合文本,供无轨迹回退时回放)。
|
||||||
|
func (db *DB) UpdateAssistantMessageFinalize(messageID, content string, mcpExecutionIDs []string, reasoningContent string) error {
|
||||||
|
var mcpIDsJSON string
|
||||||
|
if len(mcpExecutionIDs) > 0 {
|
||||||
|
jsonData, err := json.Marshal(mcpExecutionIDs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("序列化MCP执行ID失败: %w", err)
|
||||||
|
}
|
||||||
|
mcpIDsJSON = string(jsonData)
|
||||||
|
}
|
||||||
|
_, err := db.Exec(
|
||||||
|
"UPDATE messages SET content = ?, mcp_execution_ids = ?, reasoning_content = ?, updated_at = ? WHERE id = ?",
|
||||||
|
content, mcpIDsJSON, strings.TrimSpace(reasoningContent), time.Now(), messageID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("更新助手消息失败: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetMessages 获取对话的所有消息
|
// GetMessages 获取对话的所有消息
|
||||||
func (db *DB) GetMessages(conversationID string) ([]Message, error) {
|
func (db *DB) GetMessages(conversationID string) ([]Message, error) {
|
||||||
rows, err := db.Query(
|
rows, err := db.Query(
|
||||||
"SELECT id, conversation_id, role, content, mcp_execution_ids, created_at, updated_at FROM messages WHERE conversation_id = ? ORDER BY created_at ASC",
|
"SELECT id, conversation_id, role, content, reasoning_content, mcp_execution_ids, created_at, updated_at FROM messages WHERE conversation_id = ? ORDER BY created_at ASC",
|
||||||
conversationID,
|
conversationID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -537,13 +558,17 @@ func (db *DB) GetMessages(conversationID string) ([]Message, error) {
|
|||||||
var messages []Message
|
var messages []Message
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var msg Message
|
var msg Message
|
||||||
|
var reasoning sql.NullString
|
||||||
var mcpIDsJSON sql.NullString
|
var mcpIDsJSON sql.NullString
|
||||||
var createdAt string
|
var createdAt string
|
||||||
var updatedAt sql.NullString
|
var updatedAt sql.NullString
|
||||||
|
|
||||||
if err := rows.Scan(&msg.ID, &msg.ConversationID, &msg.Role, &msg.Content, &mcpIDsJSON, &createdAt, &updatedAt); err != nil {
|
if err := rows.Scan(&msg.ID, &msg.ConversationID, &msg.Role, &msg.Content, &reasoning, &mcpIDsJSON, &createdAt, &updatedAt); err != nil {
|
||||||
return nil, fmt.Errorf("扫描消息失败: %w", err)
|
return nil, fmt.Errorf("扫描消息失败: %w", err)
|
||||||
}
|
}
|
||||||
|
if reasoning.Valid {
|
||||||
|
msg.ReasoningContent = reasoning.String
|
||||||
|
}
|
||||||
|
|
||||||
// 尝试多种时间格式解析
|
// 尝试多种时间格式解析
|
||||||
var err error
|
var err error
|
||||||
@@ -683,7 +708,7 @@ type ProcessDetail struct {
|
|||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
MessageID string `json:"messageId"`
|
MessageID string `json:"messageId"`
|
||||||
ConversationID string `json:"conversationId"`
|
ConversationID string `json:"conversationId"`
|
||||||
EventType string `json:"eventType"` // iteration, thinking, tool_calls_detected, tool_call, tool_result, progress, error
|
EventType string `json:"eventType"` // iteration, thinking, reasoning_chain, tool_calls_detected, tool_call, tool_result, progress, error
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Data string `json:"data"` // JSON格式的数据
|
Data string `json:"data"` // JSON格式的数据
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
|||||||
@@ -594,6 +594,25 @@ func (db *DB) migrateMessagesTable() error {
|
|||||||
|
|
||||||
// 回填已有数据:让 updated_at 至少等于 created_at,避免前端出现空/当前时间回退。
|
// 回填已有数据:让 updated_at 至少等于 created_at,避免前端出现空/当前时间回退。
|
||||||
_, _ = db.Exec("UPDATE messages SET updated_at = created_at WHERE updated_at IS NULL OR updated_at = ''")
|
_, _ = db.Exec("UPDATE messages SET updated_at = created_at WHERE updated_at IS NULL OR updated_at = ''")
|
||||||
|
|
||||||
|
// reasoning_content:DeepSeek 思考模式 + 工具调用续跑;与 last_react_input 互补,供消息表回退路径回放
|
||||||
|
var rcColCount int
|
||||||
|
errRC := db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('messages') WHERE name='reasoning_content'").Scan(&rcColCount)
|
||||||
|
if errRC != nil {
|
||||||
|
if _, addErr := db.Exec("ALTER TABLE messages ADD COLUMN reasoning_content TEXT"); addErr != nil {
|
||||||
|
errMsg := strings.ToLower(addErr.Error())
|
||||||
|
if !strings.Contains(errMsg, "duplicate column") && !strings.Contains(errMsg, "already exists") {
|
||||||
|
return fmt.Errorf("添加 messages.reasoning_content 字段失败: %w", addErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if rcColCount == 0 {
|
||||||
|
if _, err := db.Exec("ALTER TABLE messages ADD COLUMN reasoning_content TEXT"); err != nil {
|
||||||
|
errMsg := strings.ToLower(err.Error())
|
||||||
|
if !strings.Contains(errMsg, "duplicate column") && !strings.Contains(errMsg, "already exists") {
|
||||||
|
return fmt.Errorf("添加 messages.reasoning_content 字段失败: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+81
-81
@@ -19,6 +19,7 @@ import (
|
|||||||
"cyberstrike-ai/internal/agent"
|
"cyberstrike-ai/internal/agent"
|
||||||
"cyberstrike-ai/internal/config"
|
"cyberstrike-ai/internal/config"
|
||||||
"cyberstrike-ai/internal/database"
|
"cyberstrike-ai/internal/database"
|
||||||
|
"cyberstrike-ai/internal/reasoning"
|
||||||
"cyberstrike-ai/internal/mcp"
|
"cyberstrike-ai/internal/mcp"
|
||||||
"cyberstrike-ai/internal/mcp/builtin"
|
"cyberstrike-ai/internal/mcp/builtin"
|
||||||
"cyberstrike-ai/internal/multiagent"
|
"cyberstrike-ai/internal/multiagent"
|
||||||
@@ -201,6 +202,14 @@ type ChatAttachment struct {
|
|||||||
ServerPath string `json:"serverPath,omitempty"` // 已保存在 chat_uploads 下的绝对路径(由 POST /api/chat-uploads 返回)
|
ServerPath string `json:"serverPath,omitempty"` // 已保存在 chat_uploads 下的绝对路径(由 POST /api/chat-uploads 返回)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ChatReasoningRequest 对话页「模型推理」意图(仅 Eino 路径消费;原生 agent-loop 忽略)。
|
||||||
|
type ChatReasoningRequest struct {
|
||||||
|
// Mode: default(跟随系统)| off | on | auto
|
||||||
|
Mode string `json:"mode,omitempty"`
|
||||||
|
// Effort: low | medium | high | max;空表示不指定(由系统默认与各 profile 决定)。
|
||||||
|
Effort string `json:"effort,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// ChatRequest 聊天请求
|
// ChatRequest 聊天请求
|
||||||
type ChatRequest struct {
|
type ChatRequest struct {
|
||||||
Message string `json:"message" binding:"required"`
|
Message string `json:"message" binding:"required"`
|
||||||
@@ -209,10 +218,18 @@ type ChatRequest struct {
|
|||||||
Attachments []ChatAttachment `json:"attachments,omitempty"`
|
Attachments []ChatAttachment `json:"attachments,omitempty"`
|
||||||
WebShellConnectionID string `json:"webshellConnectionId,omitempty"` // WebShell 管理 - AI 助手:当前选中的连接 ID,仅使用 webshell_* 工具
|
WebShellConnectionID string `json:"webshellConnectionId,omitempty"` // WebShell 管理 - AI 助手:当前选中的连接 ID,仅使用 webshell_* 工具
|
||||||
Hitl *HITLRequest `json:"hitl,omitempty"`
|
Hitl *HITLRequest `json:"hitl,omitempty"`
|
||||||
|
Reasoning *ChatReasoningRequest `json:"reasoning,omitempty"`
|
||||||
// Orchestration 仅对 /api/multi-agent、/api/multi-agent/stream:deep | plan_execute | supervisor;空则等同 deep。机器人/批量等无请求体时由服务端默认 deep。/api/eino-agent* 不使用此字段。
|
// Orchestration 仅对 /api/multi-agent、/api/multi-agent/stream:deep | plan_execute | supervisor;空则等同 deep。机器人/批量等无请求体时由服务端默认 deep。/api/eino-agent* 不使用此字段。
|
||||||
Orchestration string `json:"orchestration,omitempty"`
|
Orchestration string `json:"orchestration,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func chatReasoningToClientIntent(r *ChatReasoningRequest) *reasoning.ClientIntent {
|
||||||
|
if r == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &reasoning.ClientIntent{Mode: r.Mode, Effort: r.Effort}
|
||||||
|
}
|
||||||
|
|
||||||
type HITLRequest struct {
|
type HITLRequest struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
Mode string `json:"mode,omitempty"`
|
Mode string `json:"mode,omitempty"`
|
||||||
@@ -567,14 +584,7 @@ func (h *AgentHandler) AgentLoop(c *gin.Context) {
|
|||||||
h.logger.Warn("获取历史消息失败", zap.Error(err))
|
h.logger.Warn("获取历史消息失败", zap.Error(err))
|
||||||
agentHistoryMessages = []agent.ChatMessage{}
|
agentHistoryMessages = []agent.ChatMessage{}
|
||||||
} else {
|
} else {
|
||||||
// 将数据库消息转换为Agent消息格式
|
agentHistoryMessages = dbMessagesToAgentChatMessages(historyMessages)
|
||||||
agentHistoryMessages = make([]agent.ChatMessage, 0, len(historyMessages))
|
|
||||||
for _, msg := range historyMessages {
|
|
||||||
agentHistoryMessages = append(agentHistoryMessages, agent.ChatMessage{
|
|
||||||
Role: msg.Role,
|
|
||||||
Content: msg.Content,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
h.logger.Info("从消息表加载历史消息", zap.Int("count", len(agentHistoryMessages)))
|
h.logger.Info("从消息表加载历史消息", zap.Int("count", len(agentHistoryMessages)))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -775,6 +785,7 @@ func (h *AgentHandler) ProcessMessageForRobot(ctx context.Context, conversationI
|
|||||||
progressCallback,
|
progressCallback,
|
||||||
h.agentsMarkdownDir,
|
h.agentsMarkdownDir,
|
||||||
"deep",
|
"deep",
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
if errMA != nil {
|
if errMA != nil {
|
||||||
if shouldPersistEinoAgentTraceAfterRunError(ctx) {
|
if shouldPersistEinoAgentTraceAfterRunError(ctx) {
|
||||||
@@ -788,17 +799,8 @@ func (h *AgentHandler) ProcessMessageForRobot(ctx context.Context, conversationI
|
|||||||
return "", conversationID, errMA
|
return "", conversationID, errMA
|
||||||
}
|
}
|
||||||
if assistantMessageID != "" {
|
if assistantMessageID != "" {
|
||||||
mcpIDsJSON := ""
|
if errU := h.db.UpdateAssistantMessageFinalize(assistantMessageID, resultMA.Response, resultMA.MCPExecutionIDs, multiagent.AggregatedReasoningFromTraceJSON(resultMA.LastAgentTraceInput)); errU != nil {
|
||||||
if len(resultMA.MCPExecutionIDs) > 0 {
|
h.logger.Warn("机器人:更新助手消息失败", zap.Error(errU))
|
||||||
jsonData, _ := json.Marshal(resultMA.MCPExecutionIDs)
|
|
||||||
mcpIDsJSON = string(jsonData)
|
|
||||||
}
|
|
||||||
_, err = h.db.Exec(
|
|
||||||
"UPDATE messages SET content = ?, mcp_execution_ids = ?, updated_at = ? WHERE id = ?",
|
|
||||||
resultMA.Response, mcpIDsJSON, time.Now(), assistantMessageID,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
h.logger.Warn("机器人:更新助手消息失败", zap.Error(err))
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if _, err = h.db.AddMessage(conversationID, "assistant", resultMA.Response, resultMA.MCPExecutionIDs); err != nil {
|
if _, err = h.db.AddMessage(conversationID, "assistant", resultMA.Response, resultMA.MCPExecutionIDs); err != nil {
|
||||||
@@ -823,17 +825,8 @@ func (h *AgentHandler) ProcessMessageForRobot(ctx context.Context, conversationI
|
|||||||
|
|
||||||
// 更新助手消息内容与 MCP 执行 ID(与 stream 一致)
|
// 更新助手消息内容与 MCP 执行 ID(与 stream 一致)
|
||||||
if assistantMessageID != "" {
|
if assistantMessageID != "" {
|
||||||
mcpIDsJSON := ""
|
if errU := h.db.UpdateAssistantMessageFinalize(assistantMessageID, result.Response, result.MCPExecutionIDs, multiagent.AggregatedReasoningFromTraceJSON(result.LastAgentTraceInput)); errU != nil {
|
||||||
if len(result.MCPExecutionIDs) > 0 {
|
h.logger.Warn("机器人:更新助手消息失败", zap.Error(errU))
|
||||||
jsonData, _ := json.Marshal(result.MCPExecutionIDs)
|
|
||||||
mcpIDsJSON = string(jsonData)
|
|
||||||
}
|
|
||||||
_, err = h.db.Exec(
|
|
||||||
"UPDATE messages SET content = ?, mcp_execution_ids = ?, updated_at = ? WHERE id = ?",
|
|
||||||
result.Response, mcpIDsJSON, time.Now(), assistantMessageID,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
h.logger.Warn("机器人:更新助手消息失败", zap.Error(err))
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if _, err = h.db.AddMessage(conversationID, "assistant", result.Response, result.MCPExecutionIDs); err != nil {
|
if _, err = h.db.AddMessage(conversationID, "assistant", result.Response, result.MCPExecutionIDs); err != nil {
|
||||||
@@ -891,10 +884,12 @@ func (h *AgentHandler) createProgressCallback(runCtx context.Context, cancelRun
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// thinking_stream_*:不逐条落库,按 streamId 聚合,在后续关键事件前补一条可持久化的 thinking
|
// thinking_stream_*(ReAct 等助手正文流)与 reasoning_chain_stream_*(Eino ReasoningContent):
|
||||||
|
// 不逐条落库,按 streamId 聚合,flush 时分别落 thinking / reasoning_chain。
|
||||||
type thinkingBuf struct {
|
type thinkingBuf struct {
|
||||||
b strings.Builder
|
b strings.Builder
|
||||||
meta map[string]interface{}
|
meta map[string]interface{}
|
||||||
|
persistAs string // "thinking" | "reasoning_chain"
|
||||||
}
|
}
|
||||||
thinkingStreams := make(map[string]*thinkingBuf) // streamId -> buf
|
thinkingStreams := make(map[string]*thinkingBuf) // streamId -> buf
|
||||||
flushedThinking := make(map[string]bool) // streamId -> flushed
|
flushedThinking := make(map[string]bool) // streamId -> flushed
|
||||||
@@ -948,8 +943,12 @@ func (h *AgentHandler) createProgressCallback(runCtx context.Context, cancelRun
|
|||||||
}
|
}
|
||||||
data[k] = v
|
data[k] = v
|
||||||
}
|
}
|
||||||
if err := h.db.AddProcessDetail(assistantMessageID, conversationID, "thinking", content, data); err != nil {
|
persist := tb.persistAs
|
||||||
h.logger.Warn("保存过程详情失败", zap.Error(err), zap.String("eventType", "thinking"))
|
if persist != "reasoning_chain" {
|
||||||
|
persist = "thinking"
|
||||||
|
}
|
||||||
|
if err := h.db.AddProcessDetail(assistantMessageID, conversationID, persist, content, data); err != nil {
|
||||||
|
h.logger.Warn("保存过程详情失败", zap.Error(err), zap.String("eventType", persist))
|
||||||
}
|
}
|
||||||
flushedThinking[sid] = true
|
flushedThinking[sid] = true
|
||||||
}
|
}
|
||||||
@@ -1177,14 +1176,20 @@ func (h *AgentHandler) createProgressCallback(runCtx context.Context, cancelRun
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 聚合 thinking_stream_*(ReasoningContent),不逐条落库
|
// 聚合 thinking_stream_* / reasoning_chain_stream_*,不逐条落库
|
||||||
if eventType == "thinking_stream_start" {
|
if eventType == "thinking_stream_start" || eventType == "reasoning_chain_stream_start" {
|
||||||
|
persistAs := "thinking"
|
||||||
|
if eventType == "reasoning_chain_stream_start" {
|
||||||
|
persistAs = "reasoning_chain"
|
||||||
|
}
|
||||||
if dataMap, ok := data.(map[string]interface{}); ok {
|
if dataMap, ok := data.(map[string]interface{}); ok {
|
||||||
if sid, ok2 := dataMap["streamId"].(string); ok2 && sid != "" {
|
if sid, ok2 := dataMap["streamId"].(string); ok2 && sid != "" {
|
||||||
tb := thinkingStreams[sid]
|
tb := thinkingStreams[sid]
|
||||||
if tb == nil {
|
if tb == nil {
|
||||||
tb = &thinkingBuf{meta: map[string]interface{}{}}
|
tb = &thinkingBuf{meta: map[string]interface{}{}, persistAs: persistAs}
|
||||||
thinkingStreams[sid] = tb
|
thinkingStreams[sid] = tb
|
||||||
|
} else {
|
||||||
|
tb.persistAs = persistAs
|
||||||
}
|
}
|
||||||
// 记录元信息(source/einoAgent/einoRole/iteration 等)
|
// 记录元信息(source/einoAgent/einoRole/iteration 等)
|
||||||
for k, v := range dataMap {
|
for k, v := range dataMap {
|
||||||
@@ -1194,15 +1199,21 @@ func (h *AgentHandler) createProgressCallback(runCtx context.Context, cancelRun
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if eventType == "thinking_stream_delta" {
|
if eventType == "thinking_stream_delta" || eventType == "reasoning_chain_stream_delta" {
|
||||||
|
persistAs := "thinking"
|
||||||
|
if eventType == "reasoning_chain_stream_delta" {
|
||||||
|
persistAs = "reasoning_chain"
|
||||||
|
}
|
||||||
if dataMap, ok := data.(map[string]interface{}); ok {
|
if dataMap, ok := data.(map[string]interface{}); ok {
|
||||||
if sid, ok2 := dataMap["streamId"].(string); ok2 && sid != "" {
|
if sid, ok2 := dataMap["streamId"].(string); ok2 && sid != "" {
|
||||||
tb := thinkingStreams[sid]
|
tb := thinkingStreams[sid]
|
||||||
if tb == nil {
|
if tb == nil {
|
||||||
tb = &thinkingBuf{meta: map[string]interface{}{}}
|
tb = &thinkingBuf{meta: map[string]interface{}{}, persistAs: persistAs}
|
||||||
thinkingStreams[sid] = tb
|
thinkingStreams[sid] = tb
|
||||||
|
} else if tb.persistAs == "" {
|
||||||
|
tb.persistAs = persistAs
|
||||||
}
|
}
|
||||||
// delta 片段直接拼接;message 本身就是 reasoning content
|
// delta 片段直接拼接
|
||||||
tb.b.WriteString(message)
|
tb.b.WriteString(message)
|
||||||
// 有时 delta 先到 start 未到,补充元信息
|
// 有时 delta 先到 start 未到,补充元信息
|
||||||
for k, v := range dataMap {
|
for k, v := range dataMap {
|
||||||
@@ -1213,10 +1224,9 @@ func (h *AgentHandler) createProgressCallback(runCtx context.Context, cancelRun
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 当 Agent 同时发送 thinking_stream_* 和 thinking(带同一 streamId)时,
|
// 当 Agent 同时发送 *_stream_* 与同名 streamId 的 thinking/reasoning_chain 时,
|
||||||
// thinking_stream_* 已经会在 flushThinkingStreams() 聚合落库;
|
// 流式聚合已会在 flushThinkingStreams() 落库;此处跳过逐条重复。
|
||||||
// 这里跳过同 streamId 的 thinking,避免 processDetails 双份展示。
|
if eventType == "thinking" || eventType == "reasoning_chain" {
|
||||||
if eventType == "thinking" {
|
|
||||||
if dataMap, ok := data.(map[string]interface{}); ok {
|
if dataMap, ok := data.(map[string]interface{}); ok {
|
||||||
if sid, ok2 := dataMap["streamId"].(string); ok2 && sid != "" {
|
if sid, ok2 := dataMap["streamId"].(string); ok2 && sid != "" {
|
||||||
if tb, exists := thinkingStreams[sid]; exists && tb != nil {
|
if tb, exists := thinkingStreams[sid]; exists && tb != nil {
|
||||||
@@ -1245,7 +1255,7 @@ func (h *AgentHandler) createProgressCallback(runCtx context.Context, cancelRun
|
|||||||
if eventType == "tool_result" {
|
if eventType == "tool_result" {
|
||||||
discardPlanningIfEchoesToolResult(&respPlan, data)
|
discardPlanningIfEchoesToolResult(&respPlan, data)
|
||||||
}
|
}
|
||||||
// 在关键过程事件落库前,先把「规划中」与 thinking_stream 落库
|
// 在关键过程事件落库前,先把「规划中」与聚合中的 thinking / reasoning_chain 流落库
|
||||||
flushResponsePlan()
|
flushResponsePlan()
|
||||||
flushThinkingStreams()
|
flushThinkingStreams()
|
||||||
if err := h.db.AddProcessDetail(assistantMessageID, conversationID, eventType, message, data); err != nil {
|
if err := h.db.AddProcessDetail(assistantMessageID, conversationID, eventType, message, data); err != nil {
|
||||||
@@ -1427,14 +1437,7 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
|
|||||||
h.logger.Warn("获取历史消息失败", zap.Error(err))
|
h.logger.Warn("获取历史消息失败", zap.Error(err))
|
||||||
agentHistoryMessages = []agent.ChatMessage{}
|
agentHistoryMessages = []agent.ChatMessage{}
|
||||||
} else {
|
} else {
|
||||||
// 将数据库消息转换为Agent消息格式
|
agentHistoryMessages = dbMessagesToAgentChatMessages(historyMessages)
|
||||||
agentHistoryMessages = make([]agent.ChatMessage, 0, len(historyMessages))
|
|
||||||
for _, msg := range historyMessages {
|
|
||||||
agentHistoryMessages = append(agentHistoryMessages, agent.ChatMessage{
|
|
||||||
Role: msg.Role,
|
|
||||||
Content: msg.Content,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
h.logger.Info("从消息表加载历史消息", zap.Int("count", len(agentHistoryMessages)))
|
h.logger.Info("从消息表加载历史消息", zap.Int("count", len(agentHistoryMessages)))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1727,20 +1730,8 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
|
|||||||
|
|
||||||
// 更新助手消息内容
|
// 更新助手消息内容
|
||||||
if assistantMsg != nil {
|
if assistantMsg != nil {
|
||||||
_, err = h.db.Exec(
|
if errU := h.db.UpdateAssistantMessageFinalize(assistantMessageID, result.Response, result.MCPExecutionIDs, multiagent.AggregatedReasoningFromTraceJSON(result.LastAgentTraceInput)); errU != nil {
|
||||||
"UPDATE messages SET content = ?, mcp_execution_ids = ?, updated_at = ? WHERE id = ?",
|
h.logger.Error("更新助手消息失败", zap.Error(errU))
|
||||||
result.Response,
|
|
||||||
func() string {
|
|
||||||
if len(result.MCPExecutionIDs) > 0 {
|
|
||||||
jsonData, _ := json.Marshal(result.MCPExecutionIDs)
|
|
||||||
return string(jsonData)
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}(),
|
|
||||||
time.Now(), assistantMessageID,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
h.logger.Error("更新助手消息失败", zap.Error(err))
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 如果之前创建失败,现在创建
|
// 如果之前创建失败,现在创建
|
||||||
@@ -2664,12 +2655,12 @@ func (h *AgentHandler) executeBatchQueue(queueID string) {
|
|||||||
var runErr error
|
var runErr error
|
||||||
switch {
|
switch {
|
||||||
case useBatchMulti:
|
case useBatchMulti:
|
||||||
resultMA, runErr = multiagent.RunDeepAgent(taskCtx, h.config, &h.config.MultiAgent, h.agent, h.logger, conversationID, finalMessage, []agent.ChatMessage{}, roleTools, progressCallback, h.agentsMarkdownDir, batchOrch)
|
resultMA, runErr = multiagent.RunDeepAgent(taskCtx, h.config, &h.config.MultiAgent, h.agent, h.logger, conversationID, finalMessage, []agent.ChatMessage{}, roleTools, progressCallback, h.agentsMarkdownDir, batchOrch, nil)
|
||||||
case useEinoSingle:
|
case useEinoSingle:
|
||||||
if h.config == nil {
|
if h.config == nil {
|
||||||
runErr = fmt.Errorf("服务器配置未加载")
|
runErr = fmt.Errorf("服务器配置未加载")
|
||||||
} else {
|
} else {
|
||||||
resultMA, runErr = multiagent.RunEinoSingleChatModelAgent(taskCtx, h.config, &h.config.MultiAgent, h.agent, h.logger, conversationID, finalMessage, []agent.ChatMessage{}, roleTools, progressCallback)
|
resultMA, runErr = multiagent.RunEinoSingleChatModelAgent(taskCtx, h.config, &h.config.MultiAgent, h.agent, h.logger, conversationID, finalMessage, []agent.ChatMessage{}, roleTools, progressCallback, nil)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
result, runErr = h.agent.AgentLoopWithProgress(taskCtx, finalMessage, []agent.ChatMessage{}, conversationID, progressCallback, roleTools)
|
result, runErr = h.agent.AgentLoopWithProgress(taskCtx, finalMessage, []agent.ChatMessage{}, conversationID, progressCallback, roleTools)
|
||||||
@@ -2768,17 +2759,7 @@ func (h *AgentHandler) executeBatchQueue(queueID string) {
|
|||||||
|
|
||||||
// 更新助手消息内容
|
// 更新助手消息内容
|
||||||
if assistantMessageID != "" {
|
if assistantMessageID != "" {
|
||||||
mcpIDsJSON := ""
|
if updateErr := h.db.UpdateAssistantMessageFinalize(assistantMessageID, resText, mcpIDs, multiagent.AggregatedReasoningFromTraceJSON(lastIn)); updateErr != nil {
|
||||||
if len(mcpIDs) > 0 {
|
|
||||||
jsonData, _ := json.Marshal(mcpIDs)
|
|
||||||
mcpIDsJSON = string(jsonData)
|
|
||||||
}
|
|
||||||
if _, updateErr := h.db.Exec(
|
|
||||||
"UPDATE messages SET content = ?, mcp_execution_ids = ?, updated_at = ? WHERE id = ?",
|
|
||||||
resText,
|
|
||||||
mcpIDsJSON,
|
|
||||||
time.Now(), assistantMessageID,
|
|
||||||
); updateErr != nil {
|
|
||||||
h.logger.Warn("更新助手消息失败", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.Error(updateErr))
|
h.logger.Warn("更新助手消息失败", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.Error(updateErr))
|
||||||
// 如果更新失败,尝试创建新消息
|
// 如果更新失败,尝试创建新消息
|
||||||
_, err = h.db.AddMessage(conversationID, "assistant", resText, mcpIDs)
|
_, err = h.db.AddMessage(conversationID, "assistant", resText, mcpIDs)
|
||||||
@@ -2870,6 +2851,10 @@ func (h *AgentHandler) loadHistoryFromAgentTrace(conversationID string) ([]agent
|
|||||||
if content, ok := msgMap["content"].(string); ok {
|
if content, ok := msgMap["content"].(string); ok {
|
||||||
msg.Content = content
|
msg.Content = content
|
||||||
}
|
}
|
||||||
|
// DeepSeek 思考模式:含工具调用的 assistant 须在后续请求中回传 reasoning_content
|
||||||
|
if rc, ok := msgMap["reasoning_content"].(string); ok && strings.TrimSpace(rc) != "" {
|
||||||
|
msg.ReasoningContent = rc
|
||||||
|
}
|
||||||
|
|
||||||
// 解析tool_calls(如果存在)
|
// 解析tool_calls(如果存在)
|
||||||
if toolCallsRaw, ok := msgMap["tool_calls"]; ok && toolCallsRaw != nil {
|
if toolCallsRaw, ok := msgMap["tool_calls"]; ok && toolCallsRaw != nil {
|
||||||
@@ -2975,3 +2960,18 @@ func (h *AgentHandler) loadHistoryFromAgentTrace(conversationID string) ([]agent
|
|||||||
)
|
)
|
||||||
return agentMessages, nil
|
return agentMessages, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dbMessagesToAgentChatMessages maps DB rows to agent ChatMessage for history fallback
|
||||||
|
// (includes reasoning_content for DeepSeek thinking + tool replay).
|
||||||
|
func dbMessagesToAgentChatMessages(msgs []database.Message) []agent.ChatMessage {
|
||||||
|
out := make([]agent.ChatMessage, 0, len(msgs))
|
||||||
|
for i := range msgs {
|
||||||
|
m := msgs[i]
|
||||||
|
out = append(out, agent.ChatMessage{
|
||||||
|
Role: m.Role,
|
||||||
|
Content: m.Content,
|
||||||
|
ReasoningContent: m.ReasoningContent,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
@@ -1312,6 +1312,19 @@ func updateOpenAIConfig(doc *yaml.Node, cfg config.OpenAIConfig) {
|
|||||||
if cfg.MaxTotalTokens > 0 {
|
if cfg.MaxTotalTokens > 0 {
|
||||||
setIntInMap(openaiNode, "max_total_tokens", cfg.MaxTotalTokens)
|
setIntInMap(openaiNode, "max_total_tokens", cfg.MaxTotalTokens)
|
||||||
}
|
}
|
||||||
|
rn := ensureMap(openaiNode, "reasoning")
|
||||||
|
if strings.TrimSpace(cfg.Reasoning.Mode) != "" {
|
||||||
|
setStringInMap(rn, "mode", cfg.Reasoning.Mode)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(cfg.Reasoning.Effort) != "" {
|
||||||
|
setStringInMap(rn, "effort", cfg.Reasoning.Effort)
|
||||||
|
}
|
||||||
|
if cfg.Reasoning.AllowClientReasoning != nil {
|
||||||
|
setBoolInMap(rn, "allow_client_reasoning", *cfg.Reasoning.AllowClientReasoning)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(cfg.Reasoning.Profile) != "" {
|
||||||
|
setStringInMap(rn, "profile", cfg.Reasoning.Profile)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateFOFAConfig(doc *yaml.Node, cfg config.FofaConfig) {
|
func updateFOFAConfig(doc *yaml.Node, cfg config.FofaConfig) {
|
||||||
|
|||||||
@@ -196,6 +196,7 @@ func (h *AgentHandler) EinoSingleAgentLoopStream(c *gin.Context) {
|
|||||||
curHistory,
|
curHistory,
|
||||||
roleTools,
|
roleTools,
|
||||||
progressCallback,
|
progressCallback,
|
||||||
|
chatReasoningToClientIntent(req.Reasoning),
|
||||||
)
|
)
|
||||||
timeoutCancel()
|
timeoutCancel()
|
||||||
|
|
||||||
@@ -297,18 +298,7 @@ func (h *AgentHandler) EinoSingleAgentLoopStream(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if assistantMessageID != "" {
|
if assistantMessageID != "" {
|
||||||
mcpIDsJSON := ""
|
_ = h.db.UpdateAssistantMessageFinalize(assistantMessageID, result.Response, cumulativeMCPExecutionIDs, multiagent.AggregatedReasoningFromTraceJSON(result.LastAgentTraceInput))
|
||||||
if len(cumulativeMCPExecutionIDs) > 0 {
|
|
||||||
jsonData, _ := json.Marshal(cumulativeMCPExecutionIDs)
|
|
||||||
mcpIDsJSON = string(jsonData)
|
|
||||||
}
|
|
||||||
_, _ = h.db.Exec(
|
|
||||||
"UPDATE messages SET content = ?, mcp_execution_ids = ?, updated_at = ? WHERE id = ?",
|
|
||||||
result.Response,
|
|
||||||
mcpIDsJSON,
|
|
||||||
time.Now(),
|
|
||||||
assistantMessageID,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.LastAgentTraceInput != "" || result.LastAgentTraceOutput != "" {
|
if result.LastAgentTraceInput != "" || result.LastAgentTraceOutput != "" {
|
||||||
@@ -376,6 +366,7 @@ func (h *AgentHandler) EinoSingleAgentLoop(c *gin.Context) {
|
|||||||
prep.History,
|
prep.History,
|
||||||
prep.RoleTools,
|
prep.RoleTools,
|
||||||
progressCallback,
|
progressCallback,
|
||||||
|
chatReasoningToClientIntent(req.Reasoning),
|
||||||
)
|
)
|
||||||
if runErr != nil {
|
if runErr != nil {
|
||||||
if shouldPersistEinoAgentTraceAfterRunError(baseCtx) {
|
if shouldPersistEinoAgentTraceAfterRunError(baseCtx) {
|
||||||
@@ -386,18 +377,7 @@ func (h *AgentHandler) EinoSingleAgentLoop(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if prep.AssistantMessageID != "" {
|
if prep.AssistantMessageID != "" {
|
||||||
mcpIDsJSON := ""
|
_ = h.db.UpdateAssistantMessageFinalize(prep.AssistantMessageID, result.Response, result.MCPExecutionIDs, multiagent.AggregatedReasoningFromTraceJSON(result.LastAgentTraceInput))
|
||||||
if len(result.MCPExecutionIDs) > 0 {
|
|
||||||
jsonData, _ := json.Marshal(result.MCPExecutionIDs)
|
|
||||||
mcpIDsJSON = string(jsonData)
|
|
||||||
}
|
|
||||||
_, _ = h.db.Exec(
|
|
||||||
"UPDATE messages SET content = ?, mcp_execution_ids = ?, updated_at = ? WHERE id = ?",
|
|
||||||
result.Response,
|
|
||||||
mcpIDsJSON,
|
|
||||||
time.Now(),
|
|
||||||
prep.AssistantMessageID,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if result.LastAgentTraceInput != "" || result.LastAgentTraceOutput != "" {
|
if result.LastAgentTraceInput != "" || result.LastAgentTraceOutput != "" {
|
||||||
_ = h.db.SaveAgentTrace(prep.ConversationID, result.LastAgentTraceInput, result.LastAgentTraceOutput)
|
_ = h.db.SaveAgentTrace(prep.ConversationID, result.LastAgentTraceInput, result.LastAgentTraceOutput)
|
||||||
|
|||||||
@@ -208,6 +208,7 @@ func (h *AgentHandler) MultiAgentLoopStream(c *gin.Context) {
|
|||||||
progressCallback,
|
progressCallback,
|
||||||
h.agentsMarkdownDir,
|
h.agentsMarkdownDir,
|
||||||
orch,
|
orch,
|
||||||
|
chatReasoningToClientIntent(req.Reasoning),
|
||||||
)
|
)
|
||||||
timeoutCancel()
|
timeoutCancel()
|
||||||
|
|
||||||
@@ -309,18 +310,7 @@ func (h *AgentHandler) MultiAgentLoopStream(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if assistantMessageID != "" {
|
if assistantMessageID != "" {
|
||||||
mcpIDsJSON := ""
|
_ = h.db.UpdateAssistantMessageFinalize(assistantMessageID, result.Response, cumulativeMCPExecutionIDs, multiagent.AggregatedReasoningFromTraceJSON(result.LastAgentTraceInput))
|
||||||
if len(cumulativeMCPExecutionIDs) > 0 {
|
|
||||||
jsonData, _ := json.Marshal(cumulativeMCPExecutionIDs)
|
|
||||||
mcpIDsJSON = string(jsonData)
|
|
||||||
}
|
|
||||||
_, _ = h.db.Exec(
|
|
||||||
"UPDATE messages SET content = ?, mcp_execution_ids = ?, updated_at = ? WHERE id = ?",
|
|
||||||
result.Response,
|
|
||||||
mcpIDsJSON,
|
|
||||||
time.Now(),
|
|
||||||
assistantMessageID,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.LastAgentTraceInput != "" || result.LastAgentTraceOutput != "" {
|
if result.LastAgentTraceInput != "" || result.LastAgentTraceOutput != "" {
|
||||||
@@ -390,6 +380,7 @@ func (h *AgentHandler) MultiAgentLoop(c *gin.Context) {
|
|||||||
progressCallback,
|
progressCallback,
|
||||||
h.agentsMarkdownDir,
|
h.agentsMarkdownDir,
|
||||||
strings.TrimSpace(req.Orchestration),
|
strings.TrimSpace(req.Orchestration),
|
||||||
|
chatReasoningToClientIntent(req.Reasoning),
|
||||||
)
|
)
|
||||||
if runErr != nil {
|
if runErr != nil {
|
||||||
if shouldPersistEinoAgentTraceAfterRunError(baseCtx) {
|
if shouldPersistEinoAgentTraceAfterRunError(baseCtx) {
|
||||||
@@ -405,18 +396,7 @@ func (h *AgentHandler) MultiAgentLoop(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if prep.AssistantMessageID != "" {
|
if prep.AssistantMessageID != "" {
|
||||||
mcpIDsJSON := ""
|
_ = h.db.UpdateAssistantMessageFinalize(prep.AssistantMessageID, result.Response, result.MCPExecutionIDs, multiagent.AggregatedReasoningFromTraceJSON(result.LastAgentTraceInput))
|
||||||
if len(result.MCPExecutionIDs) > 0 {
|
|
||||||
jsonData, _ := json.Marshal(result.MCPExecutionIDs)
|
|
||||||
mcpIDsJSON = string(jsonData)
|
|
||||||
}
|
|
||||||
_, _ = h.db.Exec(
|
|
||||||
"UPDATE messages SET content = ?, mcp_execution_ids = ?, updated_at = ? WHERE id = ?",
|
|
||||||
result.Response,
|
|
||||||
mcpIDsJSON,
|
|
||||||
time.Now(),
|
|
||||||
prep.AssistantMessageID,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.LastAgentTraceInput != "" || result.LastAgentTraceOutput != "" {
|
if result.LastAgentTraceInput != "" || result.LastAgentTraceOutput != "" {
|
||||||
|
|||||||
@@ -55,13 +55,7 @@ func (h *AgentHandler) prepareMultiAgentSession(req *ChatRequest) (*multiAgentPr
|
|||||||
if getErr != nil {
|
if getErr != nil {
|
||||||
agentHistoryMessages = []agent.ChatMessage{}
|
agentHistoryMessages = []agent.ChatMessage{}
|
||||||
} else {
|
} else {
|
||||||
agentHistoryMessages = make([]agent.ChatMessage, 0, len(historyMessages))
|
agentHistoryMessages = dbMessagesToAgentChatMessages(historyMessages)
|
||||||
for _, msg := range historyMessages {
|
|
||||||
agentHistoryMessages = append(agentHistoryMessages, agent.ChatMessage{
|
|
||||||
Role: msg.Role,
|
|
||||||
Content: msg.Content,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
|
|
||||||
"cyberstrike-ai/internal/agent"
|
"cyberstrike-ai/internal/agent"
|
||||||
"cyberstrike-ai/internal/einomcp"
|
"cyberstrike-ai/internal/einomcp"
|
||||||
|
"cyberstrike-ai/internal/openai"
|
||||||
|
|
||||||
"github.com/cloudwego/eino/adk"
|
"github.com/cloudwego/eino/adk"
|
||||||
"github.com/cloudwego/eino/schema"
|
"github.com/cloudwego/eino/schema"
|
||||||
@@ -550,6 +551,7 @@ func runEinoADKAgentLoop(ctx context.Context, args *einoADKRunLoopArgs, baseMsgs
|
|||||||
var mainAssistantBuf string
|
var mainAssistantBuf string
|
||||||
var mainAssistDupTarget string // 非空表示本段主助手流需缓冲至 EOF,与 execute 输出比对去重
|
var mainAssistDupTarget string // 非空表示本段主助手流需缓冲至 EOF,与 execute 输出比对去重
|
||||||
var reasoningBuf string
|
var reasoningBuf string
|
||||||
|
var prevReasoningDisplay string // UI 用:剥离 Claude 内部 signature 尾缀后的累计展示
|
||||||
var streamRecvErr error
|
var streamRecvErr error
|
||||||
type streamMsg struct {
|
type streamMsg struct {
|
||||||
chunk *schema.Message
|
chunk *schema.Message
|
||||||
@@ -597,19 +599,29 @@ func runEinoADKAgentLoop(ctx context.Context, args *einoADKRunLoopArgs, baseMsgs
|
|||||||
var reasoningDelta string
|
var reasoningDelta string
|
||||||
reasoningBuf, reasoningDelta = normalizeStreamingDelta(reasoningBuf, chunk.ReasoningContent)
|
reasoningBuf, reasoningDelta = normalizeStreamingDelta(reasoningBuf, chunk.ReasoningContent)
|
||||||
if reasoningDelta != "" {
|
if reasoningDelta != "" {
|
||||||
if reasoningStreamID == "" {
|
fullDisplay := openai.DisplayReasoningContent(reasoningBuf)
|
||||||
reasoningStreamID = fmt.Sprintf("eino-reasoning-%s-%d", conversationID, atomic.AddInt64(&reasoningStreamSeq, 1))
|
var displayDelta string
|
||||||
progress("thinking_stream_start", " ", map[string]interface{}{
|
if strings.HasPrefix(fullDisplay, prevReasoningDisplay) {
|
||||||
"streamId": reasoningStreamID,
|
displayDelta = fullDisplay[len(prevReasoningDisplay):]
|
||||||
"source": "eino",
|
} else {
|
||||||
"einoAgent": ev.AgentName,
|
displayDelta = fullDisplay
|
||||||
"einoRole": einoRoleTag(ev.AgentName),
|
}
|
||||||
"orchestration": orchMode,
|
prevReasoningDisplay = fullDisplay
|
||||||
|
if displayDelta != "" {
|
||||||
|
if reasoningStreamID == "" {
|
||||||
|
reasoningStreamID = fmt.Sprintf("eino-reasoning-%s-%d", conversationID, atomic.AddInt64(&reasoningStreamSeq, 1))
|
||||||
|
progress("reasoning_chain_stream_start", " ", map[string]interface{}{
|
||||||
|
"streamId": reasoningStreamID,
|
||||||
|
"source": "eino",
|
||||||
|
"einoAgent": ev.AgentName,
|
||||||
|
"einoRole": einoRoleTag(ev.AgentName),
|
||||||
|
"orchestration": orchMode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
progress("reasoning_chain_stream_delta", displayDelta, map[string]interface{}{
|
||||||
|
"streamId": reasoningStreamID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
progress("thinking_stream_delta", reasoningDelta, map[string]interface{}{
|
|
||||||
"streamId": reasoningStreamID,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if chunk.Content != "" {
|
if chunk.Content != "" {
|
||||||
@@ -777,7 +789,7 @@ func runEinoADKAgentLoop(ctx context.Context, args *einoADKRunLoopArgs, baseMsgs
|
|||||||
|
|
||||||
if mv.Role == schema.Assistant {
|
if mv.Role == schema.Assistant {
|
||||||
if progress != nil && strings.TrimSpace(msg.ReasoningContent) != "" {
|
if progress != nil && strings.TrimSpace(msg.ReasoningContent) != "" {
|
||||||
progress("thinking", strings.TrimSpace(msg.ReasoningContent), map[string]interface{}{
|
progress("reasoning_chain", openai.DisplayReasoningContent(strings.TrimSpace(msg.ReasoningContent)), map[string]interface{}{
|
||||||
"conversationId": conversationID,
|
"conversationId": conversationID,
|
||||||
"source": "eino",
|
"source": "eino",
|
||||||
"einoAgent": ev.AgentName,
|
"einoAgent": ev.AgentName,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"cyberstrike-ai/internal/config"
|
"cyberstrike-ai/internal/config"
|
||||||
"cyberstrike-ai/internal/einomcp"
|
"cyberstrike-ai/internal/einomcp"
|
||||||
"cyberstrike-ai/internal/openai"
|
"cyberstrike-ai/internal/openai"
|
||||||
|
"cyberstrike-ai/internal/reasoning"
|
||||||
|
|
||||||
einoopenai "github.com/cloudwego/eino-ext/components/model/openai"
|
einoopenai "github.com/cloudwego/eino-ext/components/model/openai"
|
||||||
"github.com/cloudwego/eino/adk"
|
"github.com/cloudwego/eino/adk"
|
||||||
@@ -37,6 +38,7 @@ func RunEinoSingleChatModelAgent(
|
|||||||
history []agent.ChatMessage,
|
history []agent.ChatMessage,
|
||||||
roleTools []string,
|
roleTools []string,
|
||||||
progress func(eventType, message string, data interface{}),
|
progress func(eventType, message string, data interface{}),
|
||||||
|
reasoningClient *reasoning.ClientIntent,
|
||||||
) (*RunResult, error) {
|
) (*RunResult, error) {
|
||||||
if appCfg == nil || ag == nil {
|
if appCfg == nil || ag == nil {
|
||||||
return nil, fmt.Errorf("eino single: 配置或 Agent 为空")
|
return nil, fmt.Errorf("eino single: 配置或 Agent 为空")
|
||||||
@@ -121,6 +123,7 @@ func RunEinoSingleChatModelAgent(
|
|||||||
Model: appCfg.OpenAI.Model,
|
Model: appCfg.OpenAI.Model,
|
||||||
HTTPClient: httpClient,
|
HTTPClient: httpClient,
|
||||||
}
|
}
|
||||||
|
reasoning.ApplyToEinoChatModelConfig(baseModelCfg, &appCfg.OpenAI, reasoningClient)
|
||||||
|
|
||||||
mainModel, err := einoopenai.NewChatModel(ctx, baseModelCfg)
|
mainModel, err := einoopenai.NewChatModel(ctx, baseModelCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ func summarizeFinalizeWithRecentAssistantToolTrail(
|
|||||||
selectedCount++
|
selectedCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
// 还原时间顺序
|
// 还原时间顺序。round 内为原始 *schema.Message 指针,保留 ReasoningContent(DeepSeek 工具续跑所必需)。
|
||||||
selectedMsgs := make([]adk.Message, 0, 8)
|
selectedMsgs := make([]adk.Message, 0, 8)
|
||||||
for i := len(selectedRoundsReverse) - 1; i >= 0; i-- {
|
for i := len(selectedRoundsReverse) - 1; i >= 0; i-- {
|
||||||
selectedMsgs = append(selectedMsgs, selectedRoundsReverse[i].messages...)
|
selectedMsgs = append(selectedMsgs, selectedRoundsReverse[i].messages...)
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package multiagent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AggregatedReasoningFromTraceJSON concatenates non-empty assistant `reasoning_content`
|
||||||
|
// fields from last_react-style JSON (slice of message objects) in document order.
|
||||||
|
// Used to persist on the single assistant bubble row for audit and for GetMessages fallback
|
||||||
|
// when the full trace JSON is unavailable. For strict per-message replay, prefer last_react_input.
|
||||||
|
func AggregatedReasoningFromTraceJSON(traceJSON string) string {
|
||||||
|
traceJSON = strings.TrimSpace(traceJSON)
|
||||||
|
if traceJSON == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var arr []map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(traceJSON), &arr); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var b strings.Builder
|
||||||
|
for _, m := range arr {
|
||||||
|
role, _ := m["role"].(string)
|
||||||
|
if !strings.EqualFold(strings.TrimSpace(role), "assistant") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rc := reasoningContentFromMessageMap(m)
|
||||||
|
if rc == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if b.Len() > 0 {
|
||||||
|
b.WriteByte('\n')
|
||||||
|
}
|
||||||
|
b.WriteString(rc)
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func reasoningContentFromMessageMap(m map[string]interface{}) string {
|
||||||
|
if m == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
switch v := m["reasoning_content"].(type) {
|
||||||
|
case string:
|
||||||
|
return strings.TrimSpace(v)
|
||||||
|
case nil:
|
||||||
|
return ""
|
||||||
|
default:
|
||||||
|
return strings.TrimSpace(fmt.Sprint(v))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package multiagent
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestAggregatedReasoningFromTraceJSON(t *testing.T) {
|
||||||
|
const j = `[
|
||||||
|
{"role":"user","content":"hi"},
|
||||||
|
{"role":"assistant","content":"c1","reasoning_content":"r1","tool_calls":[{"id":"1","type":"function","function":{"name":"f","arguments":"{}"}}]},
|
||||||
|
{"role":"tool","tool_call_id":"1","content":"out"},
|
||||||
|
{"role":"assistant","content":"c2","reasoning_content":"r2"}
|
||||||
|
]`
|
||||||
|
got := AggregatedReasoningFromTraceJSON(j)
|
||||||
|
want := "r1\nr2"
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("got %q want %q", got, want)
|
||||||
|
}
|
||||||
|
if AggregatedReasoningFromTraceJSON("") != "" || AggregatedReasoningFromTraceJSON("[]") != "" {
|
||||||
|
t.Fatal("empty expected")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"cyberstrike-ai/internal/config"
|
"cyberstrike-ai/internal/config"
|
||||||
"cyberstrike-ai/internal/einomcp"
|
"cyberstrike-ai/internal/einomcp"
|
||||||
"cyberstrike-ai/internal/openai"
|
"cyberstrike-ai/internal/openai"
|
||||||
|
"cyberstrike-ai/internal/reasoning"
|
||||||
|
|
||||||
einoopenai "github.com/cloudwego/eino-ext/components/model/openai"
|
einoopenai "github.com/cloudwego/eino-ext/components/model/openai"
|
||||||
"github.com/cloudwego/eino/adk"
|
"github.com/cloudwego/eino/adk"
|
||||||
@@ -48,6 +49,7 @@ type toolCallPendingInfo struct {
|
|||||||
|
|
||||||
// RunDeepAgent 使用 Eino 多代理预置编排执行一轮对话(deep / plan_execute / supervisor;流式事件通过 progress 回调输出)。
|
// RunDeepAgent 使用 Eino 多代理预置编排执行一轮对话(deep / plan_execute / supervisor;流式事件通过 progress 回调输出)。
|
||||||
// orchestrationOverride 非空时优先(如聊天/WebShell 请求体);否则用 multi_agent.orchestration(遗留 yaml);皆空则按 deep。
|
// orchestrationOverride 非空时优先(如聊天/WebShell 请求体);否则用 multi_agent.orchestration(遗留 yaml);皆空则按 deep。
|
||||||
|
// reasoningClient 来自 ChatRequest.reasoning;可为 nil(机器人/批量等走全局 openai.reasoning)。
|
||||||
func RunDeepAgent(
|
func RunDeepAgent(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
appCfg *config.Config,
|
appCfg *config.Config,
|
||||||
@@ -61,6 +63,7 @@ func RunDeepAgent(
|
|||||||
progress func(eventType, message string, data interface{}),
|
progress func(eventType, message string, data interface{}),
|
||||||
agentsMarkdownDir string,
|
agentsMarkdownDir string,
|
||||||
orchestrationOverride string,
|
orchestrationOverride string,
|
||||||
|
reasoningClient *reasoning.ClientIntent,
|
||||||
) (*RunResult, error) {
|
) (*RunResult, error) {
|
||||||
if appCfg == nil || ma == nil || ag == nil {
|
if appCfg == nil || ma == nil || ag == nil {
|
||||||
return nil, fmt.Errorf("multiagent: 配置或 Agent 为空")
|
return nil, fmt.Errorf("multiagent: 配置或 Agent 为空")
|
||||||
@@ -163,6 +166,7 @@ func RunDeepAgent(
|
|||||||
Model: appCfg.OpenAI.Model,
|
Model: appCfg.OpenAI.Model,
|
||||||
HTTPClient: httpClient,
|
HTTPClient: httpClient,
|
||||||
}
|
}
|
||||||
|
reasoning.ApplyToEinoChatModelConfig(baseModelCfg, &appCfg.OpenAI, reasoningClient)
|
||||||
|
|
||||||
deepMaxIter := ma.MaxIteration
|
deepMaxIter := ma.MaxIteration
|
||||||
if deepMaxIter <= 0 {
|
if deepMaxIter <= 0 {
|
||||||
@@ -636,8 +640,13 @@ func historyToMessages(history []agent.ChatMessage, appCfg *config.Config, mwCfg
|
|||||||
}
|
}
|
||||||
case "assistant":
|
case "assistant":
|
||||||
toolSchema := chatToolCallsToSchema(h.ToolCalls)
|
toolSchema := chatToolCallsToSchema(h.ToolCalls)
|
||||||
if len(toolSchema) > 0 || strings.TrimSpace(h.Content) != "" {
|
hasRC := strings.TrimSpace(h.ReasoningContent) != ""
|
||||||
raw = append(raw, schema.AssistantMessage(h.Content, toolSchema))
|
if len(toolSchema) > 0 || strings.TrimSpace(h.Content) != "" || hasRC {
|
||||||
|
am := schema.AssistantMessage(h.Content, toolSchema)
|
||||||
|
if hasRC {
|
||||||
|
am.ReasoningContent = strings.TrimSpace(h.ReasoningContent)
|
||||||
|
}
|
||||||
|
raw = append(raw, am)
|
||||||
}
|
}
|
||||||
case "tool":
|
case "tool":
|
||||||
if strings.TrimSpace(h.ToolCallID) == "" && strings.TrimSpace(h.Content) == "" {
|
if strings.TrimSpace(h.ToolCallID) == "" && strings.TrimSpace(h.Content) == "" {
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package multiagent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"cyberstrike-ai/internal/agent"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHistoryToMessagesPreservesReasoningContent(t *testing.T) {
|
||||||
|
h := []agent.ChatMessage{
|
||||||
|
{Role: "user", Content: "u"},
|
||||||
|
{Role: "assistant", Content: "c", ReasoningContent: "r1", ToolCalls: []agent.ToolCall{{ID: "t1", Type: "function", Function: agent.FunctionCall{Name: "f", Arguments: map[string]interface{}{}}}}},
|
||||||
|
}
|
||||||
|
msgs := historyToMessages(h, nil, nil)
|
||||||
|
if len(msgs) != 2 {
|
||||||
|
t.Fatalf("len=%d", len(msgs))
|
||||||
|
}
|
||||||
|
am := msgs[1]
|
||||||
|
if am.ReasoningContent != "r1" || am.Content != "c" {
|
||||||
|
t.Fatalf("got reasoning=%q content=%q", am.ReasoningContent, am.Content)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,9 @@ package openai
|
|||||||
// Stream: Claude SSE (event: content_block_delta / message_delta) → OpenAI SSE 格式
|
// Stream: Claude SSE (event: content_block_delta / message_delta) → OpenAI SSE 格式
|
||||||
// Auth: Bearer → x-api-key
|
// Auth: Bearer → x-api-key
|
||||||
// Tools: OpenAI tools[] → Claude tools[] (input_schema)
|
// Tools: OpenAI tools[] → Claude tools[] (input_schema)
|
||||||
|
//
|
||||||
|
// Extended thinking: 顶层 `thinking` 从 OpenAI 请求体透传;响应中 `thinking` block 映射为
|
||||||
|
// `reasoning_content`(可读前缀 + 内部 JSON 尾缀以保留 signature,供多轮工具续跑;UI 用 openai.DisplayReasoningContent 剥离)。
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
@@ -38,6 +41,7 @@ type claudeRequest struct {
|
|||||||
Messages []claudeMessage `json:"messages"`
|
Messages []claudeMessage `json:"messages"`
|
||||||
Tools []claudeTool `json:"tools,omitempty"`
|
Tools []claudeTool `json:"tools,omitempty"`
|
||||||
Stream bool `json:"stream,omitempty"`
|
Stream bool `json:"stream,omitempty"`
|
||||||
|
Thinking json.RawMessage `json:"thinking,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type claudeMessage struct {
|
type claudeMessage struct {
|
||||||
@@ -76,6 +80,10 @@ type claudeContentBlock struct {
|
|||||||
// text block
|
// text block
|
||||||
Text string `json:"text,omitempty"`
|
Text string `json:"text,omitempty"`
|
||||||
|
|
||||||
|
// thinking block (extended thinking)
|
||||||
|
Thinking string `json:"thinking,omitempty"`
|
||||||
|
Signature string `json:"signature,omitempty"`
|
||||||
|
|
||||||
// tool_use block (assistant 返回)
|
// tool_use block (assistant 返回)
|
||||||
ID string `json:"id,omitempty"`
|
ID string `json:"id,omitempty"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
@@ -176,7 +184,13 @@ func convertOpenAIToClaude(payload interface{}) (*claudeRequest, error) {
|
|||||||
|
|
||||||
// tool_calls (assistant 消息中包含工具调用)
|
// tool_calls (assistant 消息中包含工具调用)
|
||||||
if role == "assistant" {
|
if role == "assistant" {
|
||||||
|
rc, _ := mm["reasoning_content"].(string)
|
||||||
|
_, thinkingReplay := parseClaudeReasoningAssistantBlocks(rc)
|
||||||
|
|
||||||
var blocks []claudeContentBlock
|
var blocks []claudeContentBlock
|
||||||
|
for _, tb := range thinkingReplay {
|
||||||
|
blocks = append(blocks, tb)
|
||||||
|
}
|
||||||
if content != "" {
|
if content != "" {
|
||||||
blocks = append(blocks, claudeContentBlock{Type: "text", Text: content})
|
blocks = append(blocks, claudeContentBlock{Type: "text", Text: content})
|
||||||
}
|
}
|
||||||
@@ -290,6 +304,13 @@ func convertOpenAIToClaude(payload interface{}) (*claudeRequest, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extended thinking (Anthropic top-level); merged from Eino ExtraFields / admin extras.
|
||||||
|
if th, ok := oai["thinking"]; ok && th != nil {
|
||||||
|
if raw, err := json.Marshal(th); err == nil && len(raw) > 0 && string(raw) != "null" {
|
||||||
|
req.Thinking = json.RawMessage(raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return req, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,9 +339,12 @@ func claudeToOpenAIResponseJSON(claudeBody []byte) ([]byte, error) {
|
|||||||
|
|
||||||
var textContent string
|
var textContent string
|
||||||
var toolCalls []interface{}
|
var toolCalls []interface{}
|
||||||
|
var thinkingBlocks []claudeContentBlock
|
||||||
|
|
||||||
for _, block := range cr.Content {
|
for _, block := range cr.Content {
|
||||||
switch block.Type {
|
switch block.Type {
|
||||||
|
case "thinking":
|
||||||
|
thinkingBlocks = append(thinkingBlocks, block)
|
||||||
case "text":
|
case "text":
|
||||||
textContent += block.Text
|
textContent += block.Text
|
||||||
case "tool_use":
|
case "tool_use":
|
||||||
@@ -344,6 +368,18 @@ func claudeToOpenAIResponseJSON(claudeBody []byte) ([]byte, error) {
|
|||||||
if len(toolCalls) > 0 {
|
if len(toolCalls) > 0 {
|
||||||
message["tool_calls"] = toolCalls
|
message["tool_calls"] = toolCalls
|
||||||
}
|
}
|
||||||
|
if len(thinkingBlocks) > 0 {
|
||||||
|
var parts []string
|
||||||
|
for _, tb := range thinkingBlocks {
|
||||||
|
if strings.TrimSpace(tb.Thinking) != "" {
|
||||||
|
parts = append(parts, tb.Thinking)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rc := appendClaudeReasoningRoundTrip(strings.Join(parts, "\n\n"), thinkingBlocks)
|
||||||
|
if rc != "" {
|
||||||
|
message["reasoning_content"] = rc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
choice := map[string]interface{}{
|
choice := map[string]interface{}{
|
||||||
"index": 0,
|
"index": 0,
|
||||||
@@ -901,8 +937,16 @@ func (rt *claudeRoundTripper) RoundTrip(req *http.Request) (*http.Response, erro
|
|||||||
|
|
||||||
reader := bufio.NewReader(resp.Body)
|
reader := bufio.NewReader(resp.Body)
|
||||||
blockToToolIndex := make(map[int]int)
|
blockToToolIndex := make(map[int]int)
|
||||||
|
blockIndexToType := make(map[int]string)
|
||||||
nextToolIndex := 0
|
nextToolIndex := 0
|
||||||
|
|
||||||
|
type thinkingAcc struct {
|
||||||
|
text strings.Builder
|
||||||
|
sig strings.Builder
|
||||||
|
}
|
||||||
|
thinkingByIndex := make(map[int]*thinkingAcc)
|
||||||
|
var finishedThinking []claudeContentBlock
|
||||||
|
|
||||||
for {
|
for {
|
||||||
line, readErr := reader.ReadString('\n')
|
line, readErr := reader.ReadString('\n')
|
||||||
if readErr != nil {
|
if readErr != nil {
|
||||||
@@ -947,6 +991,11 @@ func (rt *claudeRoundTripper) RoundTrip(req *http.Request) (*http.Response, erro
|
|||||||
blockIdx := int(blockIdxFlt)
|
blockIdx := int(blockIdxFlt)
|
||||||
cb, _ := event["content_block"].(map[string]interface{})
|
cb, _ := event["content_block"].(map[string]interface{})
|
||||||
bt, _ := cb["type"].(string)
|
bt, _ := cb["type"].(string)
|
||||||
|
blockIndexToType[blockIdx] = bt
|
||||||
|
|
||||||
|
if bt == "thinking" {
|
||||||
|
thinkingByIndex[blockIdx] = &thinkingAcc{}
|
||||||
|
}
|
||||||
|
|
||||||
if bt == "tool_use" {
|
if bt == "tool_use" {
|
||||||
id, _ := cb["id"].(string)
|
id, _ := cb["id"].(string)
|
||||||
@@ -986,7 +1035,35 @@ func (rt *claudeRoundTripper) RoundTrip(req *http.Request) (*http.Response, erro
|
|||||||
delta, _ := event["delta"].(map[string]interface{})
|
delta, _ := event["delta"].(map[string]interface{})
|
||||||
dt, _ := delta["type"].(string)
|
dt, _ := delta["type"].(string)
|
||||||
|
|
||||||
if dt == "text_delta" {
|
if dt == "thinking_delta" {
|
||||||
|
tPart, _ := delta["thinking"].(string)
|
||||||
|
if tPart != "" {
|
||||||
|
if acc := thinkingByIndex[blockIdx]; acc != nil {
|
||||||
|
acc.text.WriteString(tPart)
|
||||||
|
}
|
||||||
|
oaiChunk := map[string]interface{}{
|
||||||
|
"choices": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"delta": map[string]interface{}{
|
||||||
|
"reasoning_content": tPart,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
b, _ := json.Marshal(oaiChunk)
|
||||||
|
if !writeLine("data: " + string(b) + "\n\n") {
|
||||||
|
pw.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if dt == "signature_delta" {
|
||||||
|
sigPart, _ := delta["signature"].(string)
|
||||||
|
if sigPart != "" {
|
||||||
|
if acc := thinkingByIndex[blockIdx]; acc != nil {
|
||||||
|
acc.sig.WriteString(sigPart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if dt == "text_delta" {
|
||||||
text, _ := delta["text"].(string)
|
text, _ := delta["text"].(string)
|
||||||
oaiChunk := map[string]interface{}{
|
oaiChunk := map[string]interface{}{
|
||||||
"choices": []map[string]interface{}{
|
"choices": []map[string]interface{}{
|
||||||
@@ -1031,6 +1108,21 @@ func (rt *claudeRoundTripper) RoundTrip(req *http.Request) (*http.Response, erro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "content_block_stop":
|
||||||
|
blockIdxFlt, _ := event["index"].(float64)
|
||||||
|
blockIdx := int(blockIdxFlt)
|
||||||
|
bt := blockIndexToType[blockIdx]
|
||||||
|
if bt == "thinking" {
|
||||||
|
if acc := thinkingByIndex[blockIdx]; acc != nil {
|
||||||
|
finishedThinking = append(finishedThinking, claudeContentBlock{
|
||||||
|
Type: "thinking",
|
||||||
|
Thinking: acc.text.String(),
|
||||||
|
Signature: acc.sig.String(),
|
||||||
|
})
|
||||||
|
delete(thinkingByIndex, blockIdx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case "message_delta":
|
case "message_delta":
|
||||||
d, _ := event["delta"].(map[string]interface{})
|
d, _ := event["delta"].(map[string]interface{})
|
||||||
if sr, ok := d["stop_reason"].(string); ok {
|
if sr, ok := d["stop_reason"].(string); ok {
|
||||||
@@ -1051,6 +1143,25 @@ func (rt *claudeRoundTripper) RoundTrip(req *http.Request) (*http.Response, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "message_stop":
|
case "message_stop":
|
||||||
|
if len(finishedThinking) > 0 {
|
||||||
|
suffix := appendClaudeReasoningRoundTrip("", finishedThinking)
|
||||||
|
if strings.TrimSpace(suffix) != "" {
|
||||||
|
oaiChunk := map[string]interface{}{
|
||||||
|
"choices": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"delta": map[string]interface{}{
|
||||||
|
"reasoning_content": suffix,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
b, _ := json.Marshal(oaiChunk)
|
||||||
|
if !writeLine("data: " + string(b) + "\n\n") {
|
||||||
|
pw.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
writeLine("data: [DONE]\n\n")
|
writeLine("data: [DONE]\n\n")
|
||||||
pw.Close()
|
pw.Close()
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package openai
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// claudeReasoningRoundTripSep separates human-readable reasoning from a JSON payload of
|
||||||
|
// Anthropic thinking blocks (with signatures) for multi-turn extended thinking + tools.
|
||||||
|
// Not shown in UI (see DisplayReasoningContent).
|
||||||
|
const claudeReasoningRoundTripSep = "\n---CSAI_CLAUDE_THINKING_BLOCKS---\n"
|
||||||
|
|
||||||
|
// DisplayReasoningContent returns reasoning text suitable for the UI (strips internal
|
||||||
|
// Claude round-trip JSON suffix). Safe for DeepSeek/plain reasoning strings (no-op).
|
||||||
|
func DisplayReasoningContent(s string) string {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
i := strings.LastIndex(s, claudeReasoningRoundTripSep)
|
||||||
|
if i < 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(s[:i])
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendClaudeReasoningRoundTrip(display string, blocks []claudeContentBlock) string {
|
||||||
|
var payload []map[string]string
|
||||||
|
for _, b := range blocks {
|
||||||
|
if b.Type != "thinking" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
payload = append(payload, map[string]string{
|
||||||
|
"type": b.Type,
|
||||||
|
"thinking": b.Thinking,
|
||||||
|
"signature": b.Signature,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(payload) == 0 {
|
||||||
|
return strings.TrimSpace(display)
|
||||||
|
}
|
||||||
|
js, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return strings.TrimSpace(display)
|
||||||
|
}
|
||||||
|
d := strings.TrimSpace(display)
|
||||||
|
if d == "" {
|
||||||
|
return claudeReasoningRoundTripSep + string(js)
|
||||||
|
}
|
||||||
|
return d + claudeReasoningRoundTripSep + string(js)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseClaudeReasoningAssistantBlocks extracts Anthropic thinking blocks from an OpenAI-style
|
||||||
|
// reasoning_content string. When no suffix is present, blocks is nil (caller must not invent signatures).
|
||||||
|
func parseClaudeReasoningAssistantBlocks(reasoningContent string) (display string, blocks []claudeContentBlock) {
|
||||||
|
reasoningContent = strings.TrimSpace(reasoningContent)
|
||||||
|
if reasoningContent == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
idx := strings.LastIndex(reasoningContent, claudeReasoningRoundTripSep)
|
||||||
|
if idx < 0 {
|
||||||
|
return reasoningContent, nil
|
||||||
|
}
|
||||||
|
display = strings.TrimSpace(reasoningContent[:idx])
|
||||||
|
jsonPart := strings.TrimSpace(reasoningContent[idx+len(claudeReasoningRoundTripSep):])
|
||||||
|
var arr []struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Thinking string `json:"thinking"`
|
||||||
|
Signature string `json:"signature"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(jsonPart), &arr); err != nil {
|
||||||
|
return reasoningContent, nil
|
||||||
|
}
|
||||||
|
for _, x := range arr {
|
||||||
|
if x.Type != "thinking" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
blocks = append(blocks, claudeContentBlock{Type: "thinking", Thinking: x.Thinking, Signature: x.Signature})
|
||||||
|
}
|
||||||
|
return display, blocks
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package openai
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDisplayReasoningContent(t *testing.T) {
|
||||||
|
raw := "hello" + claudeReasoningRoundTripSep + `[{"type":"thinking","thinking":"x","signature":"sig"}]`
|
||||||
|
if d := DisplayReasoningContent(raw); d != "hello" {
|
||||||
|
t.Fatalf("got %q", d)
|
||||||
|
}
|
||||||
|
if DisplayReasoningContent("plain") != "plain" {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppendParseClaudeReasoningRoundTrip(t *testing.T) {
|
||||||
|
blocks := []claudeContentBlock{
|
||||||
|
{Type: "thinking", Thinking: "a", Signature: "s1"},
|
||||||
|
{Type: "thinking", Thinking: "b", Signature: "s2"},
|
||||||
|
}
|
||||||
|
s := appendClaudeReasoningRoundTrip("sum", blocks)
|
||||||
|
if !strings.Contains(s, claudeReasoningRoundTripSep) {
|
||||||
|
t.Fatal("missing sep")
|
||||||
|
}
|
||||||
|
display, back := parseClaudeReasoningAssistantBlocks(s)
|
||||||
|
if display != "sum" || len(back) != 2 {
|
||||||
|
t.Fatalf("display=%q len=%d", display, len(back))
|
||||||
|
}
|
||||||
|
if back[0].Signature != "s1" || back[1].Thinking != "b" {
|
||||||
|
t.Fatalf("%+v", back)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertOpenAIToClaude_AssistantReasoningReplay(t *testing.T) {
|
||||||
|
rc := appendClaudeReasoningRoundTrip("vis", []claudeContentBlock{
|
||||||
|
{Type: "thinking", Thinking: "t1", Signature: "sig1"},
|
||||||
|
})
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"model": "claude-3-5-sonnet-latest",
|
||||||
|
"messages": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "out",
|
||||||
|
"reasoning_content": rc,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
req, err := convertOpenAIToClaude(payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(req.Messages) != 1 {
|
||||||
|
t.Fatalf("messages=%d", len(req.Messages))
|
||||||
|
}
|
||||||
|
blocks := req.Messages[0].Content.Blocks
|
||||||
|
if len(blocks) < 2 {
|
||||||
|
t.Fatalf("blocks=%d", len(blocks))
|
||||||
|
}
|
||||||
|
if blocks[0].Type != "thinking" || blocks[0].Signature != "sig1" {
|
||||||
|
t.Fatalf("first block %+v", blocks[0])
|
||||||
|
}
|
||||||
|
foundText := false
|
||||||
|
for _, b := range blocks {
|
||||||
|
if b.Type == "text" && b.Text == "out" {
|
||||||
|
foundText = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundText {
|
||||||
|
t.Fatalf("blocks=%+v", blocks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClaudeToOpenAIResponseJSON_Thinking(t *testing.T) {
|
||||||
|
claudeBody := []byte(`{
|
||||||
|
"id":"msg_1","type":"message","role":"assistant","model":"x","stop_reason":"end_turn",
|
||||||
|
"content":[
|
||||||
|
{"type":"thinking","thinking":"step","signature":"sigx"},
|
||||||
|
{"type":"text","text":"hi"}
|
||||||
|
]
|
||||||
|
}`)
|
||||||
|
oai, err := claudeToOpenAIResponseJSON(claudeBody)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var wrap map[string]interface{}
|
||||||
|
if err := json.Unmarshal(oai, &wrap); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
choices := wrap["choices"].([]interface{})
|
||||||
|
ch0 := choices[0].(map[string]interface{})
|
||||||
|
msg := ch0["message"].(map[string]interface{})
|
||||||
|
rc, _ := msg["reasoning_content"].(string)
|
||||||
|
if !strings.Contains(rc, "step") || !strings.Contains(rc, claudeReasoningRoundTripSep) {
|
||||||
|
t.Fatalf("reasoning_content=%q", rc)
|
||||||
|
}
|
||||||
|
if msg["content"] != "hi" {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
// Package reasoning maps user/config intent to CloudWeGo Eino OpenAI ChatModel fields
|
||||||
|
// (ReasoningEffort, ExtraFields such as thinking / reasoning_effort / output_config).
|
||||||
|
package reasoning
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"cyberstrike-ai/internal/config"
|
||||||
|
|
||||||
|
einoopenai "github.com/cloudwego/eino-ext/components/model/openai"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ClientIntent is optional per-request override from ChatRequest.reasoning.
|
||||||
|
type ClientIntent struct {
|
||||||
|
Mode string
|
||||||
|
Effort string
|
||||||
|
}
|
||||||
|
|
||||||
|
type wireProfile int
|
||||||
|
|
||||||
|
const (
|
||||||
|
wireNone wireProfile = iota
|
||||||
|
wireClaude
|
||||||
|
wireDeepseek
|
||||||
|
wireOpenAI
|
||||||
|
wireOutputConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
// ApplyToEinoChatModelConfig merges reasoning-related options into cfg.
|
||||||
|
// Precondition: cfg already has APIKey, BaseURL, Model, HTTPClient set.
|
||||||
|
func ApplyToEinoChatModelConfig(cfg *einoopenai.ChatModelConfig, oa *config.OpenAIConfig, client *ClientIntent) {
|
||||||
|
if cfg == nil || oa == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sr := &oa.Reasoning
|
||||||
|
allowClient := sr.AllowClientReasoningEffective()
|
||||||
|
mode := effectiveMode(sr, client, allowClient)
|
||||||
|
|
||||||
|
// Claude (Anthropic): merge admin extras first; optional extended thinking maps to top-level `thinking`
|
||||||
|
// (see internal/openai convertOpenAIToClaude). DeepSeek/OpenAI-style fields are not sent.
|
||||||
|
if strings.EqualFold(strings.TrimSpace(oa.Provider), "claude") ||
|
||||||
|
strings.EqualFold(strings.TrimSpace(oa.Provider), "anthropic") {
|
||||||
|
if len(sr.ExtraRequestFields) > 0 {
|
||||||
|
if cfg.ExtraFields == nil {
|
||||||
|
cfg.ExtraFields = make(map[string]any)
|
||||||
|
}
|
||||||
|
for k, v := range sr.ExtraRequestFields {
|
||||||
|
cfg.ExtraFields[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mode == "off" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
applyClaudeExtendedThinking(cfg, mode, effectiveEffort(sr, client, allowClient), oa.Model)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode == "off" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
effort := effectiveEffort(sr, client, allowClient)
|
||||||
|
prof := resolveWireProfile(oa, sr)
|
||||||
|
|
||||||
|
// Admin-defined extra root fields (merged first; automatic keys may follow).
|
||||||
|
if len(sr.ExtraRequestFields) > 0 {
|
||||||
|
if cfg.ExtraFields == nil {
|
||||||
|
cfg.ExtraFields = make(map[string]any)
|
||||||
|
}
|
||||||
|
for k, v := range sr.ExtraRequestFields {
|
||||||
|
cfg.ExtraFields[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch prof {
|
||||||
|
case wireClaude, wireNone:
|
||||||
|
return
|
||||||
|
case wireDeepseek:
|
||||||
|
applyDeepseek(cfg, mode, effort)
|
||||||
|
case wireOutputConfig:
|
||||||
|
applyOutputConfigEffort(cfg, mode, effort)
|
||||||
|
default: // wireOpenAI
|
||||||
|
applyOpenAICompat(cfg, mode, effort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyClaudeExtendedThinking sets Anthropic Messages API `thinking` when absent from ExtraRequestFields.
|
||||||
|
// Uses adaptive + summarized display by default (per Anthropic guidance for Claude 4.x); Sonnet 3.7 uses enabled+budget.
|
||||||
|
func applyClaudeExtendedThinking(cfg *einoopenai.ChatModelConfig, mode, effort, model string) {
|
||||||
|
if cfg == nil || mode == "off" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if cfg.ExtraFields == nil {
|
||||||
|
cfg.ExtraFields = make(map[string]any)
|
||||||
|
}
|
||||||
|
if _, exists := cfg.ExtraFields["thinking"]; exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m := strings.ToLower(strings.TrimSpace(model))
|
||||||
|
thinking := map[string]any{
|
||||||
|
"type": "adaptive",
|
||||||
|
"display": "summarized",
|
||||||
|
}
|
||||||
|
// Sonnet 3.7: manual extended thinking is the documented path.
|
||||||
|
if strings.Contains(m, "claude-3-7-sonnet") || strings.Contains(m, "3-7-sonnet") || strings.Contains(m, "sonnet-3.7") {
|
||||||
|
thinking = map[string]any{
|
||||||
|
"type": "enabled",
|
||||||
|
"budget_tokens": 10000,
|
||||||
|
"display": "summarized",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Opus 4.7+: manual enabled+budget rejected — keep adaptive only.
|
||||||
|
if strings.Contains(m, "opus-4-7") || strings.Contains(m, "opus-4.7") {
|
||||||
|
thinking = map[string]any{
|
||||||
|
"type": "adaptive",
|
||||||
|
"display": "summarized",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = effort // reserved: map to Anthropic effort / output_config when API stabilizes in one place
|
||||||
|
cfg.ExtraFields["thinking"] = thinking
|
||||||
|
}
|
||||||
|
|
||||||
|
func effectiveMode(sr *config.OpenAIReasoningConfig, client *ClientIntent, allowClient bool) string {
|
||||||
|
server := strings.ToLower(strings.TrimSpace(sr.ModeEffective()))
|
||||||
|
if server == "" || server == "default" {
|
||||||
|
server = "auto"
|
||||||
|
}
|
||||||
|
if !allowClient || client == nil {
|
||||||
|
return server
|
||||||
|
}
|
||||||
|
cm := strings.ToLower(strings.TrimSpace(client.Mode))
|
||||||
|
if cm == "" || cm == "default" {
|
||||||
|
return server
|
||||||
|
}
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
|
||||||
|
func effectiveEffort(sr *config.OpenAIReasoningConfig, client *ClientIntent, allowClient bool) string {
|
||||||
|
se := normalizeEffort(sr.Effort)
|
||||||
|
if !allowClient || client == nil {
|
||||||
|
return se
|
||||||
|
}
|
||||||
|
ce := normalizeEffort(client.Effort)
|
||||||
|
if ce != "" {
|
||||||
|
return ce
|
||||||
|
}
|
||||||
|
return se
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeEffort(s string) string {
|
||||||
|
e := strings.ToLower(strings.TrimSpace(s))
|
||||||
|
switch e {
|
||||||
|
case "low", "medium", "high", "max":
|
||||||
|
return e
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveWireProfile(oa *config.OpenAIConfig, sr *config.OpenAIReasoningConfig) wireProfile {
|
||||||
|
if strings.EqualFold(strings.TrimSpace(oa.Provider), "claude") {
|
||||||
|
return wireClaude
|
||||||
|
}
|
||||||
|
p := strings.ToLower(strings.TrimSpace(sr.ProfileEffective()))
|
||||||
|
switch p {
|
||||||
|
case "output_config", "output_config_effort":
|
||||||
|
return wireOutputConfig
|
||||||
|
case "openai", "openai_compat":
|
||||||
|
return wireOpenAI
|
||||||
|
case "deepseek", "deepseek_compat":
|
||||||
|
return wireDeepseek
|
||||||
|
case "auto", "":
|
||||||
|
bu := strings.ToLower(oa.BaseURL)
|
||||||
|
mo := strings.ToLower(oa.Model)
|
||||||
|
if strings.Contains(bu, "deepseek") || strings.Contains(mo, "deepseek") {
|
||||||
|
return wireDeepseek
|
||||||
|
}
|
||||||
|
return wireOpenAI
|
||||||
|
default:
|
||||||
|
return wireOpenAI
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyDeepseek(cfg *einoopenai.ChatModelConfig, mode, effort string) {
|
||||||
|
// auto: enable thinking for DeepSeek line; on: same; auto without effort still opens thinking.
|
||||||
|
if mode == "off" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if mode == "auto" || mode == "on" {
|
||||||
|
if cfg.ExtraFields == nil {
|
||||||
|
cfg.ExtraFields = make(map[string]any)
|
||||||
|
}
|
||||||
|
cfg.ExtraFields["thinking"] = map[string]any{"type": "enabled"}
|
||||||
|
}
|
||||||
|
if effort != "" {
|
||||||
|
if cfg.ExtraFields == nil {
|
||||||
|
cfg.ExtraFields = make(map[string]any)
|
||||||
|
}
|
||||||
|
cfg.ExtraFields["reasoning_effort"] = effortStringForAPI(effort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyOpenAICompat(cfg *einoopenai.ChatModelConfig, mode, effort string) {
|
||||||
|
if mode == "auto" && effort == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e := effort
|
||||||
|
if mode == "on" && e == "" {
|
||||||
|
e = "medium"
|
||||||
|
}
|
||||||
|
if e == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if e == "max" {
|
||||||
|
if cfg.ExtraFields == nil {
|
||||||
|
cfg.ExtraFields = make(map[string]any)
|
||||||
|
}
|
||||||
|
cfg.ExtraFields["reasoning_effort"] = "max"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch e {
|
||||||
|
case "low":
|
||||||
|
cfg.ReasoningEffort = einoopenai.ReasoningEffortLevelLow
|
||||||
|
case "medium":
|
||||||
|
cfg.ReasoningEffort = einoopenai.ReasoningEffortLevelMedium
|
||||||
|
case "high":
|
||||||
|
cfg.ReasoningEffort = einoopenai.ReasoningEffortLevelHigh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyOutputConfigEffort(cfg *einoopenai.ChatModelConfig, mode, effort string) {
|
||||||
|
if mode == "auto" && effort == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e := effort
|
||||||
|
if mode == "on" && e == "" {
|
||||||
|
e = "high"
|
||||||
|
}
|
||||||
|
if e == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if cfg.ExtraFields == nil {
|
||||||
|
cfg.ExtraFields = make(map[string]any)
|
||||||
|
}
|
||||||
|
cfg.ExtraFields["output_config"] = map[string]any{"effort": effortStringForAPI(e)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func effortStringForAPI(e string) string {
|
||||||
|
// Gateways expect lowercase strings; "max" kept as max.
|
||||||
|
return strings.ToLower(strings.TrimSpace(e))
|
||||||
|
}
|
||||||
+122
-2
@@ -2391,7 +2391,118 @@ header {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-input-container > .chat-input-with-files {
|
.chat-input-primary-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input-leading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Eino:模型推理收进浮层,保持主输入行简洁 */
|
||||||
|
.chat-reasoning-wrapper {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-reasoning-inner {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-reasoning-btn {
|
||||||
|
max-width: 10.5rem;
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
padding-right: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-reasoning-btn .chat-reasoning-btn-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1;
|
||||||
|
opacity: 0.95;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-reasoning-btn.active .chat-reasoning-btn-icon {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-reasoning-btn .chat-reasoning-btn-summary {
|
||||||
|
max-width: 7.6rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-reasoning-btn.active {
|
||||||
|
border-color: rgba(49, 130, 206, 0.45);
|
||||||
|
background: rgba(49, 130, 206, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-reasoning-panel {
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(100% + 8px);
|
||||||
|
left: 0;
|
||||||
|
width: 288px;
|
||||||
|
max-width: calc(100vw - 32px);
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 12px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12), 0 4px 16px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(0, 0, 0, 0.04);
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-reasoning-panel-header {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-reasoning-panel-hint {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-muted, #718096);
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-reasoning-fields {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-reasoning-field-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted, #718096);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-reasoning-select {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0.45rem 0.6rem;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
border: 1px solid var(--border-color, #e2e8f0);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--card-bg, #fff);
|
||||||
|
color: var(--text-color, #2d3748);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input-container .chat-input-with-files,
|
||||||
|
.chat-input-primary-row .chat-input-with-files {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -2399,7 +2510,8 @@ header {
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-input-container > .chat-input-field {
|
.chat-input-container > .chat-input-field,
|
||||||
|
.chat-input-primary-row .chat-input-field {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -3568,6 +3680,11 @@ header {
|
|||||||
background: rgba(156, 39, 176, 0.05);
|
background: rgba(156, 39, 176, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.timeline-item-reasoning_chain {
|
||||||
|
border-left-color: #5c6bc0;
|
||||||
|
background: rgba(92, 107, 192, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
.timeline-item-tool_call {
|
.timeline-item-tool_call {
|
||||||
border-left-color: #ff9800;
|
border-left-color: #ff9800;
|
||||||
background: rgba(255, 152, 0, 0.05);
|
background: rgba(255, 152, 0, 0.05);
|
||||||
@@ -12294,6 +12411,9 @@ header {
|
|||||||
.webshell-ai-process-block .webshell-ai-timeline-thinking {
|
.webshell-ai-process-block .webshell-ai-timeline-thinking {
|
||||||
border-left-color: #9c27b0;
|
border-left-color: #9c27b0;
|
||||||
}
|
}
|
||||||
|
.webshell-ai-process-block .webshell-ai-timeline-reasoning_chain {
|
||||||
|
border-left-color: #5c6bc0;
|
||||||
|
}
|
||||||
.webshell-ai-process-block .webshell-ai-timeline-tool_call,
|
.webshell-ai-process-block .webshell-ai-timeline-tool_call,
|
||||||
.webshell-ai-process-block .webshell-ai-timeline-tool_calls_detected {
|
.webshell-ai-process-block .webshell-ai-timeline-tool_calls_detected {
|
||||||
border-left-color: #ff9800;
|
border-left-color: #ff9800;
|
||||||
|
|||||||
@@ -277,6 +277,7 @@
|
|||||||
"planExecuteStreamPhase": "Phase output",
|
"planExecuteStreamPhase": "Phase output",
|
||||||
"einoSubAgentStep": "Sub-agent {{agent}} · step {{n}}",
|
"einoSubAgentStep": "Sub-agent {{agent}} · step {{n}}",
|
||||||
"aiThinking": "AI thinking",
|
"aiThinking": "AI thinking",
|
||||||
|
"reasoningChain": "Reasoning process",
|
||||||
"planning": "Planning",
|
"planning": "Planning",
|
||||||
"assistantStreamPhase": "Assistant output",
|
"assistantStreamPhase": "Assistant output",
|
||||||
"toolCallsDetected": "Detected {{count}} tool call(s)",
|
"toolCallsDetected": "Detected {{count}} tool call(s)",
|
||||||
@@ -329,6 +330,19 @@
|
|||||||
"agentModeMulti": "Multi-agent",
|
"agentModeMulti": "Multi-agent",
|
||||||
"agentModeSingleHint": "Single-model ReAct loop for chat and tool use",
|
"agentModeSingleHint": "Single-model ReAct loop for chat and tool use",
|
||||||
"agentModeMultiHint": "Eino prebuilt orchestration (deep / plan_execute / supervisor) for complex tasks",
|
"agentModeMultiHint": "Eino prebuilt orchestration (deep / plan_execute / supervisor) for complex tasks",
|
||||||
|
"reasoningModeLabel": "Model reasoning",
|
||||||
|
"reasoningEffortLabel": "Reasoning effort",
|
||||||
|
"reasoningModeDefault": "Use system default",
|
||||||
|
"reasoningModeOff": "Off",
|
||||||
|
"reasoningModeOn": "On",
|
||||||
|
"reasoningModeAuto": "Auto",
|
||||||
|
"reasoningEffortUnset": "Unspecified",
|
||||||
|
"reasoningCompactLabel": "Reasoning",
|
||||||
|
"reasoningCompactAria": "Open model reasoning options",
|
||||||
|
"reasoningPanelTitle": "Model reasoning",
|
||||||
|
"reasoningPanelHint": "Only Eino single- and multi-agent requests use these; merged with defaults in Settings.",
|
||||||
|
"reasoningSummaryFollow": "System",
|
||||||
|
"reasoningSummaryDash": "—",
|
||||||
"agentModeOrchPlanExecute": "Plan-Exec",
|
"agentModeOrchPlanExecute": "Plan-Exec",
|
||||||
"agentModeOrchSupervisor": "Supervisor",
|
"agentModeOrchSupervisor": "Supervisor",
|
||||||
"hitlTitle": "Human-in-the-loop",
|
"hitlTitle": "Human-in-the-loop",
|
||||||
@@ -1592,6 +1606,10 @@
|
|||||||
"maxTotalTokens": "Max Context Tokens",
|
"maxTotalTokens": "Max Context Tokens",
|
||||||
"maxTotalTokensPlaceholder": "120000",
|
"maxTotalTokensPlaceholder": "120000",
|
||||||
"maxTotalTokensHint": "Shared by memory compression and attack chain building. Default: 120000",
|
"maxTotalTokensHint": "Shared by memory compression and attack chain building. Default: 120000",
|
||||||
|
"openaiReasoningTitle": "Model reasoning (Eino)",
|
||||||
|
"openaiReasoningHint": "Applies to Eino single-agent and multi-agent only; works with chat-page reasoning controls.",
|
||||||
|
"openaiReasoningProfile": "Wire profile",
|
||||||
|
"openaiReasoningAllowClient": "Allow chat page to override reasoning options",
|
||||||
"fofaBaseUrlPlaceholder": "https://fofa.info/api/v1/search/all (optional)",
|
"fofaBaseUrlPlaceholder": "https://fofa.info/api/v1/search/all (optional)",
|
||||||
"fofaBaseUrlHint": "Leave empty for default.",
|
"fofaBaseUrlHint": "Leave empty for default.",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
|||||||
@@ -266,6 +266,7 @@
|
|||||||
"planExecuteStreamPhase": "阶段输出",
|
"planExecuteStreamPhase": "阶段输出",
|
||||||
"einoSubAgentStep": "子代理 {{agent}} · 第 {{n}} 步",
|
"einoSubAgentStep": "子代理 {{agent}} · 第 {{n}} 步",
|
||||||
"aiThinking": "AI思考",
|
"aiThinking": "AI思考",
|
||||||
|
"reasoningChain": "推理过程",
|
||||||
"planning": "规划中",
|
"planning": "规划中",
|
||||||
"assistantStreamPhase": "助手输出",
|
"assistantStreamPhase": "助手输出",
|
||||||
"toolCallsDetected": "检测到 {{count}} 个工具调用",
|
"toolCallsDetected": "检测到 {{count}} 个工具调用",
|
||||||
@@ -318,6 +319,19 @@
|
|||||||
"agentModeMulti": "多代理",
|
"agentModeMulti": "多代理",
|
||||||
"agentModeSingleHint": "单模型 ReAct 循环,适合常规对话与工具调用",
|
"agentModeSingleHint": "单模型 ReAct 循环,适合常规对话与工具调用",
|
||||||
"agentModeMultiHint": "Eino 预置编排(deep / plan_execute / supervisor),适合复杂任务",
|
"agentModeMultiHint": "Eino 预置编排(deep / plan_execute / supervisor),适合复杂任务",
|
||||||
|
"reasoningModeLabel": "模型推理",
|
||||||
|
"reasoningEffortLabel": "推理强度",
|
||||||
|
"reasoningModeDefault": "跟随系统",
|
||||||
|
"reasoningModeOff": "关闭",
|
||||||
|
"reasoningModeOn": "开启",
|
||||||
|
"reasoningModeAuto": "自动",
|
||||||
|
"reasoningEffortUnset": "不指定",
|
||||||
|
"reasoningCompactLabel": "推理",
|
||||||
|
"reasoningCompactAria": "打开模型推理选项",
|
||||||
|
"reasoningPanelTitle": "模型推理",
|
||||||
|
"reasoningPanelHint": "仅 Eino 单代理与多代理请求会带上这些参数;与系统设置中的默认值合并。",
|
||||||
|
"reasoningSummaryFollow": "系统",
|
||||||
|
"reasoningSummaryDash": "—",
|
||||||
"agentModeOrchPlanExecute": "Plan-Exec",
|
"agentModeOrchPlanExecute": "Plan-Exec",
|
||||||
"agentModeOrchSupervisor": "Supervisor",
|
"agentModeOrchSupervisor": "Supervisor",
|
||||||
"hitlTitle": "人机协同",
|
"hitlTitle": "人机协同",
|
||||||
@@ -1581,6 +1595,10 @@
|
|||||||
"maxTotalTokens": "最大上下文 Token 数",
|
"maxTotalTokens": "最大上下文 Token 数",
|
||||||
"maxTotalTokensPlaceholder": "120000",
|
"maxTotalTokensPlaceholder": "120000",
|
||||||
"maxTotalTokensHint": "内存压缩和攻击链构建共用此配置,默认 120000",
|
"maxTotalTokensHint": "内存压缩和攻击链构建共用此配置,默认 120000",
|
||||||
|
"openaiReasoningTitle": "模型推理(Eino)",
|
||||||
|
"openaiReasoningHint": "仅 Eino 单代理与多代理请求生效;与对话页「模型推理」下拉配合使用。",
|
||||||
|
"openaiReasoningProfile": "线路 profile",
|
||||||
|
"openaiReasoningAllowClient": "允许对话页覆盖推理选项",
|
||||||
"fofaBaseUrlPlaceholder": "https://fofa.info/api/v1/search/all(可选)",
|
"fofaBaseUrlPlaceholder": "https://fofa.info/api/v1/search/all(可选)",
|
||||||
"fofaBaseUrlHint": "留空则使用默认地址。",
|
"fofaBaseUrlHint": "留空则使用默认地址。",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ let chatAttachmentSeq = 0;
|
|||||||
|
|
||||||
// 对话模式:react = 原生 ReAct(/agent-loop);eino_single = Eino ADK 单代理(/api/eino-agent/stream);deep / plan_execute / supervisor = Eino 多代理(/api/multi-agent/stream,请求体 orchestration)
|
// 对话模式:react = 原生 ReAct(/agent-loop);eino_single = Eino ADK 单代理(/api/eino-agent/stream);deep / plan_execute / supervisor = Eino 多代理(/api/multi-agent/stream,请求体 orchestration)
|
||||||
const AGENT_MODE_STORAGE_KEY = 'cyberstrike-chat-agent-mode';
|
const AGENT_MODE_STORAGE_KEY = 'cyberstrike-chat-agent-mode';
|
||||||
|
const REASONING_MODE_LS = 'cyberstrike-chat-reasoning-mode';
|
||||||
|
const REASONING_EFFORT_LS = 'cyberstrike-chat-reasoning-effort';
|
||||||
const CHAT_AGENT_MODE_REACT = 'react';
|
const CHAT_AGENT_MODE_REACT = 'react';
|
||||||
const CHAT_AGENT_MODE_EINO_SINGLE = 'eino_single';
|
const CHAT_AGENT_MODE_EINO_SINGLE = 'eino_single';
|
||||||
const CHAT_AGENT_EINO_MODES = ['deep', 'plan_execute', 'supervisor'];
|
const CHAT_AGENT_EINO_MODES = ['deep', 'plan_execute', 'supervisor'];
|
||||||
@@ -492,6 +494,132 @@ function syncAgentModeFromValue(value) {
|
|||||||
const v = el.getAttribute('data-value');
|
const v = el.getAttribute('data-value');
|
||||||
el.classList.toggle('selected', v === value);
|
el.classList.toggle('selected', v === value);
|
||||||
});
|
});
|
||||||
|
syncReasoningRowVisibility(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncReasoningRowVisibility(modeVal) {
|
||||||
|
const wrap = document.getElementById('chat-reasoning-wrapper');
|
||||||
|
if (!wrap) return;
|
||||||
|
const show = modeVal === CHAT_AGENT_MODE_EINO_SINGLE || (multiAgentAPIEnabled && chatAgentModeIsEino(modeVal));
|
||||||
|
wrap.style.display = show ? '' : 'none';
|
||||||
|
if (!show) {
|
||||||
|
closeChatReasoningPanel();
|
||||||
|
} else {
|
||||||
|
updateChatReasoningSummary();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reasoningSummaryModeLabel(mode) {
|
||||||
|
const m = (mode || 'default').trim();
|
||||||
|
const t = (typeof window.t === 'function') ? window.t : function (k) { return k; };
|
||||||
|
switch (m) {
|
||||||
|
case 'off': return t('chat.reasoningModeOff');
|
||||||
|
case 'on': return t('chat.reasoningModeOn');
|
||||||
|
case 'auto': return t('chat.reasoningModeAuto');
|
||||||
|
default: return t('chat.reasoningSummaryFollow');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateChatReasoningSummary() {
|
||||||
|
const el = document.getElementById('chat-reasoning-summary');
|
||||||
|
const modeEl = document.getElementById('chat-reasoning-mode');
|
||||||
|
const effEl = document.getElementById('chat-reasoning-effort');
|
||||||
|
if (!el || !modeEl) return;
|
||||||
|
const mode = (modeEl.value || 'default').trim();
|
||||||
|
const effort = effEl && effEl.value ? String(effEl.value).trim() : '';
|
||||||
|
const t = (typeof window.t === 'function') ? window.t : function (k) { return k; };
|
||||||
|
const modePart = reasoningSummaryModeLabel(mode);
|
||||||
|
const effPart = effort || t('chat.reasoningSummaryDash');
|
||||||
|
el.textContent = modePart + ' / ' + effPart;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeChatReasoningPanel() {
|
||||||
|
const panel = document.getElementById('chat-reasoning-panel');
|
||||||
|
const btn = document.getElementById('chat-reasoning-btn');
|
||||||
|
if (panel) panel.style.display = 'none';
|
||||||
|
if (btn) {
|
||||||
|
btn.classList.remove('active');
|
||||||
|
btn.setAttribute('aria-expanded', 'false');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleChatReasoningPanel() {
|
||||||
|
const panel = document.getElementById('chat-reasoning-panel');
|
||||||
|
const btn = document.getElementById('chat-reasoning-btn');
|
||||||
|
if (!panel || !btn) return;
|
||||||
|
const isOpen = panel.style.display === 'flex';
|
||||||
|
if (isOpen) {
|
||||||
|
closeChatReasoningPanel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof closeAgentModePanel === 'function') {
|
||||||
|
closeAgentModePanel();
|
||||||
|
}
|
||||||
|
if (typeof closeRoleSelectionPanel === 'function') {
|
||||||
|
closeRoleSelectionPanel();
|
||||||
|
}
|
||||||
|
updateChatReasoningSummary();
|
||||||
|
panel.style.display = 'flex';
|
||||||
|
btn.classList.add('active');
|
||||||
|
btn.setAttribute('aria-expanded', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreChatReasoningControlsFromStorage() {
|
||||||
|
try {
|
||||||
|
const m = document.getElementById('chat-reasoning-mode');
|
||||||
|
const e = document.getElementById('chat-reasoning-effort');
|
||||||
|
if (m) {
|
||||||
|
const v = localStorage.getItem(REASONING_MODE_LS);
|
||||||
|
if (v && ['default', 'off', 'on', 'auto'].indexOf(v) !== -1) {
|
||||||
|
m.value = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (e) {
|
||||||
|
const v = localStorage.getItem(REASONING_EFFORT_LS);
|
||||||
|
if (v !== null && ['', 'low', 'medium', 'high', 'max'].indexOf(v) !== -1) {
|
||||||
|
e.value = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateChatReasoningSummary();
|
||||||
|
} catch (err) { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistChatReasoningPrefs() {
|
||||||
|
try {
|
||||||
|
const m = document.getElementById('chat-reasoning-mode');
|
||||||
|
const elEff = document.getElementById('chat-reasoning-effort');
|
||||||
|
if (m) localStorage.setItem(REASONING_MODE_LS, m.value || 'default');
|
||||||
|
if (elEff) localStorage.setItem(REASONING_EFFORT_LS, elEff.value || '');
|
||||||
|
updateChatReasoningSummary();
|
||||||
|
} catch (err) { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 供 WebShell 等复用:在 Eino 路径下返回 reasoning 请求片段或 undefined */
|
||||||
|
function buildReasoningRequestPayload() {
|
||||||
|
const wrap = document.getElementById('chat-reasoning-wrapper');
|
||||||
|
if (!wrap || wrap.style.display === 'none') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const modeEl = document.getElementById('chat-reasoning-mode');
|
||||||
|
const effEl = document.getElementById('chat-reasoning-effort');
|
||||||
|
if (!modeEl) return undefined;
|
||||||
|
const mode = (modeEl.value || 'default').trim();
|
||||||
|
const effort = effEl && effEl.value ? String(effEl.value).trim() : '';
|
||||||
|
if (mode === 'default' && !effort) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const o = {};
|
||||||
|
if (mode !== 'default') o.mode = mode;
|
||||||
|
if (effort) o.effort = effort;
|
||||||
|
return Object.keys(o).length ? o : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.persistChatReasoningPrefs = persistChatReasoningPrefs;
|
||||||
|
window.buildReasoningRequestPayload = buildReasoningRequestPayload;
|
||||||
|
window.closeChatReasoningPanel = closeChatReasoningPanel;
|
||||||
|
window.toggleChatReasoningPanel = toggleChatReasoningPanel;
|
||||||
|
window.updateChatReasoningSummary = updateChatReasoningSummary;
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeAgentModePanel() {
|
function closeAgentModePanel() {
|
||||||
@@ -513,6 +641,9 @@ function toggleAgentModePanel() {
|
|||||||
closeAgentModePanel();
|
closeAgentModePanel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (typeof closeChatReasoningPanel === 'function') {
|
||||||
|
closeChatReasoningPanel();
|
||||||
|
}
|
||||||
if (typeof closeRoleSelectionPanel === 'function') {
|
if (typeof closeRoleSelectionPanel === 'function') {
|
||||||
closeRoleSelectionPanel();
|
closeRoleSelectionPanel();
|
||||||
}
|
}
|
||||||
@@ -563,6 +694,8 @@ async function initChatAgentModeFromConfig() {
|
|||||||
} catch (e) { /* ignore */ }
|
} catch (e) { /* ignore */ }
|
||||||
sel.value = stored;
|
sel.value = stored;
|
||||||
syncAgentModeFromValue(stored);
|
syncAgentModeFromValue(stored);
|
||||||
|
restoreChatReasoningControlsFromStorage();
|
||||||
|
syncReasoningRowVisibility(stored);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('initChatAgentModeFromConfig', e);
|
console.warn('initChatAgentModeFromConfig', e);
|
||||||
}
|
}
|
||||||
@@ -575,6 +708,9 @@ document.addEventListener('languagechange', function () {
|
|||||||
if (v === CHAT_AGENT_MODE_REACT || chatAgentModeIsEinoSingle(v) || chatAgentModeIsEino(v)) {
|
if (v === CHAT_AGENT_MODE_REACT || chatAgentModeIsEinoSingle(v) || chatAgentModeIsEino(v)) {
|
||||||
syncAgentModeFromValue(v);
|
syncAgentModeFromValue(v);
|
||||||
}
|
}
|
||||||
|
if (typeof updateChatReasoningSummary === 'function') {
|
||||||
|
updateChatReasoningSummary();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 保存输入框草稿到localStorage(防抖版本)
|
// 保存输入框草稿到localStorage(防抖版本)
|
||||||
@@ -760,6 +896,10 @@ async function sendMessage() {
|
|||||||
serverPath: a.serverPath
|
serverPath: a.serverPath
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
const reasoningPayload = buildReasoningRequestPayload();
|
||||||
|
if (reasoningPayload) {
|
||||||
|
body.reasoning = reasoningPayload;
|
||||||
|
}
|
||||||
// 发送后清空附件列表
|
// 发送后清空附件列表
|
||||||
chatAttachments = [];
|
chatAttachments = [];
|
||||||
renderChatFileChips();
|
renderChatFileChips();
|
||||||
@@ -2228,6 +2368,8 @@ function renderProcessDetails(messageId, processDetails) {
|
|||||||
}
|
}
|
||||||
} else if (eventType === 'thinking') {
|
} else if (eventType === 'thinking') {
|
||||||
itemTitle = agPx + '🤔 ' + (typeof window.t === 'function' ? window.t('chat.aiThinking') : 'AI思考');
|
itemTitle = agPx + '🤔 ' + (typeof window.t === 'function' ? window.t('chat.aiThinking') : 'AI思考');
|
||||||
|
} else if (eventType === 'reasoning_chain') {
|
||||||
|
itemTitle = agPx + '🔗 ' + (typeof window.t === 'function' ? window.t('chat.reasoningChain') : '推理过程');
|
||||||
} else if (eventType === 'planning') {
|
} else if (eventType === 'planning') {
|
||||||
if (typeof window.einoMainStreamPlanningTitle === 'function') {
|
if (typeof window.einoMainStreamPlanningTitle === 'function') {
|
||||||
itemTitle = window.einoMainStreamPlanningTitle(data);
|
itemTitle = window.einoMainStreamPlanningTitle(data);
|
||||||
@@ -7233,6 +7375,14 @@ document.addEventListener('click', function(event) {
|
|||||||
closeAgentModePanel();
|
closeAgentModePanel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const reasoningWrap = document.getElementById('chat-reasoning-wrapper');
|
||||||
|
const reasoningPanel = document.getElementById('chat-reasoning-panel');
|
||||||
|
if (reasoningWrap && reasoningPanel && reasoningPanel.style.display === 'flex') {
|
||||||
|
if (!reasoningWrap.contains(event.target)) {
|
||||||
|
closeChatReasoningPanel();
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 创建分组
|
// 创建分组
|
||||||
|
|||||||
@@ -1223,11 +1223,14 @@ function handleStreamEvent(event, progressElement, progressId,
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'thinking_stream_start': {
|
case 'thinking_stream_start':
|
||||||
|
case 'reasoning_chain_stream_start': {
|
||||||
const d = event.data || {};
|
const d = event.data || {};
|
||||||
const streamId = d.streamId || null;
|
const streamId = d.streamId || null;
|
||||||
if (!streamId) break;
|
if (!streamId) break;
|
||||||
|
|
||||||
|
const timelineType = event.type === 'reasoning_chain_stream_start' ? 'reasoning_chain' : 'thinking';
|
||||||
|
|
||||||
let state = thinkingStreamStateByProgressId.get(progressId);
|
let state = thinkingStreamStateByProgressId.get(progressId);
|
||||||
if (!state) {
|
if (!state) {
|
||||||
state = new Map();
|
state = new Map();
|
||||||
@@ -1246,9 +1249,12 @@ function handleStreamEvent(event, progressElement, progressId,
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const thinkBase = typeof window.t === 'function' ? window.t('chat.aiThinking') : 'AI思考';
|
const labelBase = typeof window.t === 'function'
|
||||||
const title = timelineAgentBracketPrefix(d) + '🤔 ' + thinkBase;
|
? window.t(timelineType === 'reasoning_chain' ? 'chat.reasoningChain' : 'chat.aiThinking')
|
||||||
const itemId = addTimelineItem(timeline, 'thinking', {
|
: (timelineType === 'reasoning_chain' ? '推理过程' : 'AI思考');
|
||||||
|
const emoji = timelineType === 'reasoning_chain' ? '🔗' : '🤔';
|
||||||
|
const title = timelineAgentBracketPrefix(d) + emoji + ' ' + labelBase;
|
||||||
|
const itemId = addTimelineItem(timeline, timelineType, {
|
||||||
title: title,
|
title: title,
|
||||||
message: ' ',
|
message: ' ',
|
||||||
data: d
|
data: d
|
||||||
@@ -1257,7 +1263,8 @@ function handleStreamEvent(event, progressElement, progressId,
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'thinking_stream_delta': {
|
case 'thinking_stream_delta':
|
||||||
|
case 'reasoning_chain_stream_delta': {
|
||||||
const d = event.data || {};
|
const d = event.data || {};
|
||||||
const streamId = d.streamId || null;
|
const streamId = d.streamId || null;
|
||||||
if (!streamId) break;
|
if (!streamId) break;
|
||||||
@@ -1281,7 +1288,9 @@ function handleStreamEvent(event, progressElement, progressId,
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'thinking':
|
case 'thinking':
|
||||||
// 如果本 thinking 是由 thinking_stream_* 聚合出来的(带 streamId),避免重复创建 timeline item
|
case 'reasoning_chain': {
|
||||||
|
const timelineType = event.type === 'reasoning_chain' ? 'reasoning_chain' : 'thinking';
|
||||||
|
// 若已由 *_stream_* 聚合(带 streamId),避免重复创建 timeline item
|
||||||
if (event.data && event.data.streamId) {
|
if (event.data && event.data.streamId) {
|
||||||
const streamId = event.data.streamId;
|
const streamId = event.data.streamId;
|
||||||
const state = thinkingStreamStateByProgressId.get(progressId);
|
const state = thinkingStreamStateByProgressId.get(progressId);
|
||||||
@@ -1303,12 +1312,17 @@ function handleStreamEvent(event, progressElement, progressId,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addTimelineItem(timeline, 'thinking', {
|
const labelBase = typeof window.t === 'function'
|
||||||
title: timelineAgentBracketPrefix(event.data) + '🤔 ' + (typeof window.t === 'function' ? window.t('chat.aiThinking') : 'AI思考'),
|
? window.t(timelineType === 'reasoning_chain' ? 'chat.reasoningChain' : 'chat.aiThinking')
|
||||||
|
: (timelineType === 'reasoning_chain' ? '推理过程' : 'AI思考');
|
||||||
|
const emoji = timelineType === 'reasoning_chain' ? '🔗' : '🤔';
|
||||||
|
addTimelineItem(timeline, timelineType, {
|
||||||
|
title: timelineAgentBracketPrefix(event.data) + emoji + ' ' + labelBase,
|
||||||
message: event.message,
|
message: event.message,
|
||||||
data: event.data
|
data: event.data
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'tool_calls_detected':
|
case 'tool_calls_detected':
|
||||||
addTimelineItem(timeline, 'tool_calls_detected', {
|
addTimelineItem(timeline, 'tool_calls_detected', {
|
||||||
@@ -2475,7 +2489,7 @@ function addTimelineItem(timeline, type, options) {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
// 根据类型添加详细内容
|
// 根据类型添加详细内容
|
||||||
if ((type === 'thinking' || type === 'planning') && options.message) {
|
if ((type === 'thinking' || type === 'reasoning_chain' || type === 'planning') && options.message) {
|
||||||
const streamBody = typeof formatTimelineStreamBody === 'function'
|
const streamBody = typeof formatTimelineStreamBody === 'function'
|
||||||
? formatTimelineStreamBody(options.message, options.data)
|
? formatTimelineStreamBody(options.message, options.data)
|
||||||
: options.message;
|
: options.message;
|
||||||
@@ -3410,6 +3424,8 @@ function refreshProgressAndTimelineI18n() {
|
|||||||
} else {
|
} else {
|
||||||
titleSpan.textContent = ap + '\uD83E\uDD14 ' + _t('chat.aiThinking');
|
titleSpan.textContent = ap + '\uD83E\uDD14 ' + _t('chat.aiThinking');
|
||||||
}
|
}
|
||||||
|
} else if (type === 'reasoning_chain') {
|
||||||
|
titleSpan.textContent = ap + '\uD83D\uDD17 ' + _t('chat.reasoningChain');
|
||||||
} else if (type === 'planning') {
|
} else if (type === 'planning') {
|
||||||
if (item.dataset.orchestration && typeof einoMainStreamPlanningTitle === 'function') {
|
if (item.dataset.orchestration && typeof einoMainStreamPlanningTitle === 'function') {
|
||||||
titleSpan.textContent = einoMainStreamPlanningTitle({
|
titleSpan.textContent = einoMainStreamPlanningTitle({
|
||||||
|
|||||||
@@ -256,6 +256,9 @@ function toggleRoleSelectionPanel() {
|
|||||||
if (typeof closeAgentModePanel === 'function') {
|
if (typeof closeAgentModePanel === 'function') {
|
||||||
closeAgentModePanel();
|
closeAgentModePanel();
|
||||||
}
|
}
|
||||||
|
if (typeof closeChatReasoningPanel === 'function') {
|
||||||
|
closeChatReasoningPanel();
|
||||||
|
}
|
||||||
panel.style.display = 'flex'; // 使用flex布局
|
panel.style.display = 'flex'; // 使用flex布局
|
||||||
// 添加打开状态的视觉反馈
|
// 添加打开状态的视觉反馈
|
||||||
if (roleSelectorBtn) {
|
if (roleSelectorBtn) {
|
||||||
|
|||||||
@@ -159,6 +159,27 @@ async function loadConfig(loadTools = true) {
|
|||||||
if (maxTokensEl) {
|
if (maxTokensEl) {
|
||||||
maxTokensEl.value = currentConfig.openai.max_total_tokens || 120000;
|
maxTokensEl.value = currentConfig.openai.max_total_tokens || 120000;
|
||||||
}
|
}
|
||||||
|
const orm = currentConfig.openai && currentConfig.openai.reasoning ? currentConfig.openai.reasoning : {};
|
||||||
|
const orModeEl = document.getElementById('openai-reasoning-mode');
|
||||||
|
if (orModeEl) {
|
||||||
|
const mv = (orm.mode || 'auto').toString().trim().toLowerCase();
|
||||||
|
orModeEl.value = ['auto', 'on', 'off'].includes(mv) ? mv : 'auto';
|
||||||
|
}
|
||||||
|
const orEffEl = document.getElementById('openai-reasoning-effort');
|
||||||
|
if (orEffEl) {
|
||||||
|
const ev = (orm.effort || '').toString().trim().toLowerCase();
|
||||||
|
orEffEl.value = ['', 'low', 'medium', 'high', 'max'].includes(ev) ? ev : '';
|
||||||
|
}
|
||||||
|
const orProfEl = document.getElementById('openai-reasoning-profile');
|
||||||
|
if (orProfEl) {
|
||||||
|
const pv = (orm.profile || 'auto').toString().trim().toLowerCase();
|
||||||
|
const ok = ['auto', 'deepseek_compat', 'openai_compat', 'output_config_effort'];
|
||||||
|
orProfEl.value = ok.includes(pv) ? pv : 'auto';
|
||||||
|
}
|
||||||
|
const orAllowEl = document.getElementById('openai-reasoning-allow-client');
|
||||||
|
if (orAllowEl) {
|
||||||
|
orAllowEl.checked = orm.allow_client_reasoning !== false;
|
||||||
|
}
|
||||||
|
|
||||||
// 填充FOFA配置
|
// 填充FOFA配置
|
||||||
const fofa = currentConfig.fofa || {};
|
const fofa = currentConfig.fofa || {};
|
||||||
@@ -1065,13 +1086,22 @@ async function applySettings() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const wecomAgentIdVal = document.getElementById('robot-wecom-agent-id')?.value.trim();
|
const wecomAgentIdVal = document.getElementById('robot-wecom-agent-id')?.value.trim();
|
||||||
|
const prevOpenai = (currentConfig && currentConfig.openai) ? currentConfig.openai : {};
|
||||||
const config = {
|
const config = {
|
||||||
openai: {
|
openai: {
|
||||||
|
...prevOpenai,
|
||||||
provider: provider,
|
provider: provider,
|
||||||
api_key: apiKey,
|
api_key: apiKey,
|
||||||
base_url: baseUrl,
|
base_url: baseUrl,
|
||||||
model: model,
|
model: model,
|
||||||
max_total_tokens: parseInt(document.getElementById('openai-max-total-tokens')?.value) || 120000
|
max_total_tokens: parseInt(document.getElementById('openai-max-total-tokens')?.value) || 120000,
|
||||||
|
reasoning: {
|
||||||
|
...(prevOpenai.reasoning || {}),
|
||||||
|
mode: document.getElementById('openai-reasoning-mode')?.value || 'auto',
|
||||||
|
effort: (document.getElementById('openai-reasoning-effort')?.value || '').trim(),
|
||||||
|
profile: document.getElementById('openai-reasoning-profile')?.value || 'auto',
|
||||||
|
allow_client_reasoning: document.getElementById('openai-reasoning-allow-client')?.checked !== false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
fofa: {
|
fofa: {
|
||||||
email: document.getElementById('fofa-email')?.value.trim() || '',
|
email: document.getElementById('fofa-email')?.value.trim() || '',
|
||||||
|
|||||||
+23
-11
@@ -1658,6 +1658,8 @@ function buildWebshellTimelineItemFromDetail(detail) {
|
|||||||
title = ap + ((typeof window.t === 'function') ? window.t('chat.iterationRound', { n: data.iteration || 1 }) : ('第 ' + (data.iteration || 1) + ' 轮迭代'));
|
title = ap + ((typeof window.t === 'function') ? window.t('chat.iterationRound', { n: data.iteration || 1 }) : ('第 ' + (data.iteration || 1) + ' 轮迭代'));
|
||||||
} else if (eventType === 'thinking') {
|
} else if (eventType === 'thinking') {
|
||||||
title = ap + '🤔 ' + ((typeof window.t === 'function') ? window.t('chat.aiThinking') : 'AI 思考');
|
title = ap + '🤔 ' + ((typeof window.t === 'function') ? window.t('chat.aiThinking') : 'AI 思考');
|
||||||
|
} else if (eventType === 'reasoning_chain') {
|
||||||
|
title = ap + '🔗 ' + ((typeof window.t === 'function') ? window.t('chat.reasoningChain') : '推理过程');
|
||||||
} else if (eventType === 'tool_calls_detected') {
|
} else if (eventType === 'tool_calls_detected') {
|
||||||
title = ap + '🔧 ' + ((typeof window.t === 'function') ? window.t('chat.toolCallsDetected', { count: data.count || 0 }) : ('检测到 ' + (data.count || 0) + ' 个工具调用'));
|
title = ap + '🔧 ' + ((typeof window.t === 'function') ? window.t('chat.toolCallsDetected', { count: data.count || 0 }) : ('检测到 ' + (data.count || 0) + ' 个工具调用'));
|
||||||
} else if (eventType === 'tool_call') {
|
} else if (eventType === 'tool_call') {
|
||||||
@@ -2847,6 +2849,12 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
|
|||||||
if (info && info.orchestration) {
|
if (info && info.orchestration) {
|
||||||
body.orchestration = info.orchestration;
|
body.orchestration = info.orchestration;
|
||||||
}
|
}
|
||||||
|
if (typeof window.buildReasoningRequestPayload === 'function') {
|
||||||
|
var rp = window.buildReasoningRequestPayload();
|
||||||
|
if (rp) {
|
||||||
|
body.reasoning = rp;
|
||||||
|
}
|
||||||
|
}
|
||||||
return apiFetch(info.path, {
|
return apiFetch(info.path, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -2953,17 +2961,19 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
|
|||||||
appendTimelineItem('iteration', '🔍 ' + iterTitle, iterMessage, _ed);
|
appendTimelineItem('iteration', '🔍 ' + iterTitle, iterMessage, _ed);
|
||||||
if (!streamingTarget) assistantDiv.textContent = '…';
|
if (!streamingTarget) assistantDiv.textContent = '…';
|
||||||
|
|
||||||
// ─── Thinking (non-stream + stream) ───
|
// ─── Thinking / reasoning_chain(推理过程,reasoning_content) ───
|
||||||
} else if (_et === 'thinking_stream_start' && _ed.streamId) {
|
} else if ((_et === 'thinking_stream_start' || _et === 'reasoning_chain_stream_start') && _ed.streamId) {
|
||||||
|
var isRcStart = _et === 'reasoning_chain_stream_start';
|
||||||
if (wsThinkingStreams.has(_ed.streamId)) {
|
if (wsThinkingStreams.has(_ed.streamId)) {
|
||||||
var tsExist = wsThinkingStreams.get(_ed.streamId);
|
var tsExist = wsThinkingStreams.get(_ed.streamId);
|
||||||
tsExist.buf = '';
|
tsExist.buf = '';
|
||||||
if (tsExist.body) tsExist.body.textContent = '';
|
if (tsExist.body) tsExist.body.textContent = '';
|
||||||
} else {
|
} else {
|
||||||
var thinkSLabel = wsTOr('chat.aiThinking', 'AI 思考');
|
var thinkSLabel = wsTOr(isRcStart ? 'chat.reasoningChain' : 'chat.aiThinking', isRcStart ? '推理过程' : 'AI 思考');
|
||||||
|
var thinkEmoji = isRcStart ? '🔗' : '🤔';
|
||||||
var thinkSItem = document.createElement('div');
|
var thinkSItem = document.createElement('div');
|
||||||
thinkSItem.className = 'webshell-ai-timeline-item webshell-ai-timeline-thinking';
|
thinkSItem.className = 'webshell-ai-timeline-item webshell-ai-timeline-' + (isRcStart ? 'reasoning_chain' : 'thinking');
|
||||||
thinkSItem.innerHTML = '<span class="webshell-ai-timeline-title">' + escapeHtml(webshellAgentPx(_ed) + '🤔 ' + thinkSLabel) + '</span>';
|
thinkSItem.innerHTML = '<span class="webshell-ai-timeline-title">' + escapeHtml(webshellAgentPx(_ed) + thinkEmoji + ' ' + thinkSLabel) + '</span>';
|
||||||
var thinkSPre = document.createElement('div');
|
var thinkSPre = document.createElement('div');
|
||||||
thinkSPre.className = 'webshell-ai-timeline-msg webshell-thinking-stream-body';
|
thinkSPre.className = 'webshell-ai-timeline-msg webshell-thinking-stream-body';
|
||||||
thinkSItem.appendChild(thinkSPre);
|
thinkSItem.appendChild(thinkSPre);
|
||||||
@@ -2972,7 +2982,7 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
|
|||||||
wsThinkingStreams.set(_ed.streamId, { el: thinkSItem, body: thinkSPre, buf: '' });
|
wsThinkingStreams.set(_ed.streamId, { el: thinkSItem, body: thinkSPre, buf: '' });
|
||||||
}
|
}
|
||||||
if (!streamingTarget) assistantDiv.textContent = '…';
|
if (!streamingTarget) assistantDiv.textContent = '…';
|
||||||
} else if (_et === 'thinking_stream_delta' && _ed.streamId) {
|
} else if ((_et === 'thinking_stream_delta' || _et === 'reasoning_chain_stream_delta') && _ed.streamId) {
|
||||||
var tsD = wsThinkingStreams.get(_ed.streamId);
|
var tsD = wsThinkingStreams.get(_ed.streamId);
|
||||||
if (tsD) {
|
if (tsD) {
|
||||||
var normT = (typeof window.normalizeStreamingDeltaJs === 'function')
|
var normT = (typeof window.normalizeStreamingDeltaJs === 'function')
|
||||||
@@ -2985,7 +2995,7 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!streamingTarget) assistantDiv.textContent = '…';
|
if (!streamingTarget) assistantDiv.textContent = '…';
|
||||||
} else if (_et === 'thinking_stream_end' && _ed.streamId) {
|
} else if ((_et === 'thinking_stream_end' || _et === 'reasoning_chain_stream_end') && _ed.streamId) {
|
||||||
var tsE = wsThinkingStreams.get(_ed.streamId);
|
var tsE = wsThinkingStreams.get(_ed.streamId);
|
||||||
if (tsE) {
|
if (tsE) {
|
||||||
var fullThink = (_em != null && _em !== '') ? String(_em) : tsE.buf;
|
var fullThink = (_em != null && _em !== '') ? String(_em) : tsE.buf;
|
||||||
@@ -2996,13 +3006,15 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
|
|||||||
}
|
}
|
||||||
wsThinkingStreams.delete(_ed.streamId);
|
wsThinkingStreams.delete(_ed.streamId);
|
||||||
}
|
}
|
||||||
} else if (_et === 'thinking' && _em) {
|
} else if ((_et === 'thinking' || _et === 'reasoning_chain') && _em) {
|
||||||
// 如果有 streamId 且已存在流式条目,跳过避免重复
|
// 如果有 streamId 且已存在流式条目,跳过避免重复
|
||||||
if (_ed.streamId && wsThinkingStreams.has(_ed.streamId)) {
|
if (_ed.streamId && wsThinkingStreams.has(_ed.streamId)) {
|
||||||
// 已由 thinking_stream_* 处理
|
// 已由 *_stream_* 处理
|
||||||
} else {
|
} else {
|
||||||
var thinkLabel = wsTOr('chat.aiThinking', 'AI 思考');
|
var isRc = _et === 'reasoning_chain';
|
||||||
appendTimelineItem('thinking', webshellAgentPx(_ed) + '🤔 ' + thinkLabel, _em, _ed);
|
var thinkLabel = wsTOr(isRc ? 'chat.reasoningChain' : 'chat.aiThinking', isRc ? '推理过程' : 'AI 思考');
|
||||||
|
var thinkEm = isRc ? '🔗' : '🤔';
|
||||||
|
appendTimelineItem(isRc ? 'reasoning_chain' : 'thinking', webshellAgentPx(_ed) + thinkEm + ' ' + thinkLabel, _em, _ed);
|
||||||
}
|
}
|
||||||
if (!streamingTarget) assistantDiv.textContent = '…';
|
if (!streamingTarget) assistantDiv.textContent = '…';
|
||||||
|
|
||||||
|
|||||||
@@ -894,6 +894,8 @@
|
|||||||
<div id="active-tasks-bar" class="active-tasks-bar"></div>
|
<div id="active-tasks-bar" class="active-tasks-bar"></div>
|
||||||
<div id="chat-messages" class="chat-messages"></div>
|
<div id="chat-messages" class="chat-messages"></div>
|
||||||
<div id="chat-input-container" class="chat-input-container">
|
<div id="chat-input-container" class="chat-input-container">
|
||||||
|
<div class="chat-input-primary-row">
|
||||||
|
<div class="chat-input-leading">
|
||||||
<div class="role-selector-wrapper">
|
<div class="role-selector-wrapper">
|
||||||
<button id="role-selector-btn" class="role-selector-btn" onclick="toggleRoleSelectionPanel()" data-i18n="chat.selectRole" data-i18n-attr="title" title="选择角色">
|
<button id="role-selector-btn" class="role-selector-btn" onclick="toggleRoleSelectionPanel()" data-i18n="chat.selectRole" data-i18n-attr="title" title="选择角色">
|
||||||
<span id="role-selector-icon" class="role-selector-icon">🔵</span>
|
<span id="role-selector-icon" class="role-selector-icon">🔵</span>
|
||||||
@@ -979,6 +981,50 @@
|
|||||||
</div>
|
</div>
|
||||||
<input type="hidden" id="agent-mode-select" value="react" autocomplete="off">
|
<input type="hidden" id="agent-mode-select" value="react" autocomplete="off">
|
||||||
</div>
|
</div>
|
||||||
|
<div id="chat-reasoning-wrapper" class="chat-reasoning-wrapper" style="display: none;">
|
||||||
|
<div class="chat-reasoning-inner">
|
||||||
|
<button type="button" id="chat-reasoning-btn" class="role-selector-btn chat-reasoning-btn" onclick="toggleChatReasoningPanel()" aria-expanded="false" aria-haspopup="dialog" aria-controls="chat-reasoning-panel" data-i18n="chat.reasoningCompactAria" data-i18n-attr="aria-label,title" data-i18n-skip-text="true" aria-label="模型推理选项" title="模型推理选项">
|
||||||
|
<span class="chat-reasoning-btn-icon" aria-hidden="true">🔎</span>
|
||||||
|
<span id="chat-reasoning-summary" class="role-selector-text chat-reasoning-btn-summary"></span>
|
||||||
|
<svg class="role-selector-arrow" width="10" height="10" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||||
|
<path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div id="chat-reasoning-panel" class="chat-reasoning-panel" style="display: none;" role="dialog" aria-labelledby="chat-reasoning-panel-title">
|
||||||
|
<div class="role-selection-panel-header chat-reasoning-panel-header">
|
||||||
|
<h3 id="chat-reasoning-panel-title" class="role-selection-panel-title" data-i18n="chat.reasoningPanelTitle">模型推理</h3>
|
||||||
|
<button type="button" class="role-selection-panel-close" onclick="closeChatReasoningPanel()" data-i18n="common.close" data-i18n-attr="title" title="关闭">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="chat-reasoning-panel-hint" data-i18n="chat.reasoningPanelHint">仅 Eino 请求生效,与系统设置中的默认值合并。</p>
|
||||||
|
<div class="chat-reasoning-fields">
|
||||||
|
<div class="chat-reasoning-field">
|
||||||
|
<label class="chat-reasoning-field-label" for="chat-reasoning-mode"><span data-i18n="chat.reasoningModeLabel">模式</span></label>
|
||||||
|
<select id="chat-reasoning-mode" class="chat-reasoning-select" onchange="persistChatReasoningPrefs()">
|
||||||
|
<option value="default" data-i18n="chat.reasoningModeDefault">跟随系统</option>
|
||||||
|
<option value="off" data-i18n="chat.reasoningModeOff">关闭</option>
|
||||||
|
<option value="on" data-i18n="chat.reasoningModeOn">开启</option>
|
||||||
|
<option value="auto" data-i18n="chat.reasoningModeAuto">自动</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="chat-reasoning-field">
|
||||||
|
<label class="chat-reasoning-field-label" for="chat-reasoning-effort"><span data-i18n="chat.reasoningEffortLabel">推理强度</span></label>
|
||||||
|
<select id="chat-reasoning-effort" class="chat-reasoning-select" onchange="persistChatReasoningPrefs()">
|
||||||
|
<option value="" data-i18n="chat.reasoningEffortUnset">不指定</option>
|
||||||
|
<option value="low">low</option>
|
||||||
|
<option value="medium">medium</option>
|
||||||
|
<option value="high">high</option>
|
||||||
|
<option value="max">max</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="chat-input-with-files">
|
<div class="chat-input-with-files">
|
||||||
<div id="chat-file-list" class="chat-file-list" aria-label="已选文件列表"></div>
|
<div id="chat-file-list" class="chat-file-list" aria-label="已选文件列表"></div>
|
||||||
<div id="chat-attachment-progress" class="chat-upload-progress-row" hidden role="status" aria-live="polite">
|
<div id="chat-attachment-progress" class="chat-upload-progress-row" hidden role="status" aria-live="polite">
|
||||||
@@ -1002,6 +1048,7 @@
|
|||||||
<path d="M5 12h14M12 5l7 7-7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M5 12h14M12 5l7 7-7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1989,6 +2036,38 @@
|
|||||||
<input type="number" id="openai-max-total-tokens" data-i18n="settingsBasic.maxTotalTokensPlaceholder" data-i18n-attr="placeholder" placeholder="120000" min="1000" step="1000" />
|
<input type="number" id="openai-max-total-tokens" data-i18n="settingsBasic.maxTotalTokensPlaceholder" data-i18n-attr="placeholder" placeholder="120000" min="1000" step="1000" />
|
||||||
<small style="color: var(--text-muted, #718096); font-size: 0.75rem;" data-i18n="settingsBasic.maxTotalTokensHint">内存压缩和攻击链构建共用此配置,默认 120000</small>
|
<small style="color: var(--text-muted, #718096); font-size: 0.75rem;" data-i18n="settingsBasic.maxTotalTokensHint">内存压缩和攻击链构建共用此配置,默认 120000</small>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label data-i18n="settingsBasic.openaiReasoningTitle">模型推理(Eino)</label>
|
||||||
|
<small class="form-hint" data-i18n="settingsBasic.openaiReasoningHint">仅影响 Eino 单代理与多代理;对话页可覆盖(见下方「允许对话覆盖」)。</small>
|
||||||
|
<div style="display: flex; flex-wrap: wrap; gap: 10px; margin-top: 8px; align-items: center;">
|
||||||
|
<label for="openai-reasoning-mode" style="font-size: 0.8125rem;" data-i18n="chat.reasoningModeLabel">模式</label>
|
||||||
|
<select id="openai-reasoning-mode" style="min-width: 120px; padding: 0.35rem 0.5rem; border-radius: 6px; border: 1px solid var(--border-color, #e2e8f0);">
|
||||||
|
<option value="auto" data-i18n="chat.reasoningModeAuto">自动</option>
|
||||||
|
<option value="on" data-i18n="chat.reasoningModeOn">开启</option>
|
||||||
|
<option value="off" data-i18n="chat.reasoningModeOff">关闭</option>
|
||||||
|
</select>
|
||||||
|
<label for="openai-reasoning-effort" style="font-size: 0.8125rem;" data-i18n="chat.reasoningEffortLabel">强度</label>
|
||||||
|
<select id="openai-reasoning-effort" style="min-width: 100px; padding: 0.35rem 0.5rem; border-radius: 6px; border: 1px solid var(--border-color, #e2e8f0);">
|
||||||
|
<option value="" data-i18n="chat.reasoningEffortUnset">不指定</option>
|
||||||
|
<option value="low">low</option>
|
||||||
|
<option value="medium">medium</option>
|
||||||
|
<option value="high">high</option>
|
||||||
|
<option value="max">max</option>
|
||||||
|
</select>
|
||||||
|
<label for="openai-reasoning-profile" style="font-size: 0.8125rem;" data-i18n="settingsBasic.openaiReasoningProfile">线路</label>
|
||||||
|
<select id="openai-reasoning-profile" style="min-width: 140px; padding: 0.35rem 0.5rem; border-radius: 6px; border: 1px solid var(--border-color, #e2e8f0);">
|
||||||
|
<option value="auto">auto</option>
|
||||||
|
<option value="deepseek_compat">deepseek_compat</option>
|
||||||
|
<option value="openai_compat">openai_compat</option>
|
||||||
|
<option value="output_config_effort">output_config_effort</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<label class="checkbox-label" style="margin-top: 8px;">
|
||||||
|
<input type="checkbox" id="openai-reasoning-allow-client" class="modern-checkbox" checked />
|
||||||
|
<span class="checkbox-custom"></span>
|
||||||
|
<span class="checkbox-text" data-i18n="settingsBasic.openaiReasoningAllowClient">允许对话页覆盖推理选项</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div style="display: flex; align-items: center; gap: 8px; margin-top: 2px;">
|
<div style="display: flex; align-items: center; gap: 8px; margin-top: 2px;">
|
||||||
<a href="javascript:void(0)" id="test-openai-btn" onclick="testOpenAIConnection()" style="font-size: 0.8125rem; color: var(--accent-color, #3182ce); text-decoration: none; cursor: pointer; user-select: none;" data-i18n="settingsBasic.testConnection">测试连接</a>
|
<a href="javascript:void(0)" id="test-openai-btn" onclick="testOpenAIConnection()" style="font-size: 0.8125rem; color: var(--accent-color, #3182ce); text-decoration: none; cursor: pointer; user-select: none;" data-i18n="settingsBasic.testConnection">测试连接</a>
|
||||||
<span id="test-openai-result" style="font-size: 0.8125rem;"></span>
|
<span id="test-openai-result" style="font-size: 0.8125rem;"></span>
|
||||||
|
|||||||
Reference in New Issue
Block a user