Simplify Agent settings and start conversations directly
This commit is contained in:
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.
|
||||
@@ -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 只负责其他应计费模型调用的创建者词元点数账本。客户端区分目录加载中、加载失败与无可用模型,空状态提供管理员配置说明;「刷新知识库与模型」保留未提交名称,未确认的创建请求沿用原操作身份。
|
||||
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in new issue
Block a user