lingshu-solver 1.0.7 → 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 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
@@ -11097,7 +11097,7 @@ function _runTail(state) {
11097
11097
 
11098
11098
  // —— 可审计结构化溯源(切片A):每个结果附带完整证据链元数据 ——
11099
11099
  // 设计目标:Agent / 上层系统拿到的不只是裸数字,而是"为什么是这个解"的可读证据链。
11100
- var SOLVER_VERSION = "lingshu-solver/1.0.7-auditable";
11100
+ var SOLVER_VERSION = "lingshu-solver/1.0.8-auditable";
11101
11101
 
11102
11102
  // Movability 完整读取(切片B):每变量携带 status + width,供 Agent 读懂"为何是这个解"
11103
11103
  function _movabilityFull(state) {
@@ -12290,7 +12290,14 @@ function showError(msg) {
12290
12290
  </script>
12291
12291
 
12292
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;">
12293
- 粤ICP备2026031206号 ·
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>
12294
12301
  <a href="/privacy.html" style="color:#94a3b8;">隐私与安全说明</a> ·
12295
12302
  远程调用端点:<code style="color:#94a3b8;">hongchenlingjing.com/mcp</code>
12296
12303
  </footer>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lingshu-solver",
3
- "version": "1.0.7",
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
  ],