@liujitcn/kratos-taro-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 +29 -0
- package/dist/build.mjs +40 -0
- package/package.json +36 -0
- package/src/api/base/ai_message.ts +377 -0
- package/src/api/base/ai_session.ts +85 -0
- package/src/api/base/ai_tool.ts +23 -0
- package/src/build.ts +13 -0
- package/src/index.ts +14 -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/types/assets.d.ts +4 -0
- package/src/views/pages/my/my.scss +134 -0
- package/src/views/pages/my/my.tsx +82 -0
- package/src/views/pagesMember/ai/components/Composer.tsx +71 -0
- package/src/views/pagesMember/ai/components/SessionDrawer.tsx +100 -0
- package/src/views/pagesMember/ai/components/WelcomePanel.tsx +75 -0
- package/src/views/pagesMember/ai/components/composer.scss +114 -0
- package/src/views/pagesMember/ai/components/session-drawer.scss +135 -0
- package/src/views/pagesMember/ai/components/welcome-panel.scss +284 -0
- package/src/views/pagesMember/ai/index.scss +130 -0
- package/src/views/pagesMember/ai/index.tsx +945 -0
- package/src/views/pagesMember/ai/stream.ts +152 -0
- package/src/views/pagesMember/profile/profile.scss +135 -0
- package/src/views/pagesMember/profile/profile.tsx +159 -0
- package/src/views/pagesMember/settings/settings.scss +56 -0
- package/src/views/pagesMember/settings/settings.tsx +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# @liujitcn/kratos-taro-app-system
|
|
2
|
+
|
|
3
|
+
`@liujitcn/kratos-taro-app-system` 是默认应用业务模块,提供个人中心、设置、个人资料和 AI 助手页面。模块只依赖 core 与 UI 的公开入口,不持有宿主配置。
|
|
4
|
+
|
|
5
|
+
## 页面与视图键
|
|
6
|
+
|
|
7
|
+
| 页面 | 视图键 | 用途 |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| `pages/my/my` | `PROFILE_HOME` | 我的 tab 与登录概览。 |
|
|
10
|
+
| `pagesMember/profile/profile` | `PROFILE` | 头像、昵称、性别和地区资料。 |
|
|
11
|
+
| `pagesMember/settings/settings` | `SETTINGS` | 设置与退出登录。 |
|
|
12
|
+
| `pagesMember/ai/index` | `AI` | AI 会话、流式消息、附件和快捷入口。 |
|
|
13
|
+
|
|
14
|
+
页面配置统一登记在 `src/pages.ts`,运行时映射位于 `src/index.ts`,构建期描述位于 `src/build.ts`。页面私有组件放在页面目录的 `components` 中,不会被 runner 扫描为路由。
|
|
15
|
+
|
|
16
|
+
## AI 助手
|
|
17
|
+
|
|
18
|
+
AI 页面支持会话创建与切换、历史会话搜索、消息加载、附件上传、复制、删除、再生成、工具提示和快捷入口。H5 使用 Fetch SSE,微信小程序使用 chunked 请求,两端共用增量 SSE 解析器和相同事件契约。
|
|
19
|
+
|
|
20
|
+
请求统一封装在 `src/api`,RPC 类型由 backend Buf 模板生成。页面不能直接写 `Taro.request`,也不能手工修改 `src/rpc`。
|
|
21
|
+
|
|
22
|
+
## 验证
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pnpm --filter @liujitcn/kratos-taro-app-system tsc
|
|
26
|
+
pnpm test
|
|
27
|
+
pnpm build:h5
|
|
28
|
+
pnpm build:mp-weixin
|
|
29
|
+
```
|
package/dist/build.mjs
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// src/build.ts
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { defineKratosTaroBuildModule } from "@liujitcn/kratos-taro-app-core/build";
|
|
5
|
+
|
|
6
|
+
// src/pages.ts
|
|
7
|
+
var systemPages = {
|
|
8
|
+
"pages/my/my": {
|
|
9
|
+
style: {
|
|
10
|
+
navigationStyle: "custom",
|
|
11
|
+
navigationBarTextStyle: "white",
|
|
12
|
+
navigationBarTitleText: "\u6211\u7684"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"pagesMember/settings/settings": { style: { navigationBarTitleText: "\u8BBE\u7F6E" } },
|
|
16
|
+
"pagesMember/ai/index": {
|
|
17
|
+
style: {
|
|
18
|
+
navigationStyle: "custom",
|
|
19
|
+
navigationBarTitleText: "AI \u52A9\u624B"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"pagesMember/profile/profile": {
|
|
23
|
+
style: {
|
|
24
|
+
navigationStyle: "custom",
|
|
25
|
+
navigationBarTextStyle: "white",
|
|
26
|
+
navigationBarTitleText: "\u4E2A\u4EBA\u4FE1\u606F"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// src/build.ts
|
|
32
|
+
var packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
33
|
+
var buildModule = defineKratosTaroBuildModule({
|
|
34
|
+
name: "@liujitcn/kratos-taro-app-system",
|
|
35
|
+
root: packageRoot,
|
|
36
|
+
pages: systemPages
|
|
37
|
+
});
|
|
38
|
+
export {
|
|
39
|
+
buildModule
|
|
40
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@liujitcn/kratos-taro-app-system",
|
|
3
|
+
"description": "Kratos Taro 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
|
+
".": "./src/index.ts",
|
|
13
|
+
"./build": {
|
|
14
|
+
"types": "./src/build.ts",
|
|
15
|
+
"import": "./dist/build.mjs",
|
|
16
|
+
"default": "./dist/build.mjs"
|
|
17
|
+
},
|
|
18
|
+
"./views/*": "./src/views/*",
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@tarojs/components": "4.2.1",
|
|
26
|
+
"@tarojs/taro": "4.2.1",
|
|
27
|
+
"react": "18.3.1",
|
|
28
|
+
"@liujitcn/kratos-taro-app-core": "^0.0.1",
|
|
29
|
+
"@liujitcn/kratos-taro-app-ui": "^0.0.1"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "pnpm run build:entries && pnpm pack --pack-destination ../../../dist/npm",
|
|
33
|
+
"build:entries": "esbuild src/build.ts --bundle --platform=node --format=esm --packages=external --outfile=dist/build.mjs",
|
|
34
|
+
"tsc": "tsc --noEmit -p tsconfig.json"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getRequestAccessToken,
|
|
3
|
+
handleAuthExpired,
|
|
4
|
+
http,
|
|
5
|
+
requestBaseURL,
|
|
6
|
+
} from '@liujitcn/kratos-taro-app-core/utils/http'
|
|
7
|
+
import Taro from '@tarojs/taro'
|
|
8
|
+
import type { ListAiMessageRequest, ListAiMessageResponse } from '../../rpc/base/v1/ai_session'
|
|
9
|
+
import type {
|
|
10
|
+
AiMessageService,
|
|
11
|
+
DeleteAiMessageRequest,
|
|
12
|
+
DeleteAiMessageResponse,
|
|
13
|
+
RegenerateAiMessageRequest,
|
|
14
|
+
RetryAiUserMessageRequest,
|
|
15
|
+
SendAiMessageRequest,
|
|
16
|
+
SendAiMessageResponse,
|
|
17
|
+
UpdateAiMessageRequest,
|
|
18
|
+
} from '../../rpc/base/v1/ai_message'
|
|
19
|
+
|
|
20
|
+
const AI_SESSION_URL = '/v1/base/ai/session'
|
|
21
|
+
|
|
22
|
+
/** 微信小程序 chunked stream 分片处理选项。 */
|
|
23
|
+
export type AiMessageChunkedStreamOptions = {
|
|
24
|
+
onChunk: (chunkText: string) => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 微信小程序 chunked stream 请求任务。 */
|
|
28
|
+
export type AiMessageChunkedStreamTask = {
|
|
29
|
+
promise: Promise<void>
|
|
30
|
+
abort: () => void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** direct stream 请求控制选项。 */
|
|
34
|
+
export type AiMessageStreamOptions = {
|
|
35
|
+
/** 外部取消信号,用于页面卸载或会话删除时终止流式请求。 */
|
|
36
|
+
signal?: AbortSignal
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type ChunkReceivedResult = {
|
|
40
|
+
data?: ArrayBuffer
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type ChunkedRequestTask = Promise<Taro.request.SuccessCallbackResult<ArrayBuffer>> & {
|
|
44
|
+
abort: () => void
|
|
45
|
+
onChunkReceived: (listener: (result: ChunkReceivedResult) => void) => void
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 从 direct stream 错误响应中提取后端业务提示。 */
|
|
49
|
+
async function resolveStreamErrorMessage(response: Response): Promise<string> {
|
|
50
|
+
const fallbackMessage = `AI 助手请求失败(${response.status})`
|
|
51
|
+
const contentType = response.headers.get('Content-Type') ?? ''
|
|
52
|
+
if (contentType.includes('application/json')) {
|
|
53
|
+
try {
|
|
54
|
+
const payload = await response.json()
|
|
55
|
+
return String(payload?.message || payload?.error || fallbackMessage)
|
|
56
|
+
} catch {
|
|
57
|
+
return fallbackMessage
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const text = (await response.text()).trim()
|
|
63
|
+
return text || fallbackMessage
|
|
64
|
+
} catch {
|
|
65
|
+
return fallbackMessage
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 使用 direct stream 发送 AI 助手消息,并返回原始 Fetch Response 供调用方消费。 */
|
|
70
|
+
export async function SendAiMessageStream(
|
|
71
|
+
request: SendAiMessageRequest,
|
|
72
|
+
options?: AiMessageStreamOptions,
|
|
73
|
+
): Promise<Response> {
|
|
74
|
+
const accessToken = await getRequestAccessToken()
|
|
75
|
+
const headers: Record<string, string> = {
|
|
76
|
+
Accept: 'text/event-stream',
|
|
77
|
+
'Content-Type': 'application/json;charset=utf-8',
|
|
78
|
+
'source-client': 'miniapp',
|
|
79
|
+
}
|
|
80
|
+
if (accessToken) {
|
|
81
|
+
headers.Authorization = accessToken
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const response = await fetch(`${requestBaseURL}${AI_SESSION_URL}/${request.session_id}/message`, {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers,
|
|
87
|
+
body: JSON.stringify(request),
|
|
88
|
+
signal: options?.signal,
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// direct stream 不经过 uni.request 拦截器,需要在这里补齐登录失效处理。
|
|
92
|
+
if (response.status === 401 || response.status === 403) {
|
|
93
|
+
handleAuthExpired('required')
|
|
94
|
+
throw new Error('登录状态已失效,请重新登录')
|
|
95
|
+
}
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
throw new Error(await resolveStreamErrorMessage(response))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return response
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 使用微信小程序 chunked request 发送 AI 助手消息并增量返回 SSE 文本。 */
|
|
104
|
+
export function StreamAiMessageByChunkedRequest(
|
|
105
|
+
request: SendAiMessageRequest,
|
|
106
|
+
options: AiMessageChunkedStreamOptions,
|
|
107
|
+
): AiMessageChunkedStreamTask {
|
|
108
|
+
let requestTask: ChunkedRequestTask | undefined
|
|
109
|
+
let aborted = false
|
|
110
|
+
let receivedChunk = false
|
|
111
|
+
const decoder = createChunkTextDecoder()
|
|
112
|
+
|
|
113
|
+
const promise = (async () => {
|
|
114
|
+
const accessToken = await getRequestAccessToken()
|
|
115
|
+
if (aborted) {
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await new Promise<void>((resolve, reject) => {
|
|
120
|
+
requestTask = Taro.request<ArrayBuffer>({
|
|
121
|
+
url: `${requestBaseURL}${AI_SESSION_URL}/${request.session_id}/message`,
|
|
122
|
+
method: 'POST',
|
|
123
|
+
data: request,
|
|
124
|
+
dataType: 'text',
|
|
125
|
+
responseType: 'arraybuffer',
|
|
126
|
+
enableChunked: true,
|
|
127
|
+
timeout: 120000,
|
|
128
|
+
header: {
|
|
129
|
+
Accept: 'text/event-stream',
|
|
130
|
+
'Content-Type': 'application/json;charset=utf-8',
|
|
131
|
+
'source-client': 'miniapp',
|
|
132
|
+
...(accessToken ? { Authorization: accessToken } : {}),
|
|
133
|
+
},
|
|
134
|
+
success(res) {
|
|
135
|
+
if (aborted) {
|
|
136
|
+
resolve()
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
if (res.statusCode === 401 || res.statusCode === 403) {
|
|
140
|
+
handleAuthExpired('required')
|
|
141
|
+
reject(new Error('登录状态已失效,请重新登录'))
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
145
|
+
reject(new Error(resolveChunkedStreamErrorMessage(res)))
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const tailText = decoder.flush()
|
|
150
|
+
if (tailText) {
|
|
151
|
+
options.onChunk(tailText)
|
|
152
|
+
}
|
|
153
|
+
if (!receivedChunk) {
|
|
154
|
+
const fallbackText = decodeChunkedResponseData(res.data)
|
|
155
|
+
if (fallbackText) {
|
|
156
|
+
options.onChunk(fallbackText)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
resolve()
|
|
160
|
+
},
|
|
161
|
+
fail(error) {
|
|
162
|
+
if (aborted) {
|
|
163
|
+
resolve()
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
reject(error)
|
|
167
|
+
},
|
|
168
|
+
}) as ChunkedRequestTask
|
|
169
|
+
|
|
170
|
+
if (typeof requestTask.onChunkReceived !== 'function') {
|
|
171
|
+
requestTask.abort()
|
|
172
|
+
reject(new Error('当前端不支持流式回复'))
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
requestTask.onChunkReceived((result) => {
|
|
177
|
+
if (aborted || !result.data) {
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
receivedChunk = true
|
|
181
|
+
const chunkText = decoder.decode(result.data)
|
|
182
|
+
if (chunkText) {
|
|
183
|
+
options.onChunk(chunkText)
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
})()
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
promise,
|
|
191
|
+
abort() {
|
|
192
|
+
aborted = true
|
|
193
|
+
requestTask?.abort()
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** AI 助手消息服务。 */
|
|
199
|
+
export class AiMessageServiceImpl implements AiMessageService {
|
|
200
|
+
/** 查询 AI 助手消息列表。 */
|
|
201
|
+
ListAiMessage(request: ListAiMessageRequest): Promise<ListAiMessageResponse> {
|
|
202
|
+
return http<ListAiMessageResponse>({
|
|
203
|
+
url: `${AI_SESSION_URL}/${request.session_id}/message`,
|
|
204
|
+
method: 'GET',
|
|
205
|
+
authMode: 'required',
|
|
206
|
+
data: request,
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** 发送 AI 助手消息并等待完整响应。 */
|
|
211
|
+
SendAiMessage(request: SendAiMessageRequest): Promise<SendAiMessageResponse> {
|
|
212
|
+
return http<SendAiMessageResponse>({
|
|
213
|
+
url: `${AI_SESSION_URL}/${request.session_id}/message`,
|
|
214
|
+
method: 'POST',
|
|
215
|
+
authMode: 'required',
|
|
216
|
+
data: request,
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** 删除 AI 助手消息。 */
|
|
221
|
+
DeleteAiMessage(request: DeleteAiMessageRequest): Promise<DeleteAiMessageResponse> {
|
|
222
|
+
return http<DeleteAiMessageResponse>({
|
|
223
|
+
url: `${AI_SESSION_URL}/${request.session_id}/message/${request.message_id}`,
|
|
224
|
+
method: 'DELETE',
|
|
225
|
+
authMode: 'required',
|
|
226
|
+
data: request,
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** 更新 AI 助手消息文本并重新生成输出。 */
|
|
231
|
+
UpdateAiMessage(request: UpdateAiMessageRequest): Promise<SendAiMessageResponse> {
|
|
232
|
+
return http<SendAiMessageResponse>({
|
|
233
|
+
url: `${AI_SESSION_URL}/${request.session_id}/message/${request.message_id}`,
|
|
234
|
+
method: 'PUT',
|
|
235
|
+
authMode: 'required',
|
|
236
|
+
data: request,
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** 重试失败的 AI 助手消息。 */
|
|
241
|
+
RetryAiUserMessage(request: RetryAiUserMessageRequest): Promise<SendAiMessageResponse> {
|
|
242
|
+
return http<SendAiMessageResponse>({
|
|
243
|
+
url: `${AI_SESSION_URL}/${request.session_id}/message/${request.message_id}/retry`,
|
|
244
|
+
method: 'POST',
|
|
245
|
+
authMode: 'required',
|
|
246
|
+
data: request,
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** 重新生成 AI 助手输出。 */
|
|
251
|
+
RegenerateAiMessage(request: RegenerateAiMessageRequest): Promise<SendAiMessageResponse> {
|
|
252
|
+
return http<SendAiMessageResponse>({
|
|
253
|
+
url: `${AI_SESSION_URL}/${request.session_id}/message/${request.message_id}/regeneration`,
|
|
254
|
+
method: 'POST',
|
|
255
|
+
authMode: 'required',
|
|
256
|
+
data: request,
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** 使用 direct stream 发送 AI 助手消息,并返回原始 Fetch Response 供调用方消费。 */
|
|
261
|
+
StreamAiMessage(
|
|
262
|
+
request: SendAiMessageRequest,
|
|
263
|
+
options?: AiMessageStreamOptions,
|
|
264
|
+
): Promise<Response> {
|
|
265
|
+
return SendAiMessageStream(request, options)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export const defAiMessageService = new AiMessageServiceImpl()
|
|
270
|
+
|
|
271
|
+
function resolveChunkedStreamErrorMessage(
|
|
272
|
+
response: Taro.request.SuccessCallbackResult<ArrayBuffer>,
|
|
273
|
+
) {
|
|
274
|
+
const fallbackMessage = `AI 助手请求失败(${response.statusCode})`
|
|
275
|
+
const text = decodeChunkedResponseData(response.data).trim()
|
|
276
|
+
if (!text) {
|
|
277
|
+
return fallbackMessage
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
const payload = JSON.parse(text) as { message?: string; error?: string }
|
|
282
|
+
return payload.message || payload.error || fallbackMessage
|
|
283
|
+
} catch {
|
|
284
|
+
return text || fallbackMessage
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function decodeChunkedResponseData(data: unknown) {
|
|
289
|
+
if (typeof data === 'string') {
|
|
290
|
+
return data
|
|
291
|
+
}
|
|
292
|
+
if (data instanceof ArrayBuffer) {
|
|
293
|
+
return createChunkTextDecoder().decode(data)
|
|
294
|
+
}
|
|
295
|
+
if (data && typeof data === 'object') {
|
|
296
|
+
return JSON.stringify(data)
|
|
297
|
+
}
|
|
298
|
+
return ''
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function createChunkTextDecoder() {
|
|
302
|
+
if (typeof TextDecoder !== 'undefined') {
|
|
303
|
+
const decoder = new TextDecoder('utf-8')
|
|
304
|
+
return {
|
|
305
|
+
decode(data: ArrayBuffer) {
|
|
306
|
+
return decoder.decode(data, { stream: true })
|
|
307
|
+
},
|
|
308
|
+
flush() {
|
|
309
|
+
return decoder.decode()
|
|
310
|
+
},
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
let pendingBytes: number[] = []
|
|
315
|
+
return {
|
|
316
|
+
decode(data: ArrayBuffer) {
|
|
317
|
+
const bytes = [...pendingBytes, ...Array.from(new Uint8Array(data))]
|
|
318
|
+
const boundary = resolveUtf8Boundary(bytes)
|
|
319
|
+
pendingBytes = bytes.slice(boundary)
|
|
320
|
+
return decodeUtf8Bytes(bytes.slice(0, boundary))
|
|
321
|
+
},
|
|
322
|
+
flush() {
|
|
323
|
+
const text = decodeUtf8Bytes(pendingBytes)
|
|
324
|
+
pendingBytes = []
|
|
325
|
+
return text
|
|
326
|
+
},
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function resolveUtf8Boundary(bytes: number[]) {
|
|
331
|
+
let index = bytes.length - 1
|
|
332
|
+
while (index >= 0 && (bytes[index] & 0xc0) === 0x80) {
|
|
333
|
+
index--
|
|
334
|
+
}
|
|
335
|
+
if (index < 0) {
|
|
336
|
+
return 0
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const length = resolveUtf8SequenceLength(bytes[index])
|
|
340
|
+
if (length > 1 && bytes.length - index < length) {
|
|
341
|
+
return index
|
|
342
|
+
}
|
|
343
|
+
return bytes.length
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function resolveUtf8SequenceLength(lead: number) {
|
|
347
|
+
if ((lead & 0x80) === 0) {
|
|
348
|
+
return 1
|
|
349
|
+
}
|
|
350
|
+
if ((lead & 0xe0) === 0xc0) {
|
|
351
|
+
return 2
|
|
352
|
+
}
|
|
353
|
+
if ((lead & 0xf0) === 0xe0) {
|
|
354
|
+
return 3
|
|
355
|
+
}
|
|
356
|
+
if ((lead & 0xf8) === 0xf0) {
|
|
357
|
+
return 4
|
|
358
|
+
}
|
|
359
|
+
return 1
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function decodeUtf8Bytes(bytes: number[]) {
|
|
363
|
+
if (!bytes.length) {
|
|
364
|
+
return ''
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
let encoded = ''
|
|
368
|
+
for (const byte of bytes) {
|
|
369
|
+
encoded += `%${byte.toString(16).padStart(2, '0')}`
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
return decodeURIComponent(encoded)
|
|
374
|
+
} catch {
|
|
375
|
+
return String.fromCharCode(...bytes)
|
|
376
|
+
}
|
|
377
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { http } from '@liujitcn/kratos-taro-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-taro-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/build.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { dirname, resolve } from 'node:path'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { defineKratosTaroBuildModule } from '@liujitcn/kratos-taro-app-core/build'
|
|
4
|
+
import { systemPages } from './pages'
|
|
5
|
+
|
|
6
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
7
|
+
|
|
8
|
+
/** system 构建期模块描述。 */
|
|
9
|
+
export const buildModule = defineKratosTaroBuildModule({
|
|
10
|
+
name: '@liujitcn/kratos-taro-app-system',
|
|
11
|
+
root: packageRoot,
|
|
12
|
+
pages: systemPages,
|
|
13
|
+
})
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineKratosTaroModule } from '@liujitcn/kratos-taro-app-core'
|
|
2
|
+
import { systemPages } from './pages'
|
|
3
|
+
|
|
4
|
+
/** system 业务模块。 */
|
|
5
|
+
export const systemModule = defineKratosTaroModule({
|
|
6
|
+
name: '@liujitcn/kratos-taro-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
|
+
})
|
package/src/pages.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { KratosTaroPageConfig } from '@liujitcn/kratos-taro-app-core'
|
|
2
|
+
|
|
3
|
+
/** system 页面编译配置。 */
|
|
4
|
+
export const systemPages: Record<string, KratosTaroPageConfig> = {
|
|
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
|
+
}
|