@itpay/cli 2.0.34 → 2.0.36
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 +1 -0
- package/dist/src/client/backend.js +3 -0
- package/dist/src/commands/buy.js +2 -2
- package/dist/src/commands/checkout_handoff.js +10 -1
- package/dist/src/commands/feedback.js +41 -24
- package/dist/src/commands/guidance.js +3 -3
- package/dist/src/commands/install.js +2 -0
- package/dist/src/commands/order.js +2 -2
- package/dist/src/commands/pay.js +13 -5
- package/dist/src/commands/services.js +4 -4
- package/dist/src/commands/vault_handoff.js +7 -1
- package/dist/src/main.js +2 -2
- package/dist/src/state/client_context.js +2 -0
- package/dist/src/state/config.js +2 -2
- package/dist/src/state/operation_journal.js +76 -63
- package/docs/agent/buyer/identity-and-sessions.json +2 -1
- package/docs/agent/buyer/install-and-setup.json +3 -2
- package/docs/agent/buyer/quickstart.json +1 -1
- package/docs/agent/buyer/render-hosts.json +7 -0
- package/docs/agent/buyer/service-feedback.json +10 -11
- package/docs/cli-reference/agent-types.md +8 -3
- package/docs/cli-reference/commands/buy.md +2 -1
- package/docs/cli-reference/commands/cart/add.md +1 -1
- package/docs/cli-reference/commands/cart/clear.md +1 -1
- package/docs/cli-reference/commands/cart/index.md +1 -1
- package/docs/cli-reference/commands/cart/next.md +1 -1
- package/docs/cli-reference/commands/cart/remove.md +1 -1
- package/docs/cli-reference/commands/cart/show.md +1 -1
- package/docs/cli-reference/commands/catalog/index.md +1 -1
- package/docs/cli-reference/commands/catalog/list.md +1 -1
- package/docs/cli-reference/commands/checkout.md +1 -0
- package/docs/cli-reference/commands/docs/index.md +1 -1
- package/docs/cli-reference/commands/docs/list.md +1 -1
- package/docs/cli-reference/commands/docs/search.md +1 -1
- package/docs/cli-reference/commands/docs/show.md +1 -1
- package/docs/cli-reference/commands/feedback/index.md +6 -6
- package/docs/cli-reference/commands/feedback/submit.md +21 -12
- package/docs/cli-reference/commands/install.md +6 -4
- package/docs/cli-reference/commands/next.md +1 -1
- package/docs/cli-reference/commands/order.md +1 -1
- package/docs/cli-reference/commands/pay.md +1 -0
- package/docs/cli-reference/commands/refund/cancel.md +1 -1
- package/docs/cli-reference/commands/refund/create.md +1 -1
- package/docs/cli-reference/commands/refund/get.md +1 -1
- package/docs/cli-reference/commands/refund/index.md +1 -1
- package/docs/cli-reference/commands/refund/list.md +1 -1
- package/docs/cli-reference/commands/refund/watch.md +1 -1
- package/docs/cli-reference/commands/services/action.md +1 -1
- package/docs/cli-reference/commands/services/checkout.md +7 -0
- package/docs/cli-reference/commands/services/events.md +1 -1
- package/docs/cli-reference/commands/services/get.md +1 -1
- package/docs/cli-reference/commands/services/index.md +1 -1
- package/docs/cli-reference/commands/services/invoke.md +1 -1
- package/docs/cli-reference/commands/services/list.md +1 -1
- package/docs/cli-reference/commands/services/next.md +1 -1
- package/docs/cli-reference/commands/services/quote.md +1 -1
- package/docs/cli-reference/commands/services/read-result.md +1 -1
- package/docs/cli-reference/commands/vault/access.md +3 -0
- package/package.json +2 -1
- package/skills/itpay/SKILL.md +10 -5
package/README.md
CHANGED
|
@@ -53,6 +53,7 @@ Normative per-command contracts: [CLI Command Reference](docs/cli-reference/inde
|
|
|
53
53
|
| `claude-code-desktop` | `claude-code` |
|
|
54
54
|
| `claude-code-cli` | `terminal` |
|
|
55
55
|
| `workbuddy` | `plain-chat` |
|
|
56
|
+
| `zcode` | `plain-chat` |
|
|
56
57
|
| `kimi-code` | `terminal` |
|
|
57
58
|
| `openclaw` | 必须显式提供 |
|
|
58
59
|
|
|
@@ -61,6 +61,9 @@ export class BackendClient {
|
|
|
61
61
|
submitServiceFeedback(orderID, input) {
|
|
62
62
|
return this.http.post(`/v1/orders/${encodeURIComponent(orderID)}/feedback`, input);
|
|
63
63
|
}
|
|
64
|
+
getServiceFeedbackOptions(orderID) {
|
|
65
|
+
return this.http.get(`/v1/orders/${encodeURIComponent(orderID)}/feedback-options`);
|
|
66
|
+
}
|
|
64
67
|
listAccountOrders(limit, status, bearer, cursor) {
|
|
65
68
|
const qs = new URLSearchParams({ limit: String(limit) });
|
|
66
69
|
if (status) {
|
package/dist/src/commands/buy.js
CHANGED
|
@@ -17,7 +17,7 @@ import { localizeCardURL, normalizeCardLocale } from "../render/locale.js";
|
|
|
17
17
|
import { buildAgentChatHandoff } from "../render/markdown.js";
|
|
18
18
|
import { formatMoney } from "../render/output.js";
|
|
19
19
|
import { CommandContractError, writeCommandEnvelope } from "./guidance.js";
|
|
20
|
-
import { buildCheckoutHandoff, shouldPrepareLocalCheckoutImage } from "./checkout_handoff.js";
|
|
20
|
+
import { buildCheckoutHandoff, isLinkOnlyBrowserAgent, shouldPrepareLocalCheckoutImage } from "./checkout_handoff.js";
|
|
21
21
|
import { qualifyItPayCommand } from "../state/agent_type.js";
|
|
22
22
|
export async function runBuy(backend, config, options) {
|
|
23
23
|
const err = validateContext(options.host, options.target);
|
|
@@ -267,7 +267,7 @@ export function buildCheckoutQRPlan(input) {
|
|
|
267
267
|
host: input.host,
|
|
268
268
|
summary,
|
|
269
269
|
url: input.qrPayload,
|
|
270
|
-
...(input.agentType
|
|
270
|
+
...(isLinkOnlyBrowserAgent(input.agentType, platformKeyForHost(input.host)) && input.cardURL ? { linkOnlyURL: input.cardURL } : {}),
|
|
271
271
|
preferredQRSources: [input.qrPNGURL ?? input.qrPayload],
|
|
272
272
|
checkoutID: input.checkoutID,
|
|
273
273
|
platform,
|
|
@@ -5,6 +5,12 @@ export function shouldPrepareLocalCheckoutImage(platform) {
|
|
|
5
5
|
export function isWorkBuddyPlainChat(agentType, platform) {
|
|
6
6
|
return agentType?.trim().toLowerCase() === "workbuddy" && platform === "plain_chat";
|
|
7
7
|
}
|
|
8
|
+
export function isZCodePlainChat(agentType, platform) {
|
|
9
|
+
return agentType?.trim().toLowerCase() === "zcode" && platform === "plain_chat";
|
|
10
|
+
}
|
|
11
|
+
export function isLinkOnlyBrowserAgent(agentType, platform) {
|
|
12
|
+
return isWorkBuddyPlainChat(agentType, platform) || isZCodePlainChat(agentType, platform);
|
|
13
|
+
}
|
|
8
14
|
export function buildWorkBuddyPresentFilesAction(url) {
|
|
9
15
|
return {
|
|
10
16
|
tool: "present_files",
|
|
@@ -23,7 +29,7 @@ export function buildCheckoutHandoff(input) {
|
|
|
23
29
|
if (input.markdown)
|
|
24
30
|
handoff.markdown = input.markdown;
|
|
25
31
|
}
|
|
26
|
-
else if (input.platform === "plain_chat" && input.qrImageURL && !
|
|
32
|
+
else if (input.platform === "plain_chat" && input.qrImageURL && !isLinkOnlyBrowserAgent(input.agentType, input.platform)) {
|
|
27
33
|
handoff.qr_image_url = input.qrImageURL;
|
|
28
34
|
}
|
|
29
35
|
else if (input.platform === "telegram" && input.qrImageURL) {
|
|
@@ -41,6 +47,9 @@ function checkoutHandoffInstruction(agentType, platform, amount) {
|
|
|
41
47
|
if (isWorkBuddyPlainChat(agentType, platform)) {
|
|
42
48
|
return `Backend 尚未确认付款。立即严格按 handoff.agent_action.tool 和 handoff.agent_action.arguments 原样执行一次,在右侧打开 handoff.url;确认工具调用成功后说明金额 ${amount},然后停止等待。若工具失败,只发送原始 handoff.url,报告未自动打开并停止。不要用 present_files 打开本地文件或二维码 PNG,不要下载或重建二维码,不要调用 pay,不要创建新 Checkout、Payment Intent 或 Execution。只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
|
|
43
49
|
}
|
|
50
|
+
if (isZCodePlainChat(agentType, platform)) {
|
|
51
|
+
return `Backend 尚未确认付款。立即用 ZCode 内置浏览器打开 handoff.url,让用户完成付款;确认已发起打开后说明金额 ${amount},然后停止等待。不要只粘贴文字链接,不要下载、解析或重建二维码,不要创建新 Checkout、Payment Intent 或 Execution。只有内置浏览器明确不可用时,才展示同一个可点击 handoff.url。只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
|
|
52
|
+
}
|
|
44
53
|
if (platform === "markdown") {
|
|
45
54
|
return `Backend 尚未确认付款。把 handoff.markdown 原样发送到当前桌面对话,确认二维码、付款链接和金额 ${amount} 均已实际对用户可见,然后停止等待。不要创建新 Checkout、Payment Intent 或 Execution;只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
|
|
46
55
|
}
|
|
@@ -9,8 +9,8 @@ export async function runFeedbackSubmit(backend, orderID, options) {
|
|
|
9
9
|
const rating = normalizeFeedbackRating(options.rating);
|
|
10
10
|
const itemRank = normalizeItemRank(options.itemRank);
|
|
11
11
|
const userNote = normalizeUserNote(options.note ?? "");
|
|
12
|
-
const
|
|
13
|
-
const choices = feedbackItemChoices(
|
|
12
|
+
const context = await backend.getServiceFeedbackOptions(normalizedOrderID);
|
|
13
|
+
const choices = feedbackItemChoices(context);
|
|
14
14
|
if (choices.length === 0) {
|
|
15
15
|
throw new CommandContractError("feedback_unavailable", "this order has no service item available for feedback", "告诉用户这笔订单当前没有可评价的服务项目并停止;不要猜测项目 ID、切换身份或创建其他反馈。", []);
|
|
16
16
|
}
|
|
@@ -18,10 +18,10 @@ export async function runFeedbackSubmit(backend, orderID, options) {
|
|
|
18
18
|
writeCommandEnvelope({
|
|
19
19
|
status: "feedback_item_selection_required",
|
|
20
20
|
result: {
|
|
21
|
-
...(
|
|
21
|
+
...(context.order_code ? { order_code: context.order_code } : {}),
|
|
22
22
|
items: choices.map(({ rank, title, subject }) => ({ rank, title, ...(subject ? { subject } : {}) })),
|
|
23
23
|
},
|
|
24
|
-
instruction: "
|
|
24
|
+
instruction: "用服务名称和主题让用户选择要复盘哪一项;不要展示内部 ID。用户选择后,Agent 使用同一订单、已有评分或留言(如有)并加入所选 item rank 自己执行提交。",
|
|
25
25
|
next: null,
|
|
26
26
|
recovery: [],
|
|
27
27
|
}, outputOptions(options));
|
|
@@ -33,9 +33,23 @@ export async function runFeedbackSubmit(backend, orderID, options) {
|
|
|
33
33
|
if (!choice) {
|
|
34
34
|
throw new CommandContractError("feedback_item_invalid", `item rank ${itemRank} is not available for feedback`, "只使用当前订单返回的服务项目编号;不要猜测内部 ID。本次未提交反馈。", []);
|
|
35
35
|
}
|
|
36
|
+
if (rating === undefined && !userNote && choice.item.agent_feedback_submitted) {
|
|
37
|
+
writeCommandEnvelope({
|
|
38
|
+
status: "feedback_already_submitted",
|
|
39
|
+
result: {
|
|
40
|
+
...(context.order_code ? { order_code: context.order_code } : {}),
|
|
41
|
+
service_title: choice.title,
|
|
42
|
+
},
|
|
43
|
+
instruction: "这笔服务的安全复盘已经记录;无需再次提交或打扰用户,停止。用户以后明确补充评分或评论时才更新。",
|
|
44
|
+
next: null,
|
|
45
|
+
recovery: [],
|
|
46
|
+
}, outputOptions(options));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
36
49
|
const note = formatFeedbackNote({
|
|
37
50
|
userNote,
|
|
38
|
-
|
|
51
|
+
hasRating: rating !== undefined,
|
|
52
|
+
outcome: context.status,
|
|
39
53
|
serviceTitle: choice.title,
|
|
40
54
|
environment: options.environment,
|
|
41
55
|
...(options.agentType ? { agentType: options.agentType } : {}),
|
|
@@ -45,24 +59,28 @@ export async function runFeedbackSubmit(backend, orderID, options) {
|
|
|
45
59
|
}
|
|
46
60
|
const response = await backend.submitServiceFeedback(normalizedOrderID, {
|
|
47
61
|
order_item_id: choice.item.order_item_id,
|
|
48
|
-
rating,
|
|
62
|
+
...(rating !== undefined ? { rating } : {}),
|
|
49
63
|
note,
|
|
50
64
|
});
|
|
51
65
|
writeCommandEnvelope({
|
|
52
66
|
status: "feedback_submitted",
|
|
53
67
|
result: {
|
|
54
|
-
...(
|
|
68
|
+
...(context.order_code ? { order_code: context.order_code } : {}),
|
|
55
69
|
service_title: choice.title,
|
|
56
|
-
rating: response.feedback.rating,
|
|
70
|
+
...(response.feedback.rating !== undefined ? { rating: response.feedback.rating } : {}),
|
|
57
71
|
feedback_status: response.feedback.status,
|
|
58
72
|
},
|
|
59
|
-
instruction:
|
|
73
|
+
instruction: rating !== undefined || userNote
|
|
74
|
+
? "告诉用户反馈已经记录并表示感谢,然后停止。不要承诺回复、处理时间、退款或结果变更。"
|
|
75
|
+
: "服务复盘已经记录;无需打扰用户,停止。不要声称用户给了评分或评论。",
|
|
60
76
|
next: null,
|
|
61
77
|
recovery: [],
|
|
62
78
|
}, outputOptions(options));
|
|
63
79
|
}
|
|
64
80
|
export function normalizeFeedbackRating(value) {
|
|
65
81
|
const normalized = value?.trim().toLowerCase() ?? "";
|
|
82
|
+
if (!normalized)
|
|
83
|
+
return undefined;
|
|
66
84
|
const numeric = normalized.match(/^([1-5])(?:\s*(?:\/\s*5|分|星|stars?))?$/u);
|
|
67
85
|
if (numeric)
|
|
68
86
|
return Number(numeric[1]);
|
|
@@ -84,11 +102,11 @@ function normalizeItemRank(value) {
|
|
|
84
102
|
}
|
|
85
103
|
return rank;
|
|
86
104
|
}
|
|
87
|
-
function feedbackItemChoices(
|
|
88
|
-
return
|
|
105
|
+
function feedbackItemChoices(context) {
|
|
106
|
+
return context.items.flatMap((item, index) => {
|
|
89
107
|
if (!item.order_item_id?.trim())
|
|
90
108
|
return [];
|
|
91
|
-
const subject =
|
|
109
|
+
const subject = safeLine(item.subject ?? "").slice(0, 160);
|
|
92
110
|
return [{
|
|
93
111
|
rank: index + 1,
|
|
94
112
|
item,
|
|
@@ -97,17 +115,6 @@ function feedbackItemChoices(order) {
|
|
|
97
115
|
}];
|
|
98
116
|
});
|
|
99
117
|
}
|
|
100
|
-
function feedbackSubject(item) {
|
|
101
|
-
for (const field of ["company_name_or_credit_no", "company_name", "company", "target", "keyword"]) {
|
|
102
|
-
const value = item.input?.[field];
|
|
103
|
-
if (typeof value === "string") {
|
|
104
|
-
const safe = safeLine(value);
|
|
105
|
-
if (safe)
|
|
106
|
-
return safe.slice(0, 160);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return undefined;
|
|
110
|
-
}
|
|
111
118
|
function normalizeUserNote(value) {
|
|
112
119
|
return value
|
|
113
120
|
.replaceAll("\r\n", "\n")
|
|
@@ -120,7 +127,8 @@ function formatFeedbackNote(input) {
|
|
|
120
127
|
? `## Summary\n${input.userNote.split("\n").map((line) => `> ${line}`).join("\n")}\n\n`
|
|
121
128
|
: "";
|
|
122
129
|
return `${summary}## Context\n` + [
|
|
123
|
-
"- Source:
|
|
130
|
+
"- Source: agent-postmortem",
|
|
131
|
+
`- Human input: ${humanInputSummary(input.hasRating, Boolean(input.userNote))}`,
|
|
124
132
|
`- Outcome: ${safeLine(input.outcome) || "unknown"}`,
|
|
125
133
|
`- Service: ${safeLine(input.serviceTitle) || "未命名服务"}`,
|
|
126
134
|
`- Client: @itpay/cli ${CLI_VERSION}`,
|
|
@@ -128,6 +136,15 @@ function formatFeedbackNote(input) {
|
|
|
128
136
|
`- Environment: ${input.environment}`,
|
|
129
137
|
].join("\n");
|
|
130
138
|
}
|
|
139
|
+
function humanInputSummary(hasRating, hasComment) {
|
|
140
|
+
if (hasRating && hasComment)
|
|
141
|
+
return "rating and comment included";
|
|
142
|
+
if (hasRating)
|
|
143
|
+
return "rating included";
|
|
144
|
+
if (hasComment)
|
|
145
|
+
return "comment included";
|
|
146
|
+
return "not provided";
|
|
147
|
+
}
|
|
131
148
|
function safeLine(value) {
|
|
132
149
|
return value.replace(/[\r\n\u0000-\u001F\u007F]+/gu, " ").trim();
|
|
133
150
|
}
|
|
@@ -5,10 +5,10 @@ import { qualifyBackendCommand } from "../state/config.js";
|
|
|
5
5
|
export function isTerminalServiceExecutionStatus(status) {
|
|
6
6
|
return status === "failed" || status === "refunded" || status === "cancelled";
|
|
7
7
|
}
|
|
8
|
-
export function
|
|
8
|
+
export function appendFeedbackPostmortemInstruction(instruction, outcome) {
|
|
9
9
|
const invitation = outcome === "delivered"
|
|
10
|
-
? "
|
|
11
|
-
: "
|
|
10
|
+
? "结果解释完毕后,Agent 必须为同一订单提交一次安全服务复盘;用户评分和评论均可选,只在用户明确给出时忠实附上,不得猜分或上传对话、报告正文、联系方式和原始日志。"
|
|
11
|
+
: "先完成同一订单的恢复和退款权利解释;处理清楚后,Agent 必须为同一订单提交一次安全服务复盘。只附上用户明确表达的评分或卡壳点,不得猜测。";
|
|
12
12
|
return `${instruction} ${invitation}`;
|
|
13
13
|
}
|
|
14
14
|
export class CommandContractError extends Error {
|
|
@@ -7,6 +7,7 @@ export const INSTALL_AGENT_TYPES = [
|
|
|
7
7
|
"claude-code-desktop",
|
|
8
8
|
"claude-code-cli",
|
|
9
9
|
"workbuddy",
|
|
10
|
+
"zcode",
|
|
10
11
|
"kimi-code",
|
|
11
12
|
"openclaw",
|
|
12
13
|
];
|
|
@@ -16,6 +17,7 @@ const INSTRUCTIONS = {
|
|
|
16
17
|
"claude-code-desktop": "在 Claude Code Desktop 中始终传这个 Agent Type;付款时把返回的二维码和链接实际展示到当前对话。",
|
|
17
18
|
"claude-code-cli": "在 Claude Code CLI 中始终传这个 Agent Type;付款交接只应显示在用户可见终端。",
|
|
18
19
|
workbuddy: "在 WorkBuddy 中始终传这个 Agent Type。运行会持久化 ~/.itpay-v3 的 ItPay Bash 命令前,把 Shell 工具参数 dangerouslyDisableSandbox 设为 true;用户拒绝权限时停止,不要切换 Node、删除 identity 或处理 lock。Checkout 返回已渲染的付款 Card Link 和 present_files action;原样执行一次 action 打开 handoff.url,不要检查本地二维码文件。",
|
|
20
|
+
zcode: "在 ZCode 中始终传这个 Agent Type。Checkout 或授权返回 handoff.url 后,立即用 ZCode 内置浏览器打开;不要只粘贴文字链接或重建二维码。只有内置浏览器不可用时才展示同一个可点击链接。",
|
|
19
21
|
"kimi-code": "保持 kimi-code Agent Type,使用标准 CLI 命令和终端付款交接;不要切换到其他平台身份。",
|
|
20
22
|
openclaw: "保持 openclaw Agent Type;每个展示命令都从当前可信会话上下文显式传 --host,IM 入口同时传 --target。Telegram 使用返回的原生 message action,其他入口展示标准二维码和付款链接。",
|
|
21
23
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { formatMoney } from "../render/output.js";
|
|
2
|
-
import {
|
|
2
|
+
import { appendFeedbackPostmortemInstruction, writeCommandEnvelope } from "./guidance.js";
|
|
3
3
|
export async function runOrder(backend, orderID, options = {}) {
|
|
4
4
|
const order = await backend.getOrder(orderID);
|
|
5
5
|
const [delivery, refundResponse] = await Promise.all([
|
|
@@ -29,7 +29,7 @@ function orderEnvelope(order, delivery, lockedRefund) {
|
|
|
29
29
|
next = { command: `itpay services next ${delivery.service_execution_id} --json`, reason: "读取交付状态" };
|
|
30
30
|
}
|
|
31
31
|
else if (order.status === "failed") {
|
|
32
|
-
instruction =
|
|
32
|
+
instruction = appendFeedbackPostmortemInstruction("先告诉用户这笔订单没有正常交付,不需要重复付款或重新下单;先检查原订单是否已有退款,再由用户决定是否申请。", "failed");
|
|
33
33
|
next = { command: `itpay refund list --order ${order.order_id} --json`, reason: "检查同一订单的退款状态" };
|
|
34
34
|
}
|
|
35
35
|
else if (order.status === "refunded") {
|
package/dist/src/commands/pay.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Checkout page; this command exists for controlled integration recovery.
|
|
3
3
|
import { formatMoney } from "../render/output.js";
|
|
4
4
|
import { writeCommandEnvelope } from "./guidance.js";
|
|
5
|
-
import { buildWorkBuddyPresentFilesAction, isWorkBuddyPlainChat } from "./checkout_handoff.js";
|
|
5
|
+
import { buildWorkBuddyPresentFilesAction, isWorkBuddyPlainChat, isZCodePlainChat } from "./checkout_handoff.js";
|
|
6
6
|
import { platformKeyForHost } from "../render/plan.js";
|
|
7
7
|
export async function runPay(backend, options) {
|
|
8
8
|
const intent = await backend.createPaymentIntent(options.checkoutID, {
|
|
@@ -20,12 +20,17 @@ function payEnvelope(intent, options) {
|
|
|
20
20
|
const terminal = ["failed", "expired", "refunded"].includes(intent.status);
|
|
21
21
|
const verified = intent.status === "verified" || intent.status === "partially_refunded";
|
|
22
22
|
const handoff = {};
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
const platform = platformKeyForHost(options.host);
|
|
24
|
+
const workBuddyAction = isWorkBuddyPlainChat(options.agentType, platform);
|
|
25
|
+
const zcodeAction = isZCodePlainChat(options.agentType, platform);
|
|
26
|
+
if (!terminal && !verified && (workBuddyAction || zcodeAction)) {
|
|
27
|
+
const url = zcodeAction
|
|
28
|
+
? intent.action?.qr_image_url ?? intent.action?.mobile_wallet_url
|
|
29
|
+
: intent.action?.mobile_wallet_url ?? intent.action?.qr_image_url;
|
|
26
30
|
if (url) {
|
|
27
31
|
handoff.url = url;
|
|
28
|
-
|
|
32
|
+
if (workBuddyAction)
|
|
33
|
+
handoff.agent_action = buildWorkBuddyPresentFilesAction(url);
|
|
29
34
|
}
|
|
30
35
|
}
|
|
31
36
|
else {
|
|
@@ -64,6 +69,9 @@ function payInstruction(options, verified, terminal, hasAction, amount) {
|
|
|
64
69
|
if (isWorkBuddyPlainChat(options.agentType, platform)) {
|
|
65
70
|
return `这是受控逃生入口。立即严格按 handoff.agent_action 原样执行一次,在右侧打开 handoff.url;确认工具调用成功后说明金额 ${amount} 并停止等待。若工具失败,只发送原始 handoff.url,报告未自动打开并停止。不要用 present_files 打开本地文件或二维码 PNG,不要立即查询、创建替代 Checkout 或 Payment Intent。`;
|
|
66
71
|
}
|
|
72
|
+
if (isZCodePlainChat(options.agentType, platform)) {
|
|
73
|
+
return `这是受控逃生入口。立即用 ZCode 内置浏览器打开 handoff.url;确认已发起打开后说明金额 ${amount} 并停止等待。不要只粘贴文字链接,不要下载或重建二维码,不要立即查询、创建替代 Checkout 或 Payment Intent。只有内置浏览器明确不可用时,才展示同一个可点击 handoff.url。`;
|
|
74
|
+
}
|
|
67
75
|
if (options.host === "codex" || options.host === "claude-code")
|
|
68
76
|
return "这是受控逃生入口;把 handoff 中的二维码或钱包链接实际发到当前桌面对话,然后停止等待。";
|
|
69
77
|
if (options.host === "terminal")
|
|
@@ -8,7 +8,7 @@ import { buildAgentChatHandoff } from "../render/markdown.js";
|
|
|
8
8
|
import { platformKeyForHost } from "../render/plan.js";
|
|
9
9
|
import { renderTerminalQR } from "../render/qr.js";
|
|
10
10
|
import { buildCheckoutQRPlan } from "./buy.js";
|
|
11
|
-
import {
|
|
11
|
+
import { appendFeedbackPostmortemInstruction, CommandContractError, isTerminalServiceExecutionStatus, writeCommandEnvelope, } from "./guidance.js";
|
|
12
12
|
const serviceActionStatuses = new Set(["pending", "approved", "rejected", "expired", "cancelled"]);
|
|
13
13
|
export async function runServicesStart(backend, serviceID, options = {}) {
|
|
14
14
|
const host = options.host ?? "terminal";
|
|
@@ -678,7 +678,7 @@ function servicesNextEnvelope(model) {
|
|
|
678
678
|
instruction: execution.status === "refunded"
|
|
679
679
|
? "告诉用户这笔服务已经退款并永久结束。Agent 不重放服务步骤、不创建付款页面或尝试读取旧交付。"
|
|
680
680
|
: paidFailure
|
|
681
|
-
?
|
|
681
|
+
? appendFeedbackPostmortemInstruction("告诉用户:付款和订单已经记录,但本次服务没有正常完成,不需要再次付款或重新下单。然后从同一订单检查退款状态;Agent 不重放服务步骤、创建付款页面或再次调用数据来源,也不把技术故障归咎于用户。", "failed")
|
|
682
682
|
: "告诉用户本次服务已经结束且没有可继续的交付。Agent 不重放服务步骤或创建付款页面。",
|
|
683
683
|
next: null,
|
|
684
684
|
recovery: [
|
|
@@ -744,7 +744,7 @@ function servicesNextEnvelope(model) {
|
|
|
744
744
|
items,
|
|
745
745
|
},
|
|
746
746
|
instruction: delivery?.order_id
|
|
747
|
-
?
|
|
747
|
+
? appendFeedbackPostmortemInstruction(items.length > 0
|
|
748
748
|
? selection
|
|
749
749
|
? "搜索已完成。用编号、名称和可公开字段向用户说明结果,然后停止。只有用户明确选择候选并要求继续时才执行 next.command;不要提及 safe_payload。"
|
|
750
750
|
: "这一步的结果已经可用。用普通语言解释可公开字段并停止;不要提及 Graph、safe_payload 或内部 ID。"
|
|
@@ -887,7 +887,7 @@ function grantedResultEnvelope(response, orderID) {
|
|
|
887
887
|
payload: response.result,
|
|
888
888
|
},
|
|
889
889
|
instruction: orderID
|
|
890
|
-
?
|
|
890
|
+
? appendFeedbackPostmortemInstruction("结果来自当前有效 Vault Grant;只使用本次授权字段,过期后停止读取并重新请求用户同意。", "delivered")
|
|
891
891
|
: "结果来自当前有效 Vault Grant;只使用本次授权字段,过期后停止读取并重新请求用户同意。",
|
|
892
892
|
next: null,
|
|
893
893
|
recovery: [],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { platformKeyForHost } from "../render/plan.js";
|
|
2
2
|
import { downloadBrandQRToTmp } from "../render/ide.js";
|
|
3
3
|
import { renderTerminalQR } from "../render/qr.js";
|
|
4
|
-
import { buildWorkBuddyPresentFilesAction, isWorkBuddyPlainChat } from "./checkout_handoff.js";
|
|
4
|
+
import { buildWorkBuddyPresentFilesAction, isWorkBuddyPlainChat, isZCodePlainChat } from "./checkout_handoff.js";
|
|
5
5
|
const AUTH_ATTACH_REASON = "The human must be able to open or scan this official ItPay authorization handoff. " +
|
|
6
6
|
"Render the local image in the current desktop chat; if that fails, show the unchanged official URL.";
|
|
7
7
|
export async function buildVaultHandoff(input) {
|
|
@@ -14,6 +14,12 @@ export async function buildVaultHandoff(input) {
|
|
|
14
14
|
instruction: "向用户说明这是查看已购内容的只读授权,然后严格按 handoff.agent_action 原样执行一次。打开成功后停止等待;失败时只发送同一个 handoff.url。用户完成后重新运行最初的读取命令,不要再次创建授权请求。",
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
|
+
if (isZCodePlainChat(input.agentType, platform)) {
|
|
18
|
+
return {
|
|
19
|
+
handoff,
|
|
20
|
+
instruction: "向用户说明这是查看已购内容的只读授权,然后立即用 ZCode 内置浏览器打开 handoff.url。确认已发起打开后停止等待;不要只粘贴文字链接,也不要下载或重建二维码。只有内置浏览器明确不可用时,才展示同一个可点击 handoff.url。用户完成后重新运行最初的读取命令,不要再次创建授权请求。",
|
|
21
|
+
};
|
|
22
|
+
}
|
|
17
23
|
if (platform === "markdown") {
|
|
18
24
|
const downloaded = input.imageAttachEnabled && input.qrPNGURL
|
|
19
25
|
? await downloadBrandQRToTmp(input.qrPNGURL, "auth", input.requestID, {
|
package/dist/src/main.js
CHANGED
|
@@ -43,7 +43,7 @@ Common human intents:
|
|
|
43
43
|
Previously purchased item vault list
|
|
44
44
|
Purchase history orders
|
|
45
45
|
Delivery or refund problem resume the known Order or Refund
|
|
46
|
-
|
|
46
|
+
Review a completed service feedback submit
|
|
47
47
|
|
|
48
48
|
The Agent runs commands. Ask the human only to choose, authorize, pay, provide
|
|
49
49
|
required contact details, or confirm a refund. Never expose commands or internal IDs.
|
|
@@ -966,7 +966,7 @@ program
|
|
|
966
966
|
const feedback = program.command("feedback").description("Rate one service item from an existing order");
|
|
967
967
|
feedback
|
|
968
968
|
.command("submit")
|
|
969
|
-
.description("Submit a human
|
|
969
|
+
.description("Submit a safe Agent postmortem with optional human rating and comment")
|
|
970
970
|
.option("--order <order_id>")
|
|
971
971
|
.option("--rating <rating>")
|
|
972
972
|
.option("--note <text>")
|
|
@@ -62,6 +62,8 @@ export function defaultHostForAgentType(agentType) {
|
|
|
62
62
|
return "claude-code";
|
|
63
63
|
if (normalized === "workbuddy")
|
|
64
64
|
return "plain-chat";
|
|
65
|
+
if (normalized === "zcode")
|
|
66
|
+
return "plain-chat";
|
|
65
67
|
if (normalized === "openclaw")
|
|
66
68
|
return undefined;
|
|
67
69
|
return "terminal";
|
package/dist/src/state/config.js
CHANGED
|
@@ -12,8 +12,8 @@ import { DeviceAuthority } from "./device_authority.js";
|
|
|
12
12
|
import { OperationJournal } from "./operation_journal.js";
|
|
13
13
|
export const DEFAULT_BASE_URL = "https://app.itpay.ai";
|
|
14
14
|
export const DEV_BASE_URL = "https://dev.itpay.ai";
|
|
15
|
-
export const CLI_VERSION = "2.0.
|
|
16
|
-
export const API_CONTRACT_REVISION = "sha256:
|
|
15
|
+
export const CLI_VERSION = "2.0.36";
|
|
16
|
+
export const API_CONTRACT_REVISION = "sha256:20986f3d7a770f9c696c0576f3ae4898f0d23a519bd489c1727382a16270fab2";
|
|
17
17
|
const CART_SESSION_DEFAULT_DIR = ".itpay-v3";
|
|
18
18
|
const CART_SESSION_FILENAME = "cart.json";
|
|
19
19
|
const OPERATION_JOURNAL_FILENAME = "operations.json";
|
|
@@ -1,80 +1,93 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { chmodSync, existsSync, linkSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, resolve } from "node:path";
|
|
4
4
|
export class OperationJournal {
|
|
5
|
-
|
|
6
|
-
constructor(
|
|
7
|
-
this.
|
|
5
|
+
legacyPath;
|
|
6
|
+
constructor(legacyPath) {
|
|
7
|
+
this.legacyPath = legacyPath;
|
|
8
8
|
}
|
|
9
9
|
async getOrCreate(operationKey) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
10
|
+
const operationKeyHash = createHash("sha256").update(operationKey).digest("hex");
|
|
11
|
+
const recordDirectory = `${this.legacyPath}.d`;
|
|
12
|
+
const recordPath = resolve(recordDirectory, `${operationKeyHash}.json`);
|
|
13
|
+
const existing = readRecord(recordPath, operationKeyHash);
|
|
14
|
+
if (existing)
|
|
15
|
+
return existing.id;
|
|
16
|
+
mkdirSync(recordDirectory, { recursive: true, mode: 0o700 });
|
|
17
|
+
chmodSync(recordDirectory, 0o700);
|
|
18
|
+
const record = {
|
|
19
|
+
schemaVersion: "itpay.operation.v2",
|
|
20
|
+
operationKeyHash,
|
|
21
|
+
id: this.readLegacyID(operationKey) ?? `op_${randomUUID().replaceAll("-", "")}`,
|
|
22
|
+
createdAt: new Date().toISOString(),
|
|
23
|
+
};
|
|
24
|
+
const temporaryPath = resolve(recordDirectory, `.${basename(recordPath)}.${process.pid}.${randomUUID()}.tmp`);
|
|
25
|
+
try {
|
|
26
|
+
writeFileSync(temporaryPath, JSON.stringify(record, null, 2), { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
27
|
+
chmodSync(temporaryPath, 0o600);
|
|
23
28
|
try {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
linkSync(temporaryPath, recordPath);
|
|
30
|
+
chmodSync(recordPath, 0o600);
|
|
31
|
+
return record.id;
|
|
27
32
|
}
|
|
28
|
-
catch {
|
|
29
|
-
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error.code !== "EEXIST")
|
|
35
|
+
throw error;
|
|
36
|
+
const winner = readRecord(recordPath, operationKeyHash);
|
|
37
|
+
if (!winner)
|
|
38
|
+
throw new Error("ItPay operation journal record disappeared during publication");
|
|
39
|
+
return winner.id;
|
|
30
40
|
}
|
|
31
41
|
}
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
function atomicOwnerOnlyWrite(path, value) {
|
|
36
|
-
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
37
|
-
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
38
|
-
writeFileSync(temporary, value, { encoding: "utf8", mode: 0o600 });
|
|
39
|
-
chmodSync(temporary, 0o600);
|
|
40
|
-
renameSync(temporary, path);
|
|
41
|
-
chmodSync(path, 0o600);
|
|
42
|
-
}
|
|
43
|
-
async function withFileLock(path, run) {
|
|
44
|
-
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
45
|
-
let descriptor;
|
|
46
|
-
for (let attempt = 0; attempt < 200; attempt += 1) {
|
|
47
|
-
try {
|
|
48
|
-
descriptor = openSync(path, "wx", 0o600);
|
|
49
|
-
break;
|
|
50
|
-
}
|
|
51
|
-
catch (error) {
|
|
52
|
-
if (error.code !== "EEXIST")
|
|
53
|
-
throw error;
|
|
42
|
+
finally {
|
|
54
43
|
try {
|
|
55
|
-
|
|
56
|
-
unlinkSync(path);
|
|
44
|
+
unlinkSync(temporaryPath);
|
|
57
45
|
}
|
|
58
|
-
catch (
|
|
59
|
-
if (
|
|
60
|
-
throw
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (error.code !== "ENOENT")
|
|
48
|
+
throw error;
|
|
61
49
|
}
|
|
62
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
63
50
|
}
|
|
64
51
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return await run();
|
|
69
|
-
}
|
|
70
|
-
finally {
|
|
71
|
-
closeSync(descriptor);
|
|
52
|
+
readLegacyID(operationKey) {
|
|
53
|
+
if (!existsSync(this.legacyPath))
|
|
54
|
+
return undefined;
|
|
72
55
|
try {
|
|
73
|
-
|
|
56
|
+
const parsed = JSON.parse(readFileSync(this.legacyPath, "utf8"));
|
|
57
|
+
if (parsed.schemaVersion !== "itpay.operations.v1" || !parsed.operations)
|
|
58
|
+
return undefined;
|
|
59
|
+
const id = parsed.operations[operationKey]?.id;
|
|
60
|
+
return typeof id === "string" && isOperationID(id) ? id : undefined;
|
|
74
61
|
}
|
|
75
|
-
catch
|
|
76
|
-
|
|
77
|
-
throw error;
|
|
62
|
+
catch {
|
|
63
|
+
return undefined;
|
|
78
64
|
}
|
|
79
65
|
}
|
|
80
66
|
}
|
|
67
|
+
function readRecord(path, expectedHash) {
|
|
68
|
+
if (!existsSync(path))
|
|
69
|
+
return undefined;
|
|
70
|
+
let parsed;
|
|
71
|
+
try {
|
|
72
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
throw new Error("ItPay operation journal record is invalid");
|
|
76
|
+
}
|
|
77
|
+
if (!isOperationRecord(parsed, expectedHash)) {
|
|
78
|
+
throw new Error("ItPay operation journal record is invalid");
|
|
79
|
+
}
|
|
80
|
+
return parsed;
|
|
81
|
+
}
|
|
82
|
+
function isOperationRecord(value, expectedHash) {
|
|
83
|
+
if (!value || typeof value !== "object")
|
|
84
|
+
return false;
|
|
85
|
+
const record = value;
|
|
86
|
+
return record.schemaVersion === "itpay.operation.v2" &&
|
|
87
|
+
record.operationKeyHash === expectedHash &&
|
|
88
|
+
typeof record.id === "string" && isOperationID(record.id) &&
|
|
89
|
+
typeof record.createdAt === "string" && !Number.isNaN(Date.parse(record.createdAt));
|
|
90
|
+
}
|
|
91
|
+
function isOperationID(value) {
|
|
92
|
+
return /^op_[a-zA-Z0-9_-]+$/.test(value);
|
|
93
|
+
}
|
|
@@ -25,8 +25,9 @@
|
|
|
25
25
|
],
|
|
26
26
|
"local_state_recovery": [
|
|
27
27
|
"Device state updates use an atomic directory lock and owner-only atomic file replacement; no lock file is opened with fs.open or fs.openSync.",
|
|
28
|
+
"Operation idempotency uses one immutable owner-only record per operation key. Different commands and Agent processes do not share a global operation-journal lock; legacy operations.json is read only.",
|
|
28
29
|
"On device_state_unwritable, use the reported operation and errno to diagnose the failing stage without exposing local paths; keep the same Agent Type and CLI/Node launcher, restore Host-approved persistent access to ~/.itpay-v3, then retry the original command.",
|
|
29
|
-
"Do not manually create
|
|
30
|
+
"Do not manually create or delete state locks, switch Node runtimes, delete identity, or rotate Agent Type as a retry strategy."
|
|
30
31
|
],
|
|
31
32
|
"commands": [
|
|
32
33
|
{
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
{
|
|
14
14
|
"intent": "list supported Agent Types",
|
|
15
15
|
"command": "itpay install --json",
|
|
16
|
-
"success_signal": "status is install_targets and exactly
|
|
16
|
+
"success_signal": "status is install_targets and exactly eight supported Agent Type definitions are returned"
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
"intent": "read setup for the real runtime",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"Production defaults to https://app.itpay.ai. Testing may set ITPAY_BACKEND_URL to the exact official URL https://dev.itpay.ai; no other Backend is allowed.",
|
|
39
39
|
"If backend_contract_incompatible includes result.required_cli_version, stop all ItPay business commands and use only the exact distribution-specific update recovery returned by the CLI; never replace its version with latest.",
|
|
40
40
|
"After upgrading, require itpay --version to equal result.required_cli_version before running readyz again; never change Agent Type or Device identity to recover compatibility.",
|
|
41
|
-
"Use one exact type: codex-desktop, codex-cli, claude-code-desktop, claude-code-cli, workbuddy, kimi-code, or openclaw.",
|
|
41
|
+
"Use one exact type: codex-desktop, codex-cli, claude-code-desktop, claude-code-cli, workbuddy, zcode, kimi-code, or openclaw.",
|
|
42
42
|
"For compatibility, a global --agent-type codex declaration is normalized immediately to codex-desktop; all registration, output, and returned commands use codex-desktop. Never generate the alias, and do not use codex as an install target.",
|
|
43
43
|
"One local private key is reused, while Device registrations and quota lineage remain separate for app.itpay.ai and dev.itpay.ai.",
|
|
44
44
|
"Within each official Backend registration, each Agent Type has one Agent Instance; all windows and chats of the same type reuse it.",
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
"codex",
|
|
71
71
|
"claude code",
|
|
72
72
|
"workbuddy",
|
|
73
|
+
"zcode",
|
|
73
74
|
"kimi code",
|
|
74
75
|
"openclaw"
|
|
75
76
|
]
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"previous_content": "Use vault list, optionally with the subject as --query.",
|
|
26
26
|
"purchase_history": "Use orders.",
|
|
27
27
|
"refund_or_paid_problem": "Resume the known Order or Refund before creating anything new.",
|
|
28
|
-
"service_feedback": "Finish the original order outcome first
|
|
28
|
+
"service_feedback": "Finish the original order outcome first, then submit one safe Agent postmortem; include a rating or comment only when the human explicitly gives it.",
|
|
29
29
|
"ambiguous": "Ask whether the human wants an earlier purchase or a new query before calling ItPay."
|
|
30
30
|
},
|
|
31
31
|
"commands": [{
|