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:
|
||||
host: 0.0.0.0 # 监听地址,0.0.0.0 表示监听所有网络接口
|
||||
@@ -41,6 +41,13 @@ openai:
|
||||
api_key: sk-xxxxxxx # API 密钥(必填)
|
||||
model: qwen3-max # 模型名称(必填)
|
||||
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)配置(可选)
|
||||
# ============================================
|
||||
|
||||
@@ -195,6 +195,8 @@ type ChatMessage struct {
|
||||
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||
// ToolName 仅 tool 角色:从 Eino/轨迹 JSON 的 name 或 tool_name 恢复,供续跑构造 ToolMessage。
|
||||
ToolName string `json:"tool_name,omitempty"`
|
||||
// ReasoningContent 对应 OpenAI/DeepSeek 的 reasoning_content;思考模式 + 工具调用后续跑须回传(见 DeepSeek 文档)。
|
||||
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||||
}
|
||||
|
||||
// MarshalJSON 自定义JSON序列化,将tool_calls中的arguments转换为JSON字符串
|
||||
@@ -208,6 +210,9 @@ func (cm ChatMessage) MarshalJSON() ([]byte, error) {
|
||||
if cm.Content != "" {
|
||||
aux["content"] = cm.Content
|
||||
}
|
||||
if cm.ReasoningContent != "" {
|
||||
aux["reasoning_content"] = cm.ReasoningContent
|
||||
}
|
||||
|
||||
// 添加tool_call_id(如果存在)
|
||||
if cm.ToolCallID != "" {
|
||||
@@ -663,8 +668,8 @@ func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, his
|
||||
|
||||
// 检查是否有工具调用
|
||||
if len(choice.Message.ToolCalls) > 0 {
|
||||
// 思考内容:如果本轮启用了思考流式增量(thinking_stream_*),前端会去重;
|
||||
// 同时也需要在该“思考阶段结束”时补一条可落库的 thinking(用于刷新后持久化展示)。
|
||||
// ReAct 助手正文流式增量(thinking_stream_*)在 UI 上归为「思考」;若与 streamId 重复则前端会去重。
|
||||
// 该条 thinking 用于刷新后持久化展示(与流式聚合一致)。
|
||||
if choice.Message.Content != "" {
|
||||
sendProgress("thinking", choice.Message.Content, map[string]interface{}{
|
||||
"iteration": i + 1,
|
||||
|
||||
@@ -301,7 +301,7 @@ func (b *Builder) formatProcessDetailsForAttackChain(details []database.ProcessD
|
||||
// 目标:以主 agent(编排器)视角输出整轮迭代
|
||||
// - 保留:编排器工具调用/结果、对子代理的 task 调度、子代理最终回复(不含推理)
|
||||
// - 丢弃: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
|
||||
}
|
||||
|
||||
|
||||
@@ -346,6 +346,48 @@ type OpenAIConfig struct {
|
||||
BaseURL string `yaml:"base_url" json:"base_url"`
|
||||
Model string `yaml:"model" json:"model"`
|
||||
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 {
|
||||
|
||||
@@ -25,14 +25,15 @@ type Conversation struct {
|
||||
|
||||
// Message 消息
|
||||
type Message struct {
|
||||
ID string `json:"id"`
|
||||
ConversationID string `json:"conversationId"`
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
MCPExecutionIDs []string `json:"mcpExecutionIds,omitempty"`
|
||||
ProcessDetails []map[string]interface{} `json:"processDetails,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
ID string `json:"id"`
|
||||
ConversationID string `json:"conversationId"`
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
ReasoningContent string `json:"reasoningContent,omitempty"`
|
||||
MCPExecutionIDs []string `json:"mcpExecutionIds,omitempty"`
|
||||
ProcessDetails []map[string]interface{} `json:"processDetails,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// CreateConversation 创建新对话
|
||||
@@ -498,8 +499,8 @@ func (db *DB) AddMessage(conversationID, role, content string, mcpExecutionIDs [
|
||||
}
|
||||
|
||||
_, err := db.Exec(
|
||||
"INSERT INTO messages (id, conversation_id, role, content, mcp_execution_ids, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
id, conversationID, role, content, mcpIDsJSON, now, now,
|
||||
"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,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("添加消息失败: %w", err)
|
||||
@@ -523,10 +524,30 @@ func (db *DB) AddMessage(conversationID, role, content string, mcpExecutionIDs [
|
||||
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 获取对话的所有消息
|
||||
func (db *DB) GetMessages(conversationID string) ([]Message, error) {
|
||||
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,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -537,13 +558,17 @@ func (db *DB) GetMessages(conversationID string) ([]Message, error) {
|
||||
var messages []Message
|
||||
for rows.Next() {
|
||||
var msg Message
|
||||
var reasoning sql.NullString
|
||||
var mcpIDsJSON sql.NullString
|
||||
var createdAt string
|
||||
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)
|
||||
}
|
||||
if reasoning.Valid {
|
||||
msg.ReasoningContent = reasoning.String
|
||||
}
|
||||
|
||||
// 尝试多种时间格式解析
|
||||
var err error
|
||||
@@ -683,7 +708,7 @@ type ProcessDetail struct {
|
||||
ID string `json:"id"`
|
||||
MessageID string `json:"messageId"`
|
||||
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"`
|
||||
Data string `json:"data"` // JSON格式的数据
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
|
||||
@@ -594,6 +594,25 @@ func (db *DB) migrateMessagesTable() error {
|
||||
|
||||
// 回填已有数据:让 updated_at 至少等于 created_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
|
||||
}
|
||||
|
||||
|
||||
+81
-81
@@ -19,6 +19,7 @@ import (
|
||||
"cyberstrike-ai/internal/agent"
|
||||
"cyberstrike-ai/internal/config"
|
||||
"cyberstrike-ai/internal/database"
|
||||
"cyberstrike-ai/internal/reasoning"
|
||||
"cyberstrike-ai/internal/mcp"
|
||||
"cyberstrike-ai/internal/mcp/builtin"
|
||||
"cyberstrike-ai/internal/multiagent"
|
||||
@@ -201,6 +202,14 @@ type ChatAttachment struct {
|
||||
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 聊天请求
|
||||
type ChatRequest struct {
|
||||
Message string `json:"message" binding:"required"`
|
||||
@@ -209,10 +218,18 @@ type ChatRequest struct {
|
||||
Attachments []ChatAttachment `json:"attachments,omitempty"`
|
||||
WebShellConnectionID string `json:"webshellConnectionId,omitempty"` // WebShell 管理 - AI 助手:当前选中的连接 ID,仅使用 webshell_* 工具
|
||||
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 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 {
|
||||
Enabled bool `json:"enabled"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
@@ -567,14 +584,7 @@ func (h *AgentHandler) AgentLoop(c *gin.Context) {
|
||||
h.logger.Warn("获取历史消息失败", zap.Error(err))
|
||||
agentHistoryMessages = []agent.ChatMessage{}
|
||||
} else {
|
||||
// 将数据库消息转换为Agent消息格式
|
||||
agentHistoryMessages = make([]agent.ChatMessage, 0, len(historyMessages))
|
||||
for _, msg := range historyMessages {
|
||||
agentHistoryMessages = append(agentHistoryMessages, agent.ChatMessage{
|
||||
Role: msg.Role,
|
||||
Content: msg.Content,
|
||||
})
|
||||
}
|
||||
agentHistoryMessages = dbMessagesToAgentChatMessages(historyMessages)
|
||||
h.logger.Info("从消息表加载历史消息", zap.Int("count", len(agentHistoryMessages)))
|
||||
}
|
||||
} else {
|
||||
@@ -775,6 +785,7 @@ func (h *AgentHandler) ProcessMessageForRobot(ctx context.Context, conversationI
|
||||
progressCallback,
|
||||
h.agentsMarkdownDir,
|
||||
"deep",
|
||||
nil,
|
||||
)
|
||||
if errMA != nil {
|
||||
if shouldPersistEinoAgentTraceAfterRunError(ctx) {
|
||||
@@ -788,17 +799,8 @@ func (h *AgentHandler) ProcessMessageForRobot(ctx context.Context, conversationI
|
||||
return "", conversationID, errMA
|
||||
}
|
||||
if assistantMessageID != "" {
|
||||
mcpIDsJSON := ""
|
||||
if len(resultMA.MCPExecutionIDs) > 0 {
|
||||
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))
|
||||
if errU := h.db.UpdateAssistantMessageFinalize(assistantMessageID, resultMA.Response, resultMA.MCPExecutionIDs, multiagent.AggregatedReasoningFromTraceJSON(resultMA.LastAgentTraceInput)); errU != nil {
|
||||
h.logger.Warn("机器人:更新助手消息失败", zap.Error(errU))
|
||||
}
|
||||
} else {
|
||||
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 一致)
|
||||
if assistantMessageID != "" {
|
||||
mcpIDsJSON := ""
|
||||
if len(result.MCPExecutionIDs) > 0 {
|
||||
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))
|
||||
if errU := h.db.UpdateAssistantMessageFinalize(assistantMessageID, result.Response, result.MCPExecutionIDs, multiagent.AggregatedReasoningFromTraceJSON(result.LastAgentTraceInput)); errU != nil {
|
||||
h.logger.Warn("机器人:更新助手消息失败", zap.Error(errU))
|
||||
}
|
||||
} else {
|
||||
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 ""
|
||||
}
|
||||
|
||||
// thinking_stream_*:不逐条落库,按 streamId 聚合,在后续关键事件前补一条可持久化的 thinking
|
||||
// thinking_stream_*(ReAct 等助手正文流)与 reasoning_chain_stream_*(Eino ReasoningContent):
|
||||
// 不逐条落库,按 streamId 聚合,flush 时分别落 thinking / reasoning_chain。
|
||||
type thinkingBuf struct {
|
||||
b strings.Builder
|
||||
meta map[string]interface{}
|
||||
b strings.Builder
|
||||
meta map[string]interface{}
|
||||
persistAs string // "thinking" | "reasoning_chain"
|
||||
}
|
||||
thinkingStreams := make(map[string]*thinkingBuf) // streamId -> buf
|
||||
flushedThinking := make(map[string]bool) // streamId -> flushed
|
||||
@@ -948,8 +943,12 @@ func (h *AgentHandler) createProgressCallback(runCtx context.Context, cancelRun
|
||||
}
|
||||
data[k] = v
|
||||
}
|
||||
if err := h.db.AddProcessDetail(assistantMessageID, conversationID, "thinking", content, data); err != nil {
|
||||
h.logger.Warn("保存过程详情失败", zap.Error(err), zap.String("eventType", "thinking"))
|
||||
persist := tb.persistAs
|
||||
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
|
||||
}
|
||||
@@ -1177,14 +1176,20 @@ func (h *AgentHandler) createProgressCallback(runCtx context.Context, cancelRun
|
||||
return
|
||||
}
|
||||
|
||||
// 聚合 thinking_stream_*(ReasoningContent),不逐条落库
|
||||
if eventType == "thinking_stream_start" {
|
||||
// 聚合 thinking_stream_* / reasoning_chain_stream_*,不逐条落库
|
||||
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 sid, ok2 := dataMap["streamId"].(string); ok2 && sid != "" {
|
||||
tb := thinkingStreams[sid]
|
||||
if tb == nil {
|
||||
tb = &thinkingBuf{meta: map[string]interface{}{}}
|
||||
tb = &thinkingBuf{meta: map[string]interface{}{}, persistAs: persistAs}
|
||||
thinkingStreams[sid] = tb
|
||||
} else {
|
||||
tb.persistAs = persistAs
|
||||
}
|
||||
// 记录元信息(source/einoAgent/einoRole/iteration 等)
|
||||
for k, v := range dataMap {
|
||||
@@ -1194,15 +1199,21 @@ func (h *AgentHandler) createProgressCallback(runCtx context.Context, cancelRun
|
||||
}
|
||||
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 sid, ok2 := dataMap["streamId"].(string); ok2 && sid != "" {
|
||||
tb := thinkingStreams[sid]
|
||||
if tb == nil {
|
||||
tb = &thinkingBuf{meta: map[string]interface{}{}}
|
||||
tb = &thinkingBuf{meta: map[string]interface{}{}, persistAs: persistAs}
|
||||
thinkingStreams[sid] = tb
|
||||
} else if tb.persistAs == "" {
|
||||
tb.persistAs = persistAs
|
||||
}
|
||||
// delta 片段直接拼接;message 本身就是 reasoning content
|
||||
// delta 片段直接拼接
|
||||
tb.b.WriteString(message)
|
||||
// 有时 delta 先到 start 未到,补充元信息
|
||||
for k, v := range dataMap {
|
||||
@@ -1213,10 +1224,9 @@ func (h *AgentHandler) createProgressCallback(runCtx context.Context, cancelRun
|
||||
return
|
||||
}
|
||||
|
||||
// 当 Agent 同时发送 thinking_stream_* 和 thinking(带同一 streamId)时,
|
||||
// thinking_stream_* 已经会在 flushThinkingStreams() 聚合落库;
|
||||
// 这里跳过同 streamId 的 thinking,避免 processDetails 双份展示。
|
||||
if eventType == "thinking" {
|
||||
// 当 Agent 同时发送 *_stream_* 与同名 streamId 的 thinking/reasoning_chain 时,
|
||||
// 流式聚合已会在 flushThinkingStreams() 落库;此处跳过逐条重复。
|
||||
if eventType == "thinking" || eventType == "reasoning_chain" {
|
||||
if dataMap, ok := data.(map[string]interface{}); ok {
|
||||
if sid, ok2 := dataMap["streamId"].(string); ok2 && sid != "" {
|
||||
if tb, exists := thinkingStreams[sid]; exists && tb != nil {
|
||||
@@ -1245,7 +1255,7 @@ func (h *AgentHandler) createProgressCallback(runCtx context.Context, cancelRun
|
||||
if eventType == "tool_result" {
|
||||
discardPlanningIfEchoesToolResult(&respPlan, data)
|
||||
}
|
||||
// 在关键过程事件落库前,先把「规划中」与 thinking_stream 落库
|
||||
// 在关键过程事件落库前,先把「规划中」与聚合中的 thinking / reasoning_chain 流落库
|
||||
flushResponsePlan()
|
||||
flushThinkingStreams()
|
||||
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))
|
||||
agentHistoryMessages = []agent.ChatMessage{}
|
||||
} else {
|
||||
// 将数据库消息转换为Agent消息格式
|
||||
agentHistoryMessages = make([]agent.ChatMessage, 0, len(historyMessages))
|
||||
for _, msg := range historyMessages {
|
||||
agentHistoryMessages = append(agentHistoryMessages, agent.ChatMessage{
|
||||
Role: msg.Role,
|
||||
Content: msg.Content,
|
||||
})
|
||||
}
|
||||
agentHistoryMessages = dbMessagesToAgentChatMessages(historyMessages)
|
||||
h.logger.Info("从消息表加载历史消息", zap.Int("count", len(agentHistoryMessages)))
|
||||
}
|
||||
} else {
|
||||
@@ -1727,20 +1730,8 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
|
||||
|
||||
// 更新助手消息内容
|
||||
if assistantMsg != nil {
|
||||
_, err = h.db.Exec(
|
||||
"UPDATE messages SET content = ?, mcp_execution_ids = ?, updated_at = ? WHERE id = ?",
|
||||
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))
|
||||
if errU := h.db.UpdateAssistantMessageFinalize(assistantMessageID, result.Response, result.MCPExecutionIDs, multiagent.AggregatedReasoningFromTraceJSON(result.LastAgentTraceInput)); errU != nil {
|
||||
h.logger.Error("更新助手消息失败", zap.Error(errU))
|
||||
}
|
||||
} else {
|
||||
// 如果之前创建失败,现在创建
|
||||
@@ -2664,12 +2655,12 @@ func (h *AgentHandler) executeBatchQueue(queueID string) {
|
||||
var runErr error
|
||||
switch {
|
||||
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:
|
||||
if h.config == nil {
|
||||
runErr = fmt.Errorf("服务器配置未加载")
|
||||
} 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:
|
||||
result, runErr = h.agent.AgentLoopWithProgress(taskCtx, finalMessage, []agent.ChatMessage{}, conversationID, progressCallback, roleTools)
|
||||
@@ -2768,17 +2759,7 @@ func (h *AgentHandler) executeBatchQueue(queueID string) {
|
||||
|
||||
// 更新助手消息内容
|
||||
if assistantMessageID != "" {
|
||||
mcpIDsJSON := ""
|
||||
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 {
|
||||
if updateErr := h.db.UpdateAssistantMessageFinalize(assistantMessageID, resText, mcpIDs, multiagent.AggregatedReasoningFromTraceJSON(lastIn)); updateErr != nil {
|
||||
h.logger.Warn("更新助手消息失败", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.Error(updateErr))
|
||||
// 如果更新失败,尝试创建新消息
|
||||
_, 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 {
|
||||
msg.Content = content
|
||||
}
|
||||
// DeepSeek 思考模式:含工具调用的 assistant 须在后续请求中回传 reasoning_content
|
||||
if rc, ok := msgMap["reasoning_content"].(string); ok && strings.TrimSpace(rc) != "" {
|
||||
msg.ReasoningContent = rc
|
||||
}
|
||||
|
||||
// 解析tool_calls(如果存在)
|
||||
if toolCallsRaw, ok := msgMap["tool_calls"]; ok && toolCallsRaw != nil {
|
||||
@@ -2975,3 +2960,18 @@ func (h *AgentHandler) loadHistoryFromAgentTrace(conversationID string) ([]agent
|
||||
)
|
||||
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 {
|
||||
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) {
|
||||
|
||||
@@ -196,6 +196,7 @@ func (h *AgentHandler) EinoSingleAgentLoopStream(c *gin.Context) {
|
||||
curHistory,
|
||||
roleTools,
|
||||
progressCallback,
|
||||
chatReasoningToClientIntent(req.Reasoning),
|
||||
)
|
||||
timeoutCancel()
|
||||
|
||||
@@ -297,18 +298,7 @@ func (h *AgentHandler) EinoSingleAgentLoopStream(c *gin.Context) {
|
||||
}
|
||||
|
||||
if assistantMessageID != "" {
|
||||
mcpIDsJSON := ""
|
||||
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,
|
||||
)
|
||||
_ = h.db.UpdateAssistantMessageFinalize(assistantMessageID, result.Response, cumulativeMCPExecutionIDs, multiagent.AggregatedReasoningFromTraceJSON(result.LastAgentTraceInput))
|
||||
}
|
||||
|
||||
if result.LastAgentTraceInput != "" || result.LastAgentTraceOutput != "" {
|
||||
@@ -376,6 +366,7 @@ func (h *AgentHandler) EinoSingleAgentLoop(c *gin.Context) {
|
||||
prep.History,
|
||||
prep.RoleTools,
|
||||
progressCallback,
|
||||
chatReasoningToClientIntent(req.Reasoning),
|
||||
)
|
||||
if runErr != nil {
|
||||
if shouldPersistEinoAgentTraceAfterRunError(baseCtx) {
|
||||
@@ -386,18 +377,7 @@ func (h *AgentHandler) EinoSingleAgentLoop(c *gin.Context) {
|
||||
}
|
||||
|
||||
if prep.AssistantMessageID != "" {
|
||||
mcpIDsJSON := ""
|
||||
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,
|
||||
)
|
||||
_ = h.db.UpdateAssistantMessageFinalize(prep.AssistantMessageID, result.Response, result.MCPExecutionIDs, multiagent.AggregatedReasoningFromTraceJSON(result.LastAgentTraceInput))
|
||||
}
|
||||
if 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,
|
||||
h.agentsMarkdownDir,
|
||||
orch,
|
||||
chatReasoningToClientIntent(req.Reasoning),
|
||||
)
|
||||
timeoutCancel()
|
||||
|
||||
@@ -309,18 +310,7 @@ func (h *AgentHandler) MultiAgentLoopStream(c *gin.Context) {
|
||||
}
|
||||
|
||||
if assistantMessageID != "" {
|
||||
mcpIDsJSON := ""
|
||||
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,
|
||||
)
|
||||
_ = h.db.UpdateAssistantMessageFinalize(assistantMessageID, result.Response, cumulativeMCPExecutionIDs, multiagent.AggregatedReasoningFromTraceJSON(result.LastAgentTraceInput))
|
||||
}
|
||||
|
||||
if result.LastAgentTraceInput != "" || result.LastAgentTraceOutput != "" {
|
||||
@@ -390,6 +380,7 @@ func (h *AgentHandler) MultiAgentLoop(c *gin.Context) {
|
||||
progressCallback,
|
||||
h.agentsMarkdownDir,
|
||||
strings.TrimSpace(req.Orchestration),
|
||||
chatReasoningToClientIntent(req.Reasoning),
|
||||
)
|
||||
if runErr != nil {
|
||||
if shouldPersistEinoAgentTraceAfterRunError(baseCtx) {
|
||||
@@ -405,18 +396,7 @@ func (h *AgentHandler) MultiAgentLoop(c *gin.Context) {
|
||||
}
|
||||
|
||||
if prep.AssistantMessageID != "" {
|
||||
mcpIDsJSON := ""
|
||||
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,
|
||||
)
|
||||
_ = h.db.UpdateAssistantMessageFinalize(prep.AssistantMessageID, result.Response, result.MCPExecutionIDs, multiagent.AggregatedReasoningFromTraceJSON(result.LastAgentTraceInput))
|
||||
}
|
||||
|
||||
if result.LastAgentTraceInput != "" || result.LastAgentTraceOutput != "" {
|
||||
|
||||
@@ -55,13 +55,7 @@ func (h *AgentHandler) prepareMultiAgentSession(req *ChatRequest) (*multiAgentPr
|
||||
if getErr != nil {
|
||||
agentHistoryMessages = []agent.ChatMessage{}
|
||||
} else {
|
||||
agentHistoryMessages = make([]agent.ChatMessage, 0, len(historyMessages))
|
||||
for _, msg := range historyMessages {
|
||||
agentHistoryMessages = append(agentHistoryMessages, agent.ChatMessage{
|
||||
Role: msg.Role,
|
||||
Content: msg.Content,
|
||||
})
|
||||
}
|
||||
agentHistoryMessages = dbMessagesToAgentChatMessages(historyMessages)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
"cyberstrike-ai/internal/agent"
|
||||
"cyberstrike-ai/internal/einomcp"
|
||||
"cyberstrike-ai/internal/openai"
|
||||
|
||||
"github.com/cloudwego/eino/adk"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
@@ -550,6 +551,7 @@ func runEinoADKAgentLoop(ctx context.Context, args *einoADKRunLoopArgs, baseMsgs
|
||||
var mainAssistantBuf string
|
||||
var mainAssistDupTarget string // 非空表示本段主助手流需缓冲至 EOF,与 execute 输出比对去重
|
||||
var reasoningBuf string
|
||||
var prevReasoningDisplay string // UI 用:剥离 Claude 内部 signature 尾缀后的累计展示
|
||||
var streamRecvErr error
|
||||
type streamMsg struct {
|
||||
chunk *schema.Message
|
||||
@@ -597,19 +599,29 @@ func runEinoADKAgentLoop(ctx context.Context, args *einoADKRunLoopArgs, baseMsgs
|
||||
var reasoningDelta string
|
||||
reasoningBuf, reasoningDelta = normalizeStreamingDelta(reasoningBuf, chunk.ReasoningContent)
|
||||
if reasoningDelta != "" {
|
||||
if reasoningStreamID == "" {
|
||||
reasoningStreamID = fmt.Sprintf("eino-reasoning-%s-%d", conversationID, atomic.AddInt64(&reasoningStreamSeq, 1))
|
||||
progress("thinking_stream_start", " ", map[string]interface{}{
|
||||
"streamId": reasoningStreamID,
|
||||
"source": "eino",
|
||||
"einoAgent": ev.AgentName,
|
||||
"einoRole": einoRoleTag(ev.AgentName),
|
||||
"orchestration": orchMode,
|
||||
fullDisplay := openai.DisplayReasoningContent(reasoningBuf)
|
||||
var displayDelta string
|
||||
if strings.HasPrefix(fullDisplay, prevReasoningDisplay) {
|
||||
displayDelta = fullDisplay[len(prevReasoningDisplay):]
|
||||
} else {
|
||||
displayDelta = fullDisplay
|
||||
}
|
||||
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 != "" {
|
||||
@@ -777,7 +789,7 @@ func runEinoADKAgentLoop(ctx context.Context, args *einoADKRunLoopArgs, baseMsgs
|
||||
|
||||
if mv.Role == schema.Assistant {
|
||||
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,
|
||||
"source": "eino",
|
||||
"einoAgent": ev.AgentName,
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"cyberstrike-ai/internal/config"
|
||||
"cyberstrike-ai/internal/einomcp"
|
||||
"cyberstrike-ai/internal/openai"
|
||||
"cyberstrike-ai/internal/reasoning"
|
||||
|
||||
einoopenai "github.com/cloudwego/eino-ext/components/model/openai"
|
||||
"github.com/cloudwego/eino/adk"
|
||||
@@ -37,6 +38,7 @@ func RunEinoSingleChatModelAgent(
|
||||
history []agent.ChatMessage,
|
||||
roleTools []string,
|
||||
progress func(eventType, message string, data interface{}),
|
||||
reasoningClient *reasoning.ClientIntent,
|
||||
) (*RunResult, error) {
|
||||
if appCfg == nil || ag == nil {
|
||||
return nil, fmt.Errorf("eino single: 配置或 Agent 为空")
|
||||
@@ -121,6 +123,7 @@ func RunEinoSingleChatModelAgent(
|
||||
Model: appCfg.OpenAI.Model,
|
||||
HTTPClient: httpClient,
|
||||
}
|
||||
reasoning.ApplyToEinoChatModelConfig(baseModelCfg, &appCfg.OpenAI, reasoningClient)
|
||||
|
||||
mainModel, err := einoopenai.NewChatModel(ctx, baseModelCfg)
|
||||
if err != nil {
|
||||
|
||||
@@ -214,7 +214,7 @@ func summarizeFinalizeWithRecentAssistantToolTrail(
|
||||
selectedCount++
|
||||
}
|
||||
|
||||
// 还原时间顺序
|
||||
// 还原时间顺序。round 内为原始 *schema.Message 指针,保留 ReasoningContent(DeepSeek 工具续跑所必需)。
|
||||
selectedMsgs := make([]adk.Message, 0, 8)
|
||||
for i := len(selectedRoundsReverse) - 1; i >= 0; i-- {
|
||||
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/einomcp"
|
||||
"cyberstrike-ai/internal/openai"
|
||||
"cyberstrike-ai/internal/reasoning"
|
||||
|
||||
einoopenai "github.com/cloudwego/eino-ext/components/model/openai"
|
||||
"github.com/cloudwego/eino/adk"
|
||||
@@ -48,6 +49,7 @@ type toolCallPendingInfo struct {
|
||||
|
||||
// RunDeepAgent 使用 Eino 多代理预置编排执行一轮对话(deep / plan_execute / supervisor;流式事件通过 progress 回调输出)。
|
||||
// orchestrationOverride 非空时优先(如聊天/WebShell 请求体);否则用 multi_agent.orchestration(遗留 yaml);皆空则按 deep。
|
||||
// reasoningClient 来自 ChatRequest.reasoning;可为 nil(机器人/批量等走全局 openai.reasoning)。
|
||||
func RunDeepAgent(
|
||||
ctx context.Context,
|
||||
appCfg *config.Config,
|
||||
@@ -61,6 +63,7 @@ func RunDeepAgent(
|
||||
progress func(eventType, message string, data interface{}),
|
||||
agentsMarkdownDir string,
|
||||
orchestrationOverride string,
|
||||
reasoningClient *reasoning.ClientIntent,
|
||||
) (*RunResult, error) {
|
||||
if appCfg == nil || ma == nil || ag == nil {
|
||||
return nil, fmt.Errorf("multiagent: 配置或 Agent 为空")
|
||||
@@ -163,6 +166,7 @@ func RunDeepAgent(
|
||||
Model: appCfg.OpenAI.Model,
|
||||
HTTPClient: httpClient,
|
||||
}
|
||||
reasoning.ApplyToEinoChatModelConfig(baseModelCfg, &appCfg.OpenAI, reasoningClient)
|
||||
|
||||
deepMaxIter := ma.MaxIteration
|
||||
if deepMaxIter <= 0 {
|
||||
@@ -636,8 +640,13 @@ func historyToMessages(history []agent.ChatMessage, appCfg *config.Config, mwCfg
|
||||
}
|
||||
case "assistant":
|
||||
toolSchema := chatToolCallsToSchema(h.ToolCalls)
|
||||
if len(toolSchema) > 0 || strings.TrimSpace(h.Content) != "" {
|
||||
raw = append(raw, schema.AssistantMessage(h.Content, toolSchema))
|
||||
hasRC := strings.TrimSpace(h.ReasoningContent) != ""
|
||||
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":
|
||||
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 格式
|
||||
// Auth: Bearer → x-api-key
|
||||
// Tools: OpenAI tools[] → Claude tools[] (input_schema)
|
||||
//
|
||||
// Extended thinking: 顶层 `thinking` 从 OpenAI 请求体透传;响应中 `thinking` block 映射为
|
||||
// `reasoning_content`(可读前缀 + 内部 JSON 尾缀以保留 signature,供多轮工具续跑;UI 用 openai.DisplayReasoningContent 剥离)。
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -38,6 +41,7 @@ type claudeRequest struct {
|
||||
Messages []claudeMessage `json:"messages"`
|
||||
Tools []claudeTool `json:"tools,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
Thinking json.RawMessage `json:"thinking,omitempty"`
|
||||
}
|
||||
|
||||
type claudeMessage struct {
|
||||
@@ -76,6 +80,10 @@ type claudeContentBlock struct {
|
||||
// text block
|
||||
Text string `json:"text,omitempty"`
|
||||
|
||||
// thinking block (extended thinking)
|
||||
Thinking string `json:"thinking,omitempty"`
|
||||
Signature string `json:"signature,omitempty"`
|
||||
|
||||
// tool_use block (assistant 返回)
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
@@ -176,7 +184,13 @@ func convertOpenAIToClaude(payload interface{}) (*claudeRequest, error) {
|
||||
|
||||
// tool_calls (assistant 消息中包含工具调用)
|
||||
if role == "assistant" {
|
||||
rc, _ := mm["reasoning_content"].(string)
|
||||
_, thinkingReplay := parseClaudeReasoningAssistantBlocks(rc)
|
||||
|
||||
var blocks []claudeContentBlock
|
||||
for _, tb := range thinkingReplay {
|
||||
blocks = append(blocks, tb)
|
||||
}
|
||||
if 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
|
||||
}
|
||||
|
||||
@@ -318,9 +339,12 @@ func claudeToOpenAIResponseJSON(claudeBody []byte) ([]byte, error) {
|
||||
|
||||
var textContent string
|
||||
var toolCalls []interface{}
|
||||
var thinkingBlocks []claudeContentBlock
|
||||
|
||||
for _, block := range cr.Content {
|
||||
switch block.Type {
|
||||
case "thinking":
|
||||
thinkingBlocks = append(thinkingBlocks, block)
|
||||
case "text":
|
||||
textContent += block.Text
|
||||
case "tool_use":
|
||||
@@ -344,6 +368,18 @@ func claudeToOpenAIResponseJSON(claudeBody []byte) ([]byte, error) {
|
||||
if len(toolCalls) > 0 {
|
||||
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{}{
|
||||
"index": 0,
|
||||
@@ -901,8 +937,16 @@ func (rt *claudeRoundTripper) RoundTrip(req *http.Request) (*http.Response, erro
|
||||
|
||||
reader := bufio.NewReader(resp.Body)
|
||||
blockToToolIndex := make(map[int]int)
|
||||
blockIndexToType := make(map[int]string)
|
||||
nextToolIndex := 0
|
||||
|
||||
type thinkingAcc struct {
|
||||
text strings.Builder
|
||||
sig strings.Builder
|
||||
}
|
||||
thinkingByIndex := make(map[int]*thinkingAcc)
|
||||
var finishedThinking []claudeContentBlock
|
||||
|
||||
for {
|
||||
line, readErr := reader.ReadString('\n')
|
||||
if readErr != nil {
|
||||
@@ -947,6 +991,11 @@ func (rt *claudeRoundTripper) RoundTrip(req *http.Request) (*http.Response, erro
|
||||
blockIdx := int(blockIdxFlt)
|
||||
cb, _ := event["content_block"].(map[string]interface{})
|
||||
bt, _ := cb["type"].(string)
|
||||
blockIndexToType[blockIdx] = bt
|
||||
|
||||
if bt == "thinking" {
|
||||
thinkingByIndex[blockIdx] = &thinkingAcc{}
|
||||
}
|
||||
|
||||
if bt == "tool_use" {
|
||||
id, _ := cb["id"].(string)
|
||||
@@ -986,7 +1035,35 @@ func (rt *claudeRoundTripper) RoundTrip(req *http.Request) (*http.Response, erro
|
||||
delta, _ := event["delta"].(map[string]interface{})
|
||||
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)
|
||||
oaiChunk := 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":
|
||||
d, _ := event["delta"].(map[string]interface{})
|
||||
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":
|
||||
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")
|
||||
pw.Close()
|
||||
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;
|
||||
}
|
||||
|
||||
.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;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -2399,7 +2510,8 @@ header {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.chat-input-container > .chat-input-field {
|
||||
.chat-input-container > .chat-input-field,
|
||||
.chat-input-primary-row .chat-input-field {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
@@ -3568,6 +3680,11 @@ header {
|
||||
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 {
|
||||
border-left-color: #ff9800;
|
||||
background: rgba(255, 152, 0, 0.05);
|
||||
@@ -12294,6 +12411,9 @@ header {
|
||||
.webshell-ai-process-block .webshell-ai-timeline-thinking {
|
||||
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_calls_detected {
|
||||
border-left-color: #ff9800;
|
||||
|
||||
@@ -277,6 +277,7 @@
|
||||
"planExecuteStreamPhase": "Phase output",
|
||||
"einoSubAgentStep": "Sub-agent {{agent}} · step {{n}}",
|
||||
"aiThinking": "AI thinking",
|
||||
"reasoningChain": "Reasoning process",
|
||||
"planning": "Planning",
|
||||
"assistantStreamPhase": "Assistant output",
|
||||
"toolCallsDetected": "Detected {{count}} tool call(s)",
|
||||
@@ -329,6 +330,19 @@
|
||||
"agentModeMulti": "Multi-agent",
|
||||
"agentModeSingleHint": "Single-model ReAct loop for chat and tool use",
|
||||
"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",
|
||||
"agentModeOrchSupervisor": "Supervisor",
|
||||
"hitlTitle": "Human-in-the-loop",
|
||||
@@ -1592,6 +1606,10 @@
|
||||
"maxTotalTokens": "Max Context Tokens",
|
||||
"maxTotalTokensPlaceholder": "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)",
|
||||
"fofaBaseUrlHint": "Leave empty for default.",
|
||||
"email": "Email",
|
||||
|
||||
@@ -266,6 +266,7 @@
|
||||
"planExecuteStreamPhase": "阶段输出",
|
||||
"einoSubAgentStep": "子代理 {{agent}} · 第 {{n}} 步",
|
||||
"aiThinking": "AI思考",
|
||||
"reasoningChain": "推理过程",
|
||||
"planning": "规划中",
|
||||
"assistantStreamPhase": "助手输出",
|
||||
"toolCallsDetected": "检测到 {{count}} 个工具调用",
|
||||
@@ -318,6 +319,19 @@
|
||||
"agentModeMulti": "多代理",
|
||||
"agentModeSingleHint": "单模型 ReAct 循环,适合常规对话与工具调用",
|
||||
"agentModeMultiHint": "Eino 预置编排(deep / plan_execute / supervisor),适合复杂任务",
|
||||
"reasoningModeLabel": "模型推理",
|
||||
"reasoningEffortLabel": "推理强度",
|
||||
"reasoningModeDefault": "跟随系统",
|
||||
"reasoningModeOff": "关闭",
|
||||
"reasoningModeOn": "开启",
|
||||
"reasoningModeAuto": "自动",
|
||||
"reasoningEffortUnset": "不指定",
|
||||
"reasoningCompactLabel": "推理",
|
||||
"reasoningCompactAria": "打开模型推理选项",
|
||||
"reasoningPanelTitle": "模型推理",
|
||||
"reasoningPanelHint": "仅 Eino 单代理与多代理请求会带上这些参数;与系统设置中的默认值合并。",
|
||||
"reasoningSummaryFollow": "系统",
|
||||
"reasoningSummaryDash": "—",
|
||||
"agentModeOrchPlanExecute": "Plan-Exec",
|
||||
"agentModeOrchSupervisor": "Supervisor",
|
||||
"hitlTitle": "人机协同",
|
||||
@@ -1581,6 +1595,10 @@
|
||||
"maxTotalTokens": "最大上下文 Token 数",
|
||||
"maxTotalTokensPlaceholder": "120000",
|
||||
"maxTotalTokensHint": "内存压缩和攻击链构建共用此配置,默认 120000",
|
||||
"openaiReasoningTitle": "模型推理(Eino)",
|
||||
"openaiReasoningHint": "仅 Eino 单代理与多代理请求生效;与对话页「模型推理」下拉配合使用。",
|
||||
"openaiReasoningProfile": "线路 profile",
|
||||
"openaiReasoningAllowClient": "允许对话页覆盖推理选项",
|
||||
"fofaBaseUrlPlaceholder": "https://fofa.info/api/v1/search/all(可选)",
|
||||
"fofaBaseUrlHint": "留空则使用默认地址。",
|
||||
"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)
|
||||
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_EINO_SINGLE = 'eino_single';
|
||||
const CHAT_AGENT_EINO_MODES = ['deep', 'plan_execute', 'supervisor'];
|
||||
@@ -492,6 +494,132 @@ function syncAgentModeFromValue(value) {
|
||||
const v = el.getAttribute('data-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() {
|
||||
@@ -513,6 +641,9 @@ function toggleAgentModePanel() {
|
||||
closeAgentModePanel();
|
||||
return;
|
||||
}
|
||||
if (typeof closeChatReasoningPanel === 'function') {
|
||||
closeChatReasoningPanel();
|
||||
}
|
||||
if (typeof closeRoleSelectionPanel === 'function') {
|
||||
closeRoleSelectionPanel();
|
||||
}
|
||||
@@ -563,6 +694,8 @@ async function initChatAgentModeFromConfig() {
|
||||
} catch (e) { /* ignore */ }
|
||||
sel.value = stored;
|
||||
syncAgentModeFromValue(stored);
|
||||
restoreChatReasoningControlsFromStorage();
|
||||
syncReasoningRowVisibility(stored);
|
||||
} catch (e) {
|
||||
console.warn('initChatAgentModeFromConfig', e);
|
||||
}
|
||||
@@ -575,6 +708,9 @@ document.addEventListener('languagechange', function () {
|
||||
if (v === CHAT_AGENT_MODE_REACT || chatAgentModeIsEinoSingle(v) || chatAgentModeIsEino(v)) {
|
||||
syncAgentModeFromValue(v);
|
||||
}
|
||||
if (typeof updateChatReasoningSummary === 'function') {
|
||||
updateChatReasoningSummary();
|
||||
}
|
||||
});
|
||||
|
||||
// 保存输入框草稿到localStorage(防抖版本)
|
||||
@@ -760,6 +896,10 @@ async function sendMessage() {
|
||||
serverPath: a.serverPath
|
||||
}));
|
||||
}
|
||||
const reasoningPayload = buildReasoningRequestPayload();
|
||||
if (reasoningPayload) {
|
||||
body.reasoning = reasoningPayload;
|
||||
}
|
||||
// 发送后清空附件列表
|
||||
chatAttachments = [];
|
||||
renderChatFileChips();
|
||||
@@ -2228,6 +2368,8 @@ function renderProcessDetails(messageId, processDetails) {
|
||||
}
|
||||
} else if (eventType === 'thinking') {
|
||||
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') {
|
||||
if (typeof window.einoMainStreamPlanningTitle === 'function') {
|
||||
itemTitle = window.einoMainStreamPlanningTitle(data);
|
||||
@@ -7233,6 +7375,14 @@ document.addEventListener('click', function(event) {
|
||||
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;
|
||||
}
|
||||
|
||||
case 'thinking_stream_start': {
|
||||
case 'thinking_stream_start':
|
||||
case 'reasoning_chain_stream_start': {
|
||||
const d = event.data || {};
|
||||
const streamId = d.streamId || null;
|
||||
if (!streamId) break;
|
||||
|
||||
const timelineType = event.type === 'reasoning_chain_stream_start' ? 'reasoning_chain' : 'thinking';
|
||||
|
||||
let state = thinkingStreamStateByProgressId.get(progressId);
|
||||
if (!state) {
|
||||
state = new Map();
|
||||
@@ -1246,9 +1249,12 @@ function handleStreamEvent(event, progressElement, progressId,
|
||||
}
|
||||
break;
|
||||
}
|
||||
const thinkBase = typeof window.t === 'function' ? window.t('chat.aiThinking') : 'AI思考';
|
||||
const title = timelineAgentBracketPrefix(d) + '🤔 ' + thinkBase;
|
||||
const itemId = addTimelineItem(timeline, 'thinking', {
|
||||
const labelBase = typeof window.t === 'function'
|
||||
? window.t(timelineType === 'reasoning_chain' ? 'chat.reasoningChain' : 'chat.aiThinking')
|
||||
: (timelineType === 'reasoning_chain' ? '推理过程' : 'AI思考');
|
||||
const emoji = timelineType === 'reasoning_chain' ? '🔗' : '🤔';
|
||||
const title = timelineAgentBracketPrefix(d) + emoji + ' ' + labelBase;
|
||||
const itemId = addTimelineItem(timeline, timelineType, {
|
||||
title: title,
|
||||
message: ' ',
|
||||
data: d
|
||||
@@ -1257,7 +1263,8 @@ function handleStreamEvent(event, progressElement, progressId,
|
||||
break;
|
||||
}
|
||||
|
||||
case 'thinking_stream_delta': {
|
||||
case 'thinking_stream_delta':
|
||||
case 'reasoning_chain_stream_delta': {
|
||||
const d = event.data || {};
|
||||
const streamId = d.streamId || null;
|
||||
if (!streamId) break;
|
||||
@@ -1281,7 +1288,9 @@ function handleStreamEvent(event, progressElement, progressId,
|
||||
}
|
||||
|
||||
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) {
|
||||
const streamId = event.data.streamId;
|
||||
const state = thinkingStreamStateByProgressId.get(progressId);
|
||||
@@ -1303,12 +1312,17 @@ function handleStreamEvent(event, progressElement, progressId,
|
||||
}
|
||||
}
|
||||
|
||||
addTimelineItem(timeline, 'thinking', {
|
||||
title: timelineAgentBracketPrefix(event.data) + '🤔 ' + (typeof window.t === 'function' ? window.t('chat.aiThinking') : 'AI思考'),
|
||||
const labelBase = typeof window.t === 'function'
|
||||
? 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,
|
||||
data: event.data
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case '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'
|
||||
? formatTimelineStreamBody(options.message, options.data)
|
||||
: options.message;
|
||||
@@ -3410,6 +3424,8 @@ function refreshProgressAndTimelineI18n() {
|
||||
} else {
|
||||
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') {
|
||||
if (item.dataset.orchestration && typeof einoMainStreamPlanningTitle === 'function') {
|
||||
titleSpan.textContent = einoMainStreamPlanningTitle({
|
||||
|
||||
@@ -256,6 +256,9 @@ function toggleRoleSelectionPanel() {
|
||||
if (typeof closeAgentModePanel === 'function') {
|
||||
closeAgentModePanel();
|
||||
}
|
||||
if (typeof closeChatReasoningPanel === 'function') {
|
||||
closeChatReasoningPanel();
|
||||
}
|
||||
panel.style.display = 'flex'; // 使用flex布局
|
||||
// 添加打开状态的视觉反馈
|
||||
if (roleSelectorBtn) {
|
||||
|
||||
@@ -159,6 +159,27 @@ async function loadConfig(loadTools = true) {
|
||||
if (maxTokensEl) {
|
||||
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配置
|
||||
const fofa = currentConfig.fofa || {};
|
||||
@@ -1065,13 +1086,22 @@ async function applySettings() {
|
||||
};
|
||||
|
||||
const wecomAgentIdVal = document.getElementById('robot-wecom-agent-id')?.value.trim();
|
||||
const prevOpenai = (currentConfig && currentConfig.openai) ? currentConfig.openai : {};
|
||||
const config = {
|
||||
openai: {
|
||||
...prevOpenai,
|
||||
provider: provider,
|
||||
api_key: apiKey,
|
||||
base_url: baseUrl,
|
||||
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: {
|
||||
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) + ' 轮迭代'));
|
||||
} else if (eventType === 'thinking') {
|
||||
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') {
|
||||
title = ap + '🔧 ' + ((typeof window.t === 'function') ? window.t('chat.toolCallsDetected', { count: data.count || 0 }) : ('检测到 ' + (data.count || 0) + ' 个工具调用'));
|
||||
} else if (eventType === 'tool_call') {
|
||||
@@ -2847,6 +2849,12 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
|
||||
if (info && info.orchestration) {
|
||||
body.orchestration = info.orchestration;
|
||||
}
|
||||
if (typeof window.buildReasoningRequestPayload === 'function') {
|
||||
var rp = window.buildReasoningRequestPayload();
|
||||
if (rp) {
|
||||
body.reasoning = rp;
|
||||
}
|
||||
}
|
||||
return apiFetch(info.path, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -2953,17 +2961,19 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
|
||||
appendTimelineItem('iteration', '🔍 ' + iterTitle, iterMessage, _ed);
|
||||
if (!streamingTarget) assistantDiv.textContent = '…';
|
||||
|
||||
// ─── Thinking (non-stream + stream) ───
|
||||
} else if (_et === 'thinking_stream_start' && _ed.streamId) {
|
||||
// ─── Thinking / reasoning_chain(推理过程,reasoning_content) ───
|
||||
} 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)) {
|
||||
var tsExist = wsThinkingStreams.get(_ed.streamId);
|
||||
tsExist.buf = '';
|
||||
if (tsExist.body) tsExist.body.textContent = '';
|
||||
} 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');
|
||||
thinkSItem.className = 'webshell-ai-timeline-item webshell-ai-timeline-thinking';
|
||||
thinkSItem.innerHTML = '<span class="webshell-ai-timeline-title">' + escapeHtml(webshellAgentPx(_ed) + '🤔 ' + thinkSLabel) + '</span>';
|
||||
thinkSItem.className = 'webshell-ai-timeline-item webshell-ai-timeline-' + (isRcStart ? 'reasoning_chain' : 'thinking');
|
||||
thinkSItem.innerHTML = '<span class="webshell-ai-timeline-title">' + escapeHtml(webshellAgentPx(_ed) + thinkEmoji + ' ' + thinkSLabel) + '</span>';
|
||||
var thinkSPre = document.createElement('div');
|
||||
thinkSPre.className = 'webshell-ai-timeline-msg webshell-thinking-stream-body';
|
||||
thinkSItem.appendChild(thinkSPre);
|
||||
@@ -2972,7 +2982,7 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
|
||||
wsThinkingStreams.set(_ed.streamId, { el: thinkSItem, body: thinkSPre, buf: '' });
|
||||
}
|
||||
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);
|
||||
if (tsD) {
|
||||
var normT = (typeof window.normalizeStreamingDeltaJs === 'function')
|
||||
@@ -2985,7 +2995,7 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
|
||||
}
|
||||
}
|
||||
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);
|
||||
if (tsE) {
|
||||
var fullThink = (_em != null && _em !== '') ? String(_em) : tsE.buf;
|
||||
@@ -2996,13 +3006,15 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
|
||||
}
|
||||
wsThinkingStreams.delete(_ed.streamId);
|
||||
}
|
||||
} else if (_et === 'thinking' && _em) {
|
||||
} else if ((_et === 'thinking' || _et === 'reasoning_chain') && _em) {
|
||||
// 如果有 streamId 且已存在流式条目,跳过避免重复
|
||||
if (_ed.streamId && wsThinkingStreams.has(_ed.streamId)) {
|
||||
// 已由 thinking_stream_* 处理
|
||||
// 已由 *_stream_* 处理
|
||||
} else {
|
||||
var thinkLabel = wsTOr('chat.aiThinking', 'AI 思考');
|
||||
appendTimelineItem('thinking', webshellAgentPx(_ed) + '🤔 ' + thinkLabel, _em, _ed);
|
||||
var isRc = _et === 'reasoning_chain';
|
||||
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 = '…';
|
||||
|
||||
|
||||
@@ -894,6 +894,8 @@
|
||||
<div id="active-tasks-bar" class="active-tasks-bar"></div>
|
||||
<div id="chat-messages" class="chat-messages"></div>
|
||||
<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">
|
||||
<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>
|
||||
@@ -979,6 +981,50 @@
|
||||
</div>
|
||||
<input type="hidden" id="agent-mode-select" value="react" autocomplete="off">
|
||||
</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 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">
|
||||
@@ -1002,6 +1048,7 @@
|
||||
<path d="M5 12h14M12 5l7 7-7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</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" />
|
||||
<small style="color: var(--text-muted, #718096); font-size: 0.75rem;" data-i18n="settingsBasic.maxTotalTokensHint">内存压缩和攻击链构建共用此配置,默认 120000</small>
|
||||
</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;">
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user