基于 4sapi 打造零后端 AI 对话 Demo:单文件 HTML,10 分钟上线

6 阅读6分钟

前言

日常开发中,我们经常需要快速搭建 AI 对话 Demo,用于客户方案演示、团队模型效果测试、AI 功能原型验证。但传统开发模式里,光是搭建后端代理、解决跨域问题、适配多模型接口、处理密钥安全,就要花大半天时间,完全偏离了「快速验证、快速演示」的核心需求。

本文将基于 4sapi,用单文件纯 HTML 实现完整的 AI 对话 Demo,零后端、零构建依赖、10 分钟即可完成开发与上线,原生支持流式输出、多模型一键切换、多轮上下文记忆,同时完美解决密钥安全、跨域、网络卡顿等核心痛点,代码可直接复制复用。

方案核心优势

这套方案能实现零后端快速落地,核心依托 4sapi 的原生能力,完美解决传统 Demo 开发的所有痛点:

  1. 原生跨域支持,无需后端代理:4sapi 接口原生支持前端跨域请求,无需自建后端代理层,直接在前端 JS 中调用,彻底告别跨域报错。
  2. 子令牌精细化管控,密钥安全无忧:可为 Demo 创建独立子令牌,设置严格的模型权限、调用额度、过期时间,哪怕令牌泄露也不会造成全局风险,完全满足前端公开部署的安全需求。
  3. 100% 兼容 OpenAI 接口,零适配成本:完全对齐 OpenAI 原生接口规范,一套代码支持 GPT、Claude、DeepSeek、Qwen 等 50 + 主流模型,修改 model 参数即可一键切换。
  4. 国内 BGP 加速,访问流畅无卡顿:国内多线节点加速,哪怕调用海外旗舰模型,也能实现低延迟流式响应,告别跨境访问超时、卡顿问题。
  5. 单文件开箱即用,无任何依赖:纯 HTML + 原生 JS 实现,不需要 Node.js、构建工具,本地双击即可运行,一键部署到任何静态托管平台。

前置准备

  1. 前往 4sapi 平台完成账号注册与实名认证,进入控制台;
  2. 为 Demo 创建专属子令牌:仅开放对话模型权限、设置单日调用额度上限、合理的过期时间,生成后保存好子令牌;
  3. 仅需浏览器 + 文本编辑器,无需其他任何环境准备。

完整代码实现

将以下代码保存为index.html,替换YOUR_4SAPI_SUB_KEY为你创建的子令牌,双击即可在浏览器中打开使用:

html

预览

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>4sapi AI对话Demo</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
        }
        body {
            background-color: #f5f5f5;
            height: 100vh;
            display: flex;
            flex-direction: column;
        }
        .header {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 16px 20px;
            display: flex;
            justify-content: space-between;
            align-items: center;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
        }
        .header h1 {
            font-size: 18px;
            font-weight: 600;
        }
        .model-select {
            padding: 6px 12px;
            border-radius: 6px;
            border: none;
            outline: none;
            background: rgba(255,255,255,0.2);
            color: white;
            font-size: 14px;
        }
        .model-select option {
            color: #333;
        }
        .chat-container {
            flex: 1;
            overflow-y: auto;
            padding: 20px;
            max-width: 900px;
            width: 100%;
            margin: 0 auto;
        }
        .message {
            margin-bottom: 20px;
            display: flex;
            animation: fadeIn 0.3s ease;
        }
        @keyframes fadeIn {
            from { opacity: 0; transform: translateY(10px); }
            to { opacity: 1; transform: translateY(0); }
        }
        .message.user {
            justify-content: flex-end;
        }
        .message .avatar {
            width: 36px;
            height: 36px;
            border-radius: 50%;
            flex-shrink: 0;
            display: flex;
            align-items: center;
            justify-content: center;
            font-weight: 600;
            font-size: 14px;
            color: white;
        }
        .message.user .avatar {
            background-color: #667eea;
            margin-left: 12px;
        }
        .message.assistant .avatar {
            background-color: #10b981;
            margin-right: 12px;
        }
        .message .content {
            max-width: 70%;
            padding: 12px 16px;
            border-radius: 12px;
            line-height: 1.6;
            font-size: 15px;
            white-space: pre-wrap;
            word-break: break-word;
        }
        .message.user .content {
            background-color: #667eea;
            color: white;
            border-bottom-right-radius: 4px;
        }
        .message.assistant .content {
            background-color: white;
            color: #333;
            border-bottom-left-radius: 4px;
            box-shadow: 0 1px 4px rgba(0,0,0,0.08);
        }
        .input-container {
            max-width: 900px;
            width: 100%;
            margin: 0 auto 20px;
            padding: 0 20px;
        }
        .input-wrapper {
            background: white;
            border-radius: 12px;
            box-shadow: 0 2px 12px rgba(0,0,0,0.08);
            display: flex;
            align-items: flex-end;
            padding: 12px 16px;
        }
        #user-input {
            flex: 1;
            border: none;
            outline: none;
            font-size: 15px;
            line-height: 1.6;
            max-height: 200px;
            overflow-y: auto;
            resize: none;
            font-family: inherit;
        }
        #send-btn {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border: none;
            border-radius: 8px;
            padding: 8px 16px;
            margin-left: 12px;
            cursor: pointer;
            font-size: 14px;
            font-weight: 500;
            transition: opacity 0.2s;
            flex-shrink: 0;
        }
        #send-btn:disabled {
            opacity: 0.5;
            cursor: not-allowed;
        }
        .loading {
            display: inline-block;
            width: 12px;
            height: 12px;
            border: 2px solid rgba(255,255,255,0.3);
            border-top-color: white;
            border-radius: 50%;
            animation: spin 0.8s linear infinite;
        }
        @keyframes spin {
            to { transform: rotate(360deg); }
        }
        .tips {
            text-align: center;
            font-size: 12px;
            color: #999;
            margin-top: 8px;
        }
    </style>
</head>
<body>
    <div class="header">
        <h1>4sapi AI对话Demo</h1>
        <select class="model-select" id="model-select">
            <option value="deepseek-v4">DeepSeek V4</option>
            <option value="claude-opus-4-6">Claude Opus 4.6</option>
            <option value="gpt-5.4">GPT-5.4</option>
            <option value="gemini-3.1-pro">Gemini 3.1 Pro</option>
            <option value="qwen3.5-plus">通义千问3.5 Plus</option>
        </select>
    </div>

    <div class="chat-container" id="chat-container"></div>

    <div class="input-container">
        <div class="input-wrapper">
            <textarea id="user-input" placeholder="请输入你的问题,Shift+Enter换行,Enter发送" rows="1"></textarea>
            <button id="send-btn">发送</button>
        </div>
        <p class="tips">基于4sapi提供AI能力,支持多模型一键切换</p>
    </div>

    <script>
        // 核心配置,替换为你的4sapi子令牌
        const API_KEY = "YOUR_4SAPI_SUB_KEY";
        const BASE_URL = "https://4sapi.com/v1";

        // DOM元素
        const chatContainer = document.getElementById("chat-container");
        const userInput = document.getElementById("user-input");
        const sendBtn = document.getElementById("send-btn");
        const modelSelect = document.getElementById("model-select");

        // 对话历史
        let messages = [];
        let isLoading = false;

        // 自动调整输入框高度
        userInput.addEventListener("input", () => {
            userInput.style.height = "auto";
            userInput.style.height = Math.min(userInput.scrollHeight, 200) + "px";
        });

        // 发送消息
        async function sendMessage() {
            const content = userInput.value.trim();
            if (!content || isLoading) return;

            // 重置输入框
            userInput.value = "";
            userInput.style.height = "auto";

            // 添加用户消息
            addMessage("user", content);
            messages.push({ role: "user", content });

            // 加载状态
            isLoading = true;
            sendBtn.disabled = true;
            sendBtn.innerHTML = '<span class="loading"></span>';

            // 添加AI回复占位
            const assistantMessageEl = addMessage("assistant", "");
            let assistantContent = "";

            try {
                // 调用4sapi接口,流式输出
                const response = await fetch(`${BASE_URL}/chat/completions`, {
                    method: "POST",
                    headers: {
                        "Content-Type": "application/json",
                        "Authorization": `Bearer ${API_KEY}`
                    },
                    body: JSON.stringify({
                        model: modelSelect.value,
                        messages: messages,
                        stream: true,
                        temperature: 0.7
                    })
                });

                if (!response.ok) throw new Error(`请求失败:${response.statusText}`);

                // 处理流式响应
                const reader = response.body.getReader();
                const decoder = new TextDecoder("utf-8");

                while (true) {
                    const { done, value } = await reader.read();
                    if (done) break;

                    // 解析流式数据
                    const chunk = decoder.decode(value);
                    const lines = chunk.split("\n").filter(line => line.trim() !== "");

                    for (const line of lines) {
                        if (line.startsWith("data: ") && line !== "data: [DONE]") {
                            try {
                                const data = JSON.parse(line.slice(6));
                                const content = data.choices[0]?.delta?.content || "";
                                if (content) {
                                    assistantContent += content;
                                    assistantMessageEl.querySelector(".content").textContent = assistantContent;
                                    // 滚动到底部
                                    chatContainer.scrollTop = chatContainer.scrollHeight;
                                }
                            } catch (e) {
                                // 忽略解析错误
                            }
                        }
                    }
                }

                // 保存对话历史
                messages.push({ role: "assistant", content: assistantContent });

            } catch (error) {
                assistantMessageEl.querySelector(".content").textContent = `请求失败:${error.message},请检查API Key是否正确、额度是否充足`;
            } finally {
                // 恢复状态
                isLoading = false;
                sendBtn.disabled = false;
                sendBtn.innerHTML = "发送";
                chatContainer.scrollTop = chatContainer.scrollHeight;
            }
        }

        // 添加消息到页面
        function addMessage(role, content) {
            const messageDiv = document.createElement("div");
            messageDiv.className = `message ${role}`;
            messageDiv.innerHTML = `
                <div class="content">${content}</div>
                <div class="avatar">${role === "user" ? "你" : "AI"}</div>
            `;
            // 调整顺序
            if (role === "assistant") {
                const avatar = messageDiv.querySelector(".avatar");
                const content = messageDiv.querySelector(".content");
                messageDiv.insertBefore(avatar, content);
            }
            chatContainer.appendChild(messageDiv);
            chatContainer.scrollTop = chatContainer.scrollHeight;
            return messageDiv;
        }

        // 事件监听
        sendBtn.addEventListener("click", sendMessage);
        userInput.addEventListener("keydown", (e) => {
            if (e.key === "Enter" && !e.shiftKey) {
                e.preventDefault();
                sendMessage();
            }
        });
    </script>
</body>
</html>

核心功能说明

  1. 流式对话输出:模拟原生 ChatGPT 的逐字输出效果,提升对话体验;
  2. 多轮上下文记忆:自动维护对话历史,支持连续多轮对话,AI 可精准理解上下文语境;
  3. 多模型一键切换:下拉框即可切换主流大模型,无需修改代码,快速对比不同模型的回答效果;
  4. 响应式布局:完美适配 PC 端、移动端,手机端也能正常使用;
  5. 完善的异常处理:接口调用失败、密钥错误、额度用尽时,会给出友好的错误提示,便于排查问题。

快速上线与最佳实践

1. 快速部署上线

  • 本地运行:代码保存为index.html,双击即可在浏览器中打开使用;
  • 公开上线:可直接上传到 GitHub Pages、Gitee Pages、Netlify、Vercel 等静态托管平台,1 分钟即可完成公开部署,无需服务器。

2. 安全最佳实践

  • 严格遵循最小权限原则:Demo 使用的子令牌,仅开放所需的模型权限,设置最低够用的调用额度与较短的过期时间,定期更换令牌;
  • 禁止使用主令牌:绝对不要把 4sapi 主 API Key 放到前端代码中,必须使用独立的子令牌;
  • 开启额度预警:在 4sapi 控制台设置额度预警,用量达到阈值时及时收到提醒,避免异常消耗。

3. 自定义扩展

可根据需求快速扩展功能,无需修改核心对接逻辑:比如添加 Prompt 模板库、对话历史导出 / 导入、多模态图片上传、Markdown 格式渲染等,轻松适配不同的演示场景。