This commit is contained in:
2026-04-25 16:36:34 +08:00
commit db90e7579b
1876 changed files with 189777 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
{
"label": "Electron Support",
"label.zh-CN": "Electron 支持",
"name": "com.msgbyte.env.electron",
"url": "/plugins/com.msgbyte.env.electron/index.js",
"version": "0.0.0",
"author": "moonrailgun",
"description": "Add support for Electron environment in Tailchat",
"description.zh-CN": "在 Tailchat 添加对 Electron 环境的支持",
"requireRestart": true
}

View File

@@ -0,0 +1,19 @@
{
"name": "@plugins/com.msgbyte.env.electron",
"main": "src/index.tsx",
"version": "0.0.0",
"description": "Add support for Electron environment in Tailchat",
"private": true,
"scripts": {
"sync:declaration": "tailchat declaration github"
},
"dependencies": {
"semver": "^7.5.4"
},
"devDependencies": {
"@types/semver": "^7.5.0",
"@types/styled-components": "^5.1.26",
"react": "18.2.0",
"styled-components": "^5.3.6"
}
}

View File

@@ -0,0 +1,35 @@
import { showSuccessToasts } from '@capital/common';
import { Button } from '@capital/component';
import React from 'react';
import { checkUpdate } from './checkUpdate';
import { Translate } from './translate';
import { getDeviceInfo } from './utils';
export const DeviceInfoPanel: React.FC = React.memo(() => {
const deviceInfo = getDeviceInfo();
return (
<div>
<div>
{Translate.clientName}: {deviceInfo.name}
</div>
<div>
{Translate.clientVersion}: {deviceInfo.version}
</div>
<div>
{Translate.platform}: {deviceInfo.platform}
</div>
<Button
onClick={async () => {
const res = await checkUpdate();
if (res === false) {
showSuccessToasts(Translate.isLatest);
}
}}
>
{Translate.checkVersion}
</Button>
</div>
);
});
DeviceInfoPanel.displayName = 'DeviceInfoPanel';

View File

@@ -0,0 +1,121 @@
import React, { useEffect, useRef, useState } from 'react';
import { Translate } from './translate';
interface ElectronWebviewProps {
className?: string;
src: string;
}
export const ElectronWebview: React.FC<ElectronWebviewProps> = React.memo(
(props) => {
const containerRef = useRef<HTMLDivElement>(null);
const [isVisiable, setIsVisiable] = useState(true);
const key = props.src;
const url = props.src;
useEffect(() => {
if (!containerRef.current) {
return;
}
const rect = containerRef.current.getBoundingClientRect();
(window as any).electron.ipcRenderer.sendMessage('$mount-webview', {
key,
url,
rect: {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
},
});
return () => {
(window as any).electron.ipcRenderer.sendMessage('$unmount-webview', {
key,
});
};
}, [key, url]);
useEffect(() => {
if (!containerRef.current) {
return;
}
const intersectionObserver = new IntersectionObserver(
(entries) => {
entries.forEach((entry: any) => {
if (entry.isVisible === true) {
// 完全可见,显示
(window as any).electron.ipcRenderer.sendMessage(
'$show-webview',
{
key: key,
}
);
} else {
(window as any).electron.ipcRenderer.sendMessage(
'$hide-webview',
{
key: key,
}
);
}
setIsVisiable(entry.isVisible);
});
},
{
trackVisibility: true,
delay: 200,
} as any
);
const resizeObserver = new ResizeObserver((entries) => {
entries.forEach((entry) => {
const { target } = entry;
if (!target.parentElement) {
return;
}
const rect = target.getBoundingClientRect();
(window as any).electron.ipcRenderer.sendMessage(
'$update-webview-rect',
{
key: key,
rect: {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
},
}
);
});
});
intersectionObserver.observe(containerRef.current);
resizeObserver.observe(containerRef.current);
return () => {
if (containerRef.current) {
intersectionObserver.unobserve(containerRef.current);
resizeObserver.unobserve(containerRef.current);
}
};
}, [key]);
return (
<div
ref={containerRef}
className={props.className}
style={{ width: '100%', height: '100%' }}
>
{isVisiable === false && (
<span>{Translate.nativeWebviewRenderHideTip}</span>
)}
</div>
);
}
);
ElectronWebview.displayName = 'ElectronWebview';

View File

@@ -0,0 +1,65 @@
import { Button, notification } from '@capital/component';
import React from 'react';
import { Translate } from './translate';
import { getDeviceInfo } from './utils';
const url = 'https://tailchat.msgbyte.com/downloads/client.json';
export async function checkUpdate(): Promise<boolean> {
const deviceInfo = getDeviceInfo();
const [semver, config] = await Promise.all([
import('semver'),
fetch(url).then((res) => res.json()),
]);
const version = deviceInfo.version;
const platform = deviceInfo.platform;
const latestConfig = config?.[platform];
const latestVersion = latestConfig?.version;
const latestUrl = latestConfig?.url;
if (!latestVersion) {
console.warn('Not found latest version');
return true;
}
if (!latestUrl) {
console.warn('Not found latest url');
return true;
}
if (version === '0.0.0') {
console.warn('Current version not valid');
return true;
}
if (semver.gt(latestVersion, version)) {
// 有新版本
notification.info({
message: Translate.newVersionTip,
description: (
<div>
<div>{Translate.newVersionDesc}</div>
<div>
{version} -&gt; {latestVersion}
</div>
</div>
),
btn: (
<Button
type="primary"
onClick={() => {
window.open(latestUrl);
}}
>
{Translate.upgradeNow}
</Button>
),
placement: 'topRight',
duration: 0,
});
return true;
}
return false;
}

View File

@@ -0,0 +1,101 @@
import {
regCustomPanel,
regChatInputButton,
postMessageEvent,
sharedEvent,
regPluginSettings,
getCachedUserSettings,
} from '@capital/common';
import { Icon } from '@capital/component';
import React from 'react';
import { DeviceInfoPanel } from './DeviceInfoPanel';
import { Translate } from './translate';
import { forwardSharedEvent } from './utils';
import { checkUpdate } from './checkUpdate';
import { setWebviewKernel, resetWebviewKernel } from '@capital/common';
import { ElectronWebview } from './ElectronWebview';
import './overwrite.css';
const PLUGIN_NAME = 'Electron Support';
const WEBVIEW_CONFIG = 'electron:nativeWebviewRender';
console.log(`Plugin ${PLUGIN_NAME} is loaded`);
regCustomPanel({
position: 'setting',
icon: '',
name: 'com.msgbyte.env.electron/deviceInfoPanel',
label: Translate.deviceInfo,
render: DeviceInfoPanel,
});
regChatInputButton({
render: () => {
return (
<Icon
className="text-2xl cursor-pointer"
icon="mdi:content-cut"
rotate={3}
onClick={() => {
postMessageEvent('callScreenshotsTool');
}}
/>
);
},
});
regPluginSettings({
position: 'system',
type: 'boolean',
name: WEBVIEW_CONFIG,
label: Translate.nativeWebviewRender,
desc: Translate.nativeWebviewRenderDesc,
});
forwardSharedEvent('receiveUnmutedMessage');
setTimeout(() => {
checkUpdate();
}, 1000);
let changedWithElectron = false;
const checkSettingConfig = (settings: Record<string, any>) => {
if (settings[WEBVIEW_CONFIG] === true) {
setWebviewKernel(() => ElectronWebview);
changedWithElectron = true;
} else if (changedWithElectron === true) {
// 如果关闭了配置且仅当之前用electron设置了webview则重置
resetWebviewKernel();
}
};
sharedEvent.on('loginSuccess', () => {
getCachedUserSettings().then((settings) => {
checkSettingConfig(settings);
});
});
sharedEvent.on('userSettingsUpdate', (settings) => {
checkSettingConfig(settings);
});
navigator.mediaDevices.getDisplayMedia = async (
options: DisplayMediaStreamOptions
) => {
const source = await (
window as any
).electron.ipcRenderer.getDesktopCapturerSource();
const stream = await window.navigator.mediaDevices.getUserMedia({
// audio: options.audio,
video: {
mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId: source.id,
},
} as any,
});
return stream;
};

View File

@@ -0,0 +1,3 @@
.ant-dropdown-menu {
box-shadow: none; /* avoid group detail dropdown's shadow will make dom invisiable */
}

View File

@@ -0,0 +1,53 @@
import { localTrans } from '@capital/common';
export const Translate = {
deviceInfo: localTrans({
'zh-CN': '设备信息',
'en-US': 'Device Info',
}),
clientName: localTrans({
'zh-CN': '客户端名称',
'en-US': 'Client Name',
}),
clientVersion: localTrans({
'zh-CN': '客户端版本号',
'en-US': 'Client Version',
}),
platform: localTrans({
'zh-CN': '平台',
'en-US': 'Platform',
}),
newVersionTip: localTrans({
'zh-CN': '新版本提示',
'en-US': 'New Version Upgrade Tip',
}),
newVersionDesc: localTrans({
'zh-CN': '发现有新的版本可以安装',
'en-US': 'A new version was found to be installed',
}),
upgradeNow: localTrans({
'zh-CN': '立即更新',
'en-US': 'Upgrade Now',
}),
checkVersion: localTrans({
'zh-CN': '检查更新',
'en-US': 'Check for updates',
}),
isLatest: localTrans({
'zh-CN': '已经是最新版',
'en-US': 'Already the latest version',
}),
nativeWebviewRender: localTrans({
'zh-CN': '启用原生浏览器内核渲染',
'en-US': 'Use Native Webview Render',
}),
nativeWebviewRenderDesc: localTrans({
'zh-CN': '解除默认网页访问限制允许在Tailchat嵌入任意网站内容',
'en-US':
'Lift default web page access restrictions and allow any website content to be embedded in Tailchat',
}),
nativeWebviewRenderHideTip: localTrans({
'zh-CN': '组件被遮挡,暂时隐藏网页视图',
'en-US': 'The component is obscured, temporarily hiding the web view',
}),
};

View File

@@ -0,0 +1,41 @@
import { sharedEvent, postMessageEvent } from '@capital/common';
/**
* 转发事件
*/
export function forwardSharedEvent(
eventName: string,
processPayload?: (payload: any) => Promise<{ type: string; payload: any }>
) {
sharedEvent.on(eventName as any, async (payload: any) => {
let type = eventName;
if (processPayload) {
const res = await processPayload(payload);
if (!res) {
// Skip if res is undefined
return;
}
payload = res.payload;
type = res.type;
}
postMessageEvent(type, payload);
});
}
interface ElectronDeviceInfo {
name: string;
version: string;
platform: string;
}
export function getDeviceInfo() {
const deviceInfo: ElectronDeviceInfo = {
name: '',
version: '0.0.0',
platform: 'windows',
...((window as any).__electronDeviceInfo ?? {}),
};
return deviceInfo;
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"esModuleInterop": true,
"jsx": "react",
"importsNotUsedAsValues": "error",
"paths": {
"@capital/*": ["../../src/plugin/*"],
}
}
}