前人工作区变更归档(元审计遗留处置·冰朔授权'交给你来处理'·不否定过去原则):hololake-phase-b-staging删除组/WORK区/submodule指针/0826壳源/neural-chain/skills-mirror/codex恢复帧/.zcode本地配置等140项原样冻结入库——不删除不修改不评判,作为历史一环保留;WX-SECRETS回执经查仅含公开AppID无AppSecret,安全
This commit is contained in:
parent
28480f466c
commit
a40f29c12e
2442 changed files with 596545 additions and 6601 deletions
|
|
@ -0,0 +1,81 @@
|
|||
// 铸渊专线订阅服务 · 流量计数 + subscription-userinfo 头
|
||||
// 部署:BS-GZ-006 · 2026-08-23 · 配额 1000GiB/自然月重置
|
||||
// 计数源:iptables 计数规则(relay 端口 8443-8446 进出字节)
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execFile } = require('child_process');
|
||||
|
||||
const PORT = 3944;
|
||||
const DATA_DIR = '/var/www/guanghulab-front-door/zy-sub';
|
||||
const STATE_FILE = '/opt/zy-sub-service/state.json';
|
||||
const TOTAL_QUOTA = 1000 * 1024 * 1024 * 1024;
|
||||
|
||||
let state = { month: '', upload: 0, download: 0, lastIn: 0, lastOut: 0 };
|
||||
try { Object.assign(state, JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'))); } catch {}
|
||||
|
||||
function monthKey(d = new Date()) {
|
||||
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0');
|
||||
}
|
||||
function save() {
|
||||
try { fs.writeFileSync(STATE_FILE, JSON.stringify(state)); }
|
||||
catch (e) { console.error('save fail:', e.message); }
|
||||
}
|
||||
|
||||
function readCounter(chain, tag, cb) {
|
||||
execFile('iptables-save', ['-c'], (err, out) => {
|
||||
let bytes = 0;
|
||||
if (!err) {
|
||||
for (const line of out.split('\n')) {
|
||||
if (line.includes('zy-sub-acct-' + tag) && line.includes('-A ' + chain + ' ')) {
|
||||
const m = line.match(/^\[(\d+):(\d+)\]/);
|
||||
if (m) bytes += parseInt(m[2], 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
cb(bytes);
|
||||
});
|
||||
}
|
||||
|
||||
function poll() {
|
||||
readCounter('INPUT', 'in', (inB) => {
|
||||
readCounter('OUTPUT', 'out', (outB) => {
|
||||
const mk = monthKey();
|
||||
if (state.month !== mk) { state.month = mk; state.upload = 0; state.download = 0; }
|
||||
if (inB < state.lastIn) state.lastIn = inB; // 计数器重启归零时对齐
|
||||
if (outB < state.lastOut) state.lastOut = outB;
|
||||
state.upload += inB - state.lastIn;
|
||||
state.download += outB - state.lastOut;
|
||||
state.lastIn = inB; state.lastOut = outB;
|
||||
save();
|
||||
});
|
||||
});
|
||||
}
|
||||
setInterval(poll, 60 * 1000);
|
||||
poll();
|
||||
|
||||
const used = () => state.upload + state.download;
|
||||
const userinfo = () =>
|
||||
`upload=${state.upload}; download=${state.download}; total=${TOTAL_QUOTA}; expire=0`;
|
||||
|
||||
http.createServer((req, res) => {
|
||||
const url = req.url.split('?')[0];
|
||||
const m = url.match(/^\/zy-sub\/([0-9a-f]{16,64})\/(clash\.yaml|shadowrocket\.txt|status)$/);
|
||||
if (!m) { res.writeHead(404); return res.end('not found'); }
|
||||
if (m[2] === 'status') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
return res.end(JSON.stringify({
|
||||
month: state.month, upload: state.upload, download: state.download,
|
||||
total: TOTAL_QUOTA, used: used(), remaining: Math.max(0, TOTAL_QUOTA - used())
|
||||
}, null, 2));
|
||||
}
|
||||
fs.readFile(path.join(DATA_DIR, m[1], m[2]), (err, data) => {
|
||||
if (err) { res.writeHead(404); return res.end('not found'); }
|
||||
res.writeHead(200, {
|
||||
'Content-Type': m[2].endsWith('.yaml') ? 'text/yaml; charset=utf-8' : 'text/plain; charset=utf-8',
|
||||
'subscription-userinfo': userinfo(),
|
||||
'profile-update-interval': '24'
|
||||
});
|
||||
res.end(data);
|
||||
});
|
||||
}).listen(PORT, '127.0.0.1', () => console.log('zy-sub-service on', PORT));
|
||||
Loading…
Reference in a new issue