Initial commit of akmon project

This commit is contained in:
2026-01-20 08:04:15 +08:00
commit 77a2bab985
1309 changed files with 343305 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
import 'dotenv/config'
import mqtt from 'mqtt'
import { v4 as uuidv4 } from 'uuid'
const MQTT_URL = process.env.MQTT_URL
if (!MQTT_URL) throw new Error('Missing MQTT_URL')
const pattern = process.env.ACK_TOPIC_PATTERN || 'device/+/ack'
const target = process.env.SIM_ACK_TARGET // e.g. userId or deviceId to fill the '+'
const correlationId = process.env.SIM_CORRELATION_ID || uuidv4()
const topic = (() => {
const parts = pattern.split('/')
const tParts = []
let used = false
for (const p of parts) {
if (p === '+') { tParts.push(target || 'test'); used = true } else tParts.push(p)
}
if (pattern.includes('+') && !used) throw new Error('Pattern contains + but could not fill it')
return tParts.join('/')
})()
const payload = JSON.stringify({ correlation_id: correlationId, ok: true, t: Date.now() })
console.log('Publishing ACK', { topic, correlationId })
const client = mqtt.connect(MQTT_URL, { clientId: `ack-sim-${Math.random().toString(16).slice(2)}` })
client.on('connect', () => {
client.publish(topic, payload, { qos: 1 }, (err) => {
if (err) console.error('publish error', err)
else console.log('ACK published')
setTimeout(() => client.end(true, () => process.exit(err ? 1 : 0)), 200)
})
})
client.on('error', (e) => { console.error('mqtt error', e); process.exit(2) })

View File

@@ -0,0 +1,43 @@
import 'dotenv/config'
import { createClient } from '@supabase/supabase-js'
import { v4 as uuidv4 } from 'uuid'
async function main() {
const SUPABASE_URL = process.env.SUPABASE_URL
const SUPABASE_SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY
if (!SUPABASE_URL || !SUPABASE_SERVICE_ROLE_KEY) throw new Error('Missing SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY')
const conversationId = process.env.SIM_CHAT_CONVERSATION_ID
if (!conversationId) throw new Error('SIM_CHAT_CONVERSATION_ID required')
const targetUserId = process.env.SIM_TARGET_USER_ID || ''
const topic = process.env.SIM_TOPIC || (targetUserId ? `device/${targetUserId}/down` : '')
if (!topic) throw new Error('Provide SIM_TOPIC or SIM_TARGET_USER_ID to derive topic')
const correlationId = process.env.SIM_CORRELATION_ID || uuidv4()
const payloadObj = process.env.SIM_PAYLOAD
? JSON.parse(process.env.SIM_PAYLOAD)
: { type: 'ping', t: Date.now(), correlation_id: correlationId }
const payload = typeof payloadObj === 'string' ? payloadObj : JSON.stringify(payloadObj)
const qos = parseInt(process.env.SIM_QOS || '1', 10)
const retain = /^true$/i.test(process.env.SIM_RETAIN || 'false')
const supa = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, { auth: { autoRefreshToken: false, persistSession: false } })
const row = {
conversation_id: conversationId,
target_user_id: targetUserId || null,
topic,
payload,
payload_encoding: 'utf8',
qos,
retain,
status: 'pending',
scheduled_at: new Date().toISOString(),
correlation_id: correlationId
}
const { data, error } = await supa.from('chat_mqtt_downlinks').insert(row).select('*').single()
if (error) throw error
console.log('Inserted chat downlink:', { id: data.id, correlation_id: correlationId, topic })
}
main().catch((e) => { console.error(e); process.exit(1) })

View File

@@ -0,0 +1,38 @@
import 'dotenv/config'
import { createClient } from '@supabase/supabase-js'
import { v4 as uuidv4 } from 'uuid'
const SUPABASE_URL = process.env.SUPABASE_URL
const SUPABASE_SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY
if (!SUPABASE_URL || !SUPABASE_SERVICE_ROLE_KEY) throw new Error('Missing SUPABASE_* envs')
const supa = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, { auth: { autoRefreshToken: false, persistSession: false } })
const topic = process.env.SIM_DOWNLINK_TOPIC || 'device/demo-001/down'
const payload = process.env.SIM_DOWNLINK_PAYLOAD || JSON.stringify({ cmd: 'beep', duration_ms: 500 })
const payloadEncoding = process.env.SIM_DOWNLINK_ENCODING || 'json'
const qos = parseInt(process.env.SIM_DOWNLINK_QOS || '1', 10)
const retain = /^true$/i.test(process.env.SIM_DOWNLINK_RETAIN || 'false')
const row = {
id: uuidv4(),
topic,
payload,
payload_encoding: payloadEncoding,
qos,
retain,
status: 'pending',
scheduled_at: new Date().toISOString(),
created_by: null
}
async function main() {
const { data, error } = await supa.from('mqtt_downlinks').insert(row).select('id, topic, payload_encoding, qos, retain, status').single()
if (error) {
console.error('insert error:', error)
process.exit(1)
}
console.log('inserted:', data)
}
main().catch((e) => { console.error(e); process.exit(1) })

View File

@@ -0,0 +1,52 @@
import 'dotenv/config'
import http from 'http'
const HTTP_PORT = parseInt(process.env.HTTP_PORT || '3000', 10)
const WEBHOOK_TOKEN = process.env.WEBHOOK_TOKEN || ''
const conversationId = process.env.SIM_CONVERSATION_ID || '00000000-0000-0000-0000-000000000000'
const senderId = process.env.SIM_SENDER_ID || '00000000-0000-0000-0000-000000000001'
const env = {
id: process.env.SIM_MESSAGE_ID || undefined,
ts: new Date().toISOString(),
type: 'chat.message',
source: 'webhook.sim',
conversation_id: conversationId,
sender_id: senderId,
content: process.env.SIM_CONTENT || 'hello from webhook',
content_type: 'text',
metadata: { sim: true }
}
const body = JSON.stringify({
event: 'message.publish',
topic: `chat/send/${conversationId}`,
// emulate raw string payload
payload: JSON.stringify(env)
})
const options = {
hostname: '127.0.0.1',
port: HTTP_PORT,
path: '/webhooks/mqtt',
method: 'POST',
headers: {
'content-type': 'application/json',
'content-length': Buffer.byteLength(body),
...(WEBHOOK_TOKEN ? { 'x-webhook-token': WEBHOOK_TOKEN } : {})
}
}
const req = http.request(options, (res) => {
let data = ''
res.on('data', (chunk) => data += chunk)
res.on('end', () => {
console.log('status:', res.statusCode)
console.log('body :', data)
})
})
req.on('error', (err) => console.error('request error:', err))
req.write(body)
req.end()