@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 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
- # 必填:Confluence 服务地址
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> [--title <标题>] [--space <key>] [--parent <ID或标题>] [--dry-run]
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> (--page-id <ID> | --title <标题>) [--space <key>] [--minor-edit]
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 [--space <key>]
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
- | `--space-key` | Space Key |
93
- | `--pat` | Personal Access Token |
94
- | `--username` | Confluence 登录名 |
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
- | `--page-id` | 页面 ID |
100
- | `--parent` | 父页面 ID 或标题 |
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 "项目文档" --space MYSPACE
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 --space MYSPACE
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 "项目文档" --space MYSPACE
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.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 页面格式。当用户需要上传、更新、获取 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> [--title <标题>] [--space <key>] [--parent <ID或标题>] [--dry-run]
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> (--page-id <ID> | --title <标题>) [--space <key>] [--minor-edit]
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 [--space <key>]
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
- # 上传 Markdown 文件
78
- confluence-sync upload --file README.md --title "项目文档" --space MYSPACE
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
 
@@ -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 = `![${name}](${name})`;
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
- * spaceKey + title 获取页面(仅返回第一个匹配)
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 listPages(spaceKey, limit = 25) {
140
+ export async function listChildPages(parentId, limit = 50) {
94
141
  ensureApi();
95
- const { data } = await api.get('/content', {
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(spaceKey, title, bodyHtml, parentId) {
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} [opts.spaceKey] - space key
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, spaceKey: spaceKeyOpt, parentId: parentRaw, dryRun = false } = opts;
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
- const md = fs.readFileSync(filePath, 'utf-8');
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(` Space: ${effectiveSpaceKey}`);
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
- const parentId = await resolveParentId(parentRaw, effectiveSpaceKey);
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
- const existing = await getPageByTitle(effectiveSpaceKey, title);
503
- if (existing) {
504
- console.log(` 页面「${title}」已存在 (ID: ${existing.id}),将更新…`);
505
- const updated = await updatePage(existing.id, title, bodyHtml, existing.version.number);
506
- console.log(` 更新完成 → ${baseUrl}/pages/viewpage.action?pageId=${updated.id}`);
507
- return updated;
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
- const page = await createPage(effectiveSpaceKey, title, bodyHtml, parentId);
511
- console.log(` ✅ 创建完成 → ${baseUrl}/pages/viewpage.action?pageId=${page.id}`);
512
- return page;
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
- * 更新指定页面(通过 ID 或 space+title 查找)
563
+ * 更新指定页面(通过 page-id 定位)
517
564
  */
518
565
  export async function updateMarkdown(opts) {
519
- const { pageId, title: searchTitle, filePath, spaceKey: spaceKeyOpt, minorEdit = false } = opts;
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
- const md = fs.readFileSync(filePath, 'utf-8');
524
- const bodyHtml = mdToConfluence(md);
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
- let page;
527
- if (pageId) {
528
- page = await getPageById(pageId);
529
- } else if (searchTitle) {
530
- page = await getPageByTitle(effectiveSpaceKey, searchTitle);
531
- if (!page) throw new Error(`未找到页面「${searchTitle}」(space: ${effectiveSpaceKey})`);
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
- console.log(` 目标页面:${page.title} (ID: ${page.id}, 当前版本: ${page.version.number})`);
537
- const updated = await updatePage(page.id, page.title, bodyHtml, page.version.number, minorEdit);
538
- console.log(` ✅ 更新完成 ${baseUrl}/pages/viewpage.action?pageId=${updated.id}`);
539
- return updated;
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
- * 环境变量(或通过 CLI 参数配置):
6
- * 必填: CONFLUENCE_BASE_URL - Confluence 服务地址
7
- * CONFLUENCE_SPACE_KEY - Space Key
8
- * 认证方式(二选一):
9
- * 方式1 PAT CONFLUENCE_PAT - Personal Access Token
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 AuthCONFLUENCE_USERNAME + CONFLUENCE_API_TOKEN
12
10
  *
13
11
  * 用法:
14
- * confluence-sync upload --file <path> [--title <标题>] [--space <key>] [--parent <ID或标题>] [--dry-run]
15
- * confluence-sync update --file <path> (--page-id <ID> | --title <标题>) [--space <key>] [--minor-edit]
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 [--space <key>]
18
- * confluence-sync list-files --page-id <ID>
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 Confluence 服务地址
23
- * --space-key Space Key
24
- * --pat Personal Access Token
25
- * --username Confluence 登录名
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
- listPages,
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 列出 space 下所有页面
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
- --space Space Key(默认读 config.json)
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 格式(默认输出 Storage HTML)
103
+ --markdown 输出为 Markdown 格式
108
104
 
109
105
  配置参数(优先于环境变量):
110
- --base-url Confluence 服务地址
111
- --space-key Space Key
112
- --pat Personal Access Token
113
- --username Confluence 登录名
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
- 方式1 - Personal Access Token:
123
- CONFLUENCE_PAT 个人访问令牌
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'] && !args.title) {
184
- console.error('错误:必须指定 --page-id 或 --title');
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
- const space = args.space || spaceKey;
220
- console.log(`Space: ${space}\n`);
221
- const pages = await listPages(space);
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
  }