@v587d/capital-generation 2.15.0 → 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.
- package/CHANGELOG.md +109 -0
- package/README.md +4 -9
- package/THIRD_PARTY_NOTICES.md +1 -0
- package/automation/TABLER-ICONS-LICENSE +21 -0
- package/automation/builtins.js +237 -10
- package/automation/client.js +37 -5
- package/automation/client.src.cjs +578 -126
- package/automation/defaults.js +4 -4
- package/automation/index.js +1 -0
- package/automation/package.json +4 -2
- package/automation/route.js +1 -1
- package/automation/runner.js +5 -0
- package/automation/scheduler.js +34 -29
- package/automation/schema.js +39 -5
- package/automation/service.js +13 -10
- package/automation/store.js +64 -21
- package/automation/task-icons.js +75 -0
- package/chart-ui/client.js +2 -2
- package/chart-ui/package.json +1 -1
- package/config/client.js +12 -5
- package/config/client.src.cjs +143 -45
- package/config/index.js +5 -5
- package/config/package.json +1 -1
- package/lib/chart/runtime.d.ts +1 -1
- package/lib/chart/runtime.js +95 -0
- package/lib/chart/series.d.ts +4 -4
- package/lib/chart/series.js +44 -10
- package/lib/index.js +5 -5
- 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 +1 -1
- package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +6 -3
- package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +2 -0
- package/watchlist/package.json +1 -1
package/config/client.src.cjs
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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: '
|
|
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-
|
|
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
|
-
|
|
479
|
-
if (field
|
|
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
|
-
|
|
485
|
-
|
|
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
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
max,
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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: '
|
|
818
|
-
|
|
868
|
+
type: 'number',
|
|
869
|
+
min,
|
|
870
|
+
max,
|
|
871
|
+
step: 1,
|
|
872
|
+
defaultValue: stored,
|
|
819
873
|
disabled: disabled || state.saving,
|
|
820
|
-
|
|
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
|
|
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('
|
|
81
|
-
runTimeoutMinutes: z.number().default(
|
|
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('单次运行最大步骤数(
|
|
84
|
-
maxToolCalls: z.number().default(1_200).volatile().description('单次运行最大工具调用数(
|
|
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:
|
|
88
|
+
runTimeoutMinutes: 60,
|
|
89
89
|
idleTimeoutMinutes: 5,
|
|
90
90
|
maxSteps: 300,
|
|
91
91
|
maxToolCalls: 1_200,
|
package/config/package.json
CHANGED
package/lib/chart/runtime.d.ts
CHANGED
|
@@ -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";
|
package/lib/chart/runtime.js
CHANGED
|
@@ -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
|
|
package/lib/chart/series.d.ts
CHANGED
|
@@ -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
|
|
71
|
+
/** Value handed to the library: 'YYYY-MM-DD' business day, or epoch seconds. */
|
|
72
72
|
display: string | number;
|
|
73
|
-
/** Comparison key (
|
|
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
|
|
80
|
-
*
|
|
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 {
|
package/lib/chart/series.js
CHANGED
|
@@ -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
|
|
94
|
-
*
|
|
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
|
|
114
|
-
if (
|
|
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(
|
|
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
|
|
121
|
-
const
|
|
122
|
-
|
|
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 :
|
|
208
|
-
const to = spec.range.to === undefined ? undefined :
|
|
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)
|