@bty/customer-service-cli 0.4.7 → 0.4.8

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 +54 -0
  2. package/dist/bin.js +642 -2
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -473,6 +473,60 @@ cs-cli change-consumer delivery complete <delivery_id> --status completed \
473
473
  | --- | --- |
474
474
  | `monitor workspaces [--has-agent] [--page N] [--page-size N]` | 运营工作空间列表(含合同、客户信息) |
475
475
 
476
+ ### 测试集 (`testset`)
477
+
478
+ 测试集驱动 Agent 回归验证:测试集(testset)下挂多条用例(case),执行后产生批次(batch)。CLI 让上层 Agent / 工程师本地 debug 无需开浏览器即可走完「触发 → 拉结果 → 改用例 → 重跑」闭环。所有测试集相关命令收敛在 `testset` 主语下(ADR-001b)。
479
+
480
+ #### 测试集 CRUD
481
+
482
+ | 命令 | 说明 |
483
+ | --- | --- |
484
+ | `testset list --customer-agent-config-id <id>` | 列出测试集(分页,Agent ID 必填) |
485
+ | `testset show <id>` | 测试集详情 |
486
+ | `testset update <id> [--name ...] [--description ...] [--data @file.json]` | 更新测试集元数据(至少一项) |
487
+ | `testset delete <id>` | 软删除测试集 |
488
+ | `testset copy <id> [--name <副本名>]` | 克隆测试集(含用例,**不带历史批次**) |
489
+ | `testset get-eval-prompt <id> [--raw]` | 查看评估 Prompt;`--raw` 输出裸文本到 stdout |
490
+ | `testset set-eval-prompt <id> --prompt <s\|@file>` | 设置评估 Prompt(`@file` 读纯文本,不 JSON.parse) |
491
+
492
+ #### 用例(`testset case`,全部必填 `--testset`)
493
+
494
+ | 命令 | 说明 |
495
+ | --- | --- |
496
+ | `testset case list --testset <id>` | 列出测试集下用例(分页) |
497
+ | `testset case show <case_id> --testset <id>` | 查看单条用例(内部翻页 + 本地 filter) |
498
+ | `testset case create --testset <id> --data @file.json` | 新增用例(`case_content` 必填,至少 1 条对话) |
499
+ | `testset case update <case_id> --testset <id> --data @file.json` | 更新用例 |
500
+ | `testset case delete <case_id> --testset <id>` | 软删除用例 |
501
+
502
+ #### 批次只读(`testset batch`)
503
+
504
+ | 命令 | 说明 |
505
+ | --- | --- |
506
+ | `testset batch list [--testset <id>] [--status <s>]` | 列出批次(缺 `--testset` 时列全 workspace) |
507
+ | `testset batch show <batch_id>` | 批次详情(含执行记录) |
508
+ | `testset batch status <batch_id>` | 轻量批次状态(含 `is_terminal / total_count / pass_count / fail_count`) |
509
+
510
+ #### 触发命令(`testset run / run-case / export`)
511
+
512
+ | 命令 | 说明 |
513
+ | --- | --- |
514
+ | `testset run --testset <id> --agent <id> [--wait] [--timeout <sec>]` | 触发整批回归。默认异步立刻返回 `batch_id`;`--wait` 切同步阻塞 poll 到终态 |
515
+ | `testset run-case --batch <id> --case <id>` | 单跑用例(覆盖写回该批次) |
516
+ | `testset export --testset <id> --batch <id> --output <path>` | 导出批次为 xlsx 落盘 |
517
+
518
+ **⚠️ `run --wait` 协议(详见 ADR-002)**:
519
+
520
+ - Poll 间隔 3000ms,默认超时 600 秒;`--timeout <sec>` 覆盖(**单位是秒**,与全局 `--request-timeout` 的毫秒**严格区分**)
521
+ - 终态判定锚定后端 `is_terminal` 字段(不枚举 `status` 字面量,避免后端新增字面量时 CLI 卡死)
522
+ - **退出码中立**:批次 pass_count=0、status=failed、超时未完成全部 `exit 0`;只有 CLI 自身失败(鉴权 / 网络 / 参数错误 / poll 连续 3 次失败)才非 0
523
+ - Poll 单次失败容错:连续 < 3 次抛错继续,3 次中断;中断时 `lastError` 是 `APIError(2/401)` → `exit 2`,否则 `exit 3`
524
+ - 超时仍 `exit 0` + 输出 `{success:true, data:{batch_id, status:<最后状态>, is_terminal:false, timeout:true, elapsed_sec, ...}}`,调用方判 `is_terminal` 而非 `status` 字面量
525
+
526
+ **长文本输入**:所有可能超过命令行长度的字段统一 `@文件路径` 前缀(沿用现有 `@file` 约定,详见 ADR-003)。例如 `case create --data @case.json` 走 JSON 解析;`testset set-eval-prompt --prompt @prompt.md` 走纯文本。
527
+
528
+ **xlsx 二进制输出**:`export --output <path>` 必填路径,stdout 仅承载 JSON 报告 `{success:true, data:{path, bytes}}`,不支持 stdout pipe(避免与 JSON-by-default 冲突)。
529
+
476
530
  ## 输出格式
477
531
 
478
532
  默认输出 JSON:
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, path4, options) {
279
+ return async function request(baseUrl, path5, 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}${path4}`;
307
+ let url = `${baseUrl}${path5}`;
308
308
  if (options.query) {
309
309
  const params = new URLSearchParams();
310
310
  for (const [key, value] of Object.entries(options.query)) {
@@ -3447,6 +3447,645 @@ function registerSACommand(program2) {
3447
3447
  });
3448
3448
  }
3449
3449
 
3450
+ // src/commands/testset.ts
3451
+ import fs7 from "fs";
3452
+
3453
+ // src/utils/file-output.ts
3454
+ import fs6 from "fs";
3455
+ import path4 from "path";
3456
+ async function writeBinaryToFile(filePath, buffer) {
3457
+ try {
3458
+ const dir = path4.dirname(filePath);
3459
+ fs6.mkdirSync(dir, { recursive: true });
3460
+ fs6.writeFileSync(filePath, buffer);
3461
+ } catch (err) {
3462
+ const msg = err instanceof Error ? err.message : String(err);
3463
+ throw new APIError(1, `\u6587\u4EF6\u5199\u5165\u5931\u8D25: ${msg}`);
3464
+ }
3465
+ return { path: filePath, bytes: buffer.length };
3466
+ }
3467
+
3468
+ // src/client/testset-api.ts
3469
+ function unwrapPaginated(raw, fallbackPageSize) {
3470
+ return {
3471
+ items: Array.isArray(raw?.data) ? raw?.data : [],
3472
+ total: typeof raw?.total === "number" ? raw.total : 0,
3473
+ page: typeof raw?.page_no === "number" ? raw.page_no : 1,
3474
+ pageSize: typeof raw?.page_size === "number" ? raw.page_size : fallbackPageSize
3475
+ };
3476
+ }
3477
+ async function listTestSets(query) {
3478
+ const request = createRequest();
3479
+ const raw = await request(
3480
+ getCustomerServiceUrl(),
3481
+ "/v1/test_sets",
3482
+ {
3483
+ method: "GET",
3484
+ query: {
3485
+ customer_agent_config_id: query.customerAgentConfigId,
3486
+ keyword: query.keyword,
3487
+ page: query.page,
3488
+ page_size: query.pageSize
3489
+ }
3490
+ }
3491
+ );
3492
+ return unwrapPaginated(raw, query.pageSize ?? 20);
3493
+ }
3494
+ async function getTestSet(id) {
3495
+ const request = createRequest();
3496
+ return request(getCustomerServiceUrl(), `/v1/test_sets/${id}`, {
3497
+ method: "GET"
3498
+ });
3499
+ }
3500
+ async function updateTestSet(id, body) {
3501
+ const request = createRequest();
3502
+ return request(getCustomerServiceUrl(), `/v1/test_sets/${id}`, {
3503
+ method: "PUT",
3504
+ body
3505
+ });
3506
+ }
3507
+ async function deleteTestSet(id) {
3508
+ const request = createRequest();
3509
+ return request(getCustomerServiceUrl(), `/v1/test_sets/${id}`, {
3510
+ method: "DELETE"
3511
+ });
3512
+ }
3513
+ async function duplicateTestSet(id, body) {
3514
+ const request = createRequest();
3515
+ return request(getCustomerServiceUrl(), `/v1/test_sets/${id}/duplicate`, {
3516
+ method: "POST",
3517
+ body
3518
+ });
3519
+ }
3520
+ async function getEvalPrompt(id) {
3521
+ const request = createRequest();
3522
+ return request(getCustomerServiceUrl(), `/v1/test_sets/${id}/evaluate-prompt`, {
3523
+ method: "GET"
3524
+ });
3525
+ }
3526
+ async function setEvalPrompt(id, body) {
3527
+ const request = createRequest();
3528
+ return request(getCustomerServiceUrl(), `/v1/test_sets/${id}/evaluate-prompt`, {
3529
+ method: "PUT",
3530
+ body
3531
+ });
3532
+ }
3533
+ async function listCases(testSetId, query = {}) {
3534
+ const request = createRequest();
3535
+ const raw = await request(
3536
+ getCustomerServiceUrl(),
3537
+ `/v1/test_sets/${testSetId}/cases`,
3538
+ {
3539
+ method: "GET",
3540
+ query: {
3541
+ page: query.page,
3542
+ page_size: query.pageSize
3543
+ }
3544
+ }
3545
+ );
3546
+ return unwrapPaginated(raw, query.pageSize ?? 20);
3547
+ }
3548
+ async function createCase(testSetId, body) {
3549
+ const request = createRequest();
3550
+ return request(getCustomerServiceUrl(), `/v1/test_sets/${testSetId}/cases`, {
3551
+ method: "POST",
3552
+ body
3553
+ });
3554
+ }
3555
+ async function updateCase(testSetId, caseId, body) {
3556
+ const request = createRequest();
3557
+ return request(getCustomerServiceUrl(), `/v1/test_sets/${testSetId}/cases/${caseId}`, {
3558
+ method: "PUT",
3559
+ body
3560
+ });
3561
+ }
3562
+ async function deleteCase(testSetId, caseId) {
3563
+ const request = createRequest();
3564
+ return request(getCustomerServiceUrl(), `/v1/test_sets/${testSetId}/cases/${caseId}`, {
3565
+ method: "DELETE"
3566
+ });
3567
+ }
3568
+ async function listBatches(query) {
3569
+ const request = createRequest();
3570
+ const raw = await request(
3571
+ getCustomerServiceUrl(),
3572
+ "/v1/test_sets/execution_batches",
3573
+ {
3574
+ method: "GET",
3575
+ query: {
3576
+ test_set_id: query.testSetId,
3577
+ customer_agent_config_id: query.customerAgentConfigId,
3578
+ status: query.status,
3579
+ keyword: query.keyword,
3580
+ page: query.page,
3581
+ page_size: query.pageSize
3582
+ }
3583
+ }
3584
+ );
3585
+ return unwrapPaginated(raw, query.pageSize ?? 20);
3586
+ }
3587
+ async function getBatch(batchId) {
3588
+ const request = createRequest();
3589
+ return request(getCustomerServiceUrl(), `/v1/test_sets/execution_batches/${batchId}`, {
3590
+ method: "GET"
3591
+ });
3592
+ }
3593
+ async function getBatchStatus(batchId) {
3594
+ const request = createRequest();
3595
+ return request(
3596
+ getCustomerServiceUrl(),
3597
+ `/v1/test_sets/execution_batches/${batchId}/status`,
3598
+ {
3599
+ method: "GET"
3600
+ }
3601
+ );
3602
+ }
3603
+ async function executeBatch(testSetId, customerAgentConfigId) {
3604
+ const request = createRequest();
3605
+ return request(getCustomerServiceUrl(), "/v1/test_sets/execute", {
3606
+ method: "POST",
3607
+ body: {
3608
+ test_set_id: testSetId,
3609
+ customer_agent_config_id: customerAgentConfigId
3610
+ }
3611
+ });
3612
+ }
3613
+ async function rerunCase(batchId, caseId) {
3614
+ const request = createRequest();
3615
+ return request(
3616
+ getCustomerServiceUrl(),
3617
+ `/v1/test_sets/execution_batches/${batchId}/cases/${caseId}/rerun`,
3618
+ {
3619
+ method: "POST"
3620
+ }
3621
+ );
3622
+ }
3623
+ function buildAuthAndWorkspaceHeaders() {
3624
+ const creds = readCredentials();
3625
+ if (!creds) {
3626
+ throw new APIError(2, "\u672A\u767B\u5F55\uFF0C\u8BF7\u8FD0\u884C: cs-cli auth login");
3627
+ }
3628
+ if (isTokenExpired(creds.expiresAt)) {
3629
+ clearCredentials();
3630
+ throw new APIError(2, "Token \u5DF2\u8FC7\u671F\uFF0C\u8BF7\u8FD0\u884C: cs-cli auth login");
3631
+ }
3632
+ const envLock = readEnvLockState();
3633
+ const config = readConfig();
3634
+ let workspaceId;
3635
+ if (envLock.workspaceId) {
3636
+ assertNoWorkspaceOverride(getRuntimeWorkspaceId());
3637
+ workspaceId = envLock.workspaceId;
3638
+ } else {
3639
+ workspaceId = getRuntimeWorkspaceId() ?? config?.defaultWorkspaceId;
3640
+ }
3641
+ if (!workspaceId) {
3642
+ throw new APIError(1, "\u672A\u8BBE\u7F6E\u5DE5\u4F5C\u7A7A\u95F4\uFF0C\u8BF7\u8FD0\u884C: cs-cli config set-workspace <id>");
3643
+ }
3644
+ return {
3645
+ Authorization: `Bearer ${creds.accessToken}`,
3646
+ "workspace-id": workspaceId
3647
+ };
3648
+ }
3649
+ async function exportBatchToFile(testSetId, batchId, outputPath) {
3650
+ const headers = buildAuthAndWorkspaceHeaders();
3651
+ const baseUrl = getCustomerServiceUrl();
3652
+ const timeoutMs = getRuntimeRequestTimeoutMs() ?? 6e4;
3653
+ const url = `${baseUrl}/v1/test_sets/${testSetId}/export?batch_id=${encodeURIComponent(batchId)}`;
3654
+ const response = await fetch(url, {
3655
+ method: "GET",
3656
+ headers,
3657
+ signal: AbortSignal.timeout(timeoutMs)
3658
+ });
3659
+ if (!response.ok) {
3660
+ if (response.status === 401) clearCredentials();
3661
+ throw new APIError(response.status, `\u5BFC\u51FA\u5931\u8D25 HTTP ${response.status}`);
3662
+ }
3663
+ const contentType = response.headers.get("content-type") ?? "";
3664
+ if (!contentType.toLowerCase().includes("spreadsheetml")) {
3665
+ throw new APIError(1, `\u5BFC\u51FA\u54CD\u5E94 Content-Type \u975E xlsx: ${contentType || "<\u7A7A>"}`);
3666
+ }
3667
+ const arrayBuf = await response.arrayBuffer();
3668
+ const buf = Buffer.from(arrayBuf);
3669
+ await writeBinaryToFile(outputPath, buf);
3670
+ return { path: outputPath, bytes: buf.byteLength };
3671
+ }
3672
+
3673
+ // src/commands/batch.ts
3674
+ function registerBatchCommand(rootProgram, parent = rootProgram) {
3675
+ const batch = parent.command("batch").description(
3676
+ "\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"
3677
+ );
3678
+ 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) => {
3679
+ try {
3680
+ const result = await listBatches({
3681
+ testSetId: opts.testset,
3682
+ customerAgentConfigId: opts.customerAgentConfigId,
3683
+ status: opts.status,
3684
+ keyword: opts.keyword,
3685
+ page: Number(opts.page),
3686
+ pageSize: Number(opts.pageSize)
3687
+ });
3688
+ formatOutput(
3689
+ {
3690
+ success: true,
3691
+ data: result.items,
3692
+ pagination: {
3693
+ page: result.page,
3694
+ pageSize: result.pageSize,
3695
+ total: result.total
3696
+ }
3697
+ },
3698
+ rootProgram.opts().table
3699
+ );
3700
+ } catch (err) {
3701
+ reportCaughtError(err);
3702
+ process.exit(toExitCode(err));
3703
+ }
3704
+ });
3705
+ batch.command("show").description("\u67E5\u770B\u6279\u6B21\u8BE6\u60C5\uFF08summary\uFF09").argument("<batch_id>", "\u6279\u6B21 ID").action(async (batchId) => {
3706
+ try {
3707
+ const data = await getBatch(batchId);
3708
+ formatOutput({ success: true, data }, rootProgram.opts().table);
3709
+ } catch (err) {
3710
+ reportCaughtError(err);
3711
+ process.exit(toExitCode(err));
3712
+ }
3713
+ });
3714
+ 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) => {
3715
+ try {
3716
+ const data = await getBatchStatus(batchId);
3717
+ formatOutput({ success: true, data }, rootProgram.opts().table);
3718
+ } catch (err) {
3719
+ reportCaughtError(err);
3720
+ process.exit(toExitCode(err));
3721
+ }
3722
+ });
3723
+ }
3724
+
3725
+ // src/commands/case.ts
3726
+ var SHOW_PAGE_SIZE = 200;
3727
+ var SHOW_MAX_PAGES = 1e3;
3728
+ function registerCaseCommand(rootProgram, parent = rootProgram) {
3729
+ const caseCmd = parent.command("case").description(
3730
+ "\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"
3731
+ );
3732
+ 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) => {
3733
+ try {
3734
+ const result = await listCases(opts.testset, {
3735
+ page: Number(opts.page),
3736
+ pageSize: Number(opts.pageSize)
3737
+ });
3738
+ formatOutput(
3739
+ {
3740
+ success: true,
3741
+ data: result.items,
3742
+ pagination: {
3743
+ page: result.page,
3744
+ pageSize: result.pageSize,
3745
+ total: result.total
3746
+ }
3747
+ },
3748
+ rootProgram.opts().table
3749
+ );
3750
+ } catch (err) {
3751
+ reportCaughtError(err);
3752
+ process.exit(toExitCode(err));
3753
+ }
3754
+ });
3755
+ 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) => {
3756
+ try {
3757
+ let page = 1;
3758
+ let accumulated = 0;
3759
+ while (page <= SHOW_MAX_PAGES) {
3760
+ const result = await listCases(opts.testset, {
3761
+ page,
3762
+ pageSize: SHOW_PAGE_SIZE
3763
+ });
3764
+ const items = result.items ?? [];
3765
+ const hit = items.find((it) => it?.case_id === caseId);
3766
+ if (hit) {
3767
+ formatOutput({ success: true, data: hit }, rootProgram.opts().table);
3768
+ return;
3769
+ }
3770
+ accumulated += items.length;
3771
+ const isLastPage = items.length === 0 || accumulated >= result.total;
3772
+ if (isLastPage) break;
3773
+ page += 1;
3774
+ }
3775
+ outputError(1, `\u7528\u4F8B ${caseId} \u4E0D\u5B58\u5728`);
3776
+ process.exit(1);
3777
+ } catch (err) {
3778
+ reportCaughtError(err);
3779
+ process.exit(toExitCode(err));
3780
+ }
3781
+ });
3782
+ 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) => {
3783
+ try {
3784
+ const body = parseDataOption(opts.data);
3785
+ const data = await createCase(opts.testset, body);
3786
+ formatOutput({ success: true, data }, rootProgram.opts().table);
3787
+ } catch (err) {
3788
+ reportCaughtError(err);
3789
+ process.exit(toExitCode(err));
3790
+ }
3791
+ });
3792
+ 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) => {
3793
+ try {
3794
+ const body = parseDataOption(opts.data);
3795
+ const data = await updateCase(opts.testset, caseId, body);
3796
+ formatOutput({ success: true, data }, rootProgram.opts().table);
3797
+ } catch (err) {
3798
+ reportCaughtError(err);
3799
+ process.exit(toExitCode(err));
3800
+ }
3801
+ });
3802
+ 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) => {
3803
+ try {
3804
+ const data = await deleteCase(opts.testset, caseId);
3805
+ formatOutput({ success: true, data }, rootProgram.opts().table);
3806
+ } catch (err) {
3807
+ reportCaughtError(err);
3808
+ process.exit(toExitCode(err));
3809
+ }
3810
+ });
3811
+ }
3812
+
3813
+ // src/commands/export.ts
3814
+ function registerExportCommand(rootProgram, parent = rootProgram) {
3815
+ parent.command("export").description(
3816
+ "\u5BFC\u51FA\u6279\u6B21\u7ED3\u679C\u4E3A xlsx \u6587\u4EF6\uFF08GET /test_sets/{id}/export?batch_id=<id>\uFF0C\u843D\u76D8\u5230 --output\uFF09"
3817
+ ).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) => {
3818
+ try {
3819
+ const result = await exportBatchToFile(opts.testset, opts.batch, opts.output);
3820
+ formatOutput({ success: true, data: result }, rootProgram.opts().table);
3821
+ } catch (err) {
3822
+ reportCaughtError(err);
3823
+ process.exit(toExitCode(err));
3824
+ }
3825
+ });
3826
+ }
3827
+
3828
+ // src/commands/run-case.ts
3829
+ function registerRunCaseCommand(rootProgram, parent = rootProgram) {
3830
+ parent.command("run-case").description(
3831
+ "\u91CD\u8DD1\u6307\u5B9A\u6279\u6B21\u5185\u7684\u5355\u6761\u7528\u4F8B\uFF08POST /test_sets/execution_batches/{B}/cases/{C}/rerun\uFF09"
3832
+ ).requiredOption("--batch <id>", "\u6279\u6B21 ID\uFF08\u5FC5\u586B\uFF09").requiredOption("--case <id>", "\u7528\u4F8B ID\uFF08\u5FC5\u586B\uFF09").action(async (opts) => {
3833
+ try {
3834
+ const data = await rerunCase(opts.batch, opts.case);
3835
+ formatOutput({ success: true, data }, rootProgram.opts().table);
3836
+ } catch (err) {
3837
+ reportCaughtError(err);
3838
+ process.exit(toExitCode(err));
3839
+ }
3840
+ });
3841
+ }
3842
+
3843
+ // src/utils/poll.ts
3844
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3845
+ async function pollUntilDone(opts) {
3846
+ const {
3847
+ intervalMs,
3848
+ timeoutSec,
3849
+ fetch: fetchFn,
3850
+ isDone,
3851
+ maxConsecutiveFailures = 3,
3852
+ now = Date.now,
3853
+ sleep = defaultSleep
3854
+ } = opts;
3855
+ const start = now();
3856
+ let lastResult;
3857
+ let lastError;
3858
+ let consecutiveFailures = 0;
3859
+ while (true) {
3860
+ const elapsedMs = now() - start;
3861
+ if (elapsedMs >= timeoutSec * 1e3) {
3862
+ return {
3863
+ done: false,
3864
+ timedOut: true,
3865
+ lastResult,
3866
+ elapsedSec: Math.floor(elapsedMs / 1e3)
3867
+ };
3868
+ }
3869
+ try {
3870
+ const result = await fetchFn();
3871
+ lastResult = result;
3872
+ consecutiveFailures = 0;
3873
+ if (isDone(result)) {
3874
+ return { done: true, result };
3875
+ }
3876
+ } catch (err) {
3877
+ lastError = err;
3878
+ consecutiveFailures += 1;
3879
+ if (consecutiveFailures >= maxConsecutiveFailures) {
3880
+ return { done: false, failed: true, lastError };
3881
+ }
3882
+ }
3883
+ const afterFetchElapsedMs = now() - start;
3884
+ if (afterFetchElapsedMs >= timeoutSec * 1e3) {
3885
+ return {
3886
+ done: false,
3887
+ timedOut: true,
3888
+ lastResult,
3889
+ elapsedSec: Math.floor(afterFetchElapsedMs / 1e3)
3890
+ };
3891
+ }
3892
+ await sleep(intervalMs);
3893
+ }
3894
+ }
3895
+
3896
+ // src/commands/run.ts
3897
+ var DEFAULT_POLL_INTERVAL_MS = 3e3;
3898
+ var DEFAULT_TIMEOUT_SEC = 600;
3899
+ function extractBatchId(resp) {
3900
+ if (resp && typeof resp === "object" && "batch_id" in resp) {
3901
+ const v = resp.batch_id;
3902
+ if (typeof v === "string") return v;
3903
+ }
3904
+ return "";
3905
+ }
3906
+ function registerRunCommand(rootProgram, parent = rootProgram) {
3907
+ 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(
3908
+ "--timeout <sec>",
3909
+ "\u540C\u6B65\u7B49\u5F85\u4E0A\u9650\uFF08\u5355\u4F4D\uFF1A\u79D2\uFF1B\u4E0E\u5168\u5C40 --request-timeout \u7684\u6BEB\u79D2\u4E0D\u540C\uFF09",
3910
+ String(DEFAULT_TIMEOUT_SEC)
3911
+ ).action(async (opts) => {
3912
+ let exitCode = 0;
3913
+ try {
3914
+ const triggerResp = await executeBatch(opts.testset, opts.agent);
3915
+ const batchId = extractBatchId(triggerResp);
3916
+ if (!opts.wait) {
3917
+ formatOutput({ success: true, data: { batch_id: batchId } }, rootProgram.opts().table);
3918
+ } else {
3919
+ const timeoutSec = Number(opts.timeout) || DEFAULT_TIMEOUT_SEC;
3920
+ const startedAt = Date.now();
3921
+ const pollResult = await pollUntilDone({
3922
+ intervalMs: DEFAULT_POLL_INTERVAL_MS,
3923
+ timeoutSec,
3924
+ fetch: () => getBatchStatus(batchId),
3925
+ isDone: (r) => r?.is_terminal === true
3926
+ });
3927
+ if (pollResult.done) {
3928
+ const elapsedSec = Math.floor((Date.now() - startedAt) / 1e3);
3929
+ formatOutput(
3930
+ {
3931
+ success: true,
3932
+ data: {
3933
+ ...pollResult.result,
3934
+ elapsed_sec: elapsedSec
3935
+ }
3936
+ },
3937
+ rootProgram.opts().table
3938
+ );
3939
+ } else if ("timedOut" in pollResult && pollResult.timedOut) {
3940
+ const last = pollResult.lastResult ?? {};
3941
+ formatOutput(
3942
+ {
3943
+ success: true,
3944
+ data: {
3945
+ batch_id: batchId,
3946
+ status: last.status,
3947
+ is_terminal: false,
3948
+ timeout: true,
3949
+ elapsed_sec: pollResult.elapsedSec,
3950
+ total_count: last.total_count,
3951
+ pass_count: last.pass_count,
3952
+ fail_count: last.fail_count
3953
+ }
3954
+ },
3955
+ rootProgram.opts().table
3956
+ );
3957
+ } else if ("failed" in pollResult && pollResult.failed) {
3958
+ reportCaughtError(pollResult.lastError);
3959
+ const mapped = toExitCode(pollResult.lastError);
3960
+ exitCode = mapped === 1 ? 3 : mapped;
3961
+ }
3962
+ }
3963
+ } catch (err) {
3964
+ reportCaughtError(err);
3965
+ exitCode = toExitCode(err);
3966
+ }
3967
+ process.exit(exitCode);
3968
+ });
3969
+ }
3970
+
3971
+ // src/commands/testset.ts
3972
+ function readPromptInput(value) {
3973
+ if (value.startsWith("@")) {
3974
+ const filePath = value.slice(1);
3975
+ if (!filePath) {
3976
+ throw new Error("File path cannot be empty after @");
3977
+ }
3978
+ return fs7.readFileSync(filePath, "utf-8");
3979
+ }
3980
+ return value;
3981
+ }
3982
+ function registerTestsetCommand(program2) {
3983
+ const testset = program2.command("testset").description(
3984
+ "\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"
3985
+ );
3986
+ 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) => {
3987
+ try {
3988
+ const result = await listTestSets({
3989
+ customerAgentConfigId: opts.customerAgentConfigId,
3990
+ keyword: opts.keyword,
3991
+ page: Number(opts.page),
3992
+ pageSize: Number(opts.pageSize)
3993
+ });
3994
+ formatOutput(
3995
+ {
3996
+ success: true,
3997
+ data: result.items,
3998
+ pagination: {
3999
+ page: result.page,
4000
+ pageSize: result.pageSize,
4001
+ total: result.total
4002
+ }
4003
+ },
4004
+ program2.opts().table
4005
+ );
4006
+ } catch (err) {
4007
+ reportCaughtError(err);
4008
+ process.exit(toExitCode(err));
4009
+ }
4010
+ });
4011
+ testset.command("show").description("\u67E5\u770B\u5355\u4E2A\u6D4B\u8BD5\u96C6\u8BE6\u60C5").argument("<id>", "\u6D4B\u8BD5\u96C6 ID").action(async (id) => {
4012
+ try {
4013
+ const data = await getTestSet(id);
4014
+ formatOutput({ success: true, data }, program2.opts().table);
4015
+ } catch (err) {
4016
+ reportCaughtError(err);
4017
+ process.exit(toExitCode(err));
4018
+ }
4019
+ });
4020
+ 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) => {
4021
+ try {
4022
+ let body = {};
4023
+ if (opts.name !== void 0) body.test_set_name = opts.name;
4024
+ if (opts.description !== void 0) body.description = opts.description;
4025
+ if (opts.data !== void 0) {
4026
+ const parsed = parseDataOption(opts.data);
4027
+ body = { ...body, ...parsed };
4028
+ }
4029
+ if (Object.keys(body).length === 0) {
4030
+ outputError(1, "\u81F3\u5C11\u63D0\u4F9B --name/--description/--data \u4E4B\u4E00");
4031
+ process.exit(1);
4032
+ }
4033
+ const data = await updateTestSet(id, body);
4034
+ formatOutput({ success: true, data }, program2.opts().table);
4035
+ } catch (err) {
4036
+ reportCaughtError(err);
4037
+ process.exit(toExitCode(err));
4038
+ }
4039
+ });
4040
+ 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) => {
4041
+ try {
4042
+ const data = await deleteTestSet(id);
4043
+ formatOutput({ success: true, data }, program2.opts().table);
4044
+ } catch (err) {
4045
+ reportCaughtError(err);
4046
+ process.exit(toExitCode(err));
4047
+ }
4048
+ });
4049
+ 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) => {
4050
+ try {
4051
+ const data = await duplicateTestSet(id, { test_set_name: opts.name });
4052
+ formatOutput({ success: true, data }, program2.opts().table);
4053
+ } catch (err) {
4054
+ reportCaughtError(err);
4055
+ process.exit(toExitCode(err));
4056
+ }
4057
+ });
4058
+ 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) => {
4059
+ try {
4060
+ const data = await getEvalPrompt(id);
4061
+ if (opts.raw) {
4062
+ const text = data?.prompt_template ?? "";
4063
+ process.stdout.write(text);
4064
+ return;
4065
+ }
4066
+ formatOutput({ success: true, data }, program2.opts().table);
4067
+ } catch (err) {
4068
+ reportCaughtError(err);
4069
+ process.exit(toExitCode(err));
4070
+ }
4071
+ });
4072
+ 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) => {
4073
+ try {
4074
+ const prompt = readPromptInput(opts.prompt);
4075
+ const data = await setEvalPrompt(id, { prompt });
4076
+ formatOutput({ success: true, data }, program2.opts().table);
4077
+ } catch (err) {
4078
+ reportCaughtError(err);
4079
+ process.exit(toExitCode(err));
4080
+ }
4081
+ });
4082
+ registerCaseCommand(program2, testset);
4083
+ registerBatchCommand(program2, testset);
4084
+ registerRunCommand(program2, testset);
4085
+ registerRunCaseCommand(program2, testset);
4086
+ registerExportCommand(program2, testset);
4087
+ }
4088
+
3450
4089
  // src/commands/workspace.ts
3451
4090
  var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
3452
4091
  function toISODate(d) {
@@ -3533,6 +4172,7 @@ registerMonitorCommand(program);
3533
4172
  registerRepairRecordCommand(program);
3534
4173
  registerOperationsRecordCommand(program);
3535
4174
  registerChangeConsumerCommand(program);
4175
+ registerTestsetCommand(program);
3536
4176
  process.on("uncaughtException", (err) => {
3537
4177
  outputError(3, err.message);
3538
4178
  process.exit(3);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bty/customer-service-cli",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "description": "AI Customer Service CLI - Agent friendly",
5
5
  "type": "module",
6
6
  "main": "./dist/bin.js",