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:
2026-06-28 11:29:29 +08:00
parent 0d6970a2d3
commit fd44b15d81
23 changed files with 1447 additions and 254 deletions
@@ -32,8 +32,11 @@ type Adapter struct {
port string
accessToken string
remoteURL string // NapCat OneBot WS server URL, used in client mode
sendIntervalMs int // minimum interval between consecutive messages
selfID string // bot's own QQ number, populated from incoming messages
httpURL string // NapCat HTTP API base URL (optional, derived from remoteURL if empty)
httpToken_ string // NapCat HTTP API access token (optional, uses accessToken if empty)
sendIntervalMs int // minimum interval between consecutive messages
lastTypingSent time.Time // last time typing indicator was sent, for min display duration
selfID string // bot's own QQ number, populated from incoming messages
conn *websocket.Conn
connMu sync.Mutex
connected bool
@@ -64,6 +67,14 @@ func NewAdapter(configID, configName, mode, port, accessToken, remoteURL string,
}
}
// SetHTTPConfig sets the optional HTTP API configuration.
func (a *Adapter) SetHTTPConfig(url, token string) {
a.httpURL = url
if token != "" {
a.httpToken_ = token
}
}
func (a *Adapter) PlatformName() string { return "obv11" }
func (a *Adapter) ConfigID() string { return a.configID }
func (a *Adapter) ConfigName() string { return a.configName }
@@ -97,8 +108,12 @@ func (a *Adapter) SetGroupName(groupID int64, name string) {
a.groupNamesMu.Unlock()
}
// httpBase derives the HTTP base URL from the WebSocket remote URL.
// httpBase returns the HTTP API base URL.
// Uses configured httpURL if set, otherwise derives from WebSocket remoteURL.
func (a *Adapter) httpBase() string {
if a.httpURL != "" {
return a.httpURL
}
httpBase := strings.Replace(a.remoteURL, "ws://", "http://", 1)
httpBase = strings.Replace(httpBase, "wss://", "https://", 1)
if idx := strings.LastIndex(httpBase, "/"); idx > 8 {
@@ -107,6 +122,48 @@ func (a *Adapter) httpBase() string {
return httpBase
}
// httpToken returns the HTTP API access token.
func (a *Adapter) httpToken() string {
if a.httpToken_ != "" {
return a.httpToken_
}
return a.accessToken
}
// SetTypingStatus sends a typing indicator to a private chat user.
// eventType: 1 = typing started, 0 = typing stopped.
// This uses the NapCat HTTP API (not standard OneBot v11).
// Enforces a minimum 3s display duration.
func (a *Adapter) SetTypingStatus(userID int64, eventType int) error {
if eventType == 0 {
// 确保"正在输入"至少显示了 3 秒
if time.Since(a.lastTypingSent) < 3*time.Second {
time.Sleep(3*time.Second - time.Since(a.lastTypingSent))
}
} else {
a.lastTypingSent = time.Now()
}
url := a.httpBase() + "/set_input_status"
if token := a.httpToken(); token != "" {
url += "?access_token=" + token
}
body, _ := json.Marshal(map[string]interface{}{
"user_id": fmt.Sprintf("%d", userID),
"event_type": eventType,
})
resp, err := http.Post(url, "application/json", strings.NewReader(string(body)))
if err != nil {
log.Printf("[qq] set_input_status error: %v (url=%s)", err, url)
return fmt.Errorf("set_input_status: %w", err)
}
resp.Body.Close()
if resp.StatusCode != 200 {
log.Printf("[qq] set_input_status HTTP %d (url=%s)", resp.StatusCode, url)
return fmt.Errorf("set_input_status: HTTP %d", resp.StatusCode)
}
return nil
}
// fetchGroupName tries to resolve a group name via NapCat HTTP API (client mode).
func (a *Adapter) fetchGroupName(groupID int64) {
if a.mode != "client" || a.remoteURL == "" {