♻️ refactor(tsl-api-reference): split lookup workflows and maintenance tooling
This commit is contained in:
+1
-1
@@ -15,7 +15,7 @@
|
||||
| `gitea-fix-ci` | 基于 Gitea Actions run/job/log 诊断失败 CI,先形成修复计划再改代码;含 `fetch_ci_logs.py` 取证脚本 | Gitea PR checks 失败、远端 CI 红但本地需要定位 |
|
||||
| `cook-it-through` | 运行任务路由、Matt 工程主链和本地 Markdown ticket 生命周期 | 处理实质性工程任务,或操作 `.scratch` queue、claim、finish、integrate |
|
||||
| `tsl-syntax-reference` | 查询 TSL 语法条目,验证具体写法和错误边界;不负责 API、命名、风格、工具链或模块集成 | 写/改/审 TSL 时确认语言结构、表达式、控制流、对象模型和语法限制 |
|
||||
| `tsl-api-reference` | 查询随 skill 分发的 TSL API 参考:按名精确查条目,或按中文关键词发现候选 | 写/审 TSL 时确认 builtin、dotnet、模块 API 的签名、参数、返回值和示例 |
|
||||
| `tsl-api-reference` | 查询 TSL API、class/framework 契约和天软数据字典;按 workflow 路由并由脚本 help 定义调用契约 | 确认 API/成员/回调的签名和示例,或按中文业务含义查表、数据源与字段 |
|
||||
|
||||
## 第三方 Skills
|
||||
|
||||
|
||||
@@ -1,157 +1,50 @@
|
||||
---
|
||||
name: tsl-api-reference
|
||||
description: "当编写、修改或审查 TSL 代码时需要确认 builtin、dotnet 或 module API 是否存在及其精确签名、参数、返回值和示例时使用;也用于按自然语言查找天软数据字典中的表、数据源与字段。"
|
||||
description: "当编写、修改或审查 TSL 代码,需要确认 API、class 成员或框架回调是否存在及其精确签名、参数、返回值和示例时使用;也用于按中文业务说法查找天软数据字典中的表、数据源与字段。触发说法包括「argmax 怎么用」「有没有去重函数」「TStringList 有哪些方法」「TSBackTesting 要重写什么」「销售现金比率在哪个表」。不用于 TSL 语法、控制流、语言对象模型,也不用于决定 TSL 如何运行。"
|
||||
---
|
||||
|
||||
# TSL API 参考
|
||||
|
||||
本 skill 仅用于 TSL API 事实:函数/模块名、精确签名、参数、返回值和示例。
|
||||
若需查询 TSL 语法、控制流、变量、对象模型或运行时语言规则,请改用
|
||||
`tsl-syntax-reference` skill。
|
||||
本 skill 只负责可检索的 TSL API、class/framework 契约和天软数据字典事实。
|
||||
语法、控制流、语言对象模型或运行方式问题不属于本 skill。
|
||||
|
||||
## 查询路由
|
||||
|
||||
先判断用户请求是普通 API、class,还是带生命周期的框架类工作流:
|
||||
先选择一条路由,并完整读取对应文件后再执行:
|
||||
|
||||
- 普通函数、单个成员或数据字典问题走现有 `lookup.py` / `dictionary_lookup.py`。
|
||||
- 查询或使用任意 class 时,先用 `class_lookup.py` 取回紧凑的类摘要和成员分类;
|
||||
需要某个成员正文时,再运行 `lookup.py --name` 精确查询。
|
||||
- `class_lookup.py` 自动覆盖 `function_index.tsv` 中的全部 class,不要求每个普通对象类
|
||||
都维护人工脚手架。
|
||||
- 用户要实现框架子类、重写回调或组合一组有生命周期的 class API 时,类级查询会叠加
|
||||
`framework_index.json` 中证据化的 Framework Profile,返回生命周期、必要回调、
|
||||
状态、执行入口和结果查询。
|
||||
- 不得因为查询目标是 class 就推断它是框架;普通对象、服务类和数据类只使用自动生成的
|
||||
类成员视图。只有 `kind=framework` 且具有证据化生命周期的类才加载人工 Profile。
|
||||
- 已知函数或成员名,或只知道 API 行为:读取
|
||||
[`workflows/api-lookup.md`](workflows/api-lookup.md)。
|
||||
- 查询 class 摘要、实现子类、重写回调或处理生命周期:读取
|
||||
[`workflows/class-and-framework.md`](workflows/class-and-framework.md)。
|
||||
- 按中文业务含义查表、数据源或字段:读取
|
||||
[`workflows/dictionary-lookup.md`](workflows/dictionary-lookup.md)。
|
||||
|
||||
列出和查询 class:
|
||||
具体参数、输出字段和退出码以所选脚本当前的 `--help` 为权威。
|
||||
将 `<this-skill-dir>` 替换为包含本 `SKILL.md` 的目录。
|
||||
|
||||
```bash
|
||||
python <this-skill-dir>/scripts/class_lookup.py --list
|
||||
python <this-skill-dir>/scripts/class_lookup.py --class TStringList
|
||||
python <this-skill-dir>/scripts/class_lookup.py --class TSBackTesting
|
||||
python <this-skill-dir>/scripts/class_lookup.py \
|
||||
--class TSBackTesting --config fGroupType=1 --format json
|
||||
```
|
||||
## 全局硬规则
|
||||
|
||||
同名 class 出现在多个 scope 或模块时,状态为 `ambiguous`;使用 `--scope`、`--module`
|
||||
或完全限定名消歧,不能选择第一项。
|
||||
- API 名称按索引原样查询和报告。TSL 名称大小写不敏感,但下划线有意义;不得删除
|
||||
下划线或改写成驼峰式。
|
||||
- 不得凭记忆或参照相似语言推断 API 名称、签名、参数、返回值或回调字段。
|
||||
- 同名结果不得默认取第一条;必须按 `scope`、`module`、owner 或完全限定名消歧。
|
||||
- `lookup.py` 的 rc=1 表示 skill 数据缺失、损坏或正文不可达,不表示 API 不存在。
|
||||
|
||||
普通 class 的 `profile_status=not_profiled` 不是错误,只表示它使用索引自动生成的成员
|
||||
视图。框架类的 Profile 只描述 API 协议,不直接生成 TSL 语法;将结果作为
|
||||
Framework Contract Packet 交给 `tsl-syntax-reference` 组合代码结构。
|
||||
## 🔴 CHECKPOINT · 🛑 STOP
|
||||
|
||||
`scaffold_status=resolved` 只表示类级生命周期和成员引用完整,不表示模式相关的回调
|
||||
记录结构已经确定。只要 `contract_status` 不是 `resolved`,就必须停止生成回调字段,
|
||||
按 `diagnostics` 补齐模式和返回 schema 证据。
|
||||
出现任一情况时,停止生成包含该 API 或回调字段的代码,并明确报告缺口:
|
||||
|
||||
维护 Profile 后运行引用校验:
|
||||
- `lookup.py --name` 无匹配;
|
||||
- `lookup.py --kw` 换用更准确的中文关键词后仍无候选;
|
||||
- 同名 API 或 class 无法按 `scope`、`module`、owner 或完全限定名消歧;
|
||||
- `class_lookup.py` 返回 `status: not_found`;
|
||||
- 请求依赖生命周期、配置或回调,但 `profile_status=not_profiled`;
|
||||
- Framework Profile 的 `contract_status` 不是 `resolved`。
|
||||
|
||||
```bash
|
||||
python <this-skill-dir>/scripts/class_lookup.py --check
|
||||
```
|
||||
rc=1 不进入上述“API 缺失”分支;应先修复或重新安装 skill 数据,再重新查询。
|
||||
|
||||
天软数据字典中的表、数据源和字段不是可调用 API,不进入 `function_index.tsv`
|
||||
函数问题使用 `lookup.py`,表或字段问题使用 `dictionary_lookup.py`
|
||||
## 输出约定
|
||||
|
||||
## 数据字典查询
|
||||
|
||||
用户按中文业务含义描述表或字段时,运行独立的字典查询脚本
|
||||
|
||||
```bash
|
||||
python <this-skill-dir>/scripts/dictionary_lookup.py --query "股票现金流指标销售现金比率"
|
||||
```
|
||||
|
||||
已知业务范围、表名、数据源名或字段名时使用过滤参数
|
||||
|
||||
```bash
|
||||
python <this-skill-dir>/scripts/dictionary_lookup.py --query "市值" --scope fund
|
||||
python <this-skill-dir>/scripts/dictionary_lookup.py --query "买一量" --table TradeTable
|
||||
python <this-skill-dir>/scripts/dictionary_lookup.py --query "9900700" --field 销售现金比率
|
||||
```
|
||||
|
||||
查询结果包含表或数据源名称、表 ID、字段名称、字段 ID、类型、单位、提取方式、
|
||||
访问代码、对应 API 和字典页面
|
||||
|
||||
- 标识符匹配不删除下划线
|
||||
- 同义词只从 `dictionary_lexicon.json` 中取受控映射
|
||||
- `status: ambiguous` 时必须向用户列出候选并要求补充表或数据源范围,不能默认选择第一项
|
||||
- `MarketTable` 和 `TradeTable` 是 TS-SQL 数据源,不是函数
|
||||
- `status: no_match` 表示字典无匹配,不能改述为 API 的 `not found`
|
||||
|
||||
不要凭记忆或参照相似语言推断 API 签名。TSL 名称大小写不敏感,但下划线是有意义的:
|
||||
查询或报告 API 时不要删除下划线,也不要把名称改写成驼峰式。
|
||||
|
||||
运行随附的查询脚本。将 `<this-skill-dir>` 替换为包含本 `SKILL.md` 的目录。
|
||||
|
||||
- 已知 API 名称:
|
||||
|
||||
```bash
|
||||
python <this-skill-dir>/scripts/lookup.py --name argmax
|
||||
```
|
||||
|
||||
- 已知 class/unit 成员的完全限定名称:
|
||||
|
||||
```bash
|
||||
python <this-skill-dir>/scripts/lookup.py --name DemoUnit.Document.Save
|
||||
```
|
||||
|
||||
- 未知名称,但知道其行为或中文关键词:
|
||||
|
||||
```bash
|
||||
python <this-skill-dir>/scripts/lookup.py --kw 最大值 位置
|
||||
```
|
||||
|
||||
- 只查询一个 scope:
|
||||
|
||||
```bash
|
||||
python <this-skill-dir>/scripts/lookup.py --scope builtin --name boolToStr
|
||||
python <this-skill-dir>/scripts/lookup.py --scope dotnet --kw argmax
|
||||
python <this-skill-dir>/scripts/lookup.py --scope module --name bondAccruedInterest
|
||||
```
|
||||
|
||||
- 关键词采用字面 AND 语义;增加关键词可缩小结果范围。常用同义词由 API 条目的
|
||||
Tags 提供,例如“位置/下标”“列表/数组”“打印/输出”。
|
||||
- 关键词会搜索 `name`、`signature`、`tags`、`summary`、`module`、`scope` 及
|
||||
`qualified_name`、`owner`、`kind`、`binding`、`visibility`,并按字段加权稳定排序;
|
||||
因此也可用 `--kw dotnet argmax` 或 `--kw protected method` 缩小范围。
|
||||
- 使用 `--limit N` 调整打印的候选行数量,N 必须 >= 1。
|
||||
|
||||
精确查询会打印完整的条目正文和来源标记。关键词查询打印候选行;
|
||||
选定候选项后,再用 `--name` 重新执行精确查询。
|
||||
|
||||
`--name` 对简单名称和完全限定名称做大小写不敏感的精确匹配。查询简单成员名会
|
||||
返回所有 owner 下的同名 API;查询 `Unit.Class.Member` 等完全限定名称可限定所属
|
||||
class/unit。重载共享 `qualified_name`,精确查询会返回该名称的全部重载。
|
||||
|
||||
混合页面中的精确查询仍按唯一 `page#anchor` 定位,只截取目标 H2 或成员子树。
|
||||
每个 H2 的声明类型独立,不使用全局页面类型。
|
||||
|
||||
关键词候选优先显示 `qualified_name`,并根据 kind/binding 显示声明术语:实例
|
||||
method 显示 `function`,类方法显示 `class function`,静态字段与常量分别显示
|
||||
`static field`、`static const`。
|
||||
|
||||
同名 API 存在于多个 scope 或模块时(如 `host`、`login`、`port`),`--name`
|
||||
会依次打印全部同名条目,每条带自己的来源标记。已知 scope 时先用 `--scope`
|
||||
过滤;否则按当前代码所处的 scope 和模块选择条目,不要默认取第一条。
|
||||
|
||||
查询脚本兼容旧 8 列和新 13 列 `function_index.tsv`。旧 8 列索引仍可查询原有
|
||||
function;完全限定名称、owner、kind、binding、visibility 依赖新索引中的扩展列。
|
||||
|
||||
## 退出码
|
||||
|
||||
| 退出码 | 含义 | 处理方式 |
|
||||
| ------ | -------------------------------------- | ------------------------------------------------ |
|
||||
| 0 | 查询完成,含"查无此 API"和关键词零命中 | 读取输出;无匹配时换中文关键词重试 |
|
||||
| 1 | 随附数据缺失或为空 | 重新安装本 skill,或用 `--tsv PATH` 指向可用索引 |
|
||||
| 2 | 参数不合法 | 按提示修正参数后重跑 |
|
||||
|
||||
rc=1 表示 skill 数据本身有问题,不表示该 API 不存在;此时不要据此断言 API 缺失。
|
||||
|
||||
## 常见错误
|
||||
|
||||
- 删掉下划线或改写成驼峰式后查询,导致误判 API 不存在。名称按原样查。
|
||||
- 把关键词查询的 summary 当作签名事实。summary 只用于挑候选,签名必须由
|
||||
`--name` 的条目正文提供。
|
||||
- 多条同名结果里随手取第一条,忽略 scope 与模块差异。
|
||||
- 查不到就凭记忆或相似语言推断签名。查不到时说明缺口并停止。
|
||||
报告 API 事实时必须包含:索引中的原样名称或 `qualified_name`、`scope`、`module`、
|
||||
由 `lookup.py --name` 正文取得的精确签名,以及对应的 `page#anchor` 来源。
|
||||
关键词候选的 summary 只能用于选候选,不能作为签名事实。
|
||||
|
||||
@@ -1,832 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build an independent searchable snapshot of the Tinysoft data dictionary."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
import unicodedata
|
||||
from dataclasses import dataclass, replace
|
||||
from pathlib import Path
|
||||
from typing import Iterable, Sequence
|
||||
|
||||
from bs4 import BeautifulSoup, NavigableString, Tag
|
||||
|
||||
|
||||
INDEX_COLUMNS = (
|
||||
"kind",
|
||||
"scope",
|
||||
"table_id",
|
||||
"table_name",
|
||||
"table_alias",
|
||||
"field_id",
|
||||
"field_name",
|
||||
"field_alias",
|
||||
"data_type",
|
||||
"unit",
|
||||
"description",
|
||||
"extract_method",
|
||||
"access_code",
|
||||
"api_name",
|
||||
"page",
|
||||
"tags",
|
||||
)
|
||||
|
||||
SECTION_LABELS = {
|
||||
"更新日志",
|
||||
"数据说明",
|
||||
"数据来源",
|
||||
"数据更新情况",
|
||||
"基本概况",
|
||||
"字段说明",
|
||||
"特别说明",
|
||||
"数据范例",
|
||||
"访问代码",
|
||||
"访问方法",
|
||||
"取数示例",
|
||||
"参考",
|
||||
}
|
||||
|
||||
SCOPE_NAMES = {
|
||||
"股票": "stock",
|
||||
"基金": "fund",
|
||||
"基金扩展": "fund",
|
||||
"债券": "bond",
|
||||
"期货": "futures",
|
||||
"期权": "options",
|
||||
"新三板": "neeq",
|
||||
"指数": "index",
|
||||
"回购": "repo",
|
||||
"现货": "spot",
|
||||
"宏观": "macro",
|
||||
"中证": "csindex",
|
||||
"天软": "tinysoft",
|
||||
"行情": "market",
|
||||
"证券": "security",
|
||||
}
|
||||
|
||||
FIELD_NAME_HEADERS = ("字段名", "函数名", "名称", "含义", "字段")
|
||||
UNIT_HEADERS = ("单位", "单位【注1】", "单位【注4】")
|
||||
DESCRIPTION_HEADERS = ("详细说明", "说明", "备注")
|
||||
INVALID_SLUG_CHARS_RE = re.compile(r"[\\/:*?\"<>|]+")
|
||||
SPACE_RE = re.compile(r"\s+")
|
||||
|
||||
|
||||
class DictionaryParseError(ValueError):
|
||||
"""A dictionary page advertises a schema that cannot be parsed."""
|
||||
|
||||
|
||||
class NotDictionaryPage(DictionaryParseError):
|
||||
"""The source page is outside the data-dictionary branch."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DictionaryField:
|
||||
field_id: str
|
||||
field_name: str
|
||||
data_type: str
|
||||
display_name: str
|
||||
unit: str
|
||||
description: str
|
||||
api_name: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DictionaryTable:
|
||||
name: str
|
||||
table_id: str
|
||||
extract_method: str
|
||||
access_code: str
|
||||
update_info: tuple[str, ...]
|
||||
aliases: tuple[str, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DictionaryExample:
|
||||
description: str
|
||||
code: str
|
||||
output: str | None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DictionaryPage:
|
||||
kind: str
|
||||
scope: str
|
||||
title: str
|
||||
source_path: tuple[str, ...]
|
||||
source_file: str
|
||||
table: DictionaryTable
|
||||
fields: tuple[DictionaryField, ...]
|
||||
notes: tuple[str, ...]
|
||||
examples: tuple[DictionaryExample, ...]
|
||||
output_name: str = ""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DictionaryDocument:
|
||||
source_file: str
|
||||
source_path: tuple[str, ...]
|
||||
records: tuple[DictionaryPage, ...]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BuildReport:
|
||||
scanned_page_count: int
|
||||
dictionary_page_count: int
|
||||
navigation_page_count: int
|
||||
page_count: int
|
||||
table_count: int
|
||||
source_count: int
|
||||
field_count: int
|
||||
ignored_pages: tuple[str, ...]
|
||||
malformed_dictionary_pages: tuple[str, ...]
|
||||
|
||||
|
||||
def _clean_text(value: str) -> str:
|
||||
value = value.replace("\xa0", " ")
|
||||
return SPACE_RE.sub(" ", value).strip()
|
||||
|
||||
|
||||
def _cell_text(cell: Tag) -> str:
|
||||
return _clean_text(cell.get_text(" ", strip=True))
|
||||
|
||||
|
||||
def _table_rows(table: Tag) -> tuple[list[str], list[list[str]]]:
|
||||
rows = []
|
||||
for row in table.find_all("tr"):
|
||||
cells = [_cell_text(cell) for cell in row.find_all(["th", "td"], recursive=False)]
|
||||
if cells:
|
||||
rows.append(cells)
|
||||
if not rows:
|
||||
return [], []
|
||||
return rows[0], rows[1:]
|
||||
|
||||
|
||||
def _page_title(content: Tag) -> str:
|
||||
heading = content.find(["h1", "h2", "h3"])
|
||||
if heading is None:
|
||||
return ""
|
||||
return _clean_text(heading.get_text(" ", strip=True).replace("复制链接", ""))
|
||||
|
||||
|
||||
def _breadcrumb(raw: str) -> tuple[str, ...]:
|
||||
prefix = raw.split('<div id="help_content">', 1)[0]
|
||||
soup = BeautifulSoup(prefix, "html.parser")
|
||||
labels = [_clean_text(link.get_text(" ", strip=True)) for link in soup.find_all("a")]
|
||||
starts = [index for index, label in enumerate(labels) if label == "知识库"]
|
||||
if starts:
|
||||
labels = labels[starts[-1] :]
|
||||
return tuple(label for label in labels if label)
|
||||
|
||||
|
||||
def _is_dictionary_path(source_path: Sequence[str], title: str) -> bool:
|
||||
return "天软数据字典" in source_path or title == "天软数据字典"
|
||||
|
||||
|
||||
def _scope(source_path: Sequence[str]) -> str:
|
||||
if "天软数据字典" not in source_path:
|
||||
return ""
|
||||
start = source_path.index("天软数据字典") + 1
|
||||
for label in source_path[start:]:
|
||||
if label in SCOPE_NAMES:
|
||||
return SCOPE_NAMES[label]
|
||||
return ""
|
||||
|
||||
|
||||
def _domain_path(source_path: Sequence[str]) -> tuple[str, ...]:
|
||||
if "天软数据字典" not in source_path:
|
||||
return ()
|
||||
start = source_path.index("天软数据字典") + 1
|
||||
return tuple(source_path[start:])
|
||||
|
||||
|
||||
def _section_marker(content: Tag, label: str) -> Tag | None:
|
||||
for tag in content.find_all(["span", "strong"]):
|
||||
if _clean_text(tag.get_text(" ", strip=True)).rstrip("::") == label:
|
||||
return tag
|
||||
return None
|
||||
|
||||
|
||||
def _is_section_marker(node: Tag) -> bool:
|
||||
if node.name not in {"span", "strong"}:
|
||||
return False
|
||||
return _clean_text(node.get_text(" ", strip=True)).rstrip("::") in SECTION_LABELS
|
||||
|
||||
|
||||
def _inside(node: NavigableString, tag_name: str, class_name: str | None = None) -> bool:
|
||||
parent = node.parent
|
||||
while isinstance(parent, Tag):
|
||||
if parent.name == tag_name and (class_name is None or class_name in parent.get("class", [])):
|
||||
return True
|
||||
parent = parent.parent
|
||||
return False
|
||||
|
||||
|
||||
def _section_lines(content: Tag, label: str) -> tuple[str, ...]:
|
||||
marker = _section_marker(content, label)
|
||||
if marker is None:
|
||||
return ()
|
||||
lines = []
|
||||
for node in marker.next_elements:
|
||||
if isinstance(node, Tag) and node is not marker and _is_section_marker(node):
|
||||
break
|
||||
if not isinstance(node, NavigableString) or node.parent is marker:
|
||||
continue
|
||||
if _inside(node, "table") or _inside(node, "div", "text-container"):
|
||||
continue
|
||||
text = _clean_text(str(node))
|
||||
if text and (not lines or lines[-1] != text):
|
||||
lines.append(text)
|
||||
return tuple(lines)
|
||||
|
||||
|
||||
def _nearest_section(table: Tag) -> str:
|
||||
for tag in table.find_all_previous(["span", "strong"]):
|
||||
label = _clean_text(tag.get_text(" ", strip=True)).rstrip("::")
|
||||
if label in SECTION_LABELS:
|
||||
return label
|
||||
return ""
|
||||
|
||||
|
||||
def _find_basic_table(content: Tag) -> Tag | None:
|
||||
for table in content.find_all("table"):
|
||||
header, _ = _table_rows(table)
|
||||
if "表ID" in header and "表名" in header and "提取方式" in header:
|
||||
return table
|
||||
return None
|
||||
|
||||
|
||||
def _field_table_header(header: Sequence[str]) -> bool:
|
||||
return "类型" in header and any(name in header for name in FIELD_NAME_HEADERS)
|
||||
|
||||
|
||||
def _find_field_tables(content: Tag) -> tuple[Tag, ...]:
|
||||
found = []
|
||||
for table in content.find_all("table"):
|
||||
header, _ = _table_rows(table)
|
||||
if _field_table_header(header) and _nearest_section(table) == "字段说明":
|
||||
found.append(table)
|
||||
return tuple(found)
|
||||
|
||||
|
||||
def _column_index(header: Sequence[str], names: Sequence[str]) -> int | None:
|
||||
for name in names:
|
||||
if name in header:
|
||||
return header.index(name)
|
||||
return None
|
||||
|
||||
|
||||
def _value(row: Sequence[str], index: int | None) -> str:
|
||||
if index is None or index >= len(row):
|
||||
return ""
|
||||
return row[index]
|
||||
|
||||
|
||||
def _parse_fields(table: Tag) -> tuple[DictionaryField, ...]:
|
||||
header, rows = _table_rows(table)
|
||||
id_index = _column_index(header, ("ID",))
|
||||
identifier_index = _column_index(header, ("字段名", "函数名", "字段"))
|
||||
display_index = _column_index(header, ("中文名", "名称", "含义"))
|
||||
type_index = _column_index(header, ("类型",))
|
||||
unit_index = _column_index(header, UNIT_HEADERS)
|
||||
description_indices = [header.index(name) for name in DESCRIPTION_HEADERS if name in header]
|
||||
api_index = _column_index(header, ("对应函数名称",))
|
||||
fields = []
|
||||
for row in rows:
|
||||
field_id = _value(row, id_index)
|
||||
identifier = _value(row, identifier_index)
|
||||
display_name = _value(row, display_index)
|
||||
api_name = _value(row, api_index)
|
||||
if "函数名" in header and identifier:
|
||||
api_name = api_name or identifier
|
||||
field_name = identifier or display_name or field_id
|
||||
display_name = display_name or identifier or field_id
|
||||
descriptions = []
|
||||
for index in description_indices:
|
||||
value = _value(row, index)
|
||||
if value and value not in descriptions:
|
||||
descriptions.append(value)
|
||||
if not field_name:
|
||||
continue
|
||||
fields.append(
|
||||
DictionaryField(
|
||||
field_id=field_id,
|
||||
field_name=field_name,
|
||||
data_type=_value(row, type_index),
|
||||
display_name=display_name,
|
||||
unit=_value(row, unit_index),
|
||||
description=" ".join(descriptions),
|
||||
api_name=api_name,
|
||||
)
|
||||
)
|
||||
return tuple(fields)
|
||||
|
||||
|
||||
def _extract_examples(content: Tag) -> tuple[DictionaryExample, ...]:
|
||||
marker = _section_marker(content, "取数示例")
|
||||
if marker is None:
|
||||
return ()
|
||||
examples = []
|
||||
for container in marker.find_all_next("div", class_="text-container"):
|
||||
previous = container.find_previous(["span", "strong"])
|
||||
if previous is not marker and _is_section_marker(previous):
|
||||
break
|
||||
raw_lines = [_clean_text(line) for line in container.get_text("\n").splitlines()]
|
||||
chunks: list[list[str]] = []
|
||||
current: list[str] = []
|
||||
for line in (line for line in raw_lines if line):
|
||||
empty_output = re.fullmatch(r"//\s*(?:返回|输出)\s*[::]?", line, re.IGNORECASE)
|
||||
if empty_output:
|
||||
if current:
|
||||
chunks.append(current)
|
||||
current = []
|
||||
continue
|
||||
output_match = re.match(r"//\s*(?:返回|输出)\s*[::]\s*(.+)", line, re.IGNORECASE)
|
||||
if not output_match and line.startswith("//") and current and any(
|
||||
re.match(r"(?i)^\s*return\b", existing) for existing in current
|
||||
):
|
||||
chunks.append(current)
|
||||
current = []
|
||||
if output_match:
|
||||
line = f"// 输出:{output_match.group(1).strip()}"
|
||||
elif line.startswith("//"):
|
||||
line = f"// {line[2:].strip()}"
|
||||
current.append(line)
|
||||
if current:
|
||||
chunks.append(current)
|
||||
for lines in chunks:
|
||||
description = lines[0][2:].strip() if lines[0].startswith("//") else ""
|
||||
output = None
|
||||
for line in lines:
|
||||
match = re.match(r"//\s*输出:(.+)", line)
|
||||
if match:
|
||||
output = match.group(1).strip()
|
||||
examples.append(DictionaryExample(description, "\n".join(lines), output))
|
||||
return tuple(examples)
|
||||
|
||||
|
||||
def _basic_metadata(table: Tag) -> tuple[str, str, str]:
|
||||
header, rows = _table_rows(table)
|
||||
if len(rows) != 1:
|
||||
raise DictionaryParseError("basic table must contain exactly one data row")
|
||||
row = rows[0]
|
||||
return (
|
||||
_value(row, header.index("表ID")),
|
||||
_value(row, header.index("表名")),
|
||||
_value(row, header.index("提取方式")),
|
||||
)
|
||||
|
||||
|
||||
def _resolve_extract_method(content: Tag, extract_method: str) -> str:
|
||||
if not re.fullmatch(r"【注\d+】", extract_method):
|
||||
return extract_method
|
||||
page_text = _clean_text(content.get_text(" ", strip=True))
|
||||
match = re.search(r"(?:取数接口|接口)\s*([A-Za-z][A-Za-z0-9_]*)", page_text)
|
||||
return match.group(1) if match else extract_method
|
||||
|
||||
|
||||
def _normal_aliases(table_name: str, title: str) -> tuple[str, ...]:
|
||||
candidates = [title, table_name.replace(".", ""), table_name]
|
||||
if "." in table_name:
|
||||
candidates.append(table_name.rsplit(".", 1)[-1])
|
||||
return tuple(dict.fromkeys(value for value in candidates if value and value != table_name))
|
||||
|
||||
|
||||
def _field_available(field: DictionaryField, source_name: str) -> bool:
|
||||
description = unicodedata.normalize("NFKC", field.description).casefold()
|
||||
source = source_name.casefold()
|
||||
pattern = rf"{re.escape(source)}\s*[::]\s*(?:没有|无)(?:该)?字段"
|
||||
return re.search(pattern, description) is None
|
||||
|
||||
|
||||
def _split_market_sources(page: DictionaryPage) -> tuple[DictionaryPage, ...]:
|
||||
shared = page.table
|
||||
trade_table = replace(
|
||||
shared,
|
||||
name="TradeTable",
|
||||
extract_method="TradeTable",
|
||||
aliases=("交易明细", "交易明细表", "tradetable"),
|
||||
)
|
||||
market_table = replace(
|
||||
shared,
|
||||
name="MarketTable",
|
||||
extract_method="MarketTable",
|
||||
aliases=("分时", "分时表", "markettable"),
|
||||
)
|
||||
trade_examples = tuple(example for example in page.examples if "tradetable" in example.code.casefold())
|
||||
market_examples = tuple(example for example in page.examples if "markettable" in example.code.casefold())
|
||||
return (
|
||||
replace(
|
||||
page,
|
||||
kind="source",
|
||||
title="交易明细",
|
||||
table=trade_table,
|
||||
fields=tuple(field for field in page.fields if _field_available(field, "tradetable")),
|
||||
examples=trade_examples,
|
||||
),
|
||||
replace(
|
||||
page,
|
||||
kind="source",
|
||||
title="分时",
|
||||
table=market_table,
|
||||
fields=tuple(field for field in page.fields if _field_available(field, "markettable")),
|
||||
examples=market_examples,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _special_field_records(
|
||||
content: Tag,
|
||||
title: str,
|
||||
source_path: tuple[str, ...],
|
||||
source_file: str,
|
||||
fields_by_table: Sequence[tuple[Tag, tuple[DictionaryField, ...]]],
|
||||
) -> tuple[DictionaryPage, ...]:
|
||||
access_lines = _section_lines(content, "访问方法")
|
||||
methods = []
|
||||
for line in access_lines:
|
||||
if ":" in line or ":" in line:
|
||||
_, method = re.split(r"[::]", line, maxsplit=1)
|
||||
methods.append(_clean_text(method))
|
||||
all_examples = _extract_examples(content)
|
||||
records = []
|
||||
for index, (table, fields) in enumerate(fields_by_table):
|
||||
label = ""
|
||||
for sibling in table.previous_siblings:
|
||||
if isinstance(sibling, NavigableString):
|
||||
label = _clean_text(str(sibling)).rstrip("::")
|
||||
if label:
|
||||
break
|
||||
record_title = f"{title}.{label}" if label else title
|
||||
extract_method = methods[index] if index < len(methods) else ""
|
||||
matching_examples = tuple(
|
||||
example
|
||||
for example in all_examples
|
||||
if extract_method and extract_method.casefold() in example.code.casefold()
|
||||
)
|
||||
records.append(
|
||||
DictionaryPage(
|
||||
kind="source",
|
||||
scope=_scope(source_path),
|
||||
title=record_title,
|
||||
source_path=_domain_path(source_path),
|
||||
source_file=source_file,
|
||||
table=DictionaryTable(
|
||||
name=record_title,
|
||||
table_id="",
|
||||
extract_method=extract_method,
|
||||
access_code=";".join(access_lines),
|
||||
update_info=_section_lines(content, "数据更新情况"),
|
||||
aliases=tuple(filter(None, (title, label))),
|
||||
),
|
||||
fields=fields,
|
||||
notes=_section_lines(content, "数据说明"),
|
||||
examples=matching_examples or (all_examples if len(fields_by_table) == 1 else ()),
|
||||
)
|
||||
)
|
||||
return tuple(records)
|
||||
|
||||
|
||||
def parse_dictionary_document(path: Path) -> DictionaryDocument:
|
||||
raw = path.read_text(encoding="utf-8", errors="replace")
|
||||
soup = BeautifulSoup(raw, "html.parser")
|
||||
content = soup.select_one("#help_content")
|
||||
if content is None:
|
||||
raise DictionaryParseError("missing #help_content")
|
||||
title = _page_title(content)
|
||||
source_path = _breadcrumb(raw)
|
||||
if not _is_dictionary_path(source_path, title):
|
||||
raise NotDictionaryPage(path.name)
|
||||
|
||||
basic_table = _find_basic_table(content)
|
||||
field_tables = _find_field_tables(content)
|
||||
if basic_table is None and not field_tables:
|
||||
return DictionaryDocument(path.name, source_path, ())
|
||||
if not field_tables:
|
||||
raise DictionaryParseError("dictionary record has no recognized field table")
|
||||
|
||||
parsed_tables = tuple((table, _parse_fields(table)) for table in field_tables)
|
||||
if any(not fields for _, fields in parsed_tables):
|
||||
raise DictionaryParseError("field table contains no data rows")
|
||||
if basic_table is None:
|
||||
records = _special_field_records(content, title, source_path, path.name, parsed_tables)
|
||||
return DictionaryDocument(path.name, source_path, records)
|
||||
|
||||
table_id, table_name, extract_method = _basic_metadata(basic_table)
|
||||
extract_method = _resolve_extract_method(content, extract_method)
|
||||
if not table_name:
|
||||
raise DictionaryParseError("basic table has no table name")
|
||||
fields = tuple(field for _, group in parsed_tables for field in group)
|
||||
page = DictionaryPage(
|
||||
kind="table",
|
||||
scope=_scope(source_path),
|
||||
title=title,
|
||||
source_path=_domain_path(source_path),
|
||||
source_file=path.name,
|
||||
table=DictionaryTable(
|
||||
name=table_name,
|
||||
table_id=table_id,
|
||||
extract_method=extract_method,
|
||||
access_code=";".join(_section_lines(content, "访问代码")),
|
||||
update_info=_section_lines(content, "数据更新情况"),
|
||||
aliases=_normal_aliases(table_name, title),
|
||||
),
|
||||
fields=fields,
|
||||
notes=_section_lines(content, "数据说明"),
|
||||
examples=_extract_examples(content),
|
||||
)
|
||||
method_key = re.sub(r"[^a-z]", "", extract_method.casefold())
|
||||
if "markettable" in method_key and "tradetable" in method_key:
|
||||
records = _split_market_sources(page)
|
||||
else:
|
||||
records = (page,)
|
||||
return DictionaryDocument(path.name, source_path, records)
|
||||
|
||||
|
||||
def _slug_part(value: str) -> str:
|
||||
value = unicodedata.normalize("NFKC", value).strip().casefold()
|
||||
value = INVALID_SLUG_CHARS_RE.sub("_", value)
|
||||
value = re.sub(r"[.。·,,、]+", "_", value)
|
||||
value = SPACE_RE.sub("_", value)
|
||||
value = re.sub(r"_+", "_", value).strip("_")
|
||||
return value
|
||||
|
||||
|
||||
def dictionary_slug(page: DictionaryPage) -> str:
|
||||
if page.output_name:
|
||||
return page.output_name
|
||||
parts = [*page.source_path, page.title]
|
||||
if page.kind == "source" and page.table.name.casefold() not in page.title.casefold():
|
||||
parts.append(page.table.name)
|
||||
compact = []
|
||||
for part in parts:
|
||||
slug = _slug_part(part)
|
||||
if slug and (not compact or compact[-1] != slug):
|
||||
compact.append(slug)
|
||||
if not compact:
|
||||
raise DictionaryParseError(f"cannot create slug for {page.source_file}")
|
||||
return "__".join(compact) + ".md"
|
||||
|
||||
|
||||
def _resolve_slug_collisions(pages: Sequence[DictionaryPage]) -> tuple[DictionaryPage, ...]:
|
||||
grouped: dict[str, list[DictionaryPage]] = {}
|
||||
for page in pages:
|
||||
grouped.setdefault(dictionary_slug(page), []).append(page)
|
||||
resolved = []
|
||||
used = set()
|
||||
for page in pages:
|
||||
base = dictionary_slug(page)
|
||||
group = grouped[base]
|
||||
if len(group) == 1:
|
||||
output_name = base
|
||||
else:
|
||||
discriminator = _slug_part(page.table.table_id or page.table.extract_method)
|
||||
if not discriminator or "注" in discriminator:
|
||||
sources = ", ".join(item.source_file for item in group)
|
||||
raise DictionaryParseError(f"slug collision needs a semantic discriminator: {base}: {sources}")
|
||||
output_name = f"{Path(base).stem}__{discriminator}.md"
|
||||
if output_name in used:
|
||||
sources = ", ".join(item.source_file for item in group)
|
||||
raise DictionaryParseError(f"slug collision: {output_name}: {sources}")
|
||||
used.add(output_name)
|
||||
resolved.append(replace(page, output_name=output_name))
|
||||
return tuple(resolved)
|
||||
|
||||
|
||||
def _markdown_cell(value: str) -> str:
|
||||
return value.replace("|", "\\|").replace("\n", "<br>")
|
||||
|
||||
|
||||
def render_dictionary_page(page: DictionaryPage) -> str:
|
||||
path = " / ".join((*page.source_path, page.title))
|
||||
lines = [f"# 天软数据字典 / {path}", "", f"类型:{page.kind}", "", "## 表信息", ""]
|
||||
name_label = "数据源名称" if page.kind == "source" else "表名"
|
||||
metadata = [(name_label, page.table.name)]
|
||||
if page.table.table_id:
|
||||
metadata.append(("表 ID", page.table.table_id))
|
||||
if page.table.extract_method:
|
||||
metadata.append(("提取方式", page.table.extract_method))
|
||||
if page.table.access_code:
|
||||
metadata.append(("访问代码", page.table.access_code))
|
||||
if page.scope:
|
||||
metadata.append(("范围", page.scope))
|
||||
lines.extend(["| 项目 | 内容 |", "| --- | --- |"])
|
||||
lines.extend(f"| {label} | {_markdown_cell(value)} |" for label, value in metadata)
|
||||
lines.extend(["", "## 字段", "", "| 字段 ID | 字段名 | 类型 | 中文名 | 单位 | 说明 | 对应 API |", "| --- | --- | --- | --- | --- | --- | --- |"])
|
||||
for field in page.fields:
|
||||
values = (
|
||||
field.field_id,
|
||||
field.field_name,
|
||||
field.data_type,
|
||||
field.display_name,
|
||||
field.unit,
|
||||
field.description,
|
||||
field.api_name,
|
||||
)
|
||||
lines.append("| " + " | ".join(_markdown_cell(value) for value in values) + " |")
|
||||
if page.notes or page.table.update_info:
|
||||
lines.extend(["", "## 数据说明", ""])
|
||||
for note in (*page.notes, *page.table.update_info):
|
||||
lines.append(f"- {note}")
|
||||
if page.examples:
|
||||
lines.extend(["", "## 取数示例", ""])
|
||||
for index, example in enumerate(page.examples, start=1):
|
||||
if example.description:
|
||||
lines.extend([f"范例{index:02d}:{example.description}", ""])
|
||||
lines.extend(["```tsl", example.code, "```", ""])
|
||||
while lines and not lines[-1]:
|
||||
lines.pop()
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def _page_reference(page: DictionaryPage) -> str:
|
||||
return f"references/data_dictionary/{dictionary_slug(page)}"
|
||||
|
||||
|
||||
def _field_alias(field: DictionaryField) -> str:
|
||||
values = []
|
||||
for value in (field.display_name, field.field_name):
|
||||
if value and value not in values:
|
||||
values.append(value)
|
||||
return "|".join(values)
|
||||
|
||||
|
||||
def build_index(pages: Iterable[DictionaryPage]) -> list[dict[str, str]]:
|
||||
rows = []
|
||||
keys = set()
|
||||
for page in sorted(pages, key=lambda item: (dictionary_slug(item), item.table.name.casefold())):
|
||||
common = {
|
||||
"scope": page.scope,
|
||||
"table_id": page.table.table_id,
|
||||
"table_name": page.table.name,
|
||||
"table_alias": "|".join(page.table.aliases),
|
||||
"extract_method": page.table.extract_method,
|
||||
"access_code": page.table.access_code,
|
||||
"page": _page_reference(page),
|
||||
"tags": "|".join((*page.source_path, page.title)),
|
||||
}
|
||||
entity = {
|
||||
**{column: "" for column in INDEX_COLUMNS},
|
||||
**common,
|
||||
"kind": page.kind,
|
||||
"description": " ".join(page.notes),
|
||||
}
|
||||
rows.append(entity)
|
||||
for field in page.fields:
|
||||
row = {
|
||||
**{column: "" for column in INDEX_COLUMNS},
|
||||
**common,
|
||||
"kind": "field",
|
||||
"field_id": field.field_id,
|
||||
"field_name": field.field_name,
|
||||
"field_alias": _field_alias(field),
|
||||
"data_type": field.data_type,
|
||||
"unit": field.unit,
|
||||
"description": field.description,
|
||||
"api_name": field.api_name,
|
||||
}
|
||||
key = (
|
||||
row["kind"],
|
||||
row["table_id"],
|
||||
row["table_name"].casefold(),
|
||||
row["field_id"],
|
||||
row["field_name"].casefold(),
|
||||
row["field_alias"].casefold(),
|
||||
row["page"],
|
||||
)
|
||||
if key in keys:
|
||||
raise DictionaryParseError(f"duplicate dictionary index key: {key}")
|
||||
keys.add(key)
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
|
||||
def _atomic_write_text(path: Path, content: str) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
encoded = content.encode("utf-8")
|
||||
descriptor, temporary_name = tempfile.mkstemp(
|
||||
dir=path.parent, prefix=f".{path.name}.", suffix=".tmp"
|
||||
)
|
||||
temporary = Path(temporary_name)
|
||||
try:
|
||||
with os.fdopen(descriptor, "wb") as handle:
|
||||
handle.write(encoded)
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
os.replace(temporary, path)
|
||||
if path.read_bytes() != encoded:
|
||||
raise OSError(f"generated file verification failed: {path}")
|
||||
finally:
|
||||
temporary.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def write_index(rows: Iterable[dict[str, str]], path: Path) -> None:
|
||||
handle = io.StringIO(newline="")
|
||||
writer = csv.DictWriter(handle, fieldnames=INDEX_COLUMNS, delimiter="\t", lineterminator="\n")
|
||||
writer.writeheader()
|
||||
for row in rows:
|
||||
writer.writerow({column: row.get(column, "") for column in INDEX_COLUMNS})
|
||||
_atomic_write_text(path, handle.getvalue())
|
||||
|
||||
|
||||
def validate_lexicon(lexicon: object) -> dict[str, list[str]]:
|
||||
if not isinstance(lexicon, dict):
|
||||
raise ValueError("dictionary lexicon must be a JSON object")
|
||||
validated = {}
|
||||
for canonical, aliases in lexicon.items():
|
||||
if not isinstance(canonical, str) or not canonical.strip():
|
||||
raise ValueError("dictionary lexicon contains an empty canonical term")
|
||||
if not isinstance(aliases, list) or not all(isinstance(item, str) and item.strip() for item in aliases):
|
||||
raise ValueError(f"aliases for {canonical!r} must be non-empty strings")
|
||||
validated[canonical] = list(dict.fromkeys(aliases))
|
||||
return validated
|
||||
|
||||
|
||||
def write_lexicon(lexicon: object, path: Path) -> None:
|
||||
validated = validate_lexicon(lexicon)
|
||||
_atomic_write_text(path, json.dumps(validated, ensure_ascii=False, indent=2) + "\n")
|
||||
|
||||
|
||||
def load_lexicon(path: Path) -> dict[str, list[str]]:
|
||||
return validate_lexicon(json.loads(path.read_text(encoding="utf-8")))
|
||||
|
||||
|
||||
def build_snapshot(source_root: Path, output_root: Path, index_path: Path) -> BuildReport:
|
||||
records = []
|
||||
ignored = []
|
||||
malformed = []
|
||||
dictionary_count = 0
|
||||
navigation_count = 0
|
||||
source_paths = sorted(source_root.glob("*.html"), key=lambda path: int(path.stem) if path.stem.isdigit() else path.name)
|
||||
for source_path in source_paths:
|
||||
try:
|
||||
document = parse_dictionary_document(source_path)
|
||||
except NotDictionaryPage:
|
||||
ignored.append(source_path.name)
|
||||
continue
|
||||
except DictionaryParseError as error:
|
||||
dictionary_count += 1
|
||||
malformed.append(f"{source_path.name}: {error}")
|
||||
continue
|
||||
dictionary_count += 1
|
||||
if not document.records:
|
||||
navigation_count += 1
|
||||
continue
|
||||
records.extend(document.records)
|
||||
|
||||
records = list(_resolve_slug_collisions(records))
|
||||
slugs = {dictionary_slug(page): page.source_file for page in records}
|
||||
|
||||
output_root.mkdir(parents=True, exist_ok=True)
|
||||
expected = set(slugs)
|
||||
for stale in output_root.glob("*.md"):
|
||||
if stale.name not in expected:
|
||||
stale.unlink()
|
||||
for page in records:
|
||||
_atomic_write_text(output_root / dictionary_slug(page), render_dictionary_page(page))
|
||||
rows = build_index(records)
|
||||
write_index(rows, index_path)
|
||||
return BuildReport(
|
||||
scanned_page_count=len(source_paths),
|
||||
dictionary_page_count=dictionary_count,
|
||||
navigation_page_count=navigation_count,
|
||||
page_count=len(records),
|
||||
table_count=sum(page.kind == "table" for page in records),
|
||||
source_count=sum(page.kind == "source" for page in records),
|
||||
field_count=sum(len(page.fields) for page in records),
|
||||
ignored_pages=tuple(ignored),
|
||||
malformed_dictionary_pages=tuple(malformed),
|
||||
)
|
||||
|
||||
|
||||
def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
|
||||
parser.add_argument("--source-root", required=True, type=Path)
|
||||
parser.add_argument("--output-root", required=True, type=Path)
|
||||
parser.add_argument("--index", required=True, type=Path)
|
||||
parser.add_argument("--lexicon", type=Path)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
args = parse_args(argv)
|
||||
if args.lexicon is not None:
|
||||
load_lexicon(args.lexicon)
|
||||
report = build_snapshot(args.source_root, args.output_root, args.index)
|
||||
print(
|
||||
f"dictionary_pages={report.dictionary_page_count} generated_pages={report.page_count} "
|
||||
f"tables={report.table_count} sources={report.source_count} fields={report.field_count} "
|
||||
f"navigation={report.navigation_page_count} malformed={len(report.malformed_dictionary_pages)}"
|
||||
)
|
||||
for item in report.malformed_dictionary_pages:
|
||||
print(f"MALFORMED\t{item}")
|
||||
return 1 if report.malformed_dictionary_pages else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -16,6 +16,43 @@ from framework_lookup import (
|
||||
from lookup import DEFAULT_TSV, codegen_root_for_tsv, load_rows, normalize
|
||||
|
||||
|
||||
HELP_EPILOG = """\
|
||||
查询顺序:
|
||||
1. 先按 class 取类摘要或 Framework Profile
|
||||
class_lookup.py --class TStringList
|
||||
class_lookup.py --class TSBackTesting --format json
|
||||
2. 需要成员正文时,再按返回的 qualified_name 调用 lookup.py --name
|
||||
|
||||
输出状态:
|
||||
status resolved、not_found 或 ambiguous
|
||||
profile_status resolved 表示有 Framework Profile;not_profiled 表示普通 class
|
||||
scaffold_status 生命周期和成员引用是否完整,仅在有 Profile 时输出
|
||||
contract_status 回调字段契约是否完整;非 resolved 时不得生成回调字段
|
||||
diagnostics 消歧、配置或契约缺口及下一步动作
|
||||
|
||||
--check 校验全部 Framework Profile 对 class、成员正文和证据的引用。
|
||||
使用自定义 --tsv 时默认不加载 Framework Profile;普通 class 仍可正常查询。若该索引
|
||||
另有 curated Profile,必须同时显式传入 --profiles。
|
||||
|
||||
退出码:
|
||||
0 查询或校验完成;not_found、ambiguous 通过输出状态表达
|
||||
1 function/framework 索引缺失、格式错误、为空或引用校验失败
|
||||
2 参数不合法
|
||||
"""
|
||||
|
||||
|
||||
def empty_profile_index():
|
||||
return {"schema_version": 1, "frameworks": []}
|
||||
|
||||
|
||||
def profile_path_for(args):
|
||||
if args.profiles:
|
||||
return Path(args.profiles)
|
||||
if args.tsv:
|
||||
return None
|
||||
return DEFAULT_INDEX
|
||||
|
||||
|
||||
def class_rows(rows):
|
||||
return [row for row in rows if row.get("kind") == "class"]
|
||||
|
||||
@@ -232,24 +269,32 @@ def main(argv=None):
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Query indexed TSL classes and curated framework profiles.",
|
||||
description="查询 TSL class 摘要及其证据化的 Framework Profile。",
|
||||
epilog=HELP_EPILOG,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
allow_abbrev=False,
|
||||
)
|
||||
action = parser.add_mutually_exclusive_group(required=True)
|
||||
action.add_argument("--class", dest="class_name", help="exact class name")
|
||||
action.add_argument("--list", action="store_true", help="list indexed classes")
|
||||
action.add_argument("--check", action="store_true", help="validate framework profiles")
|
||||
parser.add_argument("--scope", help="filter class scope")
|
||||
parser.add_argument("--module", help="filter class module")
|
||||
action.add_argument("--class", dest="class_name", help="精确 class 名或完全限定名")
|
||||
action.add_argument("--list", action="store_true", help="列出索引中的 class")
|
||||
action.add_argument("--check", action="store_true", help="校验全部 Framework Profile 引用")
|
||||
parser.add_argument("--scope", help="按 class scope 过滤")
|
||||
parser.add_argument("--module", help="按 class module 过滤")
|
||||
parser.add_argument(
|
||||
"--config", action="append", default=[], metavar="KEY=VALUE", help="raw config"
|
||||
"--config",
|
||||
action="append",
|
||||
default=[],
|
||||
metavar="KEY=VALUE",
|
||||
help="传入原始框架配置,可重复",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--include-members", action="store_true", help="include the compact member list"
|
||||
"--include-members", action="store_true", help="在类摘要中包含紧凑成员清单"
|
||||
)
|
||||
parser.add_argument("--format", choices=("text", "json"), default="text")
|
||||
parser.add_argument("--profiles", metavar="PATH", help="framework profile index")
|
||||
parser.add_argument("--tsv", metavar="PATH", help="function index")
|
||||
parser.add_argument(
|
||||
"--format", choices=("text", "json"), default="text", help="输出格式"
|
||||
)
|
||||
parser.add_argument("--profiles", metavar="PATH", help="Framework Profile 索引路径")
|
||||
parser.add_argument("--tsv", metavar="PATH", help="function_index.tsv 路径")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if args.config and not args.class_name:
|
||||
@@ -257,10 +302,12 @@ def main(argv=None):
|
||||
if args.include_members and not args.class_name:
|
||||
parser.error("--include-members requires --class")
|
||||
|
||||
profile_path = Path(args.profiles) if args.profiles else DEFAULT_INDEX
|
||||
tsv_path = Path(args.tsv) if args.tsv else DEFAULT_TSV
|
||||
profile_path = profile_path_for(args)
|
||||
try:
|
||||
profiles = load_index(profile_path)
|
||||
profiles = (
|
||||
load_index(profile_path) if profile_path else empty_profile_index()
|
||||
)
|
||||
rows = load_rows(tsv_path)
|
||||
except (OSError, UnicodeError, ValueError) as error:
|
||||
print(f"ERROR: {error}", file=sys.stderr)
|
||||
|
||||
@@ -35,6 +35,24 @@ REQUIRED_COLUMNS = (
|
||||
"tags",
|
||||
)
|
||||
|
||||
HELP_EPILOG = """\
|
||||
按中文业务含义查询表、数据源或字段:
|
||||
dictionary_lookup.py --query "股票现金流指标销售现金比率"
|
||||
dictionary_lookup.py --query "市值" --scope fund
|
||||
dictionary_lookup.py --query "买一量" --table TradeTable
|
||||
dictionary_lookup.py --query "9900700" --field 销售现金比率
|
||||
|
||||
输出第一行是 in-band 状态:
|
||||
status: ok 最高分候选唯一
|
||||
status: ambiguous 多个同分候选;必须补充 scope、table 或 field 范围
|
||||
status: no_match 数据字典无匹配,不等于 API not found
|
||||
|
||||
退出码:
|
||||
0 查询完成;ok、ambiguous、no_match 均由 status 表达
|
||||
1 dictionary_index.tsv 或词表缺失、损坏、格式错误
|
||||
2 参数不合法
|
||||
"""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class QueryItem:
|
||||
@@ -318,14 +336,21 @@ def _positive_int(value: str) -> int:
|
||||
|
||||
|
||||
def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
|
||||
parser.add_argument("--query", required=True)
|
||||
parser.add_argument("--scope")
|
||||
parser.add_argument("--table")
|
||||
parser.add_argument("--field")
|
||||
parser.add_argument("--limit", type=_positive_int, default=10)
|
||||
parser.add_argument("--tsv", type=Path, default=DEFAULT_TSV)
|
||||
parser.add_argument("--lexicon", type=Path, default=DEFAULT_LEXICON)
|
||||
parser = argparse.ArgumentParser(
|
||||
description="查询独立的天软表、数据源与字段字典索引。",
|
||||
epilog=HELP_EPILOG,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
allow_abbrev=False,
|
||||
)
|
||||
parser.add_argument("--query", required=True, help="中文业务描述、名称或 ID")
|
||||
parser.add_argument("--scope", help="按业务范围过滤")
|
||||
parser.add_argument("--table", help="按表名或数据源名过滤")
|
||||
parser.add_argument("--field", help="按字段名或字段 ID 过滤")
|
||||
parser.add_argument("--limit", type=_positive_int, default=10, help="候选条数上限")
|
||||
parser.add_argument("--tsv", type=Path, default=DEFAULT_TSV, help="字典索引路径")
|
||||
parser.add_argument(
|
||||
"--lexicon", type=Path, default=DEFAULT_LEXICON, help="受控同义词词表路径"
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Resolve framework-class profiles without replacing ordinary API lookup."""
|
||||
"""Library for validating and resolving evidence-backed framework profiles."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from lookup import (
|
||||
DEFAULT_TSV,
|
||||
codegen_root_for_tsv,
|
||||
load_rows,
|
||||
normalize,
|
||||
search_exact,
|
||||
slice_entry,
|
||||
@@ -230,33 +225,6 @@ def validate_index(data, rows, codegen_root):
|
||||
return errors
|
||||
|
||||
|
||||
def find_framework(data, name):
|
||||
requested = normalize(name)
|
||||
return next(
|
||||
(
|
||||
framework
|
||||
for framework in data.get("frameworks", [])
|
||||
if isinstance(framework, dict)
|
||||
and normalize(framework.get("qualified_name", "")) == requested
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
def parse_config(values):
|
||||
config = {}
|
||||
errors = []
|
||||
for value in values:
|
||||
key, separator, raw_value = value.partition("=")
|
||||
if not separator or not key.strip() or not raw_value.strip():
|
||||
errors.append(
|
||||
f"invalid --config {value!r}; expected a non-empty key=value pair"
|
||||
)
|
||||
continue
|
||||
config[key.strip()] = raw_value.strip()
|
||||
return config, errors
|
||||
|
||||
|
||||
def scaffold_packet(framework, config):
|
||||
configured_names = {
|
||||
entry["api"].rsplit(".", 1)[-1]
|
||||
@@ -320,135 +288,3 @@ def scaffold_packet(framework, config):
|
||||
"diagnostics": diagnostics,
|
||||
"evidence": framework.get("evidence", []),
|
||||
}
|
||||
|
||||
|
||||
def print_text(packet):
|
||||
print(f"framework: {packet['framework']}")
|
||||
print(f"scope/module: {packet['scope']}/{packet['module']}")
|
||||
print(f"scaffold_status: {packet['scaffold_status']}")
|
||||
print(f"contract_status: {packet['contract_status']}")
|
||||
print("lifecycle: " + " -> ".join(item["phase"] for item in packet["lifecycle"]))
|
||||
print("configuration: " + ", ".join(item["api"] for item in packet["configuration"]))
|
||||
required_hooks = [item["api"] for item in packet["hooks"] if item.get("required")]
|
||||
print("required_hooks: " + (", ".join(required_hooks) or "none"))
|
||||
print("state_apis: " + ", ".join(item["api"] for item in packet["state_apis"]))
|
||||
print("execution: " + ", ".join(item["api"] for item in packet["execution"]))
|
||||
print("result_apis: " + ", ".join(item["api"] for item in packet["result_apis"]))
|
||||
if packet["configured_values"]:
|
||||
print("configured_values: " + ", ".join(
|
||||
f"{key}={value}" for key, value in packet["configured_values"].items()
|
||||
))
|
||||
if packet["diagnostics"]:
|
||||
print("diagnostics:")
|
||||
for diagnostic in packet["diagnostics"]:
|
||||
print(f"- {diagnostic['code']}: {diagnostic['message']}")
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Resolve framework-class scaffolds for TSL API consumers.",
|
||||
allow_abbrev=False,
|
||||
)
|
||||
action = parser.add_mutually_exclusive_group(required=True)
|
||||
action.add_argument("--framework", help="exact framework qualified name")
|
||||
action.add_argument(
|
||||
"--check", action="store_true", help="validate all framework profile references"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
action="append",
|
||||
default=[],
|
||||
metavar="KEY=VALUE",
|
||||
help="raw framework configuration; may be repeated",
|
||||
)
|
||||
parser.add_argument("--format", choices=("text", "json"), default="text")
|
||||
parser.add_argument("--index", metavar="PATH", help="framework_index.json path")
|
||||
parser.add_argument("--tsv", metavar="PATH", help="function_index.tsv path")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
index_path = Path(args.index) if args.index else DEFAULT_INDEX
|
||||
tsv_path = Path(args.tsv) if args.tsv else DEFAULT_TSV
|
||||
try:
|
||||
data = load_index(index_path)
|
||||
rows = load_rows(tsv_path)
|
||||
except (OSError, UnicodeError, ValueError) as error:
|
||||
print(f"ERROR: {error}", file=sys.stderr)
|
||||
return 1
|
||||
if not rows:
|
||||
print(f"ERROR: {tsv_path} has no entries", file=sys.stderr)
|
||||
return 1
|
||||
codegen_root = codegen_root_for_tsv(tsv_path)
|
||||
|
||||
errors = validate_index(data, rows, codegen_root)
|
||||
if args.check:
|
||||
if errors:
|
||||
for error in errors:
|
||||
print(f"ERROR: {error}", file=sys.stderr)
|
||||
return 1
|
||||
print(f"OK: {len(data['frameworks'])} framework profile(s) validated")
|
||||
return 0
|
||||
|
||||
if errors:
|
||||
packet = {
|
||||
"status": "data_error",
|
||||
"framework": args.framework,
|
||||
"diagnostics": [
|
||||
{
|
||||
"code": "PROFILE_REFERENCE_ERROR",
|
||||
"severity": "error",
|
||||
"message": error,
|
||||
"next_action": "Fix framework_index.json or rebuild the API index.",
|
||||
}
|
||||
for error in errors
|
||||
],
|
||||
}
|
||||
if args.format == "json":
|
||||
print(json.dumps(packet, ensure_ascii=False, indent=2))
|
||||
else:
|
||||
print(f"Framework profile data is invalid for {args.framework!r}.")
|
||||
for diagnostic in packet["diagnostics"]:
|
||||
print(f"- {diagnostic['code']}: {diagnostic['message']}")
|
||||
return 1
|
||||
|
||||
framework = find_framework(data, args.framework)
|
||||
if framework is None:
|
||||
packet = {
|
||||
"status": "not_found",
|
||||
"framework": args.framework,
|
||||
"diagnostics": [
|
||||
{
|
||||
"code": "FRAMEWORK_NOT_FOUND",
|
||||
"severity": "error",
|
||||
"message": f"No framework profile named {args.framework!r}.",
|
||||
"next_action": "Use --check or add a framework profile.",
|
||||
}
|
||||
],
|
||||
}
|
||||
if args.format == "json":
|
||||
print(json.dumps(packet, ensure_ascii=False, indent=2))
|
||||
else:
|
||||
print(f"No framework profile named {args.framework!r}.")
|
||||
return 0
|
||||
|
||||
config, config_errors = parse_config(args.config)
|
||||
packet = scaffold_packet(framework, config)
|
||||
for error in config_errors:
|
||||
packet["diagnostics"].append(
|
||||
{
|
||||
"code": "INVALID_CONFIGURATION",
|
||||
"severity": "error",
|
||||
"message": error,
|
||||
"next_action": "Use KEY=VALUE syntax with non-empty values.",
|
||||
}
|
||||
)
|
||||
if args.format == "json":
|
||||
print(json.dumps(packet, ensure_ascii=False, indent=2))
|
||||
else:
|
||||
print_text(packet)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
@@ -73,11 +73,16 @@ HELP_EPILOG = """\
|
||||
lookup.py --name arrDropDuplicate
|
||||
|
||||
已知确切名称时可以直接用 --name,跳过第 1 步。
|
||||
只查询一个 API scope 时使用 --scope,例如 --scope builtin、
|
||||
--scope dotnet 或 --scope module。
|
||||
只查询一个 API scope 时使用 --scope;可用值从当前 function_index.tsv 读取,
|
||||
不存在的 scope 会列出当前索引实际提供的值并返回 2。
|
||||
|
||||
结果:
|
||||
--kw 输出候选摘要表;选定后仍须运行 --name
|
||||
--name 输出完整条目正文和 scope/module、page#anchor 来源标记;
|
||||
简单成员名可能返回多个 owner,qualified_name 可消歧;重载会全部返回
|
||||
|
||||
退出码:
|
||||
0 取回成功;--name 无匹配也是 0(打印提示,不算错误)
|
||||
0 查询完成;--name 无匹配和 --kw 零候选也通过正文提示表达
|
||||
1 function_index.tsv 缺失、格式错误,或候选指向的条目正文找不到
|
||||
2 参数不合法(缺少动作、空值、--limit 小于 1、使用短选项或缩写)
|
||||
"""
|
||||
@@ -423,7 +428,7 @@ def main(argv=None):
|
||||
type=non_empty,
|
||||
metavar="SCOPE",
|
||||
help="只查询指定 scope,大小写不敏感;作用于 --name 和 --kw。"
|
||||
"内置索引当前提供 builtin、dotnet 与 module",
|
||||
"可用值从所选 function_index.tsv 读取并在加载后校验",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--limit",
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# API 检索
|
||||
|
||||
用于普通函数、具体成员或“有没有某种行为的 API”这类请求。
|
||||
|
||||
先查看脚本当前契约:
|
||||
|
||||
```bash
|
||||
python <this-skill-dir>/scripts/lookup.py --help
|
||||
```
|
||||
|
||||
## 检索顺序
|
||||
|
||||
已知名称时直接精确查询:
|
||||
|
||||
```bash
|
||||
python <this-skill-dir>/scripts/lookup.py --name argmax
|
||||
python <this-skill-dir>/scripts/lookup.py --name DemoUnit.Document.Save
|
||||
```
|
||||
|
||||
只知道行为时必须走两步:
|
||||
|
||||
```bash
|
||||
python <this-skill-dir>/scripts/lookup.py --kw 数组 去重
|
||||
python <this-skill-dir>/scripts/lookup.py --name arrDropDuplicate
|
||||
```
|
||||
|
||||
`--kw` 使用字面 AND 语义,搜索名称、签名、tags、summary、scope、module、
|
||||
`qualified_name`、owner、kind、binding 和 visibility,并只输出候选摘要。选定候选后,
|
||||
必须再用 `--name` 取正文;不得把 summary 当作签名。
|
||||
|
||||
已知范围时用当前索引实际提供的 scope 过滤,例如:
|
||||
|
||||
```bash
|
||||
python <this-skill-dir>/scripts/lookup.py --scope builtin --name boolToStr
|
||||
python <this-skill-dir>/scripts/lookup.py --scope dotnet --kw argmax
|
||||
python <this-skill-dir>/scripts/lookup.py --scope module --name bondAccruedInterest
|
||||
```
|
||||
|
||||
## 精确结果判读
|
||||
|
||||
- `--name` 对简单名称和完全限定名称做大小写不敏感的精确匹配。
|
||||
- 简单成员名会返回所有 owner 下的同名 API;完全限定名用于限定 class 或 unit。
|
||||
- 重载共享 `qualified_name`,精确查询会返回该名称的全部重载。
|
||||
- 混合页面按唯一 `page#anchor` 截取目标 H2 或成员子树;每个 H2 的声明类型独立。
|
||||
- 同名结果跨 scope 或 module 时,结合当前代码上下文消歧,不能取第一条。
|
||||
- 候选中的实例方法显示 `function`,类方法显示 `class function`,静态字段和常量
|
||||
分别显示 `static field`、`static const`。
|
||||
|
||||
脚本可读取必需的 8 列索引;13 列索引额外提供 `qualified_name`、owner、kind、
|
||||
binding 和 visibility。使用自定义 `--tsv` 时,只有索引实际包含的扩展列才能支持对应消歧。
|
||||
|
||||
## 结果与退出码
|
||||
|
||||
- rc=0:查询完成。`--name` 无匹配和 `--kw` 零候选也属于查询完成,必须读取正文状态。
|
||||
- rc=1:索引缺失、格式错误、为空,或候选指向的正文不可达;这是数据/部署错误。
|
||||
- rc=2:参数不合法;按 stderr 和 `--help` 修正后重跑。
|
||||
|
||||
`--name` 成功输出完整条目正文和来源标记;最终报告按 `SKILL.md` 的输出约定组织。
|
||||
@@ -0,0 +1,57 @@
|
||||
# Class 与 Framework 查询
|
||||
|
||||
用于 class 摘要、成员分类、子类实现、回调重写和生命周期协议。公开入口只有
|
||||
`class_lookup.py`;Framework Profile 是 class 查询的附加层,不单独调用另一个 CLI。
|
||||
|
||||
先查看脚本当前契约:
|
||||
|
||||
```bash
|
||||
python <this-skill-dir>/scripts/class_lookup.py --help
|
||||
```
|
||||
|
||||
## 查询
|
||||
|
||||
```bash
|
||||
python <this-skill-dir>/scripts/class_lookup.py --list
|
||||
python <this-skill-dir>/scripts/class_lookup.py --class TStringList
|
||||
python <this-skill-dir>/scripts/class_lookup.py --class TSBackTesting
|
||||
python <this-skill-dir>/scripts/class_lookup.py \
|
||||
--class TSBackTesting --config FGroupType=1 --format json
|
||||
```
|
||||
|
||||
- 查询或使用任意 class 时,先取类摘要;需要具体成员正文时,再按返回的
|
||||
`qualified_name` 运行 `lookup.py --name`。
|
||||
- `class_lookup.py` 自动覆盖 `function_index.tsv` 中 `kind=class` 的条目,不要求普通
|
||||
对象类维护人工 Profile。
|
||||
- 只有 `kind=framework` 且有证据化生命周期的类才叠加 Framework Profile。不得因目标
|
||||
是 class 就推断它是框架。
|
||||
- 同名 class 的 `status=ambiguous` 时,使用 `--scope`、`--module` 或完全限定名消歧。
|
||||
|
||||
## 状态判读
|
||||
|
||||
- `status=resolved`:类查询已解析;`status=not_found`:索引中无该 class;
|
||||
`status=ambiguous`:存在多个候选。
|
||||
- `profile_status=not_profiled`:普通 class,只使用自动成员视图,不是错误,也不表示数据
|
||||
不完整;成员查询可以继续。若当前请求依赖生命周期、配置或回调,则表示没有已验证的
|
||||
framework 契约,必须停止该部分,不能假设空的 Framework Profile。
|
||||
- `profile_status=resolved`:返回了 Framework Profile。
|
||||
- `scaffold_status=resolved` 只表示生命周期和成员引用完整。
|
||||
- `contract_status` 不是 `resolved` 时,必须按 `diagnostics` 补齐模式与返回 schema 证据,
|
||||
并停止生成回调字段。
|
||||
|
||||
Framework Profile 只描述 API 协议,不定义 TSL 语法或代码结构。解析后的 Framework
|
||||
Contract Packet 仅作为已验证的 API 事实输出。
|
||||
|
||||
只有返回的 Profile 本身带有生命周期、配置、回调、执行入口或结果查询证据时,才使用
|
||||
这些协议;`not_profiled` 时不能从 class 名称或成员列表推断 framework。
|
||||
|
||||
使用自定义 `--tsv` 时默认不加载 Framework Profile,普通 class 仍可查询;该索引确有
|
||||
curated Profile 时必须同时显式传入 `--profiles`。
|
||||
|
||||
## 退出码
|
||||
|
||||
- rc=0:查询或校验完成;`not_found` 和 `ambiguous` 通过输出状态表达。
|
||||
- rc=1:索引/Profile 数据缺失、损坏或引用校验失败。
|
||||
- rc=2:参数不合法。
|
||||
|
||||
`--check` 属维护动作,普通编码查询不需要运行。
|
||||
@@ -0,0 +1,39 @@
|
||||
# 数据字典查询
|
||||
|
||||
用于按中文业务含义查询天软数据字典中的表、数据源或字段。它们不是可调用 API,
|
||||
不进入 `function_index.tsv`。
|
||||
|
||||
先查看脚本当前契约:
|
||||
|
||||
```bash
|
||||
python <this-skill-dir>/scripts/dictionary_lookup.py --help
|
||||
```
|
||||
|
||||
## 查询
|
||||
|
||||
```bash
|
||||
python <this-skill-dir>/scripts/dictionary_lookup.py \
|
||||
--query "股票现金流指标销售现金比率"
|
||||
python <this-skill-dir>/scripts/dictionary_lookup.py --query "市值" --scope fund
|
||||
python <this-skill-dir>/scripts/dictionary_lookup.py --query "买一量" --table TradeTable
|
||||
python <this-skill-dir>/scripts/dictionary_lookup.py --query "9900700" --field 销售现金比率
|
||||
```
|
||||
|
||||
结果包含表或数据源名称、表 ID、字段名称、字段 ID、类型、单位、提取方式、访问代码、
|
||||
对应 API 和字典页面。
|
||||
|
||||
## 状态判读
|
||||
|
||||
- `status: ok`:最高分候选唯一。
|
||||
- `status: ambiguous`:存在多个同分候选;列出候选并请用户补充 scope、表或数据源范围,
|
||||
不能默认选第一条。
|
||||
- `status: no_match`:数据字典无匹配,不能改述为 API 的 `not found`。
|
||||
|
||||
标识符匹配保留下划线;同义词只来自 `dictionary_lexicon.json` 的受控映射。
|
||||
`MarketTable` 和 `TradeTable` 是 TS-SQL 数据源,不是函数。
|
||||
|
||||
## 退出码
|
||||
|
||||
- rc=0:查询完成,实际结果由 `status` 判定,包括 `ambiguous` 和 `no_match`。
|
||||
- rc=1:字典索引或词表缺失、损坏、格式错误。
|
||||
- rc=2:参数不合法。
|
||||
Reference in New Issue
Block a user