@dshtrading/watchlist 0.6.3 → 0.7.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/file-store.js +76 -20
- package/lib/index.d.ts +17 -3
- package/lib/index.js +31 -2
- package/lib/plugin.js +5 -2
- package/package.json +6 -5
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
|
-
/** 文本 →
|
|
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
|
|
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: (
|
|
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) ??
|
|
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
|
|
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
|
|
223
|
-
...
|
|
224
|
-
|
|
225
|
-
|
|
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
|
|
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
|
-
/**
|
|
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)
|
|
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.
|
|
4
|
+
"version": "0.7.0",
|
|
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.
|
|
25
|
-
"@dshtrading/dsh-home": "^0.
|
|
24
|
+
"@dshtrading/api": "^0.7.0",
|
|
25
|
+
"@dshtrading/dsh-home": "^0.7.0"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@deepseek-ai/cordis": ">=4.
|
|
29
|
-
"@deepseek-ai/dsh-tools": ">=0.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
|
},
|