Skip to content

feat: support HTML to RSS navigation actions - #833

Draft
Colin-XKL wants to merge 2 commits into
devfrom
cursor/html-to-rss-navigation-actions-ace1
Draft

feat: support HTML to RSS navigation actions#833
Colin-XKL wants to merge 2 commits into
devfrom
cursor/html-to-rss-navigation-actions-ace1

Conversation

@Colin-XKL

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

Copy link
Copy Markdown
Owner

Summary

  • Add http_fetcher.navigation_actions for ordered browser actions before HTML capture.
  • Execute click, wait_for_selector, and wait actions in Browserless REST /function and CDP rendering paths.
  • Add HTML to RSS wizard controls, localized labels, and docs for pre-navigation actions.
  • Constrain navigation action count, per-step timeout/wait duration, total wait budget, and normalize hand-written action fields before execution.

Walkthrough

html_to_rss_navigation_actions_demo.mp4
HTML to RSS navigation actions final state

Testing

  • go test ./internal/util -run 'Test(GetBrowserlessContentUsesFunctionEndpointForNavigationActions|ValidateBrowserNavigationActionsRejectsInvalidAction)'
  • go test ./internal/util -run 'TestValidateBrowserNavigationActions(NormalizesActionFields|RejectsExcessiveWait)'
  • gofmt -w ... && go test ./internal/util ./internal/source/fetcher
  • pnpm run format && pnpm run type:check in web/admin
  • task fix
  • go test ./...
  • task backend-build
  • task frontend-build
  • Manual UI walkthrough in browser

To show artifacts inline, enable in settings.

Open in Web Open in Cursor 

@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 Jul 28, 2026 11:09am
feed-craft-doc Ready Ready Preview, Comment Jul 28, 2026 11:09am

@sourcery-ai sourcery-ai 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.

Sorry, we are unable to review this pull request

The GitHub API does not allow us to fetch diffs exceeding 20000 lines

@codacy-production

codacy-production Bot commented Jun 29, 2026

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 1 critical · 1 high · 40 medium · 5 minor

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

Results:
47 new issues

Category Results
BestPractice 5 minor
Security 1 critical
1 high
Complexity 40 medium

View in Codacy

🟢 Metrics 1575 complexity · 136 duplication

Metric Results
Complexity 1575
Duplication 136

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 several major features to FeedCraft, including a Web Monitor source for tracking webpage changes, an Inbox system for third-party HTTP data pushes with token authentication, semantic topic filtering and deduplication via embedding models, and a transition to a functional closure-based CraftOption processing pipeline. The code review highlights critical security vulnerabilities, including SSRF risks from removed private IP checks in the feed viewer and Stored XSS risks in the public inbox content endpoint. Additionally, the reviewer points out significant performance and stability issues, such as socket churn from repeated Redis client instantiations, data loss risks when updating inboxes or setting maximum item limits, and potential runtime panics in embedding batching and cosine similarity calculations.

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 360 to +373
func validateFeedViewerURL(rawURL string) error {
parsedURL, err := url.Parse(rawURL)
if err != nil || parsedURL == nil {
return errors.New("Please enter a valid http(s) feed URL")
return errors.New(feedViewerInvalidURLError)
}
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
return errors.New("Please enter a valid http(s) feed URL")
return errors.New(feedViewerInvalidURLError)
}
if parsedURL.Hostname() == "" {
return errors.New("Please enter a valid http(s) feed URL")
return errors.New(feedViewerInvalidURLError)
}

ips, err := net.LookupIP(parsedURL.Hostname())
if err != nil {
return fmt.Errorf("Unable to resolve this URL: %w", err)
return nil
}

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.

security-critical critical

The private IP and loopback address checks have been removed from validateFeedViewerURL. This exposes the application to Server-Side Request Forgery (SSRF) vulnerabilities, allowing users to probe or access internal services (e.g., local databases, Redis, or internal APIs) via the preview endpoint. Consider restoring the private IP check by default, or introducing an environment variable (e.g., FC_ALLOW_PRIVATE_IPS) to safely toggle this behavior for local development.

return
}

c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(item.Content))

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.

security-critical critical

Serving raw, user-supplied HTML content directly on the same domain without any restriction can lead to Stored Cross-Site Scripting (XSS) attacks. If an attacker pushes malicious JavaScript into an inbox, it will execute in the context of the FeedCraft domain when viewed, potentially hijacking admin sessions. To mitigate this, consider serving the content with a restrictive Content-Security-Policy: sandbox header to disable script execution and isolate the origin.

\tc.Header(\"Content-Security-Policy\", \"sandbox\")\n\tc.Data(http.StatusOK, \"text/html; charset=utf-8\", []byte(item.Content))

Comment thread internal/util/cache.go
Comment on lines +37 to +55
func tryGetRedisClient() (*redis.Client, error) {
envClient := GetEnvClient()
if envClient == nil {
return nil, errors.New("env client not available")
}
redisURI := strings.TrimSpace(envClient.GetString("REDIS_URI"))
if redisURI == "" {
return nil, errors.New("REDIS_URI is not configured")
}
opts, err := redis.ParseURL(redisURI)
if err != nil {
return nil, err
}
rdb := redis.NewClient(opts)
if rdb == nil {
return nil, errors.New("failed to create redis client")
}
return rdb, nil
}

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.

critical

tryGetRedisClient instantiates a new redis.Client on every single invocation. Since this function is called on every cache read and write, this will cause massive socket churn, connection pool exhaustion, and CPU overhead because connection pools are not reused. Consider caching the client instance globally using sync.Once.

var (\n\ttryRedisClient *redis.Client\n\ttryRedisOnce   sync.Once\n\ttryRedisErr    error\n)\n\nfunc tryGetRedisClient() (*redis.Client, error) {\n\ttryRedisOnce.Do(func() {\n\t\tenvClient := GetEnvClient()\n\t\tif envClient == nil {\n\t\t\ttryRedisErr = errors.New(\"env client not available\")\n\t\t\treturn\n\t\t}\n\t\tredisURI := strings.TrimSpace(envClient.GetString(\"REDIS_URI\"))\n\t\tif redisURI == \"\" {\n\t\t\ttryRedisErr = errors.New(\"REDIS_URI is not configured\")\n\t\t\treturn\n\t\t}\n\t\topts, err := redis.ParseURL(redisURI)\n\t\tif err != nil {\n\t\t\ttryRedisErr = err\n\t\t\treturn\n\t\t}\n\t\ttryRedisClient = redis.NewClient(opts)\n\t\tif tryRedisClient == nil {\n\t\t\ttryRedisErr = errors.New(\"failed to create redis client\")\n\t\t}\n\t})\n\treturn tryRedisClient, tryRedisErr\n}

Comment on lines +67 to +73
data := dao.Inbox{
ID: id,
Title: body.Title,
Description: body.Description,
MaxItems: body.MaxItems,
IsPublic: body.IsPublic,
}

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.

high

Constructing a new dao.Inbox struct with only the request body fields and calling db.Save will overwrite the existing record's created_at field with a zero value (0001-01-01 00:00:00). To prevent this, retrieve the existing record first, update its fields, and then save it, or use GORM's Updates method on a model with the primary key.

\texisting, err := dao.GetInboxByID(db, id)\n\tif err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\tc.JSON(http.StatusNotFound, util.APIResponse[any]{Msg: \"Inbox not found\"})\n\t\t\treturn\n\t\t}\n\t\tc.JSON(http.StatusInternalServerError, util.APIResponse[any]{Msg: err.Error()})\n\t\treturn\n\t}\n\n\texisting.Title = body.Title\n\texisting.Description = body.Description\n\texisting.MaxItems = body.MaxItems\n\texisting.IsPublic = body.IsPublic\n\n\tif err := dao.UpdateInbox(db, existing); err != nil {\n\t\tc.JSON(http.StatusInternalServerError, util.APIResponse[any]{Msg: err.Error()})\n\t\treturn\n\t}

Comment thread internal/dao/inbox.go
Comment on lines +117 to +119
if maxItems <= 0 {
return db.Where("inbox_id = ?", inboxID).Delete(&InboxItem{}).Error
}

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.

high

If maxItems is set to 0 or a negative value, DeleteOverflowInboxItems will delete all items in the inbox. This is highly counter-intuitive for users who might set max_items to 0 expecting 'unlimited' storage, leading to accidental data loss. Consider treating <= 0 as unlimited and skipping the deletion.

\tif maxItems <= 0 {\n\t\treturn nil\n\t}

Comment on lines +292 to +294
if err != nil {
return nil, fmt.Errorf("embedding call failed after retries (batch [%d-%d]): %w", batchStart, batchEnd-1, err)
}

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 the embedding service returns a different number of vectors than the requested batch size, iterating over result32 can either cause an out-of-bounds panic or leave nil vectors in allResults. Adding a length check prevents these potential runtime panics.

\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"embedding call failed after retries (batch [%d-%d]): %w\", batchStart, batchEnd-1, err)\n\t\t}\n\n\t\tif len(result32) != len(batch) {\n\t\t\treturn nil, fmt.Errorf(\"embedding service returned %d vectors, expected %d\", len(result32), len(batch))\n\t\t}

Comment thread internal/util/vector.go
Comment on lines +20 to +25
// 零向量检查
if normA == 0 || normB == 0 {
return 0.0
}

return dotProduct / (math.Sqrt(normA) * math.Sqrt(normB))

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 normA or normB is extremely small (subnormal float), math.Sqrt(normA) * math.Sqrt(normB) can underflow to 0.0, leading to a division by zero and returning NaN or Inf. Checking if the denominator is 0 directly is safer.

\tdenom := math.Sqrt(normA) * math.Sqrt(normB)\n\tif denom == 0 {\n\t\treturn 0.0\n\t}\n\n\treturn dotProduct / denom

return
}

siteBaseURL := strings.TrimRight(util.GetEnvClient().GetString("SITE_BASE_URL"), "/")

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 SITE_BASE_URL is not configured, siteBaseURL will be empty, causing the RSS feed to contain relative URLs. Relative URLs are a common cause of feed validation failures and reader compatibility issues. Consider dynamically resolving the base URL from the request headers as a fallback.

\tsiteBaseURL := strings.TrimRight(util.GetEnvClient().GetString(\"SITE_BASE_URL\"), \"/\")\n\tif siteBaseURL == \"\" {\n\t\tscheme := \"http\"\n\t\tif c.Request.TLS != nil {\n\t\t\tscheme = \"https\"\n\t\t}\n\t\tif proto := c.GetHeader(\"X-Forwarded-Proto\"); proto != \"\" {\n\t\t\tscheme = proto\n\t\t}\n\t\thost := c.Request.Host\n\t\tif fwdHost := c.GetHeader(\"X-Forwarded-Host\"); fwdHost != \"\" {\n\t\t\thost = fwdHost\n\t\t}\n\t\tsiteBaseURL = scheme + \"://\" + host\n\t}

Comment on lines +196 to 198
if len(modelClients) == 0 {
return "", fmt.Errorf("all models failed, last error: %v", lastErr)
}

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 modelClients is empty (e.g., due to empty or invalid model configuration), lastErr will be nil, resulting in a confusing error message: 'all models failed, last error: <nil>'. Consider returning a more descriptive error.

\tif len(modelClients) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no models configured or initialized\")\n\t}

cursoragent and others added 2 commits July 28, 2026 11:05
Co-authored-by: Colin <Colin_XKL@outlook.com>
Co-authored-by: Colin <Colin_XKL@outlook.com>
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