@v587d/capital-generation 2.15.1 → 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 +62 -0
- package/README.md +7 -7
- package/automation/client.js +47 -17
- package/automation/client.src.cjs +294 -83
- package/automation/contract.js +4 -0
- package/automation/defaults.js +2 -0
- package/automation/index.js +11 -7
- package/automation/package.json +2 -1
- package/automation/route.js +8 -1
- package/automation/runner.js +28 -6
- package/automation/service.js +89 -0
- package/automation/settings.js +6 -0
- package/automation/workspaces.js +113 -13
- package/chart-ui/package.json +1 -1
- package/config/client.js +12 -5
- package/config/client.src.cjs +223 -0
- package/config/index.js +2 -0
- 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/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 +2 -0
- 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-data-protocol/SKILL.md +9 -2
- package/preset/capital-generation/skills/capital-orchestration/SKILL.md +2 -1
- 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
|
@@ -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">;
|
package/lib/index.js
CHANGED
|
@@ -55,6 +55,7 @@ const AutomationSchema = z.object({
|
|
|
55
55
|
maxSteps: z.number().default(300).description('单次运行最大步骤数(100–2000)'),
|
|
56
56
|
maxToolCalls: z.number().default(1_200).description('单次运行最大工具调用数(100–5000)'),
|
|
57
57
|
inAppNotifications: z.boolean().default(true).description('是否写入应用内通知'),
|
|
58
|
+
defaultWorkspaceId: z.string().default('').description('统一默认工作区 id;空 = 专属默认工作区'),
|
|
58
59
|
});
|
|
59
60
|
// Derive toggles from the catalog: a hand-copied list silently drops defaults at the entry→plugin boundary.
|
|
60
61
|
const SelectedSkillsSchema = z.object(Object.fromEntries(SELECTED_SKILL_CATALOG.map(({ key, name }) => [
|
|
@@ -82,6 +83,7 @@ export const Config = z.object({
|
|
|
82
83
|
maxSteps: 300,
|
|
83
84
|
maxToolCalls: 1_200,
|
|
84
85
|
inAppNotifications: true,
|
|
86
|
+
defaultWorkspaceId: '',
|
|
85
87
|
}).description('投研自动化默认值;任务级覆盖优先'),
|
|
86
88
|
retriever: RetrieverSchema.default({ baseURL: '', credentialRef: 'ANYSEARCH_API_KEY', windDocs: { endpoint: '', credentialRef: 'WIND_API_KEY', timeoutMs: 60000 }, localFetch: { ...LOCAL_FETCH_DEFAULTS }, paddleOcr: { ...PADDLE_OCR_DEFAULTS } })
|
|
87
89
|
.description('web_retriever 配置(可选;缺省使用 AnySearch 默认地址与凭据名)'),
|
|
@@ -3,7 +3,7 @@ import { getDataTimeContract } from '../time/tools.js';
|
|
|
3
3
|
import { isLikelyIndex, normalizeSecurityCodes, parseSecurityCode, requireTencentSecurity } from './security-code.js';
|
|
4
4
|
// Shared core holds HK/US snapshots, smartbox and the one throttle chain; this file only defines
|
|
5
5
|
// capabilities and K-line/tick orchestration. Two throttles = no throttle.
|
|
6
|
-
import { QUOTE_URL, fetchOffshoreIndexQuotes, fetchOffshoreQuotes, isRecord, normalizeHkCode, normalizeOffshoreIndexCode, normalizeUsCode, numberOrNull, requiredNumber, resolveUsKlineSymbol, sourceError, tencentGetText as getText, } from './tencent-public-core.js';
|
|
6
|
+
import { QUOTE_URL, aShareQuoteTime, fetchOffshoreIndexQuotes, fetchOffshoreQuotes, isRecord, normalizeHkCode, normalizeOffshoreIndexCode, normalizeUsCode, numberOrNull, requiredNumber, resolveUsKlineSymbol, sourceError, tencentGetText as getText, } from './tencent-public-core.js';
|
|
7
7
|
const TICK_URL = 'https://stock.gtimg.cn/data/index.php';
|
|
8
8
|
const KLINE_HOSTS = [
|
|
9
9
|
'https://web.ifzq.gtimg.cn',
|
|
@@ -43,6 +43,7 @@ const quoteOutput = {
|
|
|
43
43
|
limit_down: { oneOf: [{ type: 'number' }, { type: 'null' }] },
|
|
44
44
|
vol_ratio: { oneOf: [{ type: 'number' }, { type: 'null' }] },
|
|
45
45
|
pe_static: { oneOf: [{ type: 'number' }, { type: 'null' }] },
|
|
46
|
+
quote_time: { type: 'string', description: '交易所当地时间(北京时间),不是抓取时刻' },
|
|
46
47
|
is_stale: { type: 'boolean' },
|
|
47
48
|
stale_reason: { oneOf: [{ type: 'string' }, { type: 'null' }] },
|
|
48
49
|
},
|
|
@@ -196,6 +197,9 @@ function parseQuoteRows(text, requested) {
|
|
|
196
197
|
const values = match[3].split('~');
|
|
197
198
|
if (values.length < 53)
|
|
198
199
|
throw sourceError(`Tencent quote ${symbol} returned ${values.length} fields, expected at least 53`, 'tencent_invalid_response');
|
|
200
|
+
const quoteTime = aShareQuoteTime(values[30]);
|
|
201
|
+
if (quoteTime === null)
|
|
202
|
+
throw sourceError(`Tencent quote ${symbol} has an unreadable timestamp ${JSON.stringify(values[30] ?? '')}`, 'tencent_invalid_response');
|
|
199
203
|
const value = (index) => numberOrNull(values[index]);
|
|
200
204
|
const row = {
|
|
201
205
|
code: code.canonical,
|
|
@@ -219,6 +223,7 @@ function parseQuoteRows(text, requested) {
|
|
|
219
223
|
limit_down: value(48),
|
|
220
224
|
vol_ratio: value(49),
|
|
221
225
|
pe_static: value(52),
|
|
226
|
+
quote_time: quoteTime,
|
|
222
227
|
};
|
|
223
228
|
const stale = row.amount_wan === 0 && row.price !== null && row.price === row.last_close;
|
|
224
229
|
row.is_stale = stale;
|
|
@@ -733,7 +738,7 @@ export function createTencentSources() {
|
|
|
733
738
|
cacheMaxAgeMs: 60_000,
|
|
734
739
|
name: 'get_tencent_quote',
|
|
735
740
|
summary: '腾讯实时行情、估值、涨跌停和 ETF 快照',
|
|
736
|
-
description: '批量获取 SH/SZ 股票、指数或 ETF 的腾讯实时快照。输入 codes 为完整证券代码数组(如 600519.SH、000001.SZ、000001.SH、510300.SH),返回保留腾讯字段语义的行数组:价格、涨跌、成交额(万元)、换手率、PE/PB、市值和涨跌停价。44 为流通市值、45 为总市值;43 为振幅,不是 PB。腾讯返回成交额为 0 且最新价等于昨收时标记 is_stale=true,不把它当作当日有效成交。BJ 代码当前明确拒绝。',
|
|
741
|
+
description: '批量获取 SH/SZ 股票、指数或 ETF 的腾讯实时快照。输入 codes 为完整证券代码数组(如 600519.SH、000001.SZ、000001.SH、510300.SH),返回保留腾讯字段语义的行数组:价格、涨跌、成交额(万元)、换手率、PE/PB、市值和涨跌停价。quote_time 是交易所当地时间(北京时间)的最后成交时刻,不是抓取时刻;判断“数据是不是今天”要用它,不要用 captured_at 代替。44 为流通市值、45 为总市值;43 为振幅,不是 PB。腾讯返回成交额为 0 且最新价等于昨收时标记 is_stale=true,不把它当作当日有效成交。BJ 代码当前明确拒绝。',
|
|
737
742
|
inputSchema: { type: 'object', properties: { codes: { type: 'array', minItems: 1, maxItems: 50, items: { type: 'string', description: '完整证券代码,如 600519.SH' } } }, required: ['codes'], additionalProperties: false },
|
|
738
743
|
outputSchema: quoteOutput,
|
|
739
744
|
paginated: false,
|
|
@@ -14,6 +14,8 @@ export declare const HK_QUOTE_FIELDS = 78;
|
|
|
14
14
|
export declare const US_QUOTE_FIELDS = 73;
|
|
15
15
|
/** Exchange-local time; separator drifts per row (`/` vs `-`), normalize to hyphen. */
|
|
16
16
|
export declare function offshoreQuoteTime(value: unknown): string | null;
|
|
17
|
+
/** A-share snapshots give a compact `YYYYMMDDHHMMSS` (Beijing time), not the HK/US spaced form. */
|
|
18
|
+
export declare function aShareQuoteTime(value: unknown): string | null;
|
|
17
19
|
export declare function snapshotRows(text: string, requested: Set<string>): Array<{
|
|
18
20
|
symbol: string;
|
|
19
21
|
fields: string[];
|
|
@@ -56,6 +56,11 @@ export function offshoreQuoteTime(value) {
|
|
|
56
56
|
const match = /^(\d{4})[-/](\d{2})[-/](\d{2}) (\d{2}:\d{2}:\d{2})$/.exec(String(value ?? '').trim());
|
|
57
57
|
return match ? `${match[1]}-${match[2]}-${match[3]} ${match[4]}` : null;
|
|
58
58
|
}
|
|
59
|
+
/** A-share snapshots give a compact `YYYYMMDDHHMMSS` (Beijing time), not the HK/US spaced form. */
|
|
60
|
+
export function aShareQuoteTime(value) {
|
|
61
|
+
const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/.exec(String(value ?? '').trim());
|
|
62
|
+
return match ? `${match[1]}-${match[2]}-${match[3]} ${match[4]}:${match[5]}:${match[6]}` : null;
|
|
63
|
+
}
|
|
59
64
|
export function snapshotRows(text, requested) {
|
|
60
65
|
const rows = [];
|
|
61
66
|
for (const line of text.split(';')) {
|
package/lib/sources/wind-mcp.js
CHANGED
|
@@ -472,7 +472,7 @@ export function createWindSources(resolveApiKey) {
|
|
|
472
472
|
tool: 'search_economic_indicator',
|
|
473
473
|
server: 'economic_data',
|
|
474
474
|
cacheMaxAgeMs: 6 * 3_600_000,
|
|
475
|
-
summary: 'Wind EDB 宏观/行业/汇率指标检索(只找码不取数)',
|
|
475
|
+
summary: '[计费] Wind EDB 宏观/行业/汇率指标检索(只找码不取数)',
|
|
476
476
|
description: EDB_SEARCH_DESCRIPTION,
|
|
477
477
|
inputSchema: {
|
|
478
478
|
type: 'object',
|
|
@@ -491,7 +491,7 @@ export function createWindSources(resolveApiKey) {
|
|
|
491
491
|
tool: 'query_economic_indicator_data',
|
|
492
492
|
server: 'economic_data',
|
|
493
493
|
cacheMaxAgeMs: 6 * 3_600_000,
|
|
494
|
-
summary: 'Wind EDB 指标时间序列(宏观/行业/汇率,按日期区间)',
|
|
494
|
+
summary: '[计费] Wind EDB 指标时间序列(宏观/行业/汇率,按日期区间)',
|
|
495
495
|
description: EDB_QUERY_DESCRIPTION,
|
|
496
496
|
inputSchema: {
|
|
497
497
|
type: 'object',
|
|
@@ -514,7 +514,7 @@ export function createWindSources(resolveApiKey) {
|
|
|
514
514
|
tool: 'get_stock_kline',
|
|
515
515
|
server: 'stock_data',
|
|
516
516
|
cacheMaxAgeMs: 60_000,
|
|
517
|
-
summary: 'Wind 日周月与 1–30 分钟 K 线(按日期区间,A股+港股+美股)',
|
|
517
|
+
summary: '[计费] Wind 日周月与 1–30 分钟 K 线(按日期区间,A股+港股+美股)',
|
|
518
518
|
description: KLINE_DESCRIPTION,
|
|
519
519
|
inputSchema: {
|
|
520
520
|
type: 'object',
|
package/lib/sources-entry.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `tools/retrieve-sources` row: the
|
|
2
|
+
* `tools/retrieve-sources` row: the eleven named-source tools, gated by
|
|
3
3
|
* `localFetch.enabled`. When disabled they stay registered and fail loudly per call;
|
|
4
4
|
* unregistering at boot would freeze one config read for the process lifetime.
|
|
5
5
|
*/
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Known limit: DNS resolves per request but the IP is not pinned to the connection,
|
|
4
4
|
* so a rebinding window remains (same trade-off as dsh-search-first).
|
|
5
5
|
*/
|
|
6
|
-
export declare const LOCAL_FETCH_CLIENT_VERSION = "2.
|
|
6
|
+
export declare const LOCAL_FETCH_CLIENT_VERSION = "2.16.0";
|
|
7
7
|
export declare const LOCAL_FETCH_MAX_URL_LENGTH = 2048;
|
|
8
8
|
export type LocalFetchErrorCode = 'ABORTED' | 'TIMEOUT' | 'INVALID_URL' | 'BLOCKED_URL' | 'DNS' | 'REDIRECT' | 'HTTP' | 'TOO_LARGE' | 'UNSUPPORTED_CONTENT_TYPE';
|
|
9
9
|
export declare class LocalFetchError extends Error {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* so a rebinding window remains (same trade-off as dsh-search-first).
|
|
5
5
|
*/
|
|
6
6
|
import { htmlToMarkdown, stripInvisibleText } from './html-markdown.js';
|
|
7
|
-
export const LOCAL_FETCH_CLIENT_VERSION = '2.
|
|
7
|
+
export const LOCAL_FETCH_CLIENT_VERSION = '2.16.0';
|
|
8
8
|
export const LOCAL_FETCH_MAX_URL_LENGTH = 2048;
|
|
9
9
|
export class LocalFetchError extends Error {
|
|
10
10
|
code;
|
|
@@ -128,6 +128,64 @@ export declare function eastmoneyReports(client: EastmoneyClient, input: {
|
|
|
128
128
|
pageNo: number;
|
|
129
129
|
signal?: AbortSignal;
|
|
130
130
|
}): Promise<SourceOutcome>;
|
|
131
|
+
export type AnnouncementMarket = 'A' | 'H' | 'U';
|
|
132
|
+
export interface NormalizedAnnouncementCode {
|
|
133
|
+
code: string;
|
|
134
|
+
market: AnnouncementMarket;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Accept the forms users actually paste (`600519.SH` / `SH600519` / `0700.HK` / `AAPL.O`).
|
|
138
|
+
* The market is inferred only from an unambiguous shape; a mixed list is rejected rather
|
|
139
|
+
* than silently sent to one `ann_type`.
|
|
140
|
+
*/
|
|
141
|
+
export declare function normalizeAnnouncementCode(value: unknown, market?: AnnouncementMarket | 'auto'): NormalizedAnnouncementCode;
|
|
142
|
+
export declare function normalizeAnnouncementCodes(values: unknown, market?: AnnouncementMarket | 'auto'): {
|
|
143
|
+
codes: string[];
|
|
144
|
+
market: AnnouncementMarket;
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* Eastmoney republishes issuer announcements; it is a complete issuer-scoped list by day,
|
|
148
|
+
* whereas Wind's NL RAG is semantic and can miss the newest item. The list is the primary
|
|
149
|
+
* path for "what was disclosed"; the authoritative PDF remains cninfo / the issuer.
|
|
150
|
+
*/
|
|
151
|
+
export declare function eastmoneyAnnouncements(client: EastmoneyClient, input: {
|
|
152
|
+
codes: string[];
|
|
153
|
+
market: AnnouncementMarket;
|
|
154
|
+
startDate: string;
|
|
155
|
+
endDate: string;
|
|
156
|
+
cursor: number;
|
|
157
|
+
pageSize: number;
|
|
158
|
+
signal?: AbortSignal;
|
|
159
|
+
}): Promise<SourceOutcome>;
|
|
160
|
+
export interface AnnouncementBody {
|
|
161
|
+
source: 'eastmoney';
|
|
162
|
+
operation: 'announcement_body';
|
|
163
|
+
art_code: string;
|
|
164
|
+
page: number;
|
|
165
|
+
offset: number;
|
|
166
|
+
page_chars: number;
|
|
167
|
+
total_pages: number;
|
|
168
|
+
content: string;
|
|
169
|
+
next_offset?: number;
|
|
170
|
+
next_page?: number;
|
|
171
|
+
done: boolean;
|
|
172
|
+
pdf_url?: string;
|
|
173
|
+
title?: string;
|
|
174
|
+
notice_date?: string;
|
|
175
|
+
note?: string;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* The detail API returns at most 5,000 characters per `page_index`; `page_size` is the
|
|
179
|
+
* total page count, so `done` is knowable without fetching a sentinel page. US filings
|
|
180
|
+
* often return an empty body while still carrying a PDF — that is a fact, not an error.
|
|
181
|
+
*/
|
|
182
|
+
export declare function eastmoneyAnnouncementBody(client: EastmoneyClient, input: {
|
|
183
|
+
artCode: string;
|
|
184
|
+
page: number;
|
|
185
|
+
offset: number;
|
|
186
|
+
limit: number;
|
|
187
|
+
signal?: AbortSignal;
|
|
188
|
+
}): Promise<AnnouncementBody>;
|
|
131
189
|
/**
|
|
132
190
|
* Always add the exchange prefix: bare codes get a fake empty page for STAR/ChiNext.
|
|
133
191
|
* That page is HTTP 200 and looks like "no reports", so also verify the numbered row count.
|
|
@@ -780,6 +780,296 @@ export async function eastmoneyReports(client, input) {
|
|
|
780
780
|
note: `东财研报库共 ${hits} 篇;列表不含摘要,正文在各条的 pdf_url(研报 PDF)里,交 ocr 解析(整篇一次算完)`,
|
|
781
781
|
};
|
|
782
782
|
}
|
|
783
|
+
const EM_ANNOUNCEMENT_LIST_URL = 'https://np-anotice-stock.eastmoney.com/api/security/ann';
|
|
784
|
+
const EM_ANNOUNCEMENT_BODY_URL = 'https://np-cnotice-stock.eastmoney.com/api/content/ann';
|
|
785
|
+
const EM_ANNOUNCEMENT_REFERER = 'https://data.eastmoney.com/';
|
|
786
|
+
const EM_ANNOUNCEMENT_PAGE_SIZE = 100;
|
|
787
|
+
/** Scan cap: a 7-day watchlist window rarely needs more; the note must say when it is hit. */
|
|
788
|
+
const EM_ANNOUNCEMENT_MAX_SCAN_PAGES = 5;
|
|
789
|
+
/**
|
|
790
|
+
* Accept the forms users actually paste (`600519.SH` / `SH600519` / `0700.HK` / `AAPL.O`).
|
|
791
|
+
* The market is inferred only from an unambiguous shape; a mixed list is rejected rather
|
|
792
|
+
* than silently sent to one `ann_type`.
|
|
793
|
+
*/
|
|
794
|
+
export function normalizeAnnouncementCode(value, market = 'auto') {
|
|
795
|
+
const raw = typeof value === 'string' ? value.trim().toUpperCase() : '';
|
|
796
|
+
if (raw.length === 0)
|
|
797
|
+
throw new Error('announcement code is required');
|
|
798
|
+
if (raw.length > 32)
|
|
799
|
+
throw new Error(`announcement code is too long: ${raw.slice(0, 32)}…`);
|
|
800
|
+
const suffix = /\.(SH|SZ|BJ|HK|O|N|US)$/u.exec(raw)?.[1];
|
|
801
|
+
let body = suffix === undefined ? raw : raw.slice(0, -(suffix.length + 1));
|
|
802
|
+
const prefix = /^(SH|SZ|BJ|HK)(?=\d)/u.exec(body)?.[1];
|
|
803
|
+
if (prefix !== undefined)
|
|
804
|
+
body = body.slice(prefix.length);
|
|
805
|
+
if (body.length === 0)
|
|
806
|
+
throw new Error(`announcement code is empty after removing prefix/suffix: ${raw}`);
|
|
807
|
+
const suffixMarket = suffix === undefined && prefix === undefined
|
|
808
|
+
? undefined
|
|
809
|
+
: suffix === 'HK' || prefix === 'HK' ? 'H'
|
|
810
|
+
: suffix === 'O' || suffix === 'N' || suffix === 'US' ? 'U'
|
|
811
|
+
: 'A';
|
|
812
|
+
const shaped = /^\d{6}$/u.test(body)
|
|
813
|
+
? 'A'
|
|
814
|
+
: /^\d{1,5}$/u.test(body)
|
|
815
|
+
? 'H'
|
|
816
|
+
: /^[A-Z][A-Z0-9.-]{0,9}$/u.test(body) && !body.includes('..')
|
|
817
|
+
? 'U'
|
|
818
|
+
: undefined;
|
|
819
|
+
if (shaped === undefined)
|
|
820
|
+
throw new Error(`announcement code shape not recognized: ${raw}`);
|
|
821
|
+
if (suffixMarket !== undefined && suffixMarket !== shaped && !(suffixMarket === 'A' && shaped === 'H')) {
|
|
822
|
+
throw new Error(`announcement code ${raw} has a suffix for ${suffixMarket} but a ${shaped} code; pass the right market or fix the code`);
|
|
823
|
+
}
|
|
824
|
+
const target = market === 'auto' ? (suffixMarket ?? shaped) : market;
|
|
825
|
+
if (target === 'A' && !/^\d{6}$/u.test(body))
|
|
826
|
+
throw new Error(`A-share announcement code must be six digits: ${raw}`);
|
|
827
|
+
if (target === 'H') {
|
|
828
|
+
if (!/^\d{1,5}$/u.test(body))
|
|
829
|
+
throw new Error(`HK announcement code must be 1–5 digits: ${raw}`);
|
|
830
|
+
body = body.padStart(5, '0');
|
|
831
|
+
}
|
|
832
|
+
if (target === 'U' && !(/^[A-Z][A-Z0-9.-]{0,9}$/u.test(body) && !body.includes('..'))) {
|
|
833
|
+
throw new Error(`US announcement ticker is invalid: ${raw}`);
|
|
834
|
+
}
|
|
835
|
+
return { code: body, market: target };
|
|
836
|
+
}
|
|
837
|
+
export function normalizeAnnouncementCodes(values, market = 'auto') {
|
|
838
|
+
if (!Array.isArray(values) || values.length === 0)
|
|
839
|
+
throw new Error('codes must be a non-empty array');
|
|
840
|
+
if (values.length > 50)
|
|
841
|
+
throw new Error('codes accepts at most 50 codes per call');
|
|
842
|
+
const normalized = values.map((value) => normalizeAnnouncementCode(value, market));
|
|
843
|
+
const markets = new Set(normalized.map((item) => item.market));
|
|
844
|
+
if (markets.size !== 1) {
|
|
845
|
+
throw new Error('codes must be from one market per call (A / H / U); split mixed lists and call once per market');
|
|
846
|
+
}
|
|
847
|
+
return { codes: [...new Set(normalized.map((item) => item.code))], market: normalized[0].market };
|
|
848
|
+
}
|
|
849
|
+
function announcementDate(value, field) {
|
|
850
|
+
const text = value.trim().slice(0, 10);
|
|
851
|
+
if (!/^\d{4}-\d{2}-\d{2}$/u.test(text))
|
|
852
|
+
throw new Error(`parameter ${field} must be YYYY-MM-DD`);
|
|
853
|
+
const check = new Date(`${text}T00:00:00Z`);
|
|
854
|
+
if (Number.isNaN(check.getTime()) || check.toISOString().slice(0, 10) !== text) {
|
|
855
|
+
throw new Error(`parameter ${field} must be a valid YYYY-MM-DD date`);
|
|
856
|
+
}
|
|
857
|
+
return text;
|
|
858
|
+
}
|
|
859
|
+
function announcementRowDate(row) {
|
|
860
|
+
const display = typeof row.display_time === 'string' ? row.display_time.trim() : '';
|
|
861
|
+
const notice = typeof row.notice_date === 'string' ? row.notice_date.trim() : '';
|
|
862
|
+
const raw = display || notice;
|
|
863
|
+
if (raw.length < 10)
|
|
864
|
+
throw new SourceError('eastmoney', 'INVALID_RESPONSE', '东财公告条目缺少 display_time / notice_date');
|
|
865
|
+
const date = raw.slice(0, 10);
|
|
866
|
+
const check = new Date(`${date}T00:00:00Z`);
|
|
867
|
+
if (!/^\d{4}-\d{2}-\d{2}$/u.test(date) || Number.isNaN(check.getTime()) || check.toISOString().slice(0, 10) !== date) {
|
|
868
|
+
throw new SourceError('eastmoney', 'INVALID_RESPONSE', `东财公告条目的时间不可解析:${JSON.stringify(raw.slice(0, 40))}`);
|
|
869
|
+
}
|
|
870
|
+
return { date, publishedAt: display || notice };
|
|
871
|
+
}
|
|
872
|
+
function announcementOwner(row, codes) {
|
|
873
|
+
const list = Array.isArray(row.codes) ? row.codes : [];
|
|
874
|
+
for (const raw of list) {
|
|
875
|
+
const item = asRecord(raw);
|
|
876
|
+
const code = typeof item?.stock_code === 'string' ? item.stock_code.trim() : '';
|
|
877
|
+
if (codes.includes(code)) {
|
|
878
|
+
return { code, name: typeof item?.short_name === 'string' ? item.short_name : '' };
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
const first = asRecord(list[0]);
|
|
882
|
+
return {
|
|
883
|
+
code: typeof first?.stock_code === 'string' ? first.stock_code : codes[0],
|
|
884
|
+
name: typeof first?.short_name === 'string' ? first.short_name : '',
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Eastmoney republishes issuer announcements; it is a complete issuer-scoped list by day,
|
|
889
|
+
* whereas Wind's NL RAG is semantic and can miss the newest item. The list is the primary
|
|
890
|
+
* path for "what was disclosed"; the authoritative PDF remains cninfo / the issuer.
|
|
891
|
+
*/
|
|
892
|
+
export async function eastmoneyAnnouncements(client, input) {
|
|
893
|
+
const start = announcementDate(input.startDate, 'start_date');
|
|
894
|
+
const end = announcementDate(input.endDate, 'end_date');
|
|
895
|
+
if (start > end)
|
|
896
|
+
throw new Error('parameter start_date must not be later than end_date');
|
|
897
|
+
const byArtCode = new Map();
|
|
898
|
+
let totalHits = 0;
|
|
899
|
+
let scannedPages = 0;
|
|
900
|
+
let reachedOlder = false;
|
|
901
|
+
let oldestScanned;
|
|
902
|
+
for (let page = 1; page <= EM_ANNOUNCEMENT_MAX_SCAN_PAGES; page += 1) {
|
|
903
|
+
const params = new URLSearchParams({
|
|
904
|
+
sr: '-1',
|
|
905
|
+
page_size: String(EM_ANNOUNCEMENT_PAGE_SIZE),
|
|
906
|
+
page_index: String(page),
|
|
907
|
+
ann_type: input.market,
|
|
908
|
+
client_source: 'web',
|
|
909
|
+
stock_list: input.codes.join(','),
|
|
910
|
+
});
|
|
911
|
+
const outcome = await client.fetchText(`${EM_ANNOUNCEMENT_LIST_URL}?${params.toString()}`, {
|
|
912
|
+
headers: { referer: EM_ANNOUNCEMENT_REFERER },
|
|
913
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
914
|
+
});
|
|
915
|
+
const body = asRecord(parseJson(outcome.text, 'eastmoney', '东财公告列表响应'));
|
|
916
|
+
const data = asRecord(body?.data);
|
|
917
|
+
if (!data)
|
|
918
|
+
throw new SourceError('eastmoney', 'INVALID_RESPONSE', '东财公告列表缺少 data(上游结构可能已变)');
|
|
919
|
+
if (data.list !== null && data.list !== undefined && !Array.isArray(data.list)) {
|
|
920
|
+
throw new SourceError('eastmoney', 'INVALID_RESPONSE', '东财公告列表 data.list 不是数组(上游结构可能已变)');
|
|
921
|
+
}
|
|
922
|
+
const rows = Array.isArray(data.list) ? data.list : [];
|
|
923
|
+
totalHits = typeof data.total_hits === 'number' ? data.total_hits : rows.length;
|
|
924
|
+
scannedPages = page;
|
|
925
|
+
for (const raw of rows) {
|
|
926
|
+
const row = asRecord(raw);
|
|
927
|
+
if (!row)
|
|
928
|
+
continue;
|
|
929
|
+
const { date, publishedAt } = announcementRowDate(row);
|
|
930
|
+
if (oldestScanned === undefined || date < oldestScanned)
|
|
931
|
+
oldestScanned = date;
|
|
932
|
+
if (date < start) {
|
|
933
|
+
reachedOlder = true;
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
if (date > end)
|
|
937
|
+
continue;
|
|
938
|
+
const artCode = typeof row.art_code === 'string' ? row.art_code.trim() : '';
|
|
939
|
+
if (artCode.length === 0)
|
|
940
|
+
throw new SourceError('eastmoney', 'INVALID_RESPONSE', '东财公告条目缺少 art_code');
|
|
941
|
+
if (byArtCode.has(artCode))
|
|
942
|
+
continue;
|
|
943
|
+
const owner = announcementOwner(row, input.codes);
|
|
944
|
+
const category = Array.isArray(row.columns)
|
|
945
|
+
? row.columns.map((column) => (asRecord(column)?.column_name)).filter((name) => typeof name === 'string').join('/')
|
|
946
|
+
: '';
|
|
947
|
+
const item = {
|
|
948
|
+
code: owner.code,
|
|
949
|
+
name: clip(owner.name),
|
|
950
|
+
market: input.market,
|
|
951
|
+
date,
|
|
952
|
+
published_at: publishedAt,
|
|
953
|
+
title: clip(stripHtml(typeof row.title === 'string' ? row.title : '')),
|
|
954
|
+
category: clip(category),
|
|
955
|
+
art_code: clip(artCode),
|
|
956
|
+
url: itemUrl(`https://data.eastmoney.com/notices/detail/${owner.code}/${artCode}.html`),
|
|
957
|
+
};
|
|
958
|
+
byArtCode.set(artCode, item);
|
|
959
|
+
}
|
|
960
|
+
if (reachedOlder || rows.length < EM_ANNOUNCEMENT_PAGE_SIZE || page * EM_ANNOUNCEMENT_PAGE_SIZE >= totalHits)
|
|
961
|
+
break;
|
|
962
|
+
}
|
|
963
|
+
const sorted = [...byArtCode.values()].sort((left, right) => {
|
|
964
|
+
const byTime = String(right.published_at ?? '').localeCompare(String(left.published_at ?? ''));
|
|
965
|
+
return byTime !== 0 ? byTime : String(right.date ?? '').localeCompare(String(left.date ?? ''));
|
|
966
|
+
});
|
|
967
|
+
const offset = Number.isSafeInteger(input.cursor) && input.cursor >= 0 ? input.cursor : 0;
|
|
968
|
+
const items = sorted.slice(offset, offset + input.pageSize);
|
|
969
|
+
const windowOutOfRange = !reachedOlder && oldestScanned !== undefined && oldestScanned > end;
|
|
970
|
+
const notes = ['东财公告是官方披露的转载;权威原文用巨潮 PDF / 发行人页面;正文用 eastmoney_announcement_body;窗口按 display_time(北京时间)过滤'];
|
|
971
|
+
if (sorted.length === 0 && !windowOutOfRange)
|
|
972
|
+
notes.push('窗口内没有公告(合法空结果;东财列表只覆盖最近若干页)');
|
|
973
|
+
if (windowOutOfRange) {
|
|
974
|
+
notes.push(`窗口未被覆盖:只扫描到最近 ${scannedPages * EM_ANNOUNCEMENT_PAGE_SIZE} 条(最早 ${oldestScanned}),所请求窗口早于此;请缩小日期/代码范围,或改用巨潮 PDF`);
|
|
975
|
+
}
|
|
976
|
+
if (offset + items.length < sorted.length) {
|
|
977
|
+
notes.push(`窗口内共 ${sorted.length} 条,本次返回第 ${offset + 1}–${offset + items.length} 条;继续翻页传 cursor=${offset + items.length}`);
|
|
978
|
+
}
|
|
979
|
+
if (!reachedOlder && !windowOutOfRange && totalHits > scannedPages * EM_ANNOUNCEMENT_PAGE_SIZE) {
|
|
980
|
+
notes.push(`本次只扫描前 ${scannedPages * EM_ANNOUNCEMENT_PAGE_SIZE} 条,窗口内可能仍有更早公告;请缩小日期窗或代码数`);
|
|
981
|
+
}
|
|
982
|
+
return {
|
|
983
|
+
source: 'eastmoney',
|
|
984
|
+
operation: 'announcements',
|
|
985
|
+
items: clipItems(items),
|
|
986
|
+
...(offset + items.length < sorted.length ? { next_cursor: String(offset + items.length) } : {}),
|
|
987
|
+
note: notes.join(';'),
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* The detail API returns at most 5,000 characters per `page_index`; `page_size` is the
|
|
992
|
+
* total page count, so `done` is knowable without fetching a sentinel page. US filings
|
|
993
|
+
* often return an empty body while still carrying a PDF — that is a fact, not an error.
|
|
994
|
+
*/
|
|
995
|
+
export async function eastmoneyAnnouncementBody(client, input) {
|
|
996
|
+
const artCode = input.artCode.trim().toUpperCase();
|
|
997
|
+
if (!/^AN\d{6,}$/u.test(artCode))
|
|
998
|
+
throw new Error('art_code must look like AN202611021831320924');
|
|
999
|
+
if (!Number.isSafeInteger(input.page) || input.page < 1)
|
|
1000
|
+
throw new Error('page must be a positive integer');
|
|
1001
|
+
if (!Number.isSafeInteger(input.offset) || input.offset < 0)
|
|
1002
|
+
throw new Error('offset must be a non-negative integer');
|
|
1003
|
+
if (!Number.isSafeInteger(input.limit) || input.limit < 1)
|
|
1004
|
+
throw new Error('limit must be a positive integer');
|
|
1005
|
+
const page = input.page;
|
|
1006
|
+
const params = new URLSearchParams({ art_code: artCode, client_source: 'web', page_index: String(page) });
|
|
1007
|
+
const outcome = await client.fetchText(`${EM_ANNOUNCEMENT_BODY_URL}?${params.toString()}`, {
|
|
1008
|
+
headers: { referer: EM_ANNOUNCEMENT_REFERER },
|
|
1009
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
1010
|
+
});
|
|
1011
|
+
const body = asRecord(parseJson(outcome.text, 'eastmoney', '东财公告正文响应'));
|
|
1012
|
+
const data = asRecord(body?.data);
|
|
1013
|
+
if (!data)
|
|
1014
|
+
throw new SourceError('eastmoney', 'NOT_FOUND', `东财公告正文没有 art_code=${artCode} 的数据(公告不存在或上游结构已变)`);
|
|
1015
|
+
const content = stripHtml(typeof data.notice_content === 'string' ? data.notice_content : '');
|
|
1016
|
+
const pageCount = Number(data.page_size);
|
|
1017
|
+
const totalPages = Number.isSafeInteger(pageCount) && pageCount > 0 ? pageCount : 1;
|
|
1018
|
+
const pdfUrl = itemUrl(data.attach_url_web ?? data.attach_url);
|
|
1019
|
+
const title = clip(data.notice_title);
|
|
1020
|
+
const noticeDate = clip(data.notice_date);
|
|
1021
|
+
const base = {
|
|
1022
|
+
source: 'eastmoney',
|
|
1023
|
+
operation: 'announcement_body',
|
|
1024
|
+
art_code: artCode,
|
|
1025
|
+
page,
|
|
1026
|
+
offset: input.offset,
|
|
1027
|
+
page_chars: content.length,
|
|
1028
|
+
total_pages: totalPages,
|
|
1029
|
+
...(pdfUrl.length > 0 ? { pdf_url: pdfUrl } : {}),
|
|
1030
|
+
...(title.length > 0 ? { title } : {}),
|
|
1031
|
+
...(noticeDate.length > 0 ? { notice_date: noticeDate } : {}),
|
|
1032
|
+
};
|
|
1033
|
+
if (content.length === 0) {
|
|
1034
|
+
return {
|
|
1035
|
+
...base,
|
|
1036
|
+
content: '',
|
|
1037
|
+
done: true,
|
|
1038
|
+
note: '该公告正文接口没有返回文本(美股公告常见;若为 A/H 公告,可能是上游结构变更);正文在 PDF/附件,用 ocr 解析,别把标题当正文',
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
if (page > totalPages) {
|
|
1042
|
+
return { ...base, content: '', done: true, note: `page 超过总页数 ${totalPages}(该公告只有 ${totalPages} 页)` };
|
|
1043
|
+
}
|
|
1044
|
+
if (input.offset >= content.length) {
|
|
1045
|
+
const nextPage = page < totalPages ? page + 1 : undefined;
|
|
1046
|
+
return {
|
|
1047
|
+
...base,
|
|
1048
|
+
content: '',
|
|
1049
|
+
...(nextPage === undefined ? {} : { next_page: nextPage }),
|
|
1050
|
+
done: nextPage === undefined,
|
|
1051
|
+
note: nextPage === undefined ? '已到文末' : `本页已读完;下一页传 page=${nextPage}、offset=0`,
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
const limit = Math.min(input.limit, 4_000);
|
|
1055
|
+
const slice = content.slice(input.offset, input.offset + limit);
|
|
1056
|
+
const nextOffset = input.offset + slice.length < content.length ? input.offset + slice.length : undefined;
|
|
1057
|
+
const nextPage = nextOffset === undefined && page < totalPages ? page + 1 : undefined;
|
|
1058
|
+
const done = nextOffset === undefined && nextPage === undefined;
|
|
1059
|
+
const notes = [];
|
|
1060
|
+
if (totalPages > 1)
|
|
1061
|
+
notes.push(`全文共 ${totalPages} 页(每页约 5,000 字符);用 next_offset / next_page 继续读`);
|
|
1062
|
+
if (pdfUrl.length > 0)
|
|
1063
|
+
notes.push('PDF 直链在 pdf_url;权威引用请用巨潮/发行人原文');
|
|
1064
|
+
return {
|
|
1065
|
+
...base,
|
|
1066
|
+
content: slice,
|
|
1067
|
+
...(nextOffset === undefined ? {} : { next_offset: nextOffset }),
|
|
1068
|
+
...(nextPage === undefined ? {} : { next_page: nextPage }),
|
|
1069
|
+
done,
|
|
1070
|
+
...(notes.length === 0 ? {} : { note: notes.join(';') }),
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
783
1073
|
const SINA_REPORT_URL = 'https://vip.stock.finance.sina.com.cn/q/go.php/vReport_List/kind';
|
|
784
1074
|
const SINA_REFERER = 'https://finance.sina.com.cn/';
|
|
785
1075
|
/**
|