@dsh-ft/dsh-fft-settings 0.0.0-stage → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +392 -2
- package/cordis.patch.yml +18 -0
- package/docs/compatibility-dsh-0.1.6-alpha.1.md +58 -0
- package/docs/extension-service-design.md +351 -0
- package/docs/extension-service-sequences.html +545 -0
- package/docs/ftshare-settings-er.html +139 -0
- package/docs/ftshare-settings-er.svg +113 -0
- package/docs/ftshare-settings-results-er.html +148 -0
- package/docs/ftshare-settings-results-er.svg +122 -0
- package/docs/kline-integration.md +258 -0
- package/lib/agent.d.ts +24 -0
- package/lib/agent.js +26 -0
- package/lib/brand.d.ts +53 -0
- package/lib/brand.js +50 -0
- package/lib/client.d.ts +8 -0
- package/lib/client.js +822 -0
- package/lib/client.js.map +1 -0
- package/lib/contract.d.ts +14 -0
- package/lib/contract.js +14 -0
- package/lib/core/client-transport.d.ts +15 -0
- package/lib/core/client-transport.js +31 -0
- package/lib/core/contract.d.ts +62 -0
- package/lib/core/contract.js +28 -0
- package/lib/core/module-registry.d.ts +54 -0
- package/lib/core/module-registry.js +68 -0
- package/lib/core/rpc-core.d.ts +69 -0
- package/lib/core/rpc-core.js +88 -0
- package/lib/core/shell.d.ts +59 -0
- package/lib/core/shell.js +28 -0
- package/lib/core/styles.d.ts +31 -0
- package/lib/core/styles.js +43 -0
- package/lib/core/transport.d.ts +19 -0
- package/lib/core/transport.js +80 -0
- package/lib/default-preset.d.ts +52 -0
- package/lib/default-preset.js +121 -0
- package/lib/features/api-key/card.d.ts +24 -0
- package/lib/features/api-key/card.js +39 -0
- package/lib/features/api-key/client-api.d.ts +4 -0
- package/lib/features/api-key/client-api.js +11 -0
- package/lib/features/api-key/contract.d.ts +69 -0
- package/lib/features/api-key/contract.js +24 -0
- package/lib/features/api-key/host.d.ts +17 -0
- package/lib/features/api-key/host.js +32 -0
- package/lib/features/api-key/index.d.ts +13 -0
- package/lib/features/api-key/index.js +12 -0
- package/lib/features/api-key/logging.d.ts +15 -0
- package/lib/features/api-key/logging.js +1 -0
- package/lib/features/api-key/model.d.ts +24 -0
- package/lib/features/api-key/model.js +105 -0
- package/lib/features/api-key/probe.d.ts +11 -0
- package/lib/features/api-key/probe.js +142 -0
- package/lib/features/api-key/rpc.d.ts +4 -0
- package/lib/features/api-key/rpc.js +17 -0
- package/lib/features/api-key/service.d.ts +21 -0
- package/lib/features/api-key/service.js +181 -0
- package/lib/features/api-key/styles.d.ts +9 -0
- package/lib/features/api-key/styles.js +24 -0
- package/lib/features/logs/archive.d.ts +87 -0
- package/lib/features/logs/archive.js +156 -0
- package/lib/features/logs/card.d.ts +22 -0
- package/lib/features/logs/card.js +107 -0
- package/lib/features/logs/client-api.d.ts +4 -0
- package/lib/features/logs/client-api.js +11 -0
- package/lib/features/logs/collector.d.ts +91 -0
- package/lib/features/logs/collector.js +258 -0
- package/lib/features/logs/config.d.ts +70 -0
- package/lib/features/logs/config.js +97 -0
- package/lib/features/logs/contract.d.ts +130 -0
- package/lib/features/logs/contract.js +67 -0
- package/lib/features/logs/export.d.ts +92 -0
- package/lib/features/logs/export.js +212 -0
- package/lib/features/logs/format.d.ts +42 -0
- package/lib/features/logs/format.js +172 -0
- package/lib/features/logs/home.d.ts +18 -0
- package/lib/features/logs/home.js +39 -0
- package/lib/features/logs/host.d.ts +19 -0
- package/lib/features/logs/host.js +139 -0
- package/lib/features/logs/index.d.ts +12 -0
- package/lib/features/logs/index.js +12 -0
- package/lib/features/logs/rpc.d.ts +6 -0
- package/lib/features/logs/rpc.js +27 -0
- package/lib/features/logs/styles.d.ts +9 -0
- package/lib/features/logs/styles.js +13 -0
- package/lib/guidance.d.ts +41 -0
- package/lib/guidance.js +88 -0
- package/lib/index.d.ts +67 -0
- package/lib/index.js +50 -0
- package/lib/rpc.d.ts +10 -0
- package/lib/rpc.js +10 -0
- package/package.json +86 -4
- package/src/agent.ts +31 -0
- package/src/brand.ts +79 -0
- package/src/client.tsx +21 -0
- package/src/contract.ts +14 -0
- package/src/core/client-transport.ts +37 -0
- package/src/core/contract.ts +57 -0
- package/src/core/module-registry.ts +102 -0
- package/src/core/rpc-core.ts +107 -0
- package/src/core/shell.tsx +81 -0
- package/src/core/styles.ts +43 -0
- package/src/core/transport.ts +82 -0
- package/src/default-preset.ts +151 -0
- package/src/features/api-key/card.tsx +77 -0
- package/src/features/api-key/client-api.ts +12 -0
- package/src/features/api-key/contract.ts +63 -0
- package/src/features/api-key/host.ts +36 -0
- package/src/features/api-key/index.ts +13 -0
- package/src/features/api-key/logging.ts +15 -0
- package/src/features/api-key/model.ts +63 -0
- package/src/features/api-key/probe.ts +116 -0
- package/src/features/api-key/rpc.ts +18 -0
- package/src/features/api-key/service.ts +157 -0
- package/src/features/api-key/styles.ts +24 -0
- package/src/features/logs/archive.ts +230 -0
- package/src/features/logs/card.tsx +169 -0
- package/src/features/logs/client-api.ts +20 -0
- package/src/features/logs/collector.ts +313 -0
- package/src/features/logs/config.ts +115 -0
- package/src/features/logs/contract.ts +149 -0
- package/src/features/logs/export.ts +274 -0
- package/src/features/logs/format.ts +188 -0
- package/src/features/logs/home.ts +43 -0
- package/src/features/logs/host.ts +153 -0
- package/src/features/logs/index.ts +12 -0
- package/src/features/logs/rpc.ts +29 -0
- package/src/features/logs/styles.ts +13 -0
- package/src/guidance.ts +114 -0
- package/src/index.ts +59 -0
- package/src/rpc.ts +10 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 诊断包(P2,规格 §9 / §12 决策 3):把单文件导出与额外来源原文打成一个 zip。
|
|
3
|
+
*
|
|
4
|
+
* 归档结构与封顶语义:
|
|
5
|
+
*
|
|
6
|
+
* - `manifest.json`:生成时间、插件版本、运行时、`$DSH_HOME` 展示名、上限、`truncated`、
|
|
7
|
+
* 各来源统计(label/path/lines/bytes/firstAt/lastAt/missing/truncated)、四条覆盖盲区;
|
|
8
|
+
* - `ft-runtime.log`:与单文件导出**完全相同**的文本(复用 `loadLogExport` + `renderLogExport`);
|
|
9
|
+
* - `sources/<n>-<basename>`:配置的额外来源文件原文,编号稳定、按路径去重;缺失来源只进 manifest。
|
|
10
|
+
*
|
|
11
|
+
* 封顶沿用规格 §9:单来源 2 MiB、合计 8 MiB、目录最多 20 个 `*.log`;归档以**未压缩字节**
|
|
12
|
+
* 计入同一个 8 MiB 预算(`ft-runtime.log` 先算,剩余额度再分给 `sources/*`),超限截断并在
|
|
13
|
+
* manifest 标 `truncated: true`。
|
|
14
|
+
*
|
|
15
|
+
* 本文件是 Host 半边(Node 文件系统 + `fflate`),不得被浏览器半边引用:卡片只用
|
|
16
|
+
* `./contract.js` 里的 `LOGS_ARCHIVE_ROUTE` 与 `<a download>`。
|
|
17
|
+
*/
|
|
18
|
+
import type { SettingsRoute } from '../../core/rpc-core.js';
|
|
19
|
+
import { LOGS_ARCHIVE_ROUTE, type LogsSourceSummary } from './contract.js';
|
|
20
|
+
import { type ExportOptions } from './export.js';
|
|
21
|
+
/**
|
|
22
|
+
* 与 core 的 `SettingsRoute` 同形,只把 `path` 从「那一条 POST 路由」放宽到本 feature 的精确路径。
|
|
23
|
+
* core 因此不需要认识 `/logs.zip`(架构守护测试要求 core 无 feature 词汇)。
|
|
24
|
+
*/
|
|
25
|
+
export interface LogsArchiveRoute extends Omit<SettingsRoute, 'path'> {
|
|
26
|
+
readonly path: typeof LOGS_ARCHIVE_ROUTE;
|
|
27
|
+
}
|
|
28
|
+
/** 载体侧的注册面:与 core 的 `HostFetch` 同形,收本 feature 自己的路由。 */
|
|
29
|
+
export interface LogsArchiveRouteRegistry {
|
|
30
|
+
register(route: LogsArchiveRoute): () => Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
/** 归档选项:来源定义与 header 元信息(`filePath`/`sources`/`homeDisplay`/`environment`)。 */
|
|
33
|
+
export interface LogsArchiveOptions extends Omit<ExportOptions, 'pluginVersion' | 'now'> {
|
|
34
|
+
/** 插件版本;允许传 promise,`readPluginVersion()` 的结果可以直接塞进来。 */
|
|
35
|
+
readonly pluginVersion: string | Promise<string>;
|
|
36
|
+
/** 读文件前刷盘(采集器的 10 秒合并窗口);省略时直接读当前文件。 */
|
|
37
|
+
readonly flush?: () => Promise<void>;
|
|
38
|
+
readonly now?: () => number;
|
|
39
|
+
}
|
|
40
|
+
/** manifest 里的一个来源:规格 §9 的统计字段,外加它落在归档里的条目名。 */
|
|
41
|
+
export interface LogsArchiveManifestSource extends LogsSourceSummary {
|
|
42
|
+
/** 归档内的条目路径;缺失或未纳入时不存在。 */
|
|
43
|
+
readonly entry?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface LogsArchiveEntry {
|
|
46
|
+
readonly name: string;
|
|
47
|
+
readonly bytes: number;
|
|
48
|
+
}
|
|
49
|
+
export interface LogsArchiveManifest {
|
|
50
|
+
readonly generatedAt: string;
|
|
51
|
+
readonly pluginVersion: string;
|
|
52
|
+
readonly runtime: string;
|
|
53
|
+
/** `$DSH_HOME` 的展示名(`~/.dsh` 或 `$DSH_HOME`),不写绝对路径。 */
|
|
54
|
+
readonly home: string;
|
|
55
|
+
readonly limits: {
|
|
56
|
+
readonly sourceBytes: number;
|
|
57
|
+
readonly archiveBytes: number;
|
|
58
|
+
readonly maxSourceFiles: number;
|
|
59
|
+
};
|
|
60
|
+
/** 未压缩字节是否触到任一上限。 */
|
|
61
|
+
readonly truncated: boolean;
|
|
62
|
+
/** 归档内除 `manifest.json` 自身之外的条目(它无法记录自己的字节数)。 */
|
|
63
|
+
readonly entries: readonly LogsArchiveEntry[];
|
|
64
|
+
readonly sources: readonly LogsArchiveManifestSource[];
|
|
65
|
+
readonly blindSpots: readonly string[];
|
|
66
|
+
}
|
|
67
|
+
export interface LogsArchive {
|
|
68
|
+
readonly fileName: string;
|
|
69
|
+
readonly bytes: Uint8Array;
|
|
70
|
+
readonly truncated: boolean;
|
|
71
|
+
readonly manifest: LogsArchiveManifest;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 生成诊断包:单文件导出文本 + manifest + 额外来源原文。
|
|
75
|
+
* @param options - 来源定义与 header 元信息。
|
|
76
|
+
* @returns 归档字节、文件名、manifest 与整体 `truncated`。
|
|
77
|
+
*/
|
|
78
|
+
export declare function buildLogArchive(options: LogsArchiveOptions): Promise<LogsArchive>;
|
|
79
|
+
/**
|
|
80
|
+
* 挂到 `/api` 载体上的精确 GET 路由。
|
|
81
|
+
*
|
|
82
|
+
* 不读任何请求参数、不重复鉴权(`/api` 前缀的信任与认证由载体负责);每次请求现算归档,
|
|
83
|
+
* 因此刷盘与版本解析都在这里发生。
|
|
84
|
+
* @param options - 归档选项。
|
|
85
|
+
* @returns 交给 `connection.fetch.register` 的路由定义。
|
|
86
|
+
*/
|
|
87
|
+
export declare function createLogsArchiveRoute(options: LogsArchiveOptions): LogsArchiveRoute;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 诊断包(P2,规格 §9 / §12 决策 3):把单文件导出与额外来源原文打成一个 zip。
|
|
3
|
+
*
|
|
4
|
+
* 归档结构与封顶语义:
|
|
5
|
+
*
|
|
6
|
+
* - `manifest.json`:生成时间、插件版本、运行时、`$DSH_HOME` 展示名、上限、`truncated`、
|
|
7
|
+
* 各来源统计(label/path/lines/bytes/firstAt/lastAt/missing/truncated)、四条覆盖盲区;
|
|
8
|
+
* - `ft-runtime.log`:与单文件导出**完全相同**的文本(复用 `loadLogExport` + `renderLogExport`);
|
|
9
|
+
* - `sources/<n>-<basename>`:配置的额外来源文件原文,编号稳定、按路径去重;缺失来源只进 manifest。
|
|
10
|
+
*
|
|
11
|
+
* 封顶沿用规格 §9:单来源 2 MiB、合计 8 MiB、目录最多 20 个 `*.log`;归档以**未压缩字节**
|
|
12
|
+
* 计入同一个 8 MiB 预算(`ft-runtime.log` 先算,剩余额度再分给 `sources/*`),超限截断并在
|
|
13
|
+
* manifest 标 `truncated: true`。
|
|
14
|
+
*
|
|
15
|
+
* 本文件是 Host 半边(Node 文件系统 + `fflate`),不得被浏览器半边引用:卡片只用
|
|
16
|
+
* `./contract.js` 里的 `LOGS_ARCHIVE_ROUTE` 与 `<a download>`。
|
|
17
|
+
*/
|
|
18
|
+
import { zipSync } from 'fflate';
|
|
19
|
+
import { LOGS_ARCHIVE_LOG, LOGS_ARCHIVE_MANIFEST, LOGS_ARCHIVE_ROUTE, LOGS_ARCHIVE_SOURCE_DIR, LOGS_EXPORT_MAX_BYTES, LOGS_MAX_SOURCE_FILES, LOGS_SOURCE_MAX_BYTES, } from './contract.js';
|
|
20
|
+
import { LOG_BLIND_SPOTS, loadLogExport, renderLogExport } from './export.js';
|
|
21
|
+
import { byteLength, formatArchiveFileName, formatLogTimestamp } from './format.js';
|
|
22
|
+
import { basename } from 'node:path';
|
|
23
|
+
const encoder = new TextEncoder();
|
|
24
|
+
const decoder = new TextDecoder('utf-8', { fatal: false });
|
|
25
|
+
/** 按 UTF-8 字节上限截断原文副本,并丢掉最后一个不完整行。 */
|
|
26
|
+
function truncateToBytes(text, maxBytes) {
|
|
27
|
+
let slice = decoder.decode(encoder.encode(text).subarray(0, Math.max(0, maxBytes)));
|
|
28
|
+
// 截断点可能落在多字节字符中间,去掉 UTF-8 替换字符。
|
|
29
|
+
while (slice.endsWith('\uFFFD'))
|
|
30
|
+
slice = slice.slice(0, -1);
|
|
31
|
+
const lastBreak = slice.lastIndexOf('\n');
|
|
32
|
+
return lastBreak === -1 ? slice : slice.slice(0, lastBreak + 1);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 生成诊断包:单文件导出文本 + manifest + 额外来源原文。
|
|
36
|
+
* @param options - 来源定义与 header 元信息。
|
|
37
|
+
* @returns 归档字节、文件名、manifest 与整体 `truncated`。
|
|
38
|
+
*/
|
|
39
|
+
export async function buildLogArchive(options) {
|
|
40
|
+
await options.flush?.();
|
|
41
|
+
const pluginVersion = await options.pluginVersion;
|
|
42
|
+
const generatedAt = (options.now ?? Date.now)();
|
|
43
|
+
const { loaded, truncated: logTruncated } = await loadLogExport(options);
|
|
44
|
+
const rendered = renderLogExport({
|
|
45
|
+
loaded,
|
|
46
|
+
truncated: logTruncated,
|
|
47
|
+
generatedAt,
|
|
48
|
+
homeDisplay: options.homeDisplay,
|
|
49
|
+
pluginVersion,
|
|
50
|
+
environment: options.environment,
|
|
51
|
+
});
|
|
52
|
+
let truncated = logTruncated;
|
|
53
|
+
// 未压缩字节共用同一个 8 MiB 预算:ft-runtime.log 先占,剩下的才分给原文副本。
|
|
54
|
+
let remaining = LOGS_EXPORT_MAX_BYTES - rendered.bytes;
|
|
55
|
+
const content = [[LOGS_ARCHIVE_LOG, encoder.encode(rendered.text)]];
|
|
56
|
+
const entries = [{ name: LOGS_ARCHIVE_LOG, bytes: content[0][1].byteLength }];
|
|
57
|
+
const sources = [];
|
|
58
|
+
const seen = new Set();
|
|
59
|
+
let index = 0;
|
|
60
|
+
for (const [position, item] of loaded.entries()) {
|
|
61
|
+
const summary = item.summary;
|
|
62
|
+
// 第一条固定是主日志:它的内容已经完整进了 ft-runtime.log。
|
|
63
|
+
if (position === 0) {
|
|
64
|
+
sources.push({ ...summary, entry: LOGS_ARCHIVE_LOG });
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (summary.missing === true) {
|
|
68
|
+
sources.push({ ...summary });
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
let entry;
|
|
72
|
+
let sourceTruncated = summary.truncated;
|
|
73
|
+
if (seen.has(summary.path)) {
|
|
74
|
+
// 同一路径被配置两次:归档内只留一份原文,编号仍然稳定。
|
|
75
|
+
}
|
|
76
|
+
else if (remaining <= 0) {
|
|
77
|
+
truncated = true;
|
|
78
|
+
sourceTruncated = true;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
const budget = Math.min(LOGS_SOURCE_MAX_BYTES, remaining);
|
|
82
|
+
let text = item.content;
|
|
83
|
+
if (byteLength(text) > budget) {
|
|
84
|
+
text = truncateToBytes(text, budget);
|
|
85
|
+
truncated = true;
|
|
86
|
+
sourceTruncated = true;
|
|
87
|
+
// 一份来源被切短即表示预算耗尽:剩下的零头不再喂给后续来源,条目数保持确定。
|
|
88
|
+
remaining = 0;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
remaining -= byteLength(text);
|
|
92
|
+
}
|
|
93
|
+
if (text !== '' || item.content === '') {
|
|
94
|
+
index += 1;
|
|
95
|
+
entry = `${LOGS_ARCHIVE_SOURCE_DIR}${index}-${basename(summary.path)}`;
|
|
96
|
+
seen.add(summary.path);
|
|
97
|
+
const bytes = encoder.encode(text);
|
|
98
|
+
content.push([entry, bytes]);
|
|
99
|
+
entries.push({ name: entry, bytes: bytes.byteLength });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
sources.push({ ...summary, truncated: sourceTruncated, ...(entry === undefined ? {} : { entry }) });
|
|
103
|
+
}
|
|
104
|
+
const manifest = {
|
|
105
|
+
generatedAt: formatLogTimestamp(generatedAt),
|
|
106
|
+
pluginVersion: `@dsh-ft/dsh-fft-settings ${pluginVersion}`,
|
|
107
|
+
runtime: options.environment ?? process.version,
|
|
108
|
+
home: options.homeDisplay,
|
|
109
|
+
limits: {
|
|
110
|
+
sourceBytes: LOGS_SOURCE_MAX_BYTES,
|
|
111
|
+
archiveBytes: LOGS_EXPORT_MAX_BYTES,
|
|
112
|
+
maxSourceFiles: LOGS_MAX_SOURCE_FILES,
|
|
113
|
+
},
|
|
114
|
+
truncated,
|
|
115
|
+
entries,
|
|
116
|
+
sources,
|
|
117
|
+
blindSpots: [...LOG_BLIND_SPOTS],
|
|
118
|
+
};
|
|
119
|
+
// manifest 排第一,其余条目按 ft-runtime.log → sources/<n>-… 的顺序写入(对象键保持插入序)。
|
|
120
|
+
const files = {
|
|
121
|
+
[LOGS_ARCHIVE_MANIFEST]: encoder.encode(JSON.stringify(manifest, null, 2)),
|
|
122
|
+
};
|
|
123
|
+
for (const [name, bytes] of content)
|
|
124
|
+
files[name] = bytes;
|
|
125
|
+
return { fileName: formatArchiveFileName(generatedAt), bytes: zipSync(files), truncated, manifest };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* 挂到 `/api` 载体上的精确 GET 路由。
|
|
129
|
+
*
|
|
130
|
+
* 不读任何请求参数、不重复鉴权(`/api` 前缀的信任与认证由载体负责);每次请求现算归档,
|
|
131
|
+
* 因此刷盘与版本解析都在这里发生。
|
|
132
|
+
* @param options - 归档选项。
|
|
133
|
+
* @returns 交给 `connection.fetch.register` 的路由定义。
|
|
134
|
+
*/
|
|
135
|
+
export function createLogsArchiveRoute(options) {
|
|
136
|
+
return {
|
|
137
|
+
path: LOGS_ARCHIVE_ROUTE,
|
|
138
|
+
methods: ['GET'],
|
|
139
|
+
// GET 不读体;buffered 只是与载体约定「本路由不消费请求体」。
|
|
140
|
+
requestBody: 'buffered',
|
|
141
|
+
fetch: async () => {
|
|
142
|
+
const archive = await buildLogArchive(options);
|
|
143
|
+
// 复制成独立 ArrayBuffer 承载的视图:`Response` 的 BodyInit 不接受 ArrayBufferLike 视图。
|
|
144
|
+
const body = new Uint8Array(archive.bytes);
|
|
145
|
+
return new Response(body, {
|
|
146
|
+
status: 200,
|
|
147
|
+
headers: {
|
|
148
|
+
'content-type': 'application/zip',
|
|
149
|
+
'content-disposition': `attachment; filename="${archive.fileName}"`,
|
|
150
|
+
'cache-control': 'no-store',
|
|
151
|
+
'content-length': String(body.byteLength),
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type LogsPublicApi } from './contract.js';
|
|
3
|
+
import type { FtshareSettingsShellContext } from '../../core/shell.js';
|
|
4
|
+
/**
|
|
5
|
+
* 快照缺失时的占位文案。
|
|
6
|
+
*
|
|
7
|
+
* 失败后不能继续写「读取中…」:那会把一次已经结束的失败说成正在进行的读取,掩盖真正的
|
|
8
|
+
* 原因(例如宿主插件没有这个接口,重试永远不会成功)。
|
|
9
|
+
* @param failed - 本次读取是否已经失败。
|
|
10
|
+
* @returns 失败提示,或仍在读取的提示。
|
|
11
|
+
*/
|
|
12
|
+
export declare function statsPlaceholder(failed: boolean): string;
|
|
13
|
+
/**
|
|
14
|
+
* Contribute the built-in runtime-log card through the same slot a third-party
|
|
15
|
+
* card uses. Nothing about it is privileged: same declaration, same options.
|
|
16
|
+
* @param ctx - client context carrying the renderer's slots service.
|
|
17
|
+
* @returns idempotent disposer for the card registration.
|
|
18
|
+
*/
|
|
19
|
+
export declare function registerLogsCard(ctx: FtshareSettingsShellContext): () => void;
|
|
20
|
+
export declare function LogsSection({ api }: {
|
|
21
|
+
readonly api: LogsPublicApi;
|
|
22
|
+
}): ReactNode;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
3
|
+
import { LOGS_ARCHIVE_ROUTE, LOGS_CARD_ID, LOGS_CARD_ORDER, LOGS_DEFAULT_PREVIEW_LINES, LOGS_MAX_PREVIEW_LINES, } from './contract.js';
|
|
4
|
+
import { createLogsClient } from './client-api.js';
|
|
5
|
+
import { createSettingsTransport } from '../../core/client-transport.js';
|
|
6
|
+
import { FTSHARE_SETTINGS_CARD_SLOT } from '../../core/contract.js';
|
|
7
|
+
import { LOGS_CARD_CSS } from './styles.js';
|
|
8
|
+
/** 预览行数候选:上限由 Host 侧校验兜底。 */
|
|
9
|
+
const PREVIEW_CHOICES = [50, LOGS_DEFAULT_PREVIEW_LINES, LOGS_MAX_PREVIEW_LINES];
|
|
10
|
+
/**
|
|
11
|
+
* 快照缺失时的占位文案。
|
|
12
|
+
*
|
|
13
|
+
* 失败后不能继续写「读取中…」:那会把一次已经结束的失败说成正在进行的读取,掩盖真正的
|
|
14
|
+
* 原因(例如宿主插件没有这个接口,重试永远不会成功)。
|
|
15
|
+
* @param failed - 本次读取是否已经失败。
|
|
16
|
+
* @returns 失败提示,或仍在读取的提示。
|
|
17
|
+
*/
|
|
18
|
+
export function statsPlaceholder(failed) {
|
|
19
|
+
return failed ? '读取失败' : '读取中…';
|
|
20
|
+
}
|
|
21
|
+
function displayBytes(bytes) {
|
|
22
|
+
if (bytes < 1024)
|
|
23
|
+
return `${bytes} B`;
|
|
24
|
+
if (bytes < 1024 * 1024)
|
|
25
|
+
return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
26
|
+
return `${(bytes / 1024 / 1024).toFixed(2)} MiB`;
|
|
27
|
+
}
|
|
28
|
+
/** 把文本包成 Blob,用 a[download] 触发下载(Web 与桌面同一套)。 */
|
|
29
|
+
function downloadText(fileName, text) {
|
|
30
|
+
const url = URL.createObjectURL(new Blob([text], { type: 'text/plain;charset=utf-8' }));
|
|
31
|
+
const anchor = document.createElement('a');
|
|
32
|
+
anchor.href = url;
|
|
33
|
+
anchor.download = fileName;
|
|
34
|
+
anchor.rel = 'noopener';
|
|
35
|
+
document.body.appendChild(anchor);
|
|
36
|
+
anchor.click();
|
|
37
|
+
anchor.remove();
|
|
38
|
+
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Contribute the built-in runtime-log card through the same slot a third-party
|
|
42
|
+
* card uses. Nothing about it is privileged: same declaration, same options.
|
|
43
|
+
* @param ctx - client context carrying the renderer's slots service.
|
|
44
|
+
* @returns idempotent disposer for the card registration.
|
|
45
|
+
*/
|
|
46
|
+
export function registerLogsCard(ctx) {
|
|
47
|
+
return ctx.slots.inject(FTSHARE_SETTINGS_CARD_SLOT, () => ctx.slots.register({ name: FTSHARE_SETTINGS_CARD_SLOT, id: LOGS_CARD_ID, order: LOGS_CARD_ORDER }, createLogsCard()));
|
|
48
|
+
}
|
|
49
|
+
/** Bind the card's adapter; the component only reads through it. */
|
|
50
|
+
function createLogsCard() {
|
|
51
|
+
const api = createLogsClient(createSettingsTransport());
|
|
52
|
+
return () => _jsx(LogsSection, { api: api });
|
|
53
|
+
}
|
|
54
|
+
export function LogsSection({ api }) {
|
|
55
|
+
const [stats, setStats] = useState();
|
|
56
|
+
const [preview, setPreview] = useState();
|
|
57
|
+
const [lines, setLines] = useState(LOGS_DEFAULT_PREVIEW_LINES);
|
|
58
|
+
const [busy, setBusy] = useState(false);
|
|
59
|
+
const [message, setMessage] = useState('');
|
|
60
|
+
const [error, setError] = useState('');
|
|
61
|
+
/** 读取已失败且还没有任何快照时,每个字段都必须说出来,而不是假装还在读。 */
|
|
62
|
+
const placeholder = statsPlaceholder(error !== '');
|
|
63
|
+
const refresh = useCallback(async () => {
|
|
64
|
+
setBusy(true);
|
|
65
|
+
setError('');
|
|
66
|
+
try {
|
|
67
|
+
setStats(await api.stats());
|
|
68
|
+
setPreview(await api.preview(lines));
|
|
69
|
+
}
|
|
70
|
+
catch (cause) {
|
|
71
|
+
setError(cause instanceof Error ? cause.message : '读取日志状态失败');
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
setBusy(false);
|
|
75
|
+
}
|
|
76
|
+
}, [api, lines]);
|
|
77
|
+
useEffect(() => { void refresh(); }, [refresh]);
|
|
78
|
+
const doExport = async () => {
|
|
79
|
+
setBusy(true);
|
|
80
|
+
setError('');
|
|
81
|
+
setMessage('');
|
|
82
|
+
try {
|
|
83
|
+
const result = await api.export();
|
|
84
|
+
downloadText(result.fileName, result.text);
|
|
85
|
+
setMessage(`已导出 ${result.fileName}(${displayBytes(result.bytes)}${result.truncated ? ',已达体积上限并截断' : ''})`);
|
|
86
|
+
}
|
|
87
|
+
catch (cause) {
|
|
88
|
+
setError(cause instanceof Error ? cause.message : '导出日志失败');
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
setBusy(false);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
return _jsxs(_Fragment, { children: [_jsx("style", { children: LOGS_CARD_CSS }), _jsxs("section", { className: "ftshare-settings", "aria-label": "FT \u8FD0\u884C\u65E5\u5FD7", "aria-busy": busy, children: [_jsx("header", { children: _jsxs("div", { children: [_jsx("span", { className: "ftshare-eyebrow", children: "FTSHARE / LOGS" }), _jsx("h2", { children: "\u8FD0\u884C\u65E5\u5FD7" })] }) }), _jsxs("div", { className: "ftshare-card", children: [_jsx("h3", { children: "\u91C7\u96C6\u72B6\u6001" }), _jsxs("dl", { children: [_jsxs("div", { className: "ftshare-result", children: [_jsx("dt", { children: "\u65E5\u5FD7\u6587\u4EF6" }), _jsx("dd", { children: stats?.file ?? placeholder })] }), _jsxs("div", { className: "ftshare-result", children: [_jsx("dt", { children: "\u8BB0\u5F55 / \u65F6\u95F4\u8303\u56F4" }), _jsxs("dd", { children: [stats === undefined ? placeholder : `${stats.records} 条`, stats?.firstAt !== undefined && `(${stats.firstAt} ~ ${stats.lastAt})`] })] }), _jsxs("div", { className: "ftshare-result", children: [_jsx("dt", { children: "\u6587\u4EF6\u5927\u5C0F / \u4E0A\u9650" }), _jsx("dd", { children: stats === undefined ? placeholder : `${displayBytes(stats.fileBytes)} / ${displayBytes(stats.maxBytes)}` })] }), _jsxs("div", { className: "ftshare-result", children: [_jsx("dt", { children: "\u4E22\u5F03 / \u8F6E\u8F6C / \u5199\u5931\u8D25" }), _jsx("dd", { children: stats === undefined ? placeholder : `${stats.dropped} / ${stats.rotations} / ${stats.writeFailures}` })] }), _jsxs("div", { className: "ftshare-result", children: [_jsx("dt", { children: "\u65E5\u5FD7\u7EA7\u522B" }), _jsxs("dd", { children: [stats?.level ?? placeholder, " "] })] }), _jsxs("div", { className: "ftshare-result", children: [_jsx("dt", { children: "\u7EDF\u8BA1" }), _jsx("dd", { children: stats === undefined
|
|
95
|
+
? placeholder
|
|
96
|
+
: Object.keys(stats.byName).length === 0
|
|
97
|
+
? '暂无'
|
|
98
|
+
: Object.entries(stats.byName).map(([name, count]) => `${name}=${count}`).join('、') })] }), _jsxs("div", { className: "ftshare-result", children: [_jsx("dt", { children: "\u989D\u5916\u6765\u6E90" }), _jsx("dd", { children: stats === undefined
|
|
99
|
+
? placeholder
|
|
100
|
+
: stats.sources.length === 0
|
|
101
|
+
? '暂无'
|
|
102
|
+
: stats.sources.map(source => `${source.path}(${source.kind}${source.bytes === undefined ? '' : ` ${displayBytes(source.bytes)}`})`).join(';') })] })] }), _jsxs("div", { className: "ftshare-actions", children: [_jsx("button", { className: "primary", type: "button", onClick: () => void doExport(), disabled: busy, children: "\u5BFC\u51FA\u65E5\u5FD7" }), _jsx("a", { className: "ftshare-download", href: LOGS_ARCHIVE_ROUTE, download: "", children: "\u5BFC\u51FA\u8BCA\u65AD\u5305" }), _jsx("button", { type: "button", onClick: () => void refresh(), disabled: busy, children: "\u5237\u65B0" }), _jsx("label", { htmlFor: "ftshare-log-lines", children: "\u9884\u89C8\u884C\u6570" }), _jsx("select", { id: "ftshare-log-lines", value: lines, onChange: e => setLines(Number(e.target.value)), disabled: busy, children: PREVIEW_CHOICES.map(choice => _jsx("option", { value: choice, children: choice }, choice)) })] }), busy && _jsx("p", { role: "status", children: "\u6B63\u5728\u5904\u7406\uFF0C\u8BF7\u7A0D\u5019\u2026" }), message && _jsx("p", { role: "status", children: message }), error && _jsx("p", { className: "ftshare-error", role: "alert", children: error })] }), _jsxs("div", { className: "ftshare-card", children: [_jsxs("h3", { children: ["\u6700\u8FD1\u65E5\u5FD7", preview?.truncated === true ? '(已截断)' : ''] }), preview === undefined
|
|
103
|
+
? _jsx("p", { children: error === '' ? '正在读取…' : '日志预览读取失败,见上方提示。' })
|
|
104
|
+
: preview.lines.length === 0
|
|
105
|
+
? _jsx("p", { children: "\u65E0" })
|
|
106
|
+
: _jsx("pre", { className: "ftshare-log-preview", children: preview.lines.join('\n') })] })] })] });
|
|
107
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { LOGS_DEFAULT_PREVIEW_LINES, } from './contract.js';
|
|
2
|
+
import { callSettingsRpc } from '../../core/rpc-core.js';
|
|
3
|
+
/** 浏览器适配器:只读,且不暴露本地路径以外的任何 Host 能力(刷盘与释放句柄)。 */
|
|
4
|
+
export function createLogsClient(transport) {
|
|
5
|
+
const call = (method, payload = {}) => callSettingsRpc(transport, 'logs', method, payload);
|
|
6
|
+
return {
|
|
7
|
+
stats: () => call('stats'),
|
|
8
|
+
preview: (lines = LOGS_DEFAULT_PREVIEW_LINES) => call('preview', { lines }),
|
|
9
|
+
export: () => call('export'),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FT 运行日志采集器(Host 侧,不依赖 cordis,便于单测)。
|
|
3
|
+
*
|
|
4
|
+
* 职责:接收 cordis exporter 的记录 → 节流合并 → 脱敏 → 追加 `$DSH_HOME/logs/ft/current.log`,
|
|
5
|
+
* 超限轮转、保留 `keep` 份;写失败 / 轮转 / 丢弃一律自报 stderr,绝不抛回业务插件。
|
|
6
|
+
*/
|
|
7
|
+
import { type LogLevelName } from './contract.js';
|
|
8
|
+
/** 采集器接受的最小记录形状;cordis 的 `Message` 结构上满足它。 */
|
|
9
|
+
export interface CollectorRecord {
|
|
10
|
+
readonly ts: number;
|
|
11
|
+
readonly type: LogLevelName;
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly args: readonly unknown[];
|
|
14
|
+
}
|
|
15
|
+
export interface CollectorOptions {
|
|
16
|
+
/** 落盘目录,通常是 `$DSH_HOME/logs/ft`。 */
|
|
17
|
+
readonly directory: string;
|
|
18
|
+
readonly level: LogLevelName;
|
|
19
|
+
readonly maxBytes: number;
|
|
20
|
+
readonly keep: number;
|
|
21
|
+
/** 已知密钥(如当前 FTShare Key)的延时提供者;取不到时按空处理。 */
|
|
22
|
+
readonly secrets?: () => Promise<readonly string[]>;
|
|
23
|
+
readonly fileName?: string;
|
|
24
|
+
/** 注入时钟,便于测试。 */
|
|
25
|
+
readonly now?: () => number;
|
|
26
|
+
/** 注入定时器;返回取消函数。默认 `setTimeout().unref()`,不阻止进程退出。 */
|
|
27
|
+
readonly schedule?: (run: () => void, delayMs: number) => () => void;
|
|
28
|
+
/** 自报通道;默认写 stderr。日志系统不能静默失败。 */
|
|
29
|
+
readonly selfReport?: (message: string) => void;
|
|
30
|
+
}
|
|
31
|
+
export interface CollectorSnapshot {
|
|
32
|
+
readonly filePath: string;
|
|
33
|
+
readonly fileBytes: number;
|
|
34
|
+
readonly records: number;
|
|
35
|
+
readonly dropped: number;
|
|
36
|
+
readonly rotations: number;
|
|
37
|
+
readonly writeFailures: number;
|
|
38
|
+
readonly firstTs?: number;
|
|
39
|
+
readonly lastTs?: number;
|
|
40
|
+
readonly byName: Readonly<Record<string, number>>;
|
|
41
|
+
}
|
|
42
|
+
/** 常驻采集器;生命周期由调用方(cordis `ctx.effect`)负责关闭。 */
|
|
43
|
+
export declare class FtLogCollector {
|
|
44
|
+
readonly filePath: string;
|
|
45
|
+
readonly threshold: number;
|
|
46
|
+
private readonly directory;
|
|
47
|
+
private readonly options;
|
|
48
|
+
private readonly now;
|
|
49
|
+
private readonly schedule;
|
|
50
|
+
private readonly report;
|
|
51
|
+
private readonly pending;
|
|
52
|
+
private readonly byName;
|
|
53
|
+
private chain;
|
|
54
|
+
private ready;
|
|
55
|
+
private cancelTimer;
|
|
56
|
+
private timerDeadline;
|
|
57
|
+
private bytes;
|
|
58
|
+
private records;
|
|
59
|
+
private dropped;
|
|
60
|
+
private rotations;
|
|
61
|
+
private writeFailures;
|
|
62
|
+
private firstTs;
|
|
63
|
+
private lastTs;
|
|
64
|
+
private cachedSecrets;
|
|
65
|
+
private secretsAt;
|
|
66
|
+
private closed;
|
|
67
|
+
constructor(options: CollectorOptions);
|
|
68
|
+
/** 接收一条 cordis 记录:同 key 在 10 秒窗口内合并为一行并累计重复次数。 */
|
|
69
|
+
append(record: CollectorRecord): void;
|
|
70
|
+
/** 注册时回填 cordis 内置缓冲里剩余的 `error`/`info`(`warn`/`debug` 已丢,不可恢复)。 */
|
|
71
|
+
backfill(records: readonly CollectorRecord[]): Promise<void>;
|
|
72
|
+
/** 把队列全部落盘(导出前调用),等待写入链结束。 */
|
|
73
|
+
flush(): Promise<void>;
|
|
74
|
+
/** 关闭句柄与队列;之后 `append` 变为空操作。 */
|
|
75
|
+
dispose(): Promise<void>;
|
|
76
|
+
stats(): CollectorSnapshot;
|
|
77
|
+
/** 当前已知密钥(带 TTL 缓存,避免每次写盘都解析凭据)。 */
|
|
78
|
+
private currentSecrets;
|
|
79
|
+
private armTimer;
|
|
80
|
+
private cancel;
|
|
81
|
+
/** 把窗口已过期的待写条目交给写入链(保持插入顺序)。 */
|
|
82
|
+
private sweep;
|
|
83
|
+
private dropOldest;
|
|
84
|
+
private enqueue;
|
|
85
|
+
private write;
|
|
86
|
+
private rotate;
|
|
87
|
+
/** 同一毫秒内可能连续轮转两次(小 maxBytes + 突发),文件名必须唯一,否则会互相覆盖。 */
|
|
88
|
+
private uniqueRotationPath;
|
|
89
|
+
/** 保留策略按修改时间(而不是文件名)排序:同一毫秒连转时的 `-N` 后缀不影响新旧判断。 */
|
|
90
|
+
private prune;
|
|
91
|
+
}
|