Merge branch 'develop' into feature/app_zy
This commit is contained in:
@@ -1250,9 +1250,11 @@ async def export_app(
|
|||||||
async def import_app(
|
async def import_app(
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user)
|
current_user: User = Depends(get_current_user),
|
||||||
|
app_id: Optional[str] = Form(None),
|
||||||
):
|
):
|
||||||
"""从 YAML 文件导入 agent / multi_agent / workflow 应用。
|
"""从 YAML 文件导入 agent / multi_agent / workflow 应用。
|
||||||
|
传入 app_id 时覆盖该应用的配置(类型必须一致),否则创建新应用。
|
||||||
跨空间/跨租户导入时,模型/工具/知识库会按名称匹配,匹配不到则置空并返回 warnings。
|
跨空间/跨租户导入时,模型/工具/知识库会按名称匹配,匹配不到则置空并返回 warnings。
|
||||||
"""
|
"""
|
||||||
if not file.filename.lower().endswith((".yaml", ".yml")):
|
if not file.filename.lower().endswith((".yaml", ".yml")):
|
||||||
@@ -1263,13 +1265,15 @@ async def import_app(
|
|||||||
if not dsl or "app" not in dsl:
|
if not dsl or "app" not in dsl:
|
||||||
return fail(msg="YAML 格式无效,缺少 app 字段", code=BizCode.BAD_REQUEST)
|
return fail(msg="YAML 格式无效,缺少 app 字段", code=BizCode.BAD_REQUEST)
|
||||||
|
|
||||||
new_app, warnings = AppDslService(db).import_dsl(
|
target_app_id = uuid.UUID(app_id) if app_id else None
|
||||||
|
result_app, warnings = AppDslService(db).import_dsl(
|
||||||
dsl=dsl,
|
dsl=dsl,
|
||||||
workspace_id=current_user.current_workspace_id,
|
workspace_id=current_user.current_workspace_id,
|
||||||
tenant_id=current_user.tenant_id,
|
tenant_id=current_user.tenant_id,
|
||||||
user_id=current_user.id,
|
user_id=current_user.id,
|
||||||
|
app_id=target_app_id,
|
||||||
)
|
)
|
||||||
return success(
|
return success(
|
||||||
data={"app": app_schema.App.model_validate(new_app), "warnings": warnings},
|
data={"app": app_schema.App.model_validate(result_app), "warnings": warnings},
|
||||||
msg="应用导入成功" + (",但部分资源需手动配置" if warnings else "")
|
msg="应用导入成功" + (",但部分资源需手动配置" if warnings else "")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -28,86 +28,135 @@ class IterationRuntime:
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
start_id: str,
|
|
||||||
stream: bool,
|
stream: bool,
|
||||||
graph: CompiledStateGraph,
|
|
||||||
node_id: str,
|
node_id: str,
|
||||||
config: dict[str, Any],
|
config: dict[str, Any],
|
||||||
state: WorkflowState,
|
state: WorkflowState,
|
||||||
variable_pool: VariablePool,
|
variable_pool: VariablePool,
|
||||||
child_variable_pool: VariablePool,
|
cycle_nodes: list,
|
||||||
|
cycle_edges: list,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Initialize the iteration runtime.
|
Initialize the iteration runtime.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
graph: Compiled workflow graph capable of async invocation.
|
stream: Whether to run in streaming mode. When True, each iteration
|
||||||
node_id: Unique identifier of the loop node.
|
uses graph.astream and emits cycle_item events in real time.
|
||||||
config: Dictionary containing iteration node configuration.
|
When False, graph.ainvoke is used instead.
|
||||||
state: Current workflow state at the point of iteration.
|
node_id: The unique identifier of the iteration node in the workflow.
|
||||||
|
Also used as the variable namespace for item/index inside
|
||||||
|
the subgraph (e.g. {{ node_id.item }}).
|
||||||
|
config: Raw configuration dict for the iteration node, parsed into
|
||||||
|
IterationNodeConfig. Controls input/output variable selectors,
|
||||||
|
parallel execution settings, and output flattening.
|
||||||
|
state: The parent workflow state at the point the iteration node is
|
||||||
|
entered. Each task receives a copy of this state as its
|
||||||
|
starting point.
|
||||||
|
variable_pool: The parent VariablePool containing all variables available
|
||||||
|
at the time the iteration node executes, including sys.*,
|
||||||
|
conv.*, and outputs from upstream nodes. Used as the source
|
||||||
|
for deep-copying into each task's independent child pool.
|
||||||
|
cycle_nodes: List of node config dicts belonging to this iteration's
|
||||||
|
subgraph (i.e. nodes whose cycle field equals node_id).
|
||||||
|
Passed to GraphBuilder when constructing each task's subgraph.
|
||||||
|
cycle_edges: List of edge config dicts connecting nodes within the subgraph.
|
||||||
|
Passed to GraphBuilder alongside cycle_nodes.
|
||||||
"""
|
"""
|
||||||
self.start_id = start_id
|
|
||||||
self.stream = stream
|
self.stream = stream
|
||||||
self.graph = graph
|
|
||||||
self.state = state
|
self.state = state
|
||||||
self.node_id = node_id
|
self.node_id = node_id
|
||||||
self.typed_config = IterationNodeConfig(**config)
|
self.typed_config = IterationNodeConfig(**config)
|
||||||
self.looping = True
|
self.looping = True
|
||||||
self.variable_pool = variable_pool
|
self.variable_pool = variable_pool
|
||||||
self.child_variable_pool = child_variable_pool
|
self.cycle_nodes = cycle_nodes
|
||||||
|
self.cycle_edges = cycle_edges
|
||||||
self.event_write = get_stream_writer()
|
self.event_write = get_stream_writer()
|
||||||
self.checkpoint = RunnableConfig(
|
|
||||||
configurable={
|
|
||||||
"thread_id": uuid.uuid4()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.output_value = None
|
self.output_value = None
|
||||||
self.result: list = []
|
self.result: list = []
|
||||||
|
|
||||||
async def _init_iteration_state(self, item, idx):
|
def _build_child_graph(self) -> tuple[CompiledStateGraph, VariablePool, str]:
|
||||||
"""
|
"""
|
||||||
Initialize a per-iteration copy of the workflow state.
|
Build an independent compiled subgraph for a single iteration task.
|
||||||
|
|
||||||
Args:
|
Each call creates a brand-new VariablePool by deep-copying the parent pool,
|
||||||
item: Current element from the input array for this iteration.
|
then passes it to GraphBuilder. GraphBuilder binds this pool to every node's
|
||||||
idx: Index of the element in the input array.
|
execution closure at build time, so the pool and the subgraph always reference
|
||||||
|
the same object. This is the key design invariant: item/index written into the
|
||||||
|
pool after build will be visible to all nodes inside the subgraph.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A copy of the workflow state with iteration-specific variables set.
|
graph: The compiled LangGraph subgraph ready for invocation.
|
||||||
|
child_pool: The VariablePool bound to this subgraph's node closures.
|
||||||
|
Callers must write item/index into this pool before invoking
|
||||||
|
the graph, and read output from it after invocation.
|
||||||
|
start_node_id: The ID of the CYCLE_START node inside the subgraph,
|
||||||
|
used to set the initial activation signal in workflow state.
|
||||||
"""
|
"""
|
||||||
loopstate = WorkflowState(
|
from app.core.workflow.engine.graph_builder import GraphBuilder
|
||||||
**self.state
|
child_pool = VariablePool()
|
||||||
|
child_pool.copy(self.variable_pool)
|
||||||
|
builder = GraphBuilder(
|
||||||
|
{"nodes": self.cycle_nodes, "edges": self.cycle_edges},
|
||||||
|
stream=self.stream,
|
||||||
|
variable_pool=child_pool,
|
||||||
|
cycle=self.node_id,
|
||||||
)
|
)
|
||||||
self.child_variable_pool.copy(self.variable_pool)
|
graph = builder.build()
|
||||||
await self.child_variable_pool.new(self.node_id, "item", item, VariableType.type_map(item), mut=True)
|
return graph, builder.variable_pool, builder.start_node_id
|
||||||
await self.child_variable_pool.new(self.node_id, "index", item, VariableType.type_map(item), mut=True)
|
|
||||||
loopstate["node_outputs"][self.node_id] = {
|
async def _init_iteration_state(self, item, idx, child_pool: VariablePool, start_id: str):
|
||||||
"item": item,
|
"""
|
||||||
"index": idx,
|
Initialize the workflow state for a single iteration.
|
||||||
}
|
|
||||||
|
Writes the current item and its index into child_pool under the iteration
|
||||||
|
node's namespace (e.g. iteration_xxx.item, iteration_xxx.index), making them
|
||||||
|
accessible to downstream nodes inside the subgraph via variable selectors.
|
||||||
|
|
||||||
|
Also prepares a copy of the parent workflow state with:
|
||||||
|
- node_outputs[node_id] set to {item, index} so the state snapshot is consistent
|
||||||
|
with the pool values.
|
||||||
|
- looping flag set to 1 (active) to signal the subgraph is inside a cycle.
|
||||||
|
- activate[start_id] set to True to trigger the CYCLE_START node.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
item: The current element from the input array.
|
||||||
|
idx: The zero-based index of this element in the input array.
|
||||||
|
child_pool: The VariablePool bound to this iteration's subgraph.
|
||||||
|
Must be the same object returned by _build_child_graph.
|
||||||
|
start_id: The ID of the CYCLE_START node inside the subgraph.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A WorkflowState instance ready to be passed to graph.ainvoke or graph.astream.
|
||||||
|
"""
|
||||||
|
loopstate = WorkflowState(**self.state)
|
||||||
|
await child_pool.new(self.node_id, "item", item, VariableType.type_map(item), mut=True)
|
||||||
|
await child_pool.new(self.node_id, "index", idx, VariableType.type_map(idx), mut=True)
|
||||||
|
loopstate["node_outputs"][self.node_id] = {"item": item, "index": idx}
|
||||||
loopstate["looping"] = 1
|
loopstate["looping"] = 1
|
||||||
loopstate["activate"][self.start_id] = True
|
loopstate["activate"][start_id] = True
|
||||||
return loopstate
|
return loopstate
|
||||||
|
|
||||||
def merge_conv_vars(self):
|
def _merge_conv_vars(self, child_pool: VariablePool):
|
||||||
self.variable_pool.variables["conv"].update(
|
self.variable_pool.variables["conv"].update(child_pool.variables["conv"])
|
||||||
self.child_variable_pool.variables["conv"]
|
|
||||||
)
|
|
||||||
|
|
||||||
async def run_task(self, item, idx):
|
async def run_task(self, item, idx):
|
||||||
"""
|
"""
|
||||||
Execute a single iteration asynchronously.
|
Execute a single iteration asynchronously.
|
||||||
|
Each task builds its own subgraph so the variable pool closure is independent.
|
||||||
|
|
||||||
Args:
|
Returns:
|
||||||
item: The input element for this iteration.
|
Tuple of (idx, output, result, child_pool, stopped)
|
||||||
idx: The index of this iteration.
|
|
||||||
"""
|
"""
|
||||||
|
graph, child_pool, start_id = self._build_child_graph()
|
||||||
|
checkpoint = RunnableConfig(configurable={"thread_id": uuid.uuid4()})
|
||||||
|
init_state = await self._init_iteration_state(item, idx, child_pool, start_id)
|
||||||
|
|
||||||
if self.stream:
|
if self.stream:
|
||||||
async for event in self.graph.astream(
|
async for event in graph.astream(
|
||||||
await self._init_iteration_state(item, idx),
|
init_state,
|
||||||
stream_mode=["debug"],
|
stream_mode=["debug"],
|
||||||
config=self.checkpoint
|
config=checkpoint
|
||||||
):
|
):
|
||||||
if isinstance(event, tuple) and len(event) == 2:
|
if isinstance(event, tuple) and len(event) == 2:
|
||||||
mode, data = event
|
mode, data = event
|
||||||
@@ -117,7 +166,6 @@ class IterationRuntime:
|
|||||||
event_type = data.get("type")
|
event_type = data.get("type")
|
||||||
payload = data.get("payload", {})
|
payload = data.get("payload", {})
|
||||||
node_name = payload.get("name")
|
node_name = payload.get("name")
|
||||||
|
|
||||||
if node_name and node_name.startswith("nop"):
|
if node_name and node_name.startswith("nop"):
|
||||||
continue
|
continue
|
||||||
if event_type == "task_result":
|
if event_type == "task_result":
|
||||||
@@ -140,17 +188,13 @@ class IterationRuntime:
|
|||||||
"token_usage": result.get("node_outputs", {}).get(node_name, {}).get("token_usage")
|
"token_usage": result.get("node_outputs", {}).get(node_name, {}).get("token_usage")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
result = self.graph.get_state(config=self.checkpoint).values
|
result = graph.get_state(config=checkpoint).values
|
||||||
else:
|
else:
|
||||||
result = await self.graph.ainvoke(await self._init_iteration_state(item, idx))
|
result = await graph.ainvoke(init_state)
|
||||||
output = self.child_variable_pool.get_value(self.output_value)
|
|
||||||
if isinstance(output, list) and self.typed_config.flatten:
|
output = child_pool.get_value(self.output_value)
|
||||||
self.result.extend(output)
|
stopped = result["looping"] == 2
|
||||||
else:
|
return idx, output, result, child_pool, stopped
|
||||||
self.result.append(output)
|
|
||||||
if result["looping"] == 2:
|
|
||||||
self.looping = False
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _create_iteration_tasks(self, array_obj, idx):
|
def _create_iteration_tasks(self, array_obj, idx):
|
||||||
"""
|
"""
|
||||||
@@ -196,16 +240,32 @@ class IterationRuntime:
|
|||||||
tasks = self._create_iteration_tasks(array_obj, idx)
|
tasks = self._create_iteration_tasks(array_obj, idx)
|
||||||
logger.info(f"Iteration node {self.node_id}: running, concurrency {len(tasks)}")
|
logger.info(f"Iteration node {self.node_id}: running, concurrency {len(tasks)}")
|
||||||
idx += self.typed_config.parallel_count
|
idx += self.typed_config.parallel_count
|
||||||
child_state.extend(await asyncio.gather(*tasks))
|
batch = await asyncio.gather(*tasks)
|
||||||
self.merge_conv_vars()
|
# Sort by idx to preserve order, then collect results
|
||||||
|
batch_sorted = sorted(batch, key=lambda x: x[0])
|
||||||
|
for _, output, result, child_pool, stopped in batch_sorted:
|
||||||
|
if isinstance(output, list) and self.typed_config.flatten:
|
||||||
|
self.result.extend(output)
|
||||||
|
else:
|
||||||
|
self.result.append(output)
|
||||||
|
child_state.append(result)
|
||||||
|
self._merge_conv_vars(child_pool)
|
||||||
|
if stopped:
|
||||||
|
self.looping = False
|
||||||
else:
|
else:
|
||||||
# Execute iterations sequentially
|
# Execute iterations sequentially
|
||||||
while idx < len(array_obj) and self.looping:
|
while idx < len(array_obj) and self.looping:
|
||||||
logger.info(f"Iteration node {self.node_id}: running")
|
logger.info(f"Iteration node {self.node_id}: running")
|
||||||
item = array_obj[idx]
|
item = array_obj[idx]
|
||||||
result = await self.run_task(item, idx)
|
_, output, result, child_pool, stopped = await self.run_task(item, idx)
|
||||||
self.merge_conv_vars()
|
if isinstance(output, list) and self.typed_config.flatten:
|
||||||
|
self.result.extend(output)
|
||||||
|
else:
|
||||||
|
self.result.append(output)
|
||||||
|
self._merge_conv_vars(child_pool)
|
||||||
child_state.append(result)
|
child_state.append(result)
|
||||||
|
if stopped:
|
||||||
|
self.looping = False
|
||||||
idx += 1
|
idx += 1
|
||||||
logger.info(f"Iteration node {self.node_id}: execution completed")
|
logger.info(f"Iteration node {self.node_id}: execution completed")
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ class CycleGraphNode(BaseNode):
|
|||||||
|
|
||||||
return cycle_nodes, cycle_edges
|
return cycle_nodes, cycle_edges
|
||||||
|
|
||||||
def build_graph(self):
|
def build_graph(self, variable_pool: VariablePool):
|
||||||
"""
|
"""
|
||||||
Build and compile the internal subgraph for this cycle node.
|
Build and compile the internal subgraph for this cycle node.
|
||||||
|
|
||||||
@@ -135,6 +135,7 @@ class CycleGraphNode(BaseNode):
|
|||||||
from app.core.workflow.engine.graph_builder import GraphBuilder
|
from app.core.workflow.engine.graph_builder import GraphBuilder
|
||||||
|
|
||||||
self.child_variable_pool = VariablePool()
|
self.child_variable_pool = VariablePool()
|
||||||
|
self.child_variable_pool.copy(variable_pool)
|
||||||
builder = GraphBuilder(
|
builder = GraphBuilder(
|
||||||
{
|
{
|
||||||
"nodes": self.cycle_nodes,
|
"nodes": self.cycle_nodes,
|
||||||
@@ -165,8 +166,8 @@ class CycleGraphNode(BaseNode):
|
|||||||
Raises:
|
Raises:
|
||||||
RuntimeError: If the node type is unsupported.
|
RuntimeError: If the node type is unsupported.
|
||||||
"""
|
"""
|
||||||
self.build_graph()
|
|
||||||
if self.node_type == NodeType.LOOP:
|
if self.node_type == NodeType.LOOP:
|
||||||
|
self.build_graph(variable_pool)
|
||||||
return await LoopRuntime(
|
return await LoopRuntime(
|
||||||
start_id=self.start_node_id,
|
start_id=self.start_node_id,
|
||||||
stream=False,
|
stream=False,
|
||||||
@@ -179,20 +180,19 @@ class CycleGraphNode(BaseNode):
|
|||||||
).run()
|
).run()
|
||||||
if self.node_type == NodeType.ITERATION:
|
if self.node_type == NodeType.ITERATION:
|
||||||
return await IterationRuntime(
|
return await IterationRuntime(
|
||||||
start_id=self.start_node_id,
|
|
||||||
stream=False,
|
stream=False,
|
||||||
graph=self.graph,
|
|
||||||
node_id=self.node_id,
|
node_id=self.node_id,
|
||||||
config=self.config,
|
config=self.config,
|
||||||
state=state,
|
state=state,
|
||||||
variable_pool=variable_pool,
|
variable_pool=variable_pool,
|
||||||
child_variable_pool=self.child_variable_pool
|
cycle_nodes=self.cycle_nodes,
|
||||||
|
cycle_edges=self.cycle_edges,
|
||||||
).run()
|
).run()
|
||||||
raise RuntimeError("Unknown cycle node type")
|
raise RuntimeError("Unknown cycle node type")
|
||||||
|
|
||||||
async def execute_stream(self, state: WorkflowState, variable_pool: VariablePool):
|
async def execute_stream(self, state: WorkflowState, variable_pool: VariablePool):
|
||||||
self.build_graph()
|
|
||||||
if self.node_type == NodeType.LOOP:
|
if self.node_type == NodeType.LOOP:
|
||||||
|
self.build_graph(variable_pool)
|
||||||
yield {
|
yield {
|
||||||
"__final__": True,
|
"__final__": True,
|
||||||
"result": await LoopRuntime(
|
"result": await LoopRuntime(
|
||||||
@@ -211,14 +211,13 @@ class CycleGraphNode(BaseNode):
|
|||||||
yield {
|
yield {
|
||||||
"__final__": True,
|
"__final__": True,
|
||||||
"result": await IterationRuntime(
|
"result": await IterationRuntime(
|
||||||
start_id=self.start_node_id,
|
|
||||||
stream=True,
|
stream=True,
|
||||||
graph=self.graph,
|
|
||||||
node_id=self.node_id,
|
node_id=self.node_id,
|
||||||
config=self.config,
|
config=self.config,
|
||||||
state=state,
|
state=state,
|
||||||
variable_pool=variable_pool,
|
variable_pool=variable_pool,
|
||||||
child_variable_pool=self.child_variable_pool
|
cycle_nodes=self.cycle_nodes,
|
||||||
|
cycle_edges=self.cycle_edges,
|
||||||
).run()
|
).run()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ class FileInput(BaseModel):
|
|||||||
upload_file_id: Optional[uuid.UUID] = Field(None, description="已上传文件ID(local_file时必填)")
|
upload_file_id: Optional[uuid.UUID] = Field(None, description="已上传文件ID(local_file时必填)")
|
||||||
url: Optional[str] = Field(None, description="远程URL(remote_url时必填)")
|
url: Optional[str] = Field(None, description="远程URL(remote_url时必填)")
|
||||||
file_type: Optional[str] = Field(None, description="具体文件格式(如image/jpg、audio/wav、document/docx、video/mp4)")
|
file_type: Optional[str] = Field(None, description="具体文件格式(如image/jpg、audio/wav、document/docx、video/mp4)")
|
||||||
|
name: Optional[str] = Field(None, description="文件名")
|
||||||
|
size: Optional[int] = Field(None, description="文件大小(字节)")
|
||||||
|
|
||||||
_content = None
|
_content = None
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from app.services.model_service import ModelApiKeyService
|
|||||||
from app.services.multi_agent_orchestrator import MultiAgentOrchestrator
|
from app.services.multi_agent_orchestrator import MultiAgentOrchestrator
|
||||||
from app.services.multimodal_service import MultimodalService
|
from app.services.multimodal_service import MultimodalService
|
||||||
from app.services.workflow_service import WorkflowService
|
from app.services.workflow_service import WorkflowService
|
||||||
|
from app.models.file_metadata_model import FileMetadata
|
||||||
|
|
||||||
logger = get_business_logger()
|
logger = get_business_logger()
|
||||||
|
|
||||||
@@ -218,11 +219,29 @@ class AppChatService:
|
|||||||
"reasoning_content": result.get("reasoning_content")
|
"reasoning_content": result.get("reasoning_content")
|
||||||
}
|
}
|
||||||
if files:
|
if files:
|
||||||
|
local_ids = [f.upload_file_id for f in files
|
||||||
|
if f.transfer_method.value == "local_file" and f.upload_file_id
|
||||||
|
and (not f.name or not f.size)]
|
||||||
|
meta_map = {}
|
||||||
|
if local_ids:
|
||||||
|
rows = self.db.query(FileMetadata).filter(
|
||||||
|
FileMetadata.id.in_(local_ids),
|
||||||
|
FileMetadata.status == "completed"
|
||||||
|
).all()
|
||||||
|
meta_map = {str(r.id): r for r in rows}
|
||||||
for f in files:
|
for f in files:
|
||||||
# url = await MultimodalService(self.db).get_file_url(f)
|
name, size = f.name, f.size
|
||||||
|
if f.transfer_method.value == "local_file" and f.upload_file_id and (not name or not size):
|
||||||
|
meta = meta_map.get(str(f.upload_file_id))
|
||||||
|
if meta:
|
||||||
|
name = name or meta.file_name
|
||||||
|
size = size or meta.file_size
|
||||||
human_meta["files"].append({
|
human_meta["files"].append({
|
||||||
"type": f.type,
|
"type": f.type,
|
||||||
"url": f.url
|
"url": f.url,
|
||||||
|
"name": name,
|
||||||
|
"size": size,
|
||||||
|
"file_type": f.file_type,
|
||||||
})
|
})
|
||||||
|
|
||||||
if processed_files:
|
if processed_files:
|
||||||
@@ -509,10 +528,29 @@ class AppChatService:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if files:
|
if files:
|
||||||
|
local_ids = [f.upload_file_id for f in files
|
||||||
|
if f.transfer_method.value == "local_file" and f.upload_file_id
|
||||||
|
and (not f.name or not f.size)]
|
||||||
|
meta_map = {}
|
||||||
|
if local_ids:
|
||||||
|
rows = self.db.query(FileMetadata).filter(
|
||||||
|
FileMetadata.id.in_(local_ids),
|
||||||
|
FileMetadata.status == "completed"
|
||||||
|
).all()
|
||||||
|
meta_map = {str(r.id): r for r in rows}
|
||||||
for f in files:
|
for f in files:
|
||||||
|
name, size = f.name, f.size
|
||||||
|
if f.transfer_method.value == "local_file" and f.upload_file_id and (not name or not size):
|
||||||
|
meta = meta_map.get(str(f.upload_file_id))
|
||||||
|
if meta:
|
||||||
|
name = name or meta.file_name
|
||||||
|
size = size or meta.file_size
|
||||||
human_meta["files"].append({
|
human_meta["files"].append({
|
||||||
"type": f.type,
|
"type": f.type,
|
||||||
"url": f.url
|
"url": f.url,
|
||||||
|
"name": name,
|
||||||
|
"size": size,
|
||||||
|
"file_type": f.file_type,
|
||||||
})
|
})
|
||||||
if processed_files:
|
if processed_files:
|
||||||
human_meta["history_files"] = {
|
human_meta["history_files"] = {
|
||||||
|
|||||||
@@ -229,8 +229,11 @@ class AppDslService:
|
|||||||
workspace_id: uuid.UUID,
|
workspace_id: uuid.UUID,
|
||||||
tenant_id: uuid.UUID,
|
tenant_id: uuid.UUID,
|
||||||
user_id: uuid.UUID,
|
user_id: uuid.UUID,
|
||||||
|
app_id: Optional[uuid.UUID] = None,
|
||||||
) -> tuple[App, list[str]]:
|
) -> tuple[App, list[str]]:
|
||||||
"""解析 DSL,创建应用及配置,返回 (new_app, warnings)"""
|
"""解析 DSL,创建或覆盖应用配置,返回 (app, warnings)。
|
||||||
|
app_id 不为空时:校验类型一致后覆盖配置;为空时创建新应用。
|
||||||
|
"""
|
||||||
app_meta = dsl.get("app", {})
|
app_meta = dsl.get("app", {})
|
||||||
app_type = app_meta.get("type")
|
app_type = app_meta.get("type")
|
||||||
if app_type not in (AppType.AGENT, AppType.MULTI_AGENT, AppType.WORKFLOW):
|
if app_type not in (AppType.AGENT, AppType.MULTI_AGENT, AppType.WORKFLOW):
|
||||||
@@ -239,6 +242,9 @@ class AppDslService:
|
|||||||
warnings: list[str] = []
|
warnings: list[str] = []
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
|
|
||||||
|
if app_id is not None:
|
||||||
|
return self._overwrite_dsl(dsl, app_id, app_type, workspace_id, tenant_id, warnings, now)
|
||||||
|
|
||||||
new_app = App(
|
new_app = App(
|
||||||
id=uuid.uuid4(),
|
id=uuid.uuid4(),
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
@@ -258,11 +264,57 @@ class AppDslService:
|
|||||||
self.db.add(new_app)
|
self.db.add(new_app)
|
||||||
self.db.flush()
|
self.db.flush()
|
||||||
|
|
||||||
|
self._write_config(new_app.id, app_type, dsl, workspace_id, tenant_id, warnings, now, create=True)
|
||||||
|
|
||||||
|
self.db.commit()
|
||||||
|
self.db.refresh(new_app)
|
||||||
|
return new_app, warnings
|
||||||
|
|
||||||
|
def _overwrite_dsl(
|
||||||
|
self,
|
||||||
|
dsl: dict,
|
||||||
|
app_id: uuid.UUID,
|
||||||
|
app_type: str,
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
|
tenant_id: uuid.UUID,
|
||||||
|
warnings: list,
|
||||||
|
now: datetime.datetime,
|
||||||
|
) -> tuple[App, list[str]]:
|
||||||
|
"""覆盖已有应用的配置,类型不一致时抛出异常"""
|
||||||
|
app = self.db.query(App).filter(
|
||||||
|
App.id == app_id,
|
||||||
|
App.workspace_id == workspace_id,
|
||||||
|
App.is_active.is_(True)
|
||||||
|
).first()
|
||||||
|
if not app:
|
||||||
|
raise ResourceNotFoundException("应用", str(app_id))
|
||||||
|
if app.type != app_type:
|
||||||
|
raise BusinessException(
|
||||||
|
f"YAML 类型 '{app_type}' 与应用类型 '{app.type}' 不一致,无法导入",
|
||||||
|
BizCode.BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
self._write_config(app_id, app_type, dsl, workspace_id, tenant_id, warnings, now, create=False)
|
||||||
|
|
||||||
|
self.db.commit()
|
||||||
|
self.db.refresh(app)
|
||||||
|
return app, warnings
|
||||||
|
|
||||||
|
def _write_config(
|
||||||
|
self,
|
||||||
|
app_id: uuid.UUID,
|
||||||
|
app_type: str,
|
||||||
|
dsl: dict,
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
|
tenant_id: uuid.UUID,
|
||||||
|
warnings: list,
|
||||||
|
now: datetime.datetime,
|
||||||
|
create: bool,
|
||||||
|
) -> None:
|
||||||
|
"""写入(新建或覆盖)应用配置"""
|
||||||
if app_type == AppType.AGENT:
|
if app_type == AppType.AGENT:
|
||||||
cfg = dsl.get("agent_config") or {}
|
cfg = dsl.get("agent_config") or {}
|
||||||
self.db.add(AgentConfig(
|
fields = dict(
|
||||||
id=uuid.uuid4(),
|
|
||||||
app_id=new_app.id,
|
|
||||||
system_prompt=cfg.get("system_prompt"),
|
system_prompt=cfg.get("system_prompt"),
|
||||||
model_parameters=cfg.get("model_parameters"),
|
model_parameters=cfg.get("model_parameters"),
|
||||||
default_model_config_id=self._resolve_model(cfg.get("default_model_config_ref"), tenant_id, warnings),
|
default_model_config_id=self._resolve_model(cfg.get("default_model_config_ref"), tenant_id, warnings),
|
||||||
@@ -272,16 +324,21 @@ class AppDslService:
|
|||||||
tools=self._resolve_tools(cfg.get("tools", []), tenant_id, warnings),
|
tools=self._resolve_tools(cfg.get("tools", []), tenant_id, warnings),
|
||||||
skills=self._resolve_skills(cfg.get("skills", {}), tenant_id, warnings),
|
skills=self._resolve_skills(cfg.get("skills", {}), tenant_id, warnings),
|
||||||
features=cfg.get("features", {}),
|
features=cfg.get("features", {}),
|
||||||
is_active=True,
|
|
||||||
created_at=now,
|
|
||||||
updated_at=now,
|
updated_at=now,
|
||||||
))
|
)
|
||||||
|
if create:
|
||||||
|
self.db.add(AgentConfig(id=uuid.uuid4(), app_id=app_id, is_active=True, created_at=now, **fields))
|
||||||
|
else:
|
||||||
|
existing = self.db.query(AgentConfig).filter(AgentConfig.app_id == app_id).first()
|
||||||
|
if existing:
|
||||||
|
for k, v in fields.items():
|
||||||
|
setattr(existing, k, v)
|
||||||
|
else:
|
||||||
|
self.db.add(AgentConfig(id=uuid.uuid4(), app_id=app_id, is_active=True, created_at=now, **fields))
|
||||||
|
|
||||||
elif app_type == AppType.MULTI_AGENT:
|
elif app_type == AppType.MULTI_AGENT:
|
||||||
cfg = dsl.get("multi_agent_config") or {}
|
cfg = dsl.get("multi_agent_config") or {}
|
||||||
self.db.add(MultiAgentConfig(
|
fields = dict(
|
||||||
id=uuid.uuid4(),
|
|
||||||
app_id=new_app.id,
|
|
||||||
orchestration_mode=cfg.get("orchestration_mode", "collaboration"),
|
orchestration_mode=cfg.get("orchestration_mode", "collaboration"),
|
||||||
master_agent_name=cfg.get("master_agent_name"),
|
master_agent_name=cfg.get("master_agent_name"),
|
||||||
model_parameters=cfg.get("model_parameters"),
|
model_parameters=cfg.get("model_parameters"),
|
||||||
@@ -291,10 +348,17 @@ class AppDslService:
|
|||||||
routing_rules=self._resolve_routing_rules(cfg.get("routing_rules"), warnings),
|
routing_rules=self._resolve_routing_rules(cfg.get("routing_rules"), warnings),
|
||||||
execution_config=cfg.get("execution_config", {}),
|
execution_config=cfg.get("execution_config", {}),
|
||||||
aggregation_strategy=cfg.get("aggregation_strategy", "merge"),
|
aggregation_strategy=cfg.get("aggregation_strategy", "merge"),
|
||||||
is_active=True,
|
|
||||||
created_at=now,
|
|
||||||
updated_at=now,
|
updated_at=now,
|
||||||
))
|
)
|
||||||
|
if create:
|
||||||
|
self.db.add(MultiAgentConfig(id=uuid.uuid4(), app_id=app_id, is_active=True, created_at=now, **fields))
|
||||||
|
else:
|
||||||
|
existing = self.db.query(MultiAgentConfig).filter(MultiAgentConfig.app_id == app_id).first()
|
||||||
|
if existing:
|
||||||
|
for k, v in fields.items():
|
||||||
|
setattr(existing, k, v)
|
||||||
|
else:
|
||||||
|
self.db.add(MultiAgentConfig(id=uuid.uuid4(), app_id=app_id, is_active=True, created_at=now, **fields))
|
||||||
|
|
||||||
elif app_type == AppType.WORKFLOW:
|
elif app_type == AppType.WORKFLOW:
|
||||||
adapter = MemoryBearAdapter(dsl)
|
adapter = MemoryBearAdapter(dsl)
|
||||||
@@ -306,20 +370,39 @@ class AppDslService:
|
|||||||
for w in result.warnings:
|
for w in result.warnings:
|
||||||
warnings.append(f"[节点警告] {w.node_name or w.node_id}: {w.detail}")
|
warnings.append(f"[节点警告] {w.node_name or w.node_id}: {w.detail}")
|
||||||
wf = dsl.get("workflow") or {}
|
wf = dsl.get("workflow") or {}
|
||||||
WorkflowService(self.db).create_workflow_config(
|
wf_service = WorkflowService(self.db)
|
||||||
app_id=new_app.id,
|
if create:
|
||||||
nodes=[n.model_dump() for n in result.nodes],
|
wf_service.create_workflow_config(
|
||||||
edges=[e.model_dump() for e in result.edges],
|
app_id=app_id,
|
||||||
variables=[v.model_dump() for v in result.variables],
|
nodes=[n.model_dump() for n in result.nodes],
|
||||||
execution_config=wf.get("execution_config", {}),
|
edges=[e.model_dump() for e in result.edges],
|
||||||
features=wf.get("features", {}),
|
variables=[v.model_dump() for v in result.variables],
|
||||||
triggers=wf.get("triggers", []),
|
execution_config=wf.get("execution_config", {}),
|
||||||
validate=False,
|
features=wf.get("features", {}),
|
||||||
)
|
triggers=wf.get("triggers", []),
|
||||||
|
validate=False,
|
||||||
self.db.commit()
|
)
|
||||||
self.db.refresh(new_app)
|
else:
|
||||||
return new_app, warnings
|
existing = self.db.query(WorkflowConfig).filter(WorkflowConfig.app_id == app_id).first()
|
||||||
|
if existing:
|
||||||
|
existing.nodes = [n.model_dump() for n in result.nodes]
|
||||||
|
existing.edges = [e.model_dump() for e in result.edges]
|
||||||
|
existing.variables = [v.model_dump() for v in result.variables]
|
||||||
|
existing.execution_config = wf.get("execution_config", {})
|
||||||
|
existing.features = wf.get("features", {})
|
||||||
|
existing.triggers = wf.get("triggers", [])
|
||||||
|
existing.updated_at = now
|
||||||
|
else:
|
||||||
|
wf_service.create_workflow_config(
|
||||||
|
app_id=app_id,
|
||||||
|
nodes=[n.model_dump() for n in result.nodes],
|
||||||
|
edges=[e.model_dump() for e in result.edges],
|
||||||
|
variables=[v.model_dump() for v in result.variables],
|
||||||
|
execution_config=wf.get("execution_config", {}),
|
||||||
|
features=wf.get("features", {}),
|
||||||
|
triggers=wf.get("triggers", []),
|
||||||
|
validate=False,
|
||||||
|
)
|
||||||
|
|
||||||
def _unique_app_name(self, name: str, workspace_id: uuid.UUID, app_type: AppType) -> str:
|
def _unique_app_name(self, name: str, workspace_id: uuid.UUID, app_type: AppType) -> str:
|
||||||
"""生成唯一应用名称,同时检查本空间自有应用和共享到本空间的应用"""
|
"""生成唯一应用名称,同时检查本空间自有应用和共享到本空间的应用"""
|
||||||
|
|||||||
@@ -1299,10 +1299,30 @@ class AgentRunService:
|
|||||||
"history_files": {}
|
"history_files": {}
|
||||||
}
|
}
|
||||||
if files:
|
if files:
|
||||||
|
from app.models.file_metadata_model import FileMetadata
|
||||||
|
local_ids = [f.upload_file_id for f in files
|
||||||
|
if f.transfer_method.value == "local_file" and f.upload_file_id
|
||||||
|
and (not f.name or not f.size)]
|
||||||
|
meta_map = {}
|
||||||
|
if local_ids:
|
||||||
|
rows = self.db.query(FileMetadata).filter(
|
||||||
|
FileMetadata.id.in_(local_ids),
|
||||||
|
FileMetadata.status == "completed"
|
||||||
|
).all()
|
||||||
|
meta_map = {str(r.id): r for r in rows}
|
||||||
for f in files:
|
for f in files:
|
||||||
|
name, size = f.name, f.size
|
||||||
|
if f.transfer_method.value == "local_file" and f.upload_file_id and (not name or not size):
|
||||||
|
meta = meta_map.get(str(f.upload_file_id))
|
||||||
|
if meta:
|
||||||
|
name = name or meta.file_name
|
||||||
|
size = size or meta.file_size
|
||||||
human_meta["files"].append({
|
human_meta["files"].append({
|
||||||
"type": f.type,
|
"type": f.type,
|
||||||
"url": f.url
|
"url": f.url,
|
||||||
|
"file_type": f.file_type,
|
||||||
|
"name": name,
|
||||||
|
"size": size
|
||||||
})
|
})
|
||||||
|
|
||||||
# 保存 history_files,包含 provider 和 is_omni 信息
|
# 保存 history_files,包含 provider 和 is_omni 信息
|
||||||
|
|||||||
@@ -957,7 +957,10 @@ class WorkflowService:
|
|||||||
for file in message["content"]:
|
for file in message["content"]:
|
||||||
human_meta["files"].append({
|
human_meta["files"].append({
|
||||||
"type": file.get("type"),
|
"type": file.get("type"),
|
||||||
"url": file.get("url")
|
"url": file.get("url"),
|
||||||
|
"file_type": file.get("origin_file_type"),
|
||||||
|
"name": file.get("name"),
|
||||||
|
"size": file.get("size")
|
||||||
})
|
})
|
||||||
if message["role"] == "assistant":
|
if message["role"] == "assistant":
|
||||||
assistant_message = message["content"]
|
assistant_message = message["content"]
|
||||||
|
|||||||
14
web/src/api/package.ts
Normal file
14
web/src/api/package.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { request } from '@/utils/request'
|
||||||
|
|
||||||
|
import type { Package } from '@/views/Package/types'
|
||||||
|
|
||||||
|
export const SYS_API_PREFIX = '/sys';
|
||||||
|
// 套餐列表
|
||||||
|
export const getPackageListUrl = `${SYS_API_PREFIX}/package-plans`
|
||||||
|
export const getPackageList = (query: { category: Package['category']; status: boolean; }) => {
|
||||||
|
return request.get(getPackageListUrl, query)
|
||||||
|
}
|
||||||
|
// 获取套餐详情
|
||||||
|
export const getPackageDetail = (package_plan_id: string) => {
|
||||||
|
return request.get(`${SYS_API_PREFIX}/package-plans/${package_plan_id}`)
|
||||||
|
}
|
||||||
@@ -3016,5 +3016,69 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
|||||||
apply: 'Apply',
|
apply: 'Apply',
|
||||||
tools: 'Tools',
|
tools: 'Tools',
|
||||||
},
|
},
|
||||||
|
package: {
|
||||||
|
package: 'Package Management',
|
||||||
|
saas_personal: 'SaaS Personal',
|
||||||
|
commercial_deployment: 'Commercial Deployment',
|
||||||
|
noCommercialPackages: 'No commercial deployment packages available',
|
||||||
|
|
||||||
|
addPackage: 'Add Plan',
|
||||||
|
packageName: 'Plan Name',
|
||||||
|
packageNameZh: 'Plan Name (中文)',
|
||||||
|
packageNameEn: 'Plan Name (English)',
|
||||||
|
packageNamePlaceholder: '中文, 例如:记忆体验版',
|
||||||
|
packageNamePlaceholderEn: 'English, e.g. Memory Trial Plan',
|
||||||
|
packageCategory: 'Package Category',
|
||||||
|
price: 'Price',
|
||||||
|
pricePlaceholder: 'e.g. 0, 19, 299 or Contact Us',
|
||||||
|
billingPeriod: 'Billing Period',
|
||||||
|
monthly: 'Monthly',
|
||||||
|
yearly: 'Yearly',
|
||||||
|
permanent_free: 'Permanent Free',
|
||||||
|
local_deployment: 'Local Deployment',
|
||||||
|
coreValue: 'Core Value',
|
||||||
|
coreValueZh: 'Core Value (中文)',
|
||||||
|
coreValueEn: 'Core Value (English)',
|
||||||
|
coreValuePlaceholder: '中文, 一句话描述核心价值',
|
||||||
|
coreValuePlaceholderEn: 'EngLish, describe the core value in one sentence',
|
||||||
|
tech_support: 'Technical Support',
|
||||||
|
tech_support_zh: 'Technical Support (中文)',
|
||||||
|
tech_support_en: 'Technical Support (English)',
|
||||||
|
technicalSupportPlaceholder: '中文, 例如:社群交流、工单支持',
|
||||||
|
technicalSupportPlaceholderEn: 'English, e.g. Community support, ticket support',
|
||||||
|
sla: 'SLA & Compliance',
|
||||||
|
slaZh: 'SLA & Compliance (中文)',
|
||||||
|
slaEn: 'SLA & Compliance (English)',
|
||||||
|
slaPlaceholder: '中文, 例如:无、验证力加强+审计日志',
|
||||||
|
slaPlaceholderEn: 'English, e.g. None, dedicated compute pool + audit logs',
|
||||||
|
customPage: 'Chat Page Customization',
|
||||||
|
customPageZh: 'Chat Page Customization (中文)',
|
||||||
|
customPageEn: 'Chat Page Customization (English)',
|
||||||
|
customPagePlaceholder: '中文, 例如:LOGO定制',
|
||||||
|
customPagePlaceholderEn: 'English, e.g. Logo customization',
|
||||||
|
primaryColor: 'Primary Color',
|
||||||
|
status: 'Status',
|
||||||
|
active: 'Active',
|
||||||
|
inactive: 'Inactive',
|
||||||
|
api_ops_rate_limit: 'API OPS Rate Limit',
|
||||||
|
ops: 'req/s',
|
||||||
|
pcs: 'pcs',
|
||||||
|
GB: 'GB',
|
||||||
|
tier_level: 'Tier Level',
|
||||||
|
numberPlaceholder: 'e.g. 10',
|
||||||
|
|
||||||
|
packageDetail: 'Package Detail',
|
||||||
|
basicInfo: 'Basic Info',
|
||||||
|
featureConfig: 'Billing Unit Quota',
|
||||||
|
workspace_quota: 'Workspace Quota',
|
||||||
|
skill_quota: 'Skill Library Quota',
|
||||||
|
app_quota: 'App Quota',
|
||||||
|
knowledge_capacity_quota: 'Knowledge Base Capacity',
|
||||||
|
memory_engine_quota: 'Memory Engine Quota',
|
||||||
|
end_user_quota: 'Memorable End Users',
|
||||||
|
ontology_project_quota: 'Ontology Project',
|
||||||
|
model_quota: 'Model Quota',
|
||||||
|
editPackage: 'Edit Package',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2980,5 +2980,69 @@ export const zh = {
|
|||||||
apply: '应用',
|
apply: '应用',
|
||||||
tools: '工具',
|
tools: '工具',
|
||||||
},
|
},
|
||||||
|
package: {
|
||||||
|
package: '套餐管理',
|
||||||
|
saas_personal: 'SaaS 个人版',
|
||||||
|
commercial_deployment: '商业化部署',
|
||||||
|
noCommercialPackages: '暂无商业化部署套餐',
|
||||||
|
|
||||||
|
addPackage: '添加套餐',
|
||||||
|
packageName: '套餐名称',
|
||||||
|
packageNameZh: '套餐名称 (中文)',
|
||||||
|
packageNameEn: '套餐名称 (English)',
|
||||||
|
packageNamePlaceholder: '中文, 例如:记忆体验版',
|
||||||
|
packageNamePlaceholderEn: 'English, e.g. Memory Trial Plan',
|
||||||
|
packageCategory: '套餐分类',
|
||||||
|
price: '价格',
|
||||||
|
pricePlaceholder: '例如: 0, 19, 299 或联系我们',
|
||||||
|
billingPeriod: '计费周期',
|
||||||
|
monthly: '月',
|
||||||
|
yearly: '年',
|
||||||
|
permanent_free: '永久免费',
|
||||||
|
local_deployment: '本地化部署',
|
||||||
|
coreValue: '核心价值',
|
||||||
|
coreValueZh: '核心价值 (中文)',
|
||||||
|
coreValueEn: '核心价值 (English)',
|
||||||
|
coreValuePlaceholder: '中文, 一句话描述核心价值',
|
||||||
|
coreValuePlaceholderEn: 'EngLish, describe the core value in one sentence',
|
||||||
|
tech_support: '技术支持',
|
||||||
|
tech_support_zh: '技术支持 (中文)',
|
||||||
|
tech_support_en: '技术支持 (English)',
|
||||||
|
technicalSupportPlaceholder: '中文, 例如:社群交流、工单支持',
|
||||||
|
technicalSupportPlaceholderEn: 'English, e.g. Community support, ticket support',
|
||||||
|
sla: 'SLA与合规',
|
||||||
|
slaZh: 'SLA与合规 (中文)',
|
||||||
|
slaEn: 'SLA与合规 (English)',
|
||||||
|
slaPlaceholder: '中文, 例如:无、验证力加强+审计日志',
|
||||||
|
slaPlaceholderEn: 'English, e.g. None, dedicated compute pool + audit logs',
|
||||||
|
customPage: '对应页面个性化配置',
|
||||||
|
customPageZh: '对应页面个性化配置 (中文)',
|
||||||
|
customPageEn: '对应页面个性化配置 (English)',
|
||||||
|
customPagePlaceholder: '中文, 例如:LOGO定制',
|
||||||
|
customPagePlaceholderEn: 'English, e.g. Logo customization',
|
||||||
|
primaryColor: '主题色',
|
||||||
|
status: '状态',
|
||||||
|
active: '启用',
|
||||||
|
inactive: '停用',
|
||||||
|
api_ops_rate_limit: 'API OPS 频次',
|
||||||
|
ops: '次/秒',
|
||||||
|
pcs: '个',
|
||||||
|
GB: 'GB',
|
||||||
|
tier_level: '层级',
|
||||||
|
numberPlaceholder: '如: 10',
|
||||||
|
|
||||||
|
packageDetail: '套餐详情',
|
||||||
|
basicInfo: '基础信息',
|
||||||
|
featureConfig: '计费单元配额',
|
||||||
|
workspace_quota: '空间数量',
|
||||||
|
skill_quota: '技能库数量',
|
||||||
|
app_quota: '应用数量',
|
||||||
|
knowledge_capacity_quota: '知识库容量',
|
||||||
|
memory_engine_quota: '记忆引擎数量',
|
||||||
|
end_user_quota: '可记忆终端用户数',
|
||||||
|
ontology_project_quota: '本体工程',
|
||||||
|
model_quota: '可负载模型数量',
|
||||||
|
editPackage: '编辑套餐',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-02 16:33:11
|
* @Date: 2026-02-02 16:33:11
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-02-04 18:11:34
|
* @Last Modified time: 2026-04-13 16:53:15
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Route Configuration
|
* Route Configuration
|
||||||
@@ -76,13 +76,12 @@ const componentMap: Record<string, LazyExoticComponent<ComponentType<object>>> =
|
|||||||
SpaceManagement: lazy(() => import('@/views/SpaceManagement')),
|
SpaceManagement: lazy(() => import('@/views/SpaceManagement')),
|
||||||
ApiKeyManagement: lazy(() => import('@/views/ApiKeyManagement')),
|
ApiKeyManagement: lazy(() => import('@/views/ApiKeyManagement')),
|
||||||
EmotionEngine: lazy(() => import('@/views/EmotionEngine')),
|
EmotionEngine: lazy(() => import('@/views/EmotionEngine')),
|
||||||
StatementDetail: lazy(() => import('@/views/UserMemoryDetail/pages/StatementDetail')),
|
|
||||||
ForgetDetail: lazy(() => import('@/views/UserMemoryDetail/pages/ForgetDetail')),
|
ForgetDetail: lazy(() => import('@/views/UserMemoryDetail/pages/ForgetDetail')),
|
||||||
MemoryNodeDetail: lazy(() => import('@/views/UserMemoryDetail/pages/index')),
|
MemoryNodeDetail: lazy(() => import('@/views/UserMemoryDetail/pages/index')),
|
||||||
SelfReflectionEngine: lazy(() => import('@/views/SelfReflectionEngine')),
|
SelfReflectionEngine: lazy(() => import('@/views/SelfReflectionEngine')),
|
||||||
OrderPayment: lazy(() => import('@/views/OrderPayment')),
|
OrderPayment: lazy(() => import('@/views/OrderPayment')),
|
||||||
OrderHistory: lazy(() => import('@/views/OrderHistory')),
|
OrderHistory: lazy(() => import('@/views/OrderHistory')),
|
||||||
Pricing: lazy(() => import('@/views/Pricing')),
|
Package: lazy(() => import('@/views/Package')),
|
||||||
ToolManagement: lazy(() => import('@/views/ToolManagement')),
|
ToolManagement: lazy(() => import('@/views/ToolManagement')),
|
||||||
SpaceConfig: lazy(() => import('@/views/SpaceConfig')),
|
SpaceConfig: lazy(() => import('@/views/SpaceConfig')),
|
||||||
Ontology: lazy(() => import('@/views/Ontology')),
|
Ontology: lazy(() => import('@/views/Ontology')),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
{ "path": "/model", "element": "ModelManagement" },
|
{ "path": "/model", "element": "ModelManagement" },
|
||||||
{ "path": "/space", "element": "SpaceManagement" },
|
{ "path": "/space", "element": "SpaceManagement" },
|
||||||
{ "path": "/tool", "element": "ToolManagement" },
|
{ "path": "/tool", "element": "ToolManagement" },
|
||||||
{ "path": "/pricing", "element": "Pricing" },
|
{ "path": "/pricing", "element": "Package" },
|
||||||
{ "path": "/order-pay", "element": "OrderPayment" },
|
{ "path": "/order-pay", "element": "OrderPayment" },
|
||||||
{ "path": "/orders", "element": "OrderHistory" },
|
{ "path": "/orders", "element": "OrderHistory" },
|
||||||
{ "path": "/skills", "element": "Skills" },
|
{ "path": "/skills", "element": "Skills" },
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-02 16:35:15
|
* @Date: 2026-02-02 16:35:15
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-03-06 10:39:00
|
* @Last Modified time: 2026-04-14 14:43:54
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* HTTP Request Utility Module
|
* HTTP Request Utility Module
|
||||||
@@ -23,6 +23,7 @@ import { clearAuthData } from './auth';
|
|||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
import { refreshTokenUrl, refreshToken, loginUrl, logoutUrl } from '@/api/user'
|
import { refreshTokenUrl, refreshToken, loginUrl, logoutUrl } from '@/api/user'
|
||||||
import i18n from '@/i18n'
|
import i18n from '@/i18n'
|
||||||
|
import { SYS_API_PREFIX } from '@/api/package'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Standard API response structure
|
* Standard API response structure
|
||||||
@@ -74,6 +75,10 @@ let requests: RequestQueueItem[] = [];
|
|||||||
// Request interceptor
|
// Request interceptor
|
||||||
service.interceptors.request.use(
|
service.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
|
console.log('config', config, config.url?.startsWith(SYS_API_PREFIX))
|
||||||
|
if (config.url?.startsWith(SYS_API_PREFIX)) {
|
||||||
|
config.baseURL = '';
|
||||||
|
}
|
||||||
if (!config.headers.Authorization) {
|
if (!config.headers.Authorization) {
|
||||||
const token = cookieUtils.get('authToken');
|
const token = cookieUtils.get('authToken');
|
||||||
if (token) {
|
if (token) {
|
||||||
|
|||||||
40
web/src/views/Package/constant.ts
Normal file
40
web/src/views/Package/constant.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ZhaoYing
|
||||||
|
* @Date: 2026-04-14 11:43:57
|
||||||
|
* @Last Modified by: ZhaoYing
|
||||||
|
* @Last Modified time: 2026-04-14 14:55:20
|
||||||
|
*/
|
||||||
|
export const billingUnits = [
|
||||||
|
{
|
||||||
|
key: 'workspace_quota',
|
||||||
|
unit: 'pcs', placeholder: 'numberPlaceholder',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'skill_quota',
|
||||||
|
unit: 'pcs', placeholder: 'numberPlaceholder',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'app_quota',
|
||||||
|
unit: 'pcs', placeholder: 'numberPlaceholder',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'knowledge_capacity_quota',
|
||||||
|
unit: 'GB', placeholder: 'numberPlaceholder',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'memory_engine_quota',
|
||||||
|
unit: 'pcs', placeholder: 'numberPlaceholder',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'end_user_quota',
|
||||||
|
unit: 'pcs', placeholder: 'numberPlaceholder',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ontology_project_quota',
|
||||||
|
unit: 'pcs', placeholder: 'numberPlaceholder',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'model_quota',
|
||||||
|
unit: 'ops', placeholder: 'numberPlaceholder',
|
||||||
|
},
|
||||||
|
]
|
||||||
145
web/src/views/Package/index.tsx
Normal file
145
web/src/views/Package/index.tsx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ZhaoYing
|
||||||
|
* @Date: 2026-02-25
|
||||||
|
* @Last Modified by: ZhaoYing
|
||||||
|
* @Last Modified time: 2026-04-14 14:59:11
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Package Component
|
||||||
|
*
|
||||||
|
* Package management page with:
|
||||||
|
* - Tabs for SaaS Personal and Commercial Deployment
|
||||||
|
* - Package cards showing features and pricing
|
||||||
|
* - Edit and delete actions
|
||||||
|
*
|
||||||
|
* @component
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useMemo, useState, useEffect, type FC } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Flex, Row, Col, type SegmentedProps } from 'antd';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import type { Package } from './types'
|
||||||
|
import { getPackageList } from '@/api/package';
|
||||||
|
import PageTabs from '@/components/PageTabs'
|
||||||
|
import { billingUnits } from './constant'
|
||||||
|
import RbCard from '@/components/RbCard/Card'
|
||||||
|
import BodyWrapper from '@/components/Empty/BodyWrapper'
|
||||||
|
import { useI18n } from '@/store/locale'
|
||||||
|
import RbButton from '@/components/RbButton'
|
||||||
|
|
||||||
|
const Package: FC = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { language } = useI18n()
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [data, setData] = useState<Package[]>([])
|
||||||
|
|
||||||
|
const [activeTab, setActiveTab] = useState('saas_personal');
|
||||||
|
const formatTabItems = useMemo(() => {
|
||||||
|
return ['saas_personal', 'commercial_deployment'].map(value => ({
|
||||||
|
value,
|
||||||
|
label: t(`package.${value}`),
|
||||||
|
}))
|
||||||
|
}, [t])
|
||||||
|
/** Handle tab change */
|
||||||
|
const handleChangeTab = (value: SegmentedProps['value']) => {
|
||||||
|
setActiveTab(value as string);
|
||||||
|
}
|
||||||
|
const getList = () => {
|
||||||
|
getPackageList({ category: activeTab as Package['category'], status: true })
|
||||||
|
.then(res => {
|
||||||
|
setData(res as Package[] || [])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getList()
|
||||||
|
}, [activeTab])
|
||||||
|
|
||||||
|
const getKeyWithLanguage = (key: string) => {
|
||||||
|
return (language === 'en' ? `${key}_en` : key) as keyof Package
|
||||||
|
}
|
||||||
|
/** Navigate to order history */
|
||||||
|
const goToHistory = () => {
|
||||||
|
navigate('/orders');
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Flex justify="space-between" className="rb:mb-4!">
|
||||||
|
<PageTabs
|
||||||
|
value={activeTab}
|
||||||
|
options={formatTabItems}
|
||||||
|
onChange={handleChangeTab}
|
||||||
|
/>
|
||||||
|
<RbButton className="rb:text-[#212332] rb:font-medium!" onClick={goToHistory}>
|
||||||
|
<div
|
||||||
|
className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/order/order.svg')]"
|
||||||
|
></div>
|
||||||
|
{t('pricing.orderHistory')}
|
||||||
|
</RbButton>
|
||||||
|
</Flex>
|
||||||
|
<BodyWrapper empty={data.length < 1}>
|
||||||
|
<Row gutter={[12, 12]} className="rb:max-h-[calc(100%-48px)]! rb:overflow-y-auto">
|
||||||
|
{data.map((pkg) => (
|
||||||
|
<Col key={pkg.id} span={8}>
|
||||||
|
<RbCard
|
||||||
|
className="rb:h-full! rb:shadow-md hover:rb:shadow-lg rb:transition-shadow"
|
||||||
|
bodyClassName="rb:p-6! rb:h-full!"
|
||||||
|
headerClassName="rb:min-h-0!"
|
||||||
|
>
|
||||||
|
<Flex vertical justify="space-between" className="rb:h-full!">
|
||||||
|
<div>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="rb:text-center rb:mb-6">
|
||||||
|
<h3 className="rb:text-xl rb:font-bold rb:mb-2 rb:min-h-7" style={{ color: pkg.theme_color }}>
|
||||||
|
{String(pkg[getKeyWithLanguage('name')] ?? '')}
|
||||||
|
</h3>
|
||||||
|
<p className="rb:text-sm rb:text-gray-500 rb:mb-4 rb:min-h-5">{String(pkg[getKeyWithLanguage('core_value')] ?? '')}</p>
|
||||||
|
<div className="rb:text-4xl rb:font-bold rb:mb-2">
|
||||||
|
{pkg.billing_cycle !== 'permanent_free' && <>¥{pkg.price}</>}
|
||||||
|
{pkg.billing_cycle && <span className={clsx("", {
|
||||||
|
'rb:text-base rb:font-normal rb:text-gray-500': pkg.billing_cycle !== 'permanent_free'
|
||||||
|
})}>{pkg.billing_cycle !== 'permanent_free' && '/'}{t(`package.${pkg.billing_cycle}`)}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Features */}
|
||||||
|
<div className="rb:space-y-3">
|
||||||
|
{billingUnits.map(({ key, unit }) => {
|
||||||
|
if (typeof pkg.quotas[key as keyof Package['quotas']] === 'number') {
|
||||||
|
return (
|
||||||
|
<div key={key} className="rb:flex rb:items-center rb:justify-between rb:text-sm">
|
||||||
|
<span className="rb:text-gray-500">{t(`package.${key}`)}</span>
|
||||||
|
<span>{pkg.quotas[key as keyof Package['quotas']]}{t(`package.${unit}`)}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
{pkg.api_ops_rate_limit &&
|
||||||
|
<div className="rb:flex rb:items-center rb:justify-between rb:text-sm">
|
||||||
|
<span className="rb:text-gray-500">{t(`package.api_ops_rate_limit`)}</span>
|
||||||
|
<span>{pkg.api_ops_rate_limit}{t('package.ops')}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{pkg.tech_support &&
|
||||||
|
<div className="rb:flex rb:items-center rb:justify-between rb:text-sm">
|
||||||
|
<span className="rb:text-gray-500">{t(`package.tech_support`)}</span>
|
||||||
|
<span>{String(pkg[getKeyWithLanguage('tech_support')] ?? '')}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
</RbCard>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
</BodyWrapper>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Package;
|
||||||
61
web/src/views/Package/types.ts
Normal file
61
web/src/views/Package/types.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ZhaoYing
|
||||||
|
* @Date: 2026-04-14 11:35:01
|
||||||
|
* @Last Modified by: ZhaoYing
|
||||||
|
* @Last Modified time: 2026-04-14 14:28:46
|
||||||
|
*/
|
||||||
|
export interface Package {
|
||||||
|
id: string;
|
||||||
|
// 名称
|
||||||
|
name: string;
|
||||||
|
name_en: string;
|
||||||
|
// 类型
|
||||||
|
category: "saas_personal" | "commercial_deployment";
|
||||||
|
tier_level: number;
|
||||||
|
// 版本
|
||||||
|
version: string;
|
||||||
|
// 状态
|
||||||
|
status: boolean;
|
||||||
|
// 价格
|
||||||
|
price: string;
|
||||||
|
// 计费周期
|
||||||
|
billing_cycle: "monthly" | "yearly" | "permanent_free" | "local_deployment";
|
||||||
|
// 核心价值
|
||||||
|
core_value: string;
|
||||||
|
core_value_en: string;
|
||||||
|
// 技术支持
|
||||||
|
tech_support: string;
|
||||||
|
tech_support_en: string;
|
||||||
|
// SLA与合规
|
||||||
|
sla_compliance: string;
|
||||||
|
sla_compliance_en: string;
|
||||||
|
// 对话页面个性化配置
|
||||||
|
page_customization: string;
|
||||||
|
page_customization_en: string;
|
||||||
|
// API OPS 频次(次/秒)
|
||||||
|
api_ops_rate_limit: number;
|
||||||
|
// 主题色
|
||||||
|
theme_color: string;
|
||||||
|
quotas: {
|
||||||
|
// 空间数量
|
||||||
|
workspace_quota: number;
|
||||||
|
// 技能库数量
|
||||||
|
skill_quota: number;
|
||||||
|
// 应用数量
|
||||||
|
app_quota: number;
|
||||||
|
// 知识库容量
|
||||||
|
knowledge_capacity_quota: string;
|
||||||
|
// 记忆引擎数量
|
||||||
|
memory_engine_quota: number;
|
||||||
|
// 可记忆终端用户数
|
||||||
|
end_user_quota: number;
|
||||||
|
// 本体工程
|
||||||
|
ontology_project_quota: number;
|
||||||
|
// 可负载模型数量
|
||||||
|
model_quota: number;
|
||||||
|
},
|
||||||
|
created_at: number;
|
||||||
|
updated_at: number;
|
||||||
|
created_by: string;
|
||||||
|
updated_by: string | null;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user