fix(gui): address ds-review-bot findings on image attachments

- dsh web gains --provider/--model: a non-deepseek provider mounts the
  matching pi-ai catalog route (ambient credentials), making image input
  reachable from the shipped Web assembly; requires an explicit --model
- attachment-local syncs the publication directories after the hard-link
  publish so a reported durable reference survives a crash (POSIX; Windows
  relies on filesystem metadata journaling)
- the attachment seam gains storage-free validateImage; the host validates a
  complete multi-image prompt before persisting any member, so one malformed
  image cannot strand valid members as unreferenced objects
- startSession sends before navigating: a rejected first send keeps the empty
  state, its error strip, and the complete draft mounted
- the webserver rejects an undeclared-length body the moment it crosses the
  configured limit instead of draining a potentially endless stream to EOF
This commit is contained in:
creatixchu
2026-07-24 20:55:25 +08:00
parent b868355f01
commit f57a4a044a
23 changed files with 217 additions and 41 deletions

View File

@@ -214,21 +214,19 @@ async function bridge(
}
const chunks: Buffer[] = []
let received = 0
let oversized = false
for await (const chunk of req) {
const buffer = chunk as Buffer
received += buffer.byteLength
if (received > maxRequestBodyBytes) {
oversized = true
chunks.length = 0
continue
// Reject the moment the threshold is crossed: draining a chunked body to
// EOF first would let a client without Content-Length stream
// indefinitely while holding the socket and this request task.
res.writeHead(413, { connection: 'close' })
res.end()
req.destroy()
return
}
if (!oversized) chunks.push(buffer)
}
if (oversized) {
res.writeHead(413)
res.end()
return
chunks.push(buffer)
}
/* v8 ignore next 3 -- `??` arms: node:http always sets url/method on server
requests; the fields are only optional on the client-side IncomingMessage type */

View File

@@ -435,6 +435,27 @@ describe('/api bridge', () => {
expect(status).toBe(413)
})
it('rejects an unterminated chunked body at the threshold without draining to EOF', async () => {
const base = await boot(() => undefined, 8)
const target = new URL(`${base}/api/echo`)
// The client never calls end(): the 413 must arrive the moment the limit
// is crossed, or a hostile stream would hold the socket open forever.
const status = await new Promise<number | undefined>((resolve, reject) => {
const request = httpRequest({
hostname: target.hostname,
port: target.port,
path: target.pathname,
method: 'POST',
}, (response) => {
response.resume()
response.on('end', () => { resolve(response.statusCode) })
})
request.on('error', reject)
request.write('123456789')
})
expect(status).toBe(413)
})
it('relays a bodyless response', async () => {
const base = await boot()
const response = await fetch(`${base}/api/empty`, { method: 'POST' })