Files
UHD_Media_Tool/apps/server
rootandClaude Opus 5.5 6e9e8efec2 fix(sites): HHClub parsing, resource list layout and request restraint (re-landed)
Re-lands 901b01d, which was reverted wholesale by 28974d1 (BR-2 committed
from the shared index). The agent UI files 901b01d had rolled back are
left at their AGUI-2 versions; contracts, routes, db types and the runbook
part already reached main through later commits, so this carries the rest.

HHClub (憨憨) was checked read-only against a real account (one detail
request; list, user data and health came from the product's own fetch).

Parsing fixes:
- list titles carried the site's new-torrent marker "[新]" (58 of 100
  rows; the detail page title has none); stripped with a replace filter
- refreshing a detail overwrote seeders / leechers / snatched / category
  name / H&R with null because the HHClub div detail page lacks them;
  detail values now fall back to the list snapshot (mergeDetailOverList),
  and an unknown detail promotion keeps the list promotion
- level "贵宾" is VIP (umt.vipLevels)
- sanitized minimal fixture HHANCLUB_LIST_PAGE_2026 + tests; README marks
  HHClub as verified 2026-09-29
Size, counts, category ids/names, +08:00 time and subtitle were correct.

Request restraint:
- site definitions may carry umt.defaults; merged with settings.sites
  (longer intervals, fewer pages): new settings defaultSnapshotMaxPages 1,
  defaultHealthCheckIntervalSec 6 h, defaultDetailCacheSec 24 h; default
  burst 2. HHClub: snapshot 60 min page 1 only, health 6 h, search
  concurrency 1, detail cache 24 h, 1 token / 3 s burst 2
- periodic / manual snapshots are capped to that page count; scheduled
  health checks skip sites checked within the interval; detail refresh is
  served from cache inside the window; live searches on one site queue
- every rate-limiter token is counted per site per hour
  (site_request_count, migration 0046); GET /sites/{id}/request-stats;
  site overview and resource list show requests in the last 24 hours

Resource list: two-layer rows. Main line: title + spec tags (episodes,
resolution, source, codec, HDR, group, 国语/中字, site tags) + promotion
with time left + H&R. Second line: subtitle (one line, ellipsis) and
fixed-width category / size / seed-leech-snatched / upload time
(relative, absolute on hover) columns, stale badge, inventory overlay;
folds into a card on phones.

Also aligns identity_candidate.normalized_title order in db types with
the migrations (schema drift test).

Takes effect after the stack is rebuilt (migration 0046).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 16:00:34 +08:00
..
2026-09-29 03:27:14 +08:00

@umt/server

服务进程(Deno):Hono HTTP API(前缀 /api/v1,OpenAPI 见 /api/openapi.json、/api/docs)、SSE 事件流与 worker 入口。--role api|worker|worker:agent|all 决定本进程承担哪部分,默认 all 单进程运行(worker 只认领 io 组任务,worker:agent 只认领 Agent 组;另有内部角色 --role fsworker,由 FsProxy 以子进程方式拉起执行单个文件操作,不要手动启动)。业务规则不写在这里,只做编排、鉴权、审计与协议适配。

模块骨架(W2)

src/
  main.ts                 进程入口:按 --role 装配
  context.ts              bootstrap():ServerContext、任务注册表(模块 + 空壳兜底)、outbox 路由合并、连接引擎
  http/app.ts             createApp():系统路由 + 逐个挂 SERVER_MODULES 的 registerRoutes
  worker/index.ts         startWorker():引擎 worker + OutboxRelay + 各模块 start()
  modules/
    types.ts              ServerModule / ServerContext / OutboxWriter / SecretStore / SettingsReader(共享,不随子任务改)
    index.ts              SERVER_MODULES 清单
    auth-settings/        W2-A(provided.ts 提供 secrets / settings 实现)
    sources-identity/     W2-B
    acquisition-organize/ W2-C
    file-manager/         W5-A:/files/*(浏览、mkdir/rename、move/copy/organize 两阶段、回收站、identify/probe/scrape、download),
                          见 docs/runbook/files.md
    agent/                W2-D:capability-registry.ts(authorize / audit / 幂等键 / 操作账本)、会话 API、runAgent、resolveDecision
                          (见 docs/runbook/agent.md;agent 模块可以 import 其它模块的 service 导出,反向不行)

分工规则:各 W2 子任务只改自己的 modules/<area>/ 目录,以及 packages/contracts、packages/db 中本域的文件(新迁移按编号顺延,开工前在群里认领编号)。modules/types.ts、modules/index.ts、context.ts、http/app.ts、worker/index.ts、main.ts 是共享装配,确需改动时单独提交并通知其他子任务;http/auth.ts、http/sse.ts 归 W2-A。跨模块只通过 DB 表 + outbox_event(事件类型在 contracts/events.ts)+ contracts 类型耦合,不互相 import 对方模块。

一个模块长这样(modules/<area>/index.ts 导出,目录内自由拆 routes.ts / service.ts / handlers/*.ts):

import { createRoute } from "@hono/zod-openapi";
import { capabilitySpec, EventType } from "@umt/contracts";
import { defineTask } from "@umt/tasks";
import { requireScope } from "../../http/auth.ts";
import type { ServerModule } from "../types.ts";

export const sourcesIdentityModule: ServerModule = {
  name: "sources-identity",
  // 路径相对 /api/v1,已过 API Key 鉴权;每个路由自己声明 scope
  registerRoutes(app, ctx) {
    app.use("/sites/*", requireScope("read"));
    app.openapi(createRoute({ method: "get", path: "/sites", /* … */ }), async (c) => c.json(/* … */));
  },
  // 同名任务只能由一个模块注册;未注册的 §3.1-8 任务由 @umt/tasks 空壳兜底
  registerTasks(registry, ctx) {
    registry.register(defineTask({ name: "resolveObservation", version: 1, /* … */ handler: async (t) => {
      await ctx.db.transaction().execute(async (trx) => {
        // …写业务表,同事务发事件(payload 按 contracts 校验)
        await ctx.outbox.emit(trx, EventType.releaseResolved, payload, { idempotencyKey: `release.resolved:${id}` });
      });
    } }));
  },
  // 消费别的模块发出的事件:type → 任务;同一 type 只能一个模块认领
  outboxRoutes: () => ({ "release.resolved": (ev) => ({ task: "evaluateAcquisition", input: { sourceReleaseId: (ev.payload as { sourceReleaseId: string }).sourceReleaseId } }) }),
  // 能力:规格一律取 contracts,处理器调本模块服务
  registerCapabilities(registry, ctx) {
    registry.register(capabilitySpec("release.search"), async (input, c) => ({ items: [], coverage: [] }));
  },
  // worker 角色的后台循环(qB 轮询、Feed 调度…);引擎与中继已启动
  async start(ctx) {},
  async stop() {},
};

ServerContext 字段:db(Kysely)、config、logger(pino,自动脱敏)、events(SSE 总线 EventBusPort)、engine(TaskEngine:enqueue/signal/cancel/get/list;register* 阶段不要调用)、outbox(emit(trx, type, payload, {idempotencyKey}) + worker 的 relay)、secrets(SecretStore,W2-A 实现前调用会抛错)、settings(读 settings 表并合并缺省)、capabilities(CapabilityRegistry)。domain ↔ contracts 的取值转换一律用 @umt/mapping。

角色:all 单进程时连接引擎会顺带跑引擎迁移;api / worker 分进程部署时由 --role migrate 预先迁移(db + openworkflow),两个进程都以 runMigrations:false 连接。