feat: 扩展弹框配置重构
All checks were successful
Extension Build & Release / build (push) Successful in 1m32s
Backend Deploy (Go + Docker) / deploy (push) Successful in 1m51s

This commit is contained in:
zs
2026-03-03 15:32:33 +08:00
parent eb7efae32a
commit d82d59cbe4
6 changed files with 89 additions and 55 deletions

View File

@@ -1,20 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, inject, computed, type Ref } from 'vue'
const props = defineProps<{ // Injected reactive refs from sidebar-mount.ts
tweetData?: { const currentTweetData = inject<Ref<any>>('currentTweetData', ref(null))
id: string; const isVisible = inject<Ref<boolean>>('sidebarVisible', ref(false))
author: string;
content: string; // Computed getter for template convenience
stats: { const tweetData = computed(() => currentTweetData.value)
replies: string;
retweets: string;
likes: string;
}
}
}>()
const isVisible = ref(false) // Start hidden, wait for trigger
const selectedStrategy = ref('Insightful') const selectedStrategy = ref('Insightful')
const generatedReplies = ref<Array<{strategy: string, content: string}>>([]) const generatedReplies = ref<Array<{strategy: string, content: string}>>([])
const isGenerating = ref(false) const isGenerating = ref(false)
@@ -29,30 +22,21 @@ const defaultStrategies = [
const strategies = ref([...defaultStrategies]) const strategies = ref([...defaultStrategies])
onMounted(() => { // Fetch custom strategies on mount
// Listen for toggle messages directly in the component chrome.runtime.sendMessage({ type: 'FETCH_CUSTOM_STRATEGIES' }, (response) => {
chrome.runtime.onMessage.addListener((message) => { if (response && response.success && response.data) {
if (message.type === 'TOGGLE_SIDEBAR') { const customStrategies = response.data.map((s: any) => ({
isVisible.value = !isVisible.value id: s.strategy_key,
} else if (message.type === 'SHOW_INSIGHT') { label: s.label,
isVisible.value = true icon: '<path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/>'
} }))
}) strategies.value = [...defaultStrategies, ...customStrategies]
}
chrome.runtime.sendMessage({ type: 'FETCH_CUSTOM_STRATEGIES' }, (response) => {
if (response && response.success && response.data) {
const customStrategies = response.data.map((s: any) => ({
id: s.strategy_key,
label: s.label,
icon: '<path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/>'
}))
strategies.value = [...defaultStrategies, ...customStrategies]
}
})
}) })
const generate = () => { const generate = () => {
if (!props.tweetData) return if (!tweetData.value) return
isGenerating.value = true isGenerating.value = true
generatedReplies.value = [] generatedReplies.value = []
@@ -68,7 +52,7 @@ const generate = () => {
chrome.runtime.sendMessage({ chrome.runtime.sendMessage({
type: 'GENERATE_REPLY', type: 'GENERATE_REPLY',
payload: { payload: {
tweetContent: props.tweetData!.content, tweetContent: tweetData.value!.content,
strategy: selectedStrategy.value, strategy: selectedStrategy.value,
identity: finalIdentity identity: finalIdentity
} }
@@ -93,7 +77,7 @@ const copyToClipboard = async (reply: any) => {
showProfileTip.value = true showProfileTip.value = true
setTimeout(() => { showProfileTip.value = false }, 7000) setTimeout(() => { showProfileTip.value = false }, 7000)
if (!props.tweetData || !props.tweetData.id) return; if (!tweetData.value || !tweetData.value.id) return;
chrome.storage.local.get(['jwt_token'], async (result) => { chrome.storage.local.get(['jwt_token'], async (result) => {
if (result.jwt_token) { if (result.jwt_token) {
@@ -107,7 +91,7 @@ const copyToClipboard = async (reply: any) => {
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
}, },
body: JSON.stringify({ body: JSON.stringify({
tweet_id: props.tweetData?.id, tweet_id: tweetData.value?.id,
strategy_type: reply.strategy || 'General', strategy_type: reply.strategy || 'General',
content: reply.content, content: reply.content,
language: 'en' language: 'en'

View File

@@ -1,13 +1,19 @@
import { createApp } from 'vue' import { createApp, ref } from 'vue'
import Sidebar from './Sidebar.vue' import Sidebar from './Sidebar.vue'
import '../assets/tailwind.css' // We might need to handle this specially for Shadow DOM
const MOUNT_ID = 'insight-reply-sidebar-root' const MOUNT_ID = 'insight-reply-sidebar-root'
function initSidebar(tweetData?: any) { // Shared reactive tweet data that the Sidebar component will receive
let host = document.getElementById(MOUNT_ID) const currentTweetData = ref<any>(null)
const sidebarVisible = ref(false)
if (host) return; let isMounted = false
function initSidebar() {
if (isMounted) return
let host = document.getElementById(MOUNT_ID)
if (host) return
// 1. Create Host Element // 1. Create Host Element
host = document.createElement('div') host = document.createElement('div')
@@ -48,17 +54,42 @@ function initSidebar(tweetData?: any) {
} }
injectStyles() injectStyles()
// 5. Create Vue App // 5. Create Vue App with reactive provide
const app = createApp(Sidebar, { tweetData }) const app = createApp(Sidebar)
app.provide('currentTweetData', currentTweetData)
app.provide('sidebarVisible', sidebarVisible)
app.mount(container) app.mount(container)
isMounted = true
console.log('[InsightReply] Sidebar mounted successfully')
} }
// Ensure it mounts on load if needed, but primarily triggered by messages function showSidebar(tweetData?: any) {
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { initSidebar()
if (message.type === 'SHOW_INSIGHT' || message.type === 'TOGGLE_SIDEBAR') {
initSidebar(message.payload); if (tweetData) {
// The component itself listens for TOGGLE_SIDEBAR to show/hide currentTweetData.value = tweetData
sendResponse({ received: true }); console.log('[InsightReply] Tweet data updated:', tweetData.content?.substring(0, 60))
} }
return true;
}); sidebarVisible.value = true
}
function toggleSidebar() {
initSidebar()
sidebarVisible.value = !sidebarVisible.value
}
// Listen for messages from background script
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
console.log('[InsightReply SidebarMount] Received message:', message.type)
if (message.type === 'SHOW_INSIGHT') {
showSidebar(message.payload)
sendResponse({ received: true })
} else if (message.type === 'TOGGLE_SIDEBAR') {
toggleSidebar()
sendResponse({ received: true })
}
return true
})

View File

@@ -39,6 +39,11 @@ const loadHotTweets = async () => {
} }
}) })
if (response.status === 429) {
error.value = '请求频率达到上限请稍后再试。Free 用户每日限额 10 次 API 调用。'
return
}
if (!response.ok) { if (!response.ok) {
throw new Error(`Opportunity discovery failed (${response.status})`) throw new Error(`Opportunity discovery failed (${response.status})`)
} }
@@ -46,7 +51,7 @@ const loadHotTweets = async () => {
const data = await response.json() const data = await response.json()
tweets.value = data || [] tweets.value = data || []
} catch (err: any) { } catch (err: any) {
error.value = err.message || 'Quantum network sync error.' error.value = err.message || 'Network connection error.'
} finally { } finally {
loading.value = false loading.value = false
} }

View File

@@ -2,6 +2,7 @@ package handler
import ( import (
"encoding/json" "encoding/json"
"log"
"net/http" "net/http"
"strings" "strings"
@@ -53,6 +54,7 @@ func (h *AIHandler) Generate(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
userID := ctx.Value("userID").(string) userID := ctx.Value("userID").(string)
log.Printf("[AIHandler] Generate request from userID=%s strategy=%q provider=%q model=%q", userID, body.Strategy, body.Provider, body.Model)
// Fetch Product Profile Context // Fetch Product Profile Context
var productContext string var productContext string
@@ -79,9 +81,11 @@ func (h *AIHandler) Generate(w http.ResponseWriter, r *http.Request) {
replyString, err := h.svc.GenerateReply(ctx, body.TweetContent, productContext, body.Identity, body.Provider, body.Model) replyString, err := h.svc.GenerateReply(ctx, body.TweetContent, productContext, body.Identity, body.Provider, body.Model)
if err != nil { if err != nil {
log.Printf("[AIHandler] ERROR GenerateReply for userID=%s: %v", userID, err)
SendError(w, http.StatusBadGateway, 5002, "Failed to generate AI reply: "+err.Error()) SendError(w, http.StatusBadGateway, 5002, "Failed to generate AI reply: "+err.Error())
return return
} }
log.Printf("[AIHandler] GenerateReply success for userID=%s, reply length=%d", userID, len(replyString))
// Clean up potential markdown wrappers from LLM output // Clean up potential markdown wrappers from LLM output
cleanReply := strings.TrimSpace(replyString) cleanReply := strings.TrimSpace(replyString)

View File

@@ -2,6 +2,7 @@ package handler
import ( import (
"encoding/json" "encoding/json"
"log"
"net/http" "net/http"
"github.com/zs/InsightReply/internal/repository" "github.com/zs/InsightReply/internal/repository"
@@ -17,15 +18,18 @@ func NewTweetHandler(repo *repository.TweetRepository) *TweetHandler {
// GetHotTweets returns the top heating tweets spanning across all tracking targets // GetHotTweets returns the top heating tweets spanning across all tracking targets
func (h *TweetHandler) GetHotTweets(w http.ResponseWriter, r *http.Request) { func (h *TweetHandler) GetHotTweets(w http.ResponseWriter, r *http.Request) {
// Standardize to take the top 50 hottest tweets that haven't been manually marked as processed log.Printf("[TweetHandler] GetHotTweets called from %s", r.RemoteAddr)
tweets, err := h.repo.GetTopHeatingTweets(50) tweets, err := h.repo.GetTopHeatingTweets(50)
if err != nil { if err != nil {
log.Printf("[TweetHandler] ERROR GetTopHeatingTweets: %v", err)
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]interface{}{"error": "failed to retrieve hot tweets"}) json.NewEncoder(w).Encode(map[string]interface{}{"error": "failed to retrieve hot tweets"})
return return
} }
log.Printf("[TweetHandler] GetHotTweets returning %d tweets", len(tweets))
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(tweets) json.NewEncoder(w).Encode(tweets)
} }
@@ -34,15 +38,18 @@ func (h *TweetHandler) GetHotTweets(w http.ResponseWriter, r *http.Request) {
func (h *TweetHandler) GetSearchTweets(w http.ResponseWriter, r *http.Request) { func (h *TweetHandler) GetSearchTweets(w http.ResponseWriter, r *http.Request) {
keyword := r.URL.Query().Get("keyword") keyword := r.URL.Query().Get("keyword")
handle := r.URL.Query().Get("handle") handle := r.URL.Query().Get("handle")
log.Printf("[TweetHandler] SearchTweets called: keyword=%q handle=%q", keyword, handle)
tweets, err := h.repo.SearchTweets(keyword, handle, 50) tweets, err := h.repo.SearchTweets(keyword, handle, 50)
if err != nil { if err != nil {
log.Printf("[TweetHandler] ERROR SearchTweets: %v", err)
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]interface{}{"error": "failed to search tweets"}) json.NewEncoder(w).Encode(map[string]interface{}{"error": "failed to search tweets"})
return return
} }
log.Printf("[TweetHandler] SearchTweets returning %d tweets", len(tweets))
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(tweets) json.NewEncoder(w).Encode(tweets)
} }

View File

@@ -1,6 +1,7 @@
package middleware package middleware
import ( import (
"log"
"net/http" "net/http"
"sync" "sync"
"time" "time"
@@ -49,6 +50,7 @@ func RateLimit(db *gorm.DB) func(http.Handler) http.Handler {
// For now, fallback to generic tight limit for anonymous usage // For now, fallback to generic tight limit for anonymous usage
ipLimiter := getLimiter(r.RemoteAddr, "Free") ipLimiter := getLimiter(r.RemoteAddr, "Free")
if !ipLimiter.Allow() { if !ipLimiter.Allow() {
log.Printf("[RateLimit] 429 for anonymous IP=%s path=%s", r.RemoteAddr, r.URL.Path)
http.Error(w, `{"code":429, "message":"Too Many Requests: Rate limit exceeded"}`, http.StatusTooManyRequests) http.Error(w, `{"code":429, "message":"Too Many Requests: Rate limit exceeded"}`, http.StatusTooManyRequests)
return return
} }
@@ -72,6 +74,7 @@ func RateLimit(db *gorm.DB) func(http.Handler) http.Handler {
limiter := getLimiter(userID, tier) limiter := getLimiter(userID, tier)
if !limiter.Allow() { if !limiter.Allow() {
log.Printf("[RateLimit] 429 for userID=%s tier=%s path=%s", userID, tier, r.URL.Path)
http.Error(w, `{"code":429, "message":"Too Many Requests: Daily quota or rate limit exceeded"}`, http.StatusTooManyRequests) http.Error(w, `{"code":429, "message":"Too Many Requests: Daily quota or rate limit exceeded"}`, http.StatusTooManyRequests)
return return
} }