tyc-cli 0.3.4 → 0.3.6
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/README.md +65 -50
- package/dist/catalog.json +176 -60
- package/dist/commands/capabilities.js +32 -0
- package/dist/commands/category.js +15 -72
- package/dist/commands/coreCall.js +23 -0
- package/dist/commands/init.js +27 -18
- package/dist/commands/layers.js +13 -1
- package/dist/commands/login.js +290 -0
- package/dist/commands/output.js +60 -0
- package/dist/config.js +131 -7
- package/dist/coreClient.js +121 -0
- package/dist/index.js +75 -82
- package/dist/logo.js +24 -0
- package/dist/oauth.js +430 -0
- package/dist/oauthPending.js +41 -0
- package/dist/types.js +3 -4
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/dist/mcpClient.js +0 -146
- package/dist/session.js +0 -47
package/dist/config.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
// ~/.tyc/config.json 读写
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// { "url": "https://mcp.tianyancha.com/
|
|
5
|
-
// "headers": { "Authorization": "xxxx" }
|
|
3
|
+
// 配置文件格式:
|
|
4
|
+
// { "url": "https://mcp.tianyancha.com/mcp",
|
|
5
|
+
// "headers": { "Authorization": "xxxx" },
|
|
6
|
+
// "coreUrl": "https://mcp.tianyancha.com/v1/core/tools/call" }
|
|
6
7
|
//
|
|
7
|
-
// 默认端点指向天眼查公网 MCP Server
|
|
8
|
-
//
|
|
8
|
+
// 默认端点指向天眼查公网 MCP Server 的 canonical 入口 /mcp,CLI 实际调用
|
|
9
|
+
// shared core REST 端点;连接本地或私有环境可通过
|
|
10
|
+
// tyc init --url http://localhost:8080/mcp 或 TYC_MCP_ENDPOINT 环境变量覆盖。
|
|
9
11
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "node:fs";
|
|
10
12
|
import { join } from "node:path";
|
|
11
13
|
import { homedir } from "node:os";
|
|
14
|
+
import { exchangeRefreshToken } from "./oauth.js";
|
|
12
15
|
const CONFIG_DIR = join(homedir(), ".tyc");
|
|
13
16
|
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
14
|
-
export const DEFAULT_MCP_URL = "https://mcp.tianyancha.com/
|
|
17
|
+
export const DEFAULT_MCP_URL = "https://mcp.tianyancha.com/mcp";
|
|
15
18
|
export function loadConfig() {
|
|
16
19
|
if (!existsSync(CONFIG_FILE))
|
|
17
20
|
return null;
|
|
@@ -32,9 +35,75 @@ export function saveConfig(config) {
|
|
|
32
35
|
}
|
|
33
36
|
catch { /* best effort */ }
|
|
34
37
|
}
|
|
38
|
+
const OAUTH_REFRESH_SKEW_MS = 60 * 1000;
|
|
39
|
+
function normalizeTokenType(raw) {
|
|
40
|
+
if (!raw)
|
|
41
|
+
return "Bearer";
|
|
42
|
+
return raw.toLowerCase() === "bearer" ? "Bearer" : raw;
|
|
43
|
+
}
|
|
44
|
+
function jwtExpiresAt(accessToken) {
|
|
45
|
+
const parts = accessToken.split(".");
|
|
46
|
+
if (parts.length < 2)
|
|
47
|
+
return undefined;
|
|
48
|
+
try {
|
|
49
|
+
const normalized = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
50
|
+
const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4);
|
|
51
|
+
const payload = JSON.parse(Buffer.from(padded, "base64").toString("utf-8"));
|
|
52
|
+
if (typeof payload.exp === "number" && Number.isFinite(payload.exp)) {
|
|
53
|
+
return payload.exp * 1000;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Non-JWT access tokens are valid OAuth responses too; expires_in remains authoritative.
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
export function inferAccessTokenExpiresAt(token, now = Date.now()) {
|
|
62
|
+
if (typeof token.expires_in === "number" && Number.isFinite(token.expires_in) && token.expires_in > 0) {
|
|
63
|
+
return now + token.expires_in * 1000;
|
|
64
|
+
}
|
|
65
|
+
return jwtExpiresAt(token.access_token);
|
|
66
|
+
}
|
|
67
|
+
export function applyOAuthTokenToConfig(config, token, opts) {
|
|
68
|
+
const now = opts.now ?? Date.now();
|
|
69
|
+
const tokenType = normalizeTokenType(token.token_type);
|
|
70
|
+
const refreshToken = token.refresh_token || opts.refreshToken;
|
|
71
|
+
if (!config.headers)
|
|
72
|
+
config.headers = {};
|
|
73
|
+
config.headers.Authorization = `${tokenType} ${token.access_token}`;
|
|
74
|
+
if (!refreshToken) {
|
|
75
|
+
delete config.oauth;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
config.oauth = {
|
|
79
|
+
tokenEndpoint: opts.tokenEndpoint,
|
|
80
|
+
clientId: opts.clientId,
|
|
81
|
+
clientSecret: opts.clientSecret,
|
|
82
|
+
refreshToken,
|
|
83
|
+
resource: opts.resource,
|
|
84
|
+
scope: token.scope || opts.scope,
|
|
85
|
+
tokenType,
|
|
86
|
+
accessTokenExpiresAt: inferAccessTokenExpiresAt(token, now),
|
|
87
|
+
updatedAt: now,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function defaultCoreURL(mcpURL) {
|
|
91
|
+
const u = new URL(mcpURL);
|
|
92
|
+
const path = u.pathname.replace(/\/+$/, "");
|
|
93
|
+
if (path === "" || path === "/v1" || path === "/mcp") {
|
|
94
|
+
u.pathname = "/v1/core/tools/call";
|
|
95
|
+
}
|
|
96
|
+
else if (!path.endsWith("/core/tools/call")) {
|
|
97
|
+
u.pathname = `${path}/core/tools/call`;
|
|
98
|
+
}
|
|
99
|
+
u.search = "";
|
|
100
|
+
u.hash = "";
|
|
101
|
+
return u.toString();
|
|
102
|
+
}
|
|
35
103
|
export function resolveConfig() {
|
|
36
104
|
const cfg = loadConfig() || {};
|
|
37
105
|
const url = process.env.TYC_MCP_ENDPOINT || cfg.url || DEFAULT_MCP_URL;
|
|
106
|
+
const coreUrl = process.env.TYC_CORE_ENDPOINT || cfg.coreUrl || defaultCoreURL(url);
|
|
38
107
|
const headers = { ...(cfg.headers || {}) };
|
|
39
108
|
if (!headers.Authorization && process.env.TYC_AUTHORIZATION) {
|
|
40
109
|
headers.Authorization = process.env.TYC_AUTHORIZATION;
|
|
@@ -43,5 +112,60 @@ export function resolveConfig() {
|
|
|
43
112
|
console.error("未配置 Authorization。请先运行:tyc init --authorization YOUR_API_KEY");
|
|
44
113
|
process.exit(1);
|
|
45
114
|
}
|
|
46
|
-
return { url, headers };
|
|
115
|
+
return { url, coreUrl, headers, oauth: cfg.oauth ? { ...cfg.oauth } : undefined };
|
|
116
|
+
}
|
|
117
|
+
function hasBearerAuthorization(config) {
|
|
118
|
+
return /^Bearer\s+\S+/i.test(config.headers.Authorization || "");
|
|
119
|
+
}
|
|
120
|
+
function shouldRefreshOAuth(oauth, opts) {
|
|
121
|
+
if (opts.force)
|
|
122
|
+
return true;
|
|
123
|
+
if (!oauth.accessTokenExpiresAt)
|
|
124
|
+
return false;
|
|
125
|
+
const skewMs = opts.skewMs ?? OAUTH_REFRESH_SKEW_MS;
|
|
126
|
+
return oauth.accessTokenExpiresAt <= Date.now() + skewMs;
|
|
127
|
+
}
|
|
128
|
+
export async function refreshResolvedConfigOAuth(resolved, opts = {}) {
|
|
129
|
+
const stored = loadConfig() || {};
|
|
130
|
+
const oauth = stored.oauth;
|
|
131
|
+
if (!oauth?.refreshToken || !oauth.tokenEndpoint || !oauth.clientId) {
|
|
132
|
+
return { config: resolved, refreshed: false };
|
|
133
|
+
}
|
|
134
|
+
if (!hasBearerAuthorization(resolved)) {
|
|
135
|
+
return { config: resolved, refreshed: false };
|
|
136
|
+
}
|
|
137
|
+
if (!shouldRefreshOAuth(oauth, opts)) {
|
|
138
|
+
return { config: resolved, refreshed: false };
|
|
139
|
+
}
|
|
140
|
+
let token;
|
|
141
|
+
try {
|
|
142
|
+
token = await exchangeRefreshToken({
|
|
143
|
+
tokenEndpoint: oauth.tokenEndpoint,
|
|
144
|
+
clientId: oauth.clientId,
|
|
145
|
+
clientSecret: oauth.clientSecret,
|
|
146
|
+
refreshToken: oauth.refreshToken,
|
|
147
|
+
resource: oauth.resource,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
152
|
+
throw new Error(`OAuth refresh token 刷新失败,请重新运行 tyc login:${msg}`);
|
|
153
|
+
}
|
|
154
|
+
applyOAuthTokenToConfig(stored, token, {
|
|
155
|
+
tokenEndpoint: oauth.tokenEndpoint,
|
|
156
|
+
clientId: oauth.clientId,
|
|
157
|
+
clientSecret: oauth.clientSecret,
|
|
158
|
+
refreshToken: oauth.refreshToken,
|
|
159
|
+
resource: oauth.resource,
|
|
160
|
+
scope: oauth.scope,
|
|
161
|
+
});
|
|
162
|
+
saveConfig(stored);
|
|
163
|
+
if (opts.verbose) {
|
|
164
|
+
console.error("> OAuth access token refreshed");
|
|
165
|
+
}
|
|
166
|
+
return { config: resolveConfig(), refreshed: true };
|
|
167
|
+
}
|
|
168
|
+
export async function resolveConfigWithFreshOAuth(opts = {}) {
|
|
169
|
+
const resolved = resolveConfig();
|
|
170
|
+
return refreshResolvedConfigOAuth(resolved, opts);
|
|
47
171
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
function maskToken(v) {
|
|
2
|
+
if (!v)
|
|
3
|
+
return "";
|
|
4
|
+
if (v.length <= 8)
|
|
5
|
+
return "****";
|
|
6
|
+
return v.slice(0, 4) + "****" + v.slice(-4);
|
|
7
|
+
}
|
|
8
|
+
function coreSiblingURL(coreUrl, siblingPath) {
|
|
9
|
+
const u = new URL(coreUrl);
|
|
10
|
+
const path = u.pathname.replace(/\/+$/, "");
|
|
11
|
+
if (path.endsWith("/tools/call")) {
|
|
12
|
+
u.pathname = path.slice(0, -"/tools/call".length) + siblingPath;
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
u.pathname = path + siblingPath;
|
|
16
|
+
}
|
|
17
|
+
u.search = "";
|
|
18
|
+
u.hash = "";
|
|
19
|
+
return u.toString();
|
|
20
|
+
}
|
|
21
|
+
function readyURL(coreUrl) {
|
|
22
|
+
return coreSiblingURL(coreUrl, "/ready");
|
|
23
|
+
}
|
|
24
|
+
function authReadyURL(coreUrl) {
|
|
25
|
+
return coreSiblingURL(coreUrl, "/auth/ready");
|
|
26
|
+
}
|
|
27
|
+
export async function verifyCoreEndpoint(cfg, verbose = false) {
|
|
28
|
+
const url = readyURL(cfg.coreUrl);
|
|
29
|
+
if (verbose)
|
|
30
|
+
console.error(`> GET ${url}`);
|
|
31
|
+
const resp = await fetch(url, {
|
|
32
|
+
method: "GET",
|
|
33
|
+
headers: { Accept: "text/plain" },
|
|
34
|
+
});
|
|
35
|
+
const text = await resp.text();
|
|
36
|
+
if (verbose) {
|
|
37
|
+
console.error(`< ${resp.status} ${resp.statusText}`);
|
|
38
|
+
console.error(`< body: ${text.slice(0, 500)}`);
|
|
39
|
+
}
|
|
40
|
+
if (resp.status < 200 || resp.status >= 300) {
|
|
41
|
+
throw new Error(`core endpoint 校验失败:HTTP ${resp.status}\n${text.slice(0, 500)}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export async function verifyCoreAuthEndpoint(cfg, verbose = false) {
|
|
45
|
+
const url = authReadyURL(cfg.coreUrl);
|
|
46
|
+
if (verbose)
|
|
47
|
+
console.error(`> GET ${url}`);
|
|
48
|
+
const headers = {
|
|
49
|
+
Accept: "application/json, text/plain",
|
|
50
|
+
...cfg.headers,
|
|
51
|
+
};
|
|
52
|
+
const resp = await fetch(url, {
|
|
53
|
+
method: "GET",
|
|
54
|
+
headers,
|
|
55
|
+
});
|
|
56
|
+
const text = await resp.text();
|
|
57
|
+
if (verbose) {
|
|
58
|
+
console.error(`< ${resp.status} ${resp.statusText}`);
|
|
59
|
+
console.error(`< body: ${text.slice(0, 500)}`);
|
|
60
|
+
}
|
|
61
|
+
if (resp.status < 200 || resp.status >= 300) {
|
|
62
|
+
let desc = text.slice(0, 500);
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(text);
|
|
65
|
+
desc = String(parsed.error_description || parsed.error || desc);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// keep raw text
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`core/auth/ready HTTP ${resp.status}\n${desc}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export async function callCoreTool(cfg, name, args, opts) {
|
|
74
|
+
const verbose = !!opts?.verbose;
|
|
75
|
+
const format = opts?.format || "json";
|
|
76
|
+
const headers = {
|
|
77
|
+
"Content-Type": "application/json",
|
|
78
|
+
Accept: "application/json",
|
|
79
|
+
...cfg.headers,
|
|
80
|
+
};
|
|
81
|
+
const body = {
|
|
82
|
+
tool_name: name,
|
|
83
|
+
arguments: args,
|
|
84
|
+
format,
|
|
85
|
+
};
|
|
86
|
+
if (verbose) {
|
|
87
|
+
const masked = {};
|
|
88
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
89
|
+
masked[k] = k.toLowerCase() === "authorization" ? maskToken(v) : v;
|
|
90
|
+
}
|
|
91
|
+
console.error(`> POST ${cfg.coreUrl}`);
|
|
92
|
+
console.error(`> headers: ${JSON.stringify(masked)}`);
|
|
93
|
+
console.error(`> body: ${JSON.stringify(body)}`);
|
|
94
|
+
}
|
|
95
|
+
const resp = await fetch(cfg.coreUrl, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers,
|
|
98
|
+
body: JSON.stringify(body),
|
|
99
|
+
});
|
|
100
|
+
const text = await resp.text();
|
|
101
|
+
if (verbose) {
|
|
102
|
+
console.error(`< ${resp.status} ${resp.statusText}`);
|
|
103
|
+
console.error(`< body: ${text.slice(0, 2000)}`);
|
|
104
|
+
}
|
|
105
|
+
let parsed;
|
|
106
|
+
try {
|
|
107
|
+
parsed = JSON.parse(text);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
throw new Error(`core 响应不是 JSON:HTTP ${resp.status}\n${text.slice(0, 500)}`);
|
|
111
|
+
}
|
|
112
|
+
if (resp.status < 200 || resp.status >= 300) {
|
|
113
|
+
const desc = parsed.error_description || parsed.error || text.slice(0, 500);
|
|
114
|
+
throw new Error(`core/tools/call HTTP ${resp.status}\n${desc}`);
|
|
115
|
+
}
|
|
116
|
+
const content = parsed.content;
|
|
117
|
+
const contentText = typeof content === "string" ? content : JSON.stringify(content ?? {});
|
|
118
|
+
return {
|
|
119
|
+
content: [{ type: "text", text: contentText }],
|
|
120
|
+
};
|
|
121
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,99 +1,77 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { registerInitCommand } from "./commands/init.js";
|
|
4
|
+
import { registerLoginCommand } from "./commands/login.js";
|
|
4
5
|
import { registerCategoryCommands } from "./commands/category.js";
|
|
5
6
|
import { registerLayerCommands } from "./commands/layers.js";
|
|
6
7
|
import { getCategories, getLayerCount, getPriorityTools, getTotalCount, } from "./registry.js";
|
|
7
8
|
import { VERSION } from "./version.js";
|
|
8
|
-
// 顶层 help
|
|
9
|
-
//
|
|
9
|
+
// 顶层 help 保持为快速入口:讲清分层、调用顺序、发现命令和输出控制;
|
|
10
|
+
// 更完整的架构说明放在 `tyc layers`。
|
|
10
11
|
const helpText = `
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
anchored USCC. Read _summary, then use layer lists or
|
|
30
|
-
MCP get_company_capabilities for the next tool.
|
|
31
|
-
2. Descend to L2. Pick a specific dimension
|
|
32
|
-
(shareholders / litigation / patents / annual reports …).
|
|
33
|
-
If unsure which L2, default to that facet's L2★ 优先 (below).
|
|
34
|
-
3. Land on L3. When L2 returns a list item with an \`id\`, a \`search_*\`
|
|
35
|
-
need arises, or a vertical SKILL is activated.
|
|
36
|
-
|
|
37
|
-
Rule of thumb: the smaller the layer number, the higher the information
|
|
38
|
-
density per token. L0 is MANDATORY — skipping it (without a USCC already
|
|
39
|
-
in hand) wastes downstream calls on the wrong entity.
|
|
40
|
-
Default LLM surface = L0 + L1 = ${getLayerCount("L0") + getLayerCount("L1")} tools (≤ 15-tool exposure limit).
|
|
41
|
-
|
|
42
|
-
L2 优先 ★ — \`L2★ 队副\`(${getPriorityTools().length} 个,每 facet 1 个)
|
|
12
|
+
工具分层
|
|
13
|
+
|
|
14
|
+
tyc 采用 4 层渐进发现,默认只把 L0 + L1 的 ${getLayerCount("L0") + getLayerCount("L1")} 个高密度工具暴露给 Agent,再按需下钻到 L2/L3。
|
|
15
|
+
|
|
16
|
+
┌─ L0 实体锚定 ${pad(getLayerCount("L0"), 3)} 个工具 简称 / 曾用名 / 模糊名 → 精确企业
|
|
17
|
+
├─ L1 概要总览 ${pad(getLayerCount("L1"), 3)} 个工具 每个 facet 1 个总览工具,返回 _summary
|
|
18
|
+
├─ L2 维度下钻 ${pad(getLayerCount("L2"), 3)} 个工具 股东、诉讼、商标、招投标、年报等明细维度
|
|
19
|
+
└─ L3 专项工具 ${pad(getLayerCount("L3"), 3)} 个工具 id 详情、search_*、垂直场景和专业查询
|
|
20
|
+
|
|
21
|
+
推荐调用顺序
|
|
22
|
+
|
|
23
|
+
0. 先做 L0:用用户给出的原始企业名调用 \`search_companies\`,确认唯一企业、
|
|
24
|
+
统一社会信用代码和企业全名。
|
|
25
|
+
1. 再看 L1:按问题选择 1 个概要工具,读取 _summary 判断下一步。
|
|
26
|
+
2. 下钻 L2:选择具体数据维度;不确定时优先用对应 facet 的 L2★ 工具。
|
|
27
|
+
3. 进入 L3:需要记录 id 详情、跨主体 search_* 或垂直 SKILL 时再使用。
|
|
28
|
+
|
|
29
|
+
L2 优先 ★(${getPriorityTools().length} 个,每个 facet 1 个)
|
|
43
30
|
|
|
44
31
|
Agent 拿到 L1 _summary 后若不确定走哪个 L2,对应 facet 的"L2 优先"是默认下钻:
|
|
45
32
|
|
|
46
33
|
${renderPriorityLines()}
|
|
47
34
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
DISCOVER BY LAYER
|
|
35
|
+
按层发现
|
|
51
36
|
|
|
52
|
-
tyc layers
|
|
53
|
-
tyc L0 list
|
|
54
|
-
tyc L1 list
|
|
55
|
-
tyc L2 list
|
|
56
|
-
tyc L3 list
|
|
57
|
-
tyc L0 list --md Markdown
|
|
58
|
-
tyc L0 list --json
|
|
37
|
+
tyc layers 查看一屏架构图和推荐调用顺序
|
|
38
|
+
tyc L0 list 列出 ${getLayerCount("L0")} 个 L0 工具
|
|
39
|
+
tyc L1 list 列出 ${getLayerCount("L1")} 个 L1 工具
|
|
40
|
+
tyc L2 list 列出 ${getLayerCount("L2")} 个 L2 工具(★ 优先工具置顶)
|
|
41
|
+
tyc L3 list 列出 ${getLayerCount("L3")} 个 L3 工具
|
|
42
|
+
tyc L0 list --md 输出 Markdown 表格
|
|
43
|
+
tyc L0 list --json 输出 JSON(含工具名、CLI、参数、描述、优先级)
|
|
44
|
+
tyc company capabilities <company_id> --company-name <name>
|
|
45
|
+
根据 L0 返回的 company_id 打印可直接执行的工具清单
|
|
59
46
|
|
|
60
|
-
|
|
47
|
+
按分类发现(与层级正交,共 6 组)
|
|
61
48
|
|
|
62
49
|
${renderCategoryLines()}
|
|
63
50
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
--
|
|
70
|
-
--
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
--head [N]
|
|
75
|
-
--tail [M]
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
--
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
--
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
SETUP
|
|
89
|
-
|
|
90
|
-
tyc init --authorization <KEY> default https://mcp.tianyancha.com/v1
|
|
91
|
-
tyc init --url <MCP_URL> --authorization <KEY> local / self-hosted
|
|
92
|
-
|
|
93
|
-
Every tool returns tyc OpenAPI native English keys verbatim. The MCP Server
|
|
94
|
-
handles multi-source merge, Asia/Shanghai timestamp formatting, _summary
|
|
95
|
-
injection, and empty-result normalization — the CLI just tells the Agent
|
|
96
|
-
where the data is and what it looks like.
|
|
51
|
+
全局输出选项(所有工具命令均可使用)
|
|
52
|
+
|
|
53
|
+
以下选项可用于所有工具调用;具体工具 help 不重复展开完整列表。
|
|
54
|
+
|
|
55
|
+
默认 缩进 JSON,兼顾人类阅读和 Agent 解析
|
|
56
|
+
--pretty 同默认输出,保留用于显式声明意图
|
|
57
|
+
--compact 单行紧凑 JSON,适合管道和 jq
|
|
58
|
+
--md Markdown 表格,适合终端阅读或粘贴给 Agent
|
|
59
|
+
--verbose 额外把 shared core 请求详情打印到 stderr
|
|
60
|
+
|
|
61
|
+
--head [N] 只输出前 N 行;只写 flag 时默认 50 行
|
|
62
|
+
--tail [M] 只输出后 M 行;只写 flag 时默认 20 行
|
|
63
|
+
--full 强制输出完整内容,优先级最高
|
|
64
|
+
--threshold <BYTES> 超过指定字节数时从头截断;不传则不自动截断
|
|
65
|
+
--output-file <PATH> 把完整结果写入文件;必须显式指定才会落盘
|
|
66
|
+
|
|
67
|
+
初始化与登录
|
|
68
|
+
|
|
69
|
+
tyc login OAuth 浏览器登录
|
|
70
|
+
tyc login --no-open --no-block 打印 OAuth URL 后立即退出,手动完成
|
|
71
|
+
tyc login --callback-token <CODE_OR_CALLBACK_URL> 继续非阻塞 OAuth 登录
|
|
72
|
+
tyc login --url <MCP_URL> 登录本地、预发或自托管 MCP
|
|
73
|
+
tyc init --authorization <KEY> 使用 API Key 兼容路径
|
|
74
|
+
tyc init --url <MCP_URL> --authorization <KEY> 配置本地或自托管服务
|
|
97
75
|
`;
|
|
98
76
|
function pad(n, width) {
|
|
99
77
|
const s = String(n);
|
|
@@ -102,7 +80,7 @@ function pad(n, width) {
|
|
|
102
80
|
function renderCategoryLines() {
|
|
103
81
|
const lines = [];
|
|
104
82
|
for (const c of getCategories()) {
|
|
105
|
-
lines.push(` tyc ${c.group.padEnd(22)} ${c.name_zh} (${c.tool_count}
|
|
83
|
+
lines.push(` tyc ${c.group.padEnd(22)} ${c.name_zh} (${c.tool_count} 个工具)`);
|
|
106
84
|
}
|
|
107
85
|
return lines.join("\n");
|
|
108
86
|
}
|
|
@@ -115,12 +93,26 @@ function renderPriorityLines() {
|
|
|
115
93
|
}
|
|
116
94
|
const program = new Command()
|
|
117
95
|
.name("tyc")
|
|
118
|
-
.description(
|
|
119
|
-
.version(VERSION)
|
|
96
|
+
.description(`天眼查 AI CLI:${getTotalCount()} 个商业数据工具 · L0=${getLayerCount("L0")} L1=${getLayerCount("L1")} L2=${getLayerCount("L2")} L3=${getLayerCount("L3")} · 面向 Agent 的 MCP 工具网关`)
|
|
97
|
+
.version(VERSION, "-V, --version", "输出版本号")
|
|
98
|
+
.helpOption("-h, --help", "显示命令帮助")
|
|
99
|
+
.addHelpCommand("help [command]", "显示命令帮助")
|
|
100
|
+
.configureHelp({
|
|
101
|
+
styleTitle(title) {
|
|
102
|
+
const titleMap = {
|
|
103
|
+
"Usage:": "用法:",
|
|
104
|
+
"Arguments:": "参数:",
|
|
105
|
+
"Options:": "选项:",
|
|
106
|
+
"Global Options:": "全局选项:",
|
|
107
|
+
"Commands:": "命令:",
|
|
108
|
+
};
|
|
109
|
+
return titleMap[title] || title;
|
|
110
|
+
},
|
|
111
|
+
})
|
|
120
112
|
.option("--pretty", "缩进 JSON 输出(默认行为,flag 保留以保持向后兼容)")
|
|
121
113
|
.option("--md", "Markdown 表格化输出(适合人类阅读 / Agent 上屏)")
|
|
122
114
|
.option("--compact", "紧凑单行 JSON(管道 / jq 场景;为旧默认行为)")
|
|
123
|
-
.option("--verbose", "打印
|
|
115
|
+
.option("--verbose", "打印 shared core 请求详情到 stderr")
|
|
124
116
|
.option("--head [n]", "仅输出前 N 行(不传值默认 50;与 --tail 同时给则同时输出两端)")
|
|
125
117
|
.option("--tail [m]", "仅输出后 M 行(不传值默认 20;与 --head 同时给则同时输出两端)")
|
|
126
118
|
.option("--full", "强制输出完整内容(最高优先级,覆盖 --head/--tail/--threshold)")
|
|
@@ -128,6 +120,7 @@ const program = new Command()
|
|
|
128
120
|
.option("--threshold <bytes>", "字节截断主开关:超过该字节数则从头按字节截断;不传则永不截断")
|
|
129
121
|
.addHelpText("after", helpText);
|
|
130
122
|
registerInitCommand(program);
|
|
123
|
+
registerLoginCommand(program);
|
|
131
124
|
registerLayerCommands(program);
|
|
132
125
|
registerCategoryCommands(program);
|
|
133
126
|
program.parseAsync(process.argv).catch((err) => {
|
package/dist/logo.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const TY_LOGO = `\x1b[38;2;22;135;245m ▄▄▄████████▄▄▄
|
|
2
|
+
▄▄█████████▀▀▀▀▀▀▀▀▀
|
|
3
|
+
▄████████▀▀▄▄▄▄██████▄▄▄▄
|
|
4
|
+
▄████████████████████████████▄
|
|
5
|
+
▄███████████████████▀▀▀▀▀▀▀████▄
|
|
6
|
+
▄██████████████▀▀ ███
|
|
7
|
+
████████████▀▀ ▄▄▀ █▀
|
|
8
|
+
███████████▀ ▄▄██▀
|
|
9
|
+
█████████▀ ▄█████ ▄
|
|
10
|
+
███████▀ ▄██████▀ ▄█
|
|
11
|
+
██████▀ ▄███████ █ ▄███
|
|
12
|
+
████ █████████ █▄ ▄▄▄▄▄▄▄▄▄█████
|
|
13
|
+
▀██ ██████████ ██▄ ▀▀██████████▀
|
|
14
|
+
▀ ███████████▄ ████▄ ▀▀▀▀▀▀▀▀
|
|
15
|
+
█████████████ ██████▄▄▄
|
|
16
|
+
▀███████████▄ ▀██████████▀
|
|
17
|
+
▀▀█████████▄ ▀██████▀▀
|
|
18
|
+
▀▀▀██████ ▀▀▀\x1b[0m`;
|
|
19
|
+
export const LOGIN_SUCCESS_BANNER = `登录成功
|
|
20
|
+
${TY_LOGO}
|
|
21
|
+
公平看清世界`;
|
|
22
|
+
export function printLoginSuccessBanner() {
|
|
23
|
+
console.log(LOGIN_SUCCESS_BANNER);
|
|
24
|
+
}
|