@bty/customer-service-cli 0.4.4 → 0.4.6
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/README.md +67 -0
- package/dist/bin.js +719 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -222,12 +222,15 @@ cs-cli workspace points-consumes-daily --start 2026-04-01 --end 2026-04-23
|
|
|
222
222
|
| `product get --agent <id> --product-id <商品ID>` | 获取商品详情(自动获取 db_id) |
|
|
223
223
|
| `product update --agent <id> --product-id <商品ID> --update <json\|@file>` | 更新商品信息 |
|
|
224
224
|
| `product update-sku --agent <id> (--sku <SKU名称> \| --sku-id <id>) --update <json\|@file>` | 更新 SKU 信息(`--sku-id` 优先于 `--sku`) |
|
|
225
|
+
| `product add --agent <id> --data <json\|@file> [--source <type>] [--no-identify]` | 直接传入结构化商品数据(JSON 数组)异步学习(manual / identify 默认开启) |
|
|
225
226
|
| `product learn --agent <id> --url <URL...> [--source <type>] [--no-identify]` | 通过商品 URL 异步学习(manual / identify 默认开启) |
|
|
226
227
|
| `product sync-taobao --agent <id> [--skip-hash-check] [--sync-type <full\|incremental>]` | 触发淘宝店铺商品同步 |
|
|
227
228
|
| `product sync-taobao-item --agent <id> --product-id <商品ID>` | 触发单个淘宝商品同步 |
|
|
228
229
|
|
|
229
230
|
`product update` 支持更新的字段包括:`卖点`、`补充知识`、`tag`、`参数`、`轮播图识别结果`、`商品详情页识别结果` 等。`--update` 接受 JSON 字符串或 `@文件路径`。
|
|
230
231
|
|
|
232
|
+
`product add` 与 `product learn` 走同一个上传接口(`/v1/knowledge/products/upload`),区别在于载荷:`add` 传结构化商品数据 `product_items`(已抓好的商品详情 JSON 数组),`learn` 传 `product_urls`(让后端去抓)。`--data` 接受 JSON 字符串或 `@文件路径`;传单个对象会自动包成数组,传 JSON 基本类型(数字 / 字符串等)会直接报错;`--source` 默认 `manual`,`--no-identify` 关闭自动识别。
|
|
233
|
+
|
|
231
234
|
`product learn` 提交商品详情页 URL 进入异步学习队列(`/v1/knowledge/products/upload`),返回 `flow_id` 和 `knowledge_ids`。`--url` 可多值,用空格分隔;`--source` 默认 `manual`;传 `--no-identify` 可关闭自动识别。
|
|
232
235
|
|
|
233
236
|
`product sync-taobao` / `product sync-taobao-item` 调用 customer-servhub-api 的授权店铺同步接口,要求 `--agent` 已唯一绑定一个淘宝授权店铺。前者支持 `--sync-type full|incremental`,默认 `full`;后者按 `product_id` 补同步单个淘宝商品。
|
|
@@ -281,10 +284,13 @@ cs-cli product update-sku --agent <id> --sku-id 6072595054179 --update '{"补充
|
|
|
281
284
|
| 命令 | 说明 |
|
|
282
285
|
| ---------------------------------------------------------------------------- | --------- |
|
|
283
286
|
| `faq list --agent <id>` | 列出 FAQ 文件 |
|
|
287
|
+
| `faq content --agent <id> --file <文件名> [--keyword <词>] [--page-size <n>] [--next-group-id <id>]` | 列出文件内答案组(含 `answer_group_id` / 问题 / 答案) |
|
|
284
288
|
| `faq add --agent <id> --file <文件名> --questions <问题1,问题2> --answers <答案=xxx>` | 添加 FAQ 内容 |
|
|
285
289
|
| `faq update --agent <id> --file <文件名> --group-id <id> [--questions <...>] [--answers <...>] [--delete-chunks <id1,id2>]` | 更新已有 FAQ 答案组(`--questions` / `--answers` / `--delete-chunks` 至少一个) |
|
|
286
290
|
| `faq delete --agent <id> --file <文件名> --group-id <id>` | 删除 FAQ 答案组 |
|
|
287
291
|
|
|
292
|
+
`faq content` 走 `GET /mebsuta/api/v1/dataset/{knowledge_id}/faq_contents`,列出文件内的答案组及其 `answer_group_id`,是 `faq update` / `faq delete` 拿 `--group-id` 的来源(`faq list` 只给文件,文件内的 group-id 要靠这条命令查)。支持 `--keyword` 搜索、`--page-size` 分页、`--next-group-id` 游标续翻。
|
|
293
|
+
|
|
288
294
|
`faq update` 按 `answer_group_id` 定位目标组,POST 同一个 `faq_contents/save` 接口完成字段替换、问题替换与 chunk 级删除。`faq delete` 走 `DELETE /faq_contents/{group_id}` 整组删除。
|
|
289
295
|
|
|
290
296
|
### 扩展知识库 (`knowledge`)
|
|
@@ -466,6 +472,60 @@ cs-cli change-consumer delivery complete <delivery_id> --status completed \
|
|
|
466
472
|
| --- | --- |
|
|
467
473
|
| `monitor workspaces [--has-agent] [--page N] [--page-size N]` | 运营工作空间列表(含合同、客户信息) |
|
|
468
474
|
|
|
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
|
+
|
|
469
529
|
## 输出格式
|
|
470
530
|
|
|
471
531
|
默认输出 JSON:
|
|
@@ -541,6 +601,9 @@ cs-cli product update-sku --agent <id> --sku-id 6072595054179 --update '{"补充
|
|
|
541
601
|
# 添加 FAQ
|
|
542
602
|
cs-cli faq add --agent <id> --file "常见问题" --questions "怎么退款,如何退款" --answers "答案=请联系客服处理退款"
|
|
543
603
|
|
|
604
|
+
# 列出文件内答案组(拿 answer_group_id,供下面 update/delete 用)
|
|
605
|
+
cs-cli faq content --agent <id> --file "常见问题" --keyword 退款
|
|
606
|
+
|
|
544
607
|
# 更新已有 FAQ 组答案
|
|
545
608
|
cs-cli faq update --agent <id> --file "常见问题" --group-id 2 --answers "答案=请通过订单页面申请退款"
|
|
546
609
|
|
|
@@ -577,6 +640,10 @@ cs-cli product learn --agent <id> --url https://detail.tmall.com/item.htm?id=997
|
|
|
577
640
|
# 多个 URL 一次提交,且跳过自动识别
|
|
578
641
|
cs-cli product learn --agent <id> --url https://a.example/1 https://a.example/2 --no-identify
|
|
579
642
|
|
|
643
|
+
# 直接传入结构化商品数据(JSON 数组),或从文件读取
|
|
644
|
+
cs-cli product add --agent <id> --data '[{"title":"商品A","price":99}]'
|
|
645
|
+
cs-cli product add --agent <id> --data @products.json --source batch --no-identify
|
|
646
|
+
|
|
580
647
|
# 通过上下文内容模糊搜索对话记录(自动使用当前工作空间,查询窗口最多 3 天)
|
|
581
648
|
cs-cli conversation context-search --query "退款问题" --start 2026-03-22T00:00:00 --end 2026-03-22T23:59:59
|
|
582
649
|
|
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,
|
|
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}${
|
|
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)) {
|
|
@@ -1513,6 +1513,22 @@ async function saveFaqContent(opts) {
|
|
|
1513
1513
|
}
|
|
1514
1514
|
);
|
|
1515
1515
|
}
|
|
1516
|
+
async function listFaqContents(opts) {
|
|
1517
|
+
const request = createRequest();
|
|
1518
|
+
return request(
|
|
1519
|
+
getAiApiUrl(),
|
|
1520
|
+
`/mebsuta/api/v1/dataset/${opts.knowledgeId}/faq_contents`,
|
|
1521
|
+
{
|
|
1522
|
+
method: "GET",
|
|
1523
|
+
query: {
|
|
1524
|
+
page_size: opts.pageSize ?? 20,
|
|
1525
|
+
search_words: opts.searchWords,
|
|
1526
|
+
next_group_id: opts.nextGroupId,
|
|
1527
|
+
need_total: opts.needTotal ?? true
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1516
1532
|
async function deleteFaqGroup(opts) {
|
|
1517
1533
|
const request = createRequest();
|
|
1518
1534
|
return request(
|
|
@@ -1566,6 +1582,27 @@ function registerFaqCommand(program2) {
|
|
|
1566
1582
|
process.exit(toExitCode(err));
|
|
1567
1583
|
}
|
|
1568
1584
|
});
|
|
1585
|
+
faq.command("content").description(
|
|
1586
|
+
"\u5217\u51FA\u6307\u5B9A FAQ \u6587\u4EF6\u5185\u7684\u7B54\u6848\u7EC4\uFF08\u542B answer_group_id / \u95EE\u9898 / \u7B54\u6848\uFF09\u3002\u7528\u4E8E\u4E3A faq update / faq delete \u627E\u5230\u76EE\u6807 group-id"
|
|
1587
|
+
).requiredOption("--agent <config_id>", "Agent \u914D\u7F6E ID\uFF08\u4ECE agent list \u83B7\u53D6\uFF09").requiredOption("--file <name>", "FAQ \u6587\u4EF6\u540D\uFF08\u9700\u7CBE\u786E\u5339\u914D faq list \u8FD4\u56DE\u7684 file_name\uFF09").option("--keyword <text>", "\u6309\u95EE\u9898/\u7B54\u6848\u5173\u952E\u8BCD\u641C\u7D22").option("--page-size <number>", "\u6BCF\u9875\u6570\u91CF", "20").option("--next-group-id <id>", "\u6E38\u6807\uFF1A\u4ECE\u8BE5 answer_group_id \u4E4B\u540E\u7EE7\u7EED\u7FFB\u9875").action(async (opts) => {
|
|
1588
|
+
try {
|
|
1589
|
+
const matched = await findFaqFile(opts.agent, opts.file, 100);
|
|
1590
|
+
if (!matched) {
|
|
1591
|
+
outputError(1, `FAQ file not found: ${opts.file}`);
|
|
1592
|
+
process.exit(1);
|
|
1593
|
+
}
|
|
1594
|
+
const data = await listFaqContents({
|
|
1595
|
+
knowledgeId: matched.knowledge_id,
|
|
1596
|
+
pageSize: Number(opts.pageSize),
|
|
1597
|
+
searchWords: opts.keyword,
|
|
1598
|
+
nextGroupId: opts.nextGroupId ? Number(opts.nextGroupId) : void 0
|
|
1599
|
+
});
|
|
1600
|
+
formatOutput({ success: true, data }, program2.opts().table);
|
|
1601
|
+
} catch (err) {
|
|
1602
|
+
reportCaughtError(err);
|
|
1603
|
+
process.exit(toExitCode(err));
|
|
1604
|
+
}
|
|
1605
|
+
});
|
|
1569
1606
|
faq.command("add").description("\u5411\u6307\u5B9A FAQ \u6587\u4EF6\u6DFB\u52A0\u95EE\u7B54\u6761\u76EE\u3002\u5148\u7528 faq list \u83B7\u53D6\u6587\u4EF6\u540D").requiredOption("--agent <config_id>", "Agent \u914D\u7F6E ID\uFF08\u4ECE agent list \u83B7\u53D6\uFF09").requiredOption("--file <name>", "FAQ \u6587\u4EF6\u540D\uFF08\u9700\u7CBE\u786E\u5339\u914D faq list \u8FD4\u56DE\u7684 file_name\uFF09").requiredOption(
|
|
1570
1607
|
"--questions <items>",
|
|
1571
1608
|
'\u76F8\u4F3C\u95EE\u9898\u5217\u8868\uFF08\u9017\u53F7\u5206\u9694\uFF0C\u5982 "\u600E\u4E48\u9000\u6B3E,\u5982\u4F55\u9000\u6B3E,\u9000\u6B3E\u6D41\u7A0B"\uFF09'
|
|
@@ -2832,6 +2869,18 @@ async function uploadProductByUrl(opts) {
|
|
|
2832
2869
|
}
|
|
2833
2870
|
});
|
|
2834
2871
|
}
|
|
2872
|
+
async function uploadProductData(opts) {
|
|
2873
|
+
const request = createRequest();
|
|
2874
|
+
return request(getCustomerServiceUrl(), "/v1/knowledge/products/upload", {
|
|
2875
|
+
method: "POST",
|
|
2876
|
+
body: {
|
|
2877
|
+
customer_agent_config_id: opts.agentConfigId,
|
|
2878
|
+
source_type: opts.sourceType ?? "manual",
|
|
2879
|
+
identify: opts.identify ?? true,
|
|
2880
|
+
product_items: opts.productItems
|
|
2881
|
+
}
|
|
2882
|
+
});
|
|
2883
|
+
}
|
|
2835
2884
|
async function updateSku(opts) {
|
|
2836
2885
|
if (!opts.skuId && !opts.sku) {
|
|
2837
2886
|
throw new Error("updateSku \u9700\u8981\u63D0\u4F9B sku \u6216 sku_id \u5176\u4E2D\u4E4B\u4E00");
|
|
@@ -2977,6 +3026,34 @@ function registerProductCommand(program2) {
|
|
|
2977
3026
|
process.exit(toExitCode(err));
|
|
2978
3027
|
}
|
|
2979
3028
|
});
|
|
3029
|
+
product.command("add").description(
|
|
3030
|
+
"\u76F4\u63A5\u4F20\u5165\u7ED3\u6784\u5316\u5546\u54C1\u6570\u636E\uFF08JSON \u6570\u7EC4\uFF09\u8BA9 Agent \u5B66\u4E60\uFF0C\u5BF9\u63A5 POST /v1/knowledge/products/upload \u7684 product_items\u3002\u9ED8\u8BA4 source_type=manual / identify=true\u3002--no-identify \u53EF\u5173\u95ED\u81EA\u52A8\u8BC6\u522B"
|
|
3031
|
+
).requiredOption("--agent <config_id>", "Agent \u914D\u7F6E ID\uFF08\u4ECE agent list \u83B7\u53D6\uFF09").requiredOption(
|
|
3032
|
+
"--data <json>",
|
|
3033
|
+
'\u5546\u54C1\u6570\u636E JSON \u6570\u7EC4\u6216 @\u6587\u4EF6\u8DEF\u5F84\u3002\u5355\u4E2A\u5BF9\u8C61\u4F1A\u88AB\u5305\u6210\u6570\u7EC4\u3002\u793A\u4F8B: [{"title":"xxx",...}]'
|
|
3034
|
+
).option("--source <type>", "\u6765\u6E90\u7C7B\u578B\uFF08\u5982 manual / batch\uFF09", "manual").option("--no-identify", "\u5173\u95ED\u81EA\u52A8\u8BC6\u522B\u5546\u54C1\u4FE1\u606F\uFF08\u9ED8\u8BA4\u5F00\u542F\uFF09").action(async (opts) => {
|
|
3035
|
+
try {
|
|
3036
|
+
const parsed = parseDataOption(opts.data);
|
|
3037
|
+
let productItems;
|
|
3038
|
+
if (Array.isArray(parsed)) {
|
|
3039
|
+
productItems = parsed;
|
|
3040
|
+
} else if (parsed !== null && typeof parsed === "object") {
|
|
3041
|
+
productItems = [parsed];
|
|
3042
|
+
} else {
|
|
3043
|
+
throw new Error("\u5546\u54C1\u6570\u636E\u5FC5\u987B\u662F JSON \u5BF9\u8C61\u6216\u5BF9\u8C61\u6570\u7EC4");
|
|
3044
|
+
}
|
|
3045
|
+
const data = await uploadProductData({
|
|
3046
|
+
agentConfigId: opts.agent,
|
|
3047
|
+
productItems,
|
|
3048
|
+
identify: opts.identify,
|
|
3049
|
+
sourceType: opts.source
|
|
3050
|
+
});
|
|
3051
|
+
formatOutput({ success: true, data }, program2.opts().table);
|
|
3052
|
+
} catch (err) {
|
|
3053
|
+
reportCaughtError(err);
|
|
3054
|
+
process.exit(toExitCode(err));
|
|
3055
|
+
}
|
|
3056
|
+
});
|
|
2980
3057
|
product.command("sync-taobao").description(
|
|
2981
3058
|
"\u89E6\u53D1\u6DD8\u5B9D\u5E97\u94FA\u5546\u54C1\u540C\u6B65\u3002\u8C03\u7528 customer-servhub-api \u7684\u6388\u6743\u5E97\u94FA\u540C\u6B65\u63A5\u53E3\uFF0C\u6309 Agent \u914D\u7F6E\u627E\u5230\u552F\u4E00\u7ED1\u5B9A\u5E97\u94FA\u540E\u5F02\u6B65\u542F\u52A8\u540C\u6B65\u4EFB\u52A1"
|
|
2982
3059
|
).requiredOption("--agent <config_id>", "Agent \u914D\u7F6E ID\uFF08\u5FC5\u987B\u5DF2\u552F\u4E00\u7ED1\u5B9A\u4E00\u4E2A\u6DD8\u5B9D\u6388\u6743\u5E97\u94FA\uFF09").option("--skip-hash-check", "\u8DF3\u8FC7\u54C8\u5E0C\u6821\u9A8C\uFF0C\u5F3A\u5236\u91CD\u65B0\u540C\u6B65\u5546\u54C1").option("--sync-type <type>", "\u540C\u6B65\u7C7B\u578B\uFF1Afull / incremental\uFF0C\u9ED8\u8BA4 full", "full").action(async (opts) => {
|
|
@@ -3332,6 +3409,645 @@ function registerSACommand(program2) {
|
|
|
3332
3409
|
});
|
|
3333
3410
|
}
|
|
3334
3411
|
|
|
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
|
+
|
|
3335
4051
|
// src/commands/workspace.ts
|
|
3336
4052
|
var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
3337
4053
|
function toISODate(d) {
|
|
@@ -3418,6 +4134,7 @@ registerMonitorCommand(program);
|
|
|
3418
4134
|
registerRepairRecordCommand(program);
|
|
3419
4135
|
registerOperationsRecordCommand(program);
|
|
3420
4136
|
registerChangeConsumerCommand(program);
|
|
4137
|
+
registerTestsetCommand(program);
|
|
3421
4138
|
process.on("uncaughtException", (err) => {
|
|
3422
4139
|
outputError(3, err.message);
|
|
3423
4140
|
process.exit(3);
|