@dshtrading/connector-jin10 0.3.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/LICENSE +75 -0
- package/README.md +76 -0
- package/lib/dataplane.d.ts +7 -0
- package/lib/dataplane.js +23 -0
- package/lib/errors.d.ts +18 -0
- package/lib/errors.js +26 -0
- package/lib/flash-service.d.ts +8 -0
- package/lib/flash-service.js +30 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.js +10 -0
- package/lib/market-data.d.ts +27 -0
- package/lib/market-data.js +160 -0
- package/lib/mcp.d.ts +81 -0
- package/lib/mcp.js +222 -0
- package/lib/parse.d.ts +78 -0
- package/lib/parse.js +229 -0
- package/lib/plugin.d.ts +17 -0
- package/lib/plugin.js +39 -0
- package/lib/service.d.ts +53 -0
- package/lib/service.js +117 -0
- package/lib/tools.d.ts +7 -0
- package/lib/tools.js +282 -0
- package/lib/web-flash.d.ts +34 -0
- package/lib/web-flash.js +121 -0
- package/package.json +39 -0
package/lib/service.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Jin10Error } from "./errors.js";
|
|
2
|
+
import { parseArticle, parseCalendar, parseFlashItems, parseFlashPage, parseKlines, parseNewsPage, parseQuote, parseQuoteCodes } from "./parse.js";
|
|
3
|
+
import { fetchJin10HotFlash } from "./web-flash.js";
|
|
4
|
+
import "./mcp.js";
|
|
5
|
+
//#region src/service.ts
|
|
6
|
+
/**
|
|
7
|
+
* 取数层:把 MCP 工具名(list_flash / search_flash / list_news / search_news / get_news /
|
|
8
|
+
* list_calendar / get_quote / get_kline)与 quote://codes 资源封装成本仓语义的读法,
|
|
9
|
+
* 分页统一 `cursor → data.next_cursor / data.has_more`(用户契约 5)。
|
|
10
|
+
*
|
|
11
|
+
* 工具名与参数不对外泄漏到 agent 面:工具面用业务命名(tools.ts),provider 可替换。
|
|
12
|
+
*/
|
|
13
|
+
/** 快讯/资讯单页上限(上游 list_* 每页 20 条;search_flash 一次性最多 150 条且不可翻页)。 */
|
|
14
|
+
const FLASH_PAGE_SIZE = 20;
|
|
15
|
+
const SEARCH_FLASH_MAX = 150;
|
|
16
|
+
/** 品种代码表缓存 TTL(静态名册,按小时级缓存;失败不缓存,下次调用重试)。 */
|
|
17
|
+
const INSTRUMENT_CACHE_TTL_MS = 36e5;
|
|
18
|
+
var Jin10Service = class {
|
|
19
|
+
client;
|
|
20
|
+
webFetch;
|
|
21
|
+
webTimeoutMs;
|
|
22
|
+
instruments;
|
|
23
|
+
constructor(client, web = {}) {
|
|
24
|
+
this.client = client;
|
|
25
|
+
this.webFetch = web.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
26
|
+
this.webTimeoutMs = web.timeoutMs ?? 15e3;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 按热度取快讯(金十网页版服务端过滤,火/热/沸/爆):官方 MCP 的 list_flash
|
|
30
|
+
* 无热度参数,故热度路径走网页版接口(见 web-flash.ts)。cursor = 上一页
|
|
31
|
+
* 最旧一条的 time 原串。
|
|
32
|
+
*/
|
|
33
|
+
async listFlashByHeat(options) {
|
|
34
|
+
return fetchJin10HotFlash(this.webFetch, {
|
|
35
|
+
hot: options.hot,
|
|
36
|
+
cursor: options.cursor
|
|
37
|
+
}, { timeoutMs: this.webTimeoutMs });
|
|
38
|
+
}
|
|
39
|
+
/** 最新快讯流(cursor 翻页;正文不下发,只留标题/时间/链接)。 */
|
|
40
|
+
async listFlash(options = {}) {
|
|
41
|
+
const data = await this.client.callToolData("list_flash", options.cursor === void 0 ? {} : { cursor: options.cursor });
|
|
42
|
+
return parseFlashPage(data, "list_flash", options.limit);
|
|
43
|
+
}
|
|
44
|
+
/** 关键词搜快讯:上游一次性返回(最多 150 条,不支持翻页),故无 cursor。 */
|
|
45
|
+
async searchFlash(keyword, limit) {
|
|
46
|
+
const trimmed = requireKeyword(keyword);
|
|
47
|
+
const cap = Math.min(limit ?? 150, 150);
|
|
48
|
+
const data = await this.client.callToolData("search_flash", { keyword: trimmed });
|
|
49
|
+
return parseFlashItems(data, "search_flash", cap);
|
|
50
|
+
}
|
|
51
|
+
async listNews(options = {}) {
|
|
52
|
+
const data = await this.client.callToolData("list_news", options.cursor === void 0 ? {} : { cursor: options.cursor });
|
|
53
|
+
return parseNewsPage(data, "list_news", options.limit);
|
|
54
|
+
}
|
|
55
|
+
async searchNews(keyword, options = {}) {
|
|
56
|
+
const args = { keyword: requireKeyword(keyword) };
|
|
57
|
+
if (options.cursor !== void 0) args.cursor = options.cursor;
|
|
58
|
+
const data = await this.client.callToolData("search_news", args);
|
|
59
|
+
return parseNewsPage(data, "search_news", options.limit);
|
|
60
|
+
}
|
|
61
|
+
/** 文章详情:标题/导语/时间/链接(正文按铁律 #5 不下发)。 */
|
|
62
|
+
async getArticle(id) {
|
|
63
|
+
const trimmed = (id ?? "").trim();
|
|
64
|
+
if (trimmed.length === 0) throw new Jin10Error("TRADING_UNKNOWN", "jin10 news_get: id is required");
|
|
65
|
+
return parseArticle(await this.client.callToolData("get_news", { id: trimmed }));
|
|
66
|
+
}
|
|
67
|
+
/** 本周财经日历(周一~周日;上游一次返回整周)。 */
|
|
68
|
+
async listCalendar(limit) {
|
|
69
|
+
return parseCalendar(await this.client.callToolData("list_calendar", {}), limit);
|
|
70
|
+
}
|
|
71
|
+
/** 品种名册(quote://codes 资源);query 按代码或中文名过滤。 */
|
|
72
|
+
async listInstruments(query) {
|
|
73
|
+
const items = await this.instrumentCatalog();
|
|
74
|
+
const needle = query?.trim().toLowerCase();
|
|
75
|
+
if (needle === void 0 || needle.length === 0) return items;
|
|
76
|
+
return items.filter((item) => item.code.toLowerCase().includes(needle) || item.name.toLowerCase().includes(needle));
|
|
77
|
+
}
|
|
78
|
+
async getQuote(code) {
|
|
79
|
+
const trimmed = requireCode(code);
|
|
80
|
+
return parseQuote(await this.client.callToolData("get_quote", { code: trimmed }));
|
|
81
|
+
}
|
|
82
|
+
/** 分钟级 K 线:`time` = 起始秒级时间戳(从此往后取,窗口 24h 内),count ≤ 100。 */
|
|
83
|
+
async getKlines(code, options = {}) {
|
|
84
|
+
const args = { code: requireCode(code) };
|
|
85
|
+
if (options.time !== void 0) {
|
|
86
|
+
if (!Number.isInteger(options.time) || options.time <= 0) throw new Jin10Error("TRADING_UNKNOWN", "jin10 global_klines: time must be a positive Unix timestamp in seconds");
|
|
87
|
+
args.time = options.time;
|
|
88
|
+
}
|
|
89
|
+
if (options.count !== void 0) {
|
|
90
|
+
if (!Number.isInteger(options.count) || options.count < 1 || options.count > 100) throw new Jin10Error("TRADING_UNKNOWN", "jin10 global_klines: count must be an integer from 1 to 100");
|
|
91
|
+
args.count = options.count;
|
|
92
|
+
}
|
|
93
|
+
return parseKlines(await this.client.callToolData("get_kline", args), options.count);
|
|
94
|
+
}
|
|
95
|
+
async instrumentCatalog() {
|
|
96
|
+
const cached = this.instruments;
|
|
97
|
+
if (cached !== void 0 && Date.now() - cached.at < 36e5) return cached.items;
|
|
98
|
+
const items = parseQuoteCodes(await this.client.readResource("quote://codes"));
|
|
99
|
+
this.instruments = {
|
|
100
|
+
at: Date.now(),
|
|
101
|
+
items
|
|
102
|
+
};
|
|
103
|
+
return items;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
function requireKeyword(keyword) {
|
|
107
|
+
const trimmed = (keyword ?? "").trim();
|
|
108
|
+
if (trimmed.length === 0) throw new Jin10Error("TRADING_UNKNOWN", "jin10 search: keyword is required");
|
|
109
|
+
return trimmed;
|
|
110
|
+
}
|
|
111
|
+
function requireCode(code) {
|
|
112
|
+
const trimmed = (code ?? "").trim();
|
|
113
|
+
if (trimmed.length === 0) throw new Jin10Error("TRADING_UNSUPPORTED_SYMBOL", "jin10 quote: code is required (see the global_instruments tool for the code space)");
|
|
114
|
+
return trimmed;
|
|
115
|
+
}
|
|
116
|
+
//#endregion
|
|
117
|
+
export { FLASH_PAGE_SIZE, INSTRUMENT_CACHE_TTL_MS, Jin10Service, SEARCH_FLASH_MAX };
|
package/lib/tools.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Jin10Service } from "./service.js";
|
|
2
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
3
|
+
//#region src/tools.d.ts
|
|
4
|
+
/** 工具集合:service 注入即得全部工具(测试用假 service,生产用真 MCP 链路)。 */
|
|
5
|
+
declare function createJin10Tools(service: Jin10Service): Array<ReturnType<typeof defineTool>>;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { createJin10Tools };
|
package/lib/tools.js
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import "./service.js";
|
|
2
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
3
|
+
//#region src/tools.ts
|
|
4
|
+
/**
|
|
5
|
+
* agent 工具面(市场无关命名,provider 可替换 —— 与 `symbol_search`、`knowledge_search`
|
|
6
|
+
* 同族的跨市场命名,不带交易所名):快讯、资讯、财经日历、全球品种报价/K线。
|
|
7
|
+
*
|
|
8
|
+
* 输出纪律:
|
|
9
|
+
* - 列表类工具只回**元数据**(时间/标题/链接/id),正文与摘要正文不下发(数据零再分发);
|
|
10
|
+
* - 时间统一 ISO 8601;标题与链接必须可点回原文;
|
|
11
|
+
* - 分页显式回带 `next_cursor`/`has_more`,让模型能续页而不是猜;
|
|
12
|
+
* - 上游业务错误(isError / status≠200 / 限流)抛错,绝不静默返回空列表。
|
|
13
|
+
*/
|
|
14
|
+
const textOutput = {
|
|
15
|
+
schema: { type: "string" },
|
|
16
|
+
render: (_args, value) => [{
|
|
17
|
+
type: "text",
|
|
18
|
+
text: value
|
|
19
|
+
}]
|
|
20
|
+
};
|
|
21
|
+
const SOURCE = "Jin10 (金十数据) MCP feed";
|
|
22
|
+
function renderNewsLines(items) {
|
|
23
|
+
return items.map((item) => {
|
|
24
|
+
const id = "id" in item && typeof item.id === "string" ? ` | id=${item.id}` : "";
|
|
25
|
+
return `${item.publishedAt} | ${item.title} | ${item.url}${id}`;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
function pageFooter(page, tool) {
|
|
29
|
+
if (page.hasMore && page.nextCursor !== void 0) return `has_more=true — pass cursor="${page.nextCursor}" to ${tool} for the next (older) page.`;
|
|
30
|
+
return "has_more=false — no further pages.";
|
|
31
|
+
}
|
|
32
|
+
function renderCalendarLine(entry) {
|
|
33
|
+
const fields = [
|
|
34
|
+
`前值=${entry.previous ?? "-"}`,
|
|
35
|
+
`预期=${entry.consensus ?? "-"}`,
|
|
36
|
+
`公布=${entry.actual ?? "-"}`
|
|
37
|
+
];
|
|
38
|
+
if (entry.revised !== void 0) fields.push(`修正=${entry.revised}`);
|
|
39
|
+
if (entry.affect !== void 0) fields.push(`影响=${entry.affect}`);
|
|
40
|
+
return `${entry.publishedAt} | ★${entry.star} | ${entry.title} | ${fields.join(" ")}`;
|
|
41
|
+
}
|
|
42
|
+
/** 工具集合:service 注入即得全部工具(测试用假 service,生产用真 MCP 链路)。 */
|
|
43
|
+
function createJin10Tools(service) {
|
|
44
|
+
return [
|
|
45
|
+
defineTool({
|
|
46
|
+
name: "flash_list",
|
|
47
|
+
description: `Latest market flash headlines from ${SOURCE}, newest first. Market-wide wire (macro, commodities, FX, A-shares, geopolitics) — not tied to one instrument. Each item carries publish time, headline and the source link; the full flash text stays on the linked page. Pages return up to 20 items: pass the returned cursor to read older pages. Upstream rate limit: 1500 calls per tool per Beijing calendar day.`,
|
|
48
|
+
parameters: {
|
|
49
|
+
cursor: {
|
|
50
|
+
type: "string",
|
|
51
|
+
description: "Opaque page cursor from a previous flash_list call (omit for the newest page)"
|
|
52
|
+
},
|
|
53
|
+
limit: {
|
|
54
|
+
type: "number",
|
|
55
|
+
description: `Max items to return (integer 1..50; default 20)`
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
output: textOutput,
|
|
59
|
+
async execute(raw) {
|
|
60
|
+
const args = raw ?? {};
|
|
61
|
+
const limit = readLimit(args.limit, 20, 50);
|
|
62
|
+
const cursor = readCursor(args.cursor);
|
|
63
|
+
const page = await service.listFlash({
|
|
64
|
+
limit,
|
|
65
|
+
...cursor !== void 0 ? { cursor } : {}
|
|
66
|
+
});
|
|
67
|
+
if (page.items.length === 0) return "flash_list: no flash items returned by the upstream feed.";
|
|
68
|
+
return [
|
|
69
|
+
`flash_list — ${page.items.length} item(s), newest-first, source: ${SOURCE}:`,
|
|
70
|
+
...renderNewsLines(page.items),
|
|
71
|
+
pageFooter(page, "flash_list")
|
|
72
|
+
].join("\n");
|
|
73
|
+
}
|
|
74
|
+
}),
|
|
75
|
+
defineTool({
|
|
76
|
+
name: "flash_search",
|
|
77
|
+
description: `Keyword search over recent ${SOURCE} flash headlines (e.g. 黄金 / 原油 / 美联储 / 非农 / 日本央行 / 欧佩克). Upstream returns at most 150 matches in one shot and does not paginate — narrow the keyword instead of paging. Only metadata is returned; open the link for the full text. Upstream rate limit: 1500 calls per tool per Beijing calendar day.`,
|
|
78
|
+
parameters: {
|
|
79
|
+
keyword: {
|
|
80
|
+
type: "string",
|
|
81
|
+
required: true,
|
|
82
|
+
description: "Chinese keyword, e.g. 黄金 / 原油 / 美联储"
|
|
83
|
+
},
|
|
84
|
+
limit: {
|
|
85
|
+
type: "number",
|
|
86
|
+
description: `Max matches to return (integer 1..150; default 30)`
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
output: textOutput,
|
|
90
|
+
async execute(raw) {
|
|
91
|
+
const args = raw ?? {};
|
|
92
|
+
const limit = readLimit(args.limit, 30, 150);
|
|
93
|
+
const items = await service.searchFlash(String(args.keyword ?? ""), limit);
|
|
94
|
+
if (items.length === 0) return `flash_search: no flash items matched ${JSON.stringify(String(args.keyword ?? ""))}.`;
|
|
95
|
+
return [`flash_search — ${items.length} match(es) for ${JSON.stringify(String(args.keyword ?? ""))}, newest-first, source: ${SOURCE}:`, ...renderNewsLines(items)].join("\n");
|
|
96
|
+
}
|
|
97
|
+
}),
|
|
98
|
+
defineTool({
|
|
99
|
+
name: "news_list",
|
|
100
|
+
description: `Latest ${SOURCE} editorial articles (longer-form macro/market analysis), newest first. Articles are paged: pass the returned cursor for older pages. Returns metadata plus the article id; call news_get with that id for the publisher abstract. Upstream rate limit: 1500 calls per tool per Beijing calendar day.`,
|
|
101
|
+
parameters: {
|
|
102
|
+
cursor: {
|
|
103
|
+
type: "string",
|
|
104
|
+
description: "Opaque page cursor from a previous news_list call (omit for the newest page)"
|
|
105
|
+
},
|
|
106
|
+
limit: {
|
|
107
|
+
type: "number",
|
|
108
|
+
description: "Max items to return (integer 1..50; default 10)"
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
output: textOutput,
|
|
112
|
+
async execute(raw) {
|
|
113
|
+
const args = raw ?? {};
|
|
114
|
+
const limit = readLimit(args.limit, 10, 50);
|
|
115
|
+
const cursor = readCursor(args.cursor);
|
|
116
|
+
const page = await service.listNews({
|
|
117
|
+
limit,
|
|
118
|
+
...cursor !== void 0 ? { cursor } : {}
|
|
119
|
+
});
|
|
120
|
+
if (page.items.length === 0) return "news_list: no articles returned by the upstream feed.";
|
|
121
|
+
return [
|
|
122
|
+
`news_list — ${page.items.length} article(s), newest-first, source: ${SOURCE}:`,
|
|
123
|
+
...renderNewsLines(page.items),
|
|
124
|
+
pageFooter(page, "news_list")
|
|
125
|
+
].join("\n");
|
|
126
|
+
}
|
|
127
|
+
}),
|
|
128
|
+
defineTool({
|
|
129
|
+
name: "news_search",
|
|
130
|
+
description: `Keyword search over ${SOURCE} editorial articles (e.g. 美联储 / 通胀 / 欧佩克 / 日元). Paged like news_list; each hit carries an id for news_get. Metadata only — the article body stays on the publisher page. Upstream rate limit: 1500 calls per tool per Beijing calendar day.`,
|
|
131
|
+
parameters: {
|
|
132
|
+
keyword: {
|
|
133
|
+
type: "string",
|
|
134
|
+
required: true,
|
|
135
|
+
description: "Chinese keyword, e.g. 美联储 / 通胀 / 日元"
|
|
136
|
+
},
|
|
137
|
+
cursor: {
|
|
138
|
+
type: "string",
|
|
139
|
+
description: "Opaque page cursor from a previous news_search call"
|
|
140
|
+
},
|
|
141
|
+
limit: {
|
|
142
|
+
type: "number",
|
|
143
|
+
description: "Max matches to return (integer 1..50; default 10)"
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
output: textOutput,
|
|
147
|
+
async execute(raw) {
|
|
148
|
+
const args = raw ?? {};
|
|
149
|
+
const limit = readLimit(args.limit, 10, 50);
|
|
150
|
+
const cursor = readCursor(args.cursor);
|
|
151
|
+
const page = await service.searchNews(String(args.keyword ?? ""), {
|
|
152
|
+
limit,
|
|
153
|
+
...cursor !== void 0 ? { cursor } : {}
|
|
154
|
+
});
|
|
155
|
+
if (page.items.length === 0) return `news_search: no articles matched ${JSON.stringify(String(args.keyword ?? ""))}.`;
|
|
156
|
+
return [
|
|
157
|
+
`news_search — ${page.items.length} match(es) for ${JSON.stringify(String(args.keyword ?? ""))}, newest-first, source: ${SOURCE}:`,
|
|
158
|
+
...renderNewsLines(page.items),
|
|
159
|
+
pageFooter(page, "news_search")
|
|
160
|
+
].join("\n");
|
|
161
|
+
}
|
|
162
|
+
}),
|
|
163
|
+
defineTool({
|
|
164
|
+
name: "news_get",
|
|
165
|
+
description: `Publisher metadata for one ${SOURCE} article: title, publish time, link and the publisher's own introduction/abstract, by article id from news_list or news_search. The full article body is not redistributed — open the link to read it. Upstream rate limit: 1500 calls per tool per Beijing calendar day.`,
|
|
166
|
+
parameters: { id: {
|
|
167
|
+
type: "string",
|
|
168
|
+
required: true,
|
|
169
|
+
description: "Article id from news_list / news_search, e.g. 229952"
|
|
170
|
+
} },
|
|
171
|
+
output: textOutput,
|
|
172
|
+
async execute(raw) {
|
|
173
|
+
const args = raw ?? {};
|
|
174
|
+
const article = await service.getArticle(String(args.id ?? ""));
|
|
175
|
+
return [
|
|
176
|
+
`news_get — id=${article.id}, source: ${SOURCE}`,
|
|
177
|
+
`title: ${article.title}`,
|
|
178
|
+
`publishedAt: ${article.publishedAt}`,
|
|
179
|
+
`url: ${article.url}`,
|
|
180
|
+
...article.introduction !== void 0 ? [`introduction: ${article.introduction}`] : []
|
|
181
|
+
].join("\n");
|
|
182
|
+
}
|
|
183
|
+
}),
|
|
184
|
+
defineTool({
|
|
185
|
+
name: "econ_calendar",
|
|
186
|
+
description: `Current-week economic calendar from ${SOURCE} (Monday–Sunday, Beijing time): release time, importance stars, consensus / previous / actual values and the stated market impact. Values are the publisher snapshot as of the call — the same release may be revised; treat actual=null as "not published yet". Upstream rate limit: 1500 calls per tool per Beijing calendar day.`,
|
|
187
|
+
parameters: { limit: {
|
|
188
|
+
type: "number",
|
|
189
|
+
description: "Max entries to return (integer 1..250; default 40, earliest first)"
|
|
190
|
+
} },
|
|
191
|
+
output: textOutput,
|
|
192
|
+
async execute(raw) {
|
|
193
|
+
const limit = readLimit((raw ?? {}).limit, 40, 250);
|
|
194
|
+
const entries = await service.listCalendar(limit);
|
|
195
|
+
if (entries.length === 0) return "econ_calendar: the upstream calendar is empty for the current week.";
|
|
196
|
+
return [`econ_calendar — ${entries.length} entr(ies) for the current week (Beijing time), source: ${SOURCE}:`, ...entries.map(renderCalendarLine)].join("\n");
|
|
197
|
+
}
|
|
198
|
+
}),
|
|
199
|
+
defineTool({
|
|
200
|
+
name: "global_instruments",
|
|
201
|
+
description: `Code catalog for the ${SOURCE} quote space (spot metals, energy, FX, global and A-share indices). Call this before global_quote / global_klines whenever the code is not obvious; codes are the publisher's own (XAUUSD, USOIL, USBND, USDJPY …), NOT the market symbol vocabulary. Upstream rate limit: 1500 calls per tool per Beijing calendar day.`,
|
|
202
|
+
parameters: { query: {
|
|
203
|
+
type: "string",
|
|
204
|
+
description: "Optional filter on code or Chinese name, e.g. 黄金 / 原油 / JPY"
|
|
205
|
+
} },
|
|
206
|
+
output: textOutput,
|
|
207
|
+
async execute(raw) {
|
|
208
|
+
const args = raw ?? {};
|
|
209
|
+
const query = typeof args.query === "string" && args.query.trim().length > 0 ? args.query.trim() : void 0;
|
|
210
|
+
const items = await service.listInstruments(query);
|
|
211
|
+
const label = query === void 0 ? "all instruments" : `matches for ${JSON.stringify(query)}`;
|
|
212
|
+
if (items.length === 0) return `global_instruments: no ${label} in the quote space.`;
|
|
213
|
+
return [`global_instruments — ${items.length} ${label} (source: ${SOURCE}):`, ...items.map((item) => `${item.code} — ${item.name}`)].join("\n");
|
|
214
|
+
}
|
|
215
|
+
}),
|
|
216
|
+
defineTool({
|
|
217
|
+
name: "global_quote",
|
|
218
|
+
description: `Real-time quote for one ${SOURCE} instrument (spot gold/silver/copper, WTI/Brent, FX, indices). Fields: open/close/high/low/volume plus change and change percent; \`time\` is the quote timestamp (Beijing time) — quotes may be delayed or closed-market. Use global_instruments to resolve a code. Upstream rate limit: 1500 calls per tool per Beijing calendar day.`,
|
|
219
|
+
parameters: { code: {
|
|
220
|
+
type: "string",
|
|
221
|
+
required: true,
|
|
222
|
+
description: "Publisher code, e.g. XAUUSD / XAGUSD / USOIL / UKOIL / COPPER / USDJPY / EURUSD / USDCNH"
|
|
223
|
+
} },
|
|
224
|
+
output: textOutput,
|
|
225
|
+
async execute(raw) {
|
|
226
|
+
const args = raw ?? {};
|
|
227
|
+
const quote = await service.getQuote(String(args.code ?? ""));
|
|
228
|
+
return JSON.stringify(quote);
|
|
229
|
+
}
|
|
230
|
+
}),
|
|
231
|
+
defineTool({
|
|
232
|
+
name: "global_klines",
|
|
233
|
+
description: `Minute-level candles for one ${SOURCE} instrument. Upstream semantics: \`time\` is the START Unix timestamp in seconds and candles are returned forward from it, within a 24-hour window (omit it and the window starts at the current minute, which often yields an empty result — pass a start time inside the last 24h for real data). count is 1..100 (default 100). An empty array means no candles in that window (closed market/weekend), not an upstream failure. Upstream rate limit: 1500 calls per tool per Beijing calendar day.`,
|
|
234
|
+
parameters: {
|
|
235
|
+
code: {
|
|
236
|
+
type: "string",
|
|
237
|
+
required: true,
|
|
238
|
+
description: "Publisher code, e.g. XAUUSD / USOIL / USDJPY"
|
|
239
|
+
},
|
|
240
|
+
time: {
|
|
241
|
+
type: "number",
|
|
242
|
+
description: "Start Unix timestamp in seconds (from the last 24 hours)"
|
|
243
|
+
},
|
|
244
|
+
count: {
|
|
245
|
+
type: "number",
|
|
246
|
+
description: "Number of candles from time, integer 1..100 (default 100)"
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
output: textOutput,
|
|
250
|
+
async execute(raw) {
|
|
251
|
+
const args = raw ?? {};
|
|
252
|
+
const code = String(args.code ?? "");
|
|
253
|
+
const options = {};
|
|
254
|
+
if (args.time !== void 0) options.time = Math.floor(Number(args.time));
|
|
255
|
+
if (args.count !== void 0) options.count = Math.floor(Number(args.count));
|
|
256
|
+
const klines = await service.getKlines(code, options);
|
|
257
|
+
if (klines.length === 0) return JSON.stringify({
|
|
258
|
+
code,
|
|
259
|
+
klines: [],
|
|
260
|
+
note: "no candles in the requested window (upstream returns forward from time, 24h window)"
|
|
261
|
+
});
|
|
262
|
+
return JSON.stringify({
|
|
263
|
+
code,
|
|
264
|
+
klines
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
];
|
|
269
|
+
}
|
|
270
|
+
function readLimit(value, fallback, max) {
|
|
271
|
+
if (value === void 0) return fallback;
|
|
272
|
+
const parsed = Math.floor(Number(value));
|
|
273
|
+
if (!Number.isFinite(parsed) || parsed < 1) return fallback;
|
|
274
|
+
return Math.min(parsed, max);
|
|
275
|
+
}
|
|
276
|
+
function readCursor(value) {
|
|
277
|
+
if (typeof value !== "string") return void 0;
|
|
278
|
+
const trimmed = value.trim();
|
|
279
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
280
|
+
}
|
|
281
|
+
//#endregion
|
|
282
|
+
export { createJin10Tools };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Jin10NewsItem, Jin10NewsPage } from "./parse.js";
|
|
2
|
+
//#region src/web-flash.d.ts
|
|
3
|
+
declare const JIN10_WEB_FLASH_ENDPOINT = "https://3318fc142ea545eab931e22a61ec6e5c.z3c.jin10.com/flash";
|
|
4
|
+
/** 网页版 x-app-id / x-version(bundle 内常量,网页版请求原样复刻)。 */
|
|
5
|
+
declare const JIN10_WEB_APP_ID = "bVBF4FyRTn5NJF5n";
|
|
6
|
+
declare const JIN10_WEB_X_VERSION = "1.0";
|
|
7
|
+
/** 网页版快讯频道(bundle d.Xr = [1,5,9])。 */
|
|
8
|
+
declare const JIN10_WEB_FLASH_CHANNELS: readonly [1, 5, 9];
|
|
9
|
+
/** 网页版单页条数(上游固定,无 pageSize 参数)。 */
|
|
10
|
+
declare const JIN10_WEB_PAGE_SIZE = 50;
|
|
11
|
+
/** 四级热度:1=火 2=热 3=沸 4=爆(bundle o)。数组顺序即网页版展示顺序。 */
|
|
12
|
+
declare const JIN10_HEAT_LEVELS: readonly ["火", "热", "沸", "爆"];
|
|
13
|
+
type Jin10HeatLevel = (typeof JIN10_HEAT_LEVELS)[number];
|
|
14
|
+
declare function isJin10HeatLevel(value: unknown): value is Jin10HeatLevel;
|
|
15
|
+
/** 热度快讯条目(比 MCP 快讯多一个 hot 标签)。 */
|
|
16
|
+
interface Jin10FlashItem extends Jin10NewsItem {
|
|
17
|
+
readonly hot?: string;
|
|
18
|
+
}
|
|
19
|
+
interface Jin10HotFlashOptions {
|
|
20
|
+
hot: readonly Jin10HeatLevel[];
|
|
21
|
+
/** 翻页游标 = 上一页最旧一条的网页版 time 原串(上游 max_time 语义)。 */
|
|
22
|
+
cursor?: string | undefined;
|
|
23
|
+
}
|
|
24
|
+
/** 网页版条目 → 本仓 NewsItem 形状(坏形状丢弃,绝不回退「现在」或臆造 url)。 */
|
|
25
|
+
declare function parseWebFlashItems(payload: unknown): Jin10FlashItem[];
|
|
26
|
+
/**
|
|
27
|
+
* 按热度取快讯(服务端过滤)。上游单页 50 条,max_time 翻页;
|
|
28
|
+
* hasMore 以「满页」判断,nextCursor = 本页最旧一条的 time 原串。
|
|
29
|
+
*/
|
|
30
|
+
declare function fetchJin10HotFlash(fetchImpl: typeof globalThis.fetch, options: Jin10HotFlashOptions, config: {
|
|
31
|
+
timeoutMs: number;
|
|
32
|
+
}): Promise<Jin10NewsPage<Jin10FlashItem>>;
|
|
33
|
+
//#endregion
|
|
34
|
+
export { JIN10_HEAT_LEVELS, JIN10_WEB_APP_ID, JIN10_WEB_FLASH_CHANNELS, JIN10_WEB_FLASH_ENDPOINT, JIN10_WEB_PAGE_SIZE, JIN10_WEB_X_VERSION, Jin10FlashItem, Jin10HeatLevel, Jin10HotFlashOptions, fetchJin10HotFlash, isJin10HeatLevel, parseWebFlashItems };
|
package/lib/web-flash.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Jin10Error } from "./errors.js";
|
|
2
|
+
import { flashTitle, parseJin10Time } from "./parse.js";
|
|
3
|
+
//#region src/web-flash.ts
|
|
4
|
+
/**
|
|
5
|
+
* 金十网页版「热度快讯」取数(2026-09-13)。
|
|
6
|
+
*
|
|
7
|
+
* 背景:官方 MCP 的 list_flash 不支持热度(inputSchema 只有 cursor,额外参数被上游
|
|
8
|
+
* 以 unexpected additional properties 拒绝),条目也只有 content/time/url。而金十
|
|
9
|
+
* **网页版**对快讯有 火/热/沸/爆 四级热度筛选,且是**服务端过滤**。本模块复刻网页版
|
|
10
|
+
* 实际请求(从 www.jin10.com 前端 bundle 逆向 + 真实网络复核):
|
|
11
|
+
*
|
|
12
|
+
* GET https://3318fc142ea545eab931e22a61ec6e5c.z3c.jin10.com/flash?params=<JSON>
|
|
13
|
+
* headers: x-app-id: bVBF4FyRTn5NJF5n, x-version: 1.0, handleError: 1
|
|
14
|
+
* params : { channel:[1,5,9], hot:['热','爆'], max_time?, date? }
|
|
15
|
+
* 响应 : { status, message, data:[{ id, time, data:{title,content,source,…}, hot, important, … }] }
|
|
16
|
+
*
|
|
17
|
+
* 与 MCP 是两套入口:本接口属网页版公开接口(非官方 MCP 契约),仅用于热度筛选。
|
|
18
|
+
* 铁律 #5:只下发 title/time/url(+ hot 标签),正文 content 不取不再分发。
|
|
19
|
+
*/
|
|
20
|
+
const JIN10_WEB_FLASH_ENDPOINT = "https://3318fc142ea545eab931e22a61ec6e5c.z3c.jin10.com/flash";
|
|
21
|
+
/** 网页版 x-app-id / x-version(bundle 内常量,网页版请求原样复刻)。 */
|
|
22
|
+
const JIN10_WEB_APP_ID = "bVBF4FyRTn5NJF5n";
|
|
23
|
+
const JIN10_WEB_X_VERSION = "1.0";
|
|
24
|
+
/** 网页版快讯频道(bundle d.Xr = [1,5,9])。 */
|
|
25
|
+
const JIN10_WEB_FLASH_CHANNELS = [
|
|
26
|
+
1,
|
|
27
|
+
5,
|
|
28
|
+
9
|
|
29
|
+
];
|
|
30
|
+
/** 网页版单页条数(上游固定,无 pageSize 参数)。 */
|
|
31
|
+
const JIN10_WEB_PAGE_SIZE = 50;
|
|
32
|
+
/** 四级热度:1=火 2=热 3=沸 4=爆(bundle o)。数组顺序即网页版展示顺序。 */
|
|
33
|
+
const JIN10_HEAT_LEVELS = [
|
|
34
|
+
"火",
|
|
35
|
+
"热",
|
|
36
|
+
"沸",
|
|
37
|
+
"爆"
|
|
38
|
+
];
|
|
39
|
+
function isJin10HeatLevel(value) {
|
|
40
|
+
return typeof value === "string" && JIN10_HEAT_LEVELS.includes(value);
|
|
41
|
+
}
|
|
42
|
+
function isRecord(value) {
|
|
43
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
44
|
+
}
|
|
45
|
+
/** 网页版条目 → 本仓 NewsItem 形状(坏形状丢弃,绝不回退「现在」或臆造 url)。 */
|
|
46
|
+
function parseWebFlashItems(payload) {
|
|
47
|
+
if (!isRecord(payload)) throw new Jin10Error("TRADING_EXCHANGE_ERROR", "jin10 hot-flash: unexpected payload (expected an object)");
|
|
48
|
+
if (payload.status !== 200) {
|
|
49
|
+
const message = typeof payload.message === "string" && payload.message.length > 0 ? payload.message : "unknown upstream error";
|
|
50
|
+
throw new Jin10Error("TRADING_EXCHANGE_ERROR", `jin10 hot-flash: upstream status ${String(payload.status)} — ${message}`);
|
|
51
|
+
}
|
|
52
|
+
if (!Array.isArray(payload.data)) throw new Jin10Error("TRADING_EXCHANGE_ERROR", "jin10 hot-flash: unexpected payload (expected data[])");
|
|
53
|
+
const items = [];
|
|
54
|
+
for (const raw of payload.data) {
|
|
55
|
+
if (!isRecord(raw)) continue;
|
|
56
|
+
const id = typeof raw.id === "string" ? raw.id.trim() : "";
|
|
57
|
+
const publishedAt = parseJin10Time(raw.time);
|
|
58
|
+
if (id.length === 0 || publishedAt === void 0) continue;
|
|
59
|
+
const data = isRecord(raw.data) ? raw.data : void 0;
|
|
60
|
+
const content = typeof data?.content === "string" ? data.content : "";
|
|
61
|
+
const explicit = typeof data?.title === "string" ? data.title.trim() : "";
|
|
62
|
+
const title = explicit.length > 0 ? explicit : flashTitle(content);
|
|
63
|
+
if (title === void 0) continue;
|
|
64
|
+
const hot = isJin10HeatLevel(raw.hot) ? raw.hot : void 0;
|
|
65
|
+
items.push({
|
|
66
|
+
source: "jin10",
|
|
67
|
+
title,
|
|
68
|
+
url: `https://flash.jin10.com/detail/${id}`,
|
|
69
|
+
publishedAt,
|
|
70
|
+
...hot !== void 0 ? { hot } : {}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return items;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 按热度取快讯(服务端过滤)。上游单页 50 条,max_time 翻页;
|
|
77
|
+
* hasMore 以「满页」判断,nextCursor = 本页最旧一条的 time 原串。
|
|
78
|
+
*/
|
|
79
|
+
async function fetchJin10HotFlash(fetchImpl, options, config) {
|
|
80
|
+
const hot = options.hot.filter(isJin10HeatLevel);
|
|
81
|
+
if (hot.length === 0) throw new Jin10Error("TRADING_UNKNOWN", "jin10 hot-flash: hot must contain at least one of 火/热/沸/爆");
|
|
82
|
+
const params = {
|
|
83
|
+
channel: [...JIN10_WEB_FLASH_CHANNELS],
|
|
84
|
+
hot
|
|
85
|
+
};
|
|
86
|
+
if (options.cursor !== void 0 && options.cursor.length > 0) params.max_time = options.cursor;
|
|
87
|
+
const url = new URL(JIN10_WEB_FLASH_ENDPOINT);
|
|
88
|
+
url.searchParams.set("params", JSON.stringify(params));
|
|
89
|
+
const response = await fetchImpl(url, {
|
|
90
|
+
headers: {
|
|
91
|
+
accept: "application/json",
|
|
92
|
+
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124 Safari/537.36",
|
|
93
|
+
"x-app-id": JIN10_WEB_APP_ID,
|
|
94
|
+
"x-version": "1.0",
|
|
95
|
+
handleError: "1",
|
|
96
|
+
referer: "https://www.jin10.com/"
|
|
97
|
+
},
|
|
98
|
+
signal: AbortSignal.timeout(config.timeoutMs)
|
|
99
|
+
});
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
const body = await response.text().catch(() => "");
|
|
102
|
+
throw new Jin10Error("TRADING_EXCHANGE_ERROR", `jin10 hot-flash: HTTP ${response.status}${body ? ` — ${body.slice(0, 160)}` : ""}`);
|
|
103
|
+
}
|
|
104
|
+
let parsed;
|
|
105
|
+
try {
|
|
106
|
+
parsed = JSON.parse(await response.text());
|
|
107
|
+
} catch {
|
|
108
|
+
throw new Jin10Error("TRADING_EXCHANGE_ERROR", "jin10 hot-flash: upstream returned non-JSON body");
|
|
109
|
+
}
|
|
110
|
+
const items = parseWebFlashItems(parsed);
|
|
111
|
+
const rawData = parsed.data;
|
|
112
|
+
const oldest = rawData[rawData.length - 1];
|
|
113
|
+
const cursor = typeof oldest?.time === "string" ? oldest.time : void 0;
|
|
114
|
+
return {
|
|
115
|
+
items,
|
|
116
|
+
hasMore: items.length >= 50,
|
|
117
|
+
...cursor !== void 0 ? { nextCursor: cursor } : {}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
//#endregion
|
|
121
|
+
export { JIN10_HEAT_LEVELS, JIN10_WEB_APP_ID, JIN10_WEB_FLASH_CHANNELS, JIN10_WEB_FLASH_ENDPOINT, JIN10_WEB_PAGE_SIZE, JIN10_WEB_X_VERSION, fetchJin10HotFlash, isJin10HeatLevel, parseWebFlashItems };
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dshtrading/connector-jin10",
|
|
3
|
+
"description": "金十数据 MCP 连接器:把 mcp.jin10.com 的行情/快讯/资讯/财经日历封装成本仓的连接器(市场无关,host 面工具行)",
|
|
4
|
+
"version": "0.3.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "./lib/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./dataplane": {
|
|
14
|
+
"types": "./lib/dataplane.d.ts",
|
|
15
|
+
"default": "./lib/dataplane.js"
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"lib"
|
|
21
|
+
],
|
|
22
|
+
"license": "PolyForm-Noncommercial-1.0.0",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@dshtrading/api": "^0.3.0"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@deepseek-ai/cordis": ">=4.0.0",
|
|
28
|
+
"@deepseek-ai/dsh-tools": ">=0.1.5-rc.1",
|
|
29
|
+
"@deepseek-ai/schemastery": ">=3.18.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"tsdown": "^0.22.0",
|
|
33
|
+
"vitest": "^3.0.0"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsdown",
|
|
37
|
+
"test": "vitest run"
|
|
38
|
+
}
|
|
39
|
+
}
|