@itpay/cli 2.0.2 → 2.0.5
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 +92 -142
- package/dist/src/client/backend.js +27 -9
- package/dist/src/commands/buy.js +84 -132
- package/dist/src/commands/cart.js +274 -169
- package/dist/src/commands/catalog.js +63 -38
- package/dist/src/commands/checkout.js +117 -78
- package/dist/src/commands/docs.js +97 -51
- package/dist/src/commands/guidance.js +87 -15
- package/dist/src/commands/install.js +50 -87
- package/dist/src/commands/next.js +45 -0
- package/dist/src/commands/order.js +44 -69
- package/dist/src/commands/orders.js +43 -15
- package/dist/src/commands/pay.js +51 -22
- package/dist/src/commands/readyz.js +8 -4
- package/dist/src/commands/refund.js +132 -14
- package/dist/src/commands/services.js +719 -148
- package/dist/src/main.js +711 -201
- package/dist/src/render/output.js +2 -3
- package/dist/src/state/cart_session.js +13 -17
- package/dist/src/state/client_context.js +4 -2
- package/dist/src/state/config.js +3 -5
- package/dist/src/state/device_authority.js +1 -1
- package/docs/agent/buyer/cart-checkout.json +27 -83
- package/docs/agent/buyer/install-and-setup.json +23 -67
- package/docs/agent/buyer/orders-refunds.json +31 -53
- package/docs/agent/buyer/payment-flow.json +24 -57
- package/docs/agent/buyer/quickstart.json +39 -162
- package/docs/agent/buyer/render-hosts.json +43 -57
- package/docs/cli-reference/agent-types.md +45 -0
- package/docs/cli-reference/commands/buy.md +167 -0
- package/docs/cli-reference/commands/cart/add.md +86 -0
- package/docs/cli-reference/commands/cart/clear.md +53 -0
- package/docs/cli-reference/commands/cart/index.md +30 -0
- package/docs/cli-reference/commands/cart/next.md +71 -0
- package/docs/cli-reference/commands/cart/remove.md +53 -0
- package/docs/cli-reference/commands/cart/show.md +65 -0
- package/docs/cli-reference/commands/catalog/index.md +26 -0
- package/docs/cli-reference/commands/catalog/list.md +45 -0
- package/docs/cli-reference/commands/checkout.md +74 -0
- package/docs/cli-reference/commands/docs/index.md +28 -0
- package/docs/cli-reference/commands/docs/list.md +51 -0
- package/docs/cli-reference/commands/docs/search.md +69 -0
- package/docs/cli-reference/commands/docs/show.md +68 -0
- package/docs/cli-reference/commands/install.md +112 -0
- package/docs/cli-reference/commands/next.md +87 -0
- package/docs/cli-reference/commands/order.md +92 -0
- package/docs/cli-reference/commands/orders.md +83 -0
- package/docs/cli-reference/commands/pay.md +103 -0
- package/docs/cli-reference/commands/readyz.md +39 -0
- package/docs/cli-reference/commands/refund/cancel.md +62 -0
- package/docs/cli-reference/commands/refund/create.md +85 -0
- package/docs/cli-reference/commands/refund/get.md +60 -0
- package/docs/cli-reference/commands/refund/index.md +33 -0
- package/docs/cli-reference/commands/refund/list.md +68 -0
- package/docs/cli-reference/commands/refund/watch.md +73 -0
- package/docs/cli-reference/commands/services/action.md +43 -0
- package/docs/cli-reference/commands/services/checkout.md +82 -0
- package/docs/cli-reference/commands/services/events.md +73 -0
- package/docs/cli-reference/commands/services/get.md +66 -0
- package/docs/cli-reference/commands/services/index.md +45 -0
- package/docs/cli-reference/commands/services/invoke.md +67 -0
- package/docs/cli-reference/commands/services/list.md +61 -0
- package/docs/cli-reference/commands/services/next.md +162 -0
- package/docs/cli-reference/commands/services/quote.md +59 -0
- package/docs/cli-reference/commands/services/read-result.md +98 -0
- package/docs/cli-reference/commands/services/start.md +53 -0
- package/docs/cli-reference/conventions.md +94 -0
- package/docs/cli-reference/index.md +64 -0
- package/package.json +1 -1
- package/skills/itpay-buyer/SKILL.md +47 -113
package/README.md
CHANGED
|
@@ -1,152 +1,102 @@
|
|
|
1
1
|
# ItPay CLI
|
|
2
2
|
|
|
3
|
-
The V3
|
|
3
|
+
The official V3 CLI for Agent-driven ItPay service discovery, checkout, delivery, order recovery, and refunds.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install -g @itpay/cli
|
|
7
|
-
itpay
|
|
8
|
-
itpay
|
|
7
|
+
itpay install --json
|
|
8
|
+
itpay --agent-type codex-desktop readyz --json
|
|
9
|
+
itpay --agent-type codex-desktop catalog list --json
|
|
9
10
|
```
|
|
10
11
|
|
|
11
|
-
The
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
12
|
+
The default API is `https://app.itpay.ai`. Set `ITPAY_BACKEND_URL` only for an intentional test or local environment.
|
|
13
|
+
|
|
14
|
+
## Output Contract
|
|
15
|
+
|
|
16
|
+
Normal JSON commands return one bounded envelope:
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
status current command state
|
|
20
|
+
result facts needed at this step
|
|
21
|
+
handoff optional human-visible URL/image fields for the current Host
|
|
22
|
+
instruction how the Agent must use the result
|
|
23
|
+
next zero or one preferred executable command
|
|
24
|
+
recovery exceptional recovery commands only
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Run `next.command` unchanged after filling only explicit placeholders or user-provided required fields. Do not inspect raw APIs or hardcode a service workflow.
|
|
28
|
+
|
|
29
|
+
Normative per-command contracts: [CLI Command Reference](docs/cli-reference/index.md).
|
|
30
|
+
|
|
31
|
+
## Supported Agent Types
|
|
32
|
+
|
|
33
|
+
| Agent Type | Default Host |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| `codex-desktop` | `codex` |
|
|
36
|
+
| `codex-cli` | `terminal` |
|
|
37
|
+
| `claude-code-desktop` | `claude-code` |
|
|
38
|
+
| `claude-code-cli` | `terminal` |
|
|
39
|
+
| `workbuddy` | `plain-chat` |
|
|
40
|
+
|
|
41
|
+
`--agent-type` identifies the stable runtime and registered Agent instance. `--host` only selects the human presentation surface. Use `itpay install <agent_type> --json` for the exact responsibility.
|
|
42
|
+
|
|
43
|
+
## Command Families
|
|
44
|
+
|
|
45
|
+
- `readyz`, `catalog list`: compatibility and discovery.
|
|
46
|
+
- `services start/invoke/action/checkout/next`: generic Service Execution flow.
|
|
47
|
+
- `cart add/show/remove/clear/next`, `buy`: canonical Cart and ordinary Checkout flow.
|
|
48
|
+
- `checkout`: authoritative payment and fulfillment recovery.
|
|
49
|
+
- `services read-result`: read one human-granted protected result.
|
|
50
|
+
- `order`, `orders`: exact order and account order views.
|
|
51
|
+
- `refund create/list/get/watch/cancel`: Refund Owner flow.
|
|
52
|
+
- `services get/events`: redacted support diagnostics; normal flows should use `services next`.
|
|
53
|
+
- `install`, `docs list/show/search`: offline packaged guidance.
|
|
54
|
+
- `pay`: operator escape hatch only; normal buyers use the ItPay Checkout page.
|
|
55
|
+
|
|
56
|
+
Run `itpay <command> --help` or browse [the command index](docs/cli-reference/index.md) for parameters.
|
|
57
|
+
|
|
58
|
+
## Recovery
|
|
59
|
+
|
|
60
|
+
Use server-backed recovery before creating another resource:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
itpay --agent-type <agent_type> next --json
|
|
64
|
+
itpay --agent-type <agent_type> services list --json
|
|
65
|
+
itpay --agent-type <agent_type> services next <service_execution_id> --json
|
|
66
|
+
itpay --agent-type <agent_type> services checkout <service_execution_id> --resume --json
|
|
67
|
+
itpay checkout --id <checkout_id> --token <display_token> --json
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The local `~/.itpay-v3` directory stores the signed Device Authority, Agent instances, idempotency operations, and recovery handles. Backend state remains authoritative.
|
|
54
71
|
|
|
55
72
|
## Environment
|
|
56
73
|
|
|
57
|
-
- `ITPAY_BACKEND_URL
|
|
58
|
-
- `
|
|
59
|
-
- `
|
|
60
|
-
- `
|
|
61
|
-
- `
|
|
62
|
-
- `
|
|
63
|
-
- `
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
`itpay order`) downloads the
|
|
87
|
-
brand checkout QR from the backend and writes it to a stable local
|
|
88
|
-
file the agent can hand to the IDE image viewer (Trae `Read` tool,
|
|
89
|
-
Codex, Claude Code). The contract is:
|
|
90
|
-
|
|
91
|
-
- canonical file: `<os.tmpdir()>/itpay-v3-qr/itpay-v3-<kind>-<id>.png`
|
|
92
|
-
(override with `ITPAY_IDE_IMAGE_DIR_OVERRIDE`)
|
|
93
|
-
- when `/tmp/itpay-v3-qr` is a separate, writable location it gets a
|
|
94
|
-
mirror of the same file under the same name
|
|
95
|
-
- filename is stable per checkout, so re-runs overwrite the same
|
|
96
|
-
local file rather than scattering copies across the scratch dir
|
|
97
|
-
|
|
98
|
-
Outputs that carry the IDE image attach:
|
|
99
|
-
|
|
100
|
-
- `itpay buy --json` — fields `brand_qr_local_path`, `brand_qr_mirrors`,
|
|
101
|
-
`brand_qr_stable_name`, `brand_qr_status` (`downloaded` / `failed`
|
|
102
|
-
/ `disabled` / `fallback`), `brand_qr_error`, `brand_qr_data_url`,
|
|
103
|
-
`brand_qr_must_render_reason`, `brand_qr_render_action`. Read the
|
|
104
|
-
path with the IDE's `Read` tool so the human sees the picture.
|
|
105
|
-
- `itpay services checkout --json` — same brand QR fields, plus
|
|
106
|
-
`next_action: "open_human_checkout"` and the checkout-scoped
|
|
107
|
-
`display_token`. Agents must show this ItPay checkout QR/URL to the
|
|
108
|
-
human and must not call `itpay pay` for the normal buyer flow.
|
|
109
|
-
- Markdown (Trae / Codex / Claude Code) — inlines a `data:image/png;base64,...`
|
|
110
|
-
copy of the picture plus a `[ATTACH] IDE image` reference block that
|
|
111
|
-
points at the canonical local path and mirrors.
|
|
112
|
-
- Terminal — prints `Branded QR: /path/...png` and `QR mirrors: ...`;
|
|
113
|
-
iTerm inline image renders the same file when the session supports it.
|
|
114
|
-
- Telegram — the `ide_image_attach` block on `presentation.ide_image_attach`
|
|
115
|
-
carries `status`, `local_path`, `mirrors`, `mime_type`, `source`,
|
|
116
|
-
`caption`, `error` (when failed), `must_render_reason`, and a
|
|
117
|
-
step-by-step `instructions` array.
|
|
118
|
-
- Feishu / Lark — same `ide_image_attach` block on the
|
|
119
|
-
`message.ide_image_attach` envelope.
|
|
120
|
-
|
|
121
|
-
Disable the contract with `ITPAY_IDE_IMAGE_ATTACH=0` for runners on a
|
|
122
|
-
read-only filesystem. The plan carries `status: "disabled"` instead
|
|
123
|
-
of `status: "downloaded"` and no PNG is downloaded.
|
|
124
|
-
|
|
125
|
-
## Layout
|
|
126
|
-
|
|
127
|
-
- `src/main.ts` — `commander` entrypoint, command registration
|
|
128
|
-
- `src/client/` — HTTP/JSON client and DTOs
|
|
129
|
-
- `src/commands/` — one file per command family
|
|
130
|
-
- `src/render/` — terminal formatting
|
|
131
|
-
- `plan.ts` — `RenderPlan` contract shared by all renderers
|
|
132
|
-
- `qr.ts` — local QR + format selection
|
|
133
|
-
- `terminal.ts`, `markdown.ts`, `plain_chat.ts`, `telegram.ts`, `feishu.ts` — per-host renderers
|
|
134
|
-
- `index.ts` — `dispatchRender()` picks the right renderer
|
|
135
|
-
- `sink.ts` — `OutputSink` so tests can silence stdout
|
|
136
|
-
- `src/state/` — local CLI config, cart session, client context
|
|
137
|
-
- `tests/` — node:test smoke test + in-process mock backend
|
|
138
|
-
|
|
139
|
-
## Rules
|
|
140
|
-
|
|
141
|
-
- commands orchestrate user intent only
|
|
142
|
-
- render code must not issue HTTP calls
|
|
143
|
-
- keep API access under `src/client/`
|
|
144
|
-
- persist checkout-scoped `display_token` and last server handles only in the owner-only local cart session file
|
|
145
|
-
- the default `buy` command must not create a payment intent unless `--pay` is explicit
|
|
146
|
-
- the default `services checkout` command must render the ItPay
|
|
147
|
-
checkout handoff; provider payment intents are created by the human
|
|
148
|
-
checkout page, not by the agent
|
|
149
|
-
- a renderer must consume the brand QR the V3 backend hands back
|
|
150
|
-
(`qr_payload` / `qr_png_url` / `mobile_wallet_url`) and only
|
|
151
|
-
self-generate a QR for `auth_qr` / `checkout_qr` with the explicit
|
|
152
|
-
`--qr-file` opt-in
|
|
74
|
+
- `ITPAY_BACKEND_URL`: deliberate API override.
|
|
75
|
+
- `ITPAY_AGENT_TYPE`: stable alternative to global `--agent-type`.
|
|
76
|
+
- `ITPAY_BEARER_TOKEN`: account-scoped Buyer session for account-only commands such as `orders`.
|
|
77
|
+
- `ITPAY_CART_SESSION_PATH`: local recovery-state path override.
|
|
78
|
+
- `ITPAY_CURRENCY`: ordinary Cart currency, default `CNY`.
|
|
79
|
+
- `ITPAY_IDEMPOTENCY_KEY`: explicit operation key for deterministic testing; normal use persists operation IDs automatically.
|
|
80
|
+
- `ITPAY_IDE_IMAGE_ATTACH=0`: disable local Checkout image download when the runtime filesystem is read-only.
|
|
81
|
+
- `ITPAY_IDE_IMAGE_DIR_OVERRIDE`: override the local Checkout image directory.
|
|
82
|
+
|
|
83
|
+
Provider credentials, Buyer identity, payment provider choice, amount, refund policy, quota, grant scope, and delivery access are never client-owned environment settings.
|
|
84
|
+
|
|
85
|
+
## Development
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npm run lint
|
|
89
|
+
npm test
|
|
90
|
+
npm run test:package
|
|
91
|
+
npm run pack:dry-run
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Source boundaries:
|
|
95
|
+
|
|
96
|
+
- `src/main.ts`: parser and error-envelope wiring.
|
|
97
|
+
- `src/client`: typed HTTP access.
|
|
98
|
+
- `src/commands`: command orchestration and public output projection.
|
|
99
|
+
- `src/render`: Host presentation only; no business HTTP.
|
|
100
|
+
- `src/state`: local Device Authority, idempotency journal, and recovery handles.
|
|
101
|
+
- `docs/cli-reference`: normative command contracts.
|
|
102
|
+
- `docs/agent/buyer`: packaged progressive workflow guidance.
|
|
@@ -32,17 +32,16 @@ export class BackendClient {
|
|
|
32
32
|
return this.http.delete(`/v1/carts/${encodeURIComponent(cartID)}`);
|
|
33
33
|
}
|
|
34
34
|
// --- Checkout ---
|
|
35
|
-
createCheckout(input) {
|
|
36
|
-
return this.http.post("/v1/checkouts", input);
|
|
35
|
+
createCheckout(input, idempotencyKey) {
|
|
36
|
+
return this.http.post("/v1/checkouts", input, idempotencyKey ? { idempotencyKey } : undefined);
|
|
37
37
|
}
|
|
38
38
|
getCheckoutPresentation(checkoutID, displayToken) {
|
|
39
39
|
const qs = new URLSearchParams({ display_token: displayToken });
|
|
40
40
|
return this.http.get(`/v1/checkouts/${encodeURIComponent(checkoutID)}/presentation?${qs}`);
|
|
41
41
|
}
|
|
42
42
|
// --- Payment intents ---
|
|
43
|
-
createPaymentIntent(checkoutID, input
|
|
44
|
-
|
|
45
|
-
return this.http.post(`/v1/checkouts/${encodeURIComponent(checkoutID)}/payment-intents`, input, options);
|
|
43
|
+
createPaymentIntent(checkoutID, input) {
|
|
44
|
+
return this.http.post(`/v1/checkouts/${encodeURIComponent(checkoutID)}/payment-intents`, input);
|
|
46
45
|
}
|
|
47
46
|
// --- SSE streaming ---
|
|
48
47
|
streamCheckoutEvents(checkoutID, displayToken, onEvent, signal) {
|
|
@@ -54,6 +53,9 @@ export class BackendClient {
|
|
|
54
53
|
getOrder(orderID) {
|
|
55
54
|
return this.http.get(`/v1/orders/${encodeURIComponent(orderID)}`);
|
|
56
55
|
}
|
|
56
|
+
getOrderDeliveryAccess(orderID) {
|
|
57
|
+
return this.http.get(`/v1/orders/${encodeURIComponent(orderID)}/delivery-access`);
|
|
58
|
+
}
|
|
57
59
|
listAccountOrders(limit, status, bearer) {
|
|
58
60
|
const qs = new URLSearchParams({ limit: String(limit) });
|
|
59
61
|
if (status) {
|
|
@@ -62,10 +64,19 @@ export class BackendClient {
|
|
|
62
64
|
return this.http.get(`/v1/me/orders?${qs}`, bearer ? { bearer } : {});
|
|
63
65
|
}
|
|
64
66
|
// --- Refund ---
|
|
65
|
-
createRefund(orderID, input, idempotencyKey) {
|
|
66
|
-
const options = idempotencyKey ? { idempotencyKey } : {};
|
|
67
|
+
createRefund(orderID, input, bearer, idempotencyKey) {
|
|
68
|
+
const options = { ...(bearer ? { bearer } : {}), ...(idempotencyKey ? { idempotencyKey } : {}) };
|
|
67
69
|
return this.http.post(`/v1/orders/${encodeURIComponent(orderID)}/refunds`, input, options);
|
|
68
70
|
}
|
|
71
|
+
listOrderRefunds(orderID) {
|
|
72
|
+
return this.http.get(`/v1/orders/${encodeURIComponent(orderID)}/refunds`);
|
|
73
|
+
}
|
|
74
|
+
getRefund(refundRequestID) {
|
|
75
|
+
return this.http.get(`/v1/refunds/${encodeURIComponent(refundRequestID)}`);
|
|
76
|
+
}
|
|
77
|
+
cancelRefund(refundRequestID, reason = "buyer_cancelled") {
|
|
78
|
+
return this.http.post(`/v1/refunds/${encodeURIComponent(refundRequestID)}/cancel`, { reason });
|
|
79
|
+
}
|
|
69
80
|
// --- Service Execution ---
|
|
70
81
|
startServiceExecution(input) {
|
|
71
82
|
return this.http.post("/v1/service-executions", input);
|
|
@@ -79,14 +90,21 @@ export class BackendClient {
|
|
|
79
90
|
createServiceExecutionCheckout(serviceExecutionID, input) {
|
|
80
91
|
return this.http.post(`/v1/service-executions/${encodeURIComponent(serviceExecutionID)}/checkout`, input);
|
|
81
92
|
}
|
|
93
|
+
prepareServiceQuote(serviceExecutionID, input) {
|
|
94
|
+
return this.http.post(`/v1/service-executions/${encodeURIComponent(serviceExecutionID)}/quotes`, input);
|
|
95
|
+
}
|
|
82
96
|
getServiceExecution(serviceExecutionID) {
|
|
83
97
|
return this.http.get(`/v1/service-executions/${encodeURIComponent(serviceExecutionID)}`);
|
|
84
98
|
}
|
|
85
99
|
listServiceExecutions(limit = 50) {
|
|
86
100
|
return this.http.get(`/v1/service-executions?limit=${limit}`);
|
|
87
101
|
}
|
|
88
|
-
listServiceExecutionEvents(serviceExecutionID) {
|
|
89
|
-
|
|
102
|
+
listServiceExecutionEvents(serviceExecutionID, afterSequence = 0, limit = 50) {
|
|
103
|
+
const query = new URLSearchParams({
|
|
104
|
+
after_sequence: String(afterSequence),
|
|
105
|
+
limit: String(limit),
|
|
106
|
+
});
|
|
107
|
+
return this.http.get(`/v1/service-executions/${encodeURIComponent(serviceExecutionID)}/events?${query}`);
|
|
90
108
|
}
|
|
91
109
|
getGrantedServiceResult(serviceExecutionID) {
|
|
92
110
|
return this.http.get(`/v1/service-executions/${encodeURIComponent(serviceExecutionID)}/granted-result`);
|
package/dist/src/commands/buy.js
CHANGED
|
@@ -11,54 +11,55 @@
|
|
|
11
11
|
import { operationID } from "../state/config.js";
|
|
12
12
|
import { validateContext } from "../state/client_context.js";
|
|
13
13
|
import { dispatchRender } from "../render/index.js";
|
|
14
|
-
import {
|
|
15
|
-
import { ensureIdeImageAttach
|
|
14
|
+
import { platformKeyForHost } from "../render/plan.js";
|
|
15
|
+
import { ensureIdeImageAttach } from "../render/ide.js";
|
|
16
16
|
import { buildAgentChatHandoff } from "../render/markdown.js";
|
|
17
|
+
import { formatMoney } from "../render/output.js";
|
|
18
|
+
import { CommandContractError, writeCommandEnvelope } from "./guidance.js";
|
|
17
19
|
export async function runBuy(backend, config, options) {
|
|
18
20
|
const err = validateContext(options.host, options.target);
|
|
19
21
|
if (err) {
|
|
20
22
|
throw new Error(`${err.code}: ${err.message}`);
|
|
21
23
|
}
|
|
22
24
|
const snap = options.cartSession.show();
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
const resumableCartID = !options.cartID && snap.items.length === 0 ? snap.lastCartID : undefined;
|
|
26
|
+
if (!options.cartID && !resumableCartID && snap.items.length === 0) {
|
|
27
|
+
throw new CommandContractError("cart_empty", "no local draft or canonical cart is available", "没有可购买的普通 Cart;从已发布目录选择项目,不要猜测 item、variant 或 offer。", [{ command: "itpay catalog list --json", reason: "读取已发布项目" }]);
|
|
25
28
|
}
|
|
26
29
|
const missingContactFields = findMissingContactFields(options.contact, options.requiredContactFields ?? []);
|
|
27
30
|
if (missingContactFields.length > 0) {
|
|
28
|
-
|
|
29
|
-
await dispatchInteractionRequest(options.host, interactionRequest, {
|
|
30
|
-
...(options.isTTY !== undefined ? { isTTY: options.isTTY } : {}),
|
|
31
|
-
...(options.target ? { target: options.target } : {}),
|
|
32
|
-
...(options.output ? { output: options.output } : {}),
|
|
33
|
-
});
|
|
34
|
-
return {
|
|
35
|
-
kind: "interaction_requested",
|
|
36
|
-
interactionRequest,
|
|
37
|
-
};
|
|
31
|
+
throw new CommandContractError("missing_contact", `missing required contact fields: ${missingContactFields.join(", ")}`, `向用户询问 ${missingContactFields.join(" 和 ")},然后在同一 buy 命令补充对应 contact 参数;禁止编造。`, [{ command: "itpay buy --help", reason: "查看 contact 参数" }]);
|
|
38
32
|
}
|
|
39
33
|
let cart;
|
|
40
|
-
if (options.cartID) {
|
|
41
|
-
cart = await backend.getCart(options.cartID);
|
|
34
|
+
if (options.cartID || resumableCartID) {
|
|
35
|
+
cart = await backend.getCart(options.cartID ?? resumableCartID);
|
|
42
36
|
}
|
|
43
37
|
else {
|
|
44
38
|
const request = options.cartSession.toCreateCartRequest();
|
|
45
39
|
request.client_context = {
|
|
46
40
|
host: options.host,
|
|
47
41
|
target: options.target,
|
|
48
|
-
agent_device_id: options.cartSession.ensureAgentDeviceID(config.agentDeviceID),
|
|
49
42
|
};
|
|
50
|
-
const agentDeviceID = options.cartSession.ensureAgentDeviceID(config.agentDeviceID);
|
|
51
|
-
request.agent_device_id = agentDeviceID;
|
|
52
43
|
cart = await backend.createCart(request);
|
|
53
44
|
}
|
|
45
|
+
const latestLine = cart.items[cart.items.length - 1];
|
|
46
|
+
options.cartSession.rememberServerCart({
|
|
47
|
+
cartID: cart.cart_id,
|
|
48
|
+
...(latestLine?.cart_item_id ? { cartItemID: latestLine.cart_item_id } : {}),
|
|
49
|
+
...(latestLine?.service_execution_id ? { serviceExecutionID: latestLine.service_execution_id } : {}),
|
|
50
|
+
});
|
|
51
|
+
const unquotedServiceLine = cart.items.find((item) => item.service_execution_id && !item.service_quote_lock_id);
|
|
52
|
+
if (unquotedServiceLine?.service_execution_id) {
|
|
53
|
+
throw new CommandContractError("service_quote_required", `cart ${cart.cart_id} contains an unquoted service-backed line`, "该服务项目尚未绑定 Quote Lock;回到来源 Execution 完成候选选择和报价。", [{ command: `itpay services next ${unquotedServiceLine.service_execution_id} --json`, reason: "读取当前合法动作" }]);
|
|
54
|
+
}
|
|
55
|
+
const idempotencyKey = await operationID(config, `checkout.create:${cart.cart_id}`);
|
|
54
56
|
const checkoutRequest = {
|
|
55
57
|
cart_id: cart.cart_id,
|
|
56
|
-
client_reference_id: options.clientReferenceID ??
|
|
58
|
+
client_reference_id: options.clientReferenceID ?? idempotencyKey,
|
|
57
59
|
...(options.contact ? { delivery_contact: options.contact } : {}),
|
|
58
60
|
};
|
|
59
|
-
const checkout = await backend.createCheckout(checkoutRequest);
|
|
61
|
+
const checkout = await backend.createCheckout(checkoutRequest, idempotencyKey);
|
|
60
62
|
options.cartSession.rememberCheckout({
|
|
61
|
-
cartID: cart.cart_id,
|
|
62
63
|
checkoutID: checkout.checkout.checkout_id,
|
|
63
64
|
displayToken: checkout.display_token,
|
|
64
65
|
checkoutURL: tokenizedCheckoutURL(checkout.checkout_url, checkout.display_token, checkout.qr_payload),
|
|
@@ -77,7 +78,7 @@ export async function runBuy(backend, config, options) {
|
|
|
77
78
|
let waitStatus = "skipped";
|
|
78
79
|
if (options.pay) {
|
|
79
80
|
const method = options.payMethod ?? "alipay";
|
|
80
|
-
paymentIntent = await backend.createPaymentIntent(checkoutID, { payment_method_type: method, display_token: displayToken }
|
|
81
|
+
paymentIntent = await backend.createPaymentIntent(checkoutID, { payment_method_type: method, display_token: displayToken });
|
|
81
82
|
if (!options.jsonOutput) {
|
|
82
83
|
process.stdout.write("\n--- payment intent ---\n");
|
|
83
84
|
process.stdout.write(` id: ${paymentIntent.payment_intent_id}\n`);
|
|
@@ -124,34 +125,23 @@ export async function runBuy(backend, config, options) {
|
|
|
124
125
|
});
|
|
125
126
|
// --- Output ---
|
|
126
127
|
if (options.jsonOutput) {
|
|
127
|
-
const
|
|
128
|
-
checkout: { ...checkout, checkout_url: checkoutURL },
|
|
128
|
+
const envelope = buildBuyEnvelope({
|
|
129
129
|
cart,
|
|
130
|
-
|
|
130
|
+
checkoutID,
|
|
131
|
+
checkoutURL,
|
|
132
|
+
displayToken,
|
|
131
133
|
plan,
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
jsonInput.ideImageAttach = {
|
|
138
|
-
localPath: plan.ideImageAttach.localPath,
|
|
139
|
-
mirrors: plan.ideImageAttach.mirrors,
|
|
140
|
-
mimeType: plan.ideImageAttach.mimeType,
|
|
141
|
-
...(plan.ideImageAttach.caption ? { caption: plan.ideImageAttach.caption } : {}),
|
|
142
|
-
mustRenderReason: plan.ideImageAttach.mustRenderReason,
|
|
143
|
-
status: plan.ideImageAttach.status,
|
|
144
|
-
...(plan.ideImageAttach.error ? { error: plan.ideImageAttach.error } : {}),
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
const json = buildJSONOutput(jsonInput);
|
|
148
|
-
(options.output ?? ((line) => process.stdout.write(line + "\n")))(JSON.stringify(json, null, 2) + "\n");
|
|
134
|
+
waitStatus,
|
|
135
|
+
...(checkout.qr_png_url ? { qrPNGURL: checkout.qr_png_url } : {}),
|
|
136
|
+
...(paymentIntent ? { paymentIntent } : {}),
|
|
137
|
+
});
|
|
138
|
+
writeCommandEnvelope(envelope, { jsonOutput: true, ...(options.output ? { output: options.output } : {}) });
|
|
149
139
|
return {
|
|
150
140
|
kind: "checkout_rendered",
|
|
151
141
|
plan,
|
|
152
142
|
checkoutID,
|
|
153
143
|
displayToken,
|
|
154
|
-
|
|
144
|
+
envelope,
|
|
155
145
|
};
|
|
156
146
|
}
|
|
157
147
|
// Text output — always render QR for non-JSON mode
|
|
@@ -173,6 +163,55 @@ export async function runBuy(backend, config, options) {
|
|
|
173
163
|
displayToken,
|
|
174
164
|
};
|
|
175
165
|
}
|
|
166
|
+
function buildBuyEnvelope(input) {
|
|
167
|
+
const verified = input.waitStatus === "verified";
|
|
168
|
+
const platform = platformKeyForHost(input.plan.host);
|
|
169
|
+
const handoff = { url: input.checkoutURL };
|
|
170
|
+
if (input.plan.ideImageAttach?.status === "downloaded" && input.plan.ideImageAttach.localPath) {
|
|
171
|
+
handoff.qr_local_path = input.plan.ideImageAttach.localPath;
|
|
172
|
+
}
|
|
173
|
+
if (platform === "markdown") {
|
|
174
|
+
handoff.markdown = buildAgentChatHandoff(input.plan).markdown;
|
|
175
|
+
}
|
|
176
|
+
else if (platform === "plain_chat" && input.qrPNGURL) {
|
|
177
|
+
handoff.qr_image_url = input.qrPNGURL;
|
|
178
|
+
}
|
|
179
|
+
const result = {
|
|
180
|
+
checkout_id: input.checkoutID,
|
|
181
|
+
payment: verified ? "verified" : "pending",
|
|
182
|
+
amount: formatMoney(input.cart.amount_minor, input.cart.currency),
|
|
183
|
+
item_count: input.cart.items.length,
|
|
184
|
+
...(input.paymentIntent ? {
|
|
185
|
+
payment_intent_id: input.paymentIntent.payment_intent_id,
|
|
186
|
+
payment_intent_status: input.paymentIntent.status,
|
|
187
|
+
} : {}),
|
|
188
|
+
...(input.waitStatus === "timeout" ? { wait_status: "timeout" } : {}),
|
|
189
|
+
};
|
|
190
|
+
if (verified) {
|
|
191
|
+
return {
|
|
192
|
+
status: "payment_event_observed",
|
|
193
|
+
result,
|
|
194
|
+
instruction: "已观察到付款确认事件;读取同一 Checkout 的权威完成状态,不要再次付款。",
|
|
195
|
+
next: { command: `itpay checkout --id ${input.checkoutID} --token ${input.displayToken} --json`, reason: "读取订单和履约句柄" },
|
|
196
|
+
recovery: [],
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
status: "human_checkout_required",
|
|
201
|
+
result,
|
|
202
|
+
handoff,
|
|
203
|
+
instruction: buyHandoffInstruction(platform),
|
|
204
|
+
next: { command: `itpay checkout --id ${input.checkoutID} --token ${input.displayToken} --json`, reason: "稍后查询同一笔 Checkout 状态" },
|
|
205
|
+
recovery: [],
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function buyHandoffInstruction(platform) {
|
|
209
|
+
if (platform === "markdown")
|
|
210
|
+
return "把 handoff.markdown 原样发送到当前桌面对话;二维码和链接可见后等待用户操作,不要创建新 Checkout。";
|
|
211
|
+
if (platform === "terminal")
|
|
212
|
+
return "在用户可见终端展示 handoff 中的付款入口,然后等待用户操作;不要创建新 Checkout。";
|
|
213
|
+
return "把 handoff.url 和可用二维码附件发送给用户,然后等待用户操作;不要创建新 Checkout。";
|
|
214
|
+
}
|
|
176
215
|
// --- SSE wait for payment verification ---
|
|
177
216
|
async function waitForPaymentSSE(backend, checkoutID, displayToken, timeoutSec) {
|
|
178
217
|
return new Promise((resolve) => {
|
|
@@ -194,81 +233,11 @@ async function waitForPaymentSSE(backend, checkoutID, displayToken, timeoutSec)
|
|
|
194
233
|
});
|
|
195
234
|
});
|
|
196
235
|
}
|
|
197
|
-
export function buildJSONOutput(input) {
|
|
198
|
-
const output = {
|
|
199
|
-
kind: "checkout_created",
|
|
200
|
-
checkout_id: input.checkout.checkout.checkout_id,
|
|
201
|
-
checkout_url: input.checkout.checkout_url,
|
|
202
|
-
display_token: input.checkout.display_token,
|
|
203
|
-
qr_payload: input.checkout.qr_payload,
|
|
204
|
-
checkout_status: input.checkout.checkout.status,
|
|
205
|
-
wait_status: input.waitStatus,
|
|
206
|
-
};
|
|
207
|
-
if (input.checkout.qr_png_url) {
|
|
208
|
-
output.qr_png_url = input.checkout.qr_png_url;
|
|
209
|
-
}
|
|
210
|
-
const serviceExecutions = input.cart?.items
|
|
211
|
-
.filter((item) => item.service_execution_id)
|
|
212
|
-
.map((item) => ({
|
|
213
|
-
service_execution_id: item.service_execution_id,
|
|
214
|
-
...(item.service_capability_id ? { service_capability_id: item.service_capability_id } : {}),
|
|
215
|
-
...(item.title ? { title: item.title } : {}),
|
|
216
|
-
}));
|
|
217
|
-
if (serviceExecutions && serviceExecutions.length > 0) {
|
|
218
|
-
output.service_executions = serviceExecutions;
|
|
219
|
-
}
|
|
220
|
-
if (input.paymentIntent) {
|
|
221
|
-
output.payment_intent_id = input.paymentIntent.payment_intent_id;
|
|
222
|
-
output.payment_status = input.paymentIntent.status;
|
|
223
|
-
if (input.paymentIntent.action) {
|
|
224
|
-
const pa = {};
|
|
225
|
-
if (input.paymentIntent.action.qr_image_url)
|
|
226
|
-
pa.qr_image_url = input.paymentIntent.action.qr_image_url;
|
|
227
|
-
if (input.paymentIntent.action.mobile_wallet_url)
|
|
228
|
-
pa.mobile_wallet_url = input.paymentIntent.action.mobile_wallet_url;
|
|
229
|
-
output.payment_action = pa;
|
|
230
|
-
}
|
|
231
|
-
output.kind = "payment_handoff_required";
|
|
232
|
-
}
|
|
233
|
-
if (input.waitStatus === "verified") {
|
|
234
|
-
output.kind = "payment_verified";
|
|
235
|
-
}
|
|
236
|
-
if (input.ideImageAttach) {
|
|
237
|
-
output.brand_qr_status = input.ideImageAttach.status;
|
|
238
|
-
if (input.ideImageAttach.localPath) {
|
|
239
|
-
output.brand_qr_local_path = input.ideImageAttach.localPath;
|
|
240
|
-
const dataURL = readFileAsDataURL(input.ideImageAttach.localPath, input.ideImageAttach.mimeType);
|
|
241
|
-
if (dataURL)
|
|
242
|
-
output.brand_qr_data_url = dataURL;
|
|
243
|
-
const stableName = input.ideImageAttach.localPath.split("/").pop();
|
|
244
|
-
if (stableName)
|
|
245
|
-
output.brand_qr_stable_name = stableName;
|
|
246
|
-
}
|
|
247
|
-
if (input.ideImageAttach.mirrors.length > 0) {
|
|
248
|
-
output.brand_qr_mirrors = [...input.ideImageAttach.mirrors];
|
|
249
|
-
}
|
|
250
|
-
output.brand_qr_mime_type = input.ideImageAttach.mimeType;
|
|
251
|
-
if (input.ideImageAttach.caption)
|
|
252
|
-
output.brand_qr_caption = input.ideImageAttach.caption;
|
|
253
|
-
if (input.ideImageAttach.error)
|
|
254
|
-
output.brand_qr_error = input.ideImageAttach.error;
|
|
255
|
-
output.brand_qr_must_render_reason = input.ideImageAttach.mustRenderReason;
|
|
256
|
-
output.brand_qr_render_action = "agent_must_read_local_path_into_ide_chat";
|
|
257
|
-
}
|
|
258
|
-
output.next = input.waitStatus === "verified"
|
|
259
|
-
? "itpay orders"
|
|
260
|
-
: `itpay checkout --id ${input.checkout.checkout.checkout_id} --token ${input.checkout.display_token}`;
|
|
261
|
-
if (input.plan)
|
|
262
|
-
output.agent_action = buildAgentChatHandoff(input.plan);
|
|
263
|
-
return output;
|
|
264
|
-
}
|
|
265
236
|
// --- checkout QR plan ---
|
|
266
237
|
export function buildCheckoutQRPlan(input) {
|
|
267
238
|
const summary = `Scan the QR or open ${input.checkoutURL} to start the human checkout flow.`;
|
|
268
239
|
const isPayment = input.paymentIntentID != null;
|
|
269
|
-
const afterCommand =
|
|
270
|
-
? `itpay checkout --id ${input.checkoutID} --token ${input.displayToken}`
|
|
271
|
-
: `itpay checkout --id ${input.checkoutID} --token ${input.displayToken}`;
|
|
240
|
+
const afterCommand = `itpay checkout --id ${input.checkoutID} --token ${input.displayToken} --json`;
|
|
272
241
|
const platform = {
|
|
273
242
|
text: summary,
|
|
274
243
|
links: [
|
|
@@ -307,23 +276,6 @@ export function buildCheckoutQRPlan(input) {
|
|
|
307
276
|
return plan;
|
|
308
277
|
}
|
|
309
278
|
// --- contact field interaction ---
|
|
310
|
-
export function buildMissingContactInteractionRequest(fields) {
|
|
311
|
-
return {
|
|
312
|
-
kind: "input",
|
|
313
|
-
id: "collect_delivery_contact",
|
|
314
|
-
title: "Collect buyer contact",
|
|
315
|
-
prompt: "Before creating the checkout, ask the buyer to provide the missing contact details.",
|
|
316
|
-
fields: fields.map((field) => ({
|
|
317
|
-
id: field,
|
|
318
|
-
label: field === "email" ? "Email" : "Phone number",
|
|
319
|
-
inputType: field === "email" ? "email" : "phone",
|
|
320
|
-
required: true,
|
|
321
|
-
placeholder: field === "email" ? "buyer@example.com" : "+86 138...",
|
|
322
|
-
description: field === "email" ? "Used for receipts or delivery follow-up." : "Used when the order requires buyer verification.",
|
|
323
|
-
})),
|
|
324
|
-
submitLabel: "Submit contact",
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
279
|
function findMissingContactFields(contact, fields) {
|
|
328
280
|
return fields.filter((field) => {
|
|
329
281
|
const value = contact?.[field];
|