@dshtrading/watchlist 0.1.5 → 0.1.6

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.
@@ -1,6 +1,7 @@
1
- import { SelectionStore, WatchlistStore } from "./index.js";
1
+ import { SelectionStore, WatchlistGroupsStore, WatchlistStore } from "./index.js";
2
2
  //#region src/file-store.d.ts
3
3
  declare function createFileWatchlistStore(filePath: string): WatchlistStore;
4
+ declare function createFileWatchlistGroupsStore(filePath: string): WatchlistGroupsStore;
4
5
  declare function createFileSelectionStore(filePath: string): SelectionStore;
5
6
  //#endregion
6
- export { createFileSelectionStore, createFileWatchlistStore };
7
+ export { createFileSelectionStore, createFileWatchlistGroupsStore, createFileWatchlistStore };
package/lib/file-store.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { WATCHLIST_SEEDS } from "./seeds.js";
2
+ import { newWatchlistGroupId, normalizeWatchlistRow } from "./index.js";
2
3
  import { dirname } from "node:path";
3
4
  import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
4
5
  //#region src/file-store.ts
@@ -77,7 +78,7 @@ function createFileWatchlistStore(filePath) {
77
78
  if (rows.some((row) => row.symbol === instrument.symbol)) return false;
78
79
  const next = {
79
80
  ...map,
80
- [market]: [...rows, { ...instrument }]
81
+ [market]: [...rows, normalizeWatchlistRow(instrument)]
81
82
  };
82
83
  cache = next;
83
84
  await writeNow(next);
@@ -99,6 +100,134 @@ function createFileWatchlistStore(filePath) {
99
100
  await writeNow(next);
100
101
  return true;
101
102
  });
103
+ },
104
+ async assignGroup(market, symbol, groupId, member) {
105
+ return enqueue(async () => {
106
+ const map = await load();
107
+ const rows = map[market];
108
+ if (!Array.isArray(rows)) return false;
109
+ const index = rows.findIndex((row) => row.symbol === symbol);
110
+ if (index < 0) return false;
111
+ const base = rows[index];
112
+ if (base === void 0) return false;
113
+ const current = Array.isArray(base.groups) ? base.groups : [];
114
+ if (member === current.includes(groupId)) return false;
115
+ const nextGroups = member ? [...current, groupId] : current.filter((id) => id !== groupId);
116
+ const nextRows = [...rows];
117
+ nextRows[index] = normalizeWatchlistRow({
118
+ market: base.market,
119
+ symbol: base.symbol,
120
+ ...base.name !== void 0 ? { name: base.name } : {},
121
+ ...nextGroups.length > 0 ? { groups: nextGroups } : {}
122
+ });
123
+ const next = {
124
+ ...map,
125
+ [market]: nextRows
126
+ };
127
+ cache = next;
128
+ await writeNow(next);
129
+ return true;
130
+ });
131
+ },
132
+ async stripGroup(groupId) {
133
+ return enqueue(async () => {
134
+ const map = await load();
135
+ let cleaned = 0;
136
+ const next = {};
137
+ for (const [market, rows] of Object.entries(map)) {
138
+ if (!Array.isArray(rows)) continue;
139
+ let changed = false;
140
+ const nextRows = rows.map((row) => {
141
+ const groups = Array.isArray(row.groups) ? row.groups : [];
142
+ if (!groups.includes(groupId)) return row;
143
+ changed = true;
144
+ cleaned++;
145
+ const rest = groups.filter((id) => id !== groupId);
146
+ return normalizeWatchlistRow({
147
+ market: row.market,
148
+ symbol: row.symbol,
149
+ ...row.name !== void 0 ? { name: row.name } : {},
150
+ ...rest.length > 0 ? { groups: rest } : {}
151
+ });
152
+ });
153
+ next[market] = changed ? nextRows : rows;
154
+ }
155
+ if (cleaned > 0) {
156
+ cache = next;
157
+ await writeNow(next);
158
+ }
159
+ return cleaned;
160
+ });
161
+ }
162
+ };
163
+ }
164
+ function createFileWatchlistGroupsStore(filePath) {
165
+ let cache = null;
166
+ let pendingWrite = Promise.resolve();
167
+ const enqueue = (fn) => {
168
+ const next = pendingWrite.then(fn, fn);
169
+ pendingWrite = next.then(() => {}, () => {});
170
+ return next;
171
+ };
172
+ async function load() {
173
+ if (cache !== null) return cache;
174
+ try {
175
+ const content = await readFile(filePath, "utf8");
176
+ const parsed = JSON.parse(content);
177
+ cache = Array.isArray(parsed.groups) ? parsed.groups.filter((group) => typeof group === "object" && group !== null && typeof group.id === "string" && group.id !== "" && typeof group.name === "string") : [];
178
+ } catch (err) {
179
+ if (err?.code !== "ENOENT") console.error(`[dsh-trading/watchlist] failed to read watchlist groups from ${filePath}:`, err);
180
+ cache = [];
181
+ }
182
+ return cache;
183
+ }
184
+ function writeNow(groups) {
185
+ return safeAtomicWrite(filePath, JSON.stringify({ groups }, null, 2));
186
+ }
187
+ return {
188
+ async list() {
189
+ return [...await load()];
190
+ },
191
+ async create(name) {
192
+ return enqueue(async () => {
193
+ const groups = await load();
194
+ if (groups.some((group) => group.name === name)) return { error: "duplicate" };
195
+ const group = {
196
+ id: newWatchlistGroupId(),
197
+ name,
198
+ createdAt: Date.now()
199
+ };
200
+ cache = [...groups, group];
201
+ await writeNow(cache);
202
+ return { group };
203
+ });
204
+ },
205
+ async rename(id, name) {
206
+ return enqueue(async () => {
207
+ const groups = await load();
208
+ const existing = groups.find((group) => group.id === id);
209
+ if (existing === void 0) return { error: "not-found" };
210
+ if (groups.some((group) => group.id !== id && group.name === name)) return { error: "duplicate" };
211
+ cache = groups.map((group) => group.id === id ? {
212
+ ...group,
213
+ name
214
+ } : group);
215
+ await writeNow(cache);
216
+ return { group: {
217
+ ...existing,
218
+ name
219
+ } };
220
+ });
221
+ },
222
+ async remove(id) {
223
+ return enqueue(async () => {
224
+ const groups = await load();
225
+ const next = groups.filter((group) => group.id !== id);
226
+ if (next.length === groups.length) return false;
227
+ cache = next;
228
+ await writeNow(next);
229
+ return true;
230
+ });
102
231
  }
103
232
  };
104
233
  }
@@ -139,4 +268,4 @@ function createFileSelectionStore(filePath) {
139
268
  };
140
269
  }
141
270
  //#endregion
142
- export { createFileSelectionStore, createFileWatchlistStore };
271
+ export { createFileSelectionStore, createFileWatchlistGroupsStore, createFileWatchlistStore };
package/lib/index.d.ts CHANGED
@@ -11,10 +11,13 @@ import { WATCHLIST_SEEDS, effectiveWatchlistRows, watchlistRowSource } from "./s
11
11
  * 本模块(纯类型 + 内存 store,浏览器安全):
12
12
  * - `WatchlistsMap`:按 market 分桶的多列表行(rows 只存用户定制行;市场种子列表
13
13
  * 属客户端展示回退,不进 host store——空 market = 未定制,客户端回落种子)。
14
+ * 行可带 `groups`(自定义分组 id 多归属,issue #82);分组注册表另存
15
+ * watchlist-groups.json(`./file-store.ts` 的 groups store)。
14
16
  * - `SelectionRecord`:跨市场选中标的(watchlist_select 工具与左栏点击同源)。
15
- * - `./plugin` 子路径:file store(~/.dsh/watchlists.json、~/.dsh/selection.json,
16
- * tmp+rename 原子写)+ host 平面工具 watchlist_list/add/remove/select(全会话可见,
17
- * owner 裁决 D4;select/add/remove 后 emit tradingEvents('watchlists'|'selection'))。
17
+ * - `./plugin` 子路径:file store(~/.dsh/watchlists.json、~/.dsh/selection.json、
18
+ * ~/.dsh/watchlist-groups.json,tmp+rename 原子写)+ host 平面工具
19
+ * watchlist_list/add/remove/select(全会话可见,owner 裁决 D4;select/add/remove
20
+ * 后 emit tradingEvents('watchlists'|'selection'))。
18
21
  *
19
22
  * 词汇纪律:symbol 用市场规范形(docs/symbol-vocabulary.md),本包不做归一化
20
23
  * (写入方负责——工具参数与桥端点均原样落盘)。
@@ -27,9 +30,33 @@ interface WatchlistInstrument {
27
30
  symbol: string;
28
31
  /** 展示名(可选;工具添加可缺省,客户端以 symbol 兜底展示)。 */
29
32
  name?: string;
33
+ /**
34
+ * 所属自定义分组 id 数组(issue #82;多归属,一行可入多组;缺省/空 = 未分组)。
35
+ * 分组注册表(名称等元数据)在 ./file-store.ts 的 groups store,行上只存 id。
36
+ */
37
+ groups?: string[];
30
38
  }
31
39
  /** 多列表自选:market → 用户定制行数组(空数组/缺键 = 未定制)。 */
32
40
  type WatchlistsMap = Record<string, WatchlistInstrument[]>;
41
+ /** 自定义分组注册表行(issue #82;成员关系在标的行的 groups 字段,此处只存元数据)。 */
42
+ interface WatchlistGroup {
43
+ id: string;
44
+ /** 展示名(桥面保证 trim 非空;同名单向拒绝)。 */
45
+ name: string;
46
+ createdAt: number;
47
+ }
48
+ /** 分组写操作结果(桥面转业务 ok:false + reason;'duplicate' = 同名已存在,'not-found' = id 不存在)。 */
49
+ interface WatchlistGroupWriteResult {
50
+ group?: WatchlistGroup;
51
+ error?: 'duplicate' | 'not-found';
52
+ }
53
+ /** 自定义分组注册表 store(元数据;成员关系的读写走 WatchlistStore.assignGroup/stripGroup)。 */
54
+ interface WatchlistGroupsStore {
55
+ list(): Promise<WatchlistGroup[]>;
56
+ create(name: string): Promise<WatchlistGroupWriteResult>;
57
+ rename(id: string, name: string): Promise<WatchlistGroupWriteResult>;
58
+ remove(id: string): Promise<boolean>;
59
+ }
33
60
  /** 选中标的记录(中栏切图的 SSOT)。 */
34
61
  interface SelectionRecord {
35
62
  instrument: WatchlistInstrument | null;
@@ -43,14 +70,24 @@ interface WatchlistStore {
43
70
  add(market: string, instrument: WatchlistInstrument): Promise<boolean>;
44
71
  /** 移除一行;返回是否 existed。 */
45
72
  remove(market: string, symbol: string): Promise<boolean>;
73
+ /** 加入/移出分组(issue #82):行缺席或归属无变化返回 false(读改写入队串行化)。 */
74
+ assignGroup(market: string, symbol: string, groupId: string, member: boolean): Promise<boolean>;
75
+ /** 清掉所有行上的某分组归属(删分组时调用);返回清洗行数,无变化不落盘。 */
76
+ stripGroup(groupId: string): Promise<number>;
46
77
  }
47
78
  interface SelectionStore {
48
79
  get(): Promise<SelectionRecord>;
49
80
  set(record: SelectionRecord): Promise<void>;
50
81
  }
82
+ /** 行规范化副本:groups 空数组不落键,保持落盘/输出形状干净。 */
83
+ declare function normalizeWatchlistRow(row: WatchlistInstrument): WatchlistInstrument;
84
+ /** 新分组 id(时间戳 base36 + 随机尾,进程内唯一即可——注册表单文件单写者)。 */
85
+ declare function newWatchlistGroupId(): string;
51
86
  /** 内存版自选 store(单测用)。 */
52
87
  declare function createMemoryWatchlistStore(initial?: WatchlistsMap): WatchlistStore;
88
+ /** 内存版分组注册表 store(单测用)。同名拒绝(trim 后精确匹配);调用方负责非空校验。 */
89
+ declare function createMemoryWatchlistGroupsStore(initial?: WatchlistGroup[]): WatchlistGroupsStore;
53
90
  /** 内存版选中 store(单测用)。 */
54
91
  declare function createMemorySelectionStore(initial?: SelectionRecord): SelectionStore;
55
92
  //#endregion
56
- export { SelectionRecord, SelectionStore, WATCHLIST_SEEDS, WatchlistInstrument, WatchlistStore, WatchlistsMap, createMemorySelectionStore, createMemoryWatchlistStore, effectiveWatchlistRows, watchlistRowSource };
93
+ export { SelectionRecord, SelectionStore, WATCHLIST_SEEDS, WatchlistGroup, WatchlistGroupWriteResult, WatchlistGroupsStore, WatchlistInstrument, WatchlistStore, WatchlistsMap, createMemorySelectionStore, createMemoryWatchlistGroupsStore, createMemoryWatchlistStore, effectiveWatchlistRows, newWatchlistGroupId, normalizeWatchlistRow, watchlistRowSource };
package/lib/index.js CHANGED
@@ -1,5 +1,19 @@
1
1
  import { WATCHLIST_SEEDS, effectiveWatchlistRows, watchlistRowSource } from "./seeds.js";
2
2
  //#region src/index.ts
3
+ /** 行规范化副本:groups 空数组不落键,保持落盘/输出形状干净。 */
4
+ function normalizeWatchlistRow(row) {
5
+ const groups = Array.isArray(row.groups) ? [...new Set(row.groups.filter((item) => typeof item === "string" && item))] : [];
6
+ return {
7
+ market: row.market,
8
+ symbol: row.symbol,
9
+ ...row.name !== void 0 ? { name: row.name } : {},
10
+ ...groups.length > 0 ? { groups } : {}
11
+ };
12
+ }
13
+ /** 新分组 id(时间戳 base36 + 随机尾,进程内唯一即可——注册表单文件单写者)。 */
14
+ function newWatchlistGroupId() {
15
+ return `g_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
16
+ }
3
17
  /** 内存版自选 store(单测用)。 */
4
18
  function createMemoryWatchlistStore(initial = {}) {
5
19
  let map = { ...initial };
@@ -15,7 +29,7 @@ function createMemoryWatchlistStore(initial = {}) {
15
29
  if (rows.some((row) => row.symbol === instrument.symbol)) return false;
16
30
  map = {
17
31
  ...map,
18
- [market]: [...rows, { ...instrument }]
32
+ [market]: [...rows, normalizeWatchlistRow(instrument)]
19
33
  };
20
34
  return true;
21
35
  },
@@ -29,6 +43,91 @@ function createMemoryWatchlistStore(initial = {}) {
29
43
  [market]: next
30
44
  };
31
45
  return true;
46
+ },
47
+ async assignGroup(market, symbol, groupId, member) {
48
+ const rows = map[market];
49
+ if (!Array.isArray(rows)) return false;
50
+ const index = rows.findIndex((row) => row.symbol === symbol);
51
+ if (index < 0) return false;
52
+ const base = rows[index];
53
+ if (base === void 0) return false;
54
+ const current = Array.isArray(base.groups) ? base.groups : [];
55
+ if (member === current.includes(groupId)) return false;
56
+ const nextGroups = member ? [...current, groupId] : current.filter((id) => id !== groupId);
57
+ const nextRows = [...rows];
58
+ nextRows[index] = normalizeWatchlistRow({
59
+ market: base.market,
60
+ symbol: base.symbol,
61
+ ...base.name !== void 0 ? { name: base.name } : {},
62
+ ...nextGroups.length > 0 ? { groups: nextGroups } : {}
63
+ });
64
+ map = {
65
+ ...map,
66
+ [market]: nextRows
67
+ };
68
+ return true;
69
+ },
70
+ async stripGroup(groupId) {
71
+ let cleaned = 0;
72
+ const next = {};
73
+ for (const [market, rows] of Object.entries(map)) {
74
+ if (!Array.isArray(rows)) continue;
75
+ let changed = false;
76
+ const nextRows = rows.map((row) => {
77
+ const groups = Array.isArray(row.groups) ? row.groups : [];
78
+ if (!groups.includes(groupId)) return row;
79
+ changed = true;
80
+ cleaned++;
81
+ const rest = groups.filter((id) => id !== groupId);
82
+ return normalizeWatchlistRow({
83
+ market: row.market,
84
+ symbol: row.symbol,
85
+ ...row.name !== void 0 ? { name: row.name } : {},
86
+ ...rest.length > 0 ? { groups: rest } : {}
87
+ });
88
+ });
89
+ next[market] = changed ? nextRows : rows;
90
+ }
91
+ if (cleaned > 0) map = next;
92
+ return cleaned;
93
+ }
94
+ };
95
+ }
96
+ /** 内存版分组注册表 store(单测用)。同名拒绝(trim 后精确匹配);调用方负责非空校验。 */
97
+ function createMemoryWatchlistGroupsStore(initial = []) {
98
+ let groups = [...initial];
99
+ return {
100
+ async list() {
101
+ return [...groups];
102
+ },
103
+ async create(name) {
104
+ if (groups.some((group) => group.name === name)) return { error: "duplicate" };
105
+ const group = {
106
+ id: newWatchlistGroupId(),
107
+ name,
108
+ createdAt: Date.now()
109
+ };
110
+ groups = [...groups, group];
111
+ return { group };
112
+ },
113
+ async rename(id, name) {
114
+ const existing = groups.find((group) => group.id === id);
115
+ if (existing === void 0) return { error: "not-found" };
116
+ if (groups.some((group) => group.id !== id && group.name === name)) return { error: "duplicate" };
117
+ groups = groups.map((group) => group.id === id ? {
118
+ ...group,
119
+ name
120
+ } : group);
121
+ return { group: {
122
+ ...existing,
123
+ name
124
+ } };
125
+ },
126
+ async remove(id) {
127
+ const next = groups.filter((group) => group.id !== id);
128
+ if (next.length === groups.length) return false;
129
+ groups = next;
130
+ return true;
32
131
  }
33
132
  };
34
133
  }
@@ -48,4 +147,4 @@ function createMemorySelectionStore(initial = { instrument: null }) {
48
147
  };
49
148
  }
50
149
  //#endregion
51
- export { WATCHLIST_SEEDS, createMemorySelectionStore, createMemoryWatchlistStore, effectiveWatchlistRows, watchlistRowSource };
150
+ export { WATCHLIST_SEEDS, createMemorySelectionStore, createMemoryWatchlistGroupsStore, createMemoryWatchlistStore, effectiveWatchlistRows, newWatchlistGroupId, normalizeWatchlistRow, watchlistRowSource };
package/lib/plugin.d.ts CHANGED
@@ -1,14 +1,16 @@
1
1
  import { SelectionStore, WatchlistStore, createMemorySelectionStore, createMemoryWatchlistStore } from "./index.js";
2
- import { createFileSelectionStore, createFileWatchlistStore } from "./file-store.js";
3
- import { Context } from "@deepseek-ai/cordis";
2
+ import { createFileSelectionStore, createFileWatchlistGroupsStore, createFileWatchlistStore } from "./file-store.js";
3
+ import { Context, Service } from "@deepseek-ai/cordis";
4
4
  //#region src/plugin.d.ts
5
5
  /** Cordis 插件名 = patch 行 id(TEMPLATES §8),市场无关共享行命名空间。 */
6
6
  declare const name = "dsh-trading-watchlist";
7
7
  /** 本插件不硬依赖任何服务;tools 经 ctx.inject 声明。 */
8
8
  declare const inject: string[];
9
- /** 默认存储路径。 */
9
+ /** 默认存储路径:$DSH_HOME/watchlists.json(缺省 ~/.dsh)。 */
10
10
  declare function defaultWatchlistStorePath(): string;
11
11
  declare function defaultSelectionStorePath(): string;
12
+ /** 默认分组注册表路径:$DSH_HOME/watchlist-groups.json(issue #82)。 */
13
+ declare function defaultWatchlistGroupsStorePath(): string;
12
14
  /** tradingEvents 的最小发布面(鸭式;总线缺席时静默降级)。 */
13
15
  interface TradingEventsPublisher {
14
16
  emit(store: 'watchlists' | 'selection'): void;
@@ -28,7 +30,24 @@ interface WatchlistPluginDeps {
28
30
  selection: SelectionStore;
29
31
  }
30
32
  declare function registerWatchlistTools(ctx: Context, deps: WatchlistPluginDeps): void;
33
+ /** SDK 服务键:自选/选中 store 单实例(桥与工具共享同一缓存;2026-09-08 审查 H1 收口)。 */
34
+ declare const TRADING_WATCHLIST_KEY = "tradingWatchlist";
35
+ /**
36
+ * 自选 + 选中 store 服务(单实例共享点,strategies/indicators 同款 provide 模式)。
37
+ *
38
+ * 2026-09-08 审查实证:桥(client-ui-trading node 半)此前自建第二个 file store,
39
+ * 与工具侧实例各持整表缓存、各自整表回写 → 两侧写互相覆盖(agent `watchlist_add`
40
+ * 抹掉 GUI 刚写入的行与全部分组归属)。服务缺席(老部署)时桥仍回退自建实例。
41
+ */
42
+ declare class WatchlistStoreService extends Service {
43
+ readonly store: WatchlistStore;
44
+ readonly selection: SelectionStore;
45
+ constructor(ctx: Context, deps: {
46
+ watchlists: WatchlistStore;
47
+ selection: SelectionStore;
48
+ }, serviceName?: string);
49
+ }
31
50
  /** Host plugin body:file store provide + 4 工具注册(host 平面,全会话可见)。 */
32
51
  declare function apply(ctx: Context): void;
33
52
  //#endregion
34
- export { TradingEventsPublisher, WatchlistPluginDeps, WatchlistToolDeps, apply, createFileSelectionStore, createFileWatchlistStore, createMemorySelectionStore, createMemoryWatchlistStore, createWatchlistAddTool, createWatchlistListTool, createWatchlistRemoveTool, createWatchlistSelectTool, defaultSelectionStorePath, defaultWatchlistStorePath, inject, name, registerWatchlistTools };
53
+ export { TRADING_WATCHLIST_KEY, TradingEventsPublisher, WatchlistPluginDeps, WatchlistStoreService, WatchlistToolDeps, apply, createFileSelectionStore, createFileWatchlistGroupsStore, createFileWatchlistStore, createMemorySelectionStore, createMemoryWatchlistStore, createWatchlistAddTool, createWatchlistListTool, createWatchlistRemoveTool, createWatchlistSelectTool, defaultSelectionStorePath, defaultWatchlistGroupsStorePath, defaultWatchlistStorePath, inject, name, registerWatchlistTools };
package/lib/plugin.js CHANGED
@@ -1,20 +1,25 @@
1
1
  import { WATCHLIST_SEEDS, effectiveWatchlistRows, watchlistRowSource } from "./seeds.js";
2
2
  import { createMemorySelectionStore, createMemoryWatchlistStore } from "./index.js";
3
- import { createFileSelectionStore, createFileWatchlistStore } from "./file-store.js";
3
+ import { createFileSelectionStore, createFileWatchlistGroupsStore, createFileWatchlistStore } from "./file-store.js";
4
+ import { Service } from "@deepseek-ai/cordis";
4
5
  import { defineTool } from "@deepseek-ai/dsh-tools";
5
- import os from "node:os";
6
6
  import path from "node:path";
7
+ import { dshHomeDir } from "@dshtrading/dsh-home";
7
8
  //#region src/plugin.ts
8
9
  /** Cordis 插件名 = patch 行 id(TEMPLATES §8),市场无关共享行命名空间。 */
9
10
  const name = "dsh-trading-watchlist";
10
11
  /** 本插件不硬依赖任何服务;tools 经 ctx.inject 声明。 */
11
12
  const inject = [];
12
- /** 默认存储路径。 */
13
+ /** 默认存储路径:$DSH_HOME/watchlists.json(缺省 ~/.dsh)。 */
13
14
  function defaultWatchlistStorePath() {
14
- return path.join(os.homedir(), ".dsh", "watchlists.json");
15
+ return path.join(dshHomeDir(), "watchlists.json");
15
16
  }
16
17
  function defaultSelectionStorePath() {
17
- return path.join(os.homedir(), ".dsh", "selection.json");
18
+ return path.join(dshHomeDir(), "selection.json");
19
+ }
20
+ /** 默认分组注册表路径:$DSH_HOME/watchlist-groups.json(issue #82)。 */
21
+ function defaultWatchlistGroupsStorePath() {
22
+ return path.join(dshHomeDir(), "watchlist-groups.json");
18
23
  }
19
24
  function eventsOf(ctx) {
20
25
  return ctx.get?.("tradingEvents", false);
@@ -27,7 +32,7 @@ function parseInstrumentArgs(raw) {
27
32
  return {
28
33
  market,
29
34
  symbol,
30
- name
35
+ ...name !== void 0 ? { name } : {}
31
36
  };
32
37
  }
33
38
  function createWatchlistListTool(deps) {
@@ -196,12 +201,36 @@ function registerWatchlistTools(ctx, deps) {
196
201
  shared.register(createWatchlistSelectTool(hookDeps));
197
202
  });
198
203
  }
204
+ /** SDK 服务键:自选/选中 store 单实例(桥与工具共享同一缓存;2026-09-08 审查 H1 收口)。 */
205
+ const TRADING_WATCHLIST_KEY = "tradingWatchlist";
206
+ /**
207
+ * 自选 + 选中 store 服务(单实例共享点,strategies/indicators 同款 provide 模式)。
208
+ *
209
+ * 2026-09-08 审查实证:桥(client-ui-trading node 半)此前自建第二个 file store,
210
+ * 与工具侧实例各持整表缓存、各自整表回写 → 两侧写互相覆盖(agent `watchlist_add`
211
+ * 抹掉 GUI 刚写入的行与全部分组归属)。服务缺席(老部署)时桥仍回退自建实例。
212
+ */
213
+ var WatchlistStoreService = class extends Service {
214
+ store;
215
+ selection;
216
+ constructor(ctx, deps, serviceName = TRADING_WATCHLIST_KEY) {
217
+ super(ctx, serviceName);
218
+ this.store = deps.watchlists;
219
+ this.selection = deps.selection;
220
+ }
221
+ };
199
222
  /** Host plugin body:file store provide + 4 工具注册(host 平面,全会话可见)。 */
200
223
  function apply(ctx) {
224
+ const watchlists = createFileWatchlistStore(defaultWatchlistStorePath());
225
+ const selection = createFileSelectionStore(defaultSelectionStorePath());
226
+ new WatchlistStoreService(ctx, {
227
+ watchlists,
228
+ selection
229
+ });
201
230
  registerWatchlistTools(ctx, {
202
- watchlists: createFileWatchlistStore(defaultWatchlistStorePath()),
203
- selection: createFileSelectionStore(defaultSelectionStorePath())
231
+ watchlists,
232
+ selection
204
233
  });
205
234
  }
206
235
  //#endregion
207
- export { apply, createFileSelectionStore, createFileWatchlistStore, createMemorySelectionStore, createMemoryWatchlistStore, createWatchlistAddTool, createWatchlistListTool, createWatchlistRemoveTool, createWatchlistSelectTool, defaultSelectionStorePath, defaultWatchlistStorePath, inject, name, registerWatchlistTools };
236
+ export { TRADING_WATCHLIST_KEY, WatchlistStoreService, apply, createFileSelectionStore, createFileWatchlistGroupsStore, createFileWatchlistStore, createMemorySelectionStore, createMemoryWatchlistStore, createWatchlistAddTool, createWatchlistListTool, createWatchlistRemoveTool, createWatchlistSelectTool, defaultSelectionStorePath, defaultWatchlistGroupsStorePath, defaultWatchlistStorePath, inject, name, registerWatchlistTools };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dshtrading/watchlist",
3
3
  "description": "Host-side watchlist + selection stores for dsh-trading: multi-list file store (~/.dsh/watchlists.json), selection store (~/.dsh/selection.json), and cross-market watchlist_* tools (watchlist_list/add/remove/select) on the host plane",
4
- "version": "0.1.5",
4
+ "version": "0.1.6",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "./lib/index.d.ts",
@@ -20,7 +20,9 @@
20
20
  "lib"
21
21
  ],
22
22
  "license": "PolyForm-Noncommercial-1.0.0",
23
- "dependencies": {},
23
+ "dependencies": {
24
+ "@dshtrading/dsh-home": "^0.1.6"
25
+ },
24
26
  "peerDependencies": {
25
27
  "@deepseek-ai/cordis": ">=4.0.0",
26
28
  "@deepseek-ai/dsh-tools": ">=0.1.2-rc.1"