@bty/customer-service-cli 0.6.4 → 0.6.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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.6 (2026-07-17)
4
+
5
+ - 新增 `media upload` 命令与 `product supplement` 子命令组,支持商品「补充知识」图片/视频维护,与前端上传行为一致:
6
+ - `media upload --file <path> --type image|video [--path-prefix /ai/knowledge/supplement] [--dry-run]`:本地图片/视频先经 `/v1/upload_sts` 取 STS 凭证上传到公开 OSS(`resource_bucket`),返回公网 URL;已是 http(s) URL 直接拒绝(无需上传)。上传 key 与前端一致:`${prefix}/${timestamp}_${filename}`。图片上限 10MB、视频上限 200MB,超限/类型不符在发起上传请求前直接失败。
7
+ - `product supplement list --agent <id> --product-id <id>`:读取并解析商品「补充知识」条目(容错解析数组 / JSON 字符串 / 多层编码)。
8
+ - `product supplement add --agent <id> --product-id <id> --question <Q> --answer <A> [--image <path|url>...] [--video <path|url>...] [--dry-run] [--backup <path>]`:read-modify-write 追加一条 Q/A + 媒体,保留旧条目全量写回;本地媒体自动上传、URL 直写。
9
+ - `product supplement append-media --agent <id> --product-id <id> --index <n> [--image ...] [--video ...] [--dry-run] [--backup <path>]`:仅向目标下标条目追加媒体,其它条目原样保留;去重合并,未提供任何媒体时报错。
10
+ - Dashboard 命令使用客服 API 原域名下的 `/observability` 网关前缀,无需新增地址配置。
11
+
12
+ ## 0.6.5 (2026-07-14)
13
+
14
+ - `debug ask` / `debug reproduce` 新增 `--mock <json|@file>`(工具结果 mock,形如 `[{ tool_name, result }]`)与 `--data-overlay <json|@file>`(本轮 `debug_data_overlay` 数据覆盖),发送时透传给 `/v1/debug/chat/agent_message`;`reproduce --dry-run` 输出中同步包含 mock 与 overlay。
15
+ - 新增 `debug data snapshot --agent <id> [--product-id <商品ID...>]`:把当前 Agent 的 SOP / 商品 / 活动等数据导出为 `debug_data_overlay` v1 JSON(写 stdout),配合 `--data-overlay @file` 复跑固定数据集;`--product-id` 可重复传多个只导指定商品,不传则分页导出全量商品。
16
+
3
17
  ## 0.6.4 (2026-07-04)
4
18
 
5
19
  - `conversation transfer-human` 适配服务端 cursor 分页:移除 CLI 暴露的 `--page` 参数,CLI 自动沿 `next_cursor` 拉取全部结果并合并返回,避免深翻页请求压垮数据库。
package/README.md CHANGED
@@ -29,6 +29,7 @@ pnpm build
29
29
  | 服务 | 默认地址 |
30
30
  | -------------- | --------------------------------------------- |
31
31
  | 客服 API | `https://customer-servhub-api.betteryeah.com` |
32
+ | Dashboard 只读 API | 客服 API 地址追加 `/observability` |
32
33
  | AI API | `https://ai-api.betteryeah.com` |
33
34
  | Customer Agent | `https://customer-agent.bantouyan.com` |
34
35
  | Langfuse | `http://192.168.40.10:3000` |
@@ -41,6 +42,8 @@ pnpm build
41
42
  cs-cli config set --cs-api https://your-api.example.com
42
43
  ```
43
44
 
45
+ Dashboard 命令会自动请求该地址下的 `/observability/v1/dashboard/**`。
46
+
44
47
  ### API 地址优先级
45
48
 
46
49
  每个 API 地址的读取顺序为 `环境变量 > config.json > 内置默认值`。环境变量为 `undefined` 或空字符串 `""` 都视为未设置,继续向下 fallback。
@@ -231,6 +234,9 @@ cs-cli workspace points-consumes-daily --start 2026-04-01 --end 2026-04-23
231
234
  | `product related-knowledge add --agent <id> --product-id <商品ID> --type <qa\|common> (--file <文件名> \| --file-id <id> --partition-id <id>)` | 给商品追加关联知识库文件 |
232
235
  | `product related-knowledge remove --agent <id> --product-id <商品ID> --type <qa\|common> --file-id <id>` | 移除商品绑定的指定知识库文件 |
233
236
  | `product related-knowledge set --agent <id> --product-id <商品ID> --bindings <json\|@file>` | 覆盖商品绑定的全部知识库文件 |
237
+ | `product supplement list --agent <id> --product-id <商品ID>` | 列出商品补充知识条目(含每条的 question / answer / images / videos) |
238
+ | `product supplement add --agent <id> --product-id <商品ID> --question <Q> --answer <A> [--image <path\|url>...] [--video <path\|url>...] [--dry-run] [--backup <path>]` | 新增一条补充知识(Q/A + 可选图片/视频);本地媒体自动上传公开 OSS,URL 直写;read-modify-write 保留旧条目 |
239
+ | `product supplement append-media --agent <id> --product-id <商品ID> --index <n> [--image ...] [--video ...] [--dry-run] [--backup <path>]` | 向指定下标条目追加图片/视频,其它条目原样保留、去重合并 |
234
240
 
235
241
  `product update` 支持更新的字段包括:`卖点`、`补充知识`、`tag`、`参数`、`轮播图识别结果`、`商品详情页识别结果` 等。`--update` 接受 JSON 字符串或 `@文件路径`。
236
242
 
@@ -246,6 +252,10 @@ cs-cli workspace points-consumes-daily --start 2026-04-01 --end 2026-04-23
246
252
 
247
253
  `product related-knowledge` 管理商品详情的“关联知识库”文件级绑定。`--type qa` 对应 FAQ 知识库,`--type common` 对应扩展知识库。`add` / `remove` 会先读取当前绑定,再通过服务端全量替换接口写回;重复添加或移除不存在的文件保持幂等。`--file <文件名>` 会按当前 Agent 的知识库文件列表做精确匹配;文件名不存在或不唯一时,改用 `--file-id` + `--partition-id` 显式模式。
248
254
 
255
+ `product supplement` 维护商品「补充知识」的带图/视频问答(结构为 `{ question, answer, images: [URL], videos: [URL] }`),与前端商品详情页的补充知识上传一致。写入采用 **read-modify-write**:先 `product get` 读取并解析当前补充知识,本地合并后再调既有 `/v1/knowledge/product/update`,绝不用新增项覆盖旧条目。`--image` / `--video` 可重复传多个,值可以是**本地文件**(先经 `/v1/upload_sts` 取 STS 凭证上传公开 OSS,key 为 `/ai/knowledge/supplement/${timestamp}_${filename}`)或**已有 http(s) URL**(直写不重复上传)。图片限 `image/*` ≤ 10MB、视频限 `video/*` ≤ 200MB,超限或类型不符在发起上传请求前直接失败。`--dry-run` 只输出将写入的最终 JSON、不落库、不上传;`--backup <path>` 在写前把当前补充知识快照到本地文件。`append-media` 仅向 `--index` 指定的条目追加媒体,去重合并,其它条目原样保留。
256
+
257
+ 单文件上传也可单独用 `media upload`(见下方「媒体上传」小节),拿到公网 URL 后再作为 `--image` / `--video` 的值传入。
258
+
249
259
  ```bash
250
260
  # 更新卖点
251
261
  cs-cli product update --agent <id> --product-id <商品ID> --update '{"卖点":"新卖点内容"}'
@@ -267,6 +277,39 @@ cs-cli product related-knowledge add --agent <id> --product-id <商品ID> --type
267
277
 
268
278
  # 显式 ID 模式绑定扩展知识文件
269
279
  cs-cli product related-knowledge add --agent <id> --product-id <商品ID> --type common --file-id 622474 --partition-id 12345 --file-name "售后规则.md"
280
+
281
+ # 查看商品当前补充知识条目
282
+ cs-cli product supplement list --agent <id> --product-id <商品ID>
283
+
284
+ # 新增一条带图问答(本地图片自动上传,已有 URL 直写)
285
+ cs-cli product supplement add --agent <id> --product-id <商品ID> \
286
+ --question "尺码怎么选?" --answer "参考尺码表,拍下留言身高体重" \
287
+ --image ./size-chart.png --image https://xxx.com/a.png
288
+
289
+ # 先 dry-run 预览最终写入 JSON,不落库
290
+ cs-cli product supplement add --agent <id> --product-id <商品ID> \
291
+ --question "Q" --answer "A" --video ./demo.mp4 --dry-run
292
+
293
+ # 向第 0 条补充知识追加媒体,写前备份
294
+ cs-cli product supplement append-media --agent <id> --product-id <商品ID> \
295
+ --index 0 --image ./extra.png --backup ./supplement-backup.json
296
+ ```
297
+
298
+
299
+ ### 媒体上传 (`media`)
300
+
301
+ | 命令 | 说明 |
302
+ | --- | --- |
303
+ | `media upload --file <path> [--type <image\|video>] [--path-prefix <prefix>] [--dry-run]` | 上传单个本地图片/视频到公开 OSS,返回可访问 URL |
304
+
305
+ `media upload` 把单个本地文件上传到公开 OSS(`resource_bucket`),返回公网 URL,供 `product supplement` 的 `--image` / `--video` 复用。凭证走 `GET /v1/upload_sts`(返回 AES-CBC 加密,CLI 内解密),上传 key 为 `<path-prefix>/<timestamp>_<filename>`,`--path-prefix` 默认 `/ai/knowledge/supplement`。`--type` 缺省时按扩展名推断;图片限 `image/*` ≤ 10MB、视频限 `video/*` ≤ 200MB,超限/类型不符在发起上传前直接拒绝。传入的 `--file` 如果本身是 http(s) URL 会被拒绝(无需上传)。`--dry-run` 仅校验并输出将要上传的信息、不实际上传。
306
+
307
+ ```bash
308
+ # 上传本地图片,拿到公网 URL
309
+ cs-cli media upload --file ./banner.png --type image
310
+
311
+ # 自定义路径前缀 + 预览(不实际上传)
312
+ cs-cli media upload --file ./demo.mp4 --path-prefix /ai/knowledge/supplement --dry-run
270
313
  ```
271
314
 
272
315
 
package/dist/bin.js CHANGED
@@ -318,7 +318,7 @@ function toExitCode(err) {
318
318
  return 1;
319
319
  }
320
320
  function createRequest(globalTimeout) {
321
- return async function request(baseUrl, path6, options) {
321
+ return async function request(baseUrl, path7, options) {
322
322
  const headers = {
323
323
  "Content-Type": "application/json",
324
324
  ...options.headers
@@ -346,7 +346,7 @@ function createRequest(globalTimeout) {
346
346
  if (workspaceId) {
347
347
  headers["workspace-id"] = workspaceId;
348
348
  }
349
- let url = `${baseUrl}${path6}`;
349
+ let url = `${baseUrl}${path7}`;
350
350
  if (options.query) {
351
351
  const params = new URLSearchParams();
352
352
  for (const [key, value] of Object.entries(options.query)) {
@@ -485,6 +485,9 @@ function getCustomerServiceUrl() {
485
485
  const config = readConfig();
486
486
  return config?.customerServiceApiUrl ?? DEFAULT_CS_API_URL;
487
487
  }
488
+ function getObservabilityUrl() {
489
+ return `${getCustomerServiceUrl().replace(/\/+$/, "")}/observability`;
490
+ }
488
491
  function getAiApiUrl() {
489
492
  const envUrl = process.env.CS_AI_API_URL;
490
493
  if (envUrl) return envUrl;
@@ -1401,22 +1404,22 @@ function registerConversationCommand(program2) {
1401
1404
  var DASHBOARD_PREFIX = "/v1/dashboard";
1402
1405
  async function getDashboardSummary(opts) {
1403
1406
  const request = createRequest();
1404
- const path6 = opts.configId ? `${DASHBOARD_PREFIX}/${opts.configId}/summary` : `${DASHBOARD_PREFIX}/summary`;
1407
+ const path7 = opts.configId ? `${DASHBOARD_PREFIX}/${opts.configId}/summary` : `${DASHBOARD_PREFIX}/summary`;
1405
1408
  const query = {};
1406
1409
  if (opts.startDate) query.start_date = opts.startDate;
1407
1410
  if (opts.endDate) query.end_date = opts.endDate;
1408
1411
  if (opts.channel) query.channel = opts.channel;
1409
- return request(getCustomerServiceUrl(), path6, { method: "GET", query });
1412
+ return request(getObservabilityUrl(), path7, { method: "GET", query });
1410
1413
  }
1411
1414
  async function getDashboardTrend(opts) {
1412
1415
  const request = createRequest();
1413
- const path6 = opts.configId ? `${DASHBOARD_PREFIX}/${opts.configId}/trend` : `${DASHBOARD_PREFIX}/trend`;
1416
+ const path7 = opts.configId ? `${DASHBOARD_PREFIX}/${opts.configId}/trend` : `${DASHBOARD_PREFIX}/trend`;
1414
1417
  const query = {};
1415
1418
  if (opts.startDate) query.start_date = opts.startDate;
1416
1419
  if (opts.endDate) query.end_date = opts.endDate;
1417
1420
  if (opts.granularity) query.granularity = opts.granularity;
1418
1421
  if (opts.channel) query.channel = opts.channel;
1419
- return request(getCustomerServiceUrl(), path6, { method: "GET", query });
1422
+ return request(getObservabilityUrl(), path7, { method: "GET", query });
1420
1423
  }
1421
1424
  async function listShopStatistics(opts) {
1422
1425
  const request = createRequest();
@@ -1424,7 +1427,7 @@ async function listShopStatistics(opts) {
1424
1427
  if (opts.startDate) query.start_date = opts.startDate;
1425
1428
  if (opts.endDate) query.end_date = opts.endDate;
1426
1429
  if (opts.channel) query.channel = opts.channel;
1427
- return request(getCustomerServiceUrl(), `${DASHBOARD_PREFIX}/shop-statistics`, {
1430
+ return request(getObservabilityUrl(), `${DASHBOARD_PREFIX}/shop-statistics`, {
1428
1431
  method: "GET",
1429
1432
  query
1430
1433
  });
@@ -1435,7 +1438,7 @@ async function getIntentStatistics(opts) {
1435
1438
  if (opts.startDate) query.start_date = opts.startDate;
1436
1439
  if (opts.endDate) query.end_date = opts.endDate;
1437
1440
  return request(
1438
- getCustomerServiceUrl(),
1441
+ getObservabilityUrl(),
1439
1442
  `${DASHBOARD_PREFIX}/${opts.configId}/intent-statistics`,
1440
1443
  { method: "GET", query }
1441
1444
  );
@@ -1451,7 +1454,7 @@ async function listIntentConversations(opts) {
1451
1454
  if (opts.page) query.page = opts.page;
1452
1455
  if (opts.pageSize) query.page_size = opts.pageSize;
1453
1456
  return request(
1454
- getCustomerServiceUrl(),
1457
+ getObservabilityUrl(),
1455
1458
  `${DASHBOARD_PREFIX}/${opts.configId}/intent-conversations`,
1456
1459
  { method: "GET", query }
1457
1460
  );
@@ -1539,7 +1542,7 @@ async function exportShopData(opts) {
1539
1542
  if (opts.configIds && opts.configIds.length > 0) body.config_ids = opts.configIds;
1540
1543
  if (opts.channel) body.channel = opts.channel;
1541
1544
  const data = await request(
1542
- getCustomerServiceUrl(),
1545
+ getObservabilityUrl(),
1543
1546
  `${DASHBOARD_PREFIX}/export`,
1544
1547
  { method: "POST", body }
1545
1548
  );
@@ -1636,12 +1639,12 @@ function registerDashboardCommand(program2) {
1636
1639
  channel: opts.channel
1637
1640
  });
1638
1641
  if (opts.out) {
1639
- const { path: path6, bytes } = await writeBinaryToFile(
1642
+ const { path: path7, bytes } = await writeBinaryToFile(
1640
1643
  opts.out,
1641
1644
  Buffer.from(result.csv, "utf-8")
1642
1645
  );
1643
1646
  formatOutput(
1644
- { success: true, data: { path: path6, bytes, count: result.rows.length } },
1647
+ { success: true, data: { path: path7, bytes, count: result.rows.length } },
1645
1648
  program2.opts().table
1646
1649
  );
1647
1650
  } else {
@@ -4062,6 +4065,252 @@ function registerKnowledgeCommand(program2) {
4062
4065
  registerKnowledgeFileBindProductsCommand(knowledge, program2, "common");
4063
4066
  }
4064
4067
 
4068
+ // src/lib/media-upload.ts
4069
+ import fs9 from "fs";
4070
+ import path5 from "path";
4071
+ import OSS from "ali-oss";
4072
+
4073
+ // src/client/oss-api.ts
4074
+ import CryptoJS2 from "crypto-js";
4075
+ var STS_SECRET_KEY = "asdb2l0jskjft1q314f3ffjy71k9shj6";
4076
+ var IV_BYTES_LEN = 16;
4077
+ function decryptStsPayload(ciphertext) {
4078
+ const ciphertextBytes = CryptoJS2.enc.Base64.parse(
4079
+ ciphertext.replaceAll("-", "+").replaceAll("_", "/")
4080
+ );
4081
+ const iv = ciphertextBytes.clone();
4082
+ iv.sigBytes = IV_BYTES_LEN;
4083
+ iv.clamp();
4084
+ const encryptedContent = ciphertextBytes.clone();
4085
+ encryptedContent.words.splice(0, IV_BYTES_LEN / 4);
4086
+ encryptedContent.sigBytes -= IV_BYTES_LEN;
4087
+ const decrypted = CryptoJS2.AES.decrypt(
4088
+ // biome-ignore lint/suspicious/noExplicitAny: CryptoJS CipherParams 结构,仅需 ciphertext 字段
4089
+ { ciphertext: encryptedContent },
4090
+ CryptoJS2.enc.Utf8.parse(STS_SECRET_KEY),
4091
+ {
4092
+ iv,
4093
+ mode: CryptoJS2.mode.CBC,
4094
+ padding: CryptoJS2.pad.Pkcs7
4095
+ }
4096
+ );
4097
+ return decrypted.toString(CryptoJS2.enc.Utf8);
4098
+ }
4099
+ async function getUploadStsToken() {
4100
+ const request = createRequest();
4101
+ const encrypted = await request(getAiApiUrl(), "/v1/upload_sts", {
4102
+ method: "GET"
4103
+ });
4104
+ if (typeof encrypted !== "string" || encrypted.length === 0) {
4105
+ throw new Error("\u83B7\u53D6 OSS \u4E0A\u4F20\u51ED\u8BC1\u5931\u8D25\uFF1A\u63A5\u53E3\u672A\u8FD4\u56DE\u52A0\u5BC6\u6570\u636E");
4106
+ }
4107
+ let decoded;
4108
+ try {
4109
+ decoded = decryptStsPayload(encrypted);
4110
+ } catch (err) {
4111
+ throw new Error(`OSS \u4E0A\u4F20\u51ED\u8BC1\u89E3\u5BC6\u5931\u8D25\uFF1A${err.message}`);
4112
+ }
4113
+ let token;
4114
+ try {
4115
+ token = JSON.parse(decoded);
4116
+ } catch {
4117
+ throw new Error("OSS \u4E0A\u4F20\u51ED\u8BC1\u89E3\u6790\u5931\u8D25\uFF1A\u89E3\u5BC6\u7ED3\u679C\u4E0D\u662F\u5408\u6CD5 JSON");
4118
+ }
4119
+ if (!token.access_key_id || !token.access_key_secret) {
4120
+ throw new Error("OSS \u4E0A\u4F20\u51ED\u8BC1\u7F3A\u5C11 access_key_id / access_key_secret");
4121
+ }
4122
+ return token;
4123
+ }
4124
+
4125
+ // src/lib/media-upload.ts
4126
+ var IMAGE_MAX_BYTES = 10 * 1024 * 1024;
4127
+ var VIDEO_MAX_BYTES = 200 * 1024 * 1024;
4128
+ var DEFAULT_PATH_PREFIX = "/ai/knowledge/supplement";
4129
+ var IMAGE_EXTS = /* @__PURE__ */ new Set([
4130
+ ".jpg",
4131
+ ".jpeg",
4132
+ ".png",
4133
+ ".gif",
4134
+ ".webp",
4135
+ ".bmp",
4136
+ ".svg",
4137
+ ".tiff",
4138
+ ".tif",
4139
+ ".ico",
4140
+ ".heic",
4141
+ ".heif",
4142
+ ".avif"
4143
+ ]);
4144
+ var VIDEO_EXTS = /* @__PURE__ */ new Set([
4145
+ ".mp4",
4146
+ ".mov",
4147
+ ".avi",
4148
+ ".mkv",
4149
+ ".webm",
4150
+ ".flv",
4151
+ ".wmv",
4152
+ ".m4v",
4153
+ ".mpeg",
4154
+ ".mpg",
4155
+ ".3gp"
4156
+ ]);
4157
+ function isRemoteUrl(value) {
4158
+ return /^https?:\/\//i.test(value.trim());
4159
+ }
4160
+ function inferMediaType(filePath) {
4161
+ const ext = path5.extname(filePath).toLowerCase();
4162
+ if (IMAGE_EXTS.has(ext)) return "image";
4163
+ if (VIDEO_EXTS.has(ext)) return "video";
4164
+ return void 0;
4165
+ }
4166
+ function extMatchesType(filePath, type) {
4167
+ const ext = path5.extname(filePath).toLowerCase();
4168
+ if (type === "image") return !VIDEO_EXTS.has(ext);
4169
+ return !IMAGE_EXTS.has(ext);
4170
+ }
4171
+ function validateLocalFile(opts) {
4172
+ const { filePath } = opts;
4173
+ const absPath = path5.resolve(filePath);
4174
+ let stat;
4175
+ try {
4176
+ stat = fs9.statSync(absPath);
4177
+ } catch {
4178
+ throw new Error(`\u6587\u4EF6\u4E0D\u5B58\u5728: ${filePath}`);
4179
+ }
4180
+ if (!stat.isFile()) {
4181
+ throw new Error(`\u8DEF\u5F84\u4E0D\u662F\u6587\u4EF6: ${filePath}`);
4182
+ }
4183
+ const type = opts.type ?? inferMediaType(absPath);
4184
+ if (!type) {
4185
+ throw new Error(`\u65E0\u6CD5\u6839\u636E\u6269\u5C55\u540D\u5224\u65AD\u5A92\u4F53\u7C7B\u578B\uFF0C\u8BF7\u663E\u5F0F\u6307\u5B9A --type image|video: ${filePath}`);
4186
+ }
4187
+ if (type !== "image" && type !== "video") {
4188
+ throw new Error(`\u4E0D\u652F\u6301\u7684\u5A92\u4F53\u7C7B\u578B: ${type}\uFF08\u4EC5\u652F\u6301 image / video\uFF09`);
4189
+ }
4190
+ if (!extMatchesType(absPath, type)) {
4191
+ throw new Error(`\u6587\u4EF6\u6269\u5C55\u540D\u4E0E\u58F0\u660E\u7684\u7C7B\u578B ${type} \u4E0D\u5339\u914D: ${filePath}`);
4192
+ }
4193
+ const limit = type === "image" ? IMAGE_MAX_BYTES : VIDEO_MAX_BYTES;
4194
+ if (stat.size > limit) {
4195
+ const limitMb = Math.round(limit / (1024 * 1024));
4196
+ const sizeMb = (stat.size / (1024 * 1024)).toFixed(2);
4197
+ throw new Error(
4198
+ `${type === "image" ? "\u56FE\u7247" : "\u89C6\u9891"}\u5927\u5C0F ${sizeMb}MB \u8D85\u8FC7\u4E0A\u9650 ${limitMb}MB: ${filePath}`
4199
+ );
4200
+ }
4201
+ return {
4202
+ absPath,
4203
+ filename: path5.basename(absPath),
4204
+ type,
4205
+ sizeBytes: stat.size
4206
+ };
4207
+ }
4208
+ function buildObjectKey(opts) {
4209
+ const ts = opts.timestamp ?? Date.now();
4210
+ const prefix = opts.pathPrefix.replace(/\/+$/, "").replace(/^\/+/, "");
4211
+ return `${prefix}/${ts}_${opts.filename}`;
4212
+ }
4213
+ var defaultOssPut = async ({ token, bucket, key, bytes }) => {
4214
+ const client = new OSS({
4215
+ region: token.region || "oss-cn-hangzhou",
4216
+ accessKeyId: token.access_key_id,
4217
+ accessKeySecret: token.access_key_secret,
4218
+ stsToken: token.security_token,
4219
+ bucket,
4220
+ secure: true,
4221
+ ...token.endpoint ? { endpoint: token.endpoint } : {}
4222
+ });
4223
+ const normalizedKey = key.replace(/^\/+/, "");
4224
+ const result = await client.put(normalizedKey, bytes);
4225
+ const rawUrl = result?.url ?? result?.res?.requestUrls?.[0];
4226
+ if (!rawUrl) {
4227
+ throw new Error("\u4E0A\u4F20\u6210\u529F\u4F46\u672A\u80FD\u89E3\u6790\u8FD4\u56DE URL");
4228
+ }
4229
+ return rawUrl.split("?")[0];
4230
+ };
4231
+ var ossPutImpl = defaultOssPut;
4232
+ async function uploadLocalFile(opts) {
4233
+ const token = await getUploadStsToken();
4234
+ const bucket = token.resource_bucket;
4235
+ if (!bucket) {
4236
+ throw new Error("OSS \u4E0A\u4F20\u51ED\u8BC1\u7F3A\u5C11 resource_bucket\uFF08\u516C\u5F00\u6876\uFF09\uFF0C\u65E0\u6CD5\u4E0A\u4F20\u516C\u5F00\u5A92\u4F53");
4237
+ }
4238
+ const key = buildObjectKey({
4239
+ pathPrefix: opts.pathPrefix ?? DEFAULT_PATH_PREFIX,
4240
+ filename: opts.file.filename,
4241
+ timestamp: opts.timestamp
4242
+ });
4243
+ const bytes = fs9.readFileSync(opts.file.absPath);
4244
+ try {
4245
+ return await ossPutImpl({ token, bucket, key, bytes });
4246
+ } catch (err) {
4247
+ throw new Error(`OSS \u4E0A\u4F20\u5931\u8D25: ${err.message}`);
4248
+ }
4249
+ }
4250
+ async function resolveMediaInput(opts) {
4251
+ const value = opts.value.trim();
4252
+ if (isRemoteUrl(value)) {
4253
+ return value;
4254
+ }
4255
+ const file = validateLocalFile({ filePath: value, type: opts.type });
4256
+ if (opts.dryRun) {
4257
+ return `<dry-run:will-upload:${file.filename}>`;
4258
+ }
4259
+ return uploadLocalFile({ file, pathPrefix: opts.pathPrefix });
4260
+ }
4261
+
4262
+ // src/commands/media.ts
4263
+ function registerMediaCommand(program2) {
4264
+ const media = program2.command("media").description(
4265
+ "\u5A92\u4F53\u4E0A\u4F20 \u2014\u2014 \u5C06\u672C\u5730\u56FE\u7247/\u89C6\u9891\u4E0A\u4F20\u5230\u516C\u5F00 OSS \u5E76\u8FD4\u56DE\u516C\u7F51 URL\u3002\u7528\u4E8E\u7EF4\u62A4\u5546\u54C1\u8865\u5145\u77E5\u8BC6\u4E2D\u7684 images / videos\uFF08\u8DEF\u5F84\u524D\u7F00\u4E0E\u524D\u7AEF\u4E00\u81F4\uFF09"
4266
+ );
4267
+ media.command("upload").description(
4268
+ "\u4E0A\u4F20\u5355\u4E2A\u672C\u5730\u56FE\u7247/\u89C6\u9891\u5230\u516C\u5F00 OSS\uFF0C\u8FD4\u56DE\u53EF\u8BBF\u95EE URL\u3002\u56FE\u7247\u9650 10MB\u3001\u89C6\u9891\u9650 200MB\uFF1B\u8D85\u9650\u76F4\u63A5\u62D2\u7EDD\uFF0C\u4E0D\u53D1\u4E0A\u4F20\u8BF7\u6C42"
4269
+ ).requiredOption("--file <path>", "\u672C\u5730\u6587\u4EF6\u8DEF\u5F84").option("--type <type>", "\u5A92\u4F53\u7C7B\u578B: image | video\uFF08\u7F3A\u7701\u6309\u6269\u5C55\u540D\u63A8\u65AD\uFF09").option(
4270
+ "--path-prefix <prefix>",
4271
+ "OSS \u8DEF\u5F84\u524D\u7F00\uFF0C\u6700\u7EC8 key \u4E3A <prefix>/<timestamp>_<filename>",
4272
+ DEFAULT_PATH_PREFIX
4273
+ ).option("--dry-run", "\u4EC5\u6821\u9A8C\u5E76\u8F93\u51FA\u5C06\u8981\u4E0A\u4F20\u7684\u4FE1\u606F\uFF0C\u4E0D\u5B9E\u9645\u4E0A\u4F20").action(async (opts) => {
4274
+ try {
4275
+ if (isRemoteUrl(opts.file)) {
4276
+ throw new Error("--file \u9700\u4E3A\u672C\u5730\u8DEF\u5F84\uFF1B\u5DF2\u662F URL \u7684\u5A92\u4F53\u65E0\u9700\u4E0A\u4F20");
4277
+ }
4278
+ const type = opts.type;
4279
+ if (type && type !== "image" && type !== "video") {
4280
+ throw new Error(`--type \u4EC5\u652F\u6301 image | video\uFF0C\u6536\u5230: ${type}`);
4281
+ }
4282
+ const file = validateLocalFile({ filePath: opts.file, type });
4283
+ if (opts.dryRun) {
4284
+ formatOutput(
4285
+ {
4286
+ success: true,
4287
+ data: {
4288
+ dryRun: true,
4289
+ file: file.absPath,
4290
+ type: file.type,
4291
+ sizeBytes: file.sizeBytes,
4292
+ pathPrefix: opts.pathPrefix
4293
+ }
4294
+ },
4295
+ program2.opts().table
4296
+ );
4297
+ return;
4298
+ }
4299
+ const url = await uploadLocalFile({ file, pathPrefix: opts.pathPrefix });
4300
+ formatOutput(
4301
+ {
4302
+ success: true,
4303
+ data: { url, type: file.type, sizeBytes: file.sizeBytes }
4304
+ },
4305
+ program2.opts().table
4306
+ );
4307
+ } catch (err) {
4308
+ reportCaughtError(err);
4309
+ process.exit(toExitCode(err));
4310
+ }
4311
+ });
4312
+ }
4313
+
4065
4314
  // src/client/monitor-api.ts
4066
4315
  var MONITOR_PATH_PREFIX = "/v1/operation";
4067
4316
  async function listAgentLifecycle(opts) {
@@ -4954,44 +5203,6 @@ function registerNodeTemplateCommand(program2) {
4954
5203
  });
4955
5204
  }
4956
5205
 
4957
- // src/client/ops-agent-api.ts
4958
- async function listOpsAgentConversations(opts) {
4959
- const workspaceId = getWorkspaceId(opts.workspaceId);
4960
- const request = createRequest();
4961
- const query = {
4962
- page: opts.page ?? 1,
4963
- page_size: opts.pageSize ?? 20
4964
- };
4965
- if (opts.customerId) query.customer_id = opts.customerId;
4966
- if (opts.agentId) query.agent_id = opts.agentId;
4967
- return request(getCustomerServiceUrl(), "/v1/ops-agent/conversations", {
4968
- method: "GET",
4969
- query,
4970
- headers: { "workspace-id": workspaceId }
4971
- });
4972
- }
4973
-
4974
- // src/commands/ops-agent.ts
4975
- function registerOpsAgentCommand(program2) {
4976
- const opsAgent = program2.command("ops-agent").description("Ops Agent\uFF08\u5BA2\u670D\u52A9\u624B\uFF09\u67E5\u8BE2\u547D\u4EE4");
4977
- opsAgent.command("conversations").description(
4978
- "\u67E5\u8BE2\u5BA2\u670D\u52A9\u624B\u4F1A\u8BDD\u5217\u8868\u3002\u8FD4\u56DE workspace_id\u3001customer_id\u3001title\u3001agent_id\u3001created_at\u3001updated_at"
4979
- ).option("--customer-id <id>", "\u6309 customer_id \u7B5B\u9009").option("--agent-id <id>", "\u6309 Ops Agent \u914D\u7F6E ID\uFF08customer_agent_config_id\uFF09\u7B5B\u9009").option("--page <number>", "\u9875\u7801", "1").option("--page-size <number>", "\u6BCF\u9875\u6570\u91CF", "20").action(async (opts) => {
4980
- try {
4981
- const data = await listOpsAgentConversations({
4982
- customerId: opts.customerId,
4983
- agentId: opts.agentId,
4984
- page: Number(opts.page),
4985
- pageSize: Number(opts.pageSize)
4986
- });
4987
- formatOutput({ success: true, data }, program2.opts().table);
4988
- } catch (err) {
4989
- reportCaughtError(err);
4990
- process.exit(toExitCode(err));
4991
- }
4992
- });
4993
- }
4994
-
4995
5206
  // src/client/operations-record-api.ts
4996
5207
  var PATH_PREFIX3 = "/v1/agent_operations_records";
4997
5208
  async function listOperationsRecords(opts) {
@@ -5106,6 +5317,112 @@ function registerOperationsRecordCommand(program2) {
5106
5317
  });
5107
5318
  }
5108
5319
 
5320
+ // src/client/ops-agent-api.ts
5321
+ async function listOpsAgentConversations(opts) {
5322
+ const workspaceId = getWorkspaceId(opts.workspaceId);
5323
+ const request = createRequest();
5324
+ const query = {
5325
+ page: opts.page ?? 1,
5326
+ page_size: opts.pageSize ?? 20
5327
+ };
5328
+ if (opts.customerId) query.customer_id = opts.customerId;
5329
+ if (opts.agentId) query.agent_id = opts.agentId;
5330
+ return request(getCustomerServiceUrl(), "/v1/ops-agent/conversations", {
5331
+ method: "GET",
5332
+ query,
5333
+ headers: { "workspace-id": workspaceId }
5334
+ });
5335
+ }
5336
+
5337
+ // src/commands/ops-agent.ts
5338
+ function registerOpsAgentCommand(program2) {
5339
+ const opsAgent = program2.command("ops-agent").description("Ops Agent\uFF08\u5BA2\u670D\u52A9\u624B\uFF09\u67E5\u8BE2\u547D\u4EE4");
5340
+ opsAgent.command("conversations").description(
5341
+ "\u67E5\u8BE2\u5BA2\u670D\u52A9\u624B\u4F1A\u8BDD\u5217\u8868\u3002\u8FD4\u56DE workspace_id\u3001customer_id\u3001title\u3001agent_id\u3001created_at\u3001updated_at"
5342
+ ).option("--customer-id <id>", "\u6309 customer_id \u7B5B\u9009").option("--agent-id <id>", "\u6309 Ops Agent \u914D\u7F6E ID\uFF08customer_agent_config_id\uFF09\u7B5B\u9009").option("--page <number>", "\u9875\u7801", "1").option("--page-size <number>", "\u6BCF\u9875\u6570\u91CF", "20").action(async (opts) => {
5343
+ try {
5344
+ const data = await listOpsAgentConversations({
5345
+ customerId: opts.customerId,
5346
+ agentId: opts.agentId,
5347
+ page: Number(opts.page),
5348
+ pageSize: Number(opts.pageSize)
5349
+ });
5350
+ formatOutput({ success: true, data }, program2.opts().table);
5351
+ } catch (err) {
5352
+ reportCaughtError(err);
5353
+ process.exit(toExitCode(err));
5354
+ }
5355
+ });
5356
+ }
5357
+
5358
+ // src/commands/product.ts
5359
+ import fs10 from "fs";
5360
+
5361
+ // src/lib/supplement.ts
5362
+ function parseSupplementField(value) {
5363
+ let current = value;
5364
+ for (let i = 0; i < 3; i++) {
5365
+ if (Array.isArray(current)) {
5366
+ return current.filter((it) => it != null && typeof it === "object");
5367
+ }
5368
+ if (typeof current !== "string") return [];
5369
+ const trimmed = current.trim();
5370
+ if (trimmed === "") return [];
5371
+ try {
5372
+ current = JSON.parse(trimmed);
5373
+ } catch {
5374
+ throw new Error(`\u8865\u5145\u77E5\u8BC6 JSON \u89E3\u6790\u5931\u8D25: ${truncate(trimmed)}`);
5375
+ }
5376
+ }
5377
+ return Array.isArray(current) ? current.filter((it) => it != null && typeof it === "object") : [];
5378
+ }
5379
+ function truncate(s, max = 120) {
5380
+ return s.length > max ? `${s.slice(0, max)}\u2026` : s;
5381
+ }
5382
+ function mergeUnique(existing, incoming) {
5383
+ const seen = /* @__PURE__ */ new Set();
5384
+ const result = [];
5385
+ for (const v of [...existing ?? [], ...incoming]) {
5386
+ const t = (v ?? "").trim();
5387
+ if (t === "" || seen.has(t)) continue;
5388
+ seen.add(t);
5389
+ result.push(t);
5390
+ }
5391
+ return result;
5392
+ }
5393
+ function addSupplementItem(existing, item) {
5394
+ const next = {
5395
+ question: item.question,
5396
+ answer: item.answer
5397
+ };
5398
+ if (item.images && item.images.length > 0) next.images = mergeUnique([], item.images);
5399
+ if (item.videos && item.videos.length > 0) next.videos = mergeUnique([], item.videos);
5400
+ return [...existing, next];
5401
+ }
5402
+ function appendMediaToItem(existing, opts) {
5403
+ if (!Number.isInteger(opts.index) || opts.index < 0 || opts.index >= existing.length) {
5404
+ throw new Error(`\u8865\u5145\u77E5\u8BC6\u6761\u76EE\u4E0B\u6807\u8D8A\u754C: index=${opts.index}\uFF08\u5F53\u524D\u5171 ${existing.length} \u6761\uFF09`);
5405
+ }
5406
+ return existing.map((it, i) => {
5407
+ if (i !== opts.index) return it;
5408
+ const updated = { ...it };
5409
+ if (opts.images && opts.images.length > 0) {
5410
+ updated.images = mergeUnique(asStringArray(it.images), opts.images);
5411
+ }
5412
+ if (opts.videos && opts.videos.length > 0) {
5413
+ updated.videos = mergeUnique(asStringArray(it.videos), opts.videos);
5414
+ }
5415
+ return updated;
5416
+ });
5417
+ }
5418
+ function asStringArray(value) {
5419
+ if (!Array.isArray(value)) return [];
5420
+ return value.filter((v) => typeof v === "string");
5421
+ }
5422
+ function serializeSupplement(items) {
5423
+ return JSON.stringify(items);
5424
+ }
5425
+
5109
5426
  // src/commands/product.ts
5110
5427
  async function getDbId(agentConfigId) {
5111
5428
  const agent = await getAgent(agentConfigId);
@@ -5130,6 +5447,41 @@ async function findProduct(agentConfigId, productId) {
5130
5447
  }
5131
5448
  }
5132
5449
  }
5450
+ async function loadProductSupplement(agentConfigId, productId) {
5451
+ const [dbId, matched] = await Promise.all([
5452
+ getDbId(agentConfigId),
5453
+ findProduct(agentConfigId, productId)
5454
+ ]);
5455
+ const detail = await getProductDetail({
5456
+ agentConfigId,
5457
+ dbId,
5458
+ productId
5459
+ });
5460
+ const items = parseSupplementField(detail.\u8865\u5145\u77E5\u8BC6);
5461
+ return {
5462
+ dbId,
5463
+ dataId: detail.id,
5464
+ knowledgeId: matched.knowledge_id,
5465
+ items,
5466
+ raw: detail.\u8865\u5145\u77E5\u8BC6
5467
+ };
5468
+ }
5469
+ async function writeProductSupplement(ctx, items) {
5470
+ return updateProduct({
5471
+ dataId: ctx.dataId,
5472
+ knowledgeId: ctx.knowledgeId,
5473
+ dbId: ctx.dbId,
5474
+ updateDict: { \u8865\u5145\u77E5\u8BC6: serializeSupplement(items) }
5475
+ });
5476
+ }
5477
+ async function resolveMediaList(values, type, dryRun) {
5478
+ if (!values || values.length === 0) return [];
5479
+ const urls = [];
5480
+ for (const value of values) {
5481
+ urls.push(await resolveMediaInput({ value, type, dryRun }));
5482
+ }
5483
+ return urls;
5484
+ }
5133
5485
  function registerProductCommand(program2) {
5134
5486
  const product = program2.command("product").description(
5135
5487
  "\u5546\u54C1\u77E5\u8BC6\u5E93\u7BA1\u7406 \u2014\u2014 Agent \u5173\u8054\u7684\u5546\u54C1\u6570\u636E\uFF08\u5356\u70B9\u3001\u8865\u5145\u77E5\u8BC6\u3001SKU\u3001\u53C2\u6570\u3001\u56FE\u7247\u8BC6\u522B\u7ED3\u679C\u7B49\uFF09\u3002Agent \u56DE\u7B54\u5546\u54C1\u76F8\u5173\u95EE\u9898\u65F6\u4F9D\u8D56\u6B64\u6570\u636E\u6E90"
@@ -5315,8 +5667,117 @@ function registerProductCommand(program2) {
5315
5667
  process.exit(toExitCode(err));
5316
5668
  }
5317
5669
  });
5670
+ registerProductSupplementCommand(product, program2);
5318
5671
  registerProductRelatedKnowledgeCommand(product, program2);
5319
5672
  }
5673
+ function registerProductSupplementCommand(product, program2) {
5674
+ const supplement = product.command("supplement").description(
5675
+ "\u5546\u54C1\u8865\u5145\u77E5\u8BC6\uFF08\u95EE\u7B54 + \u56FE\u7247/\u89C6\u9891\uFF09\u7EF4\u62A4\u3002\u5199\u5165\u91C7\u7528 read-modify-write\uFF0C\u53EA\u5408\u5E76\u76EE\u6807\u6761\u76EE\uFF0C\u4E0D\u8986\u76D6\u5176\u5B83\u8865\u5145\u77E5\u8BC6"
5676
+ );
5677
+ supplement.command("list").description("\u5217\u51FA\u5546\u54C1\u5F53\u524D\u8865\u5145\u77E5\u8BC6\u6761\u76EE\uFF08\u542B\u6BCF\u6761\u7684 question / answer / images / videos\uFF09").requiredOption("--agent <config_id>", "Agent \u914D\u7F6E ID\uFF08\u4ECE agent list \u83B7\u53D6\uFF09").requiredOption("--product-id <product_id>", "\u5546\u54C1 ID\uFF08\u4ECE product list \u83B7\u53D6\uFF09").action(async (opts) => {
5678
+ try {
5679
+ const ctx = await loadProductSupplement(opts.agent, opts.productId);
5680
+ formatOutput(
5681
+ {
5682
+ success: true,
5683
+ data: {
5684
+ product_id: opts.productId,
5685
+ count: ctx.items.length,
5686
+ \u8865\u5145\u77E5\u8BC6: ctx.items
5687
+ }
5688
+ },
5689
+ program2.opts().table
5690
+ );
5691
+ } catch (err) {
5692
+ reportCaughtError(err);
5693
+ process.exit(toExitCode(err));
5694
+ }
5695
+ });
5696
+ supplement.command("add").description(
5697
+ "\u65B0\u589E\u4E00\u6761\u8865\u5145\u77E5\u8BC6\uFF08Q/A + \u53EF\u9009\u56FE\u7247/\u89C6\u9891\uFF09\u3002--image / --video \u652F\u6301\u672C\u5730\u6587\u4EF6\uFF08\u81EA\u52A8\u4E0A\u4F20\u516C\u5F00 OSS\uFF09\u6216\u5DF2\u6709 URL\uFF08\u76F4\u5199\uFF09\u3002read-modify-write\uFF1A\u4FDD\u7559\u5168\u90E8\u65E2\u6709\u6761\u76EE\uFF0C\u4EC5\u8FFD\u52A0\u65B0\u6761\u76EE"
5698
+ ).requiredOption("--agent <config_id>", "Agent \u914D\u7F6E ID\uFF08\u4ECE agent list \u83B7\u53D6\uFF09").requiredOption("--product-id <product_id>", "\u5546\u54C1 ID\uFF08\u4ECE product list \u83B7\u53D6\uFF09").requiredOption("--question <text>", "\u5E38\u95EE\u95EE\u9898").requiredOption("--answer <text>", "\u56DE\u7B54\u5185\u5BB9").option("--image <path-or-url...>", "\u56FE\u7247\u672C\u5730\u8DEF\u5F84\u6216 URL\uFF08\u53EF\u591A\u4E2A\uFF09").option("--video <path-or-url...>", "\u89C6\u9891\u672C\u5730\u8DEF\u5F84\u6216 URL\uFF08\u53EF\u591A\u4E2A\uFF09").option("--dry-run", "\u53EA\u8F93\u51FA\u6700\u7EC8\u5C06\u5199\u5165\u7684 JSON\uFF0C\u4E0D\u5B9E\u9645\u4E0A\u4F20/\u66F4\u65B0").option("--backup <path>", "\u5199\u5165\u524D\u5C06\u66F4\u65B0\u524D\u8865\u5145\u77E5\u8BC6\u5FEB\u7167\u4FDD\u5B58\u5230\u6307\u5B9A\u6587\u4EF6").action(async (opts) => {
5699
+ try {
5700
+ const ctx = await loadProductSupplement(opts.agent, opts.productId);
5701
+ maybeWriteBackup(opts.backup, ctx.items);
5702
+ const images = await resolveMediaList(opts.image, "image", Boolean(opts.dryRun));
5703
+ const videos = await resolveMediaList(opts.video, "video", Boolean(opts.dryRun));
5704
+ const nextItems = addSupplementItem(ctx.items, {
5705
+ question: opts.question,
5706
+ answer: opts.answer,
5707
+ images,
5708
+ videos
5709
+ });
5710
+ if (opts.dryRun) {
5711
+ formatOutput(
5712
+ {
5713
+ success: true,
5714
+ data: {
5715
+ dryRun: true,
5716
+ before: ctx.items,
5717
+ after: nextItems,
5718
+ update_dict: { \u8865\u5145\u77E5\u8BC6: serializeSupplement(nextItems) }
5719
+ }
5720
+ },
5721
+ program2.opts().table
5722
+ );
5723
+ return;
5724
+ }
5725
+ const data = await writeProductSupplement(ctx, nextItems);
5726
+ formatOutput(
5727
+ { success: true, data: { updated: true, count: nextItems.length, result: data } },
5728
+ program2.opts().table
5729
+ );
5730
+ } catch (err) {
5731
+ reportCaughtError(err);
5732
+ process.exit(toExitCode(err));
5733
+ }
5734
+ });
5735
+ supplement.command("append-media").description(
5736
+ "\u5411\u6307\u5B9A\u4E0B\u6807\u7684\u5DF2\u6709\u8865\u5145\u77E5\u8BC6\u6761\u76EE\u8FFD\u52A0\u56FE\u7247/\u89C6\u9891\u3002\u539F\u6709 question / answer / images / videos \u53CA\u5176\u5B83\u6761\u76EE\u5747\u4E0D\u4E22\u5931"
5737
+ ).requiredOption("--agent <config_id>", "Agent \u914D\u7F6E ID\uFF08\u4ECE agent list \u83B7\u53D6\uFF09").requiredOption("--product-id <product_id>", "\u5546\u54C1 ID\uFF08\u4ECE product list \u83B7\u53D6\uFF09").requiredOption("--index <number>", "\u76EE\u6807\u8865\u5145\u77E5\u8BC6\u6761\u76EE\u4E0B\u6807\uFF08\u4ECE 0 \u5F00\u59CB\uFF0C\u89C1 supplement list\uFF09").option("--image <path-or-url...>", "\u56FE\u7247\u672C\u5730\u8DEF\u5F84\u6216 URL\uFF08\u53EF\u591A\u4E2A\uFF09").option("--video <path-or-url...>", "\u89C6\u9891\u672C\u5730\u8DEF\u5F84\u6216 URL\uFF08\u53EF\u591A\u4E2A\uFF09").option("--dry-run", "\u53EA\u8F93\u51FA\u6700\u7EC8\u5C06\u5199\u5165\u7684 JSON\uFF0C\u4E0D\u5B9E\u9645\u4E0A\u4F20/\u66F4\u65B0").option("--backup <path>", "\u5199\u5165\u524D\u5C06\u66F4\u65B0\u524D\u8865\u5145\u77E5\u8BC6\u5FEB\u7167\u4FDD\u5B58\u5230\u6307\u5B9A\u6587\u4EF6").action(async (opts) => {
5738
+ try {
5739
+ const index = Number(opts.index);
5740
+ if (!Number.isInteger(index) || index < 0) {
5741
+ throw new Error(`--index \u5FC5\u987B\u4E3A\u975E\u8D1F\u6574\u6570\uFF0C\u6536\u5230: ${opts.index}`);
5742
+ }
5743
+ if ((!opts.image || opts.image.length === 0) && (!opts.video || opts.video.length === 0)) {
5744
+ throw new Error("\u8BF7\u81F3\u5C11\u63D0\u4F9B\u4E00\u4E2A --image \u6216 --video");
5745
+ }
5746
+ const ctx = await loadProductSupplement(opts.agent, opts.productId);
5747
+ maybeWriteBackup(opts.backup, ctx.items);
5748
+ const images = await resolveMediaList(opts.image, "image", Boolean(opts.dryRun));
5749
+ const videos = await resolveMediaList(opts.video, "video", Boolean(opts.dryRun));
5750
+ const nextItems = appendMediaToItem(ctx.items, { index, images, videos });
5751
+ if (opts.dryRun) {
5752
+ formatOutput(
5753
+ {
5754
+ success: true,
5755
+ data: {
5756
+ dryRun: true,
5757
+ before: ctx.items,
5758
+ after: nextItems,
5759
+ update_dict: { \u8865\u5145\u77E5\u8BC6: serializeSupplement(nextItems) }
5760
+ }
5761
+ },
5762
+ program2.opts().table
5763
+ );
5764
+ return;
5765
+ }
5766
+ const data = await writeProductSupplement(ctx, nextItems);
5767
+ formatOutput(
5768
+ { success: true, data: { updated: true, index, result: data } },
5769
+ program2.opts().table
5770
+ );
5771
+ } catch (err) {
5772
+ reportCaughtError(err);
5773
+ process.exit(toExitCode(err));
5774
+ }
5775
+ });
5776
+ }
5777
+ function maybeWriteBackup(backupPath, items) {
5778
+ if (!backupPath) return;
5779
+ fs10.writeFileSync(backupPath, JSON.stringify(items, null, 2), "utf-8");
5780
+ }
5320
5781
 
5321
5782
  // src/client/repair-record-api.ts
5322
5783
  var PATH_PREFIX4 = "/api/repair-records";
@@ -5534,7 +5995,7 @@ function registerSACommand(program2) {
5534
5995
  }
5535
5996
 
5536
5997
  // src/commands/special-project.ts
5537
- import fs9 from "fs";
5998
+ import fs11 from "fs";
5538
5999
 
5539
6000
  // src/client/special-project-api.ts
5540
6001
  async function createSpecialProject(input) {
@@ -5577,7 +6038,7 @@ async function upsertSpecialProjects(items, apply) {
5577
6038
  // src/commands/special-project.ts
5578
6039
  function readJsonArg(value) {
5579
6040
  if (value.startsWith("@")) {
5580
- return fs9.readFileSync(value.slice(1), "utf-8");
6041
+ return fs11.readFileSync(value.slice(1), "utf-8");
5581
6042
  }
5582
6043
  return value;
5583
6044
  }
@@ -5654,11 +6115,11 @@ function registerSpecialProjectCommand(program2) {
5654
6115
  }
5655
6116
 
5656
6117
  // src/commands/testset.ts
5657
- import fs11 from "fs";
6118
+ import fs13 from "fs";
5658
6119
 
5659
6120
  // src/client/testset-api.ts
5660
- import fs10 from "fs";
5661
- import path5 from "path";
6121
+ import fs12 from "fs";
6122
+ import path6 from "path";
5662
6123
  function unwrapPaginated(raw, fallbackPageSize) {
5663
6124
  return {
5664
6125
  items: Array.isArray(raw?.data) ? raw?.data : [],
@@ -5686,14 +6147,14 @@ async function listTestSets(query) {
5686
6147
  }
5687
6148
  async function createTestSetFromFile(input) {
5688
6149
  const request = createRequest();
5689
- const fileBytes = fs10.readFileSync(input.filePath);
6150
+ const fileBytes = fs12.readFileSync(input.filePath);
5690
6151
  const formData = new FormData();
5691
6152
  formData.append(
5692
6153
  "file",
5693
6154
  new Blob([fileBytes], {
5694
6155
  type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
5695
6156
  }),
5696
- path5.basename(input.filePath)
6157
+ path6.basename(input.filePath)
5697
6158
  );
5698
6159
  return request(getCustomerServiceUrl(), "/v1/test_sets/file/upload", {
5699
6160
  method: "POST",
@@ -6190,7 +6651,7 @@ function readPromptInput(value) {
6190
6651
  if (!filePath) {
6191
6652
  throw new Error("File path cannot be empty after @");
6192
6653
  }
6193
- return fs11.readFileSync(filePath, "utf-8");
6654
+ return fs13.readFileSync(filePath, "utf-8");
6194
6655
  }
6195
6656
  return value;
6196
6657
  }
@@ -6225,7 +6686,7 @@ function registerTestsetCommand(program2) {
6225
6686
  });
6226
6687
  testset.command("create").description("\u901A\u8FC7 xlsx \u6587\u4EF6\u521B\u5EFA\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").requiredOption("--file <path>", "\u6D4B\u8BD5\u96C6 xlsx \u6587\u4EF6\u8DEF\u5F84").option("--name <text>", "\u6D4B\u8BD5\u96C6\u540D\u79F0\uFF1B\u4E0D\u4F20\u5219\u540E\u7AEF\u53D6\u6587\u4EF6\u540D").option("--description <text>", "\u6D4B\u8BD5\u96C6\u63CF\u8FF0").option("--use-type <text>", "\u6587\u4EF6\u7528\u9014\u6807\u8BC6").action(async (opts) => {
6227
6688
  try {
6228
- if (!fs11.existsSync(opts.file)) {
6689
+ if (!fs13.existsSync(opts.file)) {
6229
6690
  outputError(1, `\u6587\u4EF6\u4E0D\u5B58\u5728: ${opts.file}`);
6230
6691
  process.exit(1);
6231
6692
  }
@@ -6414,6 +6875,7 @@ registerAgentCommand(program);
6414
6875
  registerOpsAgentCommand(program);
6415
6876
  registerSACommand(program);
6416
6877
  registerProductCommand(program);
6878
+ registerMediaCommand(program);
6417
6879
  registerKnowledgeCommand(program);
6418
6880
  registerActivityCommand(program);
6419
6881
  registerIssueCommand(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bty/customer-service-cli",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "description": "AI Customer Service CLI - Agent friendly",
5
5
  "type": "module",
6
6
  "main": "./dist/bin.js",
@@ -12,11 +12,13 @@
12
12
  "CHANGELOG.md"
13
13
  ],
14
14
  "dependencies": {
15
+ "ali-oss": "^6.22.0",
15
16
  "cli-table3": "^0.6.0",
16
17
  "commander": "^14.0.0",
17
18
  "crypto-js": "^4.1.1"
18
19
  },
19
20
  "devDependencies": {
21
+ "@types/ali-oss": "^6.16.11",
20
22
  "@types/crypto-js": "^4.1.1",
21
23
  "@types/node": "^22.10.0",
22
24
  "tsup": "^8.0.0",