Merge pull request #2938 from xinnan-tech/test_live2d

update:测试页面增加切换live2d模型、拍照识物功能
This commit is contained in:
wengzh
2026-02-05 17:00:11 +08:00
committed by GitHub
58 changed files with 21525 additions and 165 deletions
@@ -29,7 +29,15 @@ class VisionHandler(BaseHandler):
def _verify_auth_token(self, request) -> Tuple[bool, Optional[str]]:
"""验证认证token"""
# 测试模式:允许特定测试令牌或跳过验证
auth_header = request.headers.get("Authorization", "")
client_id = request.headers.get("Client-Id", "")
# 允许测试客户端跳过认证
if client_id == "web_test_client":
device_id = request.headers.get("Device-Id", "test_device")
return True, device_id
if not auth_header.startswith("Bearer "):
return False, None
+320 -27
View File
@@ -108,26 +108,68 @@ body {
animation: shadow-float 1.26s ease-in-out infinite;
}
.loading-char-float .main-char span:nth-child(1) { animation-delay: 0s; }
.loading-char-float .main-char span:nth-child(2) { animation-delay: 0.11s; }
.loading-char-float .main-char span:nth-child(3) { animation-delay: 0.22s; }
.loading-char-float .main-char span:nth-child(4) { animation-delay: 0.33s; }
.loading-char-float .main-char span:nth-child(5) { animation-delay: 0.44s; }
.loading-char-float .main-char span:nth-child(6) { animation-delay: 0.55s; }
.loading-char-float .main-char span:nth-child(1) {
animation-delay: 0s;
}
.loading-char-float .main-char span:nth-child(2) {
animation-delay: 0.11s;
}
.loading-char-float .main-char span:nth-child(3) {
animation-delay: 0.22s;
}
.loading-char-float .main-char span:nth-child(4) {
animation-delay: 0.33s;
}
.loading-char-float .main-char span:nth-child(5) {
animation-delay: 0.44s;
}
.loading-char-float .main-char span:nth-child(6) {
animation-delay: 0.55s;
}
@keyframes scan {
0% { background-position: 0 0; }
100% { background-position: 0 100px; }
0% {
background-position: 0 0;
}
100% {
background-position: 0 100px;
}
}
@keyframes char-float {
0%, 100% { transform: translateY(0); opacity: 1; text-shadow: 0 0 5px #5865f2, 0 0 10px rgba(88, 101, 242, 0.8); }
50% { transform: translateY(-5px); opacity: 0.9; text-shadow: 0 0 10px #5865f2, 0 0 20px rgba(88, 101, 242, 0.8); }
0%,
100% {
transform: translateY(0);
opacity: 1;
text-shadow: 0 0 5px #5865f2, 0 0 10px rgba(88, 101, 242, 0.8);
}
50% {
transform: translateY(-5px);
opacity: 0.9;
text-shadow: 0 0 10px #5865f2, 0 0 20px rgba(88, 101, 242, 0.8);
}
}
@keyframes shadow-float {
0%, 100% { transform: translateY(3px); opacity: 0.3; }
50% { transform: translateY(8px); opacity: 0.15; }
0%,
100% {
transform: translateY(3px);
opacity: 0.3;
}
50% {
transform: translateY(8px);
opacity: 0.15;
}
}
/* ==================== Live2D显示区域 ==================== */
@@ -402,6 +444,15 @@ body {
background: rgba(237, 66, 69, 1);
}
/* 摄像头按钮特殊样式 */
#cameraBtn.camera-active {
background: rgba(237, 66, 69, 0.9);
}
#cameraBtn.camera-active:hover {
background: rgba(237, 66, 69, 1);
}
/* 录音按钮脉冲动画 */
@keyframes pulse {
0% {
@@ -761,6 +812,72 @@ body {
font-size: 14px;
}
.model-select {
width: 100%;
padding: 10px 40px 10px 14px;
border: 1px solid #40444b;
border-radius: 8px;
background: #40444b;
color: #ffffff;
font-size: 14px;
font-weight: 500;
cursor: pointer;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='%23ffffff'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 16px 16px;
}
.model-select:hover {
border-color: #5865f2;
}
.model-select:focus {
outline: none;
border-color: #5865f2;
}
.model-select option {
background: #2f3136;
color: #ffffff;
padding: 12px;
font-size: 14px;
}
.background-btn {
width: 100%;
padding: 10px 14px;
border: 1px solid #40444b;
border-radius: 8px;
background: #40444b;
color: #ffffff;
font-size: 14px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.3s ease;
}
.background-btn:hover {
background: #4f545c;
}
.background-btn:active {
background: #40444b;
}
.background-btn .btn-icon {
width: 20px;
height: 20px;
fill: #ffffff;
}
/* ==================== MCP工具样式 ==================== */
.mcp-tools-container {
background: #2f3136;
@@ -911,35 +1028,174 @@ body {
margin-bottom: 12px;
}
.property-item {
.properties-container #addMcpPropertyBtn {
display: block;
margin: 0 auto;
}
.properties-container #addMcpPropertyBtn:hover {
background: #4752c4;
}
.mcp-checkbox-label {
display: flex;
gap: 8px;
margin-bottom: 8px;
align-items: center;
gap: 8px;
margin-top: 8px;
color: #b9bbbe;
font-size: 13px;
cursor: pointer;
justify-content: flex-start;
}
.property-item input {
flex: 1;
padding: 6px 8px;
border: 1px solid #40444b;
border-radius: 4px;
background: #40444b;
.mcp-checkbox-label input[type="checkbox"] {
width: 16px;
height: 16px;
cursor: pointer;
}
.mcp-property-name {
color: white;
font-size: 12px;
font-size: 14px;
font-weight: 500;
}
.remove-property {
background: #ed4245;
.mcp-property-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.mcp-properties-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
min-height: 60px;
padding: 8px;
}
.input-group-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.input-group-header label {
margin-bottom: 0;
}
.input-group-header .properties-btn-primary {
margin: 0;
}
.mcp-empty-state {
text-align: center;
padding: 20px;
color: #999;
font-size: 14px;
width: 100%;
display: none;
}
.properties-btn-primary {
background: #2196f3;
color: white;
border: none;
padding: 4px 8px;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
transition: background-color 0.2s;
display: block;
margin: 0 auto;
}
.remove-property:hover {
background: #c03537;
.properties-btn-primary:hover {
background: #2196f3;
}
.mcp-property-card {
background: #36393f;
border: 1px solid #40444b;
border-radius: 8px;
padding: 12px;
width: 100%;
cursor: pointer;
transition: all 0.2s ease;
}
.mcp-property-card:hover {
background: #40444b;
border-color: #5865f2;
}
.mcp-property-card:active {
transform: none;
}
.mcp-property-row-label {
display: flex;
align-items: flex-start;
margin-bottom: 6px;
gap: 8px;
}
.mcp-property-label {
color: #b9bbbe;
font-size: 13px;
width: 60px;
flex-shrink: 0;
text-align: left;
}
.mcp-property-value {
color: white;
font-size: 13px;
flex: 1;
word-break: break-all;
}
.mcp-property-required-badge {
color: #f44336;
font-size: 12px;
}
.mcp-property-row-action {
display: flex;
justify-content: flex-end;
margin-top: 6px;
padding-top: 6px;
border-top: 1px solid #40444b;
}
.mcp-property-delete-btn {
background: #f44336;
color: white;
border: none;
padding: 4px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 11px;
transition: background-color 0.2s;
}
.mcp-property-delete-btn:hover {
background: #d32f2f;
}
.mcp-property-card-optional {
background: #4f545c;
color: #b9bbbe;
font-size: 11px;
padding: 2px 8px;
border-radius: 4px;
}
.property-modal {
max-width: 450px;
}
/* ==================== 音频可视化器样式 ==================== */
@@ -1125,4 +1381,41 @@ body {
.modal-body::-webkit-scrollbar-thumb:hover,
.mcp-tools-list::-webkit-scrollbar-thumb:hover {
background: #4f545c;
}
/* ==================== 摄像头显示区域样式 ==================== */
.camera-container {
position: fixed;
top: 20px;
left: 20px;
width: 240px;
height: 180px;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(10px);
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.2);
z-index: 1000;
display: none;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
cursor: move;
}
.camera-container.active {
display: block;
}
.camera-container.dragging {
cursor: grabbing;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4);
border-color: rgba(88, 101, 242, 0.5);
}
#cameraVideo {
width: 100%;
height: 100%;
object-fit: cover;
background: #1a1a1a;
pointer-events: none;
transform: scaleX(-1);
}
+215
View File
@@ -6,12 +6,25 @@ import { initMcpTools } from './core/mcp/tools.js?v=0127';
import { uiController } from './ui/controller.js?v=0127';
import { log } from './utils/logger.js?v=0127';
// 辅助函数:将Base64数据转换为Blob
function dataURItoBlob(dataURI) {
const byteString = atob(dataURI.split(',')[1]);
const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
return new Blob([ab], { type: mimeString });
}
// 应用类
class App {
constructor() {
this.uiController = null;
this.audioPlayer = null;
this.live2dManager = null;
this.cameraStream = null;
}
// 初始化应用
@@ -31,8 +44,12 @@ class App {
initMcpTools();
// 检查麦克风可用性
await this.checkMicrophoneAvailability();
// 检查摄像头可用性
this.checkCameraAvailability();
// 初始化Live2D
await this.initLive2D();
// 初始化摄像头
this.initCamera();
// 关闭加载loading
this.setModelLoadingStatus(false);
log('应用初始化完成', 'success');
@@ -99,6 +116,204 @@ class App {
}
}
}
// 检查摄像头可用性
checkCameraAvailability() {
window.cameraAvailable = true;
log('摄像头可用性检查完成: 默认已绑定验证码', 'success');
}
// 初始化摄像头
async initCamera() {
const cameraContainer = document.getElementById('cameraContainer');
const cameraVideo = document.getElementById('cameraVideo');
if (!cameraContainer || !cameraVideo) {
log('摄像头元素未找到,跳过初始化', 'warning');
return Promise.resolve(false);
}
let isDragging = false;
let currentX, currentY, initialX, initialY;
let xOffset = 0, yOffset = 0;
cameraContainer.addEventListener('mousedown', dragStart);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
cameraContainer.addEventListener('touchstart', dragStart, { passive: false });
document.addEventListener('touchmove', drag, { passive: false });
document.addEventListener('touchend', dragEnd);
function dragStart(e) {
if (e.type === 'touchstart') {
initialX = e.touches[0].clientX - xOffset;
initialY = e.touches[0].clientY - yOffset;
} else {
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
}
isDragging = true;
cameraContainer.classList.add('dragging');
}
function drag(e) {
if (isDragging) {
e.preventDefault();
if (e.type === 'touchmove') {
currentX = e.touches[0].clientX - initialX;
currentY = e.touches[0].clientY - initialY;
} else {
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
}
xOffset = currentX;
yOffset = currentY;
cameraContainer.style.transform = `translate3d(${currentX}px, ${currentY}px, 0)`;
}
}
function dragEnd() {
initialX = currentX;
initialY = currentY;
isDragging = false;
cameraContainer.classList.remove('dragging');
}
return new Promise((resolve) => {
window.startCamera = async () => {
try {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
log('浏览器不支持摄像头API', 'warning');
return false;
}
log('正在请求摄像头权限...', 'info');
this.cameraStream = await navigator.mediaDevices.getUserMedia({
video: { width: 320, height: 240, facingMode: 'user' },
audio: false
});
cameraVideo.srcObject = this.cameraStream;
cameraContainer.classList.add('active');
log('摄像头已启动', 'success');
return true;
} catch (error) {
log(`启动摄像头失败: ${error.name} - ${error.message}`, 'error');
if (error.name === 'NotAllowedError') {
log('摄像头权限被拒绝,请检查浏览器设置', 'warning');
} else if (error.name === 'NotFoundError') {
log('未找到摄像头设备', 'warning');
} else if (error.name === 'NotReadableError') {
log('摄像头已被其他程序占用', 'warning');
}
return false;
}
};
window.stopCamera = () => {
if (this.cameraStream) {
this.cameraStream.getTracks().forEach(track => track.stop());
this.cameraStream = null;
cameraVideo.srcObject = null;
log('摄像头已关闭', 'info');
}
};
window.takePhoto = (question = '描述一下看到的物品') => {
return new Promise(async (resolve) => {
const canvas = document.createElement('canvas');
const video = cameraVideo;
if (!video || video.readyState !== video.HAVE_ENOUGH_DATA) {
log('无法拍照:摄像头未就绪', 'warning');
resolve({
success: false,
error: '摄像头未就绪,请确保已连接且摄像头已启动'
});
return;
}
canvas.width = video.videoWidth || 320;
canvas.height = video.videoHeight || 240;
const ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const photoData = canvas.toDataURL('image/jpeg', 0.8);
log(`拍照成功,图像数据长度: ${photoData.length}`, 'success');
try {
const xz_tester_vision = localStorage.getItem('xz_tester_vision');
if (xz_tester_vision) {
let visionInfo = null;
try {
visionInfo = JSON.parse(xz_tester_vision);
} catch (err) {
throw new Error(`视觉配置解析失败`);
}
const { url, token } = visionInfo || {};
if (!url || !token) {
throw new Error('视觉分析失败:配置缺少接口地址(url)或令牌(token)');
}
log(`正在发送图片到视觉分析接口: ${url}`, 'info');
const deviceId = document.getElementById('deviceMac')?.value || '';
const clientId = document.getElementById('clientId')?.value || 'web_test_client';
const formData = new FormData();
formData.append('question', question);
formData.append('image', dataURItoBlob(photoData), 'photo.jpg');
const response = await fetch(url, {
method: 'POST',
body: formData,
headers: {
'Device-Id': deviceId,
'Client-Id': clientId,
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const analysisResult = await response.json();
log(`视觉分析完成: ${JSON.stringify(analysisResult).substring(0, 200)}...`, 'success');
resolve({
success: true,
message: question,
photo_data: photoData,
photo_width: canvas.width,
photo_height: canvas.height,
vision_analysis: analysisResult
});
} else {
log('未配置视觉分析服务', 'warning');
}
} catch (error) {
log(`视觉分析失败: ${error.message}`, 'error');
resolve({
success: true,
message: question,
photo_data: photoData,
photo_width: canvas.width,
photo_height: canvas.height,
vision_analysis: {
success: false,
error: error.message,
fallback: '无法连接到视觉分析服务'
}
});
}
});
};
log('摄像头初始化完成', 'success');
resolve(true);
});
}
}
// 创建并启动应用
@@ -1,4 +1,22 @@
[
{
"name": "self_camera_take_photo",
"description": "Take a photo using the device's camera. This tool captures the current camera frame and returns the image data. You MUST call this tool when user asks to 'take a photo', 'what do you see', 'describe what you see', or similar requests. After getting the photo data, describe what you see in the image. Parameter 'question' is optional, defaults to 'describe objects seen'.",
"inputSchema": {
"type": "object",
"properties": {
"question": {
"type": "string",
"description": "Question to guide the photo analysis, e.g., 'describe the objects seen'. Can be empty."
}
}
},
"mockResponse": {
"success": true,
"photo_data": "base64_image_data",
"response": "Please describe what you see based on the returned photo_data"
}
},
{
"name": "self.get_device_status",
"description": "Provides the real-time information of the device, including the current status of the audio speaker, screen, battery, network, etc.\nUse this tool for: \n1. Answering questions about current condition (e.g. what is the current volume of the audio speaker?)\n2. As the first step to control the device (e.g. turn up / down the volume of the audio speaker, etc.)",
+183 -68
View File
@@ -27,7 +27,16 @@ export async function initMcpTools() {
const savedTools = localStorage.getItem('mcpTools');
if (savedTools) {
try {
mcpTools = JSON.parse(savedTools);
const parsedTools = JSON.parse(savedTools);
// 合并默认工具和用户保存的工具,保留用户自定义的工具
const defaultToolNames = new Set(defaultMcpTools.map(t => t.name));
// 添加默认工具中不存在的新工具
parsedTools.forEach(tool => {
if (!defaultToolNames.has(tool.name)) {
defaultMcpTools.push(tool);
}
});
mcpTools = defaultMcpTools;
} catch (e) {
log('加载MCP工具失败,使用默认工具', 'warning');
mcpTools = [...defaultMcpTools];
@@ -96,94 +105,167 @@ function renderMcpTools() {
*/
function renderMcpProperties() {
const container = document.getElementById('mcpPropertiesContainer');
const emptyState = document.getElementById('mcpEmptyState');
if (!container) {
return; // Container not found, skip rendering
}
if (mcpProperties.length === 0) {
container.innerHTML = '<div style="text-align: center; padding: 20px; color: #999; font-size: 14px;">暂无参数,点击下方按钮添加参数</div>';
if (emptyState) {
emptyState.style.display = 'block';
}
container.innerHTML = '';
return;
}
if (emptyState) {
emptyState.style.display = 'none';
}
container.innerHTML = mcpProperties.map((prop, index) => `
<div class="mcp-property-item">
<div class="mcp-property-header">
<span class="mcp-property-name">${prop.name}</span>
<button type="button" onclick="window.mcpModule.deleteMcpProperty(${index})"
style="padding: 3px 8px; border: none; border-radius: 3px; background-color: #f44336; color: white; cursor: pointer; font-size: 11px;">
删除
</button>
<div class="mcp-property-card" onclick="window.mcpModule.editMcpProperty(${index})">
<div class="mcp-property-row-label">
<span class="mcp-property-label">参数名称</span>
<span class="mcp-property-value">${prop.name}${prop.required ? ' <span class="mcp-property-required-badge">[必填]</span>' : ''}</span>
</div>
<div class="mcp-property-row">
<div>
<label class="mcp-small-label">参数名称 *</label>
<input type="text" class="mcp-small-input" value="${prop.name}"
onchange="window.mcpModule.updateMcpProperty(${index}, 'name', this.value)" required>
</div>
<div>
<label class="mcp-small-label">数据类型 *</label>
<select class="mcp-small-input" onchange="window.mcpModule.updateMcpProperty(${index}, 'type', this.value)">
<option value="string" ${prop.type === 'string' ? 'selected' : ''}>字符串</option>
<option value="integer" ${prop.type === 'integer' ? 'selected' : ''}>整数</option>
<option value="number" ${prop.type === 'number' ? 'selected' : ''}>数字</option>
<option value="boolean" ${prop.type === 'boolean' ? 'selected' : ''}>布尔值</option>
<option value="array" ${prop.type === 'array' ? 'selected' : ''}>数组</option>
<option value="object" ${prop.type === 'object' ? 'selected' : ''}>对象</option>
</select>
</div>
<div class="mcp-property-row-label">
<span class="mcp-property-label">数据类型</span>
<span class="mcp-property-value">${getTypeLabel(prop.type)}</span>
</div>
${(prop.type === 'integer' || prop.type === 'number') ? `
<div class="mcp-property-row">
<div>
<label class="mcp-small-label">最小值</label>
<input type="number" class="mcp-small-input" value="${prop.minimum !== undefined ? prop.minimum : ''}"
placeholder="可选" onchange="window.mcpModule.updateMcpProperty(${index}, 'minimum', this.value ? parseFloat(this.value) : undefined)">
</div>
<div>
<label class="mcp-small-label">最大值</label>
<input type="number" class="mcp-small-input" value="${prop.maximum !== undefined ? prop.maximum : ''}"
placeholder="可选" onchange="window.mcpModule.updateMcpProperty(${index}, 'maximum', this.value ? parseFloat(this.value) : undefined)">
</div>
<div class="mcp-property-row-label">
<span class="mcp-property-label">描述</span>
<span class="mcp-property-value">${prop.description || '-'}</span>
</div>
` : ''}
<div class="mcp-property-row-full">
<label class="mcp-small-label">参数描述</label>
<input type="text" class="mcp-small-input" value="${prop.description || ''}"
placeholder="可选" onchange="window.mcpModule.updateMcpProperty(${index}, 'description', this.value)">
<div class="mcp-property-row-action">
<button class="mcp-property-delete-btn" onclick="event.stopPropagation(); window.mcpModule.deleteMcpProperty(${index})">删除</button>
</div>
<label class="mcp-checkbox-label">
<input type="checkbox" ${prop.required ? 'checked' : ''}
onchange="window.mcpModule.updateMcpProperty(${index}, 'required', this.checked)">
必填参数
</label>
</div>
`).join('');
}
/**
* 添加参数
* 获取数据类型标签
*/
function addMcpProperty() {
mcpProperties.push({ name: `param_${mcpProperties.length + 1}`, type: 'string', required: false, description: '' });
renderMcpProperties();
function getTypeLabel(type) {
const typeMap = {
'string': '字符串',
'integer': '整数',
'number': '数字',
'boolean': '布尔值',
'array': '数组',
'object': '对象'
};
return typeMap[type] || type;
}
/**
* 更新参数
* 添加参数 - 打开参数编辑模态框
*/
function updateMcpProperty(index, field, value) {
if (field === 'name') {
const isDuplicate = mcpProperties.some((p, i) => i !== index && p.name === value);
if (isDuplicate) {
alert('参数名称已存在,请使用不同的名称');
renderMcpProperties();
return;
function addMcpProperty() {
openPropertyModal();
}
/**
* 编辑参数 - 打开参数编辑模态框
*/
function editMcpProperty(index) {
openPropertyModal(index);
}
/**
* 打开参数编辑模态框
*/
function openPropertyModal(index = null) {
const form = document.getElementById('mcpPropertyForm');
const title = document.getElementById('mcpPropertyModalTitle');
document.getElementById('mcpPropertyIndex').value = index !== null ? index : -1;
if (index !== null) {
const prop = mcpProperties[index];
title.textContent = '编辑参数';
document.getElementById('mcpPropertyName').value = prop.name;
document.getElementById('mcpPropertyType').value = prop.type || 'string';
document.getElementById('mcpPropertyMinimum').value = prop.minimum !== undefined ? prop.minimum : '';
document.getElementById('mcpPropertyMaximum').value = prop.maximum !== undefined ? prop.maximum : '';
document.getElementById('mcpPropertyDescription').value = prop.description || '';
document.getElementById('mcpPropertyRequired').checked = prop.required || false;
} else {
title.textContent = '添加参数';
form.reset();
document.getElementById('mcpPropertyName').value = `param_${mcpProperties.length + 1}`;
document.getElementById('mcpPropertyType').value = 'string';
document.getElementById('mcpPropertyMinimum').value = '';
document.getElementById('mcpPropertyMaximum').value = '';
document.getElementById('mcpPropertyDescription').value = '';
document.getElementById('mcpPropertyRequired').checked = false;
}
updatePropertyRangeVisibility();
document.getElementById('mcpPropertyModal').style.display = 'flex';
}
/**
* 关闭参数编辑模态框
*/
function closePropertyModal() {
document.getElementById('mcpPropertyModal').style.display = 'none';
}
/**
* 更新数值范围输入框的可见性
*/
function updatePropertyRangeVisibility() {
const type = document.getElementById('mcpPropertyType').value;
const rangeGroup = document.getElementById('mcpPropertyRangeGroup');
if (type === 'integer' || type === 'number') {
rangeGroup.style.display = 'block';
} else {
rangeGroup.style.display = 'none';
}
}
/**
* 处理参数表单提交
*/
function handlePropertySubmit(e) {
e.preventDefault();
const index = parseInt(document.getElementById('mcpPropertyIndex').value);
const name = document.getElementById('mcpPropertyName').value.trim();
const type = document.getElementById('mcpPropertyType').value;
const minimum = document.getElementById('mcpPropertyMinimum').value;
const maximum = document.getElementById('mcpPropertyMaximum').value;
const description = document.getElementById('mcpPropertyDescription').value.trim();
const required = document.getElementById('mcpPropertyRequired').checked;
// 检查名称重复
const isDuplicate = mcpProperties.some((p, i) => i !== index && p.name === name);
if (isDuplicate) {
alert('参数名称已存在,请使用不同的名称');
return;
}
const propData = {
name,
type,
description,
required
};
// 数值类型添加范围限制
if (type === 'integer' || type === 'number') {
if (minimum !== '') {
propData.minimum = parseFloat(minimum);
}
if (maximum !== '') {
propData.maximum = parseFloat(maximum);
}
}
mcpProperties[index][field] = value;
if (field === 'type' && value !== 'integer' && value !== 'number') {
delete mcpProperties[index].minimum;
delete mcpProperties[index].maximum;
renderMcpProperties();
if (index >= 0) {
mcpProperties[index] = propData;
} else {
mcpProperties.push(propData);
}
renderMcpProperties();
closePropertyModal();
}
/**
@@ -206,6 +288,14 @@ function setupMcpEventListeners() {
const cancelBtn = document.getElementById('cancelMcpBtn');
const form = document.getElementById('mcpToolForm');
const addPropertyBtn = document.getElementById('addMcpPropertyBtn');
// 参数编辑模态框相关元素
const propertyModal = document.getElementById('mcpPropertyModal');
const closePropertyBtn = document.getElementById('closeMcpPropertyModalBtn');
const cancelPropertyBtn = document.getElementById('cancelMcpPropertyBtn');
const propertyForm = document.getElementById('mcpPropertyForm');
const propertyTypeSelect = document.getElementById('mcpPropertyType');
// Return early if required elements don't exist (e.g., in test environment)
if (!toggleBtn || !panel || !addBtn || !modal || !closeBtn || !cancelBtn || !form || !addPropertyBtn) {
return;
@@ -225,6 +315,17 @@ function setupMcpEventListeners() {
if (e.target === modal) closeMcpModal();
});
form.addEventListener('submit', handleMcpSubmit);
// 参数编辑模态框事件
if (propertyModal && closePropertyBtn && cancelPropertyBtn && propertyForm && propertyTypeSelect) {
closePropertyBtn.addEventListener('click', closePropertyModal);
cancelPropertyBtn.addEventListener('click', closePropertyModal);
propertyModal.addEventListener('click', (e) => {
if (e.target === propertyModal) closePropertyModal();
});
propertyForm.addEventListener('submit', handlePropertySubmit);
propertyTypeSelect.addEventListener('change', updatePropertyRangeVisibility);
}
}
/**
@@ -266,7 +367,7 @@ function openMcpModal(index = null) {
mcpProperties = [];
}
renderMcpProperties();
document.getElementById('mcpToolModal').style.display = 'block';
document.getElementById('mcpToolModal').style.display = 'flex';
}
/**
@@ -392,12 +493,26 @@ export function getMcpTools() {
/**
* 执行工具调用
*/
export function executeMcpTool(toolName, toolArgs) {
export async function executeMcpTool(toolName, toolArgs) {
const tool = mcpTools.find(t => t.name === toolName);
if (!tool) {
log(`未找到工具: ${toolName}`, 'error');
return { success: false, error: `未知工具: ${toolName}` };
}
// 处理拍照工具
if (toolName === 'self_camera_take_photo') {
if (typeof window.takePhoto === 'function') {
const question = toolArgs && toolArgs.question ? toolArgs.question : '描述一下看到的物品';
log(`正在执行拍照: ${question}`, 'info');
const result = await window.takePhoto(question);
return result;
} else {
log('拍照功能不可用', 'warning');
return { success: false, error: '摄像头未启动或不支持拍照功能' };
}
}
// 如果有模拟返回结果,使用它
if (tool.mockResponse) {
// 替换模板变量
@@ -424,4 +539,4 @@ export function executeMcpTool(toolName, toolArgs) {
}
// 暴露全局方法供 HTML 内联事件调用
window.mcpModule = { updateMcpProperty, deleteMcpProperty, editMcpTool, deleteMcpTool };
window.mcpModule = { addMcpProperty, editMcpProperty, deleteMcpProperty, editMcpTool, deleteMcpTool };
@@ -74,6 +74,9 @@ export class WebSocketHandler {
handleTextMessage(message) {
if (message.type === 'hello') {
log(`服务器回应:${JSON.stringify(message, null, 2)}`, 'success');
window.cameraAvailable = true;
log('连接成功,摄像头已可用', 'success');
uiController.updateDialButton(true);
uiController.startAIChatSession();
} else if (message.type === 'tts') {
this.handleTTSMessage(message);
@@ -81,6 +84,23 @@ export class WebSocketHandler {
log(`收到音频控制消息: ${JSON.stringify(message)}`, 'info');
} else if (message.type === 'stt') {
log(`识别结果: ${message.text}`, 'info');
// 检查是否需要绑定设备
if (message.text && (message.text.includes('绑定') || message.text.includes('bind'))) {
log('收到设备绑定提示,更新摄像头状态', 'warning');
window.cameraAvailable = false;
// 关闭摄像头
if (typeof window.stopCamera === 'function') {
window.stopCamera();
}
// 更新摄像头按钮状态
const cameraBtn = document.getElementById('cameraBtn');
if (cameraBtn) {
cameraBtn.classList.remove('camera-active');
cameraBtn.querySelector('.btn-text').textContent = '摄像头';
cameraBtn.disabled = true;
cameraBtn.title = '请先绑定验证码';
}
}
// 使用新的聊天消息回调显示STT消息
if (this.onChatMessage && message.text) {
this.onChatMessage(message.text, true);
@@ -249,30 +269,57 @@ export class WebSocketHandler {
log(`调用工具: ${toolName} 参数: ${JSON.stringify(toolArgs)}`, 'info');
const result = executeMcpTool(toolName, toolArgs);
const replyMessage = JSON.stringify({
"session_id": message.session_id || "",
"type": "mcp",
"payload": {
"jsonrpc": "2.0",
"id": payload.id,
"result": {
"content": [
{
"type": "text",
"text": JSON.stringify(result)
}
],
"isError": false
executeMcpTool(toolName, toolArgs).then(result => {
const replyMessage = JSON.stringify({
"session_id": message.session_id || "",
"type": "mcp",
"payload": {
"jsonrpc": "2.0",
"id": payload.id,
"result": {
"content": [
{
"type": "text",
"text": JSON.stringify(result)
}
],
"isError": false
}
}
}
});
});
log(`客户端上报: ${replyMessage}`, 'info');
this.websocket.send(replyMessage);
log(`客户端上报: ${replyMessage}`, 'info');
this.websocket.send(replyMessage);
}).catch(error => {
log(`工具执行失败: ${error.message}`, 'error');
const errorReply = JSON.stringify({
"session_id": message.session_id || "",
"type": "mcp",
"payload": {
"jsonrpc": "2.0",
"id": payload.id,
"error": {
"code": -32603,
"message": error.message
}
}
});
this.websocket.send(errorReply);
});
} else if (payload.method === 'initialize') {
log(`收到工具初始化请求: ${JSON.stringify(payload.params)}`, 'info');
// 保存视觉分析接口地址
const visionUrl = document.getElementById('visionUrl');
const visionConfig = payload?.params?.capabilities?.vision;
if (visionConfig && typeof visionConfig === 'object' && visionConfig.url && visionConfig.token) {
const visionConfigStr = JSON.stringify(visionConfig);
localStorage.setItem('xz_tester_vision', visionConfigStr);
if (visionUrl) visionUrl.value = visionConfig.url;
} else {
localStorage.removeItem('xz_tester_vision');
if (visionUrl) visionUrl.value = '';
}
const replyMessage = JSON.stringify({
"session_id": message.session_id || "",
"type": "mcp",
@@ -387,6 +434,17 @@ export class WebSocketHandler {
const audioRecorder = getAudioRecorder();
audioRecorder.stop();
// 关闭摄像头
if (typeof window.stopCamera === 'function') {
window.stopCamera();
}
// 隐藏摄像头显示区域
const cameraContainer = document.getElementById('cameraContainer');
if (cameraContainer) {
cameraContainer.classList.remove('active');
}
};
this.websocket.onerror = (error) => {
@@ -420,6 +478,17 @@ export class WebSocketHandler {
this.websocket.close();
const audioRecorder = getAudioRecorder();
audioRecorder.stop();
// 关闭摄像头
if (typeof window.stopCamera === 'function') {
window.stopCamera();
}
// 隐藏摄像头显示区域
const cameraContainer = document.getElementById('cameraContainer');
if (cameraContainer) {
cameraContainer.classList.remove('active');
}
}
// 发送文本消息
+338 -20
View File
@@ -12,7 +12,42 @@ class Live2DManager {
this.audioContext = null;
this.analyser = null;
this.dataArray = null;
this.lastEmotionActionTime = null; // 上次情绪触发动作的时间
this.lastEmotionActionTime = null;
this.currentModelName = null;
// 模型特定配置
this.modelConfig = {
'hiyori_pro_zh': {
mouthParam: 'ParamMouthOpenY',
mouthAmplitude: 1.0,
mouthThresholds: { low: 0.3, high: 0.7 },
motionMap: {
'FlickUp': 'FlickUp',
'FlickDown': 'FlickDown',
'Tap': 'Tap',
'Tap@Body': 'Tap@Body',
'Flick': 'Flick',
'Flick@Body': 'Flick@Body'
}
},
'natori_pro_zh': {
mouthParam: 'ParamMouthOpenY',
mouthAmplitude: 1.0,
mouthThresholds: { low: 0.1, high: 0.4 },
mouthFormParam: 'ParamMouthForm',
mouthFormAmplitude: 1.0,
mouthForm2Param: 'ParamMouthForm2',
mouthForm2Amplitude: 0.8,
motionMap: {
'FlickUp': 'FlickUp',
'FlickDown': 'Flick@Body',
'Tap': 'Tap',
'Tap@Body': 'Tap@Head',
'Flick': 'Tap',
'Flick@Body': 'Flick@Body'
}
}
};
// 情绪到动作的映射
this.emotionToActionMap = {
@@ -67,10 +102,33 @@ class Live2DManager {
const currentPath = window.location.pathname;
const lastSlashIndex = currentPath.lastIndexOf('/');
const basePath = currentPath.substring(0, lastSlashIndex + 1);
const modelPath = basePath + 'hiyori_pro_zh/runtime/hiyori_pro_t11.model3.json';
// 从 localStorage 读取上次选择的模型,如果没有则使用默认
const savedModelName = localStorage.getItem('live2dModel') || 'hiyori_pro_zh';
const modelFileMap = {
'hiyori_pro_zh': 'hiyori_pro_t11.model3.json',
'natori_pro_zh': 'natori_pro_t06.model3.json'
};
const modelFileName = modelFileMap[savedModelName] || 'hiyori_pro_t11.model3.json';
const modelPath = basePath + 'resources/' + savedModelName + '/runtime/' + modelFileName;
this.live2dModel = await PIXI.live2d.Live2DModel.from(modelPath);
this.live2dApp.stage.addChild(this.live2dModel);
// 保存当前模型名称
this.currentModelName = savedModelName;
// 更新下拉框显示
const modelSelect = document.getElementById('live2dModelSelect');
if (modelSelect) {
modelSelect.value = savedModelName;
}
// 设置模型特定的嘴部参数名
if (this.modelConfig[savedModelName]) {
this.mouthParam = this.modelConfig[savedModelName].mouthParam || 'ParamMouthOpenY';
}
// 设置模型属性
this.live2dModel.scale.set(0.33);
this.live2dModel.x = (window.innerWidth - this.live2dModel.width) * 0.5;
@@ -364,33 +422,88 @@ class Live2DManager {
if (internal && internal.coreModel) {
const coreModel = internal.coreModel;
// 获取音频分贝值
let mouthValue = 0;
let mouthOpenY = 0;
let mouthForm = 0;
let mouthForm2 = 0;
let average = 0;
if (this.analyser && this.dataArray) {
this.analyser.getByteFrequencyData(this.dataArray);
average = this.dataArray.reduce((a, b) => a + b) / this.dataArray.length;
// 优化音量映射函数,使中等音量范围变化更明显
// 使用S形曲线函数,在中等音量范围有更好的响应
const normalizedVolume = average / 255;
// S形曲线:在0.3-0.7范围内有最大的斜率(变化最明显)
if (normalizedVolume < 0.3) {
// 低音量:缓慢增长
mouthValue = Math.pow(normalizedVolume / 0.3, 1.5) * 0.3;
} else if (normalizedVolume < 0.7) {
// 中等音量:线性增长,变化最明显
mouthValue = 0.3 + (normalizedVolume - 0.3) / 0.4 * 0.5;
} else {
// 高音量:缓慢接近最大值
mouthValue = 0.8 + Math.pow((normalizedVolume - 0.7) / 0.3, 1.2) * 0.2;
// 获取模型特定的阈值
let lowThreshold = 0.3;
let highThreshold = 0.7;
if (this.currentModelName && this.modelConfig[this.currentModelName]) {
lowThreshold = this.modelConfig[this.currentModelName].mouthThresholds?.low || 0.3;
highThreshold = this.modelConfig[this.currentModelName].mouthThresholds?.high || 0.7;
}
// 确保嘴部参数在0-1范围内
mouthValue = Math.min(Math.max(mouthValue, 0), 1);
// 使用模型特定的阈值进行映射
let minOpenY = 0.1;
if (this.currentModelName && this.modelConfig[this.currentModelName]) {
minOpenY = this.modelConfig[this.currentModelName].mouthMinOpenY || 0.1;
}
if (normalizedVolume < lowThreshold) {
mouthOpenY = minOpenY + Math.pow(normalizedVolume / lowThreshold, 1.5) * (0.4 - minOpenY);
} else if (normalizedVolume < highThreshold) {
mouthOpenY = 0.4 + (normalizedVolume - lowThreshold) / (highThreshold - lowThreshold) * 0.4;
} else {
mouthOpenY = 0.8 + Math.pow((normalizedVolume - highThreshold) / (1 - highThreshold), 1.2) * 0.2;
}
// 应用模型特定的嘴部开合幅度
let amplitudeMultiplier = 1.0;
let maxOpenY = 2.5;
if (this.currentModelName && this.modelConfig[this.currentModelName]) {
amplitudeMultiplier = this.modelConfig[this.currentModelName].mouthAmplitude;
maxOpenY = this.modelConfig[this.currentModelName].maxOpenY || 2.5;
}
mouthOpenY = mouthOpenY * amplitudeMultiplier;
mouthOpenY = Math.min(Math.max(mouthOpenY, 0), maxOpenY);
// 计算嘴型参数(仅对支持嘴型变化的模型)
if (this.currentModelName && this.modelConfig[this.currentModelName]?.mouthFormParam) {
const config = this.modelConfig[this.currentModelName];
const formAmplitude = config.mouthFormAmplitude || 0.5;
const form2Amplitude = config.mouthForm2Amplitude || 0;
// 嘴型随音量变化:
// 低音量:嘴型偏"一"字(负值)
// 高音量:嘴型偏"o"字(正值)
// 音量=0时:嘴型=0(自然状态)
mouthForm = (normalizedVolume - 0.5) * 2 * formAmplitude;
mouthForm = Math.max(-formAmplitude, Math.min(formAmplitude, mouthForm));
// 第二嘴型参数(natori特有)
if (config.mouthForm2Param) {
mouthForm2 = (normalizedVolume - 0.3) * 2 * form2Amplitude;
mouthForm2 = Math.max(-form2Amplitude, Math.min(form2Amplitude, mouthForm2));
}
}
// 调试日志:输出嘴部参数
console.log(`[Live2D] 模型: ${this.currentModelName || 'unknown'}, 音量: ${average?.toFixed(0)}, OpenY: ${mouthOpenY.toFixed(3)}, Form: ${mouthForm.toFixed(3)}, Form2: ${mouthForm2.toFixed(3)}`);
}
coreModel.setParameterValueById(this.mouthParam, mouthValue);
// 设置嘴部开合参数
coreModel.setParameterValueById(this.mouthParam, mouthOpenY);
// 设置嘴型参数(仅对支持嘴型变化的模型)
if (this.currentModelName && this.modelConfig[this.currentModelName]?.mouthFormParam) {
const config = this.modelConfig[this.currentModelName];
const formParam = config.mouthFormParam;
coreModel.setParameterValueById(formParam, mouthForm);
// 设置第二嘴型参数(natori特有)
if (config.mouthForm2Param) {
coreModel.setParameterValueById(config.mouthForm2Param, mouthForm2);
}
}
coreModel.update();
}
this.mouthAnimationId = requestAnimationFrame(() => this.animateMouth());
@@ -473,12 +586,129 @@ class Live2DManager {
motion(name) {
try {
if (!this.live2dModel) return;
this.live2dModel.motion(name);
// 根据当前模型获取对应的动作名称
let actualMotionName = name;
if (this.currentModelName && this.modelConfig[this.currentModelName]) {
const motionMap = this.modelConfig[this.currentModelName].motionMap;
actualMotionName = motionMap[name] || name;
}
this.live2dModel.motion(actualMotionName);
} catch (error) {
console.error('触发动作失败:', error);
}
}
/**
* 设置模型交互事件
*/
setupModelInteractions() {
if (!this.live2dModel) return;
this.live2dModel.interactive = true;
this.live2dModel.on('doublehit', (args) => {
const area = Array.isArray(args) ? args[0] : args;
if (area === 'Body') {
this.motion('Flick@Body');
} else if (area === 'Head' || area === 'Face') {
this.motion('Flick');
}
const app = window.chatApp;
const payload = JSON.stringify({ type: 'live2d', event: 'doublehit', area });
if (app && app.dataChannel && app.dataChannel.readyState === 'open') {
app.dataChannel.send(payload);
}
});
this.live2dModel.on('singlehit', (args) => {
const area = Array.isArray(args) ? args[0] : args;
if (area === 'Body') {
this.motion('Tap@Body');
} else if (area === 'Head' || area === 'Face') {
this.motion('Tap');
}
const app = window.chatApp;
const payload = JSON.stringify({ type: 'live2d', event: 'singlehit', area });
if (app && app.dataChannel && app.dataChannel.readyState === 'open') {
app.dataChannel.send(payload);
}
});
this.live2dModel.on('swipe', (args) => {
const area = Array.isArray(args) ? args[0] : args;
const dir = Array.isArray(args) ? args[1] : undefined;
if (area === 'Body') {
if (dir === 'up') {
this.motion('FlickUp');
} else if (dir === 'down') {
this.motion('FlickDown');
}
}
const app = window.chatApp;
const payload = JSON.stringify({ type: 'live2d', event: 'swipe', area, dir });
if (app && app.dataChannel && app.dataChannel.readyState === 'open') {
app.dataChannel.send(payload);
}
});
this.live2dModel.on('pointerdown', (event) => {
try {
const global = event.data.global;
const bounds = this.live2dModel.getBounds();
if (!bounds || !bounds.contains(global.x, global.y)) return;
const relX = (global.x - bounds.x) / (bounds.width || 1);
const relY = (global.y - bounds.y) / (bounds.height || 1);
let area = '';
if (relX >= 0.4 && relX <= 0.6) {
if (relY <= 0.15) {
area = 'Head';
} else if (relY >= 0.7) {
area = 'Body';
}
}
if (!area) return;
const now = Date.now();
const dt = now - (this._lastClickTime || 0);
const dx = global.x - (this._lastClickPos?.x || 0);
const dy = global.y - (this._lastClickPos?.y || 0);
const dist = Math.hypot(dx, dy);
if (this._lastClickTime && dt <= this._doubleClickMs && dist <= this._doubleClickDist) {
if (this._singleClickTimer) {
clearTimeout(this._singleClickTimer);
this._singleClickTimer = null;
}
this.live2dModel.emit('doublehit', area);
this._lastClickTime = null;
this._lastClickPos = null;
} else {
this._lastClickTime = now;
this._lastClickPos = { x: global.x, y: global.y };
this._singleClickTimer = setTimeout(() => {
this._singleClickTimer = null;
this.live2dModel.emit('singlehit', area);
}, this._doubleClickMs);
}
} catch (e) {
console.warn('pointerdown 处理出错:', e);
}
});
}
/**
* 清理资源
*/
@@ -500,6 +730,94 @@ class Live2DManager {
}
this.live2dModel = null;
}
/**
* 切换 Live2D 模型
* @param {string} modelName - 模型目录名称,如 'hiyori_pro_zh'、'natori_pro_zh'
* @returns {Promise<boolean>} - 切换是否成功
*/
async switchModel(modelName) {
try {
// 获取模型文件名映射
const modelFileMap = {
'hiyori_pro_zh': 'hiyori_pro_t11.model3.json',
'natori_pro_zh': 'natori_pro_t06.model3.json',
'chitose': 'chitose.model3.json',
'haru_greeter_pro_jp': 'haru_greeter_t05.model3.json'
};
const modelFileName = modelFileMap[modelName];
if (!modelFileName) {
console.error('未知的模型名称:', modelName);
return false;
}
// 获取基础路径
const currentPath = window.location.pathname;
const lastSlashIndex = currentPath.lastIndexOf('/');
const basePath = currentPath.substring(0, lastSlashIndex + 1);
const modelPath = basePath + 'resources/' + modelName + '/runtime/' + modelFileName;
// 如果已存在模型,先移除
if (this.live2dModel) {
this.live2dApp.stage.removeChild(this.live2dModel);
this.live2dModel.destroy();
this.live2dModel = null;
}
// 显示加载状态
const app = window.chatApp;
if (app) {
app.setModelLoadingStatus(true);
}
// 加载新模型
this.live2dModel = await PIXI.live2d.Live2DModel.from(modelPath);
this.live2dApp.stage.addChild(this.live2dModel);
// 设置模型属性
this.live2dModel.scale.set(0.33);
this.live2dModel.x = (window.innerWidth - this.live2dModel.width) * 0.5;
this.live2dModel.y = -50;
// 重新绑定交互事件
this.setupModelInteractions();
// 隐藏加载状态
if (app) {
app.setModelLoadingStatus(false);
}
// 保存当前模型名称
this.currentModelName = modelName;
// 设置模型特定的嘴部参数名
if (this.modelConfig[modelName]) {
this.mouthParam = this.modelConfig[modelName].mouthParam || 'ParamMouthOpenY';
}
// 保存到 localStorage
localStorage.setItem('live2dModel', modelName);
// 更新下拉框显示
const modelSelect = document.getElementById('live2dModelSelect');
if (modelSelect) {
modelSelect.value = modelName;
}
console.log('模型切换成功:', modelName);
return true;
} catch (error) {
console.error('切换模型失败:', error);
const app = window.chatApp;
if (app) {
app.setModelLoadingStatus(false);
}
return false;
}
}
}
// 导出全局实例
+173 -19
View File
@@ -11,8 +11,9 @@ class UIController {
this.visualizerCanvas = null;
this.visualizerContext = null;
this.audioStatsTimer = null;
this.currentBackgroundIndex = 0;
this.currentBackgroundIndex = localStorage.getItem('backgroundIndex') ? parseInt(localStorage.getItem('backgroundIndex')) : 0;
this.backgroundImages = ['1.png', '2.png', '3.png'];
this.dialBtnDisabled = false;
// Bind methods
this.init = this.init.bind(this);
@@ -20,6 +21,7 @@ class UIController {
this.updateDialButton = this.updateDialButton.bind(this);
this.addChatMessage = this.addChatMessage.bind(this);
this.switchBackground = this.switchBackground.bind(this);
this.switchLive2DModel = this.switchLive2DModel.bind(this);
this.showModal = this.showModal.bind(this);
this.hideModal = this.hideModal.bind(this);
this.switchTab = this.switchTab.bind(this);
@@ -51,6 +53,12 @@ class UIController {
// Initialize status display
this.updateConnectionUI(false);
// Apply saved background
const backgroundContainer = document.querySelector('.background-container');
if (backgroundContainer) {
backgroundContainer.style.backgroundImage = `url('./images/${this.backgroundImages[this.currentBackgroundIndex]}')`;
}
this.updateDialButton(false);
console.log('UIController init completed');
@@ -82,10 +90,25 @@ class UIController {
backgroundBtn.addEventListener('click', this.switchBackground);
}
// Model select change event
const modelSelect = document.getElementById('live2dModelSelect');
if (modelSelect) {
modelSelect.addEventListener('change', () => {
this.switchLive2DModel();
});
}
// Dial button
const dialBtn = document.getElementById('dialBtn');
if (dialBtn) {
dialBtn.addEventListener('click', () => {
dialBtn.disabled = true;
this.dialBtnDisabled = true;
setTimeout(() => {
dialBtn.disabled = false;
this.dialBtnDisabled = false;
}, 3000);
const wsHandler = getWebSocketHandler();
const isConnected = wsHandler.isConnected();
@@ -110,26 +133,80 @@ class UIController {
});
}
// Camera button
const cameraBtn = document.getElementById('cameraBtn');
let cameraTimer = null;
if (cameraBtn) {
cameraBtn.addEventListener('click', () => {
if (cameraTimer) {
clearTimeout(cameraTimer);
cameraTimer = null;
}
cameraTimer = setTimeout(() => {
const cameraContainer = document.getElementById('cameraContainer');
if (!cameraContainer) {
log('摄像头容器不存在', 'warning');
return;
}
const isActive = cameraContainer.classList.contains('active');
if (isActive) {
// 关闭摄像头
if (typeof window.stopCamera === 'function') {
window.stopCamera();
}
cameraContainer.classList.remove('active');
cameraBtn.classList.remove('camera-active');
cameraBtn.querySelector('.btn-text').textContent = '摄像头';
log('摄像头已关闭', 'info');
} else {
// 打开摄像头
if (typeof window.startCamera === 'function') {
window.startCamera().then(success => {
if (success) {
cameraBtn.classList.add('camera-active');
cameraBtn.querySelector('.btn-text').textContent = '关闭';
} else {
this.addChatMessage('⚠️ 摄像头启动失败,请检查浏览器权限', false);
}
}).catch(error => {
log(`启动摄像头异常: ${error.message}`, 'error');
});
} else {
log('startCamera函数未定义', 'warning');
}
}
}, 300);
});
}
// Record button
const recordBtn = document.getElementById('recordBtn');
if (recordBtn) {
let recordTimer = null;
recordBtn.addEventListener('click', () => {
const audioRecorder = getAudioRecorder();
if (audioRecorder.isRecording) {
audioRecorder.stop();
// Restore record button to normal state
recordBtn.classList.remove('recording');
recordBtn.querySelector('.btn-text').textContent = '录音';
} else {
// Update button state to recording
recordBtn.classList.add('recording');
recordBtn.querySelector('.btn-text').textContent = '录音中';
// Start recording, update button state after delay
setTimeout(() => {
audioRecorder.start();
}, 100);
if (recordTimer) {
clearTimeout(recordTimer);
recordTimer = null;
}
recordTimer = setTimeout(() => {
const audioRecorder = getAudioRecorder();
if (audioRecorder.isRecording) {
audioRecorder.stop();
// Restore record button to normal state
recordBtn.classList.remove('recording');
recordBtn.querySelector('.btn-text').textContent = '录音';
} else {
// Update button state to recording
recordBtn.classList.add('recording');
recordBtn.querySelector('.btn-text').textContent = '录音中';
// Start recording, update button state after delay
setTimeout(() => {
audioRecorder.start();
}, 100);
}
}, 300);
});
}
@@ -220,6 +297,7 @@ class UIController {
updateDialButton(isConnected) {
const dialBtn = document.getElementById('dialBtn');
const recordBtn = document.getElementById('recordBtn');
const cameraBtn = document.getElementById('cameraBtn');
if (dialBtn) {
if (isConnected) {
@@ -239,6 +317,33 @@ class UIController {
}
}
// Update camera button state - reset to default when disconnected
if (cameraBtn && !isConnected) {
const cameraContainer = document.getElementById('cameraContainer');
if (cameraContainer && cameraContainer.classList.contains('active')) {
cameraContainer.classList.remove('active');
}
cameraBtn.classList.remove('camera-active');
cameraBtn.querySelector('.btn-text').textContent = '摄像头';
cameraBtn.disabled = true;
cameraBtn.title = '请先连接服务器';
// 关闭摄像头
if (typeof window.stopCamera === 'function') {
window.stopCamera();
}
}
// Update camera button state - enable when connected and camera is available
if (cameraBtn && isConnected) {
if (window.cameraAvailable) {
cameraBtn.disabled = false;
cameraBtn.title = '打开/关闭摄像头';
} else {
cameraBtn.disabled = true;
cameraBtn.title = '请先绑定验证码';
}
}
// Update record button state
if (recordBtn) {
const microphoneAvailable = window.microphoneAvailable !== false;
@@ -324,6 +429,36 @@ class UIController {
if (backgroundContainer) {
backgroundContainer.style.backgroundImage = `url('./images/${this.backgroundImages[this.currentBackgroundIndex]}')`;
}
localStorage.setItem('backgroundIndex', this.currentBackgroundIndex);
}
// Switch Live2D model
switchLive2DModel() {
const modelSelect = document.getElementById('live2dModelSelect');
if (!modelSelect) {
console.error('模型选择下拉框不存在');
return;
}
const selectedModel = modelSelect.value;
const app = window.chatApp;
if (app && app.live2dManager) {
app.live2dManager.switchModel(selectedModel)
.then(success => {
if (success) {
this.addChatMessage(`已切换到模型: ${selectedModel}`, false);
} else {
this.addChatMessage('模型切换失败', false);
}
})
.catch(error => {
console.error('模型切换错误:', error);
this.addChatMessage('模型切换出错', false);
});
} else {
this.addChatMessage('Live2D管理器未初始化', false);
}
}
// Show modal
@@ -379,6 +514,22 @@ class UIController {
recordBtn.click();
}
}
// Start camera only if camera is available (bound with verification code)
if (window.cameraAvailable && typeof window.startCamera === 'function') {
window.startCamera().then(success => {
if (success) {
const cameraBtn = document.getElementById('cameraBtn');
if (cameraBtn) {
cameraBtn.classList.add('camera-active');
cameraBtn.querySelector('.btn-text').textContent = '关闭';
}
} else {
this.addChatMessage('⚠️ 摄像头启动失败,可能被浏览器拒绝', false);
}
}).catch(error => {
log(`启动摄像头异常: ${error.message}`, 'error');
});
}
}
// Handle connect button click
@@ -468,13 +619,14 @@ class UIController {
// Update dial button state
const dialBtn = document.getElementById('dialBtn');
if (dialBtn) {
dialBtn.disabled = false;
if (!this.dialBtnDisabled) {
dialBtn.disabled = false;
}
dialBtn.querySelector('.btn-text').textContent = '挂断';
dialBtn.classList.add('dial-active');
}
this.hideModal('settingsModal');
} else {
throw new Error('OTA连接失败');
}
@@ -495,7 +647,9 @@ class UIController {
// Restore dial button state
const dialBtn = document.getElementById('dialBtn');
if (dialBtn) {
dialBtn.disabled = false;
if (!this.dialBtnDisabled) {
dialBtn.disabled = false;
}
dialBtn.querySelector('.btn-text').textContent = '拨号';
dialBtn.classList.remove('dial-active');
console.log('Dial button state restored successfully');
@@ -0,0 +1,79 @@
============================================================
示例模型
名执 尽 - PRO
============================================================
 该示例可用于学习商用的游戏和App中常用到的手臂切换等难度较高的制作方法。
 该模型角色由画师先崎真琴设计。
 模型的手臂通过渐变实现不同部件间的切换,这种制作方法经常应用于商用的游戏和App中。
 通过该模型,可以学习部件的切换来实现更丰富的动画。
 另外,可以通过Cubism3 Viewer (for OW)来查看嵌入式文件组的动画,了解完整的工作流程中使用到的数据结构。
------------------------------
素材使用许可
------------------------------
 普通用户以及小规模企业在同意授权协议的情况下可用于商业用途。
 中/大规模的企业只能用于非公开的内部试用。
 在使用该素材时,请确认以下的【无偿提供素材使用授权协议】中的“授权类型”、“Live2D原创角色”等的相关内容,
 并必须接受【Live2D Cubism 示例模型的使用授权要求】中的利用条件。
 有关许可证的更多信息,请参阅以下页面。
 https://www.live2d.com/zh-CHS/download/sample-data/
------------------------------
创作者
------------------------------
 插画:先崎 真琴【http://senzakimakoto.com/】
 配音:小野友树【https://web.onoyuki.com/】
 (※配音数据的发布为限定发布,已于2018/06/04结束。)
 模型:Live2D
------------------------------
素材内容
------------------------------
 模型文件(cmo3) ※包含物理模拟的设定
 表情动画文件(can3)
 基本动画文件(can3)
 嵌入文件组(runtime文件夹)
・模型数据(moc3)
・表情数据(exp3.json)
・动作数据(motion3.json)
・模型设定文件(model3.json)
・姿势设定文件(pose3.json)
・物理模拟设定文件(physics3.json)
・辅助显示的文件(cdi3.json)
------------------------------
更新记录
------------------------------
 ※配音数据的发布已于2018/06/04结束。
【cmo3】
 natori_pro_t06.cmo3
 2021年6月10日 公开
【can3】
 natori_pro_exp_t03.can3
 2021年6月10日 公开
【can3】
 natori_pro_motions_t03.can3
 2021年6月10日 公开
@@ -0,0 +1,120 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamEyeLOpen",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLForm",
"Value": -2,
"Blend": "Add"
},
{
"Id": "ParamEyeROpen",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRForm",
"Value": -2,
"Blend": "Add"
},
{
"Id": "ParamEyeBallForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": 0.3,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": 0.3,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": -0.4,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": -0.4,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamTeethOn",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamCheek",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGlassUD",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassWhite",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlight",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlightMove",
"Value": 0,
"Blend": "Add"
}
]
}
@@ -0,0 +1,120 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamEyeLOpen",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeROpen",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamTeethOn",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamCheek",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamGlassUD",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassWhite",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlight",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlightMove",
"Value": 0,
"Blend": "Add"
}
]
}
@@ -0,0 +1,120 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamEyeLOpen",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeROpen",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamTeethOn",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamCheek",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGlassUD",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassWhite",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlight",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlightMove",
"Value": 0,
"Blend": "Add"
}
]
}
@@ -0,0 +1,120 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamEyeLOpen",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLForm",
"Value": -2,
"Blend": "Add"
},
{
"Id": "ParamEyeROpen",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRForm",
"Value": -2,
"Blend": "Add"
},
{
"Id": "ParamEyeBallForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamTeethOn",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamCheek",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGlassUD",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassWhite",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlight",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlightMove",
"Value": 0,
"Blend": "Add"
}
]
}
@@ -0,0 +1,120 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamEyeLOpen",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamEyeLSmile",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamEyeLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeROpen",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamEyeRSmile",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamEyeRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm2",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamTeethOn",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamCheek",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGlassUD",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassWhite",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlight",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlightMove",
"Value": 0,
"Blend": "Add"
}
]
}
@@ -0,0 +1,120 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamEyeLOpen",
"Value": 0.3,
"Blend": "Add"
},
{
"Id": "ParamEyeLSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeROpen",
"Value": 0.3,
"Blend": "Add"
},
{
"Id": "ParamEyeRSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallForm",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowLY",
"Value": 0.2,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": 0.2,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": -0.1,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": -0.1,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": 0.1,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": 0.1,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamTeethOn",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamCheek",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGlassUD",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassWhite",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlight",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlightMove",
"Value": 0,
"Blend": "Add"
}
]
}
@@ -0,0 +1,120 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamEyeLOpen",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLForm",
"Value": 3,
"Blend": "Add"
},
{
"Id": "ParamEyeROpen",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRForm",
"Value": 3,
"Blend": "Add"
},
{
"Id": "ParamEyeBallForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLY",
"Value": -0.1,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": -0.1,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm2",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm2",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamTeethOn",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamCheek",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGlassUD",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassWhite",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlight",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlightMove",
"Value": 0,
"Blend": "Add"
}
]
}
@@ -0,0 +1,120 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamEyeLOpen",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLForm",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamEyeROpen",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRForm",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamEyeBallForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLY",
"Value": 0.1,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": 0.1,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": 0.1,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": 0.1,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm2",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm2",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamTeethOn",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamCheek",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGlassUD",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassWhite",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlight",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlightMove",
"Value": 0,
"Blend": "Add"
}
]
}
@@ -0,0 +1,120 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamEyeLOpen",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeROpen",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamTeethOn",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamCheek",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGlassUD",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassWhite",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlight",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlightMove",
"Value": 0,
"Blend": "Add"
}
]
}
@@ -0,0 +1,120 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamEyeLOpen",
"Value": 0.2,
"Blend": "Add"
},
{
"Id": "ParamEyeLSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeROpen",
"Value": 0.2,
"Blend": "Add"
},
{
"Id": "ParamEyeRSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLY",
"Value": 0.2,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": 0.2,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": -0.1,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": -0.1,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": 0.1,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": 0.1,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamTeethOn",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamCheek",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGlassUD",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassWhite",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlight",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlightMove",
"Value": 0,
"Blend": "Add"
}
]
}
@@ -0,0 +1,120 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamEyeLOpen",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamEyeLSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeROpen",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamEyeRSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamTeethOn",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamCheek",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGlassUD",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassWhite",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlight",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlightMove",
"Value": 0,
"Blend": "Add"
}
]
}
@@ -0,0 +1,44 @@
{
"Type": "Live2D Pose",
"FadeInTime": 0.2,
"Groups": [
[
{
"Id": "PartArmAL",
"Link": []
},
{
"Id": "PartArmCL",
"Link": []
},
{
"Id": "PartArmDL",
"Link": []
}
],
[
{
"Id": "PartArmAR",
"Link": []
},
{
"Id": "PartArmBR",
"Link": []
},
{
"Id": "PartArmER",
"Link": []
}
],
[
{
"Id": "PartWatchA",
"Link": []
},
{
"Id": "PartWatchB",
"Link": []
}
]
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 MiB

@@ -0,0 +1,711 @@
{
"Version": 3,
"Parameters": [
{
"Id": "ParamAngleX",
"GroupId": "",
"Name": "角度 X"
},
{
"Id": "ParamAngleY",
"GroupId": "",
"Name": "角度 Y"
},
{
"Id": "ParamAngleZ",
"GroupId": "",
"Name": "角度 Z"
},
{
"Id": "ParamEyeLOpen",
"GroupId": "ParamGroupExpression",
"Name": "左眼 开闭"
},
{
"Id": "ParamEyeLSmile",
"GroupId": "ParamGroupExpression",
"Name": "左眼 微笑"
},
{
"Id": "ParamEyeLForm",
"GroupId": "ParamGroupExpression",
"Name": "左眼 变形"
},
{
"Id": "ParamEyeROpen",
"GroupId": "ParamGroupExpression",
"Name": "右眼 开闭"
},
{
"Id": "ParamEyeRSmile",
"GroupId": "ParamGroupExpression",
"Name": "右眼 微笑"
},
{
"Id": "ParamEyeRForm",
"GroupId": "ParamGroupExpression",
"Name": "右眼 变形"
},
{
"Id": "ParamEyeBallX",
"GroupId": "ParamGroupExpression",
"Name": "眼珠 X"
},
{
"Id": "ParamEyeBallY",
"GroupId": "ParamGroupExpression",
"Name": "眼珠 Y"
},
{
"Id": "ParamEyeBallForm",
"GroupId": "ParamGroupExpression",
"Name": "眼珠 缩放"
},
{
"Id": "ParamBrowLY",
"GroupId": "ParamGroupExpression",
"Name": "左眉 上下"
},
{
"Id": "ParamBrowRY",
"GroupId": "ParamGroupExpression",
"Name": "右眉 上下"
},
{
"Id": "ParamBrowLX",
"GroupId": "ParamGroupExpression",
"Name": "左眉 左右"
},
{
"Id": "ParamBrowRX",
"GroupId": "ParamGroupExpression",
"Name": "右眉 左右"
},
{
"Id": "ParamBrowLAngle",
"GroupId": "ParamGroupExpression",
"Name": "左眉 角度"
},
{
"Id": "ParamBrowRAngle",
"GroupId": "ParamGroupExpression",
"Name": "右眉 角度"
},
{
"Id": "ParamBrowLForm",
"GroupId": "ParamGroupExpression",
"Name": "左眉 变形"
},
{
"Id": "ParamBrowLForm2",
"GroupId": "ParamGroupExpression",
"Name": "左眉 变形2"
},
{
"Id": "ParamBrowRForm",
"GroupId": "ParamGroupExpression",
"Name": "右眉 变形"
},
{
"Id": "ParamBrowRForm2",
"GroupId": "ParamGroupExpression",
"Name": "右眉 变形2"
},
{
"Id": "ParamMouthForm",
"GroupId": "ParamGroupExpression",
"Name": "嘴 变形"
},
{
"Id": "ParamMouthOpenY",
"GroupId": "ParamGroupExpression",
"Name": "嘴 开闭"
},
{
"Id": "ParamMouthForm2",
"GroupId": "ParamGroupExpression",
"Name": "嘴 变形2"
},
{
"Id": "ParamTeethOn",
"GroupId": "ParamGroupExpression",
"Name": "牙齿的显示"
},
{
"Id": "ParamCheek",
"GroupId": "ParamGroupExpression",
"Name": "害羞"
},
{
"Id": "ParamGlassUD",
"GroupId": "ParamGroupExpression",
"Name": "眼镜 上下"
},
{
"Id": "ParamGrassWhite",
"GroupId": "ParamGroupExpression",
"Name": "镜片 白"
},
{
"Id": "ParamGrassHighlight",
"GroupId": "ParamGroupExpression",
"Name": "镜片 反光显示"
},
{
"Id": "ParamGrassHighlightMove",
"GroupId": "ParamGroupExpression",
"Name": "镜片 反光移动"
},
{
"Id": "ParamBodyAngleX",
"GroupId": "ParamGroupBody",
"Name": "身体的旋转 X"
},
{
"Id": "ParamBodyAngleY",
"GroupId": "ParamGroupBody",
"Name": "身体的旋转 Y"
},
{
"Id": "ParamBodyAngleZ",
"GroupId": "ParamGroupBody",
"Name": "身体的旋转 Z"
},
{
"Id": "ParamWaistAngleZ",
"GroupId": "ParamGroupBody",
"Name": "腰的旋转 Z"
},
{
"Id": "ParamBodyPosition",
"GroupId": "ParamGroupBody",
"Name": "身体的前后"
},
{
"Id": "ParamBreath",
"GroupId": "ParamGroupBody",
"Name": "呼吸"
},
{
"Id": "ParamLeftShoulderUp",
"GroupId": "ParamGroupBody",
"Name": "左肩的上下"
},
{
"Id": "ParamRightShoulderUp",
"GroupId": "ParamGroupBody",
"Name": "右肩的上下"
},
{
"Id": "ParamAllX",
"GroupId": "ParamGroup",
"Name": "整体的移动 X"
},
{
"Id": "ParamAllY",
"GroupId": "ParamGroup",
"Name": "整体的移动 Y"
},
{
"Id": "ParamAllRotate",
"GroupId": "ParamGroup",
"Name": "整体的旋转"
},
{
"Id": "ParamHairFront",
"GroupId": "ParamGroupSway",
"Name": "头发摇动 前"
},
{
"Id": "ParamHairSide",
"GroupId": "ParamGroupSway",
"Name": "头发摇动 侧"
},
{
"Id": "ParamHairBack",
"GroupId": "ParamGroupSway",
"Name": "头发摇动 后"
},
{
"Id": "ParamHairFrontFuwa",
"GroupId": "ParamGroupSway",
"Name": "前发 蓬松"
},
{
"Id": "ParamHairSideFuwa",
"GroupId": "ParamGroupSway",
"Name": "侧发 蓬松"
},
{
"Id": "ParamHairBackFuwa",
"GroupId": "ParamGroupSway",
"Name": "后发 蓬松"
},
{
"Id": "ParamJacket",
"GroupId": "ParamGroupSway",
"Name": "燕尾的摇动"
},
{
"Id": "ParamChainWaist",
"GroupId": "ParamGroupSway",
"Name": "表链A的摇动"
},
{
"Id": "ParamWatchSwingA1",
"GroupId": "ParamGroupSway",
"Name": "怀表A 摇动1"
},
{
"Id": "ParamWatchSwingA2",
"GroupId": "ParamGroupSway",
"Name": "怀表A 摇动2"
},
{
"Id": "ParamWatchBChain",
"GroupId": "ParamGroupSway",
"Name": "怀表B 表链的摇动"
},
{
"Id": "ParamWatchAX",
"GroupId": "ParamGroup8",
"Name": "怀表A 横向旋转"
},
{
"Id": "ParamWatchBSwitch",
"GroupId": "ParamGroup9",
"Name": "怀表B 开关"
},
{
"Id": "ParamWatchBOpen",
"GroupId": "ParamGroup9",
"Name": "怀表B 表盖的开闭"
},
{
"Id": "ParamWatchBOpen2",
"GroupId": "ParamGroup9",
"Name": "怀表B 表盘的开闭"
},
{
"Id": "ParamWatchBX",
"GroupId": "ParamGroup9",
"Name": "怀表B 横向旋转"
},
{
"Id": "ParamWatchBRoll",
"GroupId": "ParamGroup9",
"Name": "怀表B 旋转"
},
{
"Id": "ParamWatchBLR",
"GroupId": "ParamGroup9",
"Name": "怀表B 左右"
},
{
"Id": "ParamWatchBUD",
"GroupId": "ParamGroup9",
"Name": "怀表B 上下"
},
{
"Id": "ParamArmAL01",
"GroupId": "ParamGroup3",
"Name": "左手臂A 肩"
},
{
"Id": "ParamArmAL02",
"GroupId": "ParamGroup3",
"Name": "左肩A 手肘旋转"
},
{
"Id": "ParamArmAL03",
"GroupId": "ParamGroup3",
"Name": "左手臂A 手腕"
},
{
"Id": "ParamArmAL04",
"GroupId": "ParamGroup3",
"Name": "左手臂A 前臂的前后"
},
{
"Id": "ParamArmAR01",
"GroupId": "ParamGroup2",
"Name": "右手臂A 肩的旋转"
},
{
"Id": "ParamArmAR02",
"GroupId": "ParamGroup2",
"Name": "右手臂A 手肘的旋转"
},
{
"Id": "ParamArmAR03",
"GroupId": "ParamGroup2",
"Name": "右手臂A 手腕的旋转"
},
{
"Id": "ParamArmAR04",
"GroupId": "ParamGroup2",
"Name": "右手臂A 前臂的前后"
},
{
"Id": "ParamArmBR01",
"GroupId": "ParamGroup4",
"Name": "右手臂B 肩的旋转"
},
{
"Id": "ParamArmBR02",
"GroupId": "ParamGroup4",
"Name": "右手臂B 手肘的旋转"
},
{
"Id": "ParamArmBR03",
"GroupId": "ParamGroup4",
"Name": "右手臂B 手腕的旋转"
},
{
"Id": "ParamArmBRHand01",
"GroupId": "ParamGroup4",
"Name": "右手01 显示"
},
{
"Id": "ParamArmBRHand01Roll",
"GroupId": "ParamGroup4",
"Name": "右手01 手指弯曲"
},
{
"Id": "ParamArmBRHand05",
"GroupId": "ParamGroup4",
"Name": "右手05 显示"
},
{
"Id": "ParamArmBRHand05Roll1",
"GroupId": "ParamGroup4",
"Name": "右手05 手指弯曲1"
},
{
"Id": "ParamArmBRHand05Roll2",
"GroupId": "ParamGroup4",
"Name": "右手05 手指弯曲2"
},
{
"Id": "ParamArmBRHand05Roll3",
"GroupId": "ParamGroup4",
"Name": "右手05 手指弯曲3"
},
{
"Id": "ParamArmCR01",
"GroupId": "ParamGroup5",
"Name": "左手臂C 肩的旋转"
},
{
"Id": "ParamArmCR02",
"GroupId": "ParamGroup5",
"Name": "左手臂C 手肘的旋转"
},
{
"Id": "ParamArmCR03",
"GroupId": "ParamGroup5",
"Name": "左手臂C 手腕的旋转"
},
{
"Id": "ParamArmCLHandRoll1",
"GroupId": "ParamGroup5",
"Name": "左手C 手指弯曲1"
},
{
"Id": "ParamArmDL01",
"GroupId": "ParamGroup6",
"Name": "左手臂D 肩的旋转"
},
{
"Id": "ParamArmDL02",
"GroupId": "ParamGroup6",
"Name": "左手臂D 手肘的旋转"
},
{
"Id": "ParamArmDL03",
"GroupId": "ParamGroup6",
"Name": "左手臂D 手腕的旋转"
},
{
"Id": "ParamArmDLHand03Roll",
"GroupId": "ParamGroup6",
"Name": "左手03 手指弯曲"
},
{
"Id": "ParamArmER01",
"GroupId": "ParamGroup7",
"Name": "右手臂E 肩的旋转"
},
{
"Id": "ParamArmER02",
"GroupId": "ParamGroup7",
"Name": "右手臂E 手肘的旋转"
},
{
"Id": "ParamArmER03",
"GroupId": "ParamGroup7",
"Name": "右手臂E 手腕的旋转"
},
{
"Id": "ParamArmER04",
"GroupId": "ParamGroup7",
"Name": "右手臂E 上臂的长度"
},
{
"Id": "ParamArmERHand04",
"GroupId": "ParamGroup7",
"Name": "右手04 显示"
},
{
"Id": "ParamArmERHand04Roll1",
"GroupId": "ParamGroup7",
"Name": "右手04 手指弯曲1"
},
{
"Id": "ParamArmERHand04Roll2",
"GroupId": "ParamGroup7",
"Name": "右手04 手指弯曲2"
},
{
"Id": "ParamArmERHand06",
"GroupId": "ParamGroup7",
"Name": "右手06 显示"
},
{
"Id": "ParamArmERHand06Roll1",
"GroupId": "ParamGroup7",
"Name": "右手06 手指弯曲1"
},
{
"Id": "ParamArmERHand06Roll2",
"GroupId": "ParamGroup7",
"Name": "右手06 手指弯曲2"
}
],
"ParameterGroups": [
{
"Id": "ParamGroupExpression",
"GroupId": "",
"Name": "表情"
},
{
"Id": "ParamGroupBody",
"GroupId": "",
"Name": "身体"
},
{
"Id": "ParamGroup",
"GroupId": "",
"Name": "整体移动"
},
{
"Id": "ParamGroupSway",
"GroupId": "",
"Name": "摇动"
},
{
"Id": "ParamGroup8",
"GroupId": "",
"Name": "怀表A"
},
{
"Id": "ParamGroup9",
"GroupId": "",
"Name": "怀表B"
},
{
"Id": "ParamGroup3",
"GroupId": "",
"Name": "左手臂A"
},
{
"Id": "ParamGroup2",
"GroupId": "",
"Name": "右手臂A"
},
{
"Id": "ParamGroup4",
"GroupId": "",
"Name": "右手臂B"
},
{
"Id": "ParamGroup5",
"GroupId": "",
"Name": "左手臂C"
},
{
"Id": "ParamGroup6",
"GroupId": "",
"Name": "左手臂D"
},
{
"Id": "ParamGroup7",
"GroupId": "",
"Name": "右手臂E"
}
],
"Parts": [
{
"Id": "PartCredit",
"Name": "名牌"
},
{
"Id": "PartCore",
"Name": "CORE"
},
{
"Id": "PartGlass",
"Name": "眼镜"
},
{
"Id": "PartWatchA",
"Name": "怀表A"
},
{
"Id": "PartWatchB",
"Name": "怀表B"
},
{
"Id": "PartHairFront",
"Name": "前发"
},
{
"Id": "PartHead",
"Name": "头"
},
{
"Id": "PartUpperBody",
"Name": "上半身"
},
{
"Id": "PartHairBack",
"Name": "后发"
},
{
"Id": "PartLowerBody",
"Name": "下半身"
},
{
"Id": "PartArmAL",
"Name": "左手臂A"
},
{
"Id": "PartArmAR",
"Name": "右手臂A"
},
{
"Id": "PartArmBR",
"Name": "右手臂B"
},
{
"Id": "PartArmCL",
"Name": "左手臂C"
},
{
"Id": "PartArmDL",
"Name": "左手臂D"
},
{
"Id": "PartArmER",
"Name": "右手臂E"
},
{
"Id": "PartEyeBlow",
"Name": "眉毛"
},
{
"Id": "PartEyeR",
"Name": "右眼"
},
{
"Id": "PartEyeL",
"Name": "左眼"
},
{
"Id": "PartHairLine",
"Name": "发际线"
},
{
"Id": "PartHairShadow",
"Name": "头发阴影"
},
{
"Id": "PartNose",
"Name": "鼻子"
},
{
"Id": "PartMouth",
"Name": "嘴"
},
{
"Id": "PartJacket",
"Name": "燕尾服"
},
{
"Id": "PartArmALFore",
"Name": "左手臂A 前臂"
},
{
"Id": "PartArmARFore",
"Name": "右手臂A 前臂"
},
{
"Id": "PartHand11",
"Name": "手套_1"
},
{
"Id": "PartHand51",
"Name": "手套_5"
},
{
"Id": "PartHand21",
"Name": "手套_2"
},
{
"Id": "PartHand31",
"Name": "手套_3"
},
{
"Id": "PartHand41",
"Name": "手套_4"
},
{
"Id": "PartHand61",
"Name": "手套_6"
}
],
"CombinedParameters": [
[
"ParamAngleX",
"ParamAngleY"
],
[
"ParamEyeBallX",
"ParamEyeBallY"
],
[
"ParamMouthForm",
"ParamMouthOpenY"
],
[
"ParamBodyAngleX",
"ParamBodyAngleY"
],
[
"ParamLeftShoulderUp",
"ParamRightShoulderUp"
],
[
"ParamAllX",
"ParamAllY"
],
[
"ParamWatchSwingA1",
"ParamWatchSwingA2"
],
[
"ParamWatchBLR",
"ParamWatchBUD"
]
]
}
@@ -0,0 +1,123 @@
{
"Version": 3,
"FileReferences": {
"Moc": "natori_pro_t06.moc3",
"Textures": [
"natori_pro_t06.4096/texture_00.png"
],
"Physics": "natori_pro_t06.physics3.json",
"Pose": "natori.pose3.json",
"DisplayInfo": "natori_pro_t06.cdi3.json",
"Expressions": [
{
"Name": "Angry",
"File": "exp/Angry.exp3.json"
},
{
"Name": "Blushing",
"File": "exp/Blushing.exp3.json"
},
{
"Name": "Normal",
"File": "exp/Normal.exp3.json"
},
{
"Name": "Sad",
"File": "exp/Sad.exp3.json"
},
{
"Name": "Smile",
"File": "exp/Smile.exp3.json"
},
{
"Name": "Surprised",
"File": "exp/Surprised.exp3.json"
},
{
"Name": "exp_01",
"File": "exp/exp_01.exp3.json"
},
{
"Name": "exp_02",
"File": "exp/exp_02.exp3.json"
},
{
"Name": "exp_03",
"File": "exp/exp_03.exp3.json"
},
{
"Name": "exp_04",
"File": "exp/exp_04.exp3.json"
},
{
"Name": "exp_05",
"File": "exp/exp_05.exp3.json"
}
],
"Motions": {
"Idle": [
{
"File": "motions/mtn_00.motion3.json"
},
{
"File": "motions/mtn_01.motion3.json"
},
{
"File": "motions/mtn_02.motion3.json"
}
],
"Tap": [
{
"File": "motions/mtn_03.motion3.json"
}
],
"FlickUp@Head": [
{
"File": "motions/mtn_04.motion3.json"
}
],
"Flick@Body": [
{
"File": "motions/mtn_05.motion3.json"
}
],
"FlickDown@Body": [
{
"File": "motions/mtn_06.motion3.json"
}
],
"Tap@Head": [
{
"File": "motions/mtn_07.motion3.json"
}
]
}
},
"Groups": [
{
"Target": "Parameter",
"Name": "LipSync",
"Ids": [
"ParamMouthOpenY"
]
},
{
"Target": "Parameter",
"Name": "EyeBlink",
"Ids": [
"ParamEyeLOpen",
"ParamEyeROpen"
]
}
],
"HitAreas": [
{
"Id": "HitAreaHead",
"Name": ""
},
{
"Id": "HitAreaBody",
"Name": ""
}
]
}
@@ -0,0 +1,966 @@
{
"Version": 3,
"Meta": {
"PhysicsSettingCount": 11,
"TotalInputCount": 34,
"TotalOutputCount": 12,
"VertexCount": 23,
"EffectiveForces": {
"Gravity": {
"X": 0,
"Y": -1
},
"Wind": {
"X": 0,
"Y": 0
}
},
"PhysicsDictionary": [
{
"Id": "PhysicsSetting1",
"Name": "前髪 揺れ"
},
{
"Id": "PhysicsSetting2",
"Name": "横髪 揺れ"
},
{
"Id": "PhysicsSetting3",
"Name": "後ろ髪 揺れ"
},
{
"Id": "PhysicsSetting4",
"Name": "前髪ふわ"
},
{
"Id": "PhysicsSetting5",
"Name": "横髪ふわ"
},
{
"Id": "PhysicsSetting6",
"Name": "後ろ髪ふわ"
},
{
"Id": "PhysicsSetting7",
"Name": "燕尾揺れ"
},
{
"Id": "PhysicsSetting8",
"Name": "懐中時計腰 揺れ"
},
{
"Id": "PhysicsSetting9",
"Name": "腰のチェーン揺れ"
},
{
"Id": "PhysicsSetting10",
"Name": "懐中時計腰 横回転"
},
{
"Id": "PhysicsSetting11",
"Name": "懐中時計B チェーン揺れ"
}
]
},
"PhysicsSettings": [
{
"Id": "PhysicsSetting1",
"Input": [
{
"Source": {
"Target": "Parameter",
"Id": "ParamAngleX"
},
"Weight": 60,
"Type": "X",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamAngleZ"
},
"Weight": 60,
"Type": "Angle",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamBodyAngleX"
},
"Weight": 40,
"Type": "X",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamBodyAngleZ"
},
"Weight": 40,
"Type": "Angle",
"Reflect": false
}
],
"Output": [
{
"Destination": {
"Target": "Parameter",
"Id": "ParamHairFront"
},
"VertexIndex": 1,
"Scale": 1.824,
"Weight": 100,
"Type": "Angle",
"Reflect": false
}
],
"Vertices": [
{
"Position": {
"X": 0,
"Y": 0
},
"Mobility": 1,
"Delay": 1,
"Acceleration": 1,
"Radius": 0
},
{
"Position": {
"X": 0,
"Y": 12.7
},
"Mobility": 0.95,
"Delay": 0.9,
"Acceleration": 1,
"Radius": 12.7
}
],
"Normalization": {
"Position": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
},
"Angle": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
}
}
},
{
"Id": "PhysicsSetting2",
"Input": [
{
"Source": {
"Target": "Parameter",
"Id": "ParamAngleX"
},
"Weight": 60,
"Type": "X",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamAngleZ"
},
"Weight": 60,
"Type": "Angle",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamBodyAngleX"
},
"Weight": 40,
"Type": "X",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamBodyAngleZ"
},
"Weight": 40,
"Type": "Angle",
"Reflect": false
}
],
"Output": [
{
"Destination": {
"Target": "Parameter",
"Id": "ParamHairSide"
},
"VertexIndex": 1,
"Scale": 2,
"Weight": 100,
"Type": "Angle",
"Reflect": false
}
],
"Vertices": [
{
"Position": {
"X": 0,
"Y": 0
},
"Mobility": 1,
"Delay": 1,
"Acceleration": 1,
"Radius": 0
},
{
"Position": {
"X": 0,
"Y": 18.2
},
"Mobility": 0.95,
"Delay": 0.9,
"Acceleration": 1,
"Radius": 18.2
}
],
"Normalization": {
"Position": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
},
"Angle": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
}
}
},
{
"Id": "PhysicsSetting3",
"Input": [
{
"Source": {
"Target": "Parameter",
"Id": "ParamAngleX"
},
"Weight": 60,
"Type": "X",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamAngleZ"
},
"Weight": 60,
"Type": "Angle",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamBodyAngleX"
},
"Weight": 40,
"Type": "X",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamBodyAngleZ"
},
"Weight": 40,
"Type": "Angle",
"Reflect": false
}
],
"Output": [
{
"Destination": {
"Target": "Parameter",
"Id": "ParamHairBack"
},
"VertexIndex": 1,
"Scale": 2,
"Weight": 100,
"Type": "Angle",
"Reflect": false
}
],
"Vertices": [
{
"Position": {
"X": 0,
"Y": 0
},
"Mobility": 1,
"Delay": 1,
"Acceleration": 1,
"Radius": 0
},
{
"Position": {
"X": 0,
"Y": 14.3
},
"Mobility": 1,
"Delay": 0.9,
"Acceleration": 1.42,
"Radius": 14.3
}
],
"Normalization": {
"Position": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
},
"Angle": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
}
}
},
{
"Id": "PhysicsSetting4",
"Input": [
{
"Source": {
"Target": "Parameter",
"Id": "ParamAngleY"
},
"Weight": 35,
"Type": "X",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamBodyAngleY"
},
"Weight": 30,
"Type": "X",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamBodyPosition"
},
"Weight": 35,
"Type": "X",
"Reflect": true
}
],
"Output": [
{
"Destination": {
"Target": "Parameter",
"Id": "ParamHairFrontFuwa"
},
"VertexIndex": 1,
"Scale": 3,
"Weight": 100,
"Type": "Angle",
"Reflect": true
}
],
"Vertices": [
{
"Position": {
"X": 0,
"Y": 0
},
"Mobility": 1,
"Delay": 1,
"Acceleration": 1,
"Radius": 0
},
{
"Position": {
"X": 0,
"Y": 11.9
},
"Mobility": 0.79,
"Delay": 0.9,
"Acceleration": 1,
"Radius": 11.9
}
],
"Normalization": {
"Position": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
},
"Angle": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
}
}
},
{
"Id": "PhysicsSetting5",
"Input": [
{
"Source": {
"Target": "Parameter",
"Id": "ParamAngleY"
},
"Weight": 35,
"Type": "X",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamBodyAngleY"
},
"Weight": 30,
"Type": "X",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamBodyPosition"
},
"Weight": 35,
"Type": "X",
"Reflect": true
}
],
"Output": [
{
"Destination": {
"Target": "Parameter",
"Id": "ParamHairSideFuwa"
},
"VertexIndex": 1,
"Scale": 3.5,
"Weight": 100,
"Type": "Angle",
"Reflect": true
}
],
"Vertices": [
{
"Position": {
"X": 0,
"Y": 0
},
"Mobility": 1,
"Delay": 1,
"Acceleration": 1,
"Radius": 0
},
{
"Position": {
"X": 0,
"Y": 14.3
},
"Mobility": 0.79,
"Delay": 0.9,
"Acceleration": 1.1,
"Radius": 14.3
}
],
"Normalization": {
"Position": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
},
"Angle": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
}
}
},
{
"Id": "PhysicsSetting6",
"Input": [
{
"Source": {
"Target": "Parameter",
"Id": "ParamAngleY"
},
"Weight": 35,
"Type": "X",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamBodyAngleY"
},
"Weight": 30,
"Type": "X",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamBodyPosition"
},
"Weight": 35,
"Type": "X",
"Reflect": true
}
],
"Output": [
{
"Destination": {
"Target": "Parameter",
"Id": "ParamHairBackFuwa"
},
"VertexIndex": 1,
"Scale": 5,
"Weight": 100,
"Type": "Angle",
"Reflect": true
}
],
"Vertices": [
{
"Position": {
"X": 0,
"Y": 0
},
"Mobility": 1,
"Delay": 1,
"Acceleration": 1,
"Radius": 0
},
{
"Position": {
"X": 0,
"Y": 11.3
},
"Mobility": 0.79,
"Delay": 0.9,
"Acceleration": 1.16,
"Radius": 11.3
}
],
"Normalization": {
"Position": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
},
"Angle": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
}
}
},
{
"Id": "PhysicsSetting7",
"Input": [
{
"Source": {
"Target": "Parameter",
"Id": "ParamBodyAngleX"
},
"Weight": 100,
"Type": "X",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamBodyAngleZ"
},
"Weight": 70,
"Type": "Angle",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamWaistAngleZ"
},
"Weight": 30,
"Type": "Angle",
"Reflect": false
}
],
"Output": [
{
"Destination": {
"Target": "Parameter",
"Id": "ParamJacket"
},
"VertexIndex": 1,
"Scale": 1,
"Weight": 100,
"Type": "Angle",
"Reflect": false
}
],
"Vertices": [
{
"Position": {
"X": 0,
"Y": 0
},
"Mobility": 1,
"Delay": 1,
"Acceleration": 1,
"Radius": 0
},
{
"Position": {
"X": 0,
"Y": 31.9
},
"Mobility": 0.95,
"Delay": 0.8,
"Acceleration": 0.8,
"Radius": 31.9
}
],
"Normalization": {
"Position": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
},
"Angle": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
}
}
},
{
"Id": "PhysicsSetting8",
"Input": [
{
"Source": {
"Target": "Parameter",
"Id": "ParamBodyAngleX"
},
"Weight": 100,
"Type": "X",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamBodyAngleZ"
},
"Weight": 70,
"Type": "Angle",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamWaistAngleZ"
},
"Weight": 30,
"Type": "Angle",
"Reflect": false
}
],
"Output": [
{
"Destination": {
"Target": "Parameter",
"Id": "ParamWatchSwingA1"
},
"VertexIndex": 1,
"Scale": 1,
"Weight": 100,
"Type": "Angle",
"Reflect": false
},
{
"Destination": {
"Target": "Parameter",
"Id": "ParamWatchSwingA2"
},
"VertexIndex": 2,
"Scale": 1,
"Weight": 100,
"Type": "Angle",
"Reflect": false
}
],
"Vertices": [
{
"Position": {
"X": 0,
"Y": 0
},
"Mobility": 1,
"Delay": 1,
"Acceleration": 1,
"Radius": 0
},
{
"Position": {
"X": 0,
"Y": 15.4
},
"Mobility": 0.95,
"Delay": 1,
"Acceleration": 0.8,
"Radius": 15.4
},
{
"Position": {
"X": 0,
"Y": 31.9
},
"Mobility": 0.9,
"Delay": 1,
"Acceleration": 0.6,
"Radius": 16.5
}
],
"Normalization": {
"Position": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
},
"Angle": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
}
}
},
{
"Id": "PhysicsSetting9",
"Input": [
{
"Source": {
"Target": "Parameter",
"Id": "ParamBodyAngleX"
},
"Weight": 100,
"Type": "X",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamBodyAngleZ"
},
"Weight": 70,
"Type": "Angle",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamWaistAngleZ"
},
"Weight": 30,
"Type": "Angle",
"Reflect": false
}
],
"Output": [
{
"Destination": {
"Target": "Parameter",
"Id": "ParamChainWaist"
},
"VertexIndex": 1,
"Scale": 1,
"Weight": 100,
"Type": "Angle",
"Reflect": false
}
],
"Vertices": [
{
"Position": {
"X": 0,
"Y": 0
},
"Mobility": 1,
"Delay": 1,
"Acceleration": 1,
"Radius": 0
},
{
"Position": {
"X": 0,
"Y": 11.6
},
"Mobility": 0.95,
"Delay": 1,
"Acceleration": 1,
"Radius": 11.6
}
],
"Normalization": {
"Position": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
},
"Angle": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
}
}
},
{
"Id": "PhysicsSetting10",
"Input": [
{
"Source": {
"Target": "Parameter",
"Id": "ParamBodyAngleX"
},
"Weight": 100,
"Type": "X",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamBodyAngleZ"
},
"Weight": 70,
"Type": "Angle",
"Reflect": false
},
{
"Source": {
"Target": "Parameter",
"Id": "ParamWaistAngleZ"
},
"Weight": 30,
"Type": "Angle",
"Reflect": false
}
],
"Output": [
{
"Destination": {
"Target": "Parameter",
"Id": "ParamWatchAX"
},
"VertexIndex": 1,
"Scale": 2,
"Weight": 100,
"Type": "Angle",
"Reflect": false
}
],
"Vertices": [
{
"Position": {
"X": 0,
"Y": 0
},
"Mobility": 1,
"Delay": 1,
"Acceleration": 1,
"Radius": 0
},
{
"Position": {
"X": 0,
"Y": 72.1
},
"Mobility": 0.95,
"Delay": 1,
"Acceleration": 0.2,
"Radius": 72.1
}
],
"Normalization": {
"Position": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
},
"Angle": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
}
}
},
{
"Id": "PhysicsSetting11",
"Input": [
{
"Source": {
"Target": "Parameter",
"Id": "ParamArmBR03"
},
"Weight": 100,
"Type": "X",
"Reflect": false
}
],
"Output": [
{
"Destination": {
"Target": "Parameter",
"Id": "ParamWatchBChain"
},
"VertexIndex": 1,
"Scale": 2,
"Weight": 100,
"Type": "Angle",
"Reflect": false
}
],
"Vertices": [
{
"Position": {
"X": 0,
"Y": 0
},
"Mobility": 1,
"Delay": 1,
"Acceleration": 1,
"Radius": 0
},
{
"Position": {
"X": 0,
"Y": 13.1
},
"Mobility": 0.95,
"Delay": 1,
"Acceleration": 0.66,
"Radius": 13.1
}
],
"Normalization": {
"Position": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
},
"Angle": {
"Minimum": -10,
"Default": 0,
"Maximum": 10
}
}
}
]
}
+107 -11
View File
@@ -44,6 +44,11 @@
<!-- 主容器 -->
<div class="container">
<!-- 摄像头显示区域(可拖动) -->
<div class="camera-container" id="cameraContainer">
<video id="cameraVideo" autoplay playsinline muted></video>
</div>
<!-- 连接状态指示器 - 页面顶部中部 -->
<div class="connection-status-top">
<div class="status-indicator">
@@ -87,12 +92,12 @@
<span class="btn-text">设置</span>
</button>
<button class="control-btn" id="backgroundBtn" title="切换背景">
<button class="control-btn" id="cameraBtn" title="请先连接服务器" disabled>
<svg class="btn-icon" viewBox="0 0 24 24" fill="currentColor">
<path
d="M12,18A6,6 0 0,1 6,12C6,11 6.25,10.03 6.7,9.2L5.24,7.74C4.46,8.97 4,10.43 4,12A8,8 0 0,0 12,20V23L16,19L12,15V18M12,4V1L8,5L12,9V6A6,6 0 0,1 18,12C18,13 17.75,13.97 17.3,14.8L18.76,16.26C19.54,15.03 20,13.57 20,12A8,8 0 0,0 12,4Z" />
d="M17,10.5V7A1,1 0 0,0 16,6H4A1,1 0 0,0 3,7V17A1,1 0 0,0 4,18H16A1,1 0 0,0 17,17V13.5L21,17.5V6.5L17,10.5Z" />
</svg>
<span class="btn-text">背景</span>
<span class="btn-text">摄像头</span>
</button>
<button class="control-btn dial-btn" id="dialBtn" title="拨号">
@@ -124,6 +129,7 @@
<div class="settings-tabs">
<button class="tab-btn active" data-tab="device">设备配置</button>
<button class="tab-btn" data-tab="mcp">MCP工具</button>
<button class="tab-btn" data-tab="other">其他</button>
</div>
<div class="tab-content active" id="deviceTab">
@@ -162,6 +168,10 @@
<input type="text" id="serverUrl" value="" readonly disabled
placeholder="填写OTA地址后,点击拨号按钮自动连接" />
</div>
<div class="input-group">
<label for="visionUrl">视觉分析地址:</label>
<input type="text" id="visionUrl" value="" readonly disabled placeholder="成功建立ws连接后自动获取" />
</div>
</div>
</div>
@@ -186,6 +196,34 @@
</div>
</div>
</div>
<div class="tab-content" id="otherTab">
<div class="other-settings-panel">
<div class="control-panel">
<div class="config-row">
<div class="config-item">
<label>选择模型:</label>
<select id="live2dModelSelect" class="model-select">
<option value="hiyori_pro_zh">hiyori (春日)</option>
<option value="natori_pro_zh">natori (名取)</option>
</select>
</div>
</div>
<div class="config-row">
<div class="config-item">
<label></label>
<button class="background-btn" id="backgroundBtn" title="切换背景">
<svg class="btn-icon" viewBox="0 0 24 24" fill="currentColor">
<path
d="M12,18A6,6 0 0,1 6,12C6,11 6.25,10.03 6.7,9.2L5.24,7.74C4.46,8.97 4,10.43 4,12A8,8 0 0,0 12,20V23L16,19L12,15V18M12,4V1L8,5L12,9V6A6,6 0 0,1 18,12C18,13 17.75,13.97 17.3,14.8L18.76,16.26C19.54,15.03 20,13.57 20,12A8,8 0 0,0 12,4Z" />
</svg>
<span class="btn-text">切换背景</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -209,17 +247,19 @@
<textarea id="mcpToolDescription" placeholder="详细描述工具的功能和使用场景..." required></textarea>
</div>
<div class="input-group">
<label>输入参数</label>
<div class="properties-container">
<div id="mcpPropertiesContainer">
<div style="text-align: center; padding: 20px; color: #999; font-size: 14px;">
暂无参数,点击下方按钮添加参数
</div>
</div>
<button type="button" class="btn-secondary" id="addMcpPropertyBtn">
<div class="input-group-header">
<label>输入参数</label>
<button type="button" class="properties-btn-primary" id="addMcpPropertyBtn">
➕ 添加参数
</button>
</div>
<div class="properties-container">
<div class="mcp-empty-state" id="mcpEmptyState">
暂无参数,点击上方按钮添加参数
</div>
<div class="mcp-properties-list" id="mcpPropertiesContainer">
</div>
</div>
</div>
<div class="input-group">
<label for="mcpMockResponse">
@@ -237,6 +277,62 @@
</div>
</div>
<!-- 参数编辑模态框 -->
<div id="mcpPropertyModal" class="modal">
<div class="modal-content property-modal">
<div class="modal-header">
<h2 id="mcpPropertyModalTitle">编辑参数</h2>
<button class="close-btn" id="closeMcpPropertyModalBtn">&times;</button>
</div>
<div class="modal-body">
<form id="mcpPropertyForm">
<input type="hidden" id="mcpPropertyIndex" value="-1">
<div class="input-group">
<label for="mcpPropertyName">参数名称 *</label>
<input type="text" id="mcpPropertyName" placeholder="例如: param_1" required>
</div>
<div class="input-group">
<label for="mcpPropertyType">数据类型 *</label>
<select id="mcpPropertyType" class="model-select" required>
<option value="string">字符串</option>
<option value="integer">整数</option>
<option value="number">数字</option>
<option value="boolean">布尔值</option>
<option value="array">数组</option>
<option value="object">对象</option>
</select>
</div>
<div class="input-group" id="mcpPropertyRangeGroup" style="display: none;">
<div class="config-row">
<div class="config-item">
<label for="mcpPropertyMinimum">最小值</label>
<input type="number" id="mcpPropertyMinimum" placeholder="可选">
</div>
<div class="config-item">
<label for="mcpPropertyMaximum">最大值</label>
<input type="number" id="mcpPropertyMaximum" placeholder="可选">
</div>
</div>
</div>
<div class="input-group">
<label for="mcpPropertyDescription">参数描述</label>
<input type="text" id="mcpPropertyDescription" placeholder="可选">
</div>
<div class="input-group">
<label class="mcp-checkbox-label">
<input type="checkbox" id="mcpPropertyRequired">
必填参数
</label>
</div>
<div class="modal-actions">
<button type="button" class="btn-secondary" id="cancelMcpPropertyBtn">取消</button>
<button type="submit" class="btn-primary">保存</button>
</div>
</form>
</div>
</div>
</div>
<!-- 背景加载 -->
<script src="js/ui/background-load.js?v=0127"></script>