diff --git a/api/app/controllers/auth_controller.py b/api/app/controllers/auth_controller.py
index fb9ebaa5..baae44a6 100644
--- a/api/app/controllers/auth_controller.py
+++ b/api/app/controllers/auth_controller.py
@@ -136,7 +136,7 @@ async def refresh_token(
# 检查用户是否存在
user = auth_service.get_user_by_id(db, userId)
if not user:
- raise BusinessException(t("auth.user.not_found"), code=BizCode.USER_NOT_FOUND)
+ raise BusinessException(t("auth.user.not_found"), code=BizCode.USER_NO_ACCESS)
# 检查 refresh token 黑名单
if settings.ENABLE_SINGLE_SESSION:
diff --git a/api/app/core/error_codes.py b/api/app/core/error_codes.py
index 41f58734..01b6115d 100644
--- a/api/app/core/error_codes.py
+++ b/api/app/core/error_codes.py
@@ -41,6 +41,7 @@ class BizCode(IntEnum):
FILE_NOT_FOUND = 4006
APP_NOT_FOUND = 4007
RELEASE_NOT_FOUND = 4008
+ USER_NO_ACCESS = 4009
# 冲突/状态(5xxx)
DUPLICATE_NAME = 5001
@@ -118,6 +119,7 @@ HTTP_MAPPING = {
BizCode.WORKSPACE_ACCESS_DENIED: 403,
BizCode.NOT_FOUND: 400,
BizCode.USER_NOT_FOUND: 200,
+ BizCode.USER_NO_ACCESS: 401,
BizCode.WORKSPACE_NOT_FOUND: 400,
BizCode.MODEL_NOT_FOUND: 400,
BizCode.KNOWLEDGE_NOT_FOUND: 400,
diff --git a/api/app/core/models/base.py b/api/app/core/models/base.py
index eff6292f..1de4b120 100644
--- a/api/app/core/models/base.py
+++ b/api/app/core/models/base.py
@@ -206,10 +206,15 @@ class RedBearModelFactory:
if provider in [ModelProvider.XINFERENCE, ModelProvider.GPUSTACK]:
return {
"model": config.model_name,
- # "base_url": config.base_url,
"jina_api_key": config.api_key,
**config.extra_params
}
+ elif provider == ModelProvider.DASHSCOPE:
+ return {
+ "model": config.model_name,
+ "dashscope_api_key": config.api_key,
+ **config.extra_params
+ }
else:
raise BusinessException(f"不支持的提供商: {provider}", code=BizCode.PROVIDER_NOT_SUPPORTED)
@@ -265,6 +270,9 @@ def get_provider_rerank_class(provider: str):
if provider in [ModelProvider.XINFERENCE, ModelProvider.GPUSTACK]:
from langchain_community.document_compressors import JinaRerank
return JinaRerank
+ elif provider == ModelProvider.DASHSCOPE:
+ from langchain_community.document_compressors.dashscope_rerank import DashScopeRerank
+ return DashScopeRerank
# elif provider == ModelProvider.OLLAMA:
# from langchain_ollama import OllamaEmbeddings
# return OllamaEmbeddings
diff --git a/api/app/core/models/embedding.py b/api/app/core/models/embedding.py
index fb75696a..991e4498 100644
--- a/api/app/core/models/embedding.py
+++ b/api/app/core/models/embedding.py
@@ -36,9 +36,7 @@ class RedBearEmbeddings(Embeddings):
"base_url": config.base_url,
"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"
+ "max_retries": config.max_retries
}
elif provider == ModelProvider.DASHSCOPE:
params = {
diff --git a/api/app/core/models/rerank.py b/api/app/core/models/rerank.py
index c4b91e25..45b6fc88 100644
--- a/api/app/core/models/rerank.py
+++ b/api/app/core/models/rerank.py
@@ -76,5 +76,9 @@ class RedBearRerank(BaseDocumentCompressor):
from langchain_community.document_compressors import JinaRerank
model_instance: JinaRerank = self._model
return model_instance.rerank(documents=documents, query=query, top_n=top_n)
+ elif provider == ModelProvider.DASHSCOPE:
+ from langchain_community.document_compressors.dashscope_rerank import DashScopeRerank
+ model_instance: DashScopeRerank = self._model
+ return model_instance.rerank(documents=documents, query=query, top_n=top_n)
else:
raise ValueError(f"不支持的模型提供商: {provider}")
diff --git a/api/app/core/workflow/engine/graph_builder.py b/api/app/core/workflow/engine/graph_builder.py
index daef6e82..83fa7639 100644
--- a/api/app/core/workflow/engine/graph_builder.py
+++ b/api/app/core/workflow/engine/graph_builder.py
@@ -33,7 +33,7 @@ logger = logging.getLogger(__name__)
# ["Hello ", "{{user.name}}", "!"]
_OUTPUT_PATTERN = re.compile(r'\{\{.*?}}|[^{}]+')
# Strict variable format: {{ node_id.field_name }}
-_VARIABLE_PATTERN = re.compile(r'\{\{\s*[a-zA-Z0-9_]+\.[a-zA-Z0-9_]+\s*}}')
+_VARIABLE_PATTERN = re.compile(r'\{\{\s*[a-zA-Z0-9_]+\.[a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)?\s*}}')
class GraphBuilder:
diff --git a/api/app/core/workflow/engine/stream_output_coordinator.py b/api/app/core/workflow/engine/stream_output_coordinator.py
index dcc92fdb..361f99d2 100644
--- a/api/app/core/workflow/engine/stream_output_coordinator.py
+++ b/api/app/core/workflow/engine/stream_output_coordinator.py
@@ -14,7 +14,7 @@ from app.core.workflow.engine.variable_pool import VariablePool
logger = get_logger(__name__)
SCOPE_PATTERN = re.compile(
- r"\{\{\s*([a-zA-Z0-9_]+)\.[a-zA-Z0-9_]+\s*}}"
+ r"\{\{\s*([a-zA-Z0-9_]+)\.[a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)?\s*}}"
)
diff --git a/api/app/core/workflow/engine/variable_pool.py b/api/app/core/workflow/engine/variable_pool.py
index 86a85daf..08d10e22 100644
--- a/api/app/core/workflow/engine/variable_pool.py
+++ b/api/app/core/workflow/engine/variable_pool.py
@@ -34,19 +34,22 @@ class LazyVariableDict:
return self._cache[key]
var_struct = self._source.get(key)
if var_struct is None:
- raise KeyError(key)
- value = var_struct.instance.to_literal() if self._literal else var_struct.instance.get_value()
+ return None
+ raw = var_struct.instance.get_value()
+ # literal 模式下 dict/list 保留结构,让 Jinja2 能继续访问子字段(如 .type)
+ value = raw if (not self._literal or isinstance(raw, (dict, list))) else var_struct.instance.to_literal()
self._cache[key] = value
return value
def get(self, key, default=None):
- try:
- return self._resolve(key)
- except KeyError:
- return default
+ value = self._resolve(key)
+ return default if value is None else value
def __getitem__(self, key):
- return self._resolve(key)
+ value = self._resolve(key)
+ if value is None:
+ raise KeyError(key)
+ return value
def __getattr__(self, key):
if key.startswith('_'):
@@ -164,7 +167,7 @@ class VariablePool:
def transform_selector(selector):
variable_literal = VARIABLE_PATTERN.sub(r"\1", selector).strip()
selector = VariableSelector.from_string(variable_literal).path
- if len(selector) != 2:
+ if len(selector) not in (2, 3):
raise ValueError(f"Selector not valid - {selector}")
return selector
@@ -196,6 +199,16 @@ class VariablePool:
return None
return var_instance
+ @staticmethod
+ def _extract_field(struct: "VariableStruct", field: str | None) -> Any:
+ """If field is given, drill into a dict/object variable's value."""
+ if field is None:
+ return struct.instance.get_value()
+ value = struct.instance.get_value()
+ if not isinstance(value, dict):
+ raise KeyError(f"Variable is not an object, cannot access field '{field}'")
+ return value.get(field)
+
def get_instance(
self,
selector: str,
@@ -250,12 +263,14 @@ class VariablePool:
Raises:
KeyError: If strict is True and the variable does not exist.
"""
+ path = self.transform_selector(selector)
variable_struct = self._get_variable_struct(selector)
if variable_struct is None:
if strict:
raise KeyError(f"{selector} not exist")
return default
-
+ if len(path) == 3:
+ return self._extract_field(variable_struct, path[2])
return variable_struct.instance.get_value()
def get_literal(
@@ -282,12 +297,15 @@ class VariablePool:
Raises:
KeyError: If strict is True and the variable does not exist.
"""
+ path = self.transform_selector(selector)
variable_struct = self._get_variable_struct(selector)
if variable_struct is None:
if strict:
raise KeyError(f"{selector} not exist")
return default
-
+ if len(path) == 3:
+ value = self._extract_field(variable_struct, path[2])
+ return str(value) if value is not None else ""
return variable_struct.instance.to_literal()
async def set(
@@ -345,7 +363,14 @@ class VariablePool:
Returns:
变量是否存在
"""
- return self._get_variable_struct(selector) is not None
+ path = self.transform_selector(selector)
+ struct = self._get_variable_struct(selector)
+ if struct is None:
+ return False
+ if len(path) == 3:
+ value = struct.instance.get_value()
+ return isinstance(value, dict) and path[2] in value
+ return True
def lazy_namespace(self, namespace: str, literal: bool = False) -> LazyVariableDict:
return LazyVariableDict(self.variables.get(namespace, {}), literal)
diff --git a/api/app/repositories/user_repository.py b/api/app/repositories/user_repository.py
index af4449e5..2dd76b04 100644
--- a/api/app/repositories/user_repository.py
+++ b/api/app/repositories/user_repository.py
@@ -23,7 +23,7 @@ class UserRepository:
db_logger.debug(f"根据 ID 查询用户:user_id={user_id}")
try:
- user = self.db.query(User).options(joinedload(User.tenant)).filter(User.id == user_id).first()
+ user = self.db.query(User).options(joinedload(User.tenant)).filter(User.id == user_id, User.is_active.is_(True)).first()
if user:
# 检查租户状态,租户禁用时返回 None
if user.tenant and not user.tenant.is_active:
diff --git a/api/app/version_info.json b/api/app/version_info.json
index b4f6976f..d07035e2 100644
--- a/api/app/version_info.json
+++ b/api/app/version_info.json
@@ -1,4 +1,36 @@
{
+ "v0.2.10": {
+ "introduction": {
+ "codeName": "炼剑",
+ "releaseDate": "2026-4-8",
+ "upgradePosition": "🐻 全面强化工作流引擎、引入 Agent 深度思考模式与多模态记忆读取,百炼成锋,剑指生产就绪",
+ "coreUpgrades": [
+ "1. 工作流引擎增强
* 会话变量文件格式支持:支持文件类型值及本地/远程默认值配置
* 列表操作节点:新增专用列表操作节点
* 模板转换支持 HTML:扩展富内容渲染能力
* 表单返回与提交:工作流返回交互式表单,前端支持提交
* HTTP 节点 XML 响应:拓宽企业级 API 集成兼容性
* 开场白与文件引用:支持配置开场白及附件引用
* 模板转换三级变量:支持深层嵌套变量访问
* 节点连线添加按钮:连线处新增内联添加按钮",
+ "2. Agent 智能 🧠
* Agent 深度思考模式:支持更充分的推理以产出高质量回答
* 模型深度思考特性开关:模型级特性标识与应用级开关控制",
+ "3. 记忆系统升级 📚
* 用户记忆库分页:支持大规模记忆集合分页浏览
* RAG 用户记忆数据结构刷新:后端 API 数据结构重新设计
* 多模态记忆读取:支持检索图像、音频等非文本记忆
* 语义剪枝阈值提示文案:显示描述性区间标签",
+ "4. 前端与体验 🎨
* 技能工具删除状态展示:工具列表显示删除状态标识
* 仪表盘日环比数据:关键指标增加与昨日对比数据",
+ "5. 稳健性与缺陷修复 🔧
* 参数提取空值处理:优雅处理缺失数据
* Token 消耗展示优化:确保用量报告准确
* 模型参数负值修复:明确参数范围定义
* 应用共享删除同步:正确更新所有共享记录
* 记忆写入任务排序:按时间戳顺序执行
* 多模态模型缺失优雅处理:不再中断感知记忆写入
* 自定义工具 Number 变量传递:解决类型转换问题
* 集群子代理保存后显示:修复未反显问题
* 记忆开启后流式输出修复:解决字符串序列化问题",
+ "
",
+ "v0.2.10 标志着平台向生产成熟度迈出的重要一步。深度思考、交互式表单工作流与多模态记忆的结合展现了平台从记忆存储向综合认知基础设施的演进。我们期待 4 月 17 日 v0.3.0 发布会,届时将带来更深层的 Agent 推理能力、多智能体协作功能及记忆智能管线的进一步优化。剑已炼成,只待出鞘。",
+ "MemoryBear — 百炼成锋 🐻✨"
+ ]
+ },
+ "introduction_en": {
+ "codeName": "LianJian",
+ "releaseDate": "2026-4-8",
+ "upgradePosition": "🐻 Comprehensive workflow engine enhancements, Agent deep thinking mode, and multimodal memory reading — forging the blade for production readiness",
+ "coreUpgrades": [
+ "1. Workflow Engine Enhancements
* Session Variable File Support: File-type values with local/remote defaults
* List Operation Node: Dedicated node for array manipulation
* Template Conversion HTML Support: Rich-content rendering
* Form Return & Submission: Interactive forms in workflow conversations
* HTTP Node XML Response: Enterprise API integration compatibility
* Opening Remarks & File References: Configurable conversation openers
* Template Conversion Three-Level Variables: Deep nested variable access
* Node Connection Add Button: Inline add button on connections",
+ "2. Agent Intelligence 🧠
* Agent Deep Thinking Mode: Thorough reasoning for complex queries
* Model Deep Thinking Feature Toggle: Model-level flag with per-app control",
+ "3. Memory System Upgrades 📚
* User Memory Pagination: Paginated browsing for large collections
* RAG User Memory Data Structure Refresh: Redesigned backend API contracts
* Multimodal Memory Reading: Retrieval of image, audio, and non-text memory
* Semantic Pruning Threshold Hints: Descriptive range labels for configuration",
+ "4. Frontend & Usability 🎨
* Skill Tool Deletion Status Display: Deletion indicators in tool list
* Dashboard Day-over-Day Comparison: Key metrics with yesterday comparison",
+ "5. Robustness & Bug Fixes 🔧
* Parameter Extraction Null Handling: Graceful handling of missing data
* Token Consumption Display Optimization: Accurate usage reporting
* Model Parameter Negative Value Fix: Clear parameter range definitions
* App Share Deletion Sync: Correct update of all share records
* Memory Write Task Ordering: Chronological execution per end_user
* Multimodal Model Missing Graceful Handling: No more interrupted writes
* Custom Tool Number Variable Pass-through: Type coercion fix
* Cluster Sub-Agent Display After Save: Fixed UI reflection
* Memory-Enabled Streaming Output Fix: String serialization resolved",
+ "
",
+ "v0.2.10 marks a significant step toward production maturity. The combination of deep thinking, interactive form workflows, and multimodal memory demonstrates the platform's evolution from memory storage to comprehensive cognitive infrastructure. We look forward to the v0.3.0 launch on April 17, bringing deeper agent reasoning, multi-agent collaboration, and further memory intelligence refinements. The blade has been forged — now it's time to wield it.",
+ "MemoryBear — Forging the Blade 🐻✨"
+ ]
+ }
+ },
"v0.2.8": {
"introduction": {
"codeName": "景玉",