@itpay/cli 2.0.3 → 2.0.7
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 +96 -142
- package/dist/src/client/backend.js +26 -8
- package/dist/src/client/http.js +29 -23
- package/dist/src/commands/buy.js +84 -132
- package/dist/src/commands/cart.js +274 -169
- package/dist/src/commands/catalog.js +64 -38
- package/dist/src/commands/checkout.js +128 -79
- package/dist/src/commands/docs.js +97 -51
- package/dist/src/commands/guidance.js +112 -16
- 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 -11
- package/dist/src/commands/services.js +799 -147
- package/dist/src/commands/skill.js +55 -0
- package/dist/src/main.js +820 -193
- package/dist/src/render/output.js +2 -3
- package/dist/src/state/agent_type.js +19 -0
- package/dist/src/state/cart_session.js +13 -17
- package/dist/src/state/client_context.js +4 -2
- package/dist/src/state/config.js +5 -15
- package/dist/src/state/device_authority.js +175 -57
- package/docs/agent/buyer/cart-checkout.json +27 -83
- package/docs/agent/buyer/catalog-list.json +2 -1
- package/docs/agent/buyer/identity-and-sessions.json +64 -0
- package/docs/agent/buyer/install-and-setup.json +35 -65
- package/docs/agent/buyer/orders-refunds.json +31 -53
- package/docs/agent/buyer/payment-flow.json +28 -57
- package/docs/agent/buyer/quickstart.json +46 -161
- package/docs/agent/buyer/render-hosts.json +43 -57
- package/docs/cli-reference/agent-types.md +51 -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/device.md +13 -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 +114 -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 +38 -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 +48 -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 +181 -0
- package/docs/cli-reference/commands/services/quote.md +63 -0
- package/docs/cli-reference/commands/services/read-result.md +98 -0
- package/docs/cli-reference/commands/services/start.md +55 -0
- package/docs/cli-reference/commands/skill.md +17 -0
- package/docs/cli-reference/conventions.md +97 -0
- package/docs/cli-reference/index.md +65 -0
- package/package.json +1 -1
- package/skills/itpay-buyer/SKILL.md +71 -110
|
@@ -1,97 +1,137 @@
|
|
|
1
|
-
// Reads
|
|
2
|
-
// and
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
1
|
+
// Reads one canonical Checkout presentation. This command never creates a
|
|
2
|
+
// Checkout and only prepares a payment handoff while the Checkout is pending.
|
|
3
|
+
import { ensureIdeImageAttach } from "../render/ide.js";
|
|
4
|
+
import { buildAgentChatHandoff } from "../render/markdown.js";
|
|
5
|
+
import { platformKeyForHost } from "../render/plan.js";
|
|
6
|
+
import { renderTerminalQR } from "../render/qr.js";
|
|
7
7
|
import { DEFAULT_BASE_URL } from "../state/config.js";
|
|
8
|
+
import { buildCheckoutQRPlan } from "./buy.js";
|
|
9
|
+
import { writeCommandEnvelope } from "./guidance.js";
|
|
8
10
|
export async function runCheckoutPresentation(backend, options) {
|
|
9
|
-
const out = resolveOutput(options.output);
|
|
10
11
|
const presentation = await backend.getCheckoutPresentation(options.checkoutID, options.displayToken);
|
|
12
|
+
const host = options.host ?? "terminal";
|
|
13
|
+
if (!checkoutNeedsHumanHandoff(presentation.checkout.status)) {
|
|
14
|
+
const envelope = terminalCheckoutEnvelope(presentation);
|
|
15
|
+
writeCommandEnvelope(envelope, {
|
|
16
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
17
|
+
...(options.output ? { output: options.output } : {}),
|
|
18
|
+
plainResult: checkoutPlainResult(envelope.result),
|
|
19
|
+
});
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
11
22
|
const checkoutURL = checkoutPageURL(options.baseURL, options.checkoutID, options.displayToken);
|
|
12
|
-
const qrPNGURL = presentation.qr_png_url ?? checkoutQRPNGURL(options.baseURL, options.checkoutID, options.displayToken);
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
host
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
const qrPNGURL = absolutePublicURL(options.baseURL, presentation.qr_png_url ?? checkoutQRPNGURL(options.baseURL, options.checkoutID, options.displayToken));
|
|
24
|
+
const nextCommand = `itpay checkout --id ${options.checkoutID} --token ${options.displayToken} --json`;
|
|
25
|
+
const plan = buildCheckoutQRPlan({
|
|
26
|
+
host,
|
|
27
|
+
checkoutID: options.checkoutID,
|
|
28
|
+
checkoutURL,
|
|
29
|
+
displayToken: options.displayToken,
|
|
30
|
+
qrPayload: checkoutURL,
|
|
31
|
+
qrPNGURL,
|
|
32
|
+
nextAction: presentation.checkout.next_action,
|
|
33
|
+
orderItems: presentation.items.map((item) => ({
|
|
34
|
+
title: item.title,
|
|
35
|
+
quantity: item.quantity,
|
|
36
|
+
amountMinor: item.amount_minor,
|
|
37
|
+
currency: item.currency,
|
|
38
|
+
})),
|
|
39
|
+
orderCurrency: presentation.checkout.currency,
|
|
40
|
+
});
|
|
26
41
|
await ensureIdeImageAttach(plan, {
|
|
27
42
|
...(options.baseURL ? { baseURL: options.baseURL } : {}),
|
|
28
43
|
});
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
out(renderCheckoutText(presentation) + "\n");
|
|
34
|
-
if (plan.ideImageAttach) {
|
|
35
|
-
out(ideImageAttachBlock(plan.ideImageAttach).filter((l) => l.length > 0).join("\n") + "\n");
|
|
36
|
-
}
|
|
37
|
-
out(`hint: ${hintFor("checkout", presentation.checkout.status)}\n`);
|
|
44
|
+
const envelope = pendingCheckoutEnvelope(presentation, checkoutURL, plan, nextCommand);
|
|
45
|
+
const plainResult = checkoutPlainResult(envelope.result);
|
|
46
|
+
if (!options.jsonOutput && platformKeyForHost(host) === "terminal") {
|
|
47
|
+
plainResult.push("qr:", await renderTerminalQR(checkoutURL, "terminal"));
|
|
38
48
|
}
|
|
49
|
+
writeCommandEnvelope(envelope, {
|
|
50
|
+
...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
|
|
51
|
+
...(options.output ? { output: options.output } : {}),
|
|
52
|
+
plainResult,
|
|
53
|
+
});
|
|
39
54
|
}
|
|
40
|
-
function
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
lines.push(` next_action: ${c.next_action}`);
|
|
46
|
-
lines.push(` amount: ${formatMoney(c.amount_minor, c.currency)}`);
|
|
47
|
-
lines.push(` buyer: ${presentation.buyer_session.state}`);
|
|
48
|
-
if (presentation.items.length > 0) {
|
|
49
|
-
lines.push(" items:");
|
|
50
|
-
for (const item of presentation.items) {
|
|
51
|
-
lines.push(` - ${item.title} × ${item.quantity} (${formatMoney(item.amount_minor, item.currency)})`);
|
|
52
|
-
}
|
|
55
|
+
function pendingCheckoutEnvelope(presentation, checkoutURL, plan, nextCommand) {
|
|
56
|
+
const platform = platformKeyForHost(plan.host);
|
|
57
|
+
const handoff = { url: checkoutURL };
|
|
58
|
+
if (plan.ideImageAttach?.status === "downloaded" && plan.ideImageAttach.localPath) {
|
|
59
|
+
handoff.qr_local_path = plan.ideImageAttach.localPath;
|
|
53
60
|
}
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
for (const intent of presentation.payment_intents) {
|
|
57
|
-
lines.push(` - ${intent.payment_intent_id} ${intent.status} (${intent.payment_method_type}, ${formatMoney(intent.amount_minor, intent.currency)})`);
|
|
58
|
-
}
|
|
61
|
+
if (platform === "markdown") {
|
|
62
|
+
handoff.markdown = buildAgentChatHandoff(plan).markdown;
|
|
59
63
|
}
|
|
60
|
-
|
|
64
|
+
else if (platform === "plain_chat" && plan.preferredQRSources[0]) {
|
|
65
|
+
handoff.qr_image_url = plan.preferredQRSources[0];
|
|
66
|
+
}
|
|
67
|
+
const amount = formatMoney(presentation.checkout.amount_minor, presentation.checkout.currency);
|
|
68
|
+
return {
|
|
69
|
+
status: "human_checkout_required",
|
|
70
|
+
result: {
|
|
71
|
+
checkout_id: presentation.checkout.checkout_id,
|
|
72
|
+
payment: "pending",
|
|
73
|
+
amount,
|
|
74
|
+
},
|
|
75
|
+
handoff,
|
|
76
|
+
instruction: pendingInstruction(platform, amount),
|
|
77
|
+
next: { command: nextCommand, reason: "稍后只查询同一 Checkout" },
|
|
78
|
+
recovery: [],
|
|
79
|
+
};
|
|
61
80
|
}
|
|
62
|
-
function
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
function terminalCheckoutEnvelope(presentation) {
|
|
82
|
+
const checkout = presentation.checkout;
|
|
83
|
+
const serviceExecutionIDs = [...new Set(presentation.items.map((item) => item.service_execution_id).filter((id) => Boolean(id)))];
|
|
84
|
+
const payment = checkout.status === "refunded" ? "refunded"
|
|
85
|
+
: checkout.status === "payment_succeeded" || checkout.status === "completed" ? "verified"
|
|
86
|
+
: checkout.status;
|
|
87
|
+
const result = {
|
|
88
|
+
checkout_id: checkout.checkout_id,
|
|
89
|
+
payment,
|
|
90
|
+
...(presentation.completed_order_id ? { order_id: presentation.completed_order_id } : {}),
|
|
91
|
+
...(serviceExecutionIDs.length === 1 ? { service_execution_id: serviceExecutionIDs[0] } : {}),
|
|
92
|
+
...(serviceExecutionIDs.length > 1 ? { service_execution_ids: serviceExecutionIDs } : {}),
|
|
93
|
+
};
|
|
94
|
+
let status = checkout.status;
|
|
95
|
+
let instruction = "Checkout 已结束;不要再次展示付款二维码。";
|
|
96
|
+
let next = null;
|
|
97
|
+
const recovery = [];
|
|
98
|
+
if (payment === "verified") {
|
|
99
|
+
status = "completed";
|
|
100
|
+
instruction = "Backend 已确认这笔付款。不要再次展示付款入口,不要调用 pay,不要创建新 Checkout 或 Execution。现在只执行 next.command,读取同一 Execution 的履约结果。";
|
|
101
|
+
next = serviceExecutionIDs.length === 1
|
|
102
|
+
? { command: `itpay services next ${serviceExecutionIDs[0]} --json`, reason: "读取同一笔已付款 Service Execution" }
|
|
103
|
+
: presentation.completed_order_id
|
|
104
|
+
? { command: `itpay order ${presentation.completed_order_id}`, reason: "读取已创建订单" }
|
|
105
|
+
: { command: "itpay orders", reason: "恢复已付款订单" };
|
|
81
106
|
}
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
lines.push(`- \`${intent.payment_intent_id}\` — ${intent.payment_method_type} — ${intent.status} — ${formatMoney(intent.amount_minor, intent.currency)}`);
|
|
87
|
-
}
|
|
88
|
-
lines.push("");
|
|
107
|
+
else if (checkout.status === "refunded") {
|
|
108
|
+
instruction = "该 Checkout 已退款,不要再次付款或展示二维码。";
|
|
109
|
+
if (presentation.completed_order_id)
|
|
110
|
+
next = { command: `itpay order ${presentation.completed_order_id}`, reason: "读取订单与退款状态" };
|
|
89
111
|
}
|
|
90
|
-
if (
|
|
91
|
-
|
|
112
|
+
else if (checkout.status === "failed" || checkout.status === "expired") {
|
|
113
|
+
instruction = "该 Checkout 已失效;不要继续使用当前付款入口。";
|
|
114
|
+
if (serviceExecutionIDs.length === 1) {
|
|
115
|
+
recovery.push({ command: `itpay services next ${serviceExecutionIDs[0]} --json`, reason: "由服务端决定是否可恢复 Checkout" });
|
|
116
|
+
}
|
|
92
117
|
}
|
|
93
|
-
|
|
94
|
-
|
|
118
|
+
return { status, result, instruction, next, recovery };
|
|
119
|
+
}
|
|
120
|
+
function checkoutNeedsHumanHandoff(status) {
|
|
121
|
+
return !new Set(["payment_succeeded", "completed", "failed", "expired", "refunded"]).has(status);
|
|
122
|
+
}
|
|
123
|
+
function checkoutPlainResult(result) {
|
|
124
|
+
return Object.entries(result).map(([key, value]) => `${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`);
|
|
125
|
+
}
|
|
126
|
+
function pendingInstruction(platform, amount) {
|
|
127
|
+
if (platform === "markdown")
|
|
128
|
+
return `Backend 尚未确认付款。把 handoff.markdown 原样发送到当前桌面对话,确认二维码、链接和金额 ${amount} 已对用户可见,然后停止等待。不要创建新 Checkout、Execution 或 Payment Intent;稍后仍然只执行 next.command 查询这一笔 Checkout。`;
|
|
129
|
+
if (platform === "terminal")
|
|
130
|
+
return `Backend 尚未确认付款。在用户可见终端展示当前同一 Checkout 的二维码、链接和金额 ${amount},然后停止等待。不要创建新 Checkout、Execution 或 Payment Intent;稍后仍然只执行 next.command 查询这一笔 Checkout。`;
|
|
131
|
+
return `Backend 尚未确认付款。把 handoff.url 作为可点击链接发送给用户,优先把 handoff.qr_local_path 作为图片附件发送,不能发送本地附件时使用 handoff.qr_image_url,并说明金额 ${amount},然后停止等待。不要声称付款成功,不要创建新 Checkout、Execution 或 Payment Intent。稍后仍然只执行 next.command 查询这一笔 Checkout。`;
|
|
132
|
+
}
|
|
133
|
+
function formatMoney(amountMinor, currency) {
|
|
134
|
+
return `${(amountMinor / 100).toFixed(2)} ${currency}`;
|
|
95
135
|
}
|
|
96
136
|
function checkoutPageURL(baseURL, checkoutID, displayToken) {
|
|
97
137
|
const root = publicRoot(baseURL);
|
|
@@ -104,3 +144,12 @@ function checkoutQRPNGURL(baseURL, checkoutID, displayToken) {
|
|
|
104
144
|
function publicRoot(baseURL) {
|
|
105
145
|
return (baseURL ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
106
146
|
}
|
|
147
|
+
function absolutePublicURL(baseURL, value) {
|
|
148
|
+
try {
|
|
149
|
+
const root = publicRoot(baseURL);
|
|
150
|
+
return new URL(value, `${root}/`).toString();
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -1,61 +1,107 @@
|
|
|
1
|
-
import { readFileSync, readdirSync
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
const DOCS_DIR = findDocsDir();
|
|
20
|
-
function loadDocs() {
|
|
21
|
-
const files = readdirSync(DOCS_DIR).filter((f) => f.endsWith(".json"));
|
|
22
|
-
return files.map((file) => {
|
|
23
|
-
const raw = readFileSync(resolve(DOCS_DIR, file), "utf-8");
|
|
24
|
-
return JSON.parse(raw);
|
|
4
|
+
import { resolveOutput } from "../render/sink.js";
|
|
5
|
+
import { CommandContractError, writeCommandEnvelope } from "./guidance.js";
|
|
6
|
+
const commandDir = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
export function runDocsList(options = {}) {
|
|
8
|
+
const topics = loadDocs().map(({ topic, title, purpose }) => ({ topic, title, purpose }));
|
|
9
|
+
writeCommandEnvelope({
|
|
10
|
+
status: "listed",
|
|
11
|
+
result: { topics },
|
|
12
|
+
instruction: "选择与当前步骤最接近的一个 topic;不要一次加载全部文档。",
|
|
13
|
+
next: null,
|
|
14
|
+
recovery: [],
|
|
15
|
+
}, {
|
|
16
|
+
...options,
|
|
17
|
+
plainResult: topics.flatMap((doc) => [`${doc.topic}: ${doc.title}`, ` ${doc.purpose}`]),
|
|
25
18
|
});
|
|
26
19
|
}
|
|
27
|
-
export function
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
for (const doc of docs) {
|
|
31
|
-
process.stdout.write(` ${doc.topic}\n`);
|
|
32
|
-
process.stdout.write(` title: ${doc.title}\n`);
|
|
33
|
-
process.stdout.write(` purpose: ${doc.purpose}\n\n`);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
export function runDocsShow(topic) {
|
|
37
|
-
const docs = loadDocs();
|
|
38
|
-
const doc = docs.find((d) => d.topic === topic);
|
|
20
|
+
export function runDocsShow(topic, options = {}) {
|
|
21
|
+
const normalized = topic.trim();
|
|
22
|
+
const doc = loadDocs().find((candidate) => candidate.topic === normalized);
|
|
39
23
|
if (!doc) {
|
|
40
|
-
|
|
41
|
-
|
|
24
|
+
throw new CommandContractError("doc_not_found", `doc topic not found: ${topic}`, "使用稳定 topic 名称;不要根据标题猜 topic。", [
|
|
25
|
+
{ command: "itpay docs list --json", reason: "列出全部 topic" },
|
|
26
|
+
{ command: `itpay docs search ${shellWord(normalized || "topic")} --json`, reason: "按关键词重新搜索" },
|
|
27
|
+
]);
|
|
28
|
+
}
|
|
29
|
+
const envelope = {
|
|
30
|
+
status: "shown",
|
|
31
|
+
result: { topic: doc.topic, content: doc },
|
|
32
|
+
instruction: "只执行文档中与当前服务端状态匹配的步骤;服务端返回的当前 next 优先。",
|
|
33
|
+
next: null,
|
|
34
|
+
recovery: [],
|
|
35
|
+
};
|
|
36
|
+
if (options.jsonOutput) {
|
|
37
|
+
writeCommandEnvelope(envelope, options);
|
|
42
38
|
return;
|
|
43
39
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
40
|
+
const out = resolveOutput(options.output);
|
|
41
|
+
out("shown\n");
|
|
42
|
+
out(`${JSON.stringify(doc, null, 2)}\n`);
|
|
43
|
+
out(`instruction: ${envelope.instruction}\n`);
|
|
44
|
+
}
|
|
45
|
+
export function runDocsSearch(query, options = {}) {
|
|
46
|
+
const normalized = query.trim().toLowerCase();
|
|
47
|
+
if (!normalized) {
|
|
48
|
+
throw new CommandContractError("docs_query_required", "docs search query must not be empty", "提供一个 topic、标题、用途或 search term 关键词。", [{ command: "itpay docs list --json", reason: "不确定关键词时列出 topic" }]);
|
|
49
|
+
}
|
|
50
|
+
const topics = loadDocs()
|
|
51
|
+
.filter((doc) => searchableText(doc).includes(normalized))
|
|
52
|
+
.map(({ topic, title, purpose }) => ({ topic, title, purpose }));
|
|
53
|
+
if (topics.length === 0) {
|
|
54
|
+
writeCommandEnvelope({
|
|
55
|
+
status: "no_match",
|
|
56
|
+
result: { query, topics: [] },
|
|
57
|
+
instruction: "没有匹配文档;缩短关键词,或列出全部 topic。",
|
|
58
|
+
next: { command: "itpay docs list --json", reason: "浏览稳定 topic" },
|
|
59
|
+
recovery: [],
|
|
60
|
+
}, options);
|
|
55
61
|
return;
|
|
56
62
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
writeCommandEnvelope({
|
|
64
|
+
status: "matched",
|
|
65
|
+
result: { query, topics },
|
|
66
|
+
instruction: topics.length === 1
|
|
67
|
+
? "已唯一匹配;读取该 topic。"
|
|
68
|
+
: "选择最相关的一个 topic;不要同时展开全部结果。",
|
|
69
|
+
next: topics.length === 1
|
|
70
|
+
? { command: `itpay docs show ${topics[0].topic} --json`, reason: "读取唯一匹配文档" }
|
|
71
|
+
: null,
|
|
72
|
+
recovery: [],
|
|
73
|
+
}, {
|
|
74
|
+
...options,
|
|
75
|
+
plainResult: topics.map((doc) => `${doc.topic}: ${doc.title}`),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function loadDocs() {
|
|
79
|
+
const docsDir = findDocsDir();
|
|
80
|
+
const files = readdirSync(docsDir).filter((file) => file.endsWith(".json")).sort();
|
|
81
|
+
return files.map((file) => parseDoc(readFileSync(resolve(docsDir, file), "utf8"), file))
|
|
82
|
+
.sort((left, right) => left.topic.localeCompare(right.topic));
|
|
83
|
+
}
|
|
84
|
+
function findDocsDir() {
|
|
85
|
+
if (process.env.ITPAY_CLI_DOCS_DIR)
|
|
86
|
+
return resolve(process.env.ITPAY_CLI_DOCS_DIR);
|
|
87
|
+
const packagePath = resolve(commandDir, "..", "..", "..", "docs", "agent", "buyer");
|
|
88
|
+
if (existsSync(packagePath))
|
|
89
|
+
return packagePath;
|
|
90
|
+
return resolve(commandDir, "..", "..", "docs", "agent", "buyer");
|
|
91
|
+
}
|
|
92
|
+
function parseDoc(raw, file) {
|
|
93
|
+
const value = JSON.parse(raw);
|
|
94
|
+
if (typeof value.schema_version !== "string" ||
|
|
95
|
+
typeof value.topic !== "string" ||
|
|
96
|
+
typeof value.title !== "string" ||
|
|
97
|
+
typeof value.purpose !== "string") {
|
|
98
|
+
throw new Error(`invalid agent doc: ${file}`);
|
|
60
99
|
}
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
function searchableText(doc) {
|
|
103
|
+
return [doc.topic, doc.title, doc.purpose, ...(doc.search_terms ?? [])].join(" ").toLowerCase();
|
|
104
|
+
}
|
|
105
|
+
function shellWord(value) {
|
|
106
|
+
return /^[a-zA-Z0-9._-]+$/.test(value) ? value : JSON.stringify(value);
|
|
61
107
|
}
|
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
import { HttpError } from "../client/http.js";
|
|
2
2
|
import { resolveOutput } from "../render/sink.js";
|
|
3
|
+
import { declaredAgentType, qualifyItPayCommand } from "../state/agent_type.js";
|
|
4
|
+
export function isTerminalServiceExecutionStatus(status) {
|
|
5
|
+
return status === "failed" || status === "refunded" || status === "cancelled";
|
|
6
|
+
}
|
|
7
|
+
export class CommandContractError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
instruction;
|
|
10
|
+
recovery;
|
|
11
|
+
constructor(code, message, instruction, recovery) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.instruction = instruction;
|
|
15
|
+
this.recovery = recovery;
|
|
16
|
+
this.name = "CommandContractError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function writeCommandEnvelope(value, options = {}) {
|
|
20
|
+
const out = resolveOutput(options.output);
|
|
21
|
+
const agentType = options.agentType ?? declaredAgentType();
|
|
22
|
+
const qualified = qualifyEnvelope(value, agentType);
|
|
23
|
+
if (options.jsonOutput) {
|
|
24
|
+
out(JSON.stringify(qualified, null, 2) + "\n");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
out(`${qualified.status}\n`);
|
|
28
|
+
const facts = "result" in qualified ? qualified.result : qualified.error;
|
|
29
|
+
if (options.plainResult) {
|
|
30
|
+
for (const line of options.plainResult)
|
|
31
|
+
out(`${line}\n`);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
for (const [key, fact] of Object.entries(facts)) {
|
|
35
|
+
out(`${key}: ${typeof fact === "string" ? fact : JSON.stringify(fact)}\n`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if ("handoff" in qualified && qualified.handoff) {
|
|
39
|
+
for (const [key, fact] of Object.entries(qualified.handoff)) {
|
|
40
|
+
out(`handoff.${key}: ${typeof fact === "string" ? fact : JSON.stringify(fact)}\n`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
out(`instruction: ${qualified.instruction}\n`);
|
|
44
|
+
if (qualified.next)
|
|
45
|
+
out(`next: ${qualified.next.command}\n`);
|
|
46
|
+
if (qualified.recovery.length > 0) {
|
|
47
|
+
out("recovery:\n");
|
|
48
|
+
for (const action of qualified.recovery) {
|
|
49
|
+
out(` - ${action.command}\n`);
|
|
50
|
+
out(` reason: ${action.reason}\n`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
3
54
|
export function attachAgentGuidance(payload, guidance) {
|
|
4
55
|
return {
|
|
5
56
|
...payload,
|
|
@@ -8,11 +59,18 @@ export function attachAgentGuidance(payload, guidance) {
|
|
|
8
59
|
}
|
|
9
60
|
export function printAgentGuidance(guidance, output) {
|
|
10
61
|
const out = resolveOutput(output);
|
|
62
|
+
const agentType = declaredAgentType();
|
|
11
63
|
out(`${guidance.summary}\n`);
|
|
12
64
|
if (guidance.visible_results?.length) {
|
|
13
65
|
out("results:\n");
|
|
14
|
-
for (const item of guidance.visible_results)
|
|
66
|
+
for (const item of guidance.visible_results) {
|
|
15
67
|
out(` ${item.rank}. ${item.title}\n`);
|
|
68
|
+
for (const [key, value] of Object.entries(item.safe_payload)) {
|
|
69
|
+
if (key === "name" || key === "company_name" || value === "" || value === undefined || value === null)
|
|
70
|
+
continue;
|
|
71
|
+
out(` ${key}: ${String(value)}\n`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
16
74
|
}
|
|
17
75
|
if (guidance.next_actions.length === 0) {
|
|
18
76
|
out("next actions: none\n");
|
|
@@ -21,7 +79,7 @@ export function printAgentGuidance(guidance, output) {
|
|
|
21
79
|
out("next actions:\n");
|
|
22
80
|
for (const action of guidance.next_actions) {
|
|
23
81
|
out(` - ${action.label}\n`);
|
|
24
|
-
out(` ${action.command}\n`);
|
|
82
|
+
out(` ${qualifyItPayCommand(action.command, agentType)}\n`);
|
|
25
83
|
if (action.requires_human)
|
|
26
84
|
out(" requires human confirmation\n");
|
|
27
85
|
if (action.reason)
|
|
@@ -32,10 +90,20 @@ export function printAgentGuidance(guidance, output) {
|
|
|
32
90
|
out("recovery:\n");
|
|
33
91
|
for (const action of guidance.recovery) {
|
|
34
92
|
out(` - ${action.label}\n`);
|
|
35
|
-
out(` ${action.command}\n`);
|
|
93
|
+
out(` ${qualifyItPayCommand(action.command, agentType)}\n`);
|
|
36
94
|
}
|
|
37
95
|
}
|
|
38
96
|
}
|
|
97
|
+
function qualifyEnvelope(value, agentType) {
|
|
98
|
+
return {
|
|
99
|
+
...value,
|
|
100
|
+
next: value.next ? { ...value.next, command: qualifyItPayCommand(value.next.command, agentType) } : null,
|
|
101
|
+
recovery: value.recovery.map((action) => ({
|
|
102
|
+
...action,
|
|
103
|
+
command: qualifyItPayCommand(action.command, agentType),
|
|
104
|
+
})),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
39
107
|
export function buildCartGuidance(cart, serviceModel) {
|
|
40
108
|
const serviceLine = latestServiceLine(cart);
|
|
41
109
|
if (serviceLine?.service_execution_id) {
|
|
@@ -126,7 +194,6 @@ export function buildServiceActionGuidance(action) {
|
|
|
126
194
|
action_type: action.action_type,
|
|
127
195
|
status: action.status,
|
|
128
196
|
result_item_id: action.result_item_id,
|
|
129
|
-
selected_candidate_hash: action.selected_candidate_hash,
|
|
130
197
|
},
|
|
131
198
|
next_actions: [
|
|
132
199
|
{
|
|
@@ -175,9 +242,19 @@ export function errorRecoveryActions(error) {
|
|
|
175
242
|
if (error.code === "agent_identity_required") {
|
|
176
243
|
return [
|
|
177
244
|
{
|
|
178
|
-
id: "
|
|
179
|
-
label: "
|
|
180
|
-
command: "
|
|
245
|
+
id: "inspect_agent_setup",
|
|
246
|
+
label: "Inspect supported Agent Type setup",
|
|
247
|
+
command: "itpay install --json",
|
|
248
|
+
},
|
|
249
|
+
];
|
|
250
|
+
}
|
|
251
|
+
if (error.code === "agent_device_session_required") {
|
|
252
|
+
return [
|
|
253
|
+
{
|
|
254
|
+
id: "read_agent_session_rules",
|
|
255
|
+
label: "Read identity and session recovery rules",
|
|
256
|
+
command: "itpay skill show itpay-buyer --json",
|
|
257
|
+
reason: "The CLI already attempted one automatic session renewal; do not rotate identity or loop retries.",
|
|
181
258
|
},
|
|
182
259
|
];
|
|
183
260
|
}
|
|
@@ -253,6 +330,7 @@ function buildServiceGuidance(input) {
|
|
|
253
330
|
const resultItem = input.resultItems?.[0];
|
|
254
331
|
const checkoutID = input.checkoutBindings?.at(-1)?.checkout_id;
|
|
255
332
|
const delivery = input.deliveryBindings?.[0];
|
|
333
|
+
const deliveryMode = String(delivery?.redacted_summary?.delivery_mode ?? (delivery?.vault_artifact_id ? "vault_artifact" : ""));
|
|
256
334
|
const backendCheckout = input.backendNextActions?.find((action) => action.kind === "create_checkout");
|
|
257
335
|
const nextActions = [];
|
|
258
336
|
const recovery = [
|
|
@@ -262,7 +340,18 @@ function buildServiceGuidance(input) {
|
|
|
262
340
|
command: `itpay services get ${execution.service_execution_id}`,
|
|
263
341
|
},
|
|
264
342
|
];
|
|
265
|
-
if (execution.status
|
|
343
|
+
if (isTerminalServiceExecutionStatus(execution.status)) {
|
|
344
|
+
// Terminal executions are inspectable, but no command may advance or replay them.
|
|
345
|
+
}
|
|
346
|
+
else if (deliveryMode === "agent_visible_result" && (input.resultItems?.length ?? 0) > 0) {
|
|
347
|
+
nextActions.push({
|
|
348
|
+
id: "use_agent_visible_result",
|
|
349
|
+
label: "Use the safe candidate list shown above",
|
|
350
|
+
command: `itpay services next ${execution.service_execution_id} --json`,
|
|
351
|
+
reason: "This result is already visible to the agent. Do not call services read-result; that command is only for Vault deliveries authorized by a human.",
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
else if (execution.status === "completed" || execution.next_action === "completed") {
|
|
266
355
|
nextActions.push({
|
|
267
356
|
id: "inspect_order_or_grant",
|
|
268
357
|
label: "Inspect order, claim, or grant from the checkout/order owner",
|
|
@@ -327,13 +416,13 @@ function buildServiceGuidance(input) {
|
|
|
327
416
|
});
|
|
328
417
|
}
|
|
329
418
|
}
|
|
330
|
-
else if (input.providerCalled && (input.resultItems?.length ?? 0) === 0) {
|
|
419
|
+
else if ((input.providerCalled || execution.next_action === "select_candidate") && (input.resultItems?.length ?? 0) === 0) {
|
|
331
420
|
nextActions.push({
|
|
332
|
-
id: "
|
|
333
|
-
label: "
|
|
334
|
-
command: `itpay services
|
|
421
|
+
id: "start_refined_search",
|
|
422
|
+
label: "Start a new execution with a more specific company name",
|
|
423
|
+
command: `itpay services start ${execution.service_id}`,
|
|
335
424
|
requires_human: true,
|
|
336
|
-
reason: "
|
|
425
|
+
reason: "No candidates were found. This execution is finished; use one new execution per new keyword.",
|
|
337
426
|
});
|
|
338
427
|
}
|
|
339
428
|
else if (needsHumanSelection(execution, resultItem)) {
|
|
@@ -344,6 +433,12 @@ function buildServiceGuidance(input) {
|
|
|
344
433
|
requires_human: true,
|
|
345
434
|
reason: "Do not choose a candidate without explicit human confirmation.",
|
|
346
435
|
});
|
|
436
|
+
nextActions.push({
|
|
437
|
+
id: "start_another_search",
|
|
438
|
+
label: "Search another company in a new execution",
|
|
439
|
+
command: `itpay services start ${execution.service_id}`,
|
|
440
|
+
reason: "This execution has completed its one keyword lookup; do not reuse it for another keyword.",
|
|
441
|
+
});
|
|
347
442
|
}
|
|
348
443
|
else if (prePurchase) {
|
|
349
444
|
const action = {
|
|
@@ -352,7 +447,7 @@ function buildServiceGuidance(input) {
|
|
|
352
447
|
command: `itpay services invoke ${execution.service_execution_id} --capability ${prePurchase.capability_id} --input key=value --json`,
|
|
353
448
|
};
|
|
354
449
|
if (prePurchase.free_quota_limit) {
|
|
355
|
-
action.reason = `Free quota limit: ${prePurchase.free_quota_limit} per ${prePurchase.quota_subject || "subject"}.`;
|
|
450
|
+
action.reason = `One keyword per execution. Free quota limit: ${prePurchase.free_quota_limit} per ${prePurchase.quota_subject || "subject"}.`;
|
|
356
451
|
}
|
|
357
452
|
nextActions.push(action);
|
|
358
453
|
}
|
|
@@ -377,6 +472,7 @@ function buildServiceGuidance(input) {
|
|
|
377
472
|
delivery: delivery
|
|
378
473
|
? {
|
|
379
474
|
status: delivery.status,
|
|
475
|
+
delivery_mode: deliveryMode || undefined,
|
|
380
476
|
vault_artifact_id: delivery.vault_artifact_id,
|
|
381
477
|
vault_status: delivery.vault_status,
|
|
382
478
|
vault_payload_state: delivery.vault_payload_state,
|
|
@@ -397,16 +493,16 @@ function buildServiceGuidance(input) {
|
|
|
397
493
|
})),
|
|
398
494
|
result_items: (input.resultItems ?? []).map((item) => ({
|
|
399
495
|
result_item_id: item.service_capability_result_item_id,
|
|
400
|
-
stable_hash: item.stable_hash,
|
|
401
496
|
rank: item.rank,
|
|
402
497
|
display_title: item.display_title,
|
|
498
|
+
safe_payload: item.safe_payload,
|
|
403
499
|
})),
|
|
404
500
|
effective_quota: input.effectiveQuota,
|
|
405
501
|
},
|
|
406
502
|
next_actions: nextActions,
|
|
407
503
|
recovery,
|
|
408
504
|
...(input.resultItems?.length
|
|
409
|
-
? { visible_results: input.resultItems.map((item) => ({ rank: item.rank, title: item.display_title })) }
|
|
505
|
+
? { visible_results: input.resultItems.map((item) => ({ rank: item.rank, title: item.display_title, safe_payload: item.safe_payload })) }
|
|
410
506
|
: {}),
|
|
411
507
|
};
|
|
412
508
|
}
|