Skip to content

Commit 6e772ea

Browse files
awesomeYGdhsifssxbingWMonkeyCode-AIawesomeYG
authored
Feat/v2.30.0 (#205)
* feat: http webhook add user org_ids * fix: check empty error msg * fix: doc list add message return * fix: trunc error length * feat: wecom bot * fix: summary output * fix: error message length * Feat/model status event (#203) * feat: 添加模型状态监控功能 - 添加 raglite.events.model.status 消息处理 - 后端添加 LLM 状态字段 (status, message) - 前端模型管理页面显示模型状态 - 支持状态异常时显示错误信息 Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com> Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com> * feat: 智能对话模型调用时自动更新状态 - 在 LLM 服务中添加 updateChatModelStatus 方法 - Chat 方法调用成功/失败时更新模型状态 - StreamChat 方法调用成功/失败时更新模型状态 - 调用失败时记录错误信息到 message 字段 Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com> * refactor: 简化 StreamChat 错误处理逻辑 - 移除冗余的 hasError 标志 - 直接通过 streamErr 判断是否有错误 Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com> --------- Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com> * feat: 游客角色禁用组织修改,改为游客时自动设置默认组织 (#200) - 当用户角色为游客时,禁用组织选择器 - 角色改为游客时,自动将组织设置为 builtin=true 的默认组织 - 角色改为非游客时,可以正常修改组织 Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com> * feat: Integrate DingTalk bot configuration into ChatConfig, clarify bot source text, and refine UI spacing. * feat: rag * fix: Retry SSE subscriptions automatically when a CSRF token mismatch (400 error) is detected. * feat: Display document processing error messages in a tooltip and refactor document type definitions. * refactor: Improve model status handling, update chat configuration UI with radio buttons and conditional fields, and shorten organization ID label. * fix: check empty task id * style: Add border color to horizontal rule elements and remove unused EditorContent import. --------- Co-authored-by: 姚凯 <kai.yao@chaitin.com> Co-authored-by: xbingW <150229899+xbingW@users.noreply.github.com> Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com> Co-authored-by: awesomeYG <gang.yang@chaitin.com> Co-authored-by: xiaobing.wang <xiaobing.wang@chaitin.com>
1 parent d472144 commit 6e772ea

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1425
-240
lines changed

.github/workflows/build-base-img.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ jobs:
4444
- name: raglite
4545
dockerfile: docker/raglite/Dockerfile
4646
context: docker/raglite
47-
tag: "v2.12.0"
47+
tag: "v2.13.0"
4848

4949
steps:
5050
- name: Checkout code

backend/docs/docs.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10284,6 +10284,9 @@ const docTemplate = `{
1028410284
"id": {
1028510285
"type": "integer"
1028610286
},
10287+
"message": {
10288+
"type": "string"
10289+
},
1028710290
"parent_id": {
1028810291
"type": "integer"
1028910292
},

backend/docs/swagger.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10277,6 +10277,9 @@
1027710277
"id": {
1027810278
"type": "integer"
1027910279
},
10280+
"message": {
10281+
"type": "string"
10282+
},
1028010283
"parent_id": {
1028110284
"type": "integer"
1028210285
},

backend/docs/swagger.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1704,6 +1704,8 @@ definitions:
17041704
type: array
17051705
id:
17061706
type: integer
1707+
message:
1708+
type: string
17071709
parent_id:
17081710
type: integer
17091711
platform:

backend/model/llm.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@ func (m LLMType) RagSupported() bool {
6565
return m != LLMTypeChat
6666
}
6767

68+
type LLMStatus string
69+
70+
const (
71+
LLMStatusNormal LLMStatus = "normal"
72+
LLMStatusError LLMStatus = "error"
73+
)
74+
6875
type LLM struct {
6976
Base
7077

@@ -80,6 +87,9 @@ type LLM struct {
8087

8188
IsActive bool `json:"is_active" gorm:"default:false"`
8289

90+
Status LLMStatus `json:"status" gorm:"default:normal"`
91+
Message string `json:"message" gorm:"default:''"`
92+
8393
PromptTokens uint64 `json:"prompt_tokens" gorm:"default:0"`
8494
CompletionTokens uint64 `json:"completion_tokens" gorm:"default:0"`
8595
TotalTokens uint64 `json:"total_tokens" gorm:"default:0"`

backend/model/user_review.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@ type UserReview struct {
2727
type UserReviewWithUser struct {
2828
UserReview
2929

30-
UserEmail string `json:"user_email"`
31-
UserName string `json:"user_name"`
32-
UserAvatar string `json:"user_avatar"`
30+
UserEmail string `json:"user_email"`
31+
UserName string `json:"user_name"`
32+
UserAvatar string `json:"user_avatar"`
33+
UserOrgIDs Int64Array `json:"-" gorm:"column:user_org_ids;type:bigint[]"`
3334
}
3435

3536
type UserReviewHeader struct {

backend/pkg/chat/chat.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type ChatType uint
1313
const (
1414
ChatTypeUnknown ChatType = iota
1515
ChatTypeDingtalk
16+
ChatTypeWecom
1617
)
1718

1819
type BotReq struct {
@@ -22,15 +23,31 @@ type BotReq struct {
2223

2324
type BotCallback func(ctx context.Context, req BotReq) (*llm.Stream[string], error)
2425

26+
type VerifySign struct {
27+
Signature string `form:"signature" binding:"required"`
28+
Timestamp string `form:"timestamp" binding:"required"`
29+
Nonce string `form:"nonce" binding:"required"`
30+
}
31+
32+
type VerifyReq struct {
33+
VerifySign
34+
35+
Content string
36+
OnlyVerify bool
37+
}
38+
2539
type Bot interface {
2640
Start() error
41+
StreamText(context.Context, VerifyReq) (string, error)
2742
Stop()
2843
}
2944

3045
func New(typ ChatType, cfg model.SystemChatConfig, callback BotCallback) (Bot, error) {
3146
switch typ {
3247
case ChatTypeDingtalk:
3348
return newDingtalk(cfg, callback)
49+
case ChatTypeWecom:
50+
return newWecom(cfg, callback)
3451
default:
3552
return nil, errors.ErrUnsupported
3653
}

backend/pkg/chat/dingtalk.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package chat
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"net/http"
78
"strings"
@@ -253,6 +254,10 @@ func (d *dingtalk) Stop() {
253254
d.streamCli.Close()
254255
}
255256

257+
func (d *dingtalk) StreamText(_ context.Context, _ VerifyReq) (string, error) {
258+
return "", errors.ErrUnsupported
259+
}
260+
256261
func newDingtalk(cfg model.SystemChatConfig, callback BotCallback) (Bot, error) {
257262
config := &openapi.Config{}
258263
config.Protocol = tea.String("https")

backend/pkg/chat/wecom.go

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
package chat
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"strings"
8+
"sync"
9+
10+
"github.com/chaitin/koalaqa/model"
11+
"github.com/chaitin/koalaqa/pkg/crypt"
12+
"github.com/chaitin/koalaqa/pkg/glog"
13+
)
14+
15+
type streamState struct {
16+
mutex sync.Mutex
17+
question string
18+
stream strings.Builder
19+
Done bool
20+
}
21+
22+
func (s *streamState) Append(data string, done bool) {
23+
s.mutex.Lock()
24+
defer s.mutex.Unlock()
25+
26+
if data != "" {
27+
s.stream.WriteString(data)
28+
}
29+
30+
if done {
31+
s.Done = done
32+
}
33+
}
34+
35+
func (s *streamState) Text() string {
36+
s.mutex.Lock()
37+
defer s.mutex.Unlock()
38+
39+
return s.stream.String()
40+
}
41+
42+
type wecom struct {
43+
logger *glog.Logger
44+
cache sync.Map
45+
cfg model.SystemChatConfig
46+
botCallback BotCallback
47+
}
48+
49+
func (w *wecom) verify(req VerifyReq) (string, error) {
50+
wx, _, err := crypt.NewWXBizJsonMsgCrypt(w.cfg.ClientID, w.cfg.ClientSecret, "")
51+
if err != nil {
52+
return "", err
53+
}
54+
code, str := wx.VerifyURL(req.Signature, req.Timestamp, req.Nonce, req.Content)
55+
err = crypt.WecomErrorByCode(code)
56+
if err != nil {
57+
return "", err
58+
}
59+
60+
return str, nil
61+
}
62+
63+
type userReq struct {
64+
Msgid string `json:"msgid"`
65+
Aibotid string `json:"aibotid"`
66+
Chattype string `json:"chattype"`
67+
From struct {
68+
Userid string `json:"userid"`
69+
} `json:"from"`
70+
Msgtype string `json:"msgtype"`
71+
Text struct {
72+
Content string `json:"content"`
73+
} `json:"text"`
74+
Stream struct {
75+
Id string `json:"id"`
76+
} `json:"stream"`
77+
}
78+
79+
type userResp struct {
80+
Msgtype string `json:"msgtype"`
81+
Stream weComStream `json:"stream"`
82+
}
83+
84+
type weComStream struct {
85+
Id string `json:"id"`
86+
Finish bool `json:"finish"`
87+
Content string `json:"content"`
88+
MsgItem []struct {
89+
Msgtype string `json:"msgtype"`
90+
Image struct {
91+
Base64 string `json:"base64"`
92+
Md5 string `json:"md5"`
93+
} `json:"image"`
94+
} `json:"msg_item"`
95+
}
96+
97+
func (w *wecom) decryptUserReq(ctx context.Context, req VerifyReq) (*userReq, error) {
98+
wx, _, err := crypt.NewWXBizJsonMsgCrypt(
99+
w.cfg.ClientID,
100+
w.cfg.ClientSecret,
101+
"",
102+
)
103+
if err != nil {
104+
return nil, err
105+
}
106+
107+
code, reqMsg := wx.DecryptMsg(req.Content, req.Signature, req.Timestamp, req.Nonce)
108+
err = crypt.WecomErrorByCode(code)
109+
if err != nil {
110+
return nil, err
111+
}
112+
logger := w.logger.WithContext(ctx).With("req", reqMsg)
113+
logger.Info("recv wecom req")
114+
var data userReq
115+
err = json.Unmarshal([]byte(reqMsg), &data)
116+
if err != nil {
117+
return nil, err
118+
}
119+
120+
return &data, nil
121+
}
122+
123+
func (w *wecom) EncryptUserRes(ctx context.Context, nonce string, data weComStream) (string, error) {
124+
wx, _, err := crypt.NewWXBizJsonMsgCrypt(
125+
w.cfg.ClientID,
126+
w.cfg.ClientSecret,
127+
"",
128+
)
129+
if err != nil {
130+
return "", err
131+
}
132+
133+
respBytes, err := json.Marshal(userResp{
134+
Msgtype: "stream",
135+
Stream: data,
136+
})
137+
if err != nil {
138+
return "", err
139+
}
140+
141+
code, msg := wx.EncryptMsg(string(respBytes), nonce)
142+
err = crypt.WecomErrorByCode(code)
143+
if err != nil {
144+
return "", err
145+
}
146+
147+
return msg, nil
148+
}
149+
150+
func (w *wecom) StreamText(ctx context.Context, req VerifyReq) (string, error) {
151+
if req.OnlyVerify {
152+
return w.verify(req)
153+
}
154+
155+
logger := w.logger.WithContext(ctx)
156+
157+
decryptReq, err := w.decryptUserReq(ctx, req)
158+
if err != nil {
159+
logger.WithErr(err).Error("decrypt user req failed")
160+
return "", err
161+
}
162+
163+
logger = logger.With("req", decryptReq)
164+
165+
switch decryptReq.Msgtype {
166+
case "text":
167+
state := &streamState{
168+
question: decryptReq.Text.Content,
169+
mutex: sync.Mutex{},
170+
stream: strings.Builder{},
171+
Done: true,
172+
}
173+
_, loaded := w.cache.LoadOrStore(decryptReq.Msgid, state)
174+
if !loaded {
175+
stream, err := w.botCallback(ctx, BotReq{
176+
SessionID: "wecom_" + decryptReq.Msgid,
177+
Question: state.question,
178+
})
179+
if err != nil {
180+
logger.WithErr(err).Error("bot callback failed")
181+
state.Append("出错了,请稍后再试", true)
182+
} else {
183+
go func() {
184+
stream.Read(context.Background(), func(content string) {
185+
state.Append(content, false)
186+
})
187+
188+
state.Append("", true)
189+
}()
190+
}
191+
}
192+
193+
resp, err := w.EncryptUserRes(ctx, req.Nonce, weComStream{
194+
Id: decryptReq.Msgid,
195+
Finish: false,
196+
Content: "<think>正在查找相关信息...</think>",
197+
})
198+
if err != nil {
199+
logger.WithErr(err).Error("encrypt wecom user res failed")
200+
return "", err
201+
}
202+
203+
return resp, nil
204+
case "stream":
205+
stateI, ok := w.cache.Load(decryptReq.Stream.Id)
206+
if !ok {
207+
logger.Warn("msg not exist")
208+
resp, err := w.EncryptUserRes(ctx, req.Nonce, weComStream{
209+
Id: decryptReq.Stream.Id,
210+
Finish: true,
211+
Content: "出错了,请稍后再试",
212+
})
213+
if err != nil {
214+
logger.WithErr(err).Warn("encrypt user res failed")
215+
return "", err
216+
}
217+
return resp, nil
218+
}
219+
220+
state := stateI.(*streamState)
221+
content := state.Text()
222+
223+
if content == "" {
224+
content = "<think>正在查找相关信息...</think>"
225+
}
226+
227+
if state.Done {
228+
w.cache.Delete(decryptReq.Stream.Id)
229+
}
230+
231+
resp, err := w.EncryptUserRes(ctx, req.Nonce, weComStream{
232+
Id: decryptReq.Stream.Id,
233+
Finish: state.Done,
234+
Content: content,
235+
})
236+
if err != nil {
237+
logger.WithErr(err).Error("encrypt user res failed")
238+
return "", err
239+
}
240+
return resp, nil
241+
}
242+
243+
return "", errors.ErrUnsupported
244+
}
245+
246+
func (w *wecom) Start() error {
247+
return nil
248+
}
249+
250+
func (w *wecom) Stop() {}
251+
252+
func newWecom(cfg model.SystemChatConfig, callback BotCallback) (Bot, error) {
253+
return &wecom{
254+
logger: glog.Module("chat", "wecom"),
255+
cache: sync.Map{},
256+
cfg: cfg,
257+
botCallback: callback,
258+
}, nil
259+
}

0 commit comments

Comments
 (0)