feat(server): gate scheduling, search and login by site capabilities (IA-2)

- snapshots only for sites with list; feed polling only for RSS kinds;
  user data / health checks only for sites with login; search site set
  only includes searchable sites; live search / snapshot skip with
  reason unsupported instead of hitting the site
- refresh-login / credentials need login, manual snapshot needs list (400)
- POST /sites accepts kind and rssUrl (site + feed in one transaction,
  key generated from host); RSS belongs to a site: feeds created without
  siteId go to the same-host custom RSS source site, siteId cannot be
  cleared
- fix: secret name clash when a site gets a second default-named feed
- command palette regrouped; agent site.list returns kind and
  capabilities; system prompt notes RSS lives under sites

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-30 00:29:56 +08:00
1 parent 89d2c81de6
commit ec65435328
15 files changed
+486 -80

No files matched your search

+14 -4
View File
@@ -137,13 +137,23 @@ describe.skipIf(!up)("来源管理与站点工作台 API", () => {
expect(other.status).toBe(200);
expect(other.body.data.name).toBe("促销");
expect(other.body.data.warnings?.[0]).toContain("elsewhere.example");
// IA-2:RSS 属于站点。不带站点新建 → 归入同主机的「自定义 RSS 源」站点(没有就建一个)
const bare = await json(await h.req("/feed-sources", { body: { url: "https://any.example/feed.xml" } }));
expect(bare.body.data).toMatchObject({
expect(bare.body.data).toMatchObject({ name: "any.example RSS", siteAccountId: null, parserVersion: "rss@1" });
const bareSite = await json(await h.req(`/sites/${bare.body.data.siteId}`));
expect(bareSite.body.data).toMatchObject({
key: "rss-any-example",
name: "any.example",
siteId: null,
siteAccountId: null,
parserVersion: "1",
kind: "rss_feed",
adapter: "generic_rss",
capabilities: ["rss"],
searchDefault: false,
});
const again = await json(await h.req("/feed-sources", { body: { url: "https://any.example/other.xml" } }));
expect(again.body.data.siteId).toBe(bare.body.data.siteId);
// 不能把 RSS 移出站点
const detach = await h.req(`/feed-sources/${bare.body.data.id}`, { method: "PATCH", body: { siteId: null } });
expect(detach.status).toBe(400);
});
it("快照:列表页 upsert 到 source_release,记录覆盖范围;站点级限流器每站一个实例", async () => {