@dshtrading/router 0.1.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/lib/catalog.d.ts +46 -0
- package/lib/catalog.js +1073 -0
- package/lib/index.d.ts +136 -0
- package/lib/index.js +244 -0
- package/lib/tools.js +128 -0
- package/package.json +40 -0
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { Context, Service } from "@deepseek-ai/cordis";
|
|
2
|
+
import Schema from "@deepseek-ai/schemastery";
|
|
3
|
+
import { SettingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
4
|
+
import { MarketDataRegistration, MarketDataRegistry, MarketDataService, MarketRouterService as MarketRouterService$1, NewsAggregator } from "@dshtrading/api";
|
|
5
|
+
//#region src/index.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Cordis 插件名 = patch 行 id:市场无关共享行,base 拥有(铁律 #1/#4)。
|
|
8
|
+
* 全仓唯一,绝不使用 `base` 等官方保留 id(insert-only 铁律 #1)。
|
|
9
|
+
*/
|
|
10
|
+
declare const name = "dsh-trading-market-router";
|
|
11
|
+
/**
|
|
12
|
+
* 内置 provider 词汇表(slug = 路由层词汇,非包名/行 id)——**仅供 UI 展示与
|
|
13
|
+
* 运行时告警,不再是 schema 层硬门槛**(2026-08-30 开放化,架构评审整改 #4):
|
|
14
|
+
* schema 接受任意字符串 slug,第三方连接器注册同名 slug + 用户设置同名值即可
|
|
15
|
+
* 上榜,不需要改本仓任何代码。未知 slug 的代价 = router 启动/变更时 log warn +
|
|
16
|
+
* 无任何连接器激活(fail-soft),不写时拒。
|
|
17
|
+
*/
|
|
18
|
+
declare const PROVIDER_VOCABULARY: readonly ["binance", "okx", "bybit", "ccxt", "yahoo", "stooq", "alpaca", "fmp", "finnhub", "polygon", "ibkr", "tencent", "eastmoney", "tushare", "akshare", "qmt", "futu", "longbridge", "tiger", "hithink"];
|
|
19
|
+
type Provider = (typeof PROVIDER_VOCABULARY)[number];
|
|
20
|
+
interface MarketProviderEntry {
|
|
21
|
+
/** 该市场当前激活的数据/交易所提供方(连接器 consult 这个值决定激活与否)。开放词汇。 */
|
|
22
|
+
provider: string;
|
|
23
|
+
/** 预留:数据面与交易面分离时的交易提供方(不实现,仅类型面占位,见 §2.4)。 */
|
|
24
|
+
tradeProvider?: string;
|
|
25
|
+
}
|
|
26
|
+
interface NewsConfig {
|
|
27
|
+
/** 可选:CryptoPanic API token(WS2c,#4)。有值时 crypto_get_news 走 CryptoPanic 免费层增强;无值/无效则优雅降级到公共源。 */
|
|
28
|
+
cryptoPanicKey?: string;
|
|
29
|
+
}
|
|
30
|
+
interface Config {
|
|
31
|
+
/** 各市场数据提供方;dict 键开放(新市场 = 新键,schema 零改)。 */
|
|
32
|
+
markets: Record<string, MarketProviderEntry>;
|
|
33
|
+
/** 各提供方 API 凭证(apiKey / apiSecret / token / gatewayUrl 等;dict 开放)。 */
|
|
34
|
+
credentials?: Record<string, Record<string, string>>;
|
|
35
|
+
/** 新闻相关设置(WS2c):默认无 key(公共源)。 */
|
|
36
|
+
news?: NewsConfig;
|
|
37
|
+
}
|
|
38
|
+
declare const DEFAULT_MARKETS: Record<string, MarketProviderEntry>;
|
|
39
|
+
declare const Config: Schema<Config>;
|
|
40
|
+
/** settings namespace(kebab-case 品牌化,llm-pi-ai 同款)。 */
|
|
41
|
+
declare const SETTINGS_NAMESPACE: SettingsNamespace;
|
|
42
|
+
declare class MarketRouterService extends Service implements MarketRouterService$1 {
|
|
43
|
+
private source;
|
|
44
|
+
private readonly watchers;
|
|
45
|
+
private last;
|
|
46
|
+
constructor(ctx: Context, source: () => Config);
|
|
47
|
+
/** settings onChange 时替换权威源(installSettingsSection.setSource 契约)。 */
|
|
48
|
+
setSource(source: () => Config): void;
|
|
49
|
+
/** 某市场当前激活的 provider slug(settings resolved:用户层赢,缺省 base 默认)。 */
|
|
50
|
+
activeProvider(market: string): string | undefined;
|
|
51
|
+
/** 获取某提供方的 API 凭证字典(如 apiKey、apiSecret 等)。 */
|
|
52
|
+
getCredential(provider: string): Record<string, string> | undefined;
|
|
53
|
+
/** WS2c:CryptoPanic API token(settings resolved;缺省 undefined = 无 key = 新闻走公共源)。 */
|
|
54
|
+
newsKey(): string | undefined;
|
|
55
|
+
/** 订阅激活变化(settings commit 驱动;restart 型当前仅记录,未来 live 用)。 */
|
|
56
|
+
watch(cb: (next: string | undefined, prev: string | undefined) => void): () => void;
|
|
57
|
+
/** 内用:settings onChange 后 diff 并通知 watchers(通知在 watch 后注册的同步回调)。 */
|
|
58
|
+
notify(): void;
|
|
59
|
+
}
|
|
60
|
+
/** SDK 服务键(与 @dshtrading/api 的 Context 模块增强一致)。 */
|
|
61
|
+
declare const TRADING_MARKET_ROUTER_KEY = "tradingMarketRouter";
|
|
62
|
+
/**
|
|
63
|
+
* 行情服务注册表(2026-08-30 注册表模式定稿,架构评审整改 #1):
|
|
64
|
+
* 连接器 host 面数据行全部注册进本表(不再互斥式 provide 市场键),
|
|
65
|
+
* 激活解析 = 本表按 router 当前值惰性裁决——settings 变更即刻生效
|
|
66
|
+
*(GUI 热切换,修复「会话面新建会话生效、GUI 面须重启进程」的语义裂口)。
|
|
67
|
+
*
|
|
68
|
+
* 与 router 同插件同 fiber 提供:base patch 行零改动,生命周期随行。
|
|
69
|
+
*/
|
|
70
|
+
declare class MarketDataRegistryService extends Service implements MarketDataRegistry {
|
|
71
|
+
private readonly router;
|
|
72
|
+
private readonly entries;
|
|
73
|
+
constructor(ctx: Context, router: MarketRouterService);
|
|
74
|
+
private static keyOf;
|
|
75
|
+
register(market: string, provider: string, service: MarketDataService): () => void;
|
|
76
|
+
active(market: string): MarketDataRegistration | undefined;
|
|
77
|
+
list(market: string): readonly MarketDataRegistration[];
|
|
78
|
+
}
|
|
79
|
+
/** 注册表服务键(与 @dshtrading/api 的 Context 模块增强一致)。 */
|
|
80
|
+
declare const TRADING_MARKET_DATA_REGISTRY_KEY = "tradingMarketDataRegistry";
|
|
81
|
+
/** 连接器/桥侧最小形状(不定死接口)。 */
|
|
82
|
+
interface MarketDataRegistryLike {
|
|
83
|
+
register(market: string, provider: string, service: MarketDataService): () => void;
|
|
84
|
+
active(market: string): MarketDataRegistration | undefined;
|
|
85
|
+
}
|
|
86
|
+
declare const TRADING_NEWS_REGISTRY_KEY = "tradingNewsRegistry";
|
|
87
|
+
/**
|
|
88
|
+
* 新闻聚合器注册表(Issue #37,与 MarketDataRegistry 同模式):
|
|
89
|
+
* 各市场 Kit 在 Preset 平面 apply 时向本注册表注册其 aggregateNews 纯函数;
|
|
90
|
+
* GUI 行情桥按路由当前值惰性解析。注册与 Kit 生命周期绑定:
|
|
91
|
+
* Preset 销毁时调用方执行退订函数,该市场新闻自动不可用。
|
|
92
|
+
*/
|
|
93
|
+
declare class TradingNewsRegistryService extends Service {
|
|
94
|
+
private readonly providers;
|
|
95
|
+
constructor(ctx: Context);
|
|
96
|
+
/** Kit apply 时注册本市场的新闻聚合器;返回退订函数(Kit dispose 时自动清理)。 */
|
|
97
|
+
register(market: string, aggregator: NewsAggregator): () => void;
|
|
98
|
+
/** Bridge 分发时取该市场的聚合器;未注册 → undefined(该市场新闻不可用)。 */
|
|
99
|
+
get(market: string): NewsAggregator | undefined;
|
|
100
|
+
/** 全部已注册市场。 */
|
|
101
|
+
markets(): string[];
|
|
102
|
+
}
|
|
103
|
+
/** 连接器/桥侧最小形状(鸭式)。 */
|
|
104
|
+
interface TradingNewsRegistryLike {
|
|
105
|
+
register(market: string, aggregator: NewsAggregator): () => void;
|
|
106
|
+
get(market: string): NewsAggregator | undefined;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* 解析注册表服务的辅助(连接器 dataplane 与行情桥使用):
|
|
110
|
+
* 拿不到(老部署 base/router 未升级)→ undefined,调用方回退旧的直接 provide 路径。
|
|
111
|
+
*/
|
|
112
|
+
declare function resolveMarketDataRegistry(ctx: Context): MarketDataRegistryLike | undefined;
|
|
113
|
+
/** 连接器侧最小形状(api 包 MarketRouterService 的同构声明;不定死接口)。 */
|
|
114
|
+
interface MarketRouterLike {
|
|
115
|
+
activeProvider(market: string): string | undefined;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* 解析路由服务的辅助(连接器 apply 使用;ctx.get 的形态随 cordis 面,与 tools 同规则):
|
|
119
|
+
* 拿不到(无 router / 未 inject)→ undefined,调用方回退 enabled 语义(向后兼容)。
|
|
120
|
+
*/
|
|
121
|
+
declare function resolveMarketRouter(ctx: Context): MarketRouterLike | undefined;
|
|
122
|
+
/** 宿主 logger 的最小形状(ctx.logger(name) 不可用时回落 console)。 */
|
|
123
|
+
interface LogLike {
|
|
124
|
+
warn: (...args: unknown[]) => void;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* 开放词汇的运行时校验(整改 #4):schema 不再拒未知 slug,改为 warn + fail-soft
|
|
128
|
+
* (无匹配连接器注册即无激活)。已知词汇仅供此告警与设置 UI 候选清单。
|
|
129
|
+
* 返回未知 slug 清单(测试可直证)。
|
|
130
|
+
*/
|
|
131
|
+
declare function warnUnknownProviders(config: Config, log: LogLike): string[];
|
|
132
|
+
declare function apply(ctx: Context, config: Config): void;
|
|
133
|
+
/** 供测试/连接器单测使用的纯函数:给定 Config 返回市场路由判定。 */
|
|
134
|
+
declare function activeProviderOf(config: Config, market: string): string | undefined;
|
|
135
|
+
//#endregion
|
|
136
|
+
export { Config, DEFAULT_MARKETS, MarketDataRegistryLike, MarketDataRegistryService, MarketProviderEntry, MarketRouterLike, MarketRouterService, NewsConfig, PROVIDER_VOCABULARY, Provider, SETTINGS_NAMESPACE, TRADING_MARKET_DATA_REGISTRY_KEY, TRADING_MARKET_ROUTER_KEY, TRADING_NEWS_REGISTRY_KEY, TradingNewsRegistryLike, TradingNewsRegistryService, activeProviderOf, apply, name, resolveMarketDataRegistry, resolveMarketRouter, warnUnknownProviders };
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { createInstrumentsSearchTool, createRoutingGetTool } from "./tools.js";
|
|
2
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
3
|
+
import Schema from "@deepseek-ai/schemastery";
|
|
4
|
+
//#region src/index.ts
|
|
5
|
+
/**
|
|
6
|
+
* Cordis 插件名 = patch 行 id:市场无关共享行,base 拥有(铁律 #1/#4)。
|
|
7
|
+
* 全仓唯一,绝不使用 `base` 等官方保留 id(insert-only 铁律 #1)。
|
|
8
|
+
*/
|
|
9
|
+
const name = "dsh-trading-market-router";
|
|
10
|
+
/**
|
|
11
|
+
* 内置 provider 词汇表(slug = 路由层词汇,非包名/行 id)——**仅供 UI 展示与
|
|
12
|
+
* 运行时告警,不再是 schema 层硬门槛**(2026-08-30 开放化,架构评审整改 #4):
|
|
13
|
+
* schema 接受任意字符串 slug,第三方连接器注册同名 slug + 用户设置同名值即可
|
|
14
|
+
* 上榜,不需要改本仓任何代码。未知 slug 的代价 = router 启动/变更时 log warn +
|
|
15
|
+
* 无任何连接器激活(fail-soft),不写时拒。
|
|
16
|
+
*/
|
|
17
|
+
const PROVIDER_VOCABULARY = [
|
|
18
|
+
"binance",
|
|
19
|
+
"okx",
|
|
20
|
+
"bybit",
|
|
21
|
+
"ccxt",
|
|
22
|
+
"yahoo",
|
|
23
|
+
"stooq",
|
|
24
|
+
"alpaca",
|
|
25
|
+
"fmp",
|
|
26
|
+
"finnhub",
|
|
27
|
+
"polygon",
|
|
28
|
+
"ibkr",
|
|
29
|
+
"tencent",
|
|
30
|
+
"eastmoney",
|
|
31
|
+
"tushare",
|
|
32
|
+
"akshare",
|
|
33
|
+
"qmt",
|
|
34
|
+
"futu",
|
|
35
|
+
"longbridge",
|
|
36
|
+
"tiger",
|
|
37
|
+
"hithink"
|
|
38
|
+
];
|
|
39
|
+
const DEFAULT_MARKETS = {
|
|
40
|
+
crypto: { provider: "binance" },
|
|
41
|
+
us: { provider: "yahoo" },
|
|
42
|
+
cn: { provider: "tencent" },
|
|
43
|
+
hk: { provider: "tencent" }
|
|
44
|
+
};
|
|
45
|
+
const MarketProviderEntrySchema = Schema.object({
|
|
46
|
+
provider: Schema.string(),
|
|
47
|
+
tradeProvider: Schema.string().default(void 0)
|
|
48
|
+
});
|
|
49
|
+
const Config = Schema.object({
|
|
50
|
+
markets: Schema.dict(MarketProviderEntrySchema).default({ ...DEFAULT_MARKETS }),
|
|
51
|
+
credentials: Schema.dict(Schema.dict(Schema.string())).default({}),
|
|
52
|
+
news: Schema.object({ cryptoPanicKey: Schema.string().default(void 0) }).default({})
|
|
53
|
+
});
|
|
54
|
+
/** settings namespace(kebab-case 品牌化,llm-pi-ai 同款)。 */
|
|
55
|
+
const SETTINGS_NAMESPACE = "dshtrading";
|
|
56
|
+
var MarketRouterService = class extends Service {
|
|
57
|
+
source;
|
|
58
|
+
watchers = /* @__PURE__ */ new Set();
|
|
59
|
+
last = {};
|
|
60
|
+
constructor(ctx, source) {
|
|
61
|
+
super(ctx, "tradingMarketRouter");
|
|
62
|
+
this.source = source;
|
|
63
|
+
}
|
|
64
|
+
/** settings onChange 时替换权威源(installSettingsSection.setSource 契约)。 */
|
|
65
|
+
setSource(source) {
|
|
66
|
+
this.source = source;
|
|
67
|
+
}
|
|
68
|
+
/** 某市场当前激活的 provider slug(settings resolved:用户层赢,缺省 base 默认)。 */
|
|
69
|
+
activeProvider(market) {
|
|
70
|
+
return this.source().markets[market]?.provider;
|
|
71
|
+
}
|
|
72
|
+
/** 获取某提供方的 API 凭证字典(如 apiKey、apiSecret 等)。 */
|
|
73
|
+
getCredential(provider) {
|
|
74
|
+
return this.source().credentials?.[provider];
|
|
75
|
+
}
|
|
76
|
+
/** WS2c:CryptoPanic API token(settings resolved;缺省 undefined = 无 key = 新闻走公共源)。 */
|
|
77
|
+
newsKey() {
|
|
78
|
+
return this.source().news?.cryptoPanicKey;
|
|
79
|
+
}
|
|
80
|
+
/** 订阅激活变化(settings commit 驱动;restart 型当前仅记录,未来 live 用)。 */
|
|
81
|
+
watch(cb) {
|
|
82
|
+
this.watchers.add(cb);
|
|
83
|
+
return () => {
|
|
84
|
+
this.watchers.delete(cb);
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** 内用:settings onChange 后 diff 并通知 watchers(通知在 watch 后注册的同步回调)。 */
|
|
88
|
+
notify() {
|
|
89
|
+
const source = this.source();
|
|
90
|
+
const next = {};
|
|
91
|
+
for (const [market, entry] of Object.entries(source.markets)) next[market] = entry.provider;
|
|
92
|
+
for (const [market, provider] of Object.entries(next)) {
|
|
93
|
+
const prev = this.last[market];
|
|
94
|
+
if (prev !== provider) for (const cb of this.watchers) cb(provider, prev);
|
|
95
|
+
}
|
|
96
|
+
this.last = next;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
/** SDK 服务键(与 @dshtrading/api 的 Context 模块增强一致)。 */
|
|
100
|
+
const TRADING_MARKET_ROUTER_KEY = "tradingMarketRouter";
|
|
101
|
+
/**
|
|
102
|
+
* 行情服务注册表(2026-08-30 注册表模式定稿,架构评审整改 #1):
|
|
103
|
+
* 连接器 host 面数据行全部注册进本表(不再互斥式 provide 市场键),
|
|
104
|
+
* 激活解析 = 本表按 router 当前值惰性裁决——settings 变更即刻生效
|
|
105
|
+
*(GUI 热切换,修复「会话面新建会话生效、GUI 面须重启进程」的语义裂口)。
|
|
106
|
+
*
|
|
107
|
+
* 与 router 同插件同 fiber 提供:base patch 行零改动,生命周期随行。
|
|
108
|
+
*/
|
|
109
|
+
var MarketDataRegistryService = class MarketDataRegistryService extends Service {
|
|
110
|
+
router;
|
|
111
|
+
entries = /* @__PURE__ */ new Map();
|
|
112
|
+
constructor(ctx, router) {
|
|
113
|
+
super(ctx, TRADING_MARKET_DATA_REGISTRY_KEY);
|
|
114
|
+
this.router = router;
|
|
115
|
+
}
|
|
116
|
+
static keyOf(market, provider) {
|
|
117
|
+
return market + " " + provider;
|
|
118
|
+
}
|
|
119
|
+
register(market, provider, service) {
|
|
120
|
+
const key = MarketDataRegistryService.keyOf(market, provider);
|
|
121
|
+
const existing = this.entries.get(key);
|
|
122
|
+
if (existing !== void 0 && existing.service !== service) throw new Error("[dsh-trading-market-router] duplicate market data registration: " + market + "/" + provider);
|
|
123
|
+
const registration = {
|
|
124
|
+
market,
|
|
125
|
+
provider,
|
|
126
|
+
service
|
|
127
|
+
};
|
|
128
|
+
this.entries.set(key, registration);
|
|
129
|
+
return () => {
|
|
130
|
+
if (this.entries.get(key) === registration) this.entries.delete(key);
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
active(market) {
|
|
134
|
+
const routed = this.router.activeProvider(market);
|
|
135
|
+
if (routed !== void 0) return this.entries.get(MarketDataRegistryService.keyOf(market, routed));
|
|
136
|
+
const all = this.list(market);
|
|
137
|
+
return all.length === 1 ? all[0] : void 0;
|
|
138
|
+
}
|
|
139
|
+
list(market) {
|
|
140
|
+
return [...this.entries.values()].filter((entry) => entry.market === market);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
/** 注册表服务键(与 @dshtrading/api 的 Context 模块增强一致)。 */
|
|
144
|
+
const TRADING_MARKET_DATA_REGISTRY_KEY = "tradingMarketDataRegistry";
|
|
145
|
+
const TRADING_NEWS_REGISTRY_KEY = "tradingNewsRegistry";
|
|
146
|
+
/**
|
|
147
|
+
* 新闻聚合器注册表(Issue #37,与 MarketDataRegistry 同模式):
|
|
148
|
+
* 各市场 Kit 在 Preset 平面 apply 时向本注册表注册其 aggregateNews 纯函数;
|
|
149
|
+
* GUI 行情桥按路由当前值惰性解析。注册与 Kit 生命周期绑定:
|
|
150
|
+
* Preset 销毁时调用方执行退订函数,该市场新闻自动不可用。
|
|
151
|
+
*/
|
|
152
|
+
var TradingNewsRegistryService = class extends Service {
|
|
153
|
+
providers = /* @__PURE__ */ new Map();
|
|
154
|
+
constructor(ctx) {
|
|
155
|
+
super(ctx, TRADING_NEWS_REGISTRY_KEY);
|
|
156
|
+
}
|
|
157
|
+
/** Kit apply 时注册本市场的新闻聚合器;返回退订函数(Kit dispose 时自动清理)。 */
|
|
158
|
+
register(market, aggregator) {
|
|
159
|
+
this.providers.set(market, aggregator);
|
|
160
|
+
return () => {
|
|
161
|
+
if (this.providers.get(market) === aggregator) this.providers.delete(market);
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/** Bridge 分发时取该市场的聚合器;未注册 → undefined(该市场新闻不可用)。 */
|
|
165
|
+
get(market) {
|
|
166
|
+
return this.providers.get(market);
|
|
167
|
+
}
|
|
168
|
+
/** 全部已注册市场。 */
|
|
169
|
+
markets() {
|
|
170
|
+
return [...this.providers.keys()];
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* 解析注册表服务的辅助(连接器 dataplane 与行情桥使用):
|
|
175
|
+
* 拿不到(老部署 base/router 未升级)→ undefined,调用方回退旧的直接 provide 路径。
|
|
176
|
+
*/
|
|
177
|
+
function resolveMarketDataRegistry(ctx) {
|
|
178
|
+
const candidate = ctx.get?.(TRADING_MARKET_DATA_REGISTRY_KEY);
|
|
179
|
+
return candidate !== void 0 ? candidate : void 0;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* 解析路由服务的辅助(连接器 apply 使用;ctx.get 的形态随 cordis 面,与 tools 同规则):
|
|
183
|
+
* 拿不到(无 router / 未 inject)→ undefined,调用方回退 enabled 语义(向后兼容)。
|
|
184
|
+
*/
|
|
185
|
+
function resolveMarketRouter(ctx) {
|
|
186
|
+
const candidate = ctx.get?.(TRADING_MARKET_ROUTER_KEY);
|
|
187
|
+
return candidate !== void 0 ? candidate : void 0;
|
|
188
|
+
}
|
|
189
|
+
function logger(ctx) {
|
|
190
|
+
const service = ctx.logger;
|
|
191
|
+
return typeof service === "function" ? service(name) : console;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* 开放词汇的运行时校验(整改 #4):schema 不再拒未知 slug,改为 warn + fail-soft
|
|
195
|
+
* (无匹配连接器注册即无激活)。已知词汇仅供此告警与设置 UI 候选清单。
|
|
196
|
+
* 返回未知 slug 清单(测试可直证)。
|
|
197
|
+
*/
|
|
198
|
+
function warnUnknownProviders(config, log) {
|
|
199
|
+
const known = new Set(PROVIDER_VOCABULARY);
|
|
200
|
+
const unknown = [];
|
|
201
|
+
for (const [market, entry] of Object.entries(config.markets)) {
|
|
202
|
+
for (const slug of [entry.provider, entry.tradeProvider]) if (slug !== void 0 && !known.has(slug) && !unknown.includes(slug)) unknown.push(slug);
|
|
203
|
+
if (entry.provider !== void 0 && !known.has(entry.provider)) log.warn("[dsh-trading-market-router] unknown provider slug %s for market %s — no built-in connector will activate; this is only valid if a third-party connector registers the same slug (see docs/connector-playbook.md)", entry.provider, market);
|
|
204
|
+
}
|
|
205
|
+
return unknown;
|
|
206
|
+
}
|
|
207
|
+
function apply(ctx, config) {
|
|
208
|
+
const effective = {
|
|
209
|
+
markets: {
|
|
210
|
+
...DEFAULT_MARKETS,
|
|
211
|
+
...config?.markets ?? {}
|
|
212
|
+
},
|
|
213
|
+
news: config?.news ?? {}
|
|
214
|
+
};
|
|
215
|
+
const service = new MarketRouterService(ctx, () => effective);
|
|
216
|
+
const registry = new MarketDataRegistryService(ctx, service);
|
|
217
|
+
new TradingNewsRegistryService(ctx);
|
|
218
|
+
const log = logger(ctx);
|
|
219
|
+
warnUnknownProviders(effective, log);
|
|
220
|
+
ctx.inject(["tools"], (toolCtx) => {
|
|
221
|
+
const tools = toolCtx.tools;
|
|
222
|
+
if (!tools || typeof tools.register !== "function") return;
|
|
223
|
+
const services = {
|
|
224
|
+
activeProvider: (market) => service.activeProvider(market),
|
|
225
|
+
registry: { active: (market) => registry.active(market) }
|
|
226
|
+
};
|
|
227
|
+
for (const tool of [createRoutingGetTool(services), createInstrumentsSearchTool(services)]) if (tools.get(tool.name) === void 0) tools.register(tool);
|
|
228
|
+
});
|
|
229
|
+
ctx.inject(["settings"], (settingsCtx) => {
|
|
230
|
+
settingsCtx.settings.installSection(ctx, SETTINGS_NAMESPACE, Config, effective, {
|
|
231
|
+
setSource: (current) => {
|
|
232
|
+
service.setSource(current);
|
|
233
|
+
warnUnknownProviders(current(), log);
|
|
234
|
+
},
|
|
235
|
+
onChange: () => service.notify()
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
/** 供测试/连接器单测使用的纯函数:给定 Config 返回市场路由判定。 */
|
|
240
|
+
function activeProviderOf(config, market) {
|
|
241
|
+
return config.markets[market]?.provider;
|
|
242
|
+
}
|
|
243
|
+
//#endregion
|
|
244
|
+
export { Config, DEFAULT_MARKETS, MarketDataRegistryService, MarketRouterService, PROVIDER_VOCABULARY, SETTINGS_NAMESPACE, TRADING_MARKET_DATA_REGISTRY_KEY, TRADING_MARKET_ROUTER_KEY, TRADING_NEWS_REGISTRY_KEY, TradingNewsRegistryService, activeProviderOf, apply, name, resolveMarketDataRegistry, resolveMarketRouter, warnUnknownProviders };
|
package/lib/tools.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { SYMBOL_CATALOG } from "./catalog.js";
|
|
2
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
3
|
+
//#region src/tools.ts
|
|
4
|
+
/**
|
|
5
|
+
* 路由与标的检索工具(issue #33 / P4,host 平面,全会话可见 D4):
|
|
6
|
+
* - `routing_get`:各市场当前激活 provider(settings 权威);
|
|
7
|
+
* - `instruments_search`:跨市场标的检索 = registry 动态全集(listInstruments)
|
|
8
|
+
* ∪ 内置静态字典(catalog)兜底,按 query 子串过滤。
|
|
9
|
+
*/
|
|
10
|
+
const MARKETS = [
|
|
11
|
+
"crypto",
|
|
12
|
+
"us",
|
|
13
|
+
"cn",
|
|
14
|
+
"hk"
|
|
15
|
+
];
|
|
16
|
+
/** routing_get 工厂。 */
|
|
17
|
+
function createRoutingGetTool(services) {
|
|
18
|
+
return defineTool({
|
|
19
|
+
name: "routing_get",
|
|
20
|
+
description: "Show the currently active market-data provider per market (crypto/us/cn/hk) — the authoritative value comes from the dshtrading settings namespace (dshtrading.markets.<market>.provider). Read-only.",
|
|
21
|
+
parameters: {},
|
|
22
|
+
output: {
|
|
23
|
+
schema: { type: "string" },
|
|
24
|
+
render: (_args, value) => [{
|
|
25
|
+
type: "text",
|
|
26
|
+
text: value
|
|
27
|
+
}]
|
|
28
|
+
},
|
|
29
|
+
async execute() {
|
|
30
|
+
const rows = MARKETS.map((market) => {
|
|
31
|
+
const active = services.registry?.active(market);
|
|
32
|
+
const provider = services.activeProvider(market);
|
|
33
|
+
return {
|
|
34
|
+
market,
|
|
35
|
+
provider: provider ?? null,
|
|
36
|
+
active: active !== void 0,
|
|
37
|
+
activeProvider: active?.provider ?? null,
|
|
38
|
+
note: active === void 0 ? provider !== void 0 ? "selected but not registered (connector missing/disabled)" : "no provider selected" : "serving"
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
return JSON.stringify({
|
|
42
|
+
ok: true,
|
|
43
|
+
settings: "dshtrading.markets.<market>.provider",
|
|
44
|
+
markets: rows
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/** instruments_search 工厂。 */
|
|
50
|
+
function createInstrumentsSearchTool(services) {
|
|
51
|
+
return defineTool({
|
|
52
|
+
name: "instruments_search",
|
|
53
|
+
description: "Search tradable instruments across markets (crypto/us/cn/hk) by symbol or name substring, case-insensitive. Results union the routed provider dynamic roster (when it supports listing) with the built-in static catalog. Use a returned entry with watchlist_add / watchlist_select.",
|
|
54
|
+
parameters: {
|
|
55
|
+
query: {
|
|
56
|
+
type: "string",
|
|
57
|
+
required: true,
|
|
58
|
+
description: "Substring to match against symbol or name, e.g. \"腾讯\" / \"BTC\" / \"apple\""
|
|
59
|
+
},
|
|
60
|
+
market: {
|
|
61
|
+
type: "string",
|
|
62
|
+
description: "Optional market filter: crypto | us | cn | hk (default: all markets)"
|
|
63
|
+
},
|
|
64
|
+
limit: {
|
|
65
|
+
type: "number",
|
|
66
|
+
description: "Max results per market (default 10, capped at 20)"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
output: {
|
|
70
|
+
schema: { type: "string" },
|
|
71
|
+
render: (_args, value) => [{
|
|
72
|
+
type: "text",
|
|
73
|
+
text: value
|
|
74
|
+
}]
|
|
75
|
+
},
|
|
76
|
+
async execute(raw) {
|
|
77
|
+
const args = raw ?? {};
|
|
78
|
+
const query = typeof args.query === "string" ? args.query.trim().toLowerCase() : "";
|
|
79
|
+
if (!query) throw new Error("instruments_search: query is required");
|
|
80
|
+
const markets = typeof args.market === "string" && MARKETS.includes(args.market) ? [args.market] : [...MARKETS];
|
|
81
|
+
const perMarketCap = typeof args.limit === "number" && Number.isFinite(args.limit) && args.limit > 0 ? Math.min(Math.trunc(args.limit), 20) : 10;
|
|
82
|
+
const results = [];
|
|
83
|
+
for (const market of markets) {
|
|
84
|
+
const active = services.registry?.active(market);
|
|
85
|
+
const seen = /* @__PURE__ */ new Set();
|
|
86
|
+
if (active !== void 0 && typeof active.service.listInstruments === "function") try {
|
|
87
|
+
const list = await active.service.listInstruments();
|
|
88
|
+
for (const item of list ?? []) {
|
|
89
|
+
const symbol = String(item.symbol ?? "");
|
|
90
|
+
const name = item.name;
|
|
91
|
+
if (!symbol || seen.has(symbol)) continue;
|
|
92
|
+
if ((symbol + " " + (name ?? "")).toLowerCase().includes(query)) {
|
|
93
|
+
seen.add(symbol);
|
|
94
|
+
results.push({
|
|
95
|
+
market,
|
|
96
|
+
symbol,
|
|
97
|
+
...name ? { name } : {},
|
|
98
|
+
source: "dynamic"
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (seen.size >= perMarketCap) break;
|
|
102
|
+
}
|
|
103
|
+
} catch {}
|
|
104
|
+
for (const entry of SYMBOL_CATALOG[market] ?? []) {
|
|
105
|
+
if (seen.size >= perMarketCap) break;
|
|
106
|
+
if (seen.has(entry.symbol)) continue;
|
|
107
|
+
if ((entry.symbol + " " + entry.name).toLowerCase().includes(query)) {
|
|
108
|
+
seen.add(entry.symbol);
|
|
109
|
+
results.push({
|
|
110
|
+
market,
|
|
111
|
+
symbol: entry.symbol,
|
|
112
|
+
name: entry.name,
|
|
113
|
+
source: "catalog"
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return JSON.stringify({
|
|
119
|
+
ok: true,
|
|
120
|
+
query,
|
|
121
|
+
total: results.length,
|
|
122
|
+
results
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
export { MARKETS, createInstrumentsSearchTool, createRoutingGetTool };
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dshtrading/router",
|
|
3
|
+
"description": "Market routing for dsh-trading: user settings namespace dshtrading (markets.<market>.provider) + tradingMarketRouter service consulted by connectors at apply time",
|
|
4
|
+
"version": "0.1.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
|
+
"./catalog": {
|
|
14
|
+
"types": "./lib/catalog.d.ts",
|
|
15
|
+
"default": "./lib/catalog.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.1.0"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@deepseek-ai/cordis": ">=4.0.0",
|
|
28
|
+
"@deepseek-ai/dsh-settings": ">=0.1.2-alpha.1",
|
|
29
|
+
"@deepseek-ai/dsh-tools": ">=0.1.2-alpha.1",
|
|
30
|
+
"@deepseek-ai/schemastery": ">=3.18.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"tsdown": "^0.22.0",
|
|
34
|
+
"vitest": "^3.0.0"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsdown",
|
|
38
|
+
"test": "vitest run"
|
|
39
|
+
}
|
|
40
|
+
}
|