fix: 完善知识库向量模型配置提示与刷新
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
# Task: 排查知识库向量模型目录与配置入口
|
||||
|
||||
## Identity
|
||||
|
||||
- Task ID: 20260912-kb-embedding-ml-6f8a21dc
|
||||
- Mode: Feature
|
||||
- Branch: codex/20260912-kb-embedding-ml-6f8a21dc-kb-embedding
|
||||
- Worktree: D:\Datas\OthersProjects\.codex-worktrees\makelore\20260912-kb-embedding-ml-6f8a21dc
|
||||
- Base commit: a0b869a333b12d28bfd96dcd89e8bc03ad47647d
|
||||
- Owner: codex
|
||||
- Status: Ready for Integration
|
||||
|
||||
## Scope
|
||||
|
||||
- 排查创建知识库的空向量模型下拉框,补齐云端配置说明、加载/失败/空状态及原地刷新,保留知识库名称和未确认创建请求。
|
||||
|
||||
## Intent And Constraints
|
||||
|
||||
- 已加载 maintain-project-docs 与 diagnosing-bugs;bundled check/start/status 验证独立 feature 任务、分支、绝对工作树及主线基线一致,Concurrent/Planning Gates Passed。已读入口、本人记录、项目记忆、已接受个人云智能体决策、架构与相关证据/反思/承诺;peer 作用域已核对,历史占位保持未知但无具体语义依赖。
|
||||
- 不创建子智能体,不读取/写入线上模型凭据、部署配置或用户数据,不调用真实收费模型,不修改主目录或 canonical memory。WS 管理可用模型和账本,Yuxi 管理向量模型定义/维度及知识执行,客户端只消费 Main Host API。
|
||||
- 截图只证明空下拉框,未读取用户线上目录响应及模型配置;不把本地替身输入当作已确认生产根因。
|
||||
|
||||
## Outcome
|
||||
|
||||
- CloudKnowledge 显式区分加载、失败、空目录;增加刷新知识库与模型按钮、平台管理员说明及可折叠的 Yuxi/WS/one-api 配置步骤。成功刷新清除目录错误,保留输入名称;已下线模型不能用于新创建,未知创建结果仍复用原 operation_id/name/model。
|
||||
- README 同步配置入口;新增五个组件行为回归,扩展既有真实 Electron fixture 覆盖空状态、管理员说明、刷新恢复和名称保留。
|
||||
|
||||
## Verification
|
||||
|
||||
- pnpm 10.33.4 install --frozen-lockfile --offline 通过,未修改 lockfile。
|
||||
- pnpm exec vitest run tests/unit/cloud-knowledge-catalog.test.tsx --maxWorkers=1:修复前四项因缺少说明/加载状态/刷新入口失败,修复后通过;追加未知创建重试案例后,连同四个既有云模块测试文件共 50 tests passed。
|
||||
- pnpm run typecheck 与 scoped ESLint(CloudKnowledge、新单测、cloud-agents.spec)通过,无诊断。
|
||||
- pnpm run build:vite 四个目标成功,保留既有 chunk/混合导入/Browserslist 提示。
|
||||
- pnpm exec playwright test tests/e2e/cloud-agents.spec.ts --workers=1 --reporter=list:真实 Windows Electron 1 passed;回读 knowledge-empty.png,配置区独立滚动、提示与操作可见。Host API 为 fixture,无线上模型调用。
|
||||
- git diff --check 与 task-aware 文档检查通过。
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- 需要平台管理员在现有云端配置入口核对并接入模型;当前安装版配置完成后离开智能体再进入以重新加载目录。新增原地刷新与说明需后续合并/打包才能进入安装版。
|
||||
- 本次无主分支合并、推送、部署或安装包发布。线上具体配置缺口及真实向量收费调用仍未验证。
|
||||
|
||||
## Promotion Candidates
|
||||
|
||||
- Target: 集成 current-state/云智能体客户端记录。Proposal: 记录知识模型配置由平台管理员在 Yuxi/WS 完成,客户端提供明确目录状态、刷新及恢复语义。Evidence: 当前 Owner 代码、五项组件回归和 Electron 验证。Impact: 后续配置体验沿用现有目录与凭据边界。Conflict: 无产品方向冲突。Human confirmation: 无新产品决定,canonical 提升由后续 Integration Gate 执行。
|
||||
@@ -6,6 +6,9 @@ Makelore 是一个面向软件、视觉创作、智能机器人与个人云智
|
||||
|
||||
- `Makelore Code|AI 编程`:管理本地项目、项目智能体、对话、文件上下文、代码变更和运行时。
|
||||
- `Makelore Agents|AI 智能体`:配置个人云智能体并试聊,发布后正式对话、按账号分享或创建应用 API Key。支持流式回复、审批、排队请求、附件与产物、自动任务和活动历史;重新进入时恢复最近智能体与对话,未确认操作可按原输入重试。聊天可提出日程建议,由创建者核对能力、时间和费用后启用。知识库支持上传、索引、替换、删除及导入本人会话附件;个人 MCP 凭据、Skills、子智能体可自助管理。支持智能体和会话归档、发布版本比较及恢复为草稿。所有模型调用由创建者个人词元点数支付,可按智能体设置每次任务和每日上限,费用按时间、来源、应用分页统计;分享/API 调用者保有自己的内容空间。Main 管理云会话和本机文件,Renderer 通过 Host API 操作。配套服务接入见 [Yuxi MakeLore 说明](https://xerrors.github.io/Yuxi/advanced/makelore-agents.html)。 编辑采用全高桌面分栏,支持拖动调整与展开、独立滚动、小窗口配置/试用切换。配置分为指令、能力、知识、限制;草稿明确保存(Ctrl/⌘+S),保存与切换页面保留当前试用及未发送文字,新试用明确使用最新保存版本。费用上限在限制中独立保存。Enter 发送、Shift+Enter 换行,F6 切换面板焦点、Esc 恢复分栏。
|
||||
|
||||
云知识库的向量模型由平台管理员配置:Yuxi 后台「智能体管理 → 模型供应商」启用 embedding 模型并填写实际维度;Works Square 运营后台「模型管理」从 one-api 刷新并激活相同模型 ID。one-api 必须支持该模型的向量接口。客户端区分目录加载中、加载失败与无可用模型,空状态提供管理员配置说明;「刷新知识库与模型」保留未提交名称,未确认的创建请求沿用原操作身份。
|
||||
|
||||
- `Makelore Canvas|AI 绘画`:每个设计项目(Workspace)维护一份从创建起就存在的 Living Form。左侧项目栏负责新建、切换和管理 Workspace,并在桌面设计模式下以 256px 宽度常驻展开;中央沿用 AI 编程的安静对话画布、自然消息流和底部悬浮输入器,AI 整理出的制作方案作为对话内的轻量可编辑稿持续更新;桌面端右侧同为 256px 的全高历史作品栏集中展示当前项目的制作记录与生成结果。紧凑窗口通过左侧抽屉访问项目列表,历史记录保留在时间线中。参考图从本地上传后以 `@图片N` 绑定,具体用法只写在创作提示词中。
|
||||
- `Makelore Robot|AI 机器`:管理机器人智能体、设备激活绑定、智能体配置与设备分配;机器人工作台的智能体位于 Robot 全局侧栏,选中后在内容区先查看绑定设备、再查看基础设置,当前智能体通过 URL 参数保持可分享选择;绑定设备时默认先选择“引导配网”或“已有激活码”。在 Windows 与 macOS 的引导路径中,Makelore 可在弹窗内扫描并连接附近开放的 `Xiaozhi-*` 配网热点,失败时仍可通过系统 Wi-Fi 手动连接;后续继续复用机器人现有热点配网页面,不修改固件,也不由 Makelore 接收 Wi-Fi 密码。
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ export function CloudKnowledgePanel({ slug, onCreated, onDeleted }: { slug: stri
|
||||
const [name, setName] = useState('');
|
||||
const [model, setModel] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [catalogError, setCatalogError] = useState('');
|
||||
const [catalogLoading, setCatalogLoading] = useState(true);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [refresh, setRefresh] = useState(0);
|
||||
const [creating, setCreating] = useState(false);
|
||||
@@ -32,8 +34,14 @@ export function CloudKnowledgePanel({ slug, onCreated, onDeleted }: { slug: stri
|
||||
useEffect(() => { alive.current = true; return () => { alive.current = false; }; }, []);
|
||||
useEffect(() => {
|
||||
let current = true;
|
||||
cloudAgentsApi.call('knowledge', { slug }).then(v => { if (current) setCatalog(v); })
|
||||
.catch(e => { if (current) setError(String(e.message || e)); });
|
||||
setCatalogLoading(true); setCatalogError('');
|
||||
cloudAgentsApi.call('knowledge', { slug }).then(v => {
|
||||
if (!current) return;
|
||||
setCatalog(v);
|
||||
if (!createIntent.current) setModel(previous => v.models.some(item => item.id === previous) ? previous : '');
|
||||
})
|
||||
.catch(e => { if (current) setCatalogError(String(e.message || e)); })
|
||||
.finally(() => { if (current) setCatalogLoading(false); });
|
||||
return () => { current = false; };
|
||||
}, [slug, refresh]);
|
||||
useEffect(() => {
|
||||
@@ -58,21 +66,35 @@ export function CloudKnowledgePanel({ slug, onCreated, onDeleted }: { slug: stri
|
||||
<div className="mt-4 space-y-4 text-sm">
|
||||
<p className="text-xs leading-5 text-muted-foreground">上传到知识库的文档可供获准使用此智能体的人检索。解析后建立索引才会生效,向量调用扣除你的词元点数。</p>
|
||||
{error && <p role="alert" className="text-destructive">{error}</p>}
|
||||
<div className="flex gap-2">
|
||||
<select aria-label="管理的知识库" className="h-10 min-w-0 flex-1 rounded-md border bg-background px-3" value={kbId} disabled={busy}
|
||||
{catalogError && <p role="alert" className="text-destructive">知识库与模型目录加载失败:{catalogError}。请刷新重试。</p>}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<select aria-label="管理的知识库" className="h-10 min-w-0 flex-1 basis-48 rounded-md border bg-background px-3" value={kbId} disabled={busy}
|
||||
onChange={e => { setKbId(e.target.value); uploadIntent.current = null; setNext(null); }}>
|
||||
<option value="">选择我的知识库</option>
|
||||
{catalog?.databases.map(kb => <option key={kb.kb_id} value={kb.kb_id}>{kb.name}</option>)}
|
||||
</select>
|
||||
<Button type="button" variant="outline" disabled={busy} onClick={() => setCreating(v => !v)}>新建知识库</Button>
|
||||
<Button type="button" variant="ghost" disabled={busy || catalogLoading} onClick={() => setRefresh(v => v + 1)}>刷新知识库与模型</Button>
|
||||
</div>
|
||||
{creating && <div className="space-y-3 rounded-md bg-muted/30 p-3">
|
||||
<Input aria-label="知识库名称" placeholder="知识库名称" value={name} disabled={busy || !!createIntent.current} onChange={e => setName(e.target.value)} />
|
||||
<select aria-label="向量模型" className="h-10 w-full rounded-md border bg-background px-3" value={model} disabled={busy || !!createIntent.current} onChange={e => setModel(e.target.value)}>
|
||||
<option value="">选择向量模型</option>{catalog?.models.map(m => <option key={m.id} value={m.id}>{m.name}</option>)}
|
||||
<select aria-label="向量模型" className="h-10 w-full rounded-md border bg-background px-3" value={model}
|
||||
disabled={busy || !!createIntent.current || catalogLoading || !!catalogError || !catalog?.models.length} onChange={e => setModel(e.target.value)}>
|
||||
<option value="">{catalogLoading ? '正在加载向量模型…' : catalogError ? '向量模型加载失败' : !catalog?.models.length ? '暂无可用向量模型' : '选择向量模型'}</option>
|
||||
{catalog?.models.map(m => <option key={m.id} value={m.id}>{m.name}</option>)}
|
||||
</select>
|
||||
{catalog && !catalog.models.length && <p className="text-xs text-muted-foreground">服务端尚未接入可计费的向量模型。</p>}
|
||||
<Button type="button" disabled={busy || !name.trim() || !model} onClick={() => void act(async () => {
|
||||
{!catalogLoading && !catalogError && catalog && !catalog.models.length && <div className="space-y-2 text-xs leading-5 text-muted-foreground">
|
||||
<p>暂时没有可用的向量模型。请联系平台管理员完成云端配置,然后刷新。</p>
|
||||
<details>
|
||||
<summary className="cursor-pointer font-medium text-foreground">管理员配置说明</summary>
|
||||
<ol className="mt-2 list-decimal space-y-1 pl-5">
|
||||
<li>Yuxi 后台「智能体管理 → 模型供应商」:启用供应商,添加并启用 embedding 模型,填写模型实际向量维度。</li>
|
||||
<li>Works Square 运营后台「模型管理」:从 one-api 刷新,激活同一模型并保存。</li>
|
||||
<li>两端模型 ID 必须完全一致,one-api 需支持该模型的向量接口。配置完成后点击「刷新知识库与模型」。</li>
|
||||
</ol>
|
||||
</details>
|
||||
</div>}
|
||||
<Button type="button" disabled={busy || !name.trim() || (!createIntent.current && (catalogLoading || !!catalogError || !catalog?.models.some(item => item.id === model)))} onClick={() => void act(async () => {
|
||||
createIntent.current ??= { operation_id: crypto.randomUUID(), name: name.trim(), embedding_model: model };
|
||||
const kb = await cloudAgentsApi.call('createKnowledge', { slug, ...createIntent.current });
|
||||
createIntent.current = null;
|
||||
|
||||
@@ -12,6 +12,7 @@ test('personal cloud Agent creation, draft save and leave protection in Electron
|
||||
BrowserWindow.getAllWindows()[0].setSize(1366, 768);
|
||||
let draft: Record<string, unknown> | null = null;
|
||||
let recent: unknown = null;
|
||||
let knowledgeCatalogLoads = 0;
|
||||
let limits: { request_limit_points: string | null; daily_limit_points: string | null } = { request_limit_points: null, daily_limit_points: null };
|
||||
const jobs: Record<string, unknown>[] = [];
|
||||
const configuration = { model: '', tools: [], knowledges: [], mcps: [], skills: [], preload_skills: [], subagents: [], tool_approval_mode: 'default', max_execution_steps: 40, max_output_tokens: 4096, max_run_seconds: 600 };
|
||||
@@ -37,7 +38,7 @@ test('personal cloud Agent creation, draft save and leave protection in Electron
|
||||
if (path.endsWith('/actions')) {
|
||||
const input = body.input ?? {};
|
||||
if (body.operation === 'catalog') return result({ models: [{ id: 'test-model', name: '创作模型' }], resources: {}, pricing: null });
|
||||
if (body.operation === 'knowledge') return result({ databases: [], models: [] });
|
||||
if (body.operation === 'knowledge') return result({ databases: [], models: ++knowledgeCatalogLoads === 1 ? [] : [{ id: 'fixture:test-embedding', name: '资料向量模型' }] });
|
||||
if (body.operation === 'resources') return result({ mcps: [], skills: [], subagents: [] });
|
||||
if (body.operation === 'budget' || body.operation === 'saveBudget') {
|
||||
if (body.operation === 'saveBudget') limits = input;
|
||||
@@ -90,6 +91,22 @@ test('personal cloud Agent creation, draft save and leave protection in Electron
|
||||
await page.getByRole('button', { name: '保存草稿', exact: true }).click();
|
||||
await expect(page.getByRole('button', { name: '已保存', exact: true })).toBeVisible();
|
||||
await expect(page.getByTestId('titlebar-page-title')).toHaveText('智能体工作区');
|
||||
await page.getByRole('button', { name: '知识', exact: true }).click();
|
||||
await page.getByText('管理我的知识文档', { exact: true }).click();
|
||||
await page.getByRole('button', { name: '新建知识库', exact: true }).click();
|
||||
await expect(page.getByLabel('向量模型', { exact: true })).toBeDisabled();
|
||||
await page.getByText('管理员配置说明', { exact: true }).click();
|
||||
await expect(page.getByText(/Yuxi 后台/)).toContainText('智能体管理 → 模型供应商');
|
||||
await expect(page.getByText(/Works Square 运营后台/)).toBeVisible();
|
||||
await page.getByLabel('知识库名称', { exact: true }).fill('我的创作资料');
|
||||
await page.screenshot({ path: testInfo.outputPath('knowledge-empty.png') });
|
||||
await page.getByRole('button', { name: '刷新知识库与模型', exact: true }).click();
|
||||
await page.getByLabel('向量模型', { exact: true }).selectOption('fixture:test-embedding');
|
||||
await expect(page.getByLabel('知识库名称', { exact: true })).toHaveValue('我的创作资料');
|
||||
await expect(page.getByRole('button', { name: '创建并选用', exact: true })).toBeEnabled();
|
||||
await expect(page.getByText('管理员配置说明', { exact: true })).toHaveCount(0);
|
||||
await page.getByLabel('知识库名称', { exact: true }).fill('');
|
||||
await page.getByRole('button', { name: '新建知识库', exact: true }).click();
|
||||
await page.getByRole('button', { name: '指令', exact: true }).click();
|
||||
const preview = page.getByRole('region', { name: '草稿预览', exact: true });
|
||||
await preview.getByLabel('消息').fill('先给我一些创作建议');
|
||||
|
||||
86
tests/unit/cloud-knowledge-catalog.test.tsx
Normal file
86
tests/unit/cloud-knowledge-catalog.test.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
||||
import { CloudKnowledgePanel } from '@/pages/CloudAgents/CloudKnowledge';
|
||||
|
||||
const api = vi.hoisted(() => ({ call: vi.fn() }));
|
||||
vi.mock('@/lib/cloud-agents-api', () => ({ cloudAgentsApi: api }));
|
||||
const slug = 'ml-' + 'a'.repeat(32);
|
||||
const empty = { databases: [], models: [] };
|
||||
const available = { ...empty, models: [{ id: 'relay:fixture-embedding', name: '资料向量模型' }] };
|
||||
|
||||
beforeEach(() => vi.resetAllMocks());
|
||||
afterEach(cleanup);
|
||||
|
||||
function openCreation() {
|
||||
render(<CloudKnowledgePanel slug={slug} onCreated={() => undefined} />);
|
||||
fireEvent.click(screen.getByText('管理我的知识文档'));
|
||||
fireEvent.click(screen.getByRole('button', { name: '新建知识库' }));
|
||||
}
|
||||
|
||||
it('explains who configures an empty embedding catalog and refreshes without losing the name', async () => {
|
||||
api.call.mockResolvedValueOnce(empty).mockResolvedValueOnce(available);
|
||||
openCreation();
|
||||
expect(await screen.findByText('暂时没有可用的向量模型。请联系平台管理员完成云端配置,然后刷新。')).toBeVisible();
|
||||
expect(screen.getByRole('combobox', { name: '向量模型' })).toBeDisabled();
|
||||
fireEvent.click(screen.getByText('管理员配置说明'));
|
||||
expect(screen.getByText(/Yuxi 后台/)).toHaveTextContent('智能体管理 → 模型供应商');
|
||||
expect(screen.getByText(/Works Square 运营后台/)).toHaveTextContent('模型管理');
|
||||
fireEvent.change(screen.getByRole('textbox', { name: '知识库名称' }), { target: { value: '创作资料' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '刷新知识库与模型' }));
|
||||
expect(await screen.findByRole('option', { name: '资料向量模型' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox', { name: '知识库名称' })).toHaveValue('创作资料');
|
||||
expect(screen.queryByText('管理员配置说明')).not.toBeInTheDocument();
|
||||
fireEvent.change(screen.getByRole('combobox', { name: '向量模型' }), { target: { value: 'relay:fixture-embedding' } });
|
||||
expect(screen.getByRole('button', { name: '创建并选用' })).toBeEnabled();
|
||||
});
|
||||
|
||||
it('shows loading instead of suggesting configuration is missing before the catalog arrives', () => {
|
||||
api.call.mockReturnValue(new Promise(() => undefined));
|
||||
openCreation();
|
||||
expect(screen.getByRole('combobox', { name: '向量模型' })).toBeDisabled();
|
||||
expect(screen.getByRole('option', { name: '正在加载向量模型…' })).toBeInTheDocument();
|
||||
expect(screen.queryByText('管理员配置说明')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('lets a failed catalog load retry in place and clears its error after success', async () => {
|
||||
api.call.mockRejectedValueOnce(new Error('目录连接失败')).mockResolvedValueOnce(available);
|
||||
openCreation();
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent('目录连接失败');
|
||||
expect(screen.getByRole('combobox', { name: '向量模型' })).toBeDisabled();
|
||||
fireEvent.click(screen.getByRole('button', { name: '刷新知识库与模型' }));
|
||||
expect(await screen.findByRole('option', { name: '资料向量模型' })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('clears a removed selection after refresh so a new create cannot use an unavailable model', async () => {
|
||||
api.call.mockResolvedValueOnce(available).mockResolvedValueOnce(empty);
|
||||
openCreation();
|
||||
await screen.findByRole('option', { name: '资料向量模型' });
|
||||
fireEvent.change(screen.getByRole('textbox', { name: '知识库名称' }), { target: { value: '创作资料' } });
|
||||
fireEvent.change(screen.getByRole('combobox', { name: '向量模型' }), { target: { value: 'relay:fixture-embedding' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '刷新知识库与模型' }));
|
||||
await waitFor(() => expect(screen.getByRole('combobox', { name: '向量模型' })).toHaveValue(''));
|
||||
expect(screen.getByRole('button', { name: '创建并选用' })).toBeDisabled();
|
||||
expect(api.call.mock.calls.every(([operation]) => operation === 'knowledge')).toBe(true);
|
||||
});
|
||||
|
||||
it('preserves an uncertain creation intent when its model disappears on refresh', async () => {
|
||||
let loads = 0;
|
||||
api.call.mockImplementation(async operation => {
|
||||
if (operation === 'knowledge') return ++loads === 1 ? available : empty;
|
||||
throw new Error('创建结果未确认');
|
||||
});
|
||||
openCreation();
|
||||
await screen.findByRole('option', { name: '资料向量模型' });
|
||||
fireEvent.change(screen.getByRole('textbox', { name: '知识库名称' }), { target: { value: '创作资料' } });
|
||||
fireEvent.change(screen.getByRole('combobox', { name: '向量模型' }), { target: { value: 'relay:fixture-embedding' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '创建并选用' }));
|
||||
await screen.findByRole('alert');
|
||||
fireEvent.click(screen.getByRole('button', { name: '刷新知识库与模型' }));
|
||||
await screen.findByText('管理员配置说明');
|
||||
fireEvent.click(screen.getByRole('button', { name: '重试创建' }));
|
||||
await waitFor(() => expect(api.call.mock.calls.filter(([operation]) => operation === 'createKnowledge')).toHaveLength(2));
|
||||
const attempts = api.call.mock.calls.filter(([operation]) => operation === 'createKnowledge');
|
||||
expect(attempts[0][1]).toEqual(attempts[1][1]);
|
||||
expect(attempts[1][1]).toMatchObject({ slug, name: '创作资料', embedding_model: 'relay:fixture-embedding' });
|
||||
});
|
||||
Reference in New Issue
Block a user