echopai 2.0.0 → 2.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.
@@ -127,6 +127,38 @@ export function buildCommandTree(program, dispatch) {
127
127
  attachOperation(cmd, OPERATIONS["digest.get"], dispatch);
128
128
  }
129
129
  }
130
+ {
131
+ const noun = program.command("financials");
132
+ noun.description("financials commands");
133
+ {
134
+ const cmd = noun.command("pit");
135
+ attachOperation(cmd, OPERATIONS["financials.pit"], dispatch);
136
+ }
137
+ {
138
+ const cmd = noun.command("reports");
139
+ attachOperation(cmd, OPERATIONS["financials.reports"], dispatch);
140
+ }
141
+ {
142
+ const cmd = noun.command("series");
143
+ attachOperation(cmd, OPERATIONS["financials.series"], dispatch);
144
+ }
145
+ }
146
+ {
147
+ const noun = program.command("index");
148
+ noun.description("index commands");
149
+ {
150
+ const cmd = noun.command("daily-bars");
151
+ attachOperation(cmd, OPERATIONS["index.daily-bars"], dispatch);
152
+ }
153
+ {
154
+ const cmd = noun.command("minute-bars");
155
+ attachOperation(cmd, OPERATIONS["index.minute-bars"], dispatch);
156
+ }
157
+ {
158
+ const cmd = noun.command("snapshot");
159
+ attachOperation(cmd, OPERATIONS["index.snapshot"], dispatch);
160
+ }
161
+ }
130
162
  {
131
163
  const noun = program.command("market");
132
164
  noun.description("market commands");
@@ -191,6 +223,22 @@ export function buildCommandTree(program, dispatch) {
191
223
  attachOperation(cmd, OPERATIONS["research.entity-performance-list"], dispatch);
192
224
  }
193
225
  }
226
+ {
227
+ const noun = program.command("search");
228
+ noun.description("search commands");
229
+ {
230
+ const cmd = noun.command("semantic");
231
+ attachOperation(cmd, OPERATIONS["search.semantic"], dispatch);
232
+ }
233
+ }
234
+ {
235
+ const noun = program.command("securities");
236
+ noun.description("securities commands");
237
+ {
238
+ const cmd = noun.command("industry");
239
+ attachOperation(cmd, OPERATIONS["securities.industry"], dispatch);
240
+ }
241
+ }
194
242
  {
195
243
  const noun = program.command("semantic");
196
244
  noun.description("semantic commands");
@@ -62,6 +62,36 @@ export const HELP = {
62
62
  "description": "Composite endpoint: in one HTTP call returns views (PRIMARY research),\nresearch-entity performance (quality layer), real-time quote, market\nsentiment context, and supplementary news. Partial-failure tolerant:\neach bucket is independently fetched and per-bucket failures surface\nin `meta.partial_failures[]` rather than poisoning the response. If\nevery sub-bucket fails the endpoint returns 502.\n\nBucket scopes are checked *per bucket* (not at gateway level): a\ntoken with only `views:read` gets the views bucket populated and the\nrest reported as `scope_insufficient` partial failures. This mirrors\nthe CLI fan-out contract exactly so `echopai digest` can either call\nthis endpoint (preferred, single round-trip) or fall back to local\nfan-out without behavior drift.\n\nSee `docs/PLAN_CLI_V2_AGENT_SURFACE.md` §3.3 (digest spec) and §11\nPhase 5.2 (server endpoint).\n",
63
63
  "example": "echopai digest get VALUE"
64
64
  },
65
+ "financials.pit": {
66
+ "summary": "Point-in-time financial indicators for one A-share at a given date",
67
+ "description": "基于 TDX 财务数据的 Point-in-time 查询。给定 `code` 和 `date`,返回该\n`trade_date` 当时市场上可见的最新一期财务报告(`announce_date <= date`,\n无公告日时保守延迟 90 天)。专为回测、AI agent、量化策略防未来函数泄漏。\n\n返回字段包括:EPS(基本/扣非/稀释)、BPS、ROE、毛利率、营收/净利润/总资产/\n归母权益等核心 ~25 字段。\n",
68
+ "example": "echopai financials pit --code SSE:600519 --date 2024-11-01"
69
+ },
70
+ "financials.reports": {
71
+ "summary": "Recent financial reports for one A-share security",
72
+ "description": "返回该股票最近 N 期财务报告的核心指标(EPS/BPS/ROE/毛利率/营收/净利润/总\n资产/归母权益/经营现金流/总股本 等 ~25 字段)。可按 `kind` 过滤季报/半年报/\n年报。每期 `announce_date` 字段告知何时市场可见,建议结合回测时使用。\n",
73
+ "example": "echopai financials reports --code SSE:600519"
74
+ },
75
+ "financials.series": {
76
+ "summary": "Time series of a single financial metric for one security",
77
+ "description": "返回某只股票某个财务指标的历史时间序列(按报告期排列)。`metric` 是\nindicators 表中的字段名(如 `roe_simple` / `revenue` / `ni_parent` /\n`debt_asset_ratio` / `gross_margin` / `eps_basic` 等共 150 个)。\n\n典型用法:画茅台 10 年 ROE 走势、对比 5 只白酒股 net_margin。\n",
78
+ "example": "echopai financials series --code SSE:600519 --metric roe_simple"
79
+ },
80
+ "index.daily-bars": {
81
+ "summary": "Daily OHLC bars for one A-share index",
82
+ "description": "指数日线 OHLC。覆盖 v2 表中的指数(上证综指 000001.SH / 深证成指\n399001.SZ / 沪深300 000300.SH / 北证50 899050.BJ / 中证系列 000922.CSI 等)。\n与 `/v1/bars/daily` 同语义,但 index 没有 `turnover_rate` / `paused` 等股票特有字段。\n",
83
+ "example": "echopai index daily-bars --code 000001.SH --from 2024-01-01 --to 2024-12-31"
84
+ },
85
+ "index.minute-bars": {
86
+ "summary": "Minute OHLC bars for one A-share index",
87
+ "description": "指数 1min OHLC。日期范围 ≤ 7 天(与 `/v1/bars/minute-batch` 对齐)。\n返回 `bar_time` / `trade_date` / OHLC / volume / amount / pct_change。\n",
88
+ "example": "echopai index minute-bars --code 000001.SH"
89
+ },
90
+ "index.snapshot": {
91
+ "summary": "Real-time snapshot of all Sina-OK A-share indices (172 indices)",
92
+ "description": "Returns the latest real-time snapshot for the 172 indices that Sina\nexposes a quote for (SSE / SZSE / BSE — includes 北证50 `899050.BJ`\nand 专精特新 `899601.BJ`). Source is the Redis hash\n`stockpulse:index_snapshots:latest` which the Rust collector refreshes\nevery ~15 seconds during market hours; outside trading hours the last\nintraday snapshot is returned.\n\nUse `codes` to narrow to a subset (canonical format like `000001.SH`).\nOmit to receive all 172.\n\nNote: CSI-series indices (e.g. `000922.CSI`) are not available here —\nSina has no quote feed for them. Daily/minute history for those still\nlives behind `/api/internal/index/{daily-bars,minute-bars-range}`.\n\nRequires `quote:l1`, `quote:l2`, or `quote:delayed` scope.\n",
93
+ "example": "echopai index snapshot --codes 000001.SH,399001.SZ,899050.BJ"
94
+ },
65
95
  "market.status": {
66
96
  "summary": "Current A-share market session state",
67
97
  "description": "Current A-share market session: pre-open / open / lunch / closed; current and next trading day; holiday flag. Requires `market:read` or any `quote:*` scope.",
@@ -74,7 +104,7 @@ export const HELP = {
74
104
  },
75
105
  "news.get": {
76
106
  "summary": "Fetch a single news item by id",
77
- "description": "返回新闻完整内容:title、content snippet、source URL、tagged securities。",
107
+ "description": "返回新闻完整内容:title、content snippet、tagged securities。(采集端字段 source / source_id / source_url 对外不暴露,admin 路径 /v1/admin/news/{id} 可见。)",
78
108
  "example": "echopai news get VALUE"
79
109
  },
80
110
  "news.list": {
@@ -84,7 +114,7 @@ export const HELP = {
84
114
  },
85
115
  "news.search": {
86
116
  "summary": "Full-text search recent news",
87
- "description": "Full-text search recent news / market briefs. Returns title, source, published_at, snippet, tagged securities. Requires `news:read` scope. Note: news fields are user-generated; meta.untrusted_text_fields lists fields that need sanitization before passing to an LLM.",
117
+ "description": "Full-text search recent news / market briefs. Returns title, published_at, snippet, tagged securities. (Internal collector identifiers — `source`/`source_id`/`source_url` — are NOT exposed publicly; admin tools see them via /v1/admin/*.) Requires `news:read` scope. Note: news fields are user-generated; meta.untrusted_text_fields lists fields that need sanitization before passing to an LLM.",
88
118
  "example": "echopai news search --query 光伏龙头"
89
119
  },
90
120
  "news.sources": {
@@ -117,6 +147,16 @@ export const HELP = {
117
147
  "description": "全体研究实体(analyst / team / institution)的命中率、平均目标价达成率、覆盖股票数等汇总。需要 `research:read` scope。",
118
148
  "example": "echopai research entity-performance-list"
119
149
  },
150
+ "search.semantic": {
151
+ "summary": "Hybrid semantic search across news + analyst views",
152
+ "description": "语义 + 模糊泛化搜索。结合 entity 精确匹配(公司代码/分析师)、向量召回(pgvector\n+ DashScope embedding)、trigram 模糊、ILIKE 精确四路召回,RRF 融合后用 reranker\n精排,叠时间衰减 + views 加权(views 主源优先)。\n\n- 搜\"锂电池\"会带出新能源产业链(正极/负极/碳酸锂/宁德时代…)\n- 搜\"芯片\"会带出存储芯片/洁净室/晶圆代工…\n- mode=exact 兼容老 ILIKE 行为,仅在精确关键词命中时返回。\n",
153
+ "example": "echopai search semantic --q 锂电池"
154
+ },
155
+ "securities.industry": {
156
+ "summary": "TDX + 申万 industry classification for one A-share security",
157
+ "description": "返回该股票的 TDX 自定义行业代码(T1001 食品饮料等)与申万行业三级代码\nL1/L2/L3(X5001 食品饮料 / X500102 白酒 等)。\n\n用法:\n- 股票详情页\"申万行业:白酒 / 食品饮料\"标签\n- 筛选器交叉过滤:行业 + ROE/营收增长率\n- AI agent `find_peers(code)` 用申万二级行业找同业\n",
158
+ "example": "echopai securities industry --code SSE:600519"
159
+ },
120
160
  "semantic.find": {
121
161
  "summary": "Find A-share securities by name / code / pinyin",
122
162
  "description": "Find A-share securities matching a name (Chinese), code (e.g. 600519), or pinyin initials (e.g. gzmt). Use this when an agent has a description and needs canonical codes.",
@@ -486,6 +486,285 @@ export const OPERATIONS = {
486
486
  ]
487
487
  }
488
488
  },
489
+ "financials.pit": {
490
+ "cliKey": "financials.pit",
491
+ "cliName": "financials pit",
492
+ "method": "GET",
493
+ "path": "/v1/financials/pit",
494
+ "description": "基于 TDX 财务数据的 Point-in-time 查询。给定 `code` 和 `date`,返回该\n`trade_date` 当时市场上可见的最新一期财务报告(`announce_date <= date`,\n无公告日时保守延迟 90 天)。专为回测、AI agent、量化策略防未来函数泄漏。\n\n返回字段包括:EPS(基本/扣非/稀释)、BPS、ROE、毛利率、营收/净利润/总资产/\n归母权益等核心 ~25 字段。\n",
495
+ "summary": "Point-in-time financial indicators for one A-share at a given date",
496
+ "positional": [],
497
+ "outputDefault": "json",
498
+ "pagination": "none",
499
+ "stream": false,
500
+ "billable": true,
501
+ "idempotencyRequired": false,
502
+ "scopesAny": [
503
+ "financials:read"
504
+ ],
505
+ "sideEffect": "read",
506
+ "dryRunSupported": false,
507
+ "inputSchema": {
508
+ "type": "object",
509
+ "properties": {
510
+ "code": {
511
+ "type": "string",
512
+ "pattern": "^(SSE|SZSE|BSE|SH|SZ|BJ):[0-9]{6}$",
513
+ "description": "Canonical A-share code.",
514
+ "example": "SSE:600519"
515
+ },
516
+ "date": {
517
+ "type": "string",
518
+ "format": "date",
519
+ "description": "Trade date in YYYY-MM-DD; defaults to today.",
520
+ "example": "2024-11-01"
521
+ }
522
+ },
523
+ "additionalProperties": false,
524
+ "required": [
525
+ "code"
526
+ ]
527
+ }
528
+ },
529
+ "financials.reports": {
530
+ "cliKey": "financials.reports",
531
+ "cliName": "financials reports",
532
+ "method": "GET",
533
+ "path": "/v1/financials/reports",
534
+ "description": "返回该股票最近 N 期财务报告的核心指标(EPS/BPS/ROE/毛利率/营收/净利润/总\n资产/归母权益/经营现金流/总股本 等 ~25 字段)。可按 `kind` 过滤季报/半年报/\n年报。每期 `announce_date` 字段告知何时市场可见,建议结合回测时使用。\n",
535
+ "summary": "Recent financial reports for one A-share security",
536
+ "positional": [],
537
+ "outputDefault": "json",
538
+ "pagination": "none",
539
+ "stream": false,
540
+ "billable": true,
541
+ "idempotencyRequired": false,
542
+ "scopesAny": [
543
+ "financials:read"
544
+ ],
545
+ "sideEffect": "read",
546
+ "dryRunSupported": false,
547
+ "inputSchema": {
548
+ "type": "object",
549
+ "properties": {
550
+ "code": {
551
+ "type": "string",
552
+ "pattern": "^(SSE|SZSE|BSE|SH|SZ|BJ):[0-9]{6}$",
553
+ "description": "Canonical A-share code.",
554
+ "example": "SSE:600519"
555
+ },
556
+ "limit": {
557
+ "type": "integer",
558
+ "minimum": 1,
559
+ "maximum": 100,
560
+ "default": 12,
561
+ "description": "Max periods to return (1-100, default 12)."
562
+ },
563
+ "kind": {
564
+ "type": "string",
565
+ "enum": [
566
+ "Q1",
567
+ "H1",
568
+ "Q3",
569
+ "annual",
570
+ "preliminary"
571
+ ],
572
+ "description": "Filter by report kind."
573
+ }
574
+ },
575
+ "additionalProperties": false,
576
+ "required": [
577
+ "code"
578
+ ]
579
+ }
580
+ },
581
+ "financials.series": {
582
+ "cliKey": "financials.series",
583
+ "cliName": "financials series",
584
+ "method": "GET",
585
+ "path": "/v1/financials/series",
586
+ "description": "返回某只股票某个财务指标的历史时间序列(按报告期排列)。`metric` 是\nindicators 表中的字段名(如 `roe_simple` / `revenue` / `ni_parent` /\n`debt_asset_ratio` / `gross_margin` / `eps_basic` 等共 150 个)。\n\n典型用法:画茅台 10 年 ROE 走势、对比 5 只白酒股 net_margin。\n",
587
+ "summary": "Time series of a single financial metric for one security",
588
+ "positional": [],
589
+ "outputDefault": "json",
590
+ "pagination": "none",
591
+ "stream": false,
592
+ "billable": true,
593
+ "idempotencyRequired": false,
594
+ "scopesAny": [
595
+ "financials:read"
596
+ ],
597
+ "sideEffect": "read",
598
+ "dryRunSupported": false,
599
+ "inputSchema": {
600
+ "type": "object",
601
+ "properties": {
602
+ "code": {
603
+ "type": "string",
604
+ "pattern": "^(SSE|SZSE|BSE|SH|SZ|BJ):[0-9]{6}$",
605
+ "description": "Canonical A-share code.",
606
+ "example": "SSE:600519"
607
+ },
608
+ "metric": {
609
+ "type": "string",
610
+ "minLength": 1,
611
+ "maxLength": 64,
612
+ "description": "Indicator field name (e.g. roe_simple, revenue, ni_parent).",
613
+ "example": "roe_simple"
614
+ },
615
+ "from": {
616
+ "type": "string",
617
+ "format": "date",
618
+ "description": "Inclusive earliest report_date."
619
+ },
620
+ "to": {
621
+ "type": "string",
622
+ "format": "date",
623
+ "description": "Inclusive latest report_date."
624
+ },
625
+ "limit": {
626
+ "type": "integer",
627
+ "minimum": 1,
628
+ "maximum": 200,
629
+ "default": 40,
630
+ "description": "Max points (1-200, default 40)."
631
+ }
632
+ },
633
+ "additionalProperties": false,
634
+ "required": [
635
+ "code",
636
+ "metric"
637
+ ]
638
+ }
639
+ },
640
+ "index.daily-bars": {
641
+ "cliKey": "index.daily-bars",
642
+ "cliName": "index daily-bars",
643
+ "method": "GET",
644
+ "path": "/v1/index/bars/daily",
645
+ "description": "指数日线 OHLC。覆盖 v2 表中的指数(上证综指 000001.SH / 深证成指\n399001.SZ / 沪深300 000300.SH / 北证50 899050.BJ / 中证系列 000922.CSI 等)。\n与 `/v1/bars/daily` 同语义,但 index 没有 `turnover_rate` / `paused` 等股票特有字段。\n",
646
+ "summary": "Daily OHLC bars for one A-share index",
647
+ "positional": [],
648
+ "outputDefault": "json",
649
+ "pagination": "none",
650
+ "stream": false,
651
+ "billable": true,
652
+ "idempotencyRequired": false,
653
+ "scopesAny": [
654
+ "quote:l1",
655
+ "quote:l2",
656
+ "quote:delayed"
657
+ ],
658
+ "sideEffect": "read",
659
+ "dryRunSupported": false,
660
+ "inputSchema": {
661
+ "type": "object",
662
+ "properties": {
663
+ "code": {
664
+ "type": "string",
665
+ "description": "Canonical index code.",
666
+ "example": "000001.SH"
667
+ },
668
+ "from": {
669
+ "type": "string",
670
+ "format": "date",
671
+ "description": "Inclusive start date (YYYY-MM-DD).",
672
+ "example": "2024-01-01"
673
+ },
674
+ "to": {
675
+ "type": "string",
676
+ "format": "date",
677
+ "description": "Inclusive end date.",
678
+ "example": "2024-12-31"
679
+ }
680
+ },
681
+ "additionalProperties": false,
682
+ "required": [
683
+ "code",
684
+ "from",
685
+ "to"
686
+ ]
687
+ }
688
+ },
689
+ "index.minute-bars": {
690
+ "cliKey": "index.minute-bars",
691
+ "cliName": "index minute-bars",
692
+ "method": "GET",
693
+ "path": "/v1/index/bars/minute",
694
+ "description": "指数 1min OHLC。日期范围 ≤ 7 天(与 `/v1/bars/minute-batch` 对齐)。\n返回 `bar_time` / `trade_date` / OHLC / volume / amount / pct_change。\n",
695
+ "summary": "Minute OHLC bars for one A-share index",
696
+ "positional": [],
697
+ "outputDefault": "json",
698
+ "pagination": "none",
699
+ "stream": false,
700
+ "billable": true,
701
+ "idempotencyRequired": false,
702
+ "scopesAny": [
703
+ "quote:l1",
704
+ "quote:l2",
705
+ "quote:delayed"
706
+ ],
707
+ "sideEffect": "read",
708
+ "dryRunSupported": false,
709
+ "inputSchema": {
710
+ "type": "object",
711
+ "properties": {
712
+ "code": {
713
+ "type": "string",
714
+ "description": "Canonical index code.",
715
+ "example": "000001.SH"
716
+ },
717
+ "from": {
718
+ "type": "string",
719
+ "format": "date",
720
+ "description": "Inclusive start date (YYYY-MM-DD)."
721
+ },
722
+ "to": {
723
+ "type": "string",
724
+ "format": "date",
725
+ "description": "Inclusive end date. Range ≤ 7 calendar days."
726
+ }
727
+ },
728
+ "additionalProperties": false,
729
+ "required": [
730
+ "code",
731
+ "from",
732
+ "to"
733
+ ]
734
+ }
735
+ },
736
+ "index.snapshot": {
737
+ "cliKey": "index.snapshot",
738
+ "cliName": "index snapshot",
739
+ "method": "GET",
740
+ "path": "/v1/index/snapshot",
741
+ "description": "Returns the latest real-time snapshot for the 172 indices that Sina\nexposes a quote for (SSE / SZSE / BSE — includes 北证50 `899050.BJ`\nand 专精特新 `899601.BJ`). Source is the Redis hash\n`stockpulse:index_snapshots:latest` which the Rust collector refreshes\nevery ~15 seconds during market hours; outside trading hours the last\nintraday snapshot is returned.\n\nUse `codes` to narrow to a subset (canonical format like `000001.SH`).\nOmit to receive all 172.\n\nNote: CSI-series indices (e.g. `000922.CSI`) are not available here —\nSina has no quote feed for them. Daily/minute history for those still\nlives behind `/api/internal/index/{daily-bars,minute-bars-range}`.\n\nRequires `quote:l1`, `quote:l2`, or `quote:delayed` scope.\n",
742
+ "summary": "Real-time snapshot of all Sina-OK A-share indices (172 indices)",
743
+ "positional": [],
744
+ "outputDefault": "json",
745
+ "pagination": "none",
746
+ "stream": false,
747
+ "billable": true,
748
+ "idempotencyRequired": false,
749
+ "scopesAny": [
750
+ "quote:l1",
751
+ "quote:l2",
752
+ "quote:delayed"
753
+ ],
754
+ "sideEffect": "read",
755
+ "dryRunSupported": false,
756
+ "inputSchema": {
757
+ "type": "object",
758
+ "properties": {
759
+ "codes": {
760
+ "type": "string",
761
+ "description": "Comma-separated canonical index codes (e.g. `000001.SH,399001.SZ,899050.BJ`).\nOmit to return all 172.\n",
762
+ "example": "000001.SH,399001.SZ,899050.BJ"
763
+ }
764
+ },
765
+ "additionalProperties": false
766
+ }
767
+ },
489
768
  "market.status": {
490
769
  "cliKey": "market.status",
491
770
  "cliName": "market status",
@@ -540,7 +819,7 @@ export const OPERATIONS = {
540
819
  "cliName": "news get",
541
820
  "method": "GET",
542
821
  "path": "/v1/news/{news_id}",
543
- "description": "返回新闻完整内容:title、content snippet、source URL、tagged securities。",
822
+ "description": "返回新闻完整内容:title、content snippet、tagged securities。(采集端字段 source / source_id / source_url 对外不暴露,admin 路径 /v1/admin/news/{id} 可见。)",
544
823
  "summary": "Fetch a single news item by id",
545
824
  "positional": [
546
825
  "news_id"
@@ -620,7 +899,7 @@ export const OPERATIONS = {
620
899
  "cliName": "news search",
621
900
  "method": "GET",
622
901
  "path": "/v1/news/search",
623
- "description": "Full-text search recent news / market briefs. Returns title, source, published_at, snippet, tagged securities. Requires `news:read` scope. Note: news fields are user-generated; meta.untrusted_text_fields lists fields that need sanitization before passing to an LLM.",
902
+ "description": "Full-text search recent news / market briefs. Returns title, published_at, snippet, tagged securities. (Internal collector identifiers — `source`/`source_id`/`source_url` — are NOT exposed publicly; admin tools see them via /v1/admin/*.) Requires `news:read` scope. Note: news fields are user-generated; meta.untrusted_text_fields lists fields that need sanitization before passing to an LLM.",
624
903
  "summary": "Full-text search recent news",
625
904
  "positional": [],
626
905
  "outputDefault": "json",
@@ -850,6 +1129,106 @@ export const OPERATIONS = {
850
1129
  "additionalProperties": false
851
1130
  }
852
1131
  },
1132
+ "search.semantic": {
1133
+ "cliKey": "search.semantic",
1134
+ "cliName": "search semantic",
1135
+ "method": "GET",
1136
+ "path": "/v1/search",
1137
+ "description": "语义 + 模糊泛化搜索。结合 entity 精确匹配(公司代码/分析师)、向量召回(pgvector\n+ DashScope embedding)、trigram 模糊、ILIKE 精确四路召回,RRF 融合后用 reranker\n精排,叠时间衰减 + views 加权(views 主源优先)。\n\n- 搜\"锂电池\"会带出新能源产业链(正极/负极/碳酸锂/宁德时代…)\n- 搜\"芯片\"会带出存储芯片/洁净室/晶圆代工…\n- mode=exact 兼容老 ILIKE 行为,仅在精确关键词命中时返回。\n",
1138
+ "summary": "Hybrid semantic search across news + analyst views",
1139
+ "positional": [],
1140
+ "outputDefault": "json",
1141
+ "pagination": "none",
1142
+ "stream": false,
1143
+ "billable": true,
1144
+ "idempotencyRequired": false,
1145
+ "scopesAny": [
1146
+ "news:read",
1147
+ "views:read"
1148
+ ],
1149
+ "sideEffect": "read",
1150
+ "dryRunSupported": false,
1151
+ "inputSchema": {
1152
+ "type": "object",
1153
+ "properties": {
1154
+ "q": {
1155
+ "type": "string",
1156
+ "minLength": 1,
1157
+ "maxLength": 200,
1158
+ "description": "Free-text query (Chinese or English).",
1159
+ "example": "锂电池"
1160
+ },
1161
+ "type": {
1162
+ "type": "string",
1163
+ "enum": [
1164
+ "news",
1165
+ "views",
1166
+ "all"
1167
+ ],
1168
+ "default": "all",
1169
+ "description": "Search scope: news | views | all"
1170
+ },
1171
+ "mode": {
1172
+ "type": "string",
1173
+ "enum": [
1174
+ "hybrid",
1175
+ "exact"
1176
+ ],
1177
+ "default": "hybrid",
1178
+ "description": "hybrid = 语义 + 精确融合 + rerank;exact = 仅 ILIKE 兼容老逻辑"
1179
+ },
1180
+ "hours": {
1181
+ "type": "integer",
1182
+ "minimum": 1,
1183
+ "maximum": 720,
1184
+ "description": "Lookback window in hours."
1185
+ },
1186
+ "limit": {
1187
+ "type": "integer",
1188
+ "minimum": 1,
1189
+ "maximum": 50,
1190
+ "default": 20,
1191
+ "description": "Max items returned."
1192
+ }
1193
+ },
1194
+ "additionalProperties": false,
1195
+ "required": [
1196
+ "q"
1197
+ ]
1198
+ }
1199
+ },
1200
+ "securities.industry": {
1201
+ "cliKey": "securities.industry",
1202
+ "cliName": "securities industry",
1203
+ "method": "GET",
1204
+ "path": "/v1/securities/industry",
1205
+ "description": "返回该股票的 TDX 自定义行业代码(T1001 食品饮料等)与申万行业三级代码\nL1/L2/L3(X5001 食品饮料 / X500102 白酒 等)。\n\n用法:\n- 股票详情页\"申万行业:白酒 / 食品饮料\"标签\n- 筛选器交叉过滤:行业 + ROE/营收增长率\n- AI agent `find_peers(code)` 用申万二级行业找同业\n",
1206
+ "summary": "TDX + 申万 industry classification for one A-share security",
1207
+ "positional": [],
1208
+ "outputDefault": "json",
1209
+ "pagination": "none",
1210
+ "stream": false,
1211
+ "billable": false,
1212
+ "idempotencyRequired": false,
1213
+ "scopesAny": [],
1214
+ "sideEffect": "read",
1215
+ "dryRunSupported": false,
1216
+ "inputSchema": {
1217
+ "type": "object",
1218
+ "properties": {
1219
+ "code": {
1220
+ "type": "string",
1221
+ "pattern": "^(SSE|SZSE|BSE|SH|SZ|BJ):[0-9]{6}$",
1222
+ "description": "Canonical A-share code.",
1223
+ "example": "SSE:600519"
1224
+ }
1225
+ },
1226
+ "additionalProperties": false,
1227
+ "required": [
1228
+ "code"
1229
+ ]
1230
+ }
1231
+ },
853
1232
  "semantic.find": {
854
1233
  "cliKey": "semantic.find",
855
1234
  "cliName": "semantic find",
package/dist/bin.js CHANGED
@@ -20,7 +20,7 @@ import { invoke } from "./runtime/invoker.js";
20
20
  import { buildApiCommand } from "./tools/api.js";
21
21
  import { buildMcpCommand } from "./tools/mcp.js";
22
22
  import { buildRawCallCommand } from "./tools/raw.js";
23
- import { buildBarsBatchCommand, buildChartCommand, buildDigestCommand, buildHotCommand, buildLookupCommand, buildNewsCommand, buildQuoteCommand, buildResearchCommand, buildScanCommand, buildSentimentCommand, buildViewsCommand, } from "./verbs/index.js";
23
+ import { buildBarsBatchCommand, buildChartCommand, buildDigestCommand, buildHotCommand, buildLookupCommand, buildNewsCommand, buildQuoteCommand, buildResearchCommand, buildScanCommand, buildSearchCommand, buildSentimentCommand, buildViewsCommand, } from "./verbs/index.js";
24
24
  import { buildCompletionCommand } from "./tools/completion.js";
25
25
  import { buildConfigCommand } from "./tools/config.js";
26
26
  import { buildLoginCommand, buildLogoutCommand, buildStatusCommand } from "./tools/login.js";
@@ -120,6 +120,7 @@ const CURATED_NOUNS = new Set([
120
120
  "hot",
121
121
  "lookup",
122
122
  "digest",
123
+ "search",
123
124
  ]);
124
125
  const topLevelSpecDispatch = async (op, args) => {
125
126
  await dispatch(op, args);
@@ -137,6 +138,7 @@ for (const sub of topLevelStub.commands) {
137
138
  // Product priority (see feedback_views_over_news.md): views > research > news
138
139
  program.addCommand(buildLookupCommand());
139
140
  program.addCommand(buildDigestCommand()); // one-shot fan-out (Phase 5.1)
141
+ program.addCommand(buildSearchCommand()); // hybrid semantic discovery (PLAN_SEARCH_SEMANTIC_UPGRADE)
140
142
  program.addCommand(buildQuoteCommand());
141
143
  program.addCommand(buildViewsCommand()); // PRIMARY research source
142
144
  program.addCommand(buildResearchCommand()); // views quality signal layer
@@ -14,6 +14,7 @@ export { newsSpec, buildNewsCommand } from "./news.js";
14
14
  export { quoteSpec, buildQuoteCommand } from "./quote.js";
15
15
  export { researchSpec, buildResearchCommand } from "./research.js";
16
16
  export { scanSpec, buildScanCommand } from "./scan.js";
17
+ export { searchSpec, buildSearchCommand } from "./search.js";
17
18
  export { sentimentSpec, buildSentimentCommand } from "./sentiment.js";
18
19
  export { viewsSpec, buildViewsCommand } from "./views.js";
19
20
  import { barsBatchSpec } from "./bars_batch.js";
@@ -25,18 +26,20 @@ import { newsSpec } from "./news.js";
25
26
  import { quoteSpec } from "./quote.js";
26
27
  import { researchSpec } from "./research.js";
27
28
  import { scanSpec } from "./scan.js";
29
+ import { searchSpec } from "./search.js";
28
30
  import { sentimentSpec } from "./sentiment.js";
29
31
  import { viewsSpec } from "./views.js";
30
32
  /**
31
33
  * Ordering follows product priority (feedback_views_over_news.md):
32
34
  * lookup (universal first step) → digest (one-shot fan-out, agent's opening
33
- * move once it has a canonical_code) → quote views/research (PRIMARY
34
- * research) news (supplementary) → sentimenthot chartbars_batch →
35
- * scan.
35
+ * move once it has a canonical_code) → search (hybrid semantic discovery
36
+ * for themes / concepts) → quoteviews/research (PRIMARY research)news
37
+ * (supplementary) → sentiment → hot → chart → bars_batch → scan.
36
38
  */
37
39
  export const ALL_VERB_SPECS = [
38
40
  lookupSpec,
39
41
  digestSpec,
42
+ searchSpec,
40
43
  quoteSpec,
41
44
  viewsSpec,
42
45
  researchSpec,
@@ -0,0 +1,105 @@
1
+ /**
2
+ * `echopai search ...` + MCP tool `search`.
3
+ *
4
+ * HYBRID semantic search across news + analyst views.
5
+ *
6
+ * Use this verb when:
7
+ * - the user is exploring a THEME / 概念 ("锂电池", "芯片", "AI 算力") — pure
8
+ * vector + concept expansion brings out the full supply chain even when
9
+ * the query word never appears verbatim in the doc.
10
+ * - the user gives a non-canonical company name or analyst name — entity
11
+ * lane resolves it through securities / research_entities tables.
12
+ *
13
+ * Prefer this over `news` / `views` for open-ended discovery. For strict
14
+ * time-window listing or filtered feed, keep using `views` / `news`.
15
+ */
16
+ import { Command, Option } from "commander";
17
+ import { z } from "zod";
18
+ import { OPERATIONS } from "../_generated/operations.js";
19
+ import { executeVerb } from "../runtime/verb_cmd.js";
20
+ import { callOp } from "../runtime/verb_runner.js";
21
+ export const searchSpec = {
22
+ name: "search",
23
+ description: "HYBRID semantic search across analyst views + news (views weighted higher per feedback_views_over_news). Combines entity lookup (company codes, analyst names), pgvector kNN, trigram fuzzy and exact ILIKE; RRF-fused then reranked. Best for theme/concept queries: '锂电池' brings out 正极材料/碳酸锂/宁德时代; '芯片' brings out 存储芯片/洁净室/晶圆代工. Use `news` or `views` instead if you need strict time-window listing.",
24
+ inputSchema: {
25
+ q: z
26
+ .string()
27
+ .min(1)
28
+ .max(200)
29
+ .describe("Free-text query (Chinese or English)"),
30
+ type: z
31
+ .enum(["news", "views", "all"])
32
+ .default("all")
33
+ .describe("Search scope (views weighted higher in 'all')"),
34
+ mode: z
35
+ .enum(["hybrid", "exact"])
36
+ .default("hybrid")
37
+ .describe("hybrid = semantic + concept expansion + rerank; exact = ILIKE only"),
38
+ hours: z
39
+ .number()
40
+ .int()
41
+ .min(1)
42
+ .max(720)
43
+ .optional()
44
+ .describe("Lookback window in hours; omit for no time limit"),
45
+ limit: z
46
+ .number()
47
+ .int()
48
+ .min(1)
49
+ .max(50)
50
+ .default(20)
51
+ .describe("Max items returned"),
52
+ },
53
+ handler: async (args, ctx) => {
54
+ const op = OPERATIONS["search.semantic"];
55
+ if (!op)
56
+ throw new Error("search.semantic op missing from codegen");
57
+ const callArgs = {
58
+ q: args.q,
59
+ type: args.type,
60
+ mode: args.mode,
61
+ limit: args.limit,
62
+ };
63
+ if (args.hours !== undefined)
64
+ callArgs.hours = args.hours;
65
+ return callOp(op, callArgs, ctx);
66
+ },
67
+ backingOps: ["search.semantic"],
68
+ };
69
+ function clamp(raw, min, max, fallback) {
70
+ const n = Math.floor(Number(raw));
71
+ if (!Number.isFinite(n))
72
+ return fallback;
73
+ if (n < min)
74
+ return min;
75
+ if (n > max)
76
+ return max;
77
+ return n;
78
+ }
79
+ export function buildSearchCommand() {
80
+ const cmd = new Command(searchSpec.name).description(searchSpec.description);
81
+ cmd.argument("<query...>", "Search query (Chinese or English)");
82
+ cmd.addOption(new Option("--type <scope>", "Search scope")
83
+ .choices(["news", "views", "all"])
84
+ .default("all"));
85
+ cmd.addOption(new Option("--mode <mode>", "hybrid | exact")
86
+ .choices(["hybrid", "exact"])
87
+ .default("hybrid"));
88
+ cmd.addOption(new Option("--exact", "shortcut for --mode exact"));
89
+ cmd.addOption(new Option("--hours <n>", "Lookback window (1-720 hours)"));
90
+ cmd.addOption(new Option("--limit <n>", "Max items (1-50)").default("20"));
91
+ cmd.action(async (queryTokens, opts) => {
92
+ const q = queryTokens.join(" ").trim();
93
+ const args = {
94
+ q,
95
+ type: opts.type,
96
+ mode: opts.exact ? "exact" : opts.mode,
97
+ limit: clamp(opts.limit, 1, 50, 20),
98
+ };
99
+ if (opts.hours !== undefined) {
100
+ args.hours = clamp(opts.hours, 1, 720, 24);
101
+ }
102
+ await executeVerb(async (ctx) => searchSpec.handler(args, ctx));
103
+ });
104
+ return cmd;
105
+ }
package/dist/version.js CHANGED
@@ -2,4 +2,4 @@
2
2
  * Pinned at build time. Don't read package.json at runtime — adds startup latency
3
3
  * and breaks bundling.
4
4
  */
5
- export const CLI_VERSION = "2.0.0";
5
+ export const CLI_VERSION = "2.1.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "echopai",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
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",