@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
|
@@ -1,261 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export class RailCatalogEncodingError extends Error {
|
|
3
|
-
constructor(encoding) {
|
|
4
|
-
super(`catalog encoding ${JSON.stringify(encoding)} is not supported by CLI ${CLI_VERSION} ` +
|
|
5
|
-
`(${cliDistribution()}); obtain a compatible exact version through this installation channel. ` +
|
|
6
|
-
`The committed catalog is intact`);
|
|
7
|
-
this.name = "rail_catalog_encoding";
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
function scopeFromProfiles(value) {
|
|
11
|
-
const profiles = Array.isArray(value) ? value : [];
|
|
12
|
-
const selected = profiles.find((p) => Array.isArray(p) && Array.isArray(p[1]) && p[1].includes("balanced")) ?? profiles[0];
|
|
13
|
-
if (!Array.isArray(selected))
|
|
14
|
-
return { origin: "legacy_unknown", destination: "legacy_unknown",
|
|
15
|
-
duration_basis: "legacy_unknown", arrival_basis: "legacy_unknown", cost_basis: "legacy_unknown",
|
|
16
|
-
duration_minutes: null, estimated_total_cost: null, cost_incomplete: true };
|
|
17
|
-
const extras = selected[10] && typeof selected[10] === "object" ? selected[10] : {};
|
|
18
|
-
const total = typeof selected[9] === "number" && extras.ci !== true ? selected[9] / 100 : null;
|
|
19
|
-
return {
|
|
20
|
-
origin: extras.os ?? "legacy_unknown", destination: extras.ds ?? "legacy_unknown",
|
|
21
|
-
duration_basis: extras.db ?? "legacy_unknown", arrival_basis: extras.ab ?? "legacy_unknown",
|
|
22
|
-
cost_basis: extras.cb ?? "legacy_unknown", duration_minutes: selected[8] ?? null,
|
|
23
|
-
estimated_total_cost: total, cost_incomplete: extras.ci === true,
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
function asString(v) {
|
|
27
|
-
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
28
|
-
}
|
|
29
|
-
function asIndex(v, bound) {
|
|
30
|
-
if (typeof v !== "number" || !Number.isInteger(v) || v < 0 || v >= bound) {
|
|
31
|
-
throw new Error("shared_rows: index out of bounds");
|
|
32
|
-
}
|
|
33
|
-
return v;
|
|
34
|
-
}
|
|
35
|
-
function asObject(value) {
|
|
36
|
-
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
37
|
-
}
|
|
38
|
-
function datedTime(date, value) {
|
|
39
|
-
const raw = asString(value);
|
|
40
|
-
if (!raw)
|
|
41
|
-
return null;
|
|
42
|
-
if (/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}/.test(raw))
|
|
43
|
-
return raw.replace("T", " ").slice(0, 16);
|
|
44
|
-
const short = raw.match(/^(\d{2}:\d{2})(?:\+(\d+))?$/);
|
|
45
|
-
const day = asString(date);
|
|
46
|
-
if (!short || !day)
|
|
47
|
-
return null;
|
|
48
|
-
if (!short[2])
|
|
49
|
-
return `${day} ${short[1]}`;
|
|
50
|
-
const shifted = new Date(`${day}T00:00:00Z`);
|
|
51
|
-
if (Number.isNaN(shifted.getTime()))
|
|
52
|
-
return null;
|
|
53
|
-
shifted.setUTCDate(shifted.getUTCDate() + Number(short[2]));
|
|
54
|
-
return `${shifted.toISOString().slice(0, 10)} ${short[1]}`;
|
|
55
|
-
}
|
|
56
|
-
function elapsedMinutes(departure, arrival) {
|
|
57
|
-
if (!departure || !arrival)
|
|
58
|
-
return null;
|
|
59
|
-
const start = Date.parse(departure.replace(" ", "T") + "Z");
|
|
60
|
-
const end = Date.parse(arrival.replace(" ", "T") + "Z");
|
|
61
|
-
return Number.isFinite(start) && Number.isFinite(end) && end >= start ? (end - start) / 60_000 : null;
|
|
62
|
-
}
|
|
63
|
-
function seatOffer(row, names) {
|
|
64
|
-
if (!Array.isArray(row))
|
|
65
|
-
return null;
|
|
66
|
-
const extras = asObject(row[7]);
|
|
67
|
-
const code = asString(row[1]);
|
|
68
|
-
return {
|
|
69
|
-
offer_ref: row[0], seat_type: code ?? null, seat_name: code ? names[code] ?? code : null,
|
|
70
|
-
inventory_status: row[2] ?? null, quantity: row[3] ?? null, unit_price_fen: row[4] ?? null,
|
|
71
|
-
eligible: row[5] === true, requires_quote: row[6] === true,
|
|
72
|
-
price_source: extras.s ?? "sale_price", quote_price_unverified: extras.u === true,
|
|
73
|
-
conflicted: extras.c === true,
|
|
74
|
-
...(Array.isArray(extras.o) ? { price_observations_fen: extras.o } : {}),
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
function decodeRide(row, wait, stations, services) {
|
|
78
|
-
if (!Array.isArray(row)) {
|
|
79
|
-
const ride = asObject(row);
|
|
80
|
-
if (!Object.keys(ride).length)
|
|
81
|
-
throw new Error("rail catalog: malformed ride row");
|
|
82
|
-
const refs = Array.isArray(ride.service_refs) ? ride.service_refs : [];
|
|
83
|
-
const codes = [...new Set(refs.map((ref) => asObject(services[asString(ref) ?? ""]).tc)
|
|
84
|
-
.filter((code) => typeof code === "string" && code.length > 0))];
|
|
85
|
-
const departure = datedTime(ride.boarding_date, ride.departure_at ?? ride.departure);
|
|
86
|
-
const arrival = datedTime(ride.boarding_date, ride.arrival_at ?? ride.arrival);
|
|
87
|
-
return { train_codes: codes, train_code: codes.join("/"),
|
|
88
|
-
from_station: stations[asString(ride.from_station_code) ?? ""] ?? ride.from_station ?? null,
|
|
89
|
-
to_station: stations[asString(ride.to_station_code) ?? ""] ?? ride.to_station ?? null,
|
|
90
|
-
boarding_date: ride.boarding_date ?? null,
|
|
91
|
-
departure, arrival, duration_minutes: elapsedMinutes(departure, arrival),
|
|
92
|
-
wait_minutes: typeof wait === "number" ? wait : ride.wait_minutes ?? null,
|
|
93
|
-
same_run: ride.same_run === true, onboard_stops: Array.isArray(ride.onboard_stops) ? ride.onboard_stops : [],
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
const extras = asObject(row[8]);
|
|
97
|
-
const refs = Array.isArray(row[4]) ? row[4] : [];
|
|
98
|
-
const codes = [...new Set(refs.map((ref) => asObject(services[asString(ref) ?? ""]).tc)
|
|
99
|
-
.filter((code) => typeof code === "string" && code.length > 0))];
|
|
100
|
-
const base = row[0];
|
|
101
|
-
const departure = datedTime(base, extras.dep);
|
|
102
|
-
const arrival = datedTime(base, extras.arr);
|
|
103
|
-
return {
|
|
104
|
-
train_codes: codes, train_code: codes.join("/"),
|
|
105
|
-
from_station: stations[asString(row[2]) ?? ""] ?? row[2] ?? null,
|
|
106
|
-
to_station: stations[asString(row[3]) ?? ""] ?? row[3] ?? null,
|
|
107
|
-
boarding_date: base ?? null,
|
|
108
|
-
departure, arrival, duration_minutes: elapsedMinutes(departure, arrival),
|
|
109
|
-
wait_minutes: typeof wait === "number" ? wait : typeof row[5] === "number" ? row[5] : null,
|
|
110
|
-
same_run: row[6] === true, onboard_stops: Array.isArray(row[7]) ? row[7] : [],
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
function decodePlans(rawPlans, rawProfiles, packed, catalog, services, names, stations) {
|
|
114
|
-
const plans = Array.isArray(rawPlans) ? rawPlans : [];
|
|
115
|
-
const profiles = Array.isArray(rawProfiles) ? rawProfiles : [];
|
|
116
|
-
const preferred = profiles.find((profile) => Array.isArray(profile) && Array.isArray(profile[1]) && profile[1].includes("balanced")) ?? profiles[0];
|
|
117
|
-
const profile = Array.isArray(preferred) ? preferred : [];
|
|
118
|
-
const serviceIndex = Array.isArray(catalog.service_index) ? catalog.service_index : [];
|
|
119
|
-
const selectedPlan = packed && typeof profile[2] === "number" ? plans[asIndex(profile[2], plans.length)] :
|
|
120
|
-
plans.find((plan) => Array.isArray(plan) && plan[0] === profile[2]);
|
|
121
|
-
const selectedRef = Array.isArray(selectedPlan) ? selectedPlan[0] : null;
|
|
122
|
-
const decoded = plans.map((plan) => {
|
|
123
|
-
if (!Array.isArray(plan))
|
|
124
|
-
throw new Error("rail catalog: malformed ticket plan");
|
|
125
|
-
const refs = (Array.isArray(plan[1]) ? plan[1] : []).map((ref) => packed && typeof ref === "number" ? serviceIndex[asIndex(ref, serviceIndex.length)] : ref);
|
|
126
|
-
const extras = asObject(plan[7]);
|
|
127
|
-
const legs = refs.map((ref, index) => {
|
|
128
|
-
const service = asObject(services[asString(ref) ?? ""]);
|
|
129
|
-
const offers = Array.isArray(service.of) ? service.of : [];
|
|
130
|
-
const selectedOffer = plan[0] === selectedRef && Array.isArray(profile[3]) ? profile[3][index] : undefined;
|
|
131
|
-
const chosen = packed && typeof selectedOffer === "number" ? offers[asIndex(selectedOffer, offers.length)] :
|
|
132
|
-
offers.find((offer) => Array.isArray(offer) && offer[0] === selectedOffer);
|
|
133
|
-
return {
|
|
134
|
-
service_ref: ref ?? null, train_code: service.tc ?? null,
|
|
135
|
-
from_station: stations[asString(service.f) ?? ""] ?? service.f ?? null,
|
|
136
|
-
to_station: stations[asString(service.t) ?? ""] ?? service.t ?? null,
|
|
137
|
-
departure: datedTime(service.d, service.dep), arrival: datedTime(service.d, service.arr),
|
|
138
|
-
seat_options: offers.map((offer) => seatOffer(offer, names)).filter((offer) => offer !== null),
|
|
139
|
-
...(chosen ? { selected_seat: seatOffer(chosen, names) } : {}),
|
|
140
|
-
};
|
|
141
|
-
});
|
|
142
|
-
const amount = asObject(plan[3]);
|
|
143
|
-
return {
|
|
144
|
-
ticket_plan_ref: plan[0], passenger_count: plan[2] ?? null,
|
|
145
|
-
purchase_support: plan[6] ?? "none", rail_amount_verified: plan[4] === true,
|
|
146
|
-
rail_amount_fen: { default: amount.def ?? null, minimum: amount.min ?? null, maximum: amount.max ?? null },
|
|
147
|
-
service_fee_fen: plan[5] ?? null, quote_price_unverified: extras.qu === true,
|
|
148
|
-
legs,
|
|
149
|
-
};
|
|
150
|
-
});
|
|
151
|
-
const selected = decoded.find((plan) => plan.ticket_plan_ref === selectedRef);
|
|
152
|
-
return { plans: decoded, ...(selected ? { selected } : {}) };
|
|
153
|
-
}
|
|
154
|
-
// _cn_route_text port: "南头 C7608 → 广州南换乘71分 → G2944 重庆西" — kept
|
|
155
|
-
// byte-identical with the planner so a packed journey renders the same text
|
|
156
|
-
// its object form carried.
|
|
157
|
-
function regenerateRoute(ridePairs, rideTable, stations, services) {
|
|
158
|
-
const name = (code, fallback) => asString(stations[asString(code) ?? ""]) ?? asString(fallback) ?? asString(code) ?? "?";
|
|
159
|
-
const rides = ridePairs.map((pair) => {
|
|
160
|
-
if (!Array.isArray(pair) || pair.length !== 2)
|
|
161
|
-
throw new Error("shared_rows: malformed ride pair");
|
|
162
|
-
const src = rideTable[asIndex(pair[0], rideTable.length)];
|
|
163
|
-
if (!Array.isArray(src))
|
|
164
|
-
throw new Error("shared_rows: malformed ride row");
|
|
165
|
-
return { row: src, wait: pair[1] };
|
|
166
|
-
});
|
|
167
|
-
const first = rides[0];
|
|
168
|
-
if (first === undefined)
|
|
169
|
-
return "";
|
|
170
|
-
const trainCodes = (rideRow) => (Array.isArray(rideRow[4]) ? rideRow[4] : [])
|
|
171
|
-
.map((ref) => {
|
|
172
|
-
const svc = services[asString(ref) ?? ""];
|
|
173
|
-
return asString(svc?.tc);
|
|
174
|
-
})
|
|
175
|
-
.filter((c) => Boolean(c));
|
|
176
|
-
const waitOf = (ride) => typeof ride.wait === "number" ? ride.wait
|
|
177
|
-
: typeof ride.row[5] === "number" ? ride.row[5]
|
|
178
|
-
: undefined;
|
|
179
|
-
const sameRun = (rideRow) => rideRow[6] === true;
|
|
180
|
-
const parts = [`${name(first.row[2], undefined)} ${trainCodes(first.row).join("/")}`];
|
|
181
|
-
for (let i = 1; i < rides.length; i += 1) {
|
|
182
|
-
const prev = rides[i - 1].row;
|
|
183
|
-
const ride = rides[i];
|
|
184
|
-
const via = name(prev[3], undefined);
|
|
185
|
-
const wait = waitOf(ride);
|
|
186
|
-
parts.push(sameRun(ride.row)
|
|
187
|
-
? wait !== undefined ? `${via}同车停${wait}分` : `${via}同车接续`
|
|
188
|
-
: wait !== undefined ? `${via}换乘${wait}分` : `${via}换乘`);
|
|
189
|
-
parts.push(`${trainCodes(ride.row).join("/")} ${name(ride.row[3], undefined)}`);
|
|
190
|
-
}
|
|
191
|
-
if (rides.length === 1) {
|
|
192
|
-
parts.push(name(first.row[3], undefined));
|
|
193
|
-
}
|
|
194
|
-
return parts.filter((s) => s.trim().length > 0).join(" → ");
|
|
195
|
-
}
|
|
196
|
-
// decodeRailCatalogJourneys returns one summary per committed combination in
|
|
197
|
-
// catalog order. Legacy object-form catalogs (no `encoding`) pass through;
|
|
198
|
-
// shared_rows.v1 rows are positional under journey_columns
|
|
199
|
-
// [ref, route, rides, plans, gc, pr, profiles, tc, ev, rep, tw, risk].
|
|
200
|
-
// Any other encoding throws RailCatalogEncodingError — an explicit upgrade
|
|
201
|
-
// hint, never an empty catalog.
|
|
202
|
-
export function decodeRailCatalogJourneys(catalog) {
|
|
203
|
-
const encoding = catalog["encoding"];
|
|
204
|
-
const layerMap = catalog["choice_layers"]?.["journey_layer"] ?? {};
|
|
205
|
-
const layerOf = (ref) => layerMap[ref] === "backup" ? "backup" : "main";
|
|
206
|
-
const stations = asObject(catalog.stations);
|
|
207
|
-
const services = asObject(catalog.services);
|
|
208
|
-
const seatNames = asObject(catalog.seat_names);
|
|
209
|
-
if (encoding === undefined || encoding === null) {
|
|
210
|
-
const rows = Array.isArray(catalog["journeys"]) ? catalog["journeys"] : [];
|
|
211
|
-
return {
|
|
212
|
-
packed: false,
|
|
213
|
-
journeys: rows.map((j) => {
|
|
214
|
-
const rec = (j ?? {});
|
|
215
|
-
const ref = asString(rec["ref"]) ?? asString(rec["journey_id"]) ?? "?";
|
|
216
|
-
const calculationScope = scopeFromProfiles(rec["profiles"]);
|
|
217
|
-
const rides = (Array.isArray(rec.rides) ? rec.rides : []).map((ride) => decodeRide(ride, null, stations, services));
|
|
218
|
-
const ticketPlans = decodePlans(rec.plans, rec.profiles, false, catalog, services, seatNames, stations);
|
|
219
|
-
return {
|
|
220
|
-
ref,
|
|
221
|
-
route: asString(rec["route"]) ?? asString(rec["route_text"]) ?? asString(rec["summary"]) ?? "",
|
|
222
|
-
defaultLayer: layerOf(ref),
|
|
223
|
-
rides,
|
|
224
|
-
...(calculationScope ? { calculation_scope: calculationScope } : {}),
|
|
225
|
-
ticket_plans: ticketPlans.plans,
|
|
226
|
-
...(ticketPlans.selected ? { selected_plan: ticketPlans.selected } : {}),
|
|
227
|
-
};
|
|
228
|
-
}),
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
if (encoding !== "shared_rows.v1") {
|
|
232
|
-
throw new RailCatalogEncodingError(encoding);
|
|
233
|
-
}
|
|
234
|
-
const columns = catalog["journey_columns"];
|
|
235
|
-
if (!Array.isArray(columns) || columns[0] !== "ref" || columns[1] !== "route" || columns[2] !== "rides") {
|
|
236
|
-
throw new RailCatalogEncodingError(`${String(encoding)} (unexpected journey_columns)`);
|
|
237
|
-
}
|
|
238
|
-
const rideTable = (Array.isArray(catalog["ride_table"]) ? catalog["ride_table"] : []);
|
|
239
|
-
const journeys = (Array.isArray(catalog["journeys"]) ? catalog["journeys"] : []);
|
|
240
|
-
return {
|
|
241
|
-
packed: true,
|
|
242
|
-
journeys: journeys.map((row) => {
|
|
243
|
-
if (!Array.isArray(row))
|
|
244
|
-
throw new Error("shared_rows: journey row is not an array");
|
|
245
|
-
const ref = asString(row[0]) ?? "?";
|
|
246
|
-
const route = asString(row[1]) ??
|
|
247
|
-
(Array.isArray(row[2]) ? regenerateRoute(row[2], rideTable, stations, services) : "");
|
|
248
|
-
const rides = (Array.isArray(row[2]) ? row[2] : []).map((pair) => {
|
|
249
|
-
if (!Array.isArray(pair) || pair.length !== 2)
|
|
250
|
-
throw new Error("shared_rows: malformed ride pair");
|
|
251
|
-
return decodeRide(rideTable[asIndex(pair[0], rideTable.length)], pair[1], stations, services);
|
|
252
|
-
});
|
|
253
|
-
const calculationScope = scopeFromProfiles(row[6]);
|
|
254
|
-
const ticketPlans = decodePlans(row[3], row[6], true, catalog, services, seatNames, stations);
|
|
255
|
-
return { ref, route, defaultLayer: layerOf(ref), rides,
|
|
256
|
-
...(calculationScope ? { calculation_scope: calculationScope } : {}),
|
|
257
|
-
ticket_plans: ticketPlans.plans,
|
|
258
|
-
...(ticketPlans.selected ? { selected_plan: ticketPlans.selected } : {}) };
|
|
259
|
-
}),
|
|
260
|
-
};
|
|
261
|
-
}
|
|
1
|
+
export * from "../services/rail/catalog.js";
|
|
@@ -8,8 +8,8 @@ export async function runReadyz(backend, options = {}) {
|
|
|
8
8
|
status: response.status,
|
|
9
9
|
result: { backend: "available", backend_url: backendURL, environment, ...(options.agentType ? { agent_type: options.agentType } : {}) },
|
|
10
10
|
instruction: environment === "development"
|
|
11
|
-
? `ItPay 开发环境 ${backendURL}
|
|
12
|
-
: "ItPay
|
|
11
|
+
? `ItPay 开发环境 ${backendURL} 可用。只证明后端存活。尚未读此次包 Skill 时完整读取;已读同版本则无需重复,继续设备状态检查;后续必须执行返回的完整命令并保持同一 Backend。`
|
|
12
|
+
: "ItPay 可用。只证明后端存活。尚未读此次包 Skill 时完整读取;已读同版本则无需重复,继续设备状态检查;不自动登录或开始业务。",
|
|
13
13
|
next: { command: "itpay skill show itpay --json", reason: "加载完整操作与安全规则" },
|
|
14
14
|
recovery: [],
|
|
15
15
|
}, options);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { refundStateEnvelope } from "../services/shared/refund.js";
|
|
1
2
|
import { operationID } from "../state/config.js";
|
|
2
3
|
import { formatMoney } from "../render/output.js";
|
|
3
4
|
import { resolveOutput } from "../render/sink.js";
|
|
@@ -12,46 +13,6 @@ export async function runRefund(backend, config, options) {
|
|
|
12
13
|
plainResult: Object.entries(envelope.result).map(([key, value]) => `${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`),
|
|
13
14
|
});
|
|
14
15
|
}
|
|
15
|
-
export function refundStateEnvelope(refund, status) {
|
|
16
|
-
const terminal = ["succeeded", "failed", "cancelled", "rejected"].includes(refund.status);
|
|
17
|
-
let instruction = refund.decision_mode === "manual"
|
|
18
|
-
? "先告诉用户退款已进入人工审核,原交付保持冻结;人工审核不等于拒绝,等待服务器决定,不要重复申请或承诺结果。"
|
|
19
|
-
: "先告诉用户退款申请已经记录,原交付已冻结;自动路径表示系统会继续处理,但只有最终 succeeded 才能确认退款成功。然后只跟踪同一退款,不读取交付、不创建授权或重复申请。";
|
|
20
|
-
if (!refund.access_locked)
|
|
21
|
-
instruction = "先告诉用户退款当前没有锁定交付;按服务器事实解释当前状态,不要自行推断退款结果、到账时间或交付资格。";
|
|
22
|
-
if (refund.status === "succeeded")
|
|
23
|
-
instruction = "先告诉用户退款已由 ItPay 确认成功,原交付永久关闭;不需要继续跟踪或重复申请。";
|
|
24
|
-
if (refund.status === "cancelled" || refund.status === "rejected")
|
|
25
|
-
instruction = "先告诉用户退款没有执行,交付资格可以恢复;旧读取授权不会复活,需要用户重新授权。不要把取消或拒绝说成退款成功。";
|
|
26
|
-
if (refund.status === "failed") {
|
|
27
|
-
if (refund.failure_class === "known_no_effect")
|
|
28
|
-
instruction = "先告诉用户本次退款请求确认未发送,不能说退款已成功;Agent 不自行重试,请用户等待平台管理员决定是否重新执行。";
|
|
29
|
-
else if (refund.failure_class === "retryable")
|
|
30
|
-
instruction = "先告诉用户渠道明确返回可重试失败,但 Agent 不会自行重试或重复申请;请用户等待平台管理员处理。";
|
|
31
|
-
else if (refund.failure_class === "outcome_unknown")
|
|
32
|
-
instruction = "先告诉用户退款渠道结果未知,原交付继续锁定且必须先由平台对账;禁止重试、重复申请或承诺退款结果。";
|
|
33
|
-
else if (refund.failure_class === "permanent")
|
|
34
|
-
instruction = "先告诉用户渠道明确拒绝本次退款,当前不能承诺退款成功;不要重试,请用户联系平台支持。";
|
|
35
|
-
else
|
|
36
|
-
instruction = "先告诉用户退款没有正常完成,当前不能承诺退款成功;不要重试或重复申请,请用户联系平台支持。";
|
|
37
|
-
}
|
|
38
|
-
return {
|
|
39
|
-
status,
|
|
40
|
-
result: {
|
|
41
|
-
refund_request_id: refund.refund_request_id,
|
|
42
|
-
order_id: refund.order_id,
|
|
43
|
-
decision_mode: refund.decision_mode,
|
|
44
|
-
refund_status: refund.status,
|
|
45
|
-
consumption_state: refund.consumption_state,
|
|
46
|
-
...(refund.failure_class ? { failure_class: refund.failure_class } : {}),
|
|
47
|
-
access_locked: refund.access_locked,
|
|
48
|
-
can_cancel: refund.can_cancel,
|
|
49
|
-
},
|
|
50
|
-
instruction,
|
|
51
|
-
next: terminal ? null : { command: `itpay refund watch ${refund.refund_request_id} --json`, reason: "跟踪同一退款" },
|
|
52
|
-
recovery: [],
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
16
|
export async function runListRefunds(backend, options) {
|
|
56
17
|
const out = resolveOutput(options.output);
|
|
57
18
|
const response = await backend.listOrderRefunds(options.orderID);
|
|
@@ -110,36 +71,30 @@ export async function runCancelRefund(backend, refundID, reason, options = {}) {
|
|
|
110
71
|
}
|
|
111
72
|
export async function runWatchRefund(backend, refundID, options = {}) {
|
|
112
73
|
const intervalSeconds = options.intervalSeconds ?? 2;
|
|
113
|
-
const timeoutSeconds = options.timeoutSeconds ??
|
|
74
|
+
const timeoutSeconds = options.timeoutSeconds ?? 20;
|
|
114
75
|
if (!Number.isFinite(intervalSeconds) || intervalSeconds < 1)
|
|
115
76
|
throw new Error("--interval must be at least 1 second");
|
|
116
77
|
if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0)
|
|
117
78
|
throw new Error("--timeout must be a positive number");
|
|
118
79
|
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
119
80
|
let refund;
|
|
81
|
+
let initialState;
|
|
120
82
|
for (;;) {
|
|
121
83
|
refund = await backend.getRefund(refundID);
|
|
122
|
-
|
|
84
|
+
const state = JSON.stringify([refund.status, refund.decision_mode, refund.failure_class, refund.access_locked, refund.can_cancel]);
|
|
85
|
+
if (["succeeded", "failed", "cancelled", "rejected"].includes(refund.status) || refund.decision_mode === "manual" || refund.failure_class === "outcome_unknown" || (initialState !== undefined && state !== initialState)) {
|
|
123
86
|
writeRefundEnvelope(refundStateEnvelope(refund, "watch_complete"), options);
|
|
124
87
|
return;
|
|
125
88
|
}
|
|
89
|
+
initialState ??= state;
|
|
126
90
|
const remaining = deadline - Date.now();
|
|
127
91
|
if (remaining <= 0)
|
|
128
92
|
break;
|
|
129
93
|
await new Promise((resolve) => setTimeout(resolve, Math.min(intervalSeconds * 1000, remaining)));
|
|
130
94
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
refund_request_id: refund.refund_request_id,
|
|
135
|
-
last_status: refund.status,
|
|
136
|
-
access_locked: refund.access_locked,
|
|
137
|
-
can_cancel: refund.can_cancel,
|
|
138
|
-
},
|
|
139
|
-
instruction: "先告诉用户退款仍在处理,Timeout 只表示本次等待结束,并不表示退款失败;稍后继续跟踪同一退款,不要重复申请或承诺结果。",
|
|
140
|
-
next: { command: `itpay refund watch ${refund.refund_request_id} --json`, reason: "恢复轮询" },
|
|
141
|
-
recovery: [],
|
|
142
|
-
}, options);
|
|
95
|
+
const latest = refundStateEnvelope(refund, "watch_timeout");
|
|
96
|
+
writeRefundEnvelope({ ...latest, result: { ...latest.result, last_status: refund.status, wait_ended: true },
|
|
97
|
+
instruction: `本次等待结束,并不表示退款失败。${latest.instruction} 稍后按需读取同一退款,不自动重启等待,不要重复申请。` }, options);
|
|
143
98
|
}
|
|
144
99
|
function writeRefundEnvelope(envelope, options) {
|
|
145
100
|
writeCommandEnvelope(envelope, {
|
|
@@ -148,3 +103,4 @@ function writeRefundEnvelope(envelope, options) {
|
|
|
148
103
|
plainResult: Object.entries(envelope.result).map(([key, value]) => `${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`),
|
|
149
104
|
});
|
|
150
105
|
}
|
|
106
|
+
export { refundStateEnvelope } from "../services/shared/refund.js";
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { serviceAdapter } from "../../services/registry.js";
|
|
2
|
+
import { CommandContractError, writeCommandEnvelope } from "../../services/shared/envelope.js";
|
|
3
|
+
import { normalizeServiceActionStatus, paidContinuation, serviceAllowedActionCommand, purchaseConfirmationInstruction, actionInputError } from "../../services/shared/platform.js";
|
|
4
|
+
import { servicesNextEnvelope } from "../../services/presentation.js";
|
|
5
|
+
export async function runServicesAction(backend, serviceExecutionID, actionType, input, options = {}) {
|
|
6
|
+
const selection = await resolveCandidateSelection(backend, serviceExecutionID, actionType, options);
|
|
7
|
+
const request = {
|
|
8
|
+
action_type: actionType,
|
|
9
|
+
input_snapshot: input,
|
|
10
|
+
};
|
|
11
|
+
if (options.actorType)
|
|
12
|
+
request.actor_type = options.actorType;
|
|
13
|
+
if (options.actorID)
|
|
14
|
+
request.actor_id = options.actorID;
|
|
15
|
+
if (options.status)
|
|
16
|
+
request.status = normalizeServiceActionStatus(options.status, serviceExecutionID);
|
|
17
|
+
const resultItemID = selection?.resultItemID ?? options.resultItemID;
|
|
18
|
+
if (resultItemID)
|
|
19
|
+
request.result_item_id = resultItemID;
|
|
20
|
+
if (options.requiredBefore)
|
|
21
|
+
request.required_before = options.requiredBefore;
|
|
22
|
+
let response;
|
|
23
|
+
try {
|
|
24
|
+
response = await backend.recordServiceExecutionAction(serviceExecutionID, request);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
// A failed write is never replayed. Only the locked execution identifies its adapter.
|
|
28
|
+
try {
|
|
29
|
+
const model = await backend.getServiceExecution(serviceExecutionID);
|
|
30
|
+
serviceAdapter(model.execution).actionError(error, serviceExecutionID, actionType);
|
|
31
|
+
}
|
|
32
|
+
catch (recoveryError) {
|
|
33
|
+
if (recoveryError instanceof CommandContractError)
|
|
34
|
+
throw recoveryError;
|
|
35
|
+
}
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
if (actionType === "approve_purchase" && "status" in response && response.status === "approved") {
|
|
39
|
+
const quoteID = typeof input.service_quote_lock_id === "string" ? input.service_quote_lock_id : "";
|
|
40
|
+
writeCommandEnvelope({
|
|
41
|
+
status: "purchase_preparation_approved",
|
|
42
|
+
result: { service_execution_id: serviceExecutionID, evidence_source: "agent_reported", scope: "prepare_checkout", service_quote_lock_id: quoteID },
|
|
43
|
+
instruction: "已记录同范围的准备付款入口批准;这是Agent报告的真实会话同意,不是已付款或出票。继续使用本报价;条件未变不重复询问,官方页实际支付仍由用户完成。",
|
|
44
|
+
next: quoteID ? { command: `itpay cart add --quote ${quoteID} --json`, reason: "加入本次已批准报价,准备同一个付款入口" } : null,
|
|
45
|
+
recovery: [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取原执行与付款状态,不重新购买" }],
|
|
46
|
+
}, { ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}), ...(options.output ? { output: options.output } : {}) });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if ("state" in response) {
|
|
50
|
+
const updated = await backend.getServiceExecution(serviceExecutionID);
|
|
51
|
+
const envelope = servicesNextEnvelope(updated);
|
|
52
|
+
envelope.result = {
|
|
53
|
+
...envelope.result,
|
|
54
|
+
planning_action: { action_id: response.action_id, state: response.state, replayed: response.replayed },
|
|
55
|
+
};
|
|
56
|
+
writeCommandEnvelope(envelope, {
|
|
57
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
58
|
+
...(options.output ? { output: options.output } : {}),
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (selection && actionType === "select_candidate" && response.status === "approved") {
|
|
63
|
+
const updated = await backend.getServiceExecution(serviceExecutionID);
|
|
64
|
+
const preferred = updated.allowed_actions?.[0];
|
|
65
|
+
const continuation = preferred?.type === "prepare_quote"
|
|
66
|
+
? paidContinuation(updated, preferred, {})
|
|
67
|
+
: null;
|
|
68
|
+
const next = continuation?.next ?? (preferred ? serviceAllowedActionCommand(updated, preferred) : null);
|
|
69
|
+
writeCommandEnvelope({
|
|
70
|
+
status: "candidate_selected",
|
|
71
|
+
result: {
|
|
72
|
+
service_execution_id: response.service_execution_id,
|
|
73
|
+
candidate: { rank: selection.rank, title: selection.title },
|
|
74
|
+
...(continuation ? { checkout: continuation.checkout } : {}),
|
|
75
|
+
},
|
|
76
|
+
instruction: continuation
|
|
77
|
+
? purchaseConfirmationInstruction("candidate_selected", continuation.price, continuation.capability.delivery_email_required, continuation.capability.delivery_email_purpose, selection.title)
|
|
78
|
+
: "候选已绑定到来源 Execution;后续动作必须继续使用该 Execution。",
|
|
79
|
+
next,
|
|
80
|
+
recovery: [{
|
|
81
|
+
command: `itpay services next ${response.service_execution_id} --json`,
|
|
82
|
+
reason: "重新读取服务端允许的动作",
|
|
83
|
+
}],
|
|
84
|
+
}, {
|
|
85
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
86
|
+
...(options.output ? { output: options.output } : {}),
|
|
87
|
+
});
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
writeCommandEnvelope({
|
|
91
|
+
status: "action_recorded",
|
|
92
|
+
result: {
|
|
93
|
+
service_execution_id: response.service_execution_id,
|
|
94
|
+
action_type: response.action_type,
|
|
95
|
+
action_status: response.status,
|
|
96
|
+
evidence_source: "agent_reported",
|
|
97
|
+
reported_actor_type: response.actor_type,
|
|
98
|
+
},
|
|
99
|
+
instruction: "动作已记录(Agent转述;actor_type并非官方页面真人确认的证明)。读取服务端计算的新状态;不要自行假设下一 capability。",
|
|
100
|
+
next: {
|
|
101
|
+
command: `itpay services next ${response.service_execution_id} --json`,
|
|
102
|
+
reason: "取得更新后的首选动作",
|
|
103
|
+
},
|
|
104
|
+
recovery: [],
|
|
105
|
+
}, {
|
|
106
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
107
|
+
...(options.output ? { output: options.output } : {}),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
export async function resolveCandidateSelection(backend, serviceExecutionID, actionType, options) {
|
|
111
|
+
if (options.candidateRank === undefined)
|
|
112
|
+
return undefined;
|
|
113
|
+
if (actionType !== "select_candidate") {
|
|
114
|
+
throw actionInputError(serviceExecutionID, "--candidate is only valid with --action select_candidate");
|
|
115
|
+
}
|
|
116
|
+
if (options.resultItemID) {
|
|
117
|
+
throw actionInputError(serviceExecutionID, "--candidate cannot be combined with --result-item");
|
|
118
|
+
}
|
|
119
|
+
if (!Number.isInteger(options.candidateRank) || options.candidateRank < 1) {
|
|
120
|
+
throw actionInputError(serviceExecutionID, "--candidate must be a positive integer result rank");
|
|
121
|
+
}
|
|
122
|
+
const execution = await backend.getServiceExecution(serviceExecutionID);
|
|
123
|
+
const currentItems = execution.current_result_items ?? [];
|
|
124
|
+
const result = currentItems.find((item) => item.rank === options.candidateRank);
|
|
125
|
+
if (!result) {
|
|
126
|
+
throw actionInputError(serviceExecutionID, `candidate ${options.candidateRank} is not available on service execution ${serviceExecutionID}`, "candidate_not_found");
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
resultItemID: result.service_capability_result_item_id,
|
|
130
|
+
rank: result.rank,
|
|
131
|
+
title: result.display_title,
|
|
132
|
+
};
|
|
133
|
+
}
|