ソースを参照

防反编译打包

zhouyang.xie 1 ヶ月 前
コミット
a1954c1c85
10 ファイル変更1163 行追加1 行削除
  1. 50 0
      .gitignore
  2. 446 0
      build.py
  3. 46 0
      entry.py
  4. 137 0
      keygen.py
  5. 142 0
      license_checker.py
  6. 166 0
      node_caller/laser.js
  7. 64 0
      node_caller/laser_wrapper.py
  8. 52 0
      node_caller/vue3_example.js
  9. 53 1
      readme.md
  10. 7 0
      requirements.txt

+ 50 - 0
.gitignore

@@ -0,0 +1,50 @@
+# Autosave files
+*.asv
+*.m~
+*.autosave
+*.slx.r*
+*.mdl.r*
+
+# Derived content-obscured files
+*.p
+
+# Compiled MEX files
+*.mex*
+
+# Packaged app and toolbox files
+*.mlappinstall
+*.mltbx
+
+# Deployable archives
+*.ctf
+
+# Generated helpsearch folders
+helpsearch*/
+
+# Code generation folders
+slprj/
+sccprj/
+codegen/
+
+# Cache files
+*.slxc
+**/*.pyc
+**/*.pyd
+**/*.c
+
+# Cloud based storage dotfile
+.MATLABDriveTag
+
+**/conf/
+**/target/
+**/output/
+**/testData/
+**/testOutput/
+**/source_code_original/
+**/build/
+**/dist/
+**/log/
+
+**/*.egg-info/
+**/__pycache__/
+**/*venv/

+ 446 - 0
build.py

@@ -0,0 +1,446 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+laser_cal 项目构建脚本
+集成 CodeEnigma 混淆 + Nuitka 编译
+
+支持三种编译模式:
+    python build.py --module            # 编译为共享库 (.so / .pyd)
+    python build.py --exe               # 编译为 standalone 可执行程序目录(推荐)
+    python build.py --onefile           # 编译为单个可执行文件
+
+其他选项:
+    python build.py --skip-obfuscate    # 跳过 CodeEnigma 混淆步骤
+    python build.py --no-license        # 不使用 license 文件授权(打包为无授权版本)
+    python build.py --clean             # 清理构建缓存
+"""
+
+import os
+import sys
+import shutil
+import subprocess
+import argparse
+import platform
+import tempfile
+from pathlib import Path
+
+BASE_DIR = Path(__file__).parent.absolute()
+DIST_DIR = BASE_DIR / "dist" / "program"
+LICENSE_DIR = BASE_DIR / "dist" / "license"
+OBFUSCATED_DIR = BASE_DIR / "dist" / "obfuscated"
+BUILD_SRC_DIR = BASE_DIR / "dist" / "_build_src"
+
+PROJECT_NAME = "laser_cal"
+
+FILES_TO_OBFUSCATE = ["data_clean.py", "frequency_filter.py"]
+LOCAL_MODULES = ["license_checker", "data_clean", "frequency_filter", "entry"]
+PACKAGES_TO_INCLUDE = ["pandas", "numpy", "scipy", "cryptography"]
+
+def log(msg, level="INFO"):
+    print(f"[{level}] {msg}")
+
+def run_cmd(cmd, cwd=None, env=None, allow_fail=False):
+    log(f"执行: {' '.join(str(c) for c in cmd)}")
+    result = subprocess.run([str(c) for c in cmd], cwd=str(cwd or BASE_DIR), env=env)
+    if result.returncode != 0 and not allow_fail:
+        log(f"命令失败,退出码: {result.returncode}", "ERROR")
+        sys.exit(1)
+    return result
+
+def ensure_nuitka():
+    try:
+        subprocess.run([sys.executable, "-m", "nuitka", "--version"],
+                       capture_output=True, check=True)
+    except (FileNotFoundError, subprocess.CalledProcessError):
+        log("安装 Nuitka...")
+        run_cmd([sys.executable, "-m", "pip", "install",
+                 "nuitka", "ordered-set", "zstandard", "--quiet"])
+
+# ============================================================
+# 步骤 1: CodeEnigma 混淆(修复版:混淆失败不中断构建)
+# ============================================================
+
+def step_obfuscate():
+    log("=" * 60)
+    log("步骤 1: CodeEnigma 代码混淆")
+    log("=" * 60)
+
+    # 确保 codeenigma 已安装
+    try:
+        subprocess.run(["codeenigma", "version"], capture_output=True, check=True)
+    except (FileNotFoundError, subprocess.CalledProcessError):
+        log("安装 codeenigma...")
+        run_cmd([sys.executable, "-m", "pip", "install", "codeenigma", "--quiet"])
+
+    if OBFUSCATED_DIR.exists():
+        shutil.rmtree(OBFUSCATED_DIR)
+    OBFUSCATED_DIR.mkdir(parents=True)
+
+    with tempfile.TemporaryDirectory(dir=BASE_DIR / "dist", prefix="_tmp_mod_") as tmp_dir:
+        tmp_path = Path(tmp_dir)
+        for py_file in FILES_TO_OBFUSCATE:
+            src = BASE_DIR / py_file
+            if src.exists():
+                shutil.copy2(src, tmp_path / py_file)
+                log(f"  准备混淆: {py_file}")
+            else:
+                log(f"  文件不存在: {py_file}", "WARN")
+        (tmp_path / "__init__.py").touch()
+
+        # 执行混淆(允许失败)
+        result = run_cmd(
+            ["codeenigma", "obfuscate", str(tmp_path), "-o", str(OBFUSCATED_DIR)],
+            allow_fail=True
+        )
+
+        if result.returncode != 0:
+            log("CodeEnigma 报告错误,尝试自动恢复...", "WARN")
+
+        # 1. 尝试从子目录恢复混淆后的 .py 文件
+        for py_file in FILES_TO_OBFUSCATE:
+            target = OBFUSCATED_DIR / py_file
+            if not target.exists():
+                found = list(OBFUSCATED_DIR.rglob(py_file))
+                if found:
+                    # 取第一个找到的文件(通常是 build 目录下的副本)
+                    shutil.copy2(found[0], target)
+                    log(f"  从 {found[0].relative_to(OBFUSCATED_DIR)} 恢复混淆文件: {py_file}")
+                else:
+                    log(f"  未找到混淆后的 {py_file},稍后将回退到原始文件", "WARN")
+
+        # 2. 恢复运行时库
+        runtime_pyd = None
+        search_dir = OBFUSCATED_DIR / "build"
+        if search_dir.exists():
+            if platform.system() == "Windows":
+                candidates = list(search_dir.glob("**/*.pyd"))
+            else:
+                candidates = list(search_dir.glob("**/*.so"))
+            candidates = [c for c in candidates if "codeenigma_runtime" in c.name]
+            if candidates:
+                runtime_pyd = candidates[0]
+                log(f"  找到运行时库: {runtime_pyd}")
+
+        runtime_dest_dir = OBFUSCATED_DIR / "codeenigma_runtime"
+        runtime_dest_dir.mkdir(exist_ok=True)
+        if runtime_pyd and runtime_pyd.exists():
+            dest = runtime_dest_dir / runtime_pyd.name
+            if not dest.exists():
+                shutil.copy2(runtime_pyd, dest)
+                log(f"  复制运行时库到: {dest}")
+        else:
+            log("  未找到编译后的运行时库,若程序依赖 codeenigma_runtime 将无法运行", "WARN")
+
+        # 检查核心混淆文件最终是否可用
+        core_ok = all((OBFUSCATED_DIR / f).exists() for f in FILES_TO_OBFUSCATE)
+        if not core_ok:
+            log("核心混淆文件最终仍缺失,构建将回退使用原始未混淆代码!", "ERROR")
+            # 不退出,继续构建
+        else:
+            log("核心混淆文件已就绪,混淆成功")
+
+# ============================================================
+# 步骤 2: 准备编译目录(支持 --no-license,自动回退原始文件)
+# ============================================================
+
+def _generate_fake_license_module():
+    sys.path.insert(0, str(BASE_DIR))
+    try:
+        import license_checker
+    except ImportError:
+        return "# Auto-generated fake license_checker\ndef check_license(*args, **kwargs): return True\n"
+
+    public_names = [name for name in dir(license_checker) if not name.startswith('_')]
+    if hasattr(license_checker, '__all__'):
+        public_names = list(license_checker.__all__)
+
+    lines = [
+        "# -*- coding: utf-8 -*-",
+        "# Auto-generated fake license_checker for no-license builds",
+        ""
+    ]
+    for name in public_names:
+        obj = getattr(license_checker, name, None)
+        if callable(obj):
+            lines.append(f"def {name}(*args, **kwargs):")
+            lines.append(f"    return True")
+            lines.append("")
+        else:
+            lines.append(f"{name} = None")
+            lines.append("")
+    sys.path.pop(0)
+    return "\n".join(lines)
+
+def step_prepare_build_dir(mode="module", no_license=False):
+    log("=" * 60)
+    log("步骤 2: 准备编译目录")
+    log("=" * 60)
+
+    if BUILD_SRC_DIR.exists():
+        shutil.rmtree(BUILD_SRC_DIR)
+    BUILD_SRC_DIR.mkdir(parents=True)
+
+    if mode == "module":
+        files_to_copy = ["entry.py", "license_checker.py"]
+    else:
+        files_to_copy = ["api_test.py", "license_checker.py", "entry.py"]
+
+    for f in files_to_copy:
+        src = BASE_DIR / f
+        if not src.exists():
+            continue
+        if f == "license_checker.py" and no_license:
+            log("  生成无授权模块: license_checker.py")
+            with open(BUILD_SRC_DIR / f, "w", encoding="utf-8") as fout:
+                fout.write(_generate_fake_license_module())
+        else:
+            shutil.copy2(src, BUILD_SRC_DIR / f)
+            log(f"  复制: {f}")
+
+    # 复制混淆后的文件(如果存在),否则回退原始文件
+    for py_file in FILES_TO_OBFUSCATE:
+        obf_file = OBFUSCATED_DIR / py_file
+        if obf_file.exists():
+            shutil.copy2(obf_file, BUILD_SRC_DIR / py_file)
+            log(f"  复制混淆文件: {py_file}")
+        else:
+            orig_file = BASE_DIR / py_file
+            if orig_file.exists():
+                shutil.copy2(orig_file, BUILD_SRC_DIR / py_file)
+                log(f"  混淆文件缺失,使用原始文件: {py_file}", "WARN")
+
+    # 复制运行时库(如果有)
+    runtime_dir = OBFUSCATED_DIR / "codeenigma_runtime"
+    if runtime_dir.exists() and any(runtime_dir.iterdir()):
+        shutil.copytree(runtime_dir, BUILD_SRC_DIR / "codeenigma_runtime")
+        log("  复制 codeenigma_runtime")
+    else:
+        log("  未找到 codeenigma_runtime,跳过复制", "WARN")
+
+    return BUILD_SRC_DIR
+
+# ============================================================
+# 步骤 3: Nuitka 编译
+# ============================================================
+
+def step_nuitka_module(build_src):
+    log("=" * 60)
+    log("步骤 3: Nuitka 编译为共享库 (.so / .pyd)")
+    log("=" * 60)
+
+    ensure_nuitka()
+
+    cmd = [
+        sys.executable, "-m", "nuitka",
+        "--module",
+        "--follow-import-to=license_checker",
+        "--follow-import-to=data_clean",
+        "--follow-import-to=frequency_filter",
+        "--follow-import-to=entry",
+        f"--output-dir={DIST_DIR}",
+        "--python-flag=no_docstrings",
+        "--python-flag=-O",
+        "--remove-output",
+        "--assume-yes-for-downloads",
+    ]
+
+    runtime_dir = build_src / "codeenigma_runtime"
+    if runtime_dir.exists():
+        for so_file in list(runtime_dir.glob("*.so")) + list(runtime_dir.glob("*.pyd")):
+            cmd.append(f"--include-data-files={so_file}=codeenigma_runtime/{so_file.name}")
+
+    cmd.append(str(build_src / "entry.py"))
+
+    env = os.environ.copy()
+    if platform.system() == "Linux":
+        env["CC"] = "gcc"
+
+    run_cmd(cmd, cwd=build_src, env=env)
+    log(f"共享库已生成到: {DIST_DIR}")
+
+def step_nuitka_exe(build_src, onefile=False):
+    mode_name = "单文件可执行程序" if onefile else "standalone 可执行程序"
+    log("=" * 60)
+    log(f"步骤 3: Nuitka 编译为{mode_name}")
+    log("=" * 60)
+
+    ensure_nuitka()
+    output_dir = DIST_DIR
+
+    cmd = [sys.executable, "-m", "nuitka"]
+    if onefile:
+        cmd.append("--onefile")
+    else:
+        cmd.append("--standalone")
+
+    cmd.extend([
+        "--follow-imports",
+        f"--output-dir={output_dir}",
+        "--python-flag=no_docstrings",
+        "--python-flag=-O",
+        "--remove-output",
+        "--assume-yes-for-downloads",
+    ])
+
+    for pkg in PACKAGES_TO_INCLUDE:
+        cmd.append(f"--include-package={pkg}")
+
+    cmd += [
+        "--include-module=license_checker",
+        "--include-module=data_clean",
+        "--include-module=frequency_filter",
+    ]
+
+    runtime_dir = build_src / "codeenigma_runtime"
+    if runtime_dir.exists():
+        cmd.append(f"--include-data-dir={runtime_dir}=codeenigma_runtime")
+
+    if platform.system() == "Windows":
+        cmd.append(f"--output-filename={PROJECT_NAME}.exe")
+    else:
+        cmd.append(f"--output-filename={PROJECT_NAME}")
+
+    cmd.append(str(build_src / "api_test.py"))
+
+    env = os.environ.copy()
+    if platform.system() == "Linux":
+        env["CC"] = "gcc"
+
+    run_cmd(cmd, cwd=build_src, env=env)
+
+    if onefile:
+        exe_name = PROJECT_NAME + ".exe" if platform.system() == "Windows" else PROJECT_NAME
+        log(f"可执行文件已生成: {output_dir / exe_name}")
+    else:
+        log(f"可执行程序目录已生成: {output_dir / 'api_test.dist'}")
+
+# ============================================================
+# 步骤 4: 复制 License(根据 --no-license 跳过)
+# ============================================================
+
+def step_copy_license(mode="module", onefile=False, no_license=False):
+    if no_license:
+        log("=" * 60)
+        log("步骤 4: 跳过 License 文件复制(无授权模式)")
+        log("=" * 60)
+        return
+
+    log("=" * 60)
+    log("步骤 4: 复制 License 文件")
+    log("=" * 60)
+
+    if not LICENSE_DIR.exists():
+        log("  License 目录不存在,跳过", "WARN")
+        return
+
+    if mode == "module":
+        target = DIST_DIR
+    elif onefile:
+        target = DIST_DIR
+    else:
+        target = DIST_DIR / "api_test.dist"
+
+    target.mkdir(parents=True, exist_ok=True)
+    for f in ["license.lic", "public_key.pem"]:
+        src = LICENSE_DIR / f
+        if src.exists():
+            shutil.copy2(src, target / f)
+            log(f"  复制: {f}")
+
+# ============================================================
+# 清理
+# ============================================================
+
+def clean():
+    log("清理构建缓存...")
+    for d in [DIST_DIR, OBFUSCATED_DIR, BUILD_SRC_DIR]:
+        if d.exists():
+            shutil.rmtree(d)
+            log(f"  删除: {d}")
+
+# ============================================================
+# 主函数
+# ============================================================
+
+def main():
+    parser = argparse.ArgumentParser(description="laser_cal 项目构建脚本 (CodeEnigma + Nuitka)",
+                                     formatter_class=argparse.RawDescriptionHelpFormatter,
+                                     epilog="""
+编译模式说明:
+  --module      编译为共享库 (.so/.pyd),需要 Python 环境 + Node.js wrapper 调用
+  --exe         编译为 standalone 可执行程序目录,Node.js 可直接 execFile 调用
+  --onefile     编译为单个可执行文件,最便于分发
+
+示例:
+  python build.py --exe                       混淆 + 编译为可执行程序
+  python build.py --exe --skip-obfuscate      仅编译为可执行程序
+  python build.py --exe --no-license          混淆 + 编译为无授权可执行程序
+  python build.py --module --skip-obfuscate   仅编译为共享库
+  python build.py --onefile                   混淆 + 编译为单文件
+  python build.py --clean                     清理构建缓存
+        """)
+
+    mode_group = parser.add_mutually_exclusive_group()
+    mode_group.add_argument("--module", action="store_true")
+    mode_group.add_argument("--exe", action="store_true")
+    mode_group.add_argument("--onefile", action="store_true")
+
+    parser.add_argument("--skip-obfuscate", action="store_true")
+    parser.add_argument("--no-license", action="store_true")
+    parser.add_argument("--clean", action="store_true")
+
+    args = parser.parse_args()
+
+    if args.clean:
+        clean()
+        return
+
+    if not args.module and not args.exe and not args.onefile:
+        args.module = True
+
+    mode = "module" if args.module else ("exe" if args.exe else "onefile")
+
+    log("=" * 60)
+    log(f"laser_cal 构建脚本")
+    log(f"  编译模式: {mode}")
+    log(f"  代码混淆: {'是' if not args.skip_obfuscate else '否'}")
+    log(f"  授权验证: {'否(无授权模式)' if args.no_license else '是(需 license 文件)'}")
+    log(f"  操作系统: {platform.system()} ({platform.machine()})")
+    log(f"  Python:   {sys.version.split()[0]}")
+    log("=" * 60)
+
+    # 混淆步骤(失败也不中断,会自动回退原始代码)
+    if not args.skip_obfuscate:
+        step_obfuscate()
+
+    # 准备编译目录
+    if not args.skip_obfuscate and OBFUSCATED_DIR.exists():
+        build_src = step_prepare_build_dir(mode=mode, no_license=args.no_license)
+    else:
+        build_src = BASE_DIR
+
+    # 编译
+    if mode == "module":
+        step_nuitka_module(build_src)
+    elif mode == "exe":
+        step_nuitka_exe(build_src, onefile=False)
+    else:
+        step_nuitka_exe(build_src, onefile=True)
+
+    # 复制 License(无授权则跳过)
+    step_copy_license(mode=mode, onefile=(mode == "onefile"), no_license=args.no_license)
+
+    log("=" * 60)
+    log("构建完成!")
+    log(f"  输出目录: {DIST_DIR}")
+    if args.no_license:
+        log("  授权状态: 无授权版本(无需 license.lic / public_key.pem)")
+    if mode == "module":
+        log("  调用方式: Node.js -> laser_wrapper.py -> entry.pyd/.so")
+    else:
+        exe_name = PROJECT_NAME + (".exe" if platform.system() == "Windows" else "")
+        log(f"  调用方式: Node.js -> child_process.execFile('{exe_name}', [api, data])")
+    log("=" * 60)
+
+if __name__ == "__main__":
+    main()

+ 46 - 0
entry.py

@@ -0,0 +1,46 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+共享库入口文件 (.so / .pyd)
+暴露 Python 级别的函数供外部(如 Node.js / Vue3)调用。
+"""
+
+import json
+import base64
+from license_checker import verify_license
+
+# 延迟导入核心业务逻辑,确保先验证 License
+def _check_auth():
+    if not verify_license():
+        raise PermissionError("License verification failed. Please provide a valid license.lic and public_key.pem file.")
+
+def get_path():
+    """对应原 api_test.py 中的 getpath"""
+    _check_auth()
+    import data_clean as dc
+    return_path = str(dc.result_main())
+    return json.dumps({'obj': return_path}, ensure_ascii=False)
+
+def load_data(b64_json_data):
+    """对应原 api_test.py 中的 loaddata"""
+    _check_auth()
+    import data_clean as dc
+    data = json.loads(base64.b64decode(b64_json_data).decode("utf-8"))
+    return_list = dc.data_analyse(data)
+    return json.dumps(return_list, ensure_ascii=False)
+
+def history_data(b64_json_data):
+    """对应原 api_test.py 中的 historydata"""
+    _check_auth()
+    import data_clean as dc
+    data = json.loads(base64.b64decode(b64_json_data).decode("utf-8"))
+    return_list = dc.history_data(data)
+    return json.dumps(return_list, ensure_ascii=False)
+
+def delete_data(b64_json_data):
+    """对应原 api_test.py 中的 deletedata"""
+    _check_auth()
+    import data_clean as dc
+    data = json.loads(base64.b64decode(b64_json_data).decode("utf-8"))
+    return_path = dc.delete_data(data)
+    return json.dumps(return_path, ensure_ascii=False)

+ 137 - 0
keygen.py

@@ -0,0 +1,137 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+RSA 密钥对生成与 License 签发工具
+
+使用方法:
+    python keygen.py genkey
+    python keygen.py genlicense --expire "never" --product "laser_cal" --version "1.0.0" --licensee "客户名"
+"""
+
+import os
+import sys
+import json
+import argparse
+import base64
+import uuid
+from datetime import datetime
+from pathlib import Path
+
+try:
+    from cryptography.hazmat.primitives import hashes, serialization
+    from cryptography.hazmat.primitives.asymmetric import rsa, padding
+    from cryptography.hazmat.backends import default_backend
+except ImportError:
+    print("错误: 缺少 cryptography 库,请先安装: pip install cryptography")
+    sys.exit(1)
+
+BASE_DIR = Path(__file__).parent
+LICENSE_DIR = BASE_DIR / "dist" / "license"
+PRIVATE_KEY_FILE = LICENSE_DIR / "private_key.pem"
+PUBLIC_KEY_FILE = LICENSE_DIR / "public_key.pem"
+LICENSE_FILE = LICENSE_DIR / "license.lic"
+RSA_KEY_SIZE = 2048
+
+
+def ensure_dir():
+    LICENSE_DIR.mkdir(parents=True, exist_ok=True)
+
+
+def generate_rsa_keys():
+    """生成 RSA 密钥对"""
+    ensure_dir()
+    private_key = rsa.generate_private_key(
+        public_exponent=65537, key_size=RSA_KEY_SIZE, backend=default_backend()
+    )
+    private_pem = private_key.private_bytes(
+        encoding=serialization.Encoding.PEM,
+        format=serialization.PrivateFormat.PKCS8,
+        encryption_algorithm=serialization.NoEncryption()
+    )
+    public_pem = private_key.public_key().public_bytes(
+        encoding=serialization.Encoding.PEM,
+        format=serialization.PublicFormat.SubjectPublicKeyInfo
+    )
+    PRIVATE_KEY_FILE.write_bytes(private_pem)
+    PUBLIC_KEY_FILE.write_bytes(public_pem)
+    print(f"[成功] RSA 密钥对已生成:")
+    print(f"  私钥: {PRIVATE_KEY_FILE}")
+    print(f"  公钥: {PUBLIC_KEY_FILE}")
+    print(f"  密钥长度: {RSA_KEY_SIZE} bits")
+    print(f"\n[重要] 请妥善保管私钥文件,切勿泄露!")
+
+
+def generate_license(expire_date, product="laser_cal", version="1.0.0",
+                     licensee="default", max_instances=1):
+    """生成 License 文件"""
+    ensure_dir()
+    if not PRIVATE_KEY_FILE.exists():
+        print("[错误] 私钥文件不存在,请先执行 genkey。")
+        sys.exit(1)
+
+    private_key = serialization.load_pem_private_key(
+        PRIVATE_KEY_FILE.read_bytes(), password=None, backend=default_backend()
+    )
+
+    license_data = {
+        "license_id": str(uuid.uuid4()),
+        "product": product,
+        "version": version,
+        "licensee": licensee,
+        "issue_date": datetime.now().strftime("%Y-%m-%d"),
+        "expire_date": expire_date,
+        "max_instances": max_instances,
+        "features": ["all"]
+    }
+
+    license_json = json.dumps(license_data, sort_keys=True, ensure_ascii=False)
+    signature = private_key.sign(
+        license_json.encode("utf-8"), padding.PKCS1v15(), hashes.SHA256()
+    )
+
+    license_content = {
+        "license_data": license_data,
+        "signature": base64.b64encode(signature).decode("utf-8")
+    }
+    LICENSE_FILE.write_text(json.dumps(license_content, indent=2, ensure_ascii=False), encoding="utf-8")
+
+    print(f"[成功] License 文件已生成: {LICENSE_FILE}")
+    print(f"  License ID : {license_data['license_id']}")
+    print(f"  产品       : {license_data['product']}")
+    print(f"  版本       : {license_data['version']}")
+    print(f"  被授权人   : {license_data['licensee']}")
+    print(f"  过期日期   : {license_data['expire_date']}")
+
+
+def main():
+    parser = argparse.ArgumentParser(description="RSA License 工具")
+    subparsers = parser.add_subparsers(dest="command")
+
+    subparsers.add_parser("genkey", help="生成 RSA 密钥对")
+
+    lic_parser = subparsers.add_parser("genlicense", help="生成 License")
+    lic_parser.add_argument("--expire", type=str, default="never")
+    lic_parser.add_argument("--product", type=str, default="laser_cal")
+    lic_parser.add_argument("--version", type=str, default="1.0.0")
+    lic_parser.add_argument("--licensee", type=str, default="default")
+    lic_parser.add_argument("--max-instances", type=int, default=1)
+
+    args = parser.parse_args()
+
+    if args.command == "genkey":
+        generate_rsa_keys()
+    elif args.command == "genlicense":
+        expire = args.expire
+        if expire.lower() != "never":
+            try:
+                datetime.strptime(expire, "%Y-%m-%d")
+            except ValueError:
+                print("[错误] 日期格式不正确,请使用 YYYY-MM-DD 或 'never'。")
+                sys.exit(1)
+        generate_license(expire, args.product, args.version, args.licensee, args.max_instances)
+    else:
+        parser.print_help()
+
+
+if __name__ == "__main__":
+    main()

+ 142 - 0
license_checker.py

@@ -0,0 +1,142 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+License 验证模块
+"""
+
+import sys
+import json
+import base64
+import os
+from datetime import datetime
+from pathlib import Path
+
+try:
+    from cryptography.hazmat.primitives import hashes, serialization
+    from cryptography.hazmat.primitives.asymmetric import padding
+    from cryptography.hazmat.backends import default_backend
+    from cryptography.exceptions import InvalidSignature
+except ImportError:
+    print("错误: 缺少 cryptography 库")
+    sys.exit(1)
+
+PRODUCT_NAME = "laser_cal"
+
+
+def _get_base_dir():
+    """获取程序基础目录"""
+    if getattr(sys, 'frozen', False) or '__compiled__' in dir():
+        # 如果是被打包的程序,寻找当前执行路径的目录
+        return Path(sys.executable).parent
+    # 开发环境下,如果是从共享库加载,__file__ 指向当前文件
+    return Path(__file__).parent
+
+
+def _find_file(filename):
+    """在可能的位置查找文件"""
+    # 1. 尝试当前执行文件目录
+    base = _get_base_dir()
+    if (base / filename).exists():
+        return base / filename
+    
+    # 2. 尝试当前工作目录
+    cwd = Path.cwd()
+    if (cwd / filename).exists():
+        return cwd / filename
+        
+    # 3. 尝试开发环境下的 dist/license 目录
+    dev_path = Path(__file__).parent / "dist" / "license" / filename
+    if dev_path.exists():
+        return dev_path
+        
+    return None
+
+
+def verify_license(verbose=False):
+    """验证 License 文件"""
+    license_path = _find_file("license.lic")
+    pubkey_path = _find_file("public_key.pem")
+
+    if not license_path:
+        if verbose:
+            print(f"[授权错误] License 文件 (license.lic) 不存在")
+        return False
+
+    if not pubkey_path:
+        if verbose:
+            print(f"[授权错误] 公钥文件 (public_key.pem) 不存在")
+        return False
+
+    try:
+        license_content = json.loads(license_path.read_text(encoding="utf-8"))
+        license_data = license_content["license_data"]
+        signature_b64 = license_content["signature"]
+    except (json.JSONDecodeError, KeyError):
+        if verbose:
+            print("[授权错误] License 文件格式无效")
+        return False
+
+    try:
+        public_key = serialization.load_pem_public_key(
+            pubkey_path.read_bytes(), backend=default_backend()
+        )
+    except Exception:
+        if verbose:
+            print("[授权错误] 公钥加载失败")
+        return False
+
+    # 验证 RSA 签名
+    try:
+        license_json = json.dumps(license_data, sort_keys=True, ensure_ascii=False)
+        signature = base64.b64decode(signature_b64)
+        public_key.verify(
+            signature, license_json.encode("utf-8"), padding.PKCS1v15(), hashes.SHA256()
+        )
+    except InvalidSignature:
+        if verbose:
+            print("[授权错误] License 签名验证失败,文件可能被篡改")
+        return False
+    except Exception:
+        if verbose:
+            print("[授权错误] 签名验证过程出错")
+        return False
+
+    # 检查过期日期
+    expire_str = license_data.get("expire_date", "")
+    if expire_str.lower() != "never":
+        try:
+            expire_date = datetime.strptime(expire_str, "%Y-%m-%d")
+            if datetime.now() > expire_date:
+                if verbose:
+                    print(f"[授权错误] License 已过期: {expire_str}")
+                return False
+        except ValueError:
+            if verbose:
+                print("[授权错误] 过期日期格式无效")
+            return False
+
+    # 检查产品名
+    if license_data.get("product", "") != PRODUCT_NAME:
+        if verbose:
+            print("[授权错误] License 产品不匹配")
+        return False
+
+    if verbose:
+        print(f"[授权验证] 通过 | {license_data.get('licensee')} | 过期: {expire_str}")
+    return True
+
+
+def get_license_info():
+    """获取 License 信息"""
+    license_path = _find_file("license.lic")
+    if not license_path:
+        return {}
+    try:
+        content = json.loads(license_path.read_text(encoding="utf-8"))
+        return content.get("license_data", {})
+    except Exception:
+        return {}
+
+
+if __name__ == "__main__":
+    verify_license(verbose=True)

+ 166 - 0
node_caller/laser.js

@@ -0,0 +1,166 @@
+/**
+ * laser_cal Node.js 调用封装
+ * 
+ * 支持三种调用模式:
+ *   1. module 模式:通过 Python 解释器 + laser_wrapper.py 加载 .pyd/.so 共享库
+ *   2. exe 模式:直接调用编译后的可执行程序(standalone 目录)
+ *   3. onefile 模式:直接调用编译后的单文件可执行程序
+ * 
+ * 使用示例:
+ *   const laser = require('./laser');
+ *   
+ *   // 自动检测模式
+ *   const result = await laser.callLaser('getpath');
+ *   const result = await laser.callLaser('loaddata', base64JsonData);
+ */
+
+const { execFile } = require('child_process');
+const path = require('path');
+const fs = require('fs');
+
+// ============================================================
+// 配置区(根据实际部署路径修改)
+// ============================================================
+
+const PROJECT_NAME = 'laser_cal';
+const DIST_DIR = path.join(__dirname, '..', 'dist', 'program');
+const WRAPPER_SCRIPT = path.join(__dirname, 'laser_wrapper.py');
+
+// Python 路径(module 模式需要)
+// 可通过环境变量 PYTHON_PATH 覆盖
+const PYTHON_CMD = process.env.PYTHON_PATH
+    || (process.platform === 'win32'
+        ? path.join(__dirname, '..', 'myvenv', 'Scripts', 'python.exe')
+        : 'python3');
+
+// ============================================================
+// 自动检测可用的调用模式
+// ============================================================
+
+function detectMode() {
+    const exeName = process.platform === 'win32' ? `${PROJECT_NAME}.exe` : PROJECT_NAME;
+
+    // 优先检测 onefile 模式(exe 在 dist/program/ 下)
+    const onefilePath = path.join(DIST_DIR, exeName);
+    if (fs.existsSync(onefilePath)) {
+        return { mode: 'exe', execPath: onefilePath };
+    }
+
+    // 检测 standalone 模式(exe 在 dist/program/api_test.dist/ 下)
+    const standalonePath = path.join(DIST_DIR, 'api_test.dist', exeName);
+    if (fs.existsSync(standalonePath)) {
+        return { mode: 'exe', execPath: standalonePath };
+    }
+
+    // 回退到 module 模式(使用 Python + wrapper)
+    return { mode: 'module', execPath: null };
+}
+
+// ============================================================
+// 核心调用函数
+// ============================================================
+
+/**
+ * 调用 laser_cal 后端
+ * @param {string} apiName - API 名称: getpath | loaddata | historydata | deletedata
+ * @param {string|null} b64Data - Base64 编码的 JSON 参数(loaddata/historydata/deletedata 需要)
+ * @returns {Promise<object>} - 解析后的 JSON 结果
+ */
+function callLaser(apiName, b64Data = null) {
+    const detected = detectMode();
+
+    if (detected.mode === 'exe') {
+        return callExe(detected.execPath, apiName, b64Data);
+    } else {
+        return callModule(apiName, b64Data);
+    }
+}
+
+/**
+ * 直接调用可执行文件(exe/standalone 模式)
+ */
+function callExe(exePath, apiName, b64Data = null) {
+    return new Promise((resolve, reject) => {
+        const args = [apiName];
+        if (b64Data) {
+            args.push(b64Data);
+        }
+
+        const options = {
+            maxBuffer: 1024 * 1024 * 50,
+            cwd: path.dirname(exePath)  // 设置工作目录为 exe 所在目录(确保找到 license)
+        };
+
+        execFile(exePath, args, options, (error, stdout, stderr) => {
+            if (error) {
+                return reject(new Error(`Execution failed: ${error.message}\nStderr: ${stderr}`));
+            }
+            parseOutput(stdout, resolve, reject);
+        });
+    });
+}
+
+/**
+ * 通过 Python 解释器调用(module 模式)
+ */
+function callModule(apiName, b64Data = null) {
+    return new Promise((resolve, reject) => {
+        const args = [WRAPPER_SCRIPT, apiName];
+        if (b64Data) {
+            args.push(b64Data);
+        }
+
+        const options = {
+            maxBuffer: 1024 * 1024 * 50
+        };
+
+        execFile(PYTHON_CMD, args, options, (error, stdout, stderr) => {
+            if (error) {
+                return reject(new Error(`Python execution failed: ${error.message}\nStderr: ${stderr}`));
+            }
+            parseOutput(stdout, resolve, reject);
+        });
+    });
+}
+
+/**
+ * 解析 stdout JSON 输出
+ */
+function parseOutput(stdout, resolve, reject) {
+    try {
+        const lines = stdout.trim().split('\n');
+        const lastLine = lines[lines.length - 1];
+        const result = JSON.parse(lastLine);
+
+        if (result.error) {
+            reject(new Error(result.error));
+        } else {
+            resolve(result);
+        }
+    } catch (e) {
+        reject(new Error(`Failed to parse output: ${stdout}\nError: ${e.message}`));
+    }
+}
+
+// ============================================================
+// 兼容旧接口
+// ============================================================
+
+/**
+ * 兼容旧版 callPython 接口
+ */
+function callPython(apiName, b64Data = null) {
+    return callLaser(apiName, b64Data);
+}
+
+// ============================================================
+// 导出
+// ============================================================
+
+module.exports = {
+    callLaser,
+    callPython,    // 向后兼容
+    callExe,
+    callModule,
+    detectMode
+};

+ 64 - 0
node_caller/laser_wrapper.py

@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Node.js 调用的 Python 桥梁脚本。
+该脚本负责加载编译好的 entry.so / entry.pyd 共享库,并处理来自 Node.js 的 JSON 请求。
+"""
+
+import sys
+import json
+import os
+from pathlib import Path
+
+# 将 dist/program 目录加入 sys.path,以便能导入 entry.so/.pyd
+BIN_DIR = Path(__file__).parent.parent / "dist" / "program"
+sys.path.insert(0, str(BIN_DIR))
+
+# 尝试导入编译好的共享库,如果失败则尝试导入源码(开发模式)
+try:
+    import entry
+except ImportError as e:
+    print(json.dumps({"error": f"Failed to load entry module: {e}"}))
+    sys.exit(1)
+
+def main():
+    if len(sys.argv) < 2:
+        print(json.dumps({"error": "No API specified"}))
+        sys.exit(1)
+
+    api_name = sys.argv[1]
+
+    try:
+        if api_name == "getpath":
+            result_json = entry.get_path()
+            print(result_json)
+            
+        elif api_name == "loaddata":
+            if len(sys.argv) < 3:
+                print(json.dumps({"error": "No data"}))
+                sys.exit(1)
+            result_json = entry.load_data(sys.argv[2])
+            print(result_json)
+            
+        elif api_name == "historydata":
+            if len(sys.argv) < 3:
+                print(json.dumps({"error": "No data"}))
+                sys.exit(1)
+            result_json = entry.history_data(sys.argv[2])
+            print(result_json)
+            
+        elif api_name == "deletedata":
+            if len(sys.argv) < 3:
+                print(json.dumps({"error": "No data"}))
+                sys.exit(1)
+            result_json = entry.delete_data(sys.argv[2])
+            print(result_json)
+            
+        else:
+            print(json.dumps({"error": "Invalid API"}))
+            
+    except Exception as e:
+        print(json.dumps({"error": str(e)}))
+
+if __name__ == "__main__":
+    main()

+ 52 - 0
node_caller/vue3_example.js

@@ -0,0 +1,52 @@
+/**
+ * Vue3 调用示例
+ * 
+ * 架构:Vue3 -> Electron IPC (或 Node.js API) -> child_process -> laser_wrapper.py -> entry.pyd/.so
+ */
+
+const VUE3_CODE = `
+// 在 Vue3 中调用
+import { ref } from 'vue';
+
+// 假设通过 Electron IPC 调用
+const invokeLaser = async (apiName, dataObj = null) => {
+    let b64Data = null;
+    if (dataObj) {
+        b64Data = btoa(unescape(encodeURIComponent(JSON.stringify(dataObj))));
+    }
+    // 调用 electron 暴露的 API
+    return await window.electronAPI.callPythonAPI(apiName, b64Data);
+};
+
+export default {
+    setup() {
+        const result = ref(null);
+
+        const handleGetPath = async () => {
+            try {
+                result.value = await invokeLaser('getpath');
+            } catch (e) {
+                console.error(e);
+            }
+        };
+
+        const handleLoadData = async () => {
+            const params = [
+                "path/to/locate.csv",
+                "path/to/measure.csv",
+                "5.0", // angle_cone
+                "2.0"  // axial_inclination
+            ];
+            try {
+                result.value = await invokeLaser('loaddata', params);
+            } catch (e) {
+                console.error(e);
+            }
+        };
+
+        return { handleGetPath, handleLoadData, result };
+    }
+}
+`;
+
+console.log("Vue3 Example Code generated.");

+ 53 - 1
readme.md

@@ -29,4 +29,56 @@
 | data_analyse_origin.py | 调用frequency_filter.py组成的完整的算法代码,包含了data_clean.py中的全部功能。有着完整的注释、检查点和日志输出,不依赖软件即可分析,并添加了画图功能,但分析结果无法自动保存,通常用作算法调试。 |
 | data_clean.py          | 调用frequency_filter.py组成的完整算法代码,但是没有注释、检查点和日志输出,无画图功能。是api_test.py调用的主程序,用于软件部署。                                  |
 | frequency_filter.py    | 塔筒振动分析,叶根叶尖处塔筒距离计算,是完整算法的重要组成部分,内部有已经被注释掉的塔筒振动画图算法。                                                              |
-| orginal_plot.py        | 读取原始测量数据存储文件夹中的全部数据并绘图,通常用于检验原始数据基本情况。                                                                           |
+| orginal_plot.py        | 读取原始测量数据存储文件夹中的全部数据并绘图,通常用于检验原始数据基本情况。                                                                           |
+
+
+# 打包发布
+    尽可能的防反编译或增加反编译成本(时间、解读)。
+
+## 第三方依赖包
+    pip 安装 nuitka、 codeenigma 、cryptography
+
+## 授权管理 (Cryptography)
+
+### 生成 RSA 密钥对
+    执行命令:python keygen.py genkey
+    输出到 dist/license/ 目录:private_key.pem、public_key.pem
+
+### 签发 License
+    # 永不过期
+    python keygen.py genlicense --expire "never" --product "laser_cal" --version "1.0.0" --licensee "客户名"
+
+    # 指定过期日期
+    python keygen.py genlicense --expire "2027-12-31" --product "laser_cal" --version "1.0.0" --licensee "客户名"
+
+## 混淆与打包 (CodeEnigma + Nuitka)
+    # 清理
+    python build.py --clean
+
+    # 混淆 + 编译(standalone 模式)
+    python build.py
+
+    # 混淆 + 编译(单文件模式)
+    python build.py --onefile
+
+    # 混淆 + 编译为可执行程序(推荐生产使用)
+    python build.py --exe
+
+    # 仅编译,跳过混淆
+    python build.py --skip-obfuscate
+
+    # 仅编译为可执行程序(跳过混淆,开发调试用)
+    python build.py --exe --skip-obfuscate
+
+    # 编译为单文件(最便于分发)
+    python build.py --onefile --skip-obfuscate
+
+    # 编译为共享库(原有模式,向后兼容;共享库在windows下为pyd文件,linux下位so文件)
+    python build.py --module --skip-obfuscate
+
+    注意事项
+    1. --exe (编译为可执行程序后,Node.js 不再需要 Python 环境,可以直接调用) 和 --onefile 模式会将 pandas、numpy、scipy 等大型库一起打包,编译时间较长(Windows 上约 5-15 分钟),产物体积较大(约 200-500MB)。
+    2. --module 模式编译快(约 30 秒),产物小,但运行时仍需要 Python 虚拟环境。
+    3. 三种模式均支持 --skip-obfuscate 跳过混淆步骤。
+    4. license.lic 和 public_key.pem 会自动复制到编译产物目录。
+    5. node_caller文件夹为 vue3 调用算法示例, 如命令: node -e "require('./node_caller/laser.js').callPython('getpath').then(console.log).catch(console.error)"

+ 7 - 0
requirements.txt

@@ -0,0 +1,7 @@
+matplotlib==3.10.9
+numpy==2.4.6
+pandas==3.0.3
+scipy==1.17.1
+seaborn==0.13.2
+sympy==1.14.0
+sympy==1.13.3