From 8b5c6a454155c0cc8cb5e84bbbc06f9df70b2db2 Mon Sep 17 00:00:00 2001 From: wangdan-fit2cloud Date: Fri, 9 Oct 2026 19:35:31 +0800 Subject: [PATCH] feat: upload document --- ui/src/api/API_README.md | 34 +- ui/src/api/admin/file.ts | 7 +- .../api/admin/workspace/knowledge/document.ts | 64 +++- .../admin/workspace/knowledge/knowledge.ts | 2 + .../api/admin/workspace/knowledge/workflow.ts | 5 + ui/src/api/types/document.ts | 46 ++- ui/src/assets/file-type/file-qa-icon.svg | 4 + ui/src/assets/file-type/file-table-icon.svg | 11 + ui/src/components/COMPONENT_README.md | 36 +- .../GenerateQuestionsDialog.vue | 5 +- .../mk-drag-upload/MultipleFileUpload.vue | 323 ++++++++++++++++++ .../mk-drag-upload/SingleFileUpload.vue | 131 +++++++ ui/src/components/mk-drag-upload/index.vue | 143 ++++---- ui/src/components/mk-drag-upload/types.ts | 11 + ui/src/router/ROUTE_README.md | 1 + .../admin/workspace/modules/knowledge.ts | 2 +- ui/src/styles/element-plus.scss | 16 +- ui/src/views/VIEW_README.md | 48 ++- .../import-document/ButtonImportDocument.vue | 51 +++ .../LarkImportDocumentDrawer.vue | 201 +++++++++++ .../WebImportDocumentDrawer.vue | 134 ++++++++ .../WorkflowImportDocumentDrawer.vue | 164 +++++++++ .../library/document/index.vue | 36 +- .../upload-document/ButtonUploadDocument.vue | 58 ++++ .../upload-document/DocumentImportPreview.vue | 123 +++++++ .../upload-document/UploadDocumentDrawer.vue | 241 +++++++++++++ .../document/upload-document/constants.ts | 14 + .../create-knowledge/WebKnowledgeDrawer.vue | 96 +++--- .../components/DocumentStrategyForm.vue | 70 ++-- .../model/create-model/CreateModelDrawer.vue | 10 +- 30 files changed, 1898 insertions(+), 189 deletions(-) create mode 100644 ui/src/assets/file-type/file-qa-icon.svg create mode 100644 ui/src/assets/file-type/file-table-icon.svg create mode 100644 ui/src/components/mk-drag-upload/MultipleFileUpload.vue create mode 100644 ui/src/components/mk-drag-upload/SingleFileUpload.vue create mode 100644 ui/src/components/mk-drag-upload/types.ts create mode 100644 ui/src/views/knowledge-detail/library/document/import-document/ButtonImportDocument.vue create mode 100644 ui/src/views/knowledge-detail/library/document/import-document/LarkImportDocumentDrawer.vue create mode 100644 ui/src/views/knowledge-detail/library/document/import-document/WebImportDocumentDrawer.vue create mode 100644 ui/src/views/knowledge-detail/library/document/import-document/WorkflowImportDocumentDrawer.vue create mode 100644 ui/src/views/knowledge-detail/library/document/upload-document/ButtonUploadDocument.vue create mode 100644 ui/src/views/knowledge-detail/library/document/upload-document/DocumentImportPreview.vue create mode 100644 ui/src/views/knowledge-detail/library/document/upload-document/UploadDocumentDrawer.vue create mode 100644 ui/src/views/knowledge-detail/library/document/upload-document/constants.ts diff --git a/ui/src/api/API_README.md b/ui/src/api/API_README.md index 81713e8cedc..b625b32d29f 100644 --- a/ui/src/api/API_README.md +++ b/ui/src/api/API_README.md @@ -409,6 +409,11 @@ API 对象和工作空间上下文,作为该抽屉的范围选择例外;用 `KnowledgeWorkflowDetail`。前端详情与保存协议的 `default_model_setting` 复用 `DefaultModelSettingPayload`;服务端需支持该字段的持久化与回传(当前仓库知识库后端尚未实现)。 +`postKnowledgeWorkflowImport(knowledgeId, payload)` 使用 POST `//upload_document` +执行已发布版本的文档导入,参数复用 `KnowledgeWorkflowDebugPayload`,结果为 +`KnowledgeWorkflowAction`;调试仍调用独立的 `/debug`。文档页导入 Drawer 复用工作流的 +数据源、知识库输入及结果 Action,执行详情沿用 `getKnowledgeWorkflowAction`。 + ### 智能体复制 `ApplicationDetail` 复用 `ApplicationFormPayload` 中的配置字段,并保留详情接口的 `model` @@ -495,7 +500,8 @@ Admin 上传尚未创建的资源文件时,`sourceId` 可传 `undefined`,省 普通上传直接等待 `request`,需要进度时传入 `(percent, event)`;只有能获取上传总量时才回调 0–100 的百分比,100 表示请求体已上传,不代表服务端处理成功,完成状态以 `request` 为准。 取消时 Promise 仍拒绝,由调用方处理状态,请求层不弹出通用错误提示;loading 由调用方在 -`finally` 中恢复。 +`finally` 中恢复。Admin `FileApi.deleteFile(fileId)` 通过 DELETE `/oss/file/` 删除已上传文件, +返回值沿用请求层统一解包,不使用 v2 的 `Result` 包装。 资源类型使用 `@/api/enums` 的 `FILE_SOURCE_TYPE` 与 `@/api/types` 的 `FileSourceType`, 包括知识库、智能体、工具、文档、对话及三种临时文件有效期。对话 Store 使用 @@ -534,6 +540,27 @@ System 接口由资源管理服务提供;本地开源后端没有对应扩展 ### 知识库文档 +文档地址及飞书导入也维护在 `workspace/knowledge/document.ts`: +`postWebDocument` 向 `//document/web` 提交 `WebDocumentImportPayload`,包含 +`source_url_list`、`selector` 与 `doc_strategy`。 +`getLarkDocumentList` 沿用 POST `/lark///doc_list`,支持 `page_token`, +返回 `LarkDocumentList` 的 `files`、`has_more` 和 `next_page_token`。 +`postImportLarkDocuments` 沿用 POST `/lark//import` 的文档数组协议, +每项为 `LarkDocumentImportPayload`(名称、token、类型及处理策略)。飞书接口属于扩展端; +当前仓库未包含 v3 实现,`doc_strategy` 的持久化与执行需要扩展端支持,不能视为已联调。 +上述跨 API 与 UI 的类型统一通过 `@/api/types` 导出。 + +文档上传复用 `workspace/knowledge/document.ts`:`postSplitDocuments` 向 `document/split` +发送多文件 FormData(重复 `file` 字段)与 JSON 字符串 `doc_strategy`,返回 `DocumentSplitResult[]`; +页面将解析结果的 `content` 转为 `paragraphs`,保留 `source_file_id` 与策略,再通过 +`putBatchCreateDocuments` PUT `document/batch_create` 创建文档,与空白文档创建共用同一接口封装。 +`postImportTableDocumentFiles` 和 `postImportQADocumentFiles` 分别向 `document/table` 和 +`document/qa` 发送多文件 FormData, +由服务端解析并创建。`exportTableDocumentTemplate` 和 `exportQADocumentTemplate` 分别使用 GET +`/workspace/knowledge/document/table_template/export` 和 `/workspace/knowledge/document/template/export`,查询参数 `type` 为 +`excel` / `csv`。上传解析与创建类型维护在 `types/document.ts`,统一通过 `@/api/types` 导入。 +共享文档 API 不提供上传写入方法。 + `putDocument(knowledgeId, documentId, data)` 统一通过 PUT `/` 更新名称、启用状态和单项设置, 不按更新字段重复封装同一接口。载荷 `DocumentUpdatePayload` 仅包含可更新字段,按需传入 `name`、`is_active`、`hit_handling_method`、`directly_return_similarity`、`meta`,返回更新后的 @@ -567,8 +594,9 @@ System 接口由资源管理服务提供;本地开源后端没有对应扩展 `workspace/knowledge/document.ts` 维护文档分页查询,路径为 `/workspace//knowledge//document//`。 -`putQuickCreateDocuments(knowledgeId, documents)` 通过 PUT `document/batch_create` 创建空白文档, -载荷为 `DocumentQuickCreatePayload[]`(仅含 `name`),返回 `DocumentItem[]`;名称上限 128 字符。 +`putBatchCreateDocuments(knowledgeId, documents)` 统一通过 PUT `document/batch_create` 批量创建文档, +空白创建提交 `DocumentQuickCreatePayload[]`(仅含 `name`),解析后创建提交 `DocumentImportPayload[]`, +返回 `DocumentItem[]`;名称上限 128 字符。不按创建场景重复封装同一接口。 共享文档 API 不增加创建接口;文档页监听公共 MkQuickCreate 的 `create` 事件,管理创建请求和提交状态。 分页使用 `ParamsPage` / `ResponsePage`,支持 `name`、`create_user`、`status`、`task_type`、`is_active`、`hit_handling_method` 和 `tags` 筛选; 文件任务使用 `DOCUMENT_TASK_STATE` 的字符状态及 `DOCUMENT_TASK_TYPE` 的任务位置, diff --git a/ui/src/api/admin/file.ts b/ui/src/api/admin/file.ts index 4276834a5fe..3fb5eaf7ff0 100644 --- a/ui/src/api/admin/file.ts +++ b/ui/src/api/admin/file.ts @@ -1,5 +1,5 @@ import type { AxiosProgressEvent } from 'axios' -import { postUpload } from './core/request' +import { postUpload, del } from './core/request' import type { FileSourceType } from '@/api/types' /** 上传资源文件,支持可选的进度回调与取消操作。 */ @@ -16,4 +16,7 @@ const postUploadFile = ( return postUpload('/oss/file', formData, onProgress) } -export default { postUploadFile } +/** 删除已上传的资源文件。 */ +const deleteFile = (fileId: string) => del(`/oss/file/${fileId}`) + +export default { postUploadFile, deleteFile } diff --git a/ui/src/api/admin/workspace/knowledge/document.ts b/ui/src/api/admin/workspace/knowledge/document.ts index 6e6cc82c8fc..20e43a9f48e 100644 --- a/ui/src/api/admin/workspace/knowledge/document.ts +++ b/ui/src/api/admin/workspace/knowledge/document.ts @@ -3,12 +3,18 @@ import type { ParamsPage, ResponsePage } from '../../core/types' import type { Dict, DocumentGeneratePayload, + DocumentImportPayload, + DocumentSplitResult, + DocumentStrategy, DocumentItem, DocumentQuickCreatePayload, DocumentSettingPayload, DocumentTaskState, DocumentTaskType, DocumentUpdatePayload, + WebDocumentImportPayload, + LarkDocumentList, + LarkDocumentImportPayload, } from '@/api/types' import { getWorkspaceId } from '@/utils/resource-context' @@ -18,9 +24,51 @@ const getPrefix = (knowledgeId: string) => `/workspace/${getWorkspaceId()}/knowl const getDocumentPage = (knowledgeId: string, page: ParamsPage, query?: Dict) => get>(`${getPrefix(knowledgeId)}/${page.currentPage}/${page.pageSize}`, query) -/** 批量创建仅包含名称的空白文档。 */ -const putQuickCreateDocuments = (knowledgeId: string, documents: DocumentQuickCreatePayload[]) => - put(`${getPrefix(knowledgeId)}/batch_create`, documents) +/** 批量创建空白文档或导入text文档。 */ +const putBatchCreateDocuments = (knowledgeId: string, documents: DocumentQuickCreatePayload[] | DocumentImportPayload[]) => + put(`${getPrefix(knowledgeId)}/batch_create`, documents) + +/** 按处理策略解析文本文件,返回分段与原文件关联信息。 */ +const postSplitDocuments = (knowledgeId: string, files: File[], strategy: DocumentStrategy) => { + const data = new FormData() + files.forEach((file) => data.append('file', file)) + data.append('doc_strategy', JSON.stringify(strategy)) + return post(`${getPrefix(knowledgeId)}/split`, data) +} + +/** 导入表格文件,每个 sheet 由服务端创建独立文档。 */ +const postImportTableDocumentFiles = (knowledgeId: string, files: File[]) => { + const data = new FormData() + files.forEach((file) => data.append('file', file)) + return post(`${getPrefix(knowledgeId)}/table`, data) +} + +/** 导入 QA 问答对文件。 */ +const postImportQADocumentFiles = (knowledgeId: string, files: File[]) => { + const data = new FormData() + files.forEach((file) => data.append('file', file)) + return post(`${getPrefix(knowledgeId)}/qa`, data) +} + +/** 按地址导入 Web 文档并应用文档处理策略。 */ +const postWebDocument = (knowledgeId: string, payload: WebDocumentImportPayload) => + post(`${getPrefix(knowledgeId)}/web`, payload) + +/** 查询飞书文件夹中的文件,支持分页加载。 */ +const getLarkDocumentList = (knowledgeId: string, folderToken: string, query: { page_token?: string } = {}) => + post<{ page_token?: string }, LarkDocumentList>(`/workspace/${getWorkspaceId()}/knowledge/lark/${knowledgeId}/${folderToken}/doc_list`, query) + +/** 导入所选飞书文档,沿用飞书扩展接口。 */ +const postImportLarkDocuments = (knowledgeId: string, documents: LarkDocumentImportPayload[]) => + post(`/workspace/${getWorkspaceId()}/knowledge/lark/${knowledgeId}/import`, documents) + +/** 下载表格模板。 */ +const exportTableDocumentTemplate = (format: 'excel' | 'csv') => + getExportFile(`${format}-template.${format === 'excel' ? 'xlsx' : 'csv'}`, '/workspace/knowledge/document/table_template/export', { type: format }) + +/** 下载 QA 模板。 */ +const exportQADocumentTemplate = (format: 'excel' | 'csv') => + getExportFile(`${format}-template.${format === 'excel' ? 'xlsx' : 'csv'}`, '/workspace/knowledge/document/template/export', { type: format }) /** 更新文档名称、启用状态或设置,返回保存后的文档。 */ const putDocument = (knowledgeId: string, documentId: string, data: DocumentUpdatePayload) => @@ -113,7 +161,15 @@ const postReplaceDocumentSource = (knowledgeId: string, documentId: string, file export default { getDocumentPage, - putQuickCreateDocuments, + putBatchCreateDocuments, + postSplitDocuments, + postImportTableDocumentFiles, + postImportQADocumentFiles, + postWebDocument, + getLarkDocumentList, + postImportLarkDocuments, + exportTableDocumentTemplate, + exportQADocumentTemplate, putDocument, deleteDocument, putBatchDeleteDocuments, diff --git a/ui/src/api/admin/workspace/knowledge/knowledge.ts b/ui/src/api/admin/workspace/knowledge/knowledge.ts index 2f736c516c4..197f938d5fe 100644 --- a/ui/src/api/admin/workspace/knowledge/knowledge.ts +++ b/ui/src/api/admin/workspace/knowledge/knowledge.ts @@ -126,6 +126,8 @@ const getKnowledgeMcpConfig = (knowledgeId: string): Promise => const putKnowledgeKeywordIndex = (knowledgeId: string) => put(`${getPrefix()}/${knowledgeId}/tokenize`) /** 获取知识库按标签名称分组的标签选项。 */ + +// TODO const getKnowledgeTags = (knowledgeId: string) => get(`${getPrefix()}/${knowledgeId}/tags`) export default { diff --git a/ui/src/api/admin/workspace/knowledge/workflow.ts b/ui/src/api/admin/workspace/knowledge/workflow.ts index 50fac32d9f1..44030c1332a 100644 --- a/ui/src/api/admin/workspace/knowledge/workflow.ts +++ b/ui/src/api/admin/workspace/knowledge/workflow.ts @@ -43,6 +43,10 @@ const postKnowledgeWorkflowDebug = (knowledgeId: string, payload: KnowledgeWorkf return post(`${getPrefix()}/${knowledgeId}/debug`, payload) } +/** 使用已发布知识库工作流执行正式文档导入。 */ +const postKnowledgeWorkflowImport = (knowledgeId: string, payload: KnowledgeWorkflowDebugPayload) => + post(`${getPrefix()}/${knowledgeId}/upload_document`, payload) + /** 获取知识库工作流执行详情,供调试轮询和执行记录共用。 */ const getKnowledgeWorkflowAction = (knowledgeId: string, actionId: string) => { return get(`${getPrefix()}/${knowledgeId}/action/${actionId}`) @@ -76,6 +80,7 @@ export default { putKnowledgeWorkflowPublish, getKnowledgeWorkflowFormList, postKnowledgeWorkflowDebug, + postKnowledgeWorkflowImport, getKnowledgeWorkflowAction, postCancelKnowledgeWorkflowAction, } diff --git a/ui/src/api/types/document.ts b/ui/src/api/types/document.ts index 62ac0709509..44e5152e28d 100644 --- a/ui/src/api/types/document.ts +++ b/ui/src/api/types/document.ts @@ -1,5 +1,5 @@ import type { DOCUMENT_HIT_HANDLING, DOCUMENT_TASK_STATE, DOCUMENT_TASK_TYPE } from '@/api/enums' -import type { KnowledgeGeneratePayload, KnowledgeType } from './knowledge' +import type { DocumentStrategy, KnowledgeGeneratePayload, KnowledgeType } from './knowledge' export type DocumentHitHandling = (typeof DOCUMENT_HIT_HANDLING)[keyof typeof DOCUMENT_HIT_HANDLING] export type DocumentTaskState = (typeof DOCUMENT_TASK_STATE)[keyof typeof DOCUMENT_TASK_STATE] export type DocumentTaskType = (typeof DOCUMENT_TASK_TYPE)[keyof typeof DOCUMENT_TASK_TYPE] @@ -52,3 +52,47 @@ export interface DocumentItem { /** 单个文档的局部更新字段。 */ export type DocumentUpdatePayload = Partial> + +/** 文本解析结果及批量创建文档的载荷。 */ +export interface DocumentImportParagraph { + title?: string | null + content: string + problem_list?: { content: string }[] +} + +export interface DocumentImportPayload { + name: string + source_file_id?: string | null + doc_strategy: DocumentStrategy + paragraphs: DocumentImportParagraph[] +} + +export interface DocumentSplitResult extends Omit { + content: DocumentImportParagraph[] +} + +/** Web 地址导入及处理策略。 */ +export interface WebDocumentImportPayload { + source_url_list: string[] + selector: string + doc_strategy: DocumentStrategy +} + +/** 飞书文件树节点,已导入文件不可重复选择。 */ +export interface LarkDocumentNode { + name: string + token: string + type: string + is_exist: boolean +} + +export interface LarkDocumentList { + files: LarkDocumentNode[] + has_more?: boolean + next_page_token?: string +} + +/** 保留 v2 飞书数组协议,每个文件附带处理策略。 */ +export interface LarkDocumentImportPayload extends Pick { + doc_strategy: DocumentStrategy +} diff --git a/ui/src/assets/file-type/file-qa-icon.svg b/ui/src/assets/file-type/file-qa-icon.svg new file mode 100644 index 00000000000..15e37da16db --- /dev/null +++ b/ui/src/assets/file-type/file-qa-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/src/assets/file-type/file-table-icon.svg b/ui/src/assets/file-type/file-table-icon.svg new file mode 100644 index 00000000000..17b6f40a866 --- /dev/null +++ b/ui/src/assets/file-type/file-table-icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/ui/src/components/COMPONENT_README.md b/ui/src/components/COMPONENT_README.md index 6fa65f7182f..8853ea593c3 100644 --- a/ui/src/components/COMPONENT_README.md +++ b/ui/src/components/COMPONENT_README.md @@ -672,11 +672,37 @@ Logo 数据统一从 Theme Store 获取。 ### MkDragUpload -组合拖拽选择区和已选文件卡片,通过 `v-model` 管理 Element Plus `UploadUserFile[]`。`accept` -直接传给上传控件;`dragText`、`selectText`、`tipText` 和 `replaceText` 可替换展示文案。组件只负责 -文件选择与展示,使用方通过 `change` 执行校验和上传,通过 `remove` 清理业务数据;`download` -作用域插槽提供当前文件,由使用方按业务需要放置下载按钮。组件暴露 `clearFiles()`,用于请求失败 -或表单重置时清空上传控件内部状态。 +`mk-drag-upload/index.vue` 为统一入口,根据 `multiple` 选择 `SingleFileUpload.vue` 或 +`MultipleFileUpload.vue`,转发 Props、事件、插槽和公开方法。单文件的卡片、更换及整体清理逻辑 +放在 `SingleFileUpload.vue`,多文件的常驻拖拽区、文件列表及逐项删除逻辑放在 +`MultipleFileUpload.vue`,不在一个实现中混合两种模式的状态分支。 +两种模式均通过 `v-model` 管理兼容 Element Plus `UploadUserFile[]` 的 `DragUploadFile[]`, +类型定义位于 `mk-drag-upload/types.ts`,可携带 `file_id`、`errMsg` 和 `canRetry`。 +`multiple` 默认为 `false`,单文件模式选择后展示默认卡片和更换入口;传入 `multiple` 后支持 +多文件选择,拖拽区保持显示。`accept`、`limit` 直接传给上传控件,多文件模式内部也校验文件格式、 +空文件和重复文件;超出数量时提示并发出 `exceed`。`sizeLimit` 为多文件的单文件大小上限(MB), +默认 100;超限保留 `fail` 卡片和大小错误提示,不允许重试。格式不支持、空文件及重复选择移除; +目录中的 `.DS_Store` 格式不支持时静默移除。 +`dragText`、`selectText`、`tipText` 和 `replaceText` 可替换默认文案,格式或限制说明通过 +`tipText` 展示。文件列表由各模式组件直接展示,多文件模式展示 +已选数量或上传完成汇总和两列 `el-card.small` 文件卡片;`download` 作用域插槽为文件卡片提供下载操作。 +多文件列表使用 Element Plus 的 `ready`、`uploading`、`success`、`fail` 状态展示进度和失败原因, +可重试失败文件、其他失败文件、上传中文件依次置顶。尚未上传的文件显示已选择,不计入完成数。 +多文件组件通过 `inject('upload')` 获取调用方提供的 `DragUploadHandler`: +`(file, onProgress) => { request: Promise, abort }`。选择文件后先发出 `change`,等待 +调用方列表回写并完成组件校验,再上传保留的 `ready` 文件;进度回调更新 `percentage`,请求成功后 +从响应地址提取 `file_id` 并标记 `success`,网络失败标记 `fail` 并允许重试。100% 表示传输完成,成功状态以请求响应为准。 +单个和全部重试仅对 `fail` 且 `canRetry` 的文件生效,有注入接口时直接重新上传;未注入时 +保持文件选择模式,重试逐文件发出 `retry(file)`。删除、清空和卸载时取消仍在进行的上传。 +使用方可通过 `change` 增加业务校验,通过 `remove` 清理业务数据。具体接口由页面 +`provide('upload', handler)` 提供,不写入公共组件。 +多文件组件移除 `success` 且具有 `file_id` 的文件时,直接调用 Admin `FileApi.deleteFile(file_id)` +删除服务端文件;`remove` 事件仍用于通知调用方清理业务数据。 +数量限制使用 `limit`,不通过展示插槽控制行为。 +组件暴露 `clearFiles()` 清空控件状态、`handleRemove(file)` 移除文件和响应式 `uploadingCount` 上传中数量。 +多文件组件的默认选择文案内提供“选择文件夹”,内部维护隐藏的目录选择 input,选择后逐项 +加入上传控件并触发相同的 `change` 流程;等待调用方校验回写后再计算数量限制,超限发出 +`exceed`。多文件通用校验由组件负责,具体请求由业务页面提供。 ### PythonCodeEditor diff --git a/ui/src/components/business/generate-questions/GenerateQuestionsDialog.vue b/ui/src/components/business/generate-questions/GenerateQuestionsDialog.vue index 9746b3fcbf6..56f0138e125 100644 --- a/ui/src/components/business/generate-questions/GenerateQuestionsDialog.vue +++ b/ui/src/components/business/generate-questions/GenerateQuestionsDialog.vue @@ -96,8 +96,9 @@ defineExpose({ open, close }) - + @@ -232,11 +244,11 @@ defineExpose({ validate, getStrategy, setStrategy })
视觉增强

识别文档中图片,将图片按照策略发给模型或工具处理,并将返回内容写入图片的描述中

- +
- + 模型增强 工具增强 @@ -252,11 +264,11 @@ defineExpose({ validate, getStrategy, setStrategy }) v-model="visualModelId" :options="modelOptions" :provider-options="providerOptions" - :disabled="optionsLoading" + :disabled="modelOptionsLoading" placeholder="请选择视觉模型" teleported can-add - @refresh="loadOptions" + @refresh="loadModelOptions" /> - +