@v587d/capital-generation 2.15.0 → 2.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +171 -0
- package/README.md +7 -12
- package/THIRD_PARTY_NOTICES.md +1 -0
- package/automation/TABLER-ICONS-LICENSE +21 -0
- package/automation/builtins.js +237 -10
- package/automation/client.js +79 -17
- package/automation/client.src.cjs +873 -210
- package/automation/contract.js +4 -0
- package/automation/defaults.js +6 -4
- package/automation/index.js +12 -7
- package/automation/package.json +5 -2
- package/automation/route.js +9 -2
- package/automation/runner.js +33 -6
- package/automation/scheduler.js +34 -29
- package/automation/schema.js +39 -5
- package/automation/service.js +102 -10
- package/automation/settings.js +6 -0
- package/automation/store.js +64 -21
- package/automation/task-icons.js +75 -0
- package/automation/workspaces.js +113 -13
- package/chart-ui/client.js +2 -2
- package/chart-ui/package.json +1 -1
- package/config/client.js +19 -5
- package/config/client.src.cjs +365 -44
- package/config/index.js +7 -5
- package/config/package.json +1 -1
- package/lib/agents/root-tool-policy.d.ts +3 -3
- package/lib/agents/root-tool-policy.js +4 -2
- package/lib/chart/runtime.d.ts +1 -1
- package/lib/chart/runtime.js +95 -0
- package/lib/chart/series.d.ts +4 -4
- package/lib/chart/series.js +44 -10
- package/lib/data-collector/dataset-tools.js +22 -0
- package/lib/data-collector/describe.js +1 -0
- package/lib/data-collector/hub.js +5 -0
- package/lib/data-collector/store.d.ts +5 -0
- package/lib/data-collector/store.js +17 -0
- package/lib/data-collector/time-axis.d.ts +15 -0
- package/lib/data-collector/time-axis.js +73 -0
- package/lib/data-collector/tools.d.ts +1 -1
- package/lib/data-collector/tools.js +12 -1
- package/lib/index.d.ts +5 -0
- package/lib/index.js +7 -5
- package/lib/sources/tencent-http.js +7 -2
- package/lib/sources/tencent-public-core.d.ts +2 -0
- package/lib/sources/tencent-public-core.js +5 -0
- package/lib/sources/wind-mcp.js +3 -3
- package/lib/sources-entry.d.ts +1 -1
- package/lib/web-retriever/local-fetch.d.ts +1 -1
- package/lib/web-retriever/local-fetch.js +1 -1
- package/lib/web-retriever/sources.d.ts +58 -0
- package/lib/web-retriever/sources.js +290 -0
- package/lib/web-retriever/tools.js +199 -8
- package/lib/web-retriever/wind-client.d.ts +1 -1
- package/lib/web-retriever/wind-client.js +1 -1
- package/package.json +1 -1
- package/preset/capital-generation/agent.patch.yml +20 -19
- package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +6 -3
- package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +9 -2
- package/preset/capital-generation/skills/capital-orchestration/SKILL.md +2 -1
- package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +2 -0
- package/preset/capital-generation/skills/capital-web-evidence/SKILL.md +18 -13
- package/preset/capital-generation/skills/capital-web-protocol/SKILL.md +30 -15
- package/watchlist/package.json +1 -1
|
@@ -15,11 +15,7 @@ const {
|
|
|
15
15
|
IconEditOutlineRegular,
|
|
16
16
|
IconEllipsisOutlineRegular,
|
|
17
17
|
IconFolderOpenOutlineRegular,
|
|
18
|
-
IconGaugeOutlineRegular,
|
|
19
|
-
IconGlobeOutlineRegular,
|
|
20
18
|
IconInfoOutlineRegular,
|
|
21
|
-
IconLightOutlineMedium,
|
|
22
|
-
IconListPenOutlineRegular,
|
|
23
19
|
IconPauseOutlineRegular,
|
|
24
20
|
IconPlayOutlineRegular,
|
|
25
21
|
IconPlusOutlineRegular,
|
|
@@ -34,6 +30,12 @@ const {
|
|
|
34
30
|
Modal,
|
|
35
31
|
Switch,
|
|
36
32
|
} = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
33
|
+
const {
|
|
34
|
+
TASK_ICON_CATALOG,
|
|
35
|
+
TASK_ICON_IDS,
|
|
36
|
+
DEFAULT_CUSTOM_ICON,
|
|
37
|
+
builtinTaskIcon,
|
|
38
|
+
} = require('./task-icons.js')
|
|
37
39
|
|
|
38
40
|
const ENTRY_NAME = 'capital-automation'
|
|
39
41
|
const ROUTE = '/capital-automation'
|
|
@@ -42,7 +44,7 @@ const STYLE_TAG = 'capital-automation-css'
|
|
|
42
44
|
const ACTIVE_RUN_STATUSES = ['queued', 'starting', 'running']
|
|
43
45
|
const FAILURE_RUN_STATUSES = ['failed', 'timed_out', 'blocked_requires_user', 'unknown_after_restart']
|
|
44
46
|
const SKIPPED_RUN_STATUSES = ['skipped_overlap', 'skipped_calendar_unavailable', 'missed']
|
|
45
|
-
const DEFAULT_LIMITS = { runTimeoutMinutes:
|
|
47
|
+
const DEFAULT_LIMITS = { runTimeoutMinutes: 60, idleTimeoutMinutes: 5, maxSteps: 300, maxToolCalls: 1200 }
|
|
46
48
|
const WEEKDAYS = [
|
|
47
49
|
{ value: 1, label: '一' },
|
|
48
50
|
{ value: 2, label: '二' },
|
|
@@ -70,10 +72,14 @@ const TIMEZONES = [
|
|
|
70
72
|
'America/Sao_Paulo',
|
|
71
73
|
'Australia/Sydney',
|
|
72
74
|
]
|
|
73
|
-
const HISTORY_DAYS =
|
|
75
|
+
const HISTORY_DAYS = 371
|
|
76
|
+
const HEATMAP_WEEKS = 53
|
|
77
|
+
const WEEKDAY_SHORT = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
|
74
78
|
const DETAIL_RECENT_RUN_LIMIT = 3
|
|
75
79
|
const TITLE_MAX_LENGTH = 12
|
|
76
80
|
const DESCRIPTION_MAX_LENGTH = 100
|
|
81
|
+
const INSTRUCTION_MAX_LENGTH = 12_000
|
|
82
|
+
const AUTOSAVE_DELAY_MS = 600
|
|
77
83
|
|
|
78
84
|
const zh = {
|
|
79
85
|
panel: 'CapGen 投研自动化',
|
|
@@ -81,10 +87,10 @@ const zh = {
|
|
|
81
87
|
subtitle: '定时投研,自动执行,结果可追溯',
|
|
82
88
|
tabs: { tasks: '任务', runs: '运行' },
|
|
83
89
|
storageUnavailable: '存储不可用,页面只读',
|
|
84
|
-
standbyBanner: '
|
|
90
|
+
standbyBanner: '本窗口的定时任务已暂停:定时触发与手动运行不可用;若另一个 CapGen 窗口在运行,定时任务仍会由它执行。',
|
|
85
91
|
storageBanner: '投研自动化存储暂时不可用:页面只读,调度器不会启动。',
|
|
86
92
|
calendarBanner: '交易日历暂时不可用:仅交易日的任务会跳过,其他任务仍按设定时间运行。',
|
|
87
|
-
calendarCoverageBanner: '交易日历只覆盖到 {through}
|
|
93
|
+
calendarCoverageBanner: '交易日历只覆盖到 {through}:之后仅交易日的任务会跳过;交易所公布新一年安排后会自动接上。',
|
|
88
94
|
calendarField: '交易日历',
|
|
89
95
|
calendarSourceSzse: '深交所官方日历',
|
|
90
96
|
calendarSourceBundled: '内置表',
|
|
@@ -102,7 +108,6 @@ const zh = {
|
|
|
102
108
|
clearFilter: '清除筛选',
|
|
103
109
|
newTask: '新建任务',
|
|
104
110
|
newTaskTitle: '新建投研任务',
|
|
105
|
-
newTaskHint: '任务会在后台新建一个 session 执行,不需要你守在页面上。',
|
|
106
111
|
editTask: '编辑任务',
|
|
107
112
|
allTasks: '全部任务',
|
|
108
113
|
backToTasks: '所有任务',
|
|
@@ -123,7 +128,7 @@ const zh = {
|
|
|
123
128
|
filterReview: '复盘',
|
|
124
129
|
filterResearch: '研究',
|
|
125
130
|
filterLabel: '筛选任务',
|
|
126
|
-
category: { news: '资讯', market: '行情', review: '复盘', research: '研究' },
|
|
131
|
+
category: { news: '资讯', market: '行情', review: '复盘', research: '研究', portfolio: '组合', monitor: '监控' },
|
|
127
132
|
categoryField: '任务分类',
|
|
128
133
|
categoryFixed: `'复制为我的任务'后,可调整任务分类。`,
|
|
129
134
|
cadence: { daily: '每天', weekly: '每周', monthly: '每月', once: '单次' },
|
|
@@ -146,17 +151,21 @@ const zh = {
|
|
|
146
151
|
remove: '删除任务',
|
|
147
152
|
edit: '编辑',
|
|
148
153
|
more: '更多操作',
|
|
149
|
-
save: '保存',
|
|
150
154
|
saving: '保存中…',
|
|
151
155
|
processing: '处理中…',
|
|
152
|
-
saved: '已保存',
|
|
153
156
|
cancel: '取消',
|
|
154
157
|
create: '创建任务',
|
|
155
158
|
creating: '创建中…',
|
|
156
159
|
titleRequired: '标题不能为空。',
|
|
157
160
|
instructionRequired: '任务指令不能为空。',
|
|
158
161
|
invalidLimit: '请输入有效数字。',
|
|
159
|
-
|
|
162
|
+
invalidLimitRange: '请输入 {min}–{max} 的整数。',
|
|
163
|
+
autosaveHint: '修改会自动保存',
|
|
164
|
+
autosavePending: '等待保存…',
|
|
165
|
+
autosaveSaved: '已自动保存',
|
|
166
|
+
autosaveFailed: '保存失败',
|
|
167
|
+
autosaveRetry: '重试',
|
|
168
|
+
autosaveInvalid: '填写完整后自动保存',
|
|
160
169
|
noDirty: '配置已同步',
|
|
161
170
|
nextRun: '下次运行',
|
|
162
171
|
nextRunDormant: '添加后开始调度',
|
|
@@ -173,6 +182,9 @@ const zh = {
|
|
|
173
182
|
notifications: '通知',
|
|
174
183
|
descriptionField: '任务说明',
|
|
175
184
|
titleField: '任务标题',
|
|
185
|
+
iconField: '任务图标',
|
|
186
|
+
iconPickHint: '点击图标更换',
|
|
187
|
+
iconBuiltinHint: '内置模板的图标固定;复制为我的任务后可更换。',
|
|
176
188
|
instructionField: '任务指令',
|
|
177
189
|
instructionPlaceholder: '例如:读取今日自选股行情、公告与相关新闻,按「结论—证据—风险」整理成一份可复核的盘后复盘。',
|
|
178
190
|
titlePlaceholder: '给这个任务起个名字',
|
|
@@ -225,10 +237,10 @@ const zh = {
|
|
|
225
237
|
maxSteps: '最大步骤',
|
|
226
238
|
maxToolCalls: '最大工具调用',
|
|
227
239
|
notifyField: '通知',
|
|
228
|
-
notifyInApp: '
|
|
229
|
-
notifyFailure: '
|
|
240
|
+
notifyInApp: '写入任务通知',
|
|
241
|
+
notifyFailure: '失败、超时或需人工处理时',
|
|
230
242
|
notifySuccess: '成功时通知',
|
|
231
|
-
notifyHint: '
|
|
243
|
+
notifyHint: '只写入当前 profile 的任务通知中心;不会发送邮件或外部消息。',
|
|
232
244
|
presetLabel: '运行模式',
|
|
233
245
|
presetValue: '自动模式',
|
|
234
246
|
presetHint: '所有运行固定使用自动模式。',
|
|
@@ -239,11 +251,19 @@ const zh = {
|
|
|
239
251
|
techInfo: '技术信息',
|
|
240
252
|
profileKey: 'profileKey',
|
|
241
253
|
runHistory: '运行历史',
|
|
242
|
-
|
|
254
|
+
last12Months: '最近 12 个月',
|
|
243
255
|
runCount: '{n} 次运行',
|
|
244
256
|
successCount: '{n} 成功',
|
|
245
257
|
failureCount: '{n} 失败',
|
|
246
|
-
|
|
258
|
+
activeCount: '{n} 进行中',
|
|
259
|
+
historyEmpty: '最近 12 个月还没有运行记录。',
|
|
260
|
+
heatLess: '少',
|
|
261
|
+
heatMore: '多',
|
|
262
|
+
heatFailed: '有失败',
|
|
263
|
+
heatActive: '进行中',
|
|
264
|
+
heatNoRuns: '没有运行',
|
|
265
|
+
heatFuture: '尚未到来',
|
|
266
|
+
heatSummary: '最近 12 个月共 {total} 次运行:{success} 成功,{failure} 失败。',
|
|
247
267
|
historyViewAll: '查看全部运行',
|
|
248
268
|
runFilterAll: '全部',
|
|
249
269
|
runFilterActive: '进行中',
|
|
@@ -270,15 +290,32 @@ const zh = {
|
|
|
270
290
|
triggerScheduled: '定时',
|
|
271
291
|
openSession: '打开 session',
|
|
272
292
|
runSummaryFallback: '运行结束但没有摘要。',
|
|
273
|
-
notificationsEmpty: '
|
|
293
|
+
notificationsEmpty: '没有任务通知。',
|
|
274
294
|
markRead: '全部标为已读',
|
|
275
295
|
markTaskRead: '本任务标为已读',
|
|
276
|
-
notificationsTitle: '
|
|
277
|
-
|
|
296
|
+
notificationsTitle: '任务通知',
|
|
297
|
+
notificationsEntry: '任务通知',
|
|
298
|
+
notificationsHint: '运行失败、超时、成功与交易日历问题会出现在这里。',
|
|
278
299
|
notificationsUnread: '{n} 条未读',
|
|
279
300
|
allRead: '全部已读',
|
|
280
|
-
noNotifications: '
|
|
301
|
+
noNotifications: '没有未读或历史任务通知。',
|
|
281
302
|
notificationOpenTask: '查看任务',
|
|
303
|
+
systemStatus: '系统状态',
|
|
304
|
+
systemActive: '定时调度正常',
|
|
305
|
+
systemStandby: '本窗口待命,不启动新的定时运行',
|
|
306
|
+
systemStarting: '定时调度启动中',
|
|
307
|
+
systemStopped: '定时调度未运行',
|
|
308
|
+
systemUnavailable: '定时调度不可用',
|
|
309
|
+
systemCalendarOk: '交易日历正常',
|
|
310
|
+
systemCalendarUnavailable: '交易日历不可用',
|
|
311
|
+
systemDetails: '详情',
|
|
312
|
+
systemHideDetails: '收起',
|
|
313
|
+
systemLastStandby: '上次中断',
|
|
314
|
+
systemNoStandby: '没有调度中断记录。',
|
|
315
|
+
systemReasonLeaseHeld: '另一个 CapGen 窗口正在负责定时任务。',
|
|
316
|
+
systemReasonLeaseLost: '本窗口的调度权已失效,可能已由另一个窗口接管。',
|
|
317
|
+
systemReasonUnverified: '本窗口的调度权校验未通过。',
|
|
318
|
+
systemTechnical: '技术信息',
|
|
282
319
|
close: '关闭',
|
|
283
320
|
confirmDeleteTitle: '删除这个任务?',
|
|
284
321
|
confirmDeleteBody: '任务配置、运行记录与相关通知会一起删除,无法恢复。',
|
|
@@ -299,7 +336,7 @@ const zh = {
|
|
|
299
336
|
markReadDone: '已标为已读',
|
|
300
337
|
errorGeneric: '操作失败,请稍后重试。',
|
|
301
338
|
needConfirm: '需要用户确认',
|
|
302
|
-
schedulerBlocked: '
|
|
339
|
+
schedulerBlocked: '本窗口的定时任务已暂停,暂时不能手动运行。',
|
|
303
340
|
runBlocked: '任务正在运行,结束或取消后才能再次运行。',
|
|
304
341
|
configWhileRunning: '任务正在运行;修改会在下一次运行时生效。',
|
|
305
342
|
detailRecentRuns: '最近 {n} 次运行',
|
|
@@ -312,8 +349,8 @@ const zh = {
|
|
|
312
349
|
notificationFallbackFailed: '运行失败;打开对应任务或 session 查看详情。',
|
|
313
350
|
notificationFallbackTimeout: '运行超时;打开对应任务或 session 查看详情。',
|
|
314
351
|
notificationFallbackSuccess: '运行完成。',
|
|
315
|
-
notificationFallbackStandby: '
|
|
316
|
-
notificationFallbackCalendar: '
|
|
352
|
+
notificationFallbackStandby: '定时触发已暂停。',
|
|
353
|
+
notificationFallbackCalendar: '暂时无法确认交易日,相关任务已跳过。',
|
|
317
354
|
notificationFallbackSkipped: '本次计划已跳过。',
|
|
318
355
|
statusLabel: '状态',
|
|
319
356
|
triggerSummary: '日程',
|
|
@@ -343,8 +380,8 @@ function installStyles() {
|
|
|
343
380
|
const style = document.createElement('style')
|
|
344
381
|
style.setAttribute(`data-${STYLE_TAG}`, 'true')
|
|
345
382
|
style.textContent = `
|
|
346
|
-
.capital-automation-page, .capital-automation-modal { --ca-card: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-1)); --ca-line: var(--dsw-alias-border-l3, var(--dsw-alias-border-l1)); --ca-line-soft: var(--dsw-alias-border-l2, var(--dsw-alias-border-l1)); --ca-table-line: var(--ca-line); --ca-muted: var(--dsw-alias-label-tertiary); --ca-caption: var(--dsw-alias-label-caption, var(--dsw-alias-label-tertiary)); --ca-hover: var(--dsw-alias-interactive-bg-hover); --ca-field: var(--dsw-alias-bg-module-platform); --ca-business: var(--dsw-alias-state-business-primary, var(--dsw-alias-brand-primary)); --ca-success: var(--dsw-alias-state-success-primary); --ca-error: var(--dsw-alias-state-error-primary); --ca-warn: var(--dsw-alias-state-warn-label); --ca-business-text: var(--ca-business); --ca-success-text: var(--ca-success); --ca-error-text: var(--ca-error); --ca-warn-text: var(--ca-warn); }
|
|
347
|
-
body:not([data-ds-dark-theme]) .capital-automation-page, body:not([data-ds-dark-theme]) .capital-automation-modal { --ca-line: #0000001f; --ca-line-soft: #0000001a; --ca-table-line: #00000026; --ca-muted: #5d626a; --ca-caption: #6a6f77; --ca-hover: #eef1f5; --ca-field: #f7f8fa; --ca-business: #2f63d6; --ca-success: #16a34a; --ca-error: #dc2626; --ca-warn: #d97706; --ca-business-text: #2f63d6; --ca-success-text: #15803d; --ca-error-text: #c81e1e; --ca-warn-text: #b45309; --ca-tag-news: #2f6fd0; --ca-tag-market: #a85a08; --ca-tag-review: #6d4ac4; --ca-tag-research: #0f766e; --ca-tag-custom: #b3246d; }
|
|
383
|
+
.capital-automation-page, .capital-automation-modal { --ca-card: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-1)); --ca-line: var(--dsw-alias-border-l3, var(--dsw-alias-border-l1)); --ca-line-soft: var(--dsw-alias-border-l2, var(--dsw-alias-border-l1)); --ca-table-line: var(--ca-line); --ca-muted: var(--dsw-alias-label-tertiary); --ca-caption: var(--dsw-alias-label-caption, var(--dsw-alias-label-tertiary)); --ca-hover: var(--dsw-alias-interactive-bg-hover); --ca-field: var(--dsw-alias-bg-module-platform); --ca-business: var(--dsw-alias-state-business-primary, var(--dsw-alias-brand-primary)); --ca-success: var(--dsw-alias-state-success-primary); --ca-error: var(--dsw-alias-state-error-primary); --ca-warn: var(--dsw-alias-state-warn-label); --ca-business-text: var(--ca-business); --ca-success-text: var(--ca-success); --ca-error-text: var(--ca-error); --ca-warn-text: var(--ca-warn); --ca-heat-0: #262626; --ca-heat-1: #1b5e3a; --ca-heat-2: #2f8a56; --ca-heat-3: #40b366; --ca-heat-4: #56d364; --ca-heat-fail: #f85149; }
|
|
384
|
+
body:not([data-ds-dark-theme]) .capital-automation-page, body:not([data-ds-dark-theme]) .capital-automation-modal { --ca-line: #0000001f; --ca-line-soft: #0000001a; --ca-table-line: #00000026; --ca-muted: #5d626a; --ca-caption: #6a6f77; --ca-hover: #eef1f5; --ca-field: #f7f8fa; --ca-business: #2f63d6; --ca-success: #16a34a; --ca-error: #dc2626; --ca-warn: #d97706; --ca-business-text: #2f63d6; --ca-success-text: #15803d; --ca-error-text: #c81e1e; --ca-warn-text: #b45309; --ca-tag-news: #2f6fd0; --ca-tag-market: #a85a08; --ca-tag-review: #6d4ac4; --ca-tag-research: #0f766e; --ca-tag-portfolio: #4d7c0f; --ca-tag-monitor: #0e7490; --ca-tag-custom: #b3246d; --ca-heat-0: #eef1f4; --ca-heat-1: #b7e4c0; --ca-heat-2: #74c98a; --ca-heat-3: #3ba55c; --ca-heat-4: #1f7a3a; --ca-heat-fail: #cf222e; }
|
|
348
385
|
.capital-automation-page { display: flex; flex-direction: column; min-height: 0; height: 100%; color: var(--dsw-alias-label-primary); }
|
|
349
386
|
.capital-automation-head { flex: none; padding: 18px 0 0; }
|
|
350
387
|
.capital-automation-headinner { box-sizing: border-box; width: min(1080px, 100%); margin: 0 auto; padding: 0 clamp(20px, 3vw, 36px); }
|
|
@@ -384,7 +421,7 @@ function installStyles() {
|
|
|
384
421
|
.capital-automation-scroll { flex: 1 1 auto; min-height: 0; overflow: auto; }
|
|
385
422
|
.capital-automation-container { box-sizing: border-box; width: min(1080px, 100%); margin: 0 auto; padding: 20px clamp(20px, 3vw, 36px) 72px; }
|
|
386
423
|
.capital-automation-card { box-sizing: border-box; border: .5px solid var(--ca-line); border-radius: 16px; background: var(--ca-card); }
|
|
387
|
-
.capital-automation-history { padding: 18px 20px 14px; }
|
|
424
|
+
.capital-automation-history { position: relative; padding: 18px 20px 14px; }
|
|
388
425
|
.capital-automation-historyhead { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: 10px 18px; }
|
|
389
426
|
.capital-automation-historytitle { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; }
|
|
390
427
|
.capital-automation-historytitle h2 { margin: 0; font-size: 14px; line-height: 22px; font-weight: 600; }
|
|
@@ -393,18 +430,45 @@ function installStyles() {
|
|
|
393
430
|
.capital-automation-historystat { display: inline-flex; align-items: center; gap: 5px; font-variant-numeric: tabular-nums; }
|
|
394
431
|
.capital-automation-historystat[data-tone="ok"] { color: var(--ca-success-text); }
|
|
395
432
|
.capital-automation-historystat[data-tone="error"] { color: var(--ca-error-text); }
|
|
433
|
+
.capital-automation-historystat[data-tone="active"] { color: var(--ca-business-text); }
|
|
396
434
|
.capital-automation-statusdot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }
|
|
397
435
|
.capital-automation-historylink { display: inline-flex; align-items: center; gap: 2px; height: 24px; padding: 0 6px; border: 0; border-radius: 7px; background: transparent; color: var(--ca-muted); font: inherit; font-size: 12px; cursor: pointer; }
|
|
398
436
|
.capital-automation-historylink:hover { background: var(--ca-hover); color: var(--dsw-alias-label-primary); }
|
|
399
|
-
.capital-automation-
|
|
400
|
-
.capital-automation-
|
|
401
|
-
.capital-automation-
|
|
402
|
-
.capital-automation-
|
|
403
|
-
.capital-automation-
|
|
404
|
-
.capital-automation-
|
|
405
|
-
.capital-automation-
|
|
406
|
-
.capital-automation-
|
|
407
|
-
.capital-automation-
|
|
437
|
+
.capital-automation-heatmap { display: flex; align-items: flex-start; gap: 8px; margin-top: 16px; }
|
|
438
|
+
.capital-automation-heatweekdays { flex: none; display: grid; grid-template-rows: repeat(7, 1fr); gap: 3px; width: 28px; padding-top: 22px; color: var(--ca-caption); font-size: 10.5px; line-height: 1; align-self: stretch; }
|
|
439
|
+
.capital-automation-heatweekday { display: flex; align-items: center; }
|
|
440
|
+
.capital-automation-heatscroll { flex: 1 1 auto; min-width: 0; overflow-x: auto; overflow-y: hidden; padding-bottom: 2px; scrollbar-width: thin; }
|
|
441
|
+
.capital-automation-heatscroll[data-faded="true"] { -webkit-mask-image: linear-gradient(to right, transparent 0, #000 18px); mask-image: linear-gradient(to right, transparent 0, #000 18px); }
|
|
442
|
+
.capital-automation-heatcanvas { min-width: 620px; }
|
|
443
|
+
.capital-automation-heatmonths { display: grid; grid-template-columns: repeat(${HEATMAP_WEEKS}, minmax(0, 1fr)); gap: 3px; height: 16px; margin-bottom: 6px; }
|
|
444
|
+
.capital-automation-heatmonth { color: var(--ca-caption); font-size: 10.5px; line-height: 16px; white-space: nowrap; letter-spacing: .01em; }
|
|
445
|
+
.capital-automation-heatgrid { display: grid; grid-template-columns: repeat(${HEATMAP_WEEKS}, minmax(0, 1fr)); grid-template-rows: repeat(7, auto); grid-auto-flow: column; gap: 3px; }
|
|
446
|
+
.capital-automation-heatcell { display: block; min-width: 0; aspect-ratio: 1; border-radius: 2.5px; background: var(--ca-heat-0); box-shadow: inset 0 0 0 .5px color-mix(in srgb, var(--dsw-alias-label-primary) 4%, transparent); transition: background .12s, box-shadow .12s, outline-color .12s; }
|
|
447
|
+
.capital-automation-heatcell[data-level="1"] { background: var(--ca-heat-1); }
|
|
448
|
+
.capital-automation-heatcell[data-level="2"] { background: var(--ca-heat-2); }
|
|
449
|
+
.capital-automation-heatcell[data-level="3"] { background: var(--ca-heat-3); }
|
|
450
|
+
.capital-automation-heatcell[data-level="4"] { background: var(--ca-heat-4); }
|
|
451
|
+
.capital-automation-heatcell[data-future="true"] { opacity: .45; }
|
|
452
|
+
.capital-automation-heatcell[data-failed="true"] { box-shadow: inset 0 0 0 1.5px var(--ca-heat-fail); }
|
|
453
|
+
.capital-automation-heatcell[data-active="true"] { box-shadow: inset 0 0 0 1.5px var(--ca-business); animation: capital-automation-heatpulse 1.8s ease-in-out infinite; }
|
|
454
|
+
.capital-automation-heatcell:hover { outline: 1px solid var(--dsw-alias-label-primary); outline-offset: 1px; }
|
|
455
|
+
.capital-automation-heattip { position: absolute; z-index: 5; min-width: 128px; max-width: 220px; padding: 7px 9px; border: .5px solid var(--ca-line); border-radius: 9px; background: var(--dsw-alias-bg-layer-3, var(--ca-card)); box-shadow: 0 8px 22px rgba(0, 0, 0, .28); color: var(--dsw-alias-label-primary); pointer-events: none; transform: translate(-50%, -100%); }
|
|
456
|
+
.capital-automation-heattipdate { display: block; font-size: 11.5px; font-weight: 600; line-height: 16px; white-space: nowrap; }
|
|
457
|
+
.capital-automation-heattipmeta { display: block; margin-top: 2px; color: var(--ca-muted); font-size: 11px; line-height: 16px; white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
458
|
+
.capital-automation-heatfoot { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 14px; margin-top: 8px; color: var(--ca-caption); font-size: 11px; line-height: 16px; }
|
|
459
|
+
.capital-automation-heatrange { font-variant-numeric: tabular-nums; }
|
|
460
|
+
.capital-automation-heatlegend { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 5px; }
|
|
461
|
+
.capital-automation-heatlegendlabel { color: var(--ca-caption); }
|
|
462
|
+
.capital-automation-heatsample { width: 10px; height: 10px; border-radius: 2px; background: var(--ca-heat-0); box-shadow: inset 0 0 0 .5px color-mix(in srgb, var(--dsw-alias-label-primary) 5%, transparent); }
|
|
463
|
+
.capital-automation-heatsample[data-level="1"] { background: var(--ca-heat-1); }
|
|
464
|
+
.capital-automation-heatsample[data-level="2"] { background: var(--ca-heat-2); }
|
|
465
|
+
.capital-automation-heatsample[data-level="3"] { background: var(--ca-heat-3); }
|
|
466
|
+
.capital-automation-heatsample[data-level="4"] { background: var(--ca-heat-4); }
|
|
467
|
+
.capital-automation-heatkey { display: inline-flex; align-items: center; gap: 5px; margin-right: 7px; }
|
|
468
|
+
.capital-automation-heatfailbox { width: 10px; height: 10px; border-radius: 2px; background: var(--ca-heat-0); box-shadow: inset 0 0 0 1.5px var(--ca-heat-fail); }
|
|
469
|
+
.capital-automation-heatactivebox { width: 10px; height: 10px; border-radius: 2px; background: var(--ca-heat-0); box-shadow: inset 0 0 0 1.5px var(--ca-business); }
|
|
470
|
+
@keyframes capital-automation-heatpulse { 0%, 100% { opacity: 1; } 50% { opacity: .5; } }
|
|
471
|
+
@media (prefers-reduced-motion: reduce) { .capital-automation-heatcell[data-active="true"] { animation: none; } }
|
|
408
472
|
.capital-automation-section { margin-top: 26px; }
|
|
409
473
|
.capital-automation-sectionhead { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; margin-bottom: 10px; }
|
|
410
474
|
.capital-automation-sectionhead h2 { margin: 0; font-size: 14px; line-height: 22px; font-weight: 600; }
|
|
@@ -444,6 +508,8 @@ function installStyles() {
|
|
|
444
508
|
.capital-automation-glyph[data-tone="market"] { background: linear-gradient(135deg, #ffbd4a, #f97316); }
|
|
445
509
|
.capital-automation-glyph[data-tone="review"] { background: linear-gradient(135deg, #b18cff, #7c3aed); }
|
|
446
510
|
.capital-automation-glyph[data-tone="research"] { background: linear-gradient(135deg, #39d7a8, #0d9488); }
|
|
511
|
+
.capital-automation-glyph[data-tone="portfolio"] { background: linear-gradient(135deg, #84cc16, #4d7c0f); }
|
|
512
|
+
.capital-automation-glyph[data-tone="monitor"] { background: linear-gradient(135deg, #06b6d4, #0e7490); }
|
|
447
513
|
.capital-automation-glyph[data-tone="custom"] { background: linear-gradient(135deg, #f472b6, #db2777); }
|
|
448
514
|
.capital-automation-identitytext { min-width: 0; }
|
|
449
515
|
.capital-automation-tasktitlerow { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 6px; min-width: 0; }
|
|
@@ -456,12 +522,16 @@ function installStyles() {
|
|
|
456
522
|
.capital-automation-tag[data-tone="market"] { color: #ffb45c; }
|
|
457
523
|
.capital-automation-tag[data-tone="review"] { color: #b79aff; }
|
|
458
524
|
.capital-automation-tag[data-tone="research"] { color: #56d9b4; }
|
|
525
|
+
.capital-automation-tag[data-tone="portfolio"] { color: #a3e635; }
|
|
526
|
+
.capital-automation-tag[data-tone="monitor"] { color: #22d3ee; }
|
|
459
527
|
.capital-automation-tag[data-tone="custom"] { color: #f79bc3; }
|
|
460
528
|
body:not([data-ds-dark-theme]) .capital-automation-tag { background: color-mix(in srgb, currentColor 9%, var(--ca-card)); }
|
|
461
529
|
body:not([data-ds-dark-theme]) .capital-automation-tag[data-tone="news"] { color: var(--ca-tag-news); }
|
|
462
530
|
body:not([data-ds-dark-theme]) .capital-automation-tag[data-tone="market"] { color: var(--ca-tag-market); }
|
|
463
531
|
body:not([data-ds-dark-theme]) .capital-automation-tag[data-tone="review"] { color: var(--ca-tag-review); }
|
|
464
532
|
body:not([data-ds-dark-theme]) .capital-automation-tag[data-tone="research"] { color: var(--ca-tag-research); }
|
|
533
|
+
body:not([data-ds-dark-theme]) .capital-automation-tag[data-tone="portfolio"] { color: var(--ca-tag-portfolio); }
|
|
534
|
+
body:not([data-ds-dark-theme]) .capital-automation-tag[data-tone="monitor"] { color: var(--ca-tag-monitor); }
|
|
465
535
|
body:not([data-ds-dark-theme]) .capital-automation-tag[data-tone="custom"] { color: var(--ca-tag-custom); }
|
|
466
536
|
.capital-automation-schedule { display: block; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }
|
|
467
537
|
.capital-automation-schedulesub { display: block; margin-top: 1px; color: var(--ca-caption); font-size: 11px; line-height: 16px; }
|
|
@@ -483,8 +553,11 @@ function installStyles() {
|
|
|
483
553
|
.capital-automation-loading { display: flex; flex-direction: column; pointer-events: none; }
|
|
484
554
|
.capital-automation-skeletonlabel { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
|
485
555
|
.capital-automation-skeleton { display: block; flex: none; max-width: 100%; border-radius: 6px; background: var(--dsw-alias-bg-skeleton); animation: capital-automation-breathe 1.8s ease-in-out infinite; }
|
|
486
|
-
.capital-automation-
|
|
487
|
-
.capital-automation-
|
|
556
|
+
.capital-automation-skeletonheat { margin-top: 16px; padding-left: 36px; }
|
|
557
|
+
.capital-automation-skeletonmonths { display: grid; grid-template-columns: repeat(${HEATMAP_WEEKS}, minmax(0, 1fr)); gap: 3px; height: 16px; margin-bottom: 6px; }
|
|
558
|
+
.capital-automation-skeletonmonths > .capital-automation-skeleton { width: 22px; height: 8px; border-radius: 3px; }
|
|
559
|
+
.capital-automation-skeletongrid { display: grid; grid-template-columns: repeat(${HEATMAP_WEEKS}, minmax(0, 1fr)); grid-template-rows: repeat(7, auto); grid-auto-flow: column; gap: 3px; }
|
|
560
|
+
.capital-automation-skeletongrid > .capital-automation-skeleton { width: 100%; aspect-ratio: 1; border-radius: 2.5px; }
|
|
488
561
|
.capital-automation-skeletonstack { display: flex; flex: 1 1 auto; flex-direction: column; gap: 6px; min-width: 0; }
|
|
489
562
|
.capital-automation-skeletonrow .capital-automation-trowmain { min-height: 57px; cursor: default; }
|
|
490
563
|
.capital-automation-skeletonrow .capital-automation-cell[data-cell] { display: flex; flex-direction: column; gap: 5px; }
|
|
@@ -558,6 +631,22 @@ function installStyles() {
|
|
|
558
631
|
.capital-automation-fieldhead { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; min-width: 0; }
|
|
559
632
|
.capital-automation-fieldlabel { color: var(--ca-muted); font-size: 11.5px; line-height: 17px; }
|
|
560
633
|
.capital-automation-fieldhint { color: var(--ca-caption); font-size: 11px; line-height: 16px; font-variant-numeric: tabular-nums; }
|
|
634
|
+
.capital-automation-glyphbtn { display: inline-flex; flex: none; align-items: center; gap: 4px; padding: 0; border: 0; border-radius: 14px; background: transparent; cursor: pointer; }
|
|
635
|
+
.capital-automation-glyphbtn:hover:not(:disabled) .capital-automation-glyph { box-shadow: inset 0 1px 0 rgba(255,255,255,.22), 0 0 0 2px color-mix(in srgb, var(--ca-business) 52%, transparent); }
|
|
636
|
+
.capital-automation-glyphbtn:focus-visible { outline: 2px solid var(--dsw-focus-ring-color, var(--ca-business)); outline-offset: 2px; }
|
|
637
|
+
.capital-automation-glyphbtn:disabled { cursor: not-allowed; opacity: .55; }
|
|
638
|
+
.capital-automation-glyphbtnchevron { display: grid; flex: none; place-items: center; width: 12px; color: var(--ca-muted); }
|
|
639
|
+
.capital-automation-glyphbtn:hover:not(:disabled) .capital-automation-glyphbtnchevron { color: var(--ca-business-text); }
|
|
640
|
+
.capital-automation-iconrow { display: flex; align-items: center; gap: 12px; min-width: 0; }
|
|
641
|
+
.capital-automation-iconrowbody { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
642
|
+
/* The menu is portaled to body, outside the page's --ca-* scope; use host tokens. */
|
|
643
|
+
.capital-automation-iconmenu { box-sizing: border-box; width: 400px; max-width: calc(100vw - 24px); padding: 10px; }
|
|
644
|
+
.capital-automation-iconmenu-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(32px, 1fr)); gap: 6px; }
|
|
645
|
+
button.capital-automation-iconmenu-item { box-sizing: border-box; display: inline-grid; place-items: center; width: 100%; max-width: 34px; aspect-ratio: 1; justify-self: center; padding: 0; border: 1px solid transparent; border-radius: 9px; background: transparent; color: var(--dsw-alias-label-tertiary); cursor: pointer; transition: background .15s, border-color .15s, color .15s; }
|
|
646
|
+
button.capital-automation-iconmenu-item:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
|
|
647
|
+
button.capital-automation-iconmenu-item[data-selected="true"] { border-color: var(--dsw-alias-brand-primary); background: color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent); color: var(--dsw-alias-brand-primary); }
|
|
648
|
+
button.capital-automation-iconmenu-item:focus-visible { outline: 2px solid var(--dsw-focus-ring-color, var(--dsw-alias-brand-primary)); outline-offset: 2px; }
|
|
649
|
+
button.capital-automation-iconmenu-item:disabled { opacity: .4; cursor: not-allowed; }
|
|
561
650
|
.capital-automation-field input { box-sizing: border-box; width: 100%; padding: 0 10px; border: .5px solid var(--ca-line-soft); border-radius: 10px; background: var(--ca-field); color: var(--dsw-alias-label-primary); font: inherit; font-size: 12.5px; line-height: 32px; }
|
|
562
651
|
.capital-automation-field input { height: 34px; }
|
|
563
652
|
.capital-automation-texteditor { color: var(--dsw-alias-label-primary); font: inherit; white-space: pre-wrap; overflow-wrap: anywhere; overflow-y: auto; outline: 0; cursor: text; }
|
|
@@ -586,6 +675,9 @@ function installStyles() {
|
|
|
586
675
|
.capital-automation-formactions { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 8px; padding-top: 14px; border-top: .5px solid var(--ca-line); }
|
|
587
676
|
.capital-automation-formactions[data-sticky="true"] { position: sticky; bottom: -1px; z-index: 2; margin-top: 2px; padding: 12px 0 4px; background: var(--dsw-alias-bg-base); }
|
|
588
677
|
.capital-automation-formstatus { margin-right: auto; color: var(--ca-caption); font-size: 11.5px; line-height: 18px; }
|
|
678
|
+
.capital-automation-autosavestate { min-width: 0; max-width: min(520px, 70%); overflow: hidden; color: var(--ca-caption); font-size: 11.5px; line-height: 18px; font-variant-numeric: tabular-nums; text-overflow: ellipsis; white-space: nowrap; }
|
|
679
|
+
.capital-automation-autosavestate[data-tone="ok"] { color: var(--ca-success-text); }
|
|
680
|
+
.capital-automation-autosavestate[data-tone="error"] { color: var(--ca-error-text); }
|
|
589
681
|
.capital-automation-triggerbox { display: flex; flex-direction: column; gap: 12px; padding: 14px 16px; border: .5px solid var(--ca-line); border-radius: 14px; background: var(--ca-field); }
|
|
590
682
|
.capital-automation-triggerbox[data-invalid="true"] { border-color: color-mix(in srgb, var(--ca-error) 52%, var(--ca-line)); box-shadow: 0 0 0 3px color-mix(in srgb, var(--ca-error) 9%, transparent); }
|
|
591
683
|
.capital-automation-triggerhead { display: flex; flex-wrap: nowrap; align-items: center; justify-content: space-between; gap: 12px; min-width: 0; }
|
|
@@ -668,6 +760,13 @@ function installStyles() {
|
|
|
668
760
|
.capital-automation-notificationtitle { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; color: var(--dsw-alias-label-primary); font-size: 12.5px; line-height: 19px; font-weight: 500; }
|
|
669
761
|
.capital-automation-notificationmessage { display: -webkit-box; overflow: hidden; margin-top: 2px; color: var(--ca-muted); font-size: 11.5px; line-height: 18px; overflow-wrap: anywhere; -webkit-line-clamp: 3; -webkit-box-orient: vertical; }
|
|
670
762
|
.capital-automation-notificationtime { display: block; margin-top: 3px; color: var(--ca-caption); font-size: 11px; line-height: 16px; }
|
|
763
|
+
.capital-automation-systemstatus { margin-top: 22px; padding: 12px 0 4px; border-top: .5px solid var(--ca-line); color: var(--ca-caption); font-size: 11px; line-height: 17px; }
|
|
764
|
+
.capital-automation-systemline { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
|
|
765
|
+
.capital-automation-systemtoggle { border: 0; padding: 0; background: transparent; color: var(--ca-muted); font: inherit; cursor: pointer; }
|
|
766
|
+
.capital-automation-systemtoggle:hover { color: var(--dsw-alias-label-primary); text-decoration: underline; }
|
|
767
|
+
.capital-automation-systemdetail { margin-top: 6px; color: var(--ca-muted); }
|
|
768
|
+
.capital-automation-systemdetail p { margin: 2px 0; }
|
|
769
|
+
.capital-automation-systemdetail summary { color: var(--ca-caption); cursor: pointer; }
|
|
671
770
|
.capital-automation-modal { width: min(720px, calc(100vw - 32px)); max-width: min(720px, calc(100vw - 32px)); max-height: calc(100dvh - 40px); }
|
|
672
771
|
.capital-automation-modal-wizard { width: min(600px, calc(100vw - 32px)); max-width: min(600px, calc(100vw - 32px)); }
|
|
673
772
|
.capital-automation-modal-content { max-height: calc(100dvh - 128px); overflow: auto; }
|
|
@@ -755,6 +854,7 @@ function createApi() {
|
|
|
755
854
|
credentials: 'same-origin',
|
|
756
855
|
headers: options.body === undefined ? {} : { 'content-type': 'application/json' },
|
|
757
856
|
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
857
|
+
...(options.keepalive === true ? { keepalive: true } : {}),
|
|
758
858
|
...(controller === undefined ? {} : { signal: controller.signal }),
|
|
759
859
|
})
|
|
760
860
|
try {
|
|
@@ -777,12 +877,13 @@ function createApi() {
|
|
|
777
877
|
tasks: () => request('/tasks'),
|
|
778
878
|
detail: (id) => request(`/tasks/${encodeURIComponent(id)}`),
|
|
779
879
|
create: (body) => request('/tasks', { method: 'POST', body }),
|
|
780
|
-
patch: (id, body) => request(`/tasks/${encodeURIComponent(id)}`, { method: 'PATCH', body }),
|
|
880
|
+
patch: (id, body) => request(`/tasks/${encodeURIComponent(id)}`, { method: 'PATCH', body, keepalive: true }),
|
|
781
881
|
remove: (id) => request(`/tasks/${encodeURIComponent(id)}`, { method: 'DELETE' }),
|
|
782
882
|
action: (id, action) => request(`/tasks/${encodeURIComponent(id)}/${action}`, { method: 'POST' }),
|
|
783
883
|
run: (id) => request(`/tasks/${encodeURIComponent(id)}/run`, { method: 'POST' }),
|
|
784
884
|
cancel: (id) => request(`/runs/${encodeURIComponent(id)}/cancel`, { method: 'POST' }),
|
|
785
885
|
runs: (query = '') => request(`/runs${query}`),
|
|
886
|
+
history: (query = '') => request(`/runs/history${query}`),
|
|
786
887
|
notifications: () => request('/notifications'),
|
|
787
888
|
markRead: (body) => request('/notifications/read', { method: 'POST', body }),
|
|
788
889
|
}
|
|
@@ -841,24 +942,124 @@ function toneOf(task) {
|
|
|
841
942
|
return task?.origin === 'custom' ? 'custom' : (task?.category ?? 'news')
|
|
842
943
|
}
|
|
843
944
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
945
|
+
// Twelve templates cannot stay in record-id order: read them daily → weekly → monthly, then by
|
|
946
|
+
// day and wall-clock time, so the card grid matches how the user plans the week.
|
|
947
|
+
const CADENCE_RANK = { daily: 0, weekly: 1, monthly: 2, once: 3 }
|
|
948
|
+
function firstWeekday(trigger) {
|
|
949
|
+
const days = Array.isArray(trigger?.weekdays) ? trigger.weekdays : []
|
|
950
|
+
return days.length === 0 ? 0 : Math.min(...days)
|
|
951
|
+
}
|
|
952
|
+
function compareTemplates(left, right) {
|
|
953
|
+
const rank = (CADENCE_RANK[left.trigger?.cadence] ?? 9) - (CADENCE_RANK[right.trigger?.cadence] ?? 9)
|
|
954
|
+
if (rank !== 0) return rank
|
|
955
|
+
const day = firstWeekday(left.trigger) - firstWeekday(right.trigger)
|
|
956
|
+
if (day !== 0) return day
|
|
957
|
+
return String(left.trigger?.time ?? '').localeCompare(String(right.trigger?.time ?? ''))
|
|
958
|
+
|| String(left.title).localeCompare(String(right.title))
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function iconDefinition(id) {
|
|
962
|
+
return typeof id === 'string' && Object.prototype.hasOwnProperty.call(TASK_ICON_CATALOG, id)
|
|
963
|
+
? TASK_ICON_CATALOG[id]
|
|
964
|
+
: undefined
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function taskIconId(task) {
|
|
968
|
+
if (iconDefinition(task?.icon) !== undefined) return task.icon
|
|
969
|
+
if (task?.origin === 'custom') return DEFAULT_CUSTOM_ICON
|
|
970
|
+
return builtinTaskIcon(task?.sourceBuiltinId)
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function taskIcon(task) {
|
|
974
|
+
return iconDefinition(taskIconId(task)) ?? TASK_ICON_CATALOG[DEFAULT_CUSTOM_ICON]
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// Tabler glyphs are a 24 grid with 2px strokes; the tile supplies size and color so one drawing
|
|
978
|
+
// works on every category tone.
|
|
979
|
+
function TaskGlyph(props) {
|
|
980
|
+
const { task, size } = props
|
|
981
|
+
const icon = taskIcon(task)
|
|
982
|
+
return h('svg', {
|
|
983
|
+
viewBox: '0 0 24 24',
|
|
984
|
+
width: size,
|
|
985
|
+
height: size,
|
|
986
|
+
fill: 'none',
|
|
987
|
+
stroke: 'currentColor',
|
|
988
|
+
strokeWidth: 2,
|
|
989
|
+
strokeLinecap: 'round',
|
|
990
|
+
strokeLinejoin: 'round',
|
|
991
|
+
'aria-hidden': 'true',
|
|
992
|
+
focusable: 'false',
|
|
993
|
+
}, h('path', { d: icon.d }))
|
|
851
994
|
}
|
|
852
995
|
|
|
853
996
|
function Glyph(props) {
|
|
854
997
|
const { task, size = 34, large = false } = props
|
|
855
|
-
const GlyphIcon = taskGlyph(task)
|
|
856
998
|
const edge = large ? 44 : size
|
|
857
999
|
return h('span', {
|
|
858
1000
|
className: 'capital-automation-glyph',
|
|
859
1001
|
'data-tone': toneOf(task),
|
|
860
1002
|
style: { width: edge, height: edge, borderRadius: large ? 13 : 10 },
|
|
861
|
-
}, h(
|
|
1003
|
+
}, h(TaskGlyph, { task, size: large ? 22 : 17 }))
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function IconPickerTrigger(props) {
|
|
1007
|
+
const { task, size = 38, large = false, onChange, disabled } = props
|
|
1008
|
+
const [open, setOpen] = useState(false)
|
|
1009
|
+
const selectedId = taskIconId(task)
|
|
1010
|
+
const selected = iconDefinition(selectedId) ?? TASK_ICON_CATALOG[DEFAULT_CUSTOM_ICON]
|
|
1011
|
+
useEffect(() => {
|
|
1012
|
+
if (disabled === true && open) setOpen(false)
|
|
1013
|
+
}, [disabled, open])
|
|
1014
|
+
// The host Menu owns the portal, outside-click, Escape and button walk; the grid is only content.
|
|
1015
|
+
return h(Menu, {
|
|
1016
|
+
open,
|
|
1017
|
+
anchor: h('button', {
|
|
1018
|
+
type: 'button',
|
|
1019
|
+
className: 'capital-automation-glyphbtn',
|
|
1020
|
+
disabled,
|
|
1021
|
+
'aria-label': `${zh.iconField}:${selected.label},点击更换`,
|
|
1022
|
+
'aria-haspopup': 'menu',
|
|
1023
|
+
'aria-expanded': open ? 'true' : 'false',
|
|
1024
|
+
title: zh.iconPickHint,
|
|
1025
|
+
onClick: () => setOpen((current) => !current),
|
|
1026
|
+
},
|
|
1027
|
+
h(Glyph, { task, size, large }),
|
|
1028
|
+
h('span', { className: 'capital-automation-glyphbtnchevron', 'aria-hidden': 'true' },
|
|
1029
|
+
h(IconChevronDownOutlineRegular, { size: 12 }))),
|
|
1030
|
+
items: [],
|
|
1031
|
+
onClose: () => setOpen(false),
|
|
1032
|
+
align: 'start',
|
|
1033
|
+
side: 'bottom',
|
|
1034
|
+
portal: true,
|
|
1035
|
+
listClassName: 'capital-automation-iconmenu',
|
|
1036
|
+
}, h('div', { className: 'capital-automation-iconmenu-grid' },
|
|
1037
|
+
TASK_ICON_IDS.map((id) => {
|
|
1038
|
+
const icon = TASK_ICON_CATALOG[id]
|
|
1039
|
+
const active = id === selectedId
|
|
1040
|
+
return h('button', {
|
|
1041
|
+
key: id,
|
|
1042
|
+
type: 'button',
|
|
1043
|
+
role: 'menuitemradio',
|
|
1044
|
+
'aria-checked': active ? 'true' : 'false',
|
|
1045
|
+
'aria-label': icon.label,
|
|
1046
|
+
title: icon.label,
|
|
1047
|
+
className: 'capital-automation-iconmenu-item',
|
|
1048
|
+
'data-selected': active ? 'true' : undefined,
|
|
1049
|
+
onClick: () => { onChange(id); setOpen(false) },
|
|
1050
|
+
}, h('svg', {
|
|
1051
|
+
viewBox: '0 0 24 24',
|
|
1052
|
+
width: 18,
|
|
1053
|
+
height: 18,
|
|
1054
|
+
fill: 'none',
|
|
1055
|
+
stroke: 'currentColor',
|
|
1056
|
+
strokeWidth: 2,
|
|
1057
|
+
strokeLinecap: 'round',
|
|
1058
|
+
strokeLinejoin: 'round',
|
|
1059
|
+
'aria-hidden': 'true',
|
|
1060
|
+
focusable: 'false',
|
|
1061
|
+
}, h('path', { d: icon.d })))
|
|
1062
|
+
})))
|
|
862
1063
|
}
|
|
863
1064
|
|
|
864
1065
|
function StateBadge(props) {
|
|
@@ -917,10 +1118,6 @@ function formatRelative(value) {
|
|
|
917
1118
|
return ''
|
|
918
1119
|
}
|
|
919
1120
|
|
|
920
|
-
function formatRangeLabel(date) {
|
|
921
|
-
return new Intl.DateTimeFormat('zh-CN', { month: 'short', day: 'numeric' }).format(date)
|
|
922
|
-
}
|
|
923
|
-
|
|
924
1121
|
function weekdayText(trigger) {
|
|
925
1122
|
return (trigger.weekdays ?? []).slice().sort((left, right) => left - right)
|
|
926
1123
|
.map((day) => WEEKDAYS.find((item) => item.value === day)?.label ?? '')
|
|
@@ -1119,16 +1316,28 @@ function triggerChanged(cadence, trigger) {
|
|
|
1119
1316
|
return next
|
|
1120
1317
|
}
|
|
1121
1318
|
|
|
1122
|
-
|
|
1319
|
+
/** New-task limits start from the settings card; leaving them untouched keeps the task inherited. */
|
|
1320
|
+
function defaultLimits(meta) {
|
|
1321
|
+
const settings = meta?.execution?.settings
|
|
1322
|
+
return {
|
|
1323
|
+
runTimeoutMinutes: settings?.runTimeoutMinutes ?? DEFAULT_LIMITS.runTimeoutMinutes,
|
|
1324
|
+
idleTimeoutMinutes: settings?.idleTimeoutMinutes ?? DEFAULT_LIMITS.idleTimeoutMinutes,
|
|
1325
|
+
maxSteps: settings?.maxSteps ?? DEFAULT_LIMITS.maxSteps,
|
|
1326
|
+
maxToolCalls: settings?.maxToolCalls ?? DEFAULT_LIMITS.maxToolCalls,
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
function defaultDraft(meta) {
|
|
1123
1331
|
return {
|
|
1124
1332
|
title: '我的投研任务',
|
|
1125
1333
|
description: '',
|
|
1126
1334
|
instruction: '',
|
|
1127
1335
|
category: 'research',
|
|
1336
|
+
icon: DEFAULT_CUSTOM_ICON,
|
|
1128
1337
|
trigger: { kind: 'schedule', cadence: 'daily', time: '09:00', timezone: 'Asia/Shanghai', tradingDaysOnly: true },
|
|
1129
1338
|
workspaceId: '',
|
|
1130
|
-
limits:
|
|
1131
|
-
notifications: { inApp:
|
|
1339
|
+
limits: defaultLimits(meta),
|
|
1340
|
+
notifications: { inApp: meta?.execution?.settings?.inAppNotifications !== false, onFailure: true, onSuccess: false },
|
|
1132
1341
|
}
|
|
1133
1342
|
}
|
|
1134
1343
|
|
|
@@ -1138,6 +1347,7 @@ function draftPayload(task) {
|
|
|
1138
1347
|
description: task.description ?? '',
|
|
1139
1348
|
instruction: task.instruction ?? '',
|
|
1140
1349
|
category: task.category,
|
|
1350
|
+
...(task.origin === 'builtin' ? {} : { icon: task.icon ?? DEFAULT_CUSTOM_ICON }),
|
|
1141
1351
|
trigger: normalizeTrigger(task.trigger),
|
|
1142
1352
|
workspaceId: task.workspaceId ?? '',
|
|
1143
1353
|
limits: task.limits,
|
|
@@ -1145,46 +1355,138 @@ function draftPayload(task) {
|
|
|
1145
1355
|
}
|
|
1146
1356
|
}
|
|
1147
1357
|
|
|
1358
|
+
function payloadKey(task) {
|
|
1359
|
+
return JSON.stringify(draftPayload(task))
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1148
1362
|
function samePayload(left, right) {
|
|
1149
|
-
return
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
function
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
for (let index = 0; index < HISTORY_DAYS; index += 1) {
|
|
1159
|
-
const date = new Date(since)
|
|
1160
|
-
date.setDate(since.getDate() + index)
|
|
1161
|
-
const key = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`
|
|
1162
|
-
const bucket = { date, key, runs: [] }
|
|
1163
|
-
days.push(bucket)
|
|
1164
|
-
byKey.set(key, bucket)
|
|
1363
|
+
return payloadKey(left) === payloadKey(right)
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
function cloneTask(task) {
|
|
1367
|
+
return {
|
|
1368
|
+
...task,
|
|
1369
|
+
trigger: normalizeTrigger(task.trigger),
|
|
1370
|
+
limits: { ...task.limits },
|
|
1371
|
+
notifications: { ...task.notifications },
|
|
1165
1372
|
}
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
function pad2(value) {
|
|
1376
|
+
return String(value).padStart(2, '0')
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
function localDateKey(date) {
|
|
1380
|
+
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
function addLocalDays(date, delta) {
|
|
1384
|
+
const next = new Date(date)
|
|
1385
|
+
next.setDate(next.getDate() + delta)
|
|
1386
|
+
return next
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
function startOfLocalWeek(date) {
|
|
1390
|
+
const start = new Date(date)
|
|
1391
|
+
start.setHours(0, 0, 0, 0)
|
|
1392
|
+
start.setDate(start.getDate() - ((start.getDay() + 6) % 7))
|
|
1393
|
+
return start
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
function parseLocalDateKey(key) {
|
|
1397
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(key ?? ''))
|
|
1398
|
+
if (match === null) return null
|
|
1399
|
+
return { year: Number(match[1]), month: Number(match[2]), day: Number(match[3]) }
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
function historyBucketMap(history) {
|
|
1403
|
+
const buckets = new Map()
|
|
1404
|
+
for (const bucket of history?.days ?? []) {
|
|
1405
|
+
if (typeof bucket?.date === 'string') buckets.set(bucket.date, bucket)
|
|
1406
|
+
}
|
|
1407
|
+
return buckets
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
function heatLevel(total) {
|
|
1411
|
+
if (total <= 0) return 0
|
|
1412
|
+
if (total === 1) return 1
|
|
1413
|
+
if (total === 2) return 2
|
|
1414
|
+
if (total <= 4) return 3
|
|
1415
|
+
return 4
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
function buildHeatmap(history) {
|
|
1419
|
+
const byDate = historyBucketMap(history)
|
|
1420
|
+
const today = new Date()
|
|
1421
|
+
today.setHours(0, 0, 0, 0)
|
|
1422
|
+
const first = addLocalDays(startOfLocalWeek(today), -7 * (HEATMAP_WEEKS - 1))
|
|
1423
|
+
const weeks = []
|
|
1424
|
+
let total = 0
|
|
1166
1425
|
let success = 0
|
|
1167
1426
|
let failure = 0
|
|
1168
|
-
let
|
|
1169
|
-
for (
|
|
1170
|
-
const
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1427
|
+
let active = 0
|
|
1428
|
+
for (let weekIndex = 0; weekIndex < HEATMAP_WEEKS; weekIndex += 1) {
|
|
1429
|
+
const weekStart = addLocalDays(first, weekIndex * 7)
|
|
1430
|
+
const days = []
|
|
1431
|
+
let label = null
|
|
1432
|
+
for (let dayIndex = 0; dayIndex < 7; dayIndex += 1) {
|
|
1433
|
+
const date = addLocalDays(weekStart, dayIndex)
|
|
1434
|
+
const key = localDateKey(date)
|
|
1435
|
+
const bucket = byDate.get(key)
|
|
1436
|
+
const dayTotal = Number.isFinite(bucket?.total) ? bucket.total : 0
|
|
1437
|
+
total += dayTotal
|
|
1438
|
+
success += Number.isFinite(bucket?.success) ? bucket.success : 0
|
|
1439
|
+
failure += Number.isFinite(bucket?.failure) ? bucket.failure : 0
|
|
1440
|
+
active += Number.isFinite(bucket?.active) ? bucket.active : 0
|
|
1441
|
+
if (date.getDate() === 1) label = `${date.getMonth() + 1}月`
|
|
1442
|
+
days.push({
|
|
1443
|
+
date: key,
|
|
1444
|
+
total: dayTotal,
|
|
1445
|
+
success: Number.isFinite(bucket?.success) ? bucket.success : 0,
|
|
1446
|
+
failure: Number.isFinite(bucket?.failure) ? bucket.failure : 0,
|
|
1447
|
+
active: Number.isFinite(bucket?.active) ? bucket.active : 0,
|
|
1448
|
+
level: heatLevel(dayTotal),
|
|
1449
|
+
future: date.getTime() > today.getTime(),
|
|
1450
|
+
})
|
|
1451
|
+
}
|
|
1452
|
+
weeks.push({ key: localDateKey(weekStart), label, days })
|
|
1453
|
+
}
|
|
1454
|
+
// A trailing window starts mid-month; label it only when the next month is far enough away.
|
|
1455
|
+
const firstLabelIndex = weeks.findIndex((week) => week.label !== null)
|
|
1456
|
+
if (weeks[0].label === null && firstLabelIndex >= 3) weeks[0].label = `${first.getMonth() + 1}月`
|
|
1457
|
+
return {
|
|
1458
|
+
weeks,
|
|
1459
|
+
total: Number.isFinite(history?.total) ? history.total : total,
|
|
1460
|
+
success: Number.isFinite(history?.success) ? history.success : success,
|
|
1461
|
+
failure: Number.isFinite(history?.failure) ? history.failure : failure,
|
|
1462
|
+
active,
|
|
1463
|
+
first: localDateKey(first),
|
|
1464
|
+
last: localDateKey(addLocalDays(first, HEATMAP_WEEKS * 7 - 1)),
|
|
1178
1465
|
}
|
|
1179
|
-
return { days, success, failure, total }
|
|
1180
1466
|
}
|
|
1181
1467
|
|
|
1182
|
-
function
|
|
1183
|
-
|
|
1184
|
-
if (
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1468
|
+
function formatHeatDay(key) {
|
|
1469
|
+
const parts = parseLocalDateKey(key)
|
|
1470
|
+
if (parts === null) return String(key ?? '')
|
|
1471
|
+
const date = new Date(parts.year, parts.month - 1, parts.day)
|
|
1472
|
+
return `${parts.year}年${parts.month}月${parts.day}日 ${WEEKDAY_SHORT[date.getDay()]}`
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
function formatHeatRange(first, last) {
|
|
1476
|
+
const left = parseLocalDateKey(first)
|
|
1477
|
+
const right = parseLocalDateKey(last)
|
|
1478
|
+
if (left === null || right === null) return ''
|
|
1479
|
+
return `${left.year}.${pad2(left.month)} – ${right.year}.${pad2(right.month)}`
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
function heatDayMeta(day) {
|
|
1483
|
+
if (day.future) return zh.heatFuture
|
|
1484
|
+
if (day.total === 0) return zh.heatNoRuns
|
|
1485
|
+
const parts = [zh.runCount.replace('{n}', String(day.total))]
|
|
1486
|
+
if (day.success > 0) parts.push(zh.successCount.replace('{n}', String(day.success)))
|
|
1487
|
+
if (day.failure > 0) parts.push(zh.failureCount.replace('{n}', String(day.failure)))
|
|
1488
|
+
if (day.active > 0) parts.push(zh.activeCount.replace('{n}', String(day.active)))
|
|
1489
|
+
return parts.join(' · ')
|
|
1188
1490
|
}
|
|
1189
1491
|
|
|
1190
1492
|
function eventTone(kind) {
|
|
@@ -1204,11 +1506,7 @@ function notificationMessage(item) {
|
|
|
1204
1506
|
return ''
|
|
1205
1507
|
}
|
|
1206
1508
|
|
|
1207
|
-
function
|
|
1208
|
-
return !samePayload(draft, task)
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
function draftIssues(task) {
|
|
1509
|
+
function draftIssues(task, meta) {
|
|
1212
1510
|
const issues = {}
|
|
1213
1511
|
if (String(task.title ?? '').trim().length === 0) issues.title = zh.titleRequired
|
|
1214
1512
|
if (String(task.instruction ?? '').trim().length === 0) issues.instruction = zh.instructionRequired
|
|
@@ -1230,7 +1528,15 @@ function draftIssues(task) {
|
|
|
1230
1528
|
}
|
|
1231
1529
|
const limits = task.limits ?? {}
|
|
1232
1530
|
for (const key of ['runTimeoutMinutes', 'idleTimeoutMinutes', 'maxSteps', 'maxToolCalls']) {
|
|
1233
|
-
|
|
1531
|
+
const value = limits[key]
|
|
1532
|
+
if (!Number.isFinite(value) || !Number.isInteger(value)) {
|
|
1533
|
+
issues[key] = zh.invalidLimit
|
|
1534
|
+
continue
|
|
1535
|
+
}
|
|
1536
|
+
const range = meta?.execution?.limits?.[key]
|
|
1537
|
+
if (Array.isArray(range) && range.length === 2 && (value < range[0] || value > range[1])) {
|
|
1538
|
+
issues[key] = zh.invalidLimitRange.replace('{min}', String(range[0])).replace('{max}', String(range[1]))
|
|
1539
|
+
}
|
|
1234
1540
|
}
|
|
1235
1541
|
return issues
|
|
1236
1542
|
}
|
|
@@ -1239,9 +1545,9 @@ function draftIssues(task) {
|
|
|
1239
1545
|
* Tracks which fields the user has left so errors appear on blur, then update live.
|
|
1240
1546
|
* `resetKey` clears that state when the edited task or modal session changes.
|
|
1241
1547
|
*/
|
|
1242
|
-
function useDraftIssues(task, resetKey) {
|
|
1548
|
+
function useDraftIssues(task, resetKey, meta) {
|
|
1243
1549
|
const [touched, setTouched] = useState({})
|
|
1244
|
-
const issues = draftIssues(task)
|
|
1550
|
+
const issues = draftIssues(task, meta)
|
|
1245
1551
|
useEffect(() => { setTouched({}) }, [resetKey])
|
|
1246
1552
|
const errorFor = (field) => (touched[field] === true ? issues[field] : undefined)
|
|
1247
1553
|
const touch = (field) => setTouched((current) => (current[field] === true ? current : { ...current, [field]: true }))
|
|
@@ -1250,8 +1556,7 @@ function useDraftIssues(task, resetKey) {
|
|
|
1250
1556
|
for (const field of Object.keys(issues)) next[field] = true
|
|
1251
1557
|
return next
|
|
1252
1558
|
})
|
|
1253
|
-
|
|
1254
|
-
return { issues, errorFor, touch, touchAll, clear }
|
|
1559
|
+
return { issues, errorFor, touch, touchAll }
|
|
1255
1560
|
}
|
|
1256
1561
|
|
|
1257
1562
|
function limitValue(meta, key, fallback) {
|
|
@@ -1260,13 +1565,57 @@ function limitValue(meta, key, fallback) {
|
|
|
1260
1565
|
}
|
|
1261
1566
|
|
|
1262
1567
|
function RunHistoryCard(props) {
|
|
1263
|
-
const {
|
|
1264
|
-
const
|
|
1265
|
-
|
|
1568
|
+
const { history, onViewAll, showLink = true } = props
|
|
1569
|
+
const model = buildHeatmap(history)
|
|
1570
|
+
const { total, success, failure, active, weeks } = model
|
|
1571
|
+
const cardRef = useRef(null)
|
|
1572
|
+
const scrollRef = useRef(null)
|
|
1573
|
+
const [tip, setTip] = useState(null)
|
|
1574
|
+
|
|
1575
|
+
useEffect(() => {
|
|
1576
|
+
const node = scrollRef.current
|
|
1577
|
+
if (node === null) return undefined
|
|
1578
|
+
node.scrollLeft = node.scrollWidth
|
|
1579
|
+
const syncFade = () => { node.dataset.faded = node.scrollLeft > 1 ? 'true' : 'false' }
|
|
1580
|
+
node.addEventListener('scroll', syncFade, { passive: true })
|
|
1581
|
+
syncFade()
|
|
1582
|
+
return () => node.removeEventListener('scroll', syncFade)
|
|
1583
|
+
}, [])
|
|
1584
|
+
|
|
1585
|
+
function showTip(day, event) {
|
|
1586
|
+
const card = cardRef.current
|
|
1587
|
+
if (card === null || event === undefined) return
|
|
1588
|
+
const cell = event.currentTarget.getBoundingClientRect()
|
|
1589
|
+
const base = card.getBoundingClientRect()
|
|
1590
|
+
const half = 110
|
|
1591
|
+
const left = Math.min(Math.max(cell.left - base.left + cell.width / 2, half), Math.max(half, base.width - half))
|
|
1592
|
+
setTip({ day, left, top: cell.top - base.top - 7 })
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
const monthLabels = weeks.map((week, index) => week.label === null ? null : h('span', {
|
|
1596
|
+
key: week.key,
|
|
1597
|
+
className: 'capital-automation-heatmonth',
|
|
1598
|
+
style: index >= HEATMAP_WEEKS - 2
|
|
1599
|
+
? { gridColumn: index + 1, justifySelf: 'end' }
|
|
1600
|
+
: { gridColumn: index + 1 },
|
|
1601
|
+
}, week.label))
|
|
1602
|
+
|
|
1603
|
+
const cells = weeks.flatMap((week) => week.days.map((day) => h('span', {
|
|
1604
|
+
key: day.date,
|
|
1605
|
+
className: 'capital-automation-heatcell',
|
|
1606
|
+
'data-level': day.level,
|
|
1607
|
+
'data-future': day.future ? 'true' : undefined,
|
|
1608
|
+
'data-failed': day.failure > 0 ? 'true' : undefined,
|
|
1609
|
+
'data-active': day.active > 0 ? 'true' : undefined,
|
|
1610
|
+
title: `${formatHeatDay(day.date)} · ${heatDayMeta(day)}`,
|
|
1611
|
+
onMouseEnter: (event) => showTip(day, event),
|
|
1612
|
+
})))
|
|
1613
|
+
|
|
1614
|
+
return h('section', { ref: cardRef, className: 'capital-automation-card capital-automation-history', 'aria-label': zh.runHistory },
|
|
1266
1615
|
h('div', { className: 'capital-automation-historyhead' },
|
|
1267
1616
|
h('div', { className: 'capital-automation-historytitle' },
|
|
1268
1617
|
h('h2', null, zh.runHistory),
|
|
1269
|
-
h('span', null, zh.
|
|
1618
|
+
h('span', null, zh.last12Months)),
|
|
1270
1619
|
h('div', { className: 'capital-automation-historystats' },
|
|
1271
1620
|
h('span', { className: 'capital-automation-historystat', 'data-tone': 'ok' },
|
|
1272
1621
|
h('span', { className: 'capital-automation-statusdot' }),
|
|
@@ -1274,21 +1623,56 @@ function RunHistoryCard(props) {
|
|
|
1274
1623
|
h('span', { className: 'capital-automation-historystat', 'data-tone': 'error' },
|
|
1275
1624
|
h('span', { className: 'capital-automation-statusdot' }),
|
|
1276
1625
|
zh.failureCount.replace('{n}', String(failure))),
|
|
1626
|
+
active > 0
|
|
1627
|
+
? h('span', { className: 'capital-automation-historystat', 'data-tone': 'active' },
|
|
1628
|
+
h('span', { className: 'capital-automation-statusdot' }),
|
|
1629
|
+
zh.activeCount.replace('{n}', String(active)))
|
|
1630
|
+
: null,
|
|
1277
1631
|
showLink && onViewAll !== undefined
|
|
1278
1632
|
? h('button', { type: 'button', className: 'capital-automation-historylink', onClick: onViewAll },
|
|
1279
1633
|
zh.runCount.replace('{n}', String(total)),
|
|
1280
1634
|
h(IconChevronRightOutlineRegular, { size: 13 }))
|
|
1281
1635
|
: h('span', { className: 'capital-automation-historystat' }, zh.runCount.replace('{n}', String(total))))),
|
|
1282
|
-
h('div', { className: 'capital-automation-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1636
|
+
h('div', { className: 'capital-automation-heatmap' },
|
|
1637
|
+
h('div', { className: 'capital-automation-heatweekdays', 'aria-hidden': 'true' },
|
|
1638
|
+
[0, 1, 2, 3, 4, 5, 6].map((index) => h('span', { key: index, className: 'capital-automation-heatweekday' },
|
|
1639
|
+
index === 0 ? '周一' : index === 2 ? '周三' : index === 4 ? '周五' : ''))),
|
|
1640
|
+
h('div', { ref: scrollRef, className: 'capital-automation-heatscroll' },
|
|
1641
|
+
h('div', { className: 'capital-automation-heatcanvas' },
|
|
1642
|
+
h('div', { className: 'capital-automation-heatmonths', 'aria-hidden': 'true' }, monthLabels),
|
|
1643
|
+
h('div', {
|
|
1644
|
+
className: 'capital-automation-heatgrid',
|
|
1645
|
+
role: 'img',
|
|
1646
|
+
'aria-label': zh.heatSummary
|
|
1647
|
+
.replace('{total}', String(total))
|
|
1648
|
+
.replace('{success}', String(success))
|
|
1649
|
+
.replace('{failure}', String(failure)),
|
|
1650
|
+
onMouseLeave: () => setTip(null),
|
|
1651
|
+
}, cells)))),
|
|
1652
|
+
h('div', { className: 'capital-automation-heatfoot' },
|
|
1653
|
+
h('span', { className: 'capital-automation-heatrange' },
|
|
1654
|
+
total === 0 ? zh.historyEmpty : formatHeatRange(model.first, model.last)),
|
|
1655
|
+
h('div', { className: 'capital-automation-heatlegend' },
|
|
1656
|
+
active > 0
|
|
1657
|
+
? h('span', { className: 'capital-automation-heatkey' },
|
|
1658
|
+
h('span', { className: 'capital-automation-heatactivebox' }),
|
|
1659
|
+
zh.heatActive)
|
|
1660
|
+
: null,
|
|
1661
|
+
failure > 0
|
|
1662
|
+
? h('span', { className: 'capital-automation-heatkey' },
|
|
1663
|
+
h('span', { className: 'capital-automation-heatfailbox' }),
|
|
1664
|
+
zh.heatFailed)
|
|
1665
|
+
: null,
|
|
1666
|
+
h('span', { className: 'capital-automation-heatlegendlabel' }, zh.heatLess),
|
|
1667
|
+
[0, 1, 2, 3, 4].map((level) => h('span', { key: level, className: 'capital-automation-heatsample', 'data-level': level })),
|
|
1668
|
+
h('span', { className: 'capital-automation-heatlegendlabel' }, zh.heatMore))),
|
|
1669
|
+
tip === null ? null : h('div', {
|
|
1670
|
+
className: 'capital-automation-heattip',
|
|
1671
|
+
role: 'tooltip',
|
|
1672
|
+
style: { left: `${tip.left}px`, top: `${tip.top}px` },
|
|
1673
|
+
},
|
|
1674
|
+
h('span', { className: 'capital-automation-heattipdate' }, formatHeatDay(tip.day.date)),
|
|
1675
|
+
h('span', { className: 'capital-automation-heattipmeta' }, heatDayMeta(tip.day))))
|
|
1292
1676
|
}
|
|
1293
1677
|
|
|
1294
1678
|
function RunStatus(props) {
|
|
@@ -1378,14 +1762,60 @@ function NotificationsButton(props) {
|
|
|
1378
1762
|
className: 'capital-automation-btn',
|
|
1379
1763
|
'data-variant': 'ghost',
|
|
1380
1764
|
onClick,
|
|
1381
|
-
title: zh.
|
|
1382
|
-
'aria-label': zh.
|
|
1765
|
+
title: zh.notificationsEntry,
|
|
1766
|
+
'aria-label': zh.notificationsEntry,
|
|
1383
1767
|
},
|
|
1384
1768
|
h(IconAlarmClockOutlineRegular, { size: 14 }),
|
|
1385
|
-
h('span', { className: 'capital-automation-btnlabel' }, zh.
|
|
1769
|
+
h('span', { className: 'capital-automation-btnlabel' }, zh.notificationsEntry),
|
|
1386
1770
|
unread > 0 ? h('span', { className: 'capital-automation-count' }, String(unread > 99 ? '99+' : unread)) : null)
|
|
1387
1771
|
}
|
|
1388
1772
|
|
|
1773
|
+
function systemStatusReason(code) {
|
|
1774
|
+
if (code === 'lease_held') return zh.systemReasonLeaseHeld
|
|
1775
|
+
if (code === 'owner_unverified') return zh.systemReasonUnverified
|
|
1776
|
+
return zh.systemReasonLeaseLost
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
/** Profile-level scheduling state lives here, not in the task inbox. */
|
|
1780
|
+
function SystemStatus(props) {
|
|
1781
|
+
const { meta } = props
|
|
1782
|
+
const [open, setOpen] = useState(false)
|
|
1783
|
+
if (meta === null || meta === undefined) return null
|
|
1784
|
+
const scheduler = meta.scheduler ?? {}
|
|
1785
|
+
const state = scheduler.state
|
|
1786
|
+
const summary = state === 'active'
|
|
1787
|
+
? zh.systemActive
|
|
1788
|
+
: state === 'standby'
|
|
1789
|
+
? zh.systemStandby
|
|
1790
|
+
: state === 'starting'
|
|
1791
|
+
? zh.systemStarting
|
|
1792
|
+
: state === 'stopped'
|
|
1793
|
+
? zh.systemStopped
|
|
1794
|
+
: zh.systemUnavailable
|
|
1795
|
+
const calendarSummary = meta.calendar?.available === false ? zh.systemCalendarUnavailable : zh.systemCalendarOk
|
|
1796
|
+
const lastStandbyAt = scheduler.lastStandbyAt
|
|
1797
|
+
return h('section', { className: 'capital-automation-systemstatus', 'aria-label': zh.systemStatus },
|
|
1798
|
+
h('div', { className: 'capital-automation-systemline' },
|
|
1799
|
+
h('span', null, `${zh.systemStatus}:${summary} · ${calendarSummary}`),
|
|
1800
|
+
h('button', {
|
|
1801
|
+
type: 'button',
|
|
1802
|
+
className: 'capital-automation-systemtoggle',
|
|
1803
|
+
'aria-expanded': open ? 'true' : 'false',
|
|
1804
|
+
onClick: () => setOpen((value) => !value),
|
|
1805
|
+
}, open ? zh.systemHideDetails : zh.systemDetails)),
|
|
1806
|
+
open
|
|
1807
|
+
? h('div', { className: 'capital-automation-systemdetail' },
|
|
1808
|
+
lastStandbyAt
|
|
1809
|
+
? h('p', null, `${zh.systemLastStandby}:${formatShortTime(lastStandbyAt)} · ${systemStatusReason(scheduler.lastStandbyCode)}`)
|
|
1810
|
+
: h('p', null, zh.systemNoStandby),
|
|
1811
|
+
lastStandbyAt && scheduler.lastStandbyReason
|
|
1812
|
+
? h('details', null,
|
|
1813
|
+
h('summary', null, zh.systemTechnical),
|
|
1814
|
+
h('p', null, scheduler.lastStandbyReason))
|
|
1815
|
+
: null)
|
|
1816
|
+
: null)
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1389
1819
|
function Header(props) {
|
|
1390
1820
|
const { tab, onTab, onNewTask, unread, onOpenInbox } = props
|
|
1391
1821
|
return h('header', { className: 'capital-automation-head' },
|
|
@@ -1649,15 +2079,21 @@ function SkeletonHistory() {
|
|
|
1649
2079
|
h(SkeletonBlock, { width: 64, height: 12 }),
|
|
1650
2080
|
h(SkeletonBlock, { width: 64, height: 12 }),
|
|
1651
2081
|
h(SkeletonBlock, { width: 56, height: 12 }))),
|
|
1652
|
-
h('div', { className: 'capital-automation-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
h(
|
|
1660
|
-
|
|
2082
|
+
h('div', { className: 'capital-automation-skeletonheat' },
|
|
2083
|
+
h('div', { className: 'capital-automation-skeletonmonths' },
|
|
2084
|
+
Array.from({ length: 12 }, (_, index) => h('span', {
|
|
2085
|
+
key: index,
|
|
2086
|
+
className: 'capital-automation-skeleton',
|
|
2087
|
+
style: { gridColumn: 1 + index * 4, width: 22, height: 8 },
|
|
2088
|
+
}))),
|
|
2089
|
+
h('div', { className: 'capital-automation-skeletongrid' },
|
|
2090
|
+
Array.from({ length: HEATMAP_WEEKS * 7 }, (_, index) => h('span', {
|
|
2091
|
+
key: index,
|
|
2092
|
+
className: 'capital-automation-skeleton',
|
|
2093
|
+
})))),
|
|
2094
|
+
h('div', { className: 'capital-automation-heatfoot' },
|
|
2095
|
+
h(SkeletonBlock, { width: 108, height: 9 }),
|
|
2096
|
+
h(SkeletonBlock, { width: 96, height: 9 })))
|
|
1661
2097
|
}
|
|
1662
2098
|
|
|
1663
2099
|
function SkeletonSectionHead(props) {
|
|
@@ -1745,8 +2181,7 @@ function LoadingSkeleton(props) {
|
|
|
1745
2181
|
|
|
1746
2182
|
function TaskDashboardSkeleton() {
|
|
1747
2183
|
return h(LoadingSkeleton, null,
|
|
1748
|
-
h(
|
|
1749
|
-
h('section', { className: 'capital-automation-section' },
|
|
2184
|
+
h('section', { className: 'capital-automation-section', style: { marginTop: 0 } },
|
|
1750
2185
|
h(SkeletonSectionHead),
|
|
1751
2186
|
h(SkeletonTaskTable, { rows: 4 })),
|
|
1752
2187
|
h('section', { className: 'capital-automation-section' },
|
|
@@ -1766,7 +2201,7 @@ function RunsSkeleton() {
|
|
|
1766
2201
|
}
|
|
1767
2202
|
|
|
1768
2203
|
function TaskDashboard(props) {
|
|
1769
|
-
const { tasks, templates, runs, onOpenTask, onNewTask, onEnableTemplate, onAction, onRequestConfirm, onEdit,
|
|
2204
|
+
const { tasks, templates, runs, onOpenTask, onNewTask, onEnableTemplate, onAction, onRequestConfirm, onEdit, onRequestCancelRun, schedulerActive, storageReady, busyTaskId } = props
|
|
1770
2205
|
const [filter, setFilter] = useState('all')
|
|
1771
2206
|
const [query, setQuery] = useState('')
|
|
1772
2207
|
const runningTasks = tasks.filter((task) => isActiveRun(task.lastRun))
|
|
@@ -1786,11 +2221,12 @@ function TaskDashboard(props) {
|
|
|
1786
2221
|
market: roster.filter((task) => task.category === 'market').length,
|
|
1787
2222
|
review: roster.filter((task) => task.category === 'review').length,
|
|
1788
2223
|
research: roster.filter((task) => task.category === 'research').length,
|
|
2224
|
+
portfolio: roster.filter((task) => task.category === 'portfolio').length,
|
|
2225
|
+
monitor: roster.filter((task) => task.category === 'monitor').length,
|
|
1789
2226
|
}
|
|
1790
|
-
const filters = ['all', 'custom', 'news', 'market', 'review', 'research']
|
|
2227
|
+
const filters = ['all', 'custom', 'news', 'market', 'review', 'research', 'portfolio', 'monitor']
|
|
1791
2228
|
return h('div', null,
|
|
1792
|
-
h(
|
|
1793
|
-
h('section', { className: 'capital-automation-section' },
|
|
2229
|
+
h('section', { className: 'capital-automation-section', style: { marginTop: 0 } },
|
|
1794
2230
|
h('div', { className: 'capital-automation-sectionhead' },
|
|
1795
2231
|
h('h2', null, zh.taskSection),
|
|
1796
2232
|
h('span', null, zh.taskSectionHint),
|
|
@@ -1992,12 +2428,13 @@ function MonthlySelect(props) {
|
|
|
1992
2428
|
}
|
|
1993
2429
|
|
|
1994
2430
|
function TriggerFields(props) {
|
|
1995
|
-
const { trigger, onChange, disabled, meta, errorFor, touch } = props
|
|
2431
|
+
const { trigger, onChange, disabled, meta, errorFor, touch, onFlush } = props
|
|
1996
2432
|
const update = (patch) => onChange(normalizeTrigger({ ...trigger, ...patch }))
|
|
1997
2433
|
const setCadence = (cadence) => onChange(triggerChanged(cadence, trigger))
|
|
1998
2434
|
|
|
1999
2435
|
const issue = (field) => (typeof errorFor === 'function' ? errorFor(field) : undefined)
|
|
2000
2436
|
const touchField = (field) => { if (typeof touch === 'function') touch(field) }
|
|
2437
|
+
const commit = () => { if (typeof onFlush === 'function') onFlush() }
|
|
2001
2438
|
const timeError = issue('time')
|
|
2002
2439
|
const timezoneError = issue('timezone')
|
|
2003
2440
|
const weekdayError = issue('weekdays')
|
|
@@ -2051,7 +2488,7 @@ function TriggerFields(props) {
|
|
|
2051
2488
|
'aria-invalid': dateError ? 'true' : undefined,
|
|
2052
2489
|
'data-invalid': dateError ? 'true' : undefined,
|
|
2053
2490
|
onChange: (event) => update({ date: event.target.value }),
|
|
2054
|
-
onBlur: () => touchField('date'),
|
|
2491
|
+
onBlur: () => { touchField('date'); commit() },
|
|
2055
2492
|
}))
|
|
2056
2493
|
: null)),
|
|
2057
2494
|
h('div', { className: 'capital-automation-triggerfield' },
|
|
@@ -2065,7 +2502,7 @@ function TriggerFields(props) {
|
|
|
2065
2502
|
'aria-invalid': timeError ? 'true' : undefined,
|
|
2066
2503
|
'data-invalid': timeError ? 'true' : undefined,
|
|
2067
2504
|
onChange: (event) => update({ time: event.target.value }),
|
|
2068
|
-
onBlur: () => touchField('time'),
|
|
2505
|
+
onBlur: () => { touchField('time'); commit() },
|
|
2069
2506
|
}))),
|
|
2070
2507
|
h('div', { className: 'capital-automation-triggersettings' },
|
|
2071
2508
|
h('span', { className: 'capital-automation-triggerzone' },
|
|
@@ -2282,7 +2719,7 @@ function RunFilters(props) {
|
|
|
2282
2719
|
}
|
|
2283
2720
|
|
|
2284
2721
|
function RunsPage(props) {
|
|
2285
|
-
const { runs, tasks, onCancel, onOpenSession, onOpenTask, onGoTasks, onNewTask, busy } = props
|
|
2722
|
+
const { runs, history, tasks, onCancel, onOpenSession, onOpenTask, onGoTasks, onNewTask, busy } = props
|
|
2286
2723
|
const [filter, setFilter] = useState('all')
|
|
2287
2724
|
const counts = {
|
|
2288
2725
|
all: runs.length,
|
|
@@ -2297,7 +2734,7 @@ function RunsPage(props) {
|
|
|
2297
2734
|
return true
|
|
2298
2735
|
})
|
|
2299
2736
|
return h('div', null,
|
|
2300
|
-
h(RunHistoryCard, {
|
|
2737
|
+
h(RunHistoryCard, { history, showLink: false }),
|
|
2301
2738
|
h('section', { className: 'capital-automation-section' },
|
|
2302
2739
|
h('div', { className: 'capital-automation-sectionhead' },
|
|
2303
2740
|
h('h2', null, zh.runsTitle),
|
|
@@ -2393,41 +2830,31 @@ function OverviewTab(props) {
|
|
|
2393
2830
|
}
|
|
2394
2831
|
|
|
2395
2832
|
function ConfigTab(props) {
|
|
2396
|
-
const { draft, setDraft, meta, busy, dirty,
|
|
2833
|
+
const { draft, setDraft, meta, busy, dirty, saveState, saveError, retry, onFlush, disabled } = props
|
|
2397
2834
|
const task = draft
|
|
2398
|
-
const {
|
|
2399
|
-
const formRef = useRef(null)
|
|
2835
|
+
const { errorFor, touch } = useDraftIssues(task, task.id, meta)
|
|
2400
2836
|
const titleError = errorFor('title')
|
|
2401
2837
|
const instructionError = errorFor('instruction')
|
|
2402
2838
|
const set = (patch) => setDraft((current) => ({
|
|
2403
2839
|
...current,
|
|
2404
2840
|
...(typeof patch === 'function' ? patch(current) : patch),
|
|
2405
2841
|
}))
|
|
2406
|
-
const
|
|
2407
|
-
if (Object.keys(issues).length > 0) {
|
|
2408
|
-
touchAll()
|
|
2409
|
-
setTimeout(() => {
|
|
2410
|
-
const target = formRef.current?.querySelector('[data-invalid="true"] input, [data-invalid="true"] .capital-automation-texteditor')
|
|
2411
|
-
target?.focus?.()
|
|
2412
|
-
}, 0)
|
|
2413
|
-
return
|
|
2414
|
-
}
|
|
2415
|
-
void save()
|
|
2416
|
-
}
|
|
2842
|
+
const flush = () => { if (typeof onFlush === 'function') onFlush() }
|
|
2417
2843
|
const timeoutRange = limitValue(meta, 'runTimeoutMinutes', [5, 120])
|
|
2418
2844
|
const idleRange = limitValue(meta, 'idleTimeoutMinutes', [1, 30])
|
|
2419
|
-
const stepsRange = limitValue(meta, 'maxSteps', [
|
|
2420
|
-
const toolsRange = limitValue(meta, 'maxToolCalls', [
|
|
2845
|
+
const stepsRange = limitValue(meta, 'maxSteps', [100, 2000])
|
|
2846
|
+
const toolsRange = limitValue(meta, 'maxToolCalls', [100, 5000])
|
|
2421
2847
|
const instructionEditor = h(Field, { label: zh.instructionField, error: instructionError },
|
|
2422
2848
|
h(PlainTextEditor, {
|
|
2423
2849
|
className: 'capital-automation-texteditor',
|
|
2424
2850
|
value: task.instruction,
|
|
2851
|
+
maxLength: INSTRUCTION_MAX_LENGTH,
|
|
2425
2852
|
disabled: busy || disabled,
|
|
2426
2853
|
placeholder: zh.instructionPlaceholder,
|
|
2427
2854
|
ariaLabel: zh.instructionField,
|
|
2428
2855
|
invalid: instructionError ? true : undefined,
|
|
2429
2856
|
onChange: (instruction) => set({ instruction }),
|
|
2430
|
-
onBlur: () => touch('instruction'),
|
|
2857
|
+
onBlur: () => { touch('instruction'); flush() },
|
|
2431
2858
|
}))
|
|
2432
2859
|
const instructionSide = h('div', { className: 'capital-automation-instructionside' },
|
|
2433
2860
|
h(SelectField, {
|
|
@@ -2440,24 +2867,43 @@ function ConfigTab(props) {
|
|
|
2440
2867
|
meta?.workspaceError ? h('p', { className: 'capital-automation-hint', 'data-tone': 'error' }, zh.workspaceUnavailable) : null,
|
|
2441
2868
|
h('div', null,
|
|
2442
2869
|
h('span', { className: 'capital-automation-formlabel' }, zh.triggerField),
|
|
2443
|
-
h(TriggerFields, { trigger: task.trigger, disabled: busy || disabled, onChange: (trigger) => set({ trigger }), meta, errorFor, touch })))
|
|
2870
|
+
h(TriggerFields, { trigger: task.trigger, disabled: busy || disabled, onChange: (trigger) => set({ trigger }), meta, errorFor, touch, onFlush: flush })))
|
|
2444
2871
|
const advanced = h('details', { className: 'capital-automation-advanced' },
|
|
2445
2872
|
h('summary', null, zh.advanced),
|
|
2446
2873
|
h('div', { className: 'capital-automation-advancedbody' },
|
|
2447
2874
|
h('div', { className: 'capital-automation-formgrid' },
|
|
2448
|
-
h(NumberField, { label: zh.runTimeout, value: task.limits.runTimeoutMinutes, min: timeoutRange[0], max: timeoutRange[1], disabled: busy || disabled, error: errorFor('runTimeoutMinutes'), onBlur: () => touch('runTimeoutMinutes'), onChange: (value) => set((current) => ({ limits: { ...current.limits, runTimeoutMinutes: value } })) }),
|
|
2449
|
-
h(NumberField, { label: zh.idleTimeout, value: task.limits.idleTimeoutMinutes, min: idleRange[0], max: idleRange[1], disabled: busy || disabled, error: errorFor('idleTimeoutMinutes'), onBlur: () => touch('idleTimeoutMinutes'), onChange: (value) => set((current) => ({ limits: { ...current.limits, idleTimeoutMinutes: value } })) }),
|
|
2450
|
-
h(NumberField, { label: zh.maxSteps, value: task.limits.maxSteps, min: stepsRange[0], max: stepsRange[1], disabled: busy || disabled, error: errorFor('maxSteps'), onBlur: () => touch('maxSteps'), onChange: (value) => set((current) => ({ limits: { ...current.limits, maxSteps: value } })) }),
|
|
2451
|
-
h(NumberField, { label: zh.maxToolCalls, value: task.limits.maxToolCalls, min: toolsRange[0], max: toolsRange[1], disabled: busy || disabled, error: errorFor('maxToolCalls'), onBlur: () => touch('maxToolCalls'), onChange: (value) => set((current) => ({ limits: { ...current.limits, maxToolCalls: value } })) })),
|
|
2875
|
+
h(NumberField, { label: zh.runTimeout, value: task.limits.runTimeoutMinutes, min: timeoutRange[0], max: timeoutRange[1], disabled: busy || disabled, error: errorFor('runTimeoutMinutes'), onBlur: () => { touch('runTimeoutMinutes'); flush() }, onChange: (value) => set((current) => ({ limits: { ...current.limits, runTimeoutMinutes: value } })) }),
|
|
2876
|
+
h(NumberField, { label: zh.idleTimeout, value: task.limits.idleTimeoutMinutes, min: idleRange[0], max: idleRange[1], disabled: busy || disabled, error: errorFor('idleTimeoutMinutes'), onBlur: () => { touch('idleTimeoutMinutes'); flush() }, onChange: (value) => set((current) => ({ limits: { ...current.limits, idleTimeoutMinutes: value } })) }),
|
|
2877
|
+
h(NumberField, { label: zh.maxSteps, value: task.limits.maxSteps, min: stepsRange[0], max: stepsRange[1], disabled: busy || disabled, error: errorFor('maxSteps'), onBlur: () => { touch('maxSteps'); flush() }, onChange: (value) => set((current) => ({ limits: { ...current.limits, maxSteps: value } })) }),
|
|
2878
|
+
h(NumberField, { label: zh.maxToolCalls, value: task.limits.maxToolCalls, min: toolsRange[0], max: toolsRange[1], disabled: busy || disabled, error: errorFor('maxToolCalls'), onBlur: () => { touch('maxToolCalls'); flush() }, onChange: (value) => set((current) => ({ limits: { ...current.limits, maxToolCalls: value } })) })),
|
|
2452
2879
|
h('p', { className: 'capital-automation-hint' }, zh.budgetHint)))
|
|
2453
2880
|
const notificationRows = h('div', null,
|
|
2454
2881
|
h('span', { className: 'capital-automation-formlabel' }, zh.notifyField),
|
|
2455
2882
|
h('div', { className: 'capital-automation-switchlist' },
|
|
2456
2883
|
h(SwitchRow, { label: zh.notifyInApp, checked: task.notifications.inApp, disabled: busy || disabled, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, inApp: value } })) }),
|
|
2457
|
-
h(SwitchRow, { label: zh.notifyFailure, checked: task.notifications.onFailure, disabled: busy || disabled, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, onFailure: value } })) }),
|
|
2458
|
-
h(SwitchRow, { label: zh.notifySuccess, checked: task.notifications.onSuccess, disabled: busy || disabled, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, onSuccess: value } })) })),
|
|
2884
|
+
h(SwitchRow, { label: zh.notifyFailure, checked: task.notifications.onFailure, disabled: busy || disabled || task.notifications.inApp !== true, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, onFailure: value } })) }),
|
|
2885
|
+
h(SwitchRow, { label: zh.notifySuccess, checked: task.notifications.onSuccess, disabled: busy || disabled || task.notifications.inApp !== true, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, onSuccess: value } })) })),
|
|
2459
2886
|
h('p', { className: 'capital-automation-hint' }, zh.notifyHint))
|
|
2460
|
-
|
|
2887
|
+
const state = disabled
|
|
2888
|
+
? 'disabled'
|
|
2889
|
+
: dirty && (saveState === 'idle' || saveState === 'saved')
|
|
2890
|
+
? 'pending'
|
|
2891
|
+
: !dirty && saveState === 'pending'
|
|
2892
|
+
? 'idle'
|
|
2893
|
+
: saveState
|
|
2894
|
+
const statusText = state === 'saving'
|
|
2895
|
+
? zh.saving
|
|
2896
|
+
: state === 'error'
|
|
2897
|
+
? `${zh.autosaveFailed}:${saveError || zh.errorGeneric}`
|
|
2898
|
+
: state === 'invalid'
|
|
2899
|
+
? zh.autosaveInvalid
|
|
2900
|
+
: state === 'pending'
|
|
2901
|
+
? zh.autosavePending
|
|
2902
|
+
: state === 'saved'
|
|
2903
|
+
? zh.autosaveSaved
|
|
2904
|
+
: zh.noDirty
|
|
2905
|
+
const statusTone = state === 'error' ? 'error' : state === 'saved' ? 'ok' : undefined
|
|
2906
|
+
return h('div', { className: 'capital-automation-form' },
|
|
2461
2907
|
h('div', { className: 'capital-automation-titlerow' },
|
|
2462
2908
|
h(Field, {
|
|
2463
2909
|
label: zh.titleField,
|
|
@@ -2470,7 +2916,7 @@ function ConfigTab(props) {
|
|
|
2470
2916
|
disabled: busy || disabled,
|
|
2471
2917
|
'aria-invalid': titleError ? 'true' : undefined,
|
|
2472
2918
|
onChange: (event) => set({ title: event.target.value }),
|
|
2473
|
-
onBlur: () => touch('title'),
|
|
2919
|
+
onBlur: () => { touch('title'); flush() },
|
|
2474
2920
|
})),
|
|
2475
2921
|
h(SelectField, {
|
|
2476
2922
|
label: zh.categoryField,
|
|
@@ -2480,6 +2926,18 @@ function ConfigTab(props) {
|
|
|
2480
2926
|
disabled: busy || disabled || task.origin === 'builtin',
|
|
2481
2927
|
onChange: (category) => set({ category }),
|
|
2482
2928
|
})),
|
|
2929
|
+
task.origin === 'custom'
|
|
2930
|
+
? h('div', { className: 'capital-automation-iconrow' },
|
|
2931
|
+
h(IconPickerTrigger, {
|
|
2932
|
+
task,
|
|
2933
|
+
size: 38,
|
|
2934
|
+
disabled: busy || disabled,
|
|
2935
|
+
onChange: (icon) => set({ icon }),
|
|
2936
|
+
}),
|
|
2937
|
+
h('div', { className: 'capital-automation-iconrowbody' },
|
|
2938
|
+
h('span', { className: 'capital-automation-formlabel' }, zh.iconField),
|
|
2939
|
+
h('span', { className: 'capital-automation-hint' }, `${taskIcon(task).label} · ${zh.iconPickHint}`)))
|
|
2940
|
+
: h('p', { className: 'capital-automation-hint' }, zh.iconBuiltinHint),
|
|
2483
2941
|
h(Field, {
|
|
2484
2942
|
label: zh.descriptionField,
|
|
2485
2943
|
note: zh.charCount.replace('{n}', String(Array.from(task.description ?? '').length)).replace('{max}', String(DESCRIPTION_MAX_LENGTH)),
|
|
@@ -2492,6 +2950,7 @@ function ConfigTab(props) {
|
|
|
2492
2950
|
placeholder: zh.descriptionPlaceholder,
|
|
2493
2951
|
ariaLabel: zh.descriptionField,
|
|
2494
2952
|
onChange: (description) => set({ description }),
|
|
2953
|
+
onBlur: flush,
|
|
2495
2954
|
})),
|
|
2496
2955
|
h('div', { className: 'capital-automation-instructionrow' },
|
|
2497
2956
|
instructionEditor,
|
|
@@ -2499,10 +2958,18 @@ function ConfigTab(props) {
|
|
|
2499
2958
|
advanced,
|
|
2500
2959
|
notificationRows,
|
|
2501
2960
|
h('div', { className: 'capital-automation-formactions', 'data-sticky': 'true' },
|
|
2502
|
-
h('span', { className: 'capital-automation-formstatus' },
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2961
|
+
h('span', { className: 'capital-automation-formstatus' }, disabled ? zh.storageUnavailable : zh.autosaveHint),
|
|
2962
|
+
state === 'disabled'
|
|
2963
|
+
? null
|
|
2964
|
+
: h('span', {
|
|
2965
|
+
className: 'capital-automation-autosavestate',
|
|
2966
|
+
role: 'status',
|
|
2967
|
+
'data-tone': statusTone,
|
|
2968
|
+
title: state === 'error' ? saveError : undefined,
|
|
2969
|
+
}, statusText),
|
|
2970
|
+
state === 'error'
|
|
2971
|
+
? h('button', { type: 'button', className: 'capital-automation-btn', 'data-size': 'sm', disabled: busy || disabled, onClick: retry }, zh.autosaveRetry)
|
|
2972
|
+
: null))
|
|
2506
2973
|
}
|
|
2507
2974
|
|
|
2508
2975
|
function NotificationsTab(props) {
|
|
@@ -2553,19 +3020,72 @@ function DetailBar(props) {
|
|
|
2553
3020
|
function TaskDetailPage(props) {
|
|
2554
3021
|
const { detail, meta, api, onChanged, openSession, tab, onTab, onRequestConfirm, onRequestCancelRun, schedulerActive, storageReady } = props
|
|
2555
3022
|
const [draft, setDraft] = useState(null)
|
|
3023
|
+
const [savedTask, setSavedTask] = useState(null)
|
|
2556
3024
|
const [busy, setBusy] = useState('')
|
|
2557
3025
|
const [notice, setNotice] = useState('')
|
|
2558
3026
|
const [error, setError] = useState('')
|
|
3027
|
+
const [saveState, setSaveState] = useState('idle')
|
|
3028
|
+
const [saveError, setSaveError] = useState('')
|
|
2559
3029
|
const [menuOpen, setMenuOpen] = useState(false)
|
|
2560
3030
|
const lastTaskIdRef = useRef(null)
|
|
3031
|
+
const activeTaskIdRef = useRef(null)
|
|
3032
|
+
const draftRef = useRef(null)
|
|
3033
|
+
const savedTaskRef = useRef(null)
|
|
3034
|
+
const ackedPayloadRef = useRef(null)
|
|
3035
|
+
const failedPayloadRef = useRef(null)
|
|
3036
|
+
const saveTimerRef = useRef(null)
|
|
3037
|
+
const saveInFlightRef = useRef(false)
|
|
3038
|
+
const saveQueuedRef = useRef(false)
|
|
3039
|
+
const savePromiseRef = useRef(null)
|
|
3040
|
+
const flushRef = useRef(null)
|
|
3041
|
+
const storageReadyNow = storageReady !== false
|
|
3042
|
+
activeTaskIdRef.current = detail.task?.id ?? null
|
|
3043
|
+
|
|
3044
|
+
const updateDraft = (patch) => {
|
|
3045
|
+
const current = draftRef.current
|
|
3046
|
+
if (current === null) return
|
|
3047
|
+
const next = {
|
|
3048
|
+
...current,
|
|
3049
|
+
...(typeof patch === 'function' ? patch(current) : patch),
|
|
3050
|
+
}
|
|
3051
|
+
draftRef.current = next
|
|
3052
|
+
setDraft(next)
|
|
3053
|
+
}
|
|
2561
3054
|
|
|
2562
3055
|
useEffect(() => {
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
3056
|
+
const server = detail.task
|
|
3057
|
+
if (!server) {
|
|
3058
|
+
savedTaskRef.current = null
|
|
3059
|
+
ackedPayloadRef.current = null
|
|
3060
|
+
setSavedTask(null)
|
|
3061
|
+
setDraft(null)
|
|
3062
|
+
draftRef.current = null
|
|
3063
|
+
return
|
|
3064
|
+
}
|
|
3065
|
+
const nextSaved = cloneTask(server)
|
|
3066
|
+
const previousAcked = ackedPayloadRef.current
|
|
3067
|
+
savedTaskRef.current = nextSaved
|
|
3068
|
+
ackedPayloadRef.current = payloadKey(nextSaved)
|
|
3069
|
+
setSavedTask(nextSaved)
|
|
3070
|
+
const current = draftRef.current
|
|
3071
|
+
if (current === null || current.id !== nextSaved.id) {
|
|
3072
|
+
draftRef.current = nextSaved
|
|
3073
|
+
setDraft(nextSaved)
|
|
3074
|
+
return
|
|
3075
|
+
}
|
|
3076
|
+
if (samePayload(current, nextSaved)) return
|
|
3077
|
+
// Hibernate clears overrides on the server; discard a local draft that a failed save
|
|
3078
|
+
// could otherwise resurrect after the task returns to its template defaults.
|
|
3079
|
+
if (nextSaved.state === 'dormant' && current.state !== 'dormant') {
|
|
3080
|
+
draftRef.current = nextSaved
|
|
3081
|
+
setDraft(nextSaved)
|
|
3082
|
+
return
|
|
3083
|
+
}
|
|
3084
|
+
// A poll or another action must not clobber edits the user has not saved yet.
|
|
3085
|
+
if (previousAcked !== null && payloadKey(current) === previousAcked) {
|
|
3086
|
+
draftRef.current = nextSaved
|
|
3087
|
+
setDraft(nextSaved)
|
|
3088
|
+
}
|
|
2569
3089
|
}, [detail])
|
|
2570
3090
|
|
|
2571
3091
|
useEffect(() => {
|
|
@@ -2574,20 +3094,57 @@ function TaskDetailPage(props) {
|
|
|
2574
3094
|
lastTaskIdRef.current = id
|
|
2575
3095
|
setNotice('')
|
|
2576
3096
|
setError('')
|
|
3097
|
+
setSaveState('idle')
|
|
3098
|
+
setSaveError('')
|
|
3099
|
+
failedPayloadRef.current = null
|
|
2577
3100
|
}, [detail])
|
|
2578
3101
|
|
|
3102
|
+
useEffect(() => {
|
|
3103
|
+
if (draft === null || savedTask === null || storageReadyNow === false) return undefined
|
|
3104
|
+
if (samePayload(draft, savedTask)) {
|
|
3105
|
+
setSaveState((state) => (state === 'pending' || state === 'invalid' || state === 'error' ? 'idle' : state))
|
|
3106
|
+
return undefined
|
|
3107
|
+
}
|
|
3108
|
+
if (busy) return undefined
|
|
3109
|
+
if (Object.keys(draftIssues(draft, meta)).length > 0) {
|
|
3110
|
+
setSaveState('invalid')
|
|
3111
|
+
return undefined
|
|
3112
|
+
}
|
|
3113
|
+
if (failedPayloadRef.current !== null && payloadKey(draft) === failedPayloadRef.current) {
|
|
3114
|
+
setSaveState('error')
|
|
3115
|
+
return undefined
|
|
3116
|
+
}
|
|
3117
|
+
setSaveState((state) => (state === 'saving' ? state : 'pending'))
|
|
3118
|
+
saveTimerRef.current = setTimeout(() => { void flushRef.current?.() }, AUTOSAVE_DELAY_MS)
|
|
3119
|
+
return () => {
|
|
3120
|
+
if (saveTimerRef.current !== null) {
|
|
3121
|
+
clearTimeout(saveTimerRef.current)
|
|
3122
|
+
saveTimerRef.current = null
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
}, [draft, savedTask, busy, storageReadyNow, meta])
|
|
3126
|
+
|
|
3127
|
+
useEffect(() => {
|
|
3128
|
+
if (tab === 'config') return
|
|
3129
|
+
void flushRef.current?.()
|
|
3130
|
+
}, [tab])
|
|
3131
|
+
|
|
3132
|
+
useEffect(() => () => { void flushRef.current?.() }, [])
|
|
3133
|
+
|
|
3134
|
+
useEffect(() => {
|
|
3135
|
+
if (typeof document === 'undefined') return undefined
|
|
3136
|
+
const onHide = () => { if (document.visibilityState === 'hidden') void flushRef.current?.() }
|
|
3137
|
+
document.addEventListener?.('visibilitychange', onHide)
|
|
3138
|
+
return () => document.removeEventListener?.('visibilitychange', onHide)
|
|
3139
|
+
}, [])
|
|
3140
|
+
|
|
2579
3141
|
if (draft === null) return h('p', { className: 'capital-automation-empty' }, zh.loading)
|
|
2580
3142
|
const task = draft
|
|
2581
|
-
const storageReadyNow = storageReady !== false
|
|
2582
3143
|
const running = detail.runs.some((run) => isActiveRun(run))
|
|
2583
3144
|
const activeRun = detail.runs.find((run) => isActiveRun(run))
|
|
2584
3145
|
const unread = detail.notifications.filter((item) => item.readAt === undefined).length
|
|
2585
|
-
const dirty =
|
|
3146
|
+
const dirty = savedTask === null ? false : !samePayload(draft, savedTask)
|
|
2586
3147
|
const canRun = storageReadyNow && schedulerActive === true && !running
|
|
2587
|
-
const set = (patch) => setDraft((current) => ({
|
|
2588
|
-
...current,
|
|
2589
|
-
...(typeof patch === 'function' ? patch(current) : patch),
|
|
2590
|
-
}))
|
|
2591
3148
|
|
|
2592
3149
|
async function act(name, action, success) {
|
|
2593
3150
|
if (busy) return
|
|
@@ -2604,10 +3161,90 @@ function TaskDetailPage(props) {
|
|
|
2604
3161
|
}
|
|
2605
3162
|
}
|
|
2606
3163
|
|
|
2607
|
-
async function
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
3164
|
+
async function flush() {
|
|
3165
|
+
if (saveTimerRef.current !== null) {
|
|
3166
|
+
clearTimeout(saveTimerRef.current)
|
|
3167
|
+
saveTimerRef.current = null
|
|
3168
|
+
}
|
|
3169
|
+
if (saveInFlightRef.current) {
|
|
3170
|
+
saveQueuedRef.current = true
|
|
3171
|
+
return savePromiseRef.current ?? undefined
|
|
3172
|
+
}
|
|
3173
|
+
const current = draftRef.current
|
|
3174
|
+
const base = savedTaskRef.current
|
|
3175
|
+
if (current === null || base === null || storageReadyNow === false) return
|
|
3176
|
+
if (samePayload(current, base)) {
|
|
3177
|
+
setSaveState((state) => (state === 'saving' ? state : 'idle'))
|
|
3178
|
+
return
|
|
3179
|
+
}
|
|
3180
|
+
if (Object.keys(draftIssues(current, meta)).length > 0) {
|
|
3181
|
+
setSaveState('invalid')
|
|
3182
|
+
return
|
|
3183
|
+
}
|
|
3184
|
+
const payload = draftPayload(current)
|
|
3185
|
+
const taskId = current.id
|
|
3186
|
+
saveInFlightRef.current = true
|
|
3187
|
+
saveQueuedRef.current = false
|
|
3188
|
+
setSaveState('saving')
|
|
3189
|
+
setSaveError('')
|
|
3190
|
+
const savePromise = (async () => {
|
|
3191
|
+
try {
|
|
3192
|
+
const result = await api.patch(taskId, payload)
|
|
3193
|
+
if (activeTaskIdRef.current !== taskId) {
|
|
3194
|
+
void onChanged?.(taskId)
|
|
3195
|
+
return
|
|
3196
|
+
}
|
|
3197
|
+
const serverTask = result?.task
|
|
3198
|
+
const nextSaved = serverTask === undefined ? cloneTask({ ...base, ...payload }) : cloneTask(serverTask)
|
|
3199
|
+
savedTaskRef.current = nextSaved
|
|
3200
|
+
ackedPayloadRef.current = payloadKey(nextSaved)
|
|
3201
|
+
failedPayloadRef.current = null
|
|
3202
|
+
setSavedTask(nextSaved)
|
|
3203
|
+
setSaveState('saved')
|
|
3204
|
+
void onChanged?.(taskId)
|
|
3205
|
+
} catch (failure) {
|
|
3206
|
+
if (activeTaskIdRef.current === taskId) {
|
|
3207
|
+
failedPayloadRef.current = payloadKey(current)
|
|
3208
|
+
setSaveState('error')
|
|
3209
|
+
setSaveError(failure?.message ?? zh.errorGeneric)
|
|
3210
|
+
}
|
|
3211
|
+
} finally {
|
|
3212
|
+
saveInFlightRef.current = false
|
|
3213
|
+
const latest = draftRef.current
|
|
3214
|
+
const baseNow = savedTaskRef.current
|
|
3215
|
+
const queued = saveQueuedRef.current
|
|
3216
|
+
saveQueuedRef.current = false
|
|
3217
|
+
if (queued || (latest !== null && baseNow !== null && latest.id === baseNow.id && !samePayload(latest, baseNow) && failedPayloadRef.current === null)) {
|
|
3218
|
+
saveTimerRef.current = setTimeout(() => { void flushRef.current?.() }, 0)
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
})()
|
|
3222
|
+
savePromiseRef.current = savePromise
|
|
3223
|
+
try {
|
|
3224
|
+
await savePromise
|
|
3225
|
+
} finally {
|
|
3226
|
+
if (savePromiseRef.current === savePromise) savePromiseRef.current = null
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
flushRef.current = flush
|
|
3230
|
+
|
|
3231
|
+
function retrySave() {
|
|
3232
|
+
failedPayloadRef.current = null
|
|
3233
|
+
setSaveError('')
|
|
3234
|
+
void flushRef.current?.()
|
|
3235
|
+
}
|
|
3236
|
+
|
|
3237
|
+
async function flushBeforeAction() {
|
|
3238
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
3239
|
+
await flushRef.current?.()
|
|
3240
|
+
const current = draftRef.current
|
|
3241
|
+
const base = savedTaskRef.current
|
|
3242
|
+
if (current === null || base === null) return true
|
|
3243
|
+
if (samePayload(current, base)) return true
|
|
3244
|
+
if (Object.keys(draftIssues(current, meta)).length > 0) return false
|
|
3245
|
+
if (failedPayloadRef.current !== null) return false
|
|
3246
|
+
}
|
|
3247
|
+
return false
|
|
2611
3248
|
}
|
|
2612
3249
|
|
|
2613
3250
|
async function toggleState() {
|
|
@@ -2618,6 +3255,11 @@ function TaskDetailPage(props) {
|
|
|
2618
3255
|
}
|
|
2619
3256
|
|
|
2620
3257
|
async function runNow() {
|
|
3258
|
+
if (!(await flushBeforeAction())) {
|
|
3259
|
+
const current = draftRef.current
|
|
3260
|
+
setError(current !== null && Object.keys(draftIssues(current, meta)).length > 0 ? zh.autosaveInvalid : zh.autosaveFailed)
|
|
3261
|
+
return
|
|
3262
|
+
}
|
|
2621
3263
|
await act('run', async () => {
|
|
2622
3264
|
await api.run(detail.task.id)
|
|
2623
3265
|
}, zh.runSubmitted.replace('{title}', detail.task.title))
|
|
@@ -2632,6 +3274,11 @@ function TaskDetailPage(props) {
|
|
|
2632
3274
|
}
|
|
2633
3275
|
|
|
2634
3276
|
async function copyTask() {
|
|
3277
|
+
if (!(await flushBeforeAction())) {
|
|
3278
|
+
const current = draftRef.current
|
|
3279
|
+
setError(current !== null && Object.keys(draftIssues(current, meta)).length > 0 ? zh.autosaveInvalid : zh.autosaveFailed)
|
|
3280
|
+
return
|
|
3281
|
+
}
|
|
2635
3282
|
await act('copy', async () => {
|
|
2636
3283
|
await api.action(detail.task.id, 'copy')
|
|
2637
3284
|
}, zh.copied.replace('{title}', detail.task.title))
|
|
@@ -2654,17 +3301,6 @@ function TaskDetailPage(props) {
|
|
|
2654
3301
|
onClick: () => { void runNow() },
|
|
2655
3302
|
}, h(IconPlayOutlineRegular, { size: 14 }), zh.runNow)
|
|
2656
3303
|
|
|
2657
|
-
const resetDraft = () => {
|
|
2658
|
-
setDraft({
|
|
2659
|
-
...detail.task,
|
|
2660
|
-
trigger: normalizeTrigger(detail.task.trigger),
|
|
2661
|
-
limits: { ...detail.task.limits },
|
|
2662
|
-
notifications: { ...detail.task.notifications },
|
|
2663
|
-
})
|
|
2664
|
-
setError('')
|
|
2665
|
-
setNotice('')
|
|
2666
|
-
}
|
|
2667
|
-
|
|
2668
3304
|
return h('div', null,
|
|
2669
3305
|
h('div', { className: 'capital-automation-hero' },
|
|
2670
3306
|
h(Glyph, { task, large: true }),
|
|
@@ -2696,7 +3332,18 @@ function TaskDetailPage(props) {
|
|
|
2696
3332
|
else if (id === 'copy') void copyTask()
|
|
2697
3333
|
else if (id === 'activate' || id === 'pause') void toggleState()
|
|
2698
3334
|
},
|
|
2699
|
-
onRequestConfirm: (kind, target) => {
|
|
3335
|
+
onRequestConfirm: (kind, target) => {
|
|
3336
|
+
setMenuOpen(false)
|
|
3337
|
+
// A pending autosave must not land after hibernate clears the overrides.
|
|
3338
|
+
if (kind === 'hibernate' || kind === 'delete') {
|
|
3339
|
+
void (async () => {
|
|
3340
|
+
await flushBeforeAction()
|
|
3341
|
+
onRequestConfirm(kind, target)
|
|
3342
|
+
})()
|
|
3343
|
+
return
|
|
3344
|
+
}
|
|
3345
|
+
onRequestConfirm(kind, target)
|
|
3346
|
+
},
|
|
2700
3347
|
canRun,
|
|
2701
3348
|
running,
|
|
2702
3349
|
busy: busy.length > 0,
|
|
@@ -2728,12 +3375,14 @@ function TaskDetailPage(props) {
|
|
|
2728
3375
|
}) : null,
|
|
2729
3376
|
tab === 'config' ? h(ConfigTab, {
|
|
2730
3377
|
draft,
|
|
2731
|
-
setDraft,
|
|
3378
|
+
setDraft: updateDraft,
|
|
2732
3379
|
meta,
|
|
2733
3380
|
busy,
|
|
2734
3381
|
dirty,
|
|
2735
|
-
|
|
2736
|
-
|
|
3382
|
+
saveState,
|
|
3383
|
+
saveError,
|
|
3384
|
+
retry: retrySave,
|
|
3385
|
+
onFlush: () => { void flushRef.current?.() },
|
|
2737
3386
|
disabled: !storageReadyNow,
|
|
2738
3387
|
}) : null,
|
|
2739
3388
|
tab === 'runs' ? h('section', { className: 'capital-automation-card capital-automation-panel' },
|
|
@@ -2756,15 +3405,15 @@ function TaskDetailPage(props) {
|
|
|
2756
3405
|
|
|
2757
3406
|
function NewTaskModal(props) {
|
|
2758
3407
|
const { open, onClose, api, onCreated, meta } = props
|
|
2759
|
-
const [draft, setDraft] = useState(defaultDraft)
|
|
3408
|
+
const [draft, setDraft] = useState(() => defaultDraft(meta))
|
|
2760
3409
|
const [busy, setBusy] = useState(false)
|
|
2761
3410
|
const [error, setError] = useState('')
|
|
2762
|
-
const { issues, errorFor, touch, touchAll } = useDraftIssues(draft, open)
|
|
3411
|
+
const { issues, errorFor, touch, touchAll } = useDraftIssues(draft, open, meta)
|
|
2763
3412
|
const titleError = errorFor('title')
|
|
2764
3413
|
const instructionError = errorFor('instruction')
|
|
2765
3414
|
useEffect(() => {
|
|
2766
3415
|
if (!open) return
|
|
2767
|
-
setDraft(defaultDraft())
|
|
3416
|
+
setDraft(defaultDraft(meta))
|
|
2768
3417
|
setBusy(false)
|
|
2769
3418
|
setError('')
|
|
2770
3419
|
}, [open])
|
|
@@ -2774,8 +3423,8 @@ function NewTaskModal(props) {
|
|
|
2774
3423
|
}))
|
|
2775
3424
|
const timeoutRange = limitValue(meta, 'runTimeoutMinutes', [5, 120])
|
|
2776
3425
|
const idleRange = limitValue(meta, 'idleTimeoutMinutes', [1, 30])
|
|
2777
|
-
const stepsRange = limitValue(meta, 'maxSteps', [
|
|
2778
|
-
const toolsRange = limitValue(meta, 'maxToolCalls', [
|
|
3426
|
+
const stepsRange = limitValue(meta, 'maxSteps', [100, 2000])
|
|
3427
|
+
const toolsRange = limitValue(meta, 'maxToolCalls', [100, 5000])
|
|
2779
3428
|
async function create() {
|
|
2780
3429
|
if (Object.keys(issues).length > 0) {
|
|
2781
3430
|
touchAll()
|
|
@@ -2806,7 +3455,7 @@ function NewTaskModal(props) {
|
|
|
2806
3455
|
},
|
|
2807
3456
|
h('div', { className: 'capital-automation-wizard' },
|
|
2808
3457
|
h('div', { className: 'capital-automation-modalheadline', 'data-invalid': titleError ? 'true' : undefined },
|
|
2809
|
-
h(
|
|
3458
|
+
h(IconPickerTrigger, { task: draft, size: 42, onChange: (icon) => set({ icon }) }),
|
|
2810
3459
|
h('input', {
|
|
2811
3460
|
className: 'capital-automation-modaltitleinput',
|
|
2812
3461
|
value: draft.title,
|
|
@@ -2818,7 +3467,6 @@ function NewTaskModal(props) {
|
|
|
2818
3467
|
onBlur: () => touch('title'),
|
|
2819
3468
|
})),
|
|
2820
3469
|
titleError ? h('span', { className: 'capital-automation-fielderror', role: 'alert' }, titleError) : null,
|
|
2821
|
-
h('p', { className: 'capital-automation-hint' }, zh.newTaskHint),
|
|
2822
3470
|
h(Field, {
|
|
2823
3471
|
label: zh.descriptionField,
|
|
2824
3472
|
note: zh.charCount.replace('{n}', String(Array.from(draft.description ?? '').length)).replace('{max}', String(DESCRIPTION_MAX_LENGTH)),
|
|
@@ -2839,6 +3487,7 @@ function NewTaskModal(props) {
|
|
|
2839
3487
|
h(PlainTextEditor, {
|
|
2840
3488
|
className: 'capital-automation-texteditor',
|
|
2841
3489
|
value: draft.instruction,
|
|
3490
|
+
maxLength: INSTRUCTION_MAX_LENGTH,
|
|
2842
3491
|
placeholder: zh.instructionPlaceholder,
|
|
2843
3492
|
ariaLabel: zh.instructionField,
|
|
2844
3493
|
invalid: instructionError ? true : undefined,
|
|
@@ -2885,8 +3534,8 @@ function NewTaskModal(props) {
|
|
|
2885
3534
|
h('span', { className: 'capital-automation-formlabel' }, zh.notifyField),
|
|
2886
3535
|
h('div', { className: 'capital-automation-switchlist' },
|
|
2887
3536
|
h(SwitchRow, { label: zh.notifyInApp, checked: draft.notifications.inApp, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, inApp: value } })) }),
|
|
2888
|
-
h(SwitchRow, { label: zh.notifyFailure, checked: draft.notifications.onFailure, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, onFailure: value } })) }),
|
|
2889
|
-
h(SwitchRow, { label: zh.notifySuccess, checked: draft.notifications.onSuccess, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, onSuccess: value } })) })),
|
|
3537
|
+
h(SwitchRow, { label: zh.notifyFailure, checked: draft.notifications.onFailure, disabled: draft.notifications.inApp !== true, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, onFailure: value } })) }),
|
|
3538
|
+
h(SwitchRow, { label: zh.notifySuccess, checked: draft.notifications.onSuccess, disabled: draft.notifications.inApp !== true, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, onSuccess: value } })) })),
|
|
2890
3539
|
h('p', { className: 'capital-automation-hint' }, zh.notifyHint)),
|
|
2891
3540
|
error.length > 0 ? h('p', { className: 'capital-automation-error', role: 'alert' }, error) : null))
|
|
2892
3541
|
}
|
|
@@ -2951,11 +3600,21 @@ function InboxModal(props) {
|
|
|
2951
3600
|
}))))
|
|
2952
3601
|
}
|
|
2953
3602
|
|
|
3603
|
+
function browserTimeZone() {
|
|
3604
|
+
try {
|
|
3605
|
+
const zone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
3606
|
+
return typeof zone === 'string' && zone.length > 0 ? zone : 'Asia/Shanghai'
|
|
3607
|
+
} catch {
|
|
3608
|
+
return 'Asia/Shanghai'
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3611
|
+
|
|
2954
3612
|
function AutomationPage(props) {
|
|
2955
3613
|
const { api, openSession } = props
|
|
2956
3614
|
const [meta, setMeta] = useState(null)
|
|
2957
3615
|
const [tasks, setTasks] = useState([])
|
|
2958
3616
|
const [runs, setRuns] = useState([])
|
|
3617
|
+
const [history, setHistory] = useState(null)
|
|
2959
3618
|
const [notifications, setNotifications] = useState([])
|
|
2960
3619
|
const [unread, setUnread] = useState(0)
|
|
2961
3620
|
const [selectedId, setSelectedId] = useState(null)
|
|
@@ -2991,10 +3650,11 @@ function AutomationPage(props) {
|
|
|
2991
3650
|
refreshSequenceRef.current = requestSequence
|
|
2992
3651
|
const request = (async () => {
|
|
2993
3652
|
try {
|
|
2994
|
-
const [nextMeta, nextTasks, nextRuns, nextNotifications] = await Promise.all([
|
|
3653
|
+
const [nextMeta, nextTasks, nextRuns, nextHistory, nextNotifications] = await Promise.all([
|
|
2995
3654
|
api.meta(),
|
|
2996
3655
|
api.tasks(),
|
|
2997
3656
|
api.runs('?limit=200'),
|
|
3657
|
+
api.history(`?days=${HISTORY_DAYS}&tz=${encodeURIComponent(browserTimeZone())}`),
|
|
2998
3658
|
api.notifications(),
|
|
2999
3659
|
])
|
|
3000
3660
|
if (!mountedRef.current || requestSequence !== refreshSequenceRef.current) return
|
|
@@ -3003,6 +3663,7 @@ function AutomationPage(props) {
|
|
|
3003
3663
|
tasksRef.current = nextTaskList
|
|
3004
3664
|
setTasks(nextTaskList)
|
|
3005
3665
|
setRuns(Array.isArray(nextRuns.runs) ? nextRuns.runs : [])
|
|
3666
|
+
setHistory(nextHistory ?? null)
|
|
3006
3667
|
setNotifications(Array.isArray(nextNotifications.notifications) ? nextNotifications.notifications : [])
|
|
3007
3668
|
setUnread(typeof nextNotifications.unread === 'number' ? nextNotifications.unread : 0)
|
|
3008
3669
|
setLoadError('')
|
|
@@ -3201,7 +3862,9 @@ function AutomationPage(props) {
|
|
|
3201
3862
|
|
|
3202
3863
|
const schedulerActive = meta?.scheduler?.state === 'active'
|
|
3203
3864
|
const storageReady = meta?.storage?.state !== 'unavailable'
|
|
3204
|
-
const templates = tasks
|
|
3865
|
+
const templates = tasks
|
|
3866
|
+
.filter((task) => task.origin === 'builtin' && task.state === 'dormant')
|
|
3867
|
+
.sort(compareTemplates)
|
|
3205
3868
|
const selectedTask = tasks.find((task) => task.id === selectedId)
|
|
3206
3869
|
const detailOpen = tab === 'tasks' && selectedId !== null
|
|
3207
3870
|
// `/meta` arrives in the same batch as `/tasks`; until then the empty state would be a lie.
|
|
@@ -3253,7 +3916,6 @@ function AutomationPage(props) {
|
|
|
3253
3916
|
onAction: (task, action) => { void performTaskAction(task, action) },
|
|
3254
3917
|
onRequestConfirm: requestConfirm,
|
|
3255
3918
|
onEdit: (id) => selectTask(id, 'config'),
|
|
3256
|
-
onGoRuns: () => switchTab('runs'),
|
|
3257
3919
|
onRequestCancelRun: (task, run) => requestConfirm('cancel-run', task, run),
|
|
3258
3920
|
schedulerActive,
|
|
3259
3921
|
storageReady,
|
|
@@ -3263,6 +3925,7 @@ function AutomationPage(props) {
|
|
|
3263
3925
|
? h(RunsSkeleton)
|
|
3264
3926
|
: h(RunsPage, {
|
|
3265
3927
|
runs,
|
|
3928
|
+
history,
|
|
3266
3929
|
tasks,
|
|
3267
3930
|
onCancel: (run) => requestConfirm('cancel-run', { id: run.taskId, title: run.taskTitle }, run),
|
|
3268
3931
|
onOpenSession: openSession,
|
|
@@ -3270,7 +3933,7 @@ function AutomationPage(props) {
|
|
|
3270
3933
|
onGoTasks: () => switchTab('tasks'),
|
|
3271
3934
|
onNewTask: () => setWizard(true),
|
|
3272
3935
|
busy: confirmBusy,
|
|
3273
|
-
}))),
|
|
3936
|
+
}), detailOpen ? null : h(SystemStatus, { meta }))),
|
|
3274
3937
|
h(NewTaskModal, {
|
|
3275
3938
|
open: wizard,
|
|
3276
3939
|
onClose: () => setWizard(false),
|