@mathcrowd/mmarked 2.0.2 → 3.0.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/CHANGELOG.md +25 -0
- package/LICENSE_SYSTEM_DOCS.md +341 -0
- package/README.md +290 -54
- package/README.zh.md +291 -54
- package/dist/browser.umd.js +1 -1
- package/dist/demo.esm.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +84 -2
- package/dist/index.mjs +1 -1
- package/package.json +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
# [3.0.0](https://cloud.mathcrowd.cn:2444/agile/frontend/mathcrowd-marked-lib/compare/v2.0.3...v3.0.0) (2025-12-25)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **ci:** add contents write permission to release workflow ([9f4d3cc](https://cloud.mathcrowd.cn:2444/agile/frontend/mathcrowd-marked-lib/commits/9f4d3cc60d410a93f015796f3e6023f7098b982f))
|
|
7
|
+
* **ci:** remove tags-only restriction from github sync job ([5e54dd2](https://cloud.mathcrowd.cn:2444/agile/frontend/mathcrowd-marked-lib/commits/5e54dd2114cc67eab5d6bc019ad161052dbc1484))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* add Node.js-only license validation with browser exemption ([acb4f1a](https://cloud.mathcrowd.cn:2444/agile/frontend/mathcrowd-marked-lib/commits/acb4f1a5c75f4fe5ebb4434dbbdc970664a2f048))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## [2.0.3](https://cloud.mathcrowd.cn:2444/agile/frontend/mathcrowd-marked-lib/compare/v2.0.2...v2.0.3) (2025-10-12)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
* **build:** preserve .gitignore in docs directory ([f9c1e79](https://cloud.mathcrowd.cn:2444/agile/frontend/mathcrowd-marked-lib/commits/f9c1e792c8d79311fa98c30eb6cb794cfaa870c6))
|
|
22
|
+
* **ci:** remove build steps from GitHub release workflow ([d3d74c6](https://cloud.mathcrowd.cn:2444/agile/frontend/mathcrowd-marked-lib/commits/d3d74c63706546c848d6a64f71dc4281cec76414))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
1
26
|
## [2.0.2](https://cloud.mathcrowd.cn:2444/agile/frontend/mathcrowd-marked-lib/compare/v2.0.1...v2.0.2) (2025-10-12)
|
|
2
27
|
|
|
3
28
|
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
# MMarked 许可证系统状态文档
|
|
2
|
+
|
|
3
|
+
## 📋 系统概述
|
|
4
|
+
|
|
5
|
+
MMarked 是一个用于教育和数学内容的 Markdown 渲染库,支持 LaTeX 公式、定理块等高级功能。本文档描述了其许可证管理系统的工作原理和后端接口规范。
|
|
6
|
+
|
|
7
|
+
## 🎯 许可证系统设计目标
|
|
8
|
+
|
|
9
|
+
- ✅ **服务端商业使用收费**:Node.js 环境下的商业使用需要许可证
|
|
10
|
+
- ✅ **客户端免费使用**:浏览器环境完全免费,无任何限制
|
|
11
|
+
- ✅ **非侵入式警告**:未授权使用时显示友好的警告信息
|
|
12
|
+
- ✅ **使用情况统计**:自动收集和报告使用统计数据
|
|
13
|
+
- ✅ **安全性优先**:端点地址硬编码,防止恶意修改
|
|
14
|
+
|
|
15
|
+
## 🔧 系统架构
|
|
16
|
+
|
|
17
|
+
### 前端/客户端功能
|
|
18
|
+
|
|
19
|
+
#### 许可证验证逻辑
|
|
20
|
+
- **环境检测**:通过 `process` 对象检测是否在 Node.js 环境中
|
|
21
|
+
- **浏览器豁免**:浏览器环境 (`typeof process === 'undefined'`) 完全跳过验证
|
|
22
|
+
- **同步检查**:`isLicensed()` 函数在浏览器中总是返回 `false`
|
|
23
|
+
- **警告注入**:只在 Node.js 环境中检查,未授权时每 1000 次调用注入 SVG 警告
|
|
24
|
+
|
|
25
|
+
#### 统计数据收集
|
|
26
|
+
- **调用跟踪**:每次调用 `renderMarkdown()` 或 `renderMarkdownCompact()` 时记录
|
|
27
|
+
- **内存存储**:统计数据存储在内存中,随应用重启清零
|
|
28
|
+
- **Session ID机制**:每个应用实例生成唯一session_id,后端按IP或api-key汇总跨部署数据
|
|
29
|
+
- **数据字段**:
|
|
30
|
+
```typescript
|
|
31
|
+
interface UsageStats {
|
|
32
|
+
totalCalls: number // 总调用次数
|
|
33
|
+
renderMarkdownCalls: number // renderMarkdown 调用次数
|
|
34
|
+
renderMarkdownCompactCalls: number // renderMarkdownCompact 调用次数
|
|
35
|
+
firstUsedAt: number // 首次使用时间戳
|
|
36
|
+
lastUsedAt: number // 最后使用时间戳
|
|
37
|
+
apiKey?: string // API 密钥
|
|
38
|
+
nodeVersion?: string // Node.js 版本
|
|
39
|
+
sessionId?: string // 会话ID(跨部署汇总用)
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 服务端功能
|
|
44
|
+
|
|
45
|
+
#### 许可证配置
|
|
46
|
+
```typescript
|
|
47
|
+
configureLicense({
|
|
48
|
+
apiKey: 'MMARKED-XXXX-XXXX-XXXX-XXXX' // 许可证密钥
|
|
49
|
+
})
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
#### 自动报告机制
|
|
53
|
+
- **报告频率**:每小时一次 (60 * 60 * 1000 毫秒)
|
|
54
|
+
- **触发时机**:许可证配置后自动启动定时器
|
|
55
|
+
- **报告内容**:累积统计数据(从应用启动开始的所有调用)
|
|
56
|
+
- **重复报告防护**:记录最后报告时间,避免重启后重复发送相同数据
|
|
57
|
+
- **失败处理**:静默失败,不影响用户正常使用
|
|
58
|
+
|
|
59
|
+
#### 警告系统
|
|
60
|
+
- **警告频率**:每 1000 次未授权调用注入一次警告
|
|
61
|
+
- **警告格式**:伪装成 SVG 数学公式的 HTML 片段
|
|
62
|
+
- **警告内容**:
|
|
63
|
+
- 非商业使用警告
|
|
64
|
+
- 联系方式 (charles@mathcrowd.cn)
|
|
65
|
+
- 许可证配置指引
|
|
66
|
+
|
|
67
|
+
## 📊 数据流图
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
|
71
|
+
│ 客户端调用 │───▶│ 统计数据收集 │───▶│ 定时报告发送 │
|
|
72
|
+
│ │ │ │ │ │
|
|
73
|
+
│ • renderMarkdown│ │ • totalCalls++ │ │ • 每小时一次 │
|
|
74
|
+
│ • renderCompact │ │ • 生成sessionId │ │ • POST /report-usage │
|
|
75
|
+
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
|
76
|
+
│
|
|
77
|
+
▼
|
|
78
|
+
┌──────────────────┐ ┌──────────────────┐
|
|
79
|
+
│ 后端数据汇总 │───▶│ 跨部署统计 │
|
|
80
|
+
│ │ │ │
|
|
81
|
+
│ • 按IP汇总 │ │ • 按api-key汇总 │
|
|
82
|
+
│ • 按sessionId分组│ │ • 累计使用量 │
|
|
83
|
+
└──────────────────┘ └──────────────────┘
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## ⚠️ 警告注入机制
|
|
87
|
+
|
|
88
|
+
### 触发条件
|
|
89
|
+
- 运行在 Node.js 环境
|
|
90
|
+
- 未配置有效许可证
|
|
91
|
+
- 总调用次数达到 1000 的倍数
|
|
92
|
+
|
|
93
|
+
### 警告外观
|
|
94
|
+
```html
|
|
95
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="MJX-svg-equation" role="img" focusable="false"
|
|
96
|
+
viewBox="0 0 800 80" style="vertical-align: -0.5ex; margin: 1em 0; max-width: 100%; display: block;"
|
|
97
|
+
aria-label="Non-commercial use warning" data-formula-id="随机ID" data-timestamp="时间戳">
|
|
98
|
+
<defs>
|
|
99
|
+
<style type="text/css">
|
|
100
|
+
.mjx-warning-text { font-family: Arial, sans-serif; font-size: 14px; fill: #856404; }
|
|
101
|
+
.mjx-warning-link { font-family: Arial, sans-serif; font-size: 14px; fill: #0066cc; text-decoration: underline; cursor: pointer; }
|
|
102
|
+
</style>
|
|
103
|
+
</defs>
|
|
104
|
+
<rect width="800" height="80" fill="#fff3cd" stroke="#ffc107" stroke-width="1" rx="4"/>
|
|
105
|
+
<text x="20" y="25" class="mjx-warning-text" font-weight="bold">⚠️ Non-Commercial Use Only</text>
|
|
106
|
+
<text x="20" y="45" class="mjx-warning-text">This content uses @mathcrowd/mmarked without valid commercial license.</text>
|
|
107
|
+
<text x="20" y="65" class="mjx-warning-text">For commercial use, contact: </text>
|
|
108
|
+
<a href="mailto:charles@mathcrowd.cn">
|
|
109
|
+
<text x="220" y="65" class="mjx-warning-link">charles@mathcrowd.cn</text>
|
|
110
|
+
</a>
|
|
111
|
+
</svg>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
# 🔗 后端接口规范
|
|
117
|
+
|
|
118
|
+
## 1. 许可证验证接口
|
|
119
|
+
|
|
120
|
+
### 接口信息
|
|
121
|
+
- **URL**: `https://api.mathcrowd.cn/validate-license`
|
|
122
|
+
- **方法**: `POST`
|
|
123
|
+
- **内容类型**: `application/json`
|
|
124
|
+
|
|
125
|
+
### 请求体
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"apiKey": "MMARKED-XXXX-XXXX-XXXX-XXXX"
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 响应格式
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"valid": true|false,
|
|
136
|
+
"message": "验证结果描述(可选)",
|
|
137
|
+
"expiresAt": "2024-12-31T23:59:59Z(可选)",
|
|
138
|
+
"tier": "free|basic|pro|enterprise(可选)"
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### 错误处理
|
|
143
|
+
- **网络错误**: 客户端静默失败,继续使用(假设有效)
|
|
144
|
+
- **无效许可证**: 返回 `{"valid": false, "message": "License key is invalid"}`
|
|
145
|
+
- **服务器错误**: 返回 `{"valid": false, "message": "Server error"}`
|
|
146
|
+
|
|
147
|
+
## 2. 使用统计报告接口
|
|
148
|
+
|
|
149
|
+
### 接口信息
|
|
150
|
+
- **URL**: `https://api.mathcrowd.cn/report-usage`
|
|
151
|
+
- **方法**: `POST`
|
|
152
|
+
- **内容类型**: `application/json`
|
|
153
|
+
- **频率**: 每小时一次(客户端定时发送)
|
|
154
|
+
|
|
155
|
+
### 请求体
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"totalCalls": 1500,
|
|
159
|
+
"renderMarkdownCalls": 1200,
|
|
160
|
+
"renderMarkdownCompactCalls": 300,
|
|
161
|
+
"firstUsedAt": 1703452800000,
|
|
162
|
+
"lastUsedAt": 1703456400000,
|
|
163
|
+
"apiKey": "MMARKED-XXXX-XXXX-XXXX-XXXX",
|
|
164
|
+
"timestamp": 1703456400000,
|
|
165
|
+
"nodeVersion": "18.17.0",
|
|
166
|
+
"sessionId": "550e8400-e29b-41d4-a716-446655440000"
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### 字段说明
|
|
171
|
+
- **totalCalls**: 从应用启动开始的总调用次数
|
|
172
|
+
- **renderMarkdownCalls**: `renderMarkdown()` 函数调用次数
|
|
173
|
+
- **renderMarkdownCompactCalls**: `renderMarkdownCompact()` 函数调用次数
|
|
174
|
+
- **firstUsedAt**: 首次使用时间戳(毫秒)
|
|
175
|
+
- **lastUsedAt**: 最后使用时间戳(毫秒)
|
|
176
|
+
- **apiKey**: 许可证密钥
|
|
177
|
+
- **timestamp**: 报告发送时间戳(毫秒)
|
|
178
|
+
- **nodeVersion**: Node.js 版本字符串
|
|
179
|
+
- **sessionId**: 唯一会话标识符,用于跨部署数据汇总
|
|
180
|
+
|
|
181
|
+
### 响应要求
|
|
182
|
+
- **成功响应**: HTTP 200-299 状态码
|
|
183
|
+
- **失败处理**: 客户端对任何非 2xx 响应都静默忽略
|
|
184
|
+
- **超时**: 客户端设置合理的超时时间(建议 30 秒)
|
|
185
|
+
|
|
186
|
+
### 安全考虑
|
|
187
|
+
- **IP 提取**: 从 HTTP 请求头中提取客户端 IP 地址
|
|
188
|
+
- **数据验证**: 验证 apiKey 格式和必需字段
|
|
189
|
+
- **速率限制**: 防止恶意高频报告
|
|
190
|
+
- **数据存储**: 安全存储统计数据,用于分析和计费
|
|
191
|
+
|
|
192
|
+
## 3. 实现建议
|
|
193
|
+
|
|
194
|
+
### 后端数据存储
|
|
195
|
+
```sql
|
|
196
|
+
-- 使用统计表(按session存储原始数据)
|
|
197
|
+
CREATE TABLE usage_reports (
|
|
198
|
+
id SERIAL PRIMARY KEY,
|
|
199
|
+
api_key VARCHAR(255) NOT NULL,
|
|
200
|
+
client_ip INET,
|
|
201
|
+
session_id VARCHAR(255) NOT NULL, -- 新增:会话ID
|
|
202
|
+
total_calls INTEGER NOT NULL,
|
|
203
|
+
render_markdown_calls INTEGER NOT NULL,
|
|
204
|
+
render_compact_calls INTEGER NOT NULL,
|
|
205
|
+
first_used_at TIMESTAMP NOT NULL,
|
|
206
|
+
last_used_at TIMESTAMP NOT NULL,
|
|
207
|
+
report_timestamp TIMESTAMP NOT NULL,
|
|
208
|
+
node_version VARCHAR(50),
|
|
209
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
-- 汇总统计表(按IP和API密钥汇总)
|
|
213
|
+
CREATE TABLE usage_aggregates (
|
|
214
|
+
id SERIAL PRIMARY KEY,
|
|
215
|
+
api_key VARCHAR(255) NOT NULL,
|
|
216
|
+
client_ip INET,
|
|
217
|
+
total_sessions INTEGER DEFAULT 0, -- 会话总数
|
|
218
|
+
total_calls BIGINT DEFAULT 0, -- 累计调用次数
|
|
219
|
+
render_markdown_calls BIGINT DEFAULT 0, -- 累计renderMarkdown调用
|
|
220
|
+
render_compact_calls BIGINT DEFAULT 0, -- 累计renderCompact调用
|
|
221
|
+
first_used_at TIMESTAMP, -- 首次使用时间
|
|
222
|
+
last_used_at TIMESTAMP, -- 最后使用时间
|
|
223
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
224
|
+
UNIQUE(api_key, client_ip) -- 每个IP+API密钥的唯一记录
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
-- 许可证表
|
|
228
|
+
CREATE TABLE licenses (
|
|
229
|
+
id SERIAL PRIMARY KEY,
|
|
230
|
+
api_key VARCHAR(255) UNIQUE NOT NULL,
|
|
231
|
+
valid BOOLEAN DEFAULT true,
|
|
232
|
+
expires_at TIMESTAMP,
|
|
233
|
+
tier VARCHAR(20) DEFAULT 'basic',
|
|
234
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
235
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
236
|
+
);
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### API 端点实现示例(Node.js/Express)
|
|
240
|
+
```javascript
|
|
241
|
+
// 许可证验证
|
|
242
|
+
app.post('/validate-license', async (req, res) => {
|
|
243
|
+
const { apiKey } = req.body;
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const license = await db.query('SELECT * FROM licenses WHERE api_key = $1', [apiKey]);
|
|
247
|
+
|
|
248
|
+
if (license.rows.length === 0) {
|
|
249
|
+
return res.json({ valid: false, message: 'License key not found' });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const lic = license.rows[0];
|
|
253
|
+
const isValid = lic.valid && (!lic.expires_at || new Date() < lic.expires_at);
|
|
254
|
+
|
|
255
|
+
res.json({
|
|
256
|
+
valid: isValid,
|
|
257
|
+
message: isValid ? 'License is valid' : 'License expired or invalid',
|
|
258
|
+
expiresAt: lic.expires_at,
|
|
259
|
+
tier: lic.tier
|
|
260
|
+
});
|
|
261
|
+
} catch (error) {
|
|
262
|
+
console.error('License validation error:', error);
|
|
263
|
+
res.status(500).json({ valid: false, message: 'Server error' });
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// 使用统计报告
|
|
268
|
+
app.post('/report-usage', async (req, res) => {
|
|
269
|
+
const clientIp = req.ip || req.connection.remoteAddress;
|
|
270
|
+
const reportData = req.body;
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
// 1. 存储原始会话数据
|
|
274
|
+
await db.query(`
|
|
275
|
+
INSERT INTO usage_reports
|
|
276
|
+
(api_key, client_ip, session_id, total_calls, render_markdown_calls, render_compact_calls,
|
|
277
|
+
first_used_at, last_used_at, report_timestamp, node_version)
|
|
278
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
|
279
|
+
`, [
|
|
280
|
+
reportData.apiKey,
|
|
281
|
+
clientIp,
|
|
282
|
+
reportData.sessionId, // 新增sessionId
|
|
283
|
+
reportData.totalCalls,
|
|
284
|
+
reportData.renderMarkdownCalls,
|
|
285
|
+
reportData.renderMarkdownCompactCalls,
|
|
286
|
+
new Date(reportData.firstUsedAt),
|
|
287
|
+
new Date(reportData.lastUsedAt),
|
|
288
|
+
new Date(reportData.timestamp),
|
|
289
|
+
reportData.nodeVersion
|
|
290
|
+
]);
|
|
291
|
+
|
|
292
|
+
// 2. 更新汇总统计(按IP+API密钥)
|
|
293
|
+
await db.query(`
|
|
294
|
+
INSERT INTO usage_aggregates
|
|
295
|
+
(api_key, client_ip, total_sessions, total_calls, render_markdown_calls, render_compact_calls,
|
|
296
|
+
first_used_at, last_used_at, updated_at)
|
|
297
|
+
VALUES ($1, $2, 1, $3, $4, $5, $6, $7, CURRENT_TIMESTAMP)
|
|
298
|
+
ON CONFLICT (api_key, client_ip) DO UPDATE SET
|
|
299
|
+
total_sessions = usage_aggregates.total_sessions + 1,
|
|
300
|
+
total_calls = usage_aggregates.total_calls + EXCLUDED.total_calls,
|
|
301
|
+
render_markdown_calls = usage_aggregates.render_markdown_calls + EXCLUDED.render_markdown_calls,
|
|
302
|
+
render_compact_calls = usage_aggregates.render_compact_calls + EXCLUDED.render_compact_calls,
|
|
303
|
+
first_used_at = LEAST(usage_aggregates.first_used_at, EXCLUDED.first_used_at),
|
|
304
|
+
last_used_at = GREATEST(usage_aggregates.last_used_at, EXCLUDED.last_used_at),
|
|
305
|
+
updated_at = CURRENT_TIMESTAMP
|
|
306
|
+
`, [
|
|
307
|
+
reportData.apiKey,
|
|
308
|
+
clientIp,
|
|
309
|
+
reportData.totalCalls,
|
|
310
|
+
reportData.renderMarkdownCalls,
|
|
311
|
+
reportData.renderMarkdownCompactCalls,
|
|
312
|
+
new Date(reportData.firstUsedAt),
|
|
313
|
+
new Date(reportData.lastUsedAt)
|
|
314
|
+
]);
|
|
315
|
+
|
|
316
|
+
res.json({ success: true });
|
|
317
|
+
} catch (error) {
|
|
318
|
+
console.error('Usage report error:', error);
|
|
319
|
+
res.status(500).json({ error: 'Failed to save usage report' });
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## 📈 监控和分析
|
|
325
|
+
|
|
326
|
+
### 关键指标
|
|
327
|
+
- **每日活跃用户**: 基于唯一 IP 地址统计
|
|
328
|
+
- **使用频率**: 平均每小时调用次数
|
|
329
|
+
- **许可证采用率**: 配置许可证的用户比例
|
|
330
|
+
- **功能使用分布**: renderMarkdown vs renderMarkdownCompact 调用比例
|
|
331
|
+
- **会话统计**: 每个IP的平均会话数和总会话数
|
|
332
|
+
- **跨部署使用量**: 按IP和API密钥汇总的累计使用量
|
|
333
|
+
|
|
334
|
+
### 告警规则
|
|
335
|
+
- 异常高频报告(可能表示滥用)
|
|
336
|
+
- 许可证过期提醒
|
|
337
|
+
- 服务器错误率监控
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
*最后更新: 2024年12月25日*
|