fix(云智能体): 修复微信失败运行状态投影并提高默认执行步数
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
# Task: Fix MakeLore failed run projection and agent default
|
||||
|
||||
## Identity
|
||||
|
||||
- Task ID: 20260914-makelore-ppt-failure-7c4a
|
||||
- Mode: Feature
|
||||
- Branch: codex/20260914-makelore-ppt-failure-7c4a-makelore-ppt-failure
|
||||
- Worktree: D:\Datas\OthersProjects\.codex-worktrees\makelore\20260914-makelore-ppt-failure-7c4a
|
||||
- Base commit: 9044f7dd8ce199ca57cbe0bc7be1abd1bca2a9bd
|
||||
- Owner: codex-ppt-failure
|
||||
- Status: Ready for Integration
|
||||
|
||||
## Scope
|
||||
|
||||
- Change the MakeLore cloud-agent client default `max_execution_steps` from 40 to 300 for newly materialized configuration defaults.
|
||||
- Project the existing authoritative WeChat conversation run status and error visibly in the selected conversation, including historical/closed sessions and queued-only state.
|
||||
- Add focused renderer/unit and consumer-seam Electron E2E regression coverage for failed runs and the required status labels.
|
||||
- Keep the implementation inside MakeLore; do not change Yuxi, AgentBus, Works Square, backend contracts, billing, privacy, retry/resume controls, or published version data.
|
||||
|
||||
## Intent And Constraints
|
||||
|
||||
- Confirmed facts: `shared/cloud-agents.ts` defines the fallback configuration with `max_execution_steps: 40`; `electron/services/cloud-agent-operations.ts` already projects `CloudRun.status` and bounded `error.type/message`; `CloudChannelConversations` currently prioritizes the closed-session copy over run status and does not render `run.error`.
|
||||
- Decision: use 300 only when the configuration field is absent. Preserve explicit saved 40/custom values and published versions because Main's configuration projection overlays present response fields.
|
||||
- Decision: keep partial assistant messages, but render a prominent authoritative run-status/error block before them so partial output cannot conceal a terminal failure.
|
||||
- Confirmed contract: the existing channel-conversation response includes `run.status`, `run.output`, `run.error.type/message`, and `queued_requests[].status`; Main's `projectConversation` preserves those bounded fields. Yuxi now maps the graph execution limit to `execution_step_limit` with a user-facing explanation, and its thread history already merges run/messages/queued requests. No MakeLore API change is required.
|
||||
- No deployment, push, or merge is part of this task; hold the reversible commit until the independent review completes. No paid retry, automatic resume, or new framework is included.
|
||||
|
||||
## Outcome
|
||||
|
||||
- Completed implementation in the MakeLore client. New configuration defaults materialize at 300 execution steps while explicit saved values remain unchanged. The selected WeChat conversation now shows the authoritative run status for active, terminal, closed, and queued-only states, and shows the authoritative error before retaining any partial assistant transcript. The settings help explains that execution steps count model calls, tools, graph nodes, and middleware steps. No billing, privacy, scanner identity, published-version, retry, or resume behavior changed.
|
||||
- Independent review completed with no product-executable issues and approval to commit. The reviewer confirmed the 300 default, preservation of explicit 40/custom limits and published versions, failed-run error with partial output, and queued-status precedence.
|
||||
|
||||
## Verification
|
||||
|
||||
- Before the fix, the component had no status/error regression assertion; closed sessions rendered only the historical-session copy and `run.error` was absent from the view. The 42 regression tests were run only against the fixed implementation; the old implementation was not executed to produce a red test. The baseline was verified from the pre-change source. The first focused test attempt was blocked by an uninstalled workspace (`vitest: command not found`), then `pnpm install --frozen-lockfile` restored the pinned dependencies.
|
||||
- Passing: `pnpm exec vitest run tests/unit/cloud-channel-conversations.test.tsx tests/unit/cloud-agents-main.test.ts --maxWorkers=1` (42 tests); `pnpm run typecheck`; `pnpm exec eslint shared/cloud-agents.ts src/pages/CloudAgents/CloudChannelConversations.tsx src/pages/CloudAgents/ConfigurationFields.tsx tests/unit/cloud-agents-main.test.ts tests/unit/cloud-channel-conversations.test.tsx tests/e2e/cloud-agent-channels.spec.ts`; `pnpm run lint:check`; `pnpm run build:vite`; `pnpm run test:e2e -- tests/e2e/cloud-agent-channels.spec.ts` (1 test). `git diff --check` also passed. Build output contained only existing Browserslist, chunk-size, and dynamic-import warnings.
|
||||
- Review limitation: the production WeChat E2E was not executed. The available Electron E2E and Main projection regression cover the client consumer seam; Yuxi's independent real-chain validation supplied the upstream contract confirmation.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- No implementation follow-up remains before integration. Keep the production WeChat E2E limitation visible when promoting this change.
|
||||
|
||||
## Promotion Candidates
|
||||
|
||||
- None. The default and status/error behavior are client changes covered by this task; no canonical project-memory or backend contract promotion is needed.
|
||||
|
||||
## Status
|
||||
|
||||
Ready for Integration
|
||||
@@ -48,7 +48,7 @@ export interface CloudAgentConfiguration {
|
||||
|
||||
export const EMPTY_CLOUD_CONFIGURATION: CloudAgentConfiguration = {
|
||||
model: '', tools: [], knowledges: [], mcps: [], skills: [], preload_skills: [], subagents: [],
|
||||
tool_approval_mode: 'default', max_execution_steps: 40, max_output_tokens: 4096, max_run_seconds: 600,
|
||||
tool_approval_mode: 'default', max_execution_steps: 300, max_output_tokens: 4096, max_run_seconds: 600,
|
||||
};
|
||||
|
||||
export interface CloudAgentEntry {
|
||||
|
||||
@@ -112,7 +112,7 @@ function ChannelSession({ sessionId, visible, refreshRevision, onBusy, onNewSess
|
||||
return () => { live = false; clearTimeout(timer); };
|
||||
}, [sessionId, visible, revision, refreshRevision]);
|
||||
const runId = data?.run?.agent_run_id;
|
||||
const runStatus = data?.run?.status;
|
||||
const runStatus = data?.run?.status ?? data?.queued_requests?.[0]?.status;
|
||||
const threadId = data?.thread_id;
|
||||
useEffect(() => {
|
||||
if (!visible || !threadId) return;
|
||||
@@ -169,8 +169,13 @@ function ChannelSession({ sessionId, visible, refreshRevision, onBusy, onNewSess
|
||||
{file.is_dir ? <Button variant="ghost" disabled={busy} className="min-w-0 justify-start" onClick={() => setPath(file.directory_path)}><Folder className="mr-2 h-4 w-4" /><span className="truncate">{file.name}</span></Button>
|
||||
: <><span className="min-w-0 break-all">{file.name}</span><Button variant="outline" size="sm" disabled={busy} aria-label={'保存 ' + file.name} onClick={() => void save(file)}><Download className="mr-2 h-4 w-4" />保存文件</Button></>}
|
||||
</div>);
|
||||
const statusText = runStatus ? cloudStatus(runStatus) : data?.session_state === 'closed' ? '历史会话 · 可以查看消息和保存文件' : '等待微信消息';
|
||||
const runError = data?.run?.error;
|
||||
return <div className="space-y-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3"><p role="status" className="text-sm text-muted-foreground">{data?.session_state === 'closed' ? '历史会话 · 可以查看消息和保存文件' : runStatus ? cloudStatus(runStatus) : '等待微信消息'}</p>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3"><div data-testid="wechat-run-status" className="space-y-1 text-sm">
|
||||
<p role="status" aria-label="微信任务状态" className="text-muted-foreground">任务状态:{statusText}</p>
|
||||
{runError?.message && <p role="alert" aria-label="微信任务错误" className="text-destructive">任务错误:{runError.type ? runError.type + ' · ' : ''}{runError.message}</p>}
|
||||
</div>
|
||||
{controllable && <div className="flex gap-2"><Button variant="outline" size="sm" disabled={locked} onClick={() => void control({ session_id: sessionId, operation_id: crypto.randomUUID(), action: 'stop' }).catch(() => undefined)}>停止本次任务</Button>
|
||||
<Button variant="ghost" size="sm" disabled={locked || running} onClick={() => void control({ session_id: sessionId, operation_id: crypto.randomUUID(), action: 'new-session' }).catch(() => undefined)}>开始新对话</Button></div>}</div>
|
||||
{pending.map(operation => <div key={operation.id} className="space-y-2 rounded-lg border border-amber-200 p-3 text-sm"><p>上次会话操作的结果尚未确认。恢复会核对同一次操作。</p>
|
||||
|
||||
@@ -75,7 +75,7 @@ export function ConfigurationFields({ slug, value, disabled, onChange, section =
|
||||
</div>
|
||||
<div hidden={section !== 'all' && section !== 'limits'} className="space-y-6">
|
||||
<CloudBudgetEditor slug={slug} />
|
||||
<h2 className="font-medium">运行限制</h2><p className="text-xs text-muted-foreground">以下设置随草稿保存;费用上限独立保存,对后续调用生效。</p>
|
||||
<h2 className="font-medium">运行限制</h2><p className="text-xs text-muted-foreground">以下设置随草稿保存;费用上限独立保存,对后续调用生效。执行步数会统计模型调用、工具、图节点和中间件步骤。</p>
|
||||
<label className="block space-y-2 text-sm"><span>工具审批</span>
|
||||
<select className="h-10 w-full rounded-md border border-input bg-background px-3" disabled={disabled}
|
||||
value={value.tool_approval_mode} onChange={e => change('tool_approval_mode', e.target.value === 'always_trust' ? 'always_trust' : 'default')}>
|
||||
|
||||
@@ -84,8 +84,10 @@ test('own WeChat conversation approval and generated PPT delivery in the desktop
|
||||
await page.getByRole('button', { name: /我的创作助手/ }).click();
|
||||
await page.getByRole('button', { name: '微信对话', exact: true }).click();
|
||||
const own = page.getByRole('region', { name: '微信会话', exact: true });
|
||||
await expect(own.getByTestId('wechat-run-status')).toContainText('任务状态:等待你的确认');
|
||||
await own.getByRole('button', { name: '允许执行', exact: true }).click();
|
||||
await expect(own.getByRole('heading', { name: '演示文稿已完成' })).toBeVisible();
|
||||
await expect(own.getByTestId('wechat-run-status')).toContainText('任务状态:已完成');
|
||||
await own.getByRole('button', { name: '保存 创作建议.pptx', exact: true }).click();
|
||||
await expect(own.getByText('已保存 创作建议.pptx', { exact: true })).toBeVisible();
|
||||
await page.screenshot({ path: testInfo.outputPath('wechat-own-deliverables.png') });
|
||||
|
||||
@@ -155,6 +155,16 @@ describe('Main cloud Agents boundary', () => {
|
||||
expect(JSON.parse(fetchImpl.mock.calls[1][1].body)).toEqual({ request_id: 'request', thread_id: 'thread', query: 'hello' });
|
||||
});
|
||||
|
||||
it('defaults omitted execution steps to 300 while preserving an explicit saved limit', async () => {
|
||||
const { max_execution_steps: _omitted, ...configurationWithoutSteps } = EMPTY_CLOUD_CONFIGURATION;
|
||||
const fetchImpl = vi.fn().mockResolvedValueOnce(session())
|
||||
.mockResolvedValueOnce(json({ ...draft, configuration: { ...configurationWithoutSteps, max_execution_steps: 40 } }))
|
||||
.mockResolvedValueOnce(json({ ...draft, configuration: configurationWithoutSteps }));
|
||||
const module = moduleFor(fetchImpl);
|
||||
expect((await module.get(draft.slug)).configuration.max_execution_steps).toBe(40);
|
||||
expect((await module.get(draft.slug)).configuration.max_execution_steps).toBe(300);
|
||||
});
|
||||
|
||||
it('uses the WS credential directly for the creator ledger and never exchanges it into Renderer data', async () => {
|
||||
const fetchImpl = vi.fn().mockResolvedValueOnce(json({ unit: '词元点数', items: [], access_token: 'private' }));
|
||||
const result = await moduleFor(fetchImpl).execute({ operation: 'costs', input: { slug: draft.slug } });
|
||||
@@ -222,6 +232,21 @@ describe('Main cloud Agents boundary', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves a failed channel run status, partial output, and authoritative error', () => {
|
||||
const plan = operationPlan({ operation: 'channelConversation', input: { session_id: 'session-1' } });
|
||||
expect(plan.project({
|
||||
session_id: 'session-1', thread_id: 'thread-1', messages: [], queued_requests: [], next_offset: null,
|
||||
run: { agent_run_id: 'run-1', request_id: 'request-1', thread_id: 'thread-1', agent_slug: draft.slug,
|
||||
status: 'failed', output: 'PPT 只写入了一部分。', version: '1',
|
||||
error: { type: 'execution_step_limit', message: '本次运行已达到执行步数上限,尚未完成。可在智能体设置中提高上限后重试。' } },
|
||||
})).toEqual({
|
||||
session_id: 'session-1', thread_id: 'thread-1', messages: [], queued_requests: [], next_offset: null,
|
||||
run: { agent_run_id: 'run-1', request_id: 'request-1', thread_id: 'thread-1', agent_slug: draft.slug,
|
||||
status: 'failed', output: 'PPT 只写入了一部分。', version: '1',
|
||||
error: { type: 'execution_step_limit', message: '本次运行已达到执行步数上限,尚未完成。可在智能体设置中提高上限后重试。' } },
|
||||
});
|
||||
});
|
||||
|
||||
it('routes user-level channel accounts without requiring an Agent slug', () => {
|
||||
const list = operationPlan({ operation: 'channelAccounts', input: {} });
|
||||
expect(list.path).toBe('/api/makelore/channel-accounts');
|
||||
|
||||
@@ -43,6 +43,40 @@ it('approves the actual action batch through the self-session endpoint and autom
|
||||
expect(api.call.mock.calls.some(call => ['resume', 'run', 'files', 'submit'].includes(call[0]))).toBe(false);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['running', '运行中'],
|
||||
['completed', '已完成'],
|
||||
['cancelled', '已停止'],
|
||||
['interrupted', '等待你的确认'],
|
||||
] as const)('renders the authoritative %s WeChat run status', async (status, label) => {
|
||||
const base = api.call.getMockImplementation()!;
|
||||
api.call.mockImplementation(async (operation, input) => operation === 'channelConversation'
|
||||
? { ...history, run: { ...run, status } } : base(operation, input));
|
||||
render(<CloudChannelConversations slug={session.agent_slug} />);
|
||||
expect(await screen.findByTestId('wechat-run-status')).toHaveTextContent('任务状态:' + label);
|
||||
});
|
||||
|
||||
it('keeps partial assistant output visible while surfacing a closed run failure and its authoritative error', async () => {
|
||||
const base = api.call.getMockImplementation()!;
|
||||
api.call.mockImplementation(async (operation, input) => operation === 'channelConversation'
|
||||
? { ...history, session_state: 'closed', grant_state: 'revoked', run: { ...run, status: 'failed', error: {
|
||||
type: 'unexpected_error', message: 'Recursion limit of 40 reached',
|
||||
} }, messages: [{ id: 1, role: 'assistant', content: 'PPT 只写入了一部分。' }] } : base(operation, input));
|
||||
render(<CloudChannelConversations slug={session.agent_slug} />);
|
||||
const status = await screen.findByTestId('wechat-run-status');
|
||||
expect(status).toHaveTextContent('任务状态:失败');
|
||||
expect(screen.getByRole('alert', { name: '微信任务错误' })).toHaveTextContent('unexpected_error · Recursion limit of 40 reached');
|
||||
expect(screen.getByText('PPT 只写入了一部分。')).toBeVisible();
|
||||
});
|
||||
|
||||
it('shows a queued request status before the authoritative run is exposed', async () => {
|
||||
const base = api.call.getMockImplementation()!;
|
||||
api.call.mockImplementation(async (operation, input) => operation === 'channelConversation'
|
||||
? { ...history, run: null, queued_requests: [{ request_id: 'queued-request', thread_id: session.thread_id, run_id: null, status: 'running', version: '1' }] } : base(operation, input));
|
||||
render(<CloudChannelConversations slug={session.agent_slug} />);
|
||||
expect(await screen.findByTestId('wechat-run-status')).toHaveTextContent('任务状态:运行中');
|
||||
});
|
||||
|
||||
it('retries an uncertain stop with the identical operation ID and keeps other controls disabled', async () => {
|
||||
const base = api.call.getMockImplementation()!;
|
||||
let original: unknown;
|
||||
|
||||
Reference in New Issue
Block a user