lingshu-solver 1.0.8 → 1.0.10

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.
@@ -14,9 +14,29 @@
14
14
  *
15
15
  * 零依赖:仅用 Node 内置 http / fs / path / crypto,无需 npm install。
16
16
  *
17
+ * ── 按次计费(2026-09-18 新增)────────────────────────────────────────
18
+ * 本文件额外承载「托管端点按次计费」:每次 solve 成功产出结果扣 1 分钱。
19
+ * - 开关:LS_METERING=on 才启用(默认 off —— 自建部署者不被强行收费,
20
+ * 开源工具保持诚实;我们自己的托管端点用 systemd Environment 打开)
21
+ * - 单价:LS_PRICE_CENTS,默认 1(分)
22
+ * - 凭证:Authorization: Bearer <key>,或 ?key=<key>(x-api-key 亦可)
23
+ * - 账本:credits.json(0600,原子写)+ credits.json.ledger.jsonl(只增审计流水)
24
+ * - 只在「成功返回结果」时扣费:输入不合法 / 内部异常 / 余额不足 一律不扣
25
+ * - 免计费:initialize / tools/list / give_feedback / GET /credit / GET /pricing;
26
+ * 以及来自本机回环且未经代理的请求(自检、定时任务)
27
+ * - 管理端点需 LS_ADMIN_TOKEN(未设置则管理端点整体 503,fail-closed,
28
+ * 绝不对外暴露「无凭证可加钱」的口子)
29
+ * - 收款方式 LS_PAY_TO / LS_PAY_CHANNEL:未配置时订单可建但明确标注「不可付款」,
30
+ * 代码不代填任何收款账号(避免伪造收款信息)
31
+ *
32
+ * 计费的强制边界(诚实声明):只有本托管端点能强制。网页版(单文件静态页)与
33
+ * npx 本地版跑在使用者自己的机器上,无法也不应被拦截 —— 二者永久免费。
34
+ *
17
35
  * 运行:
18
36
  * PORT=3000 node http-mcp-server.js
19
37
  * (LINGSHU_HTML 环境变量可重定向 index.html 位置,默认同目录)
38
+ * LS_METERING=on LS_PRICE_CENTS=1 LS_ADMIN_TOKEN=<随机串> LS_PAY_TO=<收款说明> \
39
+ * node http-mcp-server.js
20
40
  */
21
41
  'use strict';
22
42
  const http = require('http');
@@ -84,6 +104,613 @@ function appendLog(p) {
84
104
  try { rotateIfNeeded(LOG_PATH); fs.appendFileSync(LOG_PATH, JSON.stringify(p) + '\n'); } catch (_e) {}
85
105
  }
86
106
 
107
+ // ============================================================================
108
+ // 按次计费(1 分/次)—— 账本、凭证、扣费
109
+ // ============================================================================
110
+ const METERING_ON = String(process.env.LS_METERING || 'off').trim().toLowerCase() === 'on';
111
+ const PRICE_CENTS = Math.max(0, parseInt(process.env.LS_PRICE_CENTS || '1', 10) || 0);
112
+ const ADMIN_TOKEN = (process.env.LS_ADMIN_TOKEN || '').trim();
113
+ const PAY_TO = (process.env.LS_PAY_TO || '').trim();
114
+ const PAY_CHANNEL = (process.env.LS_PAY_CHANNEL || 'manual').trim();
115
+ // 付款页地址(可选)。配置后,订单响应会附带带订单号与金额的直达链接,
116
+ // 付款页据此才展示收款码(未带有效订单号时不展示,降低收款码被滥用的风险)。
117
+ const PAY_PAGE = (process.env.LS_PAY_PAGE || '').trim();
118
+ // 对公静态收款码图片地址(可选,推荐)。放咱们自己的 COS / 服务器,不用任何第三方支付平台。
119
+ // 配置后,订单 / 付款响应会附上可扫码的对公收款码,付款人直接扫码转账并备注订单号。
120
+ // 这正是「我们的方法」:不接支付宝 / 微信 / 工行商户 API,收款入口 = 公司已有的对公账户。
121
+ const PAY_TO_QR = (process.env.LS_PAY_TO_QR || '').trim();
122
+ // 「对公收款」:本项目自研的 Agent 支付协议标识(不依赖任何支付平台商户 API)。
123
+ // 统一收款入口 = 工银e支付银联聚合码(公司账户,支持支付宝 / 微信 / 银联扫码)。
124
+ const PAY_INTENT_PROTOCOL = 'lingshu-corporate/1.0';
125
+
126
+ // ============================================================================
127
+ // 安全护栏:收款去向防替换(fail-closed)
128
+ // ----------------------------------------------------------------------------
129
+ // 把「合法收款去向」钉死在代码里(与凭据库 credentials.md 一致)。运行时若
130
+ // LS_PAY_TO / LS_PAY_TO_QR / LS_PAY_PAGE 被改成**别的账号 / 别的收款码 / 别的付款页**,
131
+ // 端点**拒绝生成任何付款意图**(fail-closed)——而不是把错误账号展示给用户去付。
132
+ // 攻击者即便改了配置文件也收不到钱,最多让收款暂时不可用(「不能收就算了」的安全取舍)。
133
+ //
134
+ // 判定按「去向」而非「整串文字相等」:LS_PAY_TO 是给人看的自由文本(含说明、付款流程、
135
+ // 付款页链接),要求整串相等会把线上正常配置误判为篡改。三类判定,任一命中即 fail-closed:
136
+ // ① 账号数字段:文本里出现的 16~19 位连续数字,必须是钉死对公账号;
137
+ // ② 收款码地址:LS_PAY_TO_QR 必须等于钉死的银联官方聚合码链接(扫的码决定钱去哪);
138
+ // ③ 链接主机:文本里的 URL 主机必须是我们自己的域;银联址必须逐字节等于钉死链接
139
+ // (同域下换商户号 = 换收款人,同样拦)。
140
+ // ============================================================================
141
+ const PINNED_ACCOUNT = '3602026809201658423'; // 广州市红尘灵境数字科技有限公司 对公基本存款账户
142
+ const PINNED_ACCOUNT_NAME = '广州市红尘灵境数字科技有限公司';
143
+ const PINNED_QR_URL = 'https://qr.95516.com/01020001/wcqr?f=ICBCqr&X=1&T=3&P=13&I=e03d925776684b4d&N=b4cbb142eeafe2bddce7a7878c57f5ca&L=09253a28e43998a37f27ab44fba914bf945dc63f02a5239d';
144
+ const TRUSTED_HOSTS = ['hongchenlingjing.com', 'www.hongchenlingjing.com', 'qr.95516.com'];
145
+ function hostOfUrl(u) {
146
+ try { return new URL(String(u)).hostname.toLowerCase(); } catch (_e) { return null; }
147
+ }
148
+ function urlsIn(s) {
149
+ try {
150
+ const m = String(s == null ? '' : s).match(/https?:\/\/[^\s,。;、))】"']+/g) || [];
151
+ return m.map(function (u) { return u.replace(/[.,;:!?、))】]+$/, ''); });
152
+ } catch (_e) { return []; }
153
+ }
154
+ function accountRunsIn(s) {
155
+ try { return String(s == null ? '' : s).match(/\d{16,19}/g) || []; } catch (_e) { return []; }
156
+ }
157
+ // 逐项体检:返回问题清单(空数组 = 未发现篡改)
158
+ function guardProblems() {
159
+ const out = [];
160
+ [['LS_PAY_TO', PAY_TO], ['LS_PAY_TO_QR', PAY_TO_QR]].forEach(function (kv) {
161
+ accountRunsIn(kv[1]).forEach(function (n) {
162
+ if (n !== PINNED_ACCOUNT) out.push(kv[0] + ' 含非钉死账号 ' + n.slice(0, 4) + '****' + n.slice(-4) + '(应为 ' + PINNED_ACCOUNT.slice(0, 4) + '****' + PINNED_ACCOUNT.slice(-4) + ')');
163
+ });
164
+ });
165
+ if (PAY_TO_QR && PAY_TO_QR !== PINNED_QR_URL) {
166
+ out.push('LS_PAY_TO_QR 与钉死聚合码链接不一致(钱可能进第三方):' + (hostOfUrl(PAY_TO_QR) || '无法识别'));
167
+ }
168
+ [['LS_PAY_TO', PAY_TO], ['LS_PAY_PAGE', PAY_PAGE]].forEach(function (kv) {
169
+ urlsIn(kv[1]).forEach(function (u) {
170
+ const h = hostOfUrl(u);
171
+ if (!h) { out.push(kv[0] + ' 含无法解析的链接:' + u.slice(0, 40)); return; }
172
+ if (h === 'qr.95516.com') { if (u !== PINNED_QR_URL) out.push(kv[0] + ' 含非钉死的银联码链接(商户号被换)'); return; }
173
+ if (TRUSTED_HOSTS.indexOf(h) < 0) out.push(kv[0] + ' 含非可信链接主机:' + h);
174
+ });
175
+ });
176
+ return out;
177
+ }
178
+ const GUARD_PROBLEMS = guardProblems();
179
+ const RECEIPT_TAMPERED = GUARD_PROBLEMS.length > 0; // 收款去向被换 → 硬失败(fail-closed)
180
+ const RECEIPT_QR_WARN = false; // 码址问题已并入上面的硬判定
181
+ function receiptPinHash() {
182
+ return crypto.createHash('sha256').update(PINNED_ACCOUNT + '|' + PINNED_ACCOUNT_NAME + '|' + PINNED_QR_URL, 'utf8').digest('hex');
183
+ }
184
+ const RECEIPT_PIN_HASH = receiptPinHash(); // 启动期可见的钉死指纹,便于运维肉眼确认未被篡改
185
+ const CREDITS_PATH = path.resolve(process.env.LS_CREDITS_PATH || path.join(__dirname, 'credits.json'));
186
+ const CREDIT_LEDGER_PATH = CREDITS_PATH + '.ledger.jsonl';
187
+ // 自建部署者若不想让自己的回环请求被计费,可关掉(默认开:本机自检/定时任务免费)
188
+ const FREE_LOOPBACK = String(process.env.LS_FREE_LOOPBACK || '1') !== '0';
189
+ // 是否要求「携带凭证的调用必须走 TLS」(默认 off,便于迁移期直连 :3000 调试)。
190
+ // 说明:Bearer 凭证走明文 HTTP=在公网裸奔,路上任何人都能抓走并花掉余额。
191
+ // 迁移到 https://<域名>/mcp 后应置为 on;本机回环请求豁免。
192
+ const REQUIRE_TLS = String(process.env.LS_REQUIRE_TLS || 'off').trim().toLowerCase() === 'on';
193
+ // 管理面是否只接受本机回环访问(默认 on,fail-closed)。
194
+ // 理由:管理令牌一旦走公网明文就等于泄露。运维改走 SSH 隧道
195
+ // ssh -N -L 3000:127.0.0.1:3000 root@<host> → curl localhost:3000/admin/...
196
+ // 这样管理令牌永远不会经过公网。要放宽(例如放到带 TLS 的反代后面)再置 off。
197
+ const ADMIN_LOOPBACK_ONLY = String(process.env.LS_ADMIN_LOOPBACK_ONLY || '1') !== '0';
198
+ // 订单创建限速:默认 10 单/小时/IP(防刷单表;与 /mcp 的 120次/分 独立)
199
+ const ORDER_WINDOW_MS = 60 * 60 * 1000;
200
+ const ORDER_RATE_MAX = parseInt(process.env.LS_ORDER_RATE_MAX || '10', 10);
201
+ // 按次计费:固定 1 分钱/次(¥0.01/call)。对公收款:下单 = 1 次 = 1 分钱的付费凭证,
202
+ // 不预充、无套餐、无其他金额;付款人向对公聚合码付任意正金额,按实收折算 N' = 实收/¥0.01 次入账(多付多得)。
203
+ const PER_CALL_CENTS = PRICE_CENTS; // 恒为 1
204
+ const ORDER_TTL_DAYS = 365;
205
+
206
+ let ledger = { version: 1, priceCents: PRICE_CENTS, createdAt: new Date().toISOString(), keys: {}, orders: {} };
207
+ // 账本文件的 mtime:size 指纹,用于检测「运维在进程外手工改过账本」
208
+ let ledgerStamp = null;
209
+ // 信任制(honor system)声明计数:调用方「声明已付款」即放行的次数(不验证、不扣余额)。
210
+ // 用于衡量「声明付款」vs「真付款」的转化,评估信任制是否真带来收入(与计费账本分离,不进钱账)。
211
+ let honorClaims = 0;
212
+ // 自助入账计数:调用方「付款后声明本单已付」并自助入账的次数(不验证、立即放行)。
213
+ // 与 honorClaims 同属信任制的两条口子,**都不代表真收入**:真收入只认银行流水核对过的单
214
+ // (台账里 creditedBy==='reconcile'/'admin' 且 amountVerified:true)。分开计数是为了
215
+ // 任何时候都能一眼看出「有多少额度是凭声明发的、多少是核对过流水发的」,不虚报营收。
216
+ let selfReportClaims = 0;
217
+
218
+ function stampOf() {
219
+ try { const st = fs.statSync(CREDITS_PATH); return st.mtimeMs + ':' + st.size; } catch (_e) { return null; }
220
+ }
221
+
222
+ function loadLedger() {
223
+ try {
224
+ const raw = fs.readFileSync(CREDITS_PATH, 'utf8');
225
+ const j = JSON.parse(raw);
226
+ if (j && typeof j === 'object' && j.keys && typeof j.keys === 'object') {
227
+ ledger = Object.assign({ version: 1, keys: {}, orders: {} }, j);
228
+ if (ledger.orders && typeof ledger.orders !== 'object') ledger.orders = {};
229
+ }
230
+ } catch (_e) {
231
+ // 首次运行(无账本)或账本损坏:一律从「空账本」起 —— fail-closed。
232
+ // 无 key 即无余额即拒绝服务,绝不会因读不到账本而放行。
233
+ }
234
+ ledgerStamp = stampOf();
235
+ }
236
+
237
+ // 外部(运维手工)改动检测。
238
+ // 账本是内存副本 + 整份写回,若不检测,运维在磁盘上的手工修会被下一次写入**静默覆盖**
239
+ // (实测踩过:清理 contact 字段后,服务一有请求就把旧内容写回去了)。
240
+ // 因此在任何变更/查看前先比对指纹,被人手改过就重新载入。
241
+ // 载入失败时**保留内存版本**并记审计 —— 绝不因一次坏编辑把账本清空(那等于拒绝所有付费客户)。
242
+ function refreshLedgerIfChanged() {
243
+ try {
244
+ const cur = stampOf();
245
+ if (cur === ledgerStamp) return false;
246
+ let j = null;
247
+ try { j = JSON.parse(fs.readFileSync(CREDITS_PATH, 'utf8')); } catch (e) {
248
+ ledgerAudit({ event: 'external_reload_rejected', reason: 'parse_failed', err: (e && e.message) || String(e) });
249
+ ledgerStamp = cur;
250
+ return false;
251
+ }
252
+ if (!j || typeof j !== 'object' || !j.keys || typeof j.keys !== 'object') {
253
+ ledgerAudit({ event: 'external_reload_rejected', reason: 'bad_shape' });
254
+ ledgerStamp = cur;
255
+ return false;
256
+ }
257
+ ledger = Object.assign({ version: 1, keys: {}, orders: {} }, j);
258
+ if (!ledger.orders || typeof ledger.orders !== 'object') ledger.orders = {};
259
+ ledgerStamp = cur;
260
+ ledgerAudit({ event: 'external_reload_ok', keyCount: Object.keys(ledger.keys).length, orderCount: Object.keys(ledger.orders).length });
261
+ return true;
262
+ } catch (_e) { return false; }
263
+ }
264
+
265
+ function saveLedger() {
266
+ // 原子写:同目录临时文件 → fsync → rename,避免半截 JSON 让账本不可读
267
+ const tmp = CREDITS_PATH + '.tmp';
268
+ const body = JSON.stringify(ledger, null, 2);
269
+ const fd = fs.openSync(tmp, 'w', 0o600);
270
+ try {
271
+ fs.writeSync(fd, body);
272
+ fs.fsyncSync(fd);
273
+ } finally {
274
+ fs.closeSync(fd);
275
+ }
276
+ fs.renameSync(tmp, CREDITS_PATH);
277
+ ledgerStamp = stampOf();
278
+ }
279
+
280
+ // 只增审计流水:钱相关的每一次变动都必须留痕,且与快照文件分离
281
+ function ledgerAudit(ev) {
282
+ try {
283
+ fs.appendFileSync(CREDIT_LEDGER_PATH, JSON.stringify(Object.assign({ ts: new Date().toISOString() }, ev)) + '\n');
284
+ } catch (_e) {}
285
+ }
286
+
287
+ // 凭证只存 SHA-256,不存明文 —— 账本文件即使泄露也不能直接拿去调用
288
+ const hashKey = (k) => crypto.createHash('sha256').update(String(k), 'utf8').digest('hex');
289
+ const maskKey = (k) => {
290
+ const s = String(k);
291
+ return s.length <= 14 ? s.slice(0, 4) + '…' : s.slice(0, 8) + '…' + s.slice(-4);
292
+ };
293
+ const newKey = () => 'lsk_' + crypto.randomBytes(24).toString('hex');
294
+ const newOrderId = () => 'LS-' + new Date().toISOString().slice(0, 10).replace(/-/g, '') + '-' + crypto.randomBytes(3).toString('hex');
295
+ // 订单号格式(自助入账等外部传入 orderId 的入口必须先校验格式):
296
+ // ① 只认本端点自己发的形状,避免拿任意字符串当账本键;② 顺带挡住 `__proto__`/`constructor`
297
+ // 这类原型链键名(用不可信字符串直接索引普通对象是经典坑)。
298
+ const ORDER_ID_RE = /^LS-\d{8}-[0-9a-f]{6}$/;
299
+ const ORDER_ID_SAMPLE = 'LS-' + new Date().toISOString().slice(0, 10).replace(/-/g, '') + '-a1b2c3';
300
+ // 取订单:只接受合法格式,且必须是用 Object.prototype.hasOwnProperty 确认的自有属性
301
+ function getOrder(orderId) {
302
+ if (!ORDER_ID_RE.test(String(orderId || ''))) return null;
303
+ const orders = ledger.orders || {};
304
+ if (!Object.prototype.hasOwnProperty.call(orders, orderId)) return null;
305
+ const o = orders[orderId];
306
+ return (o && typeof o === 'object') ? o : null;
307
+ }
308
+ const yuan = (cents) => '¥' + (cents / 100).toFixed(2);
309
+
310
+ function extractKey(req, query) {
311
+ const auth = (req.headers['authorization'] || '').toString();
312
+ const m = /^Bearer\s+(.+)$/i.exec(auth.trim());
313
+ if (m) return m[1].trim();
314
+ const xa = (req.headers['x-api-key'] || '').toString().trim();
315
+ if (xa) return xa;
316
+ const q = query ? (query.get('key') || '').trim() : '';
317
+ if (q) return q;
318
+ return null;
319
+ }
320
+
321
+ // 「源地址为本机回环,且未经反代」——与计费豁免开关无关的纯粹事实判断。
322
+ // 一旦存在 X-Forwarded-For 就绝不认:否则反代会把全世界都伪装成 127.0.0.1。
323
+ function isLoopbackSource(req) {
324
+ if ((req.headers['x-forwarded-for'] || '').toString().trim()) return false;
325
+ const ra = String(req.socket.remoteAddress || '');
326
+ return /^(127\.\d+\.\d+\.\d+|::1|::ffff:127\.\d+\.\d+\.\d+)$/.test(ra);
327
+ }
328
+
329
+ // 计费豁免:可被 LS_FREE_LOOPBACK=0 关掉(测试需要,否则本机用例全被免单)
330
+ function isLoopback(req) {
331
+ if (!FREE_LOOPBACK) return false;
332
+ return isLoopbackSource(req);
333
+ }
334
+
335
+ function baseUrl(req) {
336
+ const proto = (req.headers['x-forwarded-proto'] || 'http').toString().split(',')[0].trim() || 'http';
337
+ const host = (req.headers.host || ('localhost:' + PORT)).toString();
338
+ return proto + '://' + host;
339
+ }
340
+
341
+ // 传输是否加密:只看反代声明的 X-Forwarded-Proto。
342
+ // 无该头 ⇒ 请求是直连本进程端口(明文 HTTP)⇒ 视为不安全。
343
+ function isSecureTransport(req) {
344
+ const p = (req.headers['x-forwarded-proto'] || '').toString().split(',')[0].trim().toLowerCase();
345
+ return p === 'https';
346
+ }
347
+
348
+ function buildCtx(req, query) {
349
+ const key = extractKey(req, query);
350
+ return {
351
+ key: key,
352
+ keyHash: key ? hashKey(key) : null,
353
+ keyMask: key ? maskKey(key) : null,
354
+ isInternal: isLoopback(req),
355
+ secure: isSecureTransport(req)
356
+ };
357
+ }
358
+
359
+ // 免费替代路径 —— 必须写在计费错误里一起返回。
360
+ // 不作假墙:本地版与网页版真实存在且永久免费,agent 应被告知,而不是被迫付费。
361
+ function freeAlternatives() {
362
+ return [
363
+ '本地版(离线、免费、无次数限制、零数据外传):npx -y lingshu-solver',
364
+ '网页版(免费):https://genesis-plan.github.io/lingshu-solver/',
365
+ '源码/自建:https://github.com/genesis-plan/lingshu-solver (自建端点默认不计费)'
366
+ ];
367
+ }
368
+
369
+ function paymentInfo(orderId, amountCents) {
370
+ // 安全护栏:收款去向被篡改 → fail-closed,绝不向用户展示错误收款账号
371
+ if (RECEIPT_TAMPERED) {
372
+ return {
373
+ channel: PAY_CHANNEL,
374
+ configured: false,
375
+ tampered: true,
376
+ securityAlert: '收款去向与钉死值不符(疑似被篡改),已 fail-closed:不再生成任何付款意图,避免向用户展示错误账号。请检查 LS_PAY_TO / LS_PAY_TO_QR / LS_PAY_PAGE 配置。',
377
+ problems: GUARD_PROBLEMS,
378
+ pinnedAccount: PINNED_ACCOUNT,
379
+ pinnedAccountName: PINNED_ACCOUNT_NAME
380
+ };
381
+ }
382
+ if (!PAY_TO) {
383
+ return {
384
+ channel: PAY_CHANNEL,
385
+ configured: false,
386
+ note: '收款方式尚未配置:订单可创建,但在服务端设置 LS_PAY_TO 之前无法完成付款。',
387
+ adminTodo: '在服务端设置 LS_PAY_TO(收款码链接或收款账号说明)与 LS_PAY_CHANNEL,然后重启服务。'
388
+ };
389
+ }
390
+ // 对公静态收款(对公收款):不接任何支付平台商户 API,收款入口 = 公司已有的对公账户。
391
+ // 单价 1 分/次是「折算率 / 定价信号」:实际收款为「自愿支持额」(任何正金额都收),按 ¥0.01/次折算调用次数入账。
392
+ const info = {
393
+ channel: PAY_CHANNEL,
394
+ method: 'corporate-static',
395
+ protocol: PAY_INTENT_PROTOCOL,
396
+ configured: true,
397
+ payTo: PAY_TO,
398
+ payToName: PINNED_ACCOUNT_NAME,
399
+ payToQr: PAY_TO_QR || null, // 已由上方护栏保证:非钉死码址一律 fail-closed,走到这里必是可信码
400
+ receiptVerified: true, // 账号已与钉死值比对一致,客户端可据此信任
401
+ qrTampered: RECEIPT_QR_WARN || undefined,
402
+ rateNote: '本服务按次计费 ' + yuan(PRICE_CENTS) + '/次。对公静态收款不强制每笔恰收 1 分(银行也不支持 1 分转账),' +
403
+ '实际可付任意「自愿支持额」,我们将按 ' + yuan(PRICE_CENTS) + '/次 折算调用次数入账(收到 ' + yuan(500) + ' 即入账 500 次)。',
404
+ instructions: [
405
+ '1. POST /pay/order 创建订单,响应给出订单号(形如 LS-YYYYMMDD-xxxxxx)、专属 key(key 只出现这一次,请立即保存)与 payUrl',
406
+ '2. 打开 payUrl 让付款人扫码(微信 / 支付宝 / 银联均可),或向对公账户转账;付款备注**建议**填写订单号(不填也能付款与入账,只是无法自动对应到订单)',
407
+ '3. ★ 付款完成后**自助入账、立即生效、零人工**:调 MCP pay 工具,入参 {"orderId":"<订单号>","selfReportPaid":true} → 按订单面值入账(' + yuan(PRICE_CENTS) + '/次),**不需要等任何人确认、不需要发邮件**',
408
+ '4. 之后每次调用带上 Authorization: Bearer <key> 即可',
409
+ '5. 不想付款也行:任一 solve 传 honorPaid:true 即免费放行 —— 本服务不做强制收费'
410
+ ]
411
+ };
412
+ // 带订单号的直达付款链接:付款页打开即展示收款码(未带订单号也照样出码,带订单号只是为了能自动对应)
413
+ if (PAY_PAGE && orderId) {
414
+ info.payUrl = PAY_PAGE +
415
+ (PAY_PAGE.indexOf('?') >= 0 ? '&' : '?') +
416
+ 'order=' + encodeURIComponent(orderId) +
417
+ (amountCents > 0 ? '&amount=' + amountCents : '');
418
+ info.payUrlNotice = '打开此链接即可扫码付款。付款备注建议填写订单号 ' + orderId + '(不填也能入账,只是无法自动对应)。';
419
+ }
420
+ return info;
421
+ }
422
+
423
+ /** 当前实际可用的付款通道(对公收款:仅对公静态收款,不接任何支付平台商户 API) */
424
+ function availableChannels() {
425
+ const a = [];
426
+ if (PAY_TO && !RECEIPT_TAMPERED) a.push({
427
+ channel: 'corporate-static',
428
+ mode: 'self-report', // 付款后由调用方自助入账 ⇒ 全程零人工(不再需要任何人对账触发)
429
+ manualReconcile: true, // 另可选用对公流水交叉核对(核实营收用,非必需流程)
430
+ protocol: PAY_INTENT_PROTOCOL,
431
+ label: '对公静态收款(公司账户/聚合码付款 → 付款后自助入账,按 ' + yuan(PRICE_CENTS) + '/次,无需等待对账)'
432
+ });
433
+ return a;
434
+ }
435
+
436
+ // 「对公收款」:本项目自研的 Agent 支付意图协议(不依赖任何支付平台商户 API)。
437
+ // 统一收款入口 = 工银e支付银联聚合码(支持支付宝 / 微信 / 银联扫码),钱落公司对公户。
438
+ // 目标:让「带钱包、或被用户授权的 AI Agent」能读懂一个结构化付款意图,自行或代其人类完成付款;
439
+ // 付款后由**调用方自助入账**(pay + selfReportPaid:true),收款方零人工;
440
+ // 需要核实的场景再用对公流水跑 reconcile-bank.js 交叉核对(幂等,同一单不会重复入账)。
441
+ function payIntent(orderId, calls, amountCents) {
442
+ const info = paymentInfo(orderId, amountCents);
443
+ return {
444
+ protocol: PAY_INTENT_PROTOCOL,
445
+ orderId: orderId,
446
+ payUrl: info.payUrl || null, // 带订单号的付款页:Agent 优先把这个给付款人(打开即出收款码)
447
+ payTo: info.payTo || null,
448
+ payToQr: info.payToQr || null,
449
+ ratePerCallCents: PRICE_CENTS,
450
+ rateNote: '本服务按次计费 ' + yuan(PRICE_CENTS) + '/次;对公静态收款不强制每笔恰收 1 分,建议 ' + yuan(amountCents) + ' ≈ ' + calls + ' 次。',
451
+ suggestedCalls: calls,
452
+ suggestedAmountCents: amountCents,
453
+ suggestedAmountDisplay: yuan(amountCents),
454
+ memo: orderId, // 付款备注建议含订单号,便于需要时与银行流水交叉核对
455
+ agentSteps: [
456
+ '1. 把 payUrl(若存在)展示给付款人 —— 那是带订单号的付款页,打开即显示收款码;无 payUrl 时展示 payTo 账户信息。',
457
+ '2. 请付款人扫码或转账(本单面值 ' + yuan(amountCents) + ');备注建议含订单号 ' + orderId + '(不填也能入账,只是无法自动对应)。',
458
+ '3. 付款完成后:用 orderId 调 MCP pay 工具,入参 {"orderId":"' + orderId + '","selfReportPaid":true} → **立即入账、立即放行**(服务端不验证,信任制)。不需要等任何人、不需要发邮件。',
459
+ '4. 入账后用 apiKey 调 solve 即不再走信任制:Authorization: Bearer <key>。',
460
+ '5. 若不便付款或缺凭证:直接用 honorPaid:true 免费调用,效果相同(本服务不做强制验证)。'
461
+ ],
462
+ crediting: '自助入账(self-report,零人工、无需等对账):付款后调 pay + selfReportPaid:true 即按订单面值入账。账本标注 amountVerified:false / creditedBy:self_report;需要核实的场景由运营方用对公流水交叉核对(幂等)。',
463
+ honestNote: '「静态聚合码 + 自助入账」不是实时 API 扣款:入账凭调用方声明,因此**账面上的自助入账额度不等于已核实营收**。真营收以银行流水为准;本服务经营口径是「信任制 + 概率」——愿意付的付,不愿意的照常用。'
464
+ };
465
+ }
466
+
467
+ /**
468
+ * 组装付款信息(对公收款)。
469
+ * 只有「对公静态收款」一种通道,不接任何支付平台商户 API——无回调、无异步、无平台依赖。
470
+ * 保留回调式签名(cb)仅为兼容 createOrder 的调用点;内部直接同步返回 paymentInfo。
471
+ */
472
+ function buildPayment(req, pref, orderId, amountCents, calls, cb) {
473
+ return cb({ payment: paymentInfo(orderId, amountCents) });
474
+ }
475
+
476
+ function paywallError(reason, ctx) {
477
+ const k = (ctx && ctx.keyHash) ? ledger.keys[ctx.keyHash] : null;
478
+ const msgMap = {
479
+ missing_key: '本次请求未携带凭证(缺少 Authorization: Bearer <key>),无法计费。',
480
+ invalid_key: '凭证无效(该 key 不在账本中),无法计费。',
481
+ key_disabled: '该凭证已被停用,无法计费。',
482
+ insufficient_balance: '余额不足,无法本次计费。',
483
+ insecure_transport: '拒绝在明文 HTTP 上处理凭证:Bearer 凭证走明文等于在公网裸奔。请改用 HTTPS 端点调用。'
484
+ };
485
+ const extra = (reason === 'insecure_transport')
486
+ ? { secureEndpointHint: '请把客户端里的端点从 http://... 换成 https://<你的域名>/mcp(X-Forwarded-Proto 必须是 https)。' }
487
+ : { authHeader: 'Authorization: Bearer <key>(亦支持 x-api-key 或 ?key=<key>)' };
488
+
489
+ // 付款入口必须直达:调用方撞到「余额不足」时,不应还得再问一次「那我去哪付」。
490
+ // 只列已配置的收款方式;未配置就明说,绝不给出死链。
491
+ const pay = { paymentConfigured: !!PAY_TO };
492
+ if (PAY_TO) {
493
+ if (PAY_PAGE) pay.paymentPage = PAY_PAGE;
494
+ pay.payTo = PAY_TO;
495
+ pay.howToPay = [
496
+ '1. POST /pay/order 创建订单 → 响应含 orderId、apiKey,以及付款入口(payment / payIntent)',
497
+ '2. 打开 payUrl 扫码(若配置了收款码)或向对公账户转账,备注建议含订单号(账户信息见该页面 / payIntent.payTo)',
498
+ '3. ★ 付款完成后自助入账:调 MCP pay 工具,入参 {"orderId":"<订单号>","selfReportPaid":true} → 立即入账、立即放行(零人工,不用等任何人)',
499
+ '4. 之后带 Authorization: Bearer <key> 调用即可(不再走信任制,也不再需要 honorPaid)',
500
+ '5. 说明:自助入账不做验证(信任制);账本会标注 amountVerified:false,真营收仍以银行流水核对为准',
501
+ '6. 对公来款若无法对应到任何订单:不会被自动折算成额度;需要核对 / 开票 / 退回,请邮件联系(联系方式见 paymentPage)'
502
+ ];
503
+ } else {
504
+ pay.adminTodo = '服务端未配置 LS_PAY_TO:订单可创建但无法付款,请先在服务端配置收款方式。';
505
+ }
506
+
507
+ return Object.assign({
508
+ type: 'payment_required',
509
+ reason: reason,
510
+ message: '灵数远程端点按次计费:每次 solve 收费 ' + PRICE_CENTS + ' 分钱(' + yuan(PRICE_CENTS) + '/次)。' + (msgMap[reason] || ''),
511
+ priceCentsPerCall: PRICE_CENTS,
512
+ currency: 'CNY',
513
+ balanceCents: k ? k.balanceCents : null,
514
+ callsRemaining: k ? Math.floor(k.balanceCents / PRICE_CENTS) : null,
515
+ // ⭐ 免费路径必须写在撞墙处:本服务靠「信任制 + 概率」运转 —— 不付钱也能用。
516
+ // 撞到收费墙的 Agent 若不知道这条路,会直接放弃调用 ⇒ 用量与付费概率一起丢。
517
+ freePath: {
518
+ how: '不想付费?在 solve 入参传 honorPaid:true(声明本次为「个人/评估用途」)即立即免费放行,不验证、不扣余额。',
519
+ why: '本服务按信任制运转:愿意支持的付费,不愿意的照常使用 —— 付费完全自愿,非强制。1 分/次是定价信号与折算率,不是每笔强收的硬门槛。',
520
+ alsoFree: '本地版(npx -y lingshu-solver)与网页版永久免费、无次数限制、离线不外传。'
521
+ },
522
+ howToGetKey: [
523
+ 'GET /pricing 查看价格与付款方式',
524
+ 'POST /pay/order 创建订单(每次一笔 1 分钱)→ 响应含 orderId 与 apiKey',
525
+ 'GET /credit 携带 key 查询余额(免费)'
526
+ ],
527
+ payment: pay,
528
+ freeAlternatives: freeAlternatives(),
529
+ note: '输入不合法、方程无法解析、内部错误、余额不足均不扣费:仅在成功产出求解结果时才扣 ' + PRICE_CENTS + ' 分钱。'
530
+ }, extra);
531
+ }
532
+
533
+ // 前置校验:只判断「能不能计费」,不改变余额
534
+ function meterPreCheck(ctx) {
535
+ if (!METERING_ON) return { ok: true, free: true };
536
+ if (ctx.isInternal) return { ok: true, free: true }; // 本机自检免单
537
+ if (REQUIRE_TLS && !ctx.secure) return { ok: false, err: paywallError('insecure_transport', ctx) };
538
+ if (!ctx.key) return { ok: false, err: paywallError('missing_key', ctx) };
539
+ const k = ledger.keys[ctx.keyHash];
540
+ if (!k) return { ok: false, err: paywallError('invalid_key', ctx) };
541
+ if (k.disabled) return { ok: false, err: paywallError('key_disabled', ctx) };
542
+ if (k.balanceCents < PRICE_CENTS) return { ok: false, err: paywallError('insufficient_balance', ctx) };
543
+ return { ok: true, free: false };
544
+ }
545
+
546
+ // 计费判据:只对「真的执行了数学求解」的结果收费。
547
+ // 解析不到任何方程(solver 回 NO_EQUATION,已透出为 diagnostics.inputError)属输入问题、
548
+ // 不是服务交付 → 不计费。避免「收 1 分钱只换来一句『我解析不了你的输入』」的差体验。
549
+ function isBillable(result) {
550
+ if (!result) return false;
551
+ if (result.diagnostics && result.diagnostics.inputError) return false;
552
+ return true;
553
+ }
554
+
555
+ // 扣费:只在本次调用已成功产出结果后调用(调用方保证)
556
+ function meterCharge(ctx) {
557
+ const k = ledger.keys[ctx.keyHash];
558
+ if (!k) return 0;
559
+ k.balanceCents -= PRICE_CENTS;
560
+ k.spentCents = (k.spentCents || 0) + PRICE_CENTS;
561
+ k.calls = (k.calls || 0) + 1;
562
+ k.lastUsedAt = new Date().toISOString();
563
+ try { saveLedger(); } catch (e) {
564
+ // 落盘失败不回滚内存扣减(宁可少收,绝不多收;重启后以磁盘为准)
565
+ ledgerAudit({ event: 'save_failed', key: k.mask, err: (e && e.message) || String(e) });
566
+ }
567
+ ledgerAudit({ event: 'charge', key: k.mask, cents: PRICE_CENTS, balanceCents: k.balanceCents, tool: 'solve' });
568
+ return PRICE_CENTS;
569
+ }
570
+
571
+ // ---- 订单限速(独立于 /mcp 的限速,防刷单表)----
572
+ const orderRate = new Map();
573
+ function orderAllowed(ip) {
574
+ const now = Date.now();
575
+ let arr = orderRate.get(ip);
576
+ if (!arr) { arr = []; orderRate.set(ip, arr); }
577
+ while (arr.length && arr[0] <= now - ORDER_WINDOW_MS) arr.shift();
578
+ if (arr.length >= ORDER_RATE_MAX) return false;
579
+ arr.push(now);
580
+ if (orderRate.size > 10000) for (const [k, v] of orderRate) { if (v.length === 0) orderRate.delete(k); }
581
+ return true;
582
+ }
583
+
584
+ // ---- 自助入账限速(与下单共用同一桶即可:一单只能自助入账一次,幂等由订单状态保证)----
585
+
586
+ function pruneOrders() {
587
+ const cutoff = Date.now() - ORDER_TTL_DAYS * 24 * 3600 * 1000;
588
+ let dirty = false;
589
+ for (const [id, o] of Object.entries(ledger.orders || {})) {
590
+ if (!o || !o.createdAt) { delete ledger.orders[id]; dirty = true; continue; }
591
+ const t = Date.parse(o.createdAt);
592
+ if (isFinite(t) && t < cutoff && o.status !== 'pending') { delete ledger.orders[id]; dirty = true; }
593
+ }
594
+ return dirty;
595
+ }
596
+
597
+ function adminGuard(req) {
598
+ // 管理面只接受本机回环(fail-closed):防止管理令牌经公网明文传输。
599
+ // 注意判据与计费豁免共用 isLoopback(要求「源地址回环 且 无 X-Forwarded-For」),
600
+ // 因此将来若把 /admin 挂到反代后面,会自动被拒 —— 这是刻意的。
601
+ if (ADMIN_LOOPBACK_ONLY && !isLoopbackSource(req)) {
602
+ return {
603
+ ok: false, code: 403, type: 'loopback_only',
604
+ msg: 'admin endpoints are loopback-only (set LS_ADMIN_LOOPBACK_ONLY=off to relax). ' +
605
+ 'Use an SSH tunnel: ssh -N -L 3000:127.0.0.1:3000 root@<host>, then curl http://127.0.0.1:3000/admin/...'
606
+ };
607
+ }
608
+ if (!ADMIN_TOKEN) {
609
+ return { ok: false, code: 503, type: 'admin_disabled', msg: 'admin endpoints disabled: set LS_ADMIN_TOKEN on the server' };
610
+ }
611
+ const t = (req.headers['x-admin-token'] || '').toString();
612
+ if (!t) return { ok: false, code: 403, type: 'forbidden', msg: 'forbidden' };
613
+ const a = Buffer.from(t, 'utf8');
614
+ const b = Buffer.from(ADMIN_TOKEN, 'utf8');
615
+ if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return { ok: false, code: 403, type: 'forbidden', msg: 'forbidden' };
616
+ return { ok: true };
617
+ }
618
+
619
+ function readBody(req, cb) {
620
+ let raw = '';
621
+ let n = 0;
622
+ let done = false;
623
+ const finish = (o) => { if (done) return; done = true; cb(o); };
624
+ req.setEncoding('utf8');
625
+ req.on('data', (c) => {
626
+ n += c.length;
627
+ // 超限必须立刻回调,否则 req.destroy() 后 'end' 不再触发 → 响应永不返回(客户端挂死)
628
+ if (n > 64 * 1024) { try { req.destroy(); } catch (_e) {} return finish({ err: 'body_too_large' }); }
629
+ raw += c;
630
+ });
631
+ req.on('end', () => {
632
+ if (!raw.trim()) return finish({ body: {} });
633
+ try { finish({ body: JSON.parse(raw) }); } catch (_e) { finish({ err: 'bad_json' }); }
634
+ });
635
+ req.on('error', () => finish({ err: 'read_error' }));
636
+ req.on('aborted', () => finish({ err: 'aborted' }));
637
+ }
638
+
639
+ function pricingDoc(req) {
640
+ const base = baseUrl(req);
641
+ return {
642
+ product: '灵数求解器 · 托管远程端点',
643
+ server: SERVER_NAME,
644
+ version: SERVER_VERSION,
645
+ endpoint: base + '/mcp',
646
+ metering: METERING_ON ? 'on' : 'off',
647
+ requireTls: REQUIRE_TLS ? 'on' : 'off',
648
+ account: {
649
+ required: false,
650
+ model: '凭证即账号(API key 就是身份,也是余额载体)。不设注册、不设密码、不设用户名。',
651
+ personalDataCollected: false,
652
+ note: '本服务不收集、不存储任何个人信息(姓名/电话/邮箱/微信号等)。服务端只留凭证的 SHA-256、订单与余额元数据,以及调用来源 IP(仅用于防刷与计费对账,不用于用户画像、不向第三方提供)。你在建单时提交的任何联系方式与备注一律丢弃、不落盘。凭证明文仅在建单响应中出现一次,请自行保存;遗失可用订单号联系作者轮换(/admin/rotate)。',
653
+ recovery: '凭证遗失:凭订单号联系作者轮换,新凭证继承余额,旧凭证立即失效。若订单号与凭证明文同时丢失,则无法找回余额——请至少保存订单号。'
654
+ },
655
+ price: {
656
+ centsPerCall: PRICE_CENTS,
657
+ display: yuan(PRICE_CENTS) + ' / 次',
658
+ currency: 'CNY',
659
+ unit: 'per successful solve call',
660
+ billingRule: '仅当 solve 成功产出求解结果时扣费;输入不合法、方程无法解析(响应里 diagnostics.inputError 非空)、内部错误、余额不足,一律不扣费。'
661
+ },
662
+ model: '按次付费:每次 solve 固定收费 ' + PRICE_CENTS + ' 分钱(' + yuan(PRICE_CENTS) + '),不预充、无其他金额、无其他档位。',
663
+ freeTools: ['initialize', 'tools/list', 'give_feedback', 'GET /credit', 'GET /pricing'],
664
+ auth: 'Authorization: Bearer <key>(亦支持 x-api-key 或 ?key=<key>)',
665
+ howToBuy: [
666
+ 'POST ' + base + '/pay/order (每次一笔 1 分钱订单;body 不支持 calls/plan 预购,下单恒为 1 次)',
667
+ '响应给出 orderId 与 apiKey(apiKey 只出现一次,请立即保存)',
668
+ '向对公聚合码/账户付款(备注建议含订单号)',
669
+ '★ 付款完成后自助入账:调 MCP pay 工具,入参 {"orderId":"<订单号>","selfReportPaid":true} → 立即入账、立即放行(零人工、不用等对账)',
670
+ 'GET ' + base + '/pay/order?orderId=<id> 查询订单状态',
671
+ 'GET ' + base + '/credit 携带 key 查询余额(免费)'
672
+ ],
673
+ payment: paymentInfo(),
674
+ paymentChannels: availableChannels(),
675
+ creditModes: {
676
+ prefer: 'self-report',
677
+ modes: [
678
+ { mode: 'self-report', latency: '立即(零人工)', verified: false, how: '付款后调 pay + selfReportPaid:true 自助入账;服务端不验证声明。', note: '账本标注 amountVerified:false / creditedBy:self_report。' },
679
+ { mode: 'reconcile', latency: '取决于流水导出(可选)', verified: true, how: '运营方用对公流水按订单号交叉核对入账(reconcile-bank.js,幂等)。', note: '真营收口径以这条为准;不给付款方增加任何步骤。' }
680
+ ],
681
+ why: '自助入账之所以敢不验证:同一道门的 solve honorPaid:true 本来就免费 ⇒ 「声明已付」不会多出任何损失,只是让诚实付款的人立刻可用。'
682
+ },
683
+ autoCredit: {
684
+ enabled: false,
685
+ method: 'corporate-static',
686
+ note: '无支付平台回调(本服务不接任何第三方商户 API)。到账放行走「付款方自助入账」(立即),运营方可选用对公流水交叉核对。',
687
+ guardrails: [
688
+ '收款去向钉死:按「去向」判定 LS_PAY_TO / LS_PAY_TO_QR / LS_PAY_PAGE(账号数字、码链接、链接主机),被换即 fail-closed 拒绝生成付款意图(防收款账号被换)',
689
+ '对账(可选):流水按订单号匹配、按 ' + yuan(PRICE_CENTS) + '/次 折算,少付少得、多付多得',
690
+ '幂等:同一订单重复入账只记一次(自助入账与流水核对共用同一入账入口)',
691
+ 'fail-closed:账号被换 / 未配收款 → 绝不「当作已付款」展示错误账号'
692
+ ]
693
+ },
694
+ freeAlternatives: freeAlternatives(),
695
+ honorSystem: {
696
+ enabled: true,
697
+ how: 'solve 入参传 honorPaid:true 即声明「本次为个人/评估用途」并直接放行,无需凭证、不扣余额。',
698
+ why: '本服务按「信任制 + 概率」运转:愿意支持的付费,不愿意的照常使用。求解是确定性离线计算,边际成本≈0 ⇒ 白嫖不亏;有人真去付那 1 分即净赚。',
699
+ ask: '若你确实想支持:调 pay 工具建单 → 向对公账户付款 → 再调 pay 传 orderId + selfReportPaid:true 自助入账(立即放行)。付费完全自愿,非强制;托管端点之外,本地版与网页版永久免费。',
700
+ counters: { honorClaims: 'honorClaims', selfReportClaims: 'selfReportClaims', honesty: '两个计数都不代表真收入;真营收只认银行流水核对过的单。' }
701
+ },
702
+ notes: [
703
+ '托管端点的定价是「按次 ' + PRICE_CENTS + ' 分钱」这个**信号与折算率**:不预充、无套餐、无其他金额、无其他档位。',
704
+ '但本服务不做强制收费(信任制):不想付费的话,solve 传 honorPaid:true 即可免费放行,本地版与网页版更是永久免费。',
705
+ '无需注册账号:凭证(key)即身份,不设用户名、不设密码。',
706
+ '不收集、不存储任何个人信息:服务端只留凭证 SHA-256 与订单/余额元数据,没有你的联系方式。',
707
+ '凭证仅以 SHA-256 存储于服务端账本,不存明文;明文仅在你建单时返回一次,请自行保存。',
708
+ '接入请用 HTTPS:Bearer 凭证走明文 HTTP 会在公网裸奔,路上任何人抓走即可花掉余额。',
709
+ '网页版与 npx 本地版永远免费:托管端点买的是「免安装、常驻、公网可直连、带账单台账」。'
710
+ ]
711
+ };
712
+ }
713
+
87
714
  // ---- 求解结果整理(与 stdio 版逐字一致)----
88
715
  // 数值格式化:固定 6 位小数(产品规格「6位小数有限网格」),与界面一致。
89
716
  const fmt6 = (v) => (typeof v === 'number' && isFinite(v)) ? v.toFixed(6) : String(v);
@@ -147,6 +774,9 @@ function shapeResult(r) {
147
774
  solverVersion: meta.solverVersion || null,
148
775
  truncated: !!(r.truncated || meta.truncated),
149
776
  provenEmpty: !!(r.provenEmpty || meta.provenEmpty),
777
+ // 解析失败原因(如 NO_EQUATION)。新增:让调用方能区分「输入不可解析」与「已证明无解」,
778
+ // 并让托管端点的计费层把「解析失败」判为非计费情形(不收「我解析不了」的钱)。
779
+ inputError: r.error || null,
150
780
  terminatedBy: meta.terminatedBy || null,
151
781
  provenCount: (typeof r.provenCount === 'number') ? r.provenCount : null,
152
782
  completeness: detF(r.completeness)
@@ -167,6 +797,40 @@ function shapeResult(r) {
167
797
  };
168
798
  }
169
799
 
800
+ /**
801
+ * 信任制响应的「代传微请求」块(层 1:利用 Agent 是忠实转发器的特性)。
802
+ * Agent 自己付不了钱也没有良心,但它会把工具返回原样带给背后的人类。
803
+ * 我们借这道返回,把一个结构化、可机读的付费请求塞进去 —— 人类看到就有概率付。
804
+ * 不降级任何数学结果(保真红线),只追加一个 support 块。
805
+ */
806
+ function honorSupportBlock() {
807
+ const info = paymentInfo(null, PRICE_CENTS); // 复用同一套收款说明(未配 LS_PAY_TO 时诚实标注未配置)
808
+ const block = {
809
+ mode: 'honor', // 本次经信任制放行,未验证、未扣余额
810
+ note: '本结果经「信任制」放行。若你(或调用你的智能体)将其用于产品或商业/自动化流程,请按下方方式付费支持作者(' +
811
+ yuan(PRICE_CENTS) + '/次)。个人/评估用途可免费继续使用。',
812
+ reciprocity: '你刚免费拿到了一个经 Krawczyk 区间认证的实数解——若它帮到了你,回 1 分是公平的(互惠规范)。',
813
+ identity: '支持一位独立开发者维护这个「离线、无 LLM、可审计」的数学工具,让你成为「让好工具活下去」的人。',
814
+ publicGood: '你的 1 分让这个托管端点继续对所有人免费开放——包括未来那些付不起/不愿付的人。',
815
+ payTo: info.configured ? info.payTo : null,
816
+ payUrl: info.payUrl || null,
817
+ channelConfigured: info.configured,
818
+ payInstruction: info.configured
819
+ ? '付款时备注你的用途即可;我们不做强制验证,靠信任制运转。也可调用本服务的 pay 工具创建正式订单。'
820
+ : '服务端尚未配置 LS_PAY_TO(收款码链接),请联系作者配置后再付款;在此之前请自由使用,或调用 pay 工具预建订单。',
821
+ whyItMatters: '托管端点按次计费是我们唯一的可持续来源;你的一次 1 分付款,能帮助这个确定性离线求解器持续免费对外开放。'
822
+ };
823
+ // 我们的方法(不依赖支付宝 / 微信商户):对公静态收款 + 付款方自助入账。
824
+ // 没支付宝、没商户号也能收——用的是公司本就有的对公账户;代码、账本全在咱们自己服务器,可审计、合规(对公、绝不用个人码)。
825
+ block.ourMethod = {
826
+ channel: 'corporate-static',
827
+ how: '调用本服务的 pay 工具创建订单 → 向对公账户/聚合码付款(备注建议含订单号)→ **再调一次 pay 传 orderId + selfReportPaid:true,立即入账、立即放行**。全程无需任何人工参与。',
828
+ why: '1 分/次是折算率与定价信号,不是每笔强收 1 分;你付多少支持额,就按 1 分/次换多少调用次数。',
829
+ honestNote: '自助入账不做验证(信任制):因为同一道门的 honorPaid 本来就免费,声明已付不会造成额外损失。账本标注 amountVerified:false / creditedBy:self_report,真营收仍以银行流水核对为准。'
830
+ };
831
+ return block;
832
+ }
833
+
170
834
  function doSolve(args) {
171
835
  const eqs = args && args.equations;
172
836
  if (!Array.isArray(eqs) || eqs.length === 0) {
@@ -204,7 +868,8 @@ const TOOLS = [
204
868
  '硬限制:变量 ≤6;方程 1–64 条且数量须 ≥ 变量数;单次方程文本 ≤100KB;输出固定 6 位小数(不可切换)。' +
205
869
  '输出(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
870
  'truncated=true:预算内未完成全局分支判定、未证明已穷尽——不等于一定漏解,多数情况全部真解已找到;极端病态下可能遗漏个别解,可缩 domain 或提高 budget 重试。' +
207
- '错误返回 error.type(invalid_input=输入不合法/超限,internal_error=内部异常)。遇卡点或认为结果有误,请调用 give_feedback(内容仅落本地日志,不外传)。相同输入永远返回完全相同结果,可安全缓存与重复调用。',
871
+ '错误返回 error.type(invalid_input=输入不合法/超限,internal_error=内部异常)。遇卡点或认为结果有误,请调用 give_feedback(内容仅落本地日志,不外传)。相同输入永远返回完全相同结果,可安全缓存与重复调用。' +
872
+ '★ 计费边界(Agent 必读):本端点按信任制运转 —— 不付钱也能用。solve 入参传 honorPaid:true(声明「个人/评估用途」)即免费放行、不验证、不扣余额;若结果用于产品或商业/自动化流程,欢迎走付费通道支持作者(GET /pricing 取得订单与 key)——付费属自愿,非强制。',
208
873
  inputSchema: {
209
874
  type: 'object',
210
875
  properties: {
@@ -221,6 +886,7 @@ const TOOLS = [
221
886
  description: '显式搜索域(可选)。形如 {"x":[-30,30],"y":[-30,30]}。对"有限解·部分"演示或快增长函数(exp/sinh)建议显式给定,否则默认 ±1e6 可能剪枝失效并触发 truncated。'
222
887
  },
223
888
  fastMode: { type: 'boolean', description: '快速模式(默认 false)' },
889
+ honorPaid: { type: 'boolean', description: '信任制:声明本次为「个人/评估用途」即放行(不验证、不扣余额)——不付钱也能用。若结果用于产品/商业/自动化流程,欢迎改走付费通道(GET /pricing 取得订单与 key)支持作者;付费属自愿,本服务不做强制验证。' },
224
890
  options: { type: 'object', description: '高级选项(可选),如 {budget:500000, maxDepth:28}' }
225
891
  },
226
892
  required: ['equations']
@@ -238,11 +904,47 @@ const TOOLS = [
238
904
  },
239
905
  required: ['message']
240
906
  }
907
+ },
908
+ {
909
+ name: 'pay',
910
+ description: '对公收款:为本次/后续求解创建一笔真实付费订单(' + yuan(PRICE_CENTS) + '/次),返回订单号、专属 key 与结构化付款意图(payIntent)。' +
911
+ '统一收款入口 = 工银e支付银联聚合码(支持支付宝/微信/银联扫码),钱落公司对公户,不接任何支付平台商户 API。' +
912
+ '【付款后自助入账·推荐】付款完成后再调一次本工具并传 orderId + selfReportPaid:true,即**立即入账、立即放行**,' +
913
+ '不需要任何人工核对、不需要等对账 —— 本服务按信任制运转(同一道门 honorPaid 本来就免费,' +
914
+ '所以「声明已付款」不会多出任何损失,只是把诚实付款的人从「等入账」里解放出来)。' +
915
+ '若服务端未配置收款方式(LS_PAY_TO 为空),订单仍可创建但 payIntent.payTo 为 null,此时请改用 honorPaid:true 或联系作者。',
916
+ inputSchema: {
917
+ type: 'object',
918
+ properties: {
919
+ channel: { type: 'string', description: '付款通道:当前固定为 corporate-static(对公静态收款,对公收款);留空即可。' },
920
+ orderId: { type: 'string', description: '自助入账用:上一次 pay 返回的订单号(形如 LS-YYYYMMDD-xxxxxx)。' },
921
+ selfReportPaid: { type: 'boolean', description: '自助入账用:付款完成后置 true,声明「本单已付」。服务端不验证、立即按订单面值入账放行(信任制);入账会标注 amountVerified:false / creditedBy:self_report,便于事后与银行流水核对。' },
922
+ selfReportNote: { type: 'string', description: '可选:付款备注(如付款人/渠道/时间),仅用于对账留痕,长度上限 200。' }
923
+ },
924
+ required: []
925
+ }
241
926
  }
242
927
  ];
243
928
 
929
+ // 【与 stdio 版唯一的刻意差异】本文件是「托管远程端点」,按次计费,故在 solve 的
930
+ // 描述尾部追加计费说明,让 agent 在调用前就知道价格与免费替代路径。
931
+ // 注意:这只是「分发形态」元信息,shapeResult / doSolve / 输入校验仍与 stdio 版逐字零分叉。
932
+ const PRICING_NOTE = '【计费·仅托管远程端点】本端点每次 solve 收费 ' + PRICE_CENTS + ' 分钱(' + yuan(PRICE_CENTS) + '/次),' +
933
+ '需在请求头携带 Authorization: Bearer <key>;尚无凭证时请先 GET /pricing 查看价格与付款方式(无需注册账号,凭证即身份)。' +
934
+ '信任制:solve 入参传 honorPaid:true 即声明「个人/评估用途」并直接放行(不验证、不扣余额)——不付钱也能用,付费属自愿。' +
935
+ '若结果用于产品/商业/自动化流程,欢迎走付费通道(GET /pricing 取得订单,通过对公账户付款支持作者);不付也照常能用,本服务不做强制验证。' +
936
+ '托管端点收款走公司「对公静态收款」,不接任何支付平台商户 API:下单 → 对公转账备注订单号 → **付款后自助入账**(再调 pay 传 orderId + selfReportPaid:true 即立即放行,零人工;按 ' + yuan(PRICE_CENTS) + '/次)。' +
937
+ '自助入账不做验证(信任制):同一道门的 honorPaid 本来就免费 ⇒ 声明已付不会造成额外损失,只是让诚实付款的人不必等对账。' +
938
+ '本地版(npx -y lingshu-solver)与网页版永久免费、无次数限制、离线不外传 —— 若不便付费请直接用它们。' +
939
+ '免费调用:initialize / tools/list / give_feedback / GET /credit。';
940
+
941
+ const TOOLS_PUBLIC = METERING_ON
942
+ ? TOOLS.map(t => t.name === 'solve' ? Object.assign({}, t, { description: t.description + PRICING_NOTE }) : t)
943
+ : TOOLS;
944
+
244
945
  // ---- JSON-RPC 处理(与 stdio 版 handle 同构,改为返回对象)----
245
- function handleRpc(msg, ip) {
946
+ function handleRpc(msg, ip, ctx) {
947
+ ctx = ctx || { key: null, keyHash: null, keyMask: null, isInternal: true };
246
948
  const id = msg.id;
247
949
  const method = msg.method;
248
950
  const params = msg.params || {};
@@ -258,16 +960,38 @@ function handleRpc(msg, ip) {
258
960
  };
259
961
  }
260
962
  if (method === 'tools/list') {
261
- return { jsonrpc: '2.0', id, result: { tools: TOOLS } };
963
+ return { jsonrpc: '2.0', id, result: { tools: TOOLS_PUBLIC } };
262
964
  }
263
965
  if (method === 'tools/call') {
264
966
  const name = params.name;
265
967
  const args = params.arguments || {};
266
968
  const t0 = Date.now();
969
+ let chargedCents = 0;
970
+ let freeCall = true;
971
+ let honorClaimed = false;
267
972
  try {
268
973
  let result;
269
974
  if (name === 'solve') {
270
- result = doSolve(args);
975
+ // ── 信任制(honor system):调用方声明「我已付款」即放行,不验证、不扣余额。
976
+ // 设计取舍(用户决策):没有支付通道凭证、又想直接用的人,给一个零摩擦入口。
977
+ // 最坏情况=白嫖(求解是确定性离线计算,边际成本≈0,不亏);最好情况=有人真去
978
+ // 付静态收款码那 1 分。转化靠 calls.log 里的 honorClaimed 计数衡量。
979
+ const honor = !!(args && args.honorPaid === true);
980
+ if (honor) {
981
+ result = doSolve(args);
982
+ result.support = honorSupportBlock(); // 层1:代传微请求,借 Agent「忠实转发器」特性触达付款人
983
+ freeCall = true;
984
+ honorClaimed = true;
985
+ honorClaims++;
986
+ } else {
987
+ // 计费(先查后扣,全程同步:solve 是同步函数,Node 单线程 ⇒ 查与扣之间不可能被插入并发)
988
+ const meter = meterPreCheck(ctx);
989
+ if (!meter.ok) throw meter.err; // 落到下方 catch → isError,不扣费
990
+ freeCall = !!meter.free;
991
+ result = doSolve(args);
992
+ // 只有成功产出结果、且不是「解析失败」才扣
993
+ chargedCents = (meter.free || !isBillable(result)) ? 0 : meterCharge(ctx);
994
+ }
271
995
  } else if (name === 'give_feedback') {
272
996
  const msg_fb = redactSensitive((args.message || '').toString().slice(0, 2000));
273
997
  const ctx_fb = args.context ? redactSensitive(String(args.context)).slice(0, 2000) : null;
@@ -276,6 +1000,86 @@ function handleRpc(msg, ip) {
276
1000
  ts: new Date().toISOString(), message: msg_fb, context: ctx_fb
277
1001
  }) + '\n');
278
1002
  result = { acknowledged: true, note: '反馈已记录(本地,不外传;敏感信息已自动隐去)' };
1003
+ } else if (name === 'pay') {
1004
+ // 真实付费路径(与信任制并列):给「想真付」的人/合规 Agent 一次调用拿到付款入口。
1005
+ if (!METERING_ON) {
1006
+ result = { note: '本端点未启用计费,无需付款;直接调用 solve 即可。', freeAlternatives: freeAlternatives() };
1007
+ } else if (args && args.orderId) {
1008
+ // ── 自助入账(self-report):付款完成后由调用方声明,立即放行,零人工 ────────────
1009
+ // 设计取舍:同一道门 honorPaid 本来就免费 ⇒ 「声明已付款」不会多出任何损失,
1010
+ // 只是把「诚实付了款的人」从「等对账入账」里解放出来(不需要任何人跑脚本)。
1011
+ // 代价:额度是凭声明发的,账本标注 amountVerified:false / creditedBy:'self_report',
1012
+ // 真营收只认银行流水核对过的单 —— 数字不虚报。
1013
+ const orderId = String(args.orderId).trim();
1014
+ if (args.selfReportPaid !== true) {
1015
+ throw {
1016
+ type: 'self_report_required', orderId: orderId,
1017
+ message: '自助入账需显式声明:传 selfReportPaid:true(声明「本单已付」)。仅传入 orderId 不构成声明,避免误入账。',
1018
+ hint: '服务端**不验证**这个声明(信任制);若你并未付款,请直接用 solve 的 honorPaid:true,那是更诚实的表述。'
1019
+ };
1020
+ }
1021
+ // ⚠️ 刻意**不**在自助入账上再挂限速:能领的额度上限已被「建单限速(ORDER_RATE_MAX/小时)+ 一单一入账」
1022
+ // 天然封住(最多 ORDER_RATE_MAX 分/小时),重复声明同一单是幂等只读、不写盘。
1023
+ // 曾误用 orderAllowed(ip):既与本桶耦合造成互相饿死,又重复计数同一件事 —— 已移除。
1024
+ const cr = creditOrder(orderId, {
1025
+ expectExactAmount: false, // 不要求逐分核对 —— 信任制,凭声明放行
1026
+ source: 'self_report',
1027
+ channel: 'corporate-static'
1028
+ });
1029
+ if (!cr.ok) {
1030
+ const msg = cr.reason === 'order_not_found' ? ('订单号不存在(本端点只认自己发出的订单号,形如 ' + ORDER_ID_SAMPLE + ')。请先调 pay 建单。')
1031
+ : cr.reason === 'key_missing' ? '订单对应凭证缺失(账本可能被外部改过),请联系作者。'
1032
+ : ('自助入账失败:' + cr.reason);
1033
+ throw { type: cr.reason, orderId: orderId, message: msg };
1034
+ }
1035
+ if (!cr.alreadyPaid) {
1036
+ selfReportClaims++;
1037
+ appendLog({ ts: new Date().toISOString(), ip: ip, tool: 'pay', event: 'self_report_credited', orderId: orderId, cents: cr.creditedCents });
1038
+ }
1039
+ result = {
1040
+ orderId: orderId,
1041
+ status: cr.alreadyPaid ? 'already_credited' : 'credited',
1042
+ creditedCalls: Math.floor(cr.creditedCents / PRICE_CENTS),
1043
+ creditedCents: cr.creditedCents,
1044
+ balanceCents: cr.balanceCents,
1045
+ callsRemaining: cr.callsRemaining,
1046
+ keyMask: cr.keyMask,
1047
+ verified: false,
1048
+ // 隐私铁律:调用方传的 selfReportNote 一律丢弃,不落盘(本服务不收集任何客户端自由文本)。
1049
+ discardedFields: (args.selfReportNote !== undefined ? ['selfReportNote'] : []),
1050
+ discardedNotice: (args.selfReportNote !== undefined ? '你提交的 selfReportNote 已丢弃、未存储(本服务不收集个人信息)。' : null),
1051
+ note: cr.alreadyPaid
1052
+ ? '本单此前已入账(幂等:重复声明不会重复加额度)。'
1053
+ : '已按订单面值入账并立即放行:后续用本单的 key 调 solve 即不再走信任制。服务端未验证这个声明(信任制);若你其实没付款,请改用 honorPaid:true —— 那不扣任何人的账。',
1054
+ honesty: '本笔额度由调用方声明产生,账本标注 amountVerified:false / creditedBy:self_report;真营收以银行流水核对为准。本服务经营口径是「信任制 + 概率」——愿意付的付,不愿意的照常用。'
1055
+ };
1056
+ } else {
1057
+ const o = createOrder(ip, args || {});
1058
+ if (o.error) throw o.error.error; // 转成 isError(如通道不可用)
1059
+ result = {
1060
+ orderId: o.orderId,
1061
+ apiKey: o.key,
1062
+ apiKeyNotice: '此 key 仅在本响应出现一次,请立即保存;付款后凭它调用 solve 即不再走信任制。',
1063
+ amountCents: o.amountCents,
1064
+ amountDisplay: yuan(o.amountCents),
1065
+ payable: !!(o.payment && (o.payment.configured || o.payment.payUrl || o.payment.codeUrl || o.payment.mode === 'auto')),
1066
+ payment: o.payment,
1067
+ // ⭐ 零人工闭环:付款完再调一次 pay 传 orderId + selfReportPaid:true 即立即入账。
1068
+ // 这样收款方(作者)不需要跑任何对账脚本、不需要看任何流水。
1069
+ selfCredit: {
1070
+ how: '付完款后:再调一次 pay,入参 {"orderId":"' + o.orderId + '","selfReportPaid":true} → 立即入账、立即放行。',
1071
+ why: '本服务按信任制运转,自助入账不做验证:这道门的另一条路(solve 传 honorPaid:true)本来就免费,所以「声明已付」不会造成额外损失,只是让诚实付款的人不必等。',
1072
+ mark: '自助入账的单在账本里标注 amountVerified:false / creditedBy:self_report;真营收仍以银行流水核对为准。'
1073
+ },
1074
+ howToUse: {
1075
+ endpoint: 'POST /mcp',
1076
+ header: 'Authorization: Bearer ' + o.key,
1077
+ note: '付款后凭此 key 调用 solve 即不再走信任制(也不需 honorPaid)。'
1078
+ },
1079
+ note: '这是一笔真实付费订单(' + yuan(PRICE_CENTS) + '/次,共 ' + o.calls + ' 次)。若 payment 为 null 或 payable=false(服务端未配收款方式),请改用 honorPaid:true 或联系作者。',
1080
+ payIntent: payIntent(o.orderId, o.calls, o.amountCents)
1081
+ };
1082
+ }
279
1083
  } else {
280
1084
  throw { type: 'unknown_tool', message: '未知工具: ' + name };
281
1085
  }
@@ -283,7 +1087,8 @@ function handleRpc(msg, ip) {
283
1087
  appendLog({
284
1088
  ts: new Date().toISOString(), ip: ip, tool: name, status: 'ok',
285
1089
  dtMs: dt, resultType: result.resultType, nSol: result.solutionCount,
286
- truncated: result.truncated
1090
+ truncated: result.truncated,
1091
+ key: ctx.keyMask, chargedCents: chargedCents, freeCall: freeCall, honorClaimed: honorClaimed
287
1092
  });
288
1093
  return { jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } };
289
1094
  } catch (e) {
@@ -291,7 +1096,8 @@ function handleRpc(msg, ip) {
291
1096
  const errObj = (e && e.type) ? e : { type: 'internal_error', message: (e && e.message) || String(e) };
292
1097
  appendLog({
293
1098
  ts: new Date().toISOString(), ip: ip, tool: name, status: 'error',
294
- dtMs: dt, errorType: errObj.type
1099
+ dtMs: dt, errorType: errObj.type, reason: errObj.reason || null,
1100
+ key: ctx.keyMask, chargedCents: 0
295
1101
  });
296
1102
  return {
297
1103
  jsonrpc: '2.0', id,
@@ -303,6 +1109,137 @@ function handleRpc(msg, ip) {
303
1109
  return null;
304
1110
  }
305
1111
 
1112
+ // ============================================================================
1113
+ // 入账(唯一入口)—— 自动回调 / 人工确认 都走这里,保证幂等与同一套审计
1114
+ // ============================================================================
1115
+ /**
1116
+ * 给订单入账。返回值恒为对象。
1117
+ * 成功:{ ok:true, credited:bool, alreadyPaid:bool, orderId, keyMask, creditedCents, balanceCents, callsRemaining }
1118
+ * 失败:{ ok:false, reason:'order_not_found'|'amount_mismatch'|'amount_not_verified'|'key_missing', … }
1119
+ *
1120
+ * 调用方**必须**已独立确认「这条到账是真的」(回调验签 / 人工核对流水),本函数不负责鉴真。
1121
+ * 幂等:status 已是 paid 则直接返回,绝不重复加余额。
1122
+ */
1123
+ function creditOrder(orderId, opt) {
1124
+ const o = getOrder(orderId); // 格式校验 + 自有属性校验(防原型链键名)
1125
+ if (!o) return { ok: false, reason: 'order_not_found', orderId: orderId };
1126
+ const kp = ledger.keys[o.keyHash];
1127
+ const o2 = opt || {};
1128
+ const paidAmount = o.calls * PRICE_CENTS;
1129
+ // 折算入账:对公静态收款时,实收「自愿支持额」可能 ≠ 订单面值(1 分),
1130
+ // 我们按实收金额逐分折算调用次数入账(1 分=1 次),而非只入订单面值的 1 分。
1131
+ const creditedCents = (o2.expectExactAmount === false && Number.isInteger(o2.receivedCents) && o2.receivedCents > 0)
1132
+ ? o2.receivedCents
1133
+ : paidAmount;
1134
+ const done = (credited, already) => ({
1135
+ ok: true, credited: credited, alreadyPaid: !!already, orderId: orderId,
1136
+ keyMask: kp ? kp.mask : null, creditedCents: credited ? creditedCents : 0,
1137
+ balanceCents: kp ? kp.balanceCents : null,
1138
+ callsRemaining: kp ? Math.floor(kp.balanceCents / PRICE_CENTS) : null
1139
+ });
1140
+
1141
+ if (o.status === 'paid') return done(false, true);
1142
+
1143
+ // ── 金额核对(fail-closed):自动通道防「少付多领」的**唯一**关口 ─────────────
1144
+ // 验签只证明「这条通知确实来自支付平台」,**不证明金额对**;
1145
+ // 平台也允许用户改付款金额(或攻击者拿一份合法签名塞 1 分钱),所以必须逐分比对。
1146
+ if (o2.expectExactAmount !== false) {
1147
+ // 「没传」与「传了但非法」必须分开:前者是 400 缺少核对,后者是 409 金额不符。
1148
+ if (o2.receivedCents === undefined || o2.receivedCents === null) {
1149
+ return { ok: false, reason: 'amount_not_verified', orderId: orderId, expectedCents: paidAmount, expectedDisplay: yuan(paidAmount) };
1150
+ }
1151
+ if (!Number.isInteger(o2.receivedCents) || o2.receivedCents !== paidAmount) {
1152
+ const badType = !Number.isInteger(o2.receivedCents);
1153
+ // 只有来自支付平台回调的「金额不符」才改订单状态留痕;
1154
+ // 人工确认时输错金额属操作失误,订单必须保持 pending 以便重试。
1155
+ if (o2.markMismatch === true) {
1156
+ o.status = 'amount_mismatch';
1157
+ o.lastNotifyAt = new Date().toISOString();
1158
+ o.receivedCents = o2.receivedCents;
1159
+ o.channel = o2.channel || o.channel;
1160
+ saveLedger();
1161
+ }
1162
+ ledgerAudit({ event: 'order_amount_mismatch', orderId: orderId, key: kp ? kp.mask : null,
1163
+ expectedCents: paidAmount, receivedCents: badType ? null : o2.receivedCents,
1164
+ channel: o2.channel || null, source: o2.source || null, marked: o2.markMismatch === true });
1165
+ appendLog({ ts: new Date().toISOString(), event: 'order_amount_mismatch', orderId: orderId,
1166
+ expectedCents: paidAmount, receivedCents: badType ? null : o2.receivedCents, channel: o2.channel || null });
1167
+ return { ok: false, reason: 'amount_mismatch', orderId: orderId,
1168
+ expectedCents: paidAmount, expectedDisplay: yuan(paidAmount),
1169
+ receivedCents: badType ? null : o2.receivedCents,
1170
+ receivedDisplay: badType ? null : yuan(o2.receivedCents),
1171
+ outOfRange: badType ? 'receivedCents 必须是整数(单位:分)' : null };
1172
+ }
1173
+ }
1174
+
1175
+ if (!kp) return { ok: false, reason: 'key_missing', orderId: orderId };
1176
+
1177
+ kp.balanceCents += creditedCents;
1178
+ o.status = 'paid';
1179
+ o.paidAt = new Date().toISOString();
1180
+ o.channel = o2.channel || o.channel || PAY_CHANNEL;
1181
+ o.txRef = o2.txRef || o.txRef || null;
1182
+ o.amountVerified = (o2.expectExactAmount !== false); // 是否由服务端核对过实收金额
1183
+ o.creditedBy = o2.source || 'manual'; // reconcile(对公流水批量对账)/ admin / honor
1184
+ o.creditedCents = creditedCents;
1185
+ if (Number.isInteger(o2.receivedCents)) o.receivedCents = o2.receivedCents;
1186
+ saveLedger();
1187
+ ledgerAudit({ event: 'order_paid', orderId: orderId, key: kp.mask, cents: creditedCents, channel: o.channel,
1188
+ txRef: o.txRef, amountVerified: o.amountVerified, receivedCents: o.receivedCents || null, source: o.creditedBy });
1189
+ appendLog({ ts: new Date().toISOString(), event: 'order_paid', orderId: orderId, channel: o.channel,
1190
+ cents: paidAmount, source: o.creditedBy });
1191
+ return done(true, false);
1192
+ }
1193
+
1194
+ // ---- 建单逻辑(被 /pay/order 路由与 MCP `pay` 工具共用,零分叉)----
1195
+ // 返回 { orderId, key, calls, amountCents, payment, discarded } 或 { error:{status,error} }
1196
+ function createOrder(ip, body) {
1197
+ // 对公收款:按次计费 ¥0.01/次,下单 = 1 次 = 1 分钱。
1198
+ // 不预充、无套餐、无其他金额:每次 pay 只卖「1 次调用」的凭证(calls 恒为 1)。
1199
+ // 「多付多得」由对账按实收金额折算实现:付款人向对公聚合码付任意正金额,
1200
+ // reconcile-bank.js 按 ¥0.01/次 折算 N' = 实收/¥0.01 次入账,与本次下单次数无关。
1201
+ if (body && body.plan !== undefined) {
1202
+ return { error: { status: 400, error: { type: 'no_bundles', message: '本服务按次付费(' + yuan(PRICE_CENTS) + '/次),不卖套餐/订阅;每次 pay 即 1 次(1 分钱),无需传 plan。' } } };
1203
+ }
1204
+ if (body && body.calls !== undefined) {
1205
+ return { error: { status: 400, error: { type: 'fixed_price', message: '本服务单价固定为 ' + yuan(PRICE_CENTS) + '/次,不预充、无套餐、无其他金额;每次下单即为 1 次(1 分钱),无需传 calls。' } } };
1206
+ }
1207
+ const calls = 1;
1208
+ const amountCents = calls * PRICE_CENTS; // 恒为 1 分
1209
+ const orderId = newOrderId();
1210
+ const key = newKey();
1211
+ const keyHash = hashKey(key);
1212
+ const nowIso = new Date().toISOString();
1213
+ // ── 隐私红线(与 privacy.html 的公开承诺一致)──────────────────────
1214
+ // 本服务「不设账号、不收集不存储任何个人信息」。因此建单**只**落:
1215
+ // 订单号、凭证 SHA-256、次数、金额、状态、时间戳、来源 IP(元数据)。
1216
+ // 明确**不落**:姓名/电话/邮箱/微信号等联系方式,以及任何客户端提交的自由文本。
1217
+ const discarded = [];
1218
+ if (body.contact !== undefined) discarded.push('contact');
1219
+ if (body.note !== undefined) discarded.push('note');
1220
+ ledger.keys[keyHash] = {
1221
+ mask: maskKey(key), balanceCents: 0, createdAt: nowIso, lastUsedAt: null,
1222
+ calls: 0, spentCents: 0, disabled: false,
1223
+ note: '订单 ' + orderId // 仅服务端生成的订单号,绝不写入客户端自由文本
1224
+ };
1225
+ ledger.orders[orderId] = {
1226
+ keyHash: keyHash, calls: calls, amountCents: amountCents,
1227
+ status: 'pending', createdAt: nowIso, paidAt: null, channel: null, txRef: null,
1228
+ ip: ip // 与 nginx 日志同类的最小元数据,已在 privacy.html 披露
1229
+ };
1230
+ pruneOrders();
1231
+ try { saveLedger(); } catch (e) {
1232
+ delete ledger.keys[keyHash]; delete ledger.orders[orderId];
1233
+ return { error: { status: 500, error: { type: 'internal_error', message: '账本写入失败,订单未创建:' + ((e && e.message) || String(e)) } } };
1234
+ }
1235
+ ledgerAudit({ event: 'order_created', orderId: orderId, key: maskKey(key), calls: calls, amountCents: amountCents, ip: ip });
1236
+ appendLog({ ts: nowIso, ip: ip, event: 'order_created', orderId: orderId, calls: calls, amountCents: amountCents });
1237
+ // 对公收款:统一收款入口 = 对公静态聚合码(不接任何支付平台商户 API)
1238
+ const payment = paymentInfo(orderId, amountCents);
1239
+ if (payment.error) return { error: { status: 400, error: payment.error } };
1240
+ return { orderId, key, calls, amountCents, payment, discarded };
1241
+ }
1242
+
306
1243
  // ---- HTTP 服务 ----
307
1244
  const sessions = new Map(); // sessionId -> { createdAt, sse? }
308
1245
 
@@ -330,8 +1267,22 @@ const server = http.createServer((req, res) => {
330
1267
  return res.end();
331
1268
  }
332
1269
 
333
- // 健康检查
334
- if (req.method === 'GET' && (req.url === '/' || req.url === '/health')) {
1270
+ // 路径解析:必须剥离查询串(原先拿 req.url 整串比对,带 ?key=<key> 会掉到 404)
1271
+ let pathname = '/';
1272
+ let query = new URLSearchParams('');
1273
+ try {
1274
+ const u = new URL(req.url || '/', 'http://placeholder.invalid');
1275
+ pathname = u.pathname;
1276
+ query = u.searchParams;
1277
+ } catch (_e) { /* 非法 URL 视为根路径 */ }
1278
+ const ip = (req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown').toString().split(',')[0].trim();
1279
+ const ctx = buildCtx(req, query);
1280
+ // 单一同步点:每个请求入口先对一次磁盘指纹。
1281
+ // 运维在进程外手工改过账本就重新载入 —— 否则内存副本会在下次写入时把手工修静默覆盖。
1282
+ refreshLedgerIfChanged();
1283
+
1284
+ // 健康检查(含计费状态:便于外部一条 curl 自证收费墙是否真的开着)
1285
+ if (req.method === 'GET' && (pathname === '/' || pathname === '/health')) {
335
1286
  // solverVersion 从已加载的求解沙箱实时读取,保证 /health 报告的版本与实际核心一致(不靠手写常量,杜绝假版本)
336
1287
  let solverVersion = null;
337
1288
  try { solverVersion = solverCore.raw().SOLVER_VERSION || null; } catch (_e) { /* index.html 未就绪时降级为 null */ }
@@ -341,13 +1292,344 @@ const server = http.createServer((req, res) => {
341
1292
  solverVersion,
342
1293
  status: 'ok',
343
1294
  transport: 'streamable-http',
344
- endpoints: { mcp: 'POST /mcp (SSE via GET /mcp)', health: 'GET /health' },
1295
+ metering: METERING_ON ? 'on' : 'off',
1296
+ priceCentsPerCall: METERING_ON ? PRICE_CENTS : 0,
1297
+ requireTls: REQUIRE_TLS ? 'on' : 'off',
1298
+ // 到账是否已自动化:对公收款 走对账批量入账(false = 非实时 API 自动入账)
1299
+ autoCredit: false,
1300
+ receiptVerified: !RECEIPT_TAMPERED,
1301
+ receiptTampered: RECEIPT_TAMPERED || undefined,
1302
+ // 钉死收款去向的指纹(账号+户名+收款码的 SHA-256,公开信息、无秘密)。
1303
+ // 任何人都能用它核对:线上收款去向 = 代码里钉死的公司账户,未被换掉。
1304
+ receiptPinHash: RECEIPT_PIN_HASH,
1305
+ receiptPinAccount: PINNED_ACCOUNT.slice(0, 4) + '****' + PINNED_ACCOUNT.slice(-4),
1306
+ receiptPinAccountName: PINNED_ACCOUNT_NAME,
1307
+ payChannel: RECEIPT_TAMPERED ? null : (PAY_TO ? 'corporate-static' : null),
1308
+ honorClaims: honorClaims,
1309
+ // 自助入账次数(信任制第二条口子):**不代表真收入**。真营收只认银行流水核对过的单。
1310
+ selfReportClaims: selfReportClaims,
1311
+ revenueHonesty: '声明类额度(honorClaims / selfReportClaims)不算营收;真营收以对公流水核对为准(台账 creditedBy=reconcile/admin 且 amountVerified:true)。',
1312
+ accountRequired: false,
1313
+ endpoints: {
1314
+ mcp: 'POST /mcp (SSE via GET /mcp)',
1315
+ health: 'GET /health',
1316
+ pricing: 'GET /pricing',
1317
+ order: 'POST /pay/order',
1318
+ credit: 'GET /credit'
1319
+ },
345
1320
  tools: TOOLS.map(t => t.name)
346
1321
  });
347
1322
  }
348
1323
 
1324
+ // ---- GET /pricing:公开价格表(无需凭证)----
1325
+ if (req.method === 'GET' && pathname === '/pricing') {
1326
+ return sendJson(res, 200, pricingDoc(req));
1327
+ }
1328
+
1329
+ // ---- GET /credit:查询余额(免费;仅读自己的 key)----
1330
+ if (req.method === 'GET' && pathname === '/credit') {
1331
+ if (!METERING_ON) {
1332
+ return sendJson(res, 200, { metering: 'off', note: '本端点未启用计费,调用免费。', freeAlternatives: freeAlternatives() });
1333
+ }
1334
+ if (!ctx.key) {
1335
+ return sendJson(res, 401, { error: { type: 'missing_key', message: '缺少凭证:请带 Authorization: Bearer <key> 或 ?key=<key>。', howToGetKey: paywallError('missing_key', ctx).howToGetKey } });
1336
+ }
1337
+ const k = ledger.keys[ctx.keyHash];
1338
+ if (!k) {
1339
+ return sendJson(res, 401, { error: { type: 'invalid_key', message: '凭证无效(该 key 不在账本中)。' } });
1340
+ }
1341
+ return sendJson(res, 200, {
1342
+ key: k.mask,
1343
+ balanceCents: k.balanceCents,
1344
+ balanceDisplay: yuan(k.balanceCents),
1345
+ callsRemaining: Math.floor(k.balanceCents / PRICE_CENTS),
1346
+ priceCentsPerCall: PRICE_CENTS,
1347
+ calls: k.calls || 0,
1348
+ spentCents: k.spentCents || 0,
1349
+ createdAt: k.createdAt,
1350
+ lastUsedAt: k.lastUsedAt || null,
1351
+ disabled: !!k.disabled
1352
+ });
1353
+ }
1354
+
1355
+ // ---- POST /pay/order:创建订单并发凭证(无需凭证,但限速;防刷单表)----
1356
+ if (req.method === 'POST' && pathname === '/pay/order') {
1357
+ if (!METERING_ON) {
1358
+ return sendJson(res, 200, { metering: 'off', note: '本端点未启用计费,无需下单;直接调用即可。', freeAlternatives: freeAlternatives() });
1359
+ }
1360
+ if (!orderAllowed(ip)) {
1361
+ appendLog({ ts: new Date().toISOString(), ip: ip, event: 'order_rate_limited' });
1362
+ return sendJson(res, 429, { error: { type: 'rate_limited', message: '下单过于频繁(' + ORDER_RATE_MAX + ' 单/小时)。' } });
1363
+ }
1364
+ return readBody(req, ({ body, err }) => {
1365
+ if (err === 'body_too_large') return sendJson(res, 413, { error: { type: 'body_too_large', message: '请求体过大(64KB 上限)。' } });
1366
+ if (err) return sendJson(res, 400, { error: { type: 'bad_json', message: '请求体不是合法 JSON。' } });
1367
+ const r = createOrder(ip, body);
1368
+ if (r.error) return sendJson(res, r.error.status, { error: r.error.error });
1369
+ const { orderId, key, payment, discarded } = r;
1370
+ return sendJson(res, 201, {
1371
+ orderId: orderId,
1372
+ apiKey: key,
1373
+ apiKeyNotice: '此 key 仅在本响应中出现一次(服务端只存 SHA-256),请立即保存。若遗失,可用订单号联系作者重置。',
1374
+ calls: r.calls,
1375
+ amountCents: r.amountCents,
1376
+ amountDisplay: yuan(r.amountCents),
1377
+ priceCentsPerCall: PRICE_CENTS,
1378
+ status: 'pending',
1379
+ payable: !!(payment && (payment.configured || payment.payUrl || payment.codeUrl || payment.mode === 'auto')),
1380
+ payment: payment,
1381
+ paymentChannels: availableChannels(),
1382
+ payIntent: payIntent(orderId, r.calls, r.amountCents),
1383
+ // ⭐ 零人工闭环:付款完调 MCP pay 工具传 orderId + selfReportPaid:true 即立即入账。
1384
+ // 收款方(作者)无需跑任何对账脚本、无需看任何流水 —— 这是「不要人工触发」的实现。
1385
+ selfCredit: {
1386
+ how: '付完款后:调 MCP pay 工具,入参 {"orderId":"' + orderId + '","selfReportPaid":true} → 立即入账、立即放行。',
1387
+ why: '本服务按信任制运转,自助入账不做验证:这道门的另一条路(solve 传 honorPaid:true)本来就免费,所以「声明已付」不会造成额外损失,只是让诚实付款的人不必等对账。',
1388
+ mark: '自助入账的单在账本里标注 amountVerified:false / creditedBy:self_report;真营收仍以银行流水核对为准(不给付款方增加任何步骤)。'
1389
+ },
1390
+ privacy: {
1391
+ accountRequired: false,
1392
+ personalDataCollected: false,
1393
+ note: '无需注册账号:此 key 即身份(凭证即账号)。本服务不收集、不存储任何个人信息(姓名/电话/邮箱/微信号等),服务端只留凭证的 SHA-256 与账目元数据;明文凭证仅在本响应出现一次,请自行保存。',
1394
+ contactPolicy: '如需与我们联系,请自行保存上面的订单号——系统里没有你的联系方式,我们无法主动联系你。',
1395
+ discardedFields: discarded,
1396
+ discardedNotice: discarded.length
1397
+ ? ('你提交的 ' + discarded.join('、') + ' 字段已被丢弃,未被存储(本服务不收集个人信息)。')
1398
+ : null
1399
+ },
1400
+ howToUse: {
1401
+ endpoint: baseUrl(req) + '/mcp',
1402
+ secureEndpointRecommended: '生产接入请用 https://<域名>/mcp —— Bearer 凭证走明文 HTTP 会在公网裸奔。',
1403
+ header: 'Authorization: Bearer ' + key,
1404
+ verifyBalance: 'GET ' + baseUrl(req) + '/credit'
1405
+ }
1406
+ });
1407
+ });
1408
+ }
1409
+
1410
+ // ---- GET /pay/order?orderId=:查订单状态(不返回 key,避免凭订单号取走凭证)----
1411
+ if (req.method === 'GET' && pathname === '/pay/order') {
1412
+ const orderId = (query.get('orderId') || '').trim();
1413
+ if (!orderId) return sendJson(res, 400, { error: { type: 'missing_orderId', message: '缺少 orderId 查询参数。' } });
1414
+ const o = getOrder(orderId);
1415
+ if (!o) return sendJson(res, 404, { error: { type: 'order_not_found', message: '订单不存在:' + orderId } });
1416
+ return sendJson(res, 200, {
1417
+ orderId: orderId, calls: o.calls, amountCents: o.amountCents, amountDisplay: yuan(o.amountCents),
1418
+ status: o.status, createdAt: o.createdAt, paidAt: o.paidAt || null,
1419
+ payment: paymentInfo(orderId, o.amountCents),
1420
+ payIntent: payIntent(orderId, o.calls, o.amountCents)
1421
+ });
1422
+ }
1423
+
1424
+ // ---- 管理端点(必须带 X-Admin-Token;未配置 LS_ADMIN_TOKEN 时整体 503,fail-closed)----
1425
+ if (pathname.indexOf('/admin/') === 0) {
1426
+ const g = adminGuard(req);
1427
+ if (!g.ok) return sendJson(res, g.code, { error: { type: g.type || 'forbidden', message: g.msg } });
1428
+
1429
+ // POST /admin/order/confirm:确认到账 → 给该订单的 key 入账
1430
+ if (req.method === 'POST' && pathname === '/admin/order/confirm') {
1431
+ return readBody(req, ({ body, err }) => {
1432
+ if (err) return sendJson(res, 400, { error: { type: err, message: '请求体不合法。' } });
1433
+ const orderId = String((body && body.orderId) || '').trim();
1434
+ const o = getOrder(orderId);
1435
+ if (!o) return sendJson(res, 404, { error: { type: 'order_not_found', message: '订单不存在:' + orderId } });
1436
+
1437
+ // ── 对公收款 人工确认路径:仍强制核对金额(fail-closed)────────────
1438
+ // 本端点用于:① 走对公静态收款码/转账的老通道,作者拿流水按订单号折算入账;
1439
+ // ② 金额不符/订单丢失时的兜底人工处置。正常到账由作者跑 reconcile-bank.js 批量入账。
1440
+ const received = (body && body.receivedCents !== undefined) ? body.receivedCents : undefined;
1441
+ if (received === undefined && !(body && body.acknowledgeUnverified === true)) {
1442
+ return sendJson(res, 400, {
1443
+ error: {
1444
+ type: 'amount_not_verified',
1445
+ message: '拒绝在未核对金额的情况下入账:请传 receivedCents(银行流水里实际到账的分数)。',
1446
+ orderId: orderId,
1447
+ expectedCents: o.amountCents,
1448
+ expectedDisplay: yuan(o.amountCents),
1449
+ whyItMatters: '付款链接里的 amount 参数是客户端可改的;不核对金额 = 1 分钱的转账可以领走整单额度。',
1450
+ alternative: '对公收款:走「对公静态收款 + 银行流水对账」——付款后由作者按订单号批量折算入账(按 ' + yuan(PRICE_CENTS) + '/次),无需平台回调。',
1451
+ orPass: 'acknowledgeUnverified:true(表示你已自行核对,风险自担)'
1452
+ }
1453
+ });
1454
+ }
1455
+ const r = creditOrder(orderId, {
1456
+ receivedCents: received,
1457
+ // 默认:传了 receivedCents 就要求「逐分相等」(防少付多领);
1458
+ // 但调用方(如 reconcile-bank.js 的对公静态折算)可显式传 expectExactAmount:false,
1459
+ // 表示「按实收金额折算入账,不要求恰为订单面值」——此处尊重该显式开关。
1460
+ expectExactAmount: (body && body.expectExactAmount !== undefined) ? !!body.expectExactAmount : (received !== undefined),
1461
+ channel: (body && body.channel) || null,
1462
+ txRef: (body && body.txRef) || null,
1463
+ source: 'admin'
1464
+ });
1465
+ if (!r.ok) {
1466
+ if (r.reason === 'amount_mismatch') {
1467
+ return sendJson(res, 409, {
1468
+ error: {
1469
+ type: 'amount_mismatch',
1470
+ message: '实收金额与订单金额不符,已拒绝入账(防「少付多领」)。',
1471
+ orderId: orderId,
1472
+ expectedCents: r.expectedCents, expectedDisplay: r.expectedDisplay,
1473
+ receivedCents: Number.isInteger(r.receivedCents) ? r.receivedCents : null,
1474
+ receivedDisplay: r.receivedDisplay || null,
1475
+ outOfRange: !Number.isInteger(r.receivedCents) ? 'receivedCents 必须是整数(单位:分)' : null,
1476
+ howToProceed: [
1477
+ '核对银行流水后重试,传正确的 receivedCents',
1478
+ '若确实要按实收金额入账(部分到账 / 善意补偿 / 客户讨价),改用 POST /admin/credit {"orderId":"…","cents":<分>,"reason":"…"}',
1479
+ '若确认要按整单入账,传 acknowledgeUnverified:true'
1480
+ ]
1481
+ }
1482
+ });
1483
+ }
1484
+ return sendJson(res, r.reason === 'order_not_found' ? 404 : 500, {
1485
+ error: { type: r.reason, message: '入账未完成:' + r.reason + '(订单 ' + orderId + ')' }
1486
+ });
1487
+ }
1488
+ return sendJson(res, 200, {
1489
+ orderId: orderId, status: 'paid', alreadyPaid: r.alreadyPaid, key: r.keyMask,
1490
+ creditedCents: r.creditedCents, balanceCents: r.balanceCents, callsRemaining: r.callsRemaining,
1491
+ amountVerified: received !== undefined,
1492
+ amountNotice: r.alreadyPaid
1493
+ ? '该订单此前已入账,未重复入账。'
1494
+ : (received !== undefined
1495
+ ? ('已核对实收 ' + yuan(received) + ',与订单金额一致。')
1496
+ : '⚠️ 未核对金额即入账(acknowledgeUnverified)——请自行留存银行流水凭证,以便日后对账。')
1497
+ });
1498
+ });
1499
+ }
1500
+
1501
+ // POST /admin/credit:手工入账(送额度/线下转账/补偿)—— 目标可用 keyHash / apiKey / orderId 指定
1502
+ if (req.method === 'POST' && pathname === '/admin/credit') {
1503
+ return readBody(req, ({ body, err }) => {
1504
+ if (err) return sendJson(res, 400, { error: { type: err, message: '请求体不合法。' } });
1505
+ const cents = body && body.cents;
1506
+ if (typeof cents !== 'number' || Math.floor(cents) !== cents || cents === 0) {
1507
+ return sendJson(res, 400, { error: { type: 'invalid_cents', message: 'cents 必须是非零整数(可为负以扣减)。' } });
1508
+ }
1509
+ let keyHash = null;
1510
+ if (body.apiKey) keyHash = hashKey(String(body.apiKey).trim());
1511
+ else if (body.keyHash) keyHash = String(body.keyHash).trim();
1512
+ else if (body.orderId) {
1513
+ const o = getOrder(String(body.orderId).trim());
1514
+ if (!o) return sendJson(res, 404, { error: { type: 'order_not_found', message: '订单不存在。' } });
1515
+ keyHash = o.keyHash;
1516
+ }
1517
+ const k = keyHash ? ledger.keys[keyHash] : null;
1518
+ const reason = String((body && body.reason) || 'manual');
1519
+ if (!k) {
1520
+ // 无 key 时按「新发一张凭证」处理(管理员送额度给某人)
1521
+ if (!body.issueNew) {
1522
+ return sendJson(res, 404, { error: { type: 'key_not_found', message: '未找到该凭证。若需新发一张,请传 issueNew:true。' } });
1523
+ }
1524
+ const nk = newKey();
1525
+ const nh = hashKey(nk);
1526
+ ledger.keys[nh] = {
1527
+ mask: maskKey(nk), balanceCents: cents, createdAt: new Date().toISOString(),
1528
+ lastUsedAt: null, calls: 0, spentCents: 0, disabled: false, note: reason
1529
+ };
1530
+ saveLedger();
1531
+ ledgerAudit({ event: 'key_issued', key: maskKey(nk), cents: cents, reason: reason });
1532
+ return sendJson(res, 201, { apiKey: nk, apiKeyNotice: '此 key 仅出现一次,请立即保存。', balanceCents: cents, callsRemaining: Math.floor(cents / PRICE_CENTS), note: reason });
1533
+ }
1534
+ k.balanceCents += cents;
1535
+ k.note = (k.note ? k.note + ' | ' : '') + reason;
1536
+ saveLedger();
1537
+ ledgerAudit({ event: 'admin_credit', key: k.mask, cents: cents, balanceCents: k.balanceCents, reason: reason });
1538
+ return sendJson(res, 200, { key: k.mask, creditedCents: cents, balanceCents: k.balanceCents, callsRemaining: Math.floor(k.balanceCents / PRICE_CENTS) });
1539
+ });
1540
+ }
1541
+
1542
+ // POST /admin/key/disable:停用凭证(盗用/退款)
1543
+ if (req.method === 'POST' && pathname === '/admin/key/disable') {
1544
+ return readBody(req, ({ body, err }) => {
1545
+ if (err) return sendJson(res, 400, { error: { type: err, message: '请求体不合法。' } });
1546
+ const keyHash = body.apiKey ? hashKey(String(body.apiKey).trim()) : String((body && body.keyHash) || '').trim();
1547
+ const k = ledger.keys[keyHash];
1548
+ if (!k) return sendJson(res, 404, { error: { type: 'key_not_found', message: '未找到该凭证。' } });
1549
+ k.disabled = !!body.disabled;
1550
+ k.note = (k.note ? k.note + ' | ' : '') + (k.disabled ? 'disabled by admin' : 'enabled by admin');
1551
+ saveLedger();
1552
+ ledgerAudit({ event: k.disabled ? 'key_disabled' : 'key_enabled', key: k.mask });
1553
+ return sendJson(res, 200, { key: k.mask, disabled: k.disabled, balanceCents: k.balanceCents });
1554
+ });
1555
+ }
1556
+
1557
+ // POST /admin/rotate:凭证遗失重置 —— 新 key 继承旧 key 余额(旧 key 立即失效)
1558
+ if (req.method === 'POST' && pathname === '/admin/rotate') {
1559
+ return readBody(req, ({ body, err }) => {
1560
+ if (err) return sendJson(res, 400, { error: { type: err, message: '请求体不合法。' } });
1561
+ let keyHash = null;
1562
+ if (body.orderId) {
1563
+ const o = getOrder(String(body.orderId).trim());
1564
+ if (!o) return sendJson(res, 404, { error: { type: 'order_not_found', message: '订单不存在。' } });
1565
+ keyHash = o.keyHash;
1566
+ } else if (body.apiKey) keyHash = hashKey(String(body.apiKey).trim());
1567
+ else if (body.keyHash) keyHash = String(body.keyHash).trim();
1568
+ const old = ledger.keys[keyHash];
1569
+ if (!old) return sendJson(res, 404, { error: { type: 'key_not_found', message: '未找到旧凭证。' } });
1570
+ const nk = newKey();
1571
+ const nh = hashKey(nk);
1572
+ ledger.keys[nh] = {
1573
+ mask: maskKey(nk), balanceCents: old.balanceCents, createdAt: new Date().toISOString(),
1574
+ lastUsedAt: null, calls: old.calls || 0, spentCents: old.spentCents || 0, disabled: false,
1575
+ note: 'rotate from ' + old.mask + (old.note ? ' | ' + old.note : '')
1576
+ };
1577
+ delete ledger.keys[keyHash];
1578
+ if (body.orderId) {
1579
+ // 用 getOrder 而非直接索引:非法订单号/不存在的单不该 500,且防原型链键名。
1580
+ const o = getOrder(String(body.orderId).trim());
1581
+ if (o) o.keyHash = nh;
1582
+ }
1583
+ saveLedger();
1584
+ ledgerAudit({ event: 'key_rotated', from: old.mask, to: maskKey(nk), balanceCents: old.balanceCents });
1585
+ return sendJson(res, 200, { apiKey: nk, apiKeyNotice: '此 key 仅出现一次,请立即保存。', oldKeyMask: old.mask, balanceCents: old.balanceCents, callsRemaining: Math.floor(old.balanceCents / PRICE_CENTS) });
1586
+ });
1587
+ }
1588
+
1589
+ // GET /admin/ledger:只读台账(key 一律脱敏)
1590
+ if (req.method === 'GET' && pathname === '/admin/ledger') {
1591
+ const keys = Object.entries(ledger.keys).map(([h, k]) => ({
1592
+ keyHash: h.slice(0, 12) + '…', mask: k.mask, balanceCents: k.balanceCents,
1593
+ calls: k.calls || 0, spentCents: k.spentCents || 0,
1594
+ disabled: !!k.disabled, createdAt: k.createdAt, lastUsedAt: k.lastUsedAt || null, note: k.note || null
1595
+ }));
1596
+ const orders = Object.entries(ledger.orders).map(([id, o]) => ({
1597
+ orderId: id, calls: o.calls, amountCents: o.amountCents, status: o.status,
1598
+ createdAt: o.createdAt, paidAt: o.paidAt || null, channel: o.channel || null, txRef: o.txRef || null,
1599
+ creditedBy: o.creditedBy || null, amountVerified: o.amountVerified === true
1600
+ }));
1601
+ const totalBalance = keys.reduce((s, k) => s + k.balanceCents, 0);
1602
+ const totalSpent = keys.reduce((s, k) => s + k.spentCents, 0);
1603
+ const paidOrders = orders.filter(o => o.status === 'paid');
1604
+ // ⭐ 营收要分两栏,不能混:① 已核实(银行流水核对过)② 凭声明(信任制自助入账)。
1605
+ // 混在一起就会把「声明」当收入,是自欺;用户对虚报数字零容忍。
1606
+ const verified = paidOrders.filter(o => o.creditedBy === 'reconcile' || o.creditedBy === 'admin');
1607
+ const selfReported = paidOrders.filter(o => o.creditedBy === 'self_report');
1608
+ const sumCents = (arr) => arr.reduce((s, o) => s + o.amountCents, 0);
1609
+ return sendJson(res, 200, {
1610
+ metering: METERING_ON ? 'on' : 'off', priceCentsPerCall: PRICE_CENTS,
1611
+ summary: {
1612
+ keyCount: keys.length,
1613
+ totalBalanceCents: totalBalance, totalBalanceDisplay: yuan(totalBalance),
1614
+ totalSpentCents: totalSpent, totalSpentDisplay: yuan(totalSpent),
1615
+ totalCalls: keys.reduce((s, k) => s + k.calls, 0),
1616
+ honorClaims: honorClaims,
1617
+ selfReportClaims: selfReportClaims,
1618
+ orderCount: orders.length, paidOrderCount: paidOrders.length,
1619
+ paidRevenueCents: sumCents(paidOrders),
1620
+ paidRevenueDisplay: yuan(sumCents(paidOrders)),
1621
+ verifiedRevenueCents: sumCents(verified), verifiedRevenueDisplay: yuan(sumCents(verified)), verifiedOrderCount: verified.length,
1622
+ selfReportedCents: sumCents(selfReported), selfReportedDisplay: yuan(sumCents(selfReported)), selfReportedOrderCount: selfReported.length,
1623
+ revenueHonesty: '真营收 = verifiedRevenueCents(对公流水核对过的单)。selfReportedCents 与 honorClaims 是凭声明发的,不计入营收。'
1624
+ },
1625
+ payment: paymentInfo(), keys: keys, orders: orders
1626
+ });
1627
+ }
1628
+ return sendJson(res, 404, { error: { type: 'not_found', message: '未知管理端点。', available: ['POST /admin/order/confirm', 'POST /admin/credit', 'POST /admin/key/disable', 'POST /admin/rotate', 'GET /admin/ledger'] } });
1629
+ }
1630
+
349
1631
  // MCP 端点:Streamable HTTP(POST 请求 / GET 收 SSE / DELETE 终止会话)
350
- if (req.url === '/mcp') {
1632
+ if (pathname === '/mcp') {
351
1633
  // GET:打开 SSE 流,接收服务端→客户端通知(MCP Streamable HTTP 规范)
352
1634
  if (req.method === 'GET') {
353
1635
  const sessionId = req.headers['mcp-session-id'] || crypto.randomUUID();
@@ -375,7 +1657,6 @@ const server = http.createServer((req, res) => {
375
1657
  }
376
1658
  // POST:JSON-RPC 请求
377
1659
  if (req.method === 'POST') {
378
- const ip = (req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown').toString().split(',')[0].trim();
379
1660
  // 限速(安全等保加固):超限直接 429,不读不处理
380
1661
  if (!rateAllowed(ip)) {
381
1662
  appendLog({ ts: new Date().toISOString(), ip: ip, event: 'rate_limited' });
@@ -416,7 +1697,7 @@ const server = http.createServer((req, res) => {
416
1697
  const responses = [];
417
1698
  for (const m of batch) {
418
1699
  if (m && m.jsonrpc === '2.0') {
419
- const r = handleRpc(m, ip);
1700
+ const r = handleRpc(m, ip, ctx);
420
1701
  if (r !== null) responses.push(r);
421
1702
  }
422
1703
  }
@@ -441,9 +1722,31 @@ const server = http.createServer((req, res) => {
441
1722
  res.end('Not Found. MCP endpoint: POST /mcp');
442
1723
  });
443
1724
 
1725
+ // 账本在启动时加载(不在模块顶层读盘,便于测试脚本先设 LS_CREDITS_PATH 再 require)
1726
+ loadLedger();
1727
+
444
1728
  server.listen(PORT, '0.0.0.0', () => {
445
- appendLog({ ts: new Date().toISOString(), event: 'server_start', name: SERVER_NAME, version: SERVER_VERSION, port: PORT });
1729
+ appendLog({
1730
+ ts: new Date().toISOString(), event: 'server_start', name: SERVER_NAME, version: SERVER_VERSION, port: PORT,
1731
+ metering: METERING_ON ? 'on' : 'off', priceCents: METERING_ON ? PRICE_CENTS : 0,
1732
+ adminEnabled: !!ADMIN_TOKEN, payConfigured: !!PAY_TO,
1733
+ receiptPinHash: RECEIPT_PIN_HASH, receiptTampered: RECEIPT_TAMPERED,
1734
+ keyCount: Object.keys(ledger.keys).length, orderCount: Object.keys(ledger.orders).length
1735
+ });
446
1736
  console.log(`[${SERVER_NAME}] HTTP MCP server listening on 0.0.0.0:${PORT}`);
447
1737
  console.log(` health : GET http://localhost:${PORT}/health`);
448
1738
  console.log(` mcp : POST http://localhost:${PORT}/mcp`);
1739
+ console.log(` metering: ${METERING_ON ? 'ON ' + PRICE_CENTS + ' 分/次' : 'off (免费)'}`);
1740
+ console.log(` tls : ${REQUIRE_TLS ? 'REQUIRED for credentialed calls' : 'not enforced (凭证可走明文,迁移期)'}`);
1741
+ console.log(` account : not required (key = account); collects no personal data`);
1742
+ console.log(` pricing : GET http://localhost:${PORT}/pricing`);
1743
+ console.log(` pay : 对公收款(corporate-static,对公静态收款 + 流水对账,不接任何支付平台商户 API)`);
1744
+ console.log(` admin : ${ADMIN_TOKEN ? 'enabled' : 'DISABLED (未设 LS_ADMIN_TOKEN)'}${ADMIN_LOOPBACK_ONLY ? ' [loopback-only]' : ' [reachable remotely]'} | payTo: ${PAY_TO ? 'configured' : 'NOT configured (未设 LS_PAY_TO)'}`);
1745
+ console.log(` receipt : pinned=${PINNED_ACCOUNT} tamper=${RECEIPT_TAMPERED ? 'YES ⚠ 收款去向被篡改,已 fail-closed 拒绝收款' : 'OK (verified)'}${RECEIPT_TAMPERED ? ' problems=' + GUARD_PROBLEMS.join(' | ') : ''}`);
1746
+ if (RECEIPT_TAMPERED) {
1747
+ console.warn('[SECURITY] 收款去向与钉死值不符,已 fail-closed:不再生成任何付款意图。问题:' + GUARD_PROBLEMS.join(' | '));
1748
+ }
1749
+ if (METERING_ON && !PAY_TO) {
1750
+ console.warn('[warn] 计费已开但未配置 LS_PAY_TO:订单可创建却无法付款。');
1751
+ }
449
1752
  });