Compare commits

...

81 Commits

Author SHA1 Message Date
Coltea 24a5a1574b
Merge pull request #1566 from KuaiYu95/fe/stopPropagation
fix: 编辑器快捷键阻止冒泡事件问题
2025-11-26 18:35:45 +08:00
yu.kuai 9f9e6388fe fix: 编辑器快捷键阻止冒泡事件问题 2025-11-26 18:32:35 +08:00
Coltea 5ba7c6b73a
Merge pull request #1565 from KuaiYu95/fix/editor
修复了编辑器的一些问题
2025-11-26 17:57:41 +08:00
yu.kuai 9350e631a4 fix: 表格中已合并单元格中间无法正常插入行/列的问题
fix: 移除编辑器扩展不必要的 z-index
fix: 修复无序列表图标移位的问题
fix: 修复任务列表中子任务受父任务影响的问题
fix: 代码块样式更新
2025-11-26 17:52:45 +08:00
Coltea 039b6b3061
Merge pull request #1563 from coltea/update-pro2
pro update
2025-11-26 15:26:51 +08:00
coltea 9fa8ad5177 pro update 2025-11-26 15:24:08 +08:00
Coltea 2d7c20c799
Merge pull request #1560 from coltea/feat-pv
feat: pv 支持浏览量展示
2025-11-26 14:56:01 +08:00
coltea 4a980b92c5 feat: pv 2025-11-26 14:35:59 +08:00
Coltea fe2d70bfa9
Merge pull request #1559 from KuaiYu95/fix/editor
修复了编辑器一些问题
2025-11-25 18:15:05 +08:00
yu.kuai c9f3a61e86 fix: 导出 markdown 错误
fix: 图片预览底部呈现工具栏
fix: 表格 100% 宽度出现滚动条的问题
2025-11-25 18:04:29 +08:00
Coltea 7b75f1bf55
Merge pull request #1558 from jiangwel/chore-mcp-placeholder
docs(设置页面): 更新MCP Tool输入框的默认值提示文本
2025-11-25 17:36:12 +08:00
jiangwel 07646f0823 chore: 设置默认的tool_name和tool_desc值
设置CardMCP组件中tool_name和tool_desc的默认值,确保功能正常运行
2025-11-25 16:26:52 +08:00
Coltea 63a8d8a743
Merge pull request #1557 from guanweiwang/feature/create_wiki_model
pref: 创建wiki站点,模型配置保存按钮合并到下一步按钮
2025-11-25 16:10:19 +08:00
Coltea 587c2842be
Merge pull request #1555 from coltea/feat-sitemap
feat: auto sitemap
2025-11-25 16:09:44 +08:00
coltea fd34bb4c55 feat: auto sitemap 2025-11-25 16:04:09 +08:00
Gavan 46afba778f pref: 创建wiki站点,模型配置保存按钮合并到下一步按钮 2025-11-25 11:38:55 +08:00
Coltea 79234c2394
Merge pull request #1547 from jiangwel/feat-mcp
feat: mcp server
2025-11-24 20:40:36 +08:00
jiangwel 67e796dbb7 feat(chat): 添加 ChatRagOnlyRequset 结构并重构 ChatRagOnly 逻辑
重构 ChatRagOnly 方法,使用新的 ChatRagOnlyRequset 结构作为参数
简化聊天逻辑,直接获取并返回相关文档内容
移除不必要的会话管理代码
2025-11-24 20:21:17 +08:00
Coltea 55c563cc48
Merge pull request #1554 from KuaiYu95/fix/key
fix: 修复ctrlKey+b 富文本模式下加粗同时收起展示目录的问题
2025-11-24 19:02:33 +08:00
yu.kuai cfdc546d20 fix: 修复ctrlKey+b 富文本模式下加粗同时收起展示目录的问题 2025-11-24 18:56:45 +08:00
Coltea 7f58308dae
Merge pull request #1553 from KuaiYu95/fe/editor-update
表格功能丰富,优化编辑器体验
2025-11-24 18:25:13 +08:00
yu.kuai f34864621a feat: 表格功能丰富
----
1. hover 展示 table handle,点击可对表格的行/列进行操作
2. hover 展示 insert button,点击可相对当前行左右插入行,当前列左右插入列
3. 多选选中单元格支持对选中单元格设置
4. hover 最后一行/列展示 insert handle,点击可最后一行/列后面插入行/列
----

fix: 修复了编辑器的一些问题
----
1. 修复了编辑模式图片不能预览的问题
2. 修复了设置文字大小后,行高未能自动变化的问题
3. 修复了创建标题,输入拼音被打断 IME 问题
4. 修复了编辑页面编辑器聚焦时 cmd+s 保存无反应
5. 修复了空格缩进导致代码块不展示的问题
6. 视频支持设置自适应宽度,四角拖拽改变宽高,支持水平对齐设置
----
2025-11-24 18:14:14 +08:00
jiangwel da9039ff37 feat: mcp server 2025-11-24 15:48:47 +08:00
Coltea 797e0c033d
Merge pull request #1549 from guanweiwang/feature/pref
pref: 优化统计 tab 展示
2025-11-24 14:18:58 +08:00
Gavan ea6f958d24 pref: 优化统计 tab 展示 2025-11-21 17:31:50 +08:00
Coltea d3502e105a
Merge pull request #1546 from coltea/fix-conversation-copyright
fix: conversation copyright valid
2025-11-21 15:25:41 +08:00
coltea df4937aeb2 fix: conversation copyright valid 2025-11-21 15:22:57 +08:00
Coltea d8c869198e
Merge pull request #1542 from coltea/feat-copyright
Feat 前台问答版权配置 && 支持超级管理员修改普通用户密码
2025-11-20 18:19:44 +08:00
Coltea f04f96d894
Merge pull request #1544 from guanweiwang/feature/pref
feat: 添加智能问答版权信息, 修改超级管理员权限, 优化 banner 移动端样式, 优化 header 移动端样式
2025-11-20 18:19:28 +08:00
Gavan 74e87540e0 feat: 添加智能问答版权信息, 修改超级管理员权限, 优化 banner 移动端样式, 优化 header 移动端样式 2025-11-20 18:15:30 +08:00
coltea 4a011aa1d2 feat conversation setting 2025-11-20 16:12:03 +08:00
coltea acf17e94b2 feat admin reset user password 2025-11-20 15:46:44 +08:00
Coltea 59ca885518
Merge pull request #1539 from KuaiYu95/fe/doc
文档前台查看功能
2025-11-19 18:48:14 +08:00
Coltea 0e64ff946f
Merge pull request #1540 from coltea/feat-node-list-publisher
feat 文档相关页面支持直接跳转至前台
2025-11-19 18:47:43 +08:00
coltea 19e6a66809 feat node list publish id 2025-11-19 18:43:10 +08:00
yu.kuai c15272aeb2 feat: 分享文档 2025-11-19 18:39:18 +08:00
yu.kuai 32ed999b48 fix: 挂件 esc 快捷关闭 2025-11-19 17:24:15 +08:00
Coltea 98e4a917e0
Merge pull request #1538 from guanweiwang/feature/icon
pref: 统一图标地址, 去除无用文件
2025-11-19 17:13:59 +08:00
Gavan 6e5f780771 pref: 统一图标地址, 去除无用文件 2025-11-19 17:08:28 +08:00
Coltea 95bd31b8ed
Merge pull request #1535 from KuaiYu95/fe/widget4
feat: 添加版本控制
2025-11-18 18:16:13 +08:00
yu.kuai 8457544a30 feat: 添加版本控制 2025-11-18 18:09:47 +08:00
Coltea 55abd7452c
Merge pull request #1532 from KuaiYu95/fe/widget3
挂件优化
2025-11-18 17:18:04 +08:00
Coltea 6224d713b6
Merge pull request #1534 from coltea/feat-widget-copyright
feat widget copyright
2025-11-18 17:17:14 +08:00
coltea 3f93246d85 feat widget copyright 2025-11-18 17:02:41 +08:00
yu.kuai 3196f2d130 feat: 挂件新增版权配置 2025-11-18 16:38:10 +08:00
yu.kuai 4a9a1ff78b fix: 修复挂件点击位置偏移的问题
fix: markdown 格式下适配 附件和链接
2025-11-18 10:46:18 +08:00
Coltea e80cbf9f47
Merge pull request #1530 from guanweiwang/hotfix/bug
fix: 暗黑模式样式, 优化首屏渲染闪烁
2025-11-17 18:56:54 +08:00
Gavan 76f9878d55 fix: 暗黑模式样式, 优化首屏渲染闪烁 2025-11-17 18:51:05 +08:00
Coltea eb662208dc
Merge pull request #1528 from guanweiwang/feature/catalog
feat: 支持文档目录页
2025-11-17 18:23:38 +08:00
Coltea 0dcd65961b
Merge pull request #1529 from KuaiYu95/fe/widget-2
feat: 挂件支持暗黑模式
2025-11-17 18:16:37 +08:00
Coltea dafb8de41e
Merge pull request #1527 from coltea/fix-folder-detail
feat 前台文档目录页 && 前台问答支持流程图
2025-11-17 18:12:26 +08:00
coltea de86adf90f feat html2md data-code 2025-11-17 18:11:35 +08:00
yu.kuai 4c0990df8c feat: 挂件支持暗黑模式
fix: 粘贴 excel 复制的 表格被认为是图片
2025-11-17 17:43:31 +08:00
Gavan 87d24e06c4 feat: 目录可以点击 2025-11-17 17:21:20 +08:00
coltea b1b354b785 fix share folder detail 2025-11-17 14:16:46 +08:00
xiaomakuaiz e9d30eb3d4
Merge pull request #1512 from xiaomakuaiz/fix/openai-api-compatibility
修复 /share/v1/chat/completions 接口 OpenAI 兼容性问题
2025-11-14 20:36:32 +08:00
xiaomakuaiz b8f2b95f22 完善 OpenAI API 兼容性:增强 OpenAIContentPart 结构体
- 添加 ImageURL 字段支持 image_url 类型的内容部分
- 新增 OpenAIContentPartURL 结构体处理嵌套的 URL 对象
- 修复 MarshalJSON 使用指针接收器以保持与 UnmarshalJSON 的一致性
- 修复 String 方法的空格逻辑,使用 builder.Len() 替代索引判断

修复了测试用例中 image_url 数据被静默忽略的问题,确保正确解析包含图片 URL 的混合内容。

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: MonkeyCode-AI <monkeycode-ai@chaitin.com>
Co-authored-by: MonkeyCode-AI <monkeycode-ai@chaitin.com>
Co-authored-by: MonkeyCode-AI <monkeycode-ai@chaitin.com>
Co-authored-by: MonkeyCode-AI <monkeycode-ai@chaitin.com>
2025-11-14 19:41:37 +08:00
xiaomakuaiz 98c602819f lint: make golangci-lint happy
Co-authored-by: MonkeyCode-AI <monkeycode-ai@chaitin.com>
Co-authored-by: MonkeyCode-AI <monkeycode-ai@chaitin.com>
Co-authored-by: MonkeyCode-AI <monkeycode-ai@chaitin.com>
Co-authored-by: MonkeyCode-AI <monkeycode-ai@chaitin.com>
2025-11-14 19:41:37 +08:00
xiaomakuaiz be163a5f80 优化 OpenAI API 兼容性实现
根据 PR #1512 代码审查意见,进行以下优化:

**安全性修复 (P0)**
- 修复 JSON 注入安全漏洞:移除不安全的字符串拼接构造 JSON 的方式
- 添加 NewStringContent 和 NewArrayContent 构造函数,直接构造对象而非通过 JSON 序列化

**性能优化**
- String() 方法使用 strings.Builder 替代字符串拼接,提升性能
- 多个 text 部分之间添加空格分隔符,避免语义错误

**测试覆盖**
- 添加完整的单元测试覆盖 MessageContent 类型
- 测试字符串格式解析(包括特殊字符、Unicode、换行符等)
- 测试数组格式解析(单个/多个 text 部分、混合类型)
- 测试无效输入处理
- 测试序列化/反序列化往返

**代码改进**
- 更新 handler/share/chat.go 使用安全的构造函数
- 所有测试通过验证

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: MonkeyCode-AI <monkeycode-ai@chaitin.com>

Co-authored-by: MonkeyCode-AI <monkeycode-ai@chaitin.com>
Co-authored-by: MonkeyCode-AI <monkeycode-ai@chaitin.com>
Co-authored-by: MonkeyCode-AI <monkeycode-ai@chaitin.com>
Co-authored-by: MonkeyCode-AI <monkeycode-ai@chaitin.com>
2025-11-14 19:26:29 +08:00
xiaomakuaiz 4c03078103 修复 /share/v1/chat/completions 接口 OpenAI 兼容性问题
现有的 OpenAI API 兼容接口不支持标准的 OpenAI messages 格式,特别是当 content 字段为数组格式时会解析失败。

1. **扩展 MessageContent 类型**:实现自定义的 JSON 序列化/反序列化,支持 content 既可以是字符串,也可以是包含 text/type 的对象数组
2. **添加 stream_options 支持**:支持 OpenAI 标准的 stream_options 参数(如 include_usage)
3. **更新响应格式**:在流式响应中添加 usage 字段支持,符合 OpenAI 标准

- `domain/openai.go`:
  - 新增 `MessageContent` 类型及其 JSON 序列化方法
  - 新增 `OpenAIStreamOptions` 结构体
  - 更新 `OpenAIMessage.Content` 类型从 string 改为 *MessageContent
  - 在流式响应中添加 usage 字段

- `handler/share/chat.go`:
  - 更新消息内容提取逻辑,使用 MessageContent.String() 方法
  - 修复流式和非流式响应中的 content 序列化

- 已通过单元测试验证 MessageContent 可以正确解析字符串和数组格式
- 编译通过,无语法错误

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com>

Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com>
Co-authored-by: MonkeyCode-AI <monkeycode-ai@chaitin.com>
Co-authored-by: MonkeyCode-AI <monkeycode-ai@chaitin.com>
Co-authored-by: MonkeyCode-AI <monkeycode-ai@chaitin.com>
2025-11-14 19:26:29 +08:00
Coltea 2b372fd81d
Merge pull request #1521 from coltea/fix-widget-search-path
fix widget search path
2025-11-14 19:01:45 +08:00
Coltea 74c6ba131d
Merge pull request #1520 from guanweiwang/hotfix/bug
fix: 修复 浏览器将http自动升级为 https
2025-11-14 19:01:32 +08:00
Coltea 6c77246e34
Merge pull request #1522 from KuaiYu95/fe/apiurl
fix: api url
2025-11-14 19:01:09 +08:00
coltea d347482d31 fix widget search path 2025-11-14 19:00:15 +08:00
yu.kuai 92443dfafe fix: api url 2025-11-14 18:59:52 +08:00
Gavan 1dd2d93010 fix: 修复 浏览器将http自动升级为 https 2025-11-14 18:49:25 +08:00
Coltea 54bf1fd108
Merge pull request #1518 from guanweiwang/feature/more_feat
feat: 退出登录 & 对话记录 & md 渲染允许 img 标签 & 样式优化 & 升级nextjs 依赖
2025-11-14 18:34:28 +08:00
Coltea c23ce398d7
Merge pull request #1517 from coltea/feat-widget
feat 新版挂件机器人 && 用户登出 &&  文档目录页
2025-11-14 18:34:14 +08:00
coltea 2c90932f60 feat node detail folder 2025-11-14 18:33:30 +08:00
Gavan 035ce0284d feat: 退出登录 & 对话记录 & d 渲染允许 img 标签 & 样式优化 & 升级nextjs 依赖 2025-11-14 18:30:49 +08:00
Coltea 93559125c2
Merge pull request #1519 from KuaiYu95/fe/widget
网页挂件支持三种模式
2025-11-14 18:23:04 +08:00
yu.kuai 6c5cf256ac feat: 后台网页挂件新增配置项
feat: 前台网页挂件支持三种模式:悬浮球,侧边吸附,自定义按钮
fix: markdown 编辑器新增配置项:showAutocomplete,highlightActiveLine
fix: markdown 编辑器输入中午拼音与 placeholder 重叠
fix: 使用官方修复后的 migrateMathStrings.ts
fix: 使用官方修复后的 table-of-contents
2025-11-14 17:54:37 +08:00
coltea 23bdfc3d50 feat widget search 2025-11-14 15:53:21 +08:00
coltea 9008c537cd feat logout 2025-11-14 10:44:00 +08:00
Coltea 63a4a964b1
Merge pull request #1516 from guanweiwang/feature/perm
feat: 权限
2025-11-13 19:00:28 +08:00
Coltea 62f2b2eaf5
Merge pull request #1514 from guanweiwang/feature/ai_search
feat: 优化 ai 搜索弹窗
2025-11-13 18:59:54 +08:00
Gavan 5c1c6368b8 feat: add expandable sections for results and thinking content in AiQaContent; update styles and remove unused hooks 2025-11-13 18:59:13 +08:00
Gavan 7282503acf feat: 权限 2025-11-13 18:55:25 +08:00
Coltea 60a4177229
Merge pull request #1511 from coltea/feat-edition
feat edition
2025-11-13 18:52:20 +08:00
coltea 2f706a6100 fix share nodes position 2025-11-13 18:40:33 +08:00
coltea febcb06654 feat edition 2025-11-13 15:50:05 +08:00
252 changed files with 10101 additions and 7333 deletions

View File

@ -30,6 +30,7 @@ type NodeDetailResp struct {
CreatorAccount string `json:"creator_account"`
EditorAccount string `json:"editor_account"`
PublisherAccount string `json:"publisher_account" gorm:"-"`
PV int64 `json:"pv" gorm:"-"`
}
type NodePermissionReq struct {

View File

@ -7,21 +7,23 @@ import (
)
type ShareNodeDetailResp struct {
ID string `json:"id"`
KbID string `json:"kb_id"`
Type domain.NodeType `json:"type"`
Status domain.NodeStatus `json:"status"`
Name string `json:"name"`
Content string `json:"content"`
Meta domain.NodeMeta `json:"meta"`
ParentID string `json:"parent_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Permissions domain.NodePermissions `json:"permissions"`
CreatorId string `json:"creator_id"`
EditorId string `json:"editor_id"`
PublisherId string `json:"publisher_id"`
CreatorAccount string `json:"creator_account"`
EditorAccount string `json:"editor_account"`
PublisherAccount string `json:"publisher_account"`
ID string `json:"id"`
KbID string `json:"kb_id"`
Type domain.NodeType `json:"type"`
Status domain.NodeStatus `json:"status"`
Name string `json:"name"`
Content string `json:"content"`
Meta domain.NodeMeta `json:"meta"`
ParentID string `json:"parent_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Permissions domain.NodePermissions `json:"permissions"`
CreatorId string `json:"creator_id"`
EditorId string `json:"editor_id"`
PublisherId string `json:"publisher_id"`
CreatorAccount string `json:"creator_account"`
EditorAccount string `json:"editor_account"`
PublisherAccount string `json:"publisher_account"`
List []*domain.ShareNodeDetailItem `json:"list" gorm:"-"`
PV int64 `json:"pv" gorm:"-"`
}

View File

@ -1,5 +1,4 @@
//go:build wireinject
// +build wireinject
package main

View File

@ -1,5 +1,4 @@
//go:build wireinject
// +build wireinject
package main

View File

@ -1,5 +1,4 @@
//go:build wireinject
// +build wireinject
package main

View File

@ -24,6 +24,7 @@ const (
SourceTypeDiscordBot SourceType = "discord_bot"
SourceTypeWechatOfficialAccount SourceType = "wechat_official_account"
SourceTypeOpenAIAPI SourceType = "openai_api"
SourceTypeMcpServer SourceType = "mcp_server"
)
func (s SourceType) Name() string {
@ -46,6 +47,8 @@ func (s SourceType) Name() string {
return "Discord 机器人"
case SourceTypeWechatOfficialAccount:
return "微信公众号"
case SourceTypeMcpServer:
return "MCP 服务器"
default:
return ""
}

View File

@ -1,8 +1,6 @@
package consts
import (
"math"
"github.com/labstack/echo/v4"
)
@ -13,28 +11,13 @@ const ContextKeyEdition contextKey = "edition"
type LicenseEdition int32
const (
LicenseEditionFree LicenseEdition = 0 // 开源版
LicenseEditionContributor LicenseEdition = 1 // 联创版
LicenseEditionEnterprise LicenseEdition = 2 // 企业版
LicenseEditionFree LicenseEdition = 0 // 开源版
LicenseEditionProfession LicenseEdition = 1 // 专业版
LicenseEditionEnterprise LicenseEdition = 2 // 企业版
LicenseEditionBusiness LicenseEdition = 3 // 商业版
)
func GetLicenseEdition(c echo.Context) LicenseEdition {
edition, _ := c.Get("edition").(LicenseEdition)
return edition
}
func (e LicenseEdition) GetMaxAuth(sourceType SourceType) int {
switch e {
case LicenseEditionFree:
if sourceType == SourceTypeGitHub {
return 10
}
return 0
case LicenseEditionContributor:
return 10
case LicenseEditionEnterprise:
return math.MaxInt
default:
return 0
}
}

View File

@ -240,7 +240,8 @@ const docTemplate = `{
"wechat_service_bot",
"discord_bot",
"wechat_official_account",
"openai_api"
"openai_api",
"mcp_server"
],
"type": "string",
"x-enum-varnames": [
@ -260,7 +261,8 @@ const docTemplate = `{
"SourceTypeWechatServiceBot",
"SourceTypeDiscordBot",
"SourceTypeWechatOfficialAccount",
"SourceTypeOpenAIAPI"
"SourceTypeOpenAIAPI",
"SourceTypeMcpServer"
],
"name": "source_type",
"in": "query",
@ -3478,7 +3480,7 @@ const docTemplate = `{
"application/json"
],
"tags": [
"share_chat"
"Widget"
],
"summary": "ChatWidget",
"parameters": [
@ -3509,6 +3511,52 @@ const docTemplate = `{
}
}
},
"/share/v1/chat/widget/search": {
"post": {
"description": "WidgetSearch",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Widget"
],
"summary": "WidgetSearch",
"parameters": [
{
"description": "Comment",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.ChatSearchReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/domain.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/domain.ChatSearchResp"
}
}
}
]
}
}
}
}
},
"/share/v1/comment": {
"post": {
"description": "CreateComment",
@ -4067,22 +4115,26 @@ const docTemplate = `{
"enum": [
0,
1,
2
2,
3
],
"x-enum-comments": {
"LicenseEditionContributor": "联创版",
"LicenseEditionBusiness": "商业版",
"LicenseEditionEnterprise": "企业版",
"LicenseEditionFree": "开源版"
"LicenseEditionFree": "开源版",
"LicenseEditionProfession": "专业版"
},
"x-enum-descriptions": [
"开源版",
"联创版",
"企业版"
"专业版",
"企业版",
"商业版"
],
"x-enum-varnames": [
"LicenseEditionFree",
"LicenseEditionContributor",
"LicenseEditionEnterprise"
"LicenseEditionProfession",
"LicenseEditionEnterprise",
"LicenseEditionBusiness"
]
},
"consts.ModelSettingMode": {
@ -4218,7 +4270,8 @@ const docTemplate = `{
"wechat_service_bot",
"discord_bot",
"wechat_official_account",
"openai_api"
"openai_api",
"mcp_server"
],
"x-enum-varnames": [
"SourceTypeDingTalk",
@ -4237,7 +4290,8 @@ const docTemplate = `{
"SourceTypeWechatServiceBot",
"SourceTypeDiscordBot",
"SourceTypeWechatOfficialAccount",
"SourceTypeOpenAIAPI"
"SourceTypeOpenAIAPI",
"SourceTypeMcpServer"
]
},
"consts.StatDay": {
@ -4462,9 +4516,6 @@ const docTemplate = `{
}
]
},
"auto_sitemap": {
"type": "boolean"
},
"body_code": {
"type": "string"
},
@ -4483,6 +4534,9 @@ const docTemplate = `{
"contribute_settings": {
"$ref": "#/definitions/domain.ContributeSettings"
},
"conversation_setting": {
"$ref": "#/definitions/domain.ConversationSetting"
},
"copy_setting": {
"enum": [
"",
@ -4570,6 +4624,14 @@ const docTemplate = `{
}
]
},
"mcp_server_settings": {
"description": "MCP Server Settings",
"allOf": [
{
"$ref": "#/definitions/domain.MCPServerSettings"
}
]
},
"openai_api_bot_settings": {
"description": "OpenAI API Bot settings",
"allOf": [
@ -4593,6 +4655,9 @@ const docTemplate = `{
"search_placeholder": {
"type": "string"
},
"stats_setting": {
"$ref": "#/definitions/domain.StatsSetting"
},
"theme_and_style": {
"$ref": "#/definitions/domain.ThemeAndStyle"
},
@ -4741,9 +4806,6 @@ const docTemplate = `{
}
]
},
"auto_sitemap": {
"type": "boolean"
},
"body_code": {
"type": "string"
},
@ -4762,6 +4824,9 @@ const docTemplate = `{
"contribute_settings": {
"$ref": "#/definitions/domain.ContributeSettings"
},
"conversation_setting": {
"$ref": "#/definitions/domain.ConversationSetting"
},
"copy_setting": {
"$ref": "#/definitions/consts.CopySetting"
},
@ -4840,6 +4905,14 @@ const docTemplate = `{
}
]
},
"mcp_server_settings": {
"description": "MCP Server Settings",
"allOf": [
{
"$ref": "#/definitions/domain.MCPServerSettings"
}
]
},
"openai_api_bot_settings": {
"description": "OpenAI API settings",
"allOf": [
@ -4863,6 +4936,9 @@ const docTemplate = `{
"search_placeholder": {
"type": "string"
},
"stats_setting": {
"$ref": "#/definitions/domain.StatsSetting"
},
"theme_and_style": {
"$ref": "#/definitions/domain.ThemeAndStyle"
},
@ -5000,7 +5076,8 @@ const docTemplate = `{
8,
9,
10,
11
11,
12
],
"x-enum-varnames": [
"AppTypeWeb",
@ -5013,7 +5090,8 @@ const docTemplate = `{
"AppTypeWechatOfficialAccount",
"AppTypeOpenAIAPI",
"AppTypeWecomAIBot",
"AppTypeLarkBot"
"AppTypeLarkBot",
"AppTypeMcpServer"
]
},
"domain.AuthUserInfo": {
@ -5702,6 +5780,17 @@ const docTemplate = `{
}
}
},
"domain.ConversationSetting": {
"type": "object",
"properties": {
"copyright_hide_enabled": {
"type": "boolean"
},
"copyright_info": {
"type": "string"
}
}
},
"domain.CreateKBReleaseReq": {
"type": "object",
"required": [
@ -6311,6 +6400,34 @@ const docTemplate = `{
}
}
},
"domain.MCPServerSettings": {
"type": "object",
"properties": {
"docs_tool_settings": {
"$ref": "#/definitions/domain.MCPToolSettings"
},
"is_enabled": {
"type": "boolean"
},
"sample_auth": {
"$ref": "#/definitions/domain.SimpleAuth"
}
}
},
"domain.MCPToolSettings": {
"type": "object",
"properties": {
"desc": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"domain.MessageContent": {
"type": "object"
},
"domain.MessageFrom": {
"type": "integer",
"enum": [
@ -6529,6 +6646,9 @@ const docTemplate = `{
"position": {
"type": "number"
},
"publisher_id": {
"type": "string"
},
"rag_info": {
"$ref": "#/definitions/domain.RagInfo"
},
@ -6712,6 +6832,9 @@ const docTemplate = `{
"stream": {
"type": "boolean"
},
"stream_options": {
"$ref": "#/definitions/domain.OpenAIStreamOptions"
},
"temperature": {
"type": "number"
},
@ -6834,7 +6957,7 @@ const docTemplate = `{
],
"properties": {
"content": {
"type": "string"
"$ref": "#/definitions/domain.MessageContent"
},
"name": {
"type": "string"
@ -6864,6 +6987,14 @@ const docTemplate = `{
}
}
},
"domain.OpenAIStreamOptions": {
"type": "object",
"properties": {
"include_usage": {
"type": "boolean"
}
}
},
"domain.OpenAITool": {
"type": "object",
"required": [
@ -7130,6 +7261,44 @@ const docTemplate = `{
}
}
},
"domain.ShareNodeDetailItem": {
"type": "object",
"properties": {
"children": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.ShareNodeDetailItem"
}
},
"emoji": {
"type": "string"
},
"id": {
"type": "string"
},
"meta": {
"$ref": "#/definitions/domain.NodeMeta"
},
"name": {
"type": "string"
},
"parent_id": {
"type": "string"
},
"permissions": {
"$ref": "#/definitions/domain.NodePermissions"
},
"position": {
"type": "number"
},
"type": {
"$ref": "#/definitions/domain.NodeType"
},
"updated_at": {
"type": "string"
}
}
},
"domain.SimpleAuth": {
"type": "object",
"properties": {
@ -7214,6 +7383,14 @@ const docTemplate = `{
"StatPageSceneLogin"
]
},
"domain.StatsSetting": {
"type": "object",
"properties": {
"pv_enable": {
"type": "boolean"
}
}
},
"domain.SwitchModeReq": {
"type": "object",
"required": [
@ -7647,15 +7824,39 @@ const docTemplate = `{
"domain.WidgetBotSettings": {
"type": "object",
"properties": {
"btn_id": {
"type": "string"
},
"btn_logo": {
"type": "string"
},
"btn_position": {
"type": "string"
},
"btn_style": {
"type": "string"
},
"btn_text": {
"type": "string"
},
"copyright_hide_enabled": {
"type": "boolean"
},
"copyright_info": {
"type": "string"
},
"disclaimer": {
"type": "string"
},
"is_open": {
"type": "boolean"
},
"modal_position": {
"type": "string"
},
"placeholder": {
"type": "string"
},
"recommend_node_ids": {
"type": "array",
"items": {
@ -7668,6 +7869,9 @@ const docTemplate = `{
"type": "string"
}
},
"search_mode": {
"type": "string"
},
"theme_mode": {
"type": "string"
}
@ -8383,6 +8587,9 @@ const docTemplate = `{
"publisher_id": {
"type": "string"
},
"pv": {
"type": "integer"
},
"status": {
"$ref": "#/definitions/domain.NodeStatus"
},
@ -8536,6 +8743,12 @@ const docTemplate = `{
"kb_id": {
"type": "string"
},
"list": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.ShareNodeDetailItem"
}
},
"meta": {
"$ref": "#/definitions/domain.NodeMeta"
},
@ -8554,6 +8767,9 @@ const docTemplate = `{
"publisher_id": {
"type": "string"
},
"pv": {
"type": "integer"
},
"status": {
"$ref": "#/definitions/domain.NodeStatus"
},

View File

@ -233,7 +233,8 @@
"wechat_service_bot",
"discord_bot",
"wechat_official_account",
"openai_api"
"openai_api",
"mcp_server"
],
"type": "string",
"x-enum-varnames": [
@ -253,7 +254,8 @@
"SourceTypeWechatServiceBot",
"SourceTypeDiscordBot",
"SourceTypeWechatOfficialAccount",
"SourceTypeOpenAIAPI"
"SourceTypeOpenAIAPI",
"SourceTypeMcpServer"
],
"name": "source_type",
"in": "query",
@ -3471,7 +3473,7 @@
"application/json"
],
"tags": [
"share_chat"
"Widget"
],
"summary": "ChatWidget",
"parameters": [
@ -3502,6 +3504,52 @@
}
}
},
"/share/v1/chat/widget/search": {
"post": {
"description": "WidgetSearch",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Widget"
],
"summary": "WidgetSearch",
"parameters": [
{
"description": "Comment",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.ChatSearchReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/domain.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/domain.ChatSearchResp"
}
}
}
]
}
}
}
}
},
"/share/v1/comment": {
"post": {
"description": "CreateComment",
@ -4060,22 +4108,26 @@
"enum": [
0,
1,
2
2,
3
],
"x-enum-comments": {
"LicenseEditionContributor": "联创版",
"LicenseEditionBusiness": "商业版",
"LicenseEditionEnterprise": "企业版",
"LicenseEditionFree": "开源版"
"LicenseEditionFree": "开源版",
"LicenseEditionProfession": "专业版"
},
"x-enum-descriptions": [
"开源版",
"联创版",
"企业版"
"专业版",
"企业版",
"商业版"
],
"x-enum-varnames": [
"LicenseEditionFree",
"LicenseEditionContributor",
"LicenseEditionEnterprise"
"LicenseEditionProfession",
"LicenseEditionEnterprise",
"LicenseEditionBusiness"
]
},
"consts.ModelSettingMode": {
@ -4211,7 +4263,8 @@
"wechat_service_bot",
"discord_bot",
"wechat_official_account",
"openai_api"
"openai_api",
"mcp_server"
],
"x-enum-varnames": [
"SourceTypeDingTalk",
@ -4230,7 +4283,8 @@
"SourceTypeWechatServiceBot",
"SourceTypeDiscordBot",
"SourceTypeWechatOfficialAccount",
"SourceTypeOpenAIAPI"
"SourceTypeOpenAIAPI",
"SourceTypeMcpServer"
]
},
"consts.StatDay": {
@ -4455,9 +4509,6 @@
}
]
},
"auto_sitemap": {
"type": "boolean"
},
"body_code": {
"type": "string"
},
@ -4476,6 +4527,9 @@
"contribute_settings": {
"$ref": "#/definitions/domain.ContributeSettings"
},
"conversation_setting": {
"$ref": "#/definitions/domain.ConversationSetting"
},
"copy_setting": {
"enum": [
"",
@ -4563,6 +4617,14 @@
}
]
},
"mcp_server_settings": {
"description": "MCP Server Settings",
"allOf": [
{
"$ref": "#/definitions/domain.MCPServerSettings"
}
]
},
"openai_api_bot_settings": {
"description": "OpenAI API Bot settings",
"allOf": [
@ -4586,6 +4648,9 @@
"search_placeholder": {
"type": "string"
},
"stats_setting": {
"$ref": "#/definitions/domain.StatsSetting"
},
"theme_and_style": {
"$ref": "#/definitions/domain.ThemeAndStyle"
},
@ -4734,9 +4799,6 @@
}
]
},
"auto_sitemap": {
"type": "boolean"
},
"body_code": {
"type": "string"
},
@ -4755,6 +4817,9 @@
"contribute_settings": {
"$ref": "#/definitions/domain.ContributeSettings"
},
"conversation_setting": {
"$ref": "#/definitions/domain.ConversationSetting"
},
"copy_setting": {
"$ref": "#/definitions/consts.CopySetting"
},
@ -4833,6 +4898,14 @@
}
]
},
"mcp_server_settings": {
"description": "MCP Server Settings",
"allOf": [
{
"$ref": "#/definitions/domain.MCPServerSettings"
}
]
},
"openai_api_bot_settings": {
"description": "OpenAI API settings",
"allOf": [
@ -4856,6 +4929,9 @@
"search_placeholder": {
"type": "string"
},
"stats_setting": {
"$ref": "#/definitions/domain.StatsSetting"
},
"theme_and_style": {
"$ref": "#/definitions/domain.ThemeAndStyle"
},
@ -4993,7 +5069,8 @@
8,
9,
10,
11
11,
12
],
"x-enum-varnames": [
"AppTypeWeb",
@ -5006,7 +5083,8 @@
"AppTypeWechatOfficialAccount",
"AppTypeOpenAIAPI",
"AppTypeWecomAIBot",
"AppTypeLarkBot"
"AppTypeLarkBot",
"AppTypeMcpServer"
]
},
"domain.AuthUserInfo": {
@ -5695,6 +5773,17 @@
}
}
},
"domain.ConversationSetting": {
"type": "object",
"properties": {
"copyright_hide_enabled": {
"type": "boolean"
},
"copyright_info": {
"type": "string"
}
}
},
"domain.CreateKBReleaseReq": {
"type": "object",
"required": [
@ -6304,6 +6393,34 @@
}
}
},
"domain.MCPServerSettings": {
"type": "object",
"properties": {
"docs_tool_settings": {
"$ref": "#/definitions/domain.MCPToolSettings"
},
"is_enabled": {
"type": "boolean"
},
"sample_auth": {
"$ref": "#/definitions/domain.SimpleAuth"
}
}
},
"domain.MCPToolSettings": {
"type": "object",
"properties": {
"desc": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"domain.MessageContent": {
"type": "object"
},
"domain.MessageFrom": {
"type": "integer",
"enum": [
@ -6522,6 +6639,9 @@
"position": {
"type": "number"
},
"publisher_id": {
"type": "string"
},
"rag_info": {
"$ref": "#/definitions/domain.RagInfo"
},
@ -6705,6 +6825,9 @@
"stream": {
"type": "boolean"
},
"stream_options": {
"$ref": "#/definitions/domain.OpenAIStreamOptions"
},
"temperature": {
"type": "number"
},
@ -6827,7 +6950,7 @@
],
"properties": {
"content": {
"type": "string"
"$ref": "#/definitions/domain.MessageContent"
},
"name": {
"type": "string"
@ -6857,6 +6980,14 @@
}
}
},
"domain.OpenAIStreamOptions": {
"type": "object",
"properties": {
"include_usage": {
"type": "boolean"
}
}
},
"domain.OpenAITool": {
"type": "object",
"required": [
@ -7123,6 +7254,44 @@
}
}
},
"domain.ShareNodeDetailItem": {
"type": "object",
"properties": {
"children": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.ShareNodeDetailItem"
}
},
"emoji": {
"type": "string"
},
"id": {
"type": "string"
},
"meta": {
"$ref": "#/definitions/domain.NodeMeta"
},
"name": {
"type": "string"
},
"parent_id": {
"type": "string"
},
"permissions": {
"$ref": "#/definitions/domain.NodePermissions"
},
"position": {
"type": "number"
},
"type": {
"$ref": "#/definitions/domain.NodeType"
},
"updated_at": {
"type": "string"
}
}
},
"domain.SimpleAuth": {
"type": "object",
"properties": {
@ -7207,6 +7376,14 @@
"StatPageSceneLogin"
]
},
"domain.StatsSetting": {
"type": "object",
"properties": {
"pv_enable": {
"type": "boolean"
}
}
},
"domain.SwitchModeReq": {
"type": "object",
"required": [
@ -7640,15 +7817,39 @@
"domain.WidgetBotSettings": {
"type": "object",
"properties": {
"btn_id": {
"type": "string"
},
"btn_logo": {
"type": "string"
},
"btn_position": {
"type": "string"
},
"btn_style": {
"type": "string"
},
"btn_text": {
"type": "string"
},
"copyright_hide_enabled": {
"type": "boolean"
},
"copyright_info": {
"type": "string"
},
"disclaimer": {
"type": "string"
},
"is_open": {
"type": "boolean"
},
"modal_position": {
"type": "string"
},
"placeholder": {
"type": "string"
},
"recommend_node_ids": {
"type": "array",
"items": {
@ -7661,6 +7862,9 @@
"type": "string"
}
},
"search_mode": {
"type": "string"
},
"theme_mode": {
"type": "string"
}
@ -8376,6 +8580,9 @@
"publisher_id": {
"type": "string"
},
"pv": {
"type": "integer"
},
"status": {
"$ref": "#/definitions/domain.NodeStatus"
},
@ -8529,6 +8736,12 @@
"kb_id": {
"type": "string"
},
"list": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.ShareNodeDetailItem"
}
},
"meta": {
"$ref": "#/definitions/domain.NodeMeta"
},
@ -8547,6 +8760,9 @@
"publisher_id": {
"type": "string"
},
"pv": {
"type": "integer"
},
"status": {
"$ref": "#/definitions/domain.NodeStatus"
},

View File

@ -117,20 +117,24 @@ definitions:
- 0
- 1
- 2
- 3
format: int32
type: integer
x-enum-comments:
LicenseEditionContributor: 联创
LicenseEditionBusiness: 商业
LicenseEditionEnterprise: 企业版
LicenseEditionFree: 开源版
LicenseEditionProfession: 专业版
x-enum-descriptions:
- 开源版
- 联创
- 专业
- 企业版
- 商业版
x-enum-varnames:
- LicenseEditionFree
- LicenseEditionContributor
- LicenseEditionProfession
- LicenseEditionEnterprise
- LicenseEditionBusiness
consts.ModelSettingMode:
enum:
- manual
@ -241,6 +245,7 @@ definitions:
- discord_bot
- wechat_official_account
- openai_api
- mcp_server
type: string
x-enum-varnames:
- SourceTypeDingTalk
@ -260,6 +265,7 @@ definitions:
- SourceTypeDiscordBot
- SourceTypeWechatOfficialAccount
- SourceTypeOpenAIAPI
- SourceTypeMcpServer
consts.StatDay:
enum:
- 1
@ -414,8 +420,6 @@ definitions:
allOf:
- $ref: '#/definitions/domain.AIFeedbackSettings'
description: AI feedback
auto_sitemap:
type: boolean
body_code:
type: string
btns:
@ -427,6 +431,8 @@ definitions:
description: catalog settings
contribute_settings:
$ref: '#/definitions/domain.ContributeSettings'
conversation_setting:
$ref: '#/definitions/domain.ConversationSetting'
copy_setting:
allOf:
- $ref: '#/definitions/consts.CopySetting'
@ -482,6 +488,10 @@ definitions:
allOf:
- $ref: '#/definitions/domain.LarkBotSettings'
description: LarkBot
mcp_server_settings:
allOf:
- $ref: '#/definitions/domain.MCPServerSettings'
description: MCP Server Settings
openai_api_bot_settings:
allOf:
- $ref: '#/definitions/domain.OpenAIAPIBotSettings'
@ -496,6 +506,8 @@ definitions:
type: array
search_placeholder:
type: string
stats_setting:
$ref: '#/definitions/domain.StatsSetting'
theme_and_style:
$ref: '#/definitions/domain.ThemeAndStyle'
theme_mode:
@ -589,8 +601,6 @@ definitions:
allOf:
- $ref: '#/definitions/domain.AIFeedbackSettings'
description: AI feedback
auto_sitemap:
type: boolean
body_code:
type: string
btns:
@ -602,6 +612,8 @@ definitions:
description: catalog settings
contribute_settings:
$ref: '#/definitions/domain.ContributeSettings'
conversation_setting:
$ref: '#/definitions/domain.ConversationSetting'
copy_setting:
$ref: '#/definitions/consts.CopySetting'
desc:
@ -652,6 +664,10 @@ definitions:
allOf:
- $ref: '#/definitions/domain.LarkBotSettings'
description: LarkBot
mcp_server_settings:
allOf:
- $ref: '#/definitions/domain.MCPServerSettings'
description: MCP Server Settings
openai_api_bot_settings:
allOf:
- $ref: '#/definitions/domain.OpenAIAPIBotSettings'
@ -666,6 +682,8 @@ definitions:
type: array
search_placeholder:
type: string
stats_setting:
$ref: '#/definitions/domain.StatsSetting'
theme_and_style:
$ref: '#/definitions/domain.ThemeAndStyle'
theme_mode:
@ -759,6 +777,7 @@ definitions:
- 9
- 10
- 11
- 12
format: int32
type: integer
x-enum-varnames:
@ -773,6 +792,7 @@ definitions:
- AppTypeOpenAIAPI
- AppTypeWecomAIBot
- AppTypeLarkBot
- AppTypeMcpServer
domain.AuthUserInfo:
properties:
avatar_url:
@ -1214,6 +1234,13 @@ definitions:
url:
type: string
type: object
domain.ConversationSetting:
properties:
copyright_hide_enabled:
type: boolean
copyright_info:
type: string
type: object
domain.CreateKBReleaseReq:
properties:
kb_id:
@ -1610,6 +1637,24 @@ definitions:
url:
type: string
type: object
domain.MCPServerSettings:
properties:
docs_tool_settings:
$ref: '#/definitions/domain.MCPToolSettings'
is_enabled:
type: boolean
sample_auth:
$ref: '#/definitions/domain.SimpleAuth'
type: object
domain.MCPToolSettings:
properties:
desc:
type: string
name:
type: string
type: object
domain.MessageContent:
type: object
domain.MessageFrom:
enum:
- 1
@ -1757,6 +1802,8 @@ definitions:
$ref: '#/definitions/domain.NodePermissions'
position:
type: number
publisher_id:
type: string
rag_info:
$ref: '#/definitions/domain.RagInfo'
status:
@ -1871,6 +1918,8 @@ definitions:
type: array
stream:
type: boolean
stream_options:
$ref: '#/definitions/domain.OpenAIStreamOptions'
temperature:
type: number
tool_choice:
@ -1952,7 +2001,7 @@ definitions:
domain.OpenAIMessage:
properties:
content:
type: string
$ref: '#/definitions/domain.MessageContent'
name:
type: string
role:
@ -1973,6 +2022,11 @@ definitions:
required:
- type
type: object
domain.OpenAIStreamOptions:
properties:
include_usage:
type: boolean
type: object
domain.OpenAITool:
properties:
function:
@ -2146,6 +2200,31 @@ definitions:
role:
$ref: '#/definitions/schema.RoleType'
type: object
domain.ShareNodeDetailItem:
properties:
children:
items:
$ref: '#/definitions/domain.ShareNodeDetailItem'
type: array
emoji:
type: string
id:
type: string
meta:
$ref: '#/definitions/domain.NodeMeta'
name:
type: string
parent_id:
type: string
permissions:
$ref: '#/definitions/domain.NodePermissions'
position:
type: number
type:
$ref: '#/definitions/domain.NodeType'
updated_at:
type: string
type: object
domain.SimpleAuth:
properties:
enabled:
@ -2202,6 +2281,11 @@ definitions:
- StatPageSceneNodeDetail
- StatPageSceneChat
- StatPageSceneLogin
domain.StatsSetting:
properties:
pv_enable:
type: boolean
type: object
domain.SwitchModeReq:
properties:
auto_mode_api_key:
@ -2488,12 +2572,28 @@ definitions:
type: object
domain.WidgetBotSettings:
properties:
btn_id:
type: string
btn_logo:
type: string
btn_position:
type: string
btn_style:
type: string
btn_text:
type: string
copyright_hide_enabled:
type: boolean
copyright_info:
type: string
disclaimer:
type: string
is_open:
type: boolean
modal_position:
type: string
placeholder:
type: string
recommend_node_ids:
items:
type: string
@ -2502,6 +2602,8 @@ definitions:
items:
type: string
type: array
search_mode:
type: string
theme_mode:
type: string
type: object
@ -2971,6 +3073,8 @@ definitions:
type: string
publisher_id:
type: string
pv:
type: integer
status:
$ref: '#/definitions/domain.NodeStatus'
type:
@ -3075,6 +3179,10 @@ definitions:
type: string
kb_id:
type: string
list:
items:
$ref: '#/definitions/domain.ShareNodeDetailItem'
type: array
meta:
$ref: '#/definitions/domain.NodeMeta'
name:
@ -3087,6 +3195,8 @@ definitions:
type: string
publisher_id:
type: string
pv:
type: integer
status:
$ref: '#/definitions/domain.NodeStatus'
type:
@ -3286,6 +3396,7 @@ paths:
- discord_bot
- wechat_official_account
- openai_api
- mcp_server
in: query
name: source_type
required: true
@ -3308,6 +3419,7 @@ paths:
- SourceTypeDiscordBot
- SourceTypeWechatOfficialAccount
- SourceTypeOpenAIAPI
- SourceTypeMcpServer
produces:
- application/json
responses:
@ -5296,7 +5408,34 @@ paths:
$ref: '#/definitions/domain.Response'
summary: ChatWidget
tags:
- share_chat
- Widget
/share/v1/chat/widget/search:
post:
consumes:
- application/json
description: WidgetSearch
parameters:
- description: Comment
in: body
name: request
required: true
schema:
$ref: '#/definitions/domain.ChatSearchReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/domain.Response'
- properties:
data:
$ref: '#/definitions/domain.ChatSearchResp'
type: object
summary: WidgetSearch
tags:
- Widget
/share/v1/comment:
post:
consumes:

View File

@ -24,6 +24,7 @@ const (
AppTypeOpenAIAPI
AppTypeWecomAIBot
AppTypeLarkBot
AppTypeMcpServer
)
var AppTypes = []AppType{
@ -38,6 +39,7 @@ var AppTypes = []AppType{
AppTypeOpenAIAPI,
AppTypeWecomAIBot,
AppTypeLarkBot,
AppTypeMcpServer,
}
func (t AppType) ToSourceType() consts.SourceType {
@ -92,9 +94,8 @@ type AppSettings struct {
RecommendQuestions []string `json:"recommend_questions,omitempty"`
RecommendNodeIDs []string `json:"recommend_node_ids,omitempty"`
// seo
Desc string `json:"desc,omitempty"`
Keyword string `json:"keyword,omitempty"`
AutoSitemap bool `json:"auto_sitemap,omitempty"`
Desc string `json:"desc,omitempty"`
Keyword string `json:"keyword,omitempty"`
// inject code
HeadCode string `json:"head_code,omitempty"`
BodyCode string `json:"body_code,omitempty"`
@ -161,17 +162,41 @@ type AppSettings struct {
WebAppLandingConfigs []WebAppLandingConfig `json:"web_app_landing_configs,omitempty"`
WebAppLandingTheme WebAppLandingTheme `json:"web_app_landing_theme"`
WatermarkContent string `json:"watermark_content"`
WatermarkSetting consts.WatermarkSetting `json:"watermark_setting" validate:"omitempty,oneof='' hidden visible"`
CopySetting consts.CopySetting `json:"copy_setting" validate:"omitempty,oneof='' append disabled"`
ContributeSettings ContributeSettings `json:"contribute_settings"`
HomePageSetting consts.HomePageSetting `json:"home_page_setting"`
WatermarkContent string `json:"watermark_content"`
WatermarkSetting consts.WatermarkSetting `json:"watermark_setting" validate:"omitempty,oneof='' hidden visible"`
CopySetting consts.CopySetting `json:"copy_setting" validate:"omitempty,oneof='' append disabled"`
ContributeSettings ContributeSettings `json:"contribute_settings"`
HomePageSetting consts.HomePageSetting `json:"home_page_setting"`
ConversationSetting ConversationSetting `json:"conversation_setting"`
// MCP Server Settings
MCPServerSettings MCPServerSettings `json:"mcp_server_settings,omitempty"`
StatsSetting StatsSetting `json:"stats_setting"`
}
type StatsSetting struct {
PVEnable bool `json:"pv_enable"`
}
type ConversationSetting struct {
CopyrightInfo string `json:"copyright_info"`
CopyrightHideEnabled bool `json:"copyright_hide_enabled"`
}
type WebAppLandingTheme struct {
Name string `json:"name"`
}
type MCPServerSettings struct {
IsEnabled bool `json:"is_enabled"`
DocsToolSettings MCPToolSettings `json:"docs_tool_settings"`
SampleAuth SimpleAuth `json:"sample_auth"`
}
type MCPToolSettings struct {
Name string `json:"name"`
Desc string `json:"desc"`
}
type LarkBotSettings struct {
IsEnabled *bool `json:"is_enabled"`
AppID string `json:"app_id"`
@ -399,12 +424,21 @@ type FooterSettings struct {
}
type WidgetBotSettings struct {
IsOpen bool `json:"is_open,omitempty"`
ThemeMode string `json:"theme_mode,omitempty"`
BtnText string `json:"btn_text,omitempty"`
BtnLogo string `json:"btn_logo,omitempty"`
RecommendQuestions []string `json:"recommend_questions,omitempty"`
RecommendNodeIDs []string `json:"recommend_node_ids,omitempty"`
IsOpen bool `json:"is_open,omitempty"`
ThemeMode string `json:"theme_mode,omitempty"`
BtnText string `json:"btn_text,omitempty"`
BtnLogo string `json:"btn_logo,omitempty"`
RecommendQuestions []string `json:"recommend_questions,omitempty"`
RecommendNodeIDs []string `json:"recommend_node_ids,omitempty"`
BtnStyle string `json:"btn_style,omitempty"`
BtnID string `json:"btn_id,omitempty"`
BtnPosition string `json:"btn_position,omitempty"`
ModalPosition string `json:"modal_position,omitempty"`
SearchMode string `json:"search_mode,omitempty"`
Placeholder string `json:"placeholder,omitempty"`
Disclaimer string `json:"disclaimer,omitempty"`
CopyrightInfo string `json:"copyright_info,omitempty"`
CopyrightHideEnabled bool `json:"copyright_hide_enabled,omitempty"`
}
type BrandGroup struct {
@ -452,9 +486,8 @@ type AppSettingsResp struct {
RecommendQuestions []string `json:"recommend_questions,omitempty"`
RecommendNodeIDs []string `json:"recommend_node_ids,omitempty"`
// seo
Desc string `json:"desc,omitempty"`
Keyword string `json:"keyword,omitempty"`
AutoSitemap bool `json:"auto_sitemap,omitempty"`
Desc string `json:"desc,omitempty"`
Keyword string `json:"keyword,omitempty"`
// inject code
HeadCode string `json:"head_code,omitempty"`
BodyCode string `json:"body_code,omitempty"`
@ -528,6 +561,10 @@ type AppSettingsResp struct {
WebAppLandingConfigs []WebAppLandingConfigResp `json:"web_app_landing_configs,omitempty"`
WebAppLandingTheme WebAppLandingTheme `json:"web_app_landing_theme"`
HomePageSetting consts.HomePageSetting `json:"home_page_setting"`
ConversationSetting ConversationSetting `json:"conversation_setting"`
// MCP Server Settings
MCPServerSettings MCPServerSettings `json:"mcp_server_settings,omitempty"`
StatsSetting StatsSetting `json:"stats_setting"`
}
type WebAppLandingConfigResp struct {

View File

@ -23,6 +23,15 @@ type ChatRequest struct {
Info ConversationInfo `json:"-"`
}
type ChatRagOnlyRequest struct {
Message string `json:"message" validate:"required"`
KBID string `json:"-" validate:"required"`
UserInfo UserInfo `json:"user_info"`
AppType AppType `json:"app_type" validate:"required,oneof=1 2"`
}
type ConversationInfo struct {
UserInfo UserInfo `json:"user_info"`
}

45
backend/domain/license.go Normal file
View File

@ -0,0 +1,45 @@
package domain
import (
"context"
"encoding/json"
)
const ContextKeyEditionLimitation contextKey = "edition_limitation"
type BaseEditionLimitation struct {
MaxKb int `json:"max_kb"` // 知识库站点数量
MaxNode int `json:"max_node"` // 单个知识库下文档数量
MaxSSOUser int `json:"max_sso_users"` // SSO认证用户数量
MaxAdmin int64 `json:"max_admin"` // 后台管理员数量
AllowAdminPerm bool `json:"allow_admin_perm"` // 支持管理员分权控制
AllowCustomCopyright bool `json:"allow_custom_copyright"` // 支持自定义版权信息
AllowCommentAudit bool `json:"allow_comment_audit"` // 支持评论审核
AllowAdvancedBot bool `json:"allow_advanced_bot"` // 支持高级机器人配置
AllowWatermark bool `json:"allow_watermark"` // 支持水印
AllowCopyProtection bool `json:"allow_copy_protection"` // 支持内容复制保护
AllowOpenAIBotSettings bool `json:"allow_open_ai_bot_settings"` // 支持问答机器人
AllowMCPServer bool `json:"allow_mcp_server"` // 支持创建MCP Server
AllowNodeStats bool `json:"allow_node_stats"` // 支持文档统计
}
var baseEditionLimitationDefault = BaseEditionLimitation{
MaxKb: 1,
MaxAdmin: 1,
MaxNode: 300,
}
func GetBaseEditionLimitation(c context.Context) BaseEditionLimitation {
edition, ok := c.Value(ContextKeyEditionLimitation).([]byte)
if !ok {
return baseEditionLimitationDefault
}
var editionLimitation BaseEditionLimitation
if err := json.Unmarshal(edition, &editionLimitation); err != nil {
return baseEditionLimitationDefault
}
return editionLimitation
}

View File

@ -174,6 +174,7 @@ type NodeListItemResp struct {
EditorId string `json:"editor_id"`
Creator string `json:"creator"`
Editor string `json:"editor"`
PublisherId string `json:"publisher_id" gorm:"-"`
Permissions NodePermissions `json:"permissions" gorm:"type:jsonb"`
}
@ -251,10 +252,24 @@ type ShareNodeListItemResp struct {
ParentID string `json:"parent_id"`
Position float64 `json:"position"`
Emoji string `json:"emoji"`
Meta NodeMeta `json:"meta"`
UpdatedAt time.Time `json:"updated_at"`
Permissions NodePermissions `json:"permissions" gorm:"type:jsonb"`
}
type ShareNodeDetailItem struct {
ID string `json:"id"`
Name string `json:"name"`
Type NodeType `json:"type"`
ParentID string `json:"parent_id"`
Position float64 `json:"position"`
Emoji string `json:"emoji"`
Meta NodeMeta `json:"meta"`
UpdatedAt time.Time `json:"updated_at"`
Permissions NodePermissions `json:"permissions" gorm:"type:jsonb"`
Children []*ShareNodeDetailItem `json:"children,omitempty"`
}
func (n *ShareNodeListItemResp) GetURL(baseURL string) string {
return fmt.Sprintf("%s/node/%s", baseURL, n.ID)
}

View File

@ -1,10 +1,17 @@
package domain
import (
"encoding/json"
"fmt"
"strings"
)
// OpenAI API 请求结构体
type OpenAICompletionsRequest struct {
Model string `json:"model" validate:"required"`
Messages []OpenAIMessage `json:"messages" validate:"required"`
Stream bool `json:"stream,omitempty"`
StreamOptions *OpenAIStreamOptions `json:"stream_options,omitempty"`
Temperature *float64 `json:"temperature,omitempty"`
MaxTokens *int `json:"max_tokens,omitempty"`
TopP *float64 `json:"top_p,omitempty"`
@ -17,9 +24,95 @@ type OpenAICompletionsRequest struct {
ResponseFormat *OpenAIResponseFormat `json:"response_format,omitempty"`
}
type OpenAIStreamOptions struct {
IncludeUsage bool `json:"include_usage,omitempty"`
}
// MessageContent 支持字符串或内容数组
type MessageContent struct {
isString bool
strValue string
arrValue []OpenAIContentPart
}
// OpenAIContentPart 表示内容数组中的单个元素
type OpenAIContentPart struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
ImageURL *OpenAIContentPartURL `json:"image_url,omitempty"`
}
// OpenAIContentPartURL represents the image_url field in content parts
type OpenAIContentPartURL struct {
URL string `json:"url"`
}
// UnmarshalJSON 自定义解析,支持 string 或 array 格式
func (mc *MessageContent) UnmarshalJSON(data []byte) error {
// 尝试解析为字符串
var str string
if err := json.Unmarshal(data, &str); err == nil {
mc.isString = true
mc.strValue = str
return nil
}
// 尝试解析为数组
var arr []OpenAIContentPart
if err := json.Unmarshal(data, &arr); err == nil {
mc.isString = false
mc.arrValue = arr
return nil
}
return fmt.Errorf("content must be string or array")
}
// MarshalJSON 自定义序列化
func (mc *MessageContent) MarshalJSON() ([]byte, error) {
if mc.isString {
return json.Marshal(mc.strValue)
}
return json.Marshal(mc.arrValue)
}
// NewStringContent 创建字符串类型的 MessageContent
func NewStringContent(s string) *MessageContent {
return &MessageContent{
isString: true,
strValue: s,
}
}
// NewArrayContent 创建数组类型的 MessageContent
func NewArrayContent(parts []OpenAIContentPart) *MessageContent {
return &MessageContent{
isString: false,
arrValue: parts,
}
}
// String 获取文本内容
func (mc *MessageContent) String() string {
if mc.isString {
return mc.strValue
}
// 从数组中提取文本
var builder strings.Builder
for _, part := range mc.arrValue {
if part.Type == "text" {
if builder.Len() > 0 && part.Text != "" {
builder.WriteString(" ")
}
builder.WriteString(part.Text)
}
}
return builder.String()
}
type OpenAIMessage struct {
Role string `json:"role" validate:"required"`
Content string `json:"content,omitempty"`
Content *MessageContent `json:"content,omitempty"`
Name string `json:"name,omitempty"`
ToolCalls []OpenAIToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
@ -90,6 +183,7 @@ type OpenAIStreamResponse struct {
Created int64 `json:"created"`
Model string `json:"model"`
Choices []OpenAIStreamChoice `json:"choices"`
Usage *OpenAIUsage `json:"usage,omitempty"`
}
type OpenAIStreamChoice struct {

View File

@ -0,0 +1,186 @@
package domain
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMessageContent_UnmarshalJSON_String(t *testing.T) {
tests := []struct {
name string
json string
expected string
}{
{"simple string", `"hello"`, "hello"},
{"with quotes", `"say \"hello\""`, `say "hello"`},
{"with newline", `"line1\nline2"`, "line1\nline2"},
{"empty string", `""`, ""},
{"unicode", `"你好 🌍"`, "你好 🌍"},
{"special chars", `"Hello \"World\"\nNew Line\tTab"`, "Hello \"World\"\nNew Line\tTab"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var mc MessageContent
err := json.Unmarshal([]byte(tt.json), &mc)
require.NoError(t, err)
assert.Equal(t, tt.expected, mc.String())
assert.True(t, mc.isString)
})
}
}
func TestMessageContent_UnmarshalJSON_Array(t *testing.T) {
tests := []struct {
name string
json string
expected string
}{
{
"single text part",
`[{"type":"text","text":"Hello"}]`,
"Hello",
},
{
"multiple text parts",
`[{"type":"text","text":"Hello"},{"type":"text","text":"World"}]`,
"Hello World",
},
{
"mixed types with image",
`[{"type":"text","text":"Look at this"},{"type":"image_url","image_url":{"url":"https://example.com/img.png"}},{"type":"text","text":"image"}]`,
"Look at this image",
},
{
"empty array",
`[]`,
"",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var mc MessageContent
err := json.Unmarshal([]byte(tt.json), &mc)
require.NoError(t, err)
assert.Equal(t, tt.expected, mc.String())
assert.False(t, mc.isString)
})
}
}
func TestMessageContent_UnmarshalJSON_Invalid(t *testing.T) {
tests := []struct {
name string
json string
}{
{"number", `123`},
{"boolean", `true`},
{"object", `{"key":"value"}`},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var mc MessageContent
err := json.Unmarshal([]byte(tt.json), &mc)
assert.Error(t, err)
assert.Contains(t, err.Error(), "content must be string or array")
})
}
}
func TestMessageContent_UnmarshalJSON_Null(t *testing.T) {
var mc *MessageContent
err := json.Unmarshal([]byte(`null`), &mc)
assert.NoError(t, err)
assert.Nil(t, mc)
}
func TestMessageContent_MarshalJSON_String(t *testing.T) {
mc := NewStringContent("Hello World")
data, err := json.Marshal(mc)
require.NoError(t, err)
assert.Equal(t, `"Hello World"`, string(data))
}
func TestMessageContent_MarshalJSON_Array(t *testing.T) {
mc := NewArrayContent([]OpenAIContentPart{
{Type: "text", Text: "Hello"},
{Type: "text", Text: "World"},
})
data, err := json.Marshal(mc)
require.NoError(t, err)
assert.JSONEq(t, `[{"type":"text","text":"Hello"},{"type":"text","text":"World"}]`, string(data))
}
func TestMessageContent_Roundtrip_String(t *testing.T) {
original := NewStringContent("Test message with \"quotes\" and \nnewlines")
// Marshal
data, err := json.Marshal(original)
require.NoError(t, err)
// Unmarshal
var decoded MessageContent
err = json.Unmarshal(data, &decoded)
require.NoError(t, err)
// Verify
assert.Equal(t, original.String(), decoded.String())
assert.Equal(t, original.isString, decoded.isString)
}
func TestMessageContent_Roundtrip_Array(t *testing.T) {
parts := []OpenAIContentPart{
{Type: "text", Text: "Part 1"},
{Type: "text", Text: "Part 2"},
}
original := NewArrayContent(parts)
// Marshal
data, err := json.Marshal(original)
require.NoError(t, err)
// Unmarshal
var decoded MessageContent
err = json.Unmarshal(data, &decoded)
require.NoError(t, err)
// Verify
assert.Equal(t, original.String(), decoded.String())
assert.Equal(t, original.isString, decoded.isString)
}
func TestNewStringContent(t *testing.T) {
mc := NewStringContent("test")
assert.NotNil(t, mc)
assert.True(t, mc.isString)
assert.Equal(t, "test", mc.strValue)
assert.Equal(t, "test", mc.String())
}
func TestNewArrayContent(t *testing.T) {
parts := []OpenAIContentPart{
{Type: "text", Text: "Hello"},
}
mc := NewArrayContent(parts)
assert.NotNil(t, mc)
assert.False(t, mc.isString)
assert.Equal(t, parts, mc.arrValue)
assert.Equal(t, "Hello", mc.String())
}
func TestMessageContent_String_EmptyArray(t *testing.T) {
mc := NewArrayContent([]OpenAIContentPart{})
assert.Equal(t, "", mc.String())
}
func TestMessageContent_String_NoTextParts(t *testing.T) {
mc := NewArrayContent([]OpenAIContentPart{
{Type: "image_url", Text: ""},
})
assert.Equal(t, "", mc.String())
}

View File

@ -8,6 +8,7 @@ import (
const (
SettingKeySystemPrompt = "system_prompt"
SettingBlockWords = "block_words"
SettingCopyrightInfo = "本网站由 PandaWiki 提供技术支持"
)
// table: settings

View File

@ -105,3 +105,14 @@ type StatPageHour struct {
func (StatPageHour) TableName() string {
return "stat_page_hours"
}
// NodeStats node表统计数据
type NodeStats struct {
ID int64 `json:"id" gorm:"primaryKey;autoIncrement"`
NodeID string `json:"node_id" gorm:"uniqueIndex"`
PV int64 `json:"pv"`
}
func (NodeStats) TableName() string {
return "node_stats"
}

View File

@ -35,6 +35,7 @@ require (
github.com/larksuite/oapi-sdk-go/v3 v3.4.20
github.com/lib/pq v1.10.9
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20250508043914-ed57fa5c5274
github.com/mark3labs/mcp-go v0.43.0
github.com/microcosm-cc/bluemonday v1.0.27
github.com/mileusna/useragent v1.3.5
github.com/minio/minio-go/v7 v7.0.91
@ -49,6 +50,7 @@ require (
github.com/sbzhu/weworkapi_golang v0.0.0-20210525081115-1799804a7c8d
github.com/silenceper/wechat/v2 v2.1.9
github.com/spf13/viper v1.20.1
github.com/stretchr/testify v1.10.0
github.com/swaggo/echo-swagger v1.4.1
github.com/swaggo/swag v1.16.5
github.com/tidwall/gjson v1.14.1
@ -98,6 +100,7 @@ require (
github.com/cloudwego/eino-ext/components/model/openai v0.0.0-20250710065240-482d48888f25 // indirect
github.com/cloudwego/eino-ext/libs/acl/openai v0.0.0-20250626133421-3c142631c961 // indirect
github.com/cohesion-org/deepseek-go v1.2.8 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
@ -135,6 +138,7 @@ require (
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/invopop/yaml v0.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
@ -165,6 +169,7 @@ require (
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/sagikazarmark/locafero v0.9.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
@ -183,6 +188,7 @@ require (
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/yargevad/filepathx v1.0.0 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect

View File

@ -321,6 +321,8 @@ github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/C
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc=
github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
@ -396,6 +398,8 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mark3labs/mcp-go v0.43.0 h1:lgiKcWMddh4sngbU+hoWOZ9iAe/qp/m851RQpj3Y7jA=
github.com/mark3labs/mcp-go v0.43.0/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@ -576,6 +580,8 @@ github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJ
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc=
github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

View File

@ -2,6 +2,7 @@ package mq
import (
"context"
"time"
"github.com/robfig/cron/v3"
@ -66,6 +67,16 @@ func NewStatCronHandler(logger *log.Logger, statRepo *pg.StatRepository, statUse
func (h *CronHandler) RemoveOldStatData() {
h.logger.Info("remove old stat data start")
// 零点时同步数据至node_stats持久化
if time.Now().Hour() == 0 {
if err := h.statUseCase.MigrateYesterdayPVToNodeStats(context.Background()); err != nil {
h.logger.Error("migrate yesterday PV data to node_stats failed", log.Error(err))
} else {
h.logger.Info("migrate yesterday PV data to node_stats successful")
}
}
err := h.statRepo.RemoveOldData(context.Background())
if err != nil {
h.logger.Error("remove old stat data failed", log.Error(err))

View File

@ -61,6 +61,7 @@ func NewShareChatHandler(
share.POST("/search", h.ChatSearch, h.ShareAuthMiddleware.Authorize)
share.POST("/completions", h.ChatCompletions)
share.POST("/widget", h.ChatWidget)
share.POST("/widget/search", h.WidgetSearch)
share.POST("/feedback", h.FeedBack)
return h
}
@ -131,7 +132,7 @@ func (h *ShareChatHandler) ChatMessage(c echo.Context) error {
//
// @Summary ChatWidget
// @Description ChatWidget
// @Tags share_chat
// @Tags Widget
// @Accept json
// @Produce json
// @Param app_type query string true "app type"
@ -268,7 +269,9 @@ func (h *ShareChatHandler) ChatCompletions(c echo.Context) error {
var lastUserMessage string
for i := len(req.Messages) - 1; i >= 0; i-- {
if req.Messages[i].Role == "user" {
lastUserMessage = req.Messages[i].Content
if req.Messages[i].Content != nil {
lastUserMessage = req.Messages[i].Content.String()
}
break
}
}
@ -345,11 +348,12 @@ func (h *ShareChatHandler) handleOpenAIStreamResponse(c echo.Context, eventCh <-
Index: 0,
Delta: domain.OpenAIMessage{
Role: "assistant",
Content: event.Content,
Content: domain.NewStringContent(event.Content),
},
},
},
}
if err := h.writeOpenAIStreamEvent(c, streamResp); err != nil {
return err
}
@ -397,7 +401,7 @@ func (h *ShareChatHandler) handleOpenAINonStreamResponse(c echo.Context, eventCh
Index: 0,
Message: domain.OpenAIMessage{
Role: "assistant",
Content: content,
Content: domain.NewStringContent(content),
},
FinishReason: "stop",
},
@ -441,7 +445,7 @@ func stringPtr(s string) *string {
return &s
}
// ChatMessage chat search
// ChatSearch searches chat messages in shared knowledge base
//
// @Summary ChatSearch
// @Description ChatSearch
@ -484,3 +488,43 @@ func (h *ShareChatHandler) ChatSearch(c echo.Context) error {
}
return h.NewResponseWithData(c, resp)
}
// WidgetSearch
//
// @Summary WidgetSearch
// @Description WidgetSearch
// @Tags Widget
// @Accept json
// @Produce json
// @Param request body domain.ChatSearchReq true "Comment"
// @Success 200 {object} domain.Response{data=domain.ChatSearchResp}
// @Router /share/v1/chat/widget/search [post]
func (h *ShareChatHandler) WidgetSearch(c echo.Context) error {
var req domain.ChatSearchReq
if err := c.Bind(&req); err != nil {
return h.NewResponseWithError(c, "parse request failed", err)
}
req.KBID = c.Request().Header.Get("X-KB-ID")
if err := c.Validate(&req); err != nil {
return h.NewResponseWithError(c, "validate request failed", err)
}
ctx := c.Request().Context()
// validate widget info
widgetAppInfo, err := h.appUsecase.GetWidgetAppInfo(c.Request().Context(), req.KBID)
if err != nil {
h.logger.Error("get widget app info failed", log.Error(err))
return h.sendErrMsg(c, "get app info error")
}
if !widgetAppInfo.Settings.WidgetBotSettings.IsOpen {
return h.sendErrMsg(c, "widget is not open")
}
req.RemoteIP = c.RealIP()
resp, err := h.chatUsecase.Search(ctx, &req)
if err != nil {
return h.NewResponseWithError(c, "failed to search docs", err)
}
return h.NewResponseWithData(c, resp)
}

View File

@ -6,7 +6,6 @@ import (
"github.com/labstack/echo/v4"
"github.com/chaitin/panda-wiki/consts"
"github.com/chaitin/panda-wiki/domain"
"github.com/chaitin/panda-wiki/handler"
"github.com/chaitin/panda-wiki/log"
@ -157,7 +156,7 @@ func (h *ShareCommentHandler) GetCommentList(c echo.Context) error {
}
// 查询数据库获取所有评论-->0 所有, 12 为需要审核的评论
commentsList, err := h.usecase.GetCommentListByNodeID(ctx, nodeID, consts.GetLicenseEdition(c))
commentsList, err := h.usecase.GetCommentListByNodeID(ctx, nodeID)
if err != nil {
return h.NewResponseWithError(c, "failed to get comment list", err)
}

View File

@ -91,5 +91,15 @@ func (h *ShareNodeHandler) GetNodeDetail(c echo.Context) error {
if err != nil {
return h.NewResponseWithError(c, "failed to get node detail", err)
}
// If the node is a folder, return the list of child nodes
if node.Type == domain.NodeTypeFolder {
childNodes, err := h.usecase.GetNodeReleaseListByParentID(c.Request().Context(), kbID, id, domain.GetAuthID(c))
if err != nil {
return h.NewResponseWithError(c, "failed to get child nodes", err)
}
node.List = childNodes
}
return h.NewResponseWithData(c, node)
}

View File

@ -5,7 +5,6 @@ import (
"github.com/labstack/echo/v4"
"github.com/chaitin/panda-wiki/domain"
"github.com/chaitin/panda-wiki/handler"
"github.com/chaitin/panda-wiki/log"
"github.com/chaitin/panda-wiki/usecase"
@ -37,13 +36,6 @@ func (h *ShareSitemapHandler) GetSitemap(c echo.Context) error {
if kbID == "" {
return h.NewResponseWithError(c, "kb_id is required", nil)
}
appInfo, err := h.appUsecase.ShareGetWebAppInfo(c.Request().Context(), kbID, domain.GetAuthID(c))
if err != nil {
return h.NewResponseWithError(c, "web app not found", err)
}
if !appInfo.Settings.AutoSitemap {
return h.NewResponseWithError(c, "未开启自动生成站点地图功能", nil)
}
xml, err := h.sitemapUsecase.GetSitemap(c.Request().Context(), kbID)
if err != nil {

View File

@ -5,6 +5,7 @@ import (
v1 "github.com/chaitin/panda-wiki/api/kb/v1"
"github.com/chaitin/panda-wiki/consts"
"github.com/chaitin/panda-wiki/domain"
)
// KBUserList
@ -55,8 +56,8 @@ func (h *KnowledgeBaseHandler) KBUserInvite(c echo.Context) error {
return h.NewResponseWithError(c, "validate request failed", err)
}
if consts.GetLicenseEdition(c) != consts.LicenseEditionEnterprise && req.Perm != consts.UserKBPermissionFullControl {
return h.NewResponseWithError(c, "非企业版本只能使用完全控制权限", nil)
if !domain.GetBaseEditionLimitation(c.Request().Context()).AllowAdminPerm && req.Perm != consts.UserKBPermissionFullControl {
return h.NewResponseWithError(c, "当前版本不支持管理员分权控制", nil)
}
err := h.usecase.KBUserInvite(c.Request().Context(), req)
@ -87,8 +88,8 @@ func (h *KnowledgeBaseHandler) KBUserUpdate(c echo.Context) error {
return h.NewResponseWithError(c, "validate request failed", err)
}
if consts.GetLicenseEdition(c) != consts.LicenseEditionEnterprise && req.Perm != consts.UserKBPermissionFullControl {
return h.NewResponseWithError(c, "非企业版本只能使用完全控制权限", nil)
if !domain.GetBaseEditionLimitation(c.Request().Context()).AllowAdminPerm && req.Perm != consts.UserKBPermissionFullControl {
return h.NewResponseWithError(c, "当前版本不支持管理员分权控制", nil)
}
err := h.usecase.UpdateUserKB(c.Request().Context(), req)

View File

@ -91,11 +91,7 @@ func (h *KnowledgeBaseHandler) CreateKnowledgeBase(c echo.Context) error {
return h.NewResponseWithError(c, "ports is required", nil)
}
req.MaxKB = 1
maxKB := c.Get("max_kb")
if maxKB != nil {
req.MaxKB = maxKB.(int)
}
req.MaxKB = domain.GetBaseEditionLimitation(c.Request().Context()).MaxKb
did, err := h.usecase.CreateKnowledgeBase(c.Request().Context(), &req)
if err != nil {

View File

@ -46,7 +46,7 @@ func NewModelHandler(echo *echo.Echo, baseHandler *handler.BaseHandler, logger *
return handler
}
// get model list
// GetModelList
//
// @Summary get model list
// @Description get model list
@ -66,7 +66,7 @@ func (h *ModelHandler) GetModelList(c echo.Context) error {
return h.NewResponseWithData(c, models)
}
// create model
// CreateModel
//
// @Summary create model
// @Description create model
@ -85,9 +85,6 @@ func (h *ModelHandler) CreateModel(c echo.Context) error {
return h.NewResponseWithError(c, "invalid request", err)
}
if consts.GetLicenseEdition(c) == consts.LicenseEditionContributor && req.Provider != domain.ModelProviderBrandBaiZhiCloud {
return h.NewResponseWithError(c, "联创版只能使用百智云模型哦~", nil)
}
ctx := c.Request().Context()
param := domain.ModelParam{}
@ -112,7 +109,7 @@ func (h *ModelHandler) CreateModel(c echo.Context) error {
return h.NewResponseWithData(c, model)
}
// update model
// UpdateModel
//
// @Description update model
// @Tags model
@ -130,9 +127,6 @@ func (h *ModelHandler) UpdateModel(c echo.Context) error {
return h.NewResponseWithError(c, "invalid request", err)
}
if consts.GetLicenseEdition(c) == consts.LicenseEditionContributor && req.Provider != domain.ModelProviderBrandBaiZhiCloud {
return h.NewResponseWithError(c, "联创版只能使用百智云模型哦~", nil)
}
ctx := c.Request().Context()
if err := h.usecase.Update(ctx, &req); err != nil {
return h.NewResponseWithError(c, "update model failed", err)
@ -140,7 +134,7 @@ func (h *ModelHandler) UpdateModel(c echo.Context) error {
return h.NewResponseWithData(c, nil)
}
// check model
// CheckModel
//
// @Summary check model
// @Description check model

View File

@ -81,15 +81,13 @@ func (h *NodeHandler) CreateNode(c echo.Context) error {
if err := c.Validate(req); err != nil {
return h.NewResponseWithError(c, "validate request body failed", err)
}
req.MaxNode = 300
if maxNode := c.Get("max_node"); maxNode != nil {
req.MaxNode = maxNode.(int)
}
req.MaxNode = domain.GetBaseEditionLimitation(ctx).MaxNode
id, err := h.usecase.Create(c.Request().Context(), req, authInfo.UserId)
if err != nil {
if errors.Is(err, domain.ErrMaxNodeLimitReached) {
return h.NewResponseWithError(c, "已达到最大文档数量限制,请升级到联创版或企业版", nil)
return h.NewResponseWithError(c, "已达到最大文档数量限制,请升级到更高版本", nil)
}
return h.NewResponseWithError(c, "create node failed", err)
}
@ -148,6 +146,7 @@ func (h *NodeHandler) GetNodeDetail(c echo.Context) error {
node, err := h.usecase.GetNodeByKBID(c.Request().Context(), req.ID, req.KbId, req.Format)
if err != nil {
h.logger.Error("get node by kb id failed", log.Error(err))
return h.NewResponseWithError(c, "get node detail failed", err)
}
return h.NewResponseWithData(c, node)

View File

@ -192,12 +192,29 @@ func (h *UserHandler) ResetPassword(c echo.Context) error {
if err != nil {
return h.NewResponseWithError(c, "failed to get user", err)
}
if user.Account == "admin" && authInfo.UserId == req.ID {
return h.NewResponseWithError(c, "请修改安装目录下 .env 文件中的 ADMIN_PASSWORD并重启 panda-wiki-api 容器使更改生效。", nil)
// 非超级管理员没有改密码权限
if user.Role != consts.UserRoleAdmin {
return h.NewResponseWithErrCode(c, domain.ErrCodePermissionDenied)
}
if user.Account != "admin" && authInfo.UserId != req.ID {
return h.NewResponseWithError(c, "只有管理员可以重置其他用户密码", nil)
if user.Account == "admin" {
// admin 改不了自己的密码
if authInfo.UserId == req.ID {
return h.NewResponseWithError(c, "请修改安装目录下 .env 文件中的 ADMIN_PASSWORD并重启 panda-wiki-api 容器使更改生效。", nil)
}
} else {
targetUser, err := h.usecase.GetUser(ctx, req.ID)
if err != nil {
return h.NewResponseWithError(c, "failed to get target user", err)
}
// 超级管理员不能改其他超级管理员密码
if targetUser.Role == consts.UserRoleAdmin && targetUser.ID != authInfo.UserId {
return h.NewResponseWithError(c, "无法修改其他超级管理员密码", nil)
}
}
err = h.usecase.ResetPassword(c.Request().Context(), &req)
if err != nil {
return h.NewResponseWithError(c, "failed to reset password", err)

View File

@ -15,7 +15,7 @@ type AuthMiddleware interface {
Authorize(next echo.HandlerFunc) echo.HandlerFunc
ValidateUserRole(role consts.UserRole) echo.MiddlewareFunc
ValidateKBUserPerm(role consts.UserKBPermission) echo.MiddlewareFunc
ValidateLicenseEdition(edition consts.LicenseEdition) echo.MiddlewareFunc
ValidateLicenseEdition(edition ...consts.LicenseEdition) echo.MiddlewareFunc
MustGetUserID(c echo.Context) (string, bool)
}

View File

@ -6,6 +6,7 @@ import (
"encoding/json"
"io"
"net/http"
"slices"
"strings"
"github.com/golang-jwt/jwt/v5"
@ -194,7 +195,7 @@ func (m *JWTMiddleware) ValidateKBUserPerm(perm consts.UserKBPermission) echo.Mi
}
}
func (m *JWTMiddleware) ValidateLicenseEdition(needEdition consts.LicenseEdition) echo.MiddlewareFunc {
func (m *JWTMiddleware) ValidateLicenseEdition(needEditions ...consts.LicenseEdition) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
@ -206,7 +207,7 @@ func (m *JWTMiddleware) ValidateLicenseEdition(needEdition consts.LicenseEdition
})
}
if edition < needEdition {
if !slices.Contains(needEditions, edition) {
return c.JSON(http.StatusForbidden, domain.PWResponse{
Success: false,
Message: "Unauthorized ValidateLicenseEdition",

@ -1 +1 @@
Subproject commit c4dc498df094cb617d31c95580db8239a445d652
Subproject commit cdfb701c5e3e78ac8be5ad0b69972363a3befd06

View File

@ -2,5 +2,7 @@ package backend
import (
_ "github.com/jinzhu/copier"
_ "github.com/mark3labs/mcp-go/mcp"
_ "github.com/mark3labs/mcp-go/server"
_ "google.golang.org/protobuf/types/known/emptypb"
)

View File

@ -300,8 +300,8 @@ func (r *AuthRepo) GetOrCreateAuth(ctx context.Context, auth *domain.Auth, sourc
return err
}
if int(count) >= licenseEdition.GetMaxAuth(sourceType) {
return fmt.Errorf("exceed max auth limit for kb %s, current count: %d, max limit: %d", auth.KBID, count, licenseEdition.GetMaxAuth(sourceType))
if int(count) >= domain.GetBaseEditionLimitation(ctx).MaxSSOUser {
return fmt.Errorf("exceed max auth limit for kb %s, current count: %d, max limit: %d", auth.KBID, count, domain.GetBaseEditionLimitation(ctx).MaxSSOUser)
}
auth.LastLoginTime = time.Now()

View File

@ -26,12 +26,12 @@ func (r *CommentRepository) CreateComment(ctx context.Context, comment *domain.C
return nil
}
func (r *CommentRepository) GetCommentList(ctx context.Context, nodeID string, edition consts.LicenseEdition) ([]*domain.ShareCommentListItem, int64, error) {
func (r *CommentRepository) GetCommentList(ctx context.Context, nodeID string) ([]*domain.ShareCommentListItem, int64, error) {
// 按照时间排序来查询node_id的comments
var comments []*domain.ShareCommentListItem
query := r.db.WithContext(ctx).Model(&domain.Comment{}).Where("node_id = ?", nodeID)
if edition == consts.LicenseEditionContributor || edition == consts.LicenseEditionEnterprise {
if domain.GetBaseEditionLimitation(ctx).AllowCommentAudit {
query = query.Where("status = ?", domain.CommentStatusAccepted) //accepted
}
@ -50,14 +50,14 @@ func (r *CommentRepository) GetCommentList(ctx context.Context, nodeID string, e
func (r *CommentRepository) GetCommentListByKbID(ctx context.Context, req *domain.CommentListReq, edition consts.LicenseEdition) ([]*domain.CommentListItem, int64, error) {
comments := []*domain.CommentListItem{}
query := r.db.Model(&domain.Comment{}).Where("comments.kb_id = ?", req.KbID)
query := r.db.WithContext(ctx).Model(&domain.Comment{}).Where("comments.kb_id = ?", req.KbID)
var count int64
if req.Status == nil {
if err := query.Count(&count).Error; err != nil {
return nil, 0, err
}
} else {
if edition == consts.LicenseEditionContributor || edition == consts.LicenseEditionEnterprise {
if domain.GetBaseEditionLimitation(ctx).AllowCommentAudit {
query = query.Where("comments.status = ?", *req.Status)
}
// 按照时间排序来查询kb_id的comments ->reject pending accepted
@ -84,7 +84,7 @@ func (r *CommentRepository) GetCommentListByKbID(ctx context.Context, req *domai
func (r *CommentRepository) DeleteCommentList(ctx context.Context, commentID []string) error {
// 批量删除指定id的comment,获取删除的总的数量、
query := r.db.Model(&domain.Comment{}).Where("id IN (?)", commentID)
query := r.db.WithContext(ctx).Model(&domain.Comment{}).Where("id IN (?)", commentID)
if err := query.Delete(&domain.Comment{}).Error; err != nil {
return err

View File

@ -169,7 +169,7 @@ func (r *KnowledgeBaseRepository) SyncKBAccessSettingsToCaddy(ctx context.Contex
{
"match": []map[string]any{
{
"path": []string{"/share/v1/chat/completions", "/share/v1/app/wechat/app", "/share/v1/app/wechat/service", "/sitemap.xml", "/share/v1/app/wechat/official_account", "/share/v1/app/wechat/service/answer"},
"path": []string{"/share/v1/chat/completions", "/share/v1/app/wechat/app", "/share/v1/app/wechat/service", "/sitemap.xml", "/share/v1/app/wechat/official_account", "/share/v1/app/wechat/service/answer", "/mcp"},
},
},
"handle": []map[string]any{

View File

@ -157,6 +157,32 @@ func (r *NodeRepository) GetLatestNodeReleaseByNodeIDs(ctx context.Context, kbID
return nodeReleases, nil
}
func (r *NodeRepository) GetNodeReleasePublisherMap(ctx context.Context, kbID string) (map[string]string, error) {
type Result struct {
NodeID string `gorm:"column:node_id"`
PublisherID string `gorm:"column:publisher_id"`
}
var results []Result
if err := r.db.WithContext(ctx).
Model(&domain.NodeRelease{}).
Select("node_id, publisher_id").
Where("kb_id = ?", kbID).
Where("node_releases.doc_id != '' ").
Find(&results).Error; err != nil {
return nil, err
}
publisherMap := make(map[string]string)
for _, result := range results {
if result.PublisherID != "" {
publisherMap[result.NodeID] = result.PublisherID
}
}
return publisherMap, nil
}
func (r *NodeRepository) UpdateNodeContent(ctx context.Context, req *domain.UpdateNodeReq, userId string) error {
// Use transaction to ensure data consistency
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
@ -683,7 +709,7 @@ func (r *NodeRepository) GetNodeReleaseListByKBID(ctx context.Context, kbID stri
Where("kb_release_node_releases.kb_id = ?", kbID).
Where("kb_release_node_releases.release_id = ?", kbRelease.ID).
Where("nodes.permissions->>'visible' != ?", consts.NodeAccessPermClosed).
Select("node_releases.node_id as id, node_releases.name, node_releases.type, node_releases.parent_id, node_releases.position, node_releases.meta->>'emoji' as emoji, node_releases.updated_at, nodes.permissions").
Select("node_releases.node_id as id, node_releases.name, node_releases.type, node_releases.parent_id, nodes.position, node_releases.meta->>'emoji' as emoji, node_releases.updated_at, nodes.permissions, nodes.meta").
Find(&nodes).Error; err != nil {
return nil, err
}

View File

@ -0,0 +1,39 @@
package pg
import (
"context"
"errors"
"gorm.io/gorm"
"github.com/chaitin/panda-wiki/domain"
"github.com/chaitin/panda-wiki/utils"
)
func (r *NodeRepository) GetNodeStatsByNodeId(ctx context.Context, nodeId string) (*domain.NodeStats, error) {
var nodeStats *domain.NodeStats
if err := r.db.WithContext(ctx).
Model(&domain.NodeStats{}).
Where("node_id = ?", nodeId).
First(&nodeStats).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
nodeStats = &domain.NodeStats{
ID: 0,
NodeID: nodeId,
PV: 0,
}
} else {
return nil, err
}
}
var todayStats int64
if err := r.db.WithContext(ctx).Model(&domain.StatPage{}).
Where("created_at >= ?", utils.GetTimeHourOffset(-24)).
Where("node_id = ?", nodeId).Count(&todayStats).Error; err != nil {
return nil, err
}
nodeStats.PV += todayStats
return nodeStats, nil
}

View File

@ -3,6 +3,9 @@ package pg
import (
"context"
"gorm.io/gorm"
"gorm.io/gorm/clause"
v1 "github.com/chaitin/panda-wiki/api/stat/v1"
"github.com/chaitin/panda-wiki/domain"
"github.com/chaitin/panda-wiki/store/cache"
@ -156,3 +159,46 @@ func (r *StatRepository) RemoveOldData(ctx context.Context) error {
}
return nil
}
// GetYesterdayPVByNode 获取昨天的PV数据按node_id分组
func (r *StatRepository) GetYesterdayPVByNode(ctx context.Context) (map[string]int64, error) {
type PVResult struct {
NodeID string
Count int64
}
var results []PVResult
if err := r.db.WithContext(ctx).Model(&domain.StatPage{}).
Where("created_at < ?", utils.GetTimeHourOffset(0)).
Where("created_at >= ?", utils.GetTimeHourOffset(-24)).
Where("node_id != ?", "").
Group("node_id").
Select("node_id, COUNT(*) as count").
Find(&results).Error; err != nil {
return nil, err
}
pvMap := make(map[string]int64)
for _, result := range results {
pvMap[result.NodeID] = result.Count
}
return pvMap, nil
}
// UpsertNodeStats 插入或更新node_stats表
func (r *StatRepository) UpsertNodeStats(ctx context.Context, nodeID string, pvCount int64) error {
nodeStats := &domain.NodeStats{
NodeID: nodeID,
PV: pvCount,
}
// 使用GORM的Clauses进行upsert操作
return r.db.WithContext(ctx).
Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "node_id"}},
DoUpdates: clause.Assignments(map[string]interface{}{
"pv": gorm.Expr("node_stats.pv + ?", pvCount),
}),
}).
Create(nodeStats).Error
}

View File

@ -60,18 +60,14 @@ func (r *UserRepository) CreateUser(ctx context.Context, user *domain.User, edit
}
user.Password = string(hashedPassword)
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if edition == consts.LicenseEditionContributor || edition == consts.LicenseEditionFree {
var count int64
if err := tx.Model(&domain.User{}).Count(&count).Error; err != nil {
return err
}
if edition == consts.LicenseEditionFree && count >= 1 {
return errors.New("free edition only allows 1 user")
}
if edition == consts.LicenseEditionContributor && count >= 5 {
return errors.New("contributor edition only allows 5 user")
}
var count int64
if err := tx.Model(&domain.User{}).Count(&count).Error; err != nil {
return err
}
if count >= domain.GetBaseEditionLimitation(ctx).MaxAdmin {
return fmt.Errorf("exceed max admin limit, current count: %d, max limit: %d", count, domain.GetBaseEditionLimitation(ctx).MaxAdmin)
}
if err := tx.Create(user).Error; err != nil {
return err
}

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS mcp_calls;

View File

@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS mcp_calls (
id SERIAL PRIMARY KEY,
mcp_session_id TEXT NOT NULL,
kb_id TEXT NOT NULL,
remote_ip TEXT,
initialize_req JSONB,
initialize_resp JSONB,
tool_call_req JSONB,
tool_call_resp TEXT,
created_at timestamptz NOT NULL DEFAULT NOW()
);

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS node_stats;

View File

@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS node_stats (
id BIGSERIAL PRIMARY KEY,
node_id TEXT NOT NULL UNIQUE,
pv BIGINT NOT NULL DEFAULT 0,
created_at timestamptz NOT NULL DEFAULT NOW()
);

View File

@ -28,6 +28,8 @@ func NewHTML2MDConverter() *converter.Converter {
conv.Register.RendererFor("span", converter.TagTypeInline, renderAttachment, converter.PriorityEarly)
// task list
conv.Register.RendererFor("ul", converter.TagTypeBlock, renderTaskList, converter.PriorityEarly)
// flowchart/diagram to mermaid code block
conv.Register.RendererFor("div", converter.TagTypeBlock, renderFlowchart, converter.PriorityEarly)
return conv
}
@ -126,3 +128,40 @@ func getTextFromTaskItem(node *html.Node) string {
extractText(node)
return strings.TrimSpace(textContent.String())
}
// renderFlowchart 将流程图 div 转换为 Mermaid 代码块
func renderFlowchart(ctx converter.Context, w converter.Writer, node *html.Node) converter.RenderStatus {
if node.Type != html.ElementNode || node.Data != "div" {
return converter.RenderTryNext
}
// 仅处理 data-type="flow" 的 div
dataType, ok := dom.GetAttribute(node, "data-type")
if !ok || dataType != "flow" {
return converter.RenderTryNext
}
// 提取 data-code 属性
code, hasCode := dom.GetAttribute(node, "data-code")
if !hasCode || strings.TrimSpace(code) == "" {
return converter.RenderTryNext
}
// 解码 HTML 实体
code = html.UnescapeString(code)
// 处理转义的换行符
code = strings.ReplaceAll(code, "\\n", "\n")
// 写入 Mermaid 代码块
if _, err := w.WriteString("\n```mermaid\n"); err != nil {
return converter.RenderTryNext
}
if _, err := w.WriteString(code); err != nil {
return converter.RenderTryNext
}
if _, err := w.WriteString("\n```\n\n"); err != nil {
return converter.RenderTryNext
}
return converter.RenderSuccess
}

View File

@ -88,32 +88,55 @@ func NewAppUsecase(
}
func (u *AppUsecase) ValidateUpdateApp(ctx context.Context, id string, req *domain.UpdateAppReq, edition consts.LicenseEdition) error {
switch edition {
case consts.LicenseEditionFree:
app, err := u.repo.GetAppDetail(ctx, id)
if err != nil {
return err
}
if app.Settings.WatermarkContent != req.Settings.WatermarkContent ||
app.Settings.WatermarkSetting != req.Settings.WatermarkSetting ||
app.Settings.ContributeSettings != req.Settings.ContributeSettings ||
app.Settings.CopySetting != req.Settings.CopySetting {
app, err := u.repo.GetAppDetail(ctx, id)
if err != nil {
return err
}
limitation := domain.GetBaseEditionLimitation(ctx)
if !limitation.AllowCopyProtection && app.Settings.CopySetting != req.Settings.CopySetting {
return domain.ErrPermissionDenied
}
if !limitation.AllowWatermark {
if app.Settings.WatermarkSetting != req.Settings.WatermarkSetting || app.Settings.WatermarkContent != req.Settings.WatermarkContent {
return domain.ErrPermissionDenied
}
case consts.LicenseEditionContributor:
app, err := u.repo.GetAppDetail(ctx, id)
if err != nil {
return err
}
if app.Settings.WatermarkContent != req.Settings.WatermarkContent ||
app.Settings.WatermarkSetting != req.Settings.WatermarkSetting ||
app.Settings.CopySetting != req.Settings.CopySetting {
}
if !limitation.AllowAdvancedBot {
if !slices.Equal(app.Settings.WechatServiceContainKeywords, req.Settings.WechatServiceContainKeywords) ||
!slices.Equal(app.Settings.WechatServiceEqualKeywords, req.Settings.WechatServiceEqualKeywords) {
return domain.ErrPermissionDenied
}
}
if !limitation.AllowCommentAudit && app.Settings.WebAppCommentSettings.ModerationEnable != req.Settings.WebAppCommentSettings.ModerationEnable {
return domain.ErrPermissionDenied
}
if !limitation.AllowOpenAIBotSettings {
if app.Settings.OpenAIAPIBotSettings.IsEnabled != req.Settings.OpenAIAPIBotSettings.IsEnabled || app.Settings.OpenAIAPIBotSettings.SecretKey != req.Settings.OpenAIAPIBotSettings.SecretKey {
return domain.ErrPermissionDenied
}
}
if !limitation.AllowCustomCopyright {
if app.Settings.WidgetBotSettings.CopyrightHideEnabled != req.Settings.WidgetBotSettings.CopyrightHideEnabled || app.Settings.WidgetBotSettings.CopyrightInfo != req.Settings.WidgetBotSettings.CopyrightInfo {
return domain.ErrPermissionDenied
}
if app.Settings.ConversationSetting.CopyrightHideEnabled != req.Settings.ConversationSetting.CopyrightHideEnabled {
return domain.ErrPermissionDenied
}
if req.Settings.ConversationSetting.CopyrightInfo != domain.SettingCopyrightInfo && app.Settings.ConversationSetting.CopyrightInfo != req.Settings.ConversationSetting.CopyrightInfo {
return domain.ErrPermissionDenied
}
}
if !limitation.AllowMCPServer {
if app.Settings.MCPServerSettings.IsEnabled != req.Settings.MCPServerSettings.IsEnabled {
return domain.ErrPermissionDenied
}
case consts.LicenseEditionEnterprise:
return nil
default:
return fmt.Errorf("unsupported license type: %d", edition)
}
return nil
@ -439,7 +462,6 @@ func (u *AppUsecase) GetAppDetailByKBIDAndAppType(ctx context.Context, kbID stri
RecommendNodeIDs: app.Settings.RecommendNodeIDs,
Desc: app.Settings.Desc,
Keyword: app.Settings.Keyword,
AutoSitemap: app.Settings.AutoSitemap,
HeadCode: app.Settings.HeadCode,
BodyCode: app.Settings.BodyCode,
// DingTalkBot
@ -502,14 +524,24 @@ func (u *AppUsecase) GetAppDetailByKBIDAndAppType(ctx context.Context, kbID stri
WebAppLandingConfigs: webAppLandingConfigs,
WebAppLandingTheme: app.Settings.WebAppLandingTheme,
WatermarkContent: app.Settings.WatermarkContent,
WatermarkSetting: app.Settings.WatermarkSetting,
CopySetting: app.Settings.CopySetting,
ContributeSettings: app.Settings.ContributeSettings,
HomePageSetting: app.Settings.HomePageSetting,
WatermarkContent: app.Settings.WatermarkContent,
WatermarkSetting: app.Settings.WatermarkSetting,
CopySetting: app.Settings.CopySetting,
ContributeSettings: app.Settings.ContributeSettings,
HomePageSetting: app.Settings.HomePageSetting,
ConversationSetting: app.Settings.ConversationSetting,
WecomAIBotSettings: app.Settings.WecomAIBotSettings,
MCPServerSettings: app.Settings.MCPServerSettings,
StatsSetting: app.Settings.StatsSetting,
}
if !domain.GetBaseEditionLimitation(ctx).AllowCustomCopyright {
appDetailResp.Settings.ConversationSetting.CopyrightHideEnabled = false
appDetailResp.Settings.ConversationSetting.CopyrightInfo = domain.SettingCopyrightInfo
}
// init ai feedback string
if app.Settings.AIFeedbackSettings.AIFeedbackType == nil {
appDetailResp.Settings.AIFeedbackSettings.AIFeedbackType = []string{"内容不准确", "没有帮助", "其他"}
@ -532,6 +564,19 @@ func (u *AppUsecase) GetAppDetailByKBIDAndAppType(ctx context.Context, kbID stri
return appDetailResp, nil
}
func (u *AppUsecase) GetMCPServerAppInfo(ctx context.Context, kbID string) (*domain.AppInfoResp, error) {
apiApp, err := u.repo.GetOrCreateAppByKBIDAndType(ctx, kbID, domain.AppTypeMcpServer)
if err != nil {
return nil, err
}
appInfo := &domain.AppInfoResp{
Settings: domain.AppSettingsResp{
MCPServerSettings: apiApp.Settings.MCPServerSettings,
},
}
return appInfo, nil
}
func (u *AppUsecase) ShareGetWebAppInfo(ctx context.Context, kbID string, authId uint) (*domain.AppInfoResp, error) {
app, err := u.repo.GetOrCreateAppByKBIDAndType(ctx, kbID, domain.AppTypeWeb)
if err != nil {
@ -578,7 +623,6 @@ func (u *AppUsecase) ShareGetWebAppInfo(ctx context.Context, kbID string, authId
RecommendNodeIDs: app.Settings.RecommendNodeIDs,
Desc: app.Settings.Desc,
Keyword: app.Settings.Keyword,
AutoSitemap: app.Settings.AutoSitemap,
HeadCode: app.Settings.HeadCode,
BodyCode: app.Settings.BodyCode,
// theme
@ -602,11 +646,13 @@ func (u *AppUsecase) ShareGetWebAppInfo(ctx context.Context, kbID string, authId
WebAppLandingConfigs: webAppLandingConfigs,
WebAppLandingTheme: app.Settings.WebAppLandingTheme,
WatermarkContent: app.Settings.WatermarkContent,
WatermarkSetting: app.Settings.WatermarkSetting,
CopySetting: app.Settings.CopySetting,
ContributeSettings: app.Settings.ContributeSettings,
HomePageSetting: app.Settings.HomePageSetting,
WatermarkContent: app.Settings.WatermarkContent,
WatermarkSetting: app.Settings.WatermarkSetting,
CopySetting: app.Settings.CopySetting,
ContributeSettings: app.Settings.ContributeSettings,
HomePageSetting: app.Settings.HomePageSetting,
ConversationSetting: app.Settings.ConversationSetting,
StatsSetting: app.Settings.StatsSetting,
},
}
// init ai feedback string
@ -618,10 +664,12 @@ func (u *AppUsecase) ShareGetWebAppInfo(ctx context.Context, kbID string, authId
}
showBrand := true
defaultDisclaimer := "本回答由 PandaWiki 基于 AI 生成,仅供参考。"
licenseEdition, _ := ctx.Value(consts.ContextKeyEdition).(consts.LicenseEdition)
if licenseEdition < consts.LicenseEditionEnterprise {
if !domain.GetBaseEditionLimitation(ctx).AllowCustomCopyright {
appInfo.Settings.WebAppCustomSettings.ShowBrandInfo = &showBrand
appInfo.Settings.DisclaimerSettings.Content = &defaultDisclaimer
appInfo.Settings.ConversationSetting.CopyrightHideEnabled = false
appInfo.Settings.ConversationSetting.CopyrightInfo = domain.SettingCopyrightInfo
} else {
if appInfo.Settings.DisclaimerSettings.Content == nil {
appInfo.Settings.DisclaimerSettings.Content = &defaultDisclaimer
@ -660,6 +708,12 @@ func (u *AppUsecase) GetWidgetAppInfo(ctx context.Context, kbID string) (*domain
}
appInfo.RecommendNodes = nodes
}
if !domain.GetBaseEditionLimitation(ctx).AllowCustomCopyright {
appInfo.Settings.WidgetBotSettings.CopyrightHideEnabled = false
appInfo.Settings.WidgetBotSettings.CopyrightInfo = domain.SettingCopyrightInfo
}
return appInfo, nil
}

View File

@ -301,6 +301,64 @@ func (u *ChatUsecase) Chat(ctx context.Context, req *domain.ChatRequest) (<-chan
return eventCh, nil
}
func (u *ChatUsecase) ChatRagOnly(ctx context.Context, req *domain.ChatRagOnlyRequest) (<-chan domain.SSEEvent, error) {
eventCh := make(chan domain.SSEEvent, 100)
go func() {
defer close(eventCh)
// extra1. if user set question block words then check it
blockWords, err := u.blockWordRepo.GetBlockWords(ctx, req.KBID)
if err != nil {
u.logger.Error("failed to get question block words", log.Error(err))
eventCh <- domain.SSEEvent{Type: "error", Content: "failed to get question block words"}
return
}
if len(blockWords) > 0 { // check --> filter
questionFilter := utils.GetDFA(req.KBID)
if err := questionFilter.DFA.Check(req.Message); err != nil { // exist then return err
answer := "**您的问题包含敏感词, AI 无法回答您的问题。**"
eventCh <- domain.SSEEvent{Type: "error", Content: answer}
return
}
}
if req.UserInfo.AuthUserID == 0 {
auth, _ := u.AuthRepo.GetAuthBySourceType(ctx, req.AppType.ToSourceType())
if auth != nil {
req.UserInfo.AuthUserID = auth.ID
}
}
groupIds, err := u.AuthRepo.GetAuthGroupIdsWithParentsByAuthId(ctx, req.UserInfo.AuthUserID)
if err != nil {
u.logger.Error("failed to get auth groupIds", log.Error(err))
eventCh <- domain.SSEEvent{Type: "error", Content: "failed to get auth groupIds"}
return
}
// retrieve documents
kb, err := u.kbRepo.GetKnowledgeBaseByID(ctx, req.KBID)
if err != nil {
u.logger.Error("failed to get kb", log.Error(err))
eventCh <- domain.SSEEvent{Type: "error", Content: "failed to get kb"}
return
}
rankedNodes, err := u.llmUsecase.GetRankNodes(ctx, []string{kb.DatasetID}, req.Message, groupIds, 0, nil)
if err != nil {
u.logger.Error("failed to get rank nodes", log.Error(err))
eventCh <- domain.SSEEvent{Type: "error", Content: "failed to get rank nodes"}
return
}
documents := domain.FormatNodeChunks(rankedNodes, kb.AccessSettings.BaseURL)
u.logger.Debug("documents", log.String("documents", documents))
// send only the documents part
eventCh <- domain.SSEEvent{Type: "data", Content: documents}
eventCh <- domain.SSEEvent{Type: "done"}
}()
return eventCh, nil
}
func (u *ChatUsecase) CreateAcOnChunk(ctx context.Context, kbID string, answer *string, eventCh chan<- domain.SSEEvent, blockWords []string) (func(ctx context.Context, dataType, chunk string) error,
func(ctx context.Context, dataType string)) {
var buffer strings.Builder

View File

@ -72,8 +72,8 @@ func (u *CommentUsecase) CreateComment(ctx context.Context, commentReq *domain.C
return CommentStr, nil
}
func (u *CommentUsecase) GetCommentListByNodeID(ctx context.Context, nodeID string, edition consts.LicenseEdition) (*domain.PaginatedResult[[]*domain.ShareCommentListItem], error) {
comments, total, err := u.CommentRepo.GetCommentList(ctx, nodeID, edition)
func (u *CommentUsecase) GetCommentListByNodeID(ctx context.Context, nodeID string) (*domain.PaginatedResult[[]*domain.ShareCommentListItem], error) {
comments, total, err := u.CommentRepo.GetCommentList(ctx, nodeID)
if err != nil {
return nil, err
}

View File

@ -86,6 +86,21 @@ func (u *NodeUsecase) GetList(ctx context.Context, req *domain.GetNodeListReq) (
if err != nil {
return nil, err
}
if len(nodes) == 0 {
return nodes, nil
}
publisherMap, err := u.nodeRepo.GetNodeReleasePublisherMap(ctx, req.KBID)
if err != nil {
return nil, err
}
for _, node := range nodes {
if publisherID, exists := publisherMap[node.ID]; exists {
node.PublisherId = publisherID
}
}
return nodes, nil
}
@ -104,6 +119,12 @@ func (u *NodeUsecase) GetNodeByKBID(ctx context.Context, id, kbId, format string
node.PublisherAccount = nodeRelease.PublisherAccount
}
nodeStat, err := u.nodeRepo.GetNodeStatsByNodeId(ctx, node.ID)
if err != nil {
return nil, err
}
node.PV = nodeStat.PV
if node.Meta.ContentType == domain.ContentTypeMD {
return node, nil
}
@ -206,6 +227,21 @@ func (u *NodeUsecase) GetNodeReleaseDetailByKBIDAndID(ctx context.Context, kbID,
node.PublisherAccount = account
}
if domain.GetBaseEditionLimitation(ctx).AllowNodeStats {
webApp, err := u.appRepo.GetOrCreateAppByKBIDAndType(ctx, kbID, domain.AppTypeWeb)
if err != nil {
return nil, err
}
if webApp.Settings.StatsSetting.PVEnable {
nodeStat, err := u.nodeRepo.GetNodeStatsByNodeId(ctx, nodeId)
if err != nil {
return nil, err
}
node.PV = nodeStat.PV
}
}
if node.Meta.ContentType == domain.ContentTypeMD {
return node, nil
}
@ -350,6 +386,75 @@ func (u *NodeUsecase) GetNodeReleaseListByKBID(ctx context.Context, kbID string,
return items, nil
}
func (u *NodeUsecase) GetNodeReleaseListByParentID(ctx context.Context, kbID, parentID string, authId uint) ([]*domain.ShareNodeDetailItem, error) {
// 一次性查询所有节点
allNodes, err := u.nodeRepo.GetNodeReleaseListByKBID(ctx, kbID)
if err != nil {
return nil, err
}
nodeGroupIds, err := u.GetNodeIdsByAuthId(ctx, authId, consts.NodePermNameVisible)
if err != nil {
return nil, err
}
// 先过滤权限
visibleNodes := make([]*domain.ShareNodeListItemResp, 0)
for i, node := range allNodes {
switch node.Permissions.Visible {
case consts.NodeAccessPermOpen:
visibleNodes = append(visibleNodes, allNodes[i])
case consts.NodeAccessPermPartial:
if slices.Contains(nodeGroupIds, node.ID) {
visibleNodes = append(visibleNodes, allNodes[i])
}
}
}
// 构建父子关系映射
childrenMap := make(map[string][]*domain.ShareNodeListItemResp)
for _, node := range visibleNodes {
childrenMap[node.ParentID] = append(childrenMap[node.ParentID], node)
}
// 构建树结构
result := u.buildNodeTree(parentID, childrenMap)
return result, nil
}
// buildNodeTree 递归构建节点树结构
func (u *NodeUsecase) buildNodeTree(parentID string, childrenMap map[string][]*domain.ShareNodeListItemResp) []*domain.ShareNodeDetailItem {
children := childrenMap[parentID]
result := make([]*domain.ShareNodeDetailItem, 0, len(children))
for _, child := range children {
node := &domain.ShareNodeDetailItem{
ID: child.ID,
Name: child.Name,
Type: child.Type,
ParentID: child.ParentID,
Position: child.Position,
Meta: child.Meta,
Emoji: child.Emoji,
UpdatedAt: child.UpdatedAt,
Children: make([]*domain.ShareNodeDetailItem, 0),
}
// 如果是文件夹,递归构建其子节点
if child.Type == domain.NodeTypeFolder {
childNodes := u.buildNodeTree(child.ID, childrenMap)
if len(childNodes) > 0 {
node.Children = append(node.Children, childNodes...)
}
}
result = append(result, node)
}
return result
}
func (u *NodeUsecase) GetNodeIdsByAuthId(ctx context.Context, authId uint, PermName consts.NodePermName) ([]string, error) {
authGroups, err := u.authRepo.GetAuthGroupWithParentsByAuthId(ctx, authId)
if err != nil {
@ -407,7 +512,7 @@ func (u *NodeUsecase) GetNodePermissionsByID(ctx context.Context, id, kbID strin
}
func (u *NodeUsecase) ValidateNodePermissionsEdit(req v1.NodePermissionEditReq, edition consts.LicenseEdition) error {
if edition != consts.LicenseEditionEnterprise {
if !slices.Contains([]consts.LicenseEdition{consts.LicenseEditionBusiness, consts.LicenseEditionEnterprise}, edition) {
if req.Permissions.Answerable == consts.NodeAccessPermPartial || req.Permissions.Visitable == consts.NodeAccessPermPartial || req.Permissions.Visible == consts.NodeAccessPermPartial {
return domain.ErrPermissionDenied
}

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"slices"
"sort"
"strconv"
@ -67,12 +68,12 @@ func (u *StatUseCase) ValidateStatDay(statDay consts.StatDay, edition consts.Lic
case consts.StatDay1:
return nil
case consts.StatDay7:
if edition < consts.LicenseEditionContributor {
if edition == consts.LicenseEditionFree {
return domain.ErrPermissionDenied
}
return nil
case consts.StatDay30, consts.StatDay90:
if edition < consts.LicenseEditionEnterprise {
if !slices.Contains([]consts.LicenseEdition{consts.LicenseEditionBusiness, consts.LicenseEditionEnterprise}, edition) {
return domain.ErrPermissionDenied
}
return nil
@ -471,3 +472,28 @@ func (u *StatUseCase) AggregateHourlyStats(ctx context.Context) error {
func (u *StatUseCase) CleanupOldHourlyStats(ctx context.Context) error {
return u.repo.CleanupOldHourlyStats(ctx)
}
// MigrateYesterdayPVToNodeStats 将昨天的PV数据从stat_page迁移到node_stats
func (u *StatUseCase) MigrateYesterdayPVToNodeStats(ctx context.Context) error {
// 获取昨天的PV数据按node_id分组
pvMap, err := u.repo.GetYesterdayPVByNode(ctx)
if err != nil {
u.logger.Error("failed to get yesterday PV data", log.Error(err))
return err
}
// 遍历并插入/更新到node_stats表
for nodeID, pvCount := range pvMap {
if err := u.repo.UpsertNodeStats(ctx, nodeID, pvCount); err != nil {
u.logger.Error("failed to upsert node stats",
log.Error(err),
log.String("node_id", nodeID),
log.Int64("pv_count", pvCount))
return err
}
}
u.logger.Info("successfully migrated yesterday PV data to node_stats",
log.Int("node_count", len(pvMap)))
return nil
}

View File

@ -2,13 +2,18 @@ package utils
import (
"net"
"net/http"
"strings"
"github.com/labstack/echo/v4"
)
func GetClientIPFromRemoteAddr(c echo.Context) string {
addr := c.Request().RemoteAddr
return ExtractHostFromRemoteAddr(c.Request())
}
func ExtractHostFromRemoteAddr(r *http.Request) string {
addr := r.RemoteAddr
if addr == "" {
return ""
}

View File

@ -8,7 +8,6 @@
"build:dev": "vite build --m development",
"build": "tsc -b && vite build",
"build:analyze": "tsc -b && vite build -- --analyze",
"icon": "node ./scripts/downLoadIcon.cjs",
"api": "cx-swagger-api"
},
"dependencies": {

View File

@ -1,23 +0,0 @@
const fs = require("fs");
const path = require("path");
async function downloadFile(url) {
const iconPath = path.resolve(__dirname, "../src/assets/fonts/iconfont.js");
const iconDir = path.dirname(iconPath);
// 检查目录是否存在,不存在则创建
if (!fs.existsSync(iconDir)) {
fs.mkdirSync(iconDir, { recursive: true });
console.log(`目录 ${iconDir} 已创建`);
}
const response = await fetch(`https:${url}`, {
method: "GET",
// responseType: "stream", // fetch 不支持此参数
}).then((res) => res.text());
fs.writeFileSync(iconPath, response);
console.log("Download Icon Success");
}
let argument = process.argv.splice(2);
downloadFile(argument[0]);

View File

@ -323,7 +323,6 @@ export type WelcomeSetting = {
export type SEOSetting = {
keyword: string;
desc: string;
auto_sitemap: boolean;
};
export type CustomCodeSetting = {
@ -588,6 +587,7 @@ export type ChatConversationItem = {
export type ChatConversationPair = {
user: string;
assistant: string;
thinking_content: string;
created_at: string;
info: {
feedback_content: string;

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1,6 +1,6 @@
import Logo from '@/assets/images/logo.png';
import { Avatar as MuiAvatar, SxProps } from '@mui/material';
import { Icon } from '@ctzhian/ui';
import { IconDandulogo } from '@panda-wiki/icons';
import { ReactNode } from 'react';
interface AvatarProps {
@ -15,9 +15,8 @@ const Avatar = (props: AvatarProps) => {
const src = props.src;
const LogoIcon = (
<Icon
<IconDandulogo
sx={{ width: '100%', height: '100%', color: 'text.primary' }}
type='icon-dandulogo'
/>
);

View File

@ -65,6 +65,7 @@ const Step1Model: React.FC<Step1ModelProps> = ({ ref }) => {
}, [modelList]);
const onSubmit = async () => {
await modelConfigRef.current?.onSubmit?.();
// 检查模型模式设置
try {
const modeSetting = await getApiV1ModelModeSetting();
@ -112,6 +113,7 @@ const Step1Model: React.FC<Step1ModelProps> = ({ ref }) => {
getModelList={getModelList}
hideDocumentationHint={true}
showTip={true}
showSaveBtn={false}
/>
</Box>
);

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
import { FooterSetting } from '@/api/type';
import { Icon } from '@ctzhian/ui';
import { IconShanchu2, IconDrag, IconTianjia } from '@panda-wiki/icons';
import {
closestCenter,
DndContext,
@ -100,7 +100,7 @@ const LinkItem = forwardRef<HTMLDivElement, LinkItemProps>(
}}
{...dragHandleProps}
>
<Icon type='icon-drag' />
<IconDrag sx={{ fontSize: '18px' }} />
</IconButton>
<Box
sx={{
@ -123,7 +123,7 @@ const LinkItem = forwardRef<HTMLDivElement, LinkItemProps>(
ml: 'auto',
}}
>
<Icon type='icon-shanchu2' sx={{ fontSize: '12px' }} />
<IconShanchu2 sx={{ fontSize: '12px' }} />
</IconButton>
</Stack>
<Controller
@ -373,7 +373,7 @@ const Item = forwardRef<HTMLDivElement, ItemProps>(
}}
{...dragHandleProps}
>
<Icon type='icon-drag' />
<IconDrag sx={{ fontSize: '18px' }} />
</IconButton>
<Box
sx={{
@ -396,7 +396,7 @@ const Item = forwardRef<HTMLDivElement, ItemProps>(
ml: 'auto',
}}
>
<Icon type='icon-shanchu2' sx={{ fontSize: '12px' }} />
<IconShanchu2 sx={{ fontSize: '12px' }} />
</IconButton>
</Stack>
<Controller
@ -513,8 +513,7 @@ const Item = forwardRef<HTMLDivElement, ItemProps>(
}}
onClick={handleAddLink}
>
<Icon
type='icon-tianjia'
<IconTianjia
sx={{ fontSize: '10px !important', color: '#5F58FE' }}
/>
<Box

View File

@ -1,7 +1,7 @@
import { CardWebHeaderBtn } from '@/api';
import UploadFile from '@/components/UploadFile';
import { useAppDispatch, useAppSelector } from '@/store';
import { setAppPreviewData } from '@/store/slices/config';
import {
Box,
Checkbox,
@ -13,16 +13,15 @@ import {
Stack,
TextField,
} from '@mui/material';
import { Icon } from '@ctzhian/ui';
import { IconShanchu2, IconDrag } from '@panda-wiki/icons';
import {
CSSProperties,
Dispatch,
forwardRef,
HTMLAttributes,
SetStateAction,
useEffect,
} from 'react';
import { Control, Controller, useForm } from 'react-hook-form';
import { Control, Controller } from 'react-hook-form';
export type ItemProps = Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> & {
item: CardWebHeaderBtn;
@ -300,7 +299,7 @@ const Item = forwardRef<HTMLDivElement, ItemProps>(
height: '28px',
}}
>
<Icon type='icon-shanchu2' sx={{ fontSize: '12px' }} />
<IconShanchu2 sx={{ fontSize: '12px' }} />
</IconButton>
<IconButton
size='small'
@ -311,7 +310,7 @@ const Item = forwardRef<HTMLDivElement, ItemProps>(
}}
{...dragHandleProps}
>
<Icon type='icon-drag' />
<IconDrag sx={{ fontSize: '18px' }} />
</IconButton>
</Stack>
</Stack>

View File

@ -1,6 +1,6 @@
import UploadFile from '@/components/UploadFile';
import { DomainSocialMediaAccount } from '@/request/types';
import { Icon } from '@ctzhian/ui';
import { IconShanchu2, IconDrag } from '@panda-wiki/icons';
import {
Box,
IconButton,
@ -92,7 +92,7 @@ const Item = forwardRef<HTMLDivElement, SocialInfoProps>(
}}
{...dragHandleProps}
>
<Icon type='icon-drag' />
<IconDrag sx={{ fontSize: '18px' }} />
</IconButton>
<Box
sx={{
@ -120,7 +120,7 @@ const Item = forwardRef<HTMLDivElement, SocialInfoProps>(
ml: 'auto',
}}
>
<Icon type='icon-shanchu2' sx={{ fontSize: '12px' }} />
<IconShanchu2 sx={{ fontSize: '12px' }} />
</IconButton>
</Stack>
<Stack direction={'row'} gap={1}>
@ -142,16 +142,10 @@ const Item = forwardRef<HTMLDivElement, SocialInfoProps>(
}}
renderValue={selected => {
const option = options.find(i => i.key === selected);
const AppIcon = option?.config_type || option?.type;
return (
<Stack justifyContent={'center'} sx={{ mt: '2px' }}>
<Icon
type={
option
? option?.config_type || option?.type || ''
: ''
}
sx={{ fontSize: '14px' }}
/>
{AppIcon && <AppIcon sx={{ fontSize: '14px' }} />}
</Stack>
);
}}
@ -185,30 +179,31 @@ const Item = forwardRef<HTMLDivElement, SocialInfoProps>(
borderRadius: 1,
}}
>
{options.map(item => (
<ToggleButton
key={item.key}
value={item.key}
sx={{
p: 1,
height: 'auto',
border: '1px solid #ddd !important',
borderRadius: '0px',
}}
>
<Stack
direction='row'
gap={1}
alignItems='center'
{options.map(item => {
const AppIcon = item?.config_type || item?.type;
return (
<ToggleButton
key={item.key}
value={item.key}
sx={{
p: 1,
height: 'auto',
border: '1px solid #ddd !important',
borderRadius: '0px',
}}
>
<Icon
type={item?.config_type || item?.type}
sx={{ fontSize: '16px' }}
/>
{/* <Box>{item.value || item.key}</Box> */}
</Stack>
</ToggleButton>
))}
<Stack
direction='row'
gap={1}
alignItems='center'
>
{AppIcon && (
<AppIcon sx={{ fontSize: '16px' }} />
)}
</Stack>
</ToggleButton>
);
})}
</ToggleButtonGroup>
</MenuItem>
</Select>

View File

@ -1,99 +0,0 @@
import { Box, Button, IconButton, Stack } from '@mui/material';
import { Icon } from '@ctzhian/ui';
import { AppSetting } from '@/api';
import { useState } from 'react';
import { getButtonThemeStyle } from './buttonThemeUtils';
const NavBtns = ({ detail }: { detail?: Partial<AppSetting> }) => {
const [open, setOpen] = useState(false);
return (
<>
<IconButton
size='small'
onClick={() => setOpen(!open)}
sx={{
color: 'text.primary',
width: 40,
height: 40,
}}
>
<Icon type='icon-a-caidan' />
</IconButton>
<Box
sx={{
position: 'absolute',
width: '100%',
height: '110vh',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 999,
transition: 'all 0.3s ease-in-out',
transform: 'translateX(200%) translateY(-200%)',
...(open && {
bgcolor: 'background.default',
transform: 'translateX(0) translateY(0)',
}),
}}
>
<Stack
direction='row'
alignItems='center'
gap={1.5}
sx={{ py: '14px', cursor: 'pointer', ml: 1.5, color: 'text.primary' }}
>
{detail?.icon && <img src={detail?.icon} alt='logo' width={32} />}
<Box sx={{ fontSize: 18 }}>{detail?.title}</Box>
</Stack>
<Stack gap={4} sx={{ px: 3, mt: 4, bgcolor: 'background.default' }}>
{detail?.btns?.map(item => (
<Button
key={item.id}
fullWidth
variant={item.variant}
startIcon={
item.showIcon && item.icon ? (
<img src={item.icon} alt='logo' width={36} height={36} />
) : null
}
sx={{
textTransform: 'none',
justifyContent: 'flex-start',
height: '60px',
px: 4,
gap: 3,
fontSize: 18,
'& .MuiButton-startIcon': {
ml: 0,
mr: 0,
},
...getButtonThemeStyle(detail?.theme_mode, item.variant),
}}
>
{item.text}
</Button>
))}
</Stack>
<IconButton
size='small'
onClick={() => setOpen(!open)}
sx={{
position: 'absolute',
top: 10,
right: 10,
color: 'text.primary',
width: 40,
height: 40,
zIndex: 1,
}}
>
<Icon type='icon-chahao' />
</IconButton>
</Box>
</>
);
};
export default NavBtns;

View File

@ -1,49 +0,0 @@
import React, { Dispatch, ReactNode, SetStateAction } from 'react';
import { Box, IconButton } from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
interface OverlayProps {
open: boolean;
onClose: Dispatch<SetStateAction<boolean>>;
children: ReactNode;
}
const Overlay: React.FC<OverlayProps> = ({ open, onClose, children }) => {
return (
<>
{open && (
<Box
sx={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.7)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1300,
}}
onClick={() => onClose(false)}
>
<IconButton
onClick={() => onClose(false)}
sx={{
position: 'absolute',
top: 16,
right: 16,
color: 'white',
zIndex: 1310,
}}
>
<CloseIcon />
</IconButton>
<Box onClick={e => e.stopPropagation()}>{children}</Box>
</Box>
)}
</>
);
};
export default Overlay;

View File

@ -1,55 +0,0 @@
/**
*
* @param themeMode ('light' | 'dark')
* @param variant ('contained' | 'outlined' | 'text')
* @returns
*/
export const getButtonThemeStyle = (
themeMode: 'light' | 'dark' | undefined,
variant: string,
) => {
// 只在dark主题下应用特殊样式
if (themeMode === 'dark') {
switch (variant) {
case 'contained':
return {
bgcolor: 'primary.main',
color: 'primary.contrastText',
borderColor: 'primary.main',
'&:hover': {
bgcolor: 'primary.dark',
borderColor: 'primary.dark',
},
};
case 'outlined':
return {
borderColor: 'primary.main',
color: 'primary.main',
'&:hover': {
bgcolor: 'action.hover',
borderColor: 'primary.main',
},
};
case 'text':
return {
color: 'primary.main',
'&:hover': {
bgcolor: 'action.hover',
},
};
default:
return {};
}
}
if (themeMode === 'light') {
switch (variant) {
case 'text':
return {
color: 'text.primary',
};
}
}
return {};
};
export default getButtonThemeStyle;

View File

@ -1,69 +0,0 @@
import { SwitchProps, Box } from '@mui/material';
import { Icon } from '@ctzhian/ui';
import MySwitch from './Switch';
const ThemeSwitch = (props: SwitchProps) => {
return (
<MySwitch
{...props}
icon={
<Box
sx={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Box
sx={{
width: 22,
height: 22,
bgcolor: '#fff',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Icon
type='icon-mingliangmoshi'
sx={{ color: '#000', fontSize: 16 }}
/>
</Box>
</Box>
}
checkedIcon={
<Box
sx={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Box
sx={{
width: 22,
height: 22,
bgcolor: '#fff',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Icon
type='icon-shensemoshi'
sx={{ color: '#000', fontSize: 16 }}
/>
</Box>
</Box>
}
/>
);
};
export default ThemeSwitch;

View File

@ -10,7 +10,7 @@ import {
alpha,
} from '@mui/material';
import { v4 as uuidv4 } from 'uuid';
import { Icon } from '@ctzhian/ui';
import { IconWangyeguajian } from '@panda-wiki/icons';
import { Dispatch, SetStateAction, useMemo, useState } from 'react';
import type { CSSProperties } from 'react';
import { Component } from '../../index';
@ -185,8 +185,7 @@ const ComponentBar = ({
}}
{...(!item.fixed ? { ...attributes, ...listeners } : {})}
>
<Icon
type='icon-wangyeguajian'
<IconWangyeguajian
sx={{
color:
item.id === curComponent.id
@ -196,7 +195,7 @@ const ComponentBar = ({
: 'text.secondary',
fontSize: '14px',
}}
></Icon>
></IconWangyeguajian>
<Typography
sx={{
marginLeft: '8px',

View File

@ -1,5 +1,5 @@
import { Box, IconButton, Stack, TextField } from '@mui/material';
import { Icon } from '@ctzhian/ui';
import { IconShanchu2, IconDrag } from '@panda-wiki/icons';
import {
CSSProperties,
Dispatch,
@ -118,7 +118,7 @@ const HotSearchItem = forwardRef<HTMLDivElement, HotSearchItemProps>(
height: '28px',
}}
>
<Icon type='icon-shanchu2' sx={{ fontSize: '12px' }} />
<IconShanchu2 sx={{ fontSize: '12px' }} />
</IconButton>
<IconButton
size='small'
@ -129,7 +129,7 @@ const HotSearchItem = forwardRef<HTMLDivElement, HotSearchItemProps>(
}}
{...(dragHandleProps as any)}
>
<Icon type='icon-drag' />
<IconDrag sx={{ fontSize: '18px' }} />
</IconButton>
</Stack>
</Stack>

View File

@ -8,7 +8,7 @@ import {
Select,
MenuItem,
} from '@mui/material';
import { Icon } from '@ctzhian/ui';
import { IconShanchu2, IconDrag } from '@panda-wiki/icons';
import {
CSSProperties,
Dispatch,
@ -159,7 +159,7 @@ const Item = forwardRef<HTMLDivElement, ItemProps>(
height: '28px',
}}
>
<Icon type='icon-shanchu2' sx={{ fontSize: '12px' }} />
<IconShanchu2 sx={{ fontSize: '12px' }} />
</IconButton>
<IconButton
size='small'
@ -170,7 +170,7 @@ const Item = forwardRef<HTMLDivElement, ItemProps>(
}}
{...(dragHandleProps as any)}
>
<Icon type='icon-drag' />
<IconDrag sx={{ fontSize: '18px' }} />
</IconButton>
</Stack>
</Stack>

View File

@ -144,7 +144,7 @@ const Config: React.FC<ConfigProps> = ({ setIsEdit, id }) => {
callback();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [subscribe]);
}, [subscribe, appPreviewData, id]);
return (
<StyledCommonWrapper>

View File

@ -2,8 +2,14 @@ import { postApiV1NodeSummary } from '@/request/Node';
import { DomainRecommendNodeListResp } from '@/request/types';
import { useAppSelector } from '@/store';
import { Box, IconButton, Stack } from '@mui/material';
import { Ellipsis, Icon, message } from '@ctzhian/ui';
import { Ellipsis, message } from '@ctzhian/ui';
import { CSSProperties, forwardRef, HTMLAttributes, useState } from 'react';
import {
IconShanchu2,
IconDrag,
IconWenjianjia,
IconWenjian,
} from '@panda-wiki/icons';
export type ItemProps = HTMLAttributes<HTMLDivElement> & {
item: DomainRecommendNodeListResp;
@ -80,9 +86,12 @@ const Item = forwardRef<HTMLDivElement, ItemProps>(
<Box sx={{ fontSize: 14, color: '#2f80f7', flexShrink: 0 }}>
{item.emoji}
</Box>
) : item.type === 1 ? (
<IconWenjianjia
sx={{ fontSize: 14, color: '#2f80f7', flexShrink: 0 }}
/>
) : (
<Icon
type={item.type === 1 ? 'icon-wenjianjia' : 'icon-wenjian'}
<IconWenjian
sx={{ fontSize: 14, color: '#2f80f7', flexShrink: 0 }}
/>
)}
@ -121,12 +130,16 @@ const Item = forwardRef<HTMLDivElement, ItemProps>(
.slice(0, 4)
.map(it => (
<Stack direction={'row'} alignItems={'center'} gap={1}>
<Icon
type={
it.type === 1 ? 'icon-wenjianjia' : 'icon-wenjian'
}
sx={{ fontSize: 14, color: '#2f80f7', flexShrink: 0 }}
/>
{it.type === 1 ? (
<IconWenjianjia
sx={{ fontSize: 14, color: '#2f80f7', flexShrink: 0 }}
/>
) : (
<IconWenjian
sx={{ fontSize: 14, color: '#2f80f7', flexShrink: 0 }}
/>
)}
<Ellipsis sx={{ flex: 1, width: 0 }}>{it.name}</Ellipsis>
</Stack>
))}
@ -147,7 +160,7 @@ const Item = forwardRef<HTMLDivElement, ItemProps>(
height: '28px',
}}
>
<Icon type='icon-shanchu2' sx={{ fontSize: '12px' }} />
<IconShanchu2 sx={{ fontSize: '12px' }} />
</IconButton>
<IconButton
@ -159,7 +172,7 @@ const Item = forwardRef<HTMLDivElement, ItemProps>(
}}
{...dragHandleProps}
>
<Icon type='icon-drag' />
<IconDrag sx={{ fontSize: '18px' }} />
</IconButton>
</Stack>
</Stack>

View File

@ -1,5 +1,5 @@
import { Box, IconButton, Stack, TextField } from '@mui/material';
import { Icon } from '@ctzhian/ui';
import { IconShanchu2, IconDrag } from '@panda-wiki/icons';
import UploadFile from '@/components/UploadFile';
import {
CSSProperties,
@ -132,7 +132,7 @@ const Item = forwardRef<HTMLDivElement, ItemProps>(
height: '28px',
}}
>
<Icon type='icon-shanchu2' sx={{ fontSize: '12px' }} />
<IconShanchu2 sx={{ fontSize: '12px' }} />
</IconButton>
<IconButton
size='small'
@ -143,7 +143,7 @@ const Item = forwardRef<HTMLDivElement, ItemProps>(
}}
{...(dragHandleProps as any)}
>
<Icon type='icon-drag' />
<IconDrag sx={{ fontSize: '18px' }} />
</IconButton>
</Stack>
</Stack>

View File

@ -1,5 +1,5 @@
import { Box, IconButton, Stack, TextField } from '@mui/material';
import { Icon } from '@ctzhian/ui';
import { IconShanchu2, IconDrag } from '@panda-wiki/icons';
import UploadFile from '@/components/UploadFile';
import {
CSSProperties,
@ -152,7 +152,7 @@ const Item = forwardRef<HTMLDivElement, ItemProps>(
height: '28px',
}}
>
<Icon type='icon-shanchu2' sx={{ fontSize: '12px' }} />
<IconShanchu2 sx={{ fontSize: '12px' }} />
</IconButton>
<IconButton
size='small'
@ -163,7 +163,7 @@ const Item = forwardRef<HTMLDivElement, ItemProps>(
}}
{...(dragHandleProps as any)}
>
<Icon type='icon-drag' />
<IconDrag sx={{ fontSize: '18px' }} />
</IconButton>
</Stack>
</Stack>

View File

@ -1,5 +1,5 @@
import { Box, IconButton, Stack, TextField } from '@mui/material';
import { Icon } from '@ctzhian/ui';
import { IconShanchu2, IconDrag } from '@panda-wiki/icons';
import {
CSSProperties,
Dispatch,
@ -143,7 +143,7 @@ const ItemType = forwardRef<HTMLDivElement, ItemTypeProps>(
height: '28px',
}}
>
<Icon type='icon-shanchu2' sx={{ fontSize: '12px' }} />
<IconShanchu2 sx={{ fontSize: '12px' }} />
</IconButton>
<IconButton
size='small'
@ -154,7 +154,7 @@ const ItemType = forwardRef<HTMLDivElement, ItemTypeProps>(
}}
{...(dragHandleProps as any)}
>
<Icon type='icon-drag' />
<IconDrag sx={{ fontSize: '18px' }} />
</IconButton>
</Stack>
</Stack>

View File

@ -1,5 +1,5 @@
import { Box, IconButton, Stack, TextField } from '@mui/material';
import { Icon } from '@ctzhian/ui';
import { IconShanchu2, IconDrag } from '@panda-wiki/icons';
import {
CSSProperties,
Dispatch,
@ -185,7 +185,7 @@ const ItemType = forwardRef<HTMLDivElement, ItemTypeProps>(
height: '28px',
}}
>
<Icon type='icon-shanchu2' sx={{ fontSize: '12px' }} />
<IconShanchu2 sx={{ fontSize: '12px' }} />
</IconButton>
<IconButton
size='small'
@ -196,7 +196,7 @@ const ItemType = forwardRef<HTMLDivElement, ItemTypeProps>(
}}
{...(dragHandleProps as any)}
>
<Icon type='icon-drag' />
<IconDrag sx={{ fontSize: '18px' }} />
</IconButton>
</Stack>
</Stack>

View File

@ -2,7 +2,13 @@ import { postApiV1NodeSummary } from '@/request/Node';
import { DomainRecommendNodeListResp } from '@/request/types';
import { useAppSelector } from '@/store';
import { Box, IconButton, Stack } from '@mui/material';
import { Ellipsis, Icon, message } from '@ctzhian/ui';
import { Ellipsis, message } from '@ctzhian/ui';
import {
IconShanchu2,
IconDrag,
IconWenjianjia,
IconWenjian,
} from '@panda-wiki/icons';
import { CSSProperties, forwardRef, HTMLAttributes, useState } from 'react';
export type ItemProps = HTMLAttributes<HTMLDivElement> & {
@ -82,9 +88,12 @@ const Item = forwardRef<HTMLDivElement, ItemProps>(
<Box sx={{ fontSize: 14, color: '#2f80f7', flexShrink: 0 }}>
{item.emoji}
</Box>
) : item.type === 1 ? (
<IconWenjianjia
sx={{ fontSize: 14, color: '#2f80f7', flexShrink: 0 }}
/>
) : (
<Icon
type={item.type === 1 ? 'icon-wenjianjia' : 'icon-wenjian'}
<IconWenjian
sx={{ fontSize: 14, color: '#2f80f7', flexShrink: 0 }}
/>
)}
@ -128,11 +137,12 @@ const Item = forwardRef<HTMLDivElement, ItemProps>(
>
{it.emoji}
</Box>
) : it.type === 1 ? (
<IconWenjianjia
sx={{ fontSize: 14, color: '#2f80f7', flexShrink: 0 }}
/>
) : (
<Icon
type={
it.type === 1 ? 'icon-wenjianjia' : 'icon-wenjian'
}
<IconWenjian
sx={{ fontSize: 14, color: '#2f80f7', flexShrink: 0 }}
/>
)}
@ -157,7 +167,7 @@ const Item = forwardRef<HTMLDivElement, ItemProps>(
height: '28px',
}}
>
<Icon type='icon-shanchu2' sx={{ fontSize: '12px' }} />
<IconShanchu2 sx={{ fontSize: '12px' }} />
</IconButton>
<IconButton
@ -169,7 +179,7 @@ const Item = forwardRef<HTMLDivElement, ItemProps>(
}}
{...dragHandleProps}
>
<Icon type='icon-drag' />
<IconDrag sx={{ fontSize: '18px' }} />
</IconButton>
</Stack>
</Stack>

View File

@ -1,5 +1,5 @@
import { Box, IconButton, Stack, TextField } from '@mui/material';
import { Icon } from '@ctzhian/ui';
import { IconShanchu2, IconDrag } from '@panda-wiki/icons';
import {
CSSProperties,
Dispatch,
@ -143,7 +143,7 @@ const FaqItem = forwardRef<HTMLDivElement, FaqItemProps>(
height: '28px',
}}
>
<Icon type='icon-shanchu2' sx={{ fontSize: '12px' }} />
<IconShanchu2 sx={{ fontSize: '12px' }} />
</IconButton>
<IconButton
size='small'
@ -154,7 +154,7 @@ const FaqItem = forwardRef<HTMLDivElement, FaqItemProps>(
}}
{...(dragHandleProps as any)}
>
<Icon type='icon-drag' />
<IconDrag sx={{ fontSize: '18px' }} />
</IconButton>
</Stack>
</Stack>

View File

@ -1,5 +1,5 @@
import { Box, IconButton, Stack, TextField } from '@mui/material';
import { Icon } from '@ctzhian/ui';
import { IconShanchu2, IconDrag } from '@panda-wiki/icons';
import {
CSSProperties,
Dispatch,
@ -143,7 +143,7 @@ const Item = forwardRef<HTMLDivElement, ItemProps>(
height: '28px',
}}
>
<Icon type='icon-shanchu2' sx={{ fontSize: '12px' }} />
<IconShanchu2 sx={{ fontSize: '12px' }} />
</IconButton>
<IconButton
size='small'
@ -154,7 +154,7 @@ const Item = forwardRef<HTMLDivElement, ItemProps>(
}}
{...(dragHandleProps as any)}
>
<Icon type='icon-drag' />
<IconDrag sx={{ fontSize: '18px' }} />
</IconButton>
</Stack>
</Stack>

View File

@ -1,15 +1,23 @@
import { AppDetail, HeaderSetting } from '@/api';
import UploadFile from '@/components/UploadFile';
import { Stack, Box, TextField } from '@mui/material';
import { Stack, Box, TextField, SvgIconProps } from '@mui/material';
import DragBrand from '../basicComponents/DragBrand';
import { Icon } from '@ctzhian/ui';
import { Dispatch, SetStateAction, useEffect, useMemo } from 'react';
import { Dispatch, SetStateAction, useEffect } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useAppDispatch, useAppSelector } from '@/store';
import { setAppPreviewData } from '@/store/slices/config';
import { DomainSocialMediaAccount } from '@/request/types';
import Switch from '../basicComponents/Switch';
import DragSocialInfo from '../basicComponents/DragSocialInfo';
import VersionMask from '@/components/VersionMask';
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
import { IconTianjia } from '@panda-wiki/icons';
import {
IconWeixingongzhonghao,
IconDianhua,
IconWeixingongzhonghaoDaiyanse,
IconDianhua1,
} from '@panda-wiki/icons';
interface FooterConfigProps {
data?: AppDetail | null;
@ -19,8 +27,8 @@ interface FooterConfigProps {
export interface Option {
key: string;
value: string;
type: string;
config_type?: string;
type: React.ComponentType<SvgIconProps>;
config_type?: React.ComponentType<SvgIconProps>;
text_placeholder?: string;
text_label?: string;
}
@ -28,16 +36,16 @@ export const options: Option[] = [
{
key: 'wechat_oa',
value: '微信公众号',
type: 'icon-weixingongzhonghao',
config_type: 'icon-weixingongzhonghao-daiyanse',
type: IconWeixingongzhonghao,
config_type: IconWeixingongzhonghaoDaiyanse,
text_placeholder: '请输入公众号名称',
text_label: '公众号名称',
},
{
key: 'phone',
value: '电话',
type: 'icon-dianhua',
config_type: 'icon-dianhua1',
type: IconDianhua,
config_type: IconDianhua1,
text_placeholder: '请输入文字',
text_label: '文字',
},
@ -75,9 +83,6 @@ const FooterConfig = ({ data, setIsEdit, isEdit }: FooterConfigProps) => {
);
const footer_show_intro = watch('footer_show_intro');
const isEnterprise = useMemo(() => {
return license.edition === 2;
}, [license]);
useEffect(() => {
if (isEdit && appPreviewData) {
setValue(
@ -332,8 +337,7 @@ const FooterConfig = ({ data, setIsEdit, isEdit }: FooterConfigProps) => {
setIsEdit(true);
}}
>
<Icon
type='icon-tianjia'
<IconTianjia
sx={{ fontSize: '10px !important', color: '#5F58FE' }}
/>
<Box
@ -397,8 +401,7 @@ const FooterConfig = ({ data, setIsEdit, isEdit }: FooterConfigProps) => {
setIsEdit(true);
}}
>
<Icon
type='icon-tianjia'
<IconTianjia
sx={{ fontSize: '10px !important', color: '#5F58FE' }}
/>
<Box
@ -506,29 +509,34 @@ const FooterConfig = ({ data, setIsEdit, isEdit }: FooterConfigProps) => {
)}
/>
</Stack>
{isEnterprise && (
<Stack direction={'column'} gap={2}>
<Box
sx={{
fontSize: 14,
lineHeight: '22px',
flexShrink: 0,
display: 'flex',
alignItems: 'center',
fontWeight: 600,
'&::before': {
content: '""',
display: 'inline-block',
width: 4,
height: 12,
bgcolor: '#3248F2',
borderRadius: '2px',
mr: 1,
},
}}
>
PandaWiki
</Box>
<Stack direction={'column'} gap={2}>
<Box
sx={{
fontSize: 14,
lineHeight: '22px',
flexShrink: 0,
display: 'flex',
alignItems: 'center',
fontWeight: 600,
'&::before': {
content: '""',
display: 'inline-block',
width: 4,
height: 12,
bgcolor: '#3248F2',
borderRadius: '2px',
mr: 1,
},
}}
>
PandaWiki
</Box>
<VersionMask
permission={PROFESSION_VERSION_PERMISSION}
wrapperSx={{ px: 2 }}
sx={{ inset: '-8px 0' }}
>
<Controller
control={control}
name='show_brand_info'
@ -548,7 +556,6 @@ const FooterConfig = ({ data, setIsEdit, isEdit }: FooterConfigProps) => {
<Switch
sx={{ marginLeft: 'auto' }}
{...field}
disabled={!isEnterprise}
checked={field?.value === false ? false : true}
onChange={e => {
field.onChange(e.target.checked);
@ -558,8 +565,8 @@ const FooterConfig = ({ data, setIsEdit, isEdit }: FooterConfigProps) => {
</Stack>
)}
/>
</Stack>
)}
</VersionMask>
</Stack>
</Stack>
</>
);

View File

@ -2,12 +2,11 @@ import { AppDetail, HeaderSetting } from '@/api';
import DragBtn from '../basicComponents/DragBtn';
import UploadFile from '@/components/UploadFile';
import { Stack, Box, TextField } from '@mui/material';
import { Icon } from '@ctzhian/ui';
import { Dispatch, SetStateAction, useEffect } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useAppDispatch, useAppSelector } from '@/store';
import { setAppPreviewData } from '@/store/slices/config';
import { useAppSelector } from '@/store';
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
import { IconTianjia } from '@panda-wiki/icons';
interface CardWebHeaderProps {
data?: AppDetail | null;
@ -17,7 +16,6 @@ interface CardWebHeaderProps {
const HeaderConfig = ({ data, setIsEdit, isEdit }: CardWebHeaderProps) => {
const { appPreviewData } = useAppSelector(state => state.config);
const dispatch = useAppDispatch();
const debouncedDispatch = useDebounceAppPreviewData();
const {
control,
@ -257,8 +255,7 @@ const HeaderConfig = ({ data, setIsEdit, isEdit }: CardWebHeaderProps) => {
}}
onClick={handleAddButton}
>
<Icon
type='icon-tianjia'
<IconTianjia
sx={{ fontSize: '10px !important', color: '#5F58FE' }}
/>
<Box sx={{ fontSize: 14, lineHeight: '22px', marginLeft: 0.5 }}>

View File

@ -1,5 +1,5 @@
import { Box, IconButton, Stack, TextField } from '@mui/material';
import { Icon } from '@ctzhian/ui';
import { IconShanchu2, IconDrag } from '@panda-wiki/icons';
import {
CSSProperties,
Dispatch,
@ -143,7 +143,7 @@ const ItemType = forwardRef<HTMLDivElement, ItemTypeProps>(
height: '28px',
}}
>
<Icon type='icon-shanchu2' sx={{ fontSize: '12px' }} />
<IconShanchu2 sx={{ fontSize: '12px' }} />
</IconButton>
<IconButton
size='small'
@ -154,7 +154,7 @@ const ItemType = forwardRef<HTMLDivElement, ItemTypeProps>(
}}
{...(dragHandleProps as any)}
>
<Icon type='icon-drag' />
<IconDrag sx={{ fontSize: '18px' }} />
</IconButton>
</Stack>
</Stack>

View File

@ -1,5 +1,5 @@
import { Box, IconButton, Stack, TextField } from '@mui/material';
import { Icon } from '@ctzhian/ui';
import { IconShanchu2, IconDrag } from '@panda-wiki/icons';
import {
CSSProperties,
Dispatch,
@ -115,7 +115,7 @@ const Item = forwardRef<HTMLDivElement, ItemProps>(
height: '28px',
}}
>
<Icon type='icon-shanchu2' sx={{ fontSize: '12px' }} />
<IconShanchu2 sx={{ fontSize: '12px' }} />
</IconButton>
<IconButton
size='small'
@ -126,7 +126,7 @@ const Item = forwardRef<HTMLDivElement, ItemProps>(
}}
{...(dragHandleProps as any)}
>
<Icon type='icon-drag' />
<IconDrag sx={{ fontSize: '18px' }} />
</IconButton>
</Stack>
</Stack>

View File

@ -1,9 +1,13 @@
import { postApiV1NodeSummary } from '@/request/Node';
import { DomainRecommendNodeListResp } from '@/request/types';
import { useAppSelector } from '@/store';
import { Box, IconButton, Stack } from '@mui/material';
import { Ellipsis, Icon, message } from '@ctzhian/ui';
import { CSSProperties, forwardRef, HTMLAttributes, useState } from 'react';
import {
IconShanchu2,
IconDrag,
IconWenjianjia,
IconWenjian,
} from '@panda-wiki/icons';
import { Ellipsis } from '@ctzhian/ui';
import { CSSProperties, forwardRef, HTMLAttributes } from 'react';
export type ItemProps = HTMLAttributes<HTMLDivElement> & {
item: DomainRecommendNodeListResp;
@ -66,9 +70,12 @@ const Item = forwardRef<HTMLDivElement, ItemProps>(
<Box sx={{ fontSize: 14, color: '#2f80f7', flexShrink: 0 }}>
{item.emoji}
</Box>
) : item.type === 1 ? (
<IconWenjianjia
sx={{ fontSize: 14, color: '#2f80f7', flexShrink: 0 }}
/>
) : (
<Icon
type={item.type === 1 ? 'icon-wenjianjia' : 'icon-wenjian'}
<IconWenjian
sx={{ fontSize: 14, color: '#2f80f7', flexShrink: 0 }}
/>
)}
@ -91,7 +98,7 @@ const Item = forwardRef<HTMLDivElement, ItemProps>(
height: '28px',
}}
>
<Icon type='icon-shanchu2' sx={{ fontSize: '12px' }} />
<IconShanchu2 sx={{ fontSize: '12px' }} />
</IconButton>
<IconButton
@ -103,7 +110,7 @@ const Item = forwardRef<HTMLDivElement, ItemProps>(
}}
{...dragHandleProps}
>
<Icon type='icon-drag' />
<IconDrag sx={{ fontSize: '18px' }} />
</IconButton>
</Stack>
</Stack>

View File

@ -1,6 +1,6 @@
import { useEffect, useState, useRef } from 'react';
import { Button, Stack, Typography } from '@mui/material';
import { CusTabs, Icon, message, Modal } from '@ctzhian/ui';
import { CusTabs, message, Modal } from '@ctzhian/ui';
import {
getApiV1NodeRecommendNodes,
getApiV1KnowledgeBaseDetail,
@ -9,6 +9,7 @@ import {
DomainAppDetailResp,
DomainWebAppLandingConfigResp,
} from '@/request/types';
import { IconPCduan, IconYidongduan } from '@panda-wiki/icons';
import { getApiV1AppDetail, putApiV1App } from '@/request/App';
import { useAppSelector, useAppDispatch } from '@/store';
@ -338,18 +339,11 @@ const CustomModal = ({
<CusTabs
list={[
{
label: (
<Icon type='icon-PCduan' sx={{ height: '32px' }} />
),
label: <IconPCduan sx={{ fontSize: 14 }} />,
value: 'pc',
},
{
label: (
<Icon
type='icon-yidongduan'
sx={{ height: '32px' }}
/>
),
label: <IconYidongduan sx={{ fontSize: 14 }} />,
value: 'mobile',
},
]}

View File

@ -1,390 +0,0 @@
import { FooterSetting } from '@/api/type';
import {
closestCenter,
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
rectSortingStrategy,
SortableContext,
useSortable,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Box, Button, IconButton, Stack, TextField } from '@mui/material';
import { Icon } from '@ctzhian/ui';
import {
CSSProperties,
forwardRef,
HTMLAttributes,
useCallback,
useState,
} from 'react';
import {
Control,
Controller,
FieldErrors,
useFieldArray,
} from 'react-hook-form';
export type ItemProps = HTMLAttributes<HTMLDivElement> & {
groupIndex: number;
withOpacity?: boolean;
isDragging?: boolean;
dragHandleProps?: any;
control: Control<FooterSetting>;
errors: FieldErrors<FooterSetting>;
setIsEdit: (value: boolean) => void;
handleRemove?: () => void;
};
interface LinkItemProps extends HTMLAttributes<HTMLDivElement> {
linkId: string;
linkIndex: number;
groupIndex: number;
control: Control<FooterSetting>;
errors: FieldErrors<FooterSetting>;
setIsEdit: (value: boolean) => void;
onRemove: () => void;
withOpacity?: boolean;
isDragging?: boolean;
dragHandleProps?: any;
}
const LinkItem = forwardRef<HTMLDivElement, LinkItemProps>(
(
{
linkIndex,
groupIndex,
control,
errors,
setIsEdit,
onRemove,
withOpacity,
isDragging,
dragHandleProps,
style,
...props
},
ref,
) => {
const inlineStyles: CSSProperties = {
opacity: withOpacity ? '0.5' : '1',
cursor: isDragging ? 'grabbing' : 'grab',
...style,
};
return (
<Box ref={ref} style={inlineStyles} {...props}>
<Stack gap={1} alignItems='center' direction='row'>
<IconButton
size='small'
sx={{
cursor: 'grab',
color: 'text.secondary',
'&:hover': { color: 'primary.main' },
flexShrink: 0,
}}
{...dragHandleProps}
>
<Icon type='icon-drag' />
</IconButton>
<Box
sx={{
color: 'text.secondary',
flexShrink: 0,
fontSize: 12,
width: 20,
}}
>
{linkIndex + 1}.
</Box>
<Controller
control={control}
name={`brand_groups.${groupIndex}.links.${linkIndex}.name`}
rules={{ required: '请输入链接文字' }}
render={({ field }) => (
<TextField
{...field}
sx={{ width: 300 }}
label='链接文字'
placeholder='链接文字'
onChange={e => {
field.onChange(e.target.value);
setIsEdit(true);
}}
error={
!!errors.brand_groups?.[groupIndex]?.links?.[linkIndex]?.name
}
helperText={
errors.brand_groups?.[groupIndex]?.links?.[linkIndex]?.name
?.message
}
/>
)}
/>
<Controller
control={control}
name={`brand_groups.${groupIndex}.links.${linkIndex}.url`}
rules={{ required: '请输入链接地址' }}
render={({ field }) => (
<TextField
{...field}
fullWidth
label='链接地址'
placeholder='链接地址'
onChange={e => {
field.onChange(e.target.value);
setIsEdit(true);
}}
error={
!!errors.brand_groups?.[groupIndex]?.links?.[linkIndex]?.url
}
helperText={
errors.brand_groups?.[groupIndex]?.links?.[linkIndex]?.url
?.message
}
/>
)}
/>
<IconButton size='small' sx={{ flexShrink: 0 }} onClick={onRemove}>
<Icon type='icon-icon_tool_close' />
</IconButton>
</Stack>
</Box>
);
},
);
const SortableLinkItem: React.FC<LinkItemProps> = ({ linkId, ...rest }) => {
const {
isDragging,
attributes,
listeners,
setNodeRef,
transform,
transition,
} = useSortable({ id: linkId });
const style = {
transform: CSS.Transform.toString(transform),
transition: transition || undefined,
};
return (
<LinkItem
ref={setNodeRef}
style={style}
withOpacity={isDragging}
dragHandleProps={{
...attributes,
...listeners,
}}
linkId={linkId}
{...rest}
/>
);
};
const Item = forwardRef<HTMLDivElement, ItemProps>(
(
{
groupIndex,
withOpacity,
isDragging,
style,
dragHandleProps,
handleRemove,
control,
errors,
setIsEdit,
...props
},
ref,
) => {
const [activeId, setActiveId] = useState<string | null>(null);
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
const {
fields: linkFields,
append: appendLink,
remove: removeLink,
move: moveLink,
} = useFieldArray({
control,
name: `brand_groups.${groupIndex}.links`,
});
const inlineStyles: CSSProperties = {
opacity: withOpacity ? '0.5' : '1',
borderRadius: '10px',
cursor: isDragging ? 'grabbing' : 'grab',
backgroundColor: '#ffffff',
width: '100%',
...style,
};
const handleLinkDragStart = useCallback((event: DragStartEvent) => {
setActiveId(event.active.id as string);
}, []);
const handleLinkDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
if (active.id !== over?.id) {
const oldIndex = linkFields.findIndex(
(_, index) => `link-${groupIndex}-${index}` === active.id,
);
const newIndex = linkFields.findIndex(
(_, index) => `link-${groupIndex}-${index}` === over!.id,
);
moveLink(oldIndex, newIndex);
setIsEdit(true);
}
setActiveId(null);
},
[linkFields, moveLink, setIsEdit, groupIndex],
);
const handleLinkDragCancel = useCallback(() => {
setActiveId(null);
}, []);
const handleAddLink = () => {
appendLink({ name: '', url: '' });
setIsEdit(true);
};
const handleRemoveLink = (linkIndex: number) => {
removeLink(linkIndex);
setIsEdit(true);
};
return (
<Box ref={ref} style={inlineStyles} {...props}>
<Box
sx={{
border: '1px solid',
borderColor: 'divider',
borderRadius: '10px',
p: 2,
mb: 1,
pb: 1,
}}
>
<Stack direction='row' alignItems='center' gap={1} sx={{ mb: 1 }}>
<IconButton
size='small'
sx={{
cursor: 'grab',
color: 'text.secondary',
'&:hover': { color: 'primary.main' },
flexShrink: 0,
}}
{...dragHandleProps}
>
<Icon type='icon-drag' />
</IconButton>
<Controller
control={control}
name={`brand_groups.${groupIndex}.name`}
rules={{ required: '请输入链接组名称' }}
render={({ field }) => (
<TextField
{...field}
fullWidth
placeholder='输入链接组名称'
label='链接组名称'
onChange={e => {
field.onChange(e.target.value);
setIsEdit(true);
}}
error={!!errors.brand_groups?.[groupIndex]?.name}
helperText={errors.brand_groups?.[groupIndex]?.name?.message}
/>
)}
/>
<IconButton size='small' onClick={handleRemove}>
<Icon type='icon-icon_tool_close' />
</IconButton>
</Stack>
{/* 链接拖拽区域 */}
{linkFields.length > 0 && (
<Box
sx={{
border: '1px dashed',
borderColor: 'divider',
borderRadius: '10px',
py: 1,
pl: 2,
}}
>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleLinkDragStart}
onDragEnd={handleLinkDragEnd}
onDragCancel={handleLinkDragCancel}
>
<SortableContext
items={linkFields.map(
(_, index) => `link-${groupIndex}-${index}`,
)}
strategy={rectSortingStrategy}
>
<Stack gap={1}>
{linkFields.map((link, linkIndex) => (
<SortableLinkItem
key={link.id}
linkId={`link-${groupIndex}-${linkIndex}`}
linkIndex={linkIndex}
groupIndex={groupIndex}
control={control}
errors={errors}
setIsEdit={setIsEdit}
onRemove={() => handleRemoveLink(linkIndex)}
/>
))}
</Stack>
</SortableContext>
<DragOverlay adjustScale style={{ transformOrigin: '0 0' }}>
{activeId ? (
<LinkItem
isDragging
linkId={activeId}
linkIndex={parseInt(activeId.split('-')[2])}
groupIndex={groupIndex}
control={control}
errors={errors}
setIsEdit={setIsEdit}
onRemove={() => {}}
/>
) : null}
</DragOverlay>
</DndContext>
</Box>
)}
<Button
size='small'
startIcon={
<Icon type='icon-add' sx={{ fontSize: '12px !important' }} />
}
onClick={handleAddLink}
sx={{ mt: 1 }}
>
</Button>
</Box>
</Box>
);
},
);
export default Item;

View File

@ -1,49 +0,0 @@
import { FooterSetting } from '@/api/type';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { FC } from 'react';
import { Control, FieldErrors } from 'react-hook-form';
import Item, { ItemProps } from './Item';
type SortableItemProps = Omit<
ItemProps,
'withOpacity' | 'isDragging' | 'dragHandleProps'
> & {
id: string;
groupIndex: number;
control: Control<FooterSetting>;
errors: FieldErrors<FooterSetting>;
setIsEdit: (value: boolean) => void;
handleRemove: () => void;
};
const SortableItem: FC<SortableItemProps> = ({ id, ...rest }) => {
const {
isDragging,
attributes,
listeners,
setNodeRef,
transform,
transition,
} = useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition: transition || undefined,
};
return (
<Item
ref={setNodeRef}
style={style}
withOpacity={isDragging}
dragHandleProps={{
...attributes,
...listeners,
}}
{...rest}
/>
);
};
export default SortableItem;

Some files were not shown because too many files have changed in this diff Show More