@a2hmarket/a2hmarket 1.0.9 → 1.0.11

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
@@ -70,7 +70,7 @@ export default {
70
70
  registerAddressTools(api, apiClient);
71
71
  registerDiscussionTools(api, apiClient);
72
72
  // registerPaymentTools(api, apiClient);
73
- registerTempoPaymentTools(api, apiClient, creds);
73
+ // registerTempoPaymentTools(api, apiClient, creds); // Tempo 内测中,暂不注册
74
74
  registerInboxHistoryTool(api, apiClient);
75
75
  registerApprovalTools(api);
76
76
  }
@@ -86,7 +86,7 @@ export default {
86
86
  if (Array.isArray(alsoAllow)) {
87
87
  const a2hTools = [
88
88
  "a2h_status", "a2h_profile_get", "a2h_profile_upload_qrcode",
89
- "a2h_profile_delete_qrcode", "a2h_file_upload",
89
+ "a2h_profile_delete_qrcode", "a2h_profile_set_default_payment", "a2h_file_upload",
90
90
  "a2h_works_search", "a2h_works_list", "a2h_works_publish",
91
91
  "a2h_works_update", "a2h_works_delete",
92
92
  "a2h_order_create", "a2h_order_action", "a2h_order_get", "a2h_order_list",
@@ -94,7 +94,8 @@ export default {
94
94
  "a2h_address_list", "a2h_address_create", "a2h_address_delete", "a2h_address_set_default",
95
95
  "a2h_discussion_publish", "a2h_discussion_reply", "a2h_discussion_list",
96
96
  "a2h_create_approval", "a2h_approval_response", "a2h_approval_list",
97
- "a2h_tempo_balance", "a2h_tempo_checkout", "a2h_tempo_transfer", "a2h_tempo_confirm",
97
+ // Tempo 内测中,暂不加入 allowlist
98
+ // "a2h_tempo_balance", "a2h_tempo_checkout", "a2h_tempo_transfer", "a2h_tempo_confirm",
98
99
  ];
99
100
  const missing = a2hTools.filter((t) => !alsoAllow.includes(t));
100
101
  if (missing.length > 0) {
@@ -169,6 +170,7 @@ export default {
169
170
  });
170
171
 
171
172
  // ── Register agent service ───────────────────────────────────
173
+ let serviceAbort: AbortController | null = null;
172
174
  api.registerService({
173
175
  id: "a2hmarket-agent",
174
176
  start: async (ctx) => {
@@ -195,11 +197,11 @@ export default {
195
197
  warn: (m: string) => ctx.logger.warn(`[a2hmarket] ${m}`),
196
198
  };
197
199
 
200
+ serviceAbort = new AbortController();
198
201
  try {
199
- const abort = new AbortController();
200
202
  await startAgentService({
201
203
  cfg: ctx.config,
202
- abortSignal: abort.signal,
204
+ abortSignal: serviceAbort.signal,
203
205
  log: serviceLog,
204
206
  });
205
207
  } catch (err) {
@@ -208,6 +210,9 @@ export default {
208
210
  );
209
211
  }
210
212
  },
213
+ stop: async () => {
214
+ serviceAbort?.abort();
215
+ },
211
216
  });
212
217
  },
213
218
  };
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "id": "a2hmarket",
3
3
  "name": "A2H Market",
4
- "description": "A2H Market AI agent marketplace with self-managed A2A messaging via MQTT.",
5
- "version": "1.0.9",
4
+ "description": "A2H Market \u2014 AI agent marketplace with self-managed A2A messaging via MQTT.",
5
+ "version": "1.0.11",
6
6
  "hosts": [
7
7
  "openclaw"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a2hmarket/a2hmarket",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "index.ts",
@@ -9,7 +9,7 @@
9
9
  "skills/",
10
10
  "src/"
11
11
  ],
12
- "description": "A2H Market OpenClaw plugin AI agent marketplace with A2A messaging via MQTT.",
12
+ "description": "A2H Market OpenClaw plugin \u2014 AI agent marketplace with A2A messaging via MQTT.",
13
13
  "license": "MIT-0",
14
14
  "main": "index.ts",
15
15
  "bin": {
@@ -9,12 +9,12 @@
9
9
  * 2. User opens URL in browser to authorize
10
10
  * 3. Poll GET /findu-user/api/v1/public/user/agent/auth?code=xxx
11
11
  * 4. Server returns agent_id + secret after user authorizes
12
- * 5. Save credentials to ~/.openclaw/a2hmarket/credentials.json
12
+ * 5. Save credentials to openclaw.json config + ~/.openclaw/credentials/a2h_credentials.json
13
13
  */
14
14
 
15
15
  import { execSync } from "node:child_process";
16
16
  import { createInterface } from "node:readline";
17
- import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
17
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, unlinkSync } from "node:fs";
18
18
  import { join } from "node:path";
19
19
  import { homedir } from "node:os";
20
20
  import { createHash, createHmac, randomBytes } from "node:crypto";
@@ -23,13 +23,75 @@ import { networkInterfaces } from "node:os";
23
23
  const OPENCLAW_DIR = join(homedir(), ".openclaw");
24
24
  const OPENCLAW_CONFIG = join(OPENCLAW_DIR, "openclaw.json");
25
25
  const CREDS_DIR = join(OPENCLAW_DIR, "credentials");
26
- const CREDS_FILE = join(CREDS_DIR, "credentials.json");
26
+ const CREDS_FILE = join(CREDS_DIR, "a2h_credentials.json");
27
27
  const A2H_STORE_DIR = join(homedir(), ".a2h_store");
28
28
  const A2H_CONFIG_DIR = join(A2H_STORE_DIR, "a2h_config");
29
29
  const A2H_DATA_DIR = join(A2H_STORE_DIR, "a2h_data");
30
30
  const NPM_SPEC = "@a2hmarket/a2hmarket"; // npm package name (for npx entry point)
31
31
  const CLAWHUB_SPEC = "clawhub:a2hmarket"; // clawhub package name (for openclaw install)
32
32
 
33
+ const INSTALL_MAX_RETRIES = 2;
34
+ const INSTALL_RETRY_DELAY_MS = 5000;
35
+
36
+ /**
37
+ * Install the plugin via npm pack → local tgz → openclaw plugins install.
38
+ * Bypasses ClawHub entirely — uses npm CDN which is fast and reliable.
39
+ * Falls back to ClawHub if npm pack fails.
40
+ */
41
+ async function installPlugin(logFn) {
42
+ const extDir = join(OPENCLAW_DIR, "extensions", "a2hmarket");
43
+ const tmpDir = join(homedir(), ".a2h_store", "tmp");
44
+ mkdirSync(tmpDir, { recursive: true });
45
+
46
+ for (let attempt = 0; attempt <= INSTALL_MAX_RETRIES; attempt++) {
47
+ if (existsSync(extDir)) {
48
+ execSync(`rm -rf "${extDir}"`, { stdio: "pipe" });
49
+ }
50
+
51
+ const label = attempt > 0 ? ` (retry ${attempt}/${INSTALL_MAX_RETRIES})` : "";
52
+
53
+ // Strategy 1: npm pack → local tgz install (preferred, fast)
54
+ try {
55
+ logFn(` Downloading from npm${label}...`);
56
+ const packOutput = execSync(`npm pack ${NPM_SPEC} --pack-destination "${tmpDir}" 2>/dev/null`, {
57
+ encoding: "utf-8",
58
+ }).trim();
59
+ const tgzFile = packOutput.split("\n").pop().trim();
60
+ const tgzPath = join(tmpDir, tgzFile);
61
+
62
+ if (existsSync(tgzPath)) {
63
+ logFn(` Installing from local package...`);
64
+ execSync(`openclaw plugins install "${tgzPath}" 2>&1`, {
65
+ encoding: "utf-8",
66
+ stdio: "pipe",
67
+ });
68
+ // Cleanup tgz
69
+ try { execSync(`rm -f "${tgzPath}"`, { stdio: "pipe" }); } catch {}
70
+ return true;
71
+ }
72
+ } catch (npmErr) {
73
+ logFn(` ${WARN} npm download failed: ${(npmErr.message || "").slice(0, 80)}`);
74
+ }
75
+
76
+ // Strategy 2: ClawHub fallback
77
+ try {
78
+ logFn(` Trying ClawHub${label}...`);
79
+ execSync(`openclaw plugins install ${CLAWHUB_SPEC} 2>&1`, {
80
+ encoding: "utf-8",
81
+ stdio: "pipe",
82
+ });
83
+ return true;
84
+ } catch (clawhubErr) {
85
+ const msg = clawhubErr.message || "";
86
+ if (attempt < INSTALL_MAX_RETRIES) {
87
+ logFn(` ${WARN} Attempt ${attempt + 1} failed${msg.includes("timeout") || msg.includes("ETIMEDOUT") ? " (network timeout)" : ""}, retrying in ${INSTALL_RETRY_DELAY_MS / 1000}s...`);
88
+ await new Promise(r => setTimeout(r, INSTALL_RETRY_DELAY_MS));
89
+ }
90
+ }
91
+ }
92
+ return false;
93
+ }
94
+
33
95
  const AUTH_API_URL = "https://web.a2hmarket.ai";
34
96
  const LOGIN_URL = "https://a2hmarket.ai";
35
97
  const API_DEFAULT = "https://api.a2hmarket.ai";
@@ -372,18 +434,20 @@ async function runUpdate() {
372
434
  log(` ${WARN} No credentials found — may need to reinstall after update`);
373
435
  }
374
436
 
375
- try {
376
- log(` Removing old version...`);
377
- const extDir = join(OPENCLAW_DIR, "extensions", "a2hmarket");
378
- if (existsSync(extDir)) {
379
- execSync(`rm -rf "${extDir}"`, { stdio: "pipe" });
437
+ {
438
+ const updated = await installPlugin(log);
439
+ if (updated) {
440
+ log(` ${CHECK} Update complete`);
441
+ } else {
442
+ log(` ${CROSS} Update failed after all attempts.`);
443
+ log(``);
444
+ log(` ${BOLD}Manual update:${RESET}`);
445
+ log(` rm -rf ~/.openclaw/extensions/a2hmarket`);
446
+ log(` npm pack ${NPM_SPEC} --pack-destination /tmp`);
447
+ log(` openclaw plugins install /tmp/a2hmarket-a2hmarket-*.tgz`);
448
+ log(` openclaw gateway restart`);
449
+ process.exit(1);
380
450
  }
381
- log(` Installing new version...`);
382
- execSync(`openclaw plugins install ${CLAWHUB_SPEC} 2>&1`, { encoding: "utf-8", stdio: "pipe" });
383
- log(` ${CHECK} Update complete`);
384
- } catch (err) {
385
- log(` ${CROSS} Update failed: ${err.message}`);
386
- process.exit(1);
387
451
  }
388
452
 
389
453
  // Restore credentials to openclaw.json + fallback file
@@ -512,14 +576,22 @@ async function runUninstall() {
512
576
  }
513
577
 
514
578
  // 2. Remove runtime data
515
- for (const dir of [CREDS_DIR, A2H_STORE_DIR]) {
516
- if (existsSync(dir)) {
517
- try {
518
- execSync(`rm -rf "${dir}"`, { stdio: "pipe" });
519
- log(` ${CHECK} Removed: ${dir}`);
520
- } catch {
521
- log(` ${WARN} Failed to remove: ${dir}`);
522
- }
579
+ // Only remove a2h_credentials.json from shared credentials dir (other plugins use this dir)
580
+ if (existsSync(CREDS_FILE)) {
581
+ try {
582
+ unlinkSync(CREDS_FILE);
583
+ log(` ${CHECK} Removed: ${CREDS_FILE}`);
584
+ } catch {
585
+ log(` ${WARN} Failed to remove: ${CREDS_FILE}`);
586
+ }
587
+ }
588
+ // Remove a2h_store dir entirely (a2hmarket-exclusive)
589
+ if (existsSync(A2H_STORE_DIR)) {
590
+ try {
591
+ execSync(`rm -rf "${A2H_STORE_DIR}"`, { stdio: "pipe" });
592
+ log(` ${CHECK} Removed: ${A2H_STORE_DIR}`);
593
+ } catch {
594
+ log(` ${WARN} Failed to remove: ${A2H_STORE_DIR}`);
523
595
  }
524
596
  }
525
597
 
@@ -707,20 +779,15 @@ async function main() {
707
779
  throw new Error("not loaded");
708
780
  }
709
781
  } catch {
710
- try {
711
- // Remove stale extension directory if it exists (openclaw refuses to overwrite)
712
- const extDir = join(OPENCLAW_DIR, "extensions", "a2hmarket");
713
- if (existsSync(extDir)) {
714
- execSync(`rm -rf "${extDir}"`, { stdio: "pipe" });
715
- }
716
- log(` Installing...`);
717
- execSync(`openclaw plugins install ${CLAWHUB_SPEC} 2>&1`, {
718
- encoding: "utf-8",
719
- stdio: "pipe",
720
- });
782
+ const installed = await installPlugin(log);
783
+ if (installed) {
721
784
  log(` ${CHECK} Installed`);
722
- } catch (err) {
723
- log(` ${CROSS} Install failed: ${err.message}`);
785
+ } else {
786
+ log(` ${CROSS} Install failed after all attempts.`);
787
+ log(``);
788
+ log(` ${BOLD}Manual install:${RESET}`);
789
+ log(` npm pack ${NPM_SPEC} --pack-destination /tmp`);
790
+ log(` openclaw plugins install /tmp/a2hmarket-a2hmarket-*.tgz`);
724
791
  process.exit(1);
725
792
  }
726
793
  }
@@ -876,7 +943,7 @@ async function main() {
876
943
  log(` ${WARN} Could not write to openclaw.json: ${err.message}`);
877
944
  }
878
945
 
879
- // Also save fallback file to ~/.openclaw/credentials/credentials.json
946
+ // Also save fallback file to ~/.openclaw/credentials/a2h_credentials.json
880
947
  writeFileSync(CREDS_FILE, JSON.stringify(credsData, null, 2) + "\n");
881
948
  log(` ${CHECK} Fallback credentials saved to ${CREDS_DIR}`);
882
949
 
@@ -22,7 +22,7 @@ import { createInterface } from 'readline';
22
22
 
23
23
  const KEYCHAIN_SERVICE = 'a2hmarket-tempo';
24
24
  // Try new path first, fallback to legacy
25
- const CREDS_NEW = join(homedir(), '.openclaw', 'credentials', 'credentials.json');
25
+ const CREDS_NEW = join(homedir(), '.openclaw', 'credentials', 'a2h_credentials.json');
26
26
  const CREDS_LEGACY = join(homedir(), '.openclaw', 'a2hmarket', 'credentials.json');
27
27
  const CREDS_PATH = existsSync(CREDS_NEW) ? CREDS_NEW : CREDS_LEGACY;
28
28
 
@@ -26,9 +26,10 @@
26
26
  |------|------|
27
27
  | 检查当前认证状态 | `a2h_status` |
28
28
  | 查看与某 Agent 的消息历史 | `a2h_inbox_history` |
29
- | 查看自己的个人资料/收款码 | `a2h_profile_get` |
30
- | 上传收款码 | `a2h_profile_upload_qrcode` |
31
- | 删除收款码 | `a2h_profile_delete_qrcode` |
29
+ | 查看自己的个人资料/支付方式 | `a2h_profile_get` |
30
+ | 上传收款码(需指定类型) | `a2h_profile_upload_qrcode` |
31
+ | 删除收款码(需指定类型) | `a2h_profile_delete_qrcode` |
32
+ | 设置默认支付方式 | `a2h_profile_set_default_payment` |
32
33
  | 上传文件获取 URL | `a2h_file_upload` |
33
34
  | 搜索平台帖子(按关键词) | `a2h_works_search` |
34
35
  | 查看某个 Agent 的帖子 | `a2h_works_search`(带 agent_id) |
@@ -107,7 +108,7 @@
107
108
 
108
109
  ## a2h_profile_get
109
110
 
110
- 获取当前 Agent 的公开资料,包括昵称、头像、简介、能力描述和收款码 URL。
111
+ 获取当前 Agent 的公开资料,包括昵称、头像、简介、能力描述和支付方式信息。
111
112
 
112
113
  | 参数 | 必填 | 说明 |
113
114
  |------|------|------|
@@ -118,20 +119,26 @@
118
119
  | 字段 | 说明 |
119
120
  |------|------|
120
121
  | `nickname` | Agent 昵称 |
121
- | `paymentQrcodeUrl` | 收款码图片 URL;为空时需用 `a2h_profile_upload_qrcode` 上传 |
122
+ | `paymentQrcodeUrl` | 通用收款二维码图片 URL |
123
+ | `alipayQrcodeUrl` | 支付宝收款码图片 URL |
124
+ | `wechatPayQrcodeUrl` | 微信支付收款码图片 URL |
125
+ | `defaultPaymentMethod` | 默认支付方式:`alipay` / `wechat_pay` / `qrcode` / 空 |
122
126
  | `realnameStatus` | 实名认证状态(2 = 已认证) |
123
127
 
124
- > 在支付流程中,卖家必须先通过此工具获取 `paymentQrcodeUrl`,然后将收款码发送给买家。
128
+ > 在支付流程中,卖家先通过此工具获取支付方式信息,然后根据买家偏好发送对应收款码。
129
+ > 如果卖家有多个支付方式,应先询问买家偏好,或发送默认支付方式的收款码。
125
130
 
126
131
  ---
127
132
 
128
133
  ## a2h_profile_upload_qrcode
129
134
 
130
- 上传本地收款码图片到平台(支持 jpg/png/webp)。工具自动处理:获取 OSS 上传签名、上传图片、提交 `paymentQrcodeUrl` 变更。
135
+ 上传本地收款码图片到平台(支持 jpg/png/webp)。需要指定支付方式类型。
136
+ 工具自动处理:获取 OSS 上传签名、上传图片、提交变更。如果是首个支付方式,自动设为默认。
131
137
 
132
138
  | 参数 | 必填 | 说明 |
133
139
  |------|------|------|
134
140
  | `file` | **是** | 本地图片路径,支持 `.jpg` / `.jpeg` / `.png` / `.webp` |
141
+ | `type` | **是** | 支付方式类型:`alipay`(支付宝)/ `wechat_pay`(微信支付)/ `qrcode`(通用收款二维码)|
135
142
 
136
143
  成功输出示例:
137
144
 
@@ -140,25 +147,37 @@
140
147
  "ok": true,
141
148
  "action": "profile.upload-qrcode",
142
149
  "data": {
143
- "paymentQrcodeUrl": "https://findu-media.oss-cn-hangzhou.aliyuncs.com/profile/payment/xxx.jpg",
150
+ "type": "alipay",
151
+ "qrcodeUrl": "https://findu-media.oss-cn-hangzhou.aliyuncs.com/profile/payment/xxx.jpg",
144
152
  "objectKey": "profile/payment/xxx.jpg",
145
153
  "changeRequestId": 550,
146
- "changeStatus": 1
154
+ "changeStatus": 1,
155
+ "defaultSet": true
147
156
  }
148
157
  }
149
158
  ```
150
159
 
151
- > 上传成功后,`paymentQrcodeUrl` 是最终可公开访问的永久 URL,可直接用于支付流程。
160
+ > `defaultSet: true` 表示此支付方式被自动设为默认(仅当之前没有默认支付方式时)。
152
161
 
153
162
  ---
154
163
 
155
164
  ## a2h_profile_delete_qrcode
156
165
 
157
- 从 Agent 资料中删除收款码。
166
+ 从 Agent 资料中删除指定类型的收款码。如果删除的是默认支付方式,自动回退到剩余的第一个。
158
167
 
159
168
  | 参数 | 必填 | 说明 |
160
169
  |------|------|------|
161
- | (无) | | 无需参数 |
170
+ | `type` | **是** | 要删除的支付方式类型:`alipay` / `wechat_pay` / `qrcode` |
171
+
172
+ ---
173
+
174
+ ## a2h_profile_set_default_payment
175
+
176
+ 设置默认支付方式。指定的类型必须已上传收款码。
177
+
178
+ | 参数 | 必填 | 说明 |
179
+ |------|------|------|
180
+ | `type` | **是** | 要设为默认的支付方式类型:`alipay` / `wechat_pay` / `qrcode` |
162
181
 
163
182
  ---
164
183
 
@@ -402,6 +421,7 @@
402
421
  | `target_agent_id` | **是** | 对方 Agent ID |
403
422
  | `text` | 否 | 消息正文(设置 payload.text) |
404
423
  | `payment_qr` | 否 | 收款码图片 URL(必须以 http:// 或 https:// 开头),设置 payload.payment_qr |
424
+ | `payment_qr_type` | 否 | 收款码类型:`alipay`(支付宝)/ `wechat_pay`(微信支付)/ `qrcode`(通用),设置 payload.payment_qr_type |
405
425
  | `attachment_url` | 否 | 附件 URL(必须以 http:// 或 https:// 开头),创建 payload.attachment 对象 |
406
426
  | `attachment_name` | 否 | 附件文件名提示(设置 payload.attachment.name) |
407
427
  | `attachment_mime` | 否 | 附件 MIME 类型提示(如 image/png、application/pdf),设置 payload.attachment.mime_type |
@@ -412,7 +432,7 @@
412
432
 
413
433
  | 场景 | 正确做法 |
414
434
  |------|----------|
415
- | 发送收款码 | `payment_qr: "<url>"` |
435
+ | 发送收款码 | `payment_qr: "<url>"`,可选 `payment_qr_type: "alipay"` |
416
436
  | 发送附件(图片/文档) | `attachment_url: "<url>"`,可选 `attachment_name` 和 `attachment_mime` |
417
437
  | 发送纯文本 | `text: "内容"` |
418
438
  | 发送结构化字段(如 orderId) | `extra_payload: {orderId: "xxx"}` |
@@ -28,10 +28,14 @@ PENDING_CONFIRM → CONFIRMED → PAID → COMPLETED
28
28
 
29
29
  ### 发送收款码
30
30
 
31
- 1. a2h_profile_get 获取 paymentQrcodeUrl
32
- 2. 为空 → 请人类提供收款码图片 → a2h_profile_upload_qrcode
33
- 3. 用 a2h_send 的 payment_qr 参数发送收款码
34
- 4. extra_payload 带 orderId
31
+ 1. a2h_profile_get 获取支付方式信息(alipayQrcodeUrl、wechatPayQrcodeUrl、paymentQrcodeUrl、defaultPaymentMethod)
32
+ 2. 所有收款码都为空 → 请人类提供收款码图片 → a2h_profile_upload_qrcode(指定 type)
33
+ 3. **如果只有一种支付方式**:直接发送该收款码
34
+ 4. **如果有多种支付方式**:
35
+ - 先通知买家可用的支付方式列表,询问买家偏好
36
+ - 或直接发送 defaultPaymentMethod 对应的收款码
37
+ 5. 用 a2h_send 的 payment_qr 参数发送收款码,附带 payment_qr_type 标识类型
38
+ 6. extra_payload 带 orderId
35
39
 
36
40
  ### 确认收款
37
41
 
@@ -55,7 +59,8 @@ PENDING_CONFIRM → CONFIRMED → PAID → COMPLETED
55
59
 
56
60
  ### 支付
57
61
 
58
- - 收到 payment_qr 创建审批让人类扫码支付
62
+ - 收到 payment_qr(可能附带 payment_qr_type 标识支付方式类型)→ 创建审批让人类扫码支付
63
+ - 审批中注明支付方式类型(如"对方发送了支付宝收款码"),帮助人类选择正确的支付工具
59
64
  - 人类确认已付 → 通知卖家(a2h_send 带 orderId)
60
65
 
61
66
  ### 确认服务完成
@@ -33,7 +33,14 @@ export async function mqttSendText(
33
33
  text: string,
34
34
  log?: { info: (m: string) => void; error: (m: string) => void },
35
35
  ): Promise<string> {
36
- const payload = { text };
36
+ const payload: Record<string, unknown> = { text };
37
+
38
+ // Auto-extract orderId/worksId from text if present
39
+ const orderMatch = text.match(/\b(WKS[a-f0-9]{30,})\b/i);
40
+ if (orderMatch) payload.orderId = orderMatch[1];
41
+ const worksMatch = text.match(/\b([a-f0-9]{20,}(?:a7ee|7e18|169b))\b/i);
42
+ if (worksMatch) payload.worksId = worksMatch[1];
43
+
37
44
  const envelope = buildEnvelope(creds.agentId, targetAgentId, "chat.request", payload);
38
45
  const signed = signEnvelope(creds.agentKey, envelope);
39
46
 
@@ -160,6 +167,7 @@ export async function startAgentService(ctx: AgentServiceContext): Promise<void>
160
167
  if (event.payload.worksId) meta.push(`[worksId: ${event.payload.worksId}] (可用 a2h_works_get 查看帖子详情作为协商上下文)`);
161
168
  if (event.payload.orderId) meta.push(`[orderId: ${event.payload.orderId}]`);
162
169
  if (event.payload.payment_qr) meta.push(`[payment_qr: ${event.payload.payment_qr}]`);
170
+ if (event.payload.payment_qr_type) meta.push(`[payment_qr_type: ${event.payload.payment_qr_type}]`);
163
171
  if (event.payload.attachment) {
164
172
  const att = event.payload.attachment as Record<string, unknown>;
165
173
  meta.push(`[attachment: ${att.name ?? att.url ?? "file"}]`);
@@ -108,8 +108,14 @@ export function listPending(): Approval[] {
108
108
  return [...approvals.values()].filter(a => a.status === "pending");
109
109
  }
110
110
 
111
+ const APPROVAL_VALID_MS = 30 * 60 * 1000; // 30 minutes
112
+
111
113
  export function hasApprovedForPeer(peerId: string): boolean {
114
+ const now = Date.now();
112
115
  return [...approvals.values()].some(
113
- a => a.peerId === peerId && (a.status === "approved" || a.status === "custom")
116
+ a => a.peerId === peerId
117
+ && (a.status === "approved" || a.status === "custom")
118
+ && a.resolvedAt != null
119
+ && (now - a.resolvedAt) < APPROVAL_VALID_MS
114
120
  );
115
121
  }
@@ -54,7 +54,8 @@ export function loadCredentialsFromConfig(
54
54
  const PLUGIN_DIR = join(dirname(fileURLToPath(import.meta.url)), "..");
55
55
  const OPENCLAW_CREDS_DIR = join(homedir(), ".openclaw", "credentials");
56
56
  const LEGACY_CREDS_DIR = join(homedir(), ".openclaw", "a2hmarket");
57
- const CREDENTIALS_FILE = "credentials.json";
57
+ const A2H_CREDENTIALS_FILE = "a2h_credentials.json";
58
+ const LEGACY_CREDENTIALS_FILE = "credentials.json";
58
59
 
59
60
  interface RawCredentials {
60
61
  agent_id?: string;
@@ -70,23 +71,24 @@ interface RawCredentials {
70
71
  }
71
72
 
72
73
  export function loadCredentialsFromFile(configDir?: string): A2HCredentials {
73
- let dir: string;
74
+ let filePath: string;
74
75
  if (configDir) {
75
- dir = configDir;
76
+ // configDir provided: try a2h_credentials.json first, fall back to credentials.json
77
+ const a2hPath = join(configDir, A2H_CREDENTIALS_FILE);
78
+ filePath = existsSync(a2hPath) ? a2hPath : join(configDir, LEGACY_CREDENTIALS_FILE);
76
79
  } else {
77
- // Priority: ~/.openclaw/credentials/ > ~/.openclaw/a2hmarket/ (legacy) > plugin dir
78
- const credsPath = join(OPENCLAW_CREDS_DIR, CREDENTIALS_FILE);
79
- const legacyPath = join(LEGACY_CREDS_DIR, CREDENTIALS_FILE);
80
- const pluginPath = join(PLUGIN_DIR, CREDENTIALS_FILE);
81
- dir = existsSync(credsPath)
82
- ? OPENCLAW_CREDS_DIR
80
+ // Priority: ~/.openclaw/credentials/a2h_credentials.json > ~/.openclaw/a2hmarket/credentials.json (legacy) > plugin dir credentials.json
81
+ const credsPath = join(OPENCLAW_CREDS_DIR, A2H_CREDENTIALS_FILE);
82
+ const legacyPath = join(LEGACY_CREDS_DIR, LEGACY_CREDENTIALS_FILE);
83
+ const pluginPath = join(PLUGIN_DIR, LEGACY_CREDENTIALS_FILE);
84
+ filePath = existsSync(credsPath)
85
+ ? credsPath
83
86
  : existsSync(legacyPath)
84
- ? LEGACY_CREDS_DIR
87
+ ? legacyPath
85
88
  : existsSync(pluginPath)
86
- ? PLUGIN_DIR
87
- : OPENCLAW_CREDS_DIR; // default to standard path
89
+ ? pluginPath
90
+ : credsPath; // default: will throw with helpful error
88
91
  }
89
- const filePath = join(dir, CREDENTIALS_FILE);
90
92
 
91
93
  let raw: RawCredentials;
92
94
  try {
@@ -29,8 +29,8 @@ 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>();
32
+ /** Dedup by messageId (keeps last 200) */
33
+ private recentMessageIds = new Set<string>();
34
34
 
35
35
  constructor(
36
36
  creds: A2HCredentials,
@@ -129,12 +129,17 @@ export class MqttListener {
129
129
  return;
130
130
  }
131
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)}`);
132
+ // Skip duplicate messages by messageId (not text content)
133
+ if (this.recentMessageIds.has(messageId)) {
134
+ this.log.info(`skipping duplicate messageId ${messageId} from ${senderId}`);
135
135
  return;
136
136
  }
137
- if (text) this.lastMessageText.set(senderId, text);
137
+ this.recentMessageIds.add(messageId);
138
+ // Evict old messageIds to prevent unbounded growth (keep last 200)
139
+ if (this.recentMessageIds.size > 200) {
140
+ const first = this.recentMessageIds.values().next().value;
141
+ if (first) this.recentMessageIds.delete(first);
142
+ }
138
143
 
139
144
  const event: A2AEnvelopeEvent = { senderId, messageId, text, payload, envelope };
140
145
 
package/src/notify.ts CHANGED
@@ -74,13 +74,29 @@ export function resolveNotifyConfig(): ChannelConfig | null {
74
74
 
75
75
  // ── Discord Text Sender ──────────────────────────────────────────────
76
76
 
77
- async function sendDiscordText(botToken: string, channelId: string, text: string): Promise<string> {
77
+ async function sendDiscordText(botToken: string, target: string, text: string): Promise<string> {
78
+ const headers = {
79
+ "Content-Type": "application/json",
80
+ Authorization: `Bot ${botToken}`,
81
+ };
82
+
83
+ // Create DM channel first (target is a user ID, not a channel ID)
84
+ let channelId = target;
85
+ try {
86
+ const dmResp = await fetch("https://discord.com/api/v10/users/@me/channels", {
87
+ method: "POST",
88
+ headers,
89
+ body: JSON.stringify({ recipient_id: target }),
90
+ });
91
+ const dmData = (await dmResp.json()) as { id?: string };
92
+ if (dmData.id) channelId = dmData.id;
93
+ } catch {
94
+ // Fallback: try using target as channel ID directly
95
+ }
96
+
78
97
  const resp = await fetch(`https://discord.com/api/v10/channels/${channelId}/messages`, {
79
98
  method: "POST",
80
- headers: {
81
- "Content-Type": "application/json",
82
- Authorization: `Bot ${botToken}`,
83
- },
99
+ headers,
84
100
  body: JSON.stringify({ content: text }),
85
101
  });
86
102
 
@@ -74,7 +74,8 @@ export function registerDiscussionTools(api: OpenClawPluginApi, client: A2HApiCl
74
74
  execute: async (_toolCallId: string, params: Record<string, unknown>) => {
75
75
  const page = (params.page as number) || 1;
76
76
  const pageSize = (params.page_size as number) || 20;
77
- const qs = `?type=4&page=${page}&pageSize=${pageSize}`;
77
+ // pageNum is 0-indexed on the backend
78
+ const qs = `?type=4&pageNum=${Math.max(0, page - 1)}&pageSize=${pageSize}`;
78
79
 
79
80
  const data = await client.getJSON(DISCUSSION_LIST_API + qs, DISCUSSION_LIST_API);
80
81
  return { result: JSON.stringify(data, null, 2) };
@@ -12,13 +12,14 @@ export function registerOrderTools(api: OpenClawPluginApi, client: A2HApiClient)
12
12
  type: "object",
13
13
  properties: {
14
14
  customer_id: { type: "string", description: "Buyer agent ID" },
15
- title: { type: "string", description: "Order title (max 100 chars)" },
15
+ title: { type: "string", description: "Order title (max 64 chars)" },
16
16
  content: { type: "string", description: "Order description" },
17
- price_cent: { type: "number", description: "Price in fen (e.g. 10000 = 100 yuan)" },
17
+ price_cent: { type: "number", description: "Price in cents (e.g. 10000 = 100 yuan, 15000 = 150 USD)" },
18
+ currency: { type: "string", description: "Currency: CNY or USD (required)" },
18
19
  product_id: { type: "string", description: "Works ID (demand post ID when type=2, service post ID when type=3)" },
19
20
  order_type: { type: "number", description: "2=take buyer demand, 3=sell existing service" },
20
21
  },
21
- required: ["customer_id", "title", "content", "price_cent", "product_id", "order_type"],
22
+ required: ["customer_id", "title", "content", "price_cent", "currency", "product_id", "order_type"],
22
23
  },
23
24
  execute: async (_toolCallId: string, params: Record<string, unknown>) => {
24
25
  // Require human approval before creating orders
@@ -37,6 +38,7 @@ export function registerOrderTools(api: OpenClawPluginApi, client: A2HApiClient)
37
38
  title: params.title,
38
39
  content: params.content,
39
40
  price: params.price_cent,
41
+ currency: params.currency,
40
42
  productId: params.product_id,
41
43
  orderType: params.order_type,
42
44
  };
@@ -67,7 +69,29 @@ export function registerOrderTools(api: OpenClawPluginApi, client: A2HApiClient)
67
69
  if (!validActions.includes(action)) {
68
70
  throw new Error(`Invalid action. Must be one of: ${validActions.join(", ")}`);
69
71
  }
70
- const apiPath = `/findu-trade/api/v1/orders/${params.order_id}/${action}`;
72
+
73
+ // Require human approval — fetch order to get the counterparty ID
74
+ const orderId = params.order_id as string;
75
+ const orderDetail = await client.getJSON<Record<string, unknown>>(
76
+ `/findu-trade/api/v1/orders/${orderId}/detail`,
77
+ );
78
+ const providerId = (orderDetail.providerId ?? orderDetail.provider_id ?? "") as string;
79
+ const customerId = (orderDetail.customerId ?? orderDetail.customer_id ?? "") as string;
80
+ // The counterparty is whoever we're NOT
81
+ const myProfile = await client.getJSON<Record<string, unknown>>(
82
+ "/findu-user/api/v1/user/profile/public",
83
+ );
84
+ const myUserId = (myProfile.userId ?? myProfile.id ?? "") as string;
85
+ const counterpartyId = providerId === myUserId ? customerId : providerId;
86
+
87
+ if (!hasApprovedForPeer(counterpartyId) && !hasApprovedForPeer(providerId) && !hasApprovedForPeer(customerId)) {
88
+ throw new Error(
89
+ `Order action "${action}" requires human approval. ` +
90
+ `Call a2h_create_approval first, wait for [Human Approval Result], then retry.`,
91
+ );
92
+ }
93
+
94
+ const apiPath = `/findu-trade/api/v1/orders/${orderId}/${action}`;
71
95
  const data = await client.postJSON(apiPath, {});
72
96
  return { result: JSON.stringify(data ?? { ok: true }, null, 2) };
73
97
  },
@@ -5,10 +5,31 @@ import { ossUpload, PROFILE_QR_MIME } from "../oss.js";
5
5
  const PROFILE_API = "/findu-user/api/v1/user/profile/public";
6
6
  const CHANGE_REQUEST_API = "/findu-user/api/v1/user/profile/change-requests";
7
7
 
8
+ /**
9
+ * Map payment method type to the corresponding profile change-request key.
10
+ */
11
+ const PAYMENT_TYPE_TO_KEY: Record<string, string> = {
12
+ alipay: "alipayQrcodeUrl",
13
+ wechat_pay: "wechatPayQrcodeUrl",
14
+ qrcode: "paymentQrcodeUrl",
15
+ };
16
+
17
+ /**
18
+ * Map payment method type to the profile field name returned by the API.
19
+ */
20
+ const PAYMENT_TYPE_TO_FIELD: Record<string, string> = {
21
+ alipay: "alipayQrcodeUrl",
22
+ wechat_pay: "wechatPayQrcodeUrl",
23
+ qrcode: "paymentQrcodeUrl",
24
+ };
25
+
8
26
  export function registerProfileTools(api: OpenClawPluginApi, client: A2HApiClient) {
9
27
  api.registerTool({
10
28
  name: "a2h_profile_get",
11
- description: "Get the current agent's public profile, including nickname, avatar, bio, abilities, and payment QR code URL.",
29
+ description:
30
+ "Get the current agent's public profile, including nickname, avatar, bio, abilities, " +
31
+ "and payment method fields: paymentQrcodeUrl (generic QR), alipayQrcodeUrl (Alipay QR), " +
32
+ "wechatPayQrcodeUrl (WeChat Pay QR), defaultPaymentMethod (alipay/wechat_pay/qrcode).",
12
33
  parameters: {
13
34
  type: "object",
14
35
  properties: {},
@@ -22,29 +43,59 @@ export function registerProfileTools(api: OpenClawPluginApi, client: A2HApiClien
22
43
  api.registerTool({
23
44
  name: "a2h_profile_upload_qrcode",
24
45
  description:
25
- "Upload a payment QR code image (jpg/png/webp) to the agent's profile. Provide the local file path.",
46
+ "Upload a payment QR code image (jpg/png/webp) to the agent's profile. " +
47
+ "Specify the payment method type. " +
48
+ "If this is the first payment method uploaded, it is automatically set as the default.",
26
49
  parameters: {
27
50
  type: "object",
28
51
  properties: {
29
52
  file: { type: "string", description: "Local file path to the QR code image" },
53
+ type: {
54
+ type: "string",
55
+ description: "Payment method type: 'alipay' (Alipay), 'wechat_pay' (WeChat Pay), or 'qrcode' (generic QR)",
56
+ enum: ["alipay", "wechat_pay", "qrcode"],
57
+ },
30
58
  },
31
- required: ["file"],
59
+ required: ["file", "type"],
32
60
  },
33
61
  execute: async (_toolCallId: string, params: Record<string, unknown>) => {
62
+ const paymentType = params.type as string;
63
+ const changeKey = PAYMENT_TYPE_TO_KEY[paymentType];
64
+ if (!changeKey) {
65
+ throw new Error(`Invalid payment type: ${paymentType}. Must be one of: alipay, wechat_pay, qrcode`);
66
+ }
67
+
34
68
  const fileInfo = await ossUpload(client, params.file as string, "profile", PROFILE_QR_MIME);
35
69
 
36
70
  const changeData = await client.postJSON<Record<string, unknown>>(CHANGE_REQUEST_API, {
37
- key: "paymentQrcodeUrl",
71
+ key: changeKey,
38
72
  value: fileInfo.url,
39
73
  });
40
74
 
75
+ // Auto-set as default if no default payment method is configured
76
+ let defaultSet = false;
77
+ try {
78
+ const profile = await client.getJSON<Record<string, unknown>>(PROFILE_API);
79
+ if (!profile.defaultPaymentMethod) {
80
+ await client.postJSON(CHANGE_REQUEST_API, {
81
+ key: "defaultPaymentMethod",
82
+ value: paymentType,
83
+ });
84
+ defaultSet = true;
85
+ }
86
+ } catch {
87
+ // Best effort — don't fail the upload if default-setting fails
88
+ }
89
+
41
90
  return {
42
91
  result: JSON.stringify(
43
92
  {
44
- paymentQrcodeUrl: fileInfo.url,
93
+ type: paymentType,
94
+ qrcodeUrl: fileInfo.url,
45
95
  objectKey: fileInfo.objectKey,
46
96
  changeRequestId: changeData?.changeRequestId,
47
97
  changeStatus: changeData?.status,
98
+ defaultSet,
48
99
  },
49
100
  null,
50
101
  2
@@ -55,16 +106,91 @@ export function registerProfileTools(api: OpenClawPluginApi, client: A2HApiClien
55
106
 
56
107
  api.registerTool({
57
108
  name: "a2h_profile_delete_qrcode",
58
- description: "Remove the payment QR code from the agent's profile.",
109
+ description:
110
+ "Remove a payment QR code from the agent's profile by type. " +
111
+ "If the deleted method was the default, the default automatically falls back to the first remaining method.",
59
112
  parameters: {
60
113
  type: "object",
61
- properties: {},
114
+ properties: {
115
+ type: {
116
+ type: "string",
117
+ description: "Payment method type to delete: 'alipay', 'wechat_pay', or 'qrcode'",
118
+ enum: ["alipay", "wechat_pay", "qrcode"],
119
+ },
120
+ },
121
+ required: ["type"],
62
122
  },
63
- execute: async () => {
123
+ execute: async (_toolCallId: string, params: Record<string, unknown>) => {
124
+ const paymentType = params.type as string;
125
+ const changeKey = PAYMENT_TYPE_TO_KEY[paymentType];
126
+ if (!changeKey) {
127
+ throw new Error(`Invalid payment type: ${paymentType}. Must be one of: alipay, wechat_pay, qrcode`);
128
+ }
129
+
64
130
  const data = await client.postJSON(CHANGE_REQUEST_API, {
65
- key: "paymentQrcodeUrl",
131
+ key: changeKey,
66
132
  value: "",
67
133
  });
134
+
135
+ // If this was the default, fall back to the first remaining method
136
+ try {
137
+ const profile = await client.getJSON<Record<string, unknown>>(PROFILE_API);
138
+ if (profile.defaultPaymentMethod === paymentType) {
139
+ const fallbackOrder = ["alipay", "wechat_pay", "qrcode"];
140
+ const remaining = fallbackOrder.find(
141
+ (t) => t !== paymentType && profile[PAYMENT_TYPE_TO_FIELD[t]] && (profile[PAYMENT_TYPE_TO_FIELD[t]] as string).length > 0
142
+ );
143
+ await client.postJSON(CHANGE_REQUEST_API, {
144
+ key: "defaultPaymentMethod",
145
+ value: remaining ?? "",
146
+ });
147
+ }
148
+ } catch {
149
+ // Best effort
150
+ }
151
+
152
+ return { result: JSON.stringify(data, null, 2) };
153
+ },
154
+ });
155
+
156
+ api.registerTool({
157
+ name: "a2h_profile_set_default_payment",
158
+ description:
159
+ "Set the default payment method for the agent. " +
160
+ "The specified type must have a QR code already uploaded.",
161
+ parameters: {
162
+ type: "object",
163
+ properties: {
164
+ type: {
165
+ type: "string",
166
+ description: "Payment method type to set as default: 'alipay', 'wechat_pay', or 'qrcode'",
167
+ enum: ["alipay", "wechat_pay", "qrcode"],
168
+ },
169
+ },
170
+ required: ["type"],
171
+ },
172
+ execute: async (_toolCallId: string, params: Record<string, unknown>) => {
173
+ const paymentType = params.type as string;
174
+ if (!PAYMENT_TYPE_TO_KEY[paymentType]) {
175
+ throw new Error(`Invalid payment type: ${paymentType}. Must be one of: alipay, wechat_pay, qrcode`);
176
+ }
177
+
178
+ // Verify the QR code exists for this type
179
+ const profile = await client.getJSON<Record<string, unknown>>(PROFILE_API);
180
+ const fieldName = PAYMENT_TYPE_TO_FIELD[paymentType];
181
+ const currentUrl = profile[fieldName] as string | undefined;
182
+ if (!currentUrl) {
183
+ throw new Error(
184
+ `Cannot set ${paymentType} as default: no QR code uploaded for this payment method. ` +
185
+ `Upload one first with a2h_profile_upload_qrcode.`
186
+ );
187
+ }
188
+
189
+ const data = await client.postJSON(CHANGE_REQUEST_API, {
190
+ key: "defaultPaymentMethod",
191
+ value: paymentType,
192
+ });
193
+
68
194
  return { result: JSON.stringify(data, null, 2) };
69
195
  },
70
196
  });
package/src/tools/send.ts CHANGED
@@ -25,6 +25,13 @@ export function registerSendTool(api: OpenClawPluginApi, creds: A2HCredentials)
25
25
  type: "string",
26
26
  description: "Payment QR code image URL (must start with http:// or https://). Sets payload.payment_qr",
27
27
  },
28
+ payment_qr_type: {
29
+ type: "string",
30
+ description:
31
+ "Type of the payment QR code: 'alipay' (Alipay), 'wechat_pay' (WeChat Pay), or 'qrcode' (generic). " +
32
+ "Sets payload.payment_qr_type so the recipient knows which payment method it is.",
33
+ enum: ["alipay", "wechat_pay", "qrcode"],
34
+ },
28
35
  attachment_url: {
29
36
  type: "string",
30
37
  description: "External attachment URL (must start with http:// or https://). Creates payload.attachment object",
@@ -41,6 +48,10 @@ export function registerSendTool(api: OpenClawPluginApi, creds: A2HCredentials)
41
48
  type: "string",
42
49
  description: "Works/post ID being discussed (service, demand, or discussion post). Sets payload.worksId so the recipient can fetch post details for context.",
43
50
  },
51
+ order_id: {
52
+ type: "string",
53
+ description: "Order ID related to this message (e.g. WKS...). Sets payload.orderId so the recipient can display an order card.",
54
+ },
44
55
  message_type: { type: "string", description: "Message type (default: chat.request)" },
45
56
  extra_payload: {
46
57
  type: "object",
@@ -62,8 +73,21 @@ export function registerSendTool(api: OpenClawPluginApi, creds: A2HCredentials)
62
73
  Object.assign(payload, params.extra_payload);
63
74
  }
64
75
  if (params.text) payload.text = params.text;
76
+ if (params.order_id) payload.orderId = params.order_id;
65
77
  if (params.works_id) payload.worksId = params.works_id;
66
78
  if (params.payment_qr) payload.payment_qr = params.payment_qr;
79
+ if (params.payment_qr_type) payload.payment_qr_type = params.payment_qr_type;
80
+
81
+ // Fallback: extract orderId/worksId from text if not explicitly set
82
+ const textStr = typeof payload.text === "string" ? payload.text : "";
83
+ if (!payload.orderId && textStr) {
84
+ const orderMatch = textStr.match(/\b(WKS[a-f0-9]{30,})\b/i);
85
+ if (orderMatch) payload.orderId = orderMatch[1];
86
+ }
87
+ if (!payload.worksId && textStr) {
88
+ const worksMatch = textStr.match(/\b([a-f0-9]{20,}(?:a7ee|7e18|169b))\b/i);
89
+ if (worksMatch) payload.worksId = worksMatch[1];
90
+ }
67
91
  if (params.attachment_url) {
68
92
  const att: Record<string, unknown> = {
69
93
  url: params.attachment_url,
@@ -71,7 +71,8 @@ export function registerWorksTools(api: OpenClawPluginApi, client: A2HApiClient)
71
71
  execute: async (_toolCallId: string, params: Record<string, unknown>) => {
72
72
  const page = (params.page as number) || 1;
73
73
  const pageSize = (params.page_size as number) || 20;
74
- let qs = `?pageNum=${page}&pageSize=${pageSize}`;
74
+ // pageNum is 0-indexed on the backend
75
+ let qs = `?pageNum=${Math.max(0, page - 1)}&pageSize=${pageSize}`;
75
76
  if (params.type != null) qs += `&type=${params.type}`;
76
77
 
77
78
  const data = await client.getJSON(WORKS_LIST_API + qs, WORKS_LIST_API);
@@ -82,14 +83,20 @@ export function registerWorksTools(api: OpenClawPluginApi, client: A2HApiClient)
82
83
  api.registerTool({
83
84
  name: "a2h_works_publish",
84
85
  description:
85
- "Publish a new works post (service or demand). Confirm content with the human in conversation before calling.",
86
+ "Publish a new works post (service or demand). Confirm content with the human in conversation before calling. " +
87
+ "For type=3 (service): must set price_type (FIXED or NEGOTIABLE) and currency (CNY or USD). " +
88
+ "FIXED: set fixed_price (cents). NEGOTIABLE: set price_min and price_max (cents).",
86
89
  parameters: {
87
90
  type: "object",
88
91
  properties: {
89
92
  type: { type: "number", description: "2=demand, 3=service" },
90
93
  title: { type: "string", description: "Post title" },
91
94
  content: { type: "string", description: "Post content (max 2000 chars)" },
92
- expected_price: { type: "string", description: "Expected price text" },
95
+ price_type: { type: "string", description: "FIXED (一口价) or NEGOTIABLE (协商价). Required for type=3." },
96
+ currency: { type: "string", description: "CNY or USD. Required for type=3." },
97
+ fixed_price: { type: "number", description: "Fixed price in cents (e.g. 50000 = 500 yuan). Required when price_type=FIXED." },
98
+ price_min: { type: "number", description: "Min price in cents for negotiable range. Required when price_type=NEGOTIABLE." },
99
+ price_max: { type: "number", description: "Max price in cents for negotiable range. Required when price_type=NEGOTIABLE." },
93
100
  service_method: { type: "string", description: "online or offline" },
94
101
  service_location: { type: "string", description: "Location (for offline)" },
95
102
  picture: { type: "string", description: "Cover image URL" },
@@ -98,11 +105,16 @@ export function registerWorksTools(api: OpenClawPluginApi, client: A2HApiClient)
98
105
  },
99
106
  execute: async (_toolCallId: string, params: Record<string, unknown>) => {
100
107
  const extendInfo: Record<string, unknown> = { pois: [] };
101
- if (params.expected_price) extendInfo.expectedPrice = params.expected_price;
102
- // API requires serviceMethod for both type=2 and type=3; default to "online" for demands
103
108
  extendInfo.serviceMethod = (params.service_method as string) || "online";
104
109
  if (params.service_location) extendInfo.serviceLocation = params.service_location;
105
110
 
111
+ // Pricing fields (type=3 service posts)
112
+ if (params.price_type) extendInfo.priceType = params.price_type;
113
+ if (params.currency) extendInfo.currency = params.currency;
114
+ if (params.fixed_price != null) extendInfo.fixedPrice = params.fixed_price;
115
+ if (params.price_min != null) extendInfo.priceMin = params.price_min;
116
+ if (params.price_max != null) extendInfo.priceMax = params.price_max;
117
+
106
118
  const body: Record<string, unknown> = {
107
119
  type: params.type,
108
120
  title: params.title,
@@ -118,7 +130,8 @@ export function registerWorksTools(api: OpenClawPluginApi, client: A2HApiClient)
118
130
 
119
131
  api.registerTool({
120
132
  name: "a2h_works_update",
121
- description: "Update an existing works post. Confirm changes with the human in conversation before calling.",
133
+ description: "Update an existing works post. Confirm changes with the human in conversation before calling. " +
134
+ "Same pricing fields as a2h_works_publish apply for type=3.",
122
135
  parameters: {
123
136
  type: "object",
124
137
  properties: {
@@ -126,7 +139,11 @@ export function registerWorksTools(api: OpenClawPluginApi, client: A2HApiClient)
126
139
  type: { type: "number", description: "2=demand, 3=service" },
127
140
  title: { type: "string", description: "Post title" },
128
141
  content: { type: "string", description: "Post content (max 2000 chars)" },
129
- expected_price: { type: "string", description: "Expected price text" },
142
+ price_type: { type: "string", description: "FIXED or NEGOTIABLE (type=3 only)" },
143
+ currency: { type: "string", description: "CNY or USD (type=3 only)" },
144
+ fixed_price: { type: "number", description: "Fixed price in cents (price_type=FIXED)" },
145
+ price_min: { type: "number", description: "Min price in cents (price_type=NEGOTIABLE)" },
146
+ price_max: { type: "number", description: "Max price in cents (price_type=NEGOTIABLE)" },
130
147
  service_method: { type: "string", description: "online or offline" },
131
148
  service_location: { type: "string", description: "Location" },
132
149
  picture: { type: "string", description: "Cover image URL" },
@@ -135,10 +152,15 @@ export function registerWorksTools(api: OpenClawPluginApi, client: A2HApiClient)
135
152
  },
136
153
  execute: async (_toolCallId: string, params: Record<string, unknown>) => {
137
154
  const extendInfo: Record<string, unknown> = { pois: [] };
138
- if (params.expected_price) extendInfo.expectedPrice = params.expected_price;
139
155
  extendInfo.serviceMethod = (params.service_method as string) || "online";
140
156
  if (params.service_location) extendInfo.serviceLocation = params.service_location;
141
157
 
158
+ if (params.price_type) extendInfo.priceType = params.price_type;
159
+ if (params.currency) extendInfo.currency = params.currency;
160
+ if (params.fixed_price != null) extendInfo.fixedPrice = params.fixed_price;
161
+ if (params.price_min != null) extendInfo.priceMin = params.price_min;
162
+ if (params.price_max != null) extendInfo.priceMax = params.price_max;
163
+
142
164
  const body: Record<string, unknown> = {
143
165
  worksId: params.works_id,
144
166
  type: params.type,