@vdfor/confluence-sync 0.3.0 → 0.4.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/README.md +38 -26
- package/package.json +3 -2
- package/skills/confluence-sync.md +21 -25
- package/src/confluence.mjs +157 -93
- package/upload.mjs +42 -53
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @vdfor/confluence-sync
|
|
2
2
|
|
|
3
|
-
Confluence 文档同步 CLI 工具,支持将本地 Markdown 文件上传至 Confluence 并自动转换为 Confluence
|
|
3
|
+
Confluence 文档同步 CLI 工具,支持将本地 Markdown 文件上传至 Confluence 并自动转换为 Confluence 页面格式。支持 Mermaid 图表自动渲染为图片。
|
|
4
4
|
|
|
5
5
|
## 安装
|
|
6
6
|
|
|
@@ -20,12 +20,9 @@ yarn global add @vdfor/confluence-sync
|
|
|
20
20
|
### 方式1:环境变量
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
#
|
|
23
|
+
# Confluence 服务地址
|
|
24
24
|
export CONFLUENCE_BASE_URL="https://your-confluence-instance.com"
|
|
25
25
|
|
|
26
|
-
# 必填:Space Key
|
|
27
|
-
export CONFLUENCE_SPACE_KEY="YOUR_SPACE_KEY"
|
|
28
|
-
|
|
29
26
|
# 认证方式(二选一)
|
|
30
27
|
# 方式1(推荐):Personal Access Token
|
|
31
28
|
export CONFLUENCE_PAT="your-personal-access-token"
|
|
@@ -38,24 +35,23 @@ export CONFLUENCE_API_TOKEN="your-api-token"
|
|
|
38
35
|
### 方式2:CLI 参数(优先于环境变量)
|
|
39
36
|
|
|
40
37
|
```bash
|
|
41
|
-
confluence-sync upload --file test.md \
|
|
38
|
+
confluence-sync upload --file test.md --parent 123456 \
|
|
42
39
|
--base-url https://your-confluence-instance.com \
|
|
43
|
-
--space-key YOUR_SPACE_KEY \
|
|
44
40
|
--pat your-token
|
|
45
41
|
```
|
|
46
42
|
|
|
47
43
|
## 命令
|
|
48
44
|
|
|
49
|
-
### upload - 上传 Markdown
|
|
45
|
+
### upload - 上传 Markdown 为新页面(已存在同名页面则自动更新)
|
|
50
46
|
|
|
51
47
|
```bash
|
|
52
|
-
confluence-sync upload --file <path>
|
|
48
|
+
confluence-sync upload --file <path> --parent <页面ID> [--title <标题>] [--dry-run] [--no-mermaid-to-img]
|
|
53
49
|
```
|
|
54
50
|
|
|
55
51
|
### update - 更新已有页面
|
|
56
52
|
|
|
57
53
|
```bash
|
|
58
|
-
confluence-sync update --file <path>
|
|
54
|
+
confluence-sync update --file <path> --page-id <ID> [--minor-edit] [--no-mermaid-to-img]
|
|
59
55
|
```
|
|
60
56
|
|
|
61
57
|
### upload-file - 上传附件
|
|
@@ -64,10 +60,10 @@ confluence-sync update --file <path> (--page-id <ID> | --title <标题>) [--spac
|
|
|
64
60
|
confluence-sync upload-file --page-id <ID> --file <path> [--name <文件名>]
|
|
65
61
|
```
|
|
66
62
|
|
|
67
|
-
### list-pages -
|
|
63
|
+
### list-pages - 列出 parent 下的子页面
|
|
68
64
|
|
|
69
65
|
```bash
|
|
70
|
-
confluence-sync list-pages
|
|
66
|
+
confluence-sync list-pages --parent <页面ID>
|
|
71
67
|
```
|
|
72
68
|
|
|
73
69
|
### list-files - 列出页面附件
|
|
@@ -84,29 +80,44 @@ confluence-sync get-content --page-id <ID> [--markdown]
|
|
|
84
80
|
|
|
85
81
|
使用 `--markdown` 参数可将 Confluence 内容转换为 Markdown 格式输出。
|
|
86
82
|
|
|
83
|
+
## Mermaid 图表支持
|
|
84
|
+
|
|
85
|
+
Markdown 中的 ```` ```mermaid ```` 代码块会自动通过 [sebastianjs](https://github.com/creadri/sebastianjs) 渲染为 PNG 图片(本地离线渲染,无需外网、无需浏览器),上传后页面中显示为图片附件。
|
|
86
|
+
|
|
87
|
+
- 默认自动检测并转换,无 Mermaid 块时不影响正常流程
|
|
88
|
+
- Mermaid 渲染失败时保留原始代码块(降级处理,不阻塞上传)
|
|
89
|
+
- 使用 `--no-mermaid-to-img` 可禁用自动渲染
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# 自动处理 mermaid(默认开启)
|
|
93
|
+
confluence-sync upload --file README.md --parent 27263539
|
|
94
|
+
|
|
95
|
+
# 禁用 mermaid 渲染
|
|
96
|
+
confluence-sync upload --file README.md --parent 27263539 --no-mermaid-to-img
|
|
97
|
+
```
|
|
98
|
+
|
|
87
99
|
## 配置参数
|
|
88
100
|
|
|
89
101
|
| 参数 | 说明 |
|
|
90
102
|
|------|------|
|
|
91
|
-
| `--base-url` | Confluence
|
|
92
|
-
| `--
|
|
93
|
-
| `--
|
|
94
|
-
| `--
|
|
95
|
-
| `--api-token` | API Token 或密码 |
|
|
96
|
-
| `--space` | Space Key(覆盖默认值) |
|
|
103
|
+
| `--base-url` | Confluence 服务地址(覆盖环境变量) |
|
|
104
|
+
| `--pat` | Personal Access Token(覆盖环境变量) |
|
|
105
|
+
| `--username` | Confluence 登录名(覆盖环境变量) |
|
|
106
|
+
| `--api-token` | API Token 或密码(覆盖环境变量) |
|
|
97
107
|
| `--file` | 本地文件路径 |
|
|
98
|
-
| `--title` |
|
|
99
|
-
| `--
|
|
100
|
-
| `--
|
|
108
|
+
| `--title` | 页面标题(upload 时可选,默认取文件名) |
|
|
109
|
+
| `--parent` | 父页面 ID(数字,upload / list-pages 必填) |
|
|
110
|
+
| `--page-id` | 页面 ID(update / upload-file / list-files / get-content 必填) |
|
|
101
111
|
| `--dry-run` | 仅打印转换结果,不实际上传 |
|
|
102
112
|
| `--minor-edit` | 标记为小修改(不通知关注者) |
|
|
103
113
|
| `--markdown` | get-content 时输出为 Markdown 格式 |
|
|
114
|
+
| `--no-mermaid-to-img` | 禁用 Mermaid 图表自动渲染 |
|
|
104
115
|
|
|
105
116
|
## 示例
|
|
106
117
|
|
|
107
118
|
```bash
|
|
108
|
-
# 上传 Markdown
|
|
109
|
-
confluence-sync upload --file README.md --title "项目文档" --
|
|
119
|
+
# 上传 Markdown 文件(自动按 title 匹配 parent 下已有页面则更新)
|
|
120
|
+
confluence-sync upload --file README.md --title "项目文档" --parent 27263539
|
|
110
121
|
|
|
111
122
|
# 更新已有页面
|
|
112
123
|
confluence-sync update --file README.md --page-id 123456
|
|
@@ -114,8 +125,8 @@ confluence-sync update --file README.md --page-id 123456
|
|
|
114
125
|
# 上传附件
|
|
115
126
|
confluence-sync upload-file --page-id 123456 --file image.png
|
|
116
127
|
|
|
117
|
-
#
|
|
118
|
-
confluence-sync list-pages --
|
|
128
|
+
# 列出 parent 下的子页面
|
|
129
|
+
confluence-sync list-pages --parent 27263539
|
|
119
130
|
|
|
120
131
|
# 获取页面内容
|
|
121
132
|
confluence-sync get-content --page-id 123456
|
|
@@ -139,7 +150,8 @@ pnpm add -g @vdfor/confluence-sync
|
|
|
139
150
|
安装后,智能体可直接调用 `confluence-sync` 命令:
|
|
140
151
|
|
|
141
152
|
```bash
|
|
142
|
-
confluence-sync upload --file README.md --title "项目文档" --
|
|
153
|
+
confluence-sync upload --file README.md --title "项目文档" --parent 27263539
|
|
154
|
+
confluence-sync update --file README.md --page-id 123456
|
|
143
155
|
confluence-sync get-content --page-id 123456 --markdown
|
|
144
156
|
```
|
|
145
157
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vdfor/confluence-sync",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"author": "vdfor",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"axios": "^1.7.0",
|
|
31
31
|
"form-data": "^4.0.0",
|
|
32
|
-
"marked": "^15.0.0"
|
|
32
|
+
"marked": "^15.0.0",
|
|
33
|
+
"sebastianjs": "^0.4.0"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
35
36
|
"rimraf": "^6.1.3"
|
|
@@ -1,48 +1,43 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: "confluence-sync"
|
|
3
|
-
description: "Confluence 文档同步 CLI 工具,支持将本地 Markdown 文件上传至 Confluence 并自动转换为 Confluence
|
|
3
|
+
description: "Confluence 文档同步 CLI 工具,支持将本地 Markdown 文件上传至 Confluence 并自动转换为 Confluence 页面格式,支持 Mermaid 图表自动渲染。当用户需要上传、更新、获取 Confluence 页面内容或管理附件时调用。"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Confluence Sync
|
|
7
7
|
|
|
8
|
-
Confluence 文档同步 CLI 工具,支持将本地 Markdown 文件上传至 Confluence 并自动转换为 Confluence
|
|
8
|
+
Confluence 文档同步 CLI 工具,支持将本地 Markdown 文件上传至 Confluence 并自动转换为 Confluence 页面格式。支持 Mermaid 图表自动渲染为图片。
|
|
9
9
|
|
|
10
10
|
## 功能特性
|
|
11
11
|
|
|
12
|
-
- 上传 Markdown 文件为 Confluence
|
|
12
|
+
- 上传 Markdown 文件为 Confluence 页面(已存在同名页面则自动更新)
|
|
13
13
|
- 更新已有页面内容
|
|
14
14
|
- 上传文件作为页面附件
|
|
15
|
-
-
|
|
15
|
+
- 列出 parent 下的子页面
|
|
16
|
+
- 列出页面附件
|
|
16
17
|
- 获取页面内容(支持转换为 Markdown 格式)
|
|
18
|
+
- Mermaid 图表自动渲染为 PNG 图片
|
|
17
19
|
|
|
18
20
|
## 配置要求
|
|
19
21
|
|
|
20
22
|
使用前需要配置以下环境变量或 CLI 参数:
|
|
21
23
|
|
|
22
|
-
### 必填配置
|
|
23
|
-
|
|
24
24
|
- `CONFLUENCE_BASE_URL`: Confluence 服务地址
|
|
25
|
-
- `CONFLUENCE_SPACE_KEY`: Space Key
|
|
26
25
|
- 认证方式(二选一):
|
|
27
26
|
- `CONFLUENCE_PAT`: Personal Access Token(推荐)
|
|
28
27
|
- `CONFLUENCE_USERNAME` + `CONFLUENCE_API_TOKEN`: Basic Auth 认证
|
|
29
28
|
|
|
30
|
-
### 可选配置
|
|
31
|
-
|
|
32
|
-
- `--space`: Space Key(覆盖默认值)
|
|
33
|
-
|
|
34
29
|
## 命令列表
|
|
35
30
|
|
|
36
|
-
### upload - 上传 Markdown
|
|
31
|
+
### upload - 上传 Markdown 为新页面(已存在同名页面则自动更新)
|
|
37
32
|
|
|
38
33
|
```bash
|
|
39
|
-
confluence-sync upload --file <path>
|
|
34
|
+
confluence-sync upload --file <path> --parent <页面ID> [--title <标题>] [--dry-run] [--no-mermaid-to-img]
|
|
40
35
|
```
|
|
41
36
|
|
|
42
37
|
### update - 更新已有页面
|
|
43
38
|
|
|
44
39
|
```bash
|
|
45
|
-
confluence-sync update --file <path>
|
|
40
|
+
confluence-sync update --file <path> --page-id <ID> [--minor-edit] [--no-mermaid-to-img]
|
|
46
41
|
```
|
|
47
42
|
|
|
48
43
|
### upload-file - 上传附件
|
|
@@ -51,10 +46,10 @@ confluence-sync update --file <path> (--page-id <ID> | --title <标题>) [--spac
|
|
|
51
46
|
confluence-sync upload-file --page-id <ID> --file <path> [--name <文件名>]
|
|
52
47
|
```
|
|
53
48
|
|
|
54
|
-
### list-pages -
|
|
49
|
+
### list-pages - 列出 parent 下的子页面
|
|
55
50
|
|
|
56
51
|
```bash
|
|
57
|
-
confluence-sync list-pages
|
|
52
|
+
confluence-sync list-pages --parent <页面ID>
|
|
58
53
|
```
|
|
59
54
|
|
|
60
55
|
### list-files - 列出页面附件
|
|
@@ -71,25 +66,26 @@ confluence-sync get-content --page-id <ID> [--markdown]
|
|
|
71
66
|
|
|
72
67
|
使用 `--markdown` 参数可将 Confluence 内容转换为 Markdown 格式输出。
|
|
73
68
|
|
|
69
|
+
## Mermaid 支持
|
|
70
|
+
|
|
71
|
+
Markdown 中的 ```` ```mermaid ```` 代码块自动通过 sebastianjs 渲染为 PNG 图片(本地离线渲染,无需外网、无需浏览器),作为附件上传。默认开启,使用 `--no-mermaid-to-img` 可禁用。渲染失败时保留原始代码块,不阻塞上传。
|
|
72
|
+
|
|
74
73
|
## 使用示例
|
|
75
74
|
|
|
76
75
|
```bash
|
|
77
|
-
#
|
|
78
|
-
confluence-sync upload --file README.md --title "项目文档" --
|
|
76
|
+
# 上传(--parent 必填,已存在同名子页面则自动更新)
|
|
77
|
+
confluence-sync upload --file README.md --title "项目文档" --parent 27263539
|
|
79
78
|
|
|
80
|
-
#
|
|
79
|
+
# 更新(--page-id 必填)
|
|
81
80
|
confluence-sync update --file README.md --page-id 123456
|
|
82
81
|
|
|
82
|
+
# 列出 parent 下的子页面
|
|
83
|
+
confluence-sync list-pages --parent 27263539
|
|
84
|
+
|
|
83
85
|
# 上传附件
|
|
84
86
|
confluence-sync upload-file --page-id 123456 --file image.png
|
|
85
87
|
|
|
86
|
-
# 列出空间所有页面
|
|
87
|
-
confluence-sync list-pages --space MYSPACE
|
|
88
|
-
|
|
89
88
|
# 获取页面内容
|
|
90
|
-
confluence-sync get-content --page-id 123456
|
|
91
|
-
|
|
92
|
-
# 获取页面内容并转换为 Markdown
|
|
93
89
|
confluence-sync get-content --page-id 123456 --markdown
|
|
94
90
|
```
|
|
95
91
|
|
package/src/confluence.mjs
CHANGED
|
@@ -1,10 +1,75 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
2
|
import { marked } from 'marked';
|
|
3
3
|
import { extname } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { writeFile, unlink } from 'node:fs/promises';
|
|
6
|
+
|
|
7
|
+
// ── Mermaid 处理 ─────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
let sebastianRenderer = null;
|
|
10
|
+
async function getSebastianRenderer() {
|
|
11
|
+
if (!sebastianRenderer) {
|
|
12
|
+
const sebastian = await import('sebastianjs');
|
|
13
|
+
sebastianRenderer = { renderPng: sebastian.renderPng, dispose: sebastian.dispose };
|
|
14
|
+
}
|
|
15
|
+
return sebastianRenderer;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 检测并替换 markdown 中的 ```mermaid 代码块为图片占位符
|
|
20
|
+
* 使用 sebastianjs(本地、零浏览器、零外网依赖)渲染为 PNG
|
|
21
|
+
*
|
|
22
|
+
* @param {string} md - 原始 markdown
|
|
23
|
+
* @returns {{ md: string, images: { file: string, name: string }[], hasMermaid: boolean }}
|
|
24
|
+
*/
|
|
25
|
+
export async function preprocessMermaid(md) {
|
|
26
|
+
const mermaidRe = /```mermaid\s*\n([\s\S]*?)```/gi;
|
|
27
|
+
const matches = [...md.matchAll(mermaidRe)];
|
|
28
|
+
if (!matches.length) return { md, images: [], hasMermaid: false };
|
|
29
|
+
|
|
30
|
+
const { renderPng } = await getSebastianRenderer();
|
|
31
|
+
const images = [];
|
|
32
|
+
let result = md;
|
|
33
|
+
let offset = 0;
|
|
34
|
+
|
|
35
|
+
for (let i = 0; i < matches.length; i++) {
|
|
36
|
+
const block = matches[i];
|
|
37
|
+
const code = block[1];
|
|
38
|
+
const name = `mermaid-${i + 1}.png`;
|
|
39
|
+
const file = `${tmpdir()}/${name}`;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const t0 = Date.now();
|
|
43
|
+
const { data: pngBuffer } = await renderPng(code, { background: 'white' });
|
|
44
|
+
const t1 = Date.now();
|
|
45
|
+
await writeFile(file, pngBuffer);
|
|
46
|
+
images.push({ file, name });
|
|
47
|
+
|
|
48
|
+
const replacement = ``;
|
|
49
|
+
const start = block.index + offset;
|
|
50
|
+
const end = start + block[0].length;
|
|
51
|
+
result = result.slice(0, start) + replacement + result.slice(end);
|
|
52
|
+
offset += replacement.length - block[0].length;
|
|
53
|
+
console.log(` 📐 Mermaid 图表 ${i + 1} 渲染完成 → ${name} (${pngBuffer.length}B, ${t1 - t0}ms)`);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.warn(` ⚠️ Mermaid 图表 ${i + 1} 渲染失败:${err.message},保留原始代码块`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { md: result, images, hasMermaid: images.length > 0 };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 清理临时图片文件
|
|
64
|
+
*/
|
|
65
|
+
async function cleanupMermaidImages(images) {
|
|
66
|
+
for (const img of images) {
|
|
67
|
+
try { await unlink(img.file); } catch {}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
4
70
|
|
|
5
71
|
// ── 配置 ──────────────────────────────────────────────────────────────────────
|
|
6
72
|
let baseUrl = process.env.CONFLUENCE_BASE_URL;
|
|
7
|
-
let spaceKey = process.env.CONFLUENCE_SPACE_KEY;
|
|
8
73
|
let pat = process.env.CONFLUENCE_PAT;
|
|
9
74
|
let username = process.env.CONFLUENCE_USERNAME;
|
|
10
75
|
let apiToken = process.env.CONFLUENCE_API_TOKEN;
|
|
@@ -14,14 +79,12 @@ let api;
|
|
|
14
79
|
* 用 CLI 参数覆盖环境变量配置,需在其他函数调用前执行
|
|
15
80
|
* @param {object} [opts]
|
|
16
81
|
* @param {string} [opts.baseUrl]
|
|
17
|
-
* @param {string} [opts.spaceKey]
|
|
18
82
|
* @param {string} [opts.pat]
|
|
19
83
|
* @param {string} [opts.username]
|
|
20
84
|
* @param {string} [opts.apiToken]
|
|
21
85
|
*/
|
|
22
86
|
export function configure(opts = {}) {
|
|
23
87
|
if (opts.baseUrl) baseUrl = opts.baseUrl;
|
|
24
|
-
if (opts.spaceKey) spaceKey = opts.spaceKey;
|
|
25
88
|
if (opts.pat) pat = opts.pat;
|
|
26
89
|
if (opts.username) username = opts.username;
|
|
27
90
|
if (opts.apiToken) apiToken = opts.apiToken;
|
|
@@ -61,44 +124,23 @@ function logAxiosError(err) {
|
|
|
61
124
|
// ── 页面查询 ──────────────────────────────────────────────────────────────────
|
|
62
125
|
|
|
63
126
|
/**
|
|
64
|
-
* 按 ID
|
|
127
|
+
* 按 ID 获取页面元信息(含版本号、space)
|
|
65
128
|
*/
|
|
66
129
|
export async function getPageById(id) {
|
|
67
130
|
ensureApi();
|
|
68
131
|
const { data } = await api.get(`/content/${id}`, {
|
|
69
|
-
params: { expand: 'version,ancestors' },
|
|
132
|
+
params: { expand: 'version,ancestors,space' },
|
|
70
133
|
});
|
|
71
134
|
return data;
|
|
72
135
|
}
|
|
73
136
|
|
|
74
137
|
/**
|
|
75
|
-
*
|
|
76
|
-
*/
|
|
77
|
-
export async function getPageByTitle(spaceKey, title) {
|
|
78
|
-
ensureApi();
|
|
79
|
-
const { data } = await api.get('/content', {
|
|
80
|
-
params: {
|
|
81
|
-
spaceKey,
|
|
82
|
-
title,
|
|
83
|
-
expand: 'version',
|
|
84
|
-
limit: 1,
|
|
85
|
-
},
|
|
86
|
-
});
|
|
87
|
-
return data.results?.[0] || null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* 列出 space 下所有页面(分页)
|
|
138
|
+
* 列出指定 parent 下的子页面
|
|
92
139
|
*/
|
|
93
|
-
export async function
|
|
140
|
+
export async function listChildPages(parentId, limit = 50) {
|
|
94
141
|
ensureApi();
|
|
95
|
-
const { data } = await api.get(
|
|
96
|
-
params: {
|
|
97
|
-
spaceKey,
|
|
98
|
-
type: 'page',
|
|
99
|
-
expand: 'version',
|
|
100
|
-
limit,
|
|
101
|
-
},
|
|
142
|
+
const { data } = await api.get(`/content/${parentId}/child/page`, {
|
|
143
|
+
params: { limit, expand: 'version' },
|
|
102
144
|
});
|
|
103
145
|
return data.results;
|
|
104
146
|
}
|
|
@@ -117,19 +159,16 @@ export async function getPageContent(id) {
|
|
|
117
159
|
// ── 页面创建 / 更新 ───────────────────────────────────────────────────────────
|
|
118
160
|
|
|
119
161
|
/**
|
|
120
|
-
*
|
|
162
|
+
* 创建新页面(有 parentId 即可,Confluence 自动推断 space)
|
|
121
163
|
*/
|
|
122
|
-
export async function createPage(
|
|
164
|
+
export async function createPage(parentId, title, bodyHtml) {
|
|
123
165
|
ensureApi();
|
|
124
166
|
const payload = {
|
|
125
167
|
type: 'page',
|
|
126
168
|
title,
|
|
127
|
-
space: { key: spaceKey },
|
|
128
169
|
body: { storage: { value: bodyHtml, representation: 'storage' } },
|
|
170
|
+
ancestors: parentId ? [{ id: parentId }] : undefined,
|
|
129
171
|
};
|
|
130
|
-
if (parentId) {
|
|
131
|
-
payload.ancestors = [{ id: parentId }];
|
|
132
|
-
}
|
|
133
172
|
try {
|
|
134
173
|
const { data } = await api.post('/content', payload);
|
|
135
174
|
return data;
|
|
@@ -448,97 +487,122 @@ export async function listAttachments(pageId) {
|
|
|
448
487
|
|
|
449
488
|
// ── 高级业务流程 ──────────────────────────────────────────────────────────────
|
|
450
489
|
|
|
451
|
-
/**
|
|
452
|
-
* 查找父页面 ID(支持按 title 或直接 ID)
|
|
453
|
-
*/
|
|
454
|
-
async function resolveParentId(parentIdentifier, spaceKey) {
|
|
455
|
-
if (!parentIdentifier) return null;
|
|
456
|
-
// 如果是纯数字,当作 ID
|
|
457
|
-
if (/^\d+$/.test(parentIdentifier)) {
|
|
458
|
-
const page = await getPageById(parentIdentifier);
|
|
459
|
-
console.log(` 父页面:${page.title} (ID: ${page.id})`);
|
|
460
|
-
return page.id;
|
|
461
|
-
}
|
|
462
|
-
// 否则按 title 查找
|
|
463
|
-
const page = await getPageByTitle(spaceKey, parentIdentifier);
|
|
464
|
-
if (!page) {
|
|
465
|
-
throw new Error(`未找到父页面「${parentIdentifier}」(space: ${spaceKey})`);
|
|
466
|
-
}
|
|
467
|
-
console.log(` 父页面:${page.title} (ID: ${page.id})`);
|
|
468
|
-
return page.id;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
490
|
/**
|
|
472
491
|
* 上传 Markdown 文件到 Confluence
|
|
473
492
|
* @param {object} opts
|
|
474
493
|
* @param {string} opts.filePath - 本地 .md 文件路径
|
|
475
494
|
* @param {string} [opts.title] - 页面标题(默认取文件名)
|
|
476
|
-
* @param {string}
|
|
477
|
-
* @param {string} [opts.parentId] - 父页面 ID 或标题
|
|
495
|
+
* @param {string} opts.parentId - 父页面 ID(数字)
|
|
478
496
|
* @param {boolean} [opts.dryRun] - 仅打印转换结果
|
|
479
497
|
*/
|
|
480
498
|
export async function uploadMarkdown(opts) {
|
|
481
|
-
const { filePath, title: optTitle,
|
|
482
|
-
const effectiveSpaceKey = spaceKeyOpt || spaceKey;
|
|
499
|
+
const { filePath, title: optTitle, parentId: parentRaw, dryRun = false, mermaid = true } = opts;
|
|
483
500
|
const fs = await import('node:fs');
|
|
484
501
|
const path = await import('node:path');
|
|
485
502
|
|
|
486
|
-
|
|
503
|
+
let md = fs.readFileSync(filePath, 'utf-8');
|
|
487
504
|
const title = optTitle || path.basename(filePath, extname(filePath));
|
|
505
|
+
|
|
506
|
+
// 必须有 parentId
|
|
507
|
+
if (!parentRaw || !/^\d+$/.test(parentRaw)) {
|
|
508
|
+
throw new Error('upload 需要 --parent <数字页面ID> 作为父页面');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const parentPage = await getPageById(parentRaw);
|
|
512
|
+
console.log(` 父页面:${parentPage.title} (ID: ${parentPage.id}, Space: ${parentPage.space?.key})`);
|
|
513
|
+
|
|
514
|
+
// Mermaid 预处理
|
|
515
|
+
let mermaidImages = [];
|
|
516
|
+
if (mermaid) {
|
|
517
|
+
const result = await preprocessMermaid(md);
|
|
518
|
+
md = result.md;
|
|
519
|
+
mermaidImages = result.images;
|
|
520
|
+
}
|
|
521
|
+
|
|
488
522
|
const bodyHtml = mdToConfluence(md);
|
|
489
523
|
|
|
490
524
|
if (dryRun) {
|
|
491
525
|
console.log('── 转换结果(dry-run)────────────────────────');
|
|
492
526
|
console.log(` 标题: ${title}`);
|
|
493
|
-
console.log(`
|
|
527
|
+
console.log(` 父页面: ${parentPage.title} (ID: ${parentPage.id})`);
|
|
528
|
+
if (mermaidImages.length) console.log(` Mermaid 图片: ${mermaidImages.length} 张`);
|
|
494
529
|
console.log(' Body:\n' + bodyHtml);
|
|
495
530
|
return null;
|
|
496
531
|
}
|
|
497
532
|
|
|
498
|
-
|
|
499
|
-
|
|
533
|
+
try {
|
|
534
|
+
// 检查 parent 下是否已有同名子页面
|
|
535
|
+
const children = await listChildPages(parentPage.id, 500);
|
|
536
|
+
const existing = children.find(c => c.title === title);
|
|
537
|
+
|
|
538
|
+
let page;
|
|
539
|
+
if (existing) {
|
|
540
|
+
console.log(` 父页面下已存在同名页面「${title}」(ID: ${existing.id}),将更新…`);
|
|
541
|
+
page = await updatePage(existing.id, title, bodyHtml, existing.version.number);
|
|
542
|
+
} else {
|
|
543
|
+
page = await createPage(parentPage.id, title, bodyHtml);
|
|
544
|
+
}
|
|
500
545
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
546
|
+
// 上传 mermaid 图片作为附件
|
|
547
|
+
if (mermaidImages.length) {
|
|
548
|
+
console.log(' 📎 上传 Mermaid 图片附件…');
|
|
549
|
+
for (const img of mermaidImages) {
|
|
550
|
+
await uploadAttachment(page.id, img.file, img.name);
|
|
551
|
+
console.log(` ✅ ${img.name}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
509
554
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
555
|
+
console.log(` ✅ ${existing ? '更新' : '创建'}完成 → ${baseUrl}/pages/viewpage.action?pageId=${page.id}`);
|
|
556
|
+
return page;
|
|
557
|
+
} finally {
|
|
558
|
+
await cleanupMermaidImages(mermaidImages);
|
|
559
|
+
}
|
|
513
560
|
}
|
|
514
561
|
|
|
515
562
|
/**
|
|
516
|
-
* 更新指定页面(通过
|
|
563
|
+
* 更新指定页面(通过 page-id 定位)
|
|
517
564
|
*/
|
|
518
565
|
export async function updateMarkdown(opts) {
|
|
519
|
-
const { pageId,
|
|
520
|
-
const effectiveSpaceKey = spaceKeyOpt || spaceKey;
|
|
566
|
+
const { pageId, filePath, minorEdit = false, mermaid = true } = opts;
|
|
521
567
|
const fs = await import('node:fs');
|
|
522
568
|
|
|
523
|
-
|
|
524
|
-
|
|
569
|
+
if (!pageId || !/^\d+$/.test(pageId)) {
|
|
570
|
+
throw new Error('update 需要 --page-id <数字页面ID>');
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
let md = fs.readFileSync(filePath, 'utf-8');
|
|
525
574
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
} else {
|
|
533
|
-
throw new Error('必须指定 --page-id 或 --title 来定位目标页面');
|
|
575
|
+
// Mermaid 预处理
|
|
576
|
+
let mermaidImages = [];
|
|
577
|
+
if (mermaid) {
|
|
578
|
+
const result = await preprocessMermaid(md);
|
|
579
|
+
md = result.md;
|
|
580
|
+
mermaidImages = result.images;
|
|
534
581
|
}
|
|
535
582
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
583
|
+
const bodyHtml = mdToConfluence(md);
|
|
584
|
+
|
|
585
|
+
const page = await getPageById(pageId);
|
|
586
|
+
|
|
587
|
+
try {
|
|
588
|
+
console.log(` 目标页面:${page.title} (ID: ${page.id}, Space: ${page.space?.key}, 当前版本: ${page.version.number})`);
|
|
589
|
+
const updated = await updatePage(page.id, page.title, bodyHtml, page.version.number, minorEdit);
|
|
590
|
+
|
|
591
|
+
// 上传 mermaid 图片作为附件
|
|
592
|
+
if (mermaidImages.length) {
|
|
593
|
+
console.log(' 📎 上传 Mermaid 图片附件…');
|
|
594
|
+
for (const img of mermaidImages) {
|
|
595
|
+
await uploadAttachment(page.id, img.file, img.name);
|
|
596
|
+
console.log(` ✅ ${img.name}`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
console.log(` ✅ 更新完成 → ${baseUrl}/pages/viewpage.action?pageId=${updated.id}`);
|
|
601
|
+
return updated;
|
|
602
|
+
} finally {
|
|
603
|
+
await cleanupMermaidImages(mermaidImages);
|
|
604
|
+
}
|
|
540
605
|
}
|
|
541
606
|
|
|
542
607
|
// ── 导出配置 ──────────────────────────────────────────────────────────────────
|
|
543
608
|
export function getBaseUrl() { return baseUrl; }
|
|
544
|
-
export function getSpaceKey() { return spaceKey; }
|
package/upload.mjs
CHANGED
|
@@ -2,40 +2,37 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Confluence 文档同步 CLI
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* 方式2 Basic Auth: CONFLUENCE_USERNAME - Confluence 登录名(工号)
|
|
11
|
-
* CONFLUENCE_API_TOKEN - API Token 或密码
|
|
5
|
+
* 环境变量:
|
|
6
|
+
* CONFLUENCE_BASE_URL - Confluence 服务地址
|
|
7
|
+
* 认证方式(二选一):
|
|
8
|
+
* 方式1 PAT: CONFLUENCE_PAT - Personal Access Token
|
|
9
|
+
* 方式2 Auth: CONFLUENCE_USERNAME + CONFLUENCE_API_TOKEN
|
|
12
10
|
*
|
|
13
11
|
* 用法:
|
|
14
|
-
* confluence-sync upload
|
|
15
|
-
* confluence-sync update
|
|
12
|
+
* confluence-sync upload --file <path> --parent <页面ID> [--title <标题>] [--dry-run] [--no-mermaid-to-img]
|
|
13
|
+
* confluence-sync update --file <path> --page-id <ID> [--minor-edit] [--no-mermaid-to-img]
|
|
16
14
|
* confluence-sync upload-file --page-id <ID> --file <path> [--name <文件名>]
|
|
17
|
-
* confluence-sync list-pages
|
|
18
|
-
* confluence-sync list-files
|
|
15
|
+
* confluence-sync list-pages --parent <页面ID>
|
|
16
|
+
* confluence-sync list-files --page-id <ID>
|
|
19
17
|
* confluence-sync get-content --page-id <ID> [--markdown]
|
|
20
18
|
*
|
|
21
19
|
* 可选配置参数(优先于环境变量):
|
|
22
|
-
* --base-url
|
|
23
|
-
* --
|
|
24
|
-
* --
|
|
25
|
-
* --
|
|
26
|
-
* --api-token API Token 或密码
|
|
20
|
+
* --base-url Confluence 服务地址
|
|
21
|
+
* --pat Personal Access Token
|
|
22
|
+
* --username Confluence 登录名
|
|
23
|
+
* --api-token API Token 或密码
|
|
27
24
|
*/
|
|
28
25
|
|
|
29
26
|
import {
|
|
30
27
|
uploadMarkdown,
|
|
31
28
|
updateMarkdown,
|
|
32
29
|
uploadAttachment,
|
|
33
|
-
|
|
30
|
+
listChildPages,
|
|
34
31
|
listAttachments,
|
|
32
|
+
getPageById,
|
|
35
33
|
getPageContent,
|
|
36
34
|
confluenceToMd,
|
|
37
35
|
getBaseUrl,
|
|
38
|
-
getSpaceKey,
|
|
39
36
|
configure,
|
|
40
37
|
} from './src/confluence.mjs';
|
|
41
38
|
|
|
@@ -73,23 +70,22 @@ Confluence 文档同步工具
|
|
|
73
70
|
upload 上传 Markdown 文件为新页面(已存在则更新)
|
|
74
71
|
update 更新已有页面内容
|
|
75
72
|
upload-file 上传文件作为页面附件
|
|
76
|
-
list-pages 列出
|
|
73
|
+
list-pages 列出 parent 页面下的子页面
|
|
77
74
|
list-files 列出页面的所有附件
|
|
78
75
|
get-content 获取页面的 HTML 内容
|
|
79
76
|
|
|
80
77
|
upload 选项:
|
|
81
78
|
--file 本地 .md 文件路径(必填)
|
|
79
|
+
--parent 父页面 ID(数字,必填)
|
|
82
80
|
--title 页面标题(默认取文件名)
|
|
83
|
-
--space Space Key(默认读 config.json)
|
|
84
|
-
--parent 父页面 ID 或标题
|
|
85
81
|
--dry-run 仅打印转换结果,不实际上传
|
|
82
|
+
--no-mermaid-to-img 禁用 Mermaid 图表自动渲染
|
|
86
83
|
|
|
87
84
|
update 选项:
|
|
88
85
|
--file 本地 .md 文件路径(必填)
|
|
89
|
-
--page-id 目标页面 ID
|
|
90
|
-
--title 按标题查找目标页面(与 --page-id 二选一)
|
|
91
|
-
--space Space Key(默认读 config.json)
|
|
86
|
+
--page-id 目标页面 ID(数字,必填)
|
|
92
87
|
--minor-edit 标记为小修改(不通知关注者)
|
|
88
|
+
--no-mermaid-to-img 禁用 Mermaid 图表自动渲染
|
|
93
89
|
|
|
94
90
|
upload-file 选项:
|
|
95
91
|
--page-id 目标页面 ID(必填)
|
|
@@ -97,34 +93,26 @@ upload-file 选项:
|
|
|
97
93
|
--name 覆盖文件名
|
|
98
94
|
|
|
99
95
|
list-pages 选项:
|
|
100
|
-
--
|
|
96
|
+
--parent 父页面 ID(数字,必填)
|
|
101
97
|
|
|
102
98
|
list-files 选项:
|
|
103
99
|
--page-id 页面 ID(必填)
|
|
104
100
|
|
|
105
101
|
get-content 选项:
|
|
106
102
|
--page-id 页面 ID(必填)
|
|
107
|
-
--markdown 输出为 Markdown
|
|
103
|
+
--markdown 输出为 Markdown 格式
|
|
108
104
|
|
|
109
105
|
配置参数(优先于环境变量):
|
|
110
|
-
--base-url
|
|
111
|
-
--
|
|
112
|
-
--
|
|
113
|
-
--
|
|
114
|
-
--api-token API Token 或密码
|
|
106
|
+
--base-url Confluence 服务地址
|
|
107
|
+
--pat Personal Access Token
|
|
108
|
+
--username Confluence 登录名
|
|
109
|
+
--api-token API Token 或密码
|
|
115
110
|
|
|
116
111
|
环境变量:
|
|
117
|
-
|
|
118
|
-
CONFLUENCE_BASE_URL Confluence 服务地址
|
|
119
|
-
CONFLUENCE_SPACE_KEY Space Key
|
|
120
|
-
|
|
112
|
+
CONFLUENCE_BASE_URL Confluence 服务地址
|
|
121
113
|
认证方式(二选一):
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
方式2 - Basic Auth:
|
|
126
|
-
CONFLUENCE_USERNAME Confluence 登录名(工号)
|
|
127
|
-
CONFLUENCE_API_TOKEN API Token 或密码
|
|
114
|
+
CONFLUENCE_PAT Personal Access Token
|
|
115
|
+
CONFLUENCE_USERNAME + CONFLUENCE_API_TOKEN Basic Auth
|
|
128
116
|
|
|
129
117
|
`);
|
|
130
118
|
}
|
|
@@ -143,13 +131,11 @@ async function main() {
|
|
|
143
131
|
// 用 CLI 参数覆盖环境变量配置
|
|
144
132
|
configure({
|
|
145
133
|
baseUrl: args['base-url'],
|
|
146
|
-
spaceKey: args['space-key'],
|
|
147
134
|
pat: args['pat'],
|
|
148
135
|
username: args['username'],
|
|
149
136
|
apiToken: args['api-token'],
|
|
150
137
|
});
|
|
151
138
|
const baseUrl = getBaseUrl();
|
|
152
|
-
const spaceKey = getSpaceKey();
|
|
153
139
|
|
|
154
140
|
try {
|
|
155
141
|
switch (command) {
|
|
@@ -161,9 +147,9 @@ async function main() {
|
|
|
161
147
|
const result = await uploadMarkdown({
|
|
162
148
|
filePath: args.file,
|
|
163
149
|
title: args.title,
|
|
164
|
-
spaceKey: args.space || spaceKey,
|
|
165
150
|
parentId: args.parent,
|
|
166
151
|
dryRun: args['dry-run'] === true,
|
|
152
|
+
mermaid: args['no-mermaid-to-img'] !== true,
|
|
167
153
|
});
|
|
168
154
|
if (result) {
|
|
169
155
|
console.log('\n页面信息:');
|
|
@@ -180,16 +166,15 @@ async function main() {
|
|
|
180
166
|
console.error('错误:--file 必填');
|
|
181
167
|
process.exit(1);
|
|
182
168
|
}
|
|
183
|
-
if (!args['page-id']
|
|
184
|
-
console.error('
|
|
169
|
+
if (!args['page-id']) {
|
|
170
|
+
console.error('错误:--page-id 必填');
|
|
185
171
|
process.exit(1);
|
|
186
172
|
}
|
|
187
173
|
const result = await updateMarkdown({
|
|
188
174
|
pageId: args['page-id'],
|
|
189
|
-
title: args.title,
|
|
190
175
|
filePath: args.file,
|
|
191
|
-
spaceKey: args.space || spaceKey,
|
|
192
176
|
minorEdit: args['minor-edit'] === true,
|
|
177
|
+
mermaid: args['no-mermaid-to-img'] !== true,
|
|
193
178
|
});
|
|
194
179
|
console.log('\n页面信息:');
|
|
195
180
|
console.log(` ID: ${result.id}`);
|
|
@@ -216,18 +201,22 @@ async function main() {
|
|
|
216
201
|
}
|
|
217
202
|
|
|
218
203
|
case 'list-pages': {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
204
|
+
if (!args.parent || !/^\d+$/.test(args.parent)) {
|
|
205
|
+
console.error('错误:--parent <数字页面ID> 必填');
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
const parentPage = await getPageById(args.parent);
|
|
209
|
+
console.log(`父页面:${parentPage.title} (ID: ${parentPage.id}, Space: ${parentPage.space?.key})\n`);
|
|
210
|
+
const pages = await listChildPages(parentPage.id);
|
|
222
211
|
if (!pages.length) {
|
|
223
|
-
console.log('
|
|
212
|
+
console.log(' (无子页面)');
|
|
224
213
|
} else {
|
|
225
214
|
const maxId = Math.max(...pages.map(p => String(p.id).length));
|
|
226
215
|
for (const p of pages) {
|
|
227
216
|
const id = String(p.id).padEnd(maxId);
|
|
228
217
|
console.log(` ${id} v${p.version?.number ?? '?'} ${p.title}`);
|
|
229
218
|
}
|
|
230
|
-
console.log(`\n共 ${pages.length}
|
|
219
|
+
console.log(`\n共 ${pages.length} 个子页面`);
|
|
231
220
|
}
|
|
232
221
|
break;
|
|
233
222
|
}
|