@bty/customer-service-cli 0.6.8 → 0.6.10

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/CHANGELOG.md CHANGED
@@ -1,7 +1,15 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.6.10 (2026-08-26)
4
4
 
5
+ - `context-search`:`query_content` 增加最少 2 个字符的本地校验,避免过短关键词触发服务端全量扫描。
6
+ - `channel-metric backfill`:对齐服务端权限收紧——`--workspace-id` 逐个鉴权,缺省不再采集所有 workspace,改为只采集当前登录 workspace;同时修复不传 `--workspace-id` 时 commander 解析崩溃的问题。
7
+
8
+ ## 0.6.9 (2026-08-24)
9
+
10
+ - 新增 `channel-metric` 命令组,查询千牛等渠道店铺客服的每日排行指标(`channel_metric_snapshot`):
11
+ - `channel-metric query --workspace-id <ws> --start <date> --end <date> [--agent <id>] [--platform <channel>]`:按 workspace + 日期区间(+ 可选 agent / 渠道)查询指标,按日期×渠道分组输出。
12
+ - `channel-metric backfill --channel <channel> --start <date> --end <date> [--workspace-id <ws>...]`:手动补数据,采集指定日期区间的渠道指标;`--workspace-id` 可重复传,缺省采集所有已开启配置的 workspace。
5
13
  - 新增 `media upload` 命令与 `product supplement` 子命令组,支持商品「补充知识」图片/视频维护,与前端上传行为一致:
6
14
  - `media upload --file <path> --type image|video [--path-prefix /ai/knowledge/supplement] [--dry-run]`:本地图片/视频先经 `/v1/upload_sts` 取 STS 凭证上传到公开 OSS(`resource_bucket`),返回公网 URL;已是 http(s) URL 直接拒绝(无需上传)。上传 key 与前端一致:`${prefix}/${timestamp}_${filename}`。图片上限 10MB、视频上限 200MB,超限/类型不符在发起上传请求前直接失败。
7
15
  - `product supplement list --agent <id> --product-id <id>`:读取并解析商品「补充知识」条目(容错解析数组 / JSON 字符串 / 多层编码)。
package/dist/bin.js CHANGED
@@ -1095,6 +1095,121 @@ function registerChangeConsumerCommand(program2) {
1095
1095
  });
1096
1096
  }
1097
1097
 
1098
+ // src/commands/channel-metric.ts
1099
+ import Table2 from "cli-table3";
1100
+
1101
+ // src/client/channel-metric-api.ts
1102
+ var CHANNEL_METRIC_PREFIX = "/v1/channel-metric";
1103
+ async function backfillChannelMetrics(opts) {
1104
+ const request = createRequest();
1105
+ const body = {
1106
+ channel: opts.channel,
1107
+ start_date: opts.startDate,
1108
+ end_date: opts.endDate
1109
+ };
1110
+ if (opts.workspaceIds && opts.workspaceIds.length > 0) {
1111
+ body.workspace_ids = opts.workspaceIds;
1112
+ }
1113
+ return request(getObservabilityUrl(), `${CHANNEL_METRIC_PREFIX}/backfill`, {
1114
+ method: "POST",
1115
+ body
1116
+ });
1117
+ }
1118
+ async function queryChannelMetrics(opts) {
1119
+ const request = createRequest();
1120
+ const query = {
1121
+ workspace_id: opts.workspaceId,
1122
+ start_date: opts.startDate,
1123
+ end_date: opts.endDate
1124
+ };
1125
+ if (opts.configId) query.config_id = opts.configId;
1126
+ if (opts.channel) query.channel = opts.channel;
1127
+ return request(getObservabilityUrl(), `${CHANNEL_METRIC_PREFIX}/metrics`, {
1128
+ method: "GET",
1129
+ query
1130
+ });
1131
+ }
1132
+
1133
+ // src/commands/channel-metric.ts
1134
+ function collect(value, previous) {
1135
+ return previous.concat([value]);
1136
+ }
1137
+ function renderGroupedTable(groups) {
1138
+ let total = 0;
1139
+ for (const group of groups) {
1140
+ process.stdout.write(`
1141
+ === ${group.stat_date} \xB7 ${group.channel}\uFF08${group.items.length} \u4E2A\u8D26\u53F7\uFF09===
1142
+ `);
1143
+ const first = group.items[0];
1144
+ const headers = (first?.metrics_json ?? []).map((field) => field.label);
1145
+ const table = new Table2({ head: ["account_id", ...headers] });
1146
+ for (const row of group.items) {
1147
+ table.push([
1148
+ String(row.account_id),
1149
+ ...row.metrics_json.map(
1150
+ (field) => field.value === void 0 || field.value === null ? "-" : String(field.value)
1151
+ )
1152
+ ]);
1153
+ }
1154
+ process.stdout.write(`${table.toString()}
1155
+ `);
1156
+ total += group.items.length;
1157
+ }
1158
+ process.stdout.write(`
1159
+ \u5171 ${total} \u6761\u8BB0\u5F55
1160
+ `);
1161
+ }
1162
+ function registerChannelMetricCommand(program2) {
1163
+ const channelMetric = program2.command("channel-metric").description("\u6E20\u9053\u76D1\u63A7\u6307\u6807\u67E5\u8BE2 \u2014\u2014 \u67E5\u8BE2\u5343\u725B\u7B49\u6E20\u9053\u5E97\u94FA\u5BA2\u670D\u7684\u6BCF\u65E5\u6392\u884C\u6307\u6807\uFF08channel_metric_snapshot\uFF09");
1164
+ channelMetric.command("query").description(
1165
+ "\u6309 workspace + \u65E5\u671F\u533A\u95F4\uFF08+ \u53EF\u9009 agent / \u6E20\u9053\uFF09\u67E5\u8BE2\u6307\u6807\uFF0C\u6309\u65E5\u671F\xD7\u6E20\u9053\u5206\u7EC4\u8F93\u51FA\u3002\u4E0D\u4F20 --agent \u8F93\u51FA\u8BE5 workspace \u4E0B\u5168\u90E8 agent\uFF0C\u4E0D\u4F20 --platform \u8F93\u51FA\u5168\u90E8\u6E20\u9053"
1166
+ ).requiredOption("--workspace-id <workspace_id>", "\u5DE5\u4F5C\u7A7A\u95F4 ID\uFF08\u5FC5\u4F20\uFF0C\u4E0E\u670D\u52A1\u7AEF\u5F52\u5C5E\u6821\u9A8C\u4E00\u81F4\uFF09").option("--agent <agent_id>", "Agent \u914D\u7F6E ID\uFF1B\u7F3A\u7701\u67E5\u8BE2\u8BE5 workspace \u4E0B\u5168\u90E8 agent").requiredOption("--start <date>", "\u8D77\u59CB\u65E5\u671F\uFF08YYYY-MM-DD\uFF09").requiredOption("--end <date>", "\u7ED3\u675F\u65E5\u671F\uFF08YYYY-MM-DD\uFF09").option("--platform <channel>", "\u6E20\u9053\uFF08platform\uFF09\uFF0C\u5982 qianniu\uFF1B\u7F3A\u7701\u67E5\u8BE2\u5168\u90E8\u6E20\u9053").action(async (opts) => {
1167
+ try {
1168
+ setRuntimeWorkspaceId(opts.workspaceId);
1169
+ const data = await queryChannelMetrics({
1170
+ workspaceId: opts.workspaceId,
1171
+ configId: opts.agent,
1172
+ startDate: opts.start,
1173
+ endDate: opts.end,
1174
+ channel: opts.platform
1175
+ });
1176
+ const groups = data?.groups ?? [];
1177
+ if (groups.length === 0) {
1178
+ process.stdout.write("(empty)\n");
1179
+ return;
1180
+ }
1181
+ renderGroupedTable(groups);
1182
+ } catch (err) {
1183
+ reportCaughtError(err);
1184
+ process.exit(toExitCode(err));
1185
+ }
1186
+ });
1187
+ channelMetric.command("backfill").description(
1188
+ "\u624B\u52A8\u8865\u6570\u636E\uFF1A\u91C7\u96C6\u6307\u5B9A\u65E5\u671F\u533A\u95F4\u7684\u6E20\u9053\u6307\u6807\u3002\u53EF\u6307\u5B9A workspace\uFF08\u53EF\u91CD\u590D\u4F20\uFF0C\u9010\u4E2A\u9274\u6743\uFF09\uFF1B\u4E0D\u4F20 --workspace-id = \u53EA\u91C7\u96C6\u5F53\u524D workspace\uFF08\u767B\u5F55\u6001\u4E0B\uFF0C\u670D\u52A1\u7AEF\u9650\u5236\uFF09"
1189
+ ).requiredOption("--channel <channel>", "\u6E20\u9053\uFF08platform\uFF09\uFF0C\u5982 qianniu").requiredOption("--start <date>", "\u8D77\u59CB\u65E5\u671F\uFF08YYYY-MM-DD\uFF09").requiredOption("--end <date>", "\u7ED3\u675F\u65E5\u671F\uFF08YYYY-MM-DD\uFF09").option(
1190
+ "--workspace-id <workspace_id>",
1191
+ "\u5DE5\u4F5C\u7A7A\u95F4 ID\uFF08\u53EF\u91CD\u590D\u4F20\uFF1B\u7F3A\u7701 = \u53EA\u91C7\u96C6\u5F53\u524D workspace\uFF09",
1192
+ collect,
1193
+ []
1194
+ // 默认空数组:commander 首次解析时 previous 为 undefined,无默认值会崩
1195
+ ).action(async (opts) => {
1196
+ try {
1197
+ const workspaceIds = opts.workspaceId?.length ? opts.workspaceId : void 0;
1198
+ setRuntimeWorkspaceId(workspaceIds?.[0]);
1199
+ const data = await backfillChannelMetrics({
1200
+ channel: opts.channel,
1201
+ startDate: opts.start,
1202
+ endDate: opts.end,
1203
+ workspaceIds
1204
+ });
1205
+ formatOutput({ success: true, data }, program2.opts().table);
1206
+ } catch (err) {
1207
+ reportCaughtError(err);
1208
+ process.exit(toExitCode(err));
1209
+ }
1210
+ });
1211
+ }
1212
+
1098
1213
  // src/client/chat-history-api.ts
1099
1214
  async function listChatHistory(opts) {
1100
1215
  const request = createRequest();
@@ -1250,6 +1365,11 @@ function parseSearchTime(value, fieldName) {
1250
1365
  }
1251
1366
  return parsed;
1252
1367
  }
1368
+ function validateContextSearchQuery(queryContent) {
1369
+ if (queryContent.trim().length < 2) {
1370
+ throw new Error("query_content \u81F3\u5C11\u9700\u8981 2 \u4E2A\u5B57\u7B26");
1371
+ }
1372
+ }
1253
1373
  function validateContextSearchRange(startTime, endTime) {
1254
1374
  if (!startTime || !endTime) return;
1255
1375
  const start = parseSearchTime(startTime, "\u5F00\u59CB\u65F6\u95F4");
@@ -1288,6 +1408,7 @@ function shouldKeepTransferHumanConversation(item, onlyAbnormal) {
1288
1408
  });
1289
1409
  }
1290
1410
  async function contextSearchRecords(opts) {
1411
+ validateContextSearchQuery(opts.queryContent);
1291
1412
  validateContextSearchRange(opts.startTime, opts.endTime);
1292
1413
  const wsId = getWorkspaceId(opts.workspaceId);
1293
1414
  const request = createRequest();
@@ -1608,7 +1729,7 @@ async function writeBinaryToFile(filePath, buffer) {
1608
1729
  }
1609
1730
 
1610
1731
  // src/commands/dashboard.ts
1611
- function collect(value, previous) {
1732
+ function collect2(value, previous) {
1612
1733
  return previous.concat([value]);
1613
1734
  }
1614
1735
  function registerDashboardCommand(program2) {
@@ -1663,7 +1784,7 @@ function registerDashboardCommand(program2) {
1663
1784
  });
1664
1785
  dashboard.command("export").description(
1665
1786
  "\u5BFC\u51FA\u5E97\u94FA\u7EF4\u5EA6\u6BCF\u65E5\u660E\u7EC6\uFF08\u63A5\u5F85\u5BA2\u6237\u6570/AI\u72EC\u7ACB\u627F\u63A5/\u8F6C\u4EBA\u5DE5/AI\u56DE\u590D\u6D88\u606F\u6570\uFF09\u3002\u9ED8\u8BA4\u89E3\u6790\u4E3A JSON \u884C\uFF1B--out \u843D\u76D8\u539F\u59CB CSV\u3002\u53EF\u91CD\u590D --config-id \u9650\u5B9A\u5E97\u94FA"
1666
- ).requiredOption("--start <date>", "\u5F00\u59CB\u65E5\u671F (YYYY-MM-DD)\uFF0C\u5FC5\u586B").requiredOption("--end <date>", "\u7ED3\u675F\u65E5\u671F (YYYY-MM-DD)\uFF0C\u5FC5\u586B\uFF0C\u4E0D\u80FD\u665A\u4E8E\u6628\u5929").option("--config-id <id>", "\u5E97\u94FA\u914D\u7F6E ID\uFF08\u53EF\u91CD\u590D\u4F20\uFF1B\u4E0D\u4F20=\u5168\u90E8\u5E97\u94FA\uFF09", collect, []).option("--channel <channel>", "\u6E20\u9053\u7B5B\u9009: qianniu | jingdong | pinduoduo | douyin").option("--out <path>", "\u843D\u76D8\u539F\u59CB CSV \u8DEF\u5F84\uFF08\u7236\u76EE\u5F55\u81EA\u52A8 mkdir -p\uFF09").action(async (opts) => {
1787
+ ).requiredOption("--start <date>", "\u5F00\u59CB\u65E5\u671F (YYYY-MM-DD)\uFF0C\u5FC5\u586B").requiredOption("--end <date>", "\u7ED3\u675F\u65E5\u671F (YYYY-MM-DD)\uFF0C\u5FC5\u586B\uFF0C\u4E0D\u80FD\u665A\u4E8E\u6628\u5929").option("--config-id <id>", "\u5E97\u94FA\u914D\u7F6E ID\uFF08\u53EF\u91CD\u590D\u4F20\uFF1B\u4E0D\u4F20=\u5168\u90E8\u5E97\u94FA\uFF09", collect2, []).option("--channel <channel>", "\u6E20\u9053\u7B5B\u9009: qianniu | jingdong | pinduoduo | douyin").option("--out <path>", "\u843D\u76D8\u539F\u59CB CSV \u8DEF\u5F84\uFF08\u7236\u76EE\u5F55\u81EA\u52A8 mkdir -p\uFF09").action(async (opts) => {
1667
1788
  try {
1668
1789
  const result = await exportShopData({
1669
1790
  startDate: opts.start,
@@ -7470,6 +7591,7 @@ registerMediaCommand(program);
7470
7591
  registerKnowledgeCommand(program);
7471
7592
  registerActivityCommand(program);
7472
7593
  registerIssueCommand(program);
7594
+ registerChannelMetricCommand(program);
7473
7595
  registerChatHistoryCommand(program);
7474
7596
  registerFaqCommand(program);
7475
7597
  registerConversationCommand(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bty/customer-service-cli",
3
- "version": "0.6.8",
3
+ "version": "0.6.10",
4
4
  "description": "AI Customer Service CLI - Agent friendly",
5
5
  "type": "module",
6
6
  "main": "./dist/bin.js",