@v587d/capital-generation 2.15.0 → 2.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +171 -0
- package/README.md +7 -12
- package/THIRD_PARTY_NOTICES.md +1 -0
- package/automation/TABLER-ICONS-LICENSE +21 -0
- package/automation/builtins.js +237 -10
- package/automation/client.js +79 -17
- package/automation/client.src.cjs +873 -210
- package/automation/contract.js +4 -0
- package/automation/defaults.js +6 -4
- package/automation/index.js +12 -7
- package/automation/package.json +5 -2
- package/automation/route.js +9 -2
- package/automation/runner.js +33 -6
- package/automation/scheduler.js +34 -29
- package/automation/schema.js +39 -5
- package/automation/service.js +102 -10
- package/automation/settings.js +6 -0
- package/automation/store.js +64 -21
- package/automation/task-icons.js +75 -0
- package/automation/workspaces.js +113 -13
- package/chart-ui/client.js +2 -2
- package/chart-ui/package.json +1 -1
- package/config/client.js +19 -5
- package/config/client.src.cjs +365 -44
- package/config/index.js +7 -5
- package/config/package.json +1 -1
- package/lib/agents/root-tool-policy.d.ts +3 -3
- package/lib/agents/root-tool-policy.js +4 -2
- package/lib/chart/runtime.d.ts +1 -1
- package/lib/chart/runtime.js +95 -0
- package/lib/chart/series.d.ts +4 -4
- package/lib/chart/series.js +44 -10
- package/lib/data-collector/dataset-tools.js +22 -0
- package/lib/data-collector/describe.js +1 -0
- package/lib/data-collector/hub.js +5 -0
- package/lib/data-collector/store.d.ts +5 -0
- package/lib/data-collector/store.js +17 -0
- package/lib/data-collector/time-axis.d.ts +15 -0
- package/lib/data-collector/time-axis.js +73 -0
- package/lib/data-collector/tools.d.ts +1 -1
- package/lib/data-collector/tools.js +12 -1
- package/lib/index.d.ts +5 -0
- package/lib/index.js +7 -5
- package/lib/sources/tencent-http.js +7 -2
- package/lib/sources/tencent-public-core.d.ts +2 -0
- package/lib/sources/tencent-public-core.js +5 -0
- package/lib/sources/wind-mcp.js +3 -3
- package/lib/sources-entry.d.ts +1 -1
- package/lib/web-retriever/local-fetch.d.ts +1 -1
- package/lib/web-retriever/local-fetch.js +1 -1
- package/lib/web-retriever/sources.d.ts +58 -0
- package/lib/web-retriever/sources.js +290 -0
- package/lib/web-retriever/tools.js +199 -8
- package/lib/web-retriever/wind-client.d.ts +1 -1
- package/lib/web-retriever/wind-client.js +1 -1
- package/package.json +1 -1
- package/preset/capital-generation/agent.patch.yml +20 -19
- package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +6 -3
- package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +9 -2
- package/preset/capital-generation/skills/capital-orchestration/SKILL.md +2 -1
- package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +2 -0
- package/preset/capital-generation/skills/capital-web-evidence/SKILL.md +18 -13
- package/preset/capital-generation/skills/capital-web-protocol/SKILL.md +30 -15
- package/watchlist/package.json +1 -1
package/automation/service.js
CHANGED
|
@@ -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: [
|
|
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 () => {
|
package/automation/settings.js
CHANGED
|
@@ -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
|
|
package/automation/store.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
|