ソースを参照

基本功能完成

soease 1 週間 前
親
コミット
38a8c05867

BIN
TengTuToolbox.exe


BIN
bin/debug_test.exe


BIN
bin/腾图工具箱.apk


BIN
bin/腾图工具箱.exe


+ 9 - 0
build/appicon.icon/Assets/wails_icon_vector.svg

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 583 533" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+    <g transform="matrix(1,0,0,1,-246,-251)">
+        <g id="Ebene1">
+            <path d="M246,251L265,784L401,784L506,450L507,450L505,784L641,784L829,251L682,251L596,567L595,567L596,251L478,251L378,568L391,251L246,251Z"/>
+        </g>
+    </g>
+</svg>

+ 51 - 0
build/appicon.icon/icon.json

@@ -0,0 +1,51 @@
+{
+  "fill" : {
+    "automatic-gradient" : "extended-gray:1.00000,1.00000"
+  },
+  "groups" : [
+    {
+      "layers" : [
+        {
+          "fill-specializations" : [
+            {
+              "appearance" : "dark",
+              "value" : {
+                "solid" : "srgb:0.92143,0.92145,0.92144,1.00000"
+              }
+            },
+            {
+              "appearance" : "tinted",
+              "value" : {
+                "solid" : "srgb:0.83742,0.83744,0.83743,1.00000"
+              }
+            }
+          ],
+          "image-name" : "wails_icon_vector.svg",
+          "name" : "wails_icon_vector",
+          "position" : {
+            "scale" : 0.85,
+            "translation-in-points" : [
+              0.0,
+              0.0
+            ]
+          }
+        }
+      ],
+      "shadow" : {
+        "kind" : "neutral",
+        "opacity" : 0.5
+      },
+      "specular" : true,
+      "translucency" : {
+        "enabled" : true,
+        "value" : 0.5
+      }
+    }
+  ],
+  "supported-platforms" : {
+    "circles" : [
+      "watchOS"
+    ],
+    "squares" : "shared"
+  }
+}

BIN
build/bin/TengTuToolbox.exe


+ 2 - 0
frontend/src/main.js

@@ -2204,8 +2204,10 @@ Events.On('ntfyChat', (event) => {
         m.topic === msg.topic && m.message === msg.message);
     if (idx > -1) {
         chatLocalRecent.splice(idx, 1); // 消费该回声记录,避免后续重复命中
+        chatStatusEl.textContent = '[前端去重] 丢弃回声: ' + (msg.message || '').slice(0, 30);
         return;
     }
+    chatStatusEl.textContent = '[前端未去重] 广播: topic=' + (msg.topic || '') + ' msg=' + (msg.message || '').slice(0, 30);
     appendChatMessage(msg);
 });
 

+ 41 - 2
ntfy.go

@@ -157,13 +157,22 @@ func (s *SettingsService) GetNtfyNicknames() []string {
 // 与 SendNtfyMessage 的区别:昵称固定作为标题,便于聊天室按昵称区分发言者。
 func (s *SettingsService) SendNtfyChat(topic, nickname, message string) error {
 	nickname = strings.TrimSpace(nickname)
+	topic = strings.TrimSpace(topic)
 	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)
 	if err != nil {
+		// 发送失败:移除刚记录的回声,避免误吞后续同主题同内容的真实消息
+		forgetLocalChat(topic, message)
 		return err
 	}
-	AddNtfyNickname(nickname)   // 发送成功后记忆昵称
-	rememberLocalChat(topic, message) // 记录本地发送,用于去重 SSE 回声
+	AddNtfyNickname(nickname) // 发送成功后记忆昵称
 	return nil
 }
 
@@ -313,6 +322,19 @@ func consumeEchoChat(topic, message string) bool {
 	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 聊天室监听状态:当前订阅的主题与取消函数
 type ntfyChatState struct {
 	mu     sync.Mutex
@@ -484,8 +506,10 @@ func emitNtfyChat(topic, data string) {
 	// 去重 SSE 回声:自己刚发送的消息会被 ntfy 回传一次,
 	// 若 8 秒内同主题同内容则判定为回声,直接丢弃(本地已显示)。
 	if consumeEchoChat(topic, msg) {
+		logDebug("ECHO dropped: listenTopic=%q title=%q msg=%q", topic, title, msg)
 		return
 	}
+	logDebug("ECHO broadcast: listenTopic=%q title=%q msg=%q", topic, title, msg)
 
 	payload := NtfyChatMessage{
 		Topic:   topic,
@@ -498,3 +522,18 @@ func emitNtfyChat(topic, data string) {
 		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...))
+}