@agentmarketpro/connector 2.0.3 → 2.0.4
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 +216 -1
- package/dist/cli.js +84 -0
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +968 -3
- package/dist/index.js.map +1 -1
- package/package.json +10 -1
package/dist/index.js
CHANGED
|
@@ -44,7 +44,12 @@ const https = __importStar(require("https"));
|
|
|
44
44
|
const fs = __importStar(require("fs"));
|
|
45
45
|
const path = __importStar(require("path"));
|
|
46
46
|
const os = __importStar(require("os"));
|
|
47
|
+
const child_process_1 = require("child_process");
|
|
48
|
+
const util_1 = require("util");
|
|
47
49
|
const package_json_1 = require("../package.json");
|
|
50
|
+
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
|
51
|
+
// 临时关闭连接器的技能自动安装功能,保留完整安装链路代码,后续恢复时改回 false 即可。
|
|
52
|
+
const DISABLE_SKILL_AUTO_INSTALL_TEMPORARILY = true;
|
|
48
53
|
// ============================================================
|
|
49
54
|
// 工具函数
|
|
50
55
|
// ============================================================
|
|
@@ -136,6 +141,173 @@ function parseApiUrl(url) {
|
|
|
136
141
|
path: apiPath,
|
|
137
142
|
};
|
|
138
143
|
}
|
|
144
|
+
function normalizeSkillKey(skill) {
|
|
145
|
+
return skill.trim().toLowerCase();
|
|
146
|
+
}
|
|
147
|
+
function uniqueSkillKeys(skills) {
|
|
148
|
+
const values = Array.isArray(skills)
|
|
149
|
+
? skills
|
|
150
|
+
: typeof skills === 'string'
|
|
151
|
+
? skills.split(',')
|
|
152
|
+
: [];
|
|
153
|
+
const unique = new Set();
|
|
154
|
+
for (const value of values) {
|
|
155
|
+
if (typeof value !== 'string')
|
|
156
|
+
continue;
|
|
157
|
+
const normalized = normalizeSkillKey(value);
|
|
158
|
+
if (normalized)
|
|
159
|
+
unique.add(normalized);
|
|
160
|
+
}
|
|
161
|
+
return Array.from(unique);
|
|
162
|
+
}
|
|
163
|
+
function collectInstalledSkillKeys(config) {
|
|
164
|
+
const entries = config?.skills?.entries || {};
|
|
165
|
+
const installed = new Set();
|
|
166
|
+
for (const [entryKey, value] of Object.entries(entries)) {
|
|
167
|
+
if (!value || typeof value !== 'object')
|
|
168
|
+
continue;
|
|
169
|
+
const enabled = value.enabled;
|
|
170
|
+
if (enabled !== true)
|
|
171
|
+
continue;
|
|
172
|
+
const normalized = normalizeSkillKey(entryKey.replace('@openclaw/', ''));
|
|
173
|
+
if (normalized) {
|
|
174
|
+
installed.add(normalized);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return Array.from(installed);
|
|
178
|
+
}
|
|
179
|
+
function uniquePackageNames(packages) {
|
|
180
|
+
const unique = new Set();
|
|
181
|
+
for (const packageName of packages) {
|
|
182
|
+
if (typeof packageName !== 'string')
|
|
183
|
+
continue;
|
|
184
|
+
const normalized = packageName.trim();
|
|
185
|
+
if (!normalized)
|
|
186
|
+
continue;
|
|
187
|
+
unique.add(normalized);
|
|
188
|
+
}
|
|
189
|
+
return Array.from(unique);
|
|
190
|
+
}
|
|
191
|
+
function getSkillEntryCandidates(skillKey, packageName) {
|
|
192
|
+
const candidates = new Set();
|
|
193
|
+
const normalizedSkillKey = normalizeSkillKey(skillKey);
|
|
194
|
+
if (normalizedSkillKey) {
|
|
195
|
+
candidates.add(normalizedSkillKey);
|
|
196
|
+
}
|
|
197
|
+
if (typeof packageName === 'string' && packageName.trim()) {
|
|
198
|
+
const trimmedPackageName = packageName.trim();
|
|
199
|
+
const normalizedPackageName = normalizeSkillKey(trimmedPackageName);
|
|
200
|
+
const packageSegments = trimmedPackageName.split('/').filter(Boolean);
|
|
201
|
+
const packageBaseName = packageSegments.length > 0
|
|
202
|
+
? normalizeSkillKey(packageSegments[packageSegments.length - 1])
|
|
203
|
+
: '';
|
|
204
|
+
if (normalizedPackageName) {
|
|
205
|
+
candidates.add(normalizedPackageName);
|
|
206
|
+
}
|
|
207
|
+
if (packageBaseName) {
|
|
208
|
+
candidates.add(packageBaseName);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return Array.from(candidates);
|
|
212
|
+
}
|
|
213
|
+
function getSkillSlug(skill) {
|
|
214
|
+
if (typeof skill.sourceUrl === 'string' && skill.sourceUrl.trim()) {
|
|
215
|
+
try {
|
|
216
|
+
const url = new URL(skill.sourceUrl);
|
|
217
|
+
const segments = url.pathname.split('/').filter(Boolean);
|
|
218
|
+
if (segments.length >= 1) {
|
|
219
|
+
return segments[segments.length - 1];
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// ignore invalid url, fall through
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return skill.key || null;
|
|
227
|
+
}
|
|
228
|
+
function getCommandNameFromPackageName(packageName, skillKey) {
|
|
229
|
+
const normalizedPackageName = typeof packageName === 'string' ? packageName.trim() : '';
|
|
230
|
+
if (normalizedPackageName) {
|
|
231
|
+
const parts = normalizedPackageName.split('/').filter(Boolean);
|
|
232
|
+
if (parts.length > 0) {
|
|
233
|
+
return parts[parts.length - 1];
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return skillKey;
|
|
237
|
+
}
|
|
238
|
+
function resolveOpenClawWorkspacePath(config) {
|
|
239
|
+
const workspacePathCandidates = [
|
|
240
|
+
config?.workspace,
|
|
241
|
+
config?.workspaceDir,
|
|
242
|
+
config?.workspace_path,
|
|
243
|
+
config?.workspacePath,
|
|
244
|
+
config?.paths?.workspace,
|
|
245
|
+
config?.paths?.workspaceDir,
|
|
246
|
+
config?.paths?.workspace_path,
|
|
247
|
+
config?.paths?.workspacePath,
|
|
248
|
+
process.env.OPENCLAW_WORKSPACE,
|
|
249
|
+
];
|
|
250
|
+
for (const candidate of workspacePathCandidates) {
|
|
251
|
+
if (typeof candidate === 'string' && candidate.trim()) {
|
|
252
|
+
return path.resolve(candidate.trim());
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return path.join(os.homedir(), '.openclaw', 'workspace');
|
|
256
|
+
}
|
|
257
|
+
function findInstalledSkillEntryKey(entries, skillKey, packageName) {
|
|
258
|
+
const candidates = new Set(getSkillEntryCandidates(skillKey, packageName));
|
|
259
|
+
for (const entryKey of Object.keys(entries)) {
|
|
260
|
+
const normalizedEntryKey = normalizeSkillKey(entryKey);
|
|
261
|
+
if (!candidates.has(normalizedEntryKey)) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const entry = entries[entryKey];
|
|
265
|
+
if (entry?.enabled === true) {
|
|
266
|
+
return entryKey;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
async function commandExists(command, env) {
|
|
272
|
+
const probeCommand = process.platform === 'win32'
|
|
273
|
+
? `where ${command}`
|
|
274
|
+
: `command -v ${command}`;
|
|
275
|
+
try {
|
|
276
|
+
await execAsync(probeCommand, {
|
|
277
|
+
timeout: 30000,
|
|
278
|
+
windowsHide: true,
|
|
279
|
+
shell: process.platform === 'win32' ? 'cmd.exe' : '/bin/sh',
|
|
280
|
+
env,
|
|
281
|
+
});
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
async function checkRegistryPackage(registry, packageName) {
|
|
289
|
+
const startedAt = Date.now();
|
|
290
|
+
try {
|
|
291
|
+
const url = `${registry.replace(/\/$/, '')}/${packageName.replace('/', '%2F')}`;
|
|
292
|
+
const response = await fetch(url, { method: 'GET' });
|
|
293
|
+
return {
|
|
294
|
+
packageName,
|
|
295
|
+
ok: response.ok,
|
|
296
|
+
statusCode: response.status,
|
|
297
|
+
latencyMs: Date.now() - startedAt,
|
|
298
|
+
error: response.ok ? null : `HTTP ${response.status}`,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
return {
|
|
303
|
+
packageName,
|
|
304
|
+
ok: false,
|
|
305
|
+
statusCode: null,
|
|
306
|
+
latencyMs: Date.now() - startedAt,
|
|
307
|
+
error: error instanceof Error ? error.message : String(error),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
139
311
|
// ============================================================
|
|
140
312
|
// Connector 核心类
|
|
141
313
|
// ============================================================
|
|
@@ -150,6 +322,13 @@ class Connector {
|
|
|
150
322
|
this.parsedApiUrl = null;
|
|
151
323
|
// OpenClaw 配置
|
|
152
324
|
this.openclawConfig = null;
|
|
325
|
+
this.requiredOpenClawSkills = [];
|
|
326
|
+
this.requiredOpenClawPackages = [];
|
|
327
|
+
this.requiredOpenClawSkillCatalog = [];
|
|
328
|
+
this.agentName = null;
|
|
329
|
+
this.agentDescription = null;
|
|
330
|
+
this.systemPrompt = null;
|
|
331
|
+
this.globalNpmRoot = null;
|
|
153
332
|
this.config = config;
|
|
154
333
|
this.isCustomMode = !!config.apiUrl;
|
|
155
334
|
this.model = config.model || 'openclaw:main';
|
|
@@ -230,14 +409,559 @@ class Connector {
|
|
|
230
409
|
' 3. Or set environment variable: OPENCLAW_TOKEN=<YOUR_TOKEN>\n' +
|
|
231
410
|
' 4. Or use custom backend: --api-url <URL> --model <MODEL>');
|
|
232
411
|
}
|
|
412
|
+
await this.loadRequiredOpenClawSkills();
|
|
413
|
+
const existingEntries = this.openclawConfig?.skills?.entries || {};
|
|
414
|
+
const installedSkillKeys = collectInstalledSkillKeys(this.openclawConfig);
|
|
415
|
+
const unresolvedRequiredSkills = this.requiredOpenClawSkills.filter((skillKey) => {
|
|
416
|
+
const catalogItem = this.requiredOpenClawSkillCatalog.find((item) => item.key === skillKey);
|
|
417
|
+
if (catalogItem && (catalogItem.packageName || catalogItem.nativePackageName || catalogItem.sourceUrl)) {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
return !findInstalledSkillEntryKey(existingEntries, skillKey);
|
|
421
|
+
});
|
|
422
|
+
const blockedAutoInstallSkills = this.requiredOpenClawSkillCatalog
|
|
423
|
+
.filter((item) => !item.autoInstallAllowed)
|
|
424
|
+
.filter((item) => !findInstalledSkillEntryKey(existingEntries, item.key, item.nativePackageName || item.packageName))
|
|
425
|
+
.map((item) => item.key);
|
|
426
|
+
if (unresolvedRequiredSkills.length > 0 || blockedAutoInstallSkills.length > 0) {
|
|
427
|
+
const details = [
|
|
428
|
+
unresolvedRequiredSkills.length > 0
|
|
429
|
+
? `catalog package missing: ${unresolvedRequiredSkills.join(', ')}`
|
|
430
|
+
: null,
|
|
431
|
+
blockedAutoInstallSkills.length > 0
|
|
432
|
+
? `auto-install disabled by platform: ${blockedAutoInstallSkills.join(', ')}`
|
|
433
|
+
: null,
|
|
434
|
+
].filter((value) => Boolean(value));
|
|
435
|
+
const message = `Required OpenClaw Skills cannot be auto-installed from platform catalog. ${details.join('; ')}. Update the backend skill catalog or install and enable them locally before reconnecting.`;
|
|
436
|
+
await this.reportSkillSyncStatus({
|
|
437
|
+
status: 'FAILED',
|
|
438
|
+
requiredSkills: this.requiredOpenClawSkills,
|
|
439
|
+
installedPackages: this.requiredOpenClawPackages,
|
|
440
|
+
missingPackages: this.requiredOpenClawPackages,
|
|
441
|
+
restartRequired: false,
|
|
442
|
+
message,
|
|
443
|
+
securityScan: {
|
|
444
|
+
installedSkills: installedSkillKeys,
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
throw new Error(message);
|
|
448
|
+
}
|
|
233
449
|
// 自动启用 HTTP API
|
|
234
450
|
if (this.openclawConfig) {
|
|
235
451
|
const configChanged = ensureHttpApiEnabled(this.openclawConfig);
|
|
236
452
|
if (configChanged) {
|
|
453
|
+
await this.reportSkillSyncStatus({
|
|
454
|
+
status: 'RESTART_REQUIRED',
|
|
455
|
+
requiredSkills: this.requiredOpenClawSkills,
|
|
456
|
+
installedPackages: this.requiredOpenClawPackages,
|
|
457
|
+
missingPackages: [],
|
|
458
|
+
restartRequired: true,
|
|
459
|
+
message: 'OpenClaw HTTP API was enabled automatically. Restart OpenClaw before reconnecting.',
|
|
460
|
+
securityScan: {
|
|
461
|
+
installedSkills: collectInstalledSkillKeys(this.openclawConfig),
|
|
462
|
+
},
|
|
463
|
+
});
|
|
237
464
|
throw new Error('Configuration updated. Please restart OpenClaw, then run this connector again.\n' +
|
|
238
465
|
'Run: openclaw restart');
|
|
239
466
|
}
|
|
240
467
|
}
|
|
468
|
+
const skillSyncChanged = await this.syncRequiredOpenClawSkills();
|
|
469
|
+
if (skillSyncChanged) {
|
|
470
|
+
throw new Error('Required OpenClaw Skills have been installed or enabled automatically. Please restart OpenClaw, then run this connector again.\n' +
|
|
471
|
+
'Run: openclaw restart');
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
async loadRequiredOpenClawSkills() {
|
|
475
|
+
const baseUrl = this.config.platformApiUrl || process.env.PLATFORM_API_URL || 'http://localhost:3000';
|
|
476
|
+
try {
|
|
477
|
+
const response = await fetch(`${baseUrl.replace(/\/$/, '')}/api/connector/profile`, {
|
|
478
|
+
method: 'POST',
|
|
479
|
+
headers: { 'Content-Type': 'application/json' },
|
|
480
|
+
body: JSON.stringify({
|
|
481
|
+
connectionId: this.config.connectionId,
|
|
482
|
+
token: this.config.token,
|
|
483
|
+
}),
|
|
484
|
+
});
|
|
485
|
+
if (!response.ok) {
|
|
486
|
+
const errorText = await response.text().catch(() => 'unknown error');
|
|
487
|
+
this.log('info', `⚠️ Failed to load required OpenClaw Skills: HTTP ${response.status} ${errorText}`);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const payload = await response.json();
|
|
491
|
+
this.agentName = typeof payload.agentName === 'string' && payload.agentName.trim()
|
|
492
|
+
? payload.agentName.trim()
|
|
493
|
+
: null;
|
|
494
|
+
this.agentDescription = typeof payload.agentDescription === 'string' && payload.agentDescription.trim()
|
|
495
|
+
? payload.agentDescription.trim()
|
|
496
|
+
: null;
|
|
497
|
+
this.systemPrompt = typeof payload.systemPrompt === 'string' && payload.systemPrompt.trim()
|
|
498
|
+
? payload.systemPrompt.trim()
|
|
499
|
+
: null;
|
|
500
|
+
this.requiredOpenClawSkills = uniqueSkillKeys(payload.openclawSkills || []);
|
|
501
|
+
this.requiredOpenClawSkillCatalog = Array.isArray(payload.openclawSkillCatalog)
|
|
502
|
+
? payload.openclawSkillCatalog
|
|
503
|
+
.filter((item) => Boolean(item) && typeof item.key === 'string')
|
|
504
|
+
.map((item) => ({
|
|
505
|
+
key: normalizeSkillKey(item.key),
|
|
506
|
+
packageName: typeof item.packageName === 'string' && item.packageName.trim() ? item.packageName.trim() : null,
|
|
507
|
+
nativePackageName: typeof item.nativePackageName === 'string' && item.nativePackageName.trim() ? item.nativePackageName.trim() : null,
|
|
508
|
+
autoInstallAllowed: item.autoInstallAllowed === true,
|
|
509
|
+
sourceUrl: typeof item.sourceUrl === 'string' && item.sourceUrl.trim() ? item.sourceUrl.trim() : null,
|
|
510
|
+
}))
|
|
511
|
+
: this.requiredOpenClawSkills.map((skillKey) => ({
|
|
512
|
+
key: skillKey,
|
|
513
|
+
packageName: null,
|
|
514
|
+
nativePackageName: null,
|
|
515
|
+
autoInstallAllowed: false,
|
|
516
|
+
sourceUrl: null,
|
|
517
|
+
}));
|
|
518
|
+
const catalogByKey = new Map(this.requiredOpenClawSkillCatalog.map((item) => [item.key, item]));
|
|
519
|
+
this.requiredOpenClawSkillCatalog = this.requiredOpenClawSkills.map((skillKey) => {
|
|
520
|
+
const existing = catalogByKey.get(skillKey);
|
|
521
|
+
return existing || {
|
|
522
|
+
key: skillKey,
|
|
523
|
+
packageName: null,
|
|
524
|
+
nativePackageName: null,
|
|
525
|
+
autoInstallAllowed: false,
|
|
526
|
+
sourceUrl: null,
|
|
527
|
+
};
|
|
528
|
+
});
|
|
529
|
+
this.requiredOpenClawPackages = uniquePackageNames(this.requiredOpenClawSkillCatalog.map((item) => item.packageName));
|
|
530
|
+
if (this.requiredOpenClawSkills.length > 0) {
|
|
531
|
+
this.log('info', `🧩 Required OpenClaw Skills: ${this.requiredOpenClawSkills.join(', ')}`);
|
|
532
|
+
if (this.config.skillInstallRegistry) {
|
|
533
|
+
this.log('info', `🌏 Skill install registry: ${this.config.skillInstallRegistry}`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
if (this.agentName) {
|
|
537
|
+
this.log('info', `🤖 Loaded agent persona: ${this.agentName}`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
catch (error) {
|
|
541
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
542
|
+
this.log('info', `⚠️ Failed to load required OpenClaw Skills: ${msg}`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
async reportSkillSyncStatus(payload) {
|
|
546
|
+
const baseUrl = this.config.platformApiUrl || process.env.PLATFORM_API_URL || 'http://localhost:3000';
|
|
547
|
+
try {
|
|
548
|
+
await fetch(`${baseUrl.replace(/\/$/, '')}/api/connector/sync-status`, {
|
|
549
|
+
method: 'POST',
|
|
550
|
+
headers: { 'Content-Type': 'application/json' },
|
|
551
|
+
body: JSON.stringify({
|
|
552
|
+
connectionId: this.config.connectionId,
|
|
553
|
+
token: this.config.token,
|
|
554
|
+
...payload,
|
|
555
|
+
lastSyncedAt: payload.lastSyncedAt || new Date().toISOString(),
|
|
556
|
+
}),
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
catch (error) {
|
|
560
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
561
|
+
this.log('info', `⚠️ Failed to report skill sync status: ${msg}`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
async runSkillInstallPreflight(missingPackages) {
|
|
565
|
+
return {
|
|
566
|
+
preflight: {
|
|
567
|
+
checkedAt: new Date().toISOString(),
|
|
568
|
+
registry: this.config.skillInstallRegistry || process.env.OPENCLAW_SKILL_INSTALL_REGISTRY || 'https://registry.npmjs.org',
|
|
569
|
+
status: 'SKIPPED',
|
|
570
|
+
missingPackages,
|
|
571
|
+
reachablePackages: [],
|
|
572
|
+
unreachablePackages: [],
|
|
573
|
+
message: 'Skill installation uses managed fallback methods, registry preflight skipped.',
|
|
574
|
+
},
|
|
575
|
+
registryHealth: {
|
|
576
|
+
registry: this.config.skillInstallRegistry || process.env.OPENCLAW_SKILL_INSTALL_REGISTRY || 'https://registry.npmjs.org',
|
|
577
|
+
checkedAt: new Date().toISOString(),
|
|
578
|
+
status: 'UNKNOWN',
|
|
579
|
+
latencyMs: null,
|
|
580
|
+
packageResults: [],
|
|
581
|
+
},
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
getCommandEnv() {
|
|
585
|
+
return {
|
|
586
|
+
...process.env,
|
|
587
|
+
...(this.config.skillInstallRegistry
|
|
588
|
+
? {
|
|
589
|
+
npm_config_registry: this.config.skillInstallRegistry,
|
|
590
|
+
OPENCLAW_SKILL_INSTALL_REGISTRY: this.config.skillInstallRegistry,
|
|
591
|
+
}
|
|
592
|
+
: {}),
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
async getGlobalNpmRootPath() {
|
|
596
|
+
if (this.globalNpmRoot) {
|
|
597
|
+
return this.globalNpmRoot;
|
|
598
|
+
}
|
|
599
|
+
const env = this.getCommandEnv();
|
|
600
|
+
const { stdout } = await execAsync('npm root -g', {
|
|
601
|
+
timeout: 30000,
|
|
602
|
+
windowsHide: true,
|
|
603
|
+
shell: process.platform === 'win32' ? 'cmd.exe' : '/bin/sh',
|
|
604
|
+
env,
|
|
605
|
+
});
|
|
606
|
+
this.globalNpmRoot = stdout.trim();
|
|
607
|
+
return this.globalNpmRoot;
|
|
608
|
+
}
|
|
609
|
+
getWorkspaceSkillsDir() {
|
|
610
|
+
return path.join(resolveOpenClawWorkspacePath(this.openclawConfig), 'skills');
|
|
611
|
+
}
|
|
612
|
+
async ensureSkillHubInstalled() {
|
|
613
|
+
const env = this.getCommandEnv();
|
|
614
|
+
const cliScriptPath = path.join(os.homedir(), '.skillhub', 'skills_store_cli.py');
|
|
615
|
+
if (fs.existsSync(cliScriptPath)) {
|
|
616
|
+
this.log('info', `✅ SkillHub CLI is already installed: ${cliScriptPath}`);
|
|
617
|
+
return cliScriptPath;
|
|
618
|
+
}
|
|
619
|
+
this.log('info', '📦 SkillHub CLI not found, bootstrapping via official installer (--cli-only)');
|
|
620
|
+
const command = process.platform === 'win32'
|
|
621
|
+
? 'bash -lc "curl -fsSL https://skillhub-1388575217.cos.ap-guangzhou.myqcloud.com/install/install.sh | bash -s -- --cli-only"'
|
|
622
|
+
: 'curl -fsSL https://skillhub-1388575217.cos.ap-guangzhou.myqcloud.com/install/install.sh | bash -s -- --cli-only';
|
|
623
|
+
await execAsync(command, {
|
|
624
|
+
timeout: 180000,
|
|
625
|
+
windowsHide: true,
|
|
626
|
+
shell: process.platform === 'win32' ? 'cmd.exe' : '/bin/sh',
|
|
627
|
+
env,
|
|
628
|
+
});
|
|
629
|
+
if (!fs.existsSync(cliScriptPath)) {
|
|
630
|
+
throw new Error(`SkillHub CLI installer completed but CLI script is missing: ${cliScriptPath}`);
|
|
631
|
+
}
|
|
632
|
+
return cliScriptPath;
|
|
633
|
+
}
|
|
634
|
+
async runSkillHubInstall(skill) {
|
|
635
|
+
const env = this.getCommandEnv();
|
|
636
|
+
const cliScriptPath = await this.ensureSkillHubInstalled();
|
|
637
|
+
const skillSlug = getSkillSlug(skill);
|
|
638
|
+
if (!skillSlug) {
|
|
639
|
+
throw new Error(`Unable to resolve SkillHub slug for skill: ${skill.key}`);
|
|
640
|
+
}
|
|
641
|
+
const workspaceSkillsDir = this.getWorkspaceSkillsDir();
|
|
642
|
+
const targetSkillPath = path.join(workspaceSkillsDir, skillSlug, 'SKILL.md');
|
|
643
|
+
// 先检测是否已在 workspace 中安装
|
|
644
|
+
if (fs.existsSync(targetSkillPath)) {
|
|
645
|
+
this.log('info', `✅ Skill ${skill.key} already installed in workspace: ${skillSlug}`);
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
648
|
+
fs.mkdirSync(workspaceSkillsDir, { recursive: true });
|
|
649
|
+
const command = `python "${cliScriptPath}" install ${skillSlug} --dir "${workspaceSkillsDir}"`;
|
|
650
|
+
this.log('info', `📦 Installing OpenClaw Skill via SkillHub: ${skill.key} -> ${skillSlug}`);
|
|
651
|
+
await execAsync(command, {
|
|
652
|
+
timeout: 180000,
|
|
653
|
+
windowsHide: true,
|
|
654
|
+
shell: process.platform === 'win32' ? 'cmd.exe' : '/bin/sh',
|
|
655
|
+
env,
|
|
656
|
+
});
|
|
657
|
+
return fs.existsSync(targetSkillPath);
|
|
658
|
+
}
|
|
659
|
+
async runNpmGlobalInstall(skill) {
|
|
660
|
+
const installPackageName = skill.packageName;
|
|
661
|
+
if (!installPackageName) {
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
const env = this.getCommandEnv();
|
|
665
|
+
const commandName = getCommandNameFromPackageName(installPackageName, skill.key);
|
|
666
|
+
// 先检测是否已安装:命令存在且 workspace 中已有 SKILL.md
|
|
667
|
+
const skillSlug = getSkillSlug(skill) || skill.key;
|
|
668
|
+
const workspaceSkillPath = path.join(this.getWorkspaceSkillsDir(), skillSlug, 'SKILL.md');
|
|
669
|
+
const commandExistsAlready = await commandExists(commandName, env);
|
|
670
|
+
const skillInWorkspace = fs.existsSync(workspaceSkillPath);
|
|
671
|
+
if (commandExistsAlready && skillInWorkspace) {
|
|
672
|
+
this.log('info', `✅ Skill ${skill.key} already installed (command: ${commandName}, workspace: ${skillSlug})`);
|
|
673
|
+
return true;
|
|
674
|
+
}
|
|
675
|
+
this.log('info', `📦 Installing OpenClaw Skill via npm -g: ${installPackageName}`);
|
|
676
|
+
await execAsync(`npm install -g ${installPackageName}`, {
|
|
677
|
+
timeout: 300000,
|
|
678
|
+
windowsHide: true,
|
|
679
|
+
shell: process.platform === 'win32' ? 'cmd.exe' : '/bin/sh',
|
|
680
|
+
env,
|
|
681
|
+
});
|
|
682
|
+
if (await commandExists(commandName, env)) {
|
|
683
|
+
try {
|
|
684
|
+
await execAsync(`${commandName} install`, {
|
|
685
|
+
timeout: 300000,
|
|
686
|
+
windowsHide: true,
|
|
687
|
+
shell: process.platform === 'win32' ? 'cmd.exe' : '/bin/sh',
|
|
688
|
+
env,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
catch (error) {
|
|
692
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
693
|
+
this.log('debug', `Optional post-install failed for ${commandName} install: ${message}`);
|
|
694
|
+
}
|
|
695
|
+
try {
|
|
696
|
+
await execAsync(`${commandName} install --with-deps`, {
|
|
697
|
+
timeout: 300000,
|
|
698
|
+
windowsHide: true,
|
|
699
|
+
shell: process.platform === 'win32' ? 'cmd.exe' : '/bin/sh',
|
|
700
|
+
env,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
catch (error) {
|
|
704
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
705
|
+
this.log('debug', `Optional post-install failed for ${commandName} install --with-deps: ${message}`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
const syncedToWorkspace = await this.syncGlobalPackageSkillToWorkspace(skill);
|
|
709
|
+
return syncedToWorkspace || await commandExists(commandName, env);
|
|
710
|
+
}
|
|
711
|
+
async syncGlobalPackageSkillToWorkspace(skill) {
|
|
712
|
+
const installPackageName = skill.packageName;
|
|
713
|
+
if (!installPackageName) {
|
|
714
|
+
return false;
|
|
715
|
+
}
|
|
716
|
+
const skillSlug = getSkillSlug(skill);
|
|
717
|
+
if (!skillSlug) {
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
// 先检测是否已在 workspace 中存在
|
|
721
|
+
const workspaceTarget = path.join(this.getWorkspaceSkillsDir(), skillSlug);
|
|
722
|
+
const targetSkillMd = path.join(workspaceTarget, 'SKILL.md');
|
|
723
|
+
if (fs.existsSync(targetSkillMd)) {
|
|
724
|
+
this.log('info', `✅ Skill ${skill.key} already synced to workspace: ${skillSlug}`);
|
|
725
|
+
return true;
|
|
726
|
+
}
|
|
727
|
+
const globalNpmRoot = await this.getGlobalNpmRootPath();
|
|
728
|
+
const packageRoot = path.join(globalNpmRoot, installPackageName);
|
|
729
|
+
const candidateDirs = [
|
|
730
|
+
path.join(packageRoot, 'skills', skillSlug),
|
|
731
|
+
path.join(packageRoot, 'skills', skill.key),
|
|
732
|
+
path.join(packageRoot, 'skills', getCommandNameFromPackageName(installPackageName, skill.key)),
|
|
733
|
+
];
|
|
734
|
+
const sourceDir = candidateDirs.find((candidate) => fs.existsSync(path.join(candidate, 'SKILL.md')));
|
|
735
|
+
if (!sourceDir) {
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
738
|
+
fs.mkdirSync(path.dirname(workspaceTarget), { recursive: true });
|
|
739
|
+
fs.rmSync(workspaceTarget, { recursive: true, force: true });
|
|
740
|
+
fs.cpSync(sourceDir, workspaceTarget, { recursive: true });
|
|
741
|
+
this.log('info', `📂 Synced packaged skill into OpenClaw workspace: ${workspaceTarget}`);
|
|
742
|
+
return true;
|
|
743
|
+
}
|
|
744
|
+
async runOpenClawNativeInstall(skill) {
|
|
745
|
+
const env = this.getCommandEnv();
|
|
746
|
+
const nativePackageName = skill.nativePackageName || skill.packageName;
|
|
747
|
+
if (!nativePackageName) {
|
|
748
|
+
return false;
|
|
749
|
+
}
|
|
750
|
+
if (!await commandExists('openclaw', env)) {
|
|
751
|
+
return false;
|
|
752
|
+
}
|
|
753
|
+
this.log('info', `📦 Installing OpenClaw Skill via native installer: ${nativePackageName}`);
|
|
754
|
+
await execAsync(`openclaw skills install ${nativePackageName}`, {
|
|
755
|
+
timeout: 180000,
|
|
756
|
+
windowsHide: true,
|
|
757
|
+
shell: process.platform === 'win32' ? 'cmd.exe' : '/bin/sh',
|
|
758
|
+
env,
|
|
759
|
+
});
|
|
760
|
+
return true;
|
|
761
|
+
}
|
|
762
|
+
async syncRequiredOpenClawSkills() {
|
|
763
|
+
if (this.requiredOpenClawSkillCatalog.length === 0 || !this.openclawConfig?._path) {
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
const existingEntries = this.openclawConfig.skills?.entries || {};
|
|
767
|
+
// 检测哪些技能确实需要安装(既不在 entries 中,也不在 workspace 中)
|
|
768
|
+
const missingCatalogItems = this.requiredOpenClawSkillCatalog.filter((item) => {
|
|
769
|
+
if (!item.autoInstallAllowed) {
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
if (!item.packageName && !item.nativePackageName && !item.sourceUrl) {
|
|
773
|
+
return false;
|
|
774
|
+
}
|
|
775
|
+
// 检查是否在 OpenClaw 配置的 entries 中
|
|
776
|
+
const entryKey = findInstalledSkillEntryKey(existingEntries, item.key, item.nativePackageName || item.packageName);
|
|
777
|
+
if (entryKey) {
|
|
778
|
+
return false;
|
|
779
|
+
}
|
|
780
|
+
// 检查是否已在 workspace 中安装
|
|
781
|
+
const skillSlug = getSkillSlug(item) || item.key;
|
|
782
|
+
const workspaceSkillPath = path.join(this.getWorkspaceSkillsDir(), skillSlug, 'SKILL.md');
|
|
783
|
+
if (fs.existsSync(workspaceSkillPath)) {
|
|
784
|
+
this.log('info', `✅ Skill ${item.key} already exists in workspace, skipping installation`);
|
|
785
|
+
return false;
|
|
786
|
+
}
|
|
787
|
+
return true;
|
|
788
|
+
});
|
|
789
|
+
const missingPackages = uniquePackageNames(missingCatalogItems.map((item) => item.packageName));
|
|
790
|
+
let changed = false;
|
|
791
|
+
let installedPackages = [];
|
|
792
|
+
const preflightResult = await this.runSkillInstallPreflight(missingPackages);
|
|
793
|
+
// 这里先短路自动安装,避免连接器在启动时主动改动本地技能环境。
|
|
794
|
+
// 后续如果需要恢复原行为,只需将 DISABLE_SKILL_AUTO_INSTALL_TEMPORARILY 改为 false,
|
|
795
|
+
// 下面完整的 npm / SkillHub / 本地兜底 / OpenClaw Native 安装逻辑都还保留着。
|
|
796
|
+
if (DISABLE_SKILL_AUTO_INSTALL_TEMPORARILY && missingCatalogItems.length > 0) {
|
|
797
|
+
const disabledSkillKeys = missingCatalogItems.map((item) => item.key);
|
|
798
|
+
const message = `Connector skill auto-install is temporarily disabled. Please install and enable required skills manually before reconnecting: ${disabledSkillKeys.join(', ')}`;
|
|
799
|
+
await this.reportSkillSyncStatus({
|
|
800
|
+
status: 'FAILED',
|
|
801
|
+
requiredSkills: this.requiredOpenClawSkills,
|
|
802
|
+
installedPackages,
|
|
803
|
+
missingPackages,
|
|
804
|
+
restartRequired: false,
|
|
805
|
+
message,
|
|
806
|
+
securityScan: {
|
|
807
|
+
installedSkills: collectInstalledSkillKeys(this.openclawConfig),
|
|
808
|
+
},
|
|
809
|
+
skillPreflight: preflightResult.preflight,
|
|
810
|
+
registryHealth: preflightResult.registryHealth,
|
|
811
|
+
});
|
|
812
|
+
throw new Error(message);
|
|
813
|
+
}
|
|
814
|
+
if (missingPackages.length > 0) {
|
|
815
|
+
try {
|
|
816
|
+
for (const skill of missingCatalogItems) {
|
|
817
|
+
let installed = false;
|
|
818
|
+
const methodErrors = [];
|
|
819
|
+
try {
|
|
820
|
+
installed = await this.runNpmGlobalInstall(skill);
|
|
821
|
+
if (installed) {
|
|
822
|
+
this.log('info', `✅ Installed skill via npm global flow: ${skill.key}`);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
catch (error) {
|
|
826
|
+
methodErrors.push(`npm_global: ${error instanceof Error ? error.message : String(error)}`);
|
|
827
|
+
}
|
|
828
|
+
if (!installed) {
|
|
829
|
+
try {
|
|
830
|
+
installed = await this.runSkillHubInstall(skill);
|
|
831
|
+
if (installed) {
|
|
832
|
+
this.log('info', `✅ Installed skill via SkillHub flow: ${skill.key}`);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
catch (error) {
|
|
836
|
+
methodErrors.push(`skillhub: ${error instanceof Error ? error.message : String(error)}`);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
if (!installed) {
|
|
840
|
+
try {
|
|
841
|
+
installed = await this.syncGlobalPackageSkillToWorkspace(skill);
|
|
842
|
+
if (installed) {
|
|
843
|
+
this.log('info', `✅ Installed skill via local packaged fallback: ${skill.key}`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
catch (error) {
|
|
847
|
+
methodErrors.push(`local_packaged: ${error instanceof Error ? error.message : String(error)}`);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
if (!installed) {
|
|
851
|
+
try {
|
|
852
|
+
installed = await this.runOpenClawNativeInstall(skill);
|
|
853
|
+
if (installed) {
|
|
854
|
+
this.log('info', `✅ Installed skill via OpenClaw native flow: ${skill.key}`);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
catch (error) {
|
|
858
|
+
methodErrors.push(`openclaw_native: ${error instanceof Error ? error.message : String(error)}`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
if (!installed) {
|
|
862
|
+
throw new Error(`All install methods failed for ${skill.key}: ${methodErrors.join(' | ')}`);
|
|
863
|
+
}
|
|
864
|
+
if (skill.packageName) {
|
|
865
|
+
installedPackages.push(skill.packageName);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
changed = true;
|
|
869
|
+
this.log('info', '✅ Missing OpenClaw Skills installed via managed fallback flow');
|
|
870
|
+
}
|
|
871
|
+
catch (error) {
|
|
872
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
873
|
+
await this.reportSkillSyncStatus({
|
|
874
|
+
status: 'FAILED',
|
|
875
|
+
requiredSkills: this.requiredOpenClawSkills,
|
|
876
|
+
installedPackages,
|
|
877
|
+
missingPackages,
|
|
878
|
+
restartRequired: false,
|
|
879
|
+
message: `Automatic OpenClaw Skills install failed: ${msg}`,
|
|
880
|
+
securityScan: {
|
|
881
|
+
installedSkills: collectInstalledSkillKeys(this.openclawConfig),
|
|
882
|
+
},
|
|
883
|
+
skillPreflight: preflightResult.preflight,
|
|
884
|
+
registryHealth: preflightResult.registryHealth,
|
|
885
|
+
});
|
|
886
|
+
throw new Error(`Automatic OpenClaw Skills install failed after all methods: ${msg}`);
|
|
887
|
+
}
|
|
888
|
+
this.openclawConfig = loadOpenClawConfig();
|
|
889
|
+
if (!this.openclawConfig) {
|
|
890
|
+
return changed;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
if (!this.openclawConfig.skills) {
|
|
894
|
+
this.openclawConfig.skills = { entries: {} };
|
|
895
|
+
}
|
|
896
|
+
if (!this.openclawConfig.skills.entries) {
|
|
897
|
+
this.openclawConfig.skills.entries = {};
|
|
898
|
+
}
|
|
899
|
+
for (const item of this.requiredOpenClawSkillCatalog) {
|
|
900
|
+
const entryKey = findInstalledSkillEntryKey(this.openclawConfig.skills.entries, item.key, item.nativePackageName || item.packageName);
|
|
901
|
+
if (!entryKey) {
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
const entry = this.openclawConfig.skills.entries[entryKey] || {};
|
|
905
|
+
if (entry.enabled !== true) {
|
|
906
|
+
this.openclawConfig.skills.entries[entryKey] = { ...entry, enabled: true };
|
|
907
|
+
changed = true;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
if (changed) {
|
|
911
|
+
const configPath = this.openclawConfig._path;
|
|
912
|
+
if (!configPath) {
|
|
913
|
+
return changed;
|
|
914
|
+
}
|
|
915
|
+
const toWrite = { ...this.openclawConfig };
|
|
916
|
+
delete toWrite._path;
|
|
917
|
+
fs.writeFileSync(configPath, JSON.stringify(toWrite, null, 2), 'utf-8');
|
|
918
|
+
this.log('info', '✅ Required OpenClaw Skills have been enabled in config');
|
|
919
|
+
await this.reportSkillSyncStatus({
|
|
920
|
+
status: 'RESTART_REQUIRED',
|
|
921
|
+
requiredSkills: this.requiredOpenClawSkills,
|
|
922
|
+
installedPackages: this.requiredOpenClawPackages,
|
|
923
|
+
missingPackages: [],
|
|
924
|
+
restartRequired: true,
|
|
925
|
+
message: 'Required OpenClaw Skills were installed or enabled. Restart OpenClaw before reconnecting.',
|
|
926
|
+
securityScan: {
|
|
927
|
+
installedSkills: collectInstalledSkillKeys(this.openclawConfig),
|
|
928
|
+
},
|
|
929
|
+
skillPreflight: preflightResult.preflight,
|
|
930
|
+
registryHealth: preflightResult.registryHealth,
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
await this.reportSkillSyncStatus({
|
|
935
|
+
status: 'SYNCED',
|
|
936
|
+
requiredSkills: this.requiredOpenClawSkills,
|
|
937
|
+
installedPackages: this.requiredOpenClawPackages,
|
|
938
|
+
missingPackages: [],
|
|
939
|
+
restartRequired: false,
|
|
940
|
+
message: this.requiredOpenClawSkills.length > 0
|
|
941
|
+
? 'Required OpenClaw Skills are already installed and enabled.'
|
|
942
|
+
: 'No OpenClaw Skills are required for this agent.',
|
|
943
|
+
securityScan: {
|
|
944
|
+
installedSkills: collectInstalledSkillKeys(this.openclawConfig),
|
|
945
|
+
},
|
|
946
|
+
skillPreflight: {
|
|
947
|
+
checkedAt: new Date().toISOString(),
|
|
948
|
+
registry: this.config.skillInstallRegistry || process.env.OPENCLAW_SKILL_INSTALL_REGISTRY || 'https://registry.npmjs.org',
|
|
949
|
+
status: 'SKIPPED',
|
|
950
|
+
missingPackages: [],
|
|
951
|
+
reachablePackages: [],
|
|
952
|
+
unreachablePackages: [],
|
|
953
|
+
message: 'No missing packages require installation.',
|
|
954
|
+
},
|
|
955
|
+
registryHealth: {
|
|
956
|
+
registry: this.config.skillInstallRegistry || process.env.OPENCLAW_SKILL_INSTALL_REGISTRY || 'https://registry.npmjs.org',
|
|
957
|
+
checkedAt: new Date().toISOString(),
|
|
958
|
+
status: 'UNKNOWN',
|
|
959
|
+
latencyMs: null,
|
|
960
|
+
packageResults: [],
|
|
961
|
+
},
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
return changed;
|
|
241
965
|
}
|
|
242
966
|
// ============================================================
|
|
243
967
|
// WebSocket 连接
|
|
@@ -303,7 +1027,12 @@ class Connector {
|
|
|
303
1027
|
case 'pong':
|
|
304
1028
|
break;
|
|
305
1029
|
case 'message':
|
|
306
|
-
|
|
1030
|
+
if (message.payload?.type === 'HIVE_TASK') {
|
|
1031
|
+
await this.handleHiveTaskMessage(message.payload);
|
|
1032
|
+
}
|
|
1033
|
+
else {
|
|
1034
|
+
await this.handleUserMessage(message.payload);
|
|
1035
|
+
}
|
|
307
1036
|
break;
|
|
308
1037
|
case 'error':
|
|
309
1038
|
this.log('error', `❌ Platform error: ${message.message}`);
|
|
@@ -374,6 +1103,189 @@ class Connector {
|
|
|
374
1103
|
}
|
|
375
1104
|
await this.callAiBackend(sessionId, messageId, content);
|
|
376
1105
|
}
|
|
1106
|
+
async handleHiveTaskMessage(payload) {
|
|
1107
|
+
const taskTitle = payload.title || '未命名任务';
|
|
1108
|
+
this.log('info', '');
|
|
1109
|
+
this.log('info', `📥 Hive task [${payload.taskId}]: ${taskTitle}`);
|
|
1110
|
+
if (!payload.taskId || !payload.content) {
|
|
1111
|
+
this.log('error', '❌ Hive task payload is invalid');
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
if (!this.isAiReady) {
|
|
1115
|
+
await this.reportHiveTaskEvent(payload, 'FAILED', {
|
|
1116
|
+
summary: 'AI backend is not ready yet',
|
|
1117
|
+
content: 'AI backend is not ready yet. Please try again later.',
|
|
1118
|
+
});
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
await this.reportHiveTaskEvent(payload, 'ACCEPTED', {
|
|
1122
|
+
summary: `已接收任务:${taskTitle}`,
|
|
1123
|
+
});
|
|
1124
|
+
await this.reportHiveTaskEvent(payload, 'RUNNING', {
|
|
1125
|
+
summary: `正在执行:${taskTitle}`,
|
|
1126
|
+
});
|
|
1127
|
+
try {
|
|
1128
|
+
const finalContent = await this.generateAiCompletion(payload.content);
|
|
1129
|
+
const cleanedContent = finalContent.replace(/<final>/g, '').trim();
|
|
1130
|
+
if (!cleanedContent) {
|
|
1131
|
+
throw new Error('No content generated');
|
|
1132
|
+
}
|
|
1133
|
+
await this.reportHiveTaskEvent(payload, 'DONE', {
|
|
1134
|
+
summary: this.buildHiveTaskSummary(cleanedContent),
|
|
1135
|
+
content: cleanedContent,
|
|
1136
|
+
});
|
|
1137
|
+
this.log('info', `✅ Hive task completed: ${payload.taskId}`);
|
|
1138
|
+
}
|
|
1139
|
+
catch (error) {
|
|
1140
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1141
|
+
this.log('error', `❌ Hive task failed: ${message}`);
|
|
1142
|
+
await this.reportHiveTaskEvent(payload, 'FAILED', {
|
|
1143
|
+
summary: message,
|
|
1144
|
+
content: message,
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
buildHiveTaskSummary(content) {
|
|
1149
|
+
const normalized = content.replace(/\s+/g, ' ').trim();
|
|
1150
|
+
if (normalized.length <= 120) {
|
|
1151
|
+
return normalized;
|
|
1152
|
+
}
|
|
1153
|
+
return `${normalized.slice(0, 117)}...`;
|
|
1154
|
+
}
|
|
1155
|
+
async reportHiveTaskEvent(payload, event, options) {
|
|
1156
|
+
const baseUrl = (this.config.platformApiUrl || payload.platformApiUrl || process.env.PLATFORM_API_URL || 'http://localhost:3000').replace(/\/$/, '');
|
|
1157
|
+
const callbackAuthToken = payload.callbackAuthToken;
|
|
1158
|
+
if (!callbackAuthToken) {
|
|
1159
|
+
throw new Error(`Missing callbackAuthToken for Hive task ${payload.taskId}`);
|
|
1160
|
+
}
|
|
1161
|
+
const response = await fetch(`${baseUrl}/api/internal/hive-agent-events`, {
|
|
1162
|
+
method: 'POST',
|
|
1163
|
+
headers: {
|
|
1164
|
+
'Content-Type': 'application/json',
|
|
1165
|
+
'Authorization': `Bearer ${callbackAuthToken}`,
|
|
1166
|
+
},
|
|
1167
|
+
body: JSON.stringify({
|
|
1168
|
+
taskId: payload.taskId,
|
|
1169
|
+
event,
|
|
1170
|
+
summary: options.summary,
|
|
1171
|
+
content: options.content,
|
|
1172
|
+
}),
|
|
1173
|
+
});
|
|
1174
|
+
if (!response.ok) {
|
|
1175
|
+
const errorText = await response.text().catch(() => 'unknown error');
|
|
1176
|
+
throw new Error(`Failed to report Hive task event ${event}: HTTP ${response.status} ${errorText}`);
|
|
1177
|
+
}
|
|
1178
|
+
this.log('info', `📨 Hive task event ${event} reported to ${baseUrl}`);
|
|
1179
|
+
}
|
|
1180
|
+
async generateAiCompletion(content) {
|
|
1181
|
+
const postData = JSON.stringify({
|
|
1182
|
+
model: this.model,
|
|
1183
|
+
messages: this.buildChatMessages(content),
|
|
1184
|
+
stream: true,
|
|
1185
|
+
user: `agentmarket-hive-${this.config.connectionId}`,
|
|
1186
|
+
});
|
|
1187
|
+
return new Promise((resolve, reject) => {
|
|
1188
|
+
let options;
|
|
1189
|
+
let reqModule;
|
|
1190
|
+
if (this.isCustomMode && this.parsedApiUrl) {
|
|
1191
|
+
reqModule = this.parsedApiUrl.protocol === 'https:' ? https : http;
|
|
1192
|
+
options = {
|
|
1193
|
+
hostname: this.parsedApiUrl.hostname,
|
|
1194
|
+
port: this.parsedApiUrl.port,
|
|
1195
|
+
path: this.parsedApiUrl.path,
|
|
1196
|
+
method: 'POST',
|
|
1197
|
+
headers: {
|
|
1198
|
+
'Content-Type': 'application/json',
|
|
1199
|
+
'Content-Length': Buffer.byteLength(postData),
|
|
1200
|
+
...(this.config.apiKey ? { 'Authorization': `Bearer ${this.config.apiKey}` } : {}),
|
|
1201
|
+
},
|
|
1202
|
+
timeout: 120000,
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
else {
|
|
1206
|
+
reqModule = http;
|
|
1207
|
+
options = {
|
|
1208
|
+
hostname: 'localhost',
|
|
1209
|
+
port: this.openclawPort,
|
|
1210
|
+
path: '/v1/chat/completions',
|
|
1211
|
+
method: 'POST',
|
|
1212
|
+
headers: {
|
|
1213
|
+
'Content-Type': 'application/json',
|
|
1214
|
+
'Authorization': `Bearer ${this.openclawToken}`,
|
|
1215
|
+
'Content-Length': Buffer.byteLength(postData),
|
|
1216
|
+
},
|
|
1217
|
+
timeout: 120000,
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
const req = reqModule.request(options, (res) => {
|
|
1221
|
+
const isStream = res.headers['content-type']?.includes('text/event-stream') ||
|
|
1222
|
+
res.headers['transfer-encoding'] === 'chunked';
|
|
1223
|
+
if (isStream) {
|
|
1224
|
+
this.collectStreamResponse(res, resolve, reject);
|
|
1225
|
+
}
|
|
1226
|
+
else {
|
|
1227
|
+
this.collectNormalResponse(res, resolve, reject);
|
|
1228
|
+
}
|
|
1229
|
+
});
|
|
1230
|
+
req.on('error', (error) => {
|
|
1231
|
+
reject(new Error(`HTTP request failed: ${error.message}`));
|
|
1232
|
+
});
|
|
1233
|
+
req.on('timeout', () => {
|
|
1234
|
+
req.destroy();
|
|
1235
|
+
reject(new Error('Request timed out'));
|
|
1236
|
+
});
|
|
1237
|
+
req.write(postData);
|
|
1238
|
+
req.end();
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
collectStreamResponse(res, resolve, reject) {
|
|
1242
|
+
let fullContent = '';
|
|
1243
|
+
let buffer = '';
|
|
1244
|
+
res.on('data', (chunk) => {
|
|
1245
|
+
buffer += chunk.toString();
|
|
1246
|
+
const lines = buffer.split('\n').map(line => line.replace(/\r$/, ''));
|
|
1247
|
+
buffer = lines.pop() || '';
|
|
1248
|
+
for (const line of lines) {
|
|
1249
|
+
if (!line.startsWith('data: '))
|
|
1250
|
+
continue;
|
|
1251
|
+
const data = line.slice(6).trim();
|
|
1252
|
+
if (data === '[DONE]')
|
|
1253
|
+
continue;
|
|
1254
|
+
try {
|
|
1255
|
+
const parsed = JSON.parse(data);
|
|
1256
|
+
const delta = parsed.choices?.[0]?.delta?.content;
|
|
1257
|
+
if (delta) {
|
|
1258
|
+
fullContent += delta;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
catch {
|
|
1262
|
+
// 忽略无法解析的 SSE 片段
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
});
|
|
1266
|
+
res.on('end', () => resolve(fullContent));
|
|
1267
|
+
res.on('error', (error) => reject(new Error(`Stream read error: ${error.message}`)));
|
|
1268
|
+
}
|
|
1269
|
+
collectNormalResponse(res, resolve, reject) {
|
|
1270
|
+
let data = '';
|
|
1271
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
1272
|
+
res.on('end', () => {
|
|
1273
|
+
try {
|
|
1274
|
+
const result = JSON.parse(data);
|
|
1275
|
+
if (result.error) {
|
|
1276
|
+
reject(new Error(typeof result.error === 'string' ? result.error : JSON.stringify(result.error)));
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
const replyContent = result.choices?.[0]?.message?.content || '';
|
|
1280
|
+
resolve(replyContent);
|
|
1281
|
+
}
|
|
1282
|
+
catch (error) {
|
|
1283
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1284
|
+
reject(new Error(`Failed to parse response: ${message}`));
|
|
1285
|
+
}
|
|
1286
|
+
});
|
|
1287
|
+
res.on('error', (error) => reject(new Error(`Response read error: ${error.message}`)));
|
|
1288
|
+
}
|
|
377
1289
|
// ============================================================
|
|
378
1290
|
// AI 后端调用(流式)
|
|
379
1291
|
// ============================================================
|
|
@@ -382,7 +1294,7 @@ class Connector {
|
|
|
382
1294
|
return new Promise((resolve) => {
|
|
383
1295
|
const postData = JSON.stringify({
|
|
384
1296
|
model: this.model,
|
|
385
|
-
messages:
|
|
1297
|
+
messages: this.buildChatMessages(content),
|
|
386
1298
|
stream: true,
|
|
387
1299
|
user: `agentmarket-${sessionId}`,
|
|
388
1300
|
});
|
|
@@ -575,6 +1487,38 @@ class Connector {
|
|
|
575
1487
|
console.log(`${timestamp} ${prefix} ${message}`);
|
|
576
1488
|
}
|
|
577
1489
|
}
|
|
1490
|
+
buildChatMessages(content) {
|
|
1491
|
+
const messages = [];
|
|
1492
|
+
const systemInstruction = this.buildSystemInstruction();
|
|
1493
|
+
if (systemInstruction) {
|
|
1494
|
+
messages.push({ role: 'system', content: systemInstruction });
|
|
1495
|
+
}
|
|
1496
|
+
messages.push({ role: 'user', content });
|
|
1497
|
+
return messages;
|
|
1498
|
+
}
|
|
1499
|
+
buildSystemInstruction() {
|
|
1500
|
+
const promptParts = [];
|
|
1501
|
+
if (this.systemPrompt) {
|
|
1502
|
+
promptParts.push(this.systemPrompt);
|
|
1503
|
+
}
|
|
1504
|
+
else {
|
|
1505
|
+
const identityParts = [];
|
|
1506
|
+
if (this.agentName) {
|
|
1507
|
+
identityParts.push(`你是「${this.agentName}」`);
|
|
1508
|
+
}
|
|
1509
|
+
if (this.agentDescription) {
|
|
1510
|
+
identityParts.push(`角色说明:${this.agentDescription}`);
|
|
1511
|
+
}
|
|
1512
|
+
if (identityParts.length > 0) {
|
|
1513
|
+
promptParts.push(identityParts.join('\n'));
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
if (promptParts.length === 0) {
|
|
1517
|
+
return null;
|
|
1518
|
+
}
|
|
1519
|
+
promptParts.push('你必须始终以该智能体的角色、职责边界和风格回复用户,不要脱离设定。');
|
|
1520
|
+
return promptParts.join('\n\n');
|
|
1521
|
+
}
|
|
578
1522
|
printStartInfo() {
|
|
579
1523
|
const gatewayUrl = this.config.gatewayUrl || 'wss://gateway.agentmarketpro.ai/ws/connector';
|
|
580
1524
|
console.log('');
|
|
@@ -585,6 +1529,9 @@ class Connector {
|
|
|
585
1529
|
console.log('📋 Configuration:');
|
|
586
1530
|
console.log(` ├── Gateway: ${gatewayUrl}`);
|
|
587
1531
|
console.log(` ├── Connection ID: ${this.config.connectionId}`);
|
|
1532
|
+
if (this.agentName) {
|
|
1533
|
+
console.log(` ├── Agent: ${this.agentName}`);
|
|
1534
|
+
}
|
|
588
1535
|
if (this.isCustomMode) {
|
|
589
1536
|
const keyPreview = this.config.apiKey
|
|
590
1537
|
? `${this.config.apiKey.substring(0, 6)}...`
|
|
@@ -601,7 +1548,25 @@ class Connector {
|
|
|
601
1548
|
: process.env.OPENCLAW_TOKEN ? 'env'
|
|
602
1549
|
: 'auto';
|
|
603
1550
|
console.log(` ├── OpenClaw: localhost:${this.openclawPort} (HTTP API)`);
|
|
604
|
-
|
|
1551
|
+
if (this.requiredOpenClawSkills.length > 0) {
|
|
1552
|
+
console.log(` ├── Skills: ${this.requiredOpenClawSkills.join(', ')}`);
|
|
1553
|
+
if (this.systemPrompt || this.agentDescription) {
|
|
1554
|
+
console.log(' ├── Persona: loaded');
|
|
1555
|
+
console.log(` └── Token: ${tokenPreview} (${tokenSource})`);
|
|
1556
|
+
}
|
|
1557
|
+
else {
|
|
1558
|
+
console.log(` └── Token: ${tokenPreview} (${tokenSource})`);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
else {
|
|
1562
|
+
if (this.systemPrompt || this.agentDescription) {
|
|
1563
|
+
console.log(' ├── Persona: loaded');
|
|
1564
|
+
console.log(` └── Token: ${tokenPreview} (${tokenSource})`);
|
|
1565
|
+
}
|
|
1566
|
+
else {
|
|
1567
|
+
console.log(` └── Token: ${tokenPreview} (${tokenSource})`);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
605
1570
|
}
|
|
606
1571
|
console.log('');
|
|
607
1572
|
}
|