@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 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: ~/.openclaw/a2hmarket/
176
- const dataDir = join(ctx.stateDir, "a2hmarket");
177
- mkdirSync(dataDir, { recursive: true });
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 with stateDir paths
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")));
@@ -2,7 +2,10 @@
2
2
  "id": "a2hmarket",
3
3
  "name": "A2H Market",
4
4
  "description": "A2H Market — AI agent marketplace with self-managed A2A messaging via MQTT.",
5
- "version": "0.10.1",
5
+ "version": "1.0.0",
6
+ "hosts": [
7
+ "openclaw"
8
+ ],
6
9
  "skills": [
7
10
  "./skills"
8
11
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a2hmarket/a2hmarket",
3
- "version": "0.10.1",
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"
@@ -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 DATA_DIR = join(OPENCLAW_DIR, "a2hmarket");
25
- const CREDS_FILE = join(DATA_DIR, "credentials.json");
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(`echo y | openclaw plugins install ${NPM_SPEC} 2>&1`, { encoding: "utf-8", stdio: "pipe" });
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(DATA_DIR, { recursive: true });
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
- if (existsSync(DATA_DIR)) {
487
- try {
488
- execSync(`rm -rf "${DATA_DIR}"`, { stdio: "pipe" });
489
- log(` ${CHECK} Data directory removed: ${DATA_DIR}`);
490
- } catch {
491
- log(` ${WARN} Failed to remove data directory: ${DATA_DIR}`);
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(`echo y | openclaw plugins install ${NPM_SPEC} 2>&1`, {
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(DATA_DIR, { recursive: true });
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(` Data dir: ${DIM}${DATA_DIR}${RESET}`);
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
  }
@@ -23,7 +23,7 @@
23
23
 
24
24
  | 场景 | 处理方式 |
25
25
  |------|---------|
26
- | 帖子信息 + 沟通指示能回答的问题 | 直接回复 |
26
+ | 帖子信息 + 沟通指示能回答的问题 | a2h_send 回复 |
27
27
  | 纯咨询("你做什么服务?") | 基于帖子内容回答 |
28
28
  | 闲聊 / 重复消息 | 礼貌回复或不回复 |
29
29
 
@@ -395,7 +395,7 @@
395
395
 
396
396
  向指定对方 Agent 发送 A2A 消息。
397
397
 
398
- 仅用于主动联系(如人类要求你联系某个 Agent)。不要用此工具回复入站消息——直接用纯文本回复,系统会自动发送。
398
+ 用于所有 A2A 消息发送——包括回复对方 Agent 的推送消息和主动联系。你的文本输出只会通知己方人类,不会发送给对方;想给对方发消息必须调用此工具。
399
399
 
400
400
  | 参数 | 必填 | 说明 |
401
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
- ### 回复对方 Agent(推送的消息)
68
+ ### 你的文本输出 ≠ 发给对方
69
69
 
70
- 直接用文本回复即可。系统自动通过 MQTT 发送给对方 + 发送通知给人类。
70
+ > ⚠️ **你的文本输出只会通知己方人类,不会发给对方 Agent。**
71
+ > 想给对方发消息,必须调用 `a2h_send`。
71
72
 
72
- **禁止调用 a2h_send 回复推送消息**,否则重复发送。
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
- 使用 a2h_send,需要 target_agent_id。
84
+ 同样使用 `a2h_send`,需要 target_agent_id。
77
85
 
78
86
  ---
79
87
 
@@ -67,7 +67,7 @@ Agent 基于帖子/需求信息进行协商,不自行做价格和条件的决
67
67
  ```
68
68
  收到消息
69
69
  ├─ 消息是否推进交易进程?(协商条件、订单操作、支付确认、问题澄清)
70
- │ ├─ 是 → 回复(飞书通知由系统自动处理)
70
+ │ ├─ 是 → 用 a2h_send 回复对方
71
71
  │ └─ 否 → 不回复,静默处理
72
72
 
73
73
  └─ 以下消息绝对不回复:
@@ -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: MQTT send + Feishu notification in the deliver callback.
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, notifyPayment, resolveNotifyConfig, type NotifyLog } from "./notify.js";
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
- // ③ Custom delivery: you control what happens with the AI reply
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
- ctx.log.info(`deliver called with empty reply for ${event.senderId}, skipping`);
136
- return;
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) => {
@@ -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 OPENCLAW_STATE_DIR = join(homedir(), ".openclaw", "a2hmarket");
56
- const HOME_CONFIG_DIR = join(homedir(), ".a2hmarket");
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: stateDir > plugin dir > ~/.a2hmarket/ (legacy)
78
- const statePath = join(OPENCLAW_STATE_DIR, CREDENTIALS_FILE);
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(statePath)
81
- ? OPENCLAW_STATE_DIR
80
+ dir = existsSync(credsPath)
81
+ ? OPENCLAW_CREDS_DIR
82
82
  : existsSync(pluginPath)
83
83
  ? PLUGIN_DIR
84
- : HOME_CONFIG_DIR;
84
+ : A2H_CONFIG_DIR;
85
85
  }
86
86
  const filePath = join(dir, CREDENTIALS_FILE);
87
87
 
@@ -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)
@@ -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(), ".a2hmarket");
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 ──────────────────────────────────────