@a2hmarket/a2hmarket 0.9.0 → 0.9.1
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/index.ts +3 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/a2hmarket/SKILL.md +5 -0
- package/skills/a2hmarket/references/playbooks/reporting.md +17 -2
- package/src/agent-service.ts +1 -1
- package/src/approval-store.ts +6 -0
- package/src/tools/approval.ts +84 -3
- package/src/tools/order.ts +11 -0
package/index.ts
CHANGED
|
@@ -142,6 +142,9 @@ export default {
|
|
|
142
142
|
if (!a2hToolUsedSessions.has(sessionKey)) return;
|
|
143
143
|
a2hToolUsedSessions.delete(sessionKey);
|
|
144
144
|
|
|
145
|
+
// Skip DM per-peer sessions — they have their own notification via deliver callback
|
|
146
|
+
if (sessionKey.includes(":a2hmarket:")) return;
|
|
147
|
+
|
|
145
148
|
const content = (event as any)?.content ?? "";
|
|
146
149
|
if (typeof content !== "string" || !content.trim()) return;
|
|
147
150
|
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -8,6 +8,11 @@ version: 2.0.0
|
|
|
8
8
|
|
|
9
9
|
A2H Market 是一个人类和 AI Agent 都可以使用的 AI 交易市场。你代理人类在市场中进行买卖交易。
|
|
10
10
|
|
|
11
|
+
> ⚠️ **最高优先级规则:你是代理人,不是决策者。**
|
|
12
|
+
> 涉及价格、报价、还价、创建订单、接受订单、支付、收款确认、交付验收等决定时,
|
|
13
|
+
> **必须调用 `a2h_create_approval` 让人类确认,不得自行决定。**
|
|
14
|
+
> 你只能自主回答帖子里已有的信息。其他一切需要人类拍板。
|
|
15
|
+
|
|
11
16
|
**用三个概念理解你在市场里做的事:**
|
|
12
17
|
|
|
13
18
|
| 概念 | 含义 | 角色 |
|
|
@@ -13,10 +13,12 @@
|
|
|
13
13
|
|
|
14
14
|
## 关键节点即时汇报
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
以下事件发生时,**必须调用 `a2h_create_approval`** 创建审批,等待人类确认后再继续。**不要自行决定,不要只是"告知"——必须用审批工具。**
|
|
17
17
|
|
|
18
|
-
| 事件 |
|
|
18
|
+
| 事件 | 审批内容 | 人类需要做什么 |
|
|
19
19
|
|------|---------|--------------|
|
|
20
|
+
| **对方报价 / 你需要报价** | 报价金额和条件 | 确认是否接受 / 确定报价 |
|
|
21
|
+
| **对方还价** | 还价金额 vs 原价 | 接受 / 拒绝 / 继续还价 |
|
|
20
22
|
| 订单创建(买方收到) | 订单信息、价格、条件 | 确认是否接受订单 |
|
|
21
23
|
| 订单确认(卖方收到) | 买家已确认订单 | 知悉,进入付款阶段 |
|
|
22
24
|
| **付款阶段(卖方)** | 买家已付款通知 | 确认是否真的收到款项 |
|
|
@@ -25,6 +27,19 @@
|
|
|
25
27
|
| 帖子信息不足 | 对方提出帖子中未明确的条件 | 确认后更新帖子 |
|
|
26
28
|
| 交易异常/破裂 | 异常原因 | 决定下一步 |
|
|
27
29
|
|
|
30
|
+
**调用示例:**
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
a2h_create_approval(
|
|
34
|
+
peer_id: "ag_xxx",
|
|
35
|
+
question: "对方报价500元,是否接受?",
|
|
36
|
+
context: "对方昵称:小明\n1. 对方咨询PPT服务\n2. 你报价800\n3. 对方还价500",
|
|
37
|
+
options: ["接受", "拒绝", "还价"]
|
|
38
|
+
)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
调用后**不要回复对方**,等待 `[Human Approval Result]` 消息到达后再根据人类决定行动。
|
|
42
|
+
|
|
28
43
|
|
|
29
44
|
---
|
|
30
45
|
|
package/src/agent-service.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { setApprovalConfig } from "./tools/approval.js";
|
|
|
24
24
|
|
|
25
25
|
// ── MQTT Send Helper ─────────────────────────────────────────────────────
|
|
26
26
|
|
|
27
|
-
async function mqttSendText(
|
|
27
|
+
export async function mqttSendText(
|
|
28
28
|
creds: { agentId: string; agentKey: string; apiUrl: string; mqttUrl: string },
|
|
29
29
|
targetAgentId: string,
|
|
30
30
|
text: string,
|
package/src/approval-store.ts
CHANGED
|
@@ -107,3 +107,9 @@ export function listPending(): Approval[] {
|
|
|
107
107
|
evict();
|
|
108
108
|
return [...approvals.values()].filter(a => a.status === "pending");
|
|
109
109
|
}
|
|
110
|
+
|
|
111
|
+
export function hasApprovedForPeer(peerId: string): boolean {
|
|
112
|
+
return [...approvals.values()].some(
|
|
113
|
+
a => a.peerId === peerId && (a.status === "approved" || a.status === "custom")
|
|
114
|
+
);
|
|
115
|
+
}
|
package/src/tools/approval.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { createApproval, resolveApproval, getApproval, markNotified, listPending
|
|
|
8
8
|
import { notifyApproval, type NotifyLog } from "../notify.js";
|
|
9
9
|
import { loadCredentials } from "../credentials.js";
|
|
10
10
|
import { getA2HRuntime } from "../runtime.js";
|
|
11
|
+
import { mqttSendText } from "../agent-service.js";
|
|
11
12
|
|
|
12
13
|
let _cfg: OpenClawConfig | null = null;
|
|
13
14
|
|
|
@@ -65,8 +66,55 @@ export function registerApprovalTools(api: OpenClawPluginApi) {
|
|
|
65
66
|
const approval = createApproval({ peerId, sessionKey, question, context, options });
|
|
66
67
|
|
|
67
68
|
const creds = loadCredentials();
|
|
69
|
+
// Send notification card to human (visual notification)
|
|
68
70
|
notifyApproval(approval, creds.agentId, notifyLog);
|
|
69
71
|
|
|
72
|
+
// Inject approval request into the human's channel session so the AI there has context
|
|
73
|
+
if (_cfg && creds.notify?.target) {
|
|
74
|
+
const runtime = getA2HRuntime();
|
|
75
|
+
const channelTarget = creds.notify.target;
|
|
76
|
+
const channel = creds.notify.channel;
|
|
77
|
+
|
|
78
|
+
const optionsLine = options.length > 0 ? `Options: ${options.join(" / ")}` : "";
|
|
79
|
+
const contextLine = context ? `Context: ${context}` : "";
|
|
80
|
+
const approvalMessage = [
|
|
81
|
+
`[A2H Market Pending Approval — DO NOT reply to this message. Wait for human's next message.]`,
|
|
82
|
+
`Approval ID: ${approval.id}`,
|
|
83
|
+
`From: ${peerId}`,
|
|
84
|
+
`Question: ${question}`,
|
|
85
|
+
contextLine,
|
|
86
|
+
optionsLine,
|
|
87
|
+
``,
|
|
88
|
+
`INSTRUCTION: When the human sends their next message with a decision about this approval,`,
|
|
89
|
+
`call a2h_approval_response(approval_id='${approval.id}', decision='<the human's decision>').`,
|
|
90
|
+
`Do NOT reply to the human about this — just call the tool silently.`,
|
|
91
|
+
].filter(Boolean).join("\n");
|
|
92
|
+
|
|
93
|
+
dispatchInboundDirectDmWithRuntime({
|
|
94
|
+
cfg: _cfg,
|
|
95
|
+
runtime,
|
|
96
|
+
channel,
|
|
97
|
+
channelLabel: channel,
|
|
98
|
+
accountId: "default",
|
|
99
|
+
peer: { kind: "direct", id: channelTarget },
|
|
100
|
+
senderId: "a2hmarket-system",
|
|
101
|
+
senderAddress: `a2hmarket:system`,
|
|
102
|
+
recipientAddress: `${channel}:${channelTarget}`,
|
|
103
|
+
conversationLabel: channelTarget,
|
|
104
|
+
rawBody: approvalMessage,
|
|
105
|
+
messageId: `approval_req_${approval.id}`,
|
|
106
|
+
timestamp: Date.now(),
|
|
107
|
+
commandAuthorized: true,
|
|
108
|
+
deliver: async () => {
|
|
109
|
+
// Don't send AI's response back — this is just context injection
|
|
110
|
+
},
|
|
111
|
+
onRecordError: () => {},
|
|
112
|
+
onDispatchError: () => {},
|
|
113
|
+
}).catch((err) => {
|
|
114
|
+
notifyLog.error(`approval feishu session inject failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
70
118
|
return {
|
|
71
119
|
result: JSON.stringify({
|
|
72
120
|
approval_id: approval.id,
|
|
@@ -127,12 +175,28 @@ export function registerApprovalTools(api: OpenClawPluginApi) {
|
|
|
127
175
|
if (_cfg) {
|
|
128
176
|
const runtime = getA2HRuntime();
|
|
129
177
|
const creds = loadCredentials();
|
|
130
|
-
const
|
|
178
|
+
const resultParts = [
|
|
131
179
|
`[Human Approval Result]`,
|
|
132
180
|
`Approval: ${resolved.id}`,
|
|
181
|
+
`Peer: ${resolved.peerId}`,
|
|
133
182
|
`Decision: ${decision}`,
|
|
183
|
+
``,
|
|
134
184
|
`Original question: ${resolved.question}`,
|
|
135
|
-
]
|
|
185
|
+
];
|
|
186
|
+
if (resolved.context) {
|
|
187
|
+
resultParts.push(``, `Context: ${resolved.context}`);
|
|
188
|
+
}
|
|
189
|
+
if (resolved.options.length > 0) {
|
|
190
|
+
resultParts.push(`Options were: ${resolved.options.join(" / ")}`);
|
|
191
|
+
}
|
|
192
|
+
resultParts.push(
|
|
193
|
+
``,
|
|
194
|
+
`Action required: Based on the human's decision above, proceed accordingly.`,
|
|
195
|
+
`If the decision is to accept/approve, continue with the next step (e.g., create order with a2h_order_create).`,
|
|
196
|
+
`If the decision is to reject, inform the counterparty.`,
|
|
197
|
+
`If the decision is custom text, follow the human's specific instruction.`,
|
|
198
|
+
);
|
|
199
|
+
const resultMessage = resultParts.join("\n");
|
|
136
200
|
|
|
137
201
|
try {
|
|
138
202
|
await dispatchInboundDirectDmWithRuntime({
|
|
@@ -150,7 +214,24 @@ export function registerApprovalTools(api: OpenClawPluginApi) {
|
|
|
150
214
|
messageId: `approval_result_${resolved.id}`,
|
|
151
215
|
timestamp: Date.now(),
|
|
152
216
|
commandAuthorized: true,
|
|
153
|
-
deliver: async () => {
|
|
217
|
+
deliver: async (payload) => {
|
|
218
|
+
// DM session AI may reply to counterparty after processing approval result
|
|
219
|
+
const replyText =
|
|
220
|
+
payload && typeof payload === "object" && "text" in payload
|
|
221
|
+
? String((payload as { text?: string }).text ?? "")
|
|
222
|
+
: "";
|
|
223
|
+
if (!replyText.trim()) return;
|
|
224
|
+
// Send to counterparty via MQTT
|
|
225
|
+
try {
|
|
226
|
+
await mqttSendText(creds, resolved.peerId, replyText);
|
|
227
|
+
notifyLog.info(`approval follow-up sent to ${resolved.peerId}: ${replyText.slice(0, 80)}`);
|
|
228
|
+
} catch (err) {
|
|
229
|
+
notifyLog.error(`approval follow-up send failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
230
|
+
}
|
|
231
|
+
// Notify human
|
|
232
|
+
const { notifyHuman: notifyH } = await import("../notify.js");
|
|
233
|
+
notifyH("reply", resolved.peerId, replyText.slice(0, 500), creds.agentId, notifyLog);
|
|
234
|
+
},
|
|
154
235
|
onRecordError: (err) => {
|
|
155
236
|
notifyLog.error(`approval dispatch record error: ${err instanceof Error ? err.message : String(err)}`);
|
|
156
237
|
},
|
package/src/tools/order.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
2
|
import type { A2HApiClient } from "../api-client.js";
|
|
3
|
+
import { hasApprovedForPeer } from "../approval-store.js";
|
|
3
4
|
|
|
4
5
|
export function registerOrderTools(api: OpenClawPluginApi, client: A2HApiClient) {
|
|
5
6
|
api.registerTool({
|
|
@@ -19,6 +20,16 @@ export function registerOrderTools(api: OpenClawPluginApi, client: A2HApiClient)
|
|
|
19
20
|
required: ["customer_id", "title", "content", "price_cent", "product_id", "order_type"],
|
|
20
21
|
},
|
|
21
22
|
execute: async (_toolCallId: string, params: Record<string, unknown>) => {
|
|
23
|
+
// Require human approval before creating orders
|
|
24
|
+
const customerId = params.customer_id as string;
|
|
25
|
+
if (!hasApprovedForPeer(customerId)) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
"Cannot create order without human approval. " +
|
|
28
|
+
"Call a2h_create_approval(peer_id, question, context, options) first, " +
|
|
29
|
+
"wait for [Human Approval Result], then retry."
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
22
33
|
const body = {
|
|
23
34
|
providerId: client.agentId,
|
|
24
35
|
customerId: params.customer_id,
|