Merge branch 'dev'
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hermes-web-ui",
|
"name": "hermes-web-ui",
|
||||||
"version": "0.3.7",
|
"version": "0.3.8",
|
||||||
"description": "Web dashboard for Hermes Agent — multi-platform AI chat, session management, scheduled jobs, usage analytics & channel configuration (Telegram, Discord, Slack, WhatsApp)",
|
"description": "Web dashboard for Hermes Agent — multi-platform AI chat, session management, scheduled jobs, usage analytics & channel configuration (Telegram, Discord, Slack, WhatsApp)",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ async function checkLatestVersion(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
const res = await fetch('https://registry.npmjs.org/hermes-web-ui/latest', {
|
const res = await fetch('https://registry.npmjs.org/hermes-web-ui/latest', {
|
||||||
signal: AbortSignal.timeout(5000),
|
signal: AbortSignal.timeout(5000),
|
||||||
|
headers: { 'Cache-Control': 'no-cache' },
|
||||||
})
|
})
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
@@ -83,9 +84,11 @@ export async function bootstrap() {
|
|||||||
app.use(async (ctx, next) => {
|
app.use(async (ctx, next) => {
|
||||||
if (ctx.path === '/api/hermes/update' && ctx.method === 'POST') {
|
if (ctx.path === '/api/hermes/update' && ctx.method === 'POST') {
|
||||||
const isWin = process.platform === 'win32'
|
const isWin = process.platform === 'win32'
|
||||||
|
// Run npm install directly — calling `hermes-web-ui update` would kill this
|
||||||
|
// process (stopDaemon) before the response can be sent to the client.
|
||||||
const cmd = isWin
|
const cmd = isWin
|
||||||
? 'cmd /c hermes-web-ui update'
|
? 'cmd /c npm install -g hermes-web-ui@latest'
|
||||||
: 'hermes-web-ui update'
|
: 'npm install -g hermes-web-ui@latest'
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { execSync } = await import('child_process')
|
const { execSync } = await import('child_process')
|
||||||
@@ -95,6 +98,8 @@ export async function bootstrap() {
|
|||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
})
|
})
|
||||||
ctx.body = { success: true, message: output.trim() }
|
ctx.body = { success: true, message: output.trim() }
|
||||||
|
// Restart the server after response is sent
|
||||||
|
setTimeout(() => process.exit(0), 1000)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
ctx.status = 500
|
ctx.status = 500
|
||||||
ctx.body = { success: false, message: err.stderr || err.message }
|
ctx.body = { success: false, message: err.stderr || err.message }
|
||||||
@@ -168,7 +173,7 @@ export async function bootstrap() {
|
|||||||
|
|
||||||
// Check for updates every 4 hours
|
// Check for updates every 4 hours
|
||||||
checkLatestVersion()
|
checkLatestVersion()
|
||||||
setInterval(checkLatestVersion, 4 * 60 * 60 * 1000)
|
setInterval(checkLatestVersion, 60 * 60 * 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
|
|||||||
@@ -22,31 +22,60 @@ uploadRoutes.post('/upload', async (ctx) => {
|
|||||||
|
|
||||||
await mkdir(config.uploadDir, { recursive: true })
|
await mkdir(config.uploadDir, { recursive: true })
|
||||||
|
|
||||||
// Read raw body
|
// Read raw body as Buffer
|
||||||
const chunks: Buffer[] = []
|
const chunks: Buffer[] = []
|
||||||
for await (const chunk of ctx.req) chunks.push(chunk)
|
for await (const chunk of ctx.req) chunks.push(chunk)
|
||||||
const body = Buffer.concat(chunks).toString('latin1')
|
const raw = Buffer.concat(chunks)
|
||||||
const parts = body.split(boundary).slice(1, -1)
|
const boundaryBuf = Buffer.from(boundary)
|
||||||
|
const parts = splitMultipart(raw, boundaryBuf)
|
||||||
|
|
||||||
const results: { name: string; path: string }[] = []
|
const results: { name: string; path: string }[] = []
|
||||||
|
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
const headerEnd = part.indexOf('\r\n\r\n')
|
const headerEnd = part.indexOf(Buffer.from('\r\n\r\n'))
|
||||||
if (headerEnd === -1) continue
|
if (headerEnd === -1) continue
|
||||||
const header = part.substring(0, headerEnd)
|
const headerBuf = part.subarray(0, headerEnd)
|
||||||
const data = part.substring(headerEnd + 4, part.length - 2)
|
const header = headerBuf.toString('utf-8')
|
||||||
|
const data = part.subarray(headerEnd + 4, part.length - 2)
|
||||||
|
|
||||||
const filenameMatch = header.match(/filename="([^"]+)"/)
|
// Try RFC 5987 filename* first, fall back to filename
|
||||||
if (!filenameMatch) continue
|
let filename = ''
|
||||||
|
const filenameStarMatch = header.match(/filename\*=UTF-8''(.+)/i)
|
||||||
|
if (filenameStarMatch) {
|
||||||
|
filename = decodeURIComponent(filenameStarMatch[1])
|
||||||
|
} else {
|
||||||
|
const filenameMatch = header.match(/filename="([^"]+)"/)
|
||||||
|
if (!filenameMatch) continue
|
||||||
|
filename = filenameMatch[1]
|
||||||
|
}
|
||||||
|
|
||||||
const filename = filenameMatch[1]
|
|
||||||
const ext = filename.includes('.') ? '.' + filename.split('.').pop() : ''
|
const ext = filename.includes('.') ? '.' + filename.split('.').pop() : ''
|
||||||
const savedName = randomBytes(8).toString('hex') + ext
|
const savedName = randomBytes(8).toString('hex') + ext
|
||||||
const savedPath = `${config.uploadDir}/${savedName}`
|
const savedPath = `${config.uploadDir}/${savedName}`
|
||||||
|
|
||||||
await writeFile(savedPath, Buffer.from(data, 'binary'))
|
await writeFile(savedPath, data)
|
||||||
results.push({ name: filename, path: savedPath })
|
results.push({ name: filename, path: savedPath })
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = { files: results }
|
ctx.body = { files: results }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split a multipart Buffer by boundary, returning part Buffers.
|
||||||
|
* Avoids string decoding so multi-byte characters (e.g. Chinese filenames) are preserved.
|
||||||
|
*/
|
||||||
|
function splitMultipart(raw: Buffer, boundary: Buffer): Buffer[] {
|
||||||
|
const parts: Buffer[] = []
|
||||||
|
let start = 0
|
||||||
|
while (true) {
|
||||||
|
const idx = raw.indexOf(boundary, start)
|
||||||
|
if (idx === -1) break
|
||||||
|
if (start > 0) {
|
||||||
|
// Skip the \r\n after boundary
|
||||||
|
const partStart = start + 2
|
||||||
|
parts.push(raw.subarray(partStart, idx))
|
||||||
|
}
|
||||||
|
start = idx + boundary.length
|
||||||
|
}
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user