@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.
- package/README.md +8 -4
- package/dist/src/client/http.js +29 -23
- package/dist/src/commands/catalog.js +3 -2
- package/dist/src/commands/checkout.js +23 -13
- package/dist/src/commands/guidance.js +37 -13
- package/dist/src/commands/readyz.js +3 -3
- package/dist/src/commands/services.js +122 -41
- package/dist/src/commands/skill.js +55 -0
- package/dist/src/main.js +119 -7
- package/dist/src/state/agent_type.js +19 -0
- package/dist/src/state/config.js +5 -13
- package/dist/src/state/device_authority.js +174 -56
- package/docs/agent/buyer/catalog-list.json +2 -1
- package/docs/agent/buyer/identity-and-sessions.json +64 -0
- package/docs/agent/buyer/install-and-setup.json +19 -5
- package/docs/agent/buyer/payment-flow.json +5 -1
- package/docs/agent/buyer/quickstart.json +12 -4
- package/docs/cli-reference/agent-types.md +9 -3
- package/docs/cli-reference/commands/catalog/list.md +1 -1
- package/docs/cli-reference/commands/checkout.md +4 -4
- package/docs/cli-reference/commands/device.md +13 -0
- package/docs/cli-reference/commands/install.md +3 -1
- package/docs/cli-reference/commands/readyz.md +4 -5
- package/docs/cli-reference/commands/services/action.md +10 -5
- package/docs/cli-reference/commands/services/checkout.md +3 -3
- package/docs/cli-reference/commands/services/invoke.md +6 -6
- package/docs/cli-reference/commands/services/next.md +23 -4
- package/docs/cli-reference/commands/services/quote.md +5 -1
- package/docs/cli-reference/commands/services/start.md +5 -3
- package/docs/cli-reference/commands/skill.md +17 -0
- package/docs/cli-reference/conventions.md +4 -1
- package/docs/cli-reference/index.md +1 -0
- package/package.json +1 -1
- 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
|
-
|
|
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
|
|
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 = "
|
|
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:
|
|
132
|
-
reason:
|
|
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
|
-
|
|
189
|
-
|
|
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
|
|
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:
|
|
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,
|
|
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:
|
|
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
|
-
? "
|
|
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?.
|
|
668
|
-
? "
|
|
669
|
-
: preferred
|
|
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
|
-
|
|
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
|
|
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: "
|
|
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
|
|
938
|
+
return `把 handoff.markdown 原样发送到当前桌面对话,确认二维码、付款链接和金额 ${amount} 都已实际对用户可见,然后停止等待。不要立即执行 next.command,不要创建第二个 Checkout、Execution 或调用 pay;用户完成付款操作或要求查询后,只执行 next.command。`;
|
|
866
939
|
if (platform === "terminal")
|
|
867
|
-
return
|
|
868
|
-
return
|
|
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(
|
|
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:
|
|
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
|
|
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
|
+
}
|
package/dist/src/state/config.js
CHANGED
|
@@ -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.
|
|
13
|
-
export const API_CONTRACT_REVISION = "sha256:
|
|
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
|
|
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
|
}
|