@vdfor/confluence-sync 0.0.1 → 0.3.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 +148 -0
- package/package.json +6 -5
- package/skills/confluence-sync.md +101 -0
- package/src/confluence.mjs +112 -13
- package/upload.mjs +20 -7
package/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# @vdfor/confluence-sync
|
|
2
|
+
|
|
3
|
+
Confluence 文档同步 CLI 工具,支持将本地 Markdown 文件上传至 Confluence 并自动转换为 Confluence 页面格式。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# pnpm
|
|
9
|
+
pnpm add -g @vdfor/confluence-sync
|
|
10
|
+
|
|
11
|
+
# npm
|
|
12
|
+
npm install -g @vdfor/confluence-sync
|
|
13
|
+
|
|
14
|
+
# yarn
|
|
15
|
+
yarn global add @vdfor/confluence-sync
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 配置
|
|
19
|
+
|
|
20
|
+
### 方式1:环境变量
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# 必填:Confluence 服务地址
|
|
24
|
+
export CONFLUENCE_BASE_URL="https://your-confluence-instance.com"
|
|
25
|
+
|
|
26
|
+
# 必填:Space Key
|
|
27
|
+
export CONFLUENCE_SPACE_KEY="YOUR_SPACE_KEY"
|
|
28
|
+
|
|
29
|
+
# 认证方式(二选一)
|
|
30
|
+
# 方式1(推荐):Personal Access Token
|
|
31
|
+
export CONFLUENCE_PAT="your-personal-access-token"
|
|
32
|
+
|
|
33
|
+
# 方式2:Basic Auth
|
|
34
|
+
export CONFLUENCE_USERNAME="your-username"
|
|
35
|
+
export CONFLUENCE_API_TOKEN="your-api-token"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 方式2:CLI 参数(优先于环境变量)
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
confluence-sync upload --file test.md \
|
|
42
|
+
--base-url https://your-confluence-instance.com \
|
|
43
|
+
--space-key YOUR_SPACE_KEY \
|
|
44
|
+
--pat your-token
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## 命令
|
|
48
|
+
|
|
49
|
+
### upload - 上传 Markdown 为新页面
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
confluence-sync upload --file <path> [--title <标题>] [--space <key>] [--parent <ID或标题>] [--dry-run]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### update - 更新已有页面
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
confluence-sync update --file <path> (--page-id <ID> | --title <标题>) [--space <key>] [--minor-edit]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### upload-file - 上传附件
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
confluence-sync upload-file --page-id <ID> --file <path> [--name <文件名>]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### list-pages - 列出空间页面
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
confluence-sync list-pages [--space <key>]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### list-files - 列出页面附件
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
confluence-sync list-files --page-id <ID>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### get-content - 获取页面内容
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
confluence-sync get-content --page-id <ID> [--markdown]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
使用 `--markdown` 参数可将 Confluence 内容转换为 Markdown 格式输出。
|
|
86
|
+
|
|
87
|
+
## 配置参数
|
|
88
|
+
|
|
89
|
+
| 参数 | 说明 |
|
|
90
|
+
|------|------|
|
|
91
|
+
| `--base-url` | Confluence 服务地址 |
|
|
92
|
+
| `--space-key` | Space Key |
|
|
93
|
+
| `--pat` | Personal Access Token |
|
|
94
|
+
| `--username` | Confluence 登录名 |
|
|
95
|
+
| `--api-token` | API Token 或密码 |
|
|
96
|
+
| `--space` | Space Key(覆盖默认值) |
|
|
97
|
+
| `--file` | 本地文件路径 |
|
|
98
|
+
| `--title` | 页面标题 |
|
|
99
|
+
| `--page-id` | 页面 ID |
|
|
100
|
+
| `--parent` | 父页面 ID 或标题 |
|
|
101
|
+
| `--dry-run` | 仅打印转换结果,不实际上传 |
|
|
102
|
+
| `--minor-edit` | 标记为小修改(不通知关注者) |
|
|
103
|
+
| `--markdown` | get-content 时输出为 Markdown 格式 |
|
|
104
|
+
|
|
105
|
+
## 示例
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# 上传 Markdown 文件
|
|
109
|
+
confluence-sync upload --file README.md --title "项目文档" --space MYSPACE
|
|
110
|
+
|
|
111
|
+
# 更新已有页面
|
|
112
|
+
confluence-sync update --file README.md --page-id 123456
|
|
113
|
+
|
|
114
|
+
# 上传附件
|
|
115
|
+
confluence-sync upload-file --page-id 123456 --file image.png
|
|
116
|
+
|
|
117
|
+
# 列出空间所有页面
|
|
118
|
+
confluence-sync list-pages --space MYSPACE
|
|
119
|
+
|
|
120
|
+
# 获取页面内容
|
|
121
|
+
confluence-sync get-content --page-id 123456
|
|
122
|
+
|
|
123
|
+
# 获取页面内容并转换为 Markdown
|
|
124
|
+
confluence-sync get-content --page-id 123456 --markdown
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Skill 集成
|
|
128
|
+
|
|
129
|
+
本项目包含 Trae Skill,安装后可被智能体调用。
|
|
130
|
+
|
|
131
|
+
### 安装
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
pnpm add -g @vdfor/confluence-sync
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 使用
|
|
138
|
+
|
|
139
|
+
安装后,智能体可直接调用 `confluence-sync` 命令:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
confluence-sync upload --file README.md --title "项目文档" --space MYSPACE
|
|
143
|
+
confluence-sync get-content --page-id 123456 --markdown
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vdfor/confluence-sync",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.3.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"
|
|
@@ -37,9 +38,9 @@
|
|
|
37
38
|
"upload": "node upload.mjs upload",
|
|
38
39
|
"update": "node upload.mjs update",
|
|
39
40
|
"help": "node upload.mjs help",
|
|
40
|
-
"release:beta": "read -p '请输入版本号 (例如: patch, minor, major 或具体版本号): ' v && pnpm version $v && pnpm publish --
|
|
41
|
-
"release": "read -p '请输入版本号 (例如: patch, minor, major 或具体版本号): ' v && pnpm version $v && pnpm publish --
|
|
42
|
-
"v": "read -p '请输入版本号 (例如: patch, minor, major 或具体版本号): ' v && pnpm version $v",
|
|
41
|
+
"release:beta": "read -p '请输入版本号 (例如: patch, minor, major 或具体版本号): ' v && NPM_CONFIG_GIT_CHECKS=false pnpm version $v --no-git-tag-version --no-git-checks && pnpm publish --tag beta --access public --no-git-checks",
|
|
42
|
+
"release": "read -p '请输入版本号 (例如: patch, minor, major 或具体版本号): ' v && NPM_CONFIG_GIT_CHECKS=false pnpm version $v --no-git-tag-version --no-git-checks && pnpm publish --access public --no-git-checks",
|
|
43
|
+
"v": "read -p '请输入版本号 (例如: patch, minor, major 或具体版本号): ' v && pnpm version $v --no-git-tag-version --no-git-checks",
|
|
43
44
|
"clean": "rimraf dist node_modules"
|
|
44
45
|
}
|
|
45
46
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "confluence-sync"
|
|
3
|
+
description: "Confluence 文档同步 CLI 工具,支持将本地 Markdown 文件上传至 Confluence 并自动转换为 Confluence 页面格式。当用户需要上传、更新、获取 Confluence 页面内容或管理附件时调用。"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Confluence Sync
|
|
7
|
+
|
|
8
|
+
Confluence 文档同步 CLI 工具,支持将本地 Markdown 文件上传至 Confluence 并自动转换为 Confluence 页面格式。
|
|
9
|
+
|
|
10
|
+
## 功能特性
|
|
11
|
+
|
|
12
|
+
- 上传 Markdown 文件为 Confluence 页面
|
|
13
|
+
- 更新已有页面内容
|
|
14
|
+
- 上传文件作为页面附件
|
|
15
|
+
- 列出空间页面和页面附件
|
|
16
|
+
- 获取页面内容(支持转换为 Markdown 格式)
|
|
17
|
+
|
|
18
|
+
## 配置要求
|
|
19
|
+
|
|
20
|
+
使用前需要配置以下环境变量或 CLI 参数:
|
|
21
|
+
|
|
22
|
+
### 必填配置
|
|
23
|
+
|
|
24
|
+
- `CONFLUENCE_BASE_URL`: Confluence 服务地址
|
|
25
|
+
- `CONFLUENCE_SPACE_KEY`: Space Key
|
|
26
|
+
- 认证方式(二选一):
|
|
27
|
+
- `CONFLUENCE_PAT`: Personal Access Token(推荐)
|
|
28
|
+
- `CONFLUENCE_USERNAME` + `CONFLUENCE_API_TOKEN`: Basic Auth 认证
|
|
29
|
+
|
|
30
|
+
### 可选配置
|
|
31
|
+
|
|
32
|
+
- `--space`: Space Key(覆盖默认值)
|
|
33
|
+
|
|
34
|
+
## 命令列表
|
|
35
|
+
|
|
36
|
+
### upload - 上传 Markdown 为新页面
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
confluence-sync upload --file <path> [--title <标题>] [--space <key>] [--parent <ID或标题>] [--dry-run]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### update - 更新已有页面
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
confluence-sync update --file <path> (--page-id <ID> | --title <标题>) [--space <key>] [--minor-edit]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### upload-file - 上传附件
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
confluence-sync upload-file --page-id <ID> --file <path> [--name <文件名>]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### list-pages - 列出空间页面
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
confluence-sync list-pages [--space <key>]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### list-files - 列出页面附件
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
confluence-sync list-files --page-id <ID>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### get-content - 获取页面内容
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
confluence-sync get-content --page-id <ID> [--markdown]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
使用 `--markdown` 参数可将 Confluence 内容转换为 Markdown 格式输出。
|
|
73
|
+
|
|
74
|
+
## 使用示例
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# 上传 Markdown 文件
|
|
78
|
+
confluence-sync upload --file README.md --title "项目文档" --space MYSPACE
|
|
79
|
+
|
|
80
|
+
# 更新已有页面
|
|
81
|
+
confluence-sync update --file README.md --page-id 123456
|
|
82
|
+
|
|
83
|
+
# 上传附件
|
|
84
|
+
confluence-sync upload-file --page-id 123456 --file image.png
|
|
85
|
+
|
|
86
|
+
# 列出空间所有页面
|
|
87
|
+
confluence-sync list-pages --space MYSPACE
|
|
88
|
+
|
|
89
|
+
# 获取页面内容
|
|
90
|
+
confluence-sync get-content --page-id 123456
|
|
91
|
+
|
|
92
|
+
# 获取页面内容并转换为 Markdown
|
|
93
|
+
confluence-sync get-content --page-id 123456 --markdown
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## 注意事项
|
|
97
|
+
|
|
98
|
+
1. 使用前请确保已正确配置认证信息
|
|
99
|
+
2. Personal Access Token 需要在 Confluence 个人设置中生成
|
|
100
|
+
3. 上传附件时,如果附件已存在会自动更新
|
|
101
|
+
4. 使用 `--dry-run` 参数可以预览转换结果而不实际上传
|
package/src/confluence.mjs
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
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';
|
|
5
4
|
|
|
6
5
|
// ── 配置 ──────────────────────────────────────────────────────────────────────
|
|
7
|
-
const config = JSON.parse(readFileSync(new URL('../config.json', import.meta.url), 'utf-8'));
|
|
8
|
-
const { spaceKey: defaultSpaceKey } = config;
|
|
9
|
-
|
|
10
6
|
let baseUrl = process.env.CONFLUENCE_BASE_URL;
|
|
7
|
+
let spaceKey = process.env.CONFLUENCE_SPACE_KEY;
|
|
11
8
|
let pat = process.env.CONFLUENCE_PAT;
|
|
12
9
|
let username = process.env.CONFLUENCE_USERNAME;
|
|
13
10
|
let apiToken = process.env.CONFLUENCE_API_TOKEN;
|
|
@@ -17,12 +14,14 @@ let api;
|
|
|
17
14
|
* 用 CLI 参数覆盖环境变量配置,需在其他函数调用前执行
|
|
18
15
|
* @param {object} [opts]
|
|
19
16
|
* @param {string} [opts.baseUrl]
|
|
17
|
+
* @param {string} [opts.spaceKey]
|
|
20
18
|
* @param {string} [opts.pat]
|
|
21
19
|
* @param {string} [opts.username]
|
|
22
20
|
* @param {string} [opts.apiToken]
|
|
23
21
|
*/
|
|
24
22
|
export function configure(opts = {}) {
|
|
25
23
|
if (opts.baseUrl) baseUrl = opts.baseUrl;
|
|
24
|
+
if (opts.spaceKey) spaceKey = opts.spaceKey;
|
|
26
25
|
if (opts.pat) pat = opts.pat;
|
|
27
26
|
if (opts.username) username = opts.username;
|
|
28
27
|
if (opts.apiToken) apiToken = opts.apiToken;
|
|
@@ -271,6 +270,104 @@ export function mdToConfluence(md) {
|
|
|
271
270
|
return renderTokens(tokens);
|
|
272
271
|
}
|
|
273
272
|
|
|
273
|
+
// ── Confluence Storage Format → Markdown ─────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* 将 Confluence storage format(XHTML)转换为 Markdown
|
|
277
|
+
* @param {string} html - Confluence storage format HTML
|
|
278
|
+
* @returns {string} Markdown 文本
|
|
279
|
+
*/
|
|
280
|
+
export function confluenceToMd(html) {
|
|
281
|
+
if (!html) return '';
|
|
282
|
+
|
|
283
|
+
let md = html;
|
|
284
|
+
|
|
285
|
+
// 代码块:<ac:structured-macro ac:name="code">
|
|
286
|
+
md = md.replace(/<ac:structured-macro\s+ac:name="code">[\s\S]*?<ac:parameter\s+ac:name="language">([\s\S]*?)<\/ac:parameter>[\s\S]*?<ac:plain-text-body><!\[CDATA\[([\s\S]*?)\]\]><\/ac:plain-text-body>[\s\S]*?<\/ac:structured-macro>/g,
|
|
287
|
+
(_, lang, code) => `\n\`\`\`${lang}\n${code}\n\`\`\`\n`);
|
|
288
|
+
|
|
289
|
+
// 标题
|
|
290
|
+
md = md.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, '\n# $1\n');
|
|
291
|
+
md = md.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, '\n## $1\n');
|
|
292
|
+
md = md.replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, '\n### $1\n');
|
|
293
|
+
md = md.replace(/<h4[^>]*>([\s\S]*?)<\/h4>/gi, '\n#### $1\n');
|
|
294
|
+
md = md.replace(/<h5[^>]*>([\s\S]*?)<\/h5>/gi, '\n##### $1\n');
|
|
295
|
+
md = md.replace(/<h6[^>]*>([\s\S]*?)<\/h6>/gi, '\n###### $1\n');
|
|
296
|
+
|
|
297
|
+
// 粗体、斜体、删除线、行内代码
|
|
298
|
+
md = md.replace(/<strong>([\s\S]*?)<\/strong>/gi, '**$1**');
|
|
299
|
+
md = md.replace(/<b>([\s\S]*?)<\/b>/gi, '**$1**');
|
|
300
|
+
md = md.replace(/<em>([\s\S]*?)<\/em>/gi, '*$1*');
|
|
301
|
+
md = md.replace(/<i>([\s\S]*?)<\/i>/gi, '*$1*');
|
|
302
|
+
md = md.replace(/<s>([\s\S]*?)<\/s>/gi, '~~$1~~');
|
|
303
|
+
md = md.replace(/<del>([\s\S]*?)<\/del>/gi, '~~$1~~');
|
|
304
|
+
md = md.replace(/<code>([\s\S]*?)<\/code>/gi, '`$1`');
|
|
305
|
+
|
|
306
|
+
// 链接
|
|
307
|
+
md = md.replace(/<a\s+href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, '[$2]($1)');
|
|
308
|
+
|
|
309
|
+
// 图片:<ac:image><ri:attachment ri:filename="xxx" /></ac:image>
|
|
310
|
+
md = md.replace(/<ac:image>[\s\S]*?<ri:attachment\s+ri:filename="([^"]*)"[\s\S]*?\/>[\s\S]*?<\/ac:image>/gi, '');
|
|
311
|
+
|
|
312
|
+
// 普通图片
|
|
313
|
+
md = md.replace(/<img\s+[^>]*src="([^"]*)"[^>]*\/?>/gi, '');
|
|
314
|
+
|
|
315
|
+
// 水平线
|
|
316
|
+
md = md.replace(/<hr\s*\/?>/gi, '\n---\n');
|
|
317
|
+
|
|
318
|
+
// 换行
|
|
319
|
+
md = md.replace(/<br\s*\/?>/gi, '\n');
|
|
320
|
+
|
|
321
|
+
// 段落
|
|
322
|
+
md = md.replace(/<p>([\s\S]*?)<\/p>/gi, '\n$1\n');
|
|
323
|
+
|
|
324
|
+
// 引用块
|
|
325
|
+
md = md.replace(/<blockquote>([\s\S]*?)<\/blockquote>/gi, (_, content) => {
|
|
326
|
+
return content.split('\n').map(line => `> ${line}`).join('\n');
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// 无序列表
|
|
330
|
+
md = md.replace(/<ul>([\s\S]*?)<\/ul>/gi, (_, content) => {
|
|
331
|
+
return content.replace(/<li>([\s\S]*?)<\/li>/gi, '- $1\n');
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// 有序列表
|
|
335
|
+
md = md.replace(/<ol>([\s\S]*?)<\/ol>/gi, (_, content) => {
|
|
336
|
+
let i = 0;
|
|
337
|
+
return content.replace(/<li>([\s\S]*?)<\/li>/gi, () => `${++i}. $1\n`);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// 表格
|
|
341
|
+
md = md.replace(/<table>([\s\S]*?)<\/table>/gi, (_, tableContent) => {
|
|
342
|
+
let rows = [];
|
|
343
|
+
// 表头
|
|
344
|
+
const headerMatch = tableContent.match(/<thead>([\s\S]*?)<\/thead>/i);
|
|
345
|
+
if (headerMatch) {
|
|
346
|
+
const cells = headerMatch[1].match(/<th[^>]*>([\s\S]*?)<\/th>/gi) || [];
|
|
347
|
+
rows.push(cells.map(c => c.replace(/<th[^>]*>([\s\S]*?)<\/th>/i, '$1').trim()));
|
|
348
|
+
rows.push(rows[0].map(() => '---'));
|
|
349
|
+
}
|
|
350
|
+
// 表体
|
|
351
|
+
const bodyMatch = tableContent.match(/<tbody>([\s\S]*?)<\/tbody>/i);
|
|
352
|
+
if (bodyMatch) {
|
|
353
|
+
const trMatches = bodyMatch[1].match(/<tr>([\s\S]*?)<\/tr>/gi) || [];
|
|
354
|
+
for (const tr of trMatches) {
|
|
355
|
+
const cells = tr.match(/<td[^>]*>([\s\S]*?)<\/td>/gi) || [];
|
|
356
|
+
rows.push(cells.map(c => c.replace(/<td[^>]*>([\s\S]*?)<\/td>/i, '$1').trim()));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return '\n' + rows.map(r => '| ' + r.join(' | ') + ' |').join('\n') + '\n';
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// 移除剩余 HTML 标签
|
|
363
|
+
md = md.replace(/<[^>]+>/g, '');
|
|
364
|
+
|
|
365
|
+
// 清理多余空行
|
|
366
|
+
md = md.replace(/\n{3,}/g, '\n\n');
|
|
367
|
+
|
|
368
|
+
return md.trim();
|
|
369
|
+
}
|
|
370
|
+
|
|
274
371
|
// ── 文件上传(附件)────────────────────────────────────────────────────────────
|
|
275
372
|
|
|
276
373
|
/**
|
|
@@ -381,7 +478,8 @@ async function resolveParentId(parentIdentifier, spaceKey) {
|
|
|
381
478
|
* @param {boolean} [opts.dryRun] - 仅打印转换结果
|
|
382
479
|
*/
|
|
383
480
|
export async function uploadMarkdown(opts) {
|
|
384
|
-
const { filePath, title: optTitle, spaceKey
|
|
481
|
+
const { filePath, title: optTitle, spaceKey: spaceKeyOpt, parentId: parentRaw, dryRun = false } = opts;
|
|
482
|
+
const effectiveSpaceKey = spaceKeyOpt || spaceKey;
|
|
385
483
|
const fs = await import('node:fs');
|
|
386
484
|
const path = await import('node:path');
|
|
387
485
|
|
|
@@ -392,16 +490,16 @@ export async function uploadMarkdown(opts) {
|
|
|
392
490
|
if (dryRun) {
|
|
393
491
|
console.log('── 转换结果(dry-run)────────────────────────');
|
|
394
492
|
console.log(` 标题: ${title}`);
|
|
395
|
-
console.log(` Space: ${
|
|
493
|
+
console.log(` Space: ${effectiveSpaceKey}`);
|
|
396
494
|
console.log(' Body:\n' + bodyHtml);
|
|
397
495
|
return null;
|
|
398
496
|
}
|
|
399
497
|
|
|
400
498
|
// 查找父页面
|
|
401
|
-
const parentId = await resolveParentId(parentRaw,
|
|
499
|
+
const parentId = await resolveParentId(parentRaw, effectiveSpaceKey);
|
|
402
500
|
|
|
403
501
|
// 检查是否已存在同名页面
|
|
404
|
-
const existing = await getPageByTitle(
|
|
502
|
+
const existing = await getPageByTitle(effectiveSpaceKey, title);
|
|
405
503
|
if (existing) {
|
|
406
504
|
console.log(` 页面「${title}」已存在 (ID: ${existing.id}),将更新…`);
|
|
407
505
|
const updated = await updatePage(existing.id, title, bodyHtml, existing.version.number);
|
|
@@ -409,7 +507,7 @@ export async function uploadMarkdown(opts) {
|
|
|
409
507
|
return updated;
|
|
410
508
|
}
|
|
411
509
|
|
|
412
|
-
const page = await createPage(
|
|
510
|
+
const page = await createPage(effectiveSpaceKey, title, bodyHtml, parentId);
|
|
413
511
|
console.log(` ✅ 创建完成 → ${baseUrl}/pages/viewpage.action?pageId=${page.id}`);
|
|
414
512
|
return page;
|
|
415
513
|
}
|
|
@@ -418,7 +516,8 @@ export async function uploadMarkdown(opts) {
|
|
|
418
516
|
* 更新指定页面(通过 ID 或 space+title 查找)
|
|
419
517
|
*/
|
|
420
518
|
export async function updateMarkdown(opts) {
|
|
421
|
-
const { pageId, title: searchTitle, filePath, spaceKey
|
|
519
|
+
const { pageId, title: searchTitle, filePath, spaceKey: spaceKeyOpt, minorEdit = false } = opts;
|
|
520
|
+
const effectiveSpaceKey = spaceKeyOpt || spaceKey;
|
|
422
521
|
const fs = await import('node:fs');
|
|
423
522
|
|
|
424
523
|
const md = fs.readFileSync(filePath, 'utf-8');
|
|
@@ -428,8 +527,8 @@ export async function updateMarkdown(opts) {
|
|
|
428
527
|
if (pageId) {
|
|
429
528
|
page = await getPageById(pageId);
|
|
430
529
|
} else if (searchTitle) {
|
|
431
|
-
page = await getPageByTitle(
|
|
432
|
-
if (!page) throw new Error(`未找到页面「${searchTitle}」(space: ${
|
|
530
|
+
page = await getPageByTitle(effectiveSpaceKey, searchTitle);
|
|
531
|
+
if (!page) throw new Error(`未找到页面「${searchTitle}」(space: ${effectiveSpaceKey})`);
|
|
433
532
|
} else {
|
|
434
533
|
throw new Error('必须指定 --page-id 或 --title 来定位目标页面');
|
|
435
534
|
}
|
|
@@ -441,5 +540,5 @@ export async function updateMarkdown(opts) {
|
|
|
441
540
|
}
|
|
442
541
|
|
|
443
542
|
// ── 导出配置 ──────────────────────────────────────────────────────────────────
|
|
444
|
-
export { defaultSpaceKey };
|
|
445
543
|
export function getBaseUrl() { return baseUrl; }
|
|
544
|
+
export function getSpaceKey() { return spaceKey; }
|
package/upload.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* 环境变量(或通过 CLI 参数配置):
|
|
6
6
|
* 必填: CONFLUENCE_BASE_URL - Confluence 服务地址
|
|
7
|
+
* CONFLUENCE_SPACE_KEY - Space Key
|
|
7
8
|
* 认证方式(二选一):
|
|
8
9
|
* 方式1 PAT: CONFLUENCE_PAT - Personal Access Token
|
|
9
10
|
* 方式2 Basic Auth: CONFLUENCE_USERNAME - Confluence 登录名(工号)
|
|
@@ -15,10 +16,11 @@
|
|
|
15
16
|
* confluence-sync upload-file --page-id <ID> --file <path> [--name <文件名>]
|
|
16
17
|
* confluence-sync list-pages [--space <key>]
|
|
17
18
|
* confluence-sync list-files --page-id <ID>
|
|
18
|
-
* confluence-sync get-content --page-id <ID>
|
|
19
|
+
* confluence-sync get-content --page-id <ID> [--markdown]
|
|
19
20
|
*
|
|
20
21
|
* 可选配置参数(优先于环境变量):
|
|
21
22
|
* --base-url Confluence 服务地址
|
|
23
|
+
* --space-key Space Key
|
|
22
24
|
* --pat Personal Access Token
|
|
23
25
|
* --username Confluence 登录名
|
|
24
26
|
* --api-token API Token 或密码
|
|
@@ -31,8 +33,9 @@ import {
|
|
|
31
33
|
listPages,
|
|
32
34
|
listAttachments,
|
|
33
35
|
getPageContent,
|
|
34
|
-
|
|
36
|
+
confluenceToMd,
|
|
35
37
|
getBaseUrl,
|
|
38
|
+
getSpaceKey,
|
|
36
39
|
configure,
|
|
37
40
|
} from './src/confluence.mjs';
|
|
38
41
|
|
|
@@ -101,9 +104,11 @@ list-files 选项:
|
|
|
101
104
|
|
|
102
105
|
get-content 选项:
|
|
103
106
|
--page-id 页面 ID(必填)
|
|
107
|
+
--markdown 输出为 Markdown 格式(默认输出 Storage HTML)
|
|
104
108
|
|
|
105
109
|
配置参数(优先于环境变量):
|
|
106
110
|
--base-url Confluence 服务地址
|
|
111
|
+
--space-key Space Key
|
|
107
112
|
--pat Personal Access Token
|
|
108
113
|
--username Confluence 登录名
|
|
109
114
|
--api-token API Token 或密码
|
|
@@ -111,6 +116,7 @@ get-content 选项:
|
|
|
111
116
|
环境变量:
|
|
112
117
|
必填:
|
|
113
118
|
CONFLUENCE_BASE_URL Confluence 服务地址
|
|
119
|
+
CONFLUENCE_SPACE_KEY Space Key
|
|
114
120
|
|
|
115
121
|
认证方式(二选一):
|
|
116
122
|
方式1 - Personal Access Token:
|
|
@@ -137,11 +143,13 @@ async function main() {
|
|
|
137
143
|
// 用 CLI 参数覆盖环境变量配置
|
|
138
144
|
configure({
|
|
139
145
|
baseUrl: args['base-url'],
|
|
146
|
+
spaceKey: args['space-key'],
|
|
140
147
|
pat: args['pat'],
|
|
141
148
|
username: args['username'],
|
|
142
149
|
apiToken: args['api-token'],
|
|
143
150
|
});
|
|
144
151
|
const baseUrl = getBaseUrl();
|
|
152
|
+
const spaceKey = getSpaceKey();
|
|
145
153
|
|
|
146
154
|
try {
|
|
147
155
|
switch (command) {
|
|
@@ -153,7 +161,7 @@ async function main() {
|
|
|
153
161
|
const result = await uploadMarkdown({
|
|
154
162
|
filePath: args.file,
|
|
155
163
|
title: args.title,
|
|
156
|
-
spaceKey: args.space ||
|
|
164
|
+
spaceKey: args.space || spaceKey,
|
|
157
165
|
parentId: args.parent,
|
|
158
166
|
dryRun: args['dry-run'] === true,
|
|
159
167
|
});
|
|
@@ -180,7 +188,7 @@ async function main() {
|
|
|
180
188
|
pageId: args['page-id'],
|
|
181
189
|
title: args.title,
|
|
182
190
|
filePath: args.file,
|
|
183
|
-
spaceKey: args.space ||
|
|
191
|
+
spaceKey: args.space || spaceKey,
|
|
184
192
|
minorEdit: args['minor-edit'] === true,
|
|
185
193
|
});
|
|
186
194
|
console.log('\n页面信息:');
|
|
@@ -208,7 +216,7 @@ async function main() {
|
|
|
208
216
|
}
|
|
209
217
|
|
|
210
218
|
case 'list-pages': {
|
|
211
|
-
const space = args.space ||
|
|
219
|
+
const space = args.space || spaceKey;
|
|
212
220
|
console.log(`Space: ${space}\n`);
|
|
213
221
|
const pages = await listPages(space);
|
|
214
222
|
if (!pages.length) {
|
|
@@ -252,8 +260,13 @@ async function main() {
|
|
|
252
260
|
console.log(`标题: ${page.title}`);
|
|
253
261
|
console.log(`Space: ${page.space?.key}`);
|
|
254
262
|
console.log(`版本: ${page.version?.number}`);
|
|
255
|
-
|
|
256
|
-
|
|
263
|
+
if (args.markdown) {
|
|
264
|
+
console.log(`\n── Markdown ──────────────────────────────\n`);
|
|
265
|
+
console.log(confluenceToMd(page.body.storage.value));
|
|
266
|
+
} else {
|
|
267
|
+
console.log(`\n── Storage HTML ──────────────────────────────\n`);
|
|
268
|
+
console.log(page.body.storage.value);
|
|
269
|
+
}
|
|
257
270
|
break;
|
|
258
271
|
}
|
|
259
272
|
|