Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b26d107
feat: implement AI customer service configuration and validation
bbbbbbbbbbbbba Aug 25, 2026
0b5bc1d
feat: add AI customer service user token endpoint and related functio…
bbbbbbbbbbbbba Aug 25, 2026
80dc488
feat: add AI customer service status localization in English and Chinese
bbbbbbbbbbbbba Aug 25, 2026
72039b9
feat: enhance localization for community features and documentation c…
bbbbbbbbbbbbba Aug 29, 2026
095016d
feat: simplify support center layout and remove unused components
bbbbbbbbbbbbba Aug 29, 2026
fd686c5
feat: refactor support components to use SupportPageLayout and enhanc…
bbbbbbbbbbbbba Aug 30, 2026
7e24da7
feat: refactor CommunityCategoryNav and CommunityCategoryMenuContent …
bbbbbbbbbbbbba Aug 30, 2026
7693d9d
feat: integrate ScrollArea and ScrollBar components for improved navi…
bbbbbbbbbbbbba Aug 30, 2026
ba12527
feat: enhance ScrollArea component with customizable scrollbar and im…
bbbbbbbbbbbbba Aug 30, 2026
c85ccb9
feat: refactor SupportPageLayout and related components to use startA…
bbbbbbbbbbbbba Aug 30, 2026
3da53b7
feat: set startAsideWidth to 16rem in SupportPageLayout for consisten…
bbbbbbbbbbbbba Aug 30, 2026
c47d174
feat: enhance PostCard and CommunityPostList components with improved…
bbbbbbbbbbbbba Aug 30, 2026
22ddaf0
Refactor community post and comment handling to use simplified user i…
bbbbbbbbbbbbba Aug 30, 2026
ec38550
feat: enhance LoadMoreContent component to handle error state during …
bbbbbbbbbbbbba Aug 30, 2026
2f69a65
feat: create EmptyState component and refactor usage in CommunityPost…
bbbbbbbbbbbbba Aug 30, 2026
9361756
feat: enhance EmptyState component with additional props for descript…
bbbbbbbbbbbbba Aug 30, 2026
f976533
feat: update CommunityFrame and PostDetail components to support cond…
bbbbbbbbbbbbba Aug 30, 2026
66e8469
feat: refactor PostDetail component to improve layout and enhance com…
bbbbbbbbbbbbba Aug 30, 2026
cc26f93
feat: add asset retrieval endpoint and update avatar handling across …
bbbbbbbbbbbbba Aug 30, 2026
4da8a06
feat: remove border from comments section for improved layout
bbbbbbbbbbbbba Aug 30, 2026
433eaef
feat: update PostDetail component to improve text wrapping and adjust…
bbbbbbbbbbbbba Aug 30, 2026
d006704
feat: implement avatar retrieval endpoints and update avatar handling…
bbbbbbbbbbbbba Aug 30, 2026
9eb43c1
feat: add support for post reactions and update post detail response …
bbbbbbbbbbbbba Aug 30, 2026
2d55a53
feat: refactor avatar handling by removing utility functions and addi…
bbbbbbbbbbbbba Aug 30, 2026
1f109b2
feat: update like button styling to reflect post reaction state
bbbbbbbbbbbbba Aug 30, 2026
a61826f
feat: add category button to PostDetail for category navigation
bbbbbbbbbbbbba Aug 30, 2026
065241f
feat: enhance PostStatusBadge styling for improved visibility and use…
bbbbbbbbbbbbba Aug 30, 2026
804d3a3
feat: improve PostDetail header styling for better layout and readabi…
bbbbbbbbbbbbba Aug 30, 2026
f5b252f
增加消息处理占位,提升用户体验;增加系统日志功能,允许界面直接查看最近一个月info以上级别日志;
ExtremelyTroublesome Sep 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
546 changes: 273 additions & 273 deletions AGENTS.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ RUN pnpm build:sdk && pnpm build

FROM golang:1.26-alpine AS server-builder
WORKDIR /src
ENV GOPROXY=https://goproxy.cn,direct

RUN apk add --no-cache git
COPY go.mod go.sum ./
Expand All @@ -43,6 +44,7 @@ RUN --mount=type=cache,target=/go/pkg/mod \

FROM golang:1.26-trixie AS server-builder-lancedb
WORKDIR /src
ENV GOPROXY=https://goproxy.cn,direct

ARG TARGETOS=linux
ARG TARGETARCH
Expand Down
10 changes: 8 additions & 2 deletions README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,15 +257,21 @@ flowchart LR
如果只需要构建应用镜像,可以自行准备 MySQL 和 Qdrant,并挂载配置文件:

```bash
docker build -t mlogclub/agent-desk .
docker build -t yekay/agent-desk:1.6.3.3 .
docker run --rm -p 8083:8083 \
-v $(pwd)/docker/agent-desk.yaml:/app/config/config.yaml:ro \
-v agent-desk-data:/app/data \
mlogclub/agent-desk
yekay/agent-desk:1.6.3.3
```

Compose 使用 [docker/agent-desk.yaml](docker/agent-desk.yaml) 作为容器内配置,应用会通过 Docker 内部服务名访问 `mysql` 和 `qdrant`。

导出镜像:

```bash
docker save -o agent-desk.tar yekay/agent-desk:1.6.3.3
```

## 开源定位

`AgentDesk` 适合作为以下方向的开源基础项目:
Expand Down
314 changes: 314 additions & 0 deletions cmd/import_jsh_knowledge/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,314 @@
// Package main 是一次性数据导入工具:将本地 jsh-knowledge 目录按层级
// 导入 t_knowledge_directory / t_knowledge_document。
//
// 用法:
//
// go run ./cmd/import_jsh_knowledge -kb 3 -root ./jsh-knowledge
//
// 工具可重复执行(幂等):已存在的同名目录会复用,同目录下同名文档会跳过。
package main

import (
"crypto/sha256"
"encoding/hex"
"flag"
"fmt"
"log"
"log/slog"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"time"

"agent-desk/internal/ai/rag"
"agent-desk/internal/models"
"agent-desk/internal/pkg/enums"

"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
)

// frontmatterRe 匹配文件开头的 YAML frontmatter(---\n ... \n---\n)。
var frontmatterRe = regexp.MustCompile(`(?s)\A---\r?\n.*?\r?\n---\r?\n?`)

// leadingNumberRe 提取目录名开头的数字前缀(如 "02-合作平台" -> 2)。
var leadingNumberRe = regexp.MustCompile(`^\d+`)

type options struct {
dsn string
root string
kbID int64
stripFrontmatter bool
dryRun bool
}

func main() {
opt := options{}
flag.StringVar(&opt.dsn, "dsn", "root:123456abcde@tcp(127.0.0.1:3306)/cs_ai_agent?charset=utf8mb4&parseTime=True&loc=Local", "MySQL DSN")
flag.StringVar(&opt.root, "root", "jsh-knowledge", "待导入的知识目录根路径")
flag.Int64Var(&opt.kbID, "kb", 3, "目标知识库 ID(t_knowledge_base.id)")
flag.BoolVar(&opt.stripFrontmatter, "strip-frontmatter", true, "是否剥离 Markdown 开头的 YAML frontmatter")
flag.BoolVar(&opt.dryRun, "dry-run", false, "只演练不写库")
flag.Parse()

if err := run(opt); err != nil {
slog.Error("import failed", "error", err)
os.Exit(1)
}
}

func run(opt options) error {
entries, err := scanRoot(opt.root)
if err != nil {
return err
}
if len(entries) == 0 {
return fmt.Errorf("目录 %s 下没有可导入的子目录", opt.root)
}

db, err := gorm.Open(mysql.Open(opt.dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{TablePrefix: "t_", SingularTable: true},
Logger: logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
SlowThreshold: time.Second,
LogLevel: logger.Warn,
IgnoreRecordNotFoundError: true,
},
),
})
if err != nil {
return fmt.Errorf("连接数据库失败: %w", err)
}

// 校验目标知识库存在。
var kb models.KnowledgeBase
if err := db.Select("id", "name", "knowledge_type", "status").
First(&kb, opt.kbID).Error; err != nil {
return fmt.Errorf("知识库 id=%d 不存在或不可读: %w", opt.kbID, err)
}
slog.Info("目标知识库", "id", kb.ID, "name", kb.Name, "type", kb.KnowledgeType, "status", kb.Status)

var dirCreated, dirReused, docCreated, docSkipped int
now := time.Now()

for idx, entry := range entries {
sortNo := parseLeadingNumber(entry.name)
if sortNo == 0 {
sortNo = idx + 1
}

// 每个分类目录一个事务:目录行与其下文档要么全部落库要么整体回滚。
var dirID int64
var dirNew bool
var n importCounters
err := db.Transaction(func(tx *gorm.DB) error {
var terr error
dirID, dirNew, terr = findOrCreateDirectory(tx, opt, entry.name, sortNo, now)
if terr != nil {
return terr
}
n, terr = importDocuments(tx, opt, dirID, entry.files, now)
return terr
})
if err != nil {
return fmt.Errorf("目录 %s 处理失败: %w", entry.name, err)
}
if dirNew {
dirCreated++
} else {
dirReused++
}
docCreated += n.created
docSkipped += n.skipped
slog.Info("目录完成",
"name", entry.name, "dir_id", dirID, "new_dir", dirNew,
"new_docs", n.created, "skipped_docs", n.skipped)
}

slog.Info("导入完成",
"dry_run", opt.dryRun,
"directories_created", dirCreated,
"directories_reused", dirReused,
"documents_created", docCreated,
"documents_skipped", docSkipped)
return nil
}

type dirEntry struct {
name string
files []string
}

func scanRoot(root string) ([]dirEntry, error) {
top, err := os.ReadDir(root)
if err != nil {
return nil, fmt.Errorf("读取根目录失败: %w", err)
}
var out []dirEntry
for _, d := range top {
if !d.IsDir() {
continue
}
full := filepath.Join(root, d.Name())
files, err := collectMarkdownFiles(full)
if err != nil {
return nil, err
}
if len(files) == 0 {
slog.Warn("目录下没有 md 文件,已跳过", "dir", full)
continue
}
out = append(out, dirEntry{name: d.Name(), files: files})
}
sort.Slice(out, func(i, j int) bool { return out[i].name < out[j].name })
return out, nil
}

// collectMarkdownFiles 递归收集 .md 文件(当前数据只有一层,递归以兼容未来嵌套)。
func collectMarkdownFiles(dir string) ([]string, error) {
var files []string

err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
if path != dir {
// 当前导入器只支持一层分类目录;出现嵌套时显式告警,
// 避免文件被静默平铺到错误的目录下。
slog.Warn("发现嵌套子目录,其内文件将被平铺到一级目录(如需多层请扩展导入器)", "subdir", path)
}
return nil
}
if strings.EqualFold(filepath.Ext(d.Name()), ".md") {
files = append(files, path)
}
return nil
})
if err != nil {
return nil, err
}
sort.Strings(files)
return files, nil
}

func parseLeadingNumber(name string) int {
m := leadingNumberRe.FindString(name)
if m == "" {
return 0
}
n, err := strconv.Atoi(m)
if err != nil {
return 0
}
return n
}

// findOrCreateDirectory 在同一知识库/同一父级(0)下按名称查找目录,没有则创建。
func findOrCreateDirectory(db *gorm.DB, opt options, name string, sortNo int, now time.Time) (int64, bool, error) {
var existing models.KnowledgeDirectory
err := db.Where("knowledge_base_id = ? AND parent_id = 0 AND name = ?", opt.kbID, name).
First(&existing).Error
if err == nil {
return existing.ID, false, nil
}
if err != gorm.ErrRecordNotFound {
return 0, false, err
}

dir := &models.KnowledgeDirectory{
KnowledgeBaseID: opt.kbID,
ParentID: 0,
Name: name,
SortNo: sortNo,
Status: enums.StatusOk,
AuditFields: buildAuditFields(now),
}
if opt.dryRun {
slog.Info("[dry-run] 将创建目录", "name", name, "sort_no", sortNo)
return int64(sortNo), true, nil
}
if err := db.Create(dir).Error; err != nil {
return 0, false, err
}
return dir.ID, true, nil
}

type importCounters struct {
created int
skipped int
}

func importDocuments(db *gorm.DB, opt options, dirID int64, files []string, now time.Time) (importCounters, error) {
var c importCounters
for _, path := range files {
title := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))

var existing models.KnowledgeDocument
err := db.Where("knowledge_base_id = ? AND directory_id = ? AND title = ?", opt.kbID, dirID, title).
First(&existing).Error
if err == nil {
c.skipped++
continue
}
if err != gorm.ErrRecordNotFound {
return c, err
}

raw, err := os.ReadFile(path)
if err != nil {
return c, fmt.Errorf("读取文件 %s 失败: %w", path, err)
}
content := string(raw)
if opt.stripFrontmatter {
content = frontmatterRe.ReplaceAllString(content, "")
}
content = strings.TrimSpace(content)
if content == "" {
slog.Warn("文档内容为空,仍将导入", "file", path)
}

doc := &models.KnowledgeDocument{
KnowledgeBaseID: opt.kbID,
DirectoryID: dirID,
Title: title,
ContentType: enums.KnowledgeDocumentContentTypeMarkdown,
Content: content,
Status: enums.StatusOk,
IndexStatus: enums.KnowledgeDocumentIndexStatusPending,
AuditFields: buildAuditFields(now),
}
// 与正常建文档流程保持一致:hash 取正文纯文本的 SHA-256。
if plain := rag.ExtractPlainText(content, enums.KnowledgeDocumentContentTypeMarkdown); plain != "" {
sum := sha256.Sum256([]byte(plain))
doc.ContentHash = hex.EncodeToString(sum[:])
}

if opt.dryRun {
c.created++
continue
}
if err := db.Create(doc).Error; err != nil {
return c, fmt.Errorf("写入文档 %s 失败: %w", path, err)
}
c.created++
}
return c, nil
}

func buildAuditFields(now time.Time) models.AuditFields {
return models.AuditFields{
CreatedAt: now,
CreateUserID: 0,
CreateUserName: "system",
UpdatedAt: now,
UpdateUserID: 0,
UpdateUserName: "system",
}
}
13 changes: 13 additions & 0 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ package main
import (
"flag"
"log/slog"
"os"
"os/signal"
"syscall"

"agent-desk/internal/bootstrap"
"agent-desk/internal/pkg/config"
"agent-desk/internal/pkg/logx"
)

func main() {
Expand All @@ -25,6 +29,15 @@ func main() {
return
}

// 监听退出信号,确保 db sink 中缓冲的日志在进程退出前 flush 到数据库。
go func() {
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
<-stop
_ = logx.Close()
os.Exit(0)
}()

if err := app.Run(cfg.Server.Address()); err != nil {
slog.Error("start server failed", "error", err)
return
Expand Down
Loading