@bty/customer-service-cli 0.3.3 → 0.4.2

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 +44 -15
  2. package/dist/bin.js +240 -33
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -314,33 +314,27 @@ Agent 关联的通用规则知识库(`suffix_type=_common`),与 FAQ / 商
314
314
  | `debug ask --agent <id> --text <消息> [--user <用户名>] [--url <图片URL>] [--conversation <会话ID>] [--timeout <秒>]` | 向 Agent 发送消息并等待回复 |
315
315
  | `debug reproduce <record_id> [--agent <id>] [--timeout <秒>] [--dry-run]` | 根据 record_id 复现 Agent 回复(自动获取上下文) |
316
316
  | `debug record <record_id>` | 获取记录的调试信息(flow_info),返回中包含 `trace_id` |
317
- | `debug trace <trace_id>` | 根据 Langfuse Trace ID 获取完整 Trace 详情 |
317
+ | `debug trace <trace_id> [--env <dev\|prod>]` | 根据 Langfuse Trace ID 获取完整 Trace 详情(经服务端代理,默认 `prod`) |
318
318
 
319
319
 
320
320
  `debug ask` 会自动创建调试会话、发送消息、轮询等待 Agent 回复,默认最大等待 30 秒。支持 `--messages` 传入完整消息列表(JSON 或 @文件)重放 issue 对话。
321
321
 
322
322
  `debug reproduce` 根据一条回复记录自动复现 Agent 回复:通过 recordId 获取关联会话和 Agent 配置,拉取完整会话记录并截取目标 record 之前的上下文,使用原始用户名创建新 debug 会话并发送。`--agent` 可指定另一个 Agent 复现同一段上下文(A/B 对比),`--dry-run` 仅输出上下文不发送。
323
323
 
324
- `debug trace` 直接调用 Langfuse REST API(`GET /api/public/traces/:traceId`),使用 Basic Auth 认证。Trace ID 从 `debug record` 返回的 `trace_id` 字段获取。
324
+ `debug trace`(v0.4.0 起,**行为变更**)不再由 CLI 直连 Langfuse REST API,改为请求 customer-servhub-api 的 `GET /v1/debug/langfuse/trace/{trace_id}?env=<env>`,走 CLI 标准 Bearer 鉴权,由服务端按 env 选择 Langfuse 凭据并代理拉取 Trace。Trace ID 从 `debug record` 返回的 `trace_id` 字段获取。
325
325
 
326
- #### Langfuse 配置
327
-
328
- Langfuse 已内置默认连接信息,通常无需配置。如需覆盖,支持环境变量和配置文件两种方式:
329
-
330
- **环境变量(优先级最高):**
326
+ - `--env <dev|prod>`:指定 Langfuse 环境,默认 `prod`。只接受 `dev` / `prod`,传其它值命令直接报错并以非 0 退出(不发请求)。
331
327
 
332
328
  ```bash
333
- export LANGFUSE_BASE_URL="http://192.168.40.10:3000"
334
- export LANGFUSE_PUBLIC_KEY="pk-lf-xxx"
335
- export LANGFUSE_SECRET_KEY="sk-lf-xxx"
336
- ```
337
-
338
- **配置文件(`~/.cs-cli/config.json`):**
329
+ # 默认 prod 环境
330
+ cs-cli debug trace <trace_id>
339
331
 
340
- ```bash
341
- cs-cli config set --langfuse-host <url> --langfuse-public-key <key> --langfuse-secret-key <key>
332
+ # 指定 dev 环境
333
+ cs-cli debug trace <trace_id> --env dev
342
334
  ```
343
335
 
336
+ > **迁移(v0.4.0 BREAKING)**:`config set --langfuse-host / --langfuse-public-key / --langfuse-secret-key` 及 `CLIConfig.langfuseHost / langfusePublicKey / langfuseSecretKey` 已移除;环境变量 `LANGFUSE_BASE_URL / LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY` 在 CLI 侧不再被读取。`~/.cs-cli/config.json` 中相关字段可直接删除。Langfuse 连接信息(含 dev/prod 双环境凭据)现由 customer-servhub-api 服务端的 `system_config` 表 `langfuse_config` 行统一管理(不再读服务端环境变量 / 内置常量),CLI 侧无需也无法再配置。
337
+
344
338
  ### 修复记录 (`repair-record`)
345
339
 
346
340
  | 命令 | 说明 |
@@ -394,6 +388,41 @@ cs-cli config set --langfuse-host <url> --langfuse-public-key <key> --langfuse-s
394
388
 
395
389
  创建记录时 `operator_id` 和 `operator_name` 自动从当前登录用户获取,无需手动指定。
396
390
 
391
+ ### 商品变更事件消费 (`change-consumer`)
392
+
393
+ 客服运维助手订阅并消费商品变更事件(**pull 模式**)。后端走 `/v1/change-consumers/*`,CLI 标准 Bearer 鉴权。默认 `consumer_key=ops_agent_assistant`。
394
+
395
+ 标准工作流:`sub create` 创建订阅 → `delivery list` 轮询待消费项 → `delivery claim` 认领 → 处理 → `delivery complete` 回写。
396
+
397
+ | 命令 | 说明 |
398
+ | --- | --- |
399
+ | `change-consumer sub create --workspace-id <id> [--agent-config <id>] [--events <list>] [--resource-type product] [--consumer-key <key>] [--consumer-name <名称>] [--disabled] [--data <json\|@file>]` | 创建订阅(最简只需 `--workspace-id`;`--agent-config` 缺省表示该工作空间全部商品事件) |
400
+ | `change-consumer sub list [--workspace-id <id>] [--agent-config <id>] [--consumer-key <key>] [--enabled <true\|false>] [--page N] [--page-size N]` | 查询订阅列表 |
401
+ | `change-consumer sub update <subscription_id> --data <json\|@file>` | 更新订阅(`enabled` / `events` / `filters` / `consumer_name`) |
402
+ | `change-consumer delivery list [--workspace-id <id>] [--agent-config <id>] [--consumer-key <key>] [--statuses <list>] [--cursor-created-at <iso>] [--cursor-delivery-id <id>] [--limit N]` | 查询待消费项(不传 `--statuses` 时后端默认返回 pending/claimed/failed;`--cursor-*` 做增量拉取) |
403
+ | `change-consumer delivery claim <delivery_id> --claimed-by <id> [--consumer-key <key>] [--lease-seconds N]` | 认领待消费项(已被其他实例认领且未过期时认领失败) |
404
+ | `change-consumer delivery complete <delivery_id> --status <completed\|ignored\|failed> [--result-action <action>] [--result-note <note>] [--last-error <error>] [--consumer-key <key>] [--data <json\|@file>]` | 回写处理结果 |
405
+
406
+ - `sub create` 默认 `--events product.updated,product.deleted`、`--resource-type product`;`--disabled` 创建为停用态(默认启用)。
407
+ - `delivery complete` 的 `--status` 仅接受 `completed` / `ignored` / `failed`,其它值直接报错退出;不要长时间停留在 `claimed` 不回写。
408
+ - `--result-action` 建议值:`supplemental_knowledge` / `product_association` / `rerun_product_study` / `refresh_recommended_qa` / `ignore_change` / `manual_follow_up`。
409
+ - 各子命令 `--data <json|@file>` 为高级用法,提供时覆盖对应结构化选项。
410
+
411
+ ```bash
412
+ # 1. 创建订阅(该工作空间全部商品 更新/删除 事件)
413
+ cs-cli change-consumer sub create --workspace-id <ws> --events product.updated,product.deleted
414
+
415
+ # 2. 轮询待消费项
416
+ cs-cli change-consumer delivery list --workspace-id <ws> --statuses pending
417
+
418
+ # 3. 认领
419
+ cs-cli change-consumer delivery claim <delivery_id> --claimed-by ops-assistant-1
420
+
421
+ # 4. 处理后回写
422
+ cs-cli change-consumer delivery complete <delivery_id> --status completed \
423
+ --result-action supplemental_knowledge --result-note "已补充商品知识"
424
+ ```
425
+
397
426
  ### 运营监控 (`monitor`)
398
427
 
399
428
  直接调用 `/v1/operation` 底层接口,返回原始数据,由调用方决定如何聚合统计。
package/dist/bin.js CHANGED
@@ -437,9 +437,6 @@ function isTokenExpired(expiresAt) {
437
437
  var DEFAULT_CS_API_URL = "https://customer-servhub-api.betteryeah.com";
438
438
  var DEFAULT_AI_API_URL = "https://ai-api.betteryeah.com";
439
439
  var DEFAULT_CUSTOMER_AGENT_API_URL = "https://customer-agent.bantouyan.com";
440
- var DEFAULT_LANGFUSE_HOST = "http://192.168.40.10:3000";
441
- var DEFAULT_LANGFUSE_PUBLIC_KEY = "pk-lf-1b3aece4-021a-4a81-be4a-abd6334c5929";
442
- var DEFAULT_LANGFUSE_SECRET_KEY = "sk-lf-edc22ac8-5d0a-4f72-b285-6556adfa8f71";
443
440
  function getCustomerServiceUrl() {
444
441
  const envUrl = process.env.CS_CS_API_URL;
445
442
  if (envUrl) return envUrl;
@@ -473,14 +470,6 @@ function getWorkspaceId(overrideWorkspaceId) {
473
470
  if (globalConfig?.defaultWorkspaceId) return globalConfig.defaultWorkspaceId;
474
471
  throw new APIError(1, "\u672A\u8BBE\u7F6E\u5DE5\u4F5C\u7A7A\u95F4\uFF0C\u8BF7\u8FD0\u884C: cs-cli config set-workspace <id>");
475
472
  }
476
- function getLangfuseConfig() {
477
- const config = readConfig();
478
- return {
479
- host: process.env.LANGFUSE_BASE_URL ?? config?.langfuseHost ?? DEFAULT_LANGFUSE_HOST,
480
- publicKey: process.env.LANGFUSE_PUBLIC_KEY ?? config?.langfusePublicKey ?? DEFAULT_LANGFUSE_PUBLIC_KEY,
481
- secretKey: process.env.LANGFUSE_SECRET_KEY ?? config?.langfuseSecretKey ?? DEFAULT_LANGFUSE_SECRET_KEY
482
- };
483
- }
484
473
  function buildCookieHeaders() {
485
474
  const creds = readCredentials();
486
475
  if (!creds) throw new APIError(2, "\u672A\u767B\u5F55\uFF0C\u8BF7\u8FD0\u884C: cs-cli auth login");
@@ -696,6 +685,195 @@ function registerAuthCommand(program2) {
696
685
  });
697
686
  }
698
687
 
688
+ // src/client/change-consumer-api.ts
689
+ var SUBSCRIPTIONS_PATH = "/v1/change-consumers/subscriptions";
690
+ var DEFAULT_CONSUMER_KEY = "ops_agent_assistant";
691
+ async function createSubscription(body) {
692
+ const request = createRequest();
693
+ return request(getCustomerServiceUrl(), SUBSCRIPTIONS_PATH, {
694
+ method: "POST",
695
+ body
696
+ });
697
+ }
698
+ async function listSubscriptions(opts) {
699
+ const request = createRequest();
700
+ const query = {};
701
+ if (opts.workspaceId) query.workspace_id = opts.workspaceId;
702
+ if (opts.customerAgentConfigId) query.customer_agent_config_id = opts.customerAgentConfigId;
703
+ if (opts.consumerKey) query.consumer_key = opts.consumerKey;
704
+ if (opts.enabled !== void 0) query.enabled = opts.enabled;
705
+ if (opts.page) query.page = opts.page;
706
+ if (opts.pageSize) query.page_size = opts.pageSize;
707
+ return request(getCustomerServiceUrl(), SUBSCRIPTIONS_PATH, {
708
+ method: "GET",
709
+ query
710
+ });
711
+ }
712
+ async function updateSubscription(subscriptionId, body) {
713
+ const request = createRequest();
714
+ return request(
715
+ getCustomerServiceUrl(),
716
+ `${SUBSCRIPTIONS_PATH}/${subscriptionId}`,
717
+ { method: "PUT", body }
718
+ );
719
+ }
720
+ async function listDeliveries(consumerKey, opts) {
721
+ const request = createRequest();
722
+ const query = {};
723
+ if (opts.workspaceId) query.workspace_id = opts.workspaceId;
724
+ if (opts.customerAgentConfigId) query.customer_agent_config_id = opts.customerAgentConfigId;
725
+ if (opts.statuses) query.statuses = opts.statuses;
726
+ if (opts.cursorCreatedAt) query.cursor_created_at = opts.cursorCreatedAt;
727
+ if (opts.cursorDeliveryId) query.cursor_delivery_id = opts.cursorDeliveryId;
728
+ if (opts.limit) query.limit = opts.limit;
729
+ return request(
730
+ getCustomerServiceUrl(),
731
+ `/v1/change-consumers/${encodeURIComponent(consumerKey)}/deliveries`,
732
+ { method: "GET", query }
733
+ );
734
+ }
735
+ async function claimDelivery(deliveryId, body) {
736
+ const request = createRequest();
737
+ return request(
738
+ getCustomerServiceUrl(),
739
+ `/v1/change-consumers/deliveries/${deliveryId}/claim`,
740
+ { method: "POST", body }
741
+ );
742
+ }
743
+ async function completeDelivery(deliveryId, body) {
744
+ const request = createRequest();
745
+ return request(
746
+ getCustomerServiceUrl(),
747
+ `/v1/change-consumers/deliveries/${deliveryId}/complete`,
748
+ { method: "POST", body }
749
+ );
750
+ }
751
+
752
+ // src/commands/change-consumer.ts
753
+ function registerChangeConsumerCommand(program2) {
754
+ const cc = program2.command("change-consumer").description(
755
+ "\u5546\u54C1\u53D8\u66F4\u4E8B\u4EF6\u6D88\u8D39 \u2014\u2014 \u5BA2\u670D\u8FD0\u7EF4\u52A9\u624B\u8BA2\u9605\u5E76\u6D88\u8D39\u5546\u54C1\u53D8\u66F4\uFF08pull \u6A21\u5F0F\uFF09\u3002\u5DE5\u4F5C\u6D41\uFF1Asub create \u521B\u5EFA\u8BA2\u9605 \u2192 delivery list \u8F6E\u8BE2\u5F85\u6D88\u8D39\u9879 \u2192 delivery claim \u8BA4\u9886 \u2192 \u63D0\u793A\u7528\u6237\u5E76\u5904\u7406 \u2192 delivery complete \u56DE\u5199\u3002\u9ED8\u8BA4 consumer_key=" + DEFAULT_CONSUMER_KEY
756
+ );
757
+ const sub = cc.command("sub").description("\u8BA2\u9605\u7BA1\u7406\uFF08subscription\uFF09");
758
+ sub.command("create").description(
759
+ "\u521B\u5EFA\u8BA2\u9605\u3002\u6700\u7B80\u914D\u7F6E\u53EA\u9700 workspace-id + events\uFF1Bcustomer-agent-config-id \u4E0D\u4F20\u8868\u793A\u8BE5 Agent \u5168\u90E8\u5546\u54C1\u4E8B\u4EF6"
760
+ ).option("--consumer-key <key>", "consumer_key", DEFAULT_CONSUMER_KEY).option("--consumer-name <name>", "consumer \u663E\u793A\u540D", "\u5BA2\u670D\u8FD0\u7EF4\u52A9\u624B").requiredOption("--workspace-id <workspace_id>", "\u5DE5\u4F5C\u7A7A\u95F4 ID").option("--agent-config <id>", "customer_agent_config_id\uFF08\u53EF\u9009\uFF0C\u7F3A\u7701\u8868\u793A\u5168\u90E8\uFF09").option("--resource-type <type>", "\u8D44\u6E90\u7C7B\u578B", "product").option(
761
+ "--events <list>",
762
+ "\u9017\u53F7\u5206\u9694\u4E8B\u4EF6\u5217\u8868\uFF0C\u5982 product.updated,product.deleted",
763
+ "product.updated,product.deleted"
764
+ ).option("--disabled", "\u521B\u5EFA\u4E3A\u505C\u7528\u72B6\u6001\uFF08\u9ED8\u8BA4\u542F\u7528\uFF09", false).option("--data <json>", "\u9AD8\u7EA7\uFF1A\u5B8C\u6574\u8BA2\u9605 JSON \u6216 @\u6587\u4EF6\uFF0C\u63D0\u4F9B\u65F6\u8986\u76D6\u4E0A\u8FF0\u9009\u9879").action(async (opts) => {
765
+ try {
766
+ const body = opts.data ? parseDataOption(opts.data) : {
767
+ consumer_key: opts.consumerKey,
768
+ consumer_name: opts.consumerName,
769
+ mode: "pull",
770
+ workspace_id: opts.workspaceId,
771
+ customer_agent_config_id: opts.agentConfig,
772
+ resource_type: opts.resourceType,
773
+ enabled: !opts.disabled,
774
+ events: String(opts.events).split(",").map((e) => e.trim()).filter(Boolean)
775
+ };
776
+ const data = await createSubscription(body);
777
+ formatOutput({ success: true, data }, program2.opts().table);
778
+ } catch (err) {
779
+ reportCaughtError(err);
780
+ process.exit(toExitCode(err));
781
+ }
782
+ });
783
+ sub.command("list").description("\u67E5\u8BE2\u8BA2\u9605\u5217\u8868").option("--workspace-id <workspace_id>", "\u6309\u5DE5\u4F5C\u7A7A\u95F4\u7B5B\u9009").option("--agent-config <id>", "\u6309 customer_agent_config_id \u7B5B\u9009").option("--consumer-key <key>", "\u6309 consumer_key \u7B5B\u9009", DEFAULT_CONSUMER_KEY).option("--enabled <bool>", "\u6309\u542F\u7528\u72B6\u6001\u7B5B\u9009 true/false").option("--page <number>", "\u9875\u7801", "1").option("--page-size <number>", "\u6BCF\u9875\u6570\u91CF", "20").action(async (opts) => {
784
+ try {
785
+ const data = await listSubscriptions({
786
+ workspaceId: opts.workspaceId,
787
+ customerAgentConfigId: opts.agentConfig,
788
+ consumerKey: opts.consumerKey,
789
+ enabled: opts.enabled === void 0 ? void 0 : opts.enabled === "true",
790
+ page: Number(opts.page),
791
+ pageSize: Number(opts.pageSize)
792
+ });
793
+ formatOutput({ success: true, data }, program2.opts().table);
794
+ } catch (err) {
795
+ reportCaughtError(err);
796
+ process.exit(toExitCode(err));
797
+ }
798
+ });
799
+ sub.command("update").description("\u66F4\u65B0\u8BA2\u9605\u3002\u53EF\u66F4\u65B0 enabled / events / filters / consumer_name").argument("<subscription_id>", "\u8BA2\u9605 ID\uFF08\u4ECE sub list \u83B7\u53D6\uFF09").requiredOption(
800
+ "--data <json>",
801
+ 'JSON \u6216 @\u6587\u4EF6\uFF0C\u5982 {"enabled":true,"filters":{"changed_fields_any":["sku"]}}'
802
+ ).action(async (subscriptionId, opts) => {
803
+ try {
804
+ const body = parseDataOption(opts.data);
805
+ const data = await updateSubscription(subscriptionId, body);
806
+ formatOutput({ success: true, data }, program2.opts().table);
807
+ } catch (err) {
808
+ reportCaughtError(err);
809
+ process.exit(toExitCode(err));
810
+ }
811
+ });
812
+ const delivery = cc.command("delivery").description("\u5F85\u6D88\u8D39\u9879\u7BA1\u7406\uFF08delivery\uFF09");
813
+ delivery.command("list").description(
814
+ "\u67E5\u8BE2\u5F85\u6D88\u8D39\u9879\u3002\u4E0D\u4F20 --statuses \u65F6\u540E\u7AEF\u9ED8\u8BA4\u8FD4\u56DE pending/claimed/failed\uFF1B\u7528 --cursor-* \u505A\u589E\u91CF\u62C9\u53D6"
815
+ ).option("--consumer-key <key>", "consumer_key", DEFAULT_CONSUMER_KEY).option("--workspace-id <workspace_id>", "\u6309\u5DE5\u4F5C\u7A7A\u95F4\u7B5B\u9009").option("--agent-config <id>", "\u6309 customer_agent_config_id \u7B5B\u9009").option("--statuses <list>", "\u72B6\u6001\u8FC7\u6EE4\uFF0C\u9017\u53F7\u5206\u9694\uFF0C\u5982 pending,claimed").option("--cursor-created-at <iso>", "\u589E\u91CF\u6E38\u6807\uFF1A\u4E0A\u4E00\u9875 next_cursor.cursor_created_at").option("--cursor-delivery-id <id>", "\u589E\u91CF\u6E38\u6807\uFF1A\u4E0A\u4E00\u9875 next_cursor.cursor_delivery_id").option("--limit <number>", "\u5355\u6B21\u62C9\u53D6\u6761\u6570", "20").action(async (opts) => {
816
+ try {
817
+ const data = await listDeliveries(opts.consumerKey, {
818
+ workspaceId: opts.workspaceId,
819
+ customerAgentConfigId: opts.agentConfig,
820
+ statuses: opts.statuses,
821
+ cursorCreatedAt: opts.cursorCreatedAt,
822
+ cursorDeliveryId: opts.cursorDeliveryId,
823
+ limit: Number(opts.limit)
824
+ });
825
+ formatOutput({ success: true, data }, program2.opts().table);
826
+ } catch (err) {
827
+ reportCaughtError(err);
828
+ process.exit(toExitCode(err));
829
+ }
830
+ });
831
+ delivery.command("claim").description("\u8BA4\u9886\u5F85\u6D88\u8D39\u9879\u3002\u5DF2\u88AB\u5176\u4ED6\u5B9E\u4F8B\u8BA4\u9886\u4E14\u672A\u8FC7\u671F\u65F6\u8BA4\u9886\u4F1A\u5931\u8D25").argument("<delivery_id>", "\u5F85\u6D88\u8D39\u9879 ID\uFF08\u4ECE delivery list \u83B7\u53D6\uFF09").option("--consumer-key <key>", "consumer_key", DEFAULT_CONSUMER_KEY).requiredOption("--claimed-by <id>", "\u8BA4\u9886\u8005\u6807\u8BC6\uFF0C\u5EFA\u8BAE\u4F20\u8FD0\u7EF4\u52A9\u624B\u5B9E\u4F8B\u6807\u8BC6").option("--lease-seconds <number>", "\u8BA4\u9886\u6709\u6548\u671F\uFF08\u79D2\uFF09", "300").action(async (deliveryId, opts) => {
832
+ try {
833
+ const data = await claimDelivery(deliveryId, {
834
+ consumer_key: opts.consumerKey,
835
+ claimed_by: opts.claimedBy,
836
+ lease_seconds: Number(opts.leaseSeconds)
837
+ });
838
+ formatOutput({ success: true, data }, program2.opts().table);
839
+ } catch (err) {
840
+ reportCaughtError(err);
841
+ process.exit(toExitCode(err));
842
+ }
843
+ });
844
+ delivery.command("complete").description(
845
+ "\u56DE\u5199\u5904\u7406\u7ED3\u679C\u3002status \u53D6 completed/ignored/failed\uFF1B\u4E0D\u8981\u957F\u65F6\u95F4\u505C\u7559\u5728 claimed \u4E0D\u56DE\u5199"
846
+ ).argument("<delivery_id>", "\u5F85\u6D88\u8D39\u9879 ID\uFF08\u4ECE delivery list \u83B7\u53D6\uFF09").option("--consumer-key <key>", "consumer_key", DEFAULT_CONSUMER_KEY).requiredOption(
847
+ "--status <status>",
848
+ "completed | ignored | failed"
849
+ ).option(
850
+ "--result-action <action>",
851
+ "\u5EFA\u8BAE\u503C: supplemental_knowledge / product_association / rerun_product_study / refresh_recommended_qa / ignore_change / manual_follow_up"
852
+ ).option("--result-note <note>", "\u5904\u7406\u8BF4\u660E").option("--last-error <error>", "status=failed \u65F6\u7684\u9519\u8BEF\u63CF\u8FF0").option("--data <json>", "\u9AD8\u7EA7\uFF1A\u5B8C\u6574\u56DE\u5199 JSON \u6216 @\u6587\u4EF6\uFF0C\u63D0\u4F9B\u65F6\u8986\u76D6\u4E0A\u8FF0\u9009\u9879").action(async (deliveryId, opts) => {
853
+ try {
854
+ const status = opts.status;
855
+ if (!["completed", "ignored", "failed"].includes(status)) {
856
+ reportCaughtError(
857
+ new Error("--status \u53EA\u80FD\u662F completed / ignored / failed")
858
+ );
859
+ process.exit(1);
860
+ }
861
+ const body = opts.data ? parseDataOption(opts.data) : {
862
+ consumer_key: opts.consumerKey,
863
+ status,
864
+ result_action: opts.resultAction,
865
+ result_note: opts.resultNote,
866
+ last_error: opts.lastError
867
+ };
868
+ const data = await completeDelivery(deliveryId, body);
869
+ formatOutput({ success: true, data }, program2.opts().table);
870
+ } catch (err) {
871
+ reportCaughtError(err);
872
+ process.exit(toExitCode(err));
873
+ }
874
+ });
875
+ }
876
+
699
877
  // src/client/chat-history-api.ts
700
878
  async function listChatHistory(opts) {
701
879
  const request = createRequest();
@@ -750,14 +928,11 @@ function registerConfigCommand(program2) {
750
928
  const config = program2.command("config").description(
751
929
  "\u914D\u7F6E\u7BA1\u7406 \u2014\u2014 API \u5730\u5740\u3001\u9ED8\u8BA4\u5DE5\u4F5C\u7A7A\u95F4\u7B49\u6301\u4E45\u5316\u8BBE\u7F6E\u3002\u652F\u6301\u5168\u5C40\u914D\u7F6E\u548C\u76EE\u5F55\u7EA7\u672C\u5730\u914D\u7F6E\uFF08.cs-cli.json\uFF09"
752
930
  );
753
- config.command("set").description("\u8BBE\u7F6E API \u5730\u5740").option("--cs-api <url>", "\u5BA2\u670D API \u5730\u5740").option("--ai-api <url>", "AI API \u5730\u5740").option("--agent-api <url>", "Customer Agent API \u5730\u5740").option("--langfuse-host <url>", "Langfuse \u5730\u5740").option("--langfuse-public-key <key>", "Langfuse Public Key").option("--langfuse-secret-key <key>", "Langfuse Secret Key").action((opts) => {
931
+ config.command("set").description("\u8BBE\u7F6E API \u5730\u5740").option("--cs-api <url>", "\u5BA2\u670D API \u5730\u5740").option("--ai-api <url>", "AI API \u5730\u5740").option("--agent-api <url>", "Customer Agent API \u5730\u5740").action((opts) => {
754
932
  const updates = {};
755
933
  if (opts.csApi) updates.customerServiceApiUrl = opts.csApi;
756
934
  if (opts.aiApi) updates.aiApiUrl = opts.aiApi;
757
935
  if (opts.agentApi) updates.customerAgentApiUrl = opts.agentApi;
758
- if (opts.langfuseHost) updates.langfuseHost = opts.langfuseHost;
759
- if (opts.langfusePublicKey) updates.langfusePublicKey = opts.langfusePublicKey;
760
- if (opts.langfuseSecretKey) updates.langfuseSecretKey = opts.langfuseSecretKey;
761
936
  writeConfig(updates);
762
937
  formatOutput({ success: true, data: readConfig() }, program2.opts().table);
763
938
  });
@@ -1103,22 +1278,16 @@ async function getFlowExecuteLog(opts) {
1103
1278
  const log = await logRes.json();
1104
1279
  return { executeLog, log };
1105
1280
  }
1106
- async function getLangfuseTrace(traceId) {
1107
- const { host, publicKey, secretKey } = getLangfuseConfig();
1108
- const url = `${host}/api/public/traces/${traceId}`;
1109
- const basicAuth = Buffer.from(`${publicKey}:${secretKey}`).toString("base64");
1110
- const res = await fetch(url, {
1111
- method: "GET",
1112
- headers: {
1113
- Authorization: `Basic ${basicAuth}`,
1114
- "Content-Type": "application/json"
1281
+ async function getLangfuseTrace(traceId, env) {
1282
+ const request = createRequest();
1283
+ return request(
1284
+ getCustomerServiceUrl(),
1285
+ `/v1/debug/langfuse/trace/${encodeURIComponent(traceId)}`,
1286
+ {
1287
+ method: "GET",
1288
+ query: { env }
1115
1289
  }
1116
- });
1117
- if (!res.ok) {
1118
- const text = await res.text().catch(() => "");
1119
- throw new Error(`Langfuse API error ${res.status}: ${text}`);
1120
- }
1121
- return res.json();
1290
+ );
1122
1291
  }
1123
1292
 
1124
1293
  // src/commands/debug.ts
@@ -1292,9 +1461,13 @@ function registerDebugCommand(program2) {
1292
1461
  });
1293
1462
  debug.command("trace").description(
1294
1463
  "\u6839\u636E Langfuse Trace ID \u83B7\u53D6\u5B8C\u6574\u7684 Trace \u8BE6\u60C5\uFF08\u4ECE debug record \u8FD4\u56DE\u7684 flow_info \u4E2D\u63D0\u53D6 traceId\uFF09"
1295
- ).argument("<trace_id>", "Langfuse Trace ID\uFF08\u4ECE flow_info \u4E2D\u83B7\u53D6\uFF09").action(async (traceId) => {
1464
+ ).argument("<trace_id>", "Langfuse Trace ID\uFF08\u4ECE flow_info \u4E2D\u83B7\u53D6\uFF09").option("--env <env>", "langfuse \u73AF\u5883 dev|prod", "prod").action(async (traceId, opts) => {
1296
1465
  try {
1297
- const data = await getLangfuseTrace(traceId);
1466
+ if (opts.env !== "dev" && opts.env !== "prod") {
1467
+ outputError(1, "--env \u4EC5\u652F\u6301 dev \u6216 prod");
1468
+ process.exit(1);
1469
+ }
1470
+ const data = await getLangfuseTrace(traceId, opts.env);
1298
1471
  formatOutput({ success: true, data }, program2.opts().table);
1299
1472
  } catch (err) {
1300
1473
  reportCaughtError(err);
@@ -1504,6 +1677,9 @@ async function updateIssue(issueId, data) {
1504
1677
  body: data
1505
1678
  });
1506
1679
  }
1680
+ async function updateIssueOwner(issueId, params) {
1681
+ return updateIssue(issueId, params);
1682
+ }
1507
1683
  async function listIssueComments(issueId) {
1508
1684
  const request = createRequest();
1509
1685
  return request(getCustomerServiceUrl(), `/v1/issues/${issueId}/comments`, {
@@ -1606,6 +1782,36 @@ function registerIssueCommand(program2) {
1606
1782
  process.exit(toExitCode(err));
1607
1783
  }
1608
1784
  });
1785
+ issue.command("update-owner").description(
1786
+ "\u66F4\u65B0\u5DE5\u5355\u8D1F\u8D23\u4EBA\uFF08owner\uFF09\u3002\u4EC5\u4E8C\u9009\u4E00\uFF1A--owner <user_id> \u6216 --owner-phone <\u624B\u673A\u53F7>\uFF0C\u540E\u8005\u7531\u670D\u52A1\u7AEF\u89E3\u6790\u4E3A user_id\u3002\u6CE8\u610F\uFF1A\u540E\u7AEF\u9700\u5F00\u542F\u5BF9 owner / owner_phone \u5B57\u6BB5\u7684\u652F\u6301\u540E\u8BE5\u547D\u4EE4\u624D\u80FD\u751F\u6548"
1787
+ ).argument("<issue_id>", "\u5DE5\u5355 ID\uFF08\u4ECE issue list \u83B7\u53D6\uFF09").option("--owner <user_id>", "\u8D1F\u8D23\u4EBA user_id\uFF08\u6B63\u6574\u6570\uFF09").option("--owner-phone <phone>", "\u8D1F\u8D23\u4EBA\u624B\u673A\u53F7\uFF08\u7531\u670D\u52A1\u7AEF\u89E3\u6790\u4E3A user_id\uFF09").action(async (issueId, opts) => {
1788
+ try {
1789
+ const hasOwner = opts.owner !== void 0;
1790
+ const hasPhone = opts.ownerPhone !== void 0;
1791
+ if (hasOwner === hasPhone) {
1792
+ throw new Error("\u5FC5\u987B\u4E14\u4EC5\u80FD\u63D0\u4F9B --owner \u6216 --owner-phone \u5176\u4E2D\u4E00\u4E2A");
1793
+ }
1794
+ const body = {};
1795
+ if (hasOwner) {
1796
+ const ownerId = Number(opts.owner);
1797
+ if (!Number.isInteger(ownerId) || ownerId <= 0) {
1798
+ throw new Error(`--owner \u5FC5\u987B\u4E3A\u6B63\u6574\u6570\uFF0C\u6536\u5230: ${opts.owner}`);
1799
+ }
1800
+ body.owner = ownerId;
1801
+ } else {
1802
+ const phone = String(opts.ownerPhone).trim();
1803
+ if (!phone) {
1804
+ throw new Error("--owner-phone \u4E0D\u80FD\u4E3A\u7A7A");
1805
+ }
1806
+ body.owner_phone = phone;
1807
+ }
1808
+ const data = await updateIssueOwner(issueId, body);
1809
+ formatOutput({ success: true, data }, program2.opts().table);
1810
+ } catch (err) {
1811
+ reportCaughtError(err);
1812
+ process.exit(toExitCode(err));
1813
+ }
1814
+ });
1609
1815
  issue.command("stats").description("\u8DE8\u5DE5\u4F5C\u7A7A\u95F4 Issue \u6570\u91CF\u7EDF\u8BA1\uFF0C\u8FD4\u56DE\u6BCF\u4E2A\u5DE5\u4F5C\u7A7A\u95F4\u7684 open/closed/total \u8BA1\u6570").option("--start <date>", "\u5F00\u59CB\u65E5\u671F\uFF08YYYY-MM-DD \u683C\u5F0F\uFF0C\u5982 2026-03-01\uFF09").option("--end <date>", "\u7ED3\u675F\u65E5\u671F\uFF08YYYY-MM-DD \u683C\u5F0F\uFF0C\u5982 2026-03-23\uFF09").action(async (opts) => {
1610
1816
  try {
1611
1817
  const data = await getIssueStats({
@@ -3124,7 +3330,7 @@ var require2 = createRequire(import.meta.url);
3124
3330
  var { version } = require2("../package.json");
3125
3331
  var program = new Command();
3126
3332
  program.name("cs-cli").description(
3127
- "BetterYeah AI \u5BA2\u670D\u5E73\u53F0 CLI\u3002\u6838\u5FC3\u6982\u5FF5\uFF1Aworkspace\uFF08\u5DE5\u4F5C\u7A7A\u95F4\uFF09\u2192 agent\uFF08AI \u5BA2\u670D\u673A\u5668\u4EBA\uFF09\u2192 SA/product/FAQ\uFF08\u77E5\u8BC6\u914D\u7F6E\uFF09\u2192 issue\uFF08\u5DE5\u5355\uFF09\u2192 debug\uFF08\u8C03\u8BD5\u9A8C\u8BC1\uFF09\u2192 monitor\uFF08\u8FD0\u8425\u76D1\u63A7\uFF09\u2192 repair-record\uFF08\u4FEE\u590D\u5BA1\u8BA1\uFF09\u3002\u6240\u6709\u547D\u4EE4\u9ED8\u8BA4\u8F93\u51FA JSON\uFF0C\u8FFD\u52A0 --table \u53EF\u5207\u6362\u4E3A\u4EBA\u7C7B\u53EF\u8BFB\u8868\u683C"
3333
+ "BetterYeah AI \u5BA2\u670D\u5E73\u53F0 CLI\u3002\u6838\u5FC3\u6982\u5FF5\uFF1Aworkspace\uFF08\u5DE5\u4F5C\u7A7A\u95F4\uFF09\u2192 agent\uFF08AI \u5BA2\u670D\u673A\u5668\u4EBA\uFF09\u2192 SA/product/FAQ\uFF08\u77E5\u8BC6\u914D\u7F6E\uFF09\u2192 issue\uFF08\u5DE5\u5355\uFF09\u2192 debug\uFF08\u8C03\u8BD5\u9A8C\u8BC1\uFF09\u2192 monitor\uFF08\u8FD0\u8425\u76D1\u63A7\uFF09\u2192 repair-record\uFF08\u4FEE\u590D\u5BA1\u8BA1\uFF09\u2192 change-consumer\uFF08\u5546\u54C1\u53D8\u66F4\u4E8B\u4EF6\u6D88\u8D39\uFF09\u3002\u6240\u6709\u547D\u4EE4\u9ED8\u8BA4\u8F93\u51FA JSON\uFF0C\u8FFD\u52A0 --table \u53EF\u5207\u6362\u4E3A\u4EBA\u7C7B\u53EF\u8BFB\u8868\u683C"
3128
3334
  ).version(version).option("--table", "\u4EE5\u8868\u683C\u5F62\u5F0F\u8F93\u51FA\uFF08\u9ED8\u8BA4 JSON\uFF09\u3002\u4EBA\u5DE5\u67E5\u770B\u65F6\u4F7F\u7528", false).option("--workspace <id>", "\u4E34\u65F6\u8986\u76D6\u9ED8\u8BA4\u5DE5\u4F5C\u7A7A\u95F4 ID\uFF0C\u4E0D\u4FEE\u6539\u6301\u4E45\u5316\u914D\u7F6E").option(
3129
3335
  "--request-timeout <ms>",
3130
3336
  "\u5355\u4E2A HTTP \u8BF7\u6C42\u8D85\u65F6\uFF08\u6BEB\u79D2\uFF09\uFF0C\u590D\u6742 Agent \u56DE\u590D\u5EFA\u8BAE\u8C03\u9AD8\u3002\u4E0D\u8981\u5199 --timeout\uFF0C\u90A3\u662F\u5B50\u547D\u4EE4\u7684\u8F6E\u8BE2\u7A97\u53E3\uFF08\u79D2\uFF09",
@@ -3161,6 +3367,7 @@ registerDebugCommand(program);
3161
3367
  registerMonitorCommand(program);
3162
3368
  registerRepairRecordCommand(program);
3163
3369
  registerOperationsRecordCommand(program);
3370
+ registerChangeConsumerCommand(program);
3164
3371
  process.on("uncaughtException", (err) => {
3165
3372
  outputError(3, err.message);
3166
3373
  process.exit(3);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bty/customer-service-cli",
3
- "version": "0.3.3",
3
+ "version": "0.4.2",
4
4
  "description": "AI Customer Service CLI - Agent friendly",
5
5
  "type": "module",
6
6
  "main": "./dist/bin.js",