From 46fbae9dec70ee1bbfae671d458a81fd1d8a1c28 Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Thu, 10 Sep 2026 16:00:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(agents):=20=E6=96=B0=E5=A2=9E=E4=BA=91?= =?UTF-8?q?=E7=AB=AF=E6=99=BA=E8=83=BD=E4=BD=93=E5=85=A5=E5=8F=A3=E4=B8=8E?= =?UTF-8?q?=E8=8D=89=E7=A8=BF=E7=BC=96=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../20260910-personal-agent-entry-e69a9434.md | 52 ++++ README.md | 3 +- electron/api/route-handlers.ts | 2 + electron/api/routes/cloud-agents.ts | 34 +++ electron/services/cloud-agents.ts | 199 ++++++++++++++ shared/cloud-agents.ts | 28 ++ shared/module-access.ts | 3 + src/App.tsx | 2 + src/components/layout/Sidebar.tsx | 8 +- src/lib/ai-modules.ts | 16 +- src/lib/cloud-agents-api.ts | 16 ++ src/main.tsx | 8 +- src/pages/CloudAgents/index.tsx | 247 ++++++++++++++++++ src/pages/ModuleSelection/index.tsx | 9 +- tests/e2e/cloud-agents.spec.ts | 65 +++++ tests/e2e/main-navigation.spec.ts | 8 +- tests/unit/auth-routes.test.ts | 3 + tests/unit/auth-store.test.ts | 11 + tests/unit/cloud-agents-main.test.ts | 89 +++++++ tests/unit/cloud-agents-page.test.tsx | 122 +++++++++ tests/unit/module-access.test.ts | 13 +- 21 files changed, 921 insertions(+), 17 deletions(-) create mode 100644 .project-docs/30-worklog/tasks/20260910-personal-agent-entry-e69a9434.md create mode 100644 electron/api/routes/cloud-agents.ts create mode 100644 electron/services/cloud-agents.ts create mode 100644 shared/cloud-agents.ts create mode 100644 src/lib/cloud-agents-api.ts create mode 100644 src/pages/CloudAgents/index.tsx create mode 100644 tests/e2e/cloud-agents.spec.ts create mode 100644 tests/unit/cloud-agents-main.test.ts create mode 100644 tests/unit/cloud-agents-page.test.tsx diff --git a/.project-docs/30-worklog/tasks/20260910-personal-agent-entry-e69a9434.md b/.project-docs/30-worklog/tasks/20260910-personal-agent-entry-e69a9434.md new file mode 100644 index 0000000..39b6e86 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260910-personal-agent-entry-e69a9434.md @@ -0,0 +1,52 @@ +# Task: Implement personal cloud agent entry and draft management + +## Identity + +- Task ID: 20260910-personal-agent-entry-e69a9434 +- Mode: Feature +- Branch: codex/20260910-personal-agent-entry-e69a9434-personal-agent-entry +- Worktree: D:\Datas\OthersProjects\.codex-worktrees\makelore\20260910-personal-agent-entry-e69a9434 +- Base commit: 927a85fa22e003b2e09740f41e6a91c71ee278c1 +- Owner: codex +- Status: Ready for Integration + +## Scope + +- Implement the first personal cloud Agent slice: Main-owned cloud Agent access, module navigation and draft creation/editor. +- Sharing, published invocation, model billing and scheduled execution remain subsequent slices of the accepted first release. + +## Intent And Constraints + +- Concurrent Task Gate and Planning Gate passed; bundled ownership matches this task, mode, branch, worktree and base. All writes remain in this isolated checkout. +- Read required memory entry, active record, relevant integrated architecture/auth decisions, existing three-end proposals and peer scope/intent/promotion sections. Undefined peers remain unknown; no concrete semantic dependency on this slice was found. Existing user/module-access seams remain authoritative. +- User-approved direction: personal creators without organizations, WS stable account identity and Token Point ledger, Yuxi Agent ownership, Main-only cloud credentials. Xiaozhi is excluded. One fresh independent reviewer and local commits are explicitly approved in the current follow-up. No additional subagents, primary-runtime changes, push or deployment. +- Credential policy for this draft-only slice: access token lasts at most five minutes and no longer than the originating WS access token. Main discards cached credentials and late responses at account change/logout. WS checks current account/module eligibility on each exchange; already-issued credentials have that bounded remaining lifetime. Local Yuxi deletion/lock is checked on every call. No perpetual refresh credential or runtime access is issued. +- Plan: implement typed session exchange and test identity/error projection; implement managed identity/private drafts with idempotent creation and conditional save, verified over PostgreSQL/HTTP; implement Main/Renderer entry and verify account-switch/conflict/navigation behavior, typecheck and build. +- Source-doc changes are scoped to current behavior; canonical project-memory promotion is deferred to integration. + +## Outcome + +- Added the independent Agents module, My Agents list, create form and private draft editor. Main exclusively exchanges/caches cloud credentials and projects safe typed DTOs through Host API. It binds calls to the current WS account epoch, rejects late responses, bounds request/body-read time and preserves explicit create operation IDs. Renderer supports conflict readback with local input preserved, explicit resolution, draft/creation leave warnings, account reset and list refresh. Switched the existing hash router to the data-router API so navigation blocking covers module changes/history; existing module navigation was tested. +- Independent review approved the complete three-repository increment after the creation-card navigation fix and Yuxi configuration-template correction. There are no unresolved actionable findings. The reviewed result and verification are ready for integration; no merge, push, release or deployment was performed, and this isolated worktree is retained. + +## Verification + +- Final related Vitest set: 94 passed across cloud Agents Main/UI, module access, auth routes/store, Sidebar and MainLayout tests. +- Electron Playwright: 10 passed across cloud-agents.spec.ts, main-navigation.spec.ts and app-smoke.spec.ts; includes create/edit/save/reopen, leave protection, four-entry resizing, existing Code navigation, login/setup and signed-out route guards. The cloud UI test mocks Host API, while actual Yuxi HTTP/PG is verified in the Yuxi task; it is not a three-server production integration claim. +- `pnpm typecheck` (Renderer), targeted ESLint and `pnpm build:vite` (Renderer/Main/preload/utility) passed. The final Electron run used this build. +- Additional full Main `tsc -p tsconfig.node.json --noEmit --composite false` remains failed with 61 diagnostics in unchanged files; git diff against the recorded base confirms those diagnostic files have no task changes. The new cloud-agents diagnostic was corrected. The standard composite config also has existing missing shared-file include diagnostics. Do not report all-project/Main typecheck as passing. +- Inspected the actual Electron screenshot at test-results/cloud-agents-personal-clou-f6c7b-eave-protection-in-Electron/cloud-agent-editor.png; no clipping in the 1280x800 editor view. git diff --check passed. No installer release, push or production deployment. + +## Follow-ups + +- Retain the three reviewed worktrees for integration. Next implementation slice is capability selection/preview chat with creator billing, then publication/sharing/API and automatic tasks; these remain first-release scope, not delivered by this draft-only slice. Existing full-Main type diagnostics remain with their owning tasks. + +## Promotion Candidates + +- Target: .project-docs/current-state.md and the owning MakeLore module/auth architecture record. Proposal: a fourth cloud Agents module, credentials/network in Main, Renderer through Host API, personal draft state with create replay and revision-based explicit saves. Evidence: 94 Vitest cases, 10 Electron cases, build and inspected screenshot. Future impact: add chat/automation/config/publish workspaces on the verified Yuxi foundations; keep creator/caller/payer separate. Semantic conflict: no new organization UI or local Code runtime ownership; earlier planning should distinguish existing Yuxi capability from new product adaptation. Human confirmation: direction already accepted; the approved independent review is complete. + +## Review And Commit Follow-up + +- User approved one independent reviewer without inherited development context and local commits after material findings are resolved. Same-task ownership and planning gates passed on resume; other task records have no scope changes. Review is a read-only subtask of these existing owned tasks. +- Fresh reviewer /root/independent_review (review task 01a08a42-9a55-79f3-807f-016e84308290) read the full requirements, complete staged/working diffs against each recorded base, relevant source/standards and verification limitations, then rechecked both fixes and returned Approve with no unresolved actionable findings. One reviewer was used; no inherited development context was provided. Local commits are authorized; remote operations remain outside this handoff. +- A new regression demonstrated that selecting an existing card during creation bypassed route blocking and discarded input or the uncertain creation operation. Cards are now disabled while the creation form is open; explicit cancel/list confirmation restores selection. The regression failed on the prior implementation and passes after this two-line UI fix. The five affected cloud Agents/auth/module Vitest files pass 83 tests; targeted ESLint and the final Renderer/Main/preload/utility build pass. diff --git a/README.md b/README.md index 14010b6..48ca473 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,10 @@ **一念成光,万物可创。** -Makelore 是一个面向软件、视觉创作与智能机器人的 AI 桌面工作台。当前版本为 `2.0.0`,包含三个已开通产品模块。模块入口页采用统一的横向卡片视觉,工作区左上角入口点击后返回模块入口页: +Makelore 是一个面向软件、视觉创作、智能机器人与个人云智能体的 AI 桌面工作台。当前版本为 `2.0.0`,源码提供四个模块入口;云智能体需配套配置 WS/Yuxi 服务。模块入口页采用统一的横向卡片视觉,工作区左上角入口点击后返回模块入口页: - `Makelore Code|AI 编程`:管理本地项目、项目智能体、对话、文件上下文、代码变更和运行时。 +- `Makelore Agents|AI 智能体`:使用现有账号创建私人云草稿,编辑名称、用途和角色指令。Main 持有 WS/Yuxi 云凭据,Renderer 只通过 Host API 获取草稿;保存检查修订号,冲突保留本地输入,离开时提示未保存修改。当前没有试聊、发布、分享、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 密码。 diff --git a/electron/api/route-handlers.ts b/electron/api/route-handlers.ts index 6bd875d..334c4e2 100644 --- a/electron/api/route-handlers.ts +++ b/electron/api/route-handlers.ts @@ -4,6 +4,7 @@ import { handleAiProxyRoutes } from './routes/ai-proxy'; import { handleAiHardwareRoutes } from './routes/ai-hardware'; import { handleAppRoutes } from './routes/app'; import { handleAuthRoutes } from './routes/auth'; +import { handleCloudAgentsRoutes } from './routes/cloud-agents'; import { handleImageWorkspaceRoutes } from './routes/image-workspace'; import { handleImagePromptMuseumRoutes } from './routes/image-prompt-museum'; import { handleDataServiceRoutes } from './routes/data-service'; @@ -41,6 +42,7 @@ export const hostApiRouteHandlers: readonly HostApiRouteHandler[] = [ handleAiHardwareRoutes, handleAppRoutes, handleAuthRoutes, + handleCloudAgentsRoutes, handleImageWorkspaceRoutes, handleImagePromptMuseumRoutes, handleDataServiceRoutes, diff --git a/electron/api/routes/cloud-agents.ts b/electron/api/routes/cloud-agents.ts new file mode 100644 index 0000000..67c5da3 --- /dev/null +++ b/electron/api/routes/cloud-agents.ts @@ -0,0 +1,34 @@ +import type { IncomingMessage, ServerResponse } from 'node:http'; +import { CLOUD_AGENTS_PATH } from '../../../shared/cloud-agents'; +import { CloudAgentsError, CloudAgentsModule } from '../../services/cloud-agents'; +import { parseJsonBody, sendJson } from '../route-utils'; + +const cloudAgents = new CloudAgentsModule(); + +export async function handleCloudAgentsRoutes(req: IncomingMessage, res: ServerResponse, url: URL): Promise { + if (!url.pathname.startsWith(CLOUD_AGENTS_PATH + '/')) return false; + res.setHeader('Cache-Control', 'no-store'); + const path = url.pathname.slice(CLOUD_AGENTS_PATH.length); + try { + let data: unknown; + if ((path === '/bootstrap' || path === '/agents') && req.method === 'GET') { + data = await cloudAgents.list(url.searchParams.get('cursor')); + } else if (path === '/agents' && req.method === 'POST') { + data = await cloudAgents.create(await parseJsonBody(req)); + } else { + const match = /^\/agents\/(ml-[a-f0-9]{32})(\/draft)?$/.exec(path); + if (match && !match[2] && req.method === 'GET') data = await cloudAgents.get(match[1]); + else if (match && match[2] && req.method === 'PATCH') data = await cloudAgents.save(match[1], await parseJsonBody(req)); + else { + sendJson(res, 404, { error: '智能体接口不存在' }); + return true; + } + } + sendJson(res, 200, data); + } catch (error) { + const failure = error instanceof CloudAgentsError ? error + : new CloudAgentsError(error instanceof SyntaxError ? 400 : 502, error instanceof SyntaxError ? 'invalid_input' : 'cloud_service_unavailable'); + sendJson(res, failure.status, { success: false, error: failure.message, code: failure.code }); + } + return true; +} diff --git a/electron/services/cloud-agents.ts b/electron/services/cloud-agents.ts new file mode 100644 index 0000000..97cba47 --- /dev/null +++ b/electron/services/cloud-agents.ts @@ -0,0 +1,199 @@ +import type { CloudAgentDraft, CloudAgentPage, CreateCloudAgent, SaveCloudAgentDraft } from '../../shared/cloud-agents'; +import { WORKS_SQUARE_CONFIG } from '../api/works-config'; +import { proxyAwareFetch, runWithDeadline } from '../utils/proxy-fetch'; +import { + getValidWorksSquareAccessToken, + getWorksSquareAccountBinding, + isCurrentWorksSquareAccountBinding, + subscribeWorksSquareSession, + type WorksSquareAccountBinding, +} from './works-square-session'; + +const MESSAGES: Record = { + session_expired: '登录已过期,请重新登录', + account_changed: '账号已切换,请重新打开智能体', + account_locked: '账号已锁定,请稍后重试', + module_unavailable: 'AI 智能体模块暂不可用', + agent_not_found: '智能体不存在', + draft_revision_conflict: '草稿已在其他设备更新,你的输入已保留', + operation_conflict: '创建操作的内容发生冲突,请刷新列表确认', + cloud_service_unavailable: '智能体服务暂时不可用,请重试', + invalid_input: '请检查名称、用途和配置内容', +}; + +export class CloudAgentsError extends Error { + constructor(readonly status: number, readonly code: string) { + super(MESSAGES[code] ?? MESSAGES.cloud_service_unavailable); + } +} + +function record(value: unknown): Record { + return value !== null && typeof value === 'object' && !Array.isArray(value) + ? value as Record : {}; +} + +function textField(value: unknown, max: number, nonempty = true): string { + if (typeof value !== 'string' || value.length > max || (nonempty && !value.trim())) { + throw new CloudAgentsError(422, 'invalid_input'); + } + return value; +} + +function revision(value: unknown): number { + if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1) { + throw new CloudAgentsError(422, 'invalid_input'); + } + return value; +} + +function slug(value: string): string { + if (!/^ml-[a-f0-9]{32}$/.test(value)) throw new CloudAgentsError(422, 'invalid_input'); + return value; +} + +function projectDraft(value: unknown): CloudAgentDraft { + const item = record(value); + try { return { + slug: slug(textField(item.slug, 80)), + name: textField(item.name, 100), + purpose: textField(item.purpose, 2000), + system_prompt: textField(item.system_prompt, 32000, false), + draft_revision: revision(item.draft_revision), + updated_at: textField(item.updated_at, 40), + }; } catch { throw new CloudAgentsError(502, 'cloud_service_unavailable'); } +} + +type Session = { + binding: WorksSquareAccountBinding; + accessToken: string; + apiBaseUrl: string; + expiresAt: number; +}; + +/** Main owns both clouds' credentials; public methods return only Agent drafts. */ +export class CloudAgentsModule { + private cached: Session | null = null; + private readonly unsubscribe: () => void; + + constructor(private readonly fetchImpl: (input: string, init?: RequestInit) => Promise = proxyAwareFetch) { + this.unsubscribe = subscribeWorksSquareSession(() => { + if (this.cached && !isCurrentWorksSquareAccountBinding(this.cached.binding)) this.cached = null; + }); + } + + dispose(): void { + this.cached = null; + this.unsubscribe(); + } + + async list(cursor: string | null = null): Promise { + if (cursor !== null && !/^\d{1,16}$/.test(cursor)) throw new CloudAgentsError(422, 'invalid_input'); + const body = record(await this.request('/api/makelore/agents' + (cursor ? `?cursor=${cursor}` : ''), 'GET')); + if (!Array.isArray(body.agents) || body.agents.length > 100 + || (body.next_cursor !== null && (typeof body.next_cursor !== 'string' || !/^\d{1,16}$/.test(body.next_cursor)))) { + throw new CloudAgentsError(502, 'cloud_service_unavailable'); + } + return { agents: body.agents.map(projectDraft), next_cursor: body.next_cursor as string | null }; + } + + async get(agentSlug: string): Promise { + return projectDraft(await this.request(`/api/makelore/agents/${slug(agentSlug)}`, 'GET')); + } + + async create(value: unknown): Promise { + const input = record(value); + const operationId = textField(input.operation_id, 36); + if (!/^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(operationId)) { + throw new CloudAgentsError(422, 'invalid_input'); + } + const body: CreateCloudAgent = { + operation_id: operationId, + name: textField(input.name, 100).trim(), + purpose: textField(input.purpose, 2000).trim(), + }; + return projectDraft(await this.request('/api/makelore/agents', 'POST', body)); + } + + async save(agentSlug: string, value: unknown): Promise { + const input = record(value); + const body: SaveCloudAgentDraft = { + expected_revision: revision(input.expected_revision), + name: textField(input.name, 100).trim(), + purpose: textField(input.purpose, 2000).trim(), + system_prompt: textField(input.system_prompt, 32000, false), + }; + return projectDraft(await this.request(`/api/makelore/agents/${slug(agentSlug)}/draft`, 'PATCH', body)); + } + + private async request(path: string, method: string, body?: unknown): Promise { + const binding = getWorksSquareAccountBinding(); + if (!binding) throw new CloudAgentsError(401, 'session_expired'); + try { + return await runWithDeadline(async (signal) => { + const session = await this.session(binding, signal); + this.requireCurrent(binding); + const result = await this.fetchJson(session.apiBaseUrl + path, { + method, signal, redirect: 'error', + headers: { Authorization: `Bearer ${session.accessToken}`, 'Content-Type': 'application/json' }, + ...(body === undefined ? {} : { body: JSON.stringify(body) }), + }); + this.requireCurrent(binding); + return result; + }, 30000); + } catch (error) { + this.requireCurrent(binding); + if (error instanceof CloudAgentsError) { + if (error.status === 401) this.cached = null; + if (error.status === 422) throw new CloudAgentsError(502, 'cloud_service_unavailable'); + throw error; + } + throw new CloudAgentsError(502, 'cloud_service_unavailable'); + } + } + + private requireCurrent(binding: WorksSquareAccountBinding): void { + if (!isCurrentWorksSquareAccountBinding(binding)) throw new CloudAgentsError(409, 'account_changed'); + } + + private async session(binding: WorksSquareAccountBinding, signal: AbortSignal): Promise { + if (this.cached && this.cached.expiresAt > Date.now() + 5000 + && this.cached.binding.accountKey === binding.accountKey && this.cached.binding.epoch === binding.epoch) { + return this.cached; + } + const token = await getValidWorksSquareAccessToken(); + this.requireCurrent(binding); + if (!token) throw new CloudAgentsError(401, 'session_expired'); + const value = record(await this.fetchJson(`${WORKS_SQUARE_CONFIG.apiBaseUrl}/api/cloud-agents/session`, { + method: 'POST', signal, redirect: 'error', + headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, + })); + this.requireCurrent(binding); + const url = new URL(textField(value.api_base_url, 2048)); + if ((url.protocol !== 'https:' && !(url.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname))) + || url.username || url.password || url.search || url.hash + || value.scope !== 'makelore-agent-drafts' || value.token_type !== 'bearer' + || typeof value.expires_at !== 'number' || value.expires_at * 1000 <= Date.now() + || value.expires_at * 1000 > Date.now() + 300000) { + throw new CloudAgentsError(502, 'cloud_service_unavailable'); + } + this.cached = { + binding, accessToken: textField(value.access_token, 8192), + apiBaseUrl: url.toString().replace(/\/+$/, ''), expiresAt: value.expires_at * 1000, + }; + return this.cached; + } + + private async fetchJson(url: string, init: RequestInit): Promise { + const response = await this.fetchImpl(url, init); + const raw = await response.text(); + if (raw.length > 4_000_000) throw new CloudAgentsError(502, 'cloud_service_unavailable'); + const body: unknown = JSON.parse(raw); + if (!response.ok) { + const code = record(record(body).detail).code; + throw new CloudAgentsError( + response.status, typeof code === 'string' && Object.hasOwn(MESSAGES, code) ? code : 'cloud_service_unavailable', + ); + } + return body; + } +} diff --git a/shared/cloud-agents.ts b/shared/cloud-agents.ts new file mode 100644 index 0000000..7c58669 --- /dev/null +++ b/shared/cloud-agents.ts @@ -0,0 +1,28 @@ +export const CLOUD_AGENTS_PATH = '/api/cloud-agents'; + +export interface CloudAgentDraft { + slug: string; + name: string; + purpose: string; + system_prompt: string; + draft_revision: number; + updated_at: string; +} + +export interface CloudAgentPage { + agents: CloudAgentDraft[]; + next_cursor: string | null; +} + +export interface CreateCloudAgent { + operation_id: string; + name: string; + purpose: string; +} + +export interface SaveCloudAgentDraft { + expected_revision: number; + name: string; + purpose: string; + system_prompt: string; +} diff --git a/shared/module-access.ts b/shared/module-access.ts index 412a954..a953145 100644 --- a/shared/module-access.ts +++ b/shared/module-access.ts @@ -2,6 +2,7 @@ export const MODULE_ACCESS_KEYS = [ 'programming', 'design', 'robot', + 'cloud_agents', ] as const; export type ModuleAccessKey = typeof MODULE_ACCESS_KEYS[number]; @@ -12,6 +13,7 @@ export const DEFAULT_MODULE_ACCESS: ModuleAccess = { programming: true, design: true, robot: true, + cloud_agents: true, }; export function normalizeModuleAccess(value: unknown): ModuleAccess { @@ -23,5 +25,6 @@ export function normalizeModuleAccess(value: unknown): ModuleAccess { programming: typeof record.programming === 'boolean' ? record.programming : true, design: typeof record.design === 'boolean' ? record.design : true, robot: typeof record.robot === 'boolean' ? record.robot : true, + cloud_agents: typeof record.cloud_agents === 'boolean' ? record.cloud_agents : true, }; } diff --git a/src/App.tsx b/src/App.tsx index 4708f4d..55970cf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -40,6 +40,7 @@ const ProjectConfiguration = lazy(() => import('./pages/ProjectConfiguration').t const Workbench = lazy(() => import('./pages/Workbench').then(({ Workbench: component }) => ({ default: component }))); const ImageCanvas = lazy(() => import('./pages/ImageCanvas').then(({ ImageCanvas: component }) => ({ default: component }))); const AiHardware = lazy(() => import('./pages/AiHardware').then(({ AiHardware: component }) => ({ default: component }))); +const CloudAgents = lazy(() => import('./pages/CloudAgents')); const Settings = lazy(() => import('./pages/Settings').then(({ Settings: component }) => ({ default: component }))); const Setup = lazy(() => import('./pages/Setup').then(({ Setup: component }) => ({ default: component }))); const Login = lazy(() => import('./pages/Login').then(({ Login: component }) => ({ default: component }))); @@ -457,6 +458,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 1d8a0f2..b145ced 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -781,7 +781,13 @@ export function Sidebar({ ) : ( -
+ activeModule === 'cloud_agents' ?
+ +
:
+ + {creating && { setCreating(false); void load(); }} />} + {error &&
+

{error}

+
} + {loading && agents.length === 0 ?

正在读取云端智能体…

+ : !error && agents.length === 0 && !creating ?
+ +

你的第一个智能体,从这里开始

+

例如写作搭档、资料整理助手,或你的专属顾问。

+ +
: null} +
+ {agents.map((agent) => )} +
+ {cursor && } + + ); +} + +function CreateAgentForm({ onCreated, onCancel }: { onCreated: (agent: CloudAgentDraft) => void; onCancel: () => void }) { + const [name, setName] = useState(''); + const [purpose, setPurpose] = useState(''); + const [intent, setIntent] = useState(null); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(''); + const alive = useRef(true); + const blocker = useBlocker(busy || intent !== null || Boolean(name || purpose)); + useEffect(() => { alive.current = true; return () => { alive.current = false; }; }, []); + useEffect(() => { + const warn = (event: BeforeUnloadEvent) => { + if (busy || intent || name || purpose) { event.preventDefault(); event.returnValue = ''; } + }; + window.addEventListener('beforeunload', warn); + return () => window.removeEventListener('beforeunload', warn); + }, [busy, intent, name, purpose]); + + const submit = async () => { + if (busy || !name.trim() || !purpose.trim()) return; + const input = intent ?? { operation_id: crypto.randomUUID(), name: name.trim(), purpose: purpose.trim() }; + setIntent(input); + setBusy(true); + setError(''); + try { + const agent = await cloudAgentsApi.create(input); + if (alive.current) onCreated(agent); + } catch (failure) { + if (alive.current) setError(errorMessage(failure)); + } finally { + if (alive.current) setBusy(false); + } + }; + + return
{ event.preventDefault(); void submit(); }}> +

创建智能体

+ +