Merge branch 'refs/heads/feature/20260105_xjn' into feature/agent-tool_xjn

This commit is contained in:
Timebomb2018
2026-02-28 13:59:31 +08:00
31 changed files with 1706 additions and 942 deletions

View File

@@ -10,7 +10,6 @@ from app.core.config import settings
# 设置日志记录器
logger = logging.getLogger(__name__)
# 创建连接池
pool = ConnectionPool.from_url(
f"redis://{settings.REDIS_HOST}:{settings.REDIS_PORT}",
@@ -21,6 +20,7 @@ pool = ConnectionPool.from_url(
)
aio_redis = redis.StrictRedis(connection_pool=pool)
async def get_redis_connection():
"""获取Redis连接"""
try:
@@ -29,7 +29,8 @@ async def get_redis_connection():
logger.error(f"Redis连接失败: {str(e)}")
return None
async def aio_redis_set(key: str, val: str|dict, expire: int = None):
async def aio_redis_set(key: str, val: str | dict, expire: int = None):
"""设置Redis键值
Args:
@@ -40,7 +41,7 @@ async def aio_redis_set(key: str, val: str|dict, expire: int = None):
try:
if isinstance(val, dict):
val = json.dumps(val, ensure_ascii=False)
if expire is not None:
# 设置带过期时间的键值
await aio_redis.set(key, val, ex=expire)
@@ -50,6 +51,7 @@ async def aio_redis_set(key: str, val: str|dict, expire: int = None):
except Exception as e:
logger.error(f"Redis set错误: {str(e)}")
async def aio_redis_get(key: str):
"""获取Redis键值"""
try:
@@ -58,6 +60,7 @@ async def aio_redis_get(key: str):
logger.error(f"Redis get错误: {str(e)}")
return None
async def aio_redis_delete(key: str):
"""删除Redis键"""
try:
@@ -66,6 +69,7 @@ async def aio_redis_delete(key: str):
logger.error(f"Redis delete错误: {str(e)}")
return None
async def aio_redis_publish(channel: str, message: Dict[str, Any]) -> bool:
"""发布消息到Redis频道"""
try:
@@ -78,9 +82,10 @@ async def aio_redis_publish(channel: str, message: Dict[str, Any]) -> bool:
logger.error(f"Redis发布错误: {str(e)}")
return False
class RedisSubscriber:
"""Redis订阅器"""
def __init__(self, channel: str):
self.channel = channel
self.conn = None
@@ -88,25 +93,25 @@ class RedisSubscriber:
self.is_closed = False
self._queue = asyncio.Queue()
self._task = None
async def start(self):
"""开始订阅"""
if self.is_closed or self._task:
return
self._task = asyncio.create_task(self._receive_messages())
logger.info(f"开始订阅: {self.channel}")
async def _receive_messages(self):
"""接收消息"""
try:
self.conn = await get_redis_connection()
if not self.conn:
return
self.pubsub = self.conn.pubsub()
await self.pubsub.subscribe(self.channel)
while not self.is_closed:
try:
message = await self.pubsub.get_message(ignore_subscribe_messages=True, timeout=0.01)
@@ -127,7 +132,7 @@ class RedisSubscriber:
finally:
await self._queue.put(None)
await self._cleanup()
async def _cleanup(self):
"""清理资源"""
if self.pubsub:
@@ -141,7 +146,7 @@ class RedisSubscriber:
await self.conn.close()
except Exception:
pass
async def get_message(self) -> Optional[Dict[str, Any]]:
"""获取消息"""
if self.is_closed:
@@ -153,7 +158,7 @@ class RedisSubscriber:
except Exception as e:
logger.error(f"获取消息错误: {str(e)}")
return None
async def close(self):
"""关闭订阅器"""
if self.is_closed:
@@ -163,32 +168,33 @@ class RedisSubscriber:
self._task.cancel()
await self._cleanup()
class RedisPubSubManager:
"""Redis发布订阅管理器"""
def __init__(self):
self.subscribers = {}
async def publish(self, channel: str, message: Dict[str, Any]) -> bool:
return await aio_redis_publish(channel, message)
def get_subscriber(self, channel: str) -> RedisSubscriber:
if channel in self.subscribers:
subscriber = self.subscribers[channel]
if not subscriber.is_closed:
return subscriber
subscriber = RedisSubscriber(channel)
self.subscribers[channel] = subscriber
return subscriber
def cancel_subscription(self, channel: str) -> bool:
if channel in self.subscribers:
asyncio.create_task(self.subscribers[channel].close())
del self.subscribers[channel]
return True
return False
def cancel_all_subscriptions(self) -> int:
count = len(self.subscribers)
for subscriber in self.subscribers.values():
@@ -196,6 +202,6 @@ class RedisPubSubManager:
self.subscribers.clear()
return count
# 全局实例
pubsub_manager = RedisPubSubManager()

View File

@@ -1,7 +1,8 @@
import uuid
from typing import Optional, Annotated
from fastapi import APIRouter, Depends, Path
import yaml
from fastapi import APIRouter, Depends, Path, Form, UploadFile, File
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
@@ -17,12 +18,13 @@ from app.repositories.end_user_repository import EndUserRepository
from app.schemas import app_schema
from app.schemas.response_schema import PageData, PageMeta
from app.schemas.workflow_schema import WorkflowConfig as WorkflowConfigSchema
from app.schemas.workflow_schema import WorkflowConfigUpdate
from app.schemas.workflow_schema import WorkflowConfigUpdate, WorkflowImportSave
from app.services import app_service, workspace_service
from app.services.agent_config_helper import enrich_agent_config
from app.services.app_service import AppService
from app.services.workflow_service import WorkflowService, get_workflow_service
from app.services.app_statistics_service import AppStatisticsService
from app.services.workflow_import_service import WorkflowImportService
from app.services.workflow_service import WorkflowService, get_workflow_service
router = APIRouter(prefix="/apps", tags=["Apps"])
logger = get_business_logger()
@@ -65,7 +67,7 @@ def list_apps(
# 当 ids 存在且不为 None 时,根据 ids 获取应用
if ids is not None:
app_ids = [id.strip() for id in ids.split(',') if id.strip()]
app_ids = [app_id.strip() for app_id in ids.split(',') if app_id.strip()]
items_orm = app_service.get_apps_by_ids(db, app_ids, workspace_id)
items = [service._convert_to_schema(app, workspace_id) for app in items_orm]
return success(data=items)
@@ -879,6 +881,60 @@ async def update_workflow_config(
return success(data=WorkflowConfigSchema.model_validate(cfg))
@router.get("/{app_id}/workflow/export")
@cur_workspace_access_guard()
async def export_workflow_config(
app_id: uuid.UUID,
db: Annotated[Session, Depends(get_db)],
current_user: Annotated[User, Depends(get_current_user)]
):
"""导出工作流配置为YAML文件"""
workflow_service = WorkflowService(db)
return success(data={
"content": workflow_service.export_workflow_dsl(app_id=app_id),
})
@router.post("/workflow/import")
@cur_workspace_access_guard()
async def import_workflow_config(
file: UploadFile = File(...),
platform: str = Form(...),
app_id: str = Form(None),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""从YAML内容导入工作流配置"""
if not file.filename.lower().endswith((".yaml", ".yml")):
return fail(msg="Only yaml file is allowed", code=BizCode.BAD_REQUEST)
raw_text = (await file.read()).decode("utf-8")
import_service = WorkflowImportService(db)
config = yaml.safe_load(raw_text)
result = await import_service.upload_config(platform, config)
return success(data=result)
@router.post("/workflow/import/save")
@cur_workspace_access_guard()
async def save_workflow_import(
data: WorkflowImportSave,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
import_service = WorkflowImportService(db)
app = await import_service.save_workflow(
user_id=current_user.id,
workspace_id=current_user.current_workspace_id,
temp_id=data.temp_id,
name=data.name,
description=data.description,
)
return success(data=app_schema.App.model_validate(app))
@router.get("/{app_id}/statistics", summary="应用统计数据")
@cur_workspace_access_guard()
def get_app_statistics(
@@ -889,12 +945,14 @@ def get_app_statistics(
current_user=Depends(get_current_user),
):
"""获取应用统计数据
Args:
app_id: 应用ID
start_date: 开始时间戳(毫秒)
end_date: 结束时间戳(毫秒)
db: 数据库连接
current_user: 当前用户
Returns:
- daily_conversations: 每日会话数统计
- total_conversations: 总会话数
@@ -931,6 +989,8 @@ def get_workspace_api_statistics(
Args:
start_date: 开始时间戳(毫秒)
end_date: 结束时间戳(毫秒)
db: 数据库连接
current_user: 当前用户
Returns:
每日统计数据列表,每项包含:

View File

@@ -16,18 +16,18 @@ class Settings:
# cloud: SaaS 云服务版(全功能,按量计费)
# enterprise: 企业私有化版License 控制)
DEPLOYMENT_MODE: str = os.getenv("DEPLOYMENT_MODE", "community")
# License 配置(企业版)
LICENSE_FILE: str = os.getenv("LICENSE_FILE", "/etc/app/license.json")
LICENSE_SERVER_URL: str = os.getenv("LICENSE_SERVER_URL", "https://license.yourcompany.com")
# 计费服务配置SaaS 版)
BILLING_SERVICE_URL: str = os.getenv("BILLING_SERVICE_URL", "")
# 基础 URL用于 SSO 回调等)
BASE_URL: str = os.getenv("BASE_URL", "http://localhost:8000")
FRONTEND_URL: str = os.getenv("FRONTEND_URL", "http://localhost:3000")
ENABLE_SINGLE_WORKSPACE: bool = os.getenv("ENABLE_SINGLE_WORKSPACE", "true").lower() == "true"
# API Keys Configuration
OPENAI_API_KEY: str = os.getenv("OPENAI_API_KEY", "")
@@ -57,7 +57,6 @@ class Settings:
REDIS_PORT: int = int(os.getenv("REDIS_PORT", "6379"))
REDIS_DB: int = int(os.getenv("REDIS_DB", "1"))
REDIS_PASSWORD: str = os.getenv("REDIS_PASSWORD", "")
# ElasticSearch configuration
ELASTICSEARCH_HOST: str = os.getenv("ELASTICSEARCH_HOST", "https://127.0.0.1")
@@ -91,7 +90,7 @@ class Settings:
# Single Sign-On configuration
ENABLE_SINGLE_SESSION: bool = os.getenv("ENABLE_SINGLE_SESSION", "false").lower() == "true"
# SSO 免登配置
SSO_TOKEN_EXPIRE_SECONDS: int = int(os.getenv("SSO_TOKEN_EXPIRE_SECONDS", "300"))
SSO_TRUSTED_SOURCES_CONFIG: str = os.getenv("SSO_TRUSTED_SOURCES_CONFIG", "{}")
@@ -130,7 +129,7 @@ class Settings:
# Server Configuration
SERVER_IP: str = os.getenv("SERVER_IP", "127.0.0.1")
FILE_LOCAL_SERVER_URL : str = os.getenv("FILE_LOCAL_SERVER_URL", "http://localhost:8000/api")
FILE_LOCAL_SERVER_URL: str = os.getenv("FILE_LOCAL_SERVER_URL", "http://localhost:8000/api")
# ========================================================================
# Internal Configuration (not in .env, used by application code)
@@ -225,6 +224,7 @@ class Settings:
LOAD_MODEL: bool = os.getenv("LOAD_MODEL", "false").lower() == "true"
# workflow config
WORKFLOW_IMPORT_CACHE_TIMEOUT: int = int(os.getenv("WORKFLOW_IMPORT_CACHE_TIMEOUT", 1800))
WORKFLOW_NODE_TIMEOUT: int = int(os.getenv("WORKFLOW_NODE_TIMEOUT", 600))
# ========================================================================
@@ -232,20 +232,20 @@ class Settings:
# ========================================================================
# 通用本体文件路径列表(逗号分隔)
GENERAL_ONTOLOGY_FILES: str = os.getenv("GENERAL_ONTOLOGY_FILES", "General_purpose_entity.ttl")
# 是否启用通用本体类型功能
ENABLE_GENERAL_ONTOLOGY_TYPES: bool = os.getenv("ENABLE_GENERAL_ONTOLOGY_TYPES", "true").lower() == "true"
# Prompt 中最大类型数量
MAX_ONTOLOGY_TYPES_IN_PROMPT: int = int(os.getenv("MAX_ONTOLOGY_TYPES_IN_PROMPT", "50"))
# 核心通用类型列表(逗号分隔)
CORE_GENERAL_TYPES: str = os.getenv(
"CORE_GENERAL_TYPES",
"Person,Organization,Company,GovernmentAgency,Place,Location,City,Country,Building,"
"Event,SportsEvent,SocialEvent,Work,Book,Film,Software,Concept,TopicalConcept,AcademicSubject"
)
# 实验模式开关(允许通过 API 动态切换本体配置)
ONTOLOGY_EXPERIMENT_MODE: bool = os.getenv("ONTOLOGY_EXPERIMENT_MODE", "true").lower() == "true"

View File

@@ -0,0 +1,8 @@
# -*- coding: UTF-8 -*-
# Author: Eternity
# @Email: 1533512157@qq.com
# @Time : 2026/2/24 15:54
from app.core.workflow.adapters.dify.dify_adapter import DifyAdapter
from app.core.workflow.adapters.memory_bear.memory_bear_adapter import MemoryBearAdapter
__all__ = ["DifyAdapter", "MemoryBearAdapter"]

View File

@@ -0,0 +1,88 @@
# -*- coding: UTF-8 -*-
# Author: Eternity
# @Email: 1533512157@qq.com
# @Time : 2026/2/24 15:58
from abc import ABC, abstractmethod
from collections import defaultdict
from enum import StrEnum
from typing import Any
from pydantic import BaseModel, Field
from app.core.workflow.adapters.errors import ExceptionDefineition
from app.schemas.workflow_schema import (
EdgeDefinition,
NodeDefinition,
VariableDefinition,
ExecutionConfig,
TriggerConfig
)
class PlatformType(StrEnum):
MEMORY_BEAR = "memory_bear"
DIFY = "dify"
COZE = "coze"
class PlatformMetadata(BaseModel):
platform_name: str
version: str
support_node_types: list[str]
class WorkflowParserResult(BaseModel):
success: bool
platform: PlatformMetadata
execution_config: ExecutionConfig
origin_config: dict[str, Any]
trigger: TriggerConfig | None
edges: list[EdgeDefinition] = Field(default_factory=list)
nodes: list[NodeDefinition] = Field(default_factory=list)
variables: list[VariableDefinition] = Field(default_factory=list)
warnings: list[ExceptionDefineition] = Field(default_factory=list)
errors: list[ExceptionDefineition] = Field(default_factory=list)
class WorkflowImportResult(BaseModel):
success: bool
temp_id: str | None = Field(..., description="cache id")
workflow_id: str | None = Field(..., description="workflow id")
edges: list[EdgeDefinition] = Field(default_factory=list)
nodes: list[NodeDefinition] = Field(default_factory=list)
variables: list[VariableDefinition] = Field(default_factory=list)
warnings: list[ExceptionDefineition] = Field(default_factory=list)
errors: list[ExceptionDefineition] = Field(default_factory=list)
class BasePlatformAdapter(ABC):
def __init__(self, config: dict[str, Any]):
self.config = config
self.nodes: list[NodeDefinition] = []
self.edges: list[EdgeDefinition] = []
self.conv_variables: list[VariableDefinition] = []
self.errors = []
self.warnings = []
self.branch_node_cache = defaultdict(list)
self.error_branch_node_cache = []
@abstractmethod
def get_metadata(self) -> PlatformMetadata:
"""get platform metadata"""
pass
@abstractmethod
def validate_config(self) -> bool:
"""platform configuration validate"""
pass
@abstractmethod
def parse_workflow(self) -> WorkflowParserResult:
"""parse platform configuration to local config"""
pass
@abstractmethod
def map_node_type(self, platform_node_type: str) -> str:
pass

View File

@@ -0,0 +1,75 @@
# -*- coding: UTF-8 -*-
# Author: Eternity
# @Email: 1533512157@qq.com
# @Time : 2026/2/26 14:32
from abc import ABC, abstractmethod
from app.core.workflow.variable.base_variable import DEFAULT_VALUE, VariableType
class BaseConverter(ABC):
@staticmethod
def _convert_string(var):
try:
return str(var)
except:
return DEFAULT_VALUE(VariableType.STRING)
@staticmethod
def _convert_boolean(var):
try:
return bool(var)
except:
return DEFAULT_VALUE(VariableType.BOOLEAN)
@staticmethod
def _convert_number(var):
try:
return float(var)
except:
return DEFAULT_VALUE(VariableType.NUMBER)
@staticmethod
def _convert_object(var):
try:
return dict(var)
except:
return DEFAULT_VALUE(VariableType.OBJECT)
@staticmethod
@abstractmethod
def _convert_file(var):
pass
@staticmethod
def _convert_array_string(var):
try:
return list(var)
except:
return DEFAULT_VALUE(VariableType.ARRAY_STRING)
@staticmethod
def _convert_array_number(var):
try:
return list(var)
except:
return DEFAULT_VALUE(VariableType.ARRAY_NUMBER)
@staticmethod
def _convert_array_boolean(var):
try:
return list(var)
except:
return DEFAULT_VALUE(VariableType.ARRAY_BOOLEAN)
@staticmethod
def _convert_array_object(var):
try:
return list(var)
except:
return DEFAULT_VALUE(VariableType.ARRAY_OBJECT)
@staticmethod
@abstractmethod
def _convert_array_file(var):
pass

View File

@@ -0,0 +1,4 @@
# -*- coding: UTF-8 -*-
# Author: Eternity
# @Email: 1533512157@qq.com
# @Time : 2026/2/25 18:20

View File

@@ -0,0 +1,659 @@
# -*- coding: UTF-8 -*-
# Author: Eternity
# @Email: 1533512157@qq.com
# @Time : 2026/2/25 18:21
import base64
import re
from typing import Any
from urllib.parse import quote
from app.core.workflow.adapters.base_converter import BaseConverter
from app.core.workflow.adapters.errors import UnsupportVariableType, UnknowModelWarning, ExceptionDefineition, \
ExceptionType
from app.core.workflow.nodes.assigner import AssignerNodeConfig
from app.core.workflow.nodes.assigner.config import AssignmentItem
from app.core.workflow.nodes.base_config import VariableDefinition
from app.core.workflow.nodes.code import CodeNodeConfig
from app.core.workflow.nodes.code.config import InputVariable, OutputVariable
from app.core.workflow.nodes.configs import StartNodeConfig, LLMNodeConfig
from app.core.workflow.nodes.cycle_graph import LoopNodeConfig, IterationNodeConfig
from app.core.workflow.nodes.cycle_graph.config import ConditionDetail as LoopConditionDetail, ConditionsConfig, \
CycleVariable
from app.core.workflow.nodes.end import EndNodeConfig
from app.core.workflow.nodes.enums import ValueInputType, ComparisonOperator, AssignmentOperator, HttpAuthType, \
HttpContentType, HttpErrorHandle
from app.core.workflow.nodes.http_request import HttpRequestNodeConfig
from app.core.workflow.nodes.http_request.config import HttpAuthConfig, HttpContentTypeConfig, HttpFormData, \
HttpTimeOutConfig, HttpRetryConfig, HttpErrorDefaultTamplete, HttpErrorHandleConfig
from app.core.workflow.nodes.if_else import IfElseNodeConfig
from app.core.workflow.nodes.if_else.config import ConditionDetail, ConditionBranchConfig
from app.core.workflow.nodes.jinja_render import JinjaRenderNodeConfig
from app.core.workflow.nodes.jinja_render.config import VariablesMappingConfig
from app.core.workflow.nodes.knowledge import KnowledgeRetrievalNodeConfig
from app.core.workflow.nodes.llm.config import MemoryWindowSetting, MessageConfig
from app.core.workflow.nodes.parameter_extractor import ParameterExtractorNodeConfig
from app.core.workflow.nodes.parameter_extractor.config import ParamsConfig
from app.core.workflow.nodes.question_classifier import QuestionClassifierNodeConfig
from app.core.workflow.nodes.question_classifier.config import ClassifierConfig
from app.core.workflow.nodes.variable_aggregator import VariableAggregatorNodeConfig
from app.core.workflow.variable.base_variable import VariableType, DEFAULT_VALUE
class DifyConverter(BaseConverter):
errors: list
warnings: list
branch_node_cache: dict
error_branch_node_cache: list
def __init__(self):
self.CONFIG_CONVERT_MAP = {
"start": self.convert_start_node_config,
"llm": self.convert_llm_node_config,
"answer": self.convert_end_node_config,
"if-else": self.convert_if_else_node_config,
"loop": self.convert_loop_node_config,
"iteration": self.convert_iteration_node_config,
"assigner": self.convert_assigner_node_config,
"code": self.convert_code_node_config,
"http-request": self.convert_http_node_config,
"template-transform": self.convert_jinja_render_node_config,
"knowledge-retrieval": self.convert_knowledge_node_config,
"parameter-extractor": self.convert_parameter_extractor_node_config,
"question-classifier": self.convert_question_classifier_node_config,
"variable-aggregator": self.convert_variable_aggregator,
"loop-start": lambda x: {},
"iteration-start": lambda x: {},
"loop-end": lambda x: {},
}
def get_node_convert(self, node_type):
func = self.CONFIG_CONVERT_MAP.get(node_type, None)
return func
@staticmethod
def is_variable(expression) -> bool:
return bool(re.match(r"\{\{#(.*?)#}}", expression))
@staticmethod
def process_var_selector(var_selector):
if not var_selector:
return ""
selector = var_selector.split('.')
if len(selector) != 2:
raise Exception(f"invalid variable selector: {var_selector}")
if selector[0] == "conversation":
selector[0] = "conv"
var_selector = ".".join(selector)
mapping = {
"sys.query": "sys.message"
}
var_selector = mapping.get(var_selector, var_selector)
return var_selector
def _process_list_variable_litearl(self, variable_selector: list) -> str | None:
if not self.process_var_selector(".".join(variable_selector)):
return None
return "{{" + self.process_var_selector(".".join(variable_selector)) + "}}"
def trans_variable_format(self, content):
pattern = re.compile(r"\{\{#(.*?)#}}")
def replacer(match: re.Match) -> str:
raw_name = match.group(1)
new_name = self.process_var_selector(raw_name)
return f"{{{{{new_name}}}}}"
return pattern.sub(replacer, content)
@staticmethod
def _convert_file(var):
pass
@staticmethod
def _convert_array_file(var):
pass
@staticmethod
def variable_type_map(source_type) -> VariableType | None:
type_map = {
"file": VariableType.FILE,
"paragraph": VariableType.STRING,
"text-input": VariableType.STRING,
"number": VariableType.NUMBER,
"checkbox": VariableType.BOOLEAN,
"file-list": VariableType.ARRAY_FILE,
"select": VariableType.STRING,
}
var_type = type_map.get(source_type, source_type)
return var_type
def convert_variable_type(self, target_type: VariableType, origin_value: Any):
if not origin_value:
return DEFAULT_VALUE(target_type)
try:
match target_type:
case VariableType.STRING:
return self._convert_string(origin_value)
case VariableType.NUMBER:
return self._convert_number(origin_value)
case VariableType.BOOLEAN:
return self._convert_boolean(origin_value)
case VariableType.FILE:
return self._convert_file(origin_value)
case VariableType.ARRAY_FILE:
return self._convert_array_file(origin_value)
case _:
return origin_value
except:
raise Exception(f"convert variable failed: {target_type}")
@staticmethod
def convert_compare_operator(operator):
operator_map = {
"is": ComparisonOperator.EQ,
"is not": ComparisonOperator.NE,
"=": ComparisonOperator.EQ,
"": ComparisonOperator.NE,
">": ComparisonOperator.GT,
"<": ComparisonOperator.LT,
"": ComparisonOperator.GE,
"": ComparisonOperator.LE,
"not empty": ComparisonOperator.NOT_EMPTY,
}
return operator_map.get(operator, operator)
@staticmethod
def convert_assignment_operator(operator):
operator_map = {
"+=": AssignmentOperator.ADD,
"-=": AssignmentOperator.SUBTRACT,
"*=": AssignmentOperator.MULTIPLY,
"/=": AssignmentOperator.DIVIDE,
"over-write": AssignmentOperator.COVER,
"remove-last": AssignmentOperator.REMOVE_LAST,
"remove-first": AssignmentOperator.REMOVE_FIRST,
}
return operator_map.get(operator, operator)
@staticmethod
def convert_http_auth_type(auth_type):
auth_type_map = {
"no-auth": HttpAuthType.NONE,
"bearer": HttpAuthType.BEARER,
"basic": HttpAuthType.BASIC,
"custom": HttpAuthType.CUSTOM,
}
return auth_type_map.get(auth_type, auth_type)
@staticmethod
def convert_http_content_type(content_type):
content_type_map = {
"none": HttpContentType.NONE,
"form-data": HttpContentType.FROM_DATA,
"x-www-form-urlencoded": HttpContentType.WWW_FORM,
"json": HttpContentType.JSON,
"raw-text": HttpContentType.RAW,
"binary": HttpContentType.BINARY,
}
return content_type_map.get(content_type, content_type)
@staticmethod
def convert_http_error_handle_type(handle_type):
handle_type_map = {
"none": HttpErrorHandle.NONE,
"fail-branch": HttpErrorHandle.BRANCH,
"default-value": HttpErrorHandle.DEFAULT,
}
return handle_type_map.get(handle_type, handle_type)
def convert_start_node_config(self, node: dict) -> dict:
node_data = node["data"]
start_vars = []
for var in node_data["variables"]:
var_type = self.variable_type_map(var["type"])
if not var_type:
self.errors.append(
UnsupportVariableType(
scope=node["id"],
name=var["variable"],
var_type=var["type"],
node_id=node["id"],
node_name=node_data["title"]
)
)
continue
if var_type in ["file", "array[file]"]:
self.errors.append(
ExceptionDefineition(
type=ExceptionType.VARIABLE,
node_id=node["id"],
node_name=node_data["title"],
name=var["variable"],
detail=f"Unsupport Variable type for start node: {var_type}"
)
)
continue
var_def = VariableDefinition(
name=var["variable"],
type=var_type,
required=var["required"],
default=self.convert_variable_type(
var_type, var["default"]
),
description=var["label"],
max_length=var.get("max_length"),
)
start_vars.append(var_def)
return StartNodeConfig(
variables=start_vars
).model_dump()
def convert_question_classifier_node_config(self, node: dict) -> dict:
node_data = node["data"]
self.warnings.append(
UnknowModelWarning(
node_id=node["id"],
node_name=node_data["title"],
model_name=node_data["model"].get("name")
)
)
categories = []
for category in node_data["classes"]:
self.branch_node_cache[node["id"]].append(category["id"])
categories.append(
ClassifierConfig(
class_name=category["name"],
)
)
return QuestionClassifierNodeConfig.model_construct(
input_variable=self._process_list_variable_litearl(node_data["query_variable_selector"]),
user_supplement_prompt=self.trans_variable_format(node_data["instructions"]),
categories=categories,
).model_dump()
def convert_llm_node_config(self, node: dict) -> dict:
node_data = node["data"]
self.warnings.append(
UnknowModelWarning(
node_id=node["id"],
node_name=node_data["title"],
model_name=node_data["model"].get("name")
)
)
context = self._process_list_variable_litearl(node_data["context"]["variable_selector"])
memory = MemoryWindowSetting(
enable=bool(node_data.get("memory")),
enable_window=bool(node_data.get("memory", {}).get("window", {}).get("enabled", False)),
window_size=int(node_data.get("memory", {}).get("window", {}).get("size", 20))
)
messages = []
for message in node_data["prompt_template"]:
messages.append(
MessageConfig(
role=message["role"],
content=self.trans_variable_format(message["text"])
)
)
if memory.enable:
messages.append(
MessageConfig(
role="user",
content=self.trans_variable_format(node_data["memory"]["query_prompt_template"])
)
)
vision = node_data["vision"]["enabled"]
vision_input = self._process_list_variable_litearl(
node_data["vision"]["configs"]["variable_selector"]
) if vision else None
return LLMNodeConfig.model_construct(
model_id=None,
context=context,
memory=memory,
vision=vision,
vision_input=vision_input,
messages=messages
).model_dump()
def convert_end_node_config(self, node: dict) -> dict:
node_data = node["data"]
return EndNodeConfig(
output=self.trans_variable_format(node_data["answer"]),
).model_dump()
def convert_if_else_node_config(self, node: dict) -> dict:
node_data = node["data"]
cases = []
for case in node_data["cases"]:
case_id = case["id"]
logical_operator = case["logical_operator"]
conditions = []
for condition in case["conditions"]:
right_value = condition["value"]
condition_detail = ConditionDetail(
operator=self.convert_compare_operator(condition["comparison_operator"]),
left="{{" + self.process_var_selector(".".join(condition["variable_selector"])) + "}}",
right=self.trans_variable_format(
right_value
) if isinstance(right_value, str) and self.is_variable(right_value) else self.convert_variable_type(
self.variable_type_map(condition["varType"]),
condition["value"]
),
input_type=ValueInputType.VARIABLE
if isinstance(right_value, str) and self.is_variable(right_value) else ValueInputType.CONSTANT,
)
conditions.append(condition_detail)
cases.append(
ConditionBranchConfig(
logical_operator=logical_operator,
expressions=conditions
)
)
self.branch_node_cache[node["id"]].append(case_id)
return IfElseNodeConfig(
cases=cases
).model_dump()
def convert_loop_node_config(self, node: dict) -> dict:
node_data = node["data"]
logical_operator = node_data["logical_operator"]
conditions = []
for condition in node_data["break_conditions"]:
right_value = condition["value"]
conditions.append(
LoopConditionDetail(
operator=self.convert_compare_operator(condition["comparison_operator"]),
left=self._process_list_variable_litearl(condition["variable_selector"]),
right=self.trans_variable_format(
right_value
) if isinstance(right_value, str) and self.is_variable(right_value) else self.convert_variable_type(
self.variable_type_map(condition["varType"]),
condition["value"]
),
input_type=ValueInputType.VARIABLE
if isinstance(right_value, str) and self.is_variable(right_value) else ValueInputType.CONSTANT,
)
)
condition_config = ConditionsConfig(
logical_operator=logical_operator,
expressions=conditions
)
loop_variables = []
for variable in node_data["loop_variables"]:
right_input_type = variable["value_type"]
right_value_type = self.variable_type_map(variable["var_type"])
if right_input_type == ValueInputType.VARIABLE:
right_value = self._process_list_variable_litearl(variable["value"])
else:
right_value = self.convert_variable_type(right_value_type, variable["value"])
loop_variables.append(
CycleVariable(
name=variable["label"],
type=right_value_type,
value=right_value,
input_type=right_input_type
)
)
return LoopNodeConfig(
condition=condition_config,
cycle_vars=loop_variables,
max_loop=node_data["loop_count"]
).model_dump()
def convert_iteration_node_config(self, node: dict) -> dict:
node_data = node["data"]
return IterationNodeConfig(
input=self._process_list_variable_litearl(node_data["iterator_selector"]),
parallel=node_data["is_parallel"],
parallel_count=node_data["parallel_nums"],
output=self._process_list_variable_litearl(node_data["output_selector"]),
output_type=self.variable_type_map(node_data["output_type"]),
flatten=node_data["flatten_output"],
).model_dump()
def convert_assigner_node_config(self, node: dict) -> dict:
node_data = node["data"]
assignments = []
for assignment in node_data["items"]:
if assignment.get("operation") is None or assignment.get("value") is None:
continue
assignments.append(
AssignmentItem(
variable_selector=self._process_list_variable_litearl(assignment["variable_selector"]),
value=self._process_list_variable_litearl(
assignment["value"]
) if assignment["input_type"] == ValueInputType.VARIABLE else assignment["value"],
operation=self.convert_assignment_operator(assignment["operation"])
)
)
return AssignerNodeConfig(
assignments=assignments
).model_dump()
def convert_code_node_config(self, node: dict) -> dict:
node_data = node["data"]
input_variables = []
for input_variable in node_data["variables"]:
input_variables.append(
InputVariable(
name=input_variable["variable"],
variable=self._process_list_variable_litearl(input_variable["value_selector"]),
)
)
output_variables = []
for output_variable in node_data["outputs"]:
output_variables.append(
OutputVariable(
name=output_variable,
type=node_data["outputs"][output_variable]["type"],
)
)
code = base64.b64encode(quote(node_data["code"]).encode("utf-8")).decode("utf-8")
return CodeNodeConfig(
input_variables=input_variables,
language=node_data["code_language"],
output_variables=output_variables,
code=code
).model_dump()
def convert_http_node_config(self, node: dict) -> dict:
node_data = node["data"]
if node_data["authorization"] != 'no-auth':
auth_type = self.convert_http_auth_type(node_data["authorization"]["config"]["type"])
auth_config = HttpAuthConfig(
auth_type=auth_type,
header=node_data["authorization"]["config"].get("header"),
api_key=node_data["authorization"]["config"].get("api_key"),
)
else:
auth_config = HttpAuthConfig()
content_type = self.convert_http_content_type(node_data["body"]["type"])
if content_type == HttpContentType.FROM_DATA:
body_content = []
for content in node_data["body"]["data"]:
body_content.append(
HttpFormData(
key=self.trans_variable_format(content["key"]),
type=content["type"],
value=self.trans_variable_format(content["value"]),
)
)
elif content_type == HttpContentType.WWW_FORM:
body_content = {}
for content in node_data["body"]["data"]:
body_content[
self.trans_variable_format(content["key"])
] = self.trans_variable_format(content["value"])
else:
if node_data["body"]["data"]:
body_content = node_data["body"]["data"][0]["value"]
else:
body_content = ""
headers = {}
for header in node_data["headers"].split("\n"):
if not header:
continue
key_value = header.split(":")
if len(key_value) == 2:
headers[
self.trans_variable_format(key_value[0])
] = self.trans_variable_format(key_value[1])
else:
self.warnings.append(ExceptionDefineition(
type=ExceptionType.CONFIG,
node_id=node["id"],
node_name=node_data["title"],
detail=f"Invalid header/param - {header}",
))
params = {}
for param in node_data["params"].split("\n"):
if not param:
continue
key_value = param.split(":")
if len(key_value) == 2:
params[
self.trans_variable_format(key_value[0])
] = self.trans_variable_format(key_value[1])
else:
self.warnings.append(ExceptionDefineition(
type=ExceptionType.CONFIG,
node_id=node["id"],
node_name=node_data["title"],
detail=f"Invalid header/param - {param}",
))
error_handle_type = self.convert_http_error_handle_type(
node_data.get("error_strategy", "none")
)
default_value = None
if error_handle_type == HttpErrorHandle.DEFAULT:
default_body = ""
default_header = {}
default_status_code = 0
for var in node_data["default_value"]:
if var["key"] == "body":
default_body = var["value"]
elif var["key"] == "header":
default_header = var["value"]
elif var["key"] == "status_code":
default_status_code = var["value"]
default_value = HttpErrorDefaultTamplete(
body=default_body,
headers=default_header,
status_code=default_status_code,
)
self.error_branch_node_cache.append(node['id'])
return HttpRequestNodeConfig(
method=node_data["method"].upper(),
url=node_data["url"],
auth=auth_config,
body=HttpContentTypeConfig(
content_type=self.convert_http_content_type(node_data["body"]["type"]),
data=body_content,
),
headers=headers,
params=params,
verify_ssl=node_data["ssl_verify"],
timeouts=HttpTimeOutConfig(
connect_timeout=node_data["timeout"]["max_connect_timeout"] or 5,
read_timeout=node_data["timeout"]["max_read_timeout"] or 5,
write_timeout=node_data["timeout"]["max_write_timeout"] or 5,
),
retry=HttpRetryConfig(
enable=node_data["retry_config"]["retry_enabled"],
max_attempts=node_data["retry_config"]["max_retries"],
retry_interval=node_data["retry_config"]["retry_interval"],
),
error_handle=HttpErrorHandleConfig(
method=error_handle_type,
default=default_value,
)
).model_dump()
def convert_jinja_render_node_config(self, node: dict) -> dict:
node_data = node["data"]
mapping = []
for variable in node_data["variables"]:
mapping.append(VariablesMappingConfig(
name=variable["variable"],
value=self._process_list_variable_litearl(variable["value_selector"])
))
return JinjaRenderNodeConfig(
template=node_data["template"],
mapping=mapping,
).model_dump()
def convert_knowledge_node_config(self, node: dict) -> dict:
node_data = node["data"]
self.warnings.append(ExceptionDefineition(
node_id=node["id"],
node_name=node_data["title"],
type=ExceptionType.CONFIG,
detail=f"Please reconfigure the Knowledge Retrieval node.",
))
return KnowledgeRetrievalNodeConfig.model_construct(
query=self._process_list_variable_litearl(node_data["query_variable_selector"]),
).model_dump()
def convert_parameter_extractor_node_config(self, node: dict) -> dict:
node_data = node["data"]
self.warnings.append(
UnknowModelWarning(
node_id=node["id"],
node_name=node_data["title"],
model_name=node_data["model"].get("name")
)
)
params = []
for param in node_data["parameters"]:
params.append(
ParamsConfig(
name=param["name"],
desc=param["description"],
required=param["required"],
type=param["type"],
)
)
return ParameterExtractorNodeConfig.model_construct(
text=self._process_list_variable_litearl(node_data["query"]),
params=params,
prompt=node_data["instruction"]
).model_dump()
def convert_variable_aggregator(self, node: dict) -> dict:
node_data = node["data"]
group_enable = node_data["advanced_settings"]["group_enabled"]
group_variables = {}
group_type = {}
if not group_enable:
group_variables["output"] = [
self._process_list_variable_litearl(variable)
for variable in node_data["variables"]
]
group_type["output"] = node_data["output_type"]
else:
for group in node_data["advanced_settings"]["groups"]:
group_variables[group["group_name"]] = [
self._process_list_variable_litearl(variable)
for variable in group["variables"]
]
group_type[group["group_name"]] = group["output_type"]
return VariableAggregatorNodeConfig(
group=group_enable,
group_variables=group_variables,
group_type=group_type,
).model_dump()

View File

@@ -0,0 +1,239 @@
# -*- coding: UTF-8 -*-
# Author: Eternity
# @Email: 1533512157@qq.com
# @Time : 2026/2/24 16:05
from typing import Any
from app.core.logging_config import get_logger
from app.core.workflow.adapters.base_adapter import (
BasePlatformAdapter,
PlatformMetadata,
PlatformType,
WorkflowParserResult
)
from app.core.workflow.adapters.dify.converter import DifyConverter
from app.core.workflow.adapters.errors import ExceptionDefineition, ExceptionType
from app.core.workflow.nodes.enums import NodeType
from app.schemas.workflow_schema import (
NodeDefinition,
EdgeDefinition,
VariableDefinition,
TriggerConfig,
ExecutionConfig
)
logger = get_logger()
class DifyAdapter(BasePlatformAdapter, DifyConverter):
NODE_TYPE_MAPPING = {
"start": NodeType.START,
"llm": NodeType.LLM,
"answer": NodeType.END,
"if-else": NodeType.IF_ELSE,
"loop-start": NodeType.CYCLE_START,
"iteration-start": NodeType.CYCLE_START,
"assigner": NodeType.ASSIGNER,
"loop": NodeType.LOOP,
"iteration": NodeType.ITERATION,
"loop-end": NodeType.BREAK,
"code": NodeType.CODE,
"http-request": NodeType.HTTP_REQUEST,
"template-transform": NodeType.JINJARENDER,
"knowledge-retrieval": NodeType.KNOWLEDGE_RETRIEVAL,
"parameter-extractor": NodeType.PARAMETER_EXTRACTOR,
"question-classifier": NodeType.QUESTION_CLASSIFIER,
"variable-aggregator": NodeType.VAR_AGGREGATOR
}
def __init__(self, config: dict[str, Any]):
DifyConverter.__init__(self)
BasePlatformAdapter.__init__(self, config)
def get_metadata(self) -> PlatformMetadata:
return PlatformMetadata(
platform_name=PlatformType.DIFY,
version="0.5.0",
support_node_types=list(self.NODE_TYPE_MAPPING.keys())
)
def map_node_type(self, platform_node_type) -> str:
return self.NODE_TYPE_MAPPING.get(platform_node_type)
@property
def origin_nodes(self):
return self.config.get("workflow").get("graph").get("nodes")
@property
def origin_edges(self):
return self.config.get("workflow").get("graph").get("edges")
@staticmethod
def _valid_nodes(node: dict[str, Any]):
if "data" not in node:
return False
if "type" not in node["data"]:
return False
if "id" not in node or "type" not in node:
return False
return True
def validate_config(self) -> bool:
require_fields = frozenset({'app', 'dependencies', 'kind', 'version', 'workflow'})
if not all(field in self.config for field in require_fields):
return False
for node in self.origin_nodes:
if not self._valid_nodes(node):
return False
return True
def parse_workflow(self) -> WorkflowParserResult:
for node in self.origin_nodes:
node = self._convert_node(node)
if node:
self.nodes.append(node)
nodes_id = [node.id for node in self.nodes]
for edge in self.origin_edges:
source = edge["source"]
target = edge["target"]
if source not in nodes_id or target not in nodes_id:
continue
edge = self._convert_edge(edge)
if edge:
self.edges.append(edge)
#
for variable in self.config.get("workflow").get("conversation_variables"):
con_var = self._convert_variable(variable)
if variable:
self.conv_variables.append(con_var)
#
# for variables in config.get("workflow").get("environment_variables"):
# variable = self._convert_variable(variables)
# conv_variables.append(variable)
trigger = self._convert_trigger({})
execution_config = self._convert_execution({})
return WorkflowParserResult(
success=not self.errors and not self.warnings,
platform=self.get_metadata(),
execution_config=execution_config,
origin_config=self.config,
trigger=trigger,
edges=self.edges,
nodes=self.nodes,
variables=self.conv_variables,
warnings=self.warnings,
errors=self.errors
)
def _convert_cycle_node_position(self, node_id: str, position: dict):
for node in self.origin_nodes:
if node["id"] == node_id:
return {
"x": node["position"]["x"] + position["x"],
"y": node["position"]["y"] + position["y"]
}
self.errors.append(
ExceptionDefineition(
type=ExceptionType.NODE,
node_id=node_id,
detail="parent cycle node not found"
)
)
raise Exception("parent cycle node not found")
def _convert_node(self, node: dict[str, Any]) -> NodeDefinition | None:
node_data = node["data"]
try:
return NodeDefinition(
id=node["id"],
type=self.map_node_type(node_data["type"]),
name=node_data.get("title"),
cycle=node.get("parentId"),
description=None,
config=self._convert_node_config(node),
position={
"x": node["position"]["x"],
"y": node["position"]["y"]
} if node.get("parentId") is None else self._convert_cycle_node_position(
node["parentId"],
node["position"]
),
error_handling=None,
cache=None
)
except Exception as e:
logger.debug(f"convert node error - {e}", exc_info=True)
def _convert_node_config(self, node: dict):
node_data = node["data"]
node_type = node_data["type"]
try:
converter = self.get_node_convert(node_type)
if converter is None:
raise Exception(f"node type not supported - {node_type}")
return converter(node)
except Exception as e:
self.errors.append(ExceptionDefineition(
type=ExceptionType.NODE,
node_id=node["id"],
node_name=node["data"]["title"],
detail=f"convert node error - {e}",
))
raise e
def _convert_edge(self, edge: dict[str, Any]) -> EdgeDefinition | None:
try:
source = edge["source"]
target = edge["target"]
edge_id = edge["id"]
label = None
if source in self.branch_node_cache:
case_id = "-".join(edge_id.split("-")[1:-2])
if case_id == "false":
label = f'CASE{len(self.branch_node_cache[source])+1}'
else:
label = f'CASE{self.branch_node_cache[source].index(case_id) + 1}'
if source in self.error_branch_node_cache:
case_id = "-".join(edge_id.split("-")[1:-2])
if case_id == "source":
label = "SUCCESS"
else:
label = "ERROR"
return EdgeDefinition(
id=edge["id"],
source=source,
target=target,
label=label,
)
except Exception as e:
self.errors.append(ExceptionDefineition(
type=ExceptionType.EDGE,
detail=f"convert edge error - {e}",
))
return None
def _convert_variable(self, variable) -> VariableDefinition | None:
try:
return VariableDefinition(
name=variable["name"],
default=variable["value"],
type=variable["value_type"],
)
except Exception as e:
self.errors.append(ExceptionDefineition(
type=ExceptionType.VARIABLE,
name=variable.get("name"),
detail=f"convert variable error - {e}",
))
def _convert_trigger(self, trigger: dict[str, Any]) -> TriggerConfig | None:
pass
def _convert_execution(self, execution: dict[str, Any]) -> ExecutionConfig:
return ExecutionConfig()

View File

@@ -0,0 +1,75 @@
# -*- coding: UTF-8 -*-
# Author: Eternity
# @Email: 1533512157@qq.com
# @Time : 2026/2/26 11:29
from enum import StrEnum
from pydantic import BaseModel
class ExceptionType(StrEnum):
NODE = "node"
EDGE = "edge"
VARIABLE = "variable"
TRIGGER = "trigger"
EXECUTION = "execution"
CONFIG = "config"
PLATFORM = "platform"
UNKNOWN = "unknown"
class ExceptionDefineition(BaseModel):
type: ExceptionType
detail: str
node_id: str | None = None
node_name: str | None = None
scope: str | None = None
name: str | None = None
class UnknowModelWarning(ExceptionDefineition):
type: ExceptionType = ExceptionType.NODE
def __init__(self, node_id, node_name, model_name):
super().__init__(
detail=f"Please specify the model mapping manually for model: {model_name}",
node_id=node_id,
node_name=node_name
)
class UnknowError(ExceptionDefineition):
type: ExceptionType = ExceptionType.UNKNOWN
def __init__(self, detail: str, **kwargs):
super().__init__(detail=detail, **kwargs)
class UnsupportPlatform(ExceptionDefineition):
type: ExceptionType = ExceptionType.PLATFORM
def __init__(self, platform: str):
super().__init__(detail=f"Unsupport platform {platform}")
class UnsupportVariableType(ExceptionDefineition):
type: ExceptionType = ExceptionType.VARIABLE
def __init__(self, scope, name, var_type: str, **kwargs):
super().__init__(scope=scope, name=name, detail=f"Unsupport variable type[{var_type}]", **kwargs)
class InvalidConfiguration(ExceptionDefineition):
type: ExceptionType = ExceptionType.CONFIG
def __init__(self):
super().__init__(detail="Invalid workflow configuration format")
class UnsupportNodeType(ExceptionDefineition):
type: ExceptionType = ExceptionType.NODE
def __init__(self, node_id: str, node_type: str):
super().__init__(node_id=node_id, detail=f"Unsupport node Type {node_type}")

View File

@@ -0,0 +1,4 @@
# -*- coding: UTF-8 -*-
# Author: Eternity
# @Email: 1533512157@qq.com
# @Time : 2026/2/26 11:30

View File

@@ -0,0 +1,76 @@
# -*- coding: UTF-8 -*-
# Author: Eternity
# @Email: 1533512157@qq.com
# @Time : 2026/2/25 14:11
from typing import Any
from app.core.workflow.adapters.base_adapter import (
PlatformMetadata,
PlatformType,
BasePlatformAdapter,
WorkflowParserResult
)
from app.schemas.workflow_schema import ExecutionConfig
class MemoryBearAdapter(BasePlatformAdapter):
NODE_TYPE_MAPPING = {}
@property
def origin_nodes(self):
return self.config.get("workflow").get("nodes")
@property
def origin_edges(self):
return self.config.get("workflow").get("edges")
@property
def origin_variables(self):
return self.config.get("workflow").get("variables")
def get_metadata(self) -> PlatformMetadata:
return PlatformMetadata(
platform_name=PlatformType.MEMORY_BEAR,
version="0.2.5",
support_node_types=list(self.NODE_TYPE_MAPPING.keys())
)
def map_node_type(self, platform_node_type) -> str:
return platform_node_type
@staticmethod
def _valid_nodes(node: dict[str, Any]):
if "type" not in node["data"]:
return False
if "id" not in node or "type" not in node:
return False
return True
def validate_config(self) -> bool:
require_fields = frozenset({'app', 'workflow'})
if not all(field in self.config for field in require_fields):
return False
for node in self.origin_nodes:
if not self._valid_nodes(node):
return False
return True
def parse_workflow(self) -> WorkflowParserResult:
self.nodes = self.origin_nodes
self.edges = self.origin_edges
self.conv_variables = self.origin_variables
return WorkflowParserResult(
success=True,
platform=self.get_metadata(),
execution_config=ExecutionConfig(),
origin_config=self.config,
trigger=None,
edges=self.edges,
nodes=self.nodes,
variables=self.conv_variables,
warnings=self.warnings,
errors=self.errors,
)

View File

@@ -0,0 +1,34 @@
# -*- coding: UTF-8 -*-
# Author: Eternity
# @Email: 1533512157@qq.com
# @Time : 2026/2/25 14:19
from typing import Any
from app.core.workflow.adapters import DifyAdapter, MemoryBearAdapter
from app.core.workflow.adapters.base_adapter import BasePlatformAdapter, PlatformType
class PlatformAdapterRegistry:
_adapters: dict[str, type[BasePlatformAdapter]] = {}
@classmethod
def register(cls, platform: str, adapter: type[BasePlatformAdapter]):
cls._adapters[platform] = adapter
@classmethod
def get_adapter(cls, platform: str, config: dict[str, Any]) -> BasePlatformAdapter:
if platform not in cls._adapters:
raise ValueError(f"Unsupported platform: {platform}")
return cls._adapters.get(platform)(config)
@classmethod
def list_platforms(cls) -> list[str]:
return list(cls._adapters.keys())
@classmethod
def is_supported(cls, platform: str) -> bool:
return platform in cls._adapters
PlatformAdapterRegistry.register(PlatformType.MEMORY_BEAR, MemoryBearAdapter)
PlatformAdapterRegistry.register(PlatformType.DIFY, DifyAdapter)

View File

@@ -13,7 +13,7 @@ from app.core.workflow.engine.variable_pool import VariablePool
logger = get_logger(__name__)
SCOPE_PATTERN = re.compile(
r"\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\.[a-zA-Z0-9_]+\s*}}"
r"\{\{\s*([a-zA-Z0-9_]+)\.[a-zA-Z0-9_]+\s*}}"
)

View File

@@ -88,6 +88,8 @@ class AssignerNode(BaseNode):
await operator.remove_first()
case AssignmentOperator.REMOVE_LAST:
await operator.remove_last()
case AssignmentOperator.EXTEND:
await operator.extend()
case _:
raise ValueError(f"Invalid Operator: {assignment.operation}")
logger.info(f"Node {self.node_id}: execution completed")

View File

@@ -17,17 +17,17 @@ class EndNodeConfig(BaseNodeConfig):
description="输出模板,支持引用前置节点的输出,如:{{ llm_qa.output }}"
)
# 输出变量定义
output_variables: list[VariableDefinition] = Field(
default_factory=lambda: [
VariableDefinition(
name="output",
type=VariableType.STRING,
description="工作流的最终输出"
)
],
description="输出变量定义(自动生成,通常不需要修改)"
)
# # 输出变量定义
# output_variables: list[VariableDefinition] = Field(
# default_factory=lambda: [
# VariableDefinition(
# name="output",
# type=VariableType.STRING,
# description="工作流的最终输出"
# )
# ],
# description="输出变量定义(自动生成,通常不需要修改)"
# )
class Config:
json_schema_extra = {

View File

@@ -61,6 +61,7 @@ class AssignmentOperator(StrEnum):
APPEND = "append"
REMOVE_LAST = "remove_last"
REMOVE_FIRST = "remove_first"
EXTEND = "extend"
class HttpRequestMethod(StrEnum):

View File

@@ -236,5 +236,5 @@ class HttpRequestNode(BaseNode):
logger.warning(
f"Node {self.node_id}: HTTP request failed, switching to error handling branch"
)
return "ERROR"
return {"output": "ERROR"}
raise RuntimeError("http request failed")

View File

@@ -40,7 +40,7 @@ class KnowledgeRetrievalNodeConfig(BaseNodeConfig):
)
knowledge_bases: list[KnowledgeBaseConfig] = Field(
...,
default_factory=list,
description="Knowledge base config"
)

View File

@@ -3,7 +3,6 @@
from pydantic import Field
from app.core.workflow.nodes.base_config import BaseNodeConfig, VariableDefinition
from app.core.workflow.variable.base_variable import VariableType
class StartNodeConfig(BaseNodeConfig):
@@ -21,42 +20,42 @@ class StartNodeConfig(BaseNodeConfig):
description="自定义输入变量列表,这些变量会作为 Start 节点的输出"
)
# 输出变量定义
output_variables: list[VariableDefinition] = Field(
default_factory=lambda: [
VariableDefinition(
name="message",
type=VariableType.STRING,
description="用户输入的消息"
),
VariableDefinition(
name="conversation_vars",
type=VariableType.OBJECT,
description="会话级变量"
),
VariableDefinition(
name="execution_id",
type=VariableType.STRING,
description="执行 ID"
),
VariableDefinition(
name="conversation_id",
type=VariableType.STRING,
description="会话 ID"
),
VariableDefinition(
name="workspace_id",
type=VariableType.STRING,
description="工作空间 ID"
),
VariableDefinition(
name="user_id",
type=VariableType.STRING,
description="用户 ID"
)
],
description="输出变量定义(自动生成,通常不需要修改)"
)
# # 输出变量定义
# output_variables: list[VariableDefinition] = Field(
# default_factory=lambda: [
# VariableDefinition(
# name="message",
# type=VariableType.STRING,
# description="用户输入的消息"
# ),
# VariableDefinition(
# name="conversation_vars",
# type=VariableType.OBJECT,
# description="会话级变量"
# ),
# VariableDefinition(
# name="execution_id",
# type=VariableType.STRING,
# description="执行 ID"
# ),
# VariableDefinition(
# name="conversation_id",
# type=VariableType.STRING,
# description="会话 ID"
# ),
# VariableDefinition(
# name="workspace_id",
# type=VariableType.STRING,
# description="工作空间 ID"
# ),
# VariableDefinition(
# name="user_id",
# type=VariableType.STRING,
# description="用户 ID"
# )
# ],
# description="输出变量定义(自动生成,通常不需要修改)"
# )
class Config:
json_schema_extra = {

View File

@@ -35,7 +35,7 @@ class FileMetadata(Base):
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True, comment="Tenant ID")
workspace_id = Column(UUID(as_uuid=True), nullable=False, index=True, comment="Workspace ID")
workspace_id = Column(UUID(as_uuid=True), nullable=True, index=True, comment="Workspace ID")
file_key = Column(String(512), nullable=False, unique=True, index=True, comment="Storage file key")
file_name = Column(String(255), nullable=False, comment="Original file name")
file_ext = Column(String(32), nullable=False, comment="File extension")

View File

@@ -5,6 +5,8 @@ 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 ----------
@@ -196,6 +198,8 @@ class AppCreate(BaseModel):
# only for type=multi_agent
multi_agent_config: Optional[Dict[str, Any]] = None
workflow_config: Optional[WorkflowConfigCreate] = None
class AppUpdate(BaseModel):
name: Optional[str] = None

View File

@@ -18,7 +18,10 @@ class NodeConfig(BaseModel):
class NodeDefinition(BaseModel):
"""节点定义"""
id: str = Field(..., description="节点唯一标识")
type: str = Field(..., description="节点类型: start, end, llm, agent, tool, condition, loop, transform, human, code")
type: str = Field(
...,
description="节点类型: start, end, llm, agent, tool, condition, loop, transform, human, code"
)
name: str | None = Field(None, description="节点名称")
cycle: str | None = Field(None, description="父循环节点id")
description: str | None = Field(None, description="节点描述")
@@ -30,12 +33,12 @@ class NodeDefinition(BaseModel):
class EdgeDefinition(BaseModel):
"""边定义"""
id: str | None = Field(None, description="边唯一标识(可选)")
id: str | None = Field(default=None, description="边唯一标识(可选)")
source: str = Field(..., description="源节点 ID")
target: str = Field(..., description="目标节点 ID")
type: str | None = Field(None, description="边类型: normal, error")
condition: str | None = Field(None, description="条件表达式(条件边)")
label: str | None = Field(None, description="边标签")
type: str | None = Field(default=None, description="边类型: normal, error")
condition: str | None = Field(default=None, description="条件表达式(条件边)")
label: str | None = Field(default=None, description="边标签")
class VariableDefinition(BaseModel):
@@ -44,7 +47,7 @@ class VariableDefinition(BaseModel):
type: str = Field(default="string", description="变量类型: string, number, boolean, object, array")
required: bool = Field(default=False, description="是否必填")
default: Any = Field(None, description="默认值")
description: str | None = Field(None, description="变量描述")
description: str | None = Field(default=None, description="变量描述")
class ExecutionConfig(BaseModel):
@@ -61,6 +64,13 @@ class TriggerConfig(BaseModel):
config: dict[str, Any] = Field(default_factory=dict, description="触发器配置")
class WorkflowImportSave(BaseModel):
"""工作流导入请求"""
temp_id: str
name: str
description: str
# ==================== 工作流配置 ====================
class WorkflowConfigCreate(BaseModel):
@@ -84,7 +94,7 @@ class WorkflowConfigUpdate(BaseModel):
class WorkflowConfig(BaseModel):
"""工作流配置输出"""
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
app_id: uuid.UUID
nodes: list[dict[str, Any]]
@@ -95,11 +105,11 @@ class WorkflowConfig(BaseModel):
is_active: bool
created_at: datetime.datetime
updated_at: datetime.datetime
@field_serializer("created_at", when_used="json")
def _serialize_created_at(self, dt: datetime.datetime):
return int(dt.timestamp() * 1000) if dt else None
@field_serializer("updated_at", when_used="json")
def _serialize_updated_at(self, dt: datetime.datetime):
return int(dt.timestamp() * 1000) if dt else None
@@ -123,7 +133,8 @@ class WorkflowExecutionResponse(BaseModel):
output_data: dict[str, Any] | None = Field(None, description="所有节点的详细输出数据")
error_message: str | None = Field(None, description="错误信息")
elapsed_time: float | None = Field(None, description="耗时(秒)")
token_usage: dict[str, Any] | None = Field(None, description="Token 使用情况 {prompt_tokens, completion_tokens, total_tokens}")
token_usage: dict[str, Any] | None = Field(None,
description="Token 使用情况 {prompt_tokens, completion_tokens, total_tokens}")
class WorkflowExecutionStreamChunk(BaseModel):
@@ -136,7 +147,7 @@ class WorkflowExecutionStreamChunk(BaseModel):
class WorkflowExecution(BaseModel):
"""工作流执行记录输出"""
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
workflow_config_id: uuid.UUID
app_id: uuid.UUID
@@ -156,15 +167,15 @@ class WorkflowExecution(BaseModel):
token_usage: dict[str, Any] | None
meta_data: dict[str, Any]
created_at: datetime.datetime
@field_serializer("started_at", when_used="json")
def _serialize_started_at(self, dt: datetime.datetime):
return int(dt.timestamp() * 1000) if dt else None
@field_serializer("completed_at", when_used="json")
def _serialize_completed_at(self, dt: datetime.datetime | None):
return int(dt.timestamp() * 1000) if dt else None
@field_serializer("created_at", when_used="json")
def _serialize_created_at(self, dt: datetime.datetime):
return int(dt.timestamp() * 1000) if dt else None
@@ -173,7 +184,7 @@ class WorkflowExecution(BaseModel):
class WorkflowNodeExecution(BaseModel):
"""工作流节点执行记录输出"""
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
execution_id: uuid.UUID
node_id: str
@@ -193,15 +204,15 @@ class WorkflowNodeExecution(BaseModel):
cache_key: str | None
meta_data: dict[str, Any]
created_at: datetime.datetime
@field_serializer("started_at", when_used="json")
def _serialize_started_at(self, dt: datetime.datetime):
return int(dt.timestamp() * 1000) if dt else None
@field_serializer("completed_at", when_used="json")
def _serialize_completed_at(self, dt: datetime.datetime | None):
return int(dt.timestamp() * 1000) if dt else None
@field_serializer("created_at", when_used="json")
def _serialize_created_at(self, dt: datetime.datetime):
return int(dt.timestamp() * 1000) if dt else None

View File

@@ -321,6 +321,26 @@ class AppService:
self.db.add(agent_cfg)
logger.debug("Agent 配置已创建", extra={"app_id": str(app_id)})
def _create_workflow_config(
self,
app_id: uuid.UUID,
data: app_schema.WorkflowConfigCreate,
now: datetime.datetime
):
workflow_cfg = WorkflowConfig(
id=uuid.uuid4(),
app_id=app_id,
nodes=[node.model_dump() for node in data.nodes] if data.nodes else [],
edges=[edge.model_dump() for edge in data.edges] if data.edges else [],
variables=[var.model_dump() for var in data.variables] if data.variables else [],
execution_config=data.execution_config.model_dump() if data.execution_config else {},
triggers=[trigger.model_dump() for trigger in data.triggers] if data.triggers else [],
is_active=True,
created_at=now,
updated_at=now
)
self.db.add(workflow_cfg)
def _create_multi_agent_config(
self,
app_id: uuid.UUID,
@@ -532,6 +552,9 @@ class AppService:
if app.type == "multi_agent" and data.multi_agent_config:
self._create_multi_agent_config(app.id, data.multi_agent_config, now)
if app.type == "workflow" and data.workflow_config:
self._create_workflow_config(app.id, data.workflow_config, now)
self.db.commit()
self.db.refresh(app)
@@ -968,7 +991,7 @@ class AppService:
config = self.db.scalars(stmt).first()
try:
config_memory=config.memory
config_memory = config.memory
if 'memory_content' in config_memory:
config.memory['memory_config_id'] = config.memory.pop('memory_content')
except:
@@ -1189,9 +1212,9 @@ class AppService:
# ==================== 记忆配置提取方法 ====================
def _extract_memory_config_id(
self,
app_type: str,
config: Dict[str, Any]
self,
app_type: str,
config: Dict[str, Any]
) -> Tuple[Optional[uuid.UUID], bool]:
"""从发布配置中提取 memory_config_id委托给 MemoryConfigService
@@ -1205,13 +1228,13 @@ class AppService:
- is_legacy_int: 是否检测到旧格式 int 数据,需要回退到工作空间默认配置
"""
from app.services.memory_config_service import MemoryConfigService
service = MemoryConfigService(self.db)
return service.extract_memory_config_id(app_type, config)
def _get_workspace_default_memory_config_id(
self,
workspace_id: uuid.UUID
self,
workspace_id: uuid.UUID
) -> Optional[uuid.UUID]:
"""获取工作空间的默认记忆配置ID
@@ -1222,22 +1245,22 @@ class AppService:
Optional[uuid.UUID]: 默认记忆配置ID如果不存在则返回 None
"""
from app.services.memory_config_service import MemoryConfigService
service = MemoryConfigService(self.db)
config = service.get_workspace_default_config(workspace_id)
if not config:
logger.warning(
f"工作空间没有可用的记忆配置: workspace_id={workspace_id}"
)
return None
return config.config_id
def _update_endusers_memory_config(
self,
app_id: uuid.UUID,
memory_config_id: uuid.UUID
self,
app_id: uuid.UUID,
memory_config_id: uuid.UUID
) -> int:
"""批量更新应用下所有终端用户的 memory_config_id
@@ -1249,13 +1272,13 @@ class AppService:
int: 更新的终端用户数量
"""
from app.repositories.end_user_repository import EndUserRepository
repo = EndUserRepository(self.db)
updated_count = repo.batch_update_memory_config_id(
app_id=app_id,
memory_config_id=memory_config_id
)
return updated_count
# ==================== 应用发布管理 ====================
@@ -1403,7 +1426,7 @@ class AppService:
# 提取记忆配置ID并更新终端用户
memory_config_id, is_legacy_int = self._extract_memory_config_id(app.type, config)
# 如果检测到旧格式 int 数据,回退到工作空间默认配置
if is_legacy_int and not memory_config_id:
memory_config_id = self._get_workspace_default_memory_config_id(app.workspace_id)
@@ -1412,7 +1435,7 @@ class AppService:
f"发布时使用工作空间默认记忆配置(旧数据兼容): app_id={app_id}, "
f"workspace_id={app.workspace_id}, memory_config_id={memory_config_id}"
)
if memory_config_id:
updated_count = self._update_endusers_memory_config(app_id, memory_config_id)
logger.info(
@@ -1537,7 +1560,7 @@ class AppService:
# 提取记忆配置ID并更新终端用户
memory_config_id, is_legacy_int = self._extract_memory_config_id(release.type, release.config)
# 如果检测到旧格式 int 数据,回退到工作空间默认配置
if is_legacy_int and not memory_config_id:
memory_config_id = self._get_workspace_default_memory_config_id(app.workspace_id)
@@ -1546,7 +1569,7 @@ class AppService:
f"回滚时使用工作空间默认记忆配置(旧数据兼容): app_id={app_id}, "
f"workspace_id={app.workspace_id}, memory_config_id={memory_config_id}"
)
if memory_config_id:
updated_count = self._update_endusers_memory_config(app_id, memory_config_id)
logger.info(

View File

@@ -26,7 +26,7 @@ logger = get_business_logger()
def generate_file_key(
tenant_id: uuid.UUID,
workspace_id: uuid.UUID,
workspace_id: uuid.UUID | None,
file_id: uuid.UUID,
file_ext: str,
) -> str:
@@ -56,8 +56,9 @@ def generate_file_key(
# Ensure file_ext starts with a dot
if file_ext and not file_ext.startswith('.'):
file_ext = f'.{file_ext}'
return f"{tenant_id}/{workspace_id}/{file_id}{file_ext}"
if workspace_id:
return f"{tenant_id}/{workspace_id}/{file_id}{file_ext}"
return f"{tenant_id}/{file_id}{file_ext}"
class FileStorageService:
@@ -96,7 +97,7 @@ class FileStorageService:
async def upload_file(
self,
tenant_id: uuid.UUID,
workspace_id: uuid.UUID,
workspace_id: uuid.UUID | None,
file_id: uuid.UUID,
file_ext: str,
content: bytes,

View File

@@ -0,0 +1,102 @@
# -*- coding: UTF-8 -*-
# Author: Eternity
# @Email: 1533512157@qq.com
# @Time : 2026/2/25 14:39
import json
import uuid
from typing import Any
from sqlalchemy.orm import Session
from app.aioRedis import aio_redis_set, aio_redis_get
from app.core.config import settings
from app.core.exceptions import BusinessException
from app.core.workflow.adapters.base_adapter import WorkflowImportResult, WorkflowParserResult
from app.core.workflow.adapters.errors import UnsupportPlatform, InvalidConfiguration
from app.core.workflow.adapters.registry import PlatformAdapterRegistry
from app.schemas import AppCreate
from app.schemas.workflow_schema import WorkflowConfigCreate
from app.services.app_service import AppService
from app.services.workflow_service import WorkflowService
class WorkflowImportService:
def __init__(self, db: Session):
self.db = db
self.registry = PlatformAdapterRegistry
self.cache_timeout = settings.WORKFLOW_IMPORT_CACHE_TIMEOUT
self.app_service = AppService(db)
self.workflow_service = WorkflowService(db)
async def flush_config(self, temp_id: str, config: WorkflowParserResult):
config_cache = await aio_redis_get(temp_id)
if not config_cache:
raise BusinessException("Workflow configuration has expired. Please re-upload it.")
await aio_redis_set(temp_id, config.model_dump_json(), expire=self.cache_timeout)
async def upload_config(
self,
platform: str,
config: dict[str, Any],
):
if not self.registry.is_supported(platform):
return WorkflowImportResult(
success=False,
temp_id=None,
workflow_id=None,
errors=[UnsupportPlatform(platform=platform)]
)
adapter = self.registry.get_adapter(platform, config)
if not adapter.validate_config():
return WorkflowImportResult(
success=False,
temp_id=None,
workflow_id=None,
errors=[InvalidConfiguration()]
)
workflow_config = adapter.parse_workflow()
temp_id = uuid.uuid4().hex
await aio_redis_set(temp_id, workflow_config.model_dump(), expire=self.cache_timeout)
return WorkflowImportResult(
success=True,
temp_id=temp_id,
workflow_id=None,
edges=workflow_config.edges,
nodes=workflow_config.nodes,
variables=workflow_config.variables,
warnings=workflow_config.warnings,
errors=workflow_config.errors
)
async def save_workflow(
self,
user_id: uuid.UUID,
workspace_id: uuid.UUID,
temp_id: str,
name: str,
description: str | None,
):
config = await aio_redis_get(temp_id)
if config is None:
raise BusinessException("Configuration import timed out. Please try again.")
config = json.loads(config)
app = self.app_service.create_app(
user_id=user_id,
workspace_id=workspace_id,
data=AppCreate(
name=name,
description=description,
type="workflow",
workflow_config=WorkflowConfigCreate(
nodes=config["nodes"],
edges=config["edges"],
variables=config["variables"]
)
)
)
return app

View File

@@ -6,13 +6,16 @@ import logging
import uuid
from typing import Any, Annotated, Optional
import yaml
from fastapi import Depends
from sqlalchemy.orm import Session
from app.core.error_codes import BizCode
from app.core.exceptions import BusinessException
from app.core.workflow.adapters.registry import PlatformAdapterRegistry
from app.core.workflow.validator import validate_workflow_config
from app.db import get_db
from app.models import App
from app.models.workflow_model import WorkflowConfig, WorkflowExecution
from app.repositories.workflow_repository import (
WorkflowConfigRepository,
@@ -38,6 +41,8 @@ class WorkflowService:
self.conversation_service = ConversationService(db)
self.multimodal_service = MultimodalService(db)
self.registry = PlatformAdapterRegistry
# ==================== 配置管理 ====================
def create_workflow_config(
@@ -200,6 +205,32 @@ class WorkflowService:
logger.info(f"删除工作流配置成功: app_id={app_id}, config_id={config.id}")
return True
def export_workflow_dsl(self, app_id: uuid.UUID):
config = self.get_workflow_config(app_id)
if not config:
raise BusinessException(
code=BizCode.NOT_FOUND,
message=f"工作流配置不存在: app_id={app_id}"
)
app: App = config.app
dsl_info = {
"app": {
"name": app.name,
"description": app.description,
"icon": app.icon,
"icon_type": app.icon_type
},
"workflow": {
"variables": config.variables,
"edges": config.edges,
"nodes": config.nodes,
"execution_config": config.execution_config,
"triggers": config.triggers
}
}
return yaml.dump(dsl_info, default_flow_style=False, allow_unicode=True)
def check_config(self, app_id: uuid.UUID) -> WorkflowConfig:
"""检查工作流配置的完整性

View File

@@ -1,4 +1,38 @@
{
"v0.2.4": {
"introduction": {
"codeName": "智远",
"releaseDate": "2026-2-11",
"upgradePosition": "🐻 生产级稳健性升级版本,智慧致远,从容应对复杂场景",
"coreUpgrades": [
"1. Skills 技能框架 🛠️<br>* Skills 支持引入全新的Skills技能系统支持可扩展的能力模块可在Agent和工作流中动态加载与编排",
"2. 多模态与交互 💬<br>* 文件多模态支持全面支持消息输入、LLM处理和输出渲染中的多模态文件处理实现更丰富的媒体感知对话<br>* 语音交互:语音交互功能正在积极开发中,为免提对话体验奠定基础(开发中)",
"3. 知识库集成 📚<br>* 飞书知识库:无缝对接飞书文档库,支持企业知识检索<br>* 语雀知识库:原生连接语雀文档平台,扩展对国内企业工具生态的覆盖<br>* Web站点知识库通用Web站点抓取与索引支持从公开网页内容构建知识库<br>* 视觉模型选择优化知识库视觉模型配置现已支持LLM和Chat两种模型类型移除了此前仅限Chat类型的限制",
"4. 记忆智能 🧠<br>* 本体工程(二期):基于本体工程的高级记忆场景分类与萃取,实现结构化、领域感知的记忆组织,提升分类准确性<br>* 默认模型配置:情绪分析、反思和记忆萃取模块现默认使用空间级模型,确保开箱即用的一致性行为<br>* 智能模型回退:当已配置的情绪或反思模型为空或不可用时,系统自动回退至空间默认模型,避免静默失败<br>* 记忆模型回退兜底:当记忆中配置的模型为空或不可用时,系统优雅降级至空间默认模型",
"5. 性能与扩展 ⚡<br>* 模型并发model_api_keys支持并发模型API Key管理实现并行模型调用提升高负载场景下的吞吐能力",
"6. 稳健性与缺陷修复 🔧<br>* 记忆配置版本固定:修复用户记忆配置未跟随应用版本发布固定的问题,消除跨部署的行为不一致<br>* 空间默认记忆保护:空间级默认记忆配置现不可删除;用户级配置仍可删除<br>* Agent与工作流配置兜底解决Agent和工作流节点中记忆配置可能为空、或已选择但未配置的边界情况——全面的回退处理现可防止运行时错误<br>* 隐形记忆字段重命名将隐形记忆接口JSON响应中的user_id修正为end_user_id与规范数据模型对齐<br>* 记忆配置ID迁移将Agent和工作流记忆配置中的memory_content重命名为memory_config_id保持API一致性<br>* Worker-Memory告警解决解决worker-memory服务中的告警级别问题提升运维监控清晰度<br>* 双语接口修复修复记忆相关API接口的中英文不一致问题<br>* 新用户记忆配置自动回填新创建的EndUser若memory_config_id为None系统自动从最新Release获取memory_config_id并回填<br>* 存量用户记忆配置自动回填已有EndUser若memory_config_id为None系统同样从最新Release获取并回填确保向后兼容无需手动迁移",
"<br>",
"Memory Bear v0.2.4 向生产级稳健性迈进Skills框架与多模态支持开启认知平台新篇章。",
"记忆熊,智慧致远,从容应对真实世界的多样性。🐻✨"
]
},
"introduction_en": {
"codeName": "ZhiYuan",
"releaseDate": "2026-2-11",
"upgradePosition": "🐻 Production-grade resilience release — Wisdom Reaching Far, gracefully handling complex scenarios",
"coreUpgrades": [
"1. Skills Framework 🛠️<br>* Skills Support: Introduced a new Skills system, enabling extensible capability modules that can be dynamically loaded and orchestrated within agents and workflows",
"2. Multimodal & Interaction 💬<br>* File Multimodal Support: Full multimodal file handling across message input, LLM processing, and output rendering — supporting richer, media-aware conversations<br>* Voice Interaction: Voice-based interaction capabilities are under active development, laying the groundwork for hands-free conversational experiences (In Progress)",
"3. Knowledge Base Integration 📚<br>* Feishu Knowledge Base: Seamless integration with Feishu (Lark) document repositories for enterprise knowledge retrieval<br>* Yuque Knowledge Base: Native connector for Yuque documentation platforms, expanding coverage of Chinese enterprise tooling<br>* Web Site Knowledge Base: General-purpose web site crawling and indexing for knowledge base construction from public web content<br>* Visual Model Selection: Knowledge base visual model configuration now supports both LLM and Chat model types, removing the previous restriction to Chat-only selection",
"4. Memory Intelligence 🧠<br>* Ontology Engineering (Phase 2): Advanced memory scene classification and extraction powered by ontology engineering — enabling structured, domain-aware memory organization with improved categorization accuracy<br>* Default Model Configuration: Emotion analysis, reflection, and memory extraction modules now default to the space-level model, ensuring consistent behavior out of the box<br>* Intelligent Model Fallback: If configured emotion or reflection models are empty or unavailable, the system automatically falls back to the space default model — preventing silent failures<br>* Memory Config Fallback for Models: When any memory-configured model is empty or unavailable, the system gracefully degrades to the space default model",
"5. Performance & Scalability ⚡<br>* Model Concurrency (model_api_keys): Support for concurrent model API key management, enabling parallel model invocations and improved throughput for high-load scenarios",
"6. Robustness & Bug Fixes 🔧<br>* Memory Config Version Pinning: Fixed an issue where user memory configurations were not pinned to application release versions, causing inconsistent behavior across deployments<br>* Space Default Memory Protection: Space-level default memory configurations are now protected from deletion; user-level configurations remain deletable<br>* Agent & Workflow Config Fallback: Resolved edge cases in Agent and Workflow nodes where memory config could be empty or selected but unconfigured — comprehensive fallback handling now prevents runtime errors<br>* Implicit Memory Field Rename: Corrected user_id to end_user_id in JSON responses from implicit memory interfaces, aligning with the canonical data model<br>* Memory Config ID Migration: Renamed memory_content to memory_config_id in Agent and Workflow memory configurations for API consistency<br>* Worker-Memory Alerts: Resolved warning-level alerts in the worker-memory service, improving operational monitoring clarity<br>* Bilingual Interface Fixes: Fixed Chinese/English language inconsistencies across memory-related API interfaces<br>* EndUser Memory Config Auto-Backfill (New Users): When a newly created EndUser has memory_config_id as None, the system automatically fetches the latest release's memory_config_id and backfills it<br>* EndUser Memory Config Auto-Backfill (Existing Users): For existing EndUsers with memory_config_id as None, the system similarly retrieves and backfills from the latest release — ensuring backward compatibility without manual migration",
"<br>",
"Memory Bear v0.2.4 advances toward production-grade resilience, with the Skills framework and multimodal support opening a new chapter for the cognitive platform.",
"MemoryBear — Wisdom Reaching Far, gracefully handling real-world variability. 🐻✨"
]
}
},
"v0.2.3": {
"introduction": {
"codeName": "归墟",

View File

@@ -0,0 +1,36 @@
"""202602271020
Revision ID: 7672d8f0f939
Revises: 75e28690ae87
Create Date: 2026-02-27 10:21:46.951584
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '7672d8f0f939'
down_revision: Union[str, None] = '75e28690ae87'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('file_metadata', 'workspace_id',
existing_type=sa.UUID(),
nullable=True,
existing_comment='Workspace ID')
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('file_metadata', 'workspace_id',
existing_type=sa.UUID(),
nullable=False,
existing_comment='Workspace ID')
# ### end Alembic commands ###

View File

@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-25 11:45:07
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-25 11:45:07
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-27 09:59:41
*/
/**
* ChangeEmailModal Component
@@ -114,7 +114,7 @@ const ChangeEmailModal = forwardRef<ChangeEmailModalRef, ChangeEmailModalProps>(
sendEmailCode({ email: values.new_email })
.then(() => {
message.success(t('user.sendSuccess'))
setCountdown(300)
setCountdown(60)
const timer = setInterval(() => {
setCountdown((prev) => {
if (prev <= 1) {

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-06 21:10:56
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-24 17:55:08
* @Last Modified time: 2026-02-27 09:58:30
*/
/**
* Workflow Chat Component
@@ -50,815 +50,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
// State management
const [open, setOpen] = useState(false) // Drawer visibility
const [loading, setLoading] = useState(false) // Send button loading state
const [chatList, setChatList] = useState<ChatItem[]>([
{
"role": "assistant",
"content": "经过多次打磨,最终作品如下:\n《咏一·三题》 \n孤光未凿太初溟 \n一粟吞天万籁宁。 \n影堕千峰青未染 \n心空四象白犹灵。 \n非从烛焰求明性 \n但向尘劳见本形。 \n忽有松风穿石罅 \n泠然吹落满山星。 \n\n注本诗严守平水韵九青部溟、宁、灵、形、星其中“星”属下平声九青部异读字《广韵》息盈切与“灵”“宁”同部古诗常用以协律如王维“清溪流过碧山头空水澄鲜一色秋。隔断红尘三十里白云红叶两悠悠”中“悠”亦借韵通协。全诗紧扣“以一为魂”之旨首句“孤光未凿”化《庄子·应帝王》“浑沌凿七窍而死”典反写太初本明未分之境次句“一粟吞天”以微纳巨承“一芥”而力愈雄浑颔联“青未染”“白犹灵”双色映照暗喻性体离垢绝染而朗然常照颈联直破二边——不假烛焰破外求、不避尘劳破厌离显《坛经》“佛法在世间不离世间觉”之旨结句松风裂石、星落满山是“一”之活泼妙用寂而常照照而恒寂恰如《道德经》“天得一以清地得一以宁”之诗性证成。 \nLLM1结果\n《咏一》 \n孤峰独峙破苍冥 \n一芥微身立太清。 \n万古乾坤凝此数 \n千山雪落只无声。 \n\n注本诗以“一”为魂通过“孤峰”“一芥”“此数”层层递进赋予数字哲思——既写天地间唯一性之壮美孤峰破冥又寓渺小个体与永恒宇宙的辩证芥子纳太清。末句“千山雪落只无声”以大静写大一雪覆千山而声息俱寂暗合《道德经》“天得一以清”之境。平仄依平水韵押九青部冥、清、声。 ",
"created_at": 1771925594511,
"subContent": [
{
"id": "start_1767617465337_0djnmpk2y",
"node_id": "start_1767617465337_0djnmpk2y",
"node_name": "开始Start",
"icon": "/src/assets/images/workflow/start.png",
"content": {
"input": {
"execution_id": "exec_11a80fb1cde148cb",
"conversation_id": "37ee003e-cc53-47e7-930f-a436a1252dd1",
"message": "1",
"conversation_vars": {}
},
"output": {
"message": "1",
"execution_id": "exec_11a80fb1cde148cb",
"conversation_id": "37ee003e-cc53-47e7-930f-a436a1252dd1",
"workspace_id": "d17cd62d-a725-4fc0-813b-1093f2dfdee4",
"user_id": "ab27a27f-072b-47e9-8bbb-1f19322debcd",
"topic": "",
"number": 0,
"Boolean": false
}
},
"status": "completed",
"elapsed_time": 0
},
{
"id": "llm_1767617499720_zvqwjpw3b",
"node_id": "llm_1767617499720_zvqwjpw3b",
"node_name": "大语言模型 (LLM)-初始创作",
"icon": "/src/assets/images/workflow/llm.png",
"content": {
"input": {
"prompt": null,
"messages": [
{
"role": "system",
"content": "请根据1 为主题写一首七字诗。"
}
],
"config": {
"model_id": "2699984d-23be-4817-b81c-c38682a08306",
"temperature": 0.7,
"max_tokens": 2000
}
},
"output": "《咏一》 \n孤峰独峙破苍冥 \n一芥微身立太清。 \n万古乾坤凝此数 \n千山雪落只无声。 \n\n注本诗以“一”为魂通过“孤峰”“一芥”“此数”层层递进赋予数字哲思——既写天地间唯一性之壮美孤峰破冥又寓渺小个体与永恒宇宙的辩证芥子纳太清。末句“千山雪落只无声”以大静写大一雪覆千山而声息俱寂暗合《道德经》“天得一以清”之境。平仄依平水韵押九青部冥、清、声。"
},
"status": "completed",
"elapsed_time": 4.518743515014648
},
{
"id": "loop_1767617552451_hq3j342ha",
"node_id": "loop_1767617552451_hq3j342ha",
"node_name": "循环 (Loop)",
"icon": "/src/assets/images/workflow/loop.png",
"content": {
"input": {
"config": {
"max_loop": 10,
"condition": {
"expressions": [
{
"left": "{{loop_1767617552451_hq3j342ha.round}}",
"right": 3,
"operator": "eq",
"input_type": "Constant"
}
],
"logical_operator": "and"
},
"cycle_vars": [
{
"name": "poem_content",
"type": "string",
"value": "{{llm_1767617499720_zvqwjpw3b.output}}",
"input_type": "variable"
},
{
"name": "round",
"type": "number",
"value": "0",
"input_type": "constant"
}
]
}
},
"output": {
"poem_content": "《咏一·三题》 \n孤光未凿太初溟 \n一粟吞天万籁宁。 \n影堕千峰青未染 \n心空四象白犹灵。 \n非从烛焰求明性 \n但向尘劳见本形。 \n忽有松风穿石罅 \n泠然吹落满山星。 \n\n注本诗严守平水韵九青部溟、宁、灵、形、星其中“星”属下平声九青部异读字《广韵》息盈切与“灵”“宁”同部古诗常用以协律如王维“清溪流过碧山头空水澄鲜一色秋。隔断红尘三十里白云红叶两悠悠”中“悠”亦借韵通协。全诗紧扣“以一为魂”之旨首句“孤光未凿”化《庄子·应帝王》“浑沌凿七窍而死”典反写太初本明未分之境次句“一粟吞天”以微纳巨承“一芥”而力愈雄浑颔联“青未染”“白犹灵”双色映照暗喻性体离垢绝染而朗然常照颈联直破二边——不假烛焰破外求、不避尘劳破厌离显《坛经》“佛法在世间不离世间觉”之旨结句松风裂石、星落满山是“一”之活泼妙用寂而常照照而恒寂恰如《道德经》“天得一以清地得一以宁”之诗性证成。",
"round": 3,
"__child_state": [
{
"messages": [],
"cycle_nodes": [
"loop_1767617552451_hq3j342ha"
],
"looping": 1,
"node_outputs": {
"start_1767617465337_0djnmpk2y": {
"node_id": "start_1767617465337_0djnmpk2y",
"node_type": "start",
"node_name": "开始Start",
"status": "completed",
"input": {
"execution_id": "exec_11a80fb1cde148cb",
"conversation_id": "37ee003e-cc53-47e7-930f-a436a1252dd1",
"message": "1",
"conversation_vars": {}
},
"output": {
"message": "1",
"execution_id": "exec_11a80fb1cde148cb",
"conversation_id": "37ee003e-cc53-47e7-930f-a436a1252dd1",
"workspace_id": "d17cd62d-a725-4fc0-813b-1093f2dfdee4",
"user_id": "ab27a27f-072b-47e9-8bbb-1f19322debcd",
"topic": "",
"number": 0,
"Boolean": false
},
"elapsed_time": 0,
"token_usage": null,
"error": null
},
"llm_1767617499720_zvqwjpw3b": {
"node_id": "llm_1767617499720_zvqwjpw3b",
"node_type": "llm",
"node_name": "大语言模型 (LLM)-初始创作",
"status": "completed",
"input": {
"prompt": null,
"messages": [
{
"role": "system",
"content": "请根据1 为主题写一首七字诗。"
}
],
"config": {
"model_id": "2699984d-23be-4817-b81c-c38682a08306",
"temperature": 0.7,
"max_tokens": 2000
}
},
"output": "《咏一》 \n孤峰独峙破苍冥 \n一芥微身立太清。 \n万古乾坤凝此数 \n千山雪落只无声。 \n\n注本诗以“一”为魂通过“孤峰”“一芥”“此数”层层递进赋予数字哲思——既写天地间唯一性之壮美孤峰破冥又寓渺小个体与永恒宇宙的辩证芥子纳太清。末句“千山雪落只无声”以大静写大一雪覆千山而声息俱寂暗合《道德经》“天得一以清”之境。平仄依平水韵押九青部冥、清、声。",
"elapsed_time": 4.518743515014648,
"token_usage": {
"prompt_tokens": 25,
"completion_tokens": 165,
"total_tokens": 190
},
"error": null
},
"loop_1767617552451_hq3j342ha": {
"poem_content": "《咏一》 \n孤峰独峙破苍冥 \n一芥微身立太清。 \n万古乾坤凝此数 \n千山雪落只无声。 \n\n注本诗以“一”为魂通过“孤峰”“一芥”“此数”层层递进赋予数字哲思——既写天地间唯一性之壮美孤峰破冥又寓渺小个体与永恒宇宙的辩证芥子纳太清。末句“千山雪落只无声”以大静写大一雪覆千山而声息俱寂暗合《道德经》“天得一以清”之境。平仄依平水韵押九青部冥、清、声。",
"round": 0
},
"21046fb8-1f33-45f7-aeda-2c196471f119": {
"node_id": "21046fb8-1f33-45f7-aeda-2c196471f119",
"node_type": "cycle-start",
"node_name": null,
"status": "completed",
"input": {
"execution_id": "exec_11a80fb1cde148cb",
"conversation_id": "37ee003e-cc53-47e7-930f-a436a1252dd1",
"message": "1",
"conversation_vars": {}
},
"output": {
"message": "1",
"execution_id": "exec_11a80fb1cde148cb",
"conversation_id": "37ee003e-cc53-47e7-930f-a436a1252dd1",
"workspace_id": "d17cd62d-a725-4fc0-813b-1093f2dfdee4",
"user_id": "ab27a27f-072b-47e9-8bbb-1f19322debcd"
},
"elapsed_time": 0.0005278587341308594,
"token_usage": null,
"error": null
},
"llm_1767617560401_bsx1vhi25": {
"node_id": "llm_1767617560401_bsx1vhi25",
"node_type": "llm",
"node_name": "大语言模型 (LLM)-润色器",
"status": "completed",
"input": {
"prompt": null,
"messages": [
{
"role": "system",
"content": "请根据《咏一》 \n孤峰独峙破苍冥 \n一芥微身立太清。 \n万古乾坤凝此数 \n千山雪落只无声。 \n\n注本诗以“一”为魂通过“孤峰”“一芥”“此数”层层递进赋予数字哲思——既写天地间唯一性之壮美孤峰破冥又寓渺小个体与永恒宇宙的辩证芥子纳太清。末句“千山雪落只无声”以大静写大一雪覆千山而声息俱寂暗合《道德经》“天得一以清”之境。平仄依平水韵押九青部冥、清、声。 为主题写一首七字诗。"
}
],
"config": {
"model_id": "2699984d-23be-4817-b81c-c38682a08306",
"temperature": 0.7,
"max_tokens": 2000
}
},
"output": "《咏一·次韵》 \n千峰削玉立空青 \n一羽浮天亦自宁。 \n万籁收声归太始 \n孤光未堕即长明。 \n\n注本诗承原作“以一为魂”之旨严守平水韵九青部青、宁、明平仄谐律。首句“千峰削玉”反衬“一羽浮天”以极繁托极简次句“一羽”既承“一芥”之微更取《庄子》“鹏徙南冥”之逸气言至微者亦可持守本然之宁。三句“万籁收声”暗应原作“千山雪落只无声”而升华为宇宙初开的“太始”静界结句“孤光未堕即长明”化用《淮南子》“日月不为明而明”与禅宗“一念不生即佛”昭示“一”非寂灭之空乃不假外求、本自圆成的永恒觉性——此即《道德经》“天得一以清”的诗性证悟。",
"elapsed_time": 6.8497374057769775,
"token_usage": {
"prompt_tokens": 188,
"completion_tokens": 262,
"total_tokens": 450
},
"error": null
},
"assigner_1768285417545_qsoqleflh": {
"node_id": "assigner_1768285417545_qsoqleflh",
"node_type": "assigner",
"node_name": "变量赋值",
"status": "completed",
"input": {
"config": {
"assignments": [
{
"value": "{{llm_1767617560401_bsx1vhi25.output}}",
"operation": "cover",
"variable_selector": "{{loop_1767617552451_hq3j342ha.poem_content}}"
},
{
"value": 1,
"operation": "add",
"variable_selector": "{{loop_1767617552451_hq3j342ha.round}}"
}
]
}
},
"output": null,
"elapsed_time": 0.0003705024719238281,
"token_usage": null,
"error": null
}
},
"execution_id": "exec_11a80fb1cde148cb",
"workspace_id": "d17cd62d-a725-4fc0-813b-1093f2dfdee4",
"user_id": "ab27a27f-072b-47e9-8bbb-1f19322debcd",
"error": null,
"error_node": null,
"activate": {
"llm_1767617560401_bsx1vhi25": true,
"loop_1767617552451_hq3j342ha": true,
"start_1767617465337_0djnmpk2y": true,
"21046fb8-1f33-45f7-aeda-2c196471f119": true,
"llm_1767617499720_zvqwjpw3b": true,
"assigner_1768285417545_qsoqleflh": true
}
},
{
"messages": [],
"cycle_nodes": [
"loop_1767617552451_hq3j342ha"
],
"looping": 1,
"node_outputs": {
"start_1767617465337_0djnmpk2y": {
"node_id": "start_1767617465337_0djnmpk2y",
"node_type": "start",
"node_name": "开始Start",
"status": "completed",
"input": {
"execution_id": "exec_11a80fb1cde148cb",
"conversation_id": "37ee003e-cc53-47e7-930f-a436a1252dd1",
"message": "1",
"conversation_vars": {}
},
"output": {
"message": "1",
"execution_id": "exec_11a80fb1cde148cb",
"conversation_id": "37ee003e-cc53-47e7-930f-a436a1252dd1",
"workspace_id": "d17cd62d-a725-4fc0-813b-1093f2dfdee4",
"user_id": "ab27a27f-072b-47e9-8bbb-1f19322debcd",
"topic": "",
"number": 0,
"Boolean": false
},
"elapsed_time": 0,
"token_usage": null,
"error": null
},
"llm_1767617499720_zvqwjpw3b": {
"node_id": "llm_1767617499720_zvqwjpw3b",
"node_type": "llm",
"node_name": "大语言模型 (LLM)-初始创作",
"status": "completed",
"input": {
"prompt": null,
"messages": [
{
"role": "system",
"content": "请根据1 为主题写一首七字诗。"
}
],
"config": {
"model_id": "2699984d-23be-4817-b81c-c38682a08306",
"temperature": 0.7,
"max_tokens": 2000
}
},
"output": "《咏一》 \n孤峰独峙破苍冥 \n一芥微身立太清。 \n万古乾坤凝此数 \n千山雪落只无声。 \n\n注本诗以“一”为魂通过“孤峰”“一芥”“此数”层层递进赋予数字哲思——既写天地间唯一性之壮美孤峰破冥又寓渺小个体与永恒宇宙的辩证芥子纳太清。末句“千山雪落只无声”以大静写大一雪覆千山而声息俱寂暗合《道德经》“天得一以清”之境。平仄依平水韵押九青部冥、清、声。",
"elapsed_time": 4.518743515014648,
"token_usage": {
"prompt_tokens": 25,
"completion_tokens": 165,
"total_tokens": 190
},
"error": null
},
"loop_1767617552451_hq3j342ha": {
"poem_content": "《咏一·次韵》 \n千峰削玉立空青 \n一羽浮天亦自宁。 \n万籁收声归太始 \n孤光未堕即长明。 \n\n注本诗承原作“以一为魂”之旨严守平水韵九青部青、宁、明平仄谐律。首句“千峰削玉”反衬“一羽浮天”以极繁托极简次句“一羽”既承“一芥”之微更取《庄子》“鹏徙南冥”之逸气言至微者亦可持守本然之宁。三句“万籁收声”暗应原作“千山雪落只无声”而升华为宇宙初开的“太始”静界结句“孤光未堕即长明”化用《淮南子》“日月不为明而明”与禅宗“一念不生即佛”昭示“一”非寂灭之空乃不假外求、本自圆成的永恒觉性——此即《道德经》“天得一以清”的诗性证悟。",
"round": 1
},
"21046fb8-1f33-45f7-aeda-2c196471f119": {
"node_id": "21046fb8-1f33-45f7-aeda-2c196471f119",
"node_type": "cycle-start",
"node_name": null,
"status": "completed",
"input": {
"execution_id": "exec_11a80fb1cde148cb",
"conversation_id": "37ee003e-cc53-47e7-930f-a436a1252dd1",
"message": "1",
"conversation_vars": {}
},
"output": {
"message": "1",
"execution_id": "exec_11a80fb1cde148cb",
"conversation_id": "37ee003e-cc53-47e7-930f-a436a1252dd1",
"workspace_id": "d17cd62d-a725-4fc0-813b-1093f2dfdee4",
"user_id": "ab27a27f-072b-47e9-8bbb-1f19322debcd"
},
"elapsed_time": 0,
"token_usage": null,
"error": null
},
"llm_1767617560401_bsx1vhi25": {
"node_id": "llm_1767617560401_bsx1vhi25",
"node_type": "llm",
"node_name": "大语言模型 (LLM)-润色器",
"status": "completed",
"input": {
"prompt": null,
"messages": [
{
"role": "system",
"content": "请根据《咏一·次韵》 \n千峰削玉立空青 \n一羽浮天亦自宁。 \n万籁收声归太始 \n孤光未堕即长明。 \n\n注本诗承原作“以一为魂”之旨严守平水韵九青部青、宁、明平仄谐律。首句“千峰削玉”反衬“一羽浮天”以极繁托极简次句“一羽”既承“一芥”之微更取《庄子》“鹏徙南冥”之逸气言至微者亦可持守本然之宁。三句“万籁收声”暗应原作“千山雪落只无声”而升华为宇宙初开的“太始”静界结句“孤光未堕即长明”化用《淮南子》“日月不为明而明”与禅宗“一念不生即佛”昭示“一”非寂灭之空乃不假外求、本自圆成的永恒觉性——此即《道德经》“天得一以清”的诗性证悟。 为主题写一首七字诗。"
}
],
"config": {
"model_id": "2699984d-23be-4817-b81c-c38682a08306",
"temperature": 0.7,
"max_tokens": 2000
}
},
"output": "《咏一·再题》 \n一芥浮空万籁停 \n千峰影落太初青。 \n光非燃烛恒明在 \n心不沾尘即性灵。 \n\n注本诗续写“以一为魂”之旨严守平水韵九青部停、青、灵平仄精严。首句“一芥”承原作微渺意象而“万籁停”较“收声”更显寂然自定之境次句倒装“千峰影落”使苍茫山势如墨痕沉入宇宙初青暗契《淮南子》“虚霩生宇宙宇宙生气”之太始气象。三句翻出新境“光非燃烛”破除对光明之形器执取直指《楞严经》“性觉妙明本觉明妙”之不假缘起的本明结句“心不沾尘即性灵”化用六祖“本来无一物”与程颢“天地之大德曰生”言“一”非枯寂之数乃活泼泼的性灵朗现——此即《道德经》“昔之得一者天清地宁”的诗性澄明。",
"elapsed_time": 7.1851232051849365,
"token_usage": {
"prompt_tokens": 285,
"completion_tokens": 281,
"total_tokens": 566
},
"error": null
},
"assigner_1768285417545_qsoqleflh": {
"node_id": "assigner_1768285417545_qsoqleflh",
"node_type": "assigner",
"node_name": "变量赋值",
"status": "completed",
"input": {
"config": {
"assignments": [
{
"value": "{{llm_1767617560401_bsx1vhi25.output}}",
"operation": "cover",
"variable_selector": "{{loop_1767617552451_hq3j342ha.poem_content}}"
},
{
"value": 1,
"operation": "add",
"variable_selector": "{{loop_1767617552451_hq3j342ha.round}}"
}
]
}
},
"output": null,
"elapsed_time": 0,
"token_usage": null,
"error": null
}
},
"execution_id": "exec_11a80fb1cde148cb",
"workspace_id": "d17cd62d-a725-4fc0-813b-1093f2dfdee4",
"user_id": "ab27a27f-072b-47e9-8bbb-1f19322debcd",
"error": null,
"error_node": null,
"activate": {
"llm_1767617560401_bsx1vhi25": true,
"start_1767617465337_0djnmpk2y": true,
"loop_1767617552451_hq3j342ha": true,
"21046fb8-1f33-45f7-aeda-2c196471f119": true,
"llm_1767617499720_zvqwjpw3b": true,
"assigner_1768285417545_qsoqleflh": true
}
},
{
"messages": [],
"cycle_nodes": [
"loop_1767617552451_hq3j342ha"
],
"looping": 1,
"node_outputs": {
"start_1767617465337_0djnmpk2y": {
"node_id": "start_1767617465337_0djnmpk2y",
"node_type": "start",
"node_name": "开始Start",
"status": "completed",
"input": {
"execution_id": "exec_11a80fb1cde148cb",
"conversation_id": "37ee003e-cc53-47e7-930f-a436a1252dd1",
"message": "1",
"conversation_vars": {}
},
"output": {
"message": "1",
"execution_id": "exec_11a80fb1cde148cb",
"conversation_id": "37ee003e-cc53-47e7-930f-a436a1252dd1",
"workspace_id": "d17cd62d-a725-4fc0-813b-1093f2dfdee4",
"user_id": "ab27a27f-072b-47e9-8bbb-1f19322debcd",
"topic": "",
"number": 0,
"Boolean": false
},
"elapsed_time": 0,
"token_usage": null,
"error": null
},
"llm_1767617499720_zvqwjpw3b": {
"node_id": "llm_1767617499720_zvqwjpw3b",
"node_type": "llm",
"node_name": "大语言模型 (LLM)-初始创作",
"status": "completed",
"input": {
"prompt": null,
"messages": [
{
"role": "system",
"content": "请根据1 为主题写一首七字诗。"
}
],
"config": {
"model_id": "2699984d-23be-4817-b81c-c38682a08306",
"temperature": 0.7,
"max_tokens": 2000
}
},
"output": "《咏一》 \n孤峰独峙破苍冥 \n一芥微身立太清。 \n万古乾坤凝此数 \n千山雪落只无声。 \n\n注本诗以“一”为魂通过“孤峰”“一芥”“此数”层层递进赋予数字哲思——既写天地间唯一性之壮美孤峰破冥又寓渺小个体与永恒宇宙的辩证芥子纳太清。末句“千山雪落只无声”以大静写大一雪覆千山而声息俱寂暗合《道德经》“天得一以清”之境。平仄依平水韵押九青部冥、清、声。",
"elapsed_time": 4.518743515014648,
"token_usage": {
"prompt_tokens": 25,
"completion_tokens": 165,
"total_tokens": 190
},
"error": null
},
"loop_1767617552451_hq3j342ha": {
"poem_content": "《咏一·再题》 \n一芥浮空万籁停 \n千峰影落太初青。 \n光非燃烛恒明在 \n心不沾尘即性灵。 \n\n注本诗续写“以一为魂”之旨严守平水韵九青部停、青、灵平仄精严。首句“一芥”承原作微渺意象而“万籁停”较“收声”更显寂然自定之境次句倒装“千峰影落”使苍茫山势如墨痕沉入宇宙初青暗契《淮南子》“虚霩生宇宙宇宙生气”之太始气象。三句翻出新境“光非燃烛”破除对光明之形器执取直指《楞严经》“性觉妙明本觉明妙”之不假缘起的本明结句“心不沾尘即性灵”化用六祖“本来无一物”与程颢“天地之大德曰生”言“一”非枯寂之数乃活泼泼的性灵朗现——此即《道德经》“昔之得一者天清地宁”的诗性澄明。",
"round": 2
},
"21046fb8-1f33-45f7-aeda-2c196471f119": {
"node_id": "21046fb8-1f33-45f7-aeda-2c196471f119",
"node_type": "cycle-start",
"node_name": null,
"status": "completed",
"input": {
"execution_id": "exec_11a80fb1cde148cb",
"conversation_id": "37ee003e-cc53-47e7-930f-a436a1252dd1",
"message": "1",
"conversation_vars": {}
},
"output": {
"message": "1",
"execution_id": "exec_11a80fb1cde148cb",
"conversation_id": "37ee003e-cc53-47e7-930f-a436a1252dd1",
"workspace_id": "d17cd62d-a725-4fc0-813b-1093f2dfdee4",
"user_id": "ab27a27f-072b-47e9-8bbb-1f19322debcd"
},
"elapsed_time": 0,
"token_usage": null,
"error": null
},
"llm_1767617560401_bsx1vhi25": {
"node_id": "llm_1767617560401_bsx1vhi25",
"node_type": "llm",
"node_name": "大语言模型 (LLM)-润色器",
"status": "completed",
"input": {
"prompt": null,
"messages": [
{
"role": "system",
"content": "请根据《咏一·再题》 \n一芥浮空万籁停 \n千峰影落太初青。 \n光非燃烛恒明在 \n心不沾尘即性灵。 \n\n注本诗续写“以一为魂”之旨严守平水韵九青部停、青、灵平仄精严。首句“一芥”承原作微渺意象而“万籁停”较“收声”更显寂然自定之境次句倒装“千峰影落”使苍茫山势如墨痕沉入宇宙初青暗契《淮南子》“虚霩生宇宙宇宙生气”之太始气象。三句翻出新境“光非燃烛”破除对光明之形器执取直指《楞严经》“性觉妙明本觉明妙”之不假缘起的本明结句“心不沾尘即性灵”化用六祖“本来无一物”与程颢“天地之大德曰生”言“一”非枯寂之数乃活泼泼的性灵朗现——此即《道德经》“昔之得一者天清地宁”的诗性澄明。 为主题写一首七字诗。"
}
],
"config": {
"model_id": "2699984d-23be-4817-b81c-c38682a08306",
"temperature": 0.7,
"max_tokens": 2000
}
},
"output": "《咏一·三题》 \n孤光未凿太初溟 \n一粟吞天万籁宁。 \n影堕千峰青未染 \n心空四象白犹灵。 \n非从烛焰求明性 \n但向尘劳见本形。 \n忽有松风穿石罅 \n泠然吹落满山星。 \n\n注本诗严守平水韵九青部溟、宁、灵、形、星其中“星”属下平声九青部异读字《广韵》息盈切与“灵”“宁”同部古诗常用以协律如王维“清溪流过碧山头空水澄鲜一色秋。隔断红尘三十里白云红叶两悠悠”中“悠”亦借韵通协。全诗紧扣“以一为魂”之旨首句“孤光未凿”化《庄子·应帝王》“浑沌凿七窍而死”典反写太初本明未分之境次句“一粟吞天”以微纳巨承“一芥”而力愈雄浑颔联“青未染”“白犹灵”双色映照暗喻性体离垢绝染而朗然常照颈联直破二边——不假烛焰破外求、不避尘劳破厌离显《坛经》“佛法在世间不离世间觉”之旨结句松风裂石、星落满山是“一”之活泼妙用寂而常照照而恒寂恰如《道德经》“天得一以清地得一以宁”之诗性证成。",
"elapsed_time": 9.531717538833618,
"token_usage": {
"prompt_tokens": 304,
"completion_tokens": 390,
"total_tokens": 694
},
"error": null
},
"assigner_1768285417545_qsoqleflh": {
"node_id": "assigner_1768285417545_qsoqleflh",
"node_type": "assigner",
"node_name": "变量赋值",
"status": "completed",
"input": {
"config": {
"assignments": [
{
"value": "{{llm_1767617560401_bsx1vhi25.output}}",
"operation": "cover",
"variable_selector": "{{loop_1767617552451_hq3j342ha.poem_content}}"
},
{
"value": 1,
"operation": "add",
"variable_selector": "{{loop_1767617552451_hq3j342ha.round}}"
}
]
}
},
"output": null,
"elapsed_time": 0,
"token_usage": null,
"error": null
}
},
"execution_id": "exec_11a80fb1cde148cb",
"workspace_id": "d17cd62d-a725-4fc0-813b-1093f2dfdee4",
"user_id": "ab27a27f-072b-47e9-8bbb-1f19322debcd",
"error": null,
"error_node": null,
"activate": {
"llm_1767617560401_bsx1vhi25": true,
"start_1767617465337_0djnmpk2y": true,
"loop_1767617552451_hq3j342ha": true,
"21046fb8-1f33-45f7-aeda-2c196471f119": true,
"llm_1767617499720_zvqwjpw3b": true,
"assigner_1768285417545_qsoqleflh": true
}
}
]
}
},
"subContent": [
{
"cycle_idx": 0,
"node_id": "21046fb8-1f33-45f7-aeda-2c196471f119",
"node_name": null,
"icon": "/src/assets/images/workflow/loop.png",
"content": {
"cycle_idx": 0,
"input": {
"poem_content": "《咏一》 \n孤峰独峙破苍冥 \n一芥微身立太清。 \n万古乾坤凝此数 \n千山雪落只无声。 \n\n注本诗以“一”为魂通过“孤峰”“一芥”“此数”层层递进赋予数字哲思——既写天地间唯一性之壮美孤峰破冥又寓渺小个体与永恒宇宙的辩证芥子纳太清。末句“千山雪落只无声”以大静写大一雪覆千山而声息俱寂暗合《道德经》“天得一以清”之境。平仄依平水韵押九青部冥、清、声。",
"round": 0
},
"output": {
"poem_content": "《咏一》 \n孤峰独峙破苍冥 \n一芥微身立太清。 \n万古乾坤凝此数 \n千山雪落只无声。 \n\n注本诗以“一”为魂通过“孤峰”“一芥”“此数”层层递进赋予数字哲思——既写天地间唯一性之壮美孤峰破冥又寓渺小个体与永恒宇宙的辩证芥子纳太清。末句“千山雪落只无声”以大静写大一雪覆千山而声息俱寂暗合《道德经》“天得一以清”之境。平仄依平水韵押九青部冥、清、声。",
"round": 0
}
},
"status": "completed",
"elapsed_time": 0.0005278587341308594
},
{
"cycle_idx": 0,
"node_id": "llm_1767617560401_bsx1vhi25",
"node_name": "大语言模型 (LLM)-润色器",
"icon": "/src/assets/images/workflow/llm.png",
"content": {
"cycle_idx": 0,
"input": {
"prompt": null,
"messages": [
{
"role": "system",
"content": "请根据《咏一》 \n孤峰独峙破苍冥 \n一芥微身立太清。 \n万古乾坤凝此数 \n千山雪落只无声。 \n\n注本诗以“一”为魂通过“孤峰”“一芥”“此数”层层递进赋予数字哲思——既写天地间唯一性之壮美孤峰破冥又寓渺小个体与永恒宇宙的辩证芥子纳太清。末句“千山雪落只无声”以大静写大一雪覆千山而声息俱寂暗合《道德经》“天得一以清”之境。平仄依平水韵押九青部冥、清、声。 为主题写一首七字诗。"
}
],
"config": {
"model_id": "2699984d-23be-4817-b81c-c38682a08306",
"temperature": 0.7,
"max_tokens": 2000
}
},
"output": "《咏一·次韵》 \n千峰削玉立空青 \n一羽浮天亦自宁。 \n万籁收声归太始 \n孤光未堕即长明。 \n\n注本诗承原作“以一为魂”之旨严守平水韵九青部青、宁、明平仄谐律。首句“千峰削玉”反衬“一羽浮天”以极繁托极简次句“一羽”既承“一芥”之微更取《庄子》“鹏徙南冥”之逸气言至微者亦可持守本然之宁。三句“万籁收声”暗应原作“千山雪落只无声”而升华为宇宙初开的“太始”静界结句“孤光未堕即长明”化用《淮南子》“日月不为明而明”与禅宗“一念不生即佛”昭示“一”非寂灭之空乃不假外求、本自圆成的永恒觉性——此即《道德经》“天得一以清”的诗性证悟。"
},
"status": "completed",
"elapsed_time": 6.8497374057769775
},
{
"cycle_idx": 0,
"node_id": "assigner_1768285417545_qsoqleflh",
"node_name": "变量赋值",
"icon": "/src/assets/images/workflow/assigner.png",
"content": {
"cycle_idx": 0,
"input": {
"config": {
"assignments": [
{
"value": "{{llm_1767617560401_bsx1vhi25.output}}",
"operation": "cover",
"variable_selector": "{{loop_1767617552451_hq3j342ha.poem_content}}"
},
{
"value": 1,
"operation": "add",
"variable_selector": "{{loop_1767617552451_hq3j342ha.round}}"
}
]
}
},
"output": null
},
"status": "completed",
"elapsed_time": 0.0003705024719238281
},
{
"cycle_idx": 1,
"node_id": "21046fb8-1f33-45f7-aeda-2c196471f119",
"node_name": null,
"icon": "/src/assets/images/workflow/loop.png",
"content": {
"cycle_idx": 1,
"input": {
"poem_content": "《咏一·次韵》 \n千峰削玉立空青 \n一羽浮天亦自宁。 \n万籁收声归太始 \n孤光未堕即长明。 \n\n注本诗承原作“以一为魂”之旨严守平水韵九青部青、宁、明平仄谐律。首句“千峰削玉”反衬“一羽浮天”以极繁托极简次句“一羽”既承“一芥”之微更取《庄子》“鹏徙南冥”之逸气言至微者亦可持守本然之宁。三句“万籁收声”暗应原作“千山雪落只无声”而升华为宇宙初开的“太始”静界结句“孤光未堕即长明”化用《淮南子》“日月不为明而明”与禅宗“一念不生即佛”昭示“一”非寂灭之空乃不假外求、本自圆成的永恒觉性——此即《道德经》“天得一以清”的诗性证悟。",
"round": 1
},
"output": {
"poem_content": "《咏一·次韵》 \n千峰削玉立空青 \n一羽浮天亦自宁。 \n万籁收声归太始 \n孤光未堕即长明。 \n\n注本诗承原作“以一为魂”之旨严守平水韵九青部青、宁、明平仄谐律。首句“千峰削玉”反衬“一羽浮天”以极繁托极简次句“一羽”既承“一芥”之微更取《庄子》“鹏徙南冥”之逸气言至微者亦可持守本然之宁。三句“万籁收声”暗应原作“千山雪落只无声”而升华为宇宙初开的“太始”静界结句“孤光未堕即长明”化用《淮南子》“日月不为明而明”与禅宗“一念不生即佛”昭示“一”非寂灭之空乃不假外求、本自圆成的永恒觉性——此即《道德经》“天得一以清”的诗性证悟。",
"round": 1
}
},
"status": "completed",
"elapsed_time": 0
},
{
"cycle_idx": 1,
"node_id": "llm_1767617560401_bsx1vhi25",
"node_name": "大语言模型 (LLM)-润色器",
"icon": "/src/assets/images/workflow/llm.png",
"content": {
"cycle_idx": 1,
"input": {
"prompt": null,
"messages": [
{
"role": "system",
"content": "请根据《咏一·次韵》 \n千峰削玉立空青 \n一羽浮天亦自宁。 \n万籁收声归太始 \n孤光未堕即长明。 \n\n注本诗承原作“以一为魂”之旨严守平水韵九青部青、宁、明平仄谐律。首句“千峰削玉”反衬“一羽浮天”以极繁托极简次句“一羽”既承“一芥”之微更取《庄子》“鹏徙南冥”之逸气言至微者亦可持守本然之宁。三句“万籁收声”暗应原作“千山雪落只无声”而升华为宇宙初开的“太始”静界结句“孤光未堕即长明”化用《淮南子》“日月不为明而明”与禅宗“一念不生即佛”昭示“一”非寂灭之空乃不假外求、本自圆成的永恒觉性——此即《道德经》“天得一以清”的诗性证悟。 为主题写一首七字诗。"
}
],
"config": {
"model_id": "2699984d-23be-4817-b81c-c38682a08306",
"temperature": 0.7,
"max_tokens": 2000
}
},
"output": "《咏一·再题》 \n一芥浮空万籁停 \n千峰影落太初青。 \n光非燃烛恒明在 \n心不沾尘即性灵。 \n\n注本诗续写“以一为魂”之旨严守平水韵九青部停、青、灵平仄精严。首句“一芥”承原作微渺意象而“万籁停”较“收声”更显寂然自定之境次句倒装“千峰影落”使苍茫山势如墨痕沉入宇宙初青暗契《淮南子》“虚霩生宇宙宇宙生气”之太始气象。三句翻出新境“光非燃烛”破除对光明之形器执取直指《楞严经》“性觉妙明本觉明妙”之不假缘起的本明结句“心不沾尘即性灵”化用六祖“本来无一物”与程颢“天地之大德曰生”言“一”非枯寂之数乃活泼泼的性灵朗现——此即《道德经》“昔之得一者天清地宁”的诗性澄明。"
},
"status": "completed",
"elapsed_time": 7.1851232051849365
},
{
"cycle_idx": 1,
"node_id": "assigner_1768285417545_qsoqleflh",
"node_name": "变量赋值",
"icon": "/src/assets/images/workflow/assigner.png",
"content": {
"cycle_idx": 1,
"input": {
"config": {
"assignments": [
{
"value": "{{llm_1767617560401_bsx1vhi25.output}}",
"operation": "cover",
"variable_selector": "{{loop_1767617552451_hq3j342ha.poem_content}}"
},
{
"value": 1,
"operation": "add",
"variable_selector": "{{loop_1767617552451_hq3j342ha.round}}"
}
]
}
},
"output": null
},
"status": "completed",
"elapsed_time": 0
},
{
"cycle_idx": 2,
"node_id": "21046fb8-1f33-45f7-aeda-2c196471f119",
"node_name": null,
"icon": "/src/assets/images/workflow/loop.png",
"content": {
"cycle_idx": 2,
"input": {
"poem_content": "《咏一·再题》 \n一芥浮空万籁停 \n千峰影落太初青。 \n光非燃烛恒明在 \n心不沾尘即性灵。 \n\n注本诗续写“以一为魂”之旨严守平水韵九青部停、青、灵平仄精严。首句“一芥”承原作微渺意象而“万籁停”较“收声”更显寂然自定之境次句倒装“千峰影落”使苍茫山势如墨痕沉入宇宙初青暗契《淮南子》“虚霩生宇宙宇宙生气”之太始气象。三句翻出新境“光非燃烛”破除对光明之形器执取直指《楞严经》“性觉妙明本觉明妙”之不假缘起的本明结句“心不沾尘即性灵”化用六祖“本来无一物”与程颢“天地之大德曰生”言“一”非枯寂之数乃活泼泼的性灵朗现——此即《道德经》“昔之得一者天清地宁”的诗性澄明。",
"round": 2
},
"output": {
"poem_content": "《咏一·再题》 \n一芥浮空万籁停 \n千峰影落太初青。 \n光非燃烛恒明在 \n心不沾尘即性灵。 \n\n注本诗续写“以一为魂”之旨严守平水韵九青部停、青、灵平仄精严。首句“一芥”承原作微渺意象而“万籁停”较“收声”更显寂然自定之境次句倒装“千峰影落”使苍茫山势如墨痕沉入宇宙初青暗契《淮南子》“虚霩生宇宙宇宙生气”之太始气象。三句翻出新境“光非燃烛”破除对光明之形器执取直指《楞严经》“性觉妙明本觉明妙”之不假缘起的本明结句“心不沾尘即性灵”化用六祖“本来无一物”与程颢“天地之大德曰生”言“一”非枯寂之数乃活泼泼的性灵朗现——此即《道德经》“昔之得一者天清地宁”的诗性澄明。",
"round": 2
}
},
"status": "completed",
"elapsed_time": 0
},
{
"cycle_idx": 2,
"node_id": "llm_1767617560401_bsx1vhi25",
"node_name": "大语言模型 (LLM)-润色器",
"icon": "/src/assets/images/workflow/llm.png",
"content": {
"cycle_idx": 2,
"input": {
"prompt": null,
"messages": [
{
"role": "system",
"content": "请根据《咏一·再题》 \n一芥浮空万籁停 \n千峰影落太初青。 \n光非燃烛恒明在 \n心不沾尘即性灵。 \n\n注本诗续写“以一为魂”之旨严守平水韵九青部停、青、灵平仄精严。首句“一芥”承原作微渺意象而“万籁停”较“收声”更显寂然自定之境次句倒装“千峰影落”使苍茫山势如墨痕沉入宇宙初青暗契《淮南子》“虚霩生宇宙宇宙生气”之太始气象。三句翻出新境“光非燃烛”破除对光明之形器执取直指《楞严经》“性觉妙明本觉明妙”之不假缘起的本明结句“心不沾尘即性灵”化用六祖“本来无一物”与程颢“天地之大德曰生”言“一”非枯寂之数乃活泼泼的性灵朗现——此即《道德经》“昔之得一者天清地宁”的诗性澄明。 为主题写一首七字诗。"
}
],
"config": {
"model_id": "2699984d-23be-4817-b81c-c38682a08306",
"temperature": 0.7,
"max_tokens": 2000
}
},
"output": "《咏一·三题》 \n孤光未凿太初溟 \n一粟吞天万籁宁。 \n影堕千峰青未染 \n心空四象白犹灵。 \n非从烛焰求明性 \n但向尘劳见本形。 \n忽有松风穿石罅 \n泠然吹落满山星。 \n\n注本诗严守平水韵九青部溟、宁、灵、形、星其中“星”属下平声九青部异读字《广韵》息盈切与“灵”“宁”同部古诗常用以协律如王维“清溪流过碧山头空水澄鲜一色秋。隔断红尘三十里白云红叶两悠悠”中“悠”亦借韵通协。全诗紧扣“以一为魂”之旨首句“孤光未凿”化《庄子·应帝王》“浑沌凿七窍而死”典反写太初本明未分之境次句“一粟吞天”以微纳巨承“一芥”而力愈雄浑颔联“青未染”“白犹灵”双色映照暗喻性体离垢绝染而朗然常照颈联直破二边——不假烛焰破外求、不避尘劳破厌离显《坛经》“佛法在世间不离世间觉”之旨结句松风裂石、星落满山是“一”之活泼妙用寂而常照照而恒寂恰如《道德经》“天得一以清地得一以宁”之诗性证成。"
},
"status": "completed",
"elapsed_time": 9.531717538833618
},
{
"cycle_idx": 2,
"node_id": "assigner_1768285417545_qsoqleflh",
"node_name": "变量赋值",
"icon": "/src/assets/images/workflow/assigner.png",
"content": {
"cycle_idx": 2,
"input": {
"config": {
"assignments": [
{
"value": "{{llm_1767617560401_bsx1vhi25.output}}",
"operation": "cover",
"variable_selector": "{{loop_1767617552451_hq3j342ha.poem_content}}"
},
{
"value": 1,
"operation": "add",
"variable_selector": "{{loop_1767617552451_hq3j342ha.round}}"
}
]
}
},
"output": null
},
"status": "completed",
"elapsed_time": 0
}
],
"status": "completed",
"elapsed_time": 23.57662582397461
},
{
"id": "end_1767619139811_ko97mb12l",
"node_id": "end_1767619139811_ko97mb12l",
"node_name": "结束End",
"icon": "/src/assets/images/workflow/end.png",
"content": {
"input": {
"config": {
"output": "经过多次打磨,最终作品如下:\n{{loop_1767617552451_hq3j342ha.poem_content}} \nLLM1结果\n{{llm_1767617499720_zvqwjpw3b.output}} "
}
},
"output": "经过多次打磨,最终作品如下:\n《咏一·三题》 \n孤光未凿太初溟 \n一粟吞天万籁宁。 \n影堕千峰青未染 \n心空四象白犹灵。 \n非从烛焰求明性 \n但向尘劳见本形。 \n忽有松风穿石罅 \n泠然吹落满山星。 \n\n注本诗严守平水韵九青部溟、宁、灵、形、星其中“星”属下平声九青部异读字《广韵》息盈切与“灵”“宁”同部古诗常用以协律如王维“清溪流过碧山头空水澄鲜一色秋。隔断红尘三十里白云红叶两悠悠”中“悠”亦借韵通协。全诗紧扣“以一为魂”之旨首句“孤光未凿”化《庄子·应帝王》“浑沌凿七窍而死”典反写太初本明未分之境次句“一粟吞天”以微纳巨承“一芥”而力愈雄浑颔联“青未染”“白犹灵”双色映照暗喻性体离垢绝染而朗然常照颈联直破二边——不假烛焰破外求、不避尘劳破厌离显《坛经》“佛法在世间不离世间觉”之旨结句松风裂石、星落满山是“一”之活泼妙用寂而常照照而恒寂恰如《道德经》“天得一以清地得一以宁”之诗性证成。 \nLLM1结果\n《咏一》 \n孤峰独峙破苍冥 \n一芥微身立太清。 \n万古乾坤凝此数 \n千山雪落只无声。 \n\n注本诗以“一”为魂通过“孤峰”“一芥”“此数”层层递进赋予数字哲思——既写天地间唯一性之壮美孤峰破冥又寓渺小个体与永恒宇宙的辩证芥子纳太清。末句“千山雪落只无声”以大静写大一雪覆千山而声息俱寂暗合《道德经》“天得一以清”之境。平仄依平水韵押九青部冥、清、声。 "
},
"status": "completed",
"elapsed_time": 0.0005218982696533203
}
],
"status": "completed"
}
]) // Chat message history
const [chatList, setChatList] = useState<ChatItem[]>([]) // Chat message history
const [variables, setVariables] = useState<Variable[]>([]) // Workflow input variables
const [streamLoading, setStreamLoading] = useState(false) // SSE streaming state
const [conversationId, setConversationId] = useState<string | null>(null) // Current conversation ID