diff --git a/README.md b/README.md index d0ec98b6..903ac38d 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Makelore 是一个面向软件、视觉创作、智能机器人与个人云智 历史待确认记录收起在智能体列表标题区的「创建记录/待处理记录」入口,只有点击后才打开记录窗口;配置、聊天和创建页不再置顶这些旧提醒。记录保留智能体名称和原请求身份;应用设置的待确认记录显示“网络可能不稳定,请再点一次「应用」。”,点击「应用」继续原来的提交,成功后显示“设置已应用。”。其他记录仍可明确「再试一次」或「不再提醒」,不会自动重试或清除。当前创建失败继续在创建表单内提示;关闭记录窗口保留已收到的结果和密钥,切换配置、聊天或创建页后可返回列表继续查看。密钥须明确关闭结果后,才可处理其他记录。 - 高级配置的知识库支持下拉多选,分为「MakeLore知识」和「我的知识库」,已选项可单独移除。MakeLore知识来自当前账号有权读取的 Yuxi 共享目录,只显示名称与简介;移除仅取消当前智能体选用,不删除原库。知识库区域上方左右分区,两张卡片在并排时保持等高:左侧「上传自己的文件」仅放「新建知识库」入口及展开的创建表单,右侧「查看我的知识库」通过下拉框查看本人知识库、从电脑追加资料和管理文件;下方独立「选择知识库」,选择智能体使用的「MakeLore知识」和「我的知识库」。小窗口按新建、查看、选用顺序纵向排列。创建成功后在右侧打开新库;新建和查看均不会自动加入智能体配置,选用上限不影响管理与新建。仅提供「从电脑上传」作为添加文件入口,未选择管理目标时上传按钮置灰;聊天文件导入入口已移除。上传后自动解析、建立索引并持续显示真实处理状态;失败可重试且保留已有可用内容。暂不可选用的个人库仍可管理文件,已有绑定保留并提示不可用;未确认请求在当前库保留同一操作身份供重试。知识库选择只改草稿,显式保存/应用后生效;个人云端执行已自动接入所选知识库的检索,无需另外勾选 knowledge-base Skill。此交互复用现有接口,无需新增后端补丁。 + 高级配置的知识库支持下拉多选,分为「MakeLore知识」和「我的知识库」,已选项可单独移除。MakeLore知识来自当前账号有权读取的 Yuxi 共享目录,只显示名称与简介;移除仅取消当前智能体选用,不删除原库。知识库区域上方左右分区,两张卡片在并排时保持等高:左侧「上传自己的文件」仅放「新建知识库」入口及展开的创建表单,右侧「查看我的知识库」通过下拉框查看本人知识库、从电脑追加资料和管理文件。宽窗口中新建按钮与右侧选择、刷新、上传控件共用居中的水平线,管理下拉框约为原宽度的一半,两种目录刷新均显示为带边框按钮;下方独立「选择知识库」,选择智能体使用的「MakeLore知识」和「我的知识库」,下拉框缩至原宽度的约2/3,并与已选知识库框左右对齐。小窗口按新建、查看、选用顺序纵向排列,控件允许换行并保持可读。创建成功后在右侧打开新库;新建和查看均不会自动加入智能体配置,选用上限不影响管理与新建。仅提供「从电脑上传」作为添加文件入口,未选择管理目标时上传按钮置灰;聊天文件导入入口已移除。上传后自动解析、建立索引并持续显示真实处理状态;失败可重试且保留已有可用内容。暂不可选用的个人库仍可管理文件,已有绑定保留并提示不可用;未确认请求在当前库保留同一操作身份供重试。知识库选择只改草稿,显式保存/应用后生效;个人云端执行已自动接入所选知识库的检索,无需另外勾选 knowledge-base Skill。此交互复用现有接口,无需新增后端补丁。 云知识库的 embedding 模型只由平台管理员在 Yuxi 后台维护:在「智能体管理 → 模型供应商」启用供应商,按供应商要求填写有效的服务地址和 API Key,添加并启用 embedding 模型并填写实际向量维度;embedding 向量化费用由平台承担,不计入用户用量、词元点数或智能体费用上限;one-api 不参与 embedding 配置,Works Square 只负责其他应计费模型调用的创建者词元点数账本。客户端区分目录加载中、加载失败与无可用模型,空状态提供管理员配置说明;「刷新知识库与模型」保留未提交名称,未确认的创建请求沿用原操作身份。 diff --git a/src/pages/CloudAgents/CloudKnowledge.tsx b/src/pages/CloudAgents/CloudKnowledge.tsx index 945cde7e..c07d0b5e 100644 --- a/src/pages/CloudAgents/CloudKnowledge.tsx +++ b/src/pages/CloudAgents/CloudKnowledge.tsx @@ -28,6 +28,7 @@ export function CloudKnowledgePanel({ slug, onCreated, onDeleted, onCatalog, ref const [refresh, setRefresh] = useState(0); const [creating, setCreating] = useState(false); const [documentPending, setDocumentPending] = useState(false); + const [uploadTarget, setUploadTarget] = useState(null); const createIntent = useRef<{ operation_id: string; name: string; embedding_model: string } | null>(null); usePendingCloudInput(busy || documentPending || Boolean(createIntent.current) || (creating && Boolean(name.trim()))); const operationInFlight = useRef(false); @@ -63,8 +64,7 @@ export function CloudKnowledgePanel({ slug, onCreated, onDeleted, onCatalog, ref const selectedKnowledge = manageable.find(kb => kb.kb_id === kbId); const managementDisabled = disabled || busy || catalogLoading || Boolean(catalogError); const bindable = (id: string) => selectableIds === undefined || Boolean(selectableIds?.includes(id)); - const creationControls =
- + const creationDetails =
{creating &&
setName(e.target.value)} /> + - + +
+ {!selectedKnowledge && } +
+
+ {errorTarget === 'management' && error &&

{error}

} + {catalogError &&

知识库与模型目录加载失败:{catalogError}。请刷新重试。

} {documentPending && selectedKnowledge &&

有未确认的文件操作,请先在当前知识库重试或确认状态,再切换知识库。

} {!catalogLoading && !catalogError && !manageable.length &&

还没有可管理的知识库,可以新建一个保存自己的文件。MakeLore知识仅供检索。

} {selectedKnowledge && selectableIds !== undefined && !bindable(selectedKnowledge.kb_id) &&

@@ -131,12 +138,11 @@ export function CloudKnowledgePanel({ slug, onCreated, onDeleted, onCatalog, ref {documentPending &&

放弃只清除本地重试请求,不会删除已保存的文件。重新进入后可确认服务器状态。

}
} - {!selectedKnowledge &&
- -
} {kbId && } +
; } diff --git a/src/pages/CloudAgents/CloudKnowledgeDocuments.tsx b/src/pages/CloudAgents/CloudKnowledgeDocuments.tsx index 0cd84b6d..48d4ad95 100644 --- a/src/pages/CloudAgents/CloudKnowledgeDocuments.tsx +++ b/src/pages/CloudAgents/CloudKnowledgeDocuments.tsx @@ -1,4 +1,5 @@ import { useEffect, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; import { CheckCircle2, CircleAlert, FileText, Loader2 } from 'lucide-react'; import { cloudAgentsApi } from '@/lib/cloud-agents-api'; import { Button } from '@/components/ui/button'; @@ -47,11 +48,12 @@ function documentState(file: CloudKnowledgeFile) { return { label: '状态待确认', detail: '请刷新状态后再操作。', stage: 0 }; } -export function CloudKnowledgeDocuments({ slug, kbId, busy, act, onDeleteKnowledge, canRead = true, onPendingChange }: { +export function CloudKnowledgeDocuments({ slug, kbId, busy, act, onDeleteKnowledge, canRead = true, onPendingChange, uploadTarget }: { slug: string; kbId: string; busy: boolean; act: (operation: () => Promise) => Promise; onDeleteKnowledge: () => void; canRead?: boolean; onPendingChange?: (pending: boolean) => void; + uploadTarget?: HTMLElement | null; }) { const [files, setFiles] = useState([]); const [pageCount, setPageCount] = useState(1); @@ -150,9 +152,10 @@ export function CloudKnowledgeDocuments({ slug, kbId, busy, act, onDeleteKnowled }); const states = files.map(documentState); const active = states.some(state => state.active) || submitting !== null; + const uploadButton = ; return
- + {uploadTarget ? createPortal(uploadButton, uploadTarget) : uploadButton}

上传后自动解析并建立索引。最大 5 MB;支持 TXT、Markdown、CSV、JSON、Word、Excel、文本 PDF。

diff --git a/src/pages/CloudAgents/CloudKnowledgeSelector.tsx b/src/pages/CloudAgents/CloudKnowledgeSelector.tsx index 59764810..74502a71 100644 --- a/src/pages/CloudAgents/CloudKnowledgeSelector.tsx +++ b/src/pages/CloudAgents/CloudKnowledgeSelector.tsx @@ -24,21 +24,21 @@ export function CloudKnowledgeSelector({ slug, options, selected, disabled, load

从 MakeLore知识和我的知识库中选择,可同时使用多个。

-
- { if (!disabled && !loading && !error && event.target.value && available.some(option => option.key === event.target.value)) add(event.target.value); }}> {shared.length > 0 && {shared.map(option => )}} {personal.length > 0 && {personal.map(option => )}} {unknown.length > 0 && {unknown.map(option => )}} - +
{selectionFull &&

已选满100个知识库,请先移除一个再选择。

} {error ?

知识库目录读取失败,请刷新重试。

: !loading && ownedIds !== null && options.every(option => ownedIds.includes(option.key)) &&

暂无可选的MakeLore知识。平台开放给你读取的知识库会出现在这里。

} {owned.error &&

我的知识库目录暂不可用,仍可选用可读取的知识;上传文件请刷新重试。

} - {selected.length > 0 ?
    + {selected.length > 0 ?
      {selected.map(id => { const option = options.find(item => item.key === id); const ownKnowledge = owned.databases?.find(item => item.kb_id === id); diff --git a/src/pages/CloudAgents/agent-config.css b/src/pages/CloudAgents/agent-config.css index 430b1b65..41601706 100644 --- a/src/pages/CloudAgents/agent-config.css +++ b/src/pages/CloudAgents/agent-config.css @@ -43,16 +43,35 @@ .agent-config-resource-manager > details { border-radius: 14px; background: #ffffffa8; } .agent-resource-card:has(.agent-knowledge-workspace) { container-type: inline-size; } .agent-knowledge-workspace { display: grid; gap: 18px; min-width: 0; } -.agent-knowledge-owned-workspace { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr); align-items: stretch; gap: 18px; min-width: 0; } +.agent-knowledge-owned-workspace { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr); grid-template-rows: auto minmax(96px, auto) minmax(43px, auto); align-items: stretch; gap: 18px; min-width: 0; } .agent-knowledge-upload, .agent-knowledge-management, .agent-knowledge-selection { min-width: 0; padding: 18px; border: 1px solid #dce5e0; border-radius: 17px; background: #fffdfb; overflow-wrap: anywhere; } .agent-knowledge-upload { border-color: #dce5f3; } +.agent-knowledge-upload, .agent-knowledge-management { display: grid; grid-template-rows: subgrid; grid-row: span 3; } +.agent-knowledge-create-action { display: flex; align-items: center; justify-content: center; } +.agent-knowledge-management-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; min-width: 0; } +.agent-config-card .agent-knowledge-management-controls select { width: calc((100% - 144px - 8px) / 2); flex: none; padding-inline: 8px; font-size: 14px; } +.agent-knowledge-upload-action { display: flex; min-width: 0; } +.agent-knowledge-create-action button, .agent-knowledge-management-controls button, .agent-knowledge-selection-controls button { min-height: 48px; height: auto; border-radius: 13px; } +.agent-knowledge-management-controls button { flex-shrink: 0; } +.agent-knowledge-selection { --knowledge-choice-width: calc((100% - 102px) * 2 / 3); } +.agent-knowledge-selection-controls { display: flex; align-items: center; gap: 12px; min-width: 0; } +.agent-config-card .agent-knowledge-selection-controls select { width: var(--knowledge-choice-width); flex: none; } +.agent-knowledge-selected { width: var(--knowledge-choice-width); } .agent-knowledge-panel-heading { display: flex; align-items: center; gap: 9px; color: #33485c; } .agent-knowledge-panel-heading h3 { font-size: 16px; font-weight: 650; line-height: 1.5; } .agent-knowledge-panel-heading svg { flex-shrink: 0; color: #607d72; } .agent-knowledge-upload .agent-knowledge-panel-heading svg { color: #4a6e9c; } .agent-knowledge-selection ul { max-height: 480px; overflow: auto; overscroll-behavior: contain; } .agent-knowledge-selection button { flex-shrink: 0; } -@container (max-width: 620px) { .agent-knowledge-owned-workspace { grid-template-columns: minmax(0, 1fr); } } +@container (max-width: 840px) { + .agent-knowledge-owned-workspace { grid-template-columns: minmax(0, 1fr); grid-template-rows: none; } + .agent-knowledge-upload, .agent-knowledge-management { display: flex; flex-direction: column; gap: 16px; grid-row: auto; } + .agent-knowledge-create-action { min-height: 80px; } + .agent-knowledge-management-controls { flex-wrap: wrap; } + .agent-config-card .agent-knowledge-management-controls select { width: 100%; } + .agent-knowledge-selection { --knowledge-choice-width: 100%; } + .agent-knowledge-selection-controls { flex-wrap: wrap; } +} @media (max-width: 640px) { .agent-editor-tabs { padding: 10px 12px; } .agent-editor-tabs [role="tab"] { gap: 6px; padding-inline: 8px; font-size: 14px; } @@ -65,7 +84,7 @@ .agent-config-icon { width: 38px; height: 38px; border-radius: 13px; } .agent-config-section-top > button { margin-left: 48px; } .agent-knowledge-workspace, .agent-knowledge-owned-workspace { gap: 14px; } - .agent-knowledge-owned-workspace { grid-template-columns: minmax(0, 1fr); } + .agent-knowledge-owned-workspace { grid-template-columns: minmax(0, 1fr); grid-template-rows: none; } .agent-knowledge-upload, .agent-knowledge-management, .agent-knowledge-selection { padding: 14px; border-radius: 14px; } } @media (prefers-reduced-motion: reduce) { .agent-resource-option { transition: none; } } diff --git a/tests/e2e/cloud-agent-knowledge.spec.ts b/tests/e2e/cloud-agent-knowledge.spec.ts index 473b07bf..5368ca7c 100644 --- a/tests/e2e/cloud-agent-knowledge.spec.ts +++ b/tests/e2e/cloud-agent-knowledge.spec.ts @@ -203,12 +203,24 @@ async function expectKnowledgeLayout(page: Page) { const ownedBounds = await knowledge.locator('.agent-knowledge-owned-workspace').boundingBox(); expect(creationBounds).not.toBeNull(); expect(managementBounds).not.toBeNull(); expect(selectionBounds).not.toBeNull(); expect(ownedBounds).not.toBeNull(); - if (ownedBounds!.width > 620) { + if (ownedBounds!.width > 840) { expect(creationBounds!.x + creationBounds!.width).toBeLessThanOrEqual(managementBounds!.x + 1); expect(Math.abs(creationBounds!.y - managementBounds!.y)).toBeLessThanOrEqual(1); expect(Math.abs(creationBounds!.height - managementBounds!.height)).toBeLessThanOrEqual(1); expect(selectionBounds!.y).toBeGreaterThanOrEqual(Math.max(creationBounds!.y + creationBounds!.height, managementBounds!.y + managementBounds!.height)); expect(Math.abs(selectionBounds!.x + selectionBounds!.width - managementBounds!.x - managementBounds!.width)).toBeLessThanOrEqual(1); + const create = await creation.getByRole('button', { name: '新建知识库', exact: true }).boundingBox(); + const manager = await management.getByRole('combobox', { name: '管理的知识库', exact: true }).boundingBox(); + const refresh = await management.getByRole('button', { name: '刷新知识库与模型', exact: true }).boundingBox(); + const upload = await management.getByRole('button', { name: '从电脑上传', exact: true }).boundingBox(); + const controls = await management.locator('.agent-knowledge-management-controls').boundingBox(); + for (const control of [manager, refresh, upload]) { + expect(Math.abs(control!.y + control!.height / 2 - create!.y - create!.height / 2)).toBeLessThanOrEqual(1); + } + expect(Math.abs(manager!.width - (controls!.width - 144 - 8) / 2)).toBeLessThanOrEqual(2); + if (await creation.getByRole('textbox').count() === 0 && await management.getByRole('combobox', { name: '管理的知识库', exact: true }).inputValue() === '') { + expect(Math.abs(create!.y + create!.height / 2 - creationBounds!.y - creationBounds!.height / 2)).toBeLessThanOrEqual(2); + } } else { expect(creationBounds!.y + creationBounds!.height).toBeLessThanOrEqual(managementBounds!.y + 1); expect(managementBounds!.y + managementBounds!.height).toBeLessThanOrEqual(selectionBounds!.y + 1); @@ -217,6 +229,20 @@ async function expectKnowledgeLayout(page: Page) { expect(Math.abs(creationBounds!.width - selectionBounds!.width)).toBeLessThanOrEqual(1); } expect(Math.abs(creationBounds!.x - selectionBounds!.x)).toBeLessThanOrEqual(1); + const choice = await selection.getByRole('combobox', { name: '选择知识库', exact: true }).boundingBox(); + const choiceRow = await selection.locator('.agent-knowledge-selection-controls').boundingBox(); + if (ownedBounds!.width > 840) { + expect(Math.abs(choice!.width - (choiceRow!.width - 102) * 2 / 3)).toBeLessThanOrEqual(2); + } + const selectedList = selection.getByRole('list', { name: '已选知识库', exact: true }); + if (await selectedList.count()) { + const selectedBounds = await selectedList.boundingBox(); + expect(Math.abs(selectedBounds!.x - choice!.x)).toBeLessThanOrEqual(1); + expect(Math.abs(selectedBounds!.width - choice!.width)).toBeLessThanOrEqual(1); + } + for (const refresh of [management.getByRole('button', { name: '刷新知识库与模型', exact: true }), selection.getByRole('button', { name: '刷新知识库', exact: true })]) { + expect(await refresh.evaluate(element => getComputedStyle(element).borderTopWidth)).toBe('1px'); + } await expectNoClipping(page); } async function screenshotKnowledge(page: Page, name: string) { @@ -356,6 +382,16 @@ test('cancelled and failed uploads preserve shared choices and the personal draf // An uncertain upload response keeps its retry attached to the same library. await expect(knowledge.getByRole('combobox', { name: '管理的知识库', exact: true })).toBeDisabled(); await expect(knowledge.getByRole('button', { name: '新建知识库', exact: true })).toBeDisabled(); + await page.setViewportSize({ width: 1440, height: 850 }); + const managementCard = knowledge.getByRole('region', { name: '查看我的知识库', exact: true }); + const managementBounds = await managementCard.boundingBox(); + for (const control of await managementCard.locator('.agent-knowledge-management-controls').getByRole('button').all()) { + const bounds = await control.boundingBox(); + expect(bounds!.x).toBeGreaterThanOrEqual(managementBounds!.x); + expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(managementBounds!.x + managementBounds!.width); + } + await expectNoClipping(page); + await page.setViewportSize({ width: 375, height: 850 }); const failedUpload = (await calls(page)).filter(call => call.path.endsWith('/knowledge/pick')).at(-1)!; await page.evaluate(() => { (window as unknown as FixtureWindow).__knowledgeFixture.upload = 'cancel'; }); await knowledge.getByRole('button', { name: '重新选择同一文件并重试', exact: true }).click();