diff --git a/json/llm.json b/json/llm.json new file mode 100644 index 0000000..7f22219 --- /dev/null +++ b/json/llm.json @@ -0,0 +1,18 @@ +{ + "tblname": "llm", + "title": "大语言模型管理", + "params": { + "sortby": "created_at", + "browserfields": { + "exclouded": ["id", "api_key"], + "cwidth": {} + }, + "editexclouded": ["id", "created_at", "updated_at"], + "editable": { + "new_data_url": "{{entire_url('../api/llm_create.dspy')}}", + "update_data_url": "{{entire_url('../api/llm_update.dspy')}}", + "delete_data_url": "{{entire_url('../api/llm_delete.dspy')}}" + }, + "confidential_fields": ["api_key"] + } +} diff --git a/models/llm.json b/models/llm.json new file mode 100644 index 0000000..c4aee72 --- /dev/null +++ b/models/llm.json @@ -0,0 +1,25 @@ +{ + "summary": [ + { + "name": "llm", + "title": "大语言模型表", + "primary": ["id"] + } + ], + "fields": [ + {"name": "id", "title": "主键", "type": "str", "length": 32, "nullable": "no"}, + {"name": "name", "title": "模型名称", "type": "str", "length": 100, "nullable": "no"}, + {"name": "provider", "title": "供应商", "type": "str", "length": 50, "nullable": "no"}, + {"name": "model_id", "title": "模型标识", "type": "str", "length": 100}, + {"name": "api_base", "title": "API地址", "type": "str", "length": 500}, + {"name": "api_key", "title": "API密钥", "type": "str", "length": 500}, + {"name": "max_tokens", "title": "最大Token", "type": "int", "default": "8192"}, + {"name": "status", "title": "状态", "type": "str", "length": 20, "default": "active"}, + {"name": "description", "title": "描述", "type": "text"}, + {"name": "created_at", "title": "创建时间", "type": "timestamp"}, + {"name": "updated_at", "title": "更新时间", "type": "timestamp"} + ], + "indexes": [ + {"name": "idx_llm_status", "idxtype": "index", "idxfields": ["status"]} + ] +} diff --git a/models/pipeline_steps.json b/models/pipeline_steps.json index 7eb96ea..3763d8a 100644 --- a/models/pipeline_steps.json +++ b/models/pipeline_steps.json @@ -1,110 +1,122 @@ { - "summary": [ - { - "name": "pipeline_steps", - "title": "产线步骤表", - "primary": [ - "id" - ] - } - ], - "fields": [ - { - "name": "id", - "title": "id", - "type": "str", - "length": 32, - "nullable": "no" - }, - { - "name": "pipeline_id", - "title": "所属产线", - "type": "str", - "length": 32, - "nullable": "no" - }, - { - "name": "step_order", - "title": "步骤序号", - "type": "int", - "nullable": "no" - }, - { - "name": "step_name", - "title": "步骤名称", - "type": "str", - "length": 100, - "nullable": "no" - }, - { - "name": "step_type", - "title": "步骤类型", - "type": "str", - "length": 50, - "nullable": "no" - }, - { - "name": "model_name", - "title": "调用模型名称", - "type": "str", - "length": 100 - }, - { - "name": "step_config", - "title": "步骤配置JSON", - "type": "text" - }, - { - "name": "input_schema", - "title": "输入定义JSON", - "type": "text" - }, - { - "name": "output_schema", - "title": "输出定义JSON", - "type": "text" - }, - { - "name": "timeout_seconds", - "title": "超时秒数", - "type": "int", - "default": "300" - }, - { - "name": "retry_count", - "title": "重试次数", - "type": "int", - "default": "0" - }, - { - "name": "created_at", - "title": "创建时间", - "type": "timestamp" - } - ], - "indexes": [ - { - "name": "idx_steps_pipeline", - "idxtype": "index", - "idxfields": [ - "pipeline_id" - ] - }, - { - "name": "idx_steps_order", - "idxtype": "unique", - "idxfields": [ - "pipeline_id", - "step_order" - ] - } - ], - "codes": [ - { - "field": "pipeline_id", - "table": "pipelines", - "valuefield": "id", - "textfield": "name" - } - ] -} + "summary": [ + { + "name": "pipeline_steps", + "title": "产线步骤表", + "primary": [ + "id" + ] + } + ], + "fields": [ + { + "name": "id", + "title": "id", + "type": "str", + "length": 32, + "nullable": "no", + "uitype": "hidden" + }, + { + "name": "pipeline_id", + "title": "所属产线", + "type": "str", + "length": 32, + "nullable": "no", + "uitype": "select" + }, + { + "name": "step_order", + "title": "步骤序号", + "type": "int", + "nullable": "no", + "uitype": "number" + }, + { + "name": "step_name", + "title": "步骤名称", + "type": "str", + "length": 100, + "nullable": "no", + "uitype": "text" + }, + { + "name": "step_type", + "title": "步骤类型", + "type": "str", + "length": 50, + "nullable": "no", + "uitype": "select" + }, + { + "name": "model_name", + "title": "调用模型名称", + "type": "str", + "length": 100, + "uitype": "text" + }, + { + "name": "step_config", + "title": "步骤配置JSON", + "type": "text", + "uitype": "textarea" + }, + { + "name": "input_schema", + "title": "输入定义JSON", + "type": "text", + "uitype": "textarea" + }, + { + "name": "output_schema", + "title": "输出定义JSON", + "type": "text", + "uitype": "textarea" + }, + { + "name": "timeout_seconds", + "title": "超时秒数", + "type": "int", + "default": "300", + "uitype": "number" + }, + { + "name": "retry_count", + "title": "重试次数", + "type": "int", + "default": "0", + "uitype": "number" + }, + { + "name": "created_at", + "title": "创建时间", + "type": "timestamp", + "uitype": "timestamp" + } + ], + "indexes": [ + { + "name": "idx_steps_pipeline", + "idxtype": "index", + "idxfields": [ + "pipeline_id" + ] + }, + { + "name": "idx_steps_order", + "idxtype": "unique", + "idxfields": [ + "pipeline_id", + "step_order" + ] + } + ], + "codes": [ + { + "field": "pipeline_id", + "table": "pipelines", + "valuefield": "id", + "textfield": "name" + } + ] +} \ No newline at end of file diff --git a/models/pipeline_versions.json b/models/pipeline_versions.json index d524263..a1ed0cc 100644 --- a/models/pipeline_versions.json +++ b/models/pipeline_versions.json @@ -1,85 +1,94 @@ { - "summary": [ - { - "name": "pipeline_versions", - "title": "产线发布记录表", - "primary": [ - "id" - ] - } - ], - "fields": [ - { - "name": "id", - "title": "id", - "type": "str", - "length": 32, - "nullable": "no" - }, - { - "name": "pipeline_id", - "title": "产线ID", - "type": "str", - "length": 32, - "nullable": "no" - }, - { - "name": "version", - "title": "版本号", - "type": "str", - "length": 20, - "nullable": "no" - }, - { - "name": "publish_status", - "title": "发布状态", - "type": "str", - "length": 20, - "nullable": "no", - "default": "pending" - }, - { - "name": "published_by", - "title": "发布人", - "type": "str", - "length": 32 - }, - { - "name": "published_at", - "title": "发布时间", - "type": "timestamp" - }, - { - "name": "changelog", - "title": "变更说明", - "type": "text" - }, - { - "name": "config_snapshot", - "title": "配置快照JSON", - "type": "text" - }, - { - "name": "created_at", - "title": "创建时间", - "type": "timestamp" - } - ], - "indexes": [ - { - "name": "idx_versions_pipeline", - "idxtype": "index", - "idxfields": [ - "pipeline_id" - ] - } - ], - "codes": [ - { - "field": "pipeline_id", - "table": "pipelines", - "valuefield": "id", - "textfield": "name" - } - ] -} + "summary": [ + { + "name": "pipeline_versions", + "title": "产线发布记录表", + "primary": [ + "id" + ] + } + ], + "fields": [ + { + "name": "id", + "title": "id", + "type": "str", + "length": 32, + "nullable": "no", + "uitype": "hidden" + }, + { + "name": "pipeline_id", + "title": "产线ID", + "type": "str", + "length": 32, + "nullable": "no", + "uitype": "hidden" + }, + { + "name": "version", + "title": "版本号", + "type": "str", + "length": 20, + "nullable": "no", + "uitype": "text" + }, + { + "name": "publish_status", + "title": "发布状态", + "type": "str", + "length": 20, + "nullable": "no", + "default": "pending", + "uitype": "select" + }, + { + "name": "published_by", + "title": "发布人", + "type": "str", + "length": 32, + "uitype": "text" + }, + { + "name": "published_at", + "title": "发布时间", + "type": "timestamp", + "uitype": "timestamp" + }, + { + "name": "changelog", + "title": "变更说明", + "type": "text", + "uitype": "textarea" + }, + { + "name": "config_snapshot", + "title": "配置快照JSON", + "type": "text", + "uitype": "textarea" + }, + { + "name": "created_at", + "title": "创建时间", + "type": "timestamp", + "uitype": "timestamp" + } + ], + "indexes": [ + { + "name": "idx_versions_pipeline", + "idxtype": "index", + "idxfields": [ + "pipeline_id" + ] + } + ], + "codes": [ + { + "field": "pipeline_id", + "table": "pipelines", + "valuefield": "id", + "textfield": "name" + } + ] +} \ No newline at end of file diff --git a/models/pipelines.json b/models/pipelines.json index 8c930eb..893f0a7 100644 --- a/models/pipelines.json +++ b/models/pipelines.json @@ -1,126 +1,139 @@ { - "summary": [ - { - "name": "pipelines", - "title": "产线定义表", - "primary": [ - "id" - ] - } - ], - "fields": [ - { - "name": "id", - "title": "id", - "type": "str", - "length": 32, - "nullable": "no" - }, - { - "name": "name", - "title": "产线名称", - "type": "str", - "length": 200, - "nullable": "no" - }, - { - "name": "description", - "title": "产线描述", - "type": "text" - }, - { - "name": "pipeline_type", - "title": "产线类型", - "type": "str", - "length": 50, - "nullable": "no" - }, - { - "name": "version", - "title": "当前版本", - "type": "str", - "length": 20, - "default": "1.0.0" - }, - { - "name": "status", - "title": "状态", - "type": "str", - "length": 20, - "nullable": "no", - "default": "draft" - }, - { - "name": "pipeline_config", - "title": "产线配置JSON", - "type": "text" - }, - { - "name": "model_api_url", - "title": "模型API地址", - "type": "str", - "length": 500 - }, - { - "name": "model_api_key", - "title": "模型API密钥", - "type": "str", - "length": 500 - }, - { - "name": "org_id", - "title": "所属机构ID", - "type": "str", - "length": 32, - "default": "0" - }, - { - "name": "created_by", - "title": "创建人", - "type": "str", - "length": 32 - }, - { - "name": "created_at", - "title": "创建时间", - "type": "timestamp" - }, - { - "name": "updated_at", - "title": "更新时间", - "type": "timestamp" - } - ], - "indexes": [ - { - "name": "idx_pipelines_name", - "idxtype": "index", - "idxfields": [ - "name" - ] - }, - { - "name": "idx_pipelines_status", - "idxtype": "index", - "idxfields": [ - "status" - ] - } - ], - "codes": [ - { - "field": "pipeline_type", - "table": "appcodes_kv", - "valuefield": "k", - "textfield": "v", - "cond": "parentid='pipeline_type'" - }, - { - "field": "status", - "table": "appcodes_kv", - "valuefield": "k", - "textfield": "v", - "cond": "parentid='pipeline_status'" - } - ] + "summary": [ + { + "name": "pipelines", + "title": "产线定义表", + "primary": [ + "id" + ] + } + ], + "fields": [ + { + "name": "id", + "title": "id", + "type": "str", + "length": 32, + "nullable": "no", + "uitype": "hidden" + }, + { + "name": "name", + "title": "产线名称", + "type": "str", + "length": 200, + "nullable": "no", + "uitype": "text" + }, + { + "name": "description", + "title": "产线描述", + "type": "text", + "uitype": "textarea" + }, + { + "name": "pipeline_type", + "title": "产线类型", + "type": "str", + "length": 50, + "nullable": "no", + "uitype": "select" + }, + { + "name": "version", + "title": "当前版本", + "type": "str", + "length": 20, + "default": "1.0.0", + "uitype": "text" + }, + { + "name": "status", + "title": "状态", + "type": "str", + "length": 20, + "nullable": "no", + "default": "draft", + "uitype": "select" + }, + { + "name": "pipeline_config", + "title": "产线配置JSON", + "type": "text", + "uitype": "textarea" + }, + { + "name": "model_api_url", + "title": "模型API地址", + "type": "str", + "length": 500, + "uitype": "text" + }, + { + "name": "model_api_key", + "title": "模型API密钥", + "type": "str", + "length": 500, + "uitype": "text" + }, + { + "name": "org_id", + "title": "所属机构ID", + "type": "str", + "length": 32, + "default": "0", + "uitype": "text" + }, + { + "name": "created_by", + "title": "创建人", + "type": "str", + "length": 32, + "uitype": "hidden" + }, + { + "name": "created_at", + "title": "创建时间", + "type": "timestamp", + "uitype": "timestamp" + }, + { + "name": "updated_at", + "title": "更新时间", + "type": "timestamp", + "uitype": "timestamp" + } + ], + "indexes": [ + { + "name": "idx_pipelines_name", + "idxtype": "index", + "idxfields": [ + "name" + ] + }, + { + "name": "idx_pipelines_status", + "idxtype": "index", + "idxfields": [ + "status" + ] + } + ], + "codes": [ + { + "field": "pipeline_type", + "table": "appcodes_kv", + "valuefield": "k", + "textfield": "v", + "cond": "parentid='pipeline_type'" + }, + { + "field": "status", + "table": "appcodes_kv", + "valuefield": "k", + "textfield": "v", + "cond": "parentid='pipeline_status'" + } + ] } \ No newline at end of file diff --git a/pipeline_core/init.py b/pipeline_core/init.py index f9846f8..74c9550 100644 --- a/pipeline_core/init.py +++ b/pipeline_core/init.py @@ -249,6 +249,54 @@ async def publish_pipeline(params_kw): return json.dumps(result, ensure_ascii=False, default=str) +async def create_llm(params_kw): + result = {'success': False, 'message': ''} + try: + db, dbname = _get_sor() + async with db.sqlorContext(dbname) as sor: + data = params_kw.copy() + data.pop('page', None) + data.pop('rows', None) + data.pop('data_filter', None) + data['id'] = getID() + await sor.C('llm', data) + result['success'] = True + result['message'] = '创建成功' + except Exception as e: + result['message'] = str(e) + return json.dumps(result, ensure_ascii=False, default=str) + + +async def update_llm(params_kw): + result = {'success': False, 'message': ''} + try: + db, dbname = _get_sor() + async with db.sqlorContext(dbname) as sor: + data = params_kw.copy() + data.pop('page', None) + data.pop('rows', None) + data.pop('data_filter', None) + await sor.U('llm', data) + result['success'] = True + result['message'] = '更新成功' + except Exception as e: + result['message'] = str(e) + return json.dumps(result, ensure_ascii=False, default=str) + + +async def delete_llm(params_kw): + result = {'success': False, 'message': ''} + try: + db, dbname = _get_sor() + async with db.sqlorContext(dbname) as sor: + await sor.D('llm', {'id': params_kw.get('id')}) + result['success'] = True + result['message'] = '删除成功' + except Exception as e: + result['message'] = str(e) + return json.dumps(result, ensure_ascii=False, default=str) + + def load_pipeline_core(): """注册函数到 ServerEnv""" env = ServerEnv() @@ -259,6 +307,10 @@ def load_pipeline_core(): env.update_pipelines = update_pipeline env.delete_pipeline = delete_pipeline env.delete_pipelines = delete_pipeline + # LLM + env.create_llm = create_llm + env.update_llm = update_llm + env.delete_llm = delete_llm # Pipeline Steps env.create_pipeline_step = create_pipeline_step env.create_pipeline_steps = create_pipeline_step diff --git a/scripts/load_path.py b/scripts/load_path.py index 72aa88c..d2c4241 100644 --- a/scripts/load_path.py +++ b/scripts/load_path.py @@ -1,88 +1,69 @@ #!/usr/bin/env python3 -""" -pipeline_core 模块 RBAC 权限管理脚本 - -使用方法: - cd ~/repos/sage - ./py3/bin/python ~/test/pipeline-app/pipeline_core/scripts/load_path.py -""" - -import subprocess -import os -import sys - - -def find_sage_root(): - candidates = [ - os.path.expanduser("~/repos/sage"), - os.path.expanduser("~/sage"), - ] - for c in candidates: - if os.path.isdir(os.path.join(c, "py3")) and os.path.isdir(os.path.join(c, "wwwroot")): - return c - return None - - -SAGE_ROOT = find_sage_root() -if not SAGE_ROOT: - print("ERROR: Cannot find Sage root directory") - sys.exit(1) - -PYTHON = os.path.join(SAGE_ROOT, "py3", "bin", "python") -SET_PERM_SCRIPT = os.path.join(SAGE_ROOT, "set_role_perm.py") +"""RBAC path registration for pipeline_core module.""" +import os, sys, subprocess MOD = "pipeline_core" -# ============================================================ -# 权限路径定义 -# ============================================================ - -# operator — 产线管理人员 -PATHS_OPERATOR = [ - f"/{MOD}", +PATHS_ANY = [ f"/{MOD}/index.ui", - f"/{MOD}/pipelines/", - f"/{MOD}/pipeline_steps/", - f"/{MOD}/pipeline_versions/", - f"/{MOD}/api/pipelines_create.dspy", - f"/{MOD}/api/pipelines_update.dspy", - f"/{MOD}/api/pipelines_delete.dspy", +] + +PATHS_LOGINED = [ + f"/{MOD}", + # Pipelines CRUD + f"/{MOD}/pipelines/index.ui", + f"/{MOD}/pipelines/get_pipelines.dspy", + f"/{MOD}/pipelines/add_pipelines.dspy", + f"/{MOD}/pipelines/update_pipelines.dspy", + f"/{MOD}/pipelines/delete_pipelines.dspy", + # Pipeline Steps CRUD + f"/{MOD}/pipeline_steps/index.ui", + f"/{MOD}/pipeline_steps/get_pipeline_steps.dspy", + f"/{MOD}/pipeline_steps/add_pipeline_steps.dspy", + f"/{MOD}/pipeline_steps/update_pipeline_steps.dspy", + f"/{MOD}/pipeline_steps/delete_pipeline_steps.dspy", + # Pipeline Versions + f"/{MOD}/pipeline_versions/index.ui", + f"/{MOD}/pipeline_versions/get_pipeline_versions.dspy", + f"/{MOD}/pipeline_versions/add_pipeline_versions.dspy", + f"/{MOD}/pipeline_versions/update_pipeline_versions.dspy", + f"/{MOD}/pipeline_versions/delete_pipeline_versions.dspy", + # Pipeline Editor + f"/{MOD}/pipeline_editor/index.ui", + f"/{MOD}/pipeline_editor/save_steps.dspy", + # Data sources + f"/{MOD}/pipeline_steps/get_pipelines.dspy", + f"/{MOD}/pipeline_steps/get_step_types.dspy", + # API + f"/{MOD}/api/pipeline_publish.dspy", f"/{MOD}/api/pipeline_steps_create.dspy", f"/{MOD}/api/pipeline_steps_update.dspy", f"/{MOD}/api/pipeline_steps_delete.dspy", f"/{MOD}/api/pipeline_versions_create.dspy", f"/{MOD}/api/pipeline_versions_update.dspy", f"/{MOD}/api/pipeline_versions_delete.dspy", - f"/{MOD}/api/pipeline_publish.dspy", + f"/{MOD}/api/pipelines_create.dspy", + f"/{MOD}/api/pipelines_update.dspy", + f"/{MOD}/api/pipelines_delete.dspy", ] -# developer — 开发者(包含所有 operator 路径) -PATHS_DEVELOPER = PATHS_OPERATOR[:] - - -def run_set_perm(role, path): - cmd = [PYTHON, SET_PERM_SCRIPT, role, path] - result = subprocess.run(cmd, capture_output=True, text=True) - return result.returncode == 0 - - -def register_role_paths(role, paths): - count = 0 - for p in paths: - if run_set_perm(role, p): - count += 1 - print(f" {role}: {count}/{len(paths)} paths registered") - return count - def main(): - print(f"Sage root: {SAGE_ROOT}") - print("Registering pipeline_core module RBAC permissions...") - total = 0 - total += register_role_paths("operator", PATHS_OPERATOR) - total += register_role_paths("developer", PATHS_DEVELOPER) - print(f"\nDone. Total {total} permission entries registered.") - print("NOTE: Restart Sage after permission changes to reload RBAC cache.") + root = os.path.expanduser("~/pipeline") + if not os.path.isfile(os.path.join(root, "set_role_perm.py")): + print("ERROR: pipeline root not found") + sys.exit(1) + set_perm = os.path.join(root, "set_role_perm.py") + py = sys.executable + count = 0 + for role, paths in [("any", PATHS_ANY), ("logined", PATHS_LOGINED)]: + for path in paths: + r = subprocess.run([py, set_perm, role, path], capture_output=True, text=True, cwd=root) + if r.returncode == 0: + count += 1 + else: + print(f" WARN: {path} -> {r.stderr.strip()[-80:]}") + print(f"Registered {count}/{len(PATHS_ANY)+len(PATHS_LOGINED)} paths for {MOD}") if __name__ == "__main__": diff --git a/wwwroot/index.ui b/wwwroot/index.ui index c55127a..7a49d8c 100644 --- a/wwwroot/index.ui +++ b/wwwroot/index.ui @@ -1,68 +1,254 @@ { - "widgettype": "VBox", - "options": {"width": "100%", "height": "100%", "padding": "0"}, - "subwidgets": [ + "widgettype": "VBox", + "options": { + "width": "100%", + "height": "100%", + "padding": "0" + }, + "subwidgets": [ + { + "widgettype": "HBox", + "options": { + "width": "100%", + "alignItems": "center", + "padding": "24px 24px 12px 24px", + "cheight": 6 + }, + "subwidgets": [ { - "widgettype": "HBox", - "options": {"width": "100%", "alignItems": "center", "marginBottom": "20px"}, - "subwidgets": [ - {"widgettype": "Title2", "options": {"text": "产线管理"}}, - {"widgettype": "Filler"}, - {"widgettype": "Text", "options": {"text": "产线定义、步骤配置与发布管理", "cfontsize": 1.2}} - ] + "widgettype": "Title2", + "options": { + "text": "产线管理" + } }, { - "widgettype": "VBox", - "options": {"css": "filler", "spacing": 16}, - "subwidgets": [ + "widgettype": "Filler" + }, + { + "widgettype": "Text", + "options": { + "text": "产线定义、步骤配置与发布管理", + "cfontsize": 1.2 + } + } + ] + }, + { + "widgettype": "HBox", + "options": { + "width": "100%", + "padding": "12px 24px 12px 24px", + "cheight": 18 + }, + "subwidgets": [ + { + "widgettype": "ResponsableBox", + "options": { + "gap": "24px", + "minWidth": "260px" + }, + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "text": "EDITOR CARD HERE" + } + }, + { + "widgettype": "VBox", + "options": { + "css": "card", + "cwidth": 23, + "cheight": 12, + "padding": "24px", + "cursor": "pointer", + "bgcolor": "#fff" + }, + "binds": [ { - "widgettype": "ResponsableBox", - "options": {"gap": "24px", "minWidth": "260px"}, - "subwidgets": [ - { - "widgettype": "VBox", - "options": {"css": "card", "cwidth": 23, "cheight": 12, "padding": "24px", "cursor": "pointer", "bgcolor": "#fff"}, - "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.pipeline_core_content", "options": {"url": "{{entire_url('pipelines/')}}"}, "mode": "replace"}], - "subwidgets": [ - {"widgettype": "VBox", "options": {"width": "44px", "height": "44px", "bgcolor": "#EFF6FF", "borderRadius": "12px", "alignItems": "center", "justifyContent": "center", "marginBottom": "12px"}, "subwidgets": [ - {"widgettype": "Svg", "options": {"svg": "", "width": "24px", "height": "24px"}} - ]}, - {"widgettype": "Title3", "options": {"text": "产线定义", "marginBottom": "4px"}}, - {"widgettype": "Text", "options": {"text": "管理产线基本信息与配置", "cfontsize": 1.2}} - ] - }, - { - "widgettype": "VBox", - "options": {"css": "card", "cwidth": 23, "cheight": 12, "padding": "24px", "cursor": "pointer", "bgcolor": "#fff"}, - "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.pipeline_core_content", "options": {"url": "{{entire_url('pipeline_steps/')}}"}, "mode": "replace"}], - "subwidgets": [ - {"widgettype": "VBox", "options": {"width": "44px", "height": "44px", "bgcolor": "#F0FDF4", "borderRadius": "12px", "alignItems": "center", "justifyContent": "center", "marginBottom": "12px"}, "subwidgets": [ - {"widgettype": "Svg", "options": {"svg": "", "width": "24px", "height": "24px"}} - ]}, - {"widgettype": "Title3", "options": {"text": "产线步骤", "marginBottom": "4px"}}, - {"widgettype": "Text", "options": {"text": "配置产线执行步骤与参数", "cfontsize": 1.2}} - ] - }, - { - "widgettype": "VBox", - "options": {"css": "card", "cwidth": 23, "cheight": 12, "padding": "24px", "cursor": "pointer", "bgcolor": "#fff"}, - "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.pipeline_core_content", "options": {"url": "{{entire_url('pipeline_versions/')}}"}, "mode": "replace"}], - "subwidgets": [ - {"widgettype": "VBox", "options": {"width": "44px", "height": "44px", "bgcolor": "#FFFBEB", "borderRadius": "12px", "alignItems": "center", "justifyContent": "center", "marginBottom": "12px"}, "subwidgets": [ - {"widgettype": "Svg", "options": {"svg": "", "width": "24px", "height": "24px"}} - ]}, - {"widgettype": "Title3", "options": {"text": "发布记录", "marginBottom": "4px"}}, - {"widgettype": "Text", "options": {"text": "查看产线版本发布历史", "cfontsize": 1.2}} - ] - } - ] + "wid": "self", + "event": "click", + "actiontype": "urlwidget", + "target": "app.main_content", + "options": { + "url": "{{entire_url('/pipeline_core/pipelines/')}}" + }, + "mode": "replace" + } + ], + "subwidgets": [ + { + "widgettype": "VBox", + "options": { + "width": "44px", + "height": "44px", + "bgcolor": "#EFF6FF", + "borderRadius": "12px", + "alignItems": "center", + "justifyContent": "center", + "marginBottom": "12px" + }, + "subwidgets": [ + { + "widgettype": "Svg", + "options": { + "svg": "", + "width": "24px", + "height": "24px" + } + } + ] }, { - "widgettype": "VScrollPanel", - "id": "pipeline_core_content", - "options": {"css": "filler", "width": "100%", "height": "100%"} + "widgettype": "Title3", + "options": { + "text": "产线定义", + "marginBottom": "4px" + } + }, + { + "widgettype": "Text", + "options": { + "text": "管理产线基本信息与配置", + "cfontsize": 1.2 + } } - ] + ] + }, + { + "widgettype": "VBox", + "options": { + "css": "card", + "cwidth": 23, + "cheight": 12, + "padding": "24px", + "cursor": "pointer", + "bgcolor": "#fff" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "urlwidget", + "target": "app.main_content", + "options": { + "url": "{{entire_url('/pipeline_core/pipeline_steps/')}}" + }, + "mode": "replace" + } + ], + "subwidgets": [ + { + "widgettype": "VBox", + "options": { + "width": "44px", + "height": "44px", + "bgcolor": "#F0FDF4", + "borderRadius": "12px", + "alignItems": "center", + "justifyContent": "center", + "marginBottom": "12px" + }, + "subwidgets": [ + { + "widgettype": "Svg", + "options": { + "svg": "", + "width": "24px", + "height": "24px" + } + } + ] + }, + { + "widgettype": "Title3", + "options": { + "text": "产线步骤", + "marginBottom": "4px" + } + }, + { + "widgettype": "Text", + "options": { + "text": "配置产线执行步骤与参数", + "cfontsize": 1.2 + } + } + ] + }, + { + "widgettype": "VBox", + "options": { + "css": "card", + "cwidth": 23, + "cheight": 12, + "padding": "24px", + "cursor": "pointer", + "bgcolor": "#fff" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "urlwidget", + "target": "app.main_content", + "options": { + "url": "{{entire_url('/pipeline_core/pipeline_versions/')}}" + }, + "mode": "replace" + } + ], + "subwidgets": [ + { + "widgettype": "VBox", + "options": { + "width": "44px", + "height": "44px", + "bgcolor": "#FFFBEB", + "borderRadius": "12px", + "alignItems": "center", + "justifyContent": "center", + "marginBottom": "12px" + }, + "subwidgets": [ + { + "widgettype": "Svg", + "options": { + "svg": "", + "width": "24px", + "height": "24px" + } + } + ] + }, + { + "widgettype": "Title3", + "options": { + "text": "发布记录", + "marginBottom": "4px" + } + }, + { + "widgettype": "Text", + "options": { + "text": "查看产线版本发布历史", + "cfontsize": 1.2 + } + } + ] + } + ] } - ] -} + ] + }, + { + "widgettype": "VScrollPanel", + "id": "pipeline_core_content", + "options": { + "css": "filler", + "width": "100%", + "height": "100%" + } + } + ] +} \ No newline at end of file diff --git a/wwwroot/pipeline_editor/index.ui b/wwwroot/pipeline_editor/index.ui new file mode 100644 index 0000000..6812015 --- /dev/null +++ b/wwwroot/pipeline_editor/index.ui @@ -0,0 +1,85 @@ +{ + "widgettype": "VBox", + "options": {"width": "100%", "height": "100%", "padding": "24px", "gap": "20px"}, + "subwidgets": [ + { + "widgettype": "HBox", + "options": {"alignItems": "center", "gap": "16px", "padding": "0 0 16px 0", "borderBottom": "1px solid #334155"}, + "subwidgets": [ + {"widgettype": "Title2", "options": {"text": "产线编排画布"}}, + {"widgettype": "Filler"}, + { + "widgettype": "Button", + "options": {"label": "发布版本", "bgcolor": "#6366f1", "color": "#fff"}, + "binds": [{"wid": "self", "event": "click", "actiontype": "method", "target": "app.pipeline_editor", "method": "publish"}] + } + ] + }, + { + "widgettype": "HBox", + "options": {"gap": "12px", "alignItems": "center"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "选择产线:", "cfontsize": 1, "color": "#e2e8f0"}}, + { + "widgettype": "DataSelector", + "id": "ds_pipelines", + "options": { + "data_url": "{{entire_url('/pipeline_core/pipeline_steps/get_pipelines.dspy')}}", + "valueField": "id", + "textField": "name", + "width": "300px", + "placeholder": "选择要编辑的产线..." + }, + "binds": [{"wid": "self", "event": "change", "actiontype": "method", "target": "app.pipeline_editor", "method": "load_steps"}] + }, + { + "widgettype": "DataSelector", + "id": "ds_step_types", + "options": { + "data_url": "{{entire_url('/pipeline_core/pipeline_steps/get_step_types.dspy')}}", + "valueField": "step_type", + "textField": "display_name", + "width": "240px", + "placeholder": "添加步骤类型..." + } + }, + { + "widgettype": "Button", + "options": {"label": "+ 添加步骤", "bgcolor": "#4ade80", "color": "#000"}, + "binds": [{"wid": "self", "event": "click", "actiontype": "method", "target": "app.pipeline_editor", "method": "add_step"}] + } + ] + }, + { + "widgettype": "DataViewer", + "id": "dv_steps", + "options": { + "data_url": "", + "width": "100%", + "columns": ["id", "step_order", "step_name", "step_type"] + } + }, + { + "widgettype": "VBox", + "id": "steps_canvas", + "options": {"width": "100%", "gap": "8px", "padding": "0", "minHeight": "300px"}, + "subwidgets": [ + { + "widgettype": "Text", + "options": {"text": "选择产线后,步骤将在此显示。可以拖拽排序、点击编辑。", "cfontsize": 1, "color": "#64748b", "padding": "40px", "alignItems": "center"} + } + ] + }, + { + "widgettype": "HBox", + "options": {"gap": "12px", "justifyContent": "flex-end", "padding": "16px 0 0 0", "borderTop": "1px solid #334155"}, + "subwidgets": [ + { + "widgettype": "Button", + "options": {"label": "保存排序", "bgcolor": "#3b82f6", "color": "#fff"}, + "binds": [{"wid": "self", "event": "click", "actiontype": "method", "target": "app.pipeline_editor", "method": "save_order"}] + } + ] + } + ] +} diff --git a/wwwroot/pipeline_editor/save_steps.dspy b/wwwroot/pipeline_editor/save_steps.dspy new file mode 100644 index 0000000..c0cb516 --- /dev/null +++ b/wwwroot/pipeline_editor/save_steps.dspy @@ -0,0 +1,47 @@ +""" +Pipeline Editor: Save step order after reordering +POST params: pipeline_id, steps (JSON array of {id, step_order}) +""" +from sqlor.dbpools import DBPools + +async def main(): + userid = await get_user() + if not userid: + return {'error': 'unauthorized'} + + pipeline_id = params_kw.pipeline_id + steps = params_kw.steps # JSON array + + if not pipeline_id or not steps: + return {'error': 'missing pipeline_id or steps'} + + if isinstance(steps, str): + import json + steps = json.loads(steps) + + dbname = get_module_dbname('pipeline_core') + async with DBPools().sqlorContext(dbname) as sor: + for i, step in enumerate(steps): + await sor.U('pipeline_steps', { + 'id': step['id'], + 'step_order': i + 1, + 'step_name': step.get('step_name', ''), + 'step_type': step.get('step_type', ''), + 'step_config': step.get('step_config', '{}'), + 'timeout_seconds': step.get('timeout_seconds', 300), + 'retry_count': step.get('retry_count', 0) + }) + + # Create version record + from appPublic.uniqueID import getID + vid = getID() + await sor.C('pipeline_versions', { + 'id': vid, + 'pipeline_id': pipeline_id, + 'version': f"v{len(steps)}.0", + 'publish_status': 'draft', + 'changelog': 'Editor save', + 'config_snapshot': json.dumps(steps, ensure_ascii=False) if 'json' in dir() else str(steps) + }) + + return {'success': True, 'version_id': vid, 'step_count': len(steps)} diff --git a/wwwroot/pipeline_steps/get_pipeline_steps.dspy b/wwwroot/pipeline_steps/get_pipeline_steps.dspy index 48c6209..5a6875b 100644 --- a/wwwroot/pipeline_steps/get_pipeline_steps.dspy +++ b/wwwroot/pipeline_steps/get_pipeline_steps.dspy @@ -1,130 +1,14 @@ -ns = params_kw.copy() +from sqlor.dbpools import DBPools - -debug(f'get_pipeline_steps.dspy:{ns=}') -if not ns.get('page'): - ns['page'] = 1 -if not ns.get('sort'): - - - ns['sort'] = 'step_order' - - - -sql = '''select a.*, b.pipeline_id_text -from (select * from pipeline_steps where 1=1 [[filterstr]]) a left join (select id as pipeline_id, - name as pipeline_id_text from pipelines where 1 = 1) b on a.pipeline_id = b.pipeline_id''' - -filterjson = params_kw.get('data_filter') -if filterjson and isinstance(filterjson, str): - try: - filterjson = json.loads(filterjson) - except (json.JSONDecodeError, TypeError): - filterjson = None -fields_str=r'''[ - { - "name": "id", - "title": "id", - "type": "str", - "length": 32, - "nullable": "no" - }, - { - "name": "pipeline_id", - "title": "所属产线", - "type": "str", - "length": 32, - "nullable": "no" - }, - { - "name": "step_order", - "title": "步骤序号", - "type": "int", - "nullable": "no" - }, - { - "name": "step_name", - "title": "步骤名称", - "type": "str", - "length": 100, - "nullable": "no" - }, - { - "name": "step_type", - "title": "步骤类型", - "type": "str", - "length": 50, - "nullable": "no" - }, - { - "name": "model_name", - "title": "调用模型名称", - "type": "str", - "length": 100 - }, - { - "name": "step_config", - "title": "步骤配置JSON", - "type": "text" - }, - { - "name": "input_schema", - "title": "输入定义JSON", - "type": "text" - }, - { - "name": "output_schema", - "title": "输出定义JSON", - "type": "text" - }, - { - "name": "timeout_seconds", - "title": "超时秒数", - "type": "int", - "default": "300" - }, - { - "name": "retry_count", - "title": "重试次数", - "type": "int", - "default": "0" - }, - { - "name": "created_at", - "title": "创建时间", - "type": "timestamp" - } -]''' -ori_fields = json.loads(fields_str) -if not filterjson: - fields = [ f['name'] for f in ori_fields ] - filterjson = default_filterjson(fields, ns) - -filterdic = ns.copy() -filterdic['filterstr'] = '' -filterdic['userorgid'] = '${userorgid}$' -filterdic['userid'] = '${userid}$' -if filterjson: - dbf = DBFilter(filterjson) - conds = dbf.gen(ns) - if conds: - ns.update(dbf.consts) - conds = f' and {conds}' - filterdic['filterstr'] = conds -ac = ArgsConvert('[[', ']]') -vars = ac.findAllVariables(sql) -NameSpace = {v:'${' + v + '}$' for v in vars if v != 'filterstr' } -filterdic.update(NameSpace) -sql = ac.convert(sql, filterdic) - -debug(f'{sql=}') -db = DBPools() -dbname = get_module_dbname('pipeline_core') -async with db.sqlorContext(dbname) as sor: - r = await sor.sqlPaging(sql, ns) - return r -return { - "total":0, - "rows":[] -} \ No newline at end of file +async def main(): + pipeline_id = params_kw.pipeline_id or params_kw.id + dbname = get_module_dbname('pipeline_core') + async with DBPools().sqlorContext(dbname) as sor: + if pipeline_id: + recs = await sor.R('pipeline_steps', {'pipeline_id': pipeline_id}) + else: + recs = await sor.R('pipeline_steps', {}) + # Sort by step_order + recs = sorted(recs, key=lambda r: r.step_order if hasattr(r,'step_order') else 0) + return recs diff --git a/wwwroot/pipeline_steps/get_pipelines.dspy b/wwwroot/pipeline_steps/get_pipelines.dspy new file mode 100644 index 0000000..2034836 --- /dev/null +++ b/wwwroot/pipeline_steps/get_pipelines.dspy @@ -0,0 +1,13 @@ + +from sqlor.dbpools import DBPools + +async def main(): + dbname = get_module_dbname('pipeline_core') + async with DBPools().sqlorContext(dbname) as sor: + recs = await sor.R('pipelines', {'status': 'published'}) + if not recs: + recs = await sor.R('pipelines', {}) + result = [] + for r in recs: + result.append({'id': r.id, 'name': r.name, 'text': r.name, 'value': r.id}) + return result diff --git a/wwwroot/pipeline_steps/get_step_types.dspy b/wwwroot/pipeline_steps/get_step_types.dspy new file mode 100644 index 0000000..85881c1 --- /dev/null +++ b/wwwroot/pipeline_steps/get_step_types.dspy @@ -0,0 +1,18 @@ + +from sqlor.dbpools import DBPools + +async def main(): + dbname = get_module_dbname('pipeline_core') + async with DBPools().sqlorContext(dbname) as sor: + recs = await sor.R('pipeline_step_types', {}) + result = [] + for r in recs: + result.append({ + 'step_type': r.step_type, + 'display_name': r.display_name, + 'text': f"{r.display_name} ({r.step_type})", + 'value': r.step_type, + 'category': r.category, + 'is_interactive': r.is_interactive + }) + return result