@bty/customer-service-cli 0.5.4 → 0.6.0

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,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0 (2026-06-09)
4
+
5
+ - 新增 `node-template describe [--stage <s>]`:节点编写说明书命令(AI 编写前必读)——输出每个节点的职责/可编辑面/输出契约/平台锁定段,以及该节点可 `@引用` 的变量目录。纯静态、与具体 agent 无关,无需 `--agent`。
6
+ - `node-template` 节点 stage 新增 `image_understanding` 伪 stage(图片理解方法论可配置化,spec 2026-06-08):`describe` / `get` / `set-draft` / `publish` / `versions` / `version` / `restore` 均纳入;发布前 `{{}}` 合法性校验白名单同步放行。
7
+ - **下线 `node-template enroll` / `enroll-status` 命令**:高级模式开关从「运维 CLI 灰度准入」改为「客户在 cs-client 内自助开关」(spec 2026-06-09),CLI 不再提供开启路径;servhub `enrolled` 端点仍在,供 cs-client 使用。
8
+
9
+ ## 0.5.5 (2026-06-07)
10
+
11
+ - 修复 `debug ask --messages` 重放多轮对话时,历史消息时间戳晚于当前时间导致本次 Agent 回复插入历史消息中间、review 页面消息乱序的问题。
12
+ - 发送前将整段历史消息的时间戳统一平移,使最晚消息对齐当前时间;保留消息原有相对顺序与时间间隔,且不修改调用方传入的原始消息对象。
13
+
3
14
  ## 0.5.4 (2026-06-03)
4
15
 
5
16
  - `ops-agent` 新增 `conversations` 子命令:`ops-agent conversations [--agent-id <id>] [--customer-id <id>] [--page N] [--page-size N]`,对接 `GET /v1/ops-agent/conversations`,按当前 workspace 查询客服助手会话列表,返回 `workspace_id`、`customer_id`、`title`、`agent_id`、`created_at`、`updated_at`。
package/README.md CHANGED
@@ -299,6 +299,24 @@ cs-cli activity delete 6
299
299
  cs-cli activity v2-enabled --agent <cfg_id> --enabled true
300
300
  ```
301
301
 
302
+ ### 节点模板 (`node-template`)
303
+
304
+ 高级模板(4 真节点提示词 + `image_understanding` 图片理解方法论伪 stage)操作:编写说明书 + 获取内容 + 改动发布 + 历史/回溯。服务端 `customer-servhub-api /v1/customer-agent/{config_id}/customer_config/node-templates*`;契约见 [`docs/devkit/specs/2026-06-08-节点模板-cli-操作面.md`](../../docs/devkit/specs/2026-06-08-节点模板-cli-操作面.md)。**节点 stage:`planner|composer|qa_inspector|image_understanding`(`tool_selector` 暂不放出)。**
305
+
306
+ > 高级模式开关已改为**客户自助**(在 cs-client 内开关),CLI 不再提供 `enroll` / `enroll-status` 开启路径(已下线)。
307
+
308
+ | 命令 | 说明 |
309
+ | --- | --- |
310
+ | `node-template describe [--stage <s>]` | 节点编写说明书(AI 编写前必读):每节点职责/可编辑面/输出契约/平台锁定段 + 可 `@引用` 变量目录;纯静态、无需 `--agent` |
311
+ | `node-template get --agent <id> [--stage <s>]` | 获取内容(不传 stage=全部已开放节点) |
312
+ | `node-template set-draft --agent <id> --stage <s> (--content <t>\|--content-file <p>\|--stdin)` | 写单节点草稿(整段覆盖;空串=回落出厂默认) |
313
+ | `node-template publish --agent <id> --stage <s> [--force]` | 发布草稿上线(发布前校验 `{{}}`,`--force` 越过) |
314
+ | `node-template versions --agent <id> --stage <s>` | 列该节点历史 |
315
+ | `node-template version --agent <id> --stage <s> --version <n>` | 取某版本正文 |
316
+ | `node-template restore --agent <id> --stage <s> --version <n>` | 回溯某版本成草稿(需再 publish) |
317
+
318
+ 典型流(该 agent 已由客户在 cs-client 开启高级模式后):`get` 看现状 → `set-draft` 改草稿 → `publish` 上线。`publish` 会拒绝含非法 `{{插值}}` 名的草稿(笔误防呆)。
319
+
302
320
  ### 工单管理 (`issue`)
303
321
 
304
322
 
package/dist/bin.js CHANGED
@@ -1628,6 +1628,23 @@ function registerDashboardCommand(program2) {
1628
1628
 
1629
1629
  // src/client/debug-api.ts
1630
1630
  var DEFAULT_FLOW_WORKSPACE_ID = "531c14d1ece047cbaec22914e1f1364d";
1631
+ function toTimestampSeconds(value) {
1632
+ const timestamp = typeof value === "string" && Number.isNaN(Number(value)) ? Math.floor(new Date(value).getTime() / 1e3) : Math.floor(Number(value));
1633
+ if (!Number.isFinite(timestamp)) return null;
1634
+ return timestamp > 9999999999 ? Math.floor(timestamp / 1e3) : timestamp;
1635
+ }
1636
+ function rebaseMsgListTimestamps(msgList) {
1637
+ const timestamps = msgList.map(
1638
+ (message) => typeof message === "object" && message !== null ? toTimestampSeconds(message.timestamp) : null
1639
+ ).filter((timestamp) => timestamp !== null);
1640
+ if (timestamps.length === 0) return msgList;
1641
+ const offset = Math.floor(Date.now() / 1e3) - Math.max(...timestamps);
1642
+ return msgList.map((message) => {
1643
+ if (typeof message !== "object" || message === null) return message;
1644
+ const timestamp = toTimestampSeconds(message.timestamp);
1645
+ return timestamp === null ? message : { ...message, timestamp: timestamp + offset };
1646
+ });
1647
+ }
1631
1648
  async function createDebugConversation(opts) {
1632
1649
  const request = createRequest();
1633
1650
  return request(getCustomerServiceUrl(), "/v1/chat/conversation", {
@@ -1642,7 +1659,7 @@ async function createDebugConversation(opts) {
1642
1659
  async function sendAgentMessage(opts) {
1643
1660
  let msgList;
1644
1661
  if (opts.msgList) {
1645
- msgList = opts.msgList;
1662
+ msgList = rebaseMsgListTimestamps(opts.msgList);
1646
1663
  } else {
1647
1664
  if (opts.text == null) {
1648
1665
  throw new Error("sendAgentMessage: either msgList or text must be provided");
@@ -3769,6 +3786,317 @@ function registerMonitorCommand(program2) {
3769
3786
  });
3770
3787
  }
3771
3788
 
3789
+ // src/commands/node-template.ts
3790
+ import { readFileSync } from "fs";
3791
+
3792
+ // src/client/node-template-api.ts
3793
+ function base(configId) {
3794
+ return `/v1/customer-agent/${configId}/customer_config/node-templates`;
3795
+ }
3796
+ async function getNodeTemplates(configId) {
3797
+ const request = createRequest();
3798
+ return request(getCustomerServiceUrl(), base(configId), {
3799
+ method: "GET"
3800
+ });
3801
+ }
3802
+ async function setNodeTemplateDraft(configId, stage, content) {
3803
+ const request = createRequest();
3804
+ return request(
3805
+ getCustomerServiceUrl(),
3806
+ `${base(configId)}/${stage}/draft`,
3807
+ { method: "PUT", body: { content } }
3808
+ );
3809
+ }
3810
+ async function publishNodeTemplate(configId, stage) {
3811
+ const request = createRequest();
3812
+ return request(
3813
+ getCustomerServiceUrl(),
3814
+ `${base(configId)}/${stage}/publish`,
3815
+ { method: "POST", body: {} }
3816
+ );
3817
+ }
3818
+ async function listNodeTemplateVersions(configId, stage) {
3819
+ const request = createRequest();
3820
+ return request(
3821
+ getCustomerServiceUrl(),
3822
+ `${base(configId)}/${stage}/versions`,
3823
+ { method: "GET" }
3824
+ );
3825
+ }
3826
+ async function getNodeTemplateVersion(configId, stage, versionNo) {
3827
+ const request = createRequest();
3828
+ return request(
3829
+ getCustomerServiceUrl(),
3830
+ `${base(configId)}/${stage}/versions/${versionNo}`,
3831
+ { method: "GET" }
3832
+ );
3833
+ }
3834
+ async function restoreNodeTemplateVersion(configId, stage, versionNo) {
3835
+ const request = createRequest();
3836
+ return request(
3837
+ getCustomerServiceUrl(),
3838
+ `${base(configId)}/${stage}/versions/${versionNo}/restore`,
3839
+ { method: "POST", body: {} }
3840
+ );
3841
+ }
3842
+
3843
+ // src/lib/node-template-references.ts
3844
+ var NODE_TEMPLATE_REFERENCES = [
3845
+ // ── 数据源 fact_alias(当轮实时事实,运行时取最新数据) ──
3846
+ { name: "product_data", label: "\u5546\u54C1\u4E8B\u5B9E", desc: "\u5546\u54C1\u53C2\u6570/\u5E93\u5B58/\u4EF7\u683C\u7B49\u5546\u54C1\u4E8B\u5B9E\u6570\u636E", category: "fact_alias", stages: ["tool_selector", "composer"] },
3847
+ { name: "order_status", label: "\u8BA2\u5355/\u7269\u6D41\u72B6\u6001", desc: "\u8BA2\u5355\u72B6\u6001\u3001\u7269\u6D41\u8FDB\u5EA6\u7B49", category: "fact_alias", stages: ["tool_selector", "composer"] },
3848
+ { name: "faq_knowledge", label: "FAQ/\u5386\u53F2\u95EE\u7B54", desc: "\u5E97\u94FA\u5E38\u89C1\u95EE\u7B54\u4E0E\u5386\u53F2\u95EE\u7B54", category: "fact_alias", stages: ["tool_selector", "composer"] },
3849
+ { name: "common_knowledge", label: "\u901A\u7528\u77E5\u8BC6", desc: "\u5E97\u94FA\u901A\u7528\u77E5\u8BC6/\u8865\u5145\u77E5\u8BC6", category: "fact_alias", stages: ["tool_selector", "composer"] },
3850
+ { name: "activity_knowledge", label: "\u6D3B\u52A8\u77E5\u8BC6", desc: "\u5F53\u524D\u5E97\u94FA\u6D3B\u52A8\u4FE1\u606F", category: "fact_alias", stages: ["tool_selector", "composer"] },
3851
+ { name: "product_recommendation", label: "\u5546\u54C1\u63A8\u8350", desc: "\u627E\u76F8\u4F3C\u6B3E/\u63A8\u8350\u5546\u54C1", category: "fact_alias", stages: ["tool_selector", "composer"] },
3852
+ { name: "size_recommendation", label: "\u5C3A\u7801\u63A8\u8350", desc: "\u6309\u8EAB\u9AD8\u4F53\u91CD\u7B49\u63A8\u8350\u5C3A\u7801", category: "fact_alias", stages: ["tool_selector", "composer"] },
3853
+ // ── 店铺设置 customer_slot(你在「个性化」里填的 SOP 文本,可引用进模板) ──
3854
+ { name: "intent_instruction", label: "\u610F\u56FE\u8BC6\u522B\u89C4\u5219", desc: "\u4F60\u586B\u7684\u5E97\u94FA\u7EA7\u610F\u56FE\u8DEF\u7531 SOP", category: "customer_slot", stages: ["planner"] },
3855
+ { name: "general_qa_instruction", label: "\u901A\u7528\u54A8\u8BE2\u8BDD\u672F", desc: "\u4F60\u586B\u7684\u901A\u7528\u54A8\u8BE2/\u5BA2\u670D\u8EAB\u4EFD SOP", category: "customer_slot", stages: ["planner", "composer"] },
3856
+ { name: "recommendation_instruction", label: "\u63A8\u8350\u8BDD\u672F", desc: "\u4F60\u586B\u7684\u5546\u54C1\u63A8\u8350\u8BDD\u672F\u89C4\u5219", category: "customer_slot", stages: ["composer"] },
3857
+ { name: "takeover_rule_instruction", label: "\u8F6C\u4EBA\u5DE5\u63A5\u7BA1\u7B56\u7565", desc: "\u4F60\u586B\u7684\u633D\u7559/\u8F6C\u4EBA\u5DE5\u89C4\u5219", category: "customer_slot", stages: ["planner", "composer"] },
3858
+ { name: "image_rule_instruction", label: "\u56FE\u7247\u573A\u666F\u89C4\u5219", desc: "\u4F60\u586B\u7684\u56FE\u7247\u76F8\u5173\u4E1A\u52A1 SOP", category: "customer_slot", stages: ["planner", "composer", "qa_inspector"] },
3859
+ { name: "forbidden_words", label: "\u8FDD\u7981\u8BCD", desc: "\u4F60\u914D\u7F6E\u7684\u8FDD\u7981\u8BCD\u6E05\u5355", category: "customer_slot", stages: ["composer"] },
3860
+ // ── 平台只读块 platform_block(平台运行时注入,内容锁定,作者只决定是否/在哪引用) ──
3861
+ { name: "glossary_block", label: "\u672F\u8BED\u8868", desc: "\u5E73\u53F0\u672F\u8BED/\u6982\u5FF5\u5B9A\u4E49", category: "platform_block", stages: ["planner", "tool_selector", "qa_inspector"], readOnly: true },
3862
+ { name: "sa_summaries_block", label: "SA \u6458\u8981\u7D22\u5F15", desc: "\u5F53\u8F6E\u547D\u4E2D\u7684\u573A\u666F\u6458\u8981\u5217\u8868", category: "platform_block", stages: ["planner"], readOnly: true },
3863
+ { name: "tool_results_block", label: "\u5F53\u8F6E\u5DE5\u5177\u7ED3\u679C", desc: "\u672C\u8F6E\u5DF2\u53D6\u56DE\u7684\u5DE5\u5177\u4E8B\u5B9E\u6458\u8981", category: "platform_block", stages: ["composer", "qa_inspector"], readOnly: true },
3864
+ { name: "environment_awareness_block", label: "\u73AF\u5883/\u65F6\u95F4", desc: "\u5E73\u53F0\u4E0E\u5F53\u524D\u65F6\u95F4\u4FE1\u606F", category: "platform_block", stages: ["planner", "composer", "qa_inspector"], readOnly: true },
3865
+ { name: "image_understanding_rules", label: "\u56FE\u7247\u4FE1\u53F7\u89C4\u5219", desc: "\u5E73\u53F0\u56FE\u7247\u7406\u89E3\u89C4\u5219", category: "platform_block", stages: ["planner", "tool_selector", "composer", "qa_inspector"], readOnly: true },
3866
+ { name: "shop_image_rule_block", label: "\u5E97\u94FA\u56FE\u7247\u89C4\u5219", desc: "\u6709\u56FE\u7247\u4FE1\u53F7\u65F6\u6CE8\u5165\u7684\u5E97\u94FA\u56FE\u7247\u89C4\u5219", category: "platform_block", stages: ["composer", "qa_inspector"], readOnly: true },
3867
+ // 「① 背景层下放」(spec 2026-06-08)新增 @平台块:原 ① shell 直注的背景,现作者可 @引用 / 删 / 换。
3868
+ { name: "internal_terms_blacklist", label: "\u5185\u90E8\u672F\u8BED\u9ED1\u540D\u5355", desc: "\u7981\u8BCD/\u6CC4\u9732\u8BCD\u6E05\u7406\u5F15\u7528\u7684\u5185\u90E8\u672F\u8BED\u6E05\u5355", category: "platform_block", stages: ["composer", "qa_inspector"], readOnly: true },
3869
+ { name: "dialog_consumption_rules", label: "\u591A\u8F6E\u6D88\u8D39\u89C4\u5219", desc: "recent_chat / \u5BF9\u8BDD\u5386\u53F2\u7684\u6D88\u8D39\u89C4\u5219", category: "platform_block", stages: ["composer", "planner"], readOnly: true },
3870
+ { name: "takeover_entry_background", label: "\u63A5\u7BA1\u5165\u53E3\u80CC\u666F", desc: "\u7528\u6237\u4E3B\u52A8\u8981\u4EBA\u5DE5\u65F6\u627F\u63A5\u7684\u5E73\u53F0\u80CC\u666F\u5B9A\u4E49", category: "platform_block", stages: ["composer"], readOnly: true },
3871
+ { name: "takeover_reversal_background", label: "\u63A5\u7BA1\u53CD\u8F6C\u80CC\u666F", desc: "takeover \u573A\u666F\u53CD\u8F6C\u6821\u9A8C\u7684\u5E73\u53F0\u80CC\u666F\u5B9A\u4E49", category: "platform_block", stages: ["qa_inspector"], readOnly: true },
3872
+ { name: "action_description_signal_rules", label: "\u52A8\u4F5C\u63CF\u8FF0\u4FE1\u53F7\u89C4\u5219", desc: "\u8DEF\u7531\u7528\u52A8\u4F5C\u63CF\u8FF0\u4FE1\u53F7\u7684\u6D88\u8D39\u89C4\u5219", category: "platform_block", stages: ["planner"], readOnly: true },
3873
+ { name: "fact_trust_rules", label: "\u4E8B\u5B9E\u4FE1\u4EFB\u5C42\u7EA7", desc: "\u5F53\u524D\u8F6E\u4E8B\u5B9E vs \u5386\u53F2\u4E8B\u5B9E\u7684\u4FE1\u4EFB\u89C4\u5219", category: "platform_block", stages: ["planner"], readOnly: true },
3874
+ { name: "takeover_trigger_background", label: "\u63A5\u7BA1\u89E6\u53D1\u80CC\u666F", desc: "\u5E97\u94FA\u8F6C\u4EBA\u5DE5\u63A5\u7BA1\u89E6\u53D1\u6761\u4EF6\u80CC\u666F\u5B9A\u4E49", category: "platform_block", stages: ["planner"], readOnly: true }
3875
+ ];
3876
+ var ALL_REFERENCE_NAMES = NODE_TEMPLATE_REFERENCES.map((r) => r.name);
3877
+ function referencesForStage(stage) {
3878
+ return NODE_TEMPLATE_REFERENCES.filter((r) => r.stages.includes(stage));
3879
+ }
3880
+ var NODE_CHARTERS = {
3881
+ planner: {
3882
+ role: "\u610F\u56FE\u8BC6\u522B / \u8DEF\u7531\u8282\u70B9\uFF1A\u5224\u5B9A\u672C\u8F6E\u8D70\u5B8C\u6574\u4E1A\u52A1\u94FE\u8DEF\u8FD8\u662F\u8F6C\u4EBA\u5DE5\uFF0C\u5E76\u4EA7\u51FA\u300CSA \u6458\u8981 \u2192 \u547D\u4E2D scene\u300D\u7684\u8DEF\u7531\u3002",
3883
+ editable: "\u2461 \u4F5C\u8005\u6B63\u6587\uFF1A\u8DEF\u7531\u5224\u65AD\u903B\u8F91\u3001\u8DEF\u7531\u7EAA\u5F8B\u3001\u552E\u540E\u573A\u666F\u5224\u65AD\u3001@\u5F15\u7528\u5E97\u94FA\u610F\u56FE/\u63A5\u7BA1 SOP\u3002",
3884
+ mustOutput: "JSON \u5BF9\u8C61 {d, a, r}\uFF1Ad=\u51B3\u7B56\u7F16\u7801\uFF08business_full\u21922 / transfer\u21923\uFF09\uFF0Ca=\u662F\u5426\u552E\u540E\u573A\u666F\uFF08bool\uFF09\uFF0Cr=\u53CC\u4E0B\u6807\u8DEF\u7531\u6570\u7EC4 [[sa, sc], ...]\u3002\u683C\u5F0F\u7531\u5E73\u53F0 \u2462 \u9501\u5B9A\u2014\u2014\u4F60\u7684\u6B63\u6587\u7528\u51B3\u7B56\u540D\u5224\u65AD\u5373\u53EF\uFF0C\u4E0D\u8981\u81EA\u9020\u8F93\u51FA\u683C\u5F0F\u3002",
3885
+ locked: ["\u2462 \u8F93\u51FA\u5951\u7EA6 / JSON Schema\uFF08{d,a,r} \u7F16\u7801\u4E0E r \u53CC\u4E0B\u6807\u89C4\u5219\uFF09", "\u2463 \u8FD0\u884C\u65F6\u6570\u636E\u533A\uFF08SA \u6458\u8981 / \u8DEF\u7531\u4FE1\u53F7 / \u73AF\u5883\u65F6\u95F4\uFF0C\u6BCF\u8F6E\u81EA\u52A8\u6CE8\u5165\uFF09"]
3886
+ },
3887
+ composer: {
3888
+ role: "\u6210\u6587\u8282\u70B9\uFF1A\u4F9D\u636E\u5F53\u8F6E\u4E8B\u5B9E/\u6D41\u7A0B\uFF0C\u751F\u6210\u6700\u7EC8\u53D1\u7ED9\u7528\u6237\u7684\u5BA2\u670D\u8BDD\u672F\uFF08\u542B\u8F6C\u4EBA\u5DE5\u4FE1\u53F7\uFF09\u3002",
3889
+ editable: "\u2461 \u4F5C\u8005\u6B63\u6587\uFF1A\u56DE\u590D\u7B56\u7565\u3001\u8C03\u6027\u3001\u4E8B\u5B9E\u6D88\u8D39\u89C4\u5219\u3001@\u5F15\u7528\u5E97\u94FA\u901A\u7528\u8BDD\u672F/\u63A8\u8350/\u63A5\u7BA1 SOP\u3002",
3890
+ mustOutput: "JSON \u5BF9\u8C61 {messages:[...], transfer_to_human:bool}\uFF1Amessages \u4E3A\u7EAF\u6587\u672C\u6D88\u606F\u6570\u7EC4\uFF08\u56FE\u7247 URL \u5355\u72EC\u6210\u6761\uFF09\uFF0C\u8F6C\u4EBA\u5DE5\u7F6E transfer_to_human=true\u3002\u683C\u5F0F\u7531\u5E73\u53F0 \u2462 \u9501\u5B9A\u3002",
3891
+ locked: ["\u2462 \u8F93\u51FA\u5951\u7EA6 / JSON Schema\uFF08messages + transfer_to_human + \u5E73\u53F0\u8C03\u6027\u793A\u4F8B\uFF09", "\u2463 \u8FD0\u884C\u65F6\u6570\u636E\u533A\uFF08\u5DE5\u5177\u7ED3\u679C / SA action / \u4E8B\u5B9E\u5206\u7EA7\uFF0C\u6BCF\u8F6E\u81EA\u52A8\u6CE8\u5165\uFF09"]
3892
+ },
3893
+ qa_inspector: {
3894
+ role: "\u8D28\u68C0\u8282\u70B9\uFF1A\u5BF9 composer \u4EA7\u51FA\u7684\u7528\u6237\u53EF\u89C1\u56DE\u590D\u505A\u5408\u89C4/\u91CD\u590D\u6821\u9A8C\uFF0C\u51B3\u5B9A\u653E\u884C / \u6539\u5199 / \u8F6C\u4EBA\u5DE5\u3002",
3895
+ editable: "\u2461 \u4F5C\u8005\u6B63\u6587\uFF1A\u5224\u5B9A\u6807\u51C6\uFF08\u4F55\u4E3A\u5408\u683C / \u95EE\u9898 / \u8BED\u4E49\u91CD\u590D / \u8BE5\u8F6C\u4EBA\u5DE5\uFF09\u3001\u6E05\u7406\u89C4\u5219\u3001@\u5F15\u7528\u56FE\u7247\u89C4\u5219\u3002",
3896
+ mustOutput: '\u7ED3\u8BBA\u6309\u5E73\u53F0 \u2462 \u7F16\u7801\uFF1A\u5408\u683C\u2192`0`\uFF0C\u8BED\u4E49\u91CD\u590D\u2192`-1`\uFF0C\u6539\u5199\u2192`{"messages":[...]}`\uFF0C\u8F6C\u4EBA\u5DE5\u2192`{"messages":[...,"TRANSFER"]}`\uFF08\u672B\u9879\u6052\u4E3A "TRANSFER"\uFF09\u3002\u2462 \u53EA\u5B9A\u4E49\u300C\u7ED3\u8BBA\u2192\u7F16\u7801\u300D\u63A5\u53E3\uFF0C\u5224\u5B9A\u6807\u51C6\u5168\u5728\u4F60\u7684\u6B63\u6587\u3002',
3897
+ locked: ["\u2462 \u8F93\u51FA\u5951\u7EA6\uFF08\u7ED3\u8BBA\u2192\u7F16\u7801\uFF1A0 / -1 / JSON / TRANSFER \u534F\u8BAE\uFF09", "\u2463 \u8FD0\u884C\u65F6\u6570\u636E\u533A\uFF08\u73AF\u5883 / \u65F6\u95F4\uFF0C\u6BCF\u8F6E\u81EA\u52A8\u6CE8\u5165\uFF09"]
3898
+ },
3899
+ // 伪 stage(spec 2026-06-08):不是产出完整 system prompt 的真节点,而是被 planner/tool_selector/
3900
+ // composer/qa 四节点共享注入的「图片处理方法论」片段。
3901
+ image_understanding: {
3902
+ role: "\u56FE\u7247\u7406\u89E3\u65B9\u6CD5\u8BBA\uFF08\u4F2A stage\uFF09\uFF1A\u5B9A\u4E49\u770B\u5230\u56FE\u7247\u540E\u600E\u4E48\u63D0\u53D6\u4E8B\u5B9E / \u7ED3\u5408\u610F\u56FE / \u8DEF\u7531\uFF0C\u88AB 4 \u4E2A\u8282\u70B9\u5171\u4EAB\u6CE8\u5165\u3002",
3903
+ editable: "\u6574\u6BB5\u65B9\u6CD5\u8BBA\uFF1A\u56FE\u7247\u4E09\u6B65\u6D41\u7A0B\u7684\u63AA\u8F9E\u3001\u5E97\u94FA\u8BC6\u522B\u8868 / \u6613\u6DF7\u6DC6\u89C4\u5219 / \u56DE\u7B54\u5185\u5BB9\u5E93\u3002\u6BCF\u5BB6\u5E97\u56FE\u7247\u4E1A\u52A1\u4E0D\u540C\uFF0C\u53EF\u81EA\u7531\u6539\u5199\u3002",
3904
+ mustOutput: "\u65E0\u72EC\u7ACB\u8F93\u51FA\u5951\u7EA6\u2014\u2014\u672C\u914D\u7F6E\u662F\u88AB\u6CE8\u5165\u8FDB {{image_understanding_rules}} \u7684\u65B9\u6CD5\u8BBA\u7247\u6BB5\uFF0C\u4E0D\u4EA7\u51FA JSON\u3002\u9501\u5B9A\u7684\u54C1\u724C\u5F52\u5C5E\u7EA2\u7EBF / \u56FE\u7247\u4E8B\u5B9E\u7EA2\u7EBF / \u56FE\u7247\u5448\u73B0\u673A\u5236\u7531\u5E73\u53F0 code-assemble \u5305\u88F9\uFF0C\u4E0D\u5728\u4F60\u7684\u53EF\u7F16\u8F91\u5185\u5BB9\u91CC\u3001\u4E5F\u65E0\u6CD5\u88AB\u5220\u6539\u3002",
3905
+ locked: ["\u54C1\u724C\u5F52\u5C5E\u7EA2\u7EBF\uFF08\u4EC5 composer/qa reply \u8282\u70B9 code-assemble\uFF09", "\u56FE\u7247\u4E8B\u5B9E\u7EA2\u7EBF reply_fact\uFF08\u542B\u7EAF\u56FE\u5148\u6F84\u6E05\uFF0C\u4EC5 reply \u8282\u70B9\uFF09", "\u56FE\u7247\u5448\u73B0\u673A\u5236\uFF08\u6700\u8FD1\u56FE\u7247\u76F4\u63A5\u770B / \u66F4\u65E9\u56FE\u7247 [\u5386\u53F2\u56FE\u7247] \u5360\u4F4D\uFF09"]
3906
+ }
3907
+ };
3908
+ var EDITING_RULES = "\u6B63\u6587\u91CC @\u5F15\u7528 \u5199 {{name}}\uFF08\u4EC5 ASCII \u6807\u8BC6\u7B26\u88AB\u89E3\u6790\uFF1B\u4E2D\u6587/\u591A\u8BCD\u6309\u5B57\u9762\u6587\u672C\u4E0D\u89E3\u6790\uFF09\uFF1B\u53EA\u80FD\u5F15\u7528 variables \u91CC\u7684 name\uFF1B\u2462\u2463 \u5E73\u53F0\u9501\u5B9A\u3001\u4E0D\u51FA\u73B0\u5728\u4F60\u7684\u6B63\u6587\u91CC\uFF0C\u522B\u5C1D\u8BD5\u6539\uFF1B\u7A7A\u4E32\u8349\u7A3F = \u56DE\u843D\u5E73\u53F0\u51FA\u5382\u9ED8\u8BA4\u3002";
3909
+ function buildNodeDescribe(stage) {
3910
+ const charter = NODE_CHARTERS[stage];
3911
+ if (!charter) {
3912
+ throw new Error(`\u65E0\u8BE5\u8282\u70B9 charter\uFF1A${stage}`);
3913
+ }
3914
+ return {
3915
+ stage,
3916
+ charter,
3917
+ variables: referencesForStage(stage).map((r) => ({
3918
+ name: r.name,
3919
+ label: r.label,
3920
+ desc: r.desc,
3921
+ category: r.category,
3922
+ readOnly: !!r.readOnly
3923
+ })),
3924
+ editing_rules: EDITING_RULES
3925
+ };
3926
+ }
3927
+
3928
+ // src/utils/node-template-validate.ts
3929
+ var EXPOSED_NODE_STAGES = ["planner", "composer", "qa_inspector", "image_understanding"];
3930
+ function isExposedStage(stage) {
3931
+ return EXPOSED_NODE_STAGES.includes(stage);
3932
+ }
3933
+ var INTERP_RE = /\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;
3934
+ function extractInterpolationNames(text) {
3935
+ const seen = /* @__PURE__ */ new Set();
3936
+ const out = [];
3937
+ for (const m of text.matchAll(INTERP_RE)) {
3938
+ const name = m[1];
3939
+ if (!seen.has(name)) {
3940
+ seen.add(name);
3941
+ out.push(name);
3942
+ }
3943
+ }
3944
+ return out;
3945
+ }
3946
+ function computeUnknownNames(content, factoryDefault) {
3947
+ const legal = /* @__PURE__ */ new Set([
3948
+ ...ALL_REFERENCE_NAMES,
3949
+ ...extractInterpolationNames(factoryDefault)
3950
+ ]);
3951
+ return extractInterpolationNames(content).filter((n) => !legal.has(n));
3952
+ }
3953
+
3954
+ // src/commands/node-template.ts
3955
+ var STAGE_HINT = EXPOSED_NODE_STAGES.join("|");
3956
+ function assertStage(stage) {
3957
+ if (!isExposedStage(stage)) {
3958
+ throw new Error(`\u4E0D\u652F\u6301\u7684\u8282\u70B9\uFF1A${stage}\uFF08\u5F53\u524D\u53EF\u7528\uFF1A${STAGE_HINT}\uFF09`);
3959
+ }
3960
+ return stage;
3961
+ }
3962
+ function parseVersionNo(raw) {
3963
+ const n = Number(raw);
3964
+ if (!Number.isInteger(n) || n <= 0) {
3965
+ throw new Error(`\u975E\u6CD5\u7684 version_no: ${raw}\uFF08\u5FC5\u987B\u4E3A\u6B63\u6574\u6570\uFF09`);
3966
+ }
3967
+ return n;
3968
+ }
3969
+ function resolveContent(opts) {
3970
+ const provided = [opts.content !== void 0, !!opts.contentFile, !!opts.stdin].filter(Boolean).length;
3971
+ if (provided !== 1) {
3972
+ throw new Error("\u5185\u5BB9\u6765\u6E90\u987B\u6070\u597D\u4E00\u4E2A\uFF1A--content <\u6587\u672C> | --content-file <\u8DEF\u5F84> | --stdin");
3973
+ }
3974
+ if (opts.content !== void 0) return opts.content;
3975
+ if (opts.contentFile) return readFileSync(opts.contentFile, "utf8");
3976
+ return readFileSync(0, "utf8");
3977
+ }
3978
+ function registerNodeTemplateCommand(program2) {
3979
+ const nt = program2.command("node-template").description(
3980
+ `\u8282\u70B9\u6A21\u677F\uFF08\u9AD8\u7EA7\u6A21\u677F\uFF09\u64CD\u4F5C \u2014\u2014 \u83B7\u53D6\u5185\u5BB9\u3001\u6539\u52A8\u53D1\u5E03\u3001\u5386\u53F2/\u56DE\u6EAF\u3002\u8282\u70B9 stage\uFF1A${STAGE_HINT}\uFF08tool_selector \u6682\u4E0D\u653E\u51FA\uFF09\u3002\u9AD8\u7EA7\u6A21\u5F0F\u5F00\u5173\u5DF2\u6539\u4E3A\u5BA2\u6237\u81EA\u52A9\uFF08cs-client \u5185\u5F00\u5173\uFF09\uFF0CCLI \u4E0D\u518D\u63D0\u4F9B enroll \u5F00\u542F\u8DEF\u5F84\u3002\u670D\u52A1\u7AEF\uFF1Acustomer-servhub-api /v1/customer-agent/{config_id}/customer_config/node-templates*\uFF1B\u5951\u7EA6 docs/devkit/specs/2026-06-08-\u8282\u70B9\u6A21\u677F-cli-\u64CD\u4F5C\u9762.md`
3981
+ );
3982
+ nt.command("describe").description(
3983
+ `\u8282\u70B9\u7F16\u5199\u8BF4\u660E\u4E66\uFF08AI \u7F16\u5199\u524D\u5FC5\u8BFB\uFF09\uFF1A\u6BCF\u4E2A\u8282\u70B9\u7684\u804C\u8D23/\u53EF\u7F16\u8F91\u9762/\u8F93\u51FA\u5951\u7EA6/\u5E73\u53F0\u9501\u5B9A\u6BB5 + \u672C\u8282\u70B9\u53EF @\u5F15\u7528 \u7684\u53D8\u91CF\u76EE\u5F55\u3002\u4E0D\u4F20 --stage = \u5168\u90E8\u5DF2\u5F00\u653E\u8282\u70B9\uFF08${STAGE_HINT}\uFF09\u3002\u7EAF\u9759\u6001\u3001\u4E0E\u5177\u4F53 agent \u65E0\u5173\uFF0C\u65E0\u9700 --agent\u3002`
3984
+ ).option("--stage <stage>", `\u5355\u8282\u70B9\uFF1A${STAGE_HINT}`).action((opts) => {
3985
+ try {
3986
+ if (opts.stage) {
3987
+ const stage = assertStage(opts.stage);
3988
+ formatOutput({ success: true, data: buildNodeDescribe(stage) }, program2.opts().table);
3989
+ return;
3990
+ }
3991
+ const data = EXPOSED_NODE_STAGES.map((s) => buildNodeDescribe(s));
3992
+ formatOutput({ success: true, data }, program2.opts().table);
3993
+ } catch (err) {
3994
+ reportCaughtError(err);
3995
+ process.exit(toExitCode(err));
3996
+ }
3997
+ });
3998
+ nt.command("get").description("\u83B7\u53D6\u8282\u70B9\u6A21\u677F\u5185\u5BB9\uFF08\u4E0D\u4F20 --stage=\u5168\u90E8\u5DF2\u5F00\u653E\u8282\u70B9\uFF1Btool_selector \u4E00\u5F8B\u6EE4\u9664\uFF09").requiredOption("--agent <id>", "customer_agent_config_id").option("--stage <stage>", `\u5355\u8282\u70B9\u8FC7\u6EE4\uFF1A${STAGE_HINT}`).action(async (opts) => {
3999
+ try {
4000
+ const payload = await getNodeTemplates(opts.agent);
4001
+ if (opts.stage) {
4002
+ const stage = assertStage(opts.stage);
4003
+ const data2 = {
4004
+ config_id: payload.config_id,
4005
+ stage,
4006
+ node: payload.nodes?.[stage],
4007
+ factory_default: payload.factory_defaults?.[stage] ?? ""
4008
+ };
4009
+ formatOutput({ success: true, data: data2 }, program2.opts().table);
4010
+ return;
4011
+ }
4012
+ const exposed = new Set(EXPOSED_NODE_STAGES);
4013
+ const nodes = {};
4014
+ for (const [k, v] of Object.entries(payload.nodes ?? {})) {
4015
+ if (exposed.has(k)) nodes[k] = v;
4016
+ }
4017
+ const factory = {};
4018
+ for (const [k, v] of Object.entries(payload.factory_defaults ?? {})) {
4019
+ if (exposed.has(k)) factory[k] = v;
4020
+ }
4021
+ const data = {
4022
+ config_id: payload.config_id,
4023
+ stages: (payload.stages ?? []).filter((s) => exposed.has(s)),
4024
+ nodes,
4025
+ factory_defaults: factory
4026
+ };
4027
+ formatOutput({ success: true, data }, program2.opts().table);
4028
+ } catch (err) {
4029
+ reportCaughtError(err);
4030
+ process.exit(toExitCode(err));
4031
+ }
4032
+ });
4033
+ nt.command("set-draft").description("\u5199\u5355\u8282\u70B9\u8349\u7A3F\uFF08\u6574\u6BB5\u8986\u76D6\uFF1B\u7A7A\u4E32=\u56DE\u843D\u51FA\u5382\u9ED8\u8BA4\uFF09\u3002\u5185\u5BB9\u4E09\u9009\u4E00\uFF0C\u957F prompt \u5EFA\u8BAE --content-file").requiredOption("--agent <id>", "customer_agent_config_id").requiredOption("--stage <stage>", `\u8282\u70B9\uFF1A${STAGE_HINT}`).option("--content <text>", "\u5185\u8054\u6B63\u6587").option("--content-file <path>", "\u4ECE\u6587\u4EF6\u8BFB\u6B63\u6587").option("--stdin", "\u4ECE\u6807\u51C6\u8F93\u5165\u8BFB\u6B63\u6587", false).action(async (opts) => {
4034
+ try {
4035
+ const stage = assertStage(opts.stage);
4036
+ const content = resolveContent(opts);
4037
+ const data = await setNodeTemplateDraft(opts.agent, stage, content);
4038
+ formatOutput({ success: true, data }, program2.opts().table);
4039
+ } catch (err) {
4040
+ reportCaughtError(err);
4041
+ process.exit(toExitCode(err));
4042
+ }
4043
+ });
4044
+ nt.command("publish").description("\u53D1\u5E03\u8BE5\u8282\u70B9\u5F53\u524D\u8349\u7A3F\u4E0A\u7EBF\uFF08\u53D1\u5E03\u524D\u6821\u9A8C {{}} \u5408\u6CD5\u6027\uFF0C\u4E0D\u5408\u6CD5\u62D2\u7EDD\uFF1B--force \u8D8A\u8FC7\u6821\u9A8C\uFF09").requiredOption("--agent <id>", "customer_agent_config_id").requiredOption("--stage <stage>", `\u8282\u70B9\uFF1A${STAGE_HINT}`).option("--force", "\u8DF3\u8FC7 {{}} \u5408\u6CD5\u6027\u6821\u9A8C\u76F4\u63A5\u53D1\u5E03", false).action(async (opts) => {
4045
+ try {
4046
+ const stage = assertStage(opts.stage);
4047
+ if (!opts.force) {
4048
+ const payload = await getNodeTemplates(opts.agent);
4049
+ const node = payload.nodes?.[stage];
4050
+ const draft = node?.content ?? "";
4051
+ const factory = payload.factory_defaults?.[stage] ?? "";
4052
+ const unknown = computeUnknownNames(draft, factory);
4053
+ if (unknown.length > 0) {
4054
+ throw new Error(
4055
+ `\u8349\u7A3F\u542B\u975E\u6CD5\u63D2\u503C\u540D\uFF0C\u62D2\u7EDD\u53D1\u5E03\uFF1A${unknown.map((n) => `{{${n}}}`).join(" ")}\u3002\u8BF7\u4FEE\u6B63\u4E3A\u5408\u6CD5\u5F15\u7528\u540D\uFF0C\u6216\u786E\u8BA4\u65E0\u8BEF\u540E\u52A0 --force \u8D8A\u8FC7\u6821\u9A8C\u3002`
4056
+ );
4057
+ }
4058
+ }
4059
+ const data = await publishNodeTemplate(opts.agent, stage);
4060
+ formatOutput({ success: true, data }, program2.opts().table);
4061
+ } catch (err) {
4062
+ reportCaughtError(err);
4063
+ process.exit(toExitCode(err));
4064
+ }
4065
+ });
4066
+ nt.command("versions").description("\u5217\u8BE5\u8282\u70B9\u5386\u53F2\u7248\u672C\u65F6\u95F4\u7EBF").requiredOption("--agent <id>", "customer_agent_config_id").requiredOption("--stage <stage>", `\u8282\u70B9\uFF1A${STAGE_HINT}`).action(async (opts) => {
4067
+ try {
4068
+ const stage = assertStage(opts.stage);
4069
+ const data = await listNodeTemplateVersions(opts.agent, stage);
4070
+ formatOutput({ success: true, data }, program2.opts().table);
4071
+ } catch (err) {
4072
+ reportCaughtError(err);
4073
+ process.exit(toExitCode(err));
4074
+ }
4075
+ });
4076
+ nt.command("version").description("\u53D6\u8BE5\u8282\u70B9\u67D0\u7248\u672C\u6B63\u6587\uFF08\u9884\u89C8/diff\uFF09").requiredOption("--agent <id>", "customer_agent_config_id").requiredOption("--stage <stage>", `\u8282\u70B9\uFF1A${STAGE_HINT}`).requiredOption("--version <no>", "\u7248\u672C\u53F7\uFF08\u6B63\u6574\u6570\uFF09").action(async (opts) => {
4077
+ try {
4078
+ const stage = assertStage(opts.stage);
4079
+ const versionNo = parseVersionNo(opts.version);
4080
+ const data = await getNodeTemplateVersion(opts.agent, stage, versionNo);
4081
+ formatOutput({ success: true, data }, program2.opts().table);
4082
+ } catch (err) {
4083
+ reportCaughtError(err);
4084
+ process.exit(toExitCode(err));
4085
+ }
4086
+ });
4087
+ nt.command("restore").description("\u628A\u8BE5\u8282\u70B9\u67D0\u5386\u53F2\u7248\u672C\u590D\u5236\u6210\u8349\u7A3F\uFF08\u4E0D\u76F4\u63A5\u751F\u6548\uFF0C\u9700\u518D publish\uFF09").requiredOption("--agent <id>", "customer_agent_config_id").requiredOption("--stage <stage>", `\u8282\u70B9\uFF1A${STAGE_HINT}`).requiredOption("--version <no>", "\u8981\u56DE\u6EAF\u5230\u7684\u7248\u672C\u53F7\uFF08\u6B63\u6574\u6570\uFF09").action(async (opts) => {
4088
+ try {
4089
+ const stage = assertStage(opts.stage);
4090
+ const versionNo = parseVersionNo(opts.version);
4091
+ const data = await restoreNodeTemplateVersion(opts.agent, stage, versionNo);
4092
+ formatOutput({ success: true, data }, program2.opts().table);
4093
+ } catch (err) {
4094
+ reportCaughtError(err);
4095
+ process.exit(toExitCode(err));
4096
+ }
4097
+ });
4098
+ }
4099
+
3772
4100
  // src/client/ops-agent-api.ts
3773
4101
  async function listOpsAgentConversations(opts) {
3774
4102
  const workspaceId = getWorkspaceId(opts.workspaceId);
@@ -5399,6 +5727,7 @@ registerFaqCommand(program);
5399
5727
  registerConversationCommand(program);
5400
5728
  registerDebugCommand(program);
5401
5729
  registerMonitorCommand(program);
5730
+ registerNodeTemplateCommand(program);
5402
5731
  registerDashboardCommand(program);
5403
5732
  registerRepairRecordCommand(program);
5404
5733
  registerOperationsRecordCommand(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bty/customer-service-cli",
3
- "version": "0.5.4",
3
+ "version": "0.6.0",
4
4
  "description": "AI Customer Service CLI - Agent friendly",
5
5
  "type": "module",
6
6
  "main": "./dist/bin.js",