feat(web-monitor): 网页变动订阅新增可选的 AI 判定 - #840
Conversation
Add an optional LLM-based judgement to the web monitor source. The LLM evaluates extracted field values against a user instruction and produces a short verdict label (e.g. available/unavailable). The verdict is injected as an extra template variable and can be selected as a key field so the RSS GUID - and therefore change notifications - is driven by semantic state instead of raw text diffs. - config: add WebMonitorAIJudgeConfig (enabled/prompt/output_field/model) - parser: run judge before key-field validation; deterministic context, temperature 0, cached via adapter.CallLLMUsingContext for GUID stability - frontend: add collapsible AI Judgement card in the wizard step 2 + i18n - tests: cover verdict injection, GUID drive, conflicts, errors, disabled Co-authored-by: Colin <Colin_XKL@outlook.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Reviewer's GuideThis PR adds an optional AI-based judgement step to the Web Monitor parser and its UI so that semantic verdicts (e.g., available/unavailable) can drive templates and GUIDs instead of raw text changes, with strict validation, deterministic LLM calling, tests, and docs updates. Sequence diagram for Web Monitor preview with optional AI judgementsequenceDiagram
actor User
participant WebMonitorUI as web_monitor.vue
participant AdminAPI as /api/admin/tools/web-monitor/preview
participant WebMonitorParser as WebMonitorParser
participant LLM as adapter.CallLLMUsingContext
User->>WebMonitorUI: click runPreview
WebMonitorUI->>WebMonitorUI: buildParserConfig()
WebMonitorUI->>AdminAPI: POST web_monitor_parser (includes ai_judge)
AdminAPI->>WebMonitorParser: PreviewWebMonitor(data, cfg, pageURL)
WebMonitorParser->>WebMonitorParser: prepare(data, pageURL)
WebMonitorParser->>WebMonitorParser: extractWebMonitorValues()
WebMonitorParser->>WebMonitorParser: applyAIJudge(values)
WebMonitorParser->>LLM: webMonitorJudgeCaller(buildWebMonitorJudgePrompt, buildWebMonitorJudgeContext, ContentProcessOption)
LLM-->>WebMonitorParser: verdict
WebMonitorParser->>WebMonitorParser: normalizeWebMonitorVerdict()
WebMonitorParser->>WebMonitorParser: renderWebMonitorPreview()
WebMonitorParser-->>AdminAPI: WebMonitorPreviewResult (includes ai_verdict)
AdminAPI-->>WebMonitorUI: preview.values
WebMonitorUI->>WebMonitorUI: aiVerdict computed from preview.values
WebMonitorUI-->>User: show AI Verdict tag and preview
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| Complexity | 2 medium |
🟢 Metrics 22 complexity · 14 duplication
Metric Results Complexity 22 Duplication 14
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
Code Review
This pull request introduces an optional LLM-based AI judgement step to the web monitor parser, allowing users to evaluate extracted fields against a prompt to produce stable verdict labels that can drive change notifications. The feedback suggests several improvements: adding defensive nil checks to prevent potential panics in the parser, ensuring custom LLM models are correctly forwarded to the LLM caller, making the verdict normalization more robust against markdown formatting, and implementing frontend validation to prevent variable name conflicts.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| func (p *WebMonitorParser) prepare(data []byte, pageURL string) (*goquery.Document, map[string]string, *compiledWebMonitorTemplates, error) { | ||
| if len(p.Config.Extractors) == 0 { | ||
| return nil, nil, nil, fmt.Errorf("extractors are required") | ||
| } |
There was a problem hiding this comment.
If p or p.Config is nil, calling prepare will result in a nil pointer dereference panic when accessing p.Config.Extractors. While Parse has a nil check, PreviewWebMonitor does not check if cfg is nil before calling prepare. Adding a defensive nil check at the beginning of prepare prevents potential panics.
func (p *WebMonitorParser) prepare(data []byte, pageURL string) (*goquery.Document, map[string]string, *compiledWebMonitorTemplates, error) {
if p == nil || p.Config == nil {
return nil, nil, nil, fmt.Errorf("parser config is nil")
}
if len(p.Config.Extractors) == 0 {
return nil, nil, nil, fmt.Errorf("extractors are required")
}| verdict, err := webMonitorJudgeCaller( | ||
| buildWebMonitorJudgePrompt(prompt), | ||
| buildWebMonitorJudgeContext(values), | ||
| util.ContentProcessOption{Temperature: util.LowestLLMTemperaturePtr()}, | ||
| ) |
There was a problem hiding this comment.
The cfg.Model configuration is currently ignored. The webMonitorJudgeCaller (which defaults to adapter.CallLLMUsingContext) hardcodes UseDefaultModel and does not accept or forward a custom model parameter. To support custom models, adapter.CallLLMUsingContext needs to be updated to accept a model parameter (e.g., via util.ContentProcessOption), and it must be passed here.
| func normalizeWebMonitorVerdict(raw string) string { | ||
| verdict := strings.TrimSpace(raw) | ||
| if idx := strings.IndexAny(verdict, "\r\n"); idx >= 0 { | ||
| verdict = strings.TrimSpace(verdict[:idx]) | ||
| } | ||
| return verdict | ||
| } |
There was a problem hiding this comment.
LLMs occasionally wrap their outputs in markdown code blocks or quotes, despite prompt instructions. The current normalizeWebMonitorVerdict only trims whitespace and takes the first line, which would fail to extract the clean verdict in those cases. Consider adding robust stripping of markdown code blocks and surrounding quotes.
func normalizeWebMonitorVerdict(raw string) string {
verdict := strings.TrimSpace(raw)
tripleBacktick := string([]byte{96, 96, 96})
if strings.HasPrefix(verdict, tripleBacktick) {
verdict = strings.TrimPrefix(verdict, tripleBacktick)
verdict = strings.TrimPrefix(verdict, "text")
if idx := strings.Index(verdict, tripleBacktick); idx >= 0 {
verdict = verdict[:idx]
}
verdict = strings.TrimSpace(verdict)
}
if idx := strings.IndexAny(verdict, "\r\n"); idx >= 0 {
verdict = strings.TrimSpace(verdict[:idx])
}
verdict = strings.Trim(verdict, "\"'")
verdict = strings.Trim(verdict, string(96))
return verdict
}| if (aiJudge.enabled && !aiJudge.prompt.trim()) { | ||
| Message.warning(t('webMonitor.msg.aiPromptRequired')); | ||
| return false; | ||
| } |
There was a problem hiding this comment.
When AI judgement is enabled, the frontend should validate that the output variable name (aiJudgeFieldName) does not conflict with the reserved variable url or any existing extractor names in fields. Currently, this is only validated on the backend, which leads to a poor user experience when the form submission fails.
if (aiJudge.enabled) {
if (!aiJudge.prompt.trim()) {
Message.warning(t('webMonitor.msg.aiPromptRequired'));
return false;
}
const judgeField = aiJudgeFieldName.value;
if (judgeField === 'url') {
Message.warning('Output variable name cannot be the reserved "url"');
return false;
}
if (fields.value.some((f) => f.name.trim() === judgeField)) {
Message.warning('Output variable name conflicts with an existing variable');
return false;
}
}
背景
在已有的「网页内容变化监控(Web Monitor)」能力(见
proposal/web_monitor_plan.md)之上,新增可选的 AI 判定步骤,将"原始文本变化"升级为"语义状态变化"。纯文本 key field 有时过于敏感:库存文案可能在
Sold Out/Currently unavailable/缺货之间反复横跳,但语义上都是"不可购买"。用户真正关心的是"能不能买"。AI 判定让 LLM 根据提取到的字段值产出一个简短、稳定的判定标签(如available/unavailable),再把它当作派生变量参与模板渲染与 GUID 生成。改动
WebMonitorParserConfig新增可选ai_judge(enabled/prompt/output_field/model)。0。adapter.CallLLMUsingContext(按 prompt+context 哈希缓存),保证请求驱动模型下相同页面状态得到相同判定,避免 GUID 抖动造成通知风暴。output_field不可为保留变量url,也不可与已有 extractor 重名;判定失败直接报错(不静默 fallback)。proposal/web_monitor_plan.md补充「9.5 可选的 AI 判定」章节。工作机制
用户通常把
ai_verdict勾选为 key field:GUID 由语义判定驱动,仅当判定结果变化时 RSS Reader 才识别为新 item,从而精准推送"语义状态变化"而非文案抖动。测试
go test ./...task fix(golangci-lint 0 issues;前端仅有预存在的 v-html 告警)task backend-buildtask frontend-buildpnpm run type:checkSummary by Sourcery
Add an optional LLM-based judgement step to Web Monitor so feeds can be driven by semantic verdicts rather than raw text changes.
New Features:
Enhancements:
Documentation:
Tests: