dootask/app/Http/Controllers/IndexController.php
kuaifan aca8fc6b24 refactor(dev): 开发模式改由 nginx 同源反代 vite,移除 VSCODE_PROXY_URI 拼接
- vite 开发 base 改为 /__vite/,nginx 通过 host.docker.internal 反代(含 HMR websocket),任意访问地址/代理下均可用
- vite 端口默认 5173,被占用自动顺延,实际端口由 devProxyPlugin 写入 nginx 片段并 reload(先 nginx -t 校验,失败不保留片段)
- HMR 客户端改连当前页面源;启动输出改为应用访问地址
- public/hot 只写 base/port,不再泄露整个 .env
- cmd dev 移除随机 APP_DEV_PORT、VSCODE_PROXY_URI 与 index.html 跳转;docker-compose nginx 增加 extra_hosts
2026-10-21 17:52:41 +00:00

519 lines
20 KiB
PHP
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Http\Controllers;
use Arr;
use Cache;
use Request;
use Redirect;
use Response;
use App\Models\File;
use App\Module\Doo;
use App\Module\Base;
use App\Module\Extranet;
use App\Module\RandomColor;
use App\Tasks\LoopTask;
use App\Tasks\AppPushTask;
use App\Tasks\JokeSoupTask;
use App\Tasks\DeleteTmpTask;
use App\Tasks\EmailNoticeTask;
use App\Tasks\AutoArchivedTask;
use App\Tasks\DeleteBotMsgTask;
use App\Tasks\CheckinRemindTask;
use App\Tasks\CloseMeetingRoomTask;
use App\Tasks\ManticoreSyncTask;
use App\Tasks\UnclaimedTaskRemindTask;
use App\Tasks\TodoRemindTask;
use App\Tasks\AiTaskLoopTask;
use App\Tasks\MessageAttachmentBackfillTask;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use App\Module\PatchedAvatar as Avatar;
/**
* 页面
* Class IndexController
* @package App\Http\Controllers
*/
class IndexController extends InvokeController
{
public function __invoke($method, $action = '', $child = '')
{
$app = $method ?: 'main';
if ($action) {
$app .= "__" . $action;
}
if ($app == 'default') {
return '';
}
if (!method_exists($this, $app)) {
$app = method_exists($this, $method) ? $method : 'main';
}
return $this->$app($child);
}
/**
* 首页
* @return \Illuminate\Http\Response
*/
public function main()
{
$hotFile = public_path('hot');
$manifestFile = public_path('manifest.json');
if (file_exists($hotFile)) {
$array = Base::json2array(file_get_contents($hotFile));
$style = null;
// 开发模式:vite 经 nginx 反代在同源的 base 路径下(见 vite.config.js devProxyPlugin)
$script = asset_main(trim($array['base'] ?? '/__vite/', '/') . "/resources/assets/js/app.js");
} else {
$array = Base::json2array(file_get_contents($manifestFile));
$style = asset_main($array['resources/assets/js/app.js']['css'][0]);
$script = asset_main($array['resources/assets/js/app.js']['file']);
}
return response()->view('main', [
'system_alias' => Base::settingFind('system', 'system_alias', 'WebPage'),
'login_logo' => Base::fillUrl(Base::settingFind('system', 'login_logo')),
'version' => Base::getVersion(),
'style' => $style,
'script' => $script,
]);
}
/**
* 获取版本号
* @return \Illuminate\Http\RedirectResponse
*/
public function version()
{
return Redirect::to(Base::fillUrl('api/system/version'), 301);
}
/**
* 健康检查
* @return string
*/
public function health()
{
return "ok";
}
/**
* 头像
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response|\Symfony\Component\HttpFoundation\BinaryFileResponse
*/
public function avatar()
{
$segment = Request::segment(2);
if ($segment && preg_match('/.*?\.png$/i', $segment)) {
$name = substr($segment, 0, -4);
} else {
$name = Request::input('name', 'D');
}
$size = Request::input('size', 128);
$color = Request::input('color');
$background = Request::input('background');
// 移除各种括号及其内容
$pattern = '/[((\[【{[<<『「](.*?)[))\]】}]>>』」]/u';
$name = preg_replace($pattern, '', $name) ?: preg_replace($pattern, '$1', $name);
// 移除常见标识词(不区分大小写)
$filterWords = [
// 测试相关
'测试', '测试号', '测试账号', '内测', '体验', '试用', 'test', 'testing', 'beta',
// 账号相关
'账号', '帐号', '账户', '帐户', 'account', 'acc', 'id', 'uid',
// 临时标识
'临时', '暂用', '备用', '主号', '副号', '小号', '大号', 'temp', 'temporary', 'backup',
// 系统相关
'系统', '管理员', 'admin', 'administrator', 'system', 'sys', 'root',
// 用户相关
'用户', 'user', '会员', 'member', 'vip', 'svip', 'mvip', 'premium',
// 官方相关
'官方', '正式', '认证', 'official', 'verified', 'auth',
// 客服相关
'客服', '售后', '服务', 'service', 'support', 'helper', 'assistant',
// 游戏相关
'game', 'gaming', 'player', 'gamer',
// 社交媒体相关
'ins', 'instagram', 'fb', 'facebook', 'tiktok', 'tweet', 'weibo', 'wechat',
// 常见后缀
'official', 'real', 'fake', 'copy', 'channel', 'studio', 'team', 'group',
// 职业相关
'dev', 'developer', 'designer', 'artist', 'writer', 'editor',
// 其他
'bot', 'robot', 'auto', 'anonymous', 'guest', 'default', 'new', 'old'
];
$filterWords = array_map(function ($word) {
return preg_quote($word, '/');
}, $filterWords);
$name = preg_replace('/' . implode('|', $filterWords) . '/iu', '', $name) ?: $name;
// 移除分隔符和特殊字符
$filterSymbols = [
// 常见分隔符
'-', '_', '=', '+', '/', '\\', '|',
'~', '@', '#', '$', '%', '^', '&', '*',
// 空格类字符
' ', ' ', "\t", "\n", "\r",
// 标点符号(中英文)
'。', ',', '、', ';', ':', '?', '!',
'.', '…', '‥', '′', '″', '℃',
'.', ',', ';', ':', '?', '!',
// 引号类(修正版)
'"', "'", '‘', '’', '“', '”', '`',
// 特殊符号
'★', '☆', '○', '●', '◎', '◇', '◆',
'□', '■', '△', '▲', '▽', '▼',
'♀', '♂', '♪', '♫', '♯', '♭', '♬',
'→', '←', '↑', '↓', '↖', '↗', '↙', '↘',
'√', '×', '÷', '±', '∵', '∴',
'♠', '♥', '♣', '♦',
// emoji 表情符号范围
'\x{1F300}-\x{1F9FF}',
'\x{2600}-\x{26FF}',
'\x{2700}-\x{27BF}',
'\x{1F900}-\x{1F9FF}',
'\x{1F600}-\x{1F64F}'
];
$filterSymbols = array_map(function ($symbol) {
return preg_quote($symbol, '/');
}, $filterSymbols);
$name = preg_replace('/[' . implode('', $filterSymbols) . ']/u', '', $name) ?: $name;
//
if (preg_match('/^[\x{4e00}-\x{9fa5}]+$/u', $name)) {
$name = mb_substr($name, mb_strlen($name) - 2);
}
if (empty($name)) {
$name = 'D';
}
if (empty($color)) {
$color = '#ffffff';
$cacheKey = "avatarBackgroundColor::" . md5($name);
$background = Cache::rememberForever($cacheKey, function () {
return RandomColor::one(['luminosity' => 'dark']);
});
}
//
$path = public_path('uploads/tmp/avatar/' . substr(md5($name), 0, 2));
$file = Base::joinPath($path, md5($name) . '.png');
if (file_exists($file)) {
return response()->file($file, [
'Pragma' => 'public',
'Cache-Control' => 'max-age=1814400',
'Content-type' => 'image/png',
'Expires' => gmdate('D, d M Y H:i:s \G\M\T', time() + 1814400),
]);
}
Base::makeDir($path);
//
$avatar = new Avatar([
'shape' => 'square',
'width' => $size,
'height' => $size,
'chars' => 2,
'fontSize' => $size / 2.9,
'uppercase' => true,
'fonts' => [resource_path('assets/statics/fonts/Source_Han_Sans_SC_Regular.otf')],
'foregrounds' => [$color],
'backgrounds' => [$background],
'border' => [
'size' => 0,
'color' => 'foreground',
'radius' => 0,
],
]);
$avatar->create($name)->save($file);
return response()->file($file, [
'Pragma' => 'public',
'Cache-Control' => 'max-age=1814400',
'Content-type' => 'image/png',
'Expires' => gmdate('D, d M Y H:i:s \G\M\T', time() + 1814400),
]);
}
/**
* 接口文档
* @return \Illuminate\Http\RedirectResponse
*/
public function api()
{
return Redirect::to(Base::fillUrl('docs/index.html'), 301);
}
/**
* 系统定时任务,限制内网访问(1分钟/次)
* @return string
*/
public function crontab()
{
if (!Base::is_internal_ip(Base::getIp())) {
// 限制内网访问
return "Forbidden Access";
}
// 自动归档
Task::deliver(new AutoArchivedTask());
// 邮件通知
Task::deliver(new EmailNoticeTask());
// App推送
Task::deliver(new AppPushTask());
// 删除过期的临时表数据
Task::deliver(new DeleteTmpTask('tmp_msgs', 1));
Task::deliver(new DeleteTmpTask('tmp'));
Task::deliver(new DeleteTmpTask('task_worker', 12));
Task::deliver(new DeleteTmpTask('file'));
Task::deliver(new DeleteTmpTask('tmp_file', 24));
Task::deliver(new DeleteTmpTask('user_device', 24));
// 删除机器人消息
Task::deliver(new DeleteBotMsgTask());
// 周期任务
Task::deliver(new LoopTask());
// 签到提醒
Task::deliver(new CheckinRemindTask());
// 获取笑话/心灵鸡汤
Task::deliver(new JokeSoupTask());
// 未领取任务通知
Task::deliver(new UnclaimedTaskRemindTask());
// 待办提醒
Task::deliver(new TodoRemindTask());
// 关闭会议室
Task::deliver(new CloseMeetingRoomTask());
// Manticore Search 同步
Task::deliver(new ManticoreSyncTask());
// AI 任务建议
Task::deliver(new AiTaskLoopTask());
// 聊天附件历史索引回填
Task::deliver(new MessageAttachmentBackfillTask());
return "success";
}
/**
* 桌面客户端发布
*/
public function desktop__publish($name = '')
{
$publishVersion = Request::header('publish-version');
$latestFile = public_path("uploads/desktop/latest");
$latestVersion = file_exists($latestFile) ? trim(file_get_contents($latestFile)) : "0.0.1";
if (strtolower($name) === 'latest') {
$name = $latestVersion;
}
// 上传(header 中包含 publish-version)
if (preg_match("/^\d+\.\d+\.\d+$/", $publishVersion)) {
// 判断密钥
$publishKey = Request::header('publish-key');
if ($publishKey !== config('app.key')) {
return Base::retError("key error");
}
// 判断版本
$action = Request::get('action');
$draftPath = "uploads/desktop-draft/{$publishVersion}/";
if ($action === 'release') {
// 将草稿版本发布为正式版本
$draftPath = public_path($draftPath);
$releasePath = public_path("uploads/desktop/{$publishVersion}/");
if (!file_exists($draftPath)) {
return Base::retError("draft version not exists");
}
if (file_exists($releasePath)) {
Base::deleteDirAndFile($releasePath);
}
Base::copyDirectory($draftPath, $releasePath);
file_put_contents($latestFile, $publishVersion);
// 删除旧版本
Base::deleteDirAndFile(public_path("uploads/desktop-draft"));
$dirs = Base::recursiveDirs(public_path("uploads/desktop"), false);
sort($dirs);
$num = 0;
foreach ($dirs as $dir) {
if (!preg_match("/\/\d+\.\d+\.\d+$/", $dir)) {
continue;
}
$num++;
if ($num < 5) {
continue; // 保留最新的5个版本
}
if (filemtime($dir) > time() - 3600 * 24 * 30) {
continue; // 保留最近30天的版本
}
Base::deleteDirAndFile($dir);
}
return Base::retSuccess('success');
}
// 上传草稿版本
return Base::upload([
"file" => Request::file('file'),
"type" => 'publish',
"path" => $draftPath,
"saveName" => true,
]);
}
// 列表(访问路径 desktop/publish/{version})
if (preg_match("/^v*(\d+\.\d+\.\d+)$/", $name, $match)) {
$paths = [
"uploads/desktop/{$match[1]}/",
"uploads/desktop/v{$match[1]}/",
"uploads/desktop-draft/{$match[1]}/",
"uploads/desktop-draft/v{$match[1]}/",
];
$avaiPath = null;
foreach ($paths as $path) {
$dirPath = public_path($path);
$isDraft = str_contains($path, 'draft');
if (is_dir($dirPath)) {
$avaiPath = $path;
break;
}
}
abort_if(empty($avaiPath), 404);
$lists = Base::recursiveFiles($dirPath, false);
$files = [];
foreach ($lists as $file) {
if (preg_match('/\.(zip|yml|yaml|blockmap)$/i', $file) || str_ends_with($file, '-win.exe')) {
continue;
}
$fileName = basename($file, $dirPath);
$fileSize = filesize($file);
$files[] = [
'name' => $fileName,
'time' => date("Y-m-d H:i:s", filemtime($file)),
'size' => $fileSize > 0 ? Base::readableBytes($fileSize) : 0,
'url' => Base::fillUrl(Base::joinPath($avaiPath, $fileName)),
];
}
$otherVersion = [];
$dirs = Base::recursiveDirs(public_path("uploads/desktop"), false);
foreach ($dirs as $dir) {
if (!preg_match("/\/\d+\.\d+\.\d+$/", $dir)) {
continue;
}
$version = basename($dir);
if ($version === $match[1]) {
continue;
}
$otherVersion[] = [
'version' => $version,
'url' => Base::fillUrl("desktop/publish/{$version}"),
];
}
//
return view('desktop', [
'system_alias' => Base::settingFind('system', 'system_alias', 'WebPage'),
'version' => $match[1],
'files' => $files,
'is_draft' => $isDraft,
'latest_version' => $latestVersion,
'other_version' => array_reverse($otherVersion),
]);
}
// 下载(Latest 版本内的文件,访问路径 desktop/publish/{fileName})
if ($name) {
$filePath = public_path("uploads/desktop/{$latestVersion}/{$name}");
if (file_exists($filePath)) {
return Response::download($filePath);
}
}
// 404
abort(404);
}
/**
* Drawio 图标搜索
* @return array|mixed
*/
public function drawio__iconsearch()
{
$query = trim(Request::input('q'));
$page = trim(Request::input('p'));
$size = trim(Request::input('c'));
return Extranet::drawioIconSearch($query, $page, $size);
}
/**
* 预览文件
* @return array|mixed
*/
public function online__preview()
{
$key = trim(Request::input('key'));
//
$data = parse_url($key);
$path = Arr::get($data, 'path');
$file = public_path($path);
// 防止 ../ 穿越获取到系统文件
abort_if(!str_starts_with(realpath($file), public_path()), 404);
// 如果文件不存在,直接返回 404
abort_if(!file_exists($file), 404);
//
parse_str($data['query'], $query);
$name = Arr::get($query, 'name');
$ext = strtolower(Arr::get($query, 'ext'));
$userAgent = strtolower(Request::server('HTTP_USER_AGENT'));
if ($ext === 'pdf') {
// 文件超过 10m 不支持在线预览,提示下载
if (filesize($file) > 10 * 1024 * 1024) {
return view('download', [
'system_alias' => Base::settingFind('system', 'system_alias', 'WebPage'),
'name' => $name,
'size' => Base::readableBytes(filesize($file)),
'url' => Base::fillUrl($path),
'button' => Doo::translate('点击下载'),
]);
}
// 浏览器类型
$browser = 'none';
if (str_contains($userAgent, 'chrome') || str_contains($userAgent, 'android_kuaifan_eeui')) {
$browser = str_contains($userAgent, 'android_kuaifan_eeui') ? 'android-mobile' : 'chrome-desktop';
} elseif (str_contains($userAgent, 'safari') || str_contains($userAgent, 'ios_kuaifan_eeui')) {
$browser = str_contains($userAgent, 'ios_kuaifan_eeui') ? 'safari-mobile' : 'safari-desktop';
}
// electron 直接在线预览查看
if (str_contains($userAgent, 'electron') || str_contains($browser, 'desktop')) {
return Response::download($file, $name, [
'Content-Type' => 'application/pdf'
], 'inline');
}
// EEUI App 直接在线预览查看
if (Base::isEEUIApp() && Base::judgeClientVersion("0.34.47")) {
if ($browser === 'safari-mobile') {
$redirectUrl = Base::fillUrl($path);
return <<<EOF
<script>
window.top.postMessage({
action: "eeuiAppSendMessage",
data: [
{
action: 'setPageData', // 设置页面数据
data: {
showProgress: true,
titleFixed: true,
urlFixed: true,
}
},
{
action: 'createTarget', // 创建目标(访问新地址)
url: "{$redirectUrl}",
}
]
}, "*")
</script>
EOF;
}
}
}
//
if (in_array($ext, File::localExt)) {
$url = Base::fillUrl($path);
} else {
$url = 'http://nginx/' . $path;
}
$url = Base::urlAddparameter($url, [
'fullfilename' => Base::rightDelete($name, '.' . $ext) . '_' . filemtime($file) . '.' . $ext
]);
$redirectUrl = Base::fillUrl("fileview/onlinePreview?url=" . urlencode(base64_encode($url)));
return Redirect::to($redirectUrl, 301);
}
}