* fix(codex): avoid closing events channel before readLoop exits Close() previously closed cs.events after an 8s wait even when wg.Wait had not finished, while readLoop could still send in flushPendingAsText (turn.completed), causing "panic: send on closed channel" after /stop then /new. Defer channel close until readLoop completes. Fixes https://github.com/chenhg5/cc-connect/issues/281 * fix(telegram): UTF-8-safe bot command descriptions and table rows (#119) - Truncate BotCommand.Description by runes (Telegram 256-char limit); byte slicing broke CJK and caused API errors. - Make truncateForLog rune-aware for debug prefixes. - Parse markdown table rows without byte-slicing the outer pipes. Fixes #119 Made-with: Cursor --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
235e54db25
commit
1beffe3f25
4 files changed
+103
-36
No files matched your search
+29
-17
@@ -24,18 +24,18 @@ import (
|
||||
// codexSession manages a multi-turn Codex conversation.
|
||||
// First Send() uses `codex exec`, subsequent ones use `codex exec resume <threadID>`.
|
||||
type codexSession struct {
|
||||
workDir string
|
||||
model string
|
||||
effort string
|
||||
mode string
|
||||
extraEnv []string
|
||||
events chan core.Event
|
||||
threadID atomic.Value // stores string — Codex thread_id
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
alive atomic.Bool
|
||||
closeOnce sync.Once
|
||||
workDir string
|
||||
model string
|
||||
effort string
|
||||
mode string
|
||||
extraEnv []string
|
||||
events chan core.Event
|
||||
threadID atomic.Value // stores string — Codex thread_id
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
alive atomic.Bool
|
||||
closeOnce sync.Once
|
||||
|
||||
pendingMsgs []string // buffered agent_message texts awaiting classification
|
||||
}
|
||||
@@ -535,13 +535,25 @@ func (cs *codexSession) Close() error {
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
// readLoop has exited; safe to close the events channel.
|
||||
cs.closeOnce.Do(func() {
|
||||
close(cs.events)
|
||||
})
|
||||
return nil
|
||||
case <-time.After(8 * time.Second):
|
||||
slog.Warn("codexSession: close timed out, abandoning wg.Wait")
|
||||
// Do not close(cs.events) here: readLoop may still be in handleEvent
|
||||
// (e.g. turn.completed -> flushPendingAsText) and would panic on send.
|
||||
// See https://github.com/chenhg5/cc-connect/issues/281
|
||||
slog.Warn("codexSession: close timed out, deferring events channel close until readLoop exits",
|
||||
"wait", 8*time.Second)
|
||||
go func() {
|
||||
<-done
|
||||
cs.closeOnce.Do(func() {
|
||||
close(cs.events)
|
||||
})
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
cs.closeOnce.Do(func() {
|
||||
close(cs.events)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractItemText extracts text from an item's array field (e.g. "summary" or "content").
|
||||
|
||||
+17
-13
@@ -69,7 +69,11 @@ func MarkdownToSimpleHTML(md string) string {
|
||||
if reTableSep.MatchString(tl) {
|
||||
b.WriteString("——————————")
|
||||
} else {
|
||||
inner := tl[1 : len(tl)-1]
|
||||
tlTrim := strings.TrimSpace(tl)
|
||||
inner := tlTrim
|
||||
if strings.HasPrefix(tlTrim, "|") && strings.HasSuffix(tlTrim, "|") && len(tlTrim) >= 2 {
|
||||
inner = strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(tlTrim, "|"), "|"))
|
||||
}
|
||||
cells := strings.Split(inner, "|")
|
||||
for k := range cells {
|
||||
cells[k] = strings.TrimSpace(cells[k])
|
||||
@@ -193,18 +197,18 @@ func MarkdownToSimpleHTML(md string) string {
|
||||
}
|
||||
|
||||
var (
|
||||
reInlineCodeHTML = regexp.MustCompile("`([^`]+)`")
|
||||
reBoldItalicHTML = regexp.MustCompile(`\*\*\*(.+?)\*\*\*`)
|
||||
reBoldAstHTML = regexp.MustCompile(`\*\*(.+?)\*\*`)
|
||||
reBoldUndHTML = regexp.MustCompile(`__(.+?)__`)
|
||||
reItalicAstHTML = regexp.MustCompile(`(?:^|[^*])\*([^*]+?)\*(?:[^*]|$)`)
|
||||
reStrikeHTML = regexp.MustCompile(`~~(.+?)~~`)
|
||||
reLinkHTML = regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`)
|
||||
reWikilinkHTML = regexp.MustCompile(`\[\[([^\]|]+)\|([^\]]+)\]\]|\[\[([^\]]+)\]\]`)
|
||||
reUnorderedList = regexp.MustCompile(`^(\s*)[-*]\s+(.*)$`)
|
||||
reOrderedList = regexp.MustCompile(`^(\s*)\d+\.\s+(.*)$`)
|
||||
reTableSep = regexp.MustCompile(`^\|[\s:|-]+\|$`)
|
||||
reCallout = regexp.MustCompile(`^\[!(\w+)\]\s*(.*)$`)
|
||||
reInlineCodeHTML = regexp.MustCompile("`([^`]+)`")
|
||||
reBoldItalicHTML = regexp.MustCompile(`\*\*\*(.+?)\*\*\*`)
|
||||
reBoldAstHTML = regexp.MustCompile(`\*\*(.+?)\*\*`)
|
||||
reBoldUndHTML = regexp.MustCompile(`__(.+?)__`)
|
||||
reItalicAstHTML = regexp.MustCompile(`(?:^|[^*])\*([^*]+?)\*(?:[^*]|$)`)
|
||||
reStrikeHTML = regexp.MustCompile(`~~(.+?)~~`)
|
||||
reLinkHTML = regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`)
|
||||
reWikilinkHTML = regexp.MustCompile(`\[\[([^\]|]+)\|([^\]]+)\]\]|\[\[([^\]]+)\]\]`)
|
||||
reUnorderedList = regexp.MustCompile(`^(\s*)[-*]\s+(.*)$`)
|
||||
reOrderedList = regexp.MustCompile(`^(\s*)\d+\.\s+(.*)$`)
|
||||
reTableSep = regexp.MustCompile(`^\|[\s:|-]+\|$`)
|
||||
reCallout = regexp.MustCompile(`^\[!(\w+)\]\s*(.*)$`)
|
||||
)
|
||||
|
||||
// convertInlineHTML converts inline Markdown formatting to Telegram-compatible HTML.
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf16"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/chenhg5/cc-connect/core"
|
||||
|
||||
@@ -1232,10 +1233,29 @@ func (p *Platform) StartTyping(ctx context.Context, rctx any) (stop func()) {
|
||||
}
|
||||
|
||||
func truncateForLog(s string, maxLen int) string {
|
||||
if len(s) <= maxLen {
|
||||
if maxLen <= 0 {
|
||||
return ""
|
||||
}
|
||||
if utf8.RuneCountInString(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen] + "..."
|
||||
r := []rune(s)
|
||||
if len(r) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return string(r[:maxLen]) + "..."
|
||||
}
|
||||
|
||||
// truncateTelegramBotDescription enforces Telegram's 256-character limit for
|
||||
// BotCommand.Description. Byte slicing breaks UTF-8 for CJK text and triggers
|
||||
// "text must be encoded in UTF-8" from the API (#119).
|
||||
func truncateTelegramBotDescription(s string) string {
|
||||
const max = 256
|
||||
if utf8.RuneCountInString(s) <= max {
|
||||
return s
|
||||
}
|
||||
r := []rune(s)
|
||||
return string(r[:253]) + "..."
|
||||
}
|
||||
|
||||
func (p *Platform) Stop() error {
|
||||
@@ -1276,10 +1296,7 @@ func (p *Platform) RegisterCommands(commands []core.BotCommandInfo) error {
|
||||
continue
|
||||
}
|
||||
seen[cmd] = true
|
||||
desc := c.Description
|
||||
if len(desc) > 256 {
|
||||
desc = desc[:253] + "..."
|
||||
}
|
||||
desc := truncateTelegramBotDescription(c.Description)
|
||||
tgCommands = append(tgCommands, tgbotapi.BotCommand{
|
||||
Command: cmd,
|
||||
Description: desc,
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/chenhg5/cc-connect/core"
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
@@ -760,6 +761,39 @@ func TestSendAudioReturnsConversionErrorForWAV(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncateTelegramBotDescription_UTF8Safe(t *testing.T) {
|
||||
t.Parallel()
|
||||
cjk := strings.Repeat("你", 200)
|
||||
out := truncateTelegramBotDescription(cjk)
|
||||
if !utf8.ValidString(out) {
|
||||
t.Fatal("invalid UTF-8 from CJK description")
|
||||
}
|
||||
if got, max := utf8.RuneCountInString(out), 256; got > max {
|
||||
t.Fatalf("rune count %d > %d", got, max)
|
||||
}
|
||||
|
||||
long := strings.Repeat("b", 260)
|
||||
out2 := truncateTelegramBotDescription(long)
|
||||
if want := 256; utf8.RuneCountInString(out2) != want {
|
||||
t.Fatalf("ascii truncation: got %d runes want %d", utf8.RuneCountInString(out2), want)
|
||||
}
|
||||
if !utf8.ValidString(out2) {
|
||||
t.Fatal("invalid UTF-8 after ascii truncation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncateForLog_UTF8Safe(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := strings.Repeat("世", 50) // 50 runes
|
||||
out := truncateForLog(s, 10)
|
||||
if !utf8.ValidString(out) {
|
||||
t.Fatal("invalid UTF-8")
|
||||
}
|
||||
if utf8.RuneCountInString(out) != 13 { // 10 + "..."
|
||||
t.Fatalf("got %d runes", utf8.RuneCountInString(out))
|
||||
}
|
||||
}
|
||||
|
||||
func newTelegramTestPlatform(t *testing.T, handler func(http.ResponseWriter, *http.Request)) *Platform {
|
||||
t.Helper()
|
||||
|
||||
|
||||
Reference in new issue
Block a user