Simplify Agent settings and start conversations directly

This commit is contained in:
鲨鱼辣椒 committed 2026-10-12 19:38:57 +08:00
1 parent f7b1003c4c
commit 0a43ae901f
9 files changed
+402 -102

No files matched your search

@@ -0,0 +1,85 @@
# Task: Simplify Agent settings and open conversations directly
## Identity
- Task ID: 20261012-agent-config-simplify-c6829a4e
- Mode: Feature
- Branch: codex/20261012-agent-config-simplify-c6829a4e-agent-config-simplify
- Worktree: /Users/chillishark/Makelore 麦洛/.codex-worktrees/makelore/20261012-agent-config-simplify-c6829a4e
- Base commit: f7b1003c4c0b86a09b47e62e8e20d836fe0dd9d0
- Owner: codex
- Status: Planning
## Scope
- Simplify personal Agent advanced settings: remove Tool/Skill helper descriptions, cloud capability management, budget and execution-limit cards.
- Make the explicit Start Use action activate the saved private Agent when required and open its formal conversation directly.
- Update and verify the latest isolated temporary application using the existing trial profile.
## Intent And Constraints
- User-facing discussion stays in Chinese product language. Current explicit request supersedes older helper copy and the second activation confirmation screen.
- Keep model selection required and raw prompt authoritative. Preserve existing budget/runtime configuration and service policies; hide controls without clearing stored values.
- Keep typed Host API, explicit save, dirty/conflict/model/account guards and recovery of uncertain actions. Opening a conversation must not send a message or invoke a model.
- Do not modify another checkout, canonical project memory, installed application, or cloud services. Preserve trial data and do not activate a real cloud Agent during verification.
## Project Context Loaded
Task context:
- Task ID: 20261012-agent-config-simplify-c6829a4e
- Mode: Feature
- Branch: codex/20261012-agent-config-simplify-c6829a4e-agent-config-simplify
- Worktree: /Users/chillishark/Makelore 麦洛/.codex-worktrees/makelore/20261012-agent-config-simplify-c6829a4e
- Base commit: f7b1003c4c0b86a09b47e62e8e20d836fe0dd9d0
- Ownership: bundled start succeeded and status JSON matched identity, branch, base and absolute worktree.
- Other active local tasks: 34 peer owners; all peer task-record Scope / Intent And Constraints / Promotion Candidates read, read-only.
- Overlap: prior completed Agent configuration, design, purpose and trial tasks overlap; the current user's explicit refinement resolves their presentation differences. One old child-language task has undefined scope but no concrete dependency. No blocking conflict.
Read:
- AGENTS.md; 05-agent-entry/read-before-planning.md, memory-index.md, planning-gate.md; active task record.
- 00-brief/project-positioning.md, success-criteria.md, community-collaboration.md; 30-worklog/current-state.md integrated snapshot/current focus.
- 10-decisions/decision-index.md and ADR-2026-09-11-personal-cloud-agents.md; 20-architecture/system-overview.md; 40-domain/business-rules.md.
- 50-evidence/evidence-index.md; 60-reflection/reflection-index.md; 80-commitments/commitments.md.
Relevant understanding:
- Project goal: approachable creation and use of personal AI assistants for students and creators.
- Current integrated focus: unified client capabilities and cloud integration; the integrated guided Agent UI predates the user's October simplification.
- Active scope: presentation removals and one-click entry to formal personal conversation, followed by a local trial update.
- Active constraints: Main owns authentication/network/recovery, Yuxi owns configurations/versions/execution, Works owns eligibility and billing. Payment does not grant broader resource access.
- Decisions: model is explicitly selected; private activation remains explicit; saved draft and published version are distinct. Budget/runtime policies remain even with controls hidden.
- Evidence/reflections/commitments: fixture tests do not establish real paid-model acceptance. Existing release/provider commitments remain outside this temporary UI update; no real cloud mutation is needed for verification.
- Likely modules: ConfigurationFields, Page/DraftEditor/useCloudDraft/Publication flow, scoped tests, README, local trial lifecycle artifacts.
- Stale context: older first-use confirmation and helper descriptions are intentionally superseded by this user request. Optional description cloud deployment is a separate existing dependency.
Gate result:
- Passed.
## Plan
1. Remove the requested advanced settings sections while retaining persisted configuration fields.
2. Connect Start Use to existing activation/recovery and formal conversation without the intermediate screen; add focused regression coverage.
3. Run focused checks and production build; stage a uniquely identified temporary application, verify it before replacing the current runtime, and show the updated page.
4. Record actual verification and remaining limits in this task record, commit the result and complete task ownership.
## Outcome
- Requested helper copy and capability-manager/budget/runtime cards are removed from the personal configuration page. Knowledge management and model/resource choices remain; saving changes preserves hidden runtime policy fields and does not read/write budgets.
- Start Use now performs the existing explicit private activation and opens formal chat on confirmed success, without an access/confirmation panel. Missing model, dirty configuration, conflicting save and unreadable state remain actionable in configuration; retries retain their original operation identity.
- Implementation and fixture verification complete; real temporary-application replacement and review pending.
## Verification
- Typecheck and scoped ESLint passed; git diff --check passed.
- Seven focused unit files: 146 passed; final added activation-status/revised-draft recovery cases verified with all 9 direct-use cases passing separately (148 distinct focused cases total).
- 10 Playwright cases passed: 9 Chrome layout/avatar cases at 375/768/1024/1440 and 1 Electron identity/configuration/private-activation/direct-chat case.
- Independent read-only review found no actionable regression; added the revised-draft/old-activation recovery case from that review. Mobile advanced settings screenshot reviewed after removals.
- Production build:vite passed for Renderer, Main, preload and utility targets. Existing Browserslist age/chunk-size warnings only.
- Native current trial observed on WeChat channel details with disabled Save Target; no current unsaved form was visible. No real cloud publish, save or model message was sent during verification.
## Follow-ups
- Replace latest temporary runtime after exact build binding and isolated startup probe; verify real login/page separately from fixture results.
## Promotion Candidates
- Target: README-linked Agent UI descriptions and ADR-2026-09-11-personal-cloud-agents/current-state when integrated. Proposal: advanced settings retain model/resource selection without helper, resource-management, budget or runtime cards; explicit Start Use directly activates privately and opens formal chat. Evidence: this user's 2026-10-12 request, 148 distinct scoped unit cases and 10 layout/Electron cases. Impact: fewer configuration distractions and one fewer activation page. Semantic conflict: older first-use confirmation/UI descriptions are superseded by the user's explicit request; backend version, payment, access, budget and runtime policies remain. Human confirmation: already supplied for this implementation; canonical promotion still requires serialized Integration Gate.
+1 -1
View File
@@ -12,7 +12,7 @@ Makelore 是一个面向软件、视觉创作、智能机器人与个人云智
- `Makelore Code|AI 编程`:管理本地项目、项目智能体、对话、文件上下文、代码变更和运行时。
会话工作台采用浅蓝项目栏、彩色通用手柄图标与卡片选中态;中间对话/作品使用圆角标签,作品预览的后退、前进、刷新与网址输入统一放在标签右侧;只在作品页显示,窄窗口自动换行。右侧智能体咨询使用奶油色标题区。沿用真实项目和会话入口、侧栏伸缩与账户菜单,样式只作用于 Code 会话工作台。可编辑的空对话在输入区上方的可用区域中央显示童趣欢迎语;「去作品广场逛逛」入口紧贴输入框上方,两者一起位于底部。老师介绍以固定宽度浮在右上角,不占用中间布局;欢迎语和底部输入区始终相对项目栏右侧的整个内容区域居中;小窗口的欢迎内容独立滚动;发送首条消息后收起欢迎区。侧栏保留「作品广场」入口,只有点击后才在系统浏览器打开公共广场,不请求、摘取或内嵌展示广场作品。打开广场保留当前项目、对话、草稿和附件,不自动发送消息;老师求助按钮保留临时应用的最新文案「帮我整体看🧠」。
- `Makelore Agents|AI 智能体`:首页围绕自己的智能体和创建入口,显示童趣问候“Hi,今天想创造还是想聊聊?”,不再显示更多、收到的分享、活动或归档筛选。卡片显示名称、可为空的一句话介绍及状态,支持聊天、调整、继续制作和确认删除;删除沿用云端归档机制,移出列表并停用新调用,历史和费用保留。创建先填写名称并选择平台像素头像或上传头像,下一步进入独立配置。顶部名称有编辑图标,头像可点击修改;未设置头像时使用同一张默认像素头像,从创建到配置、改名、保存和重新打开保持一致。基础页为“介绍与提示词”,一句话介绍可留空,完整提示词保留原始文本;模型作为高级设置第一项必选,未选择时不能开始使用,草稿可分步保存,不自动选模型。介绍与提示词采用浅蓝介绍卡和浅紫提示词卡;高级设置按模型、工具箱(Tool)、做事手册(Skill)、MCP 服务、子智能体、知识库和限制纵向呈现,不再设内部分类切换。工具箱说明为“选它做事时能用的工具。”,做事手册说明为“给它一本手册,照着方法做。”。顶层页签切换保留搜索、资源选择、知识表单、费用及未保存输入。配置页移除聊天、自动任务和使用与接入导航,独立聊天仍从卡片进入,微信账号仍在侧栏渠道管理。草稿明确保存(Ctrl/⌘+S);保存只更新草稿,“应用最新修改”使新配置生效,失败重试保持原操作身份;首次确认开始使用创建仅自己可用的发布版本并打开聊天,配置不自动发送消息。头像上传、Main-owned Host API、访问隔离、费用与不可变版本规则保持;介绍留空需要配套 Yuxi DraftCreate/DraftSave 的空字符串契约。
- `Makelore Agents|AI 智能体`:首页围绕自己的智能体和创建入口,显示童趣问候“Hi,今天想创造还是想聊聊?”,不再显示更多、收到的分享、活动或归档筛选。卡片显示名称、可为空的一句话介绍及状态,支持聊天、调整、继续制作和确认删除;删除沿用云端归档机制,移出列表并停用新调用,历史和费用保留。创建先填写名称并选择平台像素头像或上传头像,下一步进入独立配置。顶部名称有编辑图标,头像可点击修改;未设置头像时使用同一张默认像素头像,从创建到配置、改名、保存和重新打开保持一致。基础页为“介绍与提示词”,一句话介绍可留空,完整提示词保留原始文本;模型作为高级设置第一项必选,未选择时不能开始使用,草稿可分步保存,不自动选模型。介绍与提示词采用浅蓝介绍卡和浅紫提示词卡;高级设置按模型、工具箱(Tool)、做事手册(Skill)、MCP 服务、子智能体和知识库纵向呈现,不再设内部分类切换;不展示 Tool/Skill 解释、云端能力管理、费用上限或运行限制卡片,保存其他配置时保留既有运行限制,费用规则不变。顶层页签切换保留搜索、资源选择、知识表单及未保存输入。配置页移除聊天、自动任务和使用与接入导航,独立聊天仍从卡片进入,微信账号仍在侧栏渠道管理。草稿明确保存(Ctrl/⌘+S);保存只更新草稿,“应用最新修改”使新配置生效,失败重试保持原操作身份;点击“开始使用”完成必要的私有启用后直接打开聊天,不再展示二次确认页;未保存修改、缺少模型、冲突或尚未确认的请求会留在配置页处理,打开聊天不自动发送消息。头像上传、Main-owned Host API、访问隔离、费用与不可变版本规则保持;介绍留空需要配套 Yuxi DraftCreate/DraftSave 的空字符串契约。
云知识库的 embedding 模型只由平台管理员在 Yuxi 后台维护:在「智能体管理 → 模型供应商」启用供应商,按供应商要求填写有效的服务地址和 API Key,添加并启用 embedding 模型并填写实际向量维度;embedding 向量化费用由平台承担,不计入用户用量、词元点数或智能体费用上限;one-api 不参与 embedding 配置,Works Square 只负责其他应计费模型调用的创建者词元点数账本。客户端区分目录加载中、加载失败与无可用模型,空状态提供管理员配置说明;「刷新知识库与模型」保留未提交名称,未确认的创建请求沿用原操作身份。
+5 -27
View File
@@ -1,17 +1,15 @@
import { useEffect, useState } from 'react';
import { BookOpen, Brain, Library, Plug, RefreshCw, SlidersHorizontal, Users, Wrench } from 'lucide-react';
import { BookOpen, Brain, Library, Plug, RefreshCw, Users, Wrench } from 'lucide-react';
import { cloudAgentsApi } from '@/lib/cloud-agents-api';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { CloudKnowledgePanel } from './CloudKnowledge';
import { CloudResources } from './CloudResources';
import { CloudBudgetEditor } from './CloudCosts';
import { AgentConfigHeading } from './AgentConfigHeading';
import type { CloudAgentConfiguration, CloudCatalog } from '../../../shared/cloud-agents';
const resourceSections = [
{ key: 'tools', label: '工具箱', title: '工具箱(Tool)', description: '选它做事时能用的工具。', icon: Wrench, tone: 'blue' },
{ key: 'skills', label: '做事手册', title: '做事手册(Skill)', description: '给它一本手册,照着方法做。', icon: BookOpen, tone: 'lilac' },
{ key: 'tools', label: '工具箱', title: '工具箱(Tool)', icon: Wrench, tone: 'blue' },
{ key: 'skills', label: '做事手册', title: '做事手册(Skill)', icon: BookOpen, tone: 'lilac' },
{ key: 'mcps', label: 'MCP 服务', title: 'MCP 服务', icon: Plug, tone: 'neutral' },
{ key: 'subagents', label: '子智能体', title: '子智能体', icon: Users, tone: 'neutral' },
{ key: 'knowledges', label: '知识库', title: '知识库', icon: Library, tone: 'mint' },
@@ -57,7 +55,7 @@ export function ConfigurationFields({ slug, value, disabled, onChange, section =
return <fieldset key={key} disabled={disabled} className="agent-config-card agent-resource-card" data-tone={tone}>
<legend className="sr-only">{title}</legend>
<div className="agent-config-section-top">
<AgentConfigHeading title={title} icon={icon} note={'已选 ' + value[key].length} description={'description' in item ? item.description : undefined} />
<AgentConfigHeading title={title} icon={icon} note={'已选 ' + value[key].length} />
<Button type="button" variant="outline" size="sm" onClick={() => { setChoosing(choosing === key ? null : key); setSearch(''); }}>{choosing === key ? '完成选择' : '选择' + label}</Button>
</div>
{choosing === key && <Input className="mb-3" aria-label={'搜索' + label} placeholder={'搜索' + label} value={search} onChange={e => setSearch(e.target.value)} />}
@@ -78,26 +76,6 @@ export function ConfigurationFields({ slug, value, disabled, onChange, section =
onCreated={kb => { change('knowledges', [...new Set([...value.knowledges, kb.kb_id])]); setRefresh(v => v + 1); }} /></div>}
</fieldset>;
})}
<div className="agent-config-card agent-config-resource-manager" data-tone="neutral">
<p className="mb-4 text-xs leading-5 text-muted-foreground">未选择的资源不会开放给此智能体。通过微信或 API 使用时也遵循本配置。</p>
<CloudResources onChanged={() => setRefresh(v => v + 1)} />
</div>
<div className="agent-config-budget"><CloudBudgetEditor slug={slug} /></div>
<section className="agent-config-card" data-tone="peach">
<AgentConfigHeading title="运行限制" icon={SlidersHorizontal} />
<p className="mb-4 text-xs leading-6 text-muted-foreground">以下设置随草稿保存;费用上限独立保存,对后续调用生效。执行步数会统计模型调用、工具、图节点和中间件步骤。</p>
<label className="block space-y-2 text-sm"><span>工具审批</span>
<select disabled={disabled} value={value.tool_approval_mode} onChange={e => change('tool_approval_mode', e.target.value === 'always_trust' ? 'always_trust' : 'default')}>
<option value="default">按工具要求确认</option><option value="always_trust">信任已选工具,自动执行</option>
</select></label>
<div className="agent-config-limit-grid">
{([{ key: 'max_execution_steps', label: '最多执行步数', min: 1, max: 300 },
{ key: 'max_output_tokens', label: '单次输出词元上限', min: 1, max: 32768 },
{ key: 'max_run_seconds', label: '单次运行时限(秒)', min: 10, max: 3600 }] as const).map(item =>
<label key={item.key} className="space-y-2 text-xs"><span>{item.label}</span>
<Input type="number" min={item.min} max={item.max} value={value[item.key]} disabled={disabled}
onChange={e => change(item.key, Number(e.target.value))} /></label>)}
</div>
</section>
<p className="text-xs leading-5 text-muted-foreground">未选择的资源不会开放给此智能体。通过微信或 API 使用时也遵循本配置。</p>
</div>;
}
+22 -19
View File
@@ -10,7 +10,6 @@ import { EMPTY_CLOUD_CONFIGURATION, type CloudAgentDraft, type CloudRecent } fro
import { ConfigurationFields } from './ConfigurationFields';
import { CloudChat } from './CloudChat';
import { CloudChannelConversations } from './CloudChannelConversations';
import { CloudAccessPanel } from './CloudAccess';
import { CloudPendingContext, useCloudPendingState } from './CloudPending';
import { AgentRequirements } from './AgentRequirements';
import { CloudAvatar, CloudAvatarEditor } from './CloudAvatar';
@@ -18,7 +17,7 @@ import { useCloudPublication } from './useCloudPublication';
import { AgentConfigHeading } from './AgentConfigHeading';
import './agent-config.css';
type Tab = 'configuration' | 'chat' | 'access';
type Tab = 'configuration' | 'chat';
const hasDraftChanges = (draft: CloudAgentDraft, saved: CloudAgentDraft) => draft.name !== saved.name || (draft.icon ?? null) !== (saved.icon ?? null) || draft.purpose !== saved.purpose || draft.system_prompt !== saved.system_prompt
|| JSON.stringify(draft.configuration) !== JSON.stringify(saved.configuration);
const errorMessage = (error: unknown) => error instanceof Error ? error.message : '暂时无法完成操作';
@@ -103,6 +102,20 @@ export function DraftEditor({ initial, recent, onBack, onSaved, startingPrompt,
} catch { /* Keep the failed save and local input available for retry. */ }
} finally { saving.current = false; if (alive.current) setBusy(false); }
};
const startUsing = () => {
if (busy || saving.current) return;
if (remote) { navigate('configuration'); setError('云端已有新修改,请先处理保存冲突。'); return; }
if (!draft.configuration.model) {
navigate('configuration');
setEditorTab('advanced');
if (configurationScroll.current) configurationScroll.current.scrollTop = 0;
setError('请先在高级设置中选择模型。'); return;
}
if (dirty) { navigate('configuration'); setError('请先保存草稿,再开始使用。'); return; }
if (!publication.access) { setError('暂时无法确认智能体状态,请刷新后重试。'); return; }
setError('');
void publication.publish();
};
useEffect(() => {
const shortcut = (event: KeyboardEvent) => {
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's') { event.preventDefault(); void save(); }
@@ -139,14 +152,10 @@ export function DraftEditor({ initial, recent, onBack, onSaved, startingPrompt,
{publication.busy && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{publication.busy ? '正在应用…' : publication.pending ? '重试本次应用' : !dirty && publication.appliedRevision === saved.draft_revision ? '已应用' : '应用最新修改'}
</Button>}
{!saved.published_version && <Button className="min-h-10" variant="outline" disabled={busy} onClick={() => {
if (!draft.configuration.model) {
setEditorTab('advanced');
if (configurationScroll.current) configurationScroll.current.scrollTop = 0;
setError('请先在高级设置中选择模型。'); return;
}
setError(''); navigate('access');
}}>开始使用</Button>}
{!saved.published_version && <Button className="min-h-10" variant="outline" disabled={busy || remote !== null || !publication.access} onClick={startUsing}>
{publication.busy && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{publication.busy ? '正在开始…' : publication.pending ? '重试开始使用' : '开始使用'}
</Button>}
</header>
{tab !== 'configuration' && <nav aria-label="智能体操作" className="agent-workspace-nav"><Button variant="ghost" onClick={() => {
if (formalPending.hasPending) { setError('对话中还有未完成的内容,请先发送或处理后再调整智能体。'); return; }
@@ -156,7 +165,7 @@ export function DraftEditor({ initial, recent, onBack, onSaved, startingPrompt,
<p className="font-medium">{dirty ? '修改尚未保存和应用' : '这次修改还没有应用到正在使用的助手'}</p>
<p className="mt-1 text-xs leading-5">保存只更新草稿。请点击顶部“应用最新修改”,再到微信等渠道使用。已开始的任务保持原配置。</p>
</div>}
{publication.error && tab !== 'access' && <p role="alert" className="shrink-0 px-6 py-2 text-sm text-destructive">{publication.error} <Button variant="ghost" disabled={busy} onClick={() => void publication.refresh()}>刷新应用状态</Button></p>}
{publication.error && <p role="alert" className="shrink-0 px-6 py-2 text-sm text-destructive">{publication.error} <Button variant="ghost" disabled={busy} onClick={() => void publication.refresh()}>刷新应用状态</Button></p>}
{error && <p role="alert" className="shrink-0 px-6 py-2 text-sm text-destructive">{error}</p>}
{remote && <div className="max-h-52 shrink-0 space-y-2 overflow-auto border-b border-amber-200 bg-amber-50 px-6 py-3 text-sm">
<p className="font-medium">云端已有修订 {remote.draft_revision},本地输入仍保留</p>
@@ -175,7 +184,7 @@ export function DraftEditor({ initial, recent, onBack, onSaved, startingPrompt,
<TabsTrigger value="advanced" className="flex-1 gap-2 data-[state=active]:text-primary"><SlidersHorizontal aria-hidden="true" className="h-4 w-4" />高级设置</TabsTrigger>
</TabsList>
</div>
{/* Keep the shared form mounted so switching tabs preserves unfinished resource and budget edits. */}
{/* Keep the shared form mounted so switching tabs preserves unfinished resource edits. */}
<TabsContent value={editorTab} className="m-0 flex min-h-0 flex-1 flex-col">
<div ref={configurationScroll} className="agent-config-scroll agent-editor-scroll"><fieldset disabled={busy} className="agent-configuration-form">
<div hidden={editorTab !== 'instructions'} className="agent-config-stack">
@@ -202,13 +211,7 @@ export function DraftEditor({ initial, recent, onBack, onSaved, startingPrompt,
<CloudChat key="published" slug={saved.slug} activeView={tab === 'chat' && conversationSource === 'desktop'} /></div>
<div hidden={conversationSource !== 'wechat'} className="min-h-0 flex-1 overflow-auto">{wechatVisited && <CloudChannelConversations slug={saved.slug} activeView={tab === 'chat' && conversationSource === 'wechat'} />}</div>
</CloudPendingContext.Provider>
: <div className="m-auto space-y-4 text-center text-sm"><p>保存配置后,开始使用这个智能体。</p><Button onClick={() => navigate('access')}>开始使用</Button></div>)}
</div>
<div hidden={tab !== 'access'} className="agent-management-view">
{dirty && <div className="mb-4 space-y-3 text-sm"><p>先保存配置,再开始使用。</p><Button disabled={!canSave} onClick={() => void save()}>保存修改</Button></div>}
<fieldset disabled={dirty || busy || remote !== null}>
{visited.has('access') && <CloudAccessPanel activeView={tab === 'access'} slug={saved.slug} revision={saved.draft_revision} publication={publication} onChanged={result => { setSaved(result); setDraft(current => !hasDraftChanges(current, saved) ? result : { ...current, draft_revision: result.draft_revision, published_version: result.published_version }); onSaved(result); }} />}
</fieldset>
: <div className="m-auto space-y-4 text-center text-sm"><p>保存配置后,开始使用这个智能体。</p><Button disabled={busy || remote !== null || !publication.access} onClick={startUsing}>{publication.pending ? '重试开始使用' : '开始使用'}</Button></div>)}
</div>
<Dialog open={editingAvatar} onOpenChange={open => { if (!uploading) setEditingAvatar(open); }}>
<DialogContent><DialogTitle>修改头像</DialogTitle><DialogDescription>选择这个智能体的头像。</DialogDescription>
+3 -9
View File
@@ -4,12 +4,11 @@
.agent-editor-tabs [role="tab"][data-state="active"] { color: #b73d16; box-shadow: 0 2px 8px #2732460a; }
.agent-editor-scroll { padding: 28px 32px 40px; background: #fffdf9; color: #273246; }
.agent-config-stack { display: flex; flex-direction: column; gap: 22px; min-width: 0; }
.agent-config-card, .agent-config-budget > section { min-width: 0; margin: 0; padding: 24px; border: 1px solid #dce5f3; border-radius: 24px; background: #f1f6ff; }
.agent-config-card { min-width: 0; margin: 0; padding: 24px; border: 1px solid #dce5f3; border-radius: 24px; background: #f1f6ff; }
.agent-config-card[data-tone="lilac"] { border-color: #e5dcf5; background: #f6f2ff; }
.agent-config-card[data-tone="peach"] { border-color: #f0e2ce; background: #fff5e9; }
.agent-config-card[data-tone="mint"] { border-color: #dce9e1; background: #f0f7f3; }
.agent-config-card[data-tone="neutral"] { border-color: #e6e3de; background: #fff; }
.agent-config-budget > section { border-color: #e6e3de; background: #fff; }
.agent-config-heading { display: flex; min-width: 0; align-items: center; gap: 13px; margin-bottom: 20px; }
.agent-config-icon { display: grid; width: 44px; height: 44px; flex-shrink: 0; place-items: center; border: 2px solid #fff; border-radius: 15px; background: #e0eafb; color: #4a6e9c; }
.agent-config-icon svg { width: 23px; height: 23px; stroke-width: 1.8; }
@@ -21,7 +20,7 @@
.agent-config-heading h2 { font-size: 18px; font-weight: 650; line-height: 1.5; }
.agent-config-note { padding: 3px 9px; border-radius: 20px; background: #fff9; font-size: 12px; line-height: 1.5; color: #62677b; white-space: nowrap; }
.agent-config-description { margin-top: 5px; font-size: 14px; line-height: 1.8; color: #59657a; }
.agent-config-card input:not([type="checkbox"]), .agent-config-card textarea, .agent-config-card select, .agent-config-budget input {
.agent-config-card input:not([type="checkbox"]), .agent-config-card textarea, .agent-config-card select {
width: 100%; min-width: 0; min-height: 48px; padding: 12px 15px; border: 1px solid #cdd5e3; border-radius: 13px; background: #fff; color: #273246; font-size: 15px; box-shadow: 0 2px 2px #27324603;
}
.agent-config-card input::placeholder, .agent-config-card textarea::placeholder { color: #6c7588; }
@@ -42,16 +41,11 @@
.agent-resource-preload { display: flex; min-height: 44px; align-items: center; gap: 9px; margin-top: 12px; cursor: pointer; font-size: 13px; }
.agent-config-resource-manager { margin-top: 18px; }
.agent-config-resource-manager > details { border-radius: 14px; background: #ffffffa8; }
.agent-config-stack > .agent-config-resource-manager { margin-top: 0; }
.agent-config-limit-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr)); gap: 18px; margin-top: 20px; }
.agent-config-budget h2 { font-size: 18px; font-weight: 650; }
.agent-config-budget form > label { min-width: 0; flex: 1 1 240px; }
.agent-config-budget form > button { border-radius: 12px; }
@media (max-width: 640px) {
.agent-editor-tabs { padding: 10px 12px; }
.agent-editor-tabs [role="tab"] { gap: 6px; padding-inline: 8px; font-size: 14px; }
.agent-editor-scroll { padding: 18px 12px 28px; }
.agent-config-card, .agent-config-budget > section { padding: 18px; border-radius: 20px; }
.agent-config-card { padding: 18px; border-radius: 20px; }
.agent-config-stack { gap: 16px; }
.agent-config-heading { gap: 10px; }
.agent-config-heading h2 { font-size: 17px; }
@@ -187,20 +187,21 @@ for (const width of [375, 768, 1024, 1440]) {
await expect(tabs.getByRole('tab', { name: '高级设置', exact: true })).toHaveAttribute('aria-selected', 'true');
await expect(advanced.getByRole('navigation')).toHaveCount(0);
await expect(advanced.getByRole('tablist')).toHaveCount(0);
const headings = ['模型', '工具箱(Tool)', '做事手册(Skill)', 'MCP 服务', '子智能体', '知识库', '费用上限', '运行限制'];
const headings = ['模型', '工具箱(Tool)', '做事手册(Skill)', 'MCP 服务', '子智能体', '知识库'];
for (const name of headings) await expect(advanced.getByRole('heading', { name, exact: true })).toBeVisible();
await expect(advanced.getByRole('heading').first()).toHaveText('模型');
await expect(advanced.getByLabel('模型', { exact: true })).toHaveAttribute('required', '');
await expect(advanced.getByText('选它做事时能用的工具。', { exact: true })).toBeVisible();
await expect(advanced.getByText('给它一本手册,照着方法做。', { exact: true })).toBeVisible();
for (const copy of ['选它做事时能用的工具。', '给它一本手册,照着方法做。', '管理我的云端能力']) {
await expect(advanced.getByText(copy, { exact: true })).toHaveCount(0);
}
for (const name of ['费用上限', '运行限制']) await expect(advanced.getByRole('heading', { name, exact: true })).toHaveCount(0);
await expect(advanced.getByLabel('模型', { exact: true })).toBeInViewport();
await expectConfigurationBounds(page);
if (width === 375 || width === 1440) {
await page.screenshot({ path: test.info().outputPath(`agent-advanced-upper-${width}.png`), animations: 'disabled' });
}
await advanced.getByLabel('单次运行时限(秒)', { exact: true }).scrollIntoViewIfNeeded();
await expect(advanced.getByLabel('单次运行时限(秒)', { exact: true })).toBeInViewport();
await expect(advanced.getByLabel('每日上限(词元点数)', { exact: true })).toBeVisible();
await advanced.getByText('管理我的知识文档', { exact: true }).scrollIntoViewIfNeeded();
await expect(advanced.getByText('管理我的知识文档', { exact: true })).toBeInViewport();
await expectConfigurationBounds(page);
if (width === 375 || width === 1440) {
await page.screenshot({ path: test.info().outputPath(`agent-advanced-lower-${width}.png`), animations: 'disabled' });
@@ -213,7 +214,7 @@ for (const width of [375, 768, 1024, 1440]) {
expect(calls.filter(call => call.method === 'PATCH' || call.path === '/api/cloud-agents/avatars/preset'
|| (call.method === 'POST' && call.path === '/api/cloud-agents/agents'))).toEqual([]);
expect(calls.filter(call => call.path === '/api/cloud-agents/actions')
.every(call => ['catalog', 'resources', 'knowledge', 'budget', 'access'].includes(String(call.input?.operation)))).toBe(true);
.every(call => ['catalog', 'knowledge', 'access'].includes(String(call.input?.operation)))).toBe(true);
});
}
+6 -5
View File
@@ -174,11 +174,12 @@ test('personal cloud Agent identity, configuration and separate published chat i
await expect(page.getByRole('button', { name: '确认开始使用', exact: true })).toHaveCount(0);
expect((await fixtureState()).operations).not.toContain('publish');
await page.getByLabel('模型', { exact: true }).selectOption('reasoning-model');
await expect(page.getByText('选它做事时能用的工具。', { exact: true })).toBeVisible();
await expect(page.getByText('给它一本手册,照着方法做。', { exact: true })).toBeVisible();
await expect(page.getByText('选它做事时能用的工具。', { exact: true })).toHaveCount(0);
await expect(page.getByText('给它一本手册,照着方法做。', { exact: true })).toHaveCount(0);
await expect(page.getByText('管理我的云端能力', { exact: true })).toHaveCount(0);
await expect(page.getByRole('group', { name: /^知识库/ })).toBeVisible();
await expect(page.getByRole('heading', { name: '费用上限', exact: true })).toBeVisible();
await expect(page.getByRole('heading', { name: '运行限制', exact: true })).toBeVisible();
await expect(page.getByRole('heading', { name: '费用上限', exact: true })).toHaveCount(0);
await expect(page.getByRole('heading', { name: '运行限制', exact: true })).toHaveCount(0);
await page.getByRole('button', { name: '选择工具箱', exact: true }).click();
await page.getByRole('checkbox', { name: '资料整理工具', exact: true }).check();
await instructionsTab.click();
@@ -225,7 +226,7 @@ test('personal cloud Agent identity, configuration and separate published chat i
await page.screenshot({ path: testInfo.outputPath('cloud-agent-introduction-prompt.png') });
expect(await messageOperations()).toEqual([]);
await page.getByRole('button', { name: '开始使用', exact: true }).click();
await page.getByRole('button', { name: '确认开始使用', exact: true }).click();
await expect(page.getByRole('button', { name: '确认开始使用', exact: true })).toHaveCount(0);
const formalChat = page.getByRole('region', { name: '智能体对话', exact: true });
await expect(formalChat).toBeVisible();
expect((await fixtureState()).operations.filter((operation: string) => operation === 'publish')).toHaveLength(1);
+236
View File
@@ -0,0 +1,236 @@
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { createMemoryRouter, RouterProvider } from 'react-router-dom';
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
import CloudAgents from '@/pages/CloudAgents';
import { EMPTY_CLOUD_CONFIGURATION } from '../../shared/cloud-agents';
const api = vi.hoisted(() => ({ list: vi.fn(), create: vi.fn(), get: vi.fn(), save: vi.fn(), call: vi.fn(), events: vi.fn(), recovery: vi.fn(), remember: vi.fn() }));
const auth = vi.hoisted(() => ({ user: { userId: 'creator' } }));
vi.mock('@/lib/cloud-agents-api', () => ({ cloudAgentsApi: api }));
vi.mock('@/stores/auth', () => ({ useAuthStore: (selector: (state: typeof auth) => unknown) => selector(auth) }));
const draft = {
slug: 'ml-' + 'a'.repeat(32), name: '写作搭档', purpose: '', system_prompt: '原始指令',
draft_revision: 3, updated_at: '2026-09-10T00:00:00Z',
configuration: { ...EMPTY_CLOUD_CONFIGURATION, model: 'model-a' }, published_version: null, enabled: true,
};
function open() {
const router = createMemoryRouter([
{ path: '/cloud-agents', element: <CloudAgents /> },
{ path: '/module-select', element: <div>模块首页</div> },
], { initialEntries: ['/cloud-agents'] });
return { router, ...render(<RouterProvider router={router} />) };
}
async function openConfiguration() {
const view = open();
fireEvent.click(await screen.findByRole('button', { name: '继续制作', exact: true }));
await waitFor(() => expect(screen.getByRole('button', { name: '开始使用', exact: true })).toBeEnabled());
return view;
}
const publications = () => api.call.mock.calls.filter(([operation]) => operation === 'publish');
const messageOperations = () => api.call.mock.calls.filter(([operation]) => ['preview', 'submit', 'resume'].includes(operation));
beforeEach(() => {
vi.resetAllMocks(); auth.user.userId = 'creator';
api.list.mockResolvedValue({ agents: [draft], next_cursor: null });
api.get.mockResolvedValue(draft);
api.recovery.mockResolvedValue({ recent: null, pending: [] });
api.remember.mockResolvedValue({ saved: true });
api.save.mockImplementation(async (_slug, input) => ({ ...draft, ...input, draft_revision: 4 }));
api.call.mockImplementation(async (operation, input) => {
if (operation === 'access') return { published_version: null, enabled: true, versions: [], grants: [], applications: [] };
if (operation === 'catalog') return { models: [{ id: 'model-a', name: '模型甲' }], resources: {}, pricing: null };
if (operation === 'knowledge') return { databases: [], models: [] };
if (operation === 'resources') return { mcps: [], skills: [], subagents: [] };
if (operation === 'threads') return { threads: [], next_offset: null };
if (operation === 'publish') return { version: 1, draft_revision: input.expected_revision };
return {};
});
});
afterEach(cleanup);
it('starts once from the saved configuration and opens conversation only after activation succeeds', async () => {
let finish!: (result: { version: number; draft_revision: number }) => void;
const base = api.call.getMockImplementation()!;
api.call.mockImplementation((operation, input) => operation === 'publish'
? new Promise(resolve => { finish = resolve; }) : base(operation, input));
const { router } = await openConfiguration();
expect(publications()).toHaveLength(0);
const start = screen.getByRole('button', { name: '开始使用', exact: true });
fireEvent.click(start); fireEvent.click(start);
expect(publications()).toHaveLength(1);
expect(publications()[0][1]).toMatchObject({ slug: draft.slug, expected_revision: 3, operation_id: expect.any(String) });
expect(screen.getByRole('button', { name: '正在开始…' })).toBeDisabled();
expect(screen.queryByRole('region', { name: '智能体对话' })).not.toBeInTheDocument();
expect(screen.queryByText('准备好使用自己的助手了吗?')).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: '确认开始使用' })).not.toBeInTheDocument();
expect(screen.getByLabelText('提示词')).toHaveValue('原始指令');
await act(async () => { await router.navigate('/module-select'); });
expect(screen.getByRole('dialog', { name: '未保存的修改' })).toBeVisible();
fireEvent.click(screen.getByRole('button', { name: '继续编辑' }));
await act(async () => finish({ version: 1, draft_revision: 3 }));
expect(await screen.findByRole('region', { name: '智能体对话' })).toBeVisible();
expect(screen.getByRole('button', { name: '调整智能体', exact: true })).toBeVisible();
expect(api.save).not.toHaveBeenCalled();
expect(messageOperations()).toHaveLength(0);
});
it('keeps unsaved input in configuration until the user saves, then starts the saved revision directly', async () => {
await openConfiguration();
fireEvent.change(screen.getByLabelText('提示词'), { target: { value: '我的新要求' } });
fireEvent.click(screen.getByRole('button', { name: '开始使用', exact: true }));
expect(screen.getByRole('alert')).toHaveTextContent('请先保存草稿,再开始使用。');
expect(screen.getByLabelText('提示词')).toBeVisible();
expect(screen.getByLabelText('提示词')).toHaveValue('我的新要求');
expect(publications()).toHaveLength(0);
expect(api.save).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole('button', { name: '保存草稿', exact: true }));
await waitFor(() => expect(screen.getByRole('button', { name: '已保存', exact: true })).toBeDisabled());
fireEvent.click(screen.getByRole('button', { name: '开始使用', exact: true }));
expect(await screen.findByRole('region', { name: '智能体对话' })).toBeVisible();
expect(publications()[0][1]).toMatchObject({ expected_revision: 4 });
expect(messageOperations()).toHaveLength(0);
});
it('returns to the model choice without activating a draft that has no model', async () => {
api.list.mockResolvedValue({ agents: [{ ...draft, configuration: EMPTY_CLOUD_CONFIGURATION }], next_cursor: null });
api.get.mockResolvedValue({ ...draft, configuration: EMPTY_CLOUD_CONFIGURATION });
await openConfiguration();
fireEvent.click(screen.getByRole('button', { name: '开始使用', exact: true }));
expect(screen.getByRole('tab', { name: '高级设置' })).toHaveAttribute('aria-selected', 'true');
expect(screen.getByLabelText('模型')).toBeVisible();
expect(screen.getByRole('alert')).toHaveTextContent('请先在高级设置中选择模型。');
expect(publications()).toHaveLength(0);
expect(screen.queryByRole('region', { name: '智能体对话' })).not.toBeInTheDocument();
});
it('does not activate while a cloud save conflict needs to be resolved', async () => {
await openConfiguration();
api.save.mockRejectedValue(new Error('保存冲突'));
api.get.mockResolvedValue({ ...draft, draft_revision: 4, system_prompt: '云端修改' });
fireEvent.change(screen.getByLabelText('提示词'), { target: { value: '保留本地修改' } });
fireEvent.click(screen.getByRole('button', { name: '保存草稿', exact: true }));
await screen.findByText('云端已有修订 4,本地输入仍保留');
const start = screen.getByRole('button', { name: '开始使用', exact: true });
expect(start).toBeDisabled();
fireEvent.click(start);
expect(publications()).toHaveLength(0);
expect(screen.getByLabelText('提示词')).toHaveValue('保留本地修改');
});
it('waits for a readable activation status and permits a read retry before starting', async () => {
const base = api.call.getMockImplementation()!;
let unavailable = true;
api.call.mockImplementation(async (operation, input) => {
if (operation === 'access' && unavailable) throw new Error('状态读取失败');
return base(operation, input);
});
open();
fireEvent.click(await screen.findByRole('button', { name: '继续制作', exact: true }));
await screen.findByText(/状态读取失败/);
expect(screen.getByRole('button', { name: '开始使用', exact: true })).toBeDisabled();
expect(publications()).toHaveLength(0);
unavailable = false;
fireEvent.click(screen.getByRole('button', { name: '刷新应用状态', exact: true }));
await waitFor(() => expect(screen.getByRole('button', { name: '开始使用', exact: true })).toBeEnabled());
expect(publications()).toHaveLength(0);
fireEvent.click(screen.getByRole('button', { name: '开始使用', exact: true }));
expect(await screen.findByRole('region', { name: '智能体对话' })).toBeVisible();
expect(publications()).toHaveLength(1);
expect(messageOperations()).toHaveLength(0);
});
it('retries an uncertain activation with the original operation and keeps pending-leave protection', async () => {
const base = api.call.getMockImplementation()!;
let attempts = 0;
api.call.mockImplementation(async (operation, input) => {
if (operation === 'publish' && ++attempts === 1) throw new Error('连接中断');
return base(operation, input);
});
await openConfiguration();
fireEvent.click(screen.getByRole('button', { name: '开始使用', exact: true }));
await screen.findByText(/连接中断/);
expect(screen.queryByRole('region', { name: '智能体对话' })).not.toBeInTheDocument();
const unload = new Event('beforeunload', { cancelable: true });
window.dispatchEvent(unload);
expect(unload.defaultPrevented).toBe(true);
fireEvent.click(screen.getByRole('button', { name: '重试开始使用', exact: true }));
expect(await screen.findByRole('region', { name: '智能体对话' })).toBeVisible();
expect(publications()).toHaveLength(2);
expect(publications()[1][1]).toEqual(publications()[0][1]);
expect(messageOperations()).toHaveLength(0);
});
it('keeps a newer saved revision unapplied when retry resolves the original uncertain activation', async () => {
const base = api.call.getMockImplementation()!;
let attempts = 0;
let activated = false;
api.call.mockImplementation(async (operation, input) => {
if (operation === 'access') return {
published_version: activated ? 1 : null, enabled: true, grants: [], applications: [],
versions: activated ? [{ version: 1, draft_revision: 3, created_at: '' }] : [],
};
if (operation === 'publish') {
if (++attempts === 1) throw new Error('连接中断');
activated = true;
return { version: 1, draft_revision: input.expected_revision };
}
return base(operation, input);
});
await openConfiguration();
fireEvent.click(screen.getByRole('button', { name: '开始使用', exact: true }));
await screen.findByText(/连接中断/);
fireEvent.change(screen.getByLabelText('提示词'), { target: { value: '这次新保存的要求' } });
fireEvent.click(screen.getByRole('button', { name: '保存草稿', exact: true }));
await waitFor(() => expect(screen.getByRole('button', { name: '已保存', exact: true })).toBeDisabled());
fireEvent.click(screen.getByRole('button', { name: '重试开始使用', exact: true }));
expect(await screen.findByRole('region', { name: '智能体对话' })).toBeVisible();
await screen.findByText('修改已保存,尚未应用');
expect(screen.getByText('这次修改还没有应用到正在使用的助手')).toBeVisible();
expect(screen.getByRole('button', { name: '应用最新修改', exact: true })).toBeEnabled();
expect(screen.getByLabelText('提示词')).toHaveValue('这次新保存的要求');
expect(api.save).toHaveBeenCalledWith(draft.slug, expect.objectContaining({ expected_revision: 3, system_prompt: '这次新保存的要求' }));
expect(publications()).toHaveLength(2);
expect(publications()[1][1]).toEqual(publications()[0][1]);
expect(publications()[1][1]).toMatchObject({ expected_revision: 3 });
expect(messageOperations()).toHaveLength(0);
});
it('keeps successful activation and conversation when the following status read fails', async () => {
const base = api.call.getMockImplementation()!;
let activated = false;
api.call.mockImplementation(async (operation, input) => {
if (operation === 'access' && activated) throw new Error('状态刷新失败');
const result = await base(operation, input);
if (operation === 'publish') activated = true;
return result;
});
await openConfiguration();
fireEvent.click(screen.getByRole('button', { name: '开始使用', exact: true }));
expect(await screen.findByRole('region', { name: '智能体对话' })).toBeVisible();
await screen.findByText(/状态刷新失败/);
expect(screen.getByRole('button', { name: '已应用', exact: true })).toBeDisabled();
fireEvent.click(screen.getByRole('button', { name: '刷新应用状态', exact: true }));
await waitFor(() => expect(api.call.mock.calls.filter(([operation]) => operation === 'access')).toHaveLength(3));
expect(publications()).toHaveLength(1);
expect(messageOperations()).toHaveLength(0);
});
it('does not carry a late activation result into another signed-in account', async () => {
let finish!: (result: { version: number; draft_revision: number }) => void;
const base = api.call.getMockImplementation()!;
api.call.mockImplementation((operation, input) => operation === 'publish'
? new Promise(resolve => { finish = resolve; }) : base(operation, input));
const view = await openConfiguration();
fireEvent.click(screen.getByRole('button', { name: '开始使用', exact: true }));
auth.user.userId = 'other';
api.list.mockResolvedValue({ agents: [], next_cursor: null });
view.rerender(<RouterProvider key="other-account" router={view.router} />);
await screen.findByText('做一个属于你的 AI 助手吧');
await act(async () => finish({ version: 1, draft_revision: 3 }));
expect(screen.queryByTestId('cloud-agent-editor')).not.toBeInTheDocument();
expect(screen.queryByRole('region', { name: '智能体对话' })).not.toBeInTheDocument();
expect(publications()).toHaveLength(1);
expect(messageOperations()).toHaveLength(0);
});
+36 -34
View File
@@ -425,6 +425,7 @@ it('requires an explicit advanced model choice before starting use but permits a
api.call.mockImplementation(async (operation, input) => {
if (operation === 'catalog') return { models: [{ id: 'model-a', name: '模型甲' }], resources: {}, pricing: null };
if (operation === 'access') return { published_version: null, enabled: true, versions: [], grants: [], applications: [] };
if (operation === 'publish') return { version: 1, draft_revision: input.expected_revision };
return base(operation, input);
});
api.save.mockImplementation(async (_slug, input) => (current = { ...current, ...input, draft_revision: current.draft_revision + 1 }));
@@ -450,11 +451,13 @@ it('requires an explicit advanced model choice before starting use but permits a
fireEvent.click(screen.getByRole('button', { name: '保存草稿' }));
await screen.findByRole('button', { name: '已保存' });
fireEvent.click(screen.getByRole('button', { name: '开始使用', exact: true }));
expect(await screen.findByRole('button', { name: '确认开始使用' })).toBeEnabled();
expect(api.call.mock.calls.some(([operation]) => ['publish', 'preview', 'submit'].includes(operation))).toBe(false);
expect(await screen.findByRole('region', { name: '智能体对话' })).toBeVisible();
expect(screen.queryByRole('button', { name: '确认开始使用' })).not.toBeInTheDocument();
expect(api.call.mock.calls.filter(([operation]) => operation === 'publish')).toHaveLength(1);
expect(api.call.mock.calls.some(([operation]) => ['preview', 'submit'].includes(operation))).toBe(false);
});
it('shows all advanced settings together and preserves unfinished selections, search and limits when switching pages', async () => {
it('shows only model and capability settings together and preserves unfinished selections and search when switching pages', async () => {
const base = api.call.getMockImplementation()!;
api.call.mockImplementation(async (operation, input) => {
if (operation === 'catalog') return {
@@ -479,11 +482,13 @@ it('shows all advanced settings together and preserves unfinished selections, se
expect(toolbox).toBeVisible();
expect(handbook).toBeVisible();
expect(model.compareDocumentPosition(toolbox) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(screen.getByText('选它做事时能用的工具。', { exact: true })).toBeVisible();
expect(screen.getByText('给它一本手册,照着方法做。', { exact: true })).toBeVisible();
expect(screen.queryByText('选它做事时能用的工具。', { exact: true })).not.toBeInTheDocument();
expect(screen.queryByText('给它一本手册,照着方法做。', { exact: true })).not.toBeInTheDocument();
expect(screen.getByRole('group', { name: /^知识库/ })).toBeVisible();
expect(screen.getByRole('heading', { name: '费用上限', exact: true })).toBeVisible();
expect(screen.getByRole('heading', { name: '运行限制', exact: true })).toBeVisible();
expect(screen.queryByText('管理我的云端能力', { exact: true })).not.toBeInTheDocument();
expect(screen.queryByRole('heading', { name: '费用上限', exact: true })).not.toBeInTheDocument();
expect(screen.queryByRole('heading', { name: '运行限制', exact: true })).not.toBeInTheDocument();
expect(screen.queryByLabelText('工具审批')).not.toBeInTheDocument();
expect(screen.queryByRole('navigation', { name: '配置分类' })).not.toBeInTheDocument();
expect(screen.getAllByRole('tab').map(tab => tab.textContent)).toEqual(['介绍与提示词', '高级设置']);
@@ -494,18 +499,12 @@ it('shows all advanced settings together and preserves unfinished selections, se
const search = screen.getByLabelText('搜索工具箱');
fireEvent.change(search, { target: { value: '文章' } });
fireEvent.click(screen.getByRole('checkbox', { name: '文章整理工具', exact: true }));
const steps = screen.getByLabelText('最多执行步数');
fireEvent.change(steps, { target: { value: '70' } });
const budget = screen.getByLabelText('每次任务上限(词元点数)');
await waitFor(() => expect(budget).toBeEnabled());
fireEvent.change(budget, { target: { value: '2.50' } });
fireEvent.mouseDown(screen.getByRole('tab', { name: '介绍与提示词' }), { button: 0, ctrlKey: false });
const prompt = ' 保留我的原始写作要求。\n一次讨论一个想法。\n';
fireEvent.change(screen.getByLabelText('提示词'), { target: { value: prompt } });
expect(model).not.toBeVisible();
expect(search).not.toBeVisible();
expect(budget).not.toBeVisible();
fireEvent.mouseDown(screen.getByRole('tab', { name: '高级设置' }), { button: 0, ctrlKey: false });
expect(screen.getByLabelText('模型')).toBe(model);
@@ -514,15 +513,12 @@ it('shows all advanced settings together and preserves unfinished selections, se
expect(search).toHaveValue('文章');
expect(screen.getByRole('checkbox', { name: '文章整理工具', exact: true })).toBeChecked();
expect(screen.getByRole('checkbox', { name: '写作方法', exact: true })).toBeChecked();
expect(steps).toHaveValue(70);
expect(budget).toHaveValue(2.5);
expect(screen.getByRole('button', { name: '保存费用上限', exact: true })).toBeEnabled();
expect(screen.getByLabelText('提示词')).toHaveValue(prompt);
const unload = new Event('beforeunload', { cancelable: true });
window.dispatchEvent(unload);
expect(unload.defaultPrevented).toBe(true);
expect(api.save).not.toHaveBeenCalled();
expect(api.call.mock.calls.some(([operation]) => ['saveBudget', 'publish', 'preview', 'submit'].includes(operation))).toBe(false);
expect(api.call.mock.calls.some(([operation]) => ['budget', 'saveBudget', 'resources', 'publish', 'preview', 'submit'].includes(operation))).toBe(false);
});
it('retains requirements when model loading fails and offers a usable retry', async () => {
@@ -719,7 +715,7 @@ it('saves an existing configured draft without creating or loading a configurati
expect(api.call.mock.calls.some(([operation]) => ['threads', 'history', 'preview', 'submit', 'publish'].includes(operation))).toBe(false);
});
it('switches editor pages while retaining unfinished knowledge and budget form inputs', async () => {
it('switches editor pages while retaining unfinished knowledge form inputs', async () => {
api.list.mockResolvedValue({ agents: [{ ...draft, configuration: { ...EMPTY_CLOUD_CONFIGURATION, model: 'test-model' } }], next_cursor: null });
open();
fireEvent.click(await screen.findByRole('button', { name: '继续制作', exact: true }));
@@ -744,22 +740,16 @@ it('switches editor pages while retaining unfinished knowledge and budget form i
fireEvent.click(screen.getByRole('button', { name: '新建知识库' }));
const knowledgeName = screen.getByLabelText('知识库名称');
fireEvent.change(knowledgeName, { target: { value: '还没创建的资料' } });
const budgetInput = screen.getByLabelText('每日上限(词元点数)');
await waitFor(() => expect(budgetInput).toBeEnabled());
fireEvent.change(budgetInput, { target: { value: '12.50' } });
fireEvent.mouseDown(advanced, { button: 0, ctrlKey: false });
expect(knowledgeName).toBeVisible();
expect(budgetInput).toBeVisible();
fireEvent.mouseDown(requirements, { button: 0, ctrlKey: false });
expect(screen.queryByRole('navigation', { name: '配置分类' })).not.toBeInTheDocument();
expect(budgetInput).not.toBeVisible();
expect(knowledgeName).not.toBeVisible();
expect(screen.getByLabelText('提示词')).toBe(instructions);
expect(instructions).toHaveValue('没有保存的要求');
fireEvent.mouseDown(advanced, { button: 0, ctrlKey: false });
expect(screen.queryByRole('navigation', { name: '配置分类' })).not.toBeInTheDocument();
expect(screen.getByLabelText('每日上限(词元点数)')).toBe(budgetInput);
expect(budgetInput).toHaveValue(12.5);
expect(screen.getByLabelText('知识库名称')).toBe(knowledgeName);
expect(knowledgeName).toHaveValue('还没创建的资料');
expect(screen.queryByLabelText('消息')).not.toBeInTheDocument();
@@ -767,27 +757,39 @@ it('switches editor pages while retaining unfinished knowledge and budget form i
expect(api.call.mock.calls.some(([operation]) => ['saveBudget', 'preview', 'submit'].includes(operation))).toBe(false);
});
it('retains unsaved instructions while saving a budget independently of the draft', async () => {
const configured = { ...draft, configuration: { ...EMPTY_CLOUD_CONFIGURATION, model: 'test-model' } };
it('preserves hidden runtime policies while saving model, prompt and capability changes without touching the budget', async () => {
const configuration = { ...EMPTY_CLOUD_CONFIGURATION, model: 'test-model',
tool_approval_mode: 'always_trust' as const, max_execution_steps: 70, max_output_tokens: 7200, max_run_seconds: 180 };
const configured = { ...draft, configuration };
api.list.mockResolvedValue({ agents: [configured], next_cursor: null });
const base = api.call.getMockImplementation()!;
api.call.mockImplementation(async (operation, input) => operation === 'saveBudget' ? { ...input, daily_committed_points: '0.00' } : base(operation, input));
api.call.mockImplementation(async (operation, input) => operation === 'catalog' ? {
models: [{ id: 'test-model', name: '原模型' }, { id: 'another-model', name: '新模型' }],
resources: { tools: [{ key: 'writing-tool', name: '文章整理工具' }] }, pricing: null,
} : base(operation, input));
api.save.mockImplementation(async (_slug, input) => ({ ...configured, ...input, draft_revision: 2 }));
open();
fireEvent.click(await screen.findByRole('button', { name: '继续制作', exact: true }));
fireEvent.change(screen.getByLabelText('提示词'), { target: { value: '未保存的指令' } });
expect(screen.queryByLabelText('消息')).not.toBeInTheDocument();
expect(screen.getByLabelText('提示词')).toHaveValue('未保存的指令');
fireEvent.mouseDown(screen.getByRole('tab', { name: '高级设置' }), { button: 0, ctrlKey: false });
const budget = screen.getByLabelText('每日上限(词元点数)');
await waitFor(() => expect(budget).toBeEnabled());
fireEvent.change(budget, { target: { value: '12' } });
fireEvent.click(screen.getByRole('button', { name: '保存费用上限' }));
await waitFor(() => expect(api.call).toHaveBeenCalledWith('saveBudget', expect.objectContaining({ daily_limit_points: '12' })));
await waitFor(() => expect(screen.getByLabelText('模型')).toBeEnabled());
fireEvent.change(screen.getByLabelText('模型'), { target: { value: 'another-model' } });
fireEvent.click(screen.getByRole('button', { name: '选择工具箱', exact: true }));
fireEvent.click(screen.getByRole('checkbox', { name: '文章整理工具', exact: true }));
expect(screen.queryByLabelText('最多执行步数')).not.toBeInTheDocument();
expect(screen.queryByLabelText('每日上限(词元点数)')).not.toBeInTheDocument();
expect(api.save).not.toHaveBeenCalled();
fireEvent.mouseDown(screen.getByRole('tab', { name: '介绍与提示词', exact: true }), { button: 0, ctrlKey: false });
expect(screen.getByLabelText('提示词')).toHaveValue('未保存的指令');
expect(screen.getByRole('button', { name: '保存草稿' })).toBeEnabled();
expect(api.call.mock.calls.some(([operation]) => ['threads', 'preview', 'submit'].includes(operation))).toBe(false);
fireEvent.click(screen.getByRole('button', { name: '保存草稿' }));
await screen.findByRole('button', { name: '已保存' });
expect(api.save).toHaveBeenCalledWith(draft.slug, expect.objectContaining({
system_prompt: '未保存的指令', configuration: { ...configuration, model: 'another-model', tools: ['writing-tool'] },
}));
expect(api.call.mock.calls.some(([operation]) => ['budget', 'saveBudget', 'resources', 'threads', 'preview', 'submit'].includes(operation))).toBe(false);
});
it('saves a cleared model selection without choosing another model or starting a preview', async () => {