@liujitcn/kratos-uni-app-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 +31 -0
- package/dist/index.mjs +93 -0
- package/package.json +38 -0
- package/src/api/base/ai_message.ts +373 -0
- package/src/api/base/ai_session.ts +85 -0
- package/src/api/base/ai_tool.ts +23 -0
- package/src/index.ts +16 -0
- package/src/pages.ts +26 -0
- package/src/rpc/base/v1/ai_message.ts +98 -0
- package/src/rpc/base/v1/ai_session.ts +214 -0
- package/src/rpc/base/v1/ai_tool.ts +74 -0
- package/src/rpc/common/v1/enum.ts +71 -0
- package/src/rpc/google/protobuf/timestamp.ts +115 -0
- package/src/views/pages/my/my.vue +238 -0
- package/src/views/pagesMember/ai/components/Composer.vue +219 -0
- package/src/views/pagesMember/ai/components/SessionDrawer.vue +308 -0
- package/src/views/pagesMember/ai/components/WelcomePanel.vue +446 -0
- package/src/views/pagesMember/ai/index.vue +1453 -0
- package/src/views/pagesMember/ai/stream.ts +190 -0
- package/src/views/pagesMember/profile/profile.vue +393 -0
- package/src/views/pagesMember/settings/settings.vue +129 -0
package/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# @liujitcn/kratos-uni-app-system
|
|
2
|
+
|
|
3
|
+
`@liujitcn/kratos-uni-app-system` 是内置 system 业务模块。个人中心、个人资料、设置和 AI 能力都归属本模块,并沿用 core 的登录态、请求、导航和页面装配协议。
|
|
4
|
+
|
|
5
|
+
## 目录结构
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
packages/modules/system
|
|
9
|
+
├── src
|
|
10
|
+
│ ├── api/base # AI 消息、会话和工具接口
|
|
11
|
+
│ ├── rpc # system 相关 Buf 生成类型
|
|
12
|
+
│ ├── views
|
|
13
|
+
│ │ ├── pages/my # 个人中心主页
|
|
14
|
+
│ │ └── pagesMember # AI、个人资料和设置页面
|
|
15
|
+
│ ├── index.ts # systemModule 定义和公开入口
|
|
16
|
+
│ └── pages.ts # 页面样式与分包声明
|
|
17
|
+
├── tsconfig.json
|
|
18
|
+
└── package.json
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`src/rpc` 是生成产物,只能通过 workspace 根目录的 `pnpm generate:rpc` 更新;该命令委托 backend 的 `make ts-app` 和 `backend/api/buf.app.core.typescript.gen.yaml` 生成。
|
|
22
|
+
|
|
23
|
+
## 功能
|
|
24
|
+
|
|
25
|
+
- 注册 `PROFILE_HOME`、`PROFILE`、`SETTINGS` 和 `AI` 四个稳定 `viewKey`。
|
|
26
|
+
- 提供个人中心、资料维护、应用设置和 AI 助手页面。
|
|
27
|
+
- 通过 core 的公开 service、store 和工具复用底座能力。
|
|
28
|
+
- 允许后续模块按相同物理路由或 `viewKey` 覆盖 system 静态视图。
|
|
29
|
+
- npm 根入口使用 `dist/index.mjs`;workspace 的 Turbo 任务和 `prepare:modules` 会先构建 system 及其依赖的 core Node 入口,干净安装后不依赖历史 dist。
|
|
30
|
+
|
|
31
|
+
模块装配与动态导航约定见 [workspace 文档](../../../README.md)。
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// ../../core/src/pages.ts
|
|
2
|
+
var corePages = {
|
|
3
|
+
"pages/index/index": {
|
|
4
|
+
style: {
|
|
5
|
+
navigationStyle: "custom",
|
|
6
|
+
navigationBarTextStyle: "white",
|
|
7
|
+
navigationBarTitleText: "\u9996\u9875"
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"pages/login/login": { style: { navigationBarTitleText: "\u767B\u5F55" } },
|
|
11
|
+
"pages/login/protocal": { style: { navigationBarTitleText: "\u534F\u8BAE\u8BE6\u60C5" } },
|
|
12
|
+
"pages/webview/webview": {
|
|
13
|
+
style: {
|
|
14
|
+
navigationBarTitleText: "",
|
|
15
|
+
"app-plus": { titleNView: false }
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"pages/status/index": {
|
|
19
|
+
style: {
|
|
20
|
+
navigationStyle: "custom",
|
|
21
|
+
navigationBarTitleText: ""
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// ../../core/src/module.ts
|
|
27
|
+
function defineKratosAppModule(module) {
|
|
28
|
+
return module;
|
|
29
|
+
}
|
|
30
|
+
var coreModule = defineKratosAppModule({
|
|
31
|
+
name: "@liujitcn/kratos-uni-app-core",
|
|
32
|
+
pages: corePages,
|
|
33
|
+
views: {
|
|
34
|
+
HOME: "pages/index/index",
|
|
35
|
+
LOGIN: "pages/login/login",
|
|
36
|
+
PROTOCOL: "pages/login/protocal",
|
|
37
|
+
WEBVIEW: "pages/webview/webview",
|
|
38
|
+
BOOTSTRAP_LOADING: "pages/status/index",
|
|
39
|
+
FORBIDDEN: "pages/status/index",
|
|
40
|
+
NOT_FOUND: "pages/status/index",
|
|
41
|
+
OFFLINE: "pages/status/index",
|
|
42
|
+
CONFIG_ERROR: "pages/status/index",
|
|
43
|
+
PAGE_UNAVAILABLE: "pages/status/index"
|
|
44
|
+
},
|
|
45
|
+
icons: {
|
|
46
|
+
HOME_DEFAULT: "static/tabs/home_default.png",
|
|
47
|
+
HOME_SELECTED: "static/tabs/home_selected.png",
|
|
48
|
+
USER_DEFAULT: "static/tabs/user_default.png",
|
|
49
|
+
USER_SELECTED: "static/tabs/user_selected.png"
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// src/pages.ts
|
|
54
|
+
var systemPages = {
|
|
55
|
+
"pages/my/my": {
|
|
56
|
+
style: {
|
|
57
|
+
navigationStyle: "custom",
|
|
58
|
+
navigationBarTextStyle: "white",
|
|
59
|
+
navigationBarTitleText: "\u6211\u7684"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"pagesMember/settings/settings": { style: { navigationBarTitleText: "\u8BBE\u7F6E" } },
|
|
63
|
+
"pagesMember/ai/index": {
|
|
64
|
+
style: {
|
|
65
|
+
navigationStyle: "custom",
|
|
66
|
+
navigationBarTitleText: "AI \u52A9\u624B"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"pagesMember/profile/profile": {
|
|
70
|
+
style: {
|
|
71
|
+
navigationStyle: "custom",
|
|
72
|
+
navigationBarTextStyle: "white",
|
|
73
|
+
navigationBarTitleText: "\u4E2A\u4EBA\u4FE1\u606F"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// src/index.ts
|
|
79
|
+
var systemModule = defineKratosAppModule({
|
|
80
|
+
name: "@liujitcn/kratos-uni-app-system",
|
|
81
|
+
pages: systemPages,
|
|
82
|
+
views: {
|
|
83
|
+
PROFILE_HOME: "pages/my/my",
|
|
84
|
+
PROFILE: "pagesMember/profile/profile",
|
|
85
|
+
SETTINGS: "pagesMember/settings/settings",
|
|
86
|
+
AI: "pagesMember/ai/index"
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
var src_default = systemModule;
|
|
90
|
+
export {
|
|
91
|
+
src_default as default,
|
|
92
|
+
systemModule
|
|
93
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@liujitcn/kratos-uni-app-system",
|
|
3
|
+
"description": "Kratos uni-app system module with profile, settings, and AI views.",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist",
|
|
8
|
+
"src",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./src/index.ts",
|
|
14
|
+
"import": "./dist/index.mjs",
|
|
15
|
+
"default": "./dist/index.mjs"
|
|
16
|
+
},
|
|
17
|
+
"./views/*": "./src/views/*",
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@dcloudio/uni-app": "3.0.0-5010520260709002",
|
|
25
|
+
"@dcloudio/uni-ui": "^1.4.28",
|
|
26
|
+
"vue": "^3.4.21",
|
|
27
|
+
"@liujitcn/kratos-uni-app-core": "^0.0.1"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"esbuild": "^0.20.2",
|
|
31
|
+
"vue-tsc": "^1.8.8"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "pnpm run build:entries && pnpm pack --pack-destination ../../../dist/npm",
|
|
35
|
+
"build:entries": "esbuild src/index.ts --bundle --platform=node --format=esm --packages=external --outfile=dist/index.mjs",
|
|
36
|
+
"tsc": "vue-tsc --noEmit -p tsconfig.json"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getRequestAccessToken,
|
|
3
|
+
handleAuthExpired,
|
|
4
|
+
http,
|
|
5
|
+
requestBaseURL,
|
|
6
|
+
} from '@liujitcn/kratos-uni-app-core/utils/http'
|
|
7
|
+
import type { ListAiMessageRequest, ListAiMessageResponse } from '../../rpc/base/v1/ai_session'
|
|
8
|
+
import type {
|
|
9
|
+
AiMessageService,
|
|
10
|
+
DeleteAiMessageRequest,
|
|
11
|
+
DeleteAiMessageResponse,
|
|
12
|
+
RegenerateAiMessageRequest,
|
|
13
|
+
RetryAiUserMessageRequest,
|
|
14
|
+
SendAiMessageRequest,
|
|
15
|
+
SendAiMessageResponse,
|
|
16
|
+
UpdateAiMessageRequest,
|
|
17
|
+
} from '../../rpc/base/v1/ai_message'
|
|
18
|
+
|
|
19
|
+
const AI_SESSION_URL = '/v1/base/ai/session'
|
|
20
|
+
|
|
21
|
+
/** 微信小程序 chunked stream 分片处理选项。 */
|
|
22
|
+
export type AiMessageChunkedStreamOptions = {
|
|
23
|
+
onChunk: (chunkText: string) => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 微信小程序 chunked stream 请求任务。 */
|
|
27
|
+
export type AiMessageChunkedStreamTask = {
|
|
28
|
+
promise: Promise<void>
|
|
29
|
+
abort: () => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** direct stream 请求控制选项。 */
|
|
33
|
+
export type AiMessageStreamOptions = {
|
|
34
|
+
/** 外部取消信号,用于页面卸载或会话删除时终止流式请求。 */
|
|
35
|
+
signal?: AbortSignal
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type ChunkReceivedResult = {
|
|
39
|
+
data?: ArrayBuffer
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type ChunkedRequestTask = UniNamespace.RequestTask & {
|
|
43
|
+
onChunkReceived?: (listener: (result: ChunkReceivedResult) => void) => number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 从 direct stream 错误响应中提取后端业务提示。 */
|
|
47
|
+
async function resolveStreamErrorMessage(response: Response): Promise<string> {
|
|
48
|
+
const fallbackMessage = `AI 助手请求失败(${response.status})`
|
|
49
|
+
const contentType = response.headers.get('Content-Type') ?? ''
|
|
50
|
+
if (contentType.includes('application/json')) {
|
|
51
|
+
try {
|
|
52
|
+
const payload = await response.json()
|
|
53
|
+
return String(payload?.message || payload?.error || fallbackMessage)
|
|
54
|
+
} catch {
|
|
55
|
+
return fallbackMessage
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const text = (await response.text()).trim()
|
|
61
|
+
return text || fallbackMessage
|
|
62
|
+
} catch {
|
|
63
|
+
return fallbackMessage
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 使用 direct stream 发送 AI 助手消息,并返回原始 Fetch Response 供调用方消费。 */
|
|
68
|
+
export async function SendAiMessageStream(
|
|
69
|
+
request: SendAiMessageRequest,
|
|
70
|
+
options?: AiMessageStreamOptions,
|
|
71
|
+
): Promise<Response> {
|
|
72
|
+
const accessToken = await getRequestAccessToken()
|
|
73
|
+
const headers: Record<string, string> = {
|
|
74
|
+
Accept: 'text/event-stream',
|
|
75
|
+
'Content-Type': 'application/json;charset=utf-8',
|
|
76
|
+
'source-client': 'miniapp',
|
|
77
|
+
}
|
|
78
|
+
if (accessToken) {
|
|
79
|
+
headers.Authorization = accessToken
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const response = await fetch(`${requestBaseURL}${AI_SESSION_URL}/${request.session_id}/message`, {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
headers,
|
|
85
|
+
body: JSON.stringify(request),
|
|
86
|
+
signal: options?.signal,
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
// direct stream 不经过 uni.request 拦截器,需要在这里补齐登录失效处理。
|
|
90
|
+
if (response.status === 401 || response.status === 403) {
|
|
91
|
+
handleAuthExpired('required')
|
|
92
|
+
throw new Error('登录状态已失效,请重新登录')
|
|
93
|
+
}
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
throw new Error(await resolveStreamErrorMessage(response))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return response
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 使用微信小程序 chunked request 发送 AI 助手消息并增量返回 SSE 文本。 */
|
|
102
|
+
export function StreamAiMessageByChunkedRequest(
|
|
103
|
+
request: SendAiMessageRequest,
|
|
104
|
+
options: AiMessageChunkedStreamOptions,
|
|
105
|
+
): AiMessageChunkedStreamTask {
|
|
106
|
+
let requestTask: ChunkedRequestTask | undefined
|
|
107
|
+
let aborted = false
|
|
108
|
+
let receivedChunk = false
|
|
109
|
+
const decoder = createChunkTextDecoder()
|
|
110
|
+
|
|
111
|
+
const promise = (async () => {
|
|
112
|
+
const accessToken = await getRequestAccessToken()
|
|
113
|
+
if (aborted) {
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await new Promise<void>((resolve, reject) => {
|
|
118
|
+
requestTask = uni.request({
|
|
119
|
+
url: `${requestBaseURL}${AI_SESSION_URL}/${request.session_id}/message`,
|
|
120
|
+
method: 'POST',
|
|
121
|
+
data: request,
|
|
122
|
+
dataType: 'text',
|
|
123
|
+
responseType: 'arraybuffer',
|
|
124
|
+
enableChunked: true,
|
|
125
|
+
timeout: 120000,
|
|
126
|
+
header: {
|
|
127
|
+
Accept: 'text/event-stream',
|
|
128
|
+
'Content-Type': 'application/json;charset=utf-8',
|
|
129
|
+
'source-client': 'miniapp',
|
|
130
|
+
...(accessToken ? { Authorization: accessToken } : {}),
|
|
131
|
+
},
|
|
132
|
+
success(res) {
|
|
133
|
+
if (aborted) {
|
|
134
|
+
resolve()
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
if (res.statusCode === 401 || res.statusCode === 403) {
|
|
138
|
+
handleAuthExpired('required')
|
|
139
|
+
reject(new Error('登录状态已失效,请重新登录'))
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
143
|
+
reject(new Error(resolveChunkedStreamErrorMessage(res)))
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const tailText = decoder.flush()
|
|
148
|
+
if (tailText) {
|
|
149
|
+
options.onChunk(tailText)
|
|
150
|
+
}
|
|
151
|
+
if (!receivedChunk) {
|
|
152
|
+
const fallbackText = decodeChunkedResponseData(res.data)
|
|
153
|
+
if (fallbackText) {
|
|
154
|
+
options.onChunk(fallbackText)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
resolve()
|
|
158
|
+
},
|
|
159
|
+
fail(error) {
|
|
160
|
+
if (aborted) {
|
|
161
|
+
resolve()
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
reject(error)
|
|
165
|
+
},
|
|
166
|
+
}) as ChunkedRequestTask
|
|
167
|
+
|
|
168
|
+
if (typeof requestTask.onChunkReceived !== 'function') {
|
|
169
|
+
requestTask.abort()
|
|
170
|
+
reject(new Error('当前端不支持流式回复'))
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
requestTask.onChunkReceived((result) => {
|
|
175
|
+
if (aborted || !result.data) {
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
receivedChunk = true
|
|
179
|
+
const chunkText = decoder.decode(result.data)
|
|
180
|
+
if (chunkText) {
|
|
181
|
+
options.onChunk(chunkText)
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
})()
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
promise,
|
|
189
|
+
abort() {
|
|
190
|
+
aborted = true
|
|
191
|
+
requestTask?.abort()
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** AI 助手消息服务。 */
|
|
197
|
+
export class AiMessageServiceImpl implements AiMessageService {
|
|
198
|
+
/** 查询 AI 助手消息列表。 */
|
|
199
|
+
ListAiMessage(request: ListAiMessageRequest): Promise<ListAiMessageResponse> {
|
|
200
|
+
return http<ListAiMessageResponse>({
|
|
201
|
+
url: `${AI_SESSION_URL}/${request.session_id}/message`,
|
|
202
|
+
method: 'GET',
|
|
203
|
+
authMode: 'required',
|
|
204
|
+
data: request,
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** 发送 AI 助手消息并等待完整响应。 */
|
|
209
|
+
SendAiMessage(request: SendAiMessageRequest): Promise<SendAiMessageResponse> {
|
|
210
|
+
return http<SendAiMessageResponse>({
|
|
211
|
+
url: `${AI_SESSION_URL}/${request.session_id}/message`,
|
|
212
|
+
method: 'POST',
|
|
213
|
+
authMode: 'required',
|
|
214
|
+
data: request,
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** 删除 AI 助手消息。 */
|
|
219
|
+
DeleteAiMessage(request: DeleteAiMessageRequest): Promise<DeleteAiMessageResponse> {
|
|
220
|
+
return http<DeleteAiMessageResponse>({
|
|
221
|
+
url: `${AI_SESSION_URL}/${request.session_id}/message/${request.message_id}`,
|
|
222
|
+
method: 'DELETE',
|
|
223
|
+
authMode: 'required',
|
|
224
|
+
data: request,
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** 更新 AI 助手消息文本并重新生成输出。 */
|
|
229
|
+
UpdateAiMessage(request: UpdateAiMessageRequest): Promise<SendAiMessageResponse> {
|
|
230
|
+
return http<SendAiMessageResponse>({
|
|
231
|
+
url: `${AI_SESSION_URL}/${request.session_id}/message/${request.message_id}`,
|
|
232
|
+
method: 'PUT',
|
|
233
|
+
authMode: 'required',
|
|
234
|
+
data: request,
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** 重试失败的 AI 助手消息。 */
|
|
239
|
+
RetryAiUserMessage(request: RetryAiUserMessageRequest): Promise<SendAiMessageResponse> {
|
|
240
|
+
return http<SendAiMessageResponse>({
|
|
241
|
+
url: `${AI_SESSION_URL}/${request.session_id}/message/${request.message_id}/retry`,
|
|
242
|
+
method: 'POST',
|
|
243
|
+
authMode: 'required',
|
|
244
|
+
data: request,
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** 重新生成 AI 助手输出。 */
|
|
249
|
+
RegenerateAiMessage(request: RegenerateAiMessageRequest): Promise<SendAiMessageResponse> {
|
|
250
|
+
return http<SendAiMessageResponse>({
|
|
251
|
+
url: `${AI_SESSION_URL}/${request.session_id}/message/${request.message_id}/regeneration`,
|
|
252
|
+
method: 'POST',
|
|
253
|
+
authMode: 'required',
|
|
254
|
+
data: request,
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** 使用 direct stream 发送 AI 助手消息,并返回原始 Fetch Response 供调用方消费。 */
|
|
259
|
+
StreamAiMessage(
|
|
260
|
+
request: SendAiMessageRequest,
|
|
261
|
+
options?: AiMessageStreamOptions,
|
|
262
|
+
): Promise<Response> {
|
|
263
|
+
return SendAiMessageStream(request, options)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export const defAiMessageService = new AiMessageServiceImpl()
|
|
268
|
+
|
|
269
|
+
function resolveChunkedStreamErrorMessage(response: UniApp.RequestSuccessCallbackResult) {
|
|
270
|
+
const fallbackMessage = `AI 助手请求失败(${response.statusCode})`
|
|
271
|
+
const text = decodeChunkedResponseData(response.data).trim()
|
|
272
|
+
if (!text) {
|
|
273
|
+
return fallbackMessage
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
const payload = JSON.parse(text) as { message?: string; error?: string }
|
|
278
|
+
return payload.message || payload.error || fallbackMessage
|
|
279
|
+
} catch {
|
|
280
|
+
return text || fallbackMessage
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function decodeChunkedResponseData(data: unknown) {
|
|
285
|
+
if (typeof data === 'string') {
|
|
286
|
+
return data
|
|
287
|
+
}
|
|
288
|
+
if (data instanceof ArrayBuffer) {
|
|
289
|
+
return createChunkTextDecoder().decode(data)
|
|
290
|
+
}
|
|
291
|
+
if (data && typeof data === 'object') {
|
|
292
|
+
return JSON.stringify(data)
|
|
293
|
+
}
|
|
294
|
+
return ''
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function createChunkTextDecoder() {
|
|
298
|
+
if (typeof TextDecoder !== 'undefined') {
|
|
299
|
+
const decoder = new TextDecoder('utf-8')
|
|
300
|
+
return {
|
|
301
|
+
decode(data: ArrayBuffer) {
|
|
302
|
+
return decoder.decode(data, { stream: true })
|
|
303
|
+
},
|
|
304
|
+
flush() {
|
|
305
|
+
return decoder.decode()
|
|
306
|
+
},
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let pendingBytes: number[] = []
|
|
311
|
+
return {
|
|
312
|
+
decode(data: ArrayBuffer) {
|
|
313
|
+
const bytes = [...pendingBytes, ...Array.from(new Uint8Array(data))]
|
|
314
|
+
const boundary = resolveUtf8Boundary(bytes)
|
|
315
|
+
pendingBytes = bytes.slice(boundary)
|
|
316
|
+
return decodeUtf8Bytes(bytes.slice(0, boundary))
|
|
317
|
+
},
|
|
318
|
+
flush() {
|
|
319
|
+
const text = decodeUtf8Bytes(pendingBytes)
|
|
320
|
+
pendingBytes = []
|
|
321
|
+
return text
|
|
322
|
+
},
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function resolveUtf8Boundary(bytes: number[]) {
|
|
327
|
+
let index = bytes.length - 1
|
|
328
|
+
while (index >= 0 && (bytes[index] & 0xc0) === 0x80) {
|
|
329
|
+
index--
|
|
330
|
+
}
|
|
331
|
+
if (index < 0) {
|
|
332
|
+
return 0
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const length = resolveUtf8SequenceLength(bytes[index])
|
|
336
|
+
if (length > 1 && bytes.length - index < length) {
|
|
337
|
+
return index
|
|
338
|
+
}
|
|
339
|
+
return bytes.length
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function resolveUtf8SequenceLength(lead: number) {
|
|
343
|
+
if ((lead & 0x80) === 0) {
|
|
344
|
+
return 1
|
|
345
|
+
}
|
|
346
|
+
if ((lead & 0xe0) === 0xc0) {
|
|
347
|
+
return 2
|
|
348
|
+
}
|
|
349
|
+
if ((lead & 0xf0) === 0xe0) {
|
|
350
|
+
return 3
|
|
351
|
+
}
|
|
352
|
+
if ((lead & 0xf8) === 0xf0) {
|
|
353
|
+
return 4
|
|
354
|
+
}
|
|
355
|
+
return 1
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function decodeUtf8Bytes(bytes: number[]) {
|
|
359
|
+
if (!bytes.length) {
|
|
360
|
+
return ''
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
let encoded = ''
|
|
364
|
+
for (const byte of bytes) {
|
|
365
|
+
encoded += `%${byte.toString(16).padStart(2, '0')}`
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
return decodeURIComponent(encoded)
|
|
370
|
+
} catch {
|
|
371
|
+
return String.fromCharCode(...bytes)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { http } from '@liujitcn/kratos-uni-app-core/utils/http'
|
|
2
|
+
import type {
|
|
3
|
+
AiSessionService,
|
|
4
|
+
CreateAiSessionBranchRequest,
|
|
5
|
+
CreateAiSessionBranchResponse,
|
|
6
|
+
CreateAiSessionRequest,
|
|
7
|
+
CreateAiSessionResponse,
|
|
8
|
+
DeleteAiSessionRequest,
|
|
9
|
+
DeleteAiSessionResponse,
|
|
10
|
+
ListAiMessageRequest,
|
|
11
|
+
ListAiMessageResponse,
|
|
12
|
+
ListAiSessionRequest,
|
|
13
|
+
ListAiSessionResponse,
|
|
14
|
+
UpdateAiSessionRequest,
|
|
15
|
+
UpdateAiSessionResponse,
|
|
16
|
+
} from '../../rpc/base/v1/ai_session'
|
|
17
|
+
|
|
18
|
+
const AI_SESSION_URL = '/v1/base/ai/session'
|
|
19
|
+
|
|
20
|
+
/** AI 助手会话服务。 */
|
|
21
|
+
export class AiSessionServiceImpl implements AiSessionService {
|
|
22
|
+
/** 查询 AI 助手会话列表。 */
|
|
23
|
+
ListAiSession(request: ListAiSessionRequest): Promise<ListAiSessionResponse> {
|
|
24
|
+
return http<ListAiSessionResponse>({
|
|
25
|
+
url: AI_SESSION_URL,
|
|
26
|
+
method: 'GET',
|
|
27
|
+
authMode: 'required',
|
|
28
|
+
data: request,
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 创建 AI 助手会话。 */
|
|
33
|
+
CreateAiSession(request: CreateAiSessionRequest): Promise<CreateAiSessionResponse> {
|
|
34
|
+
return http<CreateAiSessionResponse>({
|
|
35
|
+
url: AI_SESSION_URL,
|
|
36
|
+
method: 'POST',
|
|
37
|
+
authMode: 'required',
|
|
38
|
+
data: request,
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 更新 AI 助手会话。 */
|
|
43
|
+
UpdateAiSession(request: UpdateAiSessionRequest): Promise<UpdateAiSessionResponse> {
|
|
44
|
+
return http<UpdateAiSessionResponse>({
|
|
45
|
+
url: `${AI_SESSION_URL}/${request.id}`,
|
|
46
|
+
method: 'PUT',
|
|
47
|
+
authMode: 'required',
|
|
48
|
+
data: request,
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 删除 AI 助手会话。 */
|
|
53
|
+
DeleteAiSession(request: DeleteAiSessionRequest): Promise<DeleteAiSessionResponse> {
|
|
54
|
+
return http<DeleteAiSessionResponse>({
|
|
55
|
+
url: `${AI_SESSION_URL}/${request.id}`,
|
|
56
|
+
method: 'DELETE',
|
|
57
|
+
authMode: 'required',
|
|
58
|
+
data: request,
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 查询 AI 助手消息列表。 */
|
|
63
|
+
ListAiMessage(request: ListAiMessageRequest): Promise<ListAiMessageResponse> {
|
|
64
|
+
return http<ListAiMessageResponse>({
|
|
65
|
+
url: `${AI_SESSION_URL}/${request.session_id}/message`,
|
|
66
|
+
method: 'GET',
|
|
67
|
+
authMode: 'required',
|
|
68
|
+
data: request,
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 从指定消息创建 AI 助手分支会话。 */
|
|
73
|
+
CreateAiSessionBranch(
|
|
74
|
+
request: CreateAiSessionBranchRequest,
|
|
75
|
+
): Promise<CreateAiSessionBranchResponse> {
|
|
76
|
+
return http<CreateAiSessionBranchResponse>({
|
|
77
|
+
url: `${AI_SESSION_URL}/${request.source_session_id}/branch`,
|
|
78
|
+
method: 'POST',
|
|
79
|
+
authMode: 'required',
|
|
80
|
+
data: request,
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const defAiSessionService = new AiSessionServiceImpl()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { http } from '@liujitcn/kratos-uni-app-core/utils/http'
|
|
2
|
+
import type {
|
|
3
|
+
AiToolService,
|
|
4
|
+
ListAiShortcutRequest,
|
|
5
|
+
ListAiShortcutResponse,
|
|
6
|
+
} from '../../rpc/base/v1/ai_tool'
|
|
7
|
+
|
|
8
|
+
const AI_SHORTCUT_URL = '/v1/base/ai/shortcut'
|
|
9
|
+
|
|
10
|
+
/** AI 助手工具服务。 */
|
|
11
|
+
export class AiToolServiceImpl implements AiToolService {
|
|
12
|
+
/** 查询 AI 助手快捷入口列表。 */
|
|
13
|
+
ListAiShortcut(request: ListAiShortcutRequest): Promise<ListAiShortcutResponse> {
|
|
14
|
+
return http<ListAiShortcutResponse>({
|
|
15
|
+
url: AI_SHORTCUT_URL,
|
|
16
|
+
method: 'GET',
|
|
17
|
+
authMode: 'required',
|
|
18
|
+
data: request,
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const defAiToolService = new AiToolServiceImpl()
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineKratosAppModule } from '@liujitcn/kratos-uni-app-core/module'
|
|
2
|
+
import { systemPages } from './pages'
|
|
3
|
+
|
|
4
|
+
/** system 内置模块,AI 能力也归属 system。 */
|
|
5
|
+
export const systemModule = defineKratosAppModule({
|
|
6
|
+
name: '@liujitcn/kratos-uni-app-system',
|
|
7
|
+
pages: systemPages,
|
|
8
|
+
views: {
|
|
9
|
+
PROFILE_HOME: 'pages/my/my',
|
|
10
|
+
PROFILE: 'pagesMember/profile/profile',
|
|
11
|
+
SETTINGS: 'pagesMember/settings/settings',
|
|
12
|
+
AI: 'pagesMember/ai/index',
|
|
13
|
+
},
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export default systemModule
|
package/src/pages.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { KratosAppPageConfig } from '@liujitcn/kratos-uni-app-core/module'
|
|
2
|
+
|
|
3
|
+
/** system 页面编译配置。 */
|
|
4
|
+
export const systemPages: Record<string, KratosAppPageConfig> = {
|
|
5
|
+
'pages/my/my': {
|
|
6
|
+
style: {
|
|
7
|
+
navigationStyle: 'custom',
|
|
8
|
+
navigationBarTextStyle: 'white',
|
|
9
|
+
navigationBarTitleText: '我的',
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
'pagesMember/settings/settings': { style: { navigationBarTitleText: '设置' } },
|
|
13
|
+
'pagesMember/ai/index': {
|
|
14
|
+
style: {
|
|
15
|
+
navigationStyle: 'custom',
|
|
16
|
+
navigationBarTitleText: 'AI 助手',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
'pagesMember/profile/profile': {
|
|
20
|
+
style: {
|
|
21
|
+
navigationStyle: 'custom',
|
|
22
|
+
navigationBarTextStyle: 'white',
|
|
23
|
+
navigationBarTitleText: '个人信息',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
}
|