feat(feeds): site-scoped RSS add infers account, parser and poll

Adding a feed from a site page now only asks for the URL, purpose
(discover / track / promotion) and an optional note. POST /feed-sources
with siteId infers the site account (passkey holder first), records the
site definition version as parserVersion, defaults the name to
"<site> RSS" and polling to 30 minutes, and returns non-blocking
warnings when the URL host does not match the site. The dialog shows the
redacted URL, fetches once right after saving and reports the item count
or the error.

The generic feed form on the sources page suggests linking a registered
site when the URL host matches (can be undone). name is now optional.

Takes effect after the stack is rebuilt.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-29 14:43:19 +08:00
1 parent 00ebdda7ed
commit 4a92dfd2dc
10 files changed
+512 -17

No files matched your search

@@ -115,6 +115,37 @@ describe.skipIf(!up)("来源管理与站点工作台 API", () => {
expect((await h.req("/feed-sources/00000000-0000-4000-8000-000000000000")).status).toBe(404);
});
it("站点内添加 Feed:只给地址即可,账号 / 解析器 / 名称按站点推断;域名不符只提示", async () => {
const acc = (await json(await h.req(`/site-accounts?siteId=${siteId}`))).body.data.items[0];
const same = await json(
await h.req("/feed-sources", { body: { siteId, url: "https://audiences.me/torrentrss.php?passkey=k1" } }),
);
expect(same.status).toBe(200);
expect(same.body.data).toMatchObject({
name: "观众 RSS",
siteAccountId: acc.id,
purpose: "generic",
pollIntervalSec: 1800,
});
expect(same.body.data.parserVersion).toMatch(/^rss@1;audiences@/);
expect(same.body.data.warnings).toBeUndefined();
const other = await json(
await h.req("/feed-sources", {
body: { siteId, purpose: "promotion", name: "促销", url: "https://elsewhere.example/rss?passkey=k2" },
}),
);
expect(other.status).toBe(200);
expect(other.body.data.name).toBe("促销");
expect(other.body.data.warnings?.[0]).toContain("elsewhere.example");
const bare = await json(await h.req("/feed-sources", { body: { url: "https://any.example/feed.xml" } }));
expect(bare.body.data).toMatchObject({
name: "any.example",
siteId: null,
siteAccountId: null,
parserVersion: "1",
});
});
it("快照:列表页 upsert 到 source_release,记录覆盖范围;站点级限流器每站一个实例", async () => {
const r = await snapshotSite(h.rt, { siteId, kind: "periodic" });
expect(r.status).toBe("ok");