Build Form(动态表单构建器)
读取 form-spec.md(新增)或 templateId(修改),构建表单配置并提交平台。
> ⚠️ 本技能是远程配置操作,不是代码开发任务。 通过 HTTP API 在远程 PowerData 平台上创建/修改表单配置数据,与当前项目源码完全无关。
🔴 HARD-GATE:新增必须 spec,修改必须 templateId
> [!important] 硬约束,不可协商
> 【新增入口】唯一输入是 form-spec.md。禁止从用户口语/图片直接推断并构建表单。
> 用户没给 spec/业务名 → 停止,引导去 pacp-design-form。
> 【修改入口】唯一输入是 templateId + 修改意图。必须有 templateId 才能改。
> 两条入口都不接受"凭口语直接构建"。
反合理化表(AI 跳过 spec 的借口及反驳):
| AI 内心独白(Red Flag) | 反驳 |
|---|---|
| "这表单就几个字段,直接生成了吧" | 几个字段也要 spec——字段类型/必填/布局没确认,生成必跑偏 |
| "用户描述很清楚了,我直接构建" | 口语清晰 ≠ spec 完整。代码集编号?主键?布局范式?没确认就是赌 |
| "我先建个草稿再让用户改" | 草稿赌错方向,用户改的成本 > 先出 spec 对齐的成本 |
| "spec 阶段太慢,用户等不及" | spec 是一次性对齐,构建是落子无悔。慢在前面省在后面 |
| "用户要加外部数据对象,信息不太全但我先配上" | dataObjects 修改走门禁——缺数据来源(QUERYXH/数据源表)或缺取值方式就停,不靠推断硬配。配完无效比慢更糟 |
统一工作根目录:.pacp/
> [!important] 所有 pacp-* skill 共用 .pacp/ 作为工作根目录
定位方式:AI 运行时的 cwd(当前工作目录)下的 .pacp/。
> [!warning] cwd 边界
> "别探查项目结构"约束依然成立——AI 只往 .pacp/ 读写工作文件,不触碰项目内其他任何文件。
{cwd}/.pacp/
specs/ ← form-spec.md(pacp-design-form 产出,本 skill 读取)
动态渲染/ ← design 产出的隔离子文件夹(优先查找)
{业务名}__表单.md ← 新命名(带后缀,优先匹配)
{业务名}.md ← 旧命名(无后缀,兜底兼容)
{业务名}__表单.md ← 旧 spec 兜底路径(根目录,兼容更早的历史 spec)
{业务名}.md
session.json ← 共享会话凭证(本 skill 启动时读取复用)
active/ ← 工作区(trace、构建脚本、payload)
{业务名}_{时间戳}/
> [!important] session 跨 skill 共享
> 启动时先读 .pacp/session.json,有效则直接复用;过期(401)则重新调 pacp-system-login 并更新。session.json 是可选缓存优化。
入口分流 + 文件存在性闸门
启动第一件事——按入口类型分流:
启动分流:
1. 修改入口(用户给了 templateId + 修改意图)
→ 走修改旁路:getFormConfig → 推断改动并生成 diff → 用户确认 → 提交
→ 不走 spec 闸门
2. 新增入口(默认)
→ 文件存在性闸门:
a. 用户给了业务名 → 按优先级查 spec 文件是否存在(命中即停,后者仅兜底):
① .pacp/specs/动态渲染/{业务名}__表单.md(新命名,design 产出,最优先)
② .pacp/specs/动态渲染/{业务名}.md(旧命名兜底,兼容迁移前的 spec)
③ .pacp/specs/{业务名}__表单.md(根目录兜底,兼容更早的历史 spec)
④ .pacp/specs/{业务名}.md(根目录旧命名兜底)
├─ 任一存在 → 轻量校验(三段存在)→ 有效则继续
└─ 都不存在 → 停止,引导去 pacp-design-form
b. 用户直接给了 spec 文件路径 → 读取,轻量校验
c. 用户什么都没给 → 停止,提示:
「需要先有规格文档。请用 pacp-design-form 生成。」
> [!note] 轻量校验
> 闸门只校验三段存在(元信息段存在、字段清单段存在且非空、布局段存在),不校验段内细节。段内细节由 design 的质量门保障。
> 用户手写 spec(未经 design 质量门)直接喂给 build 时,提示「未经 design 质量门校验,可能有缺项」但不拦截。
> [!note] spec 文件名兼容四级查找
> design 产出统一用 {业务名}__表单.md(与列表配置 {业务名}__列表.md 区分)。build 查 spec 时按 ①②③④ 四级优先级查找,覆盖新命名、旧命名、子文件夹、根目录四种组合,确保历史 spec 不失效。
> [!important] 修改模式为何跳过 spec
> spec 质量门是为“从无到有”的新增场景设计的。修改场景的输入是已有表单配置(结构化、事实基准),不需要绕一圈反推成自然语言 spec 再改回去。
>
> 例外:外部数据对象(dataObjects)修改走门禁。其他改动(字段、布局、events 脚本等)维持旁路不变,但涉及 dataobjectsmanagecontroller 系列 API 的修改(对应配置页「java外部数据对象」按钮)必须走下方「🔴 外部数据对象修改门禁」章节的四步强制流程——缺数据来源/取值方式就停,不靠推断硬配。
>
> 模糊改动意图的处理:收到模糊意图(如“重新排当事人信息区”)时,自己推断具体改动并生成 diff,直接展示给用户确认。猜错用户再纠正——一轮交互,效率优先,不反问澄清。
数据来源
- 数据来源只有三个:① 远程 API(表结构、字段元数据、代码集)② 本 SKILL.md ③ 用户提供(spec 或 templateId)。本地代码库里没有任何你需要的东西。
操作模式
- 新增:用户描述含"新增/添加/创建"等关键词
- 修改:用户描述含"修改/编辑/更新"等关键词,需提供
templateId
重要约束:远程配置任务,不读任何源码(含当前项目)
> 本技能是在远程平台上创建/修改配置数据,不是在本地项目里开发代码。禁止读取任何源码——既包括本 Skill 的 lib/scripts,也包括当前项目(cwd)里的业务代码。
本 Skill 的 lib/、scripts/ 目录下的 .js 文件是运行时代码,由 execute_code 或 CLI node 命令通过 require() 加载执行。禁止使用 read_file、cat、search_files 等工具查看这些文件的内容——所有 API 签名、参数说明、示例代码都在本 SKILL.md 中,无需阅读源码实现。
当前项目的业务代码同样不要读:即使 cwd 是某个项目目录,也不要执行 ls / rg --files / module_report / read_symbol / ast_grep / lsp_* 去探查项目结构或代码——这些操作与本技能无关,只会白白消耗 token 和时间。本技能的全部输入来源是:远程 API + 本 SKILL.md + 用户描述。
行为约束
| # | 约束 | 说明 |
|---|---|---|
| 1 | 只用 CLI 命令操作 | 所有操作必须通过本 Skill 提供的 CLI 命令(form_creator.js)或 BwaiClient API 完成。禁止自行编写新脚本绕过 CLI。如果 CLI 报错,直接将错误信息报告给用户,不尝试自行修复或绕过 |
| 2 | 临时文件路径 | 所有生成的文件(JSON 配置、探查结果、临时脚本、临时数据)必须写入 .pacp/active/{业务名}_{时间戳}/ 工作区目录内。禁止在其他位置创建文件——包括子仓库根目录、当前工作目录、/tmp 等。CLI 的 --config 参数必须指向工作区内的文件路径 |
| 3 | 表名必须用户明确提供 | 自动建表(--no-table)时,用户必须给出明确的表名。AI 不得自行编造表名,只能提供建议(如根据业务语义推荐命名规则) |
| 4 | 主动确认表名 | 如果用户没有明确提供表名,AI 必须主动向用户确认:是使用已有的表(需指定表名),还是创建新表(需给出新表名)。不得自行推测表名 |
| 5 | 公共代码集必须用户指定 | 涉及公共代码集(CODESET / commonCode)时,LYZ 必须由用户明确指定。禁止 AI 根据字段语义自行推断代码集编号——推断太慢且容易出错 |
| 6 | --no-table 模式下 type 必填 |
使用 --no-table 时,elements 中每个字段的 type 必须显式指定(如 INPUT、DATE 等),无法从数据库自动推断。缺少 type 会导致创建失败 |
| 7 | 主动交互等待 | 需要用户确认或提供信息时,优先提供选项让用户选择,使用主动提问的方式让用户明确知道 AI 在等待输入。如果有可用的提问/交互工具(如 clarify、ask_user、human_input、request_input 等),应通过工具发起而非纯文本等待。适用场景:预览确认、代码集编号确认、表名确认等 |
| 8 | 缓存主动失效 | CLI 会自动缓存表结构等查询结果(默认 7 天过期)。当 AI 执行了修改操作(新增/修改代码集、修改表结构等)后,应使用 --clear-cache --base-url <url> 清除对应缓存,确保后续查询拿到最新数据 |
| 9 | 子表必须有 PXH 列 | 使用已有表作为 GROUP 子表时,必须确认该表有 PXH(INT)列。平台加载子表数据时硬编码 ORDER BY PXH,缺少该列会导致编辑表单报错。--no-table 模式下平台自动创建,无需手动处理 |
| 10 | ID_GENERATOR 必须指定系统编号ID | 使用 ID_GENERATOR(ID 自动生成)组件时,seqNumber(系统编号ID)为必填项。系统编号有业务含义(前缀格式、流水号规则等),AI 不得自行编造,必须由用户提供或经用户明确确认。获取方式:(1) 查询已有编号 --list-seq-numbers,从返回的 SXBH 中选择;(2) 如果没有合适的,向用户确认编号名称(BHMC)、前缀格式(QZGS,如"PWXK-#YEAR#-")、流水号格式(LSHGS,如"000"=3位补零)后,通过 BwaiClient.createSeqRule() 创建,返回的 SXBH 填入 seqNumber |
| 11 | 外部数据对象全量覆盖 | 使用 BwaiClient.saveDataObjects(templateId, objects) 保存外部数据对象时,后端按 FORMBH 全量覆盖——传入什么就保留什么,未传入的会被删除。修改已有配置前必须先调用 getDataObjects(templateId) 取回现有列表,在其基础上增删改后再整体回写。QUERYXH(数据查询接口序号)必须由用户提供或从查询配置中确认,AI 不得编造。涉及 dataObjects 的修改完整流程见「🔴 外部数据对象修改门禁」章节(先查事实基准 → 不符则停 → 写增量 spec 硬门槛:数据来源 + 取值方式 → 合并回写) |
🔴 外部数据对象修改门禁
> [!important] dataObjects 修改的强制门禁(对应行为约束 #11 的完整流程)
> 修改模式默认走旁路,但涉及外部数据对象(dataObjects)的修改例外——必须走本章节的四步强制流程。这是为了避免实验已证实的真根因:编造 QUERYXH、配完无效(心智模型错位——dataObjects 只预加载到上下文,不改字段组件形态,要让字段可选需配套 DYNAMIC_DATA_SELECT)。
触发条件(二值判定)
判定规则:本次修改是否要调用 dataobjectsmanagecontroller 系列 API(savedataobjectsmanages / querydataobjectsmanages)。
- 是 → 触发门禁,走四步强制流程。
- 否 → 维持现状(现有修改旁路 getFormConfig→diff→确认→提交,不变)。
> 判定依据用 API 端点 + UI 入口(动态表单配置页面的“java外部数据对象”按钮)锚定,不用模糊术语,不靠 AI 判风险等级。可程序化。
四步强制流程
flowchart TD
A[修改请求] --> B{要调 dataobjectsmanagecontroller<br/>系列 API?}
B -->|否| C[走现有修改旁路<br/>getFormConfig→diff→确认]
B -->|是| D[门禁触发]
D --> E["【1】getDataObjects 查平台事实基准"]
E --> F{"【2】现状与作者预期不符?"}
F -->|是| G["🚫 停, 让作者确认<br/>不自动覆盖"]
F -->|否| H["【3】写增量 spec 片段<br/>硬门槛: 数据来源 + 取值方式"]
H --> I{"取值方式 = 配套选择器?"}
I -->|是| J["🚫 边界拦截<br/>dataObjects 单独无效<br/>引导走 DYNAMIC_DATA_SELECT"]
I -->|否, 默认值回填/脚本取用| K["【4】标注操作类型<br/>+ 合并事实基准"]
K --> L[全量回写 saveDataObjects]
> spec 定位:spec 是“意图描述 + 变更记录”,平台 DYNAMICFORM_DATAOBJECTS 表才是事实基准。保存前必须用平台现状校验意图的前提是否成立——不符就停。
增量 spec 片段结构
写增量 spec 片段时用以下结构(只写本次改动的部分,不反推整表 spec):
### 外部数据对象:{语义名}
- 目标表单:{表单名}(templateId: {uuid})
- OBJECT_NAME:{英文键名} ← 可 AI 推断
- 中文名:{OBJECT_NAME_CN} ← 可 AI 推断
- 数据来源:【硬门槛,二选一必填】
- [ ] 已有查询接口,QUERYXH = ____________
- [ ] 无现成接口,需先基于数据源表 ____________ 建查询配置
- 运行参数(PARAMS):无 / ____________ ← 可 AI 推断
- ⭐ 取值方式:【硬门槛,必填,见下方取值方式边界规则】
- [ ] 默认值回填:取首条记录字段 ___ 填入表单字段 ___
- [ ] 脚本取用:在 onDataLoaded 里读取 dataObjects.{OBJECT_NAME}(说明:___)
- [ ] 配套选择器:字段需改为 DYNAMIC_DATA_SELECT(→ 触发边界拦截)
- 操作类型:【全量覆盖语义下必填】
- 新增第 N 条 / 修改第 N 条(原 OBJECT_NAME=___)/ 删除第 N 条(OBJECT_NAME=___)
- 影响字段:{field 列表} ← 可 AI 推断
硬门槛 vs 可推断的分档:
| 字段 | 定性 | 理由 |
|---|---|---|
| 数据来源(QUERYXH 或数据源表) | 🔴 硬门槛 | 缺了就是实验里的卡点;约束 #11 明令不可编造 |
| 取值方式 | 🔴 硬门槛 | 防心智模型错位(配完无效) |
| OBJECT_NAME / 中文名 / PARAMS / 影响字段 | 🟢 可 AI 推断 | 命名/低风险,错了可改,不影响功能 |
取值方式边界规则
| 取值方式选项 | 性质 | 门禁行为 |
|---|---|---|
| 默认值回填 | ✅ dataObjects 独立达成 | 边界内,放行,继续配 |
| 脚本取用(onDataLoaded) | ✅ dataObjects 独立达成 | 边界内,放行,继续配 |
| 配套选择器 | 🚫 需求探测器 | 边界拦截:告知“你的真实需求是字段可选,需配套 DYNAMIC_DATA_SELECT,单独配 dataObjects 达不到目的。本门禁不配置 dataObjects(配了也无效),请先明确字段组件改动方案” |
> 第三项的价值不是“让门禁去配选择器”,而是识别“光配 dataObjects 满足不了需求”的情况,阻止一次无效配置——正好守在实验真根因(“配完无效”)的关口。
系统编号(seqNumber)完整处理流程
> [!important] ID_GENERATOR 组件的 seqNumber 处理(优化5)
> 当 spec 字段表现形式为「自动生成」「ID 生成」等(映射到 ID_GENERATOR 组件)时,必须按以下流程处理 seqNumber。
快路径:spec 已给生成器标识则直接用
spec 文号字段的「生成器标识」行若有值(如 1784099117607013819904),跳过步骤1-3(查询/新建),直接把该值填入 seqNumber,进入步骤4回填。此路径用于用户已有编号生成器的场景,避免重复查询/误建。
- spec 有「生成器标识」值 → 直接用,跳到步骤4
- spec 无「生成器标识」→ 走下方步骤1-3(查询/新建)
步骤1:查询已有编号
node skills/pacp-build-form/scripts/form_creator.js \
--base-url http://localhost:8080/basesystem \
--session-id $SESSION_ID \
--list-seq-numbers
返回 [{ SXBH, BHMC, QZGS, LSHGS, ... }]。SXBH 是系统编号ID(填入 seqNumber 的值)。
步骤2:判断是否需要新建
- 已有编号中找到前缀格式符合业务需求的 → 直接用其 SXBH
- 没有合适的 → 进入步骤3新建
步骤3:新建编号规则(需向用户确认参数)
> ⚠️ BHMC、QZGS、LSHGS 有业务含义,AI 不得编造,必须用户明确提供或确认。
向用户确认以下参数:
| 参数 | 含义 | 示例 |
|---|---|---|
| BHMC | 编号名称 | "环保投诉编号" |
| QZGS | 前缀格式(支持占位符) | HDTS-#YEAR#-(#YEAR#/#MONTH#/#DATE#/#ORGID#等) |
| LSHGS | 流水号格式 | 000=3位补零(001,002,...),0000=4位 |
确认后调用:
const rule = await client.createSeqRule({
BHMC: '环保投诉编号',
QZGS: 'HDTS-#YEAR#-',
LSHGS: '000', // 3位补零
// HZGS: '', // 后缀(可选)
// ZXZ: 1, // 流水号起始值(默认1)
// ZDZ: 9999, // 流水号上限(默认9999)
});
// 返回 rule.SXBH,填入字段的 seqNumber
步骤4:回填 + 回写 spec
- seqNumber 填入字段配置
- 新建的 SXBH + 编号规则(BHMC/QZGS/LSHGS)回写到 spec 对应字段的「说明」行(见阶段4 spec 回写)
代码结构
pacp-build-form/
SKILL.md ← 本文档
lib/
bwai_client.js ← BwaiClient 类(HTTP 封装、探查、表单 API)
scripts/
widgets/ ← 自包含组件目录(每个组件独立文件,index.js 自动发现注册)
index.js ← 自动发现+注册中心(inferType/resolve/create/listTypes)
├── 基础字段
│ text.js ← TEXT_ELEMENT 文本输入框
│ number.js ← NUMBER_ELEMENT 数字输入框
│ date.js ← DATE_ELEMENT 日期选择器
│ textarea.js ← TEXTAREA_ELEMENT 多行文本框
│ select.js ← SELECT_ELEMENT 下拉选择框(公共代码表)
│ radio.js ← RADIO_ELEMENT 单选框
│ checkbox.js ← CHECKBOX_ELEMENT 多选框
│ codeset.js ← CODESET_ELEMENT 代码集下拉
│ password.js ← PASSWORD_ELEMENT 加密文本
│ id-generator.js ← ID_GENERATOR_ELEMENT ID生成器
│ ueditor.js ← UEDITOR_ELEMENT 富文本编辑器
├── 附件/媒体
│ attach.js ← ATTACH_ELEMENT 附件上传
│ simple-attach.js ← SAMPLE_ATTACHMENT_UPLOAD 简易附件上传
├── 展示/布局
│ title.js ← TITLE_ELEMENT 标题
│ description.js ← DESCRIPTION_ELEMENT 描述文字
│ customhtml.js ← CUSTOM_HTML_ELEMENT 自定义HTML
│ iframe.js ← IFRAME_ELEMENT 内嵌页面
│ button.js ← BUTTON_ELEMENT 按钮
├── 业务选择器
│ department.js ← DEPARTMENT_ELEMENT 部门选择
│ department-asyn.js ← DEPARTMENT_ASYN_ELEMENT 部门选择(异步)
│ person.js ← PERSON_ELEMENT 用户选择
│ regionselect.js ← REGIONSELECT_ELEMENT 行政区划(四级)
│ industry.js ← INDUSTRY_ELEMENT 行业级联树
│ industry-asyn.js ← DIVISION_TREE_ASYN_ELEMENT 行业异步树
│ industryselect.js ← INDUSTRYSELECT_ELEMENT 行业四级级联
│ treedataselect.js ← TREE_DATA_SELECT_ELEMENT 树形数据选择
│ dynamicdataselect.js ← DYNAMIC_DATA_SELECT_ELEMENT 动态列表选择
│ map-selector.js ← LATITUDE_LONGITUDE_ELEMENT 地图经纬度
└── 容器
group.js ← GROUP_ELEMENT 子表(一对多)
table.js ← TABLE_ELEMENT 表格布局
form_builder.js ← createFormConfig(client, opts)
- lib/bwai_client.js — 底层 HTTP 客户端,提供会话管理、探查 API(getTables、getColumns、getPrimaryKey 等)及表单保存 API。
- scripts/widgets/ — 自包含组件目录,每个组件一个独立文件,
index.js为自动发现+注册中心(提供 inferType/resolve/create/listTypes)。 - scripts/form_builder.js — 表单编排器,
createFormConfig(client, opts)接收已连接的 client 和表单选项,完成组装、保存并返回{ templateId }。
API 速查
> 以下内容覆盖所有 agent 需要调用的函数签名、参数与返回值,无需再读源码。
require 引入
const { BwaiClient } = require('./lib/bwai_client');
const widgets = require('./scripts/widgets');
const { createFormConfig } = require('./scripts/form_builder');
BwaiClient 方法
| 方法签名 | 返回值 | 说明 |
|---|---|---|
new BwaiClient(baseUrl, sessionId?) |
客户端实例 | sessionId 可选,手动模式时传入 |
await client.getTables() |
[{ TABLE_NAME, TABLE_COMMENT }] |
获取所有表 |
await client.getColumns(tableName) |
[{ COLUMN_NAME, COLUMN_COMMENT, DATA_TYPE }] |
获取列信息 |
await client.getPrimaryKey(tableName) |
string (列名) |
获取主键列名 |
await client.getCommonCodeClassList({ pageSize: 10000 }) |
{ list: [{ DMJBH, DMJMC }] } |
获取代码集分类 |
await client.getFormConfig(templateId) |
object |
获取现有表单配置(修改模式) |
await client.getDataObjects(templateId) |
{ list, total } |
查询表单的外部数据对象配置 |
await client.saveDataObjects(templateId, objects) |
object |
保存外部数据对象配置(全量覆盖,详见 JSDoc) |
await client.getElementProperties(type) |
object |
获取元素属性定义 |
client.setTraceDir(dir) |
void |
开启请求追踪,写入 api-trace.jsonl |
client._writeSummary(obj) |
void |
写入 trace-summary.jsonl |
BwaiClient.createWorkspace(name) |
string (绝对路径) |
创建隔离工作区目录(基于 .pacp/active/) |
BwaiClient.uuid() |
string (UUID) |
生成 UUID |
makeFormElement(type, label, field, extraAttrs?)
extraAttrs 常用字段:{ required: '1', LYZ: 'XZCF_AJLY' }
运行 form_creator.js --list-types 查看所有可用组件类型、别名和配置示例。
返回值:
{
attrbute: 'TEXT_ELEMENT', // 映射后的组件类型
id: 'uuid',
attrbutevalues: {
YSMC, // 显示名 = label
YSDYZD, // 元素对应字段 = field
FIELD, // 绑定字段 = field
dbcolum, // 数据库列 = field
label, // 标签 = label
// ...extraAttrs 展开
}
}
createFormConfig(client, opts)
| 字段 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| title | string | 是 | - | 表单标题 (BDBT) |
| templateName | string | 是 | - | 模板名称 (MBMC) |
| tableName | string | 是 | - | 关联表名 (GTB) |
| primaryKey | string | 否 | 自动获取 | 主键字段 |
| elements | array | 是 | - | makeFormElement 返回值数组 |
| czan | string | 否 | 'save,submit' | 操作按钮(逗号分隔) |
| lx | string | 否 | - | 分类 ID |
| dataSource | string | 否 | 'jdbc/default' | 数据源 |
返回值:{ templateId: string, warnings: string[], raw: object }
CLI 调用方式(推荐)
本 skill 提供命令行入口,无需编写 Node.js 脚本,通过 JSON 配置 + CLI 命令即可完成表单创建。
前置条件
本 skill 的所有 CLI 命令都需要 --session-id(即服务端的 JSESSIONID cookie)。
> [!important] 登录强制要求 chrome-devtools MCP(优化2)
> 获取 JSESSIONID 只接受两种方式(按优先级):
> 1. 用户直接提供 JSESSIONID → 直接使用,跳过登录
> 2. 通过 pacp-system-login skill(内部基于 chrome-devtools MCP)→ 自动获取
>
> ❌ 不接受其他方式(如 evaluate_script 读 cookie、手动猜测等)——不可靠,浪费时间。
>
> chrome-devtools MCP 不可用时:
> - 建议用户安装 chrome-devtools MCP
> - 或让用户直接提供 JSESSIONID(从浏览器开发者工具 Application > Cookies 复制)
> - 不要尝试其他方法浪费时间
# pacp-system-login skill 的核心流程(基于 chrome-devtools MCP):
# 1. 查 .pacp/session.json 缓存,有则校验有效性,有效直接复用
# 2. 无缓存或失效 → chrome-devtools 自动登录(fill 模拟键盘输入)
# 3. 取 JSESSIONID(主动 fetch 制造请求 → 读网络请求头 cookie)
# 4. 写入 .pacp/session.json 供下次复用
命令格式
# 查看可用组件类型
node skills/pacp-build-form/scripts/form_creator.js --list-types
# 查看可用系统编号(用于 ID_GENERATOR 的 seqNumber)
node skills/pacp-build-form/scripts/form_creator.js \
--base-url http://localhost:8080/basesystem \
--session-id $SESSION_ID \
--list-seq-numbers
# 创建表单
node skills/pacp-build-form/scripts/form_creator.js \
--base-url http://localhost:8080/basesystem \
--session-id $SESSION_ID \
--config form-config.json
# 仅探查表结构(不创建)— 2-step 模式
# Step 1: 获取列索引(只有列名+注释,精简)
node skills/pacp-build-form/scripts/form_creator.js \
--base-url http://localhost:8080/basesystem \
--session-id $SESSION_ID \
--check-only \
--config - <<'EOF'
{"tableName": "T_HJJC_WTJCGZDJ"}
EOF
# → 返回 78 列的 COLUMN_NAME + COLUMN_COMMENT + primaryKey
# Step 2(可选): 获取指定列的完整详情(DATA_TYPE、ZDLX 等)
# form_creator 内置类型推断,elements[].type 可省略,CLI 自动根据 DATA_TYPE 推断控件类型。
# 只有需要提前确认列类型时才用。
node skills/pacp-build-form/scripts/form_creator.js \
--base-url http://localhost:8080/basesystem \
--session-id $SESSION_ID \
--check-only \
--config - <<'EOF'
{"tableName": "T_HJJC_WTJCGZDJ", "columns": ["RWBH", "SJCDWMC", "JCLB", "JCYY", "NCYJCRQ", "TJR", "TJDW", "WCZT", "BZ"]}
EOF
# → 只返回 9 列的完整属性
参数说明
| 参数 | 必填 | 说明 |
|---|---|---|
| --base-url | 是 | 服务地址 |
| --session-id | 是 | 有效的 sessionId |
| --config | 是 | JSON 配置文件路径,传 "-" 从 stdin 读取 |
| --check-only | 否 | 仅探查表结构不创建 |
| --no-table | 否 | 无表模式:表不存在时由平台自动建表(需 modifyDBStructure=true),primaryKey 默认 XH |
| --list-seq-numbers | 否 | 查询已有系统编号规则列表(用于 ID_GENERATOR 的 seqNumber)。需配合 --base-url + --session-id,不需要 --config |
| --list-types | 否 | 列出所有可用组件类型(不需要 auth) |
| --describe <type> | 否 | 查看指定组件类型的详细属性(不需要 auth) |
JSON 配置格式
{
"title": "污染源委托监测登记",
"templateName": "污染源委托监测登记",
"tableName": "T_HJJC_WTJCGZDJ",
"primaryKey": "RWBH",
"czan": "save,submit",
"elements": [
{ "type": "INPUT", "label": "任务编号", "field": "RWBH", "required": "1" },
{ "type": "CODESET", "label": "监测类别", "field": "JCLB", "LYZ": "DMJ_XX" },
{ "type": "DATE", "label": "拟采样监测日期", "field": "NCYJCRQ" },
{ "type": "TEXTAREA", "label": "备注", "field": "BZ" }
],
"events": {
"onBeforeSave": "const m = this.getFormModel(); if (!m.value.RWBH) { this.ElMessage.warning('任务编号不能为空'); return false; }"
}
}
字段说明:
- title/templateName/tableName: 必填
- primaryKey: 可选,空=自动获取
- czan: 可选,默认 save,submit
- elements[].type:
--no-table模式下必填,普通模式可选。普通模式下 CLI 根据 DATA_TYPE 自动推断:datetime→DATE, decimal/int→NUMBER, text/clob→TEXTAREA, 其他→INPUT。--no-table模式无法查询数据库列类型,每个 element 必须显式指定 type,否则平台不知道该创建什么类型的字段。显式指定时运行--list-types查看所有可选值 - elements[].required: string "0"|"1",是否必填(注意:必须是字符串,不是 boolean)
- elements[].LYZ: CODESET 类型的代码集编号(后端存储字段)
- elements[].disabled: boolean,是否禁用
- primaryKey 为空时会自动调用 getPrimaryKey() 获取
- events: 可选,表单生命周期事件脚本,key 为钩子名,value 为 JS 代码字符串
- jsTools: 可选,工具函数代码,events 中通过
this.getContext().fnName()调用。⚠️ 必须用var fn = function(){}; return {fn: fn}模式(平台运行时只注册 return 对象字面量,function fn(){}声明会静默失效),详见references/script-guide.md§2.6 - businessJs/formDataSaveImpl/externalJs/externalCss: 可选,后端脚本字段
子表(GROUP 容器)
当表单需要一对多子表时,在 elements 数组中使用 GROUP 类型。子表关联一张物理表(KZZB),通过外键(WJZD)与主表关联。子表字段嵌套在 elements 中。
{
"title": "执法案件表单",
"templateName": "执法案件表单",
"tableName": "T_ZF_AJ",
"primaryKey": "XH",
"elements": [
{ "type": "INPUT", "label": "案件编号", "field": "AJBH", "required": "1" },
{ "type": "INPUT", "label": "案件名称", "field": "AJMC", "required": "1" },
{ "type": "DATE", "label": "立案日期", "field": "LARQ" },
{
"type": "GROUP",
"label": "检查记录",
"KZZB": "T_ZF_JCJL",
"WJZD": "AJBH",
"elements": [
{ "type": "DATE", "label": "检查日期", "field": "JCRQ", "required": "1" },
{ "type": "INPUT", "label": "检查人员", "field": "JCRY", "required": "1" },
{ "type": "TEXTAREA", "label": "存在问题", "field": "CZWT" }
]
}
]
}
子表字段说明:
type: 必须为GROUP(或别名DYNAGROUP、GROUP_ELEMENT)label: 子表显示名称(必填)KZZB: 子表物理表名(必填)WJZD: 子表中用于关联主表的外键字段名(必填)。编辑时后端用主表主键值去子表的此字段匹配加载数据。例如主表主键为 XH,子表通过 XKZBH 字段存主表 XH 的值,则 WJZD 填 XKZBH。不填会导致编辑时子表数据无法加载query_primary_key: 子表主键字段(默认ID)elements: 子表字段数组,每个元素与普通字段格式相同- 注意:GROUP 没有
field属性(容器不绑定数据库列)
⚠️ 子表必须有 PXH 列(排序号)
平台加载子表数据时硬编码了 ORDER BY PXH(见 FormData.queryBean()),如果子表没有 PXH 列,编辑表单时会报错:SQL 排序字段 [PXH] 不存在。
- 新建子表时(
--no-table模式):平台会自动创建列,无需手动处理 - 使用已有表作为子表时:必须确认该表有
PXH(INT 类型)列,如果没有,需要先执行ALTER TABLE <表名> ADD COLUMN PXH INT DEFAULT 0 COMMENT '排序号'
无表模式(--no-table)
当目标表在数据库中不存在时,使用 --no-table 参数。平台在保存表单配置时会自动创建表和字段(需系统配置 modifyDBStructure=true)。
node form_creator.js \
--base-url http://localhost:8080/basesystem \
--session-id $SESSION_ID \
--no-table \
--config - <<'EOF'
{
"title": "新业务表单",
"templateName": "新业务表单",
"tableName": "T_NEW_BIZ",
"elements": [
{ "type": "INPUT", "label": "名称", "field": "MC", "required": "1" },
{ "type": "DATE", "label": "日期", "field": "RQ" }
]
}
EOF
无表模式下:
- 跳过所有依赖数据库的校验(主键检测、列名校验、类型推断)
- primaryKey 默认为
XH(VARCHAR(50)),可在配置中覆盖 - ⚠️ elements 中的 type 是必填项:由于无法查询数据库列信息,CLI 无法自动推断组件类型,每个 element 必须显式指定 type(如
INPUT、DATE、NUMBER等)。运行--list-types查看所有可用类型。未指定 type 的 element 将导致创建失败 - 永远不要通过其他手段自行建表,完全依赖平台的 modifyDBStructure 能力
脚本能力
> 🔴 必读:5.x 平台知识权威源
>
> 涉及以下主题时,优先加载 pacp-dynamic-wiki skill 的 references(本 skill 自带的 script-guide.md 为兜底副本):
>
> - 表单脚本写法(生命周期钩子、formApi、formModel、字段级 onChange、jsTools 约束、常见 patterns、后端脚本字段)→ wiki 的 5x/form-script-guide.md
> - 字段控制 API(显隐/必填控制、form-level/field-level 双 API、联动语义)→ wiki 的 5x/form-script-guide.md §6
> - 表单组件目录(30+ 组件的 type+aliases+核心属性)→ wiki 的 5x/form-component-catalog.md
> - 平台 API 端点(save/getformconfig/getDataObjects/...)→ wiki 的 5x/platform-api-endpoints.md
> - 平台数据结构(Template JSON/MODELJSON/formModel/attrbutevalues)→ wiki 的 5x/platform-data-structures.md
> - 平台机制(GROUP 子表、无表模式、ID_GENERATOR 系统编号、必填校验门、dataObjects、DYNAMIC_DATA_SELECT)→ wiki 的 5x/platform-mechanisms.md
> - 平台陷阱(tempateId 拼写、_ELEMENT 后缀、getcloumninfo 小写、主键 XH)→ wiki 的 5x/platform-pitfalls.md
> - Preview 渲染规范(--preview 输出格式)→ wiki 的 5x/preview-mode-design.md
>
> ZCode 会根据 description 自动触发 wiki skill(progressive disclosure)。
> 如未自动触发,AI 应主动通过 Skill 工具调用 pacp-dynamic-wiki。
>
> 5.x 平台升级时,wiki skill 是单一同步点,本 skill 不再单独维护这些知识。
动态表单支持两种脚本机制:
| 机制 | 作用域 | 适用场景 |
|---|---|---|
| 表单级 events | 整个表单的生命周期 | 保存校验、多字段联动、数据初始化 |
| 字段级 onChange | 单个字段 | 字段值变化的独立响应 |
快速查询能力清单:
# 列出所有生命周期钩子
node form_creator.js --describe-script list
# 查询脚本中可用的 API(this 指向的 formApi)
node form_creator.js --describe-script api
# 查询后端脚本字段
node form_creator.js --describe-script backend
完整的脚本编写指南(生命周期时序、formApi 用法、字段读写、onChange 配置、常见模式示例):见 references/script-guide.md。涉及脚本编写时必须先加载该文档。
外部数据对象(dataObjects):表单需要在渲染时从查询接口预加载数据(字段默认值回填、下拉联动等),通过 BwaiClient.saveDataObjects/getDataObjects 配置,脚本中用 globalVars.custom_('dataObjects.xxx') 访问。完整机制(四字段含义、渲染流程、PARAMS 与 useFormContext 配合)见 references/script-guide.md 第 4 章。涉及外部数据对象时必须先加载该文档。
动态列表选择器(DYNAMIC_DATA_SELECT):点击弹窗选择数据并回填表单字段的特殊组件。涉及 dynamicId、query_primary_key、query_column、name_column、query_column_array(键名大写)、dtcxcs(注意是 dtcxcs 不是 dtcxps)、callback 等属性配置时,完整属性表、三层回填机制、选择器列表约束(LX=2 + confirmClick)、callback 用法和完整示例见 references/script-guide.md 第 5 章。涉及该组件时必须先加载该文档。
字段控制 API(显隐/必填):表单支持按字段名动态控制显隐和必填状态。涉及 setFieldVisible、setFieldRequired(form-level,在 events 中使用)和 setVisible、setRequired(field-level,在 onChange 中使用)时,双层级 API 签名、联动语义(隐则非必填、改必填后自动 clearValidate)、条件显隐典型模式(onDataLoaded + $watch)见 references/script-guide.md 第 6 章。涉及字段显隐/必填控制时必须先加载该文档。
输出格式(JSON 到 stdout)
成功:
{
"success": true,
"templateId": "xxx",
"templateName": "xxx",
"tableName": "xxx",
"primaryKey": "xxx",
"elementCount": 9,
"formUrl": "http://...basesystem/app/index.html#/form-render/xxx",
"warnings": []
}
失败:
{ "success": false, "error": "错误信息" }
check-only 输出(2-step 模式):
Step 1(不带 config.columns)— 精简列索引:
{
"tableName": "T_HJJC_WTJCGZDJ",
"primaryKey": "RWBH",
"columns": [
{ "COLUMN_NAME": "RWBH", "COLUMN_COMMENT": "任务编号" },
{ "COLUMN_NAME": "SJCDWMC", "COLUMN_COMMENT": "受监测单位名称" }
],
"columnCount": 78
}
Step 2(带 config.columns 白名单)— 完整列详情:
{
"tableName": "T_HJJC_WTJCGZDJ",
"primaryKey": "RWBH",
"columns": [
{ "COLUMN_NAME": "RWBH", "COLUMN_COMMENT": "任务编号", "DATA_TYPE": "varchar", "ZDLX": "string" },
{ "COLUMN_NAME": "NCYJCRQ", "COLUMN_COMMENT": "拟采样监测日期", "DATA_TYPE": "datetime", "ZDLX": "date" }
],
"columnCount": 9
}
进度信息输出到 stderr,不污染 stdout 的 JSON。
处理流程:spec 驱动(新增)+ templateId 驱动(修改)
入口分流已在上方「入口分流 + 文件存在性闸门」定义。以下为两条入口的具体执行流程。
新增入口:spec 驱动
阶段0 文件存在性闸门 → 阶段1 登录+工作区 → 阶段2 [按需]探查列/代码集 → 阶段3 构建 JSON → 阶段4 提交+打开页面
阶段0 文件存在性闸门
见上方「入口分流 + 文件存在性闸门」。按四级优先级查找 spec 文件(.pacp/specs/动态渲染/{业务名}__表单.md 优先),轻量校验三段存在。
阶段1 登录前置 + 工作区初始化
> 🔴 登录是第一件事:先读 .pacp/session.json 复用;无缓存或过期则通过 pacp-system-login skill 取 JSESSIONID。用户提供账号密码时,浏览器用 chrome_devtools_fill 模拟键盘输入(禁止 evaluate_script 设 value,会绕过前端加密)。
登录完成后,从 spec 元信息提取业务名,在 .pacp/active/ 下创建工作区子目录:
const workspace = path.join(process.cwd(), '.pacp', 'active', businessName + '_' + timestamp);
fs.mkdirSync(workspace, { recursive: true });
创建 BwaiClient 实例后,立即开启请求追踪:
const { BwaiClient } = require('./lib/bwai_client');
const client = new BwaiClient(BASE_URL, SESSION_ID);
client.setTraceDir(workspace);
阶段2 [按需] 探查列/代码集
> 按需执行:仅当 spec 字段需核对列细节、或代码集字段标了「待定」时才执行。
使用 lib/bwai_client.js 的 BwaiClient 类探查:
const columns = await client.getColumns(spec.关联表);
// 排除系统字段 → 选择业务字段 → 核对/补全 spec 的表现形式与必填
> [!important] 阶段2 探查时遇到"代码值:待定"的处理
> spec 里标"待定"的代码集字段,停下来问用户要 LYZ 编号(保持"LYZ 必须用户给,AI 不猜"铁律)。
> - 用户给了编号 → 核对编号存在性(可查 ggdmz/list)→ 回填 spec → 继续
> - 用户给不出 → 字段先按普通文本输入构建,构建后提醒用户自行配置代码集
代码集编号核验——spec 里代码集字段若标了「待定」或用户给了编号,核对编号是否存在 + 选项是否与字段语义匹配(查 ggdmz/list,详见「代码集查询」章节);不符告知用户定夺,相符则回填 spec 的代码值。
将列信息和代码集核验结果保存到工作区 table-info.md。
阶段3 构建 JSON
> [!important] 前置步骤:spec→config 逐字段对照(优化4)
> 构建 JSON 前,必须先产出一份字段对照表,写在工作区 .pacp/active/{任务}/field-mapping.md:
>
> | spec 字段 | 表现形式 | →组件 | 布局语义 | →XSGS | 必填 | 代码集 | 备注 |
> |----------|---------|-------|---------|-------|------|--------|------|
> | AJBH 案件编号 | 单行文本 | INPUT | 半行 | 2 | 是 | - | 主键 |
> | CLZT 处理状态 | 下拉选择 | CODESET | 半行 | 2 | 否 | XZCF_CLZT | - |
>
> 强制要求:
> - 每个 spec 字段必须出现在对照表中(不允许遗漏)
> - XSGS 值必须显式写出,标注来源(spec 布局段对应行),不允许使用默认值
> - 自定义组件(spec 标⚠️)必须标注"查风格包"并写出最终映射结果
>
> 目的:构建过程可见、可审计。产物布局不对时,对照表能快速定位是 spec 写错还是 build 映射错。
> 指定风格时,先读对应风格包(stylepacks/{风格名}.md),严格按风格约定构建(组件用法 / 布局 / 样式 / 数据类型)。
>
> ⚠️ 复杂表单(TABLE_JSON 网格、签名组件等)超出 form_creator 能力时,用 BwaiClient 编程直接调 save 接口(见末尾「复杂表单编程构建」)。
基于 spec 构建的核心:表现形式词汇 → _ELEMENT 组件映射。
> [!important] 表现形式词汇 → 组件映射(spec 驱动构建的核心)
> spec 的「表现形式」用词汇描述,构建时映射到平台组件。映射优先级:
> 1. 标准词汇 → 内置映射表(单行文本→INPUT、下拉选择→SELECT、多选→CHECKBOX、单选→RADIO、日期→DATE...)
> 2. 自定义组件(spec 标⚠️) → 查 stylepack 风格包找映射(如签名→XZCF-QM-TABLE-ELEMENT)
> 3. 都映射不到 → AI 基于语义自行决定(兜底,不卡住)
>
> [!important] 选择类组件优先用标准组件(SELECT/RADIO/CHECKBOX)
> 「下拉选择」「单选」「多选」默认映射到 SELECT / RADIO / CHECKBOX(标准组件)。这三个标准组件同样支持通过 LYZ 加载公共代码表选项。
>
> CODESET 不作为默认选择——公共代码值组件目前尚不稳定,仅在 spec 显式标注「公共代码值组件」时才使用。默认一律走标准组件 + LYZ。
类word 文书表单:基于模板流构建(有模板时优先)
> [!important] 模板流构建 vs 逐字段拼接
> spec 布局段含模板时,必须基于模板文本流构建,不逐字段拼接。
>
> 构建步骤:
> 1. 取模板文本流(spec 布局段的模板内容)
> 2. 按字段清单语义匹配每个 ___,替换为 ${{"formKey":"字段名","type":"text|date","value":"..."}}
> 3. 同行多个 ___ 保持在一行内,不拆行
> 4. 模板末尾签名区整体替换为签名组件(XZCF-QM-TABLE-ELEMENT),不逐个 ___
> 5. 按语义区块切分 EDITABLE-TEXT-ELEMENT(见下方切分规则)
> 6. 用 CUSTOM_HTML 承载样式骨架(公共 CSS + 网格线 + 标题 HTML)
> [!important] EDITABLE-TEXT-ELEMENT 切分单位 = 语义区块
> 切分单位是语义区块,不是单个字段,也不是整个文档。
>
> 规则:
> 1. 按语义区块切分——基本信息区、权利告知区、现场情况区、确认意见区各自一个 EDITABLE-TEXT-ELEMENT
> 2. 同一区块内多段落 → 一个元素,段落间用换行(<br>)分隔
> 3. 多段落指向同一个 formKey 时 → 必须合并到一个元素(一个 formKey 不能跨多个元素)
标准表单构建
用 makeFormElement() 构建元素,createFormConfig() 一步完成组装与保存:
const elements = [
makeFormElement('INPUT', '编号', 'BH', { required: '1' }),
makeFormElement('INPUT', '名称', 'CSMC', { required: '1' }),
makeFormElement('DATE', '创建时间', 'CJSJ'),
];
const result = await createFormConfig(client, {
title: '系统参数配置',
templateName: 'SYS_PARAM_CONFIG_FORM',
tableName: 'SYS_PARAM_CONFIG',
elements,
});
// result.templateId
> [!important] 占位符括号 lint:含 EDITABLE-TEXT 的表单提交前必检(优化7)
> 构建 JSON 完成后、提交平台前,扫描所有 EDITABLE-TEXT-ELEMENT 的 html_content。${{...}} 占位符的字面双花括号是书写约定,正确拼接只有 $ + 一个 {(JSON 开始);错误的三层括号 ${{{ 是 $ 后紧跟两个 {。
>
> 若 html_content 出现 ${{($ 后两个左花括号紧挨),直接报错停止提交——这是占位符拼接多写了一层括号,前端正则 /\$\{({.*?})\}/g 匹配不到,会以原始代码文本渲染。
>
> javascript
> // lint 伪代码:扫描所有 EDITABLE-TEXT 元素
> for (const el of elements) {
> const a = el.attrbutevalues || el;
> if ((el.attrbute || a.type) === 'EDITABLE-TEXT-ELEMENT' && a.html_content && /\$\{\{/.test(a.html_content)) {
> throw new Error(`占位符三层括号错误:元素 ${el.id || a.YSMC} 的 html_content 含 \${{,应用 PH() 函数拼接('${' + JSON.stringify(opts) + '}')`);
> }
> }
>
>
> 正则 /\$\{\{/ 原理:正确占位符 ${"name":...} 在 $ 后只有一个 {,不命中;错误的 ${{ 有两个 {,命中即错。正确拼接用风格包的 PH() 工具函数:PH(opts) 返回 '${' + JSON.stringify(opts) + '}'。
> [!important] 必填属性校验门:提交 save 前必检(优化7·校验门)
> 与占位符 lint 并列,构建 JSON 完成后、提交 save 前,自动校验每个元素的必填属性是否非空。本机制已内嵌在 createFormConfig(form_builder.js),CLI/编程调用都自动生效,无需手动调用。
>
> 解决的问题:表单构建时若漏填某些必填属性(如 CUSTOM_HTML 的 YSMC、文本字段的 ZDCD),渲染正常但进设计器二次编辑时报错——平台在编辑时才校验完整性。校验门把这个拦截点前移到保存前。
>
> 机制:通过平台元数据接口获取每个元素类型的必填属性集,逐元素核对。
>
> 数据源:平台「元数据管理 → 关联属性」(#/admin/metadata-list)。接口 listYsSxgxdy(YSDYLX) 返回该类型的属性池,其中 XH非空 && ISMUST==='1' 的属性即为该类型的必填属性集。
>
> > 校验流程(内嵌在 createFormConfig,saveForm 前):
> 1. 扁平化收集所有元素(含 GROUP 子表子元素)
> 2. 归一化元素类型码(别名→标准码,如 INPUT→TEXT_ELEMENT)
> 3. 批量拉取各类型必填属性集(走 URL 隔离缓存,见下方)
> 4. 逐元素逐属性核对:必填属性在 attrbutevalues 里是否非空
> 5. 有缺失 → 抛错停止提交,错误报告精确到「元素X缺属性Y」
>
>
> 缓存:元数据按 baseUrl 哈希隔离缓存到 .pacp/metadata-cache/{hash}.json,TTL 7 天。多系统互不干扰。用户在元数据管理页改了配置后,删除该缓存文件即可强制刷新(或调 client.getMustProps(types, {refresh:true}))。
>
> 缺失处理(AI 收到校验报错后):
> 校验门纯拦截不兜底。拦截后 AI 按 3.3 节流程分类补全:
> - A 类(spec 该给但漏了,如 ZDCD):回 spec 补声明(单行文本 100 / 多行文本 2000),重新构建
> - B 类(构建层职责,如骨架元素 YSMC):构建 JSON 层补——优先从 html_content 提取可见文本,提取不到用「区块骨架{N}」
> - 语义不明/规则覆盖不了:问用户
>
> 降级:元数据接口拉取失败时,校验门降级为「放行 + 告警」(校验是增强,不是硬依赖),不阻塞构建。
>
> 相关文件:
> - scripts/must_prop_check.js — 校验逻辑(checkMustProps + normalizeType)
> - lib/bwai_client.js — listYsSxgxdy / getMustProps(含缓存)
阶段4 提交 + 打开页面供用户确认
提交成功后,打开表单页面让用户自行查看:
- 打开表单页(两步导航,见下方刷新策略):先导航到中转页,再进
form-render/{templateId}。若无浏览器工具,把 URL 给用户自行打开。 - 告知用户:「表单已提交并在浏览器打开,请查看渲染是否正常。有问题告诉我,没问题即可。」
- 等用户反馈:不主动做快照验证、不自动修复。
> [!important] 浏览器预览刷新策略(Vue Router hash 模式,优化6)
> 平台前端用 Vue Router hash 模式。直接从 form-render/A 改 URL 到 form-render/B,Vue Router 检测到"同组件 param 变化",不触发组件重载——页面显示的还是旧表单。
>
> 正确操作(两步导航):
> 1. 先导航到中转页(任何非 form-render 的页面)
> 2. 再导航到目标表单页 #/form-render/{templateId}
> 3. 两步之间无需等待,Vue Router 因路由组件切换而强制重新挂载
>
> javascript
> // 伪代码
> navigate_page(baseUrl + '#/penalty/case/todo') // 第1步:中转
> navigate_page(baseUrl + '#/form-render/' + templateId) // 第2步:目标
>
交付提醒:主动告知用户本次未确定的配置(代码集选项内容、特殊组件配置、布局微调、字段顺序),提示在配置页自行调整或下一轮迭代。
> [!important] spec 回写(闭环,优化1)
> 提交成功后,向 spec 回写事实信息(不回写组件/XSGS 等实现细节,保持 spec 纯需求层):
>
> 回写内容:
> | 回写项 | 写在哪 |
> |--------|--------|
> | templateId + formUrl(访问地址)+ 构建时间 | spec 末尾新增 ## 构建记录 段 |
> | 代码集编号(spec 标「待定」→ build 问到后填入) | 字段清单对应字段的「代码值」行(待定 → 实际编号) |
> | 系统编号(build 新建的 seqNumber/SXBH) | 字段清单对应字段的「说明」行补充 |
>
> 示例——spec 末尾新增:
> markdown
> ## 构建记录
> - templateId:1b8e72e6-11d3-4ad2-bcd3-a4c623c00d89
> - 访问地址:http://localhost:8080/basesystem/app/index.html#/form-render/1b8e72e6...
> - 构建时间:2026-07-15T15:30:00
>
>
> 不回写的:组件映射(CODESET/INPUT 等)、XSGS 值、布局微调——这些是实现细节,回写会污染 spec 的纯需求语义。
修改入口:templateId 驱动
getFormConfig(templateId) → 推断改动并生成 diff → 用户确认 → 提交
详见下方「修改模式」章节。
数据结构
Template JSON(保存格式)
{
ForeignKey: '',
ISPRINT: '',
attrbute: {
BDBT: '表单标题', // 表单标题
LX: 'TABLE_ELEMENT', // 类型
MBMC: '模板名称', // 模板名称
PRIMARYKEY: 'BH', // 主键字段
dataSource: 'jdbc/default',
GTB: 'TABLE_NAME', // 关联表名
CZAN: 'save,submit', // 操作按钮(逗号分隔)
elementchild: [
{ BUSINESSJS: '' }, // 业务 JS
{ FORMDATASAVEIMPL: '' }, // 保存实现
{ EXTERNALJS: '' }, // 外部 JS
{ EXTERNALCSS: '' }, // 外部 CSS
],
events: '{}', // 事件配置 JSON
bdysl: '', // 表单元素数量
jsTools: '', // JS 工具代码
},
elements: [
{
attrbute: 'INPUT', // 组件类型
id: 'uuid', // 元素唯一 ID
attrbutevalues: {
YSMC: '字段名', // 显示名
FIELD: 'FIELD_NAME', // 绑定字段
disabled: false,
required: '0',
elementchild: [],
// ... 其他属性根据组件类型
},
},
],
tempateId: 'uuid', // 注意:后端拼写是 tempateId(非 templateId)
}
⚠️ 关键注意事项:
tempateId必须是有效的 UUID,空字符串会导致 120340 错误elementsSize必须与elements.length一致attrbutevalues中属性是直接键值对,不是{value: ...}嵌套格式
布局规范
动态表单使用 12 列 CSS Grid 布局,通过元素配置中的 XSGS 属性控制每个字段的显示占比。
| XSGS | 含义 | 视觉效果 | 适用场景 |
|---|---|---|---|
'4' |
一行 4 个 | 每字段占 1/4 宽 | 较窄字段(编号、类别等) |
'3' |
一行 3 个 | 每字段占 1/3 宽 | 中等宽度字段 |
'2' |
一行 2 个 | 每字段占 1/2 宽 | 普通输入、日期、下拉选择等(默认) |
'1' 或空 |
独占整行 | 每字段占整行 | 标题、富文本、表格布局、附件等 |
> [!important] spec 布局语义 → XSGS 映射(优化3)
> spec 布局段用语义描述式(逐字段声明),build 必须按 spec 的布局语义映射 XSGS,不允许使用默认值:
>
> | spec 布局语义 | → XSGS |
> |-------------|--------|
> | 独占行 | 1 |
> | 半行(一行2个) | 2 |
> | 三分之一行(一行3个) | 3 |
> | 四分之一行(一行4个) | 4 |
>
> 逐字段对照表(阶段3 前置步骤)中,每个字段的 XSGS 必须标注来源(spec 布局段对应行)。
默认布局建议(仅 spec 未声明布局语义时使用):
- 标题组件(TITLE):使用
XSGS: '1'(独占一行),用于区分表单的不同业务区域 - 普通字段(INPUT / NUMBER / DATE / SELECT / RADIO / CHECKBOX / PASSWORD 等):默认
XSGS: '2'(一行2个字段) - 占位较大的字段(UEDITOR / ATTACH / SIMPLEATTACH / MAPSELECTOR / REGIONSELECT / INDUSTRY 等):默认
XSGS: '2'(一行2个字段) - 容器组件(GROUP / TABLE / IFRAME / CUSTOMHTML):默认
XSGS: '1'(独占一行)
用户可能有个性化布局要求时,按用户要求调整 XSGS 值。
注意:widget 工厂方法已内置默认 XSGS 值(默认 '2'),AI 生成表单时无需显式指定 XSGS,除非用户有特殊布局要求。
> [!warning] 编程构建时属性名必须小写
> 上述 XSGS 大写写法仅适用于 form_creator CLI 的 JSON 配置(CLI 内部会转换)。编程构建(BwaiClient 直接调 save)时,EMPTY() 工厂里属性全小写(xsgs),组件工厂 Object.assign 覆盖时必须用小写 xsgs:'1'。混用大小写会导致主键冲突(见陷阱 9)。
配置示例:
{
"elements": [
{ "type": "TITLE", "label": "基本信息" },
{ "type": "INPUT", "label": "姓名", "field": "XM", "XSGS": "2" },
{ "type": "DATE", "label": "日期", "field": "RQ", "XSGS": "2" },
{ "type": "SELECT", "label": "类型", "field": "LX", "LYZ": "BDLX", "XSGS": "2" },
{ "type": "NUMBER", "label": "数量", "field": "SL", "XSGS": "2" },
{ "type": "TITLE", "label": "详细信息" },
{ "type": "UEDITOR", "label": "描述", "field": "MS", "XSGS": "1" },
{ "type": "ATTACH", "label": "附件", "field": "FJ", "XSGS": "1" }
]
}
组件选择指南
根据字段特征选择合适的组件,不要无脑全部用 INPUT。
文本类:INPUT / TEXTAREA / UEDITOR / PASSWORD
| 场景 | 推荐组件 | 判断依据 |
|---|---|---|
| 短文本(姓名、编号、电话、地址等) | INPUT |
内容≤200字符,一行可显示完 |
| 长文本(备注、说明、描述、意见等) | TEXTAREA |
内容可能超200字符,需要多行输入 |
| 富文本(带格式的通知、公告、报告等) | UEDITOR |
需要加粗、列表、图片等富文本排版 |
| 密码、加密字段 | PASSWORD |
内容需要加密存储,输入时显示星号 |
常见错误:备注字段用 INPUT → 用户只能输一行,体验差。应判断:字段名含"备注/说明/描述/意见/详情/内容"且数据库字段类型是 text/clob → 用 TEXTAREA。
数字类:NUMBER / INPUT
| 场景 | 推荐组件 | 判断依据 |
|---|---|---|
| 数值型(金额、数量、年龄、比例等) | NUMBER |
需要数学运算、范围校验、步进控制 |
| 编号型(虽然由数字组成但不参与运算) | INPUT |
如手机号、身份证号、邮政编码等,不参与数学运算 |
日期类:DATE
| 场景 | 推荐组件 | 判断依据 |
|---|---|---|
| 日期/时间(出生日期、登记时间、截止日期等) | DATE |
字段名含"日期/时间/年月日"或数据库类型为 datetime/date/timestamp |
选择类:CODESET / SELECT / RADIO / CHECKBOX
| 场景 | 推荐组件 | 判断依据 |
|---|---|---|
| 从平台代码集中选择(性别、状态、类型等) | SELECT |
标准下拉,通过 LYZ 加载公共代码表选项(默认首选) |
| 单选且选项固定且少(≤5个) | RADIO |
直接在界面上展示所有选项,一目了然 |
| 多选 | CHECKBOX |
可以勾选多个值 |
| 下拉选择(选项较多或节省空间) | SELECT |
选项>5个,或表单空间紧凑时用下拉 |
| 公共代码值组件(spec 显式指定时) | CODESET |
公共代码值专用组件,当前不稳定,仅 spec 明确要求时使用 |
SELECT vs CODESET:默认用 SELECT(标准下拉,支持 LYZ 加载公共代码表,稳定)。CODESET 仅在 spec 显式标注「公共代码值组件」时使用——该组件当前尚不稳定。SELECT/RADIO/CHECKBOX 均支持 LYZ 绑定公共代码表。
文件类:ATTACH / SIMPLE_ATTACH
| 场景 | 推荐组件 | 判断依据 |
|---|---|---|
| 需要上传多个文件或图片(附件列表) | ATTACH |
支持多文件、分类管理 |
| 只需上传单个文件(简单场景) | SIMPLE_ATTACH |
轻量,一个文件就够了 |
业务选择器
| 场景 | 推荐组件 |
|---|---|
| 选择部门 | DEPARTMENT |
| 选择用户/人员 | PERSON |
| 选择行政区划(省市区街道) | REGIONSELECT |
| 选择行业分类 | INDUSTRY / INDUSTRYSELECT |
| 从动态数据列表中弹窗选择并回填 | DYNAMIC_DATA_SELECT |
| 树形结构选择(如组织树、分类树) | TREE_DATA_SELECT |
| 选择地图位置(经纬度) | LATITUDE_LONGITUDE |
组件类型
> [!warning] 组件 attrbute 一律带 _ELEMENT 后缀(坑1·最严重铁律)
> 下表「类型」列给的是逻辑名(CHECKBOX/RADIO/CODESET/SELECT...),但 save 时 attrbute 字段实际值必须带 _ELEMENT 后缀:CHECKBOX_ELEMENT/RADIO_ELEMENT/CODESET_ELEMENT/SELECT_ELEMENT(TEXT/DATE/TABLE 等同理)。
>
> ⚠️ 写成无后缀逻辑名:save 返回 status:000(成功),但 formconfig/{id} 读取时后端按 type 找组件处理器找不到 → NPE → 500「未知异常」。这是「save 成功却 500」的首要嫌疑。getElementProperties(type) 返回 [] 也说明该 type 不在注册表。
>
> form_creator CLI 会自动补后缀,但编程直接调 save 时必须手写完整 _ELEMENT。
> 运行 form_creator.js --list-types 查看所有可用组件的类型、别名和配置示例。
> 以下按分类列出平台支持的全部组件。
基础字段
| 类型 | 别名 | 说明 | 特有属性 |
|---|---|---|---|
INPUT |
TEXT | 文本输入(兜底类型) | MRZ, ZDCD, XSGS, DW |
NUMBER |
数字输入 | min, max, step | |
DATE |
日期/时间选择 | dateType, dateFormat | |
TEXTAREA |
多行文本 | rows | |
CODESET |
SELECT, CODESET | 代码集下拉(需 LYZ) | LYZ, DMJKJLX |
SELECT |
下拉选择(公共代码表,需 LYZ) | LYZ | |
RADIO |
单选框(公共代码表,需 LYZ) | LYZ | |
CHECKBOX |
多选框(公共代码表,需 LYZ) | LYZ | |
PASSWORD |
TEXT_ENCRYPTION | 加密文本 | |
ID_GENERATOR |
IDGEN | ID 自动生成 | seqNumber(必填,系统编号ID,通过 --list-seq-numbers 获取), generate, automatic |
| UEDITOR | RICHTEXT, QUILL | 富文本编辑器 | |
附件/媒体
| 类型 | 别名 | 说明 | 特有属性 |
|---|---|---|---|
ATTACH |
ATTACHMENT | 附件上传(多文件/图片) | uploadFileBigClass |
SIMPLE_ATTACH |
SIMPLE_ATTACHMENT | 简易附件上传 |
展示/布局
| 类型 | 别名 | 说明 | 特有属性 |
|---|---|---|---|
TITLE |
分区标题(非数据字段) | ||
DESCRIPTION |
DESC | 描述文字(非数据字段) | html_content |
CUSTOMHTML |
CUSTOM_HTML | 自定义 HTML(非数据字段) | html_content |
EDITABLETEXT |
EDITABLE_TEXT, EDITABLE-TEXT | 可编辑文本(文书正文,类word「正文+可填位」,${{...}} 占位嵌字段,详见「文书表单构建」) |
html_content, ALIGN |
IFRAME |
内嵌页面 | iframe_content | |
BUTTON |
按钮(非数据字段) | onClick |
业务选择器
| 类型 | 别名 | 说明 | 特有属性 |
|---|---|---|---|
DEPARTMENT |
部门选择 | name_column | |
DEPARTMENT_ASYN |
部门选择(异步弹窗) | name_column | |
PERSON |
PERSON_ASYN | 用户选择 | name_column |
REGIONSELECT |
DIVISION_TREE_ASYNCHRONOUS | 行政区划(四级:省/市/县/街道) | province_column, city_column, area_column, town_column |
INDUSTRY |
行业级联树(同步) | ||
DIVISION_TREE_ASYN |
行业级联树(异步懒加载) | ||
INDUSTRYSELECT |
行业四级级联(四独立 select) | province_column, city_column, area_column, town_column | |
TREE_DATA_SELECT |
TREESELECT, CASCADER | 树形/级联选择器 | dynamicId |
DYNAMIC_DATA_SELECT |
DYNAMICSELECT | 动态列表选择(弹窗回填) | dynamicId, query_primary_key, query_column, name_column, query_column_array(键名大写), SFDX, dtcxcs(注意不是dtcxps), callback |
LATITUDE_LONGITUDE |
MAP_SELECTOR | 地图经纬度选择 | longitude_column, latitude_column |
容器
| 类型 | 别名 | 说明 | 特有属性 |
|---|---|---|---|
GROUP |
DYNAGROUP | 子表容器(一对多),子元素在 elements 数组 | KZZB(子表名), WJZD(外键), query_primary_key(子表主键) |
TABLE |
表格布局(行列容器),支持单元格合并 | rows, cols |
> 类型推断规则:datetime/date/timestamp → DATE,decimal/int/float → NUMBER,text/clob → TEXTAREA,其他 → INPUT。业务选择器和容器类不参与推断,必须显式指定 type。
操作按钮(CZAN)
逗号分隔:save, submit, delete, print, export
修改模式
- 用
client.getFormConfig(templateId)获取现有配置 - 解析 MODELJSON 中的 elements
- 对比变更(diff 分析)
- 传入已有的
templateId
方式一:CLI --template-id 参数(推荐)
node form_creator.js \
--base-url http://host:port/contextPath \
--session-id $SESSION_ID \
--template-id 1b8e72e6-11d3-4ad2-bcd3-a4c623c00d89 \
--config form-config.json
方式二:JSON 配置中写 templateId
{
"templateId": "1b8e72e6-11d3-4ad2-bcd3-a4c623c00d89",
"title": "...",
"...": "..."
}
方式三:编程 API
const result = await createFormConfig(client, {
templateId: existingTemplateId, // 传入已有 templateId 进入更新模式
// ... 同新增的参数
});
> 优先级:--template-id 命令行参数 > config.templateId JSON 字段 > 空(新建)
修改模式下的 MODELJSON 结构
通过 getFormConfig(templateId) 获取的 MODELJSON 是一个 JSON 字符串,解析后结构如下:
{
templateName: "表单名称",
templateTable: "T_MAIN_TABLE",
primarykey: "XH",
BDBT: "表单标题",
dataSource: "jdbc/default",
events: { onBeforeSave: "...", onDataLoaded: "..." },
child: [ // ← 扁平数组,包含所有字段和 GROUP
{
type: "TEXT_ELEMENT",
id: "uuid-1",
YSMC: "名称", YSDYZD: "MC", FIELD: "MC",
// ...40+ 属性
},
{
type: "GROUP_ELEMENT",
id: "uuid-2",
YSMC: "子表名称",
KZZB: "T_SUB_TABLE", // 子表物理表名
WJZD: "MAIN_ID", // 外键字段
query_primary_key: "XH", // 子表主键
child: [ // ← 子字段在 child 中(注意:不是 elements)
{
type: "TEXT_ELEMENT",
id: "uuid-3",
YSMC: "费用名称", YSDYZD: "FYMC",
// ...
},
],
},
],
}
MODELJSON ↔ save 请求的关键差异:
| 方面 | MODELJSON(读取) | save 请求(写入) |
|---|---|---|
| 子表子字段位置 | group.child[] |
groupElement.elements[] |
| 属性容器 | 属性直接在节点上 | 属性在 attrbutevalues 对象中 |
| 子表子字段 key | child |
elements(后端 FormDesignerService.saveListElements 第 805 行要求) |
编程修改表单的正确方式:
// 1. 获取现有配置
const cfgResp = await client._post('/api/v2/dynamicform/dynamicformcontroller/formconfig/' + templateId);
const model = JSON.parse(cfgResp.MODELJSON); // 返回顶层 MODELJSON,非 data.MODELJSON
// 2. 修改 model(如修改某个字段属性、修改 events 等)
model.child.find(c => c.YSDYZD === 'YYJE').SFZD = '1';
// 3. 用 createFormConfig 保存(form_builder.js 的 childToElement 自动处理 child→elements 转换)
const result = await createFormConfig(client, {
templateId,
title: model.BDBT || model.templateName,
templateName: model.templateName,
tableName: model.templateTable,
primaryKey: model.primarykey,
dataSource: model.dataSource || 'jdbc/default',
elements: [], // 空数组 → createFormConfig 从 model.child 重建 elements
events: model.events,
});
> ⚠️ 已知问题:updateFormElement / _internalMutateForm 在有 GROUP 子表的表单上可能报 120340 错误。如果遇到,改用上面的 createFormConfig 方式绕过。
复杂表单编程构建(TABLE_JSON 网格 / 签名组件等)
文书表单构建(EDITABLE-TEXT-ELEMENT,类word 范式)
> [!important] 可编辑文本 = 文书范式核心
> EDITABLE-TEXT-ELEMENT 实现 word 风格「正文+可填位」:html_content 写文书正文,可编辑位用 ${{...}} 占位嵌字段绑定,渲染后变成正文里的输入框。用于决定书/笔录/告知书等文书表单。
>
> **完整 ${{...}}$ 语法(3种type + 属性)+ 属性模板 + 实现范式见风格包执法处罚-文书.md的「EDITABLE-TEXT-ELEMENT 详解」**。源码实证:XZCF_QD/src/components/dynamic-form/widget/field-widget/hooks/use-editable-text.js`。
`${{...}}$ 速查(html_content 里嵌可编辑位):
${{"name":"显示名","type":"text|date|fixedText","formKey":"数据库字段","value":"默认值","readonly":bool}}
text文本输入 /date日期(含 dateType)/fixedText固定文本不可编辑formKey绑定数据库字段(必填);name显示名;value默认值;readonly只读- 文书里固定法律文字用
fixedText或纯文本写死,当事人填写位用text/date
> [!note] 文书表单 = CUSTOM_HTML(样式骨架) + 多个 EDITABLE-TEXT-ELEMENT(正文段)。复杂文书直接调 save 接口,元素属性是抽平结构(type/YSMC/html_content/ALIGN 直接在元素上,非 attrbutevalues 嵌套)。实证源:表单 3a3efbac(不予行政处罚决定书,16 个 EDITABLE-TEXT)。
当表单含 TABLE_ELEMENT 网格布局、平台特有组件(如 XZCF-QM-TABLE-ELEMENT 签名)等超出 form_creator 能力时,绕过 createFormConfig,用 BwaiClient 编程直接调 save 接口。以下为实证进出的硬知识。
save 接口精确 payload
接口:POST /api/v2/dynamicform/dynamicformcontroller/save
const payload = {
template: JSON.stringify({
ForeignKey: '', ISPRINT: '',
attrbute: {
BDBT, DYNACODE:'', FOREIGNKEY:'', CZAN:'save', GTB, ISPRINT:'',
LX:'1692778465032026730496', MBMC, PRIMARYKEY, dataSource:'jdbc/default',
elementchild: [{BUSINESSJS:''},{FORMDATASAVEIMPL:''},{EXTERNALJS:''},{EXTERNALCSS:''}],
events:'{}', bdysl:String(elements.length), jsTools:''
},
elements,
tempateId // ⚠️ 拼写:少一个 l
}),
isSave: '1',
elementsSize: String(elements.length)
};
const resp = await client._post('/api/v2/dynamicform/dynamicformcontroller/save', payload);
// 成功: {status:'000', data:{NEWTEMPLATEID:'...'}}
elements 完整格式(区别于 form_creator 的简化 {type,label,field})
const elements = child.map(c => {
const { type, id, ...rest } = c;
return { attrbute: type, id, attrbutevalues: rest };
});
⚠️ 每个元素 attrbutevalues 必含 elementchild: [],否则报 JSONObject["elementchild"] not found(120340)。
TABLE_JSON 网格结构
{ trList: [{ index: 0, tdList: [{
colspan: '1', // ⚠️ 字符串,不是数字
rowspan: '1', // ⚠️ 字符串
index: 0, // 数字
element: '子元素id' 或 'NONELEMENT', // 被合并格用 NONELEMENT
hide: null // null 或 {sourceRow,sourceCol} 对象(被合并格)
}]}] }
- 子元素平铺在顶层 elements,TABLE_ELEMENT 不嵌套子元素,TABLE_JSON 靠 element 字段引用 id
- 一个单元格只能放一个组件(td.element 数组实际不生效)——标题与内容须分两个单元格
- id 可用
14位时间戳+32hex(实证风格)或标准 UUID
各组件 attrbutevalues 要点(实证模板)
- 通用必备:
YSMC, label, dbcolum, YSDYZD, elementchild:[], rowspans:'1',外加组件特有属性 - TEXT_AREA 默认值:
defaultvalue单独不生效,须同时设MRZ+value - CUSTOM_HTML 标题:
ISSHOWLABEL:'true', SFXS:'1', isshow:'1', html_content - CHECKBOX 多选分两列:CSS
.container .el-checkbox-group{display:grid;grid-template-columns:1fr 1fr}
getFormConfig 返回结构(修正)
getFormConfig(templateId) 返回顶层对象,MODELJSON 在顶层(不是 data.MODELJSON):
const cfg = await client.getFormConfig(templateId);
// cfg = { TEMPLATEID, BTNS, MODELJSON(JSON字符串), LX, RELATIONS, EXTERNAL_FILE }
const model = JSON.parse(cfg.MODELJSON); // 不是 cfg.data.MODELJSON
代码集查询(仅在必须核对选项时)
- 分类:
POST /api/v2/platform/system/commoncode/ggdmj/query(参数 pageNum/pageSize/searchValue) - 选项明细:
POST /api/v2/platform/system/commoncode/ggdmz/query(参数含 DMJBH 过滤);旧接口findcommoncodes返回全量需客户端按 DMJBH 过滤 - 明细字段:
DMJBH(代码集编号) /DM(代码值,不是 DMZ) /DMMC(名称) /PXH(排序) /SFYX(有效)。取代码值用c.DM(坑6)
> 原则:LYZ 编号必须用户给(AI 不猜),但用户提供后可核对选项匹配度,不符告知用户定夺(坑4)。
渲染验证缓存
Vue Router 同组件 param 变化(form-render/A → form-render/B)不触发重载。验证时须先导航到中转页(如 todo)再进入目标表单,强制重新挂载。
save 成功但 formconfig 500 诊断法(坑2/5)
save 返回 status:000 但 formconfig/{id} 持续 500 时,按以下顺序排查(切勿只轮询等缓存):
- 先排除缓存延迟:
ensureFormCacheSynced(id)+ 轮询 3-4 次(每次 2.5s)。某次恢复 → 纯缓存延迟(第一轮坑8)。 - 持续 500 = 配置缺陷:
getTables确认表已建(排除建表失败)。 - 二分定位组件:生成最小变体表单(全局标题 + 2行1列表格 + 单个被测组件),逐组件测试,一次跑多个变体瞬间锁定问题组件。
- 首要嫌疑:type 后缀:被锁定组件的
attrbute是否漏_ELEMENT?getElementProperties(type)返回[]的类型(CHECKBOX/QM 等)最易踩此坑。
> 实战:CHECKBOX 漏后缀 → save 成功 → formconfig 500 → 轮询 45s 无果 → 二分 5 变体锁定 → 补后缀重跑秒过。开局用此法可省整个诊断循环。
构建脚本模板(EMPTY 工厂 + 组件工厂 + 程序化 TABLE_JSON)
// 1. 实证 40 字段基础集(内置 elementchild,杜绝 120340)
function EMPTY(){return{ysdymb:'',yddysdymb:'',pbdysdumb:'',xsbl:'',xsgs:'',sjlx:'',
yssjjylx:'',dmjkjlx:'',isshow:'',readonly:'',defaultSearchCloum:'',
defaultAssociatedTable:'',defaultAssociatedCloum:'',must:'',dbcolum:'',dbtable:'',
explain:'',maxlength:'',history:'',isjcr:'',datasource:'',sourcevalue:'',
isListCloum:'',value:'',dw:'',defaultvalue:'',bootstrap_col:'',table_head_width:'',
YSZ:'',XSZ:'',rowspans:'1',elementchild:[]}}
// 2. 组件工厂(attrbute 一律 _ELEMENT;网格字段 ISSHOWLABEL:false)
const CB=(l,f,lyz)=>({attrbute:'CHECKBOX_ELEMENT',id:uuid(), // ← 后缀铁律
attrbutevalues:Object.assign(EMPTY(),{YSMC:l,label:l,YSDYZD:f,dbcolum:f,FIELD:f,LYZ:lyz,ISSHOWLABEL:'false',SFXS:'1'})});
const TA=(l,f,dv='')=>({attrbute:'TEXT_AREA_ELEMENT',id:uuid(),
attrbutevalues:Object.assign(EMPTY(),{YSMC:l,label:l,YSDYZD:f,dbcolum:f,ISSHOWLABEL:'false',defaultvalue:dv,MRZ:dv,value:dv})});
// 签名组件:顶层独立元素,不嵌网格,放 TABLE 之后
const mkQmTable=(f)=>({attrbute:'XZCF-QM-TABLE-ELEMENT',id:uuid(),
attrbutevalues:Object.assign(EMPTY(),{'XZCF-QM-KD':'180px',xsgs:'1',ISSHOWLABEL:'false'})});
// 3. 程序化 TABLE_JSON(逻辑单元格 → 网格 → tdList,绝不手写)
const cells=[{r:0,c:0,el:tbAjly,colspan:1,rowspan:2}, /* ...声明布局 */];
const grid=Array.from({length:ROWS},()=>Array(COLS).fill(null));
for(const c of cells){for(let r=c.r;r<c.r+c.rowspan;r++)for(let cc=c.c;cc<c.c+c.colspan;cc++)
grid[r][cc]={origin:c,merged:!(r===c.r&&cc===c.c)}};
const trList=grid.map((row,r)=>({index:r,tdList:row.map((g,c)=>
g.merged?{colspan:'1',rowspan:'1',index:c,element:'NONELEMENT',hide:{sourceRow:g.origin.r,sourceCol:g.origin.c}}
:{colspan:String(g.origin.colspan),rowspan:String(g.origin.rowspan),index:c,element:g.origin.el.id,hide:null})}));
> [!note] 更新模式语义(坑9,已确认)
> 更新模式复用 templateId 时,元素 id 可全部重建,save 按整体替换语义处理,TABLE_JSON 引用同步更新即可,表结构不重建。
API 端点(已验证 ✅)
| 端点 | 方法 | 说明 | 状态 |
|---|---|---|---|
/api/v2/dynamicform/dynamicformcontroller/save |
POST | 保存表单 | ✅ |
/api/v2/dynamicform/dynamicformcontroller/formconfig/{id} |
POST | 获取表单配置 | ⚠️ |
/api/v2/element/elementcontroller/property |
POST | 元素属性定义 | ✅ |
/api/.../getcloumninfo |
POST | 列信息 | ✅ |
/api/v2/platform/database/tables |
POST | 表列表 | ✅ |
连接
必须先确定连接地址再创建客户端。
获取连接地址
BwaiClient 需要 baseUrl(格式:http://{host}:{port}/{contextPath})。按以下优先级获取:
- 从上下文推导:检查当前会话中是否已出现过可用的后端地址(如用户之前提到过项目部署地址、其他技能使用了某个地址等)
- 向用户询问:如果上下文无法推导,主动提问:
> "请提供后端服务地址(格式如http://host:port/contextPath)。"
无论哪种方式获取到地址,使用前必须向用户确认:
> "确认使用后端地址 {baseUrl} 是否正确?"
用户确认后才可创建客户端。
Session
const client = new BwaiClient(baseUrl, 'JSESSIONID');
Session 过期时(401),提示用户提供新 JSESSIONID。
工作区文件
| 文件 | 用途 |
|---|---|
api-trace.jsonl |
所有 API 请求/响应的完整追踪(每行一个 JSON,含请求体、响应体、耗时) |
trace-summary.jsonl |
高级方法(createFormConfig)的结构化入参/出参摘要 |
table-info.md |
数据表结构信息 |
form-params.json |
AI 输出的表单参数 |
existing-form.json |
已有配置(修改模式) |
⚠️ 已知陷阱(必读)
以下陷阱均来自实际调试中遇到的血泪教训。创建表单前必须逐条检查。
| # | 陷阱 | 正确做法 | 症状 |
|---|---|---|---|
| 1 | tempateId 拼写错误 |
后端字段名是 tempateId(少一个 l),不是 templateId |
空字符串 → 错误码 120340 |
| 2 | PRIMARYKEY 用占位符 |
必须是实际的主键列名(如 BH、XH),不能用 *** 或空字符串 |
表单渲染成功但保存数据失败 (120111) |
| 3 | LX 用了元素类型 |
LX 是分类 ID,不是元素类型。默认值 1692778465032026730496 |
不影响保存但分类不正确 |
| 4 | getcloumninfo 参数大小写 |
参数名是全小写 tablename,不是 tableName |
API 调用失败 |
| 5 | attrbutevalues 嵌套格式 |
属性是直接键值对 {FIELD: 'xxx'},不是 {FIELD: {value: 'xxx'}} |
后端解析异常 |
| 6 | attrbutevalues 缺少 dbcolum/YSDYZD |
每个元素必须包含 dbcolum、YSDYZD、YSMC、label、FIELD 五个属性,值都等于字段名 |
渲染时字段名变为 undefined,保存时字段映射丢失 |
| 7 | 表主键不是 XH | 不同表的主键不同,必须用 getPrimaryKey(tableName) 查询实际主键 |
指定错误主键 → 保存失败 (120111) |
| 8 | formReload 异步延迟 | 保存后缓存同步是异步的(新线程),立即读取可能失败 | formconfig/{id} 偶尔返回错误,等待 2-3 秒后重试 |
| 9 | 属性名大小写冲突 | EMPTY() 工厂里属性全小写(如 xsgs),组件工厂里必须也用小写(xsgs:'1'),禁止用大写 XSGS。Windows MySQL 不区分列名大小写,Object.assign 后两个键共存,后端写入属性表时同一属性插入两次 → 主键冲突 |
save 报 Duplicate entry '...-XSGS' for key 'PRIMARY'(错误码 120340) |
| 10 | 必填属性缺失导致二次编辑报错 | 每个元素类型的必填属性(如 TEXT 的 ZDCD、CUSTOM_HTML 的 YSMC)必须非空。校验门已在 saveForm 前自动拦截(见上方「必填属性校验门」)。AI 收到拦截报错后按分类补全:A 类(ZDCD)回 spec 补(单行100/多行2000),B 类(YSMC)构建层补(从 html_content 提取或用「区块骨架N」) | 渲染正常,但进设计器编辑报「属性X不能为空」 |
跨 Skill 协作
与 design-list / build-list 联动
用户要求"增删改查/CRUD"时,需要先创建表单(本 skill),再设计列表 spec(pacp-design-list)并构建关联(pacp-build-list)。
两个 skill 的 BwaiClient 是独立的,方法集不同,必须各自创建各自的实例,不可共享。但两个 CLI 共用同一个 session-id。
// ─── 各自引入各自的模块 ───────────────────────────────────
const { BwaiClient: FormClient } = require('./skills/pacp-build-form/lib/bwai_client');
const widgets = require('./skills/pacp-build-form/scripts/widgets');
const { createFormConfig } = require('./skills/pacp-build-form/scripts/form_builder');
const { BwaiClient: ListClient } = require('./skills/pacp-build-list/lib/bwai_client');
const colWidgets = require('./skills/pacp-build-list/scripts/col_widgets');
const { createListConfig, associateForm } = require('./skills/pacp-build-list/scripts/list_builder');
// ─── 各自创建各自的 client,使用同一个 sessionId ──────────
const fClient = new FormClient(baseUrl, SESSION_ID);
const lClient = new ListClient(baseUrl, SESSION_ID);
// ─── 第1步:用 fClient 创建表单 ──────────────────────────
const formResult = await createFormConfig(fClient, {
title: '...', templateName: '...', tableName: '...',
elements: [ makeFormElement('INPUT', '名称', 'MC'), ... ],
});
const templateId = formResult.templateId;
// ─── 第2步:用 lClient 创建列表 ──────────────────────────
const listResult = await createListConfig(lClient, {
title: '...', sqlSelect: '* FROM TABLE_NAME',
columns: [ makeSeqColumn('序号', 1), makeColumn('MC', '名称', 'string', 2, '20%'), ... ],
});
// ─── 第3步:用 lClient 关联表单 ──────────────────────────
const assocResult = await associateForm(lClient, listResult.XH, templateId, {
primaryKey: 'XH',
});
// assocResult.listUrl, assocResult.formUrl
⚠️ 关键约束:
fClient和lClient是不同类的实例,方法集不同,不可混用fClient有saveForm()但没有saveQueryConfig()lClient有saveQueryConfig()但没有saveForm()- 表单操作(createFormConfig)传
fClient,列表操作(createListConfig / associateForm)传lClient
判断逻辑:
- 用户要求"做某个表的增删改查/CRUD" → 先执行本 skill 创建表单,再由 pacp-design-list 设计列表 spec、pacp-build-list 构建并关联
- 用户只要求"创建表单" → 仅执行本 skill,不涉及列表