From e7370489e8f12bfe8e238a25ac5557e082757f3a Mon Sep 17 00:00:00 2001 From: Ke Sun <33739460+keeees@users.noreply.github.com> Date: Fri, 30 Jan 2026 15:10:22 +0800 Subject: [PATCH] Release/v0.2.2 (#260) * [modify] migration script * [add] migration script * fix(web): change form message * fix(web): the memoryContent field is compatible with numbers and strings * feat(web): code node hidden * fix(model): 1. create a basic model to check if the name and provider are duplicated. 2. The result shows error models because the provider created API Keys for all matching models. --------- Co-authored-by: Mark Co-authored-by: zhaoying Co-authored-by: yingzhao Co-authored-by: Timebomb2018 <18868801967@163.com> --- api/app/repositories/model_repository.py | 7 ++ api/app/services/model_service.py | 8 +- .../versions/325b759cd66b_2026011240.py | 10 ++- .../versions/5de9b1e28509_20260129212722.py | 80 +++++++++++++++++++ web/src/views/ApplicationConfig/Agent.tsx | 6 +- .../components/KeyConfigModal.tsx | 2 +- web/src/views/Workflow/constant.ts | 52 ++++++------ 7 files changed, 132 insertions(+), 33 deletions(-) create mode 100644 api/migrations/versions/5de9b1e28509_20260129212722.py diff --git a/api/app/repositories/model_repository.py b/api/app/repositories/model_repository.py index 36f7062f..3d66964a 100644 --- a/api/app/repositories/model_repository.py +++ b/api/app/repositories/model_repository.py @@ -630,6 +630,13 @@ class ModelBaseRepository: db.add(model_base) return model_base + @staticmethod + def get_by_name_and_provider(db: Session, name: str, provider: str) -> Optional['ModelBase']: + return db.query(ModelBase).filter( + ModelBase.name == name, + ModelBase.provider == provider + ).first() + @staticmethod def update(db: Session, model_base_id: uuid.UUID, data: dict) -> Optional['ModelBase']: model_base = db.query(ModelBase).filter(ModelBase.id == model_base_id).first() diff --git a/api/app/services/model_service.py b/api/app/services/model_service.py index 904821c1..dee6cd1d 100644 --- a/api/app/services/model_service.py +++ b/api/app/services/model_service.py @@ -508,10 +508,7 @@ class ModelApiKeyService: ) if not validation_result["valid"]: # 记录验证失败的模型,但不抛出异常 - failed_models.append({ - "model_name": model_name, - "error": validation_result["error"] - }) + failed_models.append(model_name) continue # 创建API Key @@ -692,6 +689,9 @@ class ModelBaseService: @staticmethod def create_model_base(db: Session, data: model_schema.ModelBaseCreate): + existing = ModelBaseRepository.get_by_name_and_provider(db, data.name, data.provider) + if existing: + raise BusinessException("模型已存在", BizCode.DUPLICATE_NAME) model_base = ModelBaseRepository.create(db, data.model_dump()) db.commit() db.refresh(model_base) diff --git a/api/migrations/versions/325b759cd66b_2026011240.py b/api/migrations/versions/325b759cd66b_2026011240.py index 3d7443a8..048b109b 100644 --- a/api/migrations/versions/325b759cd66b_2026011240.py +++ b/api/migrations/versions/325b759cd66b_2026011240.py @@ -28,7 +28,15 @@ def upgrade() -> None: op.drop_constraint('data_config_pkey', 'memory_config', type_='primary') op.alter_column('memory_config', 'config_id', new_column_name='config_id_old', nullable=True) op.add_column('memory_config', sa.Column('config_id', sa.UUID(), nullable=True)) - op.execute("UPDATE memory_config SET config_id = apply_id::uuid") + # Handle rows where apply_id might be NULL or invalid - generate new UUIDs for those + op.execute(""" + UPDATE memory_config + SET config_id = CASE + WHEN apply_id IS NOT NULL AND apply_id ~ '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' + THEN apply_id::uuid + ELSE gen_random_uuid() + END + """) op.alter_column('memory_config', 'config_id', nullable=False) op.create_primary_key('memory_config_pkey', 'memory_config', ['config_id']) op.execute("ALTER TABLE memory_config ALTER COLUMN config_id_old DROP DEFAULT") diff --git a/api/migrations/versions/5de9b1e28509_20260129212722.py b/api/migrations/versions/5de9b1e28509_20260129212722.py new file mode 100644 index 00000000..cbffad68 --- /dev/null +++ b/api/migrations/versions/5de9b1e28509_20260129212722.py @@ -0,0 +1,80 @@ +"""20260129212722 + +Revision ID: 5de9b1e28509 +Revises: 5ca246ee7dd4 +Create Date: 2026-01-29 21:34:30.978031 + +""" +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '5de9b1e28509' +down_revision: Union[str, None] = '5ca246ee7dd4' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Neo4j migration: rename group_id to end_user_id + import asyncio + + from app.repositories.neo4j.neo4j_connector import Neo4jConnector + + async def run_neo4j_upgrade(): + connector = Neo4jConnector() + try: + async def transaction_func(tx): + result = await tx.run(""" + MATCH (n) + WHERE n.group_id IS NOT NULL + SET n.end_user_id = n.group_id + REMOVE n.group_id + WITH count(n) AS node_count + MATCH ()-[r]->() + WHERE r.group_id IS NOT NULL + SET r.end_user_id = r.group_id + REMOVE r.group_id + RETURN node_count, count(r) AS rel_count + """) + return await result.data() + + await connector.execute_write_transaction(transaction_func) + finally: + await connector.close() + + asyncio.run(run_neo4j_upgrade()) + + +def downgrade() -> None: + # Neo4j migration: rename end_user_id back to group_id + import asyncio + + from app.repositories.neo4j.neo4j_connector import Neo4jConnector + + async def run_neo4j_downgrade(): + connector = Neo4jConnector() + try: + async def transaction_func(tx): + result = await tx.run(""" + MATCH (n) + WHERE n.end_user_id IS NOT NULL + SET n.group_id = n.end_user_id + REMOVE n.end_user_id + WITH count(n) AS node_count + MATCH ()-[r]->() + WHERE r.end_user_id IS NOT NULL + SET r.group_id = r.end_user_id + REMOVE r.end_user_id + RETURN node_count, count(r) AS rel_count + """) + return await result.data() + + await connector.execute_write_transaction(transaction_func) + finally: + await connector.close() + + asyncio.run(run_neo4j_downgrade()) \ No newline at end of file diff --git a/web/src/views/ApplicationConfig/Agent.tsx b/web/src/views/ApplicationConfig/Agent.tsx index 8898897a..0e9e8b44 100644 --- a/web/src/views/ApplicationConfig/Agent.tsx +++ b/web/src/views/ApplicationConfig/Agent.tsx @@ -126,12 +126,16 @@ const Agent = forwardRef((_props, ref) => { getApplicationConfig(id as string).then(res => { const response = res as Config let allTools = Array.isArray(response.tools) ? response.tools : [] + const memoryContent = response.memory?.memory_content + const parsedMemoryContent = memoryContent === null || memoryContent === '' + ? undefined + : !isNaN(Number(memoryContent)) ? Number(memoryContent) : memoryContent form.setFieldsValue({ ...response, tools: allTools, memory: { ...response.memory, - memory_content: response.memory?.memory_content ? Number(response.memory?.memory_content) : undefined + memory_content: parsedMemoryContent } }) setData({ diff --git a/web/src/views/ModelManagement/components/KeyConfigModal.tsx b/web/src/views/ModelManagement/components/KeyConfigModal.tsx index 3a4592ef..7481a6ad 100644 --- a/web/src/views/ModelManagement/components/KeyConfigModal.tsx +++ b/web/src/views/ModelManagement/components/KeyConfigModal.tsx @@ -72,7 +72,7 @@ const KeyConfigModal = forwardRef(({ diff --git a/web/src/views/Workflow/constant.ts b/web/src/views/Workflow/constant.ts index 64bb5757..25570afd 100644 --- a/web/src/views/Workflow/constant.ts +++ b/web/src/views/Workflow/constant.ts @@ -431,32 +431,32 @@ export const nodeLibrary: NodeLibrary[] = [ } } }, - { type: "code", icon: codeExecutionIcon, - config: { - input_variables: { - type: 'inputList', - defaultValue: [{ name: 'arg1' }, { name: 'arg2' }] - }, - language: { - type: 'select', - defaultValue: 'python3' - }, - code: { - type: 'messageEditor', - isArray: false, - language: ['python3', 'javascript'], - titleVariant: 'borderless', - defaultValue: `def main(arg1: str, arg2: str): - return { - "result": arg1 + arg2, - }` - }, - output_variables: { - type: 'outputList', - defaultValue: [{name: 'result', type: 'string'}] - }, - } - }, + // { type: "code", icon: codeExecutionIcon, + // config: { + // input_variables: { + // type: 'inputList', + // defaultValue: [{ name: 'arg1' }, { name: 'arg2' }] + // }, + // language: { + // type: 'select', + // defaultValue: 'python3' + // }, + // code: { + // type: 'messageEditor', + // isArray: false, + // language: ['python3', 'javascript'], + // titleVariant: 'borderless', + // defaultValue: `def main(arg1: str, arg2: str): + // return { + // "result": arg1 + arg2, + // }` + // }, + // output_variables: { + // type: 'outputList', + // defaultValue: [{name: 'result', type: 'string'}] + // }, + // } + // }, { type: "jinja-render", icon: templateRenderingIcon, config: { mapping: {