@dshtrading/client-ui-trading 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/lib/index.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { Context } from "@deepseek-ai/cordis";
2
+ //#region src/index.d.ts
3
+ /** 本插件不硬依赖任何服务(headless 宿主零要求);web 面依赖在 apply 内声明。 */
4
+ declare const inject: readonly string[];
5
+ /**
6
+ * Host plugin body:注册 /dshtrading/api 路由(web 宿主)或静默挂起(headless)。
7
+ * @param ctx - Host cordis context(bundle loader entry)。
8
+ */
9
+ declare function apply(ctx: Context): void;
10
+ //#endregion
11
+ export { apply, inject };
package/lib/index.js ADDED
@@ -0,0 +1,141 @@
1
+ import { BridgeProtocolError, MARKET_SERVICE_KEYS, TradingBridge, createBridgeHost, dispatchBridgeRequest } from "./bridge.js";
2
+ import { attachEventStream } from "./sse.js";
3
+ import { createFileCustomIndicatorStore } from "@dshtrading/indicators/plugin";
4
+ import { createFileKnowledgeCardStore } from "@dshtrading/knowledge/plugin";
5
+ import { createFileCustomStrategyStore } from "@dshtrading/strategies/plugin";
6
+ import { createFileSelectionStore, createFileWatchlistStore } from "@dshtrading/watchlist/plugin";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+ //#region src/index.ts
10
+ /** 本插件不硬依赖任何服务(headless 宿主零要求);web 面依赖在 apply 内声明。 */
11
+ const inject = [];
12
+ /** 发送 JSON 响应(禁缓存:行情是易变数据,代理层也不许中间层缓存)。 */
13
+ function sendJson(res, status, payload) {
14
+ const body = JSON.stringify(payload);
15
+ res.writeHead(status, {
16
+ "content-type": "application/json; charset=utf-8",
17
+ "cache-control": "no-store"
18
+ });
19
+ res.end(body);
20
+ }
21
+ /**
22
+ * Host plugin body:注册 /dshtrading/api 路由(web 宿主)或静默挂起(headless)。
23
+ * @param ctx - Host cordis context(bundle loader entry)。
24
+ */
25
+ function apply(ctx) {
26
+ const serviceGet = (key) => ctx.get?.(key, false);
27
+ const customIndicatorsStore = serviceGet("tradingCustomIndicators")?.store ?? createFileCustomIndicatorStore(path.join(os.homedir(), ".dsh", "indicators", "custom.json"));
28
+ const knowledgeStore = serviceGet("tradingKnowledgeCards")?.store ?? createFileKnowledgeCardStore(path.join(os.homedir(), ".dsh", "knowledge", "cards.json"));
29
+ const strategyStorePath = path.join(os.homedir(), ".dsh", "strategies", "custom.json");
30
+ const strategyStore = createFileCustomStrategyStore(strategyStorePath);
31
+ const watchlistStorePath = path.join(os.homedir(), ".dsh", "watchlists.json");
32
+ const watchlistStore = createFileWatchlistStore(watchlistStorePath);
33
+ const selectionStorePath = path.join(os.homedir(), ".dsh", "selection.json");
34
+ const selectionStore = createFileSelectionStore(selectionStorePath);
35
+ const eventsOf = () => ctx.get?.("tradingEvents", false);
36
+ ctx.inject(["webServer", "connection"], (webCtx) => {
37
+ const webServer = webCtx.get("webServer");
38
+ const connection = webCtx.get("connection");
39
+ if (webServer === void 0 || connection === void 0) return;
40
+ const host = createBridgeHost({
41
+ registry: webCtx.get("tradingMarketDataRegistry", false),
42
+ tradeRegistry: webCtx.get("tradingTradeRegistry", false),
43
+ router: webCtx.get("tradingMarketRouter", false),
44
+ legacy: (market) => webCtx.get(MARKET_SERVICE_KEYS[market]),
45
+ customIndicatorsStore,
46
+ knowledgeStore,
47
+ strategyStore,
48
+ watchlistStore,
49
+ selectionStore,
50
+ newsRegistry: webCtx.get("tradingNewsRegistry", false),
51
+ newsKey: () => {
52
+ return webCtx.get("tradingMarketRouter", false)?.newsKey?.();
53
+ }
54
+ });
55
+ const bridge = new TradingBridge(host);
56
+ const route = {
57
+ kind: "prefix",
58
+ path: "/dshtrading/api",
59
+ handler: async (req, res) => {
60
+ const rejection = connection.requestRejection(req);
61
+ if (rejection !== void 0) {
62
+ res.writeHead(rejection);
63
+ res.end(rejection === 401 ? "unauthorized" : "forbidden");
64
+ return;
65
+ }
66
+ try {
67
+ const url = new URL(req.url ?? "/", "http://dsh.local");
68
+ const mount = "/dshtrading/api";
69
+ const raw = url.pathname;
70
+ const sub = raw === mount || raw.startsWith(`${mount}/`) ? raw.slice(15) || "/" : raw;
71
+ if (req.method === "GET" && sub === "/events") {
72
+ const events = eventsOf();
73
+ if (events === void 0) {
74
+ sendJson(res, 503, {
75
+ ok: false,
76
+ code: "TRADING_EVENTS_UNAVAILABLE",
77
+ message: "tradingEvents service is not mounted"
78
+ });
79
+ return;
80
+ }
81
+ attachEventStream(res, events);
82
+ return;
83
+ }
84
+ let body;
85
+ if (req.method === "PUT" || req.method === "POST") body = await readJsonBody(req);
86
+ const { status, payload } = await dispatchBridgeRequest(bridge, req.method ?? "GET", sub, url.searchParams, body);
87
+ if (status === 200 && payload?.ok === true) {
88
+ if (req.method === "DELETE" && sub === "/indicators/custom") eventsOf()?.emit("indicators");
89
+ if (req.method === "DELETE" && sub === "/strategies/custom") eventsOf()?.emit("strategies");
90
+ if ((req.method === "PUT" || req.method === "POST" || req.method === "DELETE") && (sub === "/watchlists" || sub === "/watchlists/import")) eventsOf()?.emit("watchlists");
91
+ if (req.method === "PUT" && sub === "/selection") eventsOf()?.emit("selection");
92
+ }
93
+ sendJson(res, status, payload);
94
+ } catch (error) {
95
+ if (error instanceof BridgeProtocolError) {
96
+ sendJson(res, error.status, {
97
+ ok: false,
98
+ code: "TRADING_PROTOCOL",
99
+ message: error.message
100
+ });
101
+ return;
102
+ }
103
+ sendJson(res, 200, {
104
+ ok: false,
105
+ ...errorPayloadOf(error)
106
+ });
107
+ }
108
+ }
109
+ };
110
+ ctx.effect(() => webServer.register(route), "dsh-trading-client-ui-trading: /dshtrading/api route");
111
+ });
112
+ }
113
+ /** JSON body 读取(PUT/POST 用;1MB 封顶,非法 JSON → 400 协议错误)。 */
114
+ async function readJsonBody(req) {
115
+ const chunks = [];
116
+ let total = 0;
117
+ for await (const chunk of req) {
118
+ total += chunk.length;
119
+ if (total > 1048576) throw new BridgeProtocolError(413, "request body too large (1MB cap)");
120
+ chunks.push(chunk);
121
+ }
122
+ const text = Buffer.concat(chunks).toString("utf8").trim();
123
+ if (!text) return {};
124
+ try {
125
+ return JSON.parse(text);
126
+ } catch {
127
+ throw new BridgeProtocolError(400, "request body must be valid JSON");
128
+ }
129
+ }
130
+ function errorPayloadOf(error) {
131
+ if (error instanceof Error) return {
132
+ code: typeof error.code === "string" ? error.code : "TRADING_UNKNOWN",
133
+ message: error.message
134
+ };
135
+ return {
136
+ code: "TRADING_UNKNOWN",
137
+ message: String(error)
138
+ };
139
+ }
140
+ //#endregion
141
+ export { apply, inject };
package/lib/sse.js ADDED
@@ -0,0 +1,30 @@
1
+ //#region src/sse.ts
2
+ /** 心跳间隔(issue 规格:15s)。 */
3
+ const SSE_HEARTBEAT_MS = 15e3;
4
+ /** 挂载 SSE 流:写响应头 + 订阅扇出 + 心跳;返回清理函数(res.close 时自动调用)。 */
5
+ function attachEventStream(res, events) {
6
+ res.writeHead(200, {
7
+ "content-type": "text/event-stream; charset=utf-8",
8
+ "cache-control": "no-store",
9
+ "x-accel-buffering": "no"
10
+ });
11
+ res.write(": connected\n\n");
12
+ const unsubscribe = events.subscribe((event) => {
13
+ res.write(`event: store.changed\ndata: ${JSON.stringify(event)}\n\n`);
14
+ });
15
+ const heartbeat = setInterval(() => {
16
+ res.write(": heartbeat\n\n");
17
+ }, SSE_HEARTBEAT_MS);
18
+ let cleaned = false;
19
+ const cleanup = () => {
20
+ if (cleaned) return;
21
+ cleaned = true;
22
+ clearInterval(heartbeat);
23
+ unsubscribe();
24
+ };
25
+ res.once("close", cleanup);
26
+ res.once("error", cleanup);
27
+ return cleanup;
28
+ }
29
+ //#endregion
30
+ export { SSE_HEARTBEAT_MS, attachEventStream };
package/package.json ADDED
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "@dshtrading/client-ui-trading",
3
+ "description": "Professional three-column trading terminal shell for the dsh web client: market/watchlist sidebar (shell.overlay), Lightweight Charts v5 quote stage view (MiddleStage), hero-fused session history (shadows sidebar.workspaces), session rail (fold/new/settings), and the /dshtrading/api market-data HTTP bridge on the node half.",
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
+ "./client": {
14
+ "types": "./lib/types/client/index.d.ts",
15
+ "default": "./lib/client.js"
16
+ },
17
+ "./locales": {
18
+ "types": "./lib/client/locales.d.ts",
19
+ "default": "./lib/client/locales.js"
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+ "files": [
24
+ "lib"
25
+ ],
26
+ "license": "PolyForm-Noncommercial-1.0.0",
27
+ "dependencies": {
28
+ "lightweight-charts": "^5.2.1",
29
+ "@dshtrading/eventbus": "^0.1.0",
30
+ "@dshtrading/indicators": "^0.1.0",
31
+ "@dshtrading/api": "^0.1.0",
32
+ "@dshtrading/knowledge": "^0.1.0",
33
+ "@dshtrading/strategies": "^0.1.0",
34
+ "@dshtrading/router": "^0.1.0",
35
+ "@dshtrading/watchlist": "^0.1.0",
36
+ "@dshtrading/kit-cn": "^0.1.0",
37
+ "@dshtrading/kit-us": "^0.1.0",
38
+ "@dshtrading/kit-crypto": "^0.1.0",
39
+ "@dshtrading/kit-hk": "^0.1.0",
40
+ "@dshtrading/client-ui-strategies": "^0.1.0",
41
+ "@dshtrading/client-ui-knowledge": "^0.1.0"
42
+ },
43
+ "peerDependencies": {
44
+ "@deepseek-ai/cordis": ">=4.0.0",
45
+ "@deepseek-ai/dsh-api-session-controller": ">=0.1.2-alpha.1",
46
+ "@deepseek-ai/dsh-client-locale": ">=0.1.2-alpha.1",
47
+ "@deepseek-ai/dsh-client-ui-conversation": ">=0.1.2-alpha.1",
48
+ "@deepseek-ai/dsh-client-ui-layout": ">=0.1.2-alpha.1",
49
+ "@deepseek-ai/dsh-client-ui-sidebar": ">=0.1.2-alpha.1",
50
+ "@deepseek-ai/dsh-client-ui-slots": ">=0.1.2-alpha.1",
51
+ "@deepseek-ai/dsh-client-ui-workspace": ">=0.1.2-alpha.1",
52
+ "@types/react": "~18.3.1",
53
+ "react": "^18.2.0"
54
+ },
55
+ "devDependencies": {
56
+ "@types/react": "~18.3.1",
57
+ "@types/react-dom": "~18.3.7",
58
+ "lightningcss": "^1.0.0",
59
+ "react": "^18.2.0",
60
+ "tsdown": "^0.22.0",
61
+ "vitest": "^3.0.0",
62
+ "@deepseek-ai/dsh-client-ui-tool": "0.1.2-alpha.3",
63
+ "@testing-library/dom": "^10.4.1",
64
+ "@testing-library/react": "^16.3.3",
65
+ "jsdom": "^30.0.1",
66
+ "react-dom": "^18.3.1"
67
+ },
68
+ "dsh": {
69
+ "client": {
70
+ "inject": [
71
+ "@deepseek-ai/dsh-client-locale",
72
+ "@deepseek-ai/dsh-client-ui-slots",
73
+ "@deepseek-ai/dsh-api-session-controller",
74
+ "@deepseek-ai/dsh-client-ui-workspace"
75
+ ],
76
+ "platform": "web"
77
+ }
78
+ },
79
+ "scripts": {
80
+ "bundle": "tsdown && tsdown --config tsdown.client.config.mjs",
81
+ "build": "tsdown && tsdown --config tsdown.client.config.mjs",
82
+ "test": "vitest run"
83
+ }
84
+ }