优化 Topic Feed 管理:分步向导、输入备注与预览#806
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Reviewer's GuideRefactors Topic Feed management to use a dedicated multi-step editor with reusable input/aggregator utilities, adds backend support for rich input metadata (description, disabled) while keeping input_uris derived and compatible, and exposes input descriptions and enable/disable controls in the detail view along with a reusable feed preview panel for sub-feeds. Sequence diagram for toggling TopicInput disabled statesequenceDiagram
actor Admin
participant DetailPage as TopicFeedDetailView
participant TopicAPI as updateTopicFeed
participant DAO as TopicFeed(NormalizeInputs)
participant DB
Admin->>DetailPage: toggleInputDisabled(uri, disabled)
DetailPage->>DetailPage: build payload (inputs, input_uris)
DetailPage->>TopicAPI: updateTopicFeed(id, payload)
TopicAPI->>DAO: bind JSON to TopicFeed
TopicAPI->>DAO: TopicFeed.NormalizeInputs()
DAO->>DB: save TopicFeed (Inputs, InputURIs)
DB-->>DAO: ok
DAO-->>TopicAPI: updated TopicFeed
TopicAPI-->>DetailPage: APIResponse(data)
DetailPage->>DetailPage: fetchDetail() and refresh inputs
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
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>
There was a problem hiding this comment.
Code Review
This pull request refactors the Topic Feed management in the admin panel, moving from a modal-based editor to a multi-step wizard editor, and introduces support for input descriptions alongside URIs. On the backend, input normalization has been added to keep the new Inputs field and legacy InputURIs in sync. The review feedback highlights several opportunities to improve robustness, specifically by adding defensive checks against potential TypeErrors when handling potentially null or undefined fields in the frontend, catching errors in asynchronous API calls, and removing redundant input normalization calls in the backend controllers.
| const inputs = | ||
| record.inputs && record.inputs.length > 0 | ||
| ? record.inputs | ||
| : record.input_uris.map((uri) => ({ uri, description: '' })); |
There was a problem hiding this comment.
如果 record.input_uris 为 null 或 undefined(这在历史遗留数据或没有输入源的记录中可能会发生),对其调用 .map() 将会抛出运行时的 TypeError 错误。我们应该通过提供默认的空数组来防御性地避免此问题。
| const inputs = | |
| record.inputs && record.inputs.length > 0 | |
| ? record.inputs | |
| : record.input_uris.map((uri) => ({ uri, description: '' })); | |
| const inputs = | |
| record.inputs && record.inputs.length > 0 | |
| ? record.inputs | |
| : (record.input_uris || []).map((uri) => ({ uri, description: '' })); |
| const topicInputs = computed(() => { | ||
| if (!detail.value) return []; | ||
| const { topic } = detail.value; | ||
| if (topic.inputs?.length) return topic.inputs; | ||
| return topic.input_uris.map((uri) => ({ uri, description: '' })); | ||
| }); |
There was a problem hiding this comment.
如果 topic.input_uris 为 null 或 undefined,对其调用 .map() 将会抛出运行时的 TypeError 错误。我们应该使用备用的空数组来防止潜在的页面崩溃。
const topicInputs = computed(() => {
if (!detail.value) return [];
const { topic } = detail.value;
if (topic.inputs?.length) return topic.inputs;
return (topic.input_uris || []).map((uri) => ({ uri, description: '' }));
});
| const inputCount = (record: TopicFeed) => { | ||
| if (record.inputs?.length) return record.inputs.length; | ||
| return record.input_uris.length; | ||
| }; |
There was a problem hiding this comment.
| previewDrawerTitle.value = | ||
| source.description.trim() || | ||
| t('topic.inputPreview.title', { index: idx + 1 }); |
| export const sourceToUri = (source: InputSourceItem): string => { | ||
| if (source.sourceType === 'recipe') { | ||
| return `feedcraft://recipe/${source.resourceId}`; | ||
| } | ||
| if (source.sourceType === 'topic') { | ||
| return `feedcraft://topic/${source.resourceId}`; | ||
| } | ||
| return source.externalUrl.trim(); | ||
| }; |
There was a problem hiding this comment.
如果 source.externalUrl 在运行时为 undefined 或 null,对其调用 .trim() 将会抛出 TypeError 错误。我们应该使用备用的空字符串来防止潜在的崩溃。
| export const sourceToUri = (source: InputSourceItem): string => { | |
| if (source.sourceType === 'recipe') { | |
| return `feedcraft://recipe/${source.resourceId}`; | |
| } | |
| if (source.sourceType === 'topic') { | |
| return `feedcraft://topic/${source.resourceId}`; | |
| } | |
| return source.externalUrl.trim(); | |
| }; | |
| export const sourceToUri = (source: InputSourceItem): string => { | |
| if (source.sourceType === 'recipe') { | |
| return `feedcraft://recipe/${source.resourceId}`; | |
| } | |
| if (source.sourceType === 'topic') { | |
| return `feedcraft://topic/${source.resourceId}`; | |
| } | |
| return (source.externalUrl || '').trim(); | |
| }; |
| id: formData.id.trim(), | ||
| title: formData.title.trim(), | ||
| description: formData.description.trim(), |
There was a problem hiding this comment.
如果 formData.id、formData.title 或 formData.description 在运行时为 undefined 或 null,对其调用 .trim() 将会抛出 TypeError 错误。我们应该使用备用的空字符串来确保代码的健壮性。
| id: formData.id.trim(), | |
| title: formData.title.trim(), | |
| description: formData.description.trim(), | |
| id: (formData.id || '').trim(), | |
| title: (formData.title || '').trim(), | |
| description: (formData.description || '').trim(), |
| const loadPickerData = async () => { | ||
| pickerLoading.value = true; | ||
| try { | ||
| const [recipesRes, topicsRes] = await Promise.all([ | ||
| getCustomRecipes(), | ||
| listTopicFeeds(), | ||
| ]); | ||
| availableRecipes.value = recipesRes.data ?? []; | ||
| availableTopics.value = topicsRes.data ?? []; | ||
| } finally { | ||
| pickerLoading.value = false; | ||
| } | ||
| }; |
There was a problem hiding this comment.
loadPickerData 函数没有捕获异步 API 调用中的错误。如果任何 API 调用失败,Promise 的 rejection 将会是未处理的(unhandled),且用户不会收到任何错误反馈。我们应该添加 catch 块来优雅地处理错误并向用户展示提示。
const loadPickerData = async () => {
pickerLoading.value = true;
try {
const [recipesRes, topicsRes] = await Promise.all([
getCustomRecipes(),
listTopicFeeds(),
]);
availableRecipes.value = recipesRes.data ?? [];
availableTopics.value = topicsRes.data ?? [];
} catch (err: any) {
Message.error(err.message || t('topic.fetchFailed'));
} finally {
pickerLoading.value = false;
}
};
| c.JSON(http.StatusInternalServerError, util.APIResponse[any]{Msg: err.Error()}) | ||
| return | ||
| } | ||
| topicData.NormalizeInputs() |
| c.JSON(http.StatusInternalServerError, util.APIResponse[any]{Msg: err.Error()}) | ||
| return | ||
| } | ||
| topicData.NormalizeInputs() |
| c.JSON(http.StatusBadRequest, util.APIResponse[any]{Msg: err.Error()}) | ||
| return | ||
| } | ||
| topicData.NormalizeInputs() |
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| BestPractice | 5 minor |
| Complexity | 4 medium |
🟢 Metrics 68 complexity · 0 duplication
Metric Results Complexity 68 Duplication 0
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.
Review Summary by QodoRefactor Topic Feed management with wizard editor, input descriptions, and preview panel
WalkthroughsDescription• Replace modal-based create/edit with dedicated multi-step wizard pages • Add per-input description and disabled flag with API synchronization • Extract reusable feed preview component for inline input previews • Support toggling input disabled state from detail view without full editor Diagramflowchart LR
A["Topic Feed List"] -->|Create/Edit| B["Wizard Editor Page"]
B -->|Step 1| C["Basic Info"]
B -->|Step 2| D["Input Sources"]
D -->|Preview| E["Feed Preview Panel"]
B -->|Step 3| F["Aggregation Rules"]
B -->|Step 4| G["Review & Save"]
G -->|Submit| H["API Normalize Inputs"]
H -->|Sync| I["Inputs + InputURIs"]
J["Detail View"] -->|Toggle Disabled| K["Update Input State"]
File Changes1. internal/dao/topic.go
|
Code Review by Qodo
1. Inputs not validated on save
|
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- In the controller layer you are calling
NormalizeInputsmultiple times for the same payload (e.g., before and after create/update, and both inValidateTopicFeedand insidevalidateTopicConfig), which is redundant; consider centralizing these calls at the API boundary or inside a model hook so each request normalizes only once. - In
GetTopicFeedDetail,subFeedHealthis now built fromtopicData.Inputsinstead ofInputURIs, which means disabled inputs will still be probed for health even though they are excluded from aggregation; consider filtering out disabled inputs here to align behavior with theDisabledflag semantics.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In the controller layer you are calling `NormalizeInputs` multiple times for the same payload (e.g., before and after create/update, and both in `ValidateTopicFeed` and inside `validateTopicConfig`), which is redundant; consider centralizing these calls at the API boundary or inside a model hook so each request normalizes only once.
- In `GetTopicFeedDetail`, `subFeedHealth` is now built from `topicData.Inputs` instead of `InputURIs`, which means disabled inputs will still be probed for health even though they are excluded from aggregation; consider filtering out disabled inputs here to align behavior with the `Disabled` flag semantics.
## Individual Comments
### Comment 1
<location path="internal/controller/topic_feed.go" line_range="107-108" />
<code_context>
return
}
+ for _, topic := range topicList {
+ topic.NormalizeInputs()
+ }
+
</code_context>
<issue_to_address>
**issue (bug_risk):** NormalizeInputs is called on a copy of each TopicFeed, so list responses never expose normalized Inputs
Ranging with `for _, topic := range topicList` passes each `TopicFeed` by value, so `NormalizeInputs` only updates a copy and `topicList` is unchanged. That means the `/topic_feed` list response won’t actually contain normalized `Inputs` that the frontend expects.
Iterate by index instead so you mutate the slice elements:
```go
for i := range topicList {
topicList[i].NormalizeInputs()
}
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| for _, topic := range topicList { | ||
| topic.NormalizeInputs() |
There was a problem hiding this comment.
issue (bug_risk): NormalizeInputs is called on a copy of each TopicFeed, so list responses never expose normalized Inputs
Ranging with for _, topic := range topicList passes each TopicFeed by value, so NormalizeInputs only updates a copy and topicList is unchanged. That means the /topic_feed list response won’t actually contain normalized Inputs that the frontend expects.
Iterate by index instead so you mutate the slice elements:
for i := range topicList {
topicList[i].NormalizeInputs()
}| const loadPickerData = async () => { | ||
| pickerLoading.value = true; | ||
| try { | ||
| const [recipesRes, topicsRes] = await Promise.all([ | ||
| getCustomRecipes(), | ||
| listTopicFeeds(), | ||
| ]); | ||
| availableRecipes.value = recipesRes.data ?? []; | ||
| availableTopics.value = topicsRes.data ?? []; | ||
| } finally { | ||
| pickerLoading.value = false; | ||
| } | ||
| }; |
There was a problem hiding this comment.
1. loadpickerdata() lacks error notice 📘 Rule violation ☼ Reliability
loadPickerData() performs network fetches without a catch, so failures can become unhandled and no user-visible notification is shown. This violates the requirement to surface failing actions to users instead of only failing silently/unobservably.
Agent Prompt
## Issue description
`loadPickerData()` fetches picker data via `getCustomRecipes()` and `listTopicFeeds()` but does not handle errors with a user-visible notification (and can result in an unhandled rejection).
## Issue Context
This is part of the new Topic Feed wizard page; if these calls fail, users may see an incomplete/broken picker experience without any clear feedback.
## Fix Focus Areas
- web/admin/src/views/dashboard/topic_feed/editor.vue[245-257]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| // Inputs carries URI plus optional description for admin display. | ||
| Inputs []TopicInput `json:"inputs,omitempty" gorm:"serializer:json"` | ||
|
|
||
| // List of URIs representing inputs (derived from Inputs for runtime). | ||
| // Uses a custom protocol for internal resources to make routing elegant and standard. | ||
| // Examples: | ||
| // - "feedcraft://recipe/my-tech-recipe" (Internal RecipeFeed) | ||
| // - "feedcraft://topic/sub-topic-id" (Nested internal TopicFeed) | ||
| // - "https://external.com/rss.xml" (External raw feed) | ||
| InputURIs []string `json:"input_uris" binding:"required" gorm:"serializer:json"` | ||
| InputURIs []string `json:"input_uris" gorm:"serializer:json"` | ||
|
|
There was a problem hiding this comment.
2. Inputs not validated on save 🐞 Bug ≡ Correctness
CreateTopicFeed/UpdateTopicFeed 在写入数据库前未强制校验“至少一个启用输入源”,且 InputURIs 不再是 binding required,导致可保存 inputs/input_uris 为空的 TopicFeed,运行时构建 Topic 会得到 0 个输入或直接失败。
Agent Prompt
## Issue description
Create/Update API can persist a TopicFeed with no enabled inputs because `input_uris` is no longer `binding:"required"` and the controllers don’t validate `len(topicData.InputURIs) > 0` after `NormalizeInputs()`.
## Issue Context
The codebase already defines the business rule in `validateTopicConfig`: at least one enabled input is required. Runtime topic building iterates `topic.InputURIs` to construct providers.
## Fix Focus Areas
- internal/controller/topic_feed.go[63-79]
- internal/controller/topic_feed.go[114-147]
- internal/controller/topic_feed.go[281-351]
## Suggested fix
1. In `CreateTopicFeed` and `UpdateTopicFeed`, after `topicData.NormalizeInputs()`:
- If `len(topicData.InputURIs) == 0`, return `400 BadRequest` with a clear message (e.g. "At least one enabled input source is required").
2. (Optional but stronger) Reuse/extend `validateTopicConfig` to provide richer errors (runtime build errors) for Create/Update as well, or add a lightweight URI validation step for internal URIs.
3. Ensure responses consistently return `input_uris` as an empty array (not null) when invalid input is rejected.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
|
Caution Review failedPull request was closed or merged during review WalkthroughReplace flat topic input URIs with structured per-input objects (uri/description/disabled), add normalization and migration, update controllers and runtime to use enabled inputs, and implement a multi-step frontend editor, preview component, routes, and list/detail UI changes. ChangesTopic Feed Input Management Redesign
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal/controller/topic_feed.go (1)
287-324:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winPer-input "Input URI cannot be empty" check is now unreachable.
NormalizeInputs()is invoked at Line 171 (inValidateTopicFeed) and again at Line 288 here, and it drops any input whose trimmed URI is empty (lines 51-53 intopic.go). By the time the loop at Lines 316-324 runs,topicData.Inputscan no longer contain an empty URI, so theinputs[%d].urierror is never produced. Net effect: a submitted empty URI is silently dropped instead of being reported; if it was the only source, the user gets the generic "At least one enabled input source is required" message instead.If silent dropping is intended, remove the dead loop; if you want to surface empty-URI errors, validate before normalizing.
♻️ Option A — validate raw inputs before normalization
func validateTopicConfig(ctx context.Context, db *gorm.DB, topicData *dao.TopicFeed) (*TopicValidationResult, error) { result := &TopicValidationResult{ Valid: true, Errors: []TopicValidationIssue{}, Warnings: []TopicValidationIssue{}, } - if topicData != nil { - topicData.NormalizeInputs() - } if topicData == nil { result.Valid = false result.Errors = append(result.Errors, TopicValidationIssue{ Field: "topic", Message: "Topic config is required", }) return result, nil } + + for idx, input := range topicData.Inputs { + if strings.TrimSpace(input.URI) == "" { + result.Valid = false + result.Errors = append(result.Errors, TopicValidationIssue{ + Field: fmt.Sprintf("inputs[%d].uri", idx), + Message: "Input URI cannot be empty", + }) + } + } + topicData.NormalizeInputs()Then drop the now-duplicated loop at Lines 316-324.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/controller/topic_feed.go` around lines 287 - 324, The per-input "Input URI cannot be empty" check is dead because topicData.NormalizeInputs() removes empty URIs; move the per-input validation to run before calling NormalizeInputs() in ValidateTopicFeed (and/or in the current function where topicData.NormalizeInputs() is called) so empty URIs are reported from topicData.Inputs (validate strings.TrimSpace(input.URI) == "" and append the TopicValidationIssue for inputs[%d].uri), then remove the later redundant loop that checks topicData.Inputs after normalization; keep the existing overall "At least one enabled input source is required" check that uses topicData.InputURIs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@web/admin/src/views/dashboard/topic_feed/topicInputUtils.ts`:
- Around line 96-108: sourceToUri currently returns
feedcraft://recipe/${resourceId} or feedcraft://topic/${resourceId} even when
resourceId is an empty string, causing countEnabledInputs, canPreviewSource and
normalizeTopicPayload to treat empty IDs as valid; update sourceToUri to treat
empty/whitespace resourceId as an empty URI by checking source.resourceId.trim()
=== '' and returning '' in that case for both 'recipe' and 'topic' branches so
client-side gating aligns with backend
parseInternalResourceURI/validateTopicFeed behavior.
---
Outside diff comments:
In `@internal/controller/topic_feed.go`:
- Around line 287-324: The per-input "Input URI cannot be empty" check is dead
because topicData.NormalizeInputs() removes empty URIs; move the per-input
validation to run before calling NormalizeInputs() in ValidateTopicFeed (and/or
in the current function where topicData.NormalizeInputs() is called) so empty
URIs are reported from topicData.Inputs (validate strings.TrimSpace(input.URI)
== "" and append the TopicValidationIssue for inputs[%d].uri), then remove the
later redundant loop that checks topicData.Inputs after normalization; keep the
existing overall "At least one enabled input source is required" check that uses
topicData.InputURIs.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: ecf26c98-4a76-44ef-831b-e4ce77ab7f2d
📒 Files selected for processing (15)
.gitignoreinternal/controller/topic_feed.gointernal/dao/topic.gointernal/dao/topic_test.goweb/admin/src/api/topic.tsweb/admin/src/components/feed-preview/FeedPreviewPanel.vueweb/admin/src/locale/en-US/topic.tsweb/admin/src/locale/zh-CN/topic.tsweb/admin/src/router/routes/modules/worktable.tsweb/admin/src/views/dashboard/topic_feed/components/TopicAggregatorEditor.vueweb/admin/src/views/dashboard/topic_feed/components/TopicInputSourcesEditor.vueweb/admin/src/views/dashboard/topic_feed/detail.vueweb/admin/src/views/dashboard/topic_feed/editor.vueweb/admin/src/views/dashboard/topic_feed/topicInputUtils.tsweb/admin/src/views/dashboard/topic_feed/topic_feed.vue
| export const sourceToUri = (source: InputSourceItem): string => { | ||
| if (source.sourceType === 'recipe') { | ||
| return `feedcraft://recipe/${source.resourceId}`; | ||
| } | ||
| if (source.sourceType === 'topic') { | ||
| return `feedcraft://topic/${source.resourceId}`; | ||
| } | ||
| return source.externalUrl.trim(); | ||
| }; | ||
|
|
||
| export const countEnabledInputs = (sources: InputSourceItem[]): number => | ||
| sources.filter((source) => sourceToUri(source) !== '' && !source.disabled) | ||
| .length; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Inspect frontend utils around the mentioned snippet and usages
rg -n "topicInputUtils\.ts|sourceToUri|countEnabledInputs|canPreviewSource|normalizeTopicPayload" web/admin/src/views/dashboard/topic_feed/topicInputUtils.ts
sed -n '1,220p' web/admin/src/views/dashboard/topic_feed/topicInputUtils.ts
# Find call sites (editor.vue / topic input editor)
rg -n "countEnabledInputs|canPreviewSource|normalizeTopicPayload|sourceToUri" web/admin/src/views/dashboard/topic_feed
rg -n "configuredInputCount|at least one enabled input|TopicInputSourcesEditor" web/admin/src/views/dashboard/topic_feed -S
# 2) Locate backend validation/normalization (by name first)
rg -n "validateTopicFeed|NormalizeInputs|normalize.*inputs|topic.?feed" -S .Repository: Colin-XKL/FeedCraft
Length of output: 12694
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect frontend API wrapper
sed -n '70,150p' web/admin/src/api/topic.ts
# Inspect backend controller + DAO normalization
sed -n '1,220p' internal/dao/topic.go
sed -n '220,520p' internal/dao/topic.go
sed -n '1,240p' internal/controller/topic_feed.go
sed -n '240,420p' internal/controller/topic_feed.go
sed -n '420,700p' internal/controller/topic_feed.go
# Inspect tests for NormalizeInputs
sed -n '1,220p' internal/dao/topic_test.go
sed -n '220,520p' internal/dao/topic_test.go
# Search for parsing of feedcraft://recipe/ and topic and how empty ids are handled
rg -n "feedcraft://recipe/|feedcraft://topic/|recipe/\\$|topic/\\$" internal/dao internal/controller -SRepository: Colin-XKL/FeedCraft
Length of output: 19427
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate runtime URI parsing/building for topic feed inputs
rg -n "BuildTopicProvider|NewBuilder|feedcraft://recipe/|feedcraft://topic/" internal/feedruntime -S
rg -n "recipe/|topic/" internal/feedruntime -S
# Inspect the builder + topic provider implementation(s)
fd -t f -e go internal/feedruntime | head -n 50
# Print the files that likely contain builder/provider logic
ls -R internal/feedruntime | head -n 200
# Search for how recipe/topic IDs are extracted from URIs and whether empty IDs are rejected
rg -n "strings\\.TrimSpace\\(.*\\)|Split\\(|slice\\(|TrimPrefix|feedcraft://recipe/\\'" internal/feedruntime -S
rg -n "resourceId|recipeID|topicID|sub-topic" internal/feedruntime -SRepository: Colin-XKL/FeedCraft
Length of output: 3519
🏁 Script executed:
#!/bin/bash
set -euo pipefail
ls internal/feedruntime
sed -n '1,220p' internal/feedruntime/builder.go
sed -n '220,520p' internal/feedruntime/builder.go
# Search within internal/feedruntime for the actual parsing logic
rg -n "feedcraft://recipe/|feedcraft://topic/" internal/feedruntime -S
rg -n "recipeID|topicID|strings\\.TrimSpace\\(.*\\)|Split\\(|TrimPrefix\\(|slice\\(" internal/feedruntime -S
rg -n "empty|missing|invalid|cannot be empty" internal/feedruntime -S
# Also inspect feed viewer tests that build from URIs
rg -n "Build.*Provider|feedcraft://recipe/|feedcraft://topic/" internal/controller -S
sed -n '1,260p' internal/controller/feed_viewer_test.goRepository: Colin-XKL/FeedCraft
Length of output: 32532
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Frontend submit/validation gating
sed -n '180,420p' web/admin/src/views/dashboard/topic_feed/editor.vue
sed -n '1,120p' web/admin/src/views/dashboard/topic_feed/components/TopicInputSourcesEditor.vue
sed -n '160,330p' web/admin/src/views/dashboard/topic_feed/components/TopicInputSourcesEditor.vue
# Backend internal URI parsing (resource id empty handling)
sed -n '560,720p' internal/feedruntime/builder.go
# Find parseInternalResourceURI definition precisely
rg -n "func parseInternalResourceURI" -n internal/feedruntime/builder.goRepository: Colin-XKL/FeedCraft
Length of output: 17740
Fix client-side gating for empty recipe/topic resourceId (UX consistency)
sourceToUrireturnsfeedcraft://recipe//feedcraft://topic/even whenresourceId === '', socountEnabledInputs,canPreviewSource, andnormalizeTopicPayloadtreat it as enabled/previewable.validateTopicFeedultimately rejects it: backendparseInternalResourceURIerrors withmissing resource id, andvalidateTopicConfigrunsBuildTopicProviderduring validation; the admin editor also blocks create/update when validation fails. Remaining gap is UX/wizard behavior (and preview may fail).
🐛 Proposed fix to treat empty resource id as empty URI
export const sourceToUri = (source: InputSourceItem): string => {
if (source.sourceType === 'recipe') {
- return `feedcraft://recipe/${source.resourceId}`;
+ return source.resourceId ? `feedcraft://recipe/${source.resourceId}` : '';
}
if (source.sourceType === 'topic') {
- return `feedcraft://topic/${source.resourceId}`;
+ return source.resourceId ? `feedcraft://topic/${source.resourceId}` : '';
}
return source.externalUrl.trim();
};🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/admin/src/views/dashboard/topic_feed/topicInputUtils.ts` around lines 96
- 108, sourceToUri currently returns feedcraft://recipe/${resourceId} or
feedcraft://topic/${resourceId} even when resourceId is an empty string, causing
countEnabledInputs, canPreviewSource and normalizeTopicPayload to treat empty
IDs as valid; update sourceToUri to treat empty/whitespace resourceId as an
empty URI by checking source.resourceId.trim() === '' and returning '' in that
case for both 'recipe' and 'topic' branches so client-side gating aligns with
backend parseInternalResourceURI/validateTopicFeed behavior.
Co-authored-by: Colin <Colin_XKL@outlook.com>
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>
概述
优化 Topic Feed 管理后台的创建/编辑体验,使配置流程更清晰、输入源更易识别,并支持在添加子 Feed 时即时预览。子 Feed 支持禁用状态,且数据库最终只保留一个 URI 相关字段:
inputs。主要变更
/worktable/topic_feed/create与/worktable/topic_feed/:id/edit分步向导inputs: [{ uri, description, disabled }]input_uris,运行时通过EnabledInputURIs()计算启用 URItopic_feeds.input_uris到inputs后删除旧列input_uris测试
go test ./...task fix && task backend-build && task frontend-buildinput_uris,DB schema 仅包含inputstopic_feed_disable_edit_success.mp4
To show artifacts inline, enable in settings.
Summary by Sourcery
Refine Topic Feed management by moving creation/editing into a dedicated step-by-step editor, enriching input metadata and preview capabilities, and syncing new input structures across backend and detail views.
New Features:
Bug Fixes:
Enhancements:
Tests: