ci / vet-and-test (pull_request) Successful in 1m4s
Why: Known-boundary follow-up: owner opt-in auto page suggestions on semantic task events without self-trigger from surface revisions. Evidence: go test ./internal/genui/ ./internal/runmgr/ ./internal/settings/ ./internal/app/ Impact: Default-off settings key; material/plan/todo/document hooks nudge present. Refs: docs/proposals/2026-10-12-generative-ui-auto-trigger.md Co-authored-by: Cursor <cursoragent@cursor.com>
96 lines
3.0 KiB
Go
96 lines
3.0 KiB
Go
package genui
|
||
|
||
import (
|
||
"sync"
|
||
"time"
|
||
)
|
||
|
||
// AutoBudgetPerHour 是每任务(conversation)滑动一小时内允许的自动建议次数。
|
||
const AutoBudgetPerHour = 3
|
||
|
||
// AutoCooldown 是同会话两次自动触发的最小间隔。
|
||
const AutoCooldown = 60 * time.Second
|
||
|
||
// SettingAutoPresent 是 owner 级 opt-in 设置键(默认 false;与 activation 分离)。
|
||
const SettingAutoPresent = "plugins.generative-ui.autoPresent"
|
||
|
||
// AutoReminder 是注入 system-reminder 的建议文案(只引导 present,禁止改任务/点动作)。
|
||
const AutoReminder = "任务语义事件已发生(阶段或材料/工件完成)。若 generative-ui 已启用,请调用一次 generative-ui.present 生成或修订建议页,按当前任务阶段组织信息。不要点击页面 actions,不要改写 plan/todo/任务完成状态;任务事实只能来自真实任务记录。"
|
||
|
||
// ActivationLookup 查询 owner 的 Surface activation(generative-ui)。
|
||
type ActivationLookup func(owner string) (enabled bool)
|
||
|
||
// SettingsLookup 查询 owner 的 autoPresent opt-in。
|
||
type SettingsLookup func(owner string) (enabled bool)
|
||
|
||
// Auditor 写审计事件(耗尽等);nil = 跳过。
|
||
type Auditor func(owner, kind, detail string)
|
||
|
||
// Auto 是自动触发闸门:opt-in ∧ activation ∧ 预算 ∧ 冷却。
|
||
// 触发源由 runmgr 显式调用;present/surface/action 路径不得调用。
|
||
type Auto struct {
|
||
activation ActivationLookup
|
||
settings SettingsLookup
|
||
audit Auditor
|
||
now func() time.Time
|
||
|
||
mu sync.Mutex
|
||
budget map[string][]time.Time // convID → 触发时刻
|
||
last map[string]time.Time // convID → 上次成功放行
|
||
}
|
||
|
||
// NewAuto 构造闸门(lookup 不得为 nil)。
|
||
func NewAuto(activation ActivationLookup, settings SettingsLookup, audit Auditor) *Auto {
|
||
return &Auto{
|
||
activation: activation,
|
||
settings: settings,
|
||
audit: audit,
|
||
now: func() time.Time { return time.Now().UTC() },
|
||
budget: map[string][]time.Time{},
|
||
last: map[string]time.Time{},
|
||
}
|
||
}
|
||
|
||
// Try 尝试放行一次自动建议。ok=false 时 skip 含原因(供测试);ok=true 时调用方应注入 AutoReminder。
|
||
func (a *Auto) Try(owner, convID, reason string) (ok bool, skip string) {
|
||
if a == nil || a.activation == nil || a.settings == nil {
|
||
return false, "unconfigured"
|
||
}
|
||
if owner == "" || convID == "" {
|
||
return false, "missing_ids"
|
||
}
|
||
if !a.settings(owner) {
|
||
return false, "opt_in_off"
|
||
}
|
||
if !a.activation(owner) {
|
||
return false, "activation_off"
|
||
}
|
||
|
||
now := a.now()
|
||
a.mu.Lock()
|
||
defer a.mu.Unlock()
|
||
|
||
if t, hit := a.last[convID]; hit && now.Sub(t) < AutoCooldown {
|
||
return false, "cooldown"
|
||
}
|
||
|
||
cutoff := now.Add(-time.Hour)
|
||
kept := a.budget[convID][:0]
|
||
for _, t := range a.budget[convID] {
|
||
if t.After(cutoff) {
|
||
kept = append(kept, t)
|
||
}
|
||
}
|
||
a.budget[convID] = kept
|
||
if len(kept) >= AutoBudgetPerHour {
|
||
if a.audit != nil {
|
||
a.audit(owner, "genui.auto_present_exhausted", "conversation="+convID+" reason="+reason)
|
||
}
|
||
return false, "budget"
|
||
}
|
||
|
||
a.budget[convID] = append(kept, now)
|
||
a.last[convID] = now
|
||
return true, ""
|
||
}
|