@dshtrading/watchlist 0.6.3 → 0.7.1

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/file-store.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { WATCHLIST_SEEDS } from "./seeds.js";
2
- import { newWatchlistGroupId, normalizeWatchlistRow } from "./index.js";
3
- import { createRecordStoreAdapter, readStoreText } from "@dshtrading/dsh-home";
2
+ import { newWatchlistGroupId, normalizePinnedKey, normalizeWatchlistRow } from "./index.js";
3
+ import { SKIP_WRITE, createRecordStoreAdapter, readStoreText } from "@dshtrading/dsh-home";
4
4
  //#region src/file-store.ts
5
5
  /**
6
6
  * 文件持久化版自选/选中 store(Node.js 宿主侧使用,issue #32 规格)。
@@ -154,15 +154,24 @@ function createFileWatchlistGroupsStore(filePath) {
154
154
  };
155
155
  /** 读盘/解析失败的日志前缀(坏 JSON 与非 ENOENT 读失败同一条,与既有 load 口径逐字一致)。 */
156
156
  const READ_FAILED = `[dsh-trading/watchlist] failed to read watchlist groups from ${filePath}:`;
157
- /** 文本 → 分组表:容器包裹(`{ groups: [] }`)逐条过滤形状;坏 JSON 降级空表并打日志。 */
157
+ /** 文本 → 容器记录:`{ groups, pinned }` 逐条过滤形状;坏 JSON 降级空容器并打日志。 */
158
158
  function decode(text) {
159
- if (text === void 0) return [];
159
+ if (text === void 0) return {
160
+ groups: [],
161
+ pinned: []
162
+ };
160
163
  try {
161
164
  const parsed = JSON.parse(text);
162
- return Array.isArray(parsed.groups) ? parsed.groups.filter((group) => typeof group === "object" && group !== null && typeof group.id === "string" && group.id !== "" && typeof group.name === "string") : [];
165
+ return {
166
+ groups: Array.isArray(parsed.groups) ? parsed.groups.filter((group) => typeof group === "object" && group !== null && typeof group.id === "string" && group.id !== "" && typeof group.name === "string") : [],
167
+ pinned: Array.isArray(parsed.pinned) ? [...new Set(parsed.pinned.map(normalizePinnedKey).filter((key) => key !== null))] : []
168
+ };
163
169
  } catch (err) {
164
170
  console.error(READ_FAILED, err);
165
- return [];
171
+ return {
172
+ groups: [],
173
+ pinned: []
174
+ };
166
175
  }
167
176
  }
168
177
  const store = createRecordStoreAdapter({
@@ -170,24 +179,27 @@ function createFileWatchlistGroupsStore(filePath) {
170
179
  logPrefix: LOG_PREFIX,
171
180
  readFromDisk: () => readStoreText(filePath, READ_FAILED),
172
181
  decode,
173
- encode: (groups) => ({ groups })
182
+ encode: (record) => ({
183
+ groups: record.groups,
184
+ ...record.pinned.length > 0 ? { pinned: record.pinned } : {}
185
+ })
174
186
  });
175
187
  /**
176
188
  * 跨进程读改写;mutate 返回 null 表示无变化(不落盘),缓存仍取锁内新鲜值。
177
189
  * 返回 null 是分组工具的既有写法,逐字保留。
178
190
  */
179
191
  async function commit(mutate) {
180
- return store.commit((onDisk) => mutate(onDisk) ?? onDisk);
192
+ return store.commit((onDisk) => mutate(onDisk) ?? SKIP_WRITE);
181
193
  }
182
194
  return {
183
195
  async list() {
184
- return [...await store.load()];
196
+ return [...(await store.load()).groups];
185
197
  },
186
198
  async create(name) {
187
199
  let result;
188
200
  await enqueue(async () => {
189
201
  await commit((onDisk) => {
190
- if (onDisk.some((group) => group.name === name)) {
202
+ if (onDisk.groups.some((group) => group.name === name)) {
191
203
  result = { error: "duplicate" };
192
204
  return null;
193
205
  }
@@ -197,7 +209,10 @@ function createFileWatchlistGroupsStore(filePath) {
197
209
  createdAt: Date.now()
198
210
  };
199
211
  result = { group };
200
- return [...onDisk, group];
212
+ return {
213
+ ...onDisk,
214
+ groups: [...onDisk.groups, group]
215
+ };
201
216
  });
202
217
  });
203
218
  return result ?? { error: "duplicate" };
@@ -206,12 +221,12 @@ function createFileWatchlistGroupsStore(filePath) {
206
221
  let result;
207
222
  await enqueue(async () => {
208
223
  await commit((onDisk) => {
209
- const existing = onDisk.find((group) => group.id === id);
224
+ const existing = onDisk.groups.find((group) => group.id === id);
210
225
  if (existing === void 0) {
211
226
  result = { error: "not-found" };
212
227
  return null;
213
228
  }
214
- if (onDisk.some((group) => group.id !== id && group.name === name)) {
229
+ if (onDisk.groups.some((group) => group.id !== id && group.name === name)) {
215
230
  result = { error: "duplicate" };
216
231
  return null;
217
232
  }
@@ -219,10 +234,13 @@ function createFileWatchlistGroupsStore(filePath) {
219
234
  ...existing,
220
235
  name
221
236
  } };
222
- return onDisk.map((group) => group.id === id ? {
223
- ...group,
224
- name
225
- } : group);
237
+ return {
238
+ ...onDisk,
239
+ groups: onDisk.groups.map((group) => group.id === id ? {
240
+ ...group,
241
+ name
242
+ } : group)
243
+ };
226
244
  });
227
245
  });
228
246
  return result ?? { error: "not-found" };
@@ -231,13 +249,51 @@ function createFileWatchlistGroupsStore(filePath) {
231
249
  let removed = false;
232
250
  await enqueue(async () => {
233
251
  await commit((onDisk) => {
234
- const next = onDisk.filter((group) => group.id !== id);
235
- if (next.length === onDisk.length) return null;
252
+ const next = onDisk.groups.filter((group) => group.id !== id);
253
+ if (next.length === onDisk.groups.length) return null;
236
254
  removed = true;
237
- return next;
255
+ return {
256
+ ...onDisk,
257
+ groups: next
258
+ };
238
259
  });
239
260
  });
240
261
  return removed;
262
+ },
263
+ async pinned() {
264
+ return [...(await store.load()).pinned];
265
+ },
266
+ async setPinned(key, want) {
267
+ const normalized = normalizePinnedKey(key);
268
+ if (normalized === null) return false;
269
+ let changed = false;
270
+ await enqueue(async () => {
271
+ await commit((onDisk) => {
272
+ if (onDisk.pinned.includes(normalized) === want) return null;
273
+ changed = true;
274
+ return {
275
+ ...onDisk,
276
+ pinned: want ? [...onDisk.pinned, normalized] : onDisk.pinned.filter((entry) => entry !== normalized)
277
+ };
278
+ });
279
+ });
280
+ return changed;
281
+ },
282
+ async stripPinned(key) {
283
+ const normalized = normalizePinnedKey(key);
284
+ if (normalized === null) return false;
285
+ let changed = false;
286
+ await enqueue(async () => {
287
+ await commit((onDisk) => {
288
+ if (!onDisk.pinned.includes(normalized)) return null;
289
+ changed = true;
290
+ return {
291
+ ...onDisk,
292
+ pinned: onDisk.pinned.filter((entry) => entry !== normalized)
293
+ };
294
+ });
295
+ });
296
+ return changed;
241
297
  }
242
298
  };
243
299
  }
package/lib/index.d.ts CHANGED
@@ -35,13 +35,27 @@ interface WatchlistGroupWriteResult {
35
35
  group?: WatchlistGroup;
36
36
  error?: 'duplicate' | 'not-found';
37
37
  }
38
- /** 自定义分组注册表 store(元数据;成员关系的读写走 WatchlistStore.assignGroup/stripGroup)。 */
38
+ /**
39
+ * 自定义分组注册表 store(元数据;成员关系的读写走 WatchlistStore.assignGroup/stripGroup)。
40
+ * 置顶键集与注册表同一容器、同一个 file store 实例(2026-09-08 双 store 前科:
41
+ * 同路径两个实例各持整表缓存,互相覆盖)。
42
+ */
39
43
  interface WatchlistGroupsStore {
40
44
  list(): Promise<WatchlistGroup[]>;
41
45
  create(name: string): Promise<WatchlistGroupWriteResult>;
42
46
  rename(id: string, name: string): Promise<WatchlistGroupWriteResult>;
43
47
  remove(id: string): Promise<boolean>;
48
+ /** 置顶键集(`market:symbol`,见 {@link watchlistRowKey}):面板级用户偏好,各视图共用。 */
49
+ pinned(): Promise<string[]>;
50
+ /** 置顶/取消置顶;键已处于目标状态返回 false 且不落盘。 */
51
+ setPinned(key: string, pinned: boolean): Promise<boolean>;
52
+ /** 行被移除时清掉它的置顶键(防僵尸置顶:重新加回不会自动置顶)。 */
53
+ stripPinned(key: string): Promise<boolean>;
44
54
  }
55
+ /** 行键/置顶键(`market:symbol`):客户端 rowKey、桥端 pin 与删行清洗同源格式。 */
56
+ declare function watchlistRowKey(market: string, symbol: string): string;
57
+ /** 置顶键清洗:只收 `market:symbol` 形(空段/无冒号一律剔除,坏值不落盘)。 */
58
+ declare function normalizePinnedKey(raw: unknown): string | null;
45
59
  /** 选中标的记录(中栏切图的 SSOT)。 */
46
60
  interface SelectionRecord {
47
61
  instrument: WatchlistInstrument | null;
@@ -71,8 +85,8 @@ declare function newWatchlistGroupId(): string;
71
85
  /** 内存版自选 store(单测用)。 */
72
86
  declare function createMemoryWatchlistStore(initial?: WatchlistsMap): WatchlistStore;
73
87
  /** 内存版分组注册表 store(单测用)。同名拒绝(trim 后精确匹配);调用方负责非空校验。 */
74
- declare function createMemoryWatchlistGroupsStore(initial?: WatchlistGroup[]): WatchlistGroupsStore;
88
+ declare function createMemoryWatchlistGroupsStore(initial?: WatchlistGroup[], initialPinned?: string[]): WatchlistGroupsStore;
75
89
  /** 内存版选中 store(单测用)。 */
76
90
  declare function createMemorySelectionStore(initial?: SelectionRecord): SelectionStore;
77
91
  //#endregion
78
- export { SelectionRecord, SelectionStore, WATCHLIST_SEEDS, WatchlistGroup, WatchlistGroupWriteResult, WatchlistGroupsStore, WatchlistInstrument, WatchlistStore, WatchlistsMap, createMemorySelectionStore, createMemoryWatchlistGroupsStore, createMemoryWatchlistStore, effectiveWatchlistRows, newWatchlistGroupId, normalizeWatchlistRow, watchlistRowSource };
92
+ export { SelectionRecord, SelectionStore, WATCHLIST_SEEDS, WatchlistGroup, WatchlistGroupWriteResult, WatchlistGroupsStore, WatchlistInstrument, WatchlistStore, WatchlistsMap, createMemorySelectionStore, createMemoryWatchlistGroupsStore, createMemoryWatchlistStore, effectiveWatchlistRows, newWatchlistGroupId, normalizePinnedKey, normalizeWatchlistRow, watchlistRowKey, watchlistRowSource };
package/lib/index.js CHANGED
@@ -1,5 +1,17 @@
1
1
  import { WATCHLIST_SEEDS, effectiveWatchlistRows, watchlistRowSource } from "./seeds.js";
2
2
  //#region src/index.ts
3
+ /** 行键/置顶键(`market:symbol`):客户端 rowKey、桥端 pin 与删行清洗同源格式。 */
4
+ function watchlistRowKey(market, symbol) {
5
+ return `${market}:${symbol}`;
6
+ }
7
+ /** 置顶键清洗:只收 `market:symbol` 形(空段/无冒号一律剔除,坏值不落盘)。 */
8
+ function normalizePinnedKey(raw) {
9
+ if (typeof raw !== "string") return null;
10
+ const key = raw.trim();
11
+ const split = key.indexOf(":");
12
+ if (split <= 0 || split === key.length - 1) return null;
13
+ return key;
14
+ }
3
15
  /** 行规范化副本:groups 空数组不落键,保持落盘/输出形状干净;形态/资产类别可选项原样保真。 */
4
16
  function normalizeWatchlistRow(row) {
5
17
  const groups = Array.isArray(row.groups) ? [...new Set(row.groups.filter((item) => typeof item === "string" && item))] : [];
@@ -100,8 +112,9 @@ function createMemoryWatchlistStore(initial = {}) {
100
112
  };
101
113
  }
102
114
  /** 内存版分组注册表 store(单测用)。同名拒绝(trim 后精确匹配);调用方负责非空校验。 */
103
- function createMemoryWatchlistGroupsStore(initial = []) {
115
+ function createMemoryWatchlistGroupsStore(initial = [], initialPinned = []) {
104
116
  let groups = [...initial];
117
+ let pinnedKeys = [...new Set(initialPinned.map(normalizePinnedKey).filter((key) => key !== null))];
105
118
  return {
106
119
  async list() {
107
120
  return [...groups];
@@ -134,6 +147,22 @@ function createMemoryWatchlistGroupsStore(initial = []) {
134
147
  if (next.length === groups.length) return false;
135
148
  groups = next;
136
149
  return true;
150
+ },
151
+ async pinned() {
152
+ return [...pinnedKeys];
153
+ },
154
+ async setPinned(key, want) {
155
+ const normalized = normalizePinnedKey(key);
156
+ if (normalized === null) return false;
157
+ if (pinnedKeys.includes(normalized) === want) return false;
158
+ pinnedKeys = want ? [...pinnedKeys, normalized] : pinnedKeys.filter((entry) => entry !== normalized);
159
+ return true;
160
+ },
161
+ async stripPinned(key) {
162
+ const normalized = normalizePinnedKey(key);
163
+ if (normalized === null || !pinnedKeys.includes(normalized)) return false;
164
+ pinnedKeys = pinnedKeys.filter((entry) => entry !== normalized);
165
+ return true;
137
166
  }
138
167
  };
139
168
  }
@@ -153,4 +182,4 @@ function createMemorySelectionStore(initial = { instrument: null }) {
153
182
  };
154
183
  }
155
184
  //#endregion
156
- export { WATCHLIST_SEEDS, createMemorySelectionStore, createMemoryWatchlistGroupsStore, createMemoryWatchlistStore, effectiveWatchlistRows, newWatchlistGroupId, normalizeWatchlistRow, watchlistRowSource };
185
+ export { WATCHLIST_SEEDS, createMemorySelectionStore, createMemoryWatchlistGroupsStore, createMemoryWatchlistStore, effectiveWatchlistRows, newWatchlistGroupId, normalizePinnedKey, normalizeWatchlistRow, watchlistRowKey, watchlistRowSource };
package/lib/plugin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { WATCHLIST_SEEDS, effectiveWatchlistRows, watchlistRowSource } from "./seeds.js";
2
- import { createMemorySelectionStore, createMemoryWatchlistGroupsStore, createMemoryWatchlistStore } from "./index.js";
2
+ import { createMemorySelectionStore, createMemoryWatchlistGroupsStore, createMemoryWatchlistStore, watchlistRowKey } from "./index.js";
3
3
  import { createFileSelectionStore, createFileWatchlistGroupsStore, createFileWatchlistStore } from "./file-store.js";
4
4
  import { Service } from "@deepseek-ai/cordis";
5
5
  import { defineTool } from "@deepseek-ai/dsh-tools";
@@ -199,7 +199,10 @@ function createWatchlistRemoveTool(deps) {
199
199
  async execute(raw) {
200
200
  const { market, symbol } = parseInstrumentArgs(raw ?? {});
201
201
  const removed = await deps.watchlists.remove(market, symbol);
202
- if (removed) deps.onWatchlistsChanged?.();
202
+ if (removed) {
203
+ await deps.groups?.stripPinned(watchlistRowKey(market, symbol));
204
+ deps.onWatchlistsChanged?.();
205
+ }
203
206
  return {
204
207
  ok: true,
205
208
  removed,
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.6.3",
4
+ "version": "0.7.1",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "./lib/index.d.ts",
@@ -21,14 +21,15 @@
21
21
  ],
22
22
  "license": "PolyForm-Noncommercial-1.0.0",
23
23
  "dependencies": {
24
- "@dshtrading/api": "^0.6.3",
25
- "@dshtrading/dsh-home": "^0.6.3"
24
+ "@dshtrading/api": "^0.7.1",
25
+ "@dshtrading/dsh-home": "^0.7.1"
26
26
  },
27
27
  "peerDependencies": {
28
- "@deepseek-ai/cordis": ">=4.0.0",
29
- "@deepseek-ai/dsh-tools": ">=0.2.1-rc.2"
28
+ "@deepseek-ai/cordis": ">=4.1.0",
29
+ "@deepseek-ai/dsh-tools": ">=0.2.2-rc.1"
30
30
  },
31
31
  "devDependencies": {
32
+ "@deepseek-ai/dsh-tool-executor-native": "0.2.2-rc.1",
32
33
  "tsdown": "^0.22.0",
33
34
  "vitest": "^3.0.0"
34
35
  },