@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.
@@ -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']
@@ -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 { createTradingCalendar } from './calendar.js'
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')
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v587d/capital-automation",
3
- "version": "2.13.0",
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",
@@ -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 event.callId === 'string') {
406
- return `${sessionId}:${event.type}:${event.callId}`
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(event.turn) && Number.isSafeInteger(event.step)) {
409
- return `${sessionId}:step:${event.turn}:${event.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 event.callId === 'string' && typeof event.name === 'string') entry.toolNames.set(event.callId, event.name)
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 (event.message?.isError !== true) return
460
- const tool = entry.toolNames.get(event.callId)
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: event.error?.code ?? 'tool_error',
464
- message: event.error?.reason ?? '工具调用失败。',
505
+ code: data.error?.code ?? 'tool_error',
506
+ message: textOfMessage(data.message) || '工具调用失败。',
465
507
  ...(tool === undefined ? {} : { tool }),
466
- ...(typeof event.callId === 'string' ? { callId: event.callId } : {}),
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(event.message)
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: event.turn, reason: event.reason }
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,
@@ -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} 个上次进程的运行已标记 unknown_after_restart`)
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
@@ -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(),
@@ -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
- /** Startup recovery: no live Agent owns a previous-process run, so never retry it silently. */
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: 'unknown_after_restart',
604
- finishedAt: at,
605
- durationMs: Math.max(0, Date.parse(at) - Date.parse(run.queuedAt)),
606
- errors: [...run.errors, {
607
- at,
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 next = parseStrict(AttemptRecordSchema, { ...attempt, state: 'unknown', updatedAt: at }, 'attempt')
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v587d/capital-charts",
3
- "version": "2.13.0",
3
+ "version": "2.13.1",
4
4
  "description": "自动模式的图表前端组件(浏览器半边 + 序列旁路路由):由 @v587d/capital-generation 随包携带并作为 host 平面行挂载。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v587d/config",
3
- "version": "2.13.0",
3
+ "version": "2.13.1",
4
4
  "description": "自动模式的取数凭据与偏好配置行:卡片坐在 @v587d/capital-generation 插件主页的「配置」段,点这一行本身没有表单。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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,不猜单位。取数走 wind_edb_query:确定性靠"先 search 拿 code、再按 code 取数"这两步拿到,不是靠 NL 稳定。没有匹配时返回 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 参与计算**。已实测指标代码直接当 indicator 与同义中文问句回同一份序列(M0000612 = 中国 CPI 当月同比):先 wind_edb_search 拿码再回来,确定性就在这两步。';
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: '一个 EDB 指标代码(如 M0000612,先走 wind_edb_search 拿码)或一条中文指标问句;不接受逗号分隔的多个' },
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.0";
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.0';
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.0";
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.0';
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.0",
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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v587d/watchlist",
3
- "version": "2.13.0",
3
+ "version": "2.13.1",
4
4
  "description": "自动模式的用户自选股(宿主级清单 + Fuyao 报价快照 + /capital-watchlist 路由 + 输入框菜单弹窗):由 @v587d/capital-generation 随包携带并作为 host 平面行挂载。",
5
5
  "license": "MIT",
6
6
  "type": "module",