@bty/customer-service-cli 0.4.6 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +7 -54
  2. package/dist/bin.js +40 -642
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -316,6 +316,7 @@ Agent 关联的通用规则知识库(`suffix_type=_common`),与 FAQ / 商
316
316
  | `conversation list [--agent <id>] [--equipment-id <设备ID>] [--user <用户名>] [--start <日期>] [--end <日期>]` | 搜索会话 |
317
317
  | `conversation records <conversation_id> [--page-size N] [--direction prev|next]` | 获取会话聊天记录 |
318
318
  | `conversation context-search --query <文本> [--start <时间>] [--end <时间>]` | 通过上下文内容模糊搜索对话记录(返回匹配消息详情,时间范围最多 3 天) |
319
+ | `conversation transfer-human --agent <customer_agent_config_id> --start <时间> --end <时间> [--page N]` | 查询转人工会话及对应转人工消息记录(时间范围最多 7 天,分页大小固定 100) |
319
320
 
320
321
 
321
322
  ### Agent 调试 (`debug`)
@@ -472,60 +473,6 @@ cs-cli change-consumer delivery complete <delivery_id> --status completed \
472
473
  | --- | --- |
473
474
  | `monitor workspaces [--has-agent] [--page N] [--page-size N]` | 运营工作空间列表(含合同、客户信息) |
474
475
 
475
- ### 测试集 (`testset`)
476
-
477
- 测试集驱动 Agent 回归验证:测试集(testset)下挂多条用例(case),执行后产生批次(batch)。CLI 让上层 Agent / 工程师本地 debug 无需开浏览器即可走完「触发 → 拉结果 → 改用例 → 重跑」闭环。所有测试集相关命令收敛在 `testset` 主语下(ADR-001b)。
478
-
479
- #### 测试集 CRUD
480
-
481
- | 命令 | 说明 |
482
- | --- | --- |
483
- | `testset list --customer-agent-config-id <id>` | 列出测试集(分页,Agent ID 必填) |
484
- | `testset show <id>` | 测试集详情 |
485
- | `testset update <id> [--name ...] [--description ...] [--data @file.json]` | 更新测试集元数据(至少一项) |
486
- | `testset delete <id>` | 软删除测试集 |
487
- | `testset copy <id> [--name <副本名>]` | 克隆测试集(含用例,**不带历史批次**) |
488
- | `testset get-eval-prompt <id> [--raw]` | 查看评估 Prompt;`--raw` 输出裸文本到 stdout |
489
- | `testset set-eval-prompt <id> --prompt <s\|@file>` | 设置评估 Prompt(`@file` 读纯文本,不 JSON.parse) |
490
-
491
- #### 用例(`testset case`,全部必填 `--testset`)
492
-
493
- | 命令 | 说明 |
494
- | --- | --- |
495
- | `testset case list --testset <id>` | 列出测试集下用例(分页) |
496
- | `testset case show <case_id> --testset <id>` | 查看单条用例(内部翻页 + 本地 filter) |
497
- | `testset case create --testset <id> --data @file.json` | 新增用例(`case_content` 必填,至少 1 条对话) |
498
- | `testset case update <case_id> --testset <id> --data @file.json` | 更新用例 |
499
- | `testset case delete <case_id> --testset <id>` | 软删除用例 |
500
-
501
- #### 批次只读(`testset batch`)
502
-
503
- | 命令 | 说明 |
504
- | --- | --- |
505
- | `testset batch list [--testset <id>] [--status <s>]` | 列出批次(缺 `--testset` 时列全 workspace) |
506
- | `testset batch show <batch_id>` | 批次详情(含执行记录) |
507
- | `testset batch status <batch_id>` | 轻量批次状态(含 `is_terminal / total_count / pass_count / fail_count`) |
508
-
509
- #### 触发命令(`testset run / run-case / export`)
510
-
511
- | 命令 | 说明 |
512
- | --- | --- |
513
- | `testset run --testset <id> --agent <id> [--wait] [--timeout <sec>]` | 触发整批回归。默认异步立刻返回 `batch_id`;`--wait` 切同步阻塞 poll 到终态 |
514
- | `testset run-case --batch <id> --case <id>` | 单跑用例(覆盖写回该批次) |
515
- | `testset export --testset <id> --batch <id> --output <path>` | 导出批次为 xlsx 落盘 |
516
-
517
- **⚠️ `run --wait` 协议(详见 ADR-002)**:
518
-
519
- - Poll 间隔 3000ms,默认超时 600 秒;`--timeout <sec>` 覆盖(**单位是秒**,与全局 `--request-timeout` 的毫秒**严格区分**)
520
- - 终态判定锚定后端 `is_terminal` 字段(不枚举 `status` 字面量,避免后端新增字面量时 CLI 卡死)
521
- - **退出码中立**:批次 pass_count=0、status=failed、超时未完成全部 `exit 0`;只有 CLI 自身失败(鉴权 / 网络 / 参数错误 / poll 连续 3 次失败)才非 0
522
- - Poll 单次失败容错:连续 < 3 次抛错继续,3 次中断;中断时 `lastError` 是 `APIError(2/401)` → `exit 2`,否则 `exit 3`
523
- - 超时仍 `exit 0` + 输出 `{success:true, data:{batch_id, status:<最后状态>, is_terminal:false, timeout:true, elapsed_sec, ...}}`,调用方判 `is_terminal` 而非 `status` 字面量
524
-
525
- **长文本输入**:所有可能超过命令行长度的字段统一 `@文件路径` 前缀(沿用现有 `@file` 约定,详见 ADR-003)。例如 `case create --data @case.json` 走 JSON 解析;`testset set-eval-prompt --prompt @prompt.md` 走纯文本。
526
-
527
- **xlsx 二进制输出**:`export --output <path>` 必填路径,stdout 仅承载 JSON 报告 `{success:true, data:{path, bytes}}`,不支持 stdout pipe(避免与 JSON-by-default 冲突)。
528
-
529
476
  ## 输出格式
530
477
 
531
478
  默认输出 JSON:
@@ -647,6 +594,12 @@ cs-cli product add --agent <id> --data @products.json --source batch --no-identi
647
594
  # 通过上下文内容模糊搜索对话记录(自动使用当前工作空间,查询窗口最多 3 天)
648
595
  cs-cli conversation context-search --query "退款问题" --start 2026-03-22T00:00:00 --end 2026-03-22T23:59:59
649
596
 
597
+ # 查询转人工会话及对应转人工消息记录(按 MANUAL_TAKEOVER_REQUIRED 事件时间过滤,查询窗口最多 7 天)
598
+ cs-cli conversation transfer-human --agent <customer_agent_config_id> --start 2026-05-24T00:00:00 --end 2026-05-24T23:59:59
599
+
600
+ # 查询第 2 页转人工会话
601
+ cs-cli conversation transfer-human --agent <customer_agent_config_id> --start 2026-05-24T00:00:00 --end 2026-05-24T23:59:59 --page 2
602
+
650
603
  # 复现某条回复(自动获取上下文、原始用户名、Agent 配置)
651
604
  cs-cli debug reproduce <record_id>
652
605
 
package/dist/bin.js CHANGED
@@ -276,7 +276,7 @@ function toExitCode(err) {
276
276
  return 1;
277
277
  }
278
278
  function createRequest(globalTimeout) {
279
- return async function request(baseUrl, path5, options) {
279
+ return async function request(baseUrl, path4, options) {
280
280
  const headers = {
281
281
  "Content-Type": "application/json",
282
282
  ...options.headers
@@ -304,7 +304,7 @@ function createRequest(globalTimeout) {
304
304
  if (workspaceId) {
305
305
  headers["workspace-id"] = workspaceId;
306
306
  }
307
- let url = `${baseUrl}${path5}`;
307
+ let url = `${baseUrl}${path4}`;
308
308
  if (options.query) {
309
309
  const params = new URLSearchParams();
310
310
  for (const [key, value] of Object.entries(options.query)) {
@@ -1004,6 +1004,7 @@ function registerConfigCommand(program2) {
1004
1004
 
1005
1005
  // src/client/conversation-api.ts
1006
1006
  var MAX_CONTEXT_SEARCH_RANGE_MS = 3 * 24 * 60 * 60 * 1e3;
1007
+ var MAX_TRANSFER_HUMAN_RANGE_MS = 7 * 24 * 60 * 60 * 1e3;
1007
1008
  function parseSearchTime(value, fieldName) {
1008
1009
  const parsed = new Date(value);
1009
1010
  if (Number.isNaN(parsed.getTime())) {
@@ -1022,6 +1023,16 @@ function validateContextSearchRange(startTime, endTime) {
1022
1023
  throw new Error("\u67E5\u8BE2\u65F6\u95F4\u8303\u56F4\u4E0D\u80FD\u8D85\u8FC73\u5929");
1023
1024
  }
1024
1025
  }
1026
+ function validateTransferHumanRange(startTime, endTime) {
1027
+ const start = parseSearchTime(startTime, "\u5F00\u59CB\u65F6\u95F4");
1028
+ const end = parseSearchTime(endTime, "\u7ED3\u675F\u65F6\u95F4");
1029
+ if (end.getTime() < start.getTime()) {
1030
+ throw new Error("\u7ED3\u675F\u65F6\u95F4\u4E0D\u80FD\u65E9\u4E8E\u5F00\u59CB\u65F6\u95F4");
1031
+ }
1032
+ if (end.getTime() - start.getTime() > MAX_TRANSFER_HUMAN_RANGE_MS) {
1033
+ throw new Error("\u67E5\u8BE2\u65F6\u95F4\u8303\u56F4\u4E0D\u80FD\u8D85\u8FC77\u5929");
1034
+ }
1035
+ }
1025
1036
  async function contextSearchRecords(opts) {
1026
1037
  validateContextSearchRange(opts.startTime, opts.endTime);
1027
1038
  const wsId = getWorkspaceId(opts.workspaceId);
@@ -1063,6 +1074,19 @@ async function listConversations(opts) {
1063
1074
  }
1064
1075
  });
1065
1076
  }
1077
+ async function listTransferHumanConversations(opts) {
1078
+ validateTransferHumanRange(opts.startDate, opts.endDate);
1079
+ const request = createRequest();
1080
+ return request(getCustomerServiceUrl(), "/v1/chat/conversations/transfer-human", {
1081
+ method: "GET",
1082
+ query: {
1083
+ customer_agent_config_id: opts.customerAgentConfigId,
1084
+ start_date: opts.startDate,
1085
+ end_date: opts.endDate,
1086
+ page: opts.page ?? 1
1087
+ }
1088
+ });
1089
+ }
1066
1090
 
1067
1091
  // src/commands/conversation.ts
1068
1092
  function registerConversationCommand(program2) {
@@ -1117,6 +1141,20 @@ function registerConversationCommand(program2) {
1117
1141
  process.exit(toExitCode(err));
1118
1142
  }
1119
1143
  });
1144
+ conversation.command("transfer-human").description("\u67E5\u8BE2\u8F6C\u4EBA\u5DE5\u4F1A\u8BDD\u53CA\u5BF9\u5E94\u8F6C\u4EBA\u5DE5\u6D88\u606F\u8BB0\u5F55").requiredOption("--agent <config_id>", "Agent \u914D\u7F6E ID\uFF08customer_agent_config_id\uFF09").requiredOption("--start <date>", "\u5F00\u59CB\u65F6\u95F4\uFF08ISO \u683C\u5F0F\uFF0C\u5982 2026-05-20T00:00:00\uFF09").requiredOption("--end <date>", "\u7ED3\u675F\u65F6\u95F4\uFF08ISO \u683C\u5F0F\uFF0C\u5982 2026-05-20T23:59:59\uFF09").option("--page <number>", "\u9875\u7801", "1").action(async (opts) => {
1145
+ try {
1146
+ const data = await listTransferHumanConversations({
1147
+ customerAgentConfigId: opts.agent,
1148
+ startDate: opts.start,
1149
+ endDate: opts.end,
1150
+ page: Number(opts.page)
1151
+ });
1152
+ formatOutput({ success: true, data }, program2.opts().table);
1153
+ } catch (err) {
1154
+ reportCaughtError(err);
1155
+ process.exit(toExitCode(err));
1156
+ }
1157
+ });
1120
1158
  }
1121
1159
 
1122
1160
  // src/client/debug-api.ts
@@ -3409,645 +3447,6 @@ function registerSACommand(program2) {
3409
3447
  });
3410
3448
  }
3411
3449
 
3412
- // src/commands/testset.ts
3413
- import fs7 from "fs";
3414
-
3415
- // src/utils/file-output.ts
3416
- import fs6 from "fs";
3417
- import path4 from "path";
3418
- async function writeBinaryToFile(filePath, buffer) {
3419
- try {
3420
- const dir = path4.dirname(filePath);
3421
- fs6.mkdirSync(dir, { recursive: true });
3422
- fs6.writeFileSync(filePath, buffer);
3423
- } catch (err) {
3424
- const msg = err instanceof Error ? err.message : String(err);
3425
- throw new APIError(1, `\u6587\u4EF6\u5199\u5165\u5931\u8D25: ${msg}`);
3426
- }
3427
- return { path: filePath, bytes: buffer.length };
3428
- }
3429
-
3430
- // src/client/testset-api.ts
3431
- function unwrapPaginated(raw, fallbackPageSize) {
3432
- return {
3433
- items: Array.isArray(raw?.data) ? raw?.data : [],
3434
- total: typeof raw?.total === "number" ? raw.total : 0,
3435
- page: typeof raw?.page_no === "number" ? raw.page_no : 1,
3436
- pageSize: typeof raw?.page_size === "number" ? raw.page_size : fallbackPageSize
3437
- };
3438
- }
3439
- async function listTestSets(query) {
3440
- const request = createRequest();
3441
- const raw = await request(
3442
- getCustomerServiceUrl(),
3443
- "/v1/test_sets",
3444
- {
3445
- method: "GET",
3446
- query: {
3447
- customer_agent_config_id: query.customerAgentConfigId,
3448
- keyword: query.keyword,
3449
- page: query.page,
3450
- page_size: query.pageSize
3451
- }
3452
- }
3453
- );
3454
- return unwrapPaginated(raw, query.pageSize ?? 20);
3455
- }
3456
- async function getTestSet(id) {
3457
- const request = createRequest();
3458
- return request(getCustomerServiceUrl(), `/v1/test_sets/${id}`, {
3459
- method: "GET"
3460
- });
3461
- }
3462
- async function updateTestSet(id, body) {
3463
- const request = createRequest();
3464
- return request(getCustomerServiceUrl(), `/v1/test_sets/${id}`, {
3465
- method: "PUT",
3466
- body
3467
- });
3468
- }
3469
- async function deleteTestSet(id) {
3470
- const request = createRequest();
3471
- return request(getCustomerServiceUrl(), `/v1/test_sets/${id}`, {
3472
- method: "DELETE"
3473
- });
3474
- }
3475
- async function duplicateTestSet(id, body) {
3476
- const request = createRequest();
3477
- return request(getCustomerServiceUrl(), `/v1/test_sets/${id}/duplicate`, {
3478
- method: "POST",
3479
- body
3480
- });
3481
- }
3482
- async function getEvalPrompt(id) {
3483
- const request = createRequest();
3484
- return request(getCustomerServiceUrl(), `/v1/test_sets/${id}/evaluate-prompt`, {
3485
- method: "GET"
3486
- });
3487
- }
3488
- async function setEvalPrompt(id, body) {
3489
- const request = createRequest();
3490
- return request(getCustomerServiceUrl(), `/v1/test_sets/${id}/evaluate-prompt`, {
3491
- method: "PUT",
3492
- body
3493
- });
3494
- }
3495
- async function listCases(testSetId, query = {}) {
3496
- const request = createRequest();
3497
- const raw = await request(
3498
- getCustomerServiceUrl(),
3499
- `/v1/test_sets/${testSetId}/cases`,
3500
- {
3501
- method: "GET",
3502
- query: {
3503
- page: query.page,
3504
- page_size: query.pageSize
3505
- }
3506
- }
3507
- );
3508
- return unwrapPaginated(raw, query.pageSize ?? 20);
3509
- }
3510
- async function createCase(testSetId, body) {
3511
- const request = createRequest();
3512
- return request(getCustomerServiceUrl(), `/v1/test_sets/${testSetId}/cases`, {
3513
- method: "POST",
3514
- body
3515
- });
3516
- }
3517
- async function updateCase(testSetId, caseId, body) {
3518
- const request = createRequest();
3519
- return request(getCustomerServiceUrl(), `/v1/test_sets/${testSetId}/cases/${caseId}`, {
3520
- method: "PUT",
3521
- body
3522
- });
3523
- }
3524
- async function deleteCase(testSetId, caseId) {
3525
- const request = createRequest();
3526
- return request(getCustomerServiceUrl(), `/v1/test_sets/${testSetId}/cases/${caseId}`, {
3527
- method: "DELETE"
3528
- });
3529
- }
3530
- async function listBatches(query) {
3531
- const request = createRequest();
3532
- const raw = await request(
3533
- getCustomerServiceUrl(),
3534
- "/v1/test_sets/execution_batches",
3535
- {
3536
- method: "GET",
3537
- query: {
3538
- test_set_id: query.testSetId,
3539
- customer_agent_config_id: query.customerAgentConfigId,
3540
- status: query.status,
3541
- keyword: query.keyword,
3542
- page: query.page,
3543
- page_size: query.pageSize
3544
- }
3545
- }
3546
- );
3547
- return unwrapPaginated(raw, query.pageSize ?? 20);
3548
- }
3549
- async function getBatch(batchId) {
3550
- const request = createRequest();
3551
- return request(getCustomerServiceUrl(), `/v1/test_sets/execution_batches/${batchId}`, {
3552
- method: "GET"
3553
- });
3554
- }
3555
- async function getBatchStatus(batchId) {
3556
- const request = createRequest();
3557
- return request(
3558
- getCustomerServiceUrl(),
3559
- `/v1/test_sets/execution_batches/${batchId}/status`,
3560
- {
3561
- method: "GET"
3562
- }
3563
- );
3564
- }
3565
- async function executeBatch(testSetId, customerAgentConfigId) {
3566
- const request = createRequest();
3567
- return request(getCustomerServiceUrl(), "/v1/test_sets/execute", {
3568
- method: "POST",
3569
- body: {
3570
- test_set_id: testSetId,
3571
- customer_agent_config_id: customerAgentConfigId
3572
- }
3573
- });
3574
- }
3575
- async function rerunCase(batchId, caseId) {
3576
- const request = createRequest();
3577
- return request(
3578
- getCustomerServiceUrl(),
3579
- `/v1/test_sets/execution_batches/${batchId}/cases/${caseId}/rerun`,
3580
- {
3581
- method: "POST"
3582
- }
3583
- );
3584
- }
3585
- function buildAuthAndWorkspaceHeaders() {
3586
- const creds = readCredentials();
3587
- if (!creds) {
3588
- throw new APIError(2, "\u672A\u767B\u5F55\uFF0C\u8BF7\u8FD0\u884C: cs-cli auth login");
3589
- }
3590
- if (isTokenExpired(creds.expiresAt)) {
3591
- clearCredentials();
3592
- throw new APIError(2, "Token \u5DF2\u8FC7\u671F\uFF0C\u8BF7\u8FD0\u884C: cs-cli auth login");
3593
- }
3594
- const envLock = readEnvLockState();
3595
- const config = readConfig();
3596
- let workspaceId;
3597
- if (envLock.workspaceId) {
3598
- assertNoWorkspaceOverride(getRuntimeWorkspaceId());
3599
- workspaceId = envLock.workspaceId;
3600
- } else {
3601
- workspaceId = getRuntimeWorkspaceId() ?? config?.defaultWorkspaceId;
3602
- }
3603
- if (!workspaceId) {
3604
- throw new APIError(1, "\u672A\u8BBE\u7F6E\u5DE5\u4F5C\u7A7A\u95F4\uFF0C\u8BF7\u8FD0\u884C: cs-cli config set-workspace <id>");
3605
- }
3606
- return {
3607
- Authorization: `Bearer ${creds.accessToken}`,
3608
- "workspace-id": workspaceId
3609
- };
3610
- }
3611
- async function exportBatchToFile(testSetId, batchId, outputPath) {
3612
- const headers = buildAuthAndWorkspaceHeaders();
3613
- const baseUrl = getCustomerServiceUrl();
3614
- const timeoutMs = getRuntimeRequestTimeoutMs() ?? 6e4;
3615
- const url = `${baseUrl}/v1/test_sets/${testSetId}/export?batch_id=${encodeURIComponent(batchId)}`;
3616
- const response = await fetch(url, {
3617
- method: "GET",
3618
- headers,
3619
- signal: AbortSignal.timeout(timeoutMs)
3620
- });
3621
- if (!response.ok) {
3622
- if (response.status === 401) clearCredentials();
3623
- throw new APIError(response.status, `\u5BFC\u51FA\u5931\u8D25 HTTP ${response.status}`);
3624
- }
3625
- const contentType = response.headers.get("content-type") ?? "";
3626
- if (!contentType.toLowerCase().includes("spreadsheetml")) {
3627
- throw new APIError(1, `\u5BFC\u51FA\u54CD\u5E94 Content-Type \u975E xlsx: ${contentType || "<\u7A7A>"}`);
3628
- }
3629
- const arrayBuf = await response.arrayBuffer();
3630
- const buf = Buffer.from(arrayBuf);
3631
- await writeBinaryToFile(outputPath, buf);
3632
- return { path: outputPath, bytes: buf.byteLength };
3633
- }
3634
-
3635
- // src/commands/batch.ts
3636
- function registerBatchCommand(rootProgram, parent = rootProgram) {
3637
- const batch = parent.command("batch").description(
3638
- "\u6267\u884C\u6279\u6B21\uFF08ExecutionBatch\uFF09\u7BA1\u7406 \u2014\u2014 \u6D4B\u8BD5\u96C6\u56DE\u5F52\u6267\u884C\u540E\u7684\u6279\u6B21\u5217\u8868 / \u8BE6\u60C5 / \u72B6\u6001\u67E5\u8BE2\u3002"
3639
- );
3640
- batch.command("list").description("\u5217\u51FA\u6267\u884C\u6279\u6B21\uFF08--testset \u53EF\u9009\uFF1B\u7F3A\u7701\u5217\u5F53\u524D workspace \u5168\u90E8\u6279\u6B21\uFF09").option("--testset <id>", "\u6309\u6D4B\u8BD5\u96C6\u7B5B\u9009").option("--customer-agent-config-id <id>", "\u6309 Agent \u914D\u7F6E\u7B5B\u9009").option("--status <status>", "\u6309\u72B6\u6001\u7B5B\u9009\uFF08\u5982 running / finished / failed\uFF09").option("--keyword <text>", "\u6309\u5173\u952E\u8BCD\u6A21\u7CCA\u641C\u7D22").option("--page <number>", "\u9875\u7801", "1").option("--page-size <number>", "\u6BCF\u9875\u6570\u91CF", "20").action(async (opts) => {
3641
- try {
3642
- const result = await listBatches({
3643
- testSetId: opts.testset,
3644
- customerAgentConfigId: opts.customerAgentConfigId,
3645
- status: opts.status,
3646
- keyword: opts.keyword,
3647
- page: Number(opts.page),
3648
- pageSize: Number(opts.pageSize)
3649
- });
3650
- formatOutput(
3651
- {
3652
- success: true,
3653
- data: result.items,
3654
- pagination: {
3655
- page: result.page,
3656
- pageSize: result.pageSize,
3657
- total: result.total
3658
- }
3659
- },
3660
- rootProgram.opts().table
3661
- );
3662
- } catch (err) {
3663
- reportCaughtError(err);
3664
- process.exit(toExitCode(err));
3665
- }
3666
- });
3667
- batch.command("show").description("\u67E5\u770B\u6279\u6B21\u8BE6\u60C5\uFF08summary\uFF09").argument("<batch_id>", "\u6279\u6B21 ID").action(async (batchId) => {
3668
- try {
3669
- const data = await getBatch(batchId);
3670
- formatOutput({ success: true, data }, rootProgram.opts().table);
3671
- } catch (err) {
3672
- reportCaughtError(err);
3673
- process.exit(toExitCode(err));
3674
- }
3675
- });
3676
- batch.command("status").description("\u67E5\u770B\u6279\u6B21\u5B9E\u65F6\u72B6\u6001\uFF08\u542B is_terminal \u5B57\u6BB5\uFF09").argument("<batch_id>", "\u6279\u6B21 ID").action(async (batchId) => {
3677
- try {
3678
- const data = await getBatchStatus(batchId);
3679
- formatOutput({ success: true, data }, rootProgram.opts().table);
3680
- } catch (err) {
3681
- reportCaughtError(err);
3682
- process.exit(toExitCode(err));
3683
- }
3684
- });
3685
- }
3686
-
3687
- // src/commands/case.ts
3688
- var SHOW_PAGE_SIZE = 200;
3689
- var SHOW_MAX_PAGES = 1e3;
3690
- function registerCaseCommand(rootProgram, parent = rootProgram) {
3691
- const caseCmd = parent.command("case").description(
3692
- "\u6D4B\u8BD5\u7528\u4F8B\uFF08TestCase\uFF09\u7BA1\u7406 \u2014\u2014 \u5355\u6761 case \u7684 CRUD\uFF0C\u6240\u6709\u5B50\u547D\u4EE4\u9700\u8981 --testset \u951A\u5B9A\u5F52\u5C5E\u6D4B\u8BD5\u96C6\u3002"
3693
- );
3694
- caseCmd.command("list").description("\u5217\u51FA\u6307\u5B9A\u6D4B\u8BD5\u96C6\u4E0B\u7684\u7528\u4F8B").requiredOption("--testset <id>", "\u6D4B\u8BD5\u96C6 ID\uFF08\u5FC5\u586B\uFF09").option("--page <number>", "\u9875\u7801", "1").option("--page-size <number>", "\u6BCF\u9875\u6570\u91CF", "20").action(async (opts) => {
3695
- try {
3696
- const result = await listCases(opts.testset, {
3697
- page: Number(opts.page),
3698
- pageSize: Number(opts.pageSize)
3699
- });
3700
- formatOutput(
3701
- {
3702
- success: true,
3703
- data: result.items,
3704
- pagination: {
3705
- page: result.page,
3706
- pageSize: result.pageSize,
3707
- total: result.total
3708
- }
3709
- },
3710
- rootProgram.opts().table
3711
- );
3712
- } catch (err) {
3713
- reportCaughtError(err);
3714
- process.exit(toExitCode(err));
3715
- }
3716
- });
3717
- caseCmd.command("show").description("\u67E5\u770B\u5355\u6761\u7528\u4F8B\u8BE6\u60C5\uFF08\u5185\u90E8\u7528 list+filter \u5B9E\u73B0\uFF0C\u81EA\u52A8\u7FFB\u9875\uFF09").argument("<case_id>", "\u7528\u4F8B ID").requiredOption("--testset <id>", "\u6D4B\u8BD5\u96C6 ID\uFF08\u5FC5\u586B\uFF09").action(async (caseId, opts) => {
3718
- try {
3719
- let page = 1;
3720
- let accumulated = 0;
3721
- while (page <= SHOW_MAX_PAGES) {
3722
- const result = await listCases(opts.testset, {
3723
- page,
3724
- pageSize: SHOW_PAGE_SIZE
3725
- });
3726
- const items = result.items ?? [];
3727
- const hit = items.find((it) => it?.case_id === caseId);
3728
- if (hit) {
3729
- formatOutput({ success: true, data: hit }, rootProgram.opts().table);
3730
- return;
3731
- }
3732
- accumulated += items.length;
3733
- const isLastPage = items.length === 0 || accumulated >= result.total;
3734
- if (isLastPage) break;
3735
- page += 1;
3736
- }
3737
- outputError(1, `\u7528\u4F8B ${caseId} \u4E0D\u5B58\u5728`);
3738
- process.exit(1);
3739
- } catch (err) {
3740
- reportCaughtError(err);
3741
- process.exit(toExitCode(err));
3742
- }
3743
- });
3744
- caseCmd.command("create").description("\u65B0\u5EFA\u7528\u4F8B").requiredOption("--testset <id>", "\u6D4B\u8BD5\u96C6 ID\uFF08\u5FC5\u586B\uFF09").requiredOption("--data <json|@file>", "\u7528\u4F8B JSON body \u6216 @file \u8DEF\u5F84\uFF08\u5FC5\u586B\uFF09").action(async (opts) => {
3745
- try {
3746
- const body = parseDataOption(opts.data);
3747
- const data = await createCase(opts.testset, body);
3748
- formatOutput({ success: true, data }, rootProgram.opts().table);
3749
- } catch (err) {
3750
- reportCaughtError(err);
3751
- process.exit(toExitCode(err));
3752
- }
3753
- });
3754
- caseCmd.command("update").description("\u66F4\u65B0\u7528\u4F8B").argument("<case_id>", "\u7528\u4F8B ID").requiredOption("--testset <id>", "\u6D4B\u8BD5\u96C6 ID\uFF08\u5FC5\u586B\uFF09").requiredOption("--data <json|@file>", "\u66F4\u65B0 JSON body \u6216 @file \u8DEF\u5F84\uFF08\u5FC5\u586B\uFF09").action(async (caseId, opts) => {
3755
- try {
3756
- const body = parseDataOption(opts.data);
3757
- const data = await updateCase(opts.testset, caseId, body);
3758
- formatOutput({ success: true, data }, rootProgram.opts().table);
3759
- } catch (err) {
3760
- reportCaughtError(err);
3761
- process.exit(toExitCode(err));
3762
- }
3763
- });
3764
- caseCmd.command("delete").description("\u5220\u9664\u7528\u4F8B\uFF08\u4E0D\u5F39\u4E8C\u6B21\u786E\u8BA4\uFF09").argument("<case_id>", "\u7528\u4F8B ID").requiredOption("--testset <id>", "\u6D4B\u8BD5\u96C6 ID\uFF08\u5FC5\u586B\uFF09").action(async (caseId, opts) => {
3765
- try {
3766
- const data = await deleteCase(opts.testset, caseId);
3767
- formatOutput({ success: true, data }, rootProgram.opts().table);
3768
- } catch (err) {
3769
- reportCaughtError(err);
3770
- process.exit(toExitCode(err));
3771
- }
3772
- });
3773
- }
3774
-
3775
- // src/commands/export.ts
3776
- function registerExportCommand(rootProgram, parent = rootProgram) {
3777
- parent.command("export").description(
3778
- "\u5BFC\u51FA\u6279\u6B21\u7ED3\u679C\u4E3A xlsx \u6587\u4EF6\uFF08GET /test_sets/{id}/export?batch_id=<id>\uFF0C\u843D\u76D8\u5230 --output\uFF09"
3779
- ).requiredOption("--testset <id>", "\u6D4B\u8BD5\u96C6 ID\uFF08\u5FC5\u586B\uFF09").requiredOption("--batch <id>", "\u6279\u6B21 ID\uFF08\u5FC5\u586B\uFF09").requiredOption("--output <path>", "\u843D\u76D8\u6587\u4EF6\u8DEF\u5F84\uFF08\u5FC5\u586B\uFF0C\u7236\u76EE\u5F55\u81EA\u52A8 mkdir -p\uFF09").action(async (opts) => {
3780
- try {
3781
- const result = await exportBatchToFile(opts.testset, opts.batch, opts.output);
3782
- formatOutput({ success: true, data: result }, rootProgram.opts().table);
3783
- } catch (err) {
3784
- reportCaughtError(err);
3785
- process.exit(toExitCode(err));
3786
- }
3787
- });
3788
- }
3789
-
3790
- // src/commands/run-case.ts
3791
- function registerRunCaseCommand(rootProgram, parent = rootProgram) {
3792
- parent.command("run-case").description(
3793
- "\u91CD\u8DD1\u6307\u5B9A\u6279\u6B21\u5185\u7684\u5355\u6761\u7528\u4F8B\uFF08POST /test_sets/execution_batches/{B}/cases/{C}/rerun\uFF09"
3794
- ).requiredOption("--batch <id>", "\u6279\u6B21 ID\uFF08\u5FC5\u586B\uFF09").requiredOption("--case <id>", "\u7528\u4F8B ID\uFF08\u5FC5\u586B\uFF09").action(async (opts) => {
3795
- try {
3796
- const data = await rerunCase(opts.batch, opts.case);
3797
- formatOutput({ success: true, data }, rootProgram.opts().table);
3798
- } catch (err) {
3799
- reportCaughtError(err);
3800
- process.exit(toExitCode(err));
3801
- }
3802
- });
3803
- }
3804
-
3805
- // src/utils/poll.ts
3806
- var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3807
- async function pollUntilDone(opts) {
3808
- const {
3809
- intervalMs,
3810
- timeoutSec,
3811
- fetch: fetchFn,
3812
- isDone,
3813
- maxConsecutiveFailures = 3,
3814
- now = Date.now,
3815
- sleep = defaultSleep
3816
- } = opts;
3817
- const start = now();
3818
- let lastResult;
3819
- let lastError;
3820
- let consecutiveFailures = 0;
3821
- while (true) {
3822
- const elapsedMs = now() - start;
3823
- if (elapsedMs >= timeoutSec * 1e3) {
3824
- return {
3825
- done: false,
3826
- timedOut: true,
3827
- lastResult,
3828
- elapsedSec: Math.floor(elapsedMs / 1e3)
3829
- };
3830
- }
3831
- try {
3832
- const result = await fetchFn();
3833
- lastResult = result;
3834
- consecutiveFailures = 0;
3835
- if (isDone(result)) {
3836
- return { done: true, result };
3837
- }
3838
- } catch (err) {
3839
- lastError = err;
3840
- consecutiveFailures += 1;
3841
- if (consecutiveFailures >= maxConsecutiveFailures) {
3842
- return { done: false, failed: true, lastError };
3843
- }
3844
- }
3845
- const afterFetchElapsedMs = now() - start;
3846
- if (afterFetchElapsedMs >= timeoutSec * 1e3) {
3847
- return {
3848
- done: false,
3849
- timedOut: true,
3850
- lastResult,
3851
- elapsedSec: Math.floor(afterFetchElapsedMs / 1e3)
3852
- };
3853
- }
3854
- await sleep(intervalMs);
3855
- }
3856
- }
3857
-
3858
- // src/commands/run.ts
3859
- var DEFAULT_POLL_INTERVAL_MS = 3e3;
3860
- var DEFAULT_TIMEOUT_SEC = 600;
3861
- function extractBatchId(resp) {
3862
- if (resp && typeof resp === "object" && "batch_id" in resp) {
3863
- const v = resp.batch_id;
3864
- if (typeof v === "string") return v;
3865
- }
3866
- return "";
3867
- }
3868
- function registerRunCommand(rootProgram, parent = rootProgram) {
3869
- parent.command("run").description("\u89E6\u53D1\u6D4B\u8BD5\u96C6\u56DE\u5F52\u6267\u884C\uFF08\u9ED8\u8BA4\u5F02\u6B65\uFF1B--wait \u5207\u540C\u6B65\u8F6E\u8BE2\uFF0C\u8D85\u65F6\u4ECD exit 0 / ADR-002\uFF09").requiredOption("--testset <id>", "\u6D4B\u8BD5\u96C6 ID\uFF08\u5FC5\u586B\uFF09").requiredOption("--agent <id>", "Agent \u914D\u7F6E ID\uFF08\u5FC5\u586B\uFF0C\u5BF9\u5E94\u540E\u7AEF customer_agent_config_id\uFF09").option("--wait", "\u540C\u6B65\u7B49\u5F85\u7EC8\u6001\uFF08poll /status\uFF0C3s \u95F4\u9694\uFF0C--timeout \u63A7\u5236\u4E0A\u9650\uFF0C\u5355\u4F4D\uFF1A\u79D2\uFF09", false).option(
3870
- "--timeout <sec>",
3871
- "\u540C\u6B65\u7B49\u5F85\u4E0A\u9650\uFF08\u5355\u4F4D\uFF1A\u79D2\uFF1B\u4E0E\u5168\u5C40 --request-timeout \u7684\u6BEB\u79D2\u4E0D\u540C\uFF09",
3872
- String(DEFAULT_TIMEOUT_SEC)
3873
- ).action(async (opts) => {
3874
- let exitCode = 0;
3875
- try {
3876
- const triggerResp = await executeBatch(opts.testset, opts.agent);
3877
- const batchId = extractBatchId(triggerResp);
3878
- if (!opts.wait) {
3879
- formatOutput({ success: true, data: { batch_id: batchId } }, rootProgram.opts().table);
3880
- } else {
3881
- const timeoutSec = Number(opts.timeout) || DEFAULT_TIMEOUT_SEC;
3882
- const startedAt = Date.now();
3883
- const pollResult = await pollUntilDone({
3884
- intervalMs: DEFAULT_POLL_INTERVAL_MS,
3885
- timeoutSec,
3886
- fetch: () => getBatchStatus(batchId),
3887
- isDone: (r) => r?.is_terminal === true
3888
- });
3889
- if (pollResult.done) {
3890
- const elapsedSec = Math.floor((Date.now() - startedAt) / 1e3);
3891
- formatOutput(
3892
- {
3893
- success: true,
3894
- data: {
3895
- ...pollResult.result,
3896
- elapsed_sec: elapsedSec
3897
- }
3898
- },
3899
- rootProgram.opts().table
3900
- );
3901
- } else if ("timedOut" in pollResult && pollResult.timedOut) {
3902
- const last = pollResult.lastResult ?? {};
3903
- formatOutput(
3904
- {
3905
- success: true,
3906
- data: {
3907
- batch_id: batchId,
3908
- status: last.status,
3909
- is_terminal: false,
3910
- timeout: true,
3911
- elapsed_sec: pollResult.elapsedSec,
3912
- total_count: last.total_count,
3913
- pass_count: last.pass_count,
3914
- fail_count: last.fail_count
3915
- }
3916
- },
3917
- rootProgram.opts().table
3918
- );
3919
- } else if ("failed" in pollResult && pollResult.failed) {
3920
- reportCaughtError(pollResult.lastError);
3921
- const mapped = toExitCode(pollResult.lastError);
3922
- exitCode = mapped === 1 ? 3 : mapped;
3923
- }
3924
- }
3925
- } catch (err) {
3926
- reportCaughtError(err);
3927
- exitCode = toExitCode(err);
3928
- }
3929
- process.exit(exitCode);
3930
- });
3931
- }
3932
-
3933
- // src/commands/testset.ts
3934
- function readPromptInput(value) {
3935
- if (value.startsWith("@")) {
3936
- const filePath = value.slice(1);
3937
- if (!filePath) {
3938
- throw new Error("File path cannot be empty after @");
3939
- }
3940
- return fs7.readFileSync(filePath, "utf-8");
3941
- }
3942
- return value;
3943
- }
3944
- function registerTestsetCommand(program2) {
3945
- const testset = program2.command("testset").description(
3946
- "\u6D4B\u8BD5\u96C6\uFF08TestSet\uFF09\u7BA1\u7406 \u2014\u2014 \u5BF9\u8BDD\u56DE\u5F52\u6D4B\u8BD5\u7528\u4F8B\u96C6\u5408\uFF0C\u914D\u5408 batch / run / export \u5F62\u6210\u95ED\u73AF\u3002"
3947
- );
3948
- testset.command("list").description("\u5217\u51FA\u6D4B\u8BD5\u96C6").requiredOption("--customer-agent-config-id <id>", "Agent \u914D\u7F6E ID\uFF08\u5FC5\u586B\uFF0C\u4E0E\u540E\u7AEF\u5951\u7EA6\u5BF9\u9F50\uFF09").option("--keyword <text>", "\u6309\u540D\u79F0\u5173\u952E\u8BCD\u6A21\u7CCA\u641C\u7D22").option("--page <number>", "\u9875\u7801", "1").option("--page-size <number>", "\u6BCF\u9875\u6570\u91CF", "20").action(async (opts) => {
3949
- try {
3950
- const result = await listTestSets({
3951
- customerAgentConfigId: opts.customerAgentConfigId,
3952
- keyword: opts.keyword,
3953
- page: Number(opts.page),
3954
- pageSize: Number(opts.pageSize)
3955
- });
3956
- formatOutput(
3957
- {
3958
- success: true,
3959
- data: result.items,
3960
- pagination: {
3961
- page: result.page,
3962
- pageSize: result.pageSize,
3963
- total: result.total
3964
- }
3965
- },
3966
- program2.opts().table
3967
- );
3968
- } catch (err) {
3969
- reportCaughtError(err);
3970
- process.exit(toExitCode(err));
3971
- }
3972
- });
3973
- testset.command("show").description("\u67E5\u770B\u5355\u4E2A\u6D4B\u8BD5\u96C6\u8BE6\u60C5").argument("<id>", "\u6D4B\u8BD5\u96C6 ID").action(async (id) => {
3974
- try {
3975
- const data = await getTestSet(id);
3976
- formatOutput({ success: true, data }, program2.opts().table);
3977
- } catch (err) {
3978
- reportCaughtError(err);
3979
- process.exit(toExitCode(err));
3980
- }
3981
- });
3982
- testset.command("update").description("\u66F4\u65B0\u6D4B\u8BD5\u96C6\uFF08\u81F3\u5C11\u63D0\u4F9B --name / --description / --data \u4E4B\u4E00\uFF1B--data \u4F18\u5148\u5408\u5E76\uFF09").argument("<id>", "\u6D4B\u8BD5\u96C6 ID").option("--name <text>", "\u6D4B\u8BD5\u96C6\u540D\u79F0").option("--description <text>", "\u63CF\u8FF0").option("--data <json|@file>", "\u5B8C\u6574 JSON body\uFF08\u8986\u76D6 --name / --description\uFF09").action(async (id, opts) => {
3983
- try {
3984
- let body = {};
3985
- if (opts.name !== void 0) body.test_set_name = opts.name;
3986
- if (opts.description !== void 0) body.description = opts.description;
3987
- if (opts.data !== void 0) {
3988
- const parsed = parseDataOption(opts.data);
3989
- body = { ...body, ...parsed };
3990
- }
3991
- if (Object.keys(body).length === 0) {
3992
- outputError(1, "\u81F3\u5C11\u63D0\u4F9B --name/--description/--data \u4E4B\u4E00");
3993
- process.exit(1);
3994
- }
3995
- const data = await updateTestSet(id, body);
3996
- formatOutput({ success: true, data }, program2.opts().table);
3997
- } catch (err) {
3998
- reportCaughtError(err);
3999
- process.exit(toExitCode(err));
4000
- }
4001
- });
4002
- testset.command("delete").description("\u5220\u9664\u6D4B\u8BD5\u96C6\uFF08\u76F4\u63A5\u6267\u884C\uFF0C\u4E0D\u5F39\u4E8C\u6B21\u786E\u8BA4\uFF09").argument("<id>", "\u6D4B\u8BD5\u96C6 ID").action(async (id) => {
4003
- try {
4004
- const data = await deleteTestSet(id);
4005
- formatOutput({ success: true, data }, program2.opts().table);
4006
- } catch (err) {
4007
- reportCaughtError(err);
4008
- process.exit(toExitCode(err));
4009
- }
4010
- });
4011
- testset.command("copy").description("\u590D\u5236\u6D4B\u8BD5\u96C6").argument("<id>", "\u6E90\u6D4B\u8BD5\u96C6 ID").option("--name <text>", "\u526F\u672C\u540D\u79F0").action(async (id, opts) => {
4012
- try {
4013
- const data = await duplicateTestSet(id, { test_set_name: opts.name });
4014
- formatOutput({ success: true, data }, program2.opts().table);
4015
- } catch (err) {
4016
- reportCaughtError(err);
4017
- process.exit(toExitCode(err));
4018
- }
4019
- });
4020
- testset.command("get-eval-prompt").description("\u67E5\u770B\u6D4B\u8BD5\u96C6\u7684\u8BC4\u4EF7\u63D0\u793A\u8BCD").argument("<id>", "\u6D4B\u8BD5\u96C6 ID").option("--raw", "\u88F8\u6587\u672C\u8F93\u51FA\u5230 stdout\uFF08\u4E0D\u5E26 JSON \u5305\u88C5\uFF09", false).action(async (id, opts) => {
4021
- try {
4022
- const data = await getEvalPrompt(id);
4023
- if (opts.raw) {
4024
- const text = data?.prompt_template ?? "";
4025
- process.stdout.write(text);
4026
- return;
4027
- }
4028
- formatOutput({ success: true, data }, program2.opts().table);
4029
- } catch (err) {
4030
- reportCaughtError(err);
4031
- process.exit(toExitCode(err));
4032
- }
4033
- });
4034
- testset.command("set-eval-prompt").description("\u8BBE\u7F6E\u6D4B\u8BD5\u96C6\u7684\u8BC4\u4EF7\u63D0\u793A\u8BCD\uFF08\u652F\u6301 @file \u8BFB\u7EAF\u6587\u672C\uFF09").argument("<id>", "\u6D4B\u8BD5\u96C6 ID").requiredOption("--prompt <text|@file>", "\u63D0\u793A\u8BCD\u5B57\u9762\u91CF\u6216 @file \u8DEF\u5F84").action(async (id, opts) => {
4035
- try {
4036
- const prompt = readPromptInput(opts.prompt);
4037
- const data = await setEvalPrompt(id, { prompt });
4038
- formatOutput({ success: true, data }, program2.opts().table);
4039
- } catch (err) {
4040
- reportCaughtError(err);
4041
- process.exit(toExitCode(err));
4042
- }
4043
- });
4044
- registerCaseCommand(program2, testset);
4045
- registerBatchCommand(program2, testset);
4046
- registerRunCommand(program2, testset);
4047
- registerRunCaseCommand(program2, testset);
4048
- registerExportCommand(program2, testset);
4049
- }
4050
-
4051
3450
  // src/commands/workspace.ts
4052
3451
  var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
4053
3452
  function toISODate(d) {
@@ -4134,7 +3533,6 @@ registerMonitorCommand(program);
4134
3533
  registerRepairRecordCommand(program);
4135
3534
  registerOperationsRecordCommand(program);
4136
3535
  registerChangeConsumerCommand(program);
4137
- registerTestsetCommand(program);
4138
3536
  process.on("uncaughtException", (err) => {
4139
3537
  outputError(3, err.message);
4140
3538
  process.exit(3);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bty/customer-service-cli",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "description": "AI Customer Service CLI - Agent friendly",
5
5
  "type": "module",
6
6
  "main": "./dist/bin.js",