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/CHANGELOG.md +9 -0
- package/README.md +16 -10
- package/dist/catalog.json +332 -170
- package/dist/commands/capabilities.js +1 -2
- package/dist/commands/category.js +2 -3
- package/dist/commands/init.js +35 -11
- package/dist/commands/login.js +185 -81
- package/dist/commands/output.js +2 -4
- package/dist/config.js +1 -2
- package/dist/index.js +13 -4
- package/dist/oauth.js +129 -25
- package/dist/oauthPending.js +3 -2
- package/dist/registry.js +1 -1
- package/dist/utils/history.js +68 -0
- package/dist/utils/jsonToMarkdown.js +134 -26
- package/dist/utils/personnelChanges.js +74 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
270
|
+
close();
|
|
245
271
|
});
|
|
246
272
|
});
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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");
|
package/dist/oauthPending.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// ~/.tyc/oauth_pending.json 读写
|
|
2
2
|
//
|
|
3
|
-
// 非阻塞 OAuth
|
|
4
|
-
//
|
|
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
|
-
|
|
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)
|
|
45
|
+
return JSON.stringify(arr);
|
|
53
46
|
}
|
|
54
47
|
function formatObjectInline(obj) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
+
}
|