基于NyaDeskPet的二次开发 软件开发与创新日志#1
NyaDeskPet 二次开发优化文档
本文是基于 GitHub 开源项目 NyaDeskPet 进行的二次开发。
该项目是基于 Live2D + AI Agent 的跨平台桌面宠物应用,拥有完全开源的模块化桌宠框架,支持 Live2D 交互、内置 AI Agent。
项目集成了 Live2D 动画、智能聊天、语音识别、文本转语音等丰富功能。
本文将从实际使用中遇到的问题出发进行优化改进,并为后续二次开发提供方向参考。
值得注意的是,笔者非专业开发人员,文中若存在错误,欢迎指正。
原项目地址:NyaDeskPet 仓库
一、运行环境
- Node.js:项目核心运行环境
- Electron:跨平台桌面应用开发框架
- 支持平台:Windows、macOS、Linux
二、项目技术栈
前端技术
- HTML5:负责应用页面结构搭建
- CSS3:负责页面样式设计,支持透明效果、半透明滚动条
- JavaScript/TypeScript:实现核心业务逻辑
核心框架
- Electron:桌面应用核心框架,采用进程分离架构
- 主进程:src/main.ts,负责窗口管理、IPC 通信等核心操作
- 渲染进程:renderer 目录,负责 UI 界面渲染
其他核心技术
- Live2D:桌面宠物动画渲染核心
- 依赖库:pixi.min.js、live2dcubismcore.min.js、pixi-live2d-cubism4.min.js
- WebSockets:用于前后端实时通信
- TTS :文本转语音功能
- ASR :语音识别功能
(还有部分技术笔者可能并未涵盖完全,欢迎补充)
三、运行截图



四、问题、重构思路与优化方案
问题 1:角色边缘割裂问题
问题描述:移动角色至程序窗口边缘时,模型会被窗口裁剪(附图),呈现凭空消失的视觉效果。

解决方案:
一开始想到的解决办法是增大程序窗口铺满整个屏幕,这样无论如何移动角色都不会被边缘裁剪,因为移出了电脑屏幕本来就看不见了……

但随之而来的问题是此时窗口会覆盖整个屏幕,导致鼠标无法正常点击到桌面的其它程序,就相当该程序一直作为主控程序而"霸占"了鼠标

于是想到的另一个方法:将窗口跟随角色移动,且大小也随角色大小调整(刚好容纳下角色模型),这样既保证了无论如何移动角色也不会出现被窗口边缘"割裂"的情况,也保证了程序窗口"占地面积"不会过大,让鼠标能够正常点击其它程序。最后为了美观,将上下黑色边框设置为透明,并将原先界面上的各种按钮收纳进了一个按钮。下面是针对该问题的前后实现:
1.实现窗口跟随角色移动 src\main.ts
点击查看代码
// IPC 通信处理
ipcMain.handle('set-window-bounds', (_event: IpcMainInvokeEvent, bounds: Partial<Electron.Rectangle>) => {
if (!mainWindow) return;
const current = mainWindow.getBounds();
mainWindow.setBounds({
x: bounds.x ?? current.x,
y: bounds.y ?? current.y,
width: bounds.width ?? current.width,
height: bounds.height ?? current.height
});
});
ipcMain.handle('move-window', (_event: IpcMainInvokeEvent, deltaX: number, deltaY: number) => {
if (!mainWindow) return;
const bounds = mainWindow.getBounds();
mainWindow.setBounds({
x: Math.round(bounds.x + deltaX),
y: Math.round(bounds.y + deltaY),
width: bounds.width,
height: bounds.height
});
});
2.实现窗口跟随角色缩放 renderer\js\live2d-manager.ts
点击查看代码
// 使用原始模型尺寸
let modelWidth: number;
let modelHeight: number;
if (this.originalModelBounds) {
modelWidth = this.originalModelBounds.width;
modelHeight = this.originalModelBounds.height;
} else {
const currentScale = model.scale.x||1;
modelWidth = model.width / currentScale;
modelHeight = model.height / currentScale;
}
// 默认初始缩放
if (this.baseScale === 1.0) {
const { width: screenWidth, height:screenHeight } = screen;
const targetHeight = screenHeight*0.4;
this.baseScale = targetHeight/modelHeight;
}
3.隐藏上下边框并重构UI按钮到右下角的可展开卡片中 renderer\index.html
点击查看代码
<!-- 可展开的功能卡片 -->
<div id="function-card" class="function-card collapsed">
<button id="btn-card-toggle" class="card-toggle-btn">
<i data-lucide="more-vertical"></i>
</button>
<div class="card-content">
<div class="card-buttons">
<button id="btn-agent-card" class="card-btn hidden" data-i18n-title="topBar.agent">
<i data-lucide="bot"></i>
</button>
<button id="btn-plugins-card" class="card-btn" data-i18n-title="topBar.plugins">
<i data-lucide="puzzle"></i>
</button>
<button id="btn-settings-card" class="card-btn" data-i18n-title="topBar.settings">
<i data-lucide="settings"></i>
</button>
<button id="btn-open-chat-card" class="card-btn" data-i18n-title="bottomBar.openChat">
<i data-lucide="message-circle"></i>
</button>
<button id="btn-minimize-card" class="card-btn" data-i18n-title="topBar.minimize">
<i data-lucide="minus"></i>
</button>
<button id="btn-close-card" class="card-btn" data-i18n-title="topBar.close">
<i data-lucide="x"></i>
</button>
</div>
</div>
</div>
"可展开卡片"的样式表 renderer\styles.css
点击查看代码
/* 功能卡片 */
.function-card {
position: fixed;
bottom: 10px;
right: 10px;
display: flex;
flex-direction: column-reverse;
align-items: flex-end;
z-index: 2000;
pointer-events: auto;
}
.card-toggle-btn {
width: 32px;
height: 32px;
border-radius: 50%;
border: none;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(5px);
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
margin-top: 5px;
color: #555;
transition: all 0.2s;
}
.card-toggle-btn:hover {
background: white;
transform: scale(1.1);
}
.card-content {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(8px);
border-radius: 12px;
padding: 8px;
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transform-origin: bottom right;
opacity: 1;
transform: scale(1);
}
.function-card.collapsed .card-content {
opacity: 0;
transform: scale(0.8);
pointer-events: none;
}
.card-buttons {
display: flex;
flex-direction: column;
gap: 8px;
}
.card-btn {
width: 36px;
height: 36px;
border-radius: 8px;
border: none;
background: transparent;
color: #444;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.card-btn:hover {
background: rgba(0,0,0,0.08);
color: #000;
}
.card-btn.hidden {
display: none;
}
.card-btn svg {
width: 20px;
height: 20px;
}
#btn-close-card:hover {
background: #f44336;
color: white;
}
.toggle-ui-btn {
display: none;
}
为按钮添加展开时的页面交互 renderer\js\renderer.ts
点击查看代码
function updateAgentButtonVisibility(): void {
const btnAgent = document.getElementById('btn-agent');
const btnAgentCard = document.getElementById('btn-agent-card');
if (!btnAgent && !btnAgentCard) return;
const settings = window.settingsManager.getSettings();
if (settings.backendMode === 'builtin') {
btnAgent?.classList.remove('hidden');
btnAgentCard?.classList.remove('hidden');
} else {
btnAgent?.classList.add('hidden');
btnAgentCard?.classList.add('hidden');
}
}
function setupFunctionCard(): void {
const card = document.getElementById('function-card');
const toggleBtn = document.getElementById('btn-card-toggle');
if (toggleBtn && card) {
toggleBtn.addEventListener('click', (e) => {
e.stopPropagation();
card.classList.toggle('collapsed');
//切换图标<----
const icon = toggleBtn.querySelector('i');
if (icon) {
const isCollapsed = card.classList.contains('collapsed');
icon.setAttribute('data-lucide', isCollapsed ? 'more-vertical' : 'chevron-down');
if (window.lucide) {
window.lucide.createIcons();
}
}
});
}
//点击卡片外区域关闭卡片
document.addEventListener('click', (e) => {
if (card && !card.contains(e.target as Node)) {
card.classList.add('collapsed');
const icon = toggleBtn?.querySelector('i');
if (icon) {
icon.setAttribute('data-lucide', 'more-vertical');
if (window.lucide) {
window.lucide.createIcons();
}
}
}
});
//绑定卡片内按钮事件
const btnAgent = document.getElementById('btn-agent-card');
if (btnAgent) btnAgent.addEventListener('click', showAgentPanel);
const btnPlugins = document.getElementById('btn-plugins-card');
if (btnPlugins) btnPlugins.addEventListener('click', showPluginsPanel);
const btnSettings = document.getElementById('btn-settings-card');
if (btnSettings) btnSettings.addEventListener('click', showSettingsPanel);
const btnChat = document.getElementById('btn-open-chat-card');
if (btnChat) btnChat.addEventListener('click', () => {
const chatWindow = document.getElementById('chat-window');
if (chatWindow?.classList.contains('hidden')) {
showChatWindow();
} else {
hideChatWindow();
}
});
const btnMinimize = document.getElementById('btn-minimize-card');
if (btnMinimize) btnMinimize.addEventListener('click', () => window.electronAPI.minimizeWindow());
const btnClose = document.getElementById('btn-close-card');
if (btnClose) btnClose.addEventListener('click', () => {
if (confirm(window.i18nManager.t('messages.confirmClose'))) {
window.electronAPI.closeWindow();
}
});
}
实现效果截图:

问题 2:聊天对话框遮挡角色问题
问题描述:原项目设计的对话框必须从窗口右侧向左侧展开,在一定程度上对角色进行了遮挡(让我不能在聊天的同时欣赏角色),沉浸感有所下降

解决方案:
将聊天框的背景,按钮都设置为透明,但实际体验发现依然对角色存在遮挡,于是又将聊天框设置成向右展开,实际上的实现思路就是点击"打开聊天"按钮时向右拓展程序窗口面积,并将聊天栏放置于此,再在收起聊天栏时返回的原来的窗口大小
聊天窗口透明化 renderer/styles.css
下面列举了一些样式表的透明化修改:
- 第247行 : .chat-sidebar 背景设置为透明
点击查看代码
.chat-sidebar {
position: absolute;
top: 0;
right: -320px;
width: 280px;
height: 100%;
background: transparent !important;
box-shadow: -4px 0 16px rgba(0, 0, 0, 0.2);
display: flex;
flex-direction: column;
z-index: 9998;
-webkit-app-region: no-drag;
transition: right 0.3s ease, visibility 0s 0.3s;
visibility: hidden;
pointer-events: none;
}
- 第274行 : .chat-sidebar-header 背景设置为透明
点击查看代码
.chat-sidebar-header {
padding: 0 8px 0 0;
background: transparent;
color: white;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
min-height: 50px;
}
- 第356行 : .conversation-dropdown 背景设置为透明
点击查看代码
.conversation-dropdown {
max-height: 0;
overflow: hidden;
transition: max-height 0.25s ease;
background: transparent;
border-bottom: 1px solid transparent;
}
实现窗口右侧展开 src/main.ts
点击查看代码
if (state.chatOpen !== undefined) {
isChatOpen = state.chatOpen;
if (mainWindow) {
const bounds = mainWindow.getBounds();
if (state.chatOpen) {
// 打开聊天窗口,增加窗口宽度
const chatWidth = 280;
mainWindow.setBounds({
x: bounds.x,
y: bounds.y,
width: bounds.width + chatWidth,
height: bounds.height
});
} else {
// 关闭聊天窗口,恢复窗口宽度
const chatWidth = 280;
mainWindow.setBounds({
x: bounds.x,
y: bounds.y,
width: Math.max(bounds.width - chatWidth, 300),
height: bounds.height
});
}
}
}
实现效果图:

问题 3:Windows 系统兼容性问题
问题描述:原项目在部分 Windows 系统中运行报错,文件 NyaDeskPet-main\plugins\ui-automation-plugin\main.py 因包含不兼容的表情符号,且系统不支持 UTF-8 编码,导致脚本无法正常执行(也许原作者仅在 macOS 开发环境下进行过测试)。
解决方案:直接删除该文件第 41 行与第 611 行的错误表情符号,就修复了 Windows 系统兼容性问题。
五、总结
遇到的难点
本次二次开发的核心难点在于 Electron 窗口的动态适配与 Live2D 模型的联动控制,需平衡窗口大小、角色位置与用户操作体验,同时解决跨平台编码兼容性问题,界面交互的重构需兼顾功能实现与视觉美观,对我这样非专业开发者而言,调试进程通信和窗口渲染逻辑也耗费了大量时间。
逆向工程的一些思考
在对 NyaDeskPet 进行逆向分析与二次开发的过程中,我深刻体会到开源项目模块化架构的重要性。原项目采用 Electron 框架,实现模块化设计,这为二次开发提供了很大的便捷。
对于跨平台开发的兼容性也同样值得注意,原作者仅针对 macOS 开发,未充分适配 Windows 系统的编码与环境,导致基础功能异常。
这也提醒我们,二次开发不仅是功能修改,更要完善环境适配、提升用户体验。
posted on 2026-03-12 03:18 wobuxiangxiedaima 阅读(252) 评论(0) 收藏 举报
浙公网安备 33010602011771号