Skip to content

feat(web-monitor): 网页变动订阅新增可选的 AI 判定 - #840

Draft
Colin-XKL wants to merge 1 commit into
devfrom
cursor/web-monitor-ai-judge-9b02
Draft

feat(web-monitor): 网页变动订阅新增可选的 AI 判定#840
Colin-XKL wants to merge 1 commit into
devfrom
cursor/web-monitor-ai-judge-9b02

Conversation

@Colin-XKL

@Colin-XKL Colin-XKL commented Jun 29, 2026

Copy link
Copy Markdown
Owner

背景

在已有的「网页内容变化监控(Web Monitor)」能力(见 proposal/web_monitor_plan.md)之上,新增可选的 AI 判定步骤,将"原始文本变化"升级为"语义状态变化"。

纯文本 key field 有时过于敏感:库存文案可能在 Sold Out / Currently unavailable / 缺货 之间反复横跳,但语义上都是"不可购买"。用户真正关心的是"能不能买"。AI 判定让 LLM 根据提取到的字段值产出一个简短、稳定的判定标签(如 available / unavailable),再把它当作派生变量参与模板渲染与 GUID 生成。

改动

  • configWebMonitorParserConfig 新增可选 ai_judgeenabled / prompt / output_field / model)。
  • parser
    • AI 判定先于 key field 校验执行,因此判定结果本身可被选为 key field。
    • 输入上下文按 key 排序,确定性拼接;温度固定为 0
    • 复用 adapter.CallLLMUsingContext(按 prompt+context 哈希缓存),保证请求驱动模型下相同页面状态得到相同判定,避免 GUID 抖动造成通知风暴。
    • 校验:启用时 prompt 必填;output_field 不可为保留变量 url,也不可与已有 extractor 重名;判定失败直接报错(不静默 fallback)。
  • frontend:Web Monitor 向导第 2 步新增可折叠的「AI 判定(可选)」卡片(开关 / 判定指令 / 输出变量名 / 模型 / 一键勾选用作关键字段),预览结果以标签展示判定值;中英文 i18n 同步。
  • tests:覆盖判定变量注入、AI 判定驱动 GUID、自定义输出字段、prompt 缺失、字段冲突、判定报错、未启用时不调用等。
  • docs:在 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-build
  • task frontend-build
  • pnpm run type:check
  • ✅ 手动联调:mock LLM 端点下通过向导启用 AI 判定并运行预览,验证判定标签注入、模板渲染与 GUID 行为(见下方演示)。
Open in Web Open in Cursor 

Summary 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:

  • Introduce configurable AI judgement (prompt, output variable, model) in Web Monitor parser to derive a stable verdict label from extracted fields.
  • Expose an "AI 判定 / AI Judgement" card in the Web Monitor wizard to configure the judgement instruction, output variable, model, and whether the verdict should be a key field, and show the verdict in preview.

Enhancements:

  • Ensure AI verdict is injected before key-field validation so it can be used as a key field driving GUID stability.
  • Build deterministic LLM prompt/context and normalize verdict output to keep GUIDs stable across identical page states.
  • Refactor Web Monitor frontend parser config construction to include optional ai_judge settings when enabled.

Documentation:

  • Extend the Web Monitor proposal with a new section describing the optional AI judgement capability, configuration, flow, and stability considerations.

Tests:

  • Add tests covering AI verdict injection, GUID behavior when verdict changes vs raw text changes, custom output field handling, missing prompt and output-field conflicts, error propagation, and disabled mode behavior.

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>
@vercel

vercel Bot commented Jun 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
feed-craft-admin Ready Ready Preview, Comment Jun 29, 2026 7:28am
feed-craft-doc Ready Ready Preview, Comment Jun 29, 2026 7:28am

@sourcery-ai

sourcery-ai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

This 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 judgement

sequenceDiagram
    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
Loading

File-Level Changes

Change Details Files
Introduce configurable AI judgement to WebMonitorParser, calling an LLM deterministically before key-field validation and injecting a normalized verdict variable that can drive GUIDs.
  • Extend WebMonitorParserConfig with an optional AIJudge block (enabled/prompt/output_field/model).
  • Add DefaultWebMonitorAIJudgeField, a package-level webMonitorJudgeCaller using adapter.CallLLMUsingContext, and applyAIJudge in prepare to run judgement before key-field checks.
  • Build LLM prompt and a key-sorted context string, call the judge at fixed temperature 0, and normalize the verdict (trim + first line only) into the chosen output field while enforcing reserved-name and extractor-name constraints.
  • Refactor prepare/PreviewWebMonitor to accept pageURL, always inject url into values, and surface AI judge errors directly without fallback.
internal/config/source_config.go
internal/source/parser/web_monitor_parser.go
Update the Web Monitor frontend wizard to configure and preview the AI judgement step and to send the correct parser config to backend.
  • Add an AI Judgement card in step 2 with enable switch, judgement prompt textarea, output field and model inputs, and a checkbox to use the verdict as a key field.
  • Track aiJudge state (including default outputField ai_verdict and useAsKey) and compute aiJudgeFieldName, selectedKeyFields (including AI verdict when enabled/useAsKey), and aiVerdict from preview values.
  • Validate that when AI judgement is enabled the prompt is non-empty, and refactor preview/save requests to build parser config via buildParserConfig including ai_judge when enabled.
  • Display the AI verdict as a purple tag in the preview section and import IconInfoCircle for tooltip UI.
web/admin/src/views/dashboard/web_monitor/web_monitor.vue
Add tests covering AI judgement behavior, config validation, GUID stability, and ensure AI judge is only called when enabled.
  • Introduce stubWebMonitorJudge helper to stub the package-level LLM caller in tests.
  • Test verdict injection into templates, deterministic context includes stock and url, and temperature fixed at 0.
  • Test GUID being driven by ai_verdict instead of raw stock text, including unchanged GUID on text-only changes and changed GUID when verdict flips.
  • Test required prompt, output_field conflicts with existing extractor, judge error propagation, and that AI judge is not called when disabled.
internal/source/parser/web_monitor_parser_test.go
Document the optional AI judgement extension and add i18n entries for the new frontend UI.
  • Add section 9.5 to web_monitor_plan.md describing AI judgement motivation, config model, flow, stability considerations, and frontend entry.
  • Extend en-US and zh-CN webMonitor locale files with AI judgement labels, tooltips, placeholders, verdict label text, and validation message for missing prompt when enabled.
proposal/web_monitor_plan.md
web/admin/src/locale/en-US/webMonitor.ts
web/admin/src/locale/zh-CN/webMonitor.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@codacy-production

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 2 medium

Alerts:
⚠ 2 issues (≤ 0 issues of at least medium severity)

Results:
2 new issues

Category Results
Complexity 2 medium

View in Codacy

🟢 Metrics 22 complexity · 14 duplication

Metric Results
Complexity 22
Duplication 14

View in Codacy

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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +90 to 93
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")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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")
	}

Comment on lines +152 to +156
verdict, err := webMonitorJudgeCaller(
buildWebMonitorJudgePrompt(prompt),
buildWebMonitorJudgeContext(values),
util.ContentProcessOption{Temperature: util.LowestLLMTemperaturePtr()},
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Comment on lines +195 to +201
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
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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
}

Comment on lines +724 to +727
if (aiJudge.enabled && !aiJudge.prompt.trim()) {
Message.warning(t('webMonitor.msg.aiPromptRequired'));
return false;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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;
      }
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants