62 KiB
Raw Blame History

name version description trigger_conditions tags
pipeline-app-module 2.0.0 产线生态系统 — 通用执行引擎、产线定义/运营/分销/交互的完整开发规范和部署指南。
用户要求开发或修改产线功能
涉及 pipeline_core/pipeline_ops/pipeline_dist/pipeline_service/pipeline_task 模块
需要添加新的产线类型、步骤处理器或分销功能
讨论产线架构设计(Hermes验证→固化→独立运行)
pipeline
module
independent-app
distribution
execution-engine

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,支持版本)

引擎工作原理:

  1. 提交 → 读取 pipeline_steps 表步骤定义 → 创建 task_steps 记录 → 启动执行
  2. 执行循环 → 解析 DAG 依赖图 → 找可执行步骤 → 调用 handler → 存 artifact → 继续
  3. 多租户 → 所有查询按 tenant_id 隔离
  4. 人工干预 → 修改 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_restart
  • references/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_model name/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 合法)。裸名可用性以 grep globalEnv.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_config JSON 中({"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次) → 仍不达标暂停等待人工决策

实现模式:

  1. 创建评估函数(app/eval_*.py):
async def evaluate_xxx_quality(output: dict) -> Tuple[bool, float, str]:
    """评估函数返回 (passed, score, reason)"""
    # 多维度评估逻辑
    # 返回: (是否通过, 0-1分数, 原因说明)
    return True, 0.85, "各维度评估良好"
  1. 包装 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)
  1. 注册到 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):

后端优先级:

  1. harnessed_agent.llm_chat(ServerEnv 加载时)
  2. 数据库 llm 表 — 按 model name 查 api_base/api_key/model_id,有内存缓存
  3. 环境变量 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 导航契约(强制 — 用户明确要求)

三层导航层次,统一交互,不混用:

  1. 系统菜单 → TabPanel:侧边栏 Menu 的系统菜单项(产线管理/开发产线/运营/分销/任务中心/审计日志/远程空间等),点击用 this.open_tab({name,label,url,removable:true}) 在 app.main_content(TabPanel)打开为可切换标签页。参考 index.ui 侧边栏 sidebar_menu 的 script 写法。
  2. 用户菜单 → 弹窗:Header 右上角 👤 用户菜单(userinfo.ui 点击 target:"Popup" + popup_options),弹窗加载 user_menu.ui,不进 TabPanel。
  3. TabPanel 内容中的点击 → 弹窗:TabPanel 打开的模块页面内,点击按钮/链接一律弹窗(PopupWindow / Popup),不在 TabPanel 内再嵌套 tab,也不整页跳转破坏 tab 结构。

目的:顶层用 TabPanel 保持多任务上下文;用户级/次级操作一律弹窗(临时、可关闭),不污染 tab 列表。写任何 .ui 的 bind/script 前先套这三条。

DSPY 职责边界(dspy 简单 + 复用 py)

用户原则:dspy 要简单,多个 dspy 不能重复实现同一个功能,否则就写在 py 里。

模式:

  1. 重复逻辑抽到 .py 的统一函数(如 workspace.py 的 get_workspace_dir/get_workspace_base)
  2. load_xxx() 里 g.fn = fn 注册到 ServerEnv(否则 dspy 调用 NameError 500)
  3. 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:

  1. target: "self" 时 script 中 this 就是 Tabular 自身,不要用 bricks.getWidgetById('xxx') —— toolbar 按钮在 Tabular 内部,getWidgetById 从按钮向下搜索找不到祖先 Tabular,报 Cannot read properties of undefined (reading 'dom_element')
  2. promise .then() 回调中 this 丢失,必须用 .bind(this) 才能调用 this.render({})

开发工作流(强制)

铁律:pipeline-app 测试 = tokentest,禁在本机另搭环境。

  1. 所有代码修改必须先在 /d/ymq/pipeline/pipeline-app/ 或对应模块仓库(/d/ymq/pipeline/<module>/)完成
  2. git commit 后立即 git push
  3. SSH 到 tokentest 同步部署:ssh apitest@120.48.168.15 → cd /d/apitest/pipeline-app && git pull(伞仓);各模块 cd /d/apitest/pipeline-app/pkgs/<module> && git pull
  4. Python 代码变更后必须 pip install --upgrade:cd /d/apitest/pipeline-app && source py3/bin/activate && pip install --upgrade /d/apitest/pipeline-app/pkgs/<module>
  5. 重启:pkill -9 -f pipeline_app && sleep 2 && cd /d/apitest/pipeline-app && bash start.sh
  6. 验证:https://pipeline.opencomputing.cn/

服务器直接修改回传流程(违规补救)

当发现测试服务器上有直接修改(git status 显示 modified/untracked),必须按以下流程补救:

  1. 先 git pull 本地确认拿到远程最新
  2. scp 服务器改动回本地:scp apitest@120.48.168.15:/d/apitest/pipeline-app/pkgs/<module>/<file> /d/ymq/pipeline/<module>/<file>
  3. 本地 git add -A && git commit && git push
  4. 服务器清理:rm -f 冲突的 untracked 文件 → git stash && git pull && git stash drop
  5. 验证 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/。

  1. 严禁随意修改部署环境文件。用户原话:「为什么以前各个模块都能用的界面,现在都不能用了,这些你都可以随便改来改去的吗?」—— 每次改动必须有根有据,先查后改,不准盲目创建文件、改 symlink、或覆盖远程文件。
  2. 修复前四步确认:(1) git log 看最近变更 (2) ls -la 确认文件现状 (3) 对比 build.sh 确认构建流程 (4) 用 git show <commit>:<path> 找回丢失的文件。
  3. 修改 → commit → push → 服务器 git pull → 重启 → 浏览器测。不允许跳过任何一步。
  4. curl 200 ≠ 功能正常。必须浏览器端完整链路验证。
  5. 服务器文件改动前先告知用户。不要静默 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()

执行流程:

  1. 在 app root 执行:py3/bin/python pipeline_core/scripts/load_path.py
  2. load_path.py 调用:py3/bin/python set_role_perm.py logined /pipeline_core/index.ui
  3. 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)。不要混淆。