@itpay/cli 2.1.9 → 2.1.11
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 +7 -2
- package/dist/src/commands/buy.js +1 -0
- package/dist/src/commands/checkout.js +16 -1
- package/dist/src/commands/checkout_handoff.js +41 -9
- package/dist/src/commands/order.js +5 -0
- package/dist/src/commands/services.js +145 -6
- package/dist/src/main.js +16 -3
- package/dist/src/state/config.js +2 -2
- package/docs/agent/buyer/rail-booking.json +30 -4
- package/docs/agent/buyer/render-hosts.json +8 -7
- package/docs/cli-reference/commands/services/action.md +13 -0
- package/docs/cli-reference/commands/services/next.md +10 -0
- package/docs/cli-reference/commands/services/page.md +3 -1
- package/docs/cli-reference/commands/services/read-result.md +4 -0
- package/package.json +1 -1
- package/skills/itpay/SKILL.md +7 -0
|
@@ -139,8 +139,9 @@ export class BackendClient {
|
|
|
139
139
|
prepareServiceQuote(serviceExecutionID, input) {
|
|
140
140
|
return this.http.post(`/v1/service-executions/${encodeURIComponent(serviceExecutionID)}/quotes`, input, { replaySafe: true });
|
|
141
141
|
}
|
|
142
|
-
getServiceExecution(serviceExecutionID) {
|
|
143
|
-
|
|
142
|
+
getServiceExecution(serviceExecutionID, opts = {}) {
|
|
143
|
+
const query = opts.sinceSnapshot ? `?since_snapshot=${encodeURIComponent(opts.sinceSnapshot)}` : "";
|
|
144
|
+
return this.http.get(`/v1/service-executions/${encodeURIComponent(serviceExecutionID)}${query}`);
|
|
144
145
|
}
|
|
145
146
|
listServiceExecutions(limit = 50) {
|
|
146
147
|
return this.http.get(`/v1/service-executions?limit=${limit}`);
|
|
@@ -155,6 +156,10 @@ export class BackendClient {
|
|
|
155
156
|
getGrantedServiceResult(serviceExecutionID) {
|
|
156
157
|
return this.http.get(`/v1/service-executions/${encodeURIComponent(serviceExecutionID)}/granted-result`);
|
|
157
158
|
}
|
|
159
|
+
getRailJourneyDetail(serviceExecutionID, journeyID, snapshotID) {
|
|
160
|
+
const query = snapshotID ? `?snapshot=${encodeURIComponent(snapshotID)}` : "";
|
|
161
|
+
return this.http.get(`/v1/service-executions/${encodeURIComponent(serviceExecutionID)}/rail-planning/journeys/${encodeURIComponent(journeyID)}${query}`);
|
|
162
|
+
}
|
|
158
163
|
}
|
|
159
164
|
// --- SSE streaming helper ---
|
|
160
165
|
async function streamSSE(url, onEvent, signal) {
|
package/dist/src/commands/buy.js
CHANGED
|
@@ -203,6 +203,7 @@ function buildBuyEnvelope(input) {
|
|
|
203
203
|
const presentationHandoff = buildCheckoutHandoff({
|
|
204
204
|
platform,
|
|
205
205
|
url: input.plan.linkOnlyURL ?? input.checkoutURL,
|
|
206
|
+
mobileUrl: input.checkoutURL,
|
|
206
207
|
amount,
|
|
207
208
|
plan: input.plan,
|
|
208
209
|
...(input.agentType ? { agentType: input.agentType } : {}),
|
|
@@ -22,7 +22,7 @@ export async function runCheckoutPresentation(backend, options) {
|
|
|
22
22
|
});
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
|
-
const checkoutURL = checkoutPageURL(options.baseURL, options.checkoutID, options.displayToken);
|
|
25
|
+
const checkoutURL = savedCheckoutURLOrFallback(options.savedCheckoutURL, options.checkoutID, options.displayToken, checkoutPageURL(options.baseURL, options.checkoutID, options.displayToken));
|
|
26
26
|
const cardURL = localizeCardURL(absolutePublicURL(options.baseURL, presentation.card_url ?? checkoutCardURL(options.baseURL, options.checkoutID, options.displayToken)), locale);
|
|
27
27
|
const qrPNGURL = absolutePublicURL(options.baseURL, presentation.card_png_url ?? presentation.qr_png_url ?? checkoutCardPNGURL(options.baseURL, options.checkoutID, options.displayToken));
|
|
28
28
|
const localizedPNGURL = localizeCardURL(qrPNGURL, locale);
|
|
@@ -68,6 +68,7 @@ function pendingCheckoutEnvelope(presentation, checkoutURL, plan, nextCommand, a
|
|
|
68
68
|
const presentationHandoff = buildCheckoutHandoff({
|
|
69
69
|
platform,
|
|
70
70
|
url: plan.linkOnlyURL ?? checkoutURL,
|
|
71
|
+
mobileUrl: checkoutURL,
|
|
71
72
|
amount,
|
|
72
73
|
plan,
|
|
73
74
|
...(agentType ? { agentType } : {}),
|
|
@@ -164,6 +165,20 @@ function checkoutPageURL(baseURL, checkoutID, displayToken) {
|
|
|
164
165
|
const root = publicRoot(baseURL);
|
|
165
166
|
return `${root}/checkout/${encodeURIComponent(checkoutID)}?display_token=${encodeURIComponent(displayToken)}`;
|
|
166
167
|
}
|
|
168
|
+
function savedCheckoutURLOrFallback(savedURL, checkoutID, displayToken, fallback) {
|
|
169
|
+
if (!savedURL) {
|
|
170
|
+
return fallback;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
const parsed = new URL(savedURL);
|
|
174
|
+
const belongsToCheckout = parsed.pathname === `/checkout/${checkoutID}`;
|
|
175
|
+
const sameToken = parsed.searchParams.get("display_token") === displayToken;
|
|
176
|
+
return belongsToCheckout && sameToken ? savedURL : fallback;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return fallback;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
167
182
|
function checkoutCardURL(baseURL, checkoutID, displayToken) {
|
|
168
183
|
const root = publicRoot(baseURL);
|
|
169
184
|
return `${root}/v1/checkouts/${encodeURIComponent(checkoutID)}/card?display_token=${encodeURIComponent(displayToken)}`;
|
|
@@ -17,8 +17,26 @@ export function buildWorkBuddyPresentFilesAction(url) {
|
|
|
17
17
|
arguments: { files: [url] },
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
|
+
function hasCompleteCheckoutTokens(url) {
|
|
21
|
+
if (!url) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const parsed = new URL(url);
|
|
26
|
+
return Boolean(parsed.searchParams.get("display_token") && parsed.searchParams.get("complete_exchange_token"));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
20
32
|
export function buildCheckoutHandoff(input) {
|
|
21
33
|
const handoff = { url: input.url };
|
|
34
|
+
// mobile_url is only emitted when the link carries the complete token pair;
|
|
35
|
+
// a display-token-only link cannot finish the same-device exchange after payment.
|
|
36
|
+
const mobileUrl = hasCompleteCheckoutTokens(input.mobileUrl) ? input.mobileUrl : undefined;
|
|
37
|
+
if (mobileUrl) {
|
|
38
|
+
handoff.mobile_url = mobileUrl;
|
|
39
|
+
}
|
|
22
40
|
const workBuddyAction = isWorkBuddyPlainChat(input.agentType, input.platform);
|
|
23
41
|
if (workBuddyAction) {
|
|
24
42
|
handoff.agent_action = buildWorkBuddyPresentFilesAction(input.url);
|
|
@@ -40,27 +58,41 @@ export function buildCheckoutHandoff(input) {
|
|
|
40
58
|
}
|
|
41
59
|
return {
|
|
42
60
|
handoff,
|
|
43
|
-
instruction: checkoutHandoffInstruction(input.agentType, input.platform, input.amount),
|
|
61
|
+
instruction: checkoutHandoffInstruction(input.agentType, input.platform, input.amount, Boolean(mobileUrl)),
|
|
44
62
|
};
|
|
45
63
|
}
|
|
46
|
-
function checkoutHandoffInstruction(agentType, platform, amount) {
|
|
64
|
+
function checkoutHandoffInstruction(agentType, platform, amount, hasMobileLink) {
|
|
47
65
|
if (isWorkBuddyPlainChat(agentType, platform)) {
|
|
48
|
-
|
|
66
|
+
const mobileSentence = hasMobileLink
|
|
67
|
+
? ",然后把 handoff.mobile_url 作为可点击链接发给用户,说明“手机端点开可直接跳转支付宝或微信支付;小程序内打不开时复制到手机浏览器打开即可”"
|
|
68
|
+
: "";
|
|
69
|
+
const failLinks = hasMobileLink ? "发送 handoff.url 与 handoff.mobile_url" : "发送原始 handoff.url";
|
|
70
|
+
return `Backend 尚未确认付款。立即严格按 handoff.agent_action.tool 和 handoff.agent_action.arguments 原样执行一次,在右侧打开 handoff.url;确认工具调用成功后说明金额 ${amount}${mobileSentence},然后停止等待。若工具失败,只${failLinks},报告未自动打开并停止。不要用 present_files 打开本地文件或二维码 PNG,不要下载或重建二维码,不要调用 pay,不要创建新 Checkout、Payment Intent 或 Execution。只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
|
|
49
71
|
}
|
|
50
72
|
if (isZCodePlainChat(agentType, platform)) {
|
|
51
|
-
|
|
73
|
+
const mobileSentence = hasMobileLink
|
|
74
|
+
? ",并把 handoff.mobile_url 发给用户备用(手机端点开可直接跳转支付宝或微信支付)"
|
|
75
|
+
: "";
|
|
76
|
+
const fallbackLinks = hasMobileLink ? "同一个可点击 handoff.url 与 handoff.mobile_url" : "同一个可点击 handoff.url";
|
|
77
|
+
return `Backend 尚未确认付款。立即用 ZCode 内置浏览器打开 handoff.url,让用户完成付款;确认已发起打开后说明金额 ${amount}${mobileSentence},然后停止等待。不要只粘贴文字链接,不要下载、解析或重建二维码,不要创建新 Checkout、Payment Intent 或 Execution。只有内置浏览器明确不可用时,才展示${fallbackLinks}。只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
|
|
52
78
|
}
|
|
53
79
|
if (agentType?.trim().toLowerCase() === "doubao-work" && platform === "plain_chat") {
|
|
54
|
-
|
|
80
|
+
const directLink = hasMobileLink ? "handoff.mobile_url" : "handoff.url";
|
|
81
|
+
const directLabel = hasMobileLink ? "手机直接打开收银台(可跳转支付宝/微信)" : "手机直接打开收银台";
|
|
82
|
+
return `Backend 尚未确认付款。在当前豆包工作对话中展示两个有标签的官方入口:将 ${directLink} 标为“${directLabel}”,将 handoff.qr_image_url 标为“二维码图片(保存或用另一台设备扫码)”。说明金额 ${amount} 后停止等待。不要解析或单独输出 URL credential,不要下载或重建二维码,不要调用 pay,不要创建新 Checkout、Payment Intent 或 Execution。只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
|
|
55
83
|
}
|
|
56
84
|
if (platform === "markdown") {
|
|
57
|
-
|
|
85
|
+
const mobileSentence = hasMobileLink ? ",并附 handoff.mobile_url 供手机端点击直达支付" : "";
|
|
86
|
+
return `Backend 尚未确认付款。把 handoff.markdown 原样发送到当前桌面对话,确认二维码、付款链接和金额 ${amount} 均已实际对用户可见${mobileSentence},然后停止等待。不要创建新 Checkout、Payment Intent 或 Execution;只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
|
|
58
87
|
}
|
|
59
88
|
if (platform === "terminal") {
|
|
60
|
-
|
|
89
|
+
const mobileSentence = hasMobileLink ? ",并展示 handoff.mobile_url 供手机端点击直达支付" : "";
|
|
90
|
+
return `Backend 尚未确认付款。在用户可见终端展示当前 Checkout 的二维码、handoff.url 和金额 ${amount}${mobileSentence},然后停止等待。不要创建新 Checkout、Payment Intent 或 Execution;只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
|
|
61
91
|
}
|
|
62
92
|
if (agentType?.trim().toLowerCase() === "openclaw" && platform === "telegram") {
|
|
63
|
-
|
|
93
|
+
const failLinks = hasMobileLink ? "handoff.qr_image_url、金额、handoff.url 与 handoff.mobile_url" : "handoff.qr_image_url、金额和 handoff.url";
|
|
94
|
+
return `Backend 尚未确认付款或授权状态。立即使用 OpenClaw 原生 message tool,严格按 handoff.agent_action.tool 和 handoff.agent_action.arguments 原样执行;它会发送金额 ${amount}、二维码、📱 手机点这儿支付和 📋 已授权给我读。不得改写 presentation、换用其他消息工具、拆开按钮发送或用普通回复声称按钮已经发送。确认 message tool 调用成功后停止等待。收到 callback_data "itp:grant_confirmed:<checkout_id>" 时,表示用户声明已在收银台授权读取;立即执行 next.command 查询同一 Checkout,再只执行 Backend 返回的 next.command 进入同一 Execution 的 grant 流程。该 callback 不证明付款成功或 grant 已生效;Backend 未返回 grant_active 前不得读取或猜测结果。若原生 message tool 明确失败或当前 Telegram 未启用 inline buttons,只发送现有 ${failLinks},报告按钮不可用并停止;不要创建新的 Checkout、Payment Intent 或 Execution。`;
|
|
64
95
|
}
|
|
65
|
-
|
|
96
|
+
const urlList = hasMobileLink ? "handoff.url、handoff.mobile_url(手机端点击直达支付)和可用的 handoff.qr_image_url" : "handoff.url 和可用的 handoff.qr_image_url";
|
|
97
|
+
return `Backend 尚未确认付款。把 ${urlList} 实际发送到当前会话,说明金额 ${amount},然后停止等待。不要创建新 Checkout、Payment Intent 或 Execution;只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
|
|
66
98
|
}
|
|
@@ -38,6 +38,10 @@ function orderEnvelope(order, delivery, lockedRefund) {
|
|
|
38
38
|
else if (order.status === "cancelled") {
|
|
39
39
|
instruction = "先告诉用户这笔订单已经取消,没有可继续的付款或交付;不要创建替代订单,除非用户另行提出新的购买。";
|
|
40
40
|
}
|
|
41
|
+
else if (order.status === "pending_payment") {
|
|
42
|
+
instruction = "先告诉用户订单正在等待付款,超时未支付会被自动取消;不要创建替代订单,也不要重复发起支付授权。";
|
|
43
|
+
next = { command: `itpay order ${order.order_id} --json`, reason: "刷新订单支付状态" };
|
|
44
|
+
}
|
|
41
45
|
else if (!["delivered", "refunded", "failed", "cancelled"].includes(order.status)) {
|
|
42
46
|
instruction = "先告诉用户订单仍在处理,已记录的付款和订单不需要重复创建;稍后查询同一订单,不要创建替代订单。";
|
|
43
47
|
next = { command: `itpay order ${order.order_id} --json`, reason: "刷新订单状态" };
|
|
@@ -48,6 +52,7 @@ function orderEnvelope(order, delivery, lockedRefund) {
|
|
|
48
52
|
order_id: order.order_id,
|
|
49
53
|
...(order.order_code ? { order_code: order.order_code } : {}),
|
|
50
54
|
amount: formatMoney(order.amount_minor, order.currency),
|
|
55
|
+
...(order.payment_deadline_at ? { payment_deadline_at: order.payment_deadline_at } : {}),
|
|
51
56
|
...(delivery ? { delivery_mode: delivery.delivery_mode } : {}),
|
|
52
57
|
access_locked: Boolean(lockedRefund),
|
|
53
58
|
...(delivery?.service_execution_id ? { service_execution_id: delivery.service_execution_id } : {}),
|
|
@@ -9,6 +9,10 @@ import { localizeCardURL, normalizeCardLocale } from "../render/locale.js";
|
|
|
9
9
|
import { buildAgentChatHandoff } from "../render/markdown.js";
|
|
10
10
|
import { platformKeyForHost } from "../render/plan.js";
|
|
11
11
|
import { renderTerminalQR } from "../render/qr.js";
|
|
12
|
+
// Declared at execution creation: the client understands the rail.progressive.v2
|
|
13
|
+
// response format (rail_planning projection, snapshot paging, rsel_ handles).
|
|
14
|
+
// The server alone decides service version, quota and provider credentials.
|
|
15
|
+
const RAIL_PROGRESSIVE_FEATURES = ["rail.progressive.v2"];
|
|
12
16
|
import { buildCheckoutQRPlan } from "./buy.js";
|
|
13
17
|
import { appendFeedbackPostmortemInstruction, CommandContractError, isTerminalServiceExecutionStatus, writeCommandEnvelope, } from "./guidance.js";
|
|
14
18
|
const serviceActionStatuses = new Set(["pending", "approved", "rejected", "expired", "cancelled"]);
|
|
@@ -87,6 +91,7 @@ export async function runServicesStart(backend, serviceID, options = {}) {
|
|
|
87
91
|
service_id: serviceID,
|
|
88
92
|
client_context: {
|
|
89
93
|
host,
|
|
94
|
+
features: [...RAIL_PROGRESSIVE_FEATURES],
|
|
90
95
|
...(options.target ? { target: options.target } : {}),
|
|
91
96
|
...(options.clientContext ?? {}),
|
|
92
97
|
},
|
|
@@ -759,7 +764,7 @@ export async function runServicesGet(backend, serviceExecutionID, options = {})
|
|
|
759
764
|
});
|
|
760
765
|
}
|
|
761
766
|
export async function runServicesNext(backend, serviceExecutionID, options = {}) {
|
|
762
|
-
const response = await backend.getServiceExecution(serviceExecutionID);
|
|
767
|
+
const response = await backend.getServiceExecution(serviceExecutionID, options.sinceSnapshot ? { sinceSnapshot: options.sinceSnapshot } : {});
|
|
763
768
|
const envelope = servicesNextEnvelope(response);
|
|
764
769
|
if (response.rail_booking)
|
|
765
770
|
envelope.result = { ...envelope.result, rail_booking: response.rail_booking };
|
|
@@ -781,14 +786,19 @@ export async function runServicesPage(backend, serviceExecutionID, resultItemID,
|
|
|
781
786
|
const response = await backend.getServiceExecutionResultItemPage(serviceExecutionID, resultItemID, offset, limit);
|
|
782
787
|
const page = response.page;
|
|
783
788
|
const container = (page.result ?? page);
|
|
784
|
-
|
|
789
|
+
// rail.progressive.v2 saved pages key journeys/journey_page; legacy result
|
|
790
|
+
// items key candidates/catalog_page. Both are committed, read-only slices.
|
|
791
|
+
const v2Page = (container.journey_page ?? {});
|
|
792
|
+
const catalogPage = (container.catalog_page ?? v2Page);
|
|
785
793
|
const nextOffset = typeof catalogPage.next_offset === "number" ? catalogPage.next_offset : null;
|
|
786
|
-
const
|
|
794
|
+
const journeys = Array.isArray(container.journeys) ? container.journeys : [];
|
|
795
|
+
const candidates = Array.isArray(container.candidates) ? container.candidates : journeys;
|
|
787
796
|
const envelope = {
|
|
788
797
|
status: candidates.length > 0 ? "result_page" : "result_page_end",
|
|
789
798
|
result: {
|
|
790
799
|
service_execution_id: response.service_execution_id,
|
|
791
|
-
service_capability_result_item_id: response.service_capability_result_item_id,
|
|
800
|
+
...(response.service_capability_result_item_id ? { service_capability_result_item_id: response.service_capability_result_item_id } : {}),
|
|
801
|
+
...(response.snapshot_id ? { snapshot_id: response.snapshot_id } : {}),
|
|
792
802
|
offset: catalogPage.offset ?? offset,
|
|
793
803
|
limit: catalogPage.limit ?? limit,
|
|
794
804
|
total: catalogPage.total ?? candidates.length,
|
|
@@ -798,7 +808,10 @@ export async function runServicesPage(backend, serviceExecutionID, resultItemID,
|
|
|
798
808
|
},
|
|
799
809
|
instruction: "读取的是已保存结果的同版本分页,不重新查询、不消耗额度。用普通语言向用户说明本页候选(车次、席别、时刻、费用口径),铁路应付与地面估算费用分开表述;不要提及 safe_payload、Execution 或内部 ID。",
|
|
800
810
|
next: nextOffset !== null
|
|
801
|
-
? { command:
|
|
811
|
+
? { command: response.snapshot_id
|
|
812
|
+
? `itpay services page ${serviceExecutionID} ${resultItemID} --cursor rcur_${nextOffset} --json`
|
|
813
|
+
: `itpay services page ${serviceExecutionID} ${resultItemID} --offset ${nextOffset} --json`,
|
|
814
|
+
reason: "读取同版本结果的下一页" }
|
|
802
815
|
: null,
|
|
803
816
|
recovery: offset > 0
|
|
804
817
|
? [{ command: `itpay services page ${serviceExecutionID} ${resultItemID} --json`, reason: "回到第一页" }]
|
|
@@ -850,6 +863,33 @@ export async function runServicesList(backend, options = {}) {
|
|
|
850
863
|
});
|
|
851
864
|
}
|
|
852
865
|
export async function runServicesReadResult(backend, serviceExecutionID, options = {}) {
|
|
866
|
+
// rail.progressive.v2 planning read: --journey/--snapshot select committed
|
|
867
|
+
// planning evidence — a free owner-validated read that never touches the
|
|
868
|
+
// grant/Vault path. Without the selectors the authorized-delivery flow is
|
|
869
|
+
// unchanged.
|
|
870
|
+
if (options.journey) {
|
|
871
|
+
const detail = await backend.getRailJourneyDetail(serviceExecutionID, options.journey, options.snapshot);
|
|
872
|
+
writeCommandEnvelope({
|
|
873
|
+
status: "ready",
|
|
874
|
+
result: {
|
|
875
|
+
service_execution_id: serviceExecutionID,
|
|
876
|
+
plan_id: detail.plan_id,
|
|
877
|
+
snapshot_id: detail.snapshot_id,
|
|
878
|
+
query_revision: detail.query_revision,
|
|
879
|
+
journey: detail.journey,
|
|
880
|
+
},
|
|
881
|
+
instruction: "展示该 journey 的完整明细(车次、分段、席别报价、接驳估计与风险标注)。rail_payable 只是该行程当前可购报价的参考价,不是锁价;下单前须走受保护 Checkout 收集乘车人。",
|
|
882
|
+
next: detail.journey?.booking_support === "single_leg"
|
|
883
|
+
? { command: `itpay services action ${serviceExecutionID} --action select_journey --actor-type human --status approved --input journey_id=${detail.journey.journey_id} --json`, reason: "选定此行程" }
|
|
884
|
+
: { command: `itpay services next ${serviceExecutionID} --since-snapshot ${detail.snapshot_id} --json`, reason: "返回规划进展" },
|
|
885
|
+
recovery: [],
|
|
886
|
+
}, {
|
|
887
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
888
|
+
...(options.output ? { output: options.output } : {}),
|
|
889
|
+
plainResult: servicesNextPlainResult(detail.journey),
|
|
890
|
+
});
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
853
893
|
const response = await backend.getGrantedServiceResult(serviceExecutionID);
|
|
854
894
|
let orderID;
|
|
855
895
|
try {
|
|
@@ -941,6 +981,102 @@ function railBookingEnvelope(model) {
|
|
|
941
981
|
recovery: [],
|
|
942
982
|
};
|
|
943
983
|
}
|
|
984
|
+
function railJourneySummary(card, serviceExecutionID) {
|
|
985
|
+
const rides = Array.isArray(card.rides) ? card.rides : [];
|
|
986
|
+
const trains = rides.map((ride) => ride.train_code).filter(Boolean);
|
|
987
|
+
const first = rides[0];
|
|
988
|
+
const last = rides.at(-1);
|
|
989
|
+
const offer = card.representative_offer;
|
|
990
|
+
return {
|
|
991
|
+
journey_id: card.journey_id,
|
|
992
|
+
route: (card.route_names?.length ? card.route_names : card.route ?? []).join("→"),
|
|
993
|
+
...(trains.length ? { trains } : {}),
|
|
994
|
+
...(first?.departure || last?.arrival ? { time: `${first?.departure ?? ""}–${last?.arrival ?? ""}` } : {}),
|
|
995
|
+
...(offer ? { price: formatMoney(offer.rail_payable_minor, offer.currency) } : { price: "unknown" }),
|
|
996
|
+
availability: card.availability,
|
|
997
|
+
booking_support: card.booking_support,
|
|
998
|
+
...(card.observed_at ? { observed_at: card.observed_at } : {}),
|
|
999
|
+
...(card.risk_notes?.length ? { risk_notes: card.risk_notes } : {}),
|
|
1000
|
+
...(card.booking_support === "single_leg"
|
|
1001
|
+
? { select: `itpay services action ${serviceExecutionID} --action select_journey --actor-type human --status approved --input journey_id=${card.journey_id} --json` }
|
|
1002
|
+
: {}),
|
|
1003
|
+
...(card.booking_offer
|
|
1004
|
+
? {
|
|
1005
|
+
booking_offer: card.booking_offer,
|
|
1006
|
+
book: `itpay services run ${card.booking_offer.service_id} --input-json <file> --json # file = {"selection":{"token":"${card.booking_offer.selection_token}","seat_type":"<席别>"},"passengers":<n>}`,
|
|
1007
|
+
}
|
|
1008
|
+
: {}),
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
// rail.progressive.v2 read projection: committed snapshots only; polling never
|
|
1012
|
+
// triggers supplier calls. Recommendation is absent until the decision stage
|
|
1013
|
+
// commits — alternatives still render so users can compare early.
|
|
1014
|
+
function railPlanningEnvelope(model) {
|
|
1015
|
+
const plan = model.rail_planning;
|
|
1016
|
+
const se = model.execution.service_execution_id;
|
|
1017
|
+
const expansion = plan.search?.expansion_status ?? "running";
|
|
1018
|
+
const journeys = [plan.recommendation, ...(plan.alternatives ?? [])].filter(Boolean);
|
|
1019
|
+
const cards = journeys.map((card) => railJourneySummary(card, se));
|
|
1020
|
+
const result = {
|
|
1021
|
+
service_execution_id: se,
|
|
1022
|
+
rail_planning: {
|
|
1023
|
+
readiness: plan.readiness,
|
|
1024
|
+
query_revision: plan.query_revision,
|
|
1025
|
+
snapshot_id: plan.snapshot_id,
|
|
1026
|
+
expansion_status: expansion,
|
|
1027
|
+
...(plan.search?.reason ? { reason: plan.search.reason } : {}),
|
|
1028
|
+
...(plan.search?.poll_after_ms ? { poll_after_ms: plan.search.poll_after_ms } : {}),
|
|
1029
|
+
...(plan.result_not_updated ? { result_not_updated: true } : {}),
|
|
1030
|
+
...(plan.budgets ? { budgets: plan.budgets } : {}),
|
|
1031
|
+
...(plan.coverage ? { coverage: plan.coverage } : {}),
|
|
1032
|
+
},
|
|
1033
|
+
...(plan.recommendation ? { recommendation: railJourneySummary(plan.recommendation, se) } : {}),
|
|
1034
|
+
...(cards.length ? { journeys: cards } : {}),
|
|
1035
|
+
...(plan.available_actions?.length ? { available_actions: plan.available_actions } : {}),
|
|
1036
|
+
};
|
|
1037
|
+
const nextPoll = {
|
|
1038
|
+
command: `itpay services next ${se}${plan.snapshot_id ? ` --since-snapshot ${plan.snapshot_id}` : ""} --json`,
|
|
1039
|
+
reason: "读取同一规划的增量进展(不触发供应商调用)",
|
|
1040
|
+
};
|
|
1041
|
+
switch (expansion) {
|
|
1042
|
+
case "complete":
|
|
1043
|
+
return {
|
|
1044
|
+
status: "ready",
|
|
1045
|
+
result,
|
|
1046
|
+
instruction: "规划已收敛:向用户展示 recommendation 与 journeys 卡片(车次/时间/价格/余票状态),请用户选定 journey 后用其 select 命令提交;乘车人身份信息不在此收集,后续购买走受保护 Checkout。",
|
|
1047
|
+
next: journeys[0]?.booking_support === "single_leg"
|
|
1048
|
+
? { command: railJourneySummary(journeys[0], se).select, reason: "选定推荐行程" }
|
|
1049
|
+
: null,
|
|
1050
|
+
recovery: [],
|
|
1051
|
+
};
|
|
1052
|
+
case "paused":
|
|
1053
|
+
return {
|
|
1054
|
+
status: "awaiting_input",
|
|
1055
|
+
result,
|
|
1056
|
+
instruction: "规划已发布首批结果并暂停扩展:展示现有卡片与 available_actions,等待用户意图(expand_search 是唯一会再消耗供应商配额的命令,其余均为本地读取)。ready 后 next 为空表示向用户汇报并等待,不是永远没有更多。",
|
|
1057
|
+
next: null,
|
|
1058
|
+
recovery: [],
|
|
1059
|
+
};
|
|
1060
|
+
case "failed":
|
|
1061
|
+
case "cancelled":
|
|
1062
|
+
case "expired":
|
|
1063
|
+
return {
|
|
1064
|
+
status: expansion,
|
|
1065
|
+
result,
|
|
1066
|
+
instruction: "本次规划已结束且不可续用;展示已有卡片后如需重查请用户确认后用同一服务新建执行。不要自动重新发起。",
|
|
1067
|
+
next: null,
|
|
1068
|
+
recovery: [{ command: `itpay services events ${se} --json`, reason: "读取同一任务的处理记录" }],
|
|
1069
|
+
};
|
|
1070
|
+
default:
|
|
1071
|
+
return {
|
|
1072
|
+
status: "planning",
|
|
1073
|
+
result,
|
|
1074
|
+
instruction: "规划进行中:已提交的卡片可先向用户展示比较;稍后按 next 增量轮询同一执行(since-snapshot 命中时负载会被压缩)。不要重新发起查询。",
|
|
1075
|
+
next: nextPoll,
|
|
1076
|
+
recovery: [],
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
944
1080
|
function terminalExecutionEnvelope(model) {
|
|
945
1081
|
const execution = model.execution;
|
|
946
1082
|
const currentDelivery = model.current_delivery ?? model.delivery_bindings.at(-1);
|
|
@@ -1026,6 +1162,8 @@ function servicesNextEnvelope(model) {
|
|
|
1026
1162
|
}
|
|
1027
1163
|
if (model.rail_booking)
|
|
1028
1164
|
return railBookingEnvelope(model);
|
|
1165
|
+
if (model.rail_planning)
|
|
1166
|
+
return railPlanningEnvelope(model);
|
|
1029
1167
|
if (latestRun && ["failed", "cancelled"].includes(latestRun.status) && !model.workflow_entry) {
|
|
1030
1168
|
return {
|
|
1031
1169
|
status: latestRun.status,
|
|
@@ -1500,6 +1638,7 @@ function buildServicesCheckoutEnvelope(response, checkoutURL, plan, agentType, t
|
|
|
1500
1638
|
const presentationHandoff = buildCheckoutHandoff({
|
|
1501
1639
|
platform,
|
|
1502
1640
|
url: plan.linkOnlyURL ?? checkoutURL,
|
|
1641
|
+
mobileUrl: checkoutURL,
|
|
1503
1642
|
amount,
|
|
1504
1643
|
plan,
|
|
1505
1644
|
...(agentType ? { agentType } : {}),
|
|
@@ -1581,7 +1720,7 @@ export async function runServicesRun(backend, config, serviceID, input, options
|
|
|
1581
1720
|
if (!id) {
|
|
1582
1721
|
const started = await backend.startServiceExecution({
|
|
1583
1722
|
service_id: serviceID,
|
|
1584
|
-
client_context: { host: options.host ?? "terminal", ...(options.target ? { target: options.target } : {}) },
|
|
1723
|
+
client_context: { host: options.host ?? "terminal", features: [...RAIL_PROGRESSIVE_FEATURES], ...(options.target ? { target: options.target } : {}) },
|
|
1585
1724
|
...(input !== undefined ? { input } : {}),
|
|
1586
1725
|
});
|
|
1587
1726
|
id = started.execution.service_execution_id;
|
package/dist/src/main.js
CHANGED
|
@@ -910,6 +910,9 @@ program
|
|
|
910
910
|
await runCheckoutPresentation(backend, {
|
|
911
911
|
checkoutID,
|
|
912
912
|
displayToken,
|
|
913
|
+
...(snap.lastCheckoutID === checkoutID && snap.lastCheckoutURL
|
|
914
|
+
? { savedCheckoutURL: snap.lastCheckoutURL }
|
|
915
|
+
: {}),
|
|
913
916
|
host,
|
|
914
917
|
...(options.target ? { target: options.target } : {}),
|
|
915
918
|
...(config.agentType ? { agentType: config.agentType } : {}),
|
|
@@ -1353,14 +1356,21 @@ services
|
|
|
1353
1356
|
.argument("<service_execution_id>")
|
|
1354
1357
|
.argument("<result_item_id>")
|
|
1355
1358
|
.option("--offset <offset>", "zero-based candidate offset", Number, 0)
|
|
1359
|
+
.option("--cursor <cursor>", "opaque v2 page cursor (rcur_<offset>); takes precedence over --offset")
|
|
1356
1360
|
.option("--limit <limit>", "page size (1-20)", Number, 5)
|
|
1357
1361
|
.option("--json", "output JSON")
|
|
1358
1362
|
.action(async (serviceExecutionID, resultItemID, options) => {
|
|
1359
1363
|
const config = loadConfig();
|
|
1360
1364
|
const backend = newBackendClient(config);
|
|
1361
1365
|
try {
|
|
1366
|
+
const cursorOffset = typeof options.cursor === "string" && /^rcur_\d+$/.test(options.cursor)
|
|
1367
|
+
? Number(options.cursor.slice(5))
|
|
1368
|
+
: undefined;
|
|
1369
|
+
if (options.cursor !== undefined && cursorOffset === undefined) {
|
|
1370
|
+
throw new CommandContractError("cursor_invalid", "--cursor must be an opaque rcur_<offset> token from a previous page response", "--cursor 需为上一页返回的 rcur_<offset> 不透明游标。", []);
|
|
1371
|
+
}
|
|
1362
1372
|
await runServicesPage(backend, serviceExecutionID, resultItemID, {
|
|
1363
|
-
offset: options.offset,
|
|
1373
|
+
offset: cursorOffset ?? options.offset,
|
|
1364
1374
|
limit: options.limit,
|
|
1365
1375
|
jsonOutput: Boolean(options.json),
|
|
1366
1376
|
});
|
|
@@ -1598,12 +1608,13 @@ services
|
|
|
1598
1608
|
.command("next")
|
|
1599
1609
|
.description("Show the next recommended agent action for a Service Execution")
|
|
1600
1610
|
.argument("<service_execution_id>")
|
|
1611
|
+
.option("--since-snapshot <snapshot_id>", "delta read: suppress the journey payload when unchanged")
|
|
1601
1612
|
.option("--json", "output JSON instead of terminal text")
|
|
1602
1613
|
.action(async (serviceExecutionID, options) => {
|
|
1603
1614
|
const config = loadConfig();
|
|
1604
1615
|
const backend = newBackendClient(config);
|
|
1605
1616
|
try {
|
|
1606
|
-
await runServicesNext(backend, serviceExecutionID, { jsonOutput: Boolean(options.json) });
|
|
1617
|
+
await runServicesNext(backend, serviceExecutionID, { jsonOutput: Boolean(options.json), sinceSnapshot: options.sinceSnapshot });
|
|
1607
1618
|
}
|
|
1608
1619
|
catch (error) {
|
|
1609
1620
|
reportCLIError(error, {
|
|
@@ -1618,12 +1629,14 @@ services
|
|
|
1618
1629
|
.command("read-result")
|
|
1619
1630
|
.description("Read a human-granted service result for this agent")
|
|
1620
1631
|
.argument("<service_execution_id>")
|
|
1632
|
+
.option("--snapshot <snapshot_id>", "rail.progressive.v2: read one journey from a committed planning snapshot")
|
|
1633
|
+
.option("--journey <journey_id>", "rail.progressive.v2: journey id to read")
|
|
1621
1634
|
.option("--json", "output JSON instead of terminal text")
|
|
1622
1635
|
.action(async (serviceExecutionID, options) => {
|
|
1623
1636
|
const config = loadConfig();
|
|
1624
1637
|
const backend = newBackendClient(config);
|
|
1625
1638
|
try {
|
|
1626
|
-
await runServicesReadResult(backend, serviceExecutionID, { jsonOutput: Boolean(options.json) });
|
|
1639
|
+
await runServicesReadResult(backend, serviceExecutionID, { jsonOutput: Boolean(options.json), snapshot: options.snapshot, journey: options.journey });
|
|
1627
1640
|
}
|
|
1628
1641
|
catch (error) {
|
|
1629
1642
|
reportCLIError(error, {
|
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.11";
|
|
19
|
+
export const API_CONTRACT_REVISION = "sha256:653bead80f71a65f2edd1356bcdfe0ce3a3b99828a48cbf081f2dadac82c7e02";
|
|
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";
|
|
@@ -46,7 +46,9 @@
|
|
|
46
46
|
"depart_after": "Do not depart before this time.",
|
|
47
47
|
"arrive_before": "Do not arrive after this time.",
|
|
48
48
|
"priority": "One of balanced | fastest | cheapest | safest | flexible.",
|
|
49
|
-
"max_transfers": "Integer 0-2, allowed transfer count."
|
|
49
|
+
"max_transfers": "Integer 0-2, allowed transfer count.",
|
|
50
|
+
"passengers": "Integer 1-5, default 1: adult passenger count reflected in per-leg inventory and amounts. NOT identity records.",
|
|
51
|
+
"preference_context": "Bounded object, max 4 string-valued keys (<=200 chars each) of soft ranking preferences, e.g. {\"no_taxi\":\"true\"}. Validated allowlist; never carry passenger identity."
|
|
50
52
|
},
|
|
51
53
|
"example": {
|
|
52
54
|
"origin": "中山古镇",
|
|
@@ -74,7 +76,9 @@
|
|
|
74
76
|
"Exact results carry per-train candidates; smart results carry journeys. Bookable options carry booking_offer with a server-signed selection_token.",
|
|
75
77
|
"The token binds the stored result snapshot and the chosen option. Submit it with passengers (and seat_type when the user picked a class); the server derives the authoritative legs and re-quotes live. Do NOT hand-build legs JSON from the visible result.",
|
|
76
78
|
"An expired or foreign token returns a clear error; recover by running a fresh query or by explicit legs — never by guessing token contents.",
|
|
77
|
-
"Repeating the same selection restores the same booking execution instead of creating a second one."
|
|
79
|
+
"Repeating the same selection restores the same booking execution instead of creating a second one.",
|
|
80
|
+
"v2 single-leg cards carry an opaque rsel_ selection_token bound to owner, execution, query revision, snapshot, journey/offer, policy and expiry; a stale or foreign token returns selection_stale — re-read the snapshot for a fresh offer instead of retrying.",
|
|
81
|
+
"v2 separate_legs_only journeys never carry a whole-journey token: open the journey detail, pick one leg, and use that leg's owner-issued booking_offer; each leg is quoted and confirmed independently with no joint guarantee."
|
|
78
82
|
],
|
|
79
83
|
"trial_and_login": {
|
|
80
84
|
"policy": "itpay-rail-exact and itpay-rail-smart each allow 2 anonymous queries per enrolled device lineage — independent counters, not a shared pool of 4.",
|
|
@@ -100,7 +104,14 @@
|
|
|
100
104
|
"catalog_or_recommend": "Location resolved but no feasible trains: change the date or stations.",
|
|
101
105
|
"login_required_is_not_failure": "login_required / rate_limited pause the execution; they are NOT failures and never justify a replacement execution.",
|
|
102
106
|
"general_rule": "Never replay a failed execution and never guess different field names; correct the input and start a new execution of the same service.",
|
|
103
|
-
"booking_review_changed": "The submitted draft_revision is stale: re-read services next for the current review projection and resubmit the full input; do not bump the revision yourself."
|
|
107
|
+
"booking_review_changed": "The submitted draft_revision is stale: re-read services next for the current review projection and resubmit the full input; do not bump the revision yourself.",
|
|
108
|
+
"plan_conflict": "409: the action_request_id/expected_query_revision/snapshot_id no longer matches owner state. Re-read services next and reuse the fresh action handle; an identical replay of an accepted action still returns its original acceptance.",
|
|
109
|
+
"stale_cursor": "The paging cursor was minted against an older snapshot; restart paging from the current snapshot_id.",
|
|
110
|
+
"snapshot_not_found": "Unknown or superseded snapshot id; read the current projection for the latest snapshot.",
|
|
111
|
+
"journey_not_in_snapshot": "The rj_ reference is not inside the saved snapshot; pick a journey from the current page/projection.",
|
|
112
|
+
"plan_expired": "The 15-minute plan lifecycle ended: expansion stopped and the snapshot is read-only evidence; start a new execution for a fresh search. In-flight checkout/payment is unaffected.",
|
|
113
|
+
"action_request_invalid": "The action payload failed validation (unknown patch fields, malformed handle); copy the command from available_actions instead of composing it.",
|
|
114
|
+
"selection_stale": "The rsel_ token no longer matches the current snapshot/policy; re-read the journey and take the fresh booking_offer."
|
|
104
115
|
},
|
|
105
116
|
"seat_preferences": {
|
|
106
117
|
"meaning": "A buyer's seat-letter preference may be planned per leg before payment. The plan is submitted to the supplier when encodable; unsupported combinations fall back to automatic assignment.",
|
|
@@ -118,7 +129,12 @@
|
|
|
118
129
|
"Take draft_revision only from the current services next review projection; on booking_review_changed re-read instead of retrying the stale value.",
|
|
119
130
|
"login_required and rate_limited are pauses, not failures: resume the same execution, never spawn a replacement.",
|
|
120
131
|
"Do not repeat or replay a failed execution; correct the input and start a new one.",
|
|
121
|
-
"Do not claim payment equals ticket issuance; issuance is asynchronous and confirmed only by the issued seat status."
|
|
132
|
+
"Do not claim payment equals ticket issuance; issuance is asynchronous and confirmed only by the issued seat status.",
|
|
133
|
+
"For v2 plans, act only on available_actions[] from the current projection: run the exact command the backend returned, then re-read; never invent action types, request ids, revisions or fields.",
|
|
134
|
+
"expand_search is the only intent that may spend provider budget; refine_preferences re-ranks saved data first and does not by itself expand.",
|
|
135
|
+
"A paused/expired/complete plan is still readable and selectable from its saved snapshot; never start a replacement execution to keep polling.",
|
|
136
|
+
"stop_search pauses scheduling but keeps saved data and existing selections; booking stays reachable only through the allowed actions the owner reports.",
|
|
137
|
+
"When since_snapshot returns result_not_updated, keep the previous snapshot and still surface the latest expansion status/end reason to the user."
|
|
122
138
|
],
|
|
123
139
|
"human_language": {
|
|
124
140
|
"prefer": [
|
|
@@ -161,5 +177,15 @@
|
|
|
161
177
|
"status_read": "The order read exposes masked per-leg notification entries (keys: issued, details_pending, seat_ready) with channel, status and masked recipient — never raw numbers.",
|
|
162
178
|
"truthfulness": "Provider acceptance is not delivery; an uncertain send outcome is reconciled, not silently retried. A notification failure never changes ticket or refund state; report it as a delivery detail, not an order failure.",
|
|
163
179
|
"unavailable": "If SMS is not configured or a recipient is unverified, email still satisfies the contact policy — never present an unverified phone as enabled."
|
|
180
|
+
},
|
|
181
|
+
"progressive_v2": {
|
|
182
|
+
"capability": "New CLI declares client_context.features=[\"rail.progressive.v2\"] at service start. It is a response-format declaration only; quota, service version and provider credentials stay server-decided. Old clients keep the legacy projection; never create a second execution to probe.",
|
|
183
|
+
"read_model": "While the plan workflow is active, services next carries rail_planning: schema_version, plan_id (jp_), query_revision, snapshot_id (rps_), readiness, expansion {status, reason, poll_after_ms, new_results_guaranteed}, coverage, budgets, recommendation + up to 4 alternatives (journey cards rj_), available_actions with concrete commands, result_item_id.",
|
|
184
|
+
"expansion_states": "queued/running: automatic work remains; paused: batch sufficient or auto window ended, expand only on explicit intent; complete/cancelled: budget or frontier exhausted — snapshot stays read-only, expand action absent; failed: reason visible after a published snapshot, readiness stays ready; expired: 15-minute lifecycle end — evidence read-only, in-flight purchase unaffected.",
|
|
185
|
+
"reads": "services next --since-snapshot <rps_…> suppresses the journey payload but always returns the latest expansion status. read-result gains --snapshot, --journey <rj_…> and an opaque --cursor bound to snapshot+filter+sort (stale -> stale_cursor). Reads never call the provider, never extend lifecycle, never invoke the model.",
|
|
186
|
+
"actions": "Three allowlisted planning actions ride POST actions with action_type workflow:expand_search | workflow:refine_preferences | workflow:stop_search. input_snapshot carries server-issued action_request_id plus expected_query_revision and snapshot_id; refine adds a validated preference_patch. Always copy the concrete command from available_actions[]; never assemble fields. Same id + same payload returns the stored acceptance; same id + different payload -> plan_conflict.",
|
|
187
|
+
"journey_card": "Cards carry station names, train codes, dated times, transfer/same-run notes, observed_at price time and handles. representative_offer.ticket_offers[] lists one purchasable segment per entry (leg_index, offer_id, service_ref, station codes, travel_date, seat_type, passengers, integer-minor amounts); per-item amounts sum to the total. rides[] (ridden legs) and ticket_offers[] (purchasable segments) are different lists — never infer transfers or fees from array lengths.",
|
|
188
|
+
"selection": "booking_support=single_leg cards carry booking_offer {service_id: itpay-rail-booking, selection_token: rsel_…}; submit it to itpay-rail-booking with passengers as today. separate_legs_only cards expose no whole-journey token — each leg is quoted and confirmed separately via its protected detail; never assemble legs JSON yourself. booking_support=none has no selection action.",
|
|
189
|
+
"honesty": "Coverage is bounded: repeat searched_scope/notices verbatim, never claim national-exhaustive search, and surface missing prices as unknown — never 0."
|
|
164
190
|
}
|
|
165
191
|
}
|
|
@@ -33,17 +33,17 @@
|
|
|
33
33
|
{
|
|
34
34
|
"agent_type": "workbuddy",
|
|
35
35
|
"default_host": "plain-chat",
|
|
36
|
-
"responsibility": "execute handoff.agent_action exactly once to open the fully rendered handoff.url with present_files; show the amount and stop"
|
|
36
|
+
"responsibility": "execute handoff.agent_action exactly once to open the fully rendered handoff.url with present_files, then send handoff.mobile_url as a clickable link that jumps straight into Alipay or WeChat Pay on a phone; show the amount and stop"
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
"agent_type": "zcode",
|
|
40
40
|
"default_host": "plain-chat",
|
|
41
|
-
"responsibility": "open handoff.url immediately in the ZCode built-in browser
|
|
41
|
+
"responsibility": "open handoff.url immediately in the ZCode built-in browser, also send handoff.mobile_url as the phone-friendly entry, then stop; when the built-in browser is unavailable show both clickable links"
|
|
42
42
|
},
|
|
43
43
|
{
|
|
44
44
|
"agent_type": "doubao-work",
|
|
45
45
|
"default_host": "plain-chat",
|
|
46
|
-
"responsibility": "show handoff.
|
|
46
|
+
"responsibility": "show handoff.mobile_url as the preferred same-phone checkout or authorization link and handoff.qr_image_url as the labeled save-or-scan fallback; state the amount for Checkout and stop"
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
"agent_type": "kimi-code",
|
|
@@ -73,10 +73,11 @@
|
|
|
73
73
|
"agent_rules": [
|
|
74
74
|
"Agent Type is stable identity; Host is presentation and may be explicitly overridden.",
|
|
75
75
|
"A local QR path is not visible until a desktop Agent attaches or renders that file on the human-facing surface; WorkBuddy never receives a local QR path.",
|
|
76
|
-
"For workbuddy with plain-chat, execute handoff.agent_action exactly once; it calls present_files with handoff.url as the only files element and opens the rendered Card Link in the right-side browser.",
|
|
77
|
-
"For zcode with plain-chat, open handoff.url in the built-in browser.
|
|
78
|
-
"For doubao-work with plain-chat, show two labeled official links: handoff.
|
|
79
|
-
"If the WorkBuddy action fails, send the original handoff.url, report that it did not auto-open, and stop without creating another payment resource.",
|
|
76
|
+
"For workbuddy with plain-chat, execute handoff.agent_action exactly once; it calls present_files with handoff.url as the only files element and opens the rendered Card Link in the right-side browser, then send handoff.mobile_url so phone users can tap straight into Alipay or WeChat Pay.",
|
|
77
|
+
"For zcode with plain-chat, open handoff.url in the built-in browser and also send handoff.mobile_url as the phone entry; do not only paste text, download or rebuild a QR, or invent a private browser-tool payload; when the built-in browser is unavailable show both clickable links.",
|
|
78
|
+
"For doubao-work with plain-chat, show two labeled official links: handoff.mobile_url for direct same-phone opening and handoff.qr_image_url for saving or scanning on another device; do not parse credentials, download or rebuild the QR, or create an alternative resource.",
|
|
79
|
+
"If the WorkBuddy action fails, send the original handoff.url and handoff.mobile_url, report that it did not auto-open, and stop without creating another payment resource.",
|
|
80
|
+
"handoff.mobile_url is the phone-friendly Checkout URL; when the current client cannot open links in place (for example a mobile mini program), send it and tell the human to copy it into the phone browser where it can jump into Alipay or WeChat Pay. It is only returned when the handoff carries the complete token pair; when absent, handoff.url remains the entry.",
|
|
80
81
|
"Do not claim the handoff was shown until both a usable payment image or QR and the Checkout URL are visible.",
|
|
81
82
|
"IM Hosts that require a target must receive --target before Checkout creation. OpenClaw has no default Host; Kimi Code uses terminal.",
|
|
82
83
|
"OpenClaw Telegram uses flat url/value buttons for deployed-version compatibility. A grant_confirmed callback carries only the Checkout ID, never a display token, and never proves payment or an active grant."
|
|
@@ -27,6 +27,19 @@ itpay services action <service_execution_id> --action <action_type>
|
|
|
27
27
|
|
|
28
28
|
普通 Agent 优先使用 `--candidate <rank>`。CLI 只从当前 Execution 的 `current_result_items` 解析 Result Item ID;Backend 再读取权威 Invocation 和 Stable Hash。Agent 不提交 Hash,也不能使用其他 Execution 或外部来源的候选。`--result-item` 只用于已持有当前 Execution 内部句柄的受控恢复,不应要求用户提供。
|
|
29
29
|
|
|
30
|
+
## 规划动作(rail.progressive.v2)
|
|
31
|
+
|
|
32
|
+
`services next` 的 `rail_planning.available_actions` 会给出可直接执行的完整命令。规划动作只允许以下类型,由 planning owner 校验执行归属、plan 状态与幂等键,不影响 Arazzo `human_action` 状态机或公共执行状态:
|
|
33
|
+
|
|
34
|
+
| action_type | 供应商消耗 | 说明 |
|
|
35
|
+
| --- | --- | --- |
|
|
36
|
+
| `workflow:expand_search` | 有界新增查询 | 仅在扩展暂停(`expansion_status=paused`)时可用;必须带服务端下发的 `action_request_id`(`pa_…`)与 `expected_query_revision`,一轮一柄、用完作废。 |
|
|
37
|
+
| `workflow:refine_preferences` | 无(本地重排) | 用 `--input-json` 提交偏好补丁,对已保存证据重排序。 |
|
|
38
|
+
| `workflow:stop_search` | 无 | 停止未发送的扩展任务;不影响已开始或已完成的出票。 |
|
|
39
|
+
| `select_journey` | 无 | 记录用户选定:`--input journey_id=<rj_…>`;仅记录选择、不改排名,后续购买仍走既有报价与受保护 Checkout。 |
|
|
40
|
+
|
|
41
|
+
同一 `action_request_id` 相同内容重放返回原受理结果;相同 ID 不同内容会被判冲突。过期 `expected_query_revision` 的新动作被拒绝。
|
|
42
|
+
|
|
30
43
|
## 标准输出
|
|
31
44
|
|
|
32
45
|
```json
|
|
@@ -22,6 +22,7 @@ itpay services next <service_execution_id> [--json]
|
|
|
22
22
|
| 参数 | 必填 | 说明 |
|
|
23
23
|
| --- | --- | --- |
|
|
24
24
|
| `service_execution_id` | 是 | `services start` 或后续命令返回的 execution ID。 |
|
|
25
|
+
| `--since-snapshot <snapshot_id>` | 否 | rail.progressive.v2 增量读:传上次看到的 `snapshot_id`,未变化时只回扩展状态与 `result_not_updated`,不重复下发 journey 负载;永远不触发供应商调用。 |
|
|
25
26
|
| `--json` | 否 | 输出稳定 JSON 信封;未指定时输出相同事实的简洁文本。 |
|
|
26
27
|
|
|
27
28
|
需要有效 Agent Device session。命令不接受 Buyer token、capability 或服务输入。
|
|
@@ -291,3 +292,12 @@ itpay services get <service_execution_id> --json
|
|
|
291
292
|
## Agent Type / Host
|
|
292
293
|
|
|
293
294
|
所有正式支持的 Local Agent Type 返回完全相同的状态、safe payload、instruction 和 next。本命令不渲染二维码,也不包含 Host handoff。
|
|
295
|
+
|
|
296
|
+
## 渐进规划(rail.progressive.v2)
|
|
297
|
+
|
|
298
|
+
智能铁路规划服务在执行读模型上附带 `rail_planning` 投影:`readiness`(pending/ready/expired)、`search.expansion_status`(queued/running/paused/complete/cancelled/failed/expired)、`recommendation` 与 `alternatives` 行程卡片、以及服务端给出的完整 `available_actions` 命令。
|
|
299
|
+
|
|
300
|
+
- `running`/`queued`:稍后按 `next.command` 增量轮询同一执行;不要重新发起查询。
|
|
301
|
+
- `paused`:首批结果已就绪、扩展暂停等待用户意图;`expand_search` 是唯一会再消耗供应商配额的动作。
|
|
302
|
+
- `complete`:扩展收敛;展示推荐并等待用户选票。
|
|
303
|
+
- 选票走 `select_journey` 动作(命令在 `available_actions` 或卡片的 `select` 字段里,原样执行);乘车人身份信息永远只在受保护 Checkout 页面填写。
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
## 语法与参数
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
itpay services page <service_execution_id> <result_item_id> [--offset <offset>] [--limit <limit>] [--json]
|
|
17
|
+
itpay services page <service_execution_id> <result_item_id> [--offset <offset>] [--limit <limit>] [--cursor <rcur_n>] [--json]
|
|
18
|
+
|
|
19
|
+
rail.progressive.v2 中 `<result_item_id>` 也可以是规划快照 ID(`rps_…`,见 `services next` 返回的 `snapshot_id`):分页读取该不可变快照内的 journey 卡片,`--cursor` 使用上一页返回的 `rcur_<offset>` 不透明游标。
|
|
18
20
|
```
|
|
19
21
|
|
|
20
22
|
| 参数 | 必填 | 说明 |
|
|
@@ -18,8 +18,12 @@ itpay services read-result <service_execution_id> [--json]
|
|
|
18
18
|
| 参数 | 必填 | 说明 |
|
|
19
19
|
| --- | --- | --- |
|
|
20
20
|
| `service_execution_id` | 是 | Vault 交付对应的 execution ID。 |
|
|
21
|
+
| `--journey <journey_id>` | 否 | rail.progressive.v2 规划明细:读取指定 journey 的完整卡片(分段、席别报价、接驳估计、风险标注)。免费、属主校验、不触发供应商调用。 |
|
|
22
|
+
| `--snapshot <snapshot_id>` | 否 | 与 `--journey` 配合,钉住某个已提交快照(`rps_…`);缺省时读最新 catalog 快照。 |
|
|
21
23
|
| `--json` | 否 | 输出稳定 JSON 信封;未指定时输出相同事实的简洁文本。 |
|
|
22
24
|
|
|
25
|
+
带 `--journey` 时走规划读取分支:不经过 grant/Vault、不受 `access_locked` 影响,因为规划明细本就是免付费证据。未带选择器时原授权路径完全不变。
|
|
26
|
+
|
|
23
27
|
CLI 使用已登记设备的签名 session,不接受 Checkout token、Buyer token、`agent_device_id` 参数或开发者凭证。
|
|
24
28
|
|
|
25
29
|
CLI 直接请求 Backend 的当前有效 Grant。历史 `delivery_bindings` 不作为访问判断;Backend 负责验证当前 Vault、Agent instance、Buyer、scope、TTL 和退款锁。
|
package/package.json
CHANGED
package/skills/itpay/SKILL.md
CHANGED
|
@@ -76,6 +76,13 @@ platform whenever possible. Only show the same clickable URL when no browser
|
|
|
76
76
|
or native action is available; never ask the human to run a command or rebuild
|
|
77
77
|
a QR code.
|
|
78
78
|
|
|
79
|
+
Checkout handoffs also carry `handoff.mobile_url`: send it alongside the main
|
|
80
|
+
link and tell the human that on a phone it opens the cashier and can jump
|
|
81
|
+
straight into Alipay or WeChat Pay. When the current client cannot open links
|
|
82
|
+
itself (for example a mobile mini program without a side panel), send
|
|
83
|
+
`mobile_url` and tell the human to copy it into the phone's browser if it does
|
|
84
|
+
not open in place.
|
|
85
|
+
|
|
79
86
|
## Serve The Human
|
|
80
87
|
|
|
81
88
|
- Ask the human only to choose, authorize, pay, provide required contact
|