@v587d/capital-generation 2.15.1 → 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.
Files changed (50) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +7 -7
  3. package/automation/client.js +47 -17
  4. package/automation/client.src.cjs +294 -83
  5. package/automation/contract.js +4 -0
  6. package/automation/defaults.js +2 -0
  7. package/automation/index.js +11 -7
  8. package/automation/package.json +2 -1
  9. package/automation/route.js +8 -1
  10. package/automation/runner.js +28 -6
  11. package/automation/service.js +89 -0
  12. package/automation/settings.js +6 -0
  13. package/automation/workspaces.js +113 -13
  14. package/chart-ui/package.json +1 -1
  15. package/config/client.js +12 -5
  16. package/config/client.src.cjs +223 -0
  17. package/config/index.js +2 -0
  18. package/config/package.json +1 -1
  19. package/lib/agents/root-tool-policy.d.ts +3 -3
  20. package/lib/agents/root-tool-policy.js +4 -2
  21. package/lib/data-collector/dataset-tools.js +22 -0
  22. package/lib/data-collector/describe.js +1 -0
  23. package/lib/data-collector/hub.js +5 -0
  24. package/lib/data-collector/store.d.ts +5 -0
  25. package/lib/data-collector/store.js +17 -0
  26. package/lib/data-collector/time-axis.d.ts +15 -0
  27. package/lib/data-collector/time-axis.js +73 -0
  28. package/lib/data-collector/tools.d.ts +1 -1
  29. package/lib/data-collector/tools.js +12 -1
  30. package/lib/index.d.ts +5 -0
  31. package/lib/index.js +2 -0
  32. package/lib/sources/tencent-http.js +7 -2
  33. package/lib/sources/tencent-public-core.d.ts +2 -0
  34. package/lib/sources/tencent-public-core.js +5 -0
  35. package/lib/sources/wind-mcp.js +3 -3
  36. package/lib/sources-entry.d.ts +1 -1
  37. package/lib/web-retriever/local-fetch.d.ts +1 -1
  38. package/lib/web-retriever/local-fetch.js +1 -1
  39. package/lib/web-retriever/sources.d.ts +58 -0
  40. package/lib/web-retriever/sources.js +290 -0
  41. package/lib/web-retriever/tools.js +199 -8
  42. package/lib/web-retriever/wind-client.d.ts +1 -1
  43. package/lib/web-retriever/wind-client.js +1 -1
  44. package/package.json +1 -1
  45. package/preset/capital-generation/agent.patch.yml +20 -19
  46. package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +9 -2
  47. package/preset/capital-generation/skills/capital-orchestration/SKILL.md +2 -1
  48. package/preset/capital-generation/skills/capital-web-evidence/SKILL.md +18 -13
  49. package/preset/capital-generation/skills/capital-web-protocol/SKILL.md +30 -15
  50. package/watchlist/package.json +1 -1
@@ -72,7 +72,9 @@ const TIMEZONES = [
72
72
  'America/Sao_Paulo',
73
73
  'Australia/Sydney',
74
74
  ]
75
- const HISTORY_DAYS = 30
75
+ const HISTORY_DAYS = 371
76
+ const HEATMAP_WEEKS = 53
77
+ const WEEKDAY_SHORT = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
76
78
  const DETAIL_RECENT_RUN_LIMIT = 3
77
79
  const TITLE_MAX_LENGTH = 12
78
80
  const DESCRIPTION_MAX_LENGTH = 100
@@ -249,11 +251,19 @@ const zh = {
249
251
  techInfo: '技术信息',
250
252
  profileKey: 'profileKey',
251
253
  runHistory: '运行历史',
252
- last30Days: '最近 30 天',
254
+ last12Months: '最近 12 个月',
253
255
  runCount: '{n} 次运行',
254
256
  successCount: '{n} 成功',
255
257
  failureCount: '{n} 失败',
256
- historyEmpty: '最近 30 天还没有运行记录。',
258
+ activeCount: '{n} 进行中',
259
+ historyEmpty: '最近 12 个月还没有运行记录。',
260
+ heatLess: '少',
261
+ heatMore: '多',
262
+ heatFailed: '有失败',
263
+ heatActive: '进行中',
264
+ heatNoRuns: '没有运行',
265
+ heatFuture: '尚未到来',
266
+ heatSummary: '最近 12 个月共 {total} 次运行:{success} 成功,{failure} 失败。',
257
267
  historyViewAll: '查看全部运行',
258
268
  runFilterAll: '全部',
259
269
  runFilterActive: '进行中',
@@ -370,8 +380,8 @@ function installStyles() {
370
380
  const style = document.createElement('style')
371
381
  style.setAttribute(`data-${STYLE_TAG}`, 'true')
372
382
  style.textContent = `
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); }
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; }
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; }
375
385
  .capital-automation-page { display: flex; flex-direction: column; min-height: 0; height: 100%; color: var(--dsw-alias-label-primary); }
376
386
  .capital-automation-head { flex: none; padding: 18px 0 0; }
377
387
  .capital-automation-headinner { box-sizing: border-box; width: min(1080px, 100%); margin: 0 auto; padding: 0 clamp(20px, 3vw, 36px); }
@@ -411,7 +421,7 @@ function installStyles() {
411
421
  .capital-automation-scroll { flex: 1 1 auto; min-height: 0; overflow: auto; }
412
422
  .capital-automation-container { box-sizing: border-box; width: min(1080px, 100%); margin: 0 auto; padding: 20px clamp(20px, 3vw, 36px) 72px; }
413
423
  .capital-automation-card { box-sizing: border-box; border: .5px solid var(--ca-line); border-radius: 16px; background: var(--ca-card); }
414
- .capital-automation-history { padding: 18px 20px 14px; }
424
+ .capital-automation-history { position: relative; padding: 18px 20px 14px; }
415
425
  .capital-automation-historyhead { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: 10px 18px; }
416
426
  .capital-automation-historytitle { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; }
417
427
  .capital-automation-historytitle h2 { margin: 0; font-size: 14px; line-height: 22px; font-weight: 600; }
@@ -420,18 +430,45 @@ function installStyles() {
420
430
  .capital-automation-historystat { display: inline-flex; align-items: center; gap: 5px; font-variant-numeric: tabular-nums; }
421
431
  .capital-automation-historystat[data-tone="ok"] { color: var(--ca-success-text); }
422
432
  .capital-automation-historystat[data-tone="error"] { color: var(--ca-error-text); }
433
+ .capital-automation-historystat[data-tone="active"] { color: var(--ca-business-text); }
423
434
  .capital-automation-statusdot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }
424
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; }
425
436
  .capital-automation-historylink:hover { background: var(--ca-hover); color: var(--dsw-alias-label-primary); }
426
- .capital-automation-timeline { display: grid; grid-template-columns: repeat(${HISTORY_DAYS}, minmax(0, 1fr)); align-items: end; gap: 2px; height: 28px; margin-top: 18px; border-bottom: 1px dashed var(--ca-line); }
427
- .capital-automation-timeline-day { position: relative; display: flex; align-items: flex-end; justify-content: center; height: 100%; padding-bottom: 3px; }
428
- .capital-automation-timeline-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--ca-caption); box-shadow: 0 0 0 2px var(--ca-card); }
429
- .capital-automation-timeline-dot[data-tone="ok"] { background: var(--ca-success); }
430
- .capital-automation-timeline-dot[data-tone="error"] { background: var(--ca-error); }
431
- .capital-automation-timeline-dot[data-tone="active"] { background: var(--ca-business); }
432
- .capital-automation-timeline-dot[data-tone="warn"] { background: var(--ca-warn); }
433
- .capital-automation-timelinelabels { display: flex; justify-content: space-between; margin-top: 7px; color: var(--ca-caption); font-size: 11px; line-height: 16px; }
434
- .capital-automation-historyempty { margin-top: 12px; text-align: center; color: var(--ca-muted); font-size: 12px; line-height: 18px; }
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; } }
435
472
  .capital-automation-section { margin-top: 26px; }
436
473
  .capital-automation-sectionhead { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; margin-bottom: 10px; }
437
474
  .capital-automation-sectionhead h2 { margin: 0; font-size: 14px; line-height: 22px; font-weight: 600; }
@@ -516,8 +553,11 @@ function installStyles() {
516
553
  .capital-automation-loading { display: flex; flex-direction: column; pointer-events: none; }
517
554
  .capital-automation-skeletonlabel { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
518
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; }
519
- .capital-automation-skeletontimeline { display: flex; align-items: flex-end; gap: 2px; height: 28px; margin-top: 18px; border-bottom: 1px dashed var(--ca-line); }
520
- .capital-automation-skeletontimeline > .capital-automation-skeleton { flex: 1 1 0; min-width: 0; }
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; }
521
561
  .capital-automation-skeletonstack { display: flex; flex: 1 1 auto; flex-direction: column; gap: 6px; min-width: 0; }
522
562
  .capital-automation-skeletonrow .capital-automation-trowmain { min-height: 57px; cursor: default; }
523
563
  .capital-automation-skeletonrow .capital-automation-cell[data-cell] { display: flex; flex-direction: column; gap: 5px; }
@@ -843,6 +883,7 @@ function createApi() {
843
883
  run: (id) => request(`/tasks/${encodeURIComponent(id)}/run`, { method: 'POST' }),
844
884
  cancel: (id) => request(`/runs/${encodeURIComponent(id)}/cancel`, { method: 'POST' }),
845
885
  runs: (query = '') => request(`/runs${query}`),
886
+ history: (query = '') => request(`/runs/history${query}`),
846
887
  notifications: () => request('/notifications'),
847
888
  markRead: (body) => request('/notifications/read', { method: 'POST', body }),
848
889
  }
@@ -1077,10 +1118,6 @@ function formatRelative(value) {
1077
1118
  return ''
1078
1119
  }
1079
1120
 
1080
- function formatRangeLabel(date) {
1081
- return new Intl.DateTimeFormat('zh-CN', { month: 'short', day: 'numeric' }).format(date)
1082
- }
1083
-
1084
1121
  function weekdayText(trigger) {
1085
1122
  return (trigger.weekdays ?? []).slice().sort((left, right) => left - right)
1086
1123
  .map((day) => WEEKDAYS.find((item) => item.value === day)?.label ?? '')
@@ -1335,42 +1372,121 @@ function cloneTask(task) {
1335
1372
  }
1336
1373
  }
1337
1374
 
1338
- function groupRunCounts(runs) {
1339
- const since = new Date()
1340
- since.setHours(0, 0, 0, 0)
1341
- since.setDate(since.getDate() - (HISTORY_DAYS - 1))
1342
- const days = []
1343
- const byKey = new Map()
1344
- for (let index = 0; index < HISTORY_DAYS; index += 1) {
1345
- const date = new Date(since)
1346
- date.setDate(since.getDate() + index)
1347
- const key = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`
1348
- const bucket = { date, key, runs: [] }
1349
- days.push(bucket)
1350
- byKey.set(key, bucket)
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)
1351
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
1352
1425
  let success = 0
1353
1426
  let failure = 0
1354
- let total = 0
1355
- for (const run of runs) {
1356
- const date = new Date(run.queuedAt)
1357
- if (Number.isNaN(date.getTime()) || date < since) continue
1358
- if (run.status === 'succeeded') success += 1
1359
- if (isFailureStatus(run.status)) failure += 1
1360
- total += 1
1361
- const key = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`
1362
- const bucket = byKey.get(key)
1363
- if (bucket !== undefined) bucket.runs.push(run)
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)),
1364
1465
  }
1365
- return { days, success, failure, total }
1366
1466
  }
1367
1467
 
1368
- function dayTone(bucket) {
1369
- if (bucket.runs.some((run) => isActiveRun(run))) return 'active'
1370
- if (bucket.runs.some((run) => isFailureStatus(run.status))) return 'error'
1371
- if (bucket.runs.some((run) => run.status === 'succeeded')) return 'ok'
1372
- if (bucket.runs.length > 0) return 'warn'
1373
- return null
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(' · ')
1374
1490
  }
1375
1491
 
1376
1492
  function eventTone(kind) {
@@ -1449,13 +1565,57 @@ function limitValue(meta, key, fallback) {
1449
1565
  }
1450
1566
 
1451
1567
  function RunHistoryCard(props) {
1452
- const { runs, onViewAll, showLink = true } = props
1453
- const { days, success, failure, total } = groupRunCounts(runs)
1454
- return h('section', { className: 'capital-automation-card capital-automation-history', 'aria-label': zh.runHistory },
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 },
1455
1615
  h('div', { className: 'capital-automation-historyhead' },
1456
1616
  h('div', { className: 'capital-automation-historytitle' },
1457
1617
  h('h2', null, zh.runHistory),
1458
- h('span', null, zh.last30Days)),
1618
+ h('span', null, zh.last12Months)),
1459
1619
  h('div', { className: 'capital-automation-historystats' },
1460
1620
  h('span', { className: 'capital-automation-historystat', 'data-tone': 'ok' },
1461
1621
  h('span', { className: 'capital-automation-statusdot' }),
@@ -1463,21 +1623,56 @@ function RunHistoryCard(props) {
1463
1623
  h('span', { className: 'capital-automation-historystat', 'data-tone': 'error' },
1464
1624
  h('span', { className: 'capital-automation-statusdot' }),
1465
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,
1466
1631
  showLink && onViewAll !== undefined
1467
1632
  ? h('button', { type: 'button', className: 'capital-automation-historylink', onClick: onViewAll },
1468
1633
  zh.runCount.replace('{n}', String(total)),
1469
1634
  h(IconChevronRightOutlineRegular, { size: 13 }))
1470
1635
  : h('span', { className: 'capital-automation-historystat' }, zh.runCount.replace('{n}', String(total))))),
1471
- h('div', { className: 'capital-automation-timeline', 'aria-hidden': 'true' },
1472
- days.map((bucket) => {
1473
- const tone = dayTone(bucket)
1474
- return h('div', { key: bucket.key, className: 'capital-automation-timeline-day', title: `${formatRangeLabel(bucket.date)}:${bucket.runs.length} 次运行` },
1475
- tone === null ? null : h('span', { className: 'capital-automation-timeline-dot', 'data-tone': tone }))
1476
- })),
1477
- h('div', { className: 'capital-automation-timelinelabels' },
1478
- h('span', null, formatRangeLabel(days[0].date)),
1479
- h('span', null, formatRangeLabel(days[days.length - 1].date))),
1480
- total === 0 ? h('p', { className: 'capital-automation-historyempty' }, zh.historyEmpty) : null)
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))))
1481
1676
  }
1482
1677
 
1483
1678
  function RunStatus(props) {
@@ -1884,15 +2079,21 @@ function SkeletonHistory() {
1884
2079
  h(SkeletonBlock, { width: 64, height: 12 }),
1885
2080
  h(SkeletonBlock, { width: 64, height: 12 }),
1886
2081
  h(SkeletonBlock, { width: 56, height: 12 }))),
1887
- h('div', { className: 'capital-automation-skeletontimeline' },
1888
- Array.from({ length: HISTORY_DAYS }, (_, index) => h(SkeletonBlock, {
1889
- key: index,
1890
- height: 6 + ((index * 11) % 22),
1891
- radius: 2,
1892
- }))),
1893
- h('div', { className: 'capital-automation-timelinelabels' },
1894
- h(SkeletonBlock, { width: 34, height: 9 }),
1895
- h(SkeletonBlock, { width: 34, height: 9 })))
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 })))
1896
2097
  }
1897
2098
 
1898
2099
  function SkeletonSectionHead(props) {
@@ -1980,8 +2181,7 @@ function LoadingSkeleton(props) {
1980
2181
 
1981
2182
  function TaskDashboardSkeleton() {
1982
2183
  return h(LoadingSkeleton, null,
1983
- h(SkeletonHistory),
1984
- h('section', { className: 'capital-automation-section' },
2184
+ h('section', { className: 'capital-automation-section', style: { marginTop: 0 } },
1985
2185
  h(SkeletonSectionHead),
1986
2186
  h(SkeletonTaskTable, { rows: 4 })),
1987
2187
  h('section', { className: 'capital-automation-section' },
@@ -2001,7 +2201,7 @@ function RunsSkeleton() {
2001
2201
  }
2002
2202
 
2003
2203
  function TaskDashboard(props) {
2004
- const { tasks, templates, runs, onOpenTask, onNewTask, onEnableTemplate, onAction, onRequestConfirm, onEdit, onGoRuns, onRequestCancelRun, schedulerActive, storageReady, busyTaskId } = props
2204
+ const { tasks, templates, runs, onOpenTask, onNewTask, onEnableTemplate, onAction, onRequestConfirm, onEdit, onRequestCancelRun, schedulerActive, storageReady, busyTaskId } = props
2005
2205
  const [filter, setFilter] = useState('all')
2006
2206
  const [query, setQuery] = useState('')
2007
2207
  const runningTasks = tasks.filter((task) => isActiveRun(task.lastRun))
@@ -2026,8 +2226,7 @@ function TaskDashboard(props) {
2026
2226
  }
2027
2227
  const filters = ['all', 'custom', 'news', 'market', 'review', 'research', 'portfolio', 'monitor']
2028
2228
  return h('div', null,
2029
- h(RunHistoryCard, { runs, onViewAll: onGoRuns }),
2030
- h('section', { className: 'capital-automation-section' },
2229
+ h('section', { className: 'capital-automation-section', style: { marginTop: 0 } },
2031
2230
  h('div', { className: 'capital-automation-sectionhead' },
2032
2231
  h('h2', null, zh.taskSection),
2033
2232
  h('span', null, zh.taskSectionHint),
@@ -2520,7 +2719,7 @@ function RunFilters(props) {
2520
2719
  }
2521
2720
 
2522
2721
  function RunsPage(props) {
2523
- const { runs, tasks, onCancel, onOpenSession, onOpenTask, onGoTasks, onNewTask, busy } = props
2722
+ const { runs, history, tasks, onCancel, onOpenSession, onOpenTask, onGoTasks, onNewTask, busy } = props
2524
2723
  const [filter, setFilter] = useState('all')
2525
2724
  const counts = {
2526
2725
  all: runs.length,
@@ -2535,7 +2734,7 @@ function RunsPage(props) {
2535
2734
  return true
2536
2735
  })
2537
2736
  return h('div', null,
2538
- h(RunHistoryCard, { runs, showLink: false }),
2737
+ h(RunHistoryCard, { history, showLink: false }),
2539
2738
  h('section', { className: 'capital-automation-section' },
2540
2739
  h('div', { className: 'capital-automation-sectionhead' },
2541
2740
  h('h2', null, zh.runsTitle),
@@ -3401,11 +3600,21 @@ function InboxModal(props) {
3401
3600
  }))))
3402
3601
  }
3403
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
+
3404
3612
  function AutomationPage(props) {
3405
3613
  const { api, openSession } = props
3406
3614
  const [meta, setMeta] = useState(null)
3407
3615
  const [tasks, setTasks] = useState([])
3408
3616
  const [runs, setRuns] = useState([])
3617
+ const [history, setHistory] = useState(null)
3409
3618
  const [notifications, setNotifications] = useState([])
3410
3619
  const [unread, setUnread] = useState(0)
3411
3620
  const [selectedId, setSelectedId] = useState(null)
@@ -3441,10 +3650,11 @@ function AutomationPage(props) {
3441
3650
  refreshSequenceRef.current = requestSequence
3442
3651
  const request = (async () => {
3443
3652
  try {
3444
- const [nextMeta, nextTasks, nextRuns, nextNotifications] = await Promise.all([
3653
+ const [nextMeta, nextTasks, nextRuns, nextHistory, nextNotifications] = await Promise.all([
3445
3654
  api.meta(),
3446
3655
  api.tasks(),
3447
3656
  api.runs('?limit=200'),
3657
+ api.history(`?days=${HISTORY_DAYS}&tz=${encodeURIComponent(browserTimeZone())}`),
3448
3658
  api.notifications(),
3449
3659
  ])
3450
3660
  if (!mountedRef.current || requestSequence !== refreshSequenceRef.current) return
@@ -3453,6 +3663,7 @@ function AutomationPage(props) {
3453
3663
  tasksRef.current = nextTaskList
3454
3664
  setTasks(nextTaskList)
3455
3665
  setRuns(Array.isArray(nextRuns.runs) ? nextRuns.runs : [])
3666
+ setHistory(nextHistory ?? null)
3456
3667
  setNotifications(Array.isArray(nextNotifications.notifications) ? nextNotifications.notifications : [])
3457
3668
  setUnread(typeof nextNotifications.unread === 'number' ? nextNotifications.unread : 0)
3458
3669
  setLoadError('')
@@ -3705,7 +3916,6 @@ function AutomationPage(props) {
3705
3916
  onAction: (task, action) => { void performTaskAction(task, action) },
3706
3917
  onRequestConfirm: requestConfirm,
3707
3918
  onEdit: (id) => selectTask(id, 'config'),
3708
- onGoRuns: () => switchTab('runs'),
3709
3919
  onRequestCancelRun: (task, run) => requestConfirm('cancel-run', task, run),
3710
3920
  schedulerActive,
3711
3921
  storageReady,
@@ -3715,6 +3925,7 @@ function AutomationPage(props) {
3715
3925
  ? h(RunsSkeleton)
3716
3926
  : h(RunsPage, {
3717
3927
  runs,
3928
+ history,
3718
3929
  tasks,
3719
3930
  onCancel: (run) => requestConfirm('cancel-run', { id: run.taskId, title: run.taskTitle }, run),
3720
3931
  onOpenSession: openSession,
@@ -0,0 +1,4 @@
1
+ /** Host routes shared by the automation dispatcher and the settings card's reader. */
2
+ export const AUTOMATION_ROUTE = '/capital-automation'
3
+ export const WORKSPACES_SUFFIX = '/workspaces'
4
+ export const WORKSPACES_ROUTE = `${AUTOMATION_ROUTE}${WORKSPACES_SUFFIX}`
@@ -50,6 +50,7 @@ export const DEFAULT_AUTOMATION_SETTINGS = {
50
50
  maxSteps: DEFAULT_MAX_STEPS,
51
51
  maxToolCalls: DEFAULT_MAX_TOOL_CALLS,
52
52
  inAppNotifications: true,
53
+ defaultWorkspaceId: '',
53
54
  }
54
55
 
55
56
  export const RUN_STATUSES = [
@@ -68,6 +69,7 @@ export const RUN_STATUSES = [
68
69
  ]
69
70
 
70
71
  export const ACTIVE_RUN_STATUSES = ['queued', 'starting', 'running']
72
+ export const FAILURE_RUN_STATUSES = ['failed', 'timed_out', 'blocked_requires_user', 'unknown_after_restart']
71
73
  export const TERMINAL_RUN_STATUSES = RUN_STATUSES.filter((status) => !ACTIVE_RUN_STATUSES.includes(status))
72
74
 
73
75
  // A finished turn can be settled from its durable checkpoint; `unknown_after_restart`
@@ -4,6 +4,7 @@
4
4
  * `ui-schedule` row and its `schedules` identities are untouched.
5
5
  */
6
6
  import { readFileSync } from 'node:fs'
7
+ import { homedir } from 'node:os'
7
8
  import { join } from 'node:path'
8
9
 
9
10
  import { createRunRunner, buildRunPrompt, sessionTitleOf } from './runner.js'
@@ -14,7 +15,7 @@ import { createAutomationRouteHandler } from './route.js'
14
15
  import { resolveAutomationSettings } from './settings.js'
15
16
  import { installAutomationPolicy } from './policy.js'
16
17
  import { createCalendarSource } from './calendar-source.js'
17
- import { createWorkspaceCatalog } from './workspaces.js'
18
+ import { createWorkspaceCatalog, defaultWorkspacePath } from './workspaces.js'
18
19
  import { sanitizeMessage } from './redact.js'
19
20
 
20
21
  export const name = 'capital-automation'
@@ -28,10 +29,6 @@ function versionOf() {
28
29
  }
29
30
  }
30
31
 
31
- function safeSegment(value) {
32
- return String(value).replace(/[^A-Za-z0-9._-]+/g, '_').slice(0, 120)
33
- }
34
-
35
32
  /**
36
33
  * Build the runtime without Cordis services. Exported for tests and for the desktop carrier
37
34
  * that supplies its own HTTP/auth plane.
@@ -42,7 +39,7 @@ export function createAutomationRuntime(options) {
42
39
  const rawProfileName = String(profile.name)
43
40
  const displayName = rawProfileName.slice(0, 200)
44
41
  const home = profile.home ?? profile.dir ?? process.cwd()
45
- const runDir = profile.runDir ?? join(home, 'automation-workspace', safeSegment(displayName))
42
+ const runDir = profile.runDir ?? defaultWorkspacePath(profile)
46
43
  const ownerDir = profile.ownerDir ?? join(home, 'storages', 'capital-automation-locks')
47
44
  const calendar = options.calendar ?? createCalendarSource({
48
45
  cacheDir: join(home, 'storages', 'capital-automation-calendar'),
@@ -50,7 +47,14 @@ export function createAutomationRuntime(options) {
50
47
  now,
51
48
  })
52
49
  const policyContext = options.policyContext ?? ctx
53
- const workspaces = createWorkspaceCatalog({ ctx, displayName, runDir, logger })
50
+ const workspaces = createWorkspaceCatalog({
51
+ ctx,
52
+ displayName,
53
+ runDir,
54
+ displayHome: homedir(),
55
+ getDefaultWorkspaceId: () => getSettings()?.defaultWorkspaceId,
56
+ logger,
57
+ })
54
58
  const store = createAutomationStore({
55
59
  openDomain: () => storageDomain.open(domainSpec),
56
60
  profile: { name: rawProfileName },