@v587d/capital-generation 2.14.1 → 2.15.1
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 +137 -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 +37 -5
- package/automation/client.src.cjs +578 -126
- package/automation/defaults.js +4 -4
- package/automation/index.js +1 -0
- package/automation/package.json +4 -2
- package/automation/route.js +1 -1
- package/automation/runner.js +5 -0
- package/automation/scheduler.js +34 -29
- package/automation/schema.js +39 -5
- package/automation/service.js +13 -10
- package/automation/store.js +64 -21
- package/automation/task-icons.js +75 -0
- package/chart-ui/client.js +2 -2
- package/chart-ui/package.json +1 -1
- package/config/client.js +12 -5
- package/config/client.src.cjs +143 -45
- package/config/index.js +5 -5
- package/config/package.json +1 -1
- 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/index.js +5 -5
- 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 +4 -2
- package/lib/web-retriever/sources.js +73 -17
- package/lib/web-retriever/wind-client.d.ts +1 -1
- package/lib/web-retriever/wind-client.js +1 -1
- package/package.json +6 -6
- package/preset/capital-generation/README.md +1 -1
- package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +6 -3
- package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +2 -0
- 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: '二' },
|
|
@@ -74,6 +76,8 @@ const HISTORY_DAYS = 30
|
|
|
74
76
|
const DETAIL_RECENT_RUN_LIMIT = 3
|
|
75
77
|
const TITLE_MAX_LENGTH = 12
|
|
76
78
|
const DESCRIPTION_MAX_LENGTH = 100
|
|
79
|
+
const INSTRUCTION_MAX_LENGTH = 12_000
|
|
80
|
+
const AUTOSAVE_DELAY_MS = 600
|
|
77
81
|
|
|
78
82
|
const zh = {
|
|
79
83
|
panel: 'CapGen 投研自动化',
|
|
@@ -81,10 +85,10 @@ const zh = {
|
|
|
81
85
|
subtitle: '定时投研,自动执行,结果可追溯',
|
|
82
86
|
tabs: { tasks: '任务', runs: '运行' },
|
|
83
87
|
storageUnavailable: '存储不可用,页面只读',
|
|
84
|
-
standbyBanner: '
|
|
88
|
+
standbyBanner: '本窗口的定时任务已暂停:定时触发与手动运行不可用;若另一个 CapGen 窗口在运行,定时任务仍会由它执行。',
|
|
85
89
|
storageBanner: '投研自动化存储暂时不可用:页面只读,调度器不会启动。',
|
|
86
90
|
calendarBanner: '交易日历暂时不可用:仅交易日的任务会跳过,其他任务仍按设定时间运行。',
|
|
87
|
-
calendarCoverageBanner: '交易日历只覆盖到 {through}
|
|
91
|
+
calendarCoverageBanner: '交易日历只覆盖到 {through}:之后仅交易日的任务会跳过;交易所公布新一年安排后会自动接上。',
|
|
88
92
|
calendarField: '交易日历',
|
|
89
93
|
calendarSourceSzse: '深交所官方日历',
|
|
90
94
|
calendarSourceBundled: '内置表',
|
|
@@ -102,7 +106,6 @@ const zh = {
|
|
|
102
106
|
clearFilter: '清除筛选',
|
|
103
107
|
newTask: '新建任务',
|
|
104
108
|
newTaskTitle: '新建投研任务',
|
|
105
|
-
newTaskHint: '任务会在后台新建一个 session 执行,不需要你守在页面上。',
|
|
106
109
|
editTask: '编辑任务',
|
|
107
110
|
allTasks: '全部任务',
|
|
108
111
|
backToTasks: '所有任务',
|
|
@@ -123,7 +126,7 @@ const zh = {
|
|
|
123
126
|
filterReview: '复盘',
|
|
124
127
|
filterResearch: '研究',
|
|
125
128
|
filterLabel: '筛选任务',
|
|
126
|
-
category: { news: '资讯', market: '行情', review: '复盘', research: '研究' },
|
|
129
|
+
category: { news: '资讯', market: '行情', review: '复盘', research: '研究', portfolio: '组合', monitor: '监控' },
|
|
127
130
|
categoryField: '任务分类',
|
|
128
131
|
categoryFixed: `'复制为我的任务'后,可调整任务分类。`,
|
|
129
132
|
cadence: { daily: '每天', weekly: '每周', monthly: '每月', once: '单次' },
|
|
@@ -146,17 +149,21 @@ const zh = {
|
|
|
146
149
|
remove: '删除任务',
|
|
147
150
|
edit: '编辑',
|
|
148
151
|
more: '更多操作',
|
|
149
|
-
save: '保存',
|
|
150
152
|
saving: '保存中…',
|
|
151
153
|
processing: '处理中…',
|
|
152
|
-
saved: '已保存',
|
|
153
154
|
cancel: '取消',
|
|
154
155
|
create: '创建任务',
|
|
155
156
|
creating: '创建中…',
|
|
156
157
|
titleRequired: '标题不能为空。',
|
|
157
158
|
instructionRequired: '任务指令不能为空。',
|
|
158
159
|
invalidLimit: '请输入有效数字。',
|
|
159
|
-
|
|
160
|
+
invalidLimitRange: '请输入 {min}–{max} 的整数。',
|
|
161
|
+
autosaveHint: '修改会自动保存',
|
|
162
|
+
autosavePending: '等待保存…',
|
|
163
|
+
autosaveSaved: '已自动保存',
|
|
164
|
+
autosaveFailed: '保存失败',
|
|
165
|
+
autosaveRetry: '重试',
|
|
166
|
+
autosaveInvalid: '填写完整后自动保存',
|
|
160
167
|
noDirty: '配置已同步',
|
|
161
168
|
nextRun: '下次运行',
|
|
162
169
|
nextRunDormant: '添加后开始调度',
|
|
@@ -173,6 +180,9 @@ const zh = {
|
|
|
173
180
|
notifications: '通知',
|
|
174
181
|
descriptionField: '任务说明',
|
|
175
182
|
titleField: '任务标题',
|
|
183
|
+
iconField: '任务图标',
|
|
184
|
+
iconPickHint: '点击图标更换',
|
|
185
|
+
iconBuiltinHint: '内置模板的图标固定;复制为我的任务后可更换。',
|
|
176
186
|
instructionField: '任务指令',
|
|
177
187
|
instructionPlaceholder: '例如:读取今日自选股行情、公告与相关新闻,按「结论—证据—风险」整理成一份可复核的盘后复盘。',
|
|
178
188
|
titlePlaceholder: '给这个任务起个名字',
|
|
@@ -225,10 +235,10 @@ const zh = {
|
|
|
225
235
|
maxSteps: '最大步骤',
|
|
226
236
|
maxToolCalls: '最大工具调用',
|
|
227
237
|
notifyField: '通知',
|
|
228
|
-
notifyInApp: '
|
|
229
|
-
notifyFailure: '
|
|
238
|
+
notifyInApp: '写入任务通知',
|
|
239
|
+
notifyFailure: '失败、超时或需人工处理时',
|
|
230
240
|
notifySuccess: '成功时通知',
|
|
231
|
-
notifyHint: '
|
|
241
|
+
notifyHint: '只写入当前 profile 的任务通知中心;不会发送邮件或外部消息。',
|
|
232
242
|
presetLabel: '运行模式',
|
|
233
243
|
presetValue: '自动模式',
|
|
234
244
|
presetHint: '所有运行固定使用自动模式。',
|
|
@@ -270,15 +280,32 @@ const zh = {
|
|
|
270
280
|
triggerScheduled: '定时',
|
|
271
281
|
openSession: '打开 session',
|
|
272
282
|
runSummaryFallback: '运行结束但没有摘要。',
|
|
273
|
-
notificationsEmpty: '
|
|
283
|
+
notificationsEmpty: '没有任务通知。',
|
|
274
284
|
markRead: '全部标为已读',
|
|
275
285
|
markTaskRead: '本任务标为已读',
|
|
276
|
-
notificationsTitle: '
|
|
277
|
-
|
|
286
|
+
notificationsTitle: '任务通知',
|
|
287
|
+
notificationsEntry: '任务通知',
|
|
288
|
+
notificationsHint: '运行失败、超时、成功与交易日历问题会出现在这里。',
|
|
278
289
|
notificationsUnread: '{n} 条未读',
|
|
279
290
|
allRead: '全部已读',
|
|
280
|
-
noNotifications: '
|
|
291
|
+
noNotifications: '没有未读或历史任务通知。',
|
|
281
292
|
notificationOpenTask: '查看任务',
|
|
293
|
+
systemStatus: '系统状态',
|
|
294
|
+
systemActive: '定时调度正常',
|
|
295
|
+
systemStandby: '本窗口待命,不启动新的定时运行',
|
|
296
|
+
systemStarting: '定时调度启动中',
|
|
297
|
+
systemStopped: '定时调度未运行',
|
|
298
|
+
systemUnavailable: '定时调度不可用',
|
|
299
|
+
systemCalendarOk: '交易日历正常',
|
|
300
|
+
systemCalendarUnavailable: '交易日历不可用',
|
|
301
|
+
systemDetails: '详情',
|
|
302
|
+
systemHideDetails: '收起',
|
|
303
|
+
systemLastStandby: '上次中断',
|
|
304
|
+
systemNoStandby: '没有调度中断记录。',
|
|
305
|
+
systemReasonLeaseHeld: '另一个 CapGen 窗口正在负责定时任务。',
|
|
306
|
+
systemReasonLeaseLost: '本窗口的调度权已失效,可能已由另一个窗口接管。',
|
|
307
|
+
systemReasonUnverified: '本窗口的调度权校验未通过。',
|
|
308
|
+
systemTechnical: '技术信息',
|
|
282
309
|
close: '关闭',
|
|
283
310
|
confirmDeleteTitle: '删除这个任务?',
|
|
284
311
|
confirmDeleteBody: '任务配置、运行记录与相关通知会一起删除,无法恢复。',
|
|
@@ -299,7 +326,7 @@ const zh = {
|
|
|
299
326
|
markReadDone: '已标为已读',
|
|
300
327
|
errorGeneric: '操作失败,请稍后重试。',
|
|
301
328
|
needConfirm: '需要用户确认',
|
|
302
|
-
schedulerBlocked: '
|
|
329
|
+
schedulerBlocked: '本窗口的定时任务已暂停,暂时不能手动运行。',
|
|
303
330
|
runBlocked: '任务正在运行,结束或取消后才能再次运行。',
|
|
304
331
|
configWhileRunning: '任务正在运行;修改会在下一次运行时生效。',
|
|
305
332
|
detailRecentRuns: '最近 {n} 次运行',
|
|
@@ -312,8 +339,8 @@ const zh = {
|
|
|
312
339
|
notificationFallbackFailed: '运行失败;打开对应任务或 session 查看详情。',
|
|
313
340
|
notificationFallbackTimeout: '运行超时;打开对应任务或 session 查看详情。',
|
|
314
341
|
notificationFallbackSuccess: '运行完成。',
|
|
315
|
-
notificationFallbackStandby: '
|
|
316
|
-
notificationFallbackCalendar: '
|
|
342
|
+
notificationFallbackStandby: '定时触发已暂停。',
|
|
343
|
+
notificationFallbackCalendar: '暂时无法确认交易日,相关任务已跳过。',
|
|
317
344
|
notificationFallbackSkipped: '本次计划已跳过。',
|
|
318
345
|
statusLabel: '状态',
|
|
319
346
|
triggerSummary: '日程',
|
|
@@ -344,7 +371,7 @@ function installStyles() {
|
|
|
344
371
|
style.setAttribute(`data-${STYLE_TAG}`, 'true')
|
|
345
372
|
style.textContent = `
|
|
346
373
|
.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; }
|
|
374
|
+
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; }
|
|
348
375
|
.capital-automation-page { display: flex; flex-direction: column; min-height: 0; height: 100%; color: var(--dsw-alias-label-primary); }
|
|
349
376
|
.capital-automation-head { flex: none; padding: 18px 0 0; }
|
|
350
377
|
.capital-automation-headinner { box-sizing: border-box; width: min(1080px, 100%); margin: 0 auto; padding: 0 clamp(20px, 3vw, 36px); }
|
|
@@ -444,6 +471,8 @@ function installStyles() {
|
|
|
444
471
|
.capital-automation-glyph[data-tone="market"] { background: linear-gradient(135deg, #ffbd4a, #f97316); }
|
|
445
472
|
.capital-automation-glyph[data-tone="review"] { background: linear-gradient(135deg, #b18cff, #7c3aed); }
|
|
446
473
|
.capital-automation-glyph[data-tone="research"] { background: linear-gradient(135deg, #39d7a8, #0d9488); }
|
|
474
|
+
.capital-automation-glyph[data-tone="portfolio"] { background: linear-gradient(135deg, #84cc16, #4d7c0f); }
|
|
475
|
+
.capital-automation-glyph[data-tone="monitor"] { background: linear-gradient(135deg, #06b6d4, #0e7490); }
|
|
447
476
|
.capital-automation-glyph[data-tone="custom"] { background: linear-gradient(135deg, #f472b6, #db2777); }
|
|
448
477
|
.capital-automation-identitytext { min-width: 0; }
|
|
449
478
|
.capital-automation-tasktitlerow { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 6px; min-width: 0; }
|
|
@@ -456,12 +485,16 @@ function installStyles() {
|
|
|
456
485
|
.capital-automation-tag[data-tone="market"] { color: #ffb45c; }
|
|
457
486
|
.capital-automation-tag[data-tone="review"] { color: #b79aff; }
|
|
458
487
|
.capital-automation-tag[data-tone="research"] { color: #56d9b4; }
|
|
488
|
+
.capital-automation-tag[data-tone="portfolio"] { color: #a3e635; }
|
|
489
|
+
.capital-automation-tag[data-tone="monitor"] { color: #22d3ee; }
|
|
459
490
|
.capital-automation-tag[data-tone="custom"] { color: #f79bc3; }
|
|
460
491
|
body:not([data-ds-dark-theme]) .capital-automation-tag { background: color-mix(in srgb, currentColor 9%, var(--ca-card)); }
|
|
461
492
|
body:not([data-ds-dark-theme]) .capital-automation-tag[data-tone="news"] { color: var(--ca-tag-news); }
|
|
462
493
|
body:not([data-ds-dark-theme]) .capital-automation-tag[data-tone="market"] { color: var(--ca-tag-market); }
|
|
463
494
|
body:not([data-ds-dark-theme]) .capital-automation-tag[data-tone="review"] { color: var(--ca-tag-review); }
|
|
464
495
|
body:not([data-ds-dark-theme]) .capital-automation-tag[data-tone="research"] { color: var(--ca-tag-research); }
|
|
496
|
+
body:not([data-ds-dark-theme]) .capital-automation-tag[data-tone="portfolio"] { color: var(--ca-tag-portfolio); }
|
|
497
|
+
body:not([data-ds-dark-theme]) .capital-automation-tag[data-tone="monitor"] { color: var(--ca-tag-monitor); }
|
|
465
498
|
body:not([data-ds-dark-theme]) .capital-automation-tag[data-tone="custom"] { color: var(--ca-tag-custom); }
|
|
466
499
|
.capital-automation-schedule { display: block; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }
|
|
467
500
|
.capital-automation-schedulesub { display: block; margin-top: 1px; color: var(--ca-caption); font-size: 11px; line-height: 16px; }
|
|
@@ -558,6 +591,22 @@ function installStyles() {
|
|
|
558
591
|
.capital-automation-fieldhead { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; min-width: 0; }
|
|
559
592
|
.capital-automation-fieldlabel { color: var(--ca-muted); font-size: 11.5px; line-height: 17px; }
|
|
560
593
|
.capital-automation-fieldhint { color: var(--ca-caption); font-size: 11px; line-height: 16px; font-variant-numeric: tabular-nums; }
|
|
594
|
+
.capital-automation-glyphbtn { display: inline-flex; flex: none; align-items: center; gap: 4px; padding: 0; border: 0; border-radius: 14px; background: transparent; cursor: pointer; }
|
|
595
|
+
.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); }
|
|
596
|
+
.capital-automation-glyphbtn:focus-visible { outline: 2px solid var(--dsw-focus-ring-color, var(--ca-business)); outline-offset: 2px; }
|
|
597
|
+
.capital-automation-glyphbtn:disabled { cursor: not-allowed; opacity: .55; }
|
|
598
|
+
.capital-automation-glyphbtnchevron { display: grid; flex: none; place-items: center; width: 12px; color: var(--ca-muted); }
|
|
599
|
+
.capital-automation-glyphbtn:hover:not(:disabled) .capital-automation-glyphbtnchevron { color: var(--ca-business-text); }
|
|
600
|
+
.capital-automation-iconrow { display: flex; align-items: center; gap: 12px; min-width: 0; }
|
|
601
|
+
.capital-automation-iconrowbody { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
602
|
+
/* The menu is portaled to body, outside the page's --ca-* scope; use host tokens. */
|
|
603
|
+
.capital-automation-iconmenu { box-sizing: border-box; width: 400px; max-width: calc(100vw - 24px); padding: 10px; }
|
|
604
|
+
.capital-automation-iconmenu-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(32px, 1fr)); gap: 6px; }
|
|
605
|
+
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; }
|
|
606
|
+
button.capital-automation-iconmenu-item:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
|
|
607
|
+
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); }
|
|
608
|
+
button.capital-automation-iconmenu-item:focus-visible { outline: 2px solid var(--dsw-focus-ring-color, var(--dsw-alias-brand-primary)); outline-offset: 2px; }
|
|
609
|
+
button.capital-automation-iconmenu-item:disabled { opacity: .4; cursor: not-allowed; }
|
|
561
610
|
.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
611
|
.capital-automation-field input { height: 34px; }
|
|
563
612
|
.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 +635,9 @@ function installStyles() {
|
|
|
586
635
|
.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
636
|
.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
637
|
.capital-automation-formstatus { margin-right: auto; color: var(--ca-caption); font-size: 11.5px; line-height: 18px; }
|
|
638
|
+
.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; }
|
|
639
|
+
.capital-automation-autosavestate[data-tone="ok"] { color: var(--ca-success-text); }
|
|
640
|
+
.capital-automation-autosavestate[data-tone="error"] { color: var(--ca-error-text); }
|
|
589
641
|
.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
642
|
.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
643
|
.capital-automation-triggerhead { display: flex; flex-wrap: nowrap; align-items: center; justify-content: space-between; gap: 12px; min-width: 0; }
|
|
@@ -668,6 +720,13 @@ function installStyles() {
|
|
|
668
720
|
.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
721
|
.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
722
|
.capital-automation-notificationtime { display: block; margin-top: 3px; color: var(--ca-caption); font-size: 11px; line-height: 16px; }
|
|
723
|
+
.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; }
|
|
724
|
+
.capital-automation-systemline { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
|
|
725
|
+
.capital-automation-systemtoggle { border: 0; padding: 0; background: transparent; color: var(--ca-muted); font: inherit; cursor: pointer; }
|
|
726
|
+
.capital-automation-systemtoggle:hover { color: var(--dsw-alias-label-primary); text-decoration: underline; }
|
|
727
|
+
.capital-automation-systemdetail { margin-top: 6px; color: var(--ca-muted); }
|
|
728
|
+
.capital-automation-systemdetail p { margin: 2px 0; }
|
|
729
|
+
.capital-automation-systemdetail summary { color: var(--ca-caption); cursor: pointer; }
|
|
671
730
|
.capital-automation-modal { width: min(720px, calc(100vw - 32px)); max-width: min(720px, calc(100vw - 32px)); max-height: calc(100dvh - 40px); }
|
|
672
731
|
.capital-automation-modal-wizard { width: min(600px, calc(100vw - 32px)); max-width: min(600px, calc(100vw - 32px)); }
|
|
673
732
|
.capital-automation-modal-content { max-height: calc(100dvh - 128px); overflow: auto; }
|
|
@@ -755,6 +814,7 @@ function createApi() {
|
|
|
755
814
|
credentials: 'same-origin',
|
|
756
815
|
headers: options.body === undefined ? {} : { 'content-type': 'application/json' },
|
|
757
816
|
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
817
|
+
...(options.keepalive === true ? { keepalive: true } : {}),
|
|
758
818
|
...(controller === undefined ? {} : { signal: controller.signal }),
|
|
759
819
|
})
|
|
760
820
|
try {
|
|
@@ -777,7 +837,7 @@ function createApi() {
|
|
|
777
837
|
tasks: () => request('/tasks'),
|
|
778
838
|
detail: (id) => request(`/tasks/${encodeURIComponent(id)}`),
|
|
779
839
|
create: (body) => request('/tasks', { method: 'POST', body }),
|
|
780
|
-
patch: (id, body) => request(`/tasks/${encodeURIComponent(id)}`, { method: 'PATCH', body }),
|
|
840
|
+
patch: (id, body) => request(`/tasks/${encodeURIComponent(id)}`, { method: 'PATCH', body, keepalive: true }),
|
|
781
841
|
remove: (id) => request(`/tasks/${encodeURIComponent(id)}`, { method: 'DELETE' }),
|
|
782
842
|
action: (id, action) => request(`/tasks/${encodeURIComponent(id)}/${action}`, { method: 'POST' }),
|
|
783
843
|
run: (id) => request(`/tasks/${encodeURIComponent(id)}/run`, { method: 'POST' }),
|
|
@@ -841,24 +901,124 @@ function toneOf(task) {
|
|
|
841
901
|
return task?.origin === 'custom' ? 'custom' : (task?.category ?? 'news')
|
|
842
902
|
}
|
|
843
903
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
904
|
+
// Twelve templates cannot stay in record-id order: read them daily → weekly → monthly, then by
|
|
905
|
+
// day and wall-clock time, so the card grid matches how the user plans the week.
|
|
906
|
+
const CADENCE_RANK = { daily: 0, weekly: 1, monthly: 2, once: 3 }
|
|
907
|
+
function firstWeekday(trigger) {
|
|
908
|
+
const days = Array.isArray(trigger?.weekdays) ? trigger.weekdays : []
|
|
909
|
+
return days.length === 0 ? 0 : Math.min(...days)
|
|
910
|
+
}
|
|
911
|
+
function compareTemplates(left, right) {
|
|
912
|
+
const rank = (CADENCE_RANK[left.trigger?.cadence] ?? 9) - (CADENCE_RANK[right.trigger?.cadence] ?? 9)
|
|
913
|
+
if (rank !== 0) return rank
|
|
914
|
+
const day = firstWeekday(left.trigger) - firstWeekday(right.trigger)
|
|
915
|
+
if (day !== 0) return day
|
|
916
|
+
return String(left.trigger?.time ?? '').localeCompare(String(right.trigger?.time ?? ''))
|
|
917
|
+
|| String(left.title).localeCompare(String(right.title))
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function iconDefinition(id) {
|
|
921
|
+
return typeof id === 'string' && Object.prototype.hasOwnProperty.call(TASK_ICON_CATALOG, id)
|
|
922
|
+
? TASK_ICON_CATALOG[id]
|
|
923
|
+
: undefined
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function taskIconId(task) {
|
|
927
|
+
if (iconDefinition(task?.icon) !== undefined) return task.icon
|
|
928
|
+
if (task?.origin === 'custom') return DEFAULT_CUSTOM_ICON
|
|
929
|
+
return builtinTaskIcon(task?.sourceBuiltinId)
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function taskIcon(task) {
|
|
933
|
+
return iconDefinition(taskIconId(task)) ?? TASK_ICON_CATALOG[DEFAULT_CUSTOM_ICON]
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Tabler glyphs are a 24 grid with 2px strokes; the tile supplies size and color so one drawing
|
|
937
|
+
// works on every category tone.
|
|
938
|
+
function TaskGlyph(props) {
|
|
939
|
+
const { task, size } = props
|
|
940
|
+
const icon = taskIcon(task)
|
|
941
|
+
return h('svg', {
|
|
942
|
+
viewBox: '0 0 24 24',
|
|
943
|
+
width: size,
|
|
944
|
+
height: size,
|
|
945
|
+
fill: 'none',
|
|
946
|
+
stroke: 'currentColor',
|
|
947
|
+
strokeWidth: 2,
|
|
948
|
+
strokeLinecap: 'round',
|
|
949
|
+
strokeLinejoin: 'round',
|
|
950
|
+
'aria-hidden': 'true',
|
|
951
|
+
focusable: 'false',
|
|
952
|
+
}, h('path', { d: icon.d }))
|
|
851
953
|
}
|
|
852
954
|
|
|
853
955
|
function Glyph(props) {
|
|
854
956
|
const { task, size = 34, large = false } = props
|
|
855
|
-
const GlyphIcon = taskGlyph(task)
|
|
856
957
|
const edge = large ? 44 : size
|
|
857
958
|
return h('span', {
|
|
858
959
|
className: 'capital-automation-glyph',
|
|
859
960
|
'data-tone': toneOf(task),
|
|
860
961
|
style: { width: edge, height: edge, borderRadius: large ? 13 : 10 },
|
|
861
|
-
}, h(
|
|
962
|
+
}, h(TaskGlyph, { task, size: large ? 22 : 17 }))
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function IconPickerTrigger(props) {
|
|
966
|
+
const { task, size = 38, large = false, onChange, disabled } = props
|
|
967
|
+
const [open, setOpen] = useState(false)
|
|
968
|
+
const selectedId = taskIconId(task)
|
|
969
|
+
const selected = iconDefinition(selectedId) ?? TASK_ICON_CATALOG[DEFAULT_CUSTOM_ICON]
|
|
970
|
+
useEffect(() => {
|
|
971
|
+
if (disabled === true && open) setOpen(false)
|
|
972
|
+
}, [disabled, open])
|
|
973
|
+
// The host Menu owns the portal, outside-click, Escape and button walk; the grid is only content.
|
|
974
|
+
return h(Menu, {
|
|
975
|
+
open,
|
|
976
|
+
anchor: h('button', {
|
|
977
|
+
type: 'button',
|
|
978
|
+
className: 'capital-automation-glyphbtn',
|
|
979
|
+
disabled,
|
|
980
|
+
'aria-label': `${zh.iconField}:${selected.label},点击更换`,
|
|
981
|
+
'aria-haspopup': 'menu',
|
|
982
|
+
'aria-expanded': open ? 'true' : 'false',
|
|
983
|
+
title: zh.iconPickHint,
|
|
984
|
+
onClick: () => setOpen((current) => !current),
|
|
985
|
+
},
|
|
986
|
+
h(Glyph, { task, size, large }),
|
|
987
|
+
h('span', { className: 'capital-automation-glyphbtnchevron', 'aria-hidden': 'true' },
|
|
988
|
+
h(IconChevronDownOutlineRegular, { size: 12 }))),
|
|
989
|
+
items: [],
|
|
990
|
+
onClose: () => setOpen(false),
|
|
991
|
+
align: 'start',
|
|
992
|
+
side: 'bottom',
|
|
993
|
+
portal: true,
|
|
994
|
+
listClassName: 'capital-automation-iconmenu',
|
|
995
|
+
}, h('div', { className: 'capital-automation-iconmenu-grid' },
|
|
996
|
+
TASK_ICON_IDS.map((id) => {
|
|
997
|
+
const icon = TASK_ICON_CATALOG[id]
|
|
998
|
+
const active = id === selectedId
|
|
999
|
+
return h('button', {
|
|
1000
|
+
key: id,
|
|
1001
|
+
type: 'button',
|
|
1002
|
+
role: 'menuitemradio',
|
|
1003
|
+
'aria-checked': active ? 'true' : 'false',
|
|
1004
|
+
'aria-label': icon.label,
|
|
1005
|
+
title: icon.label,
|
|
1006
|
+
className: 'capital-automation-iconmenu-item',
|
|
1007
|
+
'data-selected': active ? 'true' : undefined,
|
|
1008
|
+
onClick: () => { onChange(id); setOpen(false) },
|
|
1009
|
+
}, h('svg', {
|
|
1010
|
+
viewBox: '0 0 24 24',
|
|
1011
|
+
width: 18,
|
|
1012
|
+
height: 18,
|
|
1013
|
+
fill: 'none',
|
|
1014
|
+
stroke: 'currentColor',
|
|
1015
|
+
strokeWidth: 2,
|
|
1016
|
+
strokeLinecap: 'round',
|
|
1017
|
+
strokeLinejoin: 'round',
|
|
1018
|
+
'aria-hidden': 'true',
|
|
1019
|
+
focusable: 'false',
|
|
1020
|
+
}, h('path', { d: icon.d })))
|
|
1021
|
+
})))
|
|
862
1022
|
}
|
|
863
1023
|
|
|
864
1024
|
function StateBadge(props) {
|
|
@@ -1119,16 +1279,28 @@ function triggerChanged(cadence, trigger) {
|
|
|
1119
1279
|
return next
|
|
1120
1280
|
}
|
|
1121
1281
|
|
|
1122
|
-
|
|
1282
|
+
/** New-task limits start from the settings card; leaving them untouched keeps the task inherited. */
|
|
1283
|
+
function defaultLimits(meta) {
|
|
1284
|
+
const settings = meta?.execution?.settings
|
|
1285
|
+
return {
|
|
1286
|
+
runTimeoutMinutes: settings?.runTimeoutMinutes ?? DEFAULT_LIMITS.runTimeoutMinutes,
|
|
1287
|
+
idleTimeoutMinutes: settings?.idleTimeoutMinutes ?? DEFAULT_LIMITS.idleTimeoutMinutes,
|
|
1288
|
+
maxSteps: settings?.maxSteps ?? DEFAULT_LIMITS.maxSteps,
|
|
1289
|
+
maxToolCalls: settings?.maxToolCalls ?? DEFAULT_LIMITS.maxToolCalls,
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function defaultDraft(meta) {
|
|
1123
1294
|
return {
|
|
1124
1295
|
title: '我的投研任务',
|
|
1125
1296
|
description: '',
|
|
1126
1297
|
instruction: '',
|
|
1127
1298
|
category: 'research',
|
|
1299
|
+
icon: DEFAULT_CUSTOM_ICON,
|
|
1128
1300
|
trigger: { kind: 'schedule', cadence: 'daily', time: '09:00', timezone: 'Asia/Shanghai', tradingDaysOnly: true },
|
|
1129
1301
|
workspaceId: '',
|
|
1130
|
-
limits:
|
|
1131
|
-
notifications: { inApp:
|
|
1302
|
+
limits: defaultLimits(meta),
|
|
1303
|
+
notifications: { inApp: meta?.execution?.settings?.inAppNotifications !== false, onFailure: true, onSuccess: false },
|
|
1132
1304
|
}
|
|
1133
1305
|
}
|
|
1134
1306
|
|
|
@@ -1138,6 +1310,7 @@ function draftPayload(task) {
|
|
|
1138
1310
|
description: task.description ?? '',
|
|
1139
1311
|
instruction: task.instruction ?? '',
|
|
1140
1312
|
category: task.category,
|
|
1313
|
+
...(task.origin === 'builtin' ? {} : { icon: task.icon ?? DEFAULT_CUSTOM_ICON }),
|
|
1141
1314
|
trigger: normalizeTrigger(task.trigger),
|
|
1142
1315
|
workspaceId: task.workspaceId ?? '',
|
|
1143
1316
|
limits: task.limits,
|
|
@@ -1145,8 +1318,21 @@ function draftPayload(task) {
|
|
|
1145
1318
|
}
|
|
1146
1319
|
}
|
|
1147
1320
|
|
|
1321
|
+
function payloadKey(task) {
|
|
1322
|
+
return JSON.stringify(draftPayload(task))
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1148
1325
|
function samePayload(left, right) {
|
|
1149
|
-
return
|
|
1326
|
+
return payloadKey(left) === payloadKey(right)
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
function cloneTask(task) {
|
|
1330
|
+
return {
|
|
1331
|
+
...task,
|
|
1332
|
+
trigger: normalizeTrigger(task.trigger),
|
|
1333
|
+
limits: { ...task.limits },
|
|
1334
|
+
notifications: { ...task.notifications },
|
|
1335
|
+
}
|
|
1150
1336
|
}
|
|
1151
1337
|
|
|
1152
1338
|
function groupRunCounts(runs) {
|
|
@@ -1204,11 +1390,7 @@ function notificationMessage(item) {
|
|
|
1204
1390
|
return ''
|
|
1205
1391
|
}
|
|
1206
1392
|
|
|
1207
|
-
function
|
|
1208
|
-
return !samePayload(draft, task)
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
function draftIssues(task) {
|
|
1393
|
+
function draftIssues(task, meta) {
|
|
1212
1394
|
const issues = {}
|
|
1213
1395
|
if (String(task.title ?? '').trim().length === 0) issues.title = zh.titleRequired
|
|
1214
1396
|
if (String(task.instruction ?? '').trim().length === 0) issues.instruction = zh.instructionRequired
|
|
@@ -1230,7 +1412,15 @@ function draftIssues(task) {
|
|
|
1230
1412
|
}
|
|
1231
1413
|
const limits = task.limits ?? {}
|
|
1232
1414
|
for (const key of ['runTimeoutMinutes', 'idleTimeoutMinutes', 'maxSteps', 'maxToolCalls']) {
|
|
1233
|
-
|
|
1415
|
+
const value = limits[key]
|
|
1416
|
+
if (!Number.isFinite(value) || !Number.isInteger(value)) {
|
|
1417
|
+
issues[key] = zh.invalidLimit
|
|
1418
|
+
continue
|
|
1419
|
+
}
|
|
1420
|
+
const range = meta?.execution?.limits?.[key]
|
|
1421
|
+
if (Array.isArray(range) && range.length === 2 && (value < range[0] || value > range[1])) {
|
|
1422
|
+
issues[key] = zh.invalidLimitRange.replace('{min}', String(range[0])).replace('{max}', String(range[1]))
|
|
1423
|
+
}
|
|
1234
1424
|
}
|
|
1235
1425
|
return issues
|
|
1236
1426
|
}
|
|
@@ -1239,9 +1429,9 @@ function draftIssues(task) {
|
|
|
1239
1429
|
* Tracks which fields the user has left so errors appear on blur, then update live.
|
|
1240
1430
|
* `resetKey` clears that state when the edited task or modal session changes.
|
|
1241
1431
|
*/
|
|
1242
|
-
function useDraftIssues(task, resetKey) {
|
|
1432
|
+
function useDraftIssues(task, resetKey, meta) {
|
|
1243
1433
|
const [touched, setTouched] = useState({})
|
|
1244
|
-
const issues = draftIssues(task)
|
|
1434
|
+
const issues = draftIssues(task, meta)
|
|
1245
1435
|
useEffect(() => { setTouched({}) }, [resetKey])
|
|
1246
1436
|
const errorFor = (field) => (touched[field] === true ? issues[field] : undefined)
|
|
1247
1437
|
const touch = (field) => setTouched((current) => (current[field] === true ? current : { ...current, [field]: true }))
|
|
@@ -1250,8 +1440,7 @@ function useDraftIssues(task, resetKey) {
|
|
|
1250
1440
|
for (const field of Object.keys(issues)) next[field] = true
|
|
1251
1441
|
return next
|
|
1252
1442
|
})
|
|
1253
|
-
|
|
1254
|
-
return { issues, errorFor, touch, touchAll, clear }
|
|
1443
|
+
return { issues, errorFor, touch, touchAll }
|
|
1255
1444
|
}
|
|
1256
1445
|
|
|
1257
1446
|
function limitValue(meta, key, fallback) {
|
|
@@ -1378,14 +1567,60 @@ function NotificationsButton(props) {
|
|
|
1378
1567
|
className: 'capital-automation-btn',
|
|
1379
1568
|
'data-variant': 'ghost',
|
|
1380
1569
|
onClick,
|
|
1381
|
-
title: zh.
|
|
1382
|
-
'aria-label': zh.
|
|
1570
|
+
title: zh.notificationsEntry,
|
|
1571
|
+
'aria-label': zh.notificationsEntry,
|
|
1383
1572
|
},
|
|
1384
1573
|
h(IconAlarmClockOutlineRegular, { size: 14 }),
|
|
1385
|
-
h('span', { className: 'capital-automation-btnlabel' }, zh.
|
|
1574
|
+
h('span', { className: 'capital-automation-btnlabel' }, zh.notificationsEntry),
|
|
1386
1575
|
unread > 0 ? h('span', { className: 'capital-automation-count' }, String(unread > 99 ? '99+' : unread)) : null)
|
|
1387
1576
|
}
|
|
1388
1577
|
|
|
1578
|
+
function systemStatusReason(code) {
|
|
1579
|
+
if (code === 'lease_held') return zh.systemReasonLeaseHeld
|
|
1580
|
+
if (code === 'owner_unverified') return zh.systemReasonUnverified
|
|
1581
|
+
return zh.systemReasonLeaseLost
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
/** Profile-level scheduling state lives here, not in the task inbox. */
|
|
1585
|
+
function SystemStatus(props) {
|
|
1586
|
+
const { meta } = props
|
|
1587
|
+
const [open, setOpen] = useState(false)
|
|
1588
|
+
if (meta === null || meta === undefined) return null
|
|
1589
|
+
const scheduler = meta.scheduler ?? {}
|
|
1590
|
+
const state = scheduler.state
|
|
1591
|
+
const summary = state === 'active'
|
|
1592
|
+
? zh.systemActive
|
|
1593
|
+
: state === 'standby'
|
|
1594
|
+
? zh.systemStandby
|
|
1595
|
+
: state === 'starting'
|
|
1596
|
+
? zh.systemStarting
|
|
1597
|
+
: state === 'stopped'
|
|
1598
|
+
? zh.systemStopped
|
|
1599
|
+
: zh.systemUnavailable
|
|
1600
|
+
const calendarSummary = meta.calendar?.available === false ? zh.systemCalendarUnavailable : zh.systemCalendarOk
|
|
1601
|
+
const lastStandbyAt = scheduler.lastStandbyAt
|
|
1602
|
+
return h('section', { className: 'capital-automation-systemstatus', 'aria-label': zh.systemStatus },
|
|
1603
|
+
h('div', { className: 'capital-automation-systemline' },
|
|
1604
|
+
h('span', null, `${zh.systemStatus}:${summary} · ${calendarSummary}`),
|
|
1605
|
+
h('button', {
|
|
1606
|
+
type: 'button',
|
|
1607
|
+
className: 'capital-automation-systemtoggle',
|
|
1608
|
+
'aria-expanded': open ? 'true' : 'false',
|
|
1609
|
+
onClick: () => setOpen((value) => !value),
|
|
1610
|
+
}, open ? zh.systemHideDetails : zh.systemDetails)),
|
|
1611
|
+
open
|
|
1612
|
+
? h('div', { className: 'capital-automation-systemdetail' },
|
|
1613
|
+
lastStandbyAt
|
|
1614
|
+
? h('p', null, `${zh.systemLastStandby}:${formatShortTime(lastStandbyAt)} · ${systemStatusReason(scheduler.lastStandbyCode)}`)
|
|
1615
|
+
: h('p', null, zh.systemNoStandby),
|
|
1616
|
+
lastStandbyAt && scheduler.lastStandbyReason
|
|
1617
|
+
? h('details', null,
|
|
1618
|
+
h('summary', null, zh.systemTechnical),
|
|
1619
|
+
h('p', null, scheduler.lastStandbyReason))
|
|
1620
|
+
: null)
|
|
1621
|
+
: null)
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1389
1624
|
function Header(props) {
|
|
1390
1625
|
const { tab, onTab, onNewTask, unread, onOpenInbox } = props
|
|
1391
1626
|
return h('header', { className: 'capital-automation-head' },
|
|
@@ -1786,8 +2021,10 @@ function TaskDashboard(props) {
|
|
|
1786
2021
|
market: roster.filter((task) => task.category === 'market').length,
|
|
1787
2022
|
review: roster.filter((task) => task.category === 'review').length,
|
|
1788
2023
|
research: roster.filter((task) => task.category === 'research').length,
|
|
2024
|
+
portfolio: roster.filter((task) => task.category === 'portfolio').length,
|
|
2025
|
+
monitor: roster.filter((task) => task.category === 'monitor').length,
|
|
1789
2026
|
}
|
|
1790
|
-
const filters = ['all', 'custom', 'news', 'market', 'review', 'research']
|
|
2027
|
+
const filters = ['all', 'custom', 'news', 'market', 'review', 'research', 'portfolio', 'monitor']
|
|
1791
2028
|
return h('div', null,
|
|
1792
2029
|
h(RunHistoryCard, { runs, onViewAll: onGoRuns }),
|
|
1793
2030
|
h('section', { className: 'capital-automation-section' },
|
|
@@ -1992,12 +2229,13 @@ function MonthlySelect(props) {
|
|
|
1992
2229
|
}
|
|
1993
2230
|
|
|
1994
2231
|
function TriggerFields(props) {
|
|
1995
|
-
const { trigger, onChange, disabled, meta, errorFor, touch } = props
|
|
2232
|
+
const { trigger, onChange, disabled, meta, errorFor, touch, onFlush } = props
|
|
1996
2233
|
const update = (patch) => onChange(normalizeTrigger({ ...trigger, ...patch }))
|
|
1997
2234
|
const setCadence = (cadence) => onChange(triggerChanged(cadence, trigger))
|
|
1998
2235
|
|
|
1999
2236
|
const issue = (field) => (typeof errorFor === 'function' ? errorFor(field) : undefined)
|
|
2000
2237
|
const touchField = (field) => { if (typeof touch === 'function') touch(field) }
|
|
2238
|
+
const commit = () => { if (typeof onFlush === 'function') onFlush() }
|
|
2001
2239
|
const timeError = issue('time')
|
|
2002
2240
|
const timezoneError = issue('timezone')
|
|
2003
2241
|
const weekdayError = issue('weekdays')
|
|
@@ -2051,7 +2289,7 @@ function TriggerFields(props) {
|
|
|
2051
2289
|
'aria-invalid': dateError ? 'true' : undefined,
|
|
2052
2290
|
'data-invalid': dateError ? 'true' : undefined,
|
|
2053
2291
|
onChange: (event) => update({ date: event.target.value }),
|
|
2054
|
-
onBlur: () => touchField('date'),
|
|
2292
|
+
onBlur: () => { touchField('date'); commit() },
|
|
2055
2293
|
}))
|
|
2056
2294
|
: null)),
|
|
2057
2295
|
h('div', { className: 'capital-automation-triggerfield' },
|
|
@@ -2065,7 +2303,7 @@ function TriggerFields(props) {
|
|
|
2065
2303
|
'aria-invalid': timeError ? 'true' : undefined,
|
|
2066
2304
|
'data-invalid': timeError ? 'true' : undefined,
|
|
2067
2305
|
onChange: (event) => update({ time: event.target.value }),
|
|
2068
|
-
onBlur: () => touchField('time'),
|
|
2306
|
+
onBlur: () => { touchField('time'); commit() },
|
|
2069
2307
|
}))),
|
|
2070
2308
|
h('div', { className: 'capital-automation-triggersettings' },
|
|
2071
2309
|
h('span', { className: 'capital-automation-triggerzone' },
|
|
@@ -2393,41 +2631,31 @@ function OverviewTab(props) {
|
|
|
2393
2631
|
}
|
|
2394
2632
|
|
|
2395
2633
|
function ConfigTab(props) {
|
|
2396
|
-
const { draft, setDraft, meta, busy, dirty,
|
|
2634
|
+
const { draft, setDraft, meta, busy, dirty, saveState, saveError, retry, onFlush, disabled } = props
|
|
2397
2635
|
const task = draft
|
|
2398
|
-
const {
|
|
2399
|
-
const formRef = useRef(null)
|
|
2636
|
+
const { errorFor, touch } = useDraftIssues(task, task.id, meta)
|
|
2400
2637
|
const titleError = errorFor('title')
|
|
2401
2638
|
const instructionError = errorFor('instruction')
|
|
2402
2639
|
const set = (patch) => setDraft((current) => ({
|
|
2403
2640
|
...current,
|
|
2404
2641
|
...(typeof patch === 'function' ? patch(current) : patch),
|
|
2405
2642
|
}))
|
|
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
|
-
}
|
|
2643
|
+
const flush = () => { if (typeof onFlush === 'function') onFlush() }
|
|
2417
2644
|
const timeoutRange = limitValue(meta, 'runTimeoutMinutes', [5, 120])
|
|
2418
2645
|
const idleRange = limitValue(meta, 'idleTimeoutMinutes', [1, 30])
|
|
2419
|
-
const stepsRange = limitValue(meta, 'maxSteps', [
|
|
2420
|
-
const toolsRange = limitValue(meta, 'maxToolCalls', [
|
|
2646
|
+
const stepsRange = limitValue(meta, 'maxSteps', [100, 2000])
|
|
2647
|
+
const toolsRange = limitValue(meta, 'maxToolCalls', [100, 5000])
|
|
2421
2648
|
const instructionEditor = h(Field, { label: zh.instructionField, error: instructionError },
|
|
2422
2649
|
h(PlainTextEditor, {
|
|
2423
2650
|
className: 'capital-automation-texteditor',
|
|
2424
2651
|
value: task.instruction,
|
|
2652
|
+
maxLength: INSTRUCTION_MAX_LENGTH,
|
|
2425
2653
|
disabled: busy || disabled,
|
|
2426
2654
|
placeholder: zh.instructionPlaceholder,
|
|
2427
2655
|
ariaLabel: zh.instructionField,
|
|
2428
2656
|
invalid: instructionError ? true : undefined,
|
|
2429
2657
|
onChange: (instruction) => set({ instruction }),
|
|
2430
|
-
onBlur: () => touch('instruction'),
|
|
2658
|
+
onBlur: () => { touch('instruction'); flush() },
|
|
2431
2659
|
}))
|
|
2432
2660
|
const instructionSide = h('div', { className: 'capital-automation-instructionside' },
|
|
2433
2661
|
h(SelectField, {
|
|
@@ -2440,24 +2668,43 @@ function ConfigTab(props) {
|
|
|
2440
2668
|
meta?.workspaceError ? h('p', { className: 'capital-automation-hint', 'data-tone': 'error' }, zh.workspaceUnavailable) : null,
|
|
2441
2669
|
h('div', null,
|
|
2442
2670
|
h('span', { className: 'capital-automation-formlabel' }, zh.triggerField),
|
|
2443
|
-
h(TriggerFields, { trigger: task.trigger, disabled: busy || disabled, onChange: (trigger) => set({ trigger }), meta, errorFor, touch })))
|
|
2671
|
+
h(TriggerFields, { trigger: task.trigger, disabled: busy || disabled, onChange: (trigger) => set({ trigger }), meta, errorFor, touch, onFlush: flush })))
|
|
2444
2672
|
const advanced = h('details', { className: 'capital-automation-advanced' },
|
|
2445
2673
|
h('summary', null, zh.advanced),
|
|
2446
2674
|
h('div', { className: 'capital-automation-advancedbody' },
|
|
2447
2675
|
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 } })) })),
|
|
2676
|
+
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 } })) }),
|
|
2677
|
+
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 } })) }),
|
|
2678
|
+
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 } })) }),
|
|
2679
|
+
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
2680
|
h('p', { className: 'capital-automation-hint' }, zh.budgetHint)))
|
|
2453
2681
|
const notificationRows = h('div', null,
|
|
2454
2682
|
h('span', { className: 'capital-automation-formlabel' }, zh.notifyField),
|
|
2455
2683
|
h('div', { className: 'capital-automation-switchlist' },
|
|
2456
2684
|
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 } })) })),
|
|
2685
|
+
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 } })) }),
|
|
2686
|
+
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
2687
|
h('p', { className: 'capital-automation-hint' }, zh.notifyHint))
|
|
2460
|
-
|
|
2688
|
+
const state = disabled
|
|
2689
|
+
? 'disabled'
|
|
2690
|
+
: dirty && (saveState === 'idle' || saveState === 'saved')
|
|
2691
|
+
? 'pending'
|
|
2692
|
+
: !dirty && saveState === 'pending'
|
|
2693
|
+
? 'idle'
|
|
2694
|
+
: saveState
|
|
2695
|
+
const statusText = state === 'saving'
|
|
2696
|
+
? zh.saving
|
|
2697
|
+
: state === 'error'
|
|
2698
|
+
? `${zh.autosaveFailed}:${saveError || zh.errorGeneric}`
|
|
2699
|
+
: state === 'invalid'
|
|
2700
|
+
? zh.autosaveInvalid
|
|
2701
|
+
: state === 'pending'
|
|
2702
|
+
? zh.autosavePending
|
|
2703
|
+
: state === 'saved'
|
|
2704
|
+
? zh.autosaveSaved
|
|
2705
|
+
: zh.noDirty
|
|
2706
|
+
const statusTone = state === 'error' ? 'error' : state === 'saved' ? 'ok' : undefined
|
|
2707
|
+
return h('div', { className: 'capital-automation-form' },
|
|
2461
2708
|
h('div', { className: 'capital-automation-titlerow' },
|
|
2462
2709
|
h(Field, {
|
|
2463
2710
|
label: zh.titleField,
|
|
@@ -2470,7 +2717,7 @@ function ConfigTab(props) {
|
|
|
2470
2717
|
disabled: busy || disabled,
|
|
2471
2718
|
'aria-invalid': titleError ? 'true' : undefined,
|
|
2472
2719
|
onChange: (event) => set({ title: event.target.value }),
|
|
2473
|
-
onBlur: () => touch('title'),
|
|
2720
|
+
onBlur: () => { touch('title'); flush() },
|
|
2474
2721
|
})),
|
|
2475
2722
|
h(SelectField, {
|
|
2476
2723
|
label: zh.categoryField,
|
|
@@ -2480,6 +2727,18 @@ function ConfigTab(props) {
|
|
|
2480
2727
|
disabled: busy || disabled || task.origin === 'builtin',
|
|
2481
2728
|
onChange: (category) => set({ category }),
|
|
2482
2729
|
})),
|
|
2730
|
+
task.origin === 'custom'
|
|
2731
|
+
? h('div', { className: 'capital-automation-iconrow' },
|
|
2732
|
+
h(IconPickerTrigger, {
|
|
2733
|
+
task,
|
|
2734
|
+
size: 38,
|
|
2735
|
+
disabled: busy || disabled,
|
|
2736
|
+
onChange: (icon) => set({ icon }),
|
|
2737
|
+
}),
|
|
2738
|
+
h('div', { className: 'capital-automation-iconrowbody' },
|
|
2739
|
+
h('span', { className: 'capital-automation-formlabel' }, zh.iconField),
|
|
2740
|
+
h('span', { className: 'capital-automation-hint' }, `${taskIcon(task).label} · ${zh.iconPickHint}`)))
|
|
2741
|
+
: h('p', { className: 'capital-automation-hint' }, zh.iconBuiltinHint),
|
|
2483
2742
|
h(Field, {
|
|
2484
2743
|
label: zh.descriptionField,
|
|
2485
2744
|
note: zh.charCount.replace('{n}', String(Array.from(task.description ?? '').length)).replace('{max}', String(DESCRIPTION_MAX_LENGTH)),
|
|
@@ -2492,6 +2751,7 @@ function ConfigTab(props) {
|
|
|
2492
2751
|
placeholder: zh.descriptionPlaceholder,
|
|
2493
2752
|
ariaLabel: zh.descriptionField,
|
|
2494
2753
|
onChange: (description) => set({ description }),
|
|
2754
|
+
onBlur: flush,
|
|
2495
2755
|
})),
|
|
2496
2756
|
h('div', { className: 'capital-automation-instructionrow' },
|
|
2497
2757
|
instructionEditor,
|
|
@@ -2499,10 +2759,18 @@ function ConfigTab(props) {
|
|
|
2499
2759
|
advanced,
|
|
2500
2760
|
notificationRows,
|
|
2501
2761
|
h('div', { className: 'capital-automation-formactions', 'data-sticky': 'true' },
|
|
2502
|
-
h('span', { className: 'capital-automation-formstatus' },
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2762
|
+
h('span', { className: 'capital-automation-formstatus' }, disabled ? zh.storageUnavailable : zh.autosaveHint),
|
|
2763
|
+
state === 'disabled'
|
|
2764
|
+
? null
|
|
2765
|
+
: h('span', {
|
|
2766
|
+
className: 'capital-automation-autosavestate',
|
|
2767
|
+
role: 'status',
|
|
2768
|
+
'data-tone': statusTone,
|
|
2769
|
+
title: state === 'error' ? saveError : undefined,
|
|
2770
|
+
}, statusText),
|
|
2771
|
+
state === 'error'
|
|
2772
|
+
? h('button', { type: 'button', className: 'capital-automation-btn', 'data-size': 'sm', disabled: busy || disabled, onClick: retry }, zh.autosaveRetry)
|
|
2773
|
+
: null))
|
|
2506
2774
|
}
|
|
2507
2775
|
|
|
2508
2776
|
function NotificationsTab(props) {
|
|
@@ -2553,19 +2821,72 @@ function DetailBar(props) {
|
|
|
2553
2821
|
function TaskDetailPage(props) {
|
|
2554
2822
|
const { detail, meta, api, onChanged, openSession, tab, onTab, onRequestConfirm, onRequestCancelRun, schedulerActive, storageReady } = props
|
|
2555
2823
|
const [draft, setDraft] = useState(null)
|
|
2824
|
+
const [savedTask, setSavedTask] = useState(null)
|
|
2556
2825
|
const [busy, setBusy] = useState('')
|
|
2557
2826
|
const [notice, setNotice] = useState('')
|
|
2558
2827
|
const [error, setError] = useState('')
|
|
2828
|
+
const [saveState, setSaveState] = useState('idle')
|
|
2829
|
+
const [saveError, setSaveError] = useState('')
|
|
2559
2830
|
const [menuOpen, setMenuOpen] = useState(false)
|
|
2560
2831
|
const lastTaskIdRef = useRef(null)
|
|
2832
|
+
const activeTaskIdRef = useRef(null)
|
|
2833
|
+
const draftRef = useRef(null)
|
|
2834
|
+
const savedTaskRef = useRef(null)
|
|
2835
|
+
const ackedPayloadRef = useRef(null)
|
|
2836
|
+
const failedPayloadRef = useRef(null)
|
|
2837
|
+
const saveTimerRef = useRef(null)
|
|
2838
|
+
const saveInFlightRef = useRef(false)
|
|
2839
|
+
const saveQueuedRef = useRef(false)
|
|
2840
|
+
const savePromiseRef = useRef(null)
|
|
2841
|
+
const flushRef = useRef(null)
|
|
2842
|
+
const storageReadyNow = storageReady !== false
|
|
2843
|
+
activeTaskIdRef.current = detail.task?.id ?? null
|
|
2844
|
+
|
|
2845
|
+
const updateDraft = (patch) => {
|
|
2846
|
+
const current = draftRef.current
|
|
2847
|
+
if (current === null) return
|
|
2848
|
+
const next = {
|
|
2849
|
+
...current,
|
|
2850
|
+
...(typeof patch === 'function' ? patch(current) : patch),
|
|
2851
|
+
}
|
|
2852
|
+
draftRef.current = next
|
|
2853
|
+
setDraft(next)
|
|
2854
|
+
}
|
|
2561
2855
|
|
|
2562
2856
|
useEffect(() => {
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2857
|
+
const server = detail.task
|
|
2858
|
+
if (!server) {
|
|
2859
|
+
savedTaskRef.current = null
|
|
2860
|
+
ackedPayloadRef.current = null
|
|
2861
|
+
setSavedTask(null)
|
|
2862
|
+
setDraft(null)
|
|
2863
|
+
draftRef.current = null
|
|
2864
|
+
return
|
|
2865
|
+
}
|
|
2866
|
+
const nextSaved = cloneTask(server)
|
|
2867
|
+
const previousAcked = ackedPayloadRef.current
|
|
2868
|
+
savedTaskRef.current = nextSaved
|
|
2869
|
+
ackedPayloadRef.current = payloadKey(nextSaved)
|
|
2870
|
+
setSavedTask(nextSaved)
|
|
2871
|
+
const current = draftRef.current
|
|
2872
|
+
if (current === null || current.id !== nextSaved.id) {
|
|
2873
|
+
draftRef.current = nextSaved
|
|
2874
|
+
setDraft(nextSaved)
|
|
2875
|
+
return
|
|
2876
|
+
}
|
|
2877
|
+
if (samePayload(current, nextSaved)) return
|
|
2878
|
+
// Hibernate clears overrides on the server; discard a local draft that a failed save
|
|
2879
|
+
// could otherwise resurrect after the task returns to its template defaults.
|
|
2880
|
+
if (nextSaved.state === 'dormant' && current.state !== 'dormant') {
|
|
2881
|
+
draftRef.current = nextSaved
|
|
2882
|
+
setDraft(nextSaved)
|
|
2883
|
+
return
|
|
2884
|
+
}
|
|
2885
|
+
// A poll or another action must not clobber edits the user has not saved yet.
|
|
2886
|
+
if (previousAcked !== null && payloadKey(current) === previousAcked) {
|
|
2887
|
+
draftRef.current = nextSaved
|
|
2888
|
+
setDraft(nextSaved)
|
|
2889
|
+
}
|
|
2569
2890
|
}, [detail])
|
|
2570
2891
|
|
|
2571
2892
|
useEffect(() => {
|
|
@@ -2574,20 +2895,57 @@ function TaskDetailPage(props) {
|
|
|
2574
2895
|
lastTaskIdRef.current = id
|
|
2575
2896
|
setNotice('')
|
|
2576
2897
|
setError('')
|
|
2898
|
+
setSaveState('idle')
|
|
2899
|
+
setSaveError('')
|
|
2900
|
+
failedPayloadRef.current = null
|
|
2577
2901
|
}, [detail])
|
|
2578
2902
|
|
|
2903
|
+
useEffect(() => {
|
|
2904
|
+
if (draft === null || savedTask === null || storageReadyNow === false) return undefined
|
|
2905
|
+
if (samePayload(draft, savedTask)) {
|
|
2906
|
+
setSaveState((state) => (state === 'pending' || state === 'invalid' || state === 'error' ? 'idle' : state))
|
|
2907
|
+
return undefined
|
|
2908
|
+
}
|
|
2909
|
+
if (busy) return undefined
|
|
2910
|
+
if (Object.keys(draftIssues(draft, meta)).length > 0) {
|
|
2911
|
+
setSaveState('invalid')
|
|
2912
|
+
return undefined
|
|
2913
|
+
}
|
|
2914
|
+
if (failedPayloadRef.current !== null && payloadKey(draft) === failedPayloadRef.current) {
|
|
2915
|
+
setSaveState('error')
|
|
2916
|
+
return undefined
|
|
2917
|
+
}
|
|
2918
|
+
setSaveState((state) => (state === 'saving' ? state : 'pending'))
|
|
2919
|
+
saveTimerRef.current = setTimeout(() => { void flushRef.current?.() }, AUTOSAVE_DELAY_MS)
|
|
2920
|
+
return () => {
|
|
2921
|
+
if (saveTimerRef.current !== null) {
|
|
2922
|
+
clearTimeout(saveTimerRef.current)
|
|
2923
|
+
saveTimerRef.current = null
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
}, [draft, savedTask, busy, storageReadyNow, meta])
|
|
2927
|
+
|
|
2928
|
+
useEffect(() => {
|
|
2929
|
+
if (tab === 'config') return
|
|
2930
|
+
void flushRef.current?.()
|
|
2931
|
+
}, [tab])
|
|
2932
|
+
|
|
2933
|
+
useEffect(() => () => { void flushRef.current?.() }, [])
|
|
2934
|
+
|
|
2935
|
+
useEffect(() => {
|
|
2936
|
+
if (typeof document === 'undefined') return undefined
|
|
2937
|
+
const onHide = () => { if (document.visibilityState === 'hidden') void flushRef.current?.() }
|
|
2938
|
+
document.addEventListener?.('visibilitychange', onHide)
|
|
2939
|
+
return () => document.removeEventListener?.('visibilitychange', onHide)
|
|
2940
|
+
}, [])
|
|
2941
|
+
|
|
2579
2942
|
if (draft === null) return h('p', { className: 'capital-automation-empty' }, zh.loading)
|
|
2580
2943
|
const task = draft
|
|
2581
|
-
const storageReadyNow = storageReady !== false
|
|
2582
2944
|
const running = detail.runs.some((run) => isActiveRun(run))
|
|
2583
2945
|
const activeRun = detail.runs.find((run) => isActiveRun(run))
|
|
2584
2946
|
const unread = detail.notifications.filter((item) => item.readAt === undefined).length
|
|
2585
|
-
const dirty =
|
|
2947
|
+
const dirty = savedTask === null ? false : !samePayload(draft, savedTask)
|
|
2586
2948
|
const canRun = storageReadyNow && schedulerActive === true && !running
|
|
2587
|
-
const set = (patch) => setDraft((current) => ({
|
|
2588
|
-
...current,
|
|
2589
|
-
...(typeof patch === 'function' ? patch(current) : patch),
|
|
2590
|
-
}))
|
|
2591
2949
|
|
|
2592
2950
|
async function act(name, action, success) {
|
|
2593
2951
|
if (busy) return
|
|
@@ -2604,10 +2962,90 @@ function TaskDetailPage(props) {
|
|
|
2604
2962
|
}
|
|
2605
2963
|
}
|
|
2606
2964
|
|
|
2607
|
-
async function
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2965
|
+
async function flush() {
|
|
2966
|
+
if (saveTimerRef.current !== null) {
|
|
2967
|
+
clearTimeout(saveTimerRef.current)
|
|
2968
|
+
saveTimerRef.current = null
|
|
2969
|
+
}
|
|
2970
|
+
if (saveInFlightRef.current) {
|
|
2971
|
+
saveQueuedRef.current = true
|
|
2972
|
+
return savePromiseRef.current ?? undefined
|
|
2973
|
+
}
|
|
2974
|
+
const current = draftRef.current
|
|
2975
|
+
const base = savedTaskRef.current
|
|
2976
|
+
if (current === null || base === null || storageReadyNow === false) return
|
|
2977
|
+
if (samePayload(current, base)) {
|
|
2978
|
+
setSaveState((state) => (state === 'saving' ? state : 'idle'))
|
|
2979
|
+
return
|
|
2980
|
+
}
|
|
2981
|
+
if (Object.keys(draftIssues(current, meta)).length > 0) {
|
|
2982
|
+
setSaveState('invalid')
|
|
2983
|
+
return
|
|
2984
|
+
}
|
|
2985
|
+
const payload = draftPayload(current)
|
|
2986
|
+
const taskId = current.id
|
|
2987
|
+
saveInFlightRef.current = true
|
|
2988
|
+
saveQueuedRef.current = false
|
|
2989
|
+
setSaveState('saving')
|
|
2990
|
+
setSaveError('')
|
|
2991
|
+
const savePromise = (async () => {
|
|
2992
|
+
try {
|
|
2993
|
+
const result = await api.patch(taskId, payload)
|
|
2994
|
+
if (activeTaskIdRef.current !== taskId) {
|
|
2995
|
+
void onChanged?.(taskId)
|
|
2996
|
+
return
|
|
2997
|
+
}
|
|
2998
|
+
const serverTask = result?.task
|
|
2999
|
+
const nextSaved = serverTask === undefined ? cloneTask({ ...base, ...payload }) : cloneTask(serverTask)
|
|
3000
|
+
savedTaskRef.current = nextSaved
|
|
3001
|
+
ackedPayloadRef.current = payloadKey(nextSaved)
|
|
3002
|
+
failedPayloadRef.current = null
|
|
3003
|
+
setSavedTask(nextSaved)
|
|
3004
|
+
setSaveState('saved')
|
|
3005
|
+
void onChanged?.(taskId)
|
|
3006
|
+
} catch (failure) {
|
|
3007
|
+
if (activeTaskIdRef.current === taskId) {
|
|
3008
|
+
failedPayloadRef.current = payloadKey(current)
|
|
3009
|
+
setSaveState('error')
|
|
3010
|
+
setSaveError(failure?.message ?? zh.errorGeneric)
|
|
3011
|
+
}
|
|
3012
|
+
} finally {
|
|
3013
|
+
saveInFlightRef.current = false
|
|
3014
|
+
const latest = draftRef.current
|
|
3015
|
+
const baseNow = savedTaskRef.current
|
|
3016
|
+
const queued = saveQueuedRef.current
|
|
3017
|
+
saveQueuedRef.current = false
|
|
3018
|
+
if (queued || (latest !== null && baseNow !== null && latest.id === baseNow.id && !samePayload(latest, baseNow) && failedPayloadRef.current === null)) {
|
|
3019
|
+
saveTimerRef.current = setTimeout(() => { void flushRef.current?.() }, 0)
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
})()
|
|
3023
|
+
savePromiseRef.current = savePromise
|
|
3024
|
+
try {
|
|
3025
|
+
await savePromise
|
|
3026
|
+
} finally {
|
|
3027
|
+
if (savePromiseRef.current === savePromise) savePromiseRef.current = null
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
flushRef.current = flush
|
|
3031
|
+
|
|
3032
|
+
function retrySave() {
|
|
3033
|
+
failedPayloadRef.current = null
|
|
3034
|
+
setSaveError('')
|
|
3035
|
+
void flushRef.current?.()
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
async function flushBeforeAction() {
|
|
3039
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
3040
|
+
await flushRef.current?.()
|
|
3041
|
+
const current = draftRef.current
|
|
3042
|
+
const base = savedTaskRef.current
|
|
3043
|
+
if (current === null || base === null) return true
|
|
3044
|
+
if (samePayload(current, base)) return true
|
|
3045
|
+
if (Object.keys(draftIssues(current, meta)).length > 0) return false
|
|
3046
|
+
if (failedPayloadRef.current !== null) return false
|
|
3047
|
+
}
|
|
3048
|
+
return false
|
|
2611
3049
|
}
|
|
2612
3050
|
|
|
2613
3051
|
async function toggleState() {
|
|
@@ -2618,6 +3056,11 @@ function TaskDetailPage(props) {
|
|
|
2618
3056
|
}
|
|
2619
3057
|
|
|
2620
3058
|
async function runNow() {
|
|
3059
|
+
if (!(await flushBeforeAction())) {
|
|
3060
|
+
const current = draftRef.current
|
|
3061
|
+
setError(current !== null && Object.keys(draftIssues(current, meta)).length > 0 ? zh.autosaveInvalid : zh.autosaveFailed)
|
|
3062
|
+
return
|
|
3063
|
+
}
|
|
2621
3064
|
await act('run', async () => {
|
|
2622
3065
|
await api.run(detail.task.id)
|
|
2623
3066
|
}, zh.runSubmitted.replace('{title}', detail.task.title))
|
|
@@ -2632,6 +3075,11 @@ function TaskDetailPage(props) {
|
|
|
2632
3075
|
}
|
|
2633
3076
|
|
|
2634
3077
|
async function copyTask() {
|
|
3078
|
+
if (!(await flushBeforeAction())) {
|
|
3079
|
+
const current = draftRef.current
|
|
3080
|
+
setError(current !== null && Object.keys(draftIssues(current, meta)).length > 0 ? zh.autosaveInvalid : zh.autosaveFailed)
|
|
3081
|
+
return
|
|
3082
|
+
}
|
|
2635
3083
|
await act('copy', async () => {
|
|
2636
3084
|
await api.action(detail.task.id, 'copy')
|
|
2637
3085
|
}, zh.copied.replace('{title}', detail.task.title))
|
|
@@ -2654,17 +3102,6 @@ function TaskDetailPage(props) {
|
|
|
2654
3102
|
onClick: () => { void runNow() },
|
|
2655
3103
|
}, h(IconPlayOutlineRegular, { size: 14 }), zh.runNow)
|
|
2656
3104
|
|
|
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
3105
|
return h('div', null,
|
|
2669
3106
|
h('div', { className: 'capital-automation-hero' },
|
|
2670
3107
|
h(Glyph, { task, large: true }),
|
|
@@ -2696,7 +3133,18 @@ function TaskDetailPage(props) {
|
|
|
2696
3133
|
else if (id === 'copy') void copyTask()
|
|
2697
3134
|
else if (id === 'activate' || id === 'pause') void toggleState()
|
|
2698
3135
|
},
|
|
2699
|
-
onRequestConfirm: (kind, target) => {
|
|
3136
|
+
onRequestConfirm: (kind, target) => {
|
|
3137
|
+
setMenuOpen(false)
|
|
3138
|
+
// A pending autosave must not land after hibernate clears the overrides.
|
|
3139
|
+
if (kind === 'hibernate' || kind === 'delete') {
|
|
3140
|
+
void (async () => {
|
|
3141
|
+
await flushBeforeAction()
|
|
3142
|
+
onRequestConfirm(kind, target)
|
|
3143
|
+
})()
|
|
3144
|
+
return
|
|
3145
|
+
}
|
|
3146
|
+
onRequestConfirm(kind, target)
|
|
3147
|
+
},
|
|
2700
3148
|
canRun,
|
|
2701
3149
|
running,
|
|
2702
3150
|
busy: busy.length > 0,
|
|
@@ -2728,12 +3176,14 @@ function TaskDetailPage(props) {
|
|
|
2728
3176
|
}) : null,
|
|
2729
3177
|
tab === 'config' ? h(ConfigTab, {
|
|
2730
3178
|
draft,
|
|
2731
|
-
setDraft,
|
|
3179
|
+
setDraft: updateDraft,
|
|
2732
3180
|
meta,
|
|
2733
3181
|
busy,
|
|
2734
3182
|
dirty,
|
|
2735
|
-
|
|
2736
|
-
|
|
3183
|
+
saveState,
|
|
3184
|
+
saveError,
|
|
3185
|
+
retry: retrySave,
|
|
3186
|
+
onFlush: () => { void flushRef.current?.() },
|
|
2737
3187
|
disabled: !storageReadyNow,
|
|
2738
3188
|
}) : null,
|
|
2739
3189
|
tab === 'runs' ? h('section', { className: 'capital-automation-card capital-automation-panel' },
|
|
@@ -2756,15 +3206,15 @@ function TaskDetailPage(props) {
|
|
|
2756
3206
|
|
|
2757
3207
|
function NewTaskModal(props) {
|
|
2758
3208
|
const { open, onClose, api, onCreated, meta } = props
|
|
2759
|
-
const [draft, setDraft] = useState(defaultDraft)
|
|
3209
|
+
const [draft, setDraft] = useState(() => defaultDraft(meta))
|
|
2760
3210
|
const [busy, setBusy] = useState(false)
|
|
2761
3211
|
const [error, setError] = useState('')
|
|
2762
|
-
const { issues, errorFor, touch, touchAll } = useDraftIssues(draft, open)
|
|
3212
|
+
const { issues, errorFor, touch, touchAll } = useDraftIssues(draft, open, meta)
|
|
2763
3213
|
const titleError = errorFor('title')
|
|
2764
3214
|
const instructionError = errorFor('instruction')
|
|
2765
3215
|
useEffect(() => {
|
|
2766
3216
|
if (!open) return
|
|
2767
|
-
setDraft(defaultDraft())
|
|
3217
|
+
setDraft(defaultDraft(meta))
|
|
2768
3218
|
setBusy(false)
|
|
2769
3219
|
setError('')
|
|
2770
3220
|
}, [open])
|
|
@@ -2774,8 +3224,8 @@ function NewTaskModal(props) {
|
|
|
2774
3224
|
}))
|
|
2775
3225
|
const timeoutRange = limitValue(meta, 'runTimeoutMinutes', [5, 120])
|
|
2776
3226
|
const idleRange = limitValue(meta, 'idleTimeoutMinutes', [1, 30])
|
|
2777
|
-
const stepsRange = limitValue(meta, 'maxSteps', [
|
|
2778
|
-
const toolsRange = limitValue(meta, 'maxToolCalls', [
|
|
3227
|
+
const stepsRange = limitValue(meta, 'maxSteps', [100, 2000])
|
|
3228
|
+
const toolsRange = limitValue(meta, 'maxToolCalls', [100, 5000])
|
|
2779
3229
|
async function create() {
|
|
2780
3230
|
if (Object.keys(issues).length > 0) {
|
|
2781
3231
|
touchAll()
|
|
@@ -2806,7 +3256,7 @@ function NewTaskModal(props) {
|
|
|
2806
3256
|
},
|
|
2807
3257
|
h('div', { className: 'capital-automation-wizard' },
|
|
2808
3258
|
h('div', { className: 'capital-automation-modalheadline', 'data-invalid': titleError ? 'true' : undefined },
|
|
2809
|
-
h(
|
|
3259
|
+
h(IconPickerTrigger, { task: draft, size: 42, onChange: (icon) => set({ icon }) }),
|
|
2810
3260
|
h('input', {
|
|
2811
3261
|
className: 'capital-automation-modaltitleinput',
|
|
2812
3262
|
value: draft.title,
|
|
@@ -2818,7 +3268,6 @@ function NewTaskModal(props) {
|
|
|
2818
3268
|
onBlur: () => touch('title'),
|
|
2819
3269
|
})),
|
|
2820
3270
|
titleError ? h('span', { className: 'capital-automation-fielderror', role: 'alert' }, titleError) : null,
|
|
2821
|
-
h('p', { className: 'capital-automation-hint' }, zh.newTaskHint),
|
|
2822
3271
|
h(Field, {
|
|
2823
3272
|
label: zh.descriptionField,
|
|
2824
3273
|
note: zh.charCount.replace('{n}', String(Array.from(draft.description ?? '').length)).replace('{max}', String(DESCRIPTION_MAX_LENGTH)),
|
|
@@ -2839,6 +3288,7 @@ function NewTaskModal(props) {
|
|
|
2839
3288
|
h(PlainTextEditor, {
|
|
2840
3289
|
className: 'capital-automation-texteditor',
|
|
2841
3290
|
value: draft.instruction,
|
|
3291
|
+
maxLength: INSTRUCTION_MAX_LENGTH,
|
|
2842
3292
|
placeholder: zh.instructionPlaceholder,
|
|
2843
3293
|
ariaLabel: zh.instructionField,
|
|
2844
3294
|
invalid: instructionError ? true : undefined,
|
|
@@ -2885,8 +3335,8 @@ function NewTaskModal(props) {
|
|
|
2885
3335
|
h('span', { className: 'capital-automation-formlabel' }, zh.notifyField),
|
|
2886
3336
|
h('div', { className: 'capital-automation-switchlist' },
|
|
2887
3337
|
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 } })) })),
|
|
3338
|
+
h(SwitchRow, { label: zh.notifyFailure, checked: draft.notifications.onFailure, disabled: draft.notifications.inApp !== true, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, onFailure: value } })) }),
|
|
3339
|
+
h(SwitchRow, { label: zh.notifySuccess, checked: draft.notifications.onSuccess, disabled: draft.notifications.inApp !== true, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, onSuccess: value } })) })),
|
|
2890
3340
|
h('p', { className: 'capital-automation-hint' }, zh.notifyHint)),
|
|
2891
3341
|
error.length > 0 ? h('p', { className: 'capital-automation-error', role: 'alert' }, error) : null))
|
|
2892
3342
|
}
|
|
@@ -3201,7 +3651,9 @@ function AutomationPage(props) {
|
|
|
3201
3651
|
|
|
3202
3652
|
const schedulerActive = meta?.scheduler?.state === 'active'
|
|
3203
3653
|
const storageReady = meta?.storage?.state !== 'unavailable'
|
|
3204
|
-
const templates = tasks
|
|
3654
|
+
const templates = tasks
|
|
3655
|
+
.filter((task) => task.origin === 'builtin' && task.state === 'dormant')
|
|
3656
|
+
.sort(compareTemplates)
|
|
3205
3657
|
const selectedTask = tasks.find((task) => task.id === selectedId)
|
|
3206
3658
|
const detailOpen = tab === 'tasks' && selectedId !== null
|
|
3207
3659
|
// `/meta` arrives in the same batch as `/tasks`; until then the empty state would be a lie.
|
|
@@ -3270,7 +3722,7 @@ function AutomationPage(props) {
|
|
|
3270
3722
|
onGoTasks: () => switchTab('tasks'),
|
|
3271
3723
|
onNewTask: () => setWizard(true),
|
|
3272
3724
|
busy: confirmBusy,
|
|
3273
|
-
}))),
|
|
3725
|
+
}), detailOpen ? null : h(SystemStatus, { meta }))),
|
|
3274
3726
|
h(NewTaskModal, {
|
|
3275
3727
|
open: wizard,
|
|
3276
3728
|
onClose: () => setWizard(false),
|