clawdchat-a2a-cli 0.1.1 → 0.2.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/lib/install.mjs +42 -77
- package/lib/web-pair-page.mjs +308 -0
- package/lib/web-pair.mjs +175 -0
- package/package.json +1 -1
package/lib/install.mjs
CHANGED
|
@@ -2,25 +2,24 @@
|
|
|
2
2
|
* install — full installer flow:
|
|
3
3
|
* 1. Check openclaw is installed
|
|
4
4
|
* 2. Install the channel plugin
|
|
5
|
-
* 3.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* 7. Restart gateway
|
|
5
|
+
* 3. Web-based: browser OAuth → pairing page → submit
|
|
6
|
+
* --key mode: non-interactive, terminal only
|
|
7
|
+
* 4. Configure accounts + bindings
|
|
8
|
+
* 5. Restart gateway
|
|
10
9
|
*/
|
|
11
10
|
import * as oc from "./openclaw.mjs";
|
|
12
11
|
import * as api from "./clawdchat-api.mjs";
|
|
13
|
-
import {
|
|
14
|
-
import { log, info, ok, warn, error, step, pairAgents
|
|
12
|
+
import { startWebPairFlow } from "./web-pair.mjs";
|
|
13
|
+
import { log, info, ok, warn, error, step, pairAgents } from "./ui.mjs";
|
|
15
14
|
|
|
16
15
|
const PLUGIN_ID = "clawdchat-a2a";
|
|
17
|
-
const TOTAL_STEPS = 6;
|
|
18
16
|
|
|
19
17
|
export async function runInstall(flags) {
|
|
20
18
|
log("\n🦐 ClawdChat A2A — OpenClaw 通道插件安装器\n");
|
|
21
19
|
|
|
22
20
|
// ── Step 1: Check openclaw ──
|
|
23
|
-
|
|
21
|
+
const TOTAL = flags.key ? 5 : 4;
|
|
22
|
+
step(1, TOTAL, "检测 OpenClaw 环境");
|
|
24
23
|
if (!oc.isInstalled()) {
|
|
25
24
|
throw new Error("未找到 openclaw 命令。请先安装 OpenClaw: https://docs.openclaw.ai");
|
|
26
25
|
}
|
|
@@ -28,7 +27,7 @@ export async function runInstall(flags) {
|
|
|
28
27
|
ok(`OpenClaw ${version}`);
|
|
29
28
|
|
|
30
29
|
// ── Step 2: Install plugin ──
|
|
31
|
-
step(2,
|
|
30
|
+
step(2, TOTAL, "安装 ClawdChat A2A 通道插件");
|
|
32
31
|
try {
|
|
33
32
|
const cfg = oc.readConfig();
|
|
34
33
|
const already = cfg.plugins?.entries?.[PLUGIN_ID]?.enabled;
|
|
@@ -43,81 +42,54 @@ export async function runInstall(flags) {
|
|
|
43
42
|
warn(`插件安装可能需要手动配置: ${err.message}`);
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
const openclawAgents = oc.listAgents();
|
|
46
|
+
if (openclawAgents.length === 0) {
|
|
47
|
+
throw new Error("未找到 OpenClaw agents。请先创建: openclaw agents create");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let pairs;
|
|
48
51
|
|
|
49
|
-
let clawdchatAgents;
|
|
50
52
|
if (flags.key) {
|
|
53
|
+
// ── Non-interactive mode (--key) ──
|
|
54
|
+
step(3, TOTAL, "ClawdChat 认证");
|
|
51
55
|
info("使用 --key 非交互模式");
|
|
52
56
|
const agentInfo = await api.verifyApiKey(flags.key);
|
|
53
57
|
ok(`API Key 验证成功: ${agentInfo.displayName}`);
|
|
54
|
-
|
|
58
|
+
|
|
59
|
+
const clawdchatAgent = {
|
|
55
60
|
id: agentInfo.id,
|
|
56
61
|
name: agentInfo.name,
|
|
57
62
|
displayName: agentInfo.displayName,
|
|
58
63
|
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
|
-
}
|
|
64
|
+
};
|
|
68
65
|
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
}
|
|
66
|
+
step(4, TOTAL, "配置 agent 绑定");
|
|
67
|
+
ok(`发现 ${openclawAgents.length} 个 OpenClaw agent`);
|
|
83
68
|
|
|
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
69
|
const ocAgent = flags["openclaw-agent"]
|
|
105
70
|
? openclawAgents.find((a) => a.id === flags["openclaw-agent"])
|
|
106
71
|
: openclawAgents[0];
|
|
107
72
|
if (!ocAgent) throw new Error(`OpenClaw agent "${flags["openclaw-agent"]}" not found`);
|
|
108
|
-
pairs = [{ openclawAgent: ocAgent, clawdchatAgent
|
|
109
|
-
info(`配对: ${
|
|
73
|
+
pairs = [{ openclawAgent: ocAgent, clawdchatAgent }];
|
|
74
|
+
info(`配对: ${clawdchatAgent.name} ↔ ${ocAgent.id}`);
|
|
75
|
+
|
|
76
|
+
step(5, TOTAL, "创建 accounts 和 bindings");
|
|
110
77
|
} else {
|
|
111
|
-
|
|
112
|
-
|
|
78
|
+
// ── Interactive mode: Web pairing flow ──
|
|
79
|
+
step(3, TOTAL, "浏览器登录 + 配对");
|
|
80
|
+
info(`发现 ${openclawAgents.length} 个 OpenClaw agent`);
|
|
81
|
+
|
|
82
|
+
const webPairs = await startWebPairFlow({ openclawAgents });
|
|
113
83
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
84
|
+
if (!webPairs || webPairs.length === 0) {
|
|
85
|
+
warn("未选择任何配对,跳过绑定。");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
pairs = webPairs;
|
|
90
|
+
step(4, TOTAL, "创建 accounts 和 bindings");
|
|
117
91
|
}
|
|
118
92
|
|
|
119
|
-
// ── Step 5: Create accounts + bindings ──
|
|
120
|
-
step(5, TOTAL_STEPS, "创建 accounts 和 bindings");
|
|
121
93
|
for (const { openclawAgent, clawdchatAgent } of pairs) {
|
|
122
94
|
const accountId = clawdchatAgent.name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
123
95
|
info(`绑定 ${clawdchatAgent.name} → ${openclawAgent.id} (account: ${accountId})`);
|
|
@@ -131,8 +103,8 @@ export async function runInstall(flags) {
|
|
|
131
103
|
}
|
|
132
104
|
}
|
|
133
105
|
|
|
134
|
-
// ──
|
|
135
|
-
|
|
106
|
+
// ── Restart gateway ──
|
|
107
|
+
info("重启 OpenClaw Gateway...");
|
|
136
108
|
try {
|
|
137
109
|
oc.gatewayRestart();
|
|
138
110
|
ok("Gateway 已重启");
|
|
@@ -143,16 +115,9 @@ export async function runInstall(flags) {
|
|
|
143
115
|
|
|
144
116
|
log(`\n${"═".repeat(50)}`);
|
|
145
117
|
ok("安装完成!ClawdChat A2A 通道已接入 OpenClaw。\n");
|
|
146
|
-
|
|
147
|
-
|
|
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"}'`);
|
|
118
|
+
if (!flags.key) {
|
|
119
|
+
log(" 浏览器已自动跳转到 https://clawa2a.com 进行测试\n");
|
|
154
120
|
}
|
|
155
|
-
log("");
|
|
156
121
|
log(" 管理绑定:");
|
|
157
122
|
log(" clawdchat-a2a status # 查看状态");
|
|
158
123
|
log(" clawdchat-a2a bind # 新增绑定");
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML for the web-based agent pairing page.
|
|
3
|
+
* Left: OpenClaw agents | Right: ClawdChat account selector
|
|
4
|
+
*/
|
|
5
|
+
export function pairingPageHTML() {
|
|
6
|
+
return `<!DOCTYPE html>
|
|
7
|
+
<html lang="zh-CN">
|
|
8
|
+
<head>
|
|
9
|
+
<meta charset="UTF-8">
|
|
10
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
11
|
+
<title>ClawdChat A2A · Agent 配对</title>
|
|
12
|
+
<style>
|
|
13
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
14
|
+
:root{
|
|
15
|
+
--bg:#f5f5f7;--card:#fff;--border:#e5e7eb;
|
|
16
|
+
--primary:#6366f1;--primary-hover:#4f46e5;--primary-light:#eef2ff;
|
|
17
|
+
--green:#059669;--green-bg:#ecfdf5;--green-border:#a7f3d0;
|
|
18
|
+
--text:#1a1a2e;--text2:#6b7280;--text3:#9ca3af;
|
|
19
|
+
--shadow:0 1px 3px rgba(0,0,0,.06),0 1px 2px rgba(0,0,0,.04);
|
|
20
|
+
--shadow-lg:0 4px 12px rgba(0,0,0,.08);
|
|
21
|
+
--radius:10px;
|
|
22
|
+
}
|
|
23
|
+
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
|
|
24
|
+
|
|
25
|
+
.header{text-align:center;padding:40px 20px 20px}
|
|
26
|
+
.header h1{font-size:24px;font-weight:700;margin-bottom:6px}
|
|
27
|
+
.header p{color:var(--text2);font-size:14px}
|
|
28
|
+
.logo{width:48px;height:48px;background:linear-gradient(135deg,#6366f1,#8b5cf6);border-radius:12px;display:inline-flex;align-items:center;justify-content:center;margin-bottom:12px}
|
|
29
|
+
.logo svg{width:28px;height:28px;fill:#fff}
|
|
30
|
+
|
|
31
|
+
.container{max-width:860px;margin:0 auto;padding:0 20px 60px}
|
|
32
|
+
|
|
33
|
+
.pair-table{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow);overflow:hidden}
|
|
34
|
+
.pair-row{display:grid;grid-template-columns:1fr 40px 1fr;align-items:center;padding:16px 20px;border-bottom:1px solid var(--border);transition:background .15s}
|
|
35
|
+
.pair-row:last-child{border-bottom:none}
|
|
36
|
+
.pair-row:hover{background:#fafafa}
|
|
37
|
+
.pair-row.header-row{background:#f9fafb;font-weight:600;font-size:13px;color:var(--text2);text-transform:uppercase;letter-spacing:.5px}
|
|
38
|
+
.pair-row.header-row:hover{background:#f9fafb}
|
|
39
|
+
|
|
40
|
+
.agent-card{display:flex;align-items:center;gap:12px}
|
|
41
|
+
.agent-avatar{width:36px;height:36px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;color:#fff;flex-shrink:0}
|
|
42
|
+
.agent-info .agent-name{font-weight:600;font-size:14px}
|
|
43
|
+
.agent-info .agent-sub{font-size:12px;color:var(--text3)}
|
|
44
|
+
|
|
45
|
+
.arrow{text-align:center;color:var(--text3);font-size:18px}
|
|
46
|
+
|
|
47
|
+
.cc-select{width:100%;padding:8px 12px;border:1px solid var(--border);border-radius:8px;font-size:14px;color:var(--text);background:#fff;cursor:pointer;transition:border-color .15s;appearance:none;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");background-position:right 8px center;background-repeat:no-repeat;background-size:16px}
|
|
48
|
+
.cc-select:focus{border-color:var(--primary);outline:none;box-shadow:0 0 0 3px rgba(99,102,241,.1)}
|
|
49
|
+
.cc-select.bound{border-color:var(--green);background-color:var(--green-bg)}
|
|
50
|
+
|
|
51
|
+
.actions{display:flex;justify-content:center;gap:12px;margin-top:24px}
|
|
52
|
+
.btn{padding:12px 32px;border:none;border-radius:8px;font-size:15px;font-weight:600;cursor:pointer;transition:all .15s;display:inline-flex;align-items:center;gap:8px}
|
|
53
|
+
.btn-primary{background:var(--primary);color:#fff}
|
|
54
|
+
.btn-primary:hover{background:var(--primary-hover);box-shadow:var(--shadow-lg)}
|
|
55
|
+
.btn-primary:disabled{opacity:.5;cursor:not-allowed}
|
|
56
|
+
.btn-secondary{background:#fff;color:var(--text);border:1px solid var(--border)}
|
|
57
|
+
.btn-secondary:hover{background:#f9fafb}
|
|
58
|
+
|
|
59
|
+
.status-bar{text-align:center;margin-top:20px;padding:16px;border-radius:var(--radius);display:none;font-size:14px}
|
|
60
|
+
.status-bar.success{display:block;background:var(--green-bg);border:1px solid var(--green-border);color:var(--green)}
|
|
61
|
+
.status-bar.error{display:block;background:#fef2f2;border:1px solid #fecaca;color:#dc2626}
|
|
62
|
+
.status-bar.loading{display:block;background:var(--primary-light);border:1px solid #c7d2fe;color:var(--primary)}
|
|
63
|
+
|
|
64
|
+
.summary{margin-top:16px;font-size:13px;color:var(--text2);text-align:center}
|
|
65
|
+
|
|
66
|
+
.done-panel{display:none;text-align:center;padding:40px 20px}
|
|
67
|
+
.done-panel h2{font-size:22px;margin-bottom:8px;color:var(--green)}
|
|
68
|
+
.done-panel p{color:var(--text2);margin-bottom:24px;line-height:1.6}
|
|
69
|
+
.done-panel .bindings-list{text-align:left;max-width:400px;margin:0 auto 24px;background:var(--green-bg);border:1px solid var(--green-border);border-radius:var(--radius);padding:16px 20px}
|
|
70
|
+
.done-panel .bindings-list .bind-item{padding:6px 0;border-bottom:1px solid var(--green-border);font-size:14px;display:flex;justify-content:space-between}
|
|
71
|
+
.done-panel .bindings-list .bind-item:last-child{border-bottom:none}
|
|
72
|
+
|
|
73
|
+
.spinner{display:inline-block;width:16px;height:16px;border:2px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;animation:spin .6s linear infinite}
|
|
74
|
+
@keyframes spin{to{transform:rotate(360deg)}}
|
|
75
|
+
|
|
76
|
+
@media(max-width:600px){
|
|
77
|
+
.pair-row{grid-template-columns:1fr;gap:8px;padding:12px 16px}
|
|
78
|
+
.arrow{display:none}
|
|
79
|
+
.header h1{font-size:20px}
|
|
80
|
+
}
|
|
81
|
+
</style>
|
|
82
|
+
</head>
|
|
83
|
+
<body>
|
|
84
|
+
|
|
85
|
+
<div class="header">
|
|
86
|
+
<div class="logo">
|
|
87
|
+
<svg viewBox="0 0 24 24"><path d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
|
88
|
+
</div>
|
|
89
|
+
<h1>Agent 配对</h1>
|
|
90
|
+
<p>为每个 OpenClaw Agent 选择要绑定的 ClawdChat 账号</p>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div class="container">
|
|
94
|
+
<div id="pair-panel">
|
|
95
|
+
<div class="pair-table" id="pair-table">
|
|
96
|
+
<div class="pair-row header-row">
|
|
97
|
+
<div>OpenClaw Agent</div>
|
|
98
|
+
<div></div>
|
|
99
|
+
<div>ClawdChat 账号</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div class="summary" id="summary">选择要绑定的配对,然后点击确认</div>
|
|
104
|
+
|
|
105
|
+
<div class="actions">
|
|
106
|
+
<button class="btn btn-secondary" onclick="skipAll()">跳过</button>
|
|
107
|
+
<button class="btn btn-primary" id="confirmBtn" onclick="submitBindings()" disabled>
|
|
108
|
+
确认绑定
|
|
109
|
+
</button>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div class="status-bar" id="statusBar"></div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div class="done-panel" id="done-panel">
|
|
116
|
+
<h2>✅ 配对完成</h2>
|
|
117
|
+
<p>以下绑定已生效,正在重启 Gateway...</p>
|
|
118
|
+
<div class="bindings-list" id="bindings-list"></div>
|
|
119
|
+
<p id="done-msg">3 秒后跳转到 ClawA2A 进行测试...</p>
|
|
120
|
+
<div class="actions">
|
|
121
|
+
<a class="btn btn-primary" href="https://clawa2a.com" id="testLink">立即测试</a>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<script>
|
|
127
|
+
let ocAgents = [];
|
|
128
|
+
let ccAgents = [];
|
|
129
|
+
|
|
130
|
+
const COLORS = ['#6366f1','#8b5cf6','#ec4899','#f59e0b','#10b981','#3b82f6','#ef4444','#14b8a6'];
|
|
131
|
+
function agentColor(name) { let h=0; for(let i=0;i<name.length;i++) h=name.charCodeAt(i)+((h<<5)-h); return COLORS[Math.abs(h)%COLORS.length]; }
|
|
132
|
+
function initials(name) { return (name||'?').slice(0,2).toUpperCase(); }
|
|
133
|
+
|
|
134
|
+
async function init() {
|
|
135
|
+
try {
|
|
136
|
+
const resp = await fetch('/api/agents');
|
|
137
|
+
const data = await resp.json();
|
|
138
|
+
ocAgents = data.openclaw || [];
|
|
139
|
+
ccAgents = data.clawdchat || [];
|
|
140
|
+
renderTable();
|
|
141
|
+
} catch(err) {
|
|
142
|
+
showStatus('error', '加载 agents 失败: ' + err.message);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function renderTable() {
|
|
147
|
+
const table = document.getElementById('pair-table');
|
|
148
|
+
const headerRow = table.querySelector('.header-row');
|
|
149
|
+
|
|
150
|
+
table.querySelectorAll('.pair-row:not(.header-row)').forEach(r => r.remove());
|
|
151
|
+
|
|
152
|
+
for (const oc of ocAgents) {
|
|
153
|
+
const row = document.createElement('div');
|
|
154
|
+
row.className = 'pair-row';
|
|
155
|
+
row.innerHTML = \`
|
|
156
|
+
<div class="agent-card">
|
|
157
|
+
<div class="agent-avatar" style="background:\${agentColor(oc.id)}">\${initials(oc.name)}</div>
|
|
158
|
+
<div class="agent-info">
|
|
159
|
+
<div class="agent-name">\${esc(oc.name)}</div>
|
|
160
|
+
<div class="agent-sub">OpenClaw Agent</div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
<div class="arrow">→</div>
|
|
164
|
+
<div>
|
|
165
|
+
<select class="cc-select" data-oc-id="\${esc(oc.id)}" onchange="onSelectChange(this)">
|
|
166
|
+
<option value="">— 不绑定 —</option>
|
|
167
|
+
\${ccAgents.map(cc => \`<option value="\${esc(cc.id)}">\${esc(cc.displayName)} (\${esc(cc.name)})</option>\`).join('')}
|
|
168
|
+
</select>
|
|
169
|
+
</div>
|
|
170
|
+
\`;
|
|
171
|
+
table.appendChild(row);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
updateSummary();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function onSelectChange(sel) {
|
|
178
|
+
const val = sel.value;
|
|
179
|
+
if (val) {
|
|
180
|
+
sel.classList.add('bound');
|
|
181
|
+
} else {
|
|
182
|
+
sel.classList.remove('bound');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
document.querySelectorAll('.cc-select').forEach(s => {
|
|
186
|
+
if (s === sel) return;
|
|
187
|
+
Array.from(s.options).forEach(opt => {
|
|
188
|
+
opt.disabled = false;
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const usedIds = new Set();
|
|
193
|
+
document.querySelectorAll('.cc-select').forEach(s => {
|
|
194
|
+
if (s.value) usedIds.add(s.value);
|
|
195
|
+
});
|
|
196
|
+
document.querySelectorAll('.cc-select').forEach(s => {
|
|
197
|
+
Array.from(s.options).forEach(opt => {
|
|
198
|
+
if (opt.value && opt.value !== s.value && usedIds.has(opt.value)) {
|
|
199
|
+
opt.disabled = true;
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
updateSummary();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function updateSummary() {
|
|
208
|
+
const pairs = getPairs();
|
|
209
|
+
const btn = document.getElementById('confirmBtn');
|
|
210
|
+
const summary = document.getElementById('summary');
|
|
211
|
+
|
|
212
|
+
if (pairs.length === 0) {
|
|
213
|
+
btn.disabled = true;
|
|
214
|
+
summary.textContent = '选择要绑定的配对,然后点击确认';
|
|
215
|
+
} else {
|
|
216
|
+
btn.disabled = false;
|
|
217
|
+
summary.textContent = \`已选择 \${pairs.length} 个配对\`;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function getPairs() {
|
|
222
|
+
const pairs = [];
|
|
223
|
+
document.querySelectorAll('.cc-select').forEach(sel => {
|
|
224
|
+
if (sel.value) {
|
|
225
|
+
pairs.push({ openclawId: sel.dataset.ocId, clawdchatId: sel.value });
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
return pairs;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function submitBindings() {
|
|
232
|
+
const pairs = getPairs();
|
|
233
|
+
if (pairs.length === 0) return;
|
|
234
|
+
|
|
235
|
+
const btn = document.getElementById('confirmBtn');
|
|
236
|
+
btn.disabled = true;
|
|
237
|
+
btn.innerHTML = '<span class="spinner"></span> 绑定中...';
|
|
238
|
+
showStatus('loading', '正在配置绑定,请稍候...');
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const resp = await fetch('/api/bind', {
|
|
242
|
+
method: 'POST',
|
|
243
|
+
headers: { 'Content-Type': 'application/json' },
|
|
244
|
+
body: JSON.stringify({ pairs }),
|
|
245
|
+
});
|
|
246
|
+
const data = await resp.json();
|
|
247
|
+
|
|
248
|
+
if (data.ok) {
|
|
249
|
+
showDone(pairs);
|
|
250
|
+
} else {
|
|
251
|
+
showStatus('error', data.error || '绑定失败');
|
|
252
|
+
btn.disabled = false;
|
|
253
|
+
btn.textContent = '重试';
|
|
254
|
+
}
|
|
255
|
+
} catch(err) {
|
|
256
|
+
showStatus('error', '网络错误: ' + err.message);
|
|
257
|
+
btn.disabled = false;
|
|
258
|
+
btn.textContent = '重试';
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function skipAll() {
|
|
263
|
+
fetch('/api/bind', {
|
|
264
|
+
method: 'POST',
|
|
265
|
+
headers: { 'Content-Type': 'application/json' },
|
|
266
|
+
body: JSON.stringify({ pairs: [] }),
|
|
267
|
+
}).catch(() => {});
|
|
268
|
+
showStatus('success', '已跳过配对。你可以稍后用 clawdchat-a2a bind 命令绑定。');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function showDone(pairs) {
|
|
272
|
+
document.getElementById('pair-panel').style.display = 'none';
|
|
273
|
+
const donePanel = document.getElementById('done-panel');
|
|
274
|
+
donePanel.style.display = 'block';
|
|
275
|
+
|
|
276
|
+
const list = document.getElementById('bindings-list');
|
|
277
|
+
list.innerHTML = pairs.map(p => {
|
|
278
|
+
const oc = ocAgents.find(a => a.id === p.openclawId);
|
|
279
|
+
const cc = ccAgents.find(a => a.id === p.clawdchatId);
|
|
280
|
+
return \`<div class="bind-item"><span>\${esc(cc?.displayName||cc?.name||'?')}</span><span>→ \${esc(oc?.name||oc?.id||'?')}</span></div>\`;
|
|
281
|
+
}).join('');
|
|
282
|
+
|
|
283
|
+
let countdown = 3;
|
|
284
|
+
const msg = document.getElementById('done-msg');
|
|
285
|
+
const timer = setInterval(() => {
|
|
286
|
+
countdown--;
|
|
287
|
+
if (countdown <= 0) {
|
|
288
|
+
clearInterval(timer);
|
|
289
|
+
window.location.href = 'https://clawa2a.com';
|
|
290
|
+
} else {
|
|
291
|
+
msg.textContent = \`\${countdown} 秒后跳转到 ClawA2A 进行测试...\`;
|
|
292
|
+
}
|
|
293
|
+
}, 1000);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function showStatus(type, text) {
|
|
297
|
+
const bar = document.getElementById('statusBar');
|
|
298
|
+
bar.className = 'status-bar ' + type;
|
|
299
|
+
bar.textContent = text;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function esc(s) { const d = document.createElement('div'); d.textContent = s||''; return d.innerHTML; }
|
|
303
|
+
|
|
304
|
+
init();
|
|
305
|
+
</script>
|
|
306
|
+
</body>
|
|
307
|
+
</html>`;
|
|
308
|
+
}
|
package/lib/web-pair.mjs
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web-based agent pairing — local HTTP server serves a pairing UI in the browser.
|
|
3
|
+
*
|
|
4
|
+
* Flow:
|
|
5
|
+
* 1. CLI starts server → opens browser to ClawdChat auth
|
|
6
|
+
* 2. Auth callback → exchange code for JWT
|
|
7
|
+
* 3. Server loads both agent lists
|
|
8
|
+
* 4. Serve pairing page → user selects bindings
|
|
9
|
+
* 5. User submits → CLI processes bindings
|
|
10
|
+
* 6. Page shows success → redirects to clawa2a.com
|
|
11
|
+
*/
|
|
12
|
+
import http from "node:http";
|
|
13
|
+
import { URL } from "node:url";
|
|
14
|
+
import { log, info, ok, warn } from "./ui.mjs";
|
|
15
|
+
import * as api from "./clawdchat-api.mjs";
|
|
16
|
+
import { pairingPageHTML } from "./web-pair-page.mjs";
|
|
17
|
+
|
|
18
|
+
const CLAWDCHAT_BASE = "https://clawdchat.cn";
|
|
19
|
+
|
|
20
|
+
export function startWebPairFlow({ openclawAgents }) {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
let jwt = null;
|
|
23
|
+
let clawdchatAgents = null;
|
|
24
|
+
let resolved = false;
|
|
25
|
+
|
|
26
|
+
const server = http.createServer(async (req, res) => {
|
|
27
|
+
const url = new URL(req.url, `http://localhost`);
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
if (url.pathname === "/callback") {
|
|
31
|
+
await handleCallback(url, res);
|
|
32
|
+
} else if (url.pathname === "/pair") {
|
|
33
|
+
handlePairPage(res);
|
|
34
|
+
} else if (url.pathname === "/api/agents") {
|
|
35
|
+
handleAgentsApi(res);
|
|
36
|
+
} else if (req.method === "POST" && url.pathname === "/api/bind") {
|
|
37
|
+
await handleBind(req, res);
|
|
38
|
+
} else if (url.pathname === "/api/bind-progress") {
|
|
39
|
+
res.writeHead(200, corsJson());
|
|
40
|
+
res.end(JSON.stringify({ status: "ready" }));
|
|
41
|
+
} else {
|
|
42
|
+
res.writeHead(404);
|
|
43
|
+
res.end("Not Found");
|
|
44
|
+
}
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error("Server error:", err);
|
|
47
|
+
res.writeHead(500, corsJson());
|
|
48
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
server.listen(0, "127.0.0.1", () => {
|
|
53
|
+
const port = server.address().port;
|
|
54
|
+
const callbackUrl = `http://localhost:${port}/callback`;
|
|
55
|
+
const authUrl = `${CLAWDCHAT_BASE}/api/v1/auth/external/authorize?callback_url=${encodeURIComponent(callbackUrl)}`;
|
|
56
|
+
|
|
57
|
+
log(`\n🌐 正在打开浏览器进行 ClawdChat 登录...\n`);
|
|
58
|
+
log(` 如果浏览器没有自动打开,请手动访问:\n ${authUrl}\n`);
|
|
59
|
+
|
|
60
|
+
openBrowser(authUrl);
|
|
61
|
+
info("⏳ 等待浏览器操作完成...");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
server.on("error", reject);
|
|
65
|
+
|
|
66
|
+
async function handleCallback(url, res) {
|
|
67
|
+
const code = url.searchParams.get("code");
|
|
68
|
+
if (!code) {
|
|
69
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
70
|
+
res.end("<h1>缺少认证码</h1>");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
log("🔑 正在换取令牌...");
|
|
75
|
+
const tokenResp = await fetch(`${CLAWDCHAT_BASE}/api/v1/auth/external/token`, {
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: { "Content-Type": "application/json" },
|
|
78
|
+
body: JSON.stringify({ code }),
|
|
79
|
+
});
|
|
80
|
+
if (!tokenResp.ok) throw new Error(`Token exchange failed: ${tokenResp.status}`);
|
|
81
|
+
const tokenData = await tokenResp.json();
|
|
82
|
+
jwt = tokenData.jwt;
|
|
83
|
+
ok(`登录成功: ${tokenData.user.nickname}`);
|
|
84
|
+
|
|
85
|
+
info("正在获取 ClawdChat agents...");
|
|
86
|
+
const rawAgents = await api.listUserAgents(jwt);
|
|
87
|
+
clawdchatAgents = [];
|
|
88
|
+
for (const a of rawAgents) {
|
|
89
|
+
try {
|
|
90
|
+
const creds = await api.getAgentCredentials(jwt, a.id);
|
|
91
|
+
clawdchatAgents.push({
|
|
92
|
+
id: a.id,
|
|
93
|
+
name: creds.agentName,
|
|
94
|
+
displayName: a.display_name || a.name,
|
|
95
|
+
apiKey: creds.apiKey,
|
|
96
|
+
});
|
|
97
|
+
} catch {}
|
|
98
|
+
}
|
|
99
|
+
ok(`发现 ${clawdchatAgents.length} 个可用 ClawdChat agent`);
|
|
100
|
+
|
|
101
|
+
const port = server.address().port;
|
|
102
|
+
res.writeHead(302, { Location: `http://localhost:${port}/pair` });
|
|
103
|
+
res.end();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function handlePairPage(res) {
|
|
107
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
108
|
+
res.end(pairingPageHTML());
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function handleAgentsApi(res) {
|
|
112
|
+
res.writeHead(200, corsJson());
|
|
113
|
+
res.end(JSON.stringify({
|
|
114
|
+
openclaw: openclawAgents.map((a) => ({ id: a.id, name: a.name || a.id })),
|
|
115
|
+
clawdchat: (clawdchatAgents || []).map((a) => ({
|
|
116
|
+
id: a.id,
|
|
117
|
+
name: a.name,
|
|
118
|
+
displayName: a.displayName,
|
|
119
|
+
})),
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function handleBind(req, res) {
|
|
124
|
+
const body = await readBody(req);
|
|
125
|
+
const { pairs } = JSON.parse(body);
|
|
126
|
+
|
|
127
|
+
if (!Array.isArray(pairs) || pairs.length === 0) {
|
|
128
|
+
res.writeHead(400, corsJson());
|
|
129
|
+
res.end(JSON.stringify({ error: "No pairs selected" }));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const results = pairs.map((p) => {
|
|
134
|
+
const ccAgent = clawdchatAgents.find((a) => a.id === p.clawdchatId);
|
|
135
|
+
const ocAgent = openclawAgents.find((a) => a.id === p.openclawId);
|
|
136
|
+
return {
|
|
137
|
+
openclawAgent: ocAgent,
|
|
138
|
+
clawdchatAgent: ccAgent,
|
|
139
|
+
};
|
|
140
|
+
}).filter((r) => r.openclawAgent && r.clawdchatAgent);
|
|
141
|
+
|
|
142
|
+
res.writeHead(200, corsJson());
|
|
143
|
+
res.end(JSON.stringify({ ok: true, count: results.length }));
|
|
144
|
+
|
|
145
|
+
if (!resolved) {
|
|
146
|
+
resolved = true;
|
|
147
|
+
setTimeout(() => { try { server.close(); } catch {} }, 2000);
|
|
148
|
+
resolve(results);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function corsJson() {
|
|
155
|
+
return { "Content-Type": "application/json; charset=utf-8", "Access-Control-Allow-Origin": "*" };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function readBody(req) {
|
|
159
|
+
return new Promise((resolve) => {
|
|
160
|
+
let data = "";
|
|
161
|
+
req.on("data", (chunk) => { data += chunk; });
|
|
162
|
+
req.on("end", () => resolve(data));
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function openBrowser(url) {
|
|
167
|
+
const { exec } = await import("node:child_process");
|
|
168
|
+
const cmd =
|
|
169
|
+
process.platform === "darwin" ? `open "${url}"` :
|
|
170
|
+
process.platform === "win32" ? `start "${url}"` :
|
|
171
|
+
`xdg-open "${url}"`;
|
|
172
|
+
return new Promise((resolve) => {
|
|
173
|
+
exec(cmd, () => resolve());
|
|
174
|
+
});
|
|
175
|
+
}
|