@itpay/cli 2.0.5 → 2.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +8 -4
  2. package/dist/src/client/http.js +29 -23
  3. package/dist/src/commands/catalog.js +3 -2
  4. package/dist/src/commands/checkout.js +23 -13
  5. package/dist/src/commands/guidance.js +37 -13
  6. package/dist/src/commands/readyz.js +3 -3
  7. package/dist/src/commands/services.js +122 -41
  8. package/dist/src/commands/skill.js +55 -0
  9. package/dist/src/main.js +119 -7
  10. package/dist/src/state/agent_type.js +19 -0
  11. package/dist/src/state/config.js +5 -13
  12. package/dist/src/state/device_authority.js +174 -56
  13. package/docs/agent/buyer/catalog-list.json +2 -1
  14. package/docs/agent/buyer/identity-and-sessions.json +64 -0
  15. package/docs/agent/buyer/install-and-setup.json +19 -5
  16. package/docs/agent/buyer/payment-flow.json +5 -1
  17. package/docs/agent/buyer/quickstart.json +12 -4
  18. package/docs/cli-reference/agent-types.md +9 -3
  19. package/docs/cli-reference/commands/catalog/list.md +1 -1
  20. package/docs/cli-reference/commands/checkout.md +4 -4
  21. package/docs/cli-reference/commands/device.md +13 -0
  22. package/docs/cli-reference/commands/install.md +3 -1
  23. package/docs/cli-reference/commands/readyz.md +4 -5
  24. package/docs/cli-reference/commands/services/action.md +10 -5
  25. package/docs/cli-reference/commands/services/checkout.md +3 -3
  26. package/docs/cli-reference/commands/services/invoke.md +6 -6
  27. package/docs/cli-reference/commands/services/next.md +23 -4
  28. package/docs/cli-reference/commands/services/quote.md +5 -1
  29. package/docs/cli-reference/commands/services/start.md +5 -3
  30. package/docs/cli-reference/commands/skill.md +17 -0
  31. package/docs/cli-reference/conventions.md +4 -1
  32. package/docs/cli-reference/index.md +1 -0
  33. package/package.json +1 -1
  34. package/skills/itpay-buyer/SKILL.md +33 -6
@@ -69,8 +69,7 @@ export async function runServicesInvoke(backend, config, serviceExecutionID, cap
69
69
  throw new CommandContractError("capability_not_found", `capability ${capabilityID} is not available on service execution ${serviceExecutionID}`, "使用 Service Execution 当前返回的 capability_id,不要猜测名称。", [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取当前可用 capability" }]);
70
70
  }
71
71
  if (requestedCapability.requires_payment) {
72
- const command = quoteCommand(serviceExecutionID, requestedCapability, input);
73
- throw new CommandContractError("checkout_required", `capability ${capabilityID} requires checkout and cannot be invoked directly`, "该 capability 需要付款;先向用户确认购买,再创建报价。", [{ command, reason: "锁定可信输入和价格" }]);
72
+ throw new CommandContractError("checkout_required", `capability ${capabilityID} requires checkout and cannot be invoked directly`, "付费 capability 不能直接 invoke。不要尝试 quote、cart、buy、checkout 或 pay 作为旁路;只恢复同一 Execution 的当前合法动作。", [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取同一 Execution 的当前合法动作" }]);
74
73
  }
75
74
  const missingInput = missingRequiredInput(requestedCapability.input_schema, input);
76
75
  if (missingInput.length > 0) {
@@ -111,12 +110,12 @@ function invokedEnvelope(response, requestedCapability, capabilities, input) {
111
110
  };
112
111
  let status = items.length > 0 ? "result_ready" : "no_result";
113
112
  let instruction = items.length > 0
114
- ? "向用户展示编号和 safe_payload;用户选择后只在当前 Execution 提交对应 rank,不要新建 Execution。"
113
+ ? "向用户展示编号和 safe_payload;若候选列表已满足用户目标,在此停止。仅在用户明确选择并希望继续时,才在当前 Execution 提交对应 rank。"
115
114
  : "Provider 已返回空结果;不要重放当前 execution,按下一步恢复。";
116
115
  let next = null;
117
116
  if (response.effective_quota?.exhausted) {
118
117
  status = "quota_exhausted";
119
- instruction = "免费额度已用完且本次未调用 Provider;先向用户说明价格并确认购买。";
118
+ instruction = "免费额度已用完且本次没有调用 Provider。当前没有可购买的 continuation;只读取同一 Execution 的服务端恢复方向。";
120
119
  const checkoutAction = response.next_actions?.find((action) => action.kind === "create_checkout");
121
120
  const checkoutCapability = capabilities.find((capability) => capability.capability_id === checkoutAction?.capability_id);
122
121
  if (checkoutCapability) {
@@ -127,9 +126,11 @@ function invokedEnvelope(response, requestedCapability, capabilities, input) {
127
126
  } : {}),
128
127
  delivery_email_required: checkoutCapability.delivery_email_required,
129
128
  };
129
+ const price = capabilityPrice(checkoutCapability);
130
+ instruction = purchaseConfirmationInstruction("quota_exhausted", price, checkoutCapability.delivery_email_required);
130
131
  next = {
131
- command: quoteCommand(response.execution.service_execution_id, checkoutCapability, input),
132
- reason: "准备当前服务的付费 continuation 报价",
132
+ command: checkoutCommand(response.execution.service_execution_id, checkoutCapability, input),
133
+ reason: `仅在用户明确同意支付 ${price} 后执行;否则停止`,
133
134
  };
134
135
  }
135
136
  else {
@@ -183,12 +184,54 @@ function missingRequiredInput(schema, input) {
183
184
  return typeof input[field] === "string" && String(input[field]).trim() === "";
184
185
  });
185
186
  }
186
- function checkoutCommand(serviceExecutionID, capability, input) {
187
+ function checkoutCommand(serviceExecutionID, capability, input, fillMissing = true) {
187
188
  const lockedInput = { ...input };
188
- for (const field of missingRequiredInput(capability.input_schema, lockedInput))
189
- lockedInput[field] = "<value>";
189
+ if (fillMissing) {
190
+ for (const field of missingRequiredInput(capability.input_schema, lockedInput))
191
+ lockedInput[field] = "<value>";
192
+ }
190
193
  return `itpay services checkout ${serviceExecutionID} --capability ${capability.capability_id}${formatInputOptions(lockedInput)}${capability.delivery_email_required ? " --email <email>" : ""} --json`;
191
194
  }
195
+ function capabilityPrice(capability) {
196
+ return capability.price_amount_minor !== undefined && capability.price_currency
197
+ ? formatMoney(capability.price_amount_minor, capability.price_currency)
198
+ : "当前发布价格";
199
+ }
200
+ function purchaseConfirmationInstruction(context, price, deliveryEmailRequired, candidateTitle = "") {
201
+ if (context === "quota_exhausted") {
202
+ return deliveryEmailRequired
203
+ ? `免费额度已用完,本次没有调用 Provider,也尚未创建 Quote 或 Checkout。现在只向用户说明:继续当前请求需要支付 ${price},交付还需要用户邮箱;请确认是否购买并提供邮箱。然后停止并等待。用户明确同意并提供真实邮箱前,不要执行 next.command,不要新建 Execution,不要尝试其他 capability、quote、cart、buy、checkout 或 pay 命令。`
204
+ : `免费额度已用完,本次没有调用 Provider,也尚未创建 Quote 或 Checkout。现在只向用户说明:“继续当前请求需要支付 ${price},是否购买?”然后停止并等待用户明确回复。用户明确同意前,不要执行 next.command,不要新建 Execution,不要尝试其他 capability、quote、cart、buy、checkout 或 pay 命令。`;
205
+ }
206
+ const selected = candidateTitle ? `已选择 ${candidateTitle}。` : "当前候选已经确认。";
207
+ return deliveryEmailRequired
208
+ ? `${selected}候选已绑定到当前 Execution,但尚未购买后续服务。现在只向用户说明:继续购买后续服务需要支付 ${price},并提供用于发送交付认领链接的邮箱;请确认是否购买并提供邮箱。然后停止。用户明确同意并提供真实邮箱前,不要执行 next.command,不要创建新 Execution 或 Checkout。`
209
+ : `${selected}候选已绑定到当前 Execution,但尚未购买后续服务。现在只向用户说明:“继续购买后续服务需要支付 ${price},是否购买?”然后停止。用户明确同意前,不要执行 next.command,不要创建新 Execution 或 Checkout。`;
210
+ }
211
+ function paidContinuation(model, action, input) {
212
+ if (!action.capability_id)
213
+ return null;
214
+ const capability = model.capabilities.find((item) => item.capability_id === action.capability_id && item.requires_payment);
215
+ if (!capability)
216
+ return null;
217
+ const price = capabilityPrice(capability);
218
+ const stateBacked = model.execution.status === "quota_exhausted" || model.execution.status === "human_action_approved";
219
+ return {
220
+ capability,
221
+ price,
222
+ checkout: {
223
+ capability_id: capability.capability_id,
224
+ ...(capability.price_amount_minor !== undefined && capability.price_currency ? {
225
+ price: { amount_minor: capability.price_amount_minor, currency: capability.price_currency },
226
+ } : {}),
227
+ delivery_email_required: capability.delivery_email_required,
228
+ },
229
+ next: {
230
+ command: checkoutCommand(model.execution.service_execution_id, capability, input, !stateBacked),
231
+ reason: `仅在用户明确同意支付 ${price}${capability.delivery_email_required ? " 并提供真实邮箱" : ""}后执行;否则停止`,
232
+ },
233
+ };
234
+ }
192
235
  function quoteCommand(serviceExecutionID, capability, input) {
193
236
  const lockedInput = { ...input };
194
237
  for (const field of missingRequiredInput(capability.input_schema, lockedInput))
@@ -232,14 +275,20 @@ export async function runServicesAction(backend, serviceExecutionID, actionType,
232
275
  if (selection && actionType === "select_candidate" && response.status === "approved") {
233
276
  const updated = await backend.getServiceExecution(serviceExecutionID);
234
277
  const preferred = updated.allowed_actions?.[0];
235
- const next = preferred ? serviceAllowedActionCommand(updated, preferred) : null;
278
+ const continuation = preferred?.type === "prepare_quote"
279
+ ? paidContinuation(updated, preferred, {})
280
+ : null;
281
+ const next = continuation?.next ?? (preferred ? serviceAllowedActionCommand(updated, preferred) : null);
236
282
  writeCommandEnvelope({
237
283
  status: "candidate_selected",
238
284
  result: {
239
285
  service_execution_id: response.service_execution_id,
240
286
  candidate: { rank: selection.rank, title: selection.title },
287
+ ...(continuation ? { checkout: continuation.checkout } : {}),
241
288
  },
242
- instruction: "候选已绑定到来源 Execution;后续动作必须继续使用该 Execution。",
289
+ instruction: continuation
290
+ ? purchaseConfirmationInstruction("candidate_selected", continuation.price, continuation.capability.delivery_email_required, selection.title)
291
+ : "候选已绑定到来源 Execution;后续动作必须继续使用该 Execution。",
243
292
  next,
244
293
  recovery: [{
245
294
  command: `itpay services next ${response.service_execution_id} --json`,
@@ -294,7 +343,9 @@ async function resolveCandidateSelection(backend, serviceExecutionID, actionType
294
343
  };
295
344
  }
296
345
  function actionInputError(serviceExecutionID, message, code = "service_action_invalid") {
297
- return new CommandContractError(code, message, "使用当前 safe result 中的合法 action 和 candidate rank;需要人确认时先询问用户。", [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "重新读取当前可选动作" }]);
346
+ return new CommandContractError(code, message, code === "candidate_not_found"
347
+ ? "当前 rank 不存在或当前候选集不可用。不要新建 Execution,不要重新 invoke,不要构造候选 ID;只恢复同一 Execution 当前仍然有效的候选。"
348
+ : "使用当前 safe result 中的合法 action 和 candidate rank;需要人确认时先询问用户。", [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "重新读取同一 Execution 的当前可选动作" }]);
298
349
  }
299
350
  export async function runServicesCheckout(backend, config, serviceExecutionID, capabilityID, options = {}) {
300
351
  const deliveryContact = {
@@ -373,7 +424,7 @@ export async function runServicesCheckout(backend, config, serviceExecutionID, c
373
424
  ...(config.baseURL ? { baseURL: config.baseURL } : {}),
374
425
  ...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
375
426
  });
376
- const envelope = buildServicesCheckoutEnvelope(response, checkoutURL, plan);
427
+ const envelope = buildServicesCheckoutEnvelope(response, checkoutURL, plan, config.baseURL);
377
428
  const plainResult = [
378
429
  `service_execution_id: ${response.binding.service_execution_id}`,
379
430
  `checkout_id: ${checkoutID}`,
@@ -578,28 +629,35 @@ function servicesNextEnvelope(model) {
578
629
  };
579
630
  }
580
631
  const currentItems = model.current_result_items ?? [];
632
+ const delivery = model.current_delivery ?? model.delivery_bindings.at(-1);
633
+ const deliveryMode = serviceDeliveryMode(model);
581
634
  const candidateSelection = model.allowed_actions?.find((action) => action.type === "select_candidate");
582
635
  if (candidateSelection && currentItems.length > 0) {
636
+ const paidCapability = delivery?.capability_id
637
+ ? model.capabilities.find((capability) => capability.capability_id === delivery.capability_id && capability.requires_payment)
638
+ : undefined;
583
639
  return {
584
640
  status: "candidate_selection_available",
585
641
  result: {
586
642
  service_execution_id: execution.service_execution_id,
643
+ ...(delivery?.capability_id ? { capability_id: delivery.capability_id } : {}),
644
+ ...(deliveryMode ? { delivery_mode: deliveryMode } : {}),
587
645
  items: currentItems.map((item) => ({
588
646
  rank: item.rank,
589
647
  title: item.display_title,
590
648
  safe_payload: item.safe_payload,
591
649
  })),
592
650
  },
593
- instruction: "向用户展示编号和 safe_payload;用户选择后只在当前 Execution 提交对应 rank,不要新建 Execution。",
651
+ instruction: paidCapability
652
+ ? "付费 Agent-visible 搜索已完成。现在把 items 中的编号、title 和 safe_payload 展示给用户,然后停止;不要调用 read-result。若用户目标只是候选搜索,任务已经完成。只有用户之后明确选择某个候选并要求继续时,才执行 next.command;不要自动购买后续报告。"
653
+ : "向用户展示编号和 safe_payload;若候选列表已满足用户目标,在此停止。仅在用户明确选择并希望继续时,才在当前 Execution 提交对应 rank。",
594
654
  next: {
595
655
  command: `itpay services action ${execution.service_execution_id} --action select_candidate --actor-type human --status approved --candidate <rank> --json`,
596
- reason: "仅在用户明确选择后锁定来源候选",
656
+ reason: paidCapability ? "仅在用户明确选择候选并要求继续时执行" : "仅在用户明确选择后锁定来源候选",
597
657
  },
598
658
  recovery: [],
599
659
  };
600
660
  }
601
- const delivery = model.current_delivery ?? model.delivery_bindings.at(-1);
602
- const deliveryMode = serviceDeliveryMode(model);
603
661
  if (deliveryMode === "agent_visible_result") {
604
662
  const items = currentItems.map((item) => ({
605
663
  rank: item.rank,
@@ -617,7 +675,7 @@ function servicesNextEnvelope(model) {
617
675
  },
618
676
  instruction: items.length > 0
619
677
  ? selection
620
- ? "这是当前 Graph 步骤对应的交付。向用户展示编号和 safe_payload;如用户选择,必须在当前 Execution 提交对应 rank。"
678
+ ? "Agent-visible 搜索已完成。向用户展示 items 中的编号、title 和 safe_payload,然后停止;不要调用 read-result。只有用户明确选择候选并要求继续时,才执行 next.command。"
621
679
  : "这是当前 Graph 步骤对应的交付;结果已可供 Agent 使用,只使用 safe_payload。"
622
680
  : "Agent-visible 交付已完成但没有结果项;不要调用 read-result 或重放当前 execution。",
623
681
  next: selection ? {
@@ -651,6 +709,23 @@ function servicesNextEnvelope(model) {
651
709
  }
652
710
  const allowedActions = model.allowed_actions ?? [];
653
711
  const preferred = allowedActions[0];
712
+ if (preferred?.type === "prepare_quote") {
713
+ const continuation = paidContinuation(model, preferred, {});
714
+ if (continuation) {
715
+ return {
716
+ status: execution.status,
717
+ result: {
718
+ service_execution_id: execution.service_execution_id,
719
+ service_id: execution.service_id,
720
+ phase: execution.phase,
721
+ checkout: continuation.checkout,
722
+ },
723
+ instruction: purchaseConfirmationInstruction(execution.status === "quota_exhausted" ? "quota_exhausted" : "candidate_selected", continuation.price, continuation.capability.delivery_email_required),
724
+ next: continuation.next,
725
+ recovery: [],
726
+ };
727
+ }
728
+ }
654
729
  const next = preferred ? serviceAllowedActionCommand(model, preferred) : null;
655
730
  return {
656
731
  status: execution.status,
@@ -664,9 +739,13 @@ function servicesNextEnvelope(model) {
664
739
  requires_human: action.requires_human,
665
740
  })),
666
741
  },
667
- instruction: preferred?.requires_human
668
- ? "当前下一步需要用户明确选择;先展示必要信息并等待确认。"
669
- : preferred ? "执行服务端返回的唯一首选动作;不要猜测其他 capability。" : "当前没有后续动作。",
742
+ instruction: preferred?.type === "resume_checkout"
743
+ ? "当前 Execution 已经有一笔 Checkout。不要创建新的 Quote、Cart、Checkout 或 Execution。现在只执行 next.command,恢复并展示同一 Checkout 的付款入口。"
744
+ : preferred?.type === "wait"
745
+ ? "付款已确认,Provider 正在处理当前 Execution。不要新建 Execution、Checkout 或再次付款;稍后只执行 next.command 查询同一 Execution。"
746
+ : preferred?.requires_human
747
+ ? "当前下一步需要用户明确选择;先展示必要信息并等待确认。"
748
+ : preferred ? "执行服务端返回的唯一首选动作;不要猜测其他 capability。" : "当前没有后续动作。",
670
749
  next,
671
750
  recovery: [{ command: `itpay services get ${execution.service_execution_id} --json`, reason: "仅在当前动作异常时检查时间线" }],
672
751
  };
@@ -692,17 +771,10 @@ function serviceAllowedActionCommand(model, action) {
692
771
  reason: "仅在用户明确选择后提交当前候选 rank",
693
772
  };
694
773
  case "prepare_quote": {
695
- if (!capability)
696
- return null;
697
- const selectionBacked = model.execution.status === "human_action_approved";
698
- const input = selectionBacked
699
- ? {}
700
- : Object.fromEntries(requiredInputFields(capability.input_schema).map((field) => [field, "<value>"]));
701
- return {
702
- command: `itpay services quote ${executionID} --capability ${capability.capability_id}${formatInputOptions(input)}${capability.delivery_email_required ? " --email <email>" : ""} --json`,
703
- reason: selectionBacked ? "为已确认候选准备报价" : "为当前输入准备报价",
704
- };
774
+ return paidContinuation(model, action, {})?.next ?? null;
705
775
  }
776
+ case "resume_checkout":
777
+ return { command: `itpay services checkout ${executionID} --resume --json`, reason: "恢复同一 Checkout,不创建第二笔" };
706
778
  case "wait":
707
779
  return { command: `itpay services next ${executionID} --json`, reason: "等待 durable execution 推进" };
708
780
  case "view_delivery":
@@ -826,7 +898,7 @@ function parseValue(value) {
826
898
  return Number(value);
827
899
  return value;
828
900
  }
829
- function buildServicesCheckoutEnvelope(response, checkoutURL, plan) {
901
+ function buildServicesCheckoutEnvelope(response, checkoutURL, plan, baseURL) {
830
902
  const checkout = response.checkout;
831
903
  const platform = platformKeyForHost(plan.host);
832
904
  const handoff = { url: checkoutURL };
@@ -837,8 +909,9 @@ function buildServicesCheckoutEnvelope(response, checkoutURL, plan) {
837
909
  handoff.markdown = buildAgentChatHandoff(plan).markdown;
838
910
  }
839
911
  else if (platform === "plain_chat" && checkout.qr_png_url) {
840
- handoff.qr_image_url = checkout.qr_png_url;
912
+ handoff.qr_image_url = absolutePublicURL(baseURL, checkout.qr_png_url);
841
913
  }
914
+ const amount = formatMoney(checkout.checkout.amount_minor, checkout.checkout.currency);
842
915
  return {
843
916
  status: "human_checkout_required",
844
917
  result: {
@@ -846,13 +919,13 @@ function buildServicesCheckoutEnvelope(response, checkoutURL, plan) {
846
919
  checkout_id: checkout.checkout.checkout_id,
847
920
  capability_id: checkoutCapabilityID(response),
848
921
  locked_input: response.locked_input,
849
- amount: formatMoney(checkout.checkout.amount_minor, checkout.checkout.currency),
922
+ amount,
850
923
  },
851
924
  handoff,
852
- instruction: checkoutInstruction(platform),
925
+ instruction: checkoutInstruction(platform, amount),
853
926
  next: {
854
- command: `itpay checkout --id ${checkout.checkout.checkout_id} --token ${checkout.display_token}`,
855
- reason: "跟踪同一笔 Checkout",
927
+ command: `itpay checkout --id ${checkout.checkout.checkout_id} --token ${checkout.display_token} --json`,
928
+ reason: "仅在用户完成付款操作或要求查询后,读取同一 Checkout 的权威状态",
856
929
  },
857
930
  recovery: [],
858
931
  };
@@ -860,12 +933,20 @@ function buildServicesCheckoutEnvelope(response, checkoutURL, plan) {
860
933
  function checkoutCapabilityID(response, fallback = "") {
861
934
  return response.capability_id || fallback;
862
935
  }
863
- function checkoutInstruction(platform) {
936
+ function checkoutInstruction(platform, amount) {
864
937
  if (platform === "markdown")
865
- return "把 handoff.markdown 原样发送到当前桌面对话;二维码和链接可见前不要查询状态或新建 Checkout。";
938
+ return `把 handoff.markdown 原样发送到当前桌面对话,确认二维码、付款链接和金额 ${amount} 都已实际对用户可见,然后停止等待。不要立即执行 next.command,不要创建第二个 Checkout、Execution 或调用 pay;用户完成付款操作或要求查询后,只执行 next.command。`;
866
939
  if (platform === "terminal")
867
- return "在用户可见终端展示二维码和付款链接;可见前不要查询状态或新建 Checkout。";
868
- return "把付款链接和可用二维码附件发送给用户;可见前不要查询状态或新建 Checkout。";
940
+ return `在用户可见终端展示二维码、付款链接和金额 ${amount},然后停止等待。不要立即执行 next.command,不要创建第二个 Checkout、Execution 或调用 pay;用户完成付款操作或要求查询后,只执行 next.command。`;
941
+ return `现在只做以下动作:1)把 handoff.url 作为可点击付款链接发送给用户;2)优先把 handoff.qr_local_path 作为图片附件发送,不能发送本地附件时使用 handoff.qr_image_url;3)明确告诉用户本次金额是 ${amount};4)发送完成后停止并等待用户操作。不要立即执行 next.command,不要创建第二个 Checkout,不要新建 Execution,不要调用 pay。用户表示已经完成付款或要求查询状态后,只执行 next.command;用户的话本身不是付款成功证明。`;
942
+ }
943
+ function absolutePublicURL(baseURL, value) {
944
+ try {
945
+ return new URL(value, baseURL.endsWith("/") ? baseURL : `${baseURL}/`).toString();
946
+ }
947
+ catch {
948
+ return value;
949
+ }
869
950
  }
870
951
  function formatMoney(amountMinor, currency) {
871
952
  return `${(amountMinor / 100).toFixed(2)} ${currency}`;
@@ -0,0 +1,55 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { declaredAgentType } from "../state/agent_type.js";
5
+ import { CommandContractError, writeCommandEnvelope } from "./guidance.js";
6
+ const commandDir = dirname(fileURLToPath(import.meta.url));
7
+ const BUYER_SKILL = "itpay-buyer";
8
+ export function runSkillShow(name, options = {}) {
9
+ const normalized = name.trim().toLowerCase();
10
+ if (normalized !== BUYER_SKILL) {
11
+ throw new CommandContractError("skill_not_found", `skill not found: ${name}`, `当前 CLI 只内置 ${BUYER_SKILL};不要猜测 Skill 名称。`, [{ command: `itpay skill show ${BUYER_SKILL} --json`, reason: "读取完整 Buyer Skill" }]);
12
+ }
13
+ let content;
14
+ try {
15
+ content = readFileSync(findSkillPath(), "utf8");
16
+ }
17
+ catch {
18
+ throw new Error("packaged skill is unavailable: itpay-buyer");
19
+ }
20
+ validateSkill(content);
21
+ const agentType = options.agentType ?? declaredAgentType();
22
+ const envelope = {
23
+ status: "shown",
24
+ result: { skill: BUYER_SKILL, content },
25
+ instruction: agentType
26
+ ? agentType === "workbuddy"
27
+ ? "完整读取并遵守 Skill;保持 workbuddy、同一 Node/CLI launcher 和可持久写入 Device 状态的执行权限。内部诊断不要逐步转述给用户。"
28
+ : `完整读取并遵守 Skill;当前 Agent Type 是 ${agentType},后续命令保持不变。`
29
+ : "完整读取并遵守 Skill;先如实选择当前运行环境对应的 Agent Type。",
30
+ next: agentType
31
+ ? { command: "itpay catalog list --json", reason: "按 Skill 开始发现服务" }
32
+ : { command: "itpay install --json", reason: "选择真实且稳定的 Agent Type" },
33
+ recovery: [],
34
+ };
35
+ writeCommandEnvelope(envelope, {
36
+ ...options,
37
+ ...(agentType ? { agentType } : {}),
38
+ plainResult: content.replace(/\r\n/g, "\n").replace(/\n$/, "").split("\n"),
39
+ });
40
+ }
41
+ function findSkillPath() {
42
+ if (process.env.ITPAY_CLI_SKILLS_DIR) {
43
+ return resolve(process.env.ITPAY_CLI_SKILLS_DIR, BUYER_SKILL, "SKILL.md");
44
+ }
45
+ const packagePath = resolve(commandDir, "..", "..", "..", "skills", BUYER_SKILL, "SKILL.md");
46
+ if (existsSync(packagePath))
47
+ return packagePath;
48
+ return resolve(commandDir, "..", "..", "skills", BUYER_SKILL, "SKILL.md");
49
+ }
50
+ function validateSkill(content) {
51
+ const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1];
52
+ if (!frontmatter || !/^name:\s*itpay-buyer\s*$/m.test(frontmatter) || !/^description:\s*(?:>|\S)/m.test(frontmatter)) {
53
+ throw new Error("invalid packaged skill: itpay-buyer");
54
+ }
55
+ }
package/dist/src/main.js CHANGED
@@ -3,6 +3,7 @@
3
3
  // orchestrate; HTTP and rendering live in src/client and src/render.
4
4
  import { Command } from "commander";
5
5
  import { CLI_VERSION, loadConfig, cartSessionPath, newBackendClient } from "./state/config.js";
6
+ import { DeviceAuthority, DeviceAuthorizationError, DeviceStateError } from "./state/device_authority.js";
6
7
  import { CartSession } from "./state/cart_session.js";
7
8
  import { defaultHostForAgentType, normalizeHost, validateContext } from "./state/client_context.js";
8
9
  import { HttpError } from "./client/http.js";
@@ -15,9 +16,10 @@ import { runOrder } from "./commands/order.js";
15
16
  import { runListOrders } from "./commands/orders.js";
16
17
  import { runCancelRefund, runGetRefund, runListRefunds, runRefund, runWatchRefund } from "./commands/refund.js";
17
18
  import { runCartAdd, runCartAddQuoteServer, runCartAddServer, runCartAbandonServer, runCartClear, runCartNext, runCartRemove, runCartRemoveServer, runCartShow, runCartShowServer, } from "./commands/cart.js";
18
- import { CommandContractError, printErrorRecovery, writeCommandEnvelope } from "./commands/guidance.js";
19
+ import { CommandContractError, errorRecoveryActions, printErrorRecovery, writeCommandEnvelope } from "./commands/guidance.js";
19
20
  import { runDocsList, runDocsShow, runDocsSearch } from "./commands/docs.js";
20
21
  import { runInstall } from "./commands/install.js";
22
+ import { runSkillShow } from "./commands/skill.js";
21
23
  import { runNext } from "./commands/next.js";
22
24
  import { collectOption, parseKeyValueList, runServicesAction, runServicesCheckout, runServicesEvents, runServicesGet, runServicesInvoke, runServicesList, runServicesNext, runServicesReadResult, runServicesQuote, runServicesStart, } from "./commands/services.js";
23
25
  const program = new Command();
@@ -25,7 +27,7 @@ program
25
27
  .name("itpay")
26
28
  .description("V3 ItPay CLI — checkout, payment, order, and refund commands")
27
29
  .option("--agent-type <type>", "agent runtime type used for device enrollment and client-specific guidance")
28
- .version("2.0.5");
30
+ .version(CLI_VERSION);
29
31
  function withHost(value) {
30
32
  const host = normalizeHost(value);
31
33
  if (!host) {
@@ -80,16 +82,50 @@ function resolveCheckoutPresentationArgs(input) {
80
82
  }
81
83
  function reportCLIError(error, contract) {
82
84
  const commandError = error instanceof CommandContractError ? error : undefined;
85
+ const deviceError = error instanceof DeviceAuthorizationError ? error : undefined;
86
+ const stateError = error instanceof DeviceStateError ? error : undefined;
87
+ const httpRecovery = errorRecoveryActions(error).map((action) => ({
88
+ command: action.command,
89
+ reason: action.reason ?? action.label,
90
+ }));
91
+ const identityRecovery = error instanceof HttpError &&
92
+ (error.code === "agent_identity_required" || error.code === "agent_device_session_required");
93
+ const incompatible = error instanceof HttpError && (error.code === "client_upgrade_required" ||
94
+ error.code === "client_compatibility_headers_required" ||
95
+ error.code === "platform_release_unavailable" ||
96
+ (error.status === 404 && error.code === "unknown_error"));
97
+ const backendInternal = error instanceof HttpError && error.status === 500 && error.code === "internal_error";
98
+ const deviceRecovery = deviceError ? [{
99
+ command: "itpay skill show itpay-buyer --json",
100
+ reason: "读取身份边界;该错误需要用户或运营恢复 Backend 登记,不能通过换类型或删除本地身份绕过",
101
+ }] : [];
102
+ const stateRecovery = stateError ? [{
103
+ command: "itpay skill show itpay-buyer --json",
104
+ reason: "读取 Device 状态边界;修复当前 Host 的持久写权限后重试原命令",
105
+ }] : [];
106
+ const authorizationInstruction = stateError
107
+ ? "当前运行环境无法写入 owner-only Device 状态;请保持同一 Node、CLI 和 Agent Type,在允许持久写入 ~/.itpay-v3 的执行环境中重试。不要手工创建 lock、删除 identity 或换运行时碰运气。"
108
+ : error instanceof HttpError && error.code === "agent_device_session_required"
109
+ ? "CLI 已自动续期并重试同一请求一次,仍被拒绝;停止重试,不要切换 Agent Type 或旋转身份。"
110
+ : deviceError?.code === "agent_device_revoked"
111
+ ? "Backend 已撤销当前 Device 登记;CLI 没有自动创建替代身份。停止重试并请用户或运营恢复登记。"
112
+ : deviceError
113
+ ? "Device 身份验证失败;停止重试,不要切换 Agent Type、删除状态或旋转私钥。"
114
+ : undefined;
83
115
  if (contract || commandError) {
84
116
  writeCommandEnvelope({
85
117
  status: "error",
86
118
  error: {
87
- code: commandError?.code ?? (error instanceof HttpError ? error.code : contract?.code ?? "command_failed"),
119
+ code: incompatible ? "backend_contract_incompatible" : commandError?.code ?? (error instanceof HttpError ? error.code : stateError?.code ?? deviceError?.code ?? contract?.code ?? "command_failed"),
88
120
  message: error instanceof Error ? error.message : String(error),
89
121
  },
90
- instruction: commandError?.instruction ?? contract?.instruction ?? "检查命令参数后重试。",
122
+ instruction: incompatible
123
+ ? "当前 Backend 不支持本 CLI 所需的交易合同。立即停止;不要尝试 services quote、services checkout、cart、buy 或 pay 作为替代路径。需要先同步 CLI 与 Backend 版本。"
124
+ : backendInternal
125
+ ? "Backend 内部故障;立即停止并向用户报告。不要重试、检查或删除 Device 身份、创建替代 Execution、切换 Backend,或尝试 quote、checkout、cart、buy、pay 等付费路径。"
126
+ : commandError?.instruction ?? authorizationInstruction ?? contract?.instruction ?? "检查命令参数后重试。",
91
127
  next: null,
92
- recovery: commandError?.recovery ?? contract?.recovery ?? [],
128
+ recovery: incompatible || backendInternal ? [] : commandError?.recovery ?? (stateError ? stateRecovery : deviceError ? deviceRecovery : identityRecovery ? httpRecovery : contract?.recovery ?? []),
93
129
  }, {
94
130
  ...(contract?.jsonOutput !== undefined ? { jsonOutput: contract.jsonOutput } : {}),
95
131
  output: (text) => { process.stderr.write(text); },
@@ -123,9 +159,10 @@ program
123
159
  .description("Probe the V3 backend readiness endpoint")
124
160
  .option("--json", "output JSON instead of terminal text")
125
161
  .action(async (options) => {
126
- const backend = newBackendClient(loadConfig());
162
+ const config = loadConfig();
163
+ const backend = newBackendClient(config);
127
164
  try {
128
- await runReadyz(backend, { jsonOutput: Boolean(options.json) });
165
+ await runReadyz(backend, { jsonOutput: Boolean(options.json), ...(config.agentType ? { agentType: config.agentType } : {}) });
129
166
  }
130
167
  catch (error) {
131
168
  reportCLIError(error, {
@@ -139,6 +176,81 @@ program
139
176
  });
140
177
  }
141
178
  });
179
+ // --- device ---------------------------------------------------------------
180
+ const deviceCmd = program.command("device").description("Recover local Device registration state after an operator-confirmed Backend reset");
181
+ deviceCmd
182
+ .command("recover")
183
+ .description("Forget only the selected Backend registration while preserving the local private key")
184
+ .option("--confirm-backend-reset", "confirm that an operator reset the selected Backend registration database")
185
+ .option("--json", "output JSON instead of terminal text")
186
+ .action(async (options) => {
187
+ const config = loadConfig();
188
+ try {
189
+ if (!config.agentType) {
190
+ throw new CommandContractError("agent_type_required", "agent type is required for Backend-scoped Device recovery", "如实声明当前 Agent Type;恢复后必须用同一类型重新登记。", [{ command: "itpay install --json", reason: "选择当前真实 Agent Type" }]);
191
+ }
192
+ if (!options.confirmBackendReset) {
193
+ throw new CommandContractError("backend_reset_confirmation_required", "--confirm-backend-reset is required", "仅在运营已确认当前 Backend 的 Device 登记数据库被重建或清空后执行;普通 session 失效或 revoked 不得使用。", [{ command: "itpay docs show identity-and-sessions --json", reason: "检查适用边界" }]);
194
+ }
195
+ const recovered = await new DeviceAuthority({
196
+ baseURL: config.baseURL,
197
+ requestedAgentType: config.agentType,
198
+ compatibilityHeaders: {},
199
+ }).recoverBackendReset();
200
+ writeCommandEnvelope({
201
+ status: recovered.removed ? "backend_registration_removed" : "backend_registration_absent",
202
+ result: {
203
+ backend: config.baseURL,
204
+ removed_agent_types: recovered.agentTypes,
205
+ private_key_preserved: true,
206
+ other_backend_registrations_preserved: true,
207
+ },
208
+ instruction: "只读列出 Service Executions,以同一私钥和 Agent Type 重新登记当前 Backend;不要删除 ~/.itpay-v3 或切换运行时。",
209
+ next: {
210
+ command: `itpay --agent-type ${config.agentType} services list --limit 1 --json`,
211
+ reason: "用无业务写入的签名请求重新登记当前 Backend",
212
+ },
213
+ recovery: [],
214
+ }, {
215
+ jsonOutput: Boolean(options.json),
216
+ plainResult: [
217
+ `backend: ${config.baseURL}`,
218
+ `registration: ${recovered.removed ? "removed" : "already absent"}`,
219
+ "private_key: preserved",
220
+ "other_backends: preserved",
221
+ ],
222
+ });
223
+ }
224
+ catch (error) {
225
+ reportCLIError(error, {
226
+ jsonOutput: Boolean(options.json),
227
+ code: "device_recovery_failed",
228
+ instruction: "仅恢复运营已确认重建的当前 Backend;不要删除整个 Device identity。",
229
+ recovery: [{ command: "itpay docs show identity-and-sessions --json", reason: "检查 Device 恢复边界" }],
230
+ });
231
+ }
232
+ });
233
+ // --- skill ----------------------------------------------------------------
234
+ const skillCmd = program.command("skill").description("Read complete packaged Agent skills");
235
+ skillCmd
236
+ .command("show")
237
+ .description("Show one complete packaged skill")
238
+ .argument("<name>", "skill name")
239
+ .option("--json", "output JSON instead of terminal text")
240
+ .action((name, options) => {
241
+ const config = loadConfig();
242
+ try {
243
+ runSkillShow(name, { jsonOutput: Boolean(options.json), ...(config.agentType ? { agentType: config.agentType } : {}) });
244
+ }
245
+ catch (error) {
246
+ reportCLIError(error, {
247
+ jsonOutput: Boolean(options.json),
248
+ code: "skill_unavailable",
249
+ instruction: "内置 Skill 缺失或损坏;重新安装同版本 CLI 后重试。",
250
+ recovery: [{ command: `npm install -g @itpay/cli@${CLI_VERSION}`, reason: "恢复随包发布的 Skill" }],
251
+ });
252
+ }
253
+ });
142
254
  program
143
255
  .command("next")
144
256
  .description("Show the next recommended agent action from remembered server handles")
@@ -0,0 +1,19 @@
1
+ export function declaredAgentType(env = process.env, argv = process.argv) {
2
+ if (env.ITPAY_AGENT_TYPE)
3
+ return env.ITPAY_AGENT_TYPE;
4
+ for (let index = 0; index < argv.length; index += 1) {
5
+ const value = argv[index];
6
+ if (value === "--agent-type")
7
+ return argv[index + 1];
8
+ if (value?.startsWith("--agent-type="))
9
+ return value.slice("--agent-type=".length);
10
+ }
11
+ return undefined;
12
+ }
13
+ export function qualifyItPayCommand(command, agentType) {
14
+ if (!agentType || !/^[a-z0-9-]+$/.test(agentType))
15
+ return command;
16
+ if (!command.startsWith("itpay ") || /^itpay\s+--agent-type(?:=|\s)/.test(command))
17
+ return command;
18
+ return `itpay --agent-type ${agentType} ${command.slice("itpay ".length)}`;
19
+ }
@@ -6,11 +6,12 @@ import { mkdirSync } from "node:fs";
6
6
  import { resolve } from "node:path";
7
7
  import { HttpClient } from "../client/http.js";
8
8
  import { BackendClient } from "../client/backend.js";
9
+ import { declaredAgentType } from "./agent_type.js";
9
10
  import { DeviceAuthority } from "./device_authority.js";
10
11
  import { OperationJournal } from "./operation_journal.js";
11
12
  export const DEFAULT_BASE_URL = "https://app.itpay.ai";
12
- export const CLI_VERSION = "2.0.5";
13
- export const API_CONTRACT_REVISION = "sha256:3e6b650c62fa54eb8b9ea6b86857cfef594313871e2bf1b9f3ca4ff3cc6e1612";
13
+ export const CLI_VERSION = "2.0.8";
14
+ export const API_CONTRACT_REVISION = "sha256:47d42ab7bbe74a806b9ec989384b28ad715ffcf05a5eb913449b8bc224ffcf49";
14
15
  const CART_SESSION_DEFAULT_DIR = ".itpay-v3";
15
16
  const CART_SESSION_FILENAME = "cart.json";
16
17
  const OPERATION_JOURNAL_FILENAME = "operations.json";
@@ -25,7 +26,7 @@ export function cartSessionPath(env = process.env) {
25
26
  export function loadConfig(env = process.env) {
26
27
  const baseURL = env.ITPAY_BACKEND_URL || DEFAULT_BASE_URL;
27
28
  const bearerToken = env.ITPAY_BEARER_TOKEN || undefined;
28
- const agentType = env.ITPAY_AGENT_TYPE || agentTypeFromArgv(process.argv);
29
+ const agentType = declaredAgentType(env);
29
30
  const checkoutCurrency = env.ITPAY_CURRENCY || "CNY";
30
31
  const idempotencyKey = env.ITPAY_IDEMPOTENCY_KEY || `cli_${shortRandom()}`;
31
32
  const ideImageAttach = env.ITPAY_IDE_IMAGE_ATTACH !== "0";
@@ -62,19 +63,10 @@ export function newBackendClient(config) {
62
63
  "X-ItPay-Contract-Revision": API_CONTRACT_REVISION,
63
64
  },
64
65
  requestAuthorizer: (input) => authority.authorizationHeaders(input),
66
+ recoverAuthorization: () => authority.recoverAuthorization(),
65
67
  });
66
68
  return new BackendClient(http);
67
69
  }
68
- function agentTypeFromArgv(argv) {
69
- for (let index = 0; index < argv.length; index += 1) {
70
- const value = argv[index];
71
- if (value === "--agent-type")
72
- return argv[index + 1];
73
- if (value?.startsWith("--agent-type="))
74
- return value.slice("--agent-type=".length);
75
- }
76
- return undefined;
77
- }
78
70
  function shortRandom() {
79
71
  return Math.random().toString(36).slice(2, 10) + Math.random().toString(36).slice(2, 6);
80
72
  }