diff --git a/Dockerfile b/Dockerfile
index 1ccd9dafc..b36f1cdeb 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM jumpserver/koko-base:20250513_030103 AS stage-build
+FROM jumpserver/koko-base:20250512_022127 AS stage-build
WORKDIR /opt/koko
ARG TARGETARCH
diff --git a/pkg/httpd/chat.go b/pkg/httpd/chat.go
index fc2e6b6d1..3d5c4b80a 100644
--- a/pkg/httpd/chat.go
+++ b/pkg/httpd/chat.go
@@ -1,163 +1,259 @@
package httpd
import (
+ "context"
"encoding/json"
+ "fmt"
"github.com/jumpserver/koko/pkg/common"
+ "github.com/jumpserver/koko/pkg/i18n"
+ "github.com/jumpserver/koko/pkg/logger"
+ "github.com/jumpserver/koko/pkg/proxy"
+ "github.com/jumpserver/koko/pkg/session"
"github.com/sashabaranov/go-openai"
"sync"
"time"
"github.com/jumpserver/koko/pkg/jms-sdk-go/model"
- "github.com/jumpserver/koko/pkg/logger"
"github.com/jumpserver/koko/pkg/srvconn"
)
var _ Handler = (*chat)(nil)
type chat struct {
- ws *UserWebsocket
+ ws *UserWebsocket
+ term *model.TerminalConfig
- conversationMap sync.Map
-
- termConf *model.TerminalConfig
+ // conversationMap: map[conversationID]*AIConversation
+ conversations sync.Map
}
func (h *chat) Name() string {
return ChatName
}
-func (h *chat) CleanUp() {
- h.CleanConversationMap()
-}
+func (h *chat) CleanUp() { h.cleanupAll() }
func (h *chat) CheckValidation() error {
return nil
}
func (h *chat) HandleMessage(msg *Message) {
- conversationID := msg.Id
- conversation := &AIConversation{}
-
- if conversationID == "" {
- id := common.UUID()
- conversation = &AIConversation{
- Id: id,
- Prompt: msg.Prompt,
- HistoryRecords: make([]string, 0),
- InterruptCurrentChat: false,
- }
+ if msg.Interrupt {
+ h.interrupt(msg.Id)
+ return
+ }
- // T000 Currently a websocket connection only retains one conversation
- h.CleanConversationMap()
- h.conversationMap.Store(id, conversation)
- } else {
- c, ok := h.conversationMap.Load(conversationID)
- if !ok {
- logger.Errorf("Ws[%s] conversation %s not found", h.ws.Uuid, conversationID)
- h.sendErrorMessage(conversationID, "conversation not found")
- return
+ conv, err := h.getOrCreateConversation(msg)
+ if err != nil {
+ h.sendError(msg.Id, err.Error())
+ return
+ }
+ conv.Question = msg.Data
+ conv.NewDialogue = true
+
+ go h.runChat(conv)
+}
+
+func (h *chat) getOrCreateConversation(msg *Message) (*AIConversation, error) {
+ if msg.Id != "" {
+ if v, ok := h.conversations.Load(msg.Id); ok {
+ return v.(*AIConversation), nil
}
- conversation = c.(*AIConversation)
+ return nil, fmt.Errorf("conversation %s not found", msg.Id)
}
- if msg.Interrupt {
- conversation.InterruptCurrentChat = true
- return
+ jmsSrv, err := proxy.NewChatJMSServer(
+ h.ws.user.String(), h.ws.ClientIP(),
+ h.ws.user.ID, h.ws.langCode, h.ws.apiClient, h.term,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("create JMS server: %w", err)
}
- openAIParam := &OpenAIParam{
- AuthToken: h.termConf.GptApiKey,
- BaseURL: h.termConf.GptBaseUrl,
- Proxy: h.termConf.GptProxy,
- Model: h.termConf.GptModel,
- Prompt: conversation.Prompt,
+ sess := session.NewSession(jmsSrv.Session, h.sessionCallback)
+ session.AddSession(sess)
+
+ conv := &AIConversation{
+ Id: jmsSrv.Session.ID,
+ Prompt: msg.Prompt,
+ Model: msg.ChatModel,
+ Context: make([]QARecord, 0),
+ JMSServer: jmsSrv,
+ }
+ h.conversations.Store(jmsSrv.Session.ID, conv)
+ go h.Monitor(conv)
+ return conv, nil
+}
+
+func (h *chat) sessionCallback(task *model.TerminalTask) error {
+ if task.Name == model.TaskKillSession {
+ h.endConversation(task.Args, "close", "kill session")
+ return nil
}
- conversation.HistoryRecords = append(conversation.HistoryRecords, msg.Data)
- go h.chat(openAIParam, conversation)
+ return fmt.Errorf("unknown session task %s", task.Name)
}
-func (h *chat) chat(
- chatGPTParam *OpenAIParam, conversation *AIConversation,
-) string {
- doneCh := make(chan string)
- answerCh := make(chan string)
- defer close(doneCh)
- defer close(answerCh)
+func (h *chat) runChat(conv *AIConversation) {
+ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
+ defer cancel()
- c := srvconn.NewOpenAIClient(
- chatGPTParam.AuthToken,
- chatGPTParam.BaseURL,
- chatGPTParam.Proxy,
+ client := srvconn.NewOpenAIClient(
+ h.term.GptApiKey, h.term.GptBaseUrl, h.term.GptProxy,
)
- startIndex := len(conversation.HistoryRecords) - 15
- if startIndex < 0 {
- startIndex = 0
+ // Keep the last 8 contexts
+ if len(conv.Context) > 8 {
+ conv.Context = conv.Context[len(conv.Context)-8:]
}
- contents := conversation.HistoryRecords[startIndex:]
+ messages := buildChatMessages(conv)
- openAIConn := &srvconn.OpenAIConn{
- Id: conversation.Id,
- Client: c,
- Prompt: chatGPTParam.Prompt,
- Model: chatGPTParam.Model,
- Contents: contents,
+ chatModel := conv.Model
+ if conv.Model == "" {
+ chatModel = h.term.GptModel
+ }
+
+ conn := &srvconn.OpenAIConn{
+ Id: conv.Id,
+ Client: client,
+ Prompt: conv.Prompt,
+ Model: chatModel,
+ Question: conv.Question,
+ Context: messages,
+ AnswerCh: make(chan string),
+ DoneCh: make(chan string),
IsReasoning: false,
- AnswerCh: answerCh,
- DoneCh: doneCh,
- Type: h.termConf.ChatAIType,
+ Type: h.term.ChatAIType,
}
- go openAIConn.Chat(&conversation.InterruptCurrentChat)
- return h.processChatMessages(openAIConn)
+ // 启动 streaming
+ go conn.Chat(&conv.InterruptCurrentChat)
+
+ conv.JMSServer.Replay.WriteInput(conv.Question)
+
+ h.streamResponses(ctx, conv, conn)
+}
+
+func buildChatMessages(conv *AIConversation) []openai.ChatCompletionMessage {
+ msgs := make([]openai.ChatCompletionMessage, 0, len(conv.Context)*2)
+ for _, r := range conv.Context {
+ msgs = append(msgs,
+ openai.ChatCompletionMessage{Role: openai.ChatMessageRoleUser, Content: r.Question},
+ openai.ChatCompletionMessage{Role: openai.ChatMessageRoleAssistant, Content: r.Answer},
+ )
+ }
+ return msgs
}
-func (h *chat) processChatMessages(
- openAIConn *srvconn.OpenAIConn,
-) string {
- messageID := common.UUID()
- id := openAIConn.Id
+func (h *chat) streamResponses(
+ ctx context.Context, conv *AIConversation, conn *srvconn.OpenAIConn,
+) {
+ msgID := common.UUID()
for {
select {
- case answer := <-openAIConn.AnswerCh:
- h.sendSessionMessage(id, answer, messageID, "message", openAIConn.IsReasoning)
- case answer := <-openAIConn.DoneCh:
- h.sendSessionMessage(id, answer, messageID, "finish", false)
- return answer
+ case <-ctx.Done():
+ h.sendError(conv.Id, "chat timeout")
+ return
+ case ans := <-conn.AnswerCh:
+ h.sendMessage(conv.Id, msgID, ans, "message", conn.IsReasoning)
+ case ans := <-conn.DoneCh:
+ h.sendMessage(conv.Id, msgID, ans, "finish", false)
+ h.finalizeConversation(conv, ans)
+ return
}
}
}
-func (h *chat) sendSessionMessage(id, answer, messageID, messageType string, isReasoning bool) {
- message := ChatGPTMessage{
- Content: answer,
- ID: messageID,
+func (h *chat) finalizeConversation(conv *AIConversation, fullAnswer string) {
+ runes := []rune(fullAnswer)
+ snippet := fullAnswer
+ if len(runes) > 100 {
+ snippet = string(runes[:100])
+ }
+ conv.Context = append(conv.Context, QARecord{Question: conv.Question, Answer: snippet})
+
+ cmd := conv.JMSServer.GenerateCommandItem(h.ws.user.String(), conv.Question, fullAnswer)
+ go conv.JMSServer.CmdR.Record(cmd)
+ go conv.JMSServer.Replay.WriteOutput(fullAnswer)
+}
+
+func (h *chat) sendMessage(
+ convID, msgID, content, typ string, reasoning bool,
+) {
+ msg := ChatGPTMessage{
+ Content: content,
+ ID: msgID,
CreateTime: time.Now(),
- Type: messageType,
+ Type: typ,
Role: openai.ChatMessageRoleAssistant,
- IsReasoning: isReasoning,
+ IsReasoning: reasoning,
}
- data, _ := json.Marshal(message)
- msg := Message{
- Id: id,
- Type: "message",
- Data: string(data),
+ data, _ := json.Marshal(msg)
+ h.ws.SendMessage(&Message{Id: convID, Type: "message", Data: string(data)})
+}
+
+func (h *chat) sendError(convID, errMsg string) {
+ h.endConversation(convID, "error", errMsg)
+}
+
+func (h *chat) endConversation(convID, typ, msg string) {
+
+ defer func() {
+ if r := recover(); r != nil {
+ logger.Errorf("panic while sending message to session %s: %v", convID, r)
+ }
+ }()
+
+ if v, ok := h.conversations.Load(convID); ok {
+ if conv, ok2 := v.(*AIConversation); ok2 && conv.JMSServer != nil {
+ conv.JMSServer.Close(msg)
+ }
}
- h.ws.SendMessage(&msg)
+ h.conversations.Delete(convID)
+ h.ws.SendMessage(&Message{Id: convID, Type: typ, Data: msg})
}
-func (h *chat) sendErrorMessage(id, message string) {
- msg := Message{
- Id: id,
- Type: "error",
- Data: message,
+func (h *chat) interrupt(convID string) {
+ if v, ok := h.conversations.Load(convID); ok {
+ v.(*AIConversation).InterruptCurrentChat = true
}
- h.ws.SendMessage(&msg)
}
-func (h *chat) CleanConversationMap() {
- h.conversationMap.Range(func(key, value interface{}) bool {
- h.conversationMap.Delete(key)
+func (h *chat) cleanupAll() {
+ h.conversations.Range(func(key, _ interface{}) bool {
+ h.endConversation(key.(string), "close", "")
return true
})
}
+
+func (h *chat) Monitor(conv *AIConversation) {
+ lang := i18n.NewLang(h.ws.langCode)
+
+ lastActiveTime := time.Now()
+ maxIdleTime := time.Duration(h.term.MaxIdleTime) * time.Minute
+ MaxSessionTime := time.Now().Add(time.Duration(h.term.MaxSessionTime) * time.Hour)
+
+ for {
+ now := time.Now()
+ if MaxSessionTime.Before(now) {
+ msg := lang.T("Session max time reached, disconnect")
+ logger.Infof("Session[%s] max session time reached, disconnect", conv.Id)
+ h.endConversation(conv.Id, "close", msg)
+ return
+ }
+
+ outTime := lastActiveTime.Add(maxIdleTime)
+ if now.After(outTime) {
+ msg := fmt.Sprintf(lang.T("Connect idle more than %d minutes, disconnect"), h.term.MaxIdleTime)
+ logger.Infof("Session[%s] idle more than %d minutes, disconnect", conv.Id, h.term.MaxIdleTime)
+ h.endConversation(conv.Id, "close", msg)
+ return
+ }
+
+ if conv.NewDialogue {
+ lastActiveTime = time.Now()
+ conv.NewDialogue = false
+ }
+
+ time.Sleep(10 * time.Second)
+ }
+}
diff --git a/pkg/httpd/message.go b/pkg/httpd/message.go
index 9ed65854f..3c98fb2d4 100644
--- a/pkg/httpd/message.go
+++ b/pkg/httpd/message.go
@@ -1,6 +1,7 @@
package httpd
import (
+ "github.com/jumpserver/koko/pkg/proxy"
"time"
"github.com/jumpserver/koko/pkg/exchange"
@@ -18,6 +19,7 @@ type Message struct {
//Chat AI
Prompt string `json:"prompt"`
Interrupt bool `json:"interrupt"`
+ ChatModel string `json:"chat_model"`
//K8s
KubernetesId string `json:"k8s_id"`
@@ -163,11 +165,20 @@ type OpenAIParam struct {
Type string
}
+type QARecord struct {
+ Question string
+ Answer string
+}
+
type AIConversation struct {
Id string
Prompt string
- HistoryRecords []string
+ Question string
+ Model string
+ Context []QARecord
+ JMSServer *proxy.ChatJMSServer
InterruptCurrentChat bool
+ NewDialogue bool
}
type ChatGPTMessage struct {
diff --git a/pkg/httpd/webserver.go b/pkg/httpd/webserver.go
index 86ff6e280..ca494d1b4 100644
--- a/pkg/httpd/webserver.go
+++ b/pkg/httpd/webserver.go
@@ -158,9 +158,9 @@ func (s *Server) ChatAIWebsocket(ctx *gin.Context) {
}
userConn.handler = &chat{
- ws: userConn,
- conversationMap: sync.Map{},
- termConf: &termConf,
+ ws: userConn,
+ conversations: sync.Map{},
+ term: &termConf,
}
s.broadCaster.EnterUserWebsocket(userConn)
defer s.broadCaster.LeaveUserWebsocket(userConn)
diff --git a/pkg/jms-sdk-go/model/account.go b/pkg/jms-sdk-go/model/account.go
index 0e9e893ed..fbedc194b 100644
--- a/pkg/jms-sdk-go/model/account.go
+++ b/pkg/jms-sdk-go/model/account.go
@@ -55,6 +55,18 @@ type AccountDetail struct {
Privileged bool `json:"privileged"`
}
+type AssetChat struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+}
+
+type AccountChatDetail struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Username string `json:"username"`
+ Asset AssetChat `json:"asset"`
+}
+
type PermAccount struct {
Name string `json:"name"`
Username string `json:"username"`
diff --git a/pkg/jms-sdk-go/service/jms_asset.go b/pkg/jms-sdk-go/service/jms_asset.go
index fc15ce8cf..39d57c430 100644
--- a/pkg/jms-sdk-go/service/jms_asset.go
+++ b/pkg/jms-sdk-go/service/jms_asset.go
@@ -23,3 +23,9 @@ func (s *JMService) GetAccountSecretById(accountId string) (res model.AccountDet
_, err = s.authClient.Get(url, &res)
return
}
+
+func (s *JMService) GetAccountChat() (res model.AccountChatDetail, err error) {
+ url := fmt.Sprintf(AccountChatURL)
+ _, err = s.authClient.Get(url, &res)
+ return
+}
diff --git a/pkg/jms-sdk-go/service/url.go b/pkg/jms-sdk-go/service/url.go
index 893e40d5b..39051e826 100644
--- a/pkg/jms-sdk-go/service/url.go
+++ b/pkg/jms-sdk-go/service/url.go
@@ -77,6 +77,7 @@ const (
UserPermsAssetAccountsURL = "/api/v1/perms/users/%s/assets/%s/"
AccountSecretURL = "/api/v1/assets/account-secrets/%s/"
+ AccountChatURL = "/api/v1/accounts/accounts/chat/"
UserPermsAssetsURL = "/api/v1/perms/users/%s/assets/"
AssetLoginConfirmURL = "/api/v1/acls/login-asset/check/"
diff --git a/pkg/proxy/chat.go b/pkg/proxy/chat.go
new file mode 100644
index 000000000..0e845f206
--- /dev/null
+++ b/pkg/proxy/chat.go
@@ -0,0 +1,170 @@
+package proxy
+
+import (
+ "fmt"
+ "github.com/jumpserver/koko/pkg/common"
+ modelCommon "github.com/jumpserver/koko/pkg/jms-sdk-go/common"
+ "github.com/jumpserver/koko/pkg/jms-sdk-go/model"
+ "github.com/jumpserver/koko/pkg/jms-sdk-go/service"
+ "github.com/jumpserver/koko/pkg/logger"
+ "github.com/jumpserver/koko/pkg/session"
+ "strings"
+ "time"
+)
+
+type ChatReplyRecorder struct {
+ *ReplyRecorder
+}
+
+func (rh *ChatReplyRecorder) WriteInput(inputStr string) {
+ currentTime := time.Now()
+ formattedTime := currentTime.Format("2006-01-02 15:04:05")
+ inputStr = fmt.Sprintf("[%s]#: %s", formattedTime, inputStr)
+ rh.Record([]byte(inputStr))
+}
+
+func (rh *ChatReplyRecorder) WriteOutput(outputStr string) {
+ wrappedText := rh.wrapText(outputStr)
+ outputStr = "\r\n" + wrappedText + "\r\n"
+ rh.Record([]byte(outputStr))
+
+}
+
+func (rh *ChatReplyRecorder) wrapText(text string) string {
+ var wrappedTextBuilder strings.Builder
+ words := strings.Fields(text)
+ currentLineLength := 0
+
+ for _, word := range words {
+ wordLength := len(word)
+
+ if currentLineLength+wordLength > rh.Writer.Width {
+ wrappedTextBuilder.WriteString("\r\n" + word + " ")
+ currentLineLength = wordLength + 1
+ } else {
+ wrappedTextBuilder.WriteString(word + " ")
+ currentLineLength += wordLength + 1
+ }
+ }
+
+ return wrappedTextBuilder.String()
+}
+
+func NewChatJMSServer(
+ user, ip, userID, langCode string,
+ jmsService *service.JMService, conf *model.TerminalConfig) (*ChatJMSServer, error) {
+ accountInfo, err := jmsService.GetAccountChat()
+ if err != nil {
+ logger.Errorf("Get account chat info error: %s", err)
+ return nil, err
+ }
+
+ id := common.UUID()
+
+ apiSession := &model.Session{
+ ID: id,
+ User: user,
+ LoginFrom: model.LoginFromWeb,
+ RemoteAddr: ip,
+ Protocol: model.ActionALL,
+ Asset: accountInfo.Asset.Name,
+ Account: accountInfo.Name,
+ AccountID: accountInfo.ID,
+ AssetID: accountInfo.Asset.ID,
+ UserID: userID,
+ OrgID: "00000000-0000-0000-0000-000000000004",
+ Type: model.NORMALType,
+ LangCode: langCode,
+ DateStart: modelCommon.NewNowUTCTime(),
+ }
+
+ _, err2 := jmsService.CreateSession(*apiSession)
+ if err2 != nil {
+ return nil, err2
+ }
+
+ chat := &ChatJMSServer{
+ JmsService: jmsService,
+ Session: apiSession,
+ Conf: conf,
+ }
+
+ chat.CmdR = chat.GetCommandRecorder()
+ chat.Replay = chat.GetReplayRecorder()
+
+ if err1 := jmsService.RecordSessionLifecycleLog(id, model.AssetConnectSuccess,
+ model.EmptyLifecycleLog); err1 != nil {
+ logger.Errorf("Record session activity log err: %s", err1)
+ }
+
+ return chat, nil
+}
+
+type ChatJMSServer struct {
+ JmsService *service.JMService
+ Session *model.Session
+ CmdR *CommandRecorder
+ Replay *ChatReplyRecorder
+ Conf *model.TerminalConfig
+}
+
+func (s *ChatJMSServer) GenerateCommandItem(user, input, output string) *model.Command {
+ createdDate := time.Now()
+ return &model.Command{
+ SessionID: s.Session.ID,
+ OrgID: s.Session.OrgID,
+ Input: input,
+ Output: output,
+ User: user,
+ Server: s.Session.Asset,
+ Account: s.Session.Account,
+ Timestamp: createdDate.Unix(),
+ RiskLevel: model.NormalLevel,
+ DateCreated: createdDate.UTC(),
+ }
+}
+
+func (s *ChatJMSServer) GetReplayRecorder() *ChatReplyRecorder {
+ info := &ReplyInfo{
+ Width: 200,
+ Height: 200,
+ TimeStamp: time.Now(),
+ }
+ recorder, err := NewReplayRecord(s.Session.ID, s.JmsService,
+ NewReplayStorage(s.JmsService, s.Conf),
+ info)
+ if err != nil {
+ logger.Error(err)
+ }
+
+ return &ChatReplyRecorder{recorder}
+}
+
+func (s *ChatJMSServer) GetCommandRecorder() *CommandRecorder {
+ cmdR := CommandRecorder{
+ sessionID: s.Session.ID,
+ storage: NewCommandStorage(s.JmsService, s.Conf),
+ queue: make(chan *model.Command, 10),
+ closed: make(chan struct{}),
+ jmsService: s.JmsService,
+ }
+ go cmdR.record()
+ return &cmdR
+}
+
+func (s *ChatJMSServer) Close(msg string) {
+ session.RemoveSessionById(s.Session.ID)
+ if err := s.JmsService.SessionFinished(s.Session.ID, modelCommon.NewNowUTCTime()); err != nil {
+ logger.Errorf("finish session %s: %v", s.Session.ID, err)
+ }
+
+ s.CmdR.End()
+ s.Replay.End()
+
+ logObj := model.SessionLifecycleLog{Reason: msg, User: s.Session.User}
+ err := s.JmsService.RecordSessionLifecycleLog(s.Session.ID, model.AssetConnectFinished, logObj)
+ if err != nil {
+ logger.Errorf("record session lifecycle log %s: %v", s.Session.ID, err)
+ return
+ }
+}
diff --git a/pkg/srvconn/conn_openai.go b/pkg/srvconn/conn_openai.go
index 9d639a6a9..50b1e5ad6 100644
--- a/pkg/srvconn/conn_openai.go
+++ b/pkg/srvconn/conn_openai.go
@@ -93,7 +93,8 @@ type OpenAIConn struct {
Client *openai.Client
Model string
Prompt string
- Contents []string
+ Question string
+ Context []openai.ChatCompletionMessage
IsReasoning bool
AnswerCh chan string
DoneCh chan string
@@ -102,11 +103,10 @@ type OpenAIConn struct {
func (conn *OpenAIConn) Chat(interruptCurrentChat *bool) {
ctx := context.Background()
- var messages []openai.ChatCompletionMessage
- messages = append(messages, openai.ChatCompletionMessage{
+ messages := append(conn.Context, openai.ChatCompletionMessage{
Role: openai.ChatMessageRoleUser,
- Content: strings.Join(conn.Contents, "\n"),
+ Content: conn.Question,
})
systemPrompt := conn.Prompt
@@ -182,6 +182,10 @@ func (conn *OpenAIConn) Chat(interruptCurrentChat *bool) {
newContent = response.Choices[0].Delta.Content
}
+ if newContent == "" {
+ continue
+ }
+
content += newContent
conn.AnswerCh <- content
}
diff --git a/ui/components.d.ts b/ui/components.d.ts
index bbd2dab51..d0dfc84f8 100644
--- a/ui/components.d.ts
+++ b/ui/components.d.ts
@@ -16,6 +16,7 @@ declare module 'vue' {
Keyboard: typeof import('./src/components/Drawer/components/Keyboard/index.vue')['default']
Logo: typeof import('./src/components/Kubernetes/Sidebar/components/Logo/index.vue')['default']
MainContent: typeof import('./src/components/Kubernetes/MainContent/index.vue')['default']
+ NAvatar: typeof import('naive-ui')['NAvatar']
NBreadcrumb: typeof import('naive-ui')['NBreadcrumb']
NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem']
NButton: typeof import('naive-ui')['NButton']
@@ -66,6 +67,7 @@ declare module 'vue' {
NTag: typeof import('naive-ui')['NTag']
NText: typeof import('naive-ui')['NText']
NThing: typeof import('naive-ui')['NThing']
+ NTime: typeof import('naive-ui')['NTime']
NTooltip: typeof import('naive-ui')['NTooltip']
NTree: typeof import('naive-ui')['NTree']
NUpload: typeof import('naive-ui')['NUpload']
diff --git a/ui/package.json b/ui/package.json
index ef75a076e..4905a818f 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -23,7 +23,7 @@
"clipboard-polyfill": "^4.1.0",
"dayjs": "^1.11.13",
"loglevel": "^1.9.1",
- "lucide-vue-next": "^0.487.0",
+ "lucide-vue-next": "^0.507.0",
"mitt": "^3.0.1",
"naive-ui": "^2.39.0",
"nora-zmodemjs": "^1.1.1",
@@ -41,6 +41,7 @@
"xterm-theme": "^1.1.0"
},
"devDependencies": {
+ "@duskmoon/vue3-typed-js": "^0.0.4",
"@eslint/js": "^9.23.0",
"@types/node": "^20.14.11",
"@types/sortablejs": "^1.15.0",
diff --git a/ui/public/icons/help.svg b/ui/public/icons/help.svg
deleted file mode 100644
index e5bdc1fbb..000000000
--- a/ui/public/icons/help.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/ui/public/icons/k8s.svg b/ui/public/icons/k8s.svg
deleted file mode 100644
index 4a4b34d91..000000000
--- a/ui/public/icons/k8s.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/ui/public/icons/logo.svg b/ui/public/icons/logo.svg
new file mode 100755
index 000000000..eb25227d1
--- /dev/null
+++ b/ui/public/icons/logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/public/icons/organize.svg b/ui/public/icons/organize.svg
deleted file mode 100644
index 0d78983d8..000000000
--- a/ui/public/icons/organize.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/ui/public/icons/setting.svg b/ui/public/icons/setting.svg
deleted file mode 100644
index a8d642278..000000000
--- a/ui/public/icons/setting.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/ui/public/icons/split.svg b/ui/public/icons/split.svg
deleted file mode 100644
index 2fdae88cb..000000000
--- a/ui/public/icons/split.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/ui/public/icons/tree.svg b/ui/public/icons/tree.svg
deleted file mode 100644
index 19b13eb69..000000000
--- a/ui/public/icons/tree.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/ui/public/icons/user.svg b/ui/public/icons/user.svg
deleted file mode 100644
index 58a1fb7af..000000000
--- a/ui/public/icons/user.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/ui/public/images/ChatGPT-Logo.png b/ui/public/images/ChatGPT-Logo.png
new file mode 100644
index 000000000..71c600e9a
Binary files /dev/null and b/ui/public/images/ChatGPT-Logo.png differ
diff --git a/ui/public/images/DeepSeek_Logo.webp b/ui/public/images/DeepSeek_Logo.webp
new file mode 100644
index 000000000..a21641385
Binary files /dev/null and b/ui/public/images/DeepSeek_Logo.webp differ
diff --git a/ui/src/enum/index.ts b/ui/src/enum/index.ts
index bd2bd8d88..086a3aadf 100644
--- a/ui/src/enum/index.ts
+++ b/ui/src/enum/index.ts
@@ -29,6 +29,7 @@ export enum MESSAGE_TYPE {
CLOSE = 'CLOSE',
ERROR = 'ERROR',
CONNECT = 'CONNECT',
+ MESSAGE = 'message',
TERMINAL_SHARE = 'TERMINAL_SHARE',
TERMINAL_ERROR = 'TERMINAL_ERROR',
MESSAGE_NOTIFY = 'MESSAGE_NOTIFY',
diff --git a/ui/src/hooks/helper/index.ts b/ui/src/hooks/helper/index.ts
index 6b99888a4..328edb554 100644
--- a/ui/src/hooks/helper/index.ts
+++ b/ui/src/hooks/helper/index.ts
@@ -306,6 +306,10 @@ export const generateWsURL = () => {
connectURL = BASE_WS_URL + '/koko/ws/terminal/?' + requireParams.toString();
break;
}
+ case 'Chat':
+ // connectURL = BASE_WS_URL + `/koko/ws/chat/system`
+ connectURL = 'ws://localhost:5050' + `/koko/ws/chat/system/`
+ break;
default: {
connectURL = urlParams ? `${BASE_WS_URL}/koko/ws/terminal/?${urlParams.toString()}` : '';
}
diff --git a/ui/src/hooks/useChat.ts b/ui/src/hooks/useChat.ts
new file mode 100644
index 000000000..945c3be21
--- /dev/null
+++ b/ui/src/hooks/useChat.ts
@@ -0,0 +1,69 @@
+import { ref } from 'vue';
+import { MessageType } from '@/enum';
+import { useMessage } from 'naive-ui';
+import { useWebSocket } from '@vueuse/core';
+import { generateWsURL } from '@/hooks/helper';
+
+import type { ChatSendMessage } from '@/types/modules/chat.type';
+
+export const useChat = () => {
+ const message = useMessage();
+ const socket = ref();
+
+ const socketOnMessage = (message: MessageEvent) => {
+ let data = '';
+ const messageData = JSON.parse(message.data);
+
+ if (typeof messageData.data === 'string') {
+ data = JSON.parse(messageData.data);
+ }
+
+ switch (messageData.type) {
+ case MessageType.CONNECT:
+ // console.log(data);
+ break;
+ case MessageType.MESSAGE:
+ console.log(data);
+ break;
+ }
+ };
+ const socketClose = () => {
+ message.error('Socket connection has been closed');
+ };
+ const socketError = () => {
+ message.error('Socket connection has been error');
+ };
+ const socketOpen = () => {
+ // TODO 发送心跳
+ };
+
+ const sendChatMessage = (message: ChatSendMessage) => {
+ socket.value?.send(JSON.stringify(message));
+ };
+
+ const createChatSocket = () => {
+ const url = generateWsURL();
+
+ const { ws } = useWebSocket(url);
+
+ if (!ws.value) {
+ return;
+ }
+
+ socket.value = ws.value;
+
+ ws.value.onopen = socketOpen;
+ ws.value.onclose = socketClose;
+ ws.value.onerror = socketError;
+ ws.value.onmessage = socketOnMessage;
+
+ return {
+ socket: socket.value
+ };
+ };
+
+ return {
+ sendChatMessage,
+ createChatSocket
+ };
+};
diff --git a/ui/src/index.css b/ui/src/index.css
index 3d552a61f..2534f1b56 100644
--- a/ui/src/index.css
+++ b/ui/src/index.css
@@ -1,2 +1,9 @@
@import "tailwindcss";
+.icon-hover-primary {
+ @apply cursor-pointer hover:text-[#16987D] focus:outline-none transition-all duration-300;
+}
+
+.icon-hover-danger {
+ @apply cursor-pointer hover:text-[#ff0000] focus:outline-none transition-all duration-300;
+}
diff --git a/ui/src/overrides.ts b/ui/src/overrides.ts
index 707313c2a..30a5dedbc 100644
--- a/ui/src/overrides.ts
+++ b/ui/src/overrides.ts
@@ -126,7 +126,7 @@ export const themeOverrides: GlobalThemeOverrides = {
},
Layout: {
color: 'rgba(0, 0, 0, 1)',
- siderColor: 'rgba(0, 0, 0, 1)',
+ siderColor: 'rgba(255, 255, 255, 0.09)',
headerColor: 'rgba(0, 0, 0, 1)'
}
};
diff --git a/ui/src/store/modules/chat.ts b/ui/src/store/modules/chat.ts
new file mode 100644
index 000000000..c776c81f1
--- /dev/null
+++ b/ui/src/store/modules/chat.ts
@@ -0,0 +1,8 @@
+import { defineStore } from 'pinia'
+
+export const useChatStore = defineStore('chat-store', {
+ state: () => ({}),
+ actions: {
+
+ }
+})
\ No newline at end of file
diff --git a/ui/src/store/modules/useChat.ts b/ui/src/store/modules/useChat.ts
new file mode 100644
index 000000000..a1a82e2e1
--- /dev/null
+++ b/ui/src/store/modules/useChat.ts
@@ -0,0 +1,27 @@
+import { defineStore } from 'pinia';
+import type { ChatState, ChatMessage } from '@/types/modules/chat.type';
+
+type Chatitem = {
+ chatItem: Map;
+};
+
+export const useChatStore = defineStore('chat', {
+ state: (): Chatitem => ({
+ chatItem: new Map()
+ }),
+ actions: {
+ addChatItem(id: string, chatState: ChatState) {
+ this.chatItem.set(id, chatState);
+ },
+ removeChatItem(id: string) {
+ this.chatItem.delete(id);
+ },
+ addMessageContext(id: string, message: ChatMessage) {
+ const chatState = this.chatItem.get(id);
+
+ if (chatState) {
+ chatState.messages.push(message);
+ }
+ }
+ }
+});
diff --git a/ui/src/types/modules/chat.type.ts b/ui/src/types/modules/chat.type.ts
new file mode 100644
index 000000000..955d405f0
--- /dev/null
+++ b/ui/src/types/modules/chat.type.ts
@@ -0,0 +1,55 @@
+// 定义会话消息类型
+export type ChatMessage = ChatSendMessage | ChatReceiveMessage;
+
+// 定义发送消息类型
+export interface ChatSendMessage {
+ data: string;
+
+ id: string;
+
+ prompt: string;
+}
+
+// 定义接收消息类型
+export interface ChatReceiveMessage {
+ chat_model: string;
+
+ data: string;
+
+ id: string;
+
+ interrupt: boolean;
+
+ prompt: string;
+
+ type: string;
+}
+
+// 定义会话状态类型
+export interface ChatState {
+ // 会话角色
+ prompt: string;
+
+ // 会话消息
+ // 数组的奇数项(index % 2 === 1)为收到的消息(ChatReceiveMessage)
+ // 数组的偶数项(index % 2 === 0)为发出去的消息(ChatSendMessage)
+ messages: ChatMessage[];
+}
+
+// 侧边栏
+export interface ChatSider {
+ time_stamp: string;
+
+ chat_items: {
+ id: string;
+
+ chat_title: string;
+ };
+}
+
+// 角色类型
+export interface RoleType {
+ content: string;
+
+ name: string;
+}
diff --git a/ui/src/views/chat/components/Content/index.vue b/ui/src/views/chat/components/Content/index.vue
new file mode 100644
index 000000000..5603bce90
--- /dev/null
+++ b/ui/src/views/chat/components/Content/index.vue
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+ 货币
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 货币
+
+
+ 货币是为了提高交易效率而用于交换的中介商品。货币有多种形式,如贝壳粮食等自然物、金属纸张等加工品、银行卡信用卡等磁条卡、移动支付加密货币等APP。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/views/chat/components/Conversation/index.vue b/ui/src/views/chat/components/Conversation/index.vue
new file mode 100644
index 000000000..7c0761a39
--- /dev/null
+++ b/ui/src/views/chat/components/Conversation/index.vue
@@ -0,0 +1,70 @@
+
+
+
+ 今天
+
+
+
+
+
+ 随便聊聊
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 昨天
+
+
+
+
+
+ 随便聊聊
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/views/chat/components/Conversation/optionRender.tsx b/ui/src/views/chat/components/Conversation/optionRender.tsx
new file mode 100644
index 000000000..544ea6332
--- /dev/null
+++ b/ui/src/views/chat/components/Conversation/optionRender.tsx
@@ -0,0 +1,72 @@
+import { useI18n } from 'vue-i18n';
+import { NSpace, NText } from 'naive-ui';
+import { SquareArrowOutUpRight, PencilLine, Trash2 } from 'lucide-vue-next';
+
+import type { SelectOption } from 'naive-ui';
+import type { FunctionalComponent } from 'vue';
+import type { LucideProps } from 'lucide-vue-next';
+
+interface OptionItem {
+ value: string;
+ label: string;
+ textColor?: string;
+ iconColor?: string;
+ click: (chatId: string) => void;
+ icon: FunctionalComponent;
+}
+
+type EmitsType = {
+ (e: 'chat-share', shareId: string): void;
+ (e: 'chat-rename', shareId: string): void;
+ (e: 'chat-delete', shareId: string): void;
+};
+
+export const OptionRender = (emits: EmitsType): SelectOption[] => {
+ const { t } = useI18n();
+
+ const optionItems: OptionItem[] = [
+ {
+ value: 'share',
+ icon: SquareArrowOutUpRight,
+ label: t('Share'),
+ iconColor: 'white',
+ click: (chatId: string) => {
+ emits('chat-share', chatId);
+ }
+ },
+ {
+ value: 'rename',
+ icon: PencilLine,
+ label: t('Rename'),
+ iconColor: 'white',
+ click: (chatId: string) => {
+ emits('chat-rename', chatId);
+ }
+ },
+ {
+ value: 'delete',
+ icon: Trash2,
+ label: t('Delete'),
+ iconColor: '#fb2c36',
+ textColor: '!text-red-500',
+ click: (chatId: string) => {
+ emits('chat-delete', chatId);
+ }
+ }
+ ];
+
+ const commonClass = 'px-4 py-2 w-30 hover:bg-[#ffffff1A] cursor-pointer transition-all duration-300';
+
+ return optionItems.map(item => ({
+ value: item.value,
+ render: () => (
+ item.click(item.value)}>
+ {item.icon && }
+
+
+ {item.label}
+
+
+ )
+ }));
+};
diff --git a/ui/src/views/chat/components/Header/index.vue b/ui/src/views/chat/components/Header/index.vue
new file mode 100644
index 000000000..a62d03f3a
--- /dev/null
+++ b/ui/src/views/chat/components/Header/index.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+ GPT-4
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/src/views/chat/components/InputArea/index.vue b/ui/src/views/chat/components/InputArea/index.vue
new file mode 100644
index 000000000..9d0ab6a58
--- /dev/null
+++ b/ui/src/views/chat/components/InputArea/index.vue
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 发送
+
+
+
+
+
diff --git a/ui/src/views/chat/components/Sider/index.vue b/ui/src/views/chat/components/Sider/index.vue
new file mode 100644
index 000000000..6e597e062
--- /dev/null
+++ b/ui/src/views/chat/components/Sider/index.vue
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/views/chat/components/Welcome/index.vue b/ui/src/views/chat/components/Welcome/index.vue
new file mode 100644
index 000000000..fcd0ec290
--- /dev/null
+++ b/ui/src/views/chat/components/Welcome/index.vue
@@ -0,0 +1,14 @@
+
+
+
+ 散财消灾
+
+
+
+
+
diff --git a/ui/src/views/chat/index.vue b/ui/src/views/chat/index.vue
new file mode 100644
index 000000000..d3f3e101e
--- /dev/null
+++ b/ui/src/views/chat/index.vue
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/yarn.lock b/ui/yarn.lock
index e7137c403..d44007d60 100644
--- a/ui/yarn.lock
+++ b/ui/yarn.lock
@@ -1145,6 +1145,24 @@
muggle-string "^0.4.1"
path-browserify "^1.0.1"
+"@vue/reactivity-transform@3.2.47":
+ version "3.2.47"
+ resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.47.tgz#e45df4d06370f8abf29081a16afd25cffba6d84e"
+ integrity sha512-m8lGXw8rdnPVVIdIFhf0LeQ/ixyHkH5plYuS83yop5n7ggVJU+z5v0zecwEnX7fa7HNLBhh2qngJJkxpwEEmYA==
+ dependencies:
+ "@babel/parser" "^7.16.4"
+ "@vue/compiler-core" "3.2.47"
+ "@vue/shared" "3.2.47"
+ estree-walker "^2.0.2"
+ magic-string "^0.25.7"
+
+"@vue/reactivity@3.2.47":
+ version "3.2.47"
+ resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.47.tgz#1d6399074eadfc3ed35c727e2fd707d6881140b6"
+ integrity sha512-7khqQ/75oyyg+N/e+iwV6lpy1f5wq759NdlS1fpAhFXa8VeAIKGgk2E/C4VF59lx5b+Ezs5fpp/5WsRYXQiKxQ==
+ dependencies:
+ "@vue/shared" "3.2.47"
+
"@vue/reactivity@3.4.38":
version "3.4.38"
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.4.38.tgz#ec2d549f4b831cd03d0baabf7d77e840b8536000"
@@ -1298,11 +1316,6 @@ binary-extensions@^2.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
-birpc@^2.3.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/birpc/-/birpc-2.3.0.tgz#e5a402dc785ef952a2383ef3cfc075e0842f3e8c"
- integrity sha512-ijbtkn/F3Pvzb6jHypHRyve2QApOCZDR25D/VnkY2G/lBNcXCTsnsCxgY4k4PkVB7zfwzYbY3O9Lcqe3xufS5g==
-
boolbase@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
@@ -1410,13 +1423,6 @@ convert-source-map@^2.0.0:
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
-copy-anything@^3.0.2:
- version "3.0.5"
- resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-3.0.5.tgz#2d92dce8c498f790fa7ad16b01a1ae5a45b020a0"
- integrity sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==
- dependencies:
- is-what "^4.1.8"
-
crc-32@^1.1.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff"
@@ -1817,11 +1823,6 @@ highlight.js@^11.8.0:
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.10.0.tgz#6e3600dc4b33d6dc23d5bd94fbf72405f5892b92"
integrity sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==
-hookable@^5.5.3:
- version "5.5.3"
- resolved "https://registry.yarnpkg.com/hookable/-/hookable-5.5.3.tgz#6cfc358984a1ef991e2518cb9ed4a778bbd3215d"
- integrity sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==
-
ignore@^5.2.0, ignore@^5.3.1:
version "5.3.2"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
@@ -1869,11 +1870,6 @@ is-number@^7.0.0:
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
-is-what@^4.1.8:
- version "4.1.16"
- resolved "https://registry.yarnpkg.com/is-what/-/is-what-4.1.16.tgz#1ad860a19da8b4895ad5495da3182ce2acdd7a6f"
- integrity sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==
-
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
@@ -2055,10 +2051,10 @@ lru-cache@^5.1.1:
dependencies:
yallist "^3.0.2"
-lucide-vue-next@^0.487.0:
- version "0.487.0"
- resolved "https://registry.yarnpkg.com/lucide-vue-next/-/lucide-vue-next-0.487.0.tgz#18d9e6b21c37c823d1c4879203b323dd97ada5f0"
- integrity sha512-ilVgu9EHkfId7WSjmoPkzp13cuzpSGO5J16AmltjRHFoj6MlCBGY8BzsBU/ISKVlDheUxM+MsIYjNo/1hSEa6w==
+lucide-vue-next@^0.507.0:
+ version "0.507.0"
+ resolved "https://registry.yarnpkg.com/lucide-vue-next/-/lucide-vue-next-0.507.0.tgz#bd2d9a5f85550875fb6f85082dd108c46a1870fd"
+ integrity sha512-n0AZRmez4xq5vu5ZOfnrhC5wKBpu5tpwDHA6Ue2+sKyJv3USAPxqIerXLbdYbkHRW1tVZ3U9GE1MACsu6X/Z7Q==
magic-string@*, magic-string@^0.30.10, magic-string@^0.30.11:
version "0.30.11"
@@ -2246,11 +2242,6 @@ pathe@^1.1.2:
resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec"
integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==
-perfect-debounce@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz#9c2e8bc30b169cc984a58b7d5b28049839591d2a"
- integrity sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==
-
picocolors@^1.0.0, picocolors@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1"
@@ -2266,12 +2257,13 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
-pinia@^3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/pinia/-/pinia-3.0.2.tgz#0616c2e1b39915f253c7626db3c81b7cdad695da"
- integrity sha512-sH2JK3wNY809JOeiiURUR0wehJ9/gd9qFN2Y828jCbxEzKEmEt0pzCXwqiSTfuRsK9vQsOflSdnbdBOGrhtn+g==
+pinia@^2.1.7:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/pinia/-/pinia-2.2.2.tgz#dcf576c9a778187d1542c5e6a9f8b8cd5b6aea14"
+ integrity sha512-ja2XqFWZC36mupU4z1ZzxeTApV7DOw44cV4dhQ9sGwun+N89v/XP7+j7q6TanS1u1tdbK4r+1BUx7heMaIdagA==
dependencies:
- "@vue/devtools-api" "^7.7.2"
+ "@vue/devtools-api" "^6.6.3"
+ vue-demi "^0.14.10"
pkg-types@^1.0.3, pkg-types@^1.1.1:
version "1.1.3"
@@ -2659,7 +2651,7 @@ vscode-uri@^3.0.8:
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f"
integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==
-vue-demi@>=0.14.8:
+vue-demi@>=0.14.8, vue-demi@^0.14.10:
version "0.14.10"
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.10.tgz#afc78de3d6f9e11bf78c55e8510ee12814522f04"
integrity sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==