Files
UHD_Media_Tool/apps/web/app/components/logs/labels.ts
T
rootandClaude Opus 5.5 cf364435eb feat(web): 存储设置与文件管理的 Alist 存储切换
- 设置 → 媒体库与目录 → 存储:登记 Alist / OpenList(令牌 / 账号密码 / 访客、路径密码、限速与监控选项),
  测试连接显示版本、用户与容量;媒体库目录可选存储
- 文件管理左栏多一组「Alist 存储」,路径显示「存储名:/路径」;移动 / 复制目标可选 Alist 存储;
  上传按钮;Alist 根点刷新才让 Alist 刷新目录缓存
- 运行状态的挂载根列出存储实例(令牌失效单独提示)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
2026-10-06 23:57:33 +08:00

300 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
CAPABILITY_CATALOG,
type EngineBusinessKind,
type KnownCapabilityId,
type LogEntry,
type LogLevel,
type LogType,
} from "@umt/contracts";
import type { StatusTone } from "~/components/console/status-badge.tsx";
import { BUSINESS_KIND_LABEL, workflowLabel } from "~/components/tasks/labels.ts";
import { actorTypeLabel } from "~/lib/labels.ts";
import { formatDateTime } from "~/lib/utils.ts";
// 日志页的标签与纯函数(界面第二轮 V 线):类型、级别、审计动作、标题 / 摘要、筛选参数、给智能体的上下文。
export const LOG_PATH = "/console/logs";
export const LOG_TYPE_LABEL: Record<LogType, string> = {
task: "任务",
auto: "定时与自动",
auth: "登录",
audit: "审计",
};
export const LOG_TYPES = Object.keys(LOG_TYPE_LABEL) as LogType[];
export const LOG_LEVEL: Record<LogLevel, { label: string; tone: StatusTone }> = {
running: { label: "进行中", tone: "neutral" },
success: { label: "成功", tone: "success" },
info: { label: "信息", tone: "neutral" },
warn: { label: "警告", tone: "warning" },
error: { label: "失败", tone: "destructive" },
};
export const LOG_LEVELS = Object.keys(LOG_LEVEL) as LogLevel[];
/** 时间范围预设 → 毫秒 */
export const LOG_SINCE: { value: string; label: string; ms: number }[] = [
{ value: "1h", label: "1 小时内", ms: 3600_000 },
{ value: "24h", label: "24 小时内", ms: 86400_000 },
{ value: "7d", label: "7 天内", ms: 7 * 86400_000 },
{ value: "30d", label: "30 天内", ms: 30 * 86400_000 },
];
// ── 审计动作(原审计页) ──
const ACTION_LABEL: Record<string, string> = {
"admin.setup": "初始化管理员",
"admin.login_success": "登录",
"admin.login_failed": "登录失败",
"admin.logout": "退出登录",
"admin.password_change": "修改密码",
"admin.totp_enable": "启用二次验证",
"admin.totp_disable": "停用二次验证",
"admin.recovery_codes_regenerate": "重新生成恢复码",
"admin.recovery_code_login": "用恢复码登录",
"admin.passkey_register": "登记 Passkey",
"admin.passkey_rename": "重命名 Passkey",
"admin.passkey_remove": "删除 Passkey",
"admin.passkey_login": "用 Passkey 登录",
"api_key.create": "创建 API Key",
"api_key.update": "修改 API Key",
"api_key.revoke": "撤销 API Key",
"settings.update": "修改设置",
"secret.create": "新建凭证",
"secret.rotate": "轮换凭证",
"secret.delete": "删除凭证",
"secret.master_key_rotate": "轮换主密钥",
"site_account.credentials_set": "录入站点凭证",
"engine.run_cancel": "取消任务",
"engine.run_retry": "重跑任务",
"pi_upstream.oauth_start": "开始订阅登录",
"pi_upstream.oauth_login": "完成订阅登录",
"pi_upstream.oauth_logout": "退出订阅登录",
"schedule.run_now": "立即执行定时任务",
"schedule.skip": "跳过定时任务",
"schedule.update": "修改定时任务",
"storage.cleanup": "立即清理存储",
"acquisition_plan.manual_submit": "手动提交下载",
"acquisition_plan.cancel": "取消下载",
"capability.invoke": "智能体操作",
"capability.denied": "智能体操作被拒绝",
"site.created": "登记站点",
"site.updated": "修改站点",
"feed_source.created": "新建 RSS",
"identity.release_assigned": "指定资源身份",
"files.mkdir": "新建目录",
"files.rename": "重命名",
"files.move": "移动文件",
"files.copy": "复制文件",
"files.organize": "整理文件",
"files.delete": "删除文件",
"files.trash_restore": "从回收站还原",
};
const VERB: Record<string, string> = { create: "新建", update: "修改", delete: "删除", test: "测试", verify: "校验" };
const NOUN: Record<string, string> = {
library_root: "媒体库目录",
downloader_instance: "下载器",
media_server_instance: "媒体服务器",
path_mapping: "路径映射",
storage_instance: "存储",
pi_upstream: "模型服务",
schedule: "定时任务",
automation_grant: "持续授权",
};
export const TARGET_LABEL: Record<string, string> = {
...NOUN,
admin_user: "管理员",
admin_session: "登录会话",
api_key: "API Key",
settings: "设置",
secret: "凭证",
site: "站点",
site_account: "站点账号",
feed_source: "RSS",
engine_run: "任务",
scheduled_job: "定时任务",
agent_run: "智能体会话",
acquisition_plan: "下载",
download_task: "下载任务",
media_entity: "作品",
source_release: "资源",
organization_record: "整理记录",
subscription: "订阅",
decision: "事项",
file: "文件",
capability: "能力",
};
export function actionLabel(a: string): string {
if (ACTION_LABEL[a]) return ACTION_LABEL[a];
const [noun, verb] = a.split(".");
if (noun && verb && NOUN[noun] && VERB[verb]) return `${VERB[verb]}${NOUN[noun]}`;
return a;
}
/** 对象文字:有展示名用展示名;能力显示中文名;其余显示对象类型 */
export function objectText(o: LogEntry["object"]): string | null {
if (!o) return null;
if (o.label) return o.label;
if (o.type === "capability" && o.id) return CAPABILITY_CATALOG[o.id as KnownCapabilityId]?.title ?? o.id;
return TARGET_LABEL[o.type] ?? o.type;
}
// ── 标题、摘要、状态 ──
const SCHEDULE_STATUS: Record<string, { label: string; tone: StatusTone }> = {
ok: { label: "成功", tone: "success" },
failed: { label: "失败", tone: "destructive" },
skipped: { label: "已跳过", tone: "neutral" },
running: { label: "进行中", tone: "neutral" },
};
const AGENT_STATUS: Record<string, { label: string; tone: StatusTone }> = {
queued: { label: "排队", tone: "neutral" },
waiting: { label: "等待", tone: "neutral" },
cancelled: { label: "已停止", tone: "warning" },
budget_exceeded: { label: "超出预算", tone: "warning" },
};
const ENGINE_STATUS: Record<string, { label: string; tone: StatusTone }> = {
pending: { label: "排队", tone: "neutral" },
canceled: { label: "已取消", tone: "warning" },
};
/** 行上的状态徽章:来源有更具体的说法时用来源的(已跳过、已取消、超出预算),否则用级别 */
export function logBadge(e: Pick<LogEntry, "source" | "status" | "level" | "extra">): {
label: string;
tone: StatusTone;
} {
const s = e.status ?? "";
if (e.source === "schedule_run") {
const agent = typeof e.extra.agentStatus === "string" ? AGENT_STATUS[e.extra.agentStatus] : undefined;
return agent ?? SCHEDULE_STATUS[s] ?? LOG_LEVEL[e.level];
}
if (e.source === "agent_run") return AGENT_STATUS[s] ?? LOG_LEVEL[e.level];
if (e.source === "engine_run") {
const outcome = typeof e.extra.outcome === "string" ? AGENT_STATUS[e.extra.outcome] : undefined;
return (e.level === "warn" ? outcome : undefined) ?? ENGINE_STATUS[s] ?? LOG_LEVEL[e.level];
}
return LOG_LEVEL[e.level];
}
/** 一行标题:任务 = 业务对象名 > 业务类型 > 工作流做什么;审计 = 动作;其余用服务端标题 */
export function logTitle(e: LogEntry): string {
if (e.source === "engine_run") {
const x = e.extra as {
businessTitle?: string | null;
businessKind?: EngineBusinessKind | null;
workflowName?: string;
};
if (x.businessTitle) return x.businessTitle;
if (x.businessKind && BUSINESS_KIND_LABEL[x.businessKind]) return BUSINESS_KIND_LABEL[x.businessKind];
return workflowLabel(x.workflowName ?? e.title);
}
if (e.source === "audit_event") return actionLabel(e.status ?? e.title);
if (e.source === "agent_run") return firstLine(e.title);
return e.title;
}
/** 第二行:任务的工作流(有业务标题时)、错误或摘要 */
export function logSubtitle(e: LogEntry): string | null {
const parts: string[] = [];
if (e.source === "engine_run" && (e.extra as { businessTitle?: string }).businessTitle)
parts.push(workflowLabel(String(e.extra.workflowName ?? "")));
if (e.source === "schedule_run" && e.extra.jobKind === "agent_check") parts.push("智能体巡检");
if (e.source === "agent_run") parts.push("智能体后台调查");
if (e.message) parts.push(firstLine(e.message));
return parts.length ? parts.join(" · ") : null;
}
/** 操作者 / 触发来源 */
export function actorLabel(e: LogEntry): string | null {
if (!e.actor) return null;
if (e.source === "schedule_run")
return e.actor.type === "manual" ? "手动" : e.actor.type === "skip" ? "跳过" : "定时";
if (e.source === "agent_run") return "事件";
return actorTypeLabel(e.actor.type);
}
function firstLine(s: string, max = 140): string {
const line = s.split("\n").find((l) => l.trim()) ?? s;
return line.length > max ? `${line.slice(0, max)}…` : line;
}
// ── 筛选参数(URL query) ──
export const LOG_FILTER_KEYS = ["type", "level", "since", "q", "objectType", "objectId", "system"] as const;
export type LogFilterKey = (typeof LOG_FILTER_KEYS)[number];
export type LogFilters = Record<LogFilterKey, string>;
/** 时间预设下界按分钟取整,查询 key 稳定 */
export function sinceToFrom(since: string, now = Date.now()): string | undefined {
const ms = LOG_SINCE.find((s) => s.value === since)?.ms;
return ms ? new Date(Math.floor((now - ms) / 60_000) * 60_000).toISOString() : undefined;
}
/** 日志页地址(其它页面链到日志用) */
export function logsPath(p: Partial<LogFilters> & { tab?: string; job?: string; log?: string } = {}): string {
const q = new URLSearchParams();
for (const [k, v] of Object.entries(p)) if (v) q.set(k, v);
const s = q.toString();
return s ? `${LOG_PATH}?${s}` : LOG_PATH;
}
/** 某个对象的全部日志 */
export const objectLogsPath = (type: string, id: string) => logsPath({ objectType: type, objectId: id });
// ── 给智能体的上下文 ──
const GOAL_MAX = 6000;
/** 一条日志的文字版(给智能体看,带 id 方便它查) */
export function logLine(e: LogEntry): string {
const obj = e.object ? ` 对象 ${e.object.type}${e.object.id ? `=${e.object.id}` : ""}` : "";
const msg = e.message ? ` · ${firstLine(e.message, 300)}` : "";
return `- ${formatDateTime(e.at)} [${LOG_TYPE_LABEL[e.type]} / ${logBadge(e).label}] ${logTitle(e)}${msg}(${e.id}${obj})`;
}
/** 选中的日志 → 问智能体的目标(按时间正序,超长截断) */
export function agentGoalFor(entries: LogEntry[]): string {
const head =
entries.length === 1
? "看看这条日志,说明发生了什么;有问题的话找出原因并给出处理建议。"
: `看看这 ${entries.length} 条日志,说明发生了什么;有问题的话找出原因并给出处理建议。`;
const lines: string[] = [];
let size = head.length;
const sorted = [...entries].sort((a, b) => Date.parse(a.at) - Date.parse(b.at));
for (const e of sorted) {
const l = logLine(e);
if (size + l.length + 1 > GOAL_MAX) {
lines.push(`- …另有 ${sorted.length - lines.length} 条未列出`);
break;
}
lines.push(l);
size += l.length + 1;
}
return `${head}\n${lines.join("\n")}`;
}
// ── 旧链接 ──
/** 旧任务页的 status / tab → 级别 */
export const RUN_STATUS_LEVEL: Record<string, LogLevel> = {
pending: "running",
running: "running",
completed: "success",
failed: "error",
canceled: "warn",
};
/** 业务对象 kind → 日志对象类型(与服务端 BUSINESS_OBJECT_TYPE 一致) */
export const BUSINESS_OBJECT_TYPE: Record<string, string> = {
download: "download_task",
organization: "organization_record",
agent: "agent_run",
subscription: "subscription",
feed: "feed_source",
site: "site",
decision: "decision",
media: "media_entity",
schedule: "scheduled_job",
};