feat: UI pages, build.sh, deployment scripts, model uitype fixes

- Add SDLC dashboard, pipeline editor, ops center UI
- build.sh: clone business modules to pkgs/, xls2ui CRUD, fix created_by
- global_func.py: password_encode None-safe wrapper
- pipeline_app.py: permission cache warmup removed
- load_path.py: RBAC permission registration for all modules
- scripts/merge_i18n.py: i18n merge tool
- bin/init_perms.py, bin/init_data.py: init scripts
- set_role_perm.py: single permission registration
- Model uitype fields set for form editing
- pipeline_core/pipeline_ops load_path.py scripts
This commit is contained in:
yumoqing 2026-07-18 22:44:34 +08:00
parent 3623d5725e
commit 3c9fdcf444
31 changed files with 1900 additions and 840 deletions

View File

@ -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

View File

@ -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)

152
bin/init_data.py Normal file
View File

@ -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))

61
bin/init_perms.py Normal file
View File

@ -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())

View File

@ -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 ==="

View File

@ -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"]
}
}

View File

@ -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"]}
]
}

View File

@ -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"
}
]
}

View File

@ -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"
}
]
}

View File

@ -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'"
}
]
}

View File

@ -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

View File

@ -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__":

View File

@ -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": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#3B82F6\" stroke-width=\"2\"><path d=\"M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z\"/></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": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#22C55E\" stroke-width=\"2\"><path d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"/></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": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#F59E0B\" stroke-width=\"2\"><path d=\"M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z\"/></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": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#3B82F6\" stroke-width=\"2\"><path d=\"M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z\"/></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": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#22C55E\" stroke-width=\"2\"><path d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"/></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": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#F59E0B\" stroke-width=\"2\"><path d=\"M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z\"/></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%"
}
}
]
}

View File

@ -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"}]
}
]
}
]
}

View File

@ -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)}

View File

@ -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":[]
}
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

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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)
}

View File

@ -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": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#EF4444\" stroke-width=\"2\"><path d=\"M12 2a10 10 0 100 20 10 10 0 000-20z\"/><path d=\"M12 6v6l4 2\"/></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": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#0EA5E9\" stroke-width=\"2\"><path d=\"M13 10V3L4 14h7v7l9-11h-7z\"/></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": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#8B5CF6\" stroke-width=\"2\"><path d=\"M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z\"/><polyline points=\"14 2 14 8 20 8\"/><line x1=\"16\" y1=\"13\" x2=\"8\" y2=\"13\"/><line x1=\"16\" y1=\"17\" x2=\"8\" y2=\"17\"/></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"}}
]
}
]

102
scripts/load_path.py Normal file
View File

@ -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())

99
scripts/merge_i18n.py Normal file
View File

@ -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()

View File

@ -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 <role> <path>
"""
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 <role> <path>
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]} <role> <path>")
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]))

View File

@ -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
}
}
]
]
}
]
}
]
}
]
}
]
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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}
]
}
}

View File

@ -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