From 15afc34a9c85e7ca17384f3f507459b0885ca72a Mon Sep 17 00:00:00 2001 From: tangsongdayo Date: Sun, 19 Jul 2026 04:51:35 +0800 Subject: [PATCH] feat: derive source names from Memos accounts --- app/api/sources/[id]/manage/route.ts | 8 +++----- app/api/sources/route.ts | 5 +++-- app/dashboard/page.tsx | 2 +- worker/index.ts | 3 ++- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/api/sources/[id]/manage/route.ts b/app/api/sources/[id]/manage/route.ts index 46dff6e..037dec9 100644 --- a/app/api/sources/[id]/manage/route.ts +++ b/app/api/sources/[id]/manage/route.ts @@ -14,10 +14,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str const member = db.prepare("SELECT role FROM source_members WHERE source_id=? AND user_id=?").get(id, user.id); if (!source || !member) throw new Error("Source not found"); const owner = source.user_id === user.id; - if (action === "rename") { - const name = String(form.get("name") || "").trim(); if (!owner || !name || name.length > 80) throw new Error("Only the owner can rename a source"); - db.prepare("UPDATE sources SET name=? WHERE id=?").run(name, id); - } else if (action === "set-enabled") { + if (action === "set-enabled") { if (!owner) throw new Error("Only the owner can change source status"); const enabled = String(form.get("enabled")) === "1"; db.prepare("UPDATE sources SET is_enabled=?,disabled_at=CASE WHEN ? THEN NULL ELSE CURRENT_TIMESTAMP END,sync_status=CASE WHEN ? THEN 'pending' ELSE 'disabled' END WHERE id=?").run(enabled ? 1 : 0, enabled ? 1 : 0, enabled ? 1 : 0, id); if (enabled) queuePull(id, "manual"); @@ -37,7 +34,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str try { const token = decrypt(source.token_encrypted); await verifyMemos(source.base_url, token); const identity = await getMemosIdentity(source.base_url, token); const avatar = identity.avatarUrl || identity.avatar || null; const avatarUrl = avatar?.startsWith("/") ? `${source.base_url.replace(/\/$/, "")}${avatar}` : avatar; - db.prepare("UPDATE sources SET last_connection_at=CURRENT_TIMESTAMP,last_connection_error=NULL,remote_display_name=?,remote_avatar_url=? WHERE id=?").run(identity.nickname || identity.username || identity.name, avatarUrl, id); + const name = identity.nickname || identity.username || identity.name; + db.prepare("UPDATE sources SET name=?,last_connection_at=CURRENT_TIMESTAMP,last_connection_error=NULL,remote_display_name=?,remote_avatar_url=? WHERE id=?").run(name, name, avatarUrl, id); } catch (connectionError) { const message = connectionError instanceof Error ? connectionError.message : "Connection failed"; db.prepare("UPDATE sources SET last_connection_error=? WHERE id=?").run(message, id); throw connectionError; } } else if (action === "leave") { if (owner) throw new Error("Transfer ownership or delete the source before leaving"); diff --git a/app/api/sources/route.ts b/app/api/sources/route.ts index e47f4cb..b7438ea 100644 --- a/app/api/sources/route.ts +++ b/app/api/sources/route.ts @@ -10,10 +10,10 @@ import { requireSameOrigin } from "@/lib/security"; export async function POST(req: Request) { try { requireSameOrigin(req); const user = await requireUser(); const form = await req.formData(); - const name = String(form.get("name") || "").trim(); const rawBaseUrl = String(form.get("baseUrl") || "").trim(); const token = String(form.get("token") || "").trim(); + const rawBaseUrl = String(form.get("baseUrl") || "").trim(); const token = String(form.get("token") || "").trim(); let baseUrl = ""; try { const url = new URL(rawBaseUrl); if (!['http:', 'https:'].includes(url.protocol)) throw new Error(); baseUrl = `${url.origin}${url.pathname.replace(/\/+$/, "")}`; } catch { throw new Error("Invalid source URL"); } - if (!name || token.length < 20) throw new Error("Invalid source"); + if (token.length < 20) throw new Error("Invalid source"); await verifyMemos(baseUrl, token); const identity = await getMemosIdentity(baseUrl, token); const legacySources = db.prepare("SELECT id,token_encrypted FROM sources WHERE base_url=? AND remote_user IS NULL").all(baseUrl) as { id: number; token_encrypted: string }[]; for (const legacy of legacySources) { @@ -21,6 +21,7 @@ export async function POST(req: Request) { } const shared = db.prepare("SELECT id FROM sources WHERE base_url=? AND remote_user=?").get(baseUrl, identity.name) as { id: number } | undefined; if (shared) { db.prepare("INSERT OR IGNORE INTO source_members(source_id,user_id) VALUES(?,?)").run(shared.id, user.id); return NextResponse.redirect(externalUrl(req, "/dashboard?source=shared")); } + const name = identity.nickname || identity.username || identity.name; const out = db.prepare("INSERT INTO sources(user_id,name,base_url,token_encrypted,remote_user,sync_status) VALUES(?,?,?,?,?, 'queued')").run(user.id, name, baseUrl, encrypt(token), identity.name); const sourceId = Number(out.lastInsertRowid); db.prepare("INSERT INTO source_members(source_id,user_id,role) VALUES(?,?,'owner')").run(sourceId, user.id); diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 77a6924..2fd210d 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -20,7 +20,7 @@ export default async function Dashboard({ searchParams }: { searchParams: Promis {query.source === "shared" ?

你已加入既有的共享 Memos 來源,不會重複同步貼文。

: query.source === "updated" ?

來源設定已更新。

: query.source &&

來源已連接,首次同步已排入佇列。

} {query.sync === "queued" &&

同步已排入佇列。

}{query.sync === "already-queued" &&

此來源已有同步工作處理中,不重複排入。

}

發佈到自己的 Memos

{publishSources.length ? :

請先連接並啟用一個 Memos 來源。

}
-

連接 Memos

Token 會使用伺服器金鑰加密保存。同一個 Memos 帳號與網址會自動共用來源,不會建立重複貼文。

+

連接 Memos

來源名稱會自動使用 API Key 對應的 Memos 帳號。Token 會使用伺服器金鑰加密保存;同一個 Memos 帳號與網址會自動共用來源,不會建立重複貼文。

已連接來源

{sources.map((source) =>
{source.name}{source.is_enabled ? source.sync_status : "disabled"}

來源 ID:{source.id}
{source.base_url}{source.remote_display_name && <>
Memos 帳號:{source.remote_avatar_url && } {source.remote_display_name}}
成員:{source.members.map((member) => `${member.username}${member.role === "owner" ? "(建立者)" : ""}`).join("、")}
上次同步:{source.last_synced_at || "尚未完成"}
附件保存:{source.attachment_storage_mode === "remote" ? "遠端連結" : source.attachment_storage_mode === "images" ? "只快取圖片" : "完整備份"}(配額 {Math.round(source.attachment_cache_limit_bytes / 1024 / 1024)} MiB)
連線:{source.last_connection_at ? `最近成功:${new Date(source.last_connection_at + "Z").toLocaleString("zh-TW")}` : "尚未測試"}
Webhook:{source.webhook_secret_hash ? (source.last_webhook_at ? (Date.now() - new Date(source.last_webhook_at + "Z").getTime() > 7 * 24 * 60 * 60 * 1000 ? `警示:超過 7 天未收到(最近:${new Date(source.last_webhook_at + "Z").toLocaleString("zh-TW")})` : `健康(最近收到:${new Date(source.last_webhook_at + "Z").toLocaleString("zh-TW")})`) : "已建立 URL,尚未收到呼叫") : "尚未建立 URL"}{!source.is_enabled && <>
已停用:{source.disabled_at ? new Date(source.disabled_at + "Z").toLocaleString("zh-TW") : "是"}}{source.last_error && <>
同步:{source.last_error}}{source.last_connection_error && <>
連線:{source.last_connection_error}}{source.attachment_cache_error && <>
附件快取:{source.attachment_cache_error}}

diff --git a/worker/index.ts b/worker/index.ts index 393da21..8e4178b 100644 --- a/worker/index.ts +++ b/worker/index.ts @@ -56,7 +56,8 @@ async function pull(source: Source) { else db.prepare("UPDATE posts SET hidden=1,updated_at=CURRENT_TIMESTAMP WHERE source_id=? AND remote_memo_name IS NOT NULL").run(source.id); await cleanupCache(source); const avatar = identity.avatarUrl || identity.avatar || null; const avatarUrl = avatar?.startsWith("/") ? `${source.base_url.replace(/\/$/, "")}${avatar}` : avatar; - db.prepare("UPDATE sources SET sync_status='synced',last_synced_at=CURRENT_TIMESTAMP,last_error=NULL,last_connection_at=CURRENT_TIMESTAMP,last_connection_error=NULL,remote_display_name=?,remote_avatar_url=? WHERE id=?").run(identity.nickname || identity.username || identity.name, avatarUrl, source.id); + const name = identity.nickname || identity.username || identity.name; + db.prepare("UPDATE sources SET name=?,sync_status='synced',last_synced_at=CURRENT_TIMESTAMP,last_error=NULL,last_connection_at=CURRENT_TIMESTAMP,last_connection_error=NULL,remote_display_name=?,remote_avatar_url=? WHERE id=?").run(name, name, avatarUrl, source.id); } async function push(source: Source, payload: any) {