@v587d/capital-generation 2.15.0 → 2.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +171 -0
- package/README.md +7 -12
- package/THIRD_PARTY_NOTICES.md +1 -0
- package/automation/TABLER-ICONS-LICENSE +21 -0
- package/automation/builtins.js +237 -10
- package/automation/client.js +79 -17
- package/automation/client.src.cjs +873 -210
- package/automation/contract.js +4 -0
- package/automation/defaults.js +6 -4
- package/automation/index.js +12 -7
- package/automation/package.json +5 -2
- package/automation/route.js +9 -2
- package/automation/runner.js +33 -6
- package/automation/scheduler.js +34 -29
- package/automation/schema.js +39 -5
- package/automation/service.js +102 -10
- package/automation/settings.js +6 -0
- package/automation/store.js +64 -21
- package/automation/task-icons.js +75 -0
- package/automation/workspaces.js +113 -13
- package/chart-ui/client.js +2 -2
- package/chart-ui/package.json +1 -1
- package/config/client.js +19 -5
- package/config/client.src.cjs +365 -44
- package/config/index.js +7 -5
- package/config/package.json +1 -1
- package/lib/agents/root-tool-policy.d.ts +3 -3
- package/lib/agents/root-tool-policy.js +4 -2
- package/lib/chart/runtime.d.ts +1 -1
- package/lib/chart/runtime.js +95 -0
- package/lib/chart/series.d.ts +4 -4
- package/lib/chart/series.js +44 -10
- package/lib/data-collector/dataset-tools.js +22 -0
- package/lib/data-collector/describe.js +1 -0
- package/lib/data-collector/hub.js +5 -0
- package/lib/data-collector/store.d.ts +5 -0
- package/lib/data-collector/store.js +17 -0
- package/lib/data-collector/time-axis.d.ts +15 -0
- package/lib/data-collector/time-axis.js +73 -0
- package/lib/data-collector/tools.d.ts +1 -1
- package/lib/data-collector/tools.js +12 -1
- package/lib/index.d.ts +5 -0
- package/lib/index.js +7 -5
- package/lib/sources/tencent-http.js +7 -2
- package/lib/sources/tencent-public-core.d.ts +2 -0
- package/lib/sources/tencent-public-core.js +5 -0
- package/lib/sources/wind-mcp.js +3 -3
- package/lib/sources-entry.d.ts +1 -1
- package/lib/web-retriever/local-fetch.d.ts +1 -1
- package/lib/web-retriever/local-fetch.js +1 -1
- package/lib/web-retriever/sources.d.ts +58 -0
- package/lib/web-retriever/sources.js +290 -0
- package/lib/web-retriever/tools.js +199 -8
- package/lib/web-retriever/wind-client.d.ts +1 -1
- package/lib/web-retriever/wind-client.js +1 -1
- package/package.json +1 -1
- package/preset/capital-generation/agent.patch.yml +20 -19
- package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +6 -3
- package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +9 -2
- package/preset/capital-generation/skills/capital-orchestration/SKILL.md +2 -1
- package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +2 -0
- package/preset/capital-generation/skills/capital-web-evidence/SKILL.md +18 -13
- package/preset/capital-generation/skills/capital-web-protocol/SKILL.md +30 -15
- package/watchlist/package.json +1 -1
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { CAPITAL_PRESET_IDS, ROOT_PRESET_ID } from '../preset-ids.js';
|
|
6
6
|
import { readPresetClaim } from '../preset-claim.js';
|
|
7
|
-
/** Outbound tools (search 2 + wind 2 + named sources
|
|
7
|
+
/** Outbound tools (search 2 + wind 2 + named sources 11); `ocr` is excluded because only its url form is restricted. */
|
|
8
8
|
export const RETRIEVAL_DENIED_TOOLS = [
|
|
9
9
|
'anysearch_search', 'web_retriever_fetch',
|
|
10
10
|
'wind_docs_announcements', 'wind_docs_news',
|
|
11
11
|
'cls_telegraph', 'wscn_lives', 'cninfo_irm', 'sseinfo_qa',
|
|
12
|
-
'eastmoney_724', 'eastmoney_stock_news', 'eastmoney_reports',
|
|
12
|
+
'eastmoney_724', 'eastmoney_stock_news', 'eastmoney_reports',
|
|
13
|
+
'eastmoney_announcements', 'eastmoney_announcement_body',
|
|
14
|
+
'sina_reports', 'ths_eps_forecast',
|
|
13
15
|
];
|
|
14
16
|
export const OCR_TOOL_NAME = 'ocr';
|
|
15
17
|
/** Name-level deny cannot express the form distinction; this is a call-level guard. */
|
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)
|
|
@@ -144,6 +144,17 @@ const datasetRefProperties = { dataset_id: { type: 'string' },
|
|
|
144
144
|
captured_at: { type: 'integer' },
|
|
145
145
|
retention_until: { type: 'integer' },
|
|
146
146
|
params_digest: { type: 'string' },
|
|
147
|
+
time_coverage: {
|
|
148
|
+
type: 'object',
|
|
149
|
+
properties: {
|
|
150
|
+
column: { oneOf: [{ type: 'string' }, { type: 'null' }] },
|
|
151
|
+
from: { oneOf: [{ type: 'string' }, { type: 'null' }] },
|
|
152
|
+
to: { oneOf: [{ type: 'string' }, { type: 'null' }] },
|
|
153
|
+
format: { type: 'string', enum: ['epoch_ms', 'date_string', 'datetime_string', 'none'] },
|
|
154
|
+
},
|
|
155
|
+
required: ['column', 'from', 'to', 'format'],
|
|
156
|
+
additionalProperties: false,
|
|
157
|
+
},
|
|
147
158
|
};
|
|
148
159
|
const datasetRefWithAccessSchema = jsonObject({
|
|
149
160
|
...datasetRefProperties,
|
|
@@ -288,6 +299,17 @@ const describeResultSchema = jsonObject({
|
|
|
288
299
|
captured_at: { type: 'integer' },
|
|
289
300
|
retention_until: { type: 'integer' },
|
|
290
301
|
params_digest: { type: 'string' },
|
|
302
|
+
time_coverage: {
|
|
303
|
+
type: 'object',
|
|
304
|
+
properties: {
|
|
305
|
+
column: { oneOf: [{ type: 'string' }, { type: 'null' }] },
|
|
306
|
+
from: { oneOf: [{ type: 'string' }, { type: 'null' }] },
|
|
307
|
+
to: { oneOf: [{ type: 'string' }, { type: 'null' }] },
|
|
308
|
+
format: { type: 'string', enum: ['epoch_ms', 'date_string', 'datetime_string', 'none'] },
|
|
309
|
+
},
|
|
310
|
+
required: ['column', 'from', 'to', 'format'],
|
|
311
|
+
additionalProperties: false,
|
|
312
|
+
},
|
|
291
313
|
query_access: jsonObject({
|
|
292
314
|
readable: { type: 'boolean' },
|
|
293
315
|
shape: { type: 'string', enum: ['array', 'envelope_item', 'document', 'none'] },
|
|
@@ -171,6 +171,7 @@ export async function describeDataset(input) {
|
|
|
171
171
|
captured_at: inspection.captured_at,
|
|
172
172
|
retention_until: inspection.retention_until,
|
|
173
173
|
params_digest: inspection.params_digest,
|
|
174
|
+
time_coverage: inspection.time_coverage,
|
|
174
175
|
query_access: inspection.query_access,
|
|
175
176
|
profile_id: profile.profile_id,
|
|
176
177
|
profile_ref: profile.artifact_ref,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { summarizeTimeCoverage } from './time-axis.js';
|
|
1
2
|
/** Normalize an internal data_key token; data_key is host-only and never model-visible. */
|
|
2
3
|
export function normalizeKeyToken(value) {
|
|
3
4
|
return value.replace(/[\\/:]+/g, '.')
|
|
@@ -320,6 +321,9 @@ export class DataCollectorHub {
|
|
|
320
321
|
throw new Error(`data source ${source.schema.name} returned data incompatible with its output contract`);
|
|
321
322
|
}
|
|
322
323
|
const { format, rowCount, rowKey } = shapeOf(output.data, source.schema.rowShape);
|
|
324
|
+
const rows = Array.isArray(output.data)
|
|
325
|
+
? output.data
|
|
326
|
+
: (rowKey === undefined ? [] : rowsAtPath(output.data, rowKey) ?? []);
|
|
323
327
|
const ref = await this.store.save({
|
|
324
328
|
session: item.request.session,
|
|
325
329
|
capability: item.request.capability,
|
|
@@ -331,6 +335,7 @@ export class DataCollectorHub {
|
|
|
331
335
|
row_count: rowCount,
|
|
332
336
|
data: output.data,
|
|
333
337
|
row_key: rowKey,
|
|
338
|
+
...(format === 'json_rows' ? { time_coverage: summarizeTimeCoverage(rows) } : {}),
|
|
334
339
|
});
|
|
335
340
|
for (const waiter of [...item.waiters])
|
|
336
341
|
this.settleWaiter(item, waiter, { kind: 'ok', ref });
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type QueryResult, type QuerySpec } from './query.js';
|
|
2
|
+
import { type DatasetTimeCoverage } from './time-axis.js';
|
|
2
3
|
/**
|
|
3
4
|
* Workspace-local Dataset/profile store: immutable artifacts, host-written only. Agents receive
|
|
4
5
|
* dataset_id / opaque artifact_ref, never real paths; raw rows never enter agent messages.
|
|
@@ -38,6 +39,8 @@ export interface DatasetRef {
|
|
|
38
39
|
captured_at: number;
|
|
39
40
|
retention_until: number;
|
|
40
41
|
params_digest: string;
|
|
42
|
+
/** Host-computed from the stored rows; omitted only by manifests written before this field existed. */
|
|
43
|
+
time_coverage?: DatasetTimeCoverage;
|
|
41
44
|
}
|
|
42
45
|
export type DatasetShape = 'array' | 'envelope_item' | 'document' | 'none';
|
|
43
46
|
export interface DatasetInspection extends DatasetRef {
|
|
@@ -293,6 +296,8 @@ export interface SaveDatasetInput {
|
|
|
293
296
|
data: unknown;
|
|
294
297
|
/** Host-only row key for envelope-shaped json_rows data. */
|
|
295
298
|
row_key?: string;
|
|
299
|
+
/** Computed by the Hub from the same rows it is about to persist. */
|
|
300
|
+
time_coverage?: DatasetTimeCoverage;
|
|
296
301
|
signal?: AbortSignal;
|
|
297
302
|
}
|
|
298
303
|
export interface WriteProfileInput {
|
|
@@ -140,6 +140,7 @@ export class WorkspaceDatasetStore {
|
|
|
140
140
|
captured_at: capturedAt,
|
|
141
141
|
retention_until: capturedAt + this.retentionMs,
|
|
142
142
|
params_digest: input.params_digest,
|
|
143
|
+
...(input.time_coverage === undefined ? {} : { time_coverage: input.time_coverage }),
|
|
143
144
|
};
|
|
144
145
|
const storedManifest = {
|
|
145
146
|
...manifest,
|
|
@@ -1103,6 +1104,19 @@ function profileQuantile(sorted, probability) {
|
|
|
1103
1104
|
return sorted[lower];
|
|
1104
1105
|
return sorted[lower] + (sorted[upper] - sorted[lower]) * (position - lower);
|
|
1105
1106
|
}
|
|
1107
|
+
function validTimeCoverage(value) {
|
|
1108
|
+
if (!isRecord(value))
|
|
1109
|
+
return false;
|
|
1110
|
+
if (Object.keys(value).some((key) => !['column', 'from', 'to', 'format'].includes(key)))
|
|
1111
|
+
return false;
|
|
1112
|
+
if (!['epoch_ms', 'date_string', 'datetime_string', 'none'].includes(String(value.format)))
|
|
1113
|
+
return false;
|
|
1114
|
+
if (value.format === 'none')
|
|
1115
|
+
return value.column === null && value.from === null && value.to === null;
|
|
1116
|
+
return typeof value.column === 'string' && value.column.length > 0 && value.column.length <= 128
|
|
1117
|
+
&& typeof value.from === 'string' && value.from.length > 0
|
|
1118
|
+
&& typeof value.to === 'string' && value.to.length > 0;
|
|
1119
|
+
}
|
|
1106
1120
|
function validateStoredDatasetManifest(value, datasetId) {
|
|
1107
1121
|
if (!isRecord(value))
|
|
1108
1122
|
return undefined;
|
|
@@ -1130,6 +1144,8 @@ function validateStoredDatasetManifest(value, datasetId) {
|
|
|
1130
1144
|
return undefined;
|
|
1131
1145
|
if (value.row_key !== undefined && (typeof value.row_key !== 'string' || value.row_key.length === 0 || value.row_key.length > 128))
|
|
1132
1146
|
return undefined;
|
|
1147
|
+
if (value.time_coverage !== undefined && !validTimeCoverage(value.time_coverage))
|
|
1148
|
+
return undefined;
|
|
1133
1149
|
const ref = {
|
|
1134
1150
|
dataset_id: value.dataset_id,
|
|
1135
1151
|
task_id: value.task_id,
|
|
@@ -1143,6 +1159,7 @@ function validateStoredDatasetManifest(value, datasetId) {
|
|
|
1143
1159
|
captured_at: value.captured_at,
|
|
1144
1160
|
retention_until: value.retention_until,
|
|
1145
1161
|
params_digest: value.params_digest,
|
|
1162
|
+
...(value.time_coverage === undefined ? {} : { time_coverage: value.time_coverage }),
|
|
1146
1163
|
};
|
|
1147
1164
|
return {
|
|
1148
1165
|
...ref,
|
|
@@ -15,6 +15,21 @@ export interface AxisBounds {
|
|
|
15
15
|
data_from: string | null;
|
|
16
16
|
data_to: string | null;
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Host-computed coverage for a DatasetRef. `format: 'none'` is intentional: a quote source
|
|
20
|
+
* without a time column must not look like it has an as-of time.
|
|
21
|
+
*/
|
|
22
|
+
export interface DatasetTimeCoverage {
|
|
23
|
+
column: string | null;
|
|
24
|
+
from: string | null;
|
|
25
|
+
to: string | null;
|
|
26
|
+
format: 'epoch_ms' | 'date_string' | 'datetime_string' | 'none';
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Pick one time column by name and return its real min/max. This is a host fact for DatasetRef;
|
|
30
|
+
* it deliberately does not convert quote times across markets (their offsets stay in the string).
|
|
31
|
+
*/
|
|
32
|
+
export declare function summarizeTimeCoverage(rows: unknown[]): DatasetTimeCoverage;
|
|
18
33
|
/**
|
|
19
34
|
* Translate a date-like filter value into comparable bounds. Dates mean whole days: `=` expands
|
|
20
35
|
* to >=/<=, while `>`, `<`, `in` and `!=` fail loudly instead of silently shifting a day.
|
|
@@ -126,6 +126,79 @@ export function probeTimeAxis(column, values) {
|
|
|
126
126
|
return { column, value_format: 'date_string', aligned_to: 'calendar_day' };
|
|
127
127
|
return { column, value_format: 'unknown' };
|
|
128
128
|
}
|
|
129
|
+
const TIME_COLUMN_PRIORITY = [
|
|
130
|
+
'quote_time', 'event_time_beijing', 'datetime', 'report_date', 'trade_date',
|
|
131
|
+
'stat_date', 'quote_date', 'collect_date', 'date', 'time',
|
|
132
|
+
'quote_time_ms', 'event_time_ms', 'report_date_ms', 'trade_date_ms',
|
|
133
|
+
'stat_date_ms', 'quote_date_ms', 'collect_date_ms', 'date_ms', 'time_ms',
|
|
134
|
+
];
|
|
135
|
+
const COVERAGE_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
136
|
+
const COVERAGE_DATETIME_RE = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:\d{2})?$/;
|
|
137
|
+
function coverageOfColumn(column, rows) {
|
|
138
|
+
const values = [];
|
|
139
|
+
for (const row of rows) {
|
|
140
|
+
const cell = cellOf(row, column);
|
|
141
|
+
if (!cell.present || cell.value === null || cell.value === undefined)
|
|
142
|
+
continue;
|
|
143
|
+
values.push(cell.value);
|
|
144
|
+
}
|
|
145
|
+
if (values.length === 0)
|
|
146
|
+
return undefined;
|
|
147
|
+
const numbers = values.filter((value) => typeof value === 'number' && Number.isFinite(value));
|
|
148
|
+
if (numbers.length === values.length) {
|
|
149
|
+
let min = Number.POSITIVE_INFINITY;
|
|
150
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
151
|
+
for (const value of numbers) {
|
|
152
|
+
if (!Number.isSafeInteger(value) || value < MIN_EPOCH_MS || value > 8_640_000_000_000_000)
|
|
153
|
+
return undefined;
|
|
154
|
+
if (value < min)
|
|
155
|
+
min = value;
|
|
156
|
+
if (value > max)
|
|
157
|
+
max = value;
|
|
158
|
+
}
|
|
159
|
+
const axis = probeTimeAxis(column, numbers);
|
|
160
|
+
const zone = axis.time_zone ?? 'UTC';
|
|
161
|
+
const text = (value) => axis.aligned_to === 'local_midnight'
|
|
162
|
+
? isoDateFromEpoch(value, zone)
|
|
163
|
+
: new Date(value).toISOString();
|
|
164
|
+
return { column, from: text(min), to: text(max), format: 'epoch_ms' };
|
|
165
|
+
}
|
|
166
|
+
const strings = values.filter((value) => typeof value === 'string');
|
|
167
|
+
if (strings.length !== values.length)
|
|
168
|
+
return undefined;
|
|
169
|
+
const trimmed = strings.map((value) => value.trim());
|
|
170
|
+
if (trimmed.every((value) => COVERAGE_DATE_RE.test(value))) {
|
|
171
|
+
const sorted = [...trimmed].sort();
|
|
172
|
+
return { column, from: sorted[0], to: sorted[sorted.length - 1], format: 'date_string' };
|
|
173
|
+
}
|
|
174
|
+
if (trimmed.every((value) => COVERAGE_DATETIME_RE.test(value))) {
|
|
175
|
+
const sorted = [...trimmed].map((value) => value.replace('T', ' ')).sort();
|
|
176
|
+
return { column, from: sorted[0], to: sorted[sorted.length - 1], format: 'datetime_string' };
|
|
177
|
+
}
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Pick one time column by name and return its real min/max. This is a host fact for DatasetRef;
|
|
182
|
+
* it deliberately does not convert quote times across markets (their offsets stay in the string).
|
|
183
|
+
*/
|
|
184
|
+
export function summarizeTimeCoverage(rows) {
|
|
185
|
+
if (rows.length === 0)
|
|
186
|
+
return { column: null, from: null, to: null, format: 'none' };
|
|
187
|
+
const first = rows.find((row) => isRecordLike(row));
|
|
188
|
+
if (!isRecordLike(first))
|
|
189
|
+
return { column: null, from: null, to: null, format: 'none' };
|
|
190
|
+
const keys = Object.keys(first);
|
|
191
|
+
const ordered = [
|
|
192
|
+
...TIME_COLUMN_PRIORITY.filter((column) => keys.includes(column)),
|
|
193
|
+
...keys.filter((column) => !TIME_COLUMN_PRIORITY.includes(column) && /(^|_)(date|time)(_|$)|_ms$/i.test(column)),
|
|
194
|
+
];
|
|
195
|
+
for (const column of ordered) {
|
|
196
|
+
const coverage = coverageOfColumn(column, rows);
|
|
197
|
+
if (coverage !== undefined)
|
|
198
|
+
return coverage;
|
|
199
|
+
}
|
|
200
|
+
return { column: null, from: null, to: null, format: 'none' };
|
|
201
|
+
}
|
|
129
202
|
/**
|
|
130
203
|
* Translate a date-like filter value into comparable bounds. Dates mean whole days: `=` expands
|
|
131
204
|
* to >=/<=, while `>`, `<`, `in` and `!=` fail loudly instead of silently shifting a day.
|
|
@@ -29,4 +29,4 @@ export interface DataCollectorDiagnostics {
|
|
|
29
29
|
}
|
|
30
30
|
export declare function registerDataCollectorTools(ctx: Context, hub: DataCollectorHub, diagnostics?: DataCollectorDiagnostics): void;
|
|
31
31
|
/** Model-visible DatasetRef field whitelist (tests and host assertions). */
|
|
32
|
-
export declare const DATASET_REF_FIELDS: readonly ["dataset_id", "task_id", "session_id", "artifact_ref", "format", "capability", "source_label", "schema", "row_count", "captured_at", "retention_until", "params_digest"];
|
|
32
|
+
export declare const DATASET_REF_FIELDS: readonly ["dataset_id", "task_id", "session_id", "artifact_ref", "format", "capability", "source_label", "schema", "row_count", "captured_at", "retention_until", "params_digest", "time_coverage"];
|
|
@@ -42,6 +42,17 @@ const datasetRefSchema = () => jsonObject({
|
|
|
42
42
|
captured_at: { type: 'integer' },
|
|
43
43
|
retention_until: { type: 'integer' },
|
|
44
44
|
params_digest: { type: 'string' },
|
|
45
|
+
time_coverage: {
|
|
46
|
+
type: 'object',
|
|
47
|
+
properties: {
|
|
48
|
+
column: { oneOf: [{ type: 'string' }, { type: 'null' }] },
|
|
49
|
+
from: { oneOf: [{ type: 'string' }, { type: 'null' }] },
|
|
50
|
+
to: { oneOf: [{ type: 'string' }, { type: 'null' }] },
|
|
51
|
+
format: { type: 'string', enum: ['epoch_ms', 'date_string', 'datetime_string', 'none'] },
|
|
52
|
+
},
|
|
53
|
+
required: ['column', 'from', 'to', 'format'],
|
|
54
|
+
additionalProperties: false,
|
|
55
|
+
},
|
|
45
56
|
}, ['dataset_id', 'task_id', 'session_id', 'artifact_ref', 'format', 'capability', 'source_label', 'schema', 'row_count', 'captured_at', 'retention_until', 'params_digest']);
|
|
46
57
|
function render(_args, value) {
|
|
47
58
|
// ToolOutputDefinition.render(args, value): the first parameter is the input, the second the output.
|
|
@@ -215,5 +226,5 @@ export function registerDataCollectorTools(ctx, hub, diagnostics) {
|
|
|
215
226
|
export const DATASET_REF_FIELDS = [
|
|
216
227
|
'dataset_id', 'task_id', 'session_id', 'artifact_ref', 'format',
|
|
217
228
|
'capability', 'source_label', 'schema', 'row_count', 'captured_at',
|
|
218
|
-
'retention_until', 'params_digest',
|
|
229
|
+
'retention_until', 'params_digest', 'time_coverage',
|
|
219
230
|
];
|
package/lib/index.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export interface Config {
|
|
|
20
20
|
maxSteps?: number;
|
|
21
21
|
maxToolCalls?: number;
|
|
22
22
|
inAppNotifications?: boolean;
|
|
23
|
+
defaultWorkspaceId?: string;
|
|
23
24
|
};
|
|
24
25
|
retriever?: RetrieverConfig;
|
|
25
26
|
}
|
|
@@ -40,6 +41,7 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
40
41
|
maxSteps: z<number, number, "defined">;
|
|
41
42
|
maxToolCalls: z<number, number, "defined">;
|
|
42
43
|
inAppNotifications: z<boolean, boolean, "defined">;
|
|
44
|
+
defaultWorkspaceId: z<string, string, "defined">;
|
|
43
45
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
44
46
|
tickMs: z<number, number, "defined">;
|
|
45
47
|
runTimeoutMinutes: z<number, number, "defined">;
|
|
@@ -47,6 +49,7 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
47
49
|
maxSteps: z<number, number, "defined">;
|
|
48
50
|
maxToolCalls: z<number, number, "defined">;
|
|
49
51
|
inAppNotifications: z<boolean, boolean, "defined">;
|
|
52
|
+
defaultWorkspaceId: z<string, string, "defined">;
|
|
50
53
|
}>>, "defined">;
|
|
51
54
|
retriever: z<Schemastery.ObjectS<NoInfer<{
|
|
52
55
|
baseURL: z<string, string, "defined">;
|
|
@@ -142,6 +145,7 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
142
145
|
maxSteps: z<number, number, "defined">;
|
|
143
146
|
maxToolCalls: z<number, number, "defined">;
|
|
144
147
|
inAppNotifications: z<boolean, boolean, "defined">;
|
|
148
|
+
defaultWorkspaceId: z<string, string, "defined">;
|
|
145
149
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
146
150
|
tickMs: z<number, number, "defined">;
|
|
147
151
|
runTimeoutMinutes: z<number, number, "defined">;
|
|
@@ -149,6 +153,7 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
149
153
|
maxSteps: z<number, number, "defined">;
|
|
150
154
|
maxToolCalls: z<number, number, "defined">;
|
|
151
155
|
inAppNotifications: z<boolean, boolean, "defined">;
|
|
156
|
+
defaultWorkspaceId: z<string, string, "defined">;
|
|
152
157
|
}>>, "defined">;
|
|
153
158
|
retriever: z<Schemastery.ObjectS<NoInfer<{
|
|
154
159
|
baseURL: z<string, string, "defined">;
|