mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-08 01:34:33 +08:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c37f00283b | |||
| 2e33cbae3a | |||
| 3e308cf9d7 | |||
| ceadf68048 | |||
| 4dc3865d96 | |||
| 29ce610f6f | |||
| 4ebc049fa0 |
@@ -442,6 +442,24 @@ export default (app: Router) => {
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/config/panel-title',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
panelTitle: Joi.string().max(100).allow('').allow(null),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
const result = await systemService.updatePanelTitle(req.body);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/config/global-ssh-key',
|
||||
celebrate({
|
||||
|
||||
@@ -32,6 +32,7 @@ export enum AuthDataType {
|
||||
|
||||
export interface SystemConfigInfo {
|
||||
lang?: string;
|
||||
panelTitle?: string;
|
||||
logRemoveFrequency?: number;
|
||||
cronConcurrency?: number;
|
||||
dependenceProxy?: string;
|
||||
|
||||
+13
-3
@@ -548,24 +548,34 @@ export default class SystemService {
|
||||
return { code: 200, data: { lang } };
|
||||
}
|
||||
|
||||
public async updatePanelTitle(info: SystemModelInfo) {
|
||||
const oDoc = await this.getSystemConfig();
|
||||
const panelTitle = info.panelTitle?.trim() || '';
|
||||
await this.updateAuthDb({
|
||||
...oDoc,
|
||||
info: { ...oDoc.info, panelTitle },
|
||||
});
|
||||
return { code: 200, data: { panelTitle } };
|
||||
}
|
||||
|
||||
public async updateGlobalSshKey(info: SystemModelInfo) {
|
||||
const oDoc = await this.getSystemConfig();
|
||||
const result = await this.updateAuthDb({
|
||||
...oDoc,
|
||||
info: { ...oDoc.info, ...info },
|
||||
});
|
||||
|
||||
|
||||
// Apply the global SSH key
|
||||
const SshKeyService = require('./sshKey').default;
|
||||
const Container = require('typedi').Container;
|
||||
const sshKeyService = Container.get(SshKeyService);
|
||||
|
||||
|
||||
if (info.globalSshKey) {
|
||||
await sshKeyService.addGlobalSSHKey(info.globalSshKey, 'global');
|
||||
} else {
|
||||
await sshKeyService.removeGlobalSSHKey('global');
|
||||
}
|
||||
|
||||
|
||||
return { code: 200, data: result };
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
|
||||
RUN set -x \
|
||||
&& apk update \
|
||||
&& apk add nodejs npm git \
|
||||
&& npm i -g pnpm@8.3.1 pm2 ts-node \
|
||||
&& npm i -g pnpm@8.3.1 pm2 ts-node typescript@5 \
|
||||
&& cd /tmp/build \
|
||||
&& pnpm install --prod
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
|
||||
RUN set -x \
|
||||
&& apk update \
|
||||
&& apk add nodejs npm git \
|
||||
&& npm i -g pnpm@8.3.1 pm2 ts-node \
|
||||
&& npm i -g pnpm@8.3.1 pm2 ts-node typescript@5 \
|
||||
&& cd /tmp/build \
|
||||
&& pnpm install --prod
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ RUN set -x && \
|
||||
git config --global user.email "qinglong@users.noreply.github.com" && \
|
||||
git config --global user.name "qinglong" && \
|
||||
git config --global http.postBuffer 524288000 && \
|
||||
npm install -g pnpm@8.3.1 pm2 ts-node && \
|
||||
npm install -g pnpm@8.3.1 pm2 ts-node typescript@5 && \
|
||||
rm -rf /root/.cache && \
|
||||
rm -rf /root/.npm && \
|
||||
rm -rf /etc/apt/apt.conf.d/docker-clean && \
|
||||
|
||||
@@ -66,7 +66,7 @@ RUN set -x && \
|
||||
git config --global user.email "qinglong@users.noreply.github.com" && \
|
||||
git config --global user.name "qinglong" && \
|
||||
git config --global http.postBuffer 524288000 && \
|
||||
npm install -g pnpm@8.3.1 pm2 ts-node && \
|
||||
npm install -g pnpm@8.3.1 pm2 ts-node typescript@5 && \
|
||||
rm -rf /root/.cache && \
|
||||
rm -rf /root/.npm && \
|
||||
rm -rf /etc/apt/apt.conf.d/docker-clean && \
|
||||
|
||||
+2
-4
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@whyour/qinglong",
|
||||
"packageManager": "pnpm@8.3.1",
|
||||
"version": "2.20.2-3",
|
||||
"version": "2.21.0-12",
|
||||
"description": "Timed task management platform supporting Python3, JavaScript, Shell, Typescript",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -21,9 +21,7 @@
|
||||
"panel": "npm run build:back && node static/build/app.js",
|
||||
"gen:proto": "protoc --experimental_allow_proto3_optional --plugin=./node_modules/.bin/protoc-gen-ts_proto ./back/protos/*.proto --ts_proto_out=./ --ts_proto_opt=outputServices=grpc-js,env=node,esModuleInterop=true,snakeToCamel=false",
|
||||
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
|
||||
"postinstall": "max setup 2>/dev/null || true",
|
||||
"test": "umi-test",
|
||||
"test:coverage": "umi-test --coverage"
|
||||
"postinstall": "max setup 2>/dev/null || true"
|
||||
},
|
||||
"gitHooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ check_pm2() {
|
||||
|
||||
main() {
|
||||
t '=====> 开始检测'
|
||||
npm i -g pnpm@8.3.1 pm2 ts-node
|
||||
npm i -g pnpm@8.3.1 pm2 ts-node typescript@5
|
||||
|
||||
reset_env
|
||||
copy_dep
|
||||
|
||||
+1
-1
@@ -74,7 +74,7 @@ if [[ $command != "reload" ]]; then
|
||||
;;
|
||||
esac
|
||||
|
||||
npm install -g pnpm@8.3.1 pm2 ts-node
|
||||
npm install -g pnpm@8.3.1 pm2 ts-node typescript@5
|
||||
fi
|
||||
|
||||
export PYTHON_SHORT_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
|
||||
|
||||
+43
-12
@@ -22,9 +22,11 @@ import WebSocketManager from '../utils/websocket';
|
||||
export interface SharedContext {
|
||||
headerStyle: React.CSSProperties;
|
||||
isPhone: boolean;
|
||||
siteTitle: string;
|
||||
theme: 'vs' | 'vs-dark';
|
||||
user: any;
|
||||
reloadUser: (needLoading?: boolean) => void;
|
||||
reloadSystemConfig: () => void;
|
||||
reloadTheme: () => void;
|
||||
systemInfo: TSystemInfo;
|
||||
}
|
||||
@@ -45,6 +47,9 @@ export default function () {
|
||||
const [user, setUser] = useState<any>({});
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [systemInfo, setSystemInfo] = useState<TSystemInfo>();
|
||||
const [siteTitle, setSiteTitle] = useState(
|
||||
() => localStorage.getItem('qinglong_panel_title')?.trim() || intl.get('青龙'),
|
||||
);
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [initLoading, setInitLoading] = useState<boolean>(true);
|
||||
const {
|
||||
@@ -124,20 +129,18 @@ export default function () {
|
||||
getUser(needLoading);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (systemInfo && systemInfo.isInitialized && !user) {
|
||||
getUser();
|
||||
}
|
||||
}, [location.pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
getHealthStatus();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const reloadSystemConfig = () => {
|
||||
request
|
||||
.get(`${config.apiPrefix}system/config`)
|
||||
.then(({ data }: any) => {
|
||||
const panelTitle = data?.info?.panelTitle?.trim();
|
||||
if (panelTitle) {
|
||||
setSiteTitle(panelTitle);
|
||||
localStorage.setItem('qinglong_panel_title', panelTitle);
|
||||
} else {
|
||||
setSiteTitle(intl.get('青龙'));
|
||||
localStorage.removeItem('qinglong_panel_title');
|
||||
}
|
||||
if (!data?.info?.lang) {
|
||||
const browserLang =
|
||||
localStorage.getItem('lang') ||
|
||||
@@ -149,8 +152,31 @@ export default function () {
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (systemInfo && systemInfo.isInitialized && !user) {
|
||||
getUser();
|
||||
}
|
||||
}, [location.pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
getHealthStatus();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
reloadSystemConfig();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!['/login', '/initialization', '/error'].includes(location.pathname)) {
|
||||
return;
|
||||
}
|
||||
const title =
|
||||
(config.documentTitleMap as any)[location.pathname] || intl.get('未找到');
|
||||
document.title = `${title} - ${siteTitle}`;
|
||||
}, [location.pathname, siteTitle]);
|
||||
|
||||
useEffect(() => {
|
||||
if (theme === 'vs-dark') {
|
||||
document.body.setAttribute('data-dark', 'true');
|
||||
@@ -229,7 +255,10 @@ export default function () {
|
||||
theme,
|
||||
user,
|
||||
reloadUser,
|
||||
reloadSystemConfig,
|
||||
reloadTheme,
|
||||
siteTitle,
|
||||
systemInfo,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -310,7 +339,7 @@ export default function () {
|
||||
const title =
|
||||
(config.documentTitleMap as any)[location.pathname] ||
|
||||
intl.get('未找到');
|
||||
return `${title} - ${intl.get('青龙')}`;
|
||||
return `${title} - ${siteTitle}`;
|
||||
}}
|
||||
onCollapse={setCollapsed}
|
||||
collapsed={collapsed}
|
||||
@@ -372,7 +401,9 @@ export default function () {
|
||||
theme,
|
||||
user,
|
||||
reloadUser,
|
||||
reloadSystemConfig,
|
||||
reloadTheme,
|
||||
siteTitle,
|
||||
systemInfo,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -581,7 +581,10 @@
|
||||
"链接": "Link",
|
||||
"错误": "Error",
|
||||
"错误日志": "Error Log",
|
||||
"留空使用默认值“青龙”": "Leave blank to use the default title \"Qinglong\"",
|
||||
"队列中": "In Queue",
|
||||
"自定义面板的站点标题,留空使用默认值“青龙”": "Customize the panel site title. Leave blank to use the default title \"Qinglong\"",
|
||||
"面板标题": "Panel Title",
|
||||
"青龙": "Qinglong",
|
||||
"项": "items",
|
||||
"顺序": "Order",
|
||||
|
||||
@@ -581,7 +581,10 @@
|
||||
"链接": "链接",
|
||||
"错误": "错误",
|
||||
"错误日志": "错误日志",
|
||||
"留空使用默认值“青龙”": "留空使用默认值“青龙”",
|
||||
"队列中": "队列中",
|
||||
"自定义面板的站点标题,留空使用默认值“青龙”": "自定义面板的站点标题,留空使用默认值“青龙”",
|
||||
"面板标题": "面板标题",
|
||||
"青龙": "青龙",
|
||||
"项": "项",
|
||||
"顺序": "顺序",
|
||||
|
||||
@@ -46,6 +46,7 @@ const Setting = () => {
|
||||
user,
|
||||
theme,
|
||||
reloadUser,
|
||||
reloadSystemConfig,
|
||||
reloadTheme,
|
||||
systemInfo,
|
||||
} = useOutletContext<SharedContext>();
|
||||
@@ -353,7 +354,11 @@ const Setting = () => {
|
||||
key: 'other',
|
||||
label: intl.get('其他设置'),
|
||||
children: (
|
||||
<Other reloadTheme={reloadTheme} systemInfo={systemInfo} />
|
||||
<Other
|
||||
reloadSystemConfig={reloadSystemConfig}
|
||||
reloadTheme={reloadTheme}
|
||||
systemInfo={systemInfo}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -27,6 +27,7 @@ import { disableBody } from '@/utils';
|
||||
import { TIMEZONES } from '@/utils/const';
|
||||
|
||||
const dataMap = {
|
||||
'panel-title': 'panelTitle',
|
||||
'log-remove-frequency': 'logRemoveFrequency',
|
||||
'cron-concurrency': 'cronConcurrency',
|
||||
timezone: 'timezone',
|
||||
@@ -48,10 +49,15 @@ const exportModules = [
|
||||
|
||||
const Other = ({
|
||||
systemInfo,
|
||||
reloadSystemConfig,
|
||||
reloadTheme,
|
||||
}: Pick<SharedContext, 'reloadTheme' | 'systemInfo'>) => {
|
||||
}: Pick<
|
||||
SharedContext,
|
||||
'reloadSystemConfig' | 'reloadTheme' | 'systemInfo'
|
||||
>) => {
|
||||
const defaultTheme = localStorage.getItem('qinglong_dark_theme') || 'auto';
|
||||
const [systemConfig, setSystemConfig] = useState<{
|
||||
panelTitle?: string | null;
|
||||
logRemoveFrequency?: number | null;
|
||||
cronConcurrency?: number | null;
|
||||
timezone?: string | null;
|
||||
@@ -120,6 +126,9 @@ const Other = ({
|
||||
.then(({ code, data }) => {
|
||||
if (code === 200) {
|
||||
message.success(intl.get('更新成功'));
|
||||
if (path === 'panel-title') {
|
||||
reloadSystemConfig();
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error: any) => {
|
||||
@@ -236,6 +245,35 @@ const Other = ({
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={intl.get('面板标题')}
|
||||
name="panelTitle"
|
||||
tooltip={intl.get('自定义面板的站点标题,留空使用默认值“青龙”')}
|
||||
>
|
||||
<Input.Group compact>
|
||||
<Input
|
||||
style={{ width: 180 }}
|
||||
maxLength={100}
|
||||
value={systemConfig?.panelTitle || ''}
|
||||
placeholder={intl.get('留空使用默认值“青龙”')}
|
||||
onChange={(e) => {
|
||||
setSystemConfig({
|
||||
...systemConfig,
|
||||
panelTitle: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
updateSystemConfig('panel-title');
|
||||
}}
|
||||
style={{ width: 84 }}
|
||||
>
|
||||
{intl.get('确认')}
|
||||
</Button>
|
||||
</Input.Group>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={intl.get('日志删除频率')}
|
||||
name="frequency"
|
||||
|
||||
+33
-4
@@ -1,6 +1,35 @@
|
||||
version: 2.20.2
|
||||
changeLogLink: https://t.me/jiao_long/434
|
||||
publishTime: 2026-03-01 1800
|
||||
version: 2.21.0
|
||||
changeLogLink: https://t.me/jiao_long/436
|
||||
publishTime: 2026-06-18 2300
|
||||
changeLog: |
|
||||
1. 修复 path 安全漏洞(重要)
|
||||
1. shell 增加国际化支持
|
||||
2. 接口提示信息国际化,更新国际化文案
|
||||
3. 统一 Alpine/Debian 分支,QL_SCHEDULER 参数化调度
|
||||
4. grpc 服务增加证书校验
|
||||
5. 定时任务增加 work_dir 设置
|
||||
6. 增加任务统计功能
|
||||
7. 新增 OpeniLink 通知渠道
|
||||
8. 支持自定义接收邮箱地址
|
||||
9. 增加运行实例功能
|
||||
10. 增加 sudo 命令判断
|
||||
11. 延迟增加运行时间提示
|
||||
12. 开机运行任务支持同时开始运行
|
||||
13. 增加 localhost 检测
|
||||
14. 增加环境变量标签功能
|
||||
15. 修复路径穿越安全漏洞
|
||||
16. 修复配置文件路径可能越权
|
||||
17. 修复 work_dir 验证和目录判断
|
||||
18. 修复多个 API 问题(getCronById、定时任务参数、任务退出码等)
|
||||
19. 修复 ESM 依赖查询路径
|
||||
20. 修复 CodeMirror 页面切换多实例崩溃
|
||||
21. 修复 IPv6 网络连接问题
|
||||
22. 修复内部服务 IP 地址
|
||||
23. 修复 gRPC extra_schedules 为空时序列化报错
|
||||
24. 修复 HITOKOTO 参数布尔/字符串类型处理
|
||||
25. 修复 Server酱 返回错误时 undefined 异常
|
||||
26. 修复环境变量 position 数据类型异常
|
||||
27. 修复 Docker 健康检查 QlPort 环境变量读取
|
||||
28. 修复任务统计日志
|
||||
29. 升级 multer 解决 CVE 漏洞
|
||||
30. 升级 nodemailer
|
||||
|
||||
Reference in New Issue
Block a user