@restoai/resto-datacli 0.1.11 → 0.2.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.
@@ -86,6 +86,21 @@ function issueToMessage(issue) {
86
86
  function findReportField(fields, fieldName) {
87
87
  return fields.find((field) => field.fieldName === fieldName);
88
88
  }
89
+ function findReportFilterSurface(report, fieldName) {
90
+ return report.filterSurfaces?.find((surface) => surface.fieldName === fieldName || surface.valueField === fieldName);
91
+ }
92
+ function filterSurfaceToPublicFieldRecord(surface) {
93
+ return {
94
+ reportId: surface.reportId,
95
+ fieldName: surface.fieldName,
96
+ displayText: surface.displayText,
97
+ fieldType: "DIMENSION",
98
+ filterable: true,
99
+ ...(surface.aliases ? { aliases: surface.aliases } : {}),
100
+ tags: ["filter", surface.surface],
101
+ explain: surface.displayText
102
+ };
103
+ }
89
104
  function orderByFieldNames(orderBy) {
90
105
  return (orderBy ?? []).flatMap((entry) => Object.keys(entry));
91
106
  }
@@ -214,7 +229,7 @@ function validateSafeFilterType(filterType, errors) {
214
229
  errors.push("Filter type SUB is not allowed for safe CLI execution");
215
230
  }
216
231
  else if (!supportedFilterType(filterType)) {
217
- errors.push(`Filter type ${filterType} is not supported by report-api`);
232
+ errors.push(`Filter type ${filterType} is not supported by the report service`);
218
233
  }
219
234
  }
220
235
  export class QueryPlanValidator {
@@ -255,16 +270,17 @@ export class QueryPlanValidator {
255
270
  }
256
271
  for (const filter of validPlan.filters) {
257
272
  const field = findReportField(reportFields, filter.fieldName);
273
+ const filterSurface = findReportFilterSurface(report, filter.fieldName);
258
274
  validateSafeFilterType(filter.filterType, errors);
259
- if (!field) {
275
+ if (!field && !filterSurface) {
260
276
  errors.push(`Filter field ${filter.fieldName} is not available on report ${validPlan.reportId}`);
261
277
  continue;
262
278
  }
263
- if (field.filterable !== true) {
279
+ if (field && field.filterable !== true && !filterSurface) {
264
280
  errors.push(`Filter field ${filter.fieldName} is not filterable on report ${validPlan.reportId}`);
265
281
  continue;
266
282
  }
267
- evidence.filterFields.push(toPublicFieldRecord(field));
283
+ evidence.filterFields.push(field && field.filterable === true ? toPublicFieldRecord(field) : filterSurfaceToPublicFieldRecord(filterSurface));
268
284
  }
269
285
  for (const filter of validPlan.aggFilters ?? []) {
270
286
  const field = findReportField(reportFields, filter.fieldName);
@@ -68,14 +68,14 @@ function throwIfBusinessError(body, sensitiveValues = []) {
68
68
  if (!isRecord(body)) {
69
69
  return;
70
70
  }
71
- const code = stringValue(body.code);
71
+ const code = stringValue(body.code ?? body.errorCode);
72
72
  const success = typeof body.success === "boolean" ? body.success : undefined;
73
73
  if ((!code || code === "000") && success !== false) {
74
74
  return;
75
75
  }
76
76
  const safeCode = code || "UNKNOWN";
77
77
  const message = sanitizeRemoteMessage(body.msg ?? body.message ?? body.errorMessage, sensitiveValues);
78
- throw new ReportApiError(`Report API request failed with code ${safeCode}${message ? `: ${message}` : ""}`);
78
+ throw new ReportApiError(`report-cli-service request failed with code ${safeCode}${message ? `: ${message}` : ""}`);
79
79
  }
80
80
  function normalizeCurrency(value) {
81
81
  if (!isRecord(value)) {
@@ -152,58 +152,148 @@ export function normalizeQueryDataResponse(body) {
152
152
  raw: body
153
153
  };
154
154
  }
155
- export class ReportApiClient {
155
+ export function normalizeReportOrderDetailResponse(body) {
156
+ const root = isRecord(body) ? body : {};
157
+ const data = isRecord(root.data) ? root.data : root;
158
+ const reportId = stringValue(data.reportId);
159
+ const shopId = stringValue(data.shopId);
160
+ const detail = isRecord(data.detail) ? data.detail : undefined;
161
+ if (!reportId || !shopId || !detail) {
162
+ throw new ReportApiError("report-cli-service returned an invalid order detail");
163
+ }
164
+ return {
165
+ reportId,
166
+ shopId,
167
+ ...(stringValue(data.posOrderId) ? { posOrderId: stringValue(data.posOrderId) } : {}),
168
+ ...(stringValue(data.orderId) ? { orderId: stringValue(data.orderId) } : {}),
169
+ detail,
170
+ raw: body
171
+ };
172
+ }
173
+ function requireReportCliFilterValues(filter) {
174
+ const rawValues = Array.isArray(filter.filterValue) ? filter.filterValue : [filter.filterValue];
175
+ const values = rawValues.flatMap((value) => {
176
+ const text = stringValue(value);
177
+ return text === undefined ? [] : [text];
178
+ });
179
+ if (values.length !== rawValues.length || values.length === 0) {
180
+ throw new ReportApiError(`report-cli-service backend does not support non-scalar filter value for ${filter.fieldName}`);
181
+ }
182
+ return values;
183
+ }
184
+ function toReportCliOperator(filterType) {
185
+ if (["EQ", "IN", "RANGE", "LIKE", "GT", "GOE", "LT", "LOE", "NE", "NOT_IN"].includes(filterType)) {
186
+ return filterType;
187
+ }
188
+ throw new ReportApiError(`report-cli-service does not support filter type ${filterType}`);
189
+ }
190
+ function toReportCliFilter(filter) {
191
+ return {
192
+ fieldName: filter.fieldName,
193
+ operator: toReportCliOperator(filter.filterType),
194
+ ...(filter.operatorType ? { operatorType: filter.operatorType } : {}),
195
+ values: requireReportCliFilterValues(filter)
196
+ };
197
+ }
198
+ function toReportCliOrderBy(orderBy) {
199
+ return (orderBy ?? []).flatMap((entry) => Object.entries(entry).map(([fieldName, direction]) => ({
200
+ fieldName,
201
+ direction
202
+ })));
203
+ }
204
+ function toReportCliServiceRequest(plan, auth) {
205
+ const filters = plan.filters.map(toReportCliFilter);
206
+ if (!filters.some((filter) => filter.fieldName === "D_corporationId")) {
207
+ filters.push({
208
+ fieldName: "D_corporationId",
209
+ operator: "EQ",
210
+ values: [auth.corporationId]
211
+ });
212
+ }
213
+ const aggFilters = (plan.aggFilters ?? []).map(toReportCliFilter);
214
+ const orderBy = toReportCliOrderBy(plan.orderBy);
215
+ return {
216
+ reportId: plan.reportId,
217
+ selectFields: plan.selectFields,
218
+ filters,
219
+ ...(aggFilters.length > 0 ? { aggFilters } : {}),
220
+ ...(orderBy.length > 0 ? { orderBy } : {}),
221
+ ...(plan.page ? { page: plan.page } : {}),
222
+ ...(plan.proportionProperty ? { proportionProperty: plan.proportionProperty } : {}),
223
+ ...(plan.metricsByDimQryV2 ? { metricsByDimQryV2: plan.metricsByDimQryV2 } : {}),
224
+ ...(plan.dimAdditionalStrategy ? { dimAdditionalStrategy: plan.dimAdditionalStrategy } : {})
225
+ };
226
+ }
227
+ export class ReportCliServiceClient {
156
228
  auth;
229
+ baseUrl;
157
230
  fetchImpl;
158
231
  constructor(options) {
159
232
  this.auth = options.auth;
233
+ this.baseUrl = options.baseUrl;
160
234
  this.fetchImpl = options.fetchImpl ?? undiciFetch;
161
235
  }
162
236
  async queryData(plan, _context = { env: "test", schemaVersion: 1 }) {
163
- const response = await this.fetchImpl(`${trimTrailingSlash(this.auth.baseUrl)}/api/report/data/queryData`, {
237
+ const response = await this.fetchImpl(`${trimTrailingSlash(this.baseUrl)}/api/report-cli/report/data/query`, {
164
238
  method: "POST",
165
239
  headers: {
166
- "corporation-id": this.auth.corporationId,
240
+ "Corporation-Id": this.auth.corporationId,
167
241
  "Vulcan-Token": this.auth.token,
168
242
  "Language-Code": this.auth.languageCode,
169
243
  "Accept-Timezone": this.auth.timezone,
170
244
  "Content-Type": "application/json"
171
245
  },
172
- body: JSON.stringify(plan)
246
+ body: JSON.stringify(toReportCliServiceRequest(plan, this.auth))
173
247
  });
174
248
  if (!response.ok) {
175
- throw new ReportApiError(`Report API request failed with status ${response.status}`, response.status);
249
+ throw new ReportApiError(`report-cli-service request failed with status ${response.status}`, response.status);
176
250
  }
177
251
  const body = await response.json();
178
252
  throwIfBusinessError(body, [this.auth.token]);
179
253
  return normalizeQueryDataResponse(body);
180
254
  }
181
255
  async getReportShopFilters(request) {
182
- const corporationId = Number.parseInt(this.auth.corporationId, 10);
256
+ const organizationType = Number.parseInt(request.organizationType, 10);
183
257
  const body = {
184
- corporationId: Number.isFinite(corporationId) ? corporationId : this.auth.corporationId,
185
- employeeCode: request.employeeCode,
186
- organizationId: request.organizationId,
258
+ organizationType: Number.isFinite(organizationType) ? organizationType : request.organizationType,
187
259
  orgTypeList: request.orgTypeList,
188
260
  ...(request.reportId === undefined ? {} : { reportId: request.reportId })
189
261
  };
190
- const response = await this.fetchImpl(`${trimTrailingSlash(this.auth.baseUrl)}/api/report/merchant/getReportShopFilters`, {
262
+ const response = await this.fetchImpl(`${trimTrailingSlash(this.baseUrl)}/api/report-cli/shop/filters`, {
191
263
  method: "POST",
192
264
  headers: {
193
- "corporation-id": this.auth.corporationId,
265
+ "Corporation-Id": this.auth.corporationId,
194
266
  "Vulcan-Token": this.auth.token,
195
267
  "Language-Code": this.auth.languageCode,
196
268
  "Accept-Timezone": this.auth.timezone,
197
- "Organization-Type": request.organizationType,
198
269
  "Content-Type": "application/json"
199
270
  },
200
271
  body: JSON.stringify(body)
201
272
  });
202
273
  if (!response.ok) {
203
- throw new ReportApiError(`Report API request failed with status ${response.status}`, response.status);
274
+ throw new ReportApiError(`report-cli-service request failed with status ${response.status}`, response.status);
204
275
  }
205
276
  const responseBody = await response.json();
206
277
  throwIfBusinessError(responseBody, [this.auth.token]);
207
278
  return normalizeReportShopFiltersResponse(responseBody);
208
279
  }
280
+ async getReportOrderDetail(request) {
281
+ const response = await this.fetchImpl(`${trimTrailingSlash(this.baseUrl)}/api/report-cli/report/order/detail`, {
282
+ method: "POST",
283
+ headers: {
284
+ "Corporation-Id": this.auth.corporationId,
285
+ "Vulcan-Token": this.auth.token,
286
+ "Language-Code": this.auth.languageCode,
287
+ "Accept-Timezone": this.auth.timezone,
288
+ "Content-Type": "application/json"
289
+ },
290
+ body: JSON.stringify(request)
291
+ });
292
+ if (!response.ok) {
293
+ throw new ReportApiError(`report-cli-service request failed with status ${response.status}`, response.status);
294
+ }
295
+ const responseBody = await response.json();
296
+ throwIfBusinessError(responseBody, [this.auth.token]);
297
+ return normalizeReportOrderDetailResponse(responseBody);
298
+ }
209
299
  }
@@ -1,6 +1,6 @@
1
1
  # Resto Data CLI 与报表问数 Agent 安装说明
2
2
 
3
- 以下步骤面向 AI Agent 和客户实施人员。部分步骤需要用户提供 BO 账号、密码或 token。
3
+ 以下步骤面向 AI Agent 和客户实施人员。部分步骤需要用户提供手机号验证码或账号密码来获取 CLI 鉴权凭证。
4
4
 
5
5
  ## 环境要求
6
6
 
@@ -95,7 +95,7 @@ $WORKBUDDY_CONFIG_DIR/skills/resto-datacli
95
95
 
96
96
  ## 第 2 步 配置访问 Resto BO 的凭证
97
97
 
98
- 推荐让用户使用交互式登录。密码和 token 粘贴后不会回显。
98
+ 推荐让用户使用交互式登录。验证码和密码粘贴后不会回显。
99
99
 
100
100
  ```shell
101
101
  resto auth login --interactive
@@ -103,35 +103,65 @@ resto auth login --interactive
103
103
 
104
104
  交互流程:
105
105
 
106
- 1. 输入 BO 地址
107
- - 测试环境:`https://bo.test.restosuite.ai`
108
- - 线上中国区:`https://bo.restosuite.cn`
106
+ 1. 输入 CLI 鉴权网关地址
107
+ - 测试环境:`https://cli.test.restosuite.ai`
108
+ - 线上中国区:以运维提供的 CLI 鉴权网关域名为准
109
109
  2. 输入集团 ID,例如 `256` 或 `302`
110
110
  3. 选择鉴权方式
111
+ - 手机号验证码
111
112
  - 账号密码
112
- - token
113
- 4. 按提示输入账号、密码或 token
113
+ 4. 手机号验证码登录会先发送验证码,再按提示输入验证码;账号密码登录按提示输入账号、密码
114
114
 
115
- 非交互式 token 登录:
115
+ 非交互式手机号验证码登录:
116
116
 
117
117
  ```shell
118
118
  resto auth login \
119
- --base-url https://bo.restosuite.cn \
119
+ --base-url https://cli.test.restosuite.ai \
120
+ --plat cli \
120
121
  --corporation-id <集团ID> \
121
- --token <BO登录token>
122
+ --phone-number <手机号> \
123
+ --dynamic-code <验证码>
122
124
  ```
123
125
 
126
+ 手机号验证码登录会通过 CLI 鉴权网关获取 `plat=cli` 专用 token,随后读取 profile 上下文并缓存员工编码。CLI 不接受用户手工粘贴 token 作为登录方式。
127
+
124
128
  非交互式账号密码登录:
125
129
 
126
130
  ```shell
127
131
  resto auth login \
128
- --base-url https://bo.restosuite.cn \
132
+ --base-url https://cli.test.restosuite.ai \
133
+ --plat cli \
129
134
  --corporation-id <集团ID> \
130
135
  --username <账号> \
131
136
  --password <密码>
132
137
  ```
133
138
 
134
- `--base-url` 是 BO 网关地址。登录、知识包同步和 `resto query execute` 都通过该 BO 网关访问;不需要单独配置 `report-knowledge-service` 域名。
139
+ `--base-url` 是 CLI 鉴权网关地址,仅用于登录和获取 token;`--plat` 默认为 `cli`。知识包 manifest 同步/检查、报表查询和门店筛选请求统一发送到 `report-cli-service`;本地或测试服务地址可通过 `--report-cli-url` 或 `RESTO_REPORT_CLI_URL` 配置。
140
+
141
+ 如果要验证本地 `report-cli-service` 查询链路,可以在启动本地 `report-api` 和 `report-cli-service` 后指定 service 地址:
142
+
143
+ ```shell
144
+ resto query execute \
145
+ --plan ./plan.json \
146
+ --report-cli-url http://127.0.0.1:18081
147
+ ```
148
+
149
+ 也可以通过环境变量固定本地 service 地址:
150
+
151
+ ```shell
152
+ RESTO_REPORT_CLI_URL=http://127.0.0.1:18081 \
153
+ resto query execute --plan ./plan.json
154
+ ```
155
+
156
+ 门店筛选同样通过 `report-cli-service`:
157
+
158
+ ```shell
159
+ resto shop search zn线上店铺 \
160
+ --report-id 888001 \
161
+ --organization-type 1 \
162
+ --org-type-list 1,2 \
163
+ --report-cli-url http://127.0.0.1:18081
164
+ ```
135
165
 
136
166
  中国区上线阶段,`language-code` 和 `timezone` 默认使用:
137
167
 
@@ -145,7 +175,7 @@ resto auth login \
145
175
  登录完成后,同步当前环境的报表知识:
146
176
 
147
177
  ```shell
148
- resto knowledge sync
178
+ resto knowledge sync --report-cli-url http://127.0.0.1:18081
149
179
  resto knowledge status
150
180
  ```
151
181
 
@@ -236,7 +266,7 @@ resto auth login --interactive
236
266
 
237
267
  ### `resto query execute` 返回 401
238
268
 
239
- 一般是 BO token 过期或组织上下文失效。重新登录后再执行:
269
+ 一般是 CLI gateway token 过期、不是 `plat=cli` 生成的 token,或组织上下文失效。重新登录后再执行:
240
270
 
241
271
  ```shell
242
272
  resto auth login --interactive
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@restoai/resto-datacli",
3
- "version": "0.1.11",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "resto": "dist/cli.js"