feat(sources,organize): site agent capabilities and organize.minSizeBytes setting (W10-pre2)
- site.list / site.health_check / site.refresh_login / site.user_data capabilities over the W9-A site center services; outputs whitelisted (no secret refs, cookies, passwords, login names); execute kinds need automation grant. IDs use snake_case (CapabilityId regex); registry count 17 -> 21. - settings.organize.minSizeBytes (default 50 MiB); file-manager identify / organize and planImport take request > setting > port default. - tests and runbook notes (sites, files). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
c08d8ce62a
commit
81bf010e3e
18 files changed
+563
-15
No files matched your search
@@ -172,7 +172,12 @@ export async function createHarness(name: string, overrides: AoOverrides = {}):
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const k of Object.keys(DEFAULT_SETTINGS) as SettingsKey[]) {
|
||||
const v = stored.get(k);
|
||||
out[k] = GlobalSettings.shape[k].parse({ ...DEFAULT_SETTINGS[k], ...((v as object) ?? {}) });
|
||||
// 整理最小体积缺省跟随端口注入的 minVideoBytes(测试用小文件)
|
||||
const base =
|
||||
k === "organize"
|
||||
? { ...DEFAULT_SETTINGS.organize, minSizeBytes: overrides.options?.minVideoBytes ?? 100_000 }
|
||||
: DEFAULT_SETTINGS[k];
|
||||
out[k] = GlobalSettings.shape[k].parse({ ...base, ...((v as object) ?? {}) });
|
||||
}
|
||||
return out as GlobalSettings;
|
||||
},
|
||||
|
||||
@@ -524,7 +524,7 @@ export async function planImport(d: AoDeps, req: ImportRequest): Promise<PlanRes
|
||||
templates: fileTemplates,
|
||||
target: { ...target, ...(health.kind === "online" ? { device: health.dev } : {}) },
|
||||
retention: { hnrSatisfied: hnr },
|
||||
minVideoBytes: req.minSizeBytes ?? d.ports.options.minVideoBytes,
|
||||
minVideoBytes: req.minSizeBytes ?? orgSettings.minSizeBytes ?? d.ports.options.minVideoBytes,
|
||||
...(req.fileOverrides ? { overrides: req.fileOverrides } : {}),
|
||||
...(d.ctx.config?.ORGANIZE_DEFAULT_SUBTITLE_LANG
|
||||
? { defaultSubtitleLang: d.ctx.config.ORGANIZE_DEFAULT_SUBTITLE_LANG }
|
||||
|
||||
@@ -86,7 +86,7 @@ export async function identify(
|
||||
): Promise<FilesIdentifyResult> {
|
||||
const db = d.ctx.db;
|
||||
const { videos, truncated } = await collectVideos(d, body);
|
||||
// G2:最小体积(请求 > settings.organize.minSizeBytes > 不过滤)
|
||||
// G2:最小体积(请求/向导 > settings.organize.minSizeBytes,缺省 50 MiB;设置读不到时不过滤)
|
||||
const orgSettings = (await d.ctx.settings.get("organize").catch(() => null)) as { minSizeBytes?: unknown } | null;
|
||||
const minSize = body.minSizeBytes ?? (typeof orgSettings?.minSizeBytes === "number" ? orgSettings.minSizeBytes : 0);
|
||||
let excludedBySize = 0;
|
||||
|
||||
@@ -118,7 +118,7 @@ function segmentsOf(plan: FrozenPlan | undefined, overrideDir: boolean): FilesOr
|
||||
);
|
||||
}
|
||||
|
||||
/** 设置里的最小体积(settings.organize.minSizeBytes;契约未加该字段前恒为 undefined → planImport 用端口默认) */
|
||||
/** 设置里的最小体积(settings.organize.minSizeBytes,缺省 50 MiB);读不到时 undefined → planImport 再读设置 / 端口默认 */
|
||||
async function settingsMinSize(d: FmDeps): Promise<number | undefined> {
|
||||
const org = (await d.ctx.settings.get("organize").catch(() => null)) as { minSizeBytes?: unknown } | null;
|
||||
return typeof org?.minSizeBytes === "number" && org.minSizeBytes >= 0 ? org.minSizeBytes : undefined;
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import type { CapabilityRegistry } from "@umt/agent";
|
||||
import { capabilitySpec, type SiteAccountUserData, type SiteLastHealth } from "@umt/contracts";
|
||||
import type { SiteAccountRow, SiteRow } from "@umt/db";
|
||||
import type { SourcesRuntime } from "./runtime.ts";
|
||||
import { checkSiteHealth, overallAccountStatus, refreshSiteAccount, userDataHistory } from "./service/account.ts";
|
||||
|
||||
// W10-pre2 站点能力:site.list / site.health_check / site.refresh_login / site.user_data,处理器调 W9-A 站点中心的 service。
|
||||
// 脱敏:输出按白名单逐字段拼装——不含 secret 引用、Cookie、密码、登录名、UA 与站点 config。
|
||||
|
||||
const iso = (d: Date | null) => (d ? d.toISOString() : null);
|
||||
|
||||
function accountBrief(a: SiteAccountRow) {
|
||||
return {
|
||||
id: a.id,
|
||||
label: a.label,
|
||||
authKind: a.auth_kind,
|
||||
enabled: a.enabled,
|
||||
status: a.status,
|
||||
lastLoginAt: iso(a.last_login_at),
|
||||
lastVerifiedAt: iso(a.last_verified_at),
|
||||
lastError: (a.last_error ?? null) as never,
|
||||
userDataAt: iso(a.user_data_at),
|
||||
hasSession: !!a.session_secret_ref,
|
||||
hasPasskey: !!a.passkey_secret_ref || (a.auth_kind === "passkey" && !!a.secret_ref),
|
||||
};
|
||||
}
|
||||
|
||||
async function liveSite(r: SourcesRuntime, siteId: string): Promise<SiteRow> {
|
||||
const s = await r.ctx.db
|
||||
.selectFrom("site")
|
||||
.selectAll()
|
||||
.where("id", "=", siteId)
|
||||
.where("deleted_at", "is", null)
|
||||
.executeTakeFirst();
|
||||
if (!s) throw new Error("站点不存在");
|
||||
return s;
|
||||
}
|
||||
|
||||
export function registerSiteCapabilities(registry: CapabilityRegistry, rt: () => SourcesRuntime): void {
|
||||
registry.register(capabilitySpec("site.list"), async (input) => {
|
||||
const { db } = rt().ctx;
|
||||
let q = db.selectFrom("site").selectAll().where("deleted_at", "is", null);
|
||||
if (!input.includeDisabled) q = q.where("enabled", "=", true);
|
||||
if (input.keys?.length) q = q.where("key", "in", input.keys);
|
||||
const sites = await q.orderBy("key").execute();
|
||||
const accounts = sites.length
|
||||
? await db
|
||||
.selectFrom("site_account")
|
||||
.selectAll()
|
||||
.where(
|
||||
"site_id",
|
||||
"in",
|
||||
sites.map((s) => s.id),
|
||||
)
|
||||
.orderBy("created_at")
|
||||
.execute()
|
||||
: [];
|
||||
return {
|
||||
items: sites.map((s) => {
|
||||
const mine = accounts.filter((a) => a.site_id === s.id);
|
||||
return {
|
||||
id: s.id,
|
||||
key: s.key,
|
||||
name: s.name,
|
||||
adapter: s.adapter,
|
||||
baseUrl: s.base_url,
|
||||
enabled: s.enabled,
|
||||
healthStatus: s.health_status,
|
||||
lastHealth: (s.last_health ?? null) as SiteLastHealth | null,
|
||||
lastHealthAt: iso(s.last_health_at),
|
||||
accountStatus: overallAccountStatus(mine),
|
||||
accounts: mine.map(accountBrief),
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
registry.register(capabilitySpec("site.health_check"), async (input, ctx) => {
|
||||
const r = rt();
|
||||
return checkSiteHealth(r, await liveSite(r, input.siteId), { type: "agent", id: ctx.actorId });
|
||||
});
|
||||
|
||||
registry.register(capabilitySpec("site.refresh_login"), async (input, ctx) => {
|
||||
const r = rt();
|
||||
const s = await liveSite(r, input.siteId);
|
||||
let q = r.ctx.db.selectFrom("site_account").select(["id"]).where("site_id", "=", s.id);
|
||||
q = input.accountId
|
||||
? q.where("id", "=", input.accountId)
|
||||
: q.where("enabled", "=", true).where("auth_kind", "!=", "passkey");
|
||||
const acc = await q.orderBy("created_at").executeTakeFirst();
|
||||
if (!acc) throw new Error(input.accountId ? "站点账号不存在" : "站点还没有凭证");
|
||||
return refreshSiteAccount(r, acc.id, {
|
||||
mode: "login",
|
||||
actor: { type: "agent", id: ctx.actorId },
|
||||
signal: AbortSignal.any([ctx.signal, AbortSignal.timeout(30_000)]),
|
||||
});
|
||||
});
|
||||
|
||||
registry.register(capabilitySpec("site.user_data"), async (input) => {
|
||||
const r = rt();
|
||||
const s = await liveSite(r, input.siteId);
|
||||
let q = r.ctx.db
|
||||
.selectFrom("site_account")
|
||||
.select(["id", "label", "status", "user_data", "user_data_at"])
|
||||
.where("site_id", "=", s.id);
|
||||
if (input.accountId) q = q.where("id", "=", input.accountId);
|
||||
const accounts = await q.orderBy("created_at").execute();
|
||||
return {
|
||||
siteId: s.id,
|
||||
accounts: accounts.map((a) => ({
|
||||
accountId: a.id,
|
||||
label: a.label,
|
||||
status: a.status,
|
||||
userData: (a.user_data ?? null) as SiteAccountUserData | null,
|
||||
userDataAt: iso(a.user_data_at),
|
||||
})),
|
||||
history: await userDataHistory(r, s.id, {
|
||||
...(input.accountId ? { accountId: input.accountId } : {}),
|
||||
days: input.days,
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type { CapabilityRegistry } from "@umt/agent";
|
||||
import { capabilitySpec, type ExternalEntityType, type SourceRelease } from "@umt/contracts";
|
||||
import { sql } from "@umt/db";
|
||||
import { normalizeTitle } from "@umt/domain";
|
||||
import { registerSiteCapabilities } from "./capabilities-sites.ts";
|
||||
import { toApi, toApiList } from "./rows.ts";
|
||||
import type { SourcesRuntime } from "./runtime.ts";
|
||||
import { assignReleaseIdentity } from "./service/identity-admin.ts";
|
||||
@@ -9,10 +10,12 @@ import { resolveRelease } from "./service/resolve.ts";
|
||||
import { refreshReleaseDetail } from "./service/snapshot.ts";
|
||||
import { liveSearch } from "./service/workbench.ts";
|
||||
|
||||
// 本模块领域的能力处理器(规格一律取 contracts):catalog.lookup / release.search / release.inspect / identity.resolve。
|
||||
// 本模块领域的能力处理器(规格一律取 contracts):catalog.lookup / release.search / release.inspect / identity.resolve;
|
||||
// 站点能力 site.* 见 capabilities-sites.ts。
|
||||
// identity.resolve 属 propose 类,授权回调由 W2-D 接入前会被注册中心拒绝(安全默认)。
|
||||
|
||||
export function registerSourcesCapabilities(registry: CapabilityRegistry, rt: () => SourcesRuntime): void {
|
||||
registerSiteCapabilities(registry, rt);
|
||||
registry.register(capabilitySpec("catalog.lookup"), async (input) => {
|
||||
const { db } = rt().ctx;
|
||||
const local: { entity: unknown; score: number; matchedAlias: string | null }[] = [];
|
||||
|
||||
Reference in new issue
Block a user