@יצחק-י.
קוד שרת
/**
* 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]