echopai 2.7.0 → 2.8.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/dist/bin.js +286 -98
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -574,6 +574,28 @@ var OPERATIONS = {
|
|
|
574
574
|
]
|
|
575
575
|
}
|
|
576
576
|
},
|
|
577
|
+
"capabilities.show": {
|
|
578
|
+
cliKey: "capabilities.show",
|
|
579
|
+
cliName: "capabilities show",
|
|
580
|
+
method: "GET",
|
|
581
|
+
path: "/v1/capabilities",
|
|
582
|
+
description: "Returns the current API spec version plus a machine-readable changelog of capability changes (new endpoints, new fields, behavior fixes), newest first. Check it at session start — or after an unexpected response shape — to discover capabilities added since your integration was written. Free and unauthenticated.",
|
|
583
|
+
summary: "Capability changelog and API version discovery.",
|
|
584
|
+
positional: [],
|
|
585
|
+
outputDefault: "json",
|
|
586
|
+
pagination: "none",
|
|
587
|
+
stream: false,
|
|
588
|
+
billable: false,
|
|
589
|
+
idempotencyRequired: false,
|
|
590
|
+
scopesAny: [],
|
|
591
|
+
sideEffect: "read",
|
|
592
|
+
dryRunSupported: false,
|
|
593
|
+
inputSchema: {
|
|
594
|
+
type: "object",
|
|
595
|
+
properties: {},
|
|
596
|
+
additionalProperties: false
|
|
597
|
+
}
|
|
598
|
+
},
|
|
577
599
|
"concepts.alerts": {
|
|
578
600
|
cliKey: "concepts.alerts",
|
|
579
601
|
cliName: "concepts alerts",
|
|
@@ -1437,7 +1459,7 @@ var OPERATIONS = {
|
|
|
1437
1459
|
include_broken: {
|
|
1438
1460
|
type: "boolean",
|
|
1439
1461
|
default: false,
|
|
1440
|
-
description: "true
|
|
1462
|
+
description: "true 时连真炸板(盘中封板后跌出、当前未回封,status=broken)也一起返回;默认只返回当前封板 status=limit_up。"
|
|
1441
1463
|
}
|
|
1442
1464
|
},
|
|
1443
1465
|
additionalProperties: false
|
|
@@ -1516,7 +1538,7 @@ var OPERATIONS = {
|
|
|
1516
1538
|
cliName: "news feed",
|
|
1517
1539
|
method: "GET",
|
|
1518
1540
|
path: "/v1/news",
|
|
1519
|
-
description: "Returns recent news. Equivalent to `/v1/news/list`; retained for backward compatibility.",
|
|
1541
|
+
description: "Returns recent news. Equivalent to `/v1/news/list`; retained for backward compatibility. Partner/agent callers receive `fields=compact` by default (id, title, ai_summary, tagged securities, first ~300 chars of content); pass `fields=full` for complete content.",
|
|
1520
1542
|
summary: "List recent news.",
|
|
1521
1543
|
positional: [],
|
|
1522
1544
|
outputDefault: "json",
|
|
@@ -1529,7 +1551,16 @@ var OPERATIONS = {
|
|
|
1529
1551
|
dryRunSupported: false,
|
|
1530
1552
|
inputSchema: {
|
|
1531
1553
|
type: "object",
|
|
1532
|
-
properties: {
|
|
1554
|
+
properties: {
|
|
1555
|
+
fields: {
|
|
1556
|
+
type: "string",
|
|
1557
|
+
enum: [
|
|
1558
|
+
"compact",
|
|
1559
|
+
"full"
|
|
1560
|
+
],
|
|
1561
|
+
description: "Response shape: `compact` (the default for partner/agent callers) returns id, title, ai_summary, tagged securities, and the first ~300 chars of content with a `content_truncated` flag; `full` returns complete content."
|
|
1562
|
+
}
|
|
1563
|
+
},
|
|
1533
1564
|
additionalProperties: false
|
|
1534
1565
|
}
|
|
1535
1566
|
},
|
|
@@ -1621,6 +1652,14 @@ var OPERATIONS = {
|
|
|
1621
1652
|
minimum: 0,
|
|
1622
1653
|
default: 0,
|
|
1623
1654
|
description: "Pagination offset (page through long history ranges)."
|
|
1655
|
+
},
|
|
1656
|
+
fields: {
|
|
1657
|
+
type: "string",
|
|
1658
|
+
enum: [
|
|
1659
|
+
"compact",
|
|
1660
|
+
"full"
|
|
1661
|
+
],
|
|
1662
|
+
description: "Response shape: `compact` (the default for partner/agent callers) returns id, title/ai_summary, attribution, tagged securities, and the first ~300 chars of content with a `content_truncated` flag; `full` returns complete content. Prefer compact for scanning; fetch full text per item only when needed."
|
|
1624
1663
|
}
|
|
1625
1664
|
},
|
|
1626
1665
|
additionalProperties: false,
|
|
@@ -1686,6 +1725,14 @@ var OPERATIONS = {
|
|
|
1686
1725
|
minimum: 0,
|
|
1687
1726
|
default: 0,
|
|
1688
1727
|
description: "Pagination offset (page through long history ranges)."
|
|
1728
|
+
},
|
|
1729
|
+
fields: {
|
|
1730
|
+
type: "string",
|
|
1731
|
+
enum: [
|
|
1732
|
+
"compact",
|
|
1733
|
+
"full"
|
|
1734
|
+
],
|
|
1735
|
+
description: "Response shape: `compact` (the default for partner/agent callers) returns id, title/ai_summary, attribution, tagged securities, and the first ~300 chars of content with a `content_truncated` flag; `full` returns complete content. Prefer compact for scanning; fetch full text per item only when needed."
|
|
1689
1736
|
}
|
|
1690
1737
|
},
|
|
1691
1738
|
additionalProperties: false,
|
|
@@ -1800,7 +1847,7 @@ var OPERATIONS = {
|
|
|
1800
1847
|
cliName: "quote",
|
|
1801
1848
|
method: "GET",
|
|
1802
1849
|
path: "/v1/quote/realtime",
|
|
1803
|
-
description: "Returns real-time quotes for one or more A-share securities (canonical codes, e.g. `SSE:600519`): last price, volume, percent change, and bid/ask. Up to 200 codes per call. Requires a `quote:l1`, `quote:l2`, or `quote:delayed` scope.",
|
|
1850
|
+
description: "Returns real-time quotes for one or more A-share securities (canonical codes, e.g. `SSE:600519`): last price, volume, percent change, and bid/ask. Up to 200 codes per call. The response carries a `session` label (`pre_open` / `auction` / `open` / `break` / `closed`) and, outside continuous trading, an explanatory `note`: during 9:00–9:14 pre-open the previous session's snapshot is returned; during the 9:15–9:29 call auction, prices are indicative auction match prices. Rows whose feed has stopped updating (suspension / feed drop) keep their last snapshot and are flagged `stale: true` with `stale_since`. Requires a `quote:l1`, `quote:l2`, or `quote:delayed` scope.",
|
|
1804
1851
|
summary: "Get real-time quotes for one or more securities.",
|
|
1805
1852
|
positional: [],
|
|
1806
1853
|
outputDefault: "json",
|
|
@@ -1851,7 +1898,7 @@ var OPERATIONS = {
|
|
|
1851
1898
|
cliName: "quote scan",
|
|
1852
1899
|
method: "GET",
|
|
1853
1900
|
path: "/v1/quote/scan",
|
|
1854
|
-
description: "Returns a real-time quote for every A-share in a single call — use it for full-market scans or to discover a universe of interest when you do not yet know which codes to query. Filter with `exchange=SSE|SZSE|BSE` to narrow to one exchange. The payload is large (~3 MB, ~5,800 instruments), so pair it with `--fields`, `--jq`, or `--max-bytes` to trim it.
|
|
1901
|
+
description: "Returns a real-time quote for every A-share in a single call — use it for full-market scans or to discover a universe of interest when you do not yet know which codes to query. Filter with `exchange=SSE|SZSE|BSE` to narrow to one exchange. The payload is large (~3 MB, ~5,800 instruments), so pair it with `--fields`, `--jq`, or `--max-bytes` to trim it. The response carries a `session` label (`pre_open` / `auction` / `open` / `break` / `closed`) plus an explanatory `note` outside continuous trading: during 9:00–9:14 pre-open the previous session's snapshot is returned; during the 9:15–9:29 call auction, prices are indicative auction match prices. Instruments whose feed has stopped updating (suspension / feed drop) are excluded from the default scan; pass `include=all` to keep them, flagged with `stale: true` and `stale_since`. Each item also includes `main_net_in` (current-day cumulative net main-capital inflow in CNY, which may be negative) and a companion `main_net_in_stale` flag (true when served from the last-close fallback). Requires a `quote:l1`, `quote:l2`, or `quote:delayed` scope.",
|
|
1855
1902
|
summary: "Snapshot every A-share real-time quote in one call.",
|
|
1856
1903
|
positional: [],
|
|
1857
1904
|
outputDefault: "json",
|
|
@@ -1880,6 +1927,15 @@ var OPERATIONS = {
|
|
|
1880
1927
|
"bse"
|
|
1881
1928
|
],
|
|
1882
1929
|
description: "Filter by exchange: SSE / SZSE / BSE (case-insensitive). Omit for all."
|
|
1930
|
+
},
|
|
1931
|
+
include: {
|
|
1932
|
+
type: "string",
|
|
1933
|
+
enum: [
|
|
1934
|
+
"active",
|
|
1935
|
+
"all"
|
|
1936
|
+
],
|
|
1937
|
+
default: "active",
|
|
1938
|
+
description: "`active` (default) excludes ST, delisted, and stale (feed-dropped / suspended) instruments; `all` returns the full set with stale rows flagged."
|
|
1883
1939
|
}
|
|
1884
1940
|
},
|
|
1885
1941
|
additionalProperties: false
|
|
@@ -2259,7 +2315,7 @@ var OPERATIONS = {
|
|
|
2259
2315
|
cliName: "views feed",
|
|
2260
2316
|
method: "GET",
|
|
2261
2317
|
path: "/v1/views",
|
|
2262
|
-
description: "Returns a stream of analyst views, sell-side research, and long-form opinion. Filter by institution, analyst, security, or recency (`hours`). Requires the `views:read` scope.",
|
|
2318
|
+
description: "Returns a stream of analyst views, sell-side research, and long-form opinion. Filter by institution, analyst, security, or recency (`hours`). Partner/agent callers receive `fields=compact` by default (id, ai_summary, attribution, tagged securities, first ~300 chars of content); pass `fields=full` for complete content. Requires the `views:read` scope.",
|
|
2263
2319
|
summary: "List analyst views.",
|
|
2264
2320
|
positional: [],
|
|
2265
2321
|
outputDefault: "json",
|
|
@@ -2272,7 +2328,16 @@ var OPERATIONS = {
|
|
|
2272
2328
|
dryRunSupported: false,
|
|
2273
2329
|
inputSchema: {
|
|
2274
2330
|
type: "object",
|
|
2275
|
-
properties: {
|
|
2331
|
+
properties: {
|
|
2332
|
+
fields: {
|
|
2333
|
+
type: "string",
|
|
2334
|
+
enum: [
|
|
2335
|
+
"compact",
|
|
2336
|
+
"full"
|
|
2337
|
+
],
|
|
2338
|
+
description: "Response shape: `compact` (the default for partner/agent callers) returns id, ai_summary, attribution, tagged securities, and the first ~300 chars of content with a `content_truncated` flag; `full` returns complete content."
|
|
2339
|
+
}
|
|
2340
|
+
},
|
|
2276
2341
|
additionalProperties: false
|
|
2277
2342
|
}
|
|
2278
2343
|
},
|
|
@@ -2372,6 +2437,14 @@ var OPERATIONS = {
|
|
|
2372
2437
|
minimum: 0,
|
|
2373
2438
|
default: 0,
|
|
2374
2439
|
description: "Pagination offset (page through long history ranges)."
|
|
2440
|
+
},
|
|
2441
|
+
fields: {
|
|
2442
|
+
type: "string",
|
|
2443
|
+
enum: [
|
|
2444
|
+
"compact",
|
|
2445
|
+
"full"
|
|
2446
|
+
],
|
|
2447
|
+
description: "Response shape: `compact` (the default for partner/agent callers) returns id, title/ai_summary, attribution, tagged securities, and the first ~300 chars of content with a `content_truncated` flag; `full` returns complete content. Prefer compact for scanning; fetch full text per item only when needed."
|
|
2375
2448
|
}
|
|
2376
2449
|
},
|
|
2377
2450
|
additionalProperties: false
|
|
@@ -2490,6 +2563,14 @@ function buildCommandTree(program, dispatch) {
|
|
|
2490
2563
|
attachOperation(cmd, OPERATIONS["bars.minute-batch"], dispatch);
|
|
2491
2564
|
}
|
|
2492
2565
|
}
|
|
2566
|
+
{
|
|
2567
|
+
const noun = program.command("capabilities");
|
|
2568
|
+
noun.description("capabilities commands");
|
|
2569
|
+
{
|
|
2570
|
+
const cmd = noun.command("show");
|
|
2571
|
+
attachOperation(cmd, OPERATIONS["capabilities.show"], dispatch);
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2493
2574
|
{
|
|
2494
2575
|
const noun = program.command("concepts");
|
|
2495
2576
|
noun.description("Thematic concept boards: constituents, quotes, and alerts.");
|
|
@@ -3349,6 +3430,22 @@ function buildHttpHeaders(ctx) {
|
|
|
3349
3430
|
function resolveRequestId(serverHeader, clientGenerated) {
|
|
3350
3431
|
return serverHeader && serverHeader.length > 0 ? serverHeader : clientGenerated;
|
|
3351
3432
|
}
|
|
3433
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
3434
|
+
var WHOAMI_TIMEOUT_MS = 8000;
|
|
3435
|
+
async function fetchWithTimeout(fn, url, init, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
3436
|
+
const ac = new AbortController;
|
|
3437
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
3438
|
+
try {
|
|
3439
|
+
return await fn(url, { ...init, signal: ac.signal });
|
|
3440
|
+
} catch (e) {
|
|
3441
|
+
if (ac.signal.aborted) {
|
|
3442
|
+
throw new Error(`request timed out after ${timeoutMs}ms: ${url}`);
|
|
3443
|
+
}
|
|
3444
|
+
throw e;
|
|
3445
|
+
} finally {
|
|
3446
|
+
clearTimeout(timer);
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3352
3449
|
|
|
3353
3450
|
// src/runtime/trace.ts
|
|
3354
3451
|
import {
|
|
@@ -3479,7 +3576,10 @@ async function paginate(op, initialParams, ctx, write = writeStdout) {
|
|
|
3479
3576
|
process.stderr.write(`> [page ${pages + 1}] ${op.method} ${url}
|
|
3480
3577
|
`);
|
|
3481
3578
|
}
|
|
3482
|
-
const res = await fetchFn
|
|
3579
|
+
const res = await fetchWithTimeout(fetchFn, url, {
|
|
3580
|
+
method: op.method,
|
|
3581
|
+
headers
|
|
3582
|
+
});
|
|
3483
3583
|
const body = await res.text();
|
|
3484
3584
|
let json = null;
|
|
3485
3585
|
try {
|
|
@@ -3619,7 +3719,7 @@ import os2 from "node:os";
|
|
|
3619
3719
|
import path2 from "node:path";
|
|
3620
3720
|
|
|
3621
3721
|
// src/version.ts
|
|
3622
|
-
var CLI_VERSION = "2.
|
|
3722
|
+
var CLI_VERSION = "2.8.1";
|
|
3623
3723
|
|
|
3624
3724
|
// src/runtime/update_check.ts
|
|
3625
3725
|
var UPDATE_CACHE_PATH = path2.join(os2.homedir(), ".config", "echopai", "update_cache.json");
|
|
@@ -3752,9 +3852,10 @@ async function invoke(op, args, ctx) {
|
|
|
3752
3852
|
}
|
|
3753
3853
|
throw e;
|
|
3754
3854
|
}
|
|
3855
|
+
const opOwnedFlags = new Set(Object.keys(op.inputSchema.properties).filter((k) => SYSTEM_FLAGS.has(k)));
|
|
3755
3856
|
const apiParams = {};
|
|
3756
3857
|
for (const [k, v] of Object.entries(args)) {
|
|
3757
|
-
if (SYSTEM_FLAGS.has(k))
|
|
3858
|
+
if (SYSTEM_FLAGS.has(k) && !opOwnedFlags.has(k))
|
|
3758
3859
|
continue;
|
|
3759
3860
|
if (v === undefined || v === "")
|
|
3760
3861
|
continue;
|
|
@@ -3861,7 +3962,7 @@ async function invoke(op, args, ctx) {
|
|
|
3861
3962
|
const startedAt = Date.now();
|
|
3862
3963
|
let res;
|
|
3863
3964
|
try {
|
|
3864
|
-
res = await fetch
|
|
3965
|
+
res = await fetchWithTimeout(fetch, url, init);
|
|
3865
3966
|
} catch (e) {
|
|
3866
3967
|
trace(op, { exit_code: 2, error_code: "network_error" });
|
|
3867
3968
|
writeError("network_error", e instanceof Error ? e.message : String(e), 2);
|
|
@@ -3921,9 +4022,10 @@ async function invoke(op, args, ctx) {
|
|
|
3921
4022
|
let envelope = rawEnvelope;
|
|
3922
4023
|
try {
|
|
3923
4024
|
const jq = typeof args.jq === "string" ? args.jq : undefined;
|
|
4025
|
+
const fieldsProjection = opOwnedFlags.has("fields") ? undefined : parseFieldsFlag(args.fields);
|
|
3924
4026
|
envelope = applyFilters(rawEnvelope, {
|
|
3925
4027
|
...jq ? { query: jq } : {},
|
|
3926
|
-
...
|
|
4028
|
+
...fieldsProjection ? { fields: fieldsProjection } : {},
|
|
3927
4029
|
...parseMaxBytesFlag(args.max_bytes) ? { maxBytes: parseMaxBytesFlag(args.max_bytes) } : {}
|
|
3928
4030
|
});
|
|
3929
4031
|
} catch (e) {
|
|
@@ -4089,7 +4191,7 @@ async function getWhoami(ctx, opts) {
|
|
|
4089
4191
|
if (inflight && inflight.key === key) {
|
|
4090
4192
|
return inflight.promise;
|
|
4091
4193
|
}
|
|
4092
|
-
const promise = doFetch(ctx, opts?.fetchImpl).then((resp) => {
|
|
4194
|
+
const promise = doFetch(ctx, opts?.fetchImpl, opts?.requestTimeoutMs).then((resp) => {
|
|
4093
4195
|
cache = { key, resp, expiresAt: Date.now() + ttlMs };
|
|
4094
4196
|
return resp;
|
|
4095
4197
|
}).finally(() => {
|
|
@@ -4099,7 +4201,7 @@ async function getWhoami(ctx, opts) {
|
|
|
4099
4201
|
inflight = { key, promise };
|
|
4100
4202
|
return promise;
|
|
4101
4203
|
}
|
|
4102
|
-
async function doFetch(ctx, fetchImpl) {
|
|
4204
|
+
async function doFetch(ctx, fetchImpl, requestTimeoutMs = WHOAMI_TIMEOUT_MS) {
|
|
4103
4205
|
const fn = fetchImpl ?? fetch;
|
|
4104
4206
|
const url = ctx.baseUrl.replace(/\/+$/, "") + "/v1/auth/whoami";
|
|
4105
4207
|
const { headers, requestId: clientRequestId } = buildHttpHeaders({
|
|
@@ -4109,7 +4211,7 @@ async function doFetch(ctx, fetchImpl) {
|
|
|
4109
4211
|
});
|
|
4110
4212
|
let res;
|
|
4111
4213
|
try {
|
|
4112
|
-
res = await fn
|
|
4214
|
+
res = await fetchWithTimeout(fn, url, { method: "GET", headers }, requestTimeoutMs);
|
|
4113
4215
|
} catch (e) {
|
|
4114
4216
|
throw new CallApiError({
|
|
4115
4217
|
code: "network_error",
|
|
@@ -4255,7 +4357,7 @@ async function callOp(op, args, ctx) {
|
|
|
4255
4357
|
const startedAt = Date.now();
|
|
4256
4358
|
let res;
|
|
4257
4359
|
try {
|
|
4258
|
-
res = await fetchFn
|
|
4360
|
+
res = await fetchWithTimeout(fetchFn, url, init);
|
|
4259
4361
|
} catch (e) {
|
|
4260
4362
|
throw new CallApiError({
|
|
4261
4363
|
code: "network_error",
|
|
@@ -4533,6 +4635,19 @@ function buildBarsBatchCommand() {
|
|
|
4533
4635
|
});
|
|
4534
4636
|
return cmd;
|
|
4535
4637
|
}
|
|
4638
|
+
// src/verbs/capabilities.ts
|
|
4639
|
+
var capabilitiesSpec = {
|
|
4640
|
+
name: "capabilities",
|
|
4641
|
+
description: "API capability changelog and spec-version discovery. Returns the current spec version plus a newest-first changelog of capability changes (new endpoints, new fields, behavior fixes). Call once at session start — or when a response shape surprises you — to learn what changed since your integration was written. Free, unauthenticated, non-billable.",
|
|
4642
|
+
inputSchema: {},
|
|
4643
|
+
handler: async (_args, ctx) => {
|
|
4644
|
+
const op = OPERATIONS["capabilities.show"];
|
|
4645
|
+
if (!op)
|
|
4646
|
+
throw new Error("capabilities.show op missing from codegen");
|
|
4647
|
+
return callOp(op, {}, ctx);
|
|
4648
|
+
},
|
|
4649
|
+
backingOps: ["capabilities.show"]
|
|
4650
|
+
};
|
|
4536
4651
|
// src/verbs/chart.ts
|
|
4537
4652
|
import { Command as Command5, Option as Option4 } from "commander";
|
|
4538
4653
|
import { z as z3 } from "zod";
|
|
@@ -5478,9 +5593,29 @@ function buildHotCommand() {
|
|
|
5478
5593
|
});
|
|
5479
5594
|
return cmd;
|
|
5480
5595
|
}
|
|
5596
|
+
// src/verbs/indices.ts
|
|
5597
|
+
import { z as z8 } from "zod";
|
|
5598
|
+
var indexSnapshotSpec = {
|
|
5599
|
+
name: "index_snapshot",
|
|
5600
|
+
description: "Real-time snapshot for the 172 A-share indices with a live quote feed (e.g. 上证指数 SSE:000001, 深证成指 SZSE:399001, 创业板指 SZSE:399006, 科创50 SSE:000688, 北证50 BSE:899050): price, pct_change, amount, speed_3min per index, refreshed ~15s during market hours (last intraday snapshot outside trading hours). Pass `codes` to narrow to a subset; omit for all 172. Use this for 'how is the broad market doing right now' — `quote` covers individual stocks only. CSI-series indices (CSI:*) have no live feed and are not returned.",
|
|
5601
|
+
inputSchema: {
|
|
5602
|
+
codes: z8.array(z8.string().regex(/^(SSE|SZSE|BSE):[0-9]{6}$/)).min(1).max(172).optional().describe("Canonical index codes to narrow the snapshot (e.g. ['SSE:000001', 'BSE:899050']); omit for all 172")
|
|
5603
|
+
},
|
|
5604
|
+
handler: async (args, ctx) => {
|
|
5605
|
+
const op = OPERATIONS["index.snapshot"];
|
|
5606
|
+
if (!op)
|
|
5607
|
+
throw new Error("index.snapshot op missing from codegen");
|
|
5608
|
+
const callArgs = {};
|
|
5609
|
+
if (Array.isArray(args.codes) && args.codes.length > 0) {
|
|
5610
|
+
callArgs.codes = args.codes.join(",");
|
|
5611
|
+
}
|
|
5612
|
+
return callOp(op, callArgs, ctx);
|
|
5613
|
+
},
|
|
5614
|
+
backingOps: ["index.snapshot"]
|
|
5615
|
+
};
|
|
5481
5616
|
// src/verbs/limit_up.ts
|
|
5482
5617
|
import { Command as Command11, Option as Option9 } from "commander";
|
|
5483
|
-
import { z as
|
|
5618
|
+
import { z as z9 } from "zod";
|
|
5484
5619
|
var DATE_RE5 = /^\d{4}-\d{2}-\d{2}$/;
|
|
5485
5620
|
var INCLUDE_VALUES = ["active", "all"];
|
|
5486
5621
|
function clampInt5(raw, min, max, fallback) {
|
|
@@ -5497,9 +5632,9 @@ var limitUpPoolSpec = {
|
|
|
5497
5632
|
name: "limit_up_pool",
|
|
5498
5633
|
description: "A-share limit-up pool for a given trade date — per-stock detail: first/last seal time, final pct, seal amount/volume, broken count, consecutive_days (连板数), board_count_within (N-day 板数), one-word-board flag, current status (limit_up / broken), board classification. Sort: consecutive_days desc → board_count_within desc → seal_amount desc. Pre-open fallback applies. Use when an agent needs the concrete list of '今天涨停了哪些票'.",
|
|
5499
5634
|
inputSchema: {
|
|
5500
|
-
trade_date:
|
|
5501
|
-
include:
|
|
5502
|
-
include_broken:
|
|
5635
|
+
trade_date: z9.string().regex(DATE_RE5).optional().describe("ISO date YYYY-MM-DD; omit for latest (pre-open fallback)"),
|
|
5636
|
+
include: z9.enum(INCLUDE_VALUES).default("active").describe("active = 排除 ST/退市(默认); all = 全部"),
|
|
5637
|
+
include_broken: z9.boolean().default(false).describe("true 时连炸板(status=broken)也一起返回;默认只返回当前封板 status=limit_up")
|
|
5503
5638
|
},
|
|
5504
5639
|
handler: async (args, ctx) => {
|
|
5505
5640
|
const op = OPERATIONS["limit-up.pool"];
|
|
@@ -5519,8 +5654,8 @@ var limitUpSummarySpec = {
|
|
|
5519
5654
|
name: "limit_up_summary",
|
|
5520
5655
|
description: "Limit-up summary for a given trade date: 涨停数 (limit_up_count) / 炸板数 (broken_count) / 跌停数 (limit_down_count) / 最高连板 (max_height) / 连板梯队 ladder[{height, count}]. 炸板/跌停 与情绪页同源 (market_breadth_intraday 当日最新分钟行). Pre-open fallback applies.",
|
|
5521
5656
|
inputSchema: {
|
|
5522
|
-
trade_date:
|
|
5523
|
-
include:
|
|
5657
|
+
trade_date: z9.string().regex(DATE_RE5).optional().describe("ISO date YYYY-MM-DD; omit for latest"),
|
|
5658
|
+
include: z9.enum(INCLUDE_VALUES).default("active").describe("active = all_a_ex_st 口径(默认); all = all_a 口径")
|
|
5524
5659
|
},
|
|
5525
5660
|
handler: async (args, ctx) => {
|
|
5526
5661
|
const op = OPERATIONS["limit-up.summary"];
|
|
@@ -5537,7 +5672,7 @@ var limitUpHistorySpec = {
|
|
|
5537
5672
|
name: "limit_up_history",
|
|
5538
5673
|
description: "Daily limit-up trend over the last N trading days: { trade_date, limit_up_count, broken_count, max_height }. 涨停数/最高连板 from market_limit_up_pool; 炸板数 from market_breadth_intraday (all_a_ex_st). Sorted by trade_date asc (oldest first).",
|
|
5539
5674
|
inputSchema: {
|
|
5540
|
-
days:
|
|
5675
|
+
days: z9.number().int().min(1).max(250).default(30).describe("Lookback window in trading days (1-250, default 30)")
|
|
5541
5676
|
},
|
|
5542
5677
|
handler: async (args, ctx) => {
|
|
5543
5678
|
const op = OPERATIONS["limit-up.history"];
|
|
@@ -5551,7 +5686,7 @@ var limitUpAnalysisSpec = {
|
|
|
5551
5686
|
name: "limit_up_analysis",
|
|
5552
5687
|
description: "Per-stock limit-up LLM attribution for a given trade date: leading_concept (主导题材) + concept_tags (相关标签) + reason (涨停理由). Themes are judged by an LLM from the latest research / views / news (NOT from the concept graph, so brand-new themes are covered) and refreshed across 6 Beijing slots daily. Complements limit-up pool (which says '哪些票涨停'); this says '为什么涨停/属什么题材'. STALE-DATA WARNING: omitting trade_date returns the latest *attributed* trading day, NOT necessarily today — before today's first slot lands (~09:47 Beijing) it falls back to the PREVIOUS day's attribution. For today's data pass trade_date explicitly and verify the returned trade_date / generated_at fields. An explicit trade_date with no attribution yet returns an empty analyses list.",
|
|
5553
5688
|
inputSchema: {
|
|
5554
|
-
trade_date:
|
|
5689
|
+
trade_date: z9.string().regex(DATE_RE5).optional().describe("ISO date YYYY-MM-DD; omit for the latest *attributed* day (falls back to the previous day until today's first slot lands ~09:47 Beijing — pass today explicitly for today's data)")
|
|
5555
5690
|
},
|
|
5556
5691
|
handler: async (args, ctx) => {
|
|
5557
5692
|
const op = OPERATIONS["limit-up.analysis"];
|
|
@@ -5617,7 +5752,7 @@ function buildLimitUpCommand() {
|
|
|
5617
5752
|
}
|
|
5618
5753
|
// src/verbs/lookup.ts
|
|
5619
5754
|
import { Command as Command12, Option as Option10 } from "commander";
|
|
5620
|
-
import { z as
|
|
5755
|
+
import { z as z10 } from "zod";
|
|
5621
5756
|
function mapLookupResponse(raw) {
|
|
5622
5757
|
if (!Array.isArray(raw))
|
|
5623
5758
|
return [];
|
|
@@ -5641,8 +5776,8 @@ var lookupSpec = {
|
|
|
5641
5776
|
name: "lookup",
|
|
5642
5777
|
description: "Resolve a Chinese name / A-share code / pinyin initials to canonical codes (e.g. SSE:600519). Use this first whenever the agent has a description but needs a canonical_code for downstream calls. AH dual-listing: when a company is listed on both A-share and HK (e.g. 工商银行 SSE:601398 / HK:01398), prefer the A-share canonical for all downstream analysis unless the user explicitly asks for the HK side.",
|
|
5643
5778
|
inputSchema: {
|
|
5644
|
-
text:
|
|
5645
|
-
limit:
|
|
5779
|
+
text: z10.string().min(1).max(50).describe("Search text: Chinese name (贵州茅台), code (600519), or pinyin (gzmt)"),
|
|
5780
|
+
limit: z10.number().int().min(1).max(30).default(10).describe("Max matches (1-30)")
|
|
5646
5781
|
},
|
|
5647
5782
|
handler: async (args, ctx) => {
|
|
5648
5783
|
const op = OPERATIONS["semantic.find"];
|
|
@@ -5676,7 +5811,7 @@ function buildLookupCommand() {
|
|
|
5676
5811
|
}
|
|
5677
5812
|
// src/verbs/market.ts
|
|
5678
5813
|
import { Command as Command13, Option as Option11 } from "commander";
|
|
5679
|
-
import { z as
|
|
5814
|
+
import { z as z11 } from "zod";
|
|
5680
5815
|
var marketStatusSpec = {
|
|
5681
5816
|
name: "market_status",
|
|
5682
5817
|
description: "Current A-share market session: pre-open / open / lunch / closed; current and next trading day; holiday flag. Cheap and non-billable — call it before any quote / bars fetch when the agent needs to decide pre-market fallback vs intraday vs after-close behavior.",
|
|
@@ -5729,10 +5864,10 @@ var marketMoversSpec = {
|
|
|
5729
5864
|
name: "market_movers",
|
|
5730
5865
|
description: "A-share market movers — top N from the full real-time snapshot, sorted by chosen field. Sort keys: `pct` (涨幅榜) / `pct_asc` (跌幅榜) / `speed` (3-min 涨速 speed_3min) / `amount` (成交额) / `turnover` (换手率 turnover_rate) / `total_mv` (总市值). Backed by `quote.scan` so 9:00-9:14 集合竞价时段会返空 (with `note`). Defaults: sort=pct, top=20, include=active (排除 ST / 退市). Use this for /market-style 'who's running today' instead of pulling the full 3 MB scan and sorting client-side.",
|
|
5731
5866
|
inputSchema: {
|
|
5732
|
-
sort:
|
|
5733
|
-
top:
|
|
5734
|
-
exchange:
|
|
5735
|
-
include:
|
|
5867
|
+
sort: z11.enum(MOVERS_SORT_VALUES).default("pct").describe("Sort field: pct / pct_asc / speed / amount / turnover / total_mv"),
|
|
5868
|
+
top: z11.number().int().min(1).max(500).default(20).describe("Top N items (1-500, default 20)"),
|
|
5869
|
+
exchange: z11.enum(EXCHANGE_VALUES).optional().describe("Narrow to one exchange: SSE / SZSE / BSE"),
|
|
5870
|
+
include: z11.enum(INCLUDE_VALUES2).default("active").describe("active = 排除 ST/退市(默认); all = 全集(含 ST + 退市 + 未知 status)")
|
|
5736
5871
|
},
|
|
5737
5872
|
handler: async (args, ctx) => {
|
|
5738
5873
|
const op = OPERATIONS["quote.scan"];
|
|
@@ -5816,18 +5951,19 @@ function buildMarketCommand() {
|
|
|
5816
5951
|
}
|
|
5817
5952
|
// src/verbs/news.ts
|
|
5818
5953
|
import { Command as Command14, Option as Option12 } from "commander";
|
|
5819
|
-
import { z as
|
|
5954
|
+
import { z as z12 } from "zod";
|
|
5820
5955
|
var newsSpec = {
|
|
5821
5956
|
name: "news",
|
|
5822
|
-
description: "SUPPLEMENTARY news / market briefs (short time-horizon event stream). Use ONLY to fill gaps not covered by `views`; prefer `views` as the primary research source. Three modes: `code` (canonical, works across A-share | HK | US — e.g. SSE:600519, HK:00700 腾讯, US:BABA) → news mentioning that security; `query` (free text) → full-text search; neither → recent time-window feed. AH dual-listing: when filtering by `code` for an A+H listed company, pass the A-share canonical (e.g. SSE:601398) for consolidated coverage; HK side only if the user explicitly asks. Note: `query` is matched against news text, so passing a canonical_code as `query` will not work — use `code` for security filtering.",
|
|
5957
|
+
description: "SUPPLEMENTARY news / market briefs (short time-horizon event stream). Use ONLY to fill gaps not covered by `views`; prefer `views` as the primary research source. Returns `fields=compact` by default (title + ai_summary + first ~300 chars of content, `content_truncated` flag) — typically 20-50x smaller than full; pass fields='full' only when you need complete article text. Three modes: `code` (canonical, works across A-share | HK | US — e.g. SSE:600519, HK:00700 腾讯, US:BABA) → news mentioning that security; `query` (free text) → full-text search; neither → recent time-window feed. AH dual-listing: when filtering by `code` for an A+H listed company, pass the A-share canonical (e.g. SSE:601398) for consolidated coverage; HK side only if the user explicitly asks. Note: `query` is matched against news text, so passing a canonical_code as `query` will not work — use `code` for security filtering.",
|
|
5823
5958
|
inputSchema: {
|
|
5824
|
-
code:
|
|
5825
|
-
query:
|
|
5826
|
-
hours:
|
|
5827
|
-
from:
|
|
5828
|
-
to:
|
|
5829
|
-
offset:
|
|
5830
|
-
limit:
|
|
5959
|
+
code: z12.string().regex(/^((SSE|SZSE|BSE):[0-9]{6}|HK:[0-9]{5}|US:[A-Z][A-Z0-9.\-]{0,5})$/).optional().describe("Filter by security canonical_code (e.g. SSE:600519, HK:00700). Takes precedence over `query`."),
|
|
5960
|
+
query: z12.string().optional().describe("Free-text query (Chinese or English) matched against news content; omit for time-window feed. Do NOT pass canonical_code here — use `code` instead."),
|
|
5961
|
+
hours: z12.number().int().min(1).max(168).default(24).describe("Rolling-feed lookback in hours (default 24 — much narrower than views). For longer history use from/to with code or query."),
|
|
5962
|
+
from: z12.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("History range start (China date YYYY-MM-DD). Range mode: needs code or query; Pro-only up to 365 days back."),
|
|
5963
|
+
to: z12.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("History range end (inclusive, China date; defaults to today). Use with from."),
|
|
5964
|
+
offset: z12.number().int().min(0).optional().describe("Pagination offset for paging through long history ranges."),
|
|
5965
|
+
limit: z12.number().int().min(1).max(100).default(20).describe("Max items per page"),
|
|
5966
|
+
fields: z12.enum(["compact", "full"]).optional().describe("Response shape: compact (default — title + ai_summary + first ~300 chars) or full (complete content)")
|
|
5831
5967
|
},
|
|
5832
5968
|
handler: async (args, ctx) => {
|
|
5833
5969
|
const { opKey, callArgs } = buildNewsCall(args);
|
|
@@ -5840,11 +5976,14 @@ var newsSpec = {
|
|
|
5840
5976
|
};
|
|
5841
5977
|
function buildNewsCall(args) {
|
|
5842
5978
|
const hasRange = typeof args.from === "string" && args.from.length > 0;
|
|
5979
|
+
const fields = args.fields === "compact" || args.fields === "full" ? args.fields : undefined;
|
|
5843
5980
|
const withWindow = (a) => {
|
|
5844
5981
|
if (typeof args.limit === "number")
|
|
5845
5982
|
a.limit = args.limit;
|
|
5846
5983
|
if (typeof args.offset === "number")
|
|
5847
5984
|
a.offset = args.offset;
|
|
5985
|
+
if (fields)
|
|
5986
|
+
a.fields = fields;
|
|
5848
5987
|
if (hasRange) {
|
|
5849
5988
|
a.from = args.from;
|
|
5850
5989
|
if (typeof args.to === "string" && args.to.length > 0)
|
|
@@ -5861,7 +6000,7 @@ function buildNewsCall(args) {
|
|
|
5861
6000
|
if (hasRange) {
|
|
5862
6001
|
throw new Error("range history (from/to) requires --code or --query; the bare feed has no narrowing filter");
|
|
5863
6002
|
}
|
|
5864
|
-
return { opKey: "news.feed", callArgs: {} };
|
|
6003
|
+
return { opKey: "news.feed", callArgs: fields ? { fields } : {} };
|
|
5865
6004
|
}
|
|
5866
6005
|
function clamp3(raw, min, max, fallback) {
|
|
5867
6006
|
const n = Math.floor(Number(raw));
|
|
@@ -5882,6 +6021,10 @@ function buildNewsCommand() {
|
|
|
5882
6021
|
cmd.addOption(new Option12("--to <date>", "History range end YYYY-MM-DD (inclusive; default today)"));
|
|
5883
6022
|
cmd.addOption(new Option12("--offset <n>", "Pagination offset for long history").default("0"));
|
|
5884
6023
|
cmd.addOption(new Option12("--limit <n>", "Max items").default("20"));
|
|
6024
|
+
cmd.addOption(new Option12("--fields <mode>", "Response shape: compact (default) or full").choices([
|
|
6025
|
+
"compact",
|
|
6026
|
+
"full"
|
|
6027
|
+
]));
|
|
5885
6028
|
cmd.action(async (opts) => {
|
|
5886
6029
|
const args = {
|
|
5887
6030
|
hours: clamp3(opts.hours, 1, 168, 24),
|
|
@@ -5896,19 +6039,21 @@ function buildNewsCommand() {
|
|
|
5896
6039
|
args.from = opts.from;
|
|
5897
6040
|
if (opts.to)
|
|
5898
6041
|
args.to = opts.to;
|
|
6042
|
+
if (opts.fields)
|
|
6043
|
+
args.fields = opts.fields;
|
|
5899
6044
|
await executeVerb(async (ctx) => newsSpec.handler(args, ctx));
|
|
5900
6045
|
});
|
|
5901
6046
|
return cmd;
|
|
5902
6047
|
}
|
|
5903
6048
|
// src/verbs/quote.ts
|
|
5904
6049
|
import { Command as Command15, Option as Option13 } from "commander";
|
|
5905
|
-
import { z as
|
|
6050
|
+
import { z as z13 } from "zod";
|
|
5906
6051
|
var quoteSpec = {
|
|
5907
6052
|
name: "quote",
|
|
5908
6053
|
description: "Real-time quote for 1-200 A-share securities (last price, volume, change %, bid/ask). For >200 codes use `scan` instead. AH dual-listing: HK codes are not supported here; for an A+H listed company use the A-share canonical (e.g. SSE:601398 not HK:01398).",
|
|
5909
6054
|
inputSchema: {
|
|
5910
|
-
codes:
|
|
5911
|
-
include_l2:
|
|
6055
|
+
codes: z13.array(z13.string().regex(/^(SSE|SZSE|BSE|SH|SZ|BJ):[0-9]{6}$/)).min(1).max(200).describe("Array of canonical codes (e.g. ['SSE:600519', 'SZSE:000001'])"),
|
|
6056
|
+
include_l2: z13.boolean().optional().describe("Include L2 5-level order book (requires quote:l2 scope)")
|
|
5912
6057
|
},
|
|
5913
6058
|
handler: async (args, ctx) => {
|
|
5914
6059
|
const op = OPERATIONS["quote"];
|
|
@@ -5939,12 +6084,12 @@ function buildQuoteCommand() {
|
|
|
5939
6084
|
}
|
|
5940
6085
|
// src/verbs/scan.ts
|
|
5941
6086
|
import { Command as Command16, Option as Option14 } from "commander";
|
|
5942
|
-
import { z as
|
|
6087
|
+
import { z as z14 } from "zod";
|
|
5943
6088
|
var scanSpec = {
|
|
5944
6089
|
name: "scan",
|
|
5945
6090
|
description: "Full-market real-time quote scan (~5800 A-share securities in one round-trip). Use for universe discovery; agents should slim output with field/byte limits in their tool host or post-processing.",
|
|
5946
6091
|
inputSchema: {
|
|
5947
|
-
exchange:
|
|
6092
|
+
exchange: z14.enum(["SSE", "SZSE", "BSE"]).optional().describe("Filter by exchange (omit for all)")
|
|
5948
6093
|
},
|
|
5949
6094
|
handler: async (args, ctx) => {
|
|
5950
6095
|
const op = OPERATIONS["quote.scan"];
|
|
@@ -5973,16 +6118,16 @@ function buildScanCommand() {
|
|
|
5973
6118
|
}
|
|
5974
6119
|
// src/verbs/search.ts
|
|
5975
6120
|
import { Command as Command17, Option as Option15 } from "commander";
|
|
5976
|
-
import { z as
|
|
6121
|
+
import { z as z15 } from "zod";
|
|
5977
6122
|
var searchSpec = {
|
|
5978
6123
|
name: "search",
|
|
5979
6124
|
description: "Structured-first hybrid search across analyst views + news (views weighted higher per feedback_views_over_news). Three lanes — entity (company codes / names, analyst names), concept-graph hub (theme alias → concept → constituents & research), and ParadeDB BM25 full-text (jieba) — fused with RRF. Deterministic & explainable: no vector kNN, no LLM rerank. Best for theme/concept queries: 'CPO' brings out 光模块/光通信/光芯片; '减肥药' brings out 创新药; '智驾' brings out 智能驾驶. Use `news` or `views` instead if you need strict time-window listing.",
|
|
5980
6125
|
inputSchema: {
|
|
5981
|
-
q:
|
|
5982
|
-
type:
|
|
5983
|
-
mode:
|
|
5984
|
-
hours:
|
|
5985
|
-
limit:
|
|
6126
|
+
q: z15.string().min(1).max(200).describe("Free-text query (Chinese or English)"),
|
|
6127
|
+
type: z15.enum(["news", "views", "all"]).default("all").describe("Search scope (views weighted higher in 'all')"),
|
|
6128
|
+
mode: z15.enum(["hybrid", "exact"]).default("hybrid").describe("hybrid = entity + concept-graph + BM25 fused; exact = BM25 keyword only"),
|
|
6129
|
+
hours: z15.number().int().min(1).max(720).optional().describe("Lookback window in hours; omit for no time limit"),
|
|
6130
|
+
limit: z15.number().int().min(1).max(50).default(20).describe("Max items returned")
|
|
5986
6131
|
},
|
|
5987
6132
|
handler: async (args, ctx) => {
|
|
5988
6133
|
const op = OPERATIONS["search.semantic"];
|
|
@@ -6035,7 +6180,7 @@ function buildSearchCommand() {
|
|
|
6035
6180
|
}
|
|
6036
6181
|
// src/verbs/sentiment.ts
|
|
6037
6182
|
import { Command as Command18, Option as Option16 } from "commander";
|
|
6038
|
-
import { z as
|
|
6183
|
+
import { z as z16 } from "zod";
|
|
6039
6184
|
var SCOPE_VALUES = [
|
|
6040
6185
|
"all_a",
|
|
6041
6186
|
"all_a_ex_st",
|
|
@@ -6059,8 +6204,8 @@ var sentimentSpec = {
|
|
|
6059
6204
|
name: "sentiment",
|
|
6060
6205
|
description: "Aggregate market sentiment indicators (limit-up/down counts, breadth, divergence index, top movers). Defaults to all_a_ex_st (all A-shares ex-ST). Pass `at_date` (YYYY-MM-DD) for any historical trading day; omit for today's latest snapshot. Backward-compat alias of `sentiment_overview` — prefer the latter in new agent code.",
|
|
6061
6206
|
inputSchema: {
|
|
6062
|
-
scope:
|
|
6063
|
-
at_date:
|
|
6207
|
+
scope: z16.enum(SCOPE_VALUES).default("all_a_ex_st").describe("Universe filter"),
|
|
6208
|
+
at_date: z16.string().regex(DATE_RE6).optional().describe("Trade date YYYY-MM-DD; omit for today's realtime snapshot")
|
|
6064
6209
|
},
|
|
6065
6210
|
handler: async (args, ctx) => {
|
|
6066
6211
|
const op = OPERATIONS["sentiment.overview"];
|
|
@@ -6077,8 +6222,8 @@ var sentimentOverviewSpec = {
|
|
|
6077
6222
|
name: "sentiment_overview",
|
|
6078
6223
|
description: "Aggregate sentiment snapshot for one trading day's latest minute: up/down/flat counts, limit-up/down, gt3/lt3 breadth, divergence index + label, MA20/50/200 breadth, 52w new high/low. `at_date` omitted = today realtime (Redis); explicit YYYY-MM-DD = historical from ClickHouse `market_breadth_intraday` last minute.",
|
|
6079
6224
|
inputSchema: {
|
|
6080
|
-
scope:
|
|
6081
|
-
at_date:
|
|
6225
|
+
scope: z16.enum(SCOPE_VALUES).default("all_a_ex_st").describe("Universe filter"),
|
|
6226
|
+
at_date: z16.string().regex(DATE_RE6).optional().describe("Trade date YYYY-MM-DD; omit for today realtime")
|
|
6082
6227
|
},
|
|
6083
6228
|
handler: async (args, ctx) => {
|
|
6084
6229
|
const op = OPERATIONS["sentiment.overview"];
|
|
@@ -6095,8 +6240,8 @@ var sentimentBreadthSpec = {
|
|
|
6095
6240
|
name: "sentiment_breadth",
|
|
6096
6241
|
description: "Intraday breadth time series for one trading day (up to 241 minute bars; 13:00 excluded due to sparse Sina ticks). `at_date` omitted = today (pre-open returns empty); explicit YYYY-MM-DD = historical replay. Returns per-minute rows of up/down/flat/limit_up/limit_down/breadth/divergence fields — same shape as `sentiment_overview` per row.",
|
|
6097
6242
|
inputSchema: {
|
|
6098
|
-
scope:
|
|
6099
|
-
at_date:
|
|
6243
|
+
scope: z16.enum(SCOPE_VALUES).default("all_a_ex_st").describe("Universe filter"),
|
|
6244
|
+
at_date: z16.string().regex(DATE_RE6).optional().describe("Trade date YYYY-MM-DD; omit for today")
|
|
6100
6245
|
},
|
|
6101
6246
|
handler: async (args, ctx) => {
|
|
6102
6247
|
const op = OPERATIONS["sentiment.breadth"];
|
|
@@ -6113,9 +6258,9 @@ var sentimentTurnoverSpec = {
|
|
|
6113
6258
|
name: "sentiment_turnover",
|
|
6114
6259
|
description: "Intraday turnover time series with `current_turnover` / `predicted_total` / `delta_vs_yesterday` headline fields. `at_date` omitted = today (pre-open returns nulls); explicit YYYY-MM-DD = historical. `days` (1-5) pulls that day + N-1 prior trading days for same-minute YoY comparison.",
|
|
6115
6260
|
inputSchema: {
|
|
6116
|
-
scope:
|
|
6117
|
-
at_date:
|
|
6118
|
-
days:
|
|
6261
|
+
scope: z16.enum(SCOPE_VALUES).default("all_a_ex_st").describe("Universe filter"),
|
|
6262
|
+
at_date: z16.string().regex(DATE_RE6).optional().describe("Trade date YYYY-MM-DD; omit for today"),
|
|
6263
|
+
days: z16.number().int().min(1).max(5).default(1).describe("at_date 当日 + 之前 days-1 天(1-5,默认 1)")
|
|
6119
6264
|
},
|
|
6120
6265
|
handler: async (args, ctx) => {
|
|
6121
6266
|
const op = OPERATIONS["sentiment.turnover"];
|
|
@@ -6210,19 +6355,20 @@ function buildSentimentCommand() {
|
|
|
6210
6355
|
}
|
|
6211
6356
|
// src/verbs/views.ts
|
|
6212
6357
|
import { Command as Command19, Option as Option17 } from "commander";
|
|
6213
|
-
import { z as
|
|
6358
|
+
import { z as z17 } from "zod";
|
|
6214
6359
|
var viewsSpec = {
|
|
6215
6360
|
name: "views",
|
|
6216
|
-
description: "PRIMARY research source for stock judgement: analyst views / sell-side reports / long-form opinion stream, with research_entity_id attribution. Prefer this over `news` when forming an investment opinion.
|
|
6361
|
+
description: "PRIMARY research source for stock judgement: analyst views / sell-side reports / long-form opinion stream, with research_entity_id attribution. Prefer this over `news` when forming an investment opinion. Returns `fields=compact` by default (ai_summary + first ~300 chars of content, `content_truncated` flag) — typically 20-50x smaller than full; pass fields='full' only when summaries are not enough. Recommended reading path: `digest` for a one-shot overview → this verb (compact) to scan → `report` for the full text of a single item (`full_text_available=true` means a parsed research report). AH dual-listing: for an A+H listed company pass the A-share canonical to get the consolidated coverage (most domestic analyst views attribute to the A-share security); use HK code only if the user explicitly asks for the HK perspective.",
|
|
6217
6362
|
inputSchema: {
|
|
6218
|
-
code:
|
|
6219
|
-
analyst:
|
|
6220
|
-
institution:
|
|
6221
|
-
since_days:
|
|
6222
|
-
from:
|
|
6223
|
-
to:
|
|
6224
|
-
offset:
|
|
6225
|
-
limit:
|
|
6363
|
+
code: z17.string().regex(/^((SSE|SZSE|BSE):[0-9]{6}|HK:[0-9]{5}|US:[A-Z][A-Z0-9.\-]{0,5})$/).optional().describe("Filter by security canonical_code (e.g. SSE:600519, HK:00700, US:AAPL)"),
|
|
6364
|
+
analyst: z17.string().optional().describe("Analyst Chinese name (exact / fuzzy)"),
|
|
6365
|
+
institution: z17.string().optional().describe("Broker / institution Chinese name"),
|
|
6366
|
+
since_days: z17.number().int().min(1).max(365).default(7).describe("Lookback in days. ≤30 = rolling feed; >30 auto-switches to range history " + "(needs a code/analyst/institution filter; Pro-only up to 365). For an explicit window use from/to."),
|
|
6367
|
+
from: z17.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("History range start (China date YYYY-MM-DD). Range mode: needs a filter; Pro-only up to 365 days back."),
|
|
6368
|
+
to: z17.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("History range end (inclusive, China date; defaults to today). Use with from."),
|
|
6369
|
+
offset: z17.number().int().min(0).optional().describe("Pagination offset for paging through long history ranges."),
|
|
6370
|
+
limit: z17.number().int().min(1).max(100).default(30).describe("Max items per page"),
|
|
6371
|
+
fields: z17.enum(["compact", "full"]).optional().describe("Response shape: compact (default — ai_summary + first ~300 chars) or full (complete content)")
|
|
6226
6372
|
},
|
|
6227
6373
|
handler: async (args, ctx) => {
|
|
6228
6374
|
const op = OPERATIONS["views.recent"];
|
|
@@ -6246,6 +6392,8 @@ function buildViewsCallArgs(args) {
|
|
|
6246
6392
|
callArgs.institution = args.institution;
|
|
6247
6393
|
if (typeof args.offset === "number")
|
|
6248
6394
|
callArgs.offset = args.offset;
|
|
6395
|
+
if (args.fields === "compact" || args.fields === "full")
|
|
6396
|
+
callArgs.fields = args.fields;
|
|
6249
6397
|
const explicitRange = typeof args.from === "string" && args.from.length > 0;
|
|
6250
6398
|
if (explicitRange || sinceDays > 30) {
|
|
6251
6399
|
if (!hasFilter) {
|
|
@@ -6267,10 +6415,10 @@ var reportSpec = {
|
|
|
6267
6415
|
name: "report",
|
|
6268
6416
|
description: "Fetch the FULL research-report text behind a view (mineru-parsed long-form), for when the `views` summary is not enough. Only works on views where `full_text_available=true` (gdrive sell-side reports). Two-step usage: default `format=outline` returns a few-KB heading map + total_chars + each heading's char_offset; then call `format=full` with `offset` (e.g. an outline char_offset) to read the body in pages — follow `next_offset` to page through large docs instead of pulling everything at once.",
|
|
6269
6417
|
inputSchema: {
|
|
6270
|
-
view_id:
|
|
6271
|
-
format:
|
|
6272
|
-
offset:
|
|
6273
|
-
max_chars:
|
|
6418
|
+
view_id: z17.string().regex(/^[0-9]+$/).describe("View id from `views` items[].id (must have full_text_available=true)"),
|
|
6419
|
+
format: z17.enum(["outline", "full"]).default("outline").describe("outline=heading map (default); full=body slice"),
|
|
6420
|
+
offset: z17.number().int().min(0).default(0).describe("full mode: start char offset (use an outline char_offset)"),
|
|
6421
|
+
max_chars: z17.number().int().min(1).max(80000).default(40000).describe("full mode: max chars per page (hard cap 80000)")
|
|
6274
6422
|
},
|
|
6275
6423
|
handler: async (args, ctx) => {
|
|
6276
6424
|
const op = OPERATIONS["views.document"];
|
|
@@ -6308,6 +6456,10 @@ function buildViewsCommand() {
|
|
|
6308
6456
|
cmd.addOption(new Option17("--to <date>", "History range end YYYY-MM-DD (inclusive; default today)"));
|
|
6309
6457
|
cmd.addOption(new Option17("--offset <n>", "Pagination offset for long history").default("0"));
|
|
6310
6458
|
cmd.addOption(new Option17("--limit <n>", "Max items (1-100)").default("30"));
|
|
6459
|
+
cmd.addOption(new Option17("--fields <mode>", "Response shape: compact (default) or full").choices([
|
|
6460
|
+
"compact",
|
|
6461
|
+
"full"
|
|
6462
|
+
]));
|
|
6311
6463
|
cmd.action(async (opts) => {
|
|
6312
6464
|
if (!OPERATIONS["views.recent"]) {
|
|
6313
6465
|
emitVerbError("internal_error", "views.recent missing from codegen", undefined, 2);
|
|
@@ -6327,6 +6479,8 @@ function buildViewsCommand() {
|
|
|
6327
6479
|
args.from = opts.from;
|
|
6328
6480
|
if (opts.to)
|
|
6329
6481
|
args.to = opts.to;
|
|
6482
|
+
if (opts.fields)
|
|
6483
|
+
args.fields = opts.fields;
|
|
6330
6484
|
await executeVerb(async (ctx) => viewsSpec.handler(args, ctx));
|
|
6331
6485
|
});
|
|
6332
6486
|
return cmd;
|
|
@@ -6360,6 +6514,7 @@ var ALL_VERB_SPECS = [
|
|
|
6360
6514
|
quoteSpec,
|
|
6361
6515
|
marketStatusSpec,
|
|
6362
6516
|
marketMoversSpec,
|
|
6517
|
+
indexSnapshotSpec,
|
|
6363
6518
|
viewsSpec,
|
|
6364
6519
|
reportSpec,
|
|
6365
6520
|
newsSpec,
|
|
@@ -6394,7 +6549,8 @@ var ALL_VERB_SPECS = [
|
|
|
6394
6549
|
financialsReportsSpec,
|
|
6395
6550
|
financialsSeriesSpec,
|
|
6396
6551
|
ownershipShowSpec,
|
|
6397
|
-
ownershipLockupsSpec
|
|
6552
|
+
ownershipLockupsSpec,
|
|
6553
|
+
capabilitiesSpec
|
|
6398
6554
|
];
|
|
6399
6555
|
|
|
6400
6556
|
// src/tools/mcp.ts
|
|
@@ -6469,24 +6625,51 @@ function buildMcpCommand() {
|
|
|
6469
6625
|
}
|
|
6470
6626
|
throw e;
|
|
6471
6627
|
}
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6628
|
+
const WHOAMI_MAX_ATTEMPTS = 3;
|
|
6629
|
+
const WHOAMI_BACKOFF_MS = [500, 1500];
|
|
6630
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
6631
|
+
let whoami = null;
|
|
6632
|
+
let lastErr = null;
|
|
6633
|
+
for (let attempt = 1;attempt <= WHOAMI_MAX_ATTEMPTS; attempt++) {
|
|
6634
|
+
try {
|
|
6635
|
+
whoami = await getWhoami({
|
|
6636
|
+
baseUrl: creds.baseUrl,
|
|
6637
|
+
bearer: creds.key,
|
|
6638
|
+
cliVersion: CLI_VERSION,
|
|
6639
|
+
channel: "mcp"
|
|
6640
|
+
});
|
|
6641
|
+
break;
|
|
6642
|
+
} catch (e) {
|
|
6643
|
+
lastErr = e;
|
|
6644
|
+
const status = e instanceof CallApiError ? e.httpStatus : undefined;
|
|
6645
|
+
const code = e instanceof CallApiError ? e.code : undefined;
|
|
6646
|
+
const isAuthError = status === 401 || status === 403 || code === "auth_missing" || code === "forbidden";
|
|
6647
|
+
if (isAuthError) {
|
|
6648
|
+
process.stderr.write(`[mcp] whoami auth failed${status ? ` (HTTP ${status})` : ""}: ` + `${e instanceof Error ? e.message : String(e)}
|
|
6649
|
+
` + "[mcp] hint: key 失效或无权限,请 `echopai login` 或检查 ECHOPAI_KEY。\n");
|
|
6650
|
+
process.exit(1);
|
|
6651
|
+
}
|
|
6652
|
+
if (attempt < WHOAMI_MAX_ATTEMPTS) {
|
|
6653
|
+
const backoffMs = WHOAMI_BACKOFF_MS[attempt - 1] ?? 1500;
|
|
6654
|
+
process.stderr.write(`[mcp] whoami attempt ${attempt}/${WHOAMI_MAX_ATTEMPTS} failed ` + `(${e instanceof Error ? e.message : String(e)}); ` + `retrying in ${backoffMs}ms
|
|
6482
6655
|
`);
|
|
6483
|
-
|
|
6656
|
+
await sleep(backoffMs);
|
|
6657
|
+
}
|
|
6658
|
+
}
|
|
6484
6659
|
}
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
|
|
6660
|
+
let availableVerbs;
|
|
6661
|
+
if (whoami) {
|
|
6662
|
+
const tokenScopes = new Set(whoami.scopes);
|
|
6663
|
+
availableVerbs = filterAvailableVerbs(ALL_VERB_SPECS, tokenScopes);
|
|
6664
|
+
process.stderr.write(`[mcp] kind=${whoami.kind} scopes=[${whoami.scopes.join(",")}]
|
|
6488
6665
|
` + `[mcp] exposing ${availableVerbs.length}/${ALL_VERB_SPECS.length} curated verbs as MCP tools
|
|
6489
6666
|
`);
|
|
6667
|
+
} else {
|
|
6668
|
+
availableVerbs = [...ALL_VERB_SPECS];
|
|
6669
|
+
process.stderr.write(`[mcp] whoami failed after ${WHOAMI_MAX_ATTEMPTS} attempts: ` + `${lastErr instanceof Error ? lastErr.message : String(lastErr)}
|
|
6670
|
+
` + `[mcp] degraded start: exposing all ${availableVerbs.length} curated verbs; ` + `scopes enforced per-call by the server
|
|
6671
|
+
`);
|
|
6672
|
+
}
|
|
6490
6673
|
const server = new McpServer({
|
|
6491
6674
|
name: "echopai",
|
|
6492
6675
|
version: CLI_VERSION,
|
|
@@ -7181,7 +7364,7 @@ function buildDoctorCommand() {
|
|
|
7181
7364
|
|
|
7182
7365
|
// src/tools/schema.ts
|
|
7183
7366
|
import { Command as Command27 } from "commander";
|
|
7184
|
-
import { z as
|
|
7367
|
+
import { z as z18 } from "zod";
|
|
7185
7368
|
|
|
7186
7369
|
// src/_generated/help.ts
|
|
7187
7370
|
var HELP = {
|
|
@@ -7245,6 +7428,11 @@ var HELP = {
|
|
|
7245
7428
|
description: "Returns minute-level OHLC bars for multiple A-share securities in one call: up to 20 codes and up to a 7 calendar-day (~5 trading-day) range. Responses use a partial-success envelope: codes the token may not access are reported in `errors[]`, the rest in `items[]` with a flat `bars[]` array where each bar carries its own `trade_date` for client-side grouping. Requires the `bars:30d` or `bars:full` scope.",
|
|
7246
7429
|
example: "echopai bars minute-batch --codes ['SSE:600519', 'SZSE:000001']"
|
|
7247
7430
|
},
|
|
7431
|
+
"capabilities.show": {
|
|
7432
|
+
summary: "Capability changelog and API version discovery.",
|
|
7433
|
+
description: "Returns the current API spec version plus a machine-readable changelog of capability changes (new endpoints, new fields, behavior fixes), newest first. Check it at session start — or after an unexpected response shape — to discover capabilities added since your integration was written. Free and unauthenticated.",
|
|
7434
|
+
example: "echopai capabilities show"
|
|
7435
|
+
},
|
|
7248
7436
|
"concepts.alerts": {
|
|
7249
7437
|
summary: "List currently active concept alerts.",
|
|
7250
7438
|
description: "Returns the live set of concept alerts. Two rules fire: `big_move` when absolute percent change exceeds 3%, and `limit_up_cluster` when a concept has at least 3 limit-up members across at least 5 member stocks.",
|
|
@@ -7357,7 +7545,7 @@ var HELP = {
|
|
|
7357
7545
|
},
|
|
7358
7546
|
"news.feed": {
|
|
7359
7547
|
summary: "List recent news.",
|
|
7360
|
-
description: "Returns recent news. Equivalent to `/v1/news/list`; retained for backward compatibility.",
|
|
7548
|
+
description: "Returns recent news. Equivalent to `/v1/news/list`; retained for backward compatibility. Partner/agent callers receive `fields=compact` by default (id, title, ai_summary, tagged securities, first ~300 chars of content); pass `fields=full` for complete content.",
|
|
7361
7549
|
example: "echopai news feed"
|
|
7362
7550
|
},
|
|
7363
7551
|
"news.get": {
|
|
@@ -7387,12 +7575,12 @@ var HELP = {
|
|
|
7387
7575
|
},
|
|
7388
7576
|
quote: {
|
|
7389
7577
|
summary: "Get real-time quotes for one or more securities.",
|
|
7390
|
-
description: "Returns real-time quotes for one or more A-share securities (canonical codes, e.g. `SSE:600519`): last price, volume, percent change, and bid/ask. Up to 200 codes per call. Requires a `quote:l1`, `quote:l2`, or `quote:delayed` scope.",
|
|
7578
|
+
description: "Returns real-time quotes for one or more A-share securities (canonical codes, e.g. `SSE:600519`): last price, volume, percent change, and bid/ask. Up to 200 codes per call. The response carries a `session` label (`pre_open` / `auction` / `open` / `break` / `closed`) and, outside continuous trading, an explanatory `note`: during 9:00–9:14 pre-open the previous session's snapshot is returned; during the 9:15–9:29 call auction, prices are indicative auction match prices. Rows whose feed has stopped updating (suspension / feed drop) keep their last snapshot and are flagged `stale: true` with `stale_since`. Requires a `quote:l1`, `quote:l2`, or `quote:delayed` scope.",
|
|
7391
7579
|
example: "echopai quote --codes ['SSE:600519', 'SZSE:000001']"
|
|
7392
7580
|
},
|
|
7393
7581
|
"quote.scan": {
|
|
7394
7582
|
summary: "Snapshot every A-share real-time quote in one call.",
|
|
7395
|
-
description: "Returns a real-time quote for every A-share in a single call — use it for full-market scans or to discover a universe of interest when you do not yet know which codes to query. Filter with `exchange=SSE|SZSE|BSE` to narrow to one exchange. The payload is large (~3 MB, ~5,800 instruments), so pair it with `--fields`, `--jq`, or `--max-bytes` to trim it.
|
|
7583
|
+
description: "Returns a real-time quote for every A-share in a single call — use it for full-market scans or to discover a universe of interest when you do not yet know which codes to query. Filter with `exchange=SSE|SZSE|BSE` to narrow to one exchange. The payload is large (~3 MB, ~5,800 instruments), so pair it with `--fields`, `--jq`, or `--max-bytes` to trim it. The response carries a `session` label (`pre_open` / `auction` / `open` / `break` / `closed`) plus an explanatory `note` outside continuous trading: during 9:00–9:14 pre-open the previous session's snapshot is returned; during the 9:15–9:29 call auction, prices are indicative auction match prices. Instruments whose feed has stopped updating (suspension / feed drop) are excluded from the default scan; pass `include=all` to keep them, flagged with `stale: true` and `stale_since`. Each item also includes `main_net_in` (current-day cumulative net main-capital inflow in CNY, which may be negative) and a companion `main_net_in_stale` flag (true when served from the last-close fallback). Requires a `quote:l1`, `quote:l2`, or `quote:delayed` scope.",
|
|
7396
7584
|
example: "echopai quote scan"
|
|
7397
7585
|
},
|
|
7398
7586
|
"search.semantic": {
|
|
@@ -7442,7 +7630,7 @@ var HELP = {
|
|
|
7442
7630
|
},
|
|
7443
7631
|
"views.feed": {
|
|
7444
7632
|
summary: "List analyst views.",
|
|
7445
|
-
description: "Returns a stream of analyst views, sell-side research, and long-form opinion. Filter by institution, analyst, security, or recency (`hours`). Requires the `views:read` scope.",
|
|
7633
|
+
description: "Returns a stream of analyst views, sell-side research, and long-form opinion. Filter by institution, analyst, security, or recency (`hours`). Partner/agent callers receive `fields=compact` by default (id, ai_summary, attribution, tagged securities, first ~300 chars of content); pass `fields=full` for complete content. Requires the `views:read` scope.",
|
|
7446
7634
|
example: "echopai views feed"
|
|
7447
7635
|
},
|
|
7448
7636
|
"views.get": {
|
|
@@ -7461,7 +7649,7 @@ var HELP = {
|
|
|
7461
7649
|
function verbToRecord(spec) {
|
|
7462
7650
|
let inputSchema;
|
|
7463
7651
|
try {
|
|
7464
|
-
inputSchema =
|
|
7652
|
+
inputSchema = z18.toJSONSchema(z18.object(spec.inputSchema));
|
|
7465
7653
|
} catch {
|
|
7466
7654
|
inputSchema = {
|
|
7467
7655
|
type: "object",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "echopai",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.1",
|
|
4
4
|
"description": "Command-line interface for the EchoPai Open Platform: stock-market data, news, analyst views, sentiment, signals, backtests.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://echopai.com",
|