@finebi/finebi-cli 1.3.0 → 1.5.0

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.
package/README.md CHANGED
@@ -42,6 +42,27 @@ finebi-cli <resource> <command> [options]
42
42
 
43
43
  Run `finebi-cli capability refresh` manually when backend commands changed, the cache expired, or you want to force a reload.
44
44
 
45
+ For dashboard component queries, use repeatable inline filters or a JSON file (mutually exclusive):
46
+
47
+ ```powershell
48
+ finebi-cli dashboard data --id <dashboardId> --entry-id <entryId> --filter "合同付款类型=分期付款" --filter "是否已经交货=是" --page-size 5
49
+ finebi-cli dashboard data --id <dashboardId> --entry-id <entryId> --filters-file filters.json --page-size 5
50
+ ```
51
+
52
+ Filter names come from `dashboard info` control entries' `filterName`. Queries return only the first page or initial view; `--page-size` controls the first-page size for tables with a pager. There is no `--page` or continuation support. A native `hasNext=true` is preserved and accompanied by `result.warnings: ["NEXT_PAGE_UNSUPPORTED"]`. Parameter and result details are defined in the [dashboard data contract](../docs/dashboard-command-design.md#73-dashboard-data). Both the client and the server capability must support the inline option; refresh capabilities after updating the plugin.
53
+
54
+ For temporary field selection, first read the target instance's field directory:
55
+
56
+ ```powershell
57
+ finebi-cli dashboard info --id <dashboardId>
58
+ finebi-cli dashboard data --id <dashboardId> --entry-id <entryId> --dimension-ids '["<dId>"]' --filter "合同付款类型=分期付款"
59
+ finebi-cli dashboard data --id <dashboardId> --entry-id <entryId> --dimension-ids '[]'
60
+ ```
61
+
62
+ Use `entries[].queryFields[].dId` where `selectable=true`; this is a component-local key, not a dataset `fieldId` or field name. `queryFields` includes disabled fields and reports their saved `used` flag. Omit `--dimension-ids` to keep the saved selection. An explicit `[]` removes selectable dimensions while retaining summary/chart metrics; a detail table returns no columns or rows. Existing filters remain effective. Empty output columns do not imply that native pagination totals are zero.
63
+
64
+ For a published dashboard, pass the same `--directory-entry-id` to `dashboard info` and `dashboard data`, and use dIds from that source. Installing this npm package updates only the client and bundled Skill. These commands require a compatible server plugin and platform Provider; the plugin directly depends on the dimension-selection and published-source APIs. Refresh capabilities after the server update.
65
+
45
66
  ### Multiple environments
46
67
 
47
68
  Create an environment by logging in with its name and address:
@@ -92,10 +113,13 @@ Agent discovery flow:
92
113
  finebi-cli --help --json
93
114
  finebi-cli config set base-url http://localhost:37799/webroot/decision
94
115
  finebi-cli auth login --from-env
95
- finebi-cli <resource> --help --json
116
+ finebi-cli system command-index --json
117
+ finebi-cli <resource> <command> --help --json
96
118
  finebi-cli <resource> <command> [options] --json
97
119
  ```
98
120
 
121
+ `system command-index` returns the whole command surface as one compact index grouped by resource (`[write]` marks mutations, `[creates]` additionally marks resource creation; output is bounded to 8KB). Start with it instead of enumerating every resource's help, and open `--help` only for the commands you actually invoke.
122
+
99
123
  For complex file-based resources, do not expect command help to contain a complete source example. `widget template` returns a Widget JS DSL source template selected by chart type and scenario; `dashboard template` returns a Dashboard JS DSL example script with named placeholders and a variable contract. Their `add` and `update` commands accept JS DSL source files. Other resources may still expose writable JSON templates according to their dynamic command help.
100
124
 
101
125
  ## Agent Skills
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finebi/finebi-cli",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "FineBI dynamic capability command line client.",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {
@@ -12,20 +12,20 @@
12
12
  "finebi-cli": "./src/index.js"
13
13
  },
14
14
  "files": [
15
- "src",
15
+ "src",
16
16
  "skills",
17
17
  "README.md"
18
18
  ],
19
19
  "engines": {
20
20
  "node": ">=18.17"
21
21
  },
22
- "scripts": {
23
- "prepack": "node ../scripts/build-finebi-cli-agent.mjs",
22
+ "scripts": {
23
+ "prepack": "node ../scripts/build-finebi-cli-agent.mjs",
24
24
  "test:pack": "node ../scripts/test-cli-package.mjs",
25
25
  "test": "npm run test:unit && npm run test:process && npm run test:contract && npm run test:integration:stub",
26
26
  "test:unit": "node --test tests/unit/access-key.test.js tests/unit/auth.test.js tests/unit/capability.test.js tests/unit/config-permissions.test.js tests/unit/config.test.js tests/unit/dynamic-command-forwarding.test.js tests/unit/dynamic-command-multipart.test.js tests/unit/dynamic-command-preflight.test.js tests/unit/error-catalog-loader.test.js tests/unit/error-catalog.test.js tests/unit/error-construction-guard.test.js tests/unit/help.test.js tests/unit/http-stub.test.js tests/unit/index-auth-login.test.js tests/unit/index-auth-session.test.js tests/unit/index-browser-auth.test.js tests/unit/index-config.test.js tests/unit/index-dynamic-errors.test.js tests/unit/index-dynamic-render.test.js tests/unit/index-help.test.js tests/unit/index-profiles.test.js tests/unit/install-skill.test.js tests/unit/output-limits.test.js tests/unit/output.test.js tests/unit/package.test.js tests/unit/schema-options-capability.test.js tests/unit/schema-options-file-input.test.js tests/unit/schema-options-scalar.test.js",
27
27
  "test:process": "node --test tests/process/bench-cli-runner.test.js tests/process/process-cli.test.js tests/process/profiles-cli.test.js",
28
- "test:contract": "node --test tests/contract/capability-contract.test.js",
28
+ "test:contract": "node --test tests/contract/capability-contract.test.js tests/contract/dashboard-data.test.js tests/contract/subject-publication.test.js",
29
29
  "test:integration:stub": "node --test tests/integration/analysis-ds-workflows.test.js tests/integration/assertions.test.js tests/integration/auth-system.test.js tests/integration/case-loader.test.js tests/integration/cli-process.test.js tests/integration/config.test.js tests/integration/dashboard-workflows.test.js tests/integration/entry.test.js tests/integration/failure-security.test.js tests/integration/file-template.test.js tests/integration/mutating-file-input.test.js tests/integration/mutating-resource-ledger.test.js tests/integration/mutating-workflow-guards.test.js tests/integration/readonly-assertions.test.js tests/integration/readonly-file-input.test.js tests/integration/readonly-workflow-contract.test.js tests/integration/report-directory.test.js tests/integration/report-entry.test.js tests/integration/report-secrets.test.js tests/integration/report-summary-artifacts.test.js tests/integration/runner.test.js",
30
30
  "test:integration:readonly": "node tests/integration/entry.js readonly",
31
31
  "test:integration:mutating": "node tests/integration/entry.js mutating",
@@ -28,7 +28,8 @@ AnalysisTableDsl.create("表名")
28
28
 
29
29
  ```js
30
30
  .from("来源表原始名")
31
- .fromExcel("附件id", "Sheet1", ...) // Excel 来源(create 专用;sheetNames 可省略=全部有效 sheet)
31
+ .fromExcel("附件id", "Sheet1", ...) // Excel 来源(create 专用;省略 sheetNames 默认仅取首个有效 Sheet)
32
+ .mergeSheets() // 可选:合并所选 Sheet;省略 sheetNames 时合并全部有效 Sheet
32
33
  .select("字段名", ...) // 可选:引用白名单(只允许后续步骤引用这些字段)
33
34
  ```
34
35
 
@@ -37,6 +38,9 @@ AnalysisTableDsl.create("表名")
37
38
  - `from` 必选第一步,来源可以是公共数据集、主题内表或 Excel 表的**原始名**(全局唯一,即
38
39
  `analysis-ds search` / `public-ds search` 返回的 `id`)。转义名/展示名允许重名,不作来源标识。
39
40
  - `fromExcel` 的附件 id 来自 `analysis-ds upload-excel`;引擎默认 Spider。
41
+ - `fromExcel` 一脚本一表:未声明 `.mergeSheets()` 时,省略 `sheetNames` 仅取首个有效 Sheet;
42
+ 显式指定多个 Sheet 会被拒绝。声明 `.mergeSheets()` 时合并指定的 Sheet;省略 `sheetNames`
43
+ 时合并全部有效 Sheet。合并要求至少两个 Sheet,且字段名、类型及顺序一致,否则报错。
40
44
  - `from` 原始名直查失败时,错误会列出两棵树的近似候选(转义名 + id + 位置),可据此修正。
41
45
  - `select` 可选:声明后续步骤可引用的字段范围(白名单);**不产生字段收窄**——7.0 前端
42
46
  第一步恒保存来源全字段,字段收窄由字段设置步骤的 `hide` 表达。
@@ -20,9 +20,9 @@
20
20
 
21
21
  | 资源 | 只读(`mutation:false`) | 非只读(`mutation:true`) |
22
22
  |---|---|---|
23
- | `analysis-ds` | `dsl`†, `info`, `operator-schema`†, `preview`, `search` | `add`, `update`†, `data-update`, `delete`, `publish` |
23
+ | `analysis-ds` | `dsl`†, `info`, `operator-schema`†, `preview`, `search` | `add`, `update`†, `data-update`, `delete`, `publish`, `unpublish` |
24
24
  | `calculated-field` | `context`, `dsl`, `info`, `validate` | `add`, `delete`, `update` |
25
- | `dashboard` | `info`, `preview` | `add`, `delete`, `update` |
25
+ | `dashboard` | `search`, `info`, `preview` | `add`, `delete`, `update` |
26
26
  | `data-connection` | `search`, `tables` | — |
27
27
  | `model` | `info`, `search` | `add`, `delete` |
28
28
  | `metric-condition` | `dsl` | `update` |
@@ -33,7 +33,7 @@
33
33
  | `public-ds-folder` | `search` | `add` |
34
34
  | `relationship` | `info` | `add`, `delete` |
35
35
  | `subject` | `info`, `search` | `add`, `data-update`, `delete` |
36
- | `widget` | `data`, `info`, `preview` | `add`, `delete`, `update` |
36
+ | `widget` | `data`, `info`, `preview`, `search` | `add`, `delete`, `update` |
37
37
 
38
38
  `geographic-role match` 的 `--values` 使用单个 JSON 数组参数,如 `'["上海","江苏"]'`;最多 200 个值,不代表整列扫描。`catalog` 按区域路径去重后分页。
39
39
 
@@ -70,6 +70,7 @@
70
70
  - `search`:定位候选资源;在宣称唯一前遍历全部分页。
71
71
  - `info`:确认准确 ID、名称、归属、状态和字段/关系元数据。
72
72
  - `preview`:预览已有资源(`--id`),不代表写入。
73
+ - `dashboard.data`:查询仪表板内单个数据组件,命令 `dashboard data --id <dashboardId> --entry-id <entryId> [--filter "名称=值" ... | --filters-file <filters.json>] [--page-size 100]`。先读 `dashboard info`,取 `entries[].entryId`(不能用底层 widgetId)及控件实际 `filterName`。`--filter` 可重复,按第一个 `=` 分隔且保留原始名称和值,拒绝空/全空白名称或值、缺少 `=`、同名重复;与 `--filters-file` 互斥。名称含 `=` 时使用文件。过滤 JSON 形如 `{"地区":["上海,北京"]}`,每个值严格为单元素非空字符串数组;日期区间键加 `_s/_e`,时间按钮组传已配置标签。未传控件保留保存值,未知/重名/树控件/清空覆盖均拒绝,不得失败后丢弃过滤重试。只返回首页或初始视图,不支持 --page、续页或 all-data;--page-size 控制有分页器表格的首页大小,默认保存页大小,图表及表头展开模式拒绝该参数。返回数据优先读取 `result`;使用前按[仪表板组件查询结果契约](dashboard-data-result.md)检查 normalization、单元格 state、行 kind、range 与 warnings,并参考明细表、分组表与 KPI 脱敏示例。NEXT_PAGE_UNSUPPORTED 表示原生结果仍有后续数据且 CLI 无法续页,不得将当前结果视作全量或将 totalRows 当作总数。缺少平台查询能力时回 UNSUPPORTED_FEATURE,不改用主题 widget.data 代替仪表板上下文。
73
74
  - `widget.data`:组件取数,用于数据核对与探查。`--id` 核对已保存组件的数据;`--subject-id` + `--file` 用临时 DSL 探查未落地的组件形态,不创建组件。与 `widget preview` 都返回字段与行数据,分工在:`preview` 附组件类型与后端摘要、仅限已保存组件;`widget.data` 可控取数范围(`--page`/`--page-size` 按页取数、`--all-data`/`--expand-all` 全量/展开档位),适合逐行核对与大数据量的分页检查。范围参数依赖平台构件提供取数范围 API:旧构件回 `UNSUPPORTED_FEATURE`,改用默认全量取数。
74
75
  - `definition`:回读公共数据集 Definition,常用于 update 前审查或恢复设计。
75
76
  - `source-preview`:验证公共数据集 JSON Definition 的源数据效果。
@@ -87,7 +88,7 @@
87
88
 
88
89
  | 文件类型 | 命令 |
89
90
  |---|---|
90
- | JSON | `public-ds.add`, `public-ds.source-preview`, `public-ds.update` |
91
+ | JSON | `public-ds.add`, `public-ds.source-preview`, `public-ds.update`, `dashboard.data`(`--filters-file`) |
91
92
  | JavaScript 文本 | `analysis-ds.add`, `analysis-ds.update`, `calculated-field.validate`, `calculated-field.add`, `calculated-field.update`, `widget.add`, `widget.update`, `dashboard.add`, `dashboard.update` |
92
93
  | Model DSL 文本 | `metric-condition.update`, `drill-directory.update` |
93
94
 
@@ -101,7 +102,7 @@
101
102
  (内部试用开关开启的服务器);正式版默认态下 `add` 仅支持原样引用创建(create/from/fromExcel),
102
103
  `template` 仅 `basic` 场景,不得规划改表工作流(见第 2 节 † 注)。
103
104
 
104
- 创建与步骤覆盖使用 AnalysisTableDsl JS 文件:`add --subject-id <id> --file <x.js>` 创建新表(DSL `from(表原始名)` 声明来源,原始名= search 返回的 `id`,全局唯一;Excel 源用 `fromExcel(attachId, [sheetNames])`,attachId 来自 `upload-excel`),`update --id <id> --file <x.js>` 覆盖步骤链(来源不变,DSL `from` 必须匹配现有来源)。创建后使用响应中的实际 `id`/`ids` 逐一调用 `info`,失败清理必须覆盖 `ids` 中的全部实际 ID。`dsl --id <id>` 反读已保存表的可编辑 DSL 文本(Bean→DSL),适用于「读取 → 修改 → 写回」;写回用 `update`。**影响面透出(2026-08-31)**:`update` 响应带 `changedFields`(保存前后字段差分,rename 以旧名消失 + 新名出现表达);`dsl` 反读响应带 `resultToIdentifyNameMap`(结果字段名 ≠ identify name 的别名差异条目,组件引用按 identify name 解析)。**别名保留(2026-08-31)**:DSL 可选 `resultName(resultFieldName, identifyName)` 声明(key=最后一步结果字段名=字段转义名,value=对外 identify name;未声明完全交给平台);`dsl` 反读把别名差异输出为 `.resultName(...)` 声明,写回即幂等——rename 涉及被组件引用的字段时,声明 `resultName("新名", "旧名")` 保留 identify name,组件不失效。改表工作流:`dsl` 看别名层(评估 rename 风险)→ `update` → 看 `changedFields` → 需要保留别名时在 DSL 声明 `resultName(...)` → `verify-data` 验证。AnalysisTableDsl 语句面见 handoff-dsl-writer 的 `analysis-dsl-api.md`(八类步骤 + 可选 resultName 声明:来源/过滤/排序/字段设置/公式列/分组汇总/左右合并/上下合并;`template --scenario <basic|formula|group|full>` 给可替换模板)。**合并步骤(2026-08-31)**:`join("右表原始名", joinType.X).on(左字段, 右字段).renameResult(旧名, 新名)` 左右合并、`union("表A").union("表B").alignField(左, 右).withFields(...).withAllFields().renameResult(...)` 上下合并——右表/合并表**只支持同主题内数据集**(用 `analysis-ds.search` 确认表在当前主题);结果字段由引擎推导(抽取表合并依据去重成一个字段、直连表全保留);改名语句是 `renameResult`(避让字段设置的 `rename`)。先按第 4 节区分引擎类型;DIRECT 数据集不得因更新状态或摘要 `usable` 规划、执行 `data-update`。SPIDER 的 `data-update` 可能触发数据重算,不能视为可逆。`delete` 必须独立确认。
105
+ 创建与步骤覆盖使用 AnalysisTableDsl JS 文件:`add --subject-id <id> --file <x.js>` 创建新表(DSL `from(表原始名)` 声明来源,原始名= search 返回的 `id`,全局唯一;Excel 源用 `fromExcel(attachId, "Sheet1", ...)`(Sheet 名逐个传字符串),attachId 来自 `upload-excel`),`update --id <id> --file <x.js>` 覆盖步骤链(来源不变,DSL `from` 必须匹配现有来源)。创建后使用响应中的实际 `id`/`ids` 逐一调用 `info`,失败清理必须覆盖 `ids` 中的全部实际 ID。`dsl --id <id>` 反读已保存表的可编辑 DSL 文本(Bean→DSL),适用于「读取 → 修改 → 写回」;写回用 `update`。**影响面透出(2026-08-31)**:`update` 响应带 `changedFields`(保存前后字段差分,rename 以旧名消失 + 新名出现表达);`dsl` 反读响应带 `resultToIdentifyNameMap`(结果字段名 ≠ identify name 的别名差异条目,组件引用按 identify name 解析)。**别名保留(2026-08-31)**:DSL 可选 `resultName(resultFieldName, identifyName)` 声明(key=最后一步结果字段名=字段转义名,value=对外 identify name;未声明完全交给平台);`dsl` 反读把别名差异输出为 `.resultName(...)` 声明,写回即幂等——rename 涉及被组件引用的字段时,声明 `resultName("新名", "旧名")` 保留 identify name,组件不失效。改表工作流:`dsl` 看别名层(评估 rename 风险)→ `update` → 看 `changedFields` → 需要保留别名时在 DSL 声明 `resultName(...)` → `verify-data` 验证。AnalysisTableDsl 语句面见 handoff-dsl-writer 的 `analysis-dsl-api.md`(八类步骤 + 可选 resultName 声明:来源/过滤/排序/字段设置/公式列/分组汇总/左右合并/上下合并;`template --scenario <basic|formula|group|full>` 给可替换模板)。**合并步骤(2026-08-31)**:`join("右表原始名", joinType.X).on(左字段, 右字段).renameResult(旧名, 新名)` 左右合并、`union("表A").union("表B").alignField(左, 右).withFields(...).withAllFields().renameResult(...)` 上下合并——右表/合并表**只支持同主题内数据集**(用 `analysis-ds.search` 确认表在当前主题);结果字段由引擎推导(抽取表合并依据去重成一个字段、直连表全保留);改名语句是 `renameResult`(避让字段设置的 `rename`)。先按第 4 节区分引擎类型;DIRECT 数据集不得因更新状态或摘要 `usable` 规划、执行 `data-update`。SPIDER 的 `data-update` 可能触发数据重算,不能视为可逆。`delete` 必须独立确认。
105
106
 
106
107
  主题共享钻取目录:`drill-directory.update --subject-id <id> --file <directory.js>` 使用 `DrillDirectoryModel.builder("目录名").levels([from("表").dim("省"), from("表").dim("市")]).build()` 创建,用 `.dissolve()` 解散。`drill-directory.dsl --subject-id <id> --name <目录名>` 回读;目录按主题内精确名称定位。解散保留原字段,操作影响主题共享字段面板;目录重名且层级不同、字段已属于其他目录时拒绝写入,不能隐式覆盖。
107
108
 
@@ -115,18 +116,27 @@
115
116
 
116
117
  ### 7.3 Dashboard 与 Widget
117
118
 
119
+ 查询仪表板组件时,先从同一来源的 `dashboard info` 读取目标条目的 `queryFields`,用 `selectable=true` 的 `dId` 构造 `dashboard data --dimension-ids '["<dId>"]'`。这些 ID 是组件内部键,不是数据集 fieldId;目录包含关闭的字段,`used` 表示保存状态。省略参数沿用配置,`[]` 关闭全部可选维度;汇总组件的指标保留,明细表按列选择。可同时传 `--filter` 或 `--filters-file`;选择不清除过滤、不保存配置。插件直接依赖支持维度选择和发布来源查询的 Provider;编译构件与部署平台需具备相同接口契约。字段目录、空列表与兼容语义见 `docs/dashboard-command-design.md` 的查询契约。
120
+
121
+ 查询目录发布版时,从 `dashboard search` 的候选位置选定 `locations[].entryId`,对 `dashboard info` 和 `dashboard data` 都携带同一 `--directory-entry-id <directoryEntryId>`。先从该来源的 `info` 获取组件实例 `entries[].entryId` 和控件实际 `filterName`,再查询该组件。检查响应 `source=PUBLISHED_DIRECTORY` 及 `directoryEntryId`;普通来源为 `MY_ANALYSIS`。目录入口不能当作组件实例 ID,平台不支持或目录授权失败时不得丢弃入口参数后重试普通看板。
122
+
123
+ 仅知道仪表板名称时,用 `dashboard search "关键词"` 搜索「我的分析」和发布目录中有预览权限的仪表板;结果按 ID 合并并通过 `locations` 保留命中的来源。目录挂载的是主题,只有发布清单中的仪表板参与搜索,匹配仪表板名而非挂载主题名。用名称、主题、目录位置消歧;仅目录来源须保留 `id/entryId/publishId`,不能据此假定可使用原主题的 `info/dsl/preview` 或写命令,也不能自行拼接预览 URL。
124
+
125
+ 组件名称使用 `widget search "关键词"`,只搜索「我的分析」。两类搜索均支持省略关键词分页枚举,`--limit` 默认 100(1–500),`--offset` 默认 0 且必须为 limit 的整数倍,遍历 `pagination.hasNext/nextOffset`。仪表板的 total 和分页以合并后的结果为准。已知主题时用 `subject info` 枚举主题内资源,搜索命令不接受 `--subject-id`。搜索失败不能解释为没有资源。
126
+
118
127
  `add`/`update` 仅消费已经验证的 JS 文件。涉及自然语言组件或仪表板设计时遵循 `scenario-workflows.md` 和 `handoff-contract.md`。创建后用响应实际 ID 调用 `info`,必要时调用 `preview`;需要核对取数(逐行、分页或展开档位)时用 `widget data`(见第 5 节)。Dashboard 引用新 Widget 时,先创建并回查 Widget,再进入 Dashboard 阶段。
119
128
 
120
129
  `widget template` 的 `basic` 支持 `seriesLine`、`ROSE`、`MULTI_PIE`。`seriesLine` 使用 `dimensionField` 分类轴、指标求和折线和 `seriesField` 颜色;`ROSE` 使用 `dimensionField` 分类,数值字段 `measureField` 求和后同时绑定角度与半径;`MULTI_PIE` 使用 `dimensionField` 颜色、指标求和大小和必填 `granularityField` 粒度,不配置 x/y 或 tooltip。公共变量为 `widgetName`、`tableName`、数值字段 `measureField`,变量按 JSON 字面量替换。
121
130
 
122
131
  `LINE basic` 使用 `dateField`(DATE/DATE_TIME,默认年月日)和两个不同的数值字段 `measureField1`、`measureField2`,分别以 `sum` 绘制上下分区折线。多指标自动按指标名称着色并生成图例;颜色由转换器绑定到各指标图形,不在“全部”卡片显式绑定。`widgetName`、`tableName` 为公共变量;仍编译为 `sliceLine`,既有输入别名保持兼容。
123
132
 
124
- **读取已有资源的 DSL 形态**:`widget dsl --id <widgetId>` / `dashboard dsl --id <dashboardId>` 把平台 Bean 反向翻译为可编辑的 DSL 文本。改板先 `dashboard info --id <dashboardId>` 清点条目与板级留痕(轻量),定位到目标后再 dsl 回读。`dashboard dsl` 的响应附带所引用数据组件的 Widget DSL 回显,便于一次读取;仪表板 DSL 本身只引用已有组件,附带回显不代表 `dashboard update` 会保存组件定义。
133
+ **读取已有资源的 DSL 形态**:`widget dsl --id <widgetId>` / `dashboard dsl --id <dashboardId>` 把平台 Bean 反向翻译为可编辑的 DSL 文本。改板先 `dashboard info --id <dashboardId>` 清点条目与板级留痕(轻量),定位到目标后再 dsl 回读。只看一个条目时用单条目切片 `dashboard dsl --id <dashboardId> --entry <名>`(重名板用 `--entry-id <entries[].entryId>` 消歧),`warnings`/组件回显同步过滤到目标条目。`dashboard dsl` 的响应附带所引用数据组件的 Widget DSL 回显,便于一次读取;仪表板 DSL 本身只引用已有组件,附带回显不代表 `dashboard update` 会保存组件定义。
125
134
 
126
135
  **按修改对象选择写入命令**:
127
136
 
128
137
  - 仅修改数据组件的字段、指标、聚合或图表配置时,从上述回显或 `widget dsl` 取得目标组件的完整 DSL,保留无关配置,修改后用 `widget update --id <widgetId> --file <widget.js>` 写回原组件。例如把“销售额”改成“利润”,只更新对应 Widget;仪表板布局和引用不变时无需执行 `dashboard update`。写后回查组件配置与数据,并核对仪表板中的目标组件。
129
- - 修改仪表板布局、组件引用或板级配置时,保留完整仪表板 DSL 中的其他条目和配置,用 `dashboard update --id <dashboardId> --file <dashboard.js>` 写回;不能用只含目标条目的局部片段覆盖整板。若同时修改数据组件,先完成 `widget update` 并回查,再执行必要的仪表板更新。`dashboard update` 不接受内联数据组件定义,不能代替 `widget update` 修改组件字段。
138
+ - 仅修改仪表板上一个条目(控件 / 文本 / 图片 / web / 复合过滤 / 组件引用条目的板侧面)时,用单条目写:`dashboard update-entry --id <dashboardId> --file <fragment.js>`(fragment 与 `dsl --entry` 切片同文法,是条目的完整新定义——切片读全条 → 改 → 写回;重名板带 `--entry-id` 消歧)。未点名条目原样保留;省略 `position` 沿用原槽位。新增 / 删除条目用 `dashboard add-entry` / `remove-entry`(被控件圈定或联动的条目删除会被拒,按错误信息先改引用);`add-entry` 的 fragment 无切片可抄、需手写——单条目语句 + 可选 `position` 行,不包整板脚本,缺省 position 落首个空位。并发编辑带 `--expected-update-time`(读板响应或上次写响应的 `lastUpdateTime`),不匹配报 `VERSION_CONFLICT`,重读后重试。
139
+ - 修改仪表板布局、组件引用或板级配置时,保留完整仪表板 DSL 中的其他条目和配置,用 `dashboard update --id <dashboardId> --file <dashboard.js>` 写回;不能用只含目标条目的局部片段覆盖整板。同名条目沿用原 entryId(名字即身份键;widget 引用条目跟随组件当前名,同组件改名后重声明仍沿用),新增条目的新 entryId 在响应 `entries[]` 直接给出。若同时修改数据组件,先完成 `widget update` 并回查,再执行必要的仪表板更新。`dashboard update` 不接受内联数据组件定义,不能代替 `widget update` 修改组件字段。并发编辑(用户手改后续改同理)带 `--expected-update-time`(读板响应 `info` / `dsl` 的 `lastUpdateTime`,或上次写响应的新值),不匹配报 `VERSION_CONFLICT`,重读后重试。
130
140
 
131
141
  DSL 语句面与 `widget template` / `dashboard template` 一致(见 handoff-dsl-writer 的 api 文档);主题模型字段会按作用域保持根级指标、数据集字段或模型计算字段路径,该回显可作为模型名/字段名证据。回读含 warnings 字段(Bean 上文本 DSL 无法表达、被丢弃的配置会带出登记),改动前应审视 warnings,避免把不可达配置覆盖丢失——条目级 `*_DROPPED` 留痕的条目整板 `dashboard update` 会被写侧防丢守卫以 `ENTRY_LOSS_BLOCKED` 拒绝(处置口径见 handoff-dsl-writer 的 dashboard-dsl-api)。
132
142
 
@@ -146,6 +156,8 @@ DSL 语句面与 `widget template` / `dashboard template` 一致(见 handoff-d
146
156
 
147
157
  ### 7.7 Relationship
148
158
 
159
+ `info/add/delete` 均由服务器内部试用开关控制,默认不开放。执行前检查能力清单;缺少所需命令时报告能力未开放,不调用或绕过门禁。
160
+
149
161
  `add` 是新增或替换语义。执行前用 `info` 判断关系是否已存在:不存在时可把 `delete` 作为创建后的清理命令;已存在时必须保存完整旧关系并以 `relationship.add` 作为恢复方案,不能把 `delete` 当作回滚。`delete` 前也要用 `info` 确认精确关系。
150
162
 
151
163
  ### 7.8 Subject
@@ -158,6 +170,49 @@ DSL 语句面与 `widget template` / `dashboard template` 一致(见 handoff-d
158
170
 
159
171
  执行 `subject add` 前通过实时 help 确认 `analysisType` 对应的 CLI 参数名和枚举值,并显式传参;省略参数的服务端默认值不能代替上述选择。创建后使用响应实际 ID 调用 `info`,核对名称、位置及 `analysisType` 与计划一致。`data-update` 不具备通用逆操作。`delete` 必须独立确认,并在计划中列出其包含资源影响。
160
172
 
173
+ 发布主题中的指定仪表板时,先用 `subject info` 确认主题和仪表板 ID,再按用户是否指定目录处理:指定目录先用 `directory list --privilege VIEW` 消歧;用户明确要求可直接发布时再用 `--privilege EDIT` 确认目录管理模块权限和目标编辑权限。发布请求使用 `--id`、`--name` 等普通参数,`--dashboard-ids '["d1","d2"]'` 传入 inline JSON 数组。省略 `parentEntryId` 时始终提交发布申请,不因用户管理其他目录而代选目录。响应为 `PUBLISHED` 才能告知已发布;`PENDING_APPROVAL` 只能告知申请已提交并保留申请 ID,不能当作目录已可访问。随后用 `subject publications --id <subjectId>` 回查;待审批结果没有入口地址,审批完成后再回查实际目录入口。发布挂载与仪表板内容保存分开,不能通过重复首次发布或无条件清快照声称完成同步;已有快照时遵循原生主题保存与数据更新结束后的生命周期。
174
+
175
+ ### 7.8.1 Directory
176
+
177
+ `directory list` 是发布前的只读目录发现命令。省略 `parentId` 查询全部真实可见目录;指定时只查询该可见父目录的直接子目录。先完成权限过滤,再做关键词和分页;`EDIT` 只返回同时具备目录管理模块权限和目标编辑权限的目录。不要把主题内文件夹 ID、Dora 资源库文件夹 ID 或仅能查看的目录当作可直接发布目标。使用返回的 `id` 作为 `--parent-entry-id` 参数,用户未指定目录时省略该参数以保留申请语义。
178
+
179
+ **完整示例:发布到指定目录**
180
+
181
+ 以下以用户指定的「经营分析 / 销售」目录为例。命令中的 `<主题ID>`、`<看板ID>`、`<目录ID>` 必须替换为查询确认的真实 ID;示例中的目录名称和路径不代表当前环境存在该资源。执行前按本 Skill 的固定入口检查实时 capability/help,并完成写入前复检。
182
+
183
+ 1. 查询用户可见的候选目录:
184
+
185
+ ```text
186
+ finebi-cli directory list "销售" --privilege VIEW --limit 50 --offset 0
187
+ ```
188
+
189
+ 从返回的 `data.items` 中核对 `name`、完整 `path` 和 `id`;存在同名候选时让用户按路径选择。若 `hasMore=true`,递增 `--offset` 继续查询。选中目标条目的 `id` 作为 `<目录ID>`,不要取该条目的 `parentId`。已知父目录时,可用 `directory list --parent-id <父目录ID> --privilege VIEW` 查看其直接子目录。
190
+
191
+ 2. 用户要求直接发布时,检查同一个目标目录是否可直接发布:
192
+
193
+ ```text
194
+ finebi-cli directory list "销售" --privilege EDIT --limit 50 --offset 0
195
+ ```
196
+
197
+ 完成分页后按第 1 步选定的 `id` 匹配,并核对 `canPublishDirectly`。目标不可直接发布时,说明需要审批,按用户意图继续申请;不要自动改选其他目录。
198
+
199
+ 3. 确认主题和看板归属,再携带目标目录 ID 发布:
200
+
201
+ ```text
202
+ finebi-cli subject info --id <主题ID>
203
+ finebi-cli subject publish --id <主题ID> --dashboard-ids '["<看板ID>"]' --name "销售分析看板" --parent-entry-id <目录ID>
204
+ ```
205
+
206
+ `--dashboard-ids` 是单个 JSON 数组参数。`--parent-entry-id` 使用第 1 步选定的目录 `id`;省略它不表示发布到默认目录。
207
+
208
+ 4. 回查发布记录:
209
+
210
+ ```text
211
+ finebi-cli subject publications --id <主题ID>
212
+ ```
213
+
214
+ 按本次响应的 `publishId` 对照记录:`PUBLISHED` 才表示已发布,`PENDING_APPROVAL` 表示等待审批。调用超时或结果不明时先回查,不直接重试发布。
215
+
161
216
  ### 7.9 Metric Condition
162
217
 
163
218
  主题共享指标条件是挂在原始数值字段或模型指标上的聚合前过滤,保存后**影响主题内所有使用该指标的组件**;向用户确认时必须透出这一影响面。工作流:`metric-condition.dsl` 回读当前条件 → 与用户确认期望条件 → `metric-condition.update` 提交 DSL 文件 → 再次 `metric-condition.dsl` 验证。`update` 是整份替换:请求 DSL 的 `where(...)` 列表就是保存后的完整条件,修改前必须先回读完整现状;清空用显式 `clear()`。字段定位必须使用实际字段 ID(`analysis-ds info`、`model info` 或 `metric-condition.update` 响应返回),展示名不进命令。计算字段、记录数、带分组/别名/快速计算的引用和跨引擎条件字段会被拒绝,条件固定 AND 合并;保存后服务端回读比对不一致时返回 `UPDATE_FAILED`,按失败处理而不是当作已生效。
@@ -0,0 +1,219 @@
1
+ # 仪表板组件查询结果契约
2
+
3
+ ## 1. 适用范围
4
+
5
+ `result` 是插件根据本次 Provider 的预览结果及返回的组件配置生成的独立契约,`schemaVersion` 为 `"1"`。现有顶层字段保持原语义,`dashboard preview` 不使用此契约。未增加过滤生效回显。值表示组件查询结果(可能经过聚合、计算、分组、权限和脱敏处理),不承诺是数据库原始值。
6
+
7
+ 响应中的 `fields/rows` 是兼容投影:交叉表仅左侧分区、复杂图表仅首个模型,不能代替标准化结果。类型专属结构保留在原生 `previewResult` 中,由 `resultType=DETAIL/BIGROUP/BICROSS/CHART` 区分;不要与本契约的 resultType 枚举混用,也不能用旧 rows 猜测填补不可用值。
8
+
9
+ ## 2. 返回结构与完整性
10
+
11
+ | 字段 | 语义 |
12
+ | --- | --- |
13
+ | `resultType` | `DETAIL_TABLE / GROUP_TABLE / CROSS_TABLE / CHART / UNKNOWN` |
14
+ | `normalization` | `COMPLETE` 完整转换本次返回的数据结构;`PARTIAL` 有结构未覆盖;`UNSUPPORTED` 无适配。与单元格值是否可用、是否取得所有页独立 |
15
+ | `range` | `mode/page/pageSize/pageUnit/hasNext/hasPrevious/total`,未知或不适用字段保留 JSON null |
16
+ | `datasets[]` | 独立数据模型,含 `id/columns/rows/returnedRowCount`;图表共用同一模型对象时去重,不同模型不能直接拼接求和 |
17
+ | `warnings[]` | 稳定原因码;结构未完整转换时必须检查原生 `previewResult`,不能把空 datasets 解释为业务无数据 |
18
+
19
+ ## 3. 分页与数量
20
+
21
+ 命令仅返回首页或初始视图,支持 `--page-size` 控制有分页器表格的首页大小,不支持指定页码或续页。`range.mode` 为 `PAGE / INITIAL_VIEW / UNPAGED / UNKNOWN`;PAGE 描述本次返回的是表格首页(page=1),不声明续页能力。明细表 pageUnit 为 ROW,普通分组表为 GROUP,展开后的 `returnedRowCount` 不必等于 pageSize。表头展开模式为 INITIAL_VIEW,不暴露普通页码和续页标志;图表为 UNPAGED。交叉表标准化范围为 UNKNOWN,原生纵向状态由现有顶层字段表达,横向状态需读取 previewResult。
22
+
23
+ 原生 `hasNext=true` 保持真实值,仅表示仍有后续数据;此时 `warnings` 包含 `NEXT_PAGE_UNSUPPORTED`,不得尝试续页或将当前结果宣称为全量。交叉表任一方向有后续数据也返回该警告;表头展开模式的 range 不提供续页标志,但原生结果标记仍有下一页时同样警告。该警告不改变 normalization:COMPLETE 只说明本次结构完整转换。
24
+
25
+ `total` 结构为 `{value,unit}`,仅明细表采用有效的原生总数(unit=DETAIL_ROW);普通分组总数未知(value=null、unit=GROUP),INITIAL_VIEW 的 total.value/unit 均为 null,图表不把模型行数当作查询总数。没有下一页不代表取得底层明细全集,也不排除组件自身 Top N、聚合等限制。
26
+
27
+ ## 4. 列、行与单元格
28
+
29
+ 列包含 `key/fieldId/name/type/encoding`。key 是当前 dataset 内唯一位置键(c0、c1…),不承诺跨查询稳定;fieldId 是结果字段 ID,不是底层数据集列 ID。类型仅采用可核实的结果/组件元数据:STRING 使用 STRING 编码,数值统一 DECIMAL + DECIMAL_STRING(支持规范科学计数法),明细表日期使用 DATE + EPOCH_MILLIS_STRING,未知类型为 UNKNOWN、encoding=null。不根据样例猜类型。数值字符串保留上游已有精度,不能恢复上游浮点误差。
30
+
31
+ 行结构为 `{kind,cells}`,cells 与 columns 位置对应。`kind` 为 DATA、SUBTOTAL、GRAND_TOTAL、UNKNOWN;正常聚合分组仍为 DATA,额外小计/总计用独立类型。明细行是 DATA,分组行依据原生 rowSumInfo;缺失或冲突为 UNKNOWN,不按“合计”等文本猜测。父节点的合计与子节点都保留,汇总时必须区分 kind;图表行没有统一的合计标记时为 UNKNOWN。
32
+
33
+ 单元格结构为 `{value,display,state,reason}`,null 不省略:
34
+
35
+ | state | value 的含义 |
36
+ | --- | --- |
37
+ | AVAILABLE | 按列 type/encoding 可使用的查询值 |
38
+ | NULL | 确认的结果空值,不承诺能区别数据库 NULL 与平台转换出的空值 |
39
+ | UNAVAILABLE | 无法可靠取得值;展示文本独立保留,reason 指出原因 |
40
+ | RESTRICTED | 原生权限标记或明确权限占位表明值受限,value=null;不区分无权限与脱敏的具体原因 |
41
+
42
+ ## 5. 各类型的值来源与限制
43
+
44
+ 明细表使用 value 通道,authority=false 时不暴露加密值。日期元数据明确为 DATE、DATE_DIMENSION、DATE_CREATED_BY_CALC 或 DATE_FROM_AGG_CALC 时,value 保留平台返回的 Unix epoch 毫秒整数文本(有符号 64 位范围),display 保留 formatText,缺失时采用 text。插件不转换时区,不从展示文本反推时间戳,也不承诺展示日期对应 UTC 零点。消费者应保留时间戳字符串或使用支持 64 位整数的解析方式,避免 JavaScript Number 精度损失;浮点数、日期标签、小数、科学计数法、空白及越界值标为 UNAVAILABLE / INVALID_TIMESTAMP_VALUE。此规则仅适用于明细表,不能套用到分组或图表的日期标签。
45
+
46
+ 分组指标使用 value,缺失时使用主工程保留的规范数值 text,绝不解析 formatText 的千分位、百分号、单位。分组字符串维度提供查询分组键;数值/日期分组键语义不能确认时为 UNAVAILABLE。字符串达到当前平台 cellCharLimit 时保守标记 POSSIBLY_TRUNCATED,不能声称它一定截断。类型缺失、展示专用值、缺少单元格分别标记不可用,不填造业务空值;日期也遵循这些规则及权限、空值规则。
47
+
48
+ 图表遍历 geom 模型和 shared 模型;已由平台处理的数值型指标可计算,字符串指标不反解析;维度字符串作为分组键,不能当作原始字段值。图表没有可直接采用的指标展示文本时 display=null。计算/缓存态未覆盖时为 PARTIAL,无内联模型时为 UNSUPPORTED。交叉表 v1 标记 UNSUPPORTED、warnings 包含 CROSS_TABLE_NORMALIZATION_UNSUPPORTED,完整原生数据继续保存在 previewResult。
49
+
50
+ ## 6. Agent 消费顺序
51
+
52
+ 消费顺序:检查 schemaVersion 和 normalization → 选择 dataset → 按列解释 AVAILABLE 单元格 → 根据 kind 区分汇总行 → 检查 range 与 warnings 并说明取数范围。出现 NEXT_PAGE_UNSUPPORTED 时必须说明仍有数据且命令不支持获取后续页。UNKNOWN 行不能自动当作明细累加。存在 PARTIAL、UNSUPPORTED 或 UNAVAILABLE 时应说明缺口,不能转用旧 rows 猜测填补。
53
+
54
+ KPI 的单个 AVAILABLE 指标可以直接读取;其 kind=UNKNOWN 表示缺少统一的行分类依据,不表示指标值不可用。不要因为 kind 未知而自动丢弃这个指标,也不要把它与其他未知行直接累加。
55
+
56
+ 命令参数、实例发现和过滤方式见[命令工作流](command-workflows.md)。此参考文档随 Skill 分发,无需访问工程源码即可解释结果。
57
+
58
+ ## 7. 脱敏返回示例
59
+
60
+ 以下只展示 `data.result`,不是整个响应 envelope。示例沿用实际查询的结构和状态,字段 ID、业务 ID、日期及统计数值均已替换为虚构内容,业务分类使用通用示例值,不可作为真实业务结论。三个示例代表同一示例看板:共 10 份合同、金额 50,000、购买数量 12;明细仅取前 2 行。
61
+
62
+ ### 7.1 明细表
63
+
64
+ PAGE 模式,第 1 页共 2 行,hasNext=true,NEXT_PAGE_UNSUPPORTED 表明仍有数据且无法续页。合同编号保留字符串语义;金额的 value 与带千分位的 display 分离。签约时间的 value 为毫秒时间戳字符串,display 为平台日期文本,state=AVAILABLE。COMPLETE 不代表拿到了全部 10 行或每个单元格都有可计算值。
65
+
66
+ ```json
67
+ {
68
+ "schemaVersion": "1",
69
+ "resultType": "DETAIL_TABLE",
70
+ "normalization": "COMPLETE",
71
+ "range": {
72
+ "mode": "PAGE",
73
+ "page": 1,
74
+ "pageSize": 2,
75
+ "pageUnit": "ROW",
76
+ "hasNext": true,
77
+ "hasPrevious": false,
78
+ "total": {"value":10,"unit":"DETAIL_ROW"}
79
+ },
80
+ "datasets": [
81
+ {
82
+ "id": "main",
83
+ "columns": [
84
+ {"key":"c0","fieldId":"example_detail_c0","name":"合同ID","type":"STRING","encoding":"STRING"},
85
+ {"key":"c1","fieldId":"example_detail_c1","name":"客户ID","type":"STRING","encoding":"STRING"},
86
+ {"key":"c2","fieldId":"example_detail_c2","name":"合同类型","type":"STRING","encoding":"STRING"},
87
+ {"key":"c3","fieldId":"example_detail_c3","name":"合同付款类型","type":"STRING","encoding":"STRING"},
88
+ {"key":"c4","fieldId":"example_detail_c4","name":"合同金额","type":"DECIMAL","encoding":"DECIMAL_STRING"},
89
+ {"key":"c5","fieldId":"example_detail_c5","name":"购买数量","type":"DECIMAL","encoding":"DECIMAL_STRING"},
90
+ {"key":"c6","fieldId":"example_detail_c6","name":"合同签约时间","type":"DATE","encoding":"EPOCH_MILLIS_STRING"},
91
+ {"key":"c7","fieldId":"example_detail_c7","name":"是否已经交货","type":"STRING","encoding":"STRING"}
92
+ ],
93
+ "rows": [
94
+ {
95
+ "kind": "DATA",
96
+ "cells": [
97
+ {"value":"CONTRACT-001","display":"CONTRACT-001","state":"AVAILABLE","reason":null},
98
+ {"value":"CUSTOMER-001","display":"CUSTOMER-001","state":"AVAILABLE","reason":null},
99
+ {"value":"示例合同","display":"示例合同","state":"AVAILABLE","reason":null},
100
+ {"value":"一次性付款","display":"一次性付款","state":"AVAILABLE","reason":null},
101
+ {"value":"1000.0","display":"1,000","state":"AVAILABLE","reason":null},
102
+ {"value":"1.0","display":"1","state":"AVAILABLE","reason":null},
103
+ {"value":"1767571200000","display":"2026-01-05","state":"AVAILABLE","reason":null},
104
+ {"value":"否","display":"否","state":"AVAILABLE","reason":null}
105
+ ]
106
+ },
107
+ {
108
+ "kind": "DATA",
109
+ "cells": [
110
+ {"value":"CONTRACT-002","display":"CONTRACT-002","state":"AVAILABLE","reason":null},
111
+ {"value":"CUSTOMER-002","display":"CUSTOMER-002","state":"AVAILABLE","reason":null},
112
+ {"value":"示例合同","display":"示例合同","state":"AVAILABLE","reason":null},
113
+ {"value":"一次性付款","display":"一次性付款","state":"AVAILABLE","reason":null},
114
+ {"value":"2000.0","display":"2,000","state":"AVAILABLE","reason":null},
115
+ {"value":"2.0","display":"2","state":"AVAILABLE","reason":null},
116
+ {"value":"1767657600000","display":"2026-01-06","state":"AVAILABLE","reason":null},
117
+ {"value":"是","display":"是","state":"AVAILABLE","reason":null}
118
+ ]
119
+ }
120
+ ],
121
+ "returnedRowCount": 2
122
+ }
123
+ ],
124
+ "warnings": ["NEXT_PAGE_UNSUPPORTED"]
125
+ }
126
+ ```
127
+
128
+ ### 7.2 分组表
129
+
130
+ 表头展开模式为 INITIAL_VIEW,page/hasNext/total 不提供普通分页含义。GRAND_TOTAL 与 DATA 都包含 50,000,不能相加成 100,000;总计行的分类单元格为 MISSING_CELL,不应把它当作业务 NULL 分组。
131
+
132
+ ```json
133
+ {
134
+ "schemaVersion": "1",
135
+ "resultType": "GROUP_TABLE",
136
+ "normalization": "COMPLETE",
137
+ "range": {
138
+ "mode": "INITIAL_VIEW",
139
+ "page": null,
140
+ "pageSize": null,
141
+ "pageUnit": null,
142
+ "hasNext": null,
143
+ "hasPrevious": null,
144
+ "total": {"value":null,"unit":null}
145
+ },
146
+ "datasets": [
147
+ {
148
+ "id": "main",
149
+ "columns": [
150
+ {"key":"c0","fieldId":"example_group_c0","name":"合同类型","type":"STRING","encoding":"STRING"},
151
+ {"key":"c1","fieldId":"example_group_c1","name":"合同数","type":"DECIMAL","encoding":"DECIMAL_STRING"},
152
+ {"key":"c2","fieldId":"example_group_c2","name":"合同金额","type":"DECIMAL","encoding":"DECIMAL_STRING"},
153
+ {"key":"c3","fieldId":"example_group_c3","name":"购买数量","type":"DECIMAL","encoding":"DECIMAL_STRING"}
154
+ ],
155
+ "rows": [
156
+ {
157
+ "kind": "GRAND_TOTAL",
158
+ "cells": [
159
+ {"value":null,"display":null,"state":"UNAVAILABLE","reason":"MISSING_CELL"},
160
+ {"value":"10.0","display":"10","state":"AVAILABLE","reason":null},
161
+ {"value":"5.0E+4","display":"50,000","state":"AVAILABLE","reason":null},
162
+ {"value":"12.0","display":"12","state":"AVAILABLE","reason":null}
163
+ ]
164
+ },
165
+ {
166
+ "kind": "DATA",
167
+ "cells": [
168
+ {"value":"示例合同","display":"示例合同","state":"AVAILABLE","reason":null},
169
+ {"value":"10.0","display":"10","state":"AVAILABLE","reason":null},
170
+ {"value":"5.0E+4","display":"50,000","state":"AVAILABLE","reason":null},
171
+ {"value":"12.0","display":"12","state":"AVAILABLE","reason":null}
172
+ ]
173
+ }
174
+ ],
175
+ "returnedRowCount": 2
176
+ }
177
+ ],
178
+ "warnings": []
179
+ }
180
+ ```
181
+
182
+ ### 7.3 KPI
183
+
184
+ KPI 使用 CHART 结果,UNPAGED 不提供查询总行数。5.0E+4 是十进制字符串,表示 50,000;display=null 表示没有可直接采用的展示文本。kind=UNKNOWN 不妨碍读取单个 AVAILABLE 指标,但不能据此跨行自动累加。
185
+
186
+ ```json
187
+ {
188
+ "schemaVersion": "1",
189
+ "resultType": "CHART",
190
+ "normalization": "COMPLETE",
191
+ "range": {
192
+ "mode": "UNPAGED",
193
+ "page": null,
194
+ "pageSize": null,
195
+ "pageUnit": null,
196
+ "hasNext": null,
197
+ "hasPrevious": null,
198
+ "total": {"value":null,"unit":null}
199
+ },
200
+ "datasets": [
201
+ {
202
+ "id": "model0",
203
+ "columns": [
204
+ {"key":"c0","fieldId":"example_kpi_c0","name":"合同总金额","type":"DECIMAL","encoding":"DECIMAL_STRING"}
205
+ ],
206
+ "rows": [
207
+ {
208
+ "kind": "UNKNOWN",
209
+ "cells": [
210
+ {"value":"5.0E+4","display":null,"state":"AVAILABLE","reason":null}
211
+ ]
212
+ }
213
+ ],
214
+ "returnedRowCount": 1
215
+ }
216
+ ],
217
+ "warnings": []
218
+ }
219
+ ```
@@ -13,6 +13,20 @@
13
13
  **改板前先 `dashboard info --id <dashboardId>`**:板级构成摘要(条目清单 + 悬浮层 +
14
14
  板级留痕)与本文语句面的条目一一对应,先清点再按需 `dashboard dsl` 取局部 DSL,
15
15
  不必为定位条目拉全量回读;`warnings` 出现条目级 `*_DROPPED` 留痕时按上一段处置。
16
+ 只看一个条目时用单条目切片 `dashboard dsl --id <dashboardId> --entry <名>`(重名板
17
+ 改用 `--entry-id <entries[].entryId>` 消歧):`dsl` 只含该条目语句 + 可选 `position`
18
+ (容器成员 position 是容器局部格位),`warnings`/`widgets` 同步过滤到目标条目。
19
+ **只改一个条目时用单条目写**:`dashboard update-entry --id <dashboardId> --file
20
+ <fragment.js>`(fragment 与切片同文法,是条目的完整新定义——读全条 → 改 → 写回;
21
+ 重名板带 `--entry-id` 消歧)。未点名条目原样保留,不会丢;widget 引用条目只改板侧面
22
+ (position/title),组件本体走 `widget update`。新增 / 删除条目用 `add-entry` /
23
+ `remove-entry`(被控件圈定或联动的条目删除会被拒,按错误信息先改引用)。`add-entry`
24
+ 的 fragment 无切片可抄、需手写:单条目语句(上文各条目语句形态之一)+ 可选
25
+ `position` 行,不包 `DashboardDsl.create` 整板脚本;缺省 position 落首个空位。
26
+ 并发编辑(含用户在设计器手改后再让 Agent 续改)带 `--expected-update-time`——基准取
27
+ 读板响应(`info` / `dsl` 的 `lastUpdateTime`)或上次写响应的新值,不匹配会报
28
+ `VERSION_CONFLICT`(附当前值),重读后重试。entryId 按条目名稳定:设计器保存与整板
29
+ `update` 下同名条目沿用原值,新增条目的新值从 update 响应 `entries[]` 直接可得。
16
30
 
17
31
  **写新看板前先取场景样例**:`dashboard template`(默认场景)返回场景目录与通用设计要点,
18
32
  `dashboard template --scenario <id>` 取某一个场景的完整可抄脚本 + `guidance` 设计要点
@@ -330,13 +344,28 @@ TextItem.of("标题", "销售概览")
330
344
  |------------------------------------------------------|---------------|
331
345
  | `.withTitle("标题")` | 设置文本组件标题。 |
332
346
  | `.withShowTitle(true)` | 设置是否显示文本组件标题。 |
333
- | `.withStyle({"fontSize":"20px", "color":"#333333"})` | 设置文本样式。 |
347
+ | `.withStyle({"backgroundColor":"#fffbe6"})` | 设置组件背景色;不设置正文字体样式。 |
334
348
 
335
349
  业务补充:
336
350
 
337
351
  - `name` 是文本组件业务名。
338
- - `content` 是文本正文内容(纯文本),或 `RichText.of()...build()` 富文本(动态字段值,见 §6.8)。
339
- - `style` 是样式键值对,可用于字体大小、颜色、字重、背景色等。
352
+ - 纯文本正文使用 `TextItem.of(name, text)`;带样式的静态文本或动态字段使用 `TextItem.ofRichText(name, RichText.of()...build())`,见 §6.8。
353
+ - `withStyle` 是组件级样式,前端只消费 `backgroundColor`。正文的字号、颜色、加粗和字体使用 `styled(...)`,不能放在 `withStyle` 中。
354
+
355
+ 静态标题也通过富文本设置字体样式:
356
+
357
+ ```js
358
+ TextItem.ofRichText("看板标题", RichText.of()
359
+ .line(styled("经营目标达成分析看板", {
360
+ "font-size": "32px",
361
+ "color": "#76b7b2",
362
+ "font-weight": "bold",
363
+ "font-family": "STKaiti, Kai, KaiTi, 楷体"
364
+ }))
365
+ .build())
366
+ ```
367
+
368
+ `styled` 使用 CSS 属性名(如 `font-size`,不能写 `fontSize`);字体能否呈现取决于浏览器所在设备可用的字体。完整富文本 API 和样式规则见 §6.8。
340
369
 
341
370
  ### 6.3 ImageItem
342
371
 
@@ -437,7 +466,7 @@ w/h 取值 100~10000(整数);省略 `.adaptive(...)` 语句 = 自动适应
437
466
  | `.line("文本", fieldRef, styled(...))` | 追加一行:静态文本 / 字段引用 / 行内样式文本段混排(字段引用见 §7 字段写法,dim/sum/avg/count 均可;也支持已保存计算字段 `measureID("配置 ID").dim()` 与业务模型字段 `model("模型").dataset("数据集").dim("字段")`)。 |
438
467
  | `styled(fieldRef, {"font-weight": "bold"})` | 行内样式**字段值**段:给动态字段值加行内样式(对齐编辑器里选中已插入的字段再点加粗/改色)。样式规则与下面的 `styled("文本", ...)` 完全一致。 |
439
468
  | `styled("文本", {"font-size": "20px"})` | 行内样式文本段:key 必须是 CSS 属性名(小写字母+连字符,如 font-size / color / font-weight),写入内联 `<span style="...">`;camelCase(如 fontSize)前端会静默丢弃,编译期直接报错。 |
440
- | `formatted(fieldRef, NumberFormatModel.number(...))` | 数值格式字段值:`formatted(from("销售表").sum("销售额"), NumberFormatModel.number(2))`。格式工厂:`NumberFormatModel.number(小数位[, NumberFormatModelScale.THOUSAND, "单位", 千分位])` / `NumberFormatModel.percentage(小数位, 千分位)`。**只作用于指标/数值值(sum/count 等聚合),dim 引用按维度文本处理格式不生效**;`auto()` 等于未设置会被编译期拒绝(用裸字段引用)。 |
469
+ | `formatted(fieldRef, NumberFormatModel.number(...))` | 数值格式字段值:`formatted(from("销售表").sum("销售额"), NumberFormatModel.number(2))`。格式工厂:`NumberFormatModel.number(小数位[, NumberFormatModelScale.THOUSAND, "单位", 千分位])` / `NumberFormatModel.percentage(小数位, 千分位)`。**只作用于指标/数值值(sum/count 等聚合),dim 引用按维度文本处理格式不生效**;默认 `auto()` 等于未设置会被编译期拒绝(用裸字段引用);带参数的自动格式可用,参数与单位规则见 [数值格式工厂](widget-dsl-api.md#8-数值格式工厂)。 |
441
470
  | `fieldValue(fieldRef)` | 带字段级配置的动态字段值(可选链,全部可省 = 平台默认):`.sort(SortOrder.ASC/DESC)` / `.sortByField(fieldRef, SortOrder.*)`(跨字段排序)/ `.dateFormat(FieldValue.SPLIT/CHINESE/SLASH)`(日期显示格式)/ `.numberFormat(NumberFormatModel...)` / `.where(fieldRef, "eq"\|"in"\|"between"\|"not_null", [值...])`(指标条件,可多次 AND 叠加;数值 between 前可用 `.closeRange(includeMin, includeMax)` 声明端点开闭,省略 = 全闭)/ `.showDefaultData()`,末尾 `.build()`。例:`fieldValue(from("销售表").dim("签约时间")).dateFormat(FieldValue.CHINESE).sort(SortOrder.DESC).build()`。**分组/汇总语义不在这里写**——由字段引用本身承载(`dim(名, DimensionGrouping.dateGranularity("YEAR_MONTH"))` / `sum(...)` / `avg(...)`)。字段类型门禁与前端字段面板一致:排序/显示默认数据限文本/日期字段,时间格式限日期字段,数值格式与指标条件限数值字段。指标条件的算子族按**被过滤字段**类型分派(数值:eq/between/not_null;文本:eq/in/not_null;日期:eq/between/not_null),过滤值必须是静态值(公式过滤平台不支持)。 |
442
471
  | `link("文本", "https://...")` | 超链接文本段:`line("详见 ", link("官网", "https://www.finebi.com"))`。查看态渲染为可点击链接;`javascript:` 协议编译期拒绝。只包静态文本——字段值/行内样式不能带链接(反读遇该形态丢弃链接保内容并给 `RICH_TEXT_LINK_DROPPED` 提示)。 |
443
472
  | `widgetName()` | 组件自身名称:查看态解析为组件在主题里的名字(组件改名后自动跟随)。**只有组件标题可用**——文本组件没有「组件名」插入项(写进文本组件会编译期报错,前端查看态也只会渲染成「元素被删除」)。 |
@@ -765,7 +794,7 @@ DashboardDsl.create("销售总览")
765
794
  ```js
766
795
  DashboardDsl.create("经营速览")
767
796
  .content()
768
- .text(TextItem.of("核心指标", RichText.of()
797
+ .text(TextItem.ofRichText("核心指标", RichText.of()
769
798
  .line("总销售额:", from("销售表").sum("销售额"), " 元")
770
799
  .line("订单数:", count(from("销售表").dim("订单号")))
771
800
  .build()))
@@ -20,7 +20,7 @@
20
20
  | 上传 | `analysis-ds upload-excel --file "<原始文件绝对路径>"` | `data.attachId`;不是表 ID | `ok=true` 且 attachId 非空;上传没有独立 info,建表验证其可消费性 |
21
21
  | 主题 | 按 [Subject 工作流](command-workflows.md#78-subject) 确认来源兼容性后复用主题,或 `subject add` 显式指定分析类型;参数以实时 help 为准 | 实际 subject ID | `subject info` 回查名称、位置和分析类型;省略 name 使用默认名称;失败但含 data.id 时先检查该资源,不重复创建 |
22
22
  | 分析表 | `analysis-ds template` 后生成 JS;`analysis-ds add --subject-id <实际主题ID> --file <table.js>` | 每张表的实际 ID、name 和字段 | `analysis-ds info/preview`,核对字段、类型、行数和样本 |
23
- | 关联 | `relationship info`;核对两侧键、粒度及唯一性后 `relationship add` | 真实关联及基数 | `relationship info` 与跨表查询验证,防止多对多重复累计 |
23
+ | 关联 | 先按 [Relationship 工作流](command-workflows.md#77-relationship) 检查能力开放;`relationship info`;核对两侧键、粒度及唯一性后 `relationship add` | 真实关联及基数 | `relationship info` 与跨表查询验证,防止多对多重复累计 |
24
24
  | 公式 | `calculated-field context --subject-id <实际主题ID>`;必须使用 `formula functions --subject-id <实际主题ID>` 查询目标主题面板(见 command-workflows.md §7.2);生成 `CalculatedFieldModel` JS,`validate` 后 `add` | 计算字段配置 ID、依赖与预期值 | info/dsl 回查;表达式通过还需在组件出数中核对结果 |
25
25
  | 组件 | 按 widget template/API 生成 JS,再 `widget add`;物理数值字段作维度/分类/粒度时先 `measure copy --as dimension` 注册副本(缺副本时 add 报错并指路该命令) | 实际组件 ID、准确名称 | `widget info/preview`;核对指标、分组、筛选和排序 |
26
26
  | 仪表板 | 按 dashboard template/schema/API 生成引用组件的 JS,再 `dashboard add` | 实际仪表板 ID/链接 | `dashboard dsl/preview`;核对组件、布局、筛选及关键数字 |
@@ -146,6 +146,8 @@
146
146
 
147
147
  ## 跨数据集关系
148
148
 
149
+ 关系命令的可用性先按 [Relationship 工作流](command-workflows.md#77-relationship) 检查;需要关系证据但缺少 `relationship info` 时进入 `BLOCKED`,不得假定关系存在或继续生成依赖该关系的制品。
150
+
149
151
  当某个 `requestedWidgets[]`(通过其 `encodings[].datasetKey`)或 `requestedDashboard` 基于字段的过滤器,引用了归属于不同
150
152
  `datasets[].key` 的字段时,这些数据集背后的分析表之间必须存在已确认的关系,FineBI 才能正确关联对应数据。解析器必须:
151
153
 
@@ -325,7 +325,7 @@ presentation()
325
325
  文本组件 content 支持动态字段值(见 `docs/dsl/dynamic-field-value-design.md`):
326
326
 
327
327
  ```
328
- TextItem.of("kpi", RichText.of()
328
+ TextItem.ofRichText("kpi", RichText.of()
329
329
  .line("总销售额:", from("Sales").sum("amount"), " 元")
330
330
  .line("按地区:", from("Sales").dim("region"))
331
331
  .build())
@@ -455,3 +455,21 @@ WidgetDsl.create("手机参数对比").chart().radar()
455
455
  .radarOptions(RadarOptions.polygon().marker(true).symbol("triangle"))
456
456
  .build();
457
457
  ```
458
+
459
+ ### 隐藏展示字段
460
+
461
+ 设计器"展示字段"开关的 DSL 表达:维度绑定进轴但查看态不展示(`used=false`,槽位保留)。
462
+ `.hidden()` 叠加在维度表达式末尾,可与分组、别名、经纬度组合;回读输出同一后缀。
463
+
464
+ ```js
465
+ WidgetDsl.create("区域销售表")
466
+ .chart().table()
467
+ .dimension(from("销售表").dim("地区"))
468
+ .dimension(from("销售表").dim("月份",
469
+ DimensionGrouping.dateGranularity("YEAR_MONTH"), "月份").hidden())
470
+ .measure(from("销售表").sum("销售额"))
471
+ .build()
472
+ ```
473
+
474
+ 适用场景:按隐藏粒度参与分组/计算,但界面只展示地区一级;或反读设计器组件时
475
+ 保留其"已关闭展示"的字段设置。同一字段多处引用须 hidden 状态一致,否则保存报错(明细表例外:同字段多列按列独立生效)。
@@ -145,8 +145,12 @@ windowCalculation(
145
145
 
146
146
  `windowCalculation(measure, configuration)` 是全局函数;`WindowCalculationModel` 是窗口配置命名空间。
147
147
  仅标准分析主题可使用移动计算;自助分析主题会在保存前报错,此限制同样适用于记录数和隐藏通道。
148
- 上述例子计算前一行与当前行的合计。计算作用于组件聚合后的结果行;窗口顺序取决于组件排序,
149
- 按日期移动时应使用日期维度并核对排序与出数。原指标可另放一列,逐行核对窗口计算值。
148
+ 组件形态需与设计器可达面一致,否则报错:组件至少要有一个维度;`calculateType` 组内要求
149
+ 表格某一轴(行/列)或图表多于一个维度;图表方向只能是 `0`(向下,设计器仅提供「默认」);
150
+ 表格方向按行/列区维度开放——`1` 向右需要列维度,`2`/`3` 组合方向要求行、列维度都有,
151
+ 且至少要有一个维度。上述例子计算前一行与当前行的合计。计算作用于组件聚合后的结果行;
152
+ 窗口顺序取决于组件排序,按日期移动时应使用日期维度并核对排序与出数。
153
+ 原指标可另放一列,逐行核对窗口计算值。
150
154
 
151
155
  | `WindowCalculationModel.of` 参数 | 可用值 |
152
156
  |---|---|
@@ -179,6 +183,7 @@ windowCalculation(
179
183
  | `from("来源表").dim("字段名", grouping, "别名")` | 同时指定分组和展示名。 |
180
184
  | `measureID("配置 ID").dim()` | 已保存计算字段使用默认分组。 |
181
185
  | `measureID("配置 ID").dim(grouping)` | 已保存计算字段使用与字段类型兼容的显式分组;不支持别名。 |
186
+ | `....hidden()` | 关闭展示字段:维度仍绑定进轴、保留槽位,查看态不展示(`used=false`);可叠加在上述任意维度形态之后,与分组/别名/经纬度组合。 |
182
187
 
183
188
  分组使用以下稳定语法:
184
189
 
@@ -211,6 +216,14 @@ DimensionGrouping.textGroups([
211
216
  孤立载荷、默认文本 ID_GROUP / 默认日期 YMD 维度旁的 stray customGroup)会作为 warning 返回;
212
217
  `cal[*]` 嵌套 customGroup 始终 warning。
213
218
 
219
+ `.hidden()` 对应设计器"展示字段"开关,回读保留(输出为维度表达式末尾的 `.hidden()` 后缀)。
220
+ 仅视图轴绑定的维度生效:filter 字段、钻取层级、迷你图依赖、KPI 通道等非展示面引用带
221
+ `.hidden()` 会报错或无效果(KPI 通道维度明确拒绝)。同一字段在同一组件内多处引用须保持
222
+ 一致 hidden 状态,否则保存报错——明细表例外:同字段多列按列独立生效,可一列隐藏一列展示;
223
+ 分类轴(`categoryAxis`)目标引用须重复携带一致的 `.hidden()` 后缀才能命中轴上维度。
224
+ 指标名维度(`metricNameInRow/InCol` 与自动指标名)不支持隐藏,设计器侧关闭时回读会以
225
+ warning 留痕。
226
+
214
227
  ### 2.2 聚合指标
215
228
 
216
229
  | DSL API | 业务含义 |
@@ -739,17 +752,29 @@ WidgetDsl.create("订单明细").chart().detailTable()
739
752
  | pageColumns(n) | 交叉表每页 1–1000 列,默认 7 |
740
753
  | sequence(boolean) | 显示序号,默认关闭 |
741
754
  | rowInfo(boolean) | 明细表显示行数信息,默认开启 |
742
- | totalRows(boolean) / totalPages(boolean) | 显示总行数/总页数,支持显式关闭;省略则继承平台查看态规则;不是合计行 |
755
+ | totalRows(boolean) / totalPages(boolean) | 显示总行数/总页数,支持显式关闭;省略则继承平台查看态规则;不是合计行;仅标准分析主题可声明,显式 true 时对应行/列展开须为 DISABLED |
743
756
  | rowExpansion(TableExpansion.DISABLED / COLLAPSED / EXPANDED) | 分组/交叉表禁止展开收起交互(全部展开)/允许交互且初始收起/允许交互且初始展开 |
744
757
  | columnExpansion(TableExpansion.DISABLED / COLLAPSED / EXPANDED) | 交叉表列维度展开规则,同上 |
745
758
  | freezeDimensions(boolean) | 分组/交叉表冻结维度,默认开启 |
746
759
  | frozenColumns(n) | 明细表冻结前 1–10 列;0 关闭冻结,默认 1 |
747
760
 
748
761
  未声明展开配置时使用 DSL 默认的可交互、初始收起;迷你图固定禁止行展开交互。
749
- 总行数/总页数配置入口属于 7.0.13+ 标准分析主题,建议同时关闭行/列展开交互;
762
+ 总行数/总页数配置入口属于 7.0.13+ 标准分析主题:自助分析主题下显式声明这两个开关中任一
763
+ 会报错(设计器不显示该开关组);标准分析主题下显式 true 要求对应行/列展开为
764
+ `TableExpansion.DISABLED`(交叉表行/列按钮任一开启即报错),显式 false 与省略等价始终放行。
750
765
  这两个布尔值省略时保留继承,显式 false 会关闭显示。宽度适应时前端不执行冻结。
751
766
  重复 tableFormat 以最后整组为准,空 Builder 恢复默认;不适用的表格专属选项直接报错。
752
767
 
768
+ **新建默认与设计器系统参数**:CLI 新建组件固定展开收起可交互、初始收起与总合计开启;标准分析
769
+ 主题下设计器**新建**表格会按系统参数 `standardAnalysisSupportExpandCollapse` /
770
+ `standardAnalysisSelectGrandTotal`(默认均为 false)改写为「隐藏展开按钮、关闭总合计」。需要与
771
+ 设计器新建同款默认形态时显式声明:`.tableFormat(...rowExpansion(TableExpansion.DISABLED)
772
+ .totalRows(false).totalPages(false)...)`(交叉表另加 `.columnExpansion(TableExpansion.DISABLED)`),
773
+ 并加 `.totalRows(TableTotal.rows().grand(false).build())`(交叉表列合计用
774
+ `.totalColumns(TableTotal.columns().grand(false).build())`);这些值都不是省略默认,回读与重放稳定。
775
+ 配方适用于分组表/交叉表;明细表不适用 `rowExpansion` 与合计配置,只需 `.tableFormat(...)`
776
+ 声明 `totalRows(false)` 与 `totalPages(false)`。
777
+
753
778
  ```javascript
754
779
  WidgetDsl.create("交叉表格式").chart().crossTable()
755
780
  .row(from("订单表").dim("地区"))
@@ -1280,6 +1305,13 @@ currentTime 在查询时取当前时钟,time 为固定整秒时刻;`.time(""
1280
1305
  | THIRD | 聚合计算过滤 | 聚合计算过滤 |
1281
1306
  | FOURTH | 快速计算过滤 | 快速计算过滤 |
1282
1307
 
1308
+ 未声明 `.level(...)` 时按设计器面板的默认落值写入(两主题不同):
1309
+ 维度自身过滤与未聚合指标过滤在标准主题恒第二层、自助主题第一层(top/bottom N 时第二层);
1310
+ 聚合指标过滤第三层、top/bottom N 时第四层;公式过滤引用指标时第二层、否则第一层。
1311
+ **明细表**过滤走独立编辑器,默认标准主题 22(固定明细档)、非标准主题第一层。已知差异
1312
+ (前端有、本实现暂未镜像,可用显式 `.level(...)` 覆盖):自助主题「数值维度/维度依赖聚合指标」
1313
+ 项前端固定第二层、本实现落第一层。带快速计算的聚合指标过滤前端默认第四层、标准化路径
1314
+ 不可声明(`.filter()` 的字段不接受携带快速计算)。
1283
1315
  普通维度进入分析区且依赖分组时可调整到四层;不在分析区时仅支持 FIRST/SECOND。
1284
1316
  自助主题的 DEF/WINDOW/FIXED 维度进入分析区后只允许 SECOND,维度依赖指标过滤允许 SECOND/THIRD/FOURTH。
1285
1317
  标准主题的聚合输出维度和分析区内依赖指标过滤仅允许 SECOND;新增列维度仅允许 FIRST/SECOND。
@@ -1314,18 +1346,24 @@ DSL 回读保留分类绑定、色板和映射,自动主题色不输出显式
1314
1346
 
1315
1347
  ```js
1316
1348
  NumberFormatModel.auto()
1349
+ NumberFormatModel.auto(2, NumberFormatModelScale.THOUSAND, null, true)
1317
1350
  NumberFormatModel.number(2)
1318
- NumberFormatModel.number(2, NumberFormatModelScale.TEN_THOUSAND, "万元", true)
1351
+ NumberFormatModel.number(2, NumberFormatModelScale.TEN_THOUSAND, "元", true)
1319
1352
  NumberFormatModel.percentage(1, true)
1320
1353
  ```
1321
1354
 
1322
1355
  | DSL API | 业务含义 |
1323
1356
  |---------------------------------------------------------------------|--------------------|
1324
1357
  | `NumberFormatModel.auto()` | 使用平台自动格式。 |
1358
+ | `NumberFormatModel.auto(decimalPlaces, scale, unit, useGrouping)` | 自动精度并保留缩放、单位和千分位配置;decimalPlaces 可为 null,保存面板值但不强制固定小数位。 |
1325
1359
  | `NumberFormatModel.number(decimalPlaces)` | 普通数值格式。 |
1326
1360
  | `NumberFormatModel.number(decimalPlaces, scale, unit, useGrouping)` | 带缩放、单位和千分位控制的数值格式。 |
1327
1361
  | `NumberFormatModel.percentage(decimalPlaces, useGrouping)` | 百分比格式。 |
1328
1362
 
1363
+ `scale` 同时决定缩放与数量级单位,`unit` 是追加的单位文本。例如 `TEN_THOUSAND` + `"元"`
1364
+ 显示“万元”;只显示“万”时使用 `null`,不要重复填写 `"万"`。`number(2, ...)` 固定两位小数;
1365
+ 带参数的 `auto(...)` 保留自动精度。只有完全默认的 `auto()` 在回读时归一为未设置。
1366
+
1329
1367
  ## 9. 可用枚举
1330
1368
 
1331
1369
  | 枚举 | 可用值 |
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finebi/finebi-cli",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "private": true,
5
5
  "description": "FineBI dynamic capability command line client.",
6
6
  "license": "UNLICENSED",
@@ -288,7 +288,7 @@ function commandOptionsFor(command) {
288
288
 
289
289
  function optionsFor(schema) {
290
290
  const required = schema.required || [];
291
- return Object.keys(schema.properties || {}).filter((fieldName) => schema.properties[fieldName].input !== "argument").map((fieldName) => {
291
+ return Object.keys(schema.properties || {}).filter((fieldName) => schema.properties[fieldName].input !== "argument").flatMap((fieldName) => {
292
292
  const property = schema.properties[fieldName];
293
293
  const optionName = optionNameFor(fieldName, property);
294
294
  const option = {
@@ -305,7 +305,15 @@ function optionsFor(schema) {
305
305
  if (defaultValue !== undefined && defaultValue !== "") {
306
306
  option.default = defaultValue;
307
307
  }
308
- return option;
308
+ if (property.keyValueOption) {
309
+ return [option, {
310
+ name: `--${property.keyValueOption}`,
311
+ required: false,
312
+ type: "name=value",
313
+ description: `Repeatable; maps each name to a one-element string array; mutually exclusive with --${optionName}. Split at the first =; empty names/values and duplicate names are rejected.`,
314
+ }];
315
+ }
316
+ return [option];
309
317
  });
310
318
  }
311
319
 
@@ -51,8 +51,11 @@ export async function runCli(argv, context = {}) {
51
51
  const management = args[0] === "auth" && ["list", "use", "remove"].includes(args[1]);
52
52
  const builtInHelp = isHelpFlagInvocation(args) && (args.length === 1 || isBuiltInResource(args[0]));
53
53
  const optional = management || builtInHelp || args.length === 0 || args[0] === "version";
54
- const profileName = management ? null : selectProfile(configPath(context), firstNonEmpty(flags.profile, env.FINEBI_PROFILE), {
55
- allowCreate: args[0] === "auth" && args[1] === "login", optional,
54
+ const requestedProfile = firstNonEmpty(flags.profile, env.FINEBI_PROFILE);
55
+ const allowCreate = args[0] === "auth" && args[1] === "login";
56
+ const initialDefault = !requestedProfile && !existsSync(configPath(context));
57
+ const profileName = management ? null : selectProfile(configPath(context), requestedProfile, {
58
+ allowCreate, optional,
56
59
  });
57
60
  if (management && (flags.profile || env.FINEBI_PROFILE) && args[1] !== "list") {
58
61
  if (flags.profile && flags.profile !== args[2]) throw new ProfileError("Use the positional environment name for auth use/remove; --profile must not select a different environment.");
@@ -64,7 +67,12 @@ export async function runCli(argv, context = {}) {
64
67
  );
65
68
  const envelope = mutatesProfile
66
69
  ? await withProfileLoginLock(configPath(context), args[0] === "auth" && args[1] === "remove" ? args[2] : profileName,
67
- () => dispatchCli(args, context))
70
+ () => {
71
+ if (!management) selectProfile(configPath(context), profileName, {
72
+ allowCreate: allowCreate || (initialDefault && profileName === "default"),
73
+ });
74
+ return dispatchCli(args, context);
75
+ })
68
76
  : await dispatchCli(args, context);
69
77
  if (profileName) {
70
78
  envelope.meta = {...envelope.meta, profile: profileName};
@@ -55,9 +55,11 @@ export async function handleInstallSkill(args, context = {}) {
55
55
  const lockPath = join(directory, "skill-install.lock");
56
56
  let lock;
57
57
  try {
58
- const npmSource = fileURLToPath(new URL("../skills/finebi-cli-agent/", import.meta.url));
59
- const source = context.skillSource || (existsSync(join(npmSource, "SKILL.md"))
60
- ? npmSource : fileURLToPath(new URL("../../../", import.meta.url)));
58
+ const source = context.skillSource || [
59
+ "../skills/finebi-cli-agent/", "../../skills/finebi-cli-agent/", "../../../",
60
+ ].map(path => fileURLToPath(new URL(path, import.meta.url)))
61
+ .find(path => existsSync(join(path, "SKILL.md")) && existsSync(join(path, "scripts/finebi-cli/src/index.js")));
62
+ if (!source) throw new Error("FineBI Skill resources were not found. Reinstall the CLI package or build the Skill in the source checkout.");
61
63
  await readFile(join(source, "SKILL.md"));
62
64
  await readFile(join(source, "scripts/finebi-cli/src/index.js"));
63
65
  await mkdir(directory, {recursive: true});
@@ -4,11 +4,12 @@ import {cliError} from "./errors.js";
4
4
  import {validateSchemaValue} from "./schema-validator.js";
5
5
 
6
6
  export function buildRequestBody(schema, args) {
7
- const parsed = parseArgs(args);
7
+ const properties = schema.properties || {};
8
+ const repeatable = new Set(Object.values(properties).map(property => property.keyValueOption).filter(Boolean));
9
+ const parsed = parseArgs(args, repeatable);
8
10
  if (!parsed.ok) {
9
11
  return parsed;
10
12
  }
11
- const properties = schema.properties || {};
12
13
  const argumentFields = Object.keys(properties).filter((fieldName) => properties[fieldName].input === "argument");
13
14
  if (parsed.positionals.length > argumentFields.length) {
14
15
  return invalidArgument(parsed.positionals[argumentFields.length], `Unexpected argument: ${parsed.positionals[argumentFields.length]}.`);
@@ -33,10 +34,20 @@ export function buildRequestBody(schema, args) {
33
34
  for (const fieldName of Object.keys(properties)) {
34
35
  const property = properties[fieldName];
35
36
  const optionName = optionNameFor(fieldName, property);
37
+ const inlineValues = property.keyValueOption && parsed.options[property.keyValueOption];
38
+ if (inlineValues && Object.prototype.hasOwnProperty.call(parsed.options, optionName)) {
39
+ return invalidArgument(fieldName, `--${property.keyValueOption} and --${optionName} are mutually exclusive.`);
40
+ }
36
41
  if (property.input === "argument" && body[fieldName] !== undefined) {
37
42
  continue;
38
43
  }
39
- if (Object.prototype.hasOwnProperty.call(parsed.options, optionName)) {
44
+ if (inlineValues) {
45
+ const coerced = coerceKeyValues(fieldName, inlineValues, property);
46
+ if (!coerced.ok) {
47
+ return coerced;
48
+ }
49
+ body[fieldName] = coerced.value;
50
+ } else if (Object.prototype.hasOwnProperty.call(parsed.options, optionName)) {
40
51
  const coerced = coerceValue(fieldName, parsed.options[optionName], property);
41
52
  if (!coerced.ok) {
42
53
  return coerced;
@@ -60,7 +71,8 @@ export function buildRequestBody(schema, args) {
60
71
  }
61
72
  }
62
73
  for (const optionName of Object.keys(parsed.options)) {
63
- if (!Object.keys(properties).some((fieldName) => optionNameFor(fieldName, properties[fieldName]) === optionName)) {
74
+ if (!Object.keys(properties).some((fieldName) => optionNameFor(fieldName, properties[fieldName]) === optionName
75
+ || properties[fieldName].keyValueOption === optionName)) {
64
76
  return invalidArgument(`--${optionName}`, `Unknown option: --${optionName}.`);
65
77
  }
66
78
  }
@@ -75,7 +87,7 @@ export function buildRequestBody(schema, args) {
75
87
  };
76
88
  }
77
89
 
78
- function parseArgs(args) {
90
+ function parseArgs(args, repeatable) {
79
91
  const options = {};
80
92
  const positionals = [];
81
93
  for (let index = 0; index < args.length; index += 1) {
@@ -86,10 +98,13 @@ function parseArgs(args) {
86
98
  }
87
99
  const optionName = current.slice(2);
88
100
  const next = args[index + 1];
89
- if (next === undefined || next.startsWith("--")) {
90
- options[optionName] = true;
101
+ const value = next === undefined || next.startsWith("--") ? true : next;
102
+ if (repeatable.has(optionName)) {
103
+ (options[optionName] ||= []).push(value);
91
104
  } else {
92
- options[optionName] = next;
105
+ options[optionName] = value;
106
+ }
107
+ if (value !== true) {
93
108
  index += 1;
94
109
  }
95
110
  }
@@ -100,6 +115,27 @@ function parseArgs(args) {
100
115
  };
101
116
  }
102
117
 
118
+ function coerceKeyValues(fieldName, values, property) {
119
+ const entries = new Map();
120
+ for (const value of values) {
121
+ const separator = typeof value === "string" ? value.indexOf("=") : -1;
122
+ if (separator < 1 || !value.slice(0, separator).trim() || !value.slice(separator + 1).trim()) {
123
+ return invalidArgument(fieldName, `--${property.keyValueOption} requires name=value with a non-empty name and value.`);
124
+ }
125
+ const name = value.slice(0, separator);
126
+ if (entries.has(name)) {
127
+ return invalidArgument(fieldName, `Duplicate name in --${property.keyValueOption}: ${name}.`);
128
+ }
129
+ entries.set(name, [value.slice(separator + 1)]);
130
+ }
131
+ const value = Object.fromEntries(entries);
132
+ if (property.schema) {
133
+ const validation = validateSchemaValue(value, property.schema, fieldName);
134
+ if (!validation.ok) return validation;
135
+ }
136
+ return {ok: true, value};
137
+ }
138
+
103
139
  function coerceValue(fieldName, value, property) {
104
140
  if (property.input === "file" || property.file) {
105
141
  if (typeof value !== "string" || value.trim() === "") {
package/src/help.js CHANGED
@@ -288,7 +288,7 @@ function commandOptionsFor(command) {
288
288
 
289
289
  function optionsFor(schema) {
290
290
  const required = schema.required || [];
291
- return Object.keys(schema.properties || {}).filter((fieldName) => schema.properties[fieldName].input !== "argument").map((fieldName) => {
291
+ return Object.keys(schema.properties || {}).filter((fieldName) => schema.properties[fieldName].input !== "argument").flatMap((fieldName) => {
292
292
  const property = schema.properties[fieldName];
293
293
  const optionName = optionNameFor(fieldName, property);
294
294
  const option = {
@@ -305,7 +305,15 @@ function optionsFor(schema) {
305
305
  if (defaultValue !== undefined && defaultValue !== "") {
306
306
  option.default = defaultValue;
307
307
  }
308
- return option;
308
+ if (property.keyValueOption) {
309
+ return [option, {
310
+ name: `--${property.keyValueOption}`,
311
+ required: false,
312
+ type: "name=value",
313
+ description: `Repeatable; maps each name to a one-element string array; mutually exclusive with --${optionName}. Split at the first =; empty names/values and duplicate names are rejected.`,
314
+ }];
315
+ }
316
+ return [option];
309
317
  });
310
318
  }
311
319
 
package/src/index.js CHANGED
@@ -51,8 +51,11 @@ export async function runCli(argv, context = {}) {
51
51
  const management = args[0] === "auth" && ["list", "use", "remove"].includes(args[1]);
52
52
  const builtInHelp = isHelpFlagInvocation(args) && (args.length === 1 || isBuiltInResource(args[0]));
53
53
  const optional = management || builtInHelp || args.length === 0 || args[0] === "version";
54
- const profileName = management ? null : selectProfile(configPath(context), firstNonEmpty(flags.profile, env.FINEBI_PROFILE), {
55
- allowCreate: args[0] === "auth" && args[1] === "login", optional,
54
+ const requestedProfile = firstNonEmpty(flags.profile, env.FINEBI_PROFILE);
55
+ const allowCreate = args[0] === "auth" && args[1] === "login";
56
+ const initialDefault = !requestedProfile && !existsSync(configPath(context));
57
+ const profileName = management ? null : selectProfile(configPath(context), requestedProfile, {
58
+ allowCreate, optional,
56
59
  });
57
60
  if (management && (flags.profile || env.FINEBI_PROFILE) && args[1] !== "list") {
58
61
  if (flags.profile && flags.profile !== args[2]) throw new ProfileError("Use the positional environment name for auth use/remove; --profile must not select a different environment.");
@@ -64,7 +67,12 @@ export async function runCli(argv, context = {}) {
64
67
  );
65
68
  const envelope = mutatesProfile
66
69
  ? await withProfileLoginLock(configPath(context), args[0] === "auth" && args[1] === "remove" ? args[2] : profileName,
67
- () => dispatchCli(args, context))
70
+ () => {
71
+ if (!management) selectProfile(configPath(context), profileName, {
72
+ allowCreate: allowCreate || (initialDefault && profileName === "default"),
73
+ });
74
+ return dispatchCli(args, context);
75
+ })
68
76
  : await dispatchCli(args, context);
69
77
  if (profileName) {
70
78
  envelope.meta = {...envelope.meta, profile: profileName};
@@ -55,9 +55,11 @@ export async function handleInstallSkill(args, context = {}) {
55
55
  const lockPath = join(directory, "skill-install.lock");
56
56
  let lock;
57
57
  try {
58
- const npmSource = fileURLToPath(new URL("../skills/finebi-cli-agent/", import.meta.url));
59
- const source = context.skillSource || (existsSync(join(npmSource, "SKILL.md"))
60
- ? npmSource : fileURLToPath(new URL("../../../", import.meta.url)));
58
+ const source = context.skillSource || [
59
+ "../skills/finebi-cli-agent/", "../../skills/finebi-cli-agent/", "../../../",
60
+ ].map(path => fileURLToPath(new URL(path, import.meta.url)))
61
+ .find(path => existsSync(join(path, "SKILL.md")) && existsSync(join(path, "scripts/finebi-cli/src/index.js")));
62
+ if (!source) throw new Error("FineBI Skill resources were not found. Reinstall the CLI package or build the Skill in the source checkout.");
61
63
  await readFile(join(source, "SKILL.md"));
62
64
  await readFile(join(source, "scripts/finebi-cli/src/index.js"));
63
65
  await mkdir(directory, {recursive: true});
@@ -4,11 +4,12 @@ import {cliError} from "./errors.js";
4
4
  import {validateSchemaValue} from "./schema-validator.js";
5
5
 
6
6
  export function buildRequestBody(schema, args) {
7
- const parsed = parseArgs(args);
7
+ const properties = schema.properties || {};
8
+ const repeatable = new Set(Object.values(properties).map(property => property.keyValueOption).filter(Boolean));
9
+ const parsed = parseArgs(args, repeatable);
8
10
  if (!parsed.ok) {
9
11
  return parsed;
10
12
  }
11
- const properties = schema.properties || {};
12
13
  const argumentFields = Object.keys(properties).filter((fieldName) => properties[fieldName].input === "argument");
13
14
  if (parsed.positionals.length > argumentFields.length) {
14
15
  return invalidArgument(parsed.positionals[argumentFields.length], `Unexpected argument: ${parsed.positionals[argumentFields.length]}.`);
@@ -33,10 +34,20 @@ export function buildRequestBody(schema, args) {
33
34
  for (const fieldName of Object.keys(properties)) {
34
35
  const property = properties[fieldName];
35
36
  const optionName = optionNameFor(fieldName, property);
37
+ const inlineValues = property.keyValueOption && parsed.options[property.keyValueOption];
38
+ if (inlineValues && Object.prototype.hasOwnProperty.call(parsed.options, optionName)) {
39
+ return invalidArgument(fieldName, `--${property.keyValueOption} and --${optionName} are mutually exclusive.`);
40
+ }
36
41
  if (property.input === "argument" && body[fieldName] !== undefined) {
37
42
  continue;
38
43
  }
39
- if (Object.prototype.hasOwnProperty.call(parsed.options, optionName)) {
44
+ if (inlineValues) {
45
+ const coerced = coerceKeyValues(fieldName, inlineValues, property);
46
+ if (!coerced.ok) {
47
+ return coerced;
48
+ }
49
+ body[fieldName] = coerced.value;
50
+ } else if (Object.prototype.hasOwnProperty.call(parsed.options, optionName)) {
40
51
  const coerced = coerceValue(fieldName, parsed.options[optionName], property);
41
52
  if (!coerced.ok) {
42
53
  return coerced;
@@ -60,7 +71,8 @@ export function buildRequestBody(schema, args) {
60
71
  }
61
72
  }
62
73
  for (const optionName of Object.keys(parsed.options)) {
63
- if (!Object.keys(properties).some((fieldName) => optionNameFor(fieldName, properties[fieldName]) === optionName)) {
74
+ if (!Object.keys(properties).some((fieldName) => optionNameFor(fieldName, properties[fieldName]) === optionName
75
+ || properties[fieldName].keyValueOption === optionName)) {
64
76
  return invalidArgument(`--${optionName}`, `Unknown option: --${optionName}.`);
65
77
  }
66
78
  }
@@ -75,7 +87,7 @@ export function buildRequestBody(schema, args) {
75
87
  };
76
88
  }
77
89
 
78
- function parseArgs(args) {
90
+ function parseArgs(args, repeatable) {
79
91
  const options = {};
80
92
  const positionals = [];
81
93
  for (let index = 0; index < args.length; index += 1) {
@@ -86,10 +98,13 @@ function parseArgs(args) {
86
98
  }
87
99
  const optionName = current.slice(2);
88
100
  const next = args[index + 1];
89
- if (next === undefined || next.startsWith("--")) {
90
- options[optionName] = true;
101
+ const value = next === undefined || next.startsWith("--") ? true : next;
102
+ if (repeatable.has(optionName)) {
103
+ (options[optionName] ||= []).push(value);
91
104
  } else {
92
- options[optionName] = next;
105
+ options[optionName] = value;
106
+ }
107
+ if (value !== true) {
93
108
  index += 1;
94
109
  }
95
110
  }
@@ -100,6 +115,27 @@ function parseArgs(args) {
100
115
  };
101
116
  }
102
117
 
118
+ function coerceKeyValues(fieldName, values, property) {
119
+ const entries = new Map();
120
+ for (const value of values) {
121
+ const separator = typeof value === "string" ? value.indexOf("=") : -1;
122
+ if (separator < 1 || !value.slice(0, separator).trim() || !value.slice(separator + 1).trim()) {
123
+ return invalidArgument(fieldName, `--${property.keyValueOption} requires name=value with a non-empty name and value.`);
124
+ }
125
+ const name = value.slice(0, separator);
126
+ if (entries.has(name)) {
127
+ return invalidArgument(fieldName, `Duplicate name in --${property.keyValueOption}: ${name}.`);
128
+ }
129
+ entries.set(name, [value.slice(separator + 1)]);
130
+ }
131
+ const value = Object.fromEntries(entries);
132
+ if (property.schema) {
133
+ const validation = validateSchemaValue(value, property.schema, fieldName);
134
+ if (!validation.ok) return validation;
135
+ }
136
+ return {ok: true, value};
137
+ }
138
+
103
139
  function coerceValue(fieldName, value, property) {
104
140
  if (property.input === "file" || property.file) {
105
141
  if (typeof value !== "string" || value.trim() === "") {