Compare commits

...

10 Commits

Author SHA1 Message Date
Aibo117 5f287a06f2 chore: remove server binary again 2026-05-26 16:00:51 +08:00
Aibo117 ff391c31c1 fix: rename request-human route to handoff for Iris MVC compatibility
PostHandoff maps to POST /handoff correctly in Iris MVC auto-routing.
2026-05-26 15:59:56 +08:00
Aibo117 9fde44f11d chore: remove committed binary and add to gitignore 2026-05-26 15:55:12 +08:00
Aibo117 ec69ea88e3 fix: s.buildEventPayload method call 2026-05-26 15:54:53 +08:00
Aibo117 63dec1d8f1 feat: add manual "转人工" (request human) action for conversations
- Backend: RequestHuman service method changes status from AI_SERVING to PENDING
- Backend: POST /api/dashboard/conversation/request-human endpoint
- Frontend: requestHumanConversation API client
- Frontend: "转人工" button in conversation list dropdown and detail dialog
2026-05-26 15:46:09 +08:00
Aibo117 c5d0317bb6 test: add bootstrap SQL dump for test environment setup
Contains current AI configs, channel, knowledge base, and document
data that can be used to restore a working test environment.
2026-05-26 15:34:46 +08:00
Aibo117 fa9f64752b fix: reduce chunk size to prevent embedding context overflow
nomic-embed-text tokenizes Chinese characters differently from the
estimateTokenCount heuristic (3-5 real tokens per character vs 1
estimated). Large chunks overflow the 8192 token context. Reduce
TargetTokens/MaxTokens to keep chunks within limits.
2026-05-26 14:56:24 +08:00
Aibo117 be91feea63 fix: propagate document indexing errors to caller
Previously indexing failures were silently logged, returning success
even when the embedding model was not configured or Qdrant was down.
Now the error is returned to the frontend so the user sees it.
2026-05-26 14:33:13 +08:00
Aibo117 6c0944d6f2 fix: add form validation error feedback in agent edit dialog
- Add onFormError callback to show toast when validation fails
- This helps debug why the save button appears to do nothing
2026-05-26 14:08:14 +08:00
Aibo117 3c39e3b7e1 fix: use handleSubmit directly instead of form requestSubmit
Bypass potential React portal form resolution issues by calling
react-hook-form handleSubmit(onFormSubmit)() directly in the
button onClick handler.
2026-05-26 14:07:06 +08:00
11 changed files with 133 additions and 8 deletions
+1 -1
View File
@@ -12,4 +12,4 @@ node_modules/
.next/
*.tsbuildinfo
.superpowers
.superpowersserver
+17
View File
File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -38,9 +38,9 @@ const knowledgeCollectionName = "knowledge_chunks"
var Index = &index{
chunkConfig: ChunkingConfig{
Provider: string(enums.KnowledgeChunkProviderStructured),
TargetTokens: 300,
MaxTokens: 400,
OverlapTokens: 40,
TargetTokens: 100,
MaxTokens: 150,
OverlapTokens: 20,
EnableFallback: true,
},
registry: ragchunk.NewDefaultRegistry(),
@@ -185,6 +185,22 @@ func (c *ConversationController) PostTransfer() *web.JsonResult {
return web.JsonSuccess()
}
func (c *ConversationController) PostHandoff() *web.JsonResult {
operator, err := services.AuthService.RequirePermission(c.Ctx, constants.PermissionConversationAssign)
if err != nil {
return web.JsonError(err)
}
req := request.RequestHumanConversationRequest{}
if err := params.ReadJSON(c.Ctx, &req); err != nil {
return web.JsonError(err)
}
if err := services.ConversationService.RequestHuman(req.ConversationID, operator); err != nil {
return web.JsonError(err)
}
return web.JsonSuccess()
}
func (c *ConversationController) PostClose() *web.JsonResult {
operator, err := services.AuthService.RequirePermission(c.Ctx, constants.PermissionConversationClose)
if err != nil {
@@ -34,6 +34,10 @@ type TransferConversationRequest struct {
Reason string `json:"reason"`
}
type RequestHumanConversationRequest struct {
ConversationID int64 `json:"conversationId"`
}
type CloseConversationRequest struct {
ConversationID int64 `json:"conversationId"`
CloseReason string `json:"closeReason"`
+32
View File
@@ -374,6 +374,38 @@ func (s *conversationService) TryOffHoursHandoffByAI(conversationID int64, aiAge
return handled, err
}
func (s *conversationService) RequestHuman(conversationID int64, operator *dto.AuthPrincipal) error {
if operator == nil {
return errorsx.Unauthorized("未登录或登录已过期")
}
conversation := s.Get(conversationID)
if conversation == nil {
return errorsx.InvalidParam("会话不存在")
}
if conversation.Status != enums.IMConversationStatusAIServing {
return errorsx.InvalidParam("当前会话状态不允许转人工")
}
now := time.Now()
return sqls.WithTransaction(func(ctx *sqls.TxContext) error {
if err := repositories.ConversationRepository.Updates(ctx.Tx, conversationID, map[string]any{
"status": enums.IMConversationStatusPending,
"update_user_id": operator.UserID,
"update_user_name": operator.Username,
"updated_at": now,
}); err != nil {
return err
}
return ConversationEventLogService.CreateEvent(ctx, conversationID,
enums.IMEventTypeTransfer, enums.IMSenderTypeSystem, 0,
"管理员手动转人工",
s.buildEventPayload(map[string]any{
"operator": operator.Username,
"fromStatus": enums.IMConversationStatusAIServing,
"toStatus": enums.IMConversationStatusPending,
}))
})
}
func (s *conversationService) CloseConversation(conversationID int64, closeReason string, operator *dto.AuthPrincipal) error {
if operator == nil {
return errorsx.Unauthorized("未登录或登录已过期")
@@ -4,6 +4,7 @@ import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"log/slog"
"time"
@@ -111,7 +112,7 @@ func (s *knowledgeDocumentService) CreateKnowledgeDocument(req request.CreateKno
return nil, err
}
if err := rag.Index.IndexDocumentByID(context.Background(), item.ID); err != nil {
slog.Error("failed to index created knowledge document", "document_id", item.ID, "error", err)
return nil, fmt.Errorf("文档索引失败: %w", err)
}
item = s.Get(item.ID)
return item, nil
@@ -160,7 +161,7 @@ func (s *knowledgeDocumentService) UpdateKnowledgeDocument(req request.UpdateKno
}
if err := rag.Index.IndexDocumentByID(context.Background(), req.ID); err != nil {
slog.Error("failed to reindex updated knowledge document", "document_id", req.ID, "error", err)
return fmt.Errorf("文档重新索引失败: %w", err)
}
return nil
}
+10 -1
View File
@@ -266,6 +266,15 @@ function AgentEditDialogBody({
await onSubmit(buildPayload(values));
}
function onFormError(errors: object) {
const firstError = Object.values(errors).find(
(e: unknown) => (e as { message?: string })?.message
) as { message?: string } | undefined
if (firstError?.message) {
toast.error(firstError.message)
}
}
return (
<ProjectDialog
open={open}
@@ -284,7 +293,7 @@ function AgentEditDialogBody({
</Button>
<Button
type="button"
onClick={() => formRef.current?.requestSubmit()}
onClick={() => handleSubmit(onFormSubmit, onFormError)()}
disabled={saving || loading}
>
{saving ? "保存中..." : "保存"}
@@ -40,7 +40,8 @@ type ConversationDetailDialogProps = {
onLoadMoreMessages?: () => void | Promise<void>;
onOpenChange: (open: boolean) => void;
onOpenAssign: () => void;
onDispatch: () => Promise<void>;
onDispatch: () => Promise<void>
onRequestHuman?: () => Promise<void>;
onOpenTransfer: () => void;
onRead: () => Promise<void>;
onOpenClose: () => void;
@@ -272,6 +273,14 @@ export function ConversationDetailDialog({
: "暂无会话信息"}
</div>
<div className="flex flex-wrap gap-2">
<Button
variant="outline"
onClick={() => void onRequestHuman?.()}
disabled={saving || !currentConversation || currentConversation.status !== 1}
>
<MessageCircleMoreIcon />
{saving ? "处理中..." : "转人工"}
</Button>
<Button
variant="outline"
onClick={onOpenAssign}
@@ -52,6 +52,7 @@ import {
fetchConversations,
fetchTagsAll,
markConversationRead,
requestHumanConversation,
type AdminAgentProfile,
type AdminAgentTeam,
type AdminConversation,
@@ -610,6 +611,22 @@ export default function DashboardConversationsPage() {
}
}
async function handleRequestHuman(item: AdminConversation) {
setActionLoadingId(item.id)
try {
await requestHumanConversation(item.id)
toast.success(`已转人工:${item.customerName || `#${item.id}`}`)
await loadConversations()
if (detailItem?.id === item.id) {
await refreshDetail()
}
} catch (error) {
toast.error(error instanceof Error ? error.message : "转人工失败")
} finally {
setActionLoadingId(null)
}
}
async function handleDispatch(item: AdminConversation) {
setActionLoadingId(item.id)
try {
@@ -761,6 +778,13 @@ export default function DashboardConversationsPage() {
<MoreHorizontalIcon />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-44 min-w-44">
<DropdownMenuItem
onClick={() => void handleRequestHuman(item)}
disabled={actionLoadingId === item.id || item.status !== 1}
>
<MessageCircleMoreIcon />
{actionLoadingId === item.id ? "处理中..." : "转人工"}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => openAssign(item)}
disabled={actionLoadingId === item.id || item.status !== 2}
@@ -838,6 +862,12 @@ export default function DashboardConversationsPage() {
loadingMoreMessages={detailMessagesLoadingMore}
onLoadMoreMessages={loadMoreDetailMessages}
onOpenChange={handleDetailOpenChange}
onRequestHuman={async () => {
if (!detailItem) {
return
}
await handleRequestHuman(detailItem)
}}
onOpenAssign={() => {
if (!detailItem) {
return
+7
View File
@@ -833,6 +833,13 @@ export function assignConversation(
})
}
export function requestHumanConversation(conversationId: number) {
return request<void>("/api/dashboard/conversation/handoff", {
method: "POST",
body: JSON.stringify({ conversationId }),
})
}
export function dispatchConversation(conversationId: number) {
return request<void>("/api/dashboard/conversation/dispatch", {
method: "POST",