Selinore commited on
Commit
191c490
·
verified ·
1 Parent(s): 031d70f

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +55 -52
server.js CHANGED
@@ -30,6 +30,7 @@ const {
30
  CONNECT_PASSWORD = '',
31
  FRONT_STREAM_INTERVAL = '1500',
32
  MAX_UPLOAD_MB = '30',
 
33
 
34
  STORAGE_EXTERNAL_ENDPOINT,
35
  STORAGE_PUBLIC_BUCKET,
@@ -39,10 +40,17 @@ const {
39
  STORAGE_S3_FORCE_PATH_STYLE
40
  } = process.env;
41
 
42
- const IS_DEBUG = String(DEBUG_LOGS).toLowerCase() !== 'false';
43
- const STREAM_INTERVAL = Math.max(parseInt(FRONT_STREAM_INTERVAL, 10) || 1500, 800);
44
- const UPLOAD_MAX_BYTES = (parseInt(MAX_UPLOAD_MB, 10) || 30) * 1024 * 1024;
 
 
 
45
 
 
 
 
 
46
  const s3 = new S3Client({
47
  region: STORAGE_REGION || 'us-east-1',
48
  endpoint: STORAGE_EXTERNAL_ENDPOINT,
@@ -56,9 +64,10 @@ const s3 = new S3Client({
56
  const S3_PREFIX_CONFIG = 'openclaw/channels.json';
57
  const S3_PREFIX_UPLOADS = 'openclaw/uploads/';
58
 
 
59
  const logBuffer = [];
60
  function addLog(type, msg) {
61
- if (type === 'info' && !IS_DEBUG) return;
62
  const time = new Date().toLocaleTimeString('zh-CN', { hour12: false });
63
  const line = `[${time}] ${type === 'err' ? '❌ ' : ''}${msg}`;
64
  console[type === 'err' ? 'error' : 'log'](line);
@@ -185,11 +194,11 @@ async function cleanOldS3Files() {
185
  }
186
 
187
  // ============================================================
188
- // 2) HTTP API
189
  // ============================================================
190
 
191
  const app = express();
192
- app.use(express.json({ limit: `${Math.max(parseInt(MAX_UPLOAD_MB, 10) || 30, 10)}mb` }));
193
  app.use(express.static(path.join(__dirname, 'public')));
194
 
195
  function passOK(req) {
@@ -204,16 +213,37 @@ function apiGuard(req, res, next) {
204
  }
205
  app.use('/api', apiGuard);
206
 
 
207
  app.get('/api/config', (req, res) => {
208
  res.json({
209
- botUsername: BOT_USERNAME,
210
- debug: IS_DEBUG,
211
  needPassword: !!CONNECT_PASSWORD,
212
- streamInterval: STREAM_INTERVAL,
213
- maxUploadMB: parseInt(MAX_UPLOAD_MB, 10) || 30
214
  });
215
  });
216
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  app.get('/api/channels', (req, res) => {
218
  res.json(Array.from(channelRuntime.values()).map(({ id, name }) => ({ id, name })));
219
  });
@@ -231,7 +261,7 @@ app.get('/api/logs', (req, res) => res.json(logBuffer));
231
  app.get('/api/messages/:cid', async (req, res) => {
232
  const cid = req.params.cid;
233
  try {
234
- const limit = parseInt(req.query.limit, 10) || 120;
235
  const { data } = await webuiApi.get(`/api/v1/channels/${cid}/messages?limit=${limit}`);
236
  res.json(Array.isArray(data) ? data : []);
237
  } catch (e) {
@@ -240,11 +270,11 @@ app.get('/api/messages/:cid', async (req, res) => {
240
  }
241
  });
242
 
243
- // SSE 增量消息
244
  app.get('/api/messages/:cid/stream', async (req, res) => {
245
  const cid = req.params.cid;
246
- const limit = parseInt(req.query.limit, 10) || 120;
247
- const interval = Math.max(parseInt(req.query.interval, 10) || STREAM_INTERVAL, 800);
248
 
249
  if (!passOK(req)) return res.status(401).end('unauthorized');
250
 
@@ -271,7 +301,7 @@ app.get('/api/messages/:cid/stream', async (req, res) => {
271
  try {
272
  const { data } = await webuiApi.get(`/api/v1/channels/${cid}/messages?limit=${limit}`);
273
  const list = Array.isArray(data) ? data : [];
274
- const sorted = [...list].reverse(); // old -> new
275
 
276
  if (!lastSeen) {
277
  if (sorted.length > 0) lastSeen = sorted[sorted.length - 1].id;
@@ -308,8 +338,8 @@ app.post('/api/upload', async (req, res) => {
308
 
309
  try {
310
  const buffer = Buffer.from(base64, 'base64');
311
- if (buffer.length > UPLOAD_MAX_BYTES) {
312
- return res.status(413).json({ error: `文件过大,超过 ${MAX_UPLOAD_MB}MB` });
313
  }
314
 
315
  const url = await uploadToS3(filename, buffer, mimeType);
@@ -349,7 +379,7 @@ app.get('/', (req, res) => {
349
  });
350
 
351
  // ============================================================
352
- // 3) OpenClaw bridge logic (保留你的稳定逻辑)
353
  // ============================================================
354
 
355
  app.listen(PORT, async () => {
@@ -385,7 +415,6 @@ async function initChannelBaseline(id) {
385
  }
386
  }
387
 
388
- // WS connect
389
  const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
390
  const rawPublicKey = publicKey.export({ type: 'spki', format: 'der' }).subarray(12);
391
  const deviceId = crypto.createHash('sha256').update(rawPublicKey).digest('hex');
@@ -454,15 +483,9 @@ function handleClawMessage(data) {
454
  const { nonce } = msg.payload;
455
  const signedAt = Date.now();
456
  const payload = [
457
- 'v2',
458
- deviceId,
459
- 'openclaw-control-ui',
460
- 'webchat',
461
- 'operator',
462
  'operator.admin,operator.approvals,operator.pairing',
463
- String(signedAt),
464
- OPENCLAW_TOKEN || '',
465
- nonce
466
  ].join('|');
467
 
468
  const signature = crypto.sign(null, Buffer.from(payload), privateKey);
@@ -474,19 +497,8 @@ function handleClawMessage(data) {
474
  params: {
475
  minProtocol: 3,
476
  maxProtocol: 3,
477
- client: {
478
- id: 'openclaw-control-ui',
479
- version: 'dev',
480
- platform: 'Win32',
481
- mode: 'webchat'
482
- },
483
- device: {
484
- id: deviceId,
485
- publicKey: toB64Url(rawPublicKey),
486
- signature: toB64Url(signature),
487
- signedAt,
488
- nonce
489
- },
490
  auth: { token: OPENCLAW_TOKEN },
491
  role: 'operator',
492
  scopes: ['operator.admin', 'operator.approvals', 'operator.pairing']
@@ -539,7 +551,6 @@ function fetchClawHistory(cid, uid) {
539
  function triggerAgent(cid, uid, name, text, isCommand) {
540
  if (!isClawReady) return;
541
  const finalMsg = isCommand ? text : `${name} 说: ${text}`;
542
-
543
  ws.send(JSON.stringify({
544
  type: 'req',
545
  id: crypto.randomUUID(),
@@ -551,13 +562,9 @@ function triggerAgent(cid, uid, name, text, isCommand) {
551
  deliver: isCommand ? false : undefined
552
  }
553
  }));
554
- log(`🚀 [发送] ${name}: ${text.slice(0, 28)}...`);
555
  }
556
 
557
- // ============================================================
558
- // 4) Sync loop (保留你的保护策略)
559
- // ============================================================
560
-
561
  async function syncChannel(cid) {
562
  const state = channelRuntime.get(cid);
563
  if (!state || !state.lastMsgId || !isClawReady) return;
@@ -576,7 +583,6 @@ async function syncChannel(cid) {
576
  const { data: msgs } = await webuiApi.get(`/api/v1/channels/${cid}/messages?skip=0&limit=10`);
577
  if (!msgs?.length) throw new Error('No messages');
578
 
579
- // 回信检测
580
  const webuiFingerprints = new Set(
581
  msgs.map(m => (m.content || m.data?.content || '').trim()).filter(Boolean)
582
  );
@@ -595,16 +601,13 @@ async function syncChannel(cid) {
595
  if (txt && !webuiFingerprints.has(txt)) {
596
  log(`📥 [回填回复] -> ${u.name}`);
597
  await webuiApi.post(`/api/v1/channels/${cid}/messages/post`, {
598
- content: txt,
599
- data: { files: [] },
600
- reply_to_id: null
601
  });
602
  webuiFingerprints.add(txt);
603
  }
604
  }
605
  }
606
 
607
- // 新消息处理
608
  const sorted = msgs.reverse();
609
  let newMsgs = [];
610
  const startIdx = sorted.findIndex(m => m.id === state.lastMsgId);
@@ -616,7 +619,7 @@ async function syncChannel(cid) {
616
  } else {
617
  if (sorted.length > 0) {
618
  const newestId = sorted[sorted.length - 1].id;
619
- log(`⚠️ [${state.name}] 锚点丢失,重置 -> ${newestId}(跳过历史)`);
620
  state.lastMsgId = newestId;
621
  }
622
  newMsgs = [];
 
30
  CONNECT_PASSWORD = '',
31
  FRONT_STREAM_INTERVAL = '1500',
32
  MAX_UPLOAD_MB = '30',
33
+ DEFAULT_MESSAGE_LIMIT = '120',
34
 
35
  STORAGE_EXTERNAL_ENDPOINT,
36
  STORAGE_PUBLIC_BUCKET,
 
40
  STORAGE_S3_FORCE_PATH_STYLE
41
  } = process.env;
42
 
43
+ const runtimeConfig = {
44
+ botUsername: BOT_USERNAME,
45
+ debugLogs: String(DEBUG_LOGS).toLowerCase() !== 'false',
46
+ streamInterval: Math.max(parseInt(FRONT_STREAM_INTERVAL, 10) || 1500, 800),
47
+ defaultMessageLimit: Math.max(parseInt(DEFAULT_MESSAGE_LIMIT, 10) || 120, 20)
48
+ };
49
 
50
+ const uploadMaxMB = Math.max(parseInt(MAX_UPLOAD_MB, 10) || 30, 5);
51
+ const uploadMaxBytes = uploadMaxMB * 1024 * 1024;
52
+
53
+ // S3
54
  const s3 = new S3Client({
55
  region: STORAGE_REGION || 'us-east-1',
56
  endpoint: STORAGE_EXTERNAL_ENDPOINT,
 
64
  const S3_PREFIX_CONFIG = 'openclaw/channels.json';
65
  const S3_PREFIX_UPLOADS = 'openclaw/uploads/';
66
 
67
+ // logs
68
  const logBuffer = [];
69
  function addLog(type, msg) {
70
+ if (type === 'info' && !runtimeConfig.debugLogs) return;
71
  const time = new Date().toLocaleTimeString('zh-CN', { hour12: false });
72
  const line = `[${time}] ${type === 'err' ? '❌ ' : ''}${msg}`;
73
  console[type === 'err' ? 'error' : 'log'](line);
 
194
  }
195
 
196
  // ============================================================
197
+ // 2) API
198
  // ============================================================
199
 
200
  const app = express();
201
+ app.use(express.json({ limit: `${uploadMaxMB}mb` }));
202
  app.use(express.static(path.join(__dirname, 'public')));
203
 
204
  function passOK(req) {
 
213
  }
214
  app.use('/api', apiGuard);
215
 
216
+ // 给前端默认配置
217
  app.get('/api/config', (req, res) => {
218
  res.json({
 
 
219
  needPassword: !!CONNECT_PASSWORD,
220
+ maxUploadMB: uploadMaxMB,
221
+ runtimeConfig
222
  });
223
  });
224
 
225
+ // 前端设置(运行期)
226
+ app.get('/api/runtime-settings', (req, res) => {
227
+ res.json(runtimeConfig);
228
+ });
229
+ app.post('/api/runtime-settings', (req, res) => {
230
+ const body = req.body || {};
231
+ if (typeof body.botUsername === 'string' && body.botUsername.trim()) {
232
+ runtimeConfig.botUsername = body.botUsername.trim();
233
+ }
234
+ if (typeof body.debugLogs === 'boolean') {
235
+ runtimeConfig.debugLogs = body.debugLogs;
236
+ }
237
+ if (Number.isFinite(Number(body.streamInterval))) {
238
+ runtimeConfig.streamInterval = Math.max(800, parseInt(body.streamInterval, 10));
239
+ }
240
+ if (Number.isFinite(Number(body.defaultMessageLimit))) {
241
+ runtimeConfig.defaultMessageLimit = Math.max(20, parseInt(body.defaultMessageLimit, 10));
242
+ }
243
+ log(`⚙️ 运行设置更新: ${JSON.stringify(runtimeConfig)}`);
244
+ res.json({ success: true, runtimeConfig });
245
+ });
246
+
247
  app.get('/api/channels', (req, res) => {
248
  res.json(Array.from(channelRuntime.values()).map(({ id, name }) => ({ id, name })));
249
  });
 
261
  app.get('/api/messages/:cid', async (req, res) => {
262
  const cid = req.params.cid;
263
  try {
264
+ const limit = Math.max(parseInt(req.query.limit, 10) || runtimeConfig.defaultMessageLimit, 20);
265
  const { data } = await webuiApi.get(`/api/v1/channels/${cid}/messages?limit=${limit}`);
266
  res.json(Array.isArray(data) ? data : []);
267
  } catch (e) {
 
270
  }
271
  });
272
 
273
+ // SSE
274
  app.get('/api/messages/:cid/stream', async (req, res) => {
275
  const cid = req.params.cid;
276
+ const limit = Math.max(parseInt(req.query.limit, 10) || runtimeConfig.defaultMessageLimit, 20);
277
+ const interval = Math.max(parseInt(req.query.interval, 10) || runtimeConfig.streamInterval, 800);
278
 
279
  if (!passOK(req)) return res.status(401).end('unauthorized');
280
 
 
301
  try {
302
  const { data } = await webuiApi.get(`/api/v1/channels/${cid}/messages?limit=${limit}`);
303
  const list = Array.isArray(data) ? data : [];
304
+ const sorted = [...list].reverse();
305
 
306
  if (!lastSeen) {
307
  if (sorted.length > 0) lastSeen = sorted[sorted.length - 1].id;
 
338
 
339
  try {
340
  const buffer = Buffer.from(base64, 'base64');
341
+ if (buffer.length > uploadMaxBytes) {
342
+ return res.status(413).json({ error: `文件过大,超过 ${uploadMaxMB}MB` });
343
  }
344
 
345
  const url = await uploadToS3(filename, buffer, mimeType);
 
379
  });
380
 
381
  // ============================================================
382
+ // 3) OpenClaw logic (你的逻辑保留)
383
  // ============================================================
384
 
385
  app.listen(PORT, async () => {
 
415
  }
416
  }
417
 
 
418
  const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
419
  const rawPublicKey = publicKey.export({ type: 'spki', format: 'der' }).subarray(12);
420
  const deviceId = crypto.createHash('sha256').update(rawPublicKey).digest('hex');
 
483
  const { nonce } = msg.payload;
484
  const signedAt = Date.now();
485
  const payload = [
486
+ 'v2', deviceId, 'openclaw-control-ui', 'webchat', 'operator',
 
 
 
 
487
  'operator.admin,operator.approvals,operator.pairing',
488
+ String(signedAt), OPENCLAW_TOKEN || '', nonce
 
 
489
  ].join('|');
490
 
491
  const signature = crypto.sign(null, Buffer.from(payload), privateKey);
 
497
  params: {
498
  minProtocol: 3,
499
  maxProtocol: 3,
500
+ client: { id: 'openclaw-control-ui', version: 'dev', platform: 'Win32', mode: 'webchat' },
501
+ device: { id: deviceId, publicKey: toB64Url(rawPublicKey), signature: toB64Url(signature), signedAt, nonce },
 
 
 
 
 
 
 
 
 
 
 
502
  auth: { token: OPENCLAW_TOKEN },
503
  role: 'operator',
504
  scopes: ['operator.admin', 'operator.approvals', 'operator.pairing']
 
551
  function triggerAgent(cid, uid, name, text, isCommand) {
552
  if (!isClawReady) return;
553
  const finalMsg = isCommand ? text : `${name} 说: ${text}`;
 
554
  ws.send(JSON.stringify({
555
  type: 'req',
556
  id: crypto.randomUUID(),
 
562
  deliver: isCommand ? false : undefined
563
  }
564
  }));
565
+ log(`🚀 [发送] ${name}: ${text.slice(0, 30)}...`);
566
  }
567
 
 
 
 
 
568
  async function syncChannel(cid) {
569
  const state = channelRuntime.get(cid);
570
  if (!state || !state.lastMsgId || !isClawReady) return;
 
583
  const { data: msgs } = await webuiApi.get(`/api/v1/channels/${cid}/messages?skip=0&limit=10`);
584
  if (!msgs?.length) throw new Error('No messages');
585
 
 
586
  const webuiFingerprints = new Set(
587
  msgs.map(m => (m.content || m.data?.content || '').trim()).filter(Boolean)
588
  );
 
601
  if (txt && !webuiFingerprints.has(txt)) {
602
  log(`📥 [回填回复] -> ${u.name}`);
603
  await webuiApi.post(`/api/v1/channels/${cid}/messages/post`, {
604
+ content: txt, data: { files: [] }, reply_to_id: null
 
 
605
  });
606
  webuiFingerprints.add(txt);
607
  }
608
  }
609
  }
610
 
 
611
  const sorted = msgs.reverse();
612
  let newMsgs = [];
613
  const startIdx = sorted.findIndex(m => m.id === state.lastMsgId);
 
619
  } else {
620
  if (sorted.length > 0) {
621
  const newestId = sorted[sorted.length - 1].id;
622
+ log(`⚠️ [${state.name}] 锚点丢失,重置 -> ${newestId}`);
623
  state.lastMsgId = newestId;
624
  }
625
  newMsgs = [];