• פוסט מסודר לנותני שירות בניית מערכות בתשלום

    נעוץ
    95
    11 הצבעות
    95 פוסטים
    7k צפיות
    מ
    @אברהם-מנס כתב: שלום לכולם! לאחרונה קיבלתי כמה פניות לבניית מערכות (אפילו מערכות קלות), לא היה לי זמן לענות לכולם. הפניתי אותם לחפש מישהו אחר, הרבה חזרו אליי עם תשובה שלא הצליחו למצוא מישהו. ראיתי שאין פוסט מסודר בפורום שבו כל נותני השירות לבניית מערכות בתשלום יכולים להציג את עצמם. כל מי שנותן שירותי בניית מערכות בתשלום שיכניס כאן את פרטיו. נא לציין בכמה מילים את התחומים בהם אתם מתמחים. מי שמפסיק לתת שירות, אנא מחקו את ההודעה שלכם שהפוסט יישאר מעודכן. אשמח שכל אחד שרואה את עצמו מתאים לזה שיכניס כאן את הפרטים שלו. זה יעזור גם לכם, וגם למחפשים שירות כזה! תודה רבה! חדש חדש! כדי להקל על החיפוש, יש רשימה מסודרת בגוגל שיטס עם פרטי נותני השירות. לרשימת בוני מערכות הקש כאן להוספת פרטיך לרשימה הקש כאן תודה ל @א-תורת-מרן על היוזמה! הוספתי בקישור. תודה על היוזמה.
  • 16 הצבעות
    378 פוסטים
    12k צפיות
    ה
    @יצחק-י. קוד שרת /** * Cloudflare Worker - ניתוב לפי מקש שנלחץ (send_api ממודול Playfile של ימות המשיח) * ------------------------------------------------------------------------------------ * הפתרון כאן: כל כתובות היעד מוגדרות ישירות ב-ext.ini של ימות באמצעות api_add, * ונשלחות אוטומטית עם כל בקשה (יחד עם PressKey - המקש שנלחץ). ה-Worker רק * קורא את הפרמטר target_<PressKey> מתוך הבקשה עצמה ומעביר אליו את הקריאה. * אין צורך בקובץ ini נפרד, DownloadFile, או עדכון קוד כשמוסיפים/משנים מקש. * * הגדרות בקובץ ext.ini של השלוחה (playfile): * type=playfile * control_play1=send_api * control_play2=send_api * control_play_moreA2=send_api ; מקש *2 (תפריט אפשרויות נוספות) * api_link=https://your-worker-subdomain.workers.dev * api_url_post=yes * api_add_0=target_1=https://server-a.example.com/webhook * api_add_1=target_2=https://server-b.example.com/webhook * api_add_2=target_star2=https://server-c.example.com/webhook * api_add_3=note_1=פנייה למכירות * api_add_4=note_2=פנייה לתמיכה * * כדי להוסיף מקש 4: מוסיפים control_play4=send_api ועוד שורת api_add * בפורמט api_add_N=target_4=https://... - וזהו, בלי לגעת בקוד ה-Worker. * * מקשים דרך "תפריט אפשרויות נוספות" (אחרי הקשה על *, למשל *2): ימות שולח * במקרה כזה PressKey בפורמט "מקש-ראשון-מקש-שני" (למשל "*-2"). כדי שזה יעבוד * בנוחות עם שמות פרמטרים תקינים, ה-Worker ממיר אוטומטית "*" למילה "star", * "#" למילה "hash", ומוריד מקפים - כך ש-PressKey="*-2" הופך ל"star2", ולכן * בהגדרות כותבים target_star2 / note_star2 וכו'. * * כל פרמטר עם סיומת _<מקש מנורמל> (למשל note_1, dept_2, note_star2) ייחשב * "שייך" לאותו מקש בלבד. ה-Worker מסנן ומעביר ליעד רק את הפרמטרים ששייכים * למקש שבאמת נלחץ (עם הסיומת מוסרת), ומשמיט פרמטרים ששייכים למקשים אחרים. * פרמטרים בלי סיומת כזו (כמו PressKey, ApiExtension, ApiPhone) מועברים כרגיל. */ // תשובת ברירת מחדל לימות אם המקש לא נמצא בפרמטרים (בפורמט הפקודות של ימות) const FALLBACK_RESPONSE = "go_to_folder="; // הופך PressKey כפי שנשלח מימות (כולל אפשרויות כמו "*-2") לסיומת שם פרמטר // בטוחה: "*" -> "star", "#" -> "hash", מקפים מוסרים. לדוגמה: "*-2" -> "star2" function sanitizeKeySuffix(rawKey) { return String(rawKey) .trim() .replace(/\*/g, "star") .replace(/#/g, "hash") .replace(/-/g, ""); } // בונה את אובייקט הפרמטרים שיישלח ליעד: פרמטרים עם סיומת _<מקש מנורמל> נשארים // רק אם הסיומת תואמת למקש שנלחץ (והסיומת מוסרת), פרמטרים ששייכים למקש אחר // מושמטים, ופרמטרים בלי סיומת כזו מועברים כמו שהם. function buildForwardParams(params, keySuffix) { const forwarded = {}; for (const [key, value] of Object.entries(params)) { const match = key.match(/^(.*)_(\d+|star\d*|hash\d*)$/); if (match) { const [, baseName, keyNum] = match; if (keyNum === keySuffix) { forwarded[baseName] = value; } // אחרת - שייך למקש אחר, מושמט בכוונה } else { forwarded[key] = value; } } return forwarded; } export default { async fetch(request, env) { const url = new URL(request.url); let params; if (request.method === "POST") { const contentType = request.headers.get("content-type") || ""; if (contentType.includes("application/json")) { params = await request.json().catch(() => ({})); } else { const text = await request.text(); params = Object.fromEntries(new URLSearchParams(text)); } } else { params = Object.fromEntries(url.searchParams); } // ימות שולח את המקש שנלחץ בפרמטר PressKey (יכול להיות "2" רגיל, או // "*-2" אם זה מקש דרך תפריט אפשרויות נוספות) - מנרמלים לסיומת בטוחה const rawPressKey = params.PressKey ?? params.pressKey ?? ""; const pressKey = sanitizeKeySuffix(rawPressKey); const targetUrl = params[`target_${pressKey}`]; if (!targetUrl) { return new Response(FALLBACK_RESPONSE, { status: 200, headers: { "content-type": "text/plain; charset=utf-8" }, }); } try { // יעד רגיל: https://... | יעד Worker אחר באותו חשבון: binding:שם_הבינדינג[/נתיב] // (Cloudflare חוסמת קריאה ישירה בין Workers של אותו חשבון דרך workers.dev - שגיאה 1042) let doFetch = fetch; let rawTarget = targetUrl; if (targetUrl.startsWith("binding:")) { const rest = targetUrl.slice("binding:".length); const slashIdx = rest.indexOf("/"); const bindingName = slashIdx === -1 ? rest : rest.slice(0, slashIdx); const path = slashIdx === -1 ? "/" : rest.slice(slashIdx); const binding = env && env[bindingName]; if (!binding) throw new Error("missing service binding: " + bindingName); doFetch = (u, init) => binding.fetch(u, init); rawTarget = "https://internal" + path; } const forwardUrl = new URL(rawTarget); const forwardParams = buildForwardParams(params, pressKey); let forwardResponse; if (request.method === "POST") { forwardResponse = await doFetch(forwardUrl.toString(), { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(forwardParams), }); } else { for (const [key, value] of Object.entries(forwardParams)) { forwardUrl.searchParams.set(key, String(value)); } forwardResponse = await doFetch(forwardUrl.toString(), { method: "GET" }); } const bodyText = await forwardResponse.text(); return new Response(bodyText, { status: 200, headers: { "content-type": "text/plain; charset=utf-8" }, }); } catch (err) { return new Response(FALLBACK_RESPONSE, { status: 200, headers: { "content-type": "text/plain; charset=utf-8" }, }); } }, }; הפתרון הנכון הוא Service Binding, חיבור פנימי בין עובדים: בראוטר נכנסים ל-Settings, אחר כך כריכות, ואז הוסף, וקביעת שירות. נותנים שם לשינוי, למשל SURVEY, ובוחרים את ה-Worker היעד. ב- ext.iniכותבים את היעד כך במקום https: api_add_0=target_1=binding:SURVEY/ api_add_1=target_2=https://script.google.com/macros/s/.../exec אחרי binding:SURVEYאפשר להוסיף נתיב, למשל binding:SURVEY/api. הוספתי את התמיכה הזו לקובץ yemot-key-router-worker.js. יעד שמתחיל ב- binding:עובר דרך ה-Service Binding, ויעד עם https://עובד כמו קודם, כך אפשר לערבב בין. לא הרצתי אותו מול Cloudflare, רק בדקתי שהתחביר תקין. אשף להגדרות השלוחה <!DOCTYPE html> <html lang="he" dir="rtl"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <title>אשף הגדרות ניתוב מקשים - ימות המשיח</title> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Heebo:wght@400;500;700;800&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet"> <style> :root { --paper: #FAF7F1; --panel: #FFFFFF; --ink: #201F1D; --ink-soft: #5B5750; --border: #E4DFD4; --accent: #1F7A6C; --accent-ink: #0E3F37; --accent-soft: #E4F1EE; --warn: #B3672B; --warn-soft: #FBEEE0; --danger: #B3413B; --danger-soft: #FBEAE8; --terminal-bg: #17211D; --terminal-ink: #DDEEE7; --terminal-accent: #7FD6BE; --radius: 10px; --shadow: 0 1px 2px rgba(32,31,29,0.06), 0 6px 16px rgba(32,31,29,0.06); } @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --paper: #14120F; --panel: #1D1A16; --ink: #F1EDE5; --ink-soft: #B7AF9F; --border: #35302A; --accent: #4FBFA8; --accent-ink: #C9F1E5; --accent-soft: #1E2E2A; --warn: #E0A868; --warn-soft: #2E2418; --danger: #E08A83; --danger-soft: #2E1B19; --terminal-bg: #0E1613; --terminal-ink: #DDEEE7; --terminal-accent: #7FD6BE; --shadow: 0 1px 2px rgba(0,0,0,0.3), 0 6px 20px rgba(0,0,0,0.35); } } :root[data-theme="dark"] { --paper: #14120F; --panel: #1D1A16; --ink: #F1EDE5; --ink-soft: #B7AF9F; --border: #35302A; --accent: #4FBFA8; --accent-ink: #C9F1E5; --accent-soft: #1E2E2A; --warn: #E0A868; --warn-soft: #2E2418; --danger: #E08A83; --danger-soft: #2E1B19; --terminal-bg: #0E1613; --terminal-ink: #DDEEE7; --terminal-accent: #7FD6BE; --shadow: 0 1px 2px rgba(0,0,0,0.3), 0 6px 20px rgba(0,0,0,0.35); } * { box-sizing: border-box; } html { scroll-padding-top: env(safe-area-inset-top, 0px); } body { margin: 0; background: var(--paper); color: var(--ink); font-family: 'Heebo', system-ui, sans-serif; padding-top: env(safe-area-inset-top, 0px); padding-bottom: env(safe-area-inset-bottom, 0px); line-height: 1.5; } .wrap { max-width: 1080px; margin: 0 auto; padding: 28px 20px 80px; } header.page-head { margin-bottom: 28px; } header.page-head .eyebrow { display: inline-flex; align-items: center; gap: 8px; font-size: 13px; color: var(--accent-ink); background: var(--accent-soft); border: 1px solid var(--accent); border-radius: 999px; padding: 4px 12px; margin-bottom: 14px; } header.page-head h1 { font-size: clamp(24px, 4vw, 34px); font-weight: 800; margin: 0 0 8px; letter-spacing: -0.01em; } header.page-head p { color: var(--ink-soft); max-width: 62ch; margin: 0; font-size: 15px; } .grid { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); gap: 20px; align-items: start; } @media (max-width: 880px) { .grid { grid-template-columns: 1fr; } } .card { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow); padding: 20px; } .card + .card { margin-top: 18px; } .card h2 { font-size: 16px; font-weight: 700; margin: 0 0 4px; } .card .sub { color: var(--ink-soft); font-size: 13px; margin: 0 0 16px; } label { display: block; font-size: 13px; font-weight: 500; color: var(--ink-soft); margin-bottom: 6px; } input[type="text"], input[type="url"] { width: 100%; padding: 9px 11px; border: 1px solid var(--border); border-radius: 8px; background: var(--paper); color: var(--ink); font-family: inherit; font-size: 14px; } input[type="text"]:focus, input[type="url"]:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); } .field { margin-bottom: 14px; } .field-row { display: flex; gap: 10px; } .field-row .field { flex: 1; margin-bottom: 0; } .toggle-row { display: flex; align-items: center; gap: 10px; font-size: 14px; color: var(--ink); margin-top: 4px; } .toggle-row input { width: 16px; height: 16px; } /* key rows - styled like small keypad tiles */ .key-row { border: 1px solid var(--border); border-radius: 10px; padding: 14px; margin-bottom: 14px; background: var(--paper); position: relative; } .key-row-top { display: grid; grid-template-columns: 84px 1fr auto; gap: 10px; align-items: end; } @media (max-width: 520px) { .key-row-top { grid-template-columns: 1fr; } } .key-cell input { text-align: center; font-family: 'JetBrains Mono', monospace; font-weight: 700; font-size: 16px; color: var(--accent-ink); background: var(--accent-soft); border-color: var(--accent); } .key-hint { font-size: 11px; color: var(--ink-soft); margin-top: 4px; } .row-remove { align-self: end; } .extra-params { margin-top: 12px; } .extra-params-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; } .extra-params-head span { font-size: 12px; font-weight: 600; color: var(--ink-soft); } .extra-param-row { display: grid; grid-template-columns: 1fr 1.4fr auto; gap: 8px; margin-bottom: 8px; } .extra-param-row input { font-size: 13px; padding: 7px 9px; } button { font-family: inherit; cursor: pointer; border-radius: 8px; border: 1px solid transparent; font-size: 13px; font-weight: 600; padding: 8px 14px; transition: transform .05s ease, background .15s ease, border-color .15s ease; } button:active { transform: translateY(1px); } .btn-primary { background: var(--accent); color: #ffffff; border-color: var(--accent); } .btn-primary:hover { filter: brightness(1.05); } .btn-ghost { background: transparent; color: var(--ink); border-color: var(--border); } .btn-ghost:hover { border-color: var(--accent); color: var(--accent-ink); } .btn-danger-ghost { background: transparent; color: var(--danger); border-color: var(--danger); padding: 6px 10px; font-size: 12px; } .btn-danger-ghost:hover { background: var(--danger-soft); } .btn-small { padding: 5px 10px; font-size: 12px; } .add-key-btn { width: 100%; padding: 12px; border: 1.5px dashed var(--border); background: transparent; color: var(--ink-soft); font-weight: 600; } .add-key-btn:hover { border-color: var(--accent); color: var(--accent-ink); background: var(--accent-soft); } .warn-box, .danger-box { border-radius: 8px; padding: 10px 12px; font-size: 13px; margin-bottom: 14px; } .warn-box { background: var(--warn-soft); color: var(--warn); border: 1px solid var(--warn); } .danger-box { background: var(--danger-soft); color: var(--danger); border: 1px solid var(--danger); } /* output / terminal panel */ .output-card { position: sticky; top: 16px; } @media (max-width: 880px) { .output-card { position: static; } } .terminal { background: var(--terminal-bg); color: var(--terminal-ink); border-radius: 8px; padding: 16px; font-family: 'JetBrains Mono', monospace; font-size: 12.5px; line-height: 1.65; white-space: pre-wrap; word-break: break-all; direction: ltr; text-align: left; max-height: 60vh; overflow: auto; } .terminal .comment { color: #7C9187; } .terminal .key { color: var(--terminal-accent); } .terminal .val { color: var(--terminal-ink); } .output-actions { display: flex; gap: 8px; margin-top: 12px; } .toast { position: fixed; bottom: calc(20px + env(safe-area-inset-bottom, 0px)); left: 50%; transform: translateX(-50%) translateY(20px); background: var(--ink); color: var(--paper); padding: 10px 18px; border-radius: 999px; font-size: 13px; font-weight: 600; opacity: 0; pointer-events: none; transition: opacity .2s ease, transform .2s ease; z-index: 50; } .toast.show { opacity: 1; transform: translateX(-50%) translateY(0); } .footnote { font-size: 12px; color: var(--ink-soft); margin-top: 10px; } </style> </head> <body> <div class="wrap"> <header class="page-head"> <span class="eyebrow">☎️ מחולל הגדרות למודול Playfile</span> <h1>אשף ניתוב מקשים ל-API בימות המשיח</h1> <p> הוסיפו מקשים, לכל מקש כתובת Worker יעודית ופרמטרים נוספים - והאשף יבנה עבורכם את שורות ה-<code>ext.ini</code> המוכנות להדבקה, כולל <code>control_play</code> ו-<code>api_add</code>. </p> </header> <div class="grid"> <div> <div class="card"> <h2>הגדרות כלליות</h2> <p class="sub">כתובת ה-Worker אחת בלבד, זהה לכל המקשים - הניתוב עצמו קורה בתוך ה-Worker.</p> <div class="field"> <label for="workerUrl">כתובת ה-Worker (api_link)</label> <input type="url" id="workerUrl" placeholder="https://your-worker-subdomain.workers.dev" autocomplete="off"> </div> <div class="toggle-row"> <input type="checkbox" id="postToggle" checked> <label for="postToggle" style="margin:0;font-weight:400;color:var(--ink);">שליחה כ-POST (api_url_post=yes)</label> </div> </div> <div class="card"> <h2>מקשים וכתובות יעד</h2> <p class="sub"> מקש רגיל: תו אחד - <code>1</code>-<code>9</code>, <code>0</code>, <code>*</code> או <code>#</code>. מקש דרך "אפשרויות נוספות": שני תווים, למשל <code>*2</code> (כוכבית ואז ספרה). </p> <div id="warnings"></div> <div id="keyRows"></div> <button class="add-key-btn" id="addKeyBtn" type="button">+ הוספת מקש</button> </div> </div> <div class="output-card"> <div class="card"> <h2>תוצאה - להדביק ב-ext.ini</h2> <p class="sub">מתעדכן אוטומטית עם כל שינוי.</p> <div class="terminal" id="output" aria-live="polite"></div> <div class="output-actions"> <button class="btn-primary" id="copyBtn" type="button">העתקת ההגדרות</button> <button class="btn-ghost" id="resetBtn" type="button">איפוס הכל</button> </div> <p class="footnote">השמירה נשמרת אוטומטית בדפדפן הזה בלבד, כדי שלא תאבדו נתונים ברענון.</p> </div> </div> </div> </div> <div class="toast" id="toast">ההגדרות הועתקו</div> <script> (function () { "use strict"; var STORAGE_KEY = "yemotKeyRouterWizardState"; var state = { workerUrl: "", postMode: true, keys: [ { key: "1", target: "", params: [] }, { key: "2", target: "", params: [] } ] }; function loadState() { try { var raw = localStorage.getItem(STORAGE_KEY); if (raw) { var parsed = JSON.parse(raw); if (parsed && Array.isArray(parsed.keys) && parsed.keys.length) { state = parsed; } } } catch (e) { /* קאש לא זמין - ממשיכים עם ברירת מחדל */ } } function saveState() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (e) { /* לא קריטי */ } } function sanitizeKeySuffix(rawKey) { return String(rawKey || "") .trim() .replace(/\*/g, "star") .replace(/#/g, "hash") .replace(/-/g, ""); } function keyInfo(rawKey) { var key = String(rawKey || "").trim(); if (!key) return { valid: false, reason: "מקש ריק" }; if (key.length === 1) { var okSingle = /^[0-9*#]$/.test(key); if (!okSingle) return { valid: false, reason: "מקש לא מוכר: " + key }; var suffix = key === "*" ? "star" : key === "#" ? "hash" : key; return { valid: true, controlLine: "control_play" + key + "=send_api", suffix: suffix, label: key }; } if (key.length === 2) { var first = key[0], second = key[1]; var firstOk = first === "*" || first === "#"; var secondOk = /^[0-9]$/.test(second); if (!firstOk || !secondOk) { return { valid: false, reason: "מקש דו-תווי חייב להיות * או # ואחריו ספרה, למשל *2" }; } var firstWord = first === "*" ? "star" : "hash"; return { valid: true, controlLine: "control_play_moreA" + second + "=send_api", suffix: firstWord + second, label: key, nonDefault: first === "#" }; } return { valid: false, reason: "מקש ארוך מדי (עד 2 תווים)" }; } function el(tag, attrs, children) { var node = document.createElement(tag); attrs = attrs || {}; Object.keys(attrs).forEach(function (k) { if (k === "class") node.className = attrs[k]; else if (k === "text") node.textContent = attrs[k]; else node.setAttribute(k, attrs[k]); }); (children || []).forEach(function (c) { node.appendChild(c); }); return node; } function renderKeyRows() { var container = document.getElementById("keyRows"); container.innerHTML = ""; state.keys.forEach(function (row, idx) { var wrap = el("div", { class: "key-row" }); var top = el("div", { class: "key-row-top" }); var keyCell = el("div", { class: "field key-cell" }); keyCell.appendChild(el("label", { text: "מקש" })); var keyInput = el("input", { type: "text", maxlength: "2", value: row.key }); keyInput.addEventListener("input", function (e) { row.key = e.target.value; syncOutputs(); }); keyCell.appendChild(keyInput); keyCell.appendChild(el("div", { class: "key-hint", text: "לדוגמה: 1 או *2" })); top.appendChild(keyCell); var targetCell = el("div", { class: "field" }); targetCell.appendChild(el("label", { text: "כתובת יעד עבור המקש הזה" })); var targetInput = el("input", { type: "url", placeholder: "https://server.example.com/webhook", value: row.target }); targetInput.addEventListener("input", function (e) { row.target = e.target.value; syncOutputs(); }); targetCell.appendChild(targetInput); top.appendChild(targetCell); var removeCell = el("div", { class: "field row-remove" }); removeCell.appendChild(el("label", { text: "\u00A0" })); var removeBtn = el("button", { class: "btn-danger-ghost", type: "button", text: "הסרת מקש" }); removeBtn.addEventListener("click", function () { state.keys.splice(idx, 1); update(); }); removeCell.appendChild(removeBtn); top.appendChild(removeCell); wrap.appendChild(top); // extra params var extra = el("div", { class: "extra-params" }); var extraHead = el("div", { class: "extra-params-head" }, [ el("span", { text: "פרמטרים נוספים למקש זה (לא חובה)" }) ]); var addParamBtn = el("button", { class: "btn-ghost btn-small", type: "button", text: "+ הוספת פרמטר" }); addParamBtn.addEventListener("click", function () { row.params.push({ name: "", value: "" }); update(); }); extraHead.appendChild(addParamBtn); extra.appendChild(extraHead); row.params.forEach(function (p, pIdx) { var pRow = el("div", { class: "extra-param-row" }); var nameInput = el("input", { type: "text", placeholder: "שם הפרמטר, למשל note", value: p.name }); nameInput.addEventListener("input", function (e) { p.name = e.target.value; syncOutputs(); }); var valueInput = el("input", { type: "text", placeholder: "ערך, למשל פנייה למכירות", value: p.value }); valueInput.addEventListener("input", function (e) { p.value = e.target.value; syncOutputs(); }); var delBtn = el("button", { class: "btn-danger-ghost btn-small", type: "button", text: "הסרה" }); delBtn.addEventListener("click", function () { row.params.splice(pIdx, 1); update(); }); pRow.appendChild(nameInput); pRow.appendChild(valueInput); pRow.appendChild(delBtn); extra.appendChild(pRow); }); wrap.appendChild(extra); container.appendChild(wrap); }); } function renderWarnings() { var box = document.getElementById("warnings"); box.innerHTML = ""; var problems = []; var suffixSeen = {}; state.keys.forEach(function (row) { var info = keyInfo(row.key); if (!info.valid && row.key.trim()) { problems.push({ type: "danger", text: "מקש \"" + row.key + "\": " + info.reason }); return; } if (info.valid) { if (suffixSeen[info.suffix]) { problems.push({ type: "danger", text: "המקשים \"" + suffixSeen[info.suffix] + "\" ו-\"" + row.key + "\" מתנגשים (אותה הגדרה בפועל)" }); } suffixSeen[info.suffix] = row.key; if (info.nonDefault) { problems.push({ type: "warn", text: "מקש \"" + row.key + "\": כניסה דרך # לתפריט אפשרויות נוספות אינה ברירת המחדל בימות - יש לוודא שהוגדרה כך בשלוחה" }); } } if (info.valid && !row.target.trim()) { problems.push({ type: "warn", text: "למקש \"" + row.key + "\" אין כתובת יעד עדיין" }); } }); if (!state.workerUrl.trim()) { problems.push({ type: "warn", text: "לא הוזנה כתובת ה-Worker (api_link)" }); } problems.forEach(function (p) { box.appendChild(el("div", { class: p.type === "danger" ? "danger-box" : "warn-box", text: p.text })); }); } function escapeForIni(value) { return String(value).replace(/[\r\n]/g, " ").trim(); } function buildOutput() { var lines = []; lines.push({ c: "comment", t: "; --- הגדרות שלוחה מסוג playfile ---" }); lines.push({ c: "comment", t: "type=playfile" }); if (state.workerUrl.trim()) { lines.push({ c: "plain", t: "api_link=" + escapeForIni(state.workerUrl) }); } else { lines.push({ c: "comment", t: "; api_link=<יש להזין כתובת Worker למעלה>" }); } lines.push({ c: "plain", t: "api_url_post=" + (state.postMode ? "yes" : "no") }); lines.push({ c: "comment", t: "" }); lines.push({ c: "comment", t: "; --- מקשים ---" }); var controlLines = []; var addLines = []; var counter = 0; state.keys.forEach(function (row) { var info = keyInfo(row.key); if (!info.valid) return; controlLines.push(info.controlLine); if (row.target.trim()) { addLines.push("api_add_" + counter + "=target_" + info.suffix + "=" + escapeForIni(row.target)); counter++; } row.params.forEach(function (p) { if (p.name.trim() && p.value.trim()) { var safeName = p.name.trim().replace(/[^A-Za-z0-9_\u05D0-\u05EA]/g, ""); if (safeName) { addLines.push("api_add_" + counter + "=" + safeName + "_" + info.suffix + "=" + escapeForIni(p.value)); counter++; } } }); }); controlLines.forEach(function (l) { lines.push({ c: "plain", t: l }); }); lines.push({ c: "comment", t: "" }); lines.push({ c: "comment", t: "; --- כתובות יעד ופרמטרים נוספים לכל מקש ---" }); addLines.forEach(function (l) { lines.push({ c: "plain", t: l }); }); return lines; } function renderOutput() { var out = document.getElementById("output"); var lines = buildOutput(); out.innerHTML = ""; lines.forEach(function (line) { var div = document.createElement("div"); if (line.c === "comment") { div.className = "comment"; div.textContent = line.t || "\u00A0"; } else { var eqIdx = line.t.indexOf("="); if (eqIdx > -1) { var keySpan = document.createElement("span"); keySpan.className = "key"; keySpan.textContent = line.t.slice(0, eqIdx + 1); var valSpan = document.createElement("span"); valSpan.className = "val"; valSpan.textContent = line.t.slice(eqIdx + 1); div.appendChild(keySpan); div.appendChild(valSpan); } else { div.textContent = line.t; } } out.appendChild(div); }); } function getOutputText() { return buildOutput().map(function (l) { return l.t; }).join("\n"); } // עדכון מלא: כשמבנה השורות משתנה (הוספה/הסרה של מקש או פרמטר) function update() { renderKeyRows(); renderWarnings(); renderOutput(); saveState(); } // עדכון קל: כשרק תוכן שדה משתנה (הקלדה) - לא נוגעים ב-DOM של השורות, // כדי לא לאבד פוקוס מהשדה שממנו מקלידים function syncOutputs() { renderWarnings(); renderOutput(); saveState(); } function showToast(msg) { var toast = document.getElementById("toast"); toast.textContent = msg; toast.classList.add("show"); clearTimeout(showToast._t); showToast._t = setTimeout(function () { toast.classList.remove("show"); }, 1800); } document.getElementById("workerUrl").addEventListener("input", function (e) { state.workerUrl = e.target.value; syncOutputs(); }); document.getElementById("postToggle").addEventListener("change", function (e) { state.postMode = e.target.checked; update(); }); document.getElementById("addKeyBtn").addEventListener("click", function () { state.keys.push({ key: "", target: "", params: [] }); update(); }); document.getElementById("resetBtn").addEventListener("click", function () { if (!confirm("לאפס את כל ההגדרות?")) return; state = { workerUrl: "", postMode: true, keys: [{ key: "1", target: "", params: [] }] }; update(); }); document.getElementById("copyBtn").addEventListener("click", function () { var text = getOutputText(); if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(function () { showToast("ההגדרות הועתקו"); }).catch(function () { fallbackCopy(text); }); } else { fallbackCopy(text); } }); function fallbackCopy(text) { var ta = document.createElement("textarea"); ta.value = text; ta.style.position = "fixed"; ta.style.opacity = "0"; document.body.appendChild(ta); ta.select(); try { document.execCommand("copy"); showToast("ההגדרות הועתקו"); } catch (e) { showToast("לא ניתן להעתיק אוטומטית - יש לסמן ולהעתיק ידנית"); } document.body.removeChild(ta); } loadState(); document.getElementById("workerUrl").value = state.workerUrl; document.getElementById("postToggle").checked = state.postMode; update(); })(); </script> </body> </html> הכריכה בדף הזה Spoiler [image: 1791555879543-13a7e0c8-0c28-486d-9e3f-0c15addc590e-image.jpeg]
  • הגבלת מקש בהודעה של מאזין מסוים

    3
    1 הצבעות
    3 פוסטים
    28 צפיות
    א
    @פיתה אפשרי באמצעות API
  • דרמה!! מישהו שמע על זה????

    152
    1 הצבעות
    152 פוסטים
    2k צפיות
    א
    @מסובך-מאוד כתב: הוא לא יכול להיות כמו מספר רגיל? לא. הוא פשוט נמצא במערכת אחרת שם מוגדר שהוא יפנה למערכת שלך.
  • 0 הצבעות
    26 פוסטים
    68 צפיות
    L
    @יענקי-פולק בבדיקה
  • הקלטת הודעה שתשלח צינתוק למספר טלפון מסויים

    נפתר
    8
    0 הצבעות
    8 פוסטים
    36 צפיות
    י
    @מערכות-טלפוניות סמן כנפתר
  • שמירת הקלטות שידור חי בשלוחה נפרדת לא עובד?

    4
    0 הצבעות
    4 פוסטים
    41 צפיות
    מ
    @lavitoren-הטי-ל לא נכון, ראה כאן
  • משמיעה כמעט בכל שלוחה שלום ל...

    39
    0 הצבעות
    39 פוסטים
    194 צפיות
    פ
    @אA נטו נגמר לי הלייקים
  • מה זה תיבה אישית?

    6
    0 הצבעות
    6 פוסטים
    62 צפיות
    א
    @מסובך-מאוד תראה כאן מיד מתחת לטבלה https://f2.freeivr.co.il/topic/7010/השמעת-קבצים/10?_=1791479026362#ID
  • הסבר על תיבה אישית

    לא נפתר
    14
    0 הצבעות
    14 פוסטים
    39 צפיות
    מ
    @lavitoren-הטי-ל תיקנתי
  • איך שמים מנגינת המתנה אישית בחדר ועידה?

    נפתר
    12
    0 הצבעות
    12 פוסטים
    68 צפיות
    מ
    @lavitoren-הטי-ל שכויייח עבד מעולה (אחד שמדבר לעניין) ממש יהונייייייסען
  • קו רקע קול לשיתוף מנגינות לקווי תוכן

    5
    3 הצבעות
    5 פוסטים
    71 צפיות
    מ
    @lavitoren-הטי-ל שיהיה כאן
  • האם ניתן לשחזר שידור חי

    לא נפתר
    10
    0 הצבעות
    10 פוסטים
    47 צפיות
    מ
    @lavitoren-הטי-ל מה אני עושה רציתי לשלוח את החשיפות לינקעלה
  • איך עושים שיהיה ניתן לרשום שם כמה פעמים?

    4
    0 הצבעות
    4 פוסטים
    46 צפיות
    נ
    @פיתה add_id_to_list_location_list=/מספר השלוחה בו הגדרת שממנו יטען השמות/EnterIDValName add_id_to_list_value_change=yes type=add_id_to_list add_id_to_list_value_type=keyboard
  • איך ניתן להנפיק דו"ח שיחות נכנסות מלא למערכת?

    3
    0 הצבעות
    3 פוסטים
    25 צפיות
    פ
    @תוכן-ותמיכה בדוח שיחות בממשק הדוחות[image: 1791451091282-%D7%A6%D7%99%D7%9C%D7%95%D7%9D-%D7%9E%D7%A1%D7%9A-2026-10-08-121602.png]
  • מאגר מנגינות רקע 🎵 פרויקט שיתופי🤝

    75
    1
    3 הצבעות
    75 פוסטים
    2k צפיות
    פ
    @מערכות-טלפוניות רעננתי את הדף וזה עזר...
  • 1 הצבעות
    154 פוסטים
    4k צפיות
    א
    @מסובך-מאוד כתב: ישמע הודעה ולא יחייג ל1700 כיון שלא יהיה לו כח לעבור מספר. אין בעיה, כנראה שהמאזין הזה אוהב לשמוע פרסומות. @מסובך-מאוד כתב: וחוץ מזה אם במספר הישן יהיה הודעה אז גם בחדש יהיה לא. אתה עושה הפנייה לפי מחייג ומחוייג שרק בחיוג למספר הישן של הקו שומעים הודעה לעבור ל1700, ו1700 יפנה למספר החדש של המערכת.
  • 0 הצבעות
    2 פוסטים
    23 צפיות
    ש
    @הדיבל-המעופף יפה מאוד! מרגישים את ההשקעה!
  • 2 הצבעות
    3 פוסטים
    36 צפיות
    ק
    @אריה-AI התוכנה מעדכנת את הקו תכנס לשם ותראה
  • בירור | העלאת קבצים - תאריך לועזי

    נפתר
    3
    0 הצבעות
    3 פוסטים
    46 צפיות
    צ
    @אז-נדברו עשיתי דרך האתר בטא העלאה. זה קבצי TTS. העלאת קובץ מהמחשב. מעלה לי לשלוחה תיקייה בשם הקובץ, ובתוכו 4 קבצי WAV ריקים.