feat: support HTML to RSS navigation actions#833
Conversation
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>
#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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| BestPractice | 5 minor |
| Security | 1 critical 1 high |
| Complexity | 40 medium |
🟢 Metrics 1575 complexity · 136 duplication
Metric Results Complexity 1575 Duplication 136
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 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.
| 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 | ||
| } |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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))| 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 | ||
| } |
There was a problem hiding this comment.
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}| data := dao.Inbox{ | ||
| ID: id, | ||
| Title: body.Title, | ||
| Description: body.Description, | ||
| MaxItems: body.MaxItems, | ||
| IsPublic: body.IsPublic, | ||
| } |
There was a problem hiding this comment.
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}| if maxItems <= 0 { | ||
| return db.Where("inbox_id = ?", inboxID).Delete(&InboxItem{}).Error | ||
| } |
There was a problem hiding this comment.
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}| if err != nil { | ||
| return nil, fmt.Errorf("embedding call failed after retries (batch [%d-%d]): %w", batchStart, batchEnd-1, err) | ||
| } |
There was a problem hiding this comment.
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}| // 零向量检查 | ||
| if normA == 0 || normB == 0 { | ||
| return 0.0 | ||
| } | ||
|
|
||
| return dotProduct / (math.Sqrt(normA) * math.Sqrt(normB)) |
There was a problem hiding this comment.
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"), "/") |
There was a problem hiding this comment.
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}| if len(modelClients) == 0 { | ||
| return "", fmt.Errorf("all models failed, last error: %v", lastErr) | ||
| } |
There was a problem hiding this comment.
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>
Summary
http_fetcher.navigation_actionsfor ordered browser actions before HTML capture.click,wait_for_selector, andwaitactions in Browserless REST/functionand CDP rendering paths.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/fetcherpnpm run format && pnpm run type:checkinweb/admintask fixgo test ./...task backend-buildtask frontend-buildTo show artifacts inline, enable in settings.