diff --git a/app/global_func.py b/app/global_func.py index 1749389..5ac05c7 100644 --- a/app/global_func.py +++ b/app/global_func.py @@ -1,13 +1,24 @@ from ahserver.serverenv import ServerEnv +from ahserver.globalEnv import initEnv, password_encode as _password_encode DBNAME = "pipeline" +def password_encode(s): + """Wrapper to handle None input during login form load.""" + if s is None: + return '' + return _password_encode(s) + + def get_module_dbname(mname): - """All modules share the pipeline database.""" - return DBNAME + if 'pipeline' in mname: + return 'pipeline' + return 'sage' def set_globalvariable(): + initEnv() g = ServerEnv() g.get_module_dbname = get_module_dbname + g.password_encode = password_encode diff --git a/app/pipeline_app.py b/app/pipeline_app.py index 71b29c8..66de353 100644 --- a/app/pipeline_app.py +++ b/app/pipeline_app.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """Pipeline Application - 产线管理独立应用""" -import os, sys +import os, sys, asyncio app_dir = os.path.dirname(os.path.abspath(__file__)) root_dir = os.path.dirname(app_dir) @@ -50,6 +50,8 @@ def init(): load_tenant() load_ktv_adapter() + # Permission cache warms up on first request + if __name__ == '__main__': webapp(init) diff --git a/bin/init_data.py b/bin/init_data.py new file mode 100644 index 0000000..9d610af --- /dev/null +++ b/bin/init_data.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +""" +Import initialization data for pipeline-app. +Creates default step types, pipeline templates, and demo data. +Run: source py3/bin/activate && python bin/init_data.py [--demo] +""" +import sys, os, asyncio, json + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +ROOT_DIR = os.path.dirname(SCRIPT_DIR) +sys.path.insert(0, os.path.join(ROOT_DIR, 'py3', 'lib', 'python3.10', 'site-packages')) +sys.path.insert(0, ROOT_DIR) + +from sqlor.dbpools import DBPools +from appPublic.jsonConfig import getConfig +from appPublic.folderUtils import ProgramPath +from appPublic.uniqueID import getID +from ahserver.serverenv import ServerEnv +from ahserver.globalEnv import initEnv +from appPublic.timeUtils import curDateString, timestampstr + +# ---- Default step types ---- +STEP_TYPES = [ + # (step_type, display_name, category, is_interactive, description, form_schema) + ('ai_generate', 'AI代码生成', 'AI生成', '0', '由AI自动生成代码', '{}'), + ('ai_review', 'AI代码审查', 'AI审查', '0', '由AI自动审查代码质量', '{}'), + ('ai_test', 'AI测试生成', 'AI测试', '0', '由AI生成测试用例', '{}'), + ('human_review', '人工代码审查', '人工审核', '1', '人工审查代码变更', + json.dumps({"fields":[{"name":"approved","label":"审批通过","uitype":"switch"},{"name":"comment","label":"审批意见","uitype":"text"}]})), + ('human_approval', '发布审批', '人工审批', '1', '人工审批发布请求', + json.dumps({"fields":[{"name":"approved","label":"批准发布","uitype":"switch"},{"name":"version","label":"发布版本","uitype":"text"}]})), + ('deploy_staging', '部署到测试环境', '部署', '0', '自动部署到测试环境', '{}'), + ('deploy_production', '部署到生产环境', '部署', '1', '审批后部署到生产环境', + json.dumps({"fields":[{"name":"confirm","label":"确认部署","uitype":"switch"}]})), + ('run_tests', '运行测试套件', '测试', '0', '自动运行测试套件', '{}'), + ('build_artifact', '构建制品', '构建', '0', '构建发布制品', '{}'), + ('notify_webhook', 'Webhook通知', '通知', '0', '发送Webhook通知', '{}'), +] + +# ---- Default pipeline (SDLC template) ---- +SDLC_PIPELINE = { + 'id': 'pipeline_sdlc_default', + 'name': '标准SDLC产线', + 'description': '软件开发生命周期标准流程:AI生成→审查→测试→部署', + 'pipeline_type': 'sdlc', + 'status': 'published', + 'version': '1.0', +} + +SDLC_STEPS = [ + ('ai_generate', 'AI代码生成', 1), + ('ai_review', 'AI代码审查', 2), + ('run_tests', '自动测试', 3), + ('human_review', '人工代码审查', 4), + ('build_artifact', '构建制品', 5), + ('human_approval', '发布审批', 6), + ('deploy_production', '生产部署', 7), +] + + +async def init_data(demo=False): + config_file = os.path.join(ROOT_DIR, 'conf', 'config.json') + if not os.path.exists(config_file): + config_file = os.path.join(ROOT_DIR, '..', 'conf', 'config.json') + config = getConfig(config_file, NS={'workdir': ROOT_DIR, 'ProgramPath': ProgramPath()}) + DBPools(config.databases) + initEnv() + env = ServerEnv() + env.get_module_dbname = lambda m: 'pipeline' if 'pipeline' in m else 'sage' + + async with DBPools().sqlorContext('pipeline') as sor: + # 1. Create step types + print('Step types:') + for stype, display, cat, interactive, desc, schema in STEP_TYPES: + existing = await sor.sqlExe( + "SELECT step_type FROM pipeline_step_types WHERE step_type=${st}$", {'st': stype}) + if existing: + print(f' SKIP {stype} ({display})') + continue + await sor.C('pipeline_step_types', { + 'step_type': stype, 'display_name': display, + 'category': cat, 'is_interactive': interactive, + 'description': desc, 'form_schema': schema, + 'created_at': timestampstr() + }) + print(f' ADD {stype} ({display})') + + # 2. Create default SDLC pipeline + print('\nPipeline:') + existing = await sor.sqlExe( + "SELECT id FROM pipelines WHERE id=${pid}$", {'pid': SDLC_PIPELINE['id']}) + if not existing: + await sor.C('pipelines', { + **SDLC_PIPELINE, + 'created_by': 'system', + 'created_at': timestampstr(), + 'updated_at': timestampstr() + }) + print(f' ADD {SDLC_PIPELINE["name"]}') + + # Create steps + for stype, sname, order in SDLC_STEPS: + await sor.C('pipeline_steps', { + 'id': getID(), + 'pipeline_id': SDLC_PIPELINE['id'], + 'step_order': order, + 'step_name': sname, + 'step_type': stype, + 'step_config': '{}', + 'timeout_seconds': 3600, + 'retry_count': 1, + 'created_at': timestampstr() + }) + print(f' step {order}: {sname}') + else: + print(f' SKIP {SDLC_PIPELINE["name"]} (exists)') + + # 3. Demo data + if demo: + print('\nDemo data:') + await _create_demo(sor) + + print('\nInitialization complete.') + + +async def _create_demo(sor): + """Create demo project and iteration.""" + pid = getID() + existing = await sor.sqlExe("SELECT id FROM sd_projects LIMIT 1", {}) + if existing: + print(' SKIP (data exists)') + return + await sor.C('sd_projects', { + 'id': pid, 'name': 'Pipeline App Demo', + 'description': '产线平台演示项目', + 'project_type': 'web', 'status': 'active', + 'created_by': 'system', 'created_at': timestampstr() + }) + print(f' ADD project: Pipeline App Demo') + + iid = getID() + await sor.C('sd_iterations', { + 'id': iid, 'name': 'Sprint 1 - 基础框架', + 'project_id': pid, 'status': 'in_progress', + 'start_date': curDateString(), 'created_at': timestampstr() + }) + print(f' ADD iteration: Sprint 1') + + +if __name__ == '__main__': + demo_mode = '--demo' in sys.argv + asyncio.run(init_data(demo=demo_mode)) diff --git a/bin/init_perms.py b/bin/init_perms.py new file mode 100644 index 0000000..964bcad --- /dev/null +++ b/bin/init_perms.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +"""Initialize RBAC permissions for pipeline modules.""" +import sys, os, asyncio + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +ROOT_DIR = os.path.dirname(SCRIPT_DIR) +sys.path.insert(0, os.path.join(ROOT_DIR, 'py3', 'lib', 'python3.10', 'site-packages')) +sys.path.insert(0, ROOT_DIR) + +from sqlor.dbpools import DBPools +from appPublic.jsonConfig import getConfig +from appPublic.folderUtils import ProgramPath +from appPublic.uniqueID import getID +from ahserver.serverenv import ServerEnv +from ahserver.globalEnv import initEnv + +PERMS = [ + ('/pipeline_core/', '产线管理访问', 'pipeline_core', 'logined'), + ('/pipeline-sdlc/', '开发产线访问', 'pipeline_sdlc', 'logined'), + ('/pipeline_ops/', '运营管理访问', 'pipeline_ops', 'logined'), + ('/pipeline_dist/', '分销管理访问', 'pipeline_dist', 'logined'), + ('/pipeline_task/', '任务中心访问', 'pipeline_task', 'logined'), + ('/showcase/', '展示平台访问', 'showcase', 'guest'), + ('/', '应用首页', 'app', 'guest'), +] + +async def init_perms(): + config = getConfig(ROOT_DIR, NS={'workdir': ROOT_DIR, 'ProgramPath': ProgramPath()}) + DBPools(config.databases) + initEnv() + env = ServerEnv() + env.get_module_dbname = lambda m: 'sage' + + async with DBPools().sqlorContext('sage') as sor: + for path, name, perm_type, role_name in PERMS: + # Build safe SQL with repr + sql = "SELECT id FROM permission WHERE path=" + repr(path) + " LIMIT 1" + existing = await sor.sqlExe(sql, {}) + if existing: + print('SKIP {:<8s} {}'.format(role_name, path)) + continue + + pid = getID() + await sor.C('permission', { + 'id': pid, 'name': name, 'path': path, + 'permtype': perm_type + }) + role_sql = "SELECT id FROM role WHERE name=" + repr(role_name) + " LIMIT 1" + role_recs = await sor.sqlExe(role_sql, {}) + if role_recs: + await sor.C('rolepermission', { + 'id': getID(), + 'roleid': role_recs[0].id, + 'permid': pid + }) + print('ADD {:<8s} {}'.format(role_name, path)) + + print('\nInitialized {} permissions'.format(len(PERMS))) + +if __name__ == '__main__': + asyncio.run(init_perms()) diff --git a/build.sh b/build.sh index 15c595d..8684151 100644 --- a/build.sh +++ b/build.sh @@ -11,7 +11,7 @@ if [ ! -d py3 ]; then fi source py3/bin/activate -# 2. Install foundation packages +# 2. Install foundation packages (clone to pkgs) mkdir -p pkgs for m in apppublic sqlor ahserver bricks-for-python xls2ddl rbac appbase; do echo "install $m ..." @@ -33,56 +33,50 @@ if [ ! -d bricks ]; then fi if [ -d bricks/bricks ]; then cd bricks/bricks && bash build.sh 2>&1 | tail -3 - ln -sf "$cdir/pkgs/bricks/dist" "$cdir/bricks" fi +ln -sf "$cdir/pkgs/bricks/dist" "$cdir/bricks" 2>/dev/null || true cd "$cdir" -# 4. Clone pipeline-sdlc and showcase -for m in pipeline-sdlc showcase; do - echo "clone $m ..." +# 4. Move local business modules into pkgs/ +for mod in pipeline_core pipeline_ops pipeline_dist; do + if [ -d "$mod" ]; then + mv "$mod" "pkgs/$mod" + fi +done + +# 5. Install business modules (clone external, local already in pkgs) +for mod in pipeline-sdlc showcase evaluate pipeline-service pipeline-task; do cd "$cdir/pkgs" - if [ ! -d "$m" ]; then - git clone https://git.opencomputing.cn/yumoqing/$m || echo "SKIP: $m clone failed" - fi - # pipeline-sdlc goes to root (same as other business modules) - if [ "$m" = "pipeline-sdlc" ] && [ ! -d "$cdir/pipeline-sdlc" ]; then - ln -sf "$cdir/pkgs/pipeline-sdlc" "$cdir/pipeline-sdlc" + if [ ! -d "$mod" ]; then + git clone https://git.opencomputing.cn/yumoqing/$mod || echo "SKIP: $mod" fi cd "$cdir" done -# 5. Install business modules -for mod in pipeline_core pipeline_ops pipeline_dist pipeline-sdlc; do - echo "install $mod ..." - cd "$cdir/$mod" - "$cdir/py3/bin/pip" install . 2>&1 | tail -1 - - # Generate DDL from models - if [ -d models ] && ls models/*.json >/dev/null 2>&1; then - "$cdir/py3/bin/json2ddl" mysql models/ > "$cdir/$mod/mysql.ddl.sql" 2>/dev/null || echo " DDL generation skipped (json2ddl not available)" +# 6. pip install all modules from pkgs/ +for mod in pipeline_core pipeline_ops pipeline_dist pipeline-sdlc showcase; do + if [ -d "pkgs/$mod" ]; then + "$cdir/py3/bin/pip" install "pkgs/$mod/" 2>&1 | tail -1 fi - - # Generate CRUD UI from json definitions - if [ -d json ] && ls json/*.json >/dev/null 2>&1; then - cd json - for f in *.json; do - "$cdir/py3/bin/xls2ui" -m ../models -o ../wwwroot "$mod" "$f" 2>/dev/null || echo " CRUD generation skipped for $f" - done - cd .. - fi - cd "$cdir" done -# 5b. Install showcase module -if [ -d "$cdir/pkgs/showcase" ]; then - echo "install showcase ..." - cd "$cdir/pkgs/showcase" - "$cdir/py3/bin/pip" install . 2>&1 | tail -1 +# 7. SDLC pipeline-sdlc: regenerate CRUD from json +if [ -d "pkgs/pipeline-sdlc/json" ]; then + cd "pkgs/pipeline-sdlc/json" + "$cdir/py3/bin/xls2ui" -m ../models -o ../wwwroot pipeline-sdlc *.json 2>&1 | grep -c 'handle' | xargs -I{} echo " xls2ui: {} tables handled" cd "$cdir" fi -# 6. Create runtime dirs -mkdir -p "$cdir/logs" "$cdir/files" +# 8. Create runtime dirs +mkdir -p "$cdir/logs" "$cdir/files" "$cdir/conf" +# 6. Fix auto-assign created_by in CRUD dspys (xls2ui overwritten) +for d in sd_projects sd_iterations; do + for t in add update; do + f="$cdir/wwwroot/pipeline-sdlc/$d/${t}_${d}.dspy" + [ -f "$f" ] && grep -q 'created_by' "$f" || sed -i "s/ns\['org_id'\] = userorgid/ns['org_id'] = userorgid\nns['created_by'] = userorgid/" "$f" + done +done -chmod +x "$cdir/start.sh" "$cdir/stop.sh" + +chmod +x "$cdir/start.sh" "$cdir/stop.sh" 2>/dev/null || true echo "=== Build complete ===" diff --git a/pipeline_core/json/llm.json b/pipeline_core/json/llm.json new file mode 100644 index 0000000..7f22219 --- /dev/null +++ b/pipeline_core/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/pipeline_core/models/llm.json b/pipeline_core/models/llm.json new file mode 100644 index 0000000..c4aee72 --- /dev/null +++ b/pipeline_core/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/pipeline_core/models/pipeline_steps.json b/pipeline_core/models/pipeline_steps.json index 7eb96ea..3763d8a 100644 --- a/pipeline_core/models/pipeline_steps.json +++ b/pipeline_core/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/pipeline_core/models/pipeline_versions.json b/pipeline_core/models/pipeline_versions.json index d524263..a1ed0cc 100644 --- a/pipeline_core/models/pipeline_versions.json +++ b/pipeline_core/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/pipeline_core/models/pipelines.json b/pipeline_core/models/pipelines.json index 8c930eb..893f0a7 100644 --- a/pipeline_core/models/pipelines.json +++ b/pipeline_core/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/pipeline_core/init.py b/pipeline_core/pipeline_core/init.py index f9846f8..74c9550 100644 --- a/pipeline_core/pipeline_core/init.py +++ b/pipeline_core/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/pipeline_core/scripts/load_path.py b/pipeline_core/scripts/load_path.py index 72aa88c..d2c4241 100644 --- a/pipeline_core/scripts/load_path.py +++ b/pipeline_core/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/pipeline_core/wwwroot/index.ui b/pipeline_core/wwwroot/index.ui index c55127a..7a49d8c 100644 --- a/pipeline_core/wwwroot/index.ui +++ b/pipeline_core/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/pipeline_core/wwwroot/pipeline_editor/index.ui b/pipeline_core/wwwroot/pipeline_editor/index.ui new file mode 100644 index 0000000..6812015 --- /dev/null +++ b/pipeline_core/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/pipeline_core/wwwroot/pipeline_editor/save_steps.dspy b/pipeline_core/wwwroot/pipeline_editor/save_steps.dspy new file mode 100644 index 0000000..c0cb516 --- /dev/null +++ b/pipeline_core/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/pipeline_core/wwwroot/pipeline_steps/get_pipeline_steps.dspy b/pipeline_core/wwwroot/pipeline_steps/get_pipeline_steps.dspy index 48c6209..5a6875b 100644 --- a/pipeline_core/wwwroot/pipeline_steps/get_pipeline_steps.dspy +++ b/pipeline_core/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/pipeline_core/wwwroot/pipeline_steps/get_pipelines.dspy b/pipeline_core/wwwroot/pipeline_steps/get_pipelines.dspy new file mode 100644 index 0000000..2034836 --- /dev/null +++ b/pipeline_core/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/pipeline_core/wwwroot/pipeline_steps/get_step_types.dspy b/pipeline_core/wwwroot/pipeline_steps/get_step_types.dspy new file mode 100644 index 0000000..85881c1 --- /dev/null +++ b/pipeline_core/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 diff --git a/pipeline_dist/scripts/load_path.py b/pipeline_dist/scripts/load_path.py index ca77c30..01f1d0a 100644 --- a/pipeline_dist/scripts/load_path.py +++ b/pipeline_dist/scripts/load_path.py @@ -1,22 +1,38 @@ -"""Register RBAC paths for pipeline_dist module.""" +#!/usr/bin/env python3 +"""RBAC path registration for pipeline_dist + showcase modules.""" +import os, sys, subprocess -RBAC_PATHS = [ - '/api/distributors_list.dspy', - '/api/distributors_get.dspy', - '/api/distributors_save.dspy', - '/api/distributors_delete.dspy', - '/api/distributor_pipeline_list.dspy', - '/api/distributor_pipeline_get.dspy', - '/api/distributor_pipeline_save.dspy', - '/api/distributor_pipeline_delete.dspy', - '/api/distributor_generate_key.dspy', +MODS = [ + ("pipeline_dist", [ + "/pipeline_dist", + "/pipeline_dist/index.ui", + "/pipeline_dist/distributors/index.ui", + "/pipeline_dist/distributor_pipeline/index.ui", + ]), + ("showcase", [ + "/showcase", + "/showcase/index.ui", + ]), ] -def load_paths(register): - """Register all RBAC paths for this module.""" - for path in RBAC_PATHS: - register(f'pipeline_dist{path}', { - 'module': 'pipeline_dist', - 'path': path, - }) +def main(): + root = os.path.expanduser("~/pipeline") + set_perm = os.path.join(root, "set_role_perm.py") + if not os.path.isfile(set_perm): + print("ERROR: pipeline root not found"); sys.exit(1) + py = sys.executable + count = 0 + total = 0 + for mod, paths in MODS: + total += len(paths) + for path in paths: + r = subprocess.run([py, set_perm, "logined", path], capture_output=True, text=True, cwd=root) + if r.returncode == 0: count += 1 + else: print(f" WARN: {path}") + print(f" {mod}: {len(paths)} paths") + print(f"Registered {count}/{total}") + + +if __name__ == "__main__": + main() diff --git a/pipeline_ops/scripts/load_path.py b/pipeline_ops/scripts/load_path.py index c6284b7..69cf30d 100644 --- a/pipeline_ops/scripts/load_path.py +++ b/pipeline_ops/scripts/load_path.py @@ -1,90 +1,62 @@ -"""Register all RBAC paths for pipeline_ops module""" +#!/usr/bin/env python3 +"""RBAC path registration for pipeline_ops module.""" +import os, sys, subprocess + +MOD = "pipeline_ops" + +PATHS_ANY = [ + f"/{MOD}/index.ui", +] + +PATHS_LOGINED = [ + f"/{MOD}", + f"/{MOD}/api/ops_dashboard_data.dspy", + # Pricing + f"/{MOD}/pipeline_pricing/index.ui", + f"/{MOD}/pipeline_pricing/get_pipeline_pricing.dspy", + f"/{MOD}/pipeline_pricing/add_pipeline_pricing.dspy", + f"/{MOD}/pipeline_pricing/update_pipeline_pricing.dspy", + f"/{MOD}/pipeline_pricing/delete_pipeline_pricing.dspy", + # Capacity + f"/{MOD}/pipeline_capacity/index.ui", + f"/{MOD}/pipeline_capacity/get_pipeline_capacity.dspy", + f"/{MOD}/pipeline_capacity/add_pipeline_capacity.dspy", + f"/{MOD}/pipeline_capacity/update_pipeline_capacity.dspy", + f"/{MOD}/pipeline_capacity/delete_pipeline_capacity.dspy", + # Usage Log + f"/{MOD}/pipeline_usage_log/index.ui", + f"/{MOD}/pipeline_usage_log/get_pipeline_usage_log.dspy", + f"/{MOD}/pipeline_usage_log/add_pipeline_usage_log.dspy", + f"/{MOD}/pipeline_usage_log/update_pipeline_usage_log.dspy", + f"/{MOD}/pipeline_usage_log/delete_pipeline_usage_log.dspy", + # API + f"/{MOD}/api/pipeline_pricing_create.dspy", + f"/{MOD}/api/pipeline_pricing_update.dspy", + f"/{MOD}/api/pipeline_pricing_delete.dspy", + f"/{MOD}/api/pipeline_capacity_create.dspy", + f"/{MOD}/api/pipeline_capacity_update.dspy", + f"/{MOD}/api/pipeline_capacity_delete.dspy", + f"/{MOD}/api/pipeline_usage_log_create.dspy", + f"/{MOD}/api/pipeline_usage_log_update.dspy", + f"/{MOD}/api/pipeline_usage_log_delete.dspy", + f"/{MOD}/api/get_search_pipeline_id.dspy", +] -def load_paths(): - """Return list of RBAC paths for this module""" - paths = [ - { - "path": "/pipeline_ops/", - "name": "产线运营", - "icon": "settings", - "parent": "", - "sort": 30 - }, - { - "path": "/pipeline_ops/pipeline_pricing", - "name": "定价管理", - "icon": "price-tag", - "parent": "/pipeline_ops/", - "sort": 31 - }, - { - "path": "/pipeline_ops/api/pipeline_pricing_create.dspy", - "name": "新增定价", - "parent": "/pipeline_ops/pipeline_pricing", - "sort": 32 - }, - { - "path": "/pipeline_ops/api/pipeline_pricing_update.dspy", - "name": "修改定价", - "parent": "/pipeline_ops/pipeline_pricing", - "sort": 33 - }, - { - "path": "/pipeline_ops/api/pipeline_pricing_delete.dspy", - "name": "删除定价", - "parent": "/pipeline_ops/pipeline_pricing", - "sort": 34 - }, - { - "path": "/pipeline_ops/pipeline_capacity", - "name": "供应量管理", - "icon": "gauge", - "parent": "/pipeline_ops/", - "sort": 40 - }, - { - "path": "/pipeline_ops/api/pipeline_capacity_create.dspy", - "name": "新增供应量配置", - "parent": "/pipeline_ops/pipeline_capacity", - "sort": 41 - }, - { - "path": "/pipeline_ops/api/pipeline_capacity_update.dspy", - "name": "修改供应量配置", - "parent": "/pipeline_ops/pipeline_capacity", - "sort": 42 - }, - { - "path": "/pipeline_ops/api/pipeline_capacity_delete.dspy", - "name": "删除供应量配置", - "parent": "/pipeline_ops/pipeline_capacity", - "sort": 43 - }, - { - "path": "/pipeline_ops/pipeline_usage_log", - "name": "使用记录", - "icon": "list", - "parent": "/pipeline_ops/", - "sort": 50 - }, - { - "path": "/pipeline_ops/api/pipeline_usage_log_create.dspy", - "name": "新增使用记录", - "parent": "/pipeline_ops/pipeline_usage_log", - "sort": 51 - }, - { - "path": "/pipeline_ops/api/pipeline_usage_log_update.dspy", - "name": "修改使用记录", - "parent": "/pipeline_ops/pipeline_usage_log", - "sort": 52 - }, - { - "path": "/pipeline_ops/api/pipeline_usage_log_delete.dspy", - "name": "删除使用记录", - "parent": "/pipeline_ops/pipeline_usage_log", - "sort": 53 - } - ] - return paths +def main(): + root = os.path.expanduser("~/pipeline") + set_perm = os.path.join(root, "set_role_perm.py") + if not os.path.isfile(set_perm): + print("ERROR: pipeline root not found"); sys.exit(1) + 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}") + print(f"Registered {count}/{len(PATHS_ANY)+len(PATHS_LOGINED)} paths for {MOD}") + + +if __name__ == "__main__": + main() diff --git a/pipeline_ops/wwwroot/api/ops_dashboard_data.dspy b/pipeline_ops/wwwroot/api/ops_dashboard_data.dspy new file mode 100644 index 0000000..2ddae22 --- /dev/null +++ b/pipeline_ops/wwwroot/api/ops_dashboard_data.dspy @@ -0,0 +1,61 @@ +""" +Operations Dashboard Data: task queue, human tasks, stats +""" +from sqlor.dbpools import DBPools + +async def main(): + dbname = get_module_dbname('pipeline_core') + + async with DBPools().sqlorContext(dbname) as sor: + # Task stats by state + task_stats = await sor.sqlExe(""" + SELECT state, COUNT(*) as cnt FROM pipeline_tasks + GROUP BY state ORDER BY cnt DESC + """, {}) + + stats = {'submitted':0, 'running':0, 'completed':0, 'failed':0, 'cancelled':0} + for r in task_stats: + stats[r.state] = r.cnt + + # Recent tasks (last 20) + tasks = await sor.sqlExe(""" + SELECT t.id, t.title, t.state, t.pipeline_id, t.created_at + FROM pipeline_tasks t + ORDER BY t.created_at DESC LIMIT 20 + """, {}) + task_list = [] + for r in tasks: + task_list.append({ + 'id': r.id, 'title': r.title, 'state': r.state, + 'pipeline_id': r.pipeline_id, 'created_at': str(r.created_at)[:19] + }) + + # Pending human tasks + human_tasks = await sor.sqlExe(""" + SELECT h.id, h.task_id, h.task_type, h.step_name, h.status, + h.assignee_role, h.created_at, h.expired_at, + t.title as task_title + FROM pipeline_human_tasks h + LEFT JOIN pipeline_tasks t ON h.task_id = t.id + WHERE h.status IN ('pending', 'in_progress') + ORDER BY h.created_at DESC LIMIT 20 + """, {}) + + human_list = [] + for r in human_tasks: + human_list.append({ + 'id': r.id, 'task_id': r.task_id, 'task_type': r.task_type, + 'step_name': r.step_name, 'status': r.status, + 'assignee_role': r.assignee_role, 'task_title': r.task_title, + 'created_at': str(r.created_at)[:19], + 'expired_at': str(r.expired_at)[:19] if r.expired_at else None + }) + + return { + 'stats': stats, + 'tasks': task_list, + 'human_tasks': human_list, + 'total_tasks': sum(stats.values()), + 'active_tasks': stats['running'], + 'pending_reviews': len(human_list) + } diff --git a/pipeline_ops/wwwroot/index.ui b/pipeline_ops/wwwroot/index.ui index 3f463eb..e3387a0 100644 --- a/pipeline_ops/wwwroot/index.ui +++ b/pipeline_ops/wwwroot/index.ui @@ -1,46 +1,120 @@ { "widgettype": "VBox", - "options": {"width": "100%", "height": "100%", "padding": "24px", "spacing": "20px"}, + "options": {"width": "100%", "height": "100%", "padding": "24px", "gap": "20px"}, "subwidgets": [ - {"widgettype": "Title2", "options": {"text": "产线运营"}}, + { + "widgettype": "HBox", + "options": {"alignItems": "center", "gap": "12px"}, + "subwidgets": [ + {"widgettype": "Title2", "options": {"text": "产线运营中心"}}, + {"widgettype": "Text", "options": {"text": "任务队列、审批管理、执行监控", "cfontsize": 1.1, "color": "#888"}} + ] + }, { "widgettype": "ResponsableBox", - "options": {"gap": "24px", "minWidth": "260px"}, + "options": {"gap": "16px", "minWidth": "150px"}, "subwidgets": [ { "widgettype": "VBox", - "options": {"css": "card", "cwidth": 23, "cheight": 11, "padding": "24px", "cursor": "pointer", "bgcolor": "#fff"}, - "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.main_content", "options": {"url": "{{entire_url('pipeline_pricing/')}}"}, "mode": "replace"}], + "options": {"bgcolor": "#1e293b", "padding": "20px", "borderRadius": "12px", "gap": "4px", "border": "1px solid #334155", "cwidth": 16, "alignItems": "center"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "-", "cfontsize": 2, "color": "#60a5fa", "fontWeight": "bold"}}, + {"widgettype": "Text", "options": {"text": "已提交", "cfontsize": 0.9, "color": "#94a3b8"}} + ] + }, + { + "widgettype": "VBox", + "options": {"bgcolor": "#1e293b", "padding": "20px", "borderRadius": "12px", "gap": "4px", "border": "1px solid #334155", "cwidth": 16, "alignItems": "center"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "-", "cfontsize": 2, "color": "#fbbf24", "fontWeight": "bold"}}, + {"widgettype": "Text", "options": {"text": "运行中", "cfontsize": 0.9, "color": "#94a3b8"}} + ] + }, + { + "widgettype": "VBox", + "options": {"bgcolor": "#1e293b", "padding": "20px", "borderRadius": "12px", "gap": "4px", "border": "1px solid #334155", "cwidth": 16, "alignItems": "center"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "-", "cfontsize": 2, "color": "#4ade80", "fontWeight": "bold"}}, + {"widgettype": "Text", "options": {"text": "已完成", "cfontsize": 0.9, "color": "#94a3b8"}} + ] + }, + { + "widgettype": "VBox", + "options": {"bgcolor": "#1e293b", "padding": "20px", "borderRadius": "12px", "gap": "4px", "border": "1px solid #334155", "cwidth": 16, "alignItems": "center"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "-", "cfontsize": 2, "color": "#f87171", "fontWeight": "bold"}}, + {"widgettype": "Text", "options": {"text": "失败", "cfontsize": 0.9, "color": "#94a3b8"}} + ] + }, + { + "widgettype": "VBox", + "options": {"bgcolor": "#1e293b", "padding": "20px", "borderRadius": "12px", "gap": "4px", "border": "1px solid #334155", "cwidth": 16, "alignItems": "center"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "-", "cfontsize": 2, "color": "#a78bfa", "fontWeight": "bold"}}, + {"widgettype": "Text", "options": {"text": "待审批", "cfontsize": 0.9, "color": "#94a3b8"}} + ] + }, + { + "widgettype": "VBox", + "options": {"bgcolor": "#1e293b", "padding": "20px", "borderRadius": "12px", "gap": "4px", "border": "1px solid #334155", "cwidth": 16, "alignItems": "center"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "-", "cfontsize": 2, "color": "#e2e8f0", "fontWeight": "bold"}}, + {"widgettype": "Text", "options": {"text": "总任务", "cfontsize": 0.9, "color": "#94a3b8"}} + ] + } + ] + }, + { + "widgettype": "ResponsableBox", + "options": {"gap": "16px", "minWidth": "400px"}, + "subwidgets": [ + { + "widgettype": "VBox", + "options": {"bgcolor": "#1e293b", "padding": "20px", "borderRadius": "12px", "gap": "12px", "border": "1px solid #334155", "cwidth": 60}, + "subwidgets": [ + {"widgettype": "Title3", "options": {"text": "近期任务"}}, + {"widgettype": "Text", "options": {"text": "暂无任务,提交产线任务后将在此显示", "cfontsize": 0.9, "color": "#64748b", "padding": "20px 0"}} + ] + }, + { + "widgettype": "VBox", + "options": {"bgcolor": "#1e293b", "padding": "20px", "borderRadius": "12px", "gap": "12px", "border": "1px solid #334155", "cwidth": 40}, + "subwidgets": [ + {"widgettype": "Title3", "options": {"text": "待审批队列"}}, + {"widgettype": "Text", "options": {"text": "暂无审批任务", "cfontsize": 0.9, "color": "#64748b", "padding": "20px 0"}} + ] + } + ] + }, + { + "widgettype": "ResponsableBox", + "options": {"gap": "16px", "minWidth": "180px"}, + "subwidgets": [ + { + "widgettype": "VBox", + "options": {"css": "card", "cwidth": 23, "cheight": 10, "padding": "24px", "cursor": "pointer", "bgcolor": "#1e293b", "border": "1px solid #334155"}, + "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.main_content", "options": {"url": "{{entire_url('/pipeline_ops/pipeline_pricing/')}}"}, "mode": "replace"}], "subwidgets": [ - {"widgettype": "VBox", "options": {"width": "44px", "height": "44px", "bgcolor": "#FEF2F2", "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": 0.9}} + {"widgettype": "Text", "options": {"text": "管理产线计费方式和价格", "cfontsize": 0.9, "color": "#94a3b8"}} ] }, { "widgettype": "VBox", - "options": {"css": "card", "cwidth": 23, "cheight": 11, "padding": "24px", "cursor": "pointer", "bgcolor": "#fff"}, - "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.main_content", "options": {"url": "{{entire_url('pipeline_capacity/')}}"}, "mode": "replace"}], + "options": {"css": "card", "cwidth": 23, "cheight": 10, "padding": "24px", "cursor": "pointer", "bgcolor": "#1e293b", "border": "1px solid #334155"}, + "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.main_content", "options": {"url": "{{entire_url('/pipeline_ops/pipeline_capacity/')}}"}, "mode": "replace"}], "subwidgets": [ - {"widgettype": "VBox", "options": {"width": "44px", "height": "44px", "bgcolor": "#F0F9FF", "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": 0.9}} + {"widgettype": "Text", "options": {"text": "配置产线并发和调用限额", "cfontsize": 0.9, "color": "#94a3b8"}} ] }, { "widgettype": "VBox", - "options": {"css": "card", "cwidth": 23, "cheight": 11, "padding": "24px", "cursor": "pointer", "bgcolor": "#fff"}, - "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.main_content", "options": {"url": "{{entire_url('pipeline_usage_log/')}}"}, "mode": "replace"}], + "options": {"css": "card", "cwidth": 23, "cheight": 10, "padding": "24px", "cursor": "pointer", "bgcolor": "#1e293b", "border": "1px solid #334155"}, + "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.main_content", "options": {"url": "{{entire_url('/pipeline_ops/pipeline_usage_log/')}}"}, "mode": "replace"}], "subwidgets": [ - {"widgettype": "VBox", "options": {"width": "44px", "height": "44px", "bgcolor": "#F5F3FF", "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": 0.9}} + {"widgettype": "Text", "options": {"text": "查看产线调用和消费记录", "cfontsize": 0.9, "color": "#94a3b8"}} ] } ] diff --git a/scripts/load_path.py b/scripts/load_path.py new file mode 100644 index 0000000..72e36c0 --- /dev/null +++ b/scripts/load_path.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""Register all pipeline module RBAC permissions in one run.""" +import sys, os, asyncio + +ROOT = os.path.expanduser("~/pipeline") +sys.path.insert(0, os.path.join(ROOT, "py3", "lib", "python3.10", "site-packages")) +sys.path.insert(0, ROOT) + +from sqlor.dbpools import DBPools +from appPublic.jsonConfig import getConfig +from appPublic.folderUtils import ProgramPath +from appPublic.uniqueID import getID +from ahserver.serverenv import ServerEnv +from ahserver.globalEnv import initEnv + +PERMS = [ + # pipeline-sdlc + ("/pipeline-sdlc", "pipeline_sdlc", "logined"), + ("/pipeline-sdlc/sd_dashboard/*", "pipeline_sdlc", "logined"), + ("/pipeline-sdlc/sd_projects/*", "pipeline_sdlc", "logined"), + ("/pipeline-sdlc/sd_project_list/*", "pipeline_sdlc", "logined"), + ("/pipeline-sdlc/sd_iterations/*", "pipeline_sdlc", "logined"), + ("/pipeline-sdlc/sd_iteration_list/*", "pipeline_sdlc", "logined"), + ("/pipeline-sdlc/sd_test_plans/*", "pipeline_sdlc", "logined"), + ("/pipeline-sdlc/sd_test_plan_list/*", "pipeline_sdlc", "logined"), + ("/pipeline-sdlc/sd_test_cases/*", "pipeline_sdlc", "logined"), + ("/pipeline-sdlc/sd_test_case_list/*", "pipeline_sdlc", "logined"), + ("/pipeline-sdlc/sd_bugs/*", "pipeline_sdlc", "logined"), + ("/pipeline-sdlc/sd_bug_list/*", "pipeline_sdlc", "logined"), + ("/pipeline-sdlc/sd_deploy_envs/*", "pipeline_sdlc", "logined"), + ("/pipeline-sdlc/sd_deploy_env_list/*", "pipeline_sdlc", "logined"), + ("/pipeline-sdlc/sd_review/*", "pipeline_sdlc", "logined"), + ("/pipeline-sdlc/api/*", "pipeline_sdlc", "logined"), + # pipeline_core + ("/pipeline_core", "pipeline_core", "logined"), + ("/pipeline_core/pipelines/*", "pipeline_core", "logined"), + ("/pipeline_core/pipeline_steps/*", "pipeline_core", "logined"), + ("/pipeline_core/pipeline_versions/*", "pipeline_core", "logined"), + ("/pipeline_core/pipeline_editor/*", "pipeline_core", "logined"), + ("/pipeline_core/api/*", "pipeline_core", "logined"), + # pipeline_ops + ("/pipeline_ops", "pipeline_ops", "logined"), + ("/pipeline_ops/api/*", "pipeline_ops", "logined"), + ("/pipeline_ops/pipeline_pricing/*", "pipeline_ops", "logined"), + ("/pipeline_ops/pipeline_capacity/*", "pipeline_ops", "logined"), + ("/pipeline_ops/pipeline_usage_log/*", "pipeline_ops", "logined"), + # pipeline_dist + ("/pipeline_dist", "pipeline_dist", "logined"), + ("/pipeline_dist/*", "pipeline_dist", "logined"), + # showcase + ("/showcase", "showcase", "logined"), + ("/showcase/*", "showcase", "logined"), + # app root (any) + ("/**", "app", "any"), + ("/index.ui", "app", "any"), + ("/pipeline-sdlc/index.ui", "pipeline_sdlc", "any"), + ("/pipeline_core/index.ui", "pipeline_core", "any"), + ("/pipeline_ops/index.ui", "pipeline_ops", "any"), + ("/pipeline_dist/index.ui", "pipeline_dist", "any"), +] + + +async def register(): + config = getConfig(ROOT, NS={"workdir": ROOT, "ProgramPath": ProgramPath()}) + DBPools(config.databases) + initEnv() + env = ServerEnv() + env.get_module_dbname = lambda m: "sage" + + async with DBPools().sqlorContext("sage") as sor: + total = 0 + for path, module, role_name in PERMS: + sql = "SELECT id FROM permission WHERE path=" + repr(path) + " LIMIT 1" + existing = await sor.sqlExe(sql, {}) + if existing: + continue + + pid = getID() + await sor.C("permission", { + "id": pid, "name": module + ":" + path, + "path": path, "permtype": module + }) + role_sql = "SELECT id FROM role WHERE name=" + repr(role_name) + " LIMIT 1" + role_recs = await sor.sqlExe(role_sql, {}) + if role_recs: + await sor.C("rolepermission", { + "id": getID(), + "roleid": role_recs[0].id, + "permid": pid + }) + total += 1 + + print(f"Registered {total} new permissions") + # Verify + async with DBPools().sqlorContext("sage") as sor: + cnt = await sor.sqlExe( + "SELECT COUNT(*) as n FROM permission WHERE path LIKE '/pipeline%' OR path LIKE '/showcase%'", {}) + print(f"Total pipeline permissions: {cnt[0].n}") + + +if __name__ == "__main__": + asyncio.run(register()) diff --git a/scripts/merge_i18n.py b/scripts/merge_i18n.py new file mode 100644 index 0000000..5ae54ea --- /dev/null +++ b/scripts/merge_i18n.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +""" +Merge i18n translations from all pipeline modules into wwwroot/i18n. +Scans each module's i18n directory and merges into the app's centralized i18n. + +Usage: python scripts/merge_i18n.py +""" +import os, json, sys +from collections import OrderedDict +from pathlib import Path + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +ROOT_DIR = os.path.dirname(SCRIPT_DIR) +WWWROOT = os.path.join(ROOT_DIR, 'wwwroot') +I18N_OUT = os.path.join(WWWROOT, 'i18n') + +# Modules to scan for i18n +MODULES = [ + ('pipeline_core', os.path.join(ROOT_DIR, 'pipeline_core', 'i18n')), + ('pipeline_ops', os.path.join(ROOT_DIR, 'pipeline_ops', 'i18n')), + ('pipeline_dist', os.path.join(ROOT_DIR, 'pipeline_dist', 'i18n')), + ('pipeline-sdlc', os.path.join(os.path.dirname(ROOT_DIR), 'pipeline-sdlc', 'i18n')), + ('showcase', os.path.join(os.path.dirname(ROOT_DIR), 'showcase', 'i18n')), +] + +LANGS = ['zh', 'en', 'ko', 'jp'] + + +def parse_msg_txt(filepath): + """Parse msg.txt: key=value format.""" + result = OrderedDict() + if not os.path.exists(filepath): + return result + with open(filepath, 'r', encoding='utf-8') as f: + for line in f: + line = line.strip() + if not line or line.startswith('#'): + continue + if '=' in line: + key, val = line.split('=', 1) + result[key.strip()] = val.strip() + return result + + +def merge(): + os.makedirs(I18N_OUT, exist_ok=True) + stats = {} + + for lang in LANGS: + merged = OrderedDict() + out_dir = os.path.join(I18N_OUT, lang) + os.makedirs(out_dir, exist_ok=True) + + # Load existing merged translations + i18n_file = os.path.join(out_dir, 'i18n.json') + if os.path.exists(i18n_file): + with open(i18n_file, 'r', encoding='utf-8') as f: + merged = json.load(f, object_pairs_hook=OrderedDict) + + # Scan each module + for mod_name, mod_i18n_dir in MODULES: + lang_dir = os.path.join(mod_i18n_dir, lang) + if not os.path.isdir(lang_dir): + # Try app's internal i18n + alt_dir = os.path.join(ROOT_DIR, 'pipeline_core', 'i18n', lang) + if not os.path.isdir(alt_dir): + continue + lang_dir = alt_dir + + # Load msg.txt + msg_file = os.path.join(lang_dir, 'msg.txt') + if os.path.exists(msg_file): + msgs = parse_msg_txt(msg_file) + for k, v in msgs.items(): + if k not in merged: + merged[k] = v + stats.setdefault(mod_name, 0) + stats[mod_name] += 1 + + # Load i18n.json + mod_i18n = os.path.join(lang_dir, 'i18n.json') + if os.path.exists(mod_i18n): + with open(mod_i18n, 'r', encoding='utf-8') as f: + extra = json.load(f, object_pairs_hook=OrderedDict) + for k, v in extra.items(): + if k not in merged: + merged[k] = v + + # Write merged + with open(i18n_file, 'w', encoding='utf-8') as f: + json.dump(merged, f, ensure_ascii=False, indent=2) + print(f' {lang}: {len(merged)} keys') + + # Write msg.txt for the app root + print(f'\nMerged i18n for {len(MODULES)} modules -> {I18N_OUT}') + + +if __name__ == '__main__': + merge() diff --git a/set_role_perm.py b/set_role_perm.py index 6d24905..1a4097f 100644 --- a/set_role_perm.py +++ b/set_role_perm.py @@ -1,52 +1,62 @@ #!/usr/bin/env python3 -"""Wrapper script for RBAC permission registration. -Called by modules' load_path.py scripts. -Usage: py3/bin/python set_role_perm.py """ -import sys -import os -import asyncio +Register a single RBAC permission for pipeline modules. +Called by init_perms.py or directly. -# Add current directory to path -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +Usage: + python set_role_perm.py + python set_role_perm.py admin /pipeline_core/pipelines/*.dspy +""" +import sys, os, asyncio + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +ROOT_DIR = os.path.dirname(SCRIPT_DIR) if os.path.basename(SCRIPT_DIR) == 'bin' else SCRIPT_DIR +sys.path.insert(0, os.path.join(ROOT_DIR, 'py3', 'lib', 'python3.10', 'site-packages')) +sys.path.insert(0, ROOT_DIR) -from rbac.set_role_perms import set_role_perm as _set_role_perm -from appPublic.jsonConfig import getConfig from sqlor.dbpools import DBPools +from appPublic.jsonConfig import getConfig +from appPublic.folderUtils import ProgramPath +from appPublic.uniqueID import getID +from ahserver.serverenv import ServerEnv +from ahserver.globalEnv import initEnv async def main(role, path): - """Register a single permission path for a role.""" - # Determine module name from path - # e.g., /pipeline_core/index.ui -> pipeline_core + config = getConfig(os.path.join(ROOT_DIR, 'conf', 'config.json'), + NS={'workdir': ROOT_DIR, 'ProgramPath': ProgramPath()}) + DBPools(config.databases) + initEnv() + env = ServerEnv() + env.get_module_dbname = lambda m: 'pipeline' if 'pipeline' in m else 'sage' + + # Determine module from path parts = path.strip('/').split('/') module = parts[0] if parts else 'app' - - # Get database name from config - config = getConfig('.', {'workdir': '.'}) - db = DBPools(config.databases) - - # Call the actual permission setter - await _set_role_perm('pipeline', module, '*', role, path) + async with DBPools().sqlorContext('sage') as sor: + existing = await sor.sqlExe( + "SELECT id FROM role_path WHERE module=${m}$ AND path=${p}$ AND role_name=${r}$", + {'m': module, 'p': path, 'r': role}) + if existing: + print(f'Permission exists: {role} {path}') + return -def run(coro): - """Run async function in event loop.""" - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - try: - loop.run_until_complete(coro) - finally: - loop.close() + await sor.C('role_path', { + 'id': getID(), + 'module': module, + 'path': path, + 'role_name': role, + 'description': f'Auto-registered: {role} -> {path}' + }) + print(f'Registered: {role} -> {path}') if __name__ == '__main__': if len(sys.argv) != 3: print(f"Usage: {sys.argv[0]} ") - print(f"Example: {sys.argv[0]} logined /pipeline_core/index.ui") + print(f" roles: guest, logined, admin") + print(f" path example: /pipeline_core/index.ui") sys.exit(1) - - role = sys.argv[1] - path = sys.argv[2] - - run(main(role, path)) + + asyncio.run(main(sys.argv[1], sys.argv[2])) diff --git a/wwwroot/index.ui b/wwwroot/index.ui index e78fe95..2e1ce3a 100644 --- a/wwwroot/index.ui +++ b/wwwroot/index.ui @@ -1,73 +1,167 @@ { - "id": "app", - "widgettype": "VBox", - "options": {"width": "100%", "height": "100%", "padding": "0"}, - "subwidgets": [ + "id": "app", + "widgettype": "VBox", + "options": { + "width": "100%", + "height": "100%", + "padding": "0" + }, + "subwidgets": [ + { + "widgettype": "HBox", + "options": { + "width": "100%", + "cheight": 2, + "bgcolor": "#1e293b", + "padding": "0 16px", + "alignItems": "center", + "gap": "12px" + }, + "subwidgets": [ { - "widgettype": "HBox", - "options": {"width": "100%", "cheight": 2, "bgcolor": "#1e293b", "padding": "0 16px", "alignItems": "center", "gap": "12px"}, - "subwidgets": [ - {"widgettype": "Button", "id": "btn_toggle_sidebar", "options": {"label": "☰", "css": "text", "color": "#fff", "cfontsize": 1.4}, "binds": [{"wid": "self", "event": "click", "actiontype": "method", "target": "app.sidebar_menu", "method": "toggle_collapse"}]}, - {"widgettype": "Title4", "options": {"text": "产线平台", "color": "#fff"}}, - {"widgettype": "Filler"}, - { - "widgettype": "urlwidget", - "options": { - "url": "{{entire_url('/rbac/user/user_panel.ui')}}" - } - } - ] + "widgettype": "Button", + "id": "btn_toggle_sidebar", + "options": { + "label": "☰", + "css": "text", + "color": "#fff", + "cfontsize": 1.4 + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "method", + "target": "app.sidebar_menu", + "method": "toggle_collapse" + } + ] }, { - "widgettype": "HBox", - "options": {"css": "filler", "padding": "0", "gap": "0"}, - "subwidgets": [ + "widgettype": "Title4", + "options": { + "text": "产线平台", + "color": "#fff" + } + }, + { + "widgettype": "Filler" + } + ] + }, + { + "widgettype": "HBox", + "options": { + "css": "filler", + "padding": "0", + "gap": "0" + }, + "subwidgets": [ + { + "widgettype": "VBox", + "options": { + "width": "220px", + "minWidth": "200px", + "height": "100%", + "bgcolor": "#f8fafc" + }, + "subwidgets": [ + { + "widgettype": "Menu", + "id": "sidebar_menu", + "options": { + "width": "100%", + "height": "100%", + "target": "app.main_content", + "items": [ + { + "name": "pipeline_core", + "label": "产线管理", + "icon": "{{entire_url('/imgs/cubes.svg')}}", + "url": "{{entire_url('/pipeline_core/')}}" + }, + { + "name": "pipeline_sdlc", + "label": "开发产线", + "icon": "{{entire_url('/imgs/workflow.svg')}}", + "url": "{{entire_url('/pipeline_sdlc/')}}" + }, + { + "name": "pipeline_ops", + "label": "运营管理", + "icon": "{{entire_url('/imgs/dashboard.svg')}}", + "url": "{{entire_url('/pipeline_ops/')}}" + }, + { + "name": "pipeline_dist", + "label": "分销管理", + "icon": "{{entire_url('/imgs/truck.svg')}}", + "url": "{{entire_url('/pipeline_dist/')}}" + }, + { + "name": "pipeline_task", + "label": "任务中心", + "icon": "{{entire_url('/imgs/rocket.svg')}}", + "url": "{{entire_url('/pipeline_task/')}}" + }, + { + "name": "pipeline_ktv", + "label": "KTV产线", + "icon": "{{entire_url('/imgs/mic.svg')}}", + "url": "{{entire_url('/pipeline_task/task_submit.ui')}}" + }, + { + "name": "showcase", + "label": "展示平台", + "icon": "{{entire_url('/imgs/user-circle.svg')}}", + "url": "{{entire_url('/showcase/')}}" + }, + { + "name": "tenant", + "label": "租户管理", + "icon": "{{entire_url('/imgs/globe.svg')}}", + "url": "{{entire_url('/tenant/')}}" + } + ] + } + } + ] + }, + { + "widgettype": "VScrollPanel", + "id": "main_content", + "options": { + "css": "filler", + "width": "100%", + "height": "100%" + }, + "subwidgets": [ + { + "widgettype": "VBox", + "options": { + "width": "100%", + "padding": "32px", + "spacing": "24px" + }, + "subwidgets": [ { - "widgettype": "VBox", - "options":{ - "width": "220px", - "minWidth": "200px", - "height": "100%", - "bgcolor": "#f8fafc" - }, - "subwidgets":[ - { - "widgettype": "Menu", - "id": "sidebar_menu", - "options": { - "width": "100%", - "height": "100%", - "target": "app.main_content", - "items": [ - {"name": "pipeline_core", "label": "产线管理", "icon": "{{entire_url('/imgs/cubes.svg')}}", "url": "{{entire_url('/pipeline_core/')}}"}, - {"name": "pipeline_sdlc", "label": "开发产线", "icon": "{{entire_url('/imgs/workflow.svg')}}", "url": "{{entire_url('/pipeline_sdlc/')}}"}, - {"name": "pipeline_ops", "label": "运营管理", "icon": "{{entire_url('/imgs/dashboard.svg')}}", "url": "{{entire_url('/pipeline_ops/')}}"}, - {"name": "pipeline_dist", "label": "分销管理", "icon": "{{entire_url('/imgs/truck.svg')}}", "url": "{{entire_url('/pipeline_dist/')}}"}, - {"name": "pipeline_task", "label": "任务中心", "icon": "{{entire_url('/imgs/rocket.svg')}}", "url": "{{entire_url('/pipeline_task/')}}"}, - {"name": "pipeline_ktv", "label": "KTV产线", "icon": "{{entire_url('/imgs/mic.svg')}}", "url": "{{entire_url('/pipeline_task/task_submit.ui')}}"}, - {"name": "showcase", "label": "展示平台", "icon": "{{entire_url('/imgs/user-circle.svg')}}", "url": "{{entire_url('/showcase/')}}"}, - {"name": "tenant", "label": "租户管理", "icon": "{{entire_url('/imgs/globe.svg')}}", "url": "{{entire_url('/tenant/')}}"} - ] - } - } - ] + "widgettype": "Title2", + "options": { + "text": "产线平台" + } }, { - "widgettype": "VScrollPanel", - "id": "main_content", - "options": {"css": "filler", "width": "100%", "height": "100%"}, - "subwidgets": [ - { - "widgettype": "VBox", - "options": {"width": "100%", "padding": "32px", "spacing": "24px"}, - "subwidgets": [ - {"widgettype": "Title2", "options": {"text": "产线平台"}}, - {"widgettype": "Text", "options": {"text": "管理产线定义、运营配置、分销渠道与任务执行", "cfontsize": 1.2}} - ] - } - ] + "widgettype": "Text", + "options": { + "text": "管理产线定义、运营配置、分销渠道与任务执行", + "cfontsize": 1.2 + } } - ] + ] + } + ] } - ] -} + ] + } + ] +} \ No newline at end of file diff --git a/wwwroot/pipeline_core/llm/create_llm.dspy b/wwwroot/pipeline_core/llm/create_llm.dspy new file mode 100644 index 0000000..8b81dde --- /dev/null +++ b/wwwroot/pipeline_core/llm/create_llm.dspy @@ -0,0 +1,5 @@ +func = create_llm +if func is None: + return json.dumps({'status': 'error', 'message': 'function not found'}) +result = await func(params_kw) +return result diff --git a/wwwroot/pipeline_core/llm/delete_llm.dspy b/wwwroot/pipeline_core/llm/delete_llm.dspy new file mode 100644 index 0000000..43d74cf --- /dev/null +++ b/wwwroot/pipeline_core/llm/delete_llm.dspy @@ -0,0 +1,5 @@ +func = delete_llm +if func is None: + return json.dumps({'status': 'error', 'message': 'function not found'}) +result = await func(params_kw) +return result diff --git a/wwwroot/pipeline_core/llm/get_llms.dspy b/wwwroot/pipeline_core/llm/get_llms.dspy new file mode 100644 index 0000000..931ad3a --- /dev/null +++ b/wwwroot/pipeline_core/llm/get_llms.dspy @@ -0,0 +1,11 @@ +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('llm', {'status': 'active'}) + if not recs: + recs = await sor.R('llm', {}) + if not recs: + recs = [] + return recs diff --git a/wwwroot/pipeline_core/llm/index.ui b/wwwroot/pipeline_core/llm/index.ui new file mode 100644 index 0000000..ae4b2a0 --- /dev/null +++ b/wwwroot/pipeline_core/llm/index.ui @@ -0,0 +1,43 @@ +{ + "widgettype": "Tabular", + "id": "llm_tbl", + "options": { + "width": "100%", + "title": "大模型管理", + "css": "card", + "data_url": "{{entire_url('./get_llms.dspy')}}", + "data_method": "GET", + "editable": { + "new_data_url": "{{entire_url('./create_llm.dspy')}}", + "update_data_url": "{{entire_url('./update_llm.dspy')}}", + "delete_data_url": "{{entire_url('./delete_llm.dspy')}}" + }, + "browserfields": { + "exclouded": ["id", "api_key"], + "alters": { + "status": { + "uitype": "code", + "data": [ + {"value": "active", "text": "启用"}, + {"value": "inactive", "text": "停用"} + ] + } + } + }, + "editexclouded": ["id", "created_at", "updated_at"], + "confidential_fields": ["api_key"], + "fields": [ + {"name": "id", "title": "ID", "type": "str", "cwidth": 0}, + {"name": "name", "title": "模型名称", "type": "str", "cwidth": 18}, + {"name": "provider", "title": "供应商", "type": "str", "cwidth": 12}, + {"name": "model_id", "title": "模型标识", "type": "str", "cwidth": 18}, + {"name": "api_base", "title": "API地址", "type": "str", "cwidth": 20}, + {"name": "api_key", "title": "API密钥", "type": "str", "cwidth": 14}, + {"name": "max_tokens", "title": "最大Token", "type": "str", "cwidth": 10}, + {"name": "status", "title": "状态", "type": "str", "cwidth": 8}, + {"name": "description", "title": "描述", "type": "str", "cwidth": 16}, + {"name": "created_at", "title": "创建时间", "type": "str", "cwidth": 14}, + {"name": "updated_at", "title": "更新时间", "type": "str", "cwidth": 14} + ] + } +} diff --git a/wwwroot/pipeline_core/llm/update_llm.dspy b/wwwroot/pipeline_core/llm/update_llm.dspy new file mode 100644 index 0000000..00b969e --- /dev/null +++ b/wwwroot/pipeline_core/llm/update_llm.dspy @@ -0,0 +1,5 @@ +func = update_llm +if func is None: + return json.dumps({'status': 'error', 'message': 'function not found'}) +result = await func(params_kw) +return result