Skip to content

feat: support HTML to RSS navigation actions#833

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

feat: support HTML to RSS navigation actions#833
Colin-XKL wants to merge 68 commits into
mainfrom
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 

google-labs-jules Bot and others added 30 commits May 10, 2026 15:44
Updates the markdown documentation to reflect code changes from the last 7 days:
- Updated tools docs to include the new Missing Crafts Panel for System Health (24bf35a)
- Updated system-craft-atoms docs to reflect `guid-fix` using FNV-1a hash instead of MD5 (c7e60a4)

Co-authored-by: Colin-XKL <49122401+Colin-XKL@users.noreply.github.com>
* fix: correctly handle logout action and provide feedback

- Update `web/admin/src/api/user.ts` to use a mock promise for logout API, as the backend uses stateless JWT and does not provide a logout endpoint (preventing a 404 error).
- Add direct logout button to the top navigation bar right menu in `web/admin/src/components/navbar/index.vue` to make it discoverable.
- Enhance the logout feedback by modifying the useUser hook to accept a success message and mapping proper i18n locales for the toast notification.

Co-authored-by: Colin-XKL <49122401+Colin-XKL@users.noreply.github.com>

* fix: correctly handle logout action and provide feedback

- Update `web/admin/src/api/user.ts` to use a mock promise for logout API, as the backend uses stateless JWT and does not provide a logout endpoint (preventing a 404 error).
- Add direct logout button to the top navigation bar right menu in `web/admin/src/components/navbar/index.vue` to make it discoverable.
- Enhance the logout feedback by modifying the useUser hook to accept a success message and mapping proper i18n locales for the toast notification.

Co-authored-by: Colin-XKL <49122401+Colin-XKL@users.noreply.github.com>

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
- Allow users to preview recipes directly from the table
- Enable quick copy of recipe links for sharing
- Clean up UI by removing duplicate buttons and updating column header
- Introduced WebMonitorParserConfig to capture extractors, key fields, and templates
- Added SourceWebMonitor type and factory to register pipeline with HttpFetcher and WebMonitorParser
- Implemented WebMonitorParser with parsing, preview, and GUID generation logic
- Created preview endpoint `/tools/web-monitor/preview` and updated router registry
- Updated SourceConfig to include WebMonitorParser field and adjusted HTML/JSON parser handling
- Added constants for new source type and updated documentation comments
- Added unit tests for parser behavior, key field handling, and factory defaults
- Updated source factory tests to verify default fetcher purpose for web monitor sources.
Replaced the invalid JSON placeholder `"{ 'foo': 'bar' }"` with the valid `{"foo": "bar"}` wrapped in single quotes in both the English (`en-US`) and Chinese (`zh-CN`) locale files (`jsonToRss.ts`).

Co-authored-by: Colin-XKL <49122401+Colin-XKL@users.noreply.github.com>
…faultClient

Replaces the usage of `http.DefaultClient` with a dedicated `http.Client`
in `HttpFetcher` to prevent unbounded requests. Adds `HttpClientTimeout` to
`HttpFetcherConfig` to allow configuring the timeout behavior, falling back
to a sensible default of 30 seconds.

Co-authored-by: Colin-XKL <49122401+Colin-XKL@users.noreply.github.com>
Co-authored-by: Colin <Colin_XKL@outlook.com>
* feat: add legacy ai filter atom craft

Co-authored-by: Colin <Colin_XKL@outlook.com>

* test: make ai filter summary assertions deterministic

Co-authored-by: Colin <Colin_XKL@outlook.com>

* test: initialize redis for advertorial processor error case

Co-authored-by: Colin <Colin_XKL@outlook.com>

* chore: satisfy ai filter lint suggestions

Co-authored-by: Colin <Colin_XKL@outlook.com>

* chore: finish ai filter lint cleanup

Co-authored-by: Colin <Colin_XKL@outlook.com>

* test: assert ai filter uses legacy adapter

Co-authored-by: Colin <Colin_XKL@outlook.com>

* test: match ai filter legacy processor wrapping

Co-authored-by: Colin <Colin_XKL@outlook.com>

* refactor: rename ai filter rule parameter

Co-authored-by: Colin <Colin_XKL@outlook.com>

* feat: cache ai filter decisions

Co-authored-by: Colin <Colin_XKL@outlook.com>

* feat: add ai filter extra payload multiselect

Co-authored-by: Colin <Colin_XKL@outlook.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
* feat: add legacy ai filter atom craft

Co-authored-by: Colin <Colin_XKL@outlook.com>

* test: make ai filter summary assertions deterministic

Co-authored-by: Colin <Colin_XKL@outlook.com>

* test: initialize redis for advertorial processor error case

Co-authored-by: Colin <Colin_XKL@outlook.com>

* chore: satisfy ai filter lint suggestions

Co-authored-by: Colin <Colin_XKL@outlook.com>

* chore: finish ai filter lint cleanup

Co-authored-by: Colin <Colin_XKL@outlook.com>

* test: assert ai filter uses legacy adapter

Co-authored-by: Colin <Colin_XKL@outlook.com>

* test: match ai filter legacy processor wrapping

Co-authored-by: Colin <Colin_XKL@outlook.com>

* refactor: rename ai filter rule parameter

Co-authored-by: Colin <Colin_XKL@outlook.com>

* feat: cache ai filter decisions

Co-authored-by: Colin <Colin_XKL@outlook.com>

* feat: add ai filter extra payload multiselect

Co-authored-by: Colin <Colin_XKL@outlook.com>

* docs: add development environment guide

Co-authored-by: Colin <Colin_XKL@outlook.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Colin <Colin_XKL@outlook.com>
* feat: add example rss feeds

Co-authored-by: Colin <Colin_XKL@outlook.com>

* style: auto-format code

* fix: open example rss feeds directly

Co-authored-by: Colin <Colin_XKL@outlook.com>

* fix: use absolute URLs in example feeds

Co-authored-by: Colin <Colin_XKL@outlook.com>

* feat: add feed format examples

Co-authored-by: Colin <Colin_XKL@outlook.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* feat: add rss format probes

Co-authored-by: Colin <Colin_XKL@outlook.com>

* fix: remove rss 0.92 example feed

Co-authored-by: Colin <Colin_XKL@outlook.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Added a sponsorship section to acknowledge project sponsors.
* feat: 新增基于 Embedding 的主题过滤器及多后端支持

* chore: 在.gitignore中添加db/目录忽略规则

* chore: 新增忽略 start.sh 与 start-full.sh 文件

* refactor: 优化countMissing可读性与新增Vite代理路径

* chore: 删除 task-item.md 实施计划文件

* chore: 更新.gitignore忽略.codebuddy目录并清理旧项

* refactor: 重构embedding模块错误处理与UTF-8安全截断

* feat: 增强embedding模块功能与测试覆盖

* feat: 拆分embedding批处理并加重试日志与结果合并

* feat: 支持自定义embedding批处理大小配置

* feat: 为 embedding-filter 新增 include 和 exclude 过滤模式

---------

Co-authored-by: gifwang <gifwang@tencent.com>
Co-authored-by: Colin <Colin_XKL@outlook.com>
Co-authored-by: Colin <Colin_XKL@outlook.com>
Co-authored-by: Colin <Colin_XKL@outlook.com>
Co-authored-by: Colin <Colin_XKL@outlook.com>
Co-authored-by: Colin <Colin_XKL@outlook.com>
* Improve admin URL input layouts

Co-authored-by: Colin <Colin_XKL@outlook.com>

* Fix admin component tag order

Co-authored-by: Colin <Colin_XKL@outlook.com>

* Use Tailwind utilities for admin tool layouts

Co-authored-by: Colin <Colin_XKL@outlook.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
* fix quick start copy url fallback

Co-authored-by: Colin <Colin_XKL@outlook.com>

* improve quick start generated url state

Co-authored-by: Colin <Colin_XKL@outlook.com>

* remove temporary quick start regression script

Co-authored-by: Colin <Colin_XKL@outlook.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
* Improve LLM retry model failover

Co-authored-by: Colin <Colin_XKL@outlook.com>

* Refine LLM retry rotation behavior

Co-authored-by: Colin <Colin_XKL@outlook.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
* feat: add AI content process craft

Co-authored-by: Colin <Colin_XKL@outlook.com>

* fix: harden AI content process caching

Co-authored-by: Colin <Colin_XKL@outlook.com>

* fix: avoid empty content separators

Co-authored-by: Colin <Colin_XKL@outlook.com>

* fix: lower temperature for deterministic AI crafts

Co-authored-by: Colin <Colin_XKL@outlook.com>

* feat: hide template-only system crafts

Co-authored-by: Colin <Colin_XKL@outlook.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Colin-XKL and others added 21 commits May 23, 2026 23:45
#779)

* fix: resolve inbox bugs found during testing

- Fix is_public=false not persisted (GORM zero-value / default:true issue)
  Change IsPublic field type from bool to *bool in dao.Inbox so GORM
  does not skip the false value and apply the database default instead.
  Update inbox_public.go to dereference the pointer safely.

- Fix UpdateInbox returning 400 due to binding:required on ID field
  The ID comes from the URL path param, not the request body.
  Use an anonymous struct without binding constraints for body parsing,
  then construct dao.Inbox with the path ID.

- Fix incorrect source_config example in frontend Inbox guide modal
  The guide showed {"inbox_id":"..."} but the correct nested format
  required by SourceConfig is {"inbox_source":{"inbox_id":"..."}}.

- Fix TestIgnoreAdvertorialProcessor_KeepsArticleOnLLMError test
  The test was missing setupTestRedis(t) call, causing log.Fatalf when
  the cache layer attempted to connect to an unconfigured Redis instance.

- Fix pre-existing golangci-lint staticcheck ST1005 warnings in
  feed_viewer.go (error strings should not be capitalized)

Co-authored-by: Colin <Colin_XKL@outlook.com>

* feat: complete inbox i18n and add documentation

i18n:
- Replace hardcoded Chinese text in inbox guide modal with i18n keys
  for all three languages (en-US, zh-CN, zh-TW)
- Replace hardcoded 'Copy' and 'Close' buttons in system_auth_token
  page with i18n keys
- Create web/admin/src/locale/zh-TW/inbox.ts with Traditional Chinese
  translations for all inbox and system auth token strings
- Update zh-TW.ts to import zh-TW/inbox.ts instead of zh-CN fallback
- Add missing menu.webMonitor key to zh-CN/menu.ts

Documentation (doc-site):
- Add inbox.md in en, zh, and zh-tw covering: overview, inbox management,
  system auth token management, push API reference, RSS subscription via
  Custom Recipe, private inbox access control, and GC maintenance

Co-authored-by: Colin <Colin_XKL@outlook.com>

* fix: align inbox docs and UI guide with backend field names and semantics

- Fix inbox guide modal JSON example: item_id -> id, published_at (ISO string) -> timestamp (Unix float64 seconds)
- Clarify overflow pruning order: items are removed by oldest created_at, not published_at
- Note that max_items=0 immediately deletes all items (not a no-limit flag)
- Clarify summary auto-truncation uses 200 Unicode runes (not bytes)

Co-authored-by: Colin <Colin_XKL@outlook.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
- Uncomment topic_feed routes in worktable.ts (TopicFeed and TopicFeedDetail)
- Remove v-if="false" guard on topic detail button in observability page
- Remove stale development-status comments

Co-authored-by: Colin <Colin_XKL@outlook.com>
* feat: unify feed preview targets

Co-authored-by: Colin <Colin_XKL@outlook.com>

* fix: sync feed preview route target

Co-authored-by: Colin <Colin_XKL@outlook.com>

* fix: harden unified preview state

Co-authored-by: Colin <Colin_XKL@outlook.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
No Custom Recipe required to subscribe to an inbox.

Backend:
- Add PublicInboxRSSFeed controller in inbox_public.go: loads inbox
  from DB, enforces public/private access (same token logic as article
  content), builds a CraftFeed from stored items, and renders RSS XML
- Extract checkInboxAccess() helper to deduplicate auth logic shared by
  both PublicInboxItemContent and the new handler
- Register GET /inbox/:inbox_id/rss in router

Frontend:
- Add directRssUrl computed property to inbox guide modal
- Show direct RSS URL with copy button; private inbox hint with ?token=

i18n (en-US / zh-CN / zh-TW):
- inbox.guide.directRss.desc
- inbox.guide.directRss.privateHint

Docs (en / zh / zh-tw):
- Restructure 'Subscribing via RSS' into two sections:
  'Direct RSS URL (simplest)' and 'Via Custom Recipe (advanced)'

Co-authored-by: Colin <Colin_XKL@outlook.com>
* feat: unhide topic feed page and re-enable detail navigation

- Uncomment topic_feed routes in worktable.ts (TopicFeed and TopicFeedDetail)
- Remove v-if="false" guard on topic detail button in observability page
- Remove stale development-status comments

Co-authored-by: Colin <Colin_XKL@outlook.com>

* feat: redesign topic feed input source editor with type picker

- Replace plain text inputs with type-aware source rows
- Each row has a segmented selector: External RSS / Recipe / Topic
- External RSS: free-form URL input (http/https)
- Recipe: searchable dropdown loading from /api/admin/recipes
- Topic: searchable dropdown loading from /api/admin/topics (excludes self when editing)
- Auto-parse existing feedcraft:// URIs into the correct type+id on edit
- Load picker data when modal opens (non-fatal fallback on error)
- Add section dividers for 'Input Sources' and 'Aggregation Rules'
- Update locale strings (zh-CN and en-US) for new UI elements

Co-authored-by: Colin <Colin_XKL@outlook.com>

* fix: resolve input field visibility and layout in topic feed editor

- Use CSS grid (auto 1fr auto) for input-source-row layout
- Add width: 100% to ensure grid container fills parent flex item
- Remove wrapper div approach; input/select are now direct grid children
- Remove redundant size='small' from radio group (prevent text wrapping)
- Add :deep(.arco-radio-group) { white-space: nowrap } for button labels

Co-authored-by: Colin <Colin_XKL@outlook.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Colin-XKL <49122401+Colin-XKL@users.noreply.github.com>
- Add dual-mode support to feed_viewer.vue: 'preview' (direct) and 'compare' (craft diff)
- In compare mode, a CraftFlowSelect appears alongside the input for selecting a Craft
- Compare mode shows side-by-side original vs. craft-processed feed panels
- Remove standalone feed_compare.vue and its route from tools.ts
- Update zh-CN and en-US locale files with new i18n keys; remove feedCompare menu entries
- Backend and frontend builds pass; all Go tests pass

Co-authored-by: Colin <Colin_XKL@outlook.com>
* fix(web-monitor): inject page URL into Parse(), add zh-CN locale, fix source_type dropdown

- Backend: add PageURL field to WebMonitorParser; Parse() now sets values["url"] = p.PageURL
  so {{.url}} templates render correctly and GUID is URL-scoped in production
- Backend: webMonitorSourceFactory passes HttpFetcher.URL to WebMonitorParser.PageURL
- Tests: update existing test to assert URL is injected; add TestWebMonitorParser_Parse_URLInjectedIntoTemplateAndGUID
- Frontend: create web/admin/src/locale/zh-CN/webMonitor.ts with full Chinese translations
- Frontend: register localeWebMonitor in zh-CN.ts
- Frontend: add web_monitor option to source_type select in custom_recipe.vue
- Frontend: fix default content template in web_monitor.vue (remove non-existent {{.title}} var)

Co-authored-by: Colin <Colin_XKL@outlook.com>

* style: auto-format code

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* feat(topic): add by_title, by_simhash, by_embedding deduplication strategies

- by_title: exact title match deduplication
- by_simhash: character 2-gram SimHash with configurable Hamming distance threshold (default 3)
  - pure Go implementation with no external dependencies
  - strips HTML and normalizes text before hashing
- by_embedding: LLM Embedding cosine similarity with configurable threshold (default 0.9)
  - reuses existing adapter.EmbedTexts and util.CosineSimilarity
  - fails open (keeps all articles) when embedding service is unavailable

Frontend:
- added new strategy options to the deduplicate step selector
- added threshold number inputs for by_simhash (integer) and by_embedding (float)
- threshold resets to defaults when switching strategy
- updated formatAggregatorStep in detail.vue to show threshold values
- updated zh-CN and en-US locale files

Tests:
- TestDeduplicate_ByTitle
- TestDeduplicate_BySimhash_IdenticalContent
- TestDeduplicate_BySimhash_InvalidThreshold
- TestDeduplicate_BySimhash_DefaultThreshold
- TestDeduplicate_ByEmbedding_InvalidStrategy
- TestDeduplicate_ByEmbedding_ThresholdOutOfRange

Co-authored-by: Colin <Colin_XKL@outlook.com>

* feat(topic): normalize simhash threshold to 0-100 range

- User-facing threshold is now 0-100 (0=identical, 100=completely different)
- Backend converts to internal Hamming distance: hamming = round(threshold * 64 / 100)
- Default value changed from 3 (raw Hamming) to 5 (normalized, equivalent to hamming≈3)
- Validation updated: backend rejects values outside [0, 100]
- Frontend: input max changed from 64 to 100, default updated to 5
- Locale descriptions updated in zh-CN and en-US
- Tests updated with new range validation (0, 100 are valid; 101, -1, 999 are invalid)

Co-authored-by: Colin <Colin_XKL@outlook.com>

* feat(topic): change simhash threshold to float [0.0, 1.0] range

Align simhash threshold scale with by_embedding for consistent UX.
- Range: 0.0 (identical only) to 1.0 (everything is duplicate)
- Default: 0.05 (equivalent to hamming≈3, same behaviour as before)
- Internal conversion: hamming = round(threshold * 64)
- Frontend: step=0.01, precision=2, same as embedding input
- Tests updated for new float range

Co-authored-by: Colin <Colin_XKL@outlook.com>

* docs: update Topic Feed deduplication strategies in concepts (en/zh/zh-tw)

- Document 5 deduplication strategies: by_link, by_id, by_title, by_simhash, by_embedding
- Add threshold range and default value for by_simhash (0.0-1.0, default 0.05)
- Add threshold range and default value for by_embedding (0.0-1.0, default 0.9)
- Note fail-open behaviour for by_embedding when Embedding API is unavailable
- Add tip recommending Sort → Deduplicate → Limit pipeline order
- Remove stale caution about Topic Feed being hidden (feature is now available)

Co-authored-by: Colin <Colin_XKL@outlook.com>

* fix(topic): guard against out-of-bounds index in embedding dedup

EmbedTexts is expected to return a slice of the same length as the
input, but an unexpected short or nil result would cause a panic at
vectors[i]. Add two layers of protection:
1. Length check immediately after EmbedTexts: if len(vectors) !=
   len(cloned.Articles), fail open and return all articles unchanged.
2. Defense-in-depth guard inside the loop: if i >= len(vectors) or
   vectors[i] is empty, keep the article unconditionally.

Co-authored-by: Colin <Colin_XKL@outlook.com>

* feat(topic): improve SimHash/Embedding strategy descriptions in UI and docs

UI (locale zh-CN / en-US):
- Rename strategy options: 'Lexical Similarity (SimHash · Lightweight)' and
  'Semantic Similarity (Embedding · Precise)'
- Add per-strategy hint text shown below the row when selected:
  SimHash: character-based, no AI needed, best for reposts/rewrites
  Embedding: semantic understanding, catches paraphrased duplicates,
  requires Embedding API with fail-open fallback
- Improve threshold placeholders to clarify direction (lower=stricter vs
  higher=stricter)
- Remove unused parseIntOption helper (SimHash now uses parseFloatOption)

Frontend template (topic_feed.vue):
- Wrap each aggregator step in .step-wrapper so hint text can appear
  below the controls row without breaking the flex layout
- Add .step-hint paragraph rendered when by_simhash or by_embedding is
  selected

Docs (en / zh / zh-tw):
- Expand by_simhash and by_embedding descriptions with lightweight vs.
  precise contrast, use-case guidance, and threshold semantics

Co-authored-by: Colin <Colin_XKL@outlook.com>

* fix(topic): unify threshold semantics — both SimHash and Embedding use lower=stricter

Previously:
  by_simhash  threshold: difference tolerance, lower = stricter  ✓
  by_embedding threshold: cosine similarity floor, higher = stricter  ✗ (opposite direction)

Now both use 'difference tolerance' in [0.0, 1.0], lower = stricter:
  by_simhash:  hamming <= round(threshold * 64)           (unchanged)
  by_embedding: cosine >= (1 - threshold)                 (flipped)

Default values:
  by_simhash:  0.05  (unchanged, hamming ≈ 3)
  by_embedding: 0.1  (was 0.9 cosine floor → same internal behaviour)

Updates:
- builder.go: flip cosine comparison to (1 - threshold), update default and comment
- builder_test.go: extend embedding out-of-range test to cover multiple bad values
- topic_feed.vue: defaultThreshold('by_embedding') 0.9 → 0.1
- locale zh-CN / en-US: both placeholder texts now say 'lower = stricter';
  hint texts updated accordingly
- docs en / zh / zh-tw: both thresholds described as 'difference tolerance',
  embedding default updated to 0.1

Co-authored-by: Colin <Colin_XKL@outlook.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
…ators (#795)

* style: optimize layout of topic creation/edition form inputs & aggregators

Co-authored-by: Colin <Colin_XKL@outlook.com>

* Delete docs/superpowers/specs/2026-05-29-optimize-topic-form-layout-design.md

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
…t generator

Co-authored-by: Colin <Colin_XKL@outlook.com>
- Change form items to vertical layout by separating search query and enhanced mode checkbox into distinct a-form-item components.
- Improve spacing and alignment for checkbox description.
- Increase prominence of the "Preview Results" button.
- Replace fixed height with min-height for better responsiveness.

Co-authored-by: Colin-XKL <49122401+Colin-XKL@users.noreply.github.com>
Converted all relative imports within the web/admin/src directory to use the @/ alias. This ensures consistency across the project and simplifies future refactoring. Verified that all tests pass after the changes.

Co-authored-by: Colin-XKL <49122401+Colin-XKL@users.noreply.github.com>
- Removed hardcoded default embedding model 'text-embedding-3-small'.
- Updated `loadEmbeddingConfig` to return an error if `FC_EMBEDDING_API_MODEL` is not set.
- Updated `.env.example` and documentation (en, zh, zh-tw) to reflect that the model is now required.
- Updated `internal/controller/dependency_monitor.go` to remove redundant default model logic.
- Updated unit tests to provide explicit model names or expect errors when missing.

Co-authored-by: Colin-XKL <49122401+Colin-XKL@users.noreply.github.com>
* feat: prevent editing AtomCraft name after creation

Co-authored-by: Colin-XKL <49122401+Colin-XKL@users.noreply.github.com>

* feat: enforce AtomCraft name immutability and refactor frontend edit logic

- Added backend defense to reject AtomCraft name changes in UpdateCraftAtom.
- Disabled name input field in frontend edit modal.
- Refactored frontend logic to use 'originalName' ref for identifying resources during updates.
- Added 'handleAdd' to properly reset form state when creating new AtomCrafts.
- Restored accidentally deleted web/admin/package-lock.json.

Co-authored-by: Colin-XKL <49122401+Colin-XKL@users.noreply.github.com>

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
…king (#805)

* feat: add optimistic caching for topic sub-feeds with per-URI health tracking

When a sub-feed in a TopicFeed fails to fetch, the system now automatically
falls back to the last cached result (stored up to 14 days in Redis) so the
topic feed remains available despite partial upstream failures.

Changes:
- internal/constant/ttl.go: add SubFeedCacheTTL (14d) and SubFeedHealthTTL (30d)
- internal/constant/cache_namespace.go: add PrefixSubFeedResult and PrefixSubFeedHealth
- internal/feedruntime/cached_provider.go (new): CachedFeedProvider wraps any
  FeedProvider; on failure returns cached snapshot; health metadata (last success,
  last failure, last error, cached_at) persisted in Redis
- internal/feedruntime/builder.go: wrap each topic input with CachedFeedProvider
- internal/util/cache.go: add TryCacheSetString / TryCacheGetString that return
  errors instead of calling log.Fatalf when Redis is unavailable (test-safe)
- internal/controller/topic_feed.go: add sub_feed_health to TopicDetailResponse
- web/admin/src/api/topic.ts: add SubFeedHealth type; update TopicDetail
- web/admin/src/views/dashboard/topic_feed/detail.vue: add sub-feed health card
  showing per-URI last-success, last-failure, error message, and cache status
- web/admin/src/locale/zh-CN/topic.ts, en-US/topic.ts: add i18n keys
- internal/feedruntime/builder_test.go: update nested-topic test for CachedFeedProvider

Co-authored-by: Colin <Colin_XKL@outlook.com>

* refactor: replace inline sha256 with util.GetMD5Hash in cached_provider

Reuse the existing util.GetMD5Hash helper instead of importing crypto/sha256
directly. MD5 is sufficient for cache key derivation.

Co-authored-by: Colin <Colin_XKL@outlook.com>

* style: rename '子源' to '子 RSS 源' in zh-CN locale

Co-authored-by: Colin <Colin_XKL@outlook.com>

* refactor: unexport GetMD5Hash, unify external callers to GetTextContentHash

- GetMD5Hash renamed to getMD5Hash (package-private)
- GetPasswordMD5Hash is now the only exported MD5 entry point (security-focused)
- adapter/embedding.go and feedruntime/cached_provider.go switched to
  GetTextContentHash (FNV-64a, efficiency-focused) for cache key derivation
- hash_test.go updated to test via GetPasswordMD5Hash

Co-authored-by: Colin <Colin_XKL@outlook.com>

* docs(AGENTS.md): add hash utility guidelines

Co-authored-by: Colin <Colin_XKL@outlook.com>

* docs(AGENTS.md): remove mention of unexported getMD5Hash

Co-authored-by: Colin <Colin_XKL@outlook.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
…se, and detail modal (#804)

* feat(feed-viewer): enhance preview with HTML mode, card expand/collapse, and detail modal

- Add raw HTML view mode with syntax highlighting (highlight.js)
- Add HTML formatting utility (htmlFormat.ts)
- Refactor FeedViewContainer into FeedItemCard + FeedItemContent + FeedItemDetailModal
- FeedItemCard: collapsible card with title/date header and snippet preview
- FeedItemContent: three view modes (normal/rich/html) with proper styling
- FeedItemDetailModal: full-screen detail modal with switchable view modes
- Fix rich text mode CSS (proper scoped deep styles for images, links, code blocks etc.)
- Update i18n (zh-CN + en-US) with new feedViewer keys
- All existing feed types (url/recipe/topic/inbox/uri) continue to work in compare mode

Co-authored-by: Colin <Colin_XKL@outlook.com>

* chore(deps): remove deprecated @types/highlight.js stub (highlight.js v11+ ships own types)

Co-authored-by: Colin <Colin_XKL@outlook.com>

* fix(htmlFormat): replace regex split with state-tracking scanner to handle '>' in quoted attributes

Co-authored-by: Colin <Colin_XKL@outlook.com>

* refactor(htmlFormat): replace custom parser with js-beautify

Co-authored-by: Colin <Colin_XKL@outlook.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
* fix(util): preserve paragraph line breaks in markdown/HTML conversion

Single newlines in markdown were rendered as literal newlines inside <p>
tags, which browsers collapse. HTML paragraphs with embedded newlines lost
them when converted to markdown.

Add post-processing to insert <br> for intentional line breaks inside <p>
elements, apply it in both Markdown2HTML and Html2Markdown, deduplicate
beautify conversion through util.Markdown2HTML, and add comprehensive tests.

Co-authored-by: Colin <Colin_XKL@outlook.com>

* refactor(util): treat single MD newlines as breaks and collapse blank runs

FeedCraft mainly consumes LLM markdown and cleanup round-trips, where
single newlines are intentional. Normalize input by collapsing consecutive
blank lines, then promote remaining single newlines to hard breaks while
skipping list boundaries, block starts, and fenced code. Also collapse
blank runs in Html2Markdown output.

Co-authored-by: Colin <Colin_XKL@outlook.com>

* refactor(util): unify markdown conversion as MarkdownToHTML and HTMLToMarkdown

Consolidate all MD↔HTML paths onto util.MarkdownToHTML and util.HTMLToMarkdown.
Remove duplicate html-to-markdown usage in ProcessContent; rename md2html.go
and update craft call sites (cleanup, beautify, LLM processors, etc.).

Co-authored-by: Colin <Colin_XKL@outlook.com>

* fix(util): avoid duplicate br when newline adjoins existing br tag

Check br tag prefix on text after newline, not only suffix before it.
Use compiled regexes for all <br> variants including attributes.

Co-authored-by: Colin <Colin_XKL@outlook.com>

* refactor(util): clean up hardcoded regex rules by using native gomarkdown extensions

Instead of prepending spaces to newlines via string manipulation (which required
custom regex to protect lists, quotes, tables, and code blocks), use the native
parser.HardLineBreak extension in gomarkdown. Remove redundant regexes like
blockStartRE and orderedListItemRE.

Co-authored-by: Colin <Colin_XKL@outlook.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
* feat(topic-feed): wizard create/edit, input labels, and preview

Replace the topic feed create/edit modal with a multi-step wizard page.
Add per-input description fields persisted via inputs[] on the API.
Extract FeedPreviewPanel for reuse when previewing sub-feeds in the editor.

Co-authored-by: Colin <Colin_XKL@outlook.com>

* feat(topic-feed): add disabled flag for sub-feed inputs

Persist optional disabled on each input; runtime uses only enabled URIs.
Detail page supports toggling disable without opening the full editor.

Co-authored-by: Colin <Colin_XKL@outlook.com>

* refactor(topic-feed): persist only structured inputs

Co-authored-by: Colin <Colin_XKL@outlook.com>

* refactor(topic-feed): clean redundant NormalizeInputs calls

Remove GORM BeforeSave hook.
Explicitly normalize inputs before Write/Update DB operations in Controller.
Strip redundant read-path normalizations.

Co-authored-by: Colin <Colin_XKL@outlook.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
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:22am
feed-craft-doc Ready Ready Preview, Comment Jun 29, 2026 7:22am

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

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.

3 participants