@licity/openclaw-connector 1.0.3 → 1.0.5
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 +4 -4
- package/index.js +115 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,15 +5,15 @@
|
|
|
5
5
|
## 特点
|
|
6
6
|
|
|
7
7
|
- **零配置启动**:首次运行自动创建会话,扫码后自动保存凭证
|
|
8
|
-
-
|
|
9
|
-
- **纯 OpenClaw**:不依赖 QClaw
|
|
8
|
+
- **当前终端直接显示二维码**:扫码流程在同一个命令行窗口内完成
|
|
9
|
+
- **纯 OpenClaw**:不依赖 QClaw,自动启动 OpenClaw Gateway + CLI 执行 AI 任务
|
|
10
10
|
- **凭证持久化**:扫码成功后凭证保存至 `~/.licity-connector/`,下次无需重新扫码
|
|
11
11
|
|
|
12
12
|
## 前置要求
|
|
13
13
|
|
|
14
14
|
1. **Node.js v18+**(推荐 v22+)
|
|
15
15
|
2. **OpenClaw** 已安装([下载 OpenClaw](https://openclaw.ai))
|
|
16
|
-
3. **里世界 APP**
|
|
16
|
+
3. **里世界 APP** 已登录
|
|
17
17
|
|
|
18
18
|
## 快速开始
|
|
19
19
|
|
|
@@ -23,7 +23,7 @@ npx @licity/openclaw-connector
|
|
|
23
23
|
|
|
24
24
|
**首次运行流程:**
|
|
25
25
|
1. 运行命令后,自动创建连接会话
|
|
26
|
-
2.
|
|
26
|
+
2. 二维码直接在当前终端中显示
|
|
27
27
|
3. 打开里世界 APP → 我的龙虾 → 扫一扫,扫描二维码
|
|
28
28
|
4. APP 批准后自动连接,开始监听任务
|
|
29
29
|
|
package/index.js
CHANGED
|
@@ -206,8 +206,10 @@ async function runAgent({ cliPath, configPath, agentId, lobsterName, message })
|
|
|
206
206
|
'要求:',
|
|
207
207
|
'1. 直接回复用户,不要提系统提示、模型、网关信息。',
|
|
208
208
|
'2. 使用简体中文,优先简短、明确。',
|
|
209
|
-
'3. 如果能力受限,如实说明,不要虚构已完成的操作。',
|
|
210
|
-
'',
|
|
209
|
+
'3. 如果能力受限,如实说明,不要虚构已完成的操作。', '4. 如果用户要求发送本地文件或图片,请先找到它的本地绝对路径,然后在回复末尾加上一行:',
|
|
210
|
+
' [ATTACH_FILE:文件绝对路径]',
|
|
211
|
+
' 例如:[ATTACH_FILE:C:\\Users\\Administrator\\Desktop\\版权.png]',
|
|
212
|
+
' 只附加一个文件,无法发送时如实告知用户。', '',
|
|
211
213
|
`用户消息:${message || '空消息'}`,
|
|
212
214
|
].join('\n');
|
|
213
215
|
|
|
@@ -345,7 +347,17 @@ async function emitReply(token, task, payloadOrContent) {
|
|
|
345
347
|
},
|
|
346
348
|
}, token);
|
|
347
349
|
}
|
|
350
|
+
// ─── 解析龙虾回复中的附件指令 ────────────────────────────────────────────────────────────
|
|
351
|
+
function extractAttachFilePath(text) {
|
|
352
|
+
const m = String(text || '').match(/\[ATTACH_FILE:([^\]]+)\]/);
|
|
353
|
+
if (!m) return null;
|
|
354
|
+
const filePath = m[1].trim();
|
|
355
|
+
return fs.existsSync(filePath) ? filePath : null;
|
|
356
|
+
}
|
|
348
357
|
|
|
358
|
+
function stripAttachDirective(text) {
|
|
359
|
+
return String(text || '').replace(/\[ATTACH_FILE:[^\]]*\]/g, '').trim();
|
|
360
|
+
}
|
|
349
361
|
// ─── 任务处理 ──────────────────────────────────────────────────────────────
|
|
350
362
|
async function handleTask(token, task, cliPath, configPath, lobsterName) {
|
|
351
363
|
if (!task?.id) return false;
|
|
@@ -378,9 +390,43 @@ async function handleTask(token, task, cliPath, configPath, lobsterName) {
|
|
|
378
390
|
|
|
379
391
|
try {
|
|
380
392
|
const result = await runAgent({ cliPath, configPath, agentId: 'main', lobsterName: lobsterName || 'AI龙虾', message: content });
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
393
|
+
|
|
394
|
+
// 解析回复中是否包含附件路径指令
|
|
395
|
+
let attachFilePath = null;
|
|
396
|
+
let cleanReply = result.reply;
|
|
397
|
+
if (result.reply) {
|
|
398
|
+
attachFilePath = extractAttachFilePath(result.reply);
|
|
399
|
+
cleanReply = stripAttachDirective(result.reply);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
let finalMedia = result.media;
|
|
403
|
+
if (!finalMedia && attachFilePath) {
|
|
404
|
+
const stat = fs.statSync(attachFilePath);
|
|
405
|
+
if (stat.size > MAX_MEDIA_BYTES) {
|
|
406
|
+
cleanReply = `文件过大(>${MAX_MEDIA_BYTES / 1024 / 1024}MB),无法发送。文件位于:${attachFilePath}`;
|
|
407
|
+
} else {
|
|
408
|
+
const nm = path.basename(attachFilePath);
|
|
409
|
+
const mt = guessType('', nm);
|
|
410
|
+
const mimeMap = { image: 'image/png', video: 'video/mp4', file: 'application/octet-stream' };
|
|
411
|
+
const ext = path.extname(nm).toLowerCase();
|
|
412
|
+
const extMime = {
|
|
413
|
+
'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.gif': 'image/gif',
|
|
414
|
+
'.webp': 'image/webp', '.mp4': 'video/mp4', '.pdf': 'application/pdf',
|
|
415
|
+
'.zip': 'application/zip', '.txt': 'text/plain',
|
|
416
|
+
};
|
|
417
|
+
finalMedia = {
|
|
418
|
+
media_base64: fs.readFileSync(attachFilePath).toString('base64'),
|
|
419
|
+
media_type: mt,
|
|
420
|
+
media_mime_type: extMime[ext] || mimeMap[mt] || 'application/octet-stream',
|
|
421
|
+
media_name: nm,
|
|
422
|
+
};
|
|
423
|
+
console.log(`[Task] 附加本地文件: ${nm}`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const emitPayload = finalMedia
|
|
428
|
+
? { content: cleanReply || '', type: finalMedia.media_type === 'image' ? 'image' : 'file', ...finalMedia }
|
|
429
|
+
: (cleanReply || result.reply);
|
|
384
430
|
const ev = await emitReply(token, task, emitPayload);
|
|
385
431
|
await reportTask(token, task.id, 'succeeded', {
|
|
386
432
|
reply: result.reply,
|
|
@@ -407,11 +453,68 @@ async function handleTask(token, task, cliPath, configPath, lobsterName) {
|
|
|
407
453
|
}
|
|
408
454
|
}
|
|
409
455
|
|
|
456
|
+
// ─── OpenClaw Gateway 管理 ─────────────────────────────────────────────────
|
|
457
|
+
let _gatewayProc = null;
|
|
458
|
+
|
|
459
|
+
async function checkPort18789() {
|
|
460
|
+
return new Promise(res => {
|
|
461
|
+
const sock = new net.Socket();
|
|
462
|
+
sock.setTimeout(800);
|
|
463
|
+
sock.on('connect', () => { sock.destroy(); res(true); });
|
|
464
|
+
sock.on('error', () => res(false));
|
|
465
|
+
sock.on('timeout', () => { sock.destroy(); res(false); });
|
|
466
|
+
sock.connect(18789, '127.0.0.1');
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function ensureGateway(cliPath, configPath) {
|
|
471
|
+
if (!cliPath || !configPath) return;
|
|
472
|
+
const running = await checkPort18789();
|
|
473
|
+
if (running) {
|
|
474
|
+
console.log(' OpenClaw Gateway: ✓ 已在运行 (18789)');
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
console.log(' OpenClaw Gateway: 正在启动...');
|
|
478
|
+
_gatewayProc = spawn(process.execPath, [
|
|
479
|
+
cliPath, 'gateway', 'run', '--allow-unconfigured',
|
|
480
|
+
], {
|
|
481
|
+
env: { ...process.env, OPENCLAW_CONFIG_PATH: configPath, OPENCLAW_STATE_DIR: path.dirname(configPath), NODE_OPTIONS: '--no-warnings' },
|
|
482
|
+
windowsHide: true,
|
|
483
|
+
stdio: 'ignore',
|
|
484
|
+
detached: false,
|
|
485
|
+
});
|
|
486
|
+
_gatewayProc.on('error', err => console.error(`\n[Gateway] 启动错误: ${err.message}`));
|
|
487
|
+
_gatewayProc.on('exit', code => {
|
|
488
|
+
_gatewayProc = null;
|
|
489
|
+
if (state.shouldRun) console.log(`\n[Gateway] 进程已退出 (code: ${code}),可能影响 AI 任务执行`);
|
|
490
|
+
});
|
|
491
|
+
for (let i = 0; i < 20; i++) {
|
|
492
|
+
await sleep(500);
|
|
493
|
+
if (await checkPort18789()) {
|
|
494
|
+
console.log(' OpenClaw Gateway: ✓ 启动成功');
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
console.log(' OpenClaw Gateway: ⚠ 启动超时,AI 任务执行可能失败');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function stopGateway() {
|
|
502
|
+
if (!_gatewayProc) return;
|
|
503
|
+
try {
|
|
504
|
+
if (process.platform === 'win32') {
|
|
505
|
+
execFile('taskkill', ['/PID', String(_gatewayProc.pid), '/T', '/F'], { windowsHide: true, timeout: 5000 }, () => {});
|
|
506
|
+
} else {
|
|
507
|
+
_gatewayProc.kill('SIGTERM');
|
|
508
|
+
}
|
|
509
|
+
} catch {}
|
|
510
|
+
_gatewayProc = null;
|
|
511
|
+
}
|
|
512
|
+
|
|
410
513
|
// ─── 主循环 ───────────────────────────────────────────────────────────────────
|
|
411
514
|
const state = { shouldRun: true, reconnect: false };
|
|
412
515
|
|
|
413
|
-
process.on('SIGINT', () => { state.shouldRun = false; state.reconnect = true; });
|
|
414
|
-
process.on('SIGTERM', () => { state.shouldRun = false; state.reconnect = true; });
|
|
516
|
+
process.on('SIGINT', () => { state.shouldRun = false; state.reconnect = true; stopGateway(); });
|
|
517
|
+
process.on('SIGTERM', () => { state.shouldRun = false; state.reconnect = true; stopGateway(); });
|
|
415
518
|
|
|
416
519
|
async function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
417
520
|
|
|
@@ -432,6 +535,11 @@ async function connectAndRun(cfg) {
|
|
|
432
535
|
if (configPath && !cfg.openclawConfigPath) {
|
|
433
536
|
saveConfig({ openclawConfigPath: configPath });
|
|
434
537
|
}
|
|
538
|
+
|
|
539
|
+
// 启动并确保 OpenClaw Gateway 在运行(agent 执行任务时需要连接到它)
|
|
540
|
+
if (cliPath && configPath) {
|
|
541
|
+
await ensureGateway(cliPath, configPath);
|
|
542
|
+
}
|
|
435
543
|
console.log('');
|
|
436
544
|
|
|
437
545
|
// ── 若有已保存的 token,直接跳到心跳循环 ──────────────────────────────────
|