diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ed48494
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+__pycache__/
+*.pyc
+*.egg-info/
+build/
+logs/
+files/
+py3/
+conf/config.json
diff --git a/app/global_func.py b/app/global_func.py
new file mode 100644
index 0000000..1749389
--- /dev/null
+++ b/app/global_func.py
@@ -0,0 +1,13 @@
+from ahserver.serverenv import ServerEnv
+
+DBNAME = "pipeline"
+
+
+def get_module_dbname(mname):
+ """All modules share the pipeline database."""
+ return DBNAME
+
+
+def set_globalvariable():
+ g = ServerEnv()
+ g.get_module_dbname = get_module_dbname
diff --git a/app/pipeline_app.py b/app/pipeline_app.py
new file mode 100644
index 0000000..7b5c29b
--- /dev/null
+++ b/app/pipeline_app.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+"""Pipeline Application - 产线管理独立应用"""
+import os, sys
+
+app_dir = os.path.dirname(os.path.abspath(__file__))
+root_dir = os.path.dirname(app_dir)
+sys.path.insert(0, root_dir)
+sys.path.insert(0, app_dir)
+
+from bricks_for_python.init import load_pybricks
+from ahserver.webapp import webapp
+from ahserver.serverenv import ServerEnv
+
+from rbac.init import load_rbac
+from appbase.init import load_appbase
+
+from pipeline_core.init import load_pipeline_core
+from pipeline_ops.init import load_pipeline_ops
+from pipeline_dist.init import load_pipeline_dist
+
+from global_func import set_globalvariable
+
+
+def init():
+ set_globalvariable()
+ load_pybricks()
+ load_appbase()
+ load_rbac()
+ load_pipeline_core()
+ load_pipeline_ops()
+ load_pipeline_dist()
+
+
+if __name__ == '__main__':
+ webapp(init)
diff --git a/build.sh b/build.sh
new file mode 100644
index 0000000..bb40ea9
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,66 @@
+#!/usr/bin/env bash
+set -e
+cdir=$(cd "$(dirname "$0")" && pwd)
+cd "$cdir"
+
+echo "=== Pipeline App Build ==="
+
+# 1. Create venv
+if [ ! -d py3 ]; then
+ python3 -m venv py3
+fi
+source py3/bin/activate
+
+# 2. Install foundation packages
+mkdir -p pkgs
+for m in apppublic sqlor ahserver bricks-for-python xls2ddl rbac appbase; do
+ echo "install $m ..."
+ cd "$cdir/pkgs"
+ if [ ! -d "$m" ]; then
+ git clone https://git.opencomputing.cn/yumoqing/$m || echo "SKIP: $m clone failed"
+ fi
+ if [ -d "$m" ]; then
+ cd "$m"
+ "$cdir/py3/bin/pip" install . 2>&1 | tail -1
+ fi
+ cd "$cdir"
+done
+
+# 3. Build bricks frontend
+cd "$cdir/pkgs"
+if [ ! -d bricks ]; then
+ git clone https://git.opencomputing.cn/yumoqing/bricks || true
+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
+cd "$cdir"
+
+# 4. Install business modules
+for mod in pipeline_core pipeline_ops pipeline_dist; 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)"
+ 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
+
+# 5. Create runtime dirs
+mkdir -p "$cdir/logs" "$cdir/files"
+
+chmod +x "$cdir/start.sh" "$cdir/stop.sh"
+echo "=== Build complete ==="
diff --git a/pipeline_core/init/data.json b/pipeline_core/init/data.json
new file mode 100644
index 0000000..2f01ed4
--- /dev/null
+++ b/pipeline_core/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/pipeline_core/json/pipeline_steps.json b/pipeline_core/json/pipeline_steps.json
new file mode 100644
index 0000000..253b6fa
--- /dev/null
+++ b/pipeline_core/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/pipeline_core/json/pipeline_versions.json b/pipeline_core/json/pipeline_versions.json
new file mode 100644
index 0000000..965d81a
--- /dev/null
+++ b/pipeline_core/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/pipeline_core/json/pipelines.json b/pipeline_core/json/pipelines.json
new file mode 100644
index 0000000..7bae2bd
--- /dev/null
+++ b/pipeline_core/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/pipeline_core/models/pipeline_steps.json b/pipeline_core/models/pipeline_steps.json
new file mode 100644
index 0000000..7eb96ea
--- /dev/null
+++ b/pipeline_core/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/pipeline_core/models/pipeline_versions.json b/pipeline_core/models/pipeline_versions.json
new file mode 100644
index 0000000..d524263
--- /dev/null
+++ b/pipeline_core/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/pipeline_core/models/pipelines.json b/pipeline_core/models/pipelines.json
new file mode 100644
index 0000000..5b6e24b
--- /dev/null
+++ b/pipeline_core/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/pipeline_core/__init__.py b/pipeline_core/pipeline_core/__init__.py
new file mode 100644
index 0000000..34a9d89
--- /dev/null
+++ b/pipeline_core/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/pipeline_core/init.py b/pipeline_core/pipeline_core/init.py
new file mode 100644
index 0000000..3bbfb57
--- /dev/null
+++ b/pipeline_core/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/pipeline_core/pyproject.toml b/pipeline_core/pyproject.toml
new file mode 100644
index 0000000..8654564
--- /dev/null
+++ b/pipeline_core/pyproject.toml
@@ -0,0 +1,8 @@
+[project]
+name = "pipeline_core"
+version = "0.1.0"
+description = "产线管理核心模块 - 产线定义、步骤配置与发布管理"
+dependencies = [
+ "sqlor",
+ "bricks_for_python",
+]
diff --git a/pipeline_core/scripts/load_path.py b/pipeline_core/scripts/load_path.py
new file mode 100644
index 0000000..72aa88c
--- /dev/null
+++ b/pipeline_core/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/pipeline_core/wwwroot/api/pipeline_publish.dspy b/pipeline_core/wwwroot/api/pipeline_publish.dspy
new file mode 100644
index 0000000..7e4afaa
--- /dev/null
+++ b/pipeline_core/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/pipeline_core/wwwroot/api/pipeline_steps_create.dspy b/pipeline_core/wwwroot/api/pipeline_steps_create.dspy
new file mode 100644
index 0000000..851c278
--- /dev/null
+++ b/pipeline_core/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/pipeline_core/wwwroot/api/pipeline_steps_delete.dspy b/pipeline_core/wwwroot/api/pipeline_steps_delete.dspy
new file mode 100644
index 0000000..d73053c
--- /dev/null
+++ b/pipeline_core/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/pipeline_core/wwwroot/api/pipeline_steps_update.dspy b/pipeline_core/wwwroot/api/pipeline_steps_update.dspy
new file mode 100644
index 0000000..998289d
--- /dev/null
+++ b/pipeline_core/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/pipeline_core/wwwroot/api/pipeline_versions_create.dspy b/pipeline_core/wwwroot/api/pipeline_versions_create.dspy
new file mode 100644
index 0000000..3567ff2
--- /dev/null
+++ b/pipeline_core/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/pipeline_core/wwwroot/api/pipeline_versions_delete.dspy b/pipeline_core/wwwroot/api/pipeline_versions_delete.dspy
new file mode 100644
index 0000000..31e496b
--- /dev/null
+++ b/pipeline_core/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/pipeline_core/wwwroot/api/pipeline_versions_update.dspy b/pipeline_core/wwwroot/api/pipeline_versions_update.dspy
new file mode 100644
index 0000000..6618b2e
--- /dev/null
+++ b/pipeline_core/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/pipeline_core/wwwroot/api/pipelines_create.dspy b/pipeline_core/wwwroot/api/pipelines_create.dspy
new file mode 100644
index 0000000..ef6e24b
--- /dev/null
+++ b/pipeline_core/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/pipeline_core/wwwroot/api/pipelines_delete.dspy b/pipeline_core/wwwroot/api/pipelines_delete.dspy
new file mode 100644
index 0000000..3a07d83
--- /dev/null
+++ b/pipeline_core/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/pipeline_core/wwwroot/api/pipelines_update.dspy b/pipeline_core/wwwroot/api/pipelines_update.dspy
new file mode 100644
index 0000000..ed3b6c0
--- /dev/null
+++ b/pipeline_core/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/pipeline_core/wwwroot/index.ui b/pipeline_core/wwwroot/index.ui
new file mode 100644
index 0000000..378bb9f
--- /dev/null
+++ b/pipeline_core/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%"
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/pipeline_dist/init/data.json b/pipeline_dist/init/data.json
new file mode 100644
index 0000000..b108a82
--- /dev/null
+++ b/pipeline_dist/init/data.json
@@ -0,0 +1,11 @@
+{
+ "distributor_status": {
+ "active": "活跃",
+ "suspended": "暂停",
+ "terminated": "终止"
+ },
+ "markup_type": {
+ "fixed": "固定加价",
+ "percentage": "百分比加价"
+ }
+}
diff --git a/pipeline_dist/json/distributor_pipeline.json b/pipeline_dist/json/distributor_pipeline.json
new file mode 100644
index 0000000..c0a5a65
--- /dev/null
+++ b/pipeline_dist/json/distributor_pipeline.json
@@ -0,0 +1,33 @@
+{
+ "tblname": "distributor_pipeline",
+ "title": "分销商产线配置",
+ "params": {
+ "sortby": "created_at",
+ "browserfields": {
+ "exclouded": ["id"],
+ "cwidth": {},
+ "alters": {
+ "distributor_id": {"dataurl": "{{entire_url('../api/get_search_distributor_id.dspy')}}"},
+ "pipeline_id": {"dataurl": "{{entire_url('../api/get_search_pipeline_id.dspy')}}"}
+ }
+ },
+ "editexclouded": [
+ "id", "created_at", "updated_at"
+ ],
+ "editable": {
+ "new_data_url": "{{entire_url('../api/distributor_pipeline_create.dspy')}}",
+ "update_data_url": "{{entire_url('../api/distributor_pipeline_update.dspy')}}",
+ "delete_data_url": "{{entire_url('../api/distributor_pipeline_delete.dspy')}}"
+ },
+ "confidential_fields": [],
+ "subtables": [
+ {"field": "distributor_id", "title": "分销商", "subtable": "distributors"},
+ {"field": "pipeline_id", "title": "产线", "subtable": "pipelines"}
+ ],
+ "data_filter": {
+ "distributor_id": "",
+ "pipeline_id": "",
+ "status": ""
+ }
+ }
+}
diff --git a/pipeline_dist/json/distributors.json b/pipeline_dist/json/distributors.json
new file mode 100644
index 0000000..5162c31
--- /dev/null
+++ b/pipeline_dist/json/distributors.json
@@ -0,0 +1,24 @@
+{
+ "tblname": "distributors",
+ "title": "分销商管理",
+ "params": {
+ "sortby": "created_at",
+ "browserfields": {
+ "exclouded": ["id", "api_key", "remarks"],
+ "cwidth": {}
+ },
+ "editexclouded": [
+ "id", "created_at", "updated_at"
+ ],
+ "editable": {
+ "new_data_url": "{{entire_url('../api/distributors_create.dspy')}}",
+ "update_data_url": "{{entire_url('../api/distributors_update.dspy')}}",
+ "delete_data_url": "{{entire_url('../api/distributors_delete.dspy')}}"
+ },
+ "confidential_fields": ["api_key"],
+ "data_filter": {
+ "name": "",
+ "status": ""
+ }
+ }
+}
diff --git a/pipeline_dist/models/distributor_pipeline.json b/pipeline_dist/models/distributor_pipeline.json
new file mode 100644
index 0000000..7c3443e
--- /dev/null
+++ b/pipeline_dist/models/distributor_pipeline.json
@@ -0,0 +1,26 @@
+{
+ "summary": [{"name": "distributor_pipeline", "title": "分销商-产线关联定价表", "primary": ["id"]}],
+ "fields": [
+ {"name": "id", "title": "主键", "type": "str", "length": 32, "nullable": "no"},
+ {"name": "distributor_id", "title": "分销商ID", "type": "str", "length": 32, "nullable": "no"},
+ {"name": "pipeline_id", "title": "产线ID", "type": "str", "length": 32, "nullable": "no"},
+ {"name": "custom_price", "title": "自定义单价", "type": "double", "length": 15, "dec": 4},
+ {"name": "markup_type", "title": "加价方式", "type": "str", "length": 20},
+ {"name": "markup_value", "title": "加价值", "type": "double", "length": 10, "dec": 2},
+ {"name": "daily_limit", "title": "日限额", "type": "int", "default": "0"},
+ {"name": "monthly_limit", "title": "月限额", "type": "int", "default": "0"},
+ {"name": "today_usage", "title": "今日已用", "type": "int", "default": "0"},
+ {"name": "month_usage", "title": "本月已用", "type": "int", "default": "0"},
+ {"name": "status", "title": "状态", "type": "str", "length": 20, "default": "active"},
+ {"name": "created_at", "title": "创建时间", "type": "timestamp"},
+ {"name": "updated_at", "title": "更新时间", "type": "timestamp"}
+ ],
+ "indexes": [
+ {"name": "idx_dp_dist_pipe", "idxtype": "unique", "idxfields": ["distributor_id", "pipeline_id"]},
+ {"name": "idx_dp_distributor", "idxtype": "index", "idxfields": ["distributor_id"]}
+ ],
+ "codes": [
+ {"field": "distributor_id", "table": "distributors", "valuefield": "id", "textfield": "name"},
+ {"field": "pipeline_id", "table": "pipelines", "valuefield": "id", "textfield": "name"}
+ ]
+}
diff --git a/pipeline_dist/models/distributors.json b/pipeline_dist/models/distributors.json
new file mode 100644
index 0000000..c69dbda
--- /dev/null
+++ b/pipeline_dist/models/distributors.json
@@ -0,0 +1,24 @@
+{
+ "summary": [{"name": "distributors", "title": "分销商表", "primary": ["id"]}],
+ "fields": [
+ {"name": "id", "title": "主键", "type": "str", "length": 32, "nullable": "no"},
+ {"name": "name", "title": "分销商名称", "type": "str", "length": 200, "nullable": "no"},
+ {"name": "org_id", "title": "关联组织ID", "type": "str", "length": 32},
+ {"name": "contact_name", "title": "联系人", "type": "str", "length": 50},
+ {"name": "contact_phone", "title": "联系电话", "type": "str", "length": 20},
+ {"name": "contact_email", "title": "联系邮箱", "type": "str", "length": 100},
+ {"name": "api_key", "title": "API密钥", "type": "str", "length": 200},
+ {"name": "commission_rate", "title": "佣金比例%", "type": "double", "length": 5, "dec": 2, "default": "0"},
+ {"name": "status", "title": "状态", "type": "str", "length": 20, "default": "active"},
+ {"name": "agreement_start", "title": "协议开始日", "type": "date"},
+ {"name": "agreement_end", "title": "协议结束日", "type": "date"},
+ {"name": "remarks", "title": "备注", "type": "text"},
+ {"name": "created_by", "title": "创建人", "type": "str", "length": 32},
+ {"name": "created_at", "title": "创建时间", "type": "timestamp"},
+ {"name": "updated_at", "title": "更新时间", "type": "timestamp"}
+ ],
+ "indexes": [
+ {"name": "idx_dist_name", "idxtype": "index", "idxfields": ["name"]},
+ {"name": "idx_dist_status", "idxtype": "index", "idxfields": ["status"]}
+ ]
+}
diff --git a/pipeline_dist/pipeline_dist/__init__.py b/pipeline_dist/pipeline_dist/__init__.py
new file mode 100644
index 0000000..c584987
--- /dev/null
+++ b/pipeline_dist/pipeline_dist/__init__.py
@@ -0,0 +1,13 @@
+from .init import (
+ MODULE_NAME,
+ DBNAME,
+ distributors_create,
+ distributors_update,
+ distributors_delete,
+ distributor_pipeline_create,
+ distributor_pipeline_update,
+ distributor_pipeline_delete,
+ generate_api_key,
+ calculate_price,
+ load_pipeline_dist,
+)
diff --git a/pipeline_dist/pipeline_dist/init.py b/pipeline_dist/pipeline_dist/init.py
new file mode 100644
index 0000000..1144c50
--- /dev/null
+++ b/pipeline_dist/pipeline_dist/init.py
@@ -0,0 +1,229 @@
+"""pipeline_dist 模块 - 分销商管理模块"""
+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_dist'
+DBNAME = 'pipeline'
+
+
+def _get_sor():
+ """Get database pool and dbname for pipeline_dist module."""
+ return DBPools(), DBNAME
+
+
+async def distributors_create(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('distributors', 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 distributors_update(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)
+ if not data.get('id'):
+ result['message'] = '缺少id'
+ else:
+ await sor.U('distributors', 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 distributors_delete(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('distributors', {'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 distributor_pipeline_create(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('distributor_pipeline', 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 distributor_pipeline_update(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)
+ if not data.get('id'):
+ result['message'] = '缺少id'
+ else:
+ await sor.U('distributor_pipeline', 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 distributor_pipeline_delete(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('distributor_pipeline', {'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 generate_api_key(params_kw):
+ """Generate a new API key for a distributor"""
+ result = {'success': False, 'message': ''}
+ try:
+ distributor_id = params_kw.get('id') or params_kw.get('distributor_id')
+ if not distributor_id:
+ result['message'] = '缺少distributor_id'
+ return json.dumps(result, ensure_ascii=False, default=str)
+
+ new_key = getID() + getID()
+ db, dbname = _get_sor()
+ async with db.sqlorContext(dbname) as sor:
+ await sor.U('distributors', {'id': distributor_id, 'api_key': new_key})
+ result['success'] = True
+ result['key'] = new_key
+ result['distributor_id'] = distributor_id
+ except Exception as e:
+ result['message'] = str(e)
+ return json.dumps(result, ensure_ascii=False, default=str)
+
+
+async def calculate_price(params_kw):
+ """Calculate effective price for a distributor-pipeline combination.
+
+ Returns the effective price considering custom pricing and markup.
+ If no custom pricing exists, returns None (caller should use base price).
+ """
+ result = {'success': False, 'message': '', 'price': None}
+ try:
+ distributor_id = params_kw.get('distributor_id')
+ pipeline_id = params_kw.get('pipeline_id')
+ if not distributor_id or not pipeline_id:
+ result['message'] = '缺少distributor_id或pipeline_id'
+ return json.dumps(result, ensure_ascii=False, default=str)
+
+ db, dbname = _get_sor()
+ async with db.sqlorContext(dbname) as sor:
+ rows = await sor.sqlExe(
+ 'select * from distributor_pipeline where distributor_id = :distributor_id and pipeline_id = :pipeline_id',
+ {'distributor_id': distributor_id, 'pipeline_id': pipeline_id}
+ )
+ if not rows:
+ result['success'] = True
+ result['price'] = None
+ return json.dumps(result, ensure_ascii=False, default=str)
+
+ dp = rows[0] if isinstance(rows, list) else rows
+
+ # If custom_price is set, use it as base
+ base_price = None
+ custom_price = dp.get('custom_price') if hasattr(dp, 'get') else getattr(dp, 'custom_price', None)
+ if custom_price is not None:
+ base_price = custom_price
+
+ # Apply markup if configured
+ markup_type = dp.get('markup_type') if hasattr(dp, 'get') else getattr(dp, 'markup_type', None)
+ markup_value = dp.get('markup_value') if hasattr(dp, 'get') else getattr(dp, 'markup_value', None)
+
+ if markup_type and markup_value is not None and base_price is not None:
+ if markup_type == 'fixed':
+ result['price'] = base_price + markup_value
+ elif markup_type == 'percentage':
+ result['price'] = base_price * (1 + markup_value / 100)
+ else:
+ result['price'] = base_price
+
+ result['success'] = True
+ except Exception as e:
+ result['message'] = str(e)
+ return json.dumps(result, ensure_ascii=False, default=str)
+
+
+def load_pipeline_dist():
+ """注册函数到 ServerEnv"""
+ env = ServerEnv()
+ # Distributors
+ env.distributors_create = distributors_create
+ env.distributors_creates = distributors_create
+ env.distributors_update = distributors_update
+ env.distributors_updates = distributors_update
+ env.distributors_delete = distributors_delete
+ env.distributors_deletes = distributors_delete
+ # Distributor Pipeline
+ env.distributor_pipeline_create = distributor_pipeline_create
+ env.distributor_pipeline_creates = distributor_pipeline_create
+ env.distributor_pipeline_update = distributor_pipeline_update
+ env.distributor_pipeline_updates = distributor_pipeline_update
+ env.distributor_pipeline_delete = distributor_pipeline_delete
+ env.distributor_pipeline_deletes = distributor_pipeline_delete
+ # Utility functions
+ env.generate_api_key = generate_api_key
+ env.calculate_price = calculate_price
+ debug(f'[{MODULE_NAME}] module loaded')
+ return True
diff --git a/pipeline_dist/pyproject.toml b/pipeline_dist/pyproject.toml
new file mode 100644
index 0000000..3036833
--- /dev/null
+++ b/pipeline_dist/pyproject.toml
@@ -0,0 +1,9 @@
+[project]
+name = "pipeline_dist"
+version = "0.1.0"
+description = "Pipeline distributor management module"
+requires-python = ">=3.8"
+dependencies = [
+ "sqlor",
+ "bricks_for_python",
+]
diff --git a/pipeline_dist/scripts/load_path.py b/pipeline_dist/scripts/load_path.py
new file mode 100644
index 0000000..ca77c30
--- /dev/null
+++ b/pipeline_dist/scripts/load_path.py
@@ -0,0 +1,22 @@
+"""Register RBAC paths for pipeline_dist module."""
+
+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',
+]
+
+
+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,
+ })
diff --git a/pipeline_dist/wwwroot/api/distributor_generate_key.dspy b/pipeline_dist/wwwroot/api/distributor_generate_key.dspy
new file mode 100644
index 0000000..2bc029f
--- /dev/null
+++ b/pipeline_dist/wwwroot/api/distributor_generate_key.dspy
@@ -0,0 +1,2 @@
+result = await generate_api_key(params_kw)
+return result
diff --git a/pipeline_dist/wwwroot/api/distributor_pipeline_create.dspy b/pipeline_dist/wwwroot/api/distributor_pipeline_create.dspy
new file mode 100644
index 0000000..180ea4b
--- /dev/null
+++ b/pipeline_dist/wwwroot/api/distributor_pipeline_create.dspy
@@ -0,0 +1,2 @@
+result = await distributor_pipeline_create(params_kw)
+return result
diff --git a/pipeline_dist/wwwroot/api/distributor_pipeline_delete.dspy b/pipeline_dist/wwwroot/api/distributor_pipeline_delete.dspy
new file mode 100644
index 0000000..9d54cc0
--- /dev/null
+++ b/pipeline_dist/wwwroot/api/distributor_pipeline_delete.dspy
@@ -0,0 +1,2 @@
+result = await distributor_pipeline_delete(params_kw)
+return result
diff --git a/pipeline_dist/wwwroot/api/distributor_pipeline_update.dspy b/pipeline_dist/wwwroot/api/distributor_pipeline_update.dspy
new file mode 100644
index 0000000..16ee224
--- /dev/null
+++ b/pipeline_dist/wwwroot/api/distributor_pipeline_update.dspy
@@ -0,0 +1,2 @@
+result = await distributor_pipeline_update(params_kw)
+return result
diff --git a/pipeline_dist/wwwroot/api/distributors_create.dspy b/pipeline_dist/wwwroot/api/distributors_create.dspy
new file mode 100644
index 0000000..999d8b5
--- /dev/null
+++ b/pipeline_dist/wwwroot/api/distributors_create.dspy
@@ -0,0 +1,2 @@
+result = await distributors_create(params_kw)
+return result
diff --git a/pipeline_dist/wwwroot/api/distributors_delete.dspy b/pipeline_dist/wwwroot/api/distributors_delete.dspy
new file mode 100644
index 0000000..a666ef2
--- /dev/null
+++ b/pipeline_dist/wwwroot/api/distributors_delete.dspy
@@ -0,0 +1,2 @@
+result = await distributors_delete(params_kw)
+return result
diff --git a/pipeline_dist/wwwroot/api/distributors_update.dspy b/pipeline_dist/wwwroot/api/distributors_update.dspy
new file mode 100644
index 0000000..7be53b3
--- /dev/null
+++ b/pipeline_dist/wwwroot/api/distributors_update.dspy
@@ -0,0 +1,2 @@
+result = await distributors_update(params_kw)
+return result
diff --git a/pipeline_dist/wwwroot/api/get_search_distributor_id.dspy b/pipeline_dist/wwwroot/api/get_search_distributor_id.dspy
new file mode 100644
index 0000000..238b917
--- /dev/null
+++ b/pipeline_dist/wwwroot/api/get_search_distributor_id.dspy
@@ -0,0 +1,8 @@
+data = [{'value': '', 'text': '全部'}]
+try:
+ async with get_sor_context(request._run_ns, 'pipeline') as sor:
+ rows = await sor.sqlExe('select id as value, name as text from distributors order by name', {})
+ data.extend(list(rows))
+except Exception as e:
+ debug('error: ' + str(e))
+return json.dumps(data, ensure_ascii=False)
diff --git a/pipeline_dist/wwwroot/api/get_search_pipeline_id.dspy b/pipeline_dist/wwwroot/api/get_search_pipeline_id.dspy
new file mode 100644
index 0000000..5519023
--- /dev/null
+++ b/pipeline_dist/wwwroot/api/get_search_pipeline_id.dspy
@@ -0,0 +1,8 @@
+data = [{'value': '', 'text': '全部'}]
+try:
+ async with get_sor_context(request._run_ns, 'pipeline') as sor:
+ rows = await sor.sqlExe('select id as value, name as text from pipelines order by name', {})
+ data.extend(list(rows))
+except Exception as e:
+ debug('error: ' + str(e))
+return json.dumps(data, ensure_ascii=False)
diff --git a/pipeline_dist/wwwroot/index.ui b/pipeline_dist/wwwroot/index.ui
new file mode 100644
index 0000000..d234498
--- /dev/null
+++ b/pipeline_dist/wwwroot/index.ui
@@ -0,0 +1,16 @@
+{
+ "title": "分销管理",
+ "layout": "cards",
+ "cards": [
+ {
+ "title": "分销商管理",
+ "icon": "people",
+ "url": "{{entire_url('../json/distributors.json')}}"
+ },
+ {
+ "title": "分销商产线配置",
+ "icon": "link",
+ "url": "{{entire_url('../json/distributor_pipeline.json')}}"
+ }
+ ]
+}
diff --git a/pipeline_ops/init/data.json b/pipeline_ops/init/data.json
new file mode 100644
index 0000000..04b8e85
--- /dev/null
+++ b/pipeline_ops/init/data.json
@@ -0,0 +1,28 @@
+{
+ "appcodes": {
+ "pricing_type": {
+ "per_call": "按次计费",
+ "per_token": "按Token计费",
+ "subscription": "订阅制"
+ },
+ "pricing_status": {
+ "active": "生效中",
+ "expired": "已过期",
+ "pending": "待生效"
+ },
+ "capacity_status": {
+ "active": "正常",
+ "paused": "已暂停",
+ "exhausted": "已耗尽"
+ },
+ "usage_status": {
+ "success": "成功",
+ "failed": "失败",
+ "timeout": "超时"
+ },
+ "currency": {
+ "CNY": "人民币",
+ "USD": "美元"
+ }
+ }
+}
diff --git a/pipeline_ops/json/pipeline_capacity.json b/pipeline_ops/json/pipeline_capacity.json
new file mode 100644
index 0000000..9ab6b41
--- /dev/null
+++ b/pipeline_ops/json/pipeline_capacity.json
@@ -0,0 +1,32 @@
+{
+ "tblname": "pipeline_capacity",
+ "title": "产线容量管理",
+ "params": {
+ "sortby": ["pipeline_id"],
+ "logined_userorgid": "org_id",
+ "browserfields": {
+ "exclouded": ["id"],
+ "alters": {
+ "pipeline_id": {
+ "uitype": "code",
+ "dataurl": "{{entire_url('../api/get_search_pipeline_id.dspy')}}"
+ },
+ "status": {
+ "uitype": "code",
+ "data": [{"value": "active", "text": "生效中"}, {"value": "disabled", "text": "已停用"}]
+ }
+ }
+ },
+ "editable": {
+ "new_data_url": "{{entire_url('../api/pipeline_capacity_create.dspy')}}",
+ "update_data_url": "{{entire_url('../api/pipeline_capacity_update.dspy')}}",
+ "delete_data_url": "{{entire_url('../api/pipeline_capacity_delete.dspy')}}"
+ },
+ "data_filter": {
+ "AND": [
+ {"field": "pipeline_id", "op": "=", "var": "filter_pipeline_id"},
+ {"field": "status", "op": "=", "var": "filter_status"}
+ ]
+ }
+ }
+}
diff --git a/pipeline_ops/json/pipeline_pricing.json b/pipeline_ops/json/pipeline_pricing.json
new file mode 100644
index 0000000..3dfbeb1
--- /dev/null
+++ b/pipeline_ops/json/pipeline_pricing.json
@@ -0,0 +1,36 @@
+{
+ "tblname": "pipeline_pricing",
+ "title": "产线定价管理",
+ "params": {
+ "sortby": ["effective_date desc"],
+ "logined_userorgid": "org_id",
+ "browserfields": {
+ "exclouded": ["id"],
+ "alters": {
+ "pipeline_id": {
+ "uitype": "code",
+ "dataurl": "{{entire_url('../api/get_search_pipeline_id.dspy')}}"
+ },
+ "pricing_type": {
+ "uitype": "code",
+ "data": [{"value": "per_call", "text": "按次计费"}, {"value": "per_token", "text": "按token计费"}, {"value": "subscription", "text": "订阅制"}]
+ },
+ "status": {
+ "uitype": "code",
+ "data": [{"value": "active", "text": "生效中"}, {"value": "expired", "text": "已过期"}, {"value": "disabled", "text": "已停用"}]
+ }
+ }
+ },
+ "editable": {
+ "new_data_url": "{{entire_url('../api/pipeline_pricing_create.dspy')}}",
+ "update_data_url": "{{entire_url('../api/pipeline_pricing_update.dspy')}}",
+ "delete_data_url": "{{entire_url('../api/pipeline_pricing_delete.dspy')}}"
+ },
+ "data_filter": {
+ "AND": [
+ {"field": "pipeline_id", "op": "=", "var": "filter_pipeline_id"},
+ {"field": "status", "op": "=", "var": "filter_status"}
+ ]
+ }
+ }
+}
diff --git a/pipeline_ops/json/pipeline_usage_log.json b/pipeline_ops/json/pipeline_usage_log.json
new file mode 100644
index 0000000..e7307af
--- /dev/null
+++ b/pipeline_ops/json/pipeline_usage_log.json
@@ -0,0 +1,35 @@
+{
+ "tblname": "pipeline_usage_log",
+ "title": "产线调用日志",
+ "params": {
+ "sortby": ["called_at desc"],
+ "logined_userorgid": "",
+ "browserfields": {
+ "exclouded": ["id"],
+ "alters": {
+ "pipeline_id": {
+ "uitype": "code",
+ "dataurl": "{{entire_url('../api/get_search_pipeline_id.dspy')}}"
+ },
+ "status": {
+ "uitype": "code",
+ "data": [{"value": "success", "text": "成功"}, {"value": "failed", "text": "失败"}]
+ }
+ }
+ },
+ "editexclouded": ["id", "pipeline_id", "user_id", "distributor_id", "call_count", "token_input", "token_output", "amount", "status", "error_message", "called_at"],
+ "editable": {
+ "new_data_url": "",
+ "update_data_url": "",
+ "delete_data_url": ""
+ },
+ "data_filter": {
+ "AND": [
+ {"field": "pipeline_id", "op": "=", "var": "filter_pipeline_id"},
+ {"field": "called_at", "op": ">=", "var": "filter_called_at_start"},
+ {"field": "called_at", "op": "<=", "var": "filter_called_at_end"},
+ {"field": "status", "op": "=", "var": "filter_status"}
+ ]
+ }
+ }
+}
diff --git a/pipeline_ops/models/pipeline_capacity.json b/pipeline_ops/models/pipeline_capacity.json
new file mode 100644
index 0000000..d5ba9a9
--- /dev/null
+++ b/pipeline_ops/models/pipeline_capacity.json
@@ -0,0 +1,21 @@
+{
+ "summary": [{"name": "pipeline_capacity", "title": "产线容量表", "primary": ["id"]}],
+ "fields": [
+ {"name": "id", "title": "主键", "type": "str", "length": 32, "nullable": "no"},
+ {"name": "pipeline_id", "title": "产线ID", "type": "str", "length": 32, "nullable": "no"},
+ {"name": "max_concurrent", "title": "最大并发数", "type": "int"},
+ {"name": "daily_limit", "title": "每日限额", "type": "int"},
+ {"name": "monthly_limit", "title": "每月限额", "type": "int"},
+ {"name": "today_usage", "title": "今日用量", "type": "int"},
+ {"name": "month_usage", "title": "本月用量", "type": "int"},
+ {"name": "status", "title": "状态", "type": "str", "length": 20, "default": "active"},
+ {"name": "org_id", "title": "所属组织", "type": "str", "length": 32, "default": "0"},
+ {"name": "updated_at", "title": "更新时间", "type": "timestamp"}
+ ],
+ "indexes": [
+ {"name": "uk_capacity_pipeline", "idxtype": "unique", "idxfields": ["pipeline_id"]}
+ ],
+ "codes": [
+ {"field": "pipeline_id", "table": "pipelines", "valuefield": "id", "textfield": "name"}
+ ]
+}
diff --git a/pipeline_ops/models/pipeline_pricing.json b/pipeline_ops/models/pipeline_pricing.json
new file mode 100644
index 0000000..88b4f8a
--- /dev/null
+++ b/pipeline_ops/models/pipeline_pricing.json
@@ -0,0 +1,25 @@
+{
+ "summary": [{"name": "pipeline_pricing", "title": "产线定价表", "primary": ["id"]}],
+ "fields": [
+ {"name": "id", "title": "主键", "type": "str", "length": 32, "nullable": "no"},
+ {"name": "pipeline_id", "title": "产线ID", "type": "str", "length": 32, "nullable": "no"},
+ {"name": "pricing_type", "title": "计费方式", "type": "str", "length": 20, "nullable": "no"},
+ {"name": "unit_price", "title": "单价", "type": "double", "length": 15, "dec": 4, "nullable": "no", "default": "0"},
+ {"name": "currency", "title": "货币", "type": "str", "length": 10, "default": "CNY"},
+ {"name": "pricing_config", "title": "定价配置JSON", "type": "text"},
+ {"name": "effective_date", "title": "生效日期", "type": "date"},
+ {"name": "expiry_date", "title": "失效日期", "type": "date"},
+ {"name": "status", "title": "状态", "type": "str", "length": 20, "default": "active"},
+ {"name": "org_id", "title": "所属组织", "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_pricing_pipeline", "idxtype": "index", "idxfields": ["pipeline_id"]},
+ {"name": "idx_pricing_status_date", "idxtype": "index", "idxfields": ["status", "effective_date"]}
+ ],
+ "codes": [
+ {"field": "pipeline_id", "table": "pipelines", "valuefield": "id", "textfield": "name"}
+ ]
+}
diff --git a/pipeline_ops/models/pipeline_usage_log.json b/pipeline_ops/models/pipeline_usage_log.json
new file mode 100644
index 0000000..20efb04
--- /dev/null
+++ b/pipeline_ops/models/pipeline_usage_log.json
@@ -0,0 +1,24 @@
+{
+ "summary": [{"name": "pipeline_usage_log", "title": "产线调用日志表", "primary": ["id"]}],
+ "fields": [
+ {"name": "id", "title": "主键", "type": "str", "length": 32, "nullable": "no"},
+ {"name": "pipeline_id", "title": "产线ID", "type": "str", "length": 32, "nullable": "no"},
+ {"name": "user_id", "title": "用户ID", "type": "str", "length": 32},
+ {"name": "distributor_id", "title": "分销商ID", "type": "str", "length": 32},
+ {"name": "call_count", "title": "调用次数", "type": "int", "default": "1"},
+ {"name": "token_input", "title": "输入Token数", "type": "int", "default": "0"},
+ {"name": "token_output", "title": "输出Token数", "type": "int", "default": "0"},
+ {"name": "amount", "title": "金额", "type": "double", "length": 15, "dec": 4, "default": "0"},
+ {"name": "status", "title": "状态", "type": "str", "length": 20},
+ {"name": "error_message", "title": "错误信息", "type": "text"},
+ {"name": "called_at", "title": "调用时间", "type": "timestamp"}
+ ],
+ "indexes": [
+ {"name": "idx_usagelog_pipeline", "idxtype": "index", "idxfields": ["pipeline_id"]},
+ {"name": "idx_usagelog_called_at", "idxtype": "index", "idxfields": ["called_at"]},
+ {"name": "idx_usagelog_distributor", "idxtype": "index", "idxfields": ["distributor_id"]}
+ ],
+ "codes": [
+ {"field": "pipeline_id", "table": "pipelines", "valuefield": "id", "textfield": "name"}
+ ]
+}
diff --git a/pipeline_ops/pipeline_ops/__init__.py b/pipeline_ops/pipeline_ops/__init__.py
new file mode 100644
index 0000000..f5538d3
--- /dev/null
+++ b/pipeline_ops/pipeline_ops/__init__.py
@@ -0,0 +1,13 @@
+from .init import (
+ create_pipeline_pricing,
+ update_pipeline_pricing,
+ delete_pipeline_pricing,
+ create_pipeline_capacity,
+ update_pipeline_capacity,
+ delete_pipeline_capacity,
+ create_pipeline_usage_log,
+ update_pipeline_usage_log,
+ delete_pipeline_usage_log,
+ increment_usage,
+ load_pipeline_ops,
+)
diff --git a/pipeline_ops/pipeline_ops/init.py b/pipeline_ops/pipeline_ops/init.py
new file mode 100644
index 0000000..b12e32f
--- /dev/null
+++ b/pipeline_ops/pipeline_ops/init.py
@@ -0,0 +1,213 @@
+import json
+from appPublic.uniqueID import getID
+from sqlor.dbpools import DBPools
+from ahserver.serverenv import ServerEnv
+from appPublic.log import debug
+from datetime import datetime
+
+MODULE_NAME = 'pipeline_ops'
+DBNAME = 'pipeline'
+
+
+def _get_sor():
+ return DBPools(), DBNAME
+
+
+# ==================== pipeline_pricing ====================
+
+async def create_pipeline_pricing(params_kw):
+ 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()
+ data['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ data['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ await sor.C('pipeline_pricing', data)
+ return json.dumps({'status': 'ok', 'data': data, 'message': '创建成功'})
+ except Exception as e:
+ return json.dumps({'status': 'error', 'message': str(e)})
+
+
+async def update_pipeline_pricing(params_kw):
+ 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['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ await sor.U('pipeline_pricing', data)
+ return json.dumps({'status': 'ok', 'message': '更新成功'})
+ except Exception as e:
+ return json.dumps({'status': 'error', 'message': str(e)})
+
+
+async def delete_pipeline_pricing(params_kw):
+ try:
+ db, dbname = _get_sor()
+ async with db.sqlorContext(dbname) as sor:
+ await sor.D('pipeline_pricing', {'id': params_kw['id']})
+ return json.dumps({'status': 'ok', 'message': '删除成功'})
+ except Exception as e:
+ return json.dumps({'status': 'error', 'message': str(e)})
+
+
+# ==================== pipeline_capacity ====================
+
+async def create_pipeline_capacity(params_kw):
+ 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()
+ data['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ data['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ await sor.C('pipeline_capacity', data)
+ return json.dumps({'status': 'ok', 'data': data, 'message': '创建成功'})
+ except Exception as e:
+ return json.dumps({'status': 'error', 'message': str(e)})
+
+
+async def update_pipeline_capacity(params_kw):
+ 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['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ await sor.U('pipeline_capacity', data)
+ return json.dumps({'status': 'ok', 'message': '更新成功'})
+ except Exception as e:
+ return json.dumps({'status': 'error', 'message': str(e)})
+
+
+async def delete_pipeline_capacity(params_kw):
+ try:
+ db, dbname = _get_sor()
+ async with db.sqlorContext(dbname) as sor:
+ await sor.D('pipeline_capacity', {'id': params_kw['id']})
+ return json.dumps({'status': 'ok', 'message': '删除成功'})
+ except Exception as e:
+ return json.dumps({'status': 'error', 'message': str(e)})
+
+
+# ==================== pipeline_usage_log ====================
+
+async def create_pipeline_usage_log(params_kw):
+ 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()
+ data['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ data['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ await sor.C('pipeline_usage_log', data)
+ return json.dumps({'status': 'ok', 'data': data, 'message': '创建成功'})
+ except Exception as e:
+ return json.dumps({'status': 'error', 'message': str(e)})
+
+
+async def update_pipeline_usage_log(params_kw):
+ 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['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ await sor.U('pipeline_usage_log', data)
+ return json.dumps({'status': 'ok', 'message': '更新成功'})
+ except Exception as e:
+ return json.dumps({'status': 'error', 'message': str(e)})
+
+
+async def delete_pipeline_usage_log(params_kw):
+ try:
+ db, dbname = _get_sor()
+ async with db.sqlorContext(dbname) as sor:
+ await sor.D('pipeline_usage_log', {'id': params_kw['id']})
+ return json.dumps({'status': 'ok', 'message': '删除成功'})
+ except Exception as e:
+ return json.dumps({'status': 'error', 'message': str(e)})
+
+
+# ==================== Special: increment_usage ====================
+
+async def increment_usage(params_kw):
+ """Increment usage for a pipeline. Increments the usage_count field."""
+ try:
+ db, dbname = _get_sor()
+ async with db.sqlorContext(dbname) as sor:
+ pipeline_id = params_kw.get('pipeline_id', '')
+ if not pipeline_id:
+ return json.dumps({'status': 'error', 'message': 'pipeline_id is required'})
+ # Get current usage
+ rows = await sor.sqlExe(
+ 'select usage_count from pipeline_usage_log where pipeline_id = :pipeline_id order by created_at desc limit 1',
+ {'pipeline_id': pipeline_id}
+ )
+ current_count = 0
+ if rows and len(rows) > 0:
+ current_count = rows[0].get('usage_count', 0) or 0
+ new_count = current_count + 1
+ data = {
+ 'id': getID(),
+ 'pipeline_id': pipeline_id,
+ 'usage_count': new_count,
+ 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+ 'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ }
+ await sor.C('pipeline_usage_log', data)
+ return json.dumps({'status': 'ok', 'data': data, 'message': '使用次数已增加'})
+ except Exception as e:
+ return json.dumps({'status': 'error', 'message': str(e)})
+
+
+# ==================== Module Loader ====================
+
+def load_pipeline_ops():
+ env = ServerEnv()
+
+ # pipeline_pricing
+ env.create_pipeline_pricing = create_pipeline_pricing
+ env.create_pipeline_pricings = create_pipeline_pricing
+ env.update_pipeline_pricing = update_pipeline_pricing
+ env.update_pipeline_pricings = update_pipeline_pricing
+ env.delete_pipeline_pricing = delete_pipeline_pricing
+ env.delete_pipeline_pricings = delete_pipeline_pricing
+
+ # pipeline_capacity
+ env.create_pipeline_capacity = create_pipeline_capacity
+ env.create_pipeline_capacitys = create_pipeline_capacity
+ env.update_pipeline_capacity = update_pipeline_capacity
+ env.update_pipeline_capacitys = update_pipeline_capacity
+ env.delete_pipeline_capacity = delete_pipeline_capacity
+ env.delete_pipeline_capacitys = delete_pipeline_capacity
+
+ # pipeline_usage_log
+ env.create_pipeline_usage_log = create_pipeline_usage_log
+ env.create_pipeline_usage_logs = create_pipeline_usage_log
+ env.update_pipeline_usage_log = update_pipeline_usage_log
+ env.update_pipeline_usage_logs = update_pipeline_usage_log
+ env.delete_pipeline_usage_log = delete_pipeline_usage_log
+ env.delete_pipeline_usage_logs = delete_pipeline_usage_log
+
+ # special functions
+ env.increment_usage = increment_usage
+
+ debug(f'{MODULE_NAME} loaded successfully')
+ return True
diff --git a/pipeline_ops/pyproject.toml b/pipeline_ops/pyproject.toml
new file mode 100644
index 0000000..d807047
--- /dev/null
+++ b/pipeline_ops/pyproject.toml
@@ -0,0 +1,12 @@
+[project]
+name = "pipeline_ops"
+version = "1.0.0"
+description = "Pipeline operations module - pricing, capacity, and usage tracking"
+dependencies = [
+ "sqlor",
+ "bricks_for_python"
+]
+
+[build-system]
+requires = ["setuptools"]
+build-backend = "setuptools.build_meta"
diff --git a/pipeline_ops/scripts/load_path.py b/pipeline_ops/scripts/load_path.py
new file mode 100644
index 0000000..c6284b7
--- /dev/null
+++ b/pipeline_ops/scripts/load_path.py
@@ -0,0 +1,90 @@
+"""Register all RBAC paths for pipeline_ops module"""
+
+
+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
diff --git a/pipeline_ops/wwwroot/api/get_search_pipeline_id.dspy b/pipeline_ops/wwwroot/api/get_search_pipeline_id.dspy
new file mode 100644
index 0000000..6573cb4
--- /dev/null
+++ b/pipeline_ops/wwwroot/api/get_search_pipeline_id.dspy
@@ -0,0 +1,8 @@
+data = [{'value': '', 'text': '全部'}]
+try:
+ async with get_sor_context(request._run_ns, 'pipeline') as sor:
+ rows = await sor.sqlExe('select id as value, name as text from pipelines order by name', {})
+ data.extend(list(rows))
+except Exception as e:
+ debug('get_search_pipeline_id error: ' + str(e))
+return json.dumps(data, ensure_ascii=False)
\ No newline at end of file
diff --git a/pipeline_ops/wwwroot/api/pipeline_capacity_create.dspy b/pipeline_ops/wwwroot/api/pipeline_capacity_create.dspy
new file mode 100644
index 0000000..ce950ae
--- /dev/null
+++ b/pipeline_ops/wwwroot/api/pipeline_capacity_create.dspy
@@ -0,0 +1,5 @@
+func = create_pipeline_capacity
+if func is None:
+ return json.dumps({'status': 'error', 'message': 'function not found'})
+result = await func(params_kw)
+return result
\ No newline at end of file
diff --git a/pipeline_ops/wwwroot/api/pipeline_capacity_delete.dspy b/pipeline_ops/wwwroot/api/pipeline_capacity_delete.dspy
new file mode 100644
index 0000000..0c2c567
--- /dev/null
+++ b/pipeline_ops/wwwroot/api/pipeline_capacity_delete.dspy
@@ -0,0 +1,5 @@
+func = delete_pipeline_capacity
+if func is None:
+ return json.dumps({'status': 'error', 'message': 'function not found'})
+result = await func(params_kw)
+return result
\ No newline at end of file
diff --git a/pipeline_ops/wwwroot/api/pipeline_capacity_update.dspy b/pipeline_ops/wwwroot/api/pipeline_capacity_update.dspy
new file mode 100644
index 0000000..d3bd146
--- /dev/null
+++ b/pipeline_ops/wwwroot/api/pipeline_capacity_update.dspy
@@ -0,0 +1,5 @@
+func = update_pipeline_capacity
+if func is None:
+ return json.dumps({'status': 'error', 'message': 'function not found'})
+result = await func(params_kw)
+return result
\ No newline at end of file
diff --git a/pipeline_ops/wwwroot/api/pipeline_pricing_create.dspy b/pipeline_ops/wwwroot/api/pipeline_pricing_create.dspy
new file mode 100644
index 0000000..43a148f
--- /dev/null
+++ b/pipeline_ops/wwwroot/api/pipeline_pricing_create.dspy
@@ -0,0 +1,5 @@
+func = create_pipeline_pricing
+if func is None:
+ return json.dumps({'status': 'error', 'message': 'function not found'})
+result = await func(params_kw)
+return result
\ No newline at end of file
diff --git a/pipeline_ops/wwwroot/api/pipeline_pricing_delete.dspy b/pipeline_ops/wwwroot/api/pipeline_pricing_delete.dspy
new file mode 100644
index 0000000..ff40702
--- /dev/null
+++ b/pipeline_ops/wwwroot/api/pipeline_pricing_delete.dspy
@@ -0,0 +1,5 @@
+func = delete_pipeline_pricing
+if func is None:
+ return json.dumps({'status': 'error', 'message': 'function not found'})
+result = await func(params_kw)
+return result
\ No newline at end of file
diff --git a/pipeline_ops/wwwroot/api/pipeline_pricing_update.dspy b/pipeline_ops/wwwroot/api/pipeline_pricing_update.dspy
new file mode 100644
index 0000000..e066fe8
--- /dev/null
+++ b/pipeline_ops/wwwroot/api/pipeline_pricing_update.dspy
@@ -0,0 +1,5 @@
+func = update_pipeline_pricing
+if func is None:
+ return json.dumps({'status': 'error', 'message': 'function not found'})
+result = await func(params_kw)
+return result
\ No newline at end of file
diff --git a/pipeline_ops/wwwroot/api/pipeline_usage_log_create.dspy b/pipeline_ops/wwwroot/api/pipeline_usage_log_create.dspy
new file mode 100644
index 0000000..53018d9
--- /dev/null
+++ b/pipeline_ops/wwwroot/api/pipeline_usage_log_create.dspy
@@ -0,0 +1,5 @@
+func = create_pipeline_usage_log
+if func is None:
+ return json.dumps({'status': 'error', 'message': 'function not found'})
+result = await func(params_kw)
+return result
\ No newline at end of file
diff --git a/pipeline_ops/wwwroot/api/pipeline_usage_log_delete.dspy b/pipeline_ops/wwwroot/api/pipeline_usage_log_delete.dspy
new file mode 100644
index 0000000..1ea0380
--- /dev/null
+++ b/pipeline_ops/wwwroot/api/pipeline_usage_log_delete.dspy
@@ -0,0 +1,5 @@
+func = delete_pipeline_usage_log
+if func is None:
+ return json.dumps({'status': 'error', 'message': 'function not found'})
+result = await func(params_kw)
+return result
\ No newline at end of file
diff --git a/pipeline_ops/wwwroot/api/pipeline_usage_log_update.dspy b/pipeline_ops/wwwroot/api/pipeline_usage_log_update.dspy
new file mode 100644
index 0000000..6e5b2f4
--- /dev/null
+++ b/pipeline_ops/wwwroot/api/pipeline_usage_log_update.dspy
@@ -0,0 +1,5 @@
+func = update_pipeline_usage_log
+if func is None:
+ return json.dumps({'status': 'error', 'message': 'function not found'})
+result = await func(params_kw)
+return result
\ No newline at end of file
diff --git a/pipeline_ops/wwwroot/index.ui b/pipeline_ops/wwwroot/index.ui
new file mode 100644
index 0000000..cc9c69a
--- /dev/null
+++ b/pipeline_ops/wwwroot/index.ui
@@ -0,0 +1,26 @@
+{
+ "title": "产线运营",
+ "layout": {
+ "type": "cards",
+ "items": [
+ {
+ "title": "定价管理",
+ "icon": "price-tag",
+ "url": "pipeline_pricing",
+ "description": "管理产线计费方式和价格"
+ },
+ {
+ "title": "供应量管理",
+ "icon": "gauge",
+ "url": "pipeline_capacity",
+ "description": "配置产线并发和调用限额"
+ },
+ {
+ "title": "使用记录",
+ "icon": "list",
+ "url": "pipeline_usage_log",
+ "description": "查看产线调用和消费记录"
+ }
+ ]
+ }
+}
diff --git a/start.sh b/start.sh
new file mode 100644
index 0000000..3d5b116
--- /dev/null
+++ b/start.sh
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+set -e
+WORKDIR="$(cd "$(dirname "$0")" && pwd)"
+cd "$WORKDIR"
+
+if [ -f pipeline.pid ]; then
+ pid=$(cat pipeline.pid)
+ if kill -0 "$pid" 2>/dev/null; then
+ echo "Already running (PID $pid)"
+ exit 0
+ fi
+ rm -f pipeline.pid
+fi
+
+echo "Starting pipeline-app on port 9090..."
+$WORKDIR/py3/bin/python $WORKDIR/app/pipeline_app.py -p 9090 -w $WORKDIR >> $WORKDIR/logs/pipeline.log 2>&1 &
+echo $! > pipeline.pid
+echo "Started PID $(cat pipeline.pid)"
diff --git a/stop.sh b/stop.sh
new file mode 100644
index 0000000..78b1483
--- /dev/null
+++ b/stop.sh
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+WORKDIR="$(cd "$(dirname "$0")" && pwd)"
+cd "$WORKDIR"
+
+if [ -f pipeline.pid ]; then
+ pid=$(cat pipeline.pid)
+ if kill -0 "$pid" 2>/dev/null; then
+ kill "$pid"
+ echo "Stopped PID $pid"
+ else
+ echo "Process $pid not running"
+ fi
+ rm -f pipeline.pid
+else
+ echo "No pid file found"
+fi