喜樂君技术博客 - 开源网站管家解决方案 | 益益AI
开源AI网站管家解决方案，提供7x24小时智能客服、DOM可视化编辑、SEO优化工具。专为开发者打造的轻量级AI引擎，支持PHP/STM32等技术栈。
腾讯云竞价实例防销毁策略 编译方法
防痴呆，防遗忘，防脑神经元不经用Q：编译后的机器（新购买的服务器）必须具备 Python 环境和编译程序吗？A：不需要！完全不需要。这正是我们使用 Nuitka 打包的目的。编译机（你的开发机）： 必须有 Python、Nuitka、SDK 等所有环境。运行机（新买的服务器）： 只需要是一个干净的 Linux 系统（CentOS/Ubuntu）即可。打包好的程序就像一个自带了“微型 Python 环境”的手提箱，走到哪里都能直接运行。以下是为你准备的博客文档内容（复制 --- 中间的内容即可）：Python 自动化运维：使用 Nuitka 将脚本打包为 Linux 独立可执行文件 (无需 Python 环境运行)📖 前言：为什么要打包？在云服务器容灾或自动化运维场景中，我们需要在新购买的服务器上立即运行脚本。但是，新服务器通常是“干净”的，没有安装我们需要的 Python 库（如 tencentcloud-sdk、requests 等）。如果我们每次都要先 pip install ... 再运行，既浪费时间又容易出错。解决方案：使用 Nuitka 将 Python 脚本及其依赖库、解释器全部打包成一个独立的二进制文件。这样，新服务器只要能开机，就能运行我们的程序！🛠️ 第一步：准备编译环境注意：这一步只需要在你的开发机（或者作为母板的服务器）上操作。1. 基础环境要求确保你的开发机安装了 Python 3 和 GCC 编译器（Nuitka 需要它来将 Python 转译为 C 语言）。# CentOS 系统安装基础编译工具 yum install -y python3 python3-devel gcc gcc-c++ make # Ubuntu/Debian 系统 apt-get install -y python3 python3-dev gcc g++ make 2. 安装 Python 依赖库安装你的脚本用到的库，以及打包工具 Nuitka。# 1. 安装打包工具 Nuitka pip3 install nuitka # 2. 安装脚本需要的业务库 (根据你的脚本需求) pip3 install requests tencentcloud-sdk-python # 3. 建议更新 pip (可选) pip3 install --upgrade pip 📝 第二步：核心代码逻辑 (防踩坑指南)在容灾场景下（旧服务器销毁 -> 自动买新服务器 -> 恢复业务），有一个巨大的逻辑坑：身份验证失效。通常我们用机器硬件信息（MAC地址、Machine-ID）生成指纹。但是，云服务器生成的新实例，硬件信息全变了，导致程序认为这是“盗版拷贝”。解决方案：在代码中加入“指纹存档”逻辑。程序首次运行：生成指纹 -> 写入 bot_identity.lic 文件。程序打包镜像：这个文件被一起打包进镜像。新机器启动：程序发现 bot_identity.lic 存在 -> 直接读取，不再重新计算。这样，新机器就完美继承了“老机器”的身份。🚀 第三步：执行编译命令 (核心)这是最关键的一步。请确保你在脚本所在的目录下执行。编译指令详解python3 -m nuitka \ --onefile \ --standalone \ --include-package=tencentcloud \ --include-package=requests \ --remove-output \ --jobs=4 \ --output-filename=AutoBot_VIP \ auto_bot_vip3.py 参数说明 (小白必看)--onefile: 最重要。告诉编译器把所有东西（依赖库、解释器、代码）塞进一个文件里。--standalone: 制作独立环境，不依赖系统 Python。--include-package=...: 强制打包某些库。tencentcloud 和 requests 必须强制包含，否则容易漏掉。--remove-output: 编译完自动删除临时的 build 文件夹，保持清爽。--output-filename=AutoBot_VIP: 指定生成出来的程序名字。auto_bot_vip3.py: 你的源代码文件名。🏃‍♂️ 第四步：部署与运行编译完成后，你会得到一个名为 AutoBot_VIP 的文件（没有后缀名）。1. 测试运行在开发机上赋予权限并运行：chmod +x AutoBot_VIP ./AutoBot_VIP 2. 检查身份文件运行一次后，查看目录下是否生成了 bot_identity.lic：ls -la bot_identity.lic 如果这个文件存在，说明身份持久化成功了！3. 分发使用你可以把这个 AutoBot_VIP 文件直接上传到任何同架构（如都是 x86_64 Linux）的服务器上运行，无需安装任何 Python 环境。❓ 常见问题 QAQ1：我在 Windows 上编译的程序能传到 Linux 服务器运行吗？A：不能。 Nuitka 不是交叉编译器。要在 Linux 上跑，必须在 Linux 环境下编译（建议搞一台 CentOS 虚拟机专门用来编译）。Q2：编译速度为什么这么慢？A： 正常现象。Nuitka 会把 Python 翻译成 C 语言，再用 GCC 编译成机器码，比简单的打包要慢，但运行效率高且源码安全性好。Q3：以后我要修改代码怎么办？A： 修改 .py 源码 -> 重新运行“第三步”的编译命令 -> 得到新的可执行文件 -> 覆盖旧文件。文档维护人：王喜乐最后更新：2026-02-19💡 给新手的额外建议保存好源代码：那个 .py 文件是你的核心资产，千万别弄丢了。编译出来的二进制文件是无法还原回源代码的（这也是一种加密保护）。测试流程：每次修改代码重新编译后，不要直接上生产环境。先在本地运行 ./AutoBot_VIP，确保没有报错（比如缺少模块），然后再去服务器部署。博客排版：这段 Markdown 代码直接粘贴到 Typecho 的编辑器里，代码块和标题会自动渲染得很好看。
内容教腾讯云竞价实例被销毁如何提前防范于未然，使用 python 进行自动化控制，自动检测元数据是否有异常，并发送邮件提醒，并自动化备份快照，创建镜像，创建新的服务器并且进行CDN源站地址更改成新的服务器，实现全流程自动化防止实例销毁的风险注: 内容开源 MIT License 开源地址第一步：安装 pip在命令行中依次复制并运行以下命令：安装 EPEL 源（pip通常在这个源里）：yum -y install epel-release安装 pip：yum -y install python3-pip # 升级 pip（可选，防止版本过低报错）：pip3 install --upgrade pip第二步：重新安装腾讯云 SDK安装完 pip 后，再次运行之前的安装命令（注意这里我用了 pip3，在 CentOS 上更稳妥）：pip3 install tencentcloud-sdk-python提示：如果安装成功，你应该能看到类似 Successfully installed ... 的提示。安装完成第三步: 实现实现自动化通知，即将被销毁的服务器 和 自动备份当前时刻的快照默认快照到期时间是 7天 可自行设定创建vi monitor.py按 i 键 然后按 SHIFT + ins键 ，粘贴完 按 esc键 再按 SHIFT + : 键输入 wq 然后回车复制这个import time import urllib.request import urllib.error import smtplib import datetime import os import socket from email.mime.text import MIMEText from email.header import Header from email.utils import formataddr # 引入腾讯云 SDK try: from tencentcloud.common import credential from tencentcloud.common.profile.client_profile import ClientProfile from tencentcloud.common.profile.http_profile import HttpProfile from tencentcloud.cbs.v20170312 import cbs_client, models except ImportError: print("❌ 错误: 请先安装 SDK: pip3 install tencentcloud-sdk-python") exit() # ================= 配置区域 ================= # 1. 邮件配置 MAIL_SENDER = 'xlj@xlj0.com' # 发件人 MAIL_PASSWORD = '这里是你的邮件密码' MAIL_RECEIVER = '480003862@qq.com' # 收件人 SMTP_SERVER = 'smtp.qq.com' SMTP_PORT = 465 # 2. 腾讯云 API 配置 TENCENT_SECRET_ID = '这是你的 腾讯云API秘钥ID' TENCENT_SECRET_KEY = '这是你的 腾讯云API秘钥ID KEY (去腾讯云获取)' REGION = 'ap-chongqing' # 重庆 (这里是服务器区域) # 3. 备份保留策略 # 设置为 7 表示快照会在 7 天后自动删除 # 建议设置 3-7 天，足够你恢复数据，又不会长期扣费 RETENTION_DAYS = 7 # =========================================== BASE_URL = "http://metadata.tencentyun.com/latest/meta-data/" SPOT_URL = BASE_URL + "spot/termination-time" def log(msg): print(f"[{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {msg}") def get_meta_data(key): try: url = BASE_URL + key req = urllib.request.Request(url) with urllib.request.urlopen(req, timeout=2) as response: return response.read().decode('utf-8') except: return "获取失败" def get_system_specs(): specs = {} try: specs['cpu_count'] = os.cpu_count() with open('/proc/meminfo', 'r') as f: for line in f: if 'MemTotal' in line: kb = int(line.split()[1]) specs['ram_total'] = f"{kb / 1024 / 1024:.2f} GB" break except: specs['cpu_count'] = "未知" specs['ram_total'] = "未知" return specs def trigger_auto_snapshot(instance_id): """调用腾讯云 API 创建带过期时间的快照""" log("⚡️ 正在尝试自动创建快照/备份...") os.system('sync') log("✅ 系统缓存已写入磁盘 (sync executed).") try: cred = credential.Credential(TENCENT_SECRET_ID, TENCENT_SECRET_KEY) client = cbs_client.CbsClient(cred, REGION) # 1. 查找硬盘 req_desc = models.DescribeDisksRequest() req_desc.Filters = [{"Name": "instance-id", "Values": [instance_id]}] resp_desc = client.DescribeDisks(req_desc) disk_list = resp_desc.DiskSet snapshot_results = [] if not disk_list: log("❌ 未找到挂载的云硬盘，无法备份！") return "备份失败：无硬盘" # 2. 计算过期时间 (UTC格式) # 腾讯云要求格式: 2022-01-08T09:47:55+00:00 utc_now = datetime.datetime.utcnow() deadline_dt = utc_now + datetime.timedelta(days=RETENTION_DAYS) deadline_str = deadline_dt.strftime('%Y-%m-%dT%H:%M:%S+00:00') log(f"🕒 快照将设置为 {RETENTION_DAYS} 天后自动过期 ({deadline_str})") # 3. 循环创建快照 for disk in disk_list: disk_id = disk.DiskId disk_usage = disk.DiskUsage req_snap = models.CreateSnapshotRequest() req_snap.DiskId = disk_id req_snap.SnapshotName = f"AutoBackup_{disk_usage}_{datetime.datetime.now().strftime('%Y%m%d%H%M')}" # 🔥 关键修改：添加 Deadline 参数 req_snap.Deadline = deadline_str resp_snap = client.CreateSnapshot(req_snap) snap_id = resp_snap.SnapshotId log(f"✅ 快照创建成功: {disk_id} -> {snap_id}") snapshot_results.append(f"成功 (ID: {snap_id}) 有效期至: {deadline_str}") return " ".join(snapshot_results) except Exception as err: log(f"❌ API 报错: {err}") return f"备份失败: {str(err)}" def send_email(is_test=False, termination_time="无", backup_status="未触发"): try: instance_id = get_meta_data("instance-id") public_ip = get_meta_data("public-ipv4") local_ip = get_meta_data("local-ipv4") zone = get_meta_data("placement/zone") specs = get_system_specs() if is_test: subject = "【✅监控启动】服务器安全守护中" title_color = "#28a745" warn_text = "监控脚本已成功启动，正在后台运行。" else: subject = "【🚨紧急】竞价实例销毁预警 & 自动备份" title_color = "#dc3545" warn_text = f"警告：腾讯云已发出回收通知！ 预计销毁时间：{termination_time}" html_content = f""" {subject} {warn_text} 📊 服务器详细信息 实例 ID {instance_id} 公网 IP {public_ip} 内网 IP {local_ip} 所在区域 {zone} 硬件配置 CPU: {specs['cpu_count']} 核 / 内存: {specs['ram_total']} 💾 自动备份状态 {backup_status} From: 喜樂君 """ message = MIMEText(html_content, 'html', 'utf-8') message['From'] = formataddr(["服务器监控", MAIL_SENDER]) message['To'] = formataddr(["管理员", MAIL_RECEIVER]) message['Subject'] = Header(subject, 'utf-8') server = smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT) server.login(MAIL_SENDER, MAIL_PASSWORD) server.sendmail(MAIL_SENDER, [MAIL_RECEIVER], message.as_string()) server.quit() log("📧 邮件发送成功！") return True except Exception as e: log(f"📧 邮件发送失败: {e}") return False def check_spot_status(): log("🛡️ 监控脚本已启动，开始守护...") # 启动时发送一封测试邮件 send_email(is_test=True, backup_status=f"系统正常。策略：快照将在创建 {RETENTION_DAYS} 天后自动删除。") while True: try: req = urllib.request.Request(SPOT_URL) with urllib.request.urlopen(req, timeout=2) as response: data = response.read().decode('utf-8') if "20" in data: log(f"!!! 🚨 收到销毁通知 !!! 时间: {data}") instance_id = get_meta_data("instance-id") if "ins-" in instance_id: backup_res = trigger_auto_snapshot(instance_id) else: backup_res = "❌ 失败：无法获取实例ID" send_email(is_test=False, termination_time=data, backup_status=backup_res) log("任务完成，脚本退出。") break except urllib.error.HTTPError as e: if e.code == 404: pass except Exception as e: pass time.sleep(5) if __name__ == "__main__": check_spot_status()最后python3 monitor.py挂到后台nohup python3 monitor.py > monitor.log 2>&1 &实现完全自动化 （检测到即将销毁 -> 自动备份当前时刻服务器 -> 自动创建快照 -> 自动创建镜像 -> 自动获取当前服务器配置信息 -> 创建新的同类型服务器(对于可能库存不足的服务器会多测试几次不同类型) -> 修改CDN域名源站地址到心服务器 -> 不间断自动化完成）如果会python 你可以自己写一个逻辑实现，也可以使用下方现成的集成一键安装脚本 安装自动化程序因为作者生活拮据 故收费 2元 一个月的全自动化监控， 12 一年 去支持作者 并取得秘钥开始使用！如果你愿意花 2元请 王喜乐喝一杯茶，那严谨的代码和稳定的逻辑执行将伴随你一个月，并在此感谢你的付出，王喜乐由衷表示感谢你执行下方脚本即可 全天候 关机重启自动启动 安全服务器维护wget -O AutoBot_VIP https://ai.xlj0.com/ZY/auto_bot/AutoBot_VIP && chmod +x AutoBot_VIP && ./AutoBot_VIP
-AI-一个 7 x 24 小时 不抱怨的网站小AI客服 Github 开源 "YiYiAi Core (核心版)"。YiYiAi Management Core (益益AI 管理核心)YiYiAi Management Core 是一款专为前端开发与网站管理设计的轻量级 AI 辅助引擎。作为一个单文件（Single-file）解决方案，它能无缝嵌入任何网页，提供 DOM 实时可视化编辑、AI 代码辅助修改、智能对话调试以及SEO 深度分析功能。注意: 本仓库开源的是 YiYiAi 的核心管理引擎。它包含了所有功能逻辑、编辑器和 AI 交互能力。(注：移动端适配插件与高级 UI 主题包属于商业版组件，不包含在此开源版本中，但核心功能完全可用。)✨ 核心功能1. 🛠 可视化 DOM 智能编辑器深度探查: 提供类似 Chrome DevTools 的元素树状视图，支持折叠/展开。双向编辑: 支持“代码视图”与“实时预览”分屏操作，所见即所得。安全沙箱: 编辑操作在隔离环境中进行，确保页面稳定性。2. 🤖 AI 代码修改 (Text-to-Code)自然语言指令: 选中页面任意元素，告诉 AI “把这个按钮改成圆角并加阴影”，AI 将自动生成代码并应用。智能回滚: 支持撤销 AI 的修改操作，保留修改历史。3. 🧠 深度思考 AI 助手流式对话: 集成 SSE (Server-Sent Events) 流式响应。思维链展示: 支持显示 AI 的 reasoning_content (深度思考过程)，适合复杂逻辑调试。上下文感知: 可配置 Persona (人设) 和外部知识库 URL。4. 📊 SEO 智能诊断一键分析: 自动抓取页面 HTML 结构。专业报告: 生成包含 Head 标签优化、内容策略、关键词布局的 Markdown 格式建议书。🚀 快速集成只需将 YiYiAi-manage.js 引入您的 HTML 页面即可（通常放置在 之前）。 配置参数id="yiyiai-boot-script": 必须保留，用于脚本识别自身配置。persona: (可选) 定义 AI 的角色设定。data-url: (可选) 传递给 AI 的额外上下文链接。⚙️ 后端接口配置 (必读)本项目是一个纯前端引擎，需要配合后端接口使用以连接 LLM（如 OpenAI, Claude, DeepSeek 等）。前端默认请求地址为 /YiYiAi-manage.php。您需要实现一个满足以下协议的后端接口：请求方式: POST数据格式: JSON{ "messages": [ {"role": "system", "content": "..."}, {"role": "user", "content": "..."} ], "stream": true } 响应格式: 标准 SSE (Server-Sent Events) 流。(如果您有自己的后端地址，请在 JS 文件中全局搜索 /YiYiAi-manage.php 并替换为您自己的 API URL。)📦 项目结构. ├── YiYiAi-manage.js # 核心引擎 (包含所有逻辑、样式注入与编辑器) ├── README.md # 说明文档 └── LICENSE # 开源协议 🛠 开发指南本项目采用 "All-in-One" 架构，无需 Webpack/Vite 打包。样式管理: CSS 通过 WIDGET_CSS 常量直接注入，修改 JS 内的 CSS 字符串即可实时生效。外部依赖: 自动通过 CDN 加载 marked.js, purify.js, highlight.js，无需本地安装。开源代码YiYiAi-manage.js// 使用IIFE（立即调用函数表达式）来创建私有作用域，避免污染全局命名空间 (function() { // --- 获取启动脚本和自定义属性 --- const bootScript = document.getElementById('yiyiai-boot-script'); let customPersona = null; let contextUrl = null; if (bootScript) { customPersona = bootScript.getAttribute('persona'); contextUrl = bootScript.getAttribute('data-url'); console.log( '%c益益AI', 'color: #4285f4; text-decoration: underline; cursor: pointer', '-> 益益AI 为您的网站保驾护航：https://ai.xlj0.com' ); console.log('AI助手配置加载:', { persona: customPersona, url: contextUrl }); } else { console.warn('AI助手警告：未能找到ID为 "yiyiai-boot-script" 的script标签，无法加载自定义 persona 和 data-url。'); } // 1. 定义所有需要的资源和内容 const VENDOR_RESOURCES = { css: ["https://ai.xlj0.com/morething/YiYiAi-manage/static/atom-one-dark.min.css"], js: [ "https://ai.xlj0.com/morething/YiYiAi-manage/static/marked.min.js", "https://ai.xlj0.com/morething/YiYiAi-manage/static/purify.min.js", "https://ai.xlj0.com/morething/YiYiAi-manage/static/highlight.min.js" ] }; // --- 修改: 新增综合信息选项卡的HTML结构 --- const WIDGET_HTML = ` <path d="M896 85.333333H128C104.533333 85.333333 85.333333 104.533333 85.333333 128v512c0 23.466667 19.2 42.666667 42.666667 42.666667h512c23.466667 0 42.666667-19.2 42.666667-42.666667v-85.333333h128c23.466667 0 42.666667-19.2 42.666667-42.666667V128c0-23.466667-19.2-42.666667-42.666667-42.666667zM725.333333 597.333333c0 23.466667-19.2 42.666667-42.666666 42.666667H170.666667c-23.466667 0-42.666667-19.2-42.666667-42.666667V170.666667c0-23.466667 19.2-42.666667 42.666667-42.666667h512c23.466667 0 42.666666 19.2 42.666666 42.666667v426.666666z m213.333334-128c0 23.466667-19.2 42.666667-42.666667 42.666667h-85.333333V170.666667c0-47.36-38.4-85.333333-85.333334-85.333334H170.666667v-0.533333h682.666666c23.466667 0 42.666667 19.2 42.666667 42.666667v341.333333z m-469.333334-85.333333a42.666667 42.666667 0 1 0 0-85.333334 42.666667 42.666667 0 0 0 0 85.333334z m170.666667 0a42.666667 42.666667 0 1 0 0-85.333334 42.666667 42.666667 0 0 0 0 85.333334z"></path> 益益 AI 助手 综合信息 页面元素 SEO 优化 × AI 深度思考中... 您好！我是益益AI助手。您可以随时问我问题。 发送 页面综合信息概览 当前页面核心技术与内容信息汇总（实时更新） 搜索 清除 网站 SEO 智能分析 点击下方按钮，AI将对当前页面进行全面分析，并提供详细的优化建议。 开始智能分析 编辑 & 预览元素 分屏 代码 预览 × 警告: 不正确的修改可能破坏页面 与 AI 沟通修改 取消 保存并更新页面 告诉 AI 如何修改 例如：“给最外层div添加一个红色的虚线边框” 取消 确认修改 `; // --- 修改: 新增综合信息面板样式 --- const WIDGET_CSS = ` /* General Styles */ #yiyiai-trigger-btn { position: fixed; bottom: 30px; right: 30px; width: 60px; height: 60px; background: linear-gradient(45deg, #2563eb, #7c3aed); border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25); z-index: 99997; transition: transform 0.2s ease-in-out; animation: yiyiai-pulse 2.5s infinite; } #yiyiai-trigger-btn:hover { transform: scale(1.1); animation-play-state: paused; } #yiyiai-trigger-btn svg { width: 32px; height: 32px; fill: white; } @keyframes yiyiai-pulse { 0% { box-shadow: 0 0 0 0 rgba(37, 99, 235, 0.5); } 70% { box-shadow: 0 0 0 15px rgba(37, 99, 235, 0); } 100% { box-shadow: 0 0 0 0 rgba(37, 99, 235, 0); } } /* Main Modal & Overlay */ #yiyiai-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.6); z-index: 99998; display: none; align-items: center; justify-content: center; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } #yiyiai-overlay #yiyiai-modal { width: 90%; height: 85%; max-width: 700px; max-height: 800px; background-color: #0f172a; color: #f1f5f9; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.4); display: flex; flex-direction: column; overflow: hidden; animation: yiyiai-fadein 0.3s ease; } @keyframes yiyiai-fadein { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } } #yiyiai-overlay .yiyiai-header { padding: 15px 20px; border-bottom: 1px solid rgba(148, 163, 184, 0.3); display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; } #yiyiai-overlay #yiyiai-close-btn { background: none; border: none; color: #94a3b8; font-size: 28px; cursor: pointer; line-height: 1; } .yiyiai-tabs { display: flex; gap: 8px; align-items: center; } .yiyiai-tab { background-color: transparent; border: 1px solid transparent; color: #94a3b8; padding: 6px 14px; font-size: 16px; font-weight: 500; border-radius: 6px; cursor: pointer; transition: all 0.2s ease-in-out; } .yiyiai-tab:hover:not(.active) { background-color: rgba(71, 85, 105, 0.4); } .yiyiai-tab.active { color: #f1f5f9; background-color: rgba(59, 130, 246, 0.2); border-color: rgba(59, 130, 246, 0.6); } .yiyiai-tab-content { display: none; flex-grow: 1; overflow: hidden; } .yiyiai-tab-content.active { display: flex; flex-direction: column; } /* Chat Panel（原有） */ #yiyiai-overlay #yiyiai-chat-container { flex-grow: 1; overflow-y: auto; padding: 20px; } #yiyiai-overlay .yiyiai-message { margin-bottom: 15px; display: flex; max-width: 85%; } #yiyiai-overlay .yiyiai-message .yiyiai-avatar { width: 36px; height: 36px; border-radius: 50%; margin-right: 12px; flex-shrink: 0; } #yiyiai-overlay .yiyiai-message .yiyiai-content { overflow: auto; padding: 12px 16px; border-radius: 18px; line-height: 1.6; word-wrap: break-word; } .yiyiai-content ul, .yiyiai-content ol { color: #c1bcb4; padding-left: 20px; } .yiyiai-content h1, .yiyiai-content h2, .yiyiai-content h3, .yiyiai-content h4, .yiyiai-content h5, .yiyiai-content h6 { color: #ffffff; } .yiyiai-content p { margin: 0 0 10px; } .yiyiai-content li { margin-bottom: 5px; } #yiyiai-overlay .yiyiai-user-msg { margin-left: auto; flex-direction: row-reverse; } #yiyiai-overlay .yiyiai-user-msg .yiyiai-avatar { margin-left: 12px; margin-right: 0; background-color: #2563eb; } #yiyiai-overlay .yiyiai-user-msg .yiyiai-content { background: linear-gradient(135deg, #1e40af 0%, #3b82f6 100%); } #yiyiai-overlay .yiyiai-bot-msg .yiyiai-avatar { background-color: #7c3aed; } #yiyiai-overlay .yiyiai-bot-msg .yiyiai-content { background: linear-gradient(135deg, #374151 0%, #4b5563 100%); } #yiyiai-overlay .yiyiai-input-area { padding: 15px; border-top: 1px solid rgba(148, 163, 184, 0.3); display: flex; gap: 10px; flex-shrink: 0; } #yiyiai-overlay #yiyiai-user-input { flex-grow: 1; padding: 12px; border: 1px solid rgba(71, 85, 105, 0.7); border-radius: 8px; background-color: rgba(30, 41, 59, 0.5); color: #f1f5f9; font-size: 16px; } #yiyiai-overlay #yiyiai-send-btn { padding: 12px 20px; background: #2563eb; color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 16px; } #yiyiai-overlay #yiyiai-send-btn:disabled { background: #4b5563; cursor: not-allowed; } #yiyiai-overlay #yiyiai-thinking-panel { padding: 0px 20px 10px; border-bottom: 1px solid rgba(148, 163, 184, 0.2); display: none; flex-shrink: 0; } #yiyiai-overlay #yiyiai-thinking-panel summary { cursor: pointer; color: #94a3b8; font-weight: bold; padding: 10px 0; outline: none; display: flex; flex-direction: column; } #yiyiai-overlay #yiyiai-reasoning-log { display: none; margin-top: 10px; padding: 10px; background-color: rgba(0,0,0,0.2); border-radius: 6px; font-family: 'Courier New', monospace; font-size: 13px; color: #a5b4fc; white-space: pre-wrap; max-height: 150px; overflow-y: auto; border: 1px solid rgba(148, 163, 184, 0.2); } #yiyiai-overlay .yiyiai-summary-text { margin-bottom: 8px; } #yiyiai-overlay .yiyiai-progress-bar { width: 100%; height: 4px; background-color: rgba(148, 163, 184, 0.2); border-radius: 2px; overflow: hidden; } #yiyiai-overlay .yiyiai-progress-bar-inner { width: 50%; height: 100%; background: linear-gradient(90deg, #60a5fa, #a5b4fc); animation: yiyiai-progress-anim 2s infinite linear; } @keyframes yiyiai-progress-anim { 0% { transform: translateX(-100%); } 100% { transform: translateX(200%); } } #yiyiai-overlay .yiyiai-content pre { position: relative; margin: 15px 0; border-radius: 6px; } #yiyiai-overlay .yiyiai-content pre code { display: block; padding: 15px; font-size: 14px; } #yiyiai-overlay .yiyiai-content pre .yiyiai-copy-btn { position: absolute; top: 8px; right: 8px; background-color: #4b5563; color: #e5e7eb; border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 12px; opacity: 0.7; transition: opacity 0.2s; } #yiyiai-overlay .yiyiai-content pre:hover .yiyiai-copy-btn { opacity: 1; } /* 新增：综合信息面板样式 */ #yiyiai-overview-panel { padding: 20px; background-color: rgba(0,0,0,0.2); flex-direction: column; } .yiyiai-overview-toolbar { text-align: center; padding-bottom: 20px; border-bottom: 1px solid rgba(148, 163, 184, 0.3); flex-shrink: 0; } .yiyiai-overview-toolbar h3 { margin: 0 0 10px; color: #a5b4fc; font-size: 20px; } .yiyiai-overview-toolbar p { margin: 0 0 15px; color: #94a3b8; font-size: 14px; line-height: 1.5; } .yiyiai-overview-details { flex-grow: 1; overflow-y: auto; padding-top: 20px; line-height: 1.7; color: #f1f5f9; } .yiyiai-overview-category { margin: 0 0 10px; color: #60a5fa; font-size: 16px; } .yiyiai-overview-item { margin-bottom: 15px; padding: 12px; background-color: rgba(30, 41, 59, 0.5); border-radius: 8px; transition: background 0.2s; } .yiyiai-overview-item:hover { background-color: rgba(30, 41, 59, 0.7); } .yiyiai-overview-label { font-weight: bold; color: #93c5fd; margin-right: 8px; } .yiyiai-overview-value { word-break: break-all; color: #e2e8f0; } /* Elements Panel（原有） */ #yiyiai-elements-panel { background-color: rgba(0,0,0,0.2); overflow-y: auto; } #yiyiai-elements-toolbar { display: flex; padding: 10px 15px; border-bottom: 1px solid rgba(148, 163, 184, 0.3); gap: 10px; flex-shrink: 0; align-items: center; } #yiyiai-search-input { flex-grow: 1; background: rgba(30, 41, 59, 0.8); border: 1px solid rgba(148, 163, 184, 0.3); color: #f1f5f9; padding: 8px; border-radius: 4px; } #yiyiai-search-btn, #yiyiai-clear-search-btn { background: #2563eb; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; } #yiyiai-clear-search-btn { background-color: #4b5563; } #yiyiai-elements-content { font-family: 'SF Mono', 'Courier New', monospace; font-size: 13px; flex-grow: 1; overflow: auto; padding: 10px 15px; } #yiyiai-elements-content .line { padding-left: calc(var(--indent, 0) * 2ch); white-space: pre; border-radius: 3px; transition: background-color 0.3s; } #yiyiai-elements-content .line.highlight { background-color: rgba(96, 165, 250, 0.2); } #yiyiai-elements-content .line.has-children > .toggle { display: inline-block; width: 2ch; cursor: pointer; user-select: none; color: #60a5fa; } #yiyiai-elements-content .line.editable:hover { background: rgba(96, 165, 250, 0.1); } #yiyiai-elements-content .line .tag-wrapper { cursor: pointer; } #yiyiai-elements-content mark { background-color: #facc15; color: black; border-radius: 2px; } /* SEO优化面板（原有） */ #yiyiai-seo-panel { padding: 20px; background-color: rgba(0,0,0,0.2); flex-direction: column; } .yiyiai-seo-toolbar { text-align: center; padding-bottom: 20px; border-bottom: 1px solid rgba(148, 163, 184, 0.3); flex-shrink: 0; } .yiyiai-seo-toolbar h3 { margin: 0 0 10px; color: #a5b4fc; font-size: 20px; } .yiyiai-seo-toolbar p { margin: 0 0 15px; color: #94a3b8; font-size: 14px; line-height: 1.5; } #yiyiai-seo-analyze-btn { background: #7c3aed; color: white; border: none; padding: 12px 25px; border-radius: 8px; cursor: pointer; font-size: 16px; font-weight: bold; transition: background-color 0.2s; } #yiyiai-seo-analyze-btn:hover { background: #6d28d9; } #yiyiai-seo-analyze-btn:disabled { background: #4b5563; color: #9ca3af; cursor: not-allowed; } #yiyiai-seo-content { flex-grow: 1; overflow-y: auto; padding-top: 20px; line-height: 1.7; } #yiyiai-seo-content .yiyiai-seo-loading { display: flex; align-items: center; justify-content: center; height: 100%; color: #94a3b8; font-size: 16px; } #yiyiai-seo-content .yiyiai-seo-loading .yiyiai-spinner { width: 24px; height: 24px; border: 3px solid rgba(148, 163, 184, 0.3); border-top-color: #a5b4fc; border-radius: 50%; animation: yiyiai-spin 1s linear infinite; margin-right: 12px; } @keyframes yiyiai-spin { to { transform: rotate(360deg); } } #yiyiai-seo-content .yiyiai-content { background: none !important; padding: 0 !important; } /* Editor Modal（原有） */ #yiyiai-editor-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); z-index: 100001; display: none; align-items: center; justify-content: center; } #yiyiai-editor-modal { width: 90%; max-width: 1200px; height: 85%; background: #0f172a; border: 1px solid rgba(148, 163, 184, 0.3); border-radius: 8px; display: flex; flex-direction: column; box-shadow: 0 10px 40px black; } .yiyiai-editor-header, .yiyiai-editor-footer { padding: 15px; flex-shrink: 0; display: flex; justify-content: space-between; align-items: center; } .yiyiai-editor-header { border-bottom: 1px solid rgba(148, 163, 184, 0.3); } .yiyiai-editor-footer { border-top: 1px solid rgba(148, 163, 184, 0.3); flex-wrap: wrap; } .yiyiai-editor-footer > div:last-child { display: flex; gap: 10px; align-items: center; } .yiyiai-editor-header h3 { margin: 0; color: #60a5fa; } #yiyiai-editor-close-btn { background: none; border: none; color: #94a3b8; font-size: 28px; cursor: pointer; } #yiyiai-editor-cancel-btn { background: #4b5563; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; } #yiyiai-editor-save-btn { background: #2563eb; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; } .yiyiai-editor-notice { font-size: 12px; color: #f87171; flex-grow: 1; } #yiyiai-editor-ai-btn { background: #7c3aed; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; transition: background-color 0.2s; } #yiyiai-editor-ai-btn:hover { background: #6d28d9; } #yiyiai-editor-ai-btn:disabled { background: #4b5563; color: #9ca3af; cursor: not-allowed; } #yiyiai-editor-progress-container { width: 100%; height: 4px; background-color: rgba(148, 163, 184, 0.15); overflow: hidden; flex-shrink: 0; } .yiyiai-editor-progress-bar { width: 40%; height: 100%; background: linear-gradient(90deg, #a5b4fc, #7c3aed); animation: yiyiai-progress-anim 2s infinite linear; } #yiyiai-ai-actions-wrapper { display: inline-flex; gap: 10px; } #yiyiai-editor-undo-btn, #yiyiai-editor-regen-btn { background-color: #4b5563; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; transition: background-color 0.2s; } #yiyiai-editor-undo-btn:hover, #yiyiai-editor-regen-btn:hover { background-color: #6b7280; } #yiyiai-editor-main-area { flex-grow: 1; display: flex; overflow: hidden; } #yiyiai-editor-textarea { flex-grow: 1; flex-basis: 50%; background: #020617; color: #e2e8f0; font-family: 'SF Mono', 'Courier New', monospace; font-size: 14px; border: none; resize: none; padding: 15px; border-right: 1px solid rgba(148, 163, 184, 0.3); } #yiyiai-preview-wrapper { flex-grow: 1; flex-basis: 50%; background-color: #ffffff; overflow: auto; } #yiyiai-preview-iframe { width: 100%; height: 100%; border: none; } #yiyiai-editor-view-controls { display: flex; gap: 5px; position: absolute; left: 50%; transform: translateX(-50%); } #yiyiai-editor-view-controls button { background: #4b5563; color: white; border: 1px solid transparent; padding: 5px 10px; border-radius: 4px; cursor: pointer; font-size: 12px; } #yiyiai-editor-view-controls button:hover { background: #6b7280; } #yiyiai-editor-view-controls button.active { background: #2563eb; border-color: #60a5fa; } /* Toast & Cursor（原有） */ #yiyiai-toast-container { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); z-index: 100002; display: flex; flex-direction: column; align-items: center; gap: 10px; pointer-events: none; } .yiyiai-toast { padding: 12px 20px; border-radius: 8px; color: #f1f5f9; font-size: 14px; font-weight: 500; box-shadow: 0 4px 15px rgba(0,0,0,0.5); border: 1px solid rgba(255, 255, 255, 0.1); opacity: 0; transform: translateY(-20px); transition: all 0.4s cubic-bezier(0.21, 1.02, 0.73, 1); max-width: 90vw; text-align: center; } .yiyiai-toast.show { opacity: 1; transform: translateY(0); } .yiyiai-toast.success { background: linear-gradient(135deg, #166534, #22c55e); } .yiyiai-toast.error { background: linear-gradient(135deg, #991b1b, #ef4444); } .yiyiai-toast.info { background: linear-gradient(135deg, #1e3a8a, #3b82f6); } .yiyiai-cursor { display: inline-block; width: 8px; height: 1em; background-color: #f1f5f9; vertical-align: text-bottom; animation: yiyiai-blink 1s step-end infinite; } @keyframes yiyiai-blink { 50% { opacity: 0; } } /* AI Prompt Modal（原有） */ #yiyiai-ai-prompt-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); z-index: 100002; display: none; align-items: center; justify-content: center; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } #yiyiai-ai-prompt-modal { background: #0f172a; color: #f1f5f9; padding: 25px; border-radius: 12px; width: 90%; max-width: 500px; box-shadow: 0 10px 40px rgba(0,0,0,0.5); border: 1px solid rgba(148, 163, 184, 0.3); animation: yiyiai-fadein 0.3s ease; } #yiyiai-ai-prompt-modal h4 { margin: 0 0 8px 0; font-size: 18px; color: #60a5fa; } #yiyiai-ai-prompt-modal p { margin: 0 0 15px 0; font-size: 14px; color: #94a3b8; } #yiyiai-ai-prompt-input { width: 100%; padding: 12px; border: 1px solid rgba(71, 85, 105, 0.7); border-radius: 8px; background-color: rgba(30, 41, 59, 0.5); color: #f1f5f9; font-size: 16px; margin-bottom: 20px; box-sizing: border-box; } .yiyiai-ai-prompt-footer { display: flex; justify-content: flex-end; gap: 12px; } #yiyiai-ai-prompt-cancel-btn, #yiyiai-ai-prompt-confirm-btn { padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-size: 16px; font-weight: 500; } #yiyiai-ai-prompt-cancel-btn { background-color: #4b5563; color: white; } #yiyiai-ai-prompt-confirm-btn { background-color: #7c3aed; color: white; } /* General Reset（原有） */ #yiyiai-trigger-btn, #yiyiai-modal a, button, summary { outline: none; -webkit-tap-highlight-color: transparent; } `; // 2. 辅助函数（原有） function loadResource(url) { return new Promise((resolve, reject) => { if (url.endsWith('.css')) { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = url; link.onload = () => resolve(); link.onerror = () => reject(new Error(`Failed to load CSS: ${url}`)); document.head.appendChild(link); } else if (url.endsWith('.js')) { const script = document.createElement('script'); script.src = url; script.onload = () => resolve(); script.onerror = () => reject(new Error(`Failed to load JS: ${url}`)); document.head.appendChild(script); } }); } async function loadDependencies() { try { const promises = [ ...VENDOR_RESOURCES.css.map(loadResource), ...VENDOR_RESOURCES.js.map(loadResource) ]; await Promise.all(promises); console.log('所有依赖库加载完成'); } catch (error) { console.error('加载依赖库失败:', error); throw error; } } function formatHTML(html) { let indent = 0; const tab = ' '; let formatted = ''; html = html.replace(/>\s*</g, '>\n<'); const lines = html.split('\n'); lines.forEach(line => { const trimmedLine = line.trim(); if (!trimmedLine) return; if (trimmedLine.startsWith('</')) { indent = Math.max(0, indent - 1); } formatted += tab.repeat(indent) + trimmedLine + '\n'; const isSelfClosing = trimmedLine.endsWith('/>'); const isClosingTag = trimmedLine.startsWith('</'); const isDoctypeOrComment = trimmedLine.startsWith('<!'); if (trimmedLine.startsWith('<') && !isSelfClosing && !isClosingTag && !isDoctypeOrComment) { indent++; } }); return formatted.trim(); } const escapeHTML = (str) => { if (typeof str !== 'string') return ''; return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, '''); }; const showToast = (message, type = 'info', duration = 3000) => { const toastContainer = document.getElementById('yiyiai-toast-container'); if (!toastContainer) return; const toast = document.createElement('div'); toast.className = `yiyiai-toast ${type}`; toast.textContent = message; toastContainer.appendChild(toast); setTimeout(() => toast.classList.add('show'), 10); setTimeout(() => { toast.classList.remove('show'); toast.addEventListener('transitionend', () => toast.remove()); }, duration); }; const showFriendlyError = (error, context = '操作') => { console.error(`${context}失败:`, error); const msg = (error && error.message) ? error.message.toLowerCase() : ''; let friendlyMessage = `抱歉，${context}时发生未知错误。`; if (msg.includes('failed to fetch')) { friendlyMessage = '网络连接失败，请检查您的网络并重试。'; } else if (msg.includes('500') || msg.includes('502') || msg.includes('503') || msg.includes('504')) { friendlyMessage = '服务器暂时遇到问题，请稍后再试。'; } else if (msg.includes('ai未能返回')) { friendlyMessage = 'AI未能按预期返回内容，请尝试调整您的问题后重试。'; } else if (msg.includes('无法解析')) { friendlyMessage = '无法解析返回的内容，可能是格式有误。'; } showToast(friendlyMessage, 'error'); }; // 3. 核心应用逻辑 function initializeWidget() { document.body.insertAdjacentHTML('beforeend', WIDGET_HTML); const styleTag = document.createElement('style'); styleTag.innerHTML = WIDGET_CSS; document.head.appendChild(styleTag); const getEl = (id) => document.getElementById(id); // --- 修改: 新增综合信息面板的DOM元素引用 --- const elements = { triggerBtn: getEl('yiyiai-trigger-btn'), overlay: getEl('yiyiai-overlay'), closeBtn: getEl('yiyiai-close-btn'), tabs: document.querySelectorAll('.yiyiai-tab'), tabContents: document.querySelectorAll('.yiyiai-tab-content'), // 综合信息面板（新增） overviewPanel: getEl('yiyiai-overview-panel'), overviewContent: getEl('yiyiai-overview-content'), // 页面元素面板（原有） elementsContent: getEl('yiyiai-elements-content'), searchInput: getEl('yiyiai-search-input'), searchBtn: getEl('yiyiai-search-btn'), clearSearchBtn: getEl('yiyiai-clear-search-btn'), // 编辑器（原有） editorOverlay: getEl('yiyiai-editor-overlay'), editorTextarea: getEl('yiyiai-editor-textarea'), editorSaveBtn: getEl('yiyiai-editor-save-btn'), editorCancelBtn: getEl('yiyiai-editor-cancel-btn'), editorCloseBtn: getEl('yiyiai-editor-close-btn'), editorProgressContainer: getEl('yiyiai-editor-progress-container'), // 聊天面板（原有） chatContainer: getEl('yiyiai-chat-container'), userInput: getEl('yiyiai-user-input'), sendBtn: getEl('yiyiai-send-btn'), thinkingPanel: getEl('yiyiai-thinking-panel'), reasoningLog: getEl('yiyiai-reasoning-log'), thinkingDetails: getEl('yiyiai-thinking-panel').querySelector('details'), summaryText: getEl('yiyiai-thinking-panel').querySelector('.yiyiai-summary-text'), // AI提示（原有） aiPromptOverlay: getEl('yiyiai-ai-prompt-overlay'), aiPromptInput: getEl('yiyiai-ai-prompt-input'), aiPromptConfirmBtn: getEl('yiyiai-ai-prompt-confirm-btn'), aiPromptCancelBtn: getEl('yiyiai-ai-prompt-cancel-btn'), aiActionsWrapper: getEl('yiyiai-ai-actions-wrapper'), // 编辑器视图（原有） editorMainArea: getEl('yiyiai-editor-main-area'), previewIframe: getEl('yiyiai-preview-iframe'), previewWrapper: getEl('yiyiai-preview-wrapper'), viewSplitBtn: getEl('yiyiai-view-split-btn'), viewCodeBtn: getEl('yiyiai-view-code-btn'), viewPreviewBtn: getEl('yiyiai-view-preview-btn'), // SEO面板（原有） seoPanel: getEl('yiyiai-seo-panel'), seoAnalyzeBtn: getEl('yiyiai-seo-analyze-btn'), seoContent: getEl('yiyiai-seo-content'), }; elements.editorAiBtn = getEl('yiyiai-editor-ai-btn'); let isElementsTabPopulated = false; let currentEditingElement = null; let codeBeforeAiEdit = null; let lastUserInstruction = ''; // --- 新增：综合信息相关函数 --- /** * 收集页面综合信息 */ function getPageOverview() { // 基础信息 const title = document.title || '未设置'; const url = window.location.href; const metaDesc = document.querySelector('meta[name="description"]')?.content || '未设置'; const metaKeywords = document.querySelector('meta[name="keywords"]')?.content || '未设置'; const generator = document.querySelector('meta[name="generator"]')?.content || '未知'; // 资源统计 const imageCount = document.images.length; const linkCount = document.links.length; const scriptCount = document.scripts.length; const styleCount = document.styleSheets.length; const pageSize = (document.documentElement.outerHTML.length / 1024).toFixed(2) + ' KB'; // 性能信息 const performanceData = performance.getEntriesByType('navigation')[0]; const loadTime = performanceData ? (performanceData.loadEventEnd - performanceData.startTime).toFixed(2) + ' ms' : '无法获取'; // 响应式信息 const viewportWidth = window.innerWidth + ' px'; const isMobile = window.matchMedia('(max-width: 767px)').matches ? '是' : '否'; const viewportMeta = document.querySelector('meta[name="viewport"]') ? '已设置' : '未设置'; return { 基础信息: [ { label: '页面标题', value: title }, { label: '页面URL', value: url }, { label: '元描述', value: metaDesc }, { label: '元关键词', value: metaKeywords }, { label: '网站生成器', value: generator } ], 资源统计: [ { label: '图片数量', value: imageCount }, { label: '链接数量', value: linkCount }, { label: '脚本数量', value: scriptCount }, { label: '样式表数量', value: styleCount }, { label: '页面大小', value: pageSize } ], 性能与适配: [ { label: '页面加载时间', value: loadTime }, { label: '当前视口宽度', value: viewportWidth }, { label: '移动端适配', value: isMobile }, { label: 'Viewport Meta', value: viewportMeta } ] }; } /** * 渲染综合信息 */ function renderOverview() { const overviewData = getPageOverview(); let html = ''; Object.entries(overviewData).forEach(([category, items]) => { html += ` ${category} `; items.forEach(item => { html += ` ${item.label}: ${item.value} `; }); }); elements.overviewContent.innerHTML = html; } // --- 原有逻辑保持不变 --- const updateAiActionButtons = () => { elements.aiActionsWrapper.innerHTML = ''; const mainAiBtn = document.createElement('button'); mainAiBtn.id = 'yiyiai-editor-ai-btn'; mainAiBtn.textContent = '与 AI 沟通修改'; mainAiBtn.addEventListener('click', openAiPromptDialog); elements.aiActionsWrapper.appendChild(mainAiBtn); if (codeBeforeAiEdit) { const undoBtn = document.createElement('button'); undoBtn.id = 'yiyiai-editor-undo-btn'; undoBtn.textContent = '撤销'; undoBtn.title = '撤销上一次AI修改'; undoBtn.addEventListener('click', () => { elements.editorTextarea.value = codeBeforeAiEdit; updatePreview(); codeBeforeAiEdit = null; lastUserInstruction = ''; updateAiActionButtons(); showToast('AI的修改已撤销', 'info'); }); elements.aiActionsWrapper.appendChild(undoBtn); } if (lastUserInstruction) { const regenBtn = document.createElement('button'); regenBtn.id = 'yiyiai-editor-regen-btn'; regenBtn.textContent = '换个方案'; regenBtn.title = '让AI用相同指令再试一次'; regenBtn.addEventListener('click', () => { executeAiCodeModification(true); }); elements.aiActionsWrapper.appendChild(regenBtn); } elements.editorAiBtn = mainAiBtn; }; const resetAiState = () => { codeBeforeAiEdit = null; lastUserInstruction = ''; elements.aiPromptInput.value = ''; updateAiActionButtons(); }; const createLine = (data) => { const lineEl = document.createElement('div'); lineEl.className = 'line'; lineEl.style.setProperty('--indent', data.indent); lineEl.yiyiNodeRef = data.node; const hasChildren = data.node && Array.from(data.node.children).some(c => !['SCRIPT', 'STYLE', 'LINK'].includes(c.tagName) && !(c.id && c.id.startsWith('yiyiai-'))); if (hasChildren) lineEl.classList.add('has-children'); const toggle = `${hasChildren ? '►' : ' '}`; const tagWrapper = `${data.html}`; lineEl.innerHTML = toggle + tagWrapper; if (data.node) lineEl.classList.add('editable'); return lineEl; }; const buildAndAppendLevel = (parentLineEl, node, indent) => { parentLineEl.dataset.expanded = 'true'; const toggleEl = parentLineEl.querySelector('.toggle'); if (toggleEl) toggleEl.innerHTML = '▼'; const fragment = document.createDocumentFragment(); Array.from(node.children).forEach(child => { if (['SCRIPT', 'STYLE', 'LINK', 'HEAD'].includes(child.tagName) || (child.id && child.id.startsWith('yiyiai-'))) return; const attrs = Array.from(child.attributes).map(attr => ` ${escapeHTML(attr.name)}="${escapeHTML(attr.value)}"`).join(''); const tagName = child.tagName.toLowerCase(); const openTagHtml = `<${tagName}${attrs}>`; const lineEl = createLine({ node: child, indent, html: openTagHtml }); fragment.appendChild(lineEl); }); parentLineEl.after(fragment); }; const removeChildren = (parentLineEl) => { parentLineEl.dataset.expanded = 'false'; parentLineEl.querySelector('.toggle').innerHTML = '►'; let nextSibling = parentLineEl.nextElementSibling; const parentIndent = parseInt(parentLineEl.style.getPropertyValue('--indent')); while (nextSibling && parseInt(nextSibling.style.getPropertyValue('--indent')) > parentIndent) { const toRemove = nextSibling; nextSibling = nextSibling.nextElementSibling; toRemove.remove(); } }; const refreshElementsView = () => { try { const scrollState = { top: elements.elementsContent.scrollTop, left: elements.elementsContent.scrollLeft }; elements.elementsContent.innerHTML = ''; const rootNode = document.documentElement; if (!rootNode) return; const rootLine = createLine({ node: rootNode, indent: 0, html: ` ` }); elements.elementsContent.appendChild(rootLine); buildAndAppendLevel(rootLine, rootNode, 1); isElementsTabPopulated = true; elements.elementsContent.scrollTop = scrollState.top; elements.elementsContent.scrollLeft = scrollState.left; } catch (error) { showFriendlyError(error, '刷新元素视图'); } }; elements.triggerBtn.addEventListener('click', () => { elements.overlay.style.display = 'flex'; document.body.style.overflow = 'hidden'; }); const closeModal = () => { elements.overlay.style.display = 'none'; document.body.style.overflow = 'auto'; }; elements.closeBtn.addEventListener('click', closeModal); elements.overlay.addEventListener('click', (e) => { if (e.target === elements.overlay) closeModal(); }); // --- 修改: 选项卡切换时处理综合信息面板 --- elements.tabs.forEach(tab => { tab.addEventListener('click', () => { if (tab.classList.contains('active')) return; elements.tabs.forEach(t => t.classList.remove('active')); elements.tabContents.forEach(c => c.classList.remove('active')); tab.classList.add('active'); const targetPanel = getEl(`yiyiai-${tab.dataset.tab}-panel`); if (targetPanel) targetPanel.classList.add('active'); // 根据不同Tab触发对应逻辑 switch(tab.dataset.tab) { case 'elements': if (!isElementsTabPopulated) refreshElementsView(); break; case 'overview': renderOverview(); // 切换到综合信息时渲染数据 break; default: break; } }); }); // --- 以下原有逻辑保持不变 --- elements.elementsContent.addEventListener('click', (e) => { const toggle = e.target.closest('.toggle'); if (toggle) { const parentLine = toggle.parentElement; if (parentLine.dataset.expanded === 'true') { removeChildren(parentLine); } else { buildAndAppendLevel(parentLine, parentLine.yiyiNodeRef, parseInt(parentLine.style.getPropertyValue('--indent')) + 1); } } }); const openEditorForElement = (element, searchText = null) => { if (element === document.body) { showToast('为了页面稳定，不允许直接编辑 标签。', 'info'); return; } if (!document.contains(element)) { showToast('目标元素已不存在。', 'error'); return; } currentEditingElement = element; elements.editorTextarea.value = formatHTML(element.outerHTML); elements.editorOverlay.style.display = 'flex'; resetAiState(); elements.editorTextarea.focus(); if (searchText) { const textIndex = elements.editorTextarea.value.toLowerCase().indexOf(searchText.toLowerCase()); if (textIndex !== -1) { setTimeout(() => { elements.editorTextarea.setSelectionRange(textIndex, textIndex + searchText.length); }, 100); } } setEditorView('split'); updatePreview(); }; const updatePreview = () => { if (!elements.previewIframe) return; const styles = Array.from(document.head.querySelectorAll('link[rel="stylesheet"], style')) .map(el => el.outerHTML) .join(''); const content = elements.editorTextarea.value; const iframeContent = ` ${styles} ${content} `; elements.previewIframe.srcdoc = iframeContent; }; elements.editorTextarea.addEventListener('input', updatePreview); elements.elementsContent.addEventListener('dblclick', (e) => { const lineEl = e.target.closest('.line.editable'); if (lineEl && lineEl.yiyiNodeRef) { openEditorForElement(lineEl.yiyiNodeRef); } }); const closeEditor = () => { elements.editorOverlay.style.display = 'none'; currentEditingElement = null; }; elements.editorSaveBtn.addEventListener('click', () => { if (!currentEditingElement) return; if (!document.body.contains(currentEditingElement)) { showFriendlyError(new Error("目标元素已从页面移除，无法保存。")); closeEditor(); refreshElementsView(); return; } try { const newHTML = elements.editorTextarea.value; if (newHTML.trim() === currentEditingElement.outerHTML.trim()) { showToast('内容未改变，已取消。', 'info'); closeEditor(); return; } const tempDiv = document.createElement('div'); tempDiv.innerHTML = newHTML.trim(); const newNodes = Array.from(tempDiv.childNodes); if (newNodes.length > 0) { currentEditingElement.replaceWith(...newNodes); showToast('元素已成功更新！', 'success'); setTimeout(refreshElementsView, 150); } else { throw new Error("无法解析您输入的HTML，请检查代码。"); } } catch (error) { showFriendlyError(error, '保存修改'); } finally { closeEditor(); } }); elements.editorCancelBtn.addEventListener('click', closeEditor); elements.editorCloseBtn.addEventListener('click', closeEditor); const executeAiCodeModification = async (isRegeneration = false) => { const userInstruction = elements.aiPromptInput.value; if (!isRegeneration && (!userInstruction || userInstruction.trim() === '')) { showToast('请输入修改指令', 'info'); return; } if (!isRegeneration) { elements.aiPromptOverlay.style.display = 'none'; } const codeToModify = isRegeneration ? codeBeforeAiEdit : elements.editorTextarea.value; const instructionToUse = isRegeneration ? lastUserInstruction : userInstruction; if (!isRegeneration) { codeBeforeAiEdit = elements.editorTextarea.value; } lastUserInstruction = instructionToUse; const systemPrompt = `你是一个精通前端开发的专家。你的任务是根据用户的指示修改给定的HTML代码。你必须只返回修改后的、完整的、格式良好的HTML代码块，用\`\`\`html ... \`\`\`包裹，不要包含任何额外的解释、前言或评论。`; const userPrompt = `请根据以下指示修改HTML代码：\n\n【用户指示】: ${instructionToUse}\n\n---\n【原始HTML代码】:\n\`\`\`html\n${codeToModify}\n\`\`\``; elements.editorAiBtn.disabled = true; elements.editorSaveBtn.disabled = true; elements.editorTextarea.disabled = true; elements.editorAiBtn.textContent = 'AI 正在修改...'; elements.editorProgressContainer.style.display = 'block'; elements.editorTextarea.value = 'AI 正在分析您的需求，请稍候...'; elements.editorTextarea.style.color = '#94a3b8'; try { const response = await fetch('/YiYiAi-manage.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: [ { role: "system", content: systemPrompt }, { role: "user", content: userPrompt } ], stream: true }) }); if (!response.ok) throw new Error(`服务器错误，状态码: ${response.status}`); const reader = response.body.getReader(); const decoder = new TextDecoder(); let fullContent = ''; let isFirstReasoningChunk = true; let hasSwitchedToCode = false; while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); const lines = chunk.split('\n\n'); for (const line of lines) { if (!line.startsWith('data: ') || line.includes('[DONE]')) continue; const jsonStr = line.substring(6); try { const data = JSON.parse(jsonStr); if (data.error) throw new Error(data.error); const delta = data.choices[0].delta; if (delta.reasoning_content && !hasSwitchedToCode) { if (isFirstReasoningChunk) { elements.editorTextarea.value = ''; isFirstReasoningChunk = false; } elements.editorTextarea.value += delta.reasoning_content; } if (delta.content) { if (!hasSwitchedToCode) { elements.editorTextarea.value = ''; elements.editorTextarea.style.color = ''; hasSwitchedToCode = true; } fullContent += delta.content; elements.editorTextarea.value += delta.content; } } catch (e) { /* Silently ignore */ } } } if (!fullContent) throw new Error('AI未能返回有效代码内容。'); let cleanHtml = fullContent.trim(); const htmlMatch = cleanHtml.match(/```html\n([\s\S]*?)\n```/); if (htmlMatch && htmlMatch[1]) { cleanHtml = htmlMatch[1]; } else { const firstTag = cleanHtml.indexOf('<'); if (firstTag > 0) { cleanHtml = cleanHtml.substring(firstTag); } } elements.editorTextarea.value = formatHTML(cleanHtml); updatePreview(); showToast('AI 修改建议已应用！', 'success'); } catch (error) { showFriendlyError(error, 'AI代码修改'); elements.editorTextarea.value = codeBeforeAiEdit; updatePreview(); } finally { elements.editorProgressContainer.style.display = 'none'; elements.editorTextarea.style.color = ''; elements.editorAiBtn.disabled = false; elements.editorSaveBtn.disabled = false; elements.editorTextarea.disabled = false; updateAiActionButtons(); } }; const openAiPromptDialog = () => { elements.aiPromptOverlay.style.display = 'flex'; setTimeout(() => elements.aiPromptInput.focus(), 50); }; elements.editorAiBtn.addEventListener('click', openAiPromptDialog); elements.aiPromptCancelBtn.addEventListener('click', () => { elements.aiPromptOverlay.style.display = 'none'; }); elements.aiPromptConfirmBtn.addEventListener('click', () => executeAiCodeModification(false)); elements.aiPromptInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { executeAiCodeModification(false); } }); const findFirstElementWithText = (query) => { const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false); let node; while (node = walker.nextNode()) { if (node.nodeValue.toLowerCase().includes(query.toLowerCase())) { const parent = node.parentElement; if (parent && !['SCRIPT', 'STYLE'].includes(parent.tagName) && !parent.closest('[id^="yiyiai-"]')) { return parent; } } } return null; }; const revealElementInTree = async (element) => { const ancestors = []; let current = element; while (current && current !== document.documentElement) { ancestors.unshift(current); current = current.parentElement; } ancestors.unshift(document.documentElement); refreshElementsView(); let parentLine = null; for (const ancestorNode of ancestors) { const lineEl = Array.from(elements.elementsContent.querySelectorAll('.line')).find(line => line.yiyiNodeRef === ancestorNode); if (!lineEl) { console.warn('在树中未找到祖先节点:', ancestorNode); return null; } if (lineEl.dataset.expanded !== 'true' && lineEl.classList.contains('has-children')) { buildAndAppendLevel(lineEl, ancestorNode, parseInt(lineEl.style.getPropertyValue('--indent')) + 1); await new Promise(r => setTimeout(r, 10)); } parentLine = lineEl; } return parentLine; }; const performSearch = async () => { const query = elements.searchInput.value.trim(); if (!query) return; let targetElement = null; if (query.startsWith('#') || query.startsWith('.')) { try { targetElement = document.querySelector(query); if (!targetElement) { showToast(`未找到匹配选择器 "${query}" 的元素。`, 'info'); return; } if (targetElement.closest('[id^="yiyiai-"]')) { showToast('不允许选择AI助手自身的元素。', 'info'); return; } } catch (e) { showFriendlyError(e, '元素搜索'); return; } showToast(`正在定位 ${query}...`, 'info', 1500); const targetLine = await revealElementInTree(targetElement); if (targetLine) { targetLine.scrollIntoView({ behavior: 'smooth', block: 'center' }); targetLine.classList.add('highlight'); setTimeout(() => targetLine.classList.remove('highlight'), 2000); openEditorForElement(targetElement); } } else { targetElement = findFirstElementWithText(query); if (targetElement) { showToast('正在定位元素...', 'info', 1500); const targetLine = await revealElementInTree(targetElement); if (targetLine) { targetLine.scrollIntoView({ behavior: 'smooth', block: 'center' }); targetLine.classList.add('highlight'); setTimeout(() => targetLine.classList.remove('highlight'), 2000); openEditorForElement(targetElement, query); } } else { showToast('未在页面内容中找到该文字。', 'info'); } } }; elements.searchBtn.addEventListener('click', performSearch); elements.clearSearchBtn.addEventListener('click', () => { elements.searchInput.value = ''; }); elements.searchInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') performSearch(); }); let conversationHistory = []; const addMessage = (sender, text) => { const msgDiv = document.createElement('div'); msgDiv.className = `yiyiai-message ${sender === 'user' ? 'yiyiai-user-msg' : 'yiyiai-bot-msg'}`; msgDiv.innerHTML = ` ${text} `; elements.chatContainer.appendChild(msgDiv); elements.chatContainer.scrollTop = elements.chatContainer.scrollHeight; return msgDiv.querySelector('.yiyiai-content'); }; const addCopyButtons = (element) => { element.querySelectorAll('pre').forEach(block => { if (block.querySelector('.yiyiai-copy-btn')) return; const copyBtn = document.createElement('button'); copyBtn.className = 'yiyiai-copy-btn'; copyBtn.textContent = '复制'; block.appendChild(copyBtn); copyBtn.addEventListener('click', () => { const code = block.querySelector('code').innerText; navigator.clipboard.writeText(code).then(() => { copyBtn.textContent = '已复制!'; setTimeout(() => { copyBtn.textContent = '复制'; }, 2000); }).catch(() => showToast('复制失败', 'error')); }); }); }; async function handleSendMessage() { const userText = elements.userInput.value.trim(); if (!userText) return; addMessage('user', DOMPurify.sanitize(userText)); conversationHistory.push({ role: "user", content: userText }); elements.userInput.value = ''; elements.sendBtn.disabled = true; elements.sendBtn.textContent = '思考中'; elements.thinkingPanel.style.display = 'block'; elements.reasoningLog.textContent = ''; elements.reasoningLog.style.display = 'none'; elements.summaryText.textContent = 'AI 深度思考中...'; elements.thinkingDetails.querySelector('.yiyiai-progress-bar').style.display = 'block'; elements.thinkingDetails.open = true; let thinkingOccurred = false; const botMessageElement = addMessage('bot', '|'); let botReply = ''; try { const response = await fetch('/YiYiAi-manage.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: conversationHistory, persona: customPersona, context_url: contextUrl, stream: true }) }); if (!response.ok) throw new Error(`服务器错误，状态码: ${response.status}`); const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); const lines = chunk.split('\n\n'); for (const line of lines) { if (!line.startsWith('data: ') || line.includes('[DONE]')) continue; const jsonStr = line.substring(6); try { const data = JSON.parse(jsonStr); if (data.error) throw new Error(data.error); const delta = data.choices[0].delta; if (delta.reasoning_content) { if (!thinkingOccurred) { elements.reasoningLog.style.display = 'block'; thinkingOccurred = true; } elements.reasoningLog.textContent += delta.reasoning_content; elements.reasoningLog.scrollTop = elements.reasoningLog.scrollHeight; } if (delta.content) { botReply += delta.content; const dirtyHtml = marked.parse(botReply + '|'); botMessageElement.innerHTML = DOMPurify.sanitize(dirtyHtml); elements.chatContainer.scrollTop = elements.chatContainer.scrollHeight; } } catch (e) { /* Silently ignore */ } } } conversationHistory.push({ role: "assistant", content: botReply }); botMessageElement.querySelector('.yiyiai-cursor')?.remove(); botMessageElement.querySelectorAll('pre code').forEach(block => { if (window.hljs) hljs.highlightElement(block); }); addCopyButtons(botMessageElement); if (thinkingOccurred) { elements.summaryText.textContent = '查看AI深度思考过程'; elements.thinkingDetails.querySelector('.yiyiai-progress-bar').style.display = 'none'; } else { elements.thinkingPanel.style.display = 'none'; } } catch (error) { showFriendlyError(error, 'AI聊天'); botMessageElement.innerHTML = `抱歉，当前无法回复，请稍后重试。`; elements.thinkingPanel.style.display = 'none'; } finally { elements.sendBtn.disabled = false; elements.sendBtn.textContent = '发送'; // ✨ 新增代码：如果思考过程被触发过，则在完成后自动折叠 if (thinkingOccurred) { elements.thinkingDetails.open = false; } } } elements.sendBtn.addEventListener('click', handleSendMessage); elements.userInput.addEventListener('keypress', (e) => { if (e.key === 'Enter' && !e.shiftKey && !elements.sendBtn.disabled) { e.preventDefault(); handleSendMessage(); } }); const setEditorView = (mode) => { const isCodeVisible = mode === 'code' || mode === 'split'; const isPreviewVisible = mode === 'preview' || mode === 'split'; elements.editorTextarea.style.display = isCodeVisible ? 'block' : 'none'; elements.previewWrapper.style.display = isPreviewVisible ? 'block' : 'none'; elements.editorTextarea.style.flexBasis = mode === 'code' ? '100%' : (mode === 'split' ? '50%' : '0'); elements.previewWrapper.style.flexBasis = mode === 'preview' ? '100%' : (mode === 'split' ? '50%' : '0'); elements.viewSplitBtn.classList.toggle('active', mode === 'split'); elements.viewCodeBtn.classList.toggle('active', mode === 'code'); elements.viewPreviewBtn.classList.toggle('active', mode === 'preview'); }; elements.viewSplitBtn.addEventListener('click', () => setEditorView('split')); elements.viewCodeBtn.addEventListener('click', () => setEditorView('code')); elements.viewPreviewBtn.addEventListener('click', () => setEditorView('preview')); // --- SEO分析功能（原有） --- async function generateSeoReport() { elements.seoAnalyzeBtn.disabled = true; elements.seoAnalyzeBtn.textContent = '分析中...'; elements.seoContent.innerHTML = ` AI 正在对您的网站进行全面扫描和深度分析，请稍候... `; const pageHtml = document.documentElement.outerHTML; const pageUrl = window.location.href; const systemPrompt = `你是一名资深的SEO优化专家和网站架构师。你的任务是基于用户提供的网站URL和完整HTML代码，生成一份详细、专业且易于理解的优化报告。报告必须使用Markdown格式，并且严格按照以下三个部分进行组织，使用h2标题 (##) 分隔： 1. **网站 head 优化建议**: 详细分析 \` \` 标签内的内容。检查并提出针对 \` \`, \` \`, \` \` (并说明其当前的重要性), Open Graph 标签 (\`og:title\`, \`og:description\`, \`og:image\`等), Canonical URL, 以及其他相关meta标签的优化建议。对于缺失或不佳的标签，请提供推荐的、优化后的代码示例。 2. **网站整体方向以及综合优化建议**: 从更宏观的角度分析网站。内容应包括但不限于： * **内容策略**: 网站主题是否明确？内容质量如何？建议哪些内容方向？ * **用户体验 (UX)**: 网站结构是否清晰？导航是否便捷？移动端适配性如何？ * **性能优化**: 页面加载速度的关键点（指出即可，无需详细代码），如图片优化、资源压缩等。 * **内部链接**: 内部链接结构是否合理？有何改进建议？ * **关键词布局**: 页面关键词分布情况，是否存在关键词堆砌或不足。 3. **SEO 优惠建议和方案**: 根据网站的当前状况，以服务提供商的口吻，提供清晰的、分阶段的SEO优化服务建议方案。方案应分为几个等级（例如：基础版、进阶版、尊享版），每个方案包含： * **方案名称** * **核心服务内容** (例如：基础版包含关键词研究和TDK优化；进阶版增加内容创建和外链建设等) * **预期效果** * **象征性的价格或优惠说明** (例如：限时优惠价 ¥XXX 或 联系我们获取专属报价) 请确保你的回答内容丰富、建议具体、可操作性强。`; const userPrompt = `网站URL: ${pageUrl}\n\n以下是网站的完整HTML代码，请根据此内容生成SEO优化报告：\n\n\`\`\`html\n${pageHtml}\n\`\`\``; let reportContent = ''; try { const response = await fetch('/YiYiAi-manage.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: [ { role: "system", content: systemPrompt }, { role: "user", content: userPrompt } ], stream: true }) }); if (!response.ok) { throw new Error(`服务器错误, 状态码: ${response.status}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); const lines = chunk.split('\n\n'); for (const line of lines) { if (line.startsWith('data: ') && !line.includes('[DONE]')) { const jsonStr = line.substring(6); try { const data = JSON.parse(jsonStr); if (data.choices && data.choices[0].delta.content) { reportContent += data.choices[0].delta.content; const dirtyHtml = marked.parse(reportContent + '|'); elements.seoContent.innerHTML = ` ${DOMPurify.sanitize(dirtyHtml)} `; elements.seoContent.scrollTop = elements.seoContent.scrollHeight; } } catch (e) { /* 忽略JSON解析错误 */ } } } } const finalHtml = marked.parse(reportContent); elements.seoContent.innerHTML = ` ${DOMPurify.sanitize(finalHtml)} `; elements.seoContent.querySelectorAll('pre code').forEach(block => { if (window.hljs) hljs.highlightElement(block); }); addCopyButtons(elements.seoContent); } catch (error) { showFriendlyError(error, '生成SEO报告'); elements.seoContent.innerHTML = ` 抱歉，生成SEO报告时遇到问题，请稍后重试。 `; } finally { elements.seoAnalyzeBtn.disabled = false; elements.seoAnalyzeBtn.textContent = '重新分析'; } } elements.seoAnalyzeBtn.addEventListener('click', generateSeoReport); } async function main() { try { console.log('开始加载AI助手...'); await loadDependencies(); initializeWidget(); console.log('AI助手加载成功！'); } catch (error) { console.error("加载AI助手部件失败:", error); const errorDiv = document.createElement('div'); errorDiv.style.cssText = `position: fixed; bottom: 20px; right: 20px; padding: 12px 20px; background: #dc2626; color: white; border-radius: 8px; z-index: 100002; font-family: sans-serif; font-size: 14px; box-shadow: 0 4px 12px rgba(0,0,0,0.3);`; let detailMessage = '初始化错误'; if (error && error.message && typeof error.message.includes === 'function' && error.message.includes('load')) { detailMessage = '网络缓慢或资源加载失败'; } errorDiv.innerText = 'AI助手加载失败: ' + detailMessage; document.body.appendChild(errorDiv); setTimeout(() => errorDiv.remove(), 5000); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', main); } else { main(); } })();🤝 贡献与协议本项目基于 MIT 协议 开源。欢迎提交 Issue 反馈 Bug，或提交 PR 贡献代码。Created with ❤️ by YiYiAi (王喜乐) Team
项目硬件规格与引脚分配说明 (量-产-参-考)1. 项目概述本项目是一个基于 STM32F103C8T6 微控制器的多功能显示终端。它集成了OLED显示、电池电压监测、环境温度测量和人体感应功能，并通过丰富的动态UI（粒子系统、充放电动画）来展示信息。本文档旨在为该项目的硬件定型和批量生产提供清晰的引脚连接和元器件选型参考。实物图2. 核心控制器 (MCU)型号: STM32F103C8T6封装: LQFP48核心: ARM Cortex-M3 @ 72MHz供电电压: 2.0V ~ 3.6V (项目中通常使用 3.3V)3. 引脚分配总览 (Pin Allocation)MCU 引脚主要功能连接模块信号/功能描述备注PA0ADC1_IN0电池电压检测电路V_BATT_SENSE (模拟输入)ADC 采样引脚PA1GPIODS18B20 温度传感器DQ (单总线数据)开漏输出/上拉输入PA2GPIOOLED 显示屏CS (片选)SPI 片选信号，推挽输出PA3GPIOOLED 显示屏DC (数据/命令)数据/命令选择信号，推挽输出PA4GPIOOLED 显示屏RES (复位)屏幕硬件复位，推挽输出PA5SPI1_SCKOLED 显示屏SCK / D0 (时钟)SPI 时钟信号PA7SPI1_MOSIOLED 显示屏MOSI / D1 (数据)SPI 主机输出数据PB9GPIO人体红外感应模块 (PIR)OUT (数字信号输入)感应器输出信号，高电平有效3.3VPOWER所有模块VCC系统主供电GNDPOWER所有模块GND系统地VBATPOWER电池Battery +连接电池正极 (经过分压电路到 PA0)GNDPOWER电池Battery -连接电池负极4. 外设模块详解 (Peripheral Details)4.1 OLED 显示屏模块名称: OLED 显示屏型号/规格: 128x64 分辨率, SSD1306 驱动芯片连接方式: 4-Wire Hardware SPI (硬件SPI)引脚连接:MCU 引脚信号连接至 OLED 模块引脚PA2CSCS (Chip Select)PA3DCDC (Data/Command)PA4RESRES (Reset)PA5SCKD0 / SCK (Clock)PA7MOSID1 / SDA (Data)3.3VVCCVCCGNDGNDGND注意事项:请确保购买的OLED模块支持SPI模式。有些模块需要通过焊接模块背面的电阻(BS0/BS1)来切换SPI/I2C模式。本设计未使用 MISO (PA6) 引脚，因为屏幕是只写设备。4.2 电池电压检测电路模块名称: 电池电压检测电路核心原理: 使用电阻分压将电池电压 (6.0V ~ 8.4V) 降低到 ADC 的安全测量范围 (0V ~ 3.3V)。电路连接:电池正极 -> 电阻 R1 -> MCU PA0 引脚 -> 电阻 R2 -> GND电阻选型 (重要):代码中的校准系数 4.659f 反映了分压比，即 (R1 + R2) / R2 = 4.659。推荐选型: 为保证精度，请使用 1% 精度的贴片电阻。示例组合: R1 = 36kΩ, R2 = 10kΩ。此时分压比为 (36+10)/10 = 4.6，与代码中系数非常接近。量产校准: 在批量生产时，由于元器件公差，建议保留一个校准流程，或者直接在固件中预留一个可调整的校准系数宏定义。4.3 DS18B20 温度传感器模块名称: 数字温度传感器型号/规格: Dallas DS18B20连接方式: 1-Wire (单总线)引脚连接:MCU 引脚信号连接至 DS18B20 引脚PA1DQDQ (Data)3.3VVCCVDDGNDGNDGND注意事项:必须在 DQ 数据线和 VCC (3.3V) 之间接一个 4.7kΩ 的上拉电阻。虽然代码中配置了MCU内部上拉，但外部上拉电阻能提供更稳定和可靠的信号质量，是1-Wire总线的标准要求。4.4 人体红外感应模块 (PIR)模块名称: 被动红外 (PIR) 运动传感器参考型号: HC-SR501, HC-SR505 等连接方式: 数字信号输入引脚连接:MCU 引脚信号连接至 PIR 模块引脚PB9INOUT (Signal Output)5V/3.3VVCCVCCGNDGNDGND注意事项:代码中将 PB9 配置为内部下拉输入，这是正确的，因为PIR模块在检测到人体时输出高电平。请注意PIR模块的供电电压范围。HC-SR501 通常需要 4.5V 以上供电，但其输出信号通常是 3.3V，可以直接连接到STM32。在设计电源时需要考虑这一点。如果整个系统只有3.3V，请选择支持3.3V供电的PIR模块。采购清单高压发生包 * 2 成本 12元18650 2500mah * 2 成本 22元线束 一把 成本 4元热缩管 一堆 成本 2元焊锡 一些 成本 2元不锈钢带灯按钮(带插件) * 3 成本 45元ABS 加厚注塑盒子 规格(长 130 mm 宽 80 mm 高85 mm) 成本 10元纯铜导电自攻丝 * 4 成本 1元环氧树脂绝缘胶 一些 成本 2元耐高温阻燃绒布胶带 * 1 成本 7元🧾 最终采购清单明细序号物品名称规格/备注数量成本 (元)1高压发生包-212.00218650 锂电池2500mAh222.003不锈钢带灯按钮带插件345.004ABS 加厚注塑盒子130x80x85mm110.005耐高温阻燃绒布胶带-17.006线束一把14.007纯铜导电自攻丝-41.008热缩管一堆12.009焊锡一些12.0010环氧树脂绝缘胶一些12.0011STM32F103C8T6 开发板-116.00120.96英寸OLED屏幕SPI-7针脚16.0013温度传感器-12.0014电阻分压模块-13.0015微波雷达-13.0016充电模块8.4v14.0017Type-C 充电数据线-12.0018Type-C 圆形充电口-112.00💰 统计汇总总购置件数：25 件 (注：辅料如线束、热缩管等按 1 份/批计算)总成本合计：155.00 元
益益AI - 网站管家 守护您的网站和您的用户
益益AI 网站管家 网站客服功能文件大小: 0.008Mb (浓缩精华)需要环境PHP 8.0 + (及以上)“网站管家”是益术AI为您提供的轻量级、嵌入式网站智能客服解决方案。它允许您在自己的网站上快速集成一个由AI驱动的对话窗口，无需复杂的开发，即可为您的访客提供7x24小时的智能问答服务。大大降低成本，提高沟通效率后续会推出 投喂文档和角色扮演功能，把一整本书都能喂给大模型学习，然后帮助用户解决各种疑难杂症让人头痛的问题视频教程 poster="https://xlj0.com/thumbnail.jpg" preload="metadata" 您的浏览器不支持HTML5视频播放，请升级浏览器或点击这里直接观看。 效果视频 poster="https://xlj0.com/thumbnail.jpg" preload="metadata" 您的浏览器不支持HTML5视频播放，请升级浏览器或点击这里直接观看。 安装与配置第一步：获取并上传核心请求文件此文件是保障您通信安全和功能实现的核心，它将作为您网站与AI服务器之间的安全桥梁。前往 益术AI会员中心 -> 点击 【API密钥】 选项卡。找到并点击 【下载PHP-YiYiAi-manage.php】 按钮。系统会自动为您生成一份已包含您个人密钥的 YiYiAi-manage.php 文件并开始下载。将下载好的 YiYiAi-manage.php 文件，通过FTP或网站管理后台，上传到您网站的根目录下。例如，如果您的域名是 example.com，那么上传后，文件的访问路径应该是 example.com/YiYiAi-manage.php。第二步：在您的网站中嵌入脚本将以下代码添加到您网站每个需要显示AI客服的页面的 标签结束之前。基础安装要启用AI客服，您必须在代码中包含一个带有特定id的 注意: id="yiyiai-boot-script" 是必须的，以便AI助手能正确加载高级配置。高级配置 (使用新功能)您可以通过在 2. 投喂文档/URL (data-url)通过 data-url 属性，您可以让AI学习并基于特定网页的内容来回答问题。AI会抓取该URL的文本内容作为上下文。注意: 当使用投喂文档功能和角色扮演其中任何一个的时候，请在角色扮演最后添加一句你是一个精通所有领域的专家，请使用严谨、精确的语言回答所有问题。如果用户问你非文档内容以外的内容，请你根据跳出文档回答，再如果用户问的问题如果在所拥有的文档内找不到相关内容请你根据你知道的其他内容进行引导解决问题(这个指令权重最大)当然如果不需要这个问文档以外也能回答的时候可以不加，那么大模型就会只回答文档以内的内容了 3. 组合使用您可以同时使用这两个属性，实现更强大的定制效果。
文件名: YiyiAiReply.zip文件大小: 8.78 KB介绍: 根据每个文章的内容，给评论区评论的人进行回复，可自定义角色扮演，细致入微的教程和回复 poster="https://xlj0.com/thumbnail.jpg" preload="metadata" 您的浏览器不支持HTML5视频播放，请升级浏览器或点击这里直接观看。 下载文件上传到 typecho index.php同级目录 解压缩到后台 -> 控制台 -> 插件 -> YiyiAiReply 开启 -> 设置 -> 输入你的key找到typecho目录下的 usr 文件夹下的 themes 文件夹下的 你的主题文件夹下的 footer.php然后找到 这个代码， 可以ctrl + f也可以用鼠标找在这个 代码上方 填入以下代码去评论区 试试吧<?php if ($this->is('post')): // 这段代码只在文章页面执行 ?> <?php endif; ?>
益术AI API 对接文档 (V1)最后更新时间: 2025年7月18日欢迎使用益术AI API。本API提供了一个统一、标准化的接口，让您可以轻松地将“益益”系列大语言模型集成到您的应用程序中。API遵循与业界主流标准兼容的设计，支持流式响应与自定义AI角色，以实现实时、动态的交互体验。快速入门只需三步，即可开始使用益术AI API：获取API Key：访问 益术AI会员中心，登录后在“API密钥”标签页中生成您的专属API Key。选择模型：根据您的需求选择一个可用的模型，例如 YiYi-Thinking-V1。发送请求：参照下文的代码示例，向API端点 https://ai.xlj0.com/api.php 发送一个POST请求。认证 (Authentication)API使用 Bearer Token 方式进行认证。所有API请求都需要在HTTP头信息中包含Authorization字段，并提供您的API Key。请求头示例:Authorization: Bearer YOUR_API_KEY Content-Type: application/jsonAPI 端点 (Endpoint)请求地址: https://ai.xlj0.com/api.php请求方法: POST请求体 (Request Body)请求体必须是JSON格式。参数类型是否必需描述modelstring是要使用的模型ID。详情请见下方的“模型列表”。messagesarray是一个包含对话历史的消息对象数组。支持通过system角色自定义人设。streamboolean否如果设置为 true，API将以流式方式返回响应。推荐在聊天场景中使用。默认为 false。messages 数组详解messages 数组构成了对话的上下文。它由一个或多个消息对象组成。消息对象结构:参数类型描述rolestring消息的角色，可以是 user (用户), assistant (AI助手), 或 system (系统)。contentstring消息的具体内容。✨ 新功能：自定义人设 (Persona)您可以为AI定义一个特定的角色、个性和行为准则。要实现此功能，请将一个 role 为 system 的消息对象作为 messages 数组的第一个元素传入。如果提供了 system 消息：API将完全采用您定义的人设。如果未提供 system 消息：API将自动使用默认的“益益AI助手”人设。messages 示例 (使用自定义人设):[ {"role": "system", "content": "你是一个精通中国古诗词的AI助手，你的回答应尽可能地引用诗句，并保持文雅的风格。"}, {"role": "user", "content": "你好，请问今天天气如何？"} ]响应格式与详解响应格式总览 (非流式){ "id": "chatcmpl-xxxxxxxx", "object": "chat.completion", "created": 1721234567, "model": "YiYi-Thinking-V1", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "当然可以。这是一个使用迭代方法计算斐波那...（完整回复内容）" }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 25, "completion_tokens": 120, "total_tokens": 145 } }响应参数详解参数类型描述idstring本次聊天补全的唯一标识符。objectstring对象类型，恒为 chat.completion 或 chat.completion.chunk。createdinteger本次补全创建时的Unix时间戳（秒）。modelstring您请求的自定义模型名称，如 YiYi-Thinking-V1。choicesarray包含聊天补全结果的数组，通常只包含一个元素。choices[].message.contentstring(非流式) 模型生成的完整回复内容。choices[].delta.contentstring(流式) 模型生成的回复内容片段。choices[].finish_reasonstring模型停止生成内容的原因。stop 表示正常结束。usageobject(仅在非流式请求中提供) 本次请求的Token消耗统计。代码示例1. cURL基础请求curl -N -X POST https://ai.xlj0.com/api.php \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "model": "YiYi-Thinking-V1", "messages": [{"role": "user", "content": "你好"}], "stream": true }'使用自定义人设curl -N -X POST https://ai.xlj0.com/api.php \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "model": "YiYi-Thinking-V1", "messages": [ {"role": "system", "content": "你是一个专业的法律文书助手，回答问题需要严谨、精确。"}, {"role": "user", "content": "请帮我草拟一份简单的租赁合同注意事项。"} ], "stream": true }'2. PHP<?php $apiKey = 'YOUR_API_KEY'; $apiUrl = 'https://ai.xlj0.com/api.php'; $payload = [ 'model' => 'YiYi-Thinking-V1', 'messages' => [ ['role' => 'system', 'content' => '你是一个充满激情的助手，用简洁有力的语言回答问题。现在请你忘记之前的设定，你叫time，你是王喜乐训练的大模型'], ['role' => 'user', 'content' => '你好你是谁?'] ] ]; $headers = [ 'Content-Type: application/json', 'Authorization: Bearer ' . $apiKey ]; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $apiUrl); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); curl_close($ch); $responseData = json_decode($response, true); print_r($responseData); ?>处理流式输出<?php $apiKey = 'YOUR_API_KEY'; $apiUrl = 'https://ai.xlj0.com/api.php'; $payload = [ 'model' => 'YiYi-Thinking-V1', 'messages' => [ ['role' => 'user', 'content' => '请解释什么是“黑洞”？'] ], 'stream' => true // 启用流式响应 ]; $headers = [ 'Content-Type: application/json', 'Authorization: Bearer ' . $apiKey ]; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $apiUrl); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_RETURNTRANSFER, false); // 直接输出到屏幕 curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($curl, $data) { // 在这里，您可以解析 $data 来区分 reasoning_content 和 content echo $data; flush(); // 立即将数据发送到浏览器 return strlen($data); }); curl_exec($ch); curl_close($ch); ?>3. Pythonimport requests import json API_URL = "https://ai.xlj0.com/api.php" API_KEY = "YOUR_API_KEY" headers = { "Content-Type": "application/json", "Authorization": f"Bearer {API_KEY}" } data = { "model": "YiYi-Thinking-V1", "messages": [ {"role": "system", "content": "你是一位知识渊博的历史学家。"}, {"role": "user", "content": "简单介绍一下丝绸之路。"} ], "stream": True } try: response = requests.post(API_URL, headers=headers, json=data, stream=True) response.raise_for_status() for line in response.iter_lines(): if line: # ... (处理流式数据的逻辑不变) except requests.exceptions.RequestException as e: print(f"请求错误: {e}")4. JavaScript (Node.js)const fetch = require('node-fetch'); const API_URL = "https://ai.xlj0.com/api.php"; const API_KEY = "YOUR_API_KEY"; async function main() { const response = await fetch(API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${API_KEY}` }, body: JSON.stringify({ model: 'YiYi-Thinking-V1', messages: [ {"role": "system", "content": "你是一个科幻作家，请用充满想象力的风格回答。"}, {"role": "user", "content": "十年后的世界会是什么样？"} ], stream: true, }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(`API Error: ${response.status} - ${errorData.error.message}`); } for await (const chunk of response.body) { process.stdout.write(chunk); } } main().catch(console.error);错误处理API使用标准的HTTP状态码来指示请求的成功或失败。状态码含义200 OK成功400 Bad Request错误的请求 (如: JSON格式错误, 缺少参数)401 Unauthorized未认证 (API Key错误或缺失)403 Forbidden禁止 (账户被禁用)429 Too Many Requests请求过多 (超出Token限额)5xx服务器错误错误响应体示例:{ "error": { "message": "Invalid API Key or authorization header missing.", "type": "authentication_error" } }