@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/lib/mcp.js ADDED
@@ -0,0 +1,222 @@
1
+ import { Jin10Error, TOKEN_HINT, codeForUpstreamMessage } from "./errors.js";
2
+ //#region src/mcp.ts
3
+ /**
4
+ * MCP 传输层(Streamable HTTP / JSON-RPC 2.0)。
5
+ *
6
+ * 标准流程(用户 2026-09-13 给定契约,本出口实测复核):
7
+ * initialize → notifications/initialized → tools/list / resources/list / resources/read → tools/call
8
+ * 响应形态两种都要吃:`application/json` 直出 JSON,或 `text/event-stream` 里
9
+ * `event: message` + 单行 `data: {…}`(金十本出口实测为后者)。若上游回
10
+ * `mcp-session-id` 头则后续请求原样回带(本出口当前无会话头,属可选能力)。
11
+ *
12
+ * 结果读取:`result.structuredContent` 优先,`result.content[].text` 仅在
13
+ * structuredContent 缺席时作可读文本兜底(用户契约 3/4)。
14
+ */
15
+ const DEFAULT_ENDPOINT = "https://mcp.jin10.com/mcp";
16
+ const DEFAULT_PROTOCOL_VERSION = "2025-11-25";
17
+ const DEFAULT_TIMEOUT_MS = 15e3;
18
+ /** 把响应体解析成 JSON-RPC 消息(直出 JSON 或 SSE 单行 data 均可)。 */
19
+ function parseMcpBody(text) {
20
+ const trimmed = text.trim();
21
+ if (trimmed.length === 0) return void 0;
22
+ if (trimmed.startsWith("{")) {
23
+ const parsed = JSON.parse(trimmed);
24
+ return isRecord(parsed) ? parsed : void 0;
25
+ }
26
+ const candidates = [];
27
+ const pending = [];
28
+ const flush = () => {
29
+ if (pending.length === 0) return;
30
+ const payload = pending.join("\n").trim();
31
+ pending.length = 0;
32
+ if (payload.length === 0) return;
33
+ try {
34
+ const parsed = JSON.parse(payload);
35
+ if (isRecord(parsed)) candidates.push(parsed);
36
+ } catch {}
37
+ };
38
+ for (const line of trimmed.split(/\r?\n/)) if (line.startsWith("data:")) pending.push(line.slice(5).replace(/^ /, ""));
39
+ else if (line.trim().length === 0) flush();
40
+ flush();
41
+ const message = candidates[candidates.length - 1];
42
+ if (message === void 0) throw new Jin10Error("TRADING_EXCHANGE_ERROR", `jin10 mcp: response is neither JSON nor a JSON-RPC SSE message — ${trimmed.slice(0, 200)}`);
43
+ return message;
44
+ }
45
+ function isRecord(value) {
46
+ return typeof value === "object" && value !== null && !Array.isArray(value);
47
+ }
48
+ function networkError(method, cause) {
49
+ const reason = cause instanceof Error ? cause.message : String(cause);
50
+ const timedOut = cause instanceof Error && (cause.name === "TimeoutError" || cause.name === "AbortError");
51
+ return new Jin10Error("TRADING_NETWORK", `jin10 mcp ${method}: ${timedOut ? "request timed out" : "network failure"} — ${reason}`, cause);
52
+ }
53
+ var Jin10McpClient = class {
54
+ endpoint;
55
+ token;
56
+ timeoutMs;
57
+ fetchImpl;
58
+ protocolVersion;
59
+ clientName;
60
+ clientVersion;
61
+ nextId = 1;
62
+ sessionId;
63
+ handshake;
64
+ constructor(options) {
65
+ this.endpoint = options.endpoint ?? "https://mcp.jin10.com/mcp";
66
+ this.token = options.token;
67
+ this.timeoutMs = options.timeoutMs ?? 15e3;
68
+ this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
69
+ this.protocolVersion = options.protocolVersion ?? "2025-11-25";
70
+ this.clientName = options.clientName ?? "dsh-trading-connector-jin10";
71
+ this.clientVersion = options.clientVersion ?? "0.2.1";
72
+ }
73
+ async listTools() {
74
+ const result = await this.rpc("tools/list", {});
75
+ const tools = isRecord(result) ? result.tools : void 0;
76
+ if (!Array.isArray(tools)) throw new Jin10Error("TRADING_EXCHANGE_ERROR", "jin10 mcp tools/list: unexpected payload (expected result.tools[])");
77
+ return tools.filter(isRecord).map((tool) => ({
78
+ name: String(tool.name ?? ""),
79
+ ...typeof tool.description === "string" ? { description: tool.description } : {},
80
+ ...tool.inputSchema !== void 0 ? { inputSchema: tool.inputSchema } : {},
81
+ ...tool.outputSchema !== void 0 ? { outputSchema: tool.outputSchema } : {}
82
+ }));
83
+ }
84
+ async listResources() {
85
+ const result = await this.rpc("resources/list", {});
86
+ const resources = isRecord(result) ? result.resources : void 0;
87
+ if (!Array.isArray(resources)) throw new Jin10Error("TRADING_EXCHANGE_ERROR", "jin10 mcp resources/list: unexpected payload (expected result.resources[])");
88
+ return resources.filter(isRecord).map((resource) => ({
89
+ uri: String(resource.uri ?? ""),
90
+ ...typeof resource.name === "string" ? { name: resource.name } : {},
91
+ ...typeof resource.mimeType === "string" ? { mimeType: resource.mimeType } : {},
92
+ ...typeof resource.description === "string" ? { description: resource.description } : {}
93
+ }));
94
+ }
95
+ /** 读资源正文(quote://codes 等;返回全部 text 段拼接)。 */
96
+ async readResource(uri) {
97
+ const result = await this.rpc("resources/read", { uri });
98
+ const contents = isRecord(result) ? result.contents : void 0;
99
+ if (!Array.isArray(contents)) throw new Jin10Error("TRADING_EXCHANGE_ERROR", `jin10 mcp resources/read ${uri}: unexpected payload (expected result.contents[])`);
100
+ return contents.filter(isRecord).map((entry) => typeof entry.text === "string" ? entry.text : "").join("");
101
+ }
102
+ /** 原始 tools/call(不做业务解包;协议/HTTP 错误在此抛出)。 */
103
+ async callTool(tool, args = {}) {
104
+ const result = await this.rpc("tools/call", {
105
+ name: tool,
106
+ arguments: args
107
+ });
108
+ if (!isRecord(result)) throw new Jin10Error("TRADING_EXCHANGE_ERROR", `jin10 mcp tools/call ${tool}: unexpected payload (expected result object)`);
109
+ return {
110
+ ...result.structuredContent !== void 0 ? { structuredContent: result.structuredContent } : {},
111
+ ...Array.isArray(result.content) ? { content: result.content.filter(isRecord) } : {},
112
+ ...typeof result.isError === "boolean" ? { isError: result.isError } : {}
113
+ };
114
+ }
115
+ /**
116
+ * 业务取数:structuredContent 优先,text 兜底;`isError`/`status !== 200`/`data === null`
117
+ * 都按上游业务错误抛出(含限流文案识别)。返回上游 `data` 字段。
118
+ */
119
+ async callToolData(tool, args = {}) {
120
+ const result = await this.callTool(tool, args);
121
+ const envelope = result.structuredContent ?? textEnvelope(result);
122
+ if (envelope === void 0) throw new Jin10Error("TRADING_EXCHANGE_ERROR", `jin10 mcp tools/call ${tool}: no structuredContent and no machine-readable text payload`);
123
+ if (!isRecord(envelope)) throw new Jin10Error("TRADING_EXCHANGE_ERROR", `jin10 mcp tools/call ${tool}: unexpected payload (expected an object envelope)`);
124
+ const message = typeof envelope.message === "string" ? envelope.message : "";
125
+ const status = typeof envelope.status === "number" ? envelope.status : void 0;
126
+ if (result.isError === true) throw new Jin10Error(codeForUpstreamMessage(message), `jin10 ${tool}: ${message || "upstream reported isError=true"}${status !== void 0 ? ` (status ${status})` : ""}`);
127
+ if (status !== void 0 && status !== 200) throw new Jin10Error(codeForUpstreamMessage(message), `jin10 ${tool}: upstream status ${status}${message ? ` — ${message}` : ""}`);
128
+ if (envelope.data === null || envelope.data === void 0) throw new Jin10Error(codeForUpstreamMessage(message), `jin10 ${tool}: upstream returned no data${message ? ` — ${message}` : ""}`);
129
+ return envelope.data;
130
+ }
131
+ async ensureHandshake() {
132
+ if (this.handshake === void 0) this.handshake = this.performHandshake().catch((error) => {
133
+ this.handshake = void 0;
134
+ throw error;
135
+ });
136
+ return this.handshake;
137
+ }
138
+ async performHandshake() {
139
+ await this.rpc("initialize", {
140
+ protocolVersion: this.protocolVersion,
141
+ capabilities: {},
142
+ clientInfo: {
143
+ name: this.clientName,
144
+ version: this.clientVersion
145
+ }
146
+ }, { skipHandshake: true });
147
+ await this.rpc("notifications/initialized", void 0, {
148
+ notification: true,
149
+ skipHandshake: true
150
+ });
151
+ }
152
+ async rpc(method, params, options = {}) {
153
+ if (options.skipHandshake !== true) await this.ensureHandshake();
154
+ const token = this.token();
155
+ if (token === void 0 || token.trim().length === 0) throw new Jin10Error("TRADING_CREDENTIALS_MISSING", `jin10 mcp ${method}: missing MCP token. ${TOKEN_HINT}`);
156
+ const payload = {
157
+ jsonrpc: "2.0",
158
+ method
159
+ };
160
+ if (options.notification !== true) payload.id = this.nextId++;
161
+ if (params !== void 0) payload.params = params;
162
+ let response;
163
+ try {
164
+ response = await this.fetchImpl(this.endpoint, {
165
+ method: "POST",
166
+ headers: {
167
+ "content-type": "application/json",
168
+ accept: "application/json, text/event-stream",
169
+ authorization: `Bearer ${token.trim()}`,
170
+ ...this.sessionId !== void 0 ? { "mcp-session-id": this.sessionId } : {}
171
+ },
172
+ body: JSON.stringify(payload),
173
+ signal: AbortSignal.timeout(this.timeoutMs)
174
+ });
175
+ } catch (error) {
176
+ throw networkError(method, error);
177
+ }
178
+ const sessionId = response.headers.get("mcp-session-id");
179
+ if (sessionId !== null && sessionId.length > 0) this.sessionId = sessionId;
180
+ if (response.status === 401 || response.status === 403) {
181
+ const body = await response.text().catch(() => "");
182
+ throw new Jin10Error("TRADING_AUTH_FAILED", `jin10 mcp ${method}: HTTP ${response.status} — invalid or expired MCP token. ${TOKEN_HINT}${body ? ` (${body.slice(0, 160)})` : ""}`);
183
+ }
184
+ if (response.status === 429) {
185
+ const body = await response.text().catch(() => "");
186
+ throw new Jin10Error("TRADING_RATE_LIMITED", `jin10 mcp ${method}: HTTP 429 — rate limited${body ? ` — ${body.slice(0, 160)}` : ""}`);
187
+ }
188
+ if (!response.ok) {
189
+ const body = await response.text().catch(() => "");
190
+ throw new Jin10Error("TRADING_EXCHANGE_ERROR", `jin10 mcp ${method}: HTTP ${response.status}${body ? ` — ${body.slice(0, 200)}` : ""}`);
191
+ }
192
+ const message = parseMcpBody(await response.text().catch((error) => {
193
+ throw networkError(method, error);
194
+ }));
195
+ if (message === void 0) return void 0;
196
+ if (isRecord(message.error)) {
197
+ const code = codeOf(message.error.code);
198
+ const detail = typeof message.error.message === "string" ? message.error.message : "JSON-RPC error";
199
+ throw new Jin10Error("TRADING_EXCHANGE_ERROR", `jin10 mcp ${method}: JSON-RPC error ${code ?? ""} — ${detail}`.replace(" ", " "), message.error);
200
+ }
201
+ return message.result;
202
+ }
203
+ };
204
+ function codeOf(value) {
205
+ return typeof value === "number" || typeof value === "string" ? value : void 0;
206
+ }
207
+ /** structuredContent 缺席时的可读文本兜底:content[].text 若能解析成 JSON 信封则采用。 */
208
+ function textEnvelope(result) {
209
+ for (const part of result.content ?? []) {
210
+ if (typeof part.text !== "string") continue;
211
+ const text = part.text.trim();
212
+ if (!text.startsWith("{")) continue;
213
+ try {
214
+ const parsed = JSON.parse(text);
215
+ if (isRecord(parsed) && ("data" in parsed || "status" in parsed || "message" in parsed)) return parsed;
216
+ } catch {
217
+ continue;
218
+ }
219
+ }
220
+ }
221
+ //#endregion
222
+ export { DEFAULT_ENDPOINT, DEFAULT_PROTOCOL_VERSION, DEFAULT_TIMEOUT_MS, Jin10McpClient, parseMcpBody };
package/lib/parse.d.ts ADDED
@@ -0,0 +1,78 @@
1
+ import { NewsItem } from "@dshtrading/api";
2
+ //#region src/parse.d.ts
3
+ /** 快讯/资讯条目(NewsItem 契约 + 金十来源标识)。 */
4
+ interface Jin10NewsItem extends NewsItem {
5
+ readonly source: 'jin10';
6
+ }
7
+ /** 资讯文章摘要条目(带 id,供 news_get 下钻)。 */
8
+ interface Jin10ArticleSummary extends Jin10NewsItem {
9
+ readonly id: string;
10
+ }
11
+ interface Jin10NewsPage<T> {
12
+ items: T[];
13
+ nextCursor?: string;
14
+ hasMore: boolean;
15
+ }
16
+ interface Jin10ArticleDetail {
17
+ id: string;
18
+ title: string;
19
+ url: string;
20
+ publishedAt: string;
21
+ introduction?: string;
22
+ }
23
+ interface Jin10CalendarEntry {
24
+ publishedAt: string;
25
+ star: number;
26
+ title: string;
27
+ previous?: string;
28
+ consensus?: string;
29
+ actual?: string;
30
+ revised?: string;
31
+ affect?: string;
32
+ }
33
+ interface Jin10Instrument {
34
+ code: string;
35
+ name: string;
36
+ }
37
+ interface Jin10Quote {
38
+ code: string;
39
+ name?: string;
40
+ time?: string;
41
+ open?: number;
42
+ close?: number;
43
+ high?: number;
44
+ low?: number;
45
+ volume?: number;
46
+ change?: number;
47
+ changePercent?: number;
48
+ }
49
+ interface Jin10Kline {
50
+ time: number;
51
+ open?: number;
52
+ high?: number;
53
+ low?: number;
54
+ close?: number;
55
+ volume?: number;
56
+ }
57
+ /** 快讯标题:【标题】正文 → 取括号内;无括号 → 首段文本截断(正文绝不下发)。 */
58
+ declare function flashTitle(content: string, maxLength?: number): string | undefined;
59
+ /** 金十时间 → ISO:既吃 ISO(带 +08:00 偏移),也吃财经日历的无时区本地格式(按东八区解释)。 */
60
+ declare function parseJin10Time(value: unknown): string | undefined;
61
+ /** list_flash / search_flash 条目 → NewsItem(正文丢弃,只留标题/时间/链接)。 */
62
+ declare function parseFlashItems(data: unknown, context?: string, limit?: number): Jin10NewsItem[];
63
+ declare function parseFlashPage(data: unknown, context?: string, limit?: number): Jin10NewsPage<Jin10NewsItem>;
64
+ /** list_news / search_news 条目 → 文章摘要(id/title/时间/链接)。 */
65
+ declare function parseNewsItems(data: unknown, context?: string, limit?: number): Jin10ArticleSummary[];
66
+ declare function parseNewsPage(data: unknown, context?: string, limit?: number): Jin10NewsPage<Jin10ArticleSummary>;
67
+ /** get_news 详情:导语(introduction)作摘要下发;正文 content 按铁律 #5 丢弃。 */
68
+ declare function parseArticle(data: unknown): Jin10ArticleDetail;
69
+ /** list_calendar:data 为数组;pub_time 无时区,按东八区解释。 */
70
+ declare function parseCalendar(data: unknown, limit?: number): Jin10CalendarEntry[];
71
+ /** quote://codes 资源正文 → 品种表(`{data:[{code,name}]}`)。 */
72
+ declare function parseQuoteCodes(resourceText: string): Jin10Instrument[];
73
+ /** get_quote:价格字段是字符串,转数字;code 缺失视为形状异常。 */
74
+ declare function parseQuote(data: unknown): Jin10Quote;
75
+ /** get_kline:分钟级数组(time 为秒级时间戳);空数组是合法结果(窗口内无数据)。 */
76
+ declare function parseKlines(data: unknown, limit?: number): Jin10Kline[];
77
+ //#endregion
78
+ export { Jin10ArticleDetail, Jin10ArticleSummary, Jin10CalendarEntry, Jin10Instrument, Jin10Kline, Jin10NewsItem, Jin10NewsPage, Jin10Quote, flashTitle, parseArticle, parseCalendar, parseFlashItems, parseFlashPage, parseJin10Time, parseKlines, parseNewsItems, parseNewsPage, parseQuote, parseQuoteCodes };
package/lib/parse.js ADDED
@@ -0,0 +1,229 @@
1
+ import { Jin10Error } from "./errors.js";
2
+ //#region src/parse.ts
3
+ /** 快讯标题:【标题】正文 → 取括号内;无括号 → 首段文本截断(正文绝不下发)。 */
4
+ function flashTitle(content, maxLength = 120) {
5
+ const text = content.trim();
6
+ if (text.length === 0) return void 0;
7
+ const title = (/^【([^】]+)】/.exec(text)?.[1] ?? text.replace(/\s+/g, " ").split("。")[0] ?? text).trim();
8
+ if (title.length === 0) return void 0;
9
+ return title.length > maxLength ? `${title.slice(0, maxLength)}…` : title;
10
+ }
11
+ /** 金十时间 → ISO:既吃 ISO(带 +08:00 偏移),也吃财经日历的无时区本地格式(按东八区解释)。 */
12
+ function parseJin10Time(value) {
13
+ if (typeof value !== "string") return void 0;
14
+ const text = value.trim();
15
+ if (text.length === 0) return void 0;
16
+ const local = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2})(?::(\d{2}))?$/.exec(text);
17
+ const candidate = local === null ? text : `${local[1] ?? ""}T${local[2] ?? ""}:${local[3] ?? "00"}+08:00`;
18
+ const ms = Date.parse(candidate);
19
+ return Number.isFinite(ms) ? new Date(ms).toISOString() : void 0;
20
+ }
21
+ function isRecord(value) {
22
+ return typeof value === "object" && value !== null && !Array.isArray(value);
23
+ }
24
+ function text(value) {
25
+ if (typeof value !== "string") return void 0;
26
+ const trimmed = value.trim();
27
+ return trimmed.length > 0 ? trimmed : void 0;
28
+ }
29
+ function numberValue(value) {
30
+ if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
31
+ if (typeof value === "string") {
32
+ const parsed = Number(value.trim());
33
+ return value.trim().length > 0 && Number.isFinite(parsed) ? parsed : void 0;
34
+ }
35
+ }
36
+ function payloadItems(data, context) {
37
+ if (!isRecord(data)) throw new Jin10Error("TRADING_EXCHANGE_ERROR", `jin10 ${context}: unexpected payload (expected an object with items[])`);
38
+ const items = data.items;
39
+ if (!Array.isArray(items)) throw new Jin10Error("TRADING_EXCHANGE_ERROR", `jin10 ${context}: unexpected payload (expected data.items[])`);
40
+ return items;
41
+ }
42
+ function pageMeta(data) {
43
+ const record = isRecord(data) ? data : {};
44
+ const cursor = record.next_cursor;
45
+ return {
46
+ ...typeof cursor === "string" && cursor.length > 0 ? { nextCursor: cursor } : {},
47
+ hasMore: record.has_more === true
48
+ };
49
+ }
50
+ function capItems(items, limit) {
51
+ if (limit === void 0 || items.length <= limit) return items;
52
+ return items.slice(0, limit);
53
+ }
54
+ /** list_flash / search_flash 条目 → NewsItem(正文丢弃,只留标题/时间/链接)。 */
55
+ function parseFlashItems(data, context = "list_flash", limit) {
56
+ const items = [];
57
+ for (const entry of payloadItems(data, context)) {
58
+ if (!isRecord(entry)) continue;
59
+ const title = typeof entry.content === "string" ? flashTitle(entry.content) : void 0;
60
+ const publishedAt = parseJin10Time(entry.time);
61
+ const url = text(entry.url);
62
+ if (title === void 0 || publishedAt === void 0 || url === void 0) continue;
63
+ items.push({
64
+ source: "jin10",
65
+ title,
66
+ url,
67
+ publishedAt
68
+ });
69
+ }
70
+ return capItems(items, limit);
71
+ }
72
+ function parseFlashPage(data, context = "list_flash", limit) {
73
+ const meta = pageMeta(data);
74
+ return {
75
+ items: parseFlashItems(data, context, limit),
76
+ ...meta
77
+ };
78
+ }
79
+ /** list_news / search_news 条目 → 文章摘要(id/title/时间/链接)。 */
80
+ function parseNewsItems(data, context = "list_news", limit) {
81
+ const items = [];
82
+ for (const entry of payloadItems(data, context)) {
83
+ if (!isRecord(entry)) continue;
84
+ const id = text(entry.id);
85
+ const title = text(entry.title);
86
+ const publishedAt = parseJin10Time(entry.time);
87
+ const url = text(entry.url);
88
+ if (id === void 0 || title === void 0 || publishedAt === void 0 || url === void 0) continue;
89
+ items.push({
90
+ source: "jin10",
91
+ id,
92
+ title,
93
+ url,
94
+ publishedAt
95
+ });
96
+ }
97
+ return capItems(items, limit);
98
+ }
99
+ function parseNewsPage(data, context = "list_news", limit) {
100
+ const meta = pageMeta(data);
101
+ return {
102
+ items: parseNewsItems(data, context, limit),
103
+ ...meta
104
+ };
105
+ }
106
+ /** get_news 详情:导语(introduction)作摘要下发;正文 content 按铁律 #5 丢弃。 */
107
+ function parseArticle(data) {
108
+ if (!isRecord(data)) throw new Jin10Error("TRADING_EXCHANGE_ERROR", "jin10 get_news: unexpected payload (expected an object)");
109
+ const id = text(data.id);
110
+ const title = text(data.title);
111
+ const url = text(data.url);
112
+ const publishedAt = parseJin10Time(data.time);
113
+ if (id === void 0 || title === void 0 || url === void 0 || publishedAt === void 0) throw new Jin10Error("TRADING_EXCHANGE_ERROR", "jin10 get_news: unexpected payload (missing id/title/url/time)");
114
+ const introduction = text(data.introduction);
115
+ return {
116
+ id,
117
+ title,
118
+ url,
119
+ publishedAt,
120
+ ...introduction !== void 0 ? { introduction } : {}
121
+ };
122
+ }
123
+ /** list_calendar:data 为数组;pub_time 无时区,按东八区解释。 */
124
+ function parseCalendar(data, limit) {
125
+ if (!Array.isArray(data)) throw new Jin10Error("TRADING_EXCHANGE_ERROR", "jin10 list_calendar: unexpected payload (expected an array)");
126
+ const entries = [];
127
+ for (const entry of data) {
128
+ if (!isRecord(entry)) continue;
129
+ const title = text(entry.title);
130
+ const publishedAt = parseJin10Time(entry.pub_time);
131
+ if (title === void 0 || publishedAt === void 0) continue;
132
+ const star = numberValue(entry.star);
133
+ const previous = text(entry.previous);
134
+ const consensus = text(entry.consensus);
135
+ const actual = text(entry.actual);
136
+ const revised = text(entry.revised);
137
+ const affect = text(entry.affect_txt);
138
+ entries.push({
139
+ publishedAt,
140
+ star: star ?? 0,
141
+ title,
142
+ ...previous !== void 0 ? { previous } : {},
143
+ ...consensus !== void 0 ? { consensus } : {},
144
+ ...actual !== void 0 ? { actual } : {},
145
+ ...revised !== void 0 ? { revised } : {},
146
+ ...affect !== void 0 ? { affect } : {}
147
+ });
148
+ }
149
+ return capItems(entries, limit);
150
+ }
151
+ /** quote://codes 资源正文 → 品种表(`{data:[{code,name}]}`)。 */
152
+ function parseQuoteCodes(resourceText) {
153
+ let parsed;
154
+ try {
155
+ parsed = JSON.parse(resourceText);
156
+ } catch {
157
+ throw new Jin10Error("TRADING_EXCHANGE_ERROR", "jin10 quote://codes: resource is not valid JSON");
158
+ }
159
+ const data = isRecord(parsed) ? parsed.data : parsed;
160
+ if (!Array.isArray(data)) throw new Jin10Error("TRADING_EXCHANGE_ERROR", "jin10 quote://codes: unexpected payload (expected data[])");
161
+ const instruments = [];
162
+ for (const entry of data) {
163
+ if (!isRecord(entry)) continue;
164
+ const code = text(entry.code);
165
+ if (code === void 0) continue;
166
+ instruments.push({
167
+ code,
168
+ name: text(entry.name) ?? code
169
+ });
170
+ }
171
+ return instruments;
172
+ }
173
+ /** get_quote:价格字段是字符串,转数字;code 缺失视为形状异常。 */
174
+ function parseQuote(data) {
175
+ if (!isRecord(data)) throw new Jin10Error("TRADING_EXCHANGE_ERROR", "jin10 get_quote: unexpected payload (expected an object)");
176
+ const code = text(data.code);
177
+ if (code === void 0) throw new Jin10Error("TRADING_EXCHANGE_ERROR", "jin10 get_quote: unexpected payload (missing code)");
178
+ const name = text(data.name);
179
+ const time = parseJin10Time(data.time);
180
+ const open = numberValue(data.open);
181
+ const close = numberValue(data.close);
182
+ const high = numberValue(data.high);
183
+ const low = numberValue(data.low);
184
+ const volume = numberValue(data.volume);
185
+ const change = numberValue(data.ups_price);
186
+ const changePercent = numberValue(data.ups_percent);
187
+ return {
188
+ code,
189
+ ...name !== void 0 ? { name } : {},
190
+ ...time !== void 0 ? { time } : {},
191
+ ...open !== void 0 ? { open } : {},
192
+ ...close !== void 0 ? { close } : {},
193
+ ...high !== void 0 ? { high } : {},
194
+ ...low !== void 0 ? { low } : {},
195
+ ...volume !== void 0 ? { volume } : {},
196
+ ...change !== void 0 ? { change } : {},
197
+ ...changePercent !== void 0 ? { changePercent } : {}
198
+ };
199
+ }
200
+ /** get_kline:分钟级数组(time 为秒级时间戳);空数组是合法结果(窗口内无数据)。 */
201
+ function parseKlines(data, limit) {
202
+ if (!isRecord(data)) throw new Jin10Error("TRADING_EXCHANGE_ERROR", "jin10 get_kline: unexpected payload (expected an object)");
203
+ const raw = data.klines;
204
+ if (!Array.isArray(raw)) throw new Jin10Error("TRADING_EXCHANGE_ERROR", "jin10 get_kline: unexpected payload (expected data.klines[])");
205
+ const klines = [];
206
+ for (const entry of raw) {
207
+ if (!isRecord(entry)) continue;
208
+ const time = numberValue(entry.time);
209
+ if (time === void 0) continue;
210
+ const open = numberValue(entry.open);
211
+ const high = numberValue(entry.high);
212
+ const low = numberValue(entry.low);
213
+ const close = numberValue(entry.close);
214
+ const volume = numberValue(entry.volume);
215
+ if (open === void 0 && high === void 0 && low === void 0 && close === void 0) continue;
216
+ klines.push({
217
+ time,
218
+ ...open !== void 0 ? { open } : {},
219
+ ...high !== void 0 ? { high } : {},
220
+ ...low !== void 0 ? { low } : {},
221
+ ...close !== void 0 ? { close } : {},
222
+ ...volume !== void 0 ? { volume } : {}
223
+ });
224
+ }
225
+ klines.sort((a, b) => a.time - b.time);
226
+ return capItems(klines, limit);
227
+ }
228
+ //#endregion
229
+ export { flashTitle, parseArticle, parseCalendar, parseFlashItems, parseFlashPage, parseJin10Time, parseKlines, parseNewsItems, parseNewsPage, parseQuote, parseQuoteCodes };
@@ -0,0 +1,17 @@
1
+ import "./service.js";
2
+ import Schema from "@deepseek-ai/schemastery";
3
+ import { Context } from "@deepseek-ai/cordis";
4
+ //#region src/plugin.d.ts
5
+ /** Cordis 插件名 = patch 行 id(TEMPLATES §8)。 */
6
+ declare const name = "dsh-trading-connector-jin10";
7
+ declare const inject: string[];
8
+ interface Config {
9
+ enabled: boolean;
10
+ endpoint: string;
11
+ tokenRef: string;
12
+ timeoutMs: number;
13
+ }
14
+ declare const Config: Schema<Config>;
15
+ declare function apply(ctx: Context, config: Config): void;
16
+ //#endregion
17
+ export { Config, apply, inject, name };
package/lib/plugin.js ADDED
@@ -0,0 +1,39 @@
1
+ import { TRADING_FLASH_FEED_KEY, createJin10FlashFeed } from "./flash-service.js";
2
+ import { DEFAULT_ENDPOINT, DEFAULT_TIMEOUT_MS, Jin10McpClient } from "./mcp.js";
3
+ import { Jin10Service } from "./service.js";
4
+ import { createJin10Tools } from "./tools.js";
5
+ import Schema from "@deepseek-ai/schemastery";
6
+ //#region src/plugin.ts
7
+ /** Cordis 插件名 = patch 行 id(TEMPLATES §8)。 */
8
+ const name = "dsh-trading-connector-jin10";
9
+ const inject = ["tools"];
10
+ const Config = Schema.object({
11
+ enabled: Schema.boolean().default(true).description("是否注册金十数据 MCP 工具"),
12
+ endpoint: Schema.string().default(DEFAULT_ENDPOINT).description("金十数据 MCP 服务地址"),
13
+ tokenRef: Schema.string().default("JIN10_MCP_TOKEN").description("金十 MCP Token 的环境变量名(BYOK)"),
14
+ timeoutMs: Schema.number().default(DEFAULT_TIMEOUT_MS).description("单次 MCP 请求超时(ms)")
15
+ });
16
+ /** 惰性凭证源:provider 键固定 `jin10`(settings credentials 字典的键)。 */
17
+ function createTokenProvider(ctx, tokenRef) {
18
+ return () => {
19
+ return (ctx.get?.("tradingMarketRouter", false))?.getCredential?.("jin10")?.token || process.env[tokenRef];
20
+ };
21
+ }
22
+ /** 组装取数服务(测试可直接注入 fetchImpl 的 MCP 客户端)。 */
23
+ function createJin10Service(ctx, config, overrides = {}) {
24
+ return new Jin10Service(new Jin10McpClient({
25
+ endpoint: config.endpoint,
26
+ token: createTokenProvider(ctx, config.tokenRef),
27
+ timeoutMs: config.timeoutMs,
28
+ ...overrides.fetchImpl !== void 0 ? { fetchImpl: overrides.fetchImpl } : {}
29
+ }));
30
+ }
31
+ function apply(ctx, config) {
32
+ if (!config.enabled) return;
33
+ const service = createJin10Service(ctx, config);
34
+ ctx.reflect.provide(TRADING_FLASH_FEED_KEY, createJin10FlashFeed(service));
35
+ const tools = ctx.tools;
36
+ for (const tool of createJin10Tools(service)) if (tools.get(tool.name) === void 0) tools.register(tool);
37
+ }
38
+ //#endregion
39
+ export { Config, apply, createJin10Service, createTokenProvider, inject, name };
@@ -0,0 +1,53 @@
1
+ import { Jin10McpClient } from "./mcp.js";
2
+ import { Jin10ArticleDetail, Jin10ArticleSummary, Jin10CalendarEntry, Jin10Instrument, Jin10Kline, Jin10NewsItem, Jin10NewsPage, Jin10Quote } from "./parse.js";
3
+ import { Jin10FlashItem, Jin10HeatLevel } from "./web-flash.js";
4
+ //#region src/service.d.ts
5
+ /** 快讯/资讯单页上限(上游 list_* 每页 20 条;search_flash 一次性最多 150 条且不可翻页)。 */
6
+ declare const FLASH_PAGE_SIZE = 20;
7
+ declare const SEARCH_FLASH_MAX = 150;
8
+ /** 品种代码表缓存 TTL(静态名册,按小时级缓存;失败不缓存,下次调用重试)。 */
9
+ declare const INSTRUMENT_CACHE_TTL_MS = 3600000;
10
+ interface Jin10PageOptions {
11
+ cursor?: string | undefined;
12
+ limit?: number | undefined;
13
+ }
14
+ declare class Jin10Service {
15
+ private readonly client;
16
+ private readonly webFetch;
17
+ private readonly webTimeoutMs;
18
+ private instruments;
19
+ constructor(client: Jin10McpClient, web?: {
20
+ fetchImpl?: typeof globalThis.fetch | undefined;
21
+ timeoutMs?: number | undefined;
22
+ });
23
+ /**
24
+ * 按热度取快讯(金十网页版服务端过滤,火/热/沸/爆):官方 MCP 的 list_flash
25
+ * 无热度参数,故热度路径走网页版接口(见 web-flash.ts)。cursor = 上一页
26
+ * 最旧一条的 time 原串。
27
+ */
28
+ listFlashByHeat(options: {
29
+ hot: readonly Jin10HeatLevel[];
30
+ cursor?: string | undefined;
31
+ }): Promise<Jin10NewsPage<Jin10FlashItem>>;
32
+ /** 最新快讯流(cursor 翻页;正文不下发,只留标题/时间/链接)。 */
33
+ listFlash(options?: Jin10PageOptions): Promise<Jin10NewsPage<Jin10NewsItem>>;
34
+ /** 关键词搜快讯:上游一次性返回(最多 150 条,不支持翻页),故无 cursor。 */
35
+ searchFlash(keyword: string, limit?: number): Promise<Jin10NewsItem[]>;
36
+ listNews(options?: Jin10PageOptions): Promise<Jin10NewsPage<Jin10ArticleSummary>>;
37
+ searchNews(keyword: string, options?: Jin10PageOptions): Promise<Jin10NewsPage<Jin10ArticleSummary>>;
38
+ /** 文章详情:标题/导语/时间/链接(正文按铁律 #5 不下发)。 */
39
+ getArticle(id: string): Promise<Jin10ArticleDetail>;
40
+ /** 本周财经日历(周一~周日;上游一次返回整周)。 */
41
+ listCalendar(limit?: number): Promise<Jin10CalendarEntry[]>;
42
+ /** 品种名册(quote://codes 资源);query 按代码或中文名过滤。 */
43
+ listInstruments(query?: string): Promise<Jin10Instrument[]>;
44
+ getQuote(code: string): Promise<Jin10Quote>;
45
+ /** 分钟级 K 线:`time` = 起始秒级时间戳(从此往后取,窗口 24h 内),count ≤ 100。 */
46
+ getKlines(code: string, options?: {
47
+ time?: number | undefined;
48
+ count?: number | undefined;
49
+ }): Promise<Jin10Kline[]>;
50
+ private instrumentCatalog;
51
+ }
52
+ //#endregion
53
+ export { FLASH_PAGE_SIZE, INSTRUMENT_CACHE_TTL_MS, Jin10PageOptions, Jin10Service, SEARCH_FLASH_MAX };