@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,221 @@
1
+ /**
2
+ * In-memory inbox event queue.
3
+ * Replaces the SQLite message_event table from the Go implementation.
4
+ * Deduplicates by message_id; evicts oldest events when queue exceeds 1000 items.
5
+ */
6
+
7
+ import type { Envelope } from "../a2a-protocol.js";
8
+
9
+ export type InboxEvent = {
10
+ event_id: string;
11
+ peer_id: string;
12
+ message_id: string;
13
+ msg_ts: number;
14
+ preview: string;
15
+ state: "NEW" | "PUSHED" | "ACKED";
16
+ payload: Record<string, unknown>;
17
+ created_at: number;
18
+ seq: number;
19
+ };
20
+
21
+ export type InboxPullResult = {
22
+ consumer_id: string;
23
+ cursor: number;
24
+ events: InboxEvent[];
25
+ count: number;
26
+ };
27
+
28
+ const MAX_QUEUE_SIZE = 1000;
29
+
30
+ // event_id → InboxEvent
31
+ const eventMap = new Map<string, InboxEvent>();
32
+
33
+ // message_id → event_id (for deduplication)
34
+ const messageIdIndex = new Map<string, string>();
35
+
36
+ // consumerId → Set<event_id> (acked events per consumer)
37
+ const ackMap = new Map<string, Set<string>>();
38
+
39
+ // Monotonically increasing sequence number; used for cursor-based pagination
40
+ let globalSeq = 0;
41
+
42
+ /**
43
+ * Ingest an envelope from MQTT into the memory queue.
44
+ * Returns the created InboxEvent, or null if it was a duplicate.
45
+ */
46
+ export function ingestEvent(envelope: Envelope): InboxEvent | null {
47
+ // Deduplicate by message_id
48
+ if (messageIdIndex.has(envelope.message_id)) {
49
+ return null;
50
+ }
51
+
52
+ const seq = ++globalSeq;
53
+ const eventId = `ev_${seq}_${envelope.message_id}`;
54
+ const now = Date.now();
55
+
56
+ // Build a human-readable preview from the payload
57
+ const text =
58
+ (envelope.payload.text as string | undefined) ||
59
+ (envelope.payload.message as string | undefined) ||
60
+ (envelope.payload.content as string | undefined) ||
61
+ "";
62
+ const preview = text.slice(0, 200) || `[${envelope.message_type}]`;
63
+
64
+ const event: InboxEvent = {
65
+ event_id: eventId,
66
+ peer_id: envelope.sender_id,
67
+ message_id: envelope.message_id,
68
+ msg_ts: now,
69
+ preview,
70
+ state: "NEW",
71
+ payload: envelope.payload,
72
+ created_at: now,
73
+ seq,
74
+ };
75
+
76
+ eventMap.set(eventId, event);
77
+ messageIdIndex.set(envelope.message_id, eventId);
78
+
79
+ // Evict oldest events when over capacity
80
+ if (eventMap.size > MAX_QUEUE_SIZE) {
81
+ // Find the event with the smallest seq
82
+ let minSeq = Infinity;
83
+ let minEventId = "";
84
+ for (const [eid, ev] of eventMap) {
85
+ if (ev.seq < minSeq) {
86
+ minSeq = ev.seq;
87
+ minEventId = eid;
88
+ }
89
+ }
90
+ if (minEventId) {
91
+ const oldEv = eventMap.get(minEventId);
92
+ if (oldEv) {
93
+ messageIdIndex.delete(oldEv.message_id);
94
+ }
95
+ eventMap.delete(minEventId);
96
+ }
97
+ }
98
+
99
+ return event;
100
+ }
101
+
102
+ /**
103
+ * Pull events for a consumer starting after `cursor` (seq number).
104
+ * Optionally filter by peer_id.
105
+ * Only returns events not yet acked by this consumer.
106
+ */
107
+ export function pullEvents(
108
+ consumerId: string,
109
+ cursor: number,
110
+ limit: number,
111
+ peerIdFilter?: string,
112
+ ): InboxPullResult {
113
+ const acked = ackMap.get(consumerId) ?? new Set<string>();
114
+
115
+ const matching: InboxEvent[] = [];
116
+ for (const ev of eventMap.values()) {
117
+ if (ev.seq <= cursor) continue;
118
+ if (acked.has(ev.event_id)) continue;
119
+ if (peerIdFilter && ev.peer_id !== peerIdFilter) continue;
120
+ matching.push(ev);
121
+ }
122
+
123
+ // Sort by seq ascending
124
+ matching.sort((a, b) => a.seq - b.seq);
125
+
126
+ const events = matching.slice(0, limit);
127
+ const newCursor = events.length > 0 ? events[events.length - 1].seq : cursor;
128
+
129
+ return {
130
+ consumer_id: consumerId,
131
+ cursor: newCursor,
132
+ events,
133
+ count: events.length,
134
+ };
135
+ }
136
+
137
+ /**
138
+ * Mark an event as acknowledged (read) by a consumer.
139
+ */
140
+ export function ackEvent(
141
+ consumerId: string,
142
+ eventId: string,
143
+ ): { acked_at: number; inserted: boolean } {
144
+ if (!ackMap.has(consumerId)) {
145
+ ackMap.set(consumerId, new Set());
146
+ }
147
+ const acked = ackMap.get(consumerId)!;
148
+ const inserted = !acked.has(eventId);
149
+ acked.add(eventId);
150
+
151
+ // Update event state if it exists
152
+ const ev = eventMap.get(eventId);
153
+ if (ev && ev.state === "NEW") {
154
+ ev.state = "ACKED";
155
+ }
156
+
157
+ return { acked_at: Date.now(), inserted };
158
+ }
159
+
160
+ /**
161
+ * Return the number of unread (unacked) events for a consumer.
162
+ */
163
+ export function peekUnread(consumerId: string): { unread: number } {
164
+ const acked = ackMap.get(consumerId) ?? new Set<string>();
165
+ let unread = 0;
166
+ for (const ev of eventMap.values()) {
167
+ if (!acked.has(ev.event_id)) {
168
+ unread++;
169
+ }
170
+ }
171
+ return { unread };
172
+ }
173
+
174
+ /**
175
+ * Return the total number of events in the queue.
176
+ */
177
+ export function getQueueSize(): number {
178
+ return eventMap.size;
179
+ }
180
+
181
+ /**
182
+ * Return a single InboxEvent by event_id, or null if not found.
183
+ */
184
+ export function getEvent(eventId: string): InboxEvent | null {
185
+ return eventMap.get(eventId) ?? null;
186
+ }
187
+
188
+ export type InboxHistoryResult = {
189
+ peer_id: string;
190
+ page: number;
191
+ limit: number;
192
+ total: number;
193
+ items: Array<InboxEvent & { direction: "recv" }>;
194
+ };
195
+
196
+ /**
197
+ * Return received messages from a specific peer, paginated.
198
+ * Note: only inbound (recv) messages are available in the in-memory queue.
199
+ * Outbound messages are not stored in the inbox queue.
200
+ */
201
+ export function historyByPeer(
202
+ peerId: string,
203
+ page: number,
204
+ limit: number,
205
+ ): InboxHistoryResult {
206
+ const matching: Array<InboxEvent & { direction: "recv" }> = [];
207
+ for (const ev of eventMap.values()) {
208
+ if (ev.peer_id === peerId) {
209
+ matching.push({ ...ev, direction: "recv" });
210
+ }
211
+ }
212
+
213
+ // Sort by seq descending (newest first)
214
+ matching.sort((a, b) => b.seq - a.seq);
215
+
216
+ const total = matching.length;
217
+ const offset = (page - 1) * limit;
218
+ const items = matching.slice(offset, offset + limit);
219
+
220
+ return { peer_id: peerId, page, limit, total, items };
221
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Order API — create, query, and manage trade orders.
3
+ */
4
+
5
+ import type { ApiClient } from "../api-client.js";
6
+
7
+ export type OrderAction =
8
+ | "confirm"
9
+ | "reject"
10
+ | "cancel"
11
+ | "confirm-received"
12
+ | "confirm-service-completed";
13
+
14
+ export async function orderCreate(
15
+ client: ApiClient,
16
+ opts: {
17
+ customerId: string;
18
+ title: string;
19
+ content: string;
20
+ price: number;
21
+ productId: string;
22
+ orderType: 2 | 3;
23
+ },
24
+ ): Promise<unknown> {
25
+ const creds = client.getCredentials();
26
+ return client.post<unknown>("/findu-trade/api/v1/orders/create", {
27
+ providerId: creds.agent_id,
28
+ customerId: opts.customerId,
29
+ title: opts.title,
30
+ content: opts.content,
31
+ price: opts.price,
32
+ productId: opts.productId,
33
+ orderType: opts.orderType,
34
+ });
35
+ }
36
+
37
+ export async function orderGet(
38
+ client: ApiClient,
39
+ orderId: string,
40
+ ): Promise<unknown> {
41
+ return client.get<unknown>(`/findu-trade/api/v1/orders/${orderId}/detail`);
42
+ }
43
+
44
+ export async function orderAction(
45
+ client: ApiClient,
46
+ orderId: string,
47
+ action: OrderAction,
48
+ ): Promise<unknown> {
49
+ return client.post<unknown>(
50
+ `/findu-trade/api/v1/orders/${orderId}/${action}`,
51
+ {},
52
+ );
53
+ }
54
+
55
+ export async function orderList(
56
+ client: ApiClient,
57
+ kind: "sales" | "purchase",
58
+ opts: { page?: number; pageSize?: number; status?: string },
59
+ ): Promise<unknown> {
60
+ const page = opts.page ?? 1;
61
+ const pageSize = opts.pageSize ?? 10;
62
+ const endpoint = kind === "sales" ? "sales-orders" : "purchase-orders";
63
+ const signPath = `/findu-trade/api/v1/orders/${endpoint}`;
64
+ let apiPath = `${signPath}?page=${page}&pageSize=${pageSize}`;
65
+ if (opts.status) {
66
+ apiPath += `&status=${encodeURIComponent(opts.status)}`;
67
+ }
68
+ return client.get<unknown>(apiPath, signPath);
69
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Profile API — get and update agent profile.
3
+ */
4
+
5
+ import type { ApiClient } from "../api-client.js";
6
+
7
+ export type ProfileData = {
8
+ nickname: string;
9
+ avatarUrl: string;
10
+ bio: string;
11
+ abilities: unknown;
12
+ realnameStatus: unknown;
13
+ paymentQrcodeUrl: string;
14
+ };
15
+
16
+ export async function profileGet(client: ApiClient): Promise<ProfileData> {
17
+ return client.get<ProfileData>("/findu-user/api/v1/user/profile/public");
18
+ }
19
+
20
+ export async function profileSetPaymentQrcode(
21
+ client: ApiClient,
22
+ url: string,
23
+ ): Promise<unknown> {
24
+ return client.post<unknown>("/findu-user/api/v1/user/profile/change-requests", {
25
+ paymentQrcodeUrl: url,
26
+ });
27
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Send A2A messages via MQTT.
3
+ * Reuses the persistent listener connection when available,
4
+ * falling back to a short-lived _pub_ connection otherwise.
5
+ */
6
+
7
+ import mqtt from "mqtt";
8
+ import type { Credentials } from "../api-client.js";
9
+ import { buildEnvelope, signEnvelope } from "../a2a-protocol.js";
10
+ import type { Envelope } from "../a2a-protocol.js";
11
+ import { fetchMqttToken } from "../api-client.js";
12
+ import { getMqttClient } from "../service/mqtt-listener.js";
13
+
14
+ export type SendOptions = {
15
+ targetAgentId: string;
16
+ text?: string;
17
+ payloadJson?: Record<string, unknown>;
18
+ messageType?: string;
19
+ attachmentLocalPath?: string; // local file path (OSS upload first)
20
+ attachmentUrl?: string; // direct external URL
21
+ paymentQr?: string;
22
+ };
23
+
24
+ export type SendResult = {
25
+ message_id: string;
26
+ target_id: string;
27
+ type: string;
28
+ };
29
+
30
+ /**
31
+ * Build the message payload from send options.
32
+ */
33
+ function buildPayload(opts: SendOptions): Record<string, unknown> {
34
+ if (opts.payloadJson) {
35
+ return opts.payloadJson;
36
+ }
37
+ const p: Record<string, unknown> = {};
38
+ if (opts.text) p.text = opts.text;
39
+ if (opts.attachmentUrl) p.attachmentUrl = opts.attachmentUrl;
40
+ if (opts.paymentQr) p.paymentQr = opts.paymentQr;
41
+ return p;
42
+ }
43
+
44
+ /**
45
+ * Publish a signed envelope to MQTT using a short-lived _pub_ connection.
46
+ * Used when the persistent listener is not connected.
47
+ */
48
+ async function publishWithPubConnection(
49
+ creds: Credentials,
50
+ targetAgentId: string,
51
+ envelope: Envelope,
52
+ ): Promise<void> {
53
+ const token = await fetchMqttToken(creds);
54
+ const random8 = Math.floor(Math.random() * 0xffffffff)
55
+ .toString(16)
56
+ .padStart(8, "0");
57
+ const clientId = `GID_agent@@@${creds.agent_id}_pub_${random8}`;
58
+ const topic = `P2P_TOPIC/p2p/GID_agent@@@${targetAgentId}`;
59
+ const payload = JSON.stringify(envelope);
60
+
61
+ const brokerUrl = `mqtts://${token.instance_id}.mqtt.aliyuncs.com:8883`;
62
+ await new Promise<void>((resolve, reject) => {
63
+ const client = mqtt.connect(brokerUrl, {
64
+ clientId,
65
+ username: token.username,
66
+ password: token.password,
67
+ clean: true, // one-shot sender uses clean session
68
+ keepalive: 60,
69
+ connectTimeout: 15000,
70
+ reconnectPeriod: 0,
71
+ rejectUnauthorized: false,
72
+ });
73
+
74
+ const timeout = setTimeout(() => {
75
+ client.end(true);
76
+ reject(new Error("MQTT publish timeout (15s)"));
77
+ }, 15000);
78
+
79
+ client.once("connect", () => {
80
+ client.publish(topic, payload, { qos: 1 }, (err) => {
81
+ clearTimeout(timeout);
82
+ client.end(false);
83
+ if (err) reject(err);
84
+ else resolve();
85
+ });
86
+ });
87
+
88
+ client.once("error", (err) => {
89
+ clearTimeout(timeout);
90
+ client.end(true);
91
+ reject(err);
92
+ });
93
+ });
94
+ }
95
+
96
+ /**
97
+ * Send an A2A message to another agent.
98
+ * Tries to reuse the persistent MQTT listener connection first;
99
+ * falls back to a short-lived _pub_ connection if not connected.
100
+ */
101
+ export async function sendMessage(
102
+ creds: Credentials,
103
+ opts: SendOptions,
104
+ ): Promise<SendResult> {
105
+ const payload = buildPayload(opts);
106
+ const messageType = opts.messageType ?? "chat.request";
107
+
108
+ // Build and sign the envelope
109
+ const unsigned = buildEnvelope(creds.agent_id, opts.targetAgentId, messageType, payload);
110
+ const envelope = signEnvelope(creds.agent_key, unsigned);
111
+
112
+ const topic = `P2P_TOPIC/p2p/GID_agent@@@${opts.targetAgentId}`;
113
+ const payloadStr = JSON.stringify(envelope);
114
+
115
+ // Try to reuse the persistent listener connection
116
+ const existingClient = getMqttClient();
117
+ if (existingClient?.connected) {
118
+ await new Promise<void>((resolve, reject) => {
119
+ existingClient.publish(topic, payloadStr, { qos: 1 }, (err) => {
120
+ if (err) reject(err);
121
+ else resolve();
122
+ });
123
+ });
124
+ } else {
125
+ // Fall back to a short-lived pub connection
126
+ await publishWithPubConnection(creds, opts.targetAgentId, envelope);
127
+ }
128
+
129
+ return {
130
+ message_id: envelope.message_id,
131
+ target_id: opts.targetAgentId,
132
+ type: messageType,
133
+ };
134
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Works API — search, publish, list, and delete works posts.
3
+ */
4
+
5
+ import type { ApiClient } from "../api-client.js";
6
+
7
+ export async function worksSearch(
8
+ client: ApiClient,
9
+ opts: {
10
+ keyword?: string;
11
+ agentId?: string;
12
+ type?: 2 | 3;
13
+ pageNum?: number;
14
+ pageSize?: number;
15
+ },
16
+ ): Promise<unknown> {
17
+ return client.post<unknown>(
18
+ "/findu-match/api/v1/inner/match/works_search",
19
+ {
20
+ serviceInfo: opts.keyword ?? "",
21
+ pageNum: opts.pageNum ?? 0,
22
+ pageSize: opts.pageSize ?? 10,
23
+ type: opts.type ?? 3,
24
+ agentId: opts.agentId ?? "",
25
+ },
26
+ );
27
+ }
28
+
29
+ export async function worksPublish(
30
+ client: ApiClient,
31
+ opts: {
32
+ type: 2 | 3;
33
+ title: string;
34
+ content: string;
35
+ expectedPrice?: string;
36
+ serviceMethod?: string;
37
+ serviceLocation?: string;
38
+ pictures?: string[];
39
+ worksId?: string;
40
+ },
41
+ ): Promise<unknown> {
42
+ const body: Record<string, unknown> = {
43
+ type: opts.type,
44
+ title: opts.title,
45
+ content: opts.content,
46
+ extendInfo: {
47
+ pois: [],
48
+ expectedPrice: opts.expectedPrice ?? "",
49
+ serviceMethod: opts.serviceMethod ?? "online",
50
+ serviceLocation: opts.serviceLocation ?? "",
51
+ },
52
+ pictures: opts.pictures ?? [],
53
+ };
54
+ if (opts.worksId) {
55
+ body.worksId = opts.worksId;
56
+ }
57
+ return client.post<unknown>("/findu-user/api/v1/user/works/change-requests", body);
58
+ }
59
+
60
+ export async function worksList(
61
+ client: ApiClient,
62
+ opts: {
63
+ type?: 2 | 3;
64
+ pageNum?: number;
65
+ pageSize?: number;
66
+ },
67
+ ): Promise<unknown> {
68
+ const pageNum = opts.pageNum ?? 1;
69
+ const pageSize = opts.pageSize ?? 10;
70
+ const type = opts.type ?? 3;
71
+ // signPath must NOT include query string
72
+ const signPath = "/findu-user/api/v1/user/works/public";
73
+ const apiPath = `${signPath}?pageNum=${pageNum}&pageSize=${pageSize}&type=${type}`;
74
+ return client.get<unknown>(apiPath, signPath);
75
+ }
76
+
77
+ export async function worksDelete(
78
+ client: ApiClient,
79
+ worksId: string,
80
+ ): Promise<unknown> {
81
+ return client.delete<unknown>(`/findu-user/api/v1/user/works/${worksId}`);
82
+ }