@v587d/capital-generation 2.8.2 → 2.9.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 +87 -1
- package/README.md +41 -20
- package/chart-ui/README.md +1 -1
- package/chart-ui/client.src.cjs +7 -16
- package/chart-ui/index.js +12 -33
- package/chart-ui/package.json +2 -2
- package/config/client.js +18 -39
- package/config/client.src.cjs +79 -217
- package/config/index.js +22 -72
- package/config/package.json +2 -2
- package/cordis.patch.yml +46 -51
- package/lib/agents/bash-guard.d.ts +9 -55
- package/lib/agents/bash-guard.js +27 -67
- package/lib/agents/preset-roster.d.ts +22 -0
- package/lib/agents/preset-roster.js +87 -0
- package/lib/agents/role-tool-policy.d.ts +46 -0
- package/lib/agents/role-tool-policy.js +205 -0
- package/lib/agents/root-tool-policy.d.ts +28 -124
- package/lib/agents/root-tool-policy.js +164 -237
- package/lib/agents/selected-skills-provider.d.ts +2 -6
- package/lib/agents/selected-skills-provider.js +27 -84
- package/lib/capital-settings.d.ts +27 -0
- package/lib/capital-settings.js +23 -0
- package/lib/chart/artifact-ref.d.ts +3 -8
- package/lib/chart/artifact-ref.js +3 -8
- package/lib/chart/downsample.d.ts +2 -5
- package/lib/chart/downsample.js +3 -8
- package/lib/chart/errors.d.ts +3 -7
- package/lib/chart/errors.js +1 -2
- package/lib/chart/event-context.d.ts +8 -0
- package/lib/chart/event-context.js +39 -0
- package/lib/chart/events.d.ts +21 -78
- package/lib/chart/events.js +40 -189
- package/lib/chart/html.d.ts +2 -5
- package/lib/chart/html.js +8 -15
- package/lib/chart/runtime.d.ts +5 -18
- package/lib/chart/runtime.js +14 -34
- package/lib/chart/series.d.ts +10 -17
- package/lib/chart/series.js +22 -47
- package/lib/chart/source-token.d.ts +5 -13
- package/lib/chart/source-token.js +18 -17
- package/lib/chart/spec.d.ts +6 -12
- package/lib/chart/spec.js +7 -31
- package/lib/chart/tool.d.ts +6 -29
- package/lib/chart/tool.js +9 -23
- package/lib/chart-render-entry.d.ts +9 -0
- package/lib/chart-render-entry.js +17 -0
- package/lib/chart-source-entry.d.ts +13 -0
- package/lib/chart-source-entry.js +20 -0
- package/lib/collect-entry.d.ts +9 -0
- package/lib/collect-entry.js +10 -0
- package/lib/collector-runtime.d.ts +22 -0
- package/lib/collector-runtime.js +97 -0
- package/lib/data-collector/args.d.ts +6 -24
- package/lib/data-collector/args.js +6 -24
- package/lib/data-collector/dataset-tools.d.ts +7 -1
- package/lib/data-collector/dataset-tools.js +27 -56
- package/lib/data-collector/describe.d.ts +8 -33
- package/lib/data-collector/describe.js +17 -70
- package/lib/data-collector/hub.d.ts +27 -85
- package/lib/data-collector/hub.js +19 -85
- package/lib/data-collector/query-time.d.ts +7 -26
- package/lib/data-collector/query-time.js +8 -27
- package/lib/data-collector/store.d.ts +40 -117
- package/lib/data-collector/store.js +32 -111
- package/lib/data-collector/time-axis.d.ts +16 -76
- package/lib/data-collector/time-axis.js +25 -111
- package/lib/data-collector/tools.d.ts +6 -6
- package/lib/data-collector/tools.js +34 -43
- package/lib/dataset-read-entry.d.ts +9 -0
- package/lib/dataset-read-entry.js +7 -0
- package/lib/index.d.ts +7 -117
- package/lib/index.js +43 -422
- package/lib/net/eastmoney-client.d.ts +13 -65
- package/lib/net/eastmoney-client.js +12 -53
- package/lib/net/throttle.d.ts +2 -13
- package/lib/net/throttle.js +3 -14
- package/lib/ocr/client.d.ts +12 -21
- package/lib/ocr/client.js +23 -40
- package/lib/ocr/markdown.d.ts +20 -26
- package/lib/ocr/markdown.js +23 -36
- package/lib/ocr/store.d.ts +14 -47
- package/lib/ocr/store.js +15 -45
- package/lib/ocr-entry.d.ts +9 -0
- package/lib/ocr-entry.js +10 -0
- package/lib/policy-entry.d.ts +17 -0
- package/lib/policy-entry.js +22 -0
- package/lib/preset-claim.d.ts +22 -0
- package/lib/preset-claim.js +24 -0
- package/lib/preset-ids.d.ts +38 -0
- package/lib/preset-ids.js +46 -0
- package/lib/retriever-config.d.ts +37 -0
- package/lib/retriever-config.js +36 -0
- package/lib/retriever-runtime.d.ts +31 -0
- package/lib/retriever-runtime.js +82 -0
- package/lib/root-extras-entry.d.ts +17 -0
- package/lib/root-extras-entry.js +25 -0
- package/lib/runtime-entry.d.ts +15 -0
- package/lib/runtime-entry.js +5 -0
- package/lib/runtime.d.ts +21 -0
- package/lib/runtime.js +33 -0
- package/lib/search-entry.d.ts +9 -0
- package/lib/search-entry.js +9 -0
- package/lib/sources/eastmoney-http.js +20 -68
- package/lib/sources/ftshare-rest.d.ts +6 -31
- package/lib/sources/ftshare-rest.js +1194 -225
- package/lib/sources/fuyao-core.d.ts +8 -18
- package/lib/sources/fuyao-core.js +10 -21
- package/lib/sources/fuyao-rest.d.ts +2 -6
- package/lib/sources/fuyao-rest.js +23 -102
- package/lib/sources/security-code.d.ts +6 -12
- package/lib/sources/security-code.js +6 -12
- package/lib/sources/security-identity.d.ts +1 -4
- package/lib/sources/security-identity.js +3 -8
- package/lib/sources/tencent-http.js +14 -28
- package/lib/sources/tencent-public-core.d.ts +8 -60
- package/lib/sources/tencent-public-core.js +12 -77
- package/lib/sources/wind-mcp.d.ts +0 -5
- package/lib/sources/wind-mcp.js +23 -78
- package/lib/sources-entry.d.ts +9 -0
- package/lib/sources-entry.js +9 -0
- package/lib/time/tools.d.ts +8 -31
- package/lib/time/tools.js +39 -64
- package/lib/time-entry.d.ts +9 -0
- package/lib/time-entry.js +7 -0
- package/lib/tool-exec.d.ts +45 -11
- package/lib/tool-exec.js +59 -6
- package/lib/user-customization.d.ts +14 -0
- package/lib/user-customization.js +47 -0
- package/lib/watchlist/tools.d.ts +3 -13
- package/lib/watchlist/tools.js +10 -30
- package/lib/watchlist-entry.d.ts +9 -0
- package/lib/watchlist-entry.js +6 -0
- package/lib/web-retriever/engines.d.ts +6 -11
- package/lib/web-retriever/engines.js +9 -14
- package/lib/web-retriever/html-markdown.d.ts +14 -34
- package/lib/web-retriever/html-markdown.js +39 -75
- package/lib/web-retriever/local-fetch.d.ts +22 -58
- package/lib/web-retriever/local-fetch.js +33 -77
- package/lib/web-retriever/retriever.d.ts +6 -18
- package/lib/web-retriever/retriever.js +7 -7
- package/lib/web-retriever/sources.d.ts +37 -124
- package/lib/web-retriever/sources.js +64 -196
- package/lib/web-retriever/tools.d.ts +43 -27
- package/lib/web-retriever/tools.js +99 -137
- package/lib/web-retriever/wind-client.d.ts +14 -31
- package/lib/web-retriever/wind-client.js +16 -32
- package/locale/zh.json +1 -1
- package/package.json +67 -7
- package/preset/capital-generation/README.md +87 -53
- package/preset/capital-generation/agent.patch.yml +526 -337
- package/preset/capital-generation/skills/capital-data-profile/SKILL.md +2 -2
- package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +1 -1
- package/preset/capital-generation/skills/capital-equity-research/SKILL.md +1 -1
- package/preset/capital-generation/skills/capital-event-impact/SKILL.md +1 -1
- package/preset/capital-generation/skills/capital-orchestration/SKILL.md +15 -19
- package/preset/capital-generation/skills/capital-portfolio-review/SKILL.md +1 -1
- package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +9 -12
- package/preset/capital-generation/skills/capital-web-evidence/SKILL.md +1 -1
- package/watchlist/client.js +15 -148
- package/watchlist/client.src.cjs +148 -662
- package/watchlist/holding-rules.js +11 -38
- package/watchlist/index.js +127 -397
- package/watchlist/package.json +2 -2
|
@@ -1,50 +1,27 @@
|
|
|
1
1
|
import type { ProfileTimeAxis } from './store.js';
|
|
2
2
|
export type AxisValueFormat = 'epoch_ms' | 'date_string' | 'unknown';
|
|
3
|
-
/**
|
|
3
|
+
/** Epoch to zone-local YYYY-MM-DD; an invalid zone falls back to UTC and never throws. */
|
|
4
4
|
export declare function isoDateFromEpoch(epochMs: number, zone: string): string;
|
|
5
|
-
/**
|
|
6
|
-
* 某时区下"该日 00:00 的 epoch"。
|
|
7
|
-
*
|
|
8
|
-
* 两遍收敛:先用目标日 UTC 午夜的偏移作估计,再按估计时刻的偏移校正一次——
|
|
9
|
-
* 对固定偏移时区(A 股数据里全部如此)一遍即准,对 DST 边界也只在跨日时刻才有差别。
|
|
10
|
-
*/
|
|
5
|
+
/** Zone-local midnight, two-pass convergence; fixed-offset zones are exact on the first pass. */
|
|
11
6
|
export declare function zoneDayStartMs(date: string, zone: string): number;
|
|
12
|
-
/** 某时区下"该日 23:59:59.999 的 epoch",与 {@link zoneDayStartMs} 同一天。 */
|
|
13
7
|
export declare function zoneDayEndMs(date: string, zone: string): number;
|
|
14
|
-
/** 把毫秒偏移写成 `+08:00` / `-03:30` / `+00:00`。 */
|
|
15
8
|
export declare function formatOffset(offsetMs: number): string;
|
|
16
|
-
/**
|
|
17
|
-
* 从列值推断时间轴的形态、偏移与对齐方式。
|
|
18
|
-
*
|
|
19
|
-
* - 全部是数字且都是 86_400_000 的整数倍 → 日粒度 epoch;偏移 = 值 mod 一天;
|
|
20
|
-
* - 全部是 `YYYY-MM-DD` 样式字符串 → date_string(ISO 写法,模型可读,无需换算);
|
|
21
|
-
* - 其余(秒级 epoch、混合类型、带时间的字符串)→ unknown,**不做猜测**:
|
|
22
|
-
* 宿主宁可让模型看到 `unknown` 并用工具确认,也不要给一个看似合理的错误边界。
|
|
23
|
-
*/
|
|
9
|
+
/** Infer format, offset and alignment from observed values only; anything else is `unknown`, never guessed. */
|
|
24
10
|
export declare function probeTimeAxis(column: string, values: unknown[]): ProfileTimeAxis;
|
|
25
|
-
/**
|
|
11
|
+
/** One calendar-to-column conversion: usable bounds plus the real first/last data dates. */
|
|
26
12
|
export interface AxisBounds {
|
|
27
|
-
/** 日历起点(含):`>= value_ge` 即"从这一天开始"。 */
|
|
28
13
|
value_ge: number | string;
|
|
29
|
-
/** 日历终点(含)的日末:`<= value_le` 即"到这一天结束"。 */
|
|
30
14
|
value_le: number | string;
|
|
31
|
-
/** 该窗口内 Dataset 首行/末行的日期(无数据时为 null)。 */
|
|
32
15
|
data_from: string | null;
|
|
33
16
|
data_to: string | null;
|
|
34
17
|
}
|
|
35
18
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* 三种结果:
|
|
39
|
-
* - `filters`:翻译后的条件(`=` 展开成 `>= value_ge` + `<= value_le` 两条),交给引擎;
|
|
40
|
-
* - `undefined`:这条不需要翻译(值不是日期 / 列不是时间列);
|
|
41
|
-
* - 抛错:**写法在此列上无意义**(日期字符串落到数值时间列等)。绝不静默放行——
|
|
42
|
-
* 旧行为是 `skip_with_warning` 把该行整条跳过,等于"筛选条件消失、返回全量",
|
|
43
|
-
* 而模型只会看到一份看起来成功的全量结果(比报错危险得多)。
|
|
19
|
+
* Translate a date-like filter value into comparable bounds. Dates mean whole days: `=` expands
|
|
20
|
+
* to >=/<=, while `>`, `<`, `in` and `!=` fail loudly instead of silently shifting a day.
|
|
44
21
|
*/
|
|
45
22
|
export declare function translateFilterDateValue(input: {
|
|
46
23
|
axis: ProfileTimeAxis;
|
|
47
|
-
/**
|
|
24
|
+
/** Probe dates in ascending order; period windows clamp to real trading days. */
|
|
48
25
|
dates: string[];
|
|
49
26
|
column: string;
|
|
50
27
|
operator: string;
|
|
@@ -54,19 +31,13 @@ export declare function translateFilterDateValue(input: {
|
|
|
54
31
|
value: unknown;
|
|
55
32
|
}[] | undefined;
|
|
56
33
|
export interface AxisWindow extends AxisBounds {
|
|
57
|
-
/** 窗口名,例如 `last_3_months`、`ytd`、`2026-10`。 */
|
|
58
34
|
name: string;
|
|
59
|
-
/** 解析出的日历区间(半开:start ≤ 日期 ≤ end)。 */
|
|
60
35
|
start_date: string;
|
|
61
36
|
end_date: string;
|
|
62
37
|
}
|
|
63
38
|
/**
|
|
64
|
-
* Dataset
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* 这是 data_junior 侧 `resolve_data_time_range` 的第二形态(第一形态仍是"给 request_data
|
|
68
|
-
* 生成上游时间参数")。两形态共用同一套 period 解析(`time/tools.ts` 的 resolveZonedPeriod),
|
|
69
|
-
* 避免同一个词在两侧得到不同答案。
|
|
39
|
+
* Dataset-mode time window: period -> usable query bounds plus the real first/last dates present.
|
|
40
|
+
* Shares period parsing with capability mode so one phrase cannot mean two things.
|
|
70
41
|
*/
|
|
71
42
|
export interface DatasetTimeResolution {
|
|
72
43
|
mode: 'dataset';
|
|
@@ -74,38 +45,33 @@ export interface DatasetTimeResolution {
|
|
|
74
45
|
timezone: string;
|
|
75
46
|
time_column: string;
|
|
76
47
|
period: string;
|
|
77
|
-
/** 解析出的日历区间(名义区间)。 */
|
|
78
48
|
range: {
|
|
79
49
|
start_date: string;
|
|
80
50
|
end_date: string;
|
|
81
51
|
};
|
|
82
|
-
/** 该区间内**真实存在**的首末日期(非交易日自动夹到相邻交易日)。 */
|
|
83
52
|
data: {
|
|
84
53
|
from: string | null;
|
|
85
54
|
to: string | null;
|
|
86
55
|
days: number | null;
|
|
87
56
|
};
|
|
88
|
-
/**
|
|
57
|
+
/** Real dates present in the range; non-trading days clamp to the nearest session. */
|
|
89
58
|
bounds: {
|
|
90
59
|
bound_ge: number | string;
|
|
91
60
|
bound_le: number | string;
|
|
92
61
|
inclusive: boolean;
|
|
93
62
|
};
|
|
94
63
|
bounds_are_intraday: boolean;
|
|
95
|
-
/** Dataset 的完整覆盖范围(ISO 形态)。 */
|
|
96
64
|
dataset: {
|
|
97
65
|
covered_from: string;
|
|
98
66
|
covered_to: string;
|
|
99
67
|
};
|
|
100
|
-
/**
|
|
68
|
+
/** Facts the model must respect (e.g. nominal range differs from actual data). */
|
|
101
69
|
warnings: string[];
|
|
102
70
|
}
|
|
103
|
-
/** period 的两种写法:字面窗口(`last_3_months`)或 `{unit,count}`。 */
|
|
104
71
|
export type PeriodInput = string | {
|
|
105
72
|
unit: string;
|
|
106
73
|
count: number;
|
|
107
74
|
};
|
|
108
|
-
/** period 的可读名字:对象形态写成 `2_year`,省略或空串写成 `all`。 */
|
|
109
75
|
export declare function periodName(period: PeriodInput | undefined): string;
|
|
110
76
|
export declare function resolveDatasetTimeWindow(input: {
|
|
111
77
|
dataset_id: string;
|
|
@@ -116,21 +82,8 @@ export declare function resolveDatasetTimeWindow(input: {
|
|
|
116
82
|
zone?: string;
|
|
117
83
|
}): DatasetTimeResolution;
|
|
118
84
|
/**
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* `data_from` / `data_to` 用**该列自己的粒度**把窗口夹到真实存在的日期上:
|
|
122
|
-
* 窗口起点落在周末/节假日时前移到下一个有数据的日期,终点回退到最后一个有数据的日期。
|
|
123
|
-
* 这样"近 1 个月"这类窗口不会因为非交易日而给出空结果。
|
|
124
|
-
*/
|
|
125
|
-
/**
|
|
126
|
-
* 把「日历窗口」解析成该时间轴上可用的边界值。
|
|
127
|
-
*
|
|
128
|
-
* `data_from` / `data_to` 用**该列自己的粒度**把窗口夹到真实存在的日期上:窗口首日落在
|
|
129
|
-
* 周末/节假日时前移到下一个有数据的日期,末日回退到最后一个有数据的日期。
|
|
130
|
-
*
|
|
131
|
-
* 锚点本身是非交易日时(例如"以周日为锚的近 1 周")末日**再往后认一天**:
|
|
132
|
-
* 名义窗口 [9/14, 9/20] 里最后一个交易日是 9/18,而锚点想表达的"最近一周"其实该到 9/21。
|
|
133
|
-
* 这不是猜测:只有当紧邻末日的下一个日期确实存在、且它晚于锚点时才会这样延伸。
|
|
85
|
+
* Resolve a calendar window into bounds on this axis. First/last data dates clamp to dates that
|
|
86
|
+
* actually exist; when the anchor is beyond the last data date, the end extends by one session.
|
|
134
87
|
*/
|
|
135
88
|
export declare function resolveAxisWindow(input: {
|
|
136
89
|
axis: ProfileTimeAxis;
|
|
@@ -139,29 +92,16 @@ export declare function resolveAxisWindow(input: {
|
|
|
139
92
|
anchorDate?: string;
|
|
140
93
|
zone?: string;
|
|
141
94
|
}): AxisWindow;
|
|
142
|
-
/**
|
|
143
|
-
* 单值 → 该列里代表"这一天"的取值。
|
|
144
|
-
*
|
|
145
|
-
* `epoch_ms` 轴取当地午夜(value_ge 与 value_le 相等时的那个值),`date_string` 轴原样返回。
|
|
146
|
-
* 用于 filter 的 `=` / `in`:先把日期压成单值,再交给引擎做相等比较。
|
|
147
|
-
*/
|
|
95
|
+
/** Single value representing a whole day: local midnight on epoch axes, the date itself otherwise. */
|
|
148
96
|
export declare function axisValueForDate(axis: ProfileTimeAxis, date: string): number | string;
|
|
149
|
-
/**
|
|
150
|
-
* 读一份 Dataset 的时间轴(形态 + 已出现的日期清单)。
|
|
151
|
-
*
|
|
152
|
-
* 日期清单只用于把窗口夹到真实存在的交易日,所以按升序逐行收集、遇到更早的值才插入,
|
|
153
|
-
* 到上限即停:`date_ms` 数据是有序的,正常情况下一次遍历就装满。
|
|
154
|
-
*/
|
|
97
|
+
/** Collect distinct axis dates in ascending order, capped at MAX_AXIS_DATES; used to clamp windows. */
|
|
155
98
|
export declare const MAX_AXIS_DATES = 20000;
|
|
156
99
|
export declare function readAxisDates(input: {
|
|
157
100
|
axis: ProfileTimeAxis;
|
|
158
101
|
rows: unknown[];
|
|
159
102
|
limit?: number;
|
|
160
103
|
}): string[];
|
|
161
|
-
/**
|
|
162
|
-
*
|
|
163
|
-
* 只给"数据里确实存在"的窗口;空窗口不出现,避免模型拿一个空区间去查询再困惑。
|
|
164
|
-
*/
|
|
104
|
+
/** Default windows for time_facts; windows with no data are omitted rather than returned empty. */
|
|
165
105
|
export declare const DEFAULT_WINDOW_PERIODS: readonly ["last_1_month", "last_3_months", "last_1_year", "ytd"];
|
|
166
106
|
export declare function defaultAxisWindows(input: {
|
|
167
107
|
axis: ProfileTimeAxis;
|
|
@@ -1,47 +1,23 @@
|
|
|
1
1
|
import { DatasetQueryError } from './query.js';
|
|
2
2
|
import { dateParts, periodInputOf, resolveZonedPeriod, zoneOffsetMsByDiff } from '../time/tools.js';
|
|
3
3
|
/**
|
|
4
|
-
* Dataset
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* data_junior 拿到 `time_facts.covered_from = 1695571200000` 之后,要判断覆盖到哪一天、
|
|
8
|
-
* 能不能做 52 周窗口、月末锚点是多少,只能自己把 epoch 换算成日历;而 `query_dataset` 的
|
|
9
|
-
* filter 值又只接受毫秒,于是"2026-08-23 是多少毫秒"这类问题被反复心算。
|
|
10
|
-
* 结果是单步思考 41,961 字符里绝大部分是时间戳算术,且口径随时可能算错(它自己在思考里
|
|
11
|
-
* 写了 "Risky")。时间换算是**确定性计算**,属于宿主:
|
|
12
|
-
*
|
|
13
|
-
* 1. 正向(值 → 日历):{@link isoFromAxisValue} 供 profile 的 time_facts 输出 ISO;
|
|
14
|
-
* 2. 反向(日历 → 值):{@link resolveAxisWindow} 把 ISO 日期/相对期解析成该列自己的边界值;
|
|
15
|
-
* 3. 语义:{@link probeTimeAxis} 从**列自身观测到的偏移**推断时区,绝不猜。
|
|
16
|
-
*
|
|
17
|
-
* 日线数据里 `date_ms` = 当地午夜 00:00 的 epoch(实测 1695571200000 = 2023-09-25 00:00 +08:00
|
|
18
|
-
* = UTC 前一天 16:00),所以"当天"的边界必须按该列的偏移算:值同时满足
|
|
19
|
-
* `>= dayStart` 与 `<= dayEnd` 才算落在这一天。
|
|
4
|
+
* Dataset time axis: aligns calendar dates with the values stored in a column. `date_ms` is
|
|
5
|
+
* local-midnight epoch, so a day means `>= dayStart && <= dayEnd` under the column's observed
|
|
6
|
+
* offset. These conversions are deterministic host work, never left to the model.
|
|
20
7
|
*/
|
|
21
|
-
/** 一天的毫秒数。 */
|
|
22
8
|
const DAY_MS = 86_400_000;
|
|
23
|
-
/**
|
|
24
|
-
* 从"当地午夜的 epoch"反解时区偏移。
|
|
25
|
-
*
|
|
26
|
-
* 列里的值就是某地 00:00 的瞬间,所以它对一天的余数恰好是"UTC 当天几点",
|
|
27
|
-
* 时区偏移 = 该余数的**相反数**:A 股数据余数 57_600_000(UTC 16:00)⇒ +08:00。
|
|
28
|
-
* 余数先归一到 `(-12h, +12h]`(超半天即换到前一天),否则 +08:00 会被读成 -16:00。
|
|
29
|
-
*/
|
|
9
|
+
/** Recover the zone offset from local-midnight epochs: offset = -(value mod day), normalized to (-12h, +12h]. */
|
|
30
10
|
function zoneOffsetFromMidnightValue(value) {
|
|
31
11
|
const remainder = value % DAY_MS;
|
|
32
12
|
const utcHourOfDay = remainder > DAY_MS / 2 ? remainder - DAY_MS : remainder;
|
|
33
13
|
return -utcHourOfDay;
|
|
34
14
|
}
|
|
35
|
-
/**
|
|
15
|
+
/** Inferred offset must lie in UTC-12:00 .. UTC+14:00. */
|
|
36
16
|
const MIN_OFFSET_MS = -12 * 3_600_000;
|
|
37
17
|
const MAX_OFFSET_MS = 14 * 3_600_000;
|
|
38
|
-
/**
|
|
39
|
-
* 把数值列当 epoch 毫秒的必要量级。秒级 epoch(10 位,约 1.7e9)与毫秒级(13 位,约 1.8e12)
|
|
40
|
-
* 差三个数量级:不加这道闸门时,秒级时间戳会被误判成"日粒度 epoch"(1.7e9 恰好是 86400 的
|
|
41
|
-
* 整数倍),于是所有窗口边界整体错到 1970 年附近。1e11 ms ≈ 1973 年,足以把两者分开。
|
|
42
|
-
*/
|
|
18
|
+
/** Epoch must be >= 1e11: second-level epochs can also look day-aligned and would land near 1970. */
|
|
43
19
|
const MIN_EPOCH_MS = 100_000_000_000;
|
|
44
|
-
/**
|
|
20
|
+
/** Offsets matching these get an IANA name; anything else is rendered as UTC±HH:MM. */
|
|
45
21
|
const CANONICAL_ZONES = [
|
|
46
22
|
{ zone: 'Asia/Shanghai', offsetMs: 8 * 3_600_000 },
|
|
47
23
|
{ zone: 'UTC', offsetMs: 0 },
|
|
@@ -49,7 +25,6 @@ const CANONICAL_ZONES = [
|
|
|
49
25
|
function isRecordLike(value) {
|
|
50
26
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
51
27
|
}
|
|
52
|
-
/** 行内取列值:与 store 的 profileCell 同义,只为不把 store 的私有实现引到这里。 */
|
|
53
28
|
function cellOf(row, column) {
|
|
54
29
|
if (!isRecordLike(row))
|
|
55
30
|
return { present: false, value: undefined };
|
|
@@ -57,7 +32,6 @@ function cellOf(row, column) {
|
|
|
57
32
|
? { present: true, value: row[column] }
|
|
58
33
|
: { present: false, value: undefined };
|
|
59
34
|
}
|
|
60
|
-
/** 某时刻在某时区下的日历年月日;时区非法或 Intl 不可用时返回 undefined。 */
|
|
61
35
|
function readDateParts(epochMs, zone) {
|
|
62
36
|
try {
|
|
63
37
|
const parts = new Intl.DateTimeFormat('en-US', { timeZone: zone, year: 'numeric', month: '2-digit', day: '2-digit' })
|
|
@@ -69,7 +43,6 @@ function readDateParts(epochMs, zone) {
|
|
|
69
43
|
return undefined;
|
|
70
44
|
}
|
|
71
45
|
}
|
|
72
|
-
/** `YYYY-MM-DD` 的严格解析:非法日期在这里就响亮失败,不带进任何换算。 */
|
|
73
46
|
function readCalendarDate(value) {
|
|
74
47
|
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value.trim());
|
|
75
48
|
if (!match)
|
|
@@ -81,7 +54,6 @@ function readCalendarDate(value) {
|
|
|
81
54
|
}
|
|
82
55
|
return parts;
|
|
83
56
|
}
|
|
84
|
-
/** 日历天数推进:只按日/周/月/季度算,调用方只用到这三种。 */
|
|
85
57
|
function shiftCalendarDate(value, unit, amount) {
|
|
86
58
|
const parts = dateParts(value);
|
|
87
59
|
if (unit === 'day') {
|
|
@@ -98,30 +70,23 @@ function shiftCalendarDate(value, unit, amount) {
|
|
|
98
70
|
function finiteNumbers(values) {
|
|
99
71
|
return values.filter((value) => typeof value === 'number' && Number.isFinite(value));
|
|
100
72
|
}
|
|
101
|
-
/**
|
|
73
|
+
/** Epoch to zone-local YYYY-MM-DD; an invalid zone falls back to UTC and never throws. */
|
|
102
74
|
export function isoDateFromEpoch(epochMs, zone) {
|
|
103
75
|
const parts = readDateParts(epochMs, zone) ?? readDateParts(epochMs, 'UTC');
|
|
104
76
|
if (parts === undefined)
|
|
105
77
|
return new Date(epochMs).toISOString().slice(0, 10);
|
|
106
78
|
return `${String(parts.year).padStart(4, '0')}-${String(parts.month).padStart(2, '0')}-${String(parts.day).padStart(2, '0')}`;
|
|
107
79
|
}
|
|
108
|
-
/**
|
|
109
|
-
* 某时区下"该日 00:00 的 epoch"。
|
|
110
|
-
*
|
|
111
|
-
* 两遍收敛:先用目标日 UTC 午夜的偏移作估计,再按估计时刻的偏移校正一次——
|
|
112
|
-
* 对固定偏移时区(A 股数据里全部如此)一遍即准,对 DST 边界也只在跨日时刻才有差别。
|
|
113
|
-
*/
|
|
80
|
+
/** Zone-local midnight, two-pass convergence; fixed-offset zones are exact on the first pass. */
|
|
114
81
|
export function zoneDayStartMs(date, zone) {
|
|
115
82
|
const parts = readCalendarDate(date);
|
|
116
83
|
const utcMidnight = Date.UTC(parts.year, parts.month - 1, parts.day);
|
|
117
84
|
const estimated = utcMidnight - zoneOffsetMsByDiff(utcMidnight, zone);
|
|
118
85
|
return utcMidnight - zoneOffsetMsByDiff(estimated, zone);
|
|
119
86
|
}
|
|
120
|
-
/** 某时区下"该日 23:59:59.999 的 epoch",与 {@link zoneDayStartMs} 同一天。 */
|
|
121
87
|
export function zoneDayEndMs(date, zone) {
|
|
122
88
|
return zoneDayStartMs(shiftCalendarDate(date, 'day', 1), zone) - 1;
|
|
123
89
|
}
|
|
124
|
-
/** 把毫秒偏移写成 `+08:00` / `-03:30` / `+00:00`。 */
|
|
125
90
|
export function formatOffset(offsetMs) {
|
|
126
91
|
const sign = offsetMs < 0 ? '-' : '+';
|
|
127
92
|
const abs = Math.abs(offsetMs);
|
|
@@ -129,28 +94,19 @@ export function formatOffset(offsetMs) {
|
|
|
129
94
|
const minutes = Math.floor((abs % 3_600_000) / 60_000);
|
|
130
95
|
return `${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`;
|
|
131
96
|
}
|
|
132
|
-
/** 偏移落到常见数据时区时返回 IANA 名,否则返回 `UTC+08:00` 形态的说明串。 */
|
|
133
97
|
function zoneForOffset(offsetMs) {
|
|
134
98
|
const canonical = CANONICAL_ZONES.find((entry) => entry.offsetMs === offsetMs);
|
|
135
99
|
return canonical ? canonical.zone : `UTC${formatOffset(offsetMs)}`;
|
|
136
100
|
}
|
|
137
|
-
/**
|
|
138
|
-
* 从列值推断时间轴的形态、偏移与对齐方式。
|
|
139
|
-
*
|
|
140
|
-
* - 全部是数字且都是 86_400_000 的整数倍 → 日粒度 epoch;偏移 = 值 mod 一天;
|
|
141
|
-
* - 全部是 `YYYY-MM-DD` 样式字符串 → date_string(ISO 写法,模型可读,无需换算);
|
|
142
|
-
* - 其余(秒级 epoch、混合类型、带时间的字符串)→ unknown,**不做猜测**:
|
|
143
|
-
* 宿主宁可让模型看到 `unknown` 并用工具确认,也不要给一个看似合理的错误边界。
|
|
144
|
-
*/
|
|
101
|
+
/** Infer format, offset and alignment from observed values only; anything else is `unknown`, never guessed. */
|
|
145
102
|
export function probeTimeAxis(column, values) {
|
|
146
103
|
const present = values.filter((value) => value !== null && value !== undefined);
|
|
147
104
|
if (present.length === 0)
|
|
148
105
|
return { column, value_format: 'unknown' };
|
|
149
106
|
const numbers = finiteNumbers(present);
|
|
150
107
|
if (numbers.length === present.length) {
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
// 余数是 57_600_000 而不是 0——按那个判据会把整列判成 unknown(实测踩过)。
|
|
108
|
+
// Daily granularity means every value shares one intra-day remainder, not remainder 0: local
|
|
109
|
+
// midnight is 16:00 UTC for A-share data, so `value % DAY_MS === 0` would reject the whole column.
|
|
154
110
|
const offsetMs = zoneOffsetFromMidnightValue(numbers[0]);
|
|
155
111
|
const dayAligned = numbers.every((value) => Number.isSafeInteger(value)
|
|
156
112
|
&& value >= MIN_EPOCH_MS
|
|
@@ -171,14 +127,8 @@ export function probeTimeAxis(column, values) {
|
|
|
171
127
|
return { column, value_format: 'unknown' };
|
|
172
128
|
}
|
|
173
129
|
/**
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
* 三种结果:
|
|
177
|
-
* - `filters`:翻译后的条件(`=` 展开成 `>= value_ge` + `<= value_le` 两条),交给引擎;
|
|
178
|
-
* - `undefined`:这条不需要翻译(值不是日期 / 列不是时间列);
|
|
179
|
-
* - 抛错:**写法在此列上无意义**(日期字符串落到数值时间列等)。绝不静默放行——
|
|
180
|
-
* 旧行为是 `skip_with_warning` 把该行整条跳过,等于"筛选条件消失、返回全量",
|
|
181
|
-
* 而模型只会看到一份看起来成功的全量结果(比报错危险得多)。
|
|
130
|
+
* Translate a date-like filter value into comparable bounds. Dates mean whole days: `=` expands
|
|
131
|
+
* to >=/<=, while `>`, `<`, `in` and `!=` fail loudly instead of silently shifting a day.
|
|
182
132
|
*/
|
|
183
133
|
export function translateFilterDateValue(input) {
|
|
184
134
|
const { axis, dates, column, operator, value } = input;
|
|
@@ -186,7 +136,7 @@ export function translateFilterDateValue(input) {
|
|
|
186
136
|
return undefined;
|
|
187
137
|
const asDate = typeof value === 'string' ? dateFromText(value) : undefined;
|
|
188
138
|
const asPeriod = isRecordLike(value) ? periodInputOf(value.period) : undefined;
|
|
189
|
-
// `in
|
|
139
|
+
// `in`: recognize dates element-wise; the value counts as a date form if any element is a date.
|
|
190
140
|
const inDates = operator === 'in' && Array.isArray(value)
|
|
191
141
|
? value.filter((item) => typeof item === 'string' && dateFromText(item) !== undefined)
|
|
192
142
|
: [];
|
|
@@ -195,13 +145,10 @@ export function translateFilterDateValue(input) {
|
|
|
195
145
|
const label = asDate ?? (asPeriod === undefined ? inDates.join(',') : `${asPeriod}(period)`);
|
|
196
146
|
const range = /^(>=|<=|=|>|<)$/.exec(operator);
|
|
197
147
|
if (range === null) {
|
|
198
|
-
// `in
|
|
199
|
-
// 展开成两条边界(会被 filter 上限打回),后者根本不是范围查询。
|
|
200
|
-
// 与其给一个差一天的答案,不如让模型改用 >= / <= 明确写出要哪些天。
|
|
148
|
+
// `in`/`!=` cannot express whole-day exclusion or multi-day sets without changing semantics.
|
|
201
149
|
throw new DatasetQueryError('query_type_conflict', `filter ${column} cannot take a date-like value with operator ${operator}; dates always mean whole days, so write the range you want with >= and <= (got ${label})`);
|
|
202
150
|
}
|
|
203
|
-
//
|
|
204
|
-
// 等于悄悄换一个窗口——宁可响亮失败,也不给一个差一天的答案。
|
|
151
|
+
// Strict comparisons would shift the window by a day; fail loudly instead.
|
|
205
152
|
if (operator === '>' || operator === '<') {
|
|
206
153
|
throw new DatasetQueryError('query_type_conflict', `filter ${column} cannot take a date-like value with operator ${operator}; a date means the whole day, so use >= (or <=) instead of comparison ${operator} (for a window, pass YYYY-MM-DD or { period })`);
|
|
207
154
|
}
|
|
@@ -225,7 +172,6 @@ export function translateFilterDateValue(input) {
|
|
|
225
172
|
throw new DatasetQueryError('query_type_conflict', `filter ${column} cannot exclude a whole day by ${label} with ${operator}; ask for the days you want with >= / <= instead`);
|
|
226
173
|
}
|
|
227
174
|
}
|
|
228
|
-
/** `YYYY-MM-DD` / `YYYY-MM` / `YYYY` → 该区间的首日;其余文本返回 undefined。 */
|
|
229
175
|
function dateFromText(value) {
|
|
230
176
|
const text = value.trim();
|
|
231
177
|
const full = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
|
|
@@ -246,7 +192,6 @@ function dateFromText(value) {
|
|
|
246
192
|
const year = /^(\d{4})$/.exec(text);
|
|
247
193
|
return year ? `${year[1]}-01-01` : undefined;
|
|
248
194
|
}
|
|
249
|
-
/** period 的可读名字:对象形态写成 `2_year`,省略或空串写成 `all`。 */
|
|
250
195
|
export function periodName(period) {
|
|
251
196
|
if (period === undefined)
|
|
252
197
|
return 'all';
|
|
@@ -299,21 +244,8 @@ function boundsFor(axis, startDate, endDate) {
|
|
|
299
244
|
};
|
|
300
245
|
}
|
|
301
246
|
/**
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
* `data_from` / `data_to` 用**该列自己的粒度**把窗口夹到真实存在的日期上:
|
|
305
|
-
* 窗口起点落在周末/节假日时前移到下一个有数据的日期,终点回退到最后一个有数据的日期。
|
|
306
|
-
* 这样"近 1 个月"这类窗口不会因为非交易日而给出空结果。
|
|
307
|
-
*/
|
|
308
|
-
/**
|
|
309
|
-
* 把「日历窗口」解析成该时间轴上可用的边界值。
|
|
310
|
-
*
|
|
311
|
-
* `data_from` / `data_to` 用**该列自己的粒度**把窗口夹到真实存在的日期上:窗口首日落在
|
|
312
|
-
* 周末/节假日时前移到下一个有数据的日期,末日回退到最后一个有数据的日期。
|
|
313
|
-
*
|
|
314
|
-
* 锚点本身是非交易日时(例如"以周日为锚的近 1 周")末日**再往后认一天**:
|
|
315
|
-
* 名义窗口 [9/14, 9/20] 里最后一个交易日是 9/18,而锚点想表达的"最近一周"其实该到 9/21。
|
|
316
|
-
* 这不是猜测:只有当紧邻末日的下一个日期确实存在、且它晚于锚点时才会这样延伸。
|
|
247
|
+
* Resolve a calendar window into bounds on this axis. First/last data dates clamp to dates that
|
|
248
|
+
* actually exist; when the anchor is beyond the last data date, the end extends by one session.
|
|
317
249
|
*/
|
|
318
250
|
export function resolveAxisWindow(input) {
|
|
319
251
|
const { axis, dates } = input;
|
|
@@ -322,11 +254,9 @@ export function resolveAxisWindow(input) {
|
|
|
322
254
|
if (sorted.length === 0)
|
|
323
255
|
throw new Error('dataset has no usable time values on the time axis');
|
|
324
256
|
const anchor = input.anchorDate ?? sorted[sorted.length - 1];
|
|
325
|
-
//
|
|
326
|
-
// 只有当锚点明确超出现有数据、且窗口末端被截断时,末日的下一个交易日才属于该窗口。
|
|
257
|
+
// An anchor at the dataset end must not extend into the future; only an anchor beyond the data may.
|
|
327
258
|
const anchorIsDatasetEnd = anchor === sorted[sorted.length - 1];
|
|
328
|
-
//
|
|
329
|
-
// 不能先转成展示名再解析(`2_year` 不是合法 period,会掉进 unsupported period)。
|
|
259
|
+
// Name is display-only: object periods must reach the resolver as-is (`2_year` is not a valid period).
|
|
330
260
|
const period = periodName(input.period);
|
|
331
261
|
const resolved = resolveZonedPeriod(input.period ?? 'all', anchor);
|
|
332
262
|
const clipped = clampToDates(sorted, resolved.start, resolved.end, { anchor, extend: !anchorIsDatasetEnd });
|
|
@@ -342,36 +272,24 @@ export function resolveAxisWindow(input) {
|
|
|
342
272
|
data_to: clipped.to,
|
|
343
273
|
};
|
|
344
274
|
}
|
|
345
|
-
/**
|
|
275
|
+
/** Clamp to existing dates: first >= start, last <= end (optionally one session past). */
|
|
346
276
|
function clampToDates(sorted, start, end, boundary) {
|
|
347
277
|
const inside = sorted.filter((date) => date >= start && date <= end);
|
|
348
278
|
if (inside.length === 0)
|
|
349
279
|
return undefined;
|
|
350
280
|
const from = inside[0];
|
|
351
281
|
const to = inside[inside.length - 1];
|
|
352
|
-
// 窗口末端被前截(末日在锚点之前)、且该日之后确实还有下一个交易日时,才把它认进来。
|
|
353
|
-
// 锚点就是数据最后一天时不延伸:那时窗口本来就该"到锚点为止",多认一天是拖到未来。
|
|
354
282
|
const next = sorted[sorted.indexOf(to) + 1];
|
|
355
283
|
const extended = boundary.extend && next !== undefined && end <= boundary.anchor ? next : to;
|
|
356
284
|
return { from, to: extended };
|
|
357
285
|
}
|
|
358
|
-
/**
|
|
359
|
-
* 单值 → 该列里代表"这一天"的取值。
|
|
360
|
-
*
|
|
361
|
-
* `epoch_ms` 轴取当地午夜(value_ge 与 value_le 相等时的那个值),`date_string` 轴原样返回。
|
|
362
|
-
* 用于 filter 的 `=` / `in`:先把日期压成单值,再交给引擎做相等比较。
|
|
363
|
-
*/
|
|
286
|
+
/** Single value representing a whole day: local midnight on epoch axes, the date itself otherwise. */
|
|
364
287
|
export function axisValueForDate(axis, date) {
|
|
365
288
|
if (axis.value_format === 'date_string')
|
|
366
289
|
return date;
|
|
367
290
|
return zoneDayStartMs(date, axis.time_zone ?? 'UTC');
|
|
368
291
|
}
|
|
369
|
-
/**
|
|
370
|
-
* 读一份 Dataset 的时间轴(形态 + 已出现的日期清单)。
|
|
371
|
-
*
|
|
372
|
-
* 日期清单只用于把窗口夹到真实存在的交易日,所以按升序逐行收集、遇到更早的值才插入,
|
|
373
|
-
* 到上限即停:`date_ms` 数据是有序的,正常情况下一次遍历就装满。
|
|
374
|
-
*/
|
|
292
|
+
/** Collect distinct axis dates in ascending order, capped at MAX_AXIS_DATES; used to clamp windows. */
|
|
375
293
|
export const MAX_AXIS_DATES = 20_000;
|
|
376
294
|
export function readAxisDates(input) {
|
|
377
295
|
const { axis, rows } = input;
|
|
@@ -397,10 +315,7 @@ export function readAxisDates(input) {
|
|
|
397
315
|
}
|
|
398
316
|
return dates.sort();
|
|
399
317
|
}
|
|
400
|
-
/**
|
|
401
|
-
*
|
|
402
|
-
* 只给"数据里确实存在"的窗口;空窗口不出现,避免模型拿一个空区间去查询再困惑。
|
|
403
|
-
*/
|
|
318
|
+
/** Default windows for time_facts; windows with no data are omitted rather than returned empty. */
|
|
404
319
|
export const DEFAULT_WINDOW_PERIODS = ['last_1_month', 'last_3_months', 'last_1_year', 'ytd'];
|
|
405
320
|
export function defaultAxisWindows(input) {
|
|
406
321
|
const windows = [];
|
|
@@ -412,7 +327,6 @@ export function defaultAxisWindows(input) {
|
|
|
412
327
|
windows.push(window);
|
|
413
328
|
}
|
|
414
329
|
catch {
|
|
415
|
-
// 数据覆盖不到这个窗口(例如上市不足一年)时不输出该窗口,不是错误。
|
|
416
330
|
}
|
|
417
331
|
}
|
|
418
332
|
return windows;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis';
|
|
2
2
|
import { DataCollectorHub } from './hub.js';
|
|
3
|
-
/** dc_status
|
|
3
|
+
/** dc_status diagnostics: credential presence only, never values. */
|
|
4
4
|
export interface DataCollectorDiagnostics {
|
|
5
5
|
probeApiKey: () => Promise<{
|
|
6
6
|
present: boolean;
|
|
@@ -8,18 +8,18 @@ export interface DataCollectorDiagnostics {
|
|
|
8
8
|
error?: string;
|
|
9
9
|
}>;
|
|
10
10
|
/**
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
* Tool-surface restriction snapshot (root/role policies), restated locally so this layer does
|
|
12
|
+
* not depend on the agents layer. `allowed` stays empty now that allow-lists are gone.
|
|
13
|
+
*/
|
|
14
14
|
rootPolicy?: () => Array<{
|
|
15
15
|
agentId: string | undefined;
|
|
16
16
|
root: boolean;
|
|
17
17
|
presetId: string;
|
|
18
18
|
outcome: string;
|
|
19
19
|
denied: string[];
|
|
20
|
+
allowed?: string[];
|
|
20
21
|
failed: string[];
|
|
21
22
|
}>;
|
|
22
|
-
/** 每次 dc_status 真实执行时回调(写执行痕迹用;模型可伪造文本,但宿主文件痕迹与计数无法伪造)。 */
|
|
23
23
|
onCall?: (snapshot: {
|
|
24
24
|
at: number;
|
|
25
25
|
call: number;
|
|
@@ -28,5 +28,5 @@ export interface DataCollectorDiagnostics {
|
|
|
28
28
|
}) => void;
|
|
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
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"];
|