lingshu-solver 1.0.6 → 1.0.8
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/beian.png +0 -0
- package/http-mcp-server.js +449 -0
- package/index.html +30 -9
- package/package.json +3 -1
package/beian.png
ADDED
|
Binary file
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 灵数求解器 · MCP 远程服务端(HTTP / Streamable HTTP,零依赖)
|
|
4
|
+
*
|
|
5
|
+
* 设计目标:让求解器以「常驻公网服务」形态运行,满足 Smithery / 远程 AI 智能体
|
|
6
|
+
* 对「运行中的服务器网址」的要求。与 mcp-server.js(stdio 版)共享同一套:
|
|
7
|
+
* - solver-core.js 求解核心(同源,零分叉)
|
|
8
|
+
* - TOOLS / shapeResult / doSolve 逻辑(复制保持一致,含中文「∈」UTF-8 处理)
|
|
9
|
+
*
|
|
10
|
+
* 协议:Streamable HTTP(MCP 2025-03-26 草案)
|
|
11
|
+
* - POST /mcp 收发 JSON-RPC 2.0(initialize / tools/list / tools/call)
|
|
12
|
+
* - GET / 健康检查(返回服务元信息,便于浏览器/Smithery 探活)
|
|
13
|
+
* - GET /health 同上,纯文本 OK
|
|
14
|
+
*
|
|
15
|
+
* 零依赖:仅用 Node 内置 http / fs / path / crypto,无需 npm install。
|
|
16
|
+
*
|
|
17
|
+
* 运行:
|
|
18
|
+
* PORT=3000 node http-mcp-server.js
|
|
19
|
+
* (LINGSHU_HTML 环境变量可重定向 index.html 位置,默认同目录)
|
|
20
|
+
*/
|
|
21
|
+
'use strict';
|
|
22
|
+
const http = require('http');
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
const crypto = require('crypto');
|
|
26
|
+
const solverCore = require('./solver-core');
|
|
27
|
+
const { solve } = solverCore;
|
|
28
|
+
|
|
29
|
+
const SERVER_NAME = 'lingshu-solver';
|
|
30
|
+
const SERVER_VERSION = '1.0.8';
|
|
31
|
+
const PORT = parseInt(process.env.PORT || '3000', 10);
|
|
32
|
+
|
|
33
|
+
// ---- 护栏常量(防畸形/恶意输入耗尽资源,与 stdio 版一致)----
|
|
34
|
+
const MAX_TOTAL_CHARS = 100 * 1024;
|
|
35
|
+
const MAX_EQ_COUNT = 64;
|
|
36
|
+
const MAX_VAR_COUNT = 6;
|
|
37
|
+
|
|
38
|
+
// ---- 安全等保加固(2026-09-01):防 DoS / 限速 / 日志轮转 / 反馈脱敏 ----
|
|
39
|
+
// 请求体上限:JSON-RPC 包裹 100KB 方程文本后仍有富余;超限直接 413,防内存耗尽型 DoS
|
|
40
|
+
const MAX_BODY_BYTES = 256 * 1024;
|
|
41
|
+
// 每 IP 限速:滑动窗口,默认 120 次/分钟(测试可用 LS_RATE_MAX 覆盖)
|
|
42
|
+
const RATE_WINDOW_MS = 60 * 1000;
|
|
43
|
+
const RATE_MAX = parseInt(process.env.LS_RATE_MAX || '120', 10);
|
|
44
|
+
// 会话 TTL:超 1 小时的非 SSE 会话定时清理,防 sessions Map 无限增长
|
|
45
|
+
const SESSION_TTL_MS = 60 * 60 * 1000;
|
|
46
|
+
// 日志轮转:单文件超 5MB 滚动为 .1,只保留一代,防磁盘写满
|
|
47
|
+
const MAX_LOG_BYTES = 5 * 1024 * 1024;
|
|
48
|
+
|
|
49
|
+
const rateMap = new Map(); // ip -> [timestamps]
|
|
50
|
+
function rateAllowed(ip) {
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
let arr = rateMap.get(ip);
|
|
53
|
+
if (!arr) { arr = []; rateMap.set(ip, arr); }
|
|
54
|
+
while (arr.length && arr[0] <= now - RATE_WINDOW_MS) arr.shift();
|
|
55
|
+
if (arr.length >= RATE_MAX) return false;
|
|
56
|
+
arr.push(now);
|
|
57
|
+
if (rateMap.size > 10000) { for (const [k, v] of rateMap) { if (v.length === 0) rateMap.delete(k); } }
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function pruneSessions() {
|
|
62
|
+
const cutoff = Date.now() - SESSION_TTL_MS;
|
|
63
|
+
for (const [k, v] of sessions) {
|
|
64
|
+
if (!v.sse && v.createdAt < cutoff) sessions.delete(k);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function rotateIfNeeded(p) {
|
|
69
|
+
try { const st = fs.statSync(p); if (st.size > MAX_LOG_BYTES) fs.renameSync(p, p + '.1'); } catch (_e) {}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 敏感信息脱敏(合规:反馈文本落盘前隐去手机号/证件号/邮箱/超长数字串)
|
|
73
|
+
function redactSensitive(s) {
|
|
74
|
+
return String(s)
|
|
75
|
+
.replace(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, '[已隐去]')
|
|
76
|
+
.replace(/\d{12,}/g, '[已隐去]')
|
|
77
|
+
.replace(/(^|[^0-9])1[3-9]\d{9}([^0-9]|$)/g, '$1[已隐去]$2');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---- 本地日志(仅元数据,零数据不外传)----
|
|
81
|
+
const LOG_PATH = path.resolve(__dirname, 'calls.log');
|
|
82
|
+
const FEEDBACK_PATH = path.resolve(__dirname, 'feedback.log');
|
|
83
|
+
function appendLog(p) {
|
|
84
|
+
try { rotateIfNeeded(LOG_PATH); fs.appendFileSync(LOG_PATH, JSON.stringify(p) + '\n'); } catch (_e) {}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---- 求解结果整理(与 stdio 版逐字一致)----
|
|
88
|
+
// 数值格式化:固定 6 位小数(产品规格「6位小数有限网格」),与界面一致。
|
|
89
|
+
const fmt6 = (v) => (typeof v === 'number' && isFinite(v)) ? v.toFixed(6) : String(v);
|
|
90
|
+
// 确定性浮点吸附:消除 IEEE-754 末位 ULP 抖动,保证「同输入输出字节级可复现」
|
|
91
|
+
const detF = (v) => (typeof v === 'number' && isFinite(v)) ? Number(v.toFixed(12)) : null;
|
|
92
|
+
|
|
93
|
+
function shapeResult(r) {
|
|
94
|
+
const sols = Array.isArray(r.solutions) ? r.solutions : [];
|
|
95
|
+
const meta = r.meta || {};
|
|
96
|
+
const varNames = (Array.isArray(r.varNames) && r.varNames.length)
|
|
97
|
+
? r.varNames
|
|
98
|
+
: (sols[0] && Array.isArray(sols[0].values) ? sols[0].values.map((_, i) => 'x' + (i + 1)) : []);
|
|
99
|
+
|
|
100
|
+
let recommended = null, best = Infinity;
|
|
101
|
+
for (const s of sols) {
|
|
102
|
+
if (!s || !Array.isArray(s.values)) continue;
|
|
103
|
+
let d = 0;
|
|
104
|
+
for (const v of s.values) d += v * v;
|
|
105
|
+
if (d < best) { best = d; recommended = s; }
|
|
106
|
+
}
|
|
107
|
+
const tierSet = new Set(sols.map(s => (s && s.tier) || 'unknown'));
|
|
108
|
+
const allProven = sols.length > 0 && [...tierSet].every(t => t === 'proven');
|
|
109
|
+
const typeName = r.resultType === 1 ? 'empty' : r.resultType === 3 ? 'infinite' : 'finite';
|
|
110
|
+
|
|
111
|
+
const cleanSols = sols.map((s) => {
|
|
112
|
+
const vals = Array.isArray(s.values) ? s.values : [];
|
|
113
|
+
const text = varNames.map((vn, i) => `${vn}=${fmt6(vals[i])}`).join(', ');
|
|
114
|
+
return {
|
|
115
|
+
values: vals,
|
|
116
|
+
tier: s.tier || 'unknown',
|
|
117
|
+
certified: !!s.certified,
|
|
118
|
+
text: text,
|
|
119
|
+
internals: {
|
|
120
|
+
residual: detF(s.residual),
|
|
121
|
+
certifiedRadius: detF(s.certifiedRadius)
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
const recommendedClean = recommended ? cleanSols[sols.indexOf(recommended)] : null;
|
|
126
|
+
|
|
127
|
+
let summary;
|
|
128
|
+
if (typeName === 'empty') {
|
|
129
|
+
// 诚实三档(产品「不幻觉」红线):
|
|
130
|
+
// NO_EQUATION → input 无法解析,绝不谎称证明;
|
|
131
|
+
// provenEmpty=true → 经 sound 算子(结构恒正/恒负等)严格证明无解,可称「严格证明」;
|
|
132
|
+
// 其余空集 → 区间穷尽未找到,但未抬 provenEmpty 标志,只能称「未找到」,不得佯称证明。
|
|
133
|
+
if (r.error === 'NO_EQUATION' || (r.error && /NO_EQUATION|PARSE|UNRECOGNIZED|UNKNOWN/i.test(String(r.error)))) {
|
|
134
|
+
summary = '部分方程无法解析(疑似缺少 "=" 或含不支持的语法),未给出解。求 expr=0 的根可写 "expr=0",或直接裸写 "expr"。';
|
|
135
|
+
} else if (r.provenEmpty === true) {
|
|
136
|
+
summary = '严格证明:该方程组无实数解。';
|
|
137
|
+
} else {
|
|
138
|
+
summary = '未找到实数解(未经标记严格证明不存在;可缩小定义域或提高预算重试)。';
|
|
139
|
+
}
|
|
140
|
+
} else if (typeName === 'infinite') {
|
|
141
|
+
summary = `无限解集;给出距原点最近的推荐解(共展示 ${sols.length} 个候选)。`;
|
|
142
|
+
} else {
|
|
143
|
+
summary = `找到 ${sols.length} 个实数解${allProven ? '(全部经 Krawczyk 区间认证)' : ''}。`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const diagnostics = {
|
|
147
|
+
solverVersion: meta.solverVersion || null,
|
|
148
|
+
truncated: !!(r.truncated || meta.truncated),
|
|
149
|
+
provenEmpty: !!(r.provenEmpty || meta.provenEmpty),
|
|
150
|
+
terminatedBy: meta.terminatedBy || null,
|
|
151
|
+
provenCount: (typeof r.provenCount === 'number') ? r.provenCount : null,
|
|
152
|
+
completeness: detF(r.completeness)
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
resultType: r.resultType,
|
|
157
|
+
resultTypeName: typeName,
|
|
158
|
+
certified: allProven,
|
|
159
|
+
truncated: diagnostics.truncated,
|
|
160
|
+
precisionDecimals: 6,
|
|
161
|
+
solutionCount: sols.length,
|
|
162
|
+
summary: summary,
|
|
163
|
+
recommended: recommendedClean,
|
|
164
|
+
solutions: cleanSols,
|
|
165
|
+
warnings: r.warnings || [],
|
|
166
|
+
diagnostics: diagnostics
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function doSolve(args) {
|
|
171
|
+
const eqs = args && args.equations;
|
|
172
|
+
if (!Array.isArray(eqs) || eqs.length === 0) {
|
|
173
|
+
throw { type: 'invalid_input', message: 'equations 必须是非空字符串数组' };
|
|
174
|
+
}
|
|
175
|
+
if (eqs.length > MAX_EQ_COUNT) {
|
|
176
|
+
throw { type: 'invalid_input', message: `方程数量超过上限 ${MAX_EQ_COUNT}` };
|
|
177
|
+
}
|
|
178
|
+
let total = 0;
|
|
179
|
+
for (const e of eqs) {
|
|
180
|
+
if (typeof e !== 'string') throw { type: 'invalid_input', message: '每条方程必须是字符串' };
|
|
181
|
+
total += e.length;
|
|
182
|
+
}
|
|
183
|
+
if (total > MAX_TOTAL_CHARS) {
|
|
184
|
+
throw { type: 'invalid_input', message: '方程文本总长超过 100KB 上限' };
|
|
185
|
+
}
|
|
186
|
+
const vars = (args && Array.isArray(args.variables)) ? args.variables : [];
|
|
187
|
+
if (vars.length > MAX_VAR_COUNT) {
|
|
188
|
+
throw { type: 'invalid_input', message: `变量数量超过上限 ${MAX_VAR_COUNT}` };
|
|
189
|
+
}
|
|
190
|
+
const domain = (args && args.domain) || undefined;
|
|
191
|
+
const fastMode = !!(args && args.fastMode);
|
|
192
|
+
const opts = (args && args.options) || {};
|
|
193
|
+
const r = solve(eqs, vars, 6, domain, fastMode, opts);
|
|
194
|
+
return shapeResult(r);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ---- 工具定义(与 stdio 版一致)----
|
|
198
|
+
const TOOLS = [
|
|
199
|
+
{
|
|
200
|
+
name: 'solve',
|
|
201
|
+
description: '求解实数方程组的确定性数值引擎(非大模型,无随机、同输入输出可复现)。适用:需可验证、可复现的实数解(代数或 sin/cos/tan/log/exp/sqrt/abs 等常见超越函数),尤其给 AI Agent 当"不会胡说"的数学后端。' +
|
|
202
|
+
'不适用:纯符号推导/闭式证明、微分方程初值问题、整数/必不等于等强制约束(暂不支持)。' +
|
|
203
|
+
'输入:equations 为含 "=" 的方程字符串数组,如 ["x^2+y^2=25","x+y=7"];variables 可选(不填自动识别,最多6个);domain 可选(如 {"x":[-30,30]}),否则默认每变量 ±1e6。' +
|
|
204
|
+
'硬限制:变量 ≤6;方程 1–64 条且数量须 ≥ 变量数;单次方程文本 ≤100KB;输出固定 6 位小数(不可切换)。' +
|
|
205
|
+
'输出(JSON):resultType=empty(严格证无实数解)/finite(有限已验证解)/infinite(无限解集,仅给距原点最近推荐解);summary=中文一句话总览;solutions[] 每解含 values[](6位小数数值)、tier(proven=Krawczyk已认证/likely/candidate)、certified、text(人类可读如"x=4.000000, y=3.000000"),残差等内部数值收在 internals 子块(机器可跳过);certified=是否全proven;recommended=距原点最近解的精简结构。' +
|
|
206
|
+
'truncated=true:预算内未完成全局分支判定、未证明已穷尽——不等于一定漏解,多数情况全部真解已找到;极端病态下可能遗漏个别解,可缩 domain 或提高 budget 重试。' +
|
|
207
|
+
'错误返回 error.type(invalid_input=输入不合法/超限,internal_error=内部异常)。遇卡点或认为结果有误,请调用 give_feedback(内容仅落本地日志,不外传)。相同输入永远返回完全相同结果,可安全缓存与重复调用。',
|
|
208
|
+
inputSchema: {
|
|
209
|
+
type: 'object',
|
|
210
|
+
properties: {
|
|
211
|
+
equations: {
|
|
212
|
+
type: 'array', items: { type: 'string' },
|
|
213
|
+
description: '方程字符串数组,如 ["x^2 + y^2 = 25", "x + y = 7"]。支持 + - * / ^ sqrt log sin cos tan exp abs,以及 in-text 域约束 "x ∈ [-30,30]"。'
|
|
214
|
+
},
|
|
215
|
+
variables: {
|
|
216
|
+
type: 'array', items: { type: 'string' },
|
|
217
|
+
description: '变量名数组(可选)。不填则由方程文本自动识别,顺序按出现。最多 6 个。'
|
|
218
|
+
},
|
|
219
|
+
domain: {
|
|
220
|
+
type: 'object',
|
|
221
|
+
description: '显式搜索域(可选)。形如 {"x":[-30,30],"y":[-30,30]}。对"有限解·部分"演示或快增长函数(exp/sinh)建议显式给定,否则默认 ±1e6 可能剪枝失效并触发 truncated。'
|
|
222
|
+
},
|
|
223
|
+
fastMode: { type: 'boolean', description: '快速模式(默认 false)' },
|
|
224
|
+
options: { type: 'object', description: '高级选项(可选),如 {budget:500000, maxDepth:28}' }
|
|
225
|
+
},
|
|
226
|
+
required: ['equations']
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: 'give_feedback',
|
|
231
|
+
description: 'AI 智能体在调用 solve 遇到卡点、错误、或认为结果有问题时,主动回报。' +
|
|
232
|
+
'回报内容仅落本地 feedback.log,不会外传。帮助作者持续改进。',
|
|
233
|
+
inputSchema: {
|
|
234
|
+
type: 'object',
|
|
235
|
+
properties: {
|
|
236
|
+
message: { type: 'string', description: '反馈内容:遇到了什么、期望什么、实际得到什么。' },
|
|
237
|
+
context: { type: 'string', description: '可选上下文:触发场景、输入特征等。' }
|
|
238
|
+
},
|
|
239
|
+
required: ['message']
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
// ---- JSON-RPC 处理(与 stdio 版 handle 同构,改为返回对象)----
|
|
245
|
+
function handleRpc(msg, ip) {
|
|
246
|
+
const id = msg.id;
|
|
247
|
+
const method = msg.method;
|
|
248
|
+
const params = msg.params || {};
|
|
249
|
+
|
|
250
|
+
if (method === 'initialize') {
|
|
251
|
+
return {
|
|
252
|
+
jsonrpc: '2.0', id,
|
|
253
|
+
result: {
|
|
254
|
+
protocolVersion: '2024-11-05',
|
|
255
|
+
capabilities: { tools: {} },
|
|
256
|
+
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
if (method === 'tools/list') {
|
|
261
|
+
return { jsonrpc: '2.0', id, result: { tools: TOOLS } };
|
|
262
|
+
}
|
|
263
|
+
if (method === 'tools/call') {
|
|
264
|
+
const name = params.name;
|
|
265
|
+
const args = params.arguments || {};
|
|
266
|
+
const t0 = Date.now();
|
|
267
|
+
try {
|
|
268
|
+
let result;
|
|
269
|
+
if (name === 'solve') {
|
|
270
|
+
result = doSolve(args);
|
|
271
|
+
} else if (name === 'give_feedback') {
|
|
272
|
+
const msg_fb = redactSensitive((args.message || '').toString().slice(0, 2000));
|
|
273
|
+
const ctx_fb = args.context ? redactSensitive(String(args.context)).slice(0, 2000) : null;
|
|
274
|
+
rotateIfNeeded(FEEDBACK_PATH);
|
|
275
|
+
fs.appendFileSync(FEEDBACK_PATH, JSON.stringify({
|
|
276
|
+
ts: new Date().toISOString(), message: msg_fb, context: ctx_fb
|
|
277
|
+
}) + '\n');
|
|
278
|
+
result = { acknowledged: true, note: '反馈已记录(本地,不外传;敏感信息已自动隐去)' };
|
|
279
|
+
} else {
|
|
280
|
+
throw { type: 'unknown_tool', message: '未知工具: ' + name };
|
|
281
|
+
}
|
|
282
|
+
const dt = Date.now() - t0;
|
|
283
|
+
appendLog({
|
|
284
|
+
ts: new Date().toISOString(), ip: ip, tool: name, status: 'ok',
|
|
285
|
+
dtMs: dt, resultType: result.resultType, nSol: result.solutionCount,
|
|
286
|
+
truncated: result.truncated
|
|
287
|
+
});
|
|
288
|
+
return { jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } };
|
|
289
|
+
} catch (e) {
|
|
290
|
+
const dt = Date.now() - t0;
|
|
291
|
+
const errObj = (e && e.type) ? e : { type: 'internal_error', message: (e && e.message) || String(e) };
|
|
292
|
+
appendLog({
|
|
293
|
+
ts: new Date().toISOString(), ip: ip, tool: name, status: 'error',
|
|
294
|
+
dtMs: dt, errorType: errObj.type
|
|
295
|
+
});
|
|
296
|
+
return {
|
|
297
|
+
jsonrpc: '2.0', id,
|
|
298
|
+
result: { content: [{ type: 'text', text: JSON.stringify(errObj, null, 2) }], isError: true }
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// 通知类(无 id)或其它方法:返回 null(HTTP 下以 202/空体处理)
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ---- HTTP 服务 ----
|
|
307
|
+
const sessions = new Map(); // sessionId -> { createdAt, sse? }
|
|
308
|
+
|
|
309
|
+
// CORS:浏览器端 MCP 客户端(含 Smithery 连接测试)需此头,否则被静默拦截
|
|
310
|
+
const CORS = {
|
|
311
|
+
'Access-Control-Allow-Origin': '*',
|
|
312
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, DELETE',
|
|
313
|
+
'Access-Control-Allow-Headers': 'Content-Type, Accept, Mcp-Session-Id, Mcp-Protocol-Version, Authorization',
|
|
314
|
+
'Access-Control-Expose-Headers': 'Mcp-Session-Id, Content-Type'
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
function sendJson(res, status, obj, extraHeaders) {
|
|
318
|
+
const body = Buffer.from(JSON.stringify(obj), 'utf8');
|
|
319
|
+
res.writeHead(status, Object.assign({
|
|
320
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
321
|
+
'Content-Length': body.length
|
|
322
|
+
}, CORS, extraHeaders || {}));
|
|
323
|
+
res.end(body);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const server = http.createServer((req, res) => {
|
|
327
|
+
// CORS 预检
|
|
328
|
+
if (req.method === 'OPTIONS') {
|
|
329
|
+
res.writeHead(204, CORS);
|
|
330
|
+
return res.end();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 健康检查
|
|
334
|
+
if (req.method === 'GET' && (req.url === '/' || req.url === '/health')) {
|
|
335
|
+
// solverVersion 从已加载的求解沙箱实时读取,保证 /health 报告的版本与实际核心一致(不靠手写常量,杜绝假版本)
|
|
336
|
+
let solverVersion = null;
|
|
337
|
+
try { solverVersion = solverCore.raw().SOLVER_VERSION || null; } catch (_e) { /* index.html 未就绪时降级为 null */ }
|
|
338
|
+
return sendJson(res, 200, {
|
|
339
|
+
name: SERVER_NAME,
|
|
340
|
+
version: SERVER_VERSION,
|
|
341
|
+
solverVersion,
|
|
342
|
+
status: 'ok',
|
|
343
|
+
transport: 'streamable-http',
|
|
344
|
+
endpoints: { mcp: 'POST /mcp (SSE via GET /mcp)', health: 'GET /health' },
|
|
345
|
+
tools: TOOLS.map(t => t.name)
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// MCP 端点:Streamable HTTP(POST 请求 / GET 收 SSE / DELETE 终止会话)
|
|
350
|
+
if (req.url === '/mcp') {
|
|
351
|
+
// GET:打开 SSE 流,接收服务端→客户端通知(MCP Streamable HTTP 规范)
|
|
352
|
+
if (req.method === 'GET') {
|
|
353
|
+
const sessionId = req.headers['mcp-session-id'] || crypto.randomUUID();
|
|
354
|
+
res.writeHead(200, Object.assign({
|
|
355
|
+
'Content-Type': 'text/event-stream',
|
|
356
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
357
|
+
'Connection': 'keep-alive',
|
|
358
|
+
'X-Accel-Buffering': 'no'
|
|
359
|
+
}, CORS, { 'mcp-session-id': sessionId }));
|
|
360
|
+
res.write('retry: 2000\n\n');
|
|
361
|
+
res.write(': connected\n\n');
|
|
362
|
+
const ka = setInterval(() => { try { res.write(': keepalive\n\n'); } catch (_e) {} }, 15000);
|
|
363
|
+
req.on('close', () => { clearInterval(ka); sessions.delete(sessionId); });
|
|
364
|
+
sessions.set(sessionId, { createdAt: Date.now(), sse: res });
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
// DELETE:终止会话
|
|
368
|
+
if (req.method === 'DELETE') {
|
|
369
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
370
|
+
const s = sessions.get(sessionId);
|
|
371
|
+
if (s && s.sse) { try { s.sse.end(); } catch (_e) {} }
|
|
372
|
+
sessions.delete(sessionId);
|
|
373
|
+
res.writeHead(200, CORS);
|
|
374
|
+
return res.end();
|
|
375
|
+
}
|
|
376
|
+
// POST:JSON-RPC 请求
|
|
377
|
+
if (req.method === 'POST') {
|
|
378
|
+
const ip = (req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown').toString().split(',')[0].trim();
|
|
379
|
+
// 限速(安全等保加固):超限直接 429,不读不处理
|
|
380
|
+
if (!rateAllowed(ip)) {
|
|
381
|
+
appendLog({ ts: new Date().toISOString(), ip: ip, event: 'rate_limited' });
|
|
382
|
+
return sendJson(res, 429, { jsonrpc: '2.0', id: null, error: { code: -32000, message: 'Rate limit exceeded. Try again later.' } });
|
|
383
|
+
}
|
|
384
|
+
const sessionId = req.headers['mcp-session-id'] || crypto.randomUUID();
|
|
385
|
+
if (!sessions.has(sessionId)) sessions.set(sessionId, { createdAt: Date.now() });
|
|
386
|
+
pruneSessions();
|
|
387
|
+
|
|
388
|
+
let raw = '';
|
|
389
|
+
let rawLen = 0;
|
|
390
|
+
let tooBig = false;
|
|
391
|
+
req.setEncoding('utf8');
|
|
392
|
+
req.on('data', (chunk) => {
|
|
393
|
+
rawLen += chunk.length;
|
|
394
|
+
if (rawLen > MAX_BODY_BYTES) {
|
|
395
|
+
if (!tooBig) {
|
|
396
|
+
tooBig = true;
|
|
397
|
+
raw = '';
|
|
398
|
+
appendLog({ ts: new Date().toISOString(), ip: ip, event: 'rejected_body_too_large' });
|
|
399
|
+
try { res.writeHead(413, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32600, message: 'Request body too large (256KB max).' } })); } catch (_e) {}
|
|
400
|
+
try { req.destroy(); } catch (_e2) {}
|
|
401
|
+
}
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
raw += chunk;
|
|
405
|
+
});
|
|
406
|
+
req.on('end', () => {
|
|
407
|
+
if (tooBig) return;
|
|
408
|
+
let msg;
|
|
409
|
+
try {
|
|
410
|
+
msg = JSON.parse(raw);
|
|
411
|
+
} catch (_e) {
|
|
412
|
+
return sendJson(res, 400, { jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } });
|
|
413
|
+
}
|
|
414
|
+
// 批量请求支持
|
|
415
|
+
const batch = Array.isArray(msg) ? msg : [msg];
|
|
416
|
+
const responses = [];
|
|
417
|
+
for (const m of batch) {
|
|
418
|
+
if (m && m.jsonrpc === '2.0') {
|
|
419
|
+
const r = handleRpc(m, ip);
|
|
420
|
+
if (r !== null) responses.push(r);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const headers = Object.assign({ 'mcp-session-id': sessionId }, CORS);
|
|
424
|
+
if (Array.isArray(msg)) {
|
|
425
|
+
if (responses.length === 0) { res.writeHead(202, headers); return res.end(); }
|
|
426
|
+
return sendJson(res, 200, responses, headers);
|
|
427
|
+
} else {
|
|
428
|
+
if (responses.length === 0) { res.writeHead(202, headers); return res.end(); }
|
|
429
|
+
return sendJson(res, 200, responses[0], headers);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
// 其它方法
|
|
435
|
+
res.writeHead(405, Object.assign({ 'Content-Type': 'text/plain; charset=utf-8', 'Allow': 'GET, POST, DELETE, OPTIONS' }, CORS));
|
|
436
|
+
return res.end('Method Not Allowed. MCP endpoint accepts GET, POST, DELETE, OPTIONS.');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// 其它:404
|
|
440
|
+
res.writeHead(404, Object.assign({ 'Content-Type': 'text/plain; charset=utf-8' }, CORS));
|
|
441
|
+
res.end('Not Found. MCP endpoint: POST /mcp');
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
server.listen(PORT, '0.0.0.0', () => {
|
|
445
|
+
appendLog({ ts: new Date().toISOString(), event: 'server_start', name: SERVER_NAME, version: SERVER_VERSION, port: PORT });
|
|
446
|
+
console.log(`[${SERVER_NAME}] HTTP MCP server listening on 0.0.0.0:${PORT}`);
|
|
447
|
+
console.log(` health : GET http://localhost:${PORT}/health`);
|
|
448
|
+
console.log(` mcp : POST http://localhost:${PORT}/mcp`);
|
|
449
|
+
});
|
package/index.html
CHANGED
|
@@ -4737,7 +4737,7 @@ function suan7(state) {
|
|
|
4737
4737
|
for (var kb in known) ibox[kb] = { min: known[kb], max: known[kb] };
|
|
4738
4738
|
ibox[uv] = box;
|
|
4739
4739
|
var iF = intervalEval(eq, ibox);
|
|
4740
|
-
if (iF && (iF
|
|
4740
|
+
if (iF && _ivExcludesZero(iF)) {
|
|
4741
4741
|
state.done = true;
|
|
4742
4742
|
var knownList = Object.keys(known).sort().map(function(v) { return v + "=" + known[v]; }).join(", ");
|
|
4743
4743
|
state.result = { solutions: [], error: "NO_SOLUTION", message: "前向传播检测到矛盾:代入已知值后,方程在变量 " + uv + " 的全域上 f 的严格区间包络不含 0(包络=[" + iF.min.toExponential(2) + "," + iF.max.toExponential(2) + "]),严格证明无根", detail: "已知值: " + knownList, executionPath: "前向传播矛盾检测", timeMs: performance.now() - state.startTime, confidence: "high", varNames: state.varNames, resultType: 1, resultTypeName: "空结果", resultTypeDesc: "前向传播检测到矛盾" };
|
|
@@ -6594,6 +6594,16 @@ var _affSym = 0;
|
|
|
6594
6594
|
function _Aff(c, e) { return { c: c, e: e || {} }; }
|
|
6595
6595
|
function _affRad(a) { if (!a) return 0; var r = 0; for (var k in a.e) r += Math.abs(a.e[k]); return r; }
|
|
6596
6596
|
function _affToInterval(a) { if (!a) return null; var r = _affRad(a); return _iNorm({ min: a.c - r, max: a.c + r }); }
|
|
6597
|
+
// sound 容差:区间求值受浮点舍入影响,包络可能偏离真值几个 ulp(如 12/5 经 12*(1/5)
|
|
6598
|
+
// 算得 2.4000000000000001,与字面 2.4 差 ~2e-16,加仿射半径后包络中心落正侧)。
|
|
6599
|
+
// 判"严格不含 0 → 证明无解/严格单调"时,必须要求包络离 0 超过此容差;否则保守地
|
|
6600
|
+
// 不判无解(降级为继续求解 / 诚实报未找到),绝不因舍入把真解误杀为假阴性(sound-safe,
|
|
6601
|
+
// 绝不引入假阳性)。容差随量级缩放(相对 1e-12)+ 1e-12 绝对下限。
|
|
6602
|
+
function _ivExcludesZero(iv) {
|
|
6603
|
+
if (!iv || !isFinite(iv.min) || !isFinite(iv.max)) return false;
|
|
6604
|
+
var pad = Math.max(1e-12, 1e-12 * Math.max(1, Math.abs(iv.min), Math.abs(iv.max)));
|
|
6605
|
+
return iv.max < -pad || iv.min > pad;
|
|
6606
|
+
}
|
|
6597
6607
|
function _affAdd(a, b) { var e = {}; for (var k in a.e) e[k] = a.e[k]; for (var k in b.e) e[k] = (e[k] || 0) + b.e[k]; return _Aff(a.c + b.c, e); }
|
|
6598
6608
|
function _affSub(a, b) { var e = {}; for (var k in a.e) e[k] = a.e[k]; for (var k in b.e) e[k] = (e[k] || 0) - b.e[k]; return _Aff(a.c - b.c, e); }
|
|
6599
6609
|
function _affMul(a, b) {
|
|
@@ -6610,14 +6620,18 @@ function _affMul(a, b) {
|
|
|
6610
6620
|
}
|
|
6611
6621
|
function _affInv(b) {
|
|
6612
6622
|
// 1/b 一阶泰勒,误差保守界 |(b-b0)^2|/(2 b0^2 ξ^2),ξ∈盒,min|ξ|=|b0|-rb
|
|
6623
|
+
// ⚠️ 修正(sound 正确性,2026-09-07):旧实现 r=0-b=-b,lin=(-1/b.c²)·(-b) 的中心 lin.c 已是 1/b.c,
|
|
6624
|
+
// return 又写 1/b.c + lin.c → 中心被双重计数为 2/b.c(如 1/5 算成 0.4、12/5 算成 4.8),
|
|
6625
|
+
// 导致所有含除法的区间包络 / 中值定理判无解(suan7/suan29/suan35 等)系统性假阴性。
|
|
6626
|
+
// 正确:dev = b - b.c(中心 0)做一阶泰勒,lin.c=0,return 中心恰为 1/b.c,且噪声符号正确。
|
|
6613
6627
|
var rb = _affRad(b); if (b.c === 0) return null;
|
|
6614
|
-
var
|
|
6615
|
-
var lin = _affMul(
|
|
6628
|
+
var dev = _affSub(b, _Aff(b.c, {})); // b - b0,中心为 0
|
|
6629
|
+
var lin = _affMul(dev, _Aff(-1 / (b.c * b.c), {})); // 一阶项,中心为 0
|
|
6616
6630
|
var e = {}; for (var k in lin.e) e[k] = lin.e[k];
|
|
6617
6631
|
var m = Math.abs(b.c) - rb; if (m <= 0) return null;
|
|
6618
6632
|
var err = (rb * rb) / (2 * b.c * b.c * m * m);
|
|
6619
6633
|
var sym = _affSym++; e[sym] = err;
|
|
6620
|
-
return _Aff(1 / b.c + lin.c, e);
|
|
6634
|
+
return _Aff(1 / b.c + lin.c, e); // 现 lin.c=0 → 中心 = 1/b.c
|
|
6621
6635
|
}
|
|
6622
6636
|
function _affDiv(a, b) { var ib = _affInv(b); return ib ? _affMul(a, ib) : null; }
|
|
6623
6637
|
function _buildAffEnv(intervals) {
|
|
@@ -6705,7 +6719,7 @@ function suan29(state) {
|
|
|
6705
6719
|
|
|
6706
6720
|
// 严格测试①(最强):f 在区间上的区间包络不含 0 → 由中值定理严格证明该盒内无根
|
|
6707
6721
|
var F = intervalEval(state.equations[0], box);
|
|
6708
|
-
if (F && (F
|
|
6722
|
+
if (F && _ivExcludesZero(F)) {
|
|
6709
6723
|
state.done = true;
|
|
6710
6724
|
state.result = { solutions: [], error: "NO_SOLUTION", provenEmpty: true, message: "f 在 [" + a + "," + b + "] 上的严格区间包络不含 0(包络=[" + F.min.toExponential(2) + "," + F.max.toExponential(2) + "]),由中值定理严格证明该盒内无根", executionPath: "导数单调性剪枝", timeMs: performance.now() - state.startTime, confidence: "high", varNames: state.varNames, resultType: 1, resultTypeName: "空结果", resultTypeDesc: "区间包络严格证明区间内无根" };
|
|
6711
6725
|
return;
|
|
@@ -6716,7 +6730,7 @@ function suan29(state) {
|
|
|
6716
6730
|
// 用符号微分+区间求值得到【紧致】导数包络,取代原“3 条割线同号”启发式(会漏根,见评审 unsound)。
|
|
6717
6731
|
var dAST = _diffAST(state.equations[0], vn);
|
|
6718
6732
|
var dI = dAST ? intervalEval(dAST, box) : null;
|
|
6719
|
-
if (dI && (dI
|
|
6733
|
+
if (dI && _ivExcludesZero(dI)) {
|
|
6720
6734
|
var faBox = {}, fbBox = {}; faBox[vn] = { min: a, max: a }; fbBox[vn] = { min: b, max: b };
|
|
6721
6735
|
var faI = intervalEval(state.equations[0], faBox);
|
|
6722
6736
|
var fbI = intervalEval(state.equations[0], fbBox);
|
|
@@ -6870,7 +6884,7 @@ function suan35(state) {
|
|
|
6870
6884
|
// 故仅在“不含 0”时返回 false(不可行)。
|
|
6871
6885
|
for (var r = 0; r < subEqs.length; r++) {
|
|
6872
6886
|
var iF = intervalEval(subEqs[r], box);
|
|
6873
|
-
if (iF &&
|
|
6887
|
+
if (iF && _ivExcludesZero(iF)) {
|
|
6874
6888
|
return false; // 该方程在 used 全域上严格无根 ⇒ 子问题不可行 ⇒ 全局不可行
|
|
6875
6889
|
}
|
|
6876
6890
|
}
|
|
@@ -11083,7 +11097,7 @@ function _runTail(state) {
|
|
|
11083
11097
|
|
|
11084
11098
|
// —— 可审计结构化溯源(切片A):每个结果附带完整证据链元数据 ——
|
|
11085
11099
|
// 设计目标:Agent / 上层系统拿到的不只是裸数字,而是"为什么是这个解"的可读证据链。
|
|
11086
|
-
var SOLVER_VERSION = "lingshu-solver/1.0.
|
|
11100
|
+
var SOLVER_VERSION = "lingshu-solver/1.0.8-auditable";
|
|
11087
11101
|
|
|
11088
11102
|
// Movability 完整读取(切片B):每变量携带 status + width,供 Agent 读懂"为何是这个解"
|
|
11089
11103
|
function _movabilityFull(state) {
|
|
@@ -12276,7 +12290,14 @@ function showError(msg) {
|
|
|
12276
12290
|
</script>
|
|
12277
12291
|
|
|
12278
12292
|
<footer style="margin-top:34px;padding:18px 12px;text-align:center;color:#64748b;font-size:12px;border-top:1px solid #334155;line-height:1.8;">
|
|
12279
|
-
|
|
12293
|
+
<div style="margin-bottom:8px;">广州市红尘灵境数字科技有限公司 版权所有</div>
|
|
12294
|
+
<div style="margin-bottom:8px;">
|
|
12295
|
+
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" style="color:#94a3b8;text-decoration:none;margin:0 8px;">粤ICP备2026031206号-3</a>
|
|
12296
|
+
<a href="https://www.beian.gov.cn/portal/registerSystemInfo?recordcode=44011402001444" target="_blank" rel="noopener" style="display:inline-flex;align-items:center;gap:6px;color:#94a3b8;text-decoration:none;margin:0 8px;">
|
|
12297
|
+
<img src="beian.png" alt="公安备案图标" style="height:18px;vertical-align:middle;" />
|
|
12298
|
+
<span>粤公网安备44011402001444号</span>
|
|
12299
|
+
</a>
|
|
12300
|
+
</div>
|
|
12280
12301
|
<a href="/privacy.html" style="color:#94a3b8;">隐私与安全说明</a> ·
|
|
12281
12302
|
远程调用端点:<code style="color:#94a3b8;">hongchenlingjing.com/mcp</code>
|
|
12282
12303
|
</footer>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lingshu-solver",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "灵数求解器 · 确定性实数方程组求解 MCP 工具(≤6变量):区间收缩 + Krawczyk 认证,离线零依赖零数据,诚实标记 truncated。",
|
|
5
5
|
"homepage": "https://genesis-plan.github.io/lingshu-solver/",
|
|
6
6
|
"repository": {
|
|
@@ -36,7 +36,9 @@
|
|
|
36
36
|
"files": [
|
|
37
37
|
"index.html",
|
|
38
38
|
"mcp-server.js",
|
|
39
|
+
"http-mcp-server.js",
|
|
39
40
|
"solver-core.js",
|
|
41
|
+
"beian.png",
|
|
40
42
|
"README.md",
|
|
41
43
|
"LICENSE"
|
|
42
44
|
],
|