chore: initialize qiming workspace repository
This commit is contained in:
971
qiming-mobile/docs/Markdown-Custom-Renderer-Guide.md
Normal file
971
qiming-mobile/docs/Markdown-Custom-Renderer-Guide.md
Normal file
@@ -0,0 +1,971 @@
|
||||
# Chat Markdown 自定义渲染元素(组件)实现指南
|
||||
|
||||
## 📋 目录
|
||||
|
||||
- [概述](#概述)
|
||||
- [核心架构](#核心架构)
|
||||
- [快速开始:生成 Markdown 并插入自定义标签](#快速开始生成-markdown-并插入自定义标签)
|
||||
- [实现步骤:创建自定义渲染组件](#实现步骤创建自定义渲染组件)
|
||||
- [现有示例](#现有示例)
|
||||
- [最佳实践](#最佳实践)
|
||||
- [常见问题](#常见问题)
|
||||
|
||||
## 概述
|
||||
|
||||
本项目使用 `mp-html` 组件作为 Markdown 渲染引擎,通过 `markdown-it-container` 插件机制实现自定义元素的渲染。当需要在 Markdown 中渲染自定义组件时(如工具调用、执行过程等),可以通过创建容器块来扩展渲染能力。
|
||||
|
||||
### 核心优势
|
||||
|
||||
- ✅ **插件化设计**:通过 `markdown-it-container` 插件扩展,不影响核心渲染逻辑
|
||||
- ✅ **类型安全**:支持 UTS/TypeScript 类型定义
|
||||
- ✅ **灵活扩展**:可以自定义任意容器标签的渲染
|
||||
- ✅ **动态数据**:支持通过 `processingList` 传递动态数据,实现实时更新
|
||||
- ✅ **多端支持**:基于 uni-app-x,支持 H5、小程序、App 多端
|
||||
|
||||
## 核心架构
|
||||
|
||||
### 技术栈
|
||||
|
||||
- **渲染引擎**:`mp-html` 组件(基于 `markdown-it`)
|
||||
- **插件系统**:`markdown-it-container`
|
||||
- **容器组件**:`container.vue` 自定义组件
|
||||
- **数据处理**:`processingList` + `getProcessingDataByPriority`
|
||||
|
||||
### 架构图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Markdown 文本 │
|
||||
│ (包含 :::container 语法) │
|
||||
└──────────────┬──────────────────────────┘
|
||||
│
|
||||
│ markdown-it 解析
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ markdown-it-container 插件 │
|
||||
│ - 识别 :::container 语法 │
|
||||
│ - 转换为 <container> 标签 │
|
||||
└──────────────┬──────────────────────────┘
|
||||
│
|
||||
│ 生成 HTML 节点
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ mp-html 组件 │
|
||||
│ (uni_modules/mp-html) │
|
||||
└──────────────┬──────────────────────────┘
|
||||
│
|
||||
│ 识别 container 标签
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ node.vue 组件 │
|
||||
│ - 识别 <container> 标签 │
|
||||
│ - 调用 getRenderData() │
|
||||
│ - 合并 processingList 数据 │
|
||||
└──────────────┬──────────────────────────┘
|
||||
│
|
||||
│ 传递合并后的数据
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ container.vue 组件 │
|
||||
│ - 渲染工具调用状态 │
|
||||
│ - 显示执行详情 │
|
||||
│ - 处理用户交互 │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 数据流
|
||||
|
||||
```
|
||||
Markdown 文本
|
||||
↓
|
||||
:::container executeId="xxx" type="Page" status="EXECUTING"
|
||||
:::
|
||||
↓
|
||||
<container data='{"executeId":"xxx","type":"Page","status":"EXECUTING"}'></container>
|
||||
↓
|
||||
container.vue 组件接收 data 属性
|
||||
↓
|
||||
getRenderData() 合并 processingList 数据
|
||||
↓
|
||||
最终渲染的组件数据 = data + processingList[executeId]
|
||||
```
|
||||
|
||||
## 快速开始:生成 Markdown 并插入自定义标签
|
||||
|
||||
在实际开发中,我们通常需要在代码中动态生成包含自定义容器标签的 Markdown 字符串。本节介绍如何在前端生成这些标签。
|
||||
|
||||
### 方式一:使用工具函数(推荐)
|
||||
|
||||
#### 1. 使用现有的工具函数
|
||||
|
||||
项目已提供 `containerHelper.uts` 工具函数,位于 `subpackages/utils/containerHelper.uts`。
|
||||
|
||||
**核心函数**:
|
||||
|
||||
```uts
|
||||
/**
|
||||
* 生成工具调用自定义块
|
||||
* @param beforeText 之前的文本内容
|
||||
* @param toolCallData 工具调用数据
|
||||
* @returns 包含自定义块的完整文本
|
||||
*/
|
||||
export function getCustomBlock(
|
||||
beforeText: string,
|
||||
toolCallData: {
|
||||
type?: string;
|
||||
name?: string;
|
||||
executeId?: string;
|
||||
status?: string
|
||||
}
|
||||
): string
|
||||
```
|
||||
|
||||
**使用示例**:
|
||||
|
||||
```uts
|
||||
import { getCustomBlock } from '@/subpackages/utils/containerHelper.uts'
|
||||
|
||||
// 在消息处理中生成容器块
|
||||
const toolCallData = {
|
||||
type: 'Page',
|
||||
name: '页面预览',
|
||||
executeId: 'exec-123',
|
||||
status: 'EXECUTING'
|
||||
}
|
||||
|
||||
let markdown = '这是一段普通文本。'
|
||||
markdown = getCustomBlock(markdown, toolCallData)
|
||||
// 结果: '这是一段普通文本。\n\n:::container executeId="exec-123" type="Page" status="EXECUTING" name="页面预览"\n:::\n\n'
|
||||
```
|
||||
|
||||
#### 2. 底层工具函数
|
||||
|
||||
如果需要更灵活的控制,可以使用底层函数:
|
||||
|
||||
```uts
|
||||
import { getBlockWrapper, getBlockName } from '@/subpackages/utils/containerHelper.uts'
|
||||
|
||||
/**
|
||||
* 生成自定义容器块
|
||||
* @param data 属性数据对象
|
||||
* @returns 格式化的容器块字符串
|
||||
*/
|
||||
function generateContainerBlock(data: Record<string, any>): string {
|
||||
const blockName = getBlockName() // 返回 'container'
|
||||
return getBlockWrapper(blockName, data)
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const block = generateContainerBlock({
|
||||
executeId: 'exec-123',
|
||||
type: 'Page',
|
||||
status: 'EXECUTING',
|
||||
name: '页面预览'
|
||||
})
|
||||
// 结果: '\n\n:::container executeId="exec-123" type="Page" status="EXECUTING" name="页面预览"\n:::\n\n'
|
||||
```
|
||||
|
||||
### 方式二:直接字符串拼接
|
||||
|
||||
如果只是偶尔使用,也可以直接拼接字符串:
|
||||
|
||||
```uts
|
||||
// 准备数据
|
||||
const executeId = 'exec-123'
|
||||
const type = 'Page'
|
||||
const status = 'EXECUTING'
|
||||
const name = '页面预览'
|
||||
|
||||
// 拼接 Markdown
|
||||
const markdown = `这是一段文本。
|
||||
|
||||
:::container executeId="${executeId}" type="${type}" status="${status}" name="${name}"
|
||||
:::
|
||||
|
||||
继续其他内容...`
|
||||
```
|
||||
|
||||
### 方式三:创建专用的生成函数
|
||||
|
||||
对于特定的业务场景,可以创建专门的生成函数:
|
||||
|
||||
```uts
|
||||
/**
|
||||
* 生成页面预览容器块
|
||||
* @param markdownText 现有的 Markdown 文本
|
||||
* @param pageData 页面数据
|
||||
* @returns 插入容器块后的 Markdown
|
||||
*/
|
||||
export function insertPagePreviewBlock(
|
||||
markdownText: string,
|
||||
pageData: { executeId: string; name: string; status: string }
|
||||
): string {
|
||||
const block = `\n\n:::container executeId="${pageData.executeId}" type="Page" status="${pageData.status}" name="${pageData.name}"\n:::\n\n`
|
||||
|
||||
// 检查是否已存在,避免重复插入
|
||||
if (markdownText.includes(`executeId="${pageData.executeId}"`)) {
|
||||
return markdownText
|
||||
}
|
||||
|
||||
return `${markdownText}${block}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成工具调用容器块
|
||||
* @param markdownText 现有的 Markdown 文本
|
||||
* @param toolCallData 工具调用数据
|
||||
* @returns 插入容器块后的 Markdown
|
||||
*/
|
||||
export function insertToolCallBlock(
|
||||
markdownText: string,
|
||||
toolCallData: { executeId: string; type: string; name: string; status: string }
|
||||
): string {
|
||||
const attrs = Object.entries(toolCallData)
|
||||
.map(([key, value]) => `${key}="${value}"`)
|
||||
.join(' ')
|
||||
|
||||
const block = `\n\n:::container ${attrs}\n:::\n\n`
|
||||
|
||||
// 检查是否已存在
|
||||
if (markdownText.includes(`executeId="${toolCallData.executeId}"`)) {
|
||||
return markdownText
|
||||
}
|
||||
|
||||
return `${markdownText}${block}`
|
||||
}
|
||||
```
|
||||
|
||||
### 数据传递规则
|
||||
|
||||
#### 1. 简单属性(字符串、数字)
|
||||
|
||||
直接在容器语法中传递:
|
||||
|
||||
```markdown
|
||||
:::container executeId="exec-123" type="Page" status="EXECUTING" name="页面预览"
|
||||
:::
|
||||
```
|
||||
|
||||
#### 2. 复杂数据(对象、数组)
|
||||
|
||||
如果需要传递复杂数据,有两种方式:
|
||||
|
||||
**方式一:通过 processingList 传递(推荐)**
|
||||
|
||||
```uts
|
||||
// 在消息对象中设置 processingList
|
||||
const message = {
|
||||
text: markdownWithContainer,
|
||||
processingList: [
|
||||
{
|
||||
executeId: 'exec-123',
|
||||
type: AgentComponentTypeEnum.Page,
|
||||
name: '页面预览',
|
||||
status: ProcessingEnum.EXECUTING,
|
||||
result: {
|
||||
input: { url: 'https://example.com' },
|
||||
data: { html: '<div>...</div>' }
|
||||
},
|
||||
// ... 其他字段
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**方式二:在容器语法中传递 JSON 字符串**
|
||||
|
||||
```markdown
|
||||
:::container executeId="exec-123" data='{"key":"value","nested":{"prop":123}}'
|
||||
:::
|
||||
```
|
||||
|
||||
### 实际使用场景
|
||||
|
||||
#### 场景 1:SSE 流式消息处理
|
||||
|
||||
```uts
|
||||
// 在 AgentDetailService.uts 中
|
||||
import { getCustomBlock } from '@/subpackages/utils/containerHelper.uts'
|
||||
|
||||
// 处理 PROCESSING 事件
|
||||
if (eventType === ConversationEventTypeEnum.PROCESSING) {
|
||||
const processingResult = responseData.result || {}
|
||||
responseData.executeId = processingResult.executeId
|
||||
|
||||
// 生成包含容器块的 Markdown
|
||||
const accumulatedText = getCustomBlock(
|
||||
currentMessage.text || '',
|
||||
responseData
|
||||
)
|
||||
|
||||
// 更新消息
|
||||
newMessage = {
|
||||
...currentMessage,
|
||||
text: accumulatedText,
|
||||
status: MessageStatusEnum.Loading,
|
||||
processingList: [
|
||||
...(currentMessage?.processingList || []),
|
||||
responseData,
|
||||
] as ProcessingInfo[],
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 场景 2:检查标签是否已存在
|
||||
|
||||
`getCustomBlock` 函数已经内置了重复检查逻辑:
|
||||
|
||||
```uts
|
||||
// 在 containerHelper.uts 中
|
||||
export function getCustomBlock(
|
||||
beforeText: string,
|
||||
toolCallData: { type?: string; name?: string; executeId?: string; status?: string }
|
||||
): string {
|
||||
const { type, executeId } = toolCallData
|
||||
|
||||
if (!type || !executeId) {
|
||||
return beforeText
|
||||
}
|
||||
|
||||
const hasBlock = beforeText.includes(`executeId="${executeId}"`)
|
||||
|
||||
if (hasBlock) {
|
||||
// 如果已经存在相同的执行ID,则不重复添加
|
||||
return beforeText
|
||||
}
|
||||
|
||||
// ... 生成新块
|
||||
}
|
||||
```
|
||||
|
||||
#### 场景 3:动态更新处理状态
|
||||
|
||||
```uts
|
||||
// 当处理状态更新时,更新 processingList
|
||||
const updateProcessingStatus = (
|
||||
executeId: string,
|
||||
newStatus: ProcessingEnum,
|
||||
result?: ExecuteResultInfo
|
||||
) => {
|
||||
const currentMessage = getCurrentMessage()
|
||||
|
||||
// 更新 processingList 中对应项的状态
|
||||
const updatedProcessingList = currentMessage.processingList?.map(item => {
|
||||
if (item.executeId === executeId) {
|
||||
return {
|
||||
...item,
|
||||
status: newStatus,
|
||||
result: result || item.result
|
||||
}
|
||||
}
|
||||
return item
|
||||
}) || []
|
||||
|
||||
// 更新消息
|
||||
updateMessage({
|
||||
processingList: updatedProcessingList
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
1. **使用换行分隔**:容器块前后需要添加换行,确保 Markdown 解析正确
|
||||
|
||||
```markdown
|
||||
✅ 正确
|
||||
文本内容
|
||||
|
||||
:::container executeId="xxx"
|
||||
:::
|
||||
|
||||
继续内容
|
||||
|
||||
❌ 可能有问题
|
||||
文本内容
|
||||
:::container executeId="xxx"
|
||||
:::
|
||||
继续内容
|
||||
```
|
||||
|
||||
2. **属性值转义**:属性值中的引号需要转义或使用单引号
|
||||
|
||||
```markdown
|
||||
✅ 正确
|
||||
:::container name="页面\"预览\""
|
||||
:::
|
||||
|
||||
✅ 也可以
|
||||
:::container name='页面"预览"'
|
||||
:::
|
||||
```
|
||||
|
||||
3. **避免重复插入**:在流式渲染中,注意检查标签是否已存在,避免重复插入
|
||||
|
||||
4. **executeId 唯一性**:确保每个容器块都有唯一的 `executeId`,用于标识和匹配 `processingList` 中的数据
|
||||
|
||||
## 实现步骤:创建自定义渲染组件
|
||||
|
||||
如果你需要创建新的自定义渲染组件,请按照以下步骤操作。
|
||||
|
||||
### 第一步:了解容器组件结构
|
||||
|
||||
容器组件位于 `uni_modules/mp-html/components/mp-html/container/container.vue`。
|
||||
|
||||
**组件 Props**:
|
||||
|
||||
```vue
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**数据合并逻辑**:
|
||||
|
||||
在 `node.vue` 中,通过 `getRenderData()` 方法合并数据:
|
||||
|
||||
```javascript
|
||||
getRenderData(data) {
|
||||
if(!data) {
|
||||
return {}
|
||||
}
|
||||
|
||||
if(typeof data === 'string') {
|
||||
data = JSON.parse(data)
|
||||
}
|
||||
|
||||
// 从 processingList 中按优先级获取数据并合并
|
||||
const result = {
|
||||
...data,
|
||||
...getProcessingDataByPriority(data.executeId, this.processingList)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
### 第二步:修改容器组件(如果需要)
|
||||
|
||||
如果需要支持新的渲染类型,可以修改 `container.vue` 组件:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<view class="tool-call-status">
|
||||
<!-- 根据 type 渲染不同的内容 -->
|
||||
<view v-if="toolCall.type === 'Page'" class="page-preview">
|
||||
<!-- 页面预览内容 -->
|
||||
</view>
|
||||
<view v-else-if="toolCall.type === 'Tool'" class="tool-call">
|
||||
<!-- 工具调用内容 -->
|
||||
</view>
|
||||
<!-- 默认渲染 -->
|
||||
<view v-else class="default-container">
|
||||
<!-- 默认内容 -->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
toolCall() {
|
||||
return this.data
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 第三步:在 Markdown 中使用
|
||||
|
||||
在代码中生成包含容器块的 Markdown:
|
||||
|
||||
```uts
|
||||
import { getCustomBlock } from '@/subpackages/utils/containerHelper.uts'
|
||||
|
||||
const markdown = getCustomBlock('', {
|
||||
executeId: 'exec-123',
|
||||
type: 'Page',
|
||||
name: '页面预览',
|
||||
status: 'EXECUTING'
|
||||
})
|
||||
```
|
||||
|
||||
### 第四步:传递 processingList
|
||||
|
||||
在消息对象中设置 `processingList`:
|
||||
|
||||
```uts
|
||||
const message = {
|
||||
text: markdown,
|
||||
processingList: [
|
||||
{
|
||||
executeId: 'exec-123',
|
||||
type: AgentComponentTypeEnum.Page,
|
||||
name: '页面预览',
|
||||
status: ProcessingEnum.EXECUTING,
|
||||
result: {
|
||||
input: { /* 输入参数 */ },
|
||||
data: { /* 输出数据 */ }
|
||||
},
|
||||
cardBindConfig: { /* 卡片配置 */ },
|
||||
targetId: 123
|
||||
}
|
||||
] as ProcessingInfo[]
|
||||
}
|
||||
```
|
||||
|
||||
### 第五步:在组件中使用
|
||||
|
||||
在消息组件中传递 `processingList`:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<mp-html
|
||||
:content="processedText"
|
||||
:processing-list="msg.processingList"
|
||||
:markdown="true"
|
||||
:container-style="`width: 100%;`"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
## 现有示例
|
||||
|
||||
### 示例 1:工具调用容器(Tool Call)
|
||||
|
||||
**生成 Markdown**:
|
||||
|
||||
```uts
|
||||
import { getCustomBlock } from '@/subpackages/utils/containerHelper.uts'
|
||||
|
||||
const markdown = getCustomBlock('', {
|
||||
executeId: 'tool-call-123',
|
||||
type: 'Tool',
|
||||
name: '读取文件',
|
||||
status: 'EXECUTING'
|
||||
})
|
||||
```
|
||||
|
||||
**Markdown 语法**:
|
||||
|
||||
```markdown
|
||||
:::container executeId="tool-call-123" type="Tool" status="EXECUTING" name="读取文件"
|
||||
:::
|
||||
```
|
||||
|
||||
**渲染效果**:
|
||||
|
||||
- 显示工具名称和状态
|
||||
- 支持展开/收起查看详情
|
||||
- 显示调用参数和结果
|
||||
- 支持复制功能
|
||||
|
||||
### 示例 2:页面预览容器(Page Preview)
|
||||
|
||||
**生成 Markdown**:
|
||||
|
||||
```uts
|
||||
const markdown = getCustomBlock('', {
|
||||
executeId: 'page-preview-123',
|
||||
type: 'Page',
|
||||
name: '页面预览',
|
||||
status: 'FINISHED'
|
||||
})
|
||||
```
|
||||
|
||||
**processingList 数据**:
|
||||
|
||||
```uts
|
||||
{
|
||||
executeId: 'page-preview-123',
|
||||
type: AgentComponentTypeEnum.Page,
|
||||
name: '页面预览',
|
||||
status: ProcessingEnum.FINISHED,
|
||||
result: {
|
||||
input: { url: 'https://example.com' },
|
||||
data: { html: '<div>页面内容</div>' }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**渲染效果**:
|
||||
|
||||
- 显示页面名称和状态
|
||||
- 支持预览页面(点击眼睛图标)
|
||||
- 显示执行结果
|
||||
|
||||
### 示例 3:动态状态更新
|
||||
|
||||
**初始状态**:
|
||||
|
||||
```uts
|
||||
// 生成 EXECUTING 状态的容器
|
||||
let markdown = getCustomBlock('', {
|
||||
executeId: 'exec-123',
|
||||
type: 'Page',
|
||||
status: 'EXECUTING'
|
||||
})
|
||||
|
||||
let processingList = [{
|
||||
executeId: 'exec-123',
|
||||
status: ProcessingEnum.EXECUTING,
|
||||
// ... 其他字段
|
||||
}]
|
||||
```
|
||||
|
||||
**更新为完成状态**:
|
||||
|
||||
```uts
|
||||
// 更新 processingList
|
||||
processingList = processingList.map(item => {
|
||||
if (item.executeId === 'exec-123') {
|
||||
return {
|
||||
...item,
|
||||
status: ProcessingEnum.FINISHED,
|
||||
result: {
|
||||
input: { /* ... */ },
|
||||
data: { /* ... */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
|
||||
// Markdown 文本不需要修改,组件会自动根据 processingList 更新显示
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 使用工具函数生成容器块
|
||||
|
||||
**推荐**:使用 `getCustomBlock` 函数
|
||||
|
||||
```uts
|
||||
import { getCustomBlock } from '@/subpackages/utils/containerHelper.uts'
|
||||
|
||||
const markdown = getCustomBlock(beforeText, toolCallData)
|
||||
```
|
||||
|
||||
**原因**:
|
||||
- 自动处理重复检查
|
||||
- 统一的格式和风格
|
||||
- 易于维护和修改
|
||||
|
||||
### 2. executeId 唯一性
|
||||
|
||||
**推荐**:确保每个容器块都有唯一的 `executeId`
|
||||
|
||||
```uts
|
||||
const executeId = `exec-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
```
|
||||
|
||||
**原因**:
|
||||
- 用于匹配 `processingList` 中的数据
|
||||
- 避免重复插入容器块
|
||||
- 支持动态更新状态
|
||||
|
||||
### 3. 数据分离
|
||||
|
||||
**推荐**:简单属性放在容器语法中,复杂数据通过 `processingList` 传递
|
||||
|
||||
```uts
|
||||
// ✅ 推荐
|
||||
// Markdown 中只包含标识信息
|
||||
:::container executeId="exec-123" type="Page" status="EXECUTING"
|
||||
:::
|
||||
|
||||
// processingList 中包含完整数据
|
||||
processingList: [{
|
||||
executeId: 'exec-123',
|
||||
result: { /* 复杂数据 */ }
|
||||
}]
|
||||
```
|
||||
|
||||
**原因**:
|
||||
- Markdown 文本保持简洁
|
||||
- 复杂数据可以动态更新
|
||||
- 支持流式渲染
|
||||
|
||||
### 4. 状态管理
|
||||
|
||||
**推荐**:使用 `ProcessingEnum` 枚举管理状态
|
||||
|
||||
```uts
|
||||
import { ProcessingEnum } from '@/types/enums/common.uts'
|
||||
|
||||
const status = ProcessingEnum.EXECUTING // 'EXECUTING'
|
||||
// 或
|
||||
const status = ProcessingEnum.FINISHED // 'FINISHED'
|
||||
const status = ProcessingEnum.FAILED // 'FAILED'
|
||||
```
|
||||
|
||||
**原因**:
|
||||
- 类型安全
|
||||
- 避免拼写错误
|
||||
- 便于维护
|
||||
|
||||
### 5. 优先级处理
|
||||
|
||||
**推荐**:理解 `getProcessingDataByPriority` 的优先级逻辑
|
||||
|
||||
优先级顺序:`FINISHED` > `FAILED` > `EXECUTING`
|
||||
|
||||
```uts
|
||||
// 如果 processingList 中有多个相同 executeId 的项
|
||||
// 会优先使用 FINISHED 状态的数据
|
||||
const result = getProcessingDataByPriority(executeId, processingList)
|
||||
```
|
||||
|
||||
**原因**:
|
||||
- 确保显示最终状态
|
||||
- 支持状态覆盖
|
||||
- 处理并发更新
|
||||
|
||||
### 6. 错误处理
|
||||
|
||||
**推荐**:在生成容器块时进行数据验证
|
||||
|
||||
```uts
|
||||
export function getCustomBlock(
|
||||
beforeText: string,
|
||||
toolCallData: { type?: string; name?: string; executeId?: string; status?: string }
|
||||
): string {
|
||||
const { type, executeId } = toolCallData
|
||||
|
||||
// ✅ 验证必要字段
|
||||
if (!type || !executeId) {
|
||||
console.warn('getCustomBlock: type 或 executeId 缺失', toolCallData)
|
||||
return beforeText
|
||||
}
|
||||
|
||||
// ... 其他逻辑
|
||||
}
|
||||
```
|
||||
|
||||
### 7. 性能优化
|
||||
|
||||
**推荐**:避免频繁更新 Markdown 文本
|
||||
|
||||
```uts
|
||||
// ✅ 推荐:只更新 processingList
|
||||
updateMessage({
|
||||
processingList: newProcessingList
|
||||
})
|
||||
|
||||
// ❌ 不推荐:每次都重新生成整个 Markdown
|
||||
updateMessage({
|
||||
text: generateNewMarkdown() // 可能导致性能问题
|
||||
})
|
||||
```
|
||||
|
||||
**原因**:
|
||||
- Markdown 解析有性能开销
|
||||
- `processingList` 更新更高效
|
||||
- 组件会自动响应数据变化
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q1: 容器块不显示怎么办?
|
||||
|
||||
**A**: 检查以下几点:
|
||||
|
||||
1. **Markdown 语法是否正确**:确保使用 `:::container` 语法,且前后有换行
|
||||
|
||||
```markdown
|
||||
✅ 正确
|
||||
:::container executeId="xxx"
|
||||
:::
|
||||
|
||||
❌ 错误
|
||||
:::container executeId="xxx":::
|
||||
```
|
||||
|
||||
2. **mp-html 组件配置**:确保 `markdown` 属性为 `true`
|
||||
|
||||
```vue
|
||||
<mp-html :markdown="true" :content="text" />
|
||||
```
|
||||
|
||||
3. **容器插件是否启用**:检查 `markdown-it/index.js` 中是否配置了 `markdown-it-container`
|
||||
|
||||
4. **控制台错误**:检查浏览器控制台是否有错误信息
|
||||
|
||||
### Q2: 如何传递复杂数据?
|
||||
|
||||
**A**: 使用 `processingList` 传递复杂数据:
|
||||
|
||||
```uts
|
||||
// 在消息对象中设置
|
||||
const message = {
|
||||
text: markdownWithContainer,
|
||||
processingList: [{
|
||||
executeId: 'exec-123',
|
||||
result: {
|
||||
input: { /* 复杂对象 */ },
|
||||
data: { /* 复杂对象 */ }
|
||||
}
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
容器组件会自动从 `processingList` 中获取并合并数据。
|
||||
|
||||
### Q3: 如何更新容器状态?
|
||||
|
||||
**A**: 更新 `processingList` 中对应项的状态:
|
||||
|
||||
```uts
|
||||
// 更新 processingList
|
||||
const updatedList = processingList.map(item => {
|
||||
if (item.executeId === targetExecuteId) {
|
||||
return {
|
||||
...item,
|
||||
status: ProcessingEnum.FINISHED,
|
||||
result: newResult
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
|
||||
// 更新消息
|
||||
updateMessage({ processingList: updatedList })
|
||||
```
|
||||
|
||||
容器组件会自动响应数据变化并更新显示。
|
||||
|
||||
### Q4: 如何支持新的容器类型?
|
||||
|
||||
**A**: 修改 `container.vue` 组件,添加新的渲染逻辑:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<view class="custom-container">
|
||||
<!-- 根据 type 渲染不同内容 -->
|
||||
<view v-if="toolCall.type === 'YourNewType'">
|
||||
<!-- 新类型的渲染内容 -->
|
||||
</view>
|
||||
<!-- 现有类型 -->
|
||||
<view v-else>
|
||||
<!-- 现有渲染逻辑 -->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Q5: 容器组件如何获取数据?
|
||||
|
||||
**A**: 数据通过以下流程传递:
|
||||
|
||||
1. **Markdown 中的属性**:通过 `:::container executeId="xxx"` 传递
|
||||
2. **processingList**:通过组件 props 传递
|
||||
3. **数据合并**:在 `node.vue` 的 `getRenderData()` 中合并
|
||||
4. **组件接收**:`container.vue` 通过 `data` prop 接收合并后的数据
|
||||
|
||||
### Q6: 如何调试容器组件?
|
||||
|
||||
**A**: 使用以下方法:
|
||||
|
||||
1. **添加 console.log**:在 `container.vue` 中打印接收的数据
|
||||
|
||||
```vue
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
data: Object
|
||||
},
|
||||
mounted() {
|
||||
console.log('Container data:', this.data)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
2. **检查 processingList**:确认 `processingList` 中是否有对应 `executeId` 的数据
|
||||
|
||||
3. **检查数据合并**:在 `node.vue` 的 `getRenderData()` 中添加日志
|
||||
|
||||
4. **Vue DevTools**:使用 Vue DevTools 检查组件 props 和 data
|
||||
|
||||
### Q7: 如何处理多个相同 executeId 的容器?
|
||||
|
||||
**A**: `getProcessingDataByPriority` 函数会按优先级返回数据:
|
||||
|
||||
```uts
|
||||
// 优先级:FINISHED > FAILED > EXECUTING
|
||||
const result = getProcessingDataByPriority(executeId, processingList)
|
||||
// 返回优先级最高的项
|
||||
```
|
||||
|
||||
如果有多个相同 `executeId` 的项,会优先使用 `FINISHED` 状态的数据。
|
||||
|
||||
### Q8: 容器组件支持哪些交互?
|
||||
|
||||
**A**: 当前 `container.vue` 组件支持:
|
||||
|
||||
- **展开/收起**:点击头部切换详情显示
|
||||
- **复制**:复制参数或结果到剪贴板
|
||||
- **预览**:对于 Page 类型,支持页面预览(点击眼睛图标)
|
||||
|
||||
可以在 `container.vue` 中添加更多交互功能。
|
||||
|
||||
### Q9: 如何在流式渲染中避免重复插入?
|
||||
|
||||
**A**: `getCustomBlock` 函数已经内置了重复检查:
|
||||
|
||||
```uts
|
||||
const hasBlock = beforeText.includes(`executeId="${executeId}"`)
|
||||
|
||||
if (hasBlock) {
|
||||
return beforeText // 已存在,不重复添加
|
||||
}
|
||||
```
|
||||
|
||||
确保每次调用时传入完整的 `beforeText`。
|
||||
|
||||
### Q10: 容器组件样式如何自定义?
|
||||
|
||||
**A**: 修改 `container.vue` 中的样式:
|
||||
|
||||
```vue
|
||||
<style lang="scss">
|
||||
.tool-call-status {
|
||||
// 自定义样式
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 8rpx;
|
||||
|
||||
// 更多样式...
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
通过本指南,你可以:
|
||||
|
||||
1. ✅ 理解 Markdown 自定义渲染的架构和原理
|
||||
2. ✅ **快速上手**:学会如何在代码中生成包含自定义容器标签的 Markdown
|
||||
3. ✅ **深入实现**:了解如何创建和自定义渲染组件
|
||||
4. ✅ 遵循最佳实践,编写高质量的代码
|
||||
5. ✅ 解决常见问题,快速定位和修复错误
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [mp-html 文档](https://github.com/jin-yufeng/mp-html)
|
||||
- [markdown-it-container 文档](https://github.com/markdown-it/markdown-it-container)
|
||||
- [uni-app-x 官方文档](https://uniapp.dcloud.net.cn/uni-app-x/)
|
||||
|
||||
---
|
||||
|
||||
**维护者**:开发团队
|
||||
|
||||
如有问题或建议,请联系开发团队或提交 Issue。
|
||||
1157
qiming-mobile/docs/SSE-Implementation-Guide.md
Normal file
1157
qiming-mobile/docs/SSE-Implementation-Guide.md
Normal file
File diff suppressed because it is too large
Load Diff
101
qiming-mobile/docs/agent_integration_guide.md
Normal file
101
qiming-mobile/docs/agent_integration_guide.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# 移动端 Agent 组件接入指南
|
||||
|
||||
本文档说明如何在移动端接入文件树、远程桌面和文件预览组件,以配合 `CustomActionService` 和 `AgentDetailService` 完成 Agent 完整能力的落地。
|
||||
|
||||
## 1. 架构概述
|
||||
|
||||
移动端采用了与 PC 端一致的事件驱动架构:
|
||||
- **消息层** (`AgentDetailService`): 解析后端流式消息,自动触发 `OPEN_DESKTOP` (事件) 和 `REFRESH_FILE_LIST` (工具调用)。
|
||||
- **交互层** (`task-result`): 用户点击任务结果时,触发 `OPEN_PREVIEW`。
|
||||
- **中枢层** (`CustomActionService`): 统一分发事件。
|
||||
- **UI 层** (待接入): 监听事件并做出响应(如弹出抽屉、跳转页面)。
|
||||
|
||||
## 2. 核心事件监听
|
||||
|
||||
在你的聊天页面(如 `chat-conversation-component.uvue`)或全局布局中,监听以下事件:
|
||||
|
||||
```typescript
|
||||
import { CustomEventType, OpenPreviewEventData, OpenDesktopEventData, RefreshFileListEventData } from '@/utils/customActionService.uts'
|
||||
|
||||
export default {
|
||||
onLoad() {
|
||||
// 1. 监听打开文件预览/文件树
|
||||
uni.$on(CustomEventType.OPEN_PREVIEW, (data: OpenPreviewEventData) => {
|
||||
console.log('收到打开预览请求:', data.conversationId)
|
||||
// TODO: 打开文件树抽屉/侧边栏
|
||||
// this.showFileTreeDrawer = true
|
||||
// this.currentViewMode = 'preview'
|
||||
// this.fetchFileList(data.conversationId)
|
||||
})
|
||||
|
||||
// 2. 监听打开远程桌面
|
||||
uni.$on(CustomEventType.OPEN_DESKTOP, (data: OpenDesktopEventData) => {
|
||||
console.log('收到打开远程桌面请求:', data.conversationId)
|
||||
// TODO: 显示远程桌面组件/跳转远程桌面页面
|
||||
// this.showDesktopView = true
|
||||
// this.currentViewMode = 'desktop'
|
||||
// this.connectVNC(data.conversationId)
|
||||
})
|
||||
|
||||
// 3. 监听刷新文件列表
|
||||
uni.$on(CustomEventType.REFRESH_FILE_LIST, (data: RefreshFileListEventData) => {
|
||||
console.log('收到刷新文件列表请求:', data.conversationId)
|
||||
// TODO: 如果当前文件树是打开的,重新获取文件列表
|
||||
// if (this.showFileTreeDrawer) {
|
||||
// this.fetchFileList(data.conversationId)
|
||||
// }
|
||||
})
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
// 记得移除监听
|
||||
uni.$off(CustomEventType.OPEN_PREVIEW)
|
||||
uni.$off(CustomEventType.OPEN_DESKTOP)
|
||||
uni.$off(CustomEventType.REFRESH_FILE_LIST)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. UI 组件接入要求
|
||||
|
||||
### 3.1 文件树组件 (File Tree)
|
||||
- **触发时机**: 收到 `OPEN_PREVIEW` 或用户主动点击文件图标。
|
||||
- **功能要求**:
|
||||
- 根据 `conversationId` 调用后端 API 获取文件列表。
|
||||
- 收到 `REFRESH_FILE_LIST` 事件时自动刷新。
|
||||
- 点击文件项时,调用 `CustomActionService.openPage` 或内部逻辑进行预览。
|
||||
|
||||
### 3.2 远程桌面组件 (Remote Desktop)
|
||||
- **触发时机**: 收到 `OPEN_DESKTOP` 或用户主动切换。
|
||||
- **功能要求**:
|
||||
- 建立 VNC 连接(这通常需要特定的 VNC 客户端组件或 WebView 方案)。
|
||||
- 处理容器保活(Keep-alive)。
|
||||
- 支持键盘输入和触摸手势映射。
|
||||
|
||||
### 3.3 文件预览组件 (Preview)
|
||||
- **触发时机**: 用户在文件树中点击文件。
|
||||
- **功能要求**:
|
||||
- 根据文件类型(图片、Markdown、代码、HTML)渲染不同内容。
|
||||
- 对于 HTML/Web 应用,建议使用 `webview` 并通过 `CustomActionService.openPage` 打开。
|
||||
|
||||
## 4. 调试方法
|
||||
|
||||
你可以手动调用 `CustomActionService` 来模拟触发,测试 UI 响应:
|
||||
|
||||
```typescript
|
||||
import { CustomActionService } from '@/utils/customActionService.uts'
|
||||
|
||||
// 模拟打开预览
|
||||
CustomActionService.openPreviewView(123)
|
||||
|
||||
// 模拟打开远程桌面
|
||||
CustomActionService.openDesktopView(123)
|
||||
```
|
||||
|
||||
## 5. 常见问题
|
||||
|
||||
- **Q: `task-result` 点击无反应?**
|
||||
- A: 检查是否传入了 `conversationId`。如果没有 `conversationId`,`task-result` 会降级发出 `task_result_click` 事件,你需要手动处理该事件。
|
||||
|
||||
- **Q: 样式不符合预期?**
|
||||
- A: `task-result` 和 `Plan` 的样式已内置在组件中,如需全局调整,请修改 `uni_modules/mp-html` 下的相关文件。
|
||||
114
qiming-mobile/docs/auto-scroll-rules.md
Normal file
114
qiming-mobile/docs/auto-scroll-rules.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# 会话组件自动滚动底部规则文档
|
||||
|
||||
> 本文档描述了 `chat-conversation-component` 会话组件中自动滚动到底部的完整逻辑规则。
|
||||
>
|
||||
> **最后更新**: 2025-12-30
|
||||
|
||||
---
|
||||
|
||||
## 核心状态变量
|
||||
|
||||
| 变量名 | 类型 | 默认值 | 说明 |
|
||||
|--------|------|--------|------|
|
||||
| `autoToLastMsg` | `boolean` | `false` | 是否启用自动滚动到底部 |
|
||||
| `scrollTouch` | `boolean` | `false` | 用户是否正在触摸滚动(touchend 后延迟 300ms 重置) |
|
||||
| `mouseScroll` | `boolean` | `false` | 鼠标滚轮是否在滚动(H5专用) |
|
||||
| `scrollInBottom` | `boolean` | `true` | 当前视图是否在底部 |
|
||||
| `needSetLockAutoToLastMsg` | `boolean` | `true` | 是否需要检查锁定自动滚动 |
|
||||
| `scrollIntoView` | `string` | `''` | 滚动目标元素ID,设为 `"last-msg"` 触发滚动 |
|
||||
|
||||
**文件位置**: `layers/AgentDetailData.uts`
|
||||
|
||||
---
|
||||
|
||||
## 触发自动滚动的场景
|
||||
|
||||
### 1. 发送消息时
|
||||
- **代码位置**: `chat-conversation-component.uvue` → `handleSendMessage`
|
||||
- **行为**: 设置 `autoToLastMsg = true`,调用 `scrollToLastMsg(false)`
|
||||
|
||||
### 2. 加载历史消息时
|
||||
- **代码位置**: `layers/AgentDetailService.uts` → `handleQueryConversation`
|
||||
- **行为**: 设置 `autoToLastMsg = true`
|
||||
|
||||
### 3. 点击"滚动到底部"按钮
|
||||
- **代码位置**: `layers/ScrollManager.uts` → `scrollToLastMsg`
|
||||
- **行为**: 设置 `autoToLastMsg = true`,`scrollIntoView = "last-msg"`
|
||||
|
||||
### 4. 流式消息更新时
|
||||
- **代码位置**: `layers/AgentDetailService.uts` → `handleChangeMessageList`
|
||||
- **行为**: 当 `autoToLastMsg = true` 时,发送 `streamMessageUpdate` 事件
|
||||
- **响应**: 100ms 防抖后调用 `scrollToLastMsg(false)`
|
||||
|
||||
### 5. 消息列表/建议列表变化时
|
||||
- **代码位置**: `chat-conversation-component.uvue`
|
||||
- **行为**: 通过 watch 监听,调用 `handleScrollToLastMsg()`
|
||||
|
||||
---
|
||||
|
||||
## 取消自动滚动的情况
|
||||
|
||||
### 1. 用户主动向上滚动
|
||||
- **条件**: 触摸/鼠标滚动 + 向上滚动 + 距离底部 > 50px
|
||||
- **代码位置**: `layers/ScrollManager.uts` → `setIsInScrollBottom`
|
||||
|
||||
### 2. 消息列表为空
|
||||
- **代码位置**: `layers/ScrollManager.uts` → `lockAutoToLastMsg`
|
||||
|
||||
---
|
||||
|
||||
## 滚动检测逻辑(兼容微信小程序)
|
||||
|
||||
```typescript
|
||||
// 使用事件参数获取滚动信息
|
||||
const scrollTop = e?.detail?.scrollTop ?? 0
|
||||
const scrollHeight = e?.detail?.scrollHeight ?? 0
|
||||
|
||||
// 微信小程序使用估算值,其他平台使用 DOM 查询
|
||||
// #ifdef MP-WEIXIN
|
||||
const offsetHeight = 600
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
const offsetHeight = uni.getElementById('msg-list')?.offsetHeight ?? 600
|
||||
// #endif
|
||||
|
||||
const diff = scrollHeight - scrollTop - offsetHeight
|
||||
|
||||
// 判断是否在底部(距离 < 30px)
|
||||
scrollInBottom = diff < 30
|
||||
|
||||
// 用户向上滚动时,距离超过 50px 才禁用自动滚动
|
||||
if ((scrollTouch || mouseScroll) && isScrollingUp) {
|
||||
autoToLastMsg = diff <= 50
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 事件总线
|
||||
|
||||
| 事件名 | 发送位置 | 监听位置 | 用途 |
|
||||
|--------|---------|---------|------|
|
||||
| `streamMessageUpdate` | `AgentDetailService.uts` | `chat-conversation-component.uvue` | 流式消息更新触发滚动(100ms防抖)|
|
||||
| `updateMessageComponent` | `AgentDetailService.uts` | `chat-conversation-component.uvue` | 更新消息组件内容 |
|
||||
|
||||
---
|
||||
|
||||
## 平台兼容性
|
||||
|
||||
| 平台 | 触摸检测 | 鼠标检测 | DOM 查询 |
|
||||
|------|----------|----------|----------|
|
||||
| 微信小程序 | `touchstart/end/cancel` | 不适用 | 使用事件参数 |
|
||||
| H5/Web | `touchstart/end/cancel` | `wheel` 事件 | `uni.getElementById` |
|
||||
| App | `touchstart/end/cancel` | 不适用 | `uni.getElementById` |
|
||||
|
||||
---
|
||||
|
||||
## 相关文件
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `layers/AgentDetailData.uts` | 状态变量定义 |
|
||||
| `layers/ScrollManager.uts` | 滚动逻辑管理(跨平台兼容)|
|
||||
| `layers/AgentDetailService.uts` | 消息处理与事件触发 |
|
||||
| `chat-conversation-component.uvue` | 主组件,事件监听与 watch |
|
||||
1069
qiming-mobile/docs/i18n-h5-20260410-execution-log.md
Normal file
1069
qiming-mobile/docs/i18n-h5-20260410-execution-log.md
Normal file
File diff suppressed because it is too large
Load Diff
267
qiming-mobile/docs/i18n-platform-default-import.csv
Normal file
267
qiming-mobile/docs/i18n-platform-default-import.csv
Normal file
@@ -0,0 +1,267 @@
|
||||
key,zh-cn,en-us
|
||||
"Mobile.AgentDev.downloadFailedWithCode","下载失败: {code}","Download failed: {code}"
|
||||
"Mobile.AgentList.collectFailed","添加收藏失败","Failed to add favorite"
|
||||
"Mobile.AgentList.confirmDeleteAgent","你确认要删除该智能体吗?","Are you sure you want to remove this agent?"
|
||||
"Mobile.AgentList.deleteFailed","删除失败","Delete failed"
|
||||
"Mobile.AgentList.fetchAgentFailed","获取智能体列表失败","Failed to load agents"
|
||||
"Mobile.AgentList.fetchCategoryFailed","获取分类信息失败","Failed to load categories"
|
||||
"Mobile.AgentList.unCollectFailed","取消收藏失败","Failed to remove favorite"
|
||||
"Mobile.AgentSearch.emptyResult","暂无结果","No results"
|
||||
"Mobile.AgentSearch.fetchAgentFailed","获取智能体列表失败","Failed to load agents"
|
||||
"Mobile.AgentSearch.fetchRecentUsedFailed","获取最近使用智能体列表失败","Failed to load recent agents"
|
||||
"Mobile.AgentSearch.placeholder","搜索智能体","Search agents"
|
||||
"Mobile.AgentUnion.fetchHomeAgentListFailed","获取主页智能体列表失败","Failed to load agents"
|
||||
"Mobile.AgentUnion.searchPlaceholder","搜索智联录","Search Directory"
|
||||
"Mobile.App.pressAgainToExit","再按一次退出应用","Press again to exit the app"
|
||||
"Mobile.AudioUploader.durationTooLong","录音时长过长,最多支持10分钟","Audio duration is too long. Maximum is 10 minutes."
|
||||
"Mobile.AudioUploader.fileEmpty","音频文件为空","Audio file is empty"
|
||||
"Mobile.AudioUploader.fileReadFailed","文件读取失败: {reason}","File read failed: {reason}"
|
||||
"Mobile.AudioUploader.fileTooLarge","音频文件过大,请控制在10MB以内","Audio file is too large. Keep it within 10MB."
|
||||
"Mobile.AudioUploader.fileTooLargeDuration","文件过大,请控制录音时长","File is too large. Please shorten recording duration."
|
||||
"Mobile.AudioUploader.formatNotSupported","音频格式不支持","Audio format is not supported"
|
||||
"Mobile.AudioUploader.networkException","网络连接异常,请检查网络设置","Network exception. Please check your connection."
|
||||
"Mobile.AudioUploader.networkFailed","网络请求失败","Network request failed"
|
||||
"Mobile.AudioUploader.networkTimeout","网络连接超时,请检查网络设置","Network timeout. Please check your connection."
|
||||
"Mobile.AudioUploader.noTextRecognized","未识别到文字","No text recognized"
|
||||
"Mobile.AudioUploader.pathRequired","音频文件路径不能为空","Audio file path is required"
|
||||
"Mobile.AudioUploader.requestFailedWithCode","请求失败,状态码: {code}","Request failed with status code: {code}"
|
||||
"Mobile.AudioUploader.responseParseFailed","响应数据解析失败","Failed to parse response data"
|
||||
"Mobile.AudioUploader.serverError","服务器内部错误,请稍后重试","Server internal error. Please try again later."
|
||||
"Mobile.AudioUploader.uploadFailed","上传失败","Upload failed"
|
||||
"Mobile.Auth.agreementPrefix","已阅读并同意协议:","I have read and agree to the following:"
|
||||
"Mobile.Auth.agreementSeparator","、",", "
|
||||
"Mobile.Auth.clipboardEmpty","剪贴板为空","Clipboard is empty"
|
||||
"Mobile.Auth.clipboardGetFailed","获取剪贴板失败","Failed to get clipboard"
|
||||
"Mobile.Auth.clipboardNotSixDigits","剪贴板内容不是6位数字验证码","Clipboard content is not a 6-digit code"
|
||||
"Mobile.Auth.clipboardReadFailed","读取剪贴板失败","Failed to read clipboard"
|
||||
"Mobile.Auth.codeAutoFilled","验证码已自动填充","Code auto-filled"
|
||||
"Mobile.Auth.codeLoginRegister","验证码登录/注册","Code Sign In / Sign Up"
|
||||
"Mobile.Auth.codeSentSuccess","验证码发送成功","Verification code sent"
|
||||
"Mobile.Auth.codeSentToEmailTarget","你的邮箱","your email"
|
||||
"Mobile.Auth.codeSentToPhoneTarget","手机号","phone number"
|
||||
"Mobile.Auth.codeSentToPrefix","验证码已发送至","The code has been sent to"
|
||||
"Mobile.Auth.confirmPassword","请确认密码","Please confirm password"
|
||||
"Mobile.Auth.defaultAreaCode","+86","+86"
|
||||
"Mobile.Auth.inputEmail","请输入邮箱","Please enter email"
|
||||
"Mobile.Auth.inputEmailCode","输入邮箱验证码","Enter email verification code"
|
||||
"Mobile.Auth.inputPassword","请输入密码","Please enter password"
|
||||
"Mobile.Auth.inputPasswordAgain","请再次输入密码","Enter password again"
|
||||
"Mobile.Auth.inputPhone","请输入手机号","Please enter phone number"
|
||||
"Mobile.Auth.inputSmsCode","输入短信验证码","Enter SMS verification code"
|
||||
"Mobile.Auth.inputVerifyCode","请输入验证码","Enter verification code"
|
||||
"Mobile.Auth.invalidEmailFormat","请输入正确的邮箱格式","Please enter a valid email address"
|
||||
"Mobile.Auth.invalidPhoneFormat","请输入正确的手机号格式","Please enter a valid phone number"
|
||||
"Mobile.Auth.login","登录","Sign In"
|
||||
"Mobile.Auth.loginFailed","登录失败","Sign in failed"
|
||||
"Mobile.Auth.loginSuccess","登录成功","Signed in"
|
||||
"Mobile.Auth.nextStep","下一步","Next"
|
||||
"Mobile.Auth.oneClickLogin","一键登录","Quick Sign In"
|
||||
"Mobile.Auth.passwordLogin","密码登录","Password Sign In"
|
||||
"Mobile.Auth.passwordMinLength","密码至少需要6个字符","Password must be at least 6 characters"
|
||||
"Mobile.Auth.passwordMismatch","两次输入的密码不一致","The two passwords do not match"
|
||||
"Mobile.Auth.passwordSetFailed","密码设置失败,请重试","Failed to set password. Please try again."
|
||||
"Mobile.Auth.passwordSetSuccess","密码设置成功","Password set successfully"
|
||||
"Mobile.Auth.pasteSixDigits","请粘贴6位数字验证码","Please paste a 6-digit code"
|
||||
"Mobile.Auth.phoneLoginRegister","手机号码登录/注册","Phone Sign In / Sign Up"
|
||||
"Mobile.Auth.pleaseAgreeProtocol","请同意服务协议及隐私保护!","Please agree to the Terms and Privacy Policy."
|
||||
"Mobile.Auth.privacyAgreement","隐私协议","Privacy Policy"
|
||||
"Mobile.Auth.resend","重新获取","Resend"
|
||||
"Mobile.Auth.resendAfterSeconds","{seconds}s后","Retry in {seconds}s"
|
||||
"Mobile.Auth.resendSuccess","重新发送成功","Code sent again"
|
||||
"Mobile.Auth.sending","正在发送...","Sending..."
|
||||
"Mobile.Auth.serviceAgreement","服务使用协议","Terms of Service"
|
||||
"Mobile.Auth.setPasswordDesc","请至少使用6个字符。请勿使用你登录其他网站的密码或容易被猜到的密码","Use at least 6 characters. Do not reuse passwords from other websites."
|
||||
"Mobile.Auth.setPasswordTitle","密码设置","Set Password"
|
||||
"Mobile.Auth.setting","设置中...","Setting..."
|
||||
"Mobile.Auth.userCancelAuthorize","用户取消授权","Authorization canceled"
|
||||
"Mobile.Auth.welcomeUse","欢迎使用{siteName}","Welcome to {siteName}"
|
||||
"Mobile.ButtonWrapper.pagePathConfigError","页面路径配置错误","Invalid page path configuration"
|
||||
"Mobile.ButtonWrapper.pagePathParamConfigError","页面路径参数配置错误","Invalid page path parameter configuration"
|
||||
"Mobile.Chat.aiGeneratedDisclaimer","内容由AI生成,请仔细甄别","AI-generated content, please verify carefully"
|
||||
"Mobile.Chat.album","相册","Album"
|
||||
"Mobile.Chat.chooseImageFailed","选择图片失败: {reason}","Failed to choose image: {reason}"
|
||||
"Mobile.Chat.file","文件","File"
|
||||
"Mobile.Chat.fileLimitReached","已经有 {count} 个文件了,请删除部分文件之后重新选择","You already have {count} files. Remove some files and try again."
|
||||
"Mobile.Chat.fileMaxSelect","最多只能选择 {count} 个文件","You can select up to {count} files."
|
||||
"Mobile.Chat.fillRequiredParams","请填写必填参数","Please fill in required fields"
|
||||
"Mobile.Chat.imageLimitReached","已经有 {count} 张图片了,请删除部分图片之后重新选择","You already have {count} images. Remove some images and try again."
|
||||
"Mobile.Chat.imageMaxSelect","最多只能选择 {count} 张图片","You can select up to {count} images."
|
||||
"Mobile.Chat.modelSelect","模型选择","Model Selection"
|
||||
"Mobile.Chat.noAgentPermission","您无该智能体权限","You do not have permission for this agent"
|
||||
"Mobile.Chat.previewTypeUnsupported","当前不支持预览该类型文件","Preview is not supported for this file type"
|
||||
"Mobile.Chat.removeUploadFailed","请先删除上传失败的图片或文件","Please remove failed uploads first"
|
||||
"Mobile.Chat.skill","技能","Skill"
|
||||
"Mobile.Chat.takePhoto","拍照","Camera"
|
||||
"Mobile.Chat.uploadFileAlt","上传文件","Uploaded file"
|
||||
"Mobile.Chat.uploadImageAlt","上传图片","Uploaded image"
|
||||
"Mobile.Common.agentIconAlt","智能体图标","Agent icon"
|
||||
"Mobile.Common.agree","同意","Agree"
|
||||
"Mobile.Common.all","全部","All"
|
||||
"Mobile.Common.appLogoAlt","应用 Logo","App logo"
|
||||
"Mobile.Common.appName","启明","Qiming"
|
||||
"Mobile.Common.avatarAlt","头像","Avatar"
|
||||
"Mobile.Common.cancel","取消","Cancel"
|
||||
"Mobile.Common.confirm","确认","Confirm"
|
||||
"Mobile.Common.conversationHistory","会话记录","Conversations"
|
||||
"Mobile.Common.copyFailed","复制失败","Copy failed"
|
||||
"Mobile.Common.copySuccess","复制成功","Copied"
|
||||
"Mobile.Common.coverImageAlt","封面图","Cover image"
|
||||
"Mobile.Common.deleteIconAlt","删除","Delete"
|
||||
"Mobile.Common.disagree","不同意","Disagree"
|
||||
"Mobile.Common.from","来自","From"
|
||||
"Mobile.Common.invalidInputString","输入为空或不是字符串","Input is empty or not a string"
|
||||
"Mobile.Common.invalidJsonFormat","JSON格式错误","Invalid JSON format"
|
||||
"Mobile.Common.loadMore","加载更多","Load more"
|
||||
"Mobile.Common.loginRegister","登录 / 注册","Sign In / Sign Up"
|
||||
"Mobile.Common.menu","菜单","Menu"
|
||||
"Mobile.Common.missingPathParam","缺少路径参数: {key}","Missing path parameter: {key}"
|
||||
"Mobile.Common.networkRetry","网络错误,请重试","Network error. Please try again."
|
||||
"Mobile.Common.noData","暂无数据","No data"
|
||||
"Mobile.Common.noDataImageAlt","暂无数据插图","No data illustration"
|
||||
"Mobile.Common.noMoreData","没有更多数据了","No more data"
|
||||
"Mobile.Common.noticeExperience","当前展示为体验数据,登录后使用完整功能","Demo data is shown now. Sign in to use full features."
|
||||
"Mobile.Common.ok","确定","OK"
|
||||
"Mobile.Common.open","打开","Open"
|
||||
"Mobile.Common.optionSeparator","/","/"
|
||||
"Mobile.Common.pageHome","页面首页","Home page"
|
||||
"Mobile.Common.pleaseInput","请输入","Please enter"
|
||||
"Mobile.Common.pleaseSelect","请选择","Please select"
|
||||
"Mobile.Common.recentUsed","最近使用","Recent"
|
||||
"Mobile.Common.requiredMark","*","*"
|
||||
"Mobile.Common.retry","重试","Retry"
|
||||
"Mobile.Common.skillIconAlt","技能图标","Skill icon"
|
||||
"Mobile.Common.switchFailed","语言切换失败","Language switch failed"
|
||||
"Mobile.Common.switchSuccess","语言已更新","Language updated"
|
||||
"Mobile.Common.switching","切换中...","Switching..."
|
||||
"Mobile.Common.tip","提示","Notice"
|
||||
"Mobile.Common.workspace","工作台","Workspace"
|
||||
"Mobile.Conversation.agentIdMissing","智能体ID不存在","Agent ID is missing"
|
||||
"Mobile.Conversation.allConversations","全部会话","All conversations"
|
||||
"Mobile.Conversation.chatKeyRequired","会话链接Key不能为空","Chat key cannot be empty"
|
||||
"Mobile.Conversation.executing","执行中","Running"
|
||||
"Mobile.Conversation.favorite","收藏","Favorite"
|
||||
"Mobile.Conversation.favoriteSuccess","已添加到收藏","Added to favorites"
|
||||
"Mobile.Conversation.idRequired","会话ID不能为空","Conversation ID cannot be empty"
|
||||
"Mobile.Conversation.loadingHistory","正在加载历史会话","Loading history conversations"
|
||||
"Mobile.Conversation.more","更多","More"
|
||||
"Mobile.Conversation.moreInfo","更多信息","More info"
|
||||
"Mobile.Conversation.newConversation","新建会话","New conversation"
|
||||
"Mobile.Conversation.relatedTitle","相关会话","Related conversations"
|
||||
"Mobile.Conversation.settingsLocked","对话开始后,对话设置将无法修改。","Settings cannot be changed after the conversation starts."
|
||||
"Mobile.Conversation.settingsTitle","对话设置","Conversation settings"
|
||||
"Mobile.Conversation.share","分享","Share"
|
||||
"Mobile.Conversation.startWithAgent","和{name}开始会话","Start chatting with {name}"
|
||||
"Mobile.Conversation.unfavoriteSuccess","已取消收藏","Removed from favorites"
|
||||
"Mobile.Conversation.unknownAgent","未知智能体","Unknown agent"
|
||||
"Mobile.Conversation.unnamed","未命名会话","Untitled conversation"
|
||||
"Mobile.Conversation.viewAll","查看全部","View all"
|
||||
"Mobile.FilePreview.documentRenderFailed","文档渲染失败","Document render failed"
|
||||
"Mobile.FilePreview.getShareKeyFailed","获取分享链接失败","Failed to get share key"
|
||||
"Mobile.FilePreview.invalidParams","会话ID或文件名不能为空","Conversation ID or file name cannot be empty"
|
||||
"Mobile.FilePreview.previewPageLoadFailed","预览页面加载失败","Preview page failed to load"
|
||||
"Mobile.FilePreview.shareDomainMissing","分享失败,无法获取当前域名","Share failed: current domain is unavailable"
|
||||
"Mobile.FilePreview.shareFailed","分享失败","Share failed"
|
||||
"Mobile.FilePreview.shareSuccess","分享成功,链接已复制","Shared successfully. Link copied."
|
||||
"Mobile.FileTree.downloadComplete","下载完成","Download complete"
|
||||
"Mobile.FileTree.downloadFailedWithCode","下载失败: {code}","Download failed: {code}"
|
||||
"Mobile.FileTree.downloadNetworkFailed","下载失败,请检查网络","Download failed. Please check your network."
|
||||
"Mobile.FileTree.downloadServerError","下载失败:服务器返回错误","Download failed: server returned an error"
|
||||
"Mobile.FileTree.downloading","下载中...","Downloading..."
|
||||
"Mobile.FileTree.exportFailed","导出失败: {message}","Export failed: {message}"
|
||||
"Mobile.FileTree.exportSuccess","导出成功!","Export successful!"
|
||||
"Mobile.FileTree.exportUnknownError","导出过程中发生未知错误","Unknown export error"
|
||||
"Mobile.FileTree.fileValidateFailed","文件验证失败","File validation failed"
|
||||
"Mobile.FileTree.invalidConversationId","会话ID不存在或无效,无法导出","Conversation ID is invalid. Cannot export."
|
||||
"Mobile.FileTree.linkFileNotSupported","该文件为链接文件,不支持预览","This link file cannot be previewed"
|
||||
"Mobile.FileTree.noFiles","暂无文件","No files"
|
||||
"Mobile.FileTree.openFailed","无法打开文件,请通过微信设置查看","Cannot open file. Check it in WeChat settings."
|
||||
"Mobile.FileTree.preparingDownload","准备下载...","Preparing download..."
|
||||
"Mobile.FileTree.saveFailed","保存文件失败","Failed to save file"
|
||||
"Mobile.FileTree.serverError","服务器返回错误","Server returned an error"
|
||||
"Mobile.FileTree.zipSavedContent","ZIP文件已保存成功!\n\n文件名:{fileName}\n文件路径:\n{filePath}\n\n提示:文件保存在小程序文件目录中,可通过微信设置中的小程序存储空间查看,或点击""打开""直接查看文件。","ZIP file saved successfully!\n\nFile name: {fileName}\nFile path:\n{filePath}\n\nTip: The file is stored in mini-program storage. You can check it in WeChat settings, or tap ""Open"" to view it."
|
||||
"Mobile.Header.logout","退出登录","Sign Out"
|
||||
"Mobile.Header.logoutFailed","退出失败","Sign out failed"
|
||||
"Mobile.Header.logoutSuccess","退出成功","Signed out"
|
||||
"Mobile.Header.profile","个人资料","Profile"
|
||||
"Mobile.Link.copied","链接已复制","Link copied"
|
||||
"Mobile.Link.copyAndOpen","链接已复制,请手动粘贴打开","Link copied. Paste it to open manually."
|
||||
"Mobile.Link.invalidConfig","链接地址配置错误","Link configuration is invalid"
|
||||
"Mobile.Link.openFailed","打开链接失败","Failed to open the link"
|
||||
"Mobile.Nav.agent","智能体","Agents"
|
||||
"Mobile.Nav.app","应用","Apps"
|
||||
"Mobile.Nav.home","首页","Home"
|
||||
"Mobile.Nav.unionRecord","智联录","Directory"
|
||||
"Mobile.Page.inputRequired","请输入内容","Please enter content"
|
||||
"Mobile.Page.loading","加载中","Loading"
|
||||
"Mobile.Page.loadingMore","加载中...","Loading..."
|
||||
"Mobile.PageCard.createdAt","创建于 {date}","Created on {date}"
|
||||
"Mobile.PageCard.noPreviewImage","无可用预览图","No preview image available"
|
||||
"Mobile.PagePreview.defaultTitle","页面预览","Page preview"
|
||||
"Mobile.Permission.cameraDescription","拍照功能需要访问你的摄像头。请在浏览器设置中允许摄像头权限,然后重新尝试。","Taking photos needs camera access. Allow camera permission in browser settings and try again."
|
||||
"Mobile.Permission.cameraTitle","需要摄像头权限","Camera permission required"
|
||||
"Mobile.Permission.gotIt","知道了","Got it"
|
||||
"Mobile.Permission.guideContent","{description}\n\n操作步骤:\n{steps}","{description}\n\nSteps:\n{steps}"
|
||||
"Mobile.Permission.microphoneDescription","录音功能需要访问你的麦克风。请在浏览器设置中允许麦克风权限,然后重新尝试。","Recording needs microphone access. Allow microphone permission in browser settings and try again."
|
||||
"Mobile.Permission.microphoneTitle","需要麦克风权限","Microphone permission required"
|
||||
"Mobile.Permission.stepAllowCamera","找到""摄像头""选项,选择""允许""","Find ""Camera"" and choose ""Allow"""
|
||||
"Mobile.Permission.stepAllowMicrophone","找到""麦克风""选项,选择""允许""","Find ""Microphone"" and choose ""Allow"""
|
||||
"Mobile.Permission.stepClickLock","点击浏览器地址栏左侧的锁定图标","Click the lock icon on the left side of the browser address bar"
|
||||
"Mobile.Permission.stepRefreshRetryPhoto","刷新页面后重新尝试拍照","Refresh the page and try taking photos again"
|
||||
"Mobile.Permission.stepRefreshRetryRecord","刷新页面后重新尝试录音","Refresh the page and try recording again"
|
||||
"Mobile.Profile.dynamicCodeExpire","动态认证码({expire} 过期)","Dynamic Code (expires at {expire})"
|
||||
"Mobile.Profile.email","邮箱","Email"
|
||||
"Mobile.Profile.language","语言","Language"
|
||||
"Mobile.Profile.languagePlaceholder","请选择语言","Select language"
|
||||
"Mobile.Profile.pendingBind","待绑定","Not linked"
|
||||
"Mobile.Profile.phone","手机号","Phone"
|
||||
"Mobile.Profile.title","个人资料","Profile"
|
||||
"Mobile.Profile.userName","用户名","Username"
|
||||
"Mobile.Profile.version","版本","Version"
|
||||
"Mobile.Sandbox.noneAvailable","无可用电脑","No available computer"
|
||||
"Mobile.Sandbox.selectorTitle","智能体电脑选择","Agent computer selection"
|
||||
"Mobile.Sandbox.switchFailed","切换失败","Switch failed"
|
||||
"Mobile.Sandbox.unavailable","电脑不可用","Computer unavailable"
|
||||
"Mobile.Skill.empty","暂无技能","No skills"
|
||||
"Mobile.Skill.myFavorites","我的收藏","My favorites"
|
||||
"Mobile.Skill.recentUsed","最近使用","Recent"
|
||||
"Mobile.Skill.searchSkillName","搜索技能名称","Search skill name"
|
||||
"Mobile.Stream.httpErrorWithCode","请求失败,状态码: {code}","Request failed with status code: {code}"
|
||||
"Mobile.Stream.timeoutDisconnected","请求超时,已自动断开","Request timed out and was disconnected"
|
||||
"Mobile.TempSession.defaultDesc","我是你的智能助手,有什么需要帮忙的吗?","I am your AI assistant. What can I help you with?"
|
||||
"Mobile.TempSession.inputPlaceholder","请输入指令","Enter your instruction"
|
||||
"Mobile.ThirdParty.MpHtml.callArguments","调用参数","Call arguments"
|
||||
"Mobile.ThirdParty.MpHtml.callResult","调用结果","Call result"
|
||||
"Mobile.ThirdParty.MpHtml.copyCode","复制代码","Copy code"
|
||||
"Mobile.ThirdParty.MpHtml.emptyResult","结果为空","Result is empty"
|
||||
"Mobile.ThirdParty.MpHtml.executionPlan","执行计划","Execution plan"
|
||||
"Mobile.ThirdParty.MpHtml.saveSuccess","保存成功","Saved successfully"
|
||||
"Mobile.ThirdParty.UniAiX.invalidMessageContent","消息内容不是字符串","Message content is not a string"
|
||||
"Mobile.ThirdParty.UniCaptcha.defaultTitle","请输入验证码","Enter verification code"
|
||||
"Mobile.ThirdParty.UniCaptcha.fillCaptcha","请填写验证码","Please enter the verification code"
|
||||
"Mobile.ThirdParty.UniCaptcha.sceneRequired","scene不能为空","Scene is required"
|
||||
"Mobile.ThirdParty.XTools.getServiceFailed","获取服务失败,不支持该操作","Failed to get service. This action is not supported"
|
||||
"Mobile.ThirdParty.XTools.saveFailed","保存失败","Save failed"
|
||||
"Mobile.ThirdParty.XTools.saveSuccess","保存成功","Saved successfully"
|
||||
"Mobile.ThirdParty.XTools.wechatUnsupported","当前环境不支持微信操作","WeChat operation is not supported in the current environment"
|
||||
"Mobile.Time.dayBeforeYesterday","前天","The day before yesterday"
|
||||
"Mobile.Time.daysAgo","{count}天前","{count} days ago"
|
||||
"Mobile.Time.hoursAgo","{count}小时前","{count} hours ago"
|
||||
"Mobile.Time.justNow","刚刚","Just now"
|
||||
"Mobile.Time.lastYear","去年","Last year"
|
||||
"Mobile.Time.minutesAgo","{count}分钟前","{count} minutes ago"
|
||||
"Mobile.Time.monthsAgo","{count}月前","{count} months ago"
|
||||
"Mobile.Time.yearsAgo","{count}年前","{count} years ago"
|
||||
"Mobile.Time.yesterday","昨天","Yesterday"
|
||||
"Mobile.Voice.connecting","正在连接","Connecting..."
|
||||
"Mobile.Voice.conversationActiveError","会话正在进行中,请先暂停现有会话","Conversation is active. Pause the current session first."
|
||||
"Mobile.Voice.goToSettings","去设置","Go to settings"
|
||||
"Mobile.Voice.holdToTalk","按住说话","Hold to talk"
|
||||
"Mobile.Voice.noSpeechDetected","未识别到文字","No speech recognized"
|
||||
"Mobile.Voice.permissionContent","录音功能需要访问你的麦克风,请在设置中允许录音权限","Recording needs microphone access. Allow permission in settings."
|
||||
"Mobile.Voice.permissionRequired","需要录音权限","Microphone permission required"
|
||||
"Mobile.Voice.recognizing","语音识别中","Recognizing speech"
|
||||
"Mobile.Voice.recordFailed","录音失败","Recording failed"
|
||||
"Mobile.Voice.serviceNotReady","音频上传服务未初始化","Audio upload service is not initialized"
|
||||
"Mobile.Webview.aiReading","AI读取网页内容中...","AI is reading webpage content..."
|
||||
"Mobile.Webview.defaultTitle","扩展页面","Extended page"
|
||||
"Mobile.Webview.loadFailed","页面加载失败","Page failed to load"
|
||||
|
1340
qiming-mobile/docs/i18n-platform-default-import.json
Normal file
1340
qiming-mobile/docs/i18n-platform-default-import.json
Normal file
File diff suppressed because it is too large
Load Diff
311
qiming-mobile/docs/i18n-progress-report.md
Normal file
311
qiming-mobile/docs/i18n-progress-report.md
Normal file
@@ -0,0 +1,311 @@
|
||||
# Qiming Mobile 多语言接入进度报告
|
||||
|
||||
## 1. 背景
|
||||
|
||||
本轮多语言改造的目标,不只是把部分页面文案替换成 `t("...")`,而是建立一套可以持续扩展的接入方案,覆盖以下几类场景:
|
||||
|
||||
- 业务页面与公共组件的用户可见文案
|
||||
- 框架组件和原生 `uni.*` API 的提示文案
|
||||
- 第三方库内部默认文案或运行时提示
|
||||
- H5 语言切换后的动态刷新与兜底能力
|
||||
|
||||
当前项目已经完成一轮较完整的 i18n 基础建设与业务接入,核心主链路已可用,且具备继续扩展的基础。
|
||||
|
||||
## 2. 当前方案总览
|
||||
|
||||
### 2.1 核心运行时
|
||||
|
||||
项目以 `utils/i18n.uts` 作为统一多语言运行时,负责以下能力:
|
||||
|
||||
- 当前语言状态管理:`i18nState.currentLang`
|
||||
- 词典缓存管理:本地缓存语言包与语言列表
|
||||
- 文案查询:`t(key)`、`translateText(text)`
|
||||
- 模板变量替换:支持 `{name}` 这类占位符
|
||||
- 初始化加载:应用启动后加载远端语言包
|
||||
- 语言切换:`setLanguage(lang)`
|
||||
- 用户语言同步:`syncLanguageFromUser(lang)`
|
||||
- H5 tabBar 文案同步:运行时调用 `uni.setTabBarItem`
|
||||
|
||||
这意味着项目内新增文案时,优先应接入这一层,而不是在页面里各自维护独立字典。
|
||||
|
||||
### 2.2 语言资源组织
|
||||
|
||||
当前语言资源由三层组成:
|
||||
|
||||
1. 远端词典
|
||||
服务端通过 i18n 接口返回当前语言的词典映射,作为首选来源。
|
||||
|
||||
2. 本地默认语言文件
|
||||
作为平台导入与本地兜底来源,主要文件为:
|
||||
- `constants/i18n-locales/zh-CN.uts`
|
||||
- `constants/i18n-locales/zh-HK.uts`
|
||||
- `constants/i18n-locales/zh-TW.uts`
|
||||
- `constants/i18n-locales/en-US.uts`
|
||||
|
||||
当前项目已采用“标准语言码 + bundle 文件 key”双层方案:
|
||||
- 标准语言码:`zh-CN`、`zh-TW`、`zh-HK`、`en-US`
|
||||
- bundle 文件 key:`zh-cn`、`zh-tw`、`zh-hk`、`en-us`
|
||||
|
||||
其中,运行时统一处理标准语言码归一,本地词包仍按小写文件 key 组织,避免将业务语言码与资源文件命名耦合在一起。
|
||||
|
||||
3. 本地字面量映射与兜底层
|
||||
`constants/i18n.local.constants.uts` 负责:
|
||||
- 把历史遗留的中文字面量映射到 `Mobile.*` key
|
||||
- 在服务端未返回某个词条时,回退到本地默认语言
|
||||
- 在旧代码仍传原始中文时,尽量兼容显示
|
||||
|
||||
### 2.3 Key 规范
|
||||
|
||||
当前项目统一采用 `Mobile.*` 前缀管理业务文案,例如:
|
||||
|
||||
- `Mobile.Nav.home`
|
||||
- `Mobile.Auth.login`
|
||||
- `Mobile.Chat.skill`
|
||||
- `Mobile.ThirdParty.MpHtml.copyCode`
|
||||
|
||||
这套命名已经覆盖导航、登录、聊天、文件预览、权限、第三方组件等主要模块。
|
||||
|
||||
### 2.4 框架组件与业务组件接入方式
|
||||
|
||||
当前推荐的接入方式分为两类:
|
||||
|
||||
1. 直接传 key
|
||||
适用于明确是固定文案的标题、按钮、占位符,例如:
|
||||
- `title="Mobile.Nav.agent"`
|
||||
- `placeholder="Mobile.AgentSearch.placeholder"`
|
||||
|
||||
2. 组件内部统一走 `translateText()`
|
||||
适用于公共组件既要兼容 i18n key,也要兼容旧调用方直接传中文或服务端返回文本的场景。
|
||||
|
||||
这样做的好处是:
|
||||
|
||||
- 新代码可以直接传 key
|
||||
- 旧代码不必一次性全部重构
|
||||
- 切语言时,动态文案仍可重新计算
|
||||
|
||||
### 2.5 第三方库接入方式
|
||||
|
||||
项目新增了 `utils/i18n-third-party.uts` 作为第三方语言适配层,当前职责包括:
|
||||
|
||||
- 维护第三方库当前语言状态
|
||||
- 对外暴露统一同步入口 `syncThirdPartyLocales(lang)`
|
||||
- 为第三方组件提供适配后的文案对象
|
||||
|
||||
目前已经落地的典型模式有:
|
||||
|
||||
1. 显式传入文案对象
|
||||
例如给 `uni-load-more` 提供 `content-text`,避免继续使用组件内部默认语言逻辑。
|
||||
|
||||
2. 直接修改第三方组件内部用户可见文案
|
||||
对一些不支持外部 locale 注入、但业务真实使用到的组件,直接将内部提示接到项目 i18n。
|
||||
|
||||
3. 避免循环依赖
|
||||
第三方适配层不直接依赖运行时 `t()`,而是只依赖语言状态和本地兜底能力,降低循环引用风险。
|
||||
|
||||
## 3. 已完成能力
|
||||
|
||||
### 3.1 基础能力
|
||||
|
||||
当前已完成以下基础建设:
|
||||
|
||||
- i18n 运行时搭建完成
|
||||
- 语言列表、语言词典、当前语言缓存机制完成
|
||||
- H5 初始化语言加载接入完成
|
||||
- H5 语言切换能力接入完成
|
||||
- 用户登录后语言与后端用户配置同步完成
|
||||
- H5 tabBar 文案运行时同步完成
|
||||
- 本地默认语言文件拆分完成
|
||||
- `zh-TW` / `zh-HK` 默认兜底语言文件已补齐
|
||||
- i18n 标准语言码已统一为 `zh-CN / zh-TW / zh-HK / en-US`
|
||||
- 本地字面量到 key 的兼容映射完成
|
||||
|
||||
### 3.2 业务主链路覆盖
|
||||
|
||||
当前主业务链路已完成较系统的文案接入,包括但不限于:
|
||||
|
||||
- 首页与 tab 页面
|
||||
- 登录、验证码、重置密码链路
|
||||
- 智能体列表与分类页
|
||||
- 搜索页
|
||||
- 临时会话页
|
||||
- 聊天主链路
|
||||
- 更多信息、相关会话、文件树、文件预览
|
||||
- H5 webview 扩展页面
|
||||
- 个人中心与语言切换页
|
||||
|
||||
H5 主链路中的标题、按钮、toast、modal、placeholder、loading、空态文案、部分 `alt` 文本都已经纳入统一管理。
|
||||
微信小程序当前仍采用“固定简中”策略,因此小程序侧的静态中文配置属于当前设计范围,不视为本轮遗漏。
|
||||
|
||||
### 3.3 公共组件与基础组件覆盖
|
||||
|
||||
以下类型的组件已完成较多接入:
|
||||
|
||||
- 导航类组件:如 `custom-nav-bar`
|
||||
- 列表/分页类组件:如 `published-agent-list`
|
||||
- 下拉菜单/标签类组件:如 `menu-dropdown`
|
||||
- 空态和图片描述类组件
|
||||
- 聊天输入、录音、技能选择等高频组件
|
||||
|
||||
这些组件已逐步改成:
|
||||
|
||||
- 默认值使用 i18n key
|
||||
- 渲染时通过 `translateText()` 兼容旧值与 key
|
||||
- 避免只在初始化时计算一次文案,降低切语言后不刷新的风险
|
||||
|
||||
### 3.4 第三方库接入进度
|
||||
|
||||
当前已处理的第三方或框架相关模块包括:
|
||||
|
||||
- `uni-load-more`
|
||||
- `mp-html`
|
||||
- `uni-ai-x`
|
||||
- `uni-captcha`
|
||||
- `x-tools`
|
||||
- `lime-cascader` 的业务实际调用路径
|
||||
|
||||
已完成的接入类型包括:
|
||||
|
||||
- 复制成功/失败提示
|
||||
- 加载中/无更多/加载更多文案
|
||||
- 弹窗标题、按钮、提示文案
|
||||
- markdown 内部按钮与执行结果相关文案
|
||||
- 验证码组件 placeholder、标题、错误提示
|
||||
- 文件或系统工具提示文案
|
||||
|
||||
## 4. 当前进度判断
|
||||
|
||||
从当前仓库状态看,可以把进度分为三个层次:
|
||||
|
||||
### 4.1 已经完成
|
||||
|
||||
- H5 主业务用户可见文案的多语言主链路基本打通
|
||||
- H5 多语言切换、初始化加载、用户语言同步已具备
|
||||
- 多数高频公共组件已收口到统一方案
|
||||
- 多个真实使用中的第三方库已完成接入
|
||||
|
||||
### 4.2 基本完成但仍需维护
|
||||
|
||||
- 历史兼容路径仍存在部分“中文字面量 -> key”的桥接逻辑
|
||||
- 某些组件仍需依赖 `translateText()` 兼容旧调用方式
|
||||
- 第三方库新增升级后,仍需按既有方案继续补接
|
||||
|
||||
### 4.3 尚未完全收敛的部分
|
||||
|
||||
当前仓库里仍可能存在以下残留,但优先级较低:
|
||||
|
||||
- 注释中的中文
|
||||
- 调试日志中的中文
|
||||
- `unpackage` 构建产物中的旧文案
|
||||
- 第三方库 demo / example 页面中的演示文案
|
||||
- 未被当前业务真正引用的三方组件内部默认文案
|
||||
|
||||
另外,`pages.json` 等配置层仍保留部分静态中文默认值。
|
||||
在当前“小程序固定简中、H5 运行时覆盖”的策略下,这些配置属于平台级默认值,而不是本轮多语言漏接项。
|
||||
|
||||
这些内容不会直接阻塞当前业务多语言能力,但会影响后续全仓扫描时的噪音与维护成本。
|
||||
|
||||
另外,从语言资源建设角度看,当前已经完成繁中默认资源补齐,但后续仍需要继续关注:
|
||||
|
||||
- `zh-TW` 与 `zh-HK` 是否需要进一步按地区表达细化词条
|
||||
- 后端语言列表与前端标准语言码是否完全一致
|
||||
|
||||
这类工作属于语言资源层和接口协同层的持续优化,不会改变当前主链路方案,但会影响后续多地区文案的一致性。
|
||||
|
||||
## 5. 已知边界与风险
|
||||
|
||||
### 5.1 H5 与小程序行为不同
|
||||
|
||||
当前策略中:
|
||||
|
||||
- H5 支持动态语言切换
|
||||
- 微信小程序仍保持固定简中策略,当前不提供动态切换能力
|
||||
|
||||
因此如果后续要扩展小程序语言切换,需要重新评估:
|
||||
|
||||
- 平台能力限制
|
||||
- 页面初始化时机
|
||||
- tabBar 与页面标题刷新策略
|
||||
|
||||
### 5.2 第三方库不一定支持标准 locale 注入
|
||||
|
||||
不是所有第三方库都提供官方多语言入口,当前项目实际采用了三种手段:
|
||||
|
||||
- 外部传入文案
|
||||
- 包装适配
|
||||
- 直接修改第三方源码
|
||||
|
||||
这意味着第三方升级时,需要特别关注:
|
||||
|
||||
- 改动是否被覆盖
|
||||
- API 是否变化
|
||||
- 已接入的 i18n patch 是否失效
|
||||
|
||||
### 5.3 历史兼容逻辑仍然存在
|
||||
|
||||
为降低一次性迁移成本,当前保留了对旧文案传入方式的兼容。这带来两个结果:
|
||||
|
||||
- 优点:旧代码不必一次性全部重构
|
||||
- 风险:新同学可能继续误传中文字面量,而不是直接传 key
|
||||
|
||||
因此后续应逐步推动“新增代码默认传 key”成为团队共识。
|
||||
|
||||
## 6. 推荐接入规范
|
||||
|
||||
后续新增页面、组件或三方库时,建议统一遵循以下规范。
|
||||
|
||||
### 6.1 新增业务文案
|
||||
|
||||
- 优先新增 `Mobile.*` key
|
||||
- 同步补齐 `zh-CN.uts`、`zh-HK.uts`、`zh-TW.uts` 与 `en-US.uts`
|
||||
- 若涉及历史兼容,再视情况补 `i18n.local.constants.uts`
|
||||
|
||||
### 6.2 公共组件
|
||||
|
||||
- 组件 `props` 默认值尽量写 key,不直接写中文
|
||||
- 渲染时统一走 `translateText()`
|
||||
- 对只允许固定 key 的新组件,可在文档中明确约束,逐步减少兼容负担
|
||||
|
||||
### 6.3 用户提示类 API
|
||||
|
||||
以下场景必须优先检查是否已接 i18n:
|
||||
|
||||
- `uni.showToast`
|
||||
- `uni.showModal`
|
||||
- `uni.showLoading`
|
||||
- `uni.setNavigationBarTitle`
|
||||
- 输入框 `placeholder`
|
||||
- 图片 `alt`
|
||||
|
||||
### 6.4 第三方库
|
||||
|
||||
新增第三方库接入时,优先按以下顺序评估:
|
||||
|
||||
1. 是否支持官方 locale 配置
|
||||
2. 是否可以通过 props 外部注入文案
|
||||
3. 是否需要在 `utils/i18n-third-party.uts` 增加适配入口
|
||||
4. 是否必须 patch 第三方源码
|
||||
|
||||
## 7. 后续建议
|
||||
|
||||
基于当前进度,后续建议按以下顺序推进:
|
||||
|
||||
1. 补充这份报告对应的维护约定
|
||||
包括“新增文案必须补双语 key”“新增第三方需评估 locale 入口”等约束。
|
||||
|
||||
2. 持续校验标准语言码与 bundle key 的映射关系
|
||||
重点确保 `zh-CN / zh-TW / zh-HK / en-US` 与本地词包 key、后端接口返回值、缓存值保持一致。
|
||||
|
||||
3. 持续做低优先级清理
|
||||
将业务源码中的中文日志、历史 fallback、非核心残留继续收敛,降低全仓扫描噪音。
|
||||
|
||||
4. 继续完善第三方审计
|
||||
对真实业务已使用但尚未完全收口的第三方组件,做按需补接。
|
||||
|
||||
5. 结合现有审计能力持续验证
|
||||
当前已有执行记录文档与多轮扫描结果,后续建议将报告、规则与审计结果保持同步。
|
||||
|
||||
## 8. 结论
|
||||
|
||||
截至当前版本,`qiming-mobile` 的多语言能力已经从“页面零散替换”演进为“统一运行时 + 本地兜底 + 第三方适配”的可维护方案。
|
||||
|
||||
从业务价值上看,当前主链路已经具备实际可用的多语言支撑能力;从工程角度看,也已经形成了继续扩展所需的基础结构。剩余工作主要集中在低优先级清理、第三方长尾收口,以及后续团队协作过程中的规范执行。
|
||||
112
qiming-mobile/docs/main-package-optimization.md
Normal file
112
qiming-mobile/docs/main-package-optimization.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# 主包大小优化说明
|
||||
|
||||
## 问题描述
|
||||
主包大小:3378KB,超过微信小程序 2MB(2048KB)限制。
|
||||
|
||||
## 优化措施
|
||||
|
||||
### 1. 删除未使用的 uni-ai-x 静态资源
|
||||
|
||||
#### 已删除的资源:
|
||||
- **ai-provider 图片**(672K):所有 AI 服务商 logo 图片
|
||||
- azure.png, baidu.png, bailian.png, deepseek.png
|
||||
- doubao.png, ifly.png, minimax.png, openai.png
|
||||
- qwen.png, siliconflow.png
|
||||
- 说明:项目中只使用了 bailian,其他图片未使用
|
||||
|
||||
- **uni-chat-logo.png**(188K)
|
||||
- 说明:代码中已被注释掉,未使用
|
||||
|
||||
- **default-avatar.png**(4K)
|
||||
- 说明:未使用
|
||||
|
||||
#### 已删除的字体:
|
||||
- **uni-ai-x/static/font/FiraCode-Regular.ttf**(284K)
|
||||
- 说明:代码块字体,项目中未直接使用
|
||||
|
||||
### 2. 删除 mp-html 中的字体资源
|
||||
|
||||
#### 已删除的字体:
|
||||
- **mp-html/static/font/FiraCode-Regular.ttf**(283K)
|
||||
- 说明:代码高亮字体,已修复引用
|
||||
|
||||
### 3. 优化 KaTeX 数学公式字体
|
||||
|
||||
#### 已删除的不常用字体:
|
||||
- **KaTeX_Typewriter-Regular.ttf**(27K)
|
||||
- **KaTeX_Size3-Regular.ttf**(7.4K)
|
||||
- **KaTeX_Size4-Regular.ttf**(10K)
|
||||
|
||||
#### 保留的必要字体:
|
||||
- KaTeX_AMS-Regular.ttf(62K)
|
||||
- KaTeX_Main-Regular.ttf(52K)
|
||||
- KaTeX_Main-Bold.ttf(50K)
|
||||
- KaTeX_Main-Italic.ttf(33K)
|
||||
- KaTeX_Main-BoldItalic.ttf(32K)
|
||||
- KaTeX_Math-Italic.ttf(31K)
|
||||
- KaTeX_Math-BoldItalic.ttf(30K)
|
||||
- KaTeX_SansSerif-Regular.ttf(19K)
|
||||
- KaTeX_SansSerif-Bold.ttf(24K)
|
||||
- KaTeX_SansSerif-Italic.ttf(22K)
|
||||
- KaTeX_Size1-Regular.ttf(12K)
|
||||
- KaTeX_Size2-Regular.ttf(11K)
|
||||
|
||||
## 优化效果统计
|
||||
|
||||
### 已删除的资源总大小:
|
||||
- ai-provider 图片:672K
|
||||
- uni-chat-logo.png:188K
|
||||
- FiraCode 字体(uni-ai-x):284K
|
||||
- FiraCode 字体(mp-html):283K
|
||||
- KaTeX Typewriter 字体:27K
|
||||
- KaTeX Size3 字体:7.4K
|
||||
- KaTeX Size4 字体:10K
|
||||
- **总计:约 1.48MB**
|
||||
|
||||
### 预期优化后主包大小:
|
||||
- 优化前:3378KB(约 3.3MB)
|
||||
- 优化后:预计 < 1900KB(约 1.9MB)
|
||||
- **减小:约 1.48MB**
|
||||
|
||||
## 修改的文件
|
||||
|
||||
1. **uni_modules/mp-html/components/mp-html/styles/highlight.scss**
|
||||
- 注释掉 FiraCode 字体引用
|
||||
|
||||
2. **uni_modules/mp-html/components/mp-html/styles/katex-fonts.scss**
|
||||
- 注释掉 Typewriter、Size3、Size4 字体定义
|
||||
|
||||
3. **删除的文件**
|
||||
- `uni_modules/uni-ai-x/static/ai-provider/*.png`
|
||||
- `uni_modules/uni-ai-x/static/uni-chat-logo.png`
|
||||
- `uni_modules/uni-ai-x/static/default-avatar.png`
|
||||
- `uni_modules/uni-ai-x/static/font/FiraCode-Regular.ttf`
|
||||
- `uni_modules/mp-html/static/font/FiraCode-Regular.ttf`
|
||||
- `static/fonts/KaTeX_Typewriter-Regular.ttf`
|
||||
- `static/fonts/KaTeX_Size3-Regular.ttf`
|
||||
- `static/fonts/KaTeX_Size4-Regular.ttf`
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **FiraCode 字体**:如果未来需要代码高亮字体,需要重新添加并修改引用
|
||||
|
||||
2. **KaTeX 特殊字体**:
|
||||
- Typewriter 字体已删除,某些等宽文本可能显示异常
|
||||
- Size3、Size4 字体已删除,特大字号可能显示异常
|
||||
|
||||
3. **AI Provider 图片**:如果未来需要添加新的 AI 服务商,需要在 `uni_modules/uni-ai-x/static/ai-provider/` 目录下添加对应的 logo 图片
|
||||
|
||||
4. **验证方法**:
|
||||
- 重新构建项目
|
||||
- 检查主包大小是否 < 2MB
|
||||
- 测试数学公式渲染是否正常
|
||||
- 测试代码块显示是否正常
|
||||
|
||||
## 后续优化建议
|
||||
|
||||
如果主包仍然超过 2MB,可以考虑:
|
||||
1. 启用分包策略(subPackages)
|
||||
2. 进一步压缩图片资源
|
||||
3. 移除更多未使用的组件和模块
|
||||
4. 使用 CDN 加载静态资源
|
||||
|
||||
62
qiming-mobile/docs/switch-git-remote.md
Normal file
62
qiming-mobile/docs/switch-git-remote.md
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
description: 如何在 GitLab 和 GitHub 之间切换 Git 远程仓库
|
||||
---
|
||||
|
||||
# Git 远程仓库切换指南
|
||||
|
||||
本项目同时托管在 GitLab 和 GitHub,可根据需要切换远程仓库地址。
|
||||
|
||||
## 仓库地址
|
||||
|
||||
| 平台 | 仓库地址 |
|
||||
| ------ | ------------------------------------------------------------------------ |
|
||||
| GitLab | `https://git.yichamao.com/agent-platform/agent-platform-front-weapp.git` |
|
||||
| GitHub | `https://github.com/qiming-ai/qiming-mobile.git` |
|
||||
|
||||
## 查看当前远程仓库
|
||||
|
||||
```bash
|
||||
git remote -v
|
||||
```
|
||||
|
||||
## 切换到 GitLab
|
||||
|
||||
```bash
|
||||
git remote set-url origin https://git.yichamao.com/agent-platform/agent-platform-front-weapp.git
|
||||
```
|
||||
|
||||
## 切换到 GitHub
|
||||
|
||||
```bash
|
||||
git remote set-url origin https://github.com/qiming-ai/qiming-mobile.git
|
||||
```
|
||||
|
||||
## 验证切换是否成功
|
||||
|
||||
```bash
|
||||
git remote -v
|
||||
```
|
||||
|
||||
输出应显示新的远程仓库地址,例如:
|
||||
|
||||
```
|
||||
origin https://github.com/qiming-ai/qiming-mobile.git (fetch)
|
||||
origin https://github.com/qiming-ai/qiming-mobile.git (push)
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 切换远程仓库前,请确保本地代码已提交或暂存
|
||||
2. 切换后首次 `git pull` 可能需要处理分支追踪关系
|
||||
3. 如需同时保留两个远程仓库,可以添加新的 remote 而非修改 origin:
|
||||
```bash
|
||||
# 添加 GitLab 为额外的远程仓库
|
||||
git remote add gitlab https://git.yichamao.com/agent-platform/agent-platform-front-weapp.git
|
||||
|
||||
# 添加 GitHub 为额外的远程仓库
|
||||
git remote add github https://github.com/qiming-ai/qiming-mobile.git
|
||||
|
||||
# 推送到指定远程
|
||||
git push gitlab main
|
||||
git push github main
|
||||
```
|
||||
35
qiming-mobile/docs/uni-modules-cleanup.md
Normal file
35
qiming-mobile/docs/uni-modules-cleanup.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# uni_modules 清理记录
|
||||
|
||||
## 背景
|
||||
本次清理目标是收缩 `uni_modules` 目录体积,优先删除未使用模块、文档文件、演示壳文件和测试遗留文件,不涉及业务功能逻辑调整,也不主动修改仍在运行链路中的核心组件代码。
|
||||
|
||||
## 本次已删除内容
|
||||
### 1. 整体删除的未使用模块
|
||||
- `uni_modules/uni-table`
|
||||
- `uni_modules/uni-datetime-picker`
|
||||
- `uni_modules/uni-config-center`
|
||||
|
||||
### 2. 删除的文档类文件
|
||||
- 各模块下的 `README.md` / `readme.md`
|
||||
- 各模块下的 `CHANGELOG.md` / `changelog.md`
|
||||
- `uni_modules/uni-ai-x/license.md`
|
||||
|
||||
### 3. 删除的 demo / 测试遗留文件
|
||||
- `lime-*` 模块中仅用于演示的外层 demo 壳文件,例如 `components/lime-*/lime-*.(vue|uvue)`
|
||||
- `uni_modules/lime-color/common/test.uts`
|
||||
- `uni_modules/lime-checkbox/components/l-checkbox/l-checkbox_old.uvue`
|
||||
|
||||
## 保留原则
|
||||
- 保留业务直接使用的模块与组件。
|
||||
- 保留被依赖链间接使用的模块,例如 `lime-cascader` 依赖的一组 `lime-*` 基础模块。
|
||||
- 保留运行时资源文件,例如字体、图标映射、GIF、富文本高亮资源。
|
||||
- 保留平台兼容相关文件,避免影响 H5、微信小程序和 App 多端行为。
|
||||
|
||||
## 验证结果
|
||||
- 已对删除目标做残留引用检索。
|
||||
- 当前未发现新增悬空引用。
|
||||
- 当前未发现新增 linter 报错。
|
||||
|
||||
## 后续建议
|
||||
- 如需继续瘦身,优先继续盘点 `static/`、`docs/`、`unpackage/` 等目录中的可清理文件。
|
||||
- 不建议继续直接删除 `uni_modules` 中的运行时代码或资源,除非先完成更细的依赖与平台验证。
|
||||
Reference in New Issue
Block a user