feat(history): add multi-account and WhatsApp foundation
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
.env
|
||||
data
|
||||
@@ -0,0 +1,23 @@
|
||||
FROM node:20-bookworm-slim
|
||||
|
||||
ENV NODE_ENV=production
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
COPY src ./src
|
||||
RUN mkdir -p /data/auth /data/store \
|
||||
&& chown -R node:node /data /app
|
||||
|
||||
USER node
|
||||
EXPOSE 3200
|
||||
|
||||
HEALTHCHECK --interval=15s --timeout=5s --start-period=20s --retries=5 \
|
||||
CMD node -e "fetch('http://127.0.0.1:3200/health/ready').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"
|
||||
|
||||
CMD ["node", "src/index.js"]
|
||||
Generated
+2761
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "lingxi-whatsapp-gateway",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "灵犀跨境普通 WhatsApp 多账号网关",
|
||||
"scripts": {
|
||||
"start": "node src/index.js",
|
||||
"check": "node --check src/index.js",
|
||||
"test": "node --test test/api.test.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hapi/boom": "10.0.1",
|
||||
"baileys": "7.0.0-rc13",
|
||||
"express": "4.22.2",
|
||||
"express-async-errors": "3.1.1",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"multer": "2.2.0",
|
||||
"pino": "9.6.0",
|
||||
"qrcode": "1.5.4"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,119 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { spawn } from 'node:child_process'
|
||||
import fs from 'node:fs/promises'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import test from 'node:test'
|
||||
|
||||
const token = 'gateway-test-token-12345678901234567890'
|
||||
|
||||
async function waitForReady(baseUrl, child) {
|
||||
for (let attempt = 0; attempt < 80; attempt++) {
|
||||
if (child.exitCode !== null) throw new Error(`网关提前退出,代码 ${child.exitCode}`)
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/health/ready`)
|
||||
if (response.ok) return
|
||||
} catch {
|
||||
// 服务正在启动。
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
}
|
||||
throw new Error('等待 WhatsApp 网关启动超时')
|
||||
}
|
||||
|
||||
test('网关执行健康检查、令牌鉴权和账号 CRUD', async () => {
|
||||
const dataDir = await fs.mkdtemp(path.join(os.tmpdir(), 'lingxi-wa-gateway-'))
|
||||
const port = 32991
|
||||
const baseUrl = `http://127.0.0.1:${port}`
|
||||
const child = spawn(process.execPath, ['src/index.js'], {
|
||||
cwd: path.resolve('.'),
|
||||
env: {
|
||||
...process.env,
|
||||
PORT: String(port),
|
||||
DATA_DIR: dataDir,
|
||||
WHATSAPP_GATEWAY_TOKEN: token,
|
||||
LOG_LEVEL: 'silent',
|
||||
},
|
||||
stdio: 'ignore',
|
||||
})
|
||||
try {
|
||||
await waitForReady(baseUrl, child)
|
||||
const unauthorized = await fetch(`${baseUrl}/accounts`)
|
||||
assert.equal(unauthorized.status, 401)
|
||||
|
||||
const headers = {
|
||||
authorization: `Bearer ${token}`,
|
||||
'content-type': 'application/json',
|
||||
}
|
||||
const created = await fetch(`${baseUrl}/accounts`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ phoneNumber: '15550008888', name: 'Gateway Test' }),
|
||||
})
|
||||
assert.equal(created.status, 201)
|
||||
const account = (await created.json()).data
|
||||
assert.equal(account.phoneNumber, '15550008888')
|
||||
assert.equal(account.status, 'created')
|
||||
|
||||
const duplicate = await fetch(`${baseUrl}/accounts`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ phoneNumber: '15550008888' }),
|
||||
})
|
||||
assert.equal(duplicate.status, 409)
|
||||
|
||||
const fetched = await fetch(`${baseUrl}/accounts/${account.id}`, { headers })
|
||||
assert.equal(fetched.status, 200)
|
||||
|
||||
const webhookSecret = 'webhook-test-secret-that-must-be-encrypted'
|
||||
const webhook = await fetch(`${baseUrl}/accounts/${account.id}/webhook`, {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
url: 'http://127.0.0.1:39999/webhook',
|
||||
secret: webhookSecret,
|
||||
active: true,
|
||||
}),
|
||||
})
|
||||
assert.equal(webhook.status, 200)
|
||||
assert.equal((await webhook.json()).data.webhookSecretConfigured, true)
|
||||
const persistedAccounts = await fs.readFile(path.join(dataDir, 'accounts.json'), 'utf8')
|
||||
assert.equal(persistedAccounts.includes(webhookSecret), false)
|
||||
|
||||
const metrics = await fetch(`${baseUrl}/metrics`, { headers })
|
||||
assert.equal(metrics.status, 200)
|
||||
assert.equal((await metrics.json()).data.accounts.total, 1)
|
||||
|
||||
const invalidProxy = await fetch(`${baseUrl}/accounts/${account.id}/proxy`, {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify({ url: 'socks5://127.0.0.1:1080' }),
|
||||
})
|
||||
assert.equal(invalidProxy.status, 400)
|
||||
|
||||
const unsafeReauthorize = await fetch(`${baseUrl}/accounts/${account.id}/reauthorize`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ confirm: 'wrong-account-id' }),
|
||||
})
|
||||
assert.equal(unsafeReauthorize.status, 400)
|
||||
|
||||
const backup = await fetch(`${baseUrl}/accounts/${account.id}/backup`, { headers })
|
||||
assert.equal(backup.status, 200)
|
||||
const backupData = (await backup.json()).data
|
||||
assert.equal(backupData.accountId, account.id)
|
||||
assert.match(backupData.blob, /^v1\./)
|
||||
|
||||
const removed = await fetch(`${baseUrl}/accounts/${account.id}`, {
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
})
|
||||
assert.equal(removed.status, 200)
|
||||
const missing = await fetch(`${baseUrl}/accounts/${account.id}`, { headers })
|
||||
assert.equal(missing.status, 404)
|
||||
} finally {
|
||||
child.kill('SIGTERM')
|
||||
await new Promise((resolve) => child.once('exit', resolve))
|
||||
await fs.rm(dataDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user