dsh-plugin-show-me-data 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.
Files changed (130) hide show
  1. package/LICENSE +27 -0
  2. package/README.md +96 -0
  3. package/cordis.patch.yml +40 -0
  4. package/docs/01-product-effect.md +178 -0
  5. package/docs/02-architecture.md +275 -0
  6. package/docs/03-data-contracts.md +291 -0
  7. package/docs/04-sources.md +342 -0
  8. package/docs/05-ui-spec.md +167 -0
  9. package/docs/06-ai-layer.md +194 -0
  10. package/docs/07-implementation-plan.md +399 -0
  11. package/docs/08-test-plan.md +133 -0
  12. package/docs/09-packaging-install.md +249 -0
  13. package/docs/10-kickoff-prompt.md +94 -0
  14. package/docs/11-decisions.md +203 -0
  15. package/docs/12-runtime-verified.md +115 -0
  16. package/docs/13-acceptance.md +153 -0
  17. package/docs/14-progress.md +150 -0
  18. package/docs/15-publish.md +185 -0
  19. package/lib/app/ai-deterministic.js +327 -0
  20. package/lib/app/ai-validate.js +284 -0
  21. package/lib/app/ai.js +440 -0
  22. package/lib/app/health.js +77 -0
  23. package/lib/app/overview.js +349 -0
  24. package/lib/app/propose-indicator.js +122 -0
  25. package/lib/app/refresh.js +251 -0
  26. package/lib/app/series-view.js +195 -0
  27. package/lib/app/watchlist.js +102 -0
  28. package/lib/client.js +4322 -0
  29. package/lib/core/ai/prompts.js +213 -0
  30. package/lib/core/chart/axis.js +133 -0
  31. package/lib/core/chart/bar.js +58 -0
  32. package/lib/core/chart/candle.js +216 -0
  33. package/lib/core/chart/line.js +186 -0
  34. package/lib/core/chart/scale.js +132 -0
  35. package/lib/core/format.js +143 -0
  36. package/lib/core/indicators/catalog.js +1011 -0
  37. package/lib/core/indicators/resolve.js +196 -0
  38. package/lib/core/insight/digest.js +250 -0
  39. package/lib/core/insight/rank.js +115 -0
  40. package/lib/core/insight/related.js +90 -0
  41. package/lib/core/insight/rules.js +417 -0
  42. package/lib/core/stats/derive.js +123 -0
  43. package/lib/core/stats/series.js +465 -0
  44. package/lib/core/time/range.js +242 -0
  45. package/lib/core/types.js +478 -0
  46. package/lib/host/ai/discussion.js +559 -0
  47. package/lib/host/ai/dsh-llm-gateway.js +333 -0
  48. package/lib/host/config.js +194 -0
  49. package/lib/host/http/respond.js +165 -0
  50. package/lib/host/http/routes.js +689 -0
  51. package/lib/host/index.js +293 -0
  52. package/lib/host/infra/fs-repos.js +179 -0
  53. package/lib/host/infra/memory-fallback.js +64 -0
  54. package/lib/host/tools/define-tool.js +295 -0
  55. package/lib/host/tools/register.js +431 -0
  56. package/lib/host.js +7 -0
  57. package/lib/ports/clock.js +57 -0
  58. package/lib/ports/snapshot-repo.js +48 -0
  59. package/lib/sources/eastmoney-macro.js +197 -0
  60. package/lib/sources/eastmoney-quote.js +201 -0
  61. package/lib/sources/ecb.js +179 -0
  62. package/lib/sources/fred.js +207 -0
  63. package/lib/sources/http.js +136 -0
  64. package/lib/sources/ohlc.js +36 -0
  65. package/lib/sources/quote-cascade.js +177 -0
  66. package/lib/sources/registry.js +153 -0
  67. package/lib/sources/sina-cn.js +197 -0
  68. package/lib/sources/sina-us.js +187 -0
  69. package/lib/sources/tencent.js +158 -0
  70. package/lib/sources/us-treasury-rates.js +275 -0
  71. package/lib/sources/us-treasury.js +196 -0
  72. package/lib/sources/worldbank.js +170 -0
  73. package/package.json +69 -0
  74. package/src/app/ai-deterministic.js +327 -0
  75. package/src/app/ai-validate.js +284 -0
  76. package/src/app/ai.js +440 -0
  77. package/src/app/health.js +77 -0
  78. package/src/app/overview.js +349 -0
  79. package/src/app/propose-indicator.js +122 -0
  80. package/src/app/refresh.js +251 -0
  81. package/src/app/series-view.js +195 -0
  82. package/src/app/watchlist.js +102 -0
  83. package/src/client/api.js +323 -0
  84. package/src/client/components.js +1877 -0
  85. package/src/client/copy.js +368 -0
  86. package/src/client/index.js +169 -0
  87. package/src/client/store.js +219 -0
  88. package/src/core/ai/prompts.js +213 -0
  89. package/src/core/chart/axis.js +133 -0
  90. package/src/core/chart/bar.js +58 -0
  91. package/src/core/chart/candle.js +216 -0
  92. package/src/core/chart/line.js +186 -0
  93. package/src/core/chart/scale.js +132 -0
  94. package/src/core/format.js +143 -0
  95. package/src/core/indicators/catalog.js +1011 -0
  96. package/src/core/indicators/resolve.js +196 -0
  97. package/src/core/insight/digest.js +250 -0
  98. package/src/core/insight/rank.js +115 -0
  99. package/src/core/insight/related.js +90 -0
  100. package/src/core/insight/rules.js +417 -0
  101. package/src/core/stats/derive.js +123 -0
  102. package/src/core/stats/series.js +465 -0
  103. package/src/core/time/range.js +242 -0
  104. package/src/core/types.js +478 -0
  105. package/src/host/ai/discussion.js +559 -0
  106. package/src/host/ai/dsh-llm-gateway.js +333 -0
  107. package/src/host/config.js +194 -0
  108. package/src/host/http/respond.js +165 -0
  109. package/src/host/http/routes.js +689 -0
  110. package/src/host/index.js +293 -0
  111. package/src/host/infra/fs-repos.js +179 -0
  112. package/src/host/infra/memory-fallback.js +64 -0
  113. package/src/host/tools/define-tool.js +295 -0
  114. package/src/host/tools/register.js +431 -0
  115. package/src/ports/clock.js +57 -0
  116. package/src/ports/snapshot-repo.js +48 -0
  117. package/src/sources/eastmoney-macro.js +197 -0
  118. package/src/sources/eastmoney-quote.js +201 -0
  119. package/src/sources/ecb.js +179 -0
  120. package/src/sources/fred.js +207 -0
  121. package/src/sources/http.js +136 -0
  122. package/src/sources/ohlc.js +36 -0
  123. package/src/sources/quote-cascade.js +177 -0
  124. package/src/sources/registry.js +153 -0
  125. package/src/sources/sina-cn.js +197 -0
  126. package/src/sources/sina-us.js +187 -0
  127. package/src/sources/tencent.js +158 -0
  128. package/src/sources/us-treasury-rates.js +275 -0
  129. package/src/sources/us-treasury.js +196 -0
  130. package/src/sources/worldbank.js +170 -0
@@ -0,0 +1,323 @@
1
+ /**
2
+ * The panel's single transport layer (docs/05 §3).
3
+ *
4
+ * Every call goes to the host half under '/api/show-me-data/*'; the browser never
5
+ * talks to an upstream data source (CORS and provenance both depend on that).
6
+ * Each request has a timeout, is cancellable, and turns a structured error
7
+ * payload into a value the UI can render instead of an exception the UI must
8
+ * catch blindly.
9
+ *
10
+ * @module client/api
11
+ */
12
+
13
+ /** Request timeout in milliseconds (docs/05 §3). */
14
+ export const REQUEST_TIMEOUT_MS = 8000
15
+
16
+ /** The API root. */
17
+ export const API_ROOT = '/api/show-me-data'
18
+
19
+ /**
20
+ * Perform one JSON request.
21
+ *
22
+ * @param {string} path - path under {@link API_ROOT}, e.g. '/overview?range=1Y'.
23
+ * @param {{ method?: string, body?: any, signal?: AbortSignal, timeoutMs?: number, fetchImpl?: Function }} [options] - request options.
24
+ * @returns {Promise<{ ok: boolean, status: number, data?: any, error?: { kind: string, detail: string, retryable: boolean } }>} response.
25
+ */
26
+ export async function request(path, { method = 'GET', body, signal, timeoutMs = REQUEST_TIMEOUT_MS, fetchImpl } = {}) {
27
+ const doFetch = fetchImpl ?? (typeof fetch === 'function' ? fetch : undefined)
28
+ if (doFetch === undefined) {
29
+ return { ok: false, status: 0, error: { kind: 'network', detail: 'fetch is unavailable in this environment', retryable: false } }
30
+ }
31
+ const controller = new AbortController()
32
+ const timer = setTimeout(() => controller.abort(), timeoutMs)
33
+ const composed = typeof AbortSignal !== 'undefined' && typeof AbortSignal.any === 'function' && signal !== undefined
34
+ ? AbortSignal.any([signal, controller.signal])
35
+ : controller.signal
36
+ try {
37
+ const response = await doFetch(`${API_ROOT}${path}`, {
38
+ method,
39
+ headers: body === undefined ? undefined : { 'content-type': 'application/json' },
40
+ body: body === undefined ? undefined : JSON.stringify(body),
41
+ signal: composed,
42
+ })
43
+ const text = await response.text()
44
+ let payload
45
+ try {
46
+ payload = text === '' ? undefined : JSON.parse(text)
47
+ } catch {
48
+ payload = undefined
49
+ }
50
+ if (!response.ok) {
51
+ return {
52
+ ok: false,
53
+ status: response.status,
54
+ // The payload rides along on failures too: a discussion whose session was
55
+ // created but whose first turn failed still returns its `sessionId`, and
56
+ // the panel needs it to offer 「打开会话」.
57
+ ...(payload === undefined ? {} : { data: payload }),
58
+ error: payload?.error ?? {
59
+ kind: response.status === 404 ? 'not-found' : 'http',
60
+ detail: `请求失败(HTTP ${response.status})`,
61
+ retryable: response.status >= 500,
62
+ },
63
+ }
64
+ }
65
+ return { ok: true, status: response.status, data: payload }
66
+ } catch (error) {
67
+ const aborted = error?.name === 'AbortError'
68
+ return {
69
+ ok: false,
70
+ status: 0,
71
+ error: {
72
+ kind: aborted ? 'timeout' : 'network',
73
+ detail: aborted ? `请求超过 ${timeoutMs}ms 未返回` : `网络错误:${error?.message ?? error}`,
74
+ retryable: true,
75
+ },
76
+ }
77
+ } finally {
78
+ clearTimeout(timer)
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Start an SSE request, invoking handlers as events arrive.
84
+ *
85
+ * Uses 'fetch' + a stream reader rather than 'EventSource' so the POST payload
86
+ * and the caller's abort signal both work.
87
+ *
88
+ * @param {string} path - path under {@link API_ROOT}.
89
+ * @param {{ body?: any, signal?: AbortSignal, onText?: (text: string) => void, onDone?: (result: any) => void, onError?: (error: object) => void, fetchImpl?: Function }} options - options.
90
+ * @returns {Promise<{ ok: boolean, result?: any, error?: object }>} completion.
91
+ */
92
+ export async function stream(path, { body, signal, onText, onDone, onError, fetchImpl } = {}) {
93
+ const doFetch = fetchImpl ?? (typeof fetch === 'function' ? fetch : undefined)
94
+ if (doFetch === undefined) {
95
+ const error = { kind: 'network', detail: 'fetch is unavailable', retryable: false }
96
+ onError?.(error)
97
+ return { ok: false, error }
98
+ }
99
+ try {
100
+ const response = await doFetch(`${API_ROOT}${path}`, {
101
+ method: 'POST',
102
+ headers: { 'content-type': 'application/json', accept: 'text/event-stream' },
103
+ body: JSON.stringify(body ?? {}),
104
+ signal,
105
+ })
106
+ if (!response.ok || response.body === undefined || response.body === null) {
107
+ // The host answers with plain JSON when streaming is unsupported; accept it.
108
+ const text = await response.text()
109
+ try {
110
+ const parsed = JSON.parse(text)
111
+ onDone?.(parsed)
112
+ return { ok: true, result: parsed }
113
+ } catch {
114
+ const error = { kind: 'http', detail: `流式请求失败(HTTP ${response.status})`, retryable: response.status >= 500 }
115
+ onError?.(error)
116
+ return { ok: false, error }
117
+ }
118
+ }
119
+ const reader = response.body.getReader()
120
+ const decoder = new TextDecoder()
121
+ let buffer = ''
122
+ let result
123
+ for (;;) {
124
+ const { value, done } = await reader.read()
125
+ if (done) break
126
+ buffer += decoder.decode(value, { stream: true })
127
+ const frames = buffer.split('\n\n')
128
+ buffer = frames.pop() ?? ''
129
+ for (const frame of frames) {
130
+ const parsed = parseSseFrame(frame)
131
+ if (parsed === undefined) continue
132
+ if (parsed.event === 'text') onText?.(parsed.data?.text ?? '')
133
+ else if (parsed.event === 'done') {
134
+ result = parsed.data?.result
135
+ onDone?.(result)
136
+ } else if (parsed.event === 'error') onError?.({ kind: parsed.data?.kind ?? 'internal', detail: parsed.data?.detail ?? '', retryable: true })
137
+ }
138
+ }
139
+ return { ok: true, result }
140
+ } catch (error) {
141
+ const aborted = error?.name === 'AbortError'
142
+ const payload = { kind: aborted ? 'aborted' : 'network', detail: `${error?.message ?? error}`, retryable: !aborted }
143
+ onError?.(payload)
144
+ return { ok: false, error: payload }
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Parse one SSE frame.
150
+ *
151
+ * @param {string} frame - frame text ('event: x\ndata: {...}').
152
+ * @returns {{ event: string, data: any }|undefined} parsed frame.
153
+ */
154
+ export function parseSseFrame(frame) {
155
+ const trimmed = String(frame ?? '').trim()
156
+ if (trimmed === '') return undefined
157
+ let event = 'message'
158
+ const dataLines = []
159
+ for (const line of trimmed.split('\n')) {
160
+ if (line.startsWith('event:')) event = line.slice(6).trim()
161
+ else if (line.startsWith('data:')) dataLines.push(line.slice(5).trim())
162
+ }
163
+ if (dataLines.length === 0) return { event, data: undefined }
164
+ const raw = dataLines.join('\n')
165
+ try {
166
+ return { event, data: JSON.parse(raw) }
167
+ } catch {
168
+ return { event, data: raw }
169
+ }
170
+ }
171
+
172
+ /** The endpoints the panel uses, so no string is built ad hoc in a component. */
173
+ export const api = {
174
+ /**
175
+ * @param {{ range?: string, groups?: string[], ids?: string[], limit?: number, force?: boolean, signal?: AbortSignal }} [options] - request.
176
+ * @returns {Promise<object>} response.
177
+ */
178
+ overview(options = {}) {
179
+ const query = []
180
+ if (options.range !== undefined) query.push(`range=${encodeURIComponent(options.range)}`)
181
+ if (options.groups !== undefined && options.groups.length > 0) query.push(`groups=${encodeURIComponent(options.groups.join(','))}`)
182
+ if (options.ids !== undefined && options.ids.length > 0) query.push(`ids=${encodeURIComponent(options.ids.join(','))}`)
183
+ if (options.limit !== undefined) query.push(`limit=${options.limit}`)
184
+ if (options.force === true) query.push('force=1')
185
+ return request(`/overview${query.length === 0 ? '' : `?${query.join('&')}`}`, { signal: options.signal })
186
+ },
187
+ /**
188
+ * @param {object} options - request.
189
+ * @returns {Promise<object>} response.
190
+ */
191
+ series(options) {
192
+ const query = [`indicator=${encodeURIComponent(options.indicator)}`]
193
+ if (options.range !== undefined) query.push(`range=${encodeURIComponent(options.range)}`)
194
+ if (options.transform !== undefined) query.push(`transform=${encodeURIComponent(options.transform)}`)
195
+ if (options.compareWith !== undefined) query.push(`compareWith=${encodeURIComponent(options.compareWith)}`)
196
+ if (options.freq !== undefined) query.push(`freq=${encodeURIComponent(options.freq)}`)
197
+ return request(`/series?${query.join('&')}`, { signal: options.signal })
198
+ },
199
+ /**
200
+ * @param {AbortSignal} [signal] - cancellation.
201
+ * @returns {Promise<object>} response.
202
+ */
203
+ watchlist(signal) {
204
+ return request('/watchlist', { signal })
205
+ },
206
+ /**
207
+ * @param {object} body - action payload.
208
+ * @param {AbortSignal} [signal] - cancellation.
209
+ * @returns {Promise<object>} response.
210
+ */
211
+ watchlistAction(body, signal) {
212
+ return request('/watchlist', { method: 'POST', body, signal })
213
+ },
214
+ /**
215
+ * @param {string} query - search text.
216
+ * @param {AbortSignal} [signal] - cancellation.
217
+ * @returns {Promise<object>} response.
218
+ */
219
+ search(query, signal) {
220
+ return request(`/catalog/search?q=${encodeURIComponent(query)}`, { signal })
221
+ },
222
+ /**
223
+ * @param {object} body - '{ indicator, range }'.
224
+ * @param {object} handlers - SSE handlers.
225
+ * @returns {Promise<object>} completion.
226
+ */
227
+ explain(body, handlers = {}) {
228
+ return stream('/ai/explain', { body: { ...body, stream: true }, ...handlers })
229
+ },
230
+ /**
231
+ * @param {object} body - '{ range, indicators }'.
232
+ * @param {object} handlers - SSE handlers.
233
+ * @returns {Promise<object>} completion.
234
+ */
235
+ summary(body, handlers = {}) {
236
+ return stream('/ai/summary', { body: { ...body, stream: true }, ...handlers })
237
+ },
238
+ /**
239
+ * @param {object} body - '{ question, range }'.
240
+ * @param {object} handlers - SSE handlers.
241
+ * @returns {Promise<object>} completion.
242
+ */
243
+ ask(body, handlers = {}) {
244
+ return stream('/ai/ask', { body: { ...body, stream: true }, ...handlers })
245
+ },
246
+ /**
247
+ * @param {object} body - '{ text }'.
248
+ * @param {AbortSignal} [signal] - cancellation.
249
+ * @returns {Promise<object>} response.
250
+ */
251
+ propose(body, signal) {
252
+ return request('/ai/propose', { method: 'POST', body, signal })
253
+ },
254
+ /**
255
+ * Open (or continue) an independent DSH session about one indicator.
256
+ *
257
+ * @param {{ indicator?: string, group?: string, noteworthy?: string[], question?: string, range?: string, mode?: 'new'|'topic', limit?: number }} body - request; exactly one of 'indicator'/'group' names the subject.
258
+ * @param {AbortSignal} [signal] - cancellation.
259
+ * @returns {Promise<object>} response with `{ sessionId, reply, pending, mode }`.
260
+ */
261
+ discuss(body, signal) {
262
+ // `wait: false` returns as soon as the session exists and the question is
263
+ // queued: the answer streams in the session the GUI switches to, so the
264
+ // panel never blocks for the length of a model turn.
265
+ return request('/discuss', { method: 'POST', body: { wait: false, ...body }, signal, timeoutMs: 30_000 })
266
+ },
267
+ /**
268
+ * Analyse the panel as a whole, or one group of it.
269
+ *
270
+ * @param {{ range?: string, groups?: string[] }} body - request.
271
+ * @param {AbortSignal} [signal] - cancellation.
272
+ * @returns {Promise<object>} response with `{ markdown, mode, scope, indicators, noteworthy }`.
273
+ */
274
+ aiOverview(body, signal) {
275
+ return request('/ai/overview', { method: 'POST', body, signal, timeoutMs: 120_000 })
276
+ },
277
+ /**
278
+ * Open a session in the plugin workspace to add a data source.
279
+ *
280
+ * @param {{ request?: string, reasons?: string[], question?: string }} body - request.
281
+ * @param {AbortSignal} [signal] - cancellation.
282
+ * @returns {Promise<object>} response with `{ sessionId }`.
283
+ */
284
+ iterate(body, signal) {
285
+ return request('/iterate', { method: 'POST', body, signal, timeoutMs: 30_000 })
286
+ },
287
+ /**
288
+ * Collect the answer a discussion session produced.
289
+ *
290
+ * Opening a discussion returns before the model finishes, so the panel asks
291
+ * for the text on a short poll instead of leaving its block empty while the
292
+ * answer streams in a session the reader has to go and find.
293
+ *
294
+ * @param {string} sessionId - session id from `discuss()`.
295
+ * @param {AbortSignal} [signal] - cancellation.
296
+ * @returns {Promise<object>} response with `{ status, reply?, error? }`.
297
+ */
298
+ discussAnswer(sessionId, signal) {
299
+ return request(`/discuss/answer?session=${encodeURIComponent(sessionId)}`, { signal, timeoutMs: 10_000 })
300
+ },
301
+ /**
302
+ * The mount's effective configuration, for the settings screen.
303
+ *
304
+ * @param {AbortSignal} [signal] - cancellation.
305
+ * @returns {Promise<object>} response.
306
+ */
307
+ settings(signal) {
308
+ return request('/settings', { signal, timeoutMs: 10_000 })
309
+ },
310
+ /**
311
+ * @param {AbortSignal} [signal] - cancellation.
312
+ * @returns {Promise<object>} response.
313
+ */
314
+ health({ probe, ids, range } = {}, signal) {
315
+ const query = []
316
+ if (probe === true) query.push('probe=1')
317
+ if (Array.isArray(ids) && ids.length > 0) query.push(`ids=${encodeURIComponent(ids.join(','))}`)
318
+ if (range !== undefined) query.push(`range=${encodeURIComponent(range)}`)
319
+ // A probe re-fetches upstream series, so it needs a longer budget than a
320
+ // cached read.
321
+ return request(`/health${query.length === 0 ? '' : `?${query.join('&')}`}`, { signal, timeoutMs: probe === true ? 60_000 : REQUEST_TIMEOUT_MS })
322
+ },
323
+ }