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

@@ -10,7 +10,7 @@ The TUI surface:
- tells the agent where its own source lives: after boot it adds a prompt section naming this harness checkout, resolved from the launcher's real path so it holds under a PATH symlink and an arbitrary cwd, so the self-referential `cordis` toolset can read and modify it;
- applies the personal overlay from `~/.dsh` (see [app-boot's Personal config](../../packages/ui/app-boot/README.md#personal-config)): `.env` fills environment gaps (ambient > project `.env` > personal `.env`), `config.yaml` patches the booted tree.
The Web surface treats its invoking directory as the default project, loads applicable `AGENTS.md`/`CLAUDE.md` instructions into each agent-loop request prefix with a 65,536-byte render budget, and opts into first-message model titles. The headless surface retains deterministic fallback titles without making the auxiliary title-model request.
The Web surface treats its invoking directory as the default project, loads applicable `AGENTS.md`/`CLAUDE.md` instructions into each agent-loop request prefix with a 65,536-byte render budget, and opts into first-message model titles. `dsh web --provider <name> --model <id>` selects the default agent route; a non-`deepseek` provider additionally mounts that pi-ai catalog route with the provider's ambient credentials (e.g. `ANTHROPIC_API_KEY`), which is how image input reaches a visual-capable model, and requires an explicit `--model`. The headless surface retains deterministic fallback titles without making the auxiliary title-model request.
## Install (developer machine)

View File

@@ -49,6 +49,8 @@ export async function runWeb(argv: string[]): Promise<void> {
host: { type: 'string', default: LOOPBACK_HOST },
port: { type: 'string', default: '3080' },
'max-request-body-bytes': { type: 'string' },
provider: { type: 'string' },
model: { type: 'string' },
dev: { type: 'boolean', default: false },
},
allowPositionals: false,
@@ -77,12 +79,28 @@ export async function runWeb(argv: string[]): Promise<void> {
process.exit(1)
}
// A missing DEEPSEEK_API_KEY throws here (plugin load is fail-loud, uncaught by design).
// Default routing: --provider deepseek (implicit) keeps the DeepSeek-only
// assembly; any other --provider additionally mounts that pi-ai catalog
// route (credentials via the provider's ambient discovery, e.g.
// ANTHROPIC_API_KEY) so visual-capable models are reachable from the shipped
// Web app. A non-default provider requires an explicit --model — this shell
// has no evidence for inventing another provider's default.
const provider = values.provider ?? 'deepseek'
if (provider !== 'deepseek' && values.model === undefined) {
process.stderr.write(`dsh web: --provider ${provider} requires an explicit --model\n`)
process.exit(1)
}
// A missing DEEPSEEK_API_KEY throws here (plugin load is fail-loud, uncaught
// by design); an unknown --provider fails the pi-ai catalog check the same way.
const host = await startHost({
boot: {
persistenceRoot: './.sessions',
workspaceContext: { maxBytes: 65_536 },
sessionTitleLlm: true,
...provider === 'deepseek' ? {} : { piAiProviders: [{ provider }] },
...values.provider === undefined ? {} : { provider: values.provider },
...values.model === undefined ? {} : { model: values.model },
},
})
const attachments = host.ctx.get('attachments')