Merge pull request #833 from SuanmoSuanyangTechnology/release/v0.2.10

Release/v0.2.10
This commit is contained in:
Ke Sun
2026-04-08 21:45:35 +08:00
committed by GitHub
89 changed files with 1712 additions and 858 deletions

View File

@@ -14,6 +14,7 @@ from typing import Any, AsyncGenerator, Dict, List, Optional, Sequence
from langchain.agents import create_agent
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage
from langchain_core.tools import BaseTool
from langgraph.errors import GraphRecursionError
from app.core.logging_config import get_business_logger
from app.core.models import RedBearLLM, RedBearModelConfig
@@ -377,7 +378,7 @@ class LangChainAgent:
{"messages": messages},
config={"recursion_limit": self.max_iterations}
)
except RecursionError as e:
except (RecursionError, GraphRecursionError) as e:
logger.warning(
f"Agent 达到最大迭代次数限制 ({self.max_iterations}),可能存在工具调用循环",
extra={"error": str(e)}
@@ -612,6 +613,12 @@ class LangChainAgent:
yield stream_total_tokens
break
except GraphRecursionError:
logger.warning(
f"Agent 达到最大迭代次数限制 ({self.max_iterations}),模型可能不支持正确的工具调用停止判断"
)
if not full_content:
yield "抱歉,我在处理您的请求时遇到了问题(已达最大处理步骤限制)。请尝试简化问题或更换模型后重试。"
except Exception as e:
logger.error(f"Agent astream_events 失败: {str(e)}", exc_info=True)
raise

View File

@@ -19,6 +19,7 @@ class BizCode(IntEnum):
TENANT_NOT_FOUND = 3002
WORKSPACE_NO_ACCESS = 3003
WORKSPACE_INVITE_NOT_FOUND = 3004
WORKSPACE_ACCESS_DENIED = 3005
# API Key 管理3xxx
API_KEY_NOT_FOUND = 3007
API_KEY_DUPLICATE_NAME = 3008
@@ -113,6 +114,8 @@ HTTP_MAPPING = {
BizCode.FORBIDDEN: 403,
BizCode.TENANT_NOT_FOUND: 400,
BizCode.WORKSPACE_NO_ACCESS: 403,
BizCode.WORKSPACE_INVITE_NOT_FOUND: 400,
BizCode.WORKSPACE_ACCESS_DENIED: 403,
BizCode.NOT_FOUND: 400,
BizCode.USER_NOT_FOUND: 200,
BizCode.WORKSPACE_NOT_FOUND: 400,

View File

@@ -79,8 +79,10 @@ class RedBearModelFactory:
model_kwargs: Dict[str, Any] = config.extra_params.get("model_kwargs", {})
if is_streaming:
model_kwargs["enable_thinking"] = config.deep_thinking
if config.deep_thinking and config.thinking_budget_tokens:
model_kwargs["thinking_budget"] = config.thinking_budget_tokens
if config.deep_thinking:
model_kwargs["incremental_output"] = True
if config.thinking_budget_tokens:
model_kwargs["thinking_budget"] = config.thinking_budget_tokens
else:
model_kwargs["enable_thinking"] = False
params["model_kwargs"] = model_kwargs
@@ -110,7 +112,7 @@ class RedBearModelFactory:
params["stream_usage"] = True
# 深度思考模式
is_streaming = bool(config.extra_params.get("streaming"))
if is_streaming:
if is_streaming and not config.is_omni:
if provider == ModelProvider.VOLCANO:
# 火山引擎深度思考仅流式调用支持,非流式时不传 thinking 参数
thinking_config: Dict[str, Any] = {
@@ -140,8 +142,10 @@ class RedBearModelFactory:
model_kwargs: Dict[str, Any] = config.extra_params.get("model_kwargs", {})
if is_streaming:
model_kwargs["enable_thinking"] = config.deep_thinking
if config.deep_thinking and config.thinking_budget_tokens:
model_kwargs["thinking_budget"] = config.thinking_budget_tokens
if config.deep_thinking:
model_kwargs["incremental_output"] = True
if config.thinking_budget_tokens:
model_kwargs["thinking_budget"] = config.thinking_budget_tokens
else:
model_kwargs["enable_thinking"] = False
params["model_kwargs"] = model_kwargs

View File

@@ -1,5 +1,5 @@
from typing import Any, Dict, List, Optional, Union
from typing import Any, Dict, List, Union
from langchain_core.embeddings import Embeddings
from app.core.models.base import RedBearModelConfig, get_provider_embedding_class, RedBearModelFactory
@@ -22,7 +22,8 @@ class RedBearEmbeddings(Embeddings):
self._model = self._create_model(config)
self._client = None
def _create_model(self, config: RedBearModelConfig) -> Embeddings:
@staticmethod
def _create_model(config: RedBearModelConfig) -> Embeddings:
"""根据配置创建 LangChain 模型"""
embedding_class = get_provider_embedding_class(config.provider)
provider = config.provider.lower()
@@ -36,6 +37,8 @@ class RedBearEmbeddings(Embeddings):
"api_key": config.api_key,
"timeout": httpx.Timeout(timeout=config.timeout, connect=60.0),
"max_retries": config.max_retries,
"check_embedding_ctx_length": False,
"encoding_format": "float"
}
elif provider == ModelProvider.DASHSCOPE:
params = {

View File

@@ -803,7 +803,6 @@ models:
- vision
- video
- audio
- thinking
is_omni: true
tags:
- 大语言模型

View File

@@ -131,7 +131,7 @@ class DifyConverter(BaseConverter):
selector = var_selector.split('.')
if len(selector) not in [2, 3] and var_selector != "context":
raise Exception(f"invalid variable selector: {var_selector}")
if len(selector) == 3:
if len(selector) == 3 and selector[0] in ("conversation", "sys"):
selector = selector[1:]
if selector[0] == "conversation":
selector[0] = "conv"
@@ -483,11 +483,11 @@ class DifyConverter(BaseConverter):
node_data = node["data"]
result = IterationNodeConfig.model_construct(
input=self._process_list_variable_literal(node_data["iterator_selector"]),
parallel=node_data["is_parallel"],
parallel_count=node_data["parallel_nums"],
parallel=node_data.get("is_parallel", False),
parallel_count=node_data.get("parallel_nums", 4),
output=self._process_list_variable_literal(node_data["output_selector"]),
output_type=self.variable_type_map(node_data.get("output_type")),
flatten=node_data["flatten_output"],
flatten=node_data.get("flatten_output", False),
).model_dump()
self.config_validate(node["id"], node["data"]["title"], IterationNodeConfig, result)
@@ -496,7 +496,23 @@ class DifyConverter(BaseConverter):
def convert_assigner_node_config(self, node: dict) -> dict:
node_data = node["data"]
assignments = []
for assignment in node_data["items"]:
# Support both formats:
# 1. New format: node_data["items"] list
# 2. Flat format: assigned_variable_selector + input_variable_selector + write_mode
if "items" in node_data:
raw_items = node_data["items"]
elif "assigned_variable_selector" in node_data and "input_variable_selector" in node_data:
raw_items = [{
"variable_selector": node_data["assigned_variable_selector"],
"value": node_data["input_variable_selector"],
"input_type": ValueInputType.VARIABLE,
"operation": node_data.get("write_mode", "over-write"),
}]
else:
raw_items = []
for assignment in raw_items:
if assignment.get("operation") is None or assignment.get("value") is None:
continue
assignments.append(

View File

@@ -1,5 +1,5 @@
from typing import Any
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, field_validator
from app.core.workflow.nodes.base_config import BaseNodeConfig
from app.core.workflow.nodes.enums import ComparisonOperator
@@ -31,6 +31,11 @@ class ExtractConfig(BaseModel):
enabled: bool = False
serial: str = "1" # 1-based index string, e.g. "1" = first
@field_validator("serial", mode="before")
@classmethod
def coerce_serial(cls, v):
return str(v)
class ListOperatorNodeConfig(BaseNodeConfig):
"""

View File

@@ -11,7 +11,7 @@ from app.core.workflow.variable.base_variable import VariableType
logger = logging.getLogger(__name__)
# File object fields that hold string values
_FILE_STRING_KEYS = {"name", "extension", "mime_type", "url", "transfer_method", "origin_file_type", "file_id"}
_FILE_STRING_KEYS = {"type", "name", "url", "extension", "mime_type", "transfer_method", "origin_file_type", "file_id"}
_FILE_NUMBER_KEYS = {"size"}
@@ -52,7 +52,7 @@ class ListOperatorNode(BaseNode):
result = [result[idx]]
# 3. Order
if cfg.order_by.enabled and cfg.order_by.key:
if cfg.order_by.enabled:
reverse = cfg.order_by.value == "desc"
key_fn = self._make_sort_key(cfg.order_by.key)
result = sorted(result, key=key_fn, reverse=reverse)
@@ -100,10 +100,17 @@ class ListOperatorNode(BaseNode):
else:
left = item # primitive array: compare element directly
# Determine if this field should be compared as a string
is_string_field = isinstance(item, dict) and cond.key in _FILE_STRING_KEYS
# Numeric operators
if op == ComparisonOperator.EQ:
if is_string_field:
return str(left) == str(value)
return self._safe_num(left) == self._safe_num(value)
if op == ComparisonOperator.NE:
if is_string_field:
return str(left) != str(value)
return self._safe_num(left) != self._safe_num(value)
if op == ComparisonOperator.LT:
return self._safe_num(left) < self._safe_num(value)

View File

@@ -246,7 +246,10 @@ class LLMNode(BaseNode):
logger.info(f"节点 {self.node_id} LLM 调用完成,输出长度: {len(content)}")
# 返回 AIMessage包含响应元数据
return AIMessage(content=content, response_metadata=response.response_metadata)
return AIMessage(content=content, response_metadata={
**response.response_metadata,
"token_usage": getattr(response, 'usage_metadata', None) or response.response_metadata.get('token_usage')
})
def _extract_input(self, state: WorkflowState, variable_pool: VariablePool) -> dict[str, Any]:
"""提取输入数据(用于记录)"""
@@ -305,15 +308,16 @@ class LLMNode(BaseNode):
# 调用 LLM流式支持字符串或消息列表
last_meta_data = {}
last_usage_metadata = {}
async for chunk in llm.astream(self.messages):
# 提取内容
if hasattr(chunk, 'content'):
content = self.process_model_output(chunk.content)
else:
content = str(chunk)
if hasattr(chunk, 'response_metadata'):
if chunk.response_metadata:
last_meta_data = chunk.response_metadata
if hasattr(chunk, 'response_metadata') and chunk.response_metadata:
last_meta_data = chunk.response_metadata
if hasattr(chunk, 'usage_metadata') and chunk.usage_metadata:
last_usage_metadata = chunk.usage_metadata
# 只有当内容不为空时才处理
if content:
@@ -336,7 +340,10 @@ class LLMNode(BaseNode):
# 构建完整的 AIMessage包含元数据
final_message = AIMessage(
content=full_response,
response_metadata=last_meta_data
response_metadata={
**last_meta_data,
"token_usage": last_usage_metadata or last_meta_data.get('token_usage')
}
)
# yield 完成标记

View File

@@ -12,7 +12,7 @@ from app.core.workflow.engine.state_manager import WorkflowState
from app.core.workflow.engine.variable_pool import VariablePool
from app.core.workflow.nodes.base_node import BaseNode
from app.core.workflow.nodes.parameter_extractor.config import ParameterExtractorNodeConfig
from app.core.workflow.variable.base_variable import VariableType
from app.core.workflow.variable.base_variable import VariableType, DEFAULT_VALUE
from app.db import get_db_read
from app.models import ModelType
from app.services.model_service import ModelConfigService
@@ -45,6 +45,12 @@ class ParameterExtractorNode(BaseNode):
"model_id": str(self.typed_config.model_id),
}
def _extract_output(self, business_result: Any) -> Any:
final_output = {}
for param in self.typed_config.params:
final_output[param.name] = business_result.get(param.name) or DEFAULT_VALUE(self.output_types[param.name])
return final_output
def _output_types(self) -> dict[str, VariableType]:
outputs = {}
for param in self.typed_config.params:
@@ -202,7 +208,10 @@ class ParameterExtractorNode(BaseNode):
])
model_resp = await llm.ainvoke(messages)
self.response_metadata = model_resp.response_metadata
self.response_metadata = {
**model_resp.response_metadata,
"token_usage": getattr(model_resp, 'usage_metadata', None) or model_resp.response_metadata.get('token_usage')
}
model_message = self.process_model_output(model_resp.content)
result = json_repair.repair_json(model_message, return_objects=True)
logger.info(f"node: {self.node_id} get params:{result}")

View File

@@ -136,7 +136,10 @@ class QuestionClassifierNode(BaseNode):
response = await llm.ainvoke(messages)
result = self.process_model_output(response.content)
self.response_metadata = response.response_metadata
self.response_metadata = {
**response.response_metadata,
"token_usage": getattr(response, 'usage_metadata', None) or response.response_metadata.get('token_usage')
}
if result in category_names:
category = result

View File

@@ -91,7 +91,7 @@ async def fetch_remote_file_meta(
"""
import httpx
name = size = mime_type = extension = None
name = extension = None
try:
async with httpx.AsyncClient(timeout=10.0) as client:
resp = await client.head(url, follow_redirects=True)

View File

@@ -4,6 +4,10 @@ from typing import Optional, Any, List, Dict, Union
from enum import Enum, StrEnum
from pydantic import BaseModel, Field, ConfigDict, field_serializer, field_validator
from app.schemas.workflow_schema import WorkflowConfigCreate
# ---------- Multimodal File Support ----------
class FileType(StrEnum):
@@ -313,7 +317,7 @@ class AppCreate(BaseModel):
# only for type=multi_agent
multi_agent_config: Optional[Dict[str, Any]] = None
workflow_config: Optional[Dict[str, Any]] = None
workflow_config: Optional[WorkflowConfigCreate] = None
class AppUpdate(BaseModel):

View File

@@ -32,7 +32,7 @@ class ChatRequest(BaseModel):
web_search: bool = Field(default=False, description="是否启用网络搜索")
memory: bool = Field(default=True, description="是否启用记忆功能")
thinking: bool = Field(default=False, description="是否启用深度思考需Agent配置支持")
files: Optional[List[FileInput]] = Field(default=None, description="附件列表(支持多文件)")
files: List[FileInput] = Field(default_factory=list, description="附件列表(支持多文件)")
# ---------- Output Schemas ----------

View File

@@ -231,8 +231,13 @@ class AppChatService:
if memory_flag:
connected_config = get_end_user_connected_config(user_id, self.db)
memory_config_id: str = connected_config.get("memory_config_id")
file_list = []
for file in files:
file_dict = file.model_dump()
file_dict["upload_file_id"] = str(file_dict["upload_file_id"]) if file_dict["upload_file_id"] else None
file_list.append(file_dict)
messages = [
{"role": "user", "content": message, "files": [file.model_dump() for file in files]},
{"role": "user", "content": message, "files": file_list},
{"role": "assistant", "content": result["content"]}
]
if memory_config_id:
@@ -506,8 +511,13 @@ class AppChatService:
if memory_flag:
connected_config = get_end_user_connected_config(user_id, self.db)
memory_config_id: str = connected_config.get("memory_config_id")
file_list = []
for file in files:
file_dict = file.model_dump()
file_dict["upload_file_id"] = str(file_dict["upload_file_id"]) if file_dict["upload_file_id"] else None
file_list.append(file_dict)
messages = [
{"role": "user", "content": message, "files": [file.model_dump() for file in files]},
{"role": "user", "content": message, "files": file_list},
{"role": "assistant", "content": full_content}
]
if memory_config_id:

View File

@@ -1360,6 +1360,7 @@ class AppService:
variables=cfg.get("variables", []),
execution_config=cfg.get("execution_config", {}),
triggers=cfg.get("triggers", []),
features=cfg.get("features", {}),
is_active=True,
created_at=now,
updated_at=now,

View File

@@ -513,34 +513,40 @@ def get_dashboard_yesterday_changes(
today_data: dict
) -> dict:
"""
计算各指标相比昨天的变化
计算各指标相比昨天的变化百分比
- total_app_change / total_knowledge_change只看活跃记录
百分比 = (截止今日活跃总量 - 截止昨日活跃总量) / 截止昨日活跃总量
- total_memory_change / total_api_call_change
百分比 = (今日总量 - 昨日总量) / 昨日总量
昨日总量为 0 时返回 None。返回值为浮点数例如 0.5 表示增长 50%
Args:
db: 数据库会话
workspace_id: 工作空间ID
storage_type: 存储类型 'neo4j' | 'rag'
today_data: 当前数据,包含 total_memory, total_app, total_knowledge, total_api_call
Returns:
{
"total_memory_change": int | None,
"total_app_change": int | None,
"total_knowledge_change": int | None,
"total_api_call_change": int | None
"total_memory_change": float | None,
"total_app_change": float | None,
"total_knowledge_change": float | None,
"total_api_call_change": float | None
}
"""
from datetime import datetime, timedelta
from datetime import datetime
from sqlalchemy import func
from app.models.api_key_model import ApiKey, ApiKeyLog
from app.models.knowledge_model import Knowledge
from app.models.app_model import App
from app.models.appshare_model import AppShare
business_logger.info(f"计算昨日对比: workspace_id={workspace_id}, storage_type={storage_type}")
business_logger.info(f"计算昨日对比百分比: workspace_id={workspace_id}, storage_type={storage_type}")
now_local = datetime.now()
today_start = now_local.replace(hour=0, minute=0, second=0, microsecond=0)
yesterday_start = today_start - timedelta(days=1)
changes = {
"total_memory_change": None,
@@ -549,134 +555,102 @@ def get_dashboard_yesterday_changes(
"total_api_call_change": None,
}
# --- total_api_call_change ---
def _calc_percentage(today_val, yesterday_val):
"""计算百分比昨日为0时返回None"""
if yesterday_val is None or yesterday_val == 0:
return None
return round((today_val - yesterday_val) / yesterday_val, 4)
# --- total_api_call_change: (截止今日累计总数 - 截止昨日累计总数) / 截止昨日累计总数 ---
try:
# 获取该workspace下所有api_key的id
api_key_ids = [
row[0] for row in db.query(ApiKey.id).filter(
ApiKey.workspace_id == workspace_id
).all()
]
if api_key_ids:
# 今日累计
today_api_count = db.query(func.count(ApiKeyLog.id)).filter(
# 截止今日累计调用总数
total_api_until_now = db.query(func.count(ApiKeyLog.id)).filter(
ApiKeyLog.api_key_id.in_(api_key_ids),
ApiKeyLog.created_at >= today_start,
ApiKeyLog.created_at < now_local
).scalar() or 0
# 昨日全天
yesterday_api_count = db.query(func.count(ApiKeyLog.id)).filter(
# 截止昨日的累计调用总数today_start 即昨日结束)
total_api_until_yesterday = db.query(func.count(ApiKeyLog.id)).filter(
ApiKeyLog.api_key_id.in_(api_key_ids),
ApiKeyLog.created_at >= yesterday_start,
ApiKeyLog.created_at < today_start
).scalar() or 0
changes["total_api_call_change"] = today_api_count - yesterday_api_count
changes["total_api_call_change"] = _calc_percentage(total_api_until_now, total_api_until_yesterday)
else:
# 没有api_key如果今日也是0则无对比意义
changes["total_api_call_change"] = None
except Exception as e:
business_logger.warning(f"计算API调用昨日对比失败: {str(e)}")
# --- total_knowledge_change ---
# --- total_knowledge_change: 只看活跃(status=1)且为顶层知识库(parent_id=workspace_id),百分比 = (今日活跃总量 - 昨日活跃总量) / 昨日活跃总量 ---
try:
# 今天有效总量当前status=1的知识库总数,排除用户知识库(permission_id='Memory')
# 截止今日的活跃知识库总量当前 status=1parent_id=workspace_id
today_knowledge = db.query(func.count(Knowledge.id)).filter(
Knowledge.workspace_id == workspace_id,
Knowledge.status == 1,
Knowledge.permission_id != "Memory"
Knowledge.parent_id == Knowledge.workspace_id
).scalar() or 0
# 昨日有效总量:昨天之前创建的、当前仍有效的知识库,排除用户知识库
# 截止昨日的活跃知识库总量(昨日之前创建的、当前仍 status=1parent_id=workspace_id
yesterday_knowledge = db.query(func.count(Knowledge.id)).filter(
Knowledge.workspace_id == workspace_id,
Knowledge.status == 1,
Knowledge.permission_id != "Memory",
Knowledge.parent_id == Knowledge.workspace_id,
Knowledge.created_at < today_start
).scalar() or 0
# 今日软删:今天被软删的知识库(status=2 且 updated_at >= today_start),排除用户知识库
today_deleted_knowledge = db.query(func.count(Knowledge.id)).filter(
Knowledge.workspace_id == workspace_id,
Knowledge.status == 2,
Knowledge.permission_id != "Memory",
Knowledge.updated_at >= today_start
).scalar() or 0
if yesterday_knowledge == 0 and today_knowledge == 0 and today_deleted_knowledge == 0:
changes["total_knowledge_change"] = None
else:
# change = 今天有效总量 - 今日软删 - 昨日有效总量
changes["total_knowledge_change"] = today_knowledge - today_deleted_knowledge - yesterday_knowledge
changes["total_knowledge_change"] = _calc_percentage(today_knowledge, yesterday_knowledge)
except Exception as e:
business_logger.warning(f"计算知识库昨日对比失败: {str(e)}")
# --- total_app_change ---
# --- total_app_change: 只看活跃(is_active=True),百分比 = (今日活跃总量 - 昨日活跃总量) / 昨日活跃总量 ---
try:
# === 自有app ===
# 今天有效总量
today_own_apps = db.query(func.count(App.id)).filter(
App.workspace_id == workspace_id,
App.is_active == True
).scalar() or 0
# 昨日有效总量
yesterday_own_apps = db.query(func.count(App.id)).filter(
App.workspace_id == workspace_id,
App.is_active == True,
App.created_at < today_start
).scalar() or 0
# 今日软删
today_deleted_own_apps = db.query(func.count(App.id)).filter(
App.workspace_id == workspace_id,
App.is_active == False,
App.updated_at >= today_start
).scalar() or 0
# === 被分享app ===
# 今天有效总量
today_shared_apps = db.query(func.count(AppShare.id)).filter(
AppShare.target_workspace_id == workspace_id,
AppShare.is_active == True
).scalar() or 0
# 昨日有效总量
yesterday_shared_apps = db.query(func.count(AppShare.id)).filter(
AppShare.target_workspace_id == workspace_id,
AppShare.is_active == True,
AppShare.created_at < today_start
).scalar() or 0
# 今日软删
today_deleted_shared_apps = db.query(func.count(AppShare.id)).filter(
AppShare.target_workspace_id == workspace_id,
AppShare.is_active == False,
AppShare.updated_at >= today_start
).scalar() or 0
today_total_app = today_own_apps + today_shared_apps
yesterday_total_app = yesterday_own_apps + yesterday_shared_apps
total_deleted = today_deleted_own_apps + today_deleted_shared_apps
if yesterday_total_app == 0 and today_total_app == 0 and total_deleted == 0:
changes["total_app_change"] = None
else:
# change = 今天有效总量 - 今日软删 - 昨日有效总量
changes["total_app_change"] = today_total_app - total_deleted - yesterday_total_app
changes["total_app_change"] = _calc_percentage(today_total_app, yesterday_total_app)
except Exception as e:
business_logger.warning(f"计算应用数量昨日对比失败: {str(e)}")
# --- total_memory_change ---
# --- total_memory_change: (今日总量 - 昨日总量) / 昨日总量 ---
try:
today_memory = today_data.get("total_memory")
if today_memory is None:
changes["total_memory_change"] = None
elif storage_type == "neo4j":
# 从 memory_increments 取最近一条 created_at < today_start 的记录
last_record = db.query(MemoryIncrement).filter(
MemoryIncrement.workspace_id == workspace_id,
MemoryIncrement.created_at < today_start
).order_by(desc(MemoryIncrement.created_at)).first()
if last_record is None:
if last_record is None or last_record.total_num == 0:
changes["total_memory_change"] = None
else:
changes["total_memory_change"] = today_memory - last_record.total_num
changes["total_memory_change"] = _calc_percentage(today_memory, last_record.total_num)
elif storage_type == "rag":
# RAG: 查 documents 表中 created_at < today_start 的 chunk_num 之和
from app.models.document_model import Document
from app.models.end_user_model import EndUser as _EndUser
from app.models.app_model import App as _App
@@ -691,18 +665,18 @@ def get_dashboard_yesterday_changes(
changes["total_memory_change"] = None
else:
file_names = [f"{uid}.txt" for uid in end_user_ids]
yesterday_chunk = db.query(func.sum(Document.chunk_num)).filter(
yesterday_chunk = int(db.query(func.sum(Document.chunk_num)).filter(
Document.file_name.in_(file_names),
Document.created_at < today_start
).scalar()
if yesterday_chunk is None:
).scalar() or 0)
if yesterday_chunk == 0:
changes["total_memory_change"] = None
else:
changes["total_memory_change"] = today_memory - int(yesterday_chunk)
changes["total_memory_change"] = _calc_percentage(today_memory, yesterday_chunk)
except Exception as e:
business_logger.warning(f"计算记忆总量昨日对比失败: {str(e)}")
business_logger.info(f"昨日对比计算完成: {changes}")
business_logger.info(f"昨日对比百分比计算完成: {changes}")
return changes
@@ -1104,15 +1078,11 @@ def get_dashboard_common_stats(db: Session, workspace_id) -> dict:
except Exception as e:
business_logger.warning(f"获取知识库数量失败: {e}")
# total_api_call: 仅统计当天 api_key_log 调用
# total_api_call: 截止当前的历史累计调用
try:
from datetime import datetime
from sqlalchemy import func as _api_func
from app.models.api_key_model import ApiKey as _ApiKey, ApiKeyLog as _ApiKeyLog
_now = datetime.now()
_today_start = _now.replace(hour=0, minute=0, second=0, microsecond=0)
_api_key_ids = [
row[0] for row in db.query(_ApiKey.id).filter(
_ApiKey.workspace_id == workspace_id
@@ -1120,9 +1090,7 @@ def get_dashboard_common_stats(db: Session, workspace_id) -> dict:
]
if _api_key_ids:
total_api_calls = db.query(_api_func.count(_ApiKeyLog.id)).filter(
_ApiKeyLog.api_key_id.in_(_api_key_ids),
_ApiKeyLog.created_at >= _today_start,
_ApiKeyLog.created_at < _now
_ApiKeyLog.api_key_id.in_(_api_key_ids)
).scalar() or 0
else:
total_api_calls = 0

View File

@@ -25,7 +25,7 @@ from app.repositories.workflow_repository import (
WorkflowExecutionRepository,
WorkflowNodeExecutionRepository
)
from app.schemas import DraftRunRequest, FileInput
from app.schemas import DraftRunRequest, FileInput, FileType
from app.services.conversation_service import ConversationService
from app.services.multi_agent_service import convert_uuids_to_str
from app.services.multimodal_service import MultimodalService
@@ -466,7 +466,7 @@ class WorkflowService:
if not isinstance(item, dict) or item.get("is_file"):
return item
transfer_method = item.get("transfer_method", "remote_url")
file_type = item.get("type", "document")
file_type = FileType.trans(item.get("type", "document"))
origin_file_type = item.get("file_type") or file_type
if transfer_method == "remote_url":
url = item.get("url", "")