mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-05-17 09:27:28 +08:00
parallel optimization groundwork
This commit is contained in:
@@ -135,7 +135,7 @@ func (c *FeishuChannel) Send(ctx context.Context, msg bus.OutboundMessage) error
|
||||
logger.WarnCF("feishu", logger.C0045, map[string]interface{}{logger.FieldError: lerr.Error(), logger.FieldChatID: msg.ChatID})
|
||||
continue
|
||||
}
|
||||
links = append(links, fmt.Sprintf("琛ㄦ牸%d: %s", i+1, link))
|
||||
links = append(links, fmt.Sprintf("表格%d: %s", i+1, link))
|
||||
}
|
||||
if len(links) > 0 {
|
||||
if strings.TrimSpace(workMsg.Content) != "" {
|
||||
@@ -883,9 +883,9 @@ func normalizeFeishuText(s string) string {
|
||||
// Headers: "## title" -> "title"
|
||||
s = regexp.MustCompile(`(?m)^#{1,6}\s+`).ReplaceAllString(s, "")
|
||||
// Bullet styles
|
||||
s = regexp.MustCompile(`(?m)^[-*]\s+`).ReplaceAllString(s, "鈥?")
|
||||
s = regexp.MustCompile(`(?m)^[-*]\s+`).ReplaceAllString(s, "- ")
|
||||
// Ordered list to bullet for readability
|
||||
s = regexp.MustCompile(`(?m)^\d+\.\s+`).ReplaceAllString(s, "鈥?")
|
||||
s = regexp.MustCompile(`(?m)^\d+\.\s+`).ReplaceAllString(s, "- ")
|
||||
// Bold/italic/strike markers
|
||||
s = regexp.MustCompile(`\*\*(.*?)\*\*`).ReplaceAllString(s, `$1`)
|
||||
s = regexp.MustCompile(`__(.*?)__`).ReplaceAllString(s, `$1`)
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -412,6 +413,10 @@ func (c *WeixinChannel) RemoveAccount(botID string) error {
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
if c.accounts[botID] == nil {
|
||||
c.mu.Unlock()
|
||||
return fmt.Errorf("bot_id not found: %s", botID)
|
||||
}
|
||||
if cancel := c.pollers[botID]; cancel != nil {
|
||||
cancel()
|
||||
delete(c.pollers, botID)
|
||||
@@ -430,8 +435,14 @@ func (c *WeixinChannel) RemoveAccount(botID string) error {
|
||||
delete(c.chatContexts, chatID)
|
||||
}
|
||||
}
|
||||
c.typingMu.Lock()
|
||||
delete(c.typingCache, botID)
|
||||
c.typingMu.Unlock()
|
||||
if strings.TrimSpace(c.config.DefaultBotID) == botID {
|
||||
c.config.DefaultBotID = ""
|
||||
if len(c.accountOrder) > 0 {
|
||||
c.config.DefaultBotID = c.accountOrder[0]
|
||||
}
|
||||
}
|
||||
c.schedulePersistLocked()
|
||||
c.mu.Unlock()
|
||||
@@ -694,17 +705,23 @@ func (c *WeixinChannel) handleInboundMessage(botID string, msg weixinInboundMess
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
var textParts []string
|
||||
var itemTypes []string
|
||||
var contentBuilder strings.Builder
|
||||
var itemTypesBuilder strings.Builder
|
||||
for _, item := range msg.ItemList {
|
||||
itemTypes = append(itemTypes, fmt.Sprintf("%d", item.Type))
|
||||
if itemTypesBuilder.Len() > 0 {
|
||||
itemTypesBuilder.WriteByte(',')
|
||||
}
|
||||
itemTypesBuilder.WriteString(strconv.Itoa(item.Type))
|
||||
if item.Type == 1 {
|
||||
if text := strings.TrimSpace(item.TextItem.Text); text != "" {
|
||||
textParts = append(textParts, text)
|
||||
if contentBuilder.Len() > 0 {
|
||||
contentBuilder.WriteByte('\n')
|
||||
}
|
||||
contentBuilder.WriteString(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
content := strings.Join(textParts, "\n")
|
||||
content := contentBuilder.String()
|
||||
if content == "" {
|
||||
return
|
||||
}
|
||||
@@ -717,7 +734,7 @@ func (c *WeixinChannel) handleInboundMessage(botID string, msg weixinInboundMess
|
||||
metadata := map[string]string{
|
||||
"bot_id": botID,
|
||||
"context_token": contextToken,
|
||||
"item_types": strings.Join(itemTypes, ","),
|
||||
"item_types": itemTypesBuilder.String(),
|
||||
"raw_chat_id": rawChatID,
|
||||
}
|
||||
c.HandleMessage(rawChatID, chatID, content, nil, metadata)
|
||||
@@ -1118,24 +1135,25 @@ func (c *WeixinChannel) doJSON(ctx context.Context, path string, payload interfa
|
||||
}
|
||||
|
||||
func (c *WeixinChannel) doJSONWithTimeout(ctx context.Context, path string, payload interface{}, out interface{}, token string, timeout time.Duration) error {
|
||||
reqCtx := ctx
|
||||
cancel := func() {}
|
||||
if timeout > 0 {
|
||||
reqCtx, cancel = context.WithTimeout(ctx, timeout)
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal request: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.config.BaseURL+path, bytes.NewReader(body))
|
||||
req, err := http.NewRequestWithContext(reqCtx, http.MethodPost, c.config.BaseURL+path, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("build request: %w", err)
|
||||
}
|
||||
c.applyHeaders(req, true, token, true)
|
||||
|
||||
client := c.httpClient
|
||||
if timeout > 0 {
|
||||
clone := *c.httpClient
|
||||
clone.Timeout = timeout
|
||||
client = &clone
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -72,6 +72,50 @@ func TestWeixinHandleInboundMessageUsesCompositeSessionChatID(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWeixinHandleInboundMessageBuildsMetadataAndContent(t *testing.T) {
|
||||
mb := bus.NewMessageBus()
|
||||
ch, err := NewWeixinChannel(config.WeixinConfig{
|
||||
BaseURL: "https://ilinkai.weixin.qq.com",
|
||||
Accounts: []config.WeixinAccountConfig{
|
||||
{BotID: "bot-a", BotToken: "token-a"},
|
||||
},
|
||||
}, mb)
|
||||
if err != nil {
|
||||
t.Fatalf("new weixin channel: %v", err)
|
||||
}
|
||||
|
||||
ch.handleInboundMessage("bot-a", weixinInboundMessage{
|
||||
FromUserID: "wx-user-1",
|
||||
ContextToken: "ctx-1",
|
||||
ItemList: []weixinMessageItem{
|
||||
{Type: 2},
|
||||
{Type: 1, TextItem: struct {
|
||||
Text string `json:"text"`
|
||||
}{Text: "hello"}},
|
||||
{Type: 1, TextItem: struct {
|
||||
Text string `json:"text"`
|
||||
}{Text: " world "}},
|
||||
{Type: 3},
|
||||
},
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
msg, ok := mb.ConsumeInbound(ctx)
|
||||
if !ok {
|
||||
t.Fatalf("expected inbound message")
|
||||
}
|
||||
if msg.Content != "hello\nworld" {
|
||||
t.Fatalf("unexpected content: %q", msg.Content)
|
||||
}
|
||||
if got := msg.Metadata["item_types"]; got != "2,1,1,3" {
|
||||
t.Fatalf("unexpected item_types: %q", got)
|
||||
}
|
||||
if got := msg.Metadata["context_token"]; got != "ctx-1" {
|
||||
t.Fatalf("unexpected context_token: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWeixinResolveAccountForCompositeChatID(t *testing.T) {
|
||||
mb := bus.NewMessageBus()
|
||||
ch, err := NewWeixinChannel(config.WeixinConfig{
|
||||
@@ -135,6 +179,56 @@ func TestWeixinSetDefaultAccount(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWeixinRemoveAccountReturnsErrorWhenMissing(t *testing.T) {
|
||||
mb := bus.NewMessageBus()
|
||||
ch, err := NewWeixinChannel(config.WeixinConfig{
|
||||
BaseURL: "https://ilinkai.weixin.qq.com",
|
||||
Accounts: []config.WeixinAccountConfig{
|
||||
{BotID: "bot-a", BotToken: "token-a"},
|
||||
},
|
||||
}, mb)
|
||||
if err != nil {
|
||||
t.Fatalf("new weixin channel: %v", err)
|
||||
}
|
||||
|
||||
err = ch.RemoveAccount("bot-missing")
|
||||
if err == nil {
|
||||
t.Fatalf("expected remove missing account to fail")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "bot_id not found") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWeixinRemoveAccountReassignsDefault(t *testing.T) {
|
||||
mb := bus.NewMessageBus()
|
||||
ch, err := NewWeixinChannel(config.WeixinConfig{
|
||||
BaseURL: "https://ilinkai.weixin.qq.com",
|
||||
DefaultBotID: "bot-b",
|
||||
Accounts: []config.WeixinAccountConfig{
|
||||
{BotID: "bot-a", BotToken: "token-a"},
|
||||
{BotID: "bot-b", BotToken: "token-b"},
|
||||
},
|
||||
}, mb)
|
||||
if err != nil {
|
||||
t.Fatalf("new weixin channel: %v", err)
|
||||
}
|
||||
|
||||
if err := ch.RemoveAccount("bot-b"); err != nil {
|
||||
t.Fatalf("remove account: %v", err)
|
||||
}
|
||||
accounts := ch.ListAccounts()
|
||||
if len(accounts) != 1 {
|
||||
t.Fatalf("expected 1 account after removal, got %d", len(accounts))
|
||||
}
|
||||
if accounts[0].BotID != "bot-a" {
|
||||
t.Fatalf("expected remaining account bot-a, got %s", accounts[0].BotID)
|
||||
}
|
||||
if !accounts[0].Default {
|
||||
t.Fatalf("expected remaining account to become default")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWeixinCancelPendingLogin(t *testing.T) {
|
||||
mb := bus.NewMessageBus()
|
||||
ch, err := NewWeixinChannel(config.WeixinConfig{BaseURL: "https://ilinkai.weixin.qq.com"}, mb)
|
||||
@@ -422,3 +516,34 @@ func TestWeixinValidateAPIStatusErrorShape(t *testing.T) {
|
||||
t.Fatalf("marshal error text")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWeixinDoJSONWithTimeoutSetsRequestDeadline(t *testing.T) {
|
||||
mb := bus.NewMessageBus()
|
||||
ch, err := NewWeixinChannel(config.WeixinConfig{
|
||||
BaseURL: "https://ilinkai.weixin.qq.com",
|
||||
Accounts: []config.WeixinAccountConfig{
|
||||
{BotID: "bot-a", BotToken: "token-a"},
|
||||
},
|
||||
}, mb)
|
||||
if err != nil {
|
||||
t.Fatalf("new weixin channel: %v", err)
|
||||
}
|
||||
|
||||
ch.httpClient = &http.Client{Transport: weixinRoundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
if _, ok := req.Context().Deadline(); !ok {
|
||||
t.Fatalf("expected request context to have deadline")
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(strings.NewReader(`{"ret":0,"errcode":0}`)),
|
||||
Header: make(http.Header),
|
||||
}, nil
|
||||
})}
|
||||
|
||||
var out weixinAPIResponse
|
||||
if err := ch.doJSONWithTimeout(context.Background(), "/ilink/bot/sendmessage", map[string]interface{}{
|
||||
"msg": map[string]interface{}{"item_list": []map[string]interface{}{}},
|
||||
}, &out, "token-a", 50*time.Millisecond); err != nil {
|
||||
t.Fatalf("doJSONWithTimeout: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user