diff --git a/init/data.json b/init/data.json
new file mode 100644
index 0000000..2f01ed4
--- /dev/null
+++ b/init/data.json
@@ -0,0 +1,24 @@
+{
+ "appcodes": [
+ {
+ "parentid": "pipeline_type",
+ "parentname": "产线类型",
+ "items": [
+ {"k": "text", "v": "文本"},
+ {"k": "audio", "v": "音频"},
+ {"k": "video", "v": "视频"},
+ {"k": "image", "v": "图片"},
+ {"k": "multimodal", "v": "多模态"}
+ ]
+ },
+ {
+ "parentid": "pipeline_status",
+ "parentname": "产线状态",
+ "items": [
+ {"k": "draft", "v": "草稿"},
+ {"k": "published", "v": "已发布"},
+ {"k": "archived", "v": "已归档"}
+ ]
+ }
+ ]
+}
diff --git a/json/pipeline_steps.json b/json/pipeline_steps.json
new file mode 100644
index 0000000..253b6fa
--- /dev/null
+++ b/json/pipeline_steps.json
@@ -0,0 +1,27 @@
+{
+ "tblname": "pipeline_steps",
+ "title": "产线步骤",
+ "params": {
+ "sortby": "step_order",
+ "browserfields": {
+ "exclouded": ["id", "step_config", "input_schema", "output_schema"],
+ "cwidth": {}
+ },
+ "editexclouded": [
+ "id", "created_at"
+ ],
+ "editable": {
+ "new_data_url": "{{entire_url('../api/pipeline_steps_create.dspy')}}",
+ "update_data_url": "{{entire_url('../api/pipeline_steps_update.dspy')}}",
+ "delete_data_url": "{{entire_url('../api/pipeline_steps_delete.dspy')}}"
+ },
+ "confidential_fields": [],
+ "subtables": [
+ {
+ "field": "pipeline_id",
+ "title": "所属产线",
+ "subtable": "pipelines"
+ }
+ ]
+ }
+}
diff --git a/json/pipeline_versions.json b/json/pipeline_versions.json
new file mode 100644
index 0000000..965d81a
--- /dev/null
+++ b/json/pipeline_versions.json
@@ -0,0 +1,27 @@
+{
+ "tblname": "pipeline_versions",
+ "title": "发布记录",
+ "params": {
+ "sortby": "created_at",
+ "browserfields": {
+ "exclouded": ["id", "config_snapshot"],
+ "cwidth": {}
+ },
+ "editexclouded": [
+ "id", "created_at"
+ ],
+ "editable": {
+ "new_data_url": "{{entire_url('../api/pipeline_versions_create.dspy')}}",
+ "update_data_url": "{{entire_url('../api/pipeline_versions_update.dspy')}}",
+ "delete_data_url": "{{entire_url('../api/pipeline_versions_delete.dspy')}}"
+ },
+ "confidential_fields": [],
+ "subtables": [
+ {
+ "field": "pipeline_id",
+ "title": "所属产线",
+ "subtable": "pipelines"
+ }
+ ]
+ }
+}
diff --git a/json/pipelines.json b/json/pipelines.json
new file mode 100644
index 0000000..7bae2bd
--- /dev/null
+++ b/json/pipelines.json
@@ -0,0 +1,21 @@
+{
+ "tblname": "pipelines",
+ "title": "产线定义",
+ "params": {
+ "sortby": "created_at",
+ "logined_userorgid": "org_id",
+ "browserfields": {
+ "exclouded": ["id", "model_api_key", "pipeline_config"],
+ "cwidth": {}
+ },
+ "editexclouded": [
+ "id", "created_at", "updated_at"
+ ],
+ "editable": {
+ "new_data_url": "{{entire_url('../api/pipelines_create.dspy')}}",
+ "update_data_url": "{{entire_url('../api/pipelines_update.dspy')}}",
+ "delete_data_url": "{{entire_url('../api/pipelines_delete.dspy')}}"
+ },
+ "confidential_fields": ["model_api_key"]
+ }
+}
diff --git a/models/pipeline_steps.json b/models/pipeline_steps.json
new file mode 100644
index 0000000..7eb96ea
--- /dev/null
+++ b/models/pipeline_steps.json
@@ -0,0 +1,110 @@
+{
+ "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"
+ }
+ ]
+}
diff --git a/models/pipeline_versions.json b/models/pipeline_versions.json
new file mode 100644
index 0000000..d524263
--- /dev/null
+++ b/models/pipeline_versions.json
@@ -0,0 +1,85 @@
+{
+ "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"
+ }
+ ]
+}
diff --git a/models/pipelines.json b/models/pipelines.json
new file mode 100644
index 0000000..5b6e24b
--- /dev/null
+++ b/models/pipelines.json
@@ -0,0 +1,126 @@
+{
+ "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": "id='pipeline_type'"
+ },
+ {
+ "field": "status",
+ "table": "appcodes_kv",
+ "valuefield": "k",
+ "textfield": "v",
+ "cond": "id='pipeline_status'"
+ }
+ ]
+}
diff --git a/pipeline_core/__init__.py b/pipeline_core/__init__.py
new file mode 100644
index 0000000..34a9d89
--- /dev/null
+++ b/pipeline_core/__init__.py
@@ -0,0 +1,14 @@
+# pipeline_core/__init__.py
+from .init import (
+ create_pipeline,
+ update_pipeline,
+ delete_pipeline,
+ create_pipeline_step,
+ update_pipeline_step,
+ delete_pipeline_step,
+ create_pipeline_version,
+ update_pipeline_version,
+ delete_pipeline_version,
+ publish_pipeline,
+ load_pipeline_core,
+)
diff --git a/pipeline_core/init.py b/pipeline_core/init.py
new file mode 100644
index 0000000..3bbfb57
--- /dev/null
+++ b/pipeline_core/init.py
@@ -0,0 +1,279 @@
+"""pipeline_core 模块 - 产线管理核心模块"""
+import json
+from appPublic.uniqueID import getID
+from sqlor.dbpools import DBPools
+from ahserver.serverenv import ServerEnv
+from appPublic.log import debug
+
+
+MODULE_NAME = 'pipeline_core'
+DBNAME = 'pipeline'
+
+
+def _get_sor():
+ """Get database pool and dbname for pipeline_core module."""
+ return DBPools(), DBNAME
+
+
+async def create_pipeline(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('pipelines', data)
+ result['success'] = True
+ result['message'] = '创建成功'
+ result['id'] = data['id']
+ except Exception as e:
+ result['message'] = str(e)
+ return json.dumps(result, ensure_ascii=False, default=str)
+
+
+async def update_pipeline(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)
+ record_id = data.pop('id', None)
+ if not record_id:
+ result['message'] = '缺少id'
+ else:
+ await sor.U('pipelines', data, {'id': record_id})
+ 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_pipeline(params_kw):
+ """删除产线记录"""
+ result = {'success': False, 'message': ''}
+ try:
+ db, dbname = _get_sor()
+ async with db.sqlorContext(dbname) as sor:
+ record_id = params_kw.get('id')
+ if not record_id:
+ result['message'] = '缺少id'
+ else:
+ await sor.D('pipelines', {'id': record_id})
+ result['success'] = True
+ result['message'] = '删除成功'
+ except Exception as e:
+ result['message'] = str(e)
+ return json.dumps(result, ensure_ascii=False, default=str)
+
+
+async def create_pipeline_step(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('pipeline_steps', data)
+ result['success'] = True
+ result['message'] = '创建成功'
+ result['id'] = data['id']
+ except Exception as e:
+ result['message'] = str(e)
+ return json.dumps(result, ensure_ascii=False, default=str)
+
+
+async def update_pipeline_step(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)
+ record_id = data.pop('id', None)
+ if not record_id:
+ result['message'] = '缺少id'
+ else:
+ await sor.U('pipeline_steps', data, {'id': record_id})
+ 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_pipeline_step(params_kw):
+ """删除产线步骤"""
+ result = {'success': False, 'message': ''}
+ try:
+ db, dbname = _get_sor()
+ async with db.sqlorContext(dbname) as sor:
+ record_id = params_kw.get('id')
+ if not record_id:
+ result['message'] = '缺少id'
+ else:
+ await sor.D('pipeline_steps', {'id': record_id})
+ result['success'] = True
+ result['message'] = '删除成功'
+ except Exception as e:
+ result['message'] = str(e)
+ return json.dumps(result, ensure_ascii=False, default=str)
+
+
+async def create_pipeline_version(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('pipeline_versions', data)
+ result['success'] = True
+ result['message'] = '创建成功'
+ result['id'] = data['id']
+ except Exception as e:
+ result['message'] = str(e)
+ return json.dumps(result, ensure_ascii=False, default=str)
+
+
+async def update_pipeline_version(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)
+ record_id = data.pop('id', None)
+ if not record_id:
+ result['message'] = '缺少id'
+ else:
+ await sor.U('pipeline_versions', data, {'id': record_id})
+ 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_pipeline_version(params_kw):
+ """删除产线发布记录"""
+ result = {'success': False, 'message': ''}
+ try:
+ db, dbname = _get_sor()
+ async with db.sqlorContext(dbname) as sor:
+ record_id = params_kw.get('id')
+ if not record_id:
+ result['message'] = '缺少id'
+ else:
+ await sor.D('pipeline_versions', {'id': record_id})
+ result['success'] = True
+ result['message'] = '删除成功'
+ except Exception as e:
+ result['message'] = str(e)
+ return json.dumps(result, ensure_ascii=False, default=str)
+
+
+async def publish_pipeline(params_kw):
+ """发布产线 - 创建发布记录并更新产线状态"""
+ result = {'success': False, 'message': ''}
+ try:
+ pipeline_id = params_kw.get('pipeline_id')
+ if not pipeline_id:
+ result['message'] = '缺少pipeline_id'
+ return json.dumps(result, ensure_ascii=False, default=str)
+
+ changelog = params_kw.get('changelog', '')
+ published_by = params_kw.get('published_by', '')
+
+ db, dbname = _get_sor()
+ async with db.sqlorContext(dbname) as sor:
+ # Get current pipeline
+ recs = await sor.R('pipelines', {'id': pipeline_id})
+ if not recs:
+ result['message'] = f'产线不存在: {pipeline_id}'
+ return json.dumps(result, ensure_ascii=False, default=str)
+
+ pipeline = recs[0]
+ current_version = pipeline.get('version', '1.0.0') if hasattr(pipeline, 'get') else getattr(pipeline, 'version', '1.0.0')
+
+ # Build config snapshot from current pipeline data
+ config_snapshot = json.dumps({
+ 'pipeline_config': getattr(pipeline, 'pipeline_config', None),
+ 'model_api_url': getattr(pipeline, 'model_api_url', None),
+ 'version': current_version,
+ }, ensure_ascii=False, default=str)
+
+ # Create version record
+ version_id = getID()
+ version_data = {
+ 'id': version_id,
+ 'pipeline_id': pipeline_id,
+ 'version': current_version,
+ 'publish_status': 'published',
+ 'published_by': published_by,
+ 'changelog': changelog,
+ 'config_snapshot': config_snapshot,
+ }
+ await sor.C('pipeline_versions', version_data)
+
+ # Update pipeline status to published
+ await sor.U('pipelines', {'status': 'published'}, {'id': pipeline_id})
+
+ result['success'] = True
+ result['message'] = '发布成功'
+ result['version_id'] = version_id
+ except Exception as e:
+ result['message'] = str(e)
+ return json.dumps(result, ensure_ascii=False, default=str)
+
+
+def load_pipeline_core():
+ """注册函数到 ServerEnv"""
+ env = ServerEnv()
+ # Pipelines
+ env.create_pipeline = create_pipeline
+ env.create_pipelines = create_pipeline
+ env.update_pipeline = update_pipeline
+ env.update_pipelines = update_pipeline
+ env.delete_pipeline = delete_pipeline
+ env.delete_pipelines = delete_pipeline
+ # Pipeline Steps
+ env.create_pipeline_step = create_pipeline_step
+ env.create_pipeline_steps = create_pipeline_step
+ env.update_pipeline_step = update_pipeline_step
+ env.update_pipeline_steps = update_pipeline_step
+ env.delete_pipeline_step = delete_pipeline_step
+ env.delete_pipeline_steps = delete_pipeline_step
+ # Pipeline Versions
+ env.create_pipeline_version = create_pipeline_version
+ env.create_pipeline_versions = create_pipeline_version
+ env.update_pipeline_version = update_pipeline_version
+ env.update_pipeline_versions = update_pipeline_version
+ env.delete_pipeline_version = delete_pipeline_version
+ env.delete_pipeline_versions = delete_pipeline_version
+ # Publish
+ env.publish_pipeline = publish_pipeline
+ debug(f'[{MODULE_NAME}] module loaded')
+ return True
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..8654564
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,8 @@
+[project]
+name = "pipeline_core"
+version = "0.1.0"
+description = "产线管理核心模块 - 产线定义、步骤配置与发布管理"
+dependencies = [
+ "sqlor",
+ "bricks_for_python",
+]
diff --git a/scripts/load_path.py b/scripts/load_path.py
new file mode 100644
index 0000000..72aa88c
--- /dev/null
+++ b/scripts/load_path.py
@@ -0,0 +1,89 @@
+#!/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")
+
+MOD = "pipeline_core"
+
+# ============================================================
+# 权限路径定义
+# ============================================================
+
+# operator — 产线管理人员
+PATHS_OPERATOR = [
+ f"/{MOD}",
+ 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",
+ 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",
+]
+
+# 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.")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/wwwroot/api/pipeline_publish.dspy b/wwwroot/api/pipeline_publish.dspy
new file mode 100644
index 0000000..7e4afaa
--- /dev/null
+++ b/wwwroot/api/pipeline_publish.dspy
@@ -0,0 +1,5 @@
+func = publish_pipeline
+if func is None:
+ return json.dumps({'status': 'error', 'message': 'function not found'})
+result = await func(params_kw)
+return result
diff --git a/wwwroot/api/pipeline_steps_create.dspy b/wwwroot/api/pipeline_steps_create.dspy
new file mode 100644
index 0000000..851c278
--- /dev/null
+++ b/wwwroot/api/pipeline_steps_create.dspy
@@ -0,0 +1,5 @@
+func = create_pipeline_step
+if func is None:
+ return json.dumps({'status': 'error', 'message': 'function not found'})
+result = await func(params_kw)
+return result
diff --git a/wwwroot/api/pipeline_steps_delete.dspy b/wwwroot/api/pipeline_steps_delete.dspy
new file mode 100644
index 0000000..d73053c
--- /dev/null
+++ b/wwwroot/api/pipeline_steps_delete.dspy
@@ -0,0 +1,5 @@
+func = delete_pipeline_step
+if func is None:
+ return json.dumps({'status': 'error', 'message': 'function not found'})
+result = await func(params_kw)
+return result
diff --git a/wwwroot/api/pipeline_steps_update.dspy b/wwwroot/api/pipeline_steps_update.dspy
new file mode 100644
index 0000000..998289d
--- /dev/null
+++ b/wwwroot/api/pipeline_steps_update.dspy
@@ -0,0 +1,5 @@
+func = update_pipeline_step
+if func is None:
+ return json.dumps({'status': 'error', 'message': 'function not found'})
+result = await func(params_kw)
+return result
diff --git a/wwwroot/api/pipeline_versions_create.dspy b/wwwroot/api/pipeline_versions_create.dspy
new file mode 100644
index 0000000..3567ff2
--- /dev/null
+++ b/wwwroot/api/pipeline_versions_create.dspy
@@ -0,0 +1,5 @@
+func = create_pipeline_version
+if func is None:
+ return json.dumps({'status': 'error', 'message': 'function not found'})
+result = await func(params_kw)
+return result
diff --git a/wwwroot/api/pipeline_versions_delete.dspy b/wwwroot/api/pipeline_versions_delete.dspy
new file mode 100644
index 0000000..31e496b
--- /dev/null
+++ b/wwwroot/api/pipeline_versions_delete.dspy
@@ -0,0 +1,5 @@
+func = delete_pipeline_version
+if func is None:
+ return json.dumps({'status': 'error', 'message': 'function not found'})
+result = await func(params_kw)
+return result
diff --git a/wwwroot/api/pipeline_versions_update.dspy b/wwwroot/api/pipeline_versions_update.dspy
new file mode 100644
index 0000000..6618b2e
--- /dev/null
+++ b/wwwroot/api/pipeline_versions_update.dspy
@@ -0,0 +1,5 @@
+func = update_pipeline_version
+if func is None:
+ return json.dumps({'status': 'error', 'message': 'function not found'})
+result = await func(params_kw)
+return result
diff --git a/wwwroot/api/pipelines_create.dspy b/wwwroot/api/pipelines_create.dspy
new file mode 100644
index 0000000..ef6e24b
--- /dev/null
+++ b/wwwroot/api/pipelines_create.dspy
@@ -0,0 +1,5 @@
+func = create_pipeline
+if func is None:
+ return json.dumps({'status': 'error', 'message': 'function not found'})
+result = await func(params_kw)
+return result
diff --git a/wwwroot/api/pipelines_delete.dspy b/wwwroot/api/pipelines_delete.dspy
new file mode 100644
index 0000000..3a07d83
--- /dev/null
+++ b/wwwroot/api/pipelines_delete.dspy
@@ -0,0 +1,5 @@
+func = delete_pipeline
+if func is None:
+ return json.dumps({'status': 'error', 'message': 'function not found'})
+result = await func(params_kw)
+return result
diff --git a/wwwroot/api/pipelines_update.dspy b/wwwroot/api/pipelines_update.dspy
new file mode 100644
index 0000000..ed3b6c0
--- /dev/null
+++ b/wwwroot/api/pipelines_update.dspy
@@ -0,0 +1,5 @@
+func = update_pipeline
+if func is None:
+ return json.dumps({'status': 'error', 'message': 'function not found'})
+result = await func(params_kw)
+return result
diff --git a/wwwroot/index.ui b/wwwroot/index.ui
new file mode 100644
index 0000000..378bb9f
--- /dev/null
+++ b/wwwroot/index.ui
@@ -0,0 +1,201 @@
+{
+ "widgettype": "VBox",
+ "options": {
+ "width": "100%",
+ "height": "100%",
+ "padding": "0"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "HBox",
+ "options": {
+ "width": "100%",
+ "alignItems": "center",
+ "marginBottom": "24px"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "Title2",
+ "options": {
+ "text": "产线管理"
+ }
+ },
+ {
+ "widgettype": "Filler"
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "产线定义、步骤配置与发布管理",
+ "cfontsize": 1.2
+ }
+ }
+ ]
+ },
+ {
+ "widgettype": "VBox",
+ "options": {
+ "css": "filler",
+ "spacing": 16
+ },
+ "subwidgets": [
+ {
+ "widgettype": "ResponsableBox",
+ "options": {
+ "gap": "16px",
+ "minWidth": "250px"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "VBox",
+ "options": {
+ "css": "card",
+ "cwidth": 23,
+ "padding": "16px",
+ "cursor": "pointer",
+ "borderRadius": "8px"
+ },
+ "binds": [
+ {
+ "wid": "self",
+ "event": "click",
+ "actiontype": "urlwidget",
+ "target": "app.pipeline_core_content",
+ "options": {
+ "url": "{{entire_url('pipelines/')}}"
+ },
+ "mode": "replace"
+ }
+ ],
+ "subwidgets": [
+ {
+ "widgettype": "Svg",
+ "options": {
+ "svg": "",
+ "width": "28px",
+ "height": "28px"
+ }
+ },
+ {
+ "widgettype": "Title4",
+ "options": {
+ "text": "产线定义",
+ "marginTop": "8px"
+ }
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "管理产线基本信息与配置",
+ "cfontsize": 1.2
+ }
+ }
+ ]
+ },
+ {
+ "widgettype": "VBox",
+ "options": {
+ "css": "card",
+ "cwidth": 23,
+ "padding": "16px",
+ "cursor": "pointer",
+ "borderRadius": "8px"
+ },
+ "binds": [
+ {
+ "wid": "self",
+ "event": "click",
+ "actiontype": "urlwidget",
+ "target": "app.pipeline_core_content",
+ "options": {
+ "url": "{{entire_url('pipeline_steps/')}}"
+ },
+ "mode": "replace"
+ }
+ ],
+ "subwidgets": [
+ {
+ "widgettype": "Svg",
+ "options": {
+ "svg": "",
+ "width": "28px",
+ "height": "28px"
+ }
+ },
+ {
+ "widgettype": "Title4",
+ "options": {
+ "text": "产线步骤",
+ "marginTop": "8px"
+ }
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "配置产线执行步骤与参数",
+ "cfontsize": 1.2
+ }
+ }
+ ]
+ },
+ {
+ "widgettype": "VBox",
+ "options": {
+ "css": "card",
+ "cwidth": 23,
+ "padding": "16px",
+ "cursor": "pointer",
+ "borderRadius": "8px"
+ },
+ "binds": [
+ {
+ "wid": "self",
+ "event": "click",
+ "actiontype": "urlwidget",
+ "target": "app.pipeline_core_content",
+ "options": {
+ "url": "{{entire_url('pipeline_versions/')}}"
+ },
+ "mode": "replace"
+ }
+ ],
+ "subwidgets": [
+ {
+ "widgettype": "Svg",
+ "options": {
+ "svg": "",
+ "width": "28px",
+ "height": "28px"
+ }
+ },
+ {
+ "widgettype": "Title4",
+ "options": {
+ "text": "发布记录",
+ "marginTop": "8px"
+ }
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "查看产线版本发布历史",
+ "cfontsize": 1.2
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "widgettype": "VScrollPanel",
+ "id": "app.pipeline_core_content",
+ "options": {
+ "css": "filler",
+ "width": "100%",
+ "height": "100%"
+ }
+ }
+ ]
+ }
+ ]
+}