docs(plan): v0.2 adds Pi subagents and scheduled-jobs board

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
rootandClaude Fable 5.1 committed 2026-09-28 02:09:39 +08:00
1 parent b839ed4056
commit 16ab2a6c84
97 files changed
+592 -648

No files matched your search

@@ -136,7 +136,7 @@ export function registerAuthRoutes(app: ModuleApp, ctx: ServerContext) {
endpoint(app, D.password, async ({ body, c, actor }) => {
const session = c.get("session");
if (!session) throw new ApiException("forbidden", "该操作只能在控制台登录后进行");
if (!session) throw new ApiException("forbidden", "只能在控制台登录后操作");
const revoked = await changePassword(deps(), body, {
adminUserId: session.adminUserId,
sessionId: session.id,
@@ -103,7 +103,7 @@ async function recentFailures(db: Kysely<DB>, col: "ip" | "username", value: str
export async function assertNotRateLimited(db: Kysely<DB>, ip: string, username: string): Promise<void> {
const [byIp, byAccount] = await Promise.all([recentFailures(db, "ip", ip), recentFailures(db, "username", username)]);
if (byIp >= MAX_FAILURES_PER_IP || byAccount >= MAX_FAILURES_PER_ACCOUNT) {
throw new ApiException("rate_limited", "失败次数过多,请 15 分钟后再试");
throw new ApiException("rate_limited", "失败次数过多,15 分钟后再试");
}
}
@@ -190,7 +190,7 @@ export async function setupAdmin(
const admin = await loadAdmin(deps.db);
if (admin?.password_hash) throw new ApiException("conflict", "管理员已初始化");
await recordAttempt(deps.db, { ip, username: "(setup)", success: false, reason: "bad_setup_token" });
throw new ApiException("unauthorized", "setup token 无效(以服务端日志最新打印的为准)");
throw new ApiException("unauthorized", "setup token 无效。使用服务端日志里最新打印的那个");
}
const admin = await loadAdmin(deps.db);
if (!admin) throw new Error("admin_user 行丢失");
@@ -206,7 +206,7 @@ export async function verifyLogin(
): Promise<LoginOutcome> {
await assertNotRateLimited(deps.db, ip, input.username);
const admin = await loadAdmin(deps.db);
if (!admin?.password_hash) throw new ApiException("precondition_failed", "管理员尚未初始化,请先完成初始化");
if (!admin?.password_hash) throw new ApiException("precondition_failed", "管理员尚未初始化");
const userOk = admin.username === input.username;
// 用户名不对也跑一次 argon2,避免用时差探测账号
let pwOk = false;
@@ -284,7 +284,7 @@ export async function totpBegin(deps: AuthDeps, admin: AdminRow): Promise<{ uri:
}
export async function totpEnable(deps: AuthDeps, admin: AdminRow, code: string): Promise<void> {
if (!admin.totp_pending_secret_ref) throw new ApiException("precondition_failed", "请先生成 TOTP 密钥");
if (!admin.totp_pending_secret_ref) throw new ApiException("precondition_failed", "先生成 TOTP 密钥");
const secret = await deps.secrets.resolve(admin.totp_pending_secret_ref);
const counter = verifyTotp(secret, code, null);
if (counter === null) throw new ApiException("unauthorized", "验证码错误");
@@ -68,7 +68,7 @@ const JSON_COLS = ["settings", "version_info", "health_detail", "verify_detail"]
const errMessage = (e: unknown) => redactString(String((e as Error)?.message ?? e)).slice(0, 500);
const LR = crudDefs("/library-roots", "媒体库根", {
const LR = crudDefs("/library-roots", "库根", {
row: LibraryRoot,
create: LibraryRootCreate,
update: LibraryRootUpdate,
@@ -174,7 +174,7 @@ export function registerKeyRoutes(app: ModuleApp, ctx: ServerContext) {
.where("id", "=", params.id)
.returning("id")
.executeTakeFirst()
.catch((e) => pgConflict(e, "凭证仍被引用(实例/站点账号/Feed/设置),请先解除引用"));
.catch((e) => pgConflict(e, "凭证仍被实例、站点账号、Feed 或设置引用,先解除引用"));
if (!r) throw new ApiException("not_found", "凭证不存在");
await audit(ctx, actor, "secret.delete", { type: "secret", id: params.id });
return { ok: true as const };