增加反链计算按钮
This commit is contained in:
+73
-8
@@ -13,6 +13,7 @@ import (
|
||||
"os" // 文件写入
|
||||
"path/filepath" // 路径拼接
|
||||
"strings" // 字符串操作
|
||||
"sync" // 互斥锁(保护手动触发的并发安全)
|
||||
"time" // 时间计算(下次运行时间、睡眠)
|
||||
|
||||
"sese-engine/storage" // 持久化存储
|
||||
@@ -22,6 +23,11 @@ import (
|
||||
type Runner struct {
|
||||
db *storage.DB
|
||||
storagePath string // 存储根目录(用于写入 prosper.json)
|
||||
mu sync.Mutex
|
||||
running bool // 是否正在计算中
|
||||
nextRun time.Time // 下次计划执行时间
|
||||
lastRunAt *time.Time // 上次完成时间(nil 表示尚未运行)
|
||||
lastError string // 上次错误信息(空字符串表示无错误)
|
||||
}
|
||||
|
||||
// New 创建一个 Runner 实例。
|
||||
@@ -29,6 +35,23 @@ func New(db *storage.DB, storagePath string) *Runner {
|
||||
return &Runner{db: db, storagePath: storagePath}
|
||||
}
|
||||
|
||||
// Status 返回反链计算器的当前状态快照。
|
||||
func (r *Runner) Status() map[string]interface{} {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
m := map[string]interface{}{
|
||||
"running": r.running,
|
||||
"next_run": r.nextRun.Format(time.RFC3339),
|
||||
}
|
||||
if r.lastRunAt != nil {
|
||||
m["last_run"] = r.lastRunAt.Format(time.RFC3339)
|
||||
}
|
||||
if r.lastError != "" {
|
||||
m["last_error"] = r.lastError
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// Run 无限循环,每 48 小时执行一次反向链接计算。
|
||||
// 每次运行对齐到凌晨 2:00(便于在低峰期执行重计算)。
|
||||
func (r *Runner) Run() {
|
||||
@@ -39,22 +62,64 @@ func (r *Runner) Run() {
|
||||
if !target.After(now) {
|
||||
target = target.Add(48 * time.Hour) // 已过凌晨 2 点,则等明天的 2 点
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
r.nextRun = target
|
||||
r.mu.Unlock()
|
||||
|
||||
sleep := target.Sub(now)
|
||||
log.Printf("[backlink] next run at %v (in %v)", target.Format(time.RFC3339), sleep.Round(time.Minute))
|
||||
time.Sleep(sleep)
|
||||
|
||||
log.Printf("[backlink] starting computation at %v", time.Now().Format(time.RFC3339))
|
||||
if err := r.compute(); err != nil {
|
||||
log.Printf("[backlink] error: %v", err)
|
||||
} else {
|
||||
log.Printf("[backlink] done")
|
||||
}
|
||||
r.doCompute()
|
||||
}
|
||||
}
|
||||
|
||||
// RunNow 立即执行一次计算(用于手动触发或测试)。
|
||||
// RunNow 立即执行一次计算(用于手动触发)。
|
||||
// 如果已有计算正在运行则返回 nil(幂等)。
|
||||
// 执行后重新计算下次自动运行的定时。
|
||||
func (r *Runner) RunNow() error {
|
||||
return r.compute()
|
||||
r.mu.Lock()
|
||||
if r.running {
|
||||
r.mu.Unlock()
|
||||
log.Printf("[backlink] RunNow: already running, skipping")
|
||||
return nil
|
||||
}
|
||||
r.mu.Unlock()
|
||||
r.doCompute()
|
||||
return nil
|
||||
}
|
||||
|
||||
// doCompute 执行一次完整的计算,更新状态字段。
|
||||
func (r *Runner) doCompute() {
|
||||
r.mu.Lock()
|
||||
r.running = true
|
||||
r.lastError = ""
|
||||
r.mu.Unlock()
|
||||
|
||||
log.Printf("[backlink] starting computation at %v", time.Now().Format(time.RFC3339))
|
||||
err := r.compute()
|
||||
|
||||
now := time.Now()
|
||||
r.mu.Lock()
|
||||
r.running = false
|
||||
r.lastRunAt = &now
|
||||
if err != nil {
|
||||
r.lastError = err.Error()
|
||||
}
|
||||
// 重新计算下次自动运行时间
|
||||
target := time.Date(now.Year(), now.Month(), now.Day(), 2, 0, 0, 0, now.Location())
|
||||
if !target.After(now) {
|
||||
target = target.Add(48 * time.Hour)
|
||||
}
|
||||
r.nextRun = target
|
||||
r.mu.Unlock()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("[backlink] error: %v", err)
|
||||
} else {
|
||||
log.Printf("[backlink] done, next run at %v", target.Format(time.RFC3339))
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 计算核心 ----
|
||||
|
||||
Vendored
-2
File diff suppressed because one or more lines are too long
Vendored
+2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -5,8 +5,8 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>SESE 爬取管理</title>
|
||||
<script type="module" crossorigin src="/assets/index-DXbMW8tX.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Bbt6GbEM.css">
|
||||
<script type="module" crossorigin src="/assets/index-w20XarNx.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CLWukEE8.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -164,6 +164,7 @@ func main() {
|
||||
|
||||
// ---- 6. 反向链接计算器:每 48 小时运行一次 ----
|
||||
bl := backlink.New(db, *storageDir)
|
||||
searchSrv.SetBacklinkRunner(bl)
|
||||
go bl.Run()
|
||||
|
||||
// ---- 7. 爬虫:从入口 URL 开始 BFS 爬取 ----
|
||||
|
||||
@@ -46,6 +46,12 @@ type Server struct {
|
||||
indexCache map[string][]storage.IndexEntry
|
||||
indexCacheMu sync.RWMutex
|
||||
indexCacheHits int64 // 缓存命中计数(原子)
|
||||
|
||||
// backlinkRunner 反向链接计算器(可为 nil,仅用于 admin 手动触发)
|
||||
backlinkRunner interface {
|
||||
Status() map[string]interface{}
|
||||
RunNow() error
|
||||
}
|
||||
}
|
||||
|
||||
// New 创建一个 search Server(内嵌收获服务,统一在同一端口)。
|
||||
@@ -64,6 +70,14 @@ func New(db *storage.DB, infoSvc *info.Service, a *analyzer.Analyzer) *Server {
|
||||
return s
|
||||
}
|
||||
|
||||
// SetBacklinkRunner 注入反向链接计算器(用于 admin 手动触发)。
|
||||
func (s *Server) SetBacklinkRunner(r interface {
|
||||
Status() map[string]interface{}
|
||||
RunNow() error
|
||||
}) {
|
||||
s.backlinkRunner = r
|
||||
}
|
||||
|
||||
// runPeriodicFlush 每隔 FlushIntervalSeconds 秒触发一次刷盘。
|
||||
func (s *Server) runPeriodicFlush() {
|
||||
ticker := time.NewTicker(time.Duration(config.FlushIntervalSeconds()) * time.Second)
|
||||
@@ -91,6 +105,7 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.HandleFunc("/admin/flush", s.handleAdminFlush)
|
||||
mux.HandleFunc("/admin/pending", s.handleAdminPending)
|
||||
mux.HandleFunc("/admin/workers", s.handleAdminWorkers)
|
||||
mux.HandleFunc("/admin/backlink", s.handleAdminBacklink)
|
||||
// 静态文件(SPA fallback)
|
||||
mux.Handle("/", spaHandler{dist: "dist"})
|
||||
return mux
|
||||
@@ -458,6 +473,39 @@ func (s *Server) handleAdminWorkers(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// handleAdminBacklink 查看反链计算状态和手动触发。
|
||||
// GET 返回 running(是否计算中)、next_run(下次执行时间)、last_run(上次完成时间)
|
||||
// POST 触发立即执行一次反链计算
|
||||
func (s *Server) handleAdminBacklink(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
if s.backlinkRunner == nil {
|
||||
http.Error(w, `{"error":"backlink runner not available"}`, 503)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodOptions:
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET,POST,OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
w.WriteHeader(204)
|
||||
return
|
||||
|
||||
case http.MethodGet:
|
||||
json.NewEncoder(w).Encode(s.backlinkRunner.Status())
|
||||
|
||||
case http.MethodPost:
|
||||
go s.backlinkRunner.RunNow() // 异步执行,避免阻塞 HTTP 请求
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "started"})
|
||||
|
||||
default:
|
||||
w.Header().Set("Allow", "GET,POST")
|
||||
http.Error(w, `{"error":"method not allowed"}`, 405)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 搜索处理器 ----
|
||||
|
||||
// searchResponse 是搜索 API 的 JSON 响应结构。
|
||||
|
||||
Reference in New Issue
Block a user