@chenguangyao/devflow-kit 0.1.43
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/CHANGELOG.md +232 -0
- package/LICENSE +21 -0
- package/README.md +539 -0
- package/bin/devflow.js +9 -0
- package/docs/RFC-001-devflow-kit.md +617 -0
- package/docs/RFC-002-workflow-kernel.md +134 -0
- package/docs/enterprise-integration-supplement.md +274 -0
- package/docs/internal-gitlab-setup.md +426 -0
- package/docs/marketplace-skills.md +231 -0
- package/docs/migration-from-arb.md +232 -0
- package/docs/tooling-overview.md +774 -0
- package/docs/workflow-orchestration.md +695 -0
- package/docs/workflow-ui-prototype.html +271 -0
- package/package.json +52 -0
- package/schemas/config.schema.json +51 -0
- package/schemas/delta.schema.json +22 -0
- package/schemas/state.schema.json +130 -0
- package/schemas/status-surface.schema.json +197 -0
- package/schemas/workflow-confirmation-surface.schema.json +70 -0
- package/schemas/workflow-picker.schema.json +94 -0
- package/scripts/postinstall.js +101 -0
- package/scripts/render-workflow-ui-prototype.js +271 -0
- package/skills/apply/SKILL.md +313 -0
- package/skills/apply/references/discipline-checklist.md +145 -0
- package/skills/apply/references/subagent-implementer-prompt.md +113 -0
- package/skills/apply/references/subagent-orchestration.md +150 -0
- package/skills/apply/references/subagent-reviewer-prompt.md +180 -0
- package/skills/apply/references/tdd-loop.md +287 -0
- package/skills/apply/references/when-plan-is-wrong.md +279 -0
- package/skills/apply/references/worktree-swarm.md +292 -0
- package/skills/archive/SKILL.md +229 -0
- package/skills/archive/references/conflict-resolution.md +336 -0
- package/skills/archive/references/knowledge-deposit.md +381 -0
- package/skills/archive/references/spec-merge.md +365 -0
- package/skills/brainstorm/SKILL.md +123 -0
- package/skills/brainstorm/references/proposal-template.md +244 -0
- package/skills/brainstorm/references/question-catalog.md +168 -0
- package/skills/brainstorm/references/session-template.md +184 -0
- package/skills/ci-fix/SKILL.md +63 -0
- package/skills/ci-fix/references/loop.md +25 -0
- package/skills/code-review/SKILL.md +279 -0
- package/skills/code-review/references/escalation-playbook.md +192 -0
- package/skills/code-review/references/language-cheatsheets/go.md +175 -0
- package/skills/code-review/references/language-cheatsheets/java-spring-mybatis.md +246 -0
- package/skills/code-review/references/language-cheatsheets/python.md +170 -0
- package/skills/code-review/references/language-cheatsheets/vue.md +199 -0
- package/skills/code-review/references/output-template.md +275 -0
- package/skills/code-review/references/review-checklist.md +251 -0
- package/skills/complexity-grading/SKILL.md +259 -0
- package/skills/deliver/SKILL.md +271 -0
- package/skills/deliver/references/delivery-modes.md +299 -0
- package/skills/deliver/references/notify.md +359 -0
- package/skills/deliver/references/pr-description.md +319 -0
- package/skills/dependency-upgrade/SKILL.md +57 -0
- package/skills/dependency-upgrade/references/risk-matrix.md +38 -0
- package/skills/df-orchestrator/SKILL.md +407 -0
- package/skills/df-orchestrator/references/complexity-grading.md +177 -0
- package/skills/df-orchestrator/references/escalation-matrix.md +191 -0
- package/skills/df-orchestrator/references/routing-rules.md +290 -0
- package/skills/df-orchestrator/references/workflow-state-machine.md +208 -0
- package/skills/frontend-quality/SKILL.md +61 -0
- package/skills/frontend-quality/references/checklist.md +35 -0
- package/skills/handoff-resume/SKILL.md +59 -0
- package/skills/handoff-resume/references/handoff-template.md +54 -0
- package/skills/plan/SKILL.md +166 -0
- package/skills/plan/references/task-breakdown.md +207 -0
- package/skills/plan/references/task-sequencing.md +143 -0
- package/skills/plan/references/task-template.md +248 -0
- package/skills/requirement-analysis/SKILL.md +499 -0
- package/skills/requirement-analysis/references/acceptance-criteria.md +183 -0
- package/skills/requirement-analysis/references/code-recon.md +151 -0
- package/skills/requirement-analysis/references/edge-case-catalog.md +164 -0
- package/skills/requirement-analysis/references/requirement-template.md +339 -0
- package/skills/requirement-analysis/references/scope-negotiation.md +162 -0
- package/skills/security-hardening/SKILL.md +60 -0
- package/skills/security-hardening/references/checklist.md +42 -0
- package/skills/tech-spec/SKILL.md +388 -0
- package/skills/tech-spec/references/api-contract-design.md +172 -0
- package/skills/tech-spec/references/decision-records.md +110 -0
- package/skills/tech-spec/references/design-template.md +301 -0
- package/skills/tech-spec/references/rollout-and-rollback.md +203 -0
- package/skills/tech-spec/references/spec-delta-conventions.md +250 -0
- package/skills/tech-spec/references/transaction-patterns.md +212 -0
- package/skills/test-spec/SKILL.md +219 -0
- package/skills/test-spec/references/coverage-strategy.md +218 -0
- package/skills/test-spec/references/edge-case-to-test.md +143 -0
- package/skills/test-spec/references/test-case-template.md +276 -0
- package/skills/verify/SKILL.md +232 -0
- package/skills/verify/references/nfr-verification.md +292 -0
- package/skills/verify/references/report-templates.md +510 -0
- package/skills/verify/references/self-test-guide.md +240 -0
- package/skills/verify/references/verify-rollback-map.md +247 -0
- package/src/cli/commands/_helpers.js +108 -0
- package/src/cli/commands/_submit.js +718 -0
- package/src/cli/commands/apply.js +198 -0
- package/src/cli/commands/archive.js +180 -0
- package/src/cli/commands/checkpoint.js +113 -0
- package/src/cli/commands/deliver.js +377 -0
- package/src/cli/commands/deploy.js +504 -0
- package/src/cli/commands/design.js +158 -0
- package/src/cli/commands/disable.js +21 -0
- package/src/cli/commands/doctor.js +178 -0
- package/src/cli/commands/enable.js +21 -0
- package/src/cli/commands/flow.js +645 -0
- package/src/cli/commands/help.js +93 -0
- package/src/cli/commands/ingest.js +602 -0
- package/src/cli/commands/init.js +341 -0
- package/src/cli/commands/knowledge.js +523 -0
- package/src/cli/commands/logs.js +43 -0
- package/src/cli/commands/new.js +202 -0
- package/src/cli/commands/plan.js +49 -0
- package/src/cli/commands/propose.js +27 -0
- package/src/cli/commands/provider.js +698 -0
- package/src/cli/commands/report.js +143 -0
- package/src/cli/commands/requirement.js +227 -0
- package/src/cli/commands/review.js +301 -0
- package/src/cli/commands/skills.js +457 -0
- package/src/cli/commands/status.js +925 -0
- package/src/cli/commands/switch.js +27 -0
- package/src/cli/commands/sync.js +47 -0
- package/src/cli/commands/test.js +366 -0
- package/src/cli/commands/uninstall.js +32 -0
- package/src/cli/commands/update.js +74 -0
- package/src/cli/commands/verify.js +354 -0
- package/src/cli/commands/worktree.js +78 -0
- package/src/cli/index.js +72 -0
- package/src/cli/parse-args.js +102 -0
- package/src/core/autodetect.js +271 -0
- package/src/core/change.js +208 -0
- package/src/core/checkpoint.js +217 -0
- package/src/core/config.js +60 -0
- package/src/core/delta.js +290 -0
- package/src/core/markers.js +59 -0
- package/src/core/paths.js +173 -0
- package/src/core/plan-tasks.js +36 -0
- package/src/core/project-routing.js +285 -0
- package/src/core/projects.js +200 -0
- package/src/core/state.js +200 -0
- package/src/core/workflow-check.js +177 -0
- package/src/core/workflow-init.js +34 -0
- package/src/core/workflow-picker.js +154 -0
- package/src/core/workflow-policy.js +119 -0
- package/src/core/workflow-suggest.js +181 -0
- package/src/core/workflow-verify.js +88 -0
- package/src/core/workflow.js +433 -0
- package/src/core/worktree.js +241 -0
- package/src/knowledge/categories.js +107 -0
- package/src/knowledge/classify.js +125 -0
- package/src/knowledge/deposit.js +414 -0
- package/src/knowledge/migrate.js +149 -0
- package/src/knowledge/mr.js +219 -0
- package/src/knowledge/query.js +131 -0
- package/src/knowledge/registry.js +151 -0
- package/src/knowledge/sync.js +179 -0
- package/src/providers/base.js +74 -0
- package/src/providers/drivers/api-yapi.js +78 -0
- package/src/providers/drivers/ci-jenkins.js +109 -0
- package/src/providers/drivers/intake-confluence.js +544 -0
- package/src/providers/drivers/kb-git.js +549 -0
- package/src/providers/drivers/kb-weknora.js +472 -0
- package/src/providers/drivers/notify-smtp.js +515 -0
- package/src/providers/drivers/observability-oss.js +43 -0
- package/src/providers/drivers/observability-sls.js +50 -0
- package/src/providers/lifecycle.js +135 -0
- package/src/providers/loader.js +132 -0
- package/src/providers/local.js +190 -0
- package/src/providers/userconfig.js +283 -0
- package/src/reports/aggregate.js +185 -0
- package/src/reports/coverage.js +163 -0
- package/src/reports/detect.js +143 -0
- package/src/reports/parse.js +236 -0
- package/src/templates/files/ci/github.yml +38 -0
- package/src/templates/files/ci/gitlab.yml +27 -0
- package/src/templates/files/design.md +63 -0
- package/src/templates/files/ide/devflow-workflow.md +58 -0
- package/src/templates/files/ide/project-overview-reference.md +1 -0
- package/src/templates/files/ide/project-overview.md +27 -0
- package/src/templates/files/knowledge-index.json +17 -0
- package/src/templates/files/knowledge.md +28 -0
- package/src/templates/files/meta.json +8 -0
- package/src/templates/files/plan.md +38 -0
- package/src/templates/files/proposal.md +33 -0
- package/src/templates/files/reports/contract-test.md +40 -0
- package/src/templates/files/reports/e2e-test.md +30 -0
- package/src/templates/files/reports/integration-test.md +36 -0
- package/src/templates/files/reports/joint-test.md +58 -0
- package/src/templates/files/reports/perf.md +24 -0
- package/src/templates/files/reports/regression.md +20 -0
- package/src/templates/files/reports/remote-test.md +55 -0
- package/src/templates/files/reports/self-test.md +43 -0
- package/src/templates/files/reports/smoke-test.md +22 -0
- package/src/templates/files/reports/unit-test.md +36 -0
- package/src/templates/files/requirement.md +51 -0
- package/src/templates/files/review.md +38 -0
- package/src/templates/files/tests.md +36 -0
- package/src/templates/files/verify.md +32 -0
- package/src/templates/index.js +21 -0
- package/src/utils/log.js +37 -0
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Confluence intake driver — zero external dependencies (Node built-in https/http).
|
|
4
|
+
*
|
|
5
|
+
* Supported auth:
|
|
6
|
+
* PAT (default) → Authorization: Bearer <token>
|
|
7
|
+
* Basic → Authorization: Basic base64(user:token) (set config.user)
|
|
8
|
+
*
|
|
9
|
+
* Supported URL formats:
|
|
10
|
+
* Server / Data Center:
|
|
11
|
+
* https://confluence.corp.com/display/SPACE/Page+Title
|
|
12
|
+
* https://confluence.corp.com/pages/viewpage.action?pageId=12345
|
|
13
|
+
* Cloud:
|
|
14
|
+
* https://myorg.atlassian.net/wiki/spaces/SPACE/pages/12345/Page+Title
|
|
15
|
+
*
|
|
16
|
+
* config shape:
|
|
17
|
+
* {
|
|
18
|
+
* baseUrl: string (required) base URL of your Confluence instance
|
|
19
|
+
* token: string (required) PAT or API token; prefer ${env:CONFLUENCE_TOKEN}
|
|
20
|
+
* user: string (optional) email/username → Basic Auth; omit → Bearer/PAT
|
|
21
|
+
* timeoutMs: number (optional, default 15000)
|
|
22
|
+
* }
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const { Provider, AuthExpiredError } = require('../base.js');
|
|
26
|
+
const log = require('../../utils/log.js');
|
|
27
|
+
|
|
28
|
+
class ConfluenceIntakeProvider extends Provider {
|
|
29
|
+
constructor({ name, type, driver, config }) {
|
|
30
|
+
super({ name, type, driver, config });
|
|
31
|
+
this.baseUrl = (config.baseUrl || '').replace(/\/+$/, '');
|
|
32
|
+
this.token = config.token || '';
|
|
33
|
+
this.user = config.user || '';
|
|
34
|
+
this.timeoutMs = toInt(config.timeoutMs, 15000);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Match any URL that starts with our configured baseUrl.
|
|
38
|
+
supports(input) {
|
|
39
|
+
if (!this.baseUrl) return false;
|
|
40
|
+
return typeof input === 'string' && input.startsWith(this.baseUrl);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Open the browser to the PAT generation page, then prompt for the token
|
|
45
|
+
* and save it back to providers.json via the rotate flow.
|
|
46
|
+
*
|
|
47
|
+
* Called by `devflow provider relogin intake.confluence`.
|
|
48
|
+
*/
|
|
49
|
+
async login({ interactive = true } = {}) {
|
|
50
|
+
const base = this.baseUrl;
|
|
51
|
+
log.raw('');
|
|
52
|
+
if (base.includes('.atlassian.net')) {
|
|
53
|
+
const url = 'https://id.atlassian.com/manage/api-tokens';
|
|
54
|
+
log.info(`Confluence Cloud — 正在打开 API Token 生成页面:`);
|
|
55
|
+
log.dim(` ${url}`);
|
|
56
|
+
openBrowser(url);
|
|
57
|
+
} else {
|
|
58
|
+
// Server/DC: PAT 路径因版本和插件而异,列出几种常见路径供用户选择
|
|
59
|
+
log.info(`Confluence Server/DC — 请在浏览器里按以下顺序尝试找到 Personal Access Tokens:`);
|
|
60
|
+
log.raw(` 1) 右上角头像 → Profile → Personal Access Tokens`);
|
|
61
|
+
log.raw(` 2) ${base}/plugins/servlet/de.resolution.apitokenauth/token`);
|
|
62
|
+
log.raw(` 3) 若以上都找不到,说明 Confluence 版本较旧,请使用 Basic Auth:`);
|
|
63
|
+
log.raw(` 在 devflow provider rotate intake.confluence 时`);
|
|
64
|
+
log.raw(` token 填登录密码,并额外设置 user 为你的用户名`);
|
|
65
|
+
log.raw('');
|
|
66
|
+
openBrowser(`${base}/plugins/servlet/de.resolution.apitokenauth/token`);
|
|
67
|
+
}
|
|
68
|
+
if (interactive) {
|
|
69
|
+
log.dim('配置完成后运行:devflow provider rotate intake.confluence');
|
|
70
|
+
}
|
|
71
|
+
log.raw('');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async validate() {
|
|
75
|
+
if (!this.baseUrl) return { ok: false, reason: 'baseUrl not configured' };
|
|
76
|
+
if (!this.token) return { ok: false, reason: 'token not configured' };
|
|
77
|
+
try {
|
|
78
|
+
// Lightweight probe — list 1 space. Works on both Server and Cloud.
|
|
79
|
+
await this._get('/rest/api/space?limit=1');
|
|
80
|
+
return { ok: true };
|
|
81
|
+
} catch (e) {
|
|
82
|
+
if (e.statusCode === 401) return { ok: false, reason: 'auth failed (401)', needsLogin: true };
|
|
83
|
+
if (e.statusCode === 403) return { ok: false, reason: 'permission denied (403)' };
|
|
84
|
+
return { ok: false, reason: e.message };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async fetch(url) {
|
|
89
|
+
const pageId = extractPageId(url);
|
|
90
|
+
let data;
|
|
91
|
+
|
|
92
|
+
if (pageId) {
|
|
93
|
+
data = await this._get(`/rest/api/content/${pageId}?expand=body.storage,version,space`);
|
|
94
|
+
} else {
|
|
95
|
+
// Fall back: derive space key + title from URL path and search
|
|
96
|
+
const { spaceKey, title } = extractSpaceTitle(url);
|
|
97
|
+
if (!spaceKey || !title) {
|
|
98
|
+
throw new Error(`Cannot extract page ID or space/title from URL: ${url}`);
|
|
99
|
+
}
|
|
100
|
+
const cql = `title="${encodeURIComponent(title)}" AND space="${spaceKey}" AND type=page`;
|
|
101
|
+
const res = await this._get(
|
|
102
|
+
`/rest/api/content?cql=${encodeURIComponent(cql)}&expand=body.storage,version,space&limit=1`,
|
|
103
|
+
);
|
|
104
|
+
if (!res.results || !res.results.length) {
|
|
105
|
+
// Retry with /rest/api/content?title=...&spaceKey=...
|
|
106
|
+
const res2 = await this._get(
|
|
107
|
+
`/rest/api/content?title=${encodeURIComponent(title)}&spaceKey=${encodeURIComponent(spaceKey)}&expand=body.storage,version,space&limit=1`,
|
|
108
|
+
);
|
|
109
|
+
if (!res2.results || !res2.results.length) {
|
|
110
|
+
throw new Error(`Page not found in space ${spaceKey}: "${title}"`);
|
|
111
|
+
}
|
|
112
|
+
data = res2.results[0];
|
|
113
|
+
} else {
|
|
114
|
+
data = res.results[0];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const title = data.title || url;
|
|
119
|
+
const contentId = data.id || pageId;
|
|
120
|
+
const stored = (data.body && data.body.storage && data.body.storage.value) || '';
|
|
121
|
+
const body_md = confluenceStorageToMd(stored);
|
|
122
|
+
const attachments = contentId ? await this._fetchImageAttachments(contentId) : [];
|
|
123
|
+
return { title, body_md, attachments, source: { type: 'url', ref: url } };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async search(q) {
|
|
127
|
+
try {
|
|
128
|
+
const cql = `text~"${q}" AND type=page`;
|
|
129
|
+
const res = await this._get(
|
|
130
|
+
`/rest/api/content/search?cql=${encodeURIComponent(cql)}&limit=10&expand=space`,
|
|
131
|
+
);
|
|
132
|
+
return (res.results || []).map((r) => ({
|
|
133
|
+
title: r.title,
|
|
134
|
+
url: this.baseUrl + ((r._links && r._links.webui) || ''),
|
|
135
|
+
snippet: (r.excerpt || '').replace(/<[^>]+>/g, '').slice(0, 200),
|
|
136
|
+
}));
|
|
137
|
+
} catch { return []; }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── internal helpers ──────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
_authHeader() {
|
|
143
|
+
if (this.user) {
|
|
144
|
+
return 'Basic ' + Buffer.from(`${this.user}:${this.token}`).toString('base64');
|
|
145
|
+
}
|
|
146
|
+
return `Bearer ${this.token}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
_get(apiPath) {
|
|
150
|
+
return new Promise((resolve, reject) => {
|
|
151
|
+
let urlStr = this.baseUrl + apiPath;
|
|
152
|
+
// Cloud Confluence REST path lives under /wiki/
|
|
153
|
+
if (this.baseUrl.includes('.atlassian.net') && !apiPath.startsWith('/wiki')) {
|
|
154
|
+
urlStr = this.baseUrl + '/wiki' + apiPath;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let parsed;
|
|
158
|
+
try { parsed = new URL(urlStr); }
|
|
159
|
+
catch (e) { return reject(new Error(`Invalid Confluence URL: ${urlStr}`)); }
|
|
160
|
+
|
|
161
|
+
const mod = parsed.protocol === 'https:' ? require('https') : require('http');
|
|
162
|
+
const opts = {
|
|
163
|
+
hostname: parsed.hostname,
|
|
164
|
+
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
165
|
+
path: parsed.pathname + parsed.search,
|
|
166
|
+
method: 'GET',
|
|
167
|
+
headers: {
|
|
168
|
+
Authorization: this._authHeader(),
|
|
169
|
+
Accept: 'application/json',
|
|
170
|
+
'User-Agent': 'devflow-kit/1.0',
|
|
171
|
+
},
|
|
172
|
+
timeout: this.timeoutMs,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const req = mod.request(opts, (res) => {
|
|
176
|
+
const chunks = [];
|
|
177
|
+
res.on('data', (c) => chunks.push(c));
|
|
178
|
+
res.on('end', () => {
|
|
179
|
+
const body = Buffer.concat(chunks).toString('utf8');
|
|
180
|
+
if (res.statusCode === 401) {
|
|
181
|
+
const err = new Error('Confluence auth failed (401) — check token/user');
|
|
182
|
+
err.statusCode = 401;
|
|
183
|
+
return reject(err);
|
|
184
|
+
}
|
|
185
|
+
if (res.statusCode >= 400) {
|
|
186
|
+
const err = new Error(`Confluence API error ${res.statusCode}: ${body.slice(0, 300)}`);
|
|
187
|
+
err.statusCode = res.statusCode;
|
|
188
|
+
return reject(err);
|
|
189
|
+
}
|
|
190
|
+
try { resolve(JSON.parse(body)); }
|
|
191
|
+
catch (e) { reject(new Error(`Non-JSON Confluence response: ${body.slice(0, 200)}`)); }
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
req.on('timeout', () => {
|
|
196
|
+
req.destroy();
|
|
197
|
+
reject(new Error(`Confluence request timed out (${this.timeoutMs}ms): ${urlStr}`));
|
|
198
|
+
});
|
|
199
|
+
req.on('error', reject);
|
|
200
|
+
req.end();
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async _fetchImageAttachments(contentId) {
|
|
205
|
+
let res;
|
|
206
|
+
try {
|
|
207
|
+
res = await this._get(`/rest/api/content/${contentId}/child/attachment?limit=100&expand=metadata`);
|
|
208
|
+
} catch {
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
const out = [];
|
|
212
|
+
for (const att of res.results || []) {
|
|
213
|
+
const filename = att.title || '';
|
|
214
|
+
const mediaType = (att.metadata && att.metadata.mediaType) || '';
|
|
215
|
+
if (!isImageAttachment(filename, mediaType)) continue;
|
|
216
|
+
const download = att._links && att._links.download;
|
|
217
|
+
if (!download) continue;
|
|
218
|
+
try {
|
|
219
|
+
const data = await this._getBinary(download);
|
|
220
|
+
out.push({ filename, mediaType, data });
|
|
221
|
+
} catch {
|
|
222
|
+
// Keep ingest resilient: missing image permissions should not block text intake.
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
_getBinary(downloadPathOrUrl, redirects = 3) {
|
|
229
|
+
return new Promise((resolve, reject) => {
|
|
230
|
+
let urlStr = downloadPathOrUrl;
|
|
231
|
+
if (!/^https?:\/\//i.test(urlStr)) {
|
|
232
|
+
urlStr = this.baseUrl + (urlStr.startsWith('/') ? urlStr : '/' + urlStr);
|
|
233
|
+
}
|
|
234
|
+
if (this.baseUrl.includes('.atlassian.net') && !new URL(urlStr).pathname.startsWith('/wiki/')) {
|
|
235
|
+
const u = new URL(urlStr);
|
|
236
|
+
u.pathname = '/wiki' + u.pathname;
|
|
237
|
+
urlStr = u.toString();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let parsed;
|
|
241
|
+
try { parsed = new URL(urlStr); }
|
|
242
|
+
catch { return reject(new Error(`Invalid Confluence download URL: ${urlStr}`)); }
|
|
243
|
+
|
|
244
|
+
const mod = parsed.protocol === 'https:' ? require('https') : require('http');
|
|
245
|
+
const opts = {
|
|
246
|
+
hostname: parsed.hostname,
|
|
247
|
+
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
248
|
+
path: parsed.pathname + parsed.search,
|
|
249
|
+
method: 'GET',
|
|
250
|
+
headers: {
|
|
251
|
+
Authorization: this._authHeader(),
|
|
252
|
+
Accept: '*/*',
|
|
253
|
+
'User-Agent': 'devflow-kit/1.0',
|
|
254
|
+
},
|
|
255
|
+
timeout: this.timeoutMs,
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const req = mod.request(opts, (res) => {
|
|
259
|
+
const chunks = [];
|
|
260
|
+
res.on('data', (c) => chunks.push(c));
|
|
261
|
+
res.on('end', () => {
|
|
262
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location && redirects > 0) {
|
|
263
|
+
return this._getBinary(res.headers.location, redirects - 1).then(resolve, reject);
|
|
264
|
+
}
|
|
265
|
+
if (res.statusCode >= 400) {
|
|
266
|
+
return reject(new Error(`Confluence attachment download failed ${res.statusCode}`));
|
|
267
|
+
}
|
|
268
|
+
resolve(Buffer.concat(chunks));
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
req.on('timeout', () => {
|
|
272
|
+
req.destroy();
|
|
273
|
+
reject(new Error(`Confluence attachment download timed out (${this.timeoutMs}ms): ${urlStr}`));
|
|
274
|
+
});
|
|
275
|
+
req.on('error', reject);
|
|
276
|
+
req.end();
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function isImageAttachment(filename, mediaType) {
|
|
282
|
+
if (mediaType && /^image\//i.test(mediaType)) return true;
|
|
283
|
+
return /\.(png|jpe?g|gif|webp|svg|bmp)$/i.test(filename || '');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ── URL parsing ──────────────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
function extractPageId(url) {
|
|
289
|
+
// ?pageId=12345 or &pageId=12345
|
|
290
|
+
const m1 = /[?&]pageId=(\d+)/.exec(url);
|
|
291
|
+
if (m1) return m1[1];
|
|
292
|
+
// /pages/12345[/] — Cloud and modern Server style
|
|
293
|
+
const m2 = /\/pages\/(\d+)/.exec(url);
|
|
294
|
+
if (m2) return m2[1];
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function extractSpaceTitle(url) {
|
|
299
|
+
// /display/SPACE/Title or /display/SPACE/Title+With+Spaces
|
|
300
|
+
const m1 = /\/display\/([^/?#]+)\/([^?#]+)/.exec(url);
|
|
301
|
+
if (m1) {
|
|
302
|
+
return {
|
|
303
|
+
spaceKey: m1[1],
|
|
304
|
+
title: decodeURIComponent(m1[2].replace(/\+/g, ' ')),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
// /wiki/spaces/SPACE/pages/ID/Title
|
|
308
|
+
const m2 = /\/spaces\/([^/?#]+)\/pages\/\d+\/([^?#]+)/.exec(url);
|
|
309
|
+
if (m2) {
|
|
310
|
+
return {
|
|
311
|
+
spaceKey: m2[1],
|
|
312
|
+
title: decodeURIComponent(m2[2].replace(/\+/g, ' ')),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
return {};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ── Confluence Storage Format → Markdown ─────────────────────────────────────
|
|
319
|
+
//
|
|
320
|
+
// Confluence Storage Format is XHTML + <ac:*> / <ri:*> macros.
|
|
321
|
+
// This converter handles common elements with zero external deps.
|
|
322
|
+
// Unknown elements are stripped (tags removed, content kept).
|
|
323
|
+
|
|
324
|
+
function confluenceStorageToMd(html) {
|
|
325
|
+
if (!html) return '';
|
|
326
|
+
|
|
327
|
+
let s = html;
|
|
328
|
+
|
|
329
|
+
// ── 1. Confluence macros ──────────────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
// Code blocks: <ac:structured-macro ac:name="code">...<ac:plain-text-body><![CDATA[...]]></ac:plain-text-body>
|
|
332
|
+
s = s.replace(
|
|
333
|
+
/<ac:structured-macro[^>]*ac:name="code"[^>]*>[\s\S]*?<ac:parameter[^>]*ac:name="language"[^>]*>([^<]*)<\/ac:parameter>[\s\S]*?<!\[CDATA\[([\s\S]*?)\]\]>[\s\S]*?<\/ac:structured-macro>/gi,
|
|
334
|
+
(_, lang, code) => `\n\`\`\`${lang.trim()}\n${code}\`\`\`\n`,
|
|
335
|
+
);
|
|
336
|
+
// Code blocks without language parameter
|
|
337
|
+
s = s.replace(
|
|
338
|
+
/<ac:structured-macro[^>]*ac:name="code"[^>]*>[\s\S]*?<!\[CDATA\[([\s\S]*?)\]\]>[\s\S]*?<\/ac:structured-macro>/gi,
|
|
339
|
+
(_, code) => `\n\`\`\`\n${code}\`\`\`\n`,
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
// Info/warning/note panels: keep content, add a > blockquote prefix
|
|
343
|
+
s = s.replace(
|
|
344
|
+
/<ac:structured-macro[^>]*ac:name="(info|note|warning|tip)"[^>]*>([\s\S]*?)<\/ac:structured-macro>/gi,
|
|
345
|
+
(_, type, inner) => `\n> **${type.toUpperCase()}**\n>\n${stripTags(inner).split('\n').map((l) => `> ${l}`).join('\n')}\n`,
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
// JIRA macro → render as inline reference
|
|
349
|
+
s = s.replace(
|
|
350
|
+
/<ac:structured-macro[^>]*ac:name="jira"[^>]*>([\s\S]*?)<\/ac:structured-macro>/gi,
|
|
351
|
+
(_, inner) => {
|
|
352
|
+
const keyMatch = inner.match(/<ac:parameter[^>]*ac:name="key"[^>]*>([^<]+)<\/ac:parameter>/i);
|
|
353
|
+
return keyMatch ? `[JIRA: ${keyMatch[1]}]` : '';
|
|
354
|
+
},
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
// Status macro → keep label text
|
|
358
|
+
s = s.replace(
|
|
359
|
+
/<ac:structured-macro[^>]*ac:name="status"[^>]*>([\s\S]*?)<\/ac:structured-macro>/gi,
|
|
360
|
+
(_, inner) => {
|
|
361
|
+
const titleMatch = inner.match(/<ac:parameter[^>]*ac:name="title"[^>]*>([^<]+)<\/ac:parameter>/i);
|
|
362
|
+
return titleMatch ? `**[${titleMatch[1]}]**` : '';
|
|
363
|
+
},
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
// Strip all remaining ac:structured-macro blocks (keep plain-text-body / rich-text-body content)
|
|
367
|
+
s = s.replace(/<ac:plain-text-body[^>]*><!\[CDATA\[([\s\S]*?)\]\]><\/ac:plain-text-body>/gi, '$1');
|
|
368
|
+
s = s.replace(/<ac:rich-text-body[^>]*>([\s\S]*?)<\/ac:rich-text-body>/gi, '$1');
|
|
369
|
+
s = s.replace(/<ac:structured-macro[\s\S]*?<\/ac:structured-macro>/gi, '');
|
|
370
|
+
|
|
371
|
+
// ac:image → render attachment name as image reference
|
|
372
|
+
s = s.replace(
|
|
373
|
+
/<ac:image[^>]*>([\s\S]*?)<\/ac:image>/gi,
|
|
374
|
+
(_, inner) => {
|
|
375
|
+
const filenameMatch = inner.match(/ri:filename="([^"]+)"/i);
|
|
376
|
+
const urlMatch = inner.match(/ri:value="([^"]+)"/i);
|
|
377
|
+
const src = urlMatch ? urlMatch[1] : (filenameMatch ? filenameMatch[1] : '');
|
|
378
|
+
const alt = filenameMatch ? filenameMatch[1] : '';
|
|
379
|
+
return src ? `` : '';
|
|
380
|
+
},
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
// Confluence links: <ac:link><ri:page ri:content-title="Title"/><ac:plain-text-link-body><![CDATA[label]]></ac:plain-text-link-body></ac:link>
|
|
384
|
+
s = s.replace(
|
|
385
|
+
/<ac:link[^>]*>[\s\S]*?<ri:page[^>]*ri:content-title="([^"]+)"[^>]*\/?>[\s\S]*?<!\[CDATA\[([\s\S]*?)\]\]>[\s\S]*?<\/ac:link>/gi,
|
|
386
|
+
(_, title, label) => `[${label || title}](${title})`,
|
|
387
|
+
);
|
|
388
|
+
s = s.replace(/<ac:link[^>]*>[\s\S]*?<\/ac:link>/gi, '');
|
|
389
|
+
|
|
390
|
+
// Strip remaining ac:* and ri:* tags
|
|
391
|
+
s = s.replace(/<\/?ac:[^>]*>/gi, '');
|
|
392
|
+
s = s.replace(/<\/?ri:[^>]*>/gi, '');
|
|
393
|
+
|
|
394
|
+
// ── 2. Standard HTML → Markdown ──────────────────────────────────────────
|
|
395
|
+
|
|
396
|
+
// Headings
|
|
397
|
+
for (let i = 6; i >= 1; i--) {
|
|
398
|
+
const hashes = '#'.repeat(i);
|
|
399
|
+
s = s.replace(new RegExp(`<h${i}[^>]*>([\\s\\S]*?)<\\/h${i}>`, 'gi'),
|
|
400
|
+
(_, inner) => `\n${hashes} ${stripTags(inner).trim()}\n`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Horizontal rules
|
|
404
|
+
s = s.replace(/<hr[^>]*\/?>/gi, '\n---\n');
|
|
405
|
+
|
|
406
|
+
// Line breaks
|
|
407
|
+
s = s.replace(/<br[^>]*\/?>/gi, '\n');
|
|
408
|
+
|
|
409
|
+
// Bold
|
|
410
|
+
s = s.replace(/<(strong|b)[^>]*>([\s\S]*?)<\/\1>/gi, (_, __, inner) => `**${stripTags(inner).trim()}**`);
|
|
411
|
+
|
|
412
|
+
// Italic
|
|
413
|
+
s = s.replace(/<(em|i)[^>]*>([\s\S]*?)<\/\1>/gi, (_, __, inner) => `*${stripTags(inner).trim()}*`);
|
|
414
|
+
|
|
415
|
+
// Strikethrough
|
|
416
|
+
s = s.replace(/<(s|del|strike)[^>]*>([\s\S]*?)<\/\1>/gi, (_, __, inner) => `~~${stripTags(inner).trim()}~~`);
|
|
417
|
+
|
|
418
|
+
// Inline code
|
|
419
|
+
s = s.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, (_, inner) => `\`${inner}\``);
|
|
420
|
+
|
|
421
|
+
// Pre blocks (without inner code already replaced)
|
|
422
|
+
s = s.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, (_, inner) => `\n\`\`\`\n${stripTags(inner)}\`\`\`\n`);
|
|
423
|
+
|
|
424
|
+
// Links
|
|
425
|
+
s = s.replace(/<a[^>]+href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi,
|
|
426
|
+
(_, href, inner) => `[${stripTags(inner).trim()}](${href})`);
|
|
427
|
+
|
|
428
|
+
// Images
|
|
429
|
+
s = s.replace(/<img[^>]+src="([^"]*)"[^>]*(?:alt="([^"]*)")?[^>]*\/?>/gi,
|
|
430
|
+
(_, src, alt) => ``);
|
|
431
|
+
|
|
432
|
+
// Tables
|
|
433
|
+
s = convertTables(s);
|
|
434
|
+
|
|
435
|
+
// Lists (must come after table to avoid interference)
|
|
436
|
+
s = convertLists(s);
|
|
437
|
+
|
|
438
|
+
// Paragraphs
|
|
439
|
+
s = s.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi,
|
|
440
|
+
(_, inner) => `\n${stripTags(inner).trim()}\n`);
|
|
441
|
+
|
|
442
|
+
// Blockquotes
|
|
443
|
+
s = s.replace(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi,
|
|
444
|
+
(_, inner) => inner.split('\n').map((l) => `> ${l.trim()}`).join('\n') + '\n');
|
|
445
|
+
|
|
446
|
+
// ── 3. Strip remaining HTML tags ─────────────────────────────────────────
|
|
447
|
+
s = s.replace(/<[^>]+>/g, '');
|
|
448
|
+
|
|
449
|
+
// ── 4. Decode common HTML entities ───────────────────────────────────────
|
|
450
|
+
s = decodeEntities(s);
|
|
451
|
+
|
|
452
|
+
// ── 5. Normalize whitespace ───────────────────────────────────────────────
|
|
453
|
+
// Collapse 3+ blank lines to 2
|
|
454
|
+
s = s.replace(/\n{3,}/g, '\n\n');
|
|
455
|
+
return s.trim();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function convertTables(s) {
|
|
459
|
+
return s.replace(/<table[^>]*>([\s\S]*?)<\/table>/gi, (_, body) => {
|
|
460
|
+
const rows = [];
|
|
461
|
+
const rowMatches = body.matchAll(/<tr[^>]*>([\s\S]*?)<\/tr>/gi);
|
|
462
|
+
for (const rm of rowMatches) {
|
|
463
|
+
const cells = [];
|
|
464
|
+
const cellRe = /<t[dh][^>]*>([\s\S]*?)<\/t[dh]>/gi;
|
|
465
|
+
let cm;
|
|
466
|
+
while ((cm = cellRe.exec(rm[1])) !== null) {
|
|
467
|
+
cells.push(stripTags(cm[1]).trim().replace(/\|/g, '\\|'));
|
|
468
|
+
}
|
|
469
|
+
rows.push(cells);
|
|
470
|
+
}
|
|
471
|
+
if (!rows.length) return '';
|
|
472
|
+
const lines = [];
|
|
473
|
+
lines.push('| ' + rows[0].join(' | ') + ' |');
|
|
474
|
+
lines.push('| ' + rows[0].map(() => '---').join(' | ') + ' |');
|
|
475
|
+
for (const row of rows.slice(1)) {
|
|
476
|
+
lines.push('| ' + row.join(' | ') + ' |');
|
|
477
|
+
}
|
|
478
|
+
return '\n' + lines.join('\n') + '\n';
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function convertLists(s) {
|
|
483
|
+
// Handle nested lists by iterating outer → inner.
|
|
484
|
+
// We do a simple approach: convert <li> inside <ul>/<ol>.
|
|
485
|
+
function convertList(html, ordered) {
|
|
486
|
+
let idx = 1;
|
|
487
|
+
return html.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_, inner) => {
|
|
488
|
+
const content = stripTags(inner).trim();
|
|
489
|
+
const prefix = ordered ? `${idx++}. ` : '- ';
|
|
490
|
+
return `\n${prefix}${content}`;
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Unordered
|
|
495
|
+
s = s.replace(/<ul[^>]*>([\s\S]*?)<\/ul>/gi, (_, inner) => `\n${convertList(inner, false)}\n`);
|
|
496
|
+
// Ordered
|
|
497
|
+
s = s.replace(/<ol[^>]*>([\s\S]*?)<\/ol>/gi, (_, inner) => `\n${convertList(inner, true)}\n`);
|
|
498
|
+
return s;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function stripTags(s) {
|
|
502
|
+
return (s || '').replace(/<[^>]+>/g, '');
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function decodeEntities(s) {
|
|
506
|
+
return s
|
|
507
|
+
.replace(/&/g, '&')
|
|
508
|
+
.replace(/</g, '<')
|
|
509
|
+
.replace(/>/g, '>')
|
|
510
|
+
.replace(/"/g, '"')
|
|
511
|
+
.replace(/'/g, "'")
|
|
512
|
+
.replace(/ /g, ' ')
|
|
513
|
+
.replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n)))
|
|
514
|
+
.replace(/&[a-z]+;/g, '');
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
518
|
+
|
|
519
|
+
function openBrowser(url) {
|
|
520
|
+
const { spawnSync } = require('child_process');
|
|
521
|
+
try {
|
|
522
|
+
if (process.platform === 'darwin') {
|
|
523
|
+
spawnSync('open', [url], { stdio: 'ignore' });
|
|
524
|
+
} else if (process.platform === 'win32') {
|
|
525
|
+
spawnSync('cmd', ['/c', 'start', '', url], { stdio: 'ignore', shell: true });
|
|
526
|
+
} else {
|
|
527
|
+
spawnSync('xdg-open', [url], { stdio: 'ignore' });
|
|
528
|
+
}
|
|
529
|
+
} catch { /* ignore if browser can't be opened */ }
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function toInt(v, def) {
|
|
533
|
+
if (v === undefined || v === null || v === '') return def;
|
|
534
|
+
const n = typeof v === 'number' ? v : parseInt(String(v), 10);
|
|
535
|
+
return Number.isFinite(n) && n >= 0 ? n : def;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
module.exports = {
|
|
539
|
+
default: ConfluenceIntakeProvider,
|
|
540
|
+
ConfluenceIntakeProvider,
|
|
541
|
+
confluenceStorageToMd,
|
|
542
|
+
extractPageId,
|
|
543
|
+
extractSpaceTitle,
|
|
544
|
+
};
|