feat(api): add resource_urls=public so API responses return directly loadable file URLs - #51
Draft
lyingbug wants to merge 5 commits into
Draft
feat(api): add resource_urls=public so API responses return directly loadable file URLs#51lyingbug wants to merge 5 commits into
lyingbug wants to merge 5 commits into
Conversation
…nal/storageurl The IM channels rewrite resource:// and provider:// references into loadable HTTP URLs because IM clients cannot attach WeKnora credentials to an image fetch. That logic was private to internal/im, so no other surface could reuse it. Move the reference pattern, HTTP-result guard, tenant-aware storage backend resolver and streaming holdback helpers into internal/storageurl, and add a Rewriter that memoises resolutions (a resource:// handle costs one access-grant row per resolution). internal/im now delegates; behaviour is unchanged. Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
…URLs API responses reference stored files as opaque resource:// handles, so an integrating app had to make a second authenticated call to /files for every image before it could render anything. Add an opt-in that resolves those references server-side into time-limited HTTP(S) URLs, using the same mechanism the IM channels already rely on: - per request: ?resource_urls=public (default: handle, unchanged) - per deployment: RESOURCE_URL_MODE=public Applied to the chat SSE endpoints (knowledge-chat, agent-chat, continue-stream), message history load, and knowledge-search. Streamed answers buffer a trailing incomplete reference so a handle split across two deltas is still rewritten. References that cannot become an HTTP URL (for example local storage with no APP_EXTERNAL_URL) stay handles, so clients keep the /files fallback. Embed channels are deliberately excluded: their visitors are anonymous. SSE payloads share SearchResult pointers and metadata maps with the stream replay buffer and the message being persisted, so those are rewritten as copies. Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
…nd SSE wire Includes an end-to-end ContinueStream test that asserts the full SSE wire output. It caught a leak: the references event carries its results twice, and the copy in Data holds the typed []*SearchResult that CopyData did not traverse, so handles reached a caller that asked for public URLs. CopyData now handles the typed slice, []string and map[string]string forms. Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
Adds a cross-cutting "文件与图片引用" section to docs/api/README.md covering the parameter, the deployment default, the endpoints it applies to, and the two caveats that matter in practice: it needs APP_EXTERNAL_URL (or a publicly reachable storage backend) to produce a link at all, and the links it produces are time-limited but anonymously readable. Also annotates the affected endpoints for Swagger and refreshes the generated files for those parameters only, leaving unrelated drift in the committed swagger output alone. Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
In public resource URL mode, building the payload consumes the chunk into the holdback buffer, so returning between build and write would drop it. Reusing emitStreamEvent also removes the duplicated flush-before-completion logic. Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
API responses reference stored files as opaque
resource://<handle>values, e.g.. A browser can't load that, so an integrating app has to make a second authenticated call toGET /files?file_path=…for every image before it can render anything.This adds an opt-in that resolves those references server-side into time-limited HTTP(S) URLs, reusing the exact mechanism the IM channels already rely on (
FileService.GetFileURL→ object-store presigned URL, orAPP_EXTERNAL_URL+/r/<token>).?resource_urls=publicRESOURCE_URL_MODE=publicresource_urlsacceptshandle(default, unchanged) orpublic; anything else returns400. An explicit parameter always beats the deployment default, so?resource_urls=handlestill opts back out.Endpoints:
POST /knowledge-chat/{session_id},POST /agent-chat/{session_id},GET /sessions/continue-stream/{session_id},GET /messages/{session_id}/load,POST /knowledge-search.Rewriting covers the answer body,
knowledge_references(includingimage_info), agent steps and tool results, and message image attachments.Design notes
internal/storageurl(new). The IM channels already did this translation, but the code was private tointernal/imso nothing else could reuse it. The first commit moves the reference pattern, the HTTP-result guard, the tenant-aware backend resolver and the streaming holdback helpers into a shared package;internal/imnow delegates with no behaviour change. The newRewritermemoises resolutions, because eachresource://resolution writes an access-grant row and a streamed answer repeats the same image across many chunks.completemarker. The buffer is bounded so a never-closed![can't buffer a whole answer.*SearchResultpointers and metadata maps with the stream replay buffer and the assistant message being persisted, so those are rewritten as copies. The end-to-end test caught a real leak here: thereferencesevent carries its results twice, and the copy indataholds a typed[]*SearchResultthe first implementation didn't traverse.localstorage with noAPP_EXTERNAL_URL), the reference stays a handle so clients keep the/filesfallback./fileswhich rejects KB-restricted API keys. That gate exists because/filestakes an arbitrary caller-supplied path it cannot bind to a KB allow-list; here the server — not the client — chooses which resources get a URL, from a response the caller is already authorized to receive.Security trade-off (documented)
publicmode issues short-lived anonymously-readable URLs (WeKnora grants 2h, MinIO presigned 24h) for each referenced file. This is the same exposure the IM channels have always had, it is opt-in, and it is called out in both.env.exampleanddocs/api/README.md.Type of Change
Not a breaking change: the default
handlemode leaves every response byte-identical.Related Issue
Addresses the request that image references be returned as directly loadable links instead of
resource://, so integrating apps don't need a separate/filescall.Testing
Verification used the real gin handler and the real SSE writer, so the assertions are made against actual wire bytes.
Default mode is unchanged,
publicreturns loadable URLs, bad values are rejected — captured fromGET /api/v1/sessions/continue-stream/...driven throughContinueStream:Note the handle was deliberately split across two answer deltas (
resource://xifDo7+NTSL300Lp1goVutw) and comes back as one intact Markdown image, which is what the holdback buffer is for.Commands run:
New tests:
internal/storageurl/{storageurl,stream,request}_test.go,internal/handler/message_resource_urls_test.go,internal/handler/session/{resource_urls,continue_stream_resource_urls}_test.go. They cover every reference form, memoisation, the no-op paths (already-HTTP, resolve failure, unknown backend, non-HTTP result), split-reference holdback and its bound, stream-key independence, copy-not-mutate for references and metadata, mode precedence, and the400.go test ./client/... ./cli/...fails withdirectory prefix client does not contain main moduleonorigin/maintoo — those are separate Go modules, not a regression.gofmt -lalso reports pre-existing drift in files this PR does not touch (internal/handler/dto/auth.go,internal/handler/session/attachment_processor.go, and others).Checklist
git diff --check origin/main...HEADpassesgolangci-lint run --new-from-rev=origin/main ./...)docs/, Swagger annotations, etc.)Docs updated: new "文件与图片引用" section in
docs/api/README.md, pointers fromchat.md/message.md/knowledge-search.md,RESOURCE_URL_MODEin.env.example, and Swagger annotations on all five endpoints. The generateddocs/{docs.go,swagger.json,swagger.yaml}were updated for these parameters only — the committed output has unrelated drift from a stale checkout, and a fullmake docsregeneration was deliberately left out of this diff.Screenshots / Recordings
Not applicable — backend-only change with no UI surface. The SSE wire output above is the user-visible artifact.