@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,515 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const fsp = require('fs/promises');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const net = require('net');
|
|
6
|
+
const tls = require('tls');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
const { Provider } = require('../base.js');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* notify-smtp — port of arb-workflow-kit deploy-submit/send-email.py to Node.
|
|
13
|
+
*
|
|
14
|
+
* Zero external deps: TLS/net + MIME composed by hand.
|
|
15
|
+
*
|
|
16
|
+
* Supports three connection modes:
|
|
17
|
+
* 465 → implicit TLS (SMTPS)
|
|
18
|
+
* 587 → STARTTLS upgrade (preferred for modern servers)
|
|
19
|
+
* 25 → plain (no auth recommended)
|
|
20
|
+
*
|
|
21
|
+
* Supports AUTH PLAIN (RFC 4616) and AUTH LOGIN (legacy, Exchange / QQ mail).
|
|
22
|
+
*
|
|
23
|
+
* Markdown → HTML conversion mirrors arb's `_md_to_html`:
|
|
24
|
+
* - `# / ## / ###` → `<h2>/h3/h4>`
|
|
25
|
+
* - `- item` → `<ul><li>`
|
|
26
|
+
* - `N. item` → `<ol><li>`
|
|
27
|
+
* - `| ... |` pipe tables (auto-detect header + separator row)
|
|
28
|
+
* - `**bold**` → `<b>`, `` `code` `` → `<code>`
|
|
29
|
+
*
|
|
30
|
+
* Config schema:
|
|
31
|
+
* {
|
|
32
|
+
* host: "smtp.exmail.qq.com",
|
|
33
|
+
* port: 465, // 465 | 587 | 25
|
|
34
|
+
* secure: true, // auto-derived from port if omitted
|
|
35
|
+
* user: "sender@corp.com", // also taken from env SMTP_USER
|
|
36
|
+
* pass: "...", // also taken from env SMTP_PASS (prefer env)
|
|
37
|
+
* authMethod: "login" | "plain" | "auto" (default "auto")
|
|
38
|
+
* from: "Name <sender@corp.com>", // defaults to user
|
|
39
|
+
* defaults: { to, cc, signatureFile } // optional, merged with call opts
|
|
40
|
+
* rejectUnauthorized: true,
|
|
41
|
+
* timeoutMs: 30000
|
|
42
|
+
* }
|
|
43
|
+
*
|
|
44
|
+
* send(opts) — opts:
|
|
45
|
+
* subject required string
|
|
46
|
+
* body_md markdown body (either body_md or bodyFile required)
|
|
47
|
+
* bodyFile path to markdown file
|
|
48
|
+
* to, cc "a@b.com, c@d.com" (string) or array
|
|
49
|
+
* attachments [pathString, ...]
|
|
50
|
+
* signatureFile optional tail appendage
|
|
51
|
+
* dryRun bool — compose but do not open socket
|
|
52
|
+
*
|
|
53
|
+
* Returns: { id, accepted, response, raw? } where raw is included in dryRun.
|
|
54
|
+
*/
|
|
55
|
+
class SmtpNotifyProvider extends Provider {
|
|
56
|
+
constructor({ name, type, driver, config }) {
|
|
57
|
+
super({ name, type, driver, config });
|
|
58
|
+
this.host = config.host || process.env.SMTP_HOST || '';
|
|
59
|
+
this.port = Number(config.port || process.env.SMTP_PORT || 465);
|
|
60
|
+
this.secure = config.secure !== undefined ? !!config.secure : this.port === 465;
|
|
61
|
+
this.user = config.user || process.env.SMTP_USER || '';
|
|
62
|
+
this.pass = config.pass || process.env.SMTP_PASS || '';
|
|
63
|
+
this.from = config.from || this.user;
|
|
64
|
+
this.authMethod = (config.authMethod || 'auto').toLowerCase();
|
|
65
|
+
this.timeoutMs = Number(config.timeoutMs || 30000);
|
|
66
|
+
this.rejectUnauthorized = config.rejectUnauthorized !== false;
|
|
67
|
+
this.defaults = config.defaults || {};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async validate() {
|
|
71
|
+
if (!this.host) return { ok: false, reason: 'missing host (set SMTP_HOST)' };
|
|
72
|
+
if (!this.user) return { ok: false, reason: 'missing user (set SMTP_USER)', needsLogin: true };
|
|
73
|
+
if (!this.pass) return { ok: false, reason: 'missing pass (set SMTP_PASS)', needsLogin: true };
|
|
74
|
+
if (!this.from) return { ok: false, reason: 'missing from address' };
|
|
75
|
+
return { ok: true };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async send(opts = {}) {
|
|
79
|
+
const subject = opts.subject;
|
|
80
|
+
if (!subject) throw new Error('notify-smtp: subject required');
|
|
81
|
+
let bodyMd = opts.body_md || '';
|
|
82
|
+
if (!bodyMd && opts.bodyFile) {
|
|
83
|
+
bodyMd = await fsp.readFile(opts.bodyFile, 'utf8');
|
|
84
|
+
}
|
|
85
|
+
if (!bodyMd && opts.body) bodyMd = opts.body;
|
|
86
|
+
if (!bodyMd) throw new Error('notify-smtp: body_md / bodyFile required');
|
|
87
|
+
|
|
88
|
+
const sigFile = opts.signatureFile || this.defaults.signatureFile;
|
|
89
|
+
if (sigFile && fs.existsSync(sigFile)) {
|
|
90
|
+
bodyMd += '\n\n---\n' + (await fsp.readFile(sigFile, 'utf8'));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const html = mdToHtml(bodyMd);
|
|
94
|
+
const to = asAddrList(opts.to || this.defaults.to);
|
|
95
|
+
const cc = asAddrList(opts.cc || this.defaults.cc || []);
|
|
96
|
+
if (!to.length) throw new Error('notify-smtp: at least one "to" recipient required');
|
|
97
|
+
|
|
98
|
+
const attachments = [];
|
|
99
|
+
for (const ap of opts.attachments || []) {
|
|
100
|
+
if (!fs.existsSync(ap)) { process.stderr.write(`[notify-smtp] skipping missing attachment: ${ap}\n`); continue; }
|
|
101
|
+
const buf = await fsp.readFile(ap);
|
|
102
|
+
attachments.push({
|
|
103
|
+
filename: path.basename(ap),
|
|
104
|
+
contentType: guessContentType(ap),
|
|
105
|
+
content: buf,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const rfc822 = composeMime({
|
|
110
|
+
from: this.from,
|
|
111
|
+
to, cc,
|
|
112
|
+
subject,
|
|
113
|
+
html,
|
|
114
|
+
textFallback: stripTags(html).slice(0, 5000),
|
|
115
|
+
attachments,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const envelope = {
|
|
119
|
+
from: extractAddr(this.from),
|
|
120
|
+
recipients: [...to, ...cc].map(extractAddr).filter(Boolean),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if (opts.dryRun || process.env.DEVFLOW_NOTIFY_DRY_RUN === '1') {
|
|
124
|
+
const id = 'dryrun-' + crypto.randomBytes(4).toString('hex');
|
|
125
|
+
process.stdout.write(`[notify-smtp dry-run] would send to ${envelope.recipients.join(',')} subject=${subject}\n`);
|
|
126
|
+
return { id, accepted: envelope.recipients, response: 'dryRun', raw: rfc822 };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const response = await smtpSend({
|
|
130
|
+
host: this.host,
|
|
131
|
+
port: this.port,
|
|
132
|
+
secure: this.secure,
|
|
133
|
+
user: this.user,
|
|
134
|
+
pass: this.pass,
|
|
135
|
+
authMethod: this.authMethod,
|
|
136
|
+
rejectUnauthorized: this.rejectUnauthorized,
|
|
137
|
+
timeoutMs: this.timeoutMs,
|
|
138
|
+
envelope,
|
|
139
|
+
message: rfc822,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
id: response.messageId,
|
|
144
|
+
accepted: envelope.recipients,
|
|
145
|
+
response: response.lastLine,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ─────────────────── SMTP protocol ───────────────────
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Minimal SMTP client. Exposed for testing via `_connectFactory` override on opts.
|
|
154
|
+
*/
|
|
155
|
+
async function smtpSend(opts) {
|
|
156
|
+
const connect = opts._connectFactory || createSmtpConnection;
|
|
157
|
+
const conn = await connect({
|
|
158
|
+
host: opts.host,
|
|
159
|
+
port: opts.port,
|
|
160
|
+
secure: opts.secure,
|
|
161
|
+
rejectUnauthorized: opts.rejectUnauthorized,
|
|
162
|
+
timeoutMs: opts.timeoutMs,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const expect = async (codes) => {
|
|
166
|
+
const lines = await conn.readResponse();
|
|
167
|
+
const first = parseInt(lines[lines.length - 1].slice(0, 3), 10);
|
|
168
|
+
if (!codes.includes(first)) {
|
|
169
|
+
throw new Error(`SMTP unexpected response: ${lines.join(' | ')} (wanted ${codes})`);
|
|
170
|
+
}
|
|
171
|
+
return lines;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
await expect([220]);
|
|
175
|
+
await conn.send(`EHLO ${os.hostname() || 'devflow'}`);
|
|
176
|
+
const ehloLines = await expect([250]);
|
|
177
|
+
const caps = new Set(ehloLines.map((l) => l.slice(4).split(/\s/)[0].toUpperCase()));
|
|
178
|
+
|
|
179
|
+
// STARTTLS upgrade when not already secure and server supports it
|
|
180
|
+
if (!opts.secure && caps.has('STARTTLS')) {
|
|
181
|
+
await conn.send('STARTTLS');
|
|
182
|
+
await expect([220]);
|
|
183
|
+
await conn.upgradeTls({ host: opts.host, rejectUnauthorized: opts.rejectUnauthorized });
|
|
184
|
+
await conn.send(`EHLO ${os.hostname() || 'devflow'}`);
|
|
185
|
+
await expect([250]);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Auth
|
|
189
|
+
const method = pickAuthMethod(opts.authMethod, caps);
|
|
190
|
+
if (method === 'PLAIN') {
|
|
191
|
+
const payload = Buffer.from(`\0${opts.user}\0${opts.pass}`, 'utf8').toString('base64');
|
|
192
|
+
await conn.send(`AUTH PLAIN ${payload}`);
|
|
193
|
+
await expect([235]);
|
|
194
|
+
} else if (method === 'LOGIN') {
|
|
195
|
+
await conn.send('AUTH LOGIN');
|
|
196
|
+
await expect([334]);
|
|
197
|
+
await conn.send(Buffer.from(opts.user, 'utf8').toString('base64'));
|
|
198
|
+
await expect([334]);
|
|
199
|
+
await conn.send(Buffer.from(opts.pass, 'utf8').toString('base64'));
|
|
200
|
+
await expect([235]);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Envelope
|
|
204
|
+
await conn.send(`MAIL FROM:<${opts.envelope.from}>`);
|
|
205
|
+
await expect([250]);
|
|
206
|
+
for (const r of opts.envelope.recipients) {
|
|
207
|
+
await conn.send(`RCPT TO:<${r}>`);
|
|
208
|
+
await expect([250, 251]);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Data
|
|
212
|
+
await conn.send('DATA');
|
|
213
|
+
await expect([354]);
|
|
214
|
+
const msg = opts.message.split(/\r?\n/).map((line) => line.startsWith('.') ? '.' + line : line).join('\r\n');
|
|
215
|
+
await conn.sendRaw(msg + '\r\n.');
|
|
216
|
+
const okLines = await expect([250]);
|
|
217
|
+
|
|
218
|
+
await conn.send('QUIT');
|
|
219
|
+
await conn.close();
|
|
220
|
+
|
|
221
|
+
return { messageId: extractMessageId(okLines), lastLine: okLines[okLines.length - 1] };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function pickAuthMethod(configured, caps) {
|
|
225
|
+
if (configured === 'plain' && caps.has('AUTH=PLAIN') || (configured === 'plain')) return 'PLAIN';
|
|
226
|
+
if (configured === 'login') return 'LOGIN';
|
|
227
|
+
// auto: prefer PLAIN (simpler), fall back to LOGIN
|
|
228
|
+
for (const cap of caps) {
|
|
229
|
+
if (cap.startsWith('AUTH')) {
|
|
230
|
+
const m = cap.split(/\s|=/);
|
|
231
|
+
if (m.includes('PLAIN') || m.some((x) => x === 'PLAIN')) return 'PLAIN';
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Default: LOGIN (most compatible with Exchange / QQ mail)
|
|
235
|
+
return 'LOGIN';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function extractMessageId(lines) {
|
|
239
|
+
for (const l of lines) {
|
|
240
|
+
const m = /queued as ([A-Za-z0-9._-]+)/i.exec(l);
|
|
241
|
+
if (m) return m[1];
|
|
242
|
+
}
|
|
243
|
+
return 'smtp-' + crypto.randomBytes(4).toString('hex');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function createSmtpConnection({ host, port, secure, rejectUnauthorized, timeoutMs }) {
|
|
247
|
+
return new Promise((resolve, reject) => {
|
|
248
|
+
const common = { host, port };
|
|
249
|
+
const mkReader = (socket) => new SocketReader(socket, timeoutMs);
|
|
250
|
+
if (secure) {
|
|
251
|
+
const socket = tls.connect({ ...common, rejectUnauthorized }, () => resolve(wrap(socket, mkReader(socket))));
|
|
252
|
+
socket.on('error', reject);
|
|
253
|
+
} else {
|
|
254
|
+
const socket = net.connect(common, () => resolve(wrap(socket, mkReader(socket))));
|
|
255
|
+
socket.on('error', reject);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
function wrap(socket, reader) {
|
|
260
|
+
let current = { socket, reader };
|
|
261
|
+
const self = {
|
|
262
|
+
async readResponse() {
|
|
263
|
+
return current.reader.readResponse();
|
|
264
|
+
},
|
|
265
|
+
async send(line) {
|
|
266
|
+
await new Promise((res, rej) => current.socket.write(line + '\r\n', (err) => err ? rej(err) : res()));
|
|
267
|
+
},
|
|
268
|
+
async sendRaw(data) {
|
|
269
|
+
await new Promise((res, rej) => current.socket.write(data + '\r\n', (err) => err ? rej(err) : res()));
|
|
270
|
+
},
|
|
271
|
+
async upgradeTls({ host, rejectUnauthorized }) {
|
|
272
|
+
const newSocket = tls.connect({ socket: current.socket, host, rejectUnauthorized });
|
|
273
|
+
await new Promise((res, rej) => {
|
|
274
|
+
newSocket.once('secureConnect', res);
|
|
275
|
+
newSocket.once('error', rej);
|
|
276
|
+
});
|
|
277
|
+
current.socket = newSocket;
|
|
278
|
+
current.reader = mkReader(newSocket);
|
|
279
|
+
},
|
|
280
|
+
async close() {
|
|
281
|
+
try { current.socket.end(); } catch (_) { /* noop */ }
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
return self;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
class SocketReader {
|
|
289
|
+
constructor(socket, timeoutMs) {
|
|
290
|
+
this.socket = socket;
|
|
291
|
+
this.timeoutMs = timeoutMs;
|
|
292
|
+
this.buf = '';
|
|
293
|
+
this.waiters = [];
|
|
294
|
+
socket.setEncoding('utf8');
|
|
295
|
+
socket.on('data', (chunk) => { this.buf += chunk; this._flush(); });
|
|
296
|
+
socket.on('end', () => { this._flush(true); });
|
|
297
|
+
socket.on('error', (err) => { for (const w of this.waiters) w.reject(err); this.waiters.length = 0; });
|
|
298
|
+
}
|
|
299
|
+
_flush() {
|
|
300
|
+
while (this.waiters.length) {
|
|
301
|
+
const lines = extractCompleteResponse(this.buf);
|
|
302
|
+
if (!lines) return;
|
|
303
|
+
const [resp, rest] = lines;
|
|
304
|
+
this.buf = rest;
|
|
305
|
+
const w = this.waiters.shift();
|
|
306
|
+
w.resolve(resp);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
readResponse() {
|
|
310
|
+
return new Promise((resolve, reject) => {
|
|
311
|
+
const timer = setTimeout(() => {
|
|
312
|
+
const i = this.waiters.findIndex((w) => w.resolve === resolve);
|
|
313
|
+
if (i >= 0) this.waiters.splice(i, 1);
|
|
314
|
+
reject(new Error(`SMTP read timeout (${this.timeoutMs}ms)`));
|
|
315
|
+
}, this.timeoutMs);
|
|
316
|
+
this.waiters.push({
|
|
317
|
+
resolve: (v) => { clearTimeout(timer); resolve(v); },
|
|
318
|
+
reject: (e) => { clearTimeout(timer); reject(e); },
|
|
319
|
+
});
|
|
320
|
+
this._flush();
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Parse accumulated SMTP response buffer; returns [lines, remaining] once a full multiline response is read. */
|
|
326
|
+
function extractCompleteResponse(buf) {
|
|
327
|
+
const raw = buf.split(/\r?\n/);
|
|
328
|
+
const lines = [];
|
|
329
|
+
for (let i = 0; i < raw.length; i++) {
|
|
330
|
+
const line = raw[i];
|
|
331
|
+
if (!/^\d{3}[- ]/.test(line)) {
|
|
332
|
+
if (i === raw.length - 1) return null; // still accumulating
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
lines.push(line);
|
|
336
|
+
if (line[3] === ' ') {
|
|
337
|
+
const rest = raw.slice(i + 1).join('\r\n');
|
|
338
|
+
return [lines, rest];
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ─────────────────── MIME composition ───────────────────
|
|
345
|
+
|
|
346
|
+
function composeMime({ from, to, cc, subject, html, textFallback, attachments }) {
|
|
347
|
+
const hasAttachments = attachments && attachments.length > 0;
|
|
348
|
+
const headers = [
|
|
349
|
+
`From: ${from}`,
|
|
350
|
+
`To: ${to.join(', ')}`,
|
|
351
|
+
...(cc.length ? [`Cc: ${cc.join(', ')}`] : []),
|
|
352
|
+
`Subject: ${encodeHeader(subject)}`,
|
|
353
|
+
`Date: ${new Date().toUTCString()}`,
|
|
354
|
+
`Message-ID: <${crypto.randomBytes(8).toString('hex')}@devflow>`,
|
|
355
|
+
`MIME-Version: 1.0`,
|
|
356
|
+
];
|
|
357
|
+
|
|
358
|
+
if (!hasAttachments) {
|
|
359
|
+
headers.push(`Content-Type: text/html; charset="utf-8"`);
|
|
360
|
+
headers.push(`Content-Transfer-Encoding: base64`);
|
|
361
|
+
return headers.join('\r\n') + '\r\n\r\n' + chunk76(Buffer.from(html, 'utf8').toString('base64'));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const boundary = 'devflow-' + crypto.randomBytes(8).toString('hex');
|
|
365
|
+
headers.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
|
|
366
|
+
|
|
367
|
+
const parts = [];
|
|
368
|
+
parts.push(
|
|
369
|
+
`--${boundary}`,
|
|
370
|
+
`Content-Type: text/html; charset="utf-8"`,
|
|
371
|
+
`Content-Transfer-Encoding: base64`,
|
|
372
|
+
'',
|
|
373
|
+
chunk76(Buffer.from(html, 'utf8').toString('base64'))
|
|
374
|
+
);
|
|
375
|
+
for (const att of attachments) {
|
|
376
|
+
parts.push(
|
|
377
|
+
`--${boundary}`,
|
|
378
|
+
`Content-Type: ${att.contentType}; name="${att.filename}"`,
|
|
379
|
+
`Content-Transfer-Encoding: base64`,
|
|
380
|
+
`Content-Disposition: attachment; filename="${att.filename}"`,
|
|
381
|
+
'',
|
|
382
|
+
chunk76(att.content.toString('base64'))
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
parts.push(`--${boundary}--`);
|
|
386
|
+
return headers.join('\r\n') + '\r\n\r\n' + parts.join('\r\n');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function chunk76(s) {
|
|
390
|
+
const out = [];
|
|
391
|
+
for (let i = 0; i < s.length; i += 76) out.push(s.slice(i, i + 76));
|
|
392
|
+
return out.join('\r\n');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function encodeHeader(s) {
|
|
396
|
+
if (/^[\x00-\x7F]+$/.test(s)) return s;
|
|
397
|
+
return `=?UTF-8?B?${Buffer.from(s, 'utf8').toString('base64')}?=`;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function guessContentType(p) {
|
|
401
|
+
const ext = path.extname(p).toLowerCase();
|
|
402
|
+
const map = {
|
|
403
|
+
'.md': 'text/markdown',
|
|
404
|
+
'.txt': 'text/plain',
|
|
405
|
+
'.html': 'text/html',
|
|
406
|
+
'.pdf': 'application/pdf',
|
|
407
|
+
'.png': 'image/png',
|
|
408
|
+
'.jpg': 'image/jpeg',
|
|
409
|
+
'.jpeg': 'image/jpeg',
|
|
410
|
+
'.gif': 'image/gif',
|
|
411
|
+
'.zip': 'application/zip',
|
|
412
|
+
'.json': 'application/json',
|
|
413
|
+
'.csv': 'text/csv',
|
|
414
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
415
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
416
|
+
};
|
|
417
|
+
return map[ext] || 'application/octet-stream';
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ─────────────────── Markdown → HTML (mirror of arb) ───────────────────
|
|
421
|
+
|
|
422
|
+
function mdToHtml(md) {
|
|
423
|
+
const lines = md.split('\n');
|
|
424
|
+
const out = [];
|
|
425
|
+
let inTable = false;
|
|
426
|
+
let inUl = false;
|
|
427
|
+
let inOl = false;
|
|
428
|
+
|
|
429
|
+
const closeLists = () => {
|
|
430
|
+
if (inUl) { out.push('</ul>'); inUl = false; }
|
|
431
|
+
if (inOl) { out.push('</ol>'); inOl = false; }
|
|
432
|
+
};
|
|
433
|
+
const closeTable = () => {
|
|
434
|
+
if (inTable) { out.push('</table>'); inTable = false; }
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
for (const rawLine of lines) {
|
|
438
|
+
const line = rawLine.trim();
|
|
439
|
+
// Table
|
|
440
|
+
if (line.startsWith('|') && line.includes('|')) {
|
|
441
|
+
const cells = line.replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim());
|
|
442
|
+
const isSeparator = cells.every((c) => /^[-:]+$/.test(c));
|
|
443
|
+
if (isSeparator) { continue; }
|
|
444
|
+
if (!inTable) {
|
|
445
|
+
out.push('<table border="1" cellpadding="8" cellspacing="0" style="border-collapse:collapse;border-color:#ddd;">');
|
|
446
|
+
inTable = true;
|
|
447
|
+
out.push('<tr style="background:#f5f5f5;">' + cells.map((c) => `<th>${escapeHtml(c)}</th>`).join('') + '</tr>');
|
|
448
|
+
} else {
|
|
449
|
+
out.push('<tr>' + cells.map((c) => `<td>${escapeHtml(c)}</td>`).join('') + '</tr>');
|
|
450
|
+
}
|
|
451
|
+
continue;
|
|
452
|
+
} else if (inTable) {
|
|
453
|
+
closeTable();
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (inUl && !line.startsWith('- ')) { out.push('</ul>'); inUl = false; }
|
|
457
|
+
if (inOl && !/^\d+\.\s/.test(line)) { out.push('</ol>'); inOl = false; }
|
|
458
|
+
|
|
459
|
+
if (line.startsWith('### ')) {
|
|
460
|
+
out.push(`<h4>${escapeHtml(line.slice(4))}</h4>`);
|
|
461
|
+
} else if (line.startsWith('## ')) {
|
|
462
|
+
out.push(`<h3>${escapeHtml(line.slice(3))}</h3>`);
|
|
463
|
+
} else if (line.startsWith('# ')) {
|
|
464
|
+
out.push(`<h2 style="color:#1a73e8;">${escapeHtml(line.slice(2))}</h2>`);
|
|
465
|
+
} else if (line.startsWith('- ')) {
|
|
466
|
+
if (!inUl) { out.push('<ul>'); inUl = true; }
|
|
467
|
+
out.push(`<li>${escapeHtml(line.slice(2))}</li>`);
|
|
468
|
+
} else if (/^\d+\.\s/.test(line)) {
|
|
469
|
+
if (!inOl) { out.push('<ol>'); inOl = true; }
|
|
470
|
+
out.push(`<li>${escapeHtml(line.replace(/^\d+\.\s*/, ''))}</li>`);
|
|
471
|
+
} else if (line === '') {
|
|
472
|
+
out.push('<br>');
|
|
473
|
+
} else {
|
|
474
|
+
out.push(`<p>${escapeHtml(line)}</p>`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
closeTable();
|
|
478
|
+
closeLists();
|
|
479
|
+
|
|
480
|
+
let body = out.join('\n');
|
|
481
|
+
body = body.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>');
|
|
482
|
+
body = body.replace(/`([^`]+?)`/g, '<code>$1</code>');
|
|
483
|
+
return `<html><body style="font-family:Arial,sans-serif;font-size:14px;color:#333;">${body}</body></html>`;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function escapeHtml(s) {
|
|
487
|
+
return String(s)
|
|
488
|
+
.replace(/&/g, '&')
|
|
489
|
+
.replace(/</g, '<')
|
|
490
|
+
.replace(/>/g, '>');
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function stripTags(html) {
|
|
494
|
+
return html.replace(/<[^>]+>/g, '').replace(/&[a-z]+;/g, ' ');
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ─────────────────── Address helpers ───────────────────
|
|
498
|
+
|
|
499
|
+
function asAddrList(v) {
|
|
500
|
+
if (!v) return [];
|
|
501
|
+
if (Array.isArray(v)) return v.filter(Boolean);
|
|
502
|
+
return String(v).split(/[,;]/).map((s) => s.trim()).filter(Boolean);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function extractAddr(s) {
|
|
506
|
+
if (!s) return '';
|
|
507
|
+
const m = /<([^>]+)>/.exec(s);
|
|
508
|
+
return m ? m[1] : String(s).trim();
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
module.exports = {
|
|
512
|
+
default: SmtpNotifyProvider,
|
|
513
|
+
SmtpNotifyProvider,
|
|
514
|
+
_internals: { mdToHtml, composeMime, smtpSend, extractCompleteResponse, asAddrList, extractAddr },
|
|
515
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { spawnSync } = require('child_process');
|
|
3
|
+
const { Provider } = require('../base.js');
|
|
4
|
+
|
|
5
|
+
class OssProvider extends Provider {
|
|
6
|
+
async validate() {
|
|
7
|
+
if (!this.config.endpoint) return { ok: false, reason: 'endpoint missing' };
|
|
8
|
+
if (!this.config.bucket) return { ok: false, reason: 'bucket missing' };
|
|
9
|
+
if (!this.config.queryCommand && (!this.config.accessKeyId || !this.config.accessKeySecret)) {
|
|
10
|
+
return { ok: false, reason: 'accessKeyId/accessKeySecret missing', needsLogin: true };
|
|
11
|
+
}
|
|
12
|
+
return { ok: true };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async query(opts = {}) {
|
|
16
|
+
const request = {
|
|
17
|
+
driver: 'oss',
|
|
18
|
+
endpoint: this.config.endpoint,
|
|
19
|
+
bucket: this.config.bucket,
|
|
20
|
+
prefix: this.config.prefix || '',
|
|
21
|
+
query: opts.query || '',
|
|
22
|
+
traceId: opts.traceId || '',
|
|
23
|
+
requestId: opts.requestId || '',
|
|
24
|
+
since: opts.since || '15m',
|
|
25
|
+
limit: opts.limit || 50,
|
|
26
|
+
};
|
|
27
|
+
if (!this.config.queryCommand || opts.dryRun) return { status: 'dry-run', request, items: [] };
|
|
28
|
+
const cmd = render(this.config.queryCommand, request);
|
|
29
|
+
const r = spawnSync(cmd, { shell: true, encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 });
|
|
30
|
+
return {
|
|
31
|
+
status: r.status === 0 ? 'pass' : 'fail',
|
|
32
|
+
command: cmd,
|
|
33
|
+
output: ((r.stdout || '') + (r.stderr || '')).slice(0, 12000),
|
|
34
|
+
items: [],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function render(s, vars) {
|
|
40
|
+
return String(s || '').replace(/\$\{([a-zA-Z0-9_]+)\}/g, (_, k) => vars[k] == null ? '' : String(vars[k]));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { default: OssProvider };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { spawnSync } = require('child_process');
|
|
3
|
+
const { Provider } = require('../base.js');
|
|
4
|
+
|
|
5
|
+
class SlsProvider extends Provider {
|
|
6
|
+
async validate() {
|
|
7
|
+
if (!this.config.endpoint) return { ok: false, reason: 'endpoint missing' };
|
|
8
|
+
if (!this.config.project) return { ok: false, reason: 'project missing' };
|
|
9
|
+
if (!this.config.logstore) return { ok: false, reason: 'logstore missing' };
|
|
10
|
+
if (!this.config.queryCommand && (!this.config.accessKeyId || !this.config.accessKeySecret)) {
|
|
11
|
+
return { ok: false, reason: 'accessKeyId/accessKeySecret missing', needsLogin: true };
|
|
12
|
+
}
|
|
13
|
+
return { ok: true };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async query(opts = {}) {
|
|
17
|
+
return runQueryCommand('sls', this.config, opts);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function runQueryCommand(driver, config, opts) {
|
|
22
|
+
const request = {
|
|
23
|
+
driver,
|
|
24
|
+
endpoint: config.endpoint,
|
|
25
|
+
project: config.project,
|
|
26
|
+
logstore: config.logstore,
|
|
27
|
+
query: opts.query || '',
|
|
28
|
+
traceId: opts.traceId || '',
|
|
29
|
+
requestId: opts.requestId || '',
|
|
30
|
+
since: opts.since || '15m',
|
|
31
|
+
limit: opts.limit || 50,
|
|
32
|
+
};
|
|
33
|
+
if (!config.queryCommand || opts.dryRun) {
|
|
34
|
+
return { status: 'dry-run', request, items: [] };
|
|
35
|
+
}
|
|
36
|
+
const cmd = render(config.queryCommand, request);
|
|
37
|
+
const r = spawnSync(cmd, { shell: true, encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 });
|
|
38
|
+
return {
|
|
39
|
+
status: r.status === 0 ? 'pass' : 'fail',
|
|
40
|
+
command: cmd,
|
|
41
|
+
output: ((r.stdout || '') + (r.stderr || '')).slice(0, 12000),
|
|
42
|
+
items: [],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function render(s, vars) {
|
|
47
|
+
return String(s || '').replace(/\$\{([a-zA-Z0-9_]+)\}/g, (_, k) => vars[k] == null ? '' : String(vars[k]));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { default: SlsProvider, _internals: { runQueryCommand } };
|