Does not involve "Online Report" (cgreport) or "Online Form" (cgform).
Temporary configuration file rules (mandatory)
All --config <xxx.json> passed to the script must be written to {系统临时目录}/{SKILL_NAME}/ , which the operating system cleans up automatically; neither the skill nor the script actively deletes this directory or files.
import tempfile, os, json
SKILL_NAME = "<SKILL_NAME>" # 请替换为实际的技能名称
skill_dir = os.path.join(tempfile.gettempdir(), SKILL_NAME)
os.makedirs(skill_dir, exist_ok=True) # 确保目录存在,不主动检查
config_path = os.path.join(skill_dir, 'sk_audit_create.json') # 示例文件名
with open(config_path, 'w', encoding='utf-8') as f:
json.dump(cfg, f, ensure_ascii=False, indent=2)
tempfile.gettempdir() Auto-adaptation: Windows %TEMP%、Linux /tmp、macOS /var/folders/.../T(note that macOS is not /tmp)。
File names are recommended to use <表名>_<步骤>.json(such as sk_audit_create.json), no need to repeat the skill prefix, since the path already contains the skill name, making troubleshooting easier.
❌ Prohibited:
- Writing to
<skill>/tmp/or the current working directory (pollutes the skill / user project) - Hardcoding
/tmp、C:\Tempor any fixed path (not cross-platform) - After each step, actively
rm/Remove-Item(the operating system will clean it up, making it a redundant tool call) - Actively
os.path.exists()check (which itself is a tool call)
(Usingos.makedirs(…, exist_ok=True)to meet the requirement does not count as an active check)
Temporary files may be asynchronously cleaned up by the operating system, but still follow optimistic invocation + error remediation: only when the script returns FileNotFoundError or 配置文件不存在 , use the same content,under the same {系统临时目录}/{SKILL名称}/ path rewrite(before rewriting, still os.makedirs(skill_dir, exist_ok=True) to ensure the directory exists), and never change the path or fall back to the skill directory.
One-click script (must-read, covers all three types of scenarios)
Before writing custom JSON / Python, first check whether the user's requirement matches an existing script in the table below; if it matches,call it directly; do not reassemble JSON or Python:
| User description (keywords) | Call directly | Default coverage |
|---|---|---|
| "all charts", "all charts", "chart collection", "test all dataset types", "SQL+API+JSON", "chart display" | python scripts/generate_all_reports.py --base-url ... --token ... --name "..." --mysql-host ... --mysql-port ... --mysql-db ... --mysql-user ... --mysql-pwd ... |
25 charts (SQL 12 + API 2 + JSON 4 + unbound 7), automatically create chart_demo_all table insert data + automatically create YApi mock + save once |
Matching rules and prohibitions
- Use immediately when keywords match: when the user says "generate all charts", "all charts test", "demonstrate all charts", "test the three dataset types SQL/API/JSON",the first reactionis
generate_all_reports.py,do notgo back and write chart_entry/echarts templates yourself - Can finish in 3 seconds: measured ~3.1s end-to-end creation. After the script starts,do not wait in chunks, do not send AskUser for confirmation, directly
Bashand wait for the result - Mock creation must use a unique path:
create_mockencountering the same path willsilently overwrite existing interface data, polluting others' interfaces. When creating a new interface, a timestamp or sequence number must be appended to the end of the path (such as/sales_20260427), only reuse the original path when the user explicitly says "modify/update an existing interface" - saveDb serial: the script has been changed to serial calls
save_db, avoidingjimu_report_db_fieldINSERT concurrency causing MySQL deadlock - When adding a new chart type: in the
CHARTSlist add one line, write onetpl_xxxfunction, no need to rewrite the main flow
Execution flow
Step zero (required): Token first
When the user message has no X-Access-Token,ask immediately, and only read any files after obtaining the token. Do not pre-read files while waiting for a reply—waiting time does not count toward the 3 minutes, file reading time does.
⚠️ Credentials must not be read from memory, ask the user directly: when any credentials such as database passwords, account passwords, etc. are needed,it is forbiddento read memory files to obtain them; you must ask the user directly in the conversation.
Step one (required): select the minimal file set according to the "Execution speed specification" table
Do not Glob examples/ first, directly check the "Execution speed specification" table below, and read only the specified files according to the scenario. It is forbidden to read additional files outside the table. Scenario matching takes priority over filename matching:
multi-level-header.mdis mainlycross-tab groupRight/dynamic, do not read it for vertical grouping + static multi-level headers (wasting ~30s reading an inapplicable example).
After reading the specified files, directly Write the JSON configuration → execute the CLI command → output the preview link.Complete in two steps, no extra actions allowed.
Report link format(output directly after successful creation; it is forbidden to call the interface to verify whether it exists):
- Designer:
http://{host}/jmreport/index/{report_id}?token={token}&tenantId=1- Preview:
http://{host}/jmreport/view/{report_id}?token={token}&tenantId=1
Report name rules: when the user explicitly specifies a name, use it directly; when not specified, the AI automatically generates a name, and after generation it must call
GET /jmreport/query/report/folder?pageNo=1&pageSize=10&reportType=&name={name}&token={token}to check whether it is duplicated; if there is a duplicate name, append a suffix (such as_2、_20260415)。
utils submodule quick reference(when you need to confirm a function signature, Grep the corresponding small file; it is forbidden to read the full jimureport_utils.py):
| Function to confirm | Which file to read |
|---|---|
| Session、gen_id/code/layer、col_letter、_compute_sign | jimureport_core.py |
| parse_api、parse_sql、save_db、update_db、parse_and_save_dataset、parallel_parse/save/api | jimureport_dataset.py |
| make_designer、base_save、get_report、report_urls、print_summary | jimureport_report.py |
| make_styles、STYLE_BASE/DATA/HEADER/TITLE/LINK(named constants, magic numbers are forbidden) | jimureport_styles.py |
| chart_entry、virtual_row、build_chart_layout、update_chart_config、parallel_fill_charts、pick_chart_axes | jimureport_chart.py |
| create_link、parallel_create_links | jimureport_link.py |
| ensure_datasource、find_datasource、get_ds_connection、query_mysql、execute_ds | jimureport_datasource.py |
| Forbidden | Replace with |
|---|---|
| Read the full jimureport_utils.py | Grep/Read the corresponding submodule according to the table above (each 25-175 lines) |
Grep/Read jimureport_gen.py(for any reason) |
api_dataset/group/standard The signatures and parameters of functions such as are fully given in the SKILL.md call examples (base_url defaults to http://192.168.1.6:8085/jmreport,无需传),"不确定参数"不构成读源码的理由,直接信任文档 |
Grep/Read jimureport_creator.py Confirm whether a certain feature is supported(such as fieldList searchMode, paramList, etc.) |
The JSON configuration templates and forbidden table in SKILL.md already cover all scenarios; directly write the JSON configuration and execute it, it is forbidden to read the source code for verification |
Grep/Read jimureport_dataset.py View the parse_sql implementation |
parse_sql Accepts SQL containing FreeMarker conditions; the server evaluates with empty parameters and then parses the field list, so just call it directly, no need to look at the source code |
| Find DB credentials | Use the configuration in memory or ask the user |
| Running python with the Bash tool on Windows | Switch to the PowerShell tool to run python xxx.py / python -c "...", returned synchronously (see "Windows execution environment" below) |
| Call external API to validate fields | Write the script directly based on the fields provided by the user, without pre-calling the API |
Unauthorized calls to create_mock() or init_yapi() |
⛔ When the user has provided the API URL, directly save_db(api_url=URL), strictly prohibited from calling create_mock() / init_yapi() / any YApi login or validation operation; only call it when the user explicitly says "help me create a mock API" or has not provided a URL at all; URL known = use it directly, no validation, no asking, no login |
sleep + cat polling output |
Bash commands are always backgrounded on Windows; if you still need to use Bash,you must use TaskOutput(task_id, block=true) to wait for the result, polling with sleep/cat is prohibited |
| After the report is created, call the API to verify whether it exists | /save returns success:true means success; directly output the design/preview link, no need to query the report list |
Hand-write border styles({"style":1} or any non-array format) |
Must use make_styles() to get the styles list, which already has the correct built-in ["thin","#d8d8d8"] array format; hand-writing borders is strictly prohibited and will cause the entire table to render blank |
customRows paired with empty columns: [] |
creator relies on columns to generate queryInfo.dbf;columns is empty then binding metadata is not written, report previewhas absolutely no data(even if the dataset records=N)。customRows only controls visual layout,columns must be filled with the actual fields of the dataset (at least one) |
Optional parameters not specified by the user are filled in by yourself(such as customEditConf.eventParams、freeze、rpbar、background , etc.) |
Only write the fields explicitly given by the user; all other optional parameters are omitted, and you must not invent default values |
Call report_urls() use the tool function as a dict (such as urls['designer']) |
report_urls(report_id, base_url, token, tenant) returns tuple (preview_url, design_url), not a dict; and the first parameter is report_id, not base_url.Directly concatenate the string according to "Report link format" in this section, do not call this function |
| Report creation uses a custom .py script | ⛔ must use a JSON configuration file + jimureport_creator.py CLI: only write xxx.json → python jimureport_creator.py --api-base URL --token TOKEN --config xxx.json. For API dataset reports that also need to create a YApi mock, use jimureport_gen.api_dataset(..., mock_data=[...], mock_path="/xxx_日期") to complete in one step; it internally automatically generates JSON and calls the CLI, with no need to hand-write a Python script. User feedback (reconfirmed on 2026-05-22): "only generate JSON from now on".⚠️ Typical error scenario 1: the user requests custom styles, and the AI thinks the advanced function does not support styles so it hand-writes rows/cols/styles/save_db/base_save——a serious violation.⚠️ Typical error scenario 2: the scenario is complex (including drill-down + charts + table creation), and the AI directly writes drill_demo.py and other full-process Python scripts——a serious violation. Correct approach: the report structure part always writes JSON; steps not supported by JSON such as table creation/link creation are written as minimal standalone PowerShell inline; the two parts are kept separate and not mixed. |
PowerShell Out-File -Encoding utf8 write JSON |
produces UTF-8 BOM causing json.load to report Unexpected UTF-8 BOM。Must use:[System.IO.File]::WriteAllText($path, $content, (New-Object System.Text.UTF8Encoding $false)) |
Windows execution environment (mandatory rule; violating it makes users complain that "execution is too slow")
Symptom: Windows' Bash tool treats the python / python -c / skill script as a long command and automatically run_in_background, the tool immediately returns a background ID, and the actual output must wait for the completion notification—inflating a millisecond-level call to several seconds, which historically dragged a single report from 1 minute to 18 minutes multiple times.
Rule:
- Windows(platform=win32) → Use PowerShell tool to directly execute
python xxx.py, returning synchronously. It is forbidden to use the Bash tool to run python (it will be backgrounded). - Linux / macOS → Use the Bash tool to directly call
python xxx.py。 - On any platform, do not use
curl: cross-platform inconsistency; under Windows Bash it is likewise backgrounded.
Mandatory checks before script execution (2 items):
- ✅ Under Windows use PowerShell tool to execute
python xxx.py,not Bash tool - ✅ The script's first line already has the encoding declaration:
import sys; sys.stdout.reconfigure(encoding='utf-8')(to prevent GBK crash retries)
Correct Windows example:
PowerShell: python <skill_base_dir>/scripts/xxx.py --base-url ... --token ...
<skill_base_dir>is the directory where this SKILL.md is located; at runtime replace it with the actual path, and it is forbidden to hardcodeC:/Users/...。
Incorrect Windows example:
Bash: python generate_all_reports.py ... ← 返回 "Command running in background with ID: xxx"
Bash: curl -X POST ... ← 同上
Historical lesson: Previously, because Bash + python was used by default, users repeatedly complained "execution is too slow / generation is so slow". The root cause is that the Bash tool backgrounds python on Windows, not limited to curl. Another common retry cause: the script lacks an encoding declaration, causing
UnicodeEncodeError: 'gbk' codec, and adding the second check can eliminate it. Typical symptom: After report generation completes, it still waits about 2 minutes before ending—this is a direct manifestation of Bash backgrounding: the script has finished but the tool is waiting for the background completion notification. If this symptom occurs, immediately confirm whether the Bash tool was mistakenly used; switching to PowerShell will eliminate it.
Prerequisites
The user must provide X-Access-Token。
SQL dataset data source selection (must be executed when the user has not specified dbSource )
Correct process (must be executed every time, cannot be skipped):
- Call
GET /jmreport/initDataSourceto get the data source list - According to the database name provided by the user (e.g.
jeecg-boot-cr)exact matchnamefield, take itsid - If not found, then ask the user
⚠️ It is forbidden to skip the query by directly using the data source ID stored in memory: the ID in memory may have expired or been rebuilt; you must query every time and then match by name to get the current valid ID. Memory is only used to remember the database name, not to remember the ID. ⚠️ It is forbidden to pull everything and then iterate and guess: when there is a clear database name, match directly by name; do not rely on fuzzy rules like containing "积木".
Handle according to the following rules:
| Returned result | Handling method |
|---|---|
result is an empty array |
Tell the user they need to first add a data source in 积木报表, and stop creation |
result is non-empty, and there exists name an item containing "积木" |
Automatically select that item, and use its id as db_source and pass it to save_db |
result is non-empty, with no item containing "积木" |
List all data source names, ask the user which one to choose, and continue only after waiting for a reply |
Interface return fields: each item contains id(passed to db_source) and name(displayed to the user).
Call directly in the script(It is forbidden to rewrite this logic manually in the script):
from jimureport_utils import resolve_db_source
# 用户未指定数据源时:
db_source = resolve_db_source(session) # 自动选含「积木」的;无则抛 RuntimeError 列出清单
RuntimeError The message already contains the data source list; after capturing it, simply pass it on to the user.
Context takes precedence: If in this conversation it has already been determined via
resolve_db_sourceor the user's reply thatdb_source, then subsequent reports in the same session directly reuse it,do not repeatedly callinitDataSource。 ⚠️ Do not perform a full fetch and then iterate to guess: do not fetch all 30+ data sources and then rely on fuzzy name matching; instead, look up precisely by the database name provided by the user, or directly read memory. Full fetching is wasteful + prone to selecting the wrong one.
API dataset prior inquiry (when the user has not provided an API address, you must ask first)
When the user has not given an API address,you must first ask:
How should the interface be created?
- mock interface: Create a mock interface via YApi (see the "YApi Mock Data Source" section below)
- local code: Please provide the local JeecgBoot project path, and I will write the Controller directly into the project
Processing rules after receiving the reply:
| User choice | Processing method |
|---|---|
| mock interface | Follow the process in the "YApi Mock Data Source" section, use yapi_mock.py to create a mock interface, and fill the returned mock URL into the dataset |
| local code | Ask for the project path (e.g. D:\path\to\jeecg-boot), generate only a Controller written into the project, and return static data ({"data": [...]}),do not generate Entity / Mapper / Service / SQL |
🚀 CLI creation (one command)
python /scripts/jimureport_creator.py \
--api-base http://BASE_URL --token TOKEN --config /path/to/config.json
Config A: SQL ordinary/grouped report
{
"action": "create", "reportName": "报表名称", "theme": "blue",
"datasets": [{"dbCode":"ds1","dbChName":"数据集","dbDynSql":"SELECT col1,col2 FROM t ORDER BY col1","dbSource":"","isPage":"0"}],
"table": {"datasetCode":"ds1","title":"报表名称","columns":[
{"field":"col1","title":"列1","width":120,"group":true},
{"field":"col2","title":"列2","width":100,"funcname":"SUM"}
]}
}
columns optional attributes:
group:true(grouping) /funcname:"SUM"(aggregation) /subtotalText:"小计"
Config B: SQL + chart
{
"action":"create","reportName":"名称","layout":"chart_bottom",
"datasets":[
{"dbCode":"dt","dbChName":"表格","dbDynSql":"SELECT ...","isPage":"1"},
{"dbCode":"dc","dbChName":"图表","dbDynSql":"SELECT x AS name,y AS value,'' AS type FROM ...","isPage":"0"}
],
"table":{"datasetCode":"dt","title":"名称","columns":[...]},
"chart":{"datasetCode":"dc","chartType":"bar.simple","title":"图表","width":"650","height":"380"}
}
layout:
chart_bottom/chart_top/chart_right/chart_only
Config C: JSON dataset (dbCode must be a string!)
{
"action":"create","reportName":"名称",
"datasets":[{"dbCode":"my_data","dbChName":"数据","dbType":"3","isList":"1","isPage":"0",
"jsonData":[{"name":"张三","age":"25"}],
"fieldList":[["name","姓名"],["age","年龄"]]}],
"table":{"datasetCode":"my_data","title":"名称","columns":[
{"field":"name","title":"姓名","width":100},{"field":"age","title":"年龄","width":80}]}
}
Do not use a pure numeric dbCode(e.g. gen_code()); the JSON dataset template engine cannot parse it.
When writing bound fields with an f-string, braces must be escaped:
f"#{{{db_code}.{field}}}"→ generate#{db_code.field}. If written asf"#{db_code}.{field}#"then the braces are consumed by Python, becoming#db_code.field#(format error, extra at the end#, data will not render).
Config D: custom rows (complex multi-level headers)
When build_table_rows cannot satisfy the requirement (e.g. four-level merged headers), pass customRows + customMerges to skip automatic construction:
{
"action":"create","reportName":"名称",
"datasets":[{"dbCode":"ds1","dbType":"3","jsonData":[...],"fieldList":[...]}],
"table":{"datasetCode":"ds1","columns":[{"field":"f1","title":"F1","width":100}]},
"groupField":"ds1.group_field",
"customRows":{"1":{"cells":{"1":{"text":"标题","style":0,"merge":[0,5]}},"height":40}},
"customMerges":["B2:G2"],
"customStyles":[{"align":"center","font":{"size":16,"bold":true}},{"align":"center","font":{"bold":true,"color":"#FFF"},"bgcolor":"#4472C4"},{"align":"center","valign":"middle"}],
"customCols":{"0":{"width":27},"1":{"width":100},"len":100}
}
⚠️
table.columnsWhen both andcustomRowsexist at the same time,columnsstill cannot be an empty array[]creator depends oncolumnsto generate thedesignerObjinqueryInfo.dbf(dataset binding metadata). Ifcolumns: [],queryInfo.dbfwill not be written, the report preview will becompletely blank (no data), even if the dataset has been saved successfully (records=N)。 Correct approach:columns: fill in the dataset's actual fields into columns (even if there is only one),customRowsthen override the visual layout; the two are independent and do not affect each other.
Config E: with drilling (drilling)——no additional Python script is required
drilling and linkages keys are alreadybuilt inIn creator: creator automatically calls /link/saveAndEdit and fills linkIds back into cells / chart extData; the whole process requires only one JSON file.
{
"action": "create",
"reportName": "主报表(三种钻取演示)",
"theme": "blue",
"layout": "chart_bottom",
"datasets": [
{
"dbCode": "sales", "dbChName": "学校汇总",
"dbDynSql": "SELECT leibie AS name, SUM(jine) AS value, '' AS type, 'jeecg.com' AS website FROM school_demo GROUP BY leibie ORDER BY value DESC",
"dbSource": "1161942757348524032", "isPage": "0"
}
],
"table": {
"datasetCode": "sales", "title": "学校汇总报表",
"columns": [
{"field": "name", "title": "类别(点击→子报表)", "width": 140},
{"field": "value", "title": "总金额", "width": 120},
{"field": "website", "title": "官网(点击→jeecg.com)","width": 180}
]
},
"chart": {
"datasetCode": "sales", "chartType": "bar.simple",
"title": "各类别总金额(点击柱子钻取)", "width": "560", "height": "360"
},
"drilling": [
{
"name": "钻取1-报表钻取报表",
"linkType": "0",
"targetReportId": "<detail_report_id>",
"ejectType": "0",
"source": {"type": "cell", "field": "name"},
"params": [
{"paramName": "leibie", "paramValue": "name", "fieldName": "name", "dbCode": "sales", "tableIndex": 0}
]
},
{
"name": "钻取2-图表钻取报表",
"linkType": "0",
"targetReportId": "<detail_report_id>",
"ejectType": "0",
"source": {"type": "chart"},
"params": [
{"paramName": "leibie", "paramValue": "name", "fieldName": "", "dbCode": "sales", "tableIndex": 0}
]
},
{
"name": "钻取3-网络钻取",
"linkType": "1",
"targetUrl": "http://jeecg.com",
"ejectType": "0",
"source": {"type": "cell", "field": "website"},
"params": [
{"paramName": "school", "paramValue": "name", "fieldName": "name", "dbCode": "sales", "tableIndex": 0}
]
}
]
}
drilling field description:
| Field | Description |
|---|---|
linkType |
"0" report drill-through / "1" network link |
targetReportId |
When linkType=0, fill in the target report ID; when linkType=1, leave it empty |
targetUrl |
When linkType=1, fill in the external URL |
ejectType |
"0" new window / "1" current window |
source.type |
"cell" cell trigger / "chart" chart trigger |
source.field |
For cell, fill in the dataset field name (creator automatically matches the data row cell containing that field and fills it back linkIds) |
params[].paramValue |
For cell drill-through, fill in the field name; for chart drill-through, fill in name(X axis) / value(Y axis) / seriesName |
order of cross-report drill-through: first create the subreport to get
detail_id, then filldetail_idinto thetargetReportIdof the main report JSON. Run the CLI on the two JSON files separately; no additional Python script is needed.
Configuration F: NoSQL datasets (ES / MongoDB / Redis)
dbType Fill "es"/"mongo"/"mongodb"/"redis", creator automatically adds the Calcite schema prefix;dbSource the data source ID is required.
{"action":"create","reportName":"名称",
"datasets":[
{"dbCode":"esDs", "dbChName":"ES员工", "dbType":"es", "esIndex":"jmreport_test_employee","dbSource":"<ES数据源ID>", "isPage":"1"},
{"dbCode":"mongoDs", "dbChName":"Mongo订单", "dbType":"mongo", "mongoCollection":"orders", "dbSource":"<Mongo数据源ID>", "isPage":"1"},
{"dbCode":"redisDs", "dbChName":"Redis缓存", "dbType":"redis", "dbDynSql":"SELECT * FROM cache", "dbSource":"<Redis数据源ID>", "isPage":"0"}
],
"table":{"datasetCode":"esDs","title":"员工列表","columns":[
{"field":"emp_id","title":"工号","width":80},{"field":"emp_name","title":"姓名","width":100}
]}
}
| Field | Description |
|---|---|
esIndex |
ES-specific shorthand, automatically generates SELECT * FROM es.{索引名}; with it, dbDynSql |
mongoCollection |
MongoDB-specific shorthand, automatically generates SELECT * FROM mongo.{集合名} |
dbDynSql |
explicit SQL; when ES/Mongo lacks the es./mongo. prefix, it is automatically completed; for Redis, it is passed through as is |
If an ES field name conflicts with a Calcite reserved word (
position/date/type/valueetc.), escape it with backticks or switch to theesIndexshorthand (SELECT *no field names need to be listed, naturally avoiding this). See pitfalls.md §Database Data Sources.
Modify an existing report
# get_report → 改 design → base_save(**design 展开,get_report 返回的 design 是安全的)
designer, design = get_report(session, report_id)
design["rows"]["3"]["cells"]["1"]["text"] = "新值"
design["chartList"] = filled_charts # 如有图表回填,直接替换 chartList
session.request("/save", base_save(report_id, designer, **design))
# ↑ get_report 返回的 design 只含 base_save 接受的 key,**design 展开无冲突
# 注意:手动拼的 design dict 禁止 **展开,必须显式列出 rows/cols/styles/merges/chartList
⚠️ Bug fixes must use a patch script; rerunning the creation script is forbidden In any case where an existing report is found to have problems (whether reported by the user or discovered by the AI itself), the correct approach is to write a separate patch script (
get_report→ modify local fields →base_savewrite back; for dataset errors, useupdate_db),It must not be modified and the creation script rerun. Rerunning the creation script will generate a report with a new ID; the original report (including already configured permissions, share links, and reference relationships) will not be fixed, and junk reports will be produced.
Report category
JimuReport reports are divided into two major categories, with data reports as the default:
| Category | Description | designerObj key fields |
|---|---|---|
| Data report(default) | Display-oriented report, rendered from dataset queries | submitForm not set or is 0 |
| Data entry report | Fill in data on the report and submit it to the backend | submitForm: 1 |
Data report type determination
| User description | Data binding | Dataset configuration |
|---|---|---|
| Detail/list | #{db.field} |
isList:"1" isPage:"1" |
| Form print/single record | ${db.field} |
isList:"0" isPage:"0" |
| Group by XX | #{db.group(field)} |
isPage:"0" |
| Cross table | #{db.groupRight(field)} + #{db.dynamic(field)} |
isPage:"0" |
Cell binding field name retrieval
Write #{dbCode.fieldName} before binding,Do nothandwrite field names based on SQL aliases.
Directly use the parse_sql returned by fieldName(recommended, fastest):
fl = parse_sql(session, sql)
fields = [f["fieldName"] for f in fl]
# MySQL 将所有别名转小写,AS totalAmount → totalamount,直接用即可
/field/tree/{reportId}is the fallback (requires the report to be first/savebefore it can be called),parse_sqlhas already returned the same real field names, so no extra call is needed.
Performance Optimization (Recommended Template for a Single Report)
For a single-report single-dataset scenario, the following 3-step process totals ≤ 5 HTTP requests (including the 1 for an existing data source). Measured end-to-end ~0.8s.
from jimureport_utils import (
Session, gen_id, make_designer, make_styles, base_save, report_urls,
ensure_datasource, parse_and_save_dataset, # ← 推荐新路径
)
session = Session(BASE_URL, TOKEN)
# ① 确保数据源存在(1-2 HTTP,已存在时只 1 次)
ds_id = ensure_datasource(
session, name="mongodb", db_type="mongodb",
db_url="<db_host>:27017/<db_name>",
db_username="qqyun", db_password="qqyun188"
)
# ② 预生成 report_id(客户端,0 HTTP)
report_id = gen_id()
# ③ parse_sql + saveDb 组合(2 HTTP,report_id 允许尚不存在)
sql = f"select * from mongo.{COLLECTION}"
field_list, db_id = parse_and_save_dataset(
session, report_id, DB_CODE, "中文名", sql,
db_source=ds_id, is_list="1", is_page="1"
)
# ④ 构建 rows/cols/styles 后,首次 /save —— 一步创建报表 + 写入布局(1 HTTP)
designer = make_designer(report_id, REPORT_NAME)
session.request("/save", base_save(report_id, designer,
rows=rows, cols=cols, styles=styles, merges=merges, chartList=[]))
| Stage | Original HTTP | Current HTTP | Description |
|---|---|---|---|
| Data Source | 3 (query + save + query again) | 1-2 | ensure_datasource merged |
| First placeholder /save | 1 | 0 | parse_and_save_dataset directly call saveDb on the orphan report_id |
| Parse SQL | 1 | 1 | — |
| Save Dataset | 1 | (merged in ③) | — |
| Final /save | 1 | 1 | First creation + write layout |
| Total (data source already exists) | 7 | 4 | Saves 3 HTTP requests |
Key Principle: saveDb accepts a
report_id(orphan) that does not yet exist on the server; when /save later creates the report for the first time with this id, the dataset will be bound correctly. Verified by testing.addDataSourcereturnsresult: true(does not return an id); after creation you must query once more;/initDataSourcewithout a signature is faster than/getDataSourceByPage.
Legacy Path Still Available (Kept for Compatibility)
parallel_init_and_parse is no longer recommended but is retained — old scripts need no changes. New scripts should always use parse_and_save_dataset。
API Dataset Fast Path (2 HTTP, measured ~0.5s)
API datasets do not require queryFieldBySql, fields are defined manually, and the entire process requires only 2 HTTP requests:
from jimureport_utils import Session, gen_id, make_designer, base_save, save_db
session = Session(BASE_URL, TOKEN)
# ① 客户端生成 report_id(0 HTTP)
report_id = gen_id()
# ② saveDb:orphan report_id 合法,直接绑定(1 HTTP)
field_list = [
{"fieldName": "f1", "fieldText": "字段1", "widgetType": "String", "orderNum": 0, "tableIndex": 0, "extJson": "", "dictCode": ""},
# ... 其余字段
]
save_db(session, report_id, DB_CODE, "数据集名称",
API_URL, field_list,
db_type="1", api_url=API_URL, api_method="0",
is_list="1", is_page="0")
# ③ /save:首次创建报表 + 完整设计一步完成(1 HTTP)
designer = make_designer(report_id, REPORT_NAME)
session.request("/save", base_save(
report_id, designer,
rows=rows, cols=cols, styles=styles, merges=merges, chartList=[],
isGroup=True, groupField=f"{DB_CODE}.group_field", # 交叉/分组报表需要
))
| Legacy Process (3 HTTP) | New Process (2 HTTP) |
|---|---|
| POST /save with an empty report → get report_id | gen_id() generated locally (0 HTTP) |
| POST /saveDb | POST /saveDb (same) |
| POST /save with the complete design | POST /save with the complete design (same) |
Measured: regional province sales crosstab report, 3 steps ~3s → 2 steps ~0.5s (verified 2026-04-22). Applicable scenarios: all API dataset reports (crosstab, grouped, and detail reports).
Special Notes on MongoDB / NoSQL Data Sources
testConnectiononly detects TCP connectivity, and does not verify username or password. It returning success does not mean the credentials are correct.- Actual authentication occurs at
queryFieldBySql/ preview. If credentials are wrong, an error will be reported at these two steps.Exception authenticating。 - Prohibitedin the creation script, trying trial-and-error loops with multiple formats (standard separated / connection string / multiple authSource) — it wastes 3-6 seconds for nothing. Try only the one format the user provided; on failure, immediately report an error and have the user check the MongoDB server
db.getUsers()。
Performance Optimization (Multi-Dataset / Multi-Report Scenario)
Core principle: parallelize everything that can be parallelized, and eliminate serial waiting.
from jimureport_utils import parallel_parse_sqls, parallel_save_dbs, parallel_create_links
from concurrent.futures import ThreadPoolExecutor
# ① 并行解析所有 SQL(一轮完成)
fl_a, fl_b, fl_c = parallel_parse_sqls(session, [
{"sql": sql_a}, {"sql": sql_b}, {"sql": sql_c},
])
# ② 并行保存所有数据集(一轮完成)
db_id_a, db_id_b, db_id_c = parallel_save_dbs(session, [
{"report_id": rid, "db_code": "dsA", "sql": sql_a, "field_list": fl_a, ...},
{"report_id": rid, "db_code": "dsB", "sql": sql_b, "field_list": fl_b, ...},
{"report_id": rid, "db_code": "dsC", "sql": sql_c, "field_list": fl_c, ...},
])
# ③ 并行创建所有钻取/联动(一轮完成)
link1, link2, link3 = parallel_create_links(session, [
{"report_id": rid, "link_name": "钻取1", "link_type": "0", ...},
{"report_id": rid, "link_name": "钻取2", "link_type": "0", ...},
{"report_id": rid, "link_name": "联动1", "link_type": "2", ...},
])
# ④ 多张报表最终 /save 并行
with ThreadPoolExecutor(max_workers=2) as ex:
f1 = ex.submit(lambda: session.request("/save", base_save(rid1, d1, ...)))
f2 = ex.submit(lambda: session.request("/save", base_save(rid2, d2, ...)))
f1.result(); f2.result()
| Optimization Point | Savings |
|---|---|
parse_sql directly get field names, skipping first_save + field/tree |
saves 2 requests per report |
parallel_parse_sqls |
N serial → 1 round of parallel |
parallel_save_dbs |
N serial → 1 parallel round |
parallel_create_links |
N serial → 1 parallel round |
Multiple reports /save parallel |
M serial → 1 parallel round |
Row/column indexing rules
- All 0-indexed, column A (col0) left blank, data starts from col1 (column B)
- merge:
[extraRows, extraCols], 0 = occupies only itself - merges uses Excel notation:
"B2:F2"(UI row number = code row number + 1)
Grouped summary
| User wording | Implementation |
|---|---|
| "Total row" | Add below the data rows =SUM(列号) |
| "Group subtotal" | subtotal:"groupField" + funcname:"SUM" + subtotalText:"Subtotal" |
| Only says "group" | Use only group() + aggregate:"group" |
🚨 When the group column has
subtotal:"groupField"enabled, all numeric columns must default toaggregate:"select"+funcname:"SUM"— otherwise the numeric cells in subtotal/total rows will all be blank; on the UI, "total label shown + numeric columns blank" is an absolutely unacceptable compromise state. Either do not show the total row (change the group column tosubtotal:"-1"、subtotalText:""), or default all numeric columns to sum. AI must not simply copy the template's "group column with subtotal, numeric columns bare text" pattern.
funcname aggregate function values (⚠️ the following strings must be used strictly; a typo will make it ineffective)
| User requirement | funcname value |
|---|---|
| Total / Sum | "SUM" |
| Average / Average value | "AVERAGE" (❌ not "AVG") |
| Maximum | "MAX" |
| Minimum | "MIN" |
| Count | "COUNT" |
| No aggregation (group column placeholder) | "-1" |
Comparison of group column vs aggregate column attributes
| Attribute | Group column (group) | Aggregate column (select) |
|---|---|---|
aggregate |
"group" |
"select" |
subtotal |
"groupField" |
"-1" |
funcname |
"-1" |
"SUM" / "AVERAGE" / "MAX" / "MIN" / "COUNT" |
subtotalText |
Subtotal row label text | Subtotal row label text |
Query parameters (paramList)
Read when query controls are included references/query-params.md § 0 (including SQL FreeMarker conditions, widgetType/searchMode comparison table, date range splitting rules).
Field query vs report parameter query (required reading rules)
| Field query (fieldList searchFlag) | Report parameter query (paramList) | |
|---|---|---|
| SQL | Pure SELECT,Do not add WHERE / FreeMarker conditions |
Must add <#if isNotEmpty(x)>... conditions |
| fieldList | searchFlag=1 + searchMode + dictCode etc. |
No need to set searchFlag |
| paramList | Not needed | paramList must be configured |
| querySetting | No need to set | Configure izOpenQueryBar as needed |
Core rule: For field queries, the JimuReport engine automatically handles filtering, and SQL stays pure; only when report parameters are used should FreeMarker WHERE conditions be added to SQL. The two methodscannot be mixed。
Style specifications (required conventions, no user reminder needed)
- All table reports must have border: Prefer
from jimureport_utils import make_styles's 5 preset styles (indices 0-4) - col0 is always left blank at 30px without borders: The title row starts merging from col1 (merges written as
B1:F1excluding column A).make_styles()The preset styles 0-4 all contain borders,col0 must have a borderless style appended separately:styles = make_styles() styles.append({"align": "center"}) # index 5:col0 专用,无边框 # 所有行的 col0 统一用 style: 5 "0": {"text": "", "style": 5} - When customizing styles
bordermust be nested (cannot**be expanded to the top level),color(text color) andfontare placed at the same level at the top level of style - ⚠️ Whenever customizing the styles array or modifying cell configuration, you must first read
references/styling.md, and must not guess the property hierarchy from experience (colorbeing at the top level rather than insidefontis a high-frequency pitfall; the engine silently ignores incorrect syntax)
Complete specification + col0 blank template code → references/styling.md
Chart type quick reference
Read when charts are included references/chart-types-quickref.md(system name → chartType mapping + series field values + map data semantic rules).
Complete ECharts configuration template → references/chart-echarts-templates.md。
Execution speed specification (complete within 3 minutes)
Mandatory check before starting (go through it before every execution):
- ✅ Data source ID has been read from memory, and
initDataSourcewas not called (unless memory really does not have it) - ✅ Database driver uses
pymysql, notmysql.connector(this project environment only has pymysql) - ✅ When using
customStylesfor styles, reuse the border format from the already-read example file; do not additionally read thestyling.md
File reading takes the minimal set by scenario,only read the files specified in the table; do not read other files additionally:
| Scenario | Files to read (precise, no extra reads) |
|---|---|
| Ordinary table / JSON dataset (no grouping) | Read only references/pitfalls.md(when encountering an error, jump by symptom index; use offset to read only the corresponding section) |
| Vertical grouping (including subtotals/aggregation) | references/pitfalls.md §Dataset save (offset=15, limit=40) + examples/vertical-group-subtotal-example.md |
| Vertical grouping + custom sorting (textOrders) | Read only examples/vertical-group-custom-sort.md;API dataset scenarioSee the API+mock+textOrders entry in "Known pitfalls" below |
| customGroup (custom horizontal grouping, each record → one column) | Read only references/horizontal-grouping.md(§5.3.2 already has the complete JSON template embedded + all key properties; no need to read examples/horizontal-group.md again) |
| groupRight + dynamic (cross table, column headers expand dynamically horizontally) | references/horizontal-grouping.md + examples/horizontal-group.md Example 1 |
| groupRight + dynamic + follow-group extended statistics row (rightFollowExten) | Read only references/horizontal-grouping.md §"Follow-group extended statistics row" section |
| Contains barcode/QR code / fill blank rows / custom editable cells | Read only references/cell-config.md |
| Contains query controls (paramList) | references/query-params.md § 0 + examples/param-query.md |
| Contains charts (no linkage/drill-down) | references/chart-types-quickref.md(choose chartType) → references/chart-canonical-configs.md + references/chart-echarts-templates.md;Chart display count/sorting/data filtering requirements must first check references/chart-echarts-props.md §9; do not change the dataset type or slice data |
| Contains chart linkage | Read only examples/chart-linkage.md(already includes the complete extData/linkType=2/paramValue="name"/parameter structure; no need to read canonical-configs again); only additionally read when chartType is uncertain references/chart-types-quickref.md(total ≤2 files).Strictly forbidden to read pitfalls.md: This file has 26742 tokens and will definitely exceed the limit and error out; linked scenarios don't need it either.⚠️ For the API dataset chart linkage reuse scenario, directly use scripts/create_chart_linkage.py;chart_linkage() 's linkages parameterdoes not take effect, must patch (see "Known Pitfalls") |
| includes report drill-down | references/report-drilling.md + examples/report-drilling.md |
| includes expressions | read-only references/expressions.md |
| data entry report | read-only references/fillform.md |
overlay printing report(${db.field} single binding, background image covers the entire form) |
read-only references/misc-config.md §overlay printing imgList configuration (complete code template); for a summary of pitfalls see references/pitfalls.md §overlay printing background image (offset=292, limit=10) |
| master-detail report (overlay-printing style or loop-block style, detail table expands vertically, eri≥40) | references/mastersub-report.md + examples/master-sub-table.md |
| column report (loopTime horizontal looping, eri=actual number of card rows, buffer rows prohibited) | read-only references/loopblock-grouping.md §5(offset=106) |
| zoned edition (parallel independent expansion, zonedEdition/zonedEditionList) | read-only references/loopblock-grouping.md §5.5(offset=219,limit=60) |
| multi-source report(multiple datasets come from different tables and have a common related field; or the user requires the same row to bind fields from both the master and detail tables simultaneously) | read-only examples/multi-source-table.md |
| style details (borders/whitespace/priority) | read-only references/styling.md |
| custom styles array (including any color / bgcolor / font properties) | must-read references/styling.md(color text color must be placed at the top level of style, not inside font; otherwise it silently fails) |
| modify existing report cell configuration (style / merge / text / direction, etc.) | must-read references/styling.md, confirm the property hierarchy before making changes |
| file dataset (Excel/CSV/JSON, including scenarios where the user has not provided a file and it needs to be created automatically) | read-only references/dataset-advanced-file.md(§pre-auto-creation + §4.0 upload process) |
| data source management (MongoDB/Redis/MySQL, etc. addDataSource) | read-only references/dataset-advanced.md § 1 |
| report combination (create/update/delete) | read-only references/report-group.md |
| has reference report ID | get_report copy directly, skip all files |
| sample data/fields needed but not yet determined | read-only references/mock-apis.md select interface |
pitfalls.md reading rules: only the "ordinary table/no grouping" row requiresthe entire sectionto be read; for other scenarios, when a runtime error occurs, find the corresponding section by the symptom index at the top of pitfalls.md, and use
Read(offset=N, limit=40)to read only that section (do not read the whole thing; each of the 9 sections is about 20 lines).⚠️ pitfalls.md has a total of 26742 tokens; reading the entire file is prohibited in any scenario (it will definitely exceed the 25000 token limit and error out); linkage/chart scenarios do not need to read it at all.
API dataset + customGroup quick path(when the user provides an API URL + token, no more than 3 steps):
- read
references/horizontal-grouping.md§5.3.2 to get the template- call it in the script
parse_api(session, api_url)to get field names (1 HTTP request, merged into the script,not executed separately)- Replace field names in the template → PowerShell execution script
Custom chart script specification (including SQL/API datasets)
Before writing a custom chart script, you must first confirm the following specifications to avoid repeated debugging.
# ① BASE_URL 必须含 /jmreport
session = Session("http://host:port/jmreport", TOKEN)
# ② make_designer:首次 /save 时 report_id 传空字符串
designer = make_designer("", REPORT_NAME)
# !! 拿到 report_id 后,最终保存前必须重新构造 designer(带真实 id),否则单元格数据不会写入 !!
# designer = make_designer(report_id, REPORT_NAME) ← 最终保存前用这个
# ③ rows 必须带 "len" key
rows = {"len": 200, "1": {"height": 25, "cells": {}}, ...}
# ④ base_save 前两个参数是位置参数,不可用关键字
resp = session.request("/save", base_save("", designer, rows=rows, cols=cols, styles=styles, merges=[], chartList=[]))
# ⑤ 首次 /save 返回的 result 是 dict,取 id
report_id = resp["result"]["id"]
# ⑥ save_db 位置参数顺序(第4个是 db_name,不是 db_ch_name)
db_id = save_db(session, report_id, "db_code", "中文名", sql_or_empty, field_list,
db_type="1", api_url=mock_url, ...) # API 数据集
# ⚠️ JSON数据集(db_type="3")json_data 必须包裹 {"data":[...]},不能传裸数组:
# json_data=json.dumps({"data": raw_list}, ensure_ascii=False)
# ⑦ 图表回填:
# SQL → parallel_fill_charts(session, chart_list)
# API → parallel_fill_charts(session, chart_list)
# JSON → parallel_fill_charts(session, chart_list, json_records_map={dbCode: records, ...})
# JSON 数据集无服务端查询接口,必须通过 json_records_map 传入原始 records 本地填充。
# 不传 json_records_map 则 JSON 数据集图表 config 保持空 data:[],预览显示空白。
# 完整代码见 references/chart-echarts-templates.md § SQL/API 图表数据回填
# ⑧ 最终保存:get_report 返回的 design 可直接 **展开
designer2, design2 = get_report(session, report_id)
design2["chartList"] = filled_charts
session.request("/save", base_save(report_id, designer2, **design2))
Known pitfalls (must read)
Before writing a script, first Read
references/pitfalls.md, to avoid wasting time on pitfalls and retries. Before writing a loop block report, you must first Readreferences/loopblock-grouping.md, which contains a complete configuration template; writing it from scratch yourself is prohibited. ⚠️ When an API dataset hasparamList,apiUrlat the end you must append?paramName=${paramName}, otherwise linkage/drill-down parameters cannot be passed in. Connect multiple parameters with&. After writingsave_db, immediately visually check that theapi_urlstring contains placeholders. ⚠️ Crosstab (groupRight+dynamic) bottom row that follows group expansion statistics: must usean independent static row +rightFollowExten:"follow"+ Excel formula(e.g.=MAX(G6)), the dynamic cell'sfuncnamemust be set to"-1",Prohibitedset to"MAX"/"MIN"/"SUM"—that will not generate a statistics row that follows horizontal expansion. For non-grouping dimension fields (such as name, gender), useaggregate:"select", notgroup(). Seereferences/horizontal-grouping.md§ "Statistics row following group expansion". ⚠️ Style patch must not skip col "0": In grouping/standard reports generated by creator, the title cell may be inrows["1"]["cells"]["0"](merged across all columns starting from col 0). When the patch script iterates over cells, itmust notif ck == '0': continue, otherwise the title style will never be updated. Correct approach: iterate over all cols, determine whether there ismergeand the text does not start with#{; that is the title cell, and directly update its style. ⚠️ mock path conflict causes retries:create_mock()will throw a RuntimeError when the path already exists. mock_path must use at the endtimestamp(import time; mock_path=f"/xxx_{int(time.time())}") rather than a fixed date, to ensure uniqueness. The original path may be reused only when the user explicitly says "update an existing interface". ✅ API dataset dbType correction and chart dataType are now built into creator (2026-05-23):jimureport_creator.pynow automatically callsdbType:"1"after saving an API dataset (update_dbto correct dbType/apiUrl, and before/saveautomatically sets the chart extData'sdataType。according to the dataset's actual type (sql/api/json/javabean/files), so there is no need to manually patch anymore; just write the JSON config directly. Supports all types: SQL→"sql", API→"api", JSON→"json", JavaBean→"javabean", file→"files", shared→"sql". ⚠️ Barcodes/QR codes must use display inline; displayConfig floating layers are prohibited: In the cell write only"display":"barcode"/"display":"qrcode",without adding"config"field, do not pass it in the payloaddisplayConfig。displayConfig + config; this method will create a floating overlay layer that covers the header and other rows, completely disrupting the layout (actually tested on 2026-05-22). ⚠️ create + patch must be merged into a single PowerShell call: When you need to patch immediately after creating the report (such as adding a barcode/QR code),must not be split into two PowerShell calls. The correct approach is in the same Python script: callapi_dataset()→ parse stdout to get report_id → immediately callget_report + base_saveto complete the patch. Splitting into two steps drags a second-level operation into a minute-level one, and any error in between requires manual intervention. ⚠️ The report_id parsing regex usesreport_id=for anchoring, don't rely on guessing the format:jimureport_creator.pystdout has a fixed output ofreport_id=<id>format, directly usere.search(r'report_id[=:\s]+(\S+)', stdout)to extract, which is more reliable than guessing the format by ID length/character set. ⚠️api_dataset()may returnCompletedProcessinstead of a report_id string: after calling, you must do a compatibility check, otherwise the subsequentget_report(session, report_id)will send the whole object as an ID in the request, causing a 400 error and forcing a split into a second PowerShell execution (seconds become minutes). The standard way:result = api_dataset(...) if hasattr(result, 'stdout'): import re report_id = re.search(r'report_id[=:\s]+(\S+)', result.stdout).group(1) else: report_id = result⚠️ Do not grep the function signature:
api_dataset、run_config、DEFAULT_BASE_URLThe complete examples of parameter formats such as these are already in SKILL.md; there is no need to grep/read jimureport_gen.py to confirm. Grepping the function signature wastes 3-5 tool calls each time; just trust the documentation. ⚠️chart_linkage()'slinkagesparameter is silently ignored:jimureport_gen.chart_linkage()accepts thelinkagesparameter butdoes not handle it—the function only creates the report layout and does not call/link/saveAndEdit, and linkIds are not written to cells/chartList either. Correct approach:chart_linkage()after creating the report, you must add a patch step: ① call/link/saveAndEditto create the linkage record and get link_id; ②get_reportfetch the design; ③ writelinkIds+display:"link"to the cell, and writelinkIds;④base_saveback to the triggering chart's extData. Already packaged asscripts/create_chart_linkage.py, call it directly.create_chart_linkage.pyis a provided tool script, calling it directly is fine; but if it fails midway (e.g.list_mocks()cannot find the create interface),do notrewrite a full-process Python script to replace it—the correct approach is: use the known interface ID to directly make up the call toset_advmock(minimal PowerShell inline), change the report structure via JSON config + creator CLI, and write a separate minimal PowerShell inline for the linkage patch, keeping the two parts strictly separate. ⚠️chart_linkage()The dataset keys passed to creator must be camelCase: keys in datasets usedbCode/apiUrl/fieldList/paramList, keys in table/charts usedatasetCode/chartType. Usingdb_code/api_url/field_listand other snake_case will causeKeyError: 'dbCode'(creator reads JSON fields directly and does not convert). ⚠️ yapi_mock does not expose a session object, uselist_mocks()to get the interface ID:_session/sessionand other attributes do not exist, don't waste time usinginspect.getmembersto find the session. Correct approach:init_yapi()after logging in, directlyfrom yapi_mock import list_mocks; ifaces = list_mocks()get the interface list, then match by path to find_id。 ⚠️mastersub()The correct signature is(token, name, master_sql, sub_sql, master_cols, sub_cols, *, link_field, sub_param, db_source):sub_sqlin position 4,master_colsin position 5, the order cannot be swapped. After swapping, creator will pass the master_cols list as SQL toqueryFieldBySql, reporting "signature verification failed". ⚠️mastersub()only generates overlay-style layouts, and does not support loop-block style (master-detail loop): after calling, creator reports "no table or chart configured" because mastersub's JSON template goes through${}single-value binding path and cannot generate loopBlockList.Correct approach: abandonmastersub(), switch to the following 3 steps: ①parse_and_save_datasetSave the primary and sub datasets; ② manually build rows (add to all cells"loopBlock":1, including merged empty cells in the header row, buffer row eri≥40); ③base_savepass inloopBlockList=[{"sci":1,"eci":N,"sri":0,"eri":40,"index":1,"db":MAIN_CODE}], then call/link/saveAndEditlinkType="4". See the complete code template inreferences/mastersub-report.md§II. ⚠️ The master-sub cyclic report JSON config does not supportloopBlockList, and the creator will report "no table or chart configured": The JSON config + creator CLI path is invalid for cyclic block-style master-sub reports. The correct approach has two steps: ① first run the JSON config to let the creator finish saving the datasets (the datasets part executes normally; the layout step error can be ignored; report_id has been generated); ② then use a minimal PowerShell inline call tobase_save(including manual rows + loopBlockList) +/link/saveAndEdit. The two steps are merged into a single PowerShell call. ⚠️ All chart configxAxis/yAxismust be an object{...}; arrays[{...}]are forbidden (even if there is only one axis): Although the array format can render normally (ECharts accepts it), it makes the designer's "Data" panel'sgetChartDataListwhen readingoptions['xAxis'].datagetundefinedand throwCannot read properties of undefined (reading 'length'), causingselectChartto crash beforehandleChartExtData→ the chart's "Data Type" dropdown stays at the default value "Api dataset", making it look as if the JSON/SQL dataset was treated as an API dataset.jimureport_creator.build_echarts_configwas fixed to the object format on 2026-05-26; when hand-writing configs or patching, the object format must likewise be used. To fix an existing report:get_report→ for each chart, changecfg['xAxis']=cfg['xAxis'][0](same for yAxis) →base_saveand write back. ⚠️ In chart linkage, the "trigger chart"'sxAxismust be in object format, otherwise clicking cannot trigger the linkage:chart_entry()In the ECharts config of the generated bar chart,xAxisis in array format[{type:'category',data:[]}](the standard multi-axis format), but the frontendrefreshChart()when updating the X-axis label writeschartOptions['xAxis']['data'] = names—performing this operation on an array merely adds a custom property to the array object, andxAxis[0].dataremains empty. Result: when a bar is clicked, the ECharts click event'sevent.name = "", the linkage parameter checkif(param["name"])is false, and the target chart never refreshes.Fix method: After the report is created, for each bar chart that "serves as a linkage trigger source" (extData haslinkIds), change the config'sxAxis/yAxisfrom array to object:import json designer, design = get_report(session, report_id) for c in design.get("chartList", []): ext = c.get("extData", {}) if ext.get("linkIds"): # 该图表是联动触发源 cfg = json.loads(c["config"]) if isinstance(cfg.get("xAxis"), list) and cfg["xAxis"]: cfg["xAxis"] = cfg["xAxis"][0] if isinstance(cfg.get("yAxis"), list) and cfg["yAxis"]: cfg["yAxis"] = cfg["yAxis"][0] c["config"] = json.dumps(cfg, ensure_ascii=False) session.request("/save", base_save(report_id, designer, **design))This patch should be executed immediately after the linkIds backfill step, merged into the same PowerShell inline. ⚠️ Correct process for API dataset + mock + textOrders custom sorting: The report structure must be in the JSON config; the mock creation + report creation + patch must not be wrapped in the same custom .py script. Correct four steps (merged into a single PowerShell call): ① minimal inline script (only containing
init_yapi + create_mock) → capture mockUrl; ② write the JSON config (including mockUrl, columns use standard properties, not including textOrders); ③ run thejimureport_creator.pyCLI → capture report_id; ④ minimal inline script (get_report→ iterate over cells to addtextOrders→base_save). ②③④ execute sequentially within the same PowerShell call; ① can be split out or merged, either is fine.⚠️ Multi-file datasets (dbType=5) must be split into two steps; merging into one custom Python script is prohibited: file upload +
saveDbare prerequisite steps that cannot be expressed in JSON config, but the report layout (rows/cols/styles) must still go through JSON + creator CLI. Correct split:
- Step 1 (minimal PowerShell inline): upload files one by one (
session.upload('/source/datasource/files/add', ...))→GET /source/datasource/files/getgetdatasource_idand table name → build JOIN SQL →queryFieldBySqlparse fields →saveDbfirst step (no id) → getdb_id→saveDbsecond step (with id, only then is fieldList truly persisted) → outputreport_id、dbCode- Step 2(JSON config + creator CLI): use Step 1's
report_idanddbCodeto write the JSON layout →python jimureport_creator.py --config xxx.jsonCalcite reserved word handling:
type、countwhen used as an Excel column name, must be escaped with backticks in SQL and given an alias (e.g.b.`type` AS prod_type,a.`count` AS sales_count); double quotes (b."type") are parsed as string literals in Calcite, causingSQL语法错误。⚠️ File dataset Excel/CSV must contain at least one row of data:
/source/dataset/files/single/saveneeds to parse the file to infer field types; a pure header (only row1) will return{"success":false,"message":"解析失败,请检查文件格式符合要求"}. When automatically creating files, row1=header, row2=one row of reasonable sample data (can be fake data); sample data does not affect report display.⚠️ Table creation scripts must INSERT with explicit column names, and DROP before creating:
INSERT INTO t VALUES (...)when column names are not written, if the target table already exists and the column count/order differs, MySQL reportsColumn count doesn't match, forcing another PowerShell call to retry, directly adding one round trip (ideally 2 times → actually 3 times). Correct way: always useINSERT INTO t (col1, col2, ...) VALUES (...)explicit column names; at the start of the table creation script firstDROP TABLE IF EXISTS, ensuring success in one go:cur.execute("DROP TABLE IF EXISTS my_table") cur.execute("CREATE TABLE my_table (id VARCHAR(32) PRIMARY KEY, ...)") cur.execute("INSERT INTO my_table (id, name, ...) VALUES (%s, %s, ...)", row)⚠️ Chart extData must have both
idandchartId; missingidwill cause the chart not to render: the chart extData generated by creator sometimes only writeschartIdand omitsid; both must equallayer_id. When patching an existing report, if extData is found to lack theidfield, it must be filled in:ext['id'] = c['layer_id']. Symptom: the chart area on the preview page is blank, with no error or reportingCannot read properties of undefined。⚠️ pie.simple's extData.series must be
""; writing"type"incorrectly will report TypeError: for a single-series pie chart, theseriesfield must be an empty string; creator occasionally sets it incorrectly to"type". During frontend initialization it groups by the series field name; if the data has notypecolumn thenundefined[0]reportsTypeError: Cannot read properties of undefined (reading '0'). When patching, check and correct:ext['series'] = ''。⚠️ Patching a chart background color requires setting two places at the same time: the chart container background is
chartList[n]['backgroud'](a typo in the framework source code; the actual field name is indeedbackgroud, notbackground), and the ECharts canvas background isconfig['backgroundColor']. Setting only one of them gives an incomplete effect. Correct way:c['backgroud'] = {'enabled': True, 'color': '#e8f5e9', 'image': ''} cfg = json.loads(c['config']) cfg['backgroundColor'] = '#e8f5e9' c['config'] = json.dumps(cfg, ensure_ascii=False)
Report creation method comparison (required reading)
Jimu Report has three creation levels;prefer choosing from top to bottom:
| Level | Method | Applicable scenario |
|---|---|---|
| ① JSON + CLI(Only recommended) | Write xxx.json → python jimureport_creator.py --api-base URL --token TOKEN --config xxx.json |
all reports, JSON files visible and reproducible |
| ② Python wrapper functions | from jimureport_gen import group, api_dataset, ... |
Internally auto-generates JSON + calls CLI; use this approach when YApi mock also needs to be created |
⛔ Do not write custom Python scripts to create reports(User 2026-05-22 clarified again:from now on only generate JSON). The report structure must be written in a JSON config file; API dataset + mock go through
jimureport_gen.api_dataset()(internally also JSON+CLI). Steps that JSON cannot express, such as table creation,/link/saveAndEditetc., should be written as minimal standalone PowerShell inline;must notmix report creation logic into Python scripts. ⚠️ Typical error scenario 3: Chart linkage scenario (table + bar chart + pie chart), becauselist_mocks()failed midway, the AI wrote mock creation +chart_linkage()call + linkIds patch all into onechart_linkage_patch.pycustom script — serious violation. Correct approach: ① When mock does not have advmock set, call in a separate PowerShell inline lineset_advmock(已知ID); ② Write report structure as JSON config → creator CLI; ③ Linkage patch (/link/saveAndEdit+get_report+base_save) written as another minimal PowerShell inline; the three steps are each independent and not mixed into the same large Python script.
jimureport_gen.py function quick reference
jimureport_gen.py is a Python wrapper for jimureport_creator.py and internally auto-writes JSON + calls CLI.
import sys
sys.path.insert(0, r'<skill_base_dir>/scripts')
from jimureport_gen import group, api_dataset, standard # 按需 import
| Function | Report type | Key parameters |
|---|---|---|
standard(token, name, sql, columns) |
Normal detail table | db_source, is_page, chart |
group(token, name, sql, columns) |
Vertical grouping/subtotal | Add to group column group=True, subtotalText="合计" |
api_dataset(token, name, field_list, columns, *, api_url=...) |
API dataset (existing URL) | report_type="group" can be changed to grouping mode |
api_dataset(token, name, field_list, columns, *, mock_data=[...], mock_path=f"/xxx_{int(time.time())}") |
API dataset (auto-create mock) | Automatically create YApi mock interface then create report, in one step; mock_path must use a timestamp, fixed dates are forbidden |
horizontal_group(token, name, sql, *, group_field, row_fields, value_fields) |
Cross table | statistics_rows append statistics row |
param_query(token, name, sql, columns, param_list) |
With query conditions | param_list includes searchMode |
multilevel(token, name, sql, columns, header_groups) |
Multi-level header | header_groups=[{"title":"分组","span":3}] |
mastersub(token, name, master_sql, sub_sql, master_cols, sub_cols, *, link_field, sub_param, db_source) |
Master-detail tableOverlay printing(${}single-value paging) |
⚠️ Limited to overlay printing style only; loop block style (master-detail loop)must not use this function, see "Known pitfalls" |
loopblock(token, name, sql, columns, *, group_label) |
Loop block | group_label is the group field name |
fillform(token, name, sql, columns, *, key_field) |
Data entry | enable_add/edit/delete |
drilling(token, name, sql, columns, drilling_list) |
Report drill-down | drilling_list includes params |
chart_linkage(token, name, datasets, table, charts, linkages) |
Chart linkage | ⚠️ linkages parameters are silently ignored, the function only creates the report layout and does not handle linkage; the datasets key must be camelCase (dbCode/apiUrl/fieldList); ✅ API dataset chart dataType has been automatically corrected by creator, no manual patch needed. For the complete process see scripts/create_chart_linkage.py |
json_dataset(token, name, records, field_list, columns) |
JSON static dataset | field_list=[["field","名称"]] |
template_print(token, name, sql, cells, bg_image) |
Overlay printing | bg_image={"localPath":...,"cols":7,"rows":15} |
file_dataset(token, name, file_path, columns) |
Excel/CSV file | columns 's field uses pinyin;Excel/CSV must have at least one row of data (pure header reports "parse failed"), when automatically creating the file add a row of sample data at row2 |
multisource(token, name, sections) |
Multi-source horizontal concatenation | sections Contains multiple datasets + columns |
multisheet(token, name, sheets) |
Multiple Sheets | sheets Corresponds to the multisheet type configuration |
Call example (API dataset grouped report, automatically creates mock):
from jimureport_gen import api_dataset
api_dataset(
token="xxx",
name="学生信息统计表",
field_list=[["grade","年级"],["class_name","班级"],["name","姓名"],
["gender","性别"],["age","年龄"],["total_score","总成绩"]],
columns=[
{"field":"grade", "title":"年级", "width":120, "group":True, "subtotalText":"合计"},
{"field":"class_name", "title":"班级", "width":120, "group":True, "subtotalText":"小计"},
{"field":"name", "title":"姓名", "width":100},
{"field":"gender", "title":"性别", "width":80},
{"field":"age", "title":"年龄", "width":80},
{"field":"total_score", "title":"总成绩", "width":100, "funcname":"AVERAGE"},
],
mock_data=[
{"grade":"一年级","class_name":"一班","name":"张三","gender":"男","age":7,"total_score":285},
# ...更多数据
],
mock_path=f"/student_info_{int(time.time())}", # 必须用时间戳,禁止固定日期
report_type="group",
)
Tool scripts (use as needed)
| Operation | Command |
|---|---|
| Query report | python report_tools.py --base-url URL --token T list [-k 关键词] |
| Report details | python report_tools.py --base-url URL --token T detail <id> |
| Delete/copy | python report_tools.py --base-url URL --token T delete/copy <id> |
| Query report group list | python report_tools.py --base-url URL --token T group-list [-k 关键词] |
| Create/update report group | python report_tools.py --base-url URL --token T group-save --name "名称" --ids "id1,id2" [--names "报表1,报表2"] [--descr "描述"] [--group-id <id>] |
| Delete report group | python report_tools.py --base-url URL --token T group-delete <组合ID> |
| Share | python report_export.py share --name 名称 |
| Export | python report_export.py export <id> --format pdf |
| Create scheduled export task | python report_export.py schedule-export --name 任务名 --reports '[{"reportId":"id","exportType":"PDF","name":"报表名"}]' --cron "0 0 8 * * ?" [--email xxx@qq.com] [--begin "YYYY-MM-DD HH:mm:ss"] [--end "YYYY-MM-DD HH:mm:ss"] |
| Modify scheduled export task | Same as above, append --job-id <任务ID>(Creation and modification share the same API) |
| Query scheduled export task list | python report_export.py job-list [--name 关键词](API is /auto/export/job/query,/list returns 404) |
| Delete scheduled export task | python report_export.py job-delete <job_id> |
| Run scheduled export task immediately | python report_export.py job-run <job_id>(returns execution batch number) |
| Start scheduled export task | python report_export.py job-start <job_id> |
| Stop scheduled export task | python report_export.py job-stop <job_id> |
| Change chart type | python chart_tools.py change-type <id> bar.multi |
| Create YApi Mock | python yapi_mock.py list |
| Multi-file dataset report(Excel/CSV JOIN) | python multi_file_report.py --token T --files a.xlsx b.xlsx --name "名称" [--join-on product_id] [--db-code myds] [--base-url URL] |
YApi Mock data source
When a report needs an API data source, use the built-in YApi platform (https://api.jeecg.com)创建 mock API. Login credential handling flow (mandatory; violation causes execution failure and retry):
- Before starting, you must first read the YApi credential record in memory (
reference_yapi_credentials.md) and must not skip it - If present, write it directly into the script;If not present or
init_yapiauthentication fails → ask the user for a new password, and immediately update memory once obtained - After the user provides it, immediately save it to memory (reference type); read it directly next time without asking again
init_yapi()credentials must be passed explicitly:init_yapi(email=EMAIL, password=PWD), otherwise it will fail with "email cannot be empty"api_dataset()Whenmock_datais present, the credentials must be passed directly toapi_dataset():api_dataset(..., yapi_email=EMAIL, yapi_password=PWD)— internally it will callinit_yapiagain; callinginit_yapi()separately in advance does not take effect and will report "email cannot be empty"
import sys
sys.path.insert(0, '<skill目录>/scripts')
from yapi_mock import init_yapi, create_mock
init_yapi() # 自动登录
mock_url = create_mock(
path='/sales', # 路径后缀,不含 basepath(/claude)
title='销售数据',
data=[
{"month": "一月", "amount": 12000},
{"month": "二月", "amount": 15000},
]
)
# mock_url = https://api.jeecg.com/mock/57/claude/sales
Fixed parameters:project_id=57,catid=1157,basepath=/claude
Path rules: Only write the suffix in the API path (e.g. /sales), full URL = https://api.jeecg.com/mock/57/claude/sales
⚠️ Create vs Modify (strictly distinguish to prevent polluting others' data):
| User intent | Approach |
|---|---|
| Create API (default) | Append a timestamp or sequence number to the path, e.g. /sales_20260427, to force creation of a new API |
| Explicitly say "modify/update existing API" | Directly use the original path to call create_mock() or update_mock() |
create_mock()encountering the same path willsilently overwrite existing API data, which will destroy others' APIs in shared projects. By default, a unique path must be used.
Pagination rule: Self-built mock APIdoes not need pagination,data and directly returns the complete array without adding pageNo / pageSize parameters.
If the user explicitly requires the mock to support pagination(A few scenarios, such as demonstrating the "pagination + field query" linkage), requires using
set_advmockto write advanced scripts, and strictly follow the response field semantics below —totalis the total number of pages,countis the total number of records, and the two must not be confused:// set_advmock 脚本中: var total = Math.ceil(count / pageSize); // 总页数(≠ 总条数) mockJson = { "data": paged, // 当前页记录数组 "count": filteredLen, // 总条数(过滤后) "total": total, // 总页数 "pageNo": pageNo, "pageSize": pageSize };assigning the total number of records to
totalwill cause the total number of pages displayed by the JimuReport paginator to be incorrect. Seereferences/dataset-core.md§3.3。
When using a mock URL as an API dataset in JimuReport, call save_db(..., db_type="1", api_url=mock_url) and it is enough;dbCode is a custom code (such as sales_data), unrelated to the URL.
API dataset query condition parameter passing rules: see the "Known Pitfalls" section (including code examples) and references/dataset-core.md § 3.2.2。
fieldList must include orderNum(starting from 0), otherwise the dataset's "sort" column is empty and the field order is uncertain:
{"fieldName": "col1", "fieldText": "列1", "fieldType": "String", "orderNum": 0},
{"fieldName": "col2", "fieldText": "列2", "fieldType": "String", "orderNum": 1},
Query the list of APIs already created by AI
init_yapi() # 先登录,自动带上 Cookie
# 列出 project_id=57 下已创建的所有接口(需登录态)
# GET https://api.jeecg.com/api/interface/list?page=1&limit=20&project_id=57
# 返回字段:_id(接口ID), path(路径), title(名称), method, up_time
# 完整 mock URL = https://api.jeecg.com/mock/57{path}
Command-line equivalent:python yapi_mock.py list(automatically logs in and lists all APIs in pages)
Advanced Mock script (dynamically returns different data based on parameters)
Automatic determination rules (AI must comply):
⛔ Prerequisite: the following rules take effect only when the user has not provided any API URL. If the user has already provided a URL → directly
parse_api→save_db, and calling is prohibited throughoutcreate_mock/init_yapi, regardless of whether the report has linkage/drill-down.
| Scenario | Approach |
|---|---|
| User has already provided an API URL(regardless of whether there is linkage) | ⛔ Creating a mock is prohibited, directly parse_api(url) → save_db(api_url=url) |
User has not provided a URL; the report has linkage/drill-down, and the dataset needs to be filtered by parameters (such as ?category=手机) |
create_mock create the interface → set_advmock write the script, and in the script return by params.xxx branch |
| User has not provided a URL, static display, and no parameter-based filtering is needed | Use only create_mock, do not call set_advmock |
⚠️ The correct API is
/api/plugin/advmock/save, and it is prohibited to use/api/interface/up'sisOpen/scriptfield — that field does not take effect.
from yapi_mock import init_yapi, set_advmock
init_yapi(email='...', password='...')
# 先用 create_mock / list_mocks 拿到接口 ID,再调:
set_advmock(
iface_id='5933', # 接口 _id(字符串)
script=r"""
if(params.leibie=='手机'){
mockJson = {"data": [{"name":"苹果手机","value":25500}]}
}else if(params.leibie=='电脑'){
mockJson = {"data": [{"name":"MacBook","value":47500}]}
}else{
mockJson = {"data": [...]} // 兜底默认值,无参数时返回
}
""",
enable=True,
)
Script rules:
- Use
params.xxxto read URL query parameters - Use
mockJson = {...}to assign the response body (notreturn) - A fallback
elsebranch must be written to ensure there is data when no parameters are present - Field names must exactly match the JimuReport dataset's
axisX/axisY(orfieldName)
Common configuration quick reference
Freeze rows and columns (freeze)
base_save(report_id, designer, ..., freeze="A3", freezeLineColor="red")
| freeze value | Effect |
|---|---|
"A1" |
Do not freeze (default) |
"A2" |
Freeze row 1 |
"A3" |
Freeze the first 2 rows |
"C1" |
Freeze the first 2 columns |
"C3" |
Freeze both the first 2 rows and the first 2 columns |
Report background color (background)
# 启用背景色(在 base_save override 里传,或 patch 时改 design["background"])
base_save(report_id, designer, ..., background={"enabled": True, "color": "#e8f5e9"})
# 禁用背景色
base_save(report_id, designer, ..., background=False)
⚠️
bgColoris written inmake_designer(**extra)inInvalid; it must be passed as a top-level override ofbase_save. When patching an existing report:design["background"] = {"enabled": True, "color": "#hex"}→base_save(id, designer, **design)
Preview page toolbar (rpbar)
btnList/childrenBtnListstores the button numbers of "Display"; deleting numbers from the list hides them,[]= hide all.rpbarmust be a dict object, cannotjson.dumps()。
base_save(report_id, designer, ..., rpbar={
"show": True, "pageSize": "",
"btnList": [1,2,3,4,5,6,7,8,9],
"childrenBtnList": [7.1,7.2,7.3,7.4,8.1,8.2,8.3,8.4,8.5,8.6]
})
| Main button | Sub button | ||
|---|---|---|---|
| 1 First page | 5 Next page | 7.1 Default print | 8.1 Export Excel |
| 2 Previous page | 6 Last page | 7.2 Print current page | 8.2 Large data Excel |
| 3 Current/total pages | 7 Print | 7.3 Paginated scaled print | 8.3 Export PDF |
| 4 Rows per page | 8 Export | 7.4 Overall scaled print | 8.4 Export PDF image |
| 9 Clarity | 8.5 Export image / 8.6 Export WORD |
Chart extData required fields
| Field | Description |
|---|---|
| chartId / id | = layer_id (both must be filled) |
| chartType | bar.simple / pie.normal / line.simple etc. |
| dataType | "sql" / "api" / "json" / "files" / "javabean" |
| apiStatus | "1"=dynamic SQL / "0"=static JSON |
| dataId | save_db() return value |
| dbCode | Dataset code |
| axisX / axisY | X/Y axis field names |
| series | Grouping field name, pass for single series "" |
Drill-down/linkage linkType reference
| linkType | Description | reportId | paramValue |
|---|---|---|---|
| "0" | Report drill-down | Target report ID | Field name |
| "1" | Web link | Current report ID | Field name |
| "2" Table→Chart | Linkage | Current report ID | Field name |
| "2" Chart→Chart | Linkage | Current report ID | name/value/seriesName |
| "4" | Main-sub report | Current report ID | — |
- Cell trigger:
cell["linkIds"] = link_id+cell["display"] = "link" - Chart trigger:
extData["linkIds"] = link_id(placed inside extData, not at the top level) - Linkage must have
linkChartId(target chart layer_id); the target chart paramList must set a paramValue default
Image cell (local image insertion)
⚠️ Do not use base64Img for large images (>20KB): base64 encoding bloats the JSON to MB scale, making save/load extremely slow.
Correct approach: first upload to get a URL, then use display:"img" to reference:
from report_tools import upload_image # 封装好的上传函数
from jimureport_utils import Session
session = Session(BASE_URL, TOKEN)
msg, _ = upload_image(session, r"C:\path\to\image.png")
# OSS 场景 msg 已是完整 https URL;本地存储需拼前缀
img_url = msg if msg.startswith("http") else f"{BASE_URL}/img/{msg}"
Cell configuration (customRows or patch method):
{"text": "http://...oss-url.../image.png", "display": "img", "merge": [extraRows, extraCols]}
base64ImgOnly for very small icons (< 20KB). Overprint background images also useupload_image, which has been internally corrected to the/uploadendpoint (2026-05-25).
searchMode mapping
| searchMode | Control |
|---|---|
| 1 | Input box |
| 2 | Range query |
| 3 | Dropdown multi-select |
| 4 | Dropdown single-select |
| 5 | Fuzzy query |
| 6 | Dropdown tree |
| 8 | Time control |