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) {