@rongyan/wxpost-server 0.1.0 → 0.1.2
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 +18 -11
- package/bin/wxpost-server.js +17 -1
- package/images/init.png +0 -0
- package/package.json +2 -1
- package/src/draft.js +1 -2
- package/src/index.js +3 -2
- package/src/logger.js +11 -6
package/README.md
CHANGED
|
@@ -11,18 +11,20 @@
|
|
|
11
11
|
## 安装
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npm install -g @rongyan/wxpost-server
|
|
14
|
+
npm install -g @rongyan/wxpost-server@latest
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
## 初始化配置
|
|
18
|
+
|
|
19
|
+
首次运行以下命令,程序会自动在 `~/.@rongyan/env.json` 创建配置模板:
|
|
18
20
|
|
|
19
21
|
```bash
|
|
20
|
-
|
|
22
|
+
wxpost-server --help
|
|
21
23
|
```
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+

|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
编辑 `~/.@rongyan/env.json`,填入真实值:
|
|
26
28
|
|
|
27
29
|
```json
|
|
28
30
|
{
|
|
@@ -52,21 +54,26 @@ npx @rongyan/wxpost-server
|
|
|
52
54
|
## 启动
|
|
53
55
|
|
|
54
56
|
```bash
|
|
55
|
-
#
|
|
57
|
+
# 直接启动
|
|
56
58
|
wxpost-server
|
|
57
59
|
|
|
58
|
-
#
|
|
60
|
+
# 指定端口
|
|
59
61
|
wxpost-server --port 8080
|
|
60
62
|
|
|
61
63
|
# 通过环境变量指定端口
|
|
62
64
|
PORT=8080 wxpost-server
|
|
63
|
-
|
|
64
|
-
# 开发模式(文件变更自动重启)
|
|
65
|
-
npm run dev
|
|
66
65
|
```
|
|
67
66
|
|
|
68
67
|
端口优先级:`--port 参数` > `PORT 环境变量` > `配置文件 port` > `默认 3000`
|
|
69
68
|
|
|
69
|
+
### 使用 PM2(推荐生产环境)
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pm2 start wxpost-server --name wxpost-server -- --port 3000
|
|
73
|
+
pm2 save # 保存进程列表
|
|
74
|
+
pm2 startup # 设置开机自启(按提示执行输出的命令)
|
|
75
|
+
```
|
|
76
|
+
|
|
70
77
|
## IP 白名单
|
|
71
78
|
|
|
72
79
|
微信接口要求服务器 IP 在白名单内。配置路径:
|
|
@@ -223,7 +230,7 @@ curl -X POST http://localhost:3000/upload-material \
|
|
|
223
230
|
| `title` | string | 是 | 超过 64 字自动截断 |
|
|
224
231
|
| `author` | string | 否 | 最多 16 字 |
|
|
225
232
|
| `digest` | string | 否 | 摘要,最多 128 字;不填则从正文自动截取 |
|
|
226
|
-
| `content` | string | 是 | 正文 HTML
|
|
233
|
+
| `content` | string | 是 | 正文 HTML |
|
|
227
234
|
| `content_source_url` | string | 否 | 点击"阅读原文"跳转的 URL |
|
|
228
235
|
| `thumb_media_id` | string | news 类型必填 | 封面图的永久素材 MediaID |
|
|
229
236
|
| `image_info` | object | newspic 类型必填 | 图片列表,`{ "list": [...] }`,最多 20 张 |
|
package/bin/wxpost-server.js
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
|
|
7
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
8
|
+
console.log(`
|
|
9
|
+
Usage: wxpost-server [--port <n>]
|
|
10
|
+
|
|
11
|
+
Options:
|
|
12
|
+
--port <n> 监听端口(优先级高于配置文件和 PORT 环境变量)
|
|
13
|
+
--help, -h 显示帮助
|
|
14
|
+
|
|
15
|
+
首次运行会自动在 ~/.@rongyan/env.json 创建配置模板。
|
|
16
|
+
配置文件路径:~/.@rongyan/env.json
|
|
17
|
+
`);
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
|
|
5
21
|
const { startServer } = require('../src/index.js');
|
|
6
22
|
|
|
7
23
|
// 解析 --port <n> 或 --port=<n>
|
|
@@ -14,5 +30,5 @@ function parsePort(args) {
|
|
|
14
30
|
return null;
|
|
15
31
|
}
|
|
16
32
|
|
|
17
|
-
const cliPort = parsePort(
|
|
33
|
+
const cliPort = parsePort(args);
|
|
18
34
|
startServer({ port: cliPort });
|
package/images/init.png
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rongyan/wxpost-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "微信公众号内容发布服务,支持多账号,提供图片上传、草稿管理和发布等 HTTP 接口",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"bin",
|
|
11
11
|
"src",
|
|
12
|
+
"images",
|
|
12
13
|
"README.md"
|
|
13
14
|
],
|
|
14
15
|
"scripts": {
|
package/src/draft.js
CHANGED
|
@@ -36,10 +36,9 @@ function validateArticle(a, idx) {
|
|
|
36
36
|
if (a.digest.length > 128) return `${p}.digest 最多 128 字,当前 ${a.digest.length} 字`;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
// content
|
|
39
|
+
// content:必填
|
|
40
40
|
if (!a.content) return `${p}.content 必填`;
|
|
41
41
|
if (typeof a.content !== 'string') return `${p}.content 须为字符串`;
|
|
42
|
-
if (a.content.length > 20000) return `${p}.content 最多 2 万字,当前 ${a.content.length} 字`;
|
|
43
42
|
|
|
44
43
|
// content_source_url:选填
|
|
45
44
|
if (a.content_source_url !== undefined && typeof a.content_source_url !== 'string') {
|
package/src/index.js
CHANGED
|
@@ -14,7 +14,7 @@ const { addImageMaterial, MAX_SIZE: MATERIAL_MAX_SIZE } = require('./material');
|
|
|
14
14
|
const { checkApiKey } = require('./auth');
|
|
15
15
|
const logger = require('./logger');
|
|
16
16
|
|
|
17
|
-
const BODY_LIMIT =
|
|
17
|
+
const BODY_LIMIT = 20 * 1024 * 1024; // 20MB,正文富文本可能较大,具体长度由微信 API 判定
|
|
18
18
|
|
|
19
19
|
function sendJSON(res, statusCode, data) {
|
|
20
20
|
if (statusCode >= 400 && data.error) res._logError = data.error;
|
|
@@ -80,7 +80,8 @@ function receiveFile(req, res, account, sizeLimit, processor, sizeLimitLabel) {
|
|
|
80
80
|
let settled = false;
|
|
81
81
|
|
|
82
82
|
bb.on('file', (_fieldname, stream, info) => {
|
|
83
|
-
|
|
83
|
+
// busboy 以 binary 解析 multipart filename,需还原为 UTF-8
|
|
84
|
+
const filename = Buffer.from(info.filename, 'binary').toString('utf-8');
|
|
84
85
|
const chunks = [];
|
|
85
86
|
|
|
86
87
|
stream.on('data', (chunk) => chunks.push(chunk));
|
package/src/logger.js
CHANGED
|
@@ -32,7 +32,7 @@ function getStream() {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
const logFile = path.join(_logDir, `${today}.log`);
|
|
35
|
-
_stream = fs.createWriteStream(logFile, { flags: 'a'
|
|
35
|
+
_stream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
36
36
|
_stream.on('error', () => {
|
|
37
37
|
// 写文件出错(如磁盘满)时重置 stream,下次重试创建;不崩进程
|
|
38
38
|
_stream = null;
|
|
@@ -62,14 +62,19 @@ function pruneOldLogs(logDir) {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
function formatLocalTime(d) {
|
|
66
|
+
const pad2 = (n) => String(n).padStart(2, '0');
|
|
67
|
+
const pad3 = (n) => String(n).padStart(3, '0');
|
|
68
|
+
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ` +
|
|
69
|
+
`${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}.${pad3(d.getMilliseconds())}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
65
72
|
function write(level, message) {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
const line = `[${ts}] [${level}] ${message}\n`;
|
|
69
|
-
process.stdout.write(line);
|
|
73
|
+
const line = `[${formatLocalTime(new Date())}] [${level}] ${message}\n`;
|
|
74
|
+
process.stdout.write(Buffer.from(line, 'utf-8'));
|
|
70
75
|
try {
|
|
71
76
|
const stream = getStream();
|
|
72
|
-
if (stream) stream.write(line);
|
|
77
|
+
if (stream) stream.write(Buffer.from(line, 'utf-8'));
|
|
73
78
|
} catch {
|
|
74
79
|
// 写文件失败不影响主流程
|
|
75
80
|
}
|