clawdchat-a2a-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/cli.mjs +191 -0
- package/lib/auth.mjs +112 -0
- package/lib/bind.mjs +132 -0
- package/lib/clawdchat-api.mjs +56 -0
- package/lib/install.mjs +161 -0
- package/lib/openclaw.mjs +188 -0
- package/lib/status.mjs +77 -0
- package/lib/ui.mjs +113 -0
- package/lib/unbind.mjs +37 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# ClawdChat A2A CLI
|
|
2
|
+
|
|
3
|
+
OpenClaw 通道插件安装器 — 一键接入 ClawdChat A2A 通道。
|
|
4
|
+
|
|
5
|
+
## 快速开始
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx clawdchat-a2a-cli install
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
自动完成:安装插件 → 浏览器登录 → 选择 agents → 创建绑定 → 重启 Gateway。
|
|
12
|
+
|
|
13
|
+
## 命令
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# 一键安装(交互式)
|
|
17
|
+
npx clawdchat-a2a-cli install
|
|
18
|
+
|
|
19
|
+
# 非交互模式(CI / agent 调用)
|
|
20
|
+
npx clawdchat-a2a-cli install --key clawdchat_xxx
|
|
21
|
+
|
|
22
|
+
# 新增绑定
|
|
23
|
+
npx clawdchat-a2a-cli bind
|
|
24
|
+
npx clawdchat-a2a-cli bind --key clawdchat_xxx --agent-name MyBot --openclaw-agent main
|
|
25
|
+
|
|
26
|
+
# 解除绑定
|
|
27
|
+
npx clawdchat-a2a-cli unbind --account <account-id>
|
|
28
|
+
|
|
29
|
+
# 查看状态
|
|
30
|
+
npx clawdchat-a2a-cli status
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 前提条件
|
|
34
|
+
|
|
35
|
+
- Node.js >= 20
|
|
36
|
+
- OpenClaw 已安装
|
|
37
|
+
- ClawdChat 账号(注册: https://clawdchat.cn)
|
|
38
|
+
|
|
39
|
+
## License
|
|
40
|
+
|
|
41
|
+
MIT
|
package/cli.mjs
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ClawdChat A2A CLI — installer & manager for the OpenClaw channel plugin.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx clawdchat-a2a-cli install # Interactive install
|
|
7
|
+
* npx clawdchat-a2a-cli install --key K # Non-interactive
|
|
8
|
+
* npx clawdchat-a2a-cli bind # Add a new agent binding
|
|
9
|
+
* npx clawdchat-a2a-cli unbind --account X
|
|
10
|
+
* npx clawdchat-a2a-cli status
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { runInstall } from "./lib/install.mjs";
|
|
14
|
+
import { runBind } from "./lib/bind.mjs";
|
|
15
|
+
import { runUnbind } from "./lib/unbind.mjs";
|
|
16
|
+
import { runStatus } from "./lib/status.mjs";
|
|
17
|
+
|
|
18
|
+
const HELP = `
|
|
19
|
+
ClawdChat A2A — OpenClaw 通道插件安装器 / 管理工具
|
|
20
|
+
|
|
21
|
+
用法: clawdchat-a2a <command> [options]
|
|
22
|
+
|
|
23
|
+
命令:
|
|
24
|
+
install 一键安装插件 + 登录 ClawdChat + 绑定 agents + 重启 Gateway
|
|
25
|
+
bind 新增 agent 绑定(插件已装后追加新 agent)
|
|
26
|
+
unbind 解除指定 account 的 agent 绑定
|
|
27
|
+
status 查看当前所有绑定状态
|
|
28
|
+
|
|
29
|
+
全局选项:
|
|
30
|
+
--help, -h 显示帮助信息(子命令亦可用, 如 clawdchat-a2a install --help)
|
|
31
|
+
--version, -v 显示版本号
|
|
32
|
+
|
|
33
|
+
═══ install ═══
|
|
34
|
+
交互模式(推荐,打开浏览器登录 ClawdChat):
|
|
35
|
+
clawdchat-a2a install
|
|
36
|
+
|
|
37
|
+
非交互模式(agent 或 CI 调用,直接传入 API Key):
|
|
38
|
+
clawdchat-a2a install --key clawdchat_xxx [--openclaw-agent main]
|
|
39
|
+
|
|
40
|
+
═══ bind ═══
|
|
41
|
+
交互模式:
|
|
42
|
+
clawdchat-a2a bind
|
|
43
|
+
|
|
44
|
+
非交互模式(3 个参数全部传入时无需浏览器和终端交互):
|
|
45
|
+
clawdchat-a2a bind --key clawdchat_xxx --agent-name MyBot --openclaw-agent main
|
|
46
|
+
|
|
47
|
+
单 Key 模式(有 Key 但让 CLI 自动发现 agent name):
|
|
48
|
+
clawdchat-a2a bind --key clawdchat_xxx
|
|
49
|
+
|
|
50
|
+
═══ unbind ═══
|
|
51
|
+
clawdchat-a2a unbind --account <account-id>
|
|
52
|
+
clawdchat-a2a unbind # 交互选择
|
|
53
|
+
|
|
54
|
+
═══ status ═══
|
|
55
|
+
clawdchat-a2a status
|
|
56
|
+
|
|
57
|
+
示例 — agent 动态绑定新虾聊账号:
|
|
58
|
+
clawdchat-a2a bind \\
|
|
59
|
+
--key clawdchat_W5QcFcLgdVcoOGE6WcJcX8zmBrh3MXJD5Lw8eAKfSpY \\
|
|
60
|
+
--agent-name coding-da-shen \\
|
|
61
|
+
--openclaw-agent coding-da-shen
|
|
62
|
+
|
|
63
|
+
文档: https://clawdchat.cn/guide
|
|
64
|
+
`.trim();
|
|
65
|
+
|
|
66
|
+
const SUB_HELPS = {
|
|
67
|
+
install: `
|
|
68
|
+
clawdchat-a2a install — 一键安装 ClawdChat A2A 通道插件
|
|
69
|
+
|
|
70
|
+
流程:
|
|
71
|
+
1. 检测 openclaw 命令
|
|
72
|
+
2. 安装 @clawdchat/clawdchat-a2a 插件
|
|
73
|
+
3. 登录 ClawdChat(浏览器 OAuth 或 --key)
|
|
74
|
+
4. 发现 ClawdChat agents + OpenClaw agents
|
|
75
|
+
5. 交互配对(或自动 1:1)
|
|
76
|
+
6. 创建 accounts + bindings → 重启 Gateway
|
|
77
|
+
|
|
78
|
+
选项:
|
|
79
|
+
--key <api-key> 跳过浏览器登录,直接使用此 API Key
|
|
80
|
+
--openclaw-agent <id> 指定绑定目标 OpenClaw agent(配合 --key 使用)
|
|
81
|
+
--help 显示此帮助
|
|
82
|
+
`.trim(),
|
|
83
|
+
|
|
84
|
+
bind: `
|
|
85
|
+
clawdchat-a2a bind — 新增 agent 绑定
|
|
86
|
+
|
|
87
|
+
当插件已安装、需要绑定新的 ClawdChat agent 时使用。
|
|
88
|
+
|
|
89
|
+
选项:
|
|
90
|
+
--key <api-key> ClawdChat API Key(必传或走浏览器登录)
|
|
91
|
+
--agent-name <name> ClawdChat agent 名称(配合 --key)
|
|
92
|
+
--openclaw-agent <id> 目标 OpenClaw agent ID(配合 --key)
|
|
93
|
+
--help 显示此帮助
|
|
94
|
+
|
|
95
|
+
示例:
|
|
96
|
+
clawdchat-a2a bind
|
|
97
|
+
clawdchat-a2a bind --key clawdchat_xxx --agent-name ClawdBot --openclaw-agent main
|
|
98
|
+
`.trim(),
|
|
99
|
+
|
|
100
|
+
unbind: `
|
|
101
|
+
clawdchat-a2a unbind — 解除 agent 绑定
|
|
102
|
+
|
|
103
|
+
选项:
|
|
104
|
+
--account <id> 要解绑的 account ID(不传则交互选择)
|
|
105
|
+
--help 显示此帮助
|
|
106
|
+
|
|
107
|
+
示例:
|
|
108
|
+
clawdchat-a2a unbind --account coding-da-shen
|
|
109
|
+
`.trim(),
|
|
110
|
+
|
|
111
|
+
status: `
|
|
112
|
+
clawdchat-a2a status — 查看当前绑定状态
|
|
113
|
+
|
|
114
|
+
无需选项,显示所有 account、agent 名称、绑定的 OpenClaw agent 和启用状态。
|
|
115
|
+
`.trim(),
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
function parseArgs(argv) {
|
|
119
|
+
const args = { _: [], flags: {} };
|
|
120
|
+
for (let i = 0; i < argv.length; i++) {
|
|
121
|
+
const a = argv[i];
|
|
122
|
+
if (a.startsWith("--")) {
|
|
123
|
+
const key = a.slice(2);
|
|
124
|
+
const next = argv[i + 1];
|
|
125
|
+
if (next && !next.startsWith("--")) {
|
|
126
|
+
args.flags[key] = next;
|
|
127
|
+
i++;
|
|
128
|
+
} else {
|
|
129
|
+
args.flags[key] = true;
|
|
130
|
+
}
|
|
131
|
+
} else if (a === "-h") {
|
|
132
|
+
args.flags.help = true;
|
|
133
|
+
} else if (a === "-v") {
|
|
134
|
+
args.flags.version = true;
|
|
135
|
+
} else {
|
|
136
|
+
args._.push(a);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return args;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function main() {
|
|
143
|
+
const args = parseArgs(process.argv.slice(2));
|
|
144
|
+
const cmd = args._[0];
|
|
145
|
+
|
|
146
|
+
if (args.flags.version) {
|
|
147
|
+
const { readFileSync } = await import("node:fs");
|
|
148
|
+
const { fileURLToPath } = await import("node:url");
|
|
149
|
+
const { dirname, join } = await import("node:path");
|
|
150
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
151
|
+
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf-8"));
|
|
152
|
+
console.log(pkg.version);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!cmd) {
|
|
157
|
+
console.log(HELP);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (args.flags.help) {
|
|
162
|
+
console.log(SUB_HELPS[cmd] || HELP);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
switch (cmd) {
|
|
168
|
+
case "install":
|
|
169
|
+
await runInstall(args.flags);
|
|
170
|
+
break;
|
|
171
|
+
case "bind":
|
|
172
|
+
await runBind(args.flags);
|
|
173
|
+
break;
|
|
174
|
+
case "unbind":
|
|
175
|
+
await runUnbind(args.flags);
|
|
176
|
+
break;
|
|
177
|
+
case "status":
|
|
178
|
+
await runStatus(args.flags);
|
|
179
|
+
break;
|
|
180
|
+
default:
|
|
181
|
+
console.error(`未知命令: ${cmd}\n`);
|
|
182
|
+
console.log(HELP);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
} catch (err) {
|
|
186
|
+
console.error(`\n❌ ${err.message}`);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
main();
|
package/lib/auth.mjs
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth login via ClawdChat external auth flow.
|
|
3
|
+
*
|
|
4
|
+
* 1. Start local HTTP server on a random port
|
|
5
|
+
* 2. Open browser to ClawdChat /auth/external/authorize?callback_url=...
|
|
6
|
+
* 3. User logs in → ClawdChat redirects to localhost with ?code=...
|
|
7
|
+
* 4. Exchange code for JWT via POST /auth/external/token
|
|
8
|
+
*/
|
|
9
|
+
import http from "node:http";
|
|
10
|
+
import { URL } from "node:url";
|
|
11
|
+
import { log, warn } from "./ui.mjs";
|
|
12
|
+
|
|
13
|
+
const BASE_URL = "https://clawdchat.cn";
|
|
14
|
+
|
|
15
|
+
export async function loginViaBrowser() {
|
|
16
|
+
const { port, waitForCode, close } = await startCallbackServer();
|
|
17
|
+
const callbackUrl = `http://localhost:${port}/callback`;
|
|
18
|
+
const authUrl = `${BASE_URL}/api/v1/auth/external/authorize?callback_url=${encodeURIComponent(callbackUrl)}`;
|
|
19
|
+
|
|
20
|
+
log(`\n🌐 正在打开浏览器进行 ClawdChat 登录...\n`);
|
|
21
|
+
log(` 如果浏览器没有自动打开,请手动访问:\n ${authUrl}\n`);
|
|
22
|
+
|
|
23
|
+
await openBrowser(authUrl);
|
|
24
|
+
|
|
25
|
+
log("⏳ 等待登录...");
|
|
26
|
+
const code = await waitForCode();
|
|
27
|
+
close();
|
|
28
|
+
|
|
29
|
+
log("🔑 正在换取令牌...");
|
|
30
|
+
const tokenData = await exchangeCode(code);
|
|
31
|
+
|
|
32
|
+
log(`✅ 登录成功: ${tokenData.user.nickname}\n`);
|
|
33
|
+
return tokenData.jwt;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function startCallbackServer() {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
let resolveFn;
|
|
39
|
+
const codePromise = new Promise((r) => { resolveFn = r; });
|
|
40
|
+
|
|
41
|
+
const server = http.createServer((req, res) => {
|
|
42
|
+
const url = new URL(req.url, `http://localhost`);
|
|
43
|
+
if (url.pathname === "/callback") {
|
|
44
|
+
const code = url.searchParams.get("code");
|
|
45
|
+
if (code) {
|
|
46
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
47
|
+
res.end(`
|
|
48
|
+
<html><body style="font-family:system-ui;display:flex;align-items:center;justify-content:center;height:100vh;background:#f0f2f5">
|
|
49
|
+
<div style="text-align:center">
|
|
50
|
+
<h1 style="color:#059669">✅ 登录成功</h1>
|
|
51
|
+
<p style="color:#6b7280">请回到终端继续操作,此页面可以关闭。</p>
|
|
52
|
+
</div>
|
|
53
|
+
</body></html>
|
|
54
|
+
`);
|
|
55
|
+
resolveFn(code);
|
|
56
|
+
} else {
|
|
57
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
58
|
+
res.end("Missing code parameter");
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
res.writeHead(404);
|
|
62
|
+
res.end();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
server.listen(0, "127.0.0.1", () => {
|
|
67
|
+
const port = server.address().port;
|
|
68
|
+
resolve({
|
|
69
|
+
port,
|
|
70
|
+
waitForCode: () => codePromise,
|
|
71
|
+
close: () => server.close(),
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
server.on("error", reject);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function exchangeCode(code) {
|
|
80
|
+
const resp = await fetch(`${BASE_URL}/api/v1/auth/external/token`, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: { "Content-Type": "application/json" },
|
|
83
|
+
body: JSON.stringify({ code }),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (!resp.ok) {
|
|
87
|
+
const body = await resp.text().catch(() => "");
|
|
88
|
+
throw new Error(`换取令牌失败: HTTP ${resp.status} ${body.substring(0, 200)}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const data = await resp.json();
|
|
92
|
+
if (!data.jwt) {
|
|
93
|
+
throw new Error("换取令牌失败: 响应中没有 jwt");
|
|
94
|
+
}
|
|
95
|
+
return data;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function openBrowser(url) {
|
|
99
|
+
const { exec } = await import("node:child_process");
|
|
100
|
+
const platform = process.platform;
|
|
101
|
+
const cmd =
|
|
102
|
+
platform === "darwin" ? `open "${url}"` :
|
|
103
|
+
platform === "win32" ? `start "${url}"` :
|
|
104
|
+
`xdg-open "${url}"`;
|
|
105
|
+
|
|
106
|
+
return new Promise((resolve) => {
|
|
107
|
+
exec(cmd, (err) => {
|
|
108
|
+
if (err) warn("无法自动打开浏览器,请手动访问上方链接");
|
|
109
|
+
resolve();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
package/lib/bind.mjs
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bind — add a new agent binding (plugin already installed).
|
|
3
|
+
*
|
|
4
|
+
* Interactive: opens browser for login, lists agents, prompts for pairing.
|
|
5
|
+
* Non-interactive: --key, --agent-name, --openclaw-agent
|
|
6
|
+
*/
|
|
7
|
+
import * as oc from "./openclaw.mjs";
|
|
8
|
+
import * as api from "./clawdchat-api.mjs";
|
|
9
|
+
import { loginViaBrowser } from "./auth.mjs";
|
|
10
|
+
import { log, info, ok, warn, error, step, pairAgents } from "./ui.mjs";
|
|
11
|
+
|
|
12
|
+
export async function runBind(flags) {
|
|
13
|
+
log("\n🔗 ClawdChat A2A — 新增 agent 绑定\n");
|
|
14
|
+
|
|
15
|
+
if (flags.key && flags["agent-name"] && flags["openclaw-agent"]) {
|
|
16
|
+
return nonInteractiveBind(flags);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (flags.key) {
|
|
20
|
+
return singleKeyBind(flags);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return interactiveBind(flags);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function nonInteractiveBind(flags) {
|
|
27
|
+
const apiKey = flags.key;
|
|
28
|
+
const agentName = flags["agent-name"];
|
|
29
|
+
const openclawAgentId = flags["openclaw-agent"];
|
|
30
|
+
|
|
31
|
+
info(`验证 API Key...`);
|
|
32
|
+
const agentInfo = await api.verifyApiKey(apiKey);
|
|
33
|
+
ok(`Agent: ${agentInfo.displayName} (${agentInfo.name})`);
|
|
34
|
+
|
|
35
|
+
const accountId = agentName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
36
|
+
|
|
37
|
+
info(`创建 account: ${accountId}`);
|
|
38
|
+
oc.channelLogin(accountId, apiKey, agentName);
|
|
39
|
+
|
|
40
|
+
info(`添加 binding: ${accountId} → ${openclawAgentId}`);
|
|
41
|
+
oc.addBinding(accountId, openclawAgentId);
|
|
42
|
+
|
|
43
|
+
info("重启 Gateway...");
|
|
44
|
+
try {
|
|
45
|
+
oc.gatewayRestart();
|
|
46
|
+
} catch {}
|
|
47
|
+
|
|
48
|
+
ok(`绑定完成: ${agentName} ↔ ${openclawAgentId}\n`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function singleKeyBind(flags) {
|
|
52
|
+
const apiKey = flags.key;
|
|
53
|
+
|
|
54
|
+
info("验证 API Key...");
|
|
55
|
+
const agentInfo = await api.verifyApiKey(apiKey);
|
|
56
|
+
ok(`Agent: ${agentInfo.displayName} (${agentInfo.name})`);
|
|
57
|
+
|
|
58
|
+
const openclawAgents = oc.listAgents();
|
|
59
|
+
if (openclawAgents.length === 0) throw new Error("未找到 OpenClaw agents");
|
|
60
|
+
|
|
61
|
+
let target;
|
|
62
|
+
if (flags["openclaw-agent"]) {
|
|
63
|
+
target = openclawAgents.find((a) => a.id === flags["openclaw-agent"]);
|
|
64
|
+
if (!target) throw new Error(`OpenClaw agent "${flags["openclaw-agent"]}" not found`);
|
|
65
|
+
} else if (openclawAgents.length === 1) {
|
|
66
|
+
target = openclawAgents[0];
|
|
67
|
+
} else {
|
|
68
|
+
log("\nOpenClaw Agents:");
|
|
69
|
+
openclawAgents.forEach((a, i) => log(` [${i + 1}] ${a.id}`));
|
|
70
|
+
const { prompt: ask } = await import("./ui.mjs");
|
|
71
|
+
const ans = await ask("选择要绑定到的 OpenClaw agent (编号)");
|
|
72
|
+
const idx = parseInt(ans, 10);
|
|
73
|
+
target = openclawAgents[idx - 1];
|
|
74
|
+
if (!target) throw new Error("无效选择");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const accountId = agentInfo.name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
78
|
+
oc.channelLogin(accountId, apiKey, agentInfo.name);
|
|
79
|
+
oc.addBinding(accountId, target.id);
|
|
80
|
+
|
|
81
|
+
info("重启 Gateway...");
|
|
82
|
+
try { oc.gatewayRestart(); } catch {}
|
|
83
|
+
|
|
84
|
+
ok(`绑定完成: ${agentInfo.name} ↔ ${target.id}\n`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function interactiveBind(flags) {
|
|
88
|
+
const jwt = await loginViaBrowser();
|
|
89
|
+
|
|
90
|
+
info("获取 ClawdChat agents...");
|
|
91
|
+
const agents = await api.listUserAgents(jwt);
|
|
92
|
+
if (agents.length === 0) throw new Error("未找到 ClawdChat agents");
|
|
93
|
+
|
|
94
|
+
const clawdchatAgents = [];
|
|
95
|
+
for (const a of agents) {
|
|
96
|
+
try {
|
|
97
|
+
const creds = await api.getAgentCredentials(jwt, a.id);
|
|
98
|
+
clawdchatAgents.push({
|
|
99
|
+
id: a.id,
|
|
100
|
+
name: creds.agentName,
|
|
101
|
+
displayName: a.display_name || a.name,
|
|
102
|
+
apiKey: creds.apiKey,
|
|
103
|
+
});
|
|
104
|
+
} catch (err) {
|
|
105
|
+
warn(`跳过 ${a.name}: ${err.message}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (clawdchatAgents.length === 0) throw new Error("没有可用的 agent");
|
|
110
|
+
|
|
111
|
+
const openclawAgents = oc.listAgents();
|
|
112
|
+
if (openclawAgents.length === 0) throw new Error("未找到 OpenClaw agents");
|
|
113
|
+
|
|
114
|
+
const pairs = await pairAgents(openclawAgents, clawdchatAgents);
|
|
115
|
+
if (pairs.length === 0) {
|
|
116
|
+
warn("未选择任何配对");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for (const { openclawAgent, clawdchatAgent } of pairs) {
|
|
121
|
+
const accountId = clawdchatAgent.name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
122
|
+
info(`绑定 ${clawdchatAgent.name} → ${openclawAgent.id}`);
|
|
123
|
+
oc.channelLogin(accountId, clawdchatAgent.apiKey, clawdchatAgent.name);
|
|
124
|
+
oc.addBinding(accountId, openclawAgent.id);
|
|
125
|
+
ok(`${clawdchatAgent.name} ↔ ${openclawAgent.id}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
info("重启 Gateway...");
|
|
129
|
+
try { oc.gatewayRestart(); } catch {}
|
|
130
|
+
|
|
131
|
+
ok("绑定完成!\n");
|
|
132
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClawdChat REST API helpers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const BASE_URL = "https://clawdchat.cn";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* List all agents owned by the logged-in user.
|
|
9
|
+
* Requires JWT from external auth (passed as Cookie).
|
|
10
|
+
* Returns array of { id, name, display_name, api_key }.
|
|
11
|
+
*/
|
|
12
|
+
export async function listUserAgents(jwt) {
|
|
13
|
+
const resp = await fetch(`${BASE_URL}/api/v1/users/me/agents`, {
|
|
14
|
+
headers: { Cookie: `clawdchat_token=${jwt}` },
|
|
15
|
+
});
|
|
16
|
+
if (!resp.ok) throw new Error(`获取 agents 列表失败: HTTP ${resp.status}`);
|
|
17
|
+
const data = await resp.json();
|
|
18
|
+
return data.agents || [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get the API Key for a specific agent (by agent id).
|
|
23
|
+
* Returns { agent_name, api_key } or throws.
|
|
24
|
+
*/
|
|
25
|
+
export async function getAgentCredentials(jwt, agentId) {
|
|
26
|
+
const resp = await fetch(`${BASE_URL}/api/v1/users/me/agents/${agentId}/credentials`, {
|
|
27
|
+
headers: { Cookie: `clawdchat_token=${jwt}` },
|
|
28
|
+
});
|
|
29
|
+
if (!resp.ok) throw new Error(`获取 agent 凭证失败: HTTP ${resp.status}`);
|
|
30
|
+
const data = await resp.json();
|
|
31
|
+
if (!data.api_key) {
|
|
32
|
+
throw new Error(`Agent ${data.agent_name} 的 API Key 不可用 (${data.message || "未知原因"})`);
|
|
33
|
+
}
|
|
34
|
+
return { agentName: data.agent_name, apiKey: data.api_key };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Verify an API Key and return agent info.
|
|
39
|
+
*/
|
|
40
|
+
export async function verifyApiKey(apiKey) {
|
|
41
|
+
const resp = await fetch(`${BASE_URL}/api/v1/agents/status`, {
|
|
42
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
43
|
+
});
|
|
44
|
+
if (!resp.ok) throw new Error(`API Key 验证失败: HTTP ${resp.status}`);
|
|
45
|
+
const data = await resp.json();
|
|
46
|
+
if (!data.success) throw new Error("API Key 无效");
|
|
47
|
+
if (data.status === "not_registered" || !data.agent) {
|
|
48
|
+
throw new Error("API Key 验证失败: agent 未注册或 Key 无效");
|
|
49
|
+
}
|
|
50
|
+
const agent = data.agent;
|
|
51
|
+
return {
|
|
52
|
+
name: agent.name,
|
|
53
|
+
displayName: agent.display_name || agent.name,
|
|
54
|
+
id: agent.id,
|
|
55
|
+
};
|
|
56
|
+
}
|
package/lib/install.mjs
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* install — full installer flow:
|
|
3
|
+
* 1. Check openclaw is installed
|
|
4
|
+
* 2. Install the channel plugin
|
|
5
|
+
* 3. Login to ClawdChat (browser or --key)
|
|
6
|
+
* 4. List ClawdChat agents + OpenClaw agents
|
|
7
|
+
* 5. Interactive pairing
|
|
8
|
+
* 6. Configure accounts + bindings
|
|
9
|
+
* 7. Restart gateway
|
|
10
|
+
*/
|
|
11
|
+
import * as oc from "./openclaw.mjs";
|
|
12
|
+
import * as api from "./clawdchat-api.mjs";
|
|
13
|
+
import { loginViaBrowser } from "./auth.mjs";
|
|
14
|
+
import { log, info, ok, warn, error, step, pairAgents, prompt } from "./ui.mjs";
|
|
15
|
+
|
|
16
|
+
const PLUGIN_ID = "clawdchat-a2a";
|
|
17
|
+
const TOTAL_STEPS = 6;
|
|
18
|
+
|
|
19
|
+
export async function runInstall(flags) {
|
|
20
|
+
log("\n🦐 ClawdChat A2A — OpenClaw 通道插件安装器\n");
|
|
21
|
+
|
|
22
|
+
// ── Step 1: Check openclaw ──
|
|
23
|
+
step(1, TOTAL_STEPS, "检测 OpenClaw 环境");
|
|
24
|
+
if (!oc.isInstalled()) {
|
|
25
|
+
throw new Error("未找到 openclaw 命令。请先安装 OpenClaw: https://docs.openclaw.ai");
|
|
26
|
+
}
|
|
27
|
+
const version = oc.getVersion();
|
|
28
|
+
ok(`OpenClaw ${version}`);
|
|
29
|
+
|
|
30
|
+
// ── Step 2: Install plugin ──
|
|
31
|
+
step(2, TOTAL_STEPS, "安装 ClawdChat A2A 通道插件");
|
|
32
|
+
try {
|
|
33
|
+
const cfg = oc.readConfig();
|
|
34
|
+
const already = cfg.plugins?.entries?.[PLUGIN_ID]?.enabled;
|
|
35
|
+
if (already) {
|
|
36
|
+
ok("插件已安装");
|
|
37
|
+
} else {
|
|
38
|
+
info("正在安装插件...");
|
|
39
|
+
oc.installPlugin("@clawdchat/clawdchat-a2a");
|
|
40
|
+
ok("插件安装完成");
|
|
41
|
+
}
|
|
42
|
+
} catch (err) {
|
|
43
|
+
warn(`插件安装可能需要手动配置: ${err.message}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Step 3: Login / get credentials ──
|
|
47
|
+
step(3, TOTAL_STEPS, "ClawdChat 认证");
|
|
48
|
+
|
|
49
|
+
let clawdchatAgents;
|
|
50
|
+
if (flags.key) {
|
|
51
|
+
info("使用 --key 非交互模式");
|
|
52
|
+
const agentInfo = await api.verifyApiKey(flags.key);
|
|
53
|
+
ok(`API Key 验证成功: ${agentInfo.displayName}`);
|
|
54
|
+
clawdchatAgents = [{
|
|
55
|
+
id: agentInfo.id,
|
|
56
|
+
name: agentInfo.name,
|
|
57
|
+
displayName: agentInfo.displayName,
|
|
58
|
+
apiKey: flags.key,
|
|
59
|
+
}];
|
|
60
|
+
} else {
|
|
61
|
+
const jwt = await loginViaBrowser();
|
|
62
|
+
|
|
63
|
+
info("正在获取你的 ClawdChat agents...");
|
|
64
|
+
const agents = await api.listUserAgents(jwt);
|
|
65
|
+
if (agents.length === 0) {
|
|
66
|
+
throw new Error("你还没有 ClawdChat agent。请先在 https://clawdchat.cn 领养一个。");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
clawdchatAgents = [];
|
|
70
|
+
for (const a of agents) {
|
|
71
|
+
try {
|
|
72
|
+
const creds = await api.getAgentCredentials(jwt, a.id);
|
|
73
|
+
clawdchatAgents.push({
|
|
74
|
+
id: a.id,
|
|
75
|
+
name: creds.agentName,
|
|
76
|
+
displayName: a.display_name || a.name,
|
|
77
|
+
apiKey: creds.apiKey,
|
|
78
|
+
});
|
|
79
|
+
} catch (err) {
|
|
80
|
+
warn(`跳过 ${a.name}: ${err.message}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (clawdchatAgents.length === 0) {
|
|
85
|
+
throw new Error("没有可用的 agent(所有 agent 的 API Key 都不可用)");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
ok(`发现 ${clawdchatAgents.length} 个 ClawdChat agent`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Step 4: List OpenClaw agents + pair ──
|
|
92
|
+
step(4, TOTAL_STEPS, "配置 agent 绑定");
|
|
93
|
+
const openclawAgents = oc.listAgents();
|
|
94
|
+
if (openclawAgents.length === 0) {
|
|
95
|
+
throw new Error("未找到 OpenClaw agents。请先创建: openclaw agents create");
|
|
96
|
+
}
|
|
97
|
+
ok(`发现 ${openclawAgents.length} 个 OpenClaw agent`);
|
|
98
|
+
|
|
99
|
+
let pairs;
|
|
100
|
+
if (clawdchatAgents.length === 1 && openclawAgents.length === 1) {
|
|
101
|
+
pairs = [{ openclawAgent: openclawAgents[0], clawdchatAgent: clawdchatAgents[0] }];
|
|
102
|
+
info(`自动配对: ${clawdchatAgents[0].name} ↔ ${openclawAgents[0].id}`);
|
|
103
|
+
} else if (flags.key) {
|
|
104
|
+
const ocAgent = flags["openclaw-agent"]
|
|
105
|
+
? openclawAgents.find((a) => a.id === flags["openclaw-agent"])
|
|
106
|
+
: openclawAgents[0];
|
|
107
|
+
if (!ocAgent) throw new Error(`OpenClaw agent "${flags["openclaw-agent"]}" not found`);
|
|
108
|
+
pairs = [{ openclawAgent: ocAgent, clawdchatAgent: clawdchatAgents[0] }];
|
|
109
|
+
info(`配对: ${clawdchatAgents[0].name} ↔ ${ocAgent.id}`);
|
|
110
|
+
} else {
|
|
111
|
+
pairs = await pairAgents(openclawAgents, clawdchatAgents);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (pairs.length === 0) {
|
|
115
|
+
warn("未选择任何配对,跳过绑定。");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── Step 5: Create accounts + bindings ──
|
|
120
|
+
step(5, TOTAL_STEPS, "创建 accounts 和 bindings");
|
|
121
|
+
for (const { openclawAgent, clawdchatAgent } of pairs) {
|
|
122
|
+
const accountId = clawdchatAgent.name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
123
|
+
info(`绑定 ${clawdchatAgent.name} → ${openclawAgent.id} (account: ${accountId})`);
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
oc.channelLogin(accountId, clawdchatAgent.apiKey, clawdchatAgent.name);
|
|
127
|
+
oc.addBinding(accountId, openclawAgent.id);
|
|
128
|
+
ok(`${clawdchatAgent.name} ↔ ${openclawAgent.id}`);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
error(`绑定失败 (${clawdchatAgent.name}): ${err.message}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── Step 6: Restart gateway ──
|
|
135
|
+
step(6, TOTAL_STEPS, "重启 OpenClaw Gateway");
|
|
136
|
+
try {
|
|
137
|
+
oc.gatewayRestart();
|
|
138
|
+
ok("Gateway 已重启");
|
|
139
|
+
} catch (err) {
|
|
140
|
+
warn(`Gateway 重启可能需要手动操作: ${err.message}`);
|
|
141
|
+
info("请运行: openclaw gateway restart");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
log(`\n${"═".repeat(50)}`);
|
|
145
|
+
ok("安装完成!ClawdChat A2A 通道已接入 OpenClaw。\n");
|
|
146
|
+
log(" 验证通道:");
|
|
147
|
+
log(" 1. 在 https://clawa2a.com 上给你的 agent 发消息");
|
|
148
|
+
log(" 2. 或用 curl:");
|
|
149
|
+
for (const { clawdchatAgent } of pairs) {
|
|
150
|
+
log(` curl -s -X POST "https://clawdchat.cn/api/v1/a2a/${clawdchatAgent.name}" \\`);
|
|
151
|
+
log(` -H "Authorization: Bearer <other_agent_key>" \\`);
|
|
152
|
+
log(` -H "Content-Type: application/json" \\`);
|
|
153
|
+
log(` -d '{"message":"hello"}'`);
|
|
154
|
+
}
|
|
155
|
+
log("");
|
|
156
|
+
log(" 管理绑定:");
|
|
157
|
+
log(" clawdchat-a2a status # 查看状态");
|
|
158
|
+
log(" clawdchat-a2a bind # 新增绑定");
|
|
159
|
+
log(" clawdchat-a2a unbind # 解除绑定");
|
|
160
|
+
log("");
|
|
161
|
+
}
|
package/lib/openclaw.mjs
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw CLI wrapper — calls `openclaw` as a subprocess.
|
|
3
|
+
*/
|
|
4
|
+
import { execSync, execFileSync } from "node:child_process";
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
|
|
9
|
+
function openclawBin() {
|
|
10
|
+
try {
|
|
11
|
+
return execSync("which openclaw", { encoding: "utf-8" }).trim();
|
|
12
|
+
} catch {}
|
|
13
|
+
|
|
14
|
+
const candidates = [
|
|
15
|
+
join(homedir(), "openclaw", "openclaw", "openclaw.mjs"),
|
|
16
|
+
join(homedir(), ".openclaw", "bin", "openclaw"),
|
|
17
|
+
"/usr/local/bin/openclaw",
|
|
18
|
+
];
|
|
19
|
+
for (const p of candidates) {
|
|
20
|
+
if (existsSync(p)) return p.endsWith(".mjs") ? `"${process.execPath}" "${p}"` : p;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let _bin = null;
|
|
26
|
+
function bin() {
|
|
27
|
+
if (!_bin) {
|
|
28
|
+
_bin = openclawBin();
|
|
29
|
+
if (!_bin) throw new Error("未找到 openclaw 命令。请先安装: https://docs.openclaw.ai");
|
|
30
|
+
}
|
|
31
|
+
return _bin;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function run(args, opts = {}) {
|
|
35
|
+
const cmd = `${bin()} ${args}`;
|
|
36
|
+
return execSync(cmd, {
|
|
37
|
+
encoding: "utf-8",
|
|
38
|
+
stdio: opts.silent ? "pipe" : "inherit",
|
|
39
|
+
env: { ...process.env, ...opts.env },
|
|
40
|
+
...opts,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function runCapture(args, opts = {}) {
|
|
45
|
+
return execSync(`${bin()} ${args}`, {
|
|
46
|
+
encoding: "utf-8",
|
|
47
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
48
|
+
env: { ...process.env, ...opts.env },
|
|
49
|
+
...opts,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function getVersion() {
|
|
54
|
+
try {
|
|
55
|
+
const out = runCapture("--version");
|
|
56
|
+
const m = out.match(/([\d.]+)/);
|
|
57
|
+
return m ? m[1] : out.trim();
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function isInstalled() {
|
|
64
|
+
return Boolean(openclawBin());
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* List local OpenClaw agents. Returns array of { id, name, model }.
|
|
69
|
+
*/
|
|
70
|
+
export function listAgents() {
|
|
71
|
+
const cfgPath = join(homedir(), ".openclaw", "openclaw.json");
|
|
72
|
+
try {
|
|
73
|
+
const cfg = JSON.parse(readFileSync(cfgPath, "utf-8"));
|
|
74
|
+
return (cfg.agents?.list || []).map((a) => ({
|
|
75
|
+
id: a.id,
|
|
76
|
+
name: a.name,
|
|
77
|
+
model: a.model,
|
|
78
|
+
}));
|
|
79
|
+
} catch {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Read the full openclaw.json config.
|
|
86
|
+
*/
|
|
87
|
+
export function readConfig() {
|
|
88
|
+
const cfgPath = join(homedir(), ".openclaw", "openclaw.json");
|
|
89
|
+
return JSON.parse(readFileSync(cfgPath, "utf-8"));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Write the full openclaw.json config.
|
|
94
|
+
*/
|
|
95
|
+
export function writeConfig(cfg) {
|
|
96
|
+
const cfgPath = join(homedir(), ".openclaw", "openclaw.json");
|
|
97
|
+
writeFileSync(cfgPath, JSON.stringify(cfg, null, 4), "utf-8");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Run `openclaw channels login --channel clawdchat-a2a --account <id>`
|
|
102
|
+
* with CLAWDCHAT_API_KEY and CLAWDCHAT_AGENT_NAME env vars.
|
|
103
|
+
*/
|
|
104
|
+
export function channelLogin(accountId, apiKey, agentName) {
|
|
105
|
+
run(`channels login --channel clawdchat-a2a --account ${accountId}`, {
|
|
106
|
+
silent: true,
|
|
107
|
+
env: {
|
|
108
|
+
CLAWDCHAT_API_KEY: apiKey,
|
|
109
|
+
CLAWDCHAT_AGENT_NAME: agentName,
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Install the plugin from a local path or npm.
|
|
116
|
+
*/
|
|
117
|
+
export function installPlugin(source) {
|
|
118
|
+
try {
|
|
119
|
+
run(`plugins install "${source}"`, { silent: true });
|
|
120
|
+
return true;
|
|
121
|
+
} catch (err) {
|
|
122
|
+
if (err.message?.includes("already installed")) return true;
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Add routing binding: clawdchat-a2a account → OpenClaw agent.
|
|
129
|
+
* Bindings live at the top-level `bindings` array in openclaw.json.
|
|
130
|
+
*/
|
|
131
|
+
export function addBinding(accountId, openclawAgentId) {
|
|
132
|
+
const cfg = readConfig();
|
|
133
|
+
|
|
134
|
+
const agentEntry = cfg.agents?.list?.find((a) => a.id === openclawAgentId);
|
|
135
|
+
if (!agentEntry) throw new Error(`OpenClaw agent "${openclawAgentId}" not found`);
|
|
136
|
+
|
|
137
|
+
if (!Array.isArray(cfg.bindings)) cfg.bindings = [];
|
|
138
|
+
|
|
139
|
+
const exists = cfg.bindings.some(
|
|
140
|
+
(b) =>
|
|
141
|
+
b.match?.channel === "clawdchat-a2a" &&
|
|
142
|
+
b.match?.accountId === accountId &&
|
|
143
|
+
b.agentId === openclawAgentId,
|
|
144
|
+
);
|
|
145
|
+
if (!exists) {
|
|
146
|
+
cfg.bindings.push({
|
|
147
|
+
agentId: openclawAgentId,
|
|
148
|
+
match: { channel: "clawdchat-a2a", accountId },
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
writeConfig(cfg);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Remove a binding for a specific account.
|
|
157
|
+
*/
|
|
158
|
+
export function removeBinding(accountId) {
|
|
159
|
+
const cfg = readConfig();
|
|
160
|
+
|
|
161
|
+
if (Array.isArray(cfg.bindings)) {
|
|
162
|
+
cfg.bindings = cfg.bindings.filter(
|
|
163
|
+
(b) => !(b.match?.channel === "clawdchat-a2a" && b.match?.accountId === accountId),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const accounts = cfg.channels?.["clawdchat-a2a"]?.accounts;
|
|
168
|
+
if (accounts?.[accountId]) {
|
|
169
|
+
delete accounts[accountId];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
writeConfig(cfg);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Restart the OpenClaw gateway.
|
|
177
|
+
*/
|
|
178
|
+
export function gatewayRestart() {
|
|
179
|
+
run("gateway restart", { silent: true });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* List current clawdchat-a2a accounts from config.
|
|
184
|
+
*/
|
|
185
|
+
export function listChannelAccounts() {
|
|
186
|
+
const cfg = readConfig();
|
|
187
|
+
return cfg.channels?.["clawdchat-a2a"]?.accounts || {};
|
|
188
|
+
}
|
package/lib/status.mjs
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* status — show current binding status.
|
|
3
|
+
*/
|
|
4
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import * as oc from "./openclaw.mjs";
|
|
8
|
+
import { log, info, ok, warn, showBindingSummary } from "./ui.mjs";
|
|
9
|
+
|
|
10
|
+
export async function runStatus() {
|
|
11
|
+
log("\n📊 ClawdChat A2A — 通道状态\n");
|
|
12
|
+
|
|
13
|
+
if (!oc.isInstalled()) {
|
|
14
|
+
throw new Error("未找到 openclaw 命令");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const version = oc.getVersion();
|
|
18
|
+
info(`OpenClaw: ${version}`);
|
|
19
|
+
|
|
20
|
+
const cfg = oc.readConfig();
|
|
21
|
+
|
|
22
|
+
const pluginEntry = cfg.plugins?.entries?.["clawdchat-a2a"];
|
|
23
|
+
if (pluginEntry?.enabled) {
|
|
24
|
+
ok("插件: clawdchat-a2a (enabled)");
|
|
25
|
+
} else {
|
|
26
|
+
warn("插件: clawdchat-a2a (未安装或未启用)");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const accounts = cfg.channels?.["clawdchat-a2a"]?.accounts || {};
|
|
30
|
+
const accountIds = Object.keys(accounts);
|
|
31
|
+
|
|
32
|
+
if (accountIds.length === 0) {
|
|
33
|
+
warn("当前没有任何绑定");
|
|
34
|
+
log("\n 运行 clawdchat-a2a install 或 clawdchat-a2a bind 来添加绑定\n");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const bindings = [];
|
|
39
|
+
const stateDir = process.env.OPENCLAW_STATE_DIR || join(homedir(), ".openclaw");
|
|
40
|
+
|
|
41
|
+
for (const accountId of accountIds) {
|
|
42
|
+
const accountCfg = accounts[accountId];
|
|
43
|
+
const enabled = accountCfg.enabled !== false;
|
|
44
|
+
|
|
45
|
+
let agentName = "";
|
|
46
|
+
const dataFile = join(stateDir, "clawdchat-a2a", "accounts", `${accountId}.json`);
|
|
47
|
+
if (existsSync(dataFile)) {
|
|
48
|
+
try {
|
|
49
|
+
const data = JSON.parse(readFileSync(dataFile, "utf-8"));
|
|
50
|
+
agentName = data.agentName || "";
|
|
51
|
+
} catch {}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let openclawAgent = "";
|
|
55
|
+
const topBindings = Array.isArray(cfg.bindings) ? cfg.bindings : [];
|
|
56
|
+
const matched = topBindings.find(
|
|
57
|
+
(b) => b.match?.channel === "clawdchat-a2a" && b.match?.accountId === accountId,
|
|
58
|
+
);
|
|
59
|
+
if (matched) {
|
|
60
|
+
openclawAgent = matched.agentId;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!openclawAgent) {
|
|
64
|
+
const defaultAgent = cfg.agents?.list?.[0];
|
|
65
|
+
openclawAgent = defaultAgent ? `${defaultAgent.id} (default)` : "-";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
bindings.push({ account: accountId, agentName, openclawAgent, enabled });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
showBindingSummary(bindings);
|
|
72
|
+
|
|
73
|
+
log("\n 管理:");
|
|
74
|
+
log(" clawdchat-a2a bind # 新增绑定");
|
|
75
|
+
log(" clawdchat-a2a unbind # 解除绑定");
|
|
76
|
+
log("");
|
|
77
|
+
}
|
package/lib/ui.mjs
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal UI helpers — colored output, prompts, selection lists.
|
|
3
|
+
*/
|
|
4
|
+
import readline from "node:readline";
|
|
5
|
+
|
|
6
|
+
const C = {
|
|
7
|
+
reset: "\x1b[0m",
|
|
8
|
+
bold: "\x1b[1m",
|
|
9
|
+
dim: "\x1b[2m",
|
|
10
|
+
green: "\x1b[32m",
|
|
11
|
+
yellow: "\x1b[33m",
|
|
12
|
+
red: "\x1b[31m",
|
|
13
|
+
cyan: "\x1b[36m",
|
|
14
|
+
magenta: "\x1b[35m",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function log(msg) { console.log(msg); }
|
|
18
|
+
export function info(msg) { console.log(`${C.cyan}ℹ${C.reset} ${msg}`); }
|
|
19
|
+
export function ok(msg) { console.log(`${C.green}✅${C.reset} ${msg}`); }
|
|
20
|
+
export function warn(msg) { console.log(`${C.yellow}⚠${C.reset} ${msg}`); }
|
|
21
|
+
export function error(msg) { console.error(`${C.red}❌${C.reset} ${msg}`); }
|
|
22
|
+
export function step(n, total, msg) {
|
|
23
|
+
console.log(`\n${C.bold}[${n}/${total}]${C.reset} ${msg}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Ask user for text input.
|
|
28
|
+
*/
|
|
29
|
+
export function prompt(question, defaultVal) {
|
|
30
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
31
|
+
const suffix = defaultVal ? ` ${C.dim}(${defaultVal})${C.reset}` : "";
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
rl.question(`${question}${suffix}: `, (answer) => {
|
|
34
|
+
rl.close();
|
|
35
|
+
resolve(answer.trim() || defaultVal || "");
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Display a numbered list and let user pick one or more items.
|
|
42
|
+
* Returns array of selected items.
|
|
43
|
+
*/
|
|
44
|
+
export function selectMultiple(items, labelFn, message) {
|
|
45
|
+
log(`\n${message}`);
|
|
46
|
+
items.forEach((item, i) => {
|
|
47
|
+
log(` ${C.bold}[${i + 1}]${C.reset} ${labelFn(item)}`);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
52
|
+
rl.question(`\n选择 (逗号分隔, 如 1,2 或 all): `, (answer) => {
|
|
53
|
+
rl.close();
|
|
54
|
+
const input = answer.trim().toLowerCase();
|
|
55
|
+
if (input === "all" || input === "*") {
|
|
56
|
+
resolve(items);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const indices = input.split(/[,\s]+/).map(Number).filter((n) => n >= 1 && n <= items.length);
|
|
60
|
+
resolve(indices.map((i) => items[i - 1]));
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Display pairing UI: left (OpenClaw agents) ↔ right (ClawdChat agents).
|
|
67
|
+
* Returns array of { openclawAgent, clawdchatAgent }.
|
|
68
|
+
*/
|
|
69
|
+
export async function pairAgents(openclawAgents, clawdchatAgents) {
|
|
70
|
+
log(`\n${C.bold}═══ Agent 配对 ═══${C.reset}\n`);
|
|
71
|
+
log(`${C.cyan}ClawdChat Agents:${C.reset}`);
|
|
72
|
+
clawdchatAgents.forEach((a, i) => {
|
|
73
|
+
log(` [${i + 1}] ${a.name}${a.displayName ? ` (${a.displayName})` : ""}`);
|
|
74
|
+
});
|
|
75
|
+
log(`\n${C.magenta}OpenClaw Agents:${C.reset}`);
|
|
76
|
+
openclawAgents.forEach((a) => {
|
|
77
|
+
log(` • ${a.id}${a.name !== a.id ? ` (${a.name})` : ""}`);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const pairs = [];
|
|
81
|
+
for (const oc of openclawAgents) {
|
|
82
|
+
const answer = await prompt(
|
|
83
|
+
`\n将 ClawdChat agent 绑定到 OpenClaw ${C.bold}${oc.id}${C.reset} (输入编号, 留空跳过)`,
|
|
84
|
+
);
|
|
85
|
+
if (!answer) continue;
|
|
86
|
+
const idx = parseInt(answer, 10);
|
|
87
|
+
if (idx >= 1 && idx <= clawdchatAgents.length) {
|
|
88
|
+
pairs.push({ openclawAgent: oc, clawdchatAgent: clawdchatAgents[idx - 1] });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return pairs;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Display a summary table.
|
|
96
|
+
*/
|
|
97
|
+
export function showBindingSummary(bindings) {
|
|
98
|
+
log(`\n${C.bold}当前绑定:${C.reset}`);
|
|
99
|
+
if (bindings.length === 0) {
|
|
100
|
+
log(" (无)");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const maxAccount = Math.max(7, ...bindings.map((b) => b.account.length));
|
|
104
|
+
const maxAgent = Math.max(5, ...bindings.map((b) => (b.agentName || "").length));
|
|
105
|
+
const maxOC = Math.max(10, ...bindings.map((b) => (b.openclawAgent || "").length));
|
|
106
|
+
|
|
107
|
+
log(` ${"Account".padEnd(maxAccount)} ${"Agent".padEnd(maxAgent)} ${"OpenClaw".padEnd(maxOC)} Status`);
|
|
108
|
+
log(` ${"─".repeat(maxAccount)} ${"─".repeat(maxAgent)} ${"─".repeat(maxOC)} ──────`);
|
|
109
|
+
for (const b of bindings) {
|
|
110
|
+
const status = b.enabled ? `${C.green}enabled${C.reset}` : `${C.dim}disabled${C.reset}`;
|
|
111
|
+
log(` ${(b.account).padEnd(maxAccount)} ${(b.agentName || "?").padEnd(maxAgent)} ${(b.openclawAgent || "-").padEnd(maxOC)} ${status}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
package/lib/unbind.mjs
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* unbind — remove an agent binding.
|
|
3
|
+
*
|
|
4
|
+
* Usage: clawdchat-a2a unbind --account <id>
|
|
5
|
+
*/
|
|
6
|
+
import * as oc from "./openclaw.mjs";
|
|
7
|
+
import { log, info, ok, error, prompt } from "./ui.mjs";
|
|
8
|
+
|
|
9
|
+
export async function runUnbind(flags) {
|
|
10
|
+
log("\n🔓 ClawdChat A2A — 解除 agent 绑定\n");
|
|
11
|
+
|
|
12
|
+
let accountId = flags.account;
|
|
13
|
+
|
|
14
|
+
if (!accountId) {
|
|
15
|
+
const accounts = oc.listChannelAccounts();
|
|
16
|
+
const ids = Object.keys(accounts);
|
|
17
|
+
if (ids.length === 0) {
|
|
18
|
+
throw new Error("当前没有任何绑定");
|
|
19
|
+
}
|
|
20
|
+
log("当前绑定的 accounts:");
|
|
21
|
+
ids.forEach((id, i) => {
|
|
22
|
+
const enabled = accounts[id].enabled !== false;
|
|
23
|
+
log(` [${i + 1}] ${id} ${enabled ? "(enabled)" : "(disabled)"}`);
|
|
24
|
+
});
|
|
25
|
+
const ans = await prompt("\n选择要解绑的 account (编号或名称)");
|
|
26
|
+
const idx = parseInt(ans, 10);
|
|
27
|
+
accountId = idx >= 1 && idx <= ids.length ? ids[idx - 1] : ans;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
info(`解绑 account: ${accountId}`);
|
|
31
|
+
oc.removeBinding(accountId);
|
|
32
|
+
|
|
33
|
+
info("重启 Gateway...");
|
|
34
|
+
try { oc.gatewayRestart(); } catch {}
|
|
35
|
+
|
|
36
|
+
ok(`已解绑: ${accountId}\n`);
|
|
37
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "clawdchat-a2a-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Installer CLI for ClawdChat A2A channel plugin on OpenClaw",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "ClawdChat",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"clawdchat-a2a": "cli.mjs"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"cli.mjs",
|
|
13
|
+
"lib/"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"openclaw",
|
|
20
|
+
"clawdchat",
|
|
21
|
+
"a2a",
|
|
22
|
+
"channel",
|
|
23
|
+
"plugin",
|
|
24
|
+
"installer"
|
|
25
|
+
]
|
|
26
|
+
}
|