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.
- package/LICENSE +27 -0
- package/README.md +96 -0
- package/cordis.patch.yml +40 -0
- package/docs/01-product-effect.md +178 -0
- package/docs/02-architecture.md +275 -0
- package/docs/03-data-contracts.md +291 -0
- package/docs/04-sources.md +342 -0
- package/docs/05-ui-spec.md +167 -0
- package/docs/06-ai-layer.md +194 -0
- package/docs/07-implementation-plan.md +399 -0
- package/docs/08-test-plan.md +133 -0
- package/docs/09-packaging-install.md +249 -0
- package/docs/10-kickoff-prompt.md +94 -0
- package/docs/11-decisions.md +203 -0
- package/docs/12-runtime-verified.md +115 -0
- package/docs/13-acceptance.md +153 -0
- package/docs/14-progress.md +150 -0
- package/docs/15-publish.md +185 -0
- package/lib/app/ai-deterministic.js +327 -0
- package/lib/app/ai-validate.js +284 -0
- package/lib/app/ai.js +440 -0
- package/lib/app/health.js +77 -0
- package/lib/app/overview.js +349 -0
- package/lib/app/propose-indicator.js +122 -0
- package/lib/app/refresh.js +251 -0
- package/lib/app/series-view.js +195 -0
- package/lib/app/watchlist.js +102 -0
- package/lib/client.js +4322 -0
- package/lib/core/ai/prompts.js +213 -0
- package/lib/core/chart/axis.js +133 -0
- package/lib/core/chart/bar.js +58 -0
- package/lib/core/chart/candle.js +216 -0
- package/lib/core/chart/line.js +186 -0
- package/lib/core/chart/scale.js +132 -0
- package/lib/core/format.js +143 -0
- package/lib/core/indicators/catalog.js +1011 -0
- package/lib/core/indicators/resolve.js +196 -0
- package/lib/core/insight/digest.js +250 -0
- package/lib/core/insight/rank.js +115 -0
- package/lib/core/insight/related.js +90 -0
- package/lib/core/insight/rules.js +417 -0
- package/lib/core/stats/derive.js +123 -0
- package/lib/core/stats/series.js +465 -0
- package/lib/core/time/range.js +242 -0
- package/lib/core/types.js +478 -0
- package/lib/host/ai/discussion.js +559 -0
- package/lib/host/ai/dsh-llm-gateway.js +333 -0
- package/lib/host/config.js +194 -0
- package/lib/host/http/respond.js +165 -0
- package/lib/host/http/routes.js +689 -0
- package/lib/host/index.js +293 -0
- package/lib/host/infra/fs-repos.js +179 -0
- package/lib/host/infra/memory-fallback.js +64 -0
- package/lib/host/tools/define-tool.js +295 -0
- package/lib/host/tools/register.js +431 -0
- package/lib/host.js +7 -0
- package/lib/ports/clock.js +57 -0
- package/lib/ports/snapshot-repo.js +48 -0
- package/lib/sources/eastmoney-macro.js +197 -0
- package/lib/sources/eastmoney-quote.js +201 -0
- package/lib/sources/ecb.js +179 -0
- package/lib/sources/fred.js +207 -0
- package/lib/sources/http.js +136 -0
- package/lib/sources/ohlc.js +36 -0
- package/lib/sources/quote-cascade.js +177 -0
- package/lib/sources/registry.js +153 -0
- package/lib/sources/sina-cn.js +197 -0
- package/lib/sources/sina-us.js +187 -0
- package/lib/sources/tencent.js +158 -0
- package/lib/sources/us-treasury-rates.js +275 -0
- package/lib/sources/us-treasury.js +196 -0
- package/lib/sources/worldbank.js +170 -0
- package/package.json +69 -0
- package/src/app/ai-deterministic.js +327 -0
- package/src/app/ai-validate.js +284 -0
- package/src/app/ai.js +440 -0
- package/src/app/health.js +77 -0
- package/src/app/overview.js +349 -0
- package/src/app/propose-indicator.js +122 -0
- package/src/app/refresh.js +251 -0
- package/src/app/series-view.js +195 -0
- package/src/app/watchlist.js +102 -0
- package/src/client/api.js +323 -0
- package/src/client/components.js +1877 -0
- package/src/client/copy.js +368 -0
- package/src/client/index.js +169 -0
- package/src/client/store.js +219 -0
- package/src/core/ai/prompts.js +213 -0
- package/src/core/chart/axis.js +133 -0
- package/src/core/chart/bar.js +58 -0
- package/src/core/chart/candle.js +216 -0
- package/src/core/chart/line.js +186 -0
- package/src/core/chart/scale.js +132 -0
- package/src/core/format.js +143 -0
- package/src/core/indicators/catalog.js +1011 -0
- package/src/core/indicators/resolve.js +196 -0
- package/src/core/insight/digest.js +250 -0
- package/src/core/insight/rank.js +115 -0
- package/src/core/insight/related.js +90 -0
- package/src/core/insight/rules.js +417 -0
- package/src/core/stats/derive.js +123 -0
- package/src/core/stats/series.js +465 -0
- package/src/core/time/range.js +242 -0
- package/src/core/types.js +478 -0
- package/src/host/ai/discussion.js +559 -0
- package/src/host/ai/dsh-llm-gateway.js +333 -0
- package/src/host/config.js +194 -0
- package/src/host/http/respond.js +165 -0
- package/src/host/http/routes.js +689 -0
- package/src/host/index.js +293 -0
- package/src/host/infra/fs-repos.js +179 -0
- package/src/host/infra/memory-fallback.js +64 -0
- package/src/host/tools/define-tool.js +295 -0
- package/src/host/tools/register.js +431 -0
- package/src/ports/clock.js +57 -0
- package/src/ports/snapshot-repo.js +48 -0
- package/src/sources/eastmoney-macro.js +197 -0
- package/src/sources/eastmoney-quote.js +201 -0
- package/src/sources/ecb.js +179 -0
- package/src/sources/fred.js +207 -0
- package/src/sources/http.js +136 -0
- package/src/sources/ohlc.js +36 -0
- package/src/sources/quote-cascade.js +177 -0
- package/src/sources/registry.js +153 -0
- package/src/sources/sina-cn.js +197 -0
- package/src/sources/sina-us.js +187 -0
- package/src/sources/tencent.js +158 -0
- package/src/sources/us-treasury-rates.js +275 -0
- package/src/sources/us-treasury.js +196 -0
- package/src/sources/worldbank.js +170 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The eight model tools (docs/06 §5).
|
|
3
|
+
*
|
|
4
|
+
* The tools are the conversational entry point into the same use cases the panel
|
|
5
|
+
* calls, so "ask the agent to add an indicator" needs no extra plumbing. Each one
|
|
6
|
+
* declares a JSON Schema for its arguments and for its canonical output; the
|
|
7
|
+
* definition builder validates both.
|
|
8
|
+
*
|
|
9
|
+
* @module host/tools/register
|
|
10
|
+
*/
|
|
11
|
+
import { defineTool, summarize } from './define-tool.js'
|
|
12
|
+
import { adapterIds, listCapabilities } from '../../sources/registry.js'
|
|
13
|
+
|
|
14
|
+
/** Tool names, in the order they are registered. */
|
|
15
|
+
export const TOOL_NAMES = [
|
|
16
|
+
'data_overview',
|
|
17
|
+
'data_series',
|
|
18
|
+
'data_search',
|
|
19
|
+
'data_watchlist',
|
|
20
|
+
'data_explain',
|
|
21
|
+
'data_digest',
|
|
22
|
+
'data_refresh',
|
|
23
|
+
'data_health',
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
/** Throttle window for 'data_refresh' (docs/06 §5). */
|
|
27
|
+
export const REFRESH_THROTTLE_MS = 60_000
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create the tool definitions.
|
|
31
|
+
*
|
|
32
|
+
* @param {object} deps - dependencies.
|
|
33
|
+
* @param {object} deps.useCases - '{ overview, seriesView, watchlist, health, ai, search }'.
|
|
34
|
+
* @param {import('../ports/clock.js').Clock} deps.clock - clock.
|
|
35
|
+
* @param {(msg: string, meta?: object) => void} [deps.log] - logger.
|
|
36
|
+
* @returns {object[]} tool definitions.
|
|
37
|
+
*/
|
|
38
|
+
export function createToolDefinitions({ useCases, clock, log = () => {} }) {
|
|
39
|
+
/** @type {number} */
|
|
40
|
+
let lastRefreshAt = 0
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Project a metric card down to what a model needs.
|
|
44
|
+
*
|
|
45
|
+
* @param {object} metric - card.
|
|
46
|
+
* @returns {object} slim metric.
|
|
47
|
+
*/
|
|
48
|
+
const slimMetric = (metric) => ({
|
|
49
|
+
indicatorId: metric.indicatorId,
|
|
50
|
+
group: metric.group,
|
|
51
|
+
label: metric.label,
|
|
52
|
+
unit: metric.unit,
|
|
53
|
+
status: metric.status,
|
|
54
|
+
latest: metric.latest,
|
|
55
|
+
latestAt: metric.latestAt,
|
|
56
|
+
changeAbs: metric.changeAbs,
|
|
57
|
+
changePct: metric.changePct,
|
|
58
|
+
yoy: metric.yoy,
|
|
59
|
+
score: metric.score,
|
|
60
|
+
sourceRef: metric.sourceRef,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const rangeParam = { type: 'string', description: 'Range preset: 1M, 3M, 6M, YTD, 1Y, 3Y, 5Y, MAX.' }
|
|
64
|
+
|
|
65
|
+
return [
|
|
66
|
+
defineTool({
|
|
67
|
+
name: 'data_overview',
|
|
68
|
+
description:
|
|
69
|
+
'Global indicator radar: today\'s most noteworthy macro/rates/equity/bond indicators plus all core cards. Use it to answer "what is worth watching today".',
|
|
70
|
+
parameters: {
|
|
71
|
+
type: 'object',
|
|
72
|
+
additionalProperties: false,
|
|
73
|
+
properties: {
|
|
74
|
+
range: rangeParam,
|
|
75
|
+
groups: { type: 'array', items: { type: 'string', enum: ['US', 'CN', 'GLOBAL', 'CUSTOM'] }, description: 'Restrict to these groups.' },
|
|
76
|
+
limit: { type: 'integer', description: 'How many noteworthy items to return (1-20).' },
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
outputSchema: {
|
|
80
|
+
type: 'object',
|
|
81
|
+
additionalProperties: true,
|
|
82
|
+
required: ['range', 'metrics', 'noteworthy', 'sources'],
|
|
83
|
+
properties: {
|
|
84
|
+
range: { type: 'object', additionalProperties: true },
|
|
85
|
+
metrics: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
86
|
+
noteworthy: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
87
|
+
sources: { type: 'object', additionalProperties: true },
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
render: (_args, value) => [
|
|
91
|
+
{
|
|
92
|
+
type: 'text',
|
|
93
|
+
text: [
|
|
94
|
+
`区间 ${value.range.from} .. ${value.range.to};共 ${value.metrics.length} 个指标。`,
|
|
95
|
+
value.noteworthy.length === 0
|
|
96
|
+
? '没有触发值得关注的规则。'
|
|
97
|
+
: `值得关注(${value.noteworthy.length}):\n${value.noteworthy
|
|
98
|
+
.map((item) => `- ${item.label?.zh ?? item.indicatorId}:${(item.reasons ?? []).map((reason) => reason.reason?.zh).join(';')}`)
|
|
99
|
+
.join('\n')}`,
|
|
100
|
+
`来源状态:${Object.entries(value.sources)
|
|
101
|
+
.map(([key, status]) => `${key} ok=${status.ok} stale=${status.degraded} failed=${status.failed}`)
|
|
102
|
+
.join(' | ')}`,
|
|
103
|
+
].join('\n'),
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
execute: async (args) => {
|
|
107
|
+
const result = await useCases.overview({
|
|
108
|
+
range: args.range ?? '1Y',
|
|
109
|
+
groups: args.groups,
|
|
110
|
+
limit: args.limit,
|
|
111
|
+
})
|
|
112
|
+
return {
|
|
113
|
+
range: result.range,
|
|
114
|
+
metrics: result.metrics.map(slimMetric),
|
|
115
|
+
noteworthy: result.noteworthy,
|
|
116
|
+
sources: result.sources,
|
|
117
|
+
degraded: result.degraded,
|
|
118
|
+
generatedAt: result.generatedAt,
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
}),
|
|
122
|
+
|
|
123
|
+
defineTool({
|
|
124
|
+
name: 'data_series',
|
|
125
|
+
description:
|
|
126
|
+
'Fetch one indicator\'s detail: the point series, its statistics and its provenance. Use it before explaining a specific number.',
|
|
127
|
+
parameters: {
|
|
128
|
+
type: 'object',
|
|
129
|
+
additionalProperties: false,
|
|
130
|
+
required: ['indicator'],
|
|
131
|
+
properties: {
|
|
132
|
+
indicator: { type: 'string', description: 'Indicator id (e.g. us.cpi.yoy) or a keyword to resolve.' },
|
|
133
|
+
range: rangeParam,
|
|
134
|
+
transform: { type: 'string', description: 'Override the display transform: raw, diff, pctChange, yoy, mom, annualize.' },
|
|
135
|
+
compareWith: { type: 'string', description: 'Second indicator id to join on dates.' },
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
outputSchema: {
|
|
139
|
+
type: 'object',
|
|
140
|
+
additionalProperties: true,
|
|
141
|
+
required: ['indicatorId', 'points', 'status'],
|
|
142
|
+
properties: {
|
|
143
|
+
indicatorId: { type: 'string' },
|
|
144
|
+
points: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
145
|
+
status: { type: 'string' },
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
render: (_args, value) => [
|
|
149
|
+
{
|
|
150
|
+
type: 'text',
|
|
151
|
+
text:
|
|
152
|
+
value.error !== undefined
|
|
153
|
+
? `无法取数:${value.error.detail ?? value.error.code}`
|
|
154
|
+
: `${value.indicatorId} ${value.status}:最新 ${value.stats?.latest ?? 'n/a'} @${value.stats?.latestAt ?? 'n/a'},共 ${value.points.length} 个点;来源 ${value.sourceRef?.url ?? 'n/a'}`,
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
execute: async (args) => {
|
|
158
|
+
const resolved = await resolveIndicatorId(useCases, args.indicator)
|
|
159
|
+
if (resolved === undefined) return { indicatorId: args.indicator, points: [], status: 'error', error: { code: 'unknown-indicator', detail: `no indicator matches ${args.indicator}` } }
|
|
160
|
+
const view = await useCases.seriesView({ indicatorId: resolved, range: args.range ?? '1Y', transform: args.transform, compareWith: args.compareWith })
|
|
161
|
+
if (view?.error !== undefined) return { indicatorId: resolved, points: [], status: 'error', error: view.error, view: view.view }
|
|
162
|
+
return {
|
|
163
|
+
indicatorId: view.indicatorId,
|
|
164
|
+
label: view.label,
|
|
165
|
+
unit: view.unit,
|
|
166
|
+
range: view.range,
|
|
167
|
+
status: view.status,
|
|
168
|
+
stats: view.stats,
|
|
169
|
+
points: view.points,
|
|
170
|
+
compare: view.compare === undefined ? undefined : { indicatorId: view.compare.indicatorId, points: view.compare.points },
|
|
171
|
+
sourceRef: view.sourceRef,
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
}),
|
|
175
|
+
|
|
176
|
+
defineTool({
|
|
177
|
+
name: 'data_search',
|
|
178
|
+
description: 'Search the indicator catalog or the source list by keyword, in Chinese or English.',
|
|
179
|
+
parameters: {
|
|
180
|
+
type: 'object',
|
|
181
|
+
additionalProperties: false,
|
|
182
|
+
required: ['q'],
|
|
183
|
+
properties: {
|
|
184
|
+
q: { type: 'string', description: 'Keyword, e.g. 非农 / CPI / 国债.' },
|
|
185
|
+
kind: { type: 'string', enum: ['indicator', 'source'], description: 'Search indicators (default) or data sources.' },
|
|
186
|
+
limit: { type: 'integer', description: 'Maximum results (1-50).' },
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
outputSchema: {
|
|
190
|
+
type: 'object',
|
|
191
|
+
additionalProperties: false,
|
|
192
|
+
required: ['kind', 'matches'],
|
|
193
|
+
properties: {
|
|
194
|
+
kind: { type: 'string' },
|
|
195
|
+
matches: { type: 'array' },
|
|
196
|
+
suggestions: { type: 'array', items: { type: 'string' } },
|
|
197
|
+
registered: { type: 'array', items: { type: 'string' } },
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
render: (_args, value) => [
|
|
201
|
+
{
|
|
202
|
+
type: 'text',
|
|
203
|
+
text:
|
|
204
|
+
value.matches.length === 0
|
|
205
|
+
? `没有匹配结果${value.suggestions?.length ? `;可试试:${value.suggestions.join(', ')}` : ''}`
|
|
206
|
+
: value.matches.map((entry) => `${entry.indicatorId ?? entry.id} — ${entry.label?.zh ?? entry.label}`).join('\n'),
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
execute: async (args) => {
|
|
210
|
+
if (args.kind === 'source') {
|
|
211
|
+
const needle = String(args.q).toLowerCase()
|
|
212
|
+
const matches = listCapabilities()
|
|
213
|
+
.filter((entry) => entry.id.includes(needle) || entry.label.toLowerCase().includes(needle))
|
|
214
|
+
.map((entry) => ({ id: entry.id, label: entry.label, capabilities: entry.capabilities }))
|
|
215
|
+
return { kind: 'source', matches: matches.slice(0, args.limit ?? 12), registered: adapterIds() }
|
|
216
|
+
}
|
|
217
|
+
const result = await useCases.search(args.q, { limit: args.limit })
|
|
218
|
+
return { kind: 'indicator', matches: result.matches, suggestions: result.suggestions }
|
|
219
|
+
},
|
|
220
|
+
}),
|
|
221
|
+
|
|
222
|
+
defineTool({
|
|
223
|
+
name: 'data_watchlist',
|
|
224
|
+
description: 'Read or change the user\'s watchlist: list, add, remove, update or reorder custom indicators. `add` is idempotent.',
|
|
225
|
+
parameters: {
|
|
226
|
+
type: 'object',
|
|
227
|
+
additionalProperties: false,
|
|
228
|
+
required: ['action'],
|
|
229
|
+
properties: {
|
|
230
|
+
action: { type: 'string', enum: ['list', 'add', 'remove', 'update', 'reorder'] },
|
|
231
|
+
indicatorId: { type: 'string', description: 'Target indicator id (add/remove/update).' },
|
|
232
|
+
item: { type: 'object', additionalProperties: true, description: 'Item to add: { indicatorId, definition?, createdBy? }.' },
|
|
233
|
+
patch: { type: 'object', additionalProperties: true, description: 'Fields to change (update).' },
|
|
234
|
+
ids: { type: 'array', items: { type: 'string' }, description: 'Desired order (reorder).' },
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
outputSchema: {
|
|
238
|
+
type: 'object',
|
|
239
|
+
additionalProperties: true,
|
|
240
|
+
required: ['status', 'items'],
|
|
241
|
+
properties: {
|
|
242
|
+
status: { type: 'string' },
|
|
243
|
+
items: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
render: (_args, value) => [
|
|
247
|
+
{
|
|
248
|
+
type: 'text',
|
|
249
|
+
text:
|
|
250
|
+
`清单(${value.items.length}):` +
|
|
251
|
+
(value.items.length === 0 ? '(空)' : value.items.map((item) => item.indicatorId).join(', ')) +
|
|
252
|
+
(value.status === 'exists' ? '\n(该项已存在,未重复添加)' : ''),
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
execute: async (args) => {
|
|
256
|
+
if (args.action === 'list') return { status: 'listed', items: await useCases.watchlist.list() }
|
|
257
|
+
if (args.action === 'add') {
|
|
258
|
+
const result = await useCases.watchlist.add(args.item ?? { indicatorId: args.indicatorId }, { createdBy: 'ai' })
|
|
259
|
+
if (result.error !== undefined) return { status: 'invalid', items: await useCases.watchlist.list(), error: result.error }
|
|
260
|
+
return { status: result.created ? 'created' : 'exists', item: result.item, conflict: result.conflict === true, items: await useCases.watchlist.list() }
|
|
261
|
+
}
|
|
262
|
+
if (args.action === 'remove') {
|
|
263
|
+
const result = await useCases.watchlist.remove(args.indicatorId)
|
|
264
|
+
return { status: result.status, items: await useCases.watchlist.list() }
|
|
265
|
+
}
|
|
266
|
+
if (args.action === 'update') {
|
|
267
|
+
const result = await useCases.watchlist.update(args.indicatorId, args.patch ?? {})
|
|
268
|
+
return { status: result.status, item: result.item, items: await useCases.watchlist.list() }
|
|
269
|
+
}
|
|
270
|
+
if (args.action === 'reorder') {
|
|
271
|
+
const result = await useCases.watchlist.reorder(args.ids ?? [])
|
|
272
|
+
return { status: 'reordered', items: result.items, missing: result.missing }
|
|
273
|
+
}
|
|
274
|
+
return { status: 'unknown-action', items: await useCases.watchlist.list() }
|
|
275
|
+
},
|
|
276
|
+
}),
|
|
277
|
+
|
|
278
|
+
defineTool({
|
|
279
|
+
name: 'data_explain',
|
|
280
|
+
description: 'Explain one indicator using only panel data: what it is, what the latest reading means, history, possible implications and reverse risks.',
|
|
281
|
+
parameters: {
|
|
282
|
+
type: 'object',
|
|
283
|
+
additionalProperties: false,
|
|
284
|
+
required: ['indicator'],
|
|
285
|
+
properties: {
|
|
286
|
+
indicator: { type: 'string', description: 'Indicator id or keyword.' },
|
|
287
|
+
range: rangeParam,
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
outputSchema: {
|
|
291
|
+
type: 'object',
|
|
292
|
+
additionalProperties: true,
|
|
293
|
+
required: ['markdown', 'mode'],
|
|
294
|
+
properties: {
|
|
295
|
+
markdown: { type: 'string' },
|
|
296
|
+
mode: { type: 'string' },
|
|
297
|
+
usedPoints: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
render: (_args, value) => [{ type: 'text', text: `${value.markdown}${value.usedPoints?.length ? `\n\n引用了 ${value.usedPoints.length} 个数据点(mode=${value.mode})。` : ''}` }],
|
|
301
|
+
execute: async (args) => {
|
|
302
|
+
const resolved = await resolveIndicatorId(useCases, args.indicator)
|
|
303
|
+
const result = await useCases.ai.explain({ indicatorId: resolved ?? args.indicator, range: args.range ?? '1Y' })
|
|
304
|
+
return result
|
|
305
|
+
},
|
|
306
|
+
}),
|
|
307
|
+
|
|
308
|
+
defineTool({
|
|
309
|
+
name: 'data_digest',
|
|
310
|
+
description: 'Produce a period summary across the panel: key changes, corroboration, contradictions and what to watch next.',
|
|
311
|
+
parameters: {
|
|
312
|
+
type: 'object',
|
|
313
|
+
additionalProperties: false,
|
|
314
|
+
properties: {
|
|
315
|
+
range: rangeParam,
|
|
316
|
+
indicators: { type: 'array', items: { type: 'string' }, description: 'Restrict the digest to these indicator ids.' },
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
outputSchema: {
|
|
320
|
+
type: 'object',
|
|
321
|
+
additionalProperties: true,
|
|
322
|
+
required: ['markdown', 'mode'],
|
|
323
|
+
properties: { markdown: { type: 'string' }, mode: { type: 'string' } },
|
|
324
|
+
},
|
|
325
|
+
render: (_args, value) => [{ type: 'text', text: value.markdown }],
|
|
326
|
+
execute: async (args) => useCases.ai.summarize({ range: args.range ?? '1Y', indicators: args.indicators }),
|
|
327
|
+
}),
|
|
328
|
+
|
|
329
|
+
defineTool({
|
|
330
|
+
name: 'data_refresh',
|
|
331
|
+
description: 'Force a cache refresh for one indicator (or all visible ones). Throttled to once per minute.',
|
|
332
|
+
parameters: {
|
|
333
|
+
type: 'object',
|
|
334
|
+
additionalProperties: false,
|
|
335
|
+
properties: {
|
|
336
|
+
indicator: { type: 'string', description: 'Indicator id; omit to refresh the default overview set.' },
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
outputSchema: {
|
|
340
|
+
type: 'object',
|
|
341
|
+
additionalProperties: true,
|
|
342
|
+
required: ['status'],
|
|
343
|
+
properties: { status: { type: 'string' }, throttled: { type: 'boolean' } },
|
|
344
|
+
},
|
|
345
|
+
render: (_args, value) => [
|
|
346
|
+
{
|
|
347
|
+
type: 'text',
|
|
348
|
+
text: value.throttled
|
|
349
|
+
? `距上次刷新不足 ${REFRESH_THROTTLE_MS / 1000} 秒,已跳过(数据仍是最新的缓存)。`
|
|
350
|
+
: `已刷新:${value.refreshed ?? 0} 项,失败 ${value.failed ?? 0} 项。`,
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
execute: async (args) => {
|
|
354
|
+
const now = clock.now().getTime()
|
|
355
|
+
if (now - lastRefreshAt < REFRESH_THROTTLE_MS) {
|
|
356
|
+
log('data_refresh throttled', { sinceMs: now - lastRefreshAt })
|
|
357
|
+
return { status: 'throttled', throttled: true, sinceMs: now - lastRefreshAt }
|
|
358
|
+
}
|
|
359
|
+
lastRefreshAt = now
|
|
360
|
+
const ids = args.indicator === undefined ? undefined : [args.indicator]
|
|
361
|
+
const result = await useCases.overview({ range: '1Y', ids, force: true, limit: 1 })
|
|
362
|
+
return {
|
|
363
|
+
status: 'refreshed',
|
|
364
|
+
throttled: false,
|
|
365
|
+
refreshed: result.metrics.filter((metric) => metric.status === 'fresh').length,
|
|
366
|
+
failed: result.metrics.filter((metric) => metric.status === 'error').length,
|
|
367
|
+
range: result.range,
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
}),
|
|
371
|
+
|
|
372
|
+
defineTool({
|
|
373
|
+
name: 'data_health',
|
|
374
|
+
description: 'Report per-source availability and the last successful fetch time. Use it to diagnose "why is there no data today".',
|
|
375
|
+
parameters: { type: 'object', additionalProperties: false, properties: {} },
|
|
376
|
+
outputSchema: {
|
|
377
|
+
type: 'object',
|
|
378
|
+
additionalProperties: true,
|
|
379
|
+
required: ['sources'],
|
|
380
|
+
properties: {
|
|
381
|
+
sources: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
render: (_args, value) => [
|
|
385
|
+
{
|
|
386
|
+
type: 'text',
|
|
387
|
+
text: value.sources.map((entry) => `${entry.adapterId}: ${entry.available ? 'ok' : 'failed'}${entry.lastSuccessAt ? ` (last success ${entry.lastSuccessAt})` : ''}`).join('\n'),
|
|
388
|
+
},
|
|
389
|
+
],
|
|
390
|
+
execute: async () => {
|
|
391
|
+
const health = await useCases.health({ probe: false })
|
|
392
|
+
return { sources: health.sources, generatedAt: health.generatedAt, aiMode: health.aiMode }
|
|
393
|
+
},
|
|
394
|
+
}),
|
|
395
|
+
]
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Resolve a user-supplied indicator reference to a catalog id.
|
|
400
|
+
*
|
|
401
|
+
* @param {object} useCases - use cases.
|
|
402
|
+
* @param {string} reference - id or keyword.
|
|
403
|
+
* @returns {Promise<string|undefined>} resolved id.
|
|
404
|
+
*/
|
|
405
|
+
async function resolveIndicatorId(useCases, reference) {
|
|
406
|
+
if (typeof reference !== 'string' || reference.trim() === '') return undefined
|
|
407
|
+
const result = await useCases.search(reference, { limit: 3 })
|
|
408
|
+
const exact = result.matches.find((entry) => entry.id === reference)
|
|
409
|
+
if (exact !== undefined) return exact.id
|
|
410
|
+
return result.matches[0]?.id
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Register every tool, returning one disposer for the whole set.
|
|
415
|
+
*
|
|
416
|
+
* @param {object} deps - dependencies.
|
|
417
|
+
* @param {{ register: (definition: object) => () => void }} deps.tools - tool registry.
|
|
418
|
+
* @param {object} deps.useCases - use cases.
|
|
419
|
+
* @param {import('../ports/clock.js').Clock} deps.clock - clock.
|
|
420
|
+
* @param {(msg: string, meta?: object) => void} [deps.log] - logger.
|
|
421
|
+
* @returns {() => void} disposer.
|
|
422
|
+
*/
|
|
423
|
+
export function registerTools({ tools, useCases, clock, log = () => {} }) {
|
|
424
|
+
const disposers = createToolDefinitions({ useCases, clock, log }).map((definition) => tools.register(definition))
|
|
425
|
+
return () => {
|
|
426
|
+
for (const dispose of disposers.reverse()) dispose()
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** Re-exported so the tool definitions can be inspected in tests. */
|
|
431
|
+
export { summarize }
|
package/lib/host.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// GENERATED FILE — do not edit. Source: src/host/index.js
|
|
2
|
+
// Rebuild with: node scripts/build-host.mjs
|
|
3
|
+
//
|
|
4
|
+
// `lib/` mirrors `src/` so the package is self-contained: the profile resolves
|
|
5
|
+
// this file through package.json's `main`, and every relative import inside the
|
|
6
|
+
// published tree resolves within it.
|
|
7
|
+
export { name, inject, apply, Config, PLUGIN_ROOT } from './host/index.js'
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time source port (docs/02 §3.2). All date arithmetic in 'core/' and 'app/'
|
|
3
|
+
* goes through a Clock so tests are deterministic and timezone-free.
|
|
4
|
+
*
|
|
5
|
+
* @typedef {Object} Clock
|
|
6
|
+
* @property {() => Date} now - current instant.
|
|
7
|
+
* @property {() => string} today - current calendar date as 'YYYY-MM-DD'.
|
|
8
|
+
* @property {(iso: string) => string} toDateString - 'YYYY-MM-DD' of an instant.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Format a 'Date' as a local 'YYYY-MM-DD' calendar date.
|
|
13
|
+
*
|
|
14
|
+
* @param {Date} date - instant to format.
|
|
15
|
+
* @returns {string} 'YYYY-MM-DD'.
|
|
16
|
+
*/
|
|
17
|
+
export function toDateString(date) {
|
|
18
|
+
const y = date.getFullYear()
|
|
19
|
+
const m = String(date.getMonth() + 1).padStart(2, '0')
|
|
20
|
+
const d = String(date.getDate()).padStart(2, '0')
|
|
21
|
+
return `${y}-${m}-${d}`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The system clock: the only place in this package allowed to read real time.
|
|
26
|
+
*
|
|
27
|
+
* @returns {Clock} system-backed clock.
|
|
28
|
+
*/
|
|
29
|
+
export function systemClock() {
|
|
30
|
+
return {
|
|
31
|
+
now: () => new Date(),
|
|
32
|
+
today: () => toDateString(new Date()),
|
|
33
|
+
toDateString: (iso) => toDateString(new Date(iso)),
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A clock pinned to one instant. Tests use this so no assertion depends on the
|
|
39
|
+
* machine's date or timezone.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} iso - pinned instant, e.g. '2026-09-12T00:00:00.000Z'.
|
|
42
|
+
* @returns {Clock & { advance: (ms: number) => void, set: (iso: string) => void }} fixed clock.
|
|
43
|
+
*/
|
|
44
|
+
export function fixedClock(iso) {
|
|
45
|
+
let at = new Date(iso)
|
|
46
|
+
return {
|
|
47
|
+
now: () => new Date(at.getTime()),
|
|
48
|
+
today: () => toDateString(at),
|
|
49
|
+
toDateString: (value) => toDateString(new Date(value)),
|
|
50
|
+
advance: (ms) => {
|
|
51
|
+
at = new Date(at.getTime() + ms)
|
|
52
|
+
},
|
|
53
|
+
set: (value) => {
|
|
54
|
+
at = new Date(value)
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot cache port (docs/02 §3.2, §4.2). Implemented by an fs-backed
|
|
3
|
+
* repository in the host half and by an in-memory one in tests.
|
|
4
|
+
*
|
|
5
|
+
* 'read' returns '{ payload, storedAt, ttlMs }' or 'undefined'; freshness is
|
|
6
|
+
* decided by the use case with an injected Clock, never by the repository.
|
|
7
|
+
*
|
|
8
|
+
* @typedef {Object} CacheEntry
|
|
9
|
+
* @property {any} payload - cached JSON value.
|
|
10
|
+
* @property {string} storedAt - ISO timestamp written by the clock.
|
|
11
|
+
* @property {number} ttlMs - TTL that applied when the entry was written.
|
|
12
|
+
*
|
|
13
|
+
* @typedef {Object} SnapshotRepository
|
|
14
|
+
* @property {(key: string) => Promise<CacheEntry | undefined>} read
|
|
15
|
+
* @property {(key: string, payload: any, opts: { ttlMs: number, at: string }) => Promise<void>} write
|
|
16
|
+
* @property {() => Promise<Array<{ key: string, storedAt: string, ttlMs: number }>>} list
|
|
17
|
+
* @property {() => Promise<void>} clear
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {Object} WatchlistItem
|
|
22
|
+
* @property {string} indicatorId
|
|
23
|
+
* @property {string} [addedAt]
|
|
24
|
+
* @property {'user'|'ai'|'catalog'} [createdBy]
|
|
25
|
+
* @property {number} [order]
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Watchlist persistence port (docs/02 §3.2).
|
|
30
|
+
*
|
|
31
|
+
* @typedef {Object} WatchlistRepository
|
|
32
|
+
* @property {() => Promise<WatchlistItem[]>} list
|
|
33
|
+
* @property {(item: WatchlistItem) => Promise<{ item: WatchlistItem, created: boolean }>} add
|
|
34
|
+
* @property {(indicatorId: string) => Promise<'removed'|'notFound'>} remove
|
|
35
|
+
* @property {(indicatorId: string, patch: Partial<WatchlistItem>) => Promise<WatchlistItem|undefined>} update
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/** @type {SnapshotRepository} */
|
|
39
|
+
export const noopSnapshotRepository = {
|
|
40
|
+
async read() {
|
|
41
|
+
return undefined
|
|
42
|
+
},
|
|
43
|
+
async write() {},
|
|
44
|
+
async list() {
|
|
45
|
+
return []
|
|
46
|
+
},
|
|
47
|
+
async clear() {},
|
|
48
|
+
}
|