@itpay/cli 2.1.34 → 2.1.40
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/dist/src/client/backend.js +5 -0
- package/dist/src/commands/aftercare.js +12 -0
- package/dist/src/commands/buy.js +12 -56
- package/dist/src/commands/cart.js +2 -0
- package/dist/src/commands/catalog.js +24 -9
- package/dist/src/commands/checkout_handoff.js +11 -99
- package/dist/src/commands/guidance.js +1 -184
- package/dist/src/commands/order.js +20 -2
- package/dist/src/commands/rail_catalog.js +1 -261
- package/dist/src/commands/readyz.js +2 -2
- package/dist/src/commands/refund.js +10 -54
- package/dist/src/commands/services/actions.js +133 -0
- package/dist/src/commands/services/commerce.js +197 -0
- package/dist/src/commands/services/lifecycle.js +418 -0
- package/dist/src/commands/services/results.js +130 -0
- package/dist/src/commands/services.js +14 -2613
- package/dist/src/main.js +81 -30
- package/dist/src/render/checkout_handoff.js +101 -0
- package/dist/src/render/checkout_plan.js +47 -0
- package/dist/src/services/adapter.js +1 -0
- package/dist/src/services/presentation.js +198 -0
- package/dist/src/services/rail/actions.js +51 -0
- package/dist/src/services/rail/adapter.js +27 -0
- package/dist/src/services/rail/aftercare.js +45 -0
- package/dist/src/services/rail/catalog.js +261 -0
- package/dist/src/services/rail/checkout.js +19 -0
- package/dist/src/services/rail/fulfillment.js +76 -0
- package/dist/src/services/rail/guide.js +110 -0
- package/dist/src/services/rail/invocation.js +54 -0
- package/dist/src/services/rail/page.js +76 -0
- package/dist/src/services/rail/pagination.js +6 -0
- package/dist/src/services/rail/query.js +71 -0
- package/dist/src/services/rail/readers.js +107 -0
- package/dist/src/services/rail/reading.js +125 -0
- package/dist/src/services/rail/recovery.js +51 -0
- package/dist/src/services/rail/results.js +378 -0
- package/dist/src/services/rail/review.js +268 -0
- package/dist/src/services/rail/saved.js +49 -0
- package/dist/src/services/rail/wait.js +11 -0
- package/dist/src/services/registry.js +31 -0
- package/dist/src/services/schema.js +21 -0
- package/dist/src/services/shared/envelope.js +184 -0
- package/dist/src/services/shared/input.js +39 -0
- package/dist/src/services/shared/pagination.js +28 -0
- package/dist/src/services/shared/platform.js +365 -0
- package/dist/src/services/shared/refund.js +40 -0
- package/dist/src/services/shared/workflow.js +28 -0
- package/dist/src/state/account_auth.js +25 -10
- package/dist/src/state/config.js +2 -2
- package/docs/agent/buyer/cart-checkout.json +2 -1
- package/docs/agent/buyer/catalog-list.json +6 -4
- package/docs/agent/buyer/identity-and-sessions.json +3 -2
- package/docs/agent/buyer/install-and-setup.json +8 -3
- package/docs/agent/buyer/orders-refunds.json +14 -5
- package/docs/agent/buyer/payment-flow.json +2 -1
- package/docs/agent/buyer/quickstart.json +3 -2
- package/docs/agent/buyer/rail-booking.json +38 -24
- package/docs/agent/buyer/service-feedback.json +2 -2
- package/docs/cli-reference/commands/aftercare.md +15 -0
- package/docs/cli-reference/commands/cart/add.md +4 -0
- package/docs/cli-reference/commands/cart/next.md +4 -0
- package/docs/cli-reference/commands/readyz.md +2 -50
- package/docs/cli-reference/commands/services/checkout.md +4 -0
- package/docs/cli-reference/commands/services/next.md +8 -35
- package/docs/cli-reference/commands/services/quote.md +1 -1
- package/docs/cli-reference/commands/services/run.md +8 -12
- package/docs/cli-reference/commands/services/start.md +3 -1
- package/docs/cli-reference/conventions.md +1 -1
- package/docs/cli-reference/index.md +2 -0
- package/package.json +4 -3
- package/skills/itpay/SKILL.md +37 -25
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { CommandContractError, writeCommandEnvelope } from "./envelope.js";
|
|
2
|
+
// Bound the final qualified output by whole saved rows; the cursor follows the emitted rows.
|
|
3
|
+
export function writeSavedPage(envelope, options, key, offset, continuation, updateMetadata) {
|
|
4
|
+
const result = envelope.result ?? {};
|
|
5
|
+
const rows = result[key];
|
|
6
|
+
if (!Array.isArray(rows))
|
|
7
|
+
throw new Error("saved page has no rows");
|
|
8
|
+
const bound = (offset === 0 ? 12 : 32) * 1024;
|
|
9
|
+
const render = (output) => writeCommandEnvelope(envelope, { ...options, output });
|
|
10
|
+
if (!options.all) {
|
|
11
|
+
for (;;) {
|
|
12
|
+
let text = "";
|
|
13
|
+
render(value => { text += value; });
|
|
14
|
+
if (Buffer.byteLength(text, "utf8") <= bound)
|
|
15
|
+
break;
|
|
16
|
+
if (rows.length <= 1) {
|
|
17
|
+
throw new CommandContractError("saved_row_too_large", "saved row exceeds the readable page limit", "这条已保存记录超过当前页面大小;事实仍保留,请读取该条详情。", rows[0]?.detail?.command ? [rows[0].detail] : []);
|
|
18
|
+
}
|
|
19
|
+
rows.pop();
|
|
20
|
+
result.truncated_reason = "cli_output_byte_budget";
|
|
21
|
+
result.count = rows.length;
|
|
22
|
+
updateMetadata(result, rows.length);
|
|
23
|
+
result.next_offset = offset + rows.length;
|
|
24
|
+
envelope.next = continuation(offset + rows.length);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
writeCommandEnvelope(envelope, options);
|
|
28
|
+
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { taskJournalPath } from "../../state/config.js";
|
|
2
|
+
import { TaskJournal } from "../../state/task_journal.js";
|
|
3
|
+
import { buildCheckoutHandoff } from "../../render/checkout_handoff.js";
|
|
4
|
+
import { buildAgentChatHandoff } from "../../render/markdown.js";
|
|
5
|
+
import { platformKeyForHost } from "../../render/plan.js";
|
|
6
|
+
import { appendFeedbackPostmortemInstruction, CommandContractError, isTerminalServiceExecutionStatus, shellArgument, plainValueLines } from "./envelope.js";
|
|
7
|
+
// AUTH04: record the pause point of each Service Execution so a later
|
|
8
|
+
// `itpay auth login` can resume the SAME task. Ids/stage/resume command only —
|
|
9
|
+
// no chat content, PII or credentials. Best-effort: journaling never breaks
|
|
10
|
+
// a command.
|
|
11
|
+
export function journalTaskState(serviceExecutionID, envelope, env) {
|
|
12
|
+
try {
|
|
13
|
+
const journal = new TaskJournal(taskJournalPath(env));
|
|
14
|
+
const serviceID = typeof envelope.result?.service_id === "string" ? envelope.result.service_id : undefined;
|
|
15
|
+
journal.observe(serviceExecutionID, envelope.status, serviceID ? `itpay services run ${serviceID} --execution ${serviceExecutionID} --json` : undefined);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
/* local journal is advisory; never fail the envelope */
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export const serviceActionStatuses = new Set(["pending", "approved", "rejected", "expired", "cancelled"]);
|
|
22
|
+
export function requiredInputFields(schema) {
|
|
23
|
+
const required = schema?.required;
|
|
24
|
+
return Array.isArray(required) ? required.filter((field) => typeof field === "string") : [];
|
|
25
|
+
}
|
|
26
|
+
export function serviceResultPlainLines(result) {
|
|
27
|
+
return plainValueLines(result);
|
|
28
|
+
}
|
|
29
|
+
export function queryText(input) {
|
|
30
|
+
const value = Object.values(input).find((item) => typeof item === "string" && item.trim() !== "");
|
|
31
|
+
return typeof value === "string" ? value : JSON.stringify(input);
|
|
32
|
+
}
|
|
33
|
+
export function missingRequiredInput(schema, input) {
|
|
34
|
+
return requiredInputFields(schema).filter((field) => {
|
|
35
|
+
if (!(field in input) || input[field] === null || input[field] === undefined)
|
|
36
|
+
return true;
|
|
37
|
+
return typeof input[field] === "string" && String(input[field]).trim() === "";
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export function checkoutCommand(serviceExecutionID, capability, input, fillMissing = true) {
|
|
41
|
+
const lockedInput = { ...input };
|
|
42
|
+
if (fillMissing) {
|
|
43
|
+
for (const field of missingRequiredInput(capability.input_schema, lockedInput))
|
|
44
|
+
lockedInput[field] = "<value>";
|
|
45
|
+
}
|
|
46
|
+
return `itpay services checkout ${serviceExecutionID} --capability ${capability.capability_id}${formatInputOptions(lockedInput)}${capability.delivery_email_required ? " --email <email>" : ""} --json`;
|
|
47
|
+
}
|
|
48
|
+
export function capabilityPrice(capability) {
|
|
49
|
+
if (capability.pricing_method !== undefined)
|
|
50
|
+
return "动态报价(最终金额以锁定报价为准)";
|
|
51
|
+
return capability.price_amount_minor !== undefined && capability.price_currency
|
|
52
|
+
? formatMoney(capability.price_amount_minor, capability.price_currency)
|
|
53
|
+
: "当前发布价格";
|
|
54
|
+
}
|
|
55
|
+
export function purchaseConfirmationInstruction(context, price, deliveryEmailRequired, deliveryEmailPurpose, candidateTitle = "") {
|
|
56
|
+
const emailPurpose = deliveryEmailPurposeText(deliveryEmailPurpose);
|
|
57
|
+
if (context === "quota_exhausted") {
|
|
58
|
+
return deliveryEmailRequired
|
|
59
|
+
? `免费额度已用完,本次没有发送到数据来源,也没有创建付款页面。只向用户说明:继续当前请求需要支付 ${price},并提供${emailPurpose};请确认是否购买并提供邮箱。然后停止等待。用户明确同意并提供真实邮箱前,Agent 不执行 next.command,也不创建或尝试其他购买路径。`
|
|
60
|
+
: `免费额度已用完,本次没有发送到数据来源,也没有创建付款页面。只向用户说明:“继续当前请求需要支付 ${price},是否购买?”然后停止等待。用户明确同意前,Agent 不执行 next.command,也不创建或尝试其他购买路径。`;
|
|
61
|
+
}
|
|
62
|
+
const selected = candidateTitle ? `已选择 ${candidateTitle}。` : "当前候选已经确认。";
|
|
63
|
+
return deliveryEmailRequired
|
|
64
|
+
? `${selected}后续服务尚未购买。只向用户说明:继续购买需要支付 ${price},并提供${emailPurpose};请确认是否购买并提供邮箱。然后停止。用户明确同意并提供真实邮箱前,Agent 不执行 next.command,也不创建新的服务或付款页面。`
|
|
65
|
+
: `${selected}后续服务尚未购买。只向用户说明:“继续购买后续服务需要支付 ${price},是否购买?”然后停止。用户明确同意前,Agent 不执行 next.command,也不创建新的服务或付款页面。`;
|
|
66
|
+
}
|
|
67
|
+
export function deliveryEmailPurposeText(purpose) {
|
|
68
|
+
switch (purpose) {
|
|
69
|
+
case "receipt":
|
|
70
|
+
return "用于发送订单收据的真实邮箱";
|
|
71
|
+
case "claim":
|
|
72
|
+
return "用于发送交付认领链接的真实邮箱";
|
|
73
|
+
case "receipt_and_claim":
|
|
74
|
+
return "用于发送订单收据和交付认领链接的真实邮箱";
|
|
75
|
+
default:
|
|
76
|
+
return "服务端声明用途的真实邮箱";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export function paidContinuation(model, action, input) {
|
|
80
|
+
if (!action.capability_id)
|
|
81
|
+
return null;
|
|
82
|
+
const capability = model.capabilities.find((item) => item.capability_id === action.capability_id && item.requires_payment);
|
|
83
|
+
if (!capability)
|
|
84
|
+
return null;
|
|
85
|
+
const price = capabilityPrice(capability);
|
|
86
|
+
const stateBacked = model.execution.status === "quota_exhausted" || model.execution.status === "human_action_approved";
|
|
87
|
+
return {
|
|
88
|
+
capability,
|
|
89
|
+
price,
|
|
90
|
+
checkout: {
|
|
91
|
+
capability_id: capability.capability_id,
|
|
92
|
+
...(capability.price_amount_minor !== undefined && capability.price_currency ? {
|
|
93
|
+
price: { amount_minor: capability.price_amount_minor, currency: capability.price_currency },
|
|
94
|
+
} : {}),
|
|
95
|
+
delivery_email_required: capability.delivery_email_required,
|
|
96
|
+
...(capability.delivery_email_purpose ? { delivery_email_purpose: capability.delivery_email_purpose } : {}),
|
|
97
|
+
},
|
|
98
|
+
next: {
|
|
99
|
+
command: checkoutCommand(model.execution.service_execution_id, capability, input, !stateBacked),
|
|
100
|
+
reason: `仅在用户明确同意支付 ${price}${capability.delivery_email_required ? " 并提供真实邮箱" : ""}后执行;否则停止`,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
export function quoteCommand(serviceExecutionID, capability, input) {
|
|
105
|
+
const lockedInput = { ...input };
|
|
106
|
+
for (const field of missingRequiredInput(capability.input_schema, lockedInput))
|
|
107
|
+
lockedInput[field] = "<value>";
|
|
108
|
+
return `itpay services quote ${serviceExecutionID} --capability ${capability.capability_id}${formatInputOptions(lockedInput)}${capability.delivery_email_required ? " --email <email>" : ""} --json`;
|
|
109
|
+
}
|
|
110
|
+
export function stableInput(input) {
|
|
111
|
+
return JSON.stringify(Object.fromEntries(Object.entries(input).sort(([left], [right]) => left.localeCompare(right))));
|
|
112
|
+
}
|
|
113
|
+
export function formatInputOptions(input) {
|
|
114
|
+
return Object.entries(input)
|
|
115
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
116
|
+
.map(([key, value]) => String(value) === "<value>"
|
|
117
|
+
? ` --input ${key}=<value>`
|
|
118
|
+
: ` --input ${shellArgument(`${key}=${typeof value === "object" && value !== null ? JSON.stringify(value) : String(value)}`)}`)
|
|
119
|
+
.join("");
|
|
120
|
+
}
|
|
121
|
+
export function actionInputError(serviceExecutionID, message, code = "service_action_invalid") {
|
|
122
|
+
return new CommandContractError(code, message, code === "candidate_not_found"
|
|
123
|
+
? "当前 rank 不存在或当前候选集不可用。不要新建 Execution,不要重新 invoke,不要构造候选 ID;只恢复同一 Execution 当前仍然有效的候选。"
|
|
124
|
+
: "使用当前 safe result 中的合法 action 和 candidate rank;需要人确认时先询问用户。", [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "重新读取同一 Execution 的当前可选动作" }]);
|
|
125
|
+
}
|
|
126
|
+
export function terminalExecutionEnvelope(model) {
|
|
127
|
+
const execution = model.execution;
|
|
128
|
+
const currentDelivery = model.current_delivery ?? model.delivery_bindings.at(-1);
|
|
129
|
+
if (!isTerminalServiceExecutionStatus(execution.status) ||
|
|
130
|
+
(model.workflow_entry && (currentDelivery || serviceDeliveryMode(model) === "agent_visible_result") && ["completed", "delivery_completed"].includes(execution.status))) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const paid = model.checkout_bindings.some((binding) => binding.status === "payment_verified") || Boolean(currentDelivery?.order_id);
|
|
134
|
+
const paidFailure = execution.status === "failed" && paid;
|
|
135
|
+
return {
|
|
136
|
+
status: execution.status,
|
|
137
|
+
result: {
|
|
138
|
+
service_execution_id: execution.service_execution_id,
|
|
139
|
+
service_id: execution.service_id,
|
|
140
|
+
phase: execution.phase,
|
|
141
|
+
...(currentDelivery?.order_id ? { order_id: currentDelivery.order_id, aftercare: { optional: true, command: `itpay feedback submit --order ${shellArgument(currentDelivery.order_id)} --json`, reason: "告知原单结果后按需复盘,不猜评分或上传隐私" } } : {}),
|
|
142
|
+
},
|
|
143
|
+
instruction: execution.status === "refunded"
|
|
144
|
+
? "告诉用户这笔服务已经退款并永久结束。Agent 不重放服务步骤、不创建付款页面或尝试读取旧交付。"
|
|
145
|
+
: paidFailure
|
|
146
|
+
? appendFeedbackPostmortemInstruction("告诉用户:付款和订单已经记录,但本次服务没有正常完成,不需要再次付款或重新下单。然后从同一订单检查退款状态;Agent 不重放服务步骤、创建付款页面或再次调用数据来源,也不把技术故障归咎于用户。", "failed")
|
|
147
|
+
: "告诉用户本次服务已经结束且没有可继续的交付。Agent 不重放服务步骤或创建付款页面。",
|
|
148
|
+
next: null,
|
|
149
|
+
recovery: [
|
|
150
|
+
...(paidFailure
|
|
151
|
+
? [{
|
|
152
|
+
command: currentDelivery?.order_id
|
|
153
|
+
? `itpay order ${currentDelivery.order_id} --json`
|
|
154
|
+
: "itpay orders --json",
|
|
155
|
+
reason: "恢复同一笔已付款订单及其退款状态",
|
|
156
|
+
}]
|
|
157
|
+
: []),
|
|
158
|
+
{
|
|
159
|
+
command: `itpay services events ${execution.service_execution_id} --json`,
|
|
160
|
+
reason: "仅在需要诊断终止原因时读取事件",
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
export function serviceAllowedActionCommand(model, action) {
|
|
166
|
+
const executionID = model.execution.service_execution_id;
|
|
167
|
+
const capability = action.capability_id
|
|
168
|
+
? model.capabilities.find((item) => item.capability_id === action.capability_id)
|
|
169
|
+
: undefined;
|
|
170
|
+
switch (action.type) {
|
|
171
|
+
case "invoke_capability": {
|
|
172
|
+
if (!capability)
|
|
173
|
+
return null;
|
|
174
|
+
const input = Object.fromEntries(requiredInputFields(capability.input_schema).map((field) => [field, "<value>"]));
|
|
175
|
+
return {
|
|
176
|
+
command: `itpay services invoke ${executionID} --capability ${capability.capability_id}${formatInputOptions(input)} --json`,
|
|
177
|
+
reason: "执行当前允许的 Agent-visible capability",
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
case "select_candidate":
|
|
181
|
+
return {
|
|
182
|
+
command: `itpay services action ${executionID} --action select_candidate --actor-type human --status approved --candidate <rank> --json`,
|
|
183
|
+
reason: "仅在用户明确选择后提交当前候选 rank",
|
|
184
|
+
};
|
|
185
|
+
case "prepare_quote": {
|
|
186
|
+
return paidContinuation(model, action, {})?.next ?? null;
|
|
187
|
+
}
|
|
188
|
+
case "resume_checkout":
|
|
189
|
+
return { command: `itpay services checkout ${executionID} --resume --json`, reason: "恢复同一 Checkout,不创建第二笔" };
|
|
190
|
+
case "wait":
|
|
191
|
+
return { command: `itpay services next ${executionID} --json`, reason: "等待 durable execution 推进" };
|
|
192
|
+
case "view_delivery":
|
|
193
|
+
return { command: `itpay services next ${executionID} --json`, reason: "读取当前交付模式" };
|
|
194
|
+
default:
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
export function serviceDeliveryMode(model) {
|
|
199
|
+
const delivery = model.current_delivery ?? model.delivery_bindings.at(-1);
|
|
200
|
+
const entry = model.capabilities.find(capability => capability.capability_id === model.workflow_entry?.capability_id);
|
|
201
|
+
if (!delivery && model.workflow?.status === "completed" && entry?.requires_payment === false && !entry.vault_required)
|
|
202
|
+
return "agent_visible_result";
|
|
203
|
+
const explicit = String(delivery?.redacted_summary?.delivery_mode ?? "");
|
|
204
|
+
if (explicit)
|
|
205
|
+
return explicit;
|
|
206
|
+
return delivery?.vault_artifact_id ? "vault_artifact" : "";
|
|
207
|
+
}
|
|
208
|
+
export function normalizeGrantStatus(status) {
|
|
209
|
+
return !status || status === "missing" ? "none" : status;
|
|
210
|
+
}
|
|
211
|
+
export function grantedResultEnvelope(response, orderID) {
|
|
212
|
+
return {
|
|
213
|
+
status: "granted_result_ready",
|
|
214
|
+
result: {
|
|
215
|
+
service_execution_id: response.service_execution_id,
|
|
216
|
+
...(orderID ? { order_id: orderID, aftercare: { optional: true, command: `itpay feedback submit --order ${shellArgument(orderID)} --json`, reason: "交付答复后按需复盘,不猜评分或上传隐私" } } : {}),
|
|
217
|
+
...(response.expires_at ? { grant_expires_at: response.expires_at } : {}),
|
|
218
|
+
granted_fields: Object.keys(response.result),
|
|
219
|
+
payload: response.result,
|
|
220
|
+
},
|
|
221
|
+
instruction: orderID
|
|
222
|
+
? appendFeedbackPostmortemInstruction("结果来自当前有效 Vault Grant;只使用本次授权字段,过期后停止读取并重新请求用户同意。", "delivered")
|
|
223
|
+
: "结果来自当前有效 Vault Grant;只使用本次授权字段,过期后停止读取并重新请求用户同意。",
|
|
224
|
+
next: null,
|
|
225
|
+
recovery: [],
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
export function servicesNextPlainResult(result) {
|
|
229
|
+
return plainValueLines(result);
|
|
230
|
+
}
|
|
231
|
+
export function parseKeyValueList(values) {
|
|
232
|
+
const result = {};
|
|
233
|
+
for (const value of values ?? []) {
|
|
234
|
+
const index = value.indexOf("=");
|
|
235
|
+
if (index <= 0) {
|
|
236
|
+
throw new Error(`invalid --input "${value}", expected key=value`);
|
|
237
|
+
}
|
|
238
|
+
result[value.slice(0, index)] = parseValue(value.slice(index + 1));
|
|
239
|
+
}
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
export function collectOption(value, previous = []) {
|
|
243
|
+
previous.push(value);
|
|
244
|
+
return previous;
|
|
245
|
+
}
|
|
246
|
+
export function parseValue(value) {
|
|
247
|
+
// Structured inputs (objects/arrays) are passed as JSON; a string that merely
|
|
248
|
+
// starts with a JSON delimiter but is not valid JSON stays a string — the
|
|
249
|
+
// backend schema validates the field type either way.
|
|
250
|
+
if (value.startsWith("{") || value.startsWith("[")) {
|
|
251
|
+
try {
|
|
252
|
+
return JSON.parse(value);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return value;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (value === "true")
|
|
259
|
+
return true;
|
|
260
|
+
if (value === "false")
|
|
261
|
+
return false;
|
|
262
|
+
if (/^-?\d+(\.\d+)?$/.test(value))
|
|
263
|
+
return Number(value);
|
|
264
|
+
return value;
|
|
265
|
+
}
|
|
266
|
+
export function buildServicesCheckoutEnvelope(response, checkoutURL, plan, agentType, target) {
|
|
267
|
+
const checkout = response.checkout;
|
|
268
|
+
const platform = platformKeyForHost(plan.host);
|
|
269
|
+
const amount = formatMoney(checkout.checkout.amount_minor, checkout.checkout.currency);
|
|
270
|
+
const presentationHandoff = buildCheckoutHandoff({
|
|
271
|
+
platform,
|
|
272
|
+
url: plan.linkOnlyURL ?? checkoutURL,
|
|
273
|
+
mobileUrl: checkoutURL,
|
|
274
|
+
amount,
|
|
275
|
+
plan,
|
|
276
|
+
...(agentType ? { agentType } : {}),
|
|
277
|
+
...(target ? { target } : {}),
|
|
278
|
+
...(plan.preferredQRSources[0] ? { qrImageURL: plan.preferredQRSources[0] } : {}),
|
|
279
|
+
...(plan.ideImageAttach?.status === "downloaded" && plan.ideImageAttach.localPath
|
|
280
|
+
? { localPath: plan.ideImageAttach.localPath }
|
|
281
|
+
: {}),
|
|
282
|
+
...(platform === "markdown" ? { markdown: buildAgentChatHandoff(plan).markdown } : {}),
|
|
283
|
+
});
|
|
284
|
+
return {
|
|
285
|
+
status: "human_checkout_required",
|
|
286
|
+
result: {
|
|
287
|
+
service_execution_id: response.binding.service_execution_id,
|
|
288
|
+
checkout_id: checkout.checkout.checkout_id,
|
|
289
|
+
capability_id: checkoutCapabilityID(response),
|
|
290
|
+
items: plan.orderItems ?? [],
|
|
291
|
+
amount,
|
|
292
|
+
...(checkout.checkout.expires_at ? {
|
|
293
|
+
// The shared server-owned payable deadline D: refreshing the page or
|
|
294
|
+
// re-creating a checkout can never extend it; past it the checkout
|
|
295
|
+
// and pending order are terminally cancelled.
|
|
296
|
+
payment_deadline_at: checkout.checkout.expires_at,
|
|
297
|
+
} : {}),
|
|
298
|
+
},
|
|
299
|
+
handoff: presentationHandoff.handoff,
|
|
300
|
+
instruction: presentationHandoff.instruction,
|
|
301
|
+
next: {
|
|
302
|
+
command: plan.afterActionCommand ?? `itpay checkout --id ${checkout.checkout.checkout_id} --token ${checkout.display_token} --json`,
|
|
303
|
+
reason: "仅在用户完成付款操作或要求查询后,读取同一 Checkout 的权威状态",
|
|
304
|
+
},
|
|
305
|
+
recovery: [],
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
export function fallbackCardURL(baseURL, checkoutID, displayToken) {
|
|
309
|
+
const root = baseURL.replace(/\/$/, "");
|
|
310
|
+
return `${root}/v1/checkouts/${encodeURIComponent(checkoutID)}/card?display_token=${encodeURIComponent(displayToken)}`;
|
|
311
|
+
}
|
|
312
|
+
export function fallbackCardPNGURL(baseURL, checkoutID, displayToken) {
|
|
313
|
+
return `${fallbackCardURL(baseURL, checkoutID, displayToken)}.png`.replace("/card?", "/card.png?");
|
|
314
|
+
}
|
|
315
|
+
export function checkoutCapabilityID(response, fallback = "") {
|
|
316
|
+
return response.capability_id || fallback;
|
|
317
|
+
}
|
|
318
|
+
export function absolutePublicURL(baseURL, value) {
|
|
319
|
+
try {
|
|
320
|
+
return new URL(value, baseURL.endsWith("/") ? baseURL : `${baseURL}/`).toString();
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return value;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
export function formatMoney(amountMinor, currency) {
|
|
327
|
+
return `${(amountMinor / 100).toFixed(2)} ${currency}`;
|
|
328
|
+
}
|
|
329
|
+
export function normalizeServiceActionStatus(status, serviceExecutionID) {
|
|
330
|
+
const normalized = status.trim().toLowerCase();
|
|
331
|
+
if (!serviceActionStatuses.has(normalized)) {
|
|
332
|
+
throw actionInputError(serviceExecutionID, `invalid --status "${status}". Supported: pending, approved, rejected, expired, cancelled`);
|
|
333
|
+
}
|
|
334
|
+
return normalized;
|
|
335
|
+
}
|
|
336
|
+
export function tokenizedCheckoutURL(checkoutURL, displayToken, qrPayload) {
|
|
337
|
+
if (qrPayload.trim().length > 0) {
|
|
338
|
+
return qrPayload;
|
|
339
|
+
}
|
|
340
|
+
if (checkoutURL.trim().length === 0 || displayToken.trim().length === 0) {
|
|
341
|
+
return checkoutURL;
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
const parsed = new URL(checkoutURL);
|
|
345
|
+
if (!parsed.searchParams.has("display_token")) {
|
|
346
|
+
parsed.searchParams.set("display_token", displayToken);
|
|
347
|
+
}
|
|
348
|
+
return parsed.toString();
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
const separator = checkoutURL.includes("?") ? "&" : "?";
|
|
352
|
+
return `${checkoutURL}${separator}display_token=${encodeURIComponent(displayToken)}`;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
export function publicPurchaseTemplate(value) {
|
|
356
|
+
if (value === undefined)
|
|
357
|
+
return undefined;
|
|
358
|
+
const invalid = () => new CommandContractError("public_operation_contract_invalid", "incomplete public purchase operation", "服务返回的准备购买合同不完整;保留当前快照与已有购买并停止,不能退回旧入口拼接凭据。", [], undefined, { reason: "purchase_contract_missing" });
|
|
359
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || !("service_id" in value) || typeof value.service_id !== "string" || !("operation_id" in value) || typeof value.operation_id !== "string" || !("input" in value) || typeof value.input !== "object" || value.input === null || Array.isArray(value.input) || !("continuation" in value))
|
|
360
|
+
throw invalid();
|
|
361
|
+
const ref = value.continuation;
|
|
362
|
+
if (typeof ref !== "object" || ref === null || Array.isArray(ref) || !("service_execution_id" in ref) || typeof ref.service_execution_id !== "string" || !("snapshot_id" in ref) || typeof ref.snapshot_id !== "string")
|
|
363
|
+
throw invalid();
|
|
364
|
+
return { command: `itpay services run ${shellArgument(value.service_id)} --operation ${shellArgument(value.operation_id)} --input-json <file> --json`, input_example: { input: value.input, continuation: { service_execution_id: ref.service_execution_id, snapshot_id: ref.snapshot_id } }, executable: false };
|
|
365
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function refundStateEnvelope(refund, status) {
|
|
2
|
+
const terminal = ["succeeded", "failed", "cancelled", "rejected"].includes(refund.status);
|
|
3
|
+
let instruction = refund.decision_mode === "manual"
|
|
4
|
+
? "先告诉用户退款已进入人工审核,原交付保持冻结;人工审核不等于拒绝,等待服务器决定,不要重复申请或承诺结果。"
|
|
5
|
+
: "先告诉用户退款申请已经记录,原交付已冻结;自动路径表示系统会继续处理,但只有最终 succeeded 才能确认退款成功。然后只跟踪同一退款,不读取交付、不创建授权或重复申请。";
|
|
6
|
+
if (!refund.access_locked)
|
|
7
|
+
instruction = "先告诉用户退款当前没有锁定交付;按服务器事实解释当前状态,不要自行推断退款结果、到账时间或交付资格。";
|
|
8
|
+
if (refund.status === "succeeded")
|
|
9
|
+
instruction = "先告诉用户本次退款的钱款已由 ItPay 确认成功;按原订单核对金额范围和其他履约事实,不能推断整单全部退款。平台交付访问仍按返回的锁处理,不恢复旧授权。";
|
|
10
|
+
if (refund.status === "cancelled" || refund.status === "rejected")
|
|
11
|
+
instruction = "先告诉用户退款没有执行,交付资格可以恢复;旧读取授权不会复活,需要用户重新授权。不要把取消或拒绝说成退款成功。";
|
|
12
|
+
if (refund.status === "failed") {
|
|
13
|
+
if (refund.failure_class === "known_no_effect")
|
|
14
|
+
instruction = "先告诉用户本次退款请求确认未发送,不能说退款已成功;Agent 不自行重试,请用户等待平台管理员决定是否重新执行。";
|
|
15
|
+
else if (refund.failure_class === "retryable")
|
|
16
|
+
instruction = "先告诉用户渠道明确返回可重试失败,但 Agent 不会自行重试或重复申请;请用户等待平台管理员处理。";
|
|
17
|
+
else if (refund.failure_class === "outcome_unknown")
|
|
18
|
+
instruction = "先告诉用户退款渠道结果未知,原交付继续锁定且必须先由平台对账;禁止重试、重复申请或承诺退款结果。";
|
|
19
|
+
else if (refund.failure_class === "permanent")
|
|
20
|
+
instruction = "先告诉用户渠道明确拒绝本次退款,当前不能承诺退款成功;不要重试,请用户联系平台支持。";
|
|
21
|
+
else
|
|
22
|
+
instruction = "先告诉用户退款没有正常完成,当前不能承诺退款成功;不要重试或重复申请,请用户联系平台支持。";
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
status,
|
|
26
|
+
result: {
|
|
27
|
+
refund_request_id: refund.refund_request_id,
|
|
28
|
+
order_id: refund.order_id,
|
|
29
|
+
decision_mode: refund.decision_mode,
|
|
30
|
+
refund_status: refund.status,
|
|
31
|
+
consumption_state: refund.consumption_state,
|
|
32
|
+
...(refund.failure_class ? { failure_class: refund.failure_class } : {}),
|
|
33
|
+
access_locked: refund.access_locked,
|
|
34
|
+
can_cancel: refund.can_cancel,
|
|
35
|
+
},
|
|
36
|
+
instruction: instruction + " consumption_state是平台交付内容的消费事实,access_locked是交付访问锁;不能据此推断线下服务是否已使用或退款资格。",
|
|
37
|
+
next: terminal ? null : { command: `itpay refund get ${refund.refund_request_id} --json`, reason: "按需读取同一退款;不自动循环等待" },
|
|
38
|
+
recovery: [],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { executionInputContext, operationInputTemplate, serviceInputTemplate } from "./input.js";
|
|
2
|
+
import { requiredInputFields } from "./platform.js";
|
|
3
|
+
export function workflowEnvelope(model) {
|
|
4
|
+
if (!model.workflow_entry || ["completed", "delivery"].includes(model.workflow?.status ?? ""))
|
|
5
|
+
return undefined;
|
|
6
|
+
const id = model.execution.service_execution_id;
|
|
7
|
+
const code = model.workflow?.error_code;
|
|
8
|
+
if (["failed", "quota_paused"].includes(model.workflow?.status ?? "") && ["login_required", "rate_limited"].includes(code ?? "")) {
|
|
9
|
+
const login = code === "login_required";
|
|
10
|
+
return { status: code, result: { service_execution_id: id, current_step: model.workflow?.current_step, ...(model.quota ? { quota: model.quota } : {}), ...(model.workflow?.dependency ? { dependency: model.workflow.dependency } : {}) }, instruction: login ? "当前账户需要登录;展示官方登录入口后,按返回恢复动作继续同一执行,不重新创建。" : "本执行达到调用额度;尊重服务端恢复时间,只继续同一执行,不自动重查或创建付款。", next: login ? { command: "itpay auth login --json", reason: "登录后恢复同一执行" } : null, recovery: [] };
|
|
11
|
+
}
|
|
12
|
+
const action = model.workflow?.human_action;
|
|
13
|
+
const state = model.workflow?.status ?? "input_required";
|
|
14
|
+
if (state === "human_action" && action)
|
|
15
|
+
return {
|
|
16
|
+
status: "confirmation_required", result: { service_execution_id: id, human_action: action },
|
|
17
|
+
instruction: "展示本次操作的待确认内容;已有明确同意且目标与版本未变时沿用,只补缺失的真实同意。按完整模板提交同一执行;选择对象不等于购买批准。",
|
|
18
|
+
next: null, recovery: [], interaction: { schema_version: "itpay.interaction.v1", stage: "confirmation_required", input_template: { command: `itpay services action ${id} --action ${action.action_type} --actor-type human --status approved --input-json <file> --json`, input: serviceInputTemplate(action.input_schema), required_input: ["file", ...requiredInputFields(action.input_schema)], executable: false } },
|
|
19
|
+
};
|
|
20
|
+
const inputRequired = state === "input_required";
|
|
21
|
+
const failed = ["failed", "recovery_required", "cancelled"].includes(state);
|
|
22
|
+
const dependency = model.workflow?.dependency;
|
|
23
|
+
return { status: dependency ? "dependency_unavailable" : state, result: { service_execution_id: id, service_id: model.execution.service_id, ...(model.workflow ? { current_step: model.workflow.current_step, workflow_revision: model.workflow.revision, error_code: model.workflow.error_code } : {}), ...(model.workflow?.failure ? { failure: model.workflow.failure } : {}), ...(dependency ? { dependency } : {}), ...(inputRequired ? { input_schema: model.workflow_entry.input_schema } : {}) },
|
|
24
|
+
instruction: dependency ? "服务返回外部依赖故障;保留原输入及已有结果,只按返回的重试条件恢复。不猜供应商或错误原因,也不要求用户更改业务条件。" : failed ? "本执行未完成;核对返回原因和已有付款事实。没有明确恢复动作时停止,不重建购买。" : inputRequired ? "按当前锁定合同写入完整输入文件,继续同一执行,不猜字段。" : state === "payment" ? "已到准备付款入口步骤;报价与当前范围批准仍需核对,实际付款由用户在官方页面完成。" : "继续读取同一执行的当前状态。",
|
|
25
|
+
next: failed || inputRequired || dependency ? null : { command: state === "payment" ? `itpay services checkout ${id} --json` : `itpay services next ${id} --json`, reason: "继续同一执行" }, recovery: [],
|
|
26
|
+
...(inputRequired ? { interaction: { schema_version: "itpay.interaction.v1", stage: "input_required", input_template: operationInputTemplate(executionInputContext(model.execution), serviceInputTemplate(model.workflow_entry.input_schema, model.workflow?.query_input), requiredInputFields(model.workflow_entry.input_schema)) } } : {}),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { HttpError } from '../client/http.js';
|
|
2
|
+
import { HttpTransportError } from '../client/transport.js';
|
|
3
|
+
import { DeviceAuthorizationError, DeviceStateError, DeviceLockBusyError } from './device_authority.js';
|
|
1
4
|
import { createHash } from 'node:crypto';
|
|
2
5
|
import { dirname, resolve } from 'node:path';
|
|
3
6
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, rmSync } from 'node:fs';
|
|
@@ -38,10 +41,10 @@ export function agentAuth(action, baseURL, backend, env = process.env, fetcher =
|
|
|
38
41
|
const ACTIVE_SESSION_STAGES = ['created', 'waiting_provider', 'email_verification_required', 'merge_confirmation_required'];
|
|
39
42
|
function stageInstruction(stage) {
|
|
40
43
|
if (stage === 'email_verification_required') {
|
|
41
|
-
return '
|
|
44
|
+
return '官方页返回显式或历史身份验证状态;按页面当前要求完成验证。普通登录只需一种有效身份,不要求补齐邮箱和手机。完成后恢复原执行。';
|
|
42
45
|
}
|
|
43
46
|
if (stage === 'merge_confirmation_required') {
|
|
44
|
-
return '
|
|
47
|
+
return '官方页需要用户核对两侧账号证明及主账号,再明确确认或取消合并;这不是付款确认。不要替用户同意,不自动重启原服务或授权请求。';
|
|
45
48
|
}
|
|
46
49
|
return '用户仍在官方页面完成登录;保留当前 handoff,不要生成新二维码或新请求。';
|
|
47
50
|
}
|
|
@@ -75,12 +78,24 @@ class AuthRequestError extends Error {
|
|
|
75
78
|
}
|
|
76
79
|
// AUTH03: transport/read uncertainty never maps to "denied" or a new session —
|
|
77
80
|
// the stored session stays on disk and the caller retries the same poll.
|
|
78
|
-
function authStatusUnknown(sessionID) {
|
|
81
|
+
function authStatusUnknown(sessionID, error) {
|
|
82
|
+
// These errors have definite compatibility/identity/state facts. Let the
|
|
83
|
+
// command boundary report them rather than turning them into polling.
|
|
84
|
+
if ((error instanceof HttpError && error.status < 500)
|
|
85
|
+
|| (error instanceof AuthRequestError && error.httpStatus < 500)
|
|
86
|
+
|| error instanceof DeviceAuthorizationError || error instanceof DeviceStateError
|
|
87
|
+
|| error instanceof DeviceLockBusyError)
|
|
88
|
+
throw error;
|
|
89
|
+
const failure = error instanceof HttpTransportError
|
|
90
|
+
? { category: error.code, attempts: error.attempts }
|
|
91
|
+
: { category: 'authorization_status_unavailable' };
|
|
79
92
|
return {
|
|
80
93
|
status: 'auth_status_unknown',
|
|
81
|
-
result: sessionID ? { dashboard_auth_session_id: sessionID } : {},
|
|
82
|
-
instruction:
|
|
83
|
-
|
|
94
|
+
result: { ...failure, ...(sessionID ? { dashboard_auth_session_id: sessionID } : {}) },
|
|
95
|
+
instruction: sessionID
|
|
96
|
+
? '读取官方授权状态失败;原授权请求已保留。稍后重读同一状态,不要新建授权、不要清除本地登记。'
|
|
97
|
+
: '本轮账号绑定检查未完成,设备与绑定状态尚未确认;没有保存的授权请求。结束本轮,网络或服务恢复后可重新检查,不发起登录。',
|
|
98
|
+
next: sessionID ? { command: 'itpay auth status --json', reason: '稍后重读同一授权状态', poll_after_ms: 5000 } : null,
|
|
84
99
|
recovery: [],
|
|
85
100
|
};
|
|
86
101
|
}
|
|
@@ -134,7 +149,7 @@ async function accountAuth(action, baseURL, env, fetcher, backend) {
|
|
|
134
149
|
throw error;
|
|
135
150
|
// AUTH03: even the binding-status probe can hit transport failure; the
|
|
136
151
|
// truthful state is "unknown", never an implicit unbound or a fresh login.
|
|
137
|
-
return authStatusUnknown(
|
|
152
|
+
return authStatusUnknown(read(baseURL, env, purpose)?.sessionID, error);
|
|
138
153
|
}
|
|
139
154
|
}
|
|
140
155
|
async function request(path, init = {}) {
|
|
@@ -204,7 +219,7 @@ async function accountAuth(action, baseURL, env, fetcher, backend) {
|
|
|
204
219
|
// remains read-only and still reports the missing old session.
|
|
205
220
|
}
|
|
206
221
|
else {
|
|
207
|
-
return authStatusUnknown(open.sessionID);
|
|
222
|
+
return authStatusUnknown(open.sessionID, error);
|
|
208
223
|
}
|
|
209
224
|
}
|
|
210
225
|
}
|
|
@@ -280,7 +295,7 @@ async function accountAuth(action, baseURL, env, fetcher, backend) {
|
|
|
280
295
|
if (error instanceof AuthRequestError && (error.httpStatus === 404 || error.httpStatus === 410)) {
|
|
281
296
|
return sessionGone(env, purpose, baseURL, state.sessionID);
|
|
282
297
|
}
|
|
283
|
-
return authStatusUnknown(state.sessionID);
|
|
298
|
+
return authStatusUnknown(state.sessionID, error);
|
|
284
299
|
}
|
|
285
300
|
if (progress.status !== 'completed') {
|
|
286
301
|
if (backend) {
|
|
@@ -318,7 +333,7 @@ async function accountAuth(action, baseURL, env, fetcher, backend) {
|
|
|
318
333
|
throw error;
|
|
319
334
|
// Binding outcome unknown: keep the session so a retry can still bind
|
|
320
335
|
// the same completed login instead of creating a new one.
|
|
321
|
-
return authStatusUnknown(state.sessionID);
|
|
336
|
+
return authStatusUnknown(state.sessionID, error);
|
|
322
337
|
}
|
|
323
338
|
}
|
|
324
339
|
const claimed = await request(`/v1/dashboard/auth-sessions/${encodeURIComponent(state.sessionID)}/claim?start_token=${encodeURIComponent(state.startToken)}`, { method: 'POST' });
|
package/dist/src/state/config.js
CHANGED
|
@@ -15,8 +15,8 @@ import { OperationJournal } from "./operation_journal.js";
|
|
|
15
15
|
export const DEFAULT_BASE_URL = "https://app.itpay.ai";
|
|
16
16
|
export const DEV_BASE_URL = "https://dev.itpay.ai";
|
|
17
17
|
export const SANDBOX_BASE_URL = "https://sandbox.itpay.ai";
|
|
18
|
-
export const CLI_VERSION = "2.1.
|
|
19
|
-
export const API_CONTRACT_REVISION = "sha256:
|
|
18
|
+
export const CLI_VERSION = "2.1.40";
|
|
19
|
+
export const API_CONTRACT_REVISION = "sha256:700d9834de969f16d0fc58cbbdbaa092f969f497e30e4ec291c790cea3e7f73a";
|
|
20
20
|
const CART_SESSION_DEFAULT_DIR = ".itpay-v3";
|
|
21
21
|
const CART_SESSION_FILENAME = "cart.json";
|
|
22
22
|
const OPERATION_JOURNAL_FILENAME = "operations.json";
|
|
@@ -60,5 +60,6 @@
|
|
|
60
60
|
"service execution",
|
|
61
61
|
"resume",
|
|
62
62
|
"contact"
|
|
63
|
-
]
|
|
63
|
+
],
|
|
64
|
+
"purchase_consent": "Candidate selection is not price approval. Service-backed quote/cart/checkout requires a current scoped approve_purchase record; use the complete server template, default false. Only genuine matching approval allows true; material price/subject/terms changes invalidate it. Restore an existing Checkout before creating anything."
|
|
64
65
|
}
|
|
@@ -18,15 +18,16 @@
|
|
|
18
18
|
],
|
|
19
19
|
"output_interpretation": {
|
|
20
20
|
"item_format": "Explain the title, what the human receives, any auxiliary discovery step, and the published price.",
|
|
21
|
-
"service_flow": "
|
|
21
|
+
"service_flow": "Legacy catalog only: read service_flow facts as published; do not reinterpret old discovery/paid identities as a new public definition. New definitions expose operations within one service.",
|
|
22
22
|
"variant_selection": "Internal IDs are for the Agent's next command only and are never shown or requested from the human.",
|
|
23
23
|
"price_note": "amount_minor is in minor units (e.g., 10000 = 100.00 CNY). The CLI formats prices as XX.YY currency automatically.",
|
|
24
|
-
"railway": "Railway entries point to itpay docs show rail-booking; use its query and booking contracts instead of a generic start→invoke→cart sequence."
|
|
24
|
+
"railway": "Railway entries point to itpay docs show rail-booking; use its query and booking contracts instead of a generic start→invoke→cart sequence.",
|
|
25
|
+
"service_definition": "Prefer immutable service_definition: one public service with declared operations, pinned schemas and billing modes. Free query does not mean free purchase; dynamic needs a real quote. Do not choose a zero placeholder variant."
|
|
25
26
|
},
|
|
26
27
|
"agent_rules": [
|
|
27
28
|
"Use catalog list only when the human asks for a new service, not when they want an earlier purchase.",
|
|
28
29
|
"Read the variant IDs and prices from the output; do not invent them.",
|
|
29
|
-
"Explain the
|
|
30
|
+
"Explain the returned public service operations and pricing; use legacy service_flow only when no service_definition is present.",
|
|
30
31
|
"State free quota, paid continuation price, email requirement, and claim purpose exactly as published; do not exaggerate or omit conditions.",
|
|
31
32
|
"Ask the human to choose only when multiple services could satisfy the goal; never make them choose an internal variant or offer ID.",
|
|
32
33
|
"Use each service’s returned input contract; railway planning and booking follow rail-booking, not a universal Cart route."
|
|
@@ -68,5 +69,6 @@
|
|
|
68
69
|
"what to buy",
|
|
69
70
|
"有哪些",
|
|
70
71
|
"服务列表"
|
|
71
|
-
]
|
|
72
|
+
],
|
|
73
|
+
"purchase_consent": "Candidate selection is not price approval. Service-backed quote/cart/checkout requires a current scoped approve_purchase record; use the complete server template, default false. Only genuine matching approval allows true; material price/subject/terms changes invalidate it. Restore an existing Checkout before creating anything."
|
|
72
74
|
}
|
|
@@ -92,12 +92,13 @@
|
|
|
92
92
|
"chat"
|
|
93
93
|
],
|
|
94
94
|
"auth_session_states": {
|
|
95
|
-
"stages": "
|
|
95
|
+
"stages": "Report the actual server stage: waiting_provider (show the official handoff), email_verification_required (explicit or legacy identity verification; follow the current official page), merge_confirmation_required (the human must prove both accounts and explicitly confirm or cancel the frozen merge), provider_pending/pending (wait), completed (bind the original session and resume the same service). Ordinary login needs one verified supported identity; do not require both phone and email.",
|
|
96
96
|
"terminal_states": "auth_expired/auth_denied/auth_cancelled/auth_failed mean this session is over; say so plainly and respect denial or cancellation. Start a new session only on an explicit login request; never keep polling a dead one.",
|
|
97
97
|
"transport_unknown": "auth_status_unknown means a network error while polling — the outcome is unknown. Keep waiting on the SAME session (it may still complete); do NOT restart auth, and do not assume denial.",
|
|
98
98
|
"missing": "auth_session_missing means the cached session is gone. An explicit auth login opens one fresh official session in that call; auth status stays read-only.",
|
|
99
99
|
"completed_resume": "After login completes, execute the returned command to resume the same paused service execution. Login does not silently run that command.",
|
|
100
|
-
"path_b": "A buyer who reaches checkout without a login completes identity inside the protected checkout page; never force a separate itpay auth login first."
|
|
100
|
+
"path_b": "A buyer who reaches checkout without a login completes identity inside the protected checkout page; never force a separate itpay auth login first.",
|
|
101
|
+
"identity_binding": "Phone is recommended first, wallets next, email is a separate Other options choice. Extra identities are optional bindings. A phone conflict does not prove account ownership: independent target authentication and explicit human merge confirmation are required. Never use the same SMS proof twice, approve a merge for the user, or treat login/identity binding as payment consent. Service-specific contact requirements stay in the service."
|
|
101
102
|
},
|
|
102
103
|
"key_reset_decision": "Only after explicit user/operator acceptance of abandoning the old identity, consult device reset-key help; this is not automatic error recovery, and old execution access does not migrate."
|
|
103
104
|
}
|