mirror of
https://github.com/coaidev/coai.git
synced 2025-05-19 04:50:14 +09:00
feat: admin service logger page
This commit is contained in:
parent
1b1dcdd0c4
commit
84485eae16
@ -80,7 +80,6 @@ func (c *ChatInstance) ProcessLine(buf, data string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
fmt.Println(item)
|
||||
if form := processChatResponse(item); form == nil {
|
||||
// recursive call
|
||||
if len(buf) > 0 {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package midjourney
|
||||
|
||||
import (
|
||||
"chat/globals"
|
||||
"chat/utils"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
@ -31,7 +32,7 @@ func InWhiteList(ip string) bool {
|
||||
|
||||
func NotifyAPI(c *gin.Context) {
|
||||
if !InWhiteList(c.ClientIP()) {
|
||||
fmt.Println(fmt.Sprintf("[midjourney] notify api: banned request from %s", c.ClientIP()))
|
||||
globals.Info(fmt.Sprintf("[midjourney] notify api: banned request from %s", c.ClientIP()))
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
@ -41,7 +42,7 @@ func NotifyAPI(c *gin.Context) {
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// fmt.Println(fmt.Sprintf("[midjourney] notify api: get notify: %s (from: %s)", utils.Marshal(form), c.ClientIP()))
|
||||
globals.Debug(fmt.Sprintf("[midjourney] notify api: get notify: %s (from: %s)", utils.Marshal(form), c.ClientIP()))
|
||||
|
||||
if !utils.Contains(form.Status, []string{InProgress, Success, Failure}) {
|
||||
// ignore
|
||||
|
@ -215,3 +215,38 @@ func UpdateRootPasswordAPI(c *gin.Context) {
|
||||
"status": true,
|
||||
})
|
||||
}
|
||||
|
||||
func ListLoggerAPI(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, ListLogs())
|
||||
}
|
||||
|
||||
func DownloadLoggerAPI(c *gin.Context) {
|
||||
path := c.Query("path")
|
||||
getBlobFile(c, path)
|
||||
}
|
||||
|
||||
func DeleteLoggerAPI(c *gin.Context) {
|
||||
path := c.Query("path")
|
||||
if err := deleteLogFile(path); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": false,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": true,
|
||||
})
|
||||
}
|
||||
|
||||
func ConsoleLoggerAPI(c *gin.Context) {
|
||||
n := utils.ParseInt(c.Query("n"))
|
||||
|
||||
content := getLatestLogs(n)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": true,
|
||||
"content": content,
|
||||
})
|
||||
}
|
||||
|
49
admin/logger.go
Normal file
49
admin/logger.go
Normal file
@ -0,0 +1,49 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"chat/globals"
|
||||
"chat/utils"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type LogFile struct {
|
||||
Path string `json:"path"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
func ListLogs() []LogFile {
|
||||
return utils.Each(utils.Walk("logs"), func(path string) LogFile {
|
||||
return LogFile{
|
||||
Path: strings.TrimLeft(path, "logs/"),
|
||||
Size: utils.GetFileSize(path),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func getLogPath(path string) string {
|
||||
return fmt.Sprintf("logs/%s", path)
|
||||
}
|
||||
|
||||
func getBlobFile(c *gin.Context, path string) {
|
||||
c.File(getLogPath(path))
|
||||
}
|
||||
|
||||
func deleteLogFile(path string) error {
|
||||
return utils.DeleteFile(getLogPath(path))
|
||||
}
|
||||
|
||||
func getLatestLogs(n int) string {
|
||||
if n <= 0 {
|
||||
n = 100
|
||||
}
|
||||
|
||||
content, err := utils.ReadFileLatestLines(getLogPath(globals.DefaultLoggerFile), n)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Sprintf("read error: %s", err.Error())
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"chat/globals"
|
||||
"fmt"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
@ -24,7 +25,7 @@ type Market struct {
|
||||
func NewMarket() *Market {
|
||||
var models MarketModelList
|
||||
if err := viper.UnmarshalKey("market", &models); err != nil {
|
||||
fmt.Println(fmt.Sprintf("[market] read config error: %s, use default config", err.Error()))
|
||||
globals.Warn(fmt.Sprintf("[market] read config error: %s, use default config", err.Error()))
|
||||
models = MarketModelList{}
|
||||
}
|
||||
|
||||
|
@ -26,4 +26,9 @@ func Register(app *gin.RouterGroup) {
|
||||
app.POST("/admin/user/root", UpdateRootPasswordAPI)
|
||||
|
||||
app.POST("/admin/market/update", UpdateMarketAPI)
|
||||
|
||||
app.GET("/admin/logger/list", ListLoggerAPI)
|
||||
app.GET("/admin/logger/download", DownloadLoggerAPI)
|
||||
app.GET("/admin/logger/console", ConsoleLoggerAPI)
|
||||
app.POST("/admin/logger/delete", DeleteLoggerAPI)
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "chatnio",
|
||||
"version": "3.8.5"
|
||||
"version": "3.8.6"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
|
55
app/src/admin/api/logger.ts
Normal file
55
app/src/admin/api/logger.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import axios from "axios";
|
||||
import { CommonResponse } from "@/admin/utils.ts";
|
||||
import { getErrorMessage } from "@/utils/base.ts";
|
||||
|
||||
export type Logger = {
|
||||
path: string;
|
||||
size: number;
|
||||
};
|
||||
|
||||
export async function listLoggers(): Promise<Logger[]> {
|
||||
try {
|
||||
const response = await axios.get("/admin/logger/list");
|
||||
return response.data as Logger[];
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLoggerConsole(n?: number): Promise<string> {
|
||||
try {
|
||||
const response = await axios.get(`/admin/logger/console?n=${n ?? 100}`);
|
||||
return response.data.content as string;
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
return `failed to get info from server: ${getErrorMessage(e)}`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadLogger(path: string): Promise<void> {
|
||||
try {
|
||||
const response = await axios.get("/admin/logger/download", {
|
||||
responseType: "blob",
|
||||
params: { path },
|
||||
});
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.setAttribute("download", path);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteLogger(path: string): Promise<CommonResponse> {
|
||||
try {
|
||||
const response = await axios.post(`/admin/logger/delete?path=${path}`);
|
||||
return response.data as CommonResponse;
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
return { status: false, error: getErrorMessage(e) };
|
||||
}
|
||||
}
|
@ -7,6 +7,8 @@
|
||||
@import "charge";
|
||||
@import "system";
|
||||
@import "subscription";
|
||||
@import "logger";
|
||||
|
||||
|
||||
.admin-page {
|
||||
position: relative;
|
||||
@ -33,6 +35,7 @@
|
||||
.channel,
|
||||
.charge,
|
||||
.system,
|
||||
.logger,
|
||||
.admin-subscription
|
||||
{
|
||||
padding: 0 !important;
|
||||
|
127
app/src/assets/admin/logger.less
Normal file
127
app/src/assets/admin/logger.less
Normal file
@ -0,0 +1,127 @@
|
||||
.logger {
|
||||
width: 100%;
|
||||
height: max-content;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.logger-card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 20vh;
|
||||
}
|
||||
}
|
||||
|
||||
.logger-container {
|
||||
.paragraph-header {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.logger-toolbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
max-width: 4.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
& > * {
|
||||
margin-right: 0.5rem !important;
|
||||
white-space: nowrap;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logger-console {
|
||||
position: relative;
|
||||
border-radius: var(--radius);
|
||||
background-color: hsl(var(--background-dark));
|
||||
color: hsl(var(--text-light));
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
height: max-content;
|
||||
overflow: hidden;
|
||||
|
||||
|
||||
pre {
|
||||
width: 100%;
|
||||
height: max-content;
|
||||
min-height: 20vh;
|
||||
max-height: 60vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
touch-action: pan-y;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.console-icon {
|
||||
position: absolute;
|
||||
top: 0.75rem;
|
||||
right: 0.75rem;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
.logger-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: max-content;
|
||||
|
||||
& > * {
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logger-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 0.75rem 1rem;
|
||||
flex-wrap: wrap;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid hsl(var(--border));
|
||||
transition: all 0.2s ease-in-out;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
border-color: hsl(var(--border-hover));
|
||||
}
|
||||
|
||||
& > * {
|
||||
margin-right: 1rem;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.logger-item-title {
|
||||
font-size: 16px;
|
||||
color: hsl(var(--text));
|
||||
}
|
||||
|
||||
.logger-item-size {
|
||||
font-size: 14px;
|
||||
color: hsl(var(--text-secondary));
|
||||
}
|
||||
|
||||
.logger-item-action {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
@ -5,6 +5,8 @@
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--background-light: 0 0% 100%;
|
||||
--background-dark: 0 0% 0%;
|
||||
--background-hover: 5 12% 100%;
|
||||
--background-container: 0, 0%, 97%, 0.8;
|
||||
--foreground: 240 10% 3.9%;
|
||||
@ -42,6 +44,7 @@
|
||||
--ring: 240 5% 64.9%;
|
||||
|
||||
--text: 0 0% 0%;
|
||||
--text-light: 0 0% 100%;
|
||||
--text-dark: 0 0% 100%;
|
||||
--text-secondary: 0 0% 35%;
|
||||
--text-secondary-dark: 0 0% 80%;
|
||||
|
@ -7,6 +7,7 @@ import Markdown from "@/components/Markdown.tsx";
|
||||
export type ParagraphProps = {
|
||||
title?: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
configParagraph?: boolean;
|
||||
isCollapsed?: boolean;
|
||||
onCollapse?: () => void;
|
||||
@ -16,6 +17,7 @@ export type ParagraphProps = {
|
||||
function Paragraph({
|
||||
title,
|
||||
children,
|
||||
className,
|
||||
configParagraph,
|
||||
isCollapsed,
|
||||
onCollapse,
|
||||
@ -32,6 +34,7 @@ function Paragraph({
|
||||
configParagraph && `config-paragraph`,
|
||||
isCollapsed && `collapsable`,
|
||||
collapsed && `collapsed`,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
BookCopy,
|
||||
CalendarRange,
|
||||
CloudCog,
|
||||
FileClock,
|
||||
Gauge,
|
||||
GitFork,
|
||||
Radio,
|
||||
@ -78,6 +79,11 @@ function MenuBar() {
|
||||
icon={<Settings />}
|
||||
path={"/system"}
|
||||
/>
|
||||
<MenuItem
|
||||
title={t("admin.logger.title")}
|
||||
icon={<FileClock />}
|
||||
path={"/logger"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ import React from "react";
|
||||
import { syncSiteInfo } from "@/admin/api/info.ts";
|
||||
import { getOfflineModels, loadPreferenceModels } from "@/utils/storage.ts";
|
||||
|
||||
export const version = "3.8.5";
|
||||
export const version = "3.8.6";
|
||||
export const dev: boolean = getDev();
|
||||
export const deploy: boolean = true;
|
||||
export let rest_api: string = getRestApi(deploy);
|
||||
|
@ -518,6 +518,11 @@
|
||||
"searchEndpoint": "搜索接入点",
|
||||
"searchQuery": "最大搜索结果数",
|
||||
"searchTip": "DuckDuckGo 搜索接入点,如不填写自动使用 WebPilot 和 New Bing 逆向进行搜索功能(速度较慢)。\nDuckDuckGo API 项目搭建:[duckduckgo-api](https://github.com/binjie09/duckduckgo-api)。"
|
||||
},
|
||||
"logger": {
|
||||
"title": "服务日志",
|
||||
"console": "控制台",
|
||||
"consoleLength": "日志条数"
|
||||
}
|
||||
},
|
||||
"mask": {
|
||||
|
@ -478,7 +478,12 @@
|
||||
"update": "Update"
|
||||
},
|
||||
"model-chart-tip": "Token usage",
|
||||
"subscription": "Subscription Management"
|
||||
"subscription": "Subscription Management",
|
||||
"logger": {
|
||||
"title": "service log",
|
||||
"console": "Console",
|
||||
"consoleLength": "Number of log entries"
|
||||
}
|
||||
},
|
||||
"mask": {
|
||||
"title": "Mask Settings",
|
||||
|
@ -478,7 +478,12 @@
|
||||
"update": "更新"
|
||||
},
|
||||
"model-chart-tip": "トークンの使用状況",
|
||||
"subscription": "サブスクリプション管理"
|
||||
"subscription": "サブスクリプション管理",
|
||||
"logger": {
|
||||
"title": "サービスログ",
|
||||
"console": "コンソール",
|
||||
"consoleLength": "ログエントリの数"
|
||||
}
|
||||
},
|
||||
"mask": {
|
||||
"title": "プリセット設定",
|
||||
|
@ -478,7 +478,12 @@
|
||||
"update": "Обновить"
|
||||
},
|
||||
"model-chart-tip": "Использование токенов",
|
||||
"subscription": "Управление подписками"
|
||||
"subscription": "Управление подписками",
|
||||
"logger": {
|
||||
"title": "Журнал обслуживания",
|
||||
"console": "Консоль",
|
||||
"consoleLength": "Количество записей в журнале"
|
||||
}
|
||||
},
|
||||
"mask": {
|
||||
"title": "Настройки маски",
|
||||
|
@ -30,6 +30,7 @@ const Broadcast = lazyFactor(() => import("@/routes/admin/Broadcast.tsx"));
|
||||
const Subscription = lazyFactor(
|
||||
() => import("@/routes/admin/Subscription.tsx"),
|
||||
);
|
||||
const Logger = lazyFactor(() => import("@/routes/admin/Logger.tsx"));
|
||||
|
||||
const router = createBrowserRouter(
|
||||
[
|
||||
@ -188,9 +189,23 @@ const router = createBrowserRouter(
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "admin-logger",
|
||||
path: "logger",
|
||||
element: (
|
||||
<Suspense>
|
||||
<Logger />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
],
|
||||
ErrorBoundary: NotFound,
|
||||
},
|
||||
{
|
||||
id: "not-found",
|
||||
path: "*",
|
||||
element: <NotFound />,
|
||||
},
|
||||
].filter(Boolean),
|
||||
);
|
||||
|
||||
|
135
app/src/routes/admin/Logger.tsx
Normal file
135
app/src/routes/admin/Logger.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useEffectAsync } from "@/utils/hook.ts";
|
||||
import {
|
||||
Logger,
|
||||
listLoggers,
|
||||
downloadLogger,
|
||||
deleteLogger,
|
||||
getLoggerConsole,
|
||||
} from "@/admin/api/logger.ts";
|
||||
import { getSizeUnit } from "@/utils/base.ts";
|
||||
import { Download, RotateCcw, Terminal, Trash } from "lucide-react";
|
||||
import { toastState } from "@/admin/utils.ts";
|
||||
import { useToast } from "@/components/ui/use-toast.ts";
|
||||
import Paragraph from "@/components/Paragraph.tsx";
|
||||
import { Label } from "@/components/ui/label.tsx";
|
||||
import { NumberInput } from "@/components/ui/number-input.tsx";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
|
||||
type LoggerItemProps = Logger & {
|
||||
onUpdate: () => void;
|
||||
};
|
||||
function LoggerItem({ path, size, onUpdate }: LoggerItemProps) {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const loggerSize = useMemo(() => getSizeUnit(size), [size]);
|
||||
|
||||
return (
|
||||
<div className={`logger-item`}>
|
||||
<div className={`logger-item-title`}>{path}</div>
|
||||
<div className={`grow`} />
|
||||
<div className={`logger-item-size`}>{loggerSize}</div>
|
||||
<div
|
||||
className={`logger-item-action`}
|
||||
onClick={async () => downloadLogger(path)}
|
||||
>
|
||||
<Download className={`w-3 h-3`} />
|
||||
</div>
|
||||
<div className={`logger-item-action`}>
|
||||
<Trash
|
||||
className={`w-3 h-3 text-red-600`}
|
||||
onClick={async () => {
|
||||
const resp = await deleteLogger(path);
|
||||
if (resp) onUpdate();
|
||||
toastState(toast, t, resp, true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LoggerList() {
|
||||
const [data, setData] = useState<Logger[]>([]);
|
||||
|
||||
const sync = async () => setData(await listLoggers());
|
||||
|
||||
useEffectAsync(async () => {
|
||||
await sync();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`logger-list`}>
|
||||
{data.map((logger, i) => (
|
||||
<LoggerItem {...logger} key={i} onUpdate={sync} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LoggerConsole() {
|
||||
const { t } = useTranslation();
|
||||
const [data, setData] = useState<string>("");
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [length, setLength] = useState<number>(100);
|
||||
|
||||
const sync = async () => {
|
||||
if (loading) return;
|
||||
setLoading(true);
|
||||
setData(await getLoggerConsole(length));
|
||||
setLoading(false);
|
||||
};
|
||||
useEffectAsync(sync, []);
|
||||
|
||||
return (
|
||||
<Paragraph
|
||||
title={t("admin.logger.console")}
|
||||
className={`logger-container mb-2`}
|
||||
isCollapsed={true}
|
||||
>
|
||||
<div className={`logger-toolbar`}>
|
||||
<Label>{t("admin.logger.consoleLength")}</Label>
|
||||
<NumberInput
|
||||
value={length}
|
||||
onValueChange={setLength}
|
||||
min={1}
|
||||
max={1000}
|
||||
/>
|
||||
<div className={`grow`} />
|
||||
<Button onClick={sync} variant={`outline`} size={`icon`}>
|
||||
<RotateCcw className={`w-4 h-4`} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className={`logger-console`}>
|
||||
<Terminal className={`w-4 h-4 console-icon`} />
|
||||
<pre className={`thin-scrollbar`}>{data}</pre>
|
||||
</div>
|
||||
</Paragraph>
|
||||
);
|
||||
}
|
||||
|
||||
function Logger() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className={`logger`}>
|
||||
<Card className={`logger-card`}>
|
||||
<CardHeader className={`select-none`}>
|
||||
<CardTitle>{t("admin.logger.title")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<LoggerConsole />
|
||||
<LoggerList />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Logger;
|
@ -88,3 +88,10 @@ export function resetJsArray<T>(arr: T[], target: T[]): T[] {
|
||||
arr.splice(0, arr.length, ...target);
|
||||
return arr;
|
||||
}
|
||||
|
||||
export function getSizeUnit(size: number): string {
|
||||
if (size < 1024) return `${size} B`;
|
||||
if (size < 1024 * 1024) return `${(size / 1024).toFixed(2)} KB`;
|
||||
if (size < 1024 * 1024 * 1024) return `${(size / 1024 / 1024).toFixed(2)} MB`;
|
||||
return `${(size / 1024 / 1024 / 1024).toFixed(2)} GB`;
|
||||
}
|
||||
|
@ -180,7 +180,6 @@ func (c *Channel) ProcessError(err error) error {
|
||||
}
|
||||
content := err.Error()
|
||||
|
||||
fmt.Println(content)
|
||||
if strings.Contains(content, c.GetEndpoint()) {
|
||||
// hide the endpoint
|
||||
replacer := fmt.Sprintf("channel://%d", c.GetId())
|
||||
|
@ -1,11 +1,11 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"chat/globals"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/spf13/viper"
|
||||
"log"
|
||||
)
|
||||
|
||||
var Cache *redis.Client
|
||||
@ -27,7 +27,7 @@ func ConnectRedis() *redis.Client {
|
||||
})
|
||||
|
||||
if err := pingRedis(Cache); err != nil {
|
||||
log.Println(
|
||||
globals.Warn(
|
||||
fmt.Sprintf(
|
||||
"[connection] failed to connect to redis host: %s (message: %s), will retry in 5 seconds",
|
||||
viper.GetString("redis.host"),
|
||||
@ -35,12 +35,12 @@ func ConnectRedis() *redis.Client {
|
||||
),
|
||||
)
|
||||
} else {
|
||||
log.Println(fmt.Sprintf("[connection] connected to redis (host: %s)", viper.GetString("redis.host")))
|
||||
globals.Debug(fmt.Sprintf("[connection] connected to redis (host: %s)", viper.GetString("redis.host")))
|
||||
}
|
||||
|
||||
if viper.GetBool("debug") {
|
||||
Cache.FlushAll(context.Background())
|
||||
log.Println(fmt.Sprintf("[connection] flush redis cache (host: %s)", viper.GetString("redis.host")))
|
||||
globals.Debug(fmt.Sprintf("[connection] flush redis cache (host: %s)", viper.GetString("redis.host")))
|
||||
}
|
||||
return Cache
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"chat/globals"
|
||||
"chat/utils"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/spf13/viper"
|
||||
"log"
|
||||
)
|
||||
|
||||
var DB *sql.DB
|
||||
@ -30,7 +30,7 @@ func ConnectMySQL() *sql.DB {
|
||||
viper.GetString("mysql.db"),
|
||||
))
|
||||
if err != nil || db.Ping() != nil {
|
||||
log.Println(
|
||||
globals.Warn(
|
||||
fmt.Sprintf("[connection] failed to connect to mysql server: %s (message: %s), will retry in 5 seconds",
|
||||
viper.GetString("mysql.host"),
|
||||
utils.GetError(err), // err.Error() may contain nil pointer
|
||||
@ -42,7 +42,7 @@ func ConnectMySQL() *sql.DB {
|
||||
|
||||
return ConnectMySQL()
|
||||
} else {
|
||||
log.Println(fmt.Sprintf("[connection] connected to mysql server (host: %s)", viper.GetString("mysql.host")))
|
||||
globals.Debug(fmt.Sprintf("[connection] connected to mysql server (host: %s)", viper.GetString("mysql.host")))
|
||||
}
|
||||
|
||||
db.SetMaxOpenConns(512)
|
||||
@ -69,21 +69,21 @@ func InitRootUser(db *sql.DB) {
|
||||
var count int
|
||||
err := db.QueryRow("SELECT COUNT(*) FROM auth").Scan(&count)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
globals.Warn(fmt.Sprintf("[service] failed to query user count: %s", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
fmt.Println("[service] no user found, creating root user (username: root, password: chatnio123456, email: root@example.com)")
|
||||
globals.Debug("[service] no user found, creating root user (username: root, password: chatnio123456, email: root@example.com)")
|
||||
_, err := db.Exec(`
|
||||
INSERT INTO auth (username, password, email, is_admin, bind_id, token)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`, "root", utils.Sha2Encrypt("chatnio123456"), "root@example.com", true, 0, "root")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
globals.Warn(fmt.Sprintf("[service] failed to create root user: %s", err.Error()))
|
||||
}
|
||||
} else {
|
||||
fmt.Println(fmt.Sprintf("[service] %d user(s) found, skip creating root user", count))
|
||||
globals.Debug(fmt.Sprintf("[service] %d user(s) found, skip creating root user", count))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,9 +4,12 @@ import (
|
||||
"fmt"
|
||||
"github.com/natefinch/lumberjack"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/viper"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const DefaultLoggerFile = "chatnio.log"
|
||||
|
||||
var Logger *logrus.Logger
|
||||
|
||||
type AppLogger struct {
|
||||
@ -21,6 +24,10 @@ func (l *AppLogger) Format(entry *logrus.Entry) ([]byte, error) {
|
||||
entry.Message,
|
||||
)
|
||||
|
||||
if !viper.GetBool("log.ignore_console") {
|
||||
fmt.Println(data)
|
||||
}
|
||||
|
||||
return []byte(data), nil
|
||||
}
|
||||
|
||||
@ -31,10 +38,10 @@ func init() {
|
||||
})
|
||||
|
||||
Logger.SetOutput(&lumberjack.Logger{
|
||||
Filename: "logs/chat.log",
|
||||
Filename: fmt.Sprintf("logs/%s", DefaultLoggerFile),
|
||||
MaxSize: 1,
|
||||
MaxBackups: 500,
|
||||
MaxAge: 1,
|
||||
MaxAge: 21, // 3 weeks
|
||||
})
|
||||
|
||||
Logger.SetLevel(logrus.DebugLevel)
|
||||
|
@ -6,7 +6,6 @@ import (
|
||||
"chat/utils"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
func (c *Conversation) SaveConversation(db *sql.DB) bool {
|
||||
@ -28,7 +27,7 @@ func (c *Conversation) SaveConversation(db *sql.DB) bool {
|
||||
defer func(stmt *sql.Stmt) {
|
||||
err := stmt.Close()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
globals.Warn(err)
|
||||
}
|
||||
}(stmt)
|
||||
|
||||
|
@ -132,7 +132,6 @@ func transformContent(content interface{}) string {
|
||||
func transform(m []Message) []globals.Message {
|
||||
var messages []globals.Message
|
||||
for _, v := range m {
|
||||
fmt.Println(transformContent(v.Content))
|
||||
messages = append(messages, globals.Message{
|
||||
Role: v.Role,
|
||||
Content: transformContent(v.Content),
|
||||
|
89
utils/fs.go
89
utils/fs.go
@ -1,6 +1,9 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"chat/globals"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@ -42,10 +45,15 @@ func WriteFile(path string, data string, folderSafe bool) bool {
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer file.Close()
|
||||
defer func(file *os.File) {
|
||||
err := file.Close()
|
||||
if err != nil {
|
||||
globals.Warn(fmt.Sprintf("[utils] close file error: %s (path: %s)", err.Error(), path))
|
||||
}
|
||||
}(file)
|
||||
|
||||
if _, err := file.WriteString(data); err != nil {
|
||||
fmt.Println(err.Error())
|
||||
globals.Warn(fmt.Sprintf("[utils] write file error: %s (path: %s, bytes len: %d)", err.Error(), path, len(data)))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@ -68,6 +76,44 @@ func Walk(path string) []string {
|
||||
return files
|
||||
}
|
||||
|
||||
func GetFileSize(path string) int64 {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
defer func(file *os.File) {
|
||||
err := file.Close()
|
||||
if err != nil {
|
||||
globals.Warn(fmt.Sprintf("[utils] close file error: %s (path: %s)", err.Error(), path))
|
||||
}
|
||||
}(file)
|
||||
|
||||
stat, err := file.Stat()
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return stat.Size()
|
||||
}
|
||||
|
||||
func GetFileCreated(path string) string {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer func(file *os.File) {
|
||||
err := file.Close()
|
||||
if err != nil {
|
||||
globals.Warn(fmt.Sprintf("[utils] close file error: %s (path: %s)", err.Error(), path))
|
||||
}
|
||||
}(file)
|
||||
|
||||
stat, err := file.Stat()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return stat.ModTime().String()
|
||||
}
|
||||
|
||||
func IsFileExist(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil || os.IsExist(err)
|
||||
@ -81,7 +127,7 @@ func CopyFile(src string, dst string) error {
|
||||
defer func(in *os.File) {
|
||||
err := in.Close()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
globals.Warn(fmt.Sprintf("[utils] close file error: %s (path: %s)", err.Error(), src))
|
||||
}
|
||||
}(in)
|
||||
|
||||
@ -93,10 +139,45 @@ func CopyFile(src string, dst string) error {
|
||||
defer func(out *os.File) {
|
||||
err := out.Close()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
globals.Warn(fmt.Sprintf("[utils] close file error: %s (path: %s)", err.Error(), dst))
|
||||
}
|
||||
}(out)
|
||||
|
||||
_, err = io.Copy(out, in)
|
||||
return err
|
||||
}
|
||||
|
||||
func DeleteFile(path string) error {
|
||||
return os.Remove(path)
|
||||
}
|
||||
|
||||
func ReadFileLatestLines(path string, length int) (string, error) {
|
||||
if length <= 0 {
|
||||
return "", errors.New("length must be greater than 0")
|
||||
}
|
||||
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func(file *os.File) {
|
||||
err := file.Close()
|
||||
if err != nil {
|
||||
globals.Warn(fmt.Sprintf("[utils] close file error: %s (path: %s)", err.Error(), path))
|
||||
}
|
||||
}(file)
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
|
||||
var lines []string
|
||||
for scanner.Scan() {
|
||||
lines = append(lines, scanner.Text())
|
||||
}
|
||||
|
||||
if len(lines) < length {
|
||||
length = len(lines)
|
||||
}
|
||||
|
||||
return strings.Join(lines[len(lines)-length:], "\n"), nil
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user