diff --git a/DESIGN.md b/DESIGN.md index 4d60c7f..9e258a9 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -41,4 +41,4 @@ 本插件只有一个通道:`ntfy`。其他推送通道和对应实现已移除。 -NTFY 元数据使用 `Title`、`Tags`、`X-Project`、`X-Session`、`X-Event`、`X-Task` headers。含中文的字段会写入 Base64 版本的 headers,完整中文内容始终保留在正文中。 +NTFY 采用 JSON 发布(POST 到 server 根路径):`title`、`message` 随 UTF-8 JSON body 传输,中文标题/正文不受 HTTP header 只允许 Latin-1 的限制;`tags`/`priority`/`click`/`icon`/`email` 同样走 JSON 字段。 diff --git a/channels/ntfy.ts b/channels/ntfy.ts index 6fa4a86..2b1cd8c 100644 --- a/channels/ntfy.ts +++ b/channels/ntfy.ts @@ -25,14 +25,6 @@ function eventTagFor(event: NotifyPayload["event"]): string { } } -function isAscii(value: string): boolean { - return [...value].every((char) => char.charCodeAt(0) <= 255); -} - -function base64(value: string): string { - return Buffer.from(value, "utf8").toString("base64url"); -} - function addErrorDetail(result: HttpResult): HttpResult { if (result.ok) return result; let detail = result.error ?? `HTTP ${result.status}`; @@ -66,44 +58,29 @@ export async function sendNtfy( : `Bearer ${token}`; } - const project = payload.vars.project ?? ""; - const session = payload.vars.session ?? ""; - const task = payload.vars.task ?? ""; - const asciiSession = isAscii(session) ? session : ""; - const asciiTitle = isAscii(payload.title) - ? payload.title - : `[${project || "pi"}] pi ${payload.event}${asciiSession ? ` · ${asciiSession}` : ""}`; - - // NTFY headers must be ASCII-safe under Node's fetch implementation. - headers.Title = asciiTitle; - headers["X-Event"] = payload.event; - if (isAscii(project)) headers["X-Project"] = project; - if (asciiSession) headers["X-Session"] = asciiSession; - if (isAscii(task) && task) headers["X-Task"] = task; - if (project) headers["X-Project-B64"] = base64(project); - if (session) headers["X-Session-B64"] = base64(session); - if (task) headers["X-Task-B64"] = base64(task); - - if (channel.priority != null && channel.priority !== "") { - headers.Priority = String(channel.priority); - } + // Publish as JSON (POST to the server root): title/message travel in a UTF-8 + // body, so Chinese titles work — plain-text publishing would force headers to + // be Latin-1 (fetch ByteString) and we had to mangle non-ASCII titles. const configuredTags = channel.tags == null - ? "" + ? [] : Array.isArray(channel.tags) - ? channel.tags.join(",") - : String(channel.tags); - const tags = [configuredTags, eventTagFor(payload.event)] - .filter(Boolean) - .join(","); - if (tags) headers.Tags = tags; - if (channel.clickUrl) headers.Click = channel.clickUrl; - if (channel.icon) headers.Icon = channel.icon; - if (channel.email) headers.Email = channel.email; + ? channel.tags + : [String(channel.tags)]; + const body = JSON.stringify({ + topic, + title: payload.title, + message: payload.text, + tags: [...configuredTags, eventTagFor(payload.event)], + ...(channel.priority != null && channel.priority !== "" && { priority: channel.priority }), + ...(channel.clickUrl && { click: channel.clickUrl }), + ...(channel.icon && { icon: channel.icon }), + ...(channel.email && { email: channel.email }), + }); const result = await httpRequest( - `${server}/${encodeURIComponent(topic)}`, - { method: "POST", headers, body: payload.text }, + server, + { method: "POST", headers, body }, timeoutMs, ); return addErrorDetail(result);