@v587d/capital-generation 2.14.1 → 2.15.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +137 -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 +37 -5
  7. package/automation/client.src.cjs +578 -126
  8. package/automation/defaults.js +4 -4
  9. package/automation/index.js +1 -0
  10. package/automation/package.json +4 -2
  11. package/automation/route.js +1 -1
  12. package/automation/runner.js +5 -0
  13. package/automation/scheduler.js +34 -29
  14. package/automation/schema.js +39 -5
  15. package/automation/service.js +13 -10
  16. package/automation/store.js +64 -21
  17. package/automation/task-icons.js +75 -0
  18. package/chart-ui/client.js +2 -2
  19. package/chart-ui/package.json +1 -1
  20. package/config/client.js +12 -5
  21. package/config/client.src.cjs +143 -45
  22. package/config/index.js +5 -5
  23. package/config/package.json +1 -1
  24. package/lib/chart/runtime.d.ts +1 -1
  25. package/lib/chart/runtime.js +95 -0
  26. package/lib/chart/series.d.ts +4 -4
  27. package/lib/chart/series.js +44 -10
  28. package/lib/index.js +5 -5
  29. package/lib/web-retriever/local-fetch.d.ts +1 -1
  30. package/lib/web-retriever/local-fetch.js +1 -1
  31. package/lib/web-retriever/sources.d.ts +4 -2
  32. package/lib/web-retriever/sources.js +73 -17
  33. package/lib/web-retriever/wind-client.d.ts +1 -1
  34. package/lib/web-retriever/wind-client.js +1 -1
  35. package/package.json +6 -6
  36. package/preset/capital-generation/README.md +1 -1
  37. package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +6 -3
  38. package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +2 -0
  39. package/watchlist/package.json +1 -1
@@ -66,12 +66,20 @@ const CREDENTIAL_FIELDS = ['fuyao', 'ftshare', 'anysearch', 'wind', 'paddleocr']
66
66
  const AUTOMATION_FIELDS = ['tickMs', 'runTimeoutMinutes', 'idleTimeoutMinutes', 'maxSteps', 'maxToolCalls', 'inAppNotifications']
67
67
  const AUTOMATION_DEFAULTS = {
68
68
  tickMs: 60_000,
69
- runTimeoutMinutes: 30,
69
+ runTimeoutMinutes: 60,
70
70
  idleTimeoutMinutes: 5,
71
71
  maxSteps: 300,
72
72
  maxToolCalls: 1_200,
73
73
  inAppNotifications: true,
74
74
  }
75
+ /** Mirrors `automation/defaults.js`; out-of-range values get an inline error instead of a silent clamp. */
76
+ const AUTOMATION_RANGES = {
77
+ tickMs: { min: 30_000, max: 3_600_000, unit: 'automationUnitMs' },
78
+ runTimeoutMinutes: { min: 5, max: 120, unit: 'automationUnitMinutes' },
79
+ idleTimeoutMinutes: { min: 1, max: 30, unit: 'automationUnitMinutes' },
80
+ maxSteps: { min: 100, max: 2_000, unit: 'automationUnitSteps' },
81
+ maxToolCalls: { min: 100, max: 5_000, unit: 'automationUnitCalls' },
82
+ }
75
83
  const SELECTED_SKILL_FIELDS = SELECTED_SKILL_CATALOG.map(({ key }) => key)
76
84
  // Stable orders: facet counts change with the other dimension, but chips must not jump under the cursor.
77
85
  const SELECTED_SKILL_TRAITS = selectedSkillTraitOrder()
@@ -102,9 +110,17 @@ const zh = {
102
110
  localFetchLabel: '允许启动本地提取网页内容',
103
111
  localFetchHint: '开启后本地抓取兜底(回执来源 local-http)。保存后新自动模式会话生效。',
104
112
  automationTitle: '投研自动化',
105
- automationHint: '调度器与无人值守运行的默认上限;任务详情里的设置优先。改动在下一个 tick 生效。',
106
- automationTick: '调度间隔(毫秒)',
107
- automationRunTimeout: 'wall-clock 上限(分钟)',
113
+ automationScheduleGroup: '调度',
114
+ automationRunGroup: '单次运行上限',
115
+ automationNotifyGroup: '通知',
116
+ automationInAppHint: '运行结束、失败或超时后,在应用内通知里留一条记录。',
117
+ automationTick: '自动检查间隔(毫秒)',
118
+ automationRunTimeout: '最长运行时间(分钟)',
119
+ automationInvalid: '请填写 {min}–{max} {unit}',
120
+ automationUnitMs: '毫秒',
121
+ automationUnitMinutes: '分钟',
122
+ automationUnitSteps: '步',
123
+ automationUnitCalls: '次',
108
124
  automationIdleTimeout: '空闲超时(分钟)',
109
125
  automationMaxSteps: '最大步骤',
110
126
  automationMaxToolCalls: '最大工具调用',
@@ -136,7 +152,9 @@ const zh = {
136
152
  save: '保存',
137
153
  saving: '保存中…',
138
154
  readOnly: '本部署的设置为只读。',
139
- saveFailed: '本部署没有接受这些值,已保留供你修改。',
155
+ // A rejected or non-landing write (stale page revision, transport hiccup). Plain words only:
156
+ // the typed value is still there, so the next action is retry, not a restart.
157
+ saveFailed: '没有保存成功,请再试一次。',
140
158
  unavailable: '该插件当前未加载,暂时无法配置。',
141
159
  githubLabel: 'GitHub 仓库',
142
160
  npmLabel: 'npm 包页面',
@@ -157,9 +175,17 @@ const en = {
157
175
  localFetchLabel: 'Allow local web page extraction',
158
176
  localFetchHint: 'When on, a failed fetch falls back to local direct fetching (receipt via: local-http). Takes effect in new Auto mode sessions.',
159
177
  automationTitle: 'Research automation',
160
- automationHint: 'Scheduler and unattended-run defaults; per-task limits win. Changes apply on the next tick.',
161
- automationTick: 'Tick interval (ms)',
162
- automationRunTimeout: 'Wall-clock limit (minutes)',
178
+ automationScheduleGroup: 'Scheduling',
179
+ automationRunGroup: 'Per-run limits',
180
+ automationNotifyGroup: 'Notifications',
181
+ automationInAppHint: 'Leave an in-app notification when a run finishes, fails, or times out.',
182
+ automationTick: 'Auto-check interval (ms)',
183
+ automationRunTimeout: 'Max run time (minutes)',
184
+ automationInvalid: 'Enter a whole number from {min} to {max} {unit}',
185
+ automationUnitMs: 'ms',
186
+ automationUnitMinutes: 'minutes',
187
+ automationUnitSteps: 'steps',
188
+ automationUnitCalls: 'calls',
163
189
  automationIdleTimeout: 'Idle timeout (minutes)',
164
190
  automationMaxSteps: 'Max steps',
165
191
  automationMaxToolCalls: 'Max tool calls',
@@ -191,7 +217,7 @@ const en = {
191
217
  save: 'Save',
192
218
  saving: 'Saving…',
193
219
  readOnly: 'This deployment stores settings read-only.',
194
- saveFailed: 'The deployment did not accept these values; they were left for you to correct.',
220
+ saveFailed: "That didn't save. Please try again.",
195
221
  unavailable: 'This plugin is not loaded, so it cannot be configured right now.',
196
222
  githubLabel: 'GitHub repository',
197
223
  npmLabel: 'npm package page',
@@ -221,6 +247,7 @@ function installStyles() {
221
247
  .capital-config-panel[hidden] { display: none; }
222
248
  /* Host rows section: positive :has rules only, so any mismatch leaves it visible. */
223
249
  [data-plugin-detail="${BUNDLE_NAME}"]:has(.capital-config-card[data-capital-config-tab="sources"]) section[data-plugin-rows] { display: none; }
250
+ [data-plugin-detail="${BUNDLE_NAME}"]:has(.capital-config-card[data-capital-config-tab="automation"]) section[data-plugin-rows] { display: none; }
224
251
  [data-plugin-detail="${BUNDLE_NAME}"]:has(.capital-config-card[data-capital-config-tab="skills"]) section[data-plugin-rows] { display: none; }
225
252
  /* Internal roles must stay declared for delegation; hide their user-selectable cards in Settings. */
226
253
  [data-agent-preset-id="capital-data-junior"],
@@ -230,8 +257,14 @@ function installStyles() {
230
257
  .capital-config-secret-row label { flex: 0 1 auto; }
231
258
  .capital-config-secret-row label ~ span { order: 1; margin-left: auto; }
232
259
  .capital-config-field { display: flex; flex-direction: column; gap: 6px; padding: 12px 0; border-top: 0.5px solid var(--dsw-alias-border-l2); }
260
+ .capital-config-automation { display: flex; flex-direction: column; gap: 16px; padding-top: 12px; }
261
+ .capital-config-automation-group { display: flex; flex-direction: column; gap: 8px; }
262
+ .capital-config-automation-group > .capital-config-group-label { color: var(--dsw-alias-label-secondary); font-size: 12px; font-weight: 500; line-height: 18px; }
263
+ .capital-config-number { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
264
+ .capital-config-number-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; min-width: 0; }
265
+ .capital-config-number-head .capital-config-write-failed { font-size: 11px; line-height: 16px; }
233
266
  .capital-config-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 8px; }
234
- .capital-config-field input[type="number"] { width: 100%; box-sizing: border-box; padding: 6px 8px; border: .5px solid var(--dsw-alias-border-l1); border-radius: 8px; background: var(--dsw-alias-bg-module-platform); color: var(--dsw-alias-label-primary); font: inherit; font-size: 12px; }
267
+ .capital-config-automation input[type="number"] { width: 100%; box-sizing: border-box; padding: 6px 8px; border: .5px solid var(--dsw-alias-border-l1); border-radius: 8px; background: var(--dsw-alias-bg-module-platform); color: var(--dsw-alias-label-primary); font: inherit; font-size: 12px; }
235
268
  .capital-config-selected-mode { padding: 12px 14px; border: 0.5px solid var(--dsw-alias-border-l1); border-radius: 10px; background: var(--dsw-alias-bg-layer-2); }
236
269
  .capital-config-switch-row { align-items: center; gap: 12px; display: flex; }
237
270
  .capital-config-switch-text { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 4px; }
@@ -303,6 +336,7 @@ class CapitalCardController {
303
336
  this.selectedSkillsModeFailed = false
304
337
  this.automationWriting = new Set()
305
338
  this.automationFailed = new Set()
339
+ this.automationInvalid = new Set()
306
340
  // Row count is external: null = unknown, never 0; fetch only after the card is shown.
307
341
  this.bundleRows = null
308
342
  this.rowsRequested = false
@@ -357,6 +391,7 @@ class CapitalCardController {
357
391
  on: snapshot.value?.automation?.[field] !== false,
358
392
  writing: this.automationWriting.has(field),
359
393
  failed: this.automationFailed.has(field),
394
+ invalid: this.automationInvalid.has(field),
360
395
  }])),
361
396
  // `count === null` means unknown, not zero: the card decides whether to draw it.
362
397
  components: { count: this.bundleRows },
@@ -471,18 +506,34 @@ class CapitalCardController {
471
506
  this.store.set(this.projection())
472
507
  }
473
508
 
509
+ /** Returns why the write did or did not land: the card restores the input only for `invalid`. */
474
510
  async writeAutomation(field, value) {
475
- if (!AUTOMATION_FIELDS.includes(field) || this.automationWriting.has(field)) return
511
+ if (!AUTOMATION_FIELDS.includes(field) || this.automationWriting.has(field)) return 'ignored'
476
512
  const snapshot = this.scope.getSnapshot()
477
- if (snapshot.status !== 'ready' || !snapshot.writable) return
478
- const next = field === 'inAppNotifications' ? value === true : Number(value)
479
- if (field !== 'inAppNotifications' && !Number.isFinite(next)) return
513
+ if (snapshot.status !== 'ready' || !snapshot.writable) return 'ignored'
514
+ let next
515
+ if (field === 'inAppNotifications') {
516
+ next = value === true
517
+ } else {
518
+ const { min, max } = AUTOMATION_RANGES[field]
519
+ const raw = typeof value === 'string' ? value.trim() : value
520
+ next = Number(raw)
521
+ if (raw === '' || raw === null || raw === undefined || !Number.isInteger(next) || next < min || next > max) {
522
+ // Point at the range and write nothing; the card puts the saved value back.
523
+ this.automationInvalid.add(field)
524
+ this.automationFailed.delete(field)
525
+ this.store.set(this.projection())
526
+ return 'invalid'
527
+ }
528
+ this.automationInvalid.delete(field)
529
+ }
480
530
  this.automationWriting.add(field)
481
531
  this.automationFailed.delete(field)
482
532
  this.store.set(this.projection())
483
533
  try {
484
- const current = this.scope.getSnapshot().value?.automation ?? { ...AUTOMATION_DEFAULTS }
485
- await this.scope.mutate([{ op: 'set', path: ['automation'], value: { ...AUTOMATION_DEFAULTS, ...current, [field]: next } }])
534
+ // Only leaves are volatile: writing the `automation` container is rejected by the settings
535
+ // service even though every child field is editable.
536
+ await this.scope.mutate([{ op: 'set', path: ['automation', field], value: next }])
486
537
  } catch {
487
538
  // The landed check below reports the failure.
488
539
  }
@@ -490,6 +541,12 @@ class CapitalCardController {
490
541
  const landed = this.scope.getSnapshot().value?.automation?.[field] ?? AUTOMATION_DEFAULTS[field]
491
542
  if (landed !== next) this.automationFailed.add(field)
492
543
  this.store.set(this.projection())
544
+ return landed === next ? 'saved' : 'failed'
545
+ }
546
+
547
+ clearAutomationInvalid(field) {
548
+ if (!this.automationInvalid.delete(field)) return
549
+ this.store.set(this.projection())
493
550
  }
494
551
 
495
552
  /**
@@ -533,6 +590,7 @@ class CapitalCardController {
533
590
  ...this.model.actions(),
534
591
  toggleLocalFetch: (value) => this.toggleLocalFetch(value),
535
592
  writeAutomation: (field, value) => this.writeAutomation(field, value),
593
+ clearAutomationInvalid: (field) => this.clearAutomationInvalid(field),
536
594
  toggleSelectedSkill: (field, value) => this.toggleSelectedSkill(field, value),
537
595
  toggleSelectedSkillsMode: (auto) => this.toggleSelectedSkillsMode(auto),
538
596
  ensureBundleRows: () => this.ensureBundleRows(),
@@ -789,37 +847,75 @@ function CapitalCard(props) {
789
847
  ),
790
848
  state.localFetch?.failed ? h('p', { role: 'status', className: 'capital-config-write-failed' }, t('saveFailed')) : null,
791
849
  )
792
- const automationNumber = (field, label, min, max) => h('label', { className: 'capital-config-field' },
793
- h('span', { className: 'capital-config-switch-label' }, t(label)),
794
- h('input', {
795
- type: 'number',
796
- min,
797
- max,
798
- defaultValue: state.automation?.[field]?.value,
799
- disabled: disabled || state.saving,
800
- onBlur: (event) => props.writeAutomation(field, event.target.value),
801
- }),
802
- state.automation?.[field]?.failed ? h('p', { role: 'status', className: 'capital-config-write-failed' }, t('saveFailed')) : null,
803
- )
804
- const automationSection = h('div', { className: 'capital-config-field' },
805
- h('span', { className: 'capital-config-switch-label' }, t('automationTitle')),
806
- h('p', { className: 'capital-config-hint' }, t('automationHint')),
807
- h('div', { className: 'capital-config-grid' },
808
- automationNumber('tickMs', 'automationTick', 30000, 3600000),
809
- automationNumber('runTimeoutMinutes', 'automationRunTimeout', 5, 120),
810
- automationNumber('idleTimeoutMinutes', 'automationIdleTimeout', 1, 30),
811
- automationNumber('maxSteps', 'automationMaxSteps', 10, 2000),
812
- automationNumber('maxToolCalls', 'automationMaxToolCalls', 10, 6000),
813
- ),
814
- h('label', { className: 'capital-config-switch-row' },
815
- h('span', { className: 'capital-config-switch-label' }, t('automationInApp')),
850
+ const automationNumber = (field, label) => {
851
+ const { min, max, unit } = AUTOMATION_RANGES[field]
852
+ const stored = state.automation?.[field]?.value
853
+ const invalid = state.automation?.[field]?.invalid === true
854
+ const error = invalid
855
+ ? t('automationInvalid', { min, max, unit: t(unit) })
856
+ : state.automation?.[field]?.failed ? t('saveFailed') : null
857
+ return h('label', { className: 'capital-config-number' },
858
+ // Error sits on the title row (the card's usual spot), not under the box.
859
+ h('span', { className: 'capital-config-number-head' },
860
+ h('span', { className: 'capital-config-switch-label' }, t(label)),
861
+ error ? h('span', {
862
+ id: `capital-config-automation-${field}-error`,
863
+ role: invalid ? 'alert' : 'status',
864
+ className: 'capital-config-write-failed',
865
+ }, error) : null,
866
+ ),
816
867
  h('input', {
817
- type: 'checkbox',
818
- checked: state.automation?.inAppNotifications?.on !== false,
868
+ type: 'number',
869
+ min,
870
+ max,
871
+ step: 1,
872
+ defaultValue: stored,
819
873
  disabled: disabled || state.saving,
820
- onChange: (event) => props.writeAutomation('inAppNotifications', event.target.checked),
874
+ 'aria-invalid': invalid ? 'true' : undefined,
875
+ 'aria-describedby': error ? `capital-config-automation-${field}-error` : undefined,
876
+ onFocus: () => props.clearAutomationInvalid(field),
877
+ onBlur: (event) => {
878
+ const input = event.target
879
+ return Promise.resolve(props.writeAutomation(field, input.value)).then((outcome) => {
880
+ if (outcome === 'invalid' && stored !== undefined) input.value = String(stored)
881
+ })
882
+ },
821
883
  }),
822
- ),
884
+ )
885
+ }
886
+ const automationGroup = (labelKey, children) => h('div', { className: 'capital-config-automation-group' },
887
+ h('span', { className: 'capital-config-group-label' }, t(labelKey)),
888
+ ...children,
889
+ )
890
+ const automationSection = h('div', { className: 'capital-config-automation' },
891
+ automationGroup('automationScheduleGroup', [
892
+ h('div', { className: 'capital-config-grid' },
893
+ automationNumber('tickMs', 'automationTick'),
894
+ ),
895
+ ]),
896
+ automationGroup('automationRunGroup', [
897
+ h('div', { className: 'capital-config-grid' },
898
+ automationNumber('runTimeoutMinutes', 'automationRunTimeout'),
899
+ automationNumber('idleTimeoutMinutes', 'automationIdleTimeout'),
900
+ automationNumber('maxSteps', 'automationMaxSteps'),
901
+ automationNumber('maxToolCalls', 'automationMaxToolCalls'),
902
+ ),
903
+ ]),
904
+ automationGroup('automationNotifyGroup', [
905
+ h('div', { className: 'capital-config-switch-row' },
906
+ h('div', { className: 'capital-config-switch-text' },
907
+ h('span', { className: 'capital-config-switch-label' }, t('automationInApp')),
908
+ h('p', { className: 'capital-config-hint' }, t('automationInAppHint')),
909
+ ),
910
+ h(Switch, {
911
+ label: t('automationInApp'),
912
+ checked: state.automation?.inAppNotifications?.on !== false,
913
+ disabled: disabled || state.saving || state.automation?.inAppNotifications?.writing === true,
914
+ onChange: (value) => props.writeAutomation('inAppNotifications', value),
915
+ }),
916
+ ),
917
+ state.automation?.inAppNotifications?.failed ? h('p', { role: 'status', className: 'capital-config-write-failed' }, t('saveFailed')) : null,
918
+ ]),
823
919
  )
824
920
  const modeSection = h('div', { className: 'capital-config-selected-mode' },
825
921
  h('div', { className: 'capital-config-switch-row' },
@@ -889,6 +985,7 @@ function CapitalCard(props) {
889
985
  const options = [
890
986
  // A dirty draft while Save is off-screen must be announced on the tab.
891
987
  { value: 'sources', label: state.dirty ? t('titleUnsaved') : t('title') },
988
+ { value: 'automation', label: t('automationTitle') },
892
989
  { value: 'skills', label: t('selectedSkillsTitle') },
893
990
  { value: 'components', label: withCount(t('componentsTitle'), state.components?.count) },
894
991
  ]
@@ -915,7 +1012,8 @@ function CapitalCard(props) {
915
1012
  if (next === 'skills') setPinnedSkills(enabledKeys())
916
1013
  },
917
1014
  }),
918
- panel('sources', [form, localFetchSection, automationSection]),
1015
+ panel('sources', [form, localFetchSection]),
1016
+ panel('automation', [automationSection]),
919
1017
  panel('skills', autoMode
920
1018
  ? [modeSection]
921
1019
  : [modeSection, h('div', { className: 'capital-config-skills-browser' },
package/config/index.js CHANGED
@@ -77,15 +77,15 @@ export const Config = z.object({
77
77
  fuyaoCredentialRef: z.string().default('FUYAO_API_KEY').volatile().description('Fuyao credentials 引用名'),
78
78
  ftshareCredentialRef: z.string().default('FTSHARE_API_KEY').volatile().description('FTShare(非凸科技)credentials 引用名'),
79
79
  automation: z.object({
80
- tickMs: z.number().default(60_000).volatile().description('投研自动化调度间隔毫秒(30000–3600000)'),
81
- runTimeoutMinutes: z.number().default(30).volatile().description('单次运行 wall-clock 上限分钟(5–120)'),
80
+ tickMs: z.number().default(60_000).volatile().description('投研自动化自动检查间隔(毫秒,30000–3600000)'),
81
+ runTimeoutMinutes: z.number().default(60).volatile().description('单次运行最长运行时间(分钟,5–120)'),
82
82
  idleTimeoutMinutes: z.number().default(5).volatile().description('单次运行空闲超时分钟(1–30)'),
83
- maxSteps: z.number().default(300).volatile().description('单次运行最大步骤数(10–2000)'),
84
- maxToolCalls: z.number().default(1_200).volatile().description('单次运行最大工具调用数(10–6000)'),
83
+ maxSteps: z.number().default(300).volatile().description('单次运行最大步骤数(100–2000)'),
84
+ maxToolCalls: z.number().default(1_200).volatile().description('单次运行最大工具调用数(100–5000)'),
85
85
  inAppNotifications: z.boolean().default(true).volatile().description('是否写入应用内通知'),
86
86
  }).default({
87
87
  tickMs: 60_000,
88
- runTimeoutMinutes: 30,
88
+ runTimeoutMinutes: 60,
89
89
  idleTimeoutMinutes: 5,
90
90
  maxSteps: 300,
91
91
  maxToolCalls: 1_200,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v587d/config",
3
- "version": "2.14.1",
3
+ "version": "2.15.1",
4
4
  "description": "自动模式的取数凭据与偏好配置行:卡片坐在 @v587d/capital-generation 插件主页的「配置」段,点这一行本身没有表单。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -4,4 +4,4 @@
4
4
  * back to `globalThis.LightweightCharts` — another plugin's copy could silently win. The IIFE
5
5
  * returns `{ render, version }`; nothing is attached to globals.
6
6
  */
7
- export declare const CHART_RUNTIME_SOURCE = "\n(function () {\n 'use strict'\n\n var FALLBACK = {\n '--dsw-alias-bg-base': '#ffffff',\n '--dsw-alias-border-l1': 'rgba(0, 0, 0, 0.10)',\n '--dsw-alias-label-primary': '#1f2328',\n '--dsw-alias-label-secondary': '#6b7280',\n '--dsw-alias-brand-primary': '#2563eb',\n '--dsw-alias-state-error-primary': '#d94a3d',\n '--dsw-alias-state-success-primary': '#2f9e44'\n }\n\n // Hex colors receive alpha; other formats convert to rgba or stay unchanged, never emit invalid CSS.\n function withAlpha(color, alpha) {\n var text = String(color || '').trim()\n var hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(text)\n if (hex) {\n var expanded = hex[1].length === 3\n ? '#' + hex[1].split('').map(function (part) { return part + part }).join('')\n : text\n return expanded + Math.round(alpha * 255).toString(16).padStart(2, '0')\n }\n var rgb = /^rgba?\\(\\s*([^)]*)\\)$/i.exec(text)\n if (rgb) {\n var parts = rgb[1].split(/[,\\s/]+/).filter(Boolean)\n if (parts.length >= 3 && parts.slice(0, 3).every(function (part) { return /^[-+]?\\d*\\.?\\d+%?$/.test(part) })) {\n return 'rgba(' + parts.slice(0, 3).join(', ') + ', ' + alpha + ')'\n }\n }\n return text\n }\n\n // First series uses brand color; later series use a stable palette so one symbol keeps its color.\n var PALETTE = ['#f59e0b', '#10b981', '#8b5cf6', '#0ea5e9', '#ec4899']\n\n function readToken(name) {\n try {\n var value = getComputedStyle(document.documentElement).getPropertyValue(name)\n if (value && value.trim().length > 0) return value.trim()\n } catch (error) {\n /* Non-browser or unavailable styles fall back to defaults. */\n }\n return FALLBACK[name]\n }\n\n function resolveColors() {\n return {\n bg: readToken('--dsw-alias-bg-base'),\n grid: readToken('--dsw-alias-border-l1'),\n text: readToken('--dsw-alias-label-secondary'),\n strong: readToken('--dsw-alias-label-primary'),\n brand: readToken('--dsw-alias-brand-primary'),\n up: readToken('--dsw-alias-state-error-primary'),\n down: readToken('--dsw-alias-state-success-primary')\n }\n }\n\n function showError(container, message) {\n container.textContent = ''\n var box = document.createElement('div')\n box.setAttribute('data-chart-error', '1')\n box.style.cssText = 'padding:12px;border:0.5px solid ' + readToken('--dsw-alias-border-l1') + ';border-radius:8px;color:' + readToken('--dsw-alias-label-secondary') + ';font-size:13px;line-height:1.6'\n box.textContent = message\n container.appendChild(box)\n }\n\n function applyIndexLabels(chart, labels) {\n function format(time) {\n var key = String(time)\n return Object.prototype.hasOwnProperty.call(labels, key) ? labels[key] : key\n }\n // Replace both tick and crosshair formatters for category axes or synthetic dates leak through.\n chart.applyOptions({\n localization: { timeFormatter: format },\n timeScale: { tickMarkFormatter: function (time) { return format(time) } }\n })\n }\n\n /** Support both v5 plugin markers and the older series API so markers never disappear silently. */\n function attachMarkers(anchor, markers, LWC) {\n if (typeof LWC.createSeriesMarkers === 'function') {\n LWC.createSeriesMarkers(anchor, markers)\n return true\n }\n if (anchor && typeof anchor.setMarkers === 'function') {\n anchor.setMarkers(markers)\n return true\n }\n return false\n }\n\n function volumePointColor(point, colors) {\n if (point.direction === 'up') return colors.up\n if (point.direction === 'down') return colors.down\n return colors.brand\n }\n\n function seriesColor(index, colors) {\n return index === 0 ? colors.brand : PALETTE[(index - 1) % PALETTE.length]\n }\n\n function appendLegend(container, payload, colors) {\n var items = []\n if (payload.ohlc && payload.ohlc.label) items.push({ label: payload.ohlc.label, color: colors.up })\n if (Array.isArray(payload.series)) {\n payload.series.forEach(function (item, index) {\n if (item && item.label) items.push({ label: item.label, color: seriesColor(index, colors) })\n })\n }\n if (payload.volume && payload.volume.label) items.push({ label: payload.volume.label, color: colors.brand })\n if (items.length === 0) return\n\n var legend = document.createElement('div')\n legend.setAttribute('data-chart-legend', '1')\n legend.style.cssText = 'position:absolute;z-index:2;top:8px;left:8px;right:8px;display:flex;flex-wrap:wrap;gap:4px 10px;align-items:center;pointer-events:none;color:' + colors.text + ';font-size:11px;line-height:16px'\n items.forEach(function (item) {\n var entry = document.createElement('span')\n entry.style.cssText = 'display:inline-flex;align-items:center;gap:4px;padding:1px 5px;border-radius:4px;background:' + withAlpha(colors.bg, 0.8)\n var swatch = document.createElement('span')\n swatch.style.cssText = 'width:7px;height:7px;border-radius:50%;background:' + item.color + ';flex:none'\n var label = document.createElement('span')\n label.textContent = item.label\n entry.appendChild(swatch)\n entry.appendChild(label)\n legend.appendChild(entry)\n })\n container.appendChild(legend)\n }\n\n function renderCapitalChart(container, payload, library) {\n if (!container) throw new Error('renderCapitalChart: container is required')\n if (!payload || typeof payload !== 'object') throw new Error('renderCapitalChart: payload is required')\n // Use the injected library; never fall back to a global copy owned by another plugin.\n var LWC = library\n if (!LWC || typeof LWC.createChart !== 'function') {\n showError(container, '\u56FE\u8868\u5E93\u672A\u52A0\u8F7D\uFF08\u8C03\u7528\u65B9\u672A\u63D0\u4F9B LightweightCharts\uFF09\u3002')\n return null\n }\n\n var colors = resolveColors()\n container.textContent = ''\n if (container.style) container.style.position = 'relative'\n container.setAttribute('data-chart-id', String(payload.chart_id || ''))\n container.setAttribute('data-chart-kind', String(payload.kind || ''))\n\n var chart = LWC.createChart(container, {\n autoSize: true,\n // attributionLogo stays enabled for Apache-2.0 attribution.\n layout: { background: { color: 'transparent' }, textColor: colors.text, fontFamily: 'inherit', attributionLogo: true },\n grid: { vertLines: { color: colors.grid }, horzLines: { color: colors.grid } },\n rightPriceScale: { borderColor: colors.grid, entireTextOnly: true },\n timeScale: { borderColor: colors.grid, rightOffset: 2, barSpacing: 6, minBarSpacing: 0.5 },\n crosshair: { mode: LWC.CrosshairMode ? LWC.CrosshairMode.Normal : 0 },\n localization: { locale: 'zh-CN' }\n })\n\n if (payload.axis === 'index' && payload.labels) applyIndexLabels(chart, payload.labels)\n\n var anchor = null\n\n if (payload.ohlc && Array.isArray(payload.ohlc.data) && payload.ohlc.data.length > 0) {\n // kind=bar uses BarSeries; other OHLC kinds use CandlestickSeries with the same data contract.\n var isOhlcBar = payload.kind === 'bar'\n var candles = chart.addSeries(isOhlcBar ? LWC.BarSeries : LWC.CandlestickSeries, {\n upColor: colors.up,\n downColor: colors.down,\n borderUpColor: colors.up,\n borderDownColor: colors.down,\n wickUpColor: colors.up,\n wickDownColor: colors.down\n })\n candles.setData(payload.ohlc.data)\n anchor = candles\n }\n\n if (Array.isArray(payload.series)) {\n for (var index = 0; index < payload.series.length; index += 1) {\n var item = payload.series[index]\n if (!item || !Array.isArray(item.data) || item.data.length === 0) continue\n var color = seriesColor(index, colors)\n var options = { title: item.label || '', priceLineVisible: false, lastValueVisible: true }\n var series = null\n if (item.type === 'area') {\n options.lineColor = color\n options.topColor = withAlpha(color, 0.2)\n options.bottomColor = withAlpha(color, 0.02)\n options.lineWidth = 2\n series = chart.addSeries(LWC.AreaSeries, options)\n } else if (item.type === 'column') {\n // Single-value histograms need HistogramSeries; BarSeries would render an empty chart.\n options.color = color\n options.base = 0\n series = chart.addSeries(LWC.HistogramSeries, options)\n } else {\n options.color = color\n options.lineWidth = 2\n series = chart.addSeries(LWC.LineSeries, options)\n }\n series.setData(item.data)\n if (anchor === null) anchor = series\n }\n }\n\n if (payload.volume && Array.isArray(payload.volume.data) && payload.volume.data.length > 0) {\n var volume = chart.addSeries(LWC.HistogramSeries, {\n title: payload.volume.label || '',\n priceFormat: { type: 'volume' },\n priceLineVisible: false,\n lastValueVisible: false\n }, 1)\n volume.setData(payload.volume.data.map(function (point) {\n return { time: point.time, value: point.value, color: volumePointColor(point, colors) }\n }))\n }\n\n if (anchor === null) {\n showError(container, '\u8FD9\u5F20\u56FE\u6CA1\u6709\u53EF\u6E32\u67D3\u7684\u5E8F\u5217\uFF08payload \u91CC\u65E2\u6CA1\u6709 ohlc \u4E5F\u6CA1\u6709 series\uFF09\u3002')\n return chart\n }\n\n if (Array.isArray(payload.markers) && payload.markers.length > 0) {\n attachMarkers(anchor, payload.markers.map(function (marker) {\n return { time: marker.time, position: marker.position, text: marker.text, color: colors.strong, shape: 'circle' }\n }), LWC)\n }\n\n appendLegend(container, payload, colors)\n\n // Main/volume panes use a 3:1 ratio; a single pane has no second pane object.\n var panes = typeof chart.panes === 'function' ? chart.panes() : []\n if (panes.length > 1) {\n if (typeof panes[0].setStretchFactor === 'function') panes[0].setStretchFactor(3)\n if (typeof panes[1].setStretchFactor === 'function') panes[1].setStretchFactor(1)\n }\n\n chart.timeScale().fitContent()\n return chart\n }\n\n return { render: renderCapitalChart, version: 1 }\n})()\n";
7
+ export declare const CHART_RUNTIME_SOURCE = "\n(function () {\n 'use strict'\n\n var FALLBACK = {\n '--dsw-alias-bg-base': '#ffffff',\n '--dsw-alias-border-l1': 'rgba(0, 0, 0, 0.10)',\n '--dsw-alias-label-primary': '#1f2328',\n '--dsw-alias-label-secondary': '#6b7280',\n '--dsw-alias-brand-primary': '#2563eb',\n '--dsw-alias-state-error-primary': '#d94a3d',\n '--dsw-alias-state-success-primary': '#2f9e44'\n }\n\n // Hex colors receive alpha; other formats convert to rgba or stay unchanged, never emit invalid CSS.\n function withAlpha(color, alpha) {\n var text = String(color || '').trim()\n var hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(text)\n if (hex) {\n var expanded = hex[1].length === 3\n ? '#' + hex[1].split('').map(function (part) { return part + part }).join('')\n : text\n return expanded + Math.round(alpha * 255).toString(16).padStart(2, '0')\n }\n var rgb = /^rgba?\\(\\s*([^)]*)\\)$/i.exec(text)\n if (rgb) {\n var parts = rgb[1].split(/[,\\s/]+/).filter(Boolean)\n if (parts.length >= 3 && parts.slice(0, 3).every(function (part) { return /^[-+]?\\d*\\.?\\d+%?$/.test(part) })) {\n return 'rgba(' + parts.slice(0, 3).join(', ') + ', ' + alpha + ')'\n }\n }\n return text\n }\n\n // First series uses brand color; later series use a stable palette so one symbol keeps its color.\n var PALETTE = ['#f59e0b', '#10b981', '#8b5cf6', '#0ea5e9', '#ec4899']\n\n function readToken(name) {\n try {\n var value = getComputedStyle(document.documentElement).getPropertyValue(name)\n if (value && value.trim().length > 0) return value.trim()\n } catch (error) {\n /* Non-browser or unavailable styles fall back to defaults. */\n }\n return FALLBACK[name]\n }\n\n function resolveColors() {\n return {\n bg: readToken('--dsw-alias-bg-base'),\n grid: readToken('--dsw-alias-border-l1'),\n text: readToken('--dsw-alias-label-secondary'),\n strong: readToken('--dsw-alias-label-primary'),\n brand: readToken('--dsw-alias-brand-primary'),\n up: readToken('--dsw-alias-state-error-primary'),\n down: readToken('--dsw-alias-state-success-primary')\n }\n }\n\n function showError(container, message) {\n container.textContent = ''\n var box = document.createElement('div')\n box.setAttribute('data-chart-error', '1')\n box.style.cssText = 'padding:12px;border:0.5px solid ' + readToken('--dsw-alias-border-l1') + ';border-radius:8px;color:' + readToken('--dsw-alias-label-secondary') + ';font-size:13px;line-height:1.6'\n box.textContent = message\n container.appendChild(box)\n }\n\n function applyIndexLabels(chart, labels) {\n function format(time) {\n var key = String(time)\n return Object.prototype.hasOwnProperty.call(labels, key) ? labels[key] : key\n }\n // Replace both tick and crosshair formatters for category axes or synthetic dates leak through.\n chart.applyOptions({\n localization: { timeFormatter: format },\n timeScale: { tickMarkFormatter: function (time) { return format(time) } }\n })\n }\n\n var CHART_TIME_ZONE = 'Asia/Shanghai'\n var CHART_DAY_SECONDS = 86400\n var CHART_LOCAL_MIDNIGHT_UTC = 57600\n\n function pad2(value) {\n var text = String(value)\n return text.length < 2 ? '0' + text : text\n }\n\n function timeLabel(time) {\n if (typeof time === 'string') return time\n if (time && typeof time === 'object') {\n var year = time.year\n var month = time.month\n var day = time.day\n if (year !== undefined && month !== undefined && day !== undefined) {\n return String(year) + '-' + pad2(month) + '-' + pad2(day)\n }\n }\n return String(time)\n }\n\n function collectNumericTimes(payload) {\n var times = []\n function add(list) {\n if (!Array.isArray(list)) return\n for (var i = 0; i < list.length; i += 1) {\n var point = list[i]\n if (point && typeof point.time === 'number' && isFinite(point.time)) times.push(point.time)\n }\n }\n if (payload.ohlc) add(payload.ohlc.data)\n if (Array.isArray(payload.series)) {\n for (var i = 0; i < payload.series.length; i += 1) add(payload.series[i] && payload.series[i].data)\n }\n if (payload.volume) add(payload.volume.data)\n return times\n }\n\n // Numeric epochs are true instants; render them in Asia/Shanghai. Business-day strings keep\n // their own labels, so no formatter is applied when every point is a date string.\n function makeTimeAxisFormatter(payload) {\n var times = collectNumericTimes(payload)\n if (times.length === 0) return null\n var min = times[0]\n var max = times[0]\n var daily = true\n for (var i = 0; i < times.length; i += 1) {\n if (times[i] < min) min = times[i]\n if (times[i] > max) max = times[i]\n var mod = ((times[i] % CHART_DAY_SECONDS) + CHART_DAY_SECONDS) % CHART_DAY_SECONDS\n if (mod !== 0 && mod !== CHART_LOCAL_MIDNIGHT_UTC) daily = false\n }\n var span = max - min\n var formatter = new Intl.DateTimeFormat('en-CA', {\n timeZone: CHART_TIME_ZONE,\n year: 'numeric', month: '2-digit', day: '2-digit',\n hour: '2-digit', minute: '2-digit', hourCycle: 'h23'\n })\n function parts(seconds) {\n var values = formatter.formatToParts(new Date(seconds * 1000))\n var out = {}\n for (var j = 0; j < values.length; j += 1) out[values[j].type] = values[j].value\n return out\n }\n function tick(time) {\n if (typeof time !== 'number') return timeLabel(time)\n var p = parts(time)\n if (daily) {\n if (span > 3 * 365 * CHART_DAY_SECONDS) return p.year\n if (span > 120 * CHART_DAY_SECONDS) return p.year + '-' + p.month\n return p.month + '-' + p.day\n }\n if (span > 2 * CHART_DAY_SECONDS) return p.month + '-' + p.day + ' ' + p.hour + ':' + p.minute\n return p.hour + ':' + p.minute\n }\n function full(time) {\n if (typeof time !== 'number') return timeLabel(time)\n var p = parts(time)\n return daily\n ? p.year + '-' + p.month + '-' + p.day\n : p.year + '-' + p.month + '-' + p.day + ' ' + p.hour + ':' + p.minute\n }\n return { tick: tick, full: full }\n }\n\n /** Support both v5 plugin markers and the older series API so markers never disappear silently. */\n function attachMarkers(anchor, markers, LWC) {\n if (typeof LWC.createSeriesMarkers === 'function') {\n LWC.createSeriesMarkers(anchor, markers)\n return true\n }\n if (anchor && typeof anchor.setMarkers === 'function') {\n anchor.setMarkers(markers)\n return true\n }\n return false\n }\n\n function volumePointColor(point, colors) {\n if (point.direction === 'up') return colors.up\n if (point.direction === 'down') return colors.down\n return colors.brand\n }\n\n function seriesColor(index, colors) {\n return index === 0 ? colors.brand : PALETTE[(index - 1) % PALETTE.length]\n }\n\n function appendLegend(container, payload, colors) {\n var items = []\n if (payload.ohlc && payload.ohlc.label) items.push({ label: payload.ohlc.label, color: colors.up })\n if (Array.isArray(payload.series)) {\n payload.series.forEach(function (item, index) {\n if (item && item.label) items.push({ label: item.label, color: seriesColor(index, colors) })\n })\n }\n if (payload.volume && payload.volume.label) items.push({ label: payload.volume.label, color: colors.brand })\n if (items.length === 0) return\n\n var legend = document.createElement('div')\n legend.setAttribute('data-chart-legend', '1')\n legend.style.cssText = 'position:absolute;z-index:2;top:8px;left:8px;right:8px;display:flex;flex-wrap:wrap;gap:4px 10px;align-items:center;pointer-events:none;color:' + colors.text + ';font-size:11px;line-height:16px'\n items.forEach(function (item) {\n var entry = document.createElement('span')\n entry.style.cssText = 'display:inline-flex;align-items:center;gap:4px;padding:1px 5px;border-radius:4px;background:' + withAlpha(colors.bg, 0.8)\n var swatch = document.createElement('span')\n swatch.style.cssText = 'width:7px;height:7px;border-radius:50%;background:' + item.color + ';flex:none'\n var label = document.createElement('span')\n label.textContent = item.label\n entry.appendChild(swatch)\n entry.appendChild(label)\n legend.appendChild(entry)\n })\n container.appendChild(legend)\n }\n\n function renderCapitalChart(container, payload, library) {\n if (!container) throw new Error('renderCapitalChart: container is required')\n if (!payload || typeof payload !== 'object') throw new Error('renderCapitalChart: payload is required')\n // Use the injected library; never fall back to a global copy owned by another plugin.\n var LWC = library\n if (!LWC || typeof LWC.createChart !== 'function') {\n showError(container, '\u56FE\u8868\u5E93\u672A\u52A0\u8F7D\uFF08\u8C03\u7528\u65B9\u672A\u63D0\u4F9B LightweightCharts\uFF09\u3002')\n return null\n }\n\n var colors = resolveColors()\n container.textContent = ''\n if (container.style) container.style.position = 'relative'\n container.setAttribute('data-chart-id', String(payload.chart_id || ''))\n container.setAttribute('data-chart-kind', String(payload.kind || ''))\n\n var chart = LWC.createChart(container, {\n autoSize: true,\n // attributionLogo stays enabled for Apache-2.0 attribution.\n layout: { background: { color: 'transparent' }, textColor: colors.text, fontFamily: 'inherit', attributionLogo: true },\n grid: { vertLines: { color: colors.grid }, horzLines: { color: colors.grid } },\n rightPriceScale: { borderColor: colors.grid, entireTextOnly: true },\n timeScale: { borderColor: colors.grid, rightOffset: 2, barSpacing: 6, minBarSpacing: 0.5 },\n crosshair: { mode: LWC.CrosshairMode ? LWC.CrosshairMode.Normal : 0 },\n localization: { locale: 'zh-CN' }\n })\n\n if (payload.axis === 'index' && payload.labels) applyIndexLabels(chart, payload.labels)\n if (payload.axis === 'time') {\n var timeAxis = makeTimeAxisFormatter(payload)\n if (timeAxis) {\n chart.applyOptions({\n localization: { timeFormatter: timeAxis.full },\n timeScale: { tickMarkFormatter: timeAxis.tick }\n })\n }\n }\n\n var anchor = null\n\n if (payload.ohlc && Array.isArray(payload.ohlc.data) && payload.ohlc.data.length > 0) {\n // kind=bar uses BarSeries; other OHLC kinds use CandlestickSeries with the same data contract.\n var isOhlcBar = payload.kind === 'bar'\n var candles = chart.addSeries(isOhlcBar ? LWC.BarSeries : LWC.CandlestickSeries, {\n upColor: colors.up,\n downColor: colors.down,\n borderUpColor: colors.up,\n borderDownColor: colors.down,\n wickUpColor: colors.up,\n wickDownColor: colors.down\n })\n candles.setData(payload.ohlc.data)\n anchor = candles\n }\n\n if (Array.isArray(payload.series)) {\n for (var index = 0; index < payload.series.length; index += 1) {\n var item = payload.series[index]\n if (!item || !Array.isArray(item.data) || item.data.length === 0) continue\n var color = seriesColor(index, colors)\n var options = { title: item.label || '', priceLineVisible: false, lastValueVisible: true }\n var series = null\n if (item.type === 'area') {\n options.lineColor = color\n options.topColor = withAlpha(color, 0.2)\n options.bottomColor = withAlpha(color, 0.02)\n options.lineWidth = 2\n series = chart.addSeries(LWC.AreaSeries, options)\n } else if (item.type === 'column') {\n // Single-value histograms need HistogramSeries; BarSeries would render an empty chart.\n options.color = color\n options.base = 0\n series = chart.addSeries(LWC.HistogramSeries, options)\n } else {\n options.color = color\n options.lineWidth = 2\n series = chart.addSeries(LWC.LineSeries, options)\n }\n series.setData(item.data)\n if (anchor === null) anchor = series\n }\n }\n\n if (payload.volume && Array.isArray(payload.volume.data) && payload.volume.data.length > 0) {\n var volume = chart.addSeries(LWC.HistogramSeries, {\n title: payload.volume.label || '',\n priceFormat: { type: 'volume' },\n priceLineVisible: false,\n lastValueVisible: false\n }, 1)\n volume.setData(payload.volume.data.map(function (point) {\n return { time: point.time, value: point.value, color: volumePointColor(point, colors) }\n }))\n }\n\n if (anchor === null) {\n showError(container, '\u8FD9\u5F20\u56FE\u6CA1\u6709\u53EF\u6E32\u67D3\u7684\u5E8F\u5217\uFF08payload \u91CC\u65E2\u6CA1\u6709 ohlc \u4E5F\u6CA1\u6709 series\uFF09\u3002')\n return chart\n }\n\n if (Array.isArray(payload.markers) && payload.markers.length > 0) {\n attachMarkers(anchor, payload.markers.map(function (marker) {\n return { time: marker.time, position: marker.position, text: marker.text, color: colors.strong, shape: 'circle' }\n }), LWC)\n }\n\n appendLegend(container, payload, colors)\n\n // Main/volume panes use a 3:1 ratio; a single pane has no second pane object.\n var panes = typeof chart.panes === 'function' ? chart.panes() : []\n if (panes.length > 1) {\n if (typeof panes[0].setStretchFactor === 'function') panes[0].setStretchFactor(3)\n if (typeof panes[1].setStretchFactor === 'function') panes[1].setStretchFactor(1)\n }\n\n chart.timeScale().fitContent()\n return chart\n }\n\n return { render: renderCapitalChart, version: 1 }\n})()\n";
@@ -84,6 +84,92 @@ export const CHART_RUNTIME_SOURCE = `
84
84
  })
85
85
  }
86
86
 
87
+ var CHART_TIME_ZONE = 'Asia/Shanghai'
88
+ var CHART_DAY_SECONDS = 86400
89
+ var CHART_LOCAL_MIDNIGHT_UTC = 57600
90
+
91
+ function pad2(value) {
92
+ var text = String(value)
93
+ return text.length < 2 ? '0' + text : text
94
+ }
95
+
96
+ function timeLabel(time) {
97
+ if (typeof time === 'string') return time
98
+ if (time && typeof time === 'object') {
99
+ var year = time.year
100
+ var month = time.month
101
+ var day = time.day
102
+ if (year !== undefined && month !== undefined && day !== undefined) {
103
+ return String(year) + '-' + pad2(month) + '-' + pad2(day)
104
+ }
105
+ }
106
+ return String(time)
107
+ }
108
+
109
+ function collectNumericTimes(payload) {
110
+ var times = []
111
+ function add(list) {
112
+ if (!Array.isArray(list)) return
113
+ for (var i = 0; i < list.length; i += 1) {
114
+ var point = list[i]
115
+ if (point && typeof point.time === 'number' && isFinite(point.time)) times.push(point.time)
116
+ }
117
+ }
118
+ if (payload.ohlc) add(payload.ohlc.data)
119
+ if (Array.isArray(payload.series)) {
120
+ for (var i = 0; i < payload.series.length; i += 1) add(payload.series[i] && payload.series[i].data)
121
+ }
122
+ if (payload.volume) add(payload.volume.data)
123
+ return times
124
+ }
125
+
126
+ // Numeric epochs are true instants; render them in Asia/Shanghai. Business-day strings keep
127
+ // their own labels, so no formatter is applied when every point is a date string.
128
+ function makeTimeAxisFormatter(payload) {
129
+ var times = collectNumericTimes(payload)
130
+ if (times.length === 0) return null
131
+ var min = times[0]
132
+ var max = times[0]
133
+ var daily = true
134
+ for (var i = 0; i < times.length; i += 1) {
135
+ if (times[i] < min) min = times[i]
136
+ if (times[i] > max) max = times[i]
137
+ var mod = ((times[i] % CHART_DAY_SECONDS) + CHART_DAY_SECONDS) % CHART_DAY_SECONDS
138
+ if (mod !== 0 && mod !== CHART_LOCAL_MIDNIGHT_UTC) daily = false
139
+ }
140
+ var span = max - min
141
+ var formatter = new Intl.DateTimeFormat('en-CA', {
142
+ timeZone: CHART_TIME_ZONE,
143
+ year: 'numeric', month: '2-digit', day: '2-digit',
144
+ hour: '2-digit', minute: '2-digit', hourCycle: 'h23'
145
+ })
146
+ function parts(seconds) {
147
+ var values = formatter.formatToParts(new Date(seconds * 1000))
148
+ var out = {}
149
+ for (var j = 0; j < values.length; j += 1) out[values[j].type] = values[j].value
150
+ return out
151
+ }
152
+ function tick(time) {
153
+ if (typeof time !== 'number') return timeLabel(time)
154
+ var p = parts(time)
155
+ if (daily) {
156
+ if (span > 3 * 365 * CHART_DAY_SECONDS) return p.year
157
+ if (span > 120 * CHART_DAY_SECONDS) return p.year + '-' + p.month
158
+ return p.month + '-' + p.day
159
+ }
160
+ if (span > 2 * CHART_DAY_SECONDS) return p.month + '-' + p.day + ' ' + p.hour + ':' + p.minute
161
+ return p.hour + ':' + p.minute
162
+ }
163
+ function full(time) {
164
+ if (typeof time !== 'number') return timeLabel(time)
165
+ var p = parts(time)
166
+ return daily
167
+ ? p.year + '-' + p.month + '-' + p.day
168
+ : p.year + '-' + p.month + '-' + p.day + ' ' + p.hour + ':' + p.minute
169
+ }
170
+ return { tick: tick, full: full }
171
+ }
172
+
87
173
  /** Support both v5 plugin markers and the older series API so markers never disappear silently. */
88
174
  function attachMarkers(anchor, markers, LWC) {
89
175
  if (typeof LWC.createSeriesMarkers === 'function') {
@@ -163,6 +249,15 @@ export const CHART_RUNTIME_SOURCE = `
163
249
  })
164
250
 
165
251
  if (payload.axis === 'index' && payload.labels) applyIndexLabels(chart, payload.labels)
252
+ if (payload.axis === 'time') {
253
+ var timeAxis = makeTimeAxisFormatter(payload)
254
+ if (timeAxis) {
255
+ chart.applyOptions({
256
+ localization: { timeFormatter: timeAxis.full },
257
+ timeScale: { tickMarkFormatter: timeAxis.tick }
258
+ })
259
+ }
260
+ }
166
261
 
167
262
  var anchor = null
168
263
 
@@ -68,16 +68,16 @@ export declare function extractRows(value: unknown): {
68
68
  shape: string;
69
69
  };
70
70
  interface ParsedTime {
71
- /** Value handed to the library: 'YYYY-MM-DD' business day, or UTC seconds. */
71
+ /** Value handed to the library: 'YYYY-MM-DD' business day, or epoch seconds. */
72
72
  display: string | number;
73
- /** Comparison key (UTC seconds) for sort/filter/alignment. */
73
+ /** Comparison key (epoch seconds); date-only and naive datetime values use Asia/Shanghai. */
74
74
  seconds: number;
75
75
  /** True when the value came from a date-only string; decides business-day rendering. */
76
76
  dateOnly: boolean;
77
77
  }
78
78
  /**
79
- * Deterministic across machines: date-only stays a business-day string (no TZ shift), naive
80
- * datetime is UTC, numbers >= 1e11 are epoch ms, unparseable means the row has no time.
79
+ * Deterministic across machines: date-only is an Asia/Shanghai calendar day, naive datetime is
80
+ * Asia/Shanghai wall clock, numbers >= 1e11 are epoch ms, unparseable means the row has no time.
81
81
  */
82
82
  export declare function parseTimeValue(value: unknown): ParsedTime | undefined;
83
83
  export interface BuildChartPayloadInput {
@@ -17,6 +17,8 @@ const INDEX_EPOCH_SECONDS = Date.UTC(2000, 0, 1) / 1000;
17
17
  const SECONDS_PER_DAY = 86_400;
18
18
  /** Values >= 1e11 are epoch ms (1e11 seconds is year 5138). */
19
19
  const MILLISECONDS_THRESHOLD = 1e11;
20
+ /** Chart convention: data timestamps are Asia/Shanghai wall clock; UTC+8 has no DST for supported dates. */
21
+ const CHART_UTC_OFFSET_MS = 8 * 3_600_000;
20
22
  function isRecord(value) {
21
23
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
22
24
  }
@@ -89,9 +91,24 @@ function toNumber(value) {
89
91
  }
90
92
  const DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
91
93
  const DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?(?:\.\d+)?(Z|[+-]\d{2}:?\d{2})?$/;
94
+ /** Calendar-valid UTC midnight, or undefined for 2025-02-30 style input. */
95
+ function calendarUtcMs(year, month, day) {
96
+ const utc = Date.UTC(year, month - 1, day);
97
+ const check = new Date(utc);
98
+ if (check.getUTCFullYear() !== year || check.getUTCMonth() + 1 !== month || check.getUTCDate() !== day)
99
+ return undefined;
100
+ return utc;
101
+ }
102
+ /** Asia/Shanghai wall clock to epoch ms; fixed offset matches every supported market date. */
103
+ function shanghaiEpochMs(year, month, day, hour, minute, second) {
104
+ const utc = calendarUtcMs(year, month, day);
105
+ if (utc === undefined)
106
+ return undefined;
107
+ return utc - CHART_UTC_OFFSET_MS + hour * 3_600_000 + minute * 60_000 + second * 1000;
108
+ }
92
109
  /**
93
- * Deterministic across machines: date-only stays a business-day string (no TZ shift), naive
94
- * datetime is UTC, numbers >= 1e11 are epoch ms, unparseable means the row has no time.
110
+ * Deterministic across machines: date-only is an Asia/Shanghai calendar day, naive datetime is
111
+ * Asia/Shanghai wall clock, numbers >= 1e11 are epoch ms, unparseable means the row has no time.
95
112
  */
96
113
  export function parseTimeValue(value) {
97
114
  if (typeof value === 'number') {
@@ -110,16 +127,26 @@ export function parseTimeValue(value) {
110
127
  const year = Number(dateOnly[1]);
111
128
  const month = Number(dateOnly[2]);
112
129
  const day = Number(dateOnly[3]);
113
- const check = new Date(Date.UTC(year, month - 1, day));
114
- if (check.getUTCFullYear() !== year || check.getUTCMonth() + 1 !== month || check.getUTCDate() !== day)
130
+ const epoch = shanghaiEpochMs(year, month, day, 0, 0, 0);
131
+ if (epoch === undefined)
115
132
  return undefined;
116
- return { display: text, seconds: Math.round(check.getTime() / 1000), dateOnly: true };
133
+ return { display: text, seconds: Math.round(epoch / 1000), dateOnly: true };
117
134
  }
118
135
  const dateTime = DATE_TIME.exec(text);
119
136
  if (dateTime) {
120
- const normalized = `${dateTime[1]}-${dateTime[2]}-${dateTime[3]}T${dateTime[4]}:${dateTime[5]}:${dateTime[6] ?? '00'}${dateTime[7] ?? 'Z'}`;
121
- const parsed = Date.parse(normalized);
122
- if (!Number.isFinite(parsed))
137
+ const year = Number(dateTime[1]);
138
+ const month = Number(dateTime[2]);
139
+ const day = Number(dateTime[3]);
140
+ const hour = Number(dateTime[4]);
141
+ const minute = Number(dateTime[5]);
142
+ const second = Number(dateTime[6] ?? '00');
143
+ if (calendarUtcMs(year, month, day) === undefined)
144
+ return undefined;
145
+ // An explicit Z/offset is an instant; a timezone-less value is Asia/Shanghai wall clock.
146
+ const parsed = dateTime[7] === undefined
147
+ ? shanghaiEpochMs(year, month, day, hour, minute, second)
148
+ : Date.parse(`${dateTime[1]}-${dateTime[2]}-${dateTime[3]}T${dateTime[4]}:${dateTime[5]}:${dateTime[6] ?? '00'}${dateTime[7]}`);
149
+ if (parsed === undefined || !Number.isFinite(parsed))
123
150
  return undefined;
124
151
  return { display: Math.round(parsed / 1000), seconds: Math.round(parsed / 1000), dateOnly: false };
125
152
  }
@@ -130,6 +157,13 @@ export function parseTimeValue(value) {
130
157
  return undefined;
131
158
  return { display: Math.round(parsed / 1000), seconds: Math.round(parsed / 1000), dateOnly: false };
132
159
  }
160
+ /** `to` on a date-only bound includes the whole Asia/Shanghai calendar day, not just midnight. */
161
+ function rangeBound(value, edge) {
162
+ const parsed = parseTimeValue(value);
163
+ if (parsed === undefined)
164
+ return undefined;
165
+ return parsed.dateOnly && edge === 'to' ? parsed.seconds + SECONDS_PER_DAY - 1 : parsed.seconds;
166
+ }
133
167
  export function buildChartPayload(input) {
134
168
  const { spec } = input;
135
169
  const warnings = [];
@@ -204,8 +238,8 @@ export function buildChartPayload(input) {
204
238
  if (axis === 'index') {
205
239
  throw new ChartError('chart_spec_invalid', 'spec.range requires a time axis; remove range for category data or provide a parseable time field in spec.x');
206
240
  }
207
- const from = spec.range.from === undefined ? undefined : parseTimeValue(spec.range.from)?.seconds;
208
- const to = spec.range.to === undefined ? undefined : parseTimeValue(spec.range.to)?.seconds;
241
+ const from = spec.range.from === undefined ? undefined : rangeBound(spec.range.from, 'from');
242
+ const to = spec.range.to === undefined ? undefined : rangeBound(spec.range.to, 'to');
209
243
  if (spec.range.from !== undefined && from === undefined)
210
244
  throw new ChartError('chart_spec_invalid', 'spec.range.from is not a recognizable date');
211
245
  if (spec.range.to !== undefined && to === undefined)