feat: jc 引擎主线搬入 app/engine(315 测试基线全绿:320 去掉 admin 5 用例)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-12 16:25:32 +08:00
parent 0ae5952002
commit 603c03eb4a
55 changed files with 10912 additions and 0 deletions
+231
View File
@@ -0,0 +1,231 @@
// ---- 动态主题色 bootstrap(spec §3):?color= → 派生 → :root 变量 ----
(function () {
if (typeof ChatTheme === "undefined") return; // theme.js 加载失败→style.css :root 品牌蓝兜底
var m = /(?:^|[?&])color=([^&]*)/.exec(location.search);
function safeDecode(s) { try { return decodeURIComponent(s); } catch (e) { return ""; } }
var color = m ? safeDecode(m[1]).trim() : "";
var palette = ChatTheme.derivePalette(color || ChatTheme.DEFAULT_COLOR);
var root = document.documentElement.style;
root.setProperty("--w-primary", palette.primary);
root.setProperty("--w-primary-rgb", palette.primaryRgb);
root.setProperty("--w-gradient-end", palette.gradientEnd);
root.setProperty("--w-deep-text", palette.deepText);
root.setProperty("--w-caption-text", palette.captionText);
root.setProperty("--w-soft-bg", palette.softBg);
root.setProperty("--w-border-soft", palette.borderSoft);
// 头像兜底(与挂件同机制)
Array.prototype.forEach.call(document.querySelectorAll(".avatar-img"), function (img) {
function hideEmoji() {
var s = img.parentElement && img.parentElement.querySelector(".avatar-emoji");
if (s) s.style.display = "none";
}
if (img.complete && img.naturalWidth > 0) { hideEmoji(); return; }
img.addEventListener("load", hideEmoji);
img.addEventListener("error", function () { img.remove(); });
});
})();
(function () {
// 联系方式配置(与 widget.js 的 data-* 机制一致)
var chatScript = document.currentScript || document.querySelector('script[src*="chat.js"]');
var whatsapp = (chatScript && chatScript.getAttribute("data-whatsapp")) || "https://wa.me/+8615937108317";
var email = (chatScript && chatScript.getAttribute("data-email")) || "mailto:hesu@hesuu.com";
var waBtn = document.querySelector('.whatsapp-btn');
var emBtn = document.querySelector('.email-btn');
if (waBtn) waBtn.href = whatsapp;
if (emBtn) emBtn.href = email;
var sessionId = localStorage.getItem("cc_session_id");
var messagesEl = document.getElementById("messages");
var inputEl = document.getElementById("input");
var sendBtn = document.getElementById("sendBtn");
var typingEl = document.getElementById("typing");
var endedEl = document.getElementById("chatEnded");
var popularTopicsEl = document.getElementById("popular-topics");
var ws = null;
var ended = false;
// Current streaming message element
var streamBubble = null;
var statusTimer = null;
function getWsUrl() {
var loc = window.location;
var proto = loc.protocol === "https:" ? "wss:" : "ws:";
return proto + "//" + loc.host + "/ws/chat";
}
function connect() {
ws = new WebSocket(getWsUrl());
ws.onopen = function () {
if (sessionId) {
ws.send(JSON.stringify({ session_id: sessionId }));
} else {
ws.send(JSON.stringify({ source: "standalone" }));
}
};
ws.onmessage = function (e) {
var data = JSON.parse(e.data);
if (data.type === "init" || data.type === "resumed") {
sessionId = data.session_id;
localStorage.setItem("cc_session_id", sessionId);
if (data.type === "resumed") {
loadHistory();
}
}
if (data.type === "stream_start") {
clearStatusTimer();
hideTyping();
streamBubble = addStreamBubble();
}
if (data.type === "stream_chunk") {
if (streamBubble) {
streamBubble.textContent += data.content;
messagesEl.scrollTop = messagesEl.scrollHeight;
}
}
if (data.type === "stream_end") {
streamBubble = null;
}
if (data.type === "evaluated") {
if (data.ended) {
ended = true;
endedEl.classList.add("active");
inputEl.disabled = true;
sendBtn.disabled = true;
}
}
if (data.type === "error") {
clearStatusTimer();
hideTyping();
streamBubble = null;
addMessage("assistant", "抱歉,出了点问题,请稍后再试。");
}
};
ws.onclose = function () {
if (!ended) {
setTimeout(connect, 3000);
}
};
ws.onerror = function () {
ws.close();
};
}
function send() {
var text = inputEl.value.trim();
if (!text || ended || !ws || ws.readyState !== WebSocket.OPEN) return;
addMessage("user", text);
inputEl.value = "";
hidePopularTopics();
// Immediate feedback: show status then typing
showStatusThenTyping();
ws.send(JSON.stringify({ content: text }));
}
function sendTopicMessage(topic) {
if (ended || !ws || ws.readyState !== WebSocket.OPEN) return;
addMessage("user", topic);
hidePopularTopics();
// Immediate feedback: show status then typing
showStatusThenTyping();
ws.send(JSON.stringify({ content: topic }));
}
function hidePopularTopics() {
if (popularTopicsEl) {
popularTopicsEl.style.display = "none";
}
}
function showStatusThenTyping() {
clearStatusTimer();
showTyping();
// Auto-hide typing is handled by stream_start replacing it
}
function clearStatusTimer() {
if (statusTimer) {
clearTimeout(statusTimer);
statusTimer = null;
}
}
function addMessage(role, content) {
var div = document.createElement("div");
div.className = "message " + role;
var wrapper = document.createElement("div");
var bubble = document.createElement("div");
bubble.className = "bubble";
bubble.textContent = content;
wrapper.appendChild(bubble);
div.appendChild(wrapper);
messagesEl.appendChild(div);
messagesEl.scrollTop = messagesEl.scrollHeight;
}
function addStreamBubble() {
var div = document.createElement("div");
div.className = "message assistant";
var wrapper = document.createElement("div");
var bubble = document.createElement("div");
bubble.className = "bubble";
bubble.textContent = "";
wrapper.appendChild(bubble);
div.appendChild(wrapper);
messagesEl.appendChild(div);
messagesEl.scrollTop = messagesEl.scrollHeight;
return bubble;
}
function showTyping() {
typingEl.classList.add("active");
messagesEl.scrollTop = messagesEl.scrollHeight;
}
function hideTyping() {
typingEl.classList.remove("active");
}
function loadHistory() {
}
sendBtn.addEventListener("click", send);
inputEl.addEventListener("keydown", function (e) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
send();
}
});
// Popular topics buttons
var topicBtns = document.querySelectorAll(".popular-topic-btn");
topicBtns.forEach(function (btn) {
btn.addEventListener("click", function () {
var topic = this.getAttribute("data-topic");
sendTopicMessage(topic);
});
});
connect();
})();
+103
View File
@@ -0,0 +1,103 @@
/* 客服主题派生——单品牌色 → 7 槽位色板(spec 2026-08-31 §5)。
* 注意:此文件与 static/widget.js 内联拷贝必须逐字符一致(空白归一化后),
* 由 tests/test_theme_static.py 强制;修改务必两边同步。 */
(function (global) {
"use strict";
var DEFAULT_COLOR = "#066fc0";
var DEFAULT_PALETTE = {
primary: "#066fc0",
primaryRgb: "6,111,192",
gradientEnd: "#0666bb",
deepText: "#14517a",
captionText: "#2174ab",
softBg: "#f3f9fe",
borderSoft: "#bbdcf2"
};
function clampNum(v, lo, hi) {
return Math.min(hi, Math.max(lo, v));
}
function hexToRgb(hex) {
hex = String(hex || "").replace(/^#/, "");
if (hex.length === 3) {
hex = hex.split("").map(function (ch) { return ch + ch; }).join("");
}
if (!/^[0-9a-fA-F]{6}$/.test(hex)) return null;
return {
r: parseInt(hex.slice(0, 2), 16),
g: parseInt(hex.slice(2, 4), 16),
b: parseInt(hex.slice(4, 6), 16)
};
}
function rgbToHsl(r, g, b) {
r /= 255; g /= 255; b /= 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var l = (max + min) / 2;
var d = max - min;
var h = 0, s = 0;
if (d !== 0) {
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
if (max === r) h = ((g - b) / d) % 6;
else if (max === g) h = (b - r) / d + 2;
else h = (r - g) / d + 4;
h *= 60;
if (h < 0) h += 360;
}
return { h: h, s: s * 100, l: l * 100 };
}
function toHex2(v) {
var s = Math.round(clampNum(v, 0, 255)).toString(16);
return s.length < 2 ? "0" + s : s;
}
function hslToHex(h, s, l) {
h = ((h % 360) + 360) % 360;
s = clampNum(s, 0, 100) / 100;
l = clampNum(l, 0, 100) / 100;
var c = (1 - Math.abs(2 * l - 1)) * s;
var x = c * (1 - Math.abs(((h / 60) % 2) - 1));
var m = l - c / 2;
var rp = 0, gp = 0, bp = 0;
if (h < 60) { rp = c; gp = x; }
else if (h < 120) { rp = x; gp = c; }
else if (h < 180) { gp = c; bp = x; }
else if (h < 240) { gp = x; bp = c; }
else if (h < 300) { rp = x; bp = c; }
else { rp = c; bp = x; }
return "#" + toHex2((rp + m) * 255) + toHex2((gp + m) * 255) + toHex2((bp + m) * 255);
}
function derivePalette(color) {
try {
var rgb = hexToRgb(color);
if (!rgb) {
if (typeof console !== "undefined" && console.warn) console.warn("[chat-theme] invalid color, fallback:", color);
return DEFAULT_PALETTE;
}
var hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
var h = hsl.h, s = hsl.s, l = hsl.l;
return {
primary: "#" + toHex2(rgb.r) + toHex2(rgb.g) + toHex2(rgb.b),
primaryRgb: rgb.r + "," + rgb.g + "," + rgb.b,
gradientEnd: hslToHex(h + 2, s, l - 1),
deepText: hslToHex(h - 2, s * 0.76, clampNum(l - 24, 28, 40)),
captionText: hslToHex(h - 2, s * 0.72, clampNum(l - 10, 40, 52)),
softBg: hslToHex(h, s * 0.9, 97.5),
borderSoft: hslToHex(h - 2, s * 0.72, 84)
};
} catch (e) {
return DEFAULT_PALETTE;
}
}
global.ChatTheme = {
derivePalette: derivePalette,
DEFAULT_COLOR: DEFAULT_COLOR,
DEFAULT_PALETTE: DEFAULT_PALETTE
};
})(typeof window !== "undefined" ? window : this);