62 KiB
| name | version | description | trigger_conditions | tags | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| pipeline-app-module | 2.0.0 | 产线生态系统 — 通用执行引擎、产线定义/运营/分销/交互的完整开发规范和部署指南。 |
|
|
Pipeline 产线生态系统
核心理念
模块不等于应用的附属品。模块就是模块,谁加载就是谁的:
- pipeline-app 加载 → pipeline-app 的模块
- sage 加载 → sage 的模块
- 任何应用加载 → 该应用的模块
Hermes Agent 是验证场,pipeline 是产品:
- Hermes Agent 中用 cron/delegate/terminal 跑通业务流程
- 验证通过后固化为产线步骤定义(pipeline_steps 表)
- pipeline-service 自动调度执行,独立于 Hermes Agent
仓库总览
⚠️ 所有 pipeline 仓库统一放在 /d/ymq/pipeline/ 目录下,不要放根层级或 ~/repos/。
| 仓库 | 路径 | 说明 |
|---|---|---|
| pipeline-app (伞仓) | /d/ymq/pipeline/pipeline-app/ |
独立应用宿主源码,内含 pipeline_core/ 目录(非独立仓库) |
| pipeline-app 测试 | apitest@120.48.168.15:/d/apitest/pipeline-app/ |
tokentest 环境 (port 9090, pipeline.opencomputing.cn) |
| pipeline-sdlc | /d/ymq/pipeline/pipeline-sdlc/ |
开发产线(SDLC:项目、迭代、测试、Bug、部署环境) |
| pipeline-service | /d/ymq/pipeline/pipeline-service/ |
通用产线执行引擎(v2.0) |
| pipeline-task | /d/ymq/pipeline/pipeline-task/ |
产线任务交互层 |
| evaluate | /d/ymq/pipeline/evaluate/ |
质量评估模块 |
| showcase | /d/ymq/pipeline/showcase/ |
展示平台(展示产线产出物) |
pipeline_core 不是独立仓库,它是 pipeline-app 伞仓根目录下的子目录 pipeline_core/(含 init/, json/, wwwroot/, models/, pipeline_core/ Python 包等)。测试服务器上 pkgs/pipeline_core/ 也没有独立 .git,git 操作走父仓库 pipeline-app。
模块架构
pipeline_core(产线定义 — 管理员)
| 表 | 说明 |
|---|---|
| pipelines | 产线主表(name, description, status, owner_id) |
| pipeline_steps | 步骤定义(pipeline_id, step_name, step_type, step_order, step_config — deps 存在 step_config JSON 中,非独立列) |
| pipeline_versions | 发布版本(pipeline_id, version_no, snapshot_json, status) |
| llm | 模型配置(name, provider, model_id, api_base, api_key, status) — llm_bridge.py 从此表读取 API 配置 |
pipeline_ops(运营管理 — 运营人员)
| 表 | 说明 |
|---|---|
| pipeline_pricing | 定价(pipeline_id, step_id, model_name, unit_price, pricing_type) |
| pipeline_capacity | 供应量(pipeline_id, quota_type, quota_value, concurrent_limit) |
| pipeline_usage_log | 使用记录(pipeline_id, user_id, step_id, usage_count, cost) |
pipeline_dist(分销管理 — 分销商)
| 表 | 说明 |
|---|---|
| distributors | 分销商(name, api_key, status, quota, org_id) |
| distributor_pipeline | 分销-产线关联(distributor_id, pipeline_id, custom_price, enabled) |
pipeline_service(执行引擎 — 核心)
通用产线执行引擎,不知道自己在跑 KTV 还是找飞机还是写小说。
| 表 | 说明 |
|---|---|
| pipeline_tasks | 任务主表(tenant_id 隔离) |
| pipeline_task_steps | 步骤执行记录 |
| pipeline_artifacts | 产物(input/output,支持版本) |
引擎工作原理:
- 提交 → 读取 pipeline_steps 表步骤定义 → 创建 task_steps 记录 → 启动执行
- 执行循环 → 解析 DAG 依赖图 → 找可执行步骤 → 调用 handler → 存 artifact → 继续
- 多租户 → 所有查询按 tenant_id 隔离
- 人工干预 → 修改 artifact → 创建新版本 → BFS 级联重跑
步骤处理器(可插拔):
async def handler(tenant_id, task_id, step_name, input_data, config) -> dict:
return output_data
register_handler("llm_generate", handler)
步骤定义中的 step_type 匹配 handler 注册名。新增产线 = 插步骤数据 + 注册 handler,引擎不改一行。
对外接口(注册到 ServerEnv):
| 函数 | 说明 |
|---|---|
| pipeline_submit | 提交新任务 |
| pipeline_role_submit | 主agent提交带 role 的角色任务(tenant_id=project_id,见 references/role-agent-question-loop.md) |
| pipeline_list | 任务列表 |
| pipeline_detail | 任务详情+步骤状态树 |
| pipeline_node | 节点 artifact |
| pipeline_modify | 修改+级联重跑 |
| pipeline_pause/resume/cancel | 任务控制 |
| pipeline_restart | 重启已完成/失败/取消任务,重置所有步骤为新版本 |
pipeline_task(引擎交互层 — 底层 API,非 UI 模块)
⚠️ 架构关键:pipeline_task 是引擎层,不是 UI 模块。 各产线类型(SDLC、KTV 等)各自拥有独立的 UI 模块(如 pipeline-sdlc),产线 UI 通过跨模块 entire_url('/pipeline_task/api/...') 调用 pipeline_task 的 API。不要把产线专属的 dashboard UI 放进 pipeline_task。
纯薄交互层,无数据表。通过 ServerEnv 调用 pipeline_service 的函数。
| dspy | 调用 |
|---|---|
| task_submit | pipeline_submit() |
| task_list | pipeline_list() |
| task_detail | pipeline_detail() |
| task_node | pipeline_node() |
| task_modify | pipeline_modify() |
| task_control | pipeline_pause/resume/cancel() |
| task_restart | pipeline_restart() |
| human_complete | human_task_complete() |
| human_list | human_task_list() |
| approval_approve | approval_approve() |
| approval_reject | approval_reject() |
UI: index.ui(内嵌 Tabular 任务列表 + 搜索筛选) / task_list.ui / task_detail.ui(Jinja2 预渲染,人工任务表单/审批按钮/重启) / task_submit.ui / step_panel.ui(步骤产物查看 + 修改级联重跑)
宿主集成
任何应用只需:
from pipeline_core.init import load_pipeline_core
from pipeline_ops.init import load_pipeline_ops
from pipeline_dist.init import load_pipeline_dist
from pipeline_service.init import load_pipeline_service
from pipeline_task.init import load_pipeline_task
load_pipeline_core()
load_pipeline_ops()
load_pipeline_dist()
load_pipeline_service()
load_pipeline_task()
pipeline-app 关键配置
conf/config.json(完整示例)
{
"password_key": "<从服务器 conf/config.json 读取,各环境不同,不要硬编码>",
"databases": {
"pipeline": {
"driver": "aiosqlor",
"kwargs": {
"host": "127.0.0.1",
"port": 3306,
"user": "hermes",
"password": "<AES加密后的密码>",
"db": "pipeline",
"charset": "utf8mb4"
}
}
},
"website": {
"port": 9090,
"paths": [
["$[workdir]$/wwwroot", ""]
],
"session_redis": {
"url": "redis://127.0.0.1:6379/3"
},
"processors": [
[".dspy", "dspy"],
[".ui", "bui"],
[".tmpl", "tmpl"]
],
"indexes": ["index.ui", "index.html"]
},
"SAGE_RBAC_DB": "pipeline"
}
配置要点(易错):
driver必须是"aiosqlor"(不是 "aiomysql"),否则 sqlor 找不到匹配的 MySQL 子类- ⚠️ PITFALL:
password_key各环境不同,不要硬编码。 读取方式:from appPublic.jsonConfig import getConfig; config = getConfig(".", {"workdir": "."}); key = config.password_key。硬编码会导致密码验证失败 - ⚠️ PITFALL: 用户密码是 RC4 加密,不是 AES。 加密函数是
appPublic.rc4.password(s, key=key),不是aes_encode_b64。password_key仅用于 RC4 加密用户密码;DB 密码(database.kwargs.password)才用 AES password必须 AES 加密:from appPublic.aes import aes_encode_b64; aes_encode_b64(password_key, plaintext)— 这是 DB 连接密码,与用户密码不同password_key字段必须存在,否则 sqlor 解密时报 NoneType 错误SAGE_RBAC_DB设为"pipeline"(本应用自己的数据库),不是"sage"paths使用统一 wwwroot 目录 + 空前缀"",不要用多个独立路径带模块前缀(会导致 url2file 解析失败)
global_func.py
所有模块共用 pipeline 数据库,动态获取 dbname:
from ahserver.serverenv import ServerEnv
DBNAME = "pipeline"
def get_module_dbname(mname):
"""All modules share the pipeline database."""
return DBNAME
def set_globalvariable():
g = ServerEnv()
g.get_module_dbname = get_module_dbname
Support Files
references/ktv-handler-implementation.md— KTV 步骤处理器实现细节references/eval-llm-integration.md— 质量评估 LLM 集成模式references/cockpit-dspy-common-issues.md— cockpit dspy 常见崩溃:msg_id 变量未定义、LLM 超时丢失上下文等问题的根因和修复references/cockpit-executor-pipeline.md— Cockpit → Pipeline Executor 交互流程:正确的pipeline_submit → start_task → 步骤执行链路 vs 错误的「直接调 LLM」反模式references/quality-gate-pattern.md— 质量门控模式详细实现references/interactive-decision-steps.md— 交互式决策步骤references/root-index-template.md— 根入口页面模板references/service-to-module-conversion.md— 服务转模块指南references/tokentest-deployment.md— pipeline-app 前移到 tokentest 测试环境的完整部署步骤references/502-troubleshooting.md— Pipeline 502 问题完整排查指南:bricks symlink 缺失诊断、nginx 反向代理链路、部署完整性检查清单references/serverenv-vs-run_ns.md— DSPY调用的async函数必须用 request._run_ns 而非 ServerEnv() 的陷阱和修复references/gpu-service-architecture.md— GPU服务原子性架构:nginx反向代理、token网关、服务不互调、不上LLM(数据库、配置、脚本、验证)references/pipeline-task-human-interaction.md— 人工任务交互流程:human_complete/approval dspy模式、Jinja2预加载、pipeline_restartreferences/sqlor-dictobject-serialization.md— sqlor DictObject 序列化泄漏方法名的根因和修复references/rbac-permission-cache.md— RBAC 权限匹配规则(通配符/**)、缓存 TTL、session-ticket 认证方式、init_data.py 表结构对齐检查references/rbac-manual-registration.md— load_path.py 无法执行时的兜底方案references/vibevoice-asr-deployment.md— VibeVoice-ASR-7B GPU 部署:ModelScope 下载、transformers 直连(无 Docker)、ahserver+LongTasks 模式、Whisper API 兼容映射references/dashboard-ui-pattern.md— 开发产线 Dashboard:快速提交表单、步骤树弹窗(Popup+Tree+apply_data)、Bug跨模块查询、产线下拉选项references/cockpit-intent-pattern.md— 驾驶舱意图识别+引导对话模式:LLM分类→缺失信息追问→动作路由分发,_select_modelname/id 双匹配references/agent-loop-deliverables.md— Agent循环引擎+交付件仓库+Agent配置:pipeline_deliverables/pipeline_project_agents 表结构,agent_loop.py 循环逻辑,DB context 冲突注意事项references/role-agent-question-loop.md— v3.2.0 主agent+角色agent架构:tenant_id=project_id 约定、角色名归一化、问题回路表/env接口、DDL双仓库同步、ALTER迁移语句、踩坑记录references/workspace-directory-pattern.md— 工作目录模式:sd_projects.workspace_dir + pipeline_project_agents.deliverable_dir,Agent 交付文件路径的目录结构和代码模式references/cockpit-pattern.md— 驾驶舱模式:对话式输入、时间线+聊天双面板、urlwidget→DSPY动态内容渲染references/getwidgetbyid-no-default-pitfall.md— bricks.js getWidgetById 新版无默认 from_widget 导致脚本崩溃的根因、修复和验证脚本references/bricks-blank-page-troubleshooting.md— 页面空白/bricks.js 构造函数崩溃完整排查:实体目录 vs 软链接、源版 vs 编译版 bricks.js、浏览器缓存诊断法、git 恢复后文件缺失
部署步骤
1. 数据库准备
CREATE DATABASE pipeline CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
2. 建表
从 sage 导出 RBAC 表结构,执行各模块的 DDL:
# RBAC 表(从 sage 复制结构)
mysqldump -u hermes -phermes123 --no-data sage users role permission rolepermission userrole organization orgtypes org_roles appcodes appcodes_kv > rbac_ddl.sql
mysql -u hermes -phermes123 pipeline < rbac_ddl.sql
# 业务表(各模块 DDL)
for mod in pipeline_core pipeline_ops pipeline_dist pipeline-service; do
mysql -u hermes -phermes123 pipeline < $mod/mysql.ddl.sql
done
3. 构建应用
bash build.sh # 创建 venv + 安装依赖 + 生成 DDL/CRUD
4. 初始化数据
运行 init_data.py 脚本(创建管理员用户、角色、权限、appcodes):
py3/bin/python init_data.py
5. 配置 wwwroot(关键——缺失会导致 502)
创建统一 wwwroot,符号链接各模块。以下两步缺一不可:
mkdir -p wwwroot
# 【关键1】bricks 前端静态文件——必须是软链接 → pkgs/bricks/dist,绝不能是实体目录!
# 实体目录会装 23KB 源版 bricks.js → 页面空白报 "bricks.App is not a constructor"
# 前置:若 dist 不存在先 `cd pkgs/bricks && mkdir -p dist && cd bricks && bash build.sh`
rm -rf wwwroot/bricks
ln -sf "$cdir/pkgs/bricks/dist" "$cdir/wwwroot/bricks" # build.sh 第44行就是这么做的
# 【关键2】根入口页面——nginx location = / 代理到 /index.ui,缺失则根路径 500
# 内容必须是完整的主布局(Header+Sidebar+Content),从 git 历史恢复:
# git show 3c9fdcf:wwwroot/index.ui > wwwroot/index.ui
# 模块 wwwroot 符号链接(仅服务静态文件,不影响 Python 导入)
ln -sf pipeline_core_repo/wwwroot wwwroot/pipeline_core
ln -sf pipeline_ops_repo/wwwroot wwwroot/pipeline_ops
ln -sf pipeline_dist_repo/wwwroot wwwroot/pipeline_dist
ln -sf ~/repos/pipeline-task/wwwroot wwwroot/pipeline_task
# ⚠️ Python 包必须放在 app 目录内才能 import!
# wwwroot 符号链接只服务静态文件,不影响 Python 导入路径
cp -r ~/repos/pipeline-task/pipeline_task ./pipeline_task
cp -r ~/repos/pipeline-service/pipeline_service ./pipeline_service
6. 启动
bash start.sh
7. 权限初始化(关键)
确保 logined 角色存在并注册所有路径:
cd ~/test/pipeline-app
py3/bin/python init_rbac.py # 或参考 rbac-permission-initialization-pattern 技能
注意:pipeline-service 是纯后端模块,无前端路径需要注册。
8. 部署验证(必须完整)
不要只测 localhost,必须验证外部访问:
# 1. 本地基础验证
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:9090/
# 期望: 200
# 2. 登录页面
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:9090/rbac/user/login.ui
# 期望: 200
# 3. 登录后模块访问
curl -s -o /dev/null -w "%{http_code}" -u admin:admin123 http://127.0.0.1:9090/pipeline_core/index.ui
# 期望: 200
# 4. 外部 HTTPS(必须验证!)
curl -s -o /dev/null -w "%{http_code}" https://pipeline.opencomputing.cn/
# 期望: 200(不是 401)
curl -s -o /dev/null -w "%{http_code}" -u admin:admin123 https://pipeline.opencomputing.cn/pipeline_task/index.ui
# 期望: 200
常见401原因:logined 角色不存在于 role 表 → 登录用户无权限。详见 rbac-permission-initialization-pattern 技能 Section 6。
技术要点
- sor.U() 只有2个参数(CRITICAL PITFALL):id 放在 data 字典中,绝不传第3个参数。常见复发模式是
data.pop('id')取走 id 后把{'id': record_id}当作独立 WHERE 参数传入——sqlor 的U()不接受独立 WHERE 参数。 - ⚠️ PITFALL: sor.I 不是 Insert,是 Inspect(查表结构)!
sor.I(table)返回表的字段描述,不带数据。插入数据用sor.C(table, data_dict)—— C 才是 Create。sor.I被错误当成 Insert 调用会造成takes 2 positional arguments but 3 were given(因为 I 只接受 tablename 一个参数 + self)。记忆口诀:C=Create, R=Read, U=Update, D=Delete, I=Inspect。# WRONG — data.pop('id') + 3rd arg = TypeError: record_id = data.pop('id', None) await sor.U('pipelines', data, {'id': record_id}) # 3 args → TypeError! # CORRECT — merge id back into data: record_id = data.pop('id', None) await sor.U('pipelines', {**data, 'id': record_id}) # 2 args ✓ # Also wrong — separate data + where dicts: await sor.U('pipelines', {'status': 'published'}, {'id': pipeline_id}) # 3 args! # Correct: await sor.U('pipelines', {'id': pipeline_id, 'status': 'published'}) # 2 args ✓ - dspy 注入集有限,标准库不全:globalEnv 注入
json/DBPools/curDateString等全局名,但不注入 os/uuid/base64 —— dspy 用到这些必须文件头显式import os(顶层 import 合法)。裸名可用性以 grepglobalEnv.py的g.xxx =为准,不凭记忆 - DBPools 动态获取 dbname:禁硬编码,通过 global_func.py 映射
- ⚠️ PITFALL: DSPY 调用的 async 函数必须用
request._run_ns,不能用ServerEnv():ServerEnv()是模块级空单例,不随请求变化。request._run_ns是processorResource按请求注入的上下文,包含entire_url、get_userorgid、get_module_dbname等。 所有从.dspy调用的 async 函数必须在第一行用env = request._run_ns。用env = ServerEnv()会导致env.entire_url等为 None,触发TypeError: 'NoneType' object is not callable(500错误)。 详细修复示例见references/serverenv-vs-run_ns.md。 - pipeline_service 仓库不存在:
pipeline_service无公开仓库。pipeline_app.py和ktv_adapter.py的导入必须用try/except ImportError包裹,优雅降级。 - 部署到 tokentest 不能用
pipeline数据库:test用户无 CREATE DATABASE 权限,直接用sage库。详见references/tokentest-deployment.md。 - 分销密钥生成:distributor_generate_key.dspy 使用 secrets.token_hex(32)
- 步骤处理器可插拔:register_handler(step_type, fn),不同产线注册不同 handler
- 多租户隔离:tenant_id 贯穿 pipeline_tasks 所有查询
- 交互层极简:pipeline_task 零数据表,JS 只写必要辅助函数,不写大段逻辑
- pipeline_steps 表无 deps 列:DAG 依赖关系存在
step_configJSON 中({"deps": ["step_a"], "desc": "..."}),由 storage.py 提取到记录顶层 - 步骤树 UI 层级:在 step_config 中添加
parent_step字段控制 Tree widget 展示层级:{"deps": [...], "parent_step": "step_a"}。parent_step与deps独立——deps 控制执行 DAG,parent_step 控制 UI 树。根节点 parent_step 为空字符串 - 跨模块 entire_url:产线专属 UI(如 pipeline-sdlc)调用 pipeline_task API 须用绝对路径
entire_url('/pipeline_task/api/task_list.dspy')。模块内用相对路径,跨模块必须/模块名/api/... - 步骤处理器文件命名:按产线类型命名
handlers_<type>.py,导出HANDLERS字典和register_<type>_handlers()函数,在load_pipeline_service()中调用注册
步骤处理器开发模式
PITFALL: 区分草稿代码与生产代码
- 草稿/骨架代码位于源仓库(如
pipeline-app/app/ktv_adapter.py) - 生产代码位于测试服务器
apitest@120.48.168.15:/d/apitest/pipeline-app/app/ktv_adapter.py - 修改前必须确认读取的是哪个版本,避免在草稿上浪费时间
新增产线 = 步骤数据入库 + handler 文件实现 + 注册。引擎代码不改一行。
质量门控模式(Quality Gate Pattern)
适用场景:关键步骤需要质量保障,自动评估-重试-人工介入闭环。
架构设计:
执行 handler → 多维度评估 → 不达标自动重试(最多3次) → 仍不达标暂停等待人工决策
实现模式:
- 创建评估函数(
app/eval_*.py):
async def evaluate_xxx_quality(output: dict) -> Tuple[bool, float, str]:
"""评估函数返回 (passed, score, reason)"""
# 多维度评估逻辑
# 返回: (是否通过, 0-1分数, 原因说明)
return True, 0.85, "各维度评估良好"
- 包装 handler:
def _make_quality_handler(original_handler, eval_func):
async def wrapper(tenant_id, task_id, step_name, input_data, config):
from app.quality_gate import with_quality_gate
result = await with_quality_gate(
task_id=task_id, step_name=step_name, version=1,
handler=original_handler, evaluator=eval_func,
tenant_id=tenant_id, input_data=input_data, config=config,
)
return result
return wrapper
# 注册质量门控版本
quality_music_polling = _make_quality_handler(handle_music_polling, eval_music_quality)
- 注册到 KTV_HANDLERS:
KTV_HANDLERS = {
"music_polling": quality_music_polling, # 质量门控 ✓
"other_step": handle_other_step, # 普通 handler
}
多维度评估示例:
视频质量(5维度):
- 视觉质量 25% - ffprobe 分析分辨率/码率/帧率
- 运动流畅度 20% - ffprobe 帧率分析
- 语义一致性 30% - 已集成 llm_bridge(关键帧base64 → LLM多模态分析prompt匹配度)
- 角色一致性 15% - 已集成 llm_bridge(参考图+关键帧 → LLM对比角色特征)
- 时间连贯性 10% - 帧差分析
音乐质量(7维度):
- 节奏质量 15% - 响度范围分析
- 和弦进行 10% - 时长推断
- 听感质量 20% - loudnorm 响度/峰值分析
- 情绪-歌词匹配 20% - 已集成 llm_bridge(歌词+音频情绪 → LLM情感匹配分析)
- 指令匹配度 15% - 已集成 llm_bridge(prompt+音频特征 → LLM指令匹配分析)
- 伴奏质量 10% - Demucs 分离后评估
- 人声质量 10% - Demucs 分离后评估
工具链:ffprobe/ffmpeg(元数据), Demucs GPU(人声伴奏分离), llm_bridge(LLM文本/多模态评估)
PITFALL — eval 模块 LLM 调用异常回退:llm_call 可能超时或返回非JSON。每个评估函数必须在 except 块中返回默认分数(如 0.8),保证产线不中断。详见 references/eval-llm-integration.md。
Executor 集成:
pipeline_service/executor.py 需特殊处理 QualityGatePausedError,避免覆盖 WAITING 状态为 FAILED。
详细实现参考 references/quality-gate-pattern.md 和 references/eval-llm-integration.md。
验证迁移完整性:运行 scripts/verify_ktv_pipeline.py 检查所有 step_types 都有对应 handler,DAG 无悬空依赖。
文件结构:
# pipeline_service/handlers_ktv.py
KTV_HANDLERS = {
"audio_preparing": handle_audio_preparing,
"demucs_separating": handle_demucs_separating,
# ...
}
def register_ktv_handlers():
from .handler import register_handler
for step_type, fn in KTV_HANDLERS.items():
register_handler(step_type, fn)
Handler 签名:
async def handler(tenant_id, task_id, step_name, input_data, config) -> dict:
# input_data: {dep_step_name: {output_key: value}, "task_params": {...}}
# config: step_config JSON from pipeline_steps table
return output_data # 存入 artifact,下游步骤可通过 input_data 访问
input_data 中找上游产物:
for dep_name, dep_output in input_data.items():
if isinstance(dep_output, dict):
value = dep_output.get("key_name")
if value:
break
LLM 调用桥接(pipeline_service/llm_bridge.py):
后端优先级:
- harnessed_agent.llm_chat(ServerEnv 加载时)
- 数据库
llm表 — 按 model name 查 api_base/api_key/model_id,有内存缓存 - 环境变量 LLM_API_BASE / LLM_API_KEY / LLM_MODEL
from pipeline_service.llm_bridge import llm_call
result = await llm_call(prompt, model="deepseek-v4-pro", temperature=0.7)
质量评估模块 LLM 集成模式(app/eval_*.py):
评估函数标准模式(视频/音乐质量评估):
async def evaluate_xxx(video_path: str, prompt: str, frames: list = None) -> Tuple[float, str]:
"""返回 (score 0-1, reason)"""
if not prompt:
return 0.5, "无 prompt,跳过评估"
try:
from pipeline_service.llm_bridge import llm_call
# 1. 构建 prompt(要求 JSON 输出)
llm_prompt = f"""分析以下内容:
描述:{prompt}
请评估:1. 维度1 2. 维度2
返回 JSON:{{"match_score": 0-10, "reason": "简要说明"}}
"""
# 2. 多模态:图像 base64 编码
frames_b64 = []
if frames:
for frame_path in frames[:3]: # 最多3帧
if os.path.exists(frame_path):
with open(frame_path, 'rb') as f:
frames_b64.append(base64.b64encode(f.read()).decode('utf-8'))
# 3. 调用 LLM
result = await llm_call(llm_prompt)
result = result.strip()
# 4. 清理 markdown code block
if result.startswith("```"):
result = result.split("\n", 1)[1].rsplit("```", 1)[0]
# 5. 解析 JSON
import json
data = json.loads(result)
match_score = data.get("match_score", 8)
reason = data.get("reason", "LLM 评估完成")
# 6. 转换为 0-1 分数
score = min(1.0, match_score / 10.0)
return score, f"评估结果: {reason}"
except Exception as e:
logger.warning(f"LLM 评估失败: {e}")
return 0.8, "评估异常,使用默认分数" # 关键:回退默认分数保证产线不中断
已实现的评估函数:
eval_video.py: semantic_consistency(关键帧+prompt→语义匹配)、character_consistency(参考图+关键帧→角色一致性)eval_music.py: emotion_lyrics_match(歌词+音频情绪→情感匹配)、prompt_adherence(prompt+音频特征→指令匹配)
PITFALL:每个评估函数的 except 块必须返回默认分数(如 0.8),否则 LLM 超时或 JSON 解析失败会导致产线中断。详见 references/eval-llm-integration.md。
GPU 远程执行模式(⚠️ 已废弃 — 请使用 Token Gateway)
SSH 直连 GPU 服务器已废弃。正确架构:pipeline-app → token平台(Sage/llmage) → nginx → GPU服务。
详见 references/gpu-service-architecture.md。
旧模式(不要用):
GPU_HOST = "ymq@opencomputing.net"
async def _run_gpu(cmd, timeout=600):
ssh_cmd = f"ssh {GPU_HOST} '{cmd}'" # ❌ 绕过token平台
新模式:
async def call_gpu(model: str, payload: dict):
return await http_post("https://token.opencomputing.cn/llmage/v1/chat/completions",
json={"model": model, "messages": [{"role":"user","content":json.dumps(payload)}]})
GPU 远程执行模式(历史参考)
GPU_HOST = "ymq@opencomputing.net"
async def _run_gpu(cmd, timeout=600):
ssh_cmd = f"ssh -o StrictHostKeyChecking=no {GPU_HOST} '{cmd}'"
return await _run_local(ssh_cmd, timeout)
async def _copy_to_gpu(local, remote):
await _run_local(f"scp -o StrictHostKeyChecking=no '{local}' {GPU_HOST}:{remote}")
详细实现参考 references/ktv-handler-implementation.md。
产线管理页面布局模式(index.ui)
产线管理模块的 index.ui 采用三层定高 + 填充区布局:
VBox (height:100%, padding:0)
├── HBox Title (cheight:6, 定高) — 标题 + 副标题
├── HBox (cheight:18, 定高) — 包裹 ResponsableBox
│ └── ResponsableBox (三卡片 cheight:12)
└── VScrollPanel (id=pipeline_core_content, css:filler, 填充剩余)
关键点:
- 卡片和内容区是 root VBox 的直接子节点,不要额外包一层 VBox wrapper
- 三卡片用 HBox 包裹以锁定高度(cheight:18),ResponsableBox 的 cheight:12 去不掉
- VScrollPanel 用
css: "filler"自动填充剩余空间,内容内部滚动 - 卡片 bind:
target: "app.pipeline_core_content",mode: "replace" - 不要用
-prefix(closest 只搜祖先,搜不到兄弟节点) - 此布局是所有模块 index.ui 的统一标准:pipeline_core、pipeline_sdlc 等模块的入口页必须使用相同结构(白底卡片 + SVG 图标 + VScrollPanel filler)。禁止使用暗色主题、emoji 图标或
backgroundColor属性
UI 导航契约(强制 — 用户明确要求)
三层导航层次,统一交互,不混用:
- 系统菜单 → TabPanel:侧边栏 Menu 的系统菜单项(产线管理/开发产线/运营/分销/任务中心/审计日志/远程空间等),点击用
this.open_tab({name,label,url,removable:true})在app.main_content(TabPanel)打开为可切换标签页。参考index.ui侧边栏sidebar_menu的 script 写法。 - 用户菜单 → 弹窗:Header 右上角 👤 用户菜单(
userinfo.ui点击target:"Popup"+popup_options),弹窗加载user_menu.ui,不进 TabPanel。 - TabPanel 内容中的点击 → 弹窗:TabPanel 打开的模块页面内,点击按钮/链接一律弹窗(PopupWindow / Popup),不在 TabPanel 内再嵌套 tab,也不整页跳转破坏 tab 结构。
目的:顶层用 TabPanel 保持多任务上下文;用户级/次级操作一律弹窗(临时、可关闭),不污染 tab 列表。写任何 .ui 的 bind/script 前先套这三条。
DSPY 职责边界(dspy 简单 + 复用 py)
用户原则:dspy 要简单,多个 dspy 不能重复实现同一个功能,否则就写在 py 里。
模式:
- 重复逻辑抽到 .py 的统一函数(如
workspace.py的get_workspace_dir/get_workspace_base) load_xxx()里g.fn = fn注册到 ServerEnv(否则 dspy 调用 NameError 500)- dspy 里按函数名直接调用,只保留各自的业务逻辑
反面教材(已收敛):8 个 workspace_*.dspy 各自内联"读 workspace_base(params 兜底) + 查 current_project_id → sd_projects 算 ws_dir",同一逻辑重复 8 次;抽成 get_workspace_dir(sor, uid) 后,8 处改成一行 ws_dir, _ = await get_workspace_dir(sor, uid)(净删 161 行)。
收敛后必须复查残留:替换掉的代码块若定义了局部变量(如 pid),后续若还引用会 NameError/500;缺 import json 也会 500。改完 grep pid/workspace_base/json. 逐文件确认无残留 + 端到端 curl 冒烟。
Tabular 自定义 toolbar 按钮 + script bind 模式
在产线定义表加"发布"等自定义操作:
toolbar 定义(index.ui 中):
"toolbar": {
"tools": [
{"name": "publish", "label": "发布", "selected_row": true}
]
}
bind 写法(PITFALL)—— 用 this 不用 getWidgetById:
"binds": [{
"wid": "self",
"event": "publish",
"actiontype": "script",
"target": "self",
"script": "var d=this.select_row.user_data;fetch('{{entire_url('../api/pipeline_publish.dspy')}}',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'pipeline_id='+encodeURIComponent(d.id)}).then(function(r){return r.json()}).then(function(r){if(r.success){this.render({})}else{bricks.show_error({title:'发布失败',message:r.message||'未知错误'})}}.bind(this))"
}]
两个关键 PITFALL:
target: "self"时 script 中this就是 Tabular 自身,不要用bricks.getWidgetById('xxx')—— toolbar 按钮在 Tabular 内部,getWidgetById从按钮向下搜索找不到祖先 Tabular,报Cannot read properties of undefined (reading 'dom_element')- promise
.then()回调中this丢失,必须用.bind(this)才能调用this.render({})
开发工作流(强制)
铁律:pipeline-app 测试 = tokentest,禁在本机另搭环境。
- 所有代码修改必须先在
/d/ymq/pipeline/pipeline-app/或对应模块仓库(/d/ymq/pipeline/<module>/)完成 git commit后立即git push- SSH 到 tokentest 同步部署:
ssh apitest@120.48.168.15→cd /d/apitest/pipeline-app && git pull(伞仓);各模块cd /d/apitest/pipeline-app/pkgs/<module> && git pull - Python 代码变更后必须
pip install --upgrade:cd /d/apitest/pipeline-app && source py3/bin/activate && pip install --upgrade /d/apitest/pipeline-app/pkgs/<module> - 重启:
pkill -9 -f pipeline_app && sleep 2 && cd /d/apitest/pipeline-app && bash start.sh - 验证:
https://pipeline.opencomputing.cn/
服务器直接修改回传流程(违规补救)
当发现测试服务器上有直接修改(git status 显示 modified/untracked),必须按以下流程补救:
- 先
git pull本地确认拿到远程最新 scp服务器改动回本地:scp apitest@120.48.168.15:/d/apitest/pipeline-app/pkgs/<module>/<file> /d/ymq/pipeline/<module>/<file>- 本地
git add -A && git commit && git push - 服务器清理:
rm -f冲突的 untracked 文件 →git stash && git pull && git stash drop - 验证
git status --short为空
常见脏模块清单(每次部署后必须检查):
ssh apitest@120.48.168.15 "
for d in /d/apitest/pipeline-app/pkgs/*/; do
[ -d \"\$d/.git\" ] || continue
out=\$(cd \"\$d\" && git status --short)
[ -n \"\$out\" ] && echo \"[DIRTY] \$(basename \$d)\" && echo \"\$out\"
done
"
## ⚠️ 工作流铁律(最高优先级 — 违反即误操作)
0. **任何代码变更前必须加载 `multi-agent-workflow` 技能**。禁止单 agent 开发,禁止直接 SSH/scp/sed 改服务器文件。必须走本地克隆 → Agent1 开发 → Agent2 审查 → Agent3 测试 → 审计的完整流水线。违反此条是本技能最严重的流程违规。
0.5. **智能体创建的重复仓库必须清理**。子代理(delegate_task)会在 `/d/ymq/` 根层级创建 `pipeline-app`、`pipeline-sdlc`、`pipeline-service`、`pipeline-task`、`pipeline_core`、`test/pipeline-app` 等同名克隆,与 `/d/ymq/pipeline/` 下的正确仓库重复。每次任务结束后检查并删除:
```bash
rm -rf /d/ymq/pipeline-app /d/ymq/pipeline-sdlc /d/ymq/pipeline-service /d/ymq/pipeline-task /d/ymq/pipeline_core /d/ymq/test/pipeline-app
正确仓库只有 /d/ymq/pipeline/pipeline-app/、/d/ymq/pipeline/pipeline-sdlc/、/d/ymq/pipeline/pipeline-service/、/d/ymq/pipeline/pipeline-task/、/d/ymq/pipeline/evaluate/、/d/ymq/pipeline/showcase/。
- 严禁随意修改部署环境文件。用户原话:「为什么以前各个模块都能用的界面,现在都不能用了,这些你都可以随便改来改去的吗?」—— 每次改动必须有根有据,先查后改,不准盲目创建文件、改 symlink、或覆盖远程文件。
- 修复前四步确认:(1)
git log看最近变更 (2)ls -la确认文件现状 (3) 对比build.sh确认构建流程 (4) 用git show <commit>:<path>找回丢失的文件。 - 修改 → commit → push → 服务器 git pull → 重启 → 浏览器测。不允许跳过任何一步。
- curl 200 ≠ 功能正常。必须浏览器端完整链路验证。
- 服务器文件改动前先告知用户。不要静默 overwrite 远程文件(用户 blocked scp/sed 命令)。
部署踩坑记录
⚠️ 排查铁律:先确认原有状态,再动手修复。 不要看到 502/500 就盲目创建文件、改 symlink、修改模块代码。先检查:(1) git log 看最近变更 (2) ls -la 确认 wwwroot 文件现状 (3) 对比 build.sh 确认构建流程 (4) 用 git show : 找回丢失的文件。在拿不准时,用 cp 备份原文件再改。
| 问题 | 原因 | 修复 |
|---|---|---|
| sqlor connect 报 "Not Implemented" | driver 名错误 | 用 "aiosqlor" 而非 "aiomysql" |
| sqlor 报 NoneType.encode | 密码未加密或缺 password_key | config.json 加 password_key 字段,密码用 aes_encode_b64(key, pwd) 加密 |
| url2file 返回 None,"invalid path" | paths 配置用独立目录+模块前缀 | 统一 wwwroot 目录,前缀用 "" 空字符串 |
| ImportError: cannot import name | CWD 模块目录遮蔽 pip 安装包 | 重命名为 *_repo,符号链接内层包 *_repo/modname/ → modname/ |
| load_path.py 报找不到 APP_ROOT | 不需要 APP_ROOT 探测 | 脚本在 app root 执行,cwd 就是根目录,用相对路径 |
| load_path.py 执行失败 "找不到 set_role_perm.py" | 缺少 wrapper 脚本 | 在 app root 创建 set_role_perm.py wrapper(见下方) |
| sor.U() 3参数报错 | 旧代码残留,常见复发模式是 data.pop('id') 后把 {'id': record_id} 当作独立 WHERE 参数传入——sqlor 的 U() 不接受独立 WHERE 参数 |
id 放 data 字典,sor.U(table, data) 只传2参数。修改时必须同时更新3处副本:源码、build/lib/、py3/lib/python3.10/site-packages/。具体修复模式见上方「技术要点」sor.U() 代码示例 |
| RBAC 查权限报错 | SAGE_RBAC_DB 指向 sage | 改为 "pipeline"(本应用自己的库) |
根路径 / 返回500 |
缺少 indexes 配置 |
config.json 加 "indexes": ["index.ui", "index.html"] |
| bricks.js 401未授权 | 静态资源未注册 any 权限 | 用通配符 /bricks/** 注册到 any 角色。SQL 风格 /module/% 通配符无效(2026-08 实测:注册 /pipeline-sdlc/% 到 any 仍 401),必须用 /**;注册后 redis-cli -n 3 FLUSHDB + 重启才生效 |
| bricks.js:684 "Class extends value undefined" + "bricks.App is not a constructor",页面一片空白 | wwwroot/bricks 变成了实体目录(内含 23KB 源版 bricks.js),不是 build.sh 标准的软链接 → pkgs/bricks/dist(424KB 编译版)。源版缺 bricks.Layout 等类定义,bricks.App = class extends bricks.Layout 直接崩 |
rm -rf wwwroot/bricks && ln -sf /d/apitest/pipeline-app/pkgs/bricks/dist wwwroot/bricks。诊断:ls -la wwwroot/bricks 看有无 ->;wc -c 对比 bricks.js 大小(编译版 ~424KB,源文件 ~23KB)。完整排查流程见 references/bricks-blank-page-troubleshooting.md |
| 服务器已修好,用户仍报一模一样的前端错误 | 浏览器缓存了旧 bricks.js | 验证法:curl -s https://域名/bricks/bricks.js -o /tmp/x && sed -n '<浏览器报错行号>p' /tmp/x —— 服务器该行内容若与报错描述不符(如报错说 684 行是 bricks.App = class extends 但服务器 684 行是无害代码),就是缓存。让用户 Ctrl+Shift+R 强刷,不要反复改服务器 |
| git clone/恢复后 git 跟踪文件磁盘缺失(如 wwwroot/index.ui),nginx 代理根路径 500 | clone 后工作区状态混乱:git ls-files 有记录但文件不在磁盘(git status 显示 D) |
git ls-files <dir> 对比 ls 找出全部缺失文件 → git checkout -- <path> 逐个恢复。恢复后重启验证 |
| 所有路径 401(含外部HTTPS) | role 表缺 logined 角色,登录用户无任何权限 |
创建 logined 角色(orgtypeid=*)+ 注册路径到该角色 |
set_role_perm.py 报 NotADirectoryError /conf/config.json/conf/config.json |
getConfig() 接收目录路径(内部拼 conf/config.json),脚本传了完整文件路径 |
改为 getConfig(ROOT_DIR, ...) 只传目录,不拼 conf/config.json |
set_role_perm.py 报 Table 'pipeline.role_path' doesn't exist |
脚本用了不存在表名 role_path;正确 RBAC 表是 permission(存路径)+ rolepermission(关联角色) |
用 permission 表 INSERT 路径,rolepermission 表关联角色;一个 path 一条 permission,多个 rolepermission 行给不同角色授权 |
set_role_perm.py 报 NoneType.get / 连不上 DB |
脚本硬编码 sqlorContext('sage') 但独立应用数据库是 pipeline,config 中无 sage 的数据库配置 |
改为 sqlorContext('pipeline') 或用 get_module_dbname() 动态获取 |
| Jinja2 ternary 报错 | .ui 文件用了不存在的过滤器 | 用 if/else 表达式或静态文本替代 |
| pipeline_steps 插入失败 "Unknown column 'deps'" | 表无 deps 列,依赖关系存在 step_config JSON 中 | step_config 字段写入 {"deps": [...], "desc": "..."} 格式 JSON |
| 整站 502 Bad Gateway(nginx返回) | wwwroot/bricks 符号链接缺失,entire_url('/bricks/header.tmpl') 返回404 → Bricks 渲染崩溃 → ahserver 500 → nginx 502 |
ln -sf <pkgs>/bricks/dist wwwroot/bricks(build.sh 第3步明确执行此操作)。完整排查流程见 references/502-troubleshooting.md |
根路径 / 返回500 "invalid path",但 index.ui 存在 |
nginx location = / 已 proxy 到 /index.ui,但 wwwroot/bricks 缺失导致 header.tmpl 找不到,Bricks 模板渲染中 path_call 返回 None → 'NoneType' object has no attribute 'be_call' |
恢复 bricks symlink 即可。不是 rbac init.py 的问题,不要修改 rbac |
| 侧边栏菜单消失 / 页面只显示空白内容块 | wwwroot/index.ui 丢失(被错误覆盖或删除) |
从 git 历史恢复:git show 3c9fdcf:wwwroot/index.ui。完整 index.ui 包含 Header + Sidebar Menu + VScrollPanel 布局,不是单个模块的内容页。不要直接复制 pipeline_core/index.ui 作为根 index |
| build_step_graph 报 KeyError deps | storage.py 未从 step_config 提取 deps | get_pipeline_steps() 解析 step_config JSON 并注入 d['deps'] = cfg.get('deps', []) |
| DSPY调用报 TypeError: NoneType not callable | async函数用了 env = ServerEnv() 而非 env = request._run_ns |
改为 env = request._run_ns,详见 references/serverenv-vs-run_ns.md |
| sqlor DictObject 序列化泄漏方法名 | dir(rec) 包含 clear/copy/fromkeys 等内置方法 |
用 vars(rec) + if not callable(v) 过滤 |
storage.py 中 init_task_steps 报 KeyError: 'step_name' |
sqlor 行对象用字典访问 rec['step_name'] 失败。sqlor 行只支持属性访问 rec.step_name |
改为 getattr(rec, 'step_name', '')。get_task_steps 中 sor.R(table, conds, sort='x') 三参数也需改为 sqlExe + ORDER BY。详见 references/storage-py-pitfalls.md |
| 模块 .ui 页面 500 报函数未定义 | Python 包不在 app 目录内,try/except ImportError 静默跳过 |
将模块 Python 包复制/链接到 app 目录下(如 pipeline-app/pipeline_task/),wwwroot 符号链接 ≠ Python 导入路径 |
| init_data.py INSERT 报 Unknown column 'del_flg' / 'ptype' / 'role' | sage RBAC 表列名与 init_data.py 不一致。permission 用 permtype 非 ptype,description 非 title;role 只有 id,orgtypeid,name(无 role/rolelabel);organization 无 del_flg |
init_data.py 修改前先 SHOW COLUMNS FROM <table> 确认列名,逐表对齐 |
| 登录后子页面 401(权限已写DB) | RBAC 权限缓存 TTL 300s,重启后才刷新;或 init_data.py 未找到 API 路径(glob 路径受 symlink 影响) | 权限变更后重启应用;用 SELECT rp.roleid, p.path FROM rolepermission rp JOIN permission p ON rp.permid=p.id WHERE rp.roleid='owner.superuser' 验证权限已写入 |
| curl -u 测试全部返回 401 | 认证机制是 session-ticket(TktAuthentication),不支持 HTTP Basic Auth | 浏览器登录获取 session cookie,或用 curl -c/-b 先 POST 登录再请求 |
load_path.py 无法执行,引用 ~/repos/sage/ 路径 |
模块的 load_path.py 是为 Sage 写的,但部署在 pipeline-app 上,set_role_perm.py 和 py3/bin/python 路径都不对 |
不用 load_path.py,直接用 SQL 手动注册 /** 通配符权限。详见 references/rbac-manual-registration.md |
| 新模块已部署(wwwroot + Python包 + 菜单)但页面 403 | RBAC 权限从未注册,permission 表无该模块路径 | 检查 SELECT * FROM permission WHERE path LIKE '/模块名%',若为空则注册 /** 通配符到 superuser + logined 角色 |
RC4 password_encode 加密后无法解密——API key 应明文存储 |
appPublic.rc4 的 password() 和 unpassword() 不对称:加密时使用了随机 salt,解密时固定 salt 不匹配,导致 password_encode(v) 存储后 password_decode 无法还原。llm 表的 api_key 若经 RC4 加密则永久不可用 |
add_llm.dspy 和 update_llm.dspy 中移除 password_encode 调用,api_key 明文存储。_call_llm 和 llm_bridge.py 中直接使用 model_info.api_key,不解密。已存在的 RC4 密文用 UPDATE llm SET api_key='' WHERE api_key LIKE 'QUZV%' 清除 |
| wwwroot/pipeline_task 是普通目录而非符号链接,新 API 文件 500 "invalid path" | build.sh 未创建 symlink 或之前手动 mkdir 覆盖。所有模块 wwwroot 必须是 symlink 指向 pkgs/ 下对应模块的 wwwroot | rm -rf wwwroot/pipeline_task && ln -sf pkgs/pipeline-task/wwwroot wwwroot/pipeline_task。验证:ls -la wwwroot/pipeline_task 应显示 -> 符号链接 |
| curl 登录返回 "Password is required" | login DSPY 字段名是 passwd 不是 password。Form data 必须用 passwd 字段 |
curl -d "username=admin&passwd=xxx" 而非 password= |
RBAC INSERT 报 Unknown column 'permtype' |
pipeline-app 的 permission 表只有 8 列(id,name,description,ptype,parentid,path,title,icon),无 permtype 列 |
用 DESCRIBE permission 确认列名后对齐;INSERT 只写 id,name,ptype,parentid,path,title |
| CRUD Tabular 新增/编辑 500 | json/<table>.json 中的 new_data_url 等 URL 与 xls2ui 实际生成的文件名不匹配。xls2ui 生成 add_<table>.dspy 但 JSON 可能写 llm_create.dspy |
改源 JSON,不手改生成文件。 修正 json/<table>.json 中的 URL → 运行 bash build.sh 让 xls2ui 重建。xls2ui 命名规则:add_<tbl>.dspy / delete_<tbl>.dspy / update_<tbl>.dspy / get_<tbl>.dspy |
| 重启后页面仍 401 但权限已写入 DB | RBAC 权限缓存在 Redis(db 3),TTL 300s | redis-cli -n 3 FLUSHDB 刷新缓存后重试 |
| 新进程启动但页面行为异常 | 旧部署路径残留进程仍占用同一端口(两个 PID 同时 LISTEN) | lsof -i :<port> 检查所有进程,kill 旧 PID 后单进程重启 |
| 动态内容 urlwidget 加载失败或显示空白 | 用 Jinja2 .ui 模板通过 {% %} 预渲染数据,但 bui 处理器不支持服务端 Jinja2 |
创建 DSPY 端点返回 Bricks widget JSON,urlwidget 指向 /api/xxx.dspy 而非 .ui 文件 |
| 用户密码验证始终失败 | password_key 硬编码错误值;或误用 AES 替代 RC4 编码 | 必须从服务器 config 读取 key:getConfig(".", {"workdir": "."}).password_key;用 appPublic.rc4.password(s, key=key) 编码 |
| 根页面需要新菜单/卡片 | 根 wwwroot/index.ui 的 Menu widget 同时驱动侧栏和主页面卡片 |
在 Menu items 数组中添加新条目即可,自动生成侧栏菜单项+主页卡片 |
| Bricks 无独立输入组件 | Form 是唯一用户输入机制,不存在独立的 textarea/input widget | 聊天式 UI 用最小 Form(仅 textarea + hide 字段),自定义按钮放 Form 外,type:\"submit\" 按钮关联 Form |
| Browserbase session-ticket 不兼容 | Browserbase 后台每个 navigation 创建新 browser context,不保留 cookies;session-ticket 认证的页面始终 401 | 浏览器测试仅用于验证公开页面;需认证的页面通过 SSH 上的 curl 验证(先 curl -c 登录再 -b 请求),最终验证用真实浏览器 |
Bricks Form 的 code 下拉框取值失败——model_id 始终为空 |
form_element.querySelector('[name=model_id]') 找不到 Bricks code widget 渲染的 select 元素,因为 code widget 的 DOM 不设置标准 name 属性。form_element.querySelector('select') 也未必能找到 |
用 document.querySelector 直接从页面取:`var el=document.querySelector('#model_selector select') |
_select_model 按 id 查不到模型——回退到第一个 |
前端下拉框传的是模型 name(如 qwen3.7-max),但 _select_model 只查 WHERE id=${lid}$ |
改为 WHERE (id=${lid}$ OR name=${lid}$) 同时匹配 id 和 name |
sqlor LIMIT ${max_msgs}$ → MySQL 1327 "Undeclared variable" |
sqlor ${param}$ 语法处理纯数字值时替换为 $N$,MySQL 将其视为 session 变量声明 |
改用 Python f-string 直接插值:f\"SELECT ... LIMIT {max_msgs}\"。不要在 LIMIT 子句中使用 sqlor 变量语法 |
pipeline_submit undefined in DSPY — "name 'pipeline_submit' is not defined" |
pipeline_service 模块未安装到 Python 路径。load_pipeline_service() 的 import 静默失败返回空 stub,所有注册到 ServerEnv 的函数都不可用 |
cd pkgs/pipeline-service && ../../py3/bin/pip install -e . 安装可编辑包。重启后日志应出现 [pipeline_service] v3.1.0 loaded |
pip --force-reinstall 破坏 sqlor DBPools 类 |
强制重装 pipeline_service 可能触发 sqlor 依赖滚动更新,导致 DBPools 失去 EventDispatcher 父类——所有 DB 操作出现 has no attribute 'bind' / NameError: EventDispatcher |
手动恢复 sqlor/dbpools.py:(1) 加 from appPublic.event_dispatcher import EventDispatcher (2) class DBPools(EventDispatcher): (3) __init__ 中加 EventDispatcher.__init__(self)。详见 references/pip-force-reinstall-sqlor-corruption.md |
_get_db() 返回空 databases——executor 找不到步骤定义 |
DBPools() 单例在某些模块加载时序下 databases 属性为空,导致 sqlorContext('pipeline') 连接失败或返回空结果。storage.py 和 executor.py 使用独立的 _get_db() 不走 DSPY 的 get_sor_context |
在 _get_db() 和 _get_task_raw() 中加检查:if not db.databases: from appPublic.jsonConfig import getConfig; config = getConfig(); if config.databases: db.databases = config.databases |
Cockpit 直接调 _call_llm 绕过产线执行器 |
驾驶舱在 new_task handler 中直接调用 LLM 获取文字回复,没有走 pipeline_submit → start_task → executor → 步骤执行 → 交付件 的完整流程。用户看到的不是「产线驱动开发」而是「LLM 对话」 |
cockpit 通过 pipeline_submit() 提交任务,executor 自动按步骤执行。不要在 cockpit 中直接调 _call_llm 做任务执行。详见 references/cockpit-executor-pipeline.md |
DSPY msg_id UnboundLocalError on LLM timeout |
async with DBPools().sqlorContext() 块内 LLM 调用超时导致块提前退出,块内定义的 msg_id 未赋值,return 引用崩溃 |
在 async with 前初始化 msg_id = '' 和 agent_reply = ''。详见 references/cockpit-dspy-common-issues.md |
| dspy 端点 500 修完一层又爆一层(编译错 → 运行时 NameError) | 多层错误:编译层 SyntaxError(f-string 引号被写成 f\"...\",报 unexpected character after line continuation character)掩盖运行时层。pipeline-app globalEnv 只注入 json/DBPools/curDateString 等,不注入 os/uuid/base64 —— dspy 用 os.listdir 必须文件头显式 import os |
日志行尾 cost 分层:~0.004s=编译失败,秒级=运行时错误(每修一层看新请求 except 是否露出下一层)。修 NameError 时全文件扫裸名一次修完。免登录冒烟:curl 该端点返回 401=编译已过到鉴权层,500=仍崩。实证 2026-08-04 cockpit_chat.dspy。详见 sage-dspy-development 技能 references/dspy-file-corruption.md §多层错误 |
new_task 创建后 start_agent 找不到任务 |
任务创建用 tenant_id=org_id,查询用 tenant_id=project_id——两字段值不同 |
v3.2 统一约定:角色任务的 tenant_id=project_id(role_agent_run 第一参数即 project_id,见 references/role-agent-question-loop.md)。主agent写任务和角色认领必须用同一个值 |
ROLE_PROMPT.format(...) KeyError 崩溃——JSON 数据含花括号 |
用 str.format() 把任务 params(JSON 字符串,含大量 {})注入 prompt,format 把 JSON 花括号当占位符解析 |
LLM prompt 模板一律用哨兵占位符 .replace('__ROLE__', role),不用 .format()。同理注入用户内容/待答问题的 prompt 也用 .replace() |
| 角色agent认领不到任务(静默 idle) | 前端/默认 role 写 'developer',认领 WHERE 匹配规范名 'develop' |
角色名经 ROLE_ALIASES 归一化(agent_loop._normalize_role)。cockpit_agent.dspy 默认 role 改 'develop';新加角色入口必须走归一化 |
| bricks.getWidgetById('id') 报 TypeError: Cannot read properties of undefined (reading 'dom_element') | 新版 bricks.js(line 2680)的 getWidgetById 不再有默认 from_widget 兜底。脚本中只传一个参数时 from_widget 为 undefined,get_by_id 访问 fromw.dom_element 崩溃。(旧版 getWidgetByIdOld 有 from_widget = bricks.Body 默认值,新版移除了。) |
所有 getWidgetById 调用必须传第二参数:搜全局树用 bricks.getWidgetById('xxx', bricks.app);搜子树用合适的容器 widget;搜兄弟节点也必须从共同祖先(通常 bricks.app)向下搜,因为 downward=true 只看子树,closest(- prefix)只看祖先,都不覆盖兄弟。 |
| 新表只在 service 的 DDL 里,sdlc 部署后缺表 | pipeline-service 和 pipeline-sdlc 共用 pipeline 库(get_module_dbname('pipeline-sdlc') 返回 'pipeline'),但各仓库有自己的 mysql.ddl.sql,新表只加到一个仓库 |
新表建表语句必须同时写入两个仓库的 mysql.ddl.sql(如 pipeline_agent_questions)。存量库还需 ALTER 迁移(先 SHOW COLUMNS 幂等检查) |
aes_decode_b64 导入失败——不在 rc4 模块 |
DB 密码解码函数在 appPublic.aes,不在 appPublic.rc4(rc4 里是用户密码的 password/unpassword)。key 也不能用字面量 'password_key' |
from appPublic.aes import aes_decode_b64; pwd = aes_decode_b64(getConfig().password_key, encrypted)。rc4=用户密码,aes=DB密码,两套函数别混 |
add_startup(coro()) → TypeError: 'coroutine' object is not callable |
add_startup 期望一个返回协程的可调用对象(函数),不是协程对象本身。add_startup(_role_poller()) 调用了 async 函数,把协程对象传进去了 |
传函数引用而非调用结果:add_startup(_role_poller),不加括号。ahserver 会在启动时调用它生成协程 |
| on_startup handler 阻塞 aiohttp 启动——HTTP 000 / 永不监听端口 | aiohttp on_startup 信号会 await 每个注册的 handler。handler 里有 while True: ... await asyncio.sleep(10) 无限循环会永久阻塞服务器启动 |
on_startup handler 只能 spawn 后台任务,不能内联执行:async def _role_poller(app): asyncio.create_task(_poll_loop())。_poll_loop 含无限循环,_role_poller spawn 后立即返回 |
_role_poller() takes 0 positional arguments but 1 was given |
aiohttp 的 app.on_startup.append(c) 会给每个 handler 传入 app 实例。函数签名少了一个参数 |
签名必须是 async def _role_poller(app):,即使不实际使用 app 参数 |
| shell_exec 返回 [Errno 13] Permission denied | 传入的 workdir(如 /d/pipeline/workspaces)在沙箱检查通过但目录本身不存在 → create_subprocess_shell(cwd=...) 打开失败 |
用 _resolve_workdir() 自动回落:检测 /d 可写性,不可写时用 /tmp/pipeline_workspaces。shell_exec 内部也对传入 workdir 做 os.path.isdir 检查,不存在时自动回落 |
devops 路由执行 Cloning: command not found |
LLM 分类器的 description 字段是自然语言("Cloning into..."),不是可执行 shell 命令;或消息含中文前缀"执行命令:" | cockpit_chat devops handler 直接用 message_text 作命令(`re.sub(r'^(执行命令[::] |
新建 dspy 端点 500: NameError: name 'dbname'/'uid' is not defined |
独立 .dspy 文件没有 dbname/uid 自动注入。每个 .dspy 是独立执行单元,不会继承其他文件的顶层变量 |
dspy 端点 boilerplate 三行头:uid = await get_user() → dbname = get_module_dbname('pipeline-sdlc') → async with DBPools().sqlorContext(dbname) as sor:。参考 cockpit_stats.dspy 的写法 |
模块 load_path.py 实现模式
模块的 scripts/load_path.py(示例:pipeline_core/scripts/load_path.py):
#!/usr/bin/env python3
"""Pipeline core module RBAC permission registration"""
import subprocess
MOD = "pipeline_core"
PATHS_ANY = [
f"/{MOD}/pipeline.css",
f"/{MOD}/pipeline.js",
]
PATHS_LOGINED = [
f"/{MOD}/",
f"/{MOD}/index.ui",
f"/{MOD}/api/pipelines_create.dspy",
f"/{MOD}/api/pipelines_update.dspy",
# ... 其他 API
]
def register_paths():
for path in PATHS_ANY:
subprocess.run(["py3/bin/python", "set_role_perm.py", "any", path])
print(f" any: {path}")
for path in PATHS_LOGINED:
subprocess.run(["py3/bin/python", "set_role_perm.py", "logined", path])
print(f" logined: {path}")
if __name__ == "__main__":
print(f"=== {MOD} RBAC registration ===")
register_paths()
print("Done.")
应用根目录的 set_role_perm.py wrapper(~/test/pipeline-app/set_role_perm.py):
#!/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 asyncio
sys.path.insert(0, '.')
from rbac.set_role_perms import set_role_perm as _set_role_perm
from appPublic.jsonConfig import getConfig
from sqlor.dbpools import DBPools
async def main(role, path):
parts = path.strip('/').split('/')
module = parts[0] if parts else 'app'
config = getConfig('.', {'workdir': '.'})
db = DBPools(config.databases)
await _set_role_perm('pipeline', module, '*', role, path)
if __name__ == '__main__':
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <role> <path>")
sys.exit(1)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(main(sys.argv[1], sys.argv[2]))
loop.close()
执行流程:
- 在 app root 执行:
py3/bin/python pipeline_core/scripts/load_path.py - load_path.py 调用:
py3/bin/python set_role_perm.py logined /pipeline_core/index.ui - wrapper 调用
rbac.set_role_perms.set_role_perm()写入数据库
RBAC 权限注册(通配符模式)
用 /** 通配符一次覆盖模块所有路径,避免逐个注册:
-- 1. 创建模块通配符权限
INSERT INTO permission (id, name, ptype, parentid, path, title)
VALUES (getID(), '/pipeline_core/**', 'page', root_perm_id, '/pipeline_core/**', '/pipeline_core/**');
-- 2. 分配给 superuser 角色
INSERT INTO rolepermission (id, roleid, permid) VALUES (getID(), 'owner.superuser', perm_id);
-- 3. 静态资源分配给 any 角色(免登录访问)
-- /bricks/**, /i18n/** → any 角色
-- /rbac/userpassword_login.ui → any 角色
any 角色:所有未登录用户的默认角色,注册静态资源和登录页面。 logined 角色:所有登录用户自动拥有,注册已登录通用页面。
根入口页面(index.ui)
独立应用需要在 wwwroot/index.ui 创建主入口页面,包含:
- 顶部导航栏(Menu 组件,链接各模块)
- 模块卡片(ResponsableBox + VBox.card,点击进入各模块)
- 用户按钮(右上角)
模板参考:references/root-index-template.md
bricks 前端构建
cd ~/test/pipeline-app/pkgs/bricks/bricks && bash build.sh
构建产物在 pkgs/bricks/dist/,符号链接 wwwroot/bricks → pkgs/bricks/dist/。
如果 bricks.js 404,检查 dist 目录是否已生成。
⚠️ 铁律:wwwroot/bricks 必须是软链接指向 pkgs/bricks/dist,绝不能是实体目录(build.sh 第43-44行 rm -rf wwwroot/bricks && ln -sf pkgs/bricks/dist wwwroot/bricks)。
实体目录里若放了 pkgs/bricks/bricks/ 的源文件 bricks.js(~23KB,缺 Layout/JsWidget 等类定义),前端直接崩:
bricks.js:684 Class extends value undefined + bricks.App is not a constructor → 页面空白。
诊断三件套:ls -la wwwroot/ | grep bricks(有无 ->)、wc -c wwwroot/bricks/bricks.js(编译版 ~424KB vs 源版 ~23KB)、对比用户报错行号与服务器该行内容是否吻合(不吻合=浏览器缓存旧文件)。
模块目录结构(部署时)
~/test/pipeline-app/
├── app/pipeline_app.py # 启动入口
├── conf/config.json # 配置(含加密密码)
├── wwwroot/ # 统一 wwwroot
│ ├── pipeline_core → ../pipeline_core_repo/wwwroot
│ ├── pipeline_ops → ../pipeline_ops_repo/wwwroot
│ ├── pipeline_dist → ../pipeline_dist_repo/wwwroot
│ ├── pipeline_task → ~/repos/pipeline-task/wwwroot
│ └── bricks → ../bricks
├── pipeline_core → pipeline_core_repo/pipeline_core/ # Python 包
├── pipeline_ops → pipeline_ops_repo/pipeline_ops/
├── pipeline_dist → pipeline_dist_repo/pipeline_dist/
├── pipeline-service → ~/repos/pipeline-service # 符号链接到 repos
├── pipeline-task → ~/repos/pipeline-task
├── pipeline_core_repo/ # 完整仓库(含 wwwroot, models, json)
├── pipeline_ops_repo/
├── pipeline_dist_repo/
├── init_data.py # 初始化脚本
├── build.sh
├── start.sh
└── stop.sh
关键:Python 导入路径是 pipeline_core/(符号链接到内层包),静态文件在 wwwroot/pipeline_core/(符号链接到 repo 的 wwwroot)。不要混淆。