diff --git a/.project-docs/30-worklog/tasks/20261012-agent-config-simplify-c6829a4e.md b/.project-docs/30-worklog/tasks/20261012-agent-config-simplify-c6829a4e.md
new file mode 100644
index 00000000..ec08682e
--- /dev/null
+++ b/.project-docs/30-worklog/tasks/20261012-agent-config-simplify-c6829a4e.md
@@ -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.
diff --git a/README.md b/README.md
index bdfeef1c..740ed14d 100644
--- a/README.md
+++ b/README.md
@@ -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 只负责其他应计费模型调用的创建者词元点数账本。客户端区分目录加载中、加载失败与无可用模型,空状态提供管理员配置说明;「刷新知识库与模型」保留未提交名称,未确认的创建请求沿用原操作身份。
diff --git a/src/pages/CloudAgents/ConfigurationFields.tsx b/src/pages/CloudAgents/ConfigurationFields.tsx
index f9b2e5e9..e26bca41 100644
--- a/src/pages/CloudAgents/ConfigurationFields.tsx
+++ b/src/pages/CloudAgents/ConfigurationFields.tsx
@@ -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
{title}
-
+
{ setChoosing(choosing === key ? null : key); setSearch(''); }}>{choosing === key ? '完成选择' : '选择' + label}
{choosing === key && 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); }} />}
;
})}
-
-
未选择的资源不会开放给此智能体。通过微信或 API 使用时也遵循本配置。
-
setRefresh(v => v + 1)} />
-
-
-
-
- 以下设置随草稿保存;费用上限独立保存,对后续调用生效。执行步数会统计模型调用、工具、图节点和中间件步骤。
- 工具审批
- change('tool_approval_mode', e.target.value === 'always_trust' ? 'always_trust' : 'default')}>
- 按工具要求确认 信任已选工具,自动执行
-
-
- {([{ 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 =>
- {item.label}
- change(item.key, Number(e.target.value))} /> )}
-
-
+ 未选择的资源不会开放给此智能体。通过微信或 API 使用时也遵循本配置。
;
}
diff --git a/src/pages/CloudAgents/DraftEditor.tsx b/src/pages/CloudAgents/DraftEditor.tsx
index ca68ad45..93ed47f7 100644
--- a/src/pages/CloudAgents/DraftEditor.tsx
+++ b/src/pages/CloudAgents/DraftEditor.tsx
@@ -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 && }
{publication.busy ? '正在应用…' : publication.pending ? '重试本次应用' : !dirty && publication.appliedRevision === saved.draft_revision ? '已应用' : '应用最新修改'}
}
- {!saved.published_version && {
- if (!draft.configuration.model) {
- setEditorTab('advanced');
- if (configurationScroll.current) configurationScroll.current.scrollTop = 0;
- setError('请先在高级设置中选择模型。'); return;
- }
- setError(''); navigate('access');
- }}>开始使用 }
+ {!saved.published_version &&
+ {publication.busy && }
+ {publication.busy ? '正在开始…' : publication.pending ? '重试开始使用' : '开始使用'}
+ }
{tab !== 'configuration' && {
if (formalPending.hasPending) { setError('对话中还有未完成的内容,请先发送或处理后再调整智能体。'); return; }
@@ -156,7 +165,7 @@ export function DraftEditor({ initial, recent, onBack, onSaved, startingPrompt,
{dirty ? '修改尚未保存和应用' : '这次修改还没有应用到正在使用的助手'}
保存只更新草稿。请点击顶部“应用最新修改”,再到微信等渠道使用。已开始的任务保持原配置。
}
- {publication.error && tab !== 'access' && {publication.error} void publication.refresh()}>刷新应用状态
}
+ {publication.error && {publication.error} void publication.refresh()}>刷新应用状态
}
{error && {error}
}
{remote &&
云端已有修订 {remote.draft_revision},本地输入仍保留
@@ -175,7 +184,7 @@ export function DraftEditor({ initial, recent, onBack, onSaved, startingPrompt,
高级设置
- {/* 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. */}
@@ -202,13 +211,7 @@ export function DraftEditor({ initial, recent, onBack, onSaved, startingPrompt,
{wechatVisited && }
- : 保存配置后,开始使用这个智能体。
navigate('access')}>开始使用 )}
-
-
- {dirty &&
先保存配置,再开始使用。
void save()}>保存修改 }
-
- {visited.has('access') && { setSaved(result); setDraft(current => !hasDraftChanges(current, saved) ? result : { ...current, draft_revision: result.draft_revision, published_version: result.published_version }); onSaved(result); }} />}
-
+ :
保存配置后,开始使用这个智能体。
{publication.pending ? '重试开始使用' : '开始使用'} )}
{ if (!uploading) setEditingAvatar(open); }}>
修改头像 选择这个智能体的头像。
diff --git a/src/pages/CloudAgents/agent-config.css b/src/pages/CloudAgents/agent-config.css
index 7ea51271..7798a365 100644
--- a/src/pages/CloudAgents/agent-config.css
+++ b/src/pages/CloudAgents/agent-config.css
@@ -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; }
diff --git a/tests/e2e/cloud-agent-platform-avatars.spec.ts b/tests/e2e/cloud-agent-platform-avatars.spec.ts
index dd0e7614..af7c4593 100644
--- a/tests/e2e/cloud-agent-platform-avatars.spec.ts
+++ b/tests/e2e/cloud-agent-platform-avatars.spec.ts
@@ -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);
});
}
diff --git a/tests/e2e/cloud-agents.spec.ts b/tests/e2e/cloud-agents.spec.ts
index ad1464cb..24c208f7 100644
--- a/tests/e2e/cloud-agents.spec.ts
+++ b/tests/e2e/cloud-agents.spec.ts
@@ -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);
diff --git a/tests/unit/cloud-agent-start-use.test.tsx b/tests/unit/cloud-agent-start-use.test.tsx
new file mode 100644
index 00000000..429e6774
--- /dev/null
+++ b/tests/unit/cloud-agent-start-use.test.tsx
@@ -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: },
+ { path: '/module-select', element: 模块首页
},
+ ], { initialEntries: ['/cloud-agents'] });
+ return { router, ...render( ) };
+}
+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( );
+ 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);
+});
diff --git a/tests/unit/cloud-agents-page.test.tsx b/tests/unit/cloud-agents-page.test.tsx
index 036f0aad..90866b87 100644
--- a/tests/unit/cloud-agents-page.test.tsx
+++ b/tests/unit/cloud-agents-page.test.tsx
@@ -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 () => {