diff --git a/app/global_func.py b/app/global_func.py
index 1749389..5ac05c7 100644
--- a/app/global_func.py
+++ b/app/global_func.py
@@ -1,13 +1,24 @@
from ahserver.serverenv import ServerEnv
+from ahserver.globalEnv import initEnv, password_encode as _password_encode
DBNAME = "pipeline"
+def password_encode(s):
+ """Wrapper to handle None input during login form load."""
+ if s is None:
+ return ''
+ return _password_encode(s)
+
+
def get_module_dbname(mname):
- """All modules share the pipeline database."""
- return DBNAME
+ if 'pipeline' in mname:
+ return 'pipeline'
+ return 'sage'
def set_globalvariable():
+ initEnv()
g = ServerEnv()
g.get_module_dbname = get_module_dbname
+ g.password_encode = password_encode
diff --git a/app/pipeline_app.py b/app/pipeline_app.py
index 71b29c8..66de353 100644
--- a/app/pipeline_app.py
+++ b/app/pipeline_app.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""Pipeline Application - 产线管理独立应用"""
-import os, sys
+import os, sys, asyncio
app_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = os.path.dirname(app_dir)
@@ -50,6 +50,8 @@ def init():
load_tenant()
load_ktv_adapter()
+ # Permission cache warms up on first request
+
if __name__ == '__main__':
webapp(init)
diff --git a/bin/init_data.py b/bin/init_data.py
new file mode 100644
index 0000000..9d610af
--- /dev/null
+++ b/bin/init_data.py
@@ -0,0 +1,152 @@
+#!/usr/bin/env python3
+"""
+Import initialization data for pipeline-app.
+Creates default step types, pipeline templates, and demo data.
+Run: source py3/bin/activate && python bin/init_data.py [--demo]
+"""
+import sys, os, asyncio, json
+
+SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
+ROOT_DIR = os.path.dirname(SCRIPT_DIR)
+sys.path.insert(0, os.path.join(ROOT_DIR, 'py3', 'lib', 'python3.10', 'site-packages'))
+sys.path.insert(0, ROOT_DIR)
+
+from sqlor.dbpools import DBPools
+from appPublic.jsonConfig import getConfig
+from appPublic.folderUtils import ProgramPath
+from appPublic.uniqueID import getID
+from ahserver.serverenv import ServerEnv
+from ahserver.globalEnv import initEnv
+from appPublic.timeUtils import curDateString, timestampstr
+
+# ---- Default step types ----
+STEP_TYPES = [
+ # (step_type, display_name, category, is_interactive, description, form_schema)
+ ('ai_generate', 'AI代码生成', 'AI生成', '0', '由AI自动生成代码', '{}'),
+ ('ai_review', 'AI代码审查', 'AI审查', '0', '由AI自动审查代码质量', '{}'),
+ ('ai_test', 'AI测试生成', 'AI测试', '0', '由AI生成测试用例', '{}'),
+ ('human_review', '人工代码审查', '人工审核', '1', '人工审查代码变更',
+ json.dumps({"fields":[{"name":"approved","label":"审批通过","uitype":"switch"},{"name":"comment","label":"审批意见","uitype":"text"}]})),
+ ('human_approval', '发布审批', '人工审批', '1', '人工审批发布请求',
+ json.dumps({"fields":[{"name":"approved","label":"批准发布","uitype":"switch"},{"name":"version","label":"发布版本","uitype":"text"}]})),
+ ('deploy_staging', '部署到测试环境', '部署', '0', '自动部署到测试环境', '{}'),
+ ('deploy_production', '部署到生产环境', '部署', '1', '审批后部署到生产环境',
+ json.dumps({"fields":[{"name":"confirm","label":"确认部署","uitype":"switch"}]})),
+ ('run_tests', '运行测试套件', '测试', '0', '自动运行测试套件', '{}'),
+ ('build_artifact', '构建制品', '构建', '0', '构建发布制品', '{}'),
+ ('notify_webhook', 'Webhook通知', '通知', '0', '发送Webhook通知', '{}'),
+]
+
+# ---- Default pipeline (SDLC template) ----
+SDLC_PIPELINE = {
+ 'id': 'pipeline_sdlc_default',
+ 'name': '标准SDLC产线',
+ 'description': '软件开发生命周期标准流程:AI生成→审查→测试→部署',
+ 'pipeline_type': 'sdlc',
+ 'status': 'published',
+ 'version': '1.0',
+}
+
+SDLC_STEPS = [
+ ('ai_generate', 'AI代码生成', 1),
+ ('ai_review', 'AI代码审查', 2),
+ ('run_tests', '自动测试', 3),
+ ('human_review', '人工代码审查', 4),
+ ('build_artifact', '构建制品', 5),
+ ('human_approval', '发布审批', 6),
+ ('deploy_production', '生产部署', 7),
+]
+
+
+async def init_data(demo=False):
+ config_file = os.path.join(ROOT_DIR, 'conf', 'config.json')
+ if not os.path.exists(config_file):
+ config_file = os.path.join(ROOT_DIR, '..', 'conf', 'config.json')
+ config = getConfig(config_file, NS={'workdir': ROOT_DIR, 'ProgramPath': ProgramPath()})
+ DBPools(config.databases)
+ initEnv()
+ env = ServerEnv()
+ env.get_module_dbname = lambda m: 'pipeline' if 'pipeline' in m else 'sage'
+
+ async with DBPools().sqlorContext('pipeline') as sor:
+ # 1. Create step types
+ print('Step types:')
+ for stype, display, cat, interactive, desc, schema in STEP_TYPES:
+ existing = await sor.sqlExe(
+ "SELECT step_type FROM pipeline_step_types WHERE step_type=${st}$", {'st': stype})
+ if existing:
+ print(f' SKIP {stype} ({display})')
+ continue
+ await sor.C('pipeline_step_types', {
+ 'step_type': stype, 'display_name': display,
+ 'category': cat, 'is_interactive': interactive,
+ 'description': desc, 'form_schema': schema,
+ 'created_at': timestampstr()
+ })
+ print(f' ADD {stype} ({display})')
+
+ # 2. Create default SDLC pipeline
+ print('\nPipeline:')
+ existing = await sor.sqlExe(
+ "SELECT id FROM pipelines WHERE id=${pid}$", {'pid': SDLC_PIPELINE['id']})
+ if not existing:
+ await sor.C('pipelines', {
+ **SDLC_PIPELINE,
+ 'created_by': 'system',
+ 'created_at': timestampstr(),
+ 'updated_at': timestampstr()
+ })
+ print(f' ADD {SDLC_PIPELINE["name"]}')
+
+ # Create steps
+ for stype, sname, order in SDLC_STEPS:
+ await sor.C('pipeline_steps', {
+ 'id': getID(),
+ 'pipeline_id': SDLC_PIPELINE['id'],
+ 'step_order': order,
+ 'step_name': sname,
+ 'step_type': stype,
+ 'step_config': '{}',
+ 'timeout_seconds': 3600,
+ 'retry_count': 1,
+ 'created_at': timestampstr()
+ })
+ print(f' step {order}: {sname}')
+ else:
+ print(f' SKIP {SDLC_PIPELINE["name"]} (exists)')
+
+ # 3. Demo data
+ if demo:
+ print('\nDemo data:')
+ await _create_demo(sor)
+
+ print('\nInitialization complete.')
+
+
+async def _create_demo(sor):
+ """Create demo project and iteration."""
+ pid = getID()
+ existing = await sor.sqlExe("SELECT id FROM sd_projects LIMIT 1", {})
+ if existing:
+ print(' SKIP (data exists)')
+ return
+ await sor.C('sd_projects', {
+ 'id': pid, 'name': 'Pipeline App Demo',
+ 'description': '产线平台演示项目',
+ 'project_type': 'web', 'status': 'active',
+ 'created_by': 'system', 'created_at': timestampstr()
+ })
+ print(f' ADD project: Pipeline App Demo')
+
+ iid = getID()
+ await sor.C('sd_iterations', {
+ 'id': iid, 'name': 'Sprint 1 - 基础框架',
+ 'project_id': pid, 'status': 'in_progress',
+ 'start_date': curDateString(), 'created_at': timestampstr()
+ })
+ print(f' ADD iteration: Sprint 1')
+
+
+if __name__ == '__main__':
+ demo_mode = '--demo' in sys.argv
+ asyncio.run(init_data(demo=demo_mode))
diff --git a/bin/init_perms.py b/bin/init_perms.py
new file mode 100644
index 0000000..964bcad
--- /dev/null
+++ b/bin/init_perms.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python3
+"""Initialize RBAC permissions for pipeline modules."""
+import sys, os, asyncio
+
+SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
+ROOT_DIR = os.path.dirname(SCRIPT_DIR)
+sys.path.insert(0, os.path.join(ROOT_DIR, 'py3', 'lib', 'python3.10', 'site-packages'))
+sys.path.insert(0, ROOT_DIR)
+
+from sqlor.dbpools import DBPools
+from appPublic.jsonConfig import getConfig
+from appPublic.folderUtils import ProgramPath
+from appPublic.uniqueID import getID
+from ahserver.serverenv import ServerEnv
+from ahserver.globalEnv import initEnv
+
+PERMS = [
+ ('/pipeline_core/', '产线管理访问', 'pipeline_core', 'logined'),
+ ('/pipeline-sdlc/', '开发产线访问', 'pipeline_sdlc', 'logined'),
+ ('/pipeline_ops/', '运营管理访问', 'pipeline_ops', 'logined'),
+ ('/pipeline_dist/', '分销管理访问', 'pipeline_dist', 'logined'),
+ ('/pipeline_task/', '任务中心访问', 'pipeline_task', 'logined'),
+ ('/showcase/', '展示平台访问', 'showcase', 'guest'),
+ ('/', '应用首页', 'app', 'guest'),
+]
+
+async def init_perms():
+ config = getConfig(ROOT_DIR, NS={'workdir': ROOT_DIR, 'ProgramPath': ProgramPath()})
+ DBPools(config.databases)
+ initEnv()
+ env = ServerEnv()
+ env.get_module_dbname = lambda m: 'sage'
+
+ async with DBPools().sqlorContext('sage') as sor:
+ for path, name, perm_type, role_name in PERMS:
+ # Build safe SQL with repr
+ sql = "SELECT id FROM permission WHERE path=" + repr(path) + " LIMIT 1"
+ existing = await sor.sqlExe(sql, {})
+ if existing:
+ print('SKIP {:<8s} {}'.format(role_name, path))
+ continue
+
+ pid = getID()
+ await sor.C('permission', {
+ 'id': pid, 'name': name, 'path': path,
+ 'permtype': perm_type
+ })
+ role_sql = "SELECT id FROM role WHERE name=" + repr(role_name) + " LIMIT 1"
+ role_recs = await sor.sqlExe(role_sql, {})
+ if role_recs:
+ await sor.C('rolepermission', {
+ 'id': getID(),
+ 'roleid': role_recs[0].id,
+ 'permid': pid
+ })
+ print('ADD {:<8s} {}'.format(role_name, path))
+
+ print('\nInitialized {} permissions'.format(len(PERMS)))
+
+if __name__ == '__main__':
+ asyncio.run(init_perms())
diff --git a/build.sh b/build.sh
index 15c595d..8684151 100644
--- a/build.sh
+++ b/build.sh
@@ -11,7 +11,7 @@ if [ ! -d py3 ]; then
fi
source py3/bin/activate
-# 2. Install foundation packages
+# 2. Install foundation packages (clone to pkgs)
mkdir -p pkgs
for m in apppublic sqlor ahserver bricks-for-python xls2ddl rbac appbase; do
echo "install $m ..."
@@ -33,56 +33,50 @@ if [ ! -d bricks ]; then
fi
if [ -d bricks/bricks ]; then
cd bricks/bricks && bash build.sh 2>&1 | tail -3
- ln -sf "$cdir/pkgs/bricks/dist" "$cdir/bricks"
fi
+ln -sf "$cdir/pkgs/bricks/dist" "$cdir/bricks" 2>/dev/null || true
cd "$cdir"
-# 4. Clone pipeline-sdlc and showcase
-for m in pipeline-sdlc showcase; do
- echo "clone $m ..."
+# 4. Move local business modules into pkgs/
+for mod in pipeline_core pipeline_ops pipeline_dist; do
+ if [ -d "$mod" ]; then
+ mv "$mod" "pkgs/$mod"
+ fi
+done
+
+# 5. Install business modules (clone external, local already in pkgs)
+for mod in pipeline-sdlc showcase evaluate pipeline-service pipeline-task; do
cd "$cdir/pkgs"
- if [ ! -d "$m" ]; then
- git clone https://git.opencomputing.cn/yumoqing/$m || echo "SKIP: $m clone failed"
- fi
- # pipeline-sdlc goes to root (same as other business modules)
- if [ "$m" = "pipeline-sdlc" ] && [ ! -d "$cdir/pipeline-sdlc" ]; then
- ln -sf "$cdir/pkgs/pipeline-sdlc" "$cdir/pipeline-sdlc"
+ if [ ! -d "$mod" ]; then
+ git clone https://git.opencomputing.cn/yumoqing/$mod || echo "SKIP: $mod"
fi
cd "$cdir"
done
-# 5. Install business modules
-for mod in pipeline_core pipeline_ops pipeline_dist pipeline-sdlc; do
- echo "install $mod ..."
- cd "$cdir/$mod"
- "$cdir/py3/bin/pip" install . 2>&1 | tail -1
-
- # Generate DDL from models
- if [ -d models ] && ls models/*.json >/dev/null 2>&1; then
- "$cdir/py3/bin/json2ddl" mysql models/ > "$cdir/$mod/mysql.ddl.sql" 2>/dev/null || echo " DDL generation skipped (json2ddl not available)"
+# 6. pip install all modules from pkgs/
+for mod in pipeline_core pipeline_ops pipeline_dist pipeline-sdlc showcase; do
+ if [ -d "pkgs/$mod" ]; then
+ "$cdir/py3/bin/pip" install "pkgs/$mod/" 2>&1 | tail -1
fi
-
- # Generate CRUD UI from json definitions
- if [ -d json ] && ls json/*.json >/dev/null 2>&1; then
- cd json
- for f in *.json; do
- "$cdir/py3/bin/xls2ui" -m ../models -o ../wwwroot "$mod" "$f" 2>/dev/null || echo " CRUD generation skipped for $f"
- done
- cd ..
- fi
- cd "$cdir"
done
-# 5b. Install showcase module
-if [ -d "$cdir/pkgs/showcase" ]; then
- echo "install showcase ..."
- cd "$cdir/pkgs/showcase"
- "$cdir/py3/bin/pip" install . 2>&1 | tail -1
+# 7. SDLC pipeline-sdlc: regenerate CRUD from json
+if [ -d "pkgs/pipeline-sdlc/json" ]; then
+ cd "pkgs/pipeline-sdlc/json"
+ "$cdir/py3/bin/xls2ui" -m ../models -o ../wwwroot pipeline-sdlc *.json 2>&1 | grep -c 'handle' | xargs -I{} echo " xls2ui: {} tables handled"
cd "$cdir"
fi
-# 6. Create runtime dirs
-mkdir -p "$cdir/logs" "$cdir/files"
+# 8. Create runtime dirs
+mkdir -p "$cdir/logs" "$cdir/files" "$cdir/conf"
+# 6. Fix auto-assign created_by in CRUD dspys (xls2ui overwritten)
+for d in sd_projects sd_iterations; do
+ for t in add update; do
+ f="$cdir/wwwroot/pipeline-sdlc/$d/${t}_${d}.dspy"
+ [ -f "$f" ] && grep -q 'created_by' "$f" || sed -i "s/ns\['org_id'\] = userorgid/ns['org_id'] = userorgid\nns['created_by'] = userorgid/" "$f"
+ done
+done
-chmod +x "$cdir/start.sh" "$cdir/stop.sh"
+
+chmod +x "$cdir/start.sh" "$cdir/stop.sh" 2>/dev/null || true
echo "=== Build complete ==="
diff --git a/pipeline_core/json/llm.json b/pipeline_core/json/llm.json
new file mode 100644
index 0000000..7f22219
--- /dev/null
+++ b/pipeline_core/json/llm.json
@@ -0,0 +1,18 @@
+{
+ "tblname": "llm",
+ "title": "大语言模型管理",
+ "params": {
+ "sortby": "created_at",
+ "browserfields": {
+ "exclouded": ["id", "api_key"],
+ "cwidth": {}
+ },
+ "editexclouded": ["id", "created_at", "updated_at"],
+ "editable": {
+ "new_data_url": "{{entire_url('../api/llm_create.dspy')}}",
+ "update_data_url": "{{entire_url('../api/llm_update.dspy')}}",
+ "delete_data_url": "{{entire_url('../api/llm_delete.dspy')}}"
+ },
+ "confidential_fields": ["api_key"]
+ }
+}
diff --git a/pipeline_core/models/llm.json b/pipeline_core/models/llm.json
new file mode 100644
index 0000000..c4aee72
--- /dev/null
+++ b/pipeline_core/models/llm.json
@@ -0,0 +1,25 @@
+{
+ "summary": [
+ {
+ "name": "llm",
+ "title": "大语言模型表",
+ "primary": ["id"]
+ }
+ ],
+ "fields": [
+ {"name": "id", "title": "主键", "type": "str", "length": 32, "nullable": "no"},
+ {"name": "name", "title": "模型名称", "type": "str", "length": 100, "nullable": "no"},
+ {"name": "provider", "title": "供应商", "type": "str", "length": 50, "nullable": "no"},
+ {"name": "model_id", "title": "模型标识", "type": "str", "length": 100},
+ {"name": "api_base", "title": "API地址", "type": "str", "length": 500},
+ {"name": "api_key", "title": "API密钥", "type": "str", "length": 500},
+ {"name": "max_tokens", "title": "最大Token", "type": "int", "default": "8192"},
+ {"name": "status", "title": "状态", "type": "str", "length": 20, "default": "active"},
+ {"name": "description", "title": "描述", "type": "text"},
+ {"name": "created_at", "title": "创建时间", "type": "timestamp"},
+ {"name": "updated_at", "title": "更新时间", "type": "timestamp"}
+ ],
+ "indexes": [
+ {"name": "idx_llm_status", "idxtype": "index", "idxfields": ["status"]}
+ ]
+}
diff --git a/pipeline_core/models/pipeline_steps.json b/pipeline_core/models/pipeline_steps.json
index 7eb96ea..3763d8a 100644
--- a/pipeline_core/models/pipeline_steps.json
+++ b/pipeline_core/models/pipeline_steps.json
@@ -1,110 +1,122 @@
{
- "summary": [
- {
- "name": "pipeline_steps",
- "title": "产线步骤表",
- "primary": [
- "id"
- ]
- }
- ],
- "fields": [
- {
- "name": "id",
- "title": "id",
- "type": "str",
- "length": 32,
- "nullable": "no"
- },
- {
- "name": "pipeline_id",
- "title": "所属产线",
- "type": "str",
- "length": 32,
- "nullable": "no"
- },
- {
- "name": "step_order",
- "title": "步骤序号",
- "type": "int",
- "nullable": "no"
- },
- {
- "name": "step_name",
- "title": "步骤名称",
- "type": "str",
- "length": 100,
- "nullable": "no"
- },
- {
- "name": "step_type",
- "title": "步骤类型",
- "type": "str",
- "length": 50,
- "nullable": "no"
- },
- {
- "name": "model_name",
- "title": "调用模型名称",
- "type": "str",
- "length": 100
- },
- {
- "name": "step_config",
- "title": "步骤配置JSON",
- "type": "text"
- },
- {
- "name": "input_schema",
- "title": "输入定义JSON",
- "type": "text"
- },
- {
- "name": "output_schema",
- "title": "输出定义JSON",
- "type": "text"
- },
- {
- "name": "timeout_seconds",
- "title": "超时秒数",
- "type": "int",
- "default": "300"
- },
- {
- "name": "retry_count",
- "title": "重试次数",
- "type": "int",
- "default": "0"
- },
- {
- "name": "created_at",
- "title": "创建时间",
- "type": "timestamp"
- }
- ],
- "indexes": [
- {
- "name": "idx_steps_pipeline",
- "idxtype": "index",
- "idxfields": [
- "pipeline_id"
- ]
- },
- {
- "name": "idx_steps_order",
- "idxtype": "unique",
- "idxfields": [
- "pipeline_id",
- "step_order"
- ]
- }
- ],
- "codes": [
- {
- "field": "pipeline_id",
- "table": "pipelines",
- "valuefield": "id",
- "textfield": "name"
- }
- ]
-}
+ "summary": [
+ {
+ "name": "pipeline_steps",
+ "title": "产线步骤表",
+ "primary": [
+ "id"
+ ]
+ }
+ ],
+ "fields": [
+ {
+ "name": "id",
+ "title": "id",
+ "type": "str",
+ "length": 32,
+ "nullable": "no",
+ "uitype": "hidden"
+ },
+ {
+ "name": "pipeline_id",
+ "title": "所属产线",
+ "type": "str",
+ "length": 32,
+ "nullable": "no",
+ "uitype": "select"
+ },
+ {
+ "name": "step_order",
+ "title": "步骤序号",
+ "type": "int",
+ "nullable": "no",
+ "uitype": "number"
+ },
+ {
+ "name": "step_name",
+ "title": "步骤名称",
+ "type": "str",
+ "length": 100,
+ "nullable": "no",
+ "uitype": "text"
+ },
+ {
+ "name": "step_type",
+ "title": "步骤类型",
+ "type": "str",
+ "length": 50,
+ "nullable": "no",
+ "uitype": "select"
+ },
+ {
+ "name": "model_name",
+ "title": "调用模型名称",
+ "type": "str",
+ "length": 100,
+ "uitype": "text"
+ },
+ {
+ "name": "step_config",
+ "title": "步骤配置JSON",
+ "type": "text",
+ "uitype": "textarea"
+ },
+ {
+ "name": "input_schema",
+ "title": "输入定义JSON",
+ "type": "text",
+ "uitype": "textarea"
+ },
+ {
+ "name": "output_schema",
+ "title": "输出定义JSON",
+ "type": "text",
+ "uitype": "textarea"
+ },
+ {
+ "name": "timeout_seconds",
+ "title": "超时秒数",
+ "type": "int",
+ "default": "300",
+ "uitype": "number"
+ },
+ {
+ "name": "retry_count",
+ "title": "重试次数",
+ "type": "int",
+ "default": "0",
+ "uitype": "number"
+ },
+ {
+ "name": "created_at",
+ "title": "创建时间",
+ "type": "timestamp",
+ "uitype": "timestamp"
+ }
+ ],
+ "indexes": [
+ {
+ "name": "idx_steps_pipeline",
+ "idxtype": "index",
+ "idxfields": [
+ "pipeline_id"
+ ]
+ },
+ {
+ "name": "idx_steps_order",
+ "idxtype": "unique",
+ "idxfields": [
+ "pipeline_id",
+ "step_order"
+ ]
+ }
+ ],
+ "codes": [
+ {
+ "field": "pipeline_id",
+ "table": "pipelines",
+ "valuefield": "id",
+ "textfield": "name"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/pipeline_core/models/pipeline_versions.json b/pipeline_core/models/pipeline_versions.json
index d524263..a1ed0cc 100644
--- a/pipeline_core/models/pipeline_versions.json
+++ b/pipeline_core/models/pipeline_versions.json
@@ -1,85 +1,94 @@
{
- "summary": [
- {
- "name": "pipeline_versions",
- "title": "产线发布记录表",
- "primary": [
- "id"
- ]
- }
- ],
- "fields": [
- {
- "name": "id",
- "title": "id",
- "type": "str",
- "length": 32,
- "nullable": "no"
- },
- {
- "name": "pipeline_id",
- "title": "产线ID",
- "type": "str",
- "length": 32,
- "nullable": "no"
- },
- {
- "name": "version",
- "title": "版本号",
- "type": "str",
- "length": 20,
- "nullable": "no"
- },
- {
- "name": "publish_status",
- "title": "发布状态",
- "type": "str",
- "length": 20,
- "nullable": "no",
- "default": "pending"
- },
- {
- "name": "published_by",
- "title": "发布人",
- "type": "str",
- "length": 32
- },
- {
- "name": "published_at",
- "title": "发布时间",
- "type": "timestamp"
- },
- {
- "name": "changelog",
- "title": "变更说明",
- "type": "text"
- },
- {
- "name": "config_snapshot",
- "title": "配置快照JSON",
- "type": "text"
- },
- {
- "name": "created_at",
- "title": "创建时间",
- "type": "timestamp"
- }
- ],
- "indexes": [
- {
- "name": "idx_versions_pipeline",
- "idxtype": "index",
- "idxfields": [
- "pipeline_id"
- ]
- }
- ],
- "codes": [
- {
- "field": "pipeline_id",
- "table": "pipelines",
- "valuefield": "id",
- "textfield": "name"
- }
- ]
-}
+ "summary": [
+ {
+ "name": "pipeline_versions",
+ "title": "产线发布记录表",
+ "primary": [
+ "id"
+ ]
+ }
+ ],
+ "fields": [
+ {
+ "name": "id",
+ "title": "id",
+ "type": "str",
+ "length": 32,
+ "nullable": "no",
+ "uitype": "hidden"
+ },
+ {
+ "name": "pipeline_id",
+ "title": "产线ID",
+ "type": "str",
+ "length": 32,
+ "nullable": "no",
+ "uitype": "hidden"
+ },
+ {
+ "name": "version",
+ "title": "版本号",
+ "type": "str",
+ "length": 20,
+ "nullable": "no",
+ "uitype": "text"
+ },
+ {
+ "name": "publish_status",
+ "title": "发布状态",
+ "type": "str",
+ "length": 20,
+ "nullable": "no",
+ "default": "pending",
+ "uitype": "select"
+ },
+ {
+ "name": "published_by",
+ "title": "发布人",
+ "type": "str",
+ "length": 32,
+ "uitype": "text"
+ },
+ {
+ "name": "published_at",
+ "title": "发布时间",
+ "type": "timestamp",
+ "uitype": "timestamp"
+ },
+ {
+ "name": "changelog",
+ "title": "变更说明",
+ "type": "text",
+ "uitype": "textarea"
+ },
+ {
+ "name": "config_snapshot",
+ "title": "配置快照JSON",
+ "type": "text",
+ "uitype": "textarea"
+ },
+ {
+ "name": "created_at",
+ "title": "创建时间",
+ "type": "timestamp",
+ "uitype": "timestamp"
+ }
+ ],
+ "indexes": [
+ {
+ "name": "idx_versions_pipeline",
+ "idxtype": "index",
+ "idxfields": [
+ "pipeline_id"
+ ]
+ }
+ ],
+ "codes": [
+ {
+ "field": "pipeline_id",
+ "table": "pipelines",
+ "valuefield": "id",
+ "textfield": "name"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/pipeline_core/models/pipelines.json b/pipeline_core/models/pipelines.json
index 8c930eb..893f0a7 100644
--- a/pipeline_core/models/pipelines.json
+++ b/pipeline_core/models/pipelines.json
@@ -1,126 +1,139 @@
{
- "summary": [
- {
- "name": "pipelines",
- "title": "产线定义表",
- "primary": [
- "id"
- ]
- }
- ],
- "fields": [
- {
- "name": "id",
- "title": "id",
- "type": "str",
- "length": 32,
- "nullable": "no"
- },
- {
- "name": "name",
- "title": "产线名称",
- "type": "str",
- "length": 200,
- "nullable": "no"
- },
- {
- "name": "description",
- "title": "产线描述",
- "type": "text"
- },
- {
- "name": "pipeline_type",
- "title": "产线类型",
- "type": "str",
- "length": 50,
- "nullable": "no"
- },
- {
- "name": "version",
- "title": "当前版本",
- "type": "str",
- "length": 20,
- "default": "1.0.0"
- },
- {
- "name": "status",
- "title": "状态",
- "type": "str",
- "length": 20,
- "nullable": "no",
- "default": "draft"
- },
- {
- "name": "pipeline_config",
- "title": "产线配置JSON",
- "type": "text"
- },
- {
- "name": "model_api_url",
- "title": "模型API地址",
- "type": "str",
- "length": 500
- },
- {
- "name": "model_api_key",
- "title": "模型API密钥",
- "type": "str",
- "length": 500
- },
- {
- "name": "org_id",
- "title": "所属机构ID",
- "type": "str",
- "length": 32,
- "default": "0"
- },
- {
- "name": "created_by",
- "title": "创建人",
- "type": "str",
- "length": 32
- },
- {
- "name": "created_at",
- "title": "创建时间",
- "type": "timestamp"
- },
- {
- "name": "updated_at",
- "title": "更新时间",
- "type": "timestamp"
- }
- ],
- "indexes": [
- {
- "name": "idx_pipelines_name",
- "idxtype": "index",
- "idxfields": [
- "name"
- ]
- },
- {
- "name": "idx_pipelines_status",
- "idxtype": "index",
- "idxfields": [
- "status"
- ]
- }
- ],
- "codes": [
- {
- "field": "pipeline_type",
- "table": "appcodes_kv",
- "valuefield": "k",
- "textfield": "v",
- "cond": "parentid='pipeline_type'"
- },
- {
- "field": "status",
- "table": "appcodes_kv",
- "valuefield": "k",
- "textfield": "v",
- "cond": "parentid='pipeline_status'"
- }
- ]
+ "summary": [
+ {
+ "name": "pipelines",
+ "title": "产线定义表",
+ "primary": [
+ "id"
+ ]
+ }
+ ],
+ "fields": [
+ {
+ "name": "id",
+ "title": "id",
+ "type": "str",
+ "length": 32,
+ "nullable": "no",
+ "uitype": "hidden"
+ },
+ {
+ "name": "name",
+ "title": "产线名称",
+ "type": "str",
+ "length": 200,
+ "nullable": "no",
+ "uitype": "text"
+ },
+ {
+ "name": "description",
+ "title": "产线描述",
+ "type": "text",
+ "uitype": "textarea"
+ },
+ {
+ "name": "pipeline_type",
+ "title": "产线类型",
+ "type": "str",
+ "length": 50,
+ "nullable": "no",
+ "uitype": "select"
+ },
+ {
+ "name": "version",
+ "title": "当前版本",
+ "type": "str",
+ "length": 20,
+ "default": "1.0.0",
+ "uitype": "text"
+ },
+ {
+ "name": "status",
+ "title": "状态",
+ "type": "str",
+ "length": 20,
+ "nullable": "no",
+ "default": "draft",
+ "uitype": "select"
+ },
+ {
+ "name": "pipeline_config",
+ "title": "产线配置JSON",
+ "type": "text",
+ "uitype": "textarea"
+ },
+ {
+ "name": "model_api_url",
+ "title": "模型API地址",
+ "type": "str",
+ "length": 500,
+ "uitype": "text"
+ },
+ {
+ "name": "model_api_key",
+ "title": "模型API密钥",
+ "type": "str",
+ "length": 500,
+ "uitype": "text"
+ },
+ {
+ "name": "org_id",
+ "title": "所属机构ID",
+ "type": "str",
+ "length": 32,
+ "default": "0",
+ "uitype": "text"
+ },
+ {
+ "name": "created_by",
+ "title": "创建人",
+ "type": "str",
+ "length": 32,
+ "uitype": "hidden"
+ },
+ {
+ "name": "created_at",
+ "title": "创建时间",
+ "type": "timestamp",
+ "uitype": "timestamp"
+ },
+ {
+ "name": "updated_at",
+ "title": "更新时间",
+ "type": "timestamp",
+ "uitype": "timestamp"
+ }
+ ],
+ "indexes": [
+ {
+ "name": "idx_pipelines_name",
+ "idxtype": "index",
+ "idxfields": [
+ "name"
+ ]
+ },
+ {
+ "name": "idx_pipelines_status",
+ "idxtype": "index",
+ "idxfields": [
+ "status"
+ ]
+ }
+ ],
+ "codes": [
+ {
+ "field": "pipeline_type",
+ "table": "appcodes_kv",
+ "valuefield": "k",
+ "textfield": "v",
+ "cond": "parentid='pipeline_type'"
+ },
+ {
+ "field": "status",
+ "table": "appcodes_kv",
+ "valuefield": "k",
+ "textfield": "v",
+ "cond": "parentid='pipeline_status'"
+ }
+ ]
}
\ No newline at end of file
diff --git a/pipeline_core/pipeline_core/init.py b/pipeline_core/pipeline_core/init.py
index f9846f8..74c9550 100644
--- a/pipeline_core/pipeline_core/init.py
+++ b/pipeline_core/pipeline_core/init.py
@@ -249,6 +249,54 @@ async def publish_pipeline(params_kw):
return json.dumps(result, ensure_ascii=False, default=str)
+async def create_llm(params_kw):
+ result = {'success': False, 'message': ''}
+ try:
+ db, dbname = _get_sor()
+ async with db.sqlorContext(dbname) as sor:
+ data = params_kw.copy()
+ data.pop('page', None)
+ data.pop('rows', None)
+ data.pop('data_filter', None)
+ data['id'] = getID()
+ await sor.C('llm', data)
+ result['success'] = True
+ result['message'] = '创建成功'
+ except Exception as e:
+ result['message'] = str(e)
+ return json.dumps(result, ensure_ascii=False, default=str)
+
+
+async def update_llm(params_kw):
+ result = {'success': False, 'message': ''}
+ try:
+ db, dbname = _get_sor()
+ async with db.sqlorContext(dbname) as sor:
+ data = params_kw.copy()
+ data.pop('page', None)
+ data.pop('rows', None)
+ data.pop('data_filter', None)
+ await sor.U('llm', data)
+ result['success'] = True
+ result['message'] = '更新成功'
+ except Exception as e:
+ result['message'] = str(e)
+ return json.dumps(result, ensure_ascii=False, default=str)
+
+
+async def delete_llm(params_kw):
+ result = {'success': False, 'message': ''}
+ try:
+ db, dbname = _get_sor()
+ async with db.sqlorContext(dbname) as sor:
+ await sor.D('llm', {'id': params_kw.get('id')})
+ result['success'] = True
+ result['message'] = '删除成功'
+ except Exception as e:
+ result['message'] = str(e)
+ return json.dumps(result, ensure_ascii=False, default=str)
+
+
def load_pipeline_core():
"""注册函数到 ServerEnv"""
env = ServerEnv()
@@ -259,6 +307,10 @@ def load_pipeline_core():
env.update_pipelines = update_pipeline
env.delete_pipeline = delete_pipeline
env.delete_pipelines = delete_pipeline
+ # LLM
+ env.create_llm = create_llm
+ env.update_llm = update_llm
+ env.delete_llm = delete_llm
# Pipeline Steps
env.create_pipeline_step = create_pipeline_step
env.create_pipeline_steps = create_pipeline_step
diff --git a/pipeline_core/scripts/load_path.py b/pipeline_core/scripts/load_path.py
index 72aa88c..d2c4241 100644
--- a/pipeline_core/scripts/load_path.py
+++ b/pipeline_core/scripts/load_path.py
@@ -1,88 +1,69 @@
#!/usr/bin/env python3
-"""
-pipeline_core 模块 RBAC 权限管理脚本
-
-使用方法:
- cd ~/repos/sage
- ./py3/bin/python ~/test/pipeline-app/pipeline_core/scripts/load_path.py
-"""
-
-import subprocess
-import os
-import sys
-
-
-def find_sage_root():
- candidates = [
- os.path.expanduser("~/repos/sage"),
- os.path.expanduser("~/sage"),
- ]
- for c in candidates:
- if os.path.isdir(os.path.join(c, "py3")) and os.path.isdir(os.path.join(c, "wwwroot")):
- return c
- return None
-
-
-SAGE_ROOT = find_sage_root()
-if not SAGE_ROOT:
- print("ERROR: Cannot find Sage root directory")
- sys.exit(1)
-
-PYTHON = os.path.join(SAGE_ROOT, "py3", "bin", "python")
-SET_PERM_SCRIPT = os.path.join(SAGE_ROOT, "set_role_perm.py")
+"""RBAC path registration for pipeline_core module."""
+import os, sys, subprocess
MOD = "pipeline_core"
-# ============================================================
-# 权限路径定义
-# ============================================================
-
-# operator — 产线管理人员
-PATHS_OPERATOR = [
- f"/{MOD}",
+PATHS_ANY = [
f"/{MOD}/index.ui",
- f"/{MOD}/pipelines/",
- f"/{MOD}/pipeline_steps/",
- f"/{MOD}/pipeline_versions/",
- f"/{MOD}/api/pipelines_create.dspy",
- f"/{MOD}/api/pipelines_update.dspy",
- f"/{MOD}/api/pipelines_delete.dspy",
+]
+
+PATHS_LOGINED = [
+ f"/{MOD}",
+ # Pipelines CRUD
+ f"/{MOD}/pipelines/index.ui",
+ f"/{MOD}/pipelines/get_pipelines.dspy",
+ f"/{MOD}/pipelines/add_pipelines.dspy",
+ f"/{MOD}/pipelines/update_pipelines.dspy",
+ f"/{MOD}/pipelines/delete_pipelines.dspy",
+ # Pipeline Steps CRUD
+ f"/{MOD}/pipeline_steps/index.ui",
+ f"/{MOD}/pipeline_steps/get_pipeline_steps.dspy",
+ f"/{MOD}/pipeline_steps/add_pipeline_steps.dspy",
+ f"/{MOD}/pipeline_steps/update_pipeline_steps.dspy",
+ f"/{MOD}/pipeline_steps/delete_pipeline_steps.dspy",
+ # Pipeline Versions
+ f"/{MOD}/pipeline_versions/index.ui",
+ f"/{MOD}/pipeline_versions/get_pipeline_versions.dspy",
+ f"/{MOD}/pipeline_versions/add_pipeline_versions.dspy",
+ f"/{MOD}/pipeline_versions/update_pipeline_versions.dspy",
+ f"/{MOD}/pipeline_versions/delete_pipeline_versions.dspy",
+ # Pipeline Editor
+ f"/{MOD}/pipeline_editor/index.ui",
+ f"/{MOD}/pipeline_editor/save_steps.dspy",
+ # Data sources
+ f"/{MOD}/pipeline_steps/get_pipelines.dspy",
+ f"/{MOD}/pipeline_steps/get_step_types.dspy",
+ # API
+ f"/{MOD}/api/pipeline_publish.dspy",
f"/{MOD}/api/pipeline_steps_create.dspy",
f"/{MOD}/api/pipeline_steps_update.dspy",
f"/{MOD}/api/pipeline_steps_delete.dspy",
f"/{MOD}/api/pipeline_versions_create.dspy",
f"/{MOD}/api/pipeline_versions_update.dspy",
f"/{MOD}/api/pipeline_versions_delete.dspy",
- f"/{MOD}/api/pipeline_publish.dspy",
+ f"/{MOD}/api/pipelines_create.dspy",
+ f"/{MOD}/api/pipelines_update.dspy",
+ f"/{MOD}/api/pipelines_delete.dspy",
]
-# developer — 开发者(包含所有 operator 路径)
-PATHS_DEVELOPER = PATHS_OPERATOR[:]
-
-
-def run_set_perm(role, path):
- cmd = [PYTHON, SET_PERM_SCRIPT, role, path]
- result = subprocess.run(cmd, capture_output=True, text=True)
- return result.returncode == 0
-
-
-def register_role_paths(role, paths):
- count = 0
- for p in paths:
- if run_set_perm(role, p):
- count += 1
- print(f" {role}: {count}/{len(paths)} paths registered")
- return count
-
def main():
- print(f"Sage root: {SAGE_ROOT}")
- print("Registering pipeline_core module RBAC permissions...")
- total = 0
- total += register_role_paths("operator", PATHS_OPERATOR)
- total += register_role_paths("developer", PATHS_DEVELOPER)
- print(f"\nDone. Total {total} permission entries registered.")
- print("NOTE: Restart Sage after permission changes to reload RBAC cache.")
+ root = os.path.expanduser("~/pipeline")
+ if not os.path.isfile(os.path.join(root, "set_role_perm.py")):
+ print("ERROR: pipeline root not found")
+ sys.exit(1)
+ set_perm = os.path.join(root, "set_role_perm.py")
+ py = sys.executable
+ count = 0
+ for role, paths in [("any", PATHS_ANY), ("logined", PATHS_LOGINED)]:
+ for path in paths:
+ r = subprocess.run([py, set_perm, role, path], capture_output=True, text=True, cwd=root)
+ if r.returncode == 0:
+ count += 1
+ else:
+ print(f" WARN: {path} -> {r.stderr.strip()[-80:]}")
+ print(f"Registered {count}/{len(PATHS_ANY)+len(PATHS_LOGINED)} paths for {MOD}")
if __name__ == "__main__":
diff --git a/pipeline_core/wwwroot/index.ui b/pipeline_core/wwwroot/index.ui
index c55127a..7a49d8c 100644
--- a/pipeline_core/wwwroot/index.ui
+++ b/pipeline_core/wwwroot/index.ui
@@ -1,68 +1,254 @@
{
- "widgettype": "VBox",
- "options": {"width": "100%", "height": "100%", "padding": "0"},
- "subwidgets": [
+ "widgettype": "VBox",
+ "options": {
+ "width": "100%",
+ "height": "100%",
+ "padding": "0"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "HBox",
+ "options": {
+ "width": "100%",
+ "alignItems": "center",
+ "padding": "24px 24px 12px 24px",
+ "cheight": 6
+ },
+ "subwidgets": [
{
- "widgettype": "HBox",
- "options": {"width": "100%", "alignItems": "center", "marginBottom": "20px"},
- "subwidgets": [
- {"widgettype": "Title2", "options": {"text": "产线管理"}},
- {"widgettype": "Filler"},
- {"widgettype": "Text", "options": {"text": "产线定义、步骤配置与发布管理", "cfontsize": 1.2}}
- ]
+ "widgettype": "Title2",
+ "options": {
+ "text": "产线管理"
+ }
},
{
- "widgettype": "VBox",
- "options": {"css": "filler", "spacing": 16},
- "subwidgets": [
+ "widgettype": "Filler"
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "产线定义、步骤配置与发布管理",
+ "cfontsize": 1.2
+ }
+ }
+ ]
+ },
+ {
+ "widgettype": "HBox",
+ "options": {
+ "width": "100%",
+ "padding": "12px 24px 12px 24px",
+ "cheight": 18
+ },
+ "subwidgets": [
+ {
+ "widgettype": "ResponsableBox",
+ "options": {
+ "gap": "24px",
+ "minWidth": "260px"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "EDITOR CARD HERE"
+ }
+ },
+ {
+ "widgettype": "VBox",
+ "options": {
+ "css": "card",
+ "cwidth": 23,
+ "cheight": 12,
+ "padding": "24px",
+ "cursor": "pointer",
+ "bgcolor": "#fff"
+ },
+ "binds": [
{
- "widgettype": "ResponsableBox",
- "options": {"gap": "24px", "minWidth": "260px"},
- "subwidgets": [
- {
- "widgettype": "VBox",
- "options": {"css": "card", "cwidth": 23, "cheight": 12, "padding": "24px", "cursor": "pointer", "bgcolor": "#fff"},
- "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.pipeline_core_content", "options": {"url": "{{entire_url('pipelines/')}}"}, "mode": "replace"}],
- "subwidgets": [
- {"widgettype": "VBox", "options": {"width": "44px", "height": "44px", "bgcolor": "#EFF6FF", "borderRadius": "12px", "alignItems": "center", "justifyContent": "center", "marginBottom": "12px"}, "subwidgets": [
- {"widgettype": "Svg", "options": {"svg": "", "width": "24px", "height": "24px"}}
- ]},
- {"widgettype": "Title3", "options": {"text": "产线定义", "marginBottom": "4px"}},
- {"widgettype": "Text", "options": {"text": "管理产线基本信息与配置", "cfontsize": 1.2}}
- ]
- },
- {
- "widgettype": "VBox",
- "options": {"css": "card", "cwidth": 23, "cheight": 12, "padding": "24px", "cursor": "pointer", "bgcolor": "#fff"},
- "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.pipeline_core_content", "options": {"url": "{{entire_url('pipeline_steps/')}}"}, "mode": "replace"}],
- "subwidgets": [
- {"widgettype": "VBox", "options": {"width": "44px", "height": "44px", "bgcolor": "#F0FDF4", "borderRadius": "12px", "alignItems": "center", "justifyContent": "center", "marginBottom": "12px"}, "subwidgets": [
- {"widgettype": "Svg", "options": {"svg": "", "width": "24px", "height": "24px"}}
- ]},
- {"widgettype": "Title3", "options": {"text": "产线步骤", "marginBottom": "4px"}},
- {"widgettype": "Text", "options": {"text": "配置产线执行步骤与参数", "cfontsize": 1.2}}
- ]
- },
- {
- "widgettype": "VBox",
- "options": {"css": "card", "cwidth": 23, "cheight": 12, "padding": "24px", "cursor": "pointer", "bgcolor": "#fff"},
- "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.pipeline_core_content", "options": {"url": "{{entire_url('pipeline_versions/')}}"}, "mode": "replace"}],
- "subwidgets": [
- {"widgettype": "VBox", "options": {"width": "44px", "height": "44px", "bgcolor": "#FFFBEB", "borderRadius": "12px", "alignItems": "center", "justifyContent": "center", "marginBottom": "12px"}, "subwidgets": [
- {"widgettype": "Svg", "options": {"svg": "", "width": "24px", "height": "24px"}}
- ]},
- {"widgettype": "Title3", "options": {"text": "发布记录", "marginBottom": "4px"}},
- {"widgettype": "Text", "options": {"text": "查看产线版本发布历史", "cfontsize": 1.2}}
- ]
- }
- ]
+ "wid": "self",
+ "event": "click",
+ "actiontype": "urlwidget",
+ "target": "app.main_content",
+ "options": {
+ "url": "{{entire_url('/pipeline_core/pipelines/')}}"
+ },
+ "mode": "replace"
+ }
+ ],
+ "subwidgets": [
+ {
+ "widgettype": "VBox",
+ "options": {
+ "width": "44px",
+ "height": "44px",
+ "bgcolor": "#EFF6FF",
+ "borderRadius": "12px",
+ "alignItems": "center",
+ "justifyContent": "center",
+ "marginBottom": "12px"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "Svg",
+ "options": {
+ "svg": "",
+ "width": "24px",
+ "height": "24px"
+ }
+ }
+ ]
},
{
- "widgettype": "VScrollPanel",
- "id": "pipeline_core_content",
- "options": {"css": "filler", "width": "100%", "height": "100%"}
+ "widgettype": "Title3",
+ "options": {
+ "text": "产线定义",
+ "marginBottom": "4px"
+ }
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "管理产线基本信息与配置",
+ "cfontsize": 1.2
+ }
}
- ]
+ ]
+ },
+ {
+ "widgettype": "VBox",
+ "options": {
+ "css": "card",
+ "cwidth": 23,
+ "cheight": 12,
+ "padding": "24px",
+ "cursor": "pointer",
+ "bgcolor": "#fff"
+ },
+ "binds": [
+ {
+ "wid": "self",
+ "event": "click",
+ "actiontype": "urlwidget",
+ "target": "app.main_content",
+ "options": {
+ "url": "{{entire_url('/pipeline_core/pipeline_steps/')}}"
+ },
+ "mode": "replace"
+ }
+ ],
+ "subwidgets": [
+ {
+ "widgettype": "VBox",
+ "options": {
+ "width": "44px",
+ "height": "44px",
+ "bgcolor": "#F0FDF4",
+ "borderRadius": "12px",
+ "alignItems": "center",
+ "justifyContent": "center",
+ "marginBottom": "12px"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "Svg",
+ "options": {
+ "svg": "",
+ "width": "24px",
+ "height": "24px"
+ }
+ }
+ ]
+ },
+ {
+ "widgettype": "Title3",
+ "options": {
+ "text": "产线步骤",
+ "marginBottom": "4px"
+ }
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "配置产线执行步骤与参数",
+ "cfontsize": 1.2
+ }
+ }
+ ]
+ },
+ {
+ "widgettype": "VBox",
+ "options": {
+ "css": "card",
+ "cwidth": 23,
+ "cheight": 12,
+ "padding": "24px",
+ "cursor": "pointer",
+ "bgcolor": "#fff"
+ },
+ "binds": [
+ {
+ "wid": "self",
+ "event": "click",
+ "actiontype": "urlwidget",
+ "target": "app.main_content",
+ "options": {
+ "url": "{{entire_url('/pipeline_core/pipeline_versions/')}}"
+ },
+ "mode": "replace"
+ }
+ ],
+ "subwidgets": [
+ {
+ "widgettype": "VBox",
+ "options": {
+ "width": "44px",
+ "height": "44px",
+ "bgcolor": "#FFFBEB",
+ "borderRadius": "12px",
+ "alignItems": "center",
+ "justifyContent": "center",
+ "marginBottom": "12px"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "Svg",
+ "options": {
+ "svg": "",
+ "width": "24px",
+ "height": "24px"
+ }
+ }
+ ]
+ },
+ {
+ "widgettype": "Title3",
+ "options": {
+ "text": "发布记录",
+ "marginBottom": "4px"
+ }
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "查看产线版本发布历史",
+ "cfontsize": 1.2
+ }
+ }
+ ]
+ }
+ ]
}
- ]
-}
+ ]
+ },
+ {
+ "widgettype": "VScrollPanel",
+ "id": "pipeline_core_content",
+ "options": {
+ "css": "filler",
+ "width": "100%",
+ "height": "100%"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/pipeline_core/wwwroot/pipeline_editor/index.ui b/pipeline_core/wwwroot/pipeline_editor/index.ui
new file mode 100644
index 0000000..6812015
--- /dev/null
+++ b/pipeline_core/wwwroot/pipeline_editor/index.ui
@@ -0,0 +1,85 @@
+{
+ "widgettype": "VBox",
+ "options": {"width": "100%", "height": "100%", "padding": "24px", "gap": "20px"},
+ "subwidgets": [
+ {
+ "widgettype": "HBox",
+ "options": {"alignItems": "center", "gap": "16px", "padding": "0 0 16px 0", "borderBottom": "1px solid #334155"},
+ "subwidgets": [
+ {"widgettype": "Title2", "options": {"text": "产线编排画布"}},
+ {"widgettype": "Filler"},
+ {
+ "widgettype": "Button",
+ "options": {"label": "发布版本", "bgcolor": "#6366f1", "color": "#fff"},
+ "binds": [{"wid": "self", "event": "click", "actiontype": "method", "target": "app.pipeline_editor", "method": "publish"}]
+ }
+ ]
+ },
+ {
+ "widgettype": "HBox",
+ "options": {"gap": "12px", "alignItems": "center"},
+ "subwidgets": [
+ {"widgettype": "Text", "options": {"text": "选择产线:", "cfontsize": 1, "color": "#e2e8f0"}},
+ {
+ "widgettype": "DataSelector",
+ "id": "ds_pipelines",
+ "options": {
+ "data_url": "{{entire_url('/pipeline_core/pipeline_steps/get_pipelines.dspy')}}",
+ "valueField": "id",
+ "textField": "name",
+ "width": "300px",
+ "placeholder": "选择要编辑的产线..."
+ },
+ "binds": [{"wid": "self", "event": "change", "actiontype": "method", "target": "app.pipeline_editor", "method": "load_steps"}]
+ },
+ {
+ "widgettype": "DataSelector",
+ "id": "ds_step_types",
+ "options": {
+ "data_url": "{{entire_url('/pipeline_core/pipeline_steps/get_step_types.dspy')}}",
+ "valueField": "step_type",
+ "textField": "display_name",
+ "width": "240px",
+ "placeholder": "添加步骤类型..."
+ }
+ },
+ {
+ "widgettype": "Button",
+ "options": {"label": "+ 添加步骤", "bgcolor": "#4ade80", "color": "#000"},
+ "binds": [{"wid": "self", "event": "click", "actiontype": "method", "target": "app.pipeline_editor", "method": "add_step"}]
+ }
+ ]
+ },
+ {
+ "widgettype": "DataViewer",
+ "id": "dv_steps",
+ "options": {
+ "data_url": "",
+ "width": "100%",
+ "columns": ["id", "step_order", "step_name", "step_type"]
+ }
+ },
+ {
+ "widgettype": "VBox",
+ "id": "steps_canvas",
+ "options": {"width": "100%", "gap": "8px", "padding": "0", "minHeight": "300px"},
+ "subwidgets": [
+ {
+ "widgettype": "Text",
+ "options": {"text": "选择产线后,步骤将在此显示。可以拖拽排序、点击编辑。", "cfontsize": 1, "color": "#64748b", "padding": "40px", "alignItems": "center"}
+ }
+ ]
+ },
+ {
+ "widgettype": "HBox",
+ "options": {"gap": "12px", "justifyContent": "flex-end", "padding": "16px 0 0 0", "borderTop": "1px solid #334155"},
+ "subwidgets": [
+ {
+ "widgettype": "Button",
+ "options": {"label": "保存排序", "bgcolor": "#3b82f6", "color": "#fff"},
+ "binds": [{"wid": "self", "event": "click", "actiontype": "method", "target": "app.pipeline_editor", "method": "save_order"}]
+ }
+ ]
+ }
+ ]
+}
diff --git a/pipeline_core/wwwroot/pipeline_editor/save_steps.dspy b/pipeline_core/wwwroot/pipeline_editor/save_steps.dspy
new file mode 100644
index 0000000..c0cb516
--- /dev/null
+++ b/pipeline_core/wwwroot/pipeline_editor/save_steps.dspy
@@ -0,0 +1,47 @@
+"""
+Pipeline Editor: Save step order after reordering
+POST params: pipeline_id, steps (JSON array of {id, step_order})
+"""
+from sqlor.dbpools import DBPools
+
+async def main():
+ userid = await get_user()
+ if not userid:
+ return {'error': 'unauthorized'}
+
+ pipeline_id = params_kw.pipeline_id
+ steps = params_kw.steps # JSON array
+
+ if not pipeline_id or not steps:
+ return {'error': 'missing pipeline_id or steps'}
+
+ if isinstance(steps, str):
+ import json
+ steps = json.loads(steps)
+
+ dbname = get_module_dbname('pipeline_core')
+ async with DBPools().sqlorContext(dbname) as sor:
+ for i, step in enumerate(steps):
+ await sor.U('pipeline_steps', {
+ 'id': step['id'],
+ 'step_order': i + 1,
+ 'step_name': step.get('step_name', ''),
+ 'step_type': step.get('step_type', ''),
+ 'step_config': step.get('step_config', '{}'),
+ 'timeout_seconds': step.get('timeout_seconds', 300),
+ 'retry_count': step.get('retry_count', 0)
+ })
+
+ # Create version record
+ from appPublic.uniqueID import getID
+ vid = getID()
+ await sor.C('pipeline_versions', {
+ 'id': vid,
+ 'pipeline_id': pipeline_id,
+ 'version': f"v{len(steps)}.0",
+ 'publish_status': 'draft',
+ 'changelog': 'Editor save',
+ 'config_snapshot': json.dumps(steps, ensure_ascii=False) if 'json' in dir() else str(steps)
+ })
+
+ return {'success': True, 'version_id': vid, 'step_count': len(steps)}
diff --git a/pipeline_core/wwwroot/pipeline_steps/get_pipeline_steps.dspy b/pipeline_core/wwwroot/pipeline_steps/get_pipeline_steps.dspy
index 48c6209..5a6875b 100644
--- a/pipeline_core/wwwroot/pipeline_steps/get_pipeline_steps.dspy
+++ b/pipeline_core/wwwroot/pipeline_steps/get_pipeline_steps.dspy
@@ -1,130 +1,14 @@
-ns = params_kw.copy()
+from sqlor.dbpools import DBPools
-
-debug(f'get_pipeline_steps.dspy:{ns=}')
-if not ns.get('page'):
- ns['page'] = 1
-if not ns.get('sort'):
-
-
- ns['sort'] = 'step_order'
-
-
-
-sql = '''select a.*, b.pipeline_id_text
-from (select * from pipeline_steps where 1=1 [[filterstr]]) a left join (select id as pipeline_id,
- name as pipeline_id_text from pipelines where 1 = 1) b on a.pipeline_id = b.pipeline_id'''
-
-filterjson = params_kw.get('data_filter')
-if filterjson and isinstance(filterjson, str):
- try:
- filterjson = json.loads(filterjson)
- except (json.JSONDecodeError, TypeError):
- filterjson = None
-fields_str=r'''[
- {
- "name": "id",
- "title": "id",
- "type": "str",
- "length": 32,
- "nullable": "no"
- },
- {
- "name": "pipeline_id",
- "title": "所属产线",
- "type": "str",
- "length": 32,
- "nullable": "no"
- },
- {
- "name": "step_order",
- "title": "步骤序号",
- "type": "int",
- "nullable": "no"
- },
- {
- "name": "step_name",
- "title": "步骤名称",
- "type": "str",
- "length": 100,
- "nullable": "no"
- },
- {
- "name": "step_type",
- "title": "步骤类型",
- "type": "str",
- "length": 50,
- "nullable": "no"
- },
- {
- "name": "model_name",
- "title": "调用模型名称",
- "type": "str",
- "length": 100
- },
- {
- "name": "step_config",
- "title": "步骤配置JSON",
- "type": "text"
- },
- {
- "name": "input_schema",
- "title": "输入定义JSON",
- "type": "text"
- },
- {
- "name": "output_schema",
- "title": "输出定义JSON",
- "type": "text"
- },
- {
- "name": "timeout_seconds",
- "title": "超时秒数",
- "type": "int",
- "default": "300"
- },
- {
- "name": "retry_count",
- "title": "重试次数",
- "type": "int",
- "default": "0"
- },
- {
- "name": "created_at",
- "title": "创建时间",
- "type": "timestamp"
- }
-]'''
-ori_fields = json.loads(fields_str)
-if not filterjson:
- fields = [ f['name'] for f in ori_fields ]
- filterjson = default_filterjson(fields, ns)
-
-filterdic = ns.copy()
-filterdic['filterstr'] = ''
-filterdic['userorgid'] = '${userorgid}$'
-filterdic['userid'] = '${userid}$'
-if filterjson:
- dbf = DBFilter(filterjson)
- conds = dbf.gen(ns)
- if conds:
- ns.update(dbf.consts)
- conds = f' and {conds}'
- filterdic['filterstr'] = conds
-ac = ArgsConvert('[[', ']]')
-vars = ac.findAllVariables(sql)
-NameSpace = {v:'${' + v + '}$' for v in vars if v != 'filterstr' }
-filterdic.update(NameSpace)
-sql = ac.convert(sql, filterdic)
-
-debug(f'{sql=}')
-db = DBPools()
-dbname = get_module_dbname('pipeline_core')
-async with db.sqlorContext(dbname) as sor:
- r = await sor.sqlPaging(sql, ns)
- return r
-return {
- "total":0,
- "rows":[]
-}
\ No newline at end of file
+async def main():
+ pipeline_id = params_kw.pipeline_id or params_kw.id
+ dbname = get_module_dbname('pipeline_core')
+ async with DBPools().sqlorContext(dbname) as sor:
+ if pipeline_id:
+ recs = await sor.R('pipeline_steps', {'pipeline_id': pipeline_id})
+ else:
+ recs = await sor.R('pipeline_steps', {})
+ # Sort by step_order
+ recs = sorted(recs, key=lambda r: r.step_order if hasattr(r,'step_order') else 0)
+ return recs
diff --git a/pipeline_core/wwwroot/pipeline_steps/get_pipelines.dspy b/pipeline_core/wwwroot/pipeline_steps/get_pipelines.dspy
new file mode 100644
index 0000000..2034836
--- /dev/null
+++ b/pipeline_core/wwwroot/pipeline_steps/get_pipelines.dspy
@@ -0,0 +1,13 @@
+
+from sqlor.dbpools import DBPools
+
+async def main():
+ dbname = get_module_dbname('pipeline_core')
+ async with DBPools().sqlorContext(dbname) as sor:
+ recs = await sor.R('pipelines', {'status': 'published'})
+ if not recs:
+ recs = await sor.R('pipelines', {})
+ result = []
+ for r in recs:
+ result.append({'id': r.id, 'name': r.name, 'text': r.name, 'value': r.id})
+ return result
diff --git a/pipeline_core/wwwroot/pipeline_steps/get_step_types.dspy b/pipeline_core/wwwroot/pipeline_steps/get_step_types.dspy
new file mode 100644
index 0000000..85881c1
--- /dev/null
+++ b/pipeline_core/wwwroot/pipeline_steps/get_step_types.dspy
@@ -0,0 +1,18 @@
+
+from sqlor.dbpools import DBPools
+
+async def main():
+ dbname = get_module_dbname('pipeline_core')
+ async with DBPools().sqlorContext(dbname) as sor:
+ recs = await sor.R('pipeline_step_types', {})
+ result = []
+ for r in recs:
+ result.append({
+ 'step_type': r.step_type,
+ 'display_name': r.display_name,
+ 'text': f"{r.display_name} ({r.step_type})",
+ 'value': r.step_type,
+ 'category': r.category,
+ 'is_interactive': r.is_interactive
+ })
+ return result
diff --git a/pipeline_dist/scripts/load_path.py b/pipeline_dist/scripts/load_path.py
index ca77c30..01f1d0a 100644
--- a/pipeline_dist/scripts/load_path.py
+++ b/pipeline_dist/scripts/load_path.py
@@ -1,22 +1,38 @@
-"""Register RBAC paths for pipeline_dist module."""
+#!/usr/bin/env python3
+"""RBAC path registration for pipeline_dist + showcase modules."""
+import os, sys, subprocess
-RBAC_PATHS = [
- '/api/distributors_list.dspy',
- '/api/distributors_get.dspy',
- '/api/distributors_save.dspy',
- '/api/distributors_delete.dspy',
- '/api/distributor_pipeline_list.dspy',
- '/api/distributor_pipeline_get.dspy',
- '/api/distributor_pipeline_save.dspy',
- '/api/distributor_pipeline_delete.dspy',
- '/api/distributor_generate_key.dspy',
+MODS = [
+ ("pipeline_dist", [
+ "/pipeline_dist",
+ "/pipeline_dist/index.ui",
+ "/pipeline_dist/distributors/index.ui",
+ "/pipeline_dist/distributor_pipeline/index.ui",
+ ]),
+ ("showcase", [
+ "/showcase",
+ "/showcase/index.ui",
+ ]),
]
-def load_paths(register):
- """Register all RBAC paths for this module."""
- for path in RBAC_PATHS:
- register(f'pipeline_dist{path}', {
- 'module': 'pipeline_dist',
- 'path': path,
- })
+def main():
+ root = os.path.expanduser("~/pipeline")
+ set_perm = os.path.join(root, "set_role_perm.py")
+ if not os.path.isfile(set_perm):
+ print("ERROR: pipeline root not found"); sys.exit(1)
+ py = sys.executable
+ count = 0
+ total = 0
+ for mod, paths in MODS:
+ total += len(paths)
+ for path in paths:
+ r = subprocess.run([py, set_perm, "logined", path], capture_output=True, text=True, cwd=root)
+ if r.returncode == 0: count += 1
+ else: print(f" WARN: {path}")
+ print(f" {mod}: {len(paths)} paths")
+ print(f"Registered {count}/{total}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/pipeline_ops/scripts/load_path.py b/pipeline_ops/scripts/load_path.py
index c6284b7..69cf30d 100644
--- a/pipeline_ops/scripts/load_path.py
+++ b/pipeline_ops/scripts/load_path.py
@@ -1,90 +1,62 @@
-"""Register all RBAC paths for pipeline_ops module"""
+#!/usr/bin/env python3
+"""RBAC path registration for pipeline_ops module."""
+import os, sys, subprocess
+
+MOD = "pipeline_ops"
+
+PATHS_ANY = [
+ f"/{MOD}/index.ui",
+]
+
+PATHS_LOGINED = [
+ f"/{MOD}",
+ f"/{MOD}/api/ops_dashboard_data.dspy",
+ # Pricing
+ f"/{MOD}/pipeline_pricing/index.ui",
+ f"/{MOD}/pipeline_pricing/get_pipeline_pricing.dspy",
+ f"/{MOD}/pipeline_pricing/add_pipeline_pricing.dspy",
+ f"/{MOD}/pipeline_pricing/update_pipeline_pricing.dspy",
+ f"/{MOD}/pipeline_pricing/delete_pipeline_pricing.dspy",
+ # Capacity
+ f"/{MOD}/pipeline_capacity/index.ui",
+ f"/{MOD}/pipeline_capacity/get_pipeline_capacity.dspy",
+ f"/{MOD}/pipeline_capacity/add_pipeline_capacity.dspy",
+ f"/{MOD}/pipeline_capacity/update_pipeline_capacity.dspy",
+ f"/{MOD}/pipeline_capacity/delete_pipeline_capacity.dspy",
+ # Usage Log
+ f"/{MOD}/pipeline_usage_log/index.ui",
+ f"/{MOD}/pipeline_usage_log/get_pipeline_usage_log.dspy",
+ f"/{MOD}/pipeline_usage_log/add_pipeline_usage_log.dspy",
+ f"/{MOD}/pipeline_usage_log/update_pipeline_usage_log.dspy",
+ f"/{MOD}/pipeline_usage_log/delete_pipeline_usage_log.dspy",
+ # API
+ f"/{MOD}/api/pipeline_pricing_create.dspy",
+ f"/{MOD}/api/pipeline_pricing_update.dspy",
+ f"/{MOD}/api/pipeline_pricing_delete.dspy",
+ f"/{MOD}/api/pipeline_capacity_create.dspy",
+ f"/{MOD}/api/pipeline_capacity_update.dspy",
+ f"/{MOD}/api/pipeline_capacity_delete.dspy",
+ f"/{MOD}/api/pipeline_usage_log_create.dspy",
+ f"/{MOD}/api/pipeline_usage_log_update.dspy",
+ f"/{MOD}/api/pipeline_usage_log_delete.dspy",
+ f"/{MOD}/api/get_search_pipeline_id.dspy",
+]
-def load_paths():
- """Return list of RBAC paths for this module"""
- paths = [
- {
- "path": "/pipeline_ops/",
- "name": "产线运营",
- "icon": "settings",
- "parent": "",
- "sort": 30
- },
- {
- "path": "/pipeline_ops/pipeline_pricing",
- "name": "定价管理",
- "icon": "price-tag",
- "parent": "/pipeline_ops/",
- "sort": 31
- },
- {
- "path": "/pipeline_ops/api/pipeline_pricing_create.dspy",
- "name": "新增定价",
- "parent": "/pipeline_ops/pipeline_pricing",
- "sort": 32
- },
- {
- "path": "/pipeline_ops/api/pipeline_pricing_update.dspy",
- "name": "修改定价",
- "parent": "/pipeline_ops/pipeline_pricing",
- "sort": 33
- },
- {
- "path": "/pipeline_ops/api/pipeline_pricing_delete.dspy",
- "name": "删除定价",
- "parent": "/pipeline_ops/pipeline_pricing",
- "sort": 34
- },
- {
- "path": "/pipeline_ops/pipeline_capacity",
- "name": "供应量管理",
- "icon": "gauge",
- "parent": "/pipeline_ops/",
- "sort": 40
- },
- {
- "path": "/pipeline_ops/api/pipeline_capacity_create.dspy",
- "name": "新增供应量配置",
- "parent": "/pipeline_ops/pipeline_capacity",
- "sort": 41
- },
- {
- "path": "/pipeline_ops/api/pipeline_capacity_update.dspy",
- "name": "修改供应量配置",
- "parent": "/pipeline_ops/pipeline_capacity",
- "sort": 42
- },
- {
- "path": "/pipeline_ops/api/pipeline_capacity_delete.dspy",
- "name": "删除供应量配置",
- "parent": "/pipeline_ops/pipeline_capacity",
- "sort": 43
- },
- {
- "path": "/pipeline_ops/pipeline_usage_log",
- "name": "使用记录",
- "icon": "list",
- "parent": "/pipeline_ops/",
- "sort": 50
- },
- {
- "path": "/pipeline_ops/api/pipeline_usage_log_create.dspy",
- "name": "新增使用记录",
- "parent": "/pipeline_ops/pipeline_usage_log",
- "sort": 51
- },
- {
- "path": "/pipeline_ops/api/pipeline_usage_log_update.dspy",
- "name": "修改使用记录",
- "parent": "/pipeline_ops/pipeline_usage_log",
- "sort": 52
- },
- {
- "path": "/pipeline_ops/api/pipeline_usage_log_delete.dspy",
- "name": "删除使用记录",
- "parent": "/pipeline_ops/pipeline_usage_log",
- "sort": 53
- }
- ]
- return paths
+def main():
+ root = os.path.expanduser("~/pipeline")
+ set_perm = os.path.join(root, "set_role_perm.py")
+ if not os.path.isfile(set_perm):
+ print("ERROR: pipeline root not found"); sys.exit(1)
+ py = sys.executable
+ count = 0
+ for role, paths in [("any", PATHS_ANY), ("logined", PATHS_LOGINED)]:
+ for path in paths:
+ r = subprocess.run([py, set_perm, role, path], capture_output=True, text=True, cwd=root)
+ if r.returncode == 0: count += 1
+ else: print(f" WARN: {path}")
+ print(f"Registered {count}/{len(PATHS_ANY)+len(PATHS_LOGINED)} paths for {MOD}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/pipeline_ops/wwwroot/api/ops_dashboard_data.dspy b/pipeline_ops/wwwroot/api/ops_dashboard_data.dspy
new file mode 100644
index 0000000..2ddae22
--- /dev/null
+++ b/pipeline_ops/wwwroot/api/ops_dashboard_data.dspy
@@ -0,0 +1,61 @@
+"""
+Operations Dashboard Data: task queue, human tasks, stats
+"""
+from sqlor.dbpools import DBPools
+
+async def main():
+ dbname = get_module_dbname('pipeline_core')
+
+ async with DBPools().sqlorContext(dbname) as sor:
+ # Task stats by state
+ task_stats = await sor.sqlExe("""
+ SELECT state, COUNT(*) as cnt FROM pipeline_tasks
+ GROUP BY state ORDER BY cnt DESC
+ """, {})
+
+ stats = {'submitted':0, 'running':0, 'completed':0, 'failed':0, 'cancelled':0}
+ for r in task_stats:
+ stats[r.state] = r.cnt
+
+ # Recent tasks (last 20)
+ tasks = await sor.sqlExe("""
+ SELECT t.id, t.title, t.state, t.pipeline_id, t.created_at
+ FROM pipeline_tasks t
+ ORDER BY t.created_at DESC LIMIT 20
+ """, {})
+ task_list = []
+ for r in tasks:
+ task_list.append({
+ 'id': r.id, 'title': r.title, 'state': r.state,
+ 'pipeline_id': r.pipeline_id, 'created_at': str(r.created_at)[:19]
+ })
+
+ # Pending human tasks
+ human_tasks = await sor.sqlExe("""
+ SELECT h.id, h.task_id, h.task_type, h.step_name, h.status,
+ h.assignee_role, h.created_at, h.expired_at,
+ t.title as task_title
+ FROM pipeline_human_tasks h
+ LEFT JOIN pipeline_tasks t ON h.task_id = t.id
+ WHERE h.status IN ('pending', 'in_progress')
+ ORDER BY h.created_at DESC LIMIT 20
+ """, {})
+
+ human_list = []
+ for r in human_tasks:
+ human_list.append({
+ 'id': r.id, 'task_id': r.task_id, 'task_type': r.task_type,
+ 'step_name': r.step_name, 'status': r.status,
+ 'assignee_role': r.assignee_role, 'task_title': r.task_title,
+ 'created_at': str(r.created_at)[:19],
+ 'expired_at': str(r.expired_at)[:19] if r.expired_at else None
+ })
+
+ return {
+ 'stats': stats,
+ 'tasks': task_list,
+ 'human_tasks': human_list,
+ 'total_tasks': sum(stats.values()),
+ 'active_tasks': stats['running'],
+ 'pending_reviews': len(human_list)
+ }
diff --git a/pipeline_ops/wwwroot/index.ui b/pipeline_ops/wwwroot/index.ui
index 3f463eb..e3387a0 100644
--- a/pipeline_ops/wwwroot/index.ui
+++ b/pipeline_ops/wwwroot/index.ui
@@ -1,46 +1,120 @@
{
"widgettype": "VBox",
- "options": {"width": "100%", "height": "100%", "padding": "24px", "spacing": "20px"},
+ "options": {"width": "100%", "height": "100%", "padding": "24px", "gap": "20px"},
"subwidgets": [
- {"widgettype": "Title2", "options": {"text": "产线运营"}},
+ {
+ "widgettype": "HBox",
+ "options": {"alignItems": "center", "gap": "12px"},
+ "subwidgets": [
+ {"widgettype": "Title2", "options": {"text": "产线运营中心"}},
+ {"widgettype": "Text", "options": {"text": "任务队列、审批管理、执行监控", "cfontsize": 1.1, "color": "#888"}}
+ ]
+ },
{
"widgettype": "ResponsableBox",
- "options": {"gap": "24px", "minWidth": "260px"},
+ "options": {"gap": "16px", "minWidth": "150px"},
"subwidgets": [
{
"widgettype": "VBox",
- "options": {"css": "card", "cwidth": 23, "cheight": 11, "padding": "24px", "cursor": "pointer", "bgcolor": "#fff"},
- "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.main_content", "options": {"url": "{{entire_url('pipeline_pricing/')}}"}, "mode": "replace"}],
+ "options": {"bgcolor": "#1e293b", "padding": "20px", "borderRadius": "12px", "gap": "4px", "border": "1px solid #334155", "cwidth": 16, "alignItems": "center"},
+ "subwidgets": [
+ {"widgettype": "Text", "options": {"text": "-", "cfontsize": 2, "color": "#60a5fa", "fontWeight": "bold"}},
+ {"widgettype": "Text", "options": {"text": "已提交", "cfontsize": 0.9, "color": "#94a3b8"}}
+ ]
+ },
+ {
+ "widgettype": "VBox",
+ "options": {"bgcolor": "#1e293b", "padding": "20px", "borderRadius": "12px", "gap": "4px", "border": "1px solid #334155", "cwidth": 16, "alignItems": "center"},
+ "subwidgets": [
+ {"widgettype": "Text", "options": {"text": "-", "cfontsize": 2, "color": "#fbbf24", "fontWeight": "bold"}},
+ {"widgettype": "Text", "options": {"text": "运行中", "cfontsize": 0.9, "color": "#94a3b8"}}
+ ]
+ },
+ {
+ "widgettype": "VBox",
+ "options": {"bgcolor": "#1e293b", "padding": "20px", "borderRadius": "12px", "gap": "4px", "border": "1px solid #334155", "cwidth": 16, "alignItems": "center"},
+ "subwidgets": [
+ {"widgettype": "Text", "options": {"text": "-", "cfontsize": 2, "color": "#4ade80", "fontWeight": "bold"}},
+ {"widgettype": "Text", "options": {"text": "已完成", "cfontsize": 0.9, "color": "#94a3b8"}}
+ ]
+ },
+ {
+ "widgettype": "VBox",
+ "options": {"bgcolor": "#1e293b", "padding": "20px", "borderRadius": "12px", "gap": "4px", "border": "1px solid #334155", "cwidth": 16, "alignItems": "center"},
+ "subwidgets": [
+ {"widgettype": "Text", "options": {"text": "-", "cfontsize": 2, "color": "#f87171", "fontWeight": "bold"}},
+ {"widgettype": "Text", "options": {"text": "失败", "cfontsize": 0.9, "color": "#94a3b8"}}
+ ]
+ },
+ {
+ "widgettype": "VBox",
+ "options": {"bgcolor": "#1e293b", "padding": "20px", "borderRadius": "12px", "gap": "4px", "border": "1px solid #334155", "cwidth": 16, "alignItems": "center"},
+ "subwidgets": [
+ {"widgettype": "Text", "options": {"text": "-", "cfontsize": 2, "color": "#a78bfa", "fontWeight": "bold"}},
+ {"widgettype": "Text", "options": {"text": "待审批", "cfontsize": 0.9, "color": "#94a3b8"}}
+ ]
+ },
+ {
+ "widgettype": "VBox",
+ "options": {"bgcolor": "#1e293b", "padding": "20px", "borderRadius": "12px", "gap": "4px", "border": "1px solid #334155", "cwidth": 16, "alignItems": "center"},
+ "subwidgets": [
+ {"widgettype": "Text", "options": {"text": "-", "cfontsize": 2, "color": "#e2e8f0", "fontWeight": "bold"}},
+ {"widgettype": "Text", "options": {"text": "总任务", "cfontsize": 0.9, "color": "#94a3b8"}}
+ ]
+ }
+ ]
+ },
+ {
+ "widgettype": "ResponsableBox",
+ "options": {"gap": "16px", "minWidth": "400px"},
+ "subwidgets": [
+ {
+ "widgettype": "VBox",
+ "options": {"bgcolor": "#1e293b", "padding": "20px", "borderRadius": "12px", "gap": "12px", "border": "1px solid #334155", "cwidth": 60},
+ "subwidgets": [
+ {"widgettype": "Title3", "options": {"text": "近期任务"}},
+ {"widgettype": "Text", "options": {"text": "暂无任务,提交产线任务后将在此显示", "cfontsize": 0.9, "color": "#64748b", "padding": "20px 0"}}
+ ]
+ },
+ {
+ "widgettype": "VBox",
+ "options": {"bgcolor": "#1e293b", "padding": "20px", "borderRadius": "12px", "gap": "12px", "border": "1px solid #334155", "cwidth": 40},
+ "subwidgets": [
+ {"widgettype": "Title3", "options": {"text": "待审批队列"}},
+ {"widgettype": "Text", "options": {"text": "暂无审批任务", "cfontsize": 0.9, "color": "#64748b", "padding": "20px 0"}}
+ ]
+ }
+ ]
+ },
+ {
+ "widgettype": "ResponsableBox",
+ "options": {"gap": "16px", "minWidth": "180px"},
+ "subwidgets": [
+ {
+ "widgettype": "VBox",
+ "options": {"css": "card", "cwidth": 23, "cheight": 10, "padding": "24px", "cursor": "pointer", "bgcolor": "#1e293b", "border": "1px solid #334155"},
+ "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.main_content", "options": {"url": "{{entire_url('/pipeline_ops/pipeline_pricing/')}}"}, "mode": "replace"}],
"subwidgets": [
- {"widgettype": "VBox", "options": {"width": "44px", "height": "44px", "bgcolor": "#FEF2F2", "borderRadius": "12px", "alignItems": "center", "justifyContent": "center", "marginBottom": "12px"}, "subwidgets": [
- {"widgettype": "Svg", "options": {"svg": "", "width": "24px", "height": "24px"}}
- ]},
{"widgettype": "Title3", "options": {"text": "定价管理", "marginBottom": "4px"}},
- {"widgettype": "Text", "options": {"text": "管理产线计费方式和价格", "cfontsize": 0.9}}
+ {"widgettype": "Text", "options": {"text": "管理产线计费方式和价格", "cfontsize": 0.9, "color": "#94a3b8"}}
]
},
{
"widgettype": "VBox",
- "options": {"css": "card", "cwidth": 23, "cheight": 11, "padding": "24px", "cursor": "pointer", "bgcolor": "#fff"},
- "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.main_content", "options": {"url": "{{entire_url('pipeline_capacity/')}}"}, "mode": "replace"}],
+ "options": {"css": "card", "cwidth": 23, "cheight": 10, "padding": "24px", "cursor": "pointer", "bgcolor": "#1e293b", "border": "1px solid #334155"},
+ "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.main_content", "options": {"url": "{{entire_url('/pipeline_ops/pipeline_capacity/')}}"}, "mode": "replace"}],
"subwidgets": [
- {"widgettype": "VBox", "options": {"width": "44px", "height": "44px", "bgcolor": "#F0F9FF", "borderRadius": "12px", "alignItems": "center", "justifyContent": "center", "marginBottom": "12px"}, "subwidgets": [
- {"widgettype": "Svg", "options": {"svg": "", "width": "24px", "height": "24px"}}
- ]},
{"widgettype": "Title3", "options": {"text": "供应量管理", "marginBottom": "4px"}},
- {"widgettype": "Text", "options": {"text": "配置产线并发和调用限额", "cfontsize": 0.9}}
+ {"widgettype": "Text", "options": {"text": "配置产线并发和调用限额", "cfontsize": 0.9, "color": "#94a3b8"}}
]
},
{
"widgettype": "VBox",
- "options": {"css": "card", "cwidth": 23, "cheight": 11, "padding": "24px", "cursor": "pointer", "bgcolor": "#fff"},
- "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.main_content", "options": {"url": "{{entire_url('pipeline_usage_log/')}}"}, "mode": "replace"}],
+ "options": {"css": "card", "cwidth": 23, "cheight": 10, "padding": "24px", "cursor": "pointer", "bgcolor": "#1e293b", "border": "1px solid #334155"},
+ "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.main_content", "options": {"url": "{{entire_url('/pipeline_ops/pipeline_usage_log/')}}"}, "mode": "replace"}],
"subwidgets": [
- {"widgettype": "VBox", "options": {"width": "44px", "height": "44px", "bgcolor": "#F5F3FF", "borderRadius": "12px", "alignItems": "center", "justifyContent": "center", "marginBottom": "12px"}, "subwidgets": [
- {"widgettype": "Svg", "options": {"svg": "", "width": "24px", "height": "24px"}}
- ]},
{"widgettype": "Title3", "options": {"text": "使用记录", "marginBottom": "4px"}},
- {"widgettype": "Text", "options": {"text": "查看产线调用和消费记录", "cfontsize": 0.9}}
+ {"widgettype": "Text", "options": {"text": "查看产线调用和消费记录", "cfontsize": 0.9, "color": "#94a3b8"}}
]
}
]
diff --git a/scripts/load_path.py b/scripts/load_path.py
new file mode 100644
index 0000000..72e36c0
--- /dev/null
+++ b/scripts/load_path.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python3
+"""Register all pipeline module RBAC permissions in one run."""
+import sys, os, asyncio
+
+ROOT = os.path.expanduser("~/pipeline")
+sys.path.insert(0, os.path.join(ROOT, "py3", "lib", "python3.10", "site-packages"))
+sys.path.insert(0, ROOT)
+
+from sqlor.dbpools import DBPools
+from appPublic.jsonConfig import getConfig
+from appPublic.folderUtils import ProgramPath
+from appPublic.uniqueID import getID
+from ahserver.serverenv import ServerEnv
+from ahserver.globalEnv import initEnv
+
+PERMS = [
+ # pipeline-sdlc
+ ("/pipeline-sdlc", "pipeline_sdlc", "logined"),
+ ("/pipeline-sdlc/sd_dashboard/*", "pipeline_sdlc", "logined"),
+ ("/pipeline-sdlc/sd_projects/*", "pipeline_sdlc", "logined"),
+ ("/pipeline-sdlc/sd_project_list/*", "pipeline_sdlc", "logined"),
+ ("/pipeline-sdlc/sd_iterations/*", "pipeline_sdlc", "logined"),
+ ("/pipeline-sdlc/sd_iteration_list/*", "pipeline_sdlc", "logined"),
+ ("/pipeline-sdlc/sd_test_plans/*", "pipeline_sdlc", "logined"),
+ ("/pipeline-sdlc/sd_test_plan_list/*", "pipeline_sdlc", "logined"),
+ ("/pipeline-sdlc/sd_test_cases/*", "pipeline_sdlc", "logined"),
+ ("/pipeline-sdlc/sd_test_case_list/*", "pipeline_sdlc", "logined"),
+ ("/pipeline-sdlc/sd_bugs/*", "pipeline_sdlc", "logined"),
+ ("/pipeline-sdlc/sd_bug_list/*", "pipeline_sdlc", "logined"),
+ ("/pipeline-sdlc/sd_deploy_envs/*", "pipeline_sdlc", "logined"),
+ ("/pipeline-sdlc/sd_deploy_env_list/*", "pipeline_sdlc", "logined"),
+ ("/pipeline-sdlc/sd_review/*", "pipeline_sdlc", "logined"),
+ ("/pipeline-sdlc/api/*", "pipeline_sdlc", "logined"),
+ # pipeline_core
+ ("/pipeline_core", "pipeline_core", "logined"),
+ ("/pipeline_core/pipelines/*", "pipeline_core", "logined"),
+ ("/pipeline_core/pipeline_steps/*", "pipeline_core", "logined"),
+ ("/pipeline_core/pipeline_versions/*", "pipeline_core", "logined"),
+ ("/pipeline_core/pipeline_editor/*", "pipeline_core", "logined"),
+ ("/pipeline_core/api/*", "pipeline_core", "logined"),
+ # pipeline_ops
+ ("/pipeline_ops", "pipeline_ops", "logined"),
+ ("/pipeline_ops/api/*", "pipeline_ops", "logined"),
+ ("/pipeline_ops/pipeline_pricing/*", "pipeline_ops", "logined"),
+ ("/pipeline_ops/pipeline_capacity/*", "pipeline_ops", "logined"),
+ ("/pipeline_ops/pipeline_usage_log/*", "pipeline_ops", "logined"),
+ # pipeline_dist
+ ("/pipeline_dist", "pipeline_dist", "logined"),
+ ("/pipeline_dist/*", "pipeline_dist", "logined"),
+ # showcase
+ ("/showcase", "showcase", "logined"),
+ ("/showcase/*", "showcase", "logined"),
+ # app root (any)
+ ("/**", "app", "any"),
+ ("/index.ui", "app", "any"),
+ ("/pipeline-sdlc/index.ui", "pipeline_sdlc", "any"),
+ ("/pipeline_core/index.ui", "pipeline_core", "any"),
+ ("/pipeline_ops/index.ui", "pipeline_ops", "any"),
+ ("/pipeline_dist/index.ui", "pipeline_dist", "any"),
+]
+
+
+async def register():
+ config = getConfig(ROOT, NS={"workdir": ROOT, "ProgramPath": ProgramPath()})
+ DBPools(config.databases)
+ initEnv()
+ env = ServerEnv()
+ env.get_module_dbname = lambda m: "sage"
+
+ async with DBPools().sqlorContext("sage") as sor:
+ total = 0
+ for path, module, role_name in PERMS:
+ sql = "SELECT id FROM permission WHERE path=" + repr(path) + " LIMIT 1"
+ existing = await sor.sqlExe(sql, {})
+ if existing:
+ continue
+
+ pid = getID()
+ await sor.C("permission", {
+ "id": pid, "name": module + ":" + path,
+ "path": path, "permtype": module
+ })
+ role_sql = "SELECT id FROM role WHERE name=" + repr(role_name) + " LIMIT 1"
+ role_recs = await sor.sqlExe(role_sql, {})
+ if role_recs:
+ await sor.C("rolepermission", {
+ "id": getID(),
+ "roleid": role_recs[0].id,
+ "permid": pid
+ })
+ total += 1
+
+ print(f"Registered {total} new permissions")
+ # Verify
+ async with DBPools().sqlorContext("sage") as sor:
+ cnt = await sor.sqlExe(
+ "SELECT COUNT(*) as n FROM permission WHERE path LIKE '/pipeline%' OR path LIKE '/showcase%'", {})
+ print(f"Total pipeline permissions: {cnt[0].n}")
+
+
+if __name__ == "__main__":
+ asyncio.run(register())
diff --git a/scripts/merge_i18n.py b/scripts/merge_i18n.py
new file mode 100644
index 0000000..5ae54ea
--- /dev/null
+++ b/scripts/merge_i18n.py
@@ -0,0 +1,99 @@
+#!/usr/bin/env python3
+"""
+Merge i18n translations from all pipeline modules into wwwroot/i18n.
+Scans each module's i18n directory and merges into the app's centralized i18n.
+
+Usage: python scripts/merge_i18n.py
+"""
+import os, json, sys
+from collections import OrderedDict
+from pathlib import Path
+
+SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
+ROOT_DIR = os.path.dirname(SCRIPT_DIR)
+WWWROOT = os.path.join(ROOT_DIR, 'wwwroot')
+I18N_OUT = os.path.join(WWWROOT, 'i18n')
+
+# Modules to scan for i18n
+MODULES = [
+ ('pipeline_core', os.path.join(ROOT_DIR, 'pipeline_core', 'i18n')),
+ ('pipeline_ops', os.path.join(ROOT_DIR, 'pipeline_ops', 'i18n')),
+ ('pipeline_dist', os.path.join(ROOT_DIR, 'pipeline_dist', 'i18n')),
+ ('pipeline-sdlc', os.path.join(os.path.dirname(ROOT_DIR), 'pipeline-sdlc', 'i18n')),
+ ('showcase', os.path.join(os.path.dirname(ROOT_DIR), 'showcase', 'i18n')),
+]
+
+LANGS = ['zh', 'en', 'ko', 'jp']
+
+
+def parse_msg_txt(filepath):
+ """Parse msg.txt: key=value format."""
+ result = OrderedDict()
+ if not os.path.exists(filepath):
+ return result
+ with open(filepath, 'r', encoding='utf-8') as f:
+ for line in f:
+ line = line.strip()
+ if not line or line.startswith('#'):
+ continue
+ if '=' in line:
+ key, val = line.split('=', 1)
+ result[key.strip()] = val.strip()
+ return result
+
+
+def merge():
+ os.makedirs(I18N_OUT, exist_ok=True)
+ stats = {}
+
+ for lang in LANGS:
+ merged = OrderedDict()
+ out_dir = os.path.join(I18N_OUT, lang)
+ os.makedirs(out_dir, exist_ok=True)
+
+ # Load existing merged translations
+ i18n_file = os.path.join(out_dir, 'i18n.json')
+ if os.path.exists(i18n_file):
+ with open(i18n_file, 'r', encoding='utf-8') as f:
+ merged = json.load(f, object_pairs_hook=OrderedDict)
+
+ # Scan each module
+ for mod_name, mod_i18n_dir in MODULES:
+ lang_dir = os.path.join(mod_i18n_dir, lang)
+ if not os.path.isdir(lang_dir):
+ # Try app's internal i18n
+ alt_dir = os.path.join(ROOT_DIR, 'pipeline_core', 'i18n', lang)
+ if not os.path.isdir(alt_dir):
+ continue
+ lang_dir = alt_dir
+
+ # Load msg.txt
+ msg_file = os.path.join(lang_dir, 'msg.txt')
+ if os.path.exists(msg_file):
+ msgs = parse_msg_txt(msg_file)
+ for k, v in msgs.items():
+ if k not in merged:
+ merged[k] = v
+ stats.setdefault(mod_name, 0)
+ stats[mod_name] += 1
+
+ # Load i18n.json
+ mod_i18n = os.path.join(lang_dir, 'i18n.json')
+ if os.path.exists(mod_i18n):
+ with open(mod_i18n, 'r', encoding='utf-8') as f:
+ extra = json.load(f, object_pairs_hook=OrderedDict)
+ for k, v in extra.items():
+ if k not in merged:
+ merged[k] = v
+
+ # Write merged
+ with open(i18n_file, 'w', encoding='utf-8') as f:
+ json.dump(merged, f, ensure_ascii=False, indent=2)
+ print(f' {lang}: {len(merged)} keys')
+
+ # Write msg.txt for the app root
+ print(f'\nMerged i18n for {len(MODULES)} modules -> {I18N_OUT}')
+
+
+if __name__ == '__main__':
+ merge()
diff --git a/set_role_perm.py b/set_role_perm.py
index 6d24905..1a4097f 100644
--- a/set_role_perm.py
+++ b/set_role_perm.py
@@ -1,52 +1,62 @@
#!/usr/bin/env python3
-"""Wrapper script for RBAC permission registration.
-Called by modules' load_path.py scripts.
-Usage: py3/bin/python set_role_perm.py
"""
-import sys
-import os
-import asyncio
+Register a single RBAC permission for pipeline modules.
+Called by init_perms.py or directly.
-# Add current directory to path
-sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+Usage:
+ python set_role_perm.py
+ python set_role_perm.py admin /pipeline_core/pipelines/*.dspy
+"""
+import sys, os, asyncio
+
+SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
+ROOT_DIR = os.path.dirname(SCRIPT_DIR) if os.path.basename(SCRIPT_DIR) == 'bin' else SCRIPT_DIR
+sys.path.insert(0, os.path.join(ROOT_DIR, 'py3', 'lib', 'python3.10', 'site-packages'))
+sys.path.insert(0, ROOT_DIR)
-from rbac.set_role_perms import set_role_perm as _set_role_perm
-from appPublic.jsonConfig import getConfig
from sqlor.dbpools import DBPools
+from appPublic.jsonConfig import getConfig
+from appPublic.folderUtils import ProgramPath
+from appPublic.uniqueID import getID
+from ahserver.serverenv import ServerEnv
+from ahserver.globalEnv import initEnv
async def main(role, path):
- """Register a single permission path for a role."""
- # Determine module name from path
- # e.g., /pipeline_core/index.ui -> pipeline_core
+ config = getConfig(os.path.join(ROOT_DIR, 'conf', 'config.json'),
+ NS={'workdir': ROOT_DIR, 'ProgramPath': ProgramPath()})
+ DBPools(config.databases)
+ initEnv()
+ env = ServerEnv()
+ env.get_module_dbname = lambda m: 'pipeline' if 'pipeline' in m else 'sage'
+
+ # Determine module from path
parts = path.strip('/').split('/')
module = parts[0] if parts else 'app'
-
- # Get database name from config
- config = getConfig('.', {'workdir': '.'})
- db = DBPools(config.databases)
-
- # Call the actual permission setter
- await _set_role_perm('pipeline', module, '*', role, path)
+ async with DBPools().sqlorContext('sage') as sor:
+ existing = await sor.sqlExe(
+ "SELECT id FROM role_path WHERE module=${m}$ AND path=${p}$ AND role_name=${r}$",
+ {'m': module, 'p': path, 'r': role})
+ if existing:
+ print(f'Permission exists: {role} {path}')
+ return
-def run(coro):
- """Run async function in event loop."""
- loop = asyncio.new_event_loop()
- asyncio.set_event_loop(loop)
- try:
- loop.run_until_complete(coro)
- finally:
- loop.close()
+ await sor.C('role_path', {
+ 'id': getID(),
+ 'module': module,
+ 'path': path,
+ 'role_name': role,
+ 'description': f'Auto-registered: {role} -> {path}'
+ })
+ print(f'Registered: {role} -> {path}')
if __name__ == '__main__':
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} ")
- print(f"Example: {sys.argv[0]} logined /pipeline_core/index.ui")
+ print(f" roles: guest, logined, admin")
+ print(f" path example: /pipeline_core/index.ui")
sys.exit(1)
-
- role = sys.argv[1]
- path = sys.argv[2]
-
- run(main(role, path))
+
+ asyncio.run(main(sys.argv[1], sys.argv[2]))
diff --git a/wwwroot/index.ui b/wwwroot/index.ui
index e78fe95..2e1ce3a 100644
--- a/wwwroot/index.ui
+++ b/wwwroot/index.ui
@@ -1,73 +1,167 @@
{
- "id": "app",
- "widgettype": "VBox",
- "options": {"width": "100%", "height": "100%", "padding": "0"},
- "subwidgets": [
+ "id": "app",
+ "widgettype": "VBox",
+ "options": {
+ "width": "100%",
+ "height": "100%",
+ "padding": "0"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "HBox",
+ "options": {
+ "width": "100%",
+ "cheight": 2,
+ "bgcolor": "#1e293b",
+ "padding": "0 16px",
+ "alignItems": "center",
+ "gap": "12px"
+ },
+ "subwidgets": [
{
- "widgettype": "HBox",
- "options": {"width": "100%", "cheight": 2, "bgcolor": "#1e293b", "padding": "0 16px", "alignItems": "center", "gap": "12px"},
- "subwidgets": [
- {"widgettype": "Button", "id": "btn_toggle_sidebar", "options": {"label": "☰", "css": "text", "color": "#fff", "cfontsize": 1.4}, "binds": [{"wid": "self", "event": "click", "actiontype": "method", "target": "app.sidebar_menu", "method": "toggle_collapse"}]},
- {"widgettype": "Title4", "options": {"text": "产线平台", "color": "#fff"}},
- {"widgettype": "Filler"},
- {
- "widgettype": "urlwidget",
- "options": {
- "url": "{{entire_url('/rbac/user/user_panel.ui')}}"
- }
- }
- ]
+ "widgettype": "Button",
+ "id": "btn_toggle_sidebar",
+ "options": {
+ "label": "☰",
+ "css": "text",
+ "color": "#fff",
+ "cfontsize": 1.4
+ },
+ "binds": [
+ {
+ "wid": "self",
+ "event": "click",
+ "actiontype": "method",
+ "target": "app.sidebar_menu",
+ "method": "toggle_collapse"
+ }
+ ]
},
{
- "widgettype": "HBox",
- "options": {"css": "filler", "padding": "0", "gap": "0"},
- "subwidgets": [
+ "widgettype": "Title4",
+ "options": {
+ "text": "产线平台",
+ "color": "#fff"
+ }
+ },
+ {
+ "widgettype": "Filler"
+ }
+ ]
+ },
+ {
+ "widgettype": "HBox",
+ "options": {
+ "css": "filler",
+ "padding": "0",
+ "gap": "0"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "VBox",
+ "options": {
+ "width": "220px",
+ "minWidth": "200px",
+ "height": "100%",
+ "bgcolor": "#f8fafc"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "Menu",
+ "id": "sidebar_menu",
+ "options": {
+ "width": "100%",
+ "height": "100%",
+ "target": "app.main_content",
+ "items": [
+ {
+ "name": "pipeline_core",
+ "label": "产线管理",
+ "icon": "{{entire_url('/imgs/cubes.svg')}}",
+ "url": "{{entire_url('/pipeline_core/')}}"
+ },
+ {
+ "name": "pipeline_sdlc",
+ "label": "开发产线",
+ "icon": "{{entire_url('/imgs/workflow.svg')}}",
+ "url": "{{entire_url('/pipeline_sdlc/')}}"
+ },
+ {
+ "name": "pipeline_ops",
+ "label": "运营管理",
+ "icon": "{{entire_url('/imgs/dashboard.svg')}}",
+ "url": "{{entire_url('/pipeline_ops/')}}"
+ },
+ {
+ "name": "pipeline_dist",
+ "label": "分销管理",
+ "icon": "{{entire_url('/imgs/truck.svg')}}",
+ "url": "{{entire_url('/pipeline_dist/')}}"
+ },
+ {
+ "name": "pipeline_task",
+ "label": "任务中心",
+ "icon": "{{entire_url('/imgs/rocket.svg')}}",
+ "url": "{{entire_url('/pipeline_task/')}}"
+ },
+ {
+ "name": "pipeline_ktv",
+ "label": "KTV产线",
+ "icon": "{{entire_url('/imgs/mic.svg')}}",
+ "url": "{{entire_url('/pipeline_task/task_submit.ui')}}"
+ },
+ {
+ "name": "showcase",
+ "label": "展示平台",
+ "icon": "{{entire_url('/imgs/user-circle.svg')}}",
+ "url": "{{entire_url('/showcase/')}}"
+ },
+ {
+ "name": "tenant",
+ "label": "租户管理",
+ "icon": "{{entire_url('/imgs/globe.svg')}}",
+ "url": "{{entire_url('/tenant/')}}"
+ }
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "widgettype": "VScrollPanel",
+ "id": "main_content",
+ "options": {
+ "css": "filler",
+ "width": "100%",
+ "height": "100%"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "VBox",
+ "options": {
+ "width": "100%",
+ "padding": "32px",
+ "spacing": "24px"
+ },
+ "subwidgets": [
{
- "widgettype": "VBox",
- "options":{
- "width": "220px",
- "minWidth": "200px",
- "height": "100%",
- "bgcolor": "#f8fafc"
- },
- "subwidgets":[
- {
- "widgettype": "Menu",
- "id": "sidebar_menu",
- "options": {
- "width": "100%",
- "height": "100%",
- "target": "app.main_content",
- "items": [
- {"name": "pipeline_core", "label": "产线管理", "icon": "{{entire_url('/imgs/cubes.svg')}}", "url": "{{entire_url('/pipeline_core/')}}"},
- {"name": "pipeline_sdlc", "label": "开发产线", "icon": "{{entire_url('/imgs/workflow.svg')}}", "url": "{{entire_url('/pipeline_sdlc/')}}"},
- {"name": "pipeline_ops", "label": "运营管理", "icon": "{{entire_url('/imgs/dashboard.svg')}}", "url": "{{entire_url('/pipeline_ops/')}}"},
- {"name": "pipeline_dist", "label": "分销管理", "icon": "{{entire_url('/imgs/truck.svg')}}", "url": "{{entire_url('/pipeline_dist/')}}"},
- {"name": "pipeline_task", "label": "任务中心", "icon": "{{entire_url('/imgs/rocket.svg')}}", "url": "{{entire_url('/pipeline_task/')}}"},
- {"name": "pipeline_ktv", "label": "KTV产线", "icon": "{{entire_url('/imgs/mic.svg')}}", "url": "{{entire_url('/pipeline_task/task_submit.ui')}}"},
- {"name": "showcase", "label": "展示平台", "icon": "{{entire_url('/imgs/user-circle.svg')}}", "url": "{{entire_url('/showcase/')}}"},
- {"name": "tenant", "label": "租户管理", "icon": "{{entire_url('/imgs/globe.svg')}}", "url": "{{entire_url('/tenant/')}}"}
- ]
- }
- }
- ]
+ "widgettype": "Title2",
+ "options": {
+ "text": "产线平台"
+ }
},
{
- "widgettype": "VScrollPanel",
- "id": "main_content",
- "options": {"css": "filler", "width": "100%", "height": "100%"},
- "subwidgets": [
- {
- "widgettype": "VBox",
- "options": {"width": "100%", "padding": "32px", "spacing": "24px"},
- "subwidgets": [
- {"widgettype": "Title2", "options": {"text": "产线平台"}},
- {"widgettype": "Text", "options": {"text": "管理产线定义、运营配置、分销渠道与任务执行", "cfontsize": 1.2}}
- ]
- }
- ]
+ "widgettype": "Text",
+ "options": {
+ "text": "管理产线定义、运营配置、分销渠道与任务执行",
+ "cfontsize": 1.2
+ }
}
- ]
+ ]
+ }
+ ]
}
- ]
-}
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/wwwroot/pipeline_core/llm/create_llm.dspy b/wwwroot/pipeline_core/llm/create_llm.dspy
new file mode 100644
index 0000000..8b81dde
--- /dev/null
+++ b/wwwroot/pipeline_core/llm/create_llm.dspy
@@ -0,0 +1,5 @@
+func = create_llm
+if func is None:
+ return json.dumps({'status': 'error', 'message': 'function not found'})
+result = await func(params_kw)
+return result
diff --git a/wwwroot/pipeline_core/llm/delete_llm.dspy b/wwwroot/pipeline_core/llm/delete_llm.dspy
new file mode 100644
index 0000000..43d74cf
--- /dev/null
+++ b/wwwroot/pipeline_core/llm/delete_llm.dspy
@@ -0,0 +1,5 @@
+func = delete_llm
+if func is None:
+ return json.dumps({'status': 'error', 'message': 'function not found'})
+result = await func(params_kw)
+return result
diff --git a/wwwroot/pipeline_core/llm/get_llms.dspy b/wwwroot/pipeline_core/llm/get_llms.dspy
new file mode 100644
index 0000000..931ad3a
--- /dev/null
+++ b/wwwroot/pipeline_core/llm/get_llms.dspy
@@ -0,0 +1,11 @@
+from sqlor.dbpools import DBPools
+
+async def main():
+ dbname = get_module_dbname('pipeline_core')
+ async with DBPools().sqlorContext(dbname) as sor:
+ recs = await sor.R('llm', {'status': 'active'})
+ if not recs:
+ recs = await sor.R('llm', {})
+ if not recs:
+ recs = []
+ return recs
diff --git a/wwwroot/pipeline_core/llm/index.ui b/wwwroot/pipeline_core/llm/index.ui
new file mode 100644
index 0000000..ae4b2a0
--- /dev/null
+++ b/wwwroot/pipeline_core/llm/index.ui
@@ -0,0 +1,43 @@
+{
+ "widgettype": "Tabular",
+ "id": "llm_tbl",
+ "options": {
+ "width": "100%",
+ "title": "大模型管理",
+ "css": "card",
+ "data_url": "{{entire_url('./get_llms.dspy')}}",
+ "data_method": "GET",
+ "editable": {
+ "new_data_url": "{{entire_url('./create_llm.dspy')}}",
+ "update_data_url": "{{entire_url('./update_llm.dspy')}}",
+ "delete_data_url": "{{entire_url('./delete_llm.dspy')}}"
+ },
+ "browserfields": {
+ "exclouded": ["id", "api_key"],
+ "alters": {
+ "status": {
+ "uitype": "code",
+ "data": [
+ {"value": "active", "text": "启用"},
+ {"value": "inactive", "text": "停用"}
+ ]
+ }
+ }
+ },
+ "editexclouded": ["id", "created_at", "updated_at"],
+ "confidential_fields": ["api_key"],
+ "fields": [
+ {"name": "id", "title": "ID", "type": "str", "cwidth": 0},
+ {"name": "name", "title": "模型名称", "type": "str", "cwidth": 18},
+ {"name": "provider", "title": "供应商", "type": "str", "cwidth": 12},
+ {"name": "model_id", "title": "模型标识", "type": "str", "cwidth": 18},
+ {"name": "api_base", "title": "API地址", "type": "str", "cwidth": 20},
+ {"name": "api_key", "title": "API密钥", "type": "str", "cwidth": 14},
+ {"name": "max_tokens", "title": "最大Token", "type": "str", "cwidth": 10},
+ {"name": "status", "title": "状态", "type": "str", "cwidth": 8},
+ {"name": "description", "title": "描述", "type": "str", "cwidth": 16},
+ {"name": "created_at", "title": "创建时间", "type": "str", "cwidth": 14},
+ {"name": "updated_at", "title": "更新时间", "type": "str", "cwidth": 14}
+ ]
+ }
+}
diff --git a/wwwroot/pipeline_core/llm/update_llm.dspy b/wwwroot/pipeline_core/llm/update_llm.dspy
new file mode 100644
index 0000000..00b969e
--- /dev/null
+++ b/wwwroot/pipeline_core/llm/update_llm.dspy
@@ -0,0 +1,5 @@
+func = update_llm
+if func is None:
+ return json.dumps({'status': 'error', 'message': 'function not found'})
+result = await func(params_kw)
+return result