@v587d/capital-generation 2.13.0 → 2.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +67 -67
- package/automation/calendar-source.js +361 -0
- package/automation/client.js +341 -58
- package/automation/client.src.cjs +2385 -428
- package/automation/defaults.js +4 -0
- package/automation/index.js +7 -2
- package/automation/package.json +2 -1
- package/automation/runner.js +68 -14
- package/automation/scheduler.js +20 -2
- package/automation/schema.js +16 -0
- package/automation/store.js +37 -12
- package/chart-ui/package.json +1 -1
- package/config/package.json +1 -1
- package/lib/sources/wind-mcp.js +3 -3
- package/lib/web-retriever/local-fetch.d.ts +1 -1
- package/lib/web-retriever/local-fetch.js +1 -1
- package/lib/web-retriever/wind-client.d.ts +1 -1
- package/lib/web-retriever/wind-client.js +1 -1
- package/package.json +2 -1
- package/watchlist/package.json +1 -1
package/automation/defaults.js
CHANGED
|
@@ -69,3 +69,7 @@ export const RUN_STATUSES = [
|
|
|
69
69
|
|
|
70
70
|
export const ACTIVE_RUN_STATUSES = ['queued', 'starting', 'running']
|
|
71
71
|
export const TERMINAL_RUN_STATUSES = RUN_STATUSES.filter((status) => !ACTIVE_RUN_STATUSES.includes(status))
|
|
72
|
+
|
|
73
|
+
// A finished turn can be settled from its durable checkpoint; `unknown_after_restart`
|
|
74
|
+
// stays the fallback for runs with no provable turn boundary.
|
|
75
|
+
export const SETTLED_RUN_STATUSES = ['succeeded', 'failed', 'canceled', 'timed_out', 'blocked_requires_user']
|
package/automation/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import { createAutomationStore, domainSpec, ROUTE_PATH } from './store.js'
|
|
|
13
13
|
import { createAutomationRouteHandler } from './route.js'
|
|
14
14
|
import { resolveAutomationSettings } from './settings.js'
|
|
15
15
|
import { installAutomationPolicy } from './policy.js'
|
|
16
|
-
import {
|
|
16
|
+
import { createCalendarSource } from './calendar-source.js'
|
|
17
17
|
import { createWorkspaceCatalog } from './workspaces.js'
|
|
18
18
|
import { sanitizeMessage } from './redact.js'
|
|
19
19
|
|
|
@@ -38,13 +38,17 @@ function safeSegment(value) {
|
|
|
38
38
|
*/
|
|
39
39
|
export function createAutomationRuntime(options) {
|
|
40
40
|
const { ctx, profile, storageDomain, settings, now = () => Date.now(), logger, version = versionOf() } = options
|
|
41
|
-
const calendar = options.calendar ?? createTradingCalendar()
|
|
42
41
|
const getSettings = typeof settings === 'function' ? settings : () => resolveAutomationSettings(ctx, settings)
|
|
43
42
|
const rawProfileName = String(profile.name)
|
|
44
43
|
const displayName = rawProfileName.slice(0, 200)
|
|
45
44
|
const home = profile.home ?? profile.dir ?? process.cwd()
|
|
46
45
|
const runDir = profile.runDir ?? join(home, 'automation-workspace', safeSegment(displayName))
|
|
47
46
|
const ownerDir = profile.ownerDir ?? join(home, 'storages', 'capital-automation-locks')
|
|
47
|
+
const calendar = options.calendar ?? createCalendarSource({
|
|
48
|
+
cacheDir: join(home, 'storages', 'capital-automation-calendar'),
|
|
49
|
+
logger,
|
|
50
|
+
now,
|
|
51
|
+
})
|
|
48
52
|
const policyContext = options.policyContext ?? ctx
|
|
49
53
|
const workspaces = createWorkspaceCatalog({ ctx, displayName, runDir, logger })
|
|
50
54
|
const store = createAutomationStore({
|
|
@@ -156,6 +160,7 @@ export function apply(ctx) {
|
|
|
156
160
|
activeService = undefined
|
|
157
161
|
}
|
|
158
162
|
await created.scheduler.stop().catch(() => {})
|
|
163
|
+
created.calendar.dispose?.()
|
|
159
164
|
await created.runner.dispose?.()
|
|
160
165
|
await created.store.close().catch(() => {})
|
|
161
166
|
}, 'capital-automation: runtime')
|
package/automation/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@v587d/capital-automation",
|
|
3
|
-
"version": "2.13.
|
|
3
|
+
"version": "2.13.1",
|
|
4
4
|
"description": "CapGen 投研自动化:独立 host bundle(固定 capital_automation 域、单 scheduler owner、/capital-automation 路由与浏览器页面)。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"builtins.js",
|
|
35
35
|
"time.js",
|
|
36
36
|
"calendar.js",
|
|
37
|
+
"calendar-source.js",
|
|
37
38
|
"owner.js",
|
|
38
39
|
"store.js",
|
|
39
40
|
"service.js",
|
package/automation/runner.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Dataset. Session creation failing still leaves a visible synthetic failed run.
|
|
5
5
|
*/
|
|
6
6
|
import { mkdir } from 'node:fs/promises'
|
|
7
|
-
import { CANCEL_GRACE_MS, MAX_ERRORS_PER_RUN, QUIET_SETTLE_MS } from './defaults.js'
|
|
7
|
+
import { CANCEL_GRACE_MS, MAX_ERRORS_PER_RUN, QUIET_SETTLE_MS, SETTLED_RUN_STATUSES } from './defaults.js'
|
|
8
8
|
import { sanitizeMessage } from './redact.js'
|
|
9
9
|
|
|
10
10
|
const PRESET_ID = 'capital-generation'
|
|
@@ -210,6 +210,38 @@ export function createRunRunner(options) {
|
|
|
210
210
|
return updated
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
// The 750ms settle window is not durable. Persist the turn boundary before waiting so a
|
|
214
|
+
// restart cannot turn a finished turn into unknown_after_restart.
|
|
215
|
+
function checkpointTurn(entry, phase, turn, at) {
|
|
216
|
+
const turnState = {
|
|
217
|
+
phase,
|
|
218
|
+
turn: Number.isSafeInteger(turn) ? turn : 0,
|
|
219
|
+
at: iso(Number.isFinite(at) ? at : now()),
|
|
220
|
+
}
|
|
221
|
+
const patch = { turnState }
|
|
222
|
+
if (phase === 'ended') {
|
|
223
|
+
turnState.status = outcomeOf(entry)
|
|
224
|
+
patch.steps = entry.steps
|
|
225
|
+
patch.toolCalls = entry.toolCalls
|
|
226
|
+
patch.blockedRequiresUser = entry.blocked === true
|
|
227
|
+
if (entry.summary !== undefined) patch.summary = entry.summary
|
|
228
|
+
}
|
|
229
|
+
void store.updateRun(entry.run.id, patch)
|
|
230
|
+
.then((updated) => { if (updated !== undefined) entry.run = updated })
|
|
231
|
+
.catch((error) => logger?.warn?.(`capital-automation: turn checkpoint failed: ${String(error)}`))
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function settledStatusOf(entry) {
|
|
235
|
+
if (entry.turnActive === true) return undefined
|
|
236
|
+
if (entry.lastTurnEnd !== undefined) {
|
|
237
|
+
const status = outcomeOf(entry)
|
|
238
|
+
if (status !== 'unknown_after_restart') return status
|
|
239
|
+
}
|
|
240
|
+
const turn = entry.run.turnState
|
|
241
|
+
if (turn?.phase === 'ended' && SETTLED_RUN_STATUSES.includes(turn.status)) return turn.status
|
|
242
|
+
return undefined
|
|
243
|
+
}
|
|
244
|
+
|
|
213
245
|
async function flushSessionBounded(agent) {
|
|
214
246
|
const sessions = service(ctx, 'sessions')
|
|
215
247
|
const flush = sessions?.flush
|
|
@@ -401,12 +433,13 @@ export function createRunRunner(options) {
|
|
|
401
433
|
|
|
402
434
|
function countedEventKey(session, event) {
|
|
403
435
|
const sessionId = typeof session?.id === 'string' ? session.id : ''
|
|
436
|
+
const data = event.data ?? event
|
|
404
437
|
if (Number.isSafeInteger(event.seq)) return `${sessionId}:seq:${event.seq}`
|
|
405
|
-
if ((event.type === 'tool/call' || event.type === 'tool/ptc-dispatch') && typeof
|
|
406
|
-
return `${sessionId}:${event.type}:${
|
|
438
|
+
if ((event.type === 'tool/call' || event.type === 'tool/ptc-dispatch') && typeof data.callId === 'string') {
|
|
439
|
+
return `${sessionId}:${event.type}:${data.callId}`
|
|
407
440
|
}
|
|
408
|
-
if (event.type === 'step/start' && Number.isSafeInteger(
|
|
409
|
-
return `${sessionId}:step:${
|
|
441
|
+
if (event.type === 'step/start' && Number.isSafeInteger(data.turn) && Number.isSafeInteger(data.step)) {
|
|
442
|
+
return `${sessionId}:step:${data.turn}:${data.step}`
|
|
410
443
|
}
|
|
411
444
|
return undefined
|
|
412
445
|
}
|
|
@@ -426,7 +459,16 @@ export function createRunRunner(options) {
|
|
|
426
459
|
if (event === undefined || typeof event.type !== 'string') return
|
|
427
460
|
const entry = entryForSession(session)
|
|
428
461
|
if (entry === undefined || entry.finished) return
|
|
462
|
+
const data = event.data ?? event
|
|
429
463
|
const rootEvent = session?.id === entry.sessionId
|
|
464
|
+
if (event.type === 'turn/start') {
|
|
465
|
+
armIdle(entry)
|
|
466
|
+
if (!rootEvent) return
|
|
467
|
+
entry.lastTurnEnd = undefined
|
|
468
|
+
entry.turnActive = true
|
|
469
|
+
checkpointTurn(entry, 'running', data.turn, event.time)
|
|
470
|
+
return
|
|
471
|
+
}
|
|
430
472
|
if (event.type === 'step/start') {
|
|
431
473
|
if (!countEventOnce(entry, session, event)) return
|
|
432
474
|
armIdle(entry)
|
|
@@ -444,7 +486,7 @@ export function createRunRunner(options) {
|
|
|
444
486
|
if (!countEventOnce(entry, session, event)) return
|
|
445
487
|
armIdle(entry)
|
|
446
488
|
entry.toolCalls += 1
|
|
447
|
-
if (typeof
|
|
489
|
+
if (typeof data.callId === 'string' && typeof data.name === 'string') entry.toolNames.set(data.callId, data.name)
|
|
448
490
|
if (entry.toolCalls > entry.task.limits.maxToolCalls) {
|
|
449
491
|
void cancelEntry(entry, 'automation_tool_limit', 'failed', {
|
|
450
492
|
phase: 'timeout',
|
|
@@ -456,28 +498,30 @@ export function createRunRunner(options) {
|
|
|
456
498
|
}
|
|
457
499
|
if (event.type === 'tool/result') {
|
|
458
500
|
armIdle(entry)
|
|
459
|
-
if (
|
|
460
|
-
const tool = entry.toolNames.get(
|
|
501
|
+
if (data.message?.isError !== true) return
|
|
502
|
+
const tool = entry.toolNames.get(data.callId)
|
|
461
503
|
pushError(entry, {
|
|
462
504
|
phase: 'tool',
|
|
463
|
-
code:
|
|
464
|
-
message:
|
|
505
|
+
code: data.error?.code ?? 'tool_error',
|
|
506
|
+
message: textOfMessage(data.message) || '工具调用失败。',
|
|
465
507
|
...(tool === undefined ? {} : { tool }),
|
|
466
|
-
...(typeof
|
|
508
|
+
...(typeof data.callId === 'string' ? { callId: data.callId } : {}),
|
|
467
509
|
})
|
|
468
510
|
return
|
|
469
511
|
}
|
|
470
512
|
if (event.type === 'assistant/message') {
|
|
471
513
|
armIdle(entry)
|
|
472
514
|
if (!rootEvent) return
|
|
473
|
-
const text = textOfMessage(
|
|
515
|
+
const text = textOfMessage(data.message)
|
|
474
516
|
if (text.length > 0) entry.summary = sanitizeMessage(text).slice(0, MAX_SUMMARY_CHARS)
|
|
475
517
|
return
|
|
476
518
|
}
|
|
477
519
|
if (event.type === 'turn/end') {
|
|
478
520
|
armIdle(entry)
|
|
479
521
|
if (!rootEvent) return
|
|
480
|
-
entry.lastTurnEnd = { turn:
|
|
522
|
+
entry.lastTurnEnd = { turn: data.turn, reason: data.reason }
|
|
523
|
+
entry.turnActive = false
|
|
524
|
+
checkpointTurn(entry, 'ended', data.turn, event.time)
|
|
481
525
|
void maybeComplete(entry)
|
|
482
526
|
}
|
|
483
527
|
}
|
|
@@ -584,6 +628,15 @@ export function createRunRunner(options) {
|
|
|
584
628
|
|
|
585
629
|
async function abandonEntry(entry, controller) {
|
|
586
630
|
if (entry.abandonPromise !== undefined) return entry.abandonPromise
|
|
631
|
+
const settled = entry.finished ? undefined : settledStatusOf(entry)
|
|
632
|
+
if (settled !== undefined) {
|
|
633
|
+
entry.abandoned = true
|
|
634
|
+
entry.abandonPromise = (async () => {
|
|
635
|
+
await finalize(entry, settled)
|
|
636
|
+
await stopEntryAgent(entry, controller)
|
|
637
|
+
})()
|
|
638
|
+
return entry.abandonPromise
|
|
639
|
+
}
|
|
587
640
|
entry.abandoned = true
|
|
588
641
|
entry.finished = true
|
|
589
642
|
clearTimers(entry)
|
|
@@ -603,7 +656,7 @@ export function createRunRunner(options) {
|
|
|
603
656
|
phase: 'session',
|
|
604
657
|
severity: 'warning',
|
|
605
658
|
code: 'unknown_after_restart',
|
|
606
|
-
message: '自动化 runtime
|
|
659
|
+
message: '自动化 runtime 结束时无法证明本轮运行已经结束,结果未知;为避免重复执行,未自动重试。',
|
|
607
660
|
}].slice(-MAX_ERRORS_PER_RUN),
|
|
608
661
|
}).catch(() => {})
|
|
609
662
|
}
|
|
@@ -662,6 +715,7 @@ export function createRunRunner(options) {
|
|
|
662
715
|
promptController: undefined,
|
|
663
716
|
summary: undefined,
|
|
664
717
|
lastTurnEnd: undefined,
|
|
718
|
+
turnActive: false,
|
|
665
719
|
promptAccepted: false,
|
|
666
720
|
finished: false,
|
|
667
721
|
completing: false,
|
package/automation/scheduler.js
CHANGED
|
@@ -140,7 +140,7 @@ export function createScheduler(options) {
|
|
|
140
140
|
recovered = true
|
|
141
141
|
try {
|
|
142
142
|
state.recoveredRuns = await store.recoverNonTerminalRuns()
|
|
143
|
-
if (state.recoveredRuns > 0) logger?.warn?.(`capital-automation: ${state.recoveredRuns}
|
|
143
|
+
if (state.recoveredRuns > 0) logger?.warn?.(`capital-automation: ${state.recoveredRuns} 个上次进程的运行已恢复收口;无回合边界的记 unknown_after_restart`)
|
|
144
144
|
} catch (error) {
|
|
145
145
|
state.lastError = String(error)
|
|
146
146
|
}
|
|
@@ -168,7 +168,7 @@ export function createScheduler(options) {
|
|
|
168
168
|
await store.addNotification({
|
|
169
169
|
kind: 'calendar_unavailable',
|
|
170
170
|
title: `交易日历不可用:${task.title}`.slice(0, 120),
|
|
171
|
-
message: '任务已按 fail-closed
|
|
171
|
+
message: '任务已按 fail-closed 跳过;日历更新后会自动恢复,持续出现请检查网络或交易所公告。',
|
|
172
172
|
runId: run.id,
|
|
173
173
|
taskId: task.id,
|
|
174
174
|
}).catch(() => {})
|
|
@@ -181,6 +181,9 @@ export function createScheduler(options) {
|
|
|
181
181
|
const latest = previousOccurrenceAtOrBefore(task.trigger, now(), { calendar })
|
|
182
182
|
if (latest.kind === 'none') return
|
|
183
183
|
if (latest.kind === 'calendar_unavailable') {
|
|
184
|
+
// A refresh already in flight can still answer today; burning the occurrence on a
|
|
185
|
+
// transient unknown would turn a slow fetch into a skipped run.
|
|
186
|
+
if (calendar?.pendingRefresh?.() === true) return
|
|
184
187
|
const key = `${store.profileKey}:calendar:${latest.localDate}`
|
|
185
188
|
if (task.lastHandledOccurrenceKey === key) return
|
|
186
189
|
await createSkipped(task, key, 'skipped_calendar_unavailable', undefined, {
|
|
@@ -292,6 +295,13 @@ export function createScheduler(options) {
|
|
|
292
295
|
tickInFlight = (async () => {
|
|
293
296
|
try {
|
|
294
297
|
if (!(await tryAcquire())) return
|
|
298
|
+
// Calendar refresh rides the tick but never blocks it; occurrence math reads the
|
|
299
|
+
// last good snapshot and keeps failing closed while a fetch is in flight.
|
|
300
|
+
try {
|
|
301
|
+
calendar?.refreshIfStale?.()
|
|
302
|
+
} catch (error) {
|
|
303
|
+
state.lastError = String(error)
|
|
304
|
+
}
|
|
295
305
|
if (timer !== undefined && currentTickMs !== Math.max(30_000, settings().tickMs)) armTick()
|
|
296
306
|
state.lastTickAt = iso(now())
|
|
297
307
|
const tasks = await store.listTasks()
|
|
@@ -318,6 +328,14 @@ export function createScheduler(options) {
|
|
|
318
328
|
}
|
|
319
329
|
|
|
320
330
|
async function start() {
|
|
331
|
+
if (stopped) return
|
|
332
|
+
if (typeof calendar?.ready === 'function') {
|
|
333
|
+
// Never start the first tick on a calendar that cannot answer today: a network fetch
|
|
334
|
+
// gets one bounded chance before fail-closed would claim the day's occurrence.
|
|
335
|
+
await calendar.ready().catch((error) => {
|
|
336
|
+
logger?.warn?.(`capital-automation: 交易日历初始化失败:${sanitizeMessage(String(error))}`)
|
|
337
|
+
})
|
|
338
|
+
}
|
|
321
339
|
if (stopped) return
|
|
322
340
|
await tick()
|
|
323
341
|
if (stopped) return
|
package/automation/schema.js
CHANGED
|
@@ -167,6 +167,21 @@ export const RunErrorSchema = z.strictObject({
|
|
|
167
167
|
callId: z.string().max(120).optional(),
|
|
168
168
|
})
|
|
169
169
|
|
|
170
|
+
/** Durable turn boundary: recovery must distinguish an interrupted turn from a finished one. */
|
|
171
|
+
export const TurnStateSchema = z.strictObject({
|
|
172
|
+
phase: z.enum(['running', 'ended']),
|
|
173
|
+
turn: z.number().int().min(0),
|
|
174
|
+
status: z.enum(RUN_STATUSES).optional(),
|
|
175
|
+
at: isoInstant,
|
|
176
|
+
}).superRefine((state, ctx) => {
|
|
177
|
+
if (state.phase === 'ended' && state.status === undefined) {
|
|
178
|
+
ctx.addIssue({ code: 'custom', message: 'ended turn state requires status' })
|
|
179
|
+
}
|
|
180
|
+
if (state.phase === 'running' && state.status !== undefined) {
|
|
181
|
+
ctx.addIssue({ code: 'custom', message: 'running turn state must not carry status' })
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
|
|
170
185
|
export const RunRecordSchema = z.strictObject({
|
|
171
186
|
schemaVersion: z.literal(1),
|
|
172
187
|
id: z.string().min(1).max(80),
|
|
@@ -180,6 +195,7 @@ export const RunRecordSchema = z.strictObject({
|
|
|
180
195
|
status: z.enum(RUN_STATUSES),
|
|
181
196
|
sessionId: z.string().min(1).max(120).optional(),
|
|
182
197
|
sessionTitle: z.string().min(1).max(200).optional(),
|
|
198
|
+
turnState: TurnStateSchema.optional(),
|
|
183
199
|
queuedAt: isoInstant,
|
|
184
200
|
startedAt: isoInstant.optional(),
|
|
185
201
|
finishedAt: isoInstant.optional(),
|
package/automation/store.js
CHANGED
|
@@ -23,9 +23,11 @@ import {
|
|
|
23
23
|
DEFAULT_MAX_STEPS,
|
|
24
24
|
DEFAULT_MAX_TOOL_CALLS,
|
|
25
25
|
DEFAULT_RUN_TIMEOUT_MINUTES,
|
|
26
|
+
MAX_ERRORS_PER_RUN,
|
|
26
27
|
MAX_NOTIFICATIONS_PER_PROFILE,
|
|
27
28
|
MAX_RUNS_PER_PROFILE,
|
|
28
29
|
MAX_RUNS_PER_TASK,
|
|
30
|
+
SETTLED_RUN_STATUSES,
|
|
29
31
|
} from './defaults.js'
|
|
30
32
|
import { canonicalTrigger, formatInZone, nextOccurrence } from './time.js'
|
|
31
33
|
|
|
@@ -591,33 +593,56 @@ export function createAutomationStore(options) {
|
|
|
591
593
|
return record?.profileKey === profileKey ? record : undefined
|
|
592
594
|
}
|
|
593
595
|
|
|
594
|
-
/**
|
|
596
|
+
/**
|
|
597
|
+
* Startup recovery: a durable turn boundary proves how the run ended. Only runs with no
|
|
598
|
+
* finished turn are left unknown (and never retried silently).
|
|
599
|
+
*/
|
|
595
600
|
async function recoverNonTerminalRuns() {
|
|
596
601
|
const tables = await ensure()
|
|
597
602
|
const at = iso(now())
|
|
603
|
+
const outcomes = new Map()
|
|
598
604
|
let recovered = 0
|
|
599
605
|
for (const [id, run] of [...tables.runs.entries()]) {
|
|
600
606
|
if (!ownRun(run) || !isActiveRun(run.status)) continue
|
|
607
|
+
const turn = run.turnState
|
|
608
|
+
const settled = turn?.phase === 'ended' && SETTLED_RUN_STATUSES.includes(turn.status) ? turn.status : undefined
|
|
609
|
+
const status = settled ?? 'unknown_after_restart'
|
|
610
|
+
const finishedAt = settled === undefined ? at : turn.at
|
|
611
|
+
const errors = settled === undefined
|
|
612
|
+
? [...run.errors, {
|
|
613
|
+
at: finishedAt,
|
|
614
|
+
phase: 'session',
|
|
615
|
+
severity: 'warning',
|
|
616
|
+
code: 'unknown_after_restart',
|
|
617
|
+
message: '进程重启时无法证明本轮运行已经结束,结果未知;为避免重复执行,未自动重试。',
|
|
618
|
+
}].slice(-MAX_ERRORS_PER_RUN)
|
|
619
|
+
: status === 'succeeded'
|
|
620
|
+
? run.errors
|
|
621
|
+
: [...run.errors, {
|
|
622
|
+
at: finishedAt,
|
|
623
|
+
phase: 'session',
|
|
624
|
+
severity: 'warning',
|
|
625
|
+
code: 'recovered_after_restart',
|
|
626
|
+
message: `进程重启前回合已结束(${status});按该终态收口。`,
|
|
627
|
+
}].slice(-MAX_ERRORS_PER_RUN)
|
|
601
628
|
const next = parseStrict(RunRecordSchema, {
|
|
602
629
|
...run,
|
|
603
|
-
status
|
|
604
|
-
finishedAt
|
|
605
|
-
durationMs: Math.max(0, Date.parse(
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
phase: 'session',
|
|
609
|
-
severity: 'warning',
|
|
610
|
-
code: 'unknown_after_restart',
|
|
611
|
-
message: '进程重启时无法证明本轮运行已经结束;未自动重试。',
|
|
612
|
-
}].slice(-20),
|
|
630
|
+
status,
|
|
631
|
+
finishedAt,
|
|
632
|
+
durationMs: Math.max(0, Date.parse(finishedAt) - Date.parse(run.startedAt ?? run.queuedAt)),
|
|
633
|
+
...(status === 'blocked_requires_user' ? { blockedRequiresUser: true } : {}),
|
|
634
|
+
errors,
|
|
613
635
|
}, 'run')
|
|
614
636
|
await tables.runs.put(id, next)
|
|
637
|
+
if (settled !== undefined) outcomes.set(id, status)
|
|
615
638
|
recovered += 1
|
|
616
639
|
}
|
|
617
640
|
for (const [id, attempt] of [...tables.attempts.entries()]) {
|
|
618
641
|
if (attempt.profileKey !== profileKey) continue
|
|
619
642
|
if (!['claimed', 'session_created', 'prompt_submitted'].includes(attempt.state)) continue
|
|
620
|
-
const
|
|
643
|
+
const status = outcomes.get(attempt.runId)
|
|
644
|
+
const state = status === undefined ? 'unknown' : status === 'failed' ? 'failed' : 'accepted'
|
|
645
|
+
const next = parseStrict(AttemptRecordSchema, { ...attempt, state, updatedAt: at }, 'attempt')
|
|
621
646
|
await tables.attempts.put(id, next)
|
|
622
647
|
}
|
|
623
648
|
return recovered
|
package/chart-ui/package.json
CHANGED
package/config/package.json
CHANGED
package/lib/sources/wind-mcp.js
CHANGED
|
@@ -462,8 +462,8 @@ function createWindSource(options) {
|
|
|
462
462
|
execute: (request, signal) => options.execute(client, normalize(request.params), signal),
|
|
463
463
|
};
|
|
464
464
|
}
|
|
465
|
-
const EDB_SEARCH_DESCRIPTION = 'Wind EDB(经济数据库)指标**检索**:只找指标不取数,一行一个候选指标(code/name/unit/freq/source + 该序列的起止与更新日期)。这是 fuyao 与东财都没有的面:宏观、行业、汇率、全球序列。⛔ question 走后端 NER,字面请求不等于字面结果——实测「人民币兑美元汇率 指标」回的是**美元兑人民币**(source 为 美联储 / 台湾统计局),先核对 name 与 source 再取数。键集随指标族变化:实测汇率类没有 unit/magnitude/currency 三个键,缺的一律 null
|
|
466
|
-
const EDB_QUERY_DESCRIPTION = 'Wind EDB 时间序列**取数**:上游回 {meta, date[], value[]} 并行数组,本能力转置成**一行一个观测期**(meta 逐行拼接,多指标可分次取后按 code 拼对比)。⛔ indicator 一次只填**一个**指标代码或一条中文问句:2026-10-06 实测逗号分隔的多码挂满 60 秒无应答,上游"多个代码用英文逗号分隔"的说法不成立,要多个就分次调用。start_date/end_date 必填——上游在日期区间与 observation 之间二选一,而 observation 实测"给 3 期只回 2 期"、期数不可信,本能力直接拒绝它。report_date 由上游紧凑串 20260731 归一而来,report_date_ms 按 Asia/Shanghai 零点(EDB 的日期本身没有时区)。unit 是上游单位原文;后端 INVALID 一律 null,**不得当 0
|
|
465
|
+
const EDB_SEARCH_DESCRIPTION = 'Wind EDB(经济数据库)指标**检索**:只找指标不取数,一行一个候选指标(code/name/unit/freq/source + 该序列的起止与更新日期)。这是 fuyao 与东财都没有的面:宏观、行业、汇率、全球序列。⛔ question 走后端 NER,字面请求不等于字面结果——实测「人民币兑美元汇率 指标」回的是**美元兑人民币**(source 为 美联储 / 台湾统计局),先核对 name 与 source 再取数。键集随指标族变化:实测汇率类没有 unit/magnitude/currency 三个键,缺的一律 null,不猜单位。⛔ 只在不确定指标叫什么、或需要候选清单时用:已知指标(美元指数、WTI 原油价格…)直接走 wind_edb_query 的中文问句,取数结果每行本来就带 code/name/source,不必先检索——两个能力都按次消耗上游积分,为已知指标多打一次 search 是纯浪费。没有匹配时返回 0 行,是合法空结果不是失败。';
|
|
466
|
+
const EDB_QUERY_DESCRIPTION = 'Wind EDB 时间序列**取数**:上游回 {meta, date[], value[]} 并行数组,本能力转置成**一行一个观测期**(meta 逐行拼接,多指标可分次取后按 code 拼对比)。⛔ indicator 一次只填**一个**指标代码或一条中文问句:2026-10-06 实测逗号分隔的多码挂满 60 秒无应答,上游"多个代码用英文逗号分隔"的说法不成立,要多个就分次调用。start_date/end_date 必填——上游在日期区间与 observation 之间二选一,而 observation 实测"给 3 期只回 2 期"、期数不可信,本能力直接拒绝它。report_date 由上游紧凑串 20260731 归一而来,report_date_ms 按 Asia/Shanghai 零点(EDB 的日期本身没有时区)。unit 是上游单位原文;后端 INVALID 一律 null,**不得当 0 参与计算**。已实测指标代码与同义中文问句回同一份序列(M0000612 = 中国 CPI 当月同比):indicator 直接填中文问句即可(官方口径就是自然语言取数),本能力按次消耗上游积分,已知指标不要先检索。中文问句可能命中同族多个指标(实测「黄金价格」回伦敦定盘 / COMEX / SGE 等 7 条),返回行自带 code/name/unit/source,按 name 与 source 取舍后再引用;只有不知道指标叫什么、需要候选清单时才用 wind_edb_search。';
|
|
467
467
|
const KLINE_DESCRIPTION = 'Wind K 线:单只标的按**日期区间**取 日/周/月 与 1/5/15/30 分钟。相对腾讯的增量(§6.6.1):按日期区间取 640 根之前的历史、每根带成交额/成交量/换手/均价的美股 K 线、以及分钟档;⚠️ **分钟档只有 A 股取到过行**,港股与美股的分钟到 2026-10-06 为止四次全挂、仍未证实,要港美股分时先小范围试一次。港美股日线与快照请走 tencent_*。⛔ code 必须自带交易所后缀(600519.SH / 00700.HK / AAPL.O),本能力不猜后缀——实测 AAPL.O 与 AAPL.N 回完全相同的行(后缀不参与解析),所以 BRK.B 一类双市场代码有认错标的的风险;每行的 code 只是请求写法回显,上游 K 线表里没有代码列。⛔ period 只放逐档打过真报文的七档,60 分钟实测两次都挂满超时、不提供。adjust 只有 none 与 qfq:后复权以 afdate 为锚向未来放大,锚默认成今天会得到一个逐位等于不复权却叫 hfq 的序列,所以宁可不给;qfq 的 afdate 由本能力锁成当日,因此 end_date 不得晚于当日;分钟档只允许 none。close 来自上游 MATCH 列(Wind 没有 CLOSE 列);上游多给的列不收录,行里以 output_fields 为准。数值列上游全是字符串(columns[].type 却一律写 string),解析不了即失败。time_ms 按**该行自带偏移**换算(A股/港股 +08:00、美股 -04:00 是交易所当地时间),整段按北京时间处理会让美股错位 12 小时。⚠️ volume 实测 A 股是**股**(腾讯 A 股 K 线是手,跨源先换);turnover_rate_pct 是百分数原值(0.2892 = 0.2892%),上游不给它的单位与分母口径。非交易日快回 0 行、是合法成功(实测 A 股国庆休市日 653ms 回空表);而"该日该市场分钟尚未落地"与上游故障都表现为挂到 12 秒超时(wind_upstream_timeout)——不要盲目重试同一区间,改更早日期或改走日线。积分计费:AUTH/RATE_LIMIT/后端错误一律不自动重试。';
|
|
468
468
|
export function createWindSources(resolveApiKey) {
|
|
469
469
|
return [
|
|
@@ -496,7 +496,7 @@ export function createWindSources(resolveApiKey) {
|
|
|
496
496
|
inputSchema: {
|
|
497
497
|
type: 'object',
|
|
498
498
|
properties: {
|
|
499
|
-
indicator: { type: 'string', maxLength: 200, description: '
|
|
499
|
+
indicator: { type: 'string', maxLength: 200, description: '一条中文指标问句(推荐,写清实体与口径,如「美元指数」「WTI 原油价格」),或一个已知的 EDB 指标代码(如 M0000612);一次只填一个,不接受逗号分隔的多个' },
|
|
500
500
|
start_date: { type: 'string', description: '起始日期 YYYY-MM-DD(含)' },
|
|
501
501
|
end_date: { type: 'string', description: '结束日期 YYYY-MM-DD(含)' },
|
|
502
502
|
},
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Known limit: DNS resolves per request but the IP is not pinned to the connection,
|
|
4
4
|
* so a rebinding window remains (same trade-off as dsh-search-first).
|
|
5
5
|
*/
|
|
6
|
-
export declare const LOCAL_FETCH_CLIENT_VERSION = "2.13.
|
|
6
|
+
export declare const LOCAL_FETCH_CLIENT_VERSION = "2.13.1";
|
|
7
7
|
export declare const LOCAL_FETCH_MAX_URL_LENGTH = 2048;
|
|
8
8
|
export type LocalFetchErrorCode = 'ABORTED' | 'TIMEOUT' | 'INVALID_URL' | 'BLOCKED_URL' | 'DNS' | 'REDIRECT' | 'HTTP' | 'TOO_LARGE' | 'UNSUPPORTED_CONTENT_TYPE';
|
|
9
9
|
export declare class LocalFetchError extends Error {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* so a rebinding window remains (same trade-off as dsh-search-first).
|
|
5
5
|
*/
|
|
6
6
|
import { htmlToMarkdown, stripInvisibleText } from './html-markdown.js';
|
|
7
|
-
export const LOCAL_FETCH_CLIENT_VERSION = '2.13.
|
|
7
|
+
export const LOCAL_FETCH_CLIENT_VERSION = '2.13.1';
|
|
8
8
|
export const LOCAL_FETCH_MAX_URL_LENGTH = 2048;
|
|
9
9
|
export class LocalFetchError extends Error {
|
|
10
10
|
code;
|
|
@@ -35,7 +35,7 @@ export interface WindCallResult {
|
|
|
35
35
|
code?: WindErrorCode;
|
|
36
36
|
}
|
|
37
37
|
/** Must match `package.json` version; a test pins the two together. */
|
|
38
|
-
export declare const WIND_CLIENT_VERSION = "2.13.
|
|
38
|
+
export declare const WIND_CLIENT_VERSION = "2.13.1";
|
|
39
39
|
export interface WindClient {
|
|
40
40
|
callTool(toolName: string, args: Record<string, unknown>, signal?: AbortSignal): Promise<WindCallResult>;
|
|
41
41
|
}
|
|
@@ -19,7 +19,7 @@ export const WIND_SERVER_ENDPOINTS = {
|
|
|
19
19
|
analytics_data: 'https://mcp.wind.com.cn/vserver_analytics_data/mcp/',
|
|
20
20
|
};
|
|
21
21
|
/** Must match `package.json` version; a test pins the two together. */
|
|
22
|
-
export const WIND_CLIENT_VERSION = '2.13.
|
|
22
|
+
export const WIND_CLIENT_VERSION = '2.13.1';
|
|
23
23
|
function errorText(error) {
|
|
24
24
|
return error instanceof Error ? error.message : String(error);
|
|
25
25
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@v587d/capital-generation",
|
|
3
|
-
"version": "2.13.
|
|
3
|
+
"version": "2.13.1",
|
|
4
4
|
"description": "Capital Generation:面向中国散户的 AI 投资助手插件(自动模式 Agent Preset)",
|
|
5
5
|
"icon": "assets/cg-icon.png",
|
|
6
6
|
"author": "v587d",
|
|
@@ -152,6 +152,7 @@
|
|
|
152
152
|
"smoke:fuyao": "node scripts/smoke-fuyao.mjs",
|
|
153
153
|
"smoke:wind": "node scripts/smoke-wind.mjs",
|
|
154
154
|
"smoke:local-fetch": "node scripts/smoke-local-fetch.mjs",
|
|
155
|
+
"smoke:calendar": "node scripts/smoke-calendar.mjs",
|
|
155
156
|
"contract:probe": "node scripts/contract-probe.mjs",
|
|
156
157
|
"docs:capabilities": "npm run build && node scripts/gen-capability-table.mjs",
|
|
157
158
|
"verify:sessions": "node scripts/verify-sessions.mjs",
|
package/watchlist/package.json
CHANGED