@liujitcn/kratos-admin-system 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -0
- package/dist/package/declarations/index.d.ts +1 -0
- package/dist/package/declarations/src/ai.d.ts +10 -0
- package/dist/package/declarations/src/api/base/ai_message.d.ts +27 -0
- package/dist/package/declarations/src/api/base/ai_session.d.ts +17 -0
- package/dist/package/declarations/src/api/base/ai_tool.d.ts +7 -0
- package/dist/package/declarations/src/api/base/oauth.d.ts +13 -0
- package/dist/package/declarations/src/api/base/sse.d.ts +31 -0
- package/dist/package/declarations/src/api/system/auth.d.ts +17 -0
- package/dist/package/declarations/src/api/system/base_api.d.ts +22 -0
- package/dist/package/declarations/src/api/system/base_area.d.ts +19 -0
- package/dist/package/declarations/src/api/system/base_config.d.ts +20 -0
- package/dist/package/declarations/src/api/system/base_dept.d.ts +21 -0
- package/dist/package/declarations/src/api/system/base_dict.d.ts +32 -0
- package/dist/package/declarations/src/api/system/base_job.d.ts +28 -0
- package/dist/package/declarations/src/api/system/base_log.d.ts +9 -0
- package/dist/package/declarations/src/api/system/base_menu.d.ts +21 -0
- package/dist/package/declarations/src/api/system/base_migration.d.ts +10 -0
- package/dist/package/declarations/src/api/system/base_post.d.ts +21 -0
- package/dist/package/declarations/src/api/system/base_role.d.ts +23 -0
- package/dist/package/declarations/src/api/system/base_tenant.d.ts +21 -0
- package/dist/package/declarations/src/api/system/base_user.d.ts +23 -0
- package/dist/package/declarations/src/api/system/code_gen.d.ts +14 -0
- package/dist/package/declarations/src/api/system/code_gen_column.d.ts +12 -0
- package/dist/package/declarations/src/api/system/code_gen_proto.d.ts +10 -0
- package/dist/package/declarations/src/api/system/code_gen_sse.d.ts +8 -0
- package/dist/package/declarations/src/api/system/code_gen_table.d.ts +18 -0
- package/dist/package/declarations/src/api/system/project_document.d.ts +9 -0
- package/dist/package/declarations/src/components/Ai.vue.d.ts +3 -0
- package/dist/package/declarations/src/index.d.ts +3 -0
- package/dist/package/declarations/src/module.d.ts +2 -0
- package/dist/package/declarations/src/rpc/base/v1/ai_message.d.ts +82 -0
- package/dist/package/declarations/src/rpc/base/v1/ai_session.d.ts +176 -0
- package/dist/package/declarations/src/rpc/base/v1/ai_tool.d.ts +59 -0
- package/dist/package/declarations/src/rpc/base/v1/oauth.d.ts +167 -0
- package/dist/package/declarations/src/rpc/base/v1/sse.d.ts +13 -0
- package/dist/package/declarations/src/rpc/common/v1/common.d.ts +57 -0
- package/dist/package/declarations/src/rpc/common/v1/enum.d.ts +58 -0
- package/dist/package/declarations/src/rpc/common/v1/types.d.ts +60 -0
- package/dist/package/declarations/src/rpc/google/protobuf/empty.d.ts +11 -0
- package/dist/package/declarations/src/rpc/google/protobuf/timestamp.d.ts +107 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/auth.d.ts +156 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_api.d.ts +193 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_area.d.ts +82 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_config.d.ts +112 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_dept.d.ts +112 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_dict.d.ts +215 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_job.d.ts +177 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_log.d.ts +87 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_menu.d.ts +151 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_migration.d.ts +64 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_post.d.ts +112 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_role.d.ts +128 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_tenant.d.ts +112 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_user.d.ts +161 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/code_gen.d.ts +155 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/code_gen_column.d.ts +143 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/code_gen_proto.d.ts +86 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/code_gen_table.d.ts +149 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/project_document.d.ts +64 -0
- package/dist/package/declarations/src/rpc/system/common/v1/enum.d.ts +67 -0
- package/dist/package/declarations/src/views/ai/chat/attachment.d.ts +14 -0
- package/dist/package/declarations/src/views/ai/chat/components/AiMarkdown.vue.d.ts +13 -0
- package/dist/package/declarations/src/views/ai/chat/components/ChatPanel.vue.d.ts +45 -0
- package/dist/package/declarations/src/views/ai/chat/components/SessionPanel.vue.d.ts +34 -0
- package/dist/package/declarations/src/views/ai/chat/components/XSender.vue.d.ts +12 -0
- package/dist/package/declarations/src/views/ai/chat/index.vue.d.ts +4 -0
- package/dist/package/declarations/src/views/ai/chat/message.d.ts +59 -0
- package/dist/package/declarations/src/views/ai/chat/stream.d.ts +10 -0
- package/dist/package/declarations/src/views/ai/chat/types.d.ts +115 -0
- package/dist/package/declarations/src/views/base/api/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/base/area/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/base/config/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/base/dept/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/base/dict/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/base/dict/item.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/base/job/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/base/job/log.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/base/log/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/base/menu/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/base/migration/index.vue.d.ts +4 -0
- package/dist/package/declarations/src/views/base/post/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/base/role/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/base/tenant/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/base/user/components/dept-tree.vue.d.ts +19 -0
- package/dist/package/declarations/src/views/base/user/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/profile/components/base.vue.d.ts +13 -0
- package/dist/package/declarations/src/views/profile/components/password.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/profile/components/security.vue.d.ts +15 -0
- package/dist/package/declarations/src/views/profile/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/tool/api-doc/index.vue.d.ts +4 -0
- package/dist/package/declarations/src/views/tool/code-gen/code-preview/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/tool/code-gen/columns/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/tool/code-gen/columns/option-copy.d.ts +25 -0
- package/dist/package/declarations/src/views/tool/code-gen/components/CodeGenProgressDialog.vue.d.ts +16 -0
- package/dist/package/declarations/src/views/tool/code-gen/components/CodePreviewPane.vue.d.ts +9 -0
- package/dist/package/declarations/src/views/tool/code-gen/config.d.ts +32 -0
- package/dist/package/declarations/src/views/tool/code-gen/preview/capabilities.d.ts +20 -0
- package/dist/package/declarations/src/views/tool/code-gen/preview/data.d.ts +42 -0
- package/dist/package/declarations/src/views/tool/code-gen/preview/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/tool/code-gen/proto/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/tool/code-gen/table/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/tool/project-doc/index.vue.d.ts +4 -0
- package/dist/package/src/ai.ts +16 -0
- package/dist/package/src/api/base/ai_message.ts +139 -0
- package/dist/package/src/api/base/ai_session.ts +77 -0
- package/dist/package/src/api/base/ai_tool.ts +22 -0
- package/dist/package/src/api/base/oauth.ts +47 -0
- package/dist/package/src/api/base/sse.ts +192 -0
- package/dist/package/src/api/system/auth.ts +65 -0
- package/dist/package/src/api/system/base_api.ts +95 -0
- package/dist/package/src/api/system/base_area.ts +73 -0
- package/dist/package/src/api/system/base_config.ts +82 -0
- package/dist/package/src/api/system/base_dept.ts +83 -0
- package/dist/package/src/api/system/base_dict.ts +144 -0
- package/dist/package/src/api/system/base_job.ts +125 -0
- package/dist/package/src/api/system/base_log.ts +32 -0
- package/dist/package/src/api/system/base_menu.ts +83 -0
- package/dist/package/src/api/system/base_migration.ts +35 -0
- package/dist/package/src/api/system/base_post.ts +83 -0
- package/dist/package/src/api/system/base_role.ts +93 -0
- package/dist/package/src/api/system/base_tenant.ts +83 -0
- package/dist/package/src/api/system/base_user.ts +93 -0
- package/dist/package/src/api/system/code_gen.ts +56 -0
- package/dist/package/src/api/system/code_gen_column.ts +43 -0
- package/dist/package/src/api/system/code_gen_proto.ts +32 -0
- package/dist/package/src/api/system/code_gen_sse.ts +33 -0
- package/dist/package/src/api/system/code_gen_table.ts +74 -0
- package/dist/package/src/api/system/project_document.ts +33 -0
- package/dist/package/src/components/Ai.vue +57 -0
- package/dist/package/src/index.ts +3 -0
- package/dist/package/src/module.ts +17 -0
- package/dist/package/src/rpc/base/v1/ai_message.ts +98 -0
- package/dist/package/src/rpc/base/v1/ai_session.ts +214 -0
- package/dist/package/src/rpc/base/v1/ai_tool.ts +74 -0
- package/dist/package/src/rpc/base/v1/oauth.ts +199 -0
- package/dist/package/src/rpc/base/v1/sse.ts +22 -0
- package/dist/package/src/rpc/common/v1/common.ts +71 -0
- package/dist/package/src/rpc/common/v1/enum.ts +71 -0
- package/dist/package/src/rpc/common/v1/types.ts +77 -0
- package/dist/package/src/rpc/google/protobuf/empty.ts +19 -0
- package/dist/package/src/rpc/google/protobuf/timestamp.ts +115 -0
- package/dist/package/src/rpc/system/admin/v1/auth.ts +208 -0
- package/dist/package/src/rpc/system/admin/v1/base_api.ts +241 -0
- package/dist/package/src/rpc/system/admin/v1/base_area.ts +105 -0
- package/dist/package/src/rpc/system/admin/v1/base_config.ts +140 -0
- package/dist/package/src/rpc/system/admin/v1/base_dept.ts +140 -0
- package/dist/package/src/rpc/system/admin/v1/base_dict.ts +249 -0
- package/dist/package/src/rpc/system/admin/v1/base_job.ts +206 -0
- package/dist/package/src/rpc/system/admin/v1/base_log.ts +101 -0
- package/dist/package/src/rpc/system/admin/v1/base_menu.ts +195 -0
- package/dist/package/src/rpc/system/admin/v1/base_migration.ts +81 -0
- package/dist/package/src/rpc/system/admin/v1/base_post.ts +134 -0
- package/dist/package/src/rpc/system/admin/v1/base_role.ts +151 -0
- package/dist/package/src/rpc/system/admin/v1/base_tenant.ts +134 -0
- package/dist/package/src/rpc/system/admin/v1/base_user.ts +190 -0
- package/dist/package/src/rpc/system/admin/v1/code_gen.ts +176 -0
- package/dist/package/src/rpc/system/admin/v1/code_gen_column.ts +170 -0
- package/dist/package/src/rpc/system/admin/v1/code_gen_proto.ts +104 -0
- package/dist/package/src/rpc/system/admin/v1/code_gen_table.ts +179 -0
- package/dist/package/src/rpc/system/admin/v1/project_document.ts +79 -0
- package/dist/package/src/rpc/system/common/v1/enum.ts +80 -0
- package/dist/package/src/typings/swagger-ui-dist.d.ts +28 -0
- package/dist/package/src/typings/x-markdown-vue.d.ts +1 -0
- package/dist/package/src/views/ai/chat/attachment.ts +65 -0
- package/dist/package/src/views/ai/chat/components/AiMarkdown.vue +162 -0
- package/dist/package/src/views/ai/chat/components/ChatPanel.vue +1861 -0
- package/dist/package/src/views/ai/chat/components/SessionPanel.vue +316 -0
- package/dist/package/src/views/ai/chat/components/XSender.vue +554 -0
- package/dist/package/src/views/ai/chat/index.vue +848 -0
- package/dist/package/src/views/ai/chat/message.ts +416 -0
- package/dist/package/src/views/ai/chat/stream.ts +115 -0
- package/dist/package/src/views/ai/chat/types.ts +126 -0
- package/dist/package/src/views/base/api/index.vue +455 -0
- package/dist/package/src/views/base/area/index.vue +269 -0
- package/dist/package/src/views/base/config/index.vue +530 -0
- package/dist/package/src/views/base/dept/index.vue +461 -0
- package/dist/package/src/views/base/dict/index.vue +304 -0
- package/dist/package/src/views/base/dict/item.vue +373 -0
- package/dist/package/src/views/base/job/index.vue +535 -0
- package/dist/package/src/views/base/job/log.vue +202 -0
- package/dist/package/src/views/base/log/index.vue +317 -0
- package/dist/package/src/views/base/menu/index.vue +944 -0
- package/dist/package/src/views/base/migration/index.vue +791 -0
- package/dist/package/src/views/base/post/index.vue +324 -0
- package/dist/package/src/views/base/role/index.vue +684 -0
- package/dist/package/src/views/base/tenant/index.vue +328 -0
- package/dist/package/src/views/base/user/components/dept-tree.vue +75 -0
- package/dist/package/src/views/base/user/index.vue +783 -0
- package/dist/package/src/views/profile/components/base.vue +320 -0
- package/dist/package/src/views/profile/components/password.vue +269 -0
- package/dist/package/src/views/profile/components/security.vue +443 -0
- package/dist/package/src/views/profile/index.vue +199 -0
- package/dist/package/src/views/tool/api-doc/index.vue +275 -0
- package/dist/package/src/views/tool/code-gen/code-preview/index.vue +233 -0
- package/dist/package/src/views/tool/code-gen/columns/index.vue +1456 -0
- package/dist/package/src/views/tool/code-gen/columns/option-copy.ts +125 -0
- package/dist/package/src/views/tool/code-gen/components/CodeGenProgressDialog.vue +328 -0
- package/dist/package/src/views/tool/code-gen/components/CodePreviewPane.vue +42 -0
- package/dist/package/src/views/tool/code-gen/config.ts +175 -0
- package/dist/package/src/views/tool/code-gen/preview/capabilities.ts +34 -0
- package/dist/package/src/views/tool/code-gen/preview/data.ts +331 -0
- package/dist/package/src/views/tool/code-gen/preview/index.vue +831 -0
- package/dist/package/src/views/tool/code-gen/proto/index.vue +703 -0
- package/dist/package/src/views/tool/code-gen/table/index.vue +864 -0
- package/dist/package/src/views/tool/project-doc/index.vue +561 -0
- package/package.json +60 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AiAttachment,
|
|
3
|
+
type AiInputContent,
|
|
4
|
+
type AiMessage,
|
|
5
|
+
type AiOutputContent,
|
|
6
|
+
type AiSession,
|
|
7
|
+
type AiToken
|
|
8
|
+
} from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_session";
|
|
9
|
+
import type { AiAction } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_message";
|
|
10
|
+
import { AiMessageStatus, Terminal } from "@liujitcn/kratos-admin-system/rpc/common/v1/enum";
|
|
11
|
+
import type { AiStreamPayload, AIFlowBlock, ChatMessageItem, ReplySourceTag } from "./types";
|
|
12
|
+
|
|
13
|
+
const THINKING_MESSAGE_ID_PREFIX = "ai-thinking";
|
|
14
|
+
const LOCAL_USER_MESSAGE_ID_PREFIX = "ai-user-local";
|
|
15
|
+
const PENDING_STREAM_MESSAGE_ID = "pending";
|
|
16
|
+
const THINKING_MESSAGE_CONTENT = "正在整理回复";
|
|
17
|
+
|
|
18
|
+
/** 生成流式消息分组键,确保同一轮回复只更新当前占位气泡。 */
|
|
19
|
+
export function buildStreamMessageKey(sessionID: string, messageID: string) {
|
|
20
|
+
return [sessionID, messageID].join(":");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 生成待绑定真实消息编号前的本地流式分组键。 */
|
|
24
|
+
export function buildPendingStreamMessageKey(sessionID: string) {
|
|
25
|
+
return buildStreamMessageKey(sessionID, PENDING_STREAM_MESSAGE_ID);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 计算消息角色排序权重,确保同一轮对话里用户问题先于助手回答。 */
|
|
29
|
+
function resolveRoleOrder(role?: string) {
|
|
30
|
+
return role === "user" ? 0 : 1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 解析 protobuf 时间戳为毫秒。 */
|
|
34
|
+
export function resolveTimestamp(timestamp?: { seconds?: number; nanos?: number }) {
|
|
35
|
+
if (!timestamp) return 0;
|
|
36
|
+
const seconds = Number(timestamp.seconds ?? 0);
|
|
37
|
+
const nanos = Number(timestamp.nanos ?? 0);
|
|
38
|
+
return seconds * 1000 + Math.floor(nanos / 1_000_000);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 归一化会话对象,避免空值影响渲染。 */
|
|
42
|
+
export function normalizeSession(session?: Partial<AiSession> | null): AiSession {
|
|
43
|
+
return {
|
|
44
|
+
id: String(session?.id ?? ""),
|
|
45
|
+
title: String(session?.title ?? "新对话"),
|
|
46
|
+
summary: String(session?.summary ?? ""),
|
|
47
|
+
updated_at: session?.updated_at,
|
|
48
|
+
terminal: Number(session?.terminal ?? Terminal.TERMINAL_ADMIN)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 将会话列表收敛成可安全渲染的数组。 */
|
|
53
|
+
export function normalizeSessionList(list?: AiSession[] | null) {
|
|
54
|
+
if (!Array.isArray(list)) return [];
|
|
55
|
+
return list.map(item => normalizeSession(item)).filter(item => item.id);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 生成回复来源标签。 */
|
|
59
|
+
export function resolveReplySourceTag(
|
|
60
|
+
item: Pick<ChatMessageItem, "role" | "fallback" | "reply_source" | "model">
|
|
61
|
+
): ReplySourceTag | undefined {
|
|
62
|
+
if (item.role === "user") return undefined;
|
|
63
|
+
if (item.fallback) return { text: "降级回复", tone: "warning" };
|
|
64
|
+
switch (String(item.reply_source ?? "")) {
|
|
65
|
+
case "network":
|
|
66
|
+
return { text: "网络数据", tone: "success" };
|
|
67
|
+
case "llm":
|
|
68
|
+
return { text: "模型回答", tone: "primary" };
|
|
69
|
+
case "fallback":
|
|
70
|
+
return { text: "降级回复", tone: "warning" };
|
|
71
|
+
default:
|
|
72
|
+
return item.model ? { text: "模型回答", tone: "primary" } : undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 归一化输入内容,避免后端空对象影响渲染。 */
|
|
77
|
+
function normalizeInputContent(content?: AiInputContent): AiInputContent {
|
|
78
|
+
return {
|
|
79
|
+
kind: String(content?.kind || "text"),
|
|
80
|
+
content: String(content?.content ?? "")
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 归一化输出内容,避免后端空对象影响渲染。 */
|
|
85
|
+
function normalizeOutputContent(content?: AiOutputContent): AiOutputContent {
|
|
86
|
+
return {
|
|
87
|
+
kind: String(content?.kind || "text"),
|
|
88
|
+
content: String(content?.content ?? ""),
|
|
89
|
+
reply_source: String(content?.reply_source ?? ""),
|
|
90
|
+
model: String(content?.model ?? ""),
|
|
91
|
+
fallback: Boolean(content?.fallback),
|
|
92
|
+
fallback_reason: String(content?.fallback_reason ?? ""),
|
|
93
|
+
flow: String(content?.flow ?? ""),
|
|
94
|
+
step: String(content?.step ?? ""),
|
|
95
|
+
blocks_json: String(content?.blocks_json ?? "")
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 解析结构化 Flow 卡片,非法 JSON 直接降级为空数组。 */
|
|
100
|
+
export function parseFlowBlocks(raw?: string) {
|
|
101
|
+
if (!raw) return [];
|
|
102
|
+
try {
|
|
103
|
+
const blocks = JSON.parse(raw) as unknown;
|
|
104
|
+
if (!Array.isArray(blocks)) return [];
|
|
105
|
+
return blocks.filter((item): item is AIFlowBlock => Boolean(item && typeof item === "object" && String(item.type ?? "")));
|
|
106
|
+
} catch {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 根据消息状态禁用过期结构化动作,减少用户误点旧步骤。 */
|
|
112
|
+
export function markFlowBlocksDisabled(blocks: AIFlowBlock[], messageID: string) {
|
|
113
|
+
return blocks.map(block => markFlowValueDisabled(block, messageID)) as AIFlowBlock[];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** 递归标记结构化卡片中的 action 是否仍属于当前消息。 */
|
|
117
|
+
function markFlowValueDisabled(value: unknown, messageID: string): unknown {
|
|
118
|
+
if (Array.isArray(value)) {
|
|
119
|
+
value.forEach(item => markFlowValueDisabled(item, messageID));
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
if (!value || typeof value !== "object") return value;
|
|
123
|
+
|
|
124
|
+
const current = value as Record<string, any>;
|
|
125
|
+
if (current.type && current.flow) {
|
|
126
|
+
current.disabled = !isFlowActionFromMessage(current, messageID);
|
|
127
|
+
}
|
|
128
|
+
if (current.action?.type) {
|
|
129
|
+
current.disabled = !isFlowActionFromMessage(current.action, messageID);
|
|
130
|
+
}
|
|
131
|
+
Object.values(current).forEach(item => markFlowValueDisabled(item, messageID));
|
|
132
|
+
return current;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** 判断结构化动作是否来自指定助手消息。 */
|
|
136
|
+
export function isFlowActionFromMessage(action: Partial<AiAction> | undefined, messageID: string) {
|
|
137
|
+
if (!messageID || !action?.source_message_id || !action.action_id) return false;
|
|
138
|
+
return action.source_message_id === messageID && String(action.flow_version || "") === messageID;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 归一化 token 统计,保证用量展示字段都有默认值。 */
|
|
142
|
+
function normalizeToken(token?: AiToken): AiToken {
|
|
143
|
+
return {
|
|
144
|
+
input: Number(token?.input ?? 0),
|
|
145
|
+
output: Number(token?.output ?? 0),
|
|
146
|
+
cache: Number(token?.cache ?? 0),
|
|
147
|
+
total: Number(token?.total ?? 0)
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** 将后端一轮消息映射为单个聊天气泡结构。 */
|
|
152
|
+
export function mapMessageItem(message: AiMessage, role: "user" | "ai"): ChatMessageItem {
|
|
153
|
+
const inputContent = normalizeInputContent(message.input_content);
|
|
154
|
+
const outputContent = normalizeOutputContent(message.output_content);
|
|
155
|
+
const content = role === "user" ? inputContent : outputContent;
|
|
156
|
+
const item: ChatMessageItem = {
|
|
157
|
+
...message,
|
|
158
|
+
role,
|
|
159
|
+
key: `${message.id}:${role}`,
|
|
160
|
+
content: content.content,
|
|
161
|
+
kind: content.kind,
|
|
162
|
+
placement: role === "user" ? "end" : "start",
|
|
163
|
+
reply_source: role === "ai" ? outputContent.reply_source : "",
|
|
164
|
+
model: role === "ai" ? outputContent.model : "",
|
|
165
|
+
fallback: role === "ai" && outputContent.fallback,
|
|
166
|
+
fallback_reason: role === "ai" ? outputContent.fallback_reason : "",
|
|
167
|
+
flow: role === "ai" ? outputContent.flow : "",
|
|
168
|
+
step: role === "ai" ? outputContent.step : "",
|
|
169
|
+
blocksJson: role === "ai" ? outputContent.blocks_json : "",
|
|
170
|
+
blocks: role === "ai" ? markFlowBlocksDisabled(parseFlowBlocks(outputContent.blocks_json), String(message.id ?? "")) : [],
|
|
171
|
+
status: Number(message.status ?? AiMessageStatus.SUCCESS_AAMS),
|
|
172
|
+
token: normalizeToken(message.token),
|
|
173
|
+
tools: Array.isArray(message.tools) ? message.tools : [],
|
|
174
|
+
variant: role === "user" ? "filled" : "borderless",
|
|
175
|
+
shape: "corner",
|
|
176
|
+
progressState:
|
|
177
|
+
message.status === AiMessageStatus.GENERATING_AAMS
|
|
178
|
+
? role === "user"
|
|
179
|
+
? "idle"
|
|
180
|
+
: "streaming"
|
|
181
|
+
: message.status === AiMessageStatus.FAILED_AAMS
|
|
182
|
+
? "failed"
|
|
183
|
+
: "idle",
|
|
184
|
+
maxWidth: role === "user" ? "380px" : "100%"
|
|
185
|
+
};
|
|
186
|
+
item.replySourceTag = resolveReplySourceTag(item);
|
|
187
|
+
return item;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** 收敛消息数组,并把每轮消息拆成用户气泡和助手气泡。 */
|
|
191
|
+
export function normalizeMessageList(list?: AiMessage[] | null) {
|
|
192
|
+
if (!Array.isArray(list)) return [];
|
|
193
|
+
return list.filter(Boolean).flatMap(item => [mapMessageItem(item, "user"), mapMessageItem(item, "ai")]);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** 创建本地用户回显消息。 */
|
|
197
|
+
export function createLocalUserMessage(payload: { text: string; attachments: AiAttachment[] }) {
|
|
198
|
+
const now = new Date();
|
|
199
|
+
const message = mapMessageItem(
|
|
200
|
+
{
|
|
201
|
+
id: `${LOCAL_USER_MESSAGE_ID_PREFIX}-${now.getTime()}`,
|
|
202
|
+
input_content: {
|
|
203
|
+
kind: "text",
|
|
204
|
+
content: payload.text
|
|
205
|
+
},
|
|
206
|
+
output_content: undefined,
|
|
207
|
+
attachments: payload.attachments,
|
|
208
|
+
created_at: {
|
|
209
|
+
seconds: Math.floor(now.getTime() / 1000),
|
|
210
|
+
nanos: (now.getTime() % 1000) * 1_000_000
|
|
211
|
+
},
|
|
212
|
+
status: AiMessageStatus.GENERATING_AAMS,
|
|
213
|
+
token: normalizeToken(),
|
|
214
|
+
tools: [],
|
|
215
|
+
first_token_ms: 0,
|
|
216
|
+
duration_ms: 0
|
|
217
|
+
},
|
|
218
|
+
"user"
|
|
219
|
+
);
|
|
220
|
+
// 本地回显消息只用于等待服务端响应,收到正式消息后需要被替换掉,避免同一问题展示两遍。
|
|
221
|
+
message.localOnly = true;
|
|
222
|
+
// 用户消息只是本地回显,不参与助手流式动画,避免用户气泡出现思考中的省略点。
|
|
223
|
+
message.progressState = "idle";
|
|
224
|
+
return message;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** 创建助手思考中的占位消息。 */
|
|
228
|
+
export function createThinkingMessage(options?: { sessionID?: string; messageID?: string }) {
|
|
229
|
+
const now = new Date();
|
|
230
|
+
const streamKey = options?.sessionID
|
|
231
|
+
? buildStreamMessageKey(options.sessionID, options.messageID || PENDING_STREAM_MESSAGE_ID)
|
|
232
|
+
: undefined;
|
|
233
|
+
const message = mapMessageItem(
|
|
234
|
+
{
|
|
235
|
+
id: streamKey || `${THINKING_MESSAGE_ID_PREFIX}-${now.getTime()}`,
|
|
236
|
+
input_content: undefined,
|
|
237
|
+
output_content: {
|
|
238
|
+
kind: "text",
|
|
239
|
+
content: THINKING_MESSAGE_CONTENT,
|
|
240
|
+
reply_source: "",
|
|
241
|
+
model: "",
|
|
242
|
+
fallback: false,
|
|
243
|
+
fallback_reason: "",
|
|
244
|
+
flow: "",
|
|
245
|
+
step: "",
|
|
246
|
+
blocks_json: ""
|
|
247
|
+
},
|
|
248
|
+
attachments: [],
|
|
249
|
+
created_at: {
|
|
250
|
+
seconds: Math.floor(now.getTime() / 1000),
|
|
251
|
+
nanos: (now.getTime() % 1000) * 1_000_000
|
|
252
|
+
},
|
|
253
|
+
status: AiMessageStatus.GENERATING_AAMS,
|
|
254
|
+
token: normalizeToken(),
|
|
255
|
+
tools: [],
|
|
256
|
+
first_token_ms: 0,
|
|
257
|
+
duration_ms: 0
|
|
258
|
+
},
|
|
259
|
+
"ai"
|
|
260
|
+
);
|
|
261
|
+
message.progressState = "streaming";
|
|
262
|
+
message.localOnly = true;
|
|
263
|
+
message.streamKey = streamKey;
|
|
264
|
+
message.replySourceTag = { text: "思考中", tone: "info" };
|
|
265
|
+
return message;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** 确保当前轮次存在流式占位消息。 */
|
|
269
|
+
export function ensureStreamingMessage(current: ChatMessageItem[], payload: AiStreamPayload) {
|
|
270
|
+
const sessionID = String(payload.session_id ?? "");
|
|
271
|
+
const messageID = String(payload.message_id ?? "");
|
|
272
|
+
if (!sessionID || !messageID) return current;
|
|
273
|
+
|
|
274
|
+
const streamKey = buildStreamMessageKey(sessionID, messageID);
|
|
275
|
+
if (current.some(item => item.streamKey === streamKey)) return current;
|
|
276
|
+
|
|
277
|
+
const pendingStreamKey = buildPendingStreamMessageKey(sessionID);
|
|
278
|
+
const next = current.map<ChatMessageItem>(item =>
|
|
279
|
+
item.streamKey === pendingStreamKey ? { ...item, id: messageID, key: `${messageID}:ai`, streamKey } : item
|
|
280
|
+
);
|
|
281
|
+
if (next.some(item => item.streamKey === streamKey)) return next;
|
|
282
|
+
|
|
283
|
+
return sortMessages([...next, createThinkingMessage({ sessionID, messageID })]);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** 将已有助手消息清空并标记为重新生成中,保留消息 ID 与原位置。 */
|
|
287
|
+
export function markAIMessageRegenerating(current: ChatMessageItem[], sessionID: string, messageID: string) {
|
|
288
|
+
const streamKey = buildStreamMessageKey(sessionID, messageID);
|
|
289
|
+
return current.map<ChatMessageItem>(item => {
|
|
290
|
+
if (String(item.id) !== messageID || item.role === "user") return item;
|
|
291
|
+
return {
|
|
292
|
+
...item,
|
|
293
|
+
content: THINKING_MESSAGE_CONTENT,
|
|
294
|
+
fallback: false,
|
|
295
|
+
fallback_reason: "",
|
|
296
|
+
token: normalizeToken(),
|
|
297
|
+
tools: [],
|
|
298
|
+
first_token_ms: 0,
|
|
299
|
+
duration_ms: 0,
|
|
300
|
+
progressState: "streaming",
|
|
301
|
+
replySourceTag: { text: "思考中", tone: "info" },
|
|
302
|
+
status: AiMessageStatus.GENERATING_AAMS,
|
|
303
|
+
streamKey
|
|
304
|
+
};
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** 将消息按创建时间排序。 */
|
|
309
|
+
export function sortMessages(list: ChatMessageItem[]) {
|
|
310
|
+
return [...list].sort((left, right) => {
|
|
311
|
+
const leftTime = resolveTimestamp(left.created_at);
|
|
312
|
+
const rightTime = resolveTimestamp(right.created_at);
|
|
313
|
+
if (leftTime === rightTime) {
|
|
314
|
+
const roleOrder = resolveRoleOrder(left.role) - resolveRoleOrder(right.role);
|
|
315
|
+
if (roleOrder !== 0) return roleOrder;
|
|
316
|
+
return String(left.id).localeCompare(String(right.id), "zh-Hans-CN", { numeric: true });
|
|
317
|
+
}
|
|
318
|
+
return leftTime - rightTime;
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** 去掉当前轮次的本地占位消息,并拼入服务端返回。 */
|
|
323
|
+
export function replacePendingMessages(current: ChatMessageItem[], nextMessages: ChatMessageItem[], payload?: AiStreamPayload) {
|
|
324
|
+
const sessionID = String(payload?.session_id ?? "");
|
|
325
|
+
const streamKey = payload && buildStreamMessageKey(sessionID, String(payload.message_id ?? ""));
|
|
326
|
+
const pendingStreamKey = payload?.message_id && sessionID ? buildPendingStreamMessageKey(sessionID) : "";
|
|
327
|
+
const stableMessages = current.filter(item => {
|
|
328
|
+
if (!item.localOnly) return true;
|
|
329
|
+
if (payload?.message_id && item.role === "user") {
|
|
330
|
+
return !nextMessages.some(message => message.role === "user" && String(message.id) === String(payload.message_id));
|
|
331
|
+
}
|
|
332
|
+
if (!streamKey) return false;
|
|
333
|
+
return item.streamKey !== streamKey && item.streamKey !== pendingStreamKey;
|
|
334
|
+
});
|
|
335
|
+
const messageMap = new Map<string, ChatMessageItem>();
|
|
336
|
+
|
|
337
|
+
for (const item of stableMessages) {
|
|
338
|
+
messageMap.set(String(item.key ?? `${item.id}:${item.role}`), item);
|
|
339
|
+
}
|
|
340
|
+
for (const item of nextMessages) {
|
|
341
|
+
messageMap.set(String(item.key ?? `${item.id}:${item.role}`), item);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return sortMessages(Array.from(messageMap.values()));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** 标记思考中消息失败,可按当前轮次限定失败范围。 */
|
|
348
|
+
export function markThinkingMessageFailed(current: ChatMessageItem[], options?: { sessionID?: string; messageID?: string }) {
|
|
349
|
+
const streamKey = options?.sessionID && options.messageID ? buildStreamMessageKey(options.sessionID, options.messageID) : "";
|
|
350
|
+
return current.map<ChatMessageItem>(item => {
|
|
351
|
+
if (!item.localOnly) return item;
|
|
352
|
+
if (streamKey && item.streamKey !== streamKey) return item;
|
|
353
|
+
if (item.role === "user" && !streamKey) {
|
|
354
|
+
return {
|
|
355
|
+
...item,
|
|
356
|
+
progressState: "failed",
|
|
357
|
+
status: AiMessageStatus.FAILED_AAMS
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
if (item.progressState !== "streaming") return item;
|
|
361
|
+
return {
|
|
362
|
+
...item,
|
|
363
|
+
progressState: "failed",
|
|
364
|
+
status: AiMessageStatus.FAILED_AAMS,
|
|
365
|
+
content: "这次回复没有成功返回,你可以直接重试刚才的问题。",
|
|
366
|
+
replySourceTag: { text: "发送失败", tone: "warning" }
|
|
367
|
+
};
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** 判断流式增量是否包含可追加内容。 */
|
|
372
|
+
export function hasStreamingDelta(payload: Pick<AiStreamPayload, "delta">) {
|
|
373
|
+
return payload.delta !== undefined && payload.delta !== "";
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** 将流式文本增量追加到本地占位消息。 */
|
|
377
|
+
export function appendStreamingDelta(current: ChatMessageItem[], payload: AiStreamPayload) {
|
|
378
|
+
if (!hasStreamingDelta(payload)) return current;
|
|
379
|
+
const streamKey = buildStreamMessageKey(String(payload.session_id ?? ""), String(payload.message_id ?? ""));
|
|
380
|
+
return current.map<ChatMessageItem>(item => {
|
|
381
|
+
if (item.streamKey !== streamKey || (!item.localOnly && item.role === "user")) return item;
|
|
382
|
+
const baseContent = item.content === THINKING_MESSAGE_CONTENT ? "" : item.content;
|
|
383
|
+
const nextContent = `${baseContent}${payload.delta ?? ""}`;
|
|
384
|
+
return {
|
|
385
|
+
...item,
|
|
386
|
+
content: nextContent || item.content,
|
|
387
|
+
progressState: "streaming",
|
|
388
|
+
replySourceTag: { text: "回答中", tone: "info" }
|
|
389
|
+
};
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** 根据流式异常事件更新本地占位消息。 */
|
|
394
|
+
export function markStreamingError(current: ChatMessageItem[], payload: AiStreamPayload) {
|
|
395
|
+
const messageID = String(payload.message_id ?? "");
|
|
396
|
+
const streamKey = messageID ? buildStreamMessageKey(String(payload.session_id ?? ""), messageID) : "";
|
|
397
|
+
return current.map<ChatMessageItem>(item => {
|
|
398
|
+
if (!item.localOnly) return item;
|
|
399
|
+
if (streamKey && item.streamKey !== streamKey) return item;
|
|
400
|
+
return {
|
|
401
|
+
...item,
|
|
402
|
+
progressState: "failed",
|
|
403
|
+
status: AiMessageStatus.FAILED_AAMS,
|
|
404
|
+
content: "这次回复没有成功返回,你可以直接重试刚才的问题。",
|
|
405
|
+
replySourceTag: { text: "发送失败", tone: "warning" }
|
|
406
|
+
};
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** 标记正在朗读的消息,保证全局只有一个气泡处于朗读态。 */
|
|
411
|
+
export function markSpeakingMessage(current: ChatMessageItem[], messageID?: string) {
|
|
412
|
+
return current.map<ChatMessageItem>(item => ({
|
|
413
|
+
...item,
|
|
414
|
+
speaking: Boolean(messageID && String(item.key ?? `${item.id}:${item.role}`) === messageID)
|
|
415
|
+
}));
|
|
416
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { AiStreamEvent, AiStreamEventName, AiStreamPayload } from "./types";
|
|
2
|
+
|
|
3
|
+
const STREAM_EVENT_NAMES = new Set<AiStreamEventName>(["delta", "finish", "error"]);
|
|
4
|
+
|
|
5
|
+
/** 读取到的原始 SSE 字段结构。 */
|
|
6
|
+
type SseOutput = Partial<Record<"data" | "event" | "id" | "retry", unknown>>;
|
|
7
|
+
|
|
8
|
+
/** AI 助手事件流消费回调。 */
|
|
9
|
+
type AiStreamEventHandler = (event: AiStreamEvent) => void;
|
|
10
|
+
|
|
11
|
+
/** 判断 SSE 事件名称是否为 AI 助手 direct stream 支持的事件。 */
|
|
12
|
+
function isAiStreamEventName(event?: unknown): event is AiStreamEventName {
|
|
13
|
+
return STREAM_EVENT_NAMES.has(String(event ?? "").trim() as AiStreamEventName);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** 解析 SSE data 字段,兼容前导空格和空消息。 */
|
|
17
|
+
function parseStreamPayload(data?: unknown): AiStreamPayload | null {
|
|
18
|
+
const rawData = String(data ?? "").trimStart();
|
|
19
|
+
if (!rawData) return null;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(rawData) as AiStreamPayload;
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 将原始 SSE 项收敛为业务事件,避免页面直接处理字符串 JSON。 */
|
|
29
|
+
export function normalizeAiStreamItem(item?: SseOutput): AiStreamEvent | null {
|
|
30
|
+
if (!item || !isAiStreamEventName(item.event)) return null;
|
|
31
|
+
|
|
32
|
+
const payload = parseStreamPayload(item.data);
|
|
33
|
+
if (!payload?.session_id || !payload.message_id) return null;
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
event: String(item.event).trim() as AiStreamEventName,
|
|
37
|
+
payload
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 读取并解析 AI 助手 direct stream,支持同一页面同时消费多条会话流。 */
|
|
42
|
+
export async function readAiEventStream(
|
|
43
|
+
readableStream: ReadableStream<Uint8Array>,
|
|
44
|
+
handler: AiStreamEventHandler,
|
|
45
|
+
signal?: AbortSignal
|
|
46
|
+
) {
|
|
47
|
+
const reader = readableStream.getReader();
|
|
48
|
+
const decoder = new TextDecoder();
|
|
49
|
+
let buffer = "";
|
|
50
|
+
let currentItem: SseOutput = {};
|
|
51
|
+
|
|
52
|
+
/** 分发当前累计的 SSE 事件。 */
|
|
53
|
+
function dispatchCurrentItem() {
|
|
54
|
+
const event = normalizeAiStreamItem(currentItem);
|
|
55
|
+
currentItem = {};
|
|
56
|
+
if (event) handler(event);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 按 SSE 行协议累积字段,空行代表一条事件结束。 */
|
|
60
|
+
function handleLine(line: string) {
|
|
61
|
+
if (line === "") {
|
|
62
|
+
dispatchCurrentItem();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (line.startsWith(":")) return;
|
|
66
|
+
|
|
67
|
+
const separatorIndex = line.indexOf(":");
|
|
68
|
+
const field = separatorIndex >= 0 ? line.slice(0, separatorIndex) : line;
|
|
69
|
+
let value = separatorIndex >= 0 ? line.slice(separatorIndex + 1) : "";
|
|
70
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
71
|
+
|
|
72
|
+
if (field === "data") {
|
|
73
|
+
currentItem.data = currentItem.data === undefined ? value : `${currentItem.data}\n${value}`;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (field === "event" || field === "id" || field === "retry") {
|
|
77
|
+
currentItem[field] = value;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 消费缓冲区里的完整行,保留最后一个未结束的半行。 */
|
|
82
|
+
function consumeBuffer(flush = false) {
|
|
83
|
+
let lineBreakIndex = buffer.indexOf("\n");
|
|
84
|
+
while (lineBreakIndex >= 0) {
|
|
85
|
+
const line = buffer.slice(0, lineBreakIndex).replace(/\r$/, "");
|
|
86
|
+
buffer = buffer.slice(lineBreakIndex + 1);
|
|
87
|
+
handleLine(line);
|
|
88
|
+
lineBreakIndex = buffer.indexOf("\n");
|
|
89
|
+
}
|
|
90
|
+
if (flush && buffer) {
|
|
91
|
+
handleLine(buffer.replace(/\r$/, ""));
|
|
92
|
+
buffer = "";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const abortReader = () => {
|
|
97
|
+
void reader.cancel();
|
|
98
|
+
};
|
|
99
|
+
signal?.addEventListener("abort", abortReader, { once: true });
|
|
100
|
+
try {
|
|
101
|
+
while (true) {
|
|
102
|
+
if (signal?.aborted) break;
|
|
103
|
+
const { value, done } = await reader.read();
|
|
104
|
+
if (done) break;
|
|
105
|
+
buffer += decoder.decode(value, { stream: true });
|
|
106
|
+
consumeBuffer();
|
|
107
|
+
}
|
|
108
|
+
buffer += decoder.decode();
|
|
109
|
+
consumeBuffer(true);
|
|
110
|
+
dispatchCurrentItem();
|
|
111
|
+
} finally {
|
|
112
|
+
signal?.removeEventListener("abort", abortReader);
|
|
113
|
+
reader.releaseLock();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { AiAction } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_message";
|
|
2
|
+
import type { AiAttachment, AiMessage, AiSession } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_session";
|
|
3
|
+
|
|
4
|
+
/** 会话菜单动作类型。 */
|
|
5
|
+
export type SessionAction = "rename" | "delete";
|
|
6
|
+
|
|
7
|
+
/** 聊天消息操作类型。 */
|
|
8
|
+
export type ChatMessageAction = "retry" | "copy" | "delete" | "branch" | "speak" | "edit";
|
|
9
|
+
|
|
10
|
+
/** 聊天消息文本编辑提交内容。 */
|
|
11
|
+
export type ChatMessageEditPayload = {
|
|
12
|
+
/** 被编辑的用户消息。 */
|
|
13
|
+
item: ChatMessageItem;
|
|
14
|
+
/** 更新后的纯文本内容。 */
|
|
15
|
+
content: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** 聊天输入提交内容。 */
|
|
19
|
+
export type SubmitPayload = {
|
|
20
|
+
/** 用户输入文本。 */
|
|
21
|
+
text: string;
|
|
22
|
+
/** 已上传附件列表。 */
|
|
23
|
+
attachments: AiAttachment[];
|
|
24
|
+
/** 点击结构化卡片或快捷入口时携带的流程动作。 */
|
|
25
|
+
action?: AiAction;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** AI 助手结构化流程卡片。 */
|
|
29
|
+
export type AIFlowBlock = {
|
|
30
|
+
/** 卡片类型,由后端 Flow 输出。 */
|
|
31
|
+
type: string;
|
|
32
|
+
/** 卡片标题。 */
|
|
33
|
+
title?: string;
|
|
34
|
+
/** 卡片说明。 */
|
|
35
|
+
desc?: string;
|
|
36
|
+
/** 单一可执行动作。 */
|
|
37
|
+
action?: AiAction;
|
|
38
|
+
/** 多个可执行动作。 */
|
|
39
|
+
actions?: AiAction[];
|
|
40
|
+
/** 前端是否禁用当前块内动作。 */
|
|
41
|
+
disabled?: boolean;
|
|
42
|
+
/** 兼容后端按不同业务卡片返回的扩展字段。 */
|
|
43
|
+
[key: string]: any;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** 消息进度状态,用于控制气泡加载、失败和常规展示。 */
|
|
47
|
+
export type MessageProgressState = "idle" | "streaming" | "failed";
|
|
48
|
+
|
|
49
|
+
/** 回复来源标签配置。 */
|
|
50
|
+
export type ReplySourceTag = {
|
|
51
|
+
/** 标签文本。 */
|
|
52
|
+
text: string;
|
|
53
|
+
/** 标签视觉语义。 */
|
|
54
|
+
tone: "primary" | "success" | "warning" | "info";
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** 聊天气泡展示项,在后端消息基础上补充 UI 状态。 */
|
|
58
|
+
export type ChatMessageItem = AiMessage & {
|
|
59
|
+
/** 当前气泡角色,由一轮消息拆分得到。 */
|
|
60
|
+
role: "user" | "ai";
|
|
61
|
+
/** 当前气泡正文。 */
|
|
62
|
+
content: string;
|
|
63
|
+
/** 当前气泡内容类型。 */
|
|
64
|
+
kind: string;
|
|
65
|
+
/** 回复来源。 */
|
|
66
|
+
reply_source?: string;
|
|
67
|
+
/** 回复模型。 */
|
|
68
|
+
model?: string;
|
|
69
|
+
/** 是否降级回复。 */
|
|
70
|
+
fallback?: boolean;
|
|
71
|
+
/** 降级原因。 */
|
|
72
|
+
fallback_reason?: string;
|
|
73
|
+
/** 固定流程标识。 */
|
|
74
|
+
flow?: string;
|
|
75
|
+
/** 固定流程步骤。 */
|
|
76
|
+
step?: string;
|
|
77
|
+
/** 原始结构化卡片 JSON。 */
|
|
78
|
+
blocksJson?: string;
|
|
79
|
+
/** 已解析的结构化卡片。 */
|
|
80
|
+
blocks?: AIFlowBlock[];
|
|
81
|
+
/** BubbleList 稳定渲染键。 */
|
|
82
|
+
key: string;
|
|
83
|
+
/** 气泡左右位置。 */
|
|
84
|
+
placement: "start" | "end";
|
|
85
|
+
/** 气泡视觉样式。 */
|
|
86
|
+
variant?: "filled" | "borderless" | "outlined" | "shadow";
|
|
87
|
+
/** 气泡形状。 */
|
|
88
|
+
shape?: "round" | "corner";
|
|
89
|
+
/** 气泡最大宽度。 */
|
|
90
|
+
maxWidth?: string;
|
|
91
|
+
/** 本地消息进度状态。 */
|
|
92
|
+
progressState?: MessageProgressState;
|
|
93
|
+
/** 是否为前端临时消息,收到服务端最终消息后会被替换。 */
|
|
94
|
+
localOnly?: boolean;
|
|
95
|
+
/** 回复来源标签。 */
|
|
96
|
+
replySourceTag?: ReplySourceTag;
|
|
97
|
+
/** 本地流式消息键,按会话和单轮消息拆分。 */
|
|
98
|
+
streamKey?: string;
|
|
99
|
+
/** 是否正在朗读当前消息。 */
|
|
100
|
+
speaking?: boolean;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/** AI 助手 direct stream SSE 事件名称。 */
|
|
104
|
+
export type AiStreamEventName = "delta" | "finish" | "error";
|
|
105
|
+
|
|
106
|
+
/** AI 助手 direct stream 事件负载。 */
|
|
107
|
+
export type AiStreamPayload = {
|
|
108
|
+
/** 会话 ID。 */
|
|
109
|
+
session_id: string;
|
|
110
|
+
/** 后端单轮消息 ID,用于关联当前轮次。 */
|
|
111
|
+
message_id: string;
|
|
112
|
+
/** 本次新增文本分片。 */
|
|
113
|
+
delta?: string;
|
|
114
|
+
/** 流式完成后的最终消息列表。 */
|
|
115
|
+
messages?: AiMessage[];
|
|
116
|
+
/** 流式完成后的最新会话。 */
|
|
117
|
+
session?: AiSession;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/** AI 助手 direct stream 标准化事件。 */
|
|
121
|
+
export type AiStreamEvent = {
|
|
122
|
+
/** SSE 事件名称。 */
|
|
123
|
+
event: AiStreamEventName;
|
|
124
|
+
/** 已解析的 JSON 负载。 */
|
|
125
|
+
payload: AiStreamPayload;
|
|
126
|
+
};
|