@itpay/cli 2.1.40 → 2.1.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -12
- package/dist/src/client/backend.js +2 -38
- package/dist/src/client/http.js +0 -2
- package/dist/src/commands/buy.js +58 -17
- package/dist/src/commands/cart.js +15 -17
- package/dist/src/commands/catalog.js +14 -39
- package/dist/src/commands/checkout.js +5 -146
- package/dist/src/commands/checkout_handoff.js +61 -11
- package/dist/src/commands/docs.js +18 -7
- package/dist/src/commands/guidance.js +134 -1
- package/dist/src/commands/install.js +3 -6
- package/dist/src/commands/order.js +3 -34
- package/dist/src/commands/orders.js +9 -4
- package/dist/src/commands/pay.js +1 -1
- package/dist/src/commands/readyz.js +1 -1
- package/dist/src/commands/refund.js +57 -13
- package/dist/src/commands/services.js +1068 -14
- package/dist/src/commands/skill.js +5 -8
- package/dist/src/commands/vault.js +20 -27
- package/dist/src/commands/vault_handoff.js +0 -18
- package/dist/src/main.js +43 -330
- package/dist/src/render/markdown.js +3 -0
- package/dist/src/render/qr.js +1 -1
- package/dist/src/state/cart_session.js +1 -4
- package/dist/src/state/client_context.js +0 -72
- package/dist/src/state/config.js +14 -44
- package/dist/src/state/device_authority.js +41 -141
- package/docs/agent/buyer/cart-checkout.json +6 -8
- package/docs/agent/buyer/catalog-list.json +9 -29
- package/docs/agent/buyer/identity-and-sessions.json +9 -26
- package/docs/agent/buyer/install-and-setup.json +9 -10
- package/docs/agent/buyer/orders-refunds.json +9 -25
- package/docs/agent/buyer/payment-flow.json +4 -6
- package/docs/agent/buyer/purchased-content.json +4 -18
- package/docs/agent/buyer/quickstart.json +17 -26
- package/docs/agent/buyer/render-hosts.json +12 -23
- package/docs/agent/buyer/service-feedback.json +3 -4
- package/docs/cli-reference/agent-types.md +2 -7
- package/docs/cli-reference/commands/buy.md +1 -2
- package/docs/cli-reference/commands/cart/add.md +1 -5
- package/docs/cli-reference/commands/cart/clear.md +1 -1
- package/docs/cli-reference/commands/cart/index.md +1 -1
- package/docs/cli-reference/commands/cart/next.md +1 -5
- package/docs/cli-reference/commands/cart/remove.md +1 -1
- package/docs/cli-reference/commands/cart/show.md +1 -1
- package/docs/cli-reference/commands/catalog/index.md +1 -1
- package/docs/cli-reference/commands/catalog/list.md +2 -2
- package/docs/cli-reference/commands/checkout.md +2 -12
- package/docs/cli-reference/commands/device.md +11 -18
- package/docs/cli-reference/commands/docs/index.md +1 -1
- package/docs/cli-reference/commands/docs/list.md +1 -1
- package/docs/cli-reference/commands/docs/search.md +1 -1
- package/docs/cli-reference/commands/docs/show.md +1 -1
- package/docs/cli-reference/commands/install.md +6 -8
- package/docs/cli-reference/commands/next.md +1 -1
- package/docs/cli-reference/commands/order.md +1 -1
- package/docs/cli-reference/commands/pay.md +1 -1
- package/docs/cli-reference/commands/readyz.md +11 -11
- package/docs/cli-reference/commands/refund/cancel.md +1 -1
- package/docs/cli-reference/commands/refund/create.md +1 -1
- package/docs/cli-reference/commands/refund/get.md +2 -2
- package/docs/cli-reference/commands/refund/index.md +1 -1
- package/docs/cli-reference/commands/refund/list.md +1 -1
- package/docs/cli-reference/commands/refund/watch.md +1 -1
- package/docs/cli-reference/commands/services/action.md +3 -24
- package/docs/cli-reference/commands/services/checkout.md +2 -17
- package/docs/cli-reference/commands/services/events.md +1 -1
- package/docs/cli-reference/commands/services/get.md +2 -4
- package/docs/cli-reference/commands/services/index.md +3 -4
- package/docs/cli-reference/commands/services/invoke.md +4 -6
- package/docs/cli-reference/commands/services/list.md +1 -1
- package/docs/cli-reference/commands/services/next.md +12 -78
- package/docs/cli-reference/commands/services/quote.md +4 -4
- package/docs/cli-reference/commands/services/read-result.md +6 -17
- package/docs/cli-reference/commands/services/start.md +3 -5
- package/docs/cli-reference/commands/vault/access.md +1 -5
- package/docs/cli-reference/conventions.md +6 -4
- package/docs/cli-reference/index.md +1 -10
- package/docs/skill-bundle-rollout/01-mcp-authentication.md +134 -0
- package/docs/skill-bundle-rollout/02-platform-bundle-repositories.md +210 -0
- package/docs/skill-bundle-rollout/03-platform-publishing.md +161 -0
- package/docs/skill-bundle-rollout/04-first-wave-platforms.md +69 -0
- package/docs/skill-bundle-rollout/05-sync-operations.md +62 -0
- package/docs/skill-bundle-rollout/README.md +151 -0
- package/package.json +12 -20
- package/skills/itpay/SKILL.md +102 -110
- package/assets/sell-preview/index.html +0 -6
- package/assets/sell-preview/preview.js +0 -9
- package/bin/darwin-amd64/itpay-sell +0 -0
- package/bin/darwin-arm64/itpay-sell +0 -0
- package/bin/linux-amd64/itpay-sell +0 -0
- package/bin/linux-arm64/itpay-sell +0 -0
- package/bin/windows-amd64/itpay-sell.exe +0 -0
- package/dist/src/commands/aftercare.js +0 -12
- package/dist/src/commands/presentation.js +0 -153
- package/dist/src/commands/rail_catalog.js +0 -1
- package/dist/src/commands/services/actions.js +0 -133
- package/dist/src/commands/services/commerce.js +0 -197
- package/dist/src/commands/services/lifecycle.js +0 -418
- package/dist/src/commands/services/results.js +0 -130
- package/dist/src/render/browser.js +0 -65
- package/dist/src/render/checkout_handoff.js +0 -101
- package/dist/src/render/checkout_plan.js +0 -47
- package/dist/src/sell/commands.js +0 -80
- package/dist/src/sell/contract.js +0 -86
- package/dist/src/sell/local.js +0 -260
- package/dist/src/sell/mcp.js +0 -49
- package/dist/src/sell/preview.js +0 -70
- package/dist/src/sell/sync.js +0 -145
- package/dist/src/services/adapter.js +0 -1
- package/dist/src/services/presentation.js +0 -198
- package/dist/src/services/rail/actions.js +0 -51
- package/dist/src/services/rail/adapter.js +0 -27
- package/dist/src/services/rail/aftercare.js +0 -45
- package/dist/src/services/rail/catalog.js +0 -261
- package/dist/src/services/rail/checkout.js +0 -19
- package/dist/src/services/rail/fulfillment.js +0 -76
- package/dist/src/services/rail/guide.js +0 -110
- package/dist/src/services/rail/invocation.js +0 -54
- package/dist/src/services/rail/page.js +0 -76
- package/dist/src/services/rail/pagination.js +0 -6
- package/dist/src/services/rail/query.js +0 -71
- package/dist/src/services/rail/readers.js +0 -107
- package/dist/src/services/rail/reading.js +0 -125
- package/dist/src/services/rail/recovery.js +0 -51
- package/dist/src/services/rail/results.js +0 -378
- package/dist/src/services/rail/review.js +0 -268
- package/dist/src/services/rail/saved.js +0 -49
- package/dist/src/services/rail/wait.js +0 -11
- package/dist/src/services/registry.js +0 -31
- package/dist/src/services/schema.js +0 -21
- package/dist/src/services/shared/envelope.js +0 -184
- package/dist/src/services/shared/input.js +0 -39
- package/dist/src/services/shared/pagination.js +0 -28
- package/dist/src/services/shared/platform.js +0 -365
- package/dist/src/services/shared/refund.js +0 -40
- package/dist/src/services/shared/workflow.js +0 -28
- package/dist/src/state/account_auth.js +0 -347
- package/dist/src/state/task_journal.js +0 -90
- package/docs/agent/buyer/rail-booking.json +0 -176
- package/docs/agent/buyer/rail-fast-checkout.json +0 -48
- package/docs/cli-reference/commands/aftercare.md +0 -15
- package/docs/cli-reference/commands/auth.md +0 -96
- package/docs/cli-reference/commands/sell.md +0 -1364
- package/docs/cli-reference/commands/services/page.md +0 -71
- package/docs/cli-reference/commands/services/run.md +0 -152
- package/docs/sell.md +0 -84
- /package/bin/{itp.mjs → itp} +0 -0
|
@@ -1,347 +0,0 @@
|
|
|
1
|
-
import { HttpError } from '../client/http.js';
|
|
2
|
-
import { HttpTransportError } from '../client/transport.js';
|
|
3
|
-
import { DeviceAuthorizationError, DeviceStateError, DeviceLockBusyError } from './device_authority.js';
|
|
4
|
-
import { createHash } from 'node:crypto';
|
|
5
|
-
import { dirname, resolve } from 'node:path';
|
|
6
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, rmSync } from 'node:fs';
|
|
7
|
-
import { writeLocalPNG } from '../render/qr.js';
|
|
8
|
-
import { TaskJournal } from './task_journal.js';
|
|
9
|
-
import { taskJournalPath } from './config.js';
|
|
10
|
-
export function sellerAuthPath(baseURL, env = process.env, purpose = "seller") {
|
|
11
|
-
return resolve(stateDir(env), `${purpose}-${createHash('sha256').update(baseURL).digest('hex').slice(0, 16)}.json`);
|
|
12
|
-
}
|
|
13
|
-
function read(baseURL, env = process.env, purpose = "seller") {
|
|
14
|
-
const path = sellerAuthPath(baseURL, env, purpose);
|
|
15
|
-
if (!existsSync(path))
|
|
16
|
-
return;
|
|
17
|
-
const state = JSON.parse(readFileSync(path, 'utf8'));
|
|
18
|
-
if (state.baseURL !== baseURL)
|
|
19
|
-
throw new Error('Seller session backend mismatch');
|
|
20
|
-
return state;
|
|
21
|
-
}
|
|
22
|
-
function save(state, env = process.env, purpose = "seller") {
|
|
23
|
-
const path = sellerAuthPath(state.baseURL, env, purpose);
|
|
24
|
-
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
25
|
-
const tmp = `${path}.${process.pid}.tmp`;
|
|
26
|
-
writeFileSync(tmp, JSON.stringify(state), { mode: 0o600 });
|
|
27
|
-
renameSync(tmp, path);
|
|
28
|
-
}
|
|
29
|
-
export function sellerSessionToken(baseURL, env = process.env) {
|
|
30
|
-
const state = read(baseURL, env);
|
|
31
|
-
return state?.expiresAt && Date.parse(state.expiresAt) > Date.now() ? state.sessionToken : undefined;
|
|
32
|
-
}
|
|
33
|
-
export function sellerAuth(action, baseURL, env = process.env, fetcher = fetch) {
|
|
34
|
-
return accountAuth(action, baseURL, env, fetcher);
|
|
35
|
-
}
|
|
36
|
-
export function agentAuth(action, baseURL, backend, env = process.env, fetcher = fetch) {
|
|
37
|
-
return accountAuth(action, baseURL, env, fetcher, backend);
|
|
38
|
-
}
|
|
39
|
-
// AUTH01: dashboard session stages stay distinct — the human must know exactly
|
|
40
|
-
// what the official page still needs instead of a generic "pending".
|
|
41
|
-
const ACTIVE_SESSION_STAGES = ['created', 'waiting_provider', 'email_verification_required', 'merge_confirmation_required'];
|
|
42
|
-
function stageInstruction(stage) {
|
|
43
|
-
if (stage === 'email_verification_required') {
|
|
44
|
-
return '官方页返回显式或历史身份验证状态;按页面当前要求完成验证。普通登录只需一种有效身份,不要求补齐邮箱和手机。完成后恢复原执行。';
|
|
45
|
-
}
|
|
46
|
-
if (stage === 'merge_confirmation_required') {
|
|
47
|
-
return '官方页需要用户核对两侧账号证明及主账号,再明确确认或取消合并;这不是付款确认。不要替用户同意,不自动重启原服务或授权请求。';
|
|
48
|
-
}
|
|
49
|
-
return '用户仍在官方页面完成登录;保留当前 handoff,不要生成新二维码或新请求。';
|
|
50
|
-
}
|
|
51
|
-
// AUTH07: distinct outcomes stay distinct — a technical failure is never
|
|
52
|
-
// reported as a user denial, and a missing session is its own state.
|
|
53
|
-
const TERMINAL_SESSION_STATES = {
|
|
54
|
-
expired: {
|
|
55
|
-
status: 'auth_expired',
|
|
56
|
-
instruction: '授权请求已过期。重新运行 itpay auth login 生成新请求;之前的查询输入在服务端保留,可恢复。',
|
|
57
|
-
},
|
|
58
|
-
failed: {
|
|
59
|
-
status: 'auth_failed',
|
|
60
|
-
instruction: '官方授权因技术原因未完成(不是用户拒绝)。重新运行 itpay auth login 生成新请求。',
|
|
61
|
-
},
|
|
62
|
-
cancelled: {
|
|
63
|
-
status: 'auth_cancelled',
|
|
64
|
-
instruction: '用户在官方页取消了授权;停止登录。只有用户明确重新要求授权时才发起新的登录。',
|
|
65
|
-
},
|
|
66
|
-
denied: {
|
|
67
|
-
status: 'auth_denied',
|
|
68
|
-
instruction: '官方授权被拒绝;停止登录,不自动创建新请求。只有用户明确重新要求授权时才重新登录。',
|
|
69
|
-
},
|
|
70
|
-
};
|
|
71
|
-
class AuthRequestError extends Error {
|
|
72
|
-
httpStatus;
|
|
73
|
-
constructor(httpStatus, message) {
|
|
74
|
-
super(message);
|
|
75
|
-
this.httpStatus = httpStatus;
|
|
76
|
-
this.name = 'AuthRequestError';
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
// AUTH03: transport/read uncertainty never maps to "denied" or a new session —
|
|
80
|
-
// the stored session stays on disk and the caller retries the same poll.
|
|
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' };
|
|
92
|
-
return {
|
|
93
|
-
status: 'auth_status_unknown',
|
|
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,
|
|
99
|
-
recovery: [],
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
function sessionGone(env, purpose, baseURL, sessionID) {
|
|
103
|
-
rmSync(sellerAuthPath(baseURL, env, purpose), { force: true });
|
|
104
|
-
return {
|
|
105
|
-
status: 'auth_session_missing',
|
|
106
|
-
result: sessionID ? { dashboard_auth_session_id: sessionID } : {},
|
|
107
|
-
instruction: '之前的授权请求在服务端已不存在。重新运行 itpay auth login 生成新请求。',
|
|
108
|
-
next: { command: 'itpay auth login --json', reason: '重新发起官方授权' },
|
|
109
|
-
recovery: [],
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
// AUTH04: a successful bind resumes the SAME task that paused for login —
|
|
113
|
-
// from the local task journal, never a placeholder and never a guess at the
|
|
114
|
-
// latest order.
|
|
115
|
-
function authenticatedEnvelope(env, phoneVerified) {
|
|
116
|
-
const paused = new TaskJournal(taskJournalPath(env)).pausedTasks()
|
|
117
|
-
.filter((task) => task.stage === 'login_required' || task.stage === 'quota_paused');
|
|
118
|
-
const resumable = paused.filter((task) => typeof task.resume_command === 'string');
|
|
119
|
-
const result = { bound: true, phone_verified: phoneVerified };
|
|
120
|
-
let instruction = '登录与绑定已完成,告知用户可继续之前的查询;已注册账号查询不消耗试用次数,仍受正常限流。';
|
|
121
|
-
let next = null;
|
|
122
|
-
if (resumable.length === 1) {
|
|
123
|
-
next = { command: resumable[0].resume_command, reason: '恢复登录前暂停的同一查询' };
|
|
124
|
-
}
|
|
125
|
-
else if (resumable.length > 1) {
|
|
126
|
-
result.paused_executions = resumable.map((task) => ({
|
|
127
|
-
service_execution_id: task.service_execution_id,
|
|
128
|
-
...(task.service_id ? { service_id: task.service_id } : {}),
|
|
129
|
-
resume_command: task.resume_command,
|
|
130
|
-
}));
|
|
131
|
-
instruction += ' 有多个暂停的查询:只恢复与当前用户目标对应的那一个,不要一次恢复全部。';
|
|
132
|
-
}
|
|
133
|
-
else {
|
|
134
|
-
instruction += ' 当前没有待恢复的暂停查询;不构造恢复命令。';
|
|
135
|
-
}
|
|
136
|
-
return { status: 'authenticated', result, instruction, next, recovery: [] };
|
|
137
|
-
}
|
|
138
|
-
async function accountAuth(action, baseURL, env, fetcher, backend) {
|
|
139
|
-
const purpose = backend ? 'agent-login' : 'seller';
|
|
140
|
-
const command = backend ? 'itpay auth status' : 'itpay sell auth status';
|
|
141
|
-
if (backend) {
|
|
142
|
-
try {
|
|
143
|
-
const current = await backend.agentAccountStatus();
|
|
144
|
-
if (current.status === 'authenticated')
|
|
145
|
-
return authenticatedEnvelope(env, current.phone_verified === true);
|
|
146
|
-
}
|
|
147
|
-
catch (error) {
|
|
148
|
-
if (error instanceof AuthRequestError)
|
|
149
|
-
throw error;
|
|
150
|
-
// AUTH03: even the binding-status probe can hit transport failure; the
|
|
151
|
-
// truthful state is "unknown", never an implicit unbound or a fresh login.
|
|
152
|
-
return authStatusUnknown(read(baseURL, env, purpose)?.sessionID, error);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
async function request(path, init = {}) {
|
|
156
|
-
const response = await fetcher(baseURL + path, { ...init, redirect: 'error', signal: AbortSignal.timeout(15000) });
|
|
157
|
-
if (!response.ok)
|
|
158
|
-
throw new AuthRequestError(response.status, `ItPay authorization failed (${response.status}); retry login if expired`);
|
|
159
|
-
return response;
|
|
160
|
-
}
|
|
161
|
-
function pollOpenSession(state) {
|
|
162
|
-
return request(`/v1/dashboard/auth-sessions/${encodeURIComponent(state.sessionID)}?poll_token=${encodeURIComponent(state.pollToken)}`)
|
|
163
|
-
.then((response) => response.json());
|
|
164
|
-
}
|
|
165
|
-
async function bindSavedSession(state) {
|
|
166
|
-
const bound = await backend.bindAgentAccount({ dashboard_auth_session_id: state.sessionID, start_token: state.startToken });
|
|
167
|
-
if (bound.status !== 'authenticated')
|
|
168
|
-
throw new Error('Agent binding did not complete');
|
|
169
|
-
rmSync(sellerAuthPath(baseURL, env, purpose), { force: true });
|
|
170
|
-
return authenticatedEnvelope(env, bound.phone_verified === true);
|
|
171
|
-
}
|
|
172
|
-
if (action === 'login') {
|
|
173
|
-
if (backend) {
|
|
174
|
-
const open = read(baseURL, env, purpose);
|
|
175
|
-
if (open?.sessionID && open.pollToken && open.startToken) {
|
|
176
|
-
try {
|
|
177
|
-
const current = await pollOpenSession(open);
|
|
178
|
-
// AUTH02: a completed-but-unbound session binds in place; never
|
|
179
|
-
// create a second auth session for the same login.
|
|
180
|
-
if (current.status === 'completed')
|
|
181
|
-
return await bindSavedSession(open);
|
|
182
|
-
if (ACTIVE_SESSION_STAGES.includes(current.status)) {
|
|
183
|
-
const url = open.authURL ?? '';
|
|
184
|
-
const qr = url ? await writeLocalPNG(url).catch(() => undefined) : undefined;
|
|
185
|
-
return {
|
|
186
|
-
status: 'auth_pending',
|
|
187
|
-
result: {
|
|
188
|
-
dashboard_auth_session_id: open.sessionID,
|
|
189
|
-
stage: current.status,
|
|
190
|
-
expires_at: current.expires_at,
|
|
191
|
-
methods: ['phone', 'email', 'alipay', 'wechat'],
|
|
192
|
-
},
|
|
193
|
-
// SEC01: the handoff URL carries one-time credentials — it goes
|
|
194
|
-
// to the human only, never into logs, prompts, or PR material.
|
|
195
|
-
handoff: {
|
|
196
|
-
url,
|
|
197
|
-
contains_credentials: true,
|
|
198
|
-
...(qr ? { qr_local_path: qr.filePath, markdown: `` } : {}),
|
|
199
|
-
},
|
|
200
|
-
instruction: `${stageInstruction(current.status)} 沿用同一链接或二维码;不要生成新请求。`,
|
|
201
|
-
next: { command: 'itpay auth status --json', reason: '用户完成页面登录后确认绑定', poll_after_ms: 5000 },
|
|
202
|
-
recovery: [],
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
const terminal = TERMINAL_SESSION_STATES[current.status];
|
|
206
|
-
if (terminal) {
|
|
207
|
-
rmSync(sellerAuthPath(baseURL, env, purpose), { force: true });
|
|
208
|
-
// Fall through to a fresh session only after a definitive
|
|
209
|
-
// terminal state; anything else keeps the stored session.
|
|
210
|
-
}
|
|
211
|
-
else {
|
|
212
|
-
return authStatusUnknown(open.sessionID);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
catch (error) {
|
|
216
|
-
if (error instanceof AuthRequestError && (error.httpStatus === 404 || error.httpStatus === 410)) {
|
|
217
|
-
rmSync(sellerAuthPath(baseURL, env, purpose), { force: true });
|
|
218
|
-
// The explicit login call may open one fresh session now. Status
|
|
219
|
-
// remains read-only and still reports the missing old session.
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
return authStatusUnknown(open.sessionID, error);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
const response = await request('/v1/dashboard/auth-sessions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ return_to: backend ? '/' : '/seller' }) });
|
|
228
|
-
const result = await response.json();
|
|
229
|
-
const url = new URL(result.start_url, baseURL);
|
|
230
|
-
const sameOrigin = url.origin === new URL(baseURL).origin;
|
|
231
|
-
const alipay = url.origin === 'https://openauth.alipay.com' && url.pathname === '/oauth2/publicAppAuthorize.htm' && !url.username && !url.password;
|
|
232
|
-
const local = response.headers.get('X-ItPay-Environment') === 'local_simulation'
|
|
233
|
-
&& new URL(baseURL).protocol === 'http:' && new URL(baseURL).hostname === '127.0.0.1'
|
|
234
|
-
&& url.protocol === 'http:' && url.hostname === '127.0.0.1' && !url.username && !url.password;
|
|
235
|
-
if (!sameOrigin && !alipay && !local)
|
|
236
|
-
throw new Error('Unexpected authorization origin');
|
|
237
|
-
const fragment = new URLSearchParams(url.hash.replace(/^#dashboard-auth\?/, ''));
|
|
238
|
-
const state = url.searchParams.get('state')?.split('.');
|
|
239
|
-
const startToken = alipay
|
|
240
|
-
? (state?.length === 2 && state[0] === result.dashboard_auth_session_id ? state[1] : undefined)
|
|
241
|
-
: url.searchParams.get('start_token') || fragment.get('start_token');
|
|
242
|
-
if (!startToken || !result.poll_token || !result.dashboard_auth_session_id)
|
|
243
|
-
throw new Error('Incomplete authorization response');
|
|
244
|
-
save({ baseURL, sessionID: result.dashboard_auth_session_id, pollToken: result.poll_token, startToken, authURL: url.href }, env, purpose);
|
|
245
|
-
if (backend) {
|
|
246
|
-
const qr = await writeLocalPNG(url.href).catch(() => undefined);
|
|
247
|
-
return {
|
|
248
|
-
status: 'auth_pending',
|
|
249
|
-
result: {
|
|
250
|
-
dashboard_auth_session_id: result.dashboard_auth_session_id,
|
|
251
|
-
stage: 'created',
|
|
252
|
-
expires_at: result.expires_at,
|
|
253
|
-
methods: ['phone', 'email', 'alipay', 'wechat'],
|
|
254
|
-
},
|
|
255
|
-
handoff: {
|
|
256
|
-
url: url.href,
|
|
257
|
-
contains_credentials: true,
|
|
258
|
-
...(qr ? { qr_local_path: qr.filePath, markdown: `` } : {}),
|
|
259
|
-
},
|
|
260
|
-
instruction: '把官方授权页或二维码交给用户。用户在页面内选择手机号验证码、邮箱或钱包完成登录;不要替用户输入手机号或验证码,start_token 不要写入聊天记录或日志。',
|
|
261
|
-
next: { command: 'itpay auth status --json', reason: '用户完成页面登录后确认绑定', poll_after_ms: 5000 },
|
|
262
|
-
recovery: [],
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
return { status: 'authorization_required', authorization_url: url.href, instruction: `Complete ItPay login in the browser, then run ${command}.` };
|
|
266
|
-
}
|
|
267
|
-
const state = read(baseURL, env, purpose);
|
|
268
|
-
if (action === 'logout') {
|
|
269
|
-
const token = sellerSessionToken(baseURL, env);
|
|
270
|
-
if (token)
|
|
271
|
-
await request('/v1/me/logout', { method: 'POST', headers: { Authorization: `Bearer ${token}` } });
|
|
272
|
-
rmSync(sellerAuthPath(baseURL, env), { force: true });
|
|
273
|
-
return { status: 'logged_out' };
|
|
274
|
-
}
|
|
275
|
-
if (!backend && sellerSessionToken(baseURL, env))
|
|
276
|
-
return { status: 'authenticated', base_url: baseURL, expires_at: state?.expiresAt };
|
|
277
|
-
if (!state?.sessionID || !state.pollToken || !state.startToken) {
|
|
278
|
-
// AUTH07: a bare `status` with no session is its own truthful state, not a
|
|
279
|
-
// failure and not an implicit login.
|
|
280
|
-
if (!backend)
|
|
281
|
-
return { status: 'login_required' };
|
|
282
|
-
return {
|
|
283
|
-
status: 'login_required',
|
|
284
|
-
result: {},
|
|
285
|
-
instruction: '当前设备尚未绑定账号,也没有进行中的授权请求。需要时运行 itpay auth login 发起官方登录。',
|
|
286
|
-
next: { command: 'itpay auth login --json', reason: '需要账号绑定时发起官方授权' },
|
|
287
|
-
recovery: [],
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
let progress;
|
|
291
|
-
try {
|
|
292
|
-
progress = await pollOpenSession(state);
|
|
293
|
-
}
|
|
294
|
-
catch (error) {
|
|
295
|
-
if (error instanceof AuthRequestError && (error.httpStatus === 404 || error.httpStatus === 410)) {
|
|
296
|
-
return sessionGone(env, purpose, baseURL, state.sessionID);
|
|
297
|
-
}
|
|
298
|
-
return authStatusUnknown(state.sessionID, error);
|
|
299
|
-
}
|
|
300
|
-
if (progress.status !== 'completed') {
|
|
301
|
-
if (backend) {
|
|
302
|
-
const terminal = TERMINAL_SESSION_STATES[progress.status];
|
|
303
|
-
if (terminal) {
|
|
304
|
-
rmSync(sellerAuthPath(baseURL, env, purpose), { force: true });
|
|
305
|
-
return {
|
|
306
|
-
status: terminal.status,
|
|
307
|
-
result: { dashboard_auth_session_id: state.sessionID, stage: progress.status },
|
|
308
|
-
instruction: terminal.instruction,
|
|
309
|
-
next: ['denied', 'cancelled'].includes(progress.status) ? null : { command: 'itpay auth login --json', reason: '需要登录时重新发起官方授权' },
|
|
310
|
-
recovery: [],
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
return {
|
|
314
|
-
status: 'auth_pending',
|
|
315
|
-
result: {
|
|
316
|
-
dashboard_auth_session_id: state.sessionID,
|
|
317
|
-
stage: progress.status,
|
|
318
|
-
...(progress.expires_at ? { expires_at: progress.expires_at } : {}),
|
|
319
|
-
},
|
|
320
|
-
instruction: stageInstruction(progress.status),
|
|
321
|
-
next: { command: 'itpay auth status --json', reason: '轮询同一授权请求', poll_after_ms: 5000 },
|
|
322
|
-
recovery: [],
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
return { status: progress.status, instruction: 'Finish login and email verification in the browser.' };
|
|
326
|
-
}
|
|
327
|
-
if (backend) {
|
|
328
|
-
try {
|
|
329
|
-
return await bindSavedSession(state);
|
|
330
|
-
}
|
|
331
|
-
catch (error) {
|
|
332
|
-
if (error instanceof AuthRequestError)
|
|
333
|
-
throw error;
|
|
334
|
-
// Binding outcome unknown: keep the session so a retry can still bind
|
|
335
|
-
// the same completed login instead of creating a new one.
|
|
336
|
-
return authStatusUnknown(state.sessionID, error);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
const claimed = await request(`/v1/dashboard/auth-sessions/${encodeURIComponent(state.sessionID)}/claim?start_token=${encodeURIComponent(state.startToken)}`, { method: 'POST' });
|
|
340
|
-
const token = /(?:^|[, ]+)itpay_buyer_session=([^;]+)/.exec(claimed.headers.get('set-cookie') ?? '')?.[1];
|
|
341
|
-
const session = await claimed.json();
|
|
342
|
-
if (!token || !session.expires_at || !(Date.parse(session.expires_at) > Date.now()))
|
|
343
|
-
throw new Error('Incomplete Seller session');
|
|
344
|
-
save({ baseURL, sessionToken: token, expiresAt: session.expires_at }, env);
|
|
345
|
-
return { status: 'authenticated', base_url: baseURL, expires_at: session.expires_at };
|
|
346
|
-
}
|
|
347
|
-
import { stateDir } from "./config.js";
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
// Minimal local task journal: which Service Executions are paused at a human
|
|
2
|
-
// gate (login, rate limit) and how to resume the SAME execution. It stores
|
|
3
|
-
// no chat history, PII, credentials or tokens — only ids, stage and the
|
|
4
|
-
// resume command the owner flow already emitted. Business truth stays
|
|
5
|
-
// server-side; this file only makes "continue" point at the right task.
|
|
6
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { dirname } from "node:path";
|
|
8
|
-
export const PAUSED_STAGES = new Set([
|
|
9
|
-
"login_required",
|
|
10
|
-
"rate_limited",
|
|
11
|
-
"quota_paused",
|
|
12
|
-
"auth_pending",
|
|
13
|
-
"awaiting_input",
|
|
14
|
-
]);
|
|
15
|
-
const TERMINAL_STAGES = new Set([
|
|
16
|
-
"completed",
|
|
17
|
-
"delivered",
|
|
18
|
-
"issued",
|
|
19
|
-
"failed",
|
|
20
|
-
"cancelled",
|
|
21
|
-
"refunded",
|
|
22
|
-
]);
|
|
23
|
-
export class TaskJournal {
|
|
24
|
-
path;
|
|
25
|
-
constructor(path) {
|
|
26
|
-
this.path = path;
|
|
27
|
-
}
|
|
28
|
-
record(task) {
|
|
29
|
-
const file = this.readFile();
|
|
30
|
-
file.tasks[task.service_execution_id] = {
|
|
31
|
-
...task,
|
|
32
|
-
updated_at: new Date().toISOString(),
|
|
33
|
-
};
|
|
34
|
-
this.writeFile(file);
|
|
35
|
-
}
|
|
36
|
-
clear(serviceExecutionID) {
|
|
37
|
-
const file = this.readFile();
|
|
38
|
-
if (!file.tasks[serviceExecutionID])
|
|
39
|
-
return;
|
|
40
|
-
delete file.tasks[serviceExecutionID];
|
|
41
|
-
this.writeFile(file);
|
|
42
|
-
}
|
|
43
|
-
// Observe the latest stage without deleting history; terminal stages stop
|
|
44
|
-
// the task from counting as resumable.
|
|
45
|
-
observe(serviceExecutionID, stage, resumeCommand) {
|
|
46
|
-
const file = this.readFile();
|
|
47
|
-
const existing = file.tasks[serviceExecutionID];
|
|
48
|
-
if (existing && existing.stage === stage && (resumeCommand === undefined || existing.resume_command === resumeCommand))
|
|
49
|
-
return;
|
|
50
|
-
file.tasks[serviceExecutionID] = {
|
|
51
|
-
service_execution_id: serviceExecutionID,
|
|
52
|
-
...(existing?.service_id ? { service_id: existing.service_id } : {}),
|
|
53
|
-
stage,
|
|
54
|
-
...(resumeCommand ?? existing?.resume_command ? { resume_command: resumeCommand ?? existing.resume_command } : {}),
|
|
55
|
-
updated_at: new Date().toISOString(),
|
|
56
|
-
};
|
|
57
|
-
this.writeFile(file);
|
|
58
|
-
}
|
|
59
|
-
// Tasks paused behind login/quota gates, newest first. Terminal or
|
|
60
|
-
// completed tasks never count as "still waiting for login".
|
|
61
|
-
pausedTasks() {
|
|
62
|
-
const file = this.readFile();
|
|
63
|
-
return Object.values(file.tasks)
|
|
64
|
-
.filter((task) => PAUSED_STAGES.has(task.stage) && !TERMINAL_STAGES.has(task.stage))
|
|
65
|
-
.sort((left, right) => right.updated_at.localeCompare(left.updated_at));
|
|
66
|
-
}
|
|
67
|
-
readFile() {
|
|
68
|
-
if (!existsSync(this.path))
|
|
69
|
-
return { schema_version: "itpay.task_journal.v1", tasks: {} };
|
|
70
|
-
try {
|
|
71
|
-
const parsed = JSON.parse(readFileSync(this.path, "utf8"));
|
|
72
|
-
if (parsed.schema_version !== "itpay.task_journal.v1" || !parsed.tasks || typeof parsed.tasks !== "object") {
|
|
73
|
-
return { schema_version: "itpay.task_journal.v1", tasks: {} };
|
|
74
|
-
}
|
|
75
|
-
return parsed;
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
return { schema_version: "itpay.task_journal.v1", tasks: {} };
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
writeFile(file) {
|
|
82
|
-
// Bound the journal: keep only the 50 most recent task records.
|
|
83
|
-
const entries = Object.entries(file.tasks).sort((left, right) => right[1].updated_at.localeCompare(left[1].updated_at));
|
|
84
|
-
file.tasks = Object.fromEntries(entries.slice(0, 50));
|
|
85
|
-
mkdirSync(dirname(this.path), { recursive: true, mode: 0o700 });
|
|
86
|
-
const tmp = `${this.path}.${process.pid}.tmp`;
|
|
87
|
-
writeFileSync(tmp, JSON.stringify(file), { mode: 0o600 });
|
|
88
|
-
renameSync(tmp, this.path);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"schema_version": "itp.agent_doc.v1",
|
|
3
|
-
"role": "buyer",
|
|
4
|
-
"product_scope": "itpay is the single public CLI entry point, and $itpay is its user-facing Skill invocation. Under that one product entry point, the two top-level commerce actions are buy and sell: Buyer workflows are available now; Seller workflows use the same entry point via itpay sell.",
|
|
5
|
-
"topic": "rail-booking",
|
|
6
|
-
"title": "Railway Query, Planning, and Ticket Booking",
|
|
7
|
-
"purpose": "One railway route from understanding the request to saved results, booking, protected checkout, order status and refunds.",
|
|
8
|
-
"when_to_use": [
|
|
9
|
-
"Any railway schedule, planning or ticket request; read this topic once before the first railway action."
|
|
10
|
-
],
|
|
11
|
-
"goal_and_overview": {
|
|
12
|
-
"route": "Understand endpoints and constraints → 2–4 line route brief → Exact or Smart → read enough saved results for the goal → select under delegation → missing real consent only → official payment entry; show and stop.",
|
|
13
|
-
"query_vs_booking": "Exact/Smart returns a query execution and saved evidence. Booking creates a separate execution from a server-issued selection; it is not a repeated search.",
|
|
14
|
-
"comparison": "If the human only asked to compare, answer within the searched scope and stop without a buy-or-not question. Reuse already-read unchanged Skill/docs in this context; do not repeat install/readyz/docs at each step. A clear purchase delegation permits selection under their rules, subject to required human terms.",
|
|
15
|
-
"public_entry": "One public service itpay-rail declares query_exact, query_smart and prepare_purchase. Query, booking, Checkout and order remain separate identities. Legacy IDs are only for recovering their pinned historical executions.",
|
|
16
|
-
"price_approval": "Choosing a candidate or confirming seat/whole-journey terms is not approving price. Read the current scoped approve_purchase template after quote; record true only for genuine matching exact-price or capped-budget approval, or an explicitly authorized official-page confirmation. Reuse matching approval; never fabricate it."
|
|
17
|
-
},
|
|
18
|
-
"flow_at_a_glance": [
|
|
19
|
-
{
|
|
20
|
-
"step": "search Exact",
|
|
21
|
-
"command": "itpay services run itpay-rail --operation query_exact --input-json <query-file> --json",
|
|
22
|
-
"returns": "one station-pair query execution, resolved scope and either useful results or a same-execution wait action",
|
|
23
|
-
"next": "follow the current next command when still working; compare results and stop if the request is satisfied"
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
"step": "search Smart",
|
|
27
|
-
"command": "itpay services run itpay-rail --operation query_smart --input-json <query-file> --json",
|
|
28
|
-
"returns": "one broader planning execution, observed coverage and either useful results or a same-execution wait action",
|
|
29
|
-
"next": "follow the current next command when still working; compare results and stop if the request is satisfied"
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
"step": "read Exact result",
|
|
33
|
-
"command": "itpay services read-result <query_execution_id> --json",
|
|
34
|
-
"returns": "saved station-pair rows with detail.command; detail returns booking_template",
|
|
35
|
-
"next": "Read enough candidates for the user objective. Only a purchase delegation permits using the chosen template."
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"step": "read Smart result",
|
|
39
|
-
"command": "itpay services read-result <query_execution_id> --snapshot <snapshot_id> --json",
|
|
40
|
-
"returns": "same-snapshot journeys and detail.command. Single-leg detail returns a selection action: use human only for a real user pick, agent for delegated selection; its result supplies booking credentials. Transfer detail returns the complete ordered selections template for one ticket plan; do not turn it into separate purchases.",
|
|
41
|
-
"next": "If current eligible options and full connections satisfy the request, answer or continue under delegation. Selecting a journey does not lock inventory; preserve whole-journey purchase. No unsupported ground distance/time estimates."
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
"step": "book",
|
|
45
|
-
"command": "itpay services run itpay-rail --operation prepare_purchase --input-json <file> --json",
|
|
46
|
-
"returns": "a separate booking execution using the returned selection token, seat code and party size",
|
|
47
|
-
"next": "follow its review or waiting action on that same booking execution"
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
"step": "review",
|
|
51
|
-
"command": "itpay services next <booking_execution_id> --json",
|
|
52
|
-
"returns": "current review revision, material terms and a non-executable confirm_booking input_template",
|
|
53
|
-
"next": "submit the returned actor-type agent template only after genuine consent; reuse an unchanged explicit consent for code-only correction"
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
"step": "checkout",
|
|
57
|
-
"command": "use the returned next or handoff for the same booking execution",
|
|
58
|
-
"returns": "an official protected payment entry",
|
|
59
|
-
"next": "show the official entry using the current host immediately before optional memory/log writing, then stop; retain necessary safety/permissions, do not infer paid or issued"
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
"step": "aftercare",
|
|
63
|
-
"command": "itpay order <order_id> --json",
|
|
64
|
-
"returns": "authoritative payment and ticket state",
|
|
65
|
-
"next": "use orders-refunds for the same order when needed"
|
|
66
|
-
}
|
|
67
|
-
],
|
|
68
|
-
"understand_before_search": [
|
|
69
|
-
"Preserve full administrative names, original endpoint meaning, date, party size, deadlines and station locks. Ask only for a missing date or genuine ambiguity changing the destination/constraints; no preference means do not pre-ask station, seat or departure time.",
|
|
70
|
-
"Before the first railway query, leave one 2–4 line conclusion brief in conversation or existing route-brief.txt (choose one, no Backend upload or private reasoning): Origin: full original meaning; station/area/place; evidence from user/context/reliable knowledge/actual lookup; unknown facts. Destination: the same. Journey: date, given party size/deadlines; credible station pair or transfer direction and evidence; Exact/Smart choice and one-sentence reason. Never replace this with “understood”, or write it after querying as pre-query evidence. Reuse it while waiting, reading, quoting or buying; update only changed endpoints/scope/date. With existing results, serve the facts rather than restart for a missing ceremony; missing prior evidence remains unobserved.",
|
|
71
|
-
"Only clear reliable route knowledge can directly support a candidate. A merely possible/probable transfer or “a long trip should transfer” is unknown: first make one targeted lookup on 12306 or 同程, with 携程 only if needed. Stop once a credible direct station pair or transfer direction is established; do not collect complete external timetables, prices or inventory. Remembered trains still require ItPay to verify the actual date and stock.",
|
|
72
|
-
"12306 https://kyfw.12306.cn/ is the official date/station/train reference; claim a date verified only after reading that date result. 同程 https://www.ly.com/mergeTrain/huochepiao helps identify station pairs and transfer directions. 携程 https://trains.ctrip.com/ is an optional supplement for transfer routes. Knowledge, search snippets and historical timetables do not prove current operation, inventory, price or purchase credentials.",
|
|
73
|
-
"If login, captcha or network blocks the short lookup, stop without repeated troubleshooting or browser installation and state the facts still unknown. Clear double-station endpoints can use Exact to verify direct trains; do not choose Smart merely because a transfer seems probable. A known transfer requirement or supported multi-station/place comparison can use Smart; genuinely unclear place meaning needs human clarification.",
|
|
74
|
-
"Date verification is required; the ItPay query for the actual date can perform it. A 12306 visit is not a mandatory extra call for every order. Valid Smart journeys and credentials do not require routine Exact rechecks of each leg.",
|
|
75
|
-
"Each side independently means area, place or station, giving all nine combinations. An area is not a same-name station; a specific station stays locked until the human consents to changing it. Smart also supports two locked stations when transfers are needed.",
|
|
76
|
-
"Distance alone does not establish a transfer requirement. One observed route through a station does not make it mandatory; a failed short lookup or incomplete search cannot establish that all direct trains are absent.",
|
|
77
|
-
"Unclear meaning that changes region/scope needs one key question, using known context first. Clear meaning with unknown coordinates can use the complete name and mark location verification pending. Use maps/browser for place identity; 12306/同程 for rail directions, 携程 as needed. Do not require a residential location to be researched on 12306. No fabricated coordinates or passenger/payment data in notes."
|
|
78
|
-
],
|
|
79
|
-
"choose_search": {
|
|
80
|
-
"exact": "Known or credible resolved station pair, direct trains for one station pair and date only. A zero result does not mean the whole city has no train. An area delegation may permit checking another evidence-based pair within that original area; a human-specified station stays locked until they consent to changing that endpoint.",
|
|
81
|
-
"smart": "Use Smart for a reliably established transfer direction or explicit transfer need, broad station comparison, or a precise place needing nearby stations and ground evidence. Preserve each endpoint as area/place/station; a probable transfer alone is not a basis for Smart. Fastest does not automatically mean max_transfers=0.",
|
|
82
|
-
"scope": "Exact cannot claim a citywide or all-transfer optimum. Smart gives its observed scope and missing evidence; its first useful options may be shown before expansion finishes."
|
|
83
|
-
},
|
|
84
|
-
"input_and_expected_result": {
|
|
85
|
-
"exact": {
|
|
86
|
-
"command": "itpay services run itpay-rail --operation query_exact --input-json <query-file> --json",
|
|
87
|
-
"example": {
|
|
88
|
-
"input": {
|
|
89
|
-
"origin": "<已核实出发站>",
|
|
90
|
-
"destination": "<已核实到达站>",
|
|
91
|
-
"travel_date": "<用户出行日期 YYYY-MM-DD>"
|
|
92
|
-
}
|
|
93
|
-
},
|
|
94
|
-
"contract": "Only origin, destination and travel_date are accepted. Use station names, not an undocumented station-code field. Filter time, seat and quantity against returned rows. Date is illustrative; use the actual requested queryable date."
|
|
95
|
-
},
|
|
96
|
-
"smart": {
|
|
97
|
-
"command": "itpay services run itpay-rail --operation query_smart --input-json <query-file> --json",
|
|
98
|
-
"example": {
|
|
99
|
-
"input": {
|
|
100
|
-
"endpoints": {
|
|
101
|
-
"origin": {
|
|
102
|
-
"kind": "area",
|
|
103
|
-
"text": "<用户出发市县镇完整名称>"
|
|
104
|
-
},
|
|
105
|
-
"destination": {
|
|
106
|
-
"kind": "station",
|
|
107
|
-
"text": "<已核实到达站名>"
|
|
108
|
-
}
|
|
109
|
-
},
|
|
110
|
-
"travel_date": "<用户出行日期 YYYY-MM-DD>"
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
"contract": "The current published input_schema is authoritative: use endpoints only when it exposes endpoints. Each side independently accepts kind area/place/station and a full text; fill kind when supported by endpoint understanding; omit it only when the object meaning is clear but its category genuinely awaits verification, explicitly recorded in the brief. An unknown city is not resolved by omitting kind. A complete named place is legal; coordinates are optional. Area centers choose nearby stations without claiming door-to-door ground travel; place uses verified POI/coordinates when available; station locks exactly one verified railway station and does not mean the traveler is already there. All nine origin/destination combinations follow this same rule. Province-only areas need a narrower location. Never mix endpoints with legacy origin/destination or send a station lock/confirmed flag. Adult passengers is an integer 1–5. arrive_before maps to arrive_by; conflicting aliases are rejected. preference_context is unsupported."
|
|
114
|
-
},
|
|
115
|
-
"result": "Read the resolved station pair or area scope, observed coverage, complete compact train rows, fare/inventory evidence and stable references. The recommendation card is not the full catalog. Unknown cost and unverified address ground facts remain unknown. Eligible means usable, not equally preferable. Prefer existing default_layer, roles, dominance and tradeoffs; show at most three meaningful representatives. A same-arrival, earlier-departing and more expensive dominated option stays in the saved catalog rather than taking the first screen.",
|
|
116
|
-
"waiting": "Use the same execution and returned bounded wait command. next is a read; background work may still proceed. Do not resubmit, sleep/get/events repeatedly, or reread the whole guide.",
|
|
117
|
-
"followup": "Read the saved result for the original execution. Smart: itpay services read-result <execution_id> --snapshot <snapshot_id> --json. Exact: use its returned saved-result reader. Read the relevant same-snapshot pages first; unread does not mean nonexistent. Do not automatically enumerate every theoretical combination. For a new Smart query, restore the original area/place/station intent, passengers, deadlines and preferences; an Exact pair is verified evidence, not proof of user station locks. Only a user-locked station change or scope expansion needs new consent. After a changed endpoint or date, supply the original intent and a same-owner reuse_from reference to an unexpired snapshot; unchanged resolved endpoints and matching fresh station-pair facts may be reused. Date change never reuses old inventory. Preference, party-size or deadline changes within the same scope use refine_preferences on the existing plan. Planning deadline does not erase saved facts; an old selection token is not refreshed by rereading. For expiry, use only the server safe fresh-query template once within the original authorization, preserving all conditions and omitting reuse_from; it may consume query quota. Existing or unknown purchases resume the same booking/order, never new queries. Refresh does not guarantee inventory or original price; confirm materially changed terms. Change to a named station: kind station with matching text/code. Original-station vicinity: kind place with verified station text, remove station_code, resolve normally or use verified POI/coordinates (existing 30 km/5 candidates). Whole city only with explicit consent: kind area with full administrative name, remove POI/location/station_code. Preserve date, passengers, time limits and preferences; refuse cross-owner reuse. The public Smart input does not expose overnight layover. A home or specific address remains the destination; nearby railway stations are candidates, never implicit station locks. Report the rail arrival station separately from any unknown last-mile connection. Preserve the endpoint brief; update only affected facts. A terminal location failure follows its explicit complete new-query template only after new evidence.",
|
|
118
|
-
"public_envelope": "New public operation files contain input plus, for prepare_purchase, the exact continuation supplied by saved result detail. Do not invent source execution/snapshot IDs. Historical executions use their own pinned schema."
|
|
119
|
-
},
|
|
120
|
-
"select_and_book": {
|
|
121
|
-
"delegation": "Apply existing seat/price/delegation rules. Exact fastest requires enough coverage to compare the station-pair candidates; without ordering/coverage evidence the first page is not proof. Smart can present up to three eligible representatives once full connections meet the goal, or continue under delegation; no routine full-catalog or all-seat paging. If an unapproved seat choice changes the price, combine that decision and missing terms into one question. Do not default to second class or require a three-tier menu. No match does not permit loosening conditions.",
|
|
122
|
-
"input": "Copy the complete current booking_template.input_example; retain passenger count, every ordered token and the returned seat_type code matched to seat_name. Never use a seat display name as the code, mix selections/selection/legs or guess missing credentials. Technical JSON errors are repaired by the Agent using current input_schema/input_errors/template, preserving original user requirements; ask the human only for a genuinely missing business choice.",
|
|
123
|
-
"command": "itpay services run itpay-rail --operation prepare_purchase --input-json <file> --json",
|
|
124
|
-
"identity": "Passenger name, ID and phone are entered on the official protected checkout page, not in query or booking JSON.",
|
|
125
|
-
"whole_journey": "Use the returned complete ticket plan booking_template: ordered selections (2–3 legs) from one owner/snapshot/revision/plan, or selection for one leg. One booking, one refreshed whole quote, one checkout, one order and one payment; each leg issues separately without connection or joint-seat guarantees. Raw legs are single-leg compatibility only; never hand-compose multi-leg legs when complete selections are missing or expired. Do not combine different plans or choose per-leg payment by default. Historical details without a whole-journey template do not prove whole-journey support."
|
|
126
|
-
},
|
|
127
|
-
"review_and_checkout": {
|
|
128
|
-
"review": "Copy the returned complete interaction.input_template.input, current revision and notice. For a whole journey, each ordered leg has its own seat_type and an object preference per passenger; default auto, never null/string arrays. Use known choices and confirm only missing genuine terms consent or material changes. Set accept_non_guaranteed=true only after real consent; delegation or choosing a train alone is not terms consent. The false default is protection, not a broken template; if consent is missing, ask the returned review.consent_question once. Seat/berth placement requests differ from the whole-journey separate-issuance and partial-outcome terms; use the current notice.",
|
|
129
|
-
"equivalent_correction": "If only seat code spelling changes while train, actual seat class, party, price and terms are identical, reuse an existing explicit human approval. Material draft changes or missing approval require human confirmation. Reuse accepted original endpoints, no-seat, overnight and incomplete-coverage facts when unchanged; never silently substitute the destination.",
|
|
130
|
-
"checkout": "Present the complete itinerary, total and single official handoff: one payment, separate issuance. The human enters one passenger set on that page; verified account notification email is reused there and can be changed for this order. Do not ask or guess traveler identity/email in chat. Use the returned checkout payment deadline, not an estimated duration. Handoff is not payment, and payment is not all legs issued."
|
|
131
|
-
},
|
|
132
|
-
"continue_and_aftercare": {
|
|
133
|
-
"same_execution": "Read the same booking/checkout/order. Report issued only when every leg is confirmed issued; partial or unknown outcomes keep the same order and successful tickets, reconcile before retry, and explain the affected leg. No automatic rebuy or full-refund promise.",
|
|
134
|
-
"saved_vs_live": "A saved snapshot remains historical evidence after expiry. Real-time booking or quote credentials may expire and require the documented fresh validation path; rereading an old token does not renew it.",
|
|
135
|
-
"refund": "For railway refund/change use itpay aftercare list --order <order_id> --json. Select returned leg/passenger targets from the user’s request, explain scope and manual processing, submit the complete current template. Request acceptance is not supplier success or money returned. Read the same case; concrete change proposals need current-revision consent and actual fee facts. No automatic change payment or replacement purchase."
|
|
136
|
-
},
|
|
137
|
-
"exceptional_branches": {
|
|
138
|
-
"login_required": "Use itpay auth login --json, complete the official handoff, then execute the returned recovery command for the same paused query. Login returning a command does not execute it.",
|
|
139
|
-
"rate_limited": "Respect returned retry_after and continue the same execution.",
|
|
140
|
-
"booking_review_invalid": "Read the same booking review and use the returned seat code; reuse real consent only under the strict equivalent-correction rule.",
|
|
141
|
-
"booking_review_changed": "Read the current revision and compare material terms; confirm new terms only when changed.",
|
|
142
|
-
"selection_stale": "Saved result stays readable, but follow the current owner action for new live inventory and quote evidence; do not assume reread refreshes the token.",
|
|
143
|
-
"unknown_paid_outcome": "Read the same checkout/order before any mutation. Never repeat payment or create a new order on an unknown outcome.",
|
|
144
|
-
"lock": "Use the CLI returned device repair-lock recovery only for a lock proven stale; preserve active owners and identity.",
|
|
145
|
-
"empty": "Report verified scope, failures and missing evidence separately. Do not guess a new date or station as a generic repair. Explain checked scope first, then propose at most two evidence-backed directions. Ask consent before changing a locked endpoint; after consent copy the complete original input and change only the accepted side in a new query.",
|
|
146
|
-
"ground_evidence_required": "If trains were found but a precise address deadline cannot be verified without ground or station-exit evidence, report the checked rail scope and missing fact. Ask for a precise location or verifiable connection fact; do not expand rail station pairs by default or call it no trains. Preserve the original place and compare its nearby candidates; only actual user consent may replace the destination with a station.",
|
|
147
|
-
"dependency_unavailable": "Map faults are external failures, not empty location candidates. Preserve resolved endpoints and original conditions. For a retryable wait, respect dependency.retry_at and execute the returned same-execution run command; never restart or sleep inside the CLI. Daily quota, permission or IP failures need service maintenance, not a different address.",
|
|
148
|
-
"partial": "Report saved usable routes together with checked scope and the precise gap. Unqueried hubs, geographic evidence gaps and budget limits are not proof of no trains. Use the returned action only; do not expand station pairs by default. A proposal to relax a condition is allowed, but requires real consent before a changed-scope query.",
|
|
149
|
-
"geographic_notice": "Geographic deviation is a ranking or explanation notice. Current eligibility and booking conditions come from verified dates, connections, inventory, prices, authorization and required ground evidence; do not reject a returned eligible train merely by geometric ratio.",
|
|
150
|
-
"selection_invalid": "Use only the current finite reason and executable recovery. An input constraint error requires the current complete template; stored_selection_inconsistent is a server problem that extra parameters cannot repair. With no recovery, stop: never append hidden fields, recreate a purchase or blame local simulation without evidence.",
|
|
151
|
-
"location_evidence_required": "A unique verified place continues directly; real multiple candidates use only the returned choice action and preserve the original place. Area-only or conflicting identity evidence is not proof the place does not exist: read affected_endpoint, original place, limited resolved_scope and finite reason; use maps/browser for missing evidence or ask only necessary human facts. A terminated query cannot resume: only after new evidence, fill the complete non-executable new_query_template and create once, preserving the other side/date/party/preferences/locks. Do not switch place to area, fabricate reuse_from or repeatedly get a terminal execution. Only a real dependency wait resumes the same execution. Correcting a proven classification within unchanged intent needs no repeated consent; changing destination or enlarging scope needs real consent."
|
|
152
|
-
},
|
|
153
|
-
"search_terms": [
|
|
154
|
-
"rail",
|
|
155
|
-
"railway",
|
|
156
|
-
"train",
|
|
157
|
-
"ticket",
|
|
158
|
-
"火车",
|
|
159
|
-
"火车票",
|
|
160
|
-
"高铁",
|
|
161
|
-
"车票",
|
|
162
|
-
"车次",
|
|
163
|
-
"seat",
|
|
164
|
-
"booking",
|
|
165
|
-
"rail-booking"
|
|
166
|
-
],
|
|
167
|
-
"reading_and_writing": "Use the formal Skill Follow one envelope rules for direct text/file reading, truncation and small JSON input; no parsing scripts.",
|
|
168
|
-
"restore_old_execution": "Only when resuming an existing v4 execution, follow its own declared legacy schema; new queries use the current three-endpoint contract.",
|
|
169
|
-
"saved_followup": "Use returned refine_preferences with expected_query_revision for arrival/departure times, passengers, seat preference or priority; rerank uses existing facts. Only endpoints/date/new scope require a new query. Profiles compare within one journey, not the whole catalog. Fares and service fees are whole-journey totals for the stated people; observed available is not proof of ample inventory. Page limit is 1-20; follow next_offset exactly, never infer full-catalog optimality from the visible page. Byte-limited pages may contain fewer rows than limit. Area station candidates are not guaranteed nearest; transfer wait/minimum are facts, not a low-risk guarantee.",
|
|
170
|
-
"actor_conventions": {
|
|
171
|
-
"location_confirmation": "Use the returned human template only for the user's real place choice; no delegated human action.",
|
|
172
|
-
"journey_terms": "workflow:confirm_booking and booking_review_invalid recovery accept agent-reported genuine consent; default false, never infer from purchase delegation.",
|
|
173
|
-
"ordinary_human_action": "Use its published actor rule; do not generalize railway permission.",
|
|
174
|
-
"price_approval": "approve_purchase uses actor-type agent with the unchanged server-scoped approval template and genuine matching approval; candidate choice is not price approval."
|
|
175
|
-
}
|
|
176
|
-
}
|