@vdfor/confluence-sync 0.1.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 +56 -20
- package/package.json +5 -3
- package/skills/confluence-sync.md +97 -0
- package/src/confluence.mjs +157 -92
- package/upload.mjs +42 -47
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,7 +20,7 @@ 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
26
|
# 认证方式(二选一)
|
|
@@ -35,23 +35,23 @@ export CONFLUENCE_API_TOKEN="your-api-token"
|
|
|
35
35
|
### 方式2:CLI 参数(优先于环境变量)
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
confluence-sync upload --file test.md \
|
|
38
|
+
confluence-sync upload --file test.md --parent 123456 \
|
|
39
39
|
--base-url https://your-confluence-instance.com \
|
|
40
40
|
--pat your-token
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
## 命令
|
|
44
44
|
|
|
45
|
-
### upload - 上传 Markdown
|
|
45
|
+
### upload - 上传 Markdown 为新页面(已存在同名页面则自动更新)
|
|
46
46
|
|
|
47
47
|
```bash
|
|
48
|
-
confluence-sync upload --file <path>
|
|
48
|
+
confluence-sync upload --file <path> --parent <页面ID> [--title <标题>] [--dry-run] [--no-mermaid-to-img]
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
### update - 更新已有页面
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
|
-
confluence-sync update --file <path>
|
|
54
|
+
confluence-sync update --file <path> --page-id <ID> [--minor-edit] [--no-mermaid-to-img]
|
|
55
55
|
```
|
|
56
56
|
|
|
57
57
|
### upload-file - 上传附件
|
|
@@ -60,10 +60,10 @@ confluence-sync update --file <path> (--page-id <ID> | --title <标题>) [--spac
|
|
|
60
60
|
confluence-sync upload-file --page-id <ID> --file <path> [--name <文件名>]
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
### list-pages -
|
|
63
|
+
### list-pages - 列出 parent 下的子页面
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
|
-
confluence-sync list-pages
|
|
66
|
+
confluence-sync list-pages --parent <页面ID>
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
### list-files - 列出页面附件
|
|
@@ -80,28 +80,44 @@ confluence-sync get-content --page-id <ID> [--markdown]
|
|
|
80
80
|
|
|
81
81
|
使用 `--markdown` 参数可将 Confluence 内容转换为 Markdown 格式输出。
|
|
82
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
|
+
|
|
83
99
|
## 配置参数
|
|
84
100
|
|
|
85
101
|
| 参数 | 说明 |
|
|
86
102
|
|------|------|
|
|
87
|
-
| `--base-url` | Confluence
|
|
88
|
-
| `--pat` | Personal Access Token |
|
|
89
|
-
| `--username` | Confluence
|
|
90
|
-
| `--api-token` | API Token
|
|
91
|
-
| `--space` | Space Key |
|
|
103
|
+
| `--base-url` | Confluence 服务地址(覆盖环境变量) |
|
|
104
|
+
| `--pat` | Personal Access Token(覆盖环境变量) |
|
|
105
|
+
| `--username` | Confluence 登录名(覆盖环境变量) |
|
|
106
|
+
| `--api-token` | API Token 或密码(覆盖环境变量) |
|
|
92
107
|
| `--file` | 本地文件路径 |
|
|
93
|
-
| `--title` |
|
|
94
|
-
| `--
|
|
95
|
-
| `--
|
|
108
|
+
| `--title` | 页面标题(upload 时可选,默认取文件名) |
|
|
109
|
+
| `--parent` | 父页面 ID(数字,upload / list-pages 必填) |
|
|
110
|
+
| `--page-id` | 页面 ID(update / upload-file / list-files / get-content 必填) |
|
|
96
111
|
| `--dry-run` | 仅打印转换结果,不实际上传 |
|
|
97
112
|
| `--minor-edit` | 标记为小修改(不通知关注者) |
|
|
98
113
|
| `--markdown` | get-content 时输出为 Markdown 格式 |
|
|
114
|
+
| `--no-mermaid-to-img` | 禁用 Mermaid 图表自动渲染 |
|
|
99
115
|
|
|
100
116
|
## 示例
|
|
101
117
|
|
|
102
118
|
```bash
|
|
103
|
-
# 上传 Markdown
|
|
104
|
-
confluence-sync upload --file README.md --title "项目文档" --
|
|
119
|
+
# 上传 Markdown 文件(自动按 title 匹配 parent 下已有页面则更新)
|
|
120
|
+
confluence-sync upload --file README.md --title "项目文档" --parent 27263539
|
|
105
121
|
|
|
106
122
|
# 更新已有页面
|
|
107
123
|
confluence-sync update --file README.md --page-id 123456
|
|
@@ -109,8 +125,8 @@ confluence-sync update --file README.md --page-id 123456
|
|
|
109
125
|
# 上传附件
|
|
110
126
|
confluence-sync upload-file --page-id 123456 --file image.png
|
|
111
127
|
|
|
112
|
-
#
|
|
113
|
-
confluence-sync list-pages --
|
|
128
|
+
# 列出 parent 下的子页面
|
|
129
|
+
confluence-sync list-pages --parent 27263539
|
|
114
130
|
|
|
115
131
|
# 获取页面内容
|
|
116
132
|
confluence-sync get-content --page-id 123456
|
|
@@ -119,6 +135,26 @@ confluence-sync get-content --page-id 123456
|
|
|
119
135
|
confluence-sync get-content --page-id 123456 --markdown
|
|
120
136
|
```
|
|
121
137
|
|
|
138
|
+
## Skill 集成
|
|
139
|
+
|
|
140
|
+
本项目包含 Trae Skill,安装后可被智能体调用。
|
|
141
|
+
|
|
142
|
+
### 安装
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
pnpm add -g @vdfor/confluence-sync
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 使用
|
|
149
|
+
|
|
150
|
+
安装后,智能体可直接调用 `confluence-sync` 命令:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
confluence-sync upload --file README.md --title "项目文档" --parent 27263539
|
|
154
|
+
confluence-sync update --file README.md --page-id 123456
|
|
155
|
+
confluence-sync get-content --page-id 123456 --markdown
|
|
156
|
+
```
|
|
157
|
+
|
|
122
158
|
## License
|
|
123
159
|
|
|
124
160
|
MIT
|
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,
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
22
|
"upload.mjs",
|
|
23
|
-
"src/"
|
|
23
|
+
"src/",
|
|
24
|
+
"skills/"
|
|
24
25
|
],
|
|
25
26
|
"engines": {
|
|
26
27
|
"node": ">=18"
|
|
@@ -28,7 +29,8 @@
|
|
|
28
29
|
"dependencies": {
|
|
29
30
|
"axios": "^1.7.0",
|
|
30
31
|
"form-data": "^4.0.0",
|
|
31
|
-
"marked": "^15.0.0"
|
|
32
|
+
"marked": "^15.0.0",
|
|
33
|
+
"sebastianjs": "^0.4.0"
|
|
32
34
|
},
|
|
33
35
|
"devDependencies": {
|
|
34
36
|
"rimraf": "^6.1.3"
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "confluence-sync"
|
|
3
|
+
description: "Confluence 文档同步 CLI 工具,支持将本地 Markdown 文件上传至 Confluence 并自动转换为 Confluence 页面格式,支持 Mermaid 图表自动渲染。当用户需要上传、更新、获取 Confluence 页面内容或管理附件时调用。"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Confluence Sync
|
|
7
|
+
|
|
8
|
+
Confluence 文档同步 CLI 工具,支持将本地 Markdown 文件上传至 Confluence 并自动转换为 Confluence 页面格式。支持 Mermaid 图表自动渲染为图片。
|
|
9
|
+
|
|
10
|
+
## 功能特性
|
|
11
|
+
|
|
12
|
+
- 上传 Markdown 文件为 Confluence 页面(已存在同名页面则自动更新)
|
|
13
|
+
- 更新已有页面内容
|
|
14
|
+
- 上传文件作为页面附件
|
|
15
|
+
- 列出 parent 下的子页面
|
|
16
|
+
- 列出页面附件
|
|
17
|
+
- 获取页面内容(支持转换为 Markdown 格式)
|
|
18
|
+
- Mermaid 图表自动渲染为 PNG 图片
|
|
19
|
+
|
|
20
|
+
## 配置要求
|
|
21
|
+
|
|
22
|
+
使用前需要配置以下环境变量或 CLI 参数:
|
|
23
|
+
|
|
24
|
+
- `CONFLUENCE_BASE_URL`: Confluence 服务地址
|
|
25
|
+
- 认证方式(二选一):
|
|
26
|
+
- `CONFLUENCE_PAT`: Personal Access Token(推荐)
|
|
27
|
+
- `CONFLUENCE_USERNAME` + `CONFLUENCE_API_TOKEN`: Basic Auth 认证
|
|
28
|
+
|
|
29
|
+
## 命令列表
|
|
30
|
+
|
|
31
|
+
### upload - 上传 Markdown 为新页面(已存在同名页面则自动更新)
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
confluence-sync upload --file <path> --parent <页面ID> [--title <标题>] [--dry-run] [--no-mermaid-to-img]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### update - 更新已有页面
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
confluence-sync update --file <path> --page-id <ID> [--minor-edit] [--no-mermaid-to-img]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### upload-file - 上传附件
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
confluence-sync upload-file --page-id <ID> --file <path> [--name <文件名>]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### list-pages - 列出 parent 下的子页面
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
confluence-sync list-pages --parent <页面ID>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### list-files - 列出页面附件
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
confluence-sync list-files --page-id <ID>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### get-content - 获取页面内容
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
confluence-sync get-content --page-id <ID> [--markdown]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
使用 `--markdown` 参数可将 Confluence 内容转换为 Markdown 格式输出。
|
|
68
|
+
|
|
69
|
+
## Mermaid 支持
|
|
70
|
+
|
|
71
|
+
Markdown 中的 ```` ```mermaid ```` 代码块自动通过 sebastianjs 渲染为 PNG 图片(本地离线渲染,无需外网、无需浏览器),作为附件上传。默认开启,使用 `--no-mermaid-to-img` 可禁用。渲染失败时保留原始代码块,不阻塞上传。
|
|
72
|
+
|
|
73
|
+
## 使用示例
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# 上传(--parent 必填,已存在同名子页面则自动更新)
|
|
77
|
+
confluence-sync upload --file README.md --title "项目文档" --parent 27263539
|
|
78
|
+
|
|
79
|
+
# 更新(--page-id 必填)
|
|
80
|
+
confluence-sync update --file README.md --page-id 123456
|
|
81
|
+
|
|
82
|
+
# 列出 parent 下的子页面
|
|
83
|
+
confluence-sync list-pages --parent 27263539
|
|
84
|
+
|
|
85
|
+
# 上传附件
|
|
86
|
+
confluence-sync upload-file --page-id 123456 --file image.png
|
|
87
|
+
|
|
88
|
+
# 获取页面内容
|
|
89
|
+
confluence-sync get-content --page-id 123456 --markdown
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## 注意事项
|
|
93
|
+
|
|
94
|
+
1. 使用前请确保已正确配置认证信息
|
|
95
|
+
2. Personal Access Token 需要在 Confluence 个人设置中生成
|
|
96
|
+
3. 上传附件时,如果附件已存在会自动更新
|
|
97
|
+
4. 使用 `--dry-run` 参数可以预览转换结果而不实际上传
|
package/src/confluence.mjs
CHANGED
|
@@ -1,12 +1,74 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
2
|
import { marked } from 'marked';
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
4
3
|
import { extname } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { writeFile, unlink } from 'node:fs/promises';
|
|
5
6
|
|
|
6
|
-
// ──
|
|
7
|
-
|
|
8
|
-
|
|
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
|
+
}
|
|
9
70
|
|
|
71
|
+
// ── 配置 ──────────────────────────────────────────────────────────────────────
|
|
10
72
|
let baseUrl = process.env.CONFLUENCE_BASE_URL;
|
|
11
73
|
let pat = process.env.CONFLUENCE_PAT;
|
|
12
74
|
let username = process.env.CONFLUENCE_USERNAME;
|
|
@@ -62,44 +124,23 @@ function logAxiosError(err) {
|
|
|
62
124
|
// ── 页面查询 ──────────────────────────────────────────────────────────────────
|
|
63
125
|
|
|
64
126
|
/**
|
|
65
|
-
* 按 ID
|
|
127
|
+
* 按 ID 获取页面元信息(含版本号、space)
|
|
66
128
|
*/
|
|
67
129
|
export async function getPageById(id) {
|
|
68
130
|
ensureApi();
|
|
69
131
|
const { data } = await api.get(`/content/${id}`, {
|
|
70
|
-
params: { expand: 'version,ancestors' },
|
|
132
|
+
params: { expand: 'version,ancestors,space' },
|
|
71
133
|
});
|
|
72
134
|
return data;
|
|
73
135
|
}
|
|
74
136
|
|
|
75
137
|
/**
|
|
76
|
-
*
|
|
138
|
+
* 列出指定 parent 下的子页面
|
|
77
139
|
*/
|
|
78
|
-
export async function
|
|
140
|
+
export async function listChildPages(parentId, limit = 50) {
|
|
79
141
|
ensureApi();
|
|
80
|
-
const { data } = await api.get(
|
|
81
|
-
params: {
|
|
82
|
-
spaceKey,
|
|
83
|
-
title,
|
|
84
|
-
expand: 'version',
|
|
85
|
-
limit: 1,
|
|
86
|
-
},
|
|
87
|
-
});
|
|
88
|
-
return data.results?.[0] || null;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* 列出 space 下所有页面(分页)
|
|
93
|
-
*/
|
|
94
|
-
export async function listPages(spaceKey, limit = 25) {
|
|
95
|
-
ensureApi();
|
|
96
|
-
const { data } = await api.get('/content', {
|
|
97
|
-
params: {
|
|
98
|
-
spaceKey,
|
|
99
|
-
type: 'page',
|
|
100
|
-
expand: 'version',
|
|
101
|
-
limit,
|
|
102
|
-
},
|
|
142
|
+
const { data } = await api.get(`/content/${parentId}/child/page`, {
|
|
143
|
+
params: { limit, expand: 'version' },
|
|
103
144
|
});
|
|
104
145
|
return data.results;
|
|
105
146
|
}
|
|
@@ -118,19 +159,16 @@ export async function getPageContent(id) {
|
|
|
118
159
|
// ── 页面创建 / 更新 ───────────────────────────────────────────────────────────
|
|
119
160
|
|
|
120
161
|
/**
|
|
121
|
-
*
|
|
162
|
+
* 创建新页面(有 parentId 即可,Confluence 自动推断 space)
|
|
122
163
|
*/
|
|
123
|
-
export async function createPage(
|
|
164
|
+
export async function createPage(parentId, title, bodyHtml) {
|
|
124
165
|
ensureApi();
|
|
125
166
|
const payload = {
|
|
126
167
|
type: 'page',
|
|
127
168
|
title,
|
|
128
|
-
space: { key: spaceKey },
|
|
129
169
|
body: { storage: { value: bodyHtml, representation: 'storage' } },
|
|
170
|
+
ancestors: parentId ? [{ id: parentId }] : undefined,
|
|
130
171
|
};
|
|
131
|
-
if (parentId) {
|
|
132
|
-
payload.ancestors = [{ id: parentId }];
|
|
133
|
-
}
|
|
134
172
|
try {
|
|
135
173
|
const { data } = await api.post('/content', payload);
|
|
136
174
|
return data;
|
|
@@ -449,95 +487,122 @@ export async function listAttachments(pageId) {
|
|
|
449
487
|
|
|
450
488
|
// ── 高级业务流程 ──────────────────────────────────────────────────────────────
|
|
451
489
|
|
|
452
|
-
/**
|
|
453
|
-
* 查找父页面 ID(支持按 title 或直接 ID)
|
|
454
|
-
*/
|
|
455
|
-
async function resolveParentId(parentIdentifier, spaceKey) {
|
|
456
|
-
if (!parentIdentifier) return null;
|
|
457
|
-
// 如果是纯数字,当作 ID
|
|
458
|
-
if (/^\d+$/.test(parentIdentifier)) {
|
|
459
|
-
const page = await getPageById(parentIdentifier);
|
|
460
|
-
console.log(` 父页面:${page.title} (ID: ${page.id})`);
|
|
461
|
-
return page.id;
|
|
462
|
-
}
|
|
463
|
-
// 否则按 title 查找
|
|
464
|
-
const page = await getPageByTitle(spaceKey, parentIdentifier);
|
|
465
|
-
if (!page) {
|
|
466
|
-
throw new Error(`未找到父页面「${parentIdentifier}」(space: ${spaceKey})`);
|
|
467
|
-
}
|
|
468
|
-
console.log(` 父页面:${page.title} (ID: ${page.id})`);
|
|
469
|
-
return page.id;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
490
|
/**
|
|
473
491
|
* 上传 Markdown 文件到 Confluence
|
|
474
492
|
* @param {object} opts
|
|
475
493
|
* @param {string} opts.filePath - 本地 .md 文件路径
|
|
476
494
|
* @param {string} [opts.title] - 页面标题(默认取文件名)
|
|
477
|
-
* @param {string}
|
|
478
|
-
* @param {string} [opts.parentId] - 父页面 ID 或标题
|
|
495
|
+
* @param {string} opts.parentId - 父页面 ID(数字)
|
|
479
496
|
* @param {boolean} [opts.dryRun] - 仅打印转换结果
|
|
480
497
|
*/
|
|
481
498
|
export async function uploadMarkdown(opts) {
|
|
482
|
-
const { filePath, title: optTitle,
|
|
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,
|
|
566
|
+
const { pageId, filePath, minorEdit = false, mermaid = true } = opts;
|
|
520
567
|
const fs = await import('node:fs');
|
|
521
568
|
|
|
522
|
-
|
|
523
|
-
|
|
569
|
+
if (!pageId || !/^\d+$/.test(pageId)) {
|
|
570
|
+
throw new Error('update 需要 --page-id <数字页面ID>');
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
let md = fs.readFileSync(filePath, 'utf-8');
|
|
524
574
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
} else {
|
|
532
|
-
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;
|
|
533
581
|
}
|
|
534
582
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
+
}
|
|
539
605
|
}
|
|
540
606
|
|
|
541
607
|
// ── 导出配置 ──────────────────────────────────────────────────────────────────
|
|
542
|
-
export { defaultSpaceKey };
|
|
543
608
|
export function getBaseUrl() { return baseUrl; }
|
package/upload.mjs
CHANGED
|
@@ -2,37 +2,36 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Confluence 文档同步 CLI
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* 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
|
|
11
10
|
*
|
|
12
11
|
* 用法:
|
|
13
|
-
* confluence-sync upload
|
|
14
|
-
* 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]
|
|
15
14
|
* confluence-sync upload-file --page-id <ID> --file <path> [--name <文件名>]
|
|
16
|
-
* confluence-sync list-pages
|
|
17
|
-
* confluence-sync list-files
|
|
15
|
+
* confluence-sync list-pages --parent <页面ID>
|
|
16
|
+
* confluence-sync list-files --page-id <ID>
|
|
18
17
|
* confluence-sync get-content --page-id <ID> [--markdown]
|
|
19
18
|
*
|
|
20
19
|
* 可选配置参数(优先于环境变量):
|
|
21
|
-
* --base-url
|
|
22
|
-
* --pat
|
|
23
|
-
* --username
|
|
24
|
-
* --api-token
|
|
20
|
+
* --base-url Confluence 服务地址
|
|
21
|
+
* --pat Personal Access Token
|
|
22
|
+
* --username Confluence 登录名
|
|
23
|
+
* --api-token API Token 或密码
|
|
25
24
|
*/
|
|
26
25
|
|
|
27
26
|
import {
|
|
28
27
|
uploadMarkdown,
|
|
29
28
|
updateMarkdown,
|
|
30
29
|
uploadAttachment,
|
|
31
|
-
|
|
30
|
+
listChildPages,
|
|
32
31
|
listAttachments,
|
|
32
|
+
getPageById,
|
|
33
33
|
getPageContent,
|
|
34
34
|
confluenceToMd,
|
|
35
|
-
defaultSpaceKey,
|
|
36
35
|
getBaseUrl,
|
|
37
36
|
configure,
|
|
38
37
|
} from './src/confluence.mjs';
|
|
@@ -71,23 +70,22 @@ Confluence 文档同步工具
|
|
|
71
70
|
upload 上传 Markdown 文件为新页面(已存在则更新)
|
|
72
71
|
update 更新已有页面内容
|
|
73
72
|
upload-file 上传文件作为页面附件
|
|
74
|
-
list-pages 列出
|
|
73
|
+
list-pages 列出 parent 页面下的子页面
|
|
75
74
|
list-files 列出页面的所有附件
|
|
76
75
|
get-content 获取页面的 HTML 内容
|
|
77
76
|
|
|
78
77
|
upload 选项:
|
|
79
78
|
--file 本地 .md 文件路径(必填)
|
|
79
|
+
--parent 父页面 ID(数字,必填)
|
|
80
80
|
--title 页面标题(默认取文件名)
|
|
81
|
-
--space Space Key(默认读 config.json)
|
|
82
|
-
--parent 父页面 ID 或标题
|
|
83
81
|
--dry-run 仅打印转换结果,不实际上传
|
|
82
|
+
--no-mermaid-to-img 禁用 Mermaid 图表自动渲染
|
|
84
83
|
|
|
85
84
|
update 选项:
|
|
86
85
|
--file 本地 .md 文件路径(必填)
|
|
87
|
-
--page-id 目标页面 ID
|
|
88
|
-
--title 按标题查找目标页面(与 --page-id 二选一)
|
|
89
|
-
--space Space Key(默认读 config.json)
|
|
86
|
+
--page-id 目标页面 ID(数字,必填)
|
|
90
87
|
--minor-edit 标记为小修改(不通知关注者)
|
|
88
|
+
--no-mermaid-to-img 禁用 Mermaid 图表自动渲染
|
|
91
89
|
|
|
92
90
|
upload-file 选项:
|
|
93
91
|
--page-id 目标页面 ID(必填)
|
|
@@ -95,32 +93,26 @@ upload-file 选项:
|
|
|
95
93
|
--name 覆盖文件名
|
|
96
94
|
|
|
97
95
|
list-pages 选项:
|
|
98
|
-
--
|
|
96
|
+
--parent 父页面 ID(数字,必填)
|
|
99
97
|
|
|
100
98
|
list-files 选项:
|
|
101
99
|
--page-id 页面 ID(必填)
|
|
102
100
|
|
|
103
101
|
get-content 选项:
|
|
104
102
|
--page-id 页面 ID(必填)
|
|
105
|
-
--markdown 输出为 Markdown
|
|
103
|
+
--markdown 输出为 Markdown 格式
|
|
106
104
|
|
|
107
105
|
配置参数(优先于环境变量):
|
|
108
|
-
--base-url
|
|
109
|
-
--pat
|
|
110
|
-
--username
|
|
111
|
-
--api-token
|
|
106
|
+
--base-url Confluence 服务地址
|
|
107
|
+
--pat Personal Access Token
|
|
108
|
+
--username Confluence 登录名
|
|
109
|
+
--api-token API Token 或密码
|
|
112
110
|
|
|
113
111
|
环境变量:
|
|
114
|
-
|
|
115
|
-
CONFLUENCE_BASE_URL Confluence 服务地址
|
|
116
|
-
|
|
112
|
+
CONFLUENCE_BASE_URL Confluence 服务地址
|
|
117
113
|
认证方式(二选一):
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
方式2 - Basic Auth:
|
|
122
|
-
CONFLUENCE_USERNAME Confluence 登录名(工号)
|
|
123
|
-
CONFLUENCE_API_TOKEN API Token 或密码
|
|
114
|
+
CONFLUENCE_PAT Personal Access Token
|
|
115
|
+
CONFLUENCE_USERNAME + CONFLUENCE_API_TOKEN Basic Auth
|
|
124
116
|
|
|
125
117
|
`);
|
|
126
118
|
}
|
|
@@ -155,9 +147,9 @@ async function main() {
|
|
|
155
147
|
const result = await uploadMarkdown({
|
|
156
148
|
filePath: args.file,
|
|
157
149
|
title: args.title,
|
|
158
|
-
spaceKey: args.space || defaultSpaceKey,
|
|
159
150
|
parentId: args.parent,
|
|
160
151
|
dryRun: args['dry-run'] === true,
|
|
152
|
+
mermaid: args['no-mermaid-to-img'] !== true,
|
|
161
153
|
});
|
|
162
154
|
if (result) {
|
|
163
155
|
console.log('\n页面信息:');
|
|
@@ -174,16 +166,15 @@ async function main() {
|
|
|
174
166
|
console.error('错误:--file 必填');
|
|
175
167
|
process.exit(1);
|
|
176
168
|
}
|
|
177
|
-
if (!args['page-id']
|
|
178
|
-
console.error('
|
|
169
|
+
if (!args['page-id']) {
|
|
170
|
+
console.error('错误:--page-id 必填');
|
|
179
171
|
process.exit(1);
|
|
180
172
|
}
|
|
181
173
|
const result = await updateMarkdown({
|
|
182
174
|
pageId: args['page-id'],
|
|
183
|
-
title: args.title,
|
|
184
175
|
filePath: args.file,
|
|
185
|
-
spaceKey: args.space || defaultSpaceKey,
|
|
186
176
|
minorEdit: args['minor-edit'] === true,
|
|
177
|
+
mermaid: args['no-mermaid-to-img'] !== true,
|
|
187
178
|
});
|
|
188
179
|
console.log('\n页面信息:');
|
|
189
180
|
console.log(` ID: ${result.id}`);
|
|
@@ -210,18 +201,22 @@ async function main() {
|
|
|
210
201
|
}
|
|
211
202
|
|
|
212
203
|
case 'list-pages': {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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);
|
|
216
211
|
if (!pages.length) {
|
|
217
|
-
console.log('
|
|
212
|
+
console.log(' (无子页面)');
|
|
218
213
|
} else {
|
|
219
214
|
const maxId = Math.max(...pages.map(p => String(p.id).length));
|
|
220
215
|
for (const p of pages) {
|
|
221
216
|
const id = String(p.id).padEnd(maxId);
|
|
222
217
|
console.log(` ${id} v${p.version?.number ?? '?'} ${p.title}`);
|
|
223
218
|
}
|
|
224
|
-
console.log(`\n共 ${pages.length}
|
|
219
|
+
console.log(`\n共 ${pages.length} 个子页面`);
|
|
225
220
|
}
|
|
226
221
|
break;
|
|
227
222
|
}
|