--- name: module-development-spec version: 1.0.0 description: 开发「业务模块」(Python 包)时必读——模块目录结构(包目录=模块名,非 src)、模块无独立 app.py/端口/Dockerfile/build.sh(那些是应用级的,模块带 app.py 就是错的)、取库名用 get_module_dbname 禁止硬编码 DBNAME、models/json/dspy 四段式。开发「应用脚手架」(app/{应用名}.py + conf/config.json + build.sh)时不要用本技能,改用 web-application-spec。 trigger_conditions: - 开发业务模块(Python 包)时加载我,否则会产出带 app.py 的错误模块结构 - Task involves implementing a component with bricks-framework frontend and sqlor database backend - Development follows the documented directory structure with wwwroot, models, json, and init directories - Task involves creating read-only/dashboard modules that display data without CRUD (no models/json needed) --- ## Work Logs / Delivery Archive For production module/application development, update a dated work log (use the REAL current system date; late-night follow-up fixes go under their real date or are labeled next-day follow-ups) before finishing, e.g. `docs/work-log-YYYY-MM-DD.md`. Entry should include: scope/background + repo/module name; timeline/commit list; key technical decisions and pitfalls; verification performed (incl. environment-limited checks that couldn't run); current branch/commit state. Do NOT mark a task done without this archive when deliverables must be retained. This skill defines the complete workflow for standardized modules: ahserver ecosystem + bricks-framework frontend + sqlor database backend. ## Module Philosophy — Host-Agnostic - A module is a self-contained unit ANY host can load (pipeline-app / sage / future apps). It depends ONLY on: ① foundation packages (sqlor, ahserver ServerEnv, appPublic utilities); ② its own data tables (or shared tables like sage's appcodes); ③ other modules it explicitly imports. - It does NOT depend on any host entry point, host-specific config files, or host wwwroot paths. - `load_{module}()` is the ONLY integration point — it registers functions to ServerEnv; the host decides how to wire it in. - **A module is NOT an independently deployable unit.** It has NO standalone `app.py` entry point, NO own port, NO own Dockerfile/service, NO own deploy script. It only runs because a host application calls `load_{module}()`. A module that carries its own `app.py` / port / deploy script is wrongly built — fix the module, don't deploy it separately. - **Interaction-layer modules** (e.g. pipeline-task) have NO data tables — pure thin wrappers calling other modules' functions via ServerEnv; they provide .dspy + .ui only. ## 模块本地仓库位置 在 pipeline 产线机构工作空间中,模块本地仓库在机构 `modules/{模块名}/`(见 project-directory-spec)。模块是应用内的功能模块(Python 包),不是独立部署单元。 **部署测试前必须给模块仓库设置远程仓库**(`git remote add origin <远程地址>`),否则部署时无法 git pull 拉取最新代码。 ## Directory Structure ``` mymodule/ # module root (replace with actual name) ├── mymodule/ # Python package: __init__.py (required), init.py, *.py ├── wwwroot/ # frontend: .ui / .dspy / .js / .css ├── models/ # {tablename}.json database table definitions ├── json/ # {alias}.json CRUD operation definitions ├── init/ # data.json / data.yaml initial data ├── scripts/ # supporting scripts (load_path.py RBAC registration) ├── skill/SKILL.md # MANDATORY agent-facing spec (data model, endpoints, pitfalls) ├── pyproject.toml # Python packaging └── README.md # module documentation ``` ## Core Implementation Requirements ### 1. Module Initialization (init.py) - Purpose: register ALL module functions with ServerEnv so .ui/.dspy can call them directly. NOT for route registration (wwwroot files auto-routed via `/{module_name}/filename.ext`). - **CRITICAL: export functions in `__init__.py`** — all async functions defined in init.py MUST be imported in the package `__init__.py`; otherwise dspy calls fail `NameError: name 'xxx' is not defined`. - **Pitfall: Triple-place function registration** — adding/removing a function requires updating THREE files in sync: ① implementation (`mymodule/mymodule.py`); ② `__init__.py` import line; ③ `init.py` `env.xxx = xxx` in `load_{module}()`. Missing ② → ImportError/AttributeError at init; missing ③ → NameError in .dspy/.ui; removed from ① only → ImportError at load. Cleanup check: `grep -rn 'function_name' mymodule/ --include='*.py'` must hit exactly those three places. - **Pitfall: CRUD dspy wrappers use PLURAL table names** — xls2ui generates wrappers (`add_suppliers.dspy`) calling `create_suppliers/update_suppliers/delete_suppliers`, while init.py usually defines singular names (`create_supplier`). Register BOTH: `env.create_supplier = create_supplier; env.create_suppliers = create_supplier` (same for update/delete). Symptom: CRUD "add" returns 500 `function not found`. - **Pitfall: Subagent delegation for module creation — ALWAYS validate after** (subagents consistently err even with correct specs; observed 2/3 failure rate). Consistent errors: ① init.py/__init__.py misplaced at module ROOT instead of package dir; ② model JSON `{"table":...,"fields":{...}}` instead of `{"summary":[...],"fields":[...],"indexes":[...],"codes":[...]}`; ③ CRUD JSON `{"table":...,"list":{...}}` instead of `{"tblname":...,"params":{"browserfields":{...},"editable":{...}}}`; ④ hallucinated sqlor APIs (`sqlor.save/list/one/delete/insert/query` DO NOT EXIST — only `sor.C/U/D/R/I/sqlExe`). Mandatory validation: move misplaced files to package dir; check model JSONs have `summary` (array primary); check CRUD JSONs have `tblname` + `params.editable`; grep for fake sqlor APIs → fix to sor.C/U/D; dspy audit (no imports/print/uuid); py_compile all .py. See `references/bulk-module-creation-pattern.md` "Subagent Validation Checklist". - **Pitfall: `debug()` in .dspy has NO filename** — logs show `[sage][debug][:N]` (framework injects dspy code into a `` context). Always prefix manually: `debug(f'product_category_create.dspy: START params_kw={dict(params_kw)}')` — essential when several dspy run in sequence (add → refresh). - **Pitfall: DSPY API handlers must forward ALL client params — never hardcode dispatch fields** — e.g. read `task_type = params_kw.get('task_type', 'separate')` and forward `output_dir` if present; do NOT build `{'task_type': 'separate', ...}`. Symptom: client sends `separate_full` but worker log shows `separate`; hours to trace because the worker is correct — the DSPY silently drops the param. Audit: grep API dspy for hardcoded `'task_type'`/`'mode'` dispatch strings inside payload construction. - **Pitfall: py_compile is INVALID for .dspy files** — dspy is injected into an async function at runtime (top-level `return`/`await` are correct); py_compile gives false positives (`'return' outside function`). Use py_compile only for .py; validate .dspy with the dspy audit script (grep-based). - **CRITICAL: dspy MUST use explicit `return`** — ahserver wraps code in `async def myfunc(request, **ns):` and awaits it; a bare `result` expression returns None. See `references/dspy-execution-and-module-structure.md` (execution model, pre-loaded globals, Tree widget data format, module symlink patterns). - **Pitfall: Sage startup requires environment variables** (e.g. `ALIPAY_PUB`) read at import time in `app/sage.py` — cannot full-start locally (FileNotFoundError). Validate with py_compile (.py) + dspy audit instead of startup. - **Pitfall: Raw SQL column names — ALWAYS verify against models/{table}.json, never guess.** Check first: `python3 -c "import json; cols=[f['name'] for f in json.load(open('models/table.json'))['fields']]; print(cols)"`. Known wrong guesses → correct: permission `permcode/permname` → `path/name`; users `created_date` → `created_at`; organization `org_name` → `orgname`. - **Pitfall: `{{id}}` in uapi response templates resolves to Python built-in `id()`** — renders `` instead of the field. NEVER use `{{id}}`; map to a different name: `'{"taskid":"{{taskid}}","status":"{{status}}"}'`. Affects any uapi whose upstream response has an `id` field. - **Pitfall: uapiio `input_fields` MUST be a JSON array** — bricks.js LlmIO calls `this.input_fields.forEach(...)`; an object `{"field":{...}}` throws `TypeError: this.input_fields.forEach is not a function`. Use array form: `[{"name":"prompt","label":"用户输入","uitype":"text","required":true}]`. Canonical reference: existing `ktv_asr_transcribe_io` record. - **Pitfall: `json.dumps()` in uapi data templates → `data=None`** — `{{json.dumps(prompt, ensure_ascii=False)}}` can render silently to None (logs `body=None`). Use plain string interpolation `"{{prompt}}"` and HARDCODE the upstream model ID (`"model":"hy-image-v3.0"`), don't pass `{{model}}`. Source real model IDs from provider `/v3/models` (ARK) or `/v1/models` (tokenhub). - **Pitfall: `return` inside `async with` → silent NoneType** — returning inside `async with db.sqlorContext(...) as sor:` returns None → `return data type error, `. Collect results inside the block, `return` AFTER it exits. Minimal repro: `return {'ok': True}` inside async with also fails. - **Pitfall: `sor.C()` silently drops records when `created_at` missing** — sqlor does NOT auto-set timestamps; with `created_at TIMESTAMP NOT NULL` the insert silently vanishes. Always set `ns['created_at'] = curDateString()` (and `ns['org_id'] = (await get_userorgid()) or '0'`). Symptom: dspy returns `{"success": true}` but SELECT returns zero rows. - **Pitfall: Bash heredoc `\$` escapes corrupt sqlor placeholders** — `cat > f.dspy << 'EOF'` with `\${pid}\$` writes the backslash literally; sqlor fails silently on the unrecognized placeholder. Never escape `$` in single-quoted heredocs — write `${pid}$` as-is. Verify: `grep 'project_id' file.dspy | cat -A` must show no `\$`. - **Pitfall: Do NOT name module functions `get_module_dbname`** — that name is already a ServerEnv-provided global used by .dspy; registering it overwrites the global. Use a private prefix (`_get_dbname()`). ### 2. Frontend Development - **Mandatory**: bricks-framework for all frontend; .ui files as PURE JSON (NOT HTML/CSS) in wwwroot/; auto-routed `/{module_name}/filename.ext` — no manual route registration. See `references/bricks-ui-pitfalls.md` (id at widget level not options, Button click, Popup/Form patterns, script actiontype Jinja2 limits). - **ahserver auto-serves .css/.js from wwwroot/** — DO NOT add ``/`