diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 439e7e404..0a8b3ce7c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "knowledage-base", - "version": "0.7.1", + "version": "0.7.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "knowledage-base", - "version": "0.7.1", + "version": "0.7.2", "dependencies": { "@microsoft/fetch-event-source": "^2.0.1", "@types/dompurify": "^3.2.0", diff --git a/frontend/src/assets/img/datasource-gitlab.png b/frontend/src/assets/img/datasource-gitlab.png new file mode 100644 index 000000000..3db779d4f Binary files /dev/null and b/frontend/src/assets/img/datasource-gitlab.png differ diff --git a/frontend/src/components/doc-content.vue b/frontend/src/components/doc-content.vue index dbf7963b4..3fcabdfdf 100644 --- a/frontend/src/components/doc-content.vue +++ b/frontend/src/components/doc-content.vue @@ -929,6 +929,7 @@ const channelLabelMap: Record = { wechat: 'knowledgeBase.channelWechat', wecom: 'knowledgeBase.channelWecom', feishu: 'knowledgeBase.channelFeishu', + gitlab: 'knowledgeBase.channelGitLab', // Drive (云盘) connectors get their own channel so Drive docs show // "飞书云盘" / "Lark 云盘", distinct from the wiki connector's "飞书". feishu_drive: 'knowledgeBase.channelFeishuDrive', diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index 9520b58bc..4a586d0b3 100755 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -466,6 +466,7 @@ export default { channelIm: 'IM Channel', channelNotion: 'Notion', channelYuque: 'Yuque', + channelGitLab: 'GitLab', channelIma: 'Tencent IMA', channelUpload: 'Upload', channelManual: 'Manual', @@ -5415,6 +5416,14 @@ export default { connectionFailed: 'Connection failed', isRequired: 'is required', credentialsLabel: 'credentials', + gitlab: { + baseUrl: 'GitLab URL', accessToken: 'Personal access token', projects: 'GitLab projects', + projectsHint: 'Enter a project ID (or URL-encoded project path), with optional branch and directories.', + project: 'Project', projectId: 'Project ID', projectIdPlaceholder: 'For example: 12345 or group%2Fproject', + ref: 'Branch', refPlaceholder: 'Leave empty to use the default branch', + paths: 'Directories', pathsPlaceholder: 'One directory per line; leave empty to sync the whole project', + addProject: 'Add project', projectRequired: 'Add at least one GitLab project', + }, resourceHint: 'Select the spaces or folders to sync', untitled: 'Untitled', resourceLoadFailed: 'Failed to load resources', @@ -5482,8 +5491,9 @@ export default { lark_drive: 'Lark Drive', notion: 'Notion', yuque: 'Yuque', + rss: 'RSS / Atom Feed', ima: 'Tencent IMA', - rss: 'RSS / Atom Feed' + gitlab: 'GitLab' }, connectorDesc: { feishu: 'Sync documents, spreadsheets and files from Feishu Wiki', @@ -5492,8 +5502,9 @@ export default { lark_drive: 'Sync documents, spreadsheets and files from a Lark Drive folder (Feishu international)', notion: 'Sync pages and databases from Notion', yuque: 'Sync documents from Yuque knowledge bases', + rss: 'Sync articles from RSS / Atom feeds', ima: 'Sync documents, notes and files from Tencent IMA knowledge bases (AI sessions and video parses are not supported)', - rss: 'Sync articles from RSS / Atom feeds' + gitlab: 'Sync files from GitLab projects' }, drive: { folderTokenLabel: 'Drive folder token', diff --git a/frontend/src/i18n/locales/ko-KR.ts b/frontend/src/i18n/locales/ko-KR.ts index ba01cff60..5ade38b47 100755 --- a/frontend/src/i18n/locales/ko-KR.ts +++ b/frontend/src/i18n/locales/ko-KR.ts @@ -583,6 +583,14 @@ export default { connectionFailed: '연결 실패', isRequired: '은(는) 필수입니다', credentialsLabel: '자격 증명', + gitlab: { + baseUrl: 'GitLab URL', accessToken: '개인 액세스 토큰', projects: 'GitLab 프로젝트', + projectsHint: '프로젝트 ID(또는 URL 인코딩된 프로젝트 경로)를 입력하고, 필요에 따라 브랜치와 디렉터리를 지정하세요.', + project: '프로젝트', projectId: '프로젝트 ID', projectIdPlaceholder: '예: 12345 또는 group%2Fproject', + ref: '브랜치', refPlaceholder: '비워 두면 기본 브랜치를 사용합니다', + paths: '디렉터리', pathsPlaceholder: '한 줄에 하나씩 입력하세요. 비워 두면 전체 프로젝트를 동기화합니다', + addProject: '프로젝트 추가', projectRequired: 'GitLab 프로젝트를 하나 이상 추가하세요', + }, resourceHint: '동기화할 공간/폴더를 선택하세요', untitled: '제목 없음', resourceLoadFailed: '리소스 목록 로드 실패', @@ -705,7 +713,8 @@ export default { notion: 'Notion에서 페이지 및 데이터베이스 동기화', yuque: '위큐 지식베이스에서 문서 동기화', ima: 'Tencent IMA 지식베이스에서 문서, 노트 및 파일 동기화 (AI 세션과 동영상 분석은 지원되지 않음)', - rss: 'RSS / Atom 피드에서 글 동기화' + rss: 'RSS / Atom 피드에서 글 동기화', + gitlab: 'GitLab 프로젝트의 파일 동기화' }, connector: { feishu: '페이슈 (Feishu)', @@ -715,7 +724,8 @@ export default { notion: 'Notion', yuque: '위큐 (Yuque)', ima: 'Tencent IMA', - rss: 'RSS / Atom 피드' + rss: 'RSS / Atom 피드', + gitlab: 'GitLab' }, logDetail: { startTime: '시작 시간', @@ -5606,6 +5616,7 @@ export default { channelIm: 'IM 채널', channelNotion: 'Notion', channelYuque: 'Yuque', + channelGitLab: 'GitLab', channelIma: 'Tencent IMA', channelUpload: '업로드', channelManual: '수동', diff --git a/frontend/src/i18n/locales/ru-RU.ts b/frontend/src/i18n/locales/ru-RU.ts index ff8e4450a..8255f2179 100755 --- a/frontend/src/i18n/locales/ru-RU.ts +++ b/frontend/src/i18n/locales/ru-RU.ts @@ -583,6 +583,14 @@ export default { connectionFailed: 'Подключение не удалось', isRequired: 'обязательно для заполнения', credentialsLabel: 'учётные данные', + gitlab: { + baseUrl: 'URL GitLab', accessToken: 'Персональный токен доступа', projects: 'Проекты GitLab', + projectsHint: 'Укажите ID проекта (или URL-кодированный путь к проекту), а также при необходимости ветку и каталоги.', + project: 'Проект', projectId: 'ID проекта', projectIdPlaceholder: 'Например: 12345 или group%2Fproject', + ref: 'Ветка', refPlaceholder: 'Оставьте пустым, чтобы использовать ветку по умолчанию', + paths: 'Каталоги', pathsPlaceholder: 'По одному каталогу в строке; оставьте пустым для синхронизации всего проекта', + addProject: 'Добавить проект', projectRequired: 'Добавьте хотя бы один проект GitLab', + }, resourceHint: 'Выберите пространства или папки для синхронизации', untitled: 'Без названия', resourceLoadFailed: 'Не удалось загрузить список ресурсов', @@ -705,7 +713,8 @@ export default { notion: 'Синхронизация страниц и баз данных из Notion', yuque: 'Синхронизация документов из баз знаний Yuque', ima: 'Синхронизация документов, заметок и файлов из баз знаний Tencent IMA (ИИ-сессии и разбор видео не поддерживаются)', - rss: 'Синхронизация статей из лент RSS / Atom' + rss: 'Синхронизация статей из лент RSS / Atom', + gitlab: 'Синхронизация файлов из проектов GitLab' }, connector: { feishu: 'Feishu (Фэйшу)', @@ -715,7 +724,8 @@ export default { notion: 'Notion', yuque: 'Yuque (Юйцюэ)', ima: 'Tencent IMA', - rss: 'RSS / Atom лента' + rss: 'RSS / Atom лента', + gitlab: 'GitLab' }, logDetail: { startTime: 'Время начала', @@ -5606,6 +5616,7 @@ export default { channelIm: 'IM канал', channelNotion: 'Notion', channelYuque: 'Yuque', + channelGitLab: 'GitLab', channelIma: 'Tencent IMA', channelUpload: 'Загрузка', channelManual: 'Вручную', diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index 9f5c200bc..d8fa881b2 100755 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -583,6 +583,16 @@ export default { connectionFailed: '连接失败', isRequired: '为必填项', credentialsLabel: '凭证', + gitlab: { + baseUrl: 'GitLab 地址', + accessToken: '个人访问令牌', + projects: 'GitLab 项目', + projectsHint: '填写项目 ID(或 URL 编码后的项目路径),可选指定分支和目录。', + project: '项目', projectId: '项目 ID', projectIdPlaceholder: '例如:12345 或 group%2Fproject', + ref: '分支', refPlaceholder: '留空时使用默认分支', + paths: '同步目录', pathsPlaceholder: '每行一个目录;留空同步整个项目', + addProject: '添加项目', projectRequired: '请至少添加一个 GitLab 项目', + }, resourceHint: '选择要同步的内容空间/文件夹', untitled: '无标题', resourceLoadFailed: '加载资源列表失败', @@ -705,7 +715,8 @@ export default { notion: '同步 Notion 中的页面和数据库', yuque: '同步语雀知识库中的文档', ima: '同步腾讯 IMA 知识库中的文档、笔记与文件(暂不支持 AI 会话与视频解析)', - rss: '同步 RSS / Atom 订阅源中的文章' + rss: '同步 RSS / Atom 订阅源中的文章', + gitlab: '同步 GitLab 项目中的文件' }, connector: { feishu: '飞书', @@ -715,7 +726,8 @@ export default { notion: 'Notion', yuque: '语雀', ima: '腾讯 IMA', - rss: 'RSS / Atom 订阅' + rss: 'RSS / Atom 订阅', + gitlab: 'GitLab' }, logDetail: { startTime: '开始时间', @@ -5606,6 +5618,7 @@ export default { channelIm: 'IM 渠道', channelNotion: 'Notion', channelYuque: '语雀', + channelGitLab: 'GitLab', channelIma: '腾讯 IMA', channelUpload: '上传', channelManual: '手动', diff --git a/frontend/src/views/chat/components/tool-results/DocumentInfo.vue b/frontend/src/views/chat/components/tool-results/DocumentInfo.vue index be30673a0..8fe01dec2 100644 --- a/frontend/src/views/chat/components/tool-results/DocumentInfo.vue +++ b/frontend/src/views/chat/components/tool-results/DocumentInfo.vue @@ -108,6 +108,7 @@ const channelLabelMap: Record = { wechat: 'knowledgeBase.channelWechat', wecom: 'knowledgeBase.channelWecom', feishu: 'knowledgeBase.channelFeishu', + gitlab: 'knowledgeBase.channelGitLab', dingtalk: 'knowledgeBase.channelDingtalk', slack: 'knowledgeBase.channelSlack', im: 'knowledgeBase.channelIm', diff --git a/frontend/src/views/knowledge/KnowledgeBase.vue b/frontend/src/views/knowledge/KnowledgeBase.vue index 8181faa58..1c1101329 100644 --- a/frontend/src/views/knowledge/KnowledgeBase.vue +++ b/frontend/src/views/knowledge/KnowledgeBase.vue @@ -599,6 +599,7 @@ const sourceOptions = computed(() => [ { label: t('knowledgeBase.channelFeishuDrive'), value: 'feishu_drive' }, { label: t('knowledgeBase.channelNotion'), value: 'notion' }, { label: t('knowledgeBase.channelYuque'), value: 'yuque' }, + { label: t('knowledgeBase.channelGitLab'), value: 'gitlab' }, { label: t('knowledgeBase.channelIma'), value: 'ima' }, { label: t('knowledgeBase.channelWechat'), value: 'wechat' }, { label: t('knowledgeBase.channelWecom'), value: 'wecom' }, diff --git a/frontend/src/views/knowledge/components/DocumentCardView.vue b/frontend/src/views/knowledge/components/DocumentCardView.vue index b3e5342ea..92e98de85 100644 --- a/frontend/src/views/knowledge/components/DocumentCardView.vue +++ b/frontend/src/views/knowledge/components/DocumentCardView.vue @@ -154,6 +154,7 @@ const channelLabelMap: Record = { wechat: 'knowledgeBase.channelWechat', wecom: 'knowledgeBase.channelWecom', feishu: 'knowledgeBase.channelFeishu', + gitlab: 'knowledgeBase.channelGitLab', dingtalk: 'knowledgeBase.channelDingtalk', slack: 'knowledgeBase.channelSlack', im: 'knowledgeBase.channelIm', diff --git a/frontend/src/views/knowledge/components/DocumentListView.vue b/frontend/src/views/knowledge/components/DocumentListView.vue index 92348c32c..2fb5fff81 100644 --- a/frontend/src/views/knowledge/components/DocumentListView.vue +++ b/frontend/src/views/knowledge/components/DocumentListView.vue @@ -114,6 +114,7 @@ const getSourceInfo = (item: KnowledgeItem): { icon: string; label: string } => if (ch === 'lark_drive') return { icon: 'cloud-download', label: t('knowledgeBase.channelLarkDrive') }; if (ch === 'notion') return { icon: 'cloud-download', label: t('knowledgeBase.channelNotion') }; if (ch === 'yuque') return { icon: 'cloud-download', label: t('knowledgeBase.channelYuque') }; + if (ch === 'gitlab') return { icon: 'cloud-download', label: t('knowledgeBase.channelGitLab') }; if (ch === 'ima') return { icon: 'cloud-download', label: t('knowledgeBase.channelIma') }; if (ch === 'wechat') return { icon: 'cloud-download', label: t('knowledgeBase.channelWechat') }; if (ch === 'wecom') return { icon: 'cloud-download', label: t('knowledgeBase.channelWecom') }; diff --git a/frontend/src/views/knowledge/settings/DataSourceEditorDialog.vue b/frontend/src/views/knowledge/settings/DataSourceEditorDialog.vue index 2346cde42..c3cf2c420 100644 --- a/frontend/src/views/knowledge/settings/DataSourceEditorDialog.vue +++ b/frontend/src/views/knowledge/settings/DataSourceEditorDialog.vue @@ -206,6 +206,21 @@ const driveFolderToken = ref('') const driveFolderTokenError = ref('') const driveRootLoaded = ref(false) const isDriveConnector = (type: string) => type === 'feishu_drive' || type === 'lark_drive' +const isGitLabConnector = (type: string) => type === 'gitlab' + +interface GitLabProjectInput { project_id: string; ref: string; pathsText: string } +const gitlabProjects = ref([]) +function syncGitLabProjectsToSettings() { + if (!isGitLabConnector(form.value.type)) return + form.value.config.settings.projects = gitlabProjects.value + .filter(project => project.project_id.trim()) + .map(project => ({ + project_id: project.project_id.trim(), ref: project.ref.trim(), + paths: project.pathsText.split(/[\n,]/).map(path => path.trim()).filter(Boolean), + })) +} +function addGitLabProject() { gitlabProjects.value.push({ project_id: '', ref: '', pathsText: '' }) } +function removeGitLabProject(index: number) { gitlabProjects.value.splice(index, 1); syncGitLabProjectsToSettings() } // extractDriveFolderToken accepts either a bare folder_token or a Drive folder // URL (https://xxx.feishu.cn/drive/folder/ or the Lark equivalent @@ -611,6 +626,13 @@ const connectorDefs = computed(() => [ { key: 'auth_headers', labelKey: 'datasource.field.authHeaders', placeholder: '', optional: true, hintKey: 'datasource.field.authHeadersHint', fieldType: 'custom_headers' }, ], }, + { + type: 'gitlab', available: true, docUrl: '', permissionDocUrl: '', permissionPageUrl: '', requiredPermissions: [], + fields: [ + { key: 'base_url', labelKey: 'datasource.gitlab.baseUrl', placeholder: 'https://gitlab.example.com' }, + { key: 'access_token', labelKey: 'datasource.gitlab.accessToken', placeholder: '', secret: true }, + ], + }, ]) @@ -645,6 +667,7 @@ watch(visible, async (v) => { driveFolderTokenError.value = '' driveRootLoaded.value = false rssAuthHeaders.value = [] + gitlabProjects.value = [] if (isEdit.value && props.dataSource) { // Reset edit/replace toggle every open so an aborted replace doesn't @@ -671,6 +694,13 @@ watch(visible, async (v) => { sync_deletions: props.dataSource.sync_deletions, } selectedResourceIds.value = form.value.config?.resource_ids || [] + if (isGitLabConnector(form.value.type)) { + const savedProjects = Array.isArray(form.value.config.settings.projects) ? form.value.config.settings.projects : [] + gitlabProjects.value = savedProjects.map((project: any) => ({ + project_id: String(project.project_id || ''), ref: String(project.ref || ''), + pathsText: Array.isArray(project.paths) ? project.paths.join('\n') : '', + })) + } // Pre-fill the Drive root folder_token from the saved resource_ids so the // user sees what they previously entered. driveRootLoaded stays false: the // tree has not been listed yet, and clicking "load" triggers listResources @@ -737,6 +767,7 @@ function selectType(def: ConnectorDef) { form.value.type = def.type form.value.name = t(`datasource.connector.${def.type}`) form.value.config.credentials = {} + if (isGitLabConnector(def.type)) addGitLabProject() rssAuthHeaders.value = [] step.value = 1 } @@ -970,6 +1001,13 @@ async function nextStep() { } driveFolderTokenError.value = '' } + if (step.value === 2 && isGitLabConnector(form.value.type)) { + syncGitLabProjectsToSettings() + if (!gitlabProjects.value.some(project => project.project_id.trim())) { + MessagePlugin.warning(t('datasource.gitlab.projectRequired')) + return + } + } step.value++ if (step.value === 2) { // Drive connectors need a user-supplied folder_token before listing. @@ -982,6 +1020,7 @@ async function nextStep() { } return } + if (isGitLabConnector(form.value.type)) return loadResources() } } @@ -1000,6 +1039,7 @@ function prevStep() { // commitCredentialsIfNeeded). Sending an empty map keeps the backend // validator happy. function buildConfigPayload(): Record { + syncGitLabProjectsToSettings() return { credentials: isEdit.value ? {} : { ...form.value.config.credentials }, resource_ids: form.value.config.resource_ids, @@ -1183,7 +1223,7 @@ const drawerConfirmText = computed(() => { v-model:visible="visible" :title="drawerTitle" :description="drawerDescription" - :class="[form.type ? `datasource-editor-drawer datasource-editor-drawer--${form.type}` : 'datasource-editor-drawer', { 'ds-fixed-step': step === 2 }]" + :class="[form.type ? `datasource-editor-drawer datasource-editor-drawer--${form.type}` : 'datasource-editor-drawer', { 'ds-fixed-step': step === 2 && !isGitLabConnector(form.type) }]" :hide-footer="step === 0" :confirm-text="drawerConfirmText" :confirm-loading="submitting || (step === 1 && testing)" @@ -1519,6 +1559,26 @@ const drawerConfirmText = computed(() => {
+ +
@@ -2605,6 +2666,27 @@ const drawerConfirmText = computed(() => { color: var(--td-text-color-primary); box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05); } + +.gitlab-project-list { + display: grid; + gap: 12px; + margin-bottom: 20px; +} + +.gitlab-project-row { + display: grid; + gap: 8px; + padding: 12px; + border: 1px solid var(--td-component-stroke); + border-radius: 6px; + background: var(--td-bg-color-container); +} + +.gitlab-project-row__header { + display: flex; + align-items: center; + justify-content: space-between; +}