wpops 0.1.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/.env.example +19 -0
- package/LICENSE +21 -0
- package/README.md +218 -0
- package/bin/wpops.js +322 -0
- package/lib/args.js +85 -0
- package/lib/client.js +136 -0
- package/lib/commands.js +704 -0
- package/lib/env.js +99 -0
- package/lib/skill.js +37 -0
- package/package.json +47 -0
- package/scripts/postinstall.mjs +15 -0
- package/skills/wpops/SKILL.md +54 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
export class WpError extends Error {
|
|
2
|
+
constructor(message, meta = {}) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = 'WpError';
|
|
5
|
+
this.status = meta.status;
|
|
6
|
+
this.code = meta.code;
|
|
7
|
+
this.data = meta.data;
|
|
8
|
+
this.method = meta.method;
|
|
9
|
+
this.path = meta.path;
|
|
10
|
+
this.network = meta.network;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function buildConfig(env) {
|
|
15
|
+
const retries = Number(env.WP_RETRIES);
|
|
16
|
+
const retryDelay = Number(env.WP_RETRY_DELAY_MS);
|
|
17
|
+
return {
|
|
18
|
+
url: String(env.WP_URL || '').replace(/\/+$/, ''),
|
|
19
|
+
user: String(env.WP_USER || ''),
|
|
20
|
+
password: String(env.WP_APP_PASSWORD || ''),
|
|
21
|
+
timeoutMs: Number(env.WP_TIMEOUT_MS) > 0 ? Number(env.WP_TIMEOUT_MS) : 30000,
|
|
22
|
+
retries: Number.isInteger(retries) && retries >= 0 ? retries : 2,
|
|
23
|
+
retryDelayMs: retryDelay > 0 ? retryDelay : 500,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function configProblems(cfg) {
|
|
28
|
+
const problems = [];
|
|
29
|
+
if (!cfg.url) problems.push('WP_URL 未设置');
|
|
30
|
+
if (!cfg.user) problems.push('WP_USER 未设置');
|
|
31
|
+
if (!cfg.password) problems.push('WP_APP_PASSWORD 未设置');
|
|
32
|
+
if (cfg.url && !/^https:\/\//i.test(cfg.url)) {
|
|
33
|
+
problems.push('WP_URL 不是 https(应用密码走明文 HTTP 不安全)');
|
|
34
|
+
}
|
|
35
|
+
return problems;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const sleep = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
39
|
+
|
|
40
|
+
export function createClient(cfg) {
|
|
41
|
+
const token = Buffer.from(`${cfg.user}:${cfg.password}`, 'utf8').toString('base64');
|
|
42
|
+
const baseHeaders = {
|
|
43
|
+
Authorization: `Basic ${token}`,
|
|
44
|
+
'User-Agent': 'wpops/0.2',
|
|
45
|
+
};
|
|
46
|
+
const retries = cfg.retries ?? 2;
|
|
47
|
+
const retryDelayMs = cfg.retryDelayMs ?? 500;
|
|
48
|
+
|
|
49
|
+
async function once(method, path, { query, body, form, headers = {} }) {
|
|
50
|
+
const url = new URL(cfg.url + path);
|
|
51
|
+
if (query) {
|
|
52
|
+
for (const [key, value] of Object.entries(query)) {
|
|
53
|
+
if (value === undefined || value === null || value === '') continue;
|
|
54
|
+
url.searchParams.set(key, String(value));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const init = { method, headers: { ...baseHeaders, ...headers }, redirect: 'follow' };
|
|
59
|
+
if (form) {
|
|
60
|
+
init.body = form;
|
|
61
|
+
} else if (body !== undefined) {
|
|
62
|
+
init.headers['Content-Type'] = 'application/json';
|
|
63
|
+
init.body = JSON.stringify(body);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const controller = new AbortController();
|
|
67
|
+
const timer = setTimeout(() => controller.abort(), cfg.timeoutMs);
|
|
68
|
+
init.signal = controller.signal;
|
|
69
|
+
|
|
70
|
+
let res;
|
|
71
|
+
try {
|
|
72
|
+
res = await fetch(url, init);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
const message =
|
|
76
|
+
err.name === 'AbortError' ? `请求超时(${cfg.timeoutMs}ms)` : `网络错误:${err.message}`;
|
|
77
|
+
throw new WpError(message, { method, path, network: true });
|
|
78
|
+
}
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
|
|
81
|
+
const text = await res.text();
|
|
82
|
+
let data;
|
|
83
|
+
try {
|
|
84
|
+
data = text ? JSON.parse(text) : null;
|
|
85
|
+
} catch {
|
|
86
|
+
data = text;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!res.ok) {
|
|
90
|
+
const message =
|
|
91
|
+
data && typeof data === 'object' && data.message
|
|
92
|
+
? data.message
|
|
93
|
+
: typeof data === 'string' && data
|
|
94
|
+
? data.slice(0, 300)
|
|
95
|
+
: res.statusText;
|
|
96
|
+
throw new WpError(`${res.status} ${message}`, {
|
|
97
|
+
status: res.status,
|
|
98
|
+
code: data && typeof data === 'object' ? data.code : undefined,
|
|
99
|
+
data,
|
|
100
|
+
method,
|
|
101
|
+
path,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { status: res.status, data, headers: res.headers };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const IDEMPOTENT = new Set(['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE']);
|
|
109
|
+
|
|
110
|
+
async function request(method, path, opts = {}) {
|
|
111
|
+
const upper = String(method).toUpperCase();
|
|
112
|
+
|
|
113
|
+
if ((opts.body !== undefined || opts.form) && (upper === 'GET' || upper === 'HEAD')) {
|
|
114
|
+
throw new WpError(`${upper} 请求不能带 body(--data 仅用于写操作)`, { method: upper, path });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let lastError;
|
|
118
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
119
|
+
try {
|
|
120
|
+
return await once(upper, path, opts);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
lastError = err;
|
|
123
|
+
// 429 / 5xx 视为可重试(服务端未成功处理);
|
|
124
|
+
// 网络错误只对幂等方法重试,避免 POST 创建重复内容。
|
|
125
|
+
const retryable = err.network
|
|
126
|
+
? IDEMPOTENT.has(upper)
|
|
127
|
+
: err.status === 429 || (err.status >= 500 && err.status <= 599);
|
|
128
|
+
if (!retryable || attempt === retries) throw err;
|
|
129
|
+
await sleep(retryDelayMs * (attempt + 1));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
throw lastError;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { request, root: cfg.url };
|
|
136
|
+
}
|