Refine message chunking and shell safety guard based on Copilot review
This commit is contained in:
parent
43609ca958
commit
2e84df5e92
4 changed files with 54 additions and 26 deletions
|
|
@ -169,7 +169,14 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err
|
|||
limit = 4000 // Default Telegram limit (~4096)
|
||||
}
|
||||
|
||||
chunks := utils.SplitMessage(msg.Content, limit)
|
||||
// Telegram HTML tags (like <pre><code>) can expand content significantly.
|
||||
// We use a safe headroom for the markdown-based split.
|
||||
effectiveMarkdownLimit := limit - 500
|
||||
if effectiveMarkdownLimit < 500 {
|
||||
effectiveMarkdownLimit = 500
|
||||
}
|
||||
|
||||
chunks := utils.SplitMessage(msg.Content, effectiveMarkdownLimit)
|
||||
|
||||
for i, chunk := range chunks {
|
||||
htmlContent := markdownToTelegramHTML(chunk)
|
||||
|
|
|
|||
|
|
@ -290,9 +290,9 @@ func (t *ExecTool) guardCommand(command, cwd string) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
// Match absolute paths: Unix (starts with / after space/start/quotes) or Windows (X:\)
|
||||
// Match absolute paths: Unix (starts with / after space/start/quotes/=/:) or Windows (X:\)
|
||||
// This regex is careful not to match scoped packages like @mastra/core
|
||||
pathPattern := regexp.MustCompile(`(?:\s|^|["'|&;])(/[^\s\"'|&;]+)|([A-Za-z]:\\[^\\\"'|&;]+)`)
|
||||
pathPattern := regexp.MustCompile(`(?:\s|^|["'|&;=:])(/[^\s\"'|&;:]+)|([A-Za-z]:\\[^\\\"'|&;:]+)`)
|
||||
matches := pathPattern.FindAllStringSubmatch(cmd, -1)
|
||||
|
||||
for _, match := range matches {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,13 @@ import (
|
|||
// but may extend to maxLen when needed. It respects rune counts to ensure multi-byte
|
||||
// characters (like emojis or CJK) are not split in half.
|
||||
func SplitMessage(content string, maxLen int) []string {
|
||||
if content == "" {
|
||||
return nil
|
||||
}
|
||||
if maxLen <= 0 {
|
||||
return []string{content}
|
||||
}
|
||||
|
||||
var messages []string
|
||||
|
||||
// Dynamic buffer: 10% of maxLen, but at least 50 chars if possible
|
||||
|
|
@ -35,7 +42,8 @@ func SplitMessage(content string, maxLen int) []string {
|
|||
}
|
||||
|
||||
// Find natural split point within the effective limit
|
||||
msgEnd := findLastSentenceBoundaryRunes(runes[:effectiveLimit], 300)
|
||||
// We pass the full slice and a limit so findLastSentenceBoundaryRunes can look ahead.
|
||||
msgEnd := findLastSentenceBoundaryRunes(runes, effectiveLimit, 300)
|
||||
if msgEnd <= 0 {
|
||||
msgEnd = findLastNewlineRunes(runes[:effectiveLimit], 200)
|
||||
}
|
||||
|
|
@ -84,7 +92,7 @@ func SplitMessage(content string, maxLen int) []string {
|
|||
if msgEnd > headerEnd+20 {
|
||||
// Find a better split point closer to maxLen
|
||||
innerLimit := maxLen - 5 // Leave room for "\n```"
|
||||
betterEnd := findLastSentenceBoundaryRunes(runes[:innerLimit], 300)
|
||||
betterEnd := findLastSentenceBoundaryRunes(runes, innerLimit, 300)
|
||||
if betterEnd <= headerEnd {
|
||||
betterEnd = findLastNewlineRunes(runes[:innerLimit], 200)
|
||||
}
|
||||
|
|
@ -105,7 +113,7 @@ func SplitMessage(content string, maxLen int) []string {
|
|||
}
|
||||
|
||||
// Otherwise, try to split before the code block starts
|
||||
newEnd := findLastSentenceBoundaryRunes(runes[:unclosedIdx], 300)
|
||||
newEnd := findLastSentenceBoundaryRunes(runes, unclosedIdx, 300)
|
||||
if newEnd <= 0 {
|
||||
newEnd = findLastNewlineRunes(runes[:unclosedIdx], 200)
|
||||
}
|
||||
|
|
@ -210,21 +218,27 @@ func findLastSpaceRunes(runes []rune, searchWindow int) int {
|
|||
return -1
|
||||
}
|
||||
|
||||
// findLastSentenceBoundaryRunes finds the last sentence-ending punctuation
|
||||
// Returns the position after the punctuation or -1 if not found
|
||||
func findLastSentenceBoundaryRunes(runes []rune, searchWindow int) int {
|
||||
searchStart := len(runes) - searchWindow
|
||||
// findLastSentenceBoundaryRunes finds the last sentence-ending punctuation within a limit.
|
||||
// It looks ahead to verify the boundary is real (followed by space, newline, or end of string).
|
||||
func findLastSentenceBoundaryRunes(runes []rune, limit int, searchWindow int) int {
|
||||
if limit > len(runes) {
|
||||
limit = len(runes)
|
||||
}
|
||||
searchStart := limit - searchWindow
|
||||
if searchStart < 0 {
|
||||
searchStart = 0
|
||||
}
|
||||
for i := len(runes) - 1; i >= searchStart; i-- {
|
||||
for i := limit - 1; i >= searchStart; i-- {
|
||||
switch runes[i] {
|
||||
case '.', '!', '?', '。', '!', '?':
|
||||
// Ensure it's the end of a sentence (followed by space, newline, or end of string)
|
||||
if i == len(runes)-1 || runes[i+1] == ' ' || runes[i+1] == '\n' || runes[i+1] == '\t' {
|
||||
// Ensure it's a true boundary:
|
||||
// either it's the very end of the full message,
|
||||
// or the NEXT rune (lookahead) is a space, newline, or tab.
|
||||
if i == len(runes)-1 || (i+1 < len(runes) && (runes[i+1] == ' ' || runes[i+1] == '\n' || runes[i+1] == '\t')) {
|
||||
return i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,12 @@ func TestSplitMessage(t *testing.T) {
|
|||
maxLen: 2000,
|
||||
expectChunks: 1,
|
||||
},
|
||||
{
|
||||
name: "MaxLen 0 (no split)",
|
||||
content: "Hello world",
|
||||
maxLen: 0,
|
||||
expectChunks: 1,
|
||||
},
|
||||
{
|
||||
name: "Simple split regular text",
|
||||
content: longText,
|
||||
|
|
@ -44,11 +50,6 @@ func TestSplitMessage(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "Split at newline",
|
||||
// 1750 chars then newline, then more chars.
|
||||
// Dynamic buffer: 2000 / 10 = 200.
|
||||
// Effective limit: 2000 - 200 = 1800.
|
||||
// Split should happen at newline because it's at 1750 (< 1800).
|
||||
// Total length must > 2000 to trigger split. 1750 + 1 + 300 = 2051.
|
||||
content: strings.Repeat("a", 1750) + "\n" + strings.Repeat("b", 300),
|
||||
maxLen: 2000,
|
||||
expectChunks: 2,
|
||||
|
|
@ -67,11 +68,9 @@ func TestSplitMessage(t *testing.T) {
|
|||
maxLen: 2000,
|
||||
expectChunks: 2,
|
||||
checkContent: func(t *testing.T, chunks []string) {
|
||||
// Check that first chunk ends with closing fence
|
||||
if !strings.HasSuffix(chunks[0], "\n```") {
|
||||
t.Error("First chunk should end with injected closing fence")
|
||||
}
|
||||
// Check that second chunk starts with execution header
|
||||
if !strings.HasPrefix(chunks[1], "```go") {
|
||||
t.Error("Second chunk should start with injected code block header")
|
||||
}
|
||||
|
|
@ -83,12 +82,20 @@ func TestSplitMessage(t *testing.T) {
|
|||
maxLen: 2000,
|
||||
expectChunks: 2,
|
||||
checkContent: func(t *testing.T, chunks []string) {
|
||||
// Just verify we didn't panic and got valid strings.
|
||||
// Go strings are UTF-8, if we split mid-rune it would be bad,
|
||||
// but standard slicing might do that.
|
||||
// Let's assume standard behavior is acceptable or check if it produces invalid rune?
|
||||
if !strings.Contains(chunks[0], "\u4e16") {
|
||||
t.Error("Chunk should contain unicode characters")
|
||||
// Each chunk should stay within max runes limit
|
||||
for i, chunk := range chunks {
|
||||
runeCount := len([]rune(chunk))
|
||||
if runeCount > 2000 {
|
||||
t.Errorf("Chunk %d has too many runes: %d", i, runeCount)
|
||||
}
|
||||
}
|
||||
// Verify total rune count
|
||||
totalRunes := 0
|
||||
for _, chunk := range chunks {
|
||||
totalRunes += len([]rune(chunk))
|
||||
}
|
||||
if totalRunes != 2500 {
|
||||
t.Errorf("Total rune count mismatch. Got %d, want 2500", totalRunes)
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue