start-relay-client.js 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. #!/usr/bin/env node
  2. 'use strict';
  3. const crypto = require('crypto');
  4. const path = require('path');
  5. const { pathToFileURL } = require('url');
  6. const { readQiweiGuid } = require('../mcp/src/core/credentials');
  7. const {
  8. getRelayBaseUrl,
  9. getTenantApiSecret,
  10. getRelayPrivateKey,
  11. getTenantId,
  12. getRelayDeviceGuid,
  13. } = require('../mcp/src/core/relay-config');
  14. function runtimeModuleUrl(relativePath) {
  15. return pathToFileURL(path.join(__dirname, '..', 'runtime', 'callback-service', 'src', relativePath)).href;
  16. }
  17. function decryptPayload(encryptedPayload, privateKeyPem) {
  18. const key = privateKeyPem && typeof privateKeyPem === 'object' && privateKeyPem.type === 'private'
  19. ? privateKeyPem
  20. : crypto.createPrivateKey(privateKeyPem);
  21. if (String(encryptedPayload).startsWith('v2:')) {
  22. const envelope = JSON.parse(Buffer.from(String(encryptedPayload).slice(3), 'base64').toString('utf8'));
  23. const aesKey = crypto.privateDecrypt(
  24. { key, oaepHash: 'sha256' },
  25. Buffer.from(envelope.key, 'base64'),
  26. );
  27. const decipher = crypto.createDecipheriv('aes-256-gcm', aesKey, Buffer.from(envelope.iv, 'base64'));
  28. decipher.setAuthTag(Buffer.from(envelope.tag, 'base64'));
  29. return Buffer.concat([
  30. decipher.update(Buffer.from(envelope.ciphertext, 'base64')),
  31. decipher.final(),
  32. ]).toString('utf8');
  33. }
  34. return crypto.privateDecrypt(
  35. { key, oaepHash: 'sha256' },
  36. Buffer.from(encryptedPayload, 'base64'),
  37. ).toString('utf8');
  38. }
  39. async function ackEvents(baseUrl, apiSecret, guid, eventIds) {
  40. if (!eventIds.length) return 0;
  41. const response = await fetch(`${String(baseUrl).replace(/\/$/, '')}/api/relay/ack`, {
  42. method: 'POST',
  43. headers: {
  44. 'Content-Type': 'application/json',
  45. Authorization: `Bearer ${apiSecret}`,
  46. },
  47. body: JSON.stringify({ guid, eventIds }),
  48. });
  49. if (!response.ok) throw new Error(`ACK failed: ${response.status} ${await response.text()}`);
  50. const data = await response.json();
  51. return Number(data.ackedCount) || eventIds.length;
  52. }
  53. function resolveGuid() {
  54. return process.argv[2] || process.env.RELAY_DEVICE_GUID || getRelayDeviceGuid() || readQiweiGuid() || '';
  55. }
  56. function resolveRuntimeConfig() {
  57. return {
  58. baseUrl: getRelayBaseUrl(),
  59. apiSecret: getTenantApiSecret(),
  60. privateKey: getRelayPrivateKey(),
  61. tenantId: getTenantId(),
  62. guid: resolveGuid(),
  63. };
  64. }
  65. async function runPollOnce(baseUrl, apiSecret, guid, privateKey) {
  66. const { EnterpriseRelayRuntime } = await import(runtimeModuleUrl('enterprise-relay-client.mjs'));
  67. const runtime = new EnterpriseRelayRuntime({
  68. config: {
  69. batchSize: 100,
  70. waitMs: 30000,
  71. retryMinMs: 1000,
  72. retryMaxMs: 60000,
  73. },
  74. });
  75. const result = await runtime.pollOnce({ baseUrl, apiSecret, guid, privateKey, tenantId: 'compat' }, {
  76. throwOnFailure: false,
  77. });
  78. return {
  79. received: result.received,
  80. acked: result.acked,
  81. failed: result.failed,
  82. };
  83. }
  84. async function main() {
  85. const { startRuntime } = await import(runtimeModuleUrl('index.mjs'));
  86. const runtime = await startRuntime({
  87. forceMode: 'enterprise',
  88. guid: process.argv[2] || '',
  89. dashboard: false,
  90. });
  91. process.stdout.write(`[RelayClient] ESM runtime started. pid=${process.pid}, mode=${runtime.mode}\n`);
  92. let shuttingDown = false;
  93. const shutdown = signal => {
  94. if (shuttingDown) return;
  95. shuttingDown = true;
  96. runtime.stop(signal).finally(() => process.exit(0));
  97. };
  98. process.once('SIGINT', () => shutdown('SIGINT'));
  99. process.once('SIGTERM', () => shutdown('SIGTERM'));
  100. }
  101. if (require.main === module) {
  102. main().catch(error => {
  103. process.stderr.write(`[RelayClient] ${error.message}\n`);
  104. process.exit(1);
  105. });
  106. }
  107. module.exports = {
  108. decryptPayload,
  109. ackEvents,
  110. runPollOnce,
  111. resolveGuid,
  112. resolveRuntimeConfig,
  113. };