Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Q
qjclaw-dmg
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
AI-甘富林
qjclaw-dmg
Commits
5f36e0b1
Commit
5f36e0b1
authored
May 19, 2026
by
edy
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(automation): add scheduled task management
parent
b1d4353e
Changes
29
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
29 changed files
with
2183 additions
and
20 deletions
+2183
-20
index.ts
apps/desktop/src/main/index.ts
+5
-1
ipc.ts
apps/desktop/src/main/ipc.ts
+55
-0
automation-task-service.ts
apps/desktop/src/main/services/automation-task-service.ts
+507
-0
index.ts
apps/desktop/src/preload/index.ts
+11
-1
automationTaskIpcSource.test.ts
apps/desktop/test/automationTaskIpcSource.test.ts
+28
-0
automationTaskService.test.ts
apps/desktop/test/automationTaskService.test.ts
+212
-0
App.tsx
apps/ui/src/App.tsx
+5
-1
AppIcons.tsx
apps/ui/src/components/icons/AppIcons.tsx
+9
-1
useAppBootstrap.ts
apps/ui/src/features/app/useAppBootstrap.ts
+1
-1
AutomationTasksView.tsx
apps/ui/src/features/automation/AutomationTasksView.tsx
+540
-0
ChatComposer.tsx
apps/ui/src/features/chat/ChatComposer.tsx
+1
-1
ConversationWorkspaceView.tsx
apps/ui/src/features/chat/ConversationWorkspaceView.tsx
+1
-1
MessageList.tsx
apps/ui/src/features/chat/MessageList.tsx
+1
-1
useChatSessionsController.ts
apps/ui/src/features/chat/useChatSessionsController.ts
+1
-1
useChatStreamingController.ts
apps/ui/src/features/chat/useChatStreamingController.ts
+1
-1
useHomeIntentSuggestion.ts
apps/ui/src/features/chat/useHomeIntentSuggestion.ts
+1
-1
usePromptSubmission.ts
apps/ui/src/features/chat/usePromptSubmission.ts
+1
-1
AppSidebar.tsx
apps/ui/src/features/shell/AppSidebar.tsx
+2
-1
ExpertTree.tsx
apps/ui/src/features/shell/ExpertTree.tsx
+1
-1
useHomeNavigation.ts
apps/ui/src/features/shell/useHomeNavigation.ts
+1
-1
useSidebarModel.ts
apps/ui/src/features/shell/useSidebarModel.ts
+1
-1
useSmokeActionHandlers.ts
apps/ui/src/features/smoke/useSmokeActionHandlers.ts
+1
-1
useSmokeActions.ts
apps/ui/src/features/smoke/useSmokeActions.ts
+1
-1
useSmokeSnapshot.ts
apps/ui/src/features/smoke/useSmokeSnapshot.ts
+1
-1
mock-desktop-api.ts
apps/ui/src/lib/mock-desktop-api.ts
+118
-0
styles.css
apps/ui/src/styles.css
+1
-0
automation.css
apps/ui/src/styles/automation.css
+509
-0
automationTasksSource.test.ts
apps/ui/test/automationTasksSource.test.ts
+85
-0
index.ts
packages/shared-types/src/index.ts
+82
-1
No files found.
apps/desktop/src/main/index.ts
View file @
5f36e0b1
...
@@ -36,6 +36,7 @@ import { ProjectIntentRouterService } from "./services/project-intent-router.js"
...
@@ -36,6 +36,7 @@ import { ProjectIntentRouterService } from "./services/project-intent-router.js"
import
{
ProjectSkillRouterService
}
from
"./services/project-skill-router.js"
;
import
{
ProjectSkillRouterService
}
from
"./services/project-skill-router.js"
;
import
{
ProjectWorkspaceExecutorService
}
from
"./services/project-workspace-executor.js"
;
import
{
ProjectWorkspaceExecutorService
}
from
"./services/project-workspace-executor.js"
;
import
{
TaskPanelService
}
from
"./services/task-panel-service.js"
;
import
{
TaskPanelService
}
from
"./services/task-panel-service.js"
;
import
{
AutomationTaskService
}
from
"./services/automation-task-service.js"
;
import
{
StartupLogger
}
from
"./services/startup-logger.js"
;
import
{
StartupLogger
}
from
"./services/startup-logger.js"
;
interface
RendererSmokeState
{
interface
RendererSmokeState
{
...
@@ -2303,6 +2304,7 @@ async function bootstrap(): Promise<void> {
...
@@ -2303,6 +2304,7 @@ async function bootstrap(): Promise<void> {
await traceBootstrap("runtime-configure-done");
await traceBootstrap("runtime-configure-done");
const projectWorkspaceExecutor = new ProjectWorkspaceExecutorService(runtimeManager);
const projectWorkspaceExecutor = new ProjectWorkspaceExecutorService(runtimeManager);
const taskPanelService = new TaskPanelService(systemSummary.userDataPath);
const taskPanelService = new TaskPanelService(systemSummary.userDataPath);
const automationTaskService = new AutomationTaskService(systemSummary.userDataPath, { autoStart: false });
const runtimeStatus = await runtimeManager.status();
const runtimeStatus = await runtimeManager.status();
const runtimeGatewayConnection = await runtimeManager.getGatewayConnection();
const runtimeGatewayConnection = await runtimeManager.getGatewayConnection();
if (systemSummary.isPackaged && runtimeStatus.payloadState !== "ready") {
if (systemSummary.isPackaged && runtimeStatus.payloadState !== "ready") {
...
@@ -2391,10 +2393,12 @@ async function bootstrap(): Promise<void> {
...
@@ -2391,10 +2393,12 @@ async function bootstrap(): Promise<void> {
projectExecutionRouter,
projectExecutionRouter,
projectWorkspaceExecutor,
projectWorkspaceExecutor,
taskPanelService,
taskPanelService,
automationTaskService,
startupLogger: startupLogger!,
startupLogger: startupLogger!,
systemSummary,
systemSummary,
localOpenClawConfig
localOpenClawConfig
});
});
await automationTaskService.start();
let beforeQuitHandled = false;
let beforeQuitHandled = false;
app.on("before-quit", (event) => {
app.on("before-quit", (event) => {
...
@@ -2406,6 +2410,7 @@ async function bootstrap(): Promise<void> {
...
@@ -2406,6 +2410,7 @@ async function bootstrap(): Promise<void> {
event.preventDefault();
event.preventDefault();
void (async () => {
void (async () => {
await runtimeCloudSupervisor.stop("app-before-quit");
await runtimeCloudSupervisor.stop("app-before-quit");
automationTaskService.stop();
await dailyReportService.stop();
await dailyReportService.stop();
await runtimeManager.stop();
await runtimeManager.stop();
await runtimeSkillBridge.clearManagedSkills().catch(() => undefined);
await runtimeSkillBridge.clearManagedSkills().catch(() => undefined);
...
@@ -2509,4 +2514,3 @@ if (!hasSingleInstanceLock) {
...
@@ -2509,4 +2514,3 @@ if (!hasSingleInstanceLock) {
});
});
}
}
apps/desktop/src/main/ipc.ts
View file @
5f36e0b1
...
@@ -5,6 +5,8 @@ import path from "node:path";
...
@@ -5,6 +5,8 @@ import path from "node:path";
import
{
import
{
IPC_CHANNELS
,
IPC_CHANNELS
,
type
AppConfig
,
type
AppConfig
,
type
AutomationTaskRun
,
type
CreateAutomationTaskInput
,
type
ChatAttachment
,
type
ChatAttachment
,
type
ChatCancelStreamResult
,
type
ChatCancelStreamResult
,
type
ChatMessage
,
type
ChatMessage
,
...
@@ -22,6 +24,7 @@ import {
...
@@ -22,6 +24,7 @@ import {
type
SaveConfigInput
,
type
SaveConfigInput
,
type
SignInInput
,
type
SignInInput
,
type
SystemSummary
,
type
SystemSummary
,
type
UpdateAutomationTaskInput
,
type
ProjectExecutionDecision
,
type
ProjectExecutionDecision
,
type
WorkspaceSummary
,
type
WorkspaceSummary
,
type
WorkspaceWarmupResult
type
WorkspaceWarmupResult
...
@@ -60,6 +63,7 @@ import type { ProjectExecutionRouter } from "./services/project-execution-router
...
@@ -60,6 +63,7 @@ import type { ProjectExecutionRouter } from "./services/project-execution-router
import
type
{
ProjectSkillRouterService
}
from
"./services/project-skill-router.js"
;
import
type
{
ProjectSkillRouterService
}
from
"./services/project-skill-router.js"
;
import
type
{
ProjectWorkspaceExecutorService
}
from
"./services/project-workspace-executor.js"
;
import
type
{
ProjectWorkspaceExecutorService
}
from
"./services/project-workspace-executor.js"
;
import
type
{
TaskPanelService
}
from
"./services/task-panel-service.js"
;
import
type
{
TaskPanelService
}
from
"./services/task-panel-service.js"
;
import
type
{
AutomationTaskService
}
from
"./services/automation-task-service.js"
;
import
{
import
{
buildProjectModelRuntime
,
buildProjectModelRuntime
,
materializeProjectModelRuntime
materializeProjectModelRuntime
...
@@ -105,6 +109,7 @@ interface MainServices {
...
@@ -105,6 +109,7 @@ interface MainServices {
projectExecutionRouter
:
ProjectExecutionRouter
;
projectExecutionRouter
:
ProjectExecutionRouter
;
projectWorkspaceExecutor
:
ProjectWorkspaceExecutorService
;
projectWorkspaceExecutor
:
ProjectWorkspaceExecutorService
;
taskPanelService
:
TaskPanelService
;
taskPanelService
:
TaskPanelService
;
automationTaskService
:
AutomationTaskService
;
startupLogger
:
StartupLogger
;
startupLogger
:
StartupLogger
;
appVersion
:
string
;
appVersion
:
string
;
systemSummary
:
SystemSummary
;
systemSummary
:
SystemSummary
;
...
@@ -535,6 +540,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
...
@@ -535,6 +540,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
projectExecutionRouter,
projectExecutionRouter,
projectWorkspaceExecutor,
projectWorkspaceExecutor,
taskPanelService,
taskPanelService,
automationTaskService,
startupLogger,
startupLogger,
systemSummary,
systemSummary,
localOpenClawConfig
localOpenClawConfig
...
@@ -2809,6 +2815,41 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
...
@@ -2809,6 +2815,41 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
return result;
return result;
};
};
automationTaskService.setExecutor(async ({ task, run }) => {
const projectId = task.expertId?.trim() || BUILTIN_HOME_PROJECT_ID;
const projects = await projectStore.listProjects().catch(() => []);
const project = projects.find((candidate) => candidate.id === projectId);
const expertName = task.expertName?.trim()
|| project?.displayName?.trim()
|| project?.name?.trim()
|| (projectId === BUILTIN_HOME_PROJECT_ID ? "
千匠问天
" : projectId);
const session = await projectStore.createSession(`[自动化] ${task.title}`, projectId);
runtimeCloudSupervisor.noteSessions([session.id]);
const result = await sendPrompt(session.id, task.prompt);
await taskPanelService.recordWorkspaceExecution({
sessionId: result.sessionId,
runId: run.id,
expertName,
taskTitle: task.title,
completedAt: new Date().toISOString(),
messageCount: 2,
artifacts: []
}).catch((error) => startupLogger.warn("
diagnostics
", "
automation
-
task
.
archive
-
failed
", "
Failed
to
archive
automation
task
output
.
", {
taskId: task.id,
runId: run.id,
error: error instanceof Error ? error.message : String(error)
}));
return {
sessionId: result.sessionId,
runId: result.reply.id,
replyText: result.reply.content,
artifacts: []
};
});
ipcMain.handle(IPC_CHANNELS.workspaceGetSummary, async () => buildWorkspaceSummary());
ipcMain.handle(IPC_CHANNELS.workspaceGetSummary, async () => buildWorkspaceSummary());
ipcMain.handle(IPC_CHANNELS.workspaceWarmup, async () => queueWorkspaceWarmup("
workspace
-
warmup
", { action: "
init
" }));
ipcMain.handle(IPC_CHANNELS.workspaceWarmup, async () => queueWorkspaceWarmup("
workspace
-
warmup
", { action: "
init
" }));
ipcMain.handle(IPC_CHANNELS.windowMinimize, async (event) => {
ipcMain.handle(IPC_CHANNELS.windowMinimize, async (event) => {
...
@@ -2903,6 +2944,12 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
...
@@ -2903,6 +2944,12 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
ipcMain.handle(IPC_CHANNELS.modelConfigGetSummary, async () => modelConfigClient.getSummary());
ipcMain.handle(IPC_CHANNELS.modelConfigGetSummary, async () => modelConfigClient.getSummary());
ipcMain.handle(IPC_CHANNELS.systemGetSummary, async () => systemSummary);
ipcMain.handle(IPC_CHANNELS.systemGetSummary, async () => systemSummary);
ipcMain.handle(IPC_CHANNELS.tasksListByDate, async (_event, date: string) => taskPanelService.listByDate(date));
ipcMain.handle(IPC_CHANNELS.tasksListByDate, async (_event, date: string) => taskPanelService.listByDate(date));
ipcMain.handle(IPC_CHANNELS.automationTasksList, async () => automationTaskService.list());
ipcMain.handle(IPC_CHANNELS.automationTasksCreate, async (_event, input: CreateAutomationTaskInput) => automationTaskService.create(input));
ipcMain.handle(IPC_CHANNELS.automationTasksUpdate, async (_event, taskId: string, input: UpdateAutomationTaskInput) => automationTaskService.update(taskId, input));
ipcMain.handle(IPC_CHANNELS.automationTasksDelete, async (_event, taskId: string) => automationTaskService.delete(taskId));
ipcMain.handle(IPC_CHANNELS.automationTasksRunNow, async (_event, taskId: string): Promise<AutomationTaskRun> => automationTaskService.runNow(taskId));
ipcMain.handle(IPC_CHANNELS.automationTasksListRuns, async (_event, taskId?: string) => automationTaskService.listRuns(taskId));
ipcMain.handle(IPC_CHANNELS.skillCatalogList, async () => skillCatalogService.listForActiveProject());
ipcMain.handle(IPC_CHANNELS.skillCatalogList, async () => skillCatalogService.listForActiveProject());
ipcMain.handle(IPC_CHANNELS.projectsList, async () => projectStore.listProjects());
ipcMain.handle(IPC_CHANNELS.projectsList, async () => projectStore.listProjects());
...
@@ -3039,6 +3086,14 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
...
@@ -3039,6 +3086,14 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
tasks: {
tasks: {
listByDate: (date: string) => taskPanelService.listByDate(date)
listByDate: (date: string) => taskPanelService.listByDate(date)
},
},
automationTasks: {
list: () => automationTaskService.list(),
create: (input: CreateAutomationTaskInput) => automationTaskService.create(input),
update: (taskId: string, input: UpdateAutomationTaskInput) => automationTaskService.update(taskId, input),
delete: (taskId: string) => automationTaskService.delete(taskId),
runNow: (taskId: string) => automationTaskService.runNow(taskId),
listRuns: (taskId?: string) => automationTaskService.listRuns(taskId)
},
chat: {
chat: {
listSessions: async () => {
listSessions: async () => {
const sessions = await listSessionsForActiveProject(projectStore);
const sessions = await listSessionsForActiveProject(projectStore);
...
...
apps/desktop/src/main/services/automation-task-service.ts
0 → 100644
View file @
5f36e0b1
This diff is collapsed.
Click to expand it.
apps/desktop/src/preload/index.ts
View file @
5f36e0b1
import
{
contextBridge
,
ipcRenderer
}
from
"electron"
;
import
{
contextBridge
,
ipcRenderer
}
from
"electron"
;
import
{
import
{
IPC_CHANNELS
,
IPC_CHANNELS
,
type
CreateAutomationTaskInput
,
type
ChatAttachment
,
type
ChatAttachment
,
type
ChatStreamListener
,
type
ChatStreamListener
,
type
ConfigSecretId
,
type
ConfigSecretId
,
type
DesktopApi
,
type
DesktopApi
,
type
RuntimeCloudFetchAction
,
type
RuntimeCloudFetchAction
,
type
SaveConfigInput
,
type
SaveConfigInput
,
type
SignInInput
type
SignInInput
,
type
UpdateAutomationTaskInput
}
from
"@qjclaw/shared-types"
;
}
from
"@qjclaw/shared-types"
;
const
desktopApi
:
DesktopApi
=
{
const
desktopApi
:
DesktopApi
=
{
...
@@ -83,6 +85,14 @@ const desktopApi: DesktopApi = {
...
@@ -83,6 +85,14 @@ const desktopApi: DesktopApi = {
tasks
:
{
tasks
:
{
listByDate
:
(
date
:
string
)
=>
ipcRenderer
.
invoke
(
IPC_CHANNELS
.
tasksListByDate
,
date
)
listByDate
:
(
date
:
string
)
=>
ipcRenderer
.
invoke
(
IPC_CHANNELS
.
tasksListByDate
,
date
)
},
},
automationTasks
:
{
list
:
()
=>
ipcRenderer
.
invoke
(
IPC_CHANNELS
.
automationTasksList
),
create
:
(
input
:
CreateAutomationTaskInput
)
=>
ipcRenderer
.
invoke
(
IPC_CHANNELS
.
automationTasksCreate
,
input
),
update
:
(
taskId
:
string
,
input
:
UpdateAutomationTaskInput
)
=>
ipcRenderer
.
invoke
(
IPC_CHANNELS
.
automationTasksUpdate
,
taskId
,
input
),
delete
:
(
taskId
:
string
)
=>
ipcRenderer
.
invoke
(
IPC_CHANNELS
.
automationTasksDelete
,
taskId
),
runNow
:
(
taskId
:
string
)
=>
ipcRenderer
.
invoke
(
IPC_CHANNELS
.
automationTasksRunNow
,
taskId
),
listRuns
:
(
taskId
?:
string
)
=>
ipcRenderer
.
invoke
(
IPC_CHANNELS
.
automationTasksListRuns
,
taskId
)
},
chat
:
{
chat
:
{
listSessions
:
()
=>
ipcRenderer
.
invoke
(
IPC_CHANNELS
.
chatListSessions
),
listSessions
:
()
=>
ipcRenderer
.
invoke
(
IPC_CHANNELS
.
chatListSessions
),
listSessionsByProject
:
(
projectId
:
string
)
=>
ipcRenderer
.
invoke
(
IPC_CHANNELS
.
chatListSessionsByProject
,
projectId
),
listSessionsByProject
:
(
projectId
:
string
)
=>
ipcRenderer
.
invoke
(
IPC_CHANNELS
.
chatListSessionsByProject
,
projectId
),
...
...
apps/desktop/test/automationTaskIpcSource.test.ts
0 → 100644
View file @
5f36e0b1
import
test
from
"node:test"
import
assert
from
"node:assert/strict"
import
{
readFileSync
}
from
"node:fs"
const
sharedTypesSource
=
readFileSync
(
new
URL
(
"../../../packages/shared-types/src/index.ts"
,
import
.
meta
.
url
),
"utf8"
)
const
ipcSource
=
readFileSync
(
new
URL
(
"../src/main/ipc.ts"
,
import
.
meta
.
url
),
"utf8"
)
const
indexSource
=
readFileSync
(
new
URL
(
"../src/main/index.ts"
,
import
.
meta
.
url
),
"utf8"
)
const
preloadSource
=
readFileSync
(
new
URL
(
"../src/preload/index.ts"
,
import
.
meta
.
url
),
"utf8"
)
test
(
"shared automation task channels and desktop API are declared"
,
()
=>
{
assert
.
match
(
sharedTypesSource
,
/automationTasksList:
\s
*"automation-tasks:list"/
)
assert
.
match
(
sharedTypesSource
,
/automationTasksCreate:
\s
*"automation-tasks:create"/
)
assert
.
match
(
sharedTypesSource
,
/automationTasksUpdate:
\s
*"automation-tasks:update"/
)
assert
.
match
(
sharedTypesSource
,
/automationTasksDelete:
\s
*"automation-tasks:delete"/
)
assert
.
match
(
sharedTypesSource
,
/automationTasksRunNow:
\s
*"automation-tasks:run-now"/
)
assert
.
match
(
sharedTypesSource
,
/automationTasksListRuns:
\s
*"automation-tasks:list-runs"/
)
assert
.
match
(
sharedTypesSource
,
/automationTasks:
\s
*
\{
/
)
})
test
(
"desktop IPC wires automation tasks to AutomationTaskService"
,
()
=>
{
assert
.
match
(
indexSource
,
/new AutomationTaskService
\(
systemSummary
\.
userDataPath/
)
assert
.
match
(
indexSource
,
/automationTaskService
\.
start
\(\)
/
)
assert
.
match
(
ipcSource
,
/automationTaskService
\.
setExecutor/
)
assert
.
match
(
ipcSource
,
/ipcMain
\.
handle
\(
IPC_CHANNELS
\.
automationTasksList/
)
assert
.
match
(
ipcSource
,
/ipcMain
\.
handle
\(
IPC_CHANNELS
\.
automationTasksRunNow/
)
assert
.
match
(
preloadSource
,
/automationTasks:
\s
*
\{
/
)
assert
.
match
(
preloadSource
,
/IPC_CHANNELS
\.
automationTasksCreate/
)
})
apps/desktop/test/automationTaskService.test.ts
0 → 100644
View file @
5f36e0b1
import
test
from
"node:test"
import
assert
from
"node:assert/strict"
import
{
mkdtemp
,
rm
}
from
"node:fs/promises"
import
{
tmpdir
}
from
"node:os"
import
path
from
"node:path"
import
{
AutomationTaskService
,
computeAutomationNextRunAt
}
from
"../src/main/services/automation-task-service.ts"
async
function
withService
<
T
>
(
run
:
(
service
:
AutomationTaskService
,
userDataPath
:
string
)
=>
Promise
<
T
>
,
now
:
Date
|
(()
=>
Date
)
=
new
Date
(
2026
,
4
,
19
,
9
,
0
,
0
,
0
)
)
{
const
userDataPath
=
await
mkdtemp
(
path
.
join
(
tmpdir
(),
"qjc-automation-"
))
try
{
const
getNow
=
()
=>
typeof
now
===
"function"
?
now
()
:
now
const
service
=
new
AutomationTaskService
(
userDataPath
,
{
now
:
()
=>
new
Date
(
getNow
()),
autoStart
:
false
})
service
.
setExecutor
(
async
({
task
,
run
:
taskRun
})
=>
({
sessionId
:
`session-
${
task
.
id
}
`
,
runId
:
taskRun
.
id
,
replyText
:
`done:
${
task
.
prompt
}
`
,
artifacts
:
[]
}))
return
await
run
(
service
,
userDataPath
)
}
finally
{
await
rm
(
userDataPath
,
{
recursive
:
true
,
force
:
true
})
}
}
test
(
"computes next run for one-time, daily, and weekly schedules"
,
()
=>
{
const
from
=
new
Date
(
2026
,
4
,
19
,
9
,
30
,
0
,
0
)
assert
.
equal
(
computeAutomationNextRunAt
({
kind
:
"once"
,
runAt
:
"2026-05-19T10:00:00.000Z"
},
from
),
"2026-05-19T10:00:00.000Z"
)
assert
.
equal
(
computeAutomationNextRunAt
({
kind
:
"once"
,
runAt
:
new
Date
(
2026
,
4
,
19
,
9
,
0
,
0
,
0
).
toISOString
()
},
from
),
undefined
)
assert
.
equal
(
computeAutomationNextRunAt
({
kind
:
"daily"
,
time
:
"10:15"
},
from
),
new
Date
(
2026
,
4
,
19
,
10
,
15
,
0
,
0
).
toISOString
()
)
assert
.
equal
(
computeAutomationNextRunAt
({
kind
:
"daily"
,
time
:
"08:15"
},
from
),
new
Date
(
2026
,
4
,
20
,
8
,
15
,
0
,
0
).
toISOString
()
)
assert
.
equal
(
computeAutomationNextRunAt
({
kind
:
"weekly"
,
time
:
"10:00"
,
weekdays
:
[
2
]
},
from
),
new
Date
(
2026
,
4
,
19
,
10
,
0
,
0
,
0
).
toISOString
()
)
assert
.
equal
(
computeAutomationNextRunAt
({
kind
:
"weekly"
,
time
:
"08:00"
,
weekdays
:
[
2
]
},
from
),
new
Date
(
2026
,
4
,
26
,
8
,
0
,
0
,
0
).
toISOString
()
)
})
test
(
"creates, updates, deletes, and persists automation tasks"
,
async
()
=>
{
await
withService
(
async
(
service
,
userDataPath
)
=>
{
const
created
=
await
service
.
create
({
title
:
"每日复盘"
,
prompt
:
"整理昨天内容表现"
,
expertId
:
"zhihu"
,
expertName
:
"知乎专家"
,
enabled
:
true
,
schedule
:
{
kind
:
"daily"
,
time
:
"10:00"
}
})
assert
.
equal
(
created
.
title
,
"每日复盘"
)
assert
.
equal
(
created
.
nextRunAt
,
new
Date
(
2026
,
4
,
19
,
10
,
0
,
0
,
0
).
toISOString
())
const
reloaded
=
new
AutomationTaskService
(
userDataPath
,
{
now
:
()
=>
new
Date
(
2026
,
4
,
19
,
9
,
0
,
0
,
0
),
autoStart
:
false
})
assert
.
equal
((
await
reloaded
.
list
()).
length
,
1
)
const
updated
=
await
service
.
update
(
created
.
id
,
{
enabled
:
false
,
title
:
"每日复盘更新"
})
assert
.
equal
(
updated
?.
enabled
,
false
)
assert
.
equal
(
updated
?.
nextRunAt
,
undefined
)
assert
.
equal
(
await
service
.
delete
(
created
.
id
),
true
)
assert
.
deepEqual
(
await
service
.
list
(),
[])
})
})
test
(
"rejects enabled one-time tasks scheduled in the past"
,
async
()
=>
{
await
withService
(
async
(
service
)
=>
{
await
assert
.
rejects
(
service
.
create
({
title
:
"过期一次性任务"
,
prompt
:
"不应启用"
,
enabled
:
true
,
schedule
:
{
kind
:
"once"
,
runAt
:
new
Date
(
2026
,
4
,
18
,
9
,
0
,
0
,
0
).
toISOString
()
}
}),
/future/
)
const
disabledPastTask
=
await
service
.
create
({
title
:
"已停用过期任务"
,
prompt
:
"允许保存"
,
enabled
:
false
,
schedule
:
{
kind
:
"once"
,
runAt
:
new
Date
(
2026
,
4
,
18
,
9
,
0
,
0
,
0
).
toISOString
()
}
})
await
assert
.
rejects
(
service
.
update
(
disabledPastTask
.
id
,
{
enabled
:
true
}),
/future/
)
})
})
test
(
"marks missed app-runtime schedules without auto backfill"
,
async
()
=>
{
await
withService
(
async
(
service
,
userDataPath
)
=>
{
await
service
.
create
({
title
:
"早报"
,
prompt
:
"生成早报"
,
enabled
:
true
,
schedule
:
{
kind
:
"daily"
,
time
:
"08:00"
}
})
const
restarted
=
new
AutomationTaskService
(
userDataPath
,
{
now
:
()
=>
new
Date
(
2026
,
4
,
19
,
9
,
0
,
0
,
0
),
autoStart
:
false
})
await
restarted
.
recoverMissedRuns
()
const
[
task
]
=
await
restarted
.
list
()
assert
.
equal
(
task
?.
lastRunStatus
,
"missed"
)
assert
.
equal
(
task
?.
nextRunAt
,
new
Date
(
2026
,
4
,
20
,
8
,
0
,
0
,
0
).
toISOString
())
const
runs
=
await
restarted
.
listRuns
(
task
!
.
id
)
assert
.
equal
(
runs
.
length
,
1
)
assert
.
equal
(
runs
[
0
]?.
status
,
"missed"
)
},
new
Date
(
2026
,
4
,
18
,
7
,
0
,
0
,
0
))
})
test
(
"manual runNow executes the configured executor and records output"
,
async
()
=>
{
await
withService
(
async
(
service
)
=>
{
const
task
=
await
service
.
create
({
title
:
"线索整理"
,
prompt
:
"整理线索"
,
enabled
:
false
,
schedule
:
{
kind
:
"once"
,
runAt
:
"2026-05-20T09:00:00.000Z"
}
})
const
run
=
await
service
.
runNow
(
task
.
id
)
assert
.
equal
(
run
.
status
,
"completed"
)
assert
.
equal
(
run
.
replyText
,
"done:整理线索"
)
assert
.
equal
(
run
.
sessionId
,
`session-
${
task
.
id
}
`
)
const
runs
=
await
service
.
listRuns
(
task
.
id
)
assert
.
equal
(
runs
[
0
]?.
id
,
run
.
id
)
assert
.
equal
((
await
service
.
list
())[
0
]?.
lastRunStatus
,
"completed"
)
})
})
test
(
"scheduled one-time task disables itself after completing"
,
async
()
=>
{
let
now
=
new
Date
(
2026
,
4
,
19
,
8
,
0
,
0
,
0
)
await
withService
(
async
(
service
)
=>
{
const
task
=
await
service
.
create
({
title
:
"一次性整理"
,
prompt
:
"执行一次"
,
enabled
:
true
,
schedule
:
{
kind
:
"once"
,
runAt
:
new
Date
(
2026
,
4
,
19
,
9
,
0
,
0
,
0
).
toISOString
()
}
})
now
=
new
Date
(
2026
,
4
,
19
,
9
,
0
,
0
,
0
)
await
service
[
"runDueTasks"
]()
const
[
updated
]
=
await
service
.
list
()
assert
.
equal
(
updated
?.
id
,
task
.
id
)
assert
.
equal
(
updated
?.
enabled
,
false
)
assert
.
equal
(
updated
?.
nextRunAt
,
undefined
)
assert
.
equal
(
updated
?.
lastRunStatus
,
"completed"
)
},
()
=>
now
)
})
test
(
"missed one-time task disables itself and keeps a missed run"
,
async
()
=>
{
await
withService
(
async
(
service
,
userDataPath
)
=>
{
const
task
=
await
service
.
create
({
title
:
"错过的一次性任务"
,
prompt
:
"错过后保留"
,
enabled
:
true
,
schedule
:
{
kind
:
"once"
,
runAt
:
new
Date
(
2026
,
4
,
18
,
9
,
0
,
0
,
0
).
toISOString
()
}
})
const
restarted
=
new
AutomationTaskService
(
userDataPath
,
{
now
:
()
=>
new
Date
(
2026
,
4
,
19
,
9
,
0
,
0
,
0
),
autoStart
:
false
})
await
restarted
.
recoverMissedRuns
()
const
[
updated
]
=
await
restarted
.
list
()
assert
.
equal
(
updated
?.
id
,
task
.
id
)
assert
.
equal
(
updated
?.
enabled
,
false
)
assert
.
equal
(
updated
?.
nextRunAt
,
undefined
)
assert
.
equal
(
updated
?.
lastRunStatus
,
"missed"
)
const
runs
=
await
restarted
.
listRuns
(
task
.
id
)
assert
.
equal
(
runs
.
length
,
1
)
assert
.
equal
(
runs
[
0
]?.
status
,
"missed"
)
},
new
Date
(
2026
,
4
,
18
,
7
,
0
,
0
,
0
))
})
apps/ui/src/App.tsx
View file @
5f36e0b1
...
@@ -50,6 +50,7 @@ import { usePromptSubmission } from "./features/chat/usePromptSubmission";
...
@@ -50,6 +50,7 @@ import { usePromptSubmission } from "./features/chat/usePromptSubmission";
import
{
useSessionMessageStore
}
from
"./features/chat/useSessionMessageStore"
;
import
{
useSessionMessageStore
}
from
"./features/chat/useSessionMessageStore"
;
import
{
KnowledgeView
}
from
"./features/knowledge/KnowledgeView"
;
import
{
KnowledgeView
}
from
"./features/knowledge/KnowledgeView"
;
import
{
TaskPanelView
}
from
"./features/tasks/TaskPanelView"
;
import
{
TaskPanelView
}
from
"./features/tasks/TaskPanelView"
;
import
{
AutomationTasksView
}
from
"./features/automation/AutomationTasksView"
;
import
{
getPluginCopy
,
getPluginStatusLabel
,
getPluginTone
,
groupPluginsByStatus
}
from
"./features/plugins/pluginDisplay"
;
import
{
getPluginCopy
,
getPluginStatusLabel
,
getPluginTone
,
groupPluginsByStatus
}
from
"./features/plugins/pluginDisplay"
;
import
{
PluginsView
}
from
"./features/plugins/PluginsView"
;
import
{
PluginsView
}
from
"./features/plugins/PluginsView"
;
import
{
AppSidebar
}
from
"./features/shell/AppSidebar"
;
import
{
AppSidebar
}
from
"./features/shell/AppSidebar"
;
...
@@ -111,7 +112,7 @@ import {
...
@@ -111,7 +112,7 @@ import {
import
{
desktopApi
,
isMockDesktopApi
,
smokeEnabled
}
from
"./lib/desktop-api"
;
import
{
desktopApi
,
isMockDesktopApi
,
smokeEnabled
}
from
"./lib/desktop-api"
;
import
{
getHiddenMessageIds
,
persistHiddenMessageId
}
from
"./lib/hiddenMessages"
;
import
{
getHiddenMessageIds
,
persistHiddenMessageId
}
from
"./lib/hiddenMessages"
;
type
ViewMode
=
"chat"
|
"experts"
|
"tasks"
|
"plugins"
|
"settings"
|
"knowledge"
;
type
ViewMode
=
"chat"
|
"experts"
|
"tasks"
|
"
automation"
|
"
plugins"
|
"settings"
|
"knowledge"
;
type
SendPhase
=
"idle"
|
"preparing"
|
"streaming"
|
"finalizing"
;
type
SendPhase
=
"idle"
|
"preparing"
|
"streaming"
|
"finalizing"
;
type
MessageReaction
=
"up"
|
"down"
;
type
MessageReaction
=
"up"
|
"down"
;
...
@@ -1611,6 +1612,9 @@ export default function App() {
...
@@ -1611,6 +1612,9 @@ export default function App() {
{
viewMode
===
"tasks"
?
(
{
viewMode
===
"tasks"
?
(
<
TaskPanelView
/>
<
TaskPanelView
/>
)
:
null
}
)
:
null
}
{
viewMode
===
"automation"
?
(
<
AutomationTasksView
projects=
{
projects
}
/>
)
:
null
}
{
viewMode
===
"settings"
?
(
{
viewMode
===
"settings"
?
(
<
SettingsView
statusHint=
{
settingsStatusHint
}
>
<
SettingsView
statusHint=
{
settingsStatusHint
}
>
<
SettingsPanels
{
...
settingsPanelsProps
}
/>
<
SettingsPanels
{
...
settingsPanelsProps
}
/>
...
...
apps/ui/src/components/icons/AppIcons.tsx
View file @
5f36e0b1
...
@@ -234,7 +234,7 @@ export function getIntentSuggestionIcon(platform?: string): ReactNode {
...
@@ -234,7 +234,7 @@ export function getIntentSuggestionIcon(platform?: string): ReactNode {
return
<
BrowserExpertIcon
/>;
return
<
BrowserExpertIcon
/>;
}
}
export
function
NavIcon
({
kind
}:
{
kind
:
"chat"
|
"experts"
|
"tasks"
|
"plugins"
|
"settings"
|
"knowledge"
})
{
export
function
NavIcon
({
kind
}:
{
kind
:
"chat"
|
"experts"
|
"tasks"
|
"
automation"
|
"
plugins"
|
"settings"
|
"knowledge"
})
{
switch
(
kind
)
{
switch
(
kind
)
{
case
"chat"
:
case
"chat"
:
return
(
return
(
...
@@ -258,6 +258,14 @@ export function NavIcon({ kind }: { kind: "chat" | "experts" | "tasks" | "plugin
...
@@ -258,6 +258,14 @@ export function NavIcon({ kind }: { kind: "chat" | "experts" | "tasks" | "plugin
<
path
d=
"M13.05 16.95h2.8"
fill=
"none"
stroke=
"#2563EB"
strokeLinecap=
"round"
strokeWidth=
"1.35"
/>
<
path
d=
"M13.05 16.95h2.8"
fill=
"none"
stroke=
"#2563EB"
strokeLinecap=
"round"
strokeWidth=
"1.35"
/>
</
svg
>
</
svg
>
);
);
case
"automation"
:
return
(
<
svg
viewBox=
"0 0 24 24"
aria
-
hidden=
"true"
focusable=
"false"
>
<
path
d=
"M5.25 7.25A2.25 2.25 0 0 1 7.5 5h9A2.25 2.25 0 0 1 18.75 7.25v9.5A2.25 2.25 0 0 1 16.5 19h-9a2.25 2.25 0 0 1-2.25-2.25v-9.5Z"
fill=
"#FEF3C7"
stroke=
"#D97706"
strokeLinecap=
"round"
strokeLinejoin=
"round"
strokeWidth=
"1.45"
/>
<
path
d=
"M8 4v3M16 4v3M5.75 9.25h12.5"
fill=
"none"
stroke=
"#B45309"
strokeLinecap=
"round"
strokeWidth=
"1.35"
/>
<
path
d=
"M8.4 13.05h2.65v2.65H8.4zM12.95 12.45l2.6 1.55-2.6 1.55v-3.1Z"
fill=
"#2563EB"
/>
</
svg
>
);
case
"plugins"
:
case
"plugins"
:
return
(
return
(
<
svg
viewBox=
"0 0 24 24"
aria
-
hidden=
"true"
focusable=
"false"
>
<
svg
viewBox=
"0 0 24 24"
aria
-
hidden=
"true"
focusable=
"false"
>
...
...
apps/ui/src/features/app/useAppBootstrap.ts
View file @
5f36e0b1
...
@@ -13,7 +13,7 @@ import type {
...
@@ -13,7 +13,7 @@ import type {
WorkspaceSummary
WorkspaceSummary
}
from
"@qjclaw/shared-types"
}
from
"@qjclaw/shared-types"
type
ViewMode
=
"chat"
|
"experts"
|
"tasks"
|
"plugins"
|
"settings"
|
"knowledge"
type
ViewMode
=
"chat"
|
"experts"
|
"tasks"
|
"
automation"
|
"
plugins"
|
"settings"
|
"knowledge"
interface
BootstrapSkill
{
interface
BootstrapSkill
{
id
:
string
id
:
string
...
...
apps/ui/src/features/automation/AutomationTasksView.tsx
0 → 100644
View file @
5f36e0b1
This diff is collapsed.
Click to expand it.
apps/ui/src/features/chat/ChatComposer.tsx
View file @
5f36e0b1
...
@@ -22,7 +22,7 @@ interface ChatComposerProps {
...
@@ -22,7 +22,7 @@ interface ChatComposerProps {
canSend
:
boolean
canSend
:
boolean
isDragOver
:
boolean
isDragOver
:
boolean
isResizeActive
:
boolean
isResizeActive
:
boolean
viewMode
:
"chat"
|
"experts"
|
"tasks"
|
"plugins"
|
"settings"
|
"knowledge"
viewMode
:
"chat"
|
"experts"
|
"tasks"
|
"
automation"
|
"
plugins"
|
"settings"
|
"knowledge"
shellStyle
:
CSSProperties
shellStyle
:
CSSProperties
attachmentInputRef
:
RefObject
<
HTMLInputElement
|
null
>
attachmentInputRef
:
RefObject
<
HTMLInputElement
|
null
>
skillMenuRef
:
RefObject
<
HTMLDivElement
|
null
>
skillMenuRef
:
RefObject
<
HTMLDivElement
|
null
>
...
...
apps/ui/src/features/chat/ConversationWorkspaceView.tsx
View file @
5f36e0b1
...
@@ -19,7 +19,7 @@ import { ConversationStatusNotice, HomeIntentSuggestionNotice } from "./Conversa
...
@@ -19,7 +19,7 @@ import { ConversationStatusNotice, HomeIntentSuggestionNotice } from "./Conversa
import
{
MessageList
,
type
ExpertKey
,
type
MessageListMessage
}
from
"./MessageList"
import
{
MessageList
,
type
ExpertKey
,
type
MessageListMessage
}
from
"./MessageList"
import
type
{
MessageTraceState
}
from
"./useMessageTraces"
import
type
{
MessageTraceState
}
from
"./useMessageTraces"
type
ViewMode
=
"chat"
|
"experts"
|
"tasks"
|
"plugins"
|
"settings"
|
"knowledge"
type
ViewMode
=
"chat"
|
"experts"
|
"tasks"
|
"
automation"
|
"
plugins"
|
"settings"
|
"knowledge"
type
MessageReaction
=
"up"
|
"down"
type
MessageReaction
=
"up"
|
"down"
interface
HomeStarterPrompt
{
interface
HomeStarterPrompt
{
...
...
apps/ui/src/features/chat/MessageList.tsx
View file @
5f36e0b1
...
@@ -4,7 +4,7 @@ import { desktopApi } from "../../lib/desktop-api"
...
@@ -4,7 +4,7 @@ import { desktopApi } from "../../lib/desktop-api"
import
{
getTraceDisplayLines
,
getTraceLineClassName
,
getTraceStripTitle
}
from
"./messageTraceDisplay"
import
{
getTraceDisplayLines
,
getTraceLineClassName
,
getTraceStripTitle
}
from
"./messageTraceDisplay"
import
type
{
MessageTraceState
}
from
"./useMessageTraces"
import
type
{
MessageTraceState
}
from
"./useMessageTraces"
type
ViewMode
=
"chat"
|
"experts"
|
"tasks"
|
"plugins"
|
"settings"
|
"knowledge"
type
ViewMode
=
"chat"
|
"experts"
|
"tasks"
|
"
automation"
|
"
plugins"
|
"settings"
|
"knowledge"
type
MessageReaction
=
"up"
|
"down"
type
MessageReaction
=
"up"
|
"down"
export
type
ExpertKey
=
"xiaohongshu"
|
"douyin"
|
"browser"
|
"general"
export
type
ExpertKey
=
"xiaohongshu"
|
"douyin"
|
"browser"
|
"general"
...
...
apps/ui/src/features/chat/useChatSessionsController.ts
View file @
5f36e0b1
...
@@ -3,7 +3,7 @@ import type { DesktopApi, WorkspaceSummary } from "@qjclaw/shared-types"
...
@@ -3,7 +3,7 @@ import type { DesktopApi, WorkspaceSummary } from "@qjclaw/shared-types"
import
{
EMPTY_SESSION_ID
,
HOME_CHAT_PROJECT_ID
}
from
"../../lib/constants"
import
{
EMPTY_SESSION_ID
,
HOME_CHAT_PROJECT_ID
}
from
"../../lib/constants"
import
{
resolvePreferredSessionId
}
from
"../../lib/chat-utils"
import
{
resolvePreferredSessionId
}
from
"../../lib/chat-utils"
type
ViewMode
=
"chat"
|
"experts"
|
"tasks"
|
"plugins"
|
"settings"
|
"knowledge"
type
ViewMode
=
"chat"
|
"experts"
|
"tasks"
|
"
automation"
|
"
plugins"
|
"settings"
|
"knowledge"
interface
UseChatSessionsControllerDeps
{
interface
UseChatSessionsControllerDeps
{
desktopApi
:
DesktopApi
desktopApi
:
DesktopApi
...
...
apps/ui/src/features/chat/useChatStreamingController.ts
View file @
5f36e0b1
...
@@ -6,7 +6,7 @@ import { TYPEWRITER_CHARS_PER_FRAME } from "../../lib/constants"
...
@@ -6,7 +6,7 @@ import { TYPEWRITER_CHARS_PER_FRAME } from "../../lib/constants"
import
{
canExchangeMessages
}
from
"../../lib/workspace-state"
import
{
canExchangeMessages
}
from
"../../lib/workspace-state"
import
type
{
SmokeStreamSnapshot
}
from
"../smoke/types"
import
type
{
SmokeStreamSnapshot
}
from
"../smoke/types"
type
ViewMode
=
"chat"
|
"experts"
|
"tasks"
|
"plugins"
|
"settings"
|
"knowledge"
type
ViewMode
=
"chat"
|
"experts"
|
"tasks"
|
"
automation"
|
"
plugins"
|
"settings"
|
"knowledge"
type
SendPhase
=
"idle"
|
"preparing"
|
"streaming"
|
"finalizing"
type
SendPhase
=
"idle"
|
"preparing"
|
"streaming"
|
"finalizing"
interface
ActiveStreamState
{
interface
ActiveStreamState
{
requestId
:
string
requestId
:
string
...
...
apps/ui/src/features/chat/useHomeIntentSuggestion.ts
View file @
5f36e0b1
...
@@ -4,7 +4,7 @@ import type { SubmitPromptOptions } from "./useChatStreamingController"
...
@@ -4,7 +4,7 @@ import type { SubmitPromptOptions } from "./useChatStreamingController"
import
{
resolvePreferredSessionId
}
from
"../../lib/chat-utils"
import
{
resolvePreferredSessionId
}
from
"../../lib/chat-utils"
import
{
HOME_CHAT_PROJECT_ID
}
from
"../../lib/constants"
import
{
HOME_CHAT_PROJECT_ID
}
from
"../../lib/constants"
type
ViewMode
=
"chat"
|
"experts"
|
"tasks"
|
"plugins"
|
"settings"
|
"knowledge"
type
ViewMode
=
"chat"
|
"experts"
|
"tasks"
|
"
automation"
|
"
plugins"
|
"settings"
|
"knowledge"
export
interface
PendingHomeIntentSuggestion
{
export
interface
PendingHomeIntentSuggestion
{
suggestion
:
ProjectIntentSuggestion
suggestion
:
ProjectIntentSuggestion
...
...
apps/ui/src/features/chat/usePromptSubmission.ts
View file @
5f36e0b1
...
@@ -4,7 +4,7 @@ import type { SubmitPromptOptions } from "./useChatStreamingController"
...
@@ -4,7 +4,7 @@ import type { SubmitPromptOptions } from "./useChatStreamingController"
import
{
HOME_CHAT_PROJECT_ID
,
HOME_EXPERT_SUGGESTION_PROJECT_IDS
}
from
"../../lib/constants"
import
{
HOME_CHAT_PROJECT_ID
,
HOME_EXPERT_SUGGESTION_PROJECT_IDS
}
from
"../../lib/constants"
import
{
shouldOfferHomeExpertSwitch
}
from
"../../lib/chat-utils"
import
{
shouldOfferHomeExpertSwitch
}
from
"../../lib/chat-utils"
type
ViewMode
=
"chat"
|
"experts"
|
"tasks"
|
"plugins"
|
"settings"
|
"knowledge"
type
ViewMode
=
"chat"
|
"experts"
|
"tasks"
|
"
automation"
|
"
plugins"
|
"settings"
|
"knowledge"
interface
ResumePromptOptions
{
interface
ResumePromptOptions
{
skipHomeIntentSuggestion
?:
boolean
skipHomeIntentSuggestion
?:
boolean
...
...
apps/ui/src/features/shell/AppSidebar.tsx
View file @
5f36e0b1
...
@@ -4,7 +4,7 @@ import { Sidebar } from "./Sidebar"
...
@@ -4,7 +4,7 @@ import { Sidebar } from "./Sidebar"
import
{
ExpertTree
,
type
ExpertCategoryId
,
type
ExpertVisualKey
,
type
SidebarExpertEntry
}
from
"./ExpertTree"
import
{
ExpertTree
,
type
ExpertCategoryId
,
type
ExpertVisualKey
,
type
SidebarExpertEntry
}
from
"./ExpertTree"
import
{
SessionList
}
from
"./SessionList"
import
{
SessionList
}
from
"./SessionList"
type
ViewMode
=
"chat"
|
"experts"
|
"tasks"
|
"plugins"
|
"settings"
|
"knowledge"
type
ViewMode
=
"chat"
|
"experts"
|
"tasks"
|
"
automation"
|
"
plugins"
|
"settings"
|
"knowledge"
type
SendPhase
=
"idle"
|
"preparing"
|
"streaming"
|
"finalizing"
type
SendPhase
=
"idle"
|
"preparing"
|
"streaming"
|
"finalizing"
interface
AppSidebarProps
{
interface
AppSidebarProps
{
...
@@ -76,6 +76,7 @@ export function AppSidebar({
...
@@ -76,6 +76,7 @@ export function AppSidebar({
{
id
:
"chat"
as
const
,
label
:
"对话"
},
{
id
:
"chat"
as
const
,
label
:
"对话"
},
{
id
:
"tasks"
as
const
,
label
:
"工作台"
},
{
id
:
"tasks"
as
const
,
label
:
"工作台"
},
{
id
:
"knowledge"
as
const
,
label
:
ui
.
knowledge
},
{
id
:
"knowledge"
as
const
,
label
:
ui
.
knowledge
},
{
id
:
"automation"
as
const
,
label
:
"自动化任务"
},
{
id
:
"settings"
as
const
,
label
:
ui
.
settings
}
{
id
:
"settings"
as
const
,
label
:
ui
.
settings
}
].
map
((
item
)
=>
(
].
map
((
item
)
=>
(
<
button
<
button
...
...
apps/ui/src/features/shell/ExpertTree.tsx
View file @
5f36e0b1
...
@@ -104,7 +104,7 @@ function expertMatchesCategory(entry: SidebarExpertEntry, categoryId: ExpertCate
...
@@ -104,7 +104,7 @@ function expertMatchesCategory(entry: SidebarExpertEntry, categoryId: ExpertCate
interface
ExpertTreeProps
{
interface
ExpertTreeProps
{
entries
:
SidebarExpertEntry
[]
entries
:
SidebarExpertEntry
[]
expandedCategories
:
Record
<
string
,
boolean
>
expandedCategories
:
Record
<
string
,
boolean
>
viewMode
:
"chat"
|
"experts"
|
"tasks"
|
"plugins"
|
"settings"
|
"knowledge"
viewMode
:
"chat"
|
"experts"
|
"tasks"
|
"
automation"
|
"
plugins"
|
"settings"
|
"knowledge"
prompt
:
string
prompt
:
string
activeProjectId
?:
string
activeProjectId
?:
string
onToggleCategory
(
categoryId
:
string
):
void
onToggleCategory
(
categoryId
:
string
):
void
...
...
apps/ui/src/features/shell/useHomeNavigation.ts
View file @
5f36e0b1
...
@@ -3,7 +3,7 @@ import type { DesktopApi, ExpertDefinition, WorkspaceSummary } from "@qjclaw/sha
...
@@ -3,7 +3,7 @@ import type { DesktopApi, ExpertDefinition, WorkspaceSummary } from "@qjclaw/sha
import
{
EMPTY_SESSION_ID
,
HOME_CHAT_PROJECT_ID
}
from
"../../lib/constants"
import
{
EMPTY_SESSION_ID
,
HOME_CHAT_PROJECT_ID
}
from
"../../lib/constants"
import
type
{
SidebarExpertEntry
}
from
"./ExpertTree"
import
type
{
SidebarExpertEntry
}
from
"./ExpertTree"
type
ViewMode
=
"chat"
|
"experts"
|
"tasks"
|
"plugins"
|
"settings"
|
"knowledge"
type
ViewMode
=
"chat"
|
"experts"
|
"tasks"
|
"
automation"
|
"
plugins"
|
"settings"
|
"knowledge"
interface
UseHomeNavigationDeps
{
interface
UseHomeNavigationDeps
{
desktopApi
:
DesktopApi
desktopApi
:
DesktopApi
...
...
apps/ui/src/features/shell/useSidebarModel.ts
View file @
5f36e0b1
...
@@ -14,7 +14,7 @@ import {
...
@@ -14,7 +14,7 @@ import {
buildStandaloneExpertEntries
buildStandaloneExpertEntries
}
from
"./expertEntries"
}
from
"./expertEntries"
type
ViewMode
=
"chat"
|
"experts"
|
"tasks"
|
"plugins"
|
"settings"
|
"knowledge"
type
ViewMode
=
"chat"
|
"experts"
|
"tasks"
|
"
automation"
|
"
plugins"
|
"settings"
|
"knowledge"
interface
UseSidebarModelOptions
{
interface
UseSidebarModelOptions
{
workspace
:
WorkspaceSummary
|
null
workspace
:
WorkspaceSummary
|
null
...
...
apps/ui/src/features/smoke/useSmokeActionHandlers.ts
View file @
5f36e0b1
...
@@ -4,7 +4,7 @@ import { HOME_CHAT_PROJECT_ID } from "../../lib/constants"
...
@@ -4,7 +4,7 @@ import { HOME_CHAT_PROJECT_ID } from "../../lib/constants"
import
type
{
SubmitPromptOptions
}
from
"../chat/useChatStreamingController"
import
type
{
SubmitPromptOptions
}
from
"../chat/useChatStreamingController"
import
{
waitForSmokeUiReady
}
from
"./useSmokeActions"
import
{
waitForSmokeUiReady
}
from
"./useSmokeActions"
type
ViewMode
=
"chat"
|
"experts"
|
"tasks"
|
"plugins"
|
"settings"
|
"knowledge"
type
ViewMode
=
"chat"
|
"experts"
|
"tasks"
|
"
automation"
|
"
plugins"
|
"settings"
|
"knowledge"
type
ExpertProject
=
WorkspaceSummary
[
"projects"
][
number
]
type
ExpertProject
=
WorkspaceSummary
[
"projects"
][
number
]
type
SidebarExpertEntry
=
{
type
SidebarExpertEntry
=
{
definition
:
ExpertDefinition
definition
:
ExpertDefinition
...
...
apps/ui/src/features/smoke/useSmokeActions.ts
View file @
5f36e0b1
import
{
useEffect
}
from
"react"
import
{
useEffect
}
from
"react"
import
{
smokeEnabled
}
from
"../../lib/desktop-api"
import
{
smokeEnabled
}
from
"../../lib/desktop-api"
type
ViewMode
=
"chat"
|
"experts"
|
"tasks"
|
"plugins"
|
"settings"
|
"knowledge"
type
ViewMode
=
"chat"
|
"experts"
|
"tasks"
|
"
automation"
|
"
plugins"
|
"settings"
|
"knowledge"
export
async
function
waitForSmokeUiReady
(
targetView
:
ViewMode
,
timeoutMs
=
10000
)
{
export
async
function
waitForSmokeUiReady
(
targetView
:
ViewMode
,
timeoutMs
=
10000
)
{
const
started
=
Date
.
now
()
const
started
=
Date
.
now
()
...
...
apps/ui/src/features/smoke/useSmokeSnapshot.ts
View file @
5f36e0b1
...
@@ -15,7 +15,7 @@ import type {
...
@@ -15,7 +15,7 @@ import type {
import
{
isMockDesktopApi
,
smokeEnabled
}
from
"../../lib/desktop-api"
import
{
isMockDesktopApi
,
smokeEnabled
}
from
"../../lib/desktop-api"
import
type
{
SmokeStreamSnapshot
}
from
"./types"
import
type
{
SmokeStreamSnapshot
}
from
"./types"
type
ViewMode
=
"chat"
|
"experts"
|
"tasks"
|
"plugins"
|
"settings"
|
"knowledge"
type
ViewMode
=
"chat"
|
"experts"
|
"tasks"
|
"
automation"
|
"
plugins"
|
"settings"
|
"knowledge"
export
interface
SmokeUiSnapshot
{
export
interface
SmokeUiSnapshot
{
shellReady
:
boolean
shellReady
:
boolean
...
...
apps/ui/src/lib/mock-desktop-api.ts
View file @
5f36e0b1
import
type
{
import
type
{
AutomationTask
,
AutomationTaskRun
,
ChatAttachment
,
ChatAttachment
,
ChatStreamEvent
,
ChatStreamEvent
,
ChatStreamListener
,
ChatStreamListener
,
CreateAutomationTaskInput
,
DesktopApi
,
DesktopApi
,
DiagnosticsExportResult
,
DiagnosticsExportResult
,
ExpertDefinition
,
ExpertDefinition
,
ProjectIntentSuggestion
,
ProjectIntentSuggestion
,
SaveConfigInput
,
SaveConfigInput
,
UpdateAutomationTaskInput
,
WorkspaceSummary
WorkspaceSummary
}
from
"@qjclaw/shared-types"
}
from
"@qjclaw/shared-types"
import
{
import
{
...
@@ -163,6 +167,59 @@ const mockProjectIntentSuggestions: Record<string, ProjectIntentSuggestion> = {
...
@@ -163,6 +167,59 @@ const mockProjectIntentSuggestions: Record<string, ProjectIntentSuggestion> = {
}
}
};
};
let
mockAutomationTasks
:
AutomationTask
[]
=
[
{
id
:
"mock-automation-daily-report"
,
title
:
"每日内容复盘"
,
prompt
:
"整理昨天内容表现,并给出今天的优化建议。"
,
expertId
:
"content-account-planner"
,
expertName
:
"内容账号规划专家"
,
enabled
:
true
,
schedule
:
{
kind
:
"daily"
,
time
:
"09:30"
},
nextRunAt
:
new
Date
(
Date
.
now
()
+
60
*
60
*
1000
).
toISOString
(),
lastRunStatus
:
"completed"
,
lastRunAt
:
new
Date
(
Date
.
now
()
-
24
*
60
*
60
*
1000
).
toISOString
(),
createdAt
:
new
Date
(
Date
.
now
()
-
3
*
24
*
60
*
60
*
1000
).
toISOString
(),
updatedAt
:
new
Date
().
toISOString
()
}
];
let
mockAutomationRuns
:
AutomationTaskRun
[]
=
[
{
id
:
"mock-automation-run-1"
,
taskId
:
"mock-automation-daily-report"
,
trigger
:
"schedule"
,
status
:
"completed"
,
scheduledFor
:
new
Date
(
Date
.
now
()
-
24
*
60
*
60
*
1000
).
toISOString
(),
startedAt
:
new
Date
(
Date
.
now
()
-
24
*
60
*
60
*
1000
).
toISOString
(),
completedAt
:
new
Date
(
Date
.
now
()
-
24
*
60
*
60
*
1000
+
5000
).
toISOString
(),
sessionId
:
"project:content-account-planner:auto"
,
runId
:
"mock-chat-run-1"
,
replyText
:
"已完成昨日内容复盘,并整理了今日优化建议。"
,
artifacts
:
[]
}
];
function
getMockAutomationNextRunAt
(
enabled
:
boolean
)
{
return
enabled
?
new
Date
(
Date
.
now
()
+
60
*
60
*
1000
).
toISOString
()
:
undefined
}
function
createMockAutomationTask
(
input
:
CreateAutomationTaskInput
):
AutomationTask
{
const
createdAt
=
new
Date
().
toISOString
()
return
{
id
:
createClientMessageId
(
"automation-task"
),
title
:
input
.
title
.
trim
()
||
"未命名自动化任务"
,
prompt
:
input
.
prompt
.
trim
(),
expertId
:
input
.
expertId
,
expertName
:
input
.
expertName
,
enabled
:
input
.
enabled
,
schedule
:
input
.
schedule
,
nextRunAt
:
getMockAutomationNextRunAt
(
input
.
enabled
),
createdAt
,
updatedAt
:
createdAt
}
}
function
resolveMockProjectIntentSuggestion
(
prompt
:
string
):
ProjectIntentSuggestion
|
null
{
function
resolveMockProjectIntentSuggestion
(
prompt
:
string
):
ProjectIntentSuggestion
|
null
{
const
normalized
=
prompt
.
trim
().
toLowerCase
();
const
normalized
=
prompt
.
trim
().
toLowerCase
();
if
(
!
normalized
)
{
if
(
!
normalized
)
{
...
@@ -382,6 +439,67 @@ export const mockDesktopApi = {
...
@@ -382,6 +439,67 @@ export const mockDesktopApi = {
modelConfig
:
{
getSummary
:
async
()
=>
({
source
:
"cloud"
,
updatedAt
:
new
Date
().
toISOString
(),
fetchedAt
:
new
Date
().
toISOString
(),
routingMode
:
"platform-managed"
,
fallbackMode
:
"cloud-required"
,
defaultChatModelId
:
"gpt-5.4-mini"
,
defaultChatModelLabel
:
"GPT-5.4 Mini"
,
items
:
[],
skillBindings
:
[],
message
:
"mock"
})
},
modelConfig
:
{
getSummary
:
async
()
=>
({
source
:
"cloud"
,
updatedAt
:
new
Date
().
toISOString
(),
fetchedAt
:
new
Date
().
toISOString
(),
routingMode
:
"platform-managed"
,
fallbackMode
:
"cloud-required"
,
defaultChatModelId
:
"gpt-5.4-mini"
,
defaultChatModelLabel
:
"GPT-5.4 Mini"
,
items
:
[],
skillBindings
:
[],
message
:
"mock"
})
},
system
:
{
getSummary
:
async
()
=>
({
appName
:
"千匠问天"
,
appVersion
:
"0.1.0"
,
isPackaged
:
false
,
platform
:
"win32"
,
arch
:
"x64"
,
appPath
:
"D:/qjclaw/apps/desktop"
,
resourcesPath
:
"D:/qjclaw/apps/desktop/dist"
,
userDataPath
:
"D:/qjclaw/.tmp/user-data"
,
logsPath
:
"D:/qjclaw/.tmp/logs"
})
},
system
:
{
getSummary
:
async
()
=>
({
appName
:
"千匠问天"
,
appVersion
:
"0.1.0"
,
isPackaged
:
false
,
platform
:
"win32"
,
arch
:
"x64"
,
appPath
:
"D:/qjclaw/apps/desktop"
,
resourcesPath
:
"D:/qjclaw/apps/desktop/dist"
,
userDataPath
:
"D:/qjclaw/.tmp/user-data"
,
logsPath
:
"D:/qjclaw/.tmp/logs"
})
},
tasks
:
{
listByDate
:
async
()
=>
[]
},
tasks
:
{
listByDate
:
async
()
=>
[]
},
automationTasks
:
{
list
:
async
()
=>
[...
mockAutomationTasks
],
create
:
async
(
input
:
CreateAutomationTaskInput
)
=>
{
const
task
=
createMockAutomationTask
(
input
)
mockAutomationTasks
=
[
task
,
...
mockAutomationTasks
]
return
task
},
update
:
async
(
taskId
:
string
,
input
:
UpdateAutomationTaskInput
)
=>
{
const
index
=
mockAutomationTasks
.
findIndex
((
task
)
=>
task
.
id
===
taskId
)
if
(
index
<
0
)
{
return
null
}
const
current
=
mockAutomationTasks
[
index
]
!
const
enabled
=
input
.
enabled
??
current
.
enabled
const
updated
:
AutomationTask
=
{
...
current
,
title
:
input
.
title
??
current
.
title
,
prompt
:
input
.
prompt
??
current
.
prompt
,
expertId
:
input
.
expertId
===
null
?
undefined
:
input
.
expertId
??
current
.
expertId
,
expertName
:
input
.
expertName
===
null
?
undefined
:
input
.
expertName
??
current
.
expertName
,
enabled
,
schedule
:
input
.
schedule
??
current
.
schedule
,
nextRunAt
:
getMockAutomationNextRunAt
(
enabled
),
updatedAt
:
new
Date
().
toISOString
()
}
mockAutomationTasks
[
index
]
=
updated
return
updated
},
delete
:
async
(
taskId
:
string
)
=>
{
const
before
=
mockAutomationTasks
.
length
mockAutomationTasks
=
mockAutomationTasks
.
filter
((
task
)
=>
task
.
id
!==
taskId
)
mockAutomationRuns
=
mockAutomationRuns
.
filter
((
run
)
=>
run
.
taskId
!==
taskId
)
return
mockAutomationTasks
.
length
!==
before
},
runNow
:
async
(
taskId
:
string
)
=>
{
const
task
=
mockAutomationTasks
.
find
((
candidate
)
=>
candidate
.
id
===
taskId
)
if
(
!
task
)
{
throw
new
Error
(
"Automation task not found"
)
}
const
now
=
new
Date
().
toISOString
()
const
run
:
AutomationTaskRun
=
{
id
:
createClientMessageId
(
"automation-run"
),
taskId
,
trigger
:
"manual"
,
status
:
"completed"
,
startedAt
:
now
,
completedAt
:
now
,
sessionId
:
`project:
${
task
.
expertId
??
HOME_CHAT_PROJECT_ID
}:
auto
`,
runId: createClientMessageId("mock-run"),
replyText: "Mock: " + task.prompt,
artifacts: []
}
mockAutomationRuns = [run, ...mockAutomationRuns]
mockAutomationTasks = mockAutomationTasks.map((candidate) => candidate.id === taskId
? { ...candidate, lastRunAt: now, lastRunStatus: "completed", updatedAt: now }
: candidate
)
return run
},
listRuns: async (taskId?: string) => mockAutomationRuns.filter((run) => !taskId || run.taskId === taskId)
},
chat: {
chat: {
listSessions: async () => getMockSessions(),
listSessions: async () => getMockSessions(),
listSessionsByProject: async (projectId: string) => getMockSessions(projectId),
listSessionsByProject: async (projectId: string) => getMockSessions(projectId),
...
...
apps/ui/src/styles.css
View file @
5f36e0b1
...
@@ -6,4 +6,5 @@
...
@@ -6,4 +6,5 @@
@import
"./styles/plugins.css"
;
@import
"./styles/plugins.css"
;
@import
"./styles/knowledge.css"
;
@import
"./styles/knowledge.css"
;
@import
"./styles/tasks.css"
;
@import
"./styles/tasks.css"
;
@import
"./styles/automation.css"
;
@import
"./styles/theme-openclaw.css"
;
@import
"./styles/theme-openclaw.css"
;
apps/ui/src/styles/automation.css
0 → 100644
View file @
5f36e0b1
.automation-page-stack
{
height
:
100%
;
display
:
flex
;
min-height
:
0
;
}
.automation-page
{
flex
:
1
;
display
:
grid
;
grid-template-rows
:
auto
minmax
(
0
,
1
fr
);
min-height
:
0
;
overflow
:
hidden
;
border-radius
:
18px
;
}
.automation-page-body
{
display
:
grid
;
grid-template-rows
:
auto
minmax
(
0
,
1
fr
);
gap
:
14px
;
min-height
:
0
;
padding
:
0
;
}
.automation-header
,
.automation-header-actions
,
.automation-detail-actions
,
.automation-form-actions
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
gap
:
10px
;
min-width
:
0
;
}
.automation-header-actions
,
.automation-detail-actions
,
.automation-form-actions
{
flex-wrap
:
wrap
;
}
.automation-detail-actions
{
justify-content
:
flex-end
;
}
.automation-action-button
,
.automation-delete-button
{
display
:
inline-flex
;
align-items
:
center
;
justify-content
:
center
;
gap
:
6px
;
white-space
:
nowrap
;
}
.automation-action-button
svg
,
.automation-delete-button
svg
{
width
:
15px
;
height
:
15px
;
}
.automation-button-primary
{
border
:
1px
solid
rgba
(
37
,
99
,
235
,
0.22
);
background
:
#2563eb
;
color
:
#ffffff
;
box-shadow
:
0
8px
18px
rgba
(
37
,
99
,
235
,
0.16
);
}
.automation-button-primary
:hover:not
(
:disabled
),
.automation-button-primary
:focus-visible
{
background
:
#1d4ed8
;
}
.automation-button-secondary
{
border
:
1px
solid
rgba
(
203
,
213
,
225
,
0.92
);
background
:
#ffffff
;
color
:
#1f2937
;
}
.automation-button-secondary
:hover:not
(
:disabled
),
.automation-button-secondary
:focus-visible
{
border-color
:
rgba
(
147
,
197
,
253
,
0.95
);
background
:
#eff6ff
;
color
:
#1d4ed8
;
}
.automation-button-accent
{
border
:
1px
solid
rgba
(
7
,
193
,
96
,
0.42
);
background
:
#07C160
;
color
:
#ffffff
;
box-shadow
:
0
6px
14px
rgba
(
7
,
193
,
96
,
0.18
);
}
.automation-button-accent
:hover:not
(
:disabled
),
.automation-button-accent
:focus-visible
{
border-color
:
rgba
(
6
,
174
,
86
,
0.54
);
background
:
#06AD56
;
color
:
#ffffff
;
}
.automation-delete-button
{
border
:
1px
solid
rgba
(
248
,
113
,
113
,
0.42
);
background
:
rgba
(
254
,
242
,
242
,
0.96
);
color
:
#dc2626
;
cursor
:
pointer
;
}
.automation-delete-button
:hover:not
(
:disabled
),
.automation-delete-button
:focus-visible
{
border-color
:
rgba
(
220
,
38
,
38
,
0.52
);
background
:
#fee2e2
;
}
.automation-header
h1
,
.automation-detail-head
h2
,
.automation-runs
h3
{
margin
:
0
;
color
:
#111827
;
letter-spacing
:
0
;
}
.automation-header
h1
{
font-size
:
20px
;
line-height
:
1.2
;
}
.automation-header
p
,
.automation-detail-head
p
,
.automation-run-item
p
{
margin
:
5px
0
0
;
color
:
#64748b
;
font-size
:
13px
;
line-height
:
1.5
;
}
.automation-layout
{
display
:
grid
;
grid-template-columns
:
minmax
(
260px
,
0.8
fr
)
minmax
(
0
,
1.6
fr
);
gap
:
14px
;
min-height
:
0
;
}
.automation-list-pane
,
.automation-detail-pane
,
.automation-form
{
min-width
:
0
;
min-height
:
0
;
border
:
1px
solid
rgba
(
203
,
213
,
225
,
0.86
);
border-radius
:
16px
;
background
:
rgba
(
255
,
255
,
255
,
0.82
);
}
.automation-list-pane
{
display
:
grid
;
grid-template-rows
:
auto
minmax
(
0
,
1
fr
);
gap
:
10px
;
padding
:
12px
;
}
.automation-filter-row
{
display
:
grid
;
grid-template-columns
:
repeat
(
3
,
minmax
(
0
,
1
fr
));
gap
:
6px
;
}
.automation-filter-row
button
{
min-height
:
32px
;
border
:
1px
solid
rgba
(
203
,
213
,
225
,
0.88
);
border-radius
:
8px
;
background
:
#ffffff
;
color
:
#475569
;
font-size
:
12px
;
font-weight
:
700
;
cursor
:
pointer
;
}
.automation-filter-row
button
.active
{
border-color
:
#93c5fd
;
background
:
#eff6ff
;
color
:
#1d4ed8
;
}
.automation-list-scroll
{
min-height
:
0
;
}
.automation-task-list
,
.automation-run-list
{
display
:
grid
;
gap
:
8px
;
}
.automation-task-row
{
min-height
:
70px
;
padding
:
12px
;
border
:
1px
solid
rgba
(
226
,
232
,
240
,
0.9
);
border-radius
:
12px
;
background
:
#ffffff
;
color
:
inherit
;
text-align
:
left
;
}
.automation-task-row.active
{
border-color
:
#60a5fa
;
background
:
linear-gradient
(
180deg
,
#eff6ff
,
#ffffff
);
}
.automation-task-select
{
display
:
grid
;
gap
:
6px
;
width
:
100%
;
min-height
:
46px
;
min-width
:
0
;
border
:
0
;
background
:
transparent
;
color
:
inherit
;
text-align
:
left
;
cursor
:
pointer
;
}
.automation-task-select
:focus-visible
{
outline
:
2px
solid
rgba
(
37
,
99
,
235
,
0.45
);
outline-offset
:
3px
;
}
.automation-task-select
strong
,
.automation-task-select
small
{
overflow
:
hidden
;
}
.automation-task-select
strong
{
color
:
#0f172a
;
font-size
:
13px
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
}
.automation-task-select
small
{
display
:
-webkit-box
;
-webkit-box-orient
:
vertical
;
-webkit-line-clamp
:
2
;
color
:
#64748b
;
font-size
:
12px
;
line-height
:
1.35
;
}
.automation-detail-pane
{
display
:
grid
;
grid-template-rows
:
auto
auto
minmax
(
0
,
1
fr
);
gap
:
12px
;
padding
:
16px
;
overflow
:
hidden
;
}
.automation-detail-head
{
display
:
grid
;
grid-template-columns
:
minmax
(
0
,
1
fr
)
auto
;
gap
:
12px
;
}
.automation-detail-head
h2
{
margin-top
:
6px
;
font-size
:
20px
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
}
.automation-detail-prompt
{
display
:
-webkit-box
;
max-width
:
100%
;
overflow
:
hidden
;
-webkit-box-orient
:
vertical
;
-webkit-line-clamp
:
3
;
}
.automation-kicker
{
display
:
inline-flex
;
max-width
:
100%
;
min-height
:
24px
;
align-items
:
center
;
padding
:
0
9px
;
border-radius
:
999px
;
background
:
#fef3c7
;
color
:
#92400e
;
font-size
:
12px
;
font-weight
:
800
;
}
.automation-meta-grid
{
display
:
grid
;
grid-template-columns
:
repeat
(
3
,
minmax
(
0
,
1
fr
));
gap
:
8px
;
}
.automation-meta-grid
div
{
display
:
grid
;
gap
:
4px
;
min-width
:
0
;
padding
:
12px
;
border-radius
:
12px
;
background
:
#f8fafc
;
border
:
1px
solid
rgba
(
226
,
232
,
240
,
0.9
);
}
.automation-meta-grid
span
{
color
:
#64748b
;
font-size
:
12px
;
font-weight
:
700
;
}
.automation-meta-grid
strong
{
overflow
:
hidden
;
color
:
#1f2937
;
font-size
:
13px
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
}
.automation-runs
{
display
:
grid
;
grid-template-rows
:
auto
minmax
(
0
,
1
fr
);
gap
:
8px
;
min-height
:
0
;
}
.automation-runs
h3
{
font-size
:
15px
;
}
.automation-run-list
{
overflow
:
auto
;
min-height
:
0
;
padding-right
:
2px
;
}
.automation-run-item
{
padding
:
12px
;
border
:
1px
solid
rgba
(
226
,
232
,
240
,
0.9
);
border-radius
:
12px
;
background
:
#ffffff
;
}
.automation-run-meta
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
gap
:
10px
;
color
:
#64748b
;
font-size
:
12px
;
}
.automation-run-error
{
color
:
#b45309
!important
;
}
.automation-run-markdown
{
margin-top
:
8px
;
color
:
#334155
;
font-size
:
13px
;
line-height
:
1.5
;
}
.automation-run-markdown
>
*
{
margin-top
:
6px
;
margin-bottom
:
0
;
}
.automation-run-markdown
>
:first-child
{
margin-top
:
0
;
}
.automation-run-markdown
.markdown-code-block
{
margin-top
:
8px
;
}
.automation-empty
{
display
:
grid
;
min-height
:
120px
;
place-items
:
center
;
color
:
#64748b
;
font-size
:
13px
;
font-weight
:
700
;
}
.automation-form
{
display
:
grid
;
grid-template-rows
:
minmax
(
0
,
1
fr
)
auto
;
gap
:
12px
;
padding
:
14px
;
}
.automation-form-grid
{
display
:
grid
;
grid-template-columns
:
repeat
(
2
,
minmax
(
0
,
1
fr
));
gap
:
12px
;
min-width
:
0
;
overflow
:
auto
;
}
.automation-form-grid
label
{
display
:
grid
;
gap
:
6px
;
min-width
:
0
;
}
.automation-form-grid
label
>
span
,
.automation-weekdays
>
label
span
,
.automation-enabled-toggle
span
{
color
:
#475569
;
font-size
:
12px
;
font-weight
:
750
;
}
.automation-form-grid
input
,
.automation-form-grid
select
,
.automation-form-grid
textarea
{
width
:
100%
;
min-width
:
0
;
border
:
1px
solid
rgba
(
203
,
213
,
225
,
0.95
);
border-radius
:
10px
;
background
:
#ffffff
;
color
:
#111827
;
font
:
inherit
;
font-size
:
13px
;
}
.automation-form-grid
input
,
.automation-form-grid
select
{
height
:
38px
;
padding
:
0
10px
;
}
.automation-form-grid
textarea
{
resize
:
vertical
;
min-height
:
120px
;
padding
:
10px
;
line-height
:
1.5
;
}
.automation-form-full
{
grid-column
:
1
/
-1
;
}
.automation-weekdays
{
display
:
grid
;
grid-template-columns
:
repeat
(
7
,
minmax
(
0
,
1
fr
));
gap
:
6px
;
}
.automation-weekdays
label
,
.automation-enabled-toggle
{
display
:
inline-flex
!important
;
align-items
:
center
;
justify-content
:
center
;
min-height
:
34px
;
padding
:
0
8px
;
border
:
1px
solid
rgba
(
203
,
213
,
225
,
0.9
);
border-radius
:
10px
;
background
:
#ffffff
;
}
.automation-weekdays
input
,
.automation-enabled-toggle
input
{
width
:
14px
;
height
:
14px
;
}
.automation-enabled-toggle
{
justify-content
:
flex-start
;
gap
:
8px
!important
;
}
@media
(
max-width
:
980px
)
{
.automation-layout
,
.automation-detail-head
{
grid-template-columns
:
1
fr
;
}
.automation-meta-grid
,
.automation-form-grid
{
grid-template-columns
:
1
fr
;
}
}
@media
(
max-width
:
720px
)
{
.automation-header
,
.automation-detail-actions
{
align-items
:
stretch
;
flex-direction
:
column
;
}
.automation-header-actions
,
.automation-detail-actions
,
.automation-form-actions
{
align-items
:
stretch
;
}
.automation-action-button
,
.automation-delete-button
{
width
:
100%
;
}
.automation-task-row
{
grid-template-columns
:
1
fr
;
}
.automation-weekdays
{
grid-template-columns
:
repeat
(
4
,
minmax
(
0
,
1
fr
));
}
}
apps/ui/test/automationTasksSource.test.ts
0 → 100644
View file @
5f36e0b1
import
test
from
"node:test"
import
assert
from
"node:assert/strict"
import
{
readFileSync
}
from
"node:fs"
const
appSource
=
readFileSync
(
new
URL
(
"../src/App.tsx"
,
import
.
meta
.
url
),
"utf8"
)
const
sidebarSource
=
readFileSync
(
new
URL
(
"../src/features/shell/AppSidebar.tsx"
,
import
.
meta
.
url
),
"utf8"
)
const
mockApiSource
=
readFileSync
(
new
URL
(
"../src/lib/mock-desktop-api.ts"
,
import
.
meta
.
url
),
"utf8"
)
const
desktopApiSource
=
readFileSync
(
new
URL
(
"../src/lib/desktop-api.ts"
,
import
.
meta
.
url
),
"utf8"
)
const
automationStyles
=
readFileSync
(
new
URL
(
"../src/styles/automation.css"
,
import
.
meta
.
url
),
"utf8"
)
test
(
"automation tasks view is available from the sidebar above settings"
,
()
=>
{
assert
.
match
(
appSource
,
/AutomationTasksView/
)
assert
.
match
(
appSource
,
/viewMode === "automation"/
)
assert
.
match
(
sidebarSource
,
/id: "automation" as const, label: "自动化任务"/
)
assert
.
match
(
sidebarSource
,
/id: "settings" as const/
)
assert
.
ok
(
sidebarSource
.
indexOf
(
'id: "automation"'
)
<
sidebarSource
.
indexOf
(
'id: "settings"'
))
})
test
(
"mock desktop API exposes automation task methods for UI development"
,
()
=>
{
assert
.
match
(
desktopApiSource
,
/mockDesktopApi/
)
assert
.
match
(
mockApiSource
,
/automationTasks:
\s
*
\{
/
)
assert
.
match
(
mockApiSource
,
/listRuns/
)
assert
.
match
(
mockApiSource
,
/runNow/
)
})
test
(
"automation task destructive and utility actions are explicit"
,
()
=>
{
const
viewSource
=
readFileSync
(
new
URL
(
"../src/features/automation/AutomationTasksView.tsx"
,
import
.
meta
.
url
),
"utf8"
)
assert
.
match
(
viewSource
,
/确认删除自动化任务/
)
assert
.
match
(
viewSource
,
/>刷新</
)
assert
.
match
(
viewSource
,
/>新建任务</
)
assert
.
match
(
viewSource
,
/>编辑</
)
assert
.
match
(
viewSource
,
/>立即执行</
)
assert
.
match
(
viewSource
,
/>删除</
)
assert
.
doesNotMatch
(
viewSource
,
/automation-button-toggle/
)
assert
.
doesNotMatch
(
viewSource
,
/aria-label="删除自动化任务" onClick=
\{\(\)
=> void deleteTask/
)
})
test
(
"automation task view exposes lifecycle text without quick enable and disable controls"
,
()
=>
{
const
viewSource
=
readFileSync
(
new
URL
(
"../src/features/automation/AutomationTasksView.tsx"
,
import
.
meta
.
url
),
"utf8"
)
assert
.
doesNotMatch
(
viewSource
,
/toggleTaskEnabled/
)
assert
.
doesNotMatch
(
viewSource
,
/automationTasks
\.
update
\(
task
\.
id,
\s
*
\{\s
*enabled: !task
\.
enabled
\s
*
\}\)
/
s
)
assert
.
doesNotMatch
(
viewSource
,
/automation-task-row-actions/
)
assert
.
doesNotMatch
(
viewSource
,
/automation-row-toggle-button/
)
assert
.
match
(
viewSource
,
/已完成,已停用/
)
assert
.
match
(
viewSource
,
/已错过,已停用/
)
assert
.
match
(
viewSource
,
/已停用/
)
})
test
(
"automation task list shows only task title and prompt"
,
()
=>
{
const
viewSource
=
readFileSync
(
new
URL
(
"../src/features/automation/AutomationTasksView.tsx"
,
import
.
meta
.
url
),
"utf8"
)
assert
.
match
(
viewSource
,
/<strong>
\{
task
\.
title
\}
<
\/
strong>/
)
assert
.
match
(
viewSource
,
/<small>
\{
task
\.
prompt
\}
<
\/
small>/
)
assert
.
doesNotMatch
(
viewSource
,
/下次执行:
\{
getTaskNextRunLabel
\(
task
\)\}
/
)
assert
.
doesNotMatch
(
viewSource
,
/<StatusChip tone=
\{
getTaskLifecycleTone
\(
task
\)\}
>
\{
getTaskLifecycleLabel
\(
task
\)\}
<
\/
StatusChip>/
)
})
test
(
"automation task edit can clear the selected expert"
,
()
=>
{
const
viewSource
=
readFileSync
(
new
URL
(
"../src/features/automation/AutomationTasksView.tsx"
,
import
.
meta
.
url
),
"utf8"
)
assert
.
match
(
viewSource
,
/const expertId = form
\.
expertId
\|\|
\(
form
\.
id
\?
null : undefined
\)
/
)
assert
.
match
(
viewSource
,
/const expertName = form
\.
expertId
\?
selectedExpert
\?\.
label :
\(
form
\.
id
\?
null : undefined
\)
/
)
})
test
(
"automation run replies use shared markdown rendering"
,
()
=>
{
const
viewSource
=
readFileSync
(
new
URL
(
"../src/features/automation/AutomationTasksView.tsx"
,
import
.
meta
.
url
),
"utf8"
)
assert
.
match
(
viewSource
,
/renderChatMessageContent/
)
assert
.
match
(
viewSource
,
/markdown-body automation-run-markdown/
)
assert
.
match
(
viewSource
,
/className="automation-run-meta"/
)
assert
.
match
(
viewSource
,
/run
\.
replyText/
)
assert
.
doesNotMatch
(
viewSource
,
/
\{
run
\.
replyText
\}
<
\/
p>/
)
assert
.
match
(
automationStyles
,
/
\.
automation-run-list
\s
*
\{[^
}
]
*overflow:
\s
*auto/i
s
)
assert
.
doesNotMatch
(
automationStyles
,
/
\.
automation-run-item
\s
*>
\s
*div
\s
*
\{
/
)
assert
.
match
(
automationStyles
,
/
\.
automation-detail-pane
\s
*
\{[^
}
]
*grid-template-rows:
\s
*auto auto minmax
\(
0,
\s
*1fr
\)
/i
s
)
assert
.
match
(
automationStyles
,
/
\.
automation-runs
\s
*
\{[^
}
]
*grid-template-rows:
\s
*auto minmax
\(
0,
\s
*1fr
\)
/i
s
)
assert
.
match
(
automationStyles
,
/
\.
automation-page
\s
*
\{[^
}
]
*display:
\s
*grid
[^
}
]
*grid-template-rows:
\s
*auto minmax
\(
0,
\s
*1fr
\)
/i
s
)
assert
.
match
(
automationStyles
,
/
\.
automation-run-meta
\s
*
\{[^
}
]
*display:
\s
*flex/i
s
)
})
test
(
"run-now action uses WeChat green accent"
,
()
=>
{
assert
.
match
(
automationStyles
,
/
\.
automation-button-accent
\s
*
\{[^
}
]
*#07C160/i
s
)
})
packages/shared-types/src/index.ts
View file @
5f36e0b1
...
@@ -51,7 +51,13 @@
...
@@ -51,7 +51,13 @@
expertsList
:
"experts:list"
,
expertsList
:
"experts:list"
,
modelConfigGetSummary
:
"model-config:get-summary"
,
modelConfigGetSummary
:
"model-config:get-summary"
,
systemGetSummary
:
"system:get-summary"
,
systemGetSummary
:
"system:get-summary"
,
tasksListByDate
:
"tasks:list-by-date"
tasksListByDate
:
"tasks:list-by-date"
,
automationTasksList
:
"automation-tasks:list"
,
automationTasksCreate
:
"automation-tasks:create"
,
automationTasksUpdate
:
"automation-tasks:update"
,
automationTasksDelete
:
"automation-tasks:delete"
,
automationTasksRunNow
:
"automation-tasks:run-now"
,
automationTasksListRuns
:
"automation-tasks:list-runs"
}
as
const
;
}
as
const
;
export
type
GatewayState
=
"unknown"
|
"connecting"
|
"connected"
|
"disconnected"
|
"error"
;
export
type
GatewayState
=
"unknown"
|
"connecting"
|
"connected"
|
"disconnected"
|
"error"
;
...
@@ -81,6 +87,9 @@ export type SkillDownloadState = "pending" | "downloading" | "ready" | "failed"
...
@@ -81,6 +87,9 @@ export type SkillDownloadState = "pending" | "downloading" | "ready" | "failed"
export
type
ExpertEntryMode
=
"standalone"
|
"home-chat-shortcut"
;
export
type
ExpertEntryMode
=
"standalone"
|
"home-chat-shortcut"
;
export
type
DailyReportDeliveryState
=
"draft"
|
"sent"
|
"failed"
;
export
type
DailyReportDeliveryState
=
"draft"
|
"sent"
|
"failed"
;
export
type
TaskPanelStatus
=
"pending"
|
"running"
|
"completed"
|
"failed"
;
export
type
TaskPanelStatus
=
"pending"
|
"running"
|
"completed"
|
"failed"
;
export
type
AutomationTaskScheduleKind
=
"once"
|
"daily"
|
"weekly"
;
export
type
AutomationTaskRunTrigger
=
"schedule"
|
"manual"
|
"startup"
;
export
type
AutomationTaskRunStatus
=
"queued"
|
"running"
|
"completed"
|
"failed"
|
"missed"
;
export
type
ConfigSecretId
=
export
type
ConfigSecretId
=
|
"lobsterKey"
|
"lobsterKey"
|
"copywritingModelApiKey"
|
"copywritingModelApiKey"
...
@@ -905,6 +914,70 @@ export interface TaskPanelItem {
...
@@ -905,6 +914,70 @@ export interface TaskPanelItem {
artifacts
:
TaskPanelArtifact
[];
artifacts
:
TaskPanelArtifact
[];
}
}
export
type
AutomationTaskSchedule
=
|
{
kind
:
"once"
;
runAt
:
string
;
}
|
{
kind
:
"daily"
;
time
:
string
;
}
|
{
kind
:
"weekly"
;
time
:
string
;
weekdays
:
number
[];
};
export
interface
AutomationTask
{
id
:
string
;
title
:
string
;
prompt
:
string
;
expertId
?:
string
;
expertName
?:
string
;
enabled
:
boolean
;
schedule
:
AutomationTaskSchedule
;
nextRunAt
?:
string
;
lastRunAt
?:
string
;
lastRunStatus
?:
AutomationTaskRunStatus
;
lastRunError
?:
string
;
createdAt
:
string
;
updatedAt
:
string
;
}
export
interface
AutomationTaskRun
{
id
:
string
;
taskId
:
string
;
trigger
:
AutomationTaskRunTrigger
;
status
:
AutomationTaskRunStatus
;
scheduledFor
?:
string
;
startedAt
?:
string
;
completedAt
?:
string
;
sessionId
?:
string
;
runId
?:
string
;
replyText
?:
string
;
error
?:
string
;
artifacts
:
TaskPanelArtifact
[];
}
export
interface
CreateAutomationTaskInput
{
title
:
string
;
prompt
:
string
;
expertId
?:
string
;
expertName
?:
string
;
enabled
:
boolean
;
schedule
:
AutomationTaskSchedule
;
}
export
interface
UpdateAutomationTaskInput
{
title
?:
string
;
prompt
?:
string
;
expertId
?:
string
|
null
;
expertName
?:
string
|
null
;
enabled
?:
boolean
;
schedule
?:
AutomationTaskSchedule
;
}
export
interface
DesktopApi
{
export
interface
DesktopApi
{
workspace
:
{
workspace
:
{
getSummary
():
Promise
<
WorkspaceSummary
>
;
getSummary
():
Promise
<
WorkspaceSummary
>
;
...
@@ -978,6 +1051,14 @@ export interface DesktopApi {
...
@@ -978,6 +1051,14 @@ export interface DesktopApi {
tasks
:
{
tasks
:
{
listByDate
(
date
:
string
):
Promise
<
TaskPanelItem
[]
>
;
listByDate
(
date
:
string
):
Promise
<
TaskPanelItem
[]
>
;
};
};
automationTasks
:
{
list
():
Promise
<
AutomationTask
[]
>
;
create
(
input
:
CreateAutomationTaskInput
):
Promise
<
AutomationTask
>
;
update
(
taskId
:
string
,
input
:
UpdateAutomationTaskInput
):
Promise
<
AutomationTask
|
null
>
;
delete
(
taskId
:
string
):
Promise
<
boolean
>
;
runNow
(
taskId
:
string
):
Promise
<
AutomationTaskRun
>
;
listRuns
(
taskId
?:
string
):
Promise
<
AutomationTaskRun
[]
>
;
};
chat
:
{
chat
:
{
listSessions
():
Promise
<
ProjectSessionSummary
[]
>
;
listSessions
():
Promise
<
ProjectSessionSummary
[]
>
;
listSessionsByProject
(
projectId
:
string
):
Promise
<
ProjectSessionSummary
[]
>
;
listSessionsByProject
(
projectId
:
string
):
Promise
<
ProjectSessionSummary
[]
>
;
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment