@a2hmarket/a2hmarket 2026.3.19

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.
@@ -0,0 +1,251 @@
1
+ /**
2
+ * HTTP API client for the A2HMarket platform.
3
+ * Implements HMAC-SHA256 signed requests and standard platform response parsing.
4
+ */
5
+
6
+ import { createHmac } from "node:crypto";
7
+ import { readCredentials } from "./credentials.js";
8
+
9
+ export type Credentials = {
10
+ agent_id: string;
11
+ agent_key: string;
12
+ api_url: string;
13
+ mqtt_url: string;
14
+ push_enabled?: boolean;
15
+ expires_at?: string;
16
+ };
17
+
18
+ // Platform standard response envelope
19
+ type PlatformResponse<T> = {
20
+ code: string | number;
21
+ message: string;
22
+ data?: T;
23
+ };
24
+
25
+ export class PlatformError extends Error {
26
+ constructor(
27
+ public readonly code: string | number,
28
+ public readonly platformMessage: string,
29
+ ) {
30
+ super(`Platform error ${code}: ${platformMessage}`);
31
+ this.name = "PlatformError";
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Compute HMAC-SHA256 signature.
37
+ * payload = "{METHOD}&{signPath}&{agentId}&{timestampSec}"
38
+ */
39
+ function computeSignature(
40
+ agentKey: string,
41
+ method: string,
42
+ signPath: string,
43
+ agentId: string,
44
+ timestampSec: string,
45
+ ): string {
46
+ const payload = `${method}&${signPath}&${agentId}&${timestampSec}`;
47
+ return createHmac("sha256", agentKey).update(payload).digest("hex");
48
+ }
49
+
50
+ /**
51
+ * Build signed headers for an API request.
52
+ */
53
+ function buildSignedHeaders(
54
+ creds: Credentials,
55
+ method: string,
56
+ signPath: string,
57
+ ): Record<string, string> {
58
+ const timestampSec = String(Math.floor(Date.now() / 1000));
59
+ const signature = computeSignature(
60
+ creds.agent_key,
61
+ method,
62
+ signPath,
63
+ creds.agent_id,
64
+ timestampSec,
65
+ );
66
+ return {
67
+ "Content-Type": "application/json",
68
+ "X-Agent-Id": creds.agent_id,
69
+ "X-Timestamp": timestampSec,
70
+ "X-Agent-Signature": signature,
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Parse a platform standard response envelope.
76
+ * Throws PlatformError if code != 200.
77
+ */
78
+ function parsePlatformResponse<T>(body: PlatformResponse<T>): T {
79
+ const code = String(body.code).trim();
80
+ if (code !== "200") {
81
+ throw new PlatformError(body.code, body.message ?? "Unknown error");
82
+ }
83
+ return body.data as T;
84
+ }
85
+
86
+ export class ApiClient {
87
+ private readonly creds: Credentials;
88
+ private readonly baseUrl: string;
89
+
90
+ constructor(creds: Credentials, baseUrl?: string) {
91
+ this.creds = creds;
92
+ this.baseUrl = baseUrl ?? creds.api_url;
93
+ }
94
+
95
+ /** GET request with HMAC signature. signPath defaults to apiPath (no query string). */
96
+ async get<T>(apiPath: string, signPath?: string): Promise<T> {
97
+ const effectiveSignPath = signPath ?? apiPath.split("?")[0];
98
+ const headers = buildSignedHeaders(this.creds, "GET", effectiveSignPath);
99
+ const url = `${this.baseUrl}${apiPath}`;
100
+ const resp = await fetch(url, { method: "GET", headers });
101
+ if (!resp.ok) {
102
+ throw new PlatformError(resp.status, `HTTP ${resp.status} ${resp.statusText}`);
103
+ }
104
+ const body = (await resp.json()) as PlatformResponse<T>;
105
+ return parsePlatformResponse(body);
106
+ }
107
+
108
+ /** POST request with HMAC signature. */
109
+ async post<T>(apiPath: string, body: unknown, signPath?: string): Promise<T> {
110
+ const effectiveSignPath = signPath ?? apiPath.split("?")[0];
111
+ const headers = buildSignedHeaders(this.creds, "POST", effectiveSignPath);
112
+ const url = `${this.baseUrl}${apiPath}`;
113
+ const resp = await fetch(url, {
114
+ method: "POST",
115
+ headers,
116
+ body: JSON.stringify(body),
117
+ });
118
+ if (!resp.ok) {
119
+ throw new PlatformError(resp.status, `HTTP ${resp.status} ${resp.statusText}`);
120
+ }
121
+ const respBody = (await resp.json()) as PlatformResponse<T>;
122
+ return parsePlatformResponse(respBody);
123
+ }
124
+
125
+ /** DELETE request with HMAC signature. */
126
+ async delete<T>(apiPath: string): Promise<T> {
127
+ const signPath = apiPath.split("?")[0];
128
+ const headers = buildSignedHeaders(this.creds, "DELETE", signPath);
129
+ const url = `${this.baseUrl}${apiPath}`;
130
+ const resp = await fetch(url, { method: "DELETE", headers });
131
+ if (!resp.ok) {
132
+ throw new PlatformError(resp.status, `HTTP ${resp.status} ${resp.statusText}`);
133
+ }
134
+ const respBody = (await resp.json()) as PlatformResponse<T>;
135
+ return parsePlatformResponse(respBody);
136
+ }
137
+
138
+ /**
139
+ * POST to a specific baseURL (for OSS and other cross-host calls).
140
+ * signPath must be provided explicitly (with service prefix).
141
+ */
142
+ async postToHost<T>(
143
+ baseUrl: string,
144
+ apiPath: string,
145
+ body: unknown,
146
+ signPath: string,
147
+ ): Promise<T> {
148
+ const headers = buildSignedHeaders(this.creds, "POST", signPath);
149
+ const url = `${baseUrl}${apiPath}`;
150
+ const resp = await fetch(url, {
151
+ method: "POST",
152
+ headers,
153
+ body: JSON.stringify(body),
154
+ });
155
+ if (!resp.ok) {
156
+ throw new PlatformError(resp.status, `HTTP ${resp.status} ${resp.statusText}`);
157
+ }
158
+ const respBody = (await resp.json()) as PlatformResponse<T>;
159
+ return parsePlatformResponse(respBody);
160
+ }
161
+
162
+ /**
163
+ * PUT binary data to a pre-signed URL (no business signature).
164
+ */
165
+ async putBinary(
166
+ uploadUrl: string,
167
+ signedHeaders: Record<string, string>,
168
+ data: Uint8Array,
169
+ ): Promise<void> {
170
+ const resp = await fetch(uploadUrl, {
171
+ method: "PUT",
172
+ headers: signedHeaders,
173
+ body: data,
174
+ });
175
+ if (!resp.ok) {
176
+ throw new Error(`OSS upload failed: HTTP ${resp.status} ${resp.statusText}`);
177
+ }
178
+ }
179
+
180
+ /** Expose credentials for use by specialized functions (e.g. fetchMqttToken). */
181
+ getCredentials(): Credentials {
182
+ return this.creds;
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Factory function: creates an ApiClient by reading credentials from disk.
188
+ * Throws if credentials are missing.
189
+ */
190
+ export function createApiClient(baseUrl?: string): ApiClient {
191
+ const creds = readCredentials();
192
+ if (!creds) {
193
+ throw new Error(
194
+ "A2HMarket credentials not found. Run: openclaw a2h setup",
195
+ );
196
+ }
197
+ return new ApiClient(creds, baseUrl);
198
+ }
199
+
200
+ /**
201
+ * Fetch MQTT token from the dedicated token endpoint.
202
+ * Response format: {"success": true, "data": {...}} — NOT the standard platform format.
203
+ */
204
+ export async function fetchMqttToken(creds: Credentials, clientId?: string): Promise<{
205
+ client_id: string;
206
+ instance_id: string;
207
+ username: string;
208
+ password: string;
209
+ expire_time: number;
210
+ }> {
211
+ const resolvedClientId = clientId ?? `GID_agent@@@${creds.agent_id}`;
212
+ const path = "/mqtt-token/api/v1/token";
213
+ const timestampSec = String(Math.floor(Date.now() / 1000));
214
+ const signature = computeSignature(
215
+ creds.agent_key,
216
+ "POST",
217
+ path,
218
+ creds.agent_id,
219
+ timestampSec,
220
+ );
221
+ const headers: Record<string, string> = {
222
+ "Content-Type": "application/json",
223
+ "X-Agent-Id": creds.agent_id,
224
+ "X-Timestamp": timestampSec,
225
+ "X-Agent-Signature": signature,
226
+ };
227
+ const url = `${creds.api_url}${path}`;
228
+ const resp = await fetch(url, {
229
+ method: "POST",
230
+ headers,
231
+ body: JSON.stringify({ client_id: resolvedClientId }),
232
+ });
233
+ if (!resp.ok) {
234
+ throw new Error(`MQTT token fetch failed: HTTP ${resp.status} ${resp.statusText}`);
235
+ }
236
+ const body = (await resp.json()) as {
237
+ success: boolean;
238
+ data?: {
239
+ client_id: string;
240
+ instance_id: string;
241
+ username: string;
242
+ password: string;
243
+ expire_time: number;
244
+ };
245
+ message?: string;
246
+ };
247
+ if (!body.success || !body.data) {
248
+ throw new Error(`MQTT token error: ${body.message ?? "unknown"}`);
249
+ }
250
+ return body.data;
251
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Auth Setup Command — `openclaw a2h setup`
3
+ * Generates an auth code, opens a browser login URL, polls for credentials,
4
+ * and writes them to ~/.a2hmarket/credentials.json.
5
+ */
6
+
7
+ import { createHash } from "node:crypto";
8
+ import { mkdirSync, writeFileSync } from "node:fs";
9
+ import { networkInterfaces } from "node:os";
10
+ import { homedir } from "node:os";
11
+ import { join } from "node:path";
12
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
13
+ import type { Credentials } from "../api-client.js";
14
+ import { CREDENTIALS_PATH } from "../credentials.js";
15
+
16
+ /** Generate N random hex chars (N must be even; length = N bytes → 2N chars). */
17
+ function randomHex(bytes: number): string {
18
+ const arr = new Uint8Array(bytes);
19
+ for (let i = 0; i < bytes; i++) {
20
+ arr[i] = Math.floor(Math.random() * 256);
21
+ }
22
+ return Array.from(arr)
23
+ .map((b) => b.toString(16).padStart(2, "0"))
24
+ .join("");
25
+ }
26
+
27
+ /** Get primary MAC address (first non-loopback interface). */
28
+ function getMacAddress(): string | null {
29
+ const ifaces = networkInterfaces();
30
+ for (const name of Object.keys(ifaces)) {
31
+ const list = ifaces[name];
32
+ if (!list) continue;
33
+ for (const iface of list) {
34
+ if (iface.internal) continue;
35
+ if (iface.mac && iface.mac !== "00:00:00:00:00:00") {
36
+ return iface.mac;
37
+ }
38
+ }
39
+ }
40
+ return null;
41
+ }
42
+
43
+ /** MD5 hash of a string → hex string. */
44
+ function md5(input: string): string {
45
+ return createHash("md5").update(input).digest("hex");
46
+ }
47
+
48
+ /** Write credentials to disk. */
49
+ function writeCredentials(creds: Credentials): void {
50
+ const dir = join(homedir(), ".a2hmarket");
51
+ mkdirSync(dir, { recursive: true });
52
+ writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2) + "\n", { mode: 0o600 });
53
+ }
54
+
55
+ type ServerCredentials = {
56
+ agentId: string;
57
+ secret: string;
58
+ };
59
+
60
+ type AuthCheckResponse = {
61
+ code: string | number;
62
+ message: string;
63
+ data?: ServerCredentials | null;
64
+ };
65
+
66
+ /** Poll the auth endpoint until credentials appear or timeout. */
67
+ async function pollForAuth(
68
+ checkUrl: string,
69
+ logger: { info: (msg: string) => void; warn: (msg: string) => void },
70
+ ): Promise<Credentials> {
71
+ const maxAttempts = 60;
72
+ const pollIntervalMs = 2000;
73
+
74
+ for (let i = 0; i < maxAttempts; i++) {
75
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
76
+
77
+ try {
78
+ const resp = await fetch(checkUrl, {
79
+ method: "GET",
80
+ headers: { Accept: "application/json" },
81
+ });
82
+
83
+ if (!resp.ok) {
84
+ logger.warn(`a2h setup: poll HTTP ${resp.status}, retrying...`);
85
+ continue;
86
+ }
87
+
88
+ const body = (await resp.json()) as AuthCheckResponse;
89
+ if (body.data?.agentId) {
90
+ return {
91
+ agent_id: body.data.agentId,
92
+ agent_key: body.data.secret,
93
+ api_url: "https://api.a2hmarket.ai",
94
+ mqtt_url: "mqtts://post-cn-e4k4o78q702.mqtt.aliyuncs.com:8883",
95
+ };
96
+ }
97
+ } catch (err) {
98
+ logger.warn(`a2h setup: poll error: ${String(err)}, retrying...`);
99
+ }
100
+
101
+ if ((i + 1) % 10 === 0) {
102
+ logger.info(`a2h setup: still waiting... (${i + 1}/${maxAttempts})`);
103
+ }
104
+ }
105
+
106
+ throw new Error("Authorization timed out (2 minutes). Please try again.");
107
+ }
108
+
109
+ export function addSetupCommand(a2h: import("commander").Command, api: OpenClawPluginApi): void {
110
+ a2h
111
+ .command("setup")
112
+ .description("Authorize this machine as an A2HMarket agent (opens browser login URL)")
113
+ .action(async () => {
114
+ const logger = api.logger;
115
+
116
+ // 1. Generate auth code:
117
+ // rawString = {random32hex}_{timestamp}_{mac_without_colons}
118
+ // authCode = MD5(rawString)
119
+ const timestamp = Math.floor(Date.now() / 1000);
120
+ const mac = getMacAddress() ?? "unknown";
121
+ const random32hex = randomHex(16); // 16 bytes = 32 hex chars
122
+ const cleanMac = mac.replace(/:/g, "");
123
+ const rawString = `${random32hex}_${timestamp}_${cleanMac}`;
124
+ const authCode = md5(rawString);
125
+
126
+ // 2. Try server-side init-login (optional; fail gracefully to local code)
127
+ let finalCode = authCode;
128
+ try {
129
+ const initResp = await fetch("https://web.a2hmarket.ai/v1/auth/init-login", {
130
+ method: "POST",
131
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
132
+ body: new URLSearchParams({
133
+ timestamp: String(timestamp),
134
+ mac: cleanMac,
135
+ feishu_user_id: "",
136
+ }).toString(),
137
+ });
138
+ if (initResp.ok) {
139
+ const initBody = (await initResp.json()) as { data?: { code?: string } };
140
+ if (initBody.data?.code) {
141
+ finalCode = initBody.data.code;
142
+ }
143
+ }
144
+ } catch {
145
+ // Silently fall back to locally generated code
146
+ }
147
+
148
+ // 3. Print login URL
149
+ const loginUrl = `https://a2hmarket.ai/authcode?code=${finalCode}`;
150
+ logger.info(`\n请在 PC 浏览器中打开以下链接扫码授权:\n${loginUrl}\n`);
151
+ logger.info("等待授权中(最多 2 分钟)...");
152
+
153
+ // 4. Poll for credentials
154
+ const checkUrl = `https://web.a2hmarket.ai/findu-user/api/v1/public/user/agent/auth?code=${finalCode}`;
155
+ try {
156
+ const creds = await pollForAuth(checkUrl, logger);
157
+
158
+ // 5. Write credentials to disk
159
+ writeCredentials(creds);
160
+ logger.info(`\n授权成功!Agent ID: ${creds.agent_id}`);
161
+ logger.info("凭证已保存到 ~/.a2hmarket/credentials.json");
162
+ } catch (err) {
163
+ logger.warn(`\n授权失败: ${String(err)}`);
164
+ process.exitCode = 1;
165
+ }
166
+ });
167
+ }
@@ -0,0 +1,56 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
2
+ import { readCredentials } from "../credentials.js";
3
+ import { getListenerStatus } from "../service/mqtt-listener.js";
4
+ import { peekUnread, getQueueSize } from "../api/inbox.js";
5
+
6
+ export function addStatusCommand(a2h: import("commander").Command, api: OpenClawPluginApi): void {
7
+ void api;
8
+ a2h
9
+ .command("status")
10
+ .description("Show A2HMarket agent status — credentials, MQTT connection, inbox queue")
11
+ .action(() => {
12
+ console.log("=== A2HMarket Status ===\n");
13
+
14
+ // Credentials
15
+ const creds = readCredentials();
16
+ if (!creds) {
17
+ console.log("Credentials: NOT FOUND");
18
+ console.log(" Run: openclaw a2h setup");
19
+ console.log("");
20
+ } else {
21
+ console.log("Credentials:");
22
+ console.log(` Agent ID : ${creds.agent_id}`);
23
+ console.log(` API URL : ${creds.api_url}`);
24
+ console.log(` MQTT URL : ${creds.mqtt_url}`);
25
+ if (creds.expires_at) {
26
+ const daysLeft = Math.floor(
27
+ (new Date(creds.expires_at).getTime() - Date.now()) /
28
+ (1000 * 60 * 60 * 24),
29
+ );
30
+ console.log(` Expires : ${creds.expires_at} (${daysLeft} days)`);
31
+ }
32
+ console.log("");
33
+ }
34
+
35
+ // MQTT listener status (in-memory, not PID file)
36
+ const listenerStatus = getListenerStatus();
37
+ if (listenerStatus.connected) {
38
+ console.log("MQTT Listener: CONNECTED");
39
+ console.log(` Session present : ${listenerStatus.sessionPresent}`);
40
+ } else {
41
+ console.log("MQTT Listener: DISCONNECTED");
42
+ if (listenerStatus.reconnectAttempts > 0) {
43
+ console.log(` Reconnect attempts: ${listenerStatus.reconnectAttempts}`);
44
+ }
45
+ }
46
+ console.log("");
47
+
48
+ // Inbox queue stats
49
+ const queueSize = getQueueSize();
50
+ const { unread } = peekUnread("default");
51
+ console.log("Inbox queue (in-memory):");
52
+ console.log(` Total events : ${queueSize}`);
53
+ console.log(` Unread : ${unread}`);
54
+ console.log("");
55
+ });
56
+ }
@@ -0,0 +1,36 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ export type A2HCredentials = {
6
+ agent_id: string;
7
+ agent_key: string;
8
+ api_url: string;
9
+ mqtt_url: string;
10
+ push_enabled?: boolean;
11
+ expires_at?: string;
12
+ };
13
+
14
+ const CREDENTIALS_PATH = join(homedir(), ".a2hmarket", "credentials.json");
15
+
16
+ /**
17
+ * Reads ~/.a2hmarket/credentials.json synchronously.
18
+ * Returns null if the file does not exist or cannot be parsed.
19
+ */
20
+ export function readCredentials(): A2HCredentials | null {
21
+ if (!existsSync(CREDENTIALS_PATH)) {
22
+ return null;
23
+ }
24
+ try {
25
+ const raw = readFileSync(CREDENTIALS_PATH, "utf-8");
26
+ const parsed = JSON.parse(raw) as A2HCredentials;
27
+ if (!parsed.agent_id || !parsed.agent_key) {
28
+ return null;
29
+ }
30
+ return parsed;
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ export { CREDENTIALS_PATH };
@@ -0,0 +1,83 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
2
+ import { readCredentials } from "../credentials.js";
3
+ import { setCachedSessionKey } from "../session-cache.js";
4
+ import { getListenerStatus } from "../service/mqtt-listener.js";
5
+ import { peekUnread } from "../api/inbox.js";
6
+
7
+ // Static system context — appended to system prompt for prompt caching
8
+ const SYSTEM_CONTEXT = `
9
+ <a2hmarket-role>
10
+ You are an AI agent on the A2HMarket platform (a2hmarket.ai), a marketplace where AI agents conduct trades on behalf of human users.
11
+
12
+ Your operation guide and references are in the skill/ directory of this extension:
13
+ - skill/SKILL.md — main operation guide (read this first for any marketplace task)
14
+ - skill/references/ — detailed references: tools.md, inbox.md, and playbooks/
15
+
16
+ Your capabilities on this platform:
17
+ - **Inbox**: Receive, inspect, and respond to messages from other agents (inbox_pull, inbox_get, inbox_ack, inbox_history)
18
+ - **Messaging**: Send A2A messages to other agents (send_message)
19
+ - **Works/Products**: Search, list, publish, update, and delete product listings (works_search, works_list, works_publish, works_update, works_delete)
20
+ - **Orders**: Create, query, list, confirm, reject, and manage orders (order_create, order_get, order_list, order_action)
21
+ - **Profile**: View your agent profile (profile_get)
22
+
23
+ Operational guidelines:
24
+ - Always acknowledge (inbox_ack) messages after reading them to mark them as processed
25
+ - When another agent sends you a trade request, evaluate it and respond professionally
26
+ - For order actions, use order_action with the appropriate action value
27
+ - Maintain professional agent-to-agent communication standards
28
+ - You represent your user's interests in all marketplace transactions
29
+ - Refer to skill/SKILL.md for detailed business flows, playbooks, and reporting requirements
30
+ </a2hmarket-role>
31
+ `.trim();
32
+
33
+ export function registerBeforeAgentStartHook(api: OpenClawPluginApi): void {
34
+ api.on("before_agent_start", (event) => {
35
+ // Cache session key so the MQTT listener can route notifications correctly
36
+ if (event.sessionKey) {
37
+ setCachedSessionKey(event.sessionKey);
38
+ }
39
+
40
+ const creds = readCredentials();
41
+ const lines: string[] = ["<a2hmarket-status>"];
42
+
43
+ if (!creds) {
44
+ lines.push("Status: NOT CONFIGURED (run: openclaw a2h setup)");
45
+ } else {
46
+ lines.push(`Agent ID: ${creds.agent_id}`);
47
+ lines.push(`Platform: ${creds.api_url}`);
48
+
49
+ const listenerStatus = getListenerStatus();
50
+ lines.push(
51
+ `Listener: ${listenerStatus.connected ? "CONNECTED" : "DISCONNECTED"}` +
52
+ (listenerStatus.reconnectAttempts > 0
53
+ ? ` (reconnect attempts: ${listenerStatus.reconnectAttempts})`
54
+ : ""),
55
+ );
56
+
57
+ if (listenerStatus.connected) {
58
+ const { unread } = peekUnread("default");
59
+ lines.push(
60
+ unread > 0
61
+ ? `Pending messages: ${unread} (use inbox_pull to read)`
62
+ : "Pending messages: 0",
63
+ );
64
+ }
65
+
66
+ if (creds.expires_at) {
67
+ const daysLeft = Math.floor(
68
+ (new Date(creds.expires_at).getTime() - Date.now()) / (1000 * 60 * 60 * 24),
69
+ );
70
+ if (daysLeft < 7) {
71
+ lines.push(`WARNING: Credentials expire in ${daysLeft} days`);
72
+ }
73
+ }
74
+ }
75
+
76
+ lines.push("</a2hmarket-status>");
77
+
78
+ return {
79
+ appendSystemContext: SYSTEM_CONTEXT,
80
+ prependContext: lines.join("\n"),
81
+ };
82
+ });
83
+ }