Compare commits

...

7 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] c37f00283b chore: prune incidental formatting changes 2026-07-09 18:03:34 +00:00
copilot-swe-agent[bot] 2e33cbae3a fix: limit custom title to browser tab 2026-07-09 17:49:54 +00:00
copilot-swe-agent[bot] 3e308cf9d7 Initial plan 2026-07-09 17:49:54 +00:00
whyour ceadf68048 ts-node 异常,锁定 typescript 版本 2026-07-09 17:49:54 +00:00
copilot-swe-agent[bot] 4dc3865d96 feat: support custom panel title 2026-07-09 17:39:16 +00:00
copilot-swe-agent[bot] 29ce610f6f Initial plan 2026-07-09 17:34:29 +00:00
whyour 4ebc049fa0 更新版本 v2.21.0 2026-07-08 00:28:42 +08:00
16 changed files with 167 additions and 31 deletions
+18
View File
@@ -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({
+1
View File
@@ -32,6 +32,7 @@ export enum AuthDataType {
export interface SystemConfigInfo {
lang?: string;
panelTitle?: string;
logRemoveFrequency?: number;
cronConcurrency?: number;
dependenceProxy?: string;
+13 -3
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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 && \
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,
}}
/>
+3
View File
@@ -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",
+3
View File
@@ -581,7 +581,10 @@
"链接": "链接",
"错误": "错误",
"错误日志": "错误日志",
"留空使用默认值“青龙”": "留空使用默认值“青龙”",
"队列中": "队列中",
"自定义面板的站点标题,留空使用默认值“青龙”": "自定义面板的站点标题,留空使用默认值“青龙”",
"面板标题": "面板标题",
"青龙": "青龙",
"项": "项",
"顺序": "顺序",
+6 -1
View File
@@ -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}
/>
),
},
{
+39 -1
View File
@@ -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
View File
@@ -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