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,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A dependency-free 'ToolDefinition' builder (docs/09 §4).
|
|
3
|
+
*
|
|
4
|
+
* The plugin deliberately does **not** import '@deepseek-ai/dsh-tools''s
|
|
5
|
+
* 'defineTool': this package is installed from the session workspace where the
|
|
6
|
+
* profile's 'node_modules' is not resolvable, and pulling a second copy of
|
|
7
|
+
* 'dsh-tools' into the process would fork the tool runtime. Building the
|
|
8
|
+
* definition shape by hand keeps the plugin dependency-free — the schema subset
|
|
9
|
+
* and the argument validation it would have provided are implemented here and
|
|
10
|
+
* covered by 'test/host/tools.test.js'.
|
|
11
|
+
*
|
|
12
|
+
* @module host/tools/define-tool
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** JSON Schema keywords the registry enforces. */
|
|
16
|
+
const ALLOWED_TYPES = ['object', 'array', 'string', 'number', 'integer', 'boolean', 'null']
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Validate a raw JSON Schema node against the enforced subset.
|
|
20
|
+
*
|
|
21
|
+
* @param {any} schema - candidate schema.
|
|
22
|
+
* @param {string} path - path for error messages.
|
|
23
|
+
* @returns {string[]} violations (empty when supported).
|
|
24
|
+
*/
|
|
25
|
+
export function schemaViolations(schema, path = 'schema') {
|
|
26
|
+
const violations = []
|
|
27
|
+
if (schema === null || typeof schema !== 'object' || Array.isArray(schema)) {
|
|
28
|
+
return [`${path} must be an object`]
|
|
29
|
+
}
|
|
30
|
+
if (schema.type !== undefined && !ALLOWED_TYPES.includes(schema.type)) {
|
|
31
|
+
violations.push(`${path}.type must be one of ${ALLOWED_TYPES.join('/')}`)
|
|
32
|
+
}
|
|
33
|
+
if (schema.properties !== undefined) {
|
|
34
|
+
if (schema.type !== 'object') violations.push(`${path}.properties requires type: 'object'`)
|
|
35
|
+
if (schema.properties === null || typeof schema.properties !== 'object') {
|
|
36
|
+
violations.push(`${path}.properties must be an object`)
|
|
37
|
+
} else {
|
|
38
|
+
for (const [key, child] of Object.entries(schema.properties)) {
|
|
39
|
+
violations.push(...schemaViolations(child, `${path}.properties.${key}`))
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (schema.required !== undefined) {
|
|
44
|
+
if (!Array.isArray(schema.required) || schema.required.some((entry) => typeof entry !== 'string')) {
|
|
45
|
+
violations.push(`${path}.required must be an array of strings`)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (schema.items !== undefined) {
|
|
49
|
+
if (schema.type !== 'array') violations.push(`${path}.items requires type: 'array'`)
|
|
50
|
+
violations.push(...schemaViolations(schema.items, `${path}.items`))
|
|
51
|
+
} else if (schema.type === 'array') {
|
|
52
|
+
// An array without `items` is legal (unconstrained entries) but almost always
|
|
53
|
+
// an authoring slip, so it is reported as a warning-level violation only when
|
|
54
|
+
// the schema also declares nothing else. We keep it legal: several tools
|
|
55
|
+
// return genuinely heterogeneous arrays.
|
|
56
|
+
}
|
|
57
|
+
if (schema.enum !== undefined && !Array.isArray(schema.enum)) {
|
|
58
|
+
violations.push(`${path}.enum must be an array`)
|
|
59
|
+
}
|
|
60
|
+
if (schema.oneOf !== undefined) {
|
|
61
|
+
if (!Array.isArray(schema.oneOf) || schema.oneOf.length !== 1) {
|
|
62
|
+
// The registry's subset allows exactly-one `oneOf`; we never emit it, so a
|
|
63
|
+
// malformed one here is an author error worth reporting.
|
|
64
|
+
violations.push(`${path}.oneOf must be an array with exactly one entry`)
|
|
65
|
+
} else {
|
|
66
|
+
violations.push(...schemaViolations(schema.oneOf[0], `${path}.oneOf[0]`))
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const known = new Set(['type', 'properties', 'required', 'additionalProperties', 'items', 'enum', 'const', 'oneOf', 'description', 'title', 'default', 'examples'])
|
|
70
|
+
for (const key of Object.keys(schema)) {
|
|
71
|
+
if (!known.has(key)) violations.push(`${path}.${key} is not a supported keyword`)
|
|
72
|
+
}
|
|
73
|
+
return violations
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Validate a value against the JSON Schema subset.
|
|
78
|
+
*
|
|
79
|
+
* @param {any} schema - schema.
|
|
80
|
+
* @param {any} value - candidate value.
|
|
81
|
+
* @param {string} [path] - path for error messages.
|
|
82
|
+
* @returns {string[]} violations.
|
|
83
|
+
*/
|
|
84
|
+
export function valueViolations(schema, value, path = '') {
|
|
85
|
+
const violations = []
|
|
86
|
+
if (schema === undefined || schema === null) return violations
|
|
87
|
+
if (schema.oneOf !== undefined) {
|
|
88
|
+
const passes = schema.oneOf.filter((candidate) => valueViolations(candidate, value, path).length === 0)
|
|
89
|
+
if (passes.length !== 1) violations.push(`${path || 'value'} must match exactly one allowed shape`)
|
|
90
|
+
return violations
|
|
91
|
+
}
|
|
92
|
+
if (schema.enum !== undefined && !schema.enum.includes(value)) {
|
|
93
|
+
violations.push(`${path || 'value'} must be one of ${schema.enum.map((entry) => JSON.stringify(entry)).join(', ')}`)
|
|
94
|
+
return violations
|
|
95
|
+
}
|
|
96
|
+
if (schema.const !== undefined && value !== schema.const) {
|
|
97
|
+
violations.push(`${path || 'value'} must be ${JSON.stringify(schema.const)}`)
|
|
98
|
+
return violations
|
|
99
|
+
}
|
|
100
|
+
const type = schema.type
|
|
101
|
+
if (type === 'object') {
|
|
102
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
103
|
+
violations.push(`${path || 'value'} must be an object`)
|
|
104
|
+
return violations
|
|
105
|
+
}
|
|
106
|
+
for (const key of schema.required ?? []) {
|
|
107
|
+
if (!(key in value)) violations.push(`${path ? `${path}.` : ''}${key} is required`)
|
|
108
|
+
}
|
|
109
|
+
const properties = schema.properties ?? {}
|
|
110
|
+
if (schema.additionalProperties === false) {
|
|
111
|
+
for (const key of Object.keys(value)) {
|
|
112
|
+
if (!(key in properties)) violations.push(`${path ? `${path}.` : ''}${key} is not allowed`)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const [key, child] of Object.entries(properties)) {
|
|
116
|
+
if (key in value) violations.push(...valueViolations(child, value[key], `${path ? `${path}.` : ''}${key}`))
|
|
117
|
+
}
|
|
118
|
+
return violations
|
|
119
|
+
}
|
|
120
|
+
if (type === 'array') {
|
|
121
|
+
if (!Array.isArray(value)) {
|
|
122
|
+
violations.push(`${path || 'value'} must be an array`)
|
|
123
|
+
return violations
|
|
124
|
+
}
|
|
125
|
+
if (schema.items !== undefined) {
|
|
126
|
+
value.forEach((entry, index) => violations.push(...valueViolations(schema.items, entry, `${path}[${index}]`)))
|
|
127
|
+
}
|
|
128
|
+
return violations
|
|
129
|
+
}
|
|
130
|
+
if (type === 'string' && typeof value !== 'string') violations.push(`${path || 'value'} must be a string`)
|
|
131
|
+
if (type === 'number' && (typeof value !== 'number' || !Number.isFinite(value))) violations.push(`${path || 'value'} must be a finite number`)
|
|
132
|
+
if (type === 'integer' && !Number.isInteger(value)) violations.push(`${path || 'value'} must be an integer`)
|
|
133
|
+
if (type === 'boolean' && typeof value !== 'boolean') violations.push(`${path || 'value'} must be a boolean`)
|
|
134
|
+
if (type === 'null' && value !== null) violations.push(`${path || 'value'} must be null`)
|
|
135
|
+
return violations
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Drop `undefined` object properties and array holes from a tool value.
|
|
140
|
+
*
|
|
141
|
+
* Arrays keep their length (a hole becomes `null`) so a point series never
|
|
142
|
+
* silently shifts; objects simply lose the absent keys, which is what
|
|
143
|
+
* `JSON.stringify` would have done anyway — except that the harness snapshots
|
|
144
|
+
* the live value, where an own property set to `undefined` is an error rather
|
|
145
|
+
* than an omission.
|
|
146
|
+
*
|
|
147
|
+
* @param {any} value - candidate value.
|
|
148
|
+
* @returns {any} a JSON-safe copy.
|
|
149
|
+
*/
|
|
150
|
+
export function dropUndefined(value) {
|
|
151
|
+
if (value === null || typeof value !== 'object') return value
|
|
152
|
+
if (Array.isArray(value)) {
|
|
153
|
+
return value.map((entry) => (entry === undefined ? null : dropUndefined(entry)))
|
|
154
|
+
}
|
|
155
|
+
/** @type {Record<string, any>} */
|
|
156
|
+
const out = {}
|
|
157
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
158
|
+
if (entry === undefined) continue
|
|
159
|
+
out[key] = dropUndefined(entry)
|
|
160
|
+
}
|
|
161
|
+
return out
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Find the first value the harness's lossless-JSON boundary would reject.
|
|
166
|
+
*
|
|
167
|
+
* A tool body's canonical value is snapshotted by the registry before it is
|
|
168
|
+
* stored, rendered or sent to the model, and that snapshot accepts only ordinary
|
|
169
|
+
* arrays, plain objects and JSON scalars: `undefined`, `NaN`, `Infinity`,
|
|
170
|
+
* `-0`, sparse arrays, `Date`, class instances and cycles are all refused. When
|
|
171
|
+
* that happens the model sees nothing but `value is not lossless JSON`, with no
|
|
172
|
+
* hint of which field caused it — so the plugin checks the same rule first and
|
|
173
|
+
* names the offending path itself.
|
|
174
|
+
*
|
|
175
|
+
* @param {any} value - candidate value.
|
|
176
|
+
* @param {string} [path] - path of `value`, for the message.
|
|
177
|
+
* @param {Set<object>} [seen] - ancestors on the current path.
|
|
178
|
+
* @returns {string|undefined} the offending path.
|
|
179
|
+
*/
|
|
180
|
+
export function findNonLossless(value, path = 'value', seen = new Set()) {
|
|
181
|
+
if (value === null) return undefined
|
|
182
|
+
const type = typeof value
|
|
183
|
+
if (type === 'string' || type === 'boolean') return undefined
|
|
184
|
+
if (type === 'number') {
|
|
185
|
+
if (!Number.isFinite(value)) return `${path} is ${String(value)} (neither is JSON)`
|
|
186
|
+
if (Object.is(value, -0)) return `${path} is -0 (not lossless)`
|
|
187
|
+
return undefined
|
|
188
|
+
}
|
|
189
|
+
if (type === 'undefined') return `${path} is undefined (dropped by JSON, refused at the boundary)`
|
|
190
|
+
if (type === 'bigint' || type === 'function' || type === 'symbol') return `${path} is a ${type}`
|
|
191
|
+
if (seen.has(value)) return `${path} is a cycle`
|
|
192
|
+
seen.add(value)
|
|
193
|
+
try {
|
|
194
|
+
if (Array.isArray(value)) {
|
|
195
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
196
|
+
if (!Object.prototype.hasOwnProperty.call(value, index)) return `${path}[${index}] is a hole (sparse array)`
|
|
197
|
+
const found = findNonLossless(value[index], `${path}[${index}]`, seen)
|
|
198
|
+
if (found !== undefined) return found
|
|
199
|
+
}
|
|
200
|
+
return undefined
|
|
201
|
+
}
|
|
202
|
+
const prototype = Object.getPrototypeOf(value)
|
|
203
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
204
|
+
return `${path} is a ${value?.constructor?.name ?? 'non-plain object'} (only plain objects survive)`
|
|
205
|
+
}
|
|
206
|
+
for (const key of Object.keys(value)) {
|
|
207
|
+
const found = findNonLossless(value[key], `${path}.${key}`, seen)
|
|
208
|
+
if (found !== undefined) return found
|
|
209
|
+
}
|
|
210
|
+
return undefined
|
|
211
|
+
} finally {
|
|
212
|
+
seen.delete(value)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Build a 'ToolDefinition' the registry accepts.
|
|
218
|
+
*
|
|
219
|
+
* @param {object} options - tool options.
|
|
220
|
+
* @param {string} options.name - tool name.
|
|
221
|
+
* @param {string} options.description - model-facing description.
|
|
222
|
+
* @param {Record<string, any>} options.parameters - argument JSON Schema (object root).
|
|
223
|
+
* @param {any} options.outputSchema - canonical output JSON Schema.
|
|
224
|
+
* @param {(args: any, value: any) => Array<{ type: string, text: string }>} options.render - content projection.
|
|
225
|
+
* @param {(args: any, exec: any) => Promise<any>} options.execute - tool body.
|
|
226
|
+
* @param {number} [options.timeoutMs] - cooperative timeout budget.
|
|
227
|
+
* @param {boolean} [options.concurrencySafe] - whether parallel dispatch is allowed.
|
|
228
|
+
* @returns {object} tool definition.
|
|
229
|
+
*/
|
|
230
|
+
export function defineTool({ name, description, parameters, outputSchema, render, execute, timeoutMs, concurrencySafe }) {
|
|
231
|
+
const parameterViolations = schemaViolations(parameters, `${name}.parameters`)
|
|
232
|
+
if (parameters?.type !== 'object') parameterViolations.push(`${name}.parameters must be an object schema`)
|
|
233
|
+
const outputViolations = schemaViolations(outputSchema, `${name}.output.schema`)
|
|
234
|
+
const violations = [...parameterViolations, ...outputViolations]
|
|
235
|
+
if (violations.length > 0) throw new Error(`defineTool(${name}): ${violations.join('; ')}`)
|
|
236
|
+
if (typeof render !== 'function') throw new Error(`defineTool(${name}): output.render is required`)
|
|
237
|
+
if (typeof execute !== 'function') throw new Error(`defineTool(${name}): execute is required`)
|
|
238
|
+
|
|
239
|
+
const definition = {
|
|
240
|
+
name,
|
|
241
|
+
description,
|
|
242
|
+
parameters,
|
|
243
|
+
output: {
|
|
244
|
+
schema: outputSchema,
|
|
245
|
+
render(args, value) {
|
|
246
|
+
return render(args, value)
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
async execute(args, exec) {
|
|
250
|
+
const argsViolations = valueViolations(parameters, args, '')
|
|
251
|
+
if (argsViolations.length > 0) {
|
|
252
|
+
const error = new Error(`invalid arguments: ${argsViolations.join('; ')}`)
|
|
253
|
+
error.name = 'ToolArgsError'
|
|
254
|
+
throw error
|
|
255
|
+
}
|
|
256
|
+
const raw = await execute(args ?? {}, exec)
|
|
257
|
+
// `undefined` is not a JSON value, but it is easy to produce: an optional
|
|
258
|
+
// field that simply is not there (`compare`, `error`, `measuredAt`) used to
|
|
259
|
+
// travel as an own property set to undefined, and the harness's snapshot
|
|
260
|
+
// then rejected the *whole* tool result with "value is not lossless JSON" —
|
|
261
|
+
// the model got no data at all and no way to tell why. Drop those keys
|
|
262
|
+
// first, then validate what is left.
|
|
263
|
+
const value = dropUndefined(raw)
|
|
264
|
+
const valueViolationsFound = valueViolations(outputSchema, value, '')
|
|
265
|
+
if (valueViolationsFound.length > 0) {
|
|
266
|
+
const error = new Error(`tool ${name} violated its declared output: ${valueViolationsFound.join('; ')}`)
|
|
267
|
+
error.name = 'ToolOutputError'
|
|
268
|
+
throw error
|
|
269
|
+
}
|
|
270
|
+
const nonLossless = findNonLossless(value, `${name}.output`)
|
|
271
|
+
if (nonLossless !== undefined) {
|
|
272
|
+
const error = new Error(`tool ${name} returned a value the harness cannot snapshot: ${nonLossless}`)
|
|
273
|
+
error.name = 'ToolOutputError'
|
|
274
|
+
throw error
|
|
275
|
+
}
|
|
276
|
+
return value
|
|
277
|
+
},
|
|
278
|
+
}
|
|
279
|
+
if (typeof timeoutMs === 'number') definition.timeoutMs = timeoutMs
|
|
280
|
+
definition.isConcurrencySafe = () => concurrencySafe === true
|
|
281
|
+
return definition
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Render one JSON tool result as model-facing text, keeping it readable in a
|
|
286
|
+
* transcript rather than dumping raw JSON for every call.
|
|
287
|
+
*
|
|
288
|
+
* @param {any} value - canonical value.
|
|
289
|
+
* @param {number} [maxChars] - truncation budget.
|
|
290
|
+
* @returns {string} text.
|
|
291
|
+
*/
|
|
292
|
+
export function summarize(value, maxChars = 4000) {
|
|
293
|
+
const text = typeof value === 'string' ? value : JSON.stringify(value, null, 2)
|
|
294
|
+
return text.length <= maxChars ? text : `${text.slice(0, maxChars)}\n… (结果已截断,完整内容见工具卡片)`
|
|
295
|
+
}
|