@v587d/capital-generation 2.15.0 → 2.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +171 -0
  2. package/README.md +7 -12
  3. package/THIRD_PARTY_NOTICES.md +1 -0
  4. package/automation/TABLER-ICONS-LICENSE +21 -0
  5. package/automation/builtins.js +237 -10
  6. package/automation/client.js +79 -17
  7. package/automation/client.src.cjs +873 -210
  8. package/automation/contract.js +4 -0
  9. package/automation/defaults.js +6 -4
  10. package/automation/index.js +12 -7
  11. package/automation/package.json +5 -2
  12. package/automation/route.js +9 -2
  13. package/automation/runner.js +33 -6
  14. package/automation/scheduler.js +34 -29
  15. package/automation/schema.js +39 -5
  16. package/automation/service.js +102 -10
  17. package/automation/settings.js +6 -0
  18. package/automation/store.js +64 -21
  19. package/automation/task-icons.js +75 -0
  20. package/automation/workspaces.js +113 -13
  21. package/chart-ui/client.js +2 -2
  22. package/chart-ui/package.json +1 -1
  23. package/config/client.js +19 -5
  24. package/config/client.src.cjs +365 -44
  25. package/config/index.js +7 -5
  26. package/config/package.json +1 -1
  27. package/lib/agents/root-tool-policy.d.ts +3 -3
  28. package/lib/agents/root-tool-policy.js +4 -2
  29. package/lib/chart/runtime.d.ts +1 -1
  30. package/lib/chart/runtime.js +95 -0
  31. package/lib/chart/series.d.ts +4 -4
  32. package/lib/chart/series.js +44 -10
  33. package/lib/data-collector/dataset-tools.js +22 -0
  34. package/lib/data-collector/describe.js +1 -0
  35. package/lib/data-collector/hub.js +5 -0
  36. package/lib/data-collector/store.d.ts +5 -0
  37. package/lib/data-collector/store.js +17 -0
  38. package/lib/data-collector/time-axis.d.ts +15 -0
  39. package/lib/data-collector/time-axis.js +73 -0
  40. package/lib/data-collector/tools.d.ts +1 -1
  41. package/lib/data-collector/tools.js +12 -1
  42. package/lib/index.d.ts +5 -0
  43. package/lib/index.js +7 -5
  44. package/lib/sources/tencent-http.js +7 -2
  45. package/lib/sources/tencent-public-core.d.ts +2 -0
  46. package/lib/sources/tencent-public-core.js +5 -0
  47. package/lib/sources/wind-mcp.js +3 -3
  48. package/lib/sources-entry.d.ts +1 -1
  49. package/lib/web-retriever/local-fetch.d.ts +1 -1
  50. package/lib/web-retriever/local-fetch.js +1 -1
  51. package/lib/web-retriever/sources.d.ts +58 -0
  52. package/lib/web-retriever/sources.js +290 -0
  53. package/lib/web-retriever/tools.js +199 -8
  54. package/lib/web-retriever/wind-client.d.ts +1 -1
  55. package/lib/web-retriever/wind-client.js +1 -1
  56. package/package.json +1 -1
  57. package/preset/capital-generation/agent.patch.yml +20 -19
  58. package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +6 -3
  59. package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +9 -2
  60. package/preset/capital-generation/skills/capital-orchestration/SKILL.md +2 -1
  61. package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +2 -0
  62. package/preset/capital-generation/skills/capital-web-evidence/SKILL.md +18 -13
  63. package/preset/capital-generation/skills/capital-web-protocol/SKILL.md +30 -15
  64. package/watchlist/package.json +1 -1
@@ -4,7 +4,9 @@
4
4
  * profile filter; the wire body never selects a profile.
5
5
  */
6
6
  import {
7
+ ACTIVE_RUN_STATUSES,
7
8
  DEFAULT_AUTOMATION_SETTINGS,
9
+ FAILURE_RUN_STATUSES,
8
10
  MAX_IDLE_TIMEOUT_MINUTES,
9
11
  MAX_MAX_STEPS,
10
12
  MAX_MAX_TOOL_CALLS,
@@ -16,19 +18,61 @@ import {
16
18
  } from './defaults.js'
17
19
  import {
18
20
  NotificationsReadSchema,
21
+ TaskCategorySchema,
19
22
  TaskCreateSchema,
20
23
  TaskPatchSchema,
21
24
  parseStrict,
22
25
  } from './schema.js'
23
26
  import { sanitizeMessage } from './redact.js'
27
+ import { addDays, localPartsAt } from './time.js'
24
28
 
25
29
  const MAX_RUN_PAGE_LIMIT = 2_000
26
30
  const RUN_CURSOR_PATTERN = /^[A-Za-z0-9_-]{1,120}$/
31
+ const HISTORY_DAYS = 371
32
+ const ACTIVE_STATUSES = new Set(ACTIVE_RUN_STATUSES)
33
+ const FAILURE_STATUSES = new Set(FAILURE_RUN_STATUSES)
27
34
 
28
35
  function invalidQuery(message) {
29
36
  return new AutomationError('invalid_query', message, 400)
30
37
  }
31
38
 
39
+ function historyDaysOf(value) {
40
+ if (value === undefined) return HISTORY_DAYS
41
+ if (typeof value !== 'string' && typeof value !== 'number') throw invalidQuery('运行历史 days 参数无效。')
42
+ const text = String(value)
43
+ if (!/^\d{1,3}$/.test(text)) throw invalidQuery(`运行历史 days 必须是 1–${HISTORY_DAYS} 的整数。`)
44
+ const days = Number(text)
45
+ if (!Number.isSafeInteger(days) || days < 1 || days > HISTORY_DAYS) {
46
+ throw invalidQuery(`运行历史 days 必须是 1–${HISTORY_DAYS} 的整数。`)
47
+ }
48
+ return days
49
+ }
50
+
51
+ function historyTimeZoneOf(value) {
52
+ if (value === undefined) {
53
+ try {
54
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
55
+ } catch {
56
+ return 'UTC'
57
+ }
58
+ }
59
+ if (typeof value !== 'string' || value.length === 0 || value.length > 64) {
60
+ throw invalidQuery('运行历史 tz 参数无效。')
61
+ }
62
+ try {
63
+ new Intl.DateTimeFormat('en-US', { timeZone: value })
64
+ } catch {
65
+ throw invalidQuery('运行历史 tz 参数无效。')
66
+ }
67
+ return value
68
+ }
69
+
70
+ function notificationScopeOf(value) {
71
+ if (value === undefined) return 'task'
72
+ if (value === 'task' || value === 'system' || value === 'all') return value
73
+ throw invalidQuery('通知 scope 参数无效。')
74
+ }
75
+
32
76
  function runLimitOf(value) {
33
77
  if (value === undefined) return 50
34
78
  if (typeof value !== 'string' && typeof value !== 'number') throw invalidQuery('运行分页 limit 无效。')
@@ -145,7 +189,7 @@ export function createAutomationService(options) {
145
189
  preset: 'capital-generation',
146
190
  cwdKind: 'profile',
147
191
  policy: 'automation-v1',
148
- categories: ['news', 'market', 'review', 'research'],
192
+ categories: [...TaskCategorySchema.options],
149
193
  cadences: ['daily', 'weekly', 'monthly', 'once'],
150
194
  limits: {
151
195
  runTimeoutMinutes: [MIN_RUN_TIMEOUT_MINUTES, MAX_RUN_TIMEOUT_MINUTES],
@@ -158,17 +202,21 @@ export function createAutomationService(options) {
158
202
  }
159
203
  }
160
204
 
205
+ async function workspaceCatalog() {
206
+ if (typeof workspaces?.details !== 'function') return { workspaces: [], defaultWorkspaceId: null }
207
+ try {
208
+ return await workspaces.details()
209
+ } catch (error) {
210
+ throw new AutomationError('workspace_unavailable', sanitizeMessage(String(error?.message ?? error)), 503)
211
+ }
212
+ }
213
+
161
214
  async function createTask(input) {
162
215
  const parsed = parseStrict(TaskCreateSchema, input, 'task')
163
216
  const defaults = { ...DEFAULT_AUTOMATION_SETTINGS, ...settings() }
217
+ // No limits here: an omitted (or unchanged) block stays inherited from the settings card.
164
218
  const resolved = {
165
219
  ...parsed,
166
- limits: parsed.limits ?? {
167
- runTimeoutMinutes: defaults.runTimeoutMinutes,
168
- idleTimeoutMinutes: defaults.idleTimeoutMinutes,
169
- maxSteps: defaults.maxSteps,
170
- maxToolCalls: defaults.maxToolCalls,
171
- },
172
220
  notifications: parsed.notifications ?? {
173
221
  inApp: defaults.inAppNotifications !== false,
174
222
  onFailure: true,
@@ -283,6 +331,47 @@ export function createAutomationService(options) {
283
331
  }
284
332
  }
285
333
 
334
+ /**
335
+ * Daily run counts for the contribution heatmap. Days are bucketed in the browser's zone,
336
+ * because `queuedAt` is an instant and the calendar day is a local fact.
337
+ */
338
+ async function runHistory(query = {}) {
339
+ const days = historyDaysOf(query.days)
340
+ const timeZone = historyTimeZoneOf(query.tz)
341
+ const end = localPartsAt(now(), timeZone)
342
+ const start = addDays(end, -(days - 1))
343
+ const buckets = []
344
+ const byDate = new Map()
345
+ for (let index = 0; index < days; index += 1) {
346
+ const date = addDays(start, index).date
347
+ const bucket = { date, total: 0, success: 0, failure: 0, active: 0 }
348
+ buckets.push(bucket)
349
+ byDate.set(date, bucket)
350
+ }
351
+ const page = await store.listRunsPage({ limit: MAX_RUN_PAGE_LIMIT })
352
+ let total = 0
353
+ let success = 0
354
+ let failure = 0
355
+ for (const run of page.runs) {
356
+ const at = Date.parse(run.queuedAt)
357
+ if (Number.isNaN(at)) continue
358
+ const bucket = byDate.get(localPartsAt(at, timeZone).date)
359
+ if (bucket === undefined) continue
360
+ bucket.total += 1
361
+ total += 1
362
+ if (run.status === 'succeeded') {
363
+ bucket.success += 1
364
+ success += 1
365
+ }
366
+ if (FAILURE_STATUSES.has(run.status)) {
367
+ bucket.failure += 1
368
+ failure += 1
369
+ }
370
+ if (ACTIVE_STATUSES.has(run.status)) bucket.active += 1
371
+ }
372
+ return { days: buckets, total, success, failure }
373
+ }
374
+
286
375
  async function listNotifications(query = {}) {
287
376
  let unreadOnly = false
288
377
  if (query.unread !== undefined) {
@@ -291,15 +380,16 @@ export function createAutomationService(options) {
291
380
  throw invalidQuery('通知 unread 参数无效。')
292
381
  }
293
382
  }
383
+ const scope = notificationScopeOf(query.scope)
294
384
  return {
295
- notifications: await store.listNotifications({ unreadOnly, limit: 200 }),
296
- unread: await store.unreadNotificationCount(),
385
+ notifications: await store.listNotifications({ unreadOnly, limit: 200, scope }),
386
+ unread: await store.unreadNotificationCount(scope),
297
387
  }
298
388
  }
299
389
 
300
390
  async function markNotificationsRead(input) {
301
391
  const parsed = parseStrict(NotificationsReadSchema, input, 'notifications/read')
302
- return { changed: await store.markNotificationsRead(parsed) }
392
+ return { changed: await store.markNotificationsRead({ ...parsed, scope: parsed.scope ?? 'task' }) }
303
393
  }
304
394
 
305
395
  async function taskDetail(id) {
@@ -313,6 +403,7 @@ export function createAutomationService(options) {
313
403
  return {
314
404
  serialize,
315
405
  meta,
406
+ workspaceCatalog,
316
407
  listTasks,
317
408
  taskDetail,
318
409
  createTask,
@@ -324,6 +415,7 @@ export function createAutomationService(options) {
324
415
  cancelRun,
325
416
  getRun,
326
417
  listRuns,
418
+ runHistory,
327
419
  listNotifications,
328
420
  markNotificationsRead,
329
421
  storageState: async () => {
@@ -26,6 +26,11 @@ function intIn(value, minimum, maximum, fallback) {
26
26
 
27
27
  export function normalizeAutomationSettings(value = {}, fallback = DEFAULT_AUTOMATION_SETTINGS) {
28
28
  const source = value ?? {}
29
+ const workspaceId = source.defaultWorkspaceId === undefined
30
+ ? fallback.defaultWorkspaceId
31
+ : typeof source.defaultWorkspaceId === 'string'
32
+ ? source.defaultWorkspaceId.trim()
33
+ : fallback.defaultWorkspaceId
29
34
  return {
30
35
  tickMs: intIn(source.tickMs, MIN_TICK_MS, MAX_TICK_MS, fallback.tickMs),
31
36
  runTimeoutMinutes: intIn(source.runTimeoutMinutes, MIN_RUN_TIMEOUT_MINUTES, MAX_RUN_TIMEOUT_MINUTES, fallback.runTimeoutMinutes),
@@ -33,6 +38,7 @@ export function normalizeAutomationSettings(value = {}, fallback = DEFAULT_AUTOM
33
38
  maxSteps: intIn(source.maxSteps, MIN_MAX_STEPS, MAX_MAX_STEPS, fallback.maxSteps),
34
39
  maxToolCalls: intIn(source.maxToolCalls, MIN_MAX_TOOL_CALLS, MAX_MAX_TOOL_CALLS, fallback.maxToolCalls),
35
40
  inAppNotifications: source.inAppNotifications === undefined ? fallback.inAppNotifications : source.inAppNotifications !== false,
41
+ defaultWorkspaceId: workspaceId ?? '',
36
42
  }
37
43
  }
38
44
 
@@ -19,11 +19,9 @@ import {
19
19
  resolveTaskRecord,
20
20
  } from './schema.js'
21
21
  import { BUILTIN_BY_ID, BUILTIN_DEFINITIONS } from './builtins.js'
22
+ import { DEFAULT_CUSTOM_ICON, builtinTaskIcon } from './task-icons.js'
22
23
  import {
23
- DEFAULT_IDLE_TIMEOUT_MINUTES,
24
- DEFAULT_MAX_STEPS,
25
- DEFAULT_MAX_TOOL_CALLS,
26
- DEFAULT_RUN_TIMEOUT_MINUTES,
24
+ DEFAULT_AUTOMATION_SETTINGS,
27
25
  MAX_ERRORS_PER_RUN,
28
26
  MAX_NOTIFICATIONS_PER_PROFILE,
29
27
  MAX_RUNS_PER_PROFILE,
@@ -35,6 +33,8 @@ import { canonicalTrigger, formatInZone, nextOccurrence } from './time.js'
35
33
  export const DOMAIN_NAME = 'capital_automation'
36
34
  export const ROUTE_PATH = '/capital-automation'
37
35
 
36
+ const SYSTEM_NOTIFICATION_KINDS = new Set(['scheduler_standby'])
37
+
38
38
  /** Declared as a plain object; `defineDomain` only re-validates the same fields. */
39
39
  export const domainSpec = {
40
40
  name: DOMAIN_NAME,
@@ -122,6 +122,7 @@ export function createAutomationStore(options) {
122
122
  now = () => Date.now(),
123
123
  random = () => randomUUID(),
124
124
  builtins = BUILTIN_DEFINITIONS,
125
+ settings,
125
126
  } = options
126
127
  const rawProfileName = String(profile?.name ?? '')
127
128
  const profileName = rawProfileName.slice(0, 200)
@@ -130,6 +131,39 @@ export function createAutomationStore(options) {
130
131
  let opening
131
132
  let claimChain = Promise.resolve()
132
133
 
134
+ /** Profile-level defaults; a task without its own limits inherits these. */
135
+ function defaultLimits() {
136
+ let current
137
+ try {
138
+ current = typeof settings === 'function' ? settings() : undefined
139
+ } catch {
140
+ current = undefined
141
+ }
142
+ const source = current ?? DEFAULT_AUTOMATION_SETTINGS
143
+ return {
144
+ runTimeoutMinutes: source.runTimeoutMinutes,
145
+ idleTimeoutMinutes: source.idleTimeoutMinutes,
146
+ maxSteps: source.maxSteps,
147
+ maxToolCalls: source.maxToolCalls,
148
+ }
149
+ }
150
+
151
+ function isDefaultLimits(limits) {
152
+ if (limits === undefined) return false
153
+ const defaults = defaultLimits()
154
+ return Object.keys(defaults).every((key) => limits[key] === defaults[key])
155
+ }
156
+
157
+ /**
158
+ * Storing the current defaults would pin the task to them. Drop the block instead so the
159
+ * resolved task keeps following the settings card.
160
+ */
161
+ function withoutInheritedLimits(config) {
162
+ if (config?.limits === undefined || !isDefaultLimits(config.limits)) return config
163
+ const { limits: _inherited, ...rest } = config
164
+ return rest
165
+ }
166
+
133
167
  function serializeClaim(operation) {
134
168
  const next = claimChain.then(operation, operation)
135
169
  claimChain = next.then(() => {}, () => {})
@@ -233,7 +267,7 @@ export function createAutomationStore(options) {
233
267
 
234
268
  function resolve(record) {
235
269
  if (record.origin === 'builtin' && definitionOf(record) === undefined) return undefined
236
- return resolveTaskRecord(record, definitionOf(record))
270
+ return resolveTaskRecord(record, definitionOf(record), defaultLimits())
237
271
  }
238
272
 
239
273
  async function listTasks() {
@@ -257,6 +291,7 @@ export function createAutomationStore(options) {
257
291
  function defaultCustomConfig() {
258
292
  return {
259
293
  category: 'research',
294
+ icon: DEFAULT_CUSTOM_ICON,
260
295
  title: '新建任务',
261
296
  description: '',
262
297
  instruction: '按任务说明完成一次研究并给出可核查的结论。',
@@ -268,23 +303,17 @@ export function createAutomationStore(options) {
268
303
  tradingDaysOnly: true,
269
304
  },
270
305
  cwdKind: 'profile',
271
- limits: {
272
- runTimeoutMinutes: DEFAULT_RUN_TIMEOUT_MINUTES,
273
- idleTimeoutMinutes: DEFAULT_IDLE_TIMEOUT_MINUTES,
274
- maxSteps: DEFAULT_MAX_STEPS,
275
- maxToolCalls: DEFAULT_MAX_TOOL_CALLS,
276
- },
277
306
  notifications: { inApp: true, onFailure: true, onSuccess: false },
278
307
  }
279
308
  }
280
309
 
281
310
  async function createTask(input) {
282
311
  const at = iso(now())
283
- const config = TaskConfigSchema.parse({
312
+ const config = TaskConfigSchema.parse(withoutInheritedLimits({
284
313
  ...defaultCustomConfig(),
285
314
  ...input,
286
315
  cwdKind: 'profile',
287
- })
316
+ }))
288
317
  const record = parseStrict(TaskRecordSchema, {
289
318
  schemaVersion: 1,
290
319
  id: `task_${random().replace(/-/g, '')}`,
@@ -316,10 +345,12 @@ export function createAutomationStore(options) {
316
345
  if (!isDeepStrictEqual(value, current[key])) changed[key] = value
317
346
  }
318
347
  const merged = TaskOverridesSchema.parse({ ...(record.overrides ?? {}), ...changed })
319
- next.overrides = merged
348
+ if (isDefaultLimits(merged.limits)) delete merged.limits
349
+ if (Object.keys(merged).length === 0) delete next.overrides
350
+ else next.overrides = merged
320
351
  } else {
321
352
  const merged = TaskConfigSchema.parse({ ...record.custom, ...patch, cwdKind: 'profile' })
322
- next.custom = merged
353
+ next.custom = withoutInheritedLimits(merged)
323
354
  }
324
355
  if (triggerChanged) {
325
356
  next.scheduleAnchorAt = at
@@ -375,8 +406,9 @@ export function createAutomationStore(options) {
375
406
  const task = resolve(record)
376
407
  if (task === undefined) return undefined
377
408
  const at = iso(now())
378
- const config = TaskConfigSchema.parse({
409
+ const config = TaskConfigSchema.parse(withoutInheritedLimits({
379
410
  category: task.category,
411
+ icon: builtinTaskIcon(record.sourceBuiltinId),
380
412
  title: `${task.title}(副本)`.slice(0, 120),
381
413
  description: task.description,
382
414
  trigger: task.trigger,
@@ -385,7 +417,7 @@ export function createAutomationStore(options) {
385
417
  ...(task.workspaceId === undefined ? {} : { workspaceId: task.workspaceId }),
386
418
  limits: task.limits,
387
419
  notifications: task.notifications,
388
- })
420
+ }))
389
421
  const custom = parseStrict(TaskRecordSchema, {
390
422
  schemaVersion: 1,
391
423
  id: `task_${random().replace(/-/g, '')}`,
@@ -591,11 +623,21 @@ export function createAutomationStore(options) {
591
623
  for (const record of records.slice(MAX_NOTIFICATIONS_PER_PROFILE)) await tables.notifications.delete(record.id)
592
624
  }
593
625
 
594
- async function listNotifications({ unreadOnly = false, limit = 50, taskId } = {}) {
626
+ // Scheduler standby is a system state, not a task outcome: keep it out of the task inbox and
627
+ // its unread badge. One predicate for list/count/read so the three cannot drift apart.
628
+ function notificationInScope(record, scope) {
629
+ const system = SYSTEM_NOTIFICATION_KINDS.has(record.kind)
630
+ if (scope === 'system') return system
631
+ if (scope === 'all') return true
632
+ return !system
633
+ }
634
+
635
+ async function listNotifications({ unreadOnly = false, limit = 50, taskId, scope = 'task' } = {}) {
595
636
  const tables = await ensure()
596
637
  const records = []
597
638
  for (const [, record] of tables.notifications.entries()) {
598
639
  if (record.profileKey !== profileKey) continue
640
+ if (!notificationInScope(record, scope)) continue
599
641
  if (taskId !== undefined && record.taskId !== taskId) continue
600
642
  if (unreadOnly && record.readAt !== undefined) continue
601
643
  records.push(record)
@@ -604,22 +646,23 @@ export function createAutomationStore(options) {
604
646
  return records.slice(0, Math.max(1, Math.min(200, limit)))
605
647
  }
606
648
 
607
- async function unreadNotificationCount() {
649
+ async function unreadNotificationCount(scope = 'task') {
608
650
  const tables = await ensure()
609
651
  let count = 0
610
652
  for (const [, record] of tables.notifications.entries()) {
611
- if (record.profileKey === profileKey && record.readAt === undefined) count += 1
653
+ if (record.profileKey === profileKey && notificationInScope(record, scope) && record.readAt === undefined) count += 1
612
654
  }
613
655
  return count
614
656
  }
615
657
 
616
- async function markNotificationsRead({ ids, all = false }) {
658
+ async function markNotificationsRead({ ids, all = false, scope = 'task' }) {
617
659
  const tables = await ensure()
618
660
  const wanted = new Set(ids ?? [])
619
661
  let changed = 0
620
662
  const at = iso(now())
621
663
  for (const [id, record] of [...tables.notifications.entries()]) {
622
664
  if (record.profileKey !== profileKey || record.readAt !== undefined) continue
665
+ if (!notificationInScope(record, scope)) continue
623
666
  if (!all && !wanted.has(id)) continue
624
667
  const next = parseStrict(NotificationSchema, { ...record, readAt: at }, 'notification')
625
668
  await tables.notifications.put(id, next)
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Task glyph catalog, adapted from Tabler Icons (MIT).
3
+ * Upstream: https://github.com/tabler/tabler-icons/tree/ed177a085b652fe04ec4f37ced867c94621e7840/icons/outline
4
+ * Only path geometry is copied; stroke and color inherit from the tile. See ./TABLER-ICONS-LICENSE.
5
+ */
6
+ const icon = (label, source, d) => ({ label, source, d })
7
+
8
+ export const DEFAULT_CUSTOM_ICON = 'sparkles'
9
+
10
+ /**
11
+ * Picker order: creation first, then time, market, documents, research, portfolio.
12
+ * These keys are persisted in task records; renaming or removing one needs a migration.
13
+ */
14
+ export const TASK_ICON_CATALOG = {
15
+ 'sparkles': icon('灵感', 'sparkles', 'M16 18a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2m0 -12a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2m-7 12a6 6 0 0 1 6 -6a6 6 0 0 1 -6 -6a6 6 0 0 1 -6 6a6 6 0 0 1 6 6'),
16
+ 'bell': icon('提醒', 'bell', 'M10 5a2 2 0 1 1 4 0a7 7 0 0 1 4 6v3a4 4 0 0 0 2 3h-16a4 4 0 0 0 2 -3v-3a7 7 0 0 1 4 -6 M9 17v1a3 3 0 0 0 6 0v-1'),
17
+ 'mail': icon('消息', 'mail', 'M3 7a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-10 M3 7l9 6l9 -6'),
18
+ 'flag': icon('里程碑', 'flag', 'M5 5a5 5 0 0 1 7 0a5 5 0 0 0 7 0v9a5 5 0 0 1 -7 0a5 5 0 0 0 -7 0v-9 M5 21v-7'),
19
+ 'calendar-event': icon('事件', 'calendar-event', 'M4 7a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2l0 -12 M16 3l0 4 M8 3l0 4 M4 11l16 0 M8 15h2v2h-2l0 -2'),
20
+ 'clock-play': icon('盘前', 'clock-play', 'M12 7v5l2 2 M17 22l5 -3l-5 -3l0 6 M13.017 20.943a9 9 0 1 1 7.831 -7.292'),
21
+ 'sunrise': icon('早间', 'sunrise', 'M3 17h1m16 0h1m-15.4 -6.4l.7 .7m12.1 -.7l-.7 .7m-9.7 5.7a4 4 0 0 1 8 0 M3 21l18 0 M12 9v-6l3 3m-6 0l3 -3'),
22
+ 'sun-high': icon('午间', 'sun-high', 'M14.828 14.828a4 4 0 1 0 -5.656 -5.656a4 4 0 0 0 5.656 5.656 M6.343 17.657l-1.414 1.414 M6.343 6.343l-1.414 -1.414 M17.657 6.343l1.414 -1.414 M17.657 17.657l1.414 1.414 M4 12h-2 M12 4v-2 M20 12h2 M12 20v2'),
23
+ 'moon-stars': icon('收盘', 'moon-stars', 'M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454l0 .008 M17 4a2 2 0 0 0 2 2a2 2 0 0 0 -2 2a2 2 0 0 0 -2 -2a2 2 0 0 0 2 -2 M19 11h2m-1 -1v2'),
24
+ 'chart-line': icon('行情', 'chart-line', 'M4 19l16 0 M4 15l4 -6l4 2l4 -5l4 4'),
25
+ 'chart-bar': icon('统计', 'chart-bar', 'M3 13a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v6a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -6 M15 9a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v10a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -10 M9 5a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -14 M4 20h14'),
26
+ 'chart-treemap': icon('行业', 'chart-treemap', 'M4 6a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2l0 -12 M12 4v16 M4 15h8 M12 12h8 M16 12v8 M16 16h4'),
27
+ 'chart-pie': icon('组合', 'chart-pie', 'M10 3.2a9 9 0 1 0 10.8 10.8a1 1 0 0 0 -1 -1h-6.8a2 2 0 0 1 -2 -2v-7a.9 .9 0 0 0 -1 -.8 M15 3.5a9 9 0 0 1 5.5 5.5h-4.5a1 1 0 0 1 -1 -1v-4.5'),
28
+ 'trending-up': icon('趋势', 'trending-up', 'M3 17l6 -6l4 4l8 -8 M14 7l7 0l0 7'),
29
+ 'report-analytics': icon('财报', 'report-analytics', 'M9 5h-2a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-12a2 2 0 0 0 -2 -2h-2 M9 5a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2a2 2 0 0 1 -2 2h-2a2 2 0 0 1 -2 -2 M9 17v-5 M12 17v-1 M15 17v-3'),
30
+ 'file-stack': icon('公告', 'file-stack', 'M14 3v4a1 1 0 0 0 1 1h4 M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4 M5 21h14 M5 18h14 M5 15h14'),
31
+ 'inbox': icon('收件', 'inbox', 'M4 6a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2l0 -12 M4 13h3l3 3h4l3 -3h3'),
32
+ 'news': icon('新闻', 'news', 'M16 6h3a1 1 0 0 1 1 1v11a2 2 0 0 1 -4 0v-13a1 1 0 0 0 -1 -1h-10a1 1 0 0 0 -1 1v12a3 3 0 0 0 3 3h11 M8 8l4 0 M8 12l4 0 M8 16l4 0'),
33
+ 'clipboard-check': icon('复盘', 'clipboard-check', 'M9 5h-2a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-12a2 2 0 0 0 -2 -2h-2 M9 5a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2a2 2 0 0 1 -2 2h-2a2 2 0 0 1 -2 -2 M9 14l2 2l4 -4'),
34
+ 'book': icon('阅读', 'book', 'M3 19a9 9 0 0 1 9 0a9 9 0 0 1 9 0 M3 6a9 9 0 0 1 9 0a9 9 0 0 1 9 0 M3 6l0 13 M12 6l0 13 M21 6l0 13'),
35
+ 'search': icon('检索', 'search', 'M3 10a7 7 0 1 0 14 0a7 7 0 1 0 -14 0 M21 21l-6 -6'),
36
+ 'microscope': icon('深研', 'microscope', 'M5 21h14 M6 18h2 M7 18v3 M9 11l3 3l6 -6l-3 -3l-6 6 M10.5 12.5l-1.5 1.5 M17 3l3 3 M12 21a6 6 0 0 0 3.715 -10.712'),
37
+ 'bulb': icon('选题', 'bulb', 'M3 12h1m8 -9v1m8 8h1m-15.4 -6.4l.7 .7m12.1 -.7l-.7 .7 M9 16a5 5 0 1 1 6 0a3.5 3.5 0 0 0 -1 3a2 2 0 0 1 -4 0a3.5 3.5 0 0 0 -1 -3 M9.7 17l4.6 0'),
38
+ 'briefcase': icon('持仓', 'briefcase', 'M3 9a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v9a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2l0 -9 M8 7v-2a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2v2 M12 12l0 .01 M3 13a20 20 0 0 0 18 0'),
39
+ 'wallet': icon('资金', 'wallet', 'M17 8v-3a1 1 0 0 0 -1 -1h-10a2 2 0 0 0 0 4h12a1 1 0 0 1 1 1v3m0 4v3a1 1 0 0 1 -1 1h-12a2 2 0 0 1 -2 -2v-12 M20 12v4h-4a2 2 0 0 1 0 -4h4'),
40
+ 'shield-check': icon('风控', 'shield-check', 'M11.46 20.846a12 12 0 0 1 -7.96 -14.846a12 12 0 0 0 8.5 -3a12 12 0 0 0 8.5 3a12 12 0 0 1 -.09 7.06 M15 19l2 2l4 -4'),
41
+ 'target': icon('目标', 'target', 'M11 12a1 1 0 1 0 2 0a1 1 0 1 0 -2 0 M7 12a5 5 0 1 0 10 0a5 5 0 1 0 -10 0 M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0'),
42
+ 'star': icon('自选', 'star', 'M12 17.75l-6.172 3.245l1.179 -6.873l-5 -4.867l6.9 -1l3.086 -6.253l3.086 6.253l6.9 1l-5 4.867l1.179 6.873l-6.158 -3.245'),
43
+ 'building-skyscraper': icon('城市', 'building-skyscraper', 'M3 21l18 0 M5 21v-14l8 -4v18 M19 21v-10l-6 -4 M9 9l0 .01 M9 12l0 .01 M9 15l0 .01 M9 18l0 .01'),
44
+ 'globe': icon('环球', 'globe', 'M7 9a4 4 0 1 0 8 0a4 4 0 0 0 -8 0 M5.75 15a8.015 8.015 0 1 0 9.25 -13 M11 17v4 M7 21h8'),
45
+ }
46
+
47
+ export const TASK_ICON_IDS = Object.keys(TASK_ICON_CATALOG)
48
+
49
+ /** Builtin templates keep a fixed glyph; custom tasks store an icon id in their config. */
50
+ export const BUILTIN_TASK_ICONS = Object.freeze({
51
+ 'us-close': 'moon-stars',
52
+ 'pre-open-filings': 'file-stack',
53
+ 'morning-brief': 'sunrise',
54
+ 'midday-snapshot': 'sun-high',
55
+ 'post-close-filings': 'inbox',
56
+ 'hk-close': 'building-skyscraper',
57
+ 'daily-review': 'clipboard-check',
58
+ 'us-premarket': 'clock-play',
59
+ 'portfolio-weekly': 'chart-pie',
60
+ 'watchlist-weekly': 'star',
61
+ 'sector-weekly': 'chart-treemap',
62
+ 'research-topics': 'bulb',
63
+ 'earnings-tracker': 'report-analytics',
64
+ 'week-ahead-calendar': 'calendar-event',
65
+ 'market-weekly': 'chart-line',
66
+ 'portfolio-risk-monthly': 'shield-check',
67
+ })
68
+
69
+ export function builtinTaskIcon(sourceBuiltinId) {
70
+ const icon = Object.prototype.hasOwnProperty.call(BUILTIN_TASK_ICONS, sourceBuiltinId)
71
+ ? BUILTIN_TASK_ICONS[sourceBuiltinId]
72
+ : undefined
73
+ return icon ?? DEFAULT_CUSTOM_ICON
74
+ }
75
+