初始化仓库
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,84 @@
|
||||
package discord
|
||||
|
||||
import "strings"
|
||||
|
||||
// wrapTablesInCodeBlocks detects markdown tables (contiguous pipe-delimited
|
||||
// lines that include a separator row like |---|---|) outside code blocks, and
|
||||
// wraps them with ``` so Discord renders them in monospace.
|
||||
func wrapTablesInCodeBlocks(s string) string {
|
||||
lines := strings.Split(s, "\n")
|
||||
var result []string
|
||||
inCodeBlock := false
|
||||
|
||||
i := 0
|
||||
for i < len(lines) {
|
||||
trimmed := strings.TrimSpace(lines[i])
|
||||
|
||||
if strings.HasPrefix(trimmed, "```") {
|
||||
inCodeBlock = !inCodeBlock
|
||||
result = append(result, lines[i])
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
if inCodeBlock {
|
||||
result = append(result, lines[i])
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
if isPipeRow(trimmed) {
|
||||
tableStart := i
|
||||
for i < len(lines) {
|
||||
t := strings.TrimSpace(lines[i])
|
||||
if !isPipeRow(t) {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
tableLines := lines[tableStart:i]
|
||||
if hasTableSeparator(tableLines) {
|
||||
result = append(result, "```")
|
||||
result = append(result, tableLines...)
|
||||
result = append(result, "```")
|
||||
} else {
|
||||
result = append(result, tableLines...)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, lines[i])
|
||||
i++
|
||||
}
|
||||
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
func isPipeRow(trimmed string) bool {
|
||||
return len(trimmed) >= 3 &&
|
||||
strings.HasPrefix(trimmed, "|") &&
|
||||
strings.HasSuffix(trimmed, "|")
|
||||
}
|
||||
|
||||
// hasTableSeparator checks if any line in the block looks like | --- | --- |.
|
||||
func hasTableSeparator(lines []string) bool {
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
inner := strings.Trim(trimmed, "|")
|
||||
cells := strings.Split(inner, "|")
|
||||
allDash := true
|
||||
for _, cell := range cells {
|
||||
c := strings.TrimSpace(cell)
|
||||
c = strings.TrimLeft(c, ":")
|
||||
c = strings.TrimRight(c, ":")
|
||||
if len(c) == 0 || strings.Trim(c, "-") != "" {
|
||||
allDash = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allDash && len(cells) >= 1 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package discord
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestWrapTablesInCodeBlocks(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "no table",
|
||||
in: "hello world\nno tables here",
|
||||
want: "hello world\nno tables here",
|
||||
},
|
||||
{
|
||||
name: "simple table",
|
||||
in: "before\n| a | b |\n| - | - |\n| 1 | 2 |\nafter",
|
||||
want: "before\n```\n| a | b |\n| - | - |\n| 1 | 2 |\n```\nafter",
|
||||
},
|
||||
{
|
||||
name: "table already in code block",
|
||||
in: "```\n| a | b |\n| 1 | 2 |\n```",
|
||||
want: "```\n| a | b |\n| 1 | 2 |\n```",
|
||||
},
|
||||
{
|
||||
name: "table at end of content with separator",
|
||||
in: "text\n| x | y |\n| --- | --- |\n| 1 | 2 |",
|
||||
want: "text\n```\n| x | y |\n| --- | --- |\n| 1 | 2 |\n```",
|
||||
},
|
||||
{
|
||||
name: "pipe rows without separator not wrapped",
|
||||
in: "text\n| x | y |\n| 1 | 2 |",
|
||||
want: "text\n| x | y |\n| 1 | 2 |",
|
||||
},
|
||||
{
|
||||
name: "multiple tables with separators",
|
||||
in: "| a | b |\n| - | - |\n| 1 | 2 |\n\ntext\n| c | d |\n| --- | --- |\n| 3 | 4 |",
|
||||
want: "```\n| a | b |\n| - | - |\n| 1 | 2 |\n```\n\ntext\n```\n| c | d |\n| --- | --- |\n| 3 | 4 |\n```",
|
||||
},
|
||||
{
|
||||
name: "pipe in regular text not treated as table",
|
||||
in: "use | for OR operations",
|
||||
want: "use | for OR operations",
|
||||
},
|
||||
{
|
||||
name: "table with code block after",
|
||||
in: "| a | b |\n| - | - |\n| 1 | 2 |\n```go\nfmt.Println()\n```",
|
||||
want: "```\n| a | b |\n| - | - |\n| 1 | 2 |\n```\n```go\nfmt.Println()\n```",
|
||||
},
|
||||
{
|
||||
name: "aligned separator with colons",
|
||||
in: "| left | center | right |\n| :--- | :---: | ---: |\n| a | b | c |",
|
||||
want: "```\n| left | center | right |\n| :--- | :---: | ---: |\n| a | b | c |\n```",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := wrapTablesInCodeBlocks(tt.in)
|
||||
if got != tt.want {
|
||||
t.Errorf("wrapTablesInCodeBlocks():\n got: %q\nwant: %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
package discord
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/chenhg5/cc-connect/core"
|
||||
)
|
||||
|
||||
const (
|
||||
maxDiscordEmbedTitleLen = 256
|
||||
maxDiscordProgressDescLen = 3500
|
||||
maxDiscordProgressLineLen = 220
|
||||
maxDiscordProgressFooterLen = 512
|
||||
)
|
||||
|
||||
func buildDiscordPreviewMessage(content string) *discordgo.MessageSend {
|
||||
if payload, ok := core.ParseProgressCardPayload(content); ok {
|
||||
return &discordgo.MessageSend{
|
||||
Embeds: []*discordgo.MessageEmbed{buildDiscordProgressEmbed(payload)},
|
||||
}
|
||||
}
|
||||
return &discordgo.MessageSend{
|
||||
Content: trimDiscordRunes(content, maxDiscordLen),
|
||||
}
|
||||
}
|
||||
|
||||
func buildDiscordPreviewEdit(channelID, messageID, content string) *discordgo.MessageEdit {
|
||||
edit := discordgo.NewMessageEdit(channelID, messageID)
|
||||
if payload, ok := core.ParseProgressCardPayload(content); ok {
|
||||
edit.SetContent("")
|
||||
edit.SetEmbeds([]*discordgo.MessageEmbed{buildDiscordProgressEmbed(payload)})
|
||||
return edit
|
||||
}
|
||||
edit.SetContent(trimDiscordRunes(content, maxDiscordLen))
|
||||
edit.SetEmbeds([]*discordgo.MessageEmbed{})
|
||||
return edit
|
||||
}
|
||||
|
||||
func buildDiscordProgressEmbed(payload *core.ProgressCardPayload) *discordgo.MessageEmbed {
|
||||
if payload == nil {
|
||||
return &discordgo.MessageEmbed{Description: " "}
|
||||
}
|
||||
agent := discordProgressAgentLabel(payload.Agent)
|
||||
title, color, footer := discordProgressStateMeta(payload.State, payload.Lang, agent)
|
||||
embed := &discordgo.MessageEmbed{
|
||||
Title: trimDiscordRunes(title, maxDiscordEmbedTitleLen),
|
||||
Description: buildDiscordProgressDescription(payload),
|
||||
Color: color,
|
||||
}
|
||||
if footer = strings.TrimSpace(footer); footer != "" {
|
||||
embed.Footer = &discordgo.MessageEmbedFooter{
|
||||
Text: trimDiscordRunes(footer, maxDiscordProgressFooterLen),
|
||||
}
|
||||
}
|
||||
return embed
|
||||
}
|
||||
|
||||
func buildDiscordProgressDescription(payload *core.ProgressCardPayload) string {
|
||||
if payload == nil {
|
||||
return " "
|
||||
}
|
||||
items := discordProgressItems(payload)
|
||||
lines := make([]string, 0, len(items)+1)
|
||||
if payload.Truncated {
|
||||
lines = append(lines, "ℹ️ "+discordProgressLatestOnlyText(payload.Lang))
|
||||
}
|
||||
for _, item := range items {
|
||||
if line := buildDiscordProgressLine(item, payload.Lang); line != "" {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
desc := strings.Join(lines, "\n")
|
||||
desc = trimDiscordRunes(desc, maxDiscordProgressDescLen)
|
||||
if strings.TrimSpace(desc) == "" {
|
||||
return " "
|
||||
}
|
||||
return desc
|
||||
}
|
||||
|
||||
func discordProgressItems(payload *core.ProgressCardPayload) []core.ProgressCardEntry {
|
||||
if payload == nil {
|
||||
return nil
|
||||
}
|
||||
if len(payload.Items) > 0 {
|
||||
return payload.Items
|
||||
}
|
||||
out := make([]core.ProgressCardEntry, 0, len(payload.Entries))
|
||||
for _, entry := range payload.Entries {
|
||||
entry = strings.TrimSpace(entry)
|
||||
if entry == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, core.ProgressCardEntry{
|
||||
Kind: core.ProgressEntryInfo,
|
||||
Text: entry,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func buildDiscordProgressLine(item core.ProgressCardEntry, lang string) string {
|
||||
text := compactDiscordProgressText(item.Text)
|
||||
switch item.Kind {
|
||||
case core.ProgressEntryThinking:
|
||||
return "💭 " + trimDiscordRunes(text, maxDiscordProgressLineLen-2)
|
||||
case core.ProgressEntryToolUse:
|
||||
return "🔧 " + trimDiscordRunes(buildDiscordToolUseSummary(item, lang), maxDiscordProgressLineLen-2)
|
||||
case core.ProgressEntryToolResult:
|
||||
return "🧾 " + trimDiscordRunes(buildDiscordToolResultSummary(item, lang), maxDiscordProgressLineLen-2)
|
||||
case core.ProgressEntryError:
|
||||
return "❌ " + trimDiscordRunes(text, maxDiscordProgressLineLen-2)
|
||||
default:
|
||||
return "• " + trimDiscordRunes(text, maxDiscordProgressLineLen-2)
|
||||
}
|
||||
}
|
||||
|
||||
func buildDiscordToolUseSummary(item core.ProgressCardEntry, lang string) string {
|
||||
toolName := strings.TrimSpace(item.Tool)
|
||||
if toolName == "" {
|
||||
toolName = discordProgressToolLabel(lang)
|
||||
}
|
||||
text := compactDiscordProgressText(item.Text)
|
||||
if text == "" {
|
||||
return toolName
|
||||
}
|
||||
return toolName + " — " + text
|
||||
}
|
||||
|
||||
func buildDiscordToolResultSummary(item core.ProgressCardEntry, lang string) string {
|
||||
toolName := strings.TrimSpace(item.Tool)
|
||||
text := compactDiscordProgressText(item.Text)
|
||||
meta := make([]string, 0, 3)
|
||||
|
||||
if status := discordProgressStatusText(item, lang); status != "" {
|
||||
meta = append(meta, status)
|
||||
}
|
||||
if item.ExitCode != nil {
|
||||
meta = append(meta, fmt.Sprintf("exit %d", *item.ExitCode))
|
||||
}
|
||||
if text != "" {
|
||||
duplicate := false
|
||||
for _, part := range meta {
|
||||
if strings.EqualFold(part, text) {
|
||||
duplicate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !duplicate {
|
||||
meta = append(meta, text)
|
||||
}
|
||||
}
|
||||
if len(meta) == 0 {
|
||||
meta = append(meta, discordProgressNoOutputText(lang))
|
||||
}
|
||||
if toolName == "" {
|
||||
return strings.Join(meta, " · ")
|
||||
}
|
||||
return toolName + " — " + strings.Join(meta, " · ")
|
||||
}
|
||||
|
||||
func discordProgressStateMeta(state core.ProgressCardState, lang string, agent string) (title string, color int, footer string) {
|
||||
switch state {
|
||||
case core.ProgressCardStateCompleted:
|
||||
return fmt.Sprintf("%s · %s", agent, discordProgressCompletedText(lang)), 0x57F287, discordProgressCompletedFooter(lang)
|
||||
case core.ProgressCardStateFailed:
|
||||
return fmt.Sprintf("%s · %s", agent, discordProgressFailedText(lang)), 0xED4245, discordProgressFailedFooter(lang)
|
||||
default:
|
||||
return fmt.Sprintf("%s · %s", agent, discordProgressRunningText(lang)), 0x5865F2, ""
|
||||
}
|
||||
}
|
||||
|
||||
func discordProgressAgentLabel(agent string) string {
|
||||
agent = strings.TrimSpace(agent)
|
||||
if agent == "" {
|
||||
return "Agent"
|
||||
}
|
||||
return agent
|
||||
}
|
||||
|
||||
func discordProgressRunningText(lang string) string {
|
||||
switch normalizeDiscordProgressLang(lang) {
|
||||
case "zh":
|
||||
return "处理中"
|
||||
case "zh-tw":
|
||||
return "處理中"
|
||||
case "ja":
|
||||
return "処理中"
|
||||
case "es":
|
||||
return "Procesando"
|
||||
default:
|
||||
return "Processing"
|
||||
}
|
||||
}
|
||||
|
||||
func discordProgressCompletedText(lang string) string {
|
||||
switch normalizeDiscordProgressLang(lang) {
|
||||
case "zh":
|
||||
return "已完成"
|
||||
case "zh-tw":
|
||||
return "已完成"
|
||||
case "ja":
|
||||
return "完了"
|
||||
case "es":
|
||||
return "Completado"
|
||||
default:
|
||||
return "Completed"
|
||||
}
|
||||
}
|
||||
|
||||
func discordProgressFailedText(lang string) string {
|
||||
switch normalizeDiscordProgressLang(lang) {
|
||||
case "zh":
|
||||
return "失败"
|
||||
case "zh-tw":
|
||||
return "失敗"
|
||||
case "ja":
|
||||
return "失敗"
|
||||
case "es":
|
||||
return "Fallido"
|
||||
default:
|
||||
return "Failed"
|
||||
}
|
||||
}
|
||||
|
||||
func discordProgressCompletedFooter(lang string) string {
|
||||
switch normalizeDiscordProgressLang(lang) {
|
||||
case "zh":
|
||||
return "本进度卡已停止更新,完整答复见下一条消息。"
|
||||
case "zh-tw":
|
||||
return "本進度卡已停止更新,完整答覆見下一條訊息。"
|
||||
case "ja":
|
||||
return "この進捗カードの更新は終了しました。完全な回答は次のメッセージにあります。"
|
||||
case "es":
|
||||
return "Esta tarjeta de progreso ya no se actualiza. La respuesta completa está en el siguiente mensaje."
|
||||
default:
|
||||
return "This progress card is no longer updating. Full response is in the next message."
|
||||
}
|
||||
}
|
||||
|
||||
func discordProgressFailedFooter(lang string) string {
|
||||
switch normalizeDiscordProgressLang(lang) {
|
||||
case "zh":
|
||||
return "本进度卡已停止更新(失败),请查看下一条消息中的错误详情。"
|
||||
case "zh-tw":
|
||||
return "本進度卡已停止更新(失敗),請查看下一條訊息中的錯誤詳情。"
|
||||
case "ja":
|
||||
return "この進捗カードは失敗で停止しました。詳細は次のメッセージを確認してください。"
|
||||
case "es":
|
||||
return "Esta tarjeta de progreso se detuvo por error. Consulta el siguiente mensaje para ver los detalles."
|
||||
default:
|
||||
return "This progress card has stopped (failed). See the next message for details."
|
||||
}
|
||||
}
|
||||
|
||||
func discordProgressLatestOnlyText(lang string) string {
|
||||
switch normalizeDiscordProgressLang(lang) {
|
||||
case "zh":
|
||||
return "仅显示最近更新。"
|
||||
case "zh-tw":
|
||||
return "僅顯示最近更新。"
|
||||
case "ja":
|
||||
return "最新の更新のみ表示しています。"
|
||||
case "es":
|
||||
return "Mostrando solo las actualizaciones más recientes."
|
||||
default:
|
||||
return "Showing latest updates only."
|
||||
}
|
||||
}
|
||||
|
||||
func discordProgressNoOutputText(lang string) string {
|
||||
switch normalizeDiscordProgressLang(lang) {
|
||||
case "zh":
|
||||
return "无输出"
|
||||
case "zh-tw":
|
||||
return "無輸出"
|
||||
case "ja":
|
||||
return "出力なし"
|
||||
case "es":
|
||||
return "Sin salida"
|
||||
default:
|
||||
return "No output"
|
||||
}
|
||||
}
|
||||
|
||||
func discordProgressToolLabel(lang string) string {
|
||||
switch normalizeDiscordProgressLang(lang) {
|
||||
case "zh":
|
||||
return "工具"
|
||||
case "zh-tw":
|
||||
return "工具"
|
||||
case "ja":
|
||||
return "ツール"
|
||||
case "es":
|
||||
return "Herramienta"
|
||||
default:
|
||||
return "Tool"
|
||||
}
|
||||
}
|
||||
|
||||
func discordProgressOKText(lang string) string {
|
||||
switch normalizeDiscordProgressLang(lang) {
|
||||
case "zh":
|
||||
return "成功"
|
||||
case "zh-tw":
|
||||
return "成功"
|
||||
case "ja":
|
||||
return "成功"
|
||||
case "es":
|
||||
return "ok"
|
||||
default:
|
||||
return "ok"
|
||||
}
|
||||
}
|
||||
|
||||
func discordProgressStatusText(item core.ProgressCardEntry, lang string) string {
|
||||
status := strings.ToLower(strings.TrimSpace(item.Status))
|
||||
switch status {
|
||||
case "completed", "complete", "success", "succeeded", "ok":
|
||||
if status == "completed" || status == "complete" {
|
||||
return strings.ToLower(discordProgressCompletedText(lang))
|
||||
}
|
||||
return discordProgressOKText(lang)
|
||||
case "failed", "failure", "error":
|
||||
return strings.ToLower(discordProgressFailedText(lang))
|
||||
}
|
||||
if item.Success != nil {
|
||||
if *item.Success {
|
||||
return discordProgressOKText(lang)
|
||||
}
|
||||
return strings.ToLower(discordProgressFailedText(lang))
|
||||
}
|
||||
return strings.TrimSpace(item.Status)
|
||||
}
|
||||
|
||||
func normalizeDiscordProgressLang(lang string) string {
|
||||
l := strings.ToLower(strings.TrimSpace(lang))
|
||||
switch {
|
||||
case l == "zh-tw" || l == "zh-hk":
|
||||
return "zh-tw"
|
||||
case strings.HasPrefix(l, "zh"):
|
||||
return "zh"
|
||||
case strings.HasPrefix(l, "ja"):
|
||||
return "ja"
|
||||
case strings.HasPrefix(l, "es"):
|
||||
return "es"
|
||||
default:
|
||||
return "en"
|
||||
}
|
||||
}
|
||||
|
||||
func compactDiscordProgressText(s string) string {
|
||||
s = strings.ReplaceAll(strings.TrimSpace(s), "\r\n", "\n")
|
||||
s = strings.ReplaceAll(s, "\r", "\n")
|
||||
s = strings.ReplaceAll(s, "\n", " ")
|
||||
return strings.Join(strings.Fields(s), " ")
|
||||
}
|
||||
|
||||
func trimDiscordRunes(s string, maxRunes int) string {
|
||||
if maxRunes <= 0 {
|
||||
return ""
|
||||
}
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
if utf8.RuneCountInString(s) <= maxRunes {
|
||||
return s
|
||||
}
|
||||
rs := []rune(s)
|
||||
if maxRunes == 1 {
|
||||
return string(rs[:1])
|
||||
}
|
||||
return string(rs[:maxRunes-1]) + "…"
|
||||
}
|
||||
Reference in New Issue
Block a user