Skip to content

Commit 969fd5c

Browse files
author
Taois
committed
feat: 重构Hipy逻辑
1 parent a8b12e4 commit 969fd5c

File tree

7 files changed

+744
-4
lines changed

7 files changed

+744
-4
lines changed

.env.development

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,6 @@ QQ_EMAIL =
3333
QQ_SMTP_AUTH_CODE =
3434

3535
# 调试猫源-推荐开启
36-
CAT_DEBUG=1
36+
CAT_DEBUG=1
37+
PYTHON_PATH=
38+
VIRTUAL_ENV=

controllers/root.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import {readdirSync, readFileSync, writeFileSync, existsSync} from 'fs';
33
import '../utils/marked.min.js';
44
import {computeHash} from '../utils/utils.js';
55
import {validateBasicAuth} from "../utils/api_validate.js";
6+
import {daemon} from "../utils/daemonManager.js";
7+
import {toBeijingTime} from "../utils/datetime-format.js"
68

79
export default (fastify, options, done) => {
810
// 添加 / 接口
@@ -108,5 +110,17 @@ export default (fastify, options, done) => {
108110
reply.status(500).send({error: 'Failed to fetch cat', details: error.message});
109111
}
110112
});
113+
114+
// 健康检查端点
115+
fastify.get('/health', async (request, reply) => {
116+
return {
117+
status: 'ok',
118+
timestamp: toBeijingTime(new Date()),
119+
python: {
120+
available: await daemon.isPythonAvailable(),
121+
daemon_running: daemon.isDaemonRunning()
122+
}
123+
};
124+
});
111125
done();
112126
};

index.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import formBody from '@fastify/formbody';
88
import {validateBasicAuth, validateJs, validatePwd} from "./utils/api_validate.js";
99
// 注册自定义import钩子
1010
import './utils/esm-register.mjs';
11+
// 引入python守护进程
12+
import {daemon} from "./utils/daemonManager.js";
1113
// 注册控制器
1214
import {registerRoutes} from './controllers/index.js';
1315

@@ -72,6 +74,26 @@ fastify.register(fastifyStatic, {
7274
// 注册插件以支持 application/x-www-form-urlencoded
7375
fastify.register(formBody);
7476

77+
fastify.addHook('onReady', async () => {
78+
try {
79+
await daemon.startDaemon();
80+
fastify.log.info('Python守护进程已启动');
81+
} catch (error) {
82+
fastify.log.error(`启动Python守护进程失败: ${error.message}`);
83+
fastify.log.error('Python相关功能将不可用');
84+
}
85+
});
86+
87+
// 停止时清理守护进程
88+
fastify.addHook('onClose', async () => {
89+
try {
90+
await daemon.stopDaemon();
91+
fastify.log.info('Python守护进程已停止');
92+
} catch (error) {
93+
fastify.log.error(`停止Python守护进程失败: ${error.message}`);
94+
}
95+
});
96+
7597
// 给静态目录插件中心挂载basic验证
7698
fastify.addHook('preHandler', (req, reply, done) => {
7799
if (req.raw.url.startsWith('/apps/')) {

libs/hipy.js

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {fileURLToPath} from 'url';
66
import {md5} from "../libs_drpy/crypto-util.js";
77
import {PythonShell, PythonShellError} from 'python-shell';
88
import {fastify} from "../controllers/fastlogger.js";
9+
import {daemon} from "../utils/daemonManager.js";
910

1011
// 缓存已初始化的模块和文件 hash 值
1112
const moduleCache = new Map();
@@ -32,7 +33,7 @@ const loadEsmWithHash = async function (filePath, fileHash, env) {
3233
const bridgePath = path.join(_lib_path, '_bridge.py'); // 桥接脚本路径
3334

3435
// 创建方法调用函数
35-
const callPythonMethod = async (methodName, env, ...args) => {
36+
const callPythonMethodOld = async (methodName, env, ...args) => {
3637

3738
const options = {
3839
mode: 'text', // 使用JSON模式自动解析
@@ -98,6 +99,69 @@ const loadEsmWithHash = async function (filePath, fileHash, env) {
9899
}
99100
};
100101

102+
const callPythonMethod = async (methodName, env, ...args) => {
103+
const config = daemon.getDaemonConfig();
104+
const command = [
105+
`"${daemon.getPythonPath()}"`,
106+
`"${config.clientScript}"`,
107+
`--script-path "${filePath}"`,
108+
`--method-name "${methodName}"`,
109+
`--env '${JSON.stringify(env)}'`,
110+
...args.map(arg => `--arg '${JSON.stringify(arg)}'`)
111+
].join(' ');
112+
// console.log(command);
113+
const cmd_args = [];
114+
args.forEach(arg => {
115+
cmd_args.push(`--arg`);
116+
cmd_args.push(`${JSON.stringify(arg)}`);
117+
});
118+
const options = {
119+
mode: 'text',
120+
pythonPath: daemon.getPythonPath(),
121+
pythonOptions: ['-u'], // 无缓冲输出
122+
env: {
123+
"PYTHONIOENCODING": 'utf-8',
124+
},
125+
args: [
126+
'--script-path', filePath,
127+
'--method-name', methodName,
128+
'--env', JSON.stringify(env),
129+
...cmd_args
130+
]
131+
};
132+
const results = await PythonShell.run(config.clientScript, {
133+
...options,
134+
});
135+
// 取最后一条返回
136+
const stdout = results.slice(-1)[0];
137+
fastify.log.info(`hipy logs: ${JSON.stringify(results.slice(0, -1))}`);
138+
// console.log(`hipy logs: ${JSON.stringify(results.slice(0, -1))}`);
139+
let vodResult = {};
140+
if (typeof stdout === 'string' && stdout) {
141+
switch (stdout) {
142+
case 'None':
143+
vodResult = null;
144+
break;
145+
case 'True':
146+
vodResult = true;
147+
break;
148+
case 'False':
149+
vodResult = false;
150+
break;
151+
default:
152+
vodResult = JSON5.parse(stdout);
153+
break;
154+
}
155+
}
156+
// console.log(typeof vodResult);
157+
// 检查是否有错误
158+
if (vodResult && vodResult.error) {
159+
throw new Error(`Python错误: ${vodResult.error}\n${vodResult.traceback}`);
160+
}
161+
// console.log(vodResult);
162+
return vodResult.result;
163+
}
164+
101165
// 定义Spider类的方法
102166
const spiderMethods = [
103167
'init', 'home', 'homeVod', 'homeContent', 'category',
@@ -162,12 +226,13 @@ const init = async function (filePath, env = {}, refresh) {
162226
module = await loadEsmWithHash(filePath, fileHash, env);
163227
// console.log('module:', module);
164228
const rule = module;
165-
await rule.init(default_init_cfg);
229+
const initValue = await rule.init(default_init_cfg) || {};
166230
let t2 = getNowTime();
167231
const moduleObject = deepCopy(rule);
168232
moduleObject.cost = t2 - t1;
169233
moduleCache.set(hashMd5, {moduleObject, hash: fileHash, proxyUrl: env.proxyUrl});
170-
return moduleObject;
234+
// return moduleObject;
235+
return {...moduleObject, ...initValue};
171236
} catch (error) {
172237
console.log(`Error in hipy.init :${filePath}`, error);
173238
throw new Error(`Failed to initialize module:${error.message}`);

spider/py/core/bridge.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
import argparse
5+
import json
6+
import pickle
7+
import socket
8+
import struct
9+
import sys
10+
11+
HOST = "127.0.0.1"
12+
PORT = 57570
13+
MAX_MSG_SIZE = 10 * 1024 * 1024
14+
TIMEOUT = 30
15+
16+
def send_packet(sock, obj: dict):
17+
payload = pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL)
18+
if len(payload) > MAX_MSG_SIZE:
19+
raise ValueError("payload too large")
20+
sock.sendall(struct.pack(">I", len(payload)))
21+
sock.sendall(payload)
22+
23+
def recv_exact(sock, n: int) -> bytes:
24+
data = b""
25+
while len(data) < n:
26+
chunk = sock.recv(n - len(data))
27+
if not chunk:
28+
raise ConnectionError("peer closed")
29+
data += chunk
30+
return data
31+
32+
def recv_packet(sock) -> dict:
33+
header = recv_exact(sock, 4)
34+
(length,) = struct.unpack(">I", header)
35+
if length <= 0 or length > MAX_MSG_SIZE:
36+
raise ValueError("invalid length")
37+
payload = recv_exact(sock, length)
38+
return pickle.loads(payload)
39+
40+
def main():
41+
p = argparse.ArgumentParser(description="T4 CLI bridge")
42+
p.add_argument("--script-path", required=True, help="Spider脚本路径或模块名")
43+
p.add_argument("--method-name", required=True, help="要调用的方法名")
44+
p.add_argument("--env", default="", help="JSON字符串(可包含 proxyUrl/ext)或普通字符串")
45+
p.add_argument("--arg", action="append", default=[], help="方法参数;可多次传入。每个参数若可解析为JSON则按JSON,否则按字符串")
46+
p.add_argument("--host", default=HOST, help="守护进程主机(默认127.0.0.1)")
47+
p.add_argument("--port", type=int, default=PORT, help="守护进程端口(默认57570)")
48+
p.add_argument("--timeout", type=int, default=TIMEOUT, help="超时秒数(默认30)")
49+
args = p.parse_args()
50+
51+
req = {
52+
"script_path": args.script_path,
53+
"method_name": args.method_name,
54+
"env": args.env,
55+
"args": args.arg,
56+
}
57+
58+
try:
59+
with socket.create_connection((args.host, args.port), timeout=args.timeout) as s:
60+
s.settimeout(args.timeout)
61+
send_packet(s, req)
62+
resp = recv_packet(s)
63+
except Exception as e:
64+
print(json.dumps({"success": False, "error": str(e)}, ensure_ascii=False))
65+
sys.exit(2)
66+
67+
# 统一以 JSON 打印到 stdout,便于脚本链路消费
68+
print(json.dumps(resp, ensure_ascii=False))
69+
# 非0退出码用于指示错误,方便shell判断
70+
sys.exit(0 if resp.get("success") else 1)
71+
72+
if __name__ == "__main__":
73+
main()

0 commit comments

Comments
 (0)