@linkaicli/linkai-cli 0.0.0-stage → 1.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/AGENTS.md +64 -0
  2. package/GUIDE.md +150 -0
  3. package/LICENSE +190 -0
  4. package/NOTICE +6 -0
  5. package/README.md +82 -2
  6. package/THIRD_PARTY_NOTICES.txt +318 -0
  7. package/cli-manifest.json +1850 -0
  8. package/clis/_shared/common.js +1 -0
  9. package/clis/_shared/site-auth.js +1 -0
  10. package/clis/linkai/build.js +1 -0
  11. package/clis/linkai/card-rules.js +1 -0
  12. package/clis/linkai/collab.js +1 -0
  13. package/clis/linkai/commands.js +4 -0
  14. package/clis/linkai/core.js +12 -0
  15. package/clis/linkai/deliverable.js +2 -0
  16. package/clis/linkai/dictionary.js +1 -0
  17. package/clis/linkai/fill-card.js +1 -0
  18. package/clis/linkai/oauth.js +8 -0
  19. package/clis/linkai/reuse.js +1 -0
  20. package/clis/linkai/templates.js +1 -0
  21. package/dist/bundle/browser-cdp.mjs +2 -0
  22. package/dist/bundle/browser-page.mjs +2 -0
  23. package/dist/bundle/browser-utils.mjs +2 -0
  24. package/dist/bundle/core.mjs +3449 -0
  25. package/dist/bundle/daemon.mjs +16 -0
  26. package/dist/bundle/download-article.mjs +2 -0
  27. package/dist/bundle/download-media.mjs +2 -0
  28. package/dist/bundle/download-progress.mjs +2 -0
  29. package/dist/bundle/download.mjs +2 -0
  30. package/dist/bundle/errors.mjs +2 -0
  31. package/dist/bundle/logger.mjs +2 -0
  32. package/dist/bundle/main.mjs +3 -0
  33. package/dist/bundle/oplog.mjs +2 -0
  34. package/dist/bundle/pipeline.mjs +2 -0
  35. package/dist/bundle/registry.mjs +2 -0
  36. package/dist/bundle/skills.mjs +2 -0
  37. package/dist/bundle/types.mjs +2 -0
  38. package/dist/bundle/utils.mjs +2 -0
  39. package/dist/bundle/vendor/readability/Readability-readerable.js +122 -0
  40. package/dist/bundle/vendor/readability/Readability.js +2786 -0
  41. package/knowledge/README.md +29 -0
  42. package/knowledge/browser/README.md +40 -0
  43. package/knowledge/data-dictionary/README.md +35 -0
  44. package/knowledge/data-dictionary/_templates/enum.md +7 -0
  45. package/knowledge/data-dictionary/_templates/metric.md +5 -0
  46. package/knowledge/data-dictionary/_templates/view.md +29 -0
  47. package/knowledge/data-dictionary/enums/.gitkeep +1 -0
  48. package/knowledge/data-dictionary/index.md +12 -0
  49. package/knowledge/data-dictionary/metrics/.gitkeep +1 -0
  50. package/knowledge/data-dictionary/views/.gitkeep +1 -0
  51. package/knowledge/report-builder/README.md +80 -0
  52. package/knowledge/report-builder/SOP.md +91 -0
  53. package/knowledge/report-builder/build-spec.md +79 -0
  54. package/knowledge/report-builder/references/api-catalog.md +106 -0
  55. package/knowledge/report-builder/references/api.md +148 -0
  56. package/knowledge/report-builder/references/auth-space.md +58 -0
  57. package/knowledge/report-builder/references/card.md +382 -0
  58. package/knowledge/report-builder/references/chart-config-schema.md +572 -0
  59. package/knowledge/report-builder/references/chart-types.md +167 -0
  60. package/knowledge/report-builder/references/dashboard-patterns.md +138 -0
  61. package/knowledge/report-builder/references/dataset.md +152 -0
  62. package/knowledge/report-builder/references/fill-import.md +95 -0
  63. package/knowledge/report-builder/references/filters.md +170 -0
  64. package/knowledge/report-builder/references/gantt.md +79 -0
  65. package/knowledge/report-builder/references/pitfalls.md +265 -0
  66. package/knowledge/report-builder/references/query-engine.md +177 -0
  67. package/knowledge/report-builder/references/report.md +249 -0
  68. package/llms.txt +37 -0
  69. package/package.json +105 -4
  70. package/skills/agent-prompt.md +37 -0
  71. package/skills/linkai-browser/SKILL.md +67 -0
  72. package/skills/linkai-collab/SKILL.md +65 -0
  73. package/skills/linkai-data-workflow/SKILL.md +49 -0
  74. package/skills/linkai-data-workflow/assets/import-run-template.json +22 -0
  75. package/skills/linkai-data-workflow/assets/solution-template.md +60 -0
  76. package/skills/linkai-data-workflow/references/excel-analysis-modeling.md +35 -0
  77. package/skills/linkai-data-workflow/references/fill-import.md +95 -0
  78. package/skills/linkai-data-workflow/references/solution-knowledge.md +35 -0
  79. package/skills/linkai-report-builder/SKILL.md +151 -0
  80. package/skills/linkai-usage/SKILL.md +66 -0
  81. package/templates/report-spec/cards/detail-table.json +12 -0
  82. package/templates/report-spec/filters.example.json +28 -0
  83. package/templates/report-spec/plan-knowledge.example.yaml +18 -0
  84. package/templates/report-spec/report.example.yaml +123 -0
  85. package/templates/report-spec/sql/channel.sql +7 -0
  86. package/templates/report-spec/sql/detail.sql +10 -0
  87. package/templates/report-spec/sql/kpi.sql +8 -0
  88. package/templates/report-spec/sql/trend.sql +9 -0
@@ -0,0 +1,148 @@
1
+ # 接口速查
2
+
3
+ 本篇只列**搭报表常用、带实测 body** 的接口。其它常用接口(方法、路径、说明)见 `references/api-catalog.md`。
4
+ 没在这两处的接口不要猜方法和 body,先问用户或领谋支持。
5
+
6
+ | 补充接口 | 方法 路径 | 说明 |
7
+ |---|---|---|
8
+ | 卡片持久排序 | PUT `/page-setting/page-card-index-block-sort-field` | `linkai card-sort`,body 见 `references/query-engine.md` §4 |
9
+ | 从看板删一张卡片 | DELETE `/page-setting/page-card/{pageCardId}` | 卡片仍在卡片库 |
10
+ | 看板自动刷新 | POST `/page-setting/save-page-setting-refersh-param` | `{"id":planId,"isPage":true,"refershSetting":分钟}` |
11
+ | 钻取开关 | POST `/page-setting/update-drill-switch` | `{"userPageCardIds":[…],"isOpen":true}` |
12
+ | 数据集被谁引用 | GET `/index-block/data-set-quoted-infos` | `linkai ds-refs` |
13
+ | 回收站 / 恢复 | POST `/index-block/get-recycle-station-page-list` / `revocer-recycle-station` | 误删卡片时用 |
14
+ | 复制卡片 | GET `/index-block/copy-index-block` | query 参数,见目录 |
15
+
16
+ ## 接口清单
17
+
18
+ ### 卡片(指标块)
19
+ | 用途 | 方法 路径 |
20
+ |---|---|
21
+ | 校验 SQL / 取字段 | POST `/index-block/get-field-info-by-data-set-sql` |
22
+ | 新建 | POST `/index-block/new` |
23
+ | **保存(生效的)** | POST `/index-block/update-new` |
24
+ | 详情 | POST `/index-block/get-new-index-block-detail` `{"id":"..."}` |
25
+ | 卡片库列表 | POST `/index-block/get-index-block-folder-bussiness-info` `{}` |
26
+ | 删除 | POST `/index-block/delete-folder-bussiness` body `["<folderBussinessId>"]` |
27
+ | 筛选器候选值 | POST `/index-block/get-filter-field-value-info` `{"indexBlockDataId":"...","fieldName":"<数据集列别名>"}` → `[{col:"值"}]` |
28
+ | 数据集字段列表 | GET `/index-block-data-argument/data-set-fields/<数据集id>` |
29
+ | 保存数据集 | POST `/index-block-data-argument/add-or-update` |
30
+
31
+ ⚠ 删除用的是列表返回的 `id`,不是 `objId`(objId 才是 indexBlockId)。
32
+
33
+ ### 分析方案(看板)
34
+ | 用途 | 方法 路径 |
35
+ |---|---|
36
+ | 列表 | POST `/index-block/get-page-setting-folder-bussiness-info` `{"isOnlySeeSelf":false,"page":{"pageIndex":1,"pageSize":20}}` |
37
+ | **新建空方案** | POST `/page-setting/new-create-empty-plan` `{"name":"...","isPrivate":false,"pageType":0}` → 返回 **planId** |
38
+ | **加卡片进方案** | POST `/page-setting/new-page-card` ⚠ body 未抓到,见下 |
39
+ | 详情 | POST `/page-setting/get-new-page-plan/<planId>?isEdit=true\|false` body `{}` |
40
+ | 保存布局 / 删卡片 | POST `/page-setting/update-page-card` |
41
+ | 批量存筛选器 | POST `/page-setting/batch-add-or-update-filter` |
42
+ | 重置用户筛选值 | POST `/page-setting/reset-user-filter` |
43
+ | 卡片数据 | POST `/page-setting/get-new-index-block-data` |
44
+
45
+ `new-page-card` 的 body 已经拿到:
46
+ `linkai plan-add --plan <planId> --card <cardId> [--x x --y y --w w --h h]` 直接带布局调它,一次落位不用再 `update-page-card`。
47
+ 响应返回的就是新 pageCard 对象,`data.id` 即后面配筛选器要用的 `pageCardId`。
48
+
49
+ 方案详情里卡片在 **`data.pageCards`**(不是 `userPageCards`),每张卡片的名字在 `indexBlock.name`。
50
+
51
+ `update-page-card` 的 body:
52
+
53
+ ```json
54
+ {"list":[{"id":"<pageCardId>","x":0,"y":0,"width":24,"height":35,
55
+ "mobileX":0,"mobileY":0,"mobileWidth":4,"mobileHeight":30,"zIndex":0}],
56
+ "userPageSettingId":"<planId>","dataScreenSetting":"<原样回传>",
57
+ "name":"...","tag":"","remark":"...","parentId":"...","isPrivate":false}
58
+ ```
59
+
60
+ **不在 `list` 里的卡片会被移出看板**(卡片本身还在卡片库)。
61
+
62
+ `batch-add-or-update-filter` 的 body:
63
+
64
+ ```json
65
+ {"isTenantSystem":true,"userPageSettingId":"<planId>","filterSourceType":4,
66
+ "filters":[{"...原 filter 全部字段...",
67
+ "filterId":"<原 filter 的 id>", "sortNum":0,
68
+ "filterSourceType":4,"userPageSettingId":"<planId>"}]}
69
+ ```
70
+
71
+ 原 filter 的 `id` / `value` 要删掉,`defaultValue` 传**已序列化**的形式(如 `"\"某值\""`)。
72
+ 存完必须再跑一次 `reset-user-filter`,否则页面读到的还是旧的用户个人层。
73
+ `isTenantSystem:true` = 存成租户系统模板(所有人可见);`false` = 只存你自己的个人副本。分层模型见 `references/filters.md`。
74
+
75
+ **`bindUserPageCards` 的字段名是 `pageCardId`,不是 `userPageCardId`**(传错了不报错,
76
+ 只表现为「筛选器在、能选、就是不过滤」):
77
+
78
+ ```json
79
+ "bindUserPageCards": [
80
+ {"pageCardId": "<方案详情 data.pageCards[].id>", "dataSettingIds": [], "dataSettingComment": "<卡片 dtos 里的 comment>"}
81
+ ]
82
+ ```
83
+
84
+ 一个筛选器可以绑多张卡片,实现**一个下拉同时过滤整页**;某张卡片没有这个字段就别绑它
85
+ (如「商品状态」只有明细卡片有,速览卡片不绑)。`dataSettingIds` 留空数组即可。
86
+
87
+ ⚠ 界面上的「关联卡片」下拉**列不全字段**(实测 60 列的卡片只列出 9 个,且翻不到想要的那个),
88
+ 所以绑定这件事**只能走接口**,界面点一次只是用来看结构。
89
+
90
+ ### 空间(租户)
91
+ | 用途 | 方法 路径 |
92
+ |---|---|
93
+ | 全量空间列表 | GET `/account/user-bind-tenant-list/<userId>` → `[{tenantId, spaceName}]` |
94
+ | **切换空间** | POST `/account/switch-tenant` `{"tenantId":"...","agentId":"..."}` → **返回新 token** |
95
+
96
+ ⚠ 光改 `Tenantid` 头切不了空间,会 `406 当前已切换账号或租户信息`——token 里绑死了租户。
97
+ `localStorage['LM_COMPANY']` 只有当前空间,全量列表必须走上面的接口。
98
+
99
+ ### 数据报表
100
+ | 用途 | 方法 路径 |
101
+ |---|---|
102
+ | 列表 | POST `/index-block/get-index-block-report-folder-bussiness-info` `{}` |
103
+ | **建报表** | POST `/index-block/operate-report` `{"indexBlockId":"<卡片id>","name":"...","tag":"","isPrivate":false}` |
104
+ | 详情 | POST `/page-setting/get-new-page-card-report` `{"indexBlockId":"<卡片id>","isSubWarn":false}` |
105
+ | 筛选器 | POST `/page-setting/batch-add-or-update-filter`(同看板,换参数) |
106
+
107
+ 一张卡片只能建一个报表,重复建返回 400「该报表已经存在了,请勿重复添加!」。
108
+
109
+ **报表详情返回的 `data.id` 就是 `userPageCardId`**——配筛选器要用它,别拿列表里的 `id`/`objId`。
110
+ (列表的 `id` 是 folderBussinessId,`objId` 是 indexBlockReportId,都不是。)
111
+
112
+ 报表侧 `batch-add-or-update-filter` 的 body:
113
+
114
+ ```json
115
+ {"isTenantSystem": false,
116
+ "filterSourceType": 2,
117
+ "userPageCardId": "<报表详情的 data.id>",
118
+ "filters": [ /* 每条见下 */ ]}
119
+ ```
120
+
121
+ 单条 filter(跟看板版的差异用 ★ 标出):
122
+
123
+ ```jsonc
124
+ {
125
+ "labelName": "店铺", // 界面上显示的标签
126
+ "propertyName": "waxis10", // ★ 报表靠这个绑卡片字段,直接写 waxisN
127
+ "fieldName": "店铺", // 数据集列别名,决定下拉候选值
128
+ "filterSourceType": 2, // ★ 2=卡片局部(看板是 4)
129
+ "userPageCardId": "<同顶层>", // ★ 看板那边用的是 userPageSettingId
130
+ "userPageSettingId": "<全0>",
131
+ "filterId": "<已有的传原 id,新增的整个字段不传>",
132
+ "sortNum": 0,
133
+ "defaultValue": "\"某个值\"", // 已序列化的 JSON 字符串,不设默认值就传 "\"\""
134
+ "defaultValueType": 1, "filterOperationType": 1, "filterType": 1, "filterProperty": 1,
135
+ "isRequired": false, "isDate": false, "isShowAllOption": false, "isTenantSystem": false,
136
+ "isSelfadd": "false", "inOuter": false, "variableIds": "", "variableType": 0,
137
+ "indexBlockDataSourceId": "<数据源id>", "indexBlockDataId": "<数据集id>",
138
+ "bindUserPageCards": [], // ★ 报表这里是空的
139
+ "userPageCardFilterValueRes": [], "userPageCardFilterSettingDtos": [],
140
+ "sortField": [], "optionsCode": [], "controlType": "",
141
+ "variableId": "<全0>", "tableFieldId": "<全0>", "tableFieldGroupId": "<全0>",
142
+ "productId": "<全0>", "erOriginId": "<全0>", "indexBlockPreFilterId": "<全0>",
143
+ "wdErr": false
144
+ }
145
+ ```
146
+
147
+ **整份提交是全量覆盖**:数组里没有的筛选器会被删掉,删筛选器就靠这个。
148
+ 新增的那条不要带 `filterId`,服务端会生成。发完刷新页面就生效。
@@ -0,0 +1,58 @@
1
+ # 登录与租户
2
+
3
+ ## 规则
4
+
5
+ - **首次登录由用户本人操作**:执行 `linkai login`,CLI 打开领谋线上授权页;用户核对终端与页面的短码、选择租户并确认。CLI 通过服务器事件收到确认、保存会话后,原页面才显示成功;浏览器不访问本机临时端口。本机只保存访问令牌、刷新令牌和设备标识(权限 0600),不保存账号密码。
6
+ - **过期自动续期**:CLI 用刷新令牌自动换取新令牌;授权失效时重新执行 `linkai login`。
7
+ - **只有一个当前租户**:同账号切换租户执行 `linkai login --tenant <租户名>`,CLI 使用现有 OAuth 身份向服务端换取目标租户会话,无需浏览器重新授权。全域租户不可用于业务操作。
8
+ - **AI 不索要账号密码**;把首次登录步骤交给用户。
9
+
10
+ ## 给用户的操作步骤(AI 代理原样转交)
11
+
12
+ > **首次登录(只需一次)**
13
+ >
14
+ > 1. 打开终端:Mac 打开「终端」(启动台搜索“终端”);Windows 打开「PowerShell」(开始菜单搜索“PowerShell”)。
15
+ > 2. 执行下面这行:
16
+ >
17
+ > ```bash
18
+ > linkai login
19
+ > ```
20
+ >
21
+ > 3. 在弹出的领谋网页登录,核对终端与页面显示的授权短码,选择租户并确认授权。若浏览器没有自动打开,可复制终端提示的授权链接。
22
+ > 4. 返回终端等待成功结果,然后回到 AI 对话里说「好了」。
23
+ >
24
+ > 想登录后直接进某个租户,可以在第 2 步用:
25
+ >
26
+ > ```bash
27
+ > linkai login --tenant 你的租户名称
28
+ > ```
29
+
30
+ 其它情况(同样交给用户在终端执行):
31
+
32
+ | 情况 | 用户执行 |
33
+ |---|---|
34
+ | 换一个账号或重新授权 | 用户确认后执行 `linkai login --switch-account`,在网页选择账号和租户 |
35
+ | 退出登录 | `linkai logout`(撤销 OAuth 会话并删除本机登录状态) |
36
+ | 浏览器无法自动打开 | `linkai login --no-browser`,手动打开终端给出的授权地址 |
37
+
38
+ ## AI 代理怎么做
39
+
40
+ | 用户说 / 遇到的情况 | 执行 | 说明 |
41
+ |---|---|---|
42
+ | 任意命令返回 `AUTH_REQUIRED` | 不要重试,不要要密码 | 把上面「首次登录」步骤交给用户,等用户说好了再继续 |
43
+ | 用户完成首次登录,说「好了」 | `linkai tenants` | 报告当前 OAuth 授权的租户 |
44
+ | 「我在哪个租户」 | `linkai status` | 报 `tenantName`;`autoRelogin:true` 表示可自动续期 |
45
+ | 「切换到 XX 租户」 | `linkai tenants` 核对名称 → 执行 `linkai login --tenant XX` → **重新读取新租户的数据字典** | 沿用当前账号的 OAuth 身份;切换后写 SQL 前必须读新租户数据字典,否则 `DICTIONARY_REQUIRED` |
46
+ | 「切换账号」等类似指令 | 先问是否要切换领谋账号;确认后执行 `linkai login --switch-account` | 浏览器重新授权 |
47
+ | 「重新登录一下」 | 询问是否要切换领谋账号;确认后执行 `linkai login --switch-account` | 浏览器重新授权 |
48
+
49
+ `status` / `login` 结果字段:`tenantName`、`tenantCode`、`tenantId`、`user`、`autoRelogin`、`authMode`。
50
+ `tenants` 结果字段:`tenantCount`、`single`、`current`、`tenants[]`(`name`/`tenantId`/`current`)、`next`(给 AI 的下一步)。
51
+
52
+ ## 注意
53
+
54
+ - OAuth 刷新令牌保存在本机 0600 权限的会话文件中;共用电脑用完请执行 `linkai logout`。
55
+ - 建错租户不会报错,只会表现为「找不到」。搭建前先 `linkai status` 确认 `tenantName`;`build` 的 spec 可写 `tenant: <租户名>`,
56
+ 执行时自动核对,不一致就停下。
57
+ - `0000`(全域)不是可用租户。
58
+ - 配置文件与登录信息的内容不输出到对话、日志或提交。
@@ -0,0 +1,382 @@
1
+ # 卡片(指标块)
2
+
3
+ ## 建卡片
4
+
5
+ | 用途 | 端点(都是 POST,前缀 `/api/app`) |
6
+ |---|---|
7
+ | 校验 SQL / 取字段 | `/index-block/get-field-info-by-data-set-sql` |
8
+ | 新建卡片 | `/index-block/new` |
9
+ | **保存卡片(真正生效的)** | `/index-block/update-new` |
10
+ | 卡片详情 | `/index-block/get-new-index-block-detail` `{"id":"..."}` |
11
+ | 卡片库列表 | `/index-block/get-index-block-folder-bussiness-info` `{}` |
12
+ | 删卡片 | `/index-block/delete-folder-bussiness` body 是 **id 数组** |
13
+
14
+ ⚠ `/index-block/update` 只会新建一个版本、**不落到业务卡片**。要用 `update-new`。
15
+
16
+ 填报卡片的左侧定义、右侧绘制和字段类型另有硬性规则;创建或修改前先读 `references/fill-import.md`。
17
+
18
+ 标准做法是**拉详情 → 改 JSON → 原样 POST 回去**:
19
+
20
+ ```python
21
+ payload = {k: d[k] for k in ['indexBlockDataId','isMulData','indexBlockShowOrderFields',
22
+ 'filters','isFreeFill','id','indexBlockType','name','tag','remark','parentId','isPrivate']}
23
+ payload['chartParentType'] = cs['parentId']; payload['chartType'] = cs['id']
24
+ payload['limitCount'] = d['limitCount']
25
+ payload['indexBlockDataSettingDtos'] = dtos
26
+ payload['chartSetting'] = json.dumps(cs, ensure_ascii=False)
27
+ ```
28
+
29
+ `chartSetting` / `conditionStyle` / 自定义渲染列的完整结构、agtable 全部可调项、字段增删的写法,看 `references/api.md`。
30
+
31
+ ### 选图表类型:74 种,靠两个字段决定
32
+
33
+ 卡片长什么样,由 payload 里这两个字段决定(它们就是 `chartSetting` 的 `id` / `parentId`):
34
+
35
+ ```python
36
+ payload['chartType'] = 'agtable' # 具体类型
37
+ payload['chartParentType'] = 'table' # 所属大类
38
+ ```
39
+
40
+ 领谋一共 **74 种**(v2 `chartRegistry.js`),分 15 个大类:`table` 表格 / `column` 柱状 / `bar` 条形 / `line` 折线 /
41
+ `area` 面积 / `pie` 饼图 / `rose` 玫瑰 / `dualAxes` 双轴 / `meter` 指标卡 / `progress` 进度 /
42
+ `more` 漏斗雷达词云等 / `widget`+`web`+`3d` 杂项。**完整中文对照表在 `references/chart-types.md`**。
43
+
44
+ ### 字段角色决定 `field` 前缀和 `argumentType`(这块最容易配错)
45
+
46
+ **这四组值是系统固定的**(对应界面上把字段拖进数值区 / 维度区 / 系列区 / 排序区):
47
+
48
+ | 拖进哪个区 | `field` | `axis` | `argumentType` | `calculationMode` |
49
+ |---|---|---|---|---|
50
+ | **数值**(度量) | `yaxis<N>` | **`'y1'`**(双轴第二轴 `'y2'`) | **1** | 0;拖的是维度列时前端自动置 1(计数)/2(求和) |
51
+ | **维度** | `waxis<N>` | `''` | **2** | 0 |
52
+ | **系列** | `waxis_type<N>` | `''` | **3** | 0 |
53
+ | **排序** | `order<N>` | `''` | **0** | 0,且 `orderByType=1` |
54
+
55
+ 编号从 1 起,新增取已有最大值 +1。
56
+
57
+ 四条血泪教训:
58
+
59
+ 1. **没有 `xaxis` 这回事**。图表的维度也用 `waxis`。
60
+ 2. **`argumentType` 按角色给固定值,别照抄数据集字段**。
61
+ (早期版本这里写的是"照抄数据集",碰巧对 —— 因为维度列恰好是 2、数值列恰好是 1;
62
+ 但排序字段必须是 0,照抄就错了。)
63
+ 3. **`yaxis` 忘了 `axis:'y1'` 的话,数据查得出来、图就是画不出来**(只剩一条光秃秃的坐标轴)。
64
+ 4. **系列字段是 `waxis_type<N>`**,不是 `type<N>`。`fieldTip` 里写「1个系列」的图
65
+ (堆叠柱状/分组条形/百分比面积……)少了它就退化成单色图。
66
+
67
+ `linkai card-chart` 已经把这四条封装好了:
68
+
69
+ ```bash
70
+ linkai card-chart --ds <dsId> --name "状态分布" --kind column/stacked --dim 具体状态 --meas 款数 --series 店铺
71
+ # ↑类型 ↑维度 ↑数值 ↑系列
72
+ ```
73
+
74
+ 排序见 `references/pitfalls.md` 第 16 条 —— 卡片侧配不了,只能在数据集 SQL 里做。
75
+
76
+ ### 拿某个类型的 chartSetting 骨架
77
+
78
+ 每种类型的 `config` 结构差很远(AG 表格是 `wd.columns` 那套,指标卡是 `content`/`group`/`card`,
79
+ 柱状图就一个 `theme` 颜色数组),**但都不复杂,也都不用自己编**。三条路,从快到慢:
80
+
81
+ 1. **`clis/linkai/templates.js`**(本仓库自带)——**74 种全量**(以 v2 chartRegistry 重建,`linkai kinds` 看标注),直接取:
82
+ ```bash
83
+ node -e "import('./clis/linkai/templates.js').then(m=>console.log(JSON.stringify(m.default['column/basic'])))"
84
+ ```
85
+ 里面字段绑定(`wd.columns`、`conditions` 等)都清空了,结构和默认样式是真实的。
86
+ 2. **从同空间已有卡片抄**——`POST /index-block/get-index-block-folder-bussiness-info`
87
+ `{"page":{"pageIndex":1,"pageSize":200}}` **列表直接返回每张卡片的 `chartSetting`**,
88
+ 按 `id`/`parentId` 挑一张同类型的,照着改(想要某空间调好样式的真实骨架时用)。
89
+ 3. 该空间一张同类型的都没有 → 界面上手工建一张空卡片,再拉详情。
90
+
91
+ ⚠ 「通用指标卡」(`commonMeter`) 和「表格图」(`antdTable`) 在界面上标了**待下线**,
92
+ 选了会报「当前图表已禁用」。要指标卡就用**趋势指标卡** `list`/`meter`。
93
+
94
+ ### 彩色标签只能走条件样式
95
+
96
+ 自定义渲染的 `valueStyle` 只认 `fontSize / fill / fontWeight / fontStyle / fontFamily / lineHeight` 六个键,**没有背景色**。想要药丸标签,配 `conditionStyle.value`——命中的字段会被自动套上白字+圆角+底色。`conditionStyle.fontColor` 则只改字色,适合做日期预警。两者配色对齐就有层次。
97
+
98
+ `conditions` 是**扁平数组**不是嵌套 children,细节见 reference。
99
+
100
+ ## chartSetting(agtable)
101
+
102
+ ```jsonc
103
+ {
104
+ "id": "agtable", "parentId": "table",
105
+ "config": {
106
+ "sheetType": "table",
107
+ "showPagination": true, "pagination": {"pageSize": 20},
108
+ "showHeader": true, "headerHeight": 36,
109
+ "rowHeight": 262, "imgHeight": 232,
110
+ "showLineNumber": false, "lineNumberTextAlign": "left", "actionColTextAlign": "left",
111
+ "cellBorder": {"visible": true, "color": "#EDEFF2", "width": 1},
112
+ "total": {"show": false, "labelColumn": "", "labelText": "总计", "rowtotalType": "all"},
113
+ "agColumnWidths": [{"key": "waxis1", "width": 660}],
114
+ "conditionStyle": { /* 见下 */ },
115
+ "bgMark": {"visible": false, "fields": []},
116
+ "wd": {"rows": [], "values": [], "columns": [ /* 见下 */ ]}
117
+ }
118
+ }
119
+ ```
120
+
121
+ 以上是常用项。**全部可调项**以 `references/chart-config-schema.md` 的 `table/agtable` 一节为准,
122
+ 另有:`cellTextAlign`(单元格对齐)、`formColumns` / `mobileViewFields`(填报表单)、`report`(填报配置)、
123
+ `syncReport`(数据同步)。前端默认值:`showPagination:false`、`headerHeight:30`、`rowHeight:52`、`imgHeight:60`、
124
+ `cellBorder.visible:false`,`pagination.pageSize` 可选 20/30/…(`pageSizeOptions`)。那张表里没有的键,界面上也调不了。
125
+
126
+ ### AG 表格自适应铺满【实测】
127
+
128
+ **规则:所有 AG 表格都开启自适应宽度**(`config.fitTableWidth: true`)。`card-table` / `build` 生成时默认开启(spec 写 `fitTableWidth: false` 才关闭),
129
+ `build` 验收逐张检查新建的 AG 表格;`card-save` 保存未开启的 AG 表格时会提示。用户明确要求固定列宽时才关闭。
130
+
131
+ `config.fitTableWidth:true` 会在列总宽小于表格可用宽度时调用 AG Grid 的 `sizeColumnsToFit`,把各列按基础宽度比例
132
+ 拉伸铺满容器;**列总宽已经超出容器时不会压缩**,仍保留横向滚动。因此只打开开关并不能修复固定列宽过大的表格。
133
+
134
+ 需要多列等分铺满时,同时把 `config.agColumnWidths` 中这些列的基础宽度设成相同的小值(最小可用 `50`):
135
+
136
+ ```json
137
+ {
138
+ "fitTableWidth": true,
139
+ "agColumnWidths": [
140
+ {"key":"waxis1","width":50},
141
+ {"key":"waxis2","width":50}
142
+ ]
143
+ }
144
+ ```
145
+
146
+ 相同基础宽度会得到等分列;不同基础宽度会按比例分配。卡片自己的 `widthHeightSetting` 优先级高于
147
+ `agColumnWidths`,若用户曾手动拖拽列宽,先回读并确认它为空或同步清理对应列宽,否则自适应仍会被个人列宽覆盖。
148
+ 写后至少回读 `fitTableWidth`、全部基础列宽与可见列数,并在有数据的空间 `query` 验证卡片仍正常取数。
149
+
150
+ ### 列的三种形态
151
+
152
+ `wd.columns` 里每一项是下面三种之一(`AGTableAttr/utils.js`):
153
+
154
+ | 形态 | 识别 | 说明 |
155
+ |---|---|---|
156
+ | 普通列 | `enableCustomRender:false`,`field:["waxisN"]` | 一个字段一列,能自动换行 |
157
+ | 自定义渲染列 | `enableCustomRender:true`,`areas[]` | 一格里多个字段/图片/进度条,见下节 |
158
+ | **分组表头** | `id:"group_<guid>"`,`name:"表头文字"`,`children:[列…]` | 多级表头;子列不能 `fixed` |
159
+
160
+ 行转列:列上 `isPivot:true` + `childPivotFields:["waxisM"]`(把 `waxisM` 的每个值展开成一列,值取 `field[0]`),
161
+ `topValueField` 控制值字段表头位置。开了行转列后查询自动带 `isPivotTable:true`,分页按第一个非行转列字段分页。【确认】
162
+
163
+ **表头文字**:普通列和自定义渲染列的表头都取 `field[0]`(行转列取 `childPivotFields[0]`)对应 dto 的
164
+ `title || comment`(`getFieldTitle`),`column.name` 只是编辑器里的标识。
165
+
166
+ ### 自定义渲染列
167
+
168
+ 列 → area(横向区)→ row(纵向行)→ item(字段):
169
+
170
+ ```jsonc
171
+ {
172
+ "id": "column_<uuid>",
173
+ "name": "机会商品", // 列头文字
174
+ "field": ["waxis1", "waxis2", "waxis9"], // 用到的所有字段,第一个同时是列宽 key
175
+ "enableCustomRender": true,
176
+ "hidden": false, "fixed": false, "contentAlign": "left",
177
+ "spanRows": false, "isPivot": false, "childPivotFields": [], "topValueField": false,
178
+ "areas": [
179
+ {"id": "area_<uuid>", "width": 224, "rows": [
180
+ {"id": "row_<uuid>", "itemGap": 10, "items": [
181
+ {"id": "item_<uuid>", "type": "image", "field": "waxis1", "imgWidth": 204,
182
+ "layout": "left", "arrangement": "horizontal",
183
+ "fieldStyle": {...}, "valueStyle": {...},
184
+ "showLabel": false, "iconPosition": "left", "labelValueGap": 4, "showColon": false}
185
+ ]}
186
+ ]},
187
+ {"id": "area_<uuid>", "width": null, "rows": [ /* 多行文字 */ ]}
188
+ ]
189
+ }
190
+ ```
191
+
192
+ `item.type`:`labelValue`(文字) / `image` / `progress` / `chartLine` / `chartBar` / `chartStackedBar`。
193
+
194
+ **`fieldStyle` / `valueStyle` 只有这六个键会生效**(`getFieldTextStyle` 硬过滤,其余全丢):
195
+
196
+ | 键 | 映射到 |
197
+ |---|---|
198
+ | `fontSize` | font-size |
199
+ | `fill` | **color** |
200
+ | `fontWeight` | font-weight |
201
+ | `fontStyle` | font-style |
202
+ | `fontFamily` | font-family |
203
+ | `lineHeight` | line-height(数字按 px) |
204
+
205
+ 没有 backgroundColor / padding / borderRadius。
206
+
207
+ 普通列(要自动换行的用这个):`enableCustomRender: false`,`field: ["waxisN"]`,areas 给一个空 item 占位即可。
208
+
209
+ 不想显示但必须存在的字段(比如筛选器要绑的、排序要用的):建成 `hidden: true` 的普通列。
210
+
211
+ ### conditionStyle
212
+
213
+ 五个通道:
214
+
215
+ | 通道 | 效果 | 是否按列过滤 |
216
+ |---|---|---|
217
+ | `value` | **药丸标签**:`{color:"#fff", backgroundColor:<配色>, borderRadius:4, padding:"2px 4px"}` | 是(`field` 必须等于 item 的 field) |
218
+ | `fontColor` | 只改字色 | 是 |
219
+ | `rowMark` | 整行背景色 | 否(`field` 填空串) |
220
+ | `cellMark` | 单元格背景色 | 是 |
221
+ | `arrow` | 对比箭头 | 是 |
222
+
223
+ **conditions 是扁平数组,不是嵌套 children**。页面按 `parentId` 把扁平项分组(同 parentId 的多条 = AND),不写 parentId 则每条自成一组(`parent_<下标>`):
224
+
225
+ ```json
226
+ {"field":"waxis13", "color":"#D93026", "depField":"waxis14", "operator":"==", "value":1}
227
+ ```
228
+
229
+ - `field` = 要上色的**列字段**
230
+ - `depField / operator / value` = 判断条件,`depField` 可以指向**另一个字段**
231
+ - `==` / `!=` 走**字符串**比较(两边套单引号后 eval);`> < >= <=` 走 `Number()` **数字**比较
232
+ - 判断优先用 SQL 里算好的数字列,别用中文文本——稳且不怕文案改动
233
+ - 多条命中时取**数组里第一条**
234
+
235
+ 一套能用的配色(紧急度递减):
236
+
237
+ | 语义 | 色值 |
238
+ |---|---|
239
+ | 危急 | `#D93026` |
240
+ | 警告 | `#E8802B` |
241
+ | 正常/可做 | `#2BA471` |
242
+ | 失效/已处理 | `#A6A9AD` |
243
+ | 信息(分类标签) | `#1456F0` |
244
+ | 次要信息 | `#8A6BE8` |
245
+ | 行背景(置灰) | `#F7F8FA` |
246
+
247
+ ### indexBlockDataSettingDtos(字段表)
248
+
249
+ ```jsonc
250
+ {
251
+ "id": "<guid,新增填全 0 让服务端生成>",
252
+ "field": "waxis17", // waxisN 维度 / yaxisN 数值 / waxis_typeN 系列 / orderN 排序(没有 xaxis)
253
+ "title": "二级类目", // 表头/图例显示名;agtable 上改它实测会让查询发不出去(坑 4),meter 上可改
254
+ "comment": "二级类目", // ⚠ 数据集列别名,映射键,改它卡片就取不到数
255
+ "argumentType": 2, // 0=排序 1=数值(显示指标) 2=维度 3=系列(IndexBlockDataSettingEnums.ArgumentType)
256
+ "orderByType": 0, // 0=不排序 1=升序 2=降序(只对 argumentType=0 生效,且有坑,排序用 card-sort)
257
+ "tableFieldType": "string",
258
+ "sortNum": 16, "format": "default", "axis": "",
259
+ "indexBlockId": "<卡片id>", "uniqueId": "<新 guid>",
260
+ "calculationMode": 0, "seniorMode": 0, "periods": 0, "periodType": 0,
261
+ "sqlText": "", "decimal": 0, "thousandSeparator": true, "unit": "",
262
+ "tableFieldId": "<全0>", "tableFieldGroupId": "<全0>", "periodsWithTableFieldId": "<全0>",
263
+ "isCustomize": false, "isAggregation": false, "isUseSnow": false, "parent": null,
264
+ "nullZero": false, "isVirtual": false, "isFill": false,
265
+ "isCustomizeFill": false, "isInnerFill": false, "fieldDesc": ""
266
+ }
267
+ ```
268
+
269
+ **补一个新字段**:往 dtos 追加一条(`field` 取 `waxis<现有最大+1>`,`comment` 写数据集列别名),
270
+ 同时在 `wd.columns` 里加一个 `hidden:true` 的普通列。
271
+
272
+ **排序**:显示列的 `orderByType` 不起作用。用卡片持久排序 `linkai card-sort`(见 `references/query-engine.md` §4),
273
+ 或数据集 SQL 自套排序(铁律 6)。列表类报表建议「业务优先级 + 唯一键」两级,只排一级时同值行的顺序不稳定。
274
+
275
+ ### 显示格式(每个 dto 自己的字段)
276
+
277
+ | 字段 | 取值 |
278
+ |---|---|
279
+ | `format` | `default` 原样 · `number` 数值 · `percent` 百分比(×100 加 %)· `url` 链接 · `img` 图片 · 日期:`YYYY-MM-DD HH:mm:ss` / `YYYY-MM-DD HH:mm` / `YYYY-MM-DD` / `YYYY-MM` / `MM-DD` |
280
+ | `decimal` | 小数位;`-1` = 取整;`0` = 不处理 |
281
+ | `thousandSeparator` | 千分位(界面新建字段默认 `true`) |
282
+ | `unit` | 追加在值后面的单位文字 |
283
+ | `nullZero` | 空值显示 0 |
284
+
285
+ `decimal`/`thousandSeparator` 只在 `format:"number"`/`"percent"` 下生效;图片列(agtable 的 `image` item、画廊)要 `format:"img"`。
286
+
287
+ 【确认】字段配置界面现在会对所有字段角色显示「单位」,不再只对数值区(Y 轴)字段显示。
288
+ 保存仍是 dto 的 `unit`,但具体图表/自定义渲染是否把它画出仍取决于组件,上线前要实际验收。
289
+ `unit` 只是**后缀**;要 `¥` 这类前缀,继续在 SQL 里生成显示列,不要把「界面可填」误当成「所有图表都会正确显示」。
290
+
291
+ ### 聚合:`calculationMode` / `isAggregation`
292
+
293
+ `0` 无处理 · `1` 计数 · `2` 求和 · `3` 平均 · `4` 去重计数 · `5` 最小 · `6` 最大。任何一个字段有聚合,
294
+ 整张卡片就 `GROUP BY` 全部维度字段;**全是 waxis、没有 yaxis 的 AG 明细表也会 GROUP BY 全部列**,完全相同的行会被合并。
295
+ 规则和后果见 `references/query-engine.md` §3。
296
+
297
+ ### 趋势指标卡模板(`list` / `meter`,已实测)
298
+
299
+ ```jsonc
300
+ // chartSetting
301
+ {"id":"list","parentId":"meter","config":{
302
+ "group":{"type":"value","groups":[]},
303
+ "content":{"type":"main","position":"left","bgColor":"lucency",
304
+ "icon":{"show":true,"img":"","postion":"left"},
305
+ "labelText":{"fontSize":12,"fill":"rgb(153, 153, 153)"},
306
+ "valueText":{"fontSize":14,"fontWeight":800,"fill":"rgba(0, 0, 0, 0.85)"},
307
+ "labelTextMain":{"fontSize":12,"fill":"rgb(153, 153, 153)"},
308
+ "valueTextMain":{"fontSize":20,"fontWeight":800,"fill":"rgba(0, 0, 0, 0.85)"},
309
+ "textPadding":0},
310
+ "card":{"title":true}}}
311
+ // dtos 只要一条:{"field":"yaxis1","argumentType":1,"calculationMode":2,"title":"机会数","comment":"机会数"}
312
+ ```
313
+
314
+ 其余类型的骨架在 `clis/linkai/templates.js`(`linkai kinds` 列出实测状态),可调项在 `references/chart-config-schema.md`。
315
+
316
+ ## 保存前校验(与界面保存规则一致)【前端 2026-10-30】
317
+
318
+ CLI 在 `POST /index-block/new`、`/index-block/update-new` 前按界面「保存」时的校验先拦一遍(`card-*` 命令、`build`、`card-save`、`api` 透传都一样),不通过报 `CONFIG_INVALID` 并列出全部问题:
319
+
320
+ - 必须有 `chartSetting`(含图表 `id`);已禁用的图表类型不能保存。
321
+ - 必须有绘制字段(`indexBlockDataSettingDtos` 中 `argumentType` 1 数值 / 2 维度 / 3 系列至少一个);富文本 `editor`、网页 `iframe`、时钟 `clock`、倒计时 `countdown` 除外。
322
+ - 排序项(`argumentType:0`)必须有 `title` 和 `orderByType`。
323
+ - 聚合的自定义字段(`isAggregation && isCustomize`)不能做维度 / 系列;填报字段(`isFill`)不能做数值 / 系列。
324
+ - 改过的字段别名(`title` 与原名 `comment` 不同):不能以数字开头、不能是纯数字、不能含英文特殊符号 ``~!@#$%^&*()+.`-={}|;'",<>/?[]``(中文括号可以)。
325
+ - 开启填报(`chartSetting.config.report.visible`)时,`fillFields[].fields[].uniqueId` 必须都在绘制字段里。
326
+ - 卡片筛选:结果集过滤(`filterProperty:1`)的 `propertyName` 必须是已绘制字段;其余表单规则见 filters.md §8。
327
+ - 多维表 `s2table` 未开分页时,`limitCount` 必须是 1~5000 的整数。
328
+
329
+ 修改已有卡片(`update-new`)时与界面一致:原来就有、这次没改动的问题(如旧卡片用了已禁用的图表类型、早先写入的别名)只提示不拦,本次引入的问题才拦(实测 2026-11-02:388 张已有卡片原样保存 0 张被拦,13 张提示)。
330
+
331
+ 保存前 CLI 还会照前端规整请求体:`chartParentType` / `chartType` 取自 `chartSetting`;`indexBlockType` 表格类(`parentId:"table"`)为 `0`、其它为 `1`(1.0.16 之前一律 0,已修正);`agtable` / `gantt` / `multiFlowStep` 开启分页时清空 `limitCount`。
332
+
333
+ ## 卡片自带的筛选器(`filters`)
334
+
335
+ `update-new` 的 `filters` 数组是**卡片源筛选器**:报表和卡片局部筛选器给所有用户复制个人副本时,模板就是它。
336
+ 要让所有人都看到报表筛选器,写在这里(字段见 `references/filters.md` §2):
337
+
338
+ ```jsonc
339
+ "filters": [{
340
+ "labelName": "店铺", "filterType": 1, "filterOperationType": 2, "filterProperty": 1,
341
+ "propertyName": "waxis10", "fieldName": "店铺",
342
+ "indexBlockDataId": "<数据集id>", "indexBlockDataSourceId": "<数据源id>",
343
+ "defaultValueType": 1, "defaultValue": "\"\"", "isRequired": false, "isDate": false,
344
+ "inOuter": true, "isSelfadd": "false", "variableIds": "", "optionsCode": [], "sortNum": 0
345
+ }],
346
+ "isTenantSystem": true // update-new 的默认值;保持 true
347
+ ```
348
+
349
+ 【确认】改完对自己的报表跑一次 `reset-user-filter`(`filterSourceType:2` + `userPageCardId`)才能看到新模板。
350
+
351
+ ## 一把出:`linkai card-table --ds <dsId> --spec spec.json`
352
+
353
+ 不用再手写 200 行 payload。spec(实测样例):
354
+
355
+ ```jsonc
356
+ {
357
+ "name": "示例客户商品生命周期清单",
358
+ "pageSize": 20, "rowHeight": 150, "imgHeight": 120, "limitCount": 2000,
359
+ "columns": [ // 只写可见列,顺序即列序;width 是比例(默认开启自适应宽度,按比例铺满表格)
360
+ {"header": "商品", "width": 300, "image": "商品", // image= 图片列(自动放左侧区)
361
+ "imgWidth": 112,
362
+ "rows": [["商品名称:b"], ["编码行:grey"], ["货号行"]]}, // 每个数组=卡片里的一行
363
+ {"header": "当前状态", "width": 110,
364
+ "rows": [["具体状态:h"], ["处理建议"], ["停留文本:grey"]]},
365
+ {"header": "资料缺失", "width": 130, "plain": "资料缺失"} // plain= 普通列(可自动换行)
366
+ ],
367
+ "hidden": ["店铺", "优先级", "具体状态序"], // 筛选器/条件样式要用的隐藏列
368
+ "pills": [ // 药丸底色(conditionStyle.value)
369
+ {"col": "具体状态", "dep": "具体状态序",
370
+ "eq": {"6": "#D93026", "10": "#8A6BE8", "11": "#2BA471"}, "gte": {"13": "#A6A9AD"}}
371
+ ],
372
+ "fontColor": [ // 字色
373
+ {"col": "处理建议", "dep": "优先级", "eq": {"1": "#D93026"}, "gte": {"9": "#A6A9AD"}},
374
+ {"col": "资料缺失", "dep": "资料缺失", "ne": {"—": "#E8802B"}}
375
+ ],
376
+ "rowGrey": {"dep": "优先级", "gte": 9}, // 行底变灰
377
+ "order": "排序键" // 只注册为隐藏列;真排序写在数据集 SQL 里(铁律6)
378
+ }
379
+ ```
380
+
381
+ 字段样式后缀:`:b` 大标题(15px 粗)/ `:h` 小标题(13px 粗)/ `:grey` 灰色辅助 / 不写=正文。
382
+ 列名必须与数据集列别名一致,对不上会直接报错并列出可用列。