feat: 全链路优化 — 死锁修复、MD3主题、上下文持久化、群聊自然化、打字状态、知识库
**死锁根因修复** - periodicThinkLoop:1015 orphaned lock → 删除(字段已原子化) - RecordUserMessage 隔离为 recordMu - atomic.Int64 替换 lastUserMessage/lastThinkTime 等 **MD3 / Android 17 主题** - 毛玻璃卡片 (backdrop-filter) - MD3 色彩令牌 (pink primary #f472b6) - icons.js 独立矢量图标库 + 运行时 emoji 替换 - 无边框卡片、圆角按钮、阴影层次 **上下文持久化** - AddMessage → saveToDB 异步写 PostgreSQL - LoadFromDB 恢复 (admin-session-main + 懒加载) - LLMMessage.Timestamp 字段 **群聊与适配器** - group_ambient 模式: 非@消息让 LLM 自己判断是否插话 - 戳一戳动作消息总是回复 - NapCat 打字状态 (set_input_status, 最小3秒显示) - HTTP API 配置 (http_url/http_token) **知识库 & 防编造** - knowledge.CanHandle 对 chat 意图也触发 - 关键词预筛选避免无关 embedding 调用 - persona + synthesizer 三重诚实规则 - 工具结果持久化到会话历史 **平台桥接器** - detached:true Go进程独立存活 - ethend 重启自动接管已运行服务 - stop() 接管模式 taskkill/F/ PID - Windows netstat 替代 fuser 获取 PID - 重复适配器种子逻辑修复 - 失败转发日志 Direction: error **崩溃诊断** - crashlog 包 (Recover + WrapHTTP + LLMCall) - /api/v1/debug/goroutines 端点 - thinker 操作日志 + 30s stats - 日志写入 logs/ 目录持久化 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -7,11 +7,21 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"regexp"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene/platform-bridge/internal/bridge"
|
||||
)
|
||||
|
||||
// Regex patterns for markdown stripping.
|
||||
var (
|
||||
mdBoldRe = regexp.MustCompile(`\*\*(.+?)\*\*`)
|
||||
mdItalicRe = regexp.MustCompile(`\*(.+?)\*`)
|
||||
mdStrikethroughRe = regexp.MustCompile(`~~(.+?)~~`)
|
||||
mdHeadingRe = regexp.MustCompile(`(?m)^#{1,6}\s+`)
|
||||
)
|
||||
|
||||
// BridgeHandler exposes the Platform Bridge REST API.
|
||||
type BridgeHandler struct {
|
||||
router *bridge.PlatformRouter
|
||||
@@ -200,39 +210,57 @@ func (h *BridgeHandler) sendProactive(w http.ResponseWriter, r *http.Request) {
|
||||
userID := parseIntSafe(req.UserID)
|
||||
groupID := parseIntSafe(req.GroupID)
|
||||
|
||||
// 过滤 <action>/<app> 标签,去除 markdown 标记
|
||||
content := filterActions(req.Content)
|
||||
content = convertMarkdownPlain(content)
|
||||
|
||||
// 按 \n\n 和 ♪ 拆分为多条消息
|
||||
messages := splitProactiveContent(content)
|
||||
|
||||
// Prepend CQ @mention tag if at_user_id is specified
|
||||
content := req.Content
|
||||
atPrefix := ""
|
||||
if req.AtUserID != "" {
|
||||
content = fmt.Sprintf("[CQ:at,qq=%s] %s", req.AtUserID, content)
|
||||
atPrefix = fmt.Sprintf("[CQ:at,qq=%s] ", req.AtUserID)
|
||||
}
|
||||
|
||||
// Resolve adapter: try exact name first, then find by platform type.
|
||||
// Resolve adapter
|
||||
adapterName := req.Platform
|
||||
err := h.router.SendProactive(adapterName, msgType, userID, groupID, content)
|
||||
if err != nil {
|
||||
for _, name := range h.router.ListAdapters() {
|
||||
if a, aErr := h.router.GetAdapter(name); aErr == nil && a.PlatformName() == req.Platform && a.IsConnected() {
|
||||
adapterName = name
|
||||
err = h.router.SendProactive(name, msgType, userID, groupID, content)
|
||||
break
|
||||
var sendErr error
|
||||
for i, msg := range messages {
|
||||
fullMsg := atPrefix + msg
|
||||
if i > 0 {
|
||||
atPrefix = "" // only first message gets @mention
|
||||
}
|
||||
sendErr = h.router.SendProactive(adapterName, msgType, userID, groupID, fullMsg)
|
||||
if sendErr != nil {
|
||||
// Fallback: try other adapters with same platform name
|
||||
for _, name := range h.router.ListAdapters() {
|
||||
if a, aErr := h.router.GetAdapter(name); aErr == nil && a.PlatformName() == req.Platform && a.IsConnected() {
|
||||
adapterName = name
|
||||
sendErr = h.router.SendProactive(name, msgType, userID, groupID, fullMsg)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if sendErr != nil {
|
||||
log.Printf("[send-proactive] 发送失败: adapter=%s err=%v", adapterName, sendErr)
|
||||
break
|
||||
}
|
||||
log.Printf("[send-proactive] 已发送: adapter=%s chat=%s user=%d group=%d at=%s len=%d msg=%d/%d",
|
||||
adapterName, msgType, userID, groupID, req.AtUserID, len(fullMsg), i+1, len(messages))
|
||||
if h.logFn != nil {
|
||||
chID := req.GroupID
|
||||
if msgType == "private" {
|
||||
chID = req.UserID
|
||||
}
|
||||
h.logFn(req.Platform, chID, "Cyrene", fullMsg, true)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("[send-proactive] 发送失败: adapter=%s err=%v", adapterName, err)
|
||||
writeJSON(w, http.StatusInternalServerError, errResp("send failed: "+err.Error()))
|
||||
if sendErr != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, errResp("send failed: "+sendErr.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[send-proactive] 已发送: adapter=%s chat=%s user=%d group=%d at=%s len=%d",
|
||||
adapterName, msgType, userID, groupID, req.AtUserID, len(content))
|
||||
if h.logFn != nil {
|
||||
chID := req.GroupID
|
||||
if msgType == "private" {
|
||||
chID = req.UserID
|
||||
}
|
||||
h.logFn(req.Platform, chID, "Cyrene", content, true)
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "消息已发送",
|
||||
@@ -259,3 +287,71 @@ func writeJSON(w http.ResponseWriter, status int, data interface{}) {
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
|
||||
// filterActions removes <action> and <app> tags and their content.
|
||||
func filterActions(text string) string {
|
||||
tags := [][2]string{
|
||||
{"<action>", "</action>"},
|
||||
{"<app>", "</app>"},
|
||||
}
|
||||
for _, t := range tags {
|
||||
openTag, closeTag := t[0], t[1]
|
||||
for {
|
||||
start := strings.Index(text, openTag)
|
||||
if start == -1 {
|
||||
break
|
||||
}
|
||||
end := strings.Index(text[start:], closeTag)
|
||||
if end == -1 {
|
||||
text = text[:start] + text[start+len(openTag):]
|
||||
continue
|
||||
}
|
||||
text = text[:start] + text[start+end+len(closeTag):]
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(text)
|
||||
}
|
||||
|
||||
// convertMarkdownPlain strips basic markdown formatting.
|
||||
func convertMarkdownPlain(md string) string {
|
||||
md = mdBoldRe.ReplaceAllString(md, "$1")
|
||||
md = mdItalicRe.ReplaceAllString(md, "$1")
|
||||
md = mdStrikethroughRe.ReplaceAllString(md, "$1")
|
||||
md = mdHeadingRe.ReplaceAllString(md, "")
|
||||
return md
|
||||
}
|
||||
|
||||
// splitProactiveContent splits long proactive content into multiple messages.
|
||||
// Strategy same as splitContent in cmd/main.go: split by \n\n, then by ♪.
|
||||
func splitProactiveContent(text string) []string {
|
||||
rawParts := strings.Split(text, "\n\n")
|
||||
var parts []string
|
||||
for _, p := range rawParts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(p, "♪") {
|
||||
for _, sub := range strings.Split(p, "♪") {
|
||||
sub = strings.TrimSpace(sub)
|
||||
if sub != "" {
|
||||
parts = append(parts, sub)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
parts = append(parts, p)
|
||||
}
|
||||
}
|
||||
|
||||
// Merge very short segments with neighbors (min 8 runes).
|
||||
const minRunes = 8
|
||||
var merged []string
|
||||
for _, part := range parts {
|
||||
if len([]rune(part)) < minRunes && len(merged) > 0 {
|
||||
merged[len(merged)-1] = merged[len(merged)-1] + part
|
||||
} else {
|
||||
merged = append(merged, part)
|
||||
}
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user