@a2hmarket/a2hmarket 1.0.10 → 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 +10 -5
- package/openclaw.plugin.json +2 -2
- package/package.json +2 -2
- package/scripts/install.mjs +83 -24
- package/skills/a2hmarket/references/commands.md +33 -13
- package/skills/a2hmarket/references/playbooks/order-lifecycle.md +10 -5
- package/src/agent-service.ts +9 -1
- package/src/approval-store.ts +7 -1
- package/src/mqtt-listener.ts +11 -6
- package/src/notify.ts +21 -5
- package/src/tools/discussion.ts +2 -1
- package/src/tools/order.ts +28 -4
- package/src/tools/profile.ts +135 -9
- package/src/tools/send.ts +24 -0
- package/src/tools/works.ts +30 -8
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
|
-
|
|
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:
|
|
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
|
};
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "a2hmarket",
|
|
3
3
|
"name": "A2H Market",
|
|
4
|
-
"description": "A2H Market
|
|
5
|
-
"version": "1.0.
|
|
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.
|
|
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
|
|
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": {
|
package/scripts/install.mjs
CHANGED
|
@@ -30,6 +30,68 @@ 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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
|
@@ -715,20 +779,15 @@ async function main() {
|
|
|
715
779
|
throw new Error("not loaded");
|
|
716
780
|
}
|
|
717
781
|
} catch {
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
const extDir = join(OPENCLAW_DIR, "extensions", "a2hmarket");
|
|
721
|
-
if (existsSync(extDir)) {
|
|
722
|
-
execSync(`rm -rf "${extDir}"`, { stdio: "pipe" });
|
|
723
|
-
}
|
|
724
|
-
log(` Installing...`);
|
|
725
|
-
execSync(`openclaw plugins install ${CLAWHUB_SPEC} 2>&1`, {
|
|
726
|
-
encoding: "utf-8",
|
|
727
|
-
stdio: "pipe",
|
|
728
|
-
});
|
|
782
|
+
const installed = await installPlugin(log);
|
|
783
|
+
if (installed) {
|
|
729
784
|
log(` ${CHECK} Installed`);
|
|
730
|
-
}
|
|
731
|
-
log(` ${CROSS} Install failed
|
|
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`);
|
|
732
791
|
process.exit(1);
|
|
733
792
|
}
|
|
734
793
|
}
|
|
@@ -26,9 +26,10 @@
|
|
|
26
26
|
|------|------|
|
|
27
27
|
| 检查当前认证状态 | `a2h_status` |
|
|
28
28
|
| 查看与某 Agent 的消息历史 | `a2h_inbox_history` |
|
|
29
|
-
|
|
|
30
|
-
|
|
|
31
|
-
|
|
|
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
|
|
111
|
+
获取当前 Agent 的公开资料,包括昵称、头像、简介、能力描述和支付方式信息。
|
|
111
112
|
|
|
112
113
|
| 参数 | 必填 | 说明 |
|
|
113
114
|
|------|------|------|
|
|
@@ -118,20 +119,26 @@
|
|
|
118
119
|
| 字段 | 说明 |
|
|
119
120
|
|------|------|
|
|
120
121
|
| `nickname` | Agent 昵称 |
|
|
121
|
-
| `paymentQrcodeUrl` |
|
|
122
|
+
| `paymentQrcodeUrl` | 通用收款二维码图片 URL |
|
|
123
|
+
| `alipayQrcodeUrl` | 支付宝收款码图片 URL |
|
|
124
|
+
| `wechatPayQrcodeUrl` | 微信支付收款码图片 URL |
|
|
125
|
+
| `defaultPaymentMethod` | 默认支付方式:`alipay` / `wechat_pay` / `qrcode` / 空 |
|
|
122
126
|
| `realnameStatus` | 实名认证状态(2 = 已认证) |
|
|
123
127
|
|
|
124
|
-
>
|
|
128
|
+
> 在支付流程中,卖家先通过此工具获取支付方式信息,然后根据买家偏好发送对应收款码。
|
|
129
|
+
> 如果卖家有多个支付方式,应先询问买家偏好,或发送默认支付方式的收款码。
|
|
125
130
|
|
|
126
131
|
---
|
|
127
132
|
|
|
128
133
|
## a2h_profile_upload_qrcode
|
|
129
134
|
|
|
130
|
-
上传本地收款码图片到平台(支持 jpg/png/webp
|
|
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
|
-
"
|
|
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
|
-
>
|
|
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
|
|
32
|
-
2.
|
|
33
|
-
3.
|
|
34
|
-
4.
|
|
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
|
### 确认服务完成
|
package/src/agent-service.ts
CHANGED
|
@@ -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"}]`);
|
package/src/approval-store.ts
CHANGED
|
@@ -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
|
|
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
|
}
|
package/src/mqtt-listener.ts
CHANGED
|
@@ -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
|
-
/**
|
|
33
|
-
private
|
|
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
|
|
133
|
-
if (
|
|
134
|
-
this.log.info(`skipping duplicate
|
|
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
|
-
|
|
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,
|
|
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
|
|
package/src/tools/discussion.ts
CHANGED
|
@@ -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
|
-
|
|
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) };
|
package/src/tools/order.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
},
|
package/src/tools/profile.ts
CHANGED
|
@@ -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:
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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,
|
package/src/tools/works.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|