7fd566bc15
IM platforms (Feishu, Slack, Telegram, DingTalk, Mattermost, WeCom) cannot render provider:// URLs (local://, minio://, s3://, etc.) that appear in LLM answers containing knowledge base images. The web frontend handles these via the authenticated /files endpoint, but IM clients need publicly resolvable HTTP URLs. Changes: - Add HMAC-SHA256 presigned URL utility (internal/utils/presign.go) for generating time-limited, signature-verified file access URLs - Add GET /api/v1/files/presigned endpoint that serves files without session auth, verified by HMAC signature and expiry - Update localFileService.GetFileURL() to return presigned HTTP URLs when APP_EXTERNAL_URL is configured (cloud backends already return presigned URLs via their SDKs) - Add IM content rewriting pipeline: strip <image> XML tags, strip citation tags, rewrite storage URLs to HTTP — applied at all IM output points (streaming flush, non-streaming reply, fallback) - Add holdback buffer in streaming flush to prevent URL/tag splitting across chunk boundaries Closes #1058
199 lines
4.1 KiB
Go
199 lines
4.1 KiB
Go
package im
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestStripIMCitationTags(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
want string
|
|
}{
|
|
{"no tags", "hello world", "hello world"},
|
|
{"kb tag", `text <kb id="1" title="doc"/> more`, "text more"},
|
|
{"web tag", `see <web url="http://x.com"/> here`, "see here"},
|
|
{"multiple", `<kb id="1"/><web url="x"/>text`, "text"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert.Equal(t, tt.want, stripIMCitationTags(tt.in))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStripImageXMLTags(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
want string
|
|
}{
|
|
{
|
|
"with image_original",
|
|
`<image url="local://1/img.png">
|
|
<image_original></image_original>
|
|
<image_caption>a cat</image_caption>
|
|
</image>`,
|
|
"",
|
|
},
|
|
{
|
|
"without image_original",
|
|
`<image url="local://1/img.png">
|
|
<image_caption>a cat</image_caption>
|
|
</image>`,
|
|
"",
|
|
},
|
|
{
|
|
"no image tags",
|
|
"just plain text ",
|
|
"just plain text ",
|
|
},
|
|
{
|
|
"mixed content",
|
|
`before <image url="x"><image_original></image_original></image> after`,
|
|
"before  after",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert.Equal(t, tt.want, stripImageXMLTags(tt.in))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFindIncompleteStorageURL(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
want int // expected return; -1 means no match expected
|
|
}{
|
|
{
|
|
"complete URL terminated by )",
|
|
"",
|
|
// The URL `local://1/abc/img.png` ends with `)` which is a terminator,
|
|
// but the regex [^\s)\]>"]* matches up to `)` — the `)` is NOT included.
|
|
// So the URL portion is `local://1/abc/img.png` and `)` terminates it.
|
|
// The match does NOT reach end of string → should return -1.
|
|
-1,
|
|
},
|
|
{
|
|
"complete URL terminated by space",
|
|
"text local://1/abc/img.png more text",
|
|
-1,
|
|
},
|
|
{
|
|
"truncated URL at end",
|
|
"text  {
|
|
got := findIncompleteStorageURL(tt.in)
|
|
assert.Equal(t, tt.want, got, "findIncompleteStorageURL(%q)", tt.in)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFindIncompleteXMLTag(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
want int
|
|
}{
|
|
{
|
|
"complete tag",
|
|
`text <kb id="1"/> more`,
|
|
-1,
|
|
},
|
|
{
|
|
"incomplete kb tag",
|
|
`text <kb id="1`,
|
|
5,
|
|
},
|
|
{
|
|
"incomplete image tag",
|
|
`text <image url="local://1/img`,
|
|
5,
|
|
},
|
|
{
|
|
"incomplete image_original",
|
|
`<image_original>, the content is truncated
|
|
},
|
|
{
|
|
"incomplete opening image_original",
|
|
`text <image_original`,
|
|
5,
|
|
},
|
|
{
|
|
"no XML",
|
|
"just plain text",
|
|
-1,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := findIncompleteXMLTag(tt.in)
|
|
assert.Equal(t, tt.want, got, "findIncompleteXMLTag(%q)", tt.in)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHoldbackCutoff(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
want int // expected cutoff (len(in) means no holdback)
|
|
}{
|
|
{
|
|
"no holdback needed",
|
|
"plain text with complete  content",
|
|
// URL is terminated by `)` then space → no holdback
|
|
-1, // placeholder, computed below
|
|
},
|
|
{
|
|
"truncated URL",
|
|
"text  {
|
|
got := holdbackCutoff(tt.in)
|
|
if tt.want == -1 {
|
|
assert.Equal(t, len(tt.in), got, "holdbackCutoff(%q) should be len(in)", tt.in)
|
|
} else {
|
|
assert.Equal(t, tt.want, got, "holdbackCutoff(%q)", tt.in)
|
|
}
|
|
})
|
|
}
|
|
}
|