@itpay/cli 2.0.31 → 2.0.33
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 +13 -3
- package/dist/src/client/backend.js +3 -0
- package/dist/src/commands/checkout.js +1 -1
- package/dist/src/commands/feedback.js +143 -0
- package/dist/src/commands/guidance.js +17 -412
- package/dist/src/commands/install.js +1 -1
- package/dist/src/commands/order.js +4 -4
- package/dist/src/commands/orders.js +1 -4
- package/dist/src/commands/readyz.js +2 -2
- package/dist/src/commands/services.js +53 -30
- package/dist/src/commands/vault.js +1 -1
- package/dist/src/main.js +69 -9
- package/dist/src/state/config.js +1 -1
- package/docs/agent/buyer/catalog-list.json +11 -8
- package/docs/agent/buyer/install-and-setup.json +15 -13
- package/docs/agent/buyer/orders-refunds.json +7 -3
- package/docs/agent/buyer/payment-flow.json +1 -2
- package/docs/agent/buyer/purchased-content.json +5 -0
- package/docs/agent/buyer/quickstart.json +29 -40
- package/docs/agent/buyer/service-feedback.json +69 -0
- package/docs/cli-reference/commands/checkout.md +1 -1
- package/docs/cli-reference/commands/feedback/index.md +29 -0
- package/docs/cli-reference/commands/feedback/submit.md +123 -0
- package/docs/cli-reference/commands/install.md +3 -1
- package/docs/cli-reference/commands/order.md +1 -1
- package/docs/cli-reference/commands/readyz.md +3 -3
- package/docs/cli-reference/commands/services/action.md +1 -1
- package/docs/cli-reference/commands/services/invoke.md +5 -5
- package/docs/cli-reference/commands/services/list.md +3 -3
- package/docs/cli-reference/commands/services/next.md +5 -5
- package/docs/cli-reference/commands/vault/read.md +1 -1
- package/docs/cli-reference/conventions.md +27 -0
- package/docs/cli-reference/index.md +5 -0
- package/package.json +1 -1
- package/skills/itpay/SKILL.md +67 -150
package/README.md
CHANGED
|
@@ -8,13 +8,23 @@ The official V3 CLI and the single ItPay entry point for services, purchases, ac
|
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
npm install -g @itpay/cli
|
|
11
|
-
itpay readyz --json
|
|
12
|
-
itpay skill show itpay --json
|
|
13
11
|
itpay install --json
|
|
14
12
|
itpay --agent-type codex-desktop readyz --json
|
|
15
|
-
#
|
|
13
|
+
# execute the returned next.command until the typed Skill is loaded
|
|
16
14
|
```
|
|
17
15
|
|
|
16
|
+
Official Agent install prompt(可直接复制给 Agent):
|
|
17
|
+
|
|
18
|
+
> 安装或更新官方 `@itpay/cli`。从 `itpay install --json` 开始,选择你真实
|
|
19
|
+
> 所在的平台,完整执行 iTPay 返回的每个 `next.command` 并读取内置 Skill。
|
|
20
|
+
> 初始化后,根据我的自然语言需求自行使用 iTPay,不要让我运行命令,也不要向
|
|
21
|
+
> 我讲解命令、内部 ID 或技术步骤。只有需要我选择、授权、付款、提供联系方式
|
|
22
|
+
> 或确认退款时,才用通俗语言询问我;展示官方操作入口后停止等待。不要猜命令、
|
|
23
|
+
> 切换身份或环境、绕过授权或重复创建资源。准备完成后只回复“iTPay 已就绪”。
|
|
24
|
+
|
|
25
|
+
Use a separate test prompt to set the exact official Dev Backend. Never put a
|
|
26
|
+
Dev override in the public install prompt.
|
|
27
|
+
|
|
18
28
|
The CLI defaults to the production Backend `https://app.itpay.ai`. Explicit tests may set `ITPAY_BACKEND_URL=https://dev.itpay.ai`; every other Backend URL is rejected before network or local state access.
|
|
19
29
|
|
|
20
30
|
## Output Contract
|
|
@@ -58,6 +58,9 @@ export class BackendClient {
|
|
|
58
58
|
getOrderDeliveryAccess(orderID) {
|
|
59
59
|
return this.http.get(`/v1/orders/${encodeURIComponent(orderID)}/delivery-access`);
|
|
60
60
|
}
|
|
61
|
+
submitServiceFeedback(orderID, input) {
|
|
62
|
+
return this.http.post(`/v1/orders/${encodeURIComponent(orderID)}/feedback`, input);
|
|
63
|
+
}
|
|
61
64
|
listAccountOrders(limit, status, bearer, cursor) {
|
|
62
65
|
const qs = new URLSearchParams({ limit: String(limit) });
|
|
63
66
|
if (status) {
|
|
@@ -110,7 +110,7 @@ function terminalCheckoutEnvelope(presentation) {
|
|
|
110
110
|
const recovery = [];
|
|
111
111
|
if (payment === "verified") {
|
|
112
112
|
status = "completed";
|
|
113
|
-
instruction = "
|
|
113
|
+
instruction = "告诉用户:付款已经确认,订单已经记录,不需要再次付款;结果会在同一订单下继续准备,如果最终无法交付,可以从原订单申请退款,处理方式由内容是否已使用决定。然后只执行 next.command 读取同一笔服务;Agent 不再展示付款入口或创建新订单,也不承诺退款结果。";
|
|
114
114
|
next = serviceExecutionIDs.length === 1
|
|
115
115
|
? { command: `itpay services next ${serviceExecutionIDs[0]} --json`, reason: "读取同一笔已付款 Service Execution" }
|
|
116
116
|
: presentation.completed_order_id
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { CLI_VERSION } from "../state/config.js";
|
|
2
|
+
import { CommandContractError, writeCommandEnvelope } from "./guidance.js";
|
|
3
|
+
const MAX_FEEDBACK_NOTE_CODE_POINTS = 2000;
|
|
4
|
+
export async function runFeedbackSubmit(backend, orderID, options) {
|
|
5
|
+
const normalizedOrderID = orderID?.trim() ?? "";
|
|
6
|
+
if (!normalizedOrderID) {
|
|
7
|
+
throw new CommandContractError("order_required", "--order is required", "先恢复用户所说的原订单;Agent 自己取得订单,不要求用户提供内部 ID。本次未提交反馈。", [{ command: "itpay orders --json", reason: "列出当前授权账号的订单摘要" }]);
|
|
8
|
+
}
|
|
9
|
+
const rating = normalizeFeedbackRating(options.rating);
|
|
10
|
+
const itemRank = normalizeItemRank(options.itemRank);
|
|
11
|
+
const userNote = normalizeUserNote(options.note ?? "");
|
|
12
|
+
const order = await backend.getOrder(normalizedOrderID);
|
|
13
|
+
const choices = feedbackItemChoices(order);
|
|
14
|
+
if (choices.length === 0) {
|
|
15
|
+
throw new CommandContractError("feedback_unavailable", "this order has no service item available for feedback", "告诉用户这笔订单当前没有可评价的服务项目并停止;不要猜测项目 ID、切换身份或创建其他反馈。", []);
|
|
16
|
+
}
|
|
17
|
+
if (choices.length > 1 && itemRank === undefined) {
|
|
18
|
+
writeCommandEnvelope({
|
|
19
|
+
status: "feedback_item_selection_required",
|
|
20
|
+
result: {
|
|
21
|
+
...(order.order_code ? { order_code: order.order_code } : {}),
|
|
22
|
+
items: choices.map(({ rank, title, subject }) => ({ rank, title, ...(subject ? { subject } : {}) })),
|
|
23
|
+
},
|
|
24
|
+
instruction: "用服务名称和主题让用户选择要评价哪一项;不要展示内部 ID。用户选择后,Agent 使用同一订单、评分和留言并加入所选 item rank 自己执行提交。",
|
|
25
|
+
next: null,
|
|
26
|
+
recovery: [],
|
|
27
|
+
}, outputOptions(options));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const choice = itemRank === undefined
|
|
31
|
+
? choices[0]
|
|
32
|
+
: choices.find((candidate) => candidate.rank === itemRank);
|
|
33
|
+
if (!choice) {
|
|
34
|
+
throw new CommandContractError("feedback_item_invalid", `item rank ${itemRank} is not available for feedback`, "只使用当前订单返回的服务项目编号;不要猜测内部 ID。本次未提交反馈。", []);
|
|
35
|
+
}
|
|
36
|
+
const note = formatFeedbackNote({
|
|
37
|
+
userNote,
|
|
38
|
+
outcome: order.status,
|
|
39
|
+
serviceTitle: choice.title,
|
|
40
|
+
environment: options.environment,
|
|
41
|
+
...(options.agentType ? { agentType: options.agentType } : {}),
|
|
42
|
+
});
|
|
43
|
+
if (codePointLength(note) > MAX_FEEDBACK_NOTE_CODE_POINTS) {
|
|
44
|
+
throw new CommandContractError("feedback_note_too_long", `formatted feedback note exceeds ${MAX_FEEDBACK_NOTE_CODE_POINTS} Unicode code points`, "请用户缩短反馈内容;不要截断、改写或拆分提交。本次未提交反馈。", []);
|
|
45
|
+
}
|
|
46
|
+
const response = await backend.submitServiceFeedback(normalizedOrderID, {
|
|
47
|
+
order_item_id: choice.item.order_item_id,
|
|
48
|
+
rating,
|
|
49
|
+
note,
|
|
50
|
+
});
|
|
51
|
+
writeCommandEnvelope({
|
|
52
|
+
status: "feedback_submitted",
|
|
53
|
+
result: {
|
|
54
|
+
...(order.order_code ? { order_code: order.order_code } : {}),
|
|
55
|
+
service_title: choice.title,
|
|
56
|
+
rating: response.feedback.rating,
|
|
57
|
+
feedback_status: response.feedback.status,
|
|
58
|
+
},
|
|
59
|
+
instruction: "告诉用户反馈已经记录并表示感谢,然后停止。不要承诺回复、处理时间、退款或结果变更。",
|
|
60
|
+
next: null,
|
|
61
|
+
recovery: [],
|
|
62
|
+
}, outputOptions(options));
|
|
63
|
+
}
|
|
64
|
+
export function normalizeFeedbackRating(value) {
|
|
65
|
+
const normalized = value?.trim().toLowerCase() ?? "";
|
|
66
|
+
const numeric = normalized.match(/^([1-5])(?:\s*(?:\/\s*5|分|星|stars?))?$/u);
|
|
67
|
+
if (numeric)
|
|
68
|
+
return Number(numeric[1]);
|
|
69
|
+
const chinese = normalized.match(/^([一二三四五])(?:分|星)?$/u)?.[1];
|
|
70
|
+
if (chinese)
|
|
71
|
+
return { 一: 1, 二: 2, 三: 3, 四: 4, 五: 5 }[chinese];
|
|
72
|
+
throw new CommandContractError("feedback_rating_invalid", "--rating must be an explicit score from 1 to 5", "请用户明确给出 1–5 分;不要从好评、差评或情绪推断评分。本次未提交反馈。", []);
|
|
73
|
+
}
|
|
74
|
+
function normalizeItemRank(value) {
|
|
75
|
+
if (value === undefined)
|
|
76
|
+
return undefined;
|
|
77
|
+
const match = value.trim().match(/^#?([1-9]\d*)$/u);
|
|
78
|
+
if (!match) {
|
|
79
|
+
throw new CommandContractError("feedback_item_invalid", "--item-rank must be a positive integer", "使用当前订单返回的正整数服务项目编号;本次未提交反馈。", []);
|
|
80
|
+
}
|
|
81
|
+
const rank = Number(match[1]);
|
|
82
|
+
if (!Number.isSafeInteger(rank)) {
|
|
83
|
+
throw new CommandContractError("feedback_item_invalid", "--item-rank is outside the supported integer range", "使用当前订单返回的服务项目编号;本次未提交反馈。", []);
|
|
84
|
+
}
|
|
85
|
+
return rank;
|
|
86
|
+
}
|
|
87
|
+
function feedbackItemChoices(order) {
|
|
88
|
+
return order.items.flatMap((item, index) => {
|
|
89
|
+
if (!item.order_item_id?.trim())
|
|
90
|
+
return [];
|
|
91
|
+
const subject = feedbackSubject(item);
|
|
92
|
+
return [{
|
|
93
|
+
rank: index + 1,
|
|
94
|
+
item,
|
|
95
|
+
title: safeLine(item.title) || "未命名服务",
|
|
96
|
+
...(subject ? { subject } : {}),
|
|
97
|
+
}];
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function feedbackSubject(item) {
|
|
101
|
+
for (const field of ["company_name_or_credit_no", "company_name", "company", "target", "keyword"]) {
|
|
102
|
+
const value = item.input?.[field];
|
|
103
|
+
if (typeof value === "string") {
|
|
104
|
+
const safe = safeLine(value);
|
|
105
|
+
if (safe)
|
|
106
|
+
return safe.slice(0, 160);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
function normalizeUserNote(value) {
|
|
112
|
+
return value
|
|
113
|
+
.replaceAll("\r\n", "\n")
|
|
114
|
+
.replaceAll("\r", "\n")
|
|
115
|
+
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/gu, " ")
|
|
116
|
+
.trim();
|
|
117
|
+
}
|
|
118
|
+
function formatFeedbackNote(input) {
|
|
119
|
+
const summary = input.userNote
|
|
120
|
+
? `## Summary\n${input.userNote.split("\n").map((line) => `> ${line}`).join("\n")}\n\n`
|
|
121
|
+
: "";
|
|
122
|
+
return `${summary}## Context\n` + [
|
|
123
|
+
"- Source: user-confirmed via agent",
|
|
124
|
+
`- Outcome: ${safeLine(input.outcome) || "unknown"}`,
|
|
125
|
+
`- Service: ${safeLine(input.serviceTitle) || "未命名服务"}`,
|
|
126
|
+
`- Client: @itpay/cli ${CLI_VERSION}`,
|
|
127
|
+
`- Agent type: ${safeLine(input.agentType ?? "unspecified")}`,
|
|
128
|
+
`- Environment: ${input.environment}`,
|
|
129
|
+
].join("\n");
|
|
130
|
+
}
|
|
131
|
+
function safeLine(value) {
|
|
132
|
+
return value.replace(/[\r\n\u0000-\u001F\u007F]+/gu, " ").trim();
|
|
133
|
+
}
|
|
134
|
+
function codePointLength(value) {
|
|
135
|
+
return Array.from(value).length;
|
|
136
|
+
}
|
|
137
|
+
function outputOptions(options) {
|
|
138
|
+
return {
|
|
139
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
140
|
+
...(options.output ? { output: options.output } : {}),
|
|
141
|
+
...(options.agentType ? { agentType: options.agentType } : {}),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -5,6 +5,12 @@ import { qualifyBackendCommand } from "../state/config.js";
|
|
|
5
5
|
export function isTerminalServiceExecutionStatus(status) {
|
|
6
6
|
return status === "failed" || status === "refunded" || status === "cancelled";
|
|
7
7
|
}
|
|
8
|
+
export function appendOptionalFeedbackInvitation(instruction, outcome) {
|
|
9
|
+
const invitation = outcome === "delivered"
|
|
10
|
+
? "结果解释完毕后,可以询问用户是否愿意给这次服务 1–5 分和一句可选建议。只有用户明确评分后,Agent 才使用同一订单提交反馈;不要展示命令或内部 ID,同一对话最多询问一次。"
|
|
11
|
+
: "先完成同一订单的恢复和退款权利解释;处理清楚后,可以询问用户是否愿意给 1–5 分并说明卡壳点。只有用户明确评分后才提交反馈,同一对话最多询问一次。";
|
|
12
|
+
return `${instruction} ${invitation}`;
|
|
13
|
+
}
|
|
8
14
|
export class CommandContractError extends Error {
|
|
9
15
|
code;
|
|
10
16
|
instruction;
|
|
@@ -57,49 +63,6 @@ export function writeCommandEnvelope(value, options = {}) {
|
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
65
|
}
|
|
60
|
-
export function attachAgentGuidance(payload, guidance) {
|
|
61
|
-
return {
|
|
62
|
-
...payload,
|
|
63
|
-
agent_guidance: guidance,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
export function printAgentGuidance(guidance, output) {
|
|
67
|
-
const out = resolveOutput(output);
|
|
68
|
-
const agentType = declaredAgentType();
|
|
69
|
-
out(`${guidance.summary}\n`);
|
|
70
|
-
if (guidance.visible_results?.length) {
|
|
71
|
-
out("results:\n");
|
|
72
|
-
for (const item of guidance.visible_results) {
|
|
73
|
-
out(` ${item.rank}. ${item.title}\n`);
|
|
74
|
-
for (const [key, value] of Object.entries(item.safe_payload)) {
|
|
75
|
-
if (key === "name" || key === "company_name" || value === "" || value === undefined || value === null)
|
|
76
|
-
continue;
|
|
77
|
-
out(` ${key}: ${String(value)}\n`);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
if (guidance.next_actions.length === 0) {
|
|
82
|
-
out("next actions: none\n");
|
|
83
|
-
}
|
|
84
|
-
else {
|
|
85
|
-
out("next actions:\n");
|
|
86
|
-
for (const action of guidance.next_actions) {
|
|
87
|
-
out(` - ${action.label}\n`);
|
|
88
|
-
out(` ${qualifyBackendCommand(qualifyItPayCommand(action.command, agentType))}\n`);
|
|
89
|
-
if (action.requires_human)
|
|
90
|
-
out(" requires human confirmation\n");
|
|
91
|
-
if (action.reason)
|
|
92
|
-
out(` reason: ${action.reason}\n`);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
if (guidance.recovery.length > 0) {
|
|
96
|
-
out("recovery:\n");
|
|
97
|
-
for (const action of guidance.recovery) {
|
|
98
|
-
out(` - ${action.label}\n`);
|
|
99
|
-
out(` ${qualifyBackendCommand(qualifyItPayCommand(action.command, agentType))}\n`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
66
|
function qualifyEnvelope(value, agentType) {
|
|
104
67
|
return {
|
|
105
68
|
...value,
|
|
@@ -110,181 +73,31 @@ function qualifyEnvelope(value, agentType) {
|
|
|
110
73
|
})),
|
|
111
74
|
};
|
|
112
75
|
}
|
|
113
|
-
export function buildCartGuidance(cart, serviceModel) {
|
|
114
|
-
const serviceLine = latestServiceLine(cart);
|
|
115
|
-
if (serviceLine?.service_execution_id) {
|
|
116
|
-
const serviceGuidance = serviceModel
|
|
117
|
-
? buildServiceReadModelGuidance(serviceModel)
|
|
118
|
-
: buildServiceHandleGuidance(serviceLine.service_execution_id, serviceLine.service_capability_id);
|
|
119
|
-
return {
|
|
120
|
-
...serviceGuidance,
|
|
121
|
-
kind: "cart_service_execution",
|
|
122
|
-
summary: `cart ${cart.cart_id}: service-backed line ${lineID(serviceLine)} is ready for Service Execution`,
|
|
123
|
-
state: {
|
|
124
|
-
...serviceGuidance.state,
|
|
125
|
-
cart_id: cart.cart_id,
|
|
126
|
-
cart_item_id: lineID(serviceLine),
|
|
127
|
-
service_capability_id: serviceLine.service_capability_id,
|
|
128
|
-
},
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
if (cart.items.length === 0) {
|
|
132
|
-
return {
|
|
133
|
-
kind: "empty_cart",
|
|
134
|
-
summary: `cart ${cart.cart_id}: empty`,
|
|
135
|
-
state: { cart_id: cart.cart_id, status: cart.status },
|
|
136
|
-
next_actions: [
|
|
137
|
-
{
|
|
138
|
-
id: "browse_catalog",
|
|
139
|
-
label: "Browse services",
|
|
140
|
-
command: "itpay catalog list",
|
|
141
|
-
},
|
|
142
|
-
],
|
|
143
|
-
recovery: [],
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
return {
|
|
147
|
-
kind: "cart_checkout_ready",
|
|
148
|
-
summary: `cart ${cart.cart_id}: ready for checkout`,
|
|
149
|
-
state: { cart_id: cart.cart_id, status: cart.status, amount_minor: cart.amount_minor, currency: cart.currency },
|
|
150
|
-
next_actions: [
|
|
151
|
-
{
|
|
152
|
-
id: "checkout_cart",
|
|
153
|
-
label: "Create ItPay checkout",
|
|
154
|
-
command: `itpay buy --cart ${cart.cart_id} --host <client> --contact-email <email>`,
|
|
155
|
-
requires_human: true,
|
|
156
|
-
reason: "The human must review and pay on the ItPay checkout page.",
|
|
157
|
-
},
|
|
158
|
-
],
|
|
159
|
-
recovery: [
|
|
160
|
-
{
|
|
161
|
-
id: "show_cart",
|
|
162
|
-
label: "Inspect current server cart",
|
|
163
|
-
command: "itpay cart show",
|
|
164
|
-
},
|
|
165
|
-
],
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
export function buildServiceStartedGuidance(response) {
|
|
169
|
-
return buildServiceGuidance({
|
|
170
|
-
execution: response.execution,
|
|
171
|
-
capabilities: response.capabilities,
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
export function buildServiceReadModelGuidance(model) {
|
|
175
|
-
return buildServiceGuidance({
|
|
176
|
-
execution: model.execution,
|
|
177
|
-
capabilities: model.capabilities,
|
|
178
|
-
resultItems: model.result_items,
|
|
179
|
-
checkoutBindings: model.checkout_bindings,
|
|
180
|
-
deliveryBindings: model.delivery_bindings,
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
export function buildServiceInvokedGuidance(response, capabilities = []) {
|
|
184
|
-
return buildServiceGuidance({
|
|
185
|
-
execution: response.execution,
|
|
186
|
-
capabilities,
|
|
187
|
-
resultItems: response.result_items,
|
|
188
|
-
...(response.next_actions ? { backendNextActions: response.next_actions } : {}),
|
|
189
|
-
...(response.effective_quota ? { effectiveQuota: response.effective_quota } : {}),
|
|
190
|
-
providerCalled: response.provider_called,
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
export function buildServiceActionGuidance(action) {
|
|
194
|
-
const serviceExecutionID = action.service_execution_id;
|
|
195
|
-
return {
|
|
196
|
-
kind: "service_action_recorded",
|
|
197
|
-
summary: `service execution ${serviceExecutionID}: action ${action.action_type} recorded`,
|
|
198
|
-
state: {
|
|
199
|
-
service_execution_id: serviceExecutionID,
|
|
200
|
-
action_type: action.action_type,
|
|
201
|
-
status: action.status,
|
|
202
|
-
result_item_id: action.result_item_id,
|
|
203
|
-
},
|
|
204
|
-
next_actions: [
|
|
205
|
-
{
|
|
206
|
-
id: "inspect_service_execution",
|
|
207
|
-
label: "Read updated Service Execution state",
|
|
208
|
-
command: `itpay services next ${serviceExecutionID} --json`,
|
|
209
|
-
},
|
|
210
|
-
],
|
|
211
|
-
recovery: [
|
|
212
|
-
{
|
|
213
|
-
id: "timeline",
|
|
214
|
-
label: "Inspect full timeline",
|
|
215
|
-
command: `itpay services get ${serviceExecutionID}`,
|
|
216
|
-
},
|
|
217
|
-
],
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
export function buildServiceHandleGuidance(serviceExecutionID, checkoutCapabilityID) {
|
|
221
|
-
const actions = [
|
|
222
|
-
{
|
|
223
|
-
id: "inspect_service_execution",
|
|
224
|
-
label: "Read Service Execution state and capabilities",
|
|
225
|
-
command: `itpay services next ${serviceExecutionID} --json`,
|
|
226
|
-
},
|
|
227
|
-
];
|
|
228
|
-
if (checkoutCapabilityID) {
|
|
229
|
-
actions.push({
|
|
230
|
-
id: "checkout_service",
|
|
231
|
-
label: "Create checkout after human confirmation",
|
|
232
|
-
command: `itpay services checkout ${serviceExecutionID} --capability ${checkoutCapabilityID} --json`,
|
|
233
|
-
requires_human: true,
|
|
234
|
-
reason: "Inspect the Service Execution first; the CLI will request delivery contact only when the selected capability requires it.",
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
return {
|
|
238
|
-
kind: "service_execution_handle",
|
|
239
|
-
summary: `service execution ${serviceExecutionID}: inspect before invoking`,
|
|
240
|
-
state: { service_execution_id: serviceExecutionID },
|
|
241
|
-
next_actions: actions,
|
|
242
|
-
recovery: [],
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
76
|
export function errorRecoveryActions(error) {
|
|
246
77
|
if (!(error instanceof HttpError))
|
|
247
78
|
return [];
|
|
248
79
|
if (error.code === "agent_identity_required") {
|
|
249
|
-
return [
|
|
250
|
-
{
|
|
251
|
-
id: "inspect_agent_setup",
|
|
252
|
-
label: "Inspect supported Agent Type setup",
|
|
253
|
-
command: "itpay install --json",
|
|
254
|
-
},
|
|
255
|
-
];
|
|
80
|
+
return [{ id: "inspect_agent_setup", label: "Inspect supported Agent Type setup", command: "itpay install --json" }];
|
|
256
81
|
}
|
|
257
82
|
if (error.code === "agent_device_session_required") {
|
|
258
|
-
return [
|
|
259
|
-
{
|
|
83
|
+
return [{
|
|
260
84
|
id: "read_agent_session_rules",
|
|
261
85
|
label: "Read ItPay identity and session recovery rules",
|
|
262
86
|
command: "itpay skill show itpay --json",
|
|
263
87
|
reason: "The CLI already attempted one automatic session renewal; do not rotate identity or loop retries.",
|
|
264
|
-
}
|
|
265
|
-
];
|
|
88
|
+
}];
|
|
266
89
|
}
|
|
267
90
|
if (error.code === "quota_exhausted" || error.code === "checkout_required") {
|
|
268
|
-
return [
|
|
269
|
-
{
|
|
91
|
+
return [{
|
|
270
92
|
id: "inspect_service_execution",
|
|
271
93
|
label: "Inspect Service Execution before checkout",
|
|
272
94
|
command: "itpay services next <service_execution_id> --json",
|
|
273
|
-
}
|
|
274
|
-
];
|
|
95
|
+
}];
|
|
275
96
|
}
|
|
276
97
|
if (error.code === "cart_item_locked" || error.status === 409) {
|
|
277
98
|
return [
|
|
278
|
-
{
|
|
279
|
-
|
|
280
|
-
label: "Inspect the canonical server cart",
|
|
281
|
-
command: "itpay cart show",
|
|
282
|
-
},
|
|
283
|
-
{
|
|
284
|
-
id: "continue_checkout",
|
|
285
|
-
label: "Continue the last locally remembered checkout",
|
|
286
|
-
command: "itpay checkout",
|
|
287
|
-
},
|
|
99
|
+
{ id: "show_cart", label: "Inspect the canonical server cart", command: "itpay cart show" },
|
|
100
|
+
{ id: "continue_checkout", label: "Continue the last locally remembered checkout", command: "itpay checkout" },
|
|
288
101
|
{
|
|
289
102
|
id: "recover_service_execution",
|
|
290
103
|
label: "List recoverable Service Executions if the local handoff is missing",
|
|
@@ -293,22 +106,18 @@ export function errorRecoveryActions(error) {
|
|
|
293
106
|
];
|
|
294
107
|
}
|
|
295
108
|
if (error.status === 404) {
|
|
296
|
-
return [
|
|
297
|
-
{
|
|
109
|
+
return [{
|
|
298
110
|
id: "recover_service_executions",
|
|
299
111
|
label: "List visible Service Executions and follow their next instruction",
|
|
300
112
|
command: "itpay services list",
|
|
301
|
-
}
|
|
302
|
-
];
|
|
113
|
+
}];
|
|
303
114
|
}
|
|
304
115
|
if (error.status === 502 || error.status === 503 || error.status === 504) {
|
|
305
|
-
return [
|
|
306
|
-
{
|
|
116
|
+
return [{
|
|
307
117
|
id: "retry_after_backend_recovers",
|
|
308
118
|
label: "Retry after the selected official Backend is reachable",
|
|
309
119
|
command: "itpay readyz",
|
|
310
|
-
}
|
|
311
|
-
];
|
|
120
|
+
}];
|
|
312
121
|
}
|
|
313
122
|
return [];
|
|
314
123
|
}
|
|
@@ -323,207 +132,3 @@ export function printErrorRecovery(error, output) {
|
|
|
323
132
|
out(` ${action.command}\n`);
|
|
324
133
|
}
|
|
325
134
|
}
|
|
326
|
-
function buildServiceGuidance(input) {
|
|
327
|
-
const execution = input.execution;
|
|
328
|
-
const capabilities = input.capabilities ?? [];
|
|
329
|
-
const prePurchase = firstPrePurchaseCapability(capabilities);
|
|
330
|
-
const paid = firstPaidCapability(capabilities);
|
|
331
|
-
const resultItem = input.resultItems?.[0];
|
|
332
|
-
const checkoutID = input.checkoutBindings?.at(-1)?.checkout_id;
|
|
333
|
-
const delivery = input.deliveryBindings?.[0];
|
|
334
|
-
const deliveryMode = String(delivery?.redacted_summary?.delivery_mode ?? (delivery?.vault_artifact_id ? "vault_artifact" : ""));
|
|
335
|
-
const backendCheckout = input.backendNextActions?.find((action) => action.kind === "create_checkout");
|
|
336
|
-
const nextActions = [];
|
|
337
|
-
const recovery = [
|
|
338
|
-
{
|
|
339
|
-
id: "timeline",
|
|
340
|
-
label: "Inspect full timeline",
|
|
341
|
-
command: `itpay services get ${execution.service_execution_id}`,
|
|
342
|
-
},
|
|
343
|
-
];
|
|
344
|
-
if (isTerminalServiceExecutionStatus(execution.status)) {
|
|
345
|
-
// Terminal executions are inspectable, but no command may advance or replay them.
|
|
346
|
-
}
|
|
347
|
-
else if (deliveryMode === "agent_visible_result" && (input.resultItems?.length ?? 0) > 0) {
|
|
348
|
-
nextActions.push({
|
|
349
|
-
id: "use_agent_visible_result",
|
|
350
|
-
label: "Use the safe candidate list shown above",
|
|
351
|
-
command: `itpay services next ${execution.service_execution_id} --json`,
|
|
352
|
-
reason: "This result is already visible to the agent. Do not call services read-result; that command is only for Vault deliveries authorized by a human.",
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
else if (execution.status === "completed" || execution.next_action === "completed") {
|
|
356
|
-
nextActions.push({
|
|
357
|
-
id: "inspect_order_or_grant",
|
|
358
|
-
label: "Inspect order, claim, or grant from the checkout/order owner",
|
|
359
|
-
command: "itpay orders --limit 20",
|
|
360
|
-
requires_human: true,
|
|
361
|
-
reason: "Delivery visibility is controlled by the human account and grant flow.",
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
else if (execution.phase === "delivery" || execution.next_action === "view_delivery") {
|
|
365
|
-
const grantStatus = String(delivery?.grant_status ?? "");
|
|
366
|
-
if (grantStatus === "active") {
|
|
367
|
-
nextActions.push({
|
|
368
|
-
id: "read_granted_result",
|
|
369
|
-
label: "Read the human-granted service result",
|
|
370
|
-
command: `itpay services read-result ${execution.service_execution_id}`,
|
|
371
|
-
reason: "The human granted temporary agent access to this delivery.",
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
else {
|
|
375
|
-
nextActions.push({
|
|
376
|
-
id: grantStatus === "expired" ? "ask_human_to_reauthorize" : "wait_for_human_agent_grant",
|
|
377
|
-
label: grantStatus === "expired" ? "Ask the human to authorize AI access again" : "Wait for human AI-access authorization",
|
|
378
|
-
command: `itpay services get ${execution.service_execution_id}`,
|
|
379
|
-
requires_human: true,
|
|
380
|
-
reason: grantStatus === "expired"
|
|
381
|
-
? "The 15 minute human grant expired."
|
|
382
|
-
: "The human must approve AI access on the Credential page.",
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
else if (execution.next_action === "pay_checkout" || execution.status === "checkout_pending") {
|
|
387
|
-
nextActions.push({
|
|
388
|
-
id: "open_existing_checkout",
|
|
389
|
-
label: "Open the existing ItPay checkout handoff",
|
|
390
|
-
command: `itpay services checkout ${execution.service_execution_id} --resume --json`,
|
|
391
|
-
requires_human: true,
|
|
392
|
-
reason: checkoutID
|
|
393
|
-
? `Checkout ${checkoutID} already exists; reissue a short-lived handoff without creating another checkout.`
|
|
394
|
-
: "Recover the existing checkout handoff from Service Execution owner facts.",
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
else if (execution.checkout_required || execution.next_action === "create_checkout") {
|
|
398
|
-
const capabilityID = backendCheckout?.capability_id ?? paid?.capability_id ?? input.checkoutCapabilityID ?? execution.current_capability_id;
|
|
399
|
-
if (capabilityID) {
|
|
400
|
-
const checkoutCapability = capabilities.find((capability) => capability.capability_id === capabilityID);
|
|
401
|
-
const emailRequired = checkoutCapability?.delivery_email_required === true;
|
|
402
|
-
nextActions.push({
|
|
403
|
-
id: "checkout_service",
|
|
404
|
-
label: "Create ItPay checkout for the paid service capability",
|
|
405
|
-
command: `itpay services checkout ${execution.service_execution_id} --capability ${capabilityID}${emailRequired ? " --email <email>" : ""} --json`,
|
|
406
|
-
requires_human: true,
|
|
407
|
-
reason: emailRequired
|
|
408
|
-
? deliveryEmailGuidance(checkoutCapability?.delivery_email_purpose)
|
|
409
|
-
: "This capability returns an agent-visible result after payment and does not require a delivery email.",
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
else {
|
|
413
|
-
nextActions.push({
|
|
414
|
-
id: "inspect_capabilities",
|
|
415
|
-
label: "Inspect capabilities before checkout",
|
|
416
|
-
command: `itpay services get ${execution.service_execution_id}`,
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
else if ((input.providerCalled || execution.next_action === "select_candidate") && (input.resultItems?.length ?? 0) === 0) {
|
|
421
|
-
// Terminal empty result: a later explicit human request starts a new flow.
|
|
422
|
-
}
|
|
423
|
-
else if (needsHumanSelection(execution, resultItem)) {
|
|
424
|
-
nextActions.push({
|
|
425
|
-
id: "select_result_item",
|
|
426
|
-
label: "Ask the human to select a result item, then submit the selection",
|
|
427
|
-
command: `itpay services action ${execution.service_execution_id} --action select_candidate --actor-type human --status approved --candidate <rank>`,
|
|
428
|
-
requires_human: true,
|
|
429
|
-
reason: "Do not choose a candidate without explicit human confirmation.",
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
else if (prePurchase) {
|
|
433
|
-
const action = {
|
|
434
|
-
id: "invoke_capability",
|
|
435
|
-
label: `Invoke ${prePurchase.capability_id}`,
|
|
436
|
-
command: `itpay services invoke ${execution.service_execution_id} --capability ${prePurchase.capability_id} --input key=value --json`,
|
|
437
|
-
};
|
|
438
|
-
if (prePurchase.free_quota_limit) {
|
|
439
|
-
action.reason = `One keyword per execution. Free quota limit: ${prePurchase.free_quota_limit} per ${prePurchase.quota_subject || "subject"}.`;
|
|
440
|
-
}
|
|
441
|
-
nextActions.push(action);
|
|
442
|
-
}
|
|
443
|
-
else {
|
|
444
|
-
nextActions.push({
|
|
445
|
-
id: "inspect_service_execution",
|
|
446
|
-
label: "Read Service Execution state",
|
|
447
|
-
command: `itpay services next ${execution.service_execution_id} --json`,
|
|
448
|
-
});
|
|
449
|
-
}
|
|
450
|
-
return {
|
|
451
|
-
kind: "service_execution",
|
|
452
|
-
summary: `service execution ${execution.service_execution_id}: ${execution.status}/${execution.phase}${input.effectiveQuota ? `, quota ${input.effectiveQuota.remaining}/${input.effectiveQuota.limit}` : ""}`,
|
|
453
|
-
state: {
|
|
454
|
-
service_execution_id: execution.service_execution_id,
|
|
455
|
-
service_id: execution.service_id,
|
|
456
|
-
status: execution.status,
|
|
457
|
-
phase: execution.phase,
|
|
458
|
-
current_capability_id: execution.current_capability_id,
|
|
459
|
-
checkout_required: execution.checkout_required,
|
|
460
|
-
next_action: execution.next_action,
|
|
461
|
-
delivery: delivery
|
|
462
|
-
? {
|
|
463
|
-
status: delivery.status,
|
|
464
|
-
delivery_mode: deliveryMode || undefined,
|
|
465
|
-
vault_artifact_id: delivery.vault_artifact_id,
|
|
466
|
-
vault_status: delivery.vault_status,
|
|
467
|
-
vault_payload_state: delivery.vault_payload_state,
|
|
468
|
-
reveal_status: delivery.reveal_status,
|
|
469
|
-
grant_status: delivery.grant_status ?? "missing",
|
|
470
|
-
grant_expires_at: delivery.grant_expires_at,
|
|
471
|
-
}
|
|
472
|
-
: undefined,
|
|
473
|
-
capabilities: capabilities.map((capability) => ({
|
|
474
|
-
capability_id: capability.capability_id,
|
|
475
|
-
agent_visible: capability.agent_visible,
|
|
476
|
-
requires_payment: capability.requires_payment,
|
|
477
|
-
vault_required: capability.vault_required,
|
|
478
|
-
delivery_email_required: capability.delivery_email_required,
|
|
479
|
-
delivery_email_purpose: capability.delivery_email_purpose,
|
|
480
|
-
price_amount_minor: capability.price_amount_minor,
|
|
481
|
-
price_currency: capability.price_currency,
|
|
482
|
-
free_quota_limit: capability.free_quota_limit,
|
|
483
|
-
})),
|
|
484
|
-
result_items: (input.resultItems ?? []).map((item) => ({
|
|
485
|
-
result_item_id: item.service_capability_result_item_id,
|
|
486
|
-
rank: item.rank,
|
|
487
|
-
display_title: item.display_title,
|
|
488
|
-
safe_payload: item.safe_payload,
|
|
489
|
-
})),
|
|
490
|
-
effective_quota: input.effectiveQuota,
|
|
491
|
-
},
|
|
492
|
-
next_actions: nextActions,
|
|
493
|
-
recovery,
|
|
494
|
-
...(input.resultItems?.length
|
|
495
|
-
? { visible_results: input.resultItems.map((item) => ({ rank: item.rank, title: item.display_title, safe_payload: item.safe_payload })) }
|
|
496
|
-
: {}),
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
function deliveryEmailGuidance(purpose) {
|
|
500
|
-
switch (purpose) {
|
|
501
|
-
case "receipt":
|
|
502
|
-
return "Ask the human for their email. It is used to send the order receipt; never invent or substitute an address.";
|
|
503
|
-
case "claim":
|
|
504
|
-
return "Ask the human for their email. It is used to send the protected result claim link; never invent or substitute an address.";
|
|
505
|
-
case "receipt_and_claim":
|
|
506
|
-
return "Ask the human for their email. It is used to send the order receipt and protected result claim link; never invent or substitute an address.";
|
|
507
|
-
default:
|
|
508
|
-
return "Ask the human for the required email and state only the Backend-declared purpose; never invent or substitute an address.";
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
function firstPrePurchaseCapability(capabilities) {
|
|
512
|
-
return capabilities.find((capability) => capability.agent_visible && !capability.requires_payment)
|
|
513
|
-
?? capabilities.find((capability) => capability.agent_visible);
|
|
514
|
-
}
|
|
515
|
-
function firstPaidCapability(capabilities) {
|
|
516
|
-
return (capabilities.find((capability) => capability.requires_payment && capability.vault_required) ??
|
|
517
|
-
capabilities.find((capability) => capability.requires_payment));
|
|
518
|
-
}
|
|
519
|
-
function needsHumanSelection(execution, resultItem) {
|
|
520
|
-
if (execution.phase !== "pre_purchase")
|
|
521
|
-
return false;
|
|
522
|
-
return execution.next_action === "select_candidate" || execution.next_action === "human_action_required" || resultItem !== undefined;
|
|
523
|
-
}
|
|
524
|
-
function latestServiceLine(cart) {
|
|
525
|
-
return [...cart.items].reverse().find((item) => item.service_execution_id);
|
|
526
|
-
}
|
|
527
|
-
function lineID(line) {
|
|
528
|
-
return line.cart_item_id ?? line.line_item_id ?? line.checkout_item_id ?? "<unknown_line>";
|
|
529
|
-
}
|