feat: update desktop workflows and app center

This commit is contained in:
inman
2026-05-13 19:14:56 +08:00
parent 20b5aff4ad
commit 7c8781a6e3
160 changed files with 55492 additions and 1423 deletions

View File

@@ -3,15 +3,31 @@ import type { IncomingMessage, ServerResponse } from 'http';
const parseJsonBodyMock = vi.fn();
const sendJsonMock = vi.fn();
const { readFileMock } = vi.hoisted(() => ({
readFileMock: vi.fn(),
}));
vi.mock('@electron/api/route-utils', () => ({
parseJsonBody: (...args: unknown[]) => parseJsonBodyMock(...args),
sendJson: (...args: unknown[]) => sendJsonMock(...args),
}));
vi.mock('node:fs/promises', async () => {
const actual = await vi.importActual<typeof import('node:fs/promises')>('node:fs/promises');
return {
...actual,
readFile: readFileMock,
default: {
...actual,
readFile: readFileMock,
},
};
});
describe('handleCronRoutes', () => {
beforeEach(() => {
vi.resetAllMocks();
readFileMock.mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
});
it('creates cron jobs with external delivery configuration', async () => {
@@ -206,4 +222,224 @@ describe('handleCronRoutes', () => {
}),
);
});
it('builds failed cron run fallback messages as visible assistant messages', async () => {
const { buildCronSessionFallbackMessages } = await import('@electron/api/routes/cron');
const messages = buildCronSessionFallbackMessages({
sessionKey: 'agent:main:cron:job-failed',
runs: [{
jobId: 'job-failed',
status: 'error',
error: '模型超时',
ts: 1773281732751,
}],
limit: 20,
});
expect(messages).toContainEqual(expect.objectContaining({
id: 'cron-run-1773281732751',
role: 'assistant',
content: 'Run failed: 模型超时',
isError: true,
}));
});
it('builds cron fallback query and running messages as visible chat messages', async () => {
const { buildCronSessionFallbackMessages } = await import('@electron/api/routes/cron');
const messages = buildCronSessionFallbackMessages({
sessionKey: 'agent:main:cron:job-running',
job: {
name: '写日报',
payload: { kind: 'agentTurn', message: '生成今天的经营日报' },
state: { runningAtMs: 1778650080011 },
},
runs: [],
limit: 20,
});
expect(messages).toContainEqual(expect.objectContaining({
id: 'cron-query-job-running-1778650080011',
role: 'user',
content: '生成今天的经营日报',
}));
expect(messages).toContainEqual(expect.objectContaining({
id: 'cron-running-job-running',
role: 'assistant',
content: '任务正在执行,完成后会自动在这里显示结果。',
}));
});
it('reconciles a failed cron state when the run trajectory finished successfully', async () => {
const rpc = vi.fn().mockResolvedValue({
jobs: [{
id: 'job-recovered',
name: 'Recovered job',
enabled: true,
createdAtMs: 1,
updatedAtMs: 2,
schedule: { kind: 'cron', expr: '0 9 * * *' },
payload: { kind: 'agentTurn', message: 'Create a document' },
delivery: { mode: 'none' },
agentId: 'main',
state: {
lastRunAtMs: 1778650080011,
lastStatus: 'error',
lastError: 'Edit failed',
lastDurationMs: 77388,
},
}],
});
readFileMock.mockImplementation(async (path: unknown) => {
const textPath = String(path);
if (textPath.endsWith('/cron/runs/job-recovered.jsonl')) {
return JSON.stringify({
ts: 1778650157404,
jobId: 'job-recovered',
action: 'finished',
status: 'error',
error: 'Edit failed',
summary: 'Edit failed',
sessionId: 'run-1',
runAtMs: 1778650080011,
});
}
if (textPath.endsWith('/agents/main/sessions/run-1.trajectory.jsonl')) {
return JSON.stringify({
type: 'trace.artifacts',
data: {
finalStatus: 'success',
assistantTexts: ['Done. Saved to the desktop.'],
},
});
}
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
});
const { handleCronRoutes } = await import('@electron/api/routes/cron');
const handled = await handleCronRoutes(
{ method: 'GET' } as IncomingMessage,
{} as ServerResponse,
new URL('http://127.0.0.1:13210/api/cron/jobs'),
{
gatewayManager: { rpc },
} as never,
);
expect(handled).toBe(true);
expect(sendJsonMock).toHaveBeenCalledWith(
expect.anything(),
200,
[expect.objectContaining({
id: 'job-recovered',
lastRun: expect.objectContaining({
success: true,
reconciled: true,
warning: 'Edit failed',
}),
})],
);
const response = sendJsonMock.mock.calls.at(-1)?.[2] as Array<{ lastRun?: { error?: string } }>;
expect(response[0]?.lastRun?.error).toBeUndefined();
});
it('returns a successful trigger response when cron.run reports a stale tool error after recovery', async () => {
parseJsonBodyMock.mockResolvedValue({ id: 'job-recovered' });
const rpc = vi.fn()
.mockRejectedValueOnce(new Error('Edit failed'))
.mockResolvedValueOnce({
jobs: [{
id: 'job-recovered',
name: 'Recovered job',
enabled: true,
createdAtMs: 1,
updatedAtMs: 2,
schedule: { kind: 'cron', expr: '0 9 * * *' },
payload: { kind: 'agentTurn', message: 'Create a document' },
delivery: { mode: 'none' },
agentId: 'main',
state: {
lastRunAtMs: 1778650080011,
lastStatus: 'error',
lastError: 'Edit failed',
lastDurationMs: 77388,
},
}],
});
readFileMock.mockImplementation(async (path: unknown) => {
const textPath = String(path);
if (textPath.endsWith('/cron/runs/job-recovered.jsonl')) {
return JSON.stringify({
ts: 1778650157404,
jobId: 'job-recovered',
action: 'finished',
status: 'error',
error: 'Edit failed',
summary: 'Edit failed',
sessionId: 'run-1',
runAtMs: 1778650080011,
});
}
if (textPath.endsWith('/agents/main/sessions/run-1.trajectory.jsonl')) {
return JSON.stringify({
type: 'trace.artifacts',
data: {
finalStatus: 'success',
assistantTexts: ['Done. Saved to the desktop.'],
},
});
}
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
});
const { handleCronRoutes } = await import('@electron/api/routes/cron');
const handled = await handleCronRoutes(
{ method: 'POST' } as IncomingMessage,
{} as ServerResponse,
new URL('http://127.0.0.1:13210/api/cron/trigger'),
{
gatewayManager: { rpc },
} as never,
);
expect(handled).toBe(true);
expect(rpc).toHaveBeenNthCalledWith(1, 'cron.run', { id: 'job-recovered', mode: 'force' });
expect(rpc).toHaveBeenNthCalledWith(2, 'cron.list', { includeDisabled: true }, 8000);
expect(sendJsonMock).toHaveBeenCalledWith(
expect.anything(),
200,
expect.objectContaining({
success: true,
recovered: true,
warning: 'Edit failed',
summary: 'Done. Saved to the desktop.',
triggerWarning: 'Error: Edit failed',
}),
);
});
it('uses recovered trajectory summaries for cron fallback messages', async () => {
const { buildCronSessionFallbackMessages } = await import('@electron/api/routes/cron');
const messages = buildCronSessionFallbackMessages({
sessionKey: 'agent:main:cron:job-recovered',
runs: [{
jobId: 'job-recovered',
status: 'error',
resolvedStatus: 'ok',
error: 'Edit failed',
resolvedSummary: 'Done. Saved to the desktop.',
ts: 1778650157404,
}],
limit: 20,
});
expect(messages).toContainEqual(expect.objectContaining({
id: 'cron-run-1778650157404',
role: 'assistant',
content: 'Done. Saved to the desktop.',
}));
expect(messages.find((message) => message.id === 'cron-run-1778650157404')?.isError).toBeUndefined();
});
});