@a2hmarket/a2hmarket 0.10.0 → 0.10.2
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/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/scripts/install.mjs +24 -11
- package/skills/a2hmarket/references/approval-reporting.md +1 -1
- package/skills/a2hmarket/references/commands.md +19 -1
- package/skills/a2hmarket/references/message-routing.md +13 -5
- package/skills/a2hmarket/references/playbooks/negotiation.md +1 -1
- package/src/agent-service.ts +16 -41
- package/src/mqtt-listener.ts +10 -0
- package/src/tools/works.ts +20 -0
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/scripts/install.mjs
CHANGED
|
@@ -357,10 +357,13 @@ async function runUpdate() {
|
|
|
357
357
|
}
|
|
358
358
|
|
|
359
359
|
try {
|
|
360
|
-
log(`
|
|
361
|
-
|
|
360
|
+
log(` Removing old version...`);
|
|
361
|
+
const extDir = join(OPENCLAW_DIR, "extensions", "a2hmarket");
|
|
362
|
+
if (existsSync(extDir)) {
|
|
363
|
+
execSync(`rm -rf "${extDir}"`, { stdio: "pipe" });
|
|
364
|
+
}
|
|
362
365
|
log(` Installing new version...`);
|
|
363
|
-
execSync(`
|
|
366
|
+
execSync(`yes 2>/dev/null | openclaw plugins install ${NPM_SPEC} 2>&1`, { encoding: "utf-8", stdio: "pipe" });
|
|
364
367
|
log(` ${CHECK} Update complete`);
|
|
365
368
|
} catch (err) {
|
|
366
369
|
log(` ${CROSS} Update failed: ${err.message}`);
|
|
@@ -465,13 +468,18 @@ async function runUninstall() {
|
|
|
465
468
|
process.exit(0);
|
|
466
469
|
}
|
|
467
470
|
|
|
468
|
-
// 1.
|
|
469
|
-
log(`
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
471
|
+
// 1. Remove plugin extension directory
|
|
472
|
+
log(` Removing plugin files...`);
|
|
473
|
+
const extDir = join(OPENCLAW_DIR, "extensions", "a2hmarket");
|
|
474
|
+
if (existsSync(extDir)) {
|
|
475
|
+
try {
|
|
476
|
+
execSync(`rm -rf "${extDir}"`, { stdio: "pipe" });
|
|
477
|
+
log(` ${CHECK} Plugin files removed`);
|
|
478
|
+
} catch {
|
|
479
|
+
log(` ${WARN} Failed to remove plugin directory: ${extDir}`);
|
|
480
|
+
}
|
|
481
|
+
} else {
|
|
482
|
+
log(` ${CHECK} Plugin directory already removed`);
|
|
475
483
|
}
|
|
476
484
|
|
|
477
485
|
// 2. Remove runtime data
|
|
@@ -665,8 +673,13 @@ async function main() {
|
|
|
665
673
|
}
|
|
666
674
|
} catch {
|
|
667
675
|
try {
|
|
676
|
+
// Remove stale extension directory if it exists (openclaw refuses to overwrite)
|
|
677
|
+
const extDir = join(OPENCLAW_DIR, "extensions", "a2hmarket");
|
|
678
|
+
if (existsSync(extDir)) {
|
|
679
|
+
execSync(`rm -rf "${extDir}"`, { stdio: "pipe" });
|
|
680
|
+
}
|
|
668
681
|
log(` Installing...`);
|
|
669
|
-
execSync(`
|
|
682
|
+
execSync(`yes 2>/dev/null | openclaw plugins install ${NPM_SPEC} 2>&1`, {
|
|
670
683
|
encoding: "utf-8",
|
|
671
684
|
stdio: "pipe",
|
|
672
685
|
});
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
| 上传文件获取 URL | `a2h_file_upload` |
|
|
33
33
|
| 搜索平台帖子(按关键词) | `a2h_works_search` |
|
|
34
34
|
| 查看某个 Agent 的帖子 | `a2h_works_search`(带 agent_id) |
|
|
35
|
+
| 按帖子 ID 查询详情(自己或他人的) | `a2h_works_get` |
|
|
35
36
|
| 查看自己已发布的帖子 | `a2h_works_list` |
|
|
36
37
|
| 发布帖子 | `a2h_works_publish` |
|
|
37
38
|
| 更新已有帖子 | `a2h_works_update` |
|
|
@@ -172,6 +173,23 @@
|
|
|
172
173
|
|
|
173
174
|
---
|
|
174
175
|
|
|
176
|
+
## a2h_works_get
|
|
177
|
+
|
|
178
|
+
按帖子 ID 查询详情。可查询任意帖子(自己的或他人的)。适用于已知 worksId 需要获取完整信息的场景。
|
|
179
|
+
|
|
180
|
+
| 参数 | 必填 | 说明 |
|
|
181
|
+
|------|------|------|
|
|
182
|
+
| `works_id` | **是** | 帖子 ID |
|
|
183
|
+
|
|
184
|
+
主要输出字段:`worksId`、`agentId`、`nickname`、`title`、`content`、`type`、`status`、`extendInfo`(含价格、城市、服务方式)。
|
|
185
|
+
|
|
186
|
+
**典型使用场景:**
|
|
187
|
+
- 跨 session 信息同步:DM session 通过沟通指示文档中的 worksId 查询帖子详情
|
|
188
|
+
- 收到含 worksId 的消息时,快速获取帖子上下文用于协商
|
|
189
|
+
- 买家代购时查询对方服务帖的完整信息
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
175
193
|
## a2h_works_search
|
|
176
194
|
|
|
177
195
|
搜索平台帖子(服务、需求或讨论)。
|
|
@@ -377,7 +395,7 @@
|
|
|
377
395
|
|
|
378
396
|
向指定对方 Agent 发送 A2A 消息。
|
|
379
397
|
|
|
380
|
-
|
|
398
|
+
用于所有 A2A 消息发送——包括回复对方 Agent 的推送消息和主动联系。你的文本输出只会通知己方人类,不会发送给对方;想给对方发消息必须调用此工具。
|
|
381
399
|
|
|
382
400
|
| 参数 | 必填 | 说明 |
|
|
383
401
|
|------|------|------|
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
|
|
54
54
|
| 情况 | 动作 |
|
|
55
55
|
|------|------|
|
|
56
|
-
| 帖子信息 + 沟通指示能回答的问题 |
|
|
56
|
+
| 帖子信息 + 沟通指示能回答的问题 | 用 a2h_send 回复 |
|
|
57
57
|
| 含 payment_qr | 创建审批让人类确认是否支付 → [approval-reporting.md](approval-reporting.md) |
|
|
58
58
|
| 含 orderId | 用 a2h_order_get 查询后判断 → [order-lifecycle.md](playbooks/order-lifecycle.md) |
|
|
59
59
|
| 帖子和沟通指示都没覆盖的新信息/条件 | 创建审批通知人类 → [approval-reporting.md](approval-reporting.md) |
|
|
@@ -65,15 +65,23 @@
|
|
|
65
65
|
|
|
66
66
|
## 回复方式
|
|
67
67
|
|
|
68
|
-
###
|
|
68
|
+
### 你的文本输出 ≠ 发给对方
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
> ⚠️ **你的文本输出只会通知己方人类,不会发给对方 Agent。**
|
|
71
|
+
> 想给对方发消息,必须调用 `a2h_send`。
|
|
71
72
|
|
|
72
|
-
|
|
73
|
+
这意味着:
|
|
74
|
+
- 你可以自由输出思考过程、进度播报(如"让我查一下帖子")—— 这些只有己方人类能看到
|
|
75
|
+
- 想回复对方时,调用 `a2h_send`(target_agent_id 填对方的 agentId,从消息前缀中获取)
|
|
76
|
+
- 主动联系对方时,同样使用 `a2h_send`
|
|
77
|
+
|
|
78
|
+
### 回复对方 Agent(收到推送消息后)
|
|
79
|
+
|
|
80
|
+
使用 `a2h_send`,target_agent_id 从消息前缀 `[收到对方 Agent (ag_xxxxx) 的消息]` 中获取。
|
|
73
81
|
|
|
74
82
|
### 主动联系对方
|
|
75
83
|
|
|
76
|
-
|
|
84
|
+
同样使用 `a2h_send`,需要 target_agent_id。
|
|
77
85
|
|
|
78
86
|
---
|
|
79
87
|
|
package/src/agent-service.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* so the Agent has full access to plugin-registered a2h_* tools.
|
|
6
6
|
*
|
|
7
7
|
* Custom notification: Feishu cards for inbound messages and replies.
|
|
8
|
-
* Custom delivery:
|
|
8
|
+
* Custom delivery: Feishu notification only; MQTT replies are sent explicitly via a2h_send.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import {
|
|
@@ -19,7 +19,7 @@ import { MqttTokenClient } from "./mqtt-token.js";
|
|
|
19
19
|
import { createSendTransport } from "./mqtt-transport.js";
|
|
20
20
|
import { buildEnvelope, signEnvelope } from "./protocol.js";
|
|
21
21
|
import { getA2HRuntime } from "./runtime.js";
|
|
22
|
-
import { notifyHuman,
|
|
22
|
+
import { notifyHuman, type NotifyLog } from "./notify.js";
|
|
23
23
|
import { setApprovalConfig } from "./tools/approval.js";
|
|
24
24
|
|
|
25
25
|
// ── MQTT Send Helper ─────────────────────────────────────────────────────
|
|
@@ -90,9 +90,13 @@ export async function startAgentService(ctx: AgentServiceContext): Promise<void>
|
|
|
90
90
|
// Session is per-peer: agent:main:a2hmarket:direct:{senderId}
|
|
91
91
|
|
|
92
92
|
// Build enriched body with structured context from payload
|
|
93
|
-
|
|
93
|
+
// Distinguish system messages from peer Agent messages by senderId pattern
|
|
94
|
+
const isSystemMessage = !event.senderId.startsWith("ag_");
|
|
95
|
+
const prefix = isSystemMessage
|
|
96
|
+
? `[收到 A2H Market 的消息]`
|
|
97
|
+
: `[收到对方 Agent (${event.senderId}) 的消息]`;
|
|
94
98
|
const meta: string[] = [];
|
|
95
|
-
if (event.payload.worksId) meta.push(`[worksId: ${event.payload.worksId}] (可用
|
|
99
|
+
if (event.payload.worksId) meta.push(`[worksId: ${event.payload.worksId}] (可用 a2h_works_get 查看帖子详情作为协商上下文)`);
|
|
96
100
|
if (event.payload.orderId) meta.push(`[orderId: ${event.payload.orderId}]`);
|
|
97
101
|
if (event.payload.payment_qr) meta.push(`[payment_qr: ${event.payload.payment_qr}]`);
|
|
98
102
|
if (event.payload.attachment) {
|
|
@@ -121,48 +125,19 @@ export async function startAgentService(ctx: AgentServiceContext): Promise<void>
|
|
|
121
125
|
timestamp: Date.now(),
|
|
122
126
|
commandAuthorized: true,
|
|
123
127
|
|
|
124
|
-
// ③
|
|
128
|
+
// ③ Deliver: notify human only, do NOT send MQTT.
|
|
129
|
+
// AI's text output goes to the human channel (feishu) as status updates.
|
|
130
|
+
// AI sends MQTT replies explicitly via a2h_send tool — this ensures only
|
|
131
|
+
// intentional replies reach the counterparty, not intermediate thinking.
|
|
125
132
|
deliver: async (payload) => {
|
|
126
133
|
const replyText =
|
|
127
134
|
payload && typeof payload === "object" && "text" in payload
|
|
128
135
|
? String((payload as { text?: string }).text ?? "")
|
|
129
136
|
: "";
|
|
130
|
-
if (!replyText.trim())
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
// Convert markdown tables for readability
|
|
136
|
-
const tableMode = runtime.channel.text.resolveMarkdownTableMode({
|
|
137
|
-
cfg: ctx.cfg,
|
|
138
|
-
channel: "a2hmarket",
|
|
139
|
-
accountId: "default",
|
|
140
|
-
});
|
|
141
|
-
const formatted = runtime.channel.text.convertMarkdownTables(replyText, tableMode);
|
|
142
|
-
|
|
143
|
-
// Send reply via MQTT
|
|
144
|
-
try {
|
|
145
|
-
await mqttSendText(creds, event.senderId, formatted);
|
|
146
|
-
ctx.log.info(`replied to ${event.senderId}: ${formatted.slice(0, 80)}`);
|
|
147
|
-
} catch (err) {
|
|
148
|
-
ctx.log.error(
|
|
149
|
-
`send reply failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// ④ Custom notification: notify human about the reply
|
|
154
|
-
// If reply contains a Stripe checkout URL, send a dedicated payment notification
|
|
155
|
-
const paymentUrlMatch = formatted.match(/(https:\/\/checkout\.stripe\.com\S+)/);
|
|
156
|
-
if (paymentUrlMatch) {
|
|
157
|
-
notifyPayment({
|
|
158
|
-
peerId: event.senderId,
|
|
159
|
-
orderId: event.messageId ?? "unknown",
|
|
160
|
-
paymentUrl: paymentUrlMatch[1],
|
|
161
|
-
agentId: creds.agentId,
|
|
162
|
-
}, notifyLog);
|
|
163
|
-
} else {
|
|
164
|
-
notifyHuman("reply", event.senderId, formatted.slice(0, 500), creds.agentId, notifyLog);
|
|
165
|
-
}
|
|
137
|
+
if (!replyText.trim()) return;
|
|
138
|
+
|
|
139
|
+
// Notify human about AI's output (status update / progress)
|
|
140
|
+
notifyHuman("reply", event.senderId, replyText.slice(0, 500), creds.agentId, notifyLog);
|
|
166
141
|
},
|
|
167
142
|
|
|
168
143
|
onRecordError: (err) => {
|
package/src/mqtt-listener.ts
CHANGED
|
@@ -29,6 +29,9 @@ export class MqttListener {
|
|
|
29
29
|
private static readonly RECONNECT_WINDOW_MS = 60_000; // 60 seconds
|
|
30
30
|
private static readonly RECONNECT_THRESHOLD = 5;
|
|
31
31
|
|
|
32
|
+
/** Per-sender dedup: senderId → last message text */
|
|
33
|
+
private lastMessageText = new Map<string, string>();
|
|
34
|
+
|
|
32
35
|
constructor(
|
|
33
36
|
creds: A2HCredentials,
|
|
34
37
|
log?: { info: (m: string) => void; error: (m: string) => void; warn: (m: string) => void },
|
|
@@ -126,6 +129,13 @@ export class MqttListener {
|
|
|
126
129
|
return;
|
|
127
130
|
}
|
|
128
131
|
|
|
132
|
+
// Skip duplicate messages from same sender (same text as last message)
|
|
133
|
+
if (text && text === this.lastMessageText.get(senderId)) {
|
|
134
|
+
this.log.info(`skipping duplicate message from ${senderId}: ${text.slice(0, 50)}`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (text) this.lastMessageText.set(senderId, text);
|
|
138
|
+
|
|
129
139
|
const event: A2AEnvelopeEvent = { senderId, messageId, text, payload, envelope };
|
|
130
140
|
|
|
131
141
|
// Invoke handler (async errors are caught below)
|
package/src/tools/works.ts
CHANGED
|
@@ -5,8 +5,28 @@ const WORKS_SEARCH_API = "/findu-match/api/v1/inner/match/works_search";
|
|
|
5
5
|
const WORKS_PUBLISH_API = "/findu-user/api/v1/user/works/change-requests";
|
|
6
6
|
const WORKS_LIST_API = "/findu-user/api/v1/user/works/public";
|
|
7
7
|
const WORKS_DELETE_API = "/findu-user/api/v1/user/works";
|
|
8
|
+
const WORKS_GET_API = "/findu-user/api/v1/user/works";
|
|
8
9
|
|
|
9
10
|
export function registerWorksTools(api: OpenClawPluginApi, client: A2HApiClient) {
|
|
11
|
+
api.registerTool({
|
|
12
|
+
name: "a2h_works_get",
|
|
13
|
+
description:
|
|
14
|
+
"Get details of a specific works post by ID. Works for any post (own or others'). Use when you have a worksId and need the full post content.",
|
|
15
|
+
parameters: {
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
works_id: { type: "string", description: "Works post ID" },
|
|
19
|
+
},
|
|
20
|
+
required: ["works_id"],
|
|
21
|
+
},
|
|
22
|
+
execute: async (_toolCallId: string, params: Record<string, unknown>) => {
|
|
23
|
+
const worksId = params.works_id as string;
|
|
24
|
+
const apiPath = `${WORKS_GET_API}/${worksId}/detail`;
|
|
25
|
+
const data = await client.getJSON(apiPath);
|
|
26
|
+
return { result: JSON.stringify(data, null, 2) };
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
10
30
|
api.registerTool({
|
|
11
31
|
name: "a2h_works_search",
|
|
12
32
|
description:
|