|
@@ -157,13 +157,22 @@ func (s *SettingsService) GetNtfyNicknames() []string {
|
|
|
// 与 SendNtfyMessage 的区别:昵称固定作为标题,便于聊天室按昵称区分发言者。
|
|
// 与 SendNtfyMessage 的区别:昵称固定作为标题,便于聊天室按昵称区分发言者。
|
|
|
func (s *SettingsService) SendNtfyChat(topic, nickname, message string) error {
|
|
func (s *SettingsService) SendNtfyChat(topic, nickname, message string) error {
|
|
|
nickname = strings.TrimSpace(nickname)
|
|
nickname = strings.TrimSpace(nickname)
|
|
|
|
|
+ topic = strings.TrimSpace(topic)
|
|
|
message = strings.TrimSpace(message)
|
|
message = strings.TrimSpace(message)
|
|
|
|
|
+ // 先记录本地发送,再发送。
|
|
|
|
|
+ // 关键:ntfy 服务器收到消息后会立即通过 SSE 广播回声,往往比 HTTP 响应
|
|
|
|
|
+ // 更快到达。若在发送成功后才记录,回声到达时记录尚不存在,去重必然失败
|
|
|
|
|
+ // → 自己发送的消息又被当作 SSE 消息显示一次(表现为「显示两次」)。
|
|
|
|
|
+ // 因此必须「先记录、后发送」,保证 SSE 回声到达时去重记录已就位。
|
|
|
|
|
+ rememberLocalChat(topic, message)
|
|
|
|
|
+ logDebug("SENT remember: topic=%q msg=%q", topic, message)
|
|
|
err := s.SendNtfyMessage(topic, nickname, message)
|
|
err := s.SendNtfyMessage(topic, nickname, message)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
|
|
+ // 发送失败:移除刚记录的回声,避免误吞后续同主题同内容的真实消息
|
|
|
|
|
+ forgetLocalChat(topic, message)
|
|
|
return err
|
|
return err
|
|
|
}
|
|
}
|
|
|
- AddNtfyNickname(nickname) // 发送成功后记忆昵称
|
|
|
|
|
- rememberLocalChat(topic, message) // 记录本地发送,用于去重 SSE 回声
|
|
|
|
|
|
|
+ AddNtfyNickname(nickname) // 发送成功后记忆昵称
|
|
|
return nil
|
|
return nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -313,6 +322,19 @@ func consumeEchoChat(topic, message string) bool {
|
|
|
return false
|
|
return false
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// forgetLocalChat 移除一条本地发送记录(发送失败时回滚)。
|
|
|
|
|
+// 若发送失败,SSE 不会收到回声,需移除记录避免误吞后续同主题同内容的真实消息。
|
|
|
|
|
+func forgetLocalChat(topic, message string) {
|
|
|
|
|
+ ntfyEchoMu.Lock()
|
|
|
|
|
+ defer ntfyEchoMu.Unlock()
|
|
|
|
|
+ for i, r := range ntfyEchoRecent {
|
|
|
|
|
+ if r.Topic == topic && r.Message == message {
|
|
|
|
|
+ ntfyEchoRecent = append(ntfyEchoRecent[:i], ntfyEchoRecent[i+1:]...)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// ntfyChatState 聊天室监听状态:当前订阅的主题与取消函数
|
|
// ntfyChatState 聊天室监听状态:当前订阅的主题与取消函数
|
|
|
type ntfyChatState struct {
|
|
type ntfyChatState struct {
|
|
|
mu sync.Mutex
|
|
mu sync.Mutex
|
|
@@ -484,8 +506,10 @@ func emitNtfyChat(topic, data string) {
|
|
|
// 去重 SSE 回声:自己刚发送的消息会被 ntfy 回传一次,
|
|
// 去重 SSE 回声:自己刚发送的消息会被 ntfy 回传一次,
|
|
|
// 若 8 秒内同主题同内容则判定为回声,直接丢弃(本地已显示)。
|
|
// 若 8 秒内同主题同内容则判定为回声,直接丢弃(本地已显示)。
|
|
|
if consumeEchoChat(topic, msg) {
|
|
if consumeEchoChat(topic, msg) {
|
|
|
|
|
+ logDebug("ECHO dropped: listenTopic=%q title=%q msg=%q", topic, title, msg)
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
+ logDebug("ECHO broadcast: listenTopic=%q title=%q msg=%q", topic, title, msg)
|
|
|
|
|
|
|
|
payload := NtfyChatMessage{
|
|
payload := NtfyChatMessage{
|
|
|
Topic: topic,
|
|
Topic: topic,
|
|
@@ -498,3 +522,18 @@ func emitNtfyChat(topic, data string) {
|
|
|
app.Event.Emit("ntfyChat", string(b))
|
|
app.Event.Emit("ntfyChat", string(b))
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+// logDebug 把调试日志追加写入 %TEMP%\ntfy_debug.log(临时诊断,定位后移除)。
|
|
|
|
|
+func logDebug(format string, args ...any) {
|
|
|
|
|
+ dir := os.Getenv("TEMP")
|
|
|
|
|
+ if dir == "" {
|
|
|
|
|
+ dir = os.TempDir()
|
|
|
|
|
+ }
|
|
|
|
|
+ path := filepath.Join(dir, "ntfy_debug.log")
|
|
|
|
|
+ f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ defer f.Close()
|
|
|
|
|
+ fmt.Fprintf(f, "%s %s\n", time.Now().Format("15:04:05.000"), fmt.Sprintf(format, args...))
|
|
|
|
|
+}
|