@v587d/capital-generation 0.0.0-stage → 2.6.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 +814 -0
- package/LICENSE +21 -0
- package/README.md +118 -2
- package/assets/cg-icon.png +0 -0
- package/capital-config/client.js +28 -0
- package/capital-config/client.src.cjs +388 -0
- package/capital-config/index.js +113 -0
- package/capital-config/package.json +36 -0
- package/capital-watchlist/client.js +182 -0
- package/capital-watchlist/client.src.cjs +1229 -0
- package/capital-watchlist/index.js +772 -0
- package/capital-watchlist/package.json +37 -0
- package/chart-ui/README.md +112 -0
- package/chart-ui/client.js +16 -0
- package/chart-ui/client.src.cjs +126 -0
- package/chart-ui/index.js +205 -0
- package/chart-ui/package.json +30 -0
- package/cordis.patch.yml +72 -0
- package/lib/agents/bash-guard.d.ts +55 -0
- package/lib/agents/bash-guard.js +162 -0
- package/lib/agents/root-tool-policy.d.ts +164 -0
- package/lib/agents/root-tool-policy.js +309 -0
- package/lib/chart/artifact-ref.d.ts +65 -0
- package/lib/chart/artifact-ref.js +107 -0
- package/lib/chart/downsample.d.ts +8 -0
- package/lib/chart/downsample.js +53 -0
- package/lib/chart/errors.d.ts +16 -0
- package/lib/chart/errors.js +15 -0
- package/lib/chart/events.d.ts +120 -0
- package/lib/chart/events.js +471 -0
- package/lib/chart/html.d.ts +21 -0
- package/lib/chart/html.js +154 -0
- package/lib/chart/runtime.d.ts +20 -0
- package/lib/chart/runtime.js +270 -0
- package/lib/chart/series.d.ts +102 -0
- package/lib/chart/series.js +424 -0
- package/lib/chart/source-token.d.ts +64 -0
- package/lib/chart/source-token.js +217 -0
- package/lib/chart/spec.d.ts +58 -0
- package/lib/chart/spec.js +265 -0
- package/lib/chart/tool.d.ts +86 -0
- package/lib/chart/tool.js +288 -0
- package/lib/chart/vendor/LICENSE +201 -0
- package/lib/chart/vendor/VERSION.json +8 -0
- package/lib/chart/vendor/lightweight-charts.standalone.production.js +7 -0
- package/lib/data-collector/args.d.ts +38 -0
- package/lib/data-collector/args.js +114 -0
- package/lib/data-collector/dataset-tools.d.ts +3 -0
- package/lib/data-collector/dataset-tools.js +421 -0
- package/lib/data-collector/describe.d.ts +99 -0
- package/lib/data-collector/describe.js +256 -0
- package/lib/data-collector/hub.d.ts +187 -0
- package/lib/data-collector/hub.js +469 -0
- package/lib/data-collector/query-time.d.ts +58 -0
- package/lib/data-collector/query-time.js +101 -0
- package/lib/data-collector/query.d.ts +57 -0
- package/lib/data-collector/query.js +473 -0
- package/lib/data-collector/store.d.ts +499 -0
- package/lib/data-collector/store.js +1486 -0
- package/lib/data-collector/time-axis.d.ts +169 -0
- package/lib/data-collector/time-axis.js +419 -0
- package/lib/data-collector/tools.d.ts +32 -0
- package/lib/data-collector/tools.js +216 -0
- package/lib/index.d.ts +275 -0
- package/lib/index.js +458 -0
- package/lib/net/eastmoney-client.d.ts +104 -0
- package/lib/net/eastmoney-client.js +118 -0
- package/lib/net/throttle.d.ts +16 -0
- package/lib/net/throttle.js +34 -0
- package/lib/ocr/client.d.ts +82 -0
- package/lib/ocr/client.js +281 -0
- package/lib/ocr/markdown.d.ts +75 -0
- package/lib/ocr/markdown.js +209 -0
- package/lib/ocr/store.d.ts +119 -0
- package/lib/ocr/store.js +226 -0
- package/lib/sources/eastmoney-http.d.ts +2 -0
- package/lib/sources/eastmoney-http.js +1253 -0
- package/lib/sources/fuyao-core.d.ts +52 -0
- package/lib/sources/fuyao-core.js +96 -0
- package/lib/sources/fuyao-rest.d.ts +14 -0
- package/lib/sources/fuyao-rest.js +2057 -0
- package/lib/sources/security-code.d.ts +27 -0
- package/lib/sources/security-code.js +104 -0
- package/lib/sources/security-identity.d.ts +24 -0
- package/lib/sources/security-identity.js +89 -0
- package/lib/sources/tencent-http.d.ts +2 -0
- package/lib/sources/tencent-http.js +1053 -0
- package/lib/sources/wind-mcp.d.ts +8 -0
- package/lib/sources/wind-mcp.js +593 -0
- package/lib/time/tools.d.ts +74 -0
- package/lib/time/tools.js +464 -0
- package/lib/tool-exec.d.ts +29 -0
- package/lib/tool-exec.js +17 -0
- package/lib/web-retriever/engines.d.ts +61 -0
- package/lib/web-retriever/engines.js +202 -0
- package/lib/web-retriever/html-markdown.d.ts +50 -0
- package/lib/web-retriever/html-markdown.js +321 -0
- package/lib/web-retriever/local-fetch.d.ts +121 -0
- package/lib/web-retriever/local-fetch.js +562 -0
- package/lib/web-retriever/retriever.d.ts +58 -0
- package/lib/web-retriever/retriever.js +94 -0
- package/lib/web-retriever/sources.d.ts +235 -0
- package/lib/web-retriever/sources.js +961 -0
- package/lib/web-retriever/tools.d.ts +50 -0
- package/lib/web-retriever/tools.js +1029 -0
- package/lib/web-retriever/wind-client.d.ts +69 -0
- package/lib/web-retriever/wind-client.js +311 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +84 -4
- package/preset/capital-generation/README.md +161 -0
- package/preset/capital-generation/agent.patch.yml +509 -0
- package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +137 -0
- package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +416 -0
- package/preset/capital-generation/skills/capital-orchestration/SKILL.md +174 -0
- package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +296 -0
- package/preset/capital-generation/skills/capital-web-protocol/SKILL.md +240 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import type { TimeAxisSnapshot } from '../data-collector/store.js';
|
|
3
|
+
/**
|
|
4
|
+
* 时间工具需要的 Dataset 通道。只依赖 store 的**只读探针**,
|
|
5
|
+
* 因此 time 平面不需要认识 Dataset 的写入、profile 或查询实现。
|
|
6
|
+
*/
|
|
7
|
+
export interface TimeAxisReader {
|
|
8
|
+
describeTimeAxis(input: {
|
|
9
|
+
session: unknown;
|
|
10
|
+
dataset_id: string;
|
|
11
|
+
time_column?: string;
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
}): Promise<TimeAxisSnapshot | undefined>;
|
|
14
|
+
}
|
|
15
|
+
export type DataTimeContractKind = 'epoch_range' | 'date_range' | 'date' | 'date_ms' | 'enum_range' | 'nested_epoch_range';
|
|
16
|
+
export interface DataTimeContract {
|
|
17
|
+
kind: DataTimeContractKind;
|
|
18
|
+
fields: string[];
|
|
19
|
+
maxDays?: number;
|
|
20
|
+
enumValues?: string[];
|
|
21
|
+
enumMap?: Record<string, string>;
|
|
22
|
+
warning?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function getDataTimeContract(capability: string): DataTimeContract | undefined;
|
|
25
|
+
/** 校验并规范化 IANA 时区名;undefined 表示宿主本地时区。'UTC'/'GMT' 为合法特殊值。 */
|
|
26
|
+
export declare function resolveTimeZone(timeZone: unknown): string | undefined;
|
|
27
|
+
/** 宿主进程的本地 IANA 时区名。 */
|
|
28
|
+
export declare function localTimeZone(): string;
|
|
29
|
+
/**
|
|
30
|
+
* 差值法计算某 IANA 时区在 now 时刻的 UTC 偏移毫秒数(timeZoneName 缺失时的回退)。
|
|
31
|
+
*/
|
|
32
|
+
export declare function zoneOffsetMsByDiff(now: number, zone: string): number;
|
|
33
|
+
/** 当前时刻读数(内部纯函数,便于测试):now 为 epoch 毫秒。 */
|
|
34
|
+
export declare function readClock(now: number, timeZone?: string): Record<string, unknown>;
|
|
35
|
+
/**
|
|
36
|
+
* 注册时间工具(会话组作用域:主 Agent 与所有子 Agent 均可见,与数据工具同一
|
|
37
|
+
* 注册通道)。模型知识截止日期不是当前时间;涉及"今天/现在/此刻/周几"的判断
|
|
38
|
+
* 必须先调用本工具,绝不凭记忆猜测。
|
|
39
|
+
*/
|
|
40
|
+
export declare function registerTimeTool(ctx: Context, store?: TimeAxisReader): void;
|
|
41
|
+
type DateParts = {
|
|
42
|
+
year: number;
|
|
43
|
+
month: number;
|
|
44
|
+
day: number;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* period 参数的**唯一归一入口**:接受字面窗口字符串、`{unit,count}`,以及被序列化成
|
|
48
|
+
* 字符串的 `{"unit":"year","count":2}`。
|
|
49
|
+
*
|
|
50
|
+
* 最后一种不是宽容癖好:真机实测模型会这么传(会话 cf464cb6 与 8002e358 各一次,
|
|
51
|
+
* 两次都掉进 `unsupported period` 白跑一轮)。宿主对 QuerySpec 的数组/对象参数早有同一套
|
|
52
|
+
* 宽容解析(`src/data-collector/args.ts`),period 没有理由更严格。
|
|
53
|
+
* 返回值 `undefined` = 调用方没给 period(取数形态按当前日期、Dataset 形态按数据最后一天)。
|
|
54
|
+
*/
|
|
55
|
+
export declare function periodInputOf(value: unknown): string | {
|
|
56
|
+
unit: string;
|
|
57
|
+
count: number;
|
|
58
|
+
} | undefined;
|
|
59
|
+
export declare function dateParts(value: string): DateParts;
|
|
60
|
+
export declare function shiftDate(value: string, unit: 'day' | 'week' | 'month' | 'quarter' | 'year', amount: number): string;
|
|
61
|
+
/**
|
|
62
|
+
* 单窗口解析(**共享**入口):`resolve_data_time_range`(能力参数那个模式)与
|
|
63
|
+
* `time-axis.ts`(Dataset 时间轴模式)都用它,避免两处对同一个 period 词表给出不同答案。
|
|
64
|
+
*
|
|
65
|
+
* `resolvePeriod` 之外再认三种字面窗口:`all`(全区间)、`ytd`(年初到锚点)、
|
|
66
|
+
* `YYYY-MM`(自然月)。它们不新增相对期语义,只是把"年内/某月"这两个最常用的
|
|
67
|
+
* 分析窗口写成模型本来就会用的词。对象形态 `{unit,count}` 原样交给 `resolvePeriod`。
|
|
68
|
+
*/
|
|
69
|
+
export declare function resolveZonedPeriod(input: unknown, anchor: string): {
|
|
70
|
+
start: string;
|
|
71
|
+
end: string;
|
|
72
|
+
};
|
|
73
|
+
export declare function resolveDataTimeRange(args: Record<string, unknown>, now?: number): Record<string, unknown>;
|
|
74
|
+
export {};
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { resolveDatasetTimeWindow } from '../data-collector/time-axis.js';
|
|
2
|
+
import { delegatedSession } from '../tool-exec.js';
|
|
3
|
+
const jsonObject = (properties = {}, required = []) => ({ type: 'object', properties, required, additionalProperties: false });
|
|
4
|
+
function render(_args, value) {
|
|
5
|
+
// ToolOutputDefinition.render(args, value):第一参是入参、第二参才是返回值。
|
|
6
|
+
return [{ type: 'text', text: JSON.stringify(value) }];
|
|
7
|
+
}
|
|
8
|
+
const IANA_TIME_ZONE = /^[A-Za-z][A-Za-z0-9_+.-]*(?:\/[A-Za-z0-9_+.-]+)+$/;
|
|
9
|
+
const LAST_YEAR_DAYS = 366;
|
|
10
|
+
const TEN_YEAR_DAYS = 3660;
|
|
11
|
+
const FIVE_YEAR_DAYS = 1830;
|
|
12
|
+
/** 需要 `query_hint`(时间范围提示语)的检索能力,显式点名(见使用处的 ⛔ 注释)。 */
|
|
13
|
+
const RETRIEVAL_HINT_CAPABILITIES = new Set([
|
|
14
|
+
'anysearch_search',
|
|
15
|
+
'web_retriever_fetch',
|
|
16
|
+
'wind_docs_announcements',
|
|
17
|
+
'wind_docs_news',
|
|
18
|
+
]);
|
|
19
|
+
/** 数据接口时间参数的唯一模型可见契约;不从自然语言 description 反推规则。 */
|
|
20
|
+
const DATA_TIME_CONTRACTS = {
|
|
21
|
+
history: { kind: 'epoch_range', fields: ['start', 'end'], maxDays: TEN_YEAR_DAYS },
|
|
22
|
+
tencent_kline: { kind: 'date_range', fields: ['start', 'end'], maxDays: TEN_YEAR_DAYS, warning: '腾讯 K 线的 start/end 仅支持日/周/月;分钟线只能取最近 count 根' },
|
|
23
|
+
index_history: { kind: 'epoch_range', fields: ['start', 'end'], maxDays: TEN_YEAR_DAYS },
|
|
24
|
+
income_statement: { kind: 'epoch_range', fields: ['start', 'end'], maxDays: TEN_YEAR_DAYS },
|
|
25
|
+
balance_sheet: { kind: 'epoch_range', fields: ['start', 'end'], maxDays: TEN_YEAR_DAYS },
|
|
26
|
+
cash_flow: { kind: 'epoch_range', fields: ['start', 'end'], maxDays: TEN_YEAR_DAYS },
|
|
27
|
+
fund_history: { kind: 'epoch_range', fields: ['start', 'end'], maxDays: FIVE_YEAR_DAYS },
|
|
28
|
+
fund_performance_history: { kind: 'epoch_range', fields: ['start', 'end'] },
|
|
29
|
+
fund_indicator_line: { kind: 'nested_epoch_range', fields: ['time_range'], warning: 'time_range 只是该接口的时间片段;indexes 仍须按能力契约另行提供' },
|
|
30
|
+
corporate_actions: { kind: 'date_range', fields: ['from', 'to'] },
|
|
31
|
+
hot_stock_rank_trend: { kind: 'date_range', fields: ['start_date', 'end_date'], maxDays: LAST_YEAR_DAYS },
|
|
32
|
+
hot_stock_history: { kind: 'date', fields: ['date'], maxDays: LAST_YEAR_DAYS },
|
|
33
|
+
dragon_tiger: { kind: 'date', fields: ['date'], maxDays: LAST_YEAR_DAYS, warning: '显式日期必须是交易日;工具不会自动回退到最近交易日' },
|
|
34
|
+
eastmoney_top_buy_sell_market: { kind: 'date_range', fields: ['start_date', 'end_date'], maxDays: LAST_YEAR_DAYS },
|
|
35
|
+
eastmoney_top_buy_sell_ticker: { kind: 'date_range', fields: ['start_date', 'end_date'], maxDays: LAST_YEAR_DAYS },
|
|
36
|
+
eastmoney_lockup_expiry: { kind: 'date_range', fields: ['start_date', 'end_date'] },
|
|
37
|
+
// 宏观表:区间是**报告期**(月度/季度),不设 maxDays——这些序列要能翻到 2010 年以前。
|
|
38
|
+
eastmoney_cpi: { kind: 'date_range', fields: ['start_date', 'end_date'] },
|
|
39
|
+
eastmoney_ppi: { kind: 'date_range', fields: ['start_date', 'end_date'] },
|
|
40
|
+
eastmoney_gdp: { kind: 'date_range', fields: ['start_date', 'end_date'] },
|
|
41
|
+
eastmoney_pmi: { kind: 'date_range', fields: ['start_date', 'end_date'] },
|
|
42
|
+
eastmoney_money_supply: { kind: 'date_range', fields: ['start_date', 'end_date'] },
|
|
43
|
+
eastmoney_rmb_loan: { kind: 'date_range', fields: ['start_date', 'end_date'] },
|
|
44
|
+
eastmoney_customs_trade: { kind: 'date_range', fields: ['start_date', 'end_date'] },
|
|
45
|
+
eastmoney_retail_sales: { kind: 'date_range', fields: ['start_date', 'end_date'] },
|
|
46
|
+
eastmoney_deposit_reserve: { kind: 'date_range', fields: ['start_date', 'end_date'], warning: '存准率是**调整事件表**:区间里没有调整就是真实空结果,不是取数失败,不要重试' },
|
|
47
|
+
eastmoney_mutual_flow: { kind: 'date_range', fields: ['start_date', 'end_date'] },
|
|
48
|
+
eastmoney_dividend_plan: { kind: 'date_range', fields: ['start_date', 'end_date'], warning: '区间过滤的是**除权除息日**,不是报告期' },
|
|
49
|
+
eastmoney_margin_trading: { kind: 'date_range', fields: ['start_date', 'end_date'], warning: '两融披露沪市比深市快一个交易日:跨市场同日比较前先对齐到共同日期' },
|
|
50
|
+
eastmoney_convertible_bond_list: { kind: 'date_range', fields: ['start_date', 'end_date'], warning: '区间过滤的是**起息日**(实测与网上申购日恒等),不是上市日也不是摘牌日' },
|
|
51
|
+
auction_benchmark: { kind: 'date', fields: ['date'], warning: '显式日期必须是交易日;工具不会自动回退到最近交易日' },
|
|
52
|
+
limit_up_pool: { kind: 'date_ms', fields: ['date_ms'], warning: 'date_ms 必须对应交易日;工具不会自动回退到最近交易日' },
|
|
53
|
+
limit_down_pool: { kind: 'date_ms', fields: ['date_ms'], warning: 'date_ms 必须对应交易日;工具不会自动回退到最近交易日' },
|
|
54
|
+
limit_break_pool: { kind: 'date_ms', fields: ['date_ms'], warning: 'date_ms 必须对应交易日;工具不会自动回退到最近交易日' },
|
|
55
|
+
fund_stock_history: { kind: 'date', fields: ['end_date'], warning: 'end_date 必须是可用报告期截止日;如需确认,请先查询 fund_stock_report_dates' },
|
|
56
|
+
fund_bond_history: { kind: 'date', fields: ['end_date'], warning: 'end_date 必须是可用报告期截止日;如需确认,请先查询 fund_bond_report_dates' },
|
|
57
|
+
fund_nav: { kind: 'enum_range', fields: ['range'], enumValues: ['week', 'month', 'tmonth', 'hyear', 'year', 'twoyear', 'tyear', 'fyear'], enumMap: { last_7_days: 'week', last_1_month: 'month', last_3_months: 'tmonth', last_6_months: 'hyear', last_1_year: 'year', last_2_years: 'twoyear', last_3_years: 'tyear', last_5_years: 'fyear' } },
|
|
58
|
+
fund_manager_performance: { kind: 'enum_range', fields: ['range'], enumValues: ['month', 'tmonth', 'year', 'nowyear', 'now'], enumMap: { last_1_month: 'month', last_3_months: 'tmonth', last_1_year: 'year', current_year: 'nowyear', since_inception: 'now' } },
|
|
59
|
+
anysearch_search: { kind: 'date_range', fields: ['start_date', 'end_date'] },
|
|
60
|
+
wind_docs_announcements: { kind: 'date_range', fields: ['start_date', 'end_date'] },
|
|
61
|
+
wind_docs_news: { kind: 'date_range', fields: ['start_date', 'end_date'] },
|
|
62
|
+
// Wind EDB 的区间是**报告期**(月/季粒度),要能翻到几十年前,与东财那九张宏观表同口径:不设 maxDays。
|
|
63
|
+
wind_edb_query: { kind: 'date_range', fields: ['start_date', 'end_date'] },
|
|
64
|
+
wind_stock_kline: {
|
|
65
|
+
kind: 'date_range',
|
|
66
|
+
fields: ['start_date', 'end_date'],
|
|
67
|
+
maxDays: TEN_YEAR_DAYS,
|
|
68
|
+
warning: '分钟档(m1/m5/m15/m30)区间上限 10 天,超出会在参数层被拒;adjust=qfq 时 end_date 不得晚于当日(afdate 由本能力锁成当日)',
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
export function getDataTimeContract(capability) {
|
|
72
|
+
const contract = DATA_TIME_CONTRACTS[capability];
|
|
73
|
+
return contract ? { ...contract, fields: [...contract.fields], ...(contract.enumValues ? { enumValues: [...contract.enumValues] } : {}), ...(contract.enumMap ? { enumMap: { ...contract.enumMap } } : {}) } : undefined;
|
|
74
|
+
}
|
|
75
|
+
/** 校验并规范化 IANA 时区名;undefined 表示宿主本地时区。'UTC'/'GMT' 为合法特殊值。 */
|
|
76
|
+
export function resolveTimeZone(timeZone) {
|
|
77
|
+
if (timeZone === undefined)
|
|
78
|
+
return undefined;
|
|
79
|
+
if (typeof timeZone !== 'string' || timeZone.length === 0 || timeZone.length > 64) {
|
|
80
|
+
throw new Error('timezone must be an IANA name string (e.g. "Asia/Shanghai")');
|
|
81
|
+
}
|
|
82
|
+
if (timeZone !== 'UTC' && timeZone !== 'GMT' && !IANA_TIME_ZONE.test(timeZone)) {
|
|
83
|
+
throw new Error('timezone must be an IANA name string (e.g. "Asia/Shanghai")');
|
|
84
|
+
}
|
|
85
|
+
let resolved;
|
|
86
|
+
try {
|
|
87
|
+
resolved = new Intl.DateTimeFormat('en-US', { timeZone }).resolvedOptions().timeZone;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
throw new Error(`unsupported IANA timezone: ${JSON.stringify(timeZone)}`);
|
|
91
|
+
}
|
|
92
|
+
return resolved;
|
|
93
|
+
}
|
|
94
|
+
/** 宿主进程的本地 IANA 时区名。 */
|
|
95
|
+
export function localTimeZone() {
|
|
96
|
+
return new Intl.DateTimeFormat('en-US').resolvedOptions().timeZone;
|
|
97
|
+
}
|
|
98
|
+
function two(value) {
|
|
99
|
+
return String(value).padStart(2, '0');
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* 把 Intl 的 timeZoneName("GMT+08:00" / "GMT-05:00" / "GMT")规范化为
|
|
103
|
+
* "+08:00" / "-05:00" / "+00:00"。解析失败返回 null,调用方回退到差值算法。
|
|
104
|
+
*/
|
|
105
|
+
function normalizeUtcOffset(timeZoneName) {
|
|
106
|
+
if (timeZoneName === 'GMT' || timeZoneName === 'UTC')
|
|
107
|
+
return '+00:00';
|
|
108
|
+
const match = /^GMT([+-])(\d{1,2})(?::(\d{2}))?$/.exec(timeZoneName);
|
|
109
|
+
if (!match)
|
|
110
|
+
return null;
|
|
111
|
+
return `${match[1]}${two(Number(match[2]))}:${match[3] ?? '00'}`;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* 差值法计算某 IANA 时区在 now 时刻的 UTC 偏移毫秒数(timeZoneName 缺失时的回退)。
|
|
115
|
+
*/
|
|
116
|
+
export function zoneOffsetMsByDiff(now, zone) {
|
|
117
|
+
const fields = {
|
|
118
|
+
year: 'numeric', month: '2-digit', day: '2-digit',
|
|
119
|
+
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
|
120
|
+
hourCycle: 'h23',
|
|
121
|
+
};
|
|
122
|
+
const read = (parts) => {
|
|
123
|
+
const map = Object.fromEntries(parts.map((part) => [part.type, part.value]));
|
|
124
|
+
return Date.UTC(Number(map.year), Number(map.month) - 1, Number(map.day), Number(map.hour), Number(map.minute), Number(map.second));
|
|
125
|
+
};
|
|
126
|
+
const utcParts = new Intl.DateTimeFormat('en-US', { ...fields, timeZone: 'UTC' }).formatToParts(now);
|
|
127
|
+
const zoneParts = new Intl.DateTimeFormat('en-US', { ...fields, timeZone: zone }).formatToParts(now);
|
|
128
|
+
return read(zoneParts) - read(utcParts);
|
|
129
|
+
}
|
|
130
|
+
/** 当前时刻读数(内部纯函数,便于测试):now 为 epoch 毫秒。 */
|
|
131
|
+
export function readClock(now, timeZone) {
|
|
132
|
+
const zone = resolveTimeZone(timeZone) ?? localTimeZone();
|
|
133
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
134
|
+
timeZone: zone,
|
|
135
|
+
year: 'numeric', month: '2-digit', day: '2-digit',
|
|
136
|
+
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
|
137
|
+
hourCycle: 'h23', weekday: 'long', timeZoneName: 'longOffset',
|
|
138
|
+
});
|
|
139
|
+
const parts = Object.fromEntries(formatter.formatToParts(now).map((part) => [part.type, part.value]));
|
|
140
|
+
let utcOffset = parts.timeZoneName ? normalizeUtcOffset(parts.timeZoneName) : null;
|
|
141
|
+
if (utcOffset === null) {
|
|
142
|
+
const offsetMs = zoneOffsetMsByDiff(now, zone);
|
|
143
|
+
const sign = offsetMs < 0 ? '-' : '+';
|
|
144
|
+
const abs = Math.abs(offsetMs);
|
|
145
|
+
utcOffset = `${sign}${two(Math.floor(abs / 3_600_000))}:${two(Math.floor((abs % 3_600_000) / 60_000))}`;
|
|
146
|
+
}
|
|
147
|
+
const offsetMinutes = (utcOffset.startsWith('-') ? -1 : 1) * (Number(utcOffset.slice(1, 3)) * 60 + Number(utcOffset.slice(4, 6)));
|
|
148
|
+
return {
|
|
149
|
+
iso_local: `${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}:${parts.second}${utcOffset}`,
|
|
150
|
+
iso_utc: new Date(now).toISOString(),
|
|
151
|
+
date: `${parts.year}-${parts.month}-${parts.day}`,
|
|
152
|
+
time: `${parts.hour}:${parts.minute}:${parts.second}`,
|
|
153
|
+
weekday: parts.weekday,
|
|
154
|
+
timezone: zone,
|
|
155
|
+
utc_offset: utcOffset,
|
|
156
|
+
utc_offset_minutes: offsetMinutes,
|
|
157
|
+
epoch_ms: now,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* 注册时间工具(会话组作用域:主 Agent 与所有子 Agent 均可见,与数据工具同一
|
|
162
|
+
* 注册通道)。模型知识截止日期不是当前时间;涉及"今天/现在/此刻/周几"的判断
|
|
163
|
+
* 必须先调用本工具,绝不凭记忆猜测。
|
|
164
|
+
*/
|
|
165
|
+
export function registerTimeTool(ctx, store) {
|
|
166
|
+
const tools = ctx.get('tools');
|
|
167
|
+
if (!tools)
|
|
168
|
+
return;
|
|
169
|
+
const definition = {
|
|
170
|
+
name: 'get_local_datetime',
|
|
171
|
+
description: '获取当前真实日期时间的权威读数:宿主机器时钟 + IANA 时区(本地时区名与 UTC 偏移)。' +
|
|
172
|
+
'模型知识截止日期绝不是当前时间;凡涉及"今天/现在/最新/此刻/周几/多少天前"的判断或时间戳换算,' +
|
|
173
|
+
'必须先调用本工具取得当前时刻,涉及数据接口时间范围时改用 resolve_data_time_range,不要自行换算。可选 timezone 参数可指定 IANA 时区显示。',
|
|
174
|
+
parameters: jsonObject({ timezone: { type: 'string', description: '可选 IANA 时区名(如 "Asia/Shanghai");缺省为宿主本地时区' } }),
|
|
175
|
+
output: {
|
|
176
|
+
schema: jsonObject({
|
|
177
|
+
iso_local: { type: 'string' },
|
|
178
|
+
iso_utc: { type: 'string' },
|
|
179
|
+
date: { type: 'string' },
|
|
180
|
+
time: { type: 'string' },
|
|
181
|
+
weekday: { type: 'string' },
|
|
182
|
+
timezone: { type: 'string' },
|
|
183
|
+
utc_offset: { type: 'string' },
|
|
184
|
+
utc_offset_minutes: { type: 'integer' },
|
|
185
|
+
epoch_ms: { type: 'integer' },
|
|
186
|
+
}, ['iso_local', 'iso_utc', 'date', 'time', 'weekday', 'timezone', 'utc_offset', 'utc_offset_minutes', 'epoch_ms']),
|
|
187
|
+
render,
|
|
188
|
+
},
|
|
189
|
+
async execute(args, _exec) {
|
|
190
|
+
return readClock(Date.now(), resolveTimeZone(args.timezone));
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
ctx.effect(() => tools.register(definition), 'capital-generation.tool(get_local_datetime)');
|
|
194
|
+
ctx.effect(() => tools.register(dataTimeRangeDefinition(store)), 'capital-generation.tool(resolve_data_time_range)');
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* period 参数的**唯一归一入口**:接受字面窗口字符串、`{unit,count}`,以及被序列化成
|
|
198
|
+
* 字符串的 `{"unit":"year","count":2}`。
|
|
199
|
+
*
|
|
200
|
+
* 最后一种不是宽容癖好:真机实测模型会这么传(会话 cf464cb6 与 8002e358 各一次,
|
|
201
|
+
* 两次都掉进 `unsupported period` 白跑一轮)。宿主对 QuerySpec 的数组/对象参数早有同一套
|
|
202
|
+
* 宽容解析(`src/data-collector/args.ts`),period 没有理由更严格。
|
|
203
|
+
* 返回值 `undefined` = 调用方没给 period(取数形态按当前日期、Dataset 形态按数据最后一天)。
|
|
204
|
+
*/
|
|
205
|
+
export function periodInputOf(value) {
|
|
206
|
+
if (typeof value === 'string') {
|
|
207
|
+
const text = value.trim();
|
|
208
|
+
if (text.length === 0)
|
|
209
|
+
return undefined;
|
|
210
|
+
if (text.startsWith('{')) {
|
|
211
|
+
try {
|
|
212
|
+
const parsed = JSON.parse(text);
|
|
213
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
214
|
+
const spec = parsed;
|
|
215
|
+
if (typeof spec.unit === 'string' && Number.isSafeInteger(spec.count))
|
|
216
|
+
return { unit: spec.unit, count: Number(spec.count) };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
// 解析不了就当字面窗口交给下面的解析器报错:错误信息更具体(列出支持的写法)。
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return text;
|
|
224
|
+
}
|
|
225
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
226
|
+
const spec = value;
|
|
227
|
+
if (typeof spec.unit === 'string' && Number.isSafeInteger(spec.count))
|
|
228
|
+
return { unit: spec.unit, count: Number(spec.count) };
|
|
229
|
+
}
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
export function dateParts(value) {
|
|
233
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
234
|
+
if (!match)
|
|
235
|
+
throw new Error('period dates must use YYYY-MM-DD');
|
|
236
|
+
const parts = { year: Number(match[1]), month: Number(match[2]), day: Number(match[3]) };
|
|
237
|
+
if (parts.year < 1990)
|
|
238
|
+
throw new Error(`date is outside the supported range: ${value}`);
|
|
239
|
+
const check = new Date(Date.UTC(parts.year, parts.month - 1, parts.day));
|
|
240
|
+
if (check.getUTCFullYear() !== parts.year || check.getUTCMonth() + 1 !== parts.month || check.getUTCDate() !== parts.day)
|
|
241
|
+
throw new Error(`invalid calendar date: ${value}`);
|
|
242
|
+
return parts;
|
|
243
|
+
}
|
|
244
|
+
function dateText(parts) { return `${String(parts.year).padStart(4, '0')}-${two(parts.month)}-${two(parts.day)}`; }
|
|
245
|
+
export function shiftDate(value, unit, amount) {
|
|
246
|
+
const source = dateParts(value);
|
|
247
|
+
if (unit === 'day' || unit === 'week') {
|
|
248
|
+
// 周就是 7 天;"含锚点当天"的减一天由调用方(resolvePeriod)负责,
|
|
249
|
+
// 这样 ±1 天与 ±1 周只差一个乘数,不会在两处各减一次。
|
|
250
|
+
const date = new Date(Date.UTC(source.year, source.month - 1, source.day));
|
|
251
|
+
date.setUTCDate(date.getUTCDate() + amount * (unit === 'week' ? 7 : 1));
|
|
252
|
+
return dateText({ year: date.getUTCFullYear(), month: date.getUTCMonth() + 1, day: date.getUTCDate() });
|
|
253
|
+
}
|
|
254
|
+
const totalMonths = source.year * 12 + source.month - 1 + amount * (unit === 'quarter' ? 3 : unit === 'year' ? 12 : 1);
|
|
255
|
+
const year = Math.floor(totalMonths / 12);
|
|
256
|
+
const month = totalMonths % 12 + 1;
|
|
257
|
+
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
258
|
+
return dateText({ year, month, day: Math.min(source.day, lastDay) });
|
|
259
|
+
}
|
|
260
|
+
function localDateAt(epoch, zone) {
|
|
261
|
+
const parts = Object.fromEntries(new Intl.DateTimeFormat('en-US', { timeZone: zone, year: 'numeric', month: '2-digit', day: '2-digit' }).formatToParts(epoch).map((part) => [part.type, part.value]));
|
|
262
|
+
return `${parts.year}-${parts.month}-${parts.day}`;
|
|
263
|
+
}
|
|
264
|
+
function zonedDateEpoch(value, zone, endOfDay = false) {
|
|
265
|
+
const parts = dateParts(value);
|
|
266
|
+
const base = Date.UTC(parts.year, parts.month - 1, parts.day, endOfDay ? 23 : 0, endOfDay ? 59 : 0, endOfDay ? 59 : 0, endOfDay ? 999 : 0);
|
|
267
|
+
return base - zoneOffsetMsByDiff(base, zone);
|
|
268
|
+
}
|
|
269
|
+
function dayDistance(start, end) {
|
|
270
|
+
const from = dateParts(start);
|
|
271
|
+
const to = dateParts(end);
|
|
272
|
+
return Math.round((Date.UTC(to.year, to.month - 1, to.day) - Date.UTC(from.year, from.month - 1, from.day)) / 86_400_000);
|
|
273
|
+
}
|
|
274
|
+
function resolvePeriod(input, anchor) {
|
|
275
|
+
const period = periodInputOf(input);
|
|
276
|
+
if (period && typeof period === 'object' && !Array.isArray(period)) {
|
|
277
|
+
const spec = period;
|
|
278
|
+
if (!['day', 'week', 'month', 'quarter', 'year'].includes(String(spec.unit)) || typeof spec.count !== 'number' || !Number.isSafeInteger(spec.count) || spec.count < 1)
|
|
279
|
+
throw new Error('period object must be { unit: day|week|month|quarter|year, count: positive integer }');
|
|
280
|
+
const unit = spec.unit;
|
|
281
|
+
const count = spec.count;
|
|
282
|
+
// 与字符串形态同口径:含锚点当天 ⇒ 只有 day 少退一步。
|
|
283
|
+
return { start: shiftDate(anchor, unit, unit === 'day' ? -(count - 1) : -count), end: anchor };
|
|
284
|
+
}
|
|
285
|
+
if (typeof period !== 'string' || period.trim().length === 0)
|
|
286
|
+
throw new Error('period is required');
|
|
287
|
+
const value = period.trim().toLowerCase();
|
|
288
|
+
if (/^\d{4}$/.test(value))
|
|
289
|
+
return { start: `${value}-01-01`, end: `${value}-12-31` };
|
|
290
|
+
if (value === 'today')
|
|
291
|
+
return { start: anchor, end: anchor };
|
|
292
|
+
if (value === 'yesterday') {
|
|
293
|
+
const date = shiftDate(anchor, 'day', -1);
|
|
294
|
+
return { start: date, end: date };
|
|
295
|
+
}
|
|
296
|
+
if (value === 'current_year')
|
|
297
|
+
return { start: `${dateParts(anchor).year}-01-01`, end: anchor };
|
|
298
|
+
if (value === 'previous_year') {
|
|
299
|
+
const year = dateParts(anchor).year - 1;
|
|
300
|
+
return { start: `${year}-01-01`, end: `${year}-12-31` };
|
|
301
|
+
}
|
|
302
|
+
const match = /^last_(\d+)_(day|days|week|weeks|month|months|year|years)$/.exec(value);
|
|
303
|
+
if (!match)
|
|
304
|
+
throw new Error('unsupported period; use today, yesterday, YYYY, current_year, previous_year, last_N_days/weeks/months/years, or {unit,count}');
|
|
305
|
+
const count = Number(match[1]);
|
|
306
|
+
const unit = match[2].replace(/s$/, '');
|
|
307
|
+
// "含锚点当天"的窗口要少退一步:天按 count-1 天回退,周按 count 周回退(周已在
|
|
308
|
+
// shiftDate 里乘 7,两边各减一次就会得到 last_1_week = 0 天的空窗口——实测 bug)。
|
|
309
|
+
return { start: shiftDate(anchor, unit, unit === 'day' ? -(count - 1) : -count), end: anchor };
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* 单窗口解析(**共享**入口):`resolve_data_time_range`(能力参数那个模式)与
|
|
313
|
+
* `time-axis.ts`(Dataset 时间轴模式)都用它,避免两处对同一个 period 词表给出不同答案。
|
|
314
|
+
*
|
|
315
|
+
* `resolvePeriod` 之外再认三种字面窗口:`all`(全区间)、`ytd`(年初到锚点)、
|
|
316
|
+
* `YYYY-MM`(自然月)。它们不新增相对期语义,只是把"年内/某月"这两个最常用的
|
|
317
|
+
* 分析窗口写成模型本来就会用的词。对象形态 `{unit,count}` 原样交给 `resolvePeriod`。
|
|
318
|
+
*/
|
|
319
|
+
export function resolveZonedPeriod(input, anchor) {
|
|
320
|
+
dateParts(anchor);
|
|
321
|
+
const period = periodInputOf(input);
|
|
322
|
+
if (period === undefined)
|
|
323
|
+
throw new Error('period is required; use today, yesterday, YYYY, YYYY-MM, ytd, current_year, previous_year, all, last_N_days/weeks/months/years, or {unit,count}');
|
|
324
|
+
if (typeof period !== 'string')
|
|
325
|
+
return resolvePeriod(period, anchor);
|
|
326
|
+
const value = period.toLowerCase();
|
|
327
|
+
if (value === 'all')
|
|
328
|
+
return { start: '1990-01-01', end: anchor };
|
|
329
|
+
if (value === 'ytd')
|
|
330
|
+
return { start: `${dateParts(anchor).year}-01-01`, end: anchor };
|
|
331
|
+
const month = /^(\d{4})-(\d{2})$/.exec(value);
|
|
332
|
+
if (month) {
|
|
333
|
+
const year = Number(month[1]);
|
|
334
|
+
const monthIndex = Number(month[2]);
|
|
335
|
+
if (monthIndex < 1 || monthIndex > 12)
|
|
336
|
+
throw new Error(`invalid month in period: ${period}`);
|
|
337
|
+
const lastDay = new Date(Date.UTC(year, monthIndex, 0)).getUTCDate();
|
|
338
|
+
return { start: `${month[1]}-${month[2]}-01`, end: `${month[1]}-${month[2]}-${String(lastDay).padStart(2, '0')}` };
|
|
339
|
+
}
|
|
340
|
+
return resolvePeriod(period, anchor);
|
|
341
|
+
}
|
|
342
|
+
function enumPeriod(contract, period) {
|
|
343
|
+
if (typeof period !== 'string')
|
|
344
|
+
throw new Error('this capability requires a named period such as last_3_months');
|
|
345
|
+
const value = contract.enumMap?.[period.toLowerCase()];
|
|
346
|
+
if (!value || !contract.enumValues?.includes(value))
|
|
347
|
+
throw new Error(`unsupported period for this capability; allowed values: ${Object.keys(contract.enumMap ?? {}).join(', ')}`);
|
|
348
|
+
return value;
|
|
349
|
+
}
|
|
350
|
+
export function resolveDataTimeRange(args, now = Date.now()) {
|
|
351
|
+
const capability = typeof args.capability === 'string' ? args.capability.trim() : '';
|
|
352
|
+
if (!capability)
|
|
353
|
+
throw new Error('capability is required');
|
|
354
|
+
const contract = getDataTimeContract(capability);
|
|
355
|
+
if (!contract)
|
|
356
|
+
throw new Error(`unsupported time capability: ${capability}`);
|
|
357
|
+
const zone = resolveTimeZone(args.timezone) ?? 'Asia/Shanghai';
|
|
358
|
+
if (contract.kind === 'enum_range') {
|
|
359
|
+
return { mode: 'capability', capability, format: 'enum', timezone: zone, params: { [contract.fields[0]]: enumPeriod(contract, args.period) }, warnings: [] };
|
|
360
|
+
}
|
|
361
|
+
const anchor = typeof args.anchor_date === 'string' ? args.anchor_date.trim() : localDateAt(now, zone);
|
|
362
|
+
dateParts(anchor);
|
|
363
|
+
const resolved = resolvePeriod(args.period, anchor);
|
|
364
|
+
const distance = dayDistance(resolved.start, resolved.end);
|
|
365
|
+
if (contract.maxDays !== undefined && distance > contract.maxDays)
|
|
366
|
+
throw new Error(`${capability} period exceeds the ${contract.maxDays} day interface limit`);
|
|
367
|
+
const startEpoch = zonedDateEpoch(resolved.start, zone);
|
|
368
|
+
const endEpoch = zonedDateEpoch(resolved.end, zone, true);
|
|
369
|
+
const params = {};
|
|
370
|
+
if (contract.kind === 'epoch_range') {
|
|
371
|
+
params.start = startEpoch;
|
|
372
|
+
params.end = endEpoch;
|
|
373
|
+
}
|
|
374
|
+
else if (contract.kind === 'nested_epoch_range')
|
|
375
|
+
params.time_range = JSON.stringify({ time_type: 'DAY_1', start: startEpoch, end: endEpoch });
|
|
376
|
+
else if (contract.kind === 'date_range') {
|
|
377
|
+
params[contract.fields[0]] = resolved.start;
|
|
378
|
+
params[contract.fields[1]] = resolved.end;
|
|
379
|
+
}
|
|
380
|
+
else if (contract.kind === 'date_ms')
|
|
381
|
+
params.date_ms = zonedDateEpoch(resolved.end, zone);
|
|
382
|
+
else
|
|
383
|
+
params[contract.fields[0]] = resolved.end;
|
|
384
|
+
// ⛔ 显式集合而不是 `web_` / `wind_` 前缀:改名 `anysearch_search` 曾让前缀判据静默失配,
|
|
385
|
+
// query_hint 无声消失(schema 仍声明该字段,契约测试也抓不到)。新增检索能力必须点名。
|
|
386
|
+
// 前缀判据还有第二种错法:`wind_*` 自 2026-10-06 起也包含**带真实日期参数**的取数能力
|
|
387
|
+
// (wind_edb_query / wind_stock_kline),给它们一句"X 至 Y"等于邀请模型把日期塞进
|
|
388
|
+
// 本来只接受指标描述的 question——那正是上游明确拒绝的写法。
|
|
389
|
+
const queryHint = RETRIEVAL_HINT_CAPABILITIES.has(capability) ? `${resolved.start} 至 ${resolved.end}` : undefined;
|
|
390
|
+
return {
|
|
391
|
+
mode: 'capability',
|
|
392
|
+
capability,
|
|
393
|
+
format: contract.kind === 'epoch_range' ? 'epoch_ms' : contract.kind === 'nested_epoch_range' ? 'epoch_ms_json' : contract.kind === 'date_ms' ? 'date_ms' : 'date_string',
|
|
394
|
+
timezone: zone,
|
|
395
|
+
range: { start_date: resolved.start, end_date: resolved.end, start_epoch_ms: startEpoch, end_epoch_ms: endEpoch },
|
|
396
|
+
params,
|
|
397
|
+
...(queryHint ? { query_hint: queryHint } : {}),
|
|
398
|
+
warnings: contract.warning ? [contract.warning] : [],
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
function dataTimeRangeDefinition(store) {
|
|
402
|
+
return {
|
|
403
|
+
name: 'resolve_data_time_range',
|
|
404
|
+
description: '解析时间窗,**两种形态**,绝不自行换算时间戳。① 取数形态:传 capability + period,输出可直接交给 request_data 的时间参数;② Dataset 分析形态:传 dataset_id(可选 time_column)+ period 或 anchor_date,按**该 Dataset 时间列自身的时区偏移**输出可直接用于 query_dataset filters 的边界(bounds.bound_ge/bound_le)与人类可读日期(range/data)。period 支持 today、yesterday、YYYY、YYYY-MM、ytd、current_year、previous_year、all、last_N_days/weeks/months/years,或 {unit,count}。anchor_date 省略时:取数形态用当前 Asia/Shanghai 日期,Dataset 形态用该 Dataset 的最后一天(相对期因此不会因数据陈旧而落空)。交易日与基金报告期不会自动猜测,结果中的 warnings 必须遵守。',
|
|
405
|
+
parameters: jsonObject({
|
|
406
|
+
capability: { type: 'string', description: '形态①:数据能力名,或 anysearch_search / wind_docs_announcements / wind_docs_news' },
|
|
407
|
+
dataset_id: { type: 'string', description: '形态②:已授权的 dataset_id;给出时按该 Dataset 的时间轴解析(此时忽略 capability)。该形态只对**被委派的子 Agent**(如 data_junior)开放' },
|
|
408
|
+
time_column: { type: 'string', description: '形态②可选:显式指定时间列;省略时宿主按列名与取值形态探测' },
|
|
409
|
+
period: { oneOf: [{ type: 'string' }, { type: 'object', properties: { unit: { type: 'string', enum: ['day', 'week', 'month', 'quarter', 'year'] }, count: { type: 'integer' } }, required: ['unit', 'count'], additionalProperties: false }] },
|
|
410
|
+
anchor_date: { type: 'string', description: '可选锚点日期 YYYY-MM-DD;省略时取数形态用当前 Asia/Shanghai 日期,Dataset 形态用该 Dataset 的最后一天' },
|
|
411
|
+
timezone: { type: 'string', description: '可选 IANA 时区;数据接口默认 Asia/Shanghai' },
|
|
412
|
+
}, ['period']),
|
|
413
|
+
output: {
|
|
414
|
+
schema: jsonObject({
|
|
415
|
+
capability: { type: 'string' },
|
|
416
|
+
mode: { type: 'string' },
|
|
417
|
+
format: { type: 'string' },
|
|
418
|
+
timezone: { type: 'string' },
|
|
419
|
+
time_column: { type: 'string' },
|
|
420
|
+
period: { type: 'string' },
|
|
421
|
+
range: { oneOf: [{ type: 'object', additionalProperties: true }, { type: 'null' }] },
|
|
422
|
+
data: { oneOf: [{ type: 'object', additionalProperties: true }, { type: 'null' }] },
|
|
423
|
+
bounds: { oneOf: [{ type: 'object', additionalProperties: true }, { type: 'null' }] },
|
|
424
|
+
bounds_are_intraday: { type: 'boolean' },
|
|
425
|
+
dataset: { oneOf: [{ type: 'object', additionalProperties: true }, { type: 'null' }] },
|
|
426
|
+
params: { type: 'object', additionalProperties: true },
|
|
427
|
+
query_hint: { type: 'string' },
|
|
428
|
+
warnings: { type: 'array', items: { type: 'string' } },
|
|
429
|
+
}, ['mode', 'format', 'timezone', 'warnings']),
|
|
430
|
+
render,
|
|
431
|
+
},
|
|
432
|
+
async execute(args, exec) {
|
|
433
|
+
const datasetId = typeof args.dataset_id === 'string' ? args.dataset_id.trim() : '';
|
|
434
|
+
if (datasetId.length === 0)
|
|
435
|
+
return resolveDataTimeRange(args);
|
|
436
|
+
if (!store)
|
|
437
|
+
throw new Error('Dataset time-range mode is unavailable: the Dataset store is not mounted in this session');
|
|
438
|
+
// ⛔ 不要写 `exec.session`:框架把它放在 `exec.agent.session`(2026-09-23 事故,
|
|
439
|
+
// 会话 66fa9666 里该形态对每个调用方都报 "requires an authorized session")。
|
|
440
|
+
// Dataset 形态读的是 Dataset 元数据 ⇒ 与 Dataset 系列工具同一道边界:只对**被委派的**
|
|
441
|
+
// 子 Agent 开放(主 Agent 只用取数形态,不直连数据工具)。
|
|
442
|
+
const session = delegatedSession(exec, 'resolve_data_time_range');
|
|
443
|
+
const snapshot = await store.describeTimeAxis({
|
|
444
|
+
session,
|
|
445
|
+
dataset_id: datasetId,
|
|
446
|
+
time_column: typeof args.time_column === 'string' && args.time_column.length > 0 ? args.time_column : undefined,
|
|
447
|
+
signal: exec.signal,
|
|
448
|
+
});
|
|
449
|
+
if (snapshot === undefined) {
|
|
450
|
+
throw new Error(`Dataset ${datasetId} has no detectable time column; pass time_column explicitly or inspect the Dataset first`);
|
|
451
|
+
}
|
|
452
|
+
return resolveDatasetTimeWindow({
|
|
453
|
+
dataset_id: datasetId,
|
|
454
|
+
axis: snapshot.axis,
|
|
455
|
+
dates: snapshot.dates,
|
|
456
|
+
// 对象形态不能在这里被丢掉:`{unit:"year",count:2}`(含它的字符串化写法)都必须走到
|
|
457
|
+
// 解析器,否则会静默退化成 `all`——把"近 2 年"变成"全部历史"。
|
|
458
|
+
period: periodInputOf(args.period),
|
|
459
|
+
anchorDate: typeof args.anchor_date === 'string' ? args.anchor_date.trim() : undefined,
|
|
460
|
+
zone: resolveTimeZone(args.timezone),
|
|
461
|
+
});
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type SessionLike } from './data-collector/store.js';
|
|
2
|
+
/**
|
|
3
|
+
* 「调用方 session 在哪里」的**唯一实现**。
|
|
4
|
+
*
|
|
5
|
+
* ⛔ 2026-09-23 事故(会话 `66fa9666`):`resolve_data_time_range` 的 Dataset 形态自己写成了
|
|
6
|
+
* `exec.session`,而框架一直把它放在 `exec.agent.session`(`dataset-tools.ts` 的 `callerSession`
|
|
7
|
+
* 从一开始就是这么读的)。结果是该形态对**任何**调用方都抛
|
|
8
|
+
* `Dataset time-range mode requires an authorized session`,模型只好绕道日期串。
|
|
9
|
+
* 同一个事实有两份实现,就一定会漂移——所以这里收敛成一份,两边都从这里取。
|
|
10
|
+
*
|
|
11
|
+
* 注意:这里**只认框架形状** `exec.agent.session`。不要加 `exec.session` 之类的兜底——
|
|
12
|
+
* 那会让本地直调测试继续绕过真实形状,正是上次漏掉该 bug 的原因。
|
|
13
|
+
*/
|
|
14
|
+
export interface AgentExecutionLike {
|
|
15
|
+
agent?: {
|
|
16
|
+
session?: {
|
|
17
|
+
id?: string;
|
|
18
|
+
header?: {
|
|
19
|
+
cwd?: string;
|
|
20
|
+
parentSession?: string;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
signal: AbortSignal;
|
|
25
|
+
}
|
|
26
|
+
/** 取调用方 session;缺失即响亮失败(`session_unavailable`)。 */
|
|
27
|
+
export declare function callerSession(exec: AgentExecutionLike): SessionLike;
|
|
28
|
+
/** 取调用方 session,并要求它是**被委派的**子 Agent(有 parentSession)。 */
|
|
29
|
+
export declare function delegatedSession(exec: AgentExecutionLike, toolName: string): SessionLike;
|
package/lib/tool-exec.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { DatasetStoreError } from './data-collector/store.js';
|
|
2
|
+
/** 取调用方 session;缺失即响亮失败(`session_unavailable`)。 */
|
|
3
|
+
export function callerSession(exec) {
|
|
4
|
+
const session = exec.agent?.session;
|
|
5
|
+
if (!session || typeof session.id !== 'string') {
|
|
6
|
+
throw new DatasetStoreError('session_unavailable', 'the calling agent session was not provided');
|
|
7
|
+
}
|
|
8
|
+
return session;
|
|
9
|
+
}
|
|
10
|
+
/** 取调用方 session,并要求它是**被委派的**子 Agent(有 parentSession)。 */
|
|
11
|
+
export function delegatedSession(exec, toolName) {
|
|
12
|
+
const session = callerSession(exec);
|
|
13
|
+
if (typeof session.header?.parentSession !== 'string' || session.header.parentSession.length === 0) {
|
|
14
|
+
throw new DatasetStoreError('dataset_session_mismatch', `${toolName} is restricted to delegated data agents; the main Agent must delegate this request`);
|
|
15
|
+
}
|
|
16
|
+
return session;
|
|
17
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AnySearch 的最小 REST 客户端。
|
|
3
|
+
*
|
|
4
|
+
* web_retriever 目前只支持一个后端,因此不预留多引擎注册表、动态 loader 或
|
|
5
|
+
* provider 目录。未来真正接入第二个后端时,再根据实际差异抽象接口。
|
|
6
|
+
*/
|
|
7
|
+
export interface SearchHit {
|
|
8
|
+
url: string;
|
|
9
|
+
title?: string;
|
|
10
|
+
snippet?: string;
|
|
11
|
+
content?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface SearchResult {
|
|
14
|
+
query: string;
|
|
15
|
+
ok: boolean;
|
|
16
|
+
sources: SearchHit[];
|
|
17
|
+
error?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface FetchResult {
|
|
20
|
+
url: string;
|
|
21
|
+
ok: boolean;
|
|
22
|
+
title?: string;
|
|
23
|
+
content?: string;
|
|
24
|
+
truncated?: boolean;
|
|
25
|
+
error?: string;
|
|
26
|
+
}
|
|
27
|
+
/** AnySearch 失败的结构化错误码。取值与拼写是下游回退判据的契约,勿随意改动。 */
|
|
28
|
+
export type AnySearchErrorCode = 'ABORTED' | 'TIMEOUT' | 'NETWORK' | 'AUTH' | 'RATE_LIMIT' | 'HTTP' | 'INVALID_RESPONSE' | 'INVALID_URL' | 'TARGET_BLOCKED' | 'CONTENT_TOO_LARGE' | 'UNSUPPORTED_CONTENT' | 'UPSTREAM';
|
|
29
|
+
/** 带结构化错误码的 AnySearch 失败;message 沿用既有拼法,保证既有断言仍通过。 */
|
|
30
|
+
export declare class AnySearchError extends Error {
|
|
31
|
+
readonly code: AnySearchErrorCode;
|
|
32
|
+
readonly upstreamCode?: string;
|
|
33
|
+
readonly status?: number;
|
|
34
|
+
constructor(code: AnySearchErrorCode, path: string, detail: string, extra?: {
|
|
35
|
+
upstreamCode?: string;
|
|
36
|
+
status?: number;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* AnySearch 失败后是否值得改走本机直连回退。
|
|
41
|
+
* 只有调用方取消、URL 非法与内容类型不受支持三类不回退;其余都回退。
|
|
42
|
+
*/
|
|
43
|
+
export declare function shouldFallbackToLocalFetch(code: AnySearchErrorCode): boolean;
|
|
44
|
+
export interface AnySearchClient {
|
|
45
|
+
search(request: {
|
|
46
|
+
query: string;
|
|
47
|
+
max_results?: number;
|
|
48
|
+
}, signal?: AbortSignal): Promise<unknown>;
|
|
49
|
+
extract(request: {
|
|
50
|
+
url: string;
|
|
51
|
+
}, signal?: AbortSignal): Promise<unknown>;
|
|
52
|
+
}
|
|
53
|
+
export declare const DEFAULT_BASE_URL = "https://api.anysearch.com";
|
|
54
|
+
export declare const REQUEST_TIMEOUT_MS = 60000;
|
|
55
|
+
export declare const MAX_CONTENT_CHARS = 20000;
|
|
56
|
+
export declare function normalizeSearchHit(raw: unknown): SearchHit | null;
|
|
57
|
+
export declare function normalizeSearchResponse(query: string, raw: unknown): SearchResult;
|
|
58
|
+
export declare function normalizeFetchResponse(url: string, raw: unknown): FetchResult;
|
|
59
|
+
export declare function envKey(name: string): string | undefined;
|
|
60
|
+
/** 创建 AnySearch REST 客户端;API key 每次请求重新解析,不缓存。 */
|
|
61
|
+
export declare function createAnySearchClient(baseURL?: string, resolveApiKey?: () => Promise<string | undefined>): AnySearchClient;
|