@a2hmarket/a2hmarket 0.10.1 → 1.0.0
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 +12 -4
- package/openclaw.plugin.json +4 -1
- package/package.json +4 -1
- package/scripts/install.mjs +26 -13
- package/skills/a2hmarket/references/approval-reporting.md +1 -1
- package/skills/a2hmarket/references/commands.md +1 -1
- package/skills/a2hmarket/references/message-routing.md +13 -5
- package/skills/a2hmarket/references/playbooks/negotiation.md +1 -1
- package/src/agent-service.ts +11 -46
- package/src/credentials.ts +7 -7
- package/src/mqtt-listener.ts +10 -0
- package/src/tempo-key-manager.ts +1 -1
package/index.ts
CHANGED
|
@@ -172,11 +172,19 @@ export default {
|
|
|
172
172
|
api.registerService({
|
|
173
173
|
id: "a2hmarket-agent",
|
|
174
174
|
start: async (ctx) => {
|
|
175
|
-
// Initialize runtime data directory: ~/.
|
|
176
|
-
const
|
|
177
|
-
|
|
175
|
+
// Initialize runtime data directory: ~/.a2h_store/
|
|
176
|
+
const { homedir } = await import("node:os");
|
|
177
|
+
const storeDir = join(homedir(), ".a2h_store");
|
|
178
|
+
const configDir = join(storeDir, "a2h_config");
|
|
179
|
+
const dataDir = join(storeDir, "a2h_data");
|
|
180
|
+
const inboxDir = join(storeDir, "a2h_inbox");
|
|
181
|
+
const negotiationDir = join(storeDir, "a2h_negotiation");
|
|
182
|
+
const logsDir = join(storeDir, "a2h_logs");
|
|
183
|
+
for (const d of [configDir, dataDir, inboxDir, negotiationDir, logsDir]) {
|
|
184
|
+
mkdirSync(d, { recursive: true });
|
|
185
|
+
}
|
|
178
186
|
|
|
179
|
-
// Initialize stores
|
|
187
|
+
// Initialize stores
|
|
180
188
|
initReplyBridge(join(dataDir, "reply-bridge.json"));
|
|
181
189
|
initApprovalStore(join(dataDir, "approvals.json"));
|
|
182
190
|
setLastChannelStore(new LastChannelStore(join(dataDir, "last-channel.json")));
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a2hmarket/a2hmarket",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"index.ts",
|
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
"extensions": [
|
|
28
28
|
"./index.ts"
|
|
29
29
|
],
|
|
30
|
+
"build": {
|
|
31
|
+
"openclawVersion": "2026.3.24"
|
|
32
|
+
},
|
|
30
33
|
"install": {
|
|
31
34
|
"npmSpec": "@a2hmarket/openclaw-plugin",
|
|
32
35
|
"defaultChoice": "npm"
|
package/scripts/install.mjs
CHANGED
|
@@ -21,8 +21,11 @@ import { createHash, createHmac, randomBytes } from "node:crypto";
|
|
|
21
21
|
import { networkInterfaces } from "node:os";
|
|
22
22
|
|
|
23
23
|
const OPENCLAW_DIR = join(homedir(), ".openclaw");
|
|
24
|
-
const
|
|
25
|
-
const CREDS_FILE = join(
|
|
24
|
+
const CREDS_DIR = join(OPENCLAW_DIR, "a2hmarket");
|
|
25
|
+
const CREDS_FILE = join(CREDS_DIR, "credentials.json");
|
|
26
|
+
const A2H_STORE_DIR = join(homedir(), ".a2h_store");
|
|
27
|
+
const A2H_CONFIG_DIR = join(A2H_STORE_DIR, "a2h_config");
|
|
28
|
+
const A2H_DATA_DIR = join(A2H_STORE_DIR, "a2h_data");
|
|
26
29
|
const NPM_SPEC = "@a2hmarket/a2hmarket";
|
|
27
30
|
|
|
28
31
|
const AUTH_API_URL = "https://web.a2hmarket.ai";
|
|
@@ -363,7 +366,7 @@ async function runUpdate() {
|
|
|
363
366
|
execSync(`rm -rf "${extDir}"`, { stdio: "pipe" });
|
|
364
367
|
}
|
|
365
368
|
log(` Installing new version...`);
|
|
366
|
-
execSync(`
|
|
369
|
+
execSync(`yes 2>/dev/null | openclaw plugins install ${NPM_SPEC} 2>&1`, { encoding: "utf-8", stdio: "pipe" });
|
|
367
370
|
log(` ${CHECK} Update complete`);
|
|
368
371
|
} catch (err) {
|
|
369
372
|
log(` ${CROSS} Update failed: ${err.message}`);
|
|
@@ -372,7 +375,7 @@ async function runUpdate() {
|
|
|
372
375
|
|
|
373
376
|
// Restore credentials file
|
|
374
377
|
if (savedCreds) {
|
|
375
|
-
mkdirSync(
|
|
378
|
+
mkdirSync(CREDS_DIR, { recursive: true });
|
|
376
379
|
const fileData = {
|
|
377
380
|
agent_id: savedCreds.agentId ?? savedCreds.agent_id,
|
|
378
381
|
agent_key: savedCreds.agentKey ?? savedCreds.agent_key,
|
|
@@ -483,12 +486,14 @@ async function runUninstall() {
|
|
|
483
486
|
}
|
|
484
487
|
|
|
485
488
|
// 2. Remove runtime data
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
}
|
|
491
|
-
|
|
489
|
+
for (const dir of [CREDS_DIR, A2H_STORE_DIR]) {
|
|
490
|
+
if (existsSync(dir)) {
|
|
491
|
+
try {
|
|
492
|
+
execSync(`rm -rf "${dir}"`, { stdio: "pipe" });
|
|
493
|
+
log(` ${CHECK} Removed: ${dir}`);
|
|
494
|
+
} catch {
|
|
495
|
+
log(` ${WARN} Failed to remove: ${dir}`);
|
|
496
|
+
}
|
|
492
497
|
}
|
|
493
498
|
}
|
|
494
499
|
|
|
@@ -673,8 +678,13 @@ async function main() {
|
|
|
673
678
|
}
|
|
674
679
|
} catch {
|
|
675
680
|
try {
|
|
681
|
+
// Remove stale extension directory if it exists (openclaw refuses to overwrite)
|
|
682
|
+
const extDir = join(OPENCLAW_DIR, "extensions", "a2hmarket");
|
|
683
|
+
if (existsSync(extDir)) {
|
|
684
|
+
execSync(`rm -rf "${extDir}"`, { stdio: "pipe" });
|
|
685
|
+
}
|
|
676
686
|
log(` Installing...`);
|
|
677
|
-
execSync(`
|
|
687
|
+
execSync(`yes 2>/dev/null | openclaw plugins install ${NPM_SPEC} 2>&1`, {
|
|
678
688
|
encoding: "utf-8",
|
|
679
689
|
stdio: "pipe",
|
|
680
690
|
});
|
|
@@ -742,7 +752,9 @@ async function main() {
|
|
|
742
752
|
|
|
743
753
|
// ── Step 4: Save credentials & configure openclaw.json ───────
|
|
744
754
|
logStep(4, "Save Configuration");
|
|
745
|
-
mkdirSync(
|
|
755
|
+
mkdirSync(CREDS_DIR, { recursive: true });
|
|
756
|
+
mkdirSync(A2H_CONFIG_DIR, { recursive: true });
|
|
757
|
+
mkdirSync(A2H_DATA_DIR, { recursive: true });
|
|
746
758
|
|
|
747
759
|
const credsData = {
|
|
748
760
|
agent_id: agentId,
|
|
@@ -896,7 +908,8 @@ async function main() {
|
|
|
896
908
|
// ── Done ───────────────────────────────────────────────────────
|
|
897
909
|
log(`\n${GREEN}${BOLD}🎉 Setup complete!${RESET}\n`);
|
|
898
910
|
log(` Agent ID: ${CYAN}${agentId}${RESET}`);
|
|
899
|
-
log(`
|
|
911
|
+
log(` Credentials: ${DIM}${CREDS_DIR}${RESET}`);
|
|
912
|
+
log(` Data dir: ${DIM}${A2H_STORE_DIR}${RESET}`);
|
|
900
913
|
if (credsData.notify) {
|
|
901
914
|
log(` Notify: ${CYAN}${credsData.notify.channel}:${credsData.notify.target}${RESET}`);
|
|
902
915
|
}
|
|
@@ -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 ─────────────────────────────────────────────────────
|
|
@@ -94,7 +94,7 @@ export async function startAgentService(ctx: AgentServiceContext): Promise<void>
|
|
|
94
94
|
const isSystemMessage = !event.senderId.startsWith("ag_");
|
|
95
95
|
const prefix = isSystemMessage
|
|
96
96
|
? `[收到 A2H Market 的消息]`
|
|
97
|
-
: `[收到对方 Agent (${event.senderId})
|
|
97
|
+
: `[收到对方 Agent (${event.senderId}) 的消息,参考a2hmarket skill进行回复]`;
|
|
98
98
|
const meta: string[] = [];
|
|
99
99
|
if (event.payload.worksId) meta.push(`[worksId: ${event.payload.worksId}] (可用 a2h_works_get 查看帖子详情作为协商上下文)`);
|
|
100
100
|
if (event.payload.orderId) meta.push(`[orderId: ${event.payload.orderId}]`);
|
|
@@ -125,54 +125,19 @@ export async function startAgentService(ctx: AgentServiceContext): Promise<void>
|
|
|
125
125
|
timestamp: Date.now(),
|
|
126
126
|
commandAuthorized: true,
|
|
127
127
|
|
|
128
|
-
// ③
|
|
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.
|
|
129
132
|
deliver: async (payload) => {
|
|
130
133
|
const replyText =
|
|
131
134
|
payload && typeof payload === "object" && "text" in payload
|
|
132
135
|
? String((payload as { text?: string }).text ?? "")
|
|
133
136
|
: "";
|
|
134
|
-
if (!replyText.trim())
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
// System messages: do not reply via MQTT (no valid target)
|
|
140
|
-
if (isSystemMessage) {
|
|
141
|
-
ctx.log.info(`system message from ${event.senderId}, skipping MQTT reply`);
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Convert markdown tables for readability
|
|
146
|
-
const tableMode = runtime.channel.text.resolveMarkdownTableMode({
|
|
147
|
-
cfg: ctx.cfg,
|
|
148
|
-
channel: "a2hmarket",
|
|
149
|
-
accountId: "default",
|
|
150
|
-
});
|
|
151
|
-
const formatted = runtime.channel.text.convertMarkdownTables(replyText, tableMode);
|
|
152
|
-
|
|
153
|
-
// Send reply via MQTT
|
|
154
|
-
try {
|
|
155
|
-
await mqttSendText(creds, event.senderId, formatted);
|
|
156
|
-
ctx.log.info(`replied to ${event.senderId}: ${formatted.slice(0, 80)}`);
|
|
157
|
-
} catch (err) {
|
|
158
|
-
ctx.log.error(
|
|
159
|
-
`send reply failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// ④ Custom notification: notify human about the reply
|
|
164
|
-
// If reply contains a Stripe checkout URL, send a dedicated payment notification
|
|
165
|
-
const paymentUrlMatch = formatted.match(/(https:\/\/checkout\.stripe\.com\S+)/);
|
|
166
|
-
if (paymentUrlMatch) {
|
|
167
|
-
notifyPayment({
|
|
168
|
-
peerId: event.senderId,
|
|
169
|
-
orderId: event.messageId ?? "unknown",
|
|
170
|
-
paymentUrl: paymentUrlMatch[1],
|
|
171
|
-
agentId: creds.agentId,
|
|
172
|
-
}, notifyLog);
|
|
173
|
-
} else {
|
|
174
|
-
notifyHuman("reply", event.senderId, formatted.slice(0, 500), creds.agentId, notifyLog);
|
|
175
|
-
}
|
|
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);
|
|
176
141
|
},
|
|
177
142
|
|
|
178
143
|
onRecordError: (err) => {
|
package/src/credentials.ts
CHANGED
|
@@ -52,8 +52,8 @@ export function loadCredentialsFromConfig(
|
|
|
52
52
|
// ── Load from file — fallback for dev mode ─────────────────────────────
|
|
53
53
|
|
|
54
54
|
const PLUGIN_DIR = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
55
|
-
const
|
|
56
|
-
const
|
|
55
|
+
const OPENCLAW_CREDS_DIR = join(homedir(), ".openclaw", "a2hmarket");
|
|
56
|
+
const A2H_CONFIG_DIR = join(homedir(), ".a2h_store", "a2h_config");
|
|
57
57
|
const CREDENTIALS_FILE = "credentials.json";
|
|
58
58
|
|
|
59
59
|
interface RawCredentials {
|
|
@@ -74,14 +74,14 @@ export function loadCredentialsFromFile(configDir?: string): A2HCredentials {
|
|
|
74
74
|
if (configDir) {
|
|
75
75
|
dir = configDir;
|
|
76
76
|
} else {
|
|
77
|
-
// Priority:
|
|
78
|
-
const
|
|
77
|
+
// Priority: ~/.openclaw/a2hmarket/ > plugin dir > ~/.a2h_store/a2h_config/
|
|
78
|
+
const credsPath = join(OPENCLAW_CREDS_DIR, CREDENTIALS_FILE);
|
|
79
79
|
const pluginPath = join(PLUGIN_DIR, CREDENTIALS_FILE);
|
|
80
|
-
dir = existsSync(
|
|
81
|
-
?
|
|
80
|
+
dir = existsSync(credsPath)
|
|
81
|
+
? OPENCLAW_CREDS_DIR
|
|
82
82
|
: existsSync(pluginPath)
|
|
83
83
|
? PLUGIN_DIR
|
|
84
|
-
:
|
|
84
|
+
: A2H_CONFIG_DIR;
|
|
85
85
|
}
|
|
86
86
|
const filePath = join(dir, CREDENTIALS_FILE);
|
|
87
87
|
|
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/tempo-key-manager.ts
CHANGED
|
@@ -35,7 +35,7 @@ export interface TempoKeyResult {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
// Fallback storage path for non-macOS environments
|
|
38
|
-
const FALLBACK_KEY_DIR = join(homedir(), ".
|
|
38
|
+
const FALLBACK_KEY_DIR = join(homedir(), ".a2h_store", "a2h_data");
|
|
39
39
|
const FALLBACK_KEY_FILE = join(FALLBACK_KEY_DIR, ".tempo-key");
|
|
40
40
|
|
|
41
41
|
// ── Internal: read/write fallback file ──────────────────────────────────────
|