@restoai/resto-datacli 0.1.12 → 0.2.1
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/.agents/skills/resto-datacli/SKILL.md +21 -2
- package/README.md +44 -8
- package/dist/cli.js +2 -0
- package/dist/commands/auth.js +366 -99
- package/dist/commands/knowledge.js +32 -13
- package/dist/commands/order.js +109 -0
- package/dist/commands/public-records.js +32 -0
- package/dist/commands/query.js +22 -4
- package/dist/commands/shop.js +66 -14
- package/dist/config/profile.js +1 -0
- package/dist/knowledge/local-store.js +2 -0
- package/dist/knowledge/manifest-client.js +52 -3
- package/dist/knowledge/schema.js +2 -0
- package/dist/knowledge/search-index.js +46 -0
- package/dist/report-api/query-plan.js +20 -4
- package/dist/report-api/report-api-client.js +105 -15
- package/docs/installation.md +44 -14
- package/package.json +1 -1
|
@@ -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
|
|
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(`
|
|
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
|
|
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.
|
|
237
|
+
const response = await this.fetchImpl(`${trimTrailingSlash(this.baseUrl)}/api/report-cli/report/data/query`, {
|
|
164
238
|
method: "POST",
|
|
165
239
|
headers: {
|
|
166
|
-
"
|
|
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(`
|
|
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
|
|
256
|
+
const organizationType = Number.parseInt(request.organizationType, 10);
|
|
183
257
|
const body = {
|
|
184
|
-
|
|
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.
|
|
262
|
+
const response = await this.fetchImpl(`${trimTrailingSlash(this.baseUrl)}/api/report-cli/shop/filters`, {
|
|
191
263
|
method: "POST",
|
|
192
264
|
headers: {
|
|
193
|
-
"
|
|
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(`
|
|
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
|
}
|
package/docs/installation.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Resto Data CLI 与报表问数 Agent 安装说明
|
|
2
2
|
|
|
3
|
-
以下步骤面向 AI Agent
|
|
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
|
-
|
|
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. 输入
|
|
107
|
-
- 测试环境:`https://
|
|
108
|
-
-
|
|
106
|
+
1. 输入 CLI 鉴权网关地址
|
|
107
|
+
- 测试环境:`https://cli.test.restosuite.ai`
|
|
108
|
+
- 线上中国区:以运维提供的 CLI 鉴权网关域名为准
|
|
109
109
|
2. 输入集团 ID,例如 `256` 或 `302`
|
|
110
110
|
3. 选择鉴权方式
|
|
111
|
+
- 手机号验证码
|
|
111
112
|
- 账号密码
|
|
112
|
-
|
|
113
|
-
4. 按提示输入账号、密码或 token
|
|
113
|
+
4. 手机号验证码登录会先发送验证码,再按提示输入验证码;账号密码登录按提示输入账号、密码
|
|
114
114
|
|
|
115
|
-
|
|
115
|
+
非交互式手机号验证码登录:
|
|
116
116
|
|
|
117
117
|
```shell
|
|
118
118
|
resto auth login \
|
|
119
|
-
--base-url https://
|
|
119
|
+
--base-url https://cli.test.restosuite.ai \
|
|
120
|
+
--plat cli \
|
|
120
121
|
--corporation-id <集团ID> \
|
|
121
|
-
--
|
|
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://
|
|
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` 是
|
|
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
|
-
一般是
|
|
269
|
+
一般是 CLI gateway token 过期、不是 `plat=cli` 生成的 token,或组织上下文失效。重新登录后再执行:
|
|
240
270
|
|
|
241
271
|
```shell
|
|
242
272
|
resto auth login --interactive
|