dsh-thesis 2.0.2 → 2.1.1
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 +5 -4
- package/docs/config.md +12 -0
- package/docs/design.md +95 -2
- package/docs/release.md +94 -85
- package/docs/tools.md +8 -5
- package/docs/troubleshooting.md +18 -0
- package/docs/usage.md +4 -1
- package/lib/config.d.ts +13 -0
- package/lib/config.js +8 -1
- package/lib/figure/actions.d.ts +83 -0
- package/lib/figure/actions.js +439 -0
- package/lib/figure/index.d.ts +24 -0
- package/lib/figure/index.js +40 -0
- package/lib/figure/lib/check.d.ts +83 -0
- package/lib/figure/lib/check.js +214 -0
- package/lib/figure/lib/drawio.d.ts +123 -0
- package/lib/figure/lib/drawio.js +156 -0
- package/lib/figure/lib/export.d.ts +120 -0
- package/lib/figure/lib/export.js +308 -0
- package/lib/figure/lib/layout.d.ts +78 -0
- package/lib/figure/lib/layout.js +496 -0
- package/lib/figure/lib/spec.d.ts +114 -0
- package/lib/figure/lib/spec.js +290 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +5 -1
- package/lib/paper/lib/check.d.ts +5 -0
- package/lib/paper/lib/check.js +21 -0
- package/lib/paper/lib/docx.d.ts +26 -0
- package/lib/paper/lib/docx.js +190 -27
- package/lib/paper/lib/image.d.ts +19 -0
- package/lib/paper/lib/image.js +82 -0
- package/lib/paper/lib/layout.js +26 -1
- package/lib/paper/lib/lit-refs.d.ts +30 -0
- package/lib/paper/lib/lit-refs.js +394 -0
- package/lib/paper/lib/lit-verify.d.ts +79 -0
- package/lib/paper/lib/lit-verify.js +556 -0
- package/lib/paper/register.d.ts +1 -1
- package/lib/paper/register.js +37 -1
- package/lib/paper/tools/build.d.ts +4 -0
- package/lib/paper/tools/build.js +79 -4
- package/lib/paper/tools/lit-verify.d.ts +35 -0
- package/lib/paper/tools/lit-verify.js +193 -0
- package/lib/ppt/check.js +5 -0
- package/lib/ppt/design.d.ts +86 -0
- package/lib/ppt/design.js +218 -0
- package/package.json +4 -1
- package/skills/thesis-defense/SKILL.md +25 -1
- package/skills/thesis-eng-design/SKILL.md +40 -20
- package/skills/thesis-literature/SKILL.md +28 -5
- package/skills/thesis-opener/SKILL.md +1 -1
- package/skills/thesis-pipeline/SKILL.md +4 -3
- package/skills/thesis-writing/SKILL.md +4 -1
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* draw.io 命令行导出:探测桌面板、执行导出、把每次尝试写成可复查的说明。
|
|
3
|
+
*
|
|
4
|
+
* 形态与 `ppt/convert.ts` 一致(探测 → 执行 → 校验产物 → 如实报告),原因也一样:
|
|
5
|
+
* **退出码 0 不等于成功**。draw.io 桌面版在若干情况下会「正常退出但没写文件」
|
|
6
|
+
* (路径不存在、被安全软件拦住、Electron 起不来),所以导出之后必须再确认
|
|
7
|
+
* 文件存在且非空,否则学生会拿到一个空图却以为导出成功了。
|
|
8
|
+
*
|
|
9
|
+
* 探测刻意**不启动 GUI**:`drawio --version` 在桌面版上会拉起窗口,所以这里改为
|
|
10
|
+
* 「在 PATH 与常见安装目录里找可执行文件」,只做文件系统检查。
|
|
11
|
+
*
|
|
12
|
+
* 没有可用的 draw.io 也不算死路:`.drawio` 源文件已经落盘,学生可以在
|
|
13
|
+
* app.diagrams.net 或 VS Code 的 Draw.io 扩展里打开并导出 PNG,{@link drawioGuide}
|
|
14
|
+
* 给出可直接照做的步骤。
|
|
15
|
+
*
|
|
16
|
+
* @module dsh-thesis/figure/export
|
|
17
|
+
*/
|
|
18
|
+
/** 一次命令执行的记录(写进导出说明)。 */
|
|
19
|
+
export interface ExportCommandRecord {
|
|
20
|
+
readonly label: string;
|
|
21
|
+
readonly command: string;
|
|
22
|
+
readonly args: readonly string[];
|
|
23
|
+
readonly status: number | null;
|
|
24
|
+
readonly ok: boolean;
|
|
25
|
+
readonly stdout: string;
|
|
26
|
+
readonly stderr: string;
|
|
27
|
+
readonly elapsedMs: number;
|
|
28
|
+
readonly timedOut: boolean;
|
|
29
|
+
}
|
|
30
|
+
/** 可注入的执行器(默认 spawnSync;测试传替身)。 */
|
|
31
|
+
export interface SpawnOptions {
|
|
32
|
+
readonly cwd?: string;
|
|
33
|
+
readonly encoding?: string;
|
|
34
|
+
readonly timeout?: number;
|
|
35
|
+
readonly maxBuffer?: number;
|
|
36
|
+
}
|
|
37
|
+
export interface SpawnResult {
|
|
38
|
+
readonly status: number | null;
|
|
39
|
+
readonly stdout: string | null;
|
|
40
|
+
readonly stderr: string | null;
|
|
41
|
+
readonly error?: Error;
|
|
42
|
+
}
|
|
43
|
+
export type SpawnFn = (command: string, args: readonly string[], options: SpawnOptions) => SpawnResult;
|
|
44
|
+
/** 探测到的 draw.io 可执行文件。 */
|
|
45
|
+
export interface DrawioEngine {
|
|
46
|
+
readonly command: string;
|
|
47
|
+
readonly label: string;
|
|
48
|
+
readonly source: 'config' | 'path' | 'common';
|
|
49
|
+
}
|
|
50
|
+
/** 探测结果。 */
|
|
51
|
+
export interface DrawioDetection {
|
|
52
|
+
/** 找到的可执行文件;没找到时 `undefined`(不是错误,是「本机没装」)。 */
|
|
53
|
+
readonly engine?: DrawioEngine;
|
|
54
|
+
/** 探测过的候选路径(写给用户看,避免「为什么没找到」)。 */
|
|
55
|
+
readonly probes: readonly string[];
|
|
56
|
+
/** 探测失败的原因摘要。 */
|
|
57
|
+
readonly problems: readonly string[];
|
|
58
|
+
}
|
|
59
|
+
/** 单次导出超时(draw.io 冷启动 + 渲染,60 秒足够)。 */
|
|
60
|
+
export declare const EXPORT_TIMEOUT_MS = 60000;
|
|
61
|
+
/** 默认导出倍率:740 单位宽的图 → 2220px,排进 15cm 栏宽约 376dpi。 */
|
|
62
|
+
export declare const DEFAULT_SCALE = 3;
|
|
63
|
+
/**
|
|
64
|
+
* 探测本机的 draw.io 桌面版。
|
|
65
|
+
*
|
|
66
|
+
* 顺序:显式配置 → PATH → 常见安装目录。返回第一个真实存在的可执行文件;
|
|
67
|
+
* 全部不存在时 `engine` 为 undefined,`problems` 里说明「本机未发现 draw.io 桌面版」。
|
|
68
|
+
*/
|
|
69
|
+
export declare function detectDrawio(options?: {
|
|
70
|
+
readonly configured?: string;
|
|
71
|
+
readonly env?: Record<string, string | undefined>;
|
|
72
|
+
readonly platform?: string;
|
|
73
|
+
readonly exists?: (path: string) => boolean;
|
|
74
|
+
}): DrawioDetection;
|
|
75
|
+
/** 导出参数。 */
|
|
76
|
+
export interface ExportOptions {
|
|
77
|
+
readonly scale?: number;
|
|
78
|
+
readonly format?: 'png' | 'svg';
|
|
79
|
+
readonly timeoutMs?: number;
|
|
80
|
+
readonly spawn?: SpawnFn;
|
|
81
|
+
readonly exists?: (path: string) => boolean;
|
|
82
|
+
readonly size?: (path: string) => number | undefined;
|
|
83
|
+
readonly cwd?: string;
|
|
84
|
+
}
|
|
85
|
+
/** 一次导出的结果。 */
|
|
86
|
+
export interface ExportOutcome {
|
|
87
|
+
readonly ok: true;
|
|
88
|
+
readonly record: ExportCommandRecord;
|
|
89
|
+
readonly outPath: string;
|
|
90
|
+
readonly bytes: number | undefined;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 把 `.drawio` 导出成图片。
|
|
94
|
+
*
|
|
95
|
+
* 失败一律抛错,错误里带**完整命令**与退出码:学生会看到自己该在哪一步修什么,
|
|
96
|
+
* 而不是一句「导出失败」。
|
|
97
|
+
*/
|
|
98
|
+
export declare function exportDiagram(engine: DrawioEngine, drawioPath: string, outPath: string, options?: ExportOptions): ExportOutcome;
|
|
99
|
+
/** 导出说明的一项(一张图)。 */
|
|
100
|
+
export interface ExportNotesItem {
|
|
101
|
+
readonly figureRel: string;
|
|
102
|
+
readonly imageRel?: string;
|
|
103
|
+
readonly outcome?: ExportOutcome;
|
|
104
|
+
readonly error?: string;
|
|
105
|
+
}
|
|
106
|
+
/** 导出说明的输入(一次导出可能包含多张图)。 */
|
|
107
|
+
export interface ExportNotesInput {
|
|
108
|
+
readonly items: readonly ExportNotesItem[];
|
|
109
|
+
readonly detection: DrawioDetection;
|
|
110
|
+
readonly format: 'png' | 'svg';
|
|
111
|
+
readonly scale: number;
|
|
112
|
+
readonly now?: Date;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* 渲染导出说明(无论成功失败都落盘):探测了哪些路径、跑了什么命令、退出码与输出摘要。
|
|
116
|
+
* 这份文件是「图为什么没导出来」的唯一现场,避免学生只看到一句错误提示。
|
|
117
|
+
*/
|
|
118
|
+
export declare function renderExportNotes(input: ExportNotesInput): string;
|
|
119
|
+
/** 安装与手工导出的指引(工具 `action=guide` 与失败信息共用)。 */
|
|
120
|
+
export declare function drawioGuide(figureRel?: string): string;
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* draw.io 命令行导出:探测桌面板、执行导出、把每次尝试写成可复查的说明。
|
|
3
|
+
*
|
|
4
|
+
* 形态与 `ppt/convert.ts` 一致(探测 → 执行 → 校验产物 → 如实报告),原因也一样:
|
|
5
|
+
* **退出码 0 不等于成功**。draw.io 桌面版在若干情况下会「正常退出但没写文件」
|
|
6
|
+
* (路径不存在、被安全软件拦住、Electron 起不来),所以导出之后必须再确认
|
|
7
|
+
* 文件存在且非空,否则学生会拿到一个空图却以为导出成功了。
|
|
8
|
+
*
|
|
9
|
+
* 探测刻意**不启动 GUI**:`drawio --version` 在桌面版上会拉起窗口,所以这里改为
|
|
10
|
+
* 「在 PATH 与常见安装目录里找可执行文件」,只做文件系统检查。
|
|
11
|
+
*
|
|
12
|
+
* 没有可用的 draw.io 也不算死路:`.drawio` 源文件已经落盘,学生可以在
|
|
13
|
+
* app.diagrams.net 或 VS Code 的 Draw.io 扩展里打开并导出 PNG,{@link drawioGuide}
|
|
14
|
+
* 给出可直接照做的步骤。
|
|
15
|
+
*
|
|
16
|
+
* @module dsh-thesis/figure/export
|
|
17
|
+
*/
|
|
18
|
+
import { spawnSync } from 'node:child_process';
|
|
19
|
+
import { existsSync, statSync } from 'node:fs';
|
|
20
|
+
import * as nodePath from 'node:path';
|
|
21
|
+
/** 单次导出超时(draw.io 冷启动 + 渲染,60 秒足够)。 */
|
|
22
|
+
export const EXPORT_TIMEOUT_MS = 60_000;
|
|
23
|
+
/** 默认导出倍率:740 单位宽的图 → 2220px,排进 15cm 栏宽约 376dpi。 */
|
|
24
|
+
export const DEFAULT_SCALE = 3;
|
|
25
|
+
const EXCERPT = 600;
|
|
26
|
+
function defaultSpawn(command, args, options) {
|
|
27
|
+
return spawnSync(command, [...args], {
|
|
28
|
+
...(options.cwd !== undefined ? { cwd: options.cwd } : {}),
|
|
29
|
+
encoding: options.encoding ?? 'utf8',
|
|
30
|
+
...(options.timeout !== undefined ? { timeout: options.timeout } : {}),
|
|
31
|
+
maxBuffer: options.maxBuffer ?? 4 * 1024 * 1024,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function fsExists(path) {
|
|
35
|
+
return existsSync(path);
|
|
36
|
+
}
|
|
37
|
+
function fsSize(path) {
|
|
38
|
+
try {
|
|
39
|
+
return statSync(path).size;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function displayCommand(command, args) {
|
|
46
|
+
const quote = (value) => (/\s/.test(value) ? `"${value}"` : value);
|
|
47
|
+
return [command, ...args].map(quote).join(' ');
|
|
48
|
+
}
|
|
49
|
+
function excerpt(text, limit = EXCERPT) {
|
|
50
|
+
const trimmed = (text ?? '').trim();
|
|
51
|
+
return trimmed.length <= limit ? trimmed : `${trimmed.slice(0, limit)}\n…(已截断)`;
|
|
52
|
+
}
|
|
53
|
+
/** 各平台常见的 draw.io 安装位置(只列桌面版;不含需要联网的网页版)。 */
|
|
54
|
+
const COMMON_INSTALLS = {
|
|
55
|
+
win32: [
|
|
56
|
+
'C:\\Program Files\\draw.io\\draw.io.exe',
|
|
57
|
+
'C:\\Program Files (x86)\\draw.io\\draw.io.exe',
|
|
58
|
+
],
|
|
59
|
+
darwin: ['/Applications/draw.io.app/Contents/MacOS/draw.io'],
|
|
60
|
+
linux: ['/usr/bin/drawio', '/snap/bin/drawio', '/opt/drawio/drawio'],
|
|
61
|
+
};
|
|
62
|
+
/** 可执行文件名(Windows 需要扩展名)。 */
|
|
63
|
+
const EXECUTABLE_NAMES = {
|
|
64
|
+
win32: ['drawio.exe', 'draw.io.exe', 'drawio'],
|
|
65
|
+
darwin: ['drawio', 'draw.io'],
|
|
66
|
+
linux: ['drawio', 'draw.io'],
|
|
67
|
+
};
|
|
68
|
+
function namesFor(platform) {
|
|
69
|
+
return EXECUTABLE_NAMES[platform] ?? EXECUTABLE_NAMES.linux ?? ['drawio'];
|
|
70
|
+
}
|
|
71
|
+
function installsFor(platform) {
|
|
72
|
+
return COMMON_INSTALLS[platform] ?? COMMON_INSTALLS.linux ?? [];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 探测本机的 draw.io 桌面版。
|
|
76
|
+
*
|
|
77
|
+
* 顺序:显式配置 → PATH → 常见安装目录。返回第一个真实存在的可执行文件;
|
|
78
|
+
* 全部不存在时 `engine` 为 undefined,`problems` 里说明「本机未发现 draw.io 桌面版」。
|
|
79
|
+
*/
|
|
80
|
+
export function detectDrawio(options = {}) {
|
|
81
|
+
const exists = options.exists ?? fsExists;
|
|
82
|
+
const platform = options.platform ?? process.platform;
|
|
83
|
+
const env = options.env ?? process.env;
|
|
84
|
+
const probes = [];
|
|
85
|
+
const problems = [];
|
|
86
|
+
if (options.configured !== undefined && options.configured.trim() !== '') {
|
|
87
|
+
const configured = options.configured.trim();
|
|
88
|
+
probes.push(configured);
|
|
89
|
+
if (exists(configured)) {
|
|
90
|
+
return { engine: { command: configured, label: 'draw.io(配置指定)', source: 'config' }, probes, problems };
|
|
91
|
+
}
|
|
92
|
+
problems.push(`配置的 draw.io 路径不存在:${configured}`);
|
|
93
|
+
}
|
|
94
|
+
const pathValue = env.PATH ?? env.Path ?? '';
|
|
95
|
+
const pathEntries = pathValue.split(nodePath.delimiter).filter(entry => entry.trim() !== '');
|
|
96
|
+
for (const name of namesFor(platform)) {
|
|
97
|
+
for (const dir of pathEntries) {
|
|
98
|
+
const candidate = nodePath.join(dir, name);
|
|
99
|
+
probes.push(candidate);
|
|
100
|
+
if (exists(candidate)) {
|
|
101
|
+
return { engine: { command: candidate, label: `draw.io(PATH:${candidate})`, source: 'path' }, probes, problems };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const candidates = [...installsFor(platform)];
|
|
106
|
+
if (platform === 'win32') {
|
|
107
|
+
const localAppData = env.LOCALAPPDATA;
|
|
108
|
+
if (localAppData !== undefined && localAppData !== '') {
|
|
109
|
+
candidates.unshift(nodePath.join(localAppData, 'Programs', 'draw.io', 'draw.io.exe'));
|
|
110
|
+
}
|
|
111
|
+
const programFiles = env.ProgramFiles;
|
|
112
|
+
if (programFiles !== undefined && programFiles !== '') {
|
|
113
|
+
candidates.push(nodePath.join(programFiles, 'draw.io', 'draw.io.exe'));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for (const candidate of candidates) {
|
|
117
|
+
probes.push(candidate);
|
|
118
|
+
if (exists(candidate)) {
|
|
119
|
+
return { engine: { command: candidate, label: `draw.io(安装目录:${candidate})`, source: 'common' }, probes, problems };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
problems.push('本机未发现 draw.io 桌面版(PATH 与常见安装目录都没有 drawio 可执行文件)。');
|
|
123
|
+
return { probes, problems };
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* 把 `.drawio` 导出成图片。
|
|
127
|
+
*
|
|
128
|
+
* 失败一律抛错,错误里带**完整命令**与退出码:学生会看到自己该在哪一步修什么,
|
|
129
|
+
* 而不是一句「导出失败」。
|
|
130
|
+
*/
|
|
131
|
+
export function exportDiagram(engine, drawioPath, outPath, options = {}) {
|
|
132
|
+
const spawn = options.spawn ?? defaultSpawn;
|
|
133
|
+
const exists = options.exists ?? fsExists;
|
|
134
|
+
const size = options.size ?? fsSize;
|
|
135
|
+
const format = options.format ?? 'png';
|
|
136
|
+
const scale = options.scale ?? DEFAULT_SCALE;
|
|
137
|
+
const args = [
|
|
138
|
+
'--export',
|
|
139
|
+
'--format', format,
|
|
140
|
+
'--scale', String(scale),
|
|
141
|
+
'--border', '10',
|
|
142
|
+
'--output', outPath,
|
|
143
|
+
drawioPath,
|
|
144
|
+
];
|
|
145
|
+
const started = Date.now();
|
|
146
|
+
let result;
|
|
147
|
+
try {
|
|
148
|
+
result = spawn(engine.command, args, {
|
|
149
|
+
...(options.cwd !== undefined ? { cwd: options.cwd } : {}),
|
|
150
|
+
encoding: 'utf8',
|
|
151
|
+
timeout: options.timeoutMs ?? EXPORT_TIMEOUT_MS,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
throw new Error(`draw.io 导出未能启动:${String(error)}\n命令:${displayCommand(engine.command, args)}`);
|
|
156
|
+
}
|
|
157
|
+
const elapsedMs = Date.now() - started;
|
|
158
|
+
const stderr = result.stderr ?? '';
|
|
159
|
+
const timedOut = result.status === null && result.error !== undefined && /ETIMEDOUT|timed out/i.test(result.error.message);
|
|
160
|
+
const record = {
|
|
161
|
+
label: 'draw.io 导出',
|
|
162
|
+
command: engine.command,
|
|
163
|
+
args,
|
|
164
|
+
status: result.status,
|
|
165
|
+
ok: result.status === 0,
|
|
166
|
+
stdout: excerpt(result.stdout),
|
|
167
|
+
stderr: excerpt(stderr !== '' ? stderr : result.error?.message),
|
|
168
|
+
elapsedMs,
|
|
169
|
+
timedOut,
|
|
170
|
+
};
|
|
171
|
+
if (!record.ok) {
|
|
172
|
+
const why = record.timedOut ? '超时被终止' : `退出码 ${record.status ?? 'null'}`;
|
|
173
|
+
throw new Error(`draw.io 导出失败(${why})。\n命令:${displayCommand(record.command, record.args)}\n`
|
|
174
|
+
+ `${record.stderr === '' ? '' : `stderr:${record.stderr.split('\n')[0] ?? ''}\n`}`
|
|
175
|
+
+ '排查:确认 draw.io 能手工打开这个 .drawio;若装了桌面版但不在 PATH,请在配置里指定 figure.drawioPath。');
|
|
176
|
+
}
|
|
177
|
+
if (!exists(outPath)) {
|
|
178
|
+
throw new Error(`draw.io 退出码为 0,但没有写出 ${outPath}。\n命令:${displayCommand(record.command, record.args)}\n`
|
|
179
|
+
+ '这通常是路径不可写或被安全软件拦截;可先手工在 draw.io 里导出一次确认。');
|
|
180
|
+
}
|
|
181
|
+
const bytes = size(outPath);
|
|
182
|
+
if (bytes === 0) {
|
|
183
|
+
throw new Error(`draw.io 写出的 ${outPath} 是 0 字节(静默失败)。\n命令:${displayCommand(record.command, record.args)}`);
|
|
184
|
+
}
|
|
185
|
+
return { ok: true, record, outPath, bytes };
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* 渲染导出说明(无论成功失败都落盘):探测了哪些路径、跑了什么命令、退出码与输出摘要。
|
|
189
|
+
* 这份文件是「图为什么没导出来」的唯一现场,避免学生只看到一句错误提示。
|
|
190
|
+
*/
|
|
191
|
+
export function renderExportNotes(input) {
|
|
192
|
+
const now = input.now ?? new Date();
|
|
193
|
+
const lines = [];
|
|
194
|
+
lines.push('# 插图导出说明');
|
|
195
|
+
lines.push('');
|
|
196
|
+
lines.push(`> 由 thesis_figure 生成;时间:${now.toISOString()}`);
|
|
197
|
+
lines.push('> `.drawio` 是可编辑源文件(唯一真源),PNG/SVG 是导出件,供论文与幻灯使用。');
|
|
198
|
+
lines.push('');
|
|
199
|
+
lines.push('## 一、draw.io 探测结果');
|
|
200
|
+
lines.push('');
|
|
201
|
+
if (input.detection.engine !== undefined) {
|
|
202
|
+
lines.push(`- 可用:${input.detection.engine.label}`);
|
|
203
|
+
lines.push(`- 命令:\`${input.detection.engine.command}\``);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
lines.push('- 未发现 draw.io 桌面版。');
|
|
207
|
+
}
|
|
208
|
+
for (const problem of input.detection.problems)
|
|
209
|
+
lines.push(`- ${problem}`);
|
|
210
|
+
if (input.detection.probes.length > 0) {
|
|
211
|
+
lines.push('');
|
|
212
|
+
lines.push(`- 已检查的候选路径:${input.detection.probes.slice(0, 8).join('、')}${input.detection.probes.length > 8 ? ` 等 ${input.detection.probes.length} 个` : ''}`);
|
|
213
|
+
}
|
|
214
|
+
lines.push('');
|
|
215
|
+
lines.push('## 二、导出记录');
|
|
216
|
+
lines.push('');
|
|
217
|
+
lines.push(`格式:${input.format};倍率:${input.scale}×`);
|
|
218
|
+
lines.push('');
|
|
219
|
+
if (input.items.length === 0) {
|
|
220
|
+
lines.push('(本次没有需要导出的图)');
|
|
221
|
+
lines.push('');
|
|
222
|
+
}
|
|
223
|
+
for (const item of input.items) {
|
|
224
|
+
lines.push(`### ${item.figureRel}`);
|
|
225
|
+
lines.push('');
|
|
226
|
+
lines.push(`- 导出件:${item.imageRel === undefined ? '(未产出)' : `\`${item.imageRel}\``}`);
|
|
227
|
+
if (item.outcome !== undefined) {
|
|
228
|
+
const record = item.outcome.record;
|
|
229
|
+
lines.push(`- 命令:\`${displayCommand(record.command, record.args)}\``);
|
|
230
|
+
lines.push(`- 退出码:${record.status ?? 'null'};耗时:${record.elapsedMs} ms${record.timedOut ? '(超时)' : ''}`);
|
|
231
|
+
lines.push(`- 产物:\`${item.outcome.outPath}\`${item.outcome.bytes === undefined ? '' : `(${item.outcome.bytes} 字节)`}`);
|
|
232
|
+
if (record.stdout !== '') {
|
|
233
|
+
lines.push('- stdout 摘要:');
|
|
234
|
+
lines.push('');
|
|
235
|
+
lines.push('```text');
|
|
236
|
+
lines.push(record.stdout);
|
|
237
|
+
lines.push('```');
|
|
238
|
+
}
|
|
239
|
+
if (record.stderr !== '') {
|
|
240
|
+
lines.push('- stderr 摘要:');
|
|
241
|
+
lines.push('');
|
|
242
|
+
lines.push('```text');
|
|
243
|
+
lines.push(record.stderr);
|
|
244
|
+
lines.push('```');
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
else if (item.error !== undefined) {
|
|
248
|
+
lines.push('- 结果:**未产出图片**');
|
|
249
|
+
lines.push('');
|
|
250
|
+
lines.push('```text');
|
|
251
|
+
lines.push(excerpt(item.error, 2000));
|
|
252
|
+
lines.push('```');
|
|
253
|
+
}
|
|
254
|
+
lines.push('');
|
|
255
|
+
}
|
|
256
|
+
lines.push('## 三、没有 draw.io 桌面版时(三选一)');
|
|
257
|
+
lines.push('');
|
|
258
|
+
lines.push('1. **在线版**:打开 https://app.diagrams.net → 打开源文件 → 文件 → 导出为 → PNG(缩放 300%)。');
|
|
259
|
+
lines.push('2. **VS Code 扩展**:安装 `hediet.vscode-drawio`(Draw.io Integration),打开 .drawio 后按 `F1` → `Draw.io: Export`。');
|
|
260
|
+
lines.push('3. **装桌面版**(推荐长期使用):`winget install --id JGraph.Draw -e`;装好后重跑 `thesis_figure action=export`。');
|
|
261
|
+
lines.push('');
|
|
262
|
+
return lines.join('\n');
|
|
263
|
+
}
|
|
264
|
+
/** 安装与手工导出的指引(工具 `action=guide` 与失败信息共用)。 */
|
|
265
|
+
export function drawioGuide(figureRel) {
|
|
266
|
+
const target = figureRel ?? '06-论文/assets/图/xxx.drawio';
|
|
267
|
+
const lines = [];
|
|
268
|
+
lines.push('# draw.io 出图指引');
|
|
269
|
+
lines.push('');
|
|
270
|
+
lines.push('论文里的图用 **draw.io 源文件 + 高分辨率 PNG**:源文件可改、PNG 能进 Word,');
|
|
271
|
+
lines.push('两者都留在 `06-论文/assets/图/` 里,随论文一起 git 版本化。');
|
|
272
|
+
lines.push('');
|
|
273
|
+
lines.push('## 一、装桌面版(可选,装了就能自动导出)');
|
|
274
|
+
lines.push('');
|
|
275
|
+
lines.push('```powershell');
|
|
276
|
+
lines.push('# Windows(winget 自带,装完重新打开终端)');
|
|
277
|
+
lines.push('winget install --id JGraph.Draw -e');
|
|
278
|
+
lines.push('```');
|
|
279
|
+
lines.push('');
|
|
280
|
+
lines.push('```bash');
|
|
281
|
+
lines.push('# macOS');
|
|
282
|
+
lines.push('brew install --cask drawio');
|
|
283
|
+
lines.push('# Ubuntu/Debian');
|
|
284
|
+
lines.push('sudo snap install drawio');
|
|
285
|
+
lines.push('```');
|
|
286
|
+
lines.push('');
|
|
287
|
+
lines.push('## 二、导出命令(桌面版)');
|
|
288
|
+
lines.push('');
|
|
289
|
+
lines.push('```powershell');
|
|
290
|
+
lines.push(`drawio --export --format png --scale 3 --border 10 --output 06-论文/assets/图/xxx.png ${target}`);
|
|
291
|
+
lines.push('```');
|
|
292
|
+
lines.push('');
|
|
293
|
+
lines.push('导出后用 `thesis_figure action=check` 核对:源文件、图片、正文引用三者是否对齐。');
|
|
294
|
+
lines.push('');
|
|
295
|
+
lines.push('## 三、没有桌面版也能导出');
|
|
296
|
+
lines.push('');
|
|
297
|
+
lines.push('1. 在线版:https://app.diagrams.net → 打开 .drawio → 文件 → 导出为 → PNG(缩放 300%)。');
|
|
298
|
+
lines.push('2. VS Code:安装 `hediet.vscode-drawio`,打开 .drawio → `F1` → `Draw.io: Export` → PNG。');
|
|
299
|
+
lines.push('');
|
|
300
|
+
lines.push('## 四、让图不糙的四条硬规则');
|
|
301
|
+
lines.push('');
|
|
302
|
+
lines.push('1. **一张图只讲一件事**:总体架构图就只放层与模块,别把数据库字段也塞进去。');
|
|
303
|
+
lines.push('2. **模块名用名词、动词留给箭头**:方框写「任务服务」,箭头标「HTTP/JSON」。');
|
|
304
|
+
lines.push('3. **字号不小于 10pt**:导出后按论文栏宽(约 15cm)看,字太小就合并模块,别缩字号。');
|
|
305
|
+
lines.push('4. **图题在图下方、正文先引用**:图 4-1 系统总体架构图;正文写「如图 4-1 所示」。');
|
|
306
|
+
lines.push('');
|
|
307
|
+
return lines.join('\n');
|
|
308
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 版面算法:把规范化模型排成**确定的坐标**。
|
|
3
|
+
*
|
|
4
|
+
* 这里是「图不糙」的关键。四种图的版面规则都很朴素,但足以覆盖本科毕设的绝大多数插图,
|
|
5
|
+
* 而且每条规则都可解释、可测试:
|
|
6
|
+
*
|
|
7
|
+
* - 架构图:层是横向色带,模块在层内按行排布(每行最多 4 个),层与层之间留出箭头的通道;
|
|
8
|
+
* - 流程图:按「最长路径」分层(Kahn 拓扑序),起点在上、终点在下,同级节点横向均分;
|
|
9
|
+
* - E-R 图:实体是表格式图元(标题栏 + 字段行),两列摆放,关系线带基数标注;
|
|
10
|
+
* - 时序图:参与者等距排开,生命线用 draw.io 自带形状,消息按时间从上往下逐行。
|
|
11
|
+
*
|
|
12
|
+
* 所有尺寸都是常量(见 {@link LAYOUT}),没有随机、没有外部依赖,同一份规格永远得到
|
|
13
|
+
* 同一张图——这样它才能被测试,也才能在 diff 里被审阅。
|
|
14
|
+
*
|
|
15
|
+
* @module dsh-thesis/figure/layout
|
|
16
|
+
*/
|
|
17
|
+
import { type FigureCell } from './drawio.ts';
|
|
18
|
+
import type { FigureModel } from './spec.ts';
|
|
19
|
+
/** 版面常量(单位与 draw.io 一致;A4 纵向页宽 827)。 */
|
|
20
|
+
export declare const LAYOUT: {
|
|
21
|
+
readonly margin: 26;
|
|
22
|
+
/** 内容区宽度上限:A4 纵向、2.54cm 页边距下正文宽约 790,留出导出边距。 */
|
|
23
|
+
readonly contentWidth: 740;
|
|
24
|
+
readonly arch: {
|
|
25
|
+
readonly labelWidth: 96;
|
|
26
|
+
readonly moduleWidth: 140;
|
|
27
|
+
readonly moduleHeight: 50;
|
|
28
|
+
readonly gapX: 16;
|
|
29
|
+
readonly gapY: 16;
|
|
30
|
+
readonly bandPadding: 12;
|
|
31
|
+
readonly bandGap: 42;
|
|
32
|
+
};
|
|
33
|
+
readonly flow: {
|
|
34
|
+
readonly nodeWidth: 176;
|
|
35
|
+
readonly nodeHeight: 54;
|
|
36
|
+
readonly decisionWidth: 150;
|
|
37
|
+
readonly decisionHeight: 86;
|
|
38
|
+
readonly gapX: 36;
|
|
39
|
+
readonly gapY: 48;
|
|
40
|
+
};
|
|
41
|
+
readonly er: {
|
|
42
|
+
readonly columnGap: 96;
|
|
43
|
+
readonly rowGap: 34;
|
|
44
|
+
readonly titleHeight: 30;
|
|
45
|
+
readonly rowHeight: 22;
|
|
46
|
+
readonly bottomPadding: 6;
|
|
47
|
+
readonly minWidth: 156;
|
|
48
|
+
readonly maxWidth: 300;
|
|
49
|
+
};
|
|
50
|
+
readonly sequence: {
|
|
51
|
+
readonly spacing: 170;
|
|
52
|
+
readonly minSpacing: 118;
|
|
53
|
+
readonly boxWidth: 132;
|
|
54
|
+
readonly header: 68;
|
|
55
|
+
readonly messageGap: 46;
|
|
56
|
+
readonly tail: 24;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
/** 版面错误(如流程有环):与规格错误区分开,调用方都按「生成失败」处理。 */
|
|
60
|
+
export declare class FigureLayoutError extends Error {
|
|
61
|
+
readonly code = "INVALID_FIGURE_LAYOUT";
|
|
62
|
+
constructor(message: string);
|
|
63
|
+
}
|
|
64
|
+
/** 一张图的版面结果。 */
|
|
65
|
+
export interface FigureDrawing {
|
|
66
|
+
/** 全部图元(custom 图为空——它自带 XML)。 */
|
|
67
|
+
readonly cells: readonly FigureCell[];
|
|
68
|
+
/** 内容宽(导出裁剪后的宽度约等于它)。 */
|
|
69
|
+
readonly width: number;
|
|
70
|
+
/** 内容高。 */
|
|
71
|
+
readonly height: number;
|
|
72
|
+
/** 排版过程中的提示(不算错误,例如模块被自动换行)。 */
|
|
73
|
+
readonly notes: readonly string[];
|
|
74
|
+
}
|
|
75
|
+
/** 中文按整字宽、西文按半字宽估算文本像素宽(用于 ER 实体自适应宽度)。 */
|
|
76
|
+
export declare function displayWidth(text: string, fontSize?: number): number;
|
|
77
|
+
/** 按图种类构建版面。 */
|
|
78
|
+
export declare function buildDrawing(model: FigureModel): FigureDrawing;
|