tyc-cli 0.3.7 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/oauth.js CHANGED
@@ -1,6 +1,19 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { createHash, randomBytes } from "node:crypto";
3
3
  import { createServer } from "node:http";
4
+ export const DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
5
+ export class OAuthTokenEndpointError extends Error {
6
+ status;
7
+ error;
8
+ errorDescription;
9
+ constructor(status, error, errorDescription) {
10
+ super(errorDescription ? `${error}: ${errorDescription}` : error);
11
+ this.name = "OAuthTokenEndpointError";
12
+ this.status = status;
13
+ this.error = error;
14
+ this.errorDescription = errorDescription;
15
+ }
16
+ }
4
17
  function trimRightSlash(v) {
5
18
  return v.replace(/\/+$/, "");
6
19
  }
@@ -123,10 +136,16 @@ export async function discoverAuthorizationServer(issuer) {
123
136
  return {
124
137
  authorizationEndpoint: metadata.authorization_endpoint,
125
138
  tokenEndpoint: metadata.token_endpoint,
139
+ deviceAuthorizationEndpoint: typeof metadata.device_authorization_endpoint === "string"
140
+ ? metadata.device_authorization_endpoint
141
+ : undefined,
126
142
  registrationEndpoint: typeof metadata.registration_endpoint === "string"
127
143
  ? metadata.registration_endpoint
128
144
  : undefined,
129
145
  issuer,
146
+ grantTypesSupported: Array.isArray(metadata.grant_types_supported)
147
+ ? metadata.grant_types_supported.filter((v) => typeof v === "string")
148
+ : undefined,
130
149
  resourceDocumentation: typeof metadata.resource_documentation === "string"
131
150
  ? metadata.resource_documentation
132
151
  : undefined,
@@ -189,13 +208,20 @@ export async function startCallbackServer(opts) {
189
208
  let server;
190
209
  let settled = false;
191
210
  let timer;
211
+ const close = () => {
212
+ settled = true;
213
+ if (timer)
214
+ clearTimeout(timer);
215
+ server?.close();
216
+ server?.closeIdleConnections?.();
217
+ };
192
218
  const codePromise = new Promise((resolve, reject) => {
193
219
  timer = setTimeout(() => {
194
220
  if (settled)
195
221
  return;
196
222
  settled = true;
197
223
  reject(new Error("等待 OAuth 回调超时"));
198
- server?.close();
224
+ close();
199
225
  }, opts.timeoutMs);
200
226
  server = createServer((req, res) => {
201
227
  const url = new URL(req.url || "/", `http://${req.headers.host || opts.host}`);
@@ -218,7 +244,7 @@ export async function startCallbackServer(opts) {
218
244
  res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
219
245
  res.end(callbackHTML("OAuth 登录失败", "state 校验失败,请回到终端重新运行 tyc login。"));
220
246
  reject(new Error("OAuth state mismatch"));
221
- server?.close();
247
+ close();
222
248
  return;
223
249
  }
224
250
  if (error) {
@@ -226,7 +252,7 @@ export async function startCallbackServer(opts) {
226
252
  res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
227
253
  res.end(callbackHTML("OAuth 登录失败", errorDescription || error));
228
254
  reject(new Error(`OAuth error: ${errorDescription || error}`));
229
- server?.close();
255
+ close();
230
256
  return;
231
257
  }
232
258
  if (!code) {
@@ -234,49 +260,52 @@ export async function startCallbackServer(opts) {
234
260
  res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
235
261
  res.end(callbackHTML("OAuth 登录失败", "授权服务未返回 code。"));
236
262
  reject(new Error("OAuth callback missing code"));
237
- server?.close();
263
+ close();
238
264
  return;
239
265
  }
240
266
  settled = true;
241
267
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
242
268
  res.end(callbackHTML("OAuth 登录成功", "tyc CLI 已收到授权结果,可以关闭这个窗口。"));
243
269
  resolve(code);
244
- server?.close();
270
+ close();
245
271
  });
246
272
  });
247
- await new Promise((resolve, reject) => {
248
- if (!server) {
249
- reject(new Error("callback server not initialized"));
250
- return;
251
- }
252
- const onError = (err) => reject(err);
253
- server.once("error", onError);
254
- server.listen(opts.port, opts.host, () => {
255
- server?.off("error", onError);
256
- resolve();
273
+ // Registration/browser setup happens before waitForCode(). Keep an early
274
+ // callback rejection handled while preserving it for that eventual await.
275
+ void codePromise.catch(() => { });
276
+ try {
277
+ await new Promise((resolve, reject) => {
278
+ if (!server) {
279
+ reject(new Error("callback server not initialized"));
280
+ return;
281
+ }
282
+ const onError = (err) => reject(err);
283
+ server.once("error", onError);
284
+ server.listen(opts.port, opts.host, () => {
285
+ server?.off("error", onError);
286
+ resolve();
287
+ });
257
288
  });
258
- });
289
+ }
290
+ catch (err) {
291
+ close();
292
+ throw err;
293
+ }
259
294
  const address = server?.address();
260
295
  if (!address || typeof address.port !== "number") {
261
- server?.close();
296
+ close();
262
297
  throw new Error("无法启动 OAuth 本地回调服务");
263
298
  }
264
299
  const redirectUri = `http://${opts.host}:${address.port}${opts.path}`;
265
300
  return {
266
301
  redirectUri,
267
- close: () => {
268
- if (timer)
269
- clearTimeout(timer);
270
- server?.close();
271
- },
302
+ close,
272
303
  waitForCode: async () => {
273
304
  try {
274
305
  return await codePromise;
275
306
  }
276
307
  finally {
277
- if (timer)
278
- clearTimeout(timer);
279
- server?.close();
308
+ close();
280
309
  }
281
310
  },
282
311
  };
@@ -315,6 +344,54 @@ export async function registerDynamicOAuthClient(opts) {
315
344
  clientSecret: registered.client_secret,
316
345
  };
317
346
  }
347
+ export async function requestDeviceAuthorization(opts) {
348
+ const body = new URLSearchParams();
349
+ body.set("client_id", opts.clientId);
350
+ body.set("scope", opts.scope);
351
+ if (opts.clientSecret)
352
+ body.set("client_secret", opts.clientSecret);
353
+ if (opts.resource)
354
+ body.set("resource", opts.resource);
355
+ const resp = await fetch(opts.deviceAuthorizationEndpoint, {
356
+ method: "POST",
357
+ headers: {
358
+ Accept: "application/json",
359
+ "Content-Type": "application/x-www-form-urlencoded",
360
+ },
361
+ body,
362
+ });
363
+ const text = await resp.text();
364
+ if (resp.status < 200 || resp.status >= 300) {
365
+ throw new Error(`device authorization HTTP ${resp.status}: ${text.slice(0, 500)}`);
366
+ }
367
+ const payload = JSON.parse(text);
368
+ if (!payload.device_code || !payload.user_code || !payload.verification_uri) {
369
+ throw new Error("device authorization response missing required fields");
370
+ }
371
+ return {
372
+ ...payload,
373
+ device_code: payload.device_code,
374
+ user_code: payload.user_code,
375
+ verification_uri: payload.verification_uri,
376
+ expires_in: Number(payload.expires_in) > 0 ? Number(payload.expires_in) : 600,
377
+ interval: Number(payload.interval) > 0 ? Number(payload.interval) : 5,
378
+ };
379
+ }
380
+ function parseOAuthTokenError(status, text) {
381
+ try {
382
+ const payload = JSON.parse(text);
383
+ const error = typeof payload.error === "string" && payload.error
384
+ ? payload.error
385
+ : `token endpoint HTTP ${status}`;
386
+ const description = typeof payload.error_description === "string"
387
+ ? payload.error_description
388
+ : undefined;
389
+ return new OAuthTokenEndpointError(status, error, description);
390
+ }
391
+ catch {
392
+ return new OAuthTokenEndpointError(status, `token endpoint HTTP ${status}`, text.slice(0, 500));
393
+ }
394
+ }
318
395
  export async function exchangeAuthorizationCode(opts) {
319
396
  const body = new URLSearchParams();
320
397
  body.set("grant_type", "authorization_code");
@@ -344,6 +421,33 @@ export async function exchangeAuthorizationCode(opts) {
344
421
  }
345
422
  return token;
346
423
  }
424
+ export async function exchangeDeviceCode(opts) {
425
+ const body = new URLSearchParams();
426
+ body.set("grant_type", DEVICE_CODE_GRANT_TYPE);
427
+ body.set("client_id", opts.clientId);
428
+ body.set("device_code", opts.deviceCode);
429
+ if (opts.clientSecret)
430
+ body.set("client_secret", opts.clientSecret);
431
+ if (opts.resource)
432
+ body.set("resource", opts.resource);
433
+ const resp = await fetch(opts.tokenEndpoint, {
434
+ method: "POST",
435
+ headers: {
436
+ Accept: "application/json",
437
+ "Content-Type": "application/x-www-form-urlencoded",
438
+ },
439
+ body,
440
+ });
441
+ const text = await resp.text();
442
+ if (resp.status < 200 || resp.status >= 300) {
443
+ throw parseOAuthTokenError(resp.status, text);
444
+ }
445
+ const token = JSON.parse(text);
446
+ if (!token.access_token) {
447
+ throw new Error("token endpoint response missing access_token");
448
+ }
449
+ return token;
450
+ }
347
451
  export async function exchangeRefreshToken(opts) {
348
452
  const body = new URLSearchParams();
349
453
  body.set("grant_type", "refresh_token");
@@ -1,7 +1,8 @@
1
1
  // ~/.tyc/oauth_pending.json 读写
2
2
  //
3
- // 非阻塞 OAuth 登录会先保存 PKCE code_verifier、state、client_id 等一次性上下文;
4
- // 用户完成浏览器授权后,再用 tyc login --callback-token <code_or_callback_url> 继续换取 access token。
3
+ // 非阻塞 OAuth 登录会保存一次性上下文:
4
+ // - legacy PKCE 模式保存 code_verifier/state,靠 --callback-token 续接;
5
+ // - RFC 8628 device flow 保存 device_code,靠 --resume 续接。
5
6
  import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
7
  import { homedir } from "node:os";
7
8
  import { join } from "node:path";
package/dist/registry.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // 从 src/catalog.json 加载命令树元数据
2
2
  //
3
3
  // catalog.json 由 apimcp 主仓 scripts/gen_cli_catalog 生成;仅包含 CLI
4
- // 需要的字段(name / group / layer / cliMethod / categoryNameZh /
4
+ // 需要的字段(name / group / layer / requiredPlan / cliMethod / categoryNameZh /
5
5
  // description / params)。不含 sources / execution / summary 等服务端
6
6
  // 内部映射,服务端实现细节不泄露。
7
7
  import { readFileSync } from "node:fs";
@@ -0,0 +1,68 @@
1
+ const object = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
2
+ const text = (v) => typeof v === "string" ? v : JSON.stringify(v) ?? "null";
3
+ const cell = (v) => text(v).replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
4
+ export function renderHistoryGuide(value) {
5
+ if (!object(value))
6
+ return [];
7
+ const lines = [`> 查询范围:${cell(value.scope)}`];
8
+ if (Array.isArray(value.nextTools))
9
+ for (const next of value.nextTools) {
10
+ if (object(next))
11
+ lines.push(`> 后续查询:\`${cell(next.toolName)}\` — ${cell(next.purpose)}`);
12
+ }
13
+ if (Array.isArray(value.notes))
14
+ for (const note of value.notes)
15
+ lines.push(`> 历史查询说明:${cell(note)}`);
16
+ return lines;
17
+ }
18
+ export function splitHistoricalPersonnel(obj, toolName) {
19
+ if (toolName !== "get_historical_registration" || !object(obj.sources) || !object(obj.sources.hi))
20
+ return undefined;
21
+ const hi = { ...obj.sources.hi };
22
+ const lines = [];
23
+ for (const [key, title] of [["pastLegalPersonList", "历史法定代表人"], ["pastStafferList", "历史主要人员"]]) {
24
+ if (!(key in hi))
25
+ continue;
26
+ const value = hi[key];
27
+ delete hi[key];
28
+ lines.push(`### ${title}\n`, "> 按来源分组完整展示;组号不代表任职阶段。\n");
29
+ let groups = Array.isArray(value) ? value : [value];
30
+ if (groups.length === 0)
31
+ lines.push("_(空列表)_\n");
32
+ if (groups.length && groups.every(object)) {
33
+ lines.push("单层名单:\n");
34
+ groups = [groups];
35
+ }
36
+ groups.forEach((group, i) => {
37
+ const people = Array.isArray(group) ? group : [group];
38
+ lines.push(`#### 第 ${i + 1} 组(${people.length} 项)\n`);
39
+ people.forEach((person, j) => {
40
+ lines.push(`人员 ${j + 1}:\n`, "| 字段 | 值 |", "|---|---|");
41
+ if (object(person))
42
+ for (const field of Object.keys(person).sort()) {
43
+ const label = field === "name" ? "名称" : field === "time" ? "变更时间" : field;
44
+ lines.push(`| ${cell(label)} | ${cell(person[field])} |`);
45
+ }
46
+ else
47
+ lines.push(`| 原始项 | ${cell(person)} |`);
48
+ lines.push("");
49
+ });
50
+ });
51
+ }
52
+ return lines.length ? { rest: { ...obj, sources: { ...obj.sources, hi } }, markdown: lines.join("\n") } : undefined;
53
+ }
54
+ function changeDate(value) {
55
+ if (!object(value) || typeof value.changeTime !== "string")
56
+ return "";
57
+ const match = /^(\d{4}-\d{2}-\d{2})(?:$|[ T])/.exec(value.changeTime);
58
+ if (!match)
59
+ return "";
60
+ const day = new Date(`${match[1]}T00:00:00Z`);
61
+ return !Number.isNaN(day.getTime()) && day.toISOString().slice(0, 10) === match[1] ? match[1] : "";
62
+ }
63
+ export function changeRecordsForDisplay(records) {
64
+ return [...records].sort((a, b) => {
65
+ const x = changeDate(a), y = changeDate(b);
66
+ return x < y ? 1 : x > y ? -1 : 0;
67
+ });
68
+ }
@@ -5,21 +5,14 @@
5
5
  * - 顶层简单字段渲染为 key/value 表格
6
6
  * - 项目元数据(_summary / _empty / _warnings)单独凸显
7
7
  * - items 列表自动渲染为表格
8
- * - 嵌套对象/对象数组优雅折叠
8
+ * - 嵌套对象/对象数组完整保留;显式截断由统一输出层负责
9
9
  */
10
- const META_KEYS = new Set(["_summary", "_empty", "_warnings", "_tip"]);
10
+ import { renderChangeRecords, splitChangeRecords } from "./personnelChanges.js";
11
+ import { renderHistoryGuide, splitHistoricalPersonnel } from "./history.js";
12
+ const META_KEYS = new Set(["_summary", "_empty", "_warnings", "_tip", "_truncated", "_pagination", "_riskOverview", "_dateSemantics", "_fieldSemantics", "_historyGuide", "_source"]);
11
13
  function isPlainObject(v) {
12
14
  return v !== null && typeof v === "object" && !Array.isArray(v);
13
15
  }
14
- function isEmptyValue(v) {
15
- if (v === null || v === undefined || v === "")
16
- return true;
17
- if (Array.isArray(v) && v.length === 0)
18
- return true;
19
- if (isPlainObject(v) && Object.keys(v).length === 0)
20
- return true;
21
- return false;
22
- }
23
16
  function formatPrimitive(v) {
24
17
  if (v === null || v === undefined || v === "")
25
18
  return "-";
@@ -49,21 +42,16 @@ function formatArrayCell(arr) {
49
42
  return arr.map(formatPrimitive).join(", ");
50
43
  }
51
44
  // 对象数组:紧凑 JSON
52
- return JSON.stringify(arr).slice(0, 80) + (JSON.stringify(arr).length > 80 ? "…" : "");
45
+ return JSON.stringify(arr);
53
46
  }
54
47
  function formatObjectInline(obj) {
55
- const parts = [];
56
- for (const [k, v] of Object.entries(obj)) {
57
- if (isEmptyValue(v))
58
- continue;
59
- if (isPlainObject(v) || Array.isArray(v)) {
60
- parts.push(`${k}=…`);
61
- }
62
- else {
63
- parts.push(`${k}=${formatPrimitive(v)}`);
64
- }
65
- }
66
- return parts.join("; ") || "-";
48
+ return JSON.stringify(obj);
49
+ }
50
+ function renderJSONBlock(value) {
51
+ const text = JSON.stringify(value, null, 2);
52
+ const longestFence = Math.max(0, ...(text.match(/`+/g) || []).map((s) => s.length));
53
+ const fence = "`".repeat(Math.max(3, longestFence + 1));
54
+ return `${fence}json\n${text}\n${fence}\n`;
67
55
  }
68
56
  function renderItemsTable(items, title = "items") {
69
57
  if (items.length === 0) {
@@ -75,6 +63,9 @@ function renderItemsTable(items, title = "items") {
75
63
  items.forEach((v, i) => lines.push(`${i + 1}. ${formatPrimitive(v)}`));
76
64
  return lines.join("\n") + "\n";
77
65
  }
66
+ if (!items.every(isPlainObject)) {
67
+ return `\n### ${title}(${items.length} 条)\n\n${renderJSONBlock(items)}`;
68
+ }
78
69
  // 对象数组 → Markdown 表格
79
70
  const cols = getAllKeys(items);
80
71
  if (cols.length === 0)
@@ -119,16 +110,71 @@ function renderTopFields(obj) {
119
110
  return lines.join("\n") + "\n";
120
111
  }
121
112
  function renderMeta(obj) {
122
- const lines = [];
113
+ const lines = renderHistoryGuide(obj._historyGuide);
114
+ const sourceNotes = (payload, prefix = "") => {
115
+ if (!isPlainObject(payload._source))
116
+ return;
117
+ const source = payload._source;
118
+ lines.push(`> ${prefix}数据来源:${escapeCell(formatPrimitive(source.api))};${escapeCell(formatPrimitive(source.meaning))}`);
119
+ lines.push(`> ${prefix}来源时间口径:${escapeCell(formatPrimitive(source.temporalNote))}`);
120
+ };
121
+ sourceNotes(obj);
122
+ if (isPlainObject(obj.sources)) {
123
+ for (const [scope, payload] of Object.entries(obj.sources)) {
124
+ if (isPlainObject(payload))
125
+ sourceNotes(payload, `${scope}:`);
126
+ }
127
+ }
128
+ if (isPlainObject(obj._fieldSemantics)) {
129
+ for (const key of Object.keys(obj._fieldSemantics).sort()) {
130
+ const note = obj._fieldSemantics[key];
131
+ if (typeof note === "string" && note)
132
+ lines.push(`> 字段口径:${escapeCell(note)}`);
133
+ }
134
+ }
135
+ if (isPlainObject(obj._dateSemantics)) {
136
+ for (const key of ["change_time", "ftShareholding", "changeTime", "createTime", "historicalTime", "firstAppointment"]) {
137
+ const note = obj._dateSemantics[key];
138
+ if (typeof note === "string" && note)
139
+ lines.push(`> 时间口径:${escapeCell(note)}`);
140
+ }
141
+ }
123
142
  if (obj._summary) {
124
143
  lines.push(`> 📋 **摘要**:${formatPrimitive(obj._summary)}`);
125
144
  }
126
145
  if (obj._tip) {
127
146
  lines.push(`> 💡 **提示**:${formatPrimitive(obj._tip)}`);
128
147
  }
129
- if (obj._empty === true) {
148
+ if (obj._empty === true && !isPlainObject(obj._riskOverview)) {
130
149
  lines.push(`> ⚪ **空结果**:未发现该维度记录`);
131
150
  }
151
+ if (isPlainObject(obj._pagination)) {
152
+ const p = obj._pagination;
153
+ const parts = [];
154
+ if (p.page !== undefined)
155
+ parts.push(`第${p.page}页`);
156
+ if (p.requested_page_size !== undefined)
157
+ parts.push(`请求每页${p.requested_page_size}条`);
158
+ if (p.effective_page_size !== undefined)
159
+ parts.push(`实际每页${p.effective_page_size}条`);
160
+ if (p.max_page_size !== undefined)
161
+ parts.push(`上游每页上限${p.max_page_size}条`);
162
+ parts.push(`本次返回${p.returned}条`);
163
+ parts.push(p.total === undefined ? "全量总数未知" : `共${p.total}条`);
164
+ if (p.has_more === true) {
165
+ parts.push(`还有下一页;沿用原命令和主体/筛选参数,传 --pageNum ${p.next_page} --pageSize ${p.effective_page_size}`);
166
+ }
167
+ else if (p.has_more === false) {
168
+ parts.push("已到末页");
169
+ }
170
+ else {
171
+ parts.push("尚不能确定是否还有下一页");
172
+ }
173
+ lines.push(`> 分页:${parts.join(";")}。`);
174
+ }
175
+ else if (obj._truncated === true) {
176
+ lines.push("> 当前返回不是全量;请根据工具分页参数继续查询。");
177
+ }
132
178
  if (obj._warnings && isPlainObject(obj._warnings) && Object.keys(obj._warnings).length > 0) {
133
179
  lines.push(`> ⚠️ **警告**:`);
134
180
  for (const [path, msg] of Object.entries(obj._warnings)) {
@@ -137,6 +183,55 @@ function renderMeta(obj) {
137
183
  }
138
184
  return lines.length > 0 ? lines.join("\n") + "\n" : "";
139
185
  }
186
+ function renderRiskOverview(obj) {
187
+ const parts = [];
188
+ const meta = obj._riskOverview;
189
+ if (isPlainObject(meta)) {
190
+ // State and counts come from Shared Core; never derive them from local rows.
191
+ parts.push(`> 数据状态:${formatPrimitive(meta.dataStatus)};摘要及诊断行数:${formatPrimitive(meta.displayRowCount)};风险事件总数:未确认。`);
192
+ parts.push(`> 计数口径:${formatPrimitive(meta.countMeaning)}`);
193
+ if (meta.reportedTotal !== undefined) {
194
+ parts.push(`> 上游报告总数:${formatPrimitive(meta.reportedTotal)}(保留原始口径,未经事件去重核实)。`);
195
+ }
196
+ if (Array.isArray(meta.reportedCounts) && meta.reportedCounts.length > 0) {
197
+ parts.push(renderItemsTable(meta.reportedCounts, "上游计数来源"));
198
+ }
199
+ if (Array.isArray(meta.issues)) {
200
+ parts.push(...meta.issues.map((issue) => `> 数据说明:${escapeCell(formatPrimitive(issue))}`));
201
+ }
202
+ }
203
+ const sections = [
204
+ ["toolRisks", "可用专项工具获取详情"],
205
+ ["detailRisks", "需通过风险 ID 获取详情"],
206
+ ["relationRiskNotes", "周边风险提示"],
207
+ ["warningRisks", "预警提醒"],
208
+ ["unclassifiedRisks", "未分类风险及诊断信息"],
209
+ ];
210
+ const sectionKeys = new Set(sections.map(([key]) => key));
211
+ const otherFields = Object.fromEntries(Object.entries(obj).filter(([key]) => !META_KEYS.has(key) && !sectionKeys.has(key) && key !== "name" && !(key === "total" && isPlainObject(meta))));
212
+ if (Object.keys(otherFields).length > 0)
213
+ parts.push(renderTopFields(otherFields));
214
+ for (const [key, title] of sections) {
215
+ const rows = obj[key];
216
+ if (!Array.isArray(rows) || rows.length === 0)
217
+ continue;
218
+ if (key === "detailRisks") {
219
+ parts.push("使用 get_risk_detail,将 riskId 作为 id 参数、同一行 type 作为 type 参数。");
220
+ }
221
+ parts.push(renderItemsTable(rows.map((row) => {
222
+ if (!isPlainObject(row) || !Array.isArray(row.examples))
223
+ return row;
224
+ const { examples, ...rest } = row;
225
+ return rest;
226
+ }), title));
227
+ for (const row of rows) {
228
+ if (isPlainObject(row) && Array.isArray(row.examples) && row.examples.length > 0) {
229
+ parts.push(renderItemsTable(row.examples, `${title} / ${formatPrimitive(row.riskType)}:关联主体与原始风险示例`));
230
+ }
231
+ }
232
+ }
233
+ return parts.join("\n\n");
234
+ }
140
235
  function renderNestedObjects(obj) {
141
236
  const lines = [];
142
237
  for (const [k, v] of Object.entries(obj)) {
@@ -181,6 +276,15 @@ export function jsonToMarkdown(value, toolName) {
181
276
  return "```\n" + formatPrimitive(value) + "\n```\n";
182
277
  }
183
278
  const obj = value;
279
+ const historical = splitHistoricalPersonnel(obj, toolName);
280
+ if (historical)
281
+ return jsonToMarkdown(historical.rest, toolName) + "\n" + historical.markdown;
282
+ const changes = splitChangeRecords(obj, toolName);
283
+ if (changes) {
284
+ // The recursive call receives a copy without the extracted list. Keep raw
285
+ // code blocks verbatim: the generic newline compaction must not touch them.
286
+ return jsonToMarkdown(changes.rest, toolName) + "\n" + renderChangeRecords(changes.records, changes.title);
287
+ }
184
288
  const parts = [];
185
289
  // 标题
186
290
  if (toolName) {
@@ -193,6 +297,10 @@ export function jsonToMarkdown(value, toolName) {
193
297
  const meta = renderMeta(obj);
194
298
  if (meta)
195
299
  parts.push(meta);
300
+ if (toolName === "get_risk_overview" || isPlainObject(obj._riskOverview)) {
301
+ parts.push(renderRiskOverview(obj));
302
+ return parts.join("\n").replace(/\n{3,}/g, "\n\n");
303
+ }
196
304
  // 顶层字段
197
305
  const topFields = renderTopFields(obj);
198
306
  parts.push(topFields);
@@ -0,0 +1,74 @@
1
+ // Presentation only: all personnel extraction and statuses come from Shared Core.
2
+ import { changeRecordsForDisplay } from "./history.js";
3
+ const object = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
4
+ const text = (v) => typeof v === "string" ? v : JSON.stringify(v) ?? "null";
5
+ const cell = (v) => (v === undefined || v === "" ? "-" : text(v)).replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
6
+ function block(value) {
7
+ const raw = text(value);
8
+ const longest = Math.max(0, ...(raw.match(/`+/g) ?? []).map((s) => s.length));
9
+ const fence = "`".repeat(Math.max(3, longest + 1));
10
+ return `${fence}text\n${raw}\n${fence}\n`;
11
+ }
12
+ export function splitChangeRecords(obj, toolName) {
13
+ if (toolName === "get_change_records" && Array.isArray(obj.items)) {
14
+ const rest = { ...obj };
15
+ delete rest.items;
16
+ return { rest, records: obj.items, title: "变更记录" };
17
+ }
18
+ if (toolName === "get_historical_registration" && object(obj.sources) && object(obj.sources.cb) && Array.isArray(obj.sources.cb.changeList)) {
19
+ const cb = { ...obj.sources.cb };
20
+ delete cb.changeList;
21
+ return { rest: { ...obj, sources: { ...obj.sources, cb } }, records: obj.sources.cb.changeList, title: "工商信息 / 变更记录" };
22
+ }
23
+ return undefined;
24
+ }
25
+ function renderSide(title, side) {
26
+ if (!object(side))
27
+ return "";
28
+ const out = [`#### ${title}\n`, `解析状态:${cell(side.status)}\n`];
29
+ const people = Array.isArray(side.people) ? side.people : [];
30
+ if (people.length) {
31
+ out.push("| 姓名 | 职务原值 | 法定代表人标志原值 | 显式变更标记 | 其他登记字段 |", "|---|---|---|---|---|");
32
+ for (const person of people) {
33
+ if (!object(person)) {
34
+ out.push(block(person));
35
+ continue;
36
+ }
37
+ const fields = Array.isArray(person.fields) ? person.fields : [];
38
+ const other = fields.filter((field) => !object(field) || !["姓名", "职务", "法定代表人标志"].includes(String(field.key)));
39
+ out.push(`| ${[person.name, person.position, person.legalRepresentativeFlag, person.marker, other].map(cell).join(" | ")} |`);
40
+ }
41
+ out.push("");
42
+ }
43
+ if (Array.isArray(side.warnings))
44
+ for (const warning of side.warnings)
45
+ out.push(`> 解析说明:${cell(warning)}\n`);
46
+ if (Array.isArray(side.unparsedFragments))
47
+ for (const fragment of side.unparsedFragments)
48
+ out.push("未解析片段:\n", block(fragment));
49
+ return out.join("\n");
50
+ }
51
+ export function renderChangeRecords(records, title) {
52
+ const out = [`## ${title}(${records.length} 条)\n`, "> 本次返回记录按变更日期倒序展示;同日维持来源顺序,日期未知置后,不代表全量历程。\n"];
53
+ changeRecordsForDisplay(records).forEach((value, index) => {
54
+ if (!object(value)) {
55
+ out.push(block(value));
56
+ return;
57
+ }
58
+ out.push(`### 第 ${index + 1} 条 · ${cell(value.changeTime)} · ${cell(value.changeItem)}\n`);
59
+ const parsed = object(value._parsed) && value._parsed.version === 1 ? value._parsed : undefined;
60
+ for (const [key, v] of Object.entries(value)) {
61
+ if (["changeTime", "changeItem", "contentBefore", "contentAfter"].includes(key) || (key === "_parsed" && parsed))
62
+ continue;
63
+ out.push(`${key === "createTime" ? "记录创建日期" : key}:${cell(v)}\n`);
64
+ }
65
+ if (parsed) {
66
+ out.push(`> 解析版本:${cell(parsed.version)};原文完整性:${cell(parsed.sourceCompleteness)}(未确认)。解析结果不是完整任职快照,标志原值未转换。\n`);
67
+ out.push(renderSide("变更前人员", parsed.before), renderSide("变更后人员", parsed.after));
68
+ }
69
+ for (const [key, name] of [["contentBefore", "变更前原文"], ["contentAfter", "变更后原文"]]) {
70
+ out.push(`#### ${name}\n`, key in value ? block(value[key]) : "_(原字段未返回)_\n");
71
+ }
72
+ });
73
+ return out.join("\n");
74
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tyc-cli",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "description": "天眼查 AI CLI — 162 个业务语义聚合工具 · 默认 Shared Core 调用 · 兼容 MCP transport",
5
5
  "main": "dist/index.js",
6
6
  "bin": {