{"id":1050,"date":"2025-07-13T22:23:45","date_gmt":"2025-07-13T14:23:45","guid":{"rendered":"https:\/\/www.jumoon.top\/?p=1050"},"modified":"2026-01-08T22:58:36","modified_gmt":"2026-01-08T14:58:36","slug":"cf%e5%89%8d%e7%ab%af%e8%87%aa%e5%bb%bantfy%e6%9c%8d%e5%8a%a1%e5%99%a8%e5%ae%9e%e7%8e%b0%e6%89%ab%e7%a0%81%e4%b8%80%e9%94%ae%e6%8c%aa%e8%bd%a6%e5%ae%9e%e6%97%b6%e6%8e%a8%e9%80%81%e6%b6%88%e6%81%af","status":"publish","type":"post","link":"https:\/\/www.jumoon.top\/?p=1050","title":{"rendered":"CF\u524d\u7aef+\u81ea\u5efantfy\u670d\u52a1\u5668\u5b9e\u73b0\u626b\u7801\u4e00\u952e\u632a\u8f66\u5b9e\u65f6\u63a8\u9001\u6d88\u606f+\u7167\u7247"},"content":{"rendered":"\n<p class=\"has-text-align-center\">ntfy\u662f\u4e00\u4e2a\u7b80\u5355\u7684\u57fa\u4e8e HTTP \u7684\u53d1\u5e03-\u8ba2\u9605\u901a\u77e5\u670d\u52a1\u3002\u5b83\u5141\u8bb8\u4f60\u901a\u8fc7\u7b80\u5355\u7684 HTTP \u8bf7\u6c42\u6216\u4f7f\u7528 ntfy \u5ba2\u6237\u7aef\u5411\u5404\u79cd\u8bbe\u5907\u53d1\u9001\u63a8\u9001\u901a\u77e5\u3002<br>\u8f7b\u91cf\u7ea7\uff1a\u8d44\u6e90\u5360\u7528\u5c11\uff0c\u9002\u5408\u5404\u79cd\u73af\u5883<br>\u8de8\u5e73\u53f0\u652f\u6301\uff1a\u652f\u6301 Android\u3001iOS\u3001Linux\u3001macOS \u548c Windows<br>\u591a\u79cd\u901a\u77e5\u65b9\u5f0f\uff1a\u652f\u6301 HTTP\u3001WebSocket\u3001MQTT \u7b49\u534f\u8bae<br><\/p>\n\n\n\n<p class=\"has-text-align-center\">\u5b98\u65b9\u90e8\u7f72\u6587\u6863\uff1a https:\/\/docs.ntfy.sh\/install<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"952\" height=\"924\" src=\"https:\/\/www.jumoon.top\/wp-content\/uploads\/2025\/07\/ntfy3-1.png\" alt=\"\" class=\"wp-image-1072\" srcset=\"https:\/\/www.jumoon.top\/wp-content\/uploads\/2025\/07\/ntfy3-1.png 952w, https:\/\/www.jumoon.top\/wp-content\/uploads\/2025\/07\/ntfy3-1-300x291.png 300w, https:\/\/www.jumoon.top\/wp-content\/uploads\/2025\/07\/ntfy3-1-768x745.png 768w\" sizes=\"auto, (max-width: 952px) 100vw, 952px\" \/><\/figure>\n\n\n\n<figure class=\"wp-block-image aligncenter size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"753\" src=\"https:\/\/www.jumoon.top\/wp-content\/uploads\/2025\/07\/ntfy2-1024x753.jpg\" alt=\"\" class=\"wp-image-1075\" srcset=\"https:\/\/www.jumoon.top\/wp-content\/uploads\/2025\/07\/ntfy2-1024x753.jpg 1024w, https:\/\/www.jumoon.top\/wp-content\/uploads\/2025\/07\/ntfy2-300x221.jpg 300w, https:\/\/www.jumoon.top\/wp-content\/uploads\/2025\/07\/ntfy2-768x565.jpg 768w, https:\/\/www.jumoon.top\/wp-content\/uploads\/2025\/07\/ntfy2-1536x1130.jpg 1536w, https:\/\/www.jumoon.top\/wp-content\/uploads\/2025\/07\/ntfy2-2048x1507.jpg 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<figure class=\"wp-block-image aligncenter size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"833\" height=\"1024\" src=\"https:\/\/www.jumoon.top\/wp-content\/uploads\/2025\/07\/ntfy4-1-833x1024.png\" alt=\"\" class=\"wp-image-1074\" srcset=\"https:\/\/www.jumoon.top\/wp-content\/uploads\/2025\/07\/ntfy4-1-833x1024.png 833w, https:\/\/www.jumoon.top\/wp-content\/uploads\/2025\/07\/ntfy4-1-244x300.png 244w, https:\/\/www.jumoon.top\/wp-content\/uploads\/2025\/07\/ntfy4-1-768x944.png 768w, https:\/\/www.jumoon.top\/wp-content\/uploads\/2025\/07\/ntfy4-1.png 839w\" sizes=\"auto, (max-width: 833px) 100vw, 833px\" \/><\/figure>\n\n\n\n<p><\/p>\n\n\n\n<p class=\"has-text-align-center\">CF\u524d\u7aef\u4ee3\u7801\uff0c\u90e8\u7f72\u65f6\u9700\u8981\u5728\u73af\u5883\u53d8\u91cf\u4e2d\u589e\u52a0\u4ee5\u4e0b\u4e09\u4e2a\u53d8\u91cf<\/p>\n\n\n\n<p class=\"has-text-align-center\">NTFY_SERVER  &#8212;  ntfy\u670d\u52a1\u5668\u5730\u5740\uff0c\u6bd4\u5982https:\/\/a.b.com<br>NTFY_TOPIC  &#8212;  \u63a8\u9001\u4e3b\u9898\uff08\u5c06\u7ec4\u6210\u5b8c\u6574URL\uff09<br>PUSH_INTERVAL  &#8212;  \u63a8\u9001\u95f4\u9694\u9650\u5236\uff08\u5206\u949f\uff0c0\u8868\u793a\u65e0\u9650\u5236\uff09<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>export default {\n    async fetch(request, env, ctx) {\n      const url = new URL(request.url);\n  \n      if (url.pathname === '\/api\/send' &amp;&amp; request.method === 'POST') {\n        try {\n          const pushInterval = parseInt(env.PUSH_INTERVAL || '0') * 60_000;\n          const now = Date.now();\n          const cookies = Object.fromEntries(\n            (request.headers.get('Cookie') || '')\n              .split(';')\n              .filter(Boolean)\n              .map(c => c.trim().split('=')),\n          );\n          const lastPushTime = parseInt(cookies.lastPushTime || '0');\n          if (pushInterval > 0 &amp;&amp; lastPushTime &amp;&amp; now - lastPushTime &lt; pushInterval) {\n            const remain = pushInterval - (now - lastPushTime);\n            const mm = Math.floor(remain \/ 60_000);\n            const ss = Math.floor((remain % 60_000) \/ 1000)\n              .toString()\n              .padStart(2, '0');\n            return json({ errcode: -2, errmsg: `\u64cd\u4f5c\u8fc7\u4e8e\u9891\u7e41\uff0c\u8bf7 ${mm}\u5206${ss}\u79d2 \u540e\u518d\u8bd5` }, 429);\n          }\n  \n          let content = '';\n          let photoFile = null;\n          const ctype = request.headers.get('Content-Type') || '';\n          if (ctype.startsWith('multipart\/form-data')) {\n            const form = await request.formData();\n            content = form.get('content') || '';\n            photoFile = form.get('photo') || null;\n          } else {\n            ({ content = '' } = await request.json());\n          }\n  \n          const result = photoFile\n            ? await sendNtfyWithPhoto(content, photoFile, env)\n            : await sendNtfyMessage(content, env);\n  \n          if (result.errcode === 0) {\n            return json(result, 200, {\n              'Set-Cookie': `lastPushTime=${now}; Max-Age=${Math.floor(pushInterval \/ 1000)}; Path=\/; HttpOnly`,\n            });\n          }\n          return json(result);\n        } catch (e) {\n          return json({ errcode: -1, errmsg: e.message || '\u670d\u52a1\u5668\u9519\u8bef' }, 500);\n        }\n      }\n  \n      return new Response(\n        `&lt;!DOCTYPE html>\n  &lt;html lang=\"zh-CN\">\n  &lt;head>\n    &lt;meta charset=\"UTF-8\">\n    &lt;meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n    &lt;title>\u901a\u77e5\u8f66\u4e3b\u632a\u8f66&lt;\/title>\n    &lt;style>\n      body{font-family:Arial,Helvetica,sans-serif;max-width:600px;margin:0 auto;padding:20px;text-align:center;}\n      h1{font-size:32px;margin-bottom:20px;color:#007bff;}\n      h2{font-size:20px;margin-bottom:20px;color:#333;}\n      .container{margin-top:30px;}\n      input,button{margin:10px 0;padding:8px;box-sizing:border-box;}\n      .form-group{margin-bottom:15px;text-align:left;}\n      label{display:block;margin-bottom:5px;font-weight:bold;}\n      .btn{background:#0080ff;color:#fff;border:none;border-radius:4px;cursor:pointer;}\n      .btn:hover{background:#0066cc;}\n      .btn-send{flex:1;padding:12px 20px;font-size:16px;}\n      .btn-photo{flex:0 0 45px;height:45px;padding:0;font-size:18px;display:flex;align-items:center;justify-content:center;}\n      .btn-row{display:flex;gap:12px;align-items:center;}\n      #preview{max-width:100%;display:none;margin-top:12px;border-radius:4px;}\n      .info-text{font-size:18px;color:#666;margin-top:20px;}\n    &lt;\/style>\n  &lt;\/head>\n  &lt;body>\n    &lt;h1>\u901a\u77e5\u8f66\u4e3b\u632a\u8f66&lt;\/h1>\n    &lt;h2>\u5982\u9700\u901a\u77e5\u8f66\u4e3b\uff0c\u8bf7\u70b9\u51fb\u4e0b\u65b9\u6309\u94ae&lt;\/h2>\n  \n    &lt;div class=\"container\">\n      &lt;div class=\"form-group\">\n        &lt;label for=\"content\">\u7559\u8a00\u8bf7\u8f93\u5165:&lt;\/label>\n        &lt;input id=\"content\" type=\"text\" placeholder=\"\u8bf7\u8f93\u5165\u60a8\u7684\u7559\u8a00\u3001\u8054\u7cfb\u65b9\u5f0f\uff0c\u6216\u76f4\u63a5\u70b9\u51fb \u901a\u77e5\u8f66\u4e3b\" style=\"width:100%;\">\n      &lt;\/div>\n\n      &lt;input id=\"photoInput\" type=\"file\" accept=\"image\/*\" capture=\"environment\" style=\"display:none;\">\n      &lt;div class=\"btn-row\">\n        &lt;button id=\"photoBtn\" class=\"btn btn-photo\" title=\"\u62cd\u7167\/\u9009\u56fe\">\ud83d\udcf7&lt;\/button>\n        &lt;button id=\"sendBtn\" class=\"btn btn-send\">\u901a\u77e5\u8f66\u4e3b&lt;\/button>\n      &lt;\/div>\n  \n      &lt;img id=\"preview\" alt=\"\u9884\u89c8\">\n  \n      &lt;div id=\"result\" style=\"margin-top:20px;\">&lt;\/div>\n      &lt;p class=\"info-text\">\u63a8\u9001\u5230 ntfy&lt;\/p>\n    &lt;\/div>\n  \n  &lt;script>\n    const $ = id => document.getElementById(id);\n    const photoInput = $('photoInput');\n    const photoBtn   = $('photoBtn');\n    const sendBtn    = $('sendBtn');\n    const resultDiv  = $('result');\n    const previewImg = $('preview');\n    const contentInp = $('content');\n  \n    let photoFile = null;\n  \n    photoBtn.addEventListener('click', () => photoInput.click());\n  \n    photoInput.addEventListener('change', async () => {\n      if (!photoInput.files.length) return;\n      try {\n        const compressed = await compressImage(photoInput.files&#91;0]);\n        photoFile = new File(&#91;compressed.blob], 'photo.jpg', { type: 'image\/jpeg' });\n        previewImg.src = compressed.url;\n        previewImg.style.display = 'block';\n        resultDiv.innerHTML = '&lt;p style=\"color:#ff9800;\">\u5df2\u9009\u62e9\u7167\u7247\u5e76\u538b\u7f29 (' +\n          Math.round(photoFile.size\/1024) + ' KB)&lt;\/p>';\n      } catch (e) {\n        photoFile = null;\n        previewImg.style.display = 'none';\n        alert('\u56fe\u7247\u538b\u7f29\u5931\u8d25\uff1a' + e.message);\n      }\n    });\n  \n    async function compressImage(file){\n      const max = 1080, quality = 0.9;\n      const img = await new Promise((res, rej) =>{\n        const i = new Image();\n        i.onload = ()=>res(i);\n        i.onerror= rej;\n        i.src = URL.createObjectURL(file);\n      });\n      let {width:w, height:h} = img;\n      if (w > h &amp;&amp; w > max){ h = Math.round(h*max\/w); w = max; }\n      else if (h >= w &amp;&amp; h > max){ w = Math.round(w*max\/h); h = max; }\n  \n      const canvas = document.createElement('canvas');\n      canvas.width = w; canvas.height = h;\n      canvas.getContext('2d').drawImage(img, 0, 0, w, h);\n  \n      const blob = await new Promise(r=>canvas.toBlob(r, 'image\/jpeg', quality));\n      const url  = URL.createObjectURL(blob);\n      return { blob, url };\n    }\n  \n    sendBtn.addEventListener('click', async () => {\n      const custom = contentInp.value.trim();\n      const full   = '\u60a8\u7684\u8f66\u8f86\u53ef\u80fd\u5360\u9053\u4e86' + (custom ? ' -- ' + custom : '');\n  \n      sendBtn.disabled = true;\n      resultDiv.innerHTML = '&lt;p>\u53d1\u9001\u4e2d...&lt;\/p>';\n  \n      try {\n        const body = new FormData();\n        body.append('content', full);\n        if (photoFile) body.append('photo', photoFile);\n  \n        const rsp = await fetch('\/api\/send', { method:'POST', body, credentials:'include' });\n        const res = await rsp.json();\n  \n        if (res.errcode === 0){\n          resultDiv.innerHTML = '&lt;p style=\"color:green;\">\u6d88\u606f\u63a8\u9001\u6210\u529f\uff01&lt;\/p>';\n          photoFile = null; previewImg.style.display = 'none'; photoInput.value='';\n        }else{\n          resultDiv.innerHTML = '&lt;p style=\"color:red;\">' + (res.errmsg||'\u63a8\u9001\u5931\u8d25') + '&lt;\/p>';\n        }\n      } catch(e){\n        resultDiv.innerHTML = '&lt;p style=\"color:red;\">\u8bf7\u6c42\u51fa\u9519: ' + e.message + '&lt;\/p>';\n      } finally{\n        sendBtn.disabled = false;\n      }\n    });\n  &lt;\/script>\n  &lt;\/body>\n  &lt;\/html>`,\n        { headers: { 'Content-Type': 'text\/html;charset=utf-8' } },\n      );\n    },\n  };\n  \n  function json(obj, status = 200, hdr = {}) {\n    return new Response(JSON.stringify(obj), {\n      status,\n      headers: { 'Content-Type': 'application\/json', ...hdr }\n    });\n  }\n  \n  function ts(txt) {\n    const d = new Date(Date.now() + 8 * 3600_000)\n      .toISOString()\n      .replace('T', ' ')\n      .substring(0, 19);\n    return `${txt} &#91;${d}]`;\n  }\n  \n  async function sendNtfyMessage(content, env) {\n    const url = `${env.NTFY_SERVER}\/${env.NTFY_TOPIC}`;\n    const r = await fetch(url, {\n      method: 'POST',\n      headers: { Title: '\u632a\u8f66\u901a\u77e5', Priority: 'high', Tags: 'car' },\n      body: ts(content)\n    });\n    if (!r.ok) throw new Error('ntfy \u8fd4\u56de ' + r.status);\n    return { errcode: 0, errmsg: '\u6587\u5b57\u63a8\u9001\u6210\u529f' };\n  }\n  \n  async function sendNtfyWithPhoto(content, file, env) {\n    const url = `${env.NTFY_SERVER}\/${env.NTFY_TOPIC}`;\n    const r = await fetch(url, {\n      method: 'PUT',\n      headers: {\n        Filename: file.name || 'photo.jpg',\n        Title: ts(content),\n        Priority: 'high',\n        Tags: 'car'\n      },\n      body: file\n    });\n    if (!r.ok) throw new Error('ntfy \u4e0a\u4f20\u9644\u4ef6\u5931\u8d25 ' + r.status);\n    return { errcode: 0, errmsg: '\u6587\u5b57+\u7167\u7247\u63a8\u9001\u6210\u529f' };\n  }\n  <\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>ntfy\u662f\u4e00\u4e2a\u7b80\u5355\u7684\u57fa\u4e8e HTTP \u7684\u53d1\u5e03-\u8ba2\u9605\u901a\u77e5\u670d\u52a1\u3002\u5b83\u5141\u8bb8\u4f60\u901a\u8fc7\u7b80\u5355\u7684 HTTP \u8bf7\u6c42\u6216\u4f7f\u7528 ntfy  [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"site-container-style":"default","site-container-layout":"default","site-sidebar-layout":"default","site-transparent-header":"default","disable-article-header":"default","disable-site-header":"default","disable-site-footer":"default","disable-content-area-spacing":"default","footnotes":""},"categories":[19],"tags":[],"class_list":["post-1050","post","type-post","status-publish","format-standard","hentry","category-19"],"_links":{"self":[{"href":"https:\/\/www.jumoon.top\/index.php?rest_route=\/wp\/v2\/posts\/1050","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.jumoon.top\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.jumoon.top\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.jumoon.top\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.jumoon.top\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=1050"}],"version-history":[{"count":14,"href":"https:\/\/www.jumoon.top\/index.php?rest_route=\/wp\/v2\/posts\/1050\/revisions"}],"predecessor-version":[{"id":1079,"href":"https:\/\/www.jumoon.top\/index.php?rest_route=\/wp\/v2\/posts\/1050\/revisions\/1079"}],"wp:attachment":[{"href":"https:\/\/www.jumoon.top\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1050"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.jumoon.top\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1050"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.jumoon.top\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1050"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}