@itpay/cli 2.1.41 → 2.1.44

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.
Files changed (148) hide show
  1. package/README.md +12 -5
  2. package/assets/sell-preview/index.html +6 -0
  3. package/assets/sell-preview/preview.js +9 -0
  4. package/bin/darwin-amd64/itpay-sell +0 -0
  5. package/bin/darwin-arm64/itpay-sell +0 -0
  6. package/bin/linux-amd64/itpay-sell +0 -0
  7. package/bin/linux-arm64/itpay-sell +0 -0
  8. package/bin/windows-amd64/itpay-sell.exe +0 -0
  9. package/dist/src/client/backend.js +38 -2
  10. package/dist/src/client/http.js +2 -0
  11. package/dist/src/commands/aftercare.js +12 -0
  12. package/dist/src/commands/buy.js +17 -58
  13. package/dist/src/commands/cart.js +17 -15
  14. package/dist/src/commands/catalog.js +39 -14
  15. package/dist/src/commands/checkout.js +146 -5
  16. package/dist/src/commands/checkout_handoff.js +11 -61
  17. package/dist/src/commands/docs.js +7 -18
  18. package/dist/src/commands/guidance.js +1 -134
  19. package/dist/src/commands/install.js +6 -3
  20. package/dist/src/commands/order.js +34 -3
  21. package/dist/src/commands/orders.js +4 -9
  22. package/dist/src/commands/pay.js +1 -1
  23. package/dist/src/commands/presentation.js +153 -0
  24. package/dist/src/commands/rail_catalog.js +1 -0
  25. package/dist/src/commands/readyz.js +1 -1
  26. package/dist/src/commands/refund.js +13 -57
  27. package/dist/src/commands/services/actions.js +133 -0
  28. package/dist/src/commands/services/commerce.js +197 -0
  29. package/dist/src/commands/services/lifecycle.js +418 -0
  30. package/dist/src/commands/services/results.js +130 -0
  31. package/dist/src/commands/services.js +14 -1068
  32. package/dist/src/commands/skill.js +8 -5
  33. package/dist/src/commands/vault.js +27 -20
  34. package/dist/src/commands/vault_handoff.js +18 -0
  35. package/dist/src/main.js +332 -46
  36. package/dist/src/render/browser.js +65 -0
  37. package/dist/src/render/checkout_handoff.js +101 -0
  38. package/dist/src/render/checkout_plan.js +47 -0
  39. package/dist/src/render/markdown.js +0 -3
  40. package/dist/src/render/qr.js +1 -1
  41. package/dist/src/sell/commands.js +80 -0
  42. package/dist/src/sell/contract.js +86 -0
  43. package/dist/src/sell/local.js +260 -0
  44. package/dist/src/sell/mcp.js +49 -0
  45. package/dist/src/sell/preview.js +70 -0
  46. package/dist/src/sell/sync.js +145 -0
  47. package/dist/src/services/adapter.js +1 -0
  48. package/dist/src/services/presentation.js +198 -0
  49. package/dist/src/services/rail/actions.js +51 -0
  50. package/dist/src/services/rail/adapter.js +36 -0
  51. package/dist/src/services/rail/aftercare.js +45 -0
  52. package/dist/src/services/rail/catalog.js +261 -0
  53. package/dist/src/services/rail/checkout.js +19 -0
  54. package/dist/src/services/rail/fulfillment.js +76 -0
  55. package/dist/src/services/rail/guide.js +110 -0
  56. package/dist/src/services/rail/invocation.js +54 -0
  57. package/dist/src/services/rail/page.js +76 -0
  58. package/dist/src/services/rail/pagination.js +6 -0
  59. package/dist/src/services/rail/query.js +71 -0
  60. package/dist/src/services/rail/readers.js +107 -0
  61. package/dist/src/services/rail/reading.js +125 -0
  62. package/dist/src/services/rail/recovery.js +51 -0
  63. package/dist/src/services/rail/results.js +378 -0
  64. package/dist/src/services/rail/review.js +268 -0
  65. package/dist/src/services/rail/saved.js +49 -0
  66. package/dist/src/services/rail/wait.js +11 -0
  67. package/dist/src/services/registry.js +31 -0
  68. package/dist/src/services/schema.js +21 -0
  69. package/dist/src/services/shared/envelope.js +184 -0
  70. package/dist/src/services/shared/input.js +39 -0
  71. package/dist/src/services/shared/pagination.js +28 -0
  72. package/dist/src/services/shared/platform.js +365 -0
  73. package/dist/src/services/shared/refund.js +40 -0
  74. package/dist/src/services/shared/workflow.js +28 -0
  75. package/dist/src/state/account_auth.js +354 -0
  76. package/dist/src/state/cart_session.js +4 -1
  77. package/dist/src/state/client_context.js +72 -0
  78. package/dist/src/state/config.js +44 -14
  79. package/dist/src/state/device_authority.js +402 -72
  80. package/dist/src/state/task_journal.js +90 -0
  81. package/docs/agent/buyer/cart-checkout.json +8 -6
  82. package/docs/agent/buyer/catalog-list.json +29 -9
  83. package/docs/agent/buyer/identity-and-sessions.json +26 -9
  84. package/docs/agent/buyer/install-and-setup.json +17 -15
  85. package/docs/agent/buyer/orders-refunds.json +25 -9
  86. package/docs/agent/buyer/payment-flow.json +6 -4
  87. package/docs/agent/buyer/purchased-content.json +18 -4
  88. package/docs/agent/buyer/quickstart.json +26 -17
  89. package/docs/agent/buyer/rail-booking.json +176 -0
  90. package/docs/agent/buyer/rail-fast-checkout.json +48 -0
  91. package/docs/agent/buyer/render-hosts.json +23 -12
  92. package/docs/agent/buyer/service-feedback.json +4 -3
  93. package/docs/cli-reference/agent-types.md +7 -2
  94. package/docs/cli-reference/commands/aftercare.md +15 -0
  95. package/docs/cli-reference/commands/auth.md +96 -0
  96. package/docs/cli-reference/commands/buy.md +2 -1
  97. package/docs/cli-reference/commands/cart/add.md +5 -1
  98. package/docs/cli-reference/commands/cart/clear.md +1 -1
  99. package/docs/cli-reference/commands/cart/index.md +1 -1
  100. package/docs/cli-reference/commands/cart/next.md +5 -1
  101. package/docs/cli-reference/commands/cart/remove.md +1 -1
  102. package/docs/cli-reference/commands/cart/show.md +1 -1
  103. package/docs/cli-reference/commands/catalog/index.md +1 -1
  104. package/docs/cli-reference/commands/catalog/list.md +2 -2
  105. package/docs/cli-reference/commands/checkout.md +12 -2
  106. package/docs/cli-reference/commands/device.md +18 -11
  107. package/docs/cli-reference/commands/docs/index.md +1 -1
  108. package/docs/cli-reference/commands/docs/list.md +1 -1
  109. package/docs/cli-reference/commands/docs/search.md +1 -1
  110. package/docs/cli-reference/commands/docs/show.md +1 -1
  111. package/docs/cli-reference/commands/install.md +8 -6
  112. package/docs/cli-reference/commands/next.md +1 -1
  113. package/docs/cli-reference/commands/order.md +1 -1
  114. package/docs/cli-reference/commands/pay.md +1 -1
  115. package/docs/cli-reference/commands/readyz.md +11 -11
  116. package/docs/cli-reference/commands/refund/cancel.md +1 -1
  117. package/docs/cli-reference/commands/refund/create.md +1 -1
  118. package/docs/cli-reference/commands/refund/get.md +2 -2
  119. package/docs/cli-reference/commands/refund/index.md +1 -1
  120. package/docs/cli-reference/commands/refund/list.md +1 -1
  121. package/docs/cli-reference/commands/refund/watch.md +1 -1
  122. package/docs/cli-reference/commands/sell.md +1364 -0
  123. package/docs/cli-reference/commands/services/action.md +24 -3
  124. package/docs/cli-reference/commands/services/checkout.md +17 -2
  125. package/docs/cli-reference/commands/services/events.md +1 -1
  126. package/docs/cli-reference/commands/services/get.md +4 -2
  127. package/docs/cli-reference/commands/services/index.md +4 -3
  128. package/docs/cli-reference/commands/services/invoke.md +6 -4
  129. package/docs/cli-reference/commands/services/list.md +1 -1
  130. package/docs/cli-reference/commands/services/next.md +78 -12
  131. package/docs/cli-reference/commands/services/page.md +71 -0
  132. package/docs/cli-reference/commands/services/quote.md +4 -4
  133. package/docs/cli-reference/commands/services/read-result.md +17 -6
  134. package/docs/cli-reference/commands/services/run.md +152 -0
  135. package/docs/cli-reference/commands/services/start.md +5 -3
  136. package/docs/cli-reference/commands/vault/access.md +5 -1
  137. package/docs/cli-reference/conventions.md +4 -6
  138. package/docs/cli-reference/index.md +10 -1
  139. package/docs/sell.md +84 -0
  140. package/package.json +20 -12
  141. package/skills/itpay/SKILL.md +112 -102
  142. package/docs/skill-bundle-rollout/01-mcp-authentication.md +0 -134
  143. package/docs/skill-bundle-rollout/02-platform-bundle-repositories.md +0 -210
  144. package/docs/skill-bundle-rollout/03-platform-publishing.md +0 -161
  145. package/docs/skill-bundle-rollout/04-first-wave-platforms.md +0 -69
  146. package/docs/skill-bundle-rollout/05-sync-operations.md +0 -62
  147. package/docs/skill-bundle-rollout/README.md +0 -151
  148. /package/bin/{itp → itp.mjs} +0 -0
@@ -0,0 +1,49 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { z } from 'zod';
4
+ import { SELL_GUIDE, SELL_OPERATIONS, sellRequest } from './contract.js';
5
+ import { LOCAL_ACTIONS, localAction } from './local.js';
6
+ import { newBackendClient, loadConfig } from '../state/config.js';
7
+ import { syncProject } from './sync.js';
8
+ import { preview } from './preview.js';
9
+ export async function serveSellMCP(directory) {
10
+ const server = new McpServer({ name: 'itpay-sell-local', version: SELL_GUIDE.version }, { instructions: 'Use itpay_seller_guide first. Local tests call Provider APIs and may incur cost; disclose side effects and obtain user approval. Never pass secret values in tool arguments. This server uses its configured project directory.' });
11
+ server.tool('itpay_seller_guide', 'Read the publishing guide and constraints', {}, async () => ({ content: [{ type: 'text', text: JSON.stringify({ ...SELL_GUIDE, operations: SELL_OPERATIONS }) }] }));
12
+ for (const action of LOCAL_ACTIONS) {
13
+ server.tool('itpay_seller_local_' + action.replaceAll(' ', '_').replaceAll('-', '_'), `Local ${action}. Paths are user-provided files; never scan for credentials. Confirm only after explicit user approval.`, { merchant_id: z.string().optional(), environment: z.string().optional(), service_id: z.string().optional(), url: z.string().url().optional(), file: z.string().optional(), name: z.string().optional(), provider_key: z.string().optional(), profile: z.string().optional(), version: z.string().optional(), run: z.string().optional(), confirmed: z.boolean().default(false) }, async (args) => {
14
+ try {
15
+ const result = await localAction(action, directory, { ...args, merchantId: args.merchant_id, providerKey: args.provider_key, serviceId: args.service_id, confirm: args.confirmed });
16
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
17
+ }
18
+ catch (error) {
19
+ return { isError: true, content: [{ type: 'text', text: error instanceof Error ? error.message : 'Local action failed' }] };
20
+ }
21
+ });
22
+ }
23
+ server.tool('itpay_seller_sync', 'Upload or download the configured project after explicit user confirmation. Never pass credential values.', { action: z.enum(['push', 'pull']), merchant_id: z.string(), draft_id: z.string().optional(), bindings_file: z.string().optional(), confirmed: z.boolean().default(false) }, async (args) => {
24
+ try {
25
+ const result = await syncProject(args.action, directory, { merchantId: args.merchant_id, draftId: args.draft_id, bindings: args.bindings_file, confirm: args.confirmed });
26
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
27
+ }
28
+ catch (error) {
29
+ return { isError: true, content: [{ type: 'text', text: error instanceof Error ? error.message : 'Sync failed' }] };
30
+ }
31
+ });
32
+ let display;
33
+ server.tool('itpay_seller_preview', 'Open the existing Builder for this local workflow; user can then confirm through the local workflow tool.', {}, async () => { display ??= await preview(directory); return { content: [{ type: 'text', text: JSON.stringify({ url: display.url }) }] }; });
34
+ for (const operation of SELL_OPERATIONS) {
35
+ server.tool('itpay_seller_platform_' + operation.command.replaceAll(' ', '_').replaceAll('-', '_'), `${operation.description}. Input contract: ${JSON.stringify(operation.fields)}. Optional fields: ${JSON.stringify(operation.optional ?? [])}.`, { merchant_id: z.string().optional(), draft_id: z.string().optional(), library_api_id: z.string().optional(), intake_id: z.string().optional(), version_id: z.string().optional(), run_id: z.string().optional(), submission_id: z.string().optional(), input: z.record(z.unknown()).default({}), confirmed: z.boolean().default(false) }, async (args) => {
36
+ try {
37
+ if (operation.confirmation && !args.confirmed)
38
+ throw new Error('Explicit user confirmation required');
39
+ const result = await newBackendClient(loadConfig()).sellRequest(sellRequest(operation, args, args.input));
40
+ return { content: [{ type: 'text', text: JSON.stringify(result ?? {}) }] };
41
+ }
42
+ catch (error) {
43
+ return { isError: true, content: [{ type: 'text', text: error instanceof Error ? error.message : 'Platform action failed' }] };
44
+ }
45
+ });
46
+ }
47
+ process.stdin.on('end', () => display?.close());
48
+ await server.connect(new StdioServerTransport());
49
+ }
@@ -0,0 +1,70 @@
1
+ import { createServer } from 'node:http';
2
+ import { randomBytes, createHash } from 'node:crypto';
3
+ import { readFileSync, existsSync } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { resolve, extname } from 'node:path';
6
+ import { project } from './local.js';
7
+ export async function preview(directory) {
8
+ const packaged = fileURLToPath(new URL('../../../assets/sell-preview/', import.meta.url));
9
+ const assets = existsSync(packaged) ? packaged : fileURLToPath(new URL('../../assets/sell-preview/', import.meta.url));
10
+ if (!existsSync(resolve(assets, 'index.html')))
11
+ throw new Error('CLI preview assets are missing; install the complete CLI package');
12
+ const token = randomBytes(24).toString('hex');
13
+ const server = createServer((req, res) => {
14
+ res.setHeader('Cache-Control', 'no-store');
15
+ res.setHeader('Referrer-Policy', 'no-referrer');
16
+ res.setHeader('X-Content-Type-Options', 'nosniff');
17
+ if (req.method !== 'GET' || !req.url?.startsWith('/' + token + '/')) {
18
+ res.writeHead(404).end();
19
+ return;
20
+ }
21
+ try {
22
+ const path = req.url.slice(token.length + 2).split('?')[0] ?? '';
23
+ if (path === 'document') {
24
+ const p = project(directory);
25
+ const versions = JSON.parse(readFileSync(resolve(p.state, 'versions.json'), 'utf8'));
26
+ const version = versions.versions.find((v) => v.version_id === versions.selected);
27
+ res.setHeader('Content-Type', 'application/json');
28
+ const actualHash = 'sha256:' + createHash('sha256').update(p.yaml).digest('hex');
29
+ let run;
30
+ const marker = resolve(p.state, 'last-run.json');
31
+ if (existsSync(marker)) {
32
+ const id = JSON.parse(readFileSync(marker, 'utf8')).run_id;
33
+ if (typeof id === 'string' && /^[A-Za-z0-9_-]+$/.test(id) && existsSync(resolve(p.state, 'runs', id, 'meta.json'))) {
34
+ const dir = resolve(p.state, 'runs', id), meta = JSON.parse(readFileSync(resolve(dir, 'meta.json'), 'utf8'));
35
+ const traces = existsSync(resolve(dir, 'nodes.json')) ? JSON.parse(readFileSync(resolve(dir, 'nodes.json'), 'utf8')) : [];
36
+ const report = existsSync(resolve(dir, 'report.json')) ? JSON.parse(readFileSync(resolve(dir, 'report.json'), 'utf8')).report : undefined;
37
+ const configHash = 'sha256:' + createHash('sha256').update(JSON.stringify(p.config)).digest('hex');
38
+ const stale = meta.hash !== actualHash || meta.config_hash !== configHash;
39
+ const fixtures = p.config.fixtures ?? [];
40
+ run = { status: stale ? 'stale' : report?.status ?? 'running', issues: report?.issues ?? [], validation: { fixture_runs: fixtures.map((f) => ({ ...f, nodes: traces.filter((t) => t.record_type === 'node' && t.fixture_id === f.fixture_id) })), api_recognitions: [], side_effect_operations: [] } };
41
+ }
42
+ }
43
+ res.end(JSON.stringify({ document: p.yaml, version: version?.hash === actualHash ? version.name : 'Unsaved', hash: actualHash, pricing: p.config.pricing, run }));
44
+ return;
45
+ }
46
+ const file = resolve(assets, path || 'index.html');
47
+ if (!file.startsWith(resolve(assets) + '/')) {
48
+ res.writeHead(404).end();
49
+ return;
50
+ }
51
+ const types = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript', '.css': 'text/css', '.svg': 'image/svg+xml', '.woff2': 'font/woff2' };
52
+ res.setHeader('Content-Type', types[extname(file)] ?? 'application/octet-stream');
53
+ res.end(readFileSync(file));
54
+ }
55
+ catch {
56
+ res.writeHead(404).end();
57
+ }
58
+ });
59
+ return new Promise((done, fail) => {
60
+ server.on('error', fail);
61
+ server.listen(0, '127.0.0.1', () => {
62
+ const address = server.address();
63
+ if (!address || typeof address === 'string') {
64
+ fail(new Error('Preview address unavailable'));
65
+ return;
66
+ }
67
+ done({ url: `http://127.0.0.1:${address.port}/${token}/`, close: () => server.close() });
68
+ });
69
+ });
70
+ }
@@ -0,0 +1,145 @@
1
+ import { HttpError } from "../client/http.js";
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ import { createHash } from 'node:crypto';
5
+ import { newBackendClient, loadConfig } from '../state/config.js';
6
+ import { companion, project, localDependencies } from './local.js';
7
+ const encode = encodeURIComponent;
8
+ const hash = (value) => 'sha256:' + createHash('sha256').update(value).digest('hex');
9
+ function read(path) { return JSON.parse(readFileSync(path, 'utf8')); }
10
+ function save(path, value) { writeFileSync(path, JSON.stringify(value, null, 2) + '\n', { mode: 0o600 }); }
11
+ function platformWorkflow(draft) {
12
+ const document = draft.arazzo?.arazzo_document;
13
+ if (typeof document !== 'string' || !document.trim())
14
+ throw new Error('Platform response is missing the workflow document');
15
+ return document;
16
+ }
17
+ export async function syncProject(action, directory, options) {
18
+ const p = project(directory), cloudPath = join(p.state, 'cloud.json');
19
+ let cloud = existsSync(cloudPath) ? read(cloudPath) : {};
20
+ const merchant = options.merchantId ?? cloud.merchant_id ?? p.config.merchant_id;
21
+ if (!merchant)
22
+ throw new Error('--merchant-id is required');
23
+ if (cloud.merchant_id && cloud.merchant_id !== merchant)
24
+ throw new Error('Project is bound to a different merchant');
25
+ const api = newBackendClient(loadConfig()), org = '/v1/seller/organizations/' + encode(merchant);
26
+ const call = async (method, path, body) => await api.sellRequest({ method, path, ...(body !== undefined ? { body } : {}) });
27
+ if (action === 'pull') {
28
+ const draftID = options.draftId ?? cloud.draft_id;
29
+ if (!draftID)
30
+ throw new Error('--draft-id is required');
31
+ if (!options.confirm)
32
+ throw new Error('Pull replaces local workflow/settings; inspect changes and confirm first');
33
+ const bundle = await call('GET', `${org}/service-drafts/${encode(draftID)}/bundle`);
34
+ const backup = join(p.state, 'backups', String(Date.now()));
35
+ mkdirSync(backup, { recursive: true, mode: 0o700 });
36
+ writeFileSync(join(backup, 'workflow.yaml'), p.yaml);
37
+ save(join(backup, 'service.json'), p.config);
38
+ const draft = bundle.draft;
39
+ writeFileSync(join(p.root, 'workflow.yaml'), platformWorkflow(draft));
40
+ save(join(p.root, 'service.json'), { ...p.config, service_id: draft.service_id, public_name: draft.public_name, pricing: draft.pricing, policy: draft.policy, fixtures: bundle.fixtures.fixtures });
41
+ save(join(p.state, 'platform-dependencies.json'), bundle.dependencies);
42
+ cloud = { merchant_id: merchant, draft_id: draftID, revision: draft.semantic_revision, hash: draft.semantic_hash };
43
+ save(cloudPath, cloud);
44
+ return { cloud, backup, next: 'Save a new local version. Platform credentials were not downloaded.' };
45
+ }
46
+ const versions = read(join(p.state, 'versions.json'));
47
+ const version = versions.versions.find((v) => v.version_id === versions.selected);
48
+ const confirmation = existsSync(join(p.state, 'confirmation.json')) ? read(join(p.state, 'confirmation.json')) : {};
49
+ if (!version || version.hash !== hash(p.yaml) || version.config_hash !== hash(JSON.stringify(p.config)) || confirmation.version_id !== version.version_id || confirmation.hash !== version.hash || confirmation.config_hash !== version.config_hash)
50
+ throw new Error('Save and confirm the exact current version before uploading');
51
+ const validation = (await companion({ action: 'validate', dependencies: localDependencies(p.state), document: p.yaml, pricing: p.config.pricing, policy: p.config.policy })).find(event => event.type === 'validation')?.value;
52
+ if (!validation?.result?.valid)
53
+ throw new Error('Workflow does not pass validation');
54
+ if (!options.confirm)
55
+ return { local_version: version, cloud, pricing: p.config.pricing, instruction: 'Review the service package and platform API calls, then confirm upload. Platform verification is a separate explicit action.' };
56
+ if (!cloud.draft_id) {
57
+ const drafts = await call('GET', `${org}/service-drafts`);
58
+ const existing = drafts.drafts?.find((d) => d.service_id === p.config.service_id);
59
+ if (existing)
60
+ throw new Error('Service ID already exists. Pull that draft explicitly before updating it.');
61
+ const draft = await call('POST', `${org}/service-drafts`, { service_id: p.config.service_id, public_name: p.config.public_name });
62
+ cloud = { merchant_id: merchant, draft_id: draft.draft_id, revision: draft.semantic_revision, operation_bindings: {} };
63
+ save(cloudPath, cloud);
64
+ }
65
+ const base = `${org}/service-drafts/${encode(cloud.draft_id)}`;
66
+ let current = await call('GET', base + '/workflow');
67
+ if (current.semantic_revision !== cloud.revision)
68
+ throw new Error('Remote draft changed; pull and review before retrying');
69
+ if (cloud.local_version_id === version.version_id && cloud.hash === current.semantic_hash && cloud.version_id) {
70
+ const saved = await call('GET', base + '/workflow/versions');
71
+ if (saved.versions.some((item) => item.version_id === cloud.version_id)) {
72
+ const fixtureState = await call('GET', base + '/fixtures');
73
+ if (fixtureState.revision !== cloud.fixture_revision)
74
+ throw new Error('Remote test inputs changed; pull and review before retrying');
75
+ return { cloud, already_uploaded: true, next: 'Read the platform Guide and verify the saved version' };
76
+ }
77
+ }
78
+ const sources = join(p.state, 'sources');
79
+ cloud.operation_bindings ??= {};
80
+ if (existsSync(sources))
81
+ for (const file of readdirSync(sources)) {
82
+ const source = read(join(sources, file));
83
+ const imported = await call('POST', `${org}/api-intakes`, { provider_key: source.provider_key, document: source.original, media_type: 'application/yaml' });
84
+ for (const local of source.operations) {
85
+ const remote = imported.operations.find((operation) => operation.operation_key === local.operation_key && operation.method === local.method && operation.path === local.path);
86
+ if (!remote)
87
+ throw new Error('Platform import differs from the local API contract');
88
+ if (remote.operation_hash !== local.operation_hash)
89
+ throw new Error('Platform API contract changed; download and review the imported contract');
90
+ cloud.operation_bindings[local.provider_operation_version_id] = remote.provider_operation_version_id;
91
+ }
92
+ save(cloudPath, cloud);
93
+ }
94
+ const credentials = options.bindings ? read(resolve(options.bindings)) : {};
95
+ const document = validation.document;
96
+ for (const workflow of document.workflows)
97
+ for (const step of workflow.steps) {
98
+ const meta = step['x-itpay-operation'];
99
+ if (meta?.type === 'api_call') {
100
+ meta.providerOperationVersionId = cloud.operation_bindings[meta.providerOperationVersionId] ?? meta.providerOperationVersionId;
101
+ if (credentials[meta.credentialProfileId])
102
+ meta.credentialProfileId = credentials[meta.credentialProfileId];
103
+ }
104
+ }
105
+ // Each durable boundary is recorded. On an ambiguous write, pull explicitly;
106
+ // never retry a guessed revision or overwrite another editor's changes.
107
+ current = await call('PUT', base + '/workflow', { expected_revision: cloud.revision, public_name: p.config.public_name, arazzo: document });
108
+ cloud.revision = current.semantic_revision;
109
+ save(cloudPath, cloud);
110
+ current = await call('PUT', base + '/pricing', { expected_revision: cloud.revision, pricing: p.config.pricing, policy: p.config.policy });
111
+ cloud.revision = current.semantic_revision;
112
+ save(cloudPath, cloud);
113
+ let fixtures;
114
+ try {
115
+ fixtures = await call('GET', base + '/fixtures');
116
+ }
117
+ catch (error) {
118
+ if (error instanceof HttpError && error.status === 404)
119
+ fixtures = { revision: 0 };
120
+ else
121
+ throw error;
122
+ }
123
+ const savedFixtures = await call('PUT', base + '/fixtures', { expected_revision: fixtures.revision, fixtures: p.config.fixtures });
124
+ const existingVersions = await call('GET', base + '/workflow/versions');
125
+ const saved = existingVersions.versions.find((item) => item.name === version.name && item.arazzo_document === platformWorkflow(current)) ?? await call('POST', base + '/workflow/versions', { name: version.name, source: 'current', expected_revision: cloud.revision, arazzo_document: platformWorkflow(current) });
126
+ cloud.version_id = saved.version_id;
127
+ cloud.fixture_revision = savedFixtures.revision;
128
+ cloud.local_version_id = version.version_id;
129
+ cloud.hash = current.semantic_hash;
130
+ save(cloudPath, cloud);
131
+ await call('POST', base + '/workflow/validate', {});
132
+ return { cloud, platform_verified: false, next: { command: 'itpay sell verify', input: { workflow_version_id: cloud.version_id, expected_semantic_revision: cloud.revision, expected_fixture_revision: cloud.fixture_revision } }, instruction: 'Review and confirm platform verification before any real API calls. Local test success is not platform evidence.' };
133
+ }
134
+ export function registerSync(sell) {
135
+ for (const action of ['push', 'pull'])
136
+ sell.command(action).option('--project <directory>', 'Project directory', '.').option('--merchant-id <id>').option('--draft-id <id>').option('--bindings <file>', 'Local profile ID to platform credential profile ID map').option('--confirm').option('--json').action(async (options) => {
137
+ try {
138
+ process.stdout.write(JSON.stringify({ status: 'ok', result: await syncProject(action, options.project, options) }, null, 2) + '\n');
139
+ }
140
+ catch (error) {
141
+ process.exitCode = 1;
142
+ process.stdout.write(JSON.stringify({ status: 'error', message: error instanceof Error ? error.message : 'Sync failed' }) + '\n');
143
+ }
144
+ });
145
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,198 @@
1
+ import { serviceAdapter } from "./registry.js";
2
+ import { refundStateEnvelope } from "./shared/refund.js";
3
+ import { appendFeedbackPostmortemInstruction, isTerminalServiceExecutionStatus } from "./shared/envelope.js";
4
+ import { terminalExecutionEnvelope, serviceDeliveryMode, normalizeGrantStatus, paidContinuation, purchaseConfirmationInstruction, serviceAllowedActionCommand } from "./shared/platform.js";
5
+ export function servicesNextEnvelope(model) {
6
+ const execution = model.execution;
7
+ const adapter = serviceAdapter(execution);
8
+ const currentDelivery = model.current_delivery ?? model.delivery_bindings.at(-1);
9
+ const lockedRefund = model.refunds.find((refund) => refund.access_locked);
10
+ if (lockedRefund) {
11
+ const refundState = refundStateEnvelope(lockedRefund, "delivery_locked");
12
+ return { ...refundState, result: { ...refundState.result, service_execution_id: execution.service_execution_id, refund: { refund_request_id: lockedRefund.refund_request_id, status: lockedRefund.status, ...(lockedRefund.failure_class ? { failure_class: lockedRefund.failure_class } : {}) } },
13
+ next: refundState.next ? { command: `itpay refund get ${lockedRefund.refund_request_id} --json`, reason: "读取同一退款权威状态" } : null };
14
+ }
15
+ const unsettledPayment = model.payment_bindings.some(binding => typeof binding.status === "string" && ["pending", "outcome_unknown", "unknown", "processing"].includes(binding.status));
16
+ if (unsettledPayment)
17
+ return { status: "payment_outcome_pending", result: { service_execution_id: execution.service_execution_id, payment_state: "unsettled", checkout_ids: model.checkout_bindings.map(binding => binding.checkout_id) }, instruction: "原付款结果尚未确认。保留同一执行、Checkout和订单,只核对权威事实;不创建新报价、购物车、付款入口或购买,也不宣称已付款或服务已完成。", next: null, recovery: [{ command: `itpay services get ${execution.service_execution_id} --json`, reason: "仅用户要求或平台恢复后核对同一付款事实" }] };
18
+ // Terminal facts take priority unless the locked adapter supports same-execution recovery.
19
+ if (!(adapter.preservesTerminalRecovery(model)) && isTerminalServiceExecutionStatus(execution.status) && !["completed", "delivery_completed"].includes(execution.status)) {
20
+ // Preserve the registered adapter's safe failure facts even at execution terminal.
21
+ const failedWorkflow = execution.status === "failed" && model.workflow?.status === "failed" ? adapter.workflow(model) : undefined;
22
+ if (failedWorkflow)
23
+ return { ...failedWorkflow, next: null };
24
+ const terminal = terminalExecutionEnvelope(model);
25
+ if (terminal)
26
+ return terminal;
27
+ }
28
+ const latestRun = model.execution_requests?.filter((request) => request.execution_kind === "service.execution.run").at(-1);
29
+ if (latestRun && ["pending", "started"].includes(latestRun.status) && model.workflow?.status !== "failed") {
30
+ return {
31
+ status: "running",
32
+ result: { service_execution_id: execution.service_execution_id, execution_request_id: latestRun.execution_request_id },
33
+ instruction: "任务仍在排队或执行中,稍后读取同一任务。不要重新查询、重复提交输入或把等待状态当作零结果。",
34
+ next: { command: `itpay services next ${execution.service_execution_id} --json`, reason: "稍后读取同一任务状态" },
35
+ recovery: [],
36
+ };
37
+ }
38
+ const businessState = adapter.projectState(model);
39
+ if (businessState)
40
+ return businessState;
41
+ if (latestRun && ["failed", "cancelled"].includes(latestRun.status) && !model.workflow_entry) {
42
+ return {
43
+ status: latestRun.status,
44
+ result: { service_execution_id: execution.service_execution_id },
45
+ instruction: "本次任务未完成,需要检查同一任务的处理记录;这不表示没有结果。不要自动重新发起。",
46
+ next: null,
47
+ recovery: [{ command: `itpay services events ${execution.service_execution_id} --json`, reason: "读取同一任务的处理记录" }],
48
+ };
49
+ }
50
+ if (!adapter.supported)
51
+ return { status: "presentation_contract_unavailable", result: { service_execution_id: execution.service_execution_id, service_id: execution.service_id, service_contract_version_id: execution.service_contract_version_id, public_binding: execution.public_binding }, instruction: "当前锁定呈现合同不受本候选支持;保留执行与已有结果并停止,取得兼容CLI后读取同一执行,不猜模式或启动新购买。", next: null, recovery: [] };
52
+ const workflowEnvelope = adapter.workflow(model);
53
+ if (workflowEnvelope)
54
+ return workflowEnvelope;
55
+ const terminalEnvelope = terminalExecutionEnvelope(model);
56
+ if (terminalEnvelope)
57
+ return terminalEnvelope;
58
+ const currentItems = model.current_result_items ?? [];
59
+ const savedRailResult = adapter.savedResult(model);
60
+ if (savedRailResult)
61
+ return savedRailResult;
62
+ const delivery = currentDelivery;
63
+ const deliveryMode = serviceDeliveryMode(model);
64
+ const candidateSelection = model.allowed_actions?.find((action) => action.type === "select_candidate");
65
+ if (candidateSelection && currentItems.length > 0) {
66
+ const paidCapability = delivery?.capability_id
67
+ ? model.capabilities.find((capability) => capability.capability_id === delivery.capability_id && capability.requires_payment)
68
+ : undefined;
69
+ return {
70
+ status: "candidate_selection_available",
71
+ result: {
72
+ service_execution_id: execution.service_execution_id,
73
+ ...(delivery?.capability_id ? { capability_id: delivery.capability_id } : {}),
74
+ ...(deliveryMode ? { delivery_mode: deliveryMode } : {}),
75
+ items: currentItems.map((item) => ({
76
+ rank: item.rank,
77
+ title: item.display_title,
78
+ safe_payload: item.safe_payload,
79
+ })),
80
+ },
81
+ instruction: paidCapability
82
+ ? "付费搜索已完成。用编号、名称和可公开字段向用户说明结果,然后停止。只有用户明确选择候选并要求继续时才提交 interaction 中的对应编号;不要提及 safe_payload 或自动购买后续服务。"
83
+ : "用编号、名称和可公开字段向用户说明候选;若候选列表已满足目标就停止。只有用户明确选择并希望继续时才提交对应编号;不要提及 safe_payload、Execution 或内部 ID。",
84
+ next: null,
85
+ interaction: candidateInteraction(execution.service_execution_id),
86
+ recovery: [],
87
+ };
88
+ }
89
+ if (deliveryMode === "agent_visible_result") {
90
+ const items = currentItems.map((item) => ({
91
+ rank: item.rank,
92
+ title: item.display_title,
93
+ safe_payload: item.safe_payload,
94
+ }));
95
+ const selection = model.allowed_actions?.find((action) => action.type === "select_candidate");
96
+ const deliveryGuidance = adapter.deliveryGuidance(model);
97
+ const instruction = (delivery?.order_id
98
+ ? appendFeedbackPostmortemInstruction(items.length > 0
99
+ ? selection
100
+ ? "搜索已完成。用编号、名称和可公开字段向用户说明结果,然后停止。只有用户明确选择候选并要求继续时才提交 interaction 中的对应编号;不要提及 safe_payload。"
101
+ : "这一步的结果已经可用。用普通语言解释可公开字段并停止;不要提及 Arazzo、safe_payload 或内部 ID。"
102
+ : "告诉用户本次查询得到 0 个结果并停止。Agent 不读取其他交付、不重放当前查询、修改输入或创建新查询。", "delivered")
103
+ : items.length > 0
104
+ ? selection
105
+ ? "搜索已完成。用编号、名称和可公开字段向用户说明结果,然后停止。只有用户明确选择候选并要求继续时才提交 interaction 中的对应编号;不要提及 safe_payload。"
106
+ : "这一步的结果已经可用。用普通语言解释可公开字段并停止;不要提及 Arazzo、safe_payload 或内部 ID。"
107
+ : "告诉用户本次查询得到 0 个结果并停止。Agent 不读取其他交付、不重放当前查询、修改输入或创建新查询。")
108
+ + (deliveryGuidance?.instruction && items.length > 0 ? ` ${deliveryGuidance.instruction}` : "");
109
+ return {
110
+ status: items.length > 0 ? "result_ready" : "no_result",
111
+ result: {
112
+ service_execution_id: execution.service_execution_id,
113
+ ...(delivery?.capability_id ? { capability_id: delivery.capability_id } : {}),
114
+ ...(delivery?.order_id ? { order_id: delivery.order_id, aftercare: { optional: true, command: `itpay feedback submit --order ${delivery.order_id} --json`, reason: "结果答复后按需复盘;不猜评分" } } : {}),
115
+ delivery_mode: deliveryMode,
116
+ items,
117
+ },
118
+ instruction,
119
+ next: null,
120
+ ...(selection ? { interaction: candidateInteraction(execution.service_execution_id) } : {}),
121
+ recovery: deliveryGuidance?.recovery ?? [],
122
+ };
123
+ }
124
+ if (deliveryMode === "vault_artifact") {
125
+ const grantStatus = normalizeGrantStatus(delivery?.grant_status);
126
+ const grantActive = grantStatus === "active";
127
+ const grantPending = grantStatus === "pending";
128
+ return {
129
+ status: grantActive ? "grant_active" : grantPending ? "result_preparing" : "human_authorization_required",
130
+ result: {
131
+ service_execution_id: execution.service_execution_id,
132
+ ...(delivery?.capability_id ? { capability_id: delivery.capability_id } : {}),
133
+ delivery_mode: deliveryMode,
134
+ grant_status: grantStatus,
135
+ ...(delivery?.preparation ? { preparation: delivery.preparation } : {}),
136
+ ...(grantActive && delivery?.grant_expires_at ? { grant_expires_at: delivery.grant_expires_at } : {}),
137
+ },
138
+ instruction: grantActive
139
+ ? "先告诉用户付费内容已经准备好且当前读取授权有效;立即读取并只解释授权字段,遵守范围与到期时间。"
140
+ : grantPending
141
+ ? "告诉用户:授权已经完成,付费结果仍在同一订单下准备,不需要再次付款或授权。然后只执行 next.command 查询同一笔服务;Agent 不创建新服务、付款页面或数据请求,也不提前读取。"
142
+ : "先告诉用户付费内容已经归入当前订单,但需要本人确认一次读取授权;请用户在订单页面授权,未授权前不要读取或猜测内容。",
143
+ next: grantPending ? {
144
+ command: `itpay services next ${execution.service_execution_id} --json`,
145
+ reason: "等待同一 Execution 的交付准备完成",
146
+ } : {
147
+ command: `itpay services read-result ${execution.service_execution_id} --json`,
148
+ reason: grantActive ? "读取当前有效 grant 的结果" : "仅在用户确认授权后执行",
149
+ },
150
+ recovery: [],
151
+ };
152
+ }
153
+ const allowedActions = model.allowed_actions ?? [];
154
+ const preferred = allowedActions[0];
155
+ if (preferred?.type === "prepare_quote") {
156
+ const continuation = paidContinuation(model, preferred, {});
157
+ if (continuation) {
158
+ return {
159
+ status: execution.status,
160
+ result: {
161
+ service_execution_id: execution.service_execution_id,
162
+ service_id: execution.service_id,
163
+ phase: execution.phase,
164
+ checkout: continuation.checkout,
165
+ },
166
+ instruction: purchaseConfirmationInstruction(execution.status === "quota_exhausted" ? "quota_exhausted" : "candidate_selected", continuation.price, continuation.capability.delivery_email_required, continuation.capability.delivery_email_purpose),
167
+ next: continuation.next,
168
+ recovery: [],
169
+ };
170
+ }
171
+ }
172
+ const next = preferred ? serviceAllowedActionCommand(model, preferred) : null;
173
+ return {
174
+ status: execution.status,
175
+ result: {
176
+ service_execution_id: execution.service_execution_id,
177
+ service_id: execution.service_id,
178
+ phase: execution.phase,
179
+ allowed_actions: allowedActions.map((action) => ({
180
+ type: action.type,
181
+ ...(action.capability_id ? { capability_id: action.capability_id } : {}),
182
+ requires_human: action.requires_human,
183
+ })),
184
+ },
185
+ instruction: preferred?.type === "resume_checkout"
186
+ ? "这笔服务已经有付款页面。只执行 next.command 恢复并展示同一个入口;Agent 不创建新的报价、购物车、付款页面或服务。"
187
+ : preferred?.type === "wait"
188
+ ? "告诉用户付款和订单已经确认,结果仍在同一笔服务中处理,不需要再次付款;如果最终无法交付,将从原订单检查退款路径。稍后只执行 next.command;Agent 不创建新服务、付款页面或数据请求,也不承诺退款结果。"
189
+ : preferred?.requires_human
190
+ ? "当前下一步需要用户明确选择;先展示必要信息并等待确认。"
191
+ : preferred ? "执行服务端返回的唯一首选动作;不要猜测其他 capability。" + adapter.locationInstruction(model.capabilities.find((item) => item.capability_id === preferred.capability_id)?.input_schema) : "当前没有后续动作。",
192
+ next,
193
+ recovery: [{ command: `itpay services get ${execution.service_execution_id} --json`, reason: "仅在当前动作异常时检查时间线" }],
194
+ };
195
+ }
196
+ function candidateInteraction(id) {
197
+ return { schema_version: "itpay.interaction.v1", stage: "candidate_selection_required", input_template: { command: `itpay services action ${id} --action select_candidate --actor-type human --status approved --candidate <rank> --json`, required_input: ["rank"], executable: false } };
198
+ }
@@ -0,0 +1,51 @@
1
+ import { shellArgument } from "../shared/envelope.js";
2
+ // Never copy server shell text. Known action facts select a fixed local CLI form.
3
+ export function railAvailableActions(plan, executionID) {
4
+ return (plan.available_actions ?? []).map(action => {
5
+ const target = action.target;
6
+ const se = shellArgument(executionID);
7
+ const snapshot = target?.snapshot_id ?? plan.catalog_snapshot_id ?? plan.snapshot_id;
8
+ const snap = snapshot ? shellArgument(snapshot) : undefined;
9
+ let command;
10
+ let input;
11
+ if (!target || target.service_execution_id === executionID) {
12
+ switch (action.type) {
13
+ case "read_updates":
14
+ command = `itpay services next ${se}${snap ? ` --since-snapshot ${snap}` : ""} --json`;
15
+ break;
16
+ case "read_saved_page":
17
+ if (snap)
18
+ command = `itpay services page ${se} ${snap} --json`;
19
+ break;
20
+ case "read_catalog":
21
+ if (snap)
22
+ command = `itpay services read-result ${se} --snapshot ${snap} --json`;
23
+ break;
24
+ case "read_journey":
25
+ if (snap && target?.journey_id)
26
+ command = `itpay services read-result ${se} --snapshot ${snap} --journey ${shellArgument(target.journey_id)} --json`;
27
+ break;
28
+ case "select_journey":
29
+ if (target?.journey_id)
30
+ command = `itpay services action ${se} --action select_journey --actor-type human --status approved --input ${shellArgument(`journey_id=${target.journey_id}`)} --json`;
31
+ break;
32
+ case "expand_search": {
33
+ const facts = action.input;
34
+ if (typeof facts?.action_request_id === "string" && typeof facts.expected_query_revision === "number" && typeof facts.snapshot_id === "string") {
35
+ input = { action_request_id: facts.action_request_id, expected_query_revision: facts.expected_query_revision, snapshot_id: facts.snapshot_id };
36
+ command = `itpay services action ${se} --action workflow:expand_search --actor-type human --status approved --input-json <file> --json`;
37
+ }
38
+ break;
39
+ }
40
+ case "refine_preferences":
41
+ input = action.input_example;
42
+ command = `itpay services action ${se} --action workflow:refine_preferences --actor-type human --status approved --input-json <file> --json`;
43
+ break;
44
+ case "stop_search":
45
+ command = `itpay services action ${se} --action workflow:stop_search --actor-type human --status approved --json`;
46
+ break;
47
+ }
48
+ }
49
+ return { type: action.type, provider_effect: action.provider_effect, ...(action.when ? { when: action.when } : {}), ...(command ? { command, ...(input ? { input_template: { input, executable: false }, ...(action.input_schema ? { input_schema: action.input_schema } : {}), ...(action.type === "refine_preferences" ? { instruction: "仅填用户改变的偏好,保留 expected_query_revision;rerank 复用已存事实,不新增查车。示例不是用户选择,端点/日期改变须新查询。" } : {}) } : {}) } : { instruction: "缺少当前动作的结构化合同事实;保留原执行,不执行服务器命令文本。" }) };
50
+ });
51
+ }
@@ -0,0 +1,36 @@
1
+ import { railAftercareOrder, railAftercareList, railAftercareEnvelope, railAftercareCreate, railAftercareRespond } from "./aftercare.js";
2
+ import { railInputTemplate, railServiceGuidance, RAIL_ENDPOINT_BRIEF, locationInputInstruction, railOverviewInstruction } from "./guide.js";
3
+ import { railShouldWait, railPollAfter } from "./wait.js";
4
+ import { railInvocationProjection } from "./invocation.js";
5
+ import { railInputRecoveryInstruction, railRunError, railActionError } from "./recovery.js";
6
+ import { railWorkflowEnvelope } from "./review.js";
7
+ import { railPlanningEnvelope } from "./results.js";
8
+ import { railBookingEnvelope } from "./fulfillment.js";
9
+ import { railSavedResultEnvelope, railSavedDeliveryGuidance } from "./saved.js";
10
+ import { railCheckoutItinerary } from "./checkout.js";
11
+ import { renderRailResultPage } from "./page.js";
12
+ import { readSavedResult } from "./reading.js";
13
+ export const railAdapter = {
14
+ entryInstruction: RAIL_ENDPOINT_BRIEF + "按 result.guidance 逐项填写输入(when_to_use 说明本服务适用场景、input_fields 是必填契约、input_example 是待填示意,必须替换用户真实值),然后继续同一服务执行。不要臆造字段名。",
15
+ supported: true, inputTemplate: railInputTemplate, inputInstruction: railInputRecoveryInstruction,
16
+ guidance: railServiceGuidance, requiresGuidance: id => id === "itpay-rail-smart",
17
+ overviewInstruction: railOverviewInstruction, locationInstruction: locationInputInstruction,
18
+ invocation: railInvocationProjection, shouldWait: railShouldWait, pollAfter: railPollAfter,
19
+ preservesTerminalRecovery: model => model.workflow?.human_action?.context?.quote_recovery === true,
20
+ projectState: model => model.rail_booking ? railBookingEnvelope(model) : model.rail_planning ? railPlanningEnvelope(model) : null,
21
+ workflow: railWorkflowEnvelope, savedResult: railSavedResultEnvelope, deliveryGuidance: railSavedDeliveryGuidance,
22
+ extraFacts: model => model.rail_booking ? { rail_booking: model.rail_booking } : {},
23
+ runError: railRunError, actionError: railActionError, readResult: readSavedResult,
24
+ renderPage: (...args) => { renderRailResultPage(...args); return true; },
25
+ checkout: async (backend, model, id, token) => railCheckoutItinerary(model, await backend.getCheckoutPresentation(id, token)),
26
+ };
27
+ const bookingAdapter = {
28
+ ...railAdapter,
29
+ aftercare: { order: railAftercareOrder, directory: railAftercareList, present: railAftercareEnvelope, create: railAftercareCreate, respond: railAftercareRespond },
30
+ };
31
+ export function railAdapterFor(identity) {
32
+ const booking = identity.public_binding
33
+ ? identity.public_binding.operation_id === "prepare_purchase"
34
+ : identity.service_id === "itpay-rail-booking";
35
+ return booking ? bookingAdapter : railAdapter;
36
+ }
@@ -0,0 +1,45 @@
1
+ import { z } from "zod";
2
+ import { operationID } from "../../state/config.js";
3
+ import { CommandContractError } from "../shared/envelope.js";
4
+ const target = z.object({ leg_index: z.number().int().nonnegative(), passenger_index: z.number().int().nonnegative() }).strict();
5
+ const inputSchema = z.object({ operation: z.enum(["refund", "change"]), targets: z.array(target).min(1).max(15), reason: z.string().trim().min(1).max(1000), scope_revision: z.literal(1), user_confirmed_scope: z.literal(true), change: z.object({ travel_date: z.string().optional(), departure_after: z.string().optional(), arrival_before: z.string().optional(), seat_type: z.string().optional(), train_code: z.string().optional(), constraints: z.string().min(1).max(1000) }).strict().optional() }).strict();
6
+ const responseSchema = z.object({ expected_revision: z.number().int().positive(), proposal_revision: z.number().int().positive().optional(), user_approved: z.boolean().optional(), additional_information: z.string().max(1000).optional(), reason: z.string().max(1000).optional() }).strict();
7
+ export function railAftercareList(directory, orderID) {
8
+ return { status: "aftercare_directory", result: { ...directory }, instruction: directory.instruction + "段/乘客索引从0起,按返回事实选目标;明确‘第二段’用leg_index:1。整程模板列全部可用目标,指定范围则仅保留对应目标,不手填票价/供应商单号。范围已明确只说明后提交;不重新询问整程选择。", next: null, recovery: [], interaction: { schema_version: "itpay.interaction.v1", stage: "input_required", input_templates: Object.fromEntries(Object.entries(directory.input_templates).map(([operation, input]) => [operation, { command: `itpay aftercare create --order ${orderID} --input-json <file> --json`, input, executable: false, required_input: ["file", "targets", "reason", "user_confirmed_scope"] }])) } };
9
+ }
10
+ export function railAftercareEnvelope(view) {
11
+ const c = view.case;
12
+ const instruction = c.status === "awaiting_confirmation" ? "展示当前具体改签方案、旧新对应、差价和条件;旧购买委托不是新方案/费用批准。没有真实回答保持false并等待;本轮费用由人工协调,没有自动支付链接。"
13
+ : c.status === "needs_input" ? "展示管理员要求补充的内容,只补缺失事实;保留原申请范围。"
14
+ : c.status === "completed" ? "人工受理已完成;分别说明供应商核验结果与平台退款金额/状态。未退段仅报告权威已知事实,不保证仍可乘;不恢复平台内容访问锁。"
15
+ : c.status === "processing" ? "原申请正在人工办理或财务处理中;没有最终事实不说退票、改签或到账成功,不重试未知资金、不取消已发生操作。"
16
+ : ["cancelled", "rejected"].includes(c.status) ? "说明原申请已取消或被拒绝及原因;没有撤销已发生供应商操作,也不自动创建替代购买。"
17
+ : "已提交人工受理,尚未确认供应商退票/改签或钱款到账。先交付申请状态入口,再按用户意愿查询原申请,不循环轮询。";
18
+ const cancellation = view.can_cancel ? { cancel_template: { command: `itpay aftercare cancel ${c.case_id} --input-json <file> --json`, input: { expected_revision: c.revision, reason: "" }, executable: false, required_input: ["file"] } } : {};
19
+ const needsReply = ["awaiting_confirmation", "needs_input"].includes(c.status);
20
+ return { status: c.status, result: { ...view }, instruction, next: needsReply || ["completed", "cancelled", "rejected"].includes(c.status) ? null : { command: `itpay aftercare get ${c.case_id} --json`, reason: "按用户要求读取同一人工申请" }, recovery: [], ...((needsReply || view.can_cancel) ? { interaction: { schema_version: "itpay.interaction.v1", stage: c.status === "awaiting_confirmation" ? "consent_required" : "input_required", ...cancellation, ...(needsReply ? { input_template: { command: `itpay aftercare respond ${c.case_id} --input-json <file> --json`, input: { expected_revision: c.revision, ...(c.status === "awaiting_confirmation" ? { proposal_revision: c.proposal_revision, user_approved: false } : { additional_information: "" }) }, executable: false, required_input: ["file"] } } : {}) } } : {}) };
21
+ }
22
+ export async function railAftercareCreate(backend, config, orderID, raw) {
23
+ const parsed = inputSchema.safeParse(raw);
24
+ if (!parsed.success)
25
+ throw new CommandContractError("rail_aftercare_input_invalid", "Aftercare input fields or genuine scope confirmation are missing", "Agent读取当前完整模板修正技术字段;只问缺失的真实范围/改签意向,不让用户填技术JSON。", [{ command: `itpay aftercare list --order ${orderID} --json`, reason: "读取当前行程、目标和完整模板" }]);
26
+ const value = parsed.data;
27
+ const change = value.change;
28
+ const input = { operation: value.operation, targets: value.targets, reason: value.reason, scope_revision: value.scope_revision, user_confirmed_scope: value.user_confirmed_scope, ...(change ? { change: { constraints: change.constraints, ...(change.travel_date !== undefined ? { travel_date: change.travel_date } : {}), ...(change.departure_after !== undefined ? { departure_after: change.departure_after } : {}), ...(change.arrival_before !== undefined ? { arrival_before: change.arrival_before } : {}), ...(change.seat_type !== undefined ? { seat_type: change.seat_type } : {}), ...(change.train_code !== undefined ? { train_code: change.train_code } : {}) } } : {}) };
29
+ return railAftercareEnvelope(await backend.createRailAftercare(orderID, input, await operationID(config, `aftercare.create:${orderID}:${JSON.stringify(input)}`)));
30
+ }
31
+ export async function railAftercareGet(backend, id) { return railAftercareEnvelope(await backend.getRailAftercare(id)); }
32
+ export async function railAftercareRespond(backend, id, raw, cancel = false) {
33
+ const parsed = responseSchema.safeParse(raw);
34
+ if (!parsed.success)
35
+ throw new CommandContractError("rail_aftercare_input_invalid", "Current revision or response fields are missing", "使用原申请当前返回模板;实质方案变化须重新取得明确同意。", [{ command: `itpay aftercare get ${id} --json`, reason: "读取同一申请当前方案和版本" }]);
36
+ const value = parsed.data;
37
+ const input = { expected_revision: value.expected_revision, ...(value.proposal_revision !== undefined ? { proposal_revision: value.proposal_revision } : {}), ...(value.user_approved !== undefined ? { user_approved: value.user_approved } : {}), ...(value.additional_information !== undefined ? { additional_information: value.additional_information } : {}), ...(value.reason !== undefined ? { reason: value.reason } : {}) };
38
+ return railAftercareEnvelope(await (cancel ? backend.cancelRailAftercare(id, input) : backend.respondRailAftercare(id, input)));
39
+ }
40
+ export async function railAftercareOrder(backend, order, envelope) {
41
+ if (!["paid", "delivery_pending", "delivered", "partially_refunded", "refunded"].includes(order.status))
42
+ return envelope;
43
+ const directory = await backend.listRailAftercare(order.order_id);
44
+ return { ...envelope, result: { ...envelope.result, journey: directory.journey, aftercare_cases: directory.cases }, instruction: (order.status === "partially_refunded" ? "订单为部分退款;仅本次选中票的供应商/金额事实已变化,不能说全程已退。" : "按原订单读取票段与人工售后事实。") + " 平台内容锁保持;脱敏剩余票段状态是合法只读事实,不保证历史已出票仍可乘坐。", next: { command: `itpay aftercare list --order ${order.order_id} --json`, reason: "读取原行程、剩余票段、申请目标及完整售后模板" } };
45
+ }