diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..6cbd282 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,58 @@ +name: Release + +on: + push: + tags: + - 'v*' + +env: + IMAGE_NAME: ${{ github.repository }} + +jobs: + release: + name: Release Binary and Docker + runs-on: ubuntu-latest + container: + image: goreleaser/goreleaser-cross:v1.24 + permissions: + contents: write + packages: write + id-token: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Fix git permissions + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: Install UPX + uses: crazy-max/ghaction-upx@v3 + with: + install-only: true + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Run GoReleaser + run: goreleaser release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..619f459 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +# syncthing files +.stfolder + +chatlog.exe# Added by goreleaser init: +dist/ diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..121ccbd --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,159 @@ +# GoReleaser v2 配置 +version: 2 + +before: + hooks: + - go mod tidy + +builds: + - id: chatlog-builds + binary: chatlog + ldflags: + - -s -w -X github.com/sjzar/chatlog/pkg/version.Version={{.Version}} + env: + - CGO_ENABLED=1 + goos: + - darwin + - windows + - linux + goarch: + - amd64 + - arm64 + overrides: + - goos: darwin + goarch: amd64 + env: + - CGO_ENABLED=1 + - CC=o64-clang + - CXX=o64-clang++ + - goos: darwin + goarch: arm64 + env: + - CGO_ENABLED=1 + - CC=oa64-clang + - CXX=oa64-clang++ + - goos: windows + goarch: amd64 + env: + - CGO_ENABLED=1 + - CC=x86_64-w64-mingw32-gcc + - CXX=x86_64-w64-mingw32-g++ + - goos: windows + goarch: arm64 + env: + - CGO_ENABLED=1 + - CC=/llvm-mingw/bin/aarch64-w64-mingw32-gcc + - CXX=/llvm-mingw/bin/aarch64-w64-mingw32-g++ + - goos: linux + goarch: amd64 + env: + - CGO_ENABLED=1 + - CC=x86_64-linux-gnu-gcc + - CXX=x86_64-linux-gnu-g++ + - goos: linux + goarch: arm64 + env: + - CGO_ENABLED=1 + - CC=aarch64-linux-gnu-gcc + - CXX=aarch64-linux-gnu-g++ + +upx: + - enabled: true + goos: [darwin, windows, linux] + goarch: [amd64, arm64] + compress: best + lzma: true + +archives: + - id: default + formats: tar.gz + name_template: >- + {{ .ProjectName }}_ + {{- .Version }}_ + {{- .Os }}_ + {{- .Arch }} + format_overrides: + - goos: windows + formats: zip + files: + - LICENSE + - README.md + +checksum: + name_template: 'checksums.txt' + algorithm: sha256 + +# 配置 GitHub Release +release: + draft: true + prerelease: auto + mode: replace + +# Docker 镜像配置 +# FIXME: 等待 goreleaser 2.12 更新后使用 dockers_v2 +dockers: + - image_templates: + - "ghcr.io/sjzar/chatlog:{{ .Tag }}-amd64" + - "sjzar/chatlog:{{ .Tag }}-amd64" + dockerfile: Dockerfile + extra_files: + - script/docker-entrypoint.sh + use: buildx + goos: linux + goarch: amd64 + build_flag_templates: + - "--platform=linux/amd64" + - "--label=org.opencontainers.image.title={{.ProjectName}}" + - "--label=org.opencontainers.image.description=chat log tool, easily use your own chat data." + - "--label=org.opencontainers.image.url=https://github.com/sjzar/chatlog" + - "--label=org.opencontainers.image.source=https://github.com/sjzar/chatlog" + - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.licenses=Apache-2.0" + + - image_templates: + - "ghcr.io/sjzar/chatlog:{{ .Tag }}-arm64" + - "sjzar/chatlog:{{ .Tag }}-arm64" + dockerfile: Dockerfile + extra_files: + - script/docker-entrypoint.sh + use: buildx + goos: linux + goarch: arm64 + build_flag_templates: + - "--platform=linux/arm64" + - "--label=org.opencontainers.image.title={{.ProjectName}}" + - "--label=org.opencontainers.image.description=chat log tool, easily use your own chat data." + - "--label=org.opencontainers.image.url=https://github.com/sjzar/chatlog" + - "--label=org.opencontainers.image.source=https://github.com/sjzar/chatlog" + - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.licenses=Apache-2.0" + +# Docker manifest 配置用于多架构镜像 +docker_manifests: + # GitHub Container Registry manifests + - name_template: "ghcr.io/sjzar/chatlog:{{ .Tag }}" + image_templates: + - "ghcr.io/sjzar/chatlog:{{ .Tag }}-amd64" + - "ghcr.io/sjzar/chatlog:{{ .Tag }}-arm64" + + # Docker Hub manifests + - name_template: "sjzar/chatlog:{{ .Tag }}" + image_templates: + - "sjzar/chatlog:{{ .Tag }}-amd64" + - "sjzar/chatlog:{{ .Tag }}-arm64" + + # GitHub Container Registry latest + - name_template: "{{ if not .Prerelease }}ghcr.io/sjzar/chatlog:latest{{ end }}" + image_templates: + - "ghcr.io/sjzar/chatlog:{{ .Tag }}-amd64" + - "ghcr.io/sjzar/chatlog:{{ .Tag }}-arm64" + + # Docker Hub latest + - name_template: "{{ if not .Prerelease }}sjzar/chatlog:latest{{ end }}" + image_templates: + - "sjzar/chatlog:{{ .Tag }}-amd64" + - "sjzar/chatlog:{{ .Tag }}-arm64" \ No newline at end of file diff --git a/DISCLAIMER.md b/DISCLAIMER.md new file mode 100644 index 0000000..6ff12e7 --- /dev/null +++ b/DISCLAIMER.md @@ -0,0 +1,121 @@ +# Chatlog 免责声明 + +## 1. 定义 + +在本免责声明中,除非上下文另有说明,下列术语应具有以下含义: + +- **"本项目"或"Chatlog"**:指本开源软件项目,包括其源代码、可执行程序、文档及相关资源。 +- **"开发者"**:指本项目的创建者、维护者及代码贡献者。 +- **"用户"**:指下载、安装、使用或以任何方式接触本项目的个人或实体。 +- **"聊天数据"**:指通过各类即时通讯软件生成的对话内容及相关元数据。 +- **"合法授权"**:指根据适用法律法规,由数据所有者或数据主体明确授予的处理其聊天数据的权限。 +- **"第三方服务"**:指由非本项目开发者提供的外部服务,如大型语言模型(LLM) API 服务。 + +## 2. 使用目的与法律遵守 + +本项目仅供学习、研究和个人合法使用。用户须严格遵守所在国家/地区的法律法规使用本工具。任何违反法律法规、侵犯他人合法权益的行为,均与本项目及其开发者无关,相关法律责任由用户自行承担。 + +⚠️ **用户应自行了解并遵守当地有关数据访问、隐私保护、计算机安全和网络安全的法律法规。不同司法管辖区对数据处理有不同的法律要求,用户有责任确保其使用行为符合所有适用法规。** + +## 3. 授权范围与隐私保护 + +- 本工具仅限于处理用户自己合法拥有的聊天数据,或已获得数据所有者明确授权的数据。 +- 严禁将本工具用于未经授权获取、查看或分析他人聊天记录,或侵犯他人隐私权。 +- 用户应采取适当措施保护通过本工具获取和处理的聊天数据安全,包括但不限于加密存储、限制访问权限、定期删除不必要数据等。 +- 用户应确保其处理的聊天数据符合相关数据保护法规,包括但不限于获得必要的同意、保障数据主体权利、遵守数据最小化原则等。 + +## 4. 使用限制 + +- 本项目仅允许在合法授权情况下对聊天数据库进行备份与查看。 +- 未经明确授权,严禁将本项目用于访问、查看、分析或处理任何第三方聊天数据。 +- 使用第三方 LLM 服务时,用户应遵守相关服务提供商的服务条款和使用政策。 +- 用户不得规避本项目中的任何技术限制,或尝试反向工程、反编译或反汇编本项目,除非适用法律明确允许此类活动。 + +## 5. 技术风险声明 + +⚠️ **使用本项目存在以下技术风险,用户应充分了解并自行承担:** + +- 本工具需要访问聊天软件的数据库文件,可能因聊天软件版本更新导致功能失效或数据不兼容。 +- 在 macOS 系统上使用时,需要临时关闭 SIP 安全机制,这可能降低系统安全性,用户应了解相关风险并自行决定是否使用。 +- 本项目可能存在未知的技术缺陷或安全漏洞,可能导致数据损坏、丢失或泄露。 +- 使用本项目处理大量数据可能导致系统性能下降或资源占用过高。 +- 第三方依赖库或 API 的变更可能影响本项目的功能或安全性。 + +## 6. 禁止非法用途 + +严禁将本项目用于以下用途: + +- 从事任何形式的非法活动,包括但不限于未授权系统测试、网络渗透或其他违反法律法规的行为。 +- 监控、窃取或未经授权获取他人聊天记录或个人信息。 +- 将获取的数据用于骚扰、诈骗、敲诈、威胁或其他侵害他人合法权益的行为。 +- 规避任何安全措施或访问控制机制。 +- 传播虚假信息、仇恨言论或违反公序良俗的内容。 +- 侵犯任何第三方的知识产权、隐私权或其他合法权益。 + +**违反上述规定的,用户应自行承担全部法律责任,并赔偿因此给开发者或第三方造成的全部损失。** + +## 7. 第三方服务集成 + +- 用户将聊天数据与第三方 LLM 服务(如 OpenAI、Claude 等)结合使用时,应仔细阅读并遵守这些服务的使用条款、隐私政策和数据处理协议。 +- 用户应了解,向第三方服务传输数据可能导致数据离开用户控制范围,并受第三方服务条款约束。 +- 本项目开发者不对第三方服务的可用性、安全性、准确性或数据处理行为负责,用户应自行评估相关风险。 +- 用户应确保其向第三方服务传输数据的行为符合适用的数据保护法规和第三方服务条款。 + +## 8. 责任限制 + +**在法律允许的最大范围内:** + +- 本项目按"原样"和"可用"状态提供,不对功能的适用性、可靠性、准确性、完整性或及时性做任何明示或暗示的保证。 +- 开发者明确否认对适销性、特定用途适用性、不侵权以及任何其他明示或暗示的保证。 +- 本项目开发者和贡献者不对用户使用本工具的行为及后果承担任何法律责任。 +- 对于因使用本工具而可能导致的任何直接、间接、附带、特殊、惩罚性或后果性损失,包括但不限于数据丢失、业务中断、隐私泄露、声誉损害、利润损失、法律纠纷等,本项目开发者概不负责,即使开发者已被告知此类损失的可能性。 +- 在任何情况下,开发者对用户的全部责任累计不超过用户为获取本软件实际支付的金额(如为免费获取则为零)。 + +## 9. 知识产权声明 + +- 本项目基于 Apache-2.0 许可证开源,用户在使用、修改和分发时应严格遵守该许可证的所有条款。 +- 本项目的名称"Chatlog"、相关标识及商标权(如有)归开发者所有,未经明确授权,用户不得以任何方式使用这些标识进行商业活动。 +- 根据 Apache-2.0 许可证,用户可自由使用、修改和分发本项目代码,但须遵守许可证规定的归属声明等要求。 +- 用户对其修改版本自行承担全部责任,且不得以原项目名义发布,必须明确标明其为修改版本并与原项目区分。 +- 用户不得移除或更改本项目中的版权声明、商标或其他所有权声明。 + +## 10. 数据处理合规性 + +- 用户在使用本项目处理个人数据时,应遵守适用的数据保护法规,包括但不限于《中华人民共和国个人信息保护法》、《通用数据保护条例》(GDPR)等。 +- 用户应确保其具有处理相关数据的合法依据,如获得数据主体的明确同意。 +- 用户应实施适当的技术和组织措施,确保数据安全,防止未授权访问、意外丢失或泄露。 +- 在跨境传输数据时,用户应确保符合相关法律对数据出境的要求。 +- 用户应尊重数据主体权利,包括访问权、更正权、删除权等。 + +## 11. 免责声明接受 + +下载、安装、使用本项目,表示用户已阅读、理解并同意遵守本免责声明的所有条款。如不同意,请立即停止使用本工具并删除相关代码和程序。 + +**用户确认:** +- 已完整阅读并理解本免责声明的全部内容 +- 自愿接受本免责声明的全部条款 +- 具有完全民事行为能力,能够理解并承担使用本项目的风险和责任 +- 将遵守本免责声明中规定的所有义务和限制 + +## 12. 免责声明修改与通知 + +- 本免责声明可能根据项目发展和法律法规变化进行修改和调整,修改后的声明将在项目官方仓库页面公布。 +- 开发者没有义务个别通知用户免责声明的变更,用户应定期查阅最新版本。 +- 重大变更将通过项目仓库的 Release Notes 或 README 文件更新进行通知。 +- 在免责声明更新后继续使用本项目,即视为接受修改后的条款。 + +## 13. 法律适用与管辖 + +- 本免责声明受中华人民共和国法律管辖,并按其解释。 +- 任何与本免责声明有关的争议,应首先通过友好协商解决;协商不成的,提交至本项目开发者所在地有管辖权的人民法院诉讼解决。 +- 对于中国境外用户,如本免责声明与用户所在地强制性法律规定冲突,应以不违反该强制性规定的方式解释和适用本声明,但本声明的其余部分仍然有效。 + +## 14. 可分割性 + +如本免责声明中的任何条款被有管辖权的法院或其他权威机构认定为无效、不合法或不可执行,不影响其余条款的有效性和可执行性。无效条款应被视为从本声明中分割,并在法律允许的最大范围内由最接近原条款意图的有效条款替代。 + +## 15. 完整协议 + +本免责声明构成用户与开发者之间关于本项目使用的完整协议,取代先前或同时期关于本项目的所有口头或书面协议、提议和陈述。本声明的任何豁免、修改或补充均应以书面形式作出并经开发者签署方为有效。 + + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2b7adec --- /dev/null +++ b/Dockerfile @@ -0,0 +1,45 @@ +FROM debian:12-slim + +LABEL maintainer="Sarv " + +ARG DEBIAN_FRONTEND=noninteractive + +ENV PUID=1000 PGID=1000 +ENV GOSU_VERSION=1.17 +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends ca-certificates tzdata curl wget; \ + wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$(dpkg --print-architecture)"; \ + chmod +x /usr/local/bin/gosu; \ + gosu --version; \ + groupadd -r -g ${PGID} chatlog; \ + useradd -r -u ${PUID} -g chatlog -m -d /home/chatlog chatlog; \ + mkdir -p /app/data /app/work; \ + apt-get purge -y --auto-remove wget; \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY script/docker-entrypoint.sh /usr/local/bin/entrypoint.sh + +COPY --from=mwader/static-ffmpeg:7.1.1 /ffmpeg /usr/local/bin/ + +COPY chatlog /usr/local/bin/chatlog + +RUN chmod +x /usr/local/bin/entrypoint.sh \ + /usr/local/bin/ffmpeg \ + /usr/local/bin/chatlog + +EXPOSE 5030 + +ENV CHATLOG_DATA_DIR=/app/data \ + CHATLOG_WORK_DIR=/app/work \ + CHATLOG_HTTP_ADDR=0.0.0.0:5030 \ + PATH="/usr/local/bin:${PATH}" + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD curl -f http://localhost:5030/health || exit 1 + +ENTRYPOINT ["entrypoint.sh"] + +CMD ["chatlog", "server"] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bb257e3 --- /dev/null +++ b/Makefile @@ -0,0 +1,60 @@ +BINARY_NAME := chatlog +GO := go +ifeq ($(VERSION),) + VERSION := $(shell git describe --tags --always --dirty="-dev") +endif +LDFLAGS := -ldflags '-X "github.com/sjzar/chatlog/pkg/version.Version=$(VERSION)" -w -s' + +PLATFORMS := \ + darwin/amd64 \ + darwin/arm64 \ + linux/amd64 \ + linux/arm64 \ + windows/amd64 \ + windows/arm64 + +UPX_PLATFORMS := \ + darwin/amd64 \ + linux/amd64 \ + linux/arm64 \ + windows/amd64 + +.PHONY: all clean lint tidy test build crossbuild upx + +all: clean lint tidy test build + +clean: + @echo "🧹 Cleaning..." + @rm -rf bin/ + +lint: + @echo "🕵️‍♂️ Running linters..." + golangci-lint run ./... + +tidy: + @echo "🧼 Tidying up dependencies..." + $(GO) mod tidy + +test: + @echo "🧪 Running tests..." + $(GO) test ./... -cover + +build: + @echo "🔨 Building for current platform..." + CGO_ENABLED=1 $(GO) build -trimpath $(LDFLAGS) -o bin/$(BINARY_NAME) main.go + +crossbuild: clean + @echo "🌍 Building for multiple platforms..." + for platform in $(PLATFORMS); do \ + os=$$(echo $$platform | cut -d/ -f1); \ + arch=$$(echo $$platform | cut -d/ -f2); \ + float=$$(echo $$platform | cut -d/ -f3); \ + output_name=bin/chatlog_$${os}_$${arch}; \ + [ "$$float" != "" ] && output_name=$$output_name_$$float; \ + echo "🔨 Building for $$os/$$arch..."; \ + echo "🔨 Building for $$output_name..."; \ + GOOS=$$os GOARCH=$$arch CGO_ENABLED=1 GOARM=$$float $(GO) build -trimpath $(LDFLAGS) -o $$output_name main.go ; \ + if [ "$(ENABLE_UPX)" = "1" ] && echo "$(UPX_PLATFORMS)" | grep -q "$$os/$$arch"; then \ + echo "⚙️ Compressing binary $$output_name..." && upx --best $$output_name; \ + fi; \ + done \ No newline at end of file diff --git a/cmd/chatlog/cmd_batch_decrypt.go b/cmd/chatlog/cmd_batch_decrypt.go new file mode 100644 index 0000000..ede546e --- /dev/null +++ b/cmd/chatlog/cmd_batch_decrypt.go @@ -0,0 +1,299 @@ +package chatlog + +import ( + "os" + "path/filepath" + "strings" + "time" + + "github.com/rs/zerolog/log" + "github.com/sjzar/chatlog/pkg/util/dat2img" + "github.com/spf13/cobra" +) + +var ( + batchDecryptCmd = &cobra.Command{ + Use: "batch-decrypt", + Short: "批量解密已存在的.dat图片文件", + Long: `扫描指定目录下的所有.dat文件,并批量解密保存为普通图片格式`, + Example: `chatlog batch-decrypt --data-dir "E:\xwechat_files\wxid_sp86q2lhlm6f22_fffc" --data-key "66363764393236353832316536663530" --platform windows --version 4`, + Run: BatchDecrypt, + } + + // 批量解密参数 + batchDataDir string + batchDataKey string + batchImgKey string + batchPlatform string + batchVersion int + batchRecursive bool + batchDryRun bool + batchConcurrency int +) + +func init() { + rootCmd.AddCommand(batchDecryptCmd) + + // 必需参数 + batchDecryptCmd.Flags().StringVar(&batchDataDir, "data-dir", "", "微信数据目录路径") + batchDecryptCmd.Flags().StringVar(&batchDataKey, "data-key", "", "数据密钥") + batchDecryptCmd.Flags().StringVar(&batchImgKey, "img-key", "", "图片密钥") + batchDecryptCmd.Flags().StringVar(&batchPlatform, "platform", "windows", "平台 (windows/darwin)") + batchDecryptCmd.Flags().IntVar(&batchVersion, "version", 4, "微信版本 (3/4)") + + // 可选参数 + batchDecryptCmd.Flags().BoolVar(&batchRecursive, "recursive", true, "递归扫描子目录") + batchDecryptCmd.Flags().BoolVar(&batchDryRun, "dry-run", false, "仅显示将要处理的文件,不实际解密") + batchDecryptCmd.Flags().IntVar(&batchConcurrency, "concurrency", 4, "并发处理数量") + + // 标记必需参数 + batchDecryptCmd.MarkFlagRequired("data-dir") + batchDecryptCmd.MarkFlagRequired("data-key") +} + +func BatchDecrypt(cmd *cobra.Command, args []string) { + // 验证参数 + if batchDataDir == "" { + log.Error().Msg("data-dir is required") + return + } + if batchDataKey == "" { + log.Error().Msg("data-key is required") + return + } + + // 设置图片密钥(如果提供) + if batchImgKey != "" { + dat2img.SetAesKey(batchImgKey) + log.Info().Str("img_key", batchImgKey).Msg("使用提供的图片密钥") + } else { + // 如果没有提供图片密钥,尝试使用数据密钥 + dat2img.SetAesKey(batchDataKey) + log.Info().Str("data_key", batchDataKey).Msg("使用数据密钥作为图片密钥") + } + + // 设置XOR密钥(微信4.x版本) + if batchVersion == 4 { + log.Info().Msg("扫描并设置XOR密钥...") + _, err := dat2img.ScanAndSetXorKey(batchDataDir) + if err != nil { + log.Warn().Err(err).Msg("设置XOR密钥失败,将使用默认值") + } + } + + log.Info(). + Str("data_dir", batchDataDir). + Str("platform", batchPlatform). + Int("version", batchVersion). + Bool("recursive", batchRecursive). + Bool("dry_run", batchDryRun). + Int("concurrency", batchConcurrency). + Msg("开始批量解密") + + // 扫描.dat文件 + datFiles, err := scanDatFiles(batchDataDir, batchRecursive) + if err != nil { + log.Error().Err(err).Msg("扫描.dat文件失败") + return + } + + if len(datFiles) == 0 { + log.Info().Msg("未找到任何.dat文件") + return + } + + log.Info().Int("count", len(datFiles)).Msg("找到.dat文件") + + // 执行批量解密 + startTime := time.Now() + stats := processBatchDecrypt(datFiles, batchConcurrency, batchDryRun) + duration := time.Since(startTime) + + // 输出统计信息 + log.Info(). + Int("total", stats.Total). + Int("success", stats.Success). + Int("failed", stats.Failed). + Int("skipped", stats.Skipped). + Dur("duration", duration). + Msg("批量解密完成") + + if stats.Failed > 0 { + log.Warn().Int("failed_count", stats.Failed).Msg("部分文件解密失败,请检查日志") + } +} + +// 扫描.dat文件 +func scanDatFiles(dataDir string, recursive bool) ([]string, error) { + var datFiles []string + + walkFunc := func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // 跳过目录 + if info.IsDir() { + return nil + } + + // 检查是否为.dat文件 + if strings.HasSuffix(strings.ToLower(path), ".dat") { + datFiles = append(datFiles, path) + } + + return nil + } + + if recursive { + err := filepath.Walk(dataDir, walkFunc) + if err != nil { + return nil, err + } + } else { + entries, err := os.ReadDir(dataDir) + if err != nil { + return nil, err + } + + for _, entry := range entries { + if !entry.IsDir() && strings.HasSuffix(strings.ToLower(entry.Name()), ".dat") { + datFiles = append(datFiles, filepath.Join(dataDir, entry.Name())) + } + } + } + + return datFiles, nil +} + +// 批量解密统计信息 +type BatchStats struct { + Total int + Success int + Failed int + Skipped int +} + +// 处理批量解密 +func processBatchDecrypt(datFiles []string, concurrency int, dryRun bool) BatchStats { + stats := BatchStats{ + Total: len(datFiles), + } + + // 创建信号量控制并发 + semaphore := make(chan struct{}, concurrency) + results := make(chan BatchResult, len(datFiles)) + + // 启动工作协程 + for _, datFile := range datFiles { + go func(file string) { + semaphore <- struct{}{} // 获取信号量 + defer func() { <-semaphore }() // 释放信号量 + + result := processSingleFile(file, dryRun) + results <- result + }(datFile) + } + + // 收集结果 + for i := 0; i < len(datFiles); i++ { + result := <-results + switch result.Status { + case "success": + stats.Success++ + case "failed": + stats.Failed++ + case "skipped": + stats.Skipped++ + } + + // 输出进度 + if (i+1)%10 == 0 || i == len(datFiles)-1 { + log.Info(). + Int("processed", i+1). + Int("total", len(datFiles)). + Int("success", stats.Success). + Int("failed", stats.Failed). + Int("skipped", stats.Skipped). + Msg("批量解密进度") + } + } + + return stats +} + +// 单个文件处理结果 +type BatchResult struct { + File string + Status string // success, failed, skipped + Error error +} + +// 处理单个文件 +func processSingleFile(datFile string, dryRun bool) BatchResult { + // 生成输出文件路径 + outputPath := strings.TrimSuffix(datFile, filepath.Ext(datFile)) + + // 检查是否已存在解密文件 + extensions := []string{".jpg", ".jpeg", ".png", ".gif", ".bmp", ".mp4"} + for _, ext := range extensions { + if _, err := os.Stat(outputPath + ext); err == nil { + return BatchResult{ + File: datFile, + Status: "skipped", + } + } + } + + if dryRun { + log.Debug().Str("file", datFile).Msg("将处理文件") + return BatchResult{ + File: datFile, + Status: "success", + } + } + + // 读取.dat文件 + data, err := os.ReadFile(datFile) + if err != nil { + return BatchResult{ + File: datFile, + Status: "failed", + Error: err, + } + } + + // 解密文件 + decryptedData, ext, err := dat2img.Dat2Image(data) + if err != nil { + log.Debug().Err(err).Str("file", datFile).Msg("解密失败") + return BatchResult{ + File: datFile, + Status: "failed", + Error: err, + } + } + + // 保存解密后的文件 + outputPath = outputPath + "." + ext + err = os.WriteFile(outputPath, decryptedData, 0644) + if err != nil { + return BatchResult{ + File: datFile, + Status: "failed", + Error: err, + } + } + + log.Debug(). + Str("dat_file", datFile). + Str("output_file", outputPath). + Str("format", ext). + Int("size", len(decryptedData)). + Msg("文件解密成功") + + return BatchResult{ + File: datFile, + Status: "success", + } +} diff --git a/cmd/chatlog/cmd_decrypt.go b/cmd/chatlog/cmd_decrypt.go new file mode 100644 index 0000000..d44b18e --- /dev/null +++ b/cmd/chatlog/cmd_decrypt.go @@ -0,0 +1,63 @@ +package chatlog + +import ( + "fmt" + + "github.com/sjzar/chatlog/internal/chatlog" + + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(decryptCmd) + decryptCmd.Flags().StringVarP(&decryptPlatform, "platform", "p", "", "platform") + decryptCmd.Flags().IntVarP(&decryptVer, "version", "v", 0, "version") + decryptCmd.Flags().StringVarP(&decryptDataDir, "data-dir", "d", "", "data dir") + decryptCmd.Flags().StringVarP(&decryptDatakey, "data-key", "k", "", "data key") + decryptCmd.Flags().StringVarP(&decryptWorkDir, "work-dir", "w", "", "work dir") +} + +var ( + decryptPlatform string + decryptVer int + decryptDataDir string + decryptDatakey string + decryptWorkDir string +) + +var decryptCmd = &cobra.Command{ + Use: "decrypt", + Short: "decrypt", + Run: func(cmd *cobra.Command, args []string) { + + cmdConf := getDecryptConfig() + + m := chatlog.New() + if err := m.CommandDecrypt("", cmdConf); err != nil { + log.Err(err).Msg("failed to decrypt") + return + } + fmt.Println("decrypt success") + }, +} + +func getDecryptConfig() map[string]any { + cmdConf := make(map[string]any) + if len(decryptDataDir) != 0 { + cmdConf["data_dir"] = decryptDataDir + } + if len(decryptDatakey) != 0 { + cmdConf["data_key"] = decryptDatakey + } + if len(decryptWorkDir) != 0 { + cmdConf["work_dir"] = decryptWorkDir + } + if len(decryptPlatform) != 0 { + cmdConf["platform"] = decryptPlatform + } + if decryptVer != 0 { + cmdConf["version"] = decryptVer + } + return cmdConf +} diff --git a/cmd/chatlog/cmd_dumpmemory.go b/cmd/chatlog/cmd_dumpmemory.go new file mode 100644 index 0000000..e7c5431 --- /dev/null +++ b/cmd/chatlog/cmd_dumpmemory.go @@ -0,0 +1,146 @@ +package chatlog + +import ( + "archive/zip" + "fmt" + "io" + "os" + "path/filepath" + "runtime" + "time" + + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + + "github.com/sjzar/chatlog/internal/wechat" + "github.com/sjzar/chatlog/internal/wechat/key/darwin/glance" +) + +func init() { + rootCmd.AddCommand(dumpmemoryCmd) +} + +var dumpmemoryCmd = &cobra.Command{ + Use: "dumpmemory", + Short: "dump memory", + Run: func(cmd *cobra.Command, args []string) { + if runtime.GOOS != "darwin" { + log.Info().Msg("dump memory only support macOS") + } + + session := time.Now().Format("20060102150405") + + dir, err := os.Getwd() + if err != nil { + log.Fatal().Err(err).Msg("get current directory failed") + return + } + log.Info().Msgf("current directory: %s", dir) + + // step 1. check pid + if err = wechat.Load(); err != nil { + log.Fatal().Err(err).Msg("load wechat failed") + return + } + accounts := wechat.GetAccounts() + if len(accounts) == 0 { + log.Fatal().Msg("no wechat account found") + return + } + + log.Info().Msgf("found %d wechat account", len(accounts)) + for i, a := range accounts { + log.Info().Msgf("%d. %s %d %s", i, a.FullVersion, a.PID, a.DataDir) + } + + // step 2. dump memory + account := accounts[0] + file := fmt.Sprintf("wechat_%s_%d_%s.bin", account.FullVersion, account.PID, session) + path := filepath.Join(dir, file) + log.Info().Msgf("dumping memory to %s", path) + + g := glance.NewGlance(account.PID) + b, err := g.Read() + if err != nil { + log.Fatal().Err(err).Msg("read memory failed") + return + } + + if err = os.WriteFile(path, b, 0644); err != nil { + log.Fatal().Err(err).Msg("write memory failed") + return + } + + log.Info().Msg("dump memory success") + + // step 3. copy encrypted database file + dbFile := "db_storage/session/session.db" + if account.Version == 3 { + dbFile = "Session/session_new.db" + } + from := filepath.Join(account.DataDir, dbFile) + to := filepath.Join(dir, fmt.Sprintf("wechat_%s_%d_session.db", account.FullVersion, account.PID)) + + log.Info().Msgf("copying %s to %s", from, to) + b, err = os.ReadFile(from) + if err != nil { + log.Fatal().Err(err).Msg("read session.db failed") + return + } + if err = os.WriteFile(to, b, 0644); err != nil { + log.Fatal().Err(err).Msg("write session.db failed") + return + } + log.Info().Msg("copy session.db success") + + // step 4. package + zipFile := fmt.Sprintf("wechat_%s_%d_%s.zip", account.FullVersion, account.PID, session) + zipPath := filepath.Join(dir, zipFile) + log.Info().Msgf("packaging to %s", zipPath) + + zf, err := os.Create(zipPath) + if err != nil { + log.Fatal().Err(err).Msg("create zip file failed") + return + } + defer zf.Close() + + zw := zip.NewWriter(zf) + + for _, file := range []string{file, to} { + f, err := os.Open(file) + if err != nil { + log.Fatal().Err(err).Msg("open file failed") + return + } + defer f.Close() + info, err := f.Stat() + if err != nil { + log.Fatal().Err(err).Msg("get file info failed") + return + } + header, err := zip.FileInfoHeader(info) + if err != nil { + log.Fatal().Err(err).Msg("create zip file info header failed") + return + } + header.Name = filepath.Base(file) + header.Method = zip.Deflate + writer, err := zw.CreateHeader(header) + if err != nil { + log.Fatal().Err(err).Msg("create zip file header failed") + return + } + if _, err = io.Copy(writer, f); err != nil { + log.Fatal().Err(err).Msg("copy file to zip failed") + return + } + } + if err = zw.Close(); err != nil { + log.Fatal().Err(err).Msg("close zip writer failed") + return + } + + log.Info().Msgf("package success, please send %s to developer", zipPath) + }, +} diff --git a/cmd/chatlog/cmd_key.go b/cmd/chatlog/cmd_key.go new file mode 100644 index 0000000..54c8c03 --- /dev/null +++ b/cmd/chatlog/cmd_key.go @@ -0,0 +1,36 @@ +package chatlog + +import ( + "fmt" + + "github.com/sjzar/chatlog/internal/chatlog" + + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(keyCmd) + keyCmd.Flags().IntVarP(&keyPID, "pid", "p", 0, "pid") + keyCmd.Flags().BoolVarP(&keyForce, "force", "f", false, "force") + keyCmd.Flags().BoolVarP(&keyShowXorKey, "xor-key", "x", false, "show xor key") +} + +var ( + keyPID int + keyForce bool + keyShowXorKey bool +) +var keyCmd = &cobra.Command{ + Use: "key", + Short: "key", + Run: func(cmd *cobra.Command, args []string) { + m := chatlog.New() + ret, err := m.CommandKey("", keyPID, keyForce, keyShowXorKey) + if err != nil { + log.Err(err).Msg("failed to get key") + return + } + fmt.Println(ret) + }, +} diff --git a/cmd/chatlog/cmd_server.go b/cmd/chatlog/cmd_server.go new file mode 100644 index 0000000..c606f62 --- /dev/null +++ b/cmd/chatlog/cmd_server.go @@ -0,0 +1,78 @@ +package chatlog + +import ( + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + + "github.com/sjzar/chatlog/internal/chatlog" +) + +func init() { + rootCmd.AddCommand(serverCmd) + serverCmd.PersistentPreRun = initLog + serverCmd.PersistentFlags().BoolVar(&Debug, "debug", false, "debug") + serverCmd.Flags().StringVarP(&serverAddr, "addr", "a", "", "server address") + serverCmd.Flags().StringVarP(&serverPlatform, "platform", "p", "", "platform") + serverCmd.Flags().IntVarP(&serverVer, "version", "v", 0, "version") + serverCmd.Flags().StringVarP(&serverDataDir, "data-dir", "d", "", "data dir") + serverCmd.Flags().StringVarP(&serverDataKey, "data-key", "k", "", "data key") + serverCmd.Flags().StringVarP(&serverImgKey, "img-key", "i", "", "img key") + serverCmd.Flags().StringVarP(&serverWorkDir, "work-dir", "w", "", "work dir") + serverCmd.Flags().BoolVarP(&serverAutoDecrypt, "auto-decrypt", "", false, "auto decrypt") +} + +var ( + serverAddr string + serverDataDir string + serverDataKey string + serverImgKey string + serverWorkDir string + serverPlatform string + serverVer int + serverAutoDecrypt bool +) + +var serverCmd = &cobra.Command{ + Use: "server", + Short: "Start HTTP server", + Run: func(cmd *cobra.Command, args []string) { + + cmdConf := getServerConfig() + log.Info().Msgf("server cmd config: %+v", cmdConf) + + m := chatlog.New() + if err := m.CommandHTTPServer("", cmdConf); err != nil { + log.Err(err).Msg("failed to start server") + return + } + }, +} + +func getServerConfig() map[string]any { + cmdConf := make(map[string]any) + if len(serverAddr) != 0 { + cmdConf["http_addr"] = serverAddr + } + if len(serverDataDir) != 0 { + cmdConf["data_dir"] = serverDataDir + } + if len(serverDataKey) != 0 { + cmdConf["data_key"] = serverDataKey + } + if len(serverImgKey) != 0 { + cmdConf["img_key"] = serverImgKey + } + if len(serverWorkDir) != 0 { + cmdConf["work_dir"] = serverWorkDir + } + if len(serverPlatform) != 0 { + cmdConf["platform"] = serverPlatform + } + if serverVer != 0 { + cmdConf["version"] = serverVer + } + if serverAutoDecrypt { + cmdConf["auto_decrypt"] = true + } + return cmdConf +} diff --git a/cmd/chatlog/cmd_version.go b/cmd/chatlog/cmd_version.go new file mode 100644 index 0000000..1af2f13 --- /dev/null +++ b/cmd/chatlog/cmd_version.go @@ -0,0 +1,27 @@ +package chatlog + +import ( + "fmt" + + "github.com/sjzar/chatlog/pkg/version" + + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(versionCmd) + versionCmd.Flags().BoolVarP(&versionM, "module", "m", false, "module version information") +} + +var versionM bool +var versionCmd = &cobra.Command{ + Use: "version [-m]", + Short: "Show the version of chatlog", + Run: func(cmd *cobra.Command, args []string) { + if versionM { + fmt.Println(version.GetMore(true)) + } else { + fmt.Printf("chatlog %s\n", version.GetMore(false)) + } + }, +} diff --git a/cmd/chatlog/log.go b/cmd/chatlog/log.go new file mode 100644 index 0000000..9b41e81 --- /dev/null +++ b/cmd/chatlog/log.go @@ -0,0 +1,45 @@ +package chatlog + +import ( + "io" + "os" + "path/filepath" + "time" + + "github.com/sjzar/chatlog/pkg/util" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var Debug bool + +func initLog(cmd *cobra.Command, args []string) { + zerolog.SetGlobalLevel(zerolog.InfoLevel) + + if Debug { + zerolog.SetGlobalLevel(zerolog.DebugLevel) + } + + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}) +} + +func initTuiLog(cmd *cobra.Command, args []string) { + logOutput := io.Discard + + debug, _ := cmd.Flags().GetBool("debug") + if debug { + logpath := util.DefaultWorkDir("") + util.PrepareDir(logpath) + logFD, err := os.OpenFile(filepath.Join(logpath, "chatlog.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, os.ModePerm) + if err != nil { + panic(err) + } + logOutput = logFD + } + + log.Logger = log.Output(zerolog.ConsoleWriter{Out: logOutput, NoColor: true, TimeFormat: time.RFC3339}) + logrus.SetOutput(logOutput) +} diff --git a/cmd/chatlog/root.go b/cmd/chatlog/root.go new file mode 100644 index 0000000..da88bcb --- /dev/null +++ b/cmd/chatlog/root.go @@ -0,0 +1,42 @@ +package chatlog + +import ( + "github.com/sjzar/chatlog/internal/chatlog" + + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +func init() { + // windows only + cobra.MousetrapHelpText = "" + + rootCmd.PersistentFlags().BoolVar(&Debug, "debug", false, "debug") + rootCmd.PersistentPreRun = initLog +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + log.Err(err).Msg("command execution failed") + } +} + +var rootCmd = &cobra.Command{ + Use: "chatlog", + Short: "chatlog", + Long: `chatlog`, + Example: `chatlog`, + Args: cobra.MinimumNArgs(0), + CompletionOptions: cobra.CompletionOptions{ + HiddenDefaultCmd: true, + }, + PreRun: initTuiLog, + Run: Root, +} + +func Root(cmd *cobra.Command, args []string) { + m := chatlog.New() + if err := m.Run(""); err != nil { + log.Err(err).Msg("failed to run chatlog instance") + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9733ca3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,39 @@ +version: '3.8' + +services: + chatlog: + image: sjzar/chatlog:latest + restart: unless-stopped + ports: + # 将主机的 5030 端口映射到 chatlog 容器的 5030 端口 + # 你可以根据需要修改主机端口,例如 "8080:5030" + - "5030:5030" + environment: + - TZ=Asia/Shanghai + # # 微信平台类型,可选:windows, darwin + # - CHATLOG_PLATFORM=${CHATLOG_PLATFORM} + # # 微信版本,可选:3, 4 + # - CHATLOG_VERSION=${CHATLOG_VERSION} + # # 微信数据密钥 + # - CHATLOG_DATA_KEY=${CHATLOG_DATA_KEY} + # # 微信图片密钥 + # - CHATLOG_IMG_KEY=${CHATLOG_IMG_KEY} + # # 服务地址 + # - CHATLOG_HTTP_ADDR=${CHATLOG_HTTP_ADDR} + # # 是否自动解密 + # - CHATLOG_AUTO_DECRYPT=${CHATLOG_AUTO_DECRYPT} + # 数据目录 + - CHATLOG_DATA_DIR=/app/data + # 工作目录 + - CHATLOG_WORK_DIR=/app/work + volumes: + # 数据目录,微信的本地数据目录 + # 请修改左侧的 /path/to/your/wechat/data 为实际的微信数据目录 + # 例如 "/Users/sarv/Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files/wxid_xxx:/app/data" + - "/path/to/your/wechat/data:/app/data" + # 工作目录,用于保存已解密的数据库文件 + - "work-dir:/app/work" + +volumes: + work-dir: + driver: local \ No newline at end of file diff --git a/docs/docker.md b/docs/docker.md new file mode 100644 index 0000000..21c13a8 --- /dev/null +++ b/docs/docker.md @@ -0,0 +1,357 @@ +# Docker 部署指南 + +## 目录 +- [Docker 部署指南](#docker-部署指南) + - [目录](#目录) + - [部署准备](#部署准备) + - [获取微信密钥](#获取微信密钥) + - [定位微信数据目录](#定位微信数据目录) + - [Docker 镜像获取](#docker-镜像获取) + - [部署方式](#部署方式) + - [Docker Run 方式](#docker-run-方式) + - [Docker Compose 方式](#docker-compose-方式) + - [环境变量配置](#环境变量配置) + - [数据目录挂载](#数据目录挂载) + - [微信数据目录](#微信数据目录) + - [工作目录](#工作目录) + - [远程同步部署](#远程同步部署) + - [配置指南](#配置指南) + - [部署注意事项](#部署注意事项) + - [部署验证](#部署验证) + - [常见问题](#常见问题) + - [1. 容器启动失败](#1-容器启动失败) + - [2. 无法访问 HTTP 服务](#2-无法访问-http-服务) + - [3. 数据目录权限问题](#3-数据目录权限问题) + - [4. 密钥格式错误](#4-密钥格式错误) + - [5. 微信版本检测失败](#5-微信版本检测失败) + - [6. 端口冲突](#6-端口冲突) + +## 部署准备 + +由于 Docker 容器运行环境与宿主机隔离,无法直接获取微信进程密钥,因此需要预先在宿主机上获取密钥信息。 + +### 获取微信密钥 + +在宿主机上运行 chatlog 获取密钥信息: + +```shell +# 下载并运行 chatlog +$ chatlog key + +# 输出示例 +Data Key: [c0163e***ac3dc6] +Image Key: [38636***653361] +``` + +> 💡 **提示**: +> - macOS 用户需要临时关闭 SIP 才能获取密钥,详见 [macOS 版本说明](../README.md#macos-版本说明) + +### 定位微信数据目录 + +根据不同操作系统,微信数据目录位置如下: + +**Windows 系统**: +``` +# 微信 3.x 版本 +C:\Users\{用户名}\Documents\WeChat Files\{微信ID} + +# 微信 4.x 版本 +C:\Users\{用户名}\Documents\xwechat_files\{微信ID} +``` + +**macOS 系统**: +``` +# 微信 3.x 版本 +/Users/{用户名}/Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/{版本号}/{微信ID} + +# 微信 4.x 版本 +/Users/{用户名}/Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files/{微信ID} +``` + +## Docker 镜像获取 + +chatlog 提供了两个镜像源: + +**Docker Hub**: +```shell +docker pull sjzar/chatlog:latest +``` + +**GitHub Container Registry (ghcr)**: +```shell +docker pull ghcr.io/sjzar/chatlog:latest +``` + +> 💡 **镜像地址**: +> - Docker Hub: https://hub.docker.com/r/sjzar/chatlog +> - GitHub Container Registry: https://ghcr.io/sjzar/chatlog + +## 部署方式 + +### Docker Run 方式 + +**基础部署**: +```shell +docker run -d \ + --name chatlog \ + -p 5030:5030 \ + -v /path/to/your/wechat/data:/app/data \ + sjzar/chatlog:latest +``` + +> 这种部署方式依赖于数据目录下的 chatlog.json 文件作为配置,通过 chatlog 获取密钥时将自动更新 chatlog.json 文件 + +**完整配置示例**: +```shell +docker run -d \ + --name chatlog \ + -p 5030:5030 \ + -e TZ=Asia/Shanghai \ + -e CHATLOG_PLATFORM=darwin \ + -e CHATLOG_VERSION=4 \ + -e CHATLOG_DATA_KEY="your-data-key" \ + -e CHATLOG_IMG_KEY="your-img-key" \ + -e CHATLOG_AUTO_DECRYPT=true \ + -e CHATLOG_HTTP_ADDR=0.0.0.0:5030 \ + -e CHATLOG_DATA_DIR=/app/data \ + -e CHATLOG_WORK_DIR=/app/work \ + -v /path/to/your/wechat/data:/app/data \ + -v /path/to/work:/app/work \ + --restart unless-stopped \ + sjzar/chatlog:latest +``` + +### Docker Compose 方式 + +**1. 创建 docker-compose.yml 文件** + +```yaml +version: '3.8' + +services: + chatlog: + image: sjzar/chatlog:latest + restart: unless-stopped + ports: + - "5030:5030" # 可修改主机端口,如 "8080:5030" + environment: + - PUID=1000 + - PGID=1000 + - TZ=Asia/Shanghai + # 微信平台类型,可选:windows, darwin + - CHATLOG_PLATFORM=darwin + # 微信版本,可选:3, 4 + - CHATLOG_VERSION=4 + # 微信数据密钥 + - CHATLOG_DATA_KEY=your-data-key + # 微信图片密钥 + - CHATLOG_IMG_KEY=your-img-key + # 是否自动解密 + - CHATLOG_AUTO_DECRYPT=true + # 服务地址 + - CHATLOG_HTTP_ADDR=0.0.0.0:5030 + # 数据目录 + - CHATLOG_DATA_DIR=/app/data + # 工作目录 + - CHATLOG_WORK_DIR=/app/work + volumes: + # 微信数据目录挂载 + - "/path/to/your/wechat/data:/app/data" + # 工作目录挂载 + - "work-dir:/app/work" + +volumes: + work-dir: + driver: local +``` + +**2. 启动服务** + +```shell +# 启动服务 +docker-compose up -d + +# 查看服务状态 +docker-compose ps + +# 查看服务日志 +docker-compose logs chatlog + +# 停止服务 +docker-compose down +``` + +## 环境变量配置 + +| 变量名 | 说明 | 默认值 | 示例 | +|--------|------|--------|------| +| `PUID` | 用户 ID | `1000` | `1000` | +| `PGID` | 用户组 ID | `1000` | `1000` | +| `TZ` | 时区设置 | `UTC` | `Asia/Shanghai` | +| `CHATLOG_PLATFORM` | 微信平台类型 | **必填** | `windows`, `darwin` | +| `CHATLOG_VERSION` | 微信版本 | **必填** | `3`, `4` | +| `CHATLOG_DATA_KEY` | 微信数据密钥 | **必填** | `c0163e***ac3dc6` | +| `CHATLOG_IMG_KEY` | 微信图片密钥 | 可选 | `38636***653361` | +| `CHATLOG_HTTP_ADDR` | HTTP 服务监听地址 | `0.0.0.0:5030` | `0.0.0.0:8080` | +| `CHATLOG_AUTO_DECRYPT` | 是否自动解密 | `false` | `true`, `false` | +| `CHATLOG_DATA_DIR` | 数据目录路径 | `/app/data` | `/app/data` | +| `CHATLOG_WORK_DIR` | 工作目录路径 | `/app/work` | `/app/work` | + +## 数据目录挂载 + +### 微信数据目录 + +**Windows 示例**: +```shell +# 微信 4.x 版本 +-v "/c/Users/username/Documents/xwechat_files/wxid_xxx:/app/data" + +# 微信 3.x 版本 +-v "/c/Users/username/Documents/WeChat\ Files/wxid_xxx:/app/data" +``` + +**macOS 示例**: +```shell +# 微信 4.x 版本 +-v "/Users/username/Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files/wxid_xxx:/app/data" + +# 微信 3.x 版本 +-v "/Users/username/Library/Containers/com.tencent.xinWeChat/Data/Library/Application\ Support/com.tencent.xinWeChat/2.0b4.0.9:/app/data" +``` + +### 工作目录 + +工作目录用于存放解密后的数据库文件,可以使用以下两种方式: + +**本地路径方式**: +```shell +-v "/path/to/local/work:/app/work" +``` + +**命名卷方式**: +```shell +-v "chatlog-work:/app/work" +``` + + +## 远程同步部署 + +对于需要将 chatlog 服务与微信客户端分离部署的场景,可以通过文件同步工具将微信数据同步到远程服务器,然后在远程服务器上运行 chatlog 服务。这种方式具有以下优势: + +- **解耦部署**:微信客户端和 chatlog 服务可以运行在不同的设备上 +- **灵活性**:可以在 NAS、VPS 等服务器上统一管理聊天数据 +- **安全性**:避免在个人电脑上长期运行服务 + +文件同步工具这里不做过多推荐,个人使用 [Syncthing](https://github.com/syncthing/syncthing),其他选择有 [Resilio Sync](https://www.resilio.com/sync/)、[rsync + inotify](https://github.com/RsyncProject/rsync) 等,可以按需选择。 + +#### 配置指南 + +- 本地配置: 同步数据目录(Data Dir),可设置为仅发送;在首次完整同步文件后,建议将 "rescanIntervalS" 设置为 0,全局扫描较为耗时,且扫描过程中会暂停同步 +- 远程服务器配置: 设置为仅接收,同样建议将 "rescanIntervalS" 设置为 0 +- 使用 Docker / Docker Compose 启动 chatlog,将数据目录映射到容器的 `/app/data` 目录 +- 按需配置 `/app/work` 映射目录,可配置到远程服务器本地路径或命名卷 +- 启动容器后,等待首次解密完成后,即可正常请求 API 或接入 MCP 服务 + +#### 部署注意事项 + +- 千万注意数据安全!chatlog 本身未提供授权机制,一定要确保服务处于安全网络环境中。 + +通过远程同步部署,您可以在保持微信客户端正常使用的同时,将 chatlog 服务部署到更适合的环境中,实现数据处理与日常使用的分离。 + +## 部署验证 + +部署完成后,通过以下方式验证服务是否正常运行: + +**1. 检查容器状态** +```shell +docker ps | grep chatlog +``` + +**2. 查看服务日志** +```shell +docker logs chatlog +``` + +**3. 访问 HTTP API** +```shell +# 检查服务健康状态 +curl http://localhost:5030/api/v1/session + +# 查看联系人列表 +curl http://localhost:5030/api/v1/contact +``` + +**4. 访问 MCP 服务** +```shell +http://localhost:5030/mcp +``` + +**5. 访问 Web 界面** + +在浏览器中打开:http://localhost:5030 + +## 常见问题 + +### 1. 容器启动失败 + +**问题**: 容器启动后立即退出 + +**解决方案**: +- 检查密钥是否正确:`docker logs chatlog` +- 确认数据目录挂载路径是否正确 +- 检查环境变量配置是否完整 + +### 2. 无法访问 HTTP 服务 + +**问题**: 浏览器无法访问 http://localhost:5030 + +**解决方案**: +- 检查端口映射是否正确:`docker port chatlog` +- 确认防火墙是否允许 5030 端口访问 +- 检查容器内服务是否正常启动 + +### 3. 数据目录权限问题 + +**问题**: 日志显示权限不足或文件无法访问 + +**解决方案**: +```shell +# Linux/macOS 系统 +chmod -R 755 /path/to/your/wechat/data + +# 或者使用 Docker 用户权限 +docker run --user $(id -u):$(id -g) ... +``` + +### 4. 密钥格式错误 + +**问题**: 显示密钥格式不正确 + +**解决方案**: +- 确保密钥为十六进制格式,不包含方括号 +- 正确格式:`CHATLOG_DATA_KEY=c0163eac3dc6` +- 错误格式:`CHATLOG_DATA_KEY=[c0163e***ac3dc6]` + +### 5. 微信版本检测失败 + +**问题**: 无法自动检测微信版本 + +**解决方案**: +- 手动设置微信平台:`CHATLOG_PLATFORM=darwin` 或 `CHATLOG_PLATFORM=windows` +- 手动设置微信版本:`CHATLOG_VERSION=4` 或 `CHATLOG_VERSION=3` + +### 6. 端口冲突 + +**问题**: 5030 端口已被占用 + +**解决方案**: +```shell +# 使用其他端口,如 8080 +docker run -p 8080:5030 ... + +# 或在 docker-compose.yml 中修改 +ports: + - "8080:5030" +``` + +> 💡 **获取更多帮助**: 如遇到其他问题,请查看项目的 [Issues](https://github.com/sjzar/chatlog/issues) 页面或提交新的问题反馈。 diff --git a/docs/mcp.md b/docs/mcp.md new file mode 100644 index 0000000..605e304 --- /dev/null +++ b/docs/mcp.md @@ -0,0 +1,151 @@ +# MCP 集成指南 + +## 目录 +- [MCP 集成指南](#mcp-集成指南) + - [目录](#目录) + - [前期准备](#前期准备) + - [mcp-proxy](#mcp-proxy) + - [ChatWise](#chatwise) + - [Cherry Studio](#cherry-studio) + - [Claude Desktop](#claude-desktop) + - [Monica Code](#monica-code) + + +## 前期准备 + +运行 `chatlog`,完成数据解密并开启 HTTP 服务 + +### mcp-proxy +如果遇到不支持 `SSE` 的客户端,可以尝试使用 `mcp-proxy` 将 `stdio` 的请求转换为 `SSE`。 + +项目地址:https://github.com/sparfenyuk/mcp-proxy + +安装方式: +```shell +# 使用 uv 工具安装,也可参考项目文档的其他安装方式 +uv tool install mcp-proxy + +# 查询 mcp-proxy 的路径,后续可直接使用该路径 +which mcp-proxy +/Users/sarv/.local/bin/mcp-proxy +``` + +## ChatWise + +- 官网:https://chatwise.app/ +- 使用方式:MCP SSE +- 注意事项:使用 ChatWise 的 MCP 功能需要 Pro 权限 + +1. 在 `设置 - 工具` 下新建 `SSE 请求` 工具 + +![chatwise-1](https://github.com/user-attachments/assets/87e40f39-9fbc-4ff1-954a-d95548cde4c2) + +1. 在 URL 中填写 `http://127.0.0.1:5030/sse`,并勾选 `自动执行工具`,点击 `查看工具` 即可检查连接 `chatlog` 是否正常 + +![chatwise-2](https://github.com/user-attachments/assets/8f98ef18-8e6c-40e6-ae78-8cd13e411c36) + +3. 返回主页,选择支持 MCP 调用的模型,打开 `chatlog` 工具选项 + +![chatwise-3](https://github.com/user-attachments/assets/ea2aa178-5439-492b-a92f-4f4fc08828e7) + +4. 测试功能是否正常 + +![chatwise-4](https://github.com/user-attachments/assets/8f82cb53-8372-40ee-a299-c02d3399403a) + +## Cherry Studio + +- 官网:https://cherry-ai.com/ +- 使用方式:MCP SSE + +1. 在 `设置 - MCP 服务器` 下点击 `添加服务器`,输入名称为 `chatlog`,选择类型为 `服务器发送事件(sse)`,填写 URL 为 `http://127.0.0.1:5030/sse`,点击 `保存`。(注意:点击保存前不要先点击左侧的开启按钮) + +![cherry-1](https://github.com/user-attachments/assets/93fc8b0a-9d95-499e-ab6c-e22b0c96fd6a) + +2. 选择支持 MCP 调用的模型,打开 `chatlog` 工具选项 + +![cherry-2](https://github.com/user-attachments/assets/4e5bf752-2eab-4e7c-b73b-1b759d4a5f29) + +3. 测试功能是否正常 + +![cherry-3](https://github.com/user-attachments/assets/c58a019f-fd5f-4fa3-830a-e81a60f2aa6f) + +## Claude Desktop + +- 官网:https://claude.ai/download +- 使用方式:mcp-proxy +- 参考资料:https://modelcontextprotocol.io/quickstart/user#2-add-the-filesystem-mcp-server + +1. 请先参考 [mcp-proxy](#mcp-proxy) 安装 `mcp-proxy` + +2. 进入 Claude Desktop `Settings - Developer`,点击 `Edit Config` 按钮,这样会创建一个 `claude_desktop_config.json` 配置文件,并引导你编辑该文件 + +3. 编辑 `claude_desktop_config.json` 文件,配置名称为 `chatlog`,command 为 `mcp-proxy` 的路径,args 为 `http://127.0.0.1:5030/sse`,如下所示: + +```json +{ + "mcpServers": { + "chatlog": { + "command": "/Users/sarv/.local/bin/mcp-proxy", + "args": [ + "http://localhost:5030/sse" + ] + } + }, + "globalShortcut": "" +} +``` + +4. 保存 `claude_desktop_config.json` 文件,重启 Claude Desktop,可以看到 `chatlog` 已经添加成功 + +![claude-1](https://github.com/user-attachments/assets/f4e872cc-e6c1-4e24-97da-266466949cdf) + +5. 测试功能是否正常 + +![claude-2](https://github.com/user-attachments/assets/832bb4d2-3639-4cbc-8b17-f4b812ea3637) + + +## Monica Code + +- 官网:https://monica.im/en/code +- 使用方式:mcp-proxy +- 参考资料:https://github.com/Monica-IM/Monica-Code/blob/main/Reference/config.md#modelcontextprotocolserver + +1. 请先参考 [mcp-proxy](#mcp-proxy) 安装 `mcp-proxy` + +2. 在 vscode 插件文件夹(`~/.vscode/extensions`)下找到 Monica Code 的目录,编辑 `config_schema.json` 文件。将 `experimental - modelContextProtocolServer` 中 `transport` 设置为如下内容: + +```json +{ + "experimental": { + "type": "object", + "title": "Experimental", + "description": "Experimental properties are subject to change.", + "properties": { + "modelContextProtocolServer": { + "type": "object", + "properties": { + "transport": { + "type": "stdio", + "command": "/Users/sarv/.local/bin/mcp-proxy", + "args": [ + "http://localhost:5030/sse" + ] + } + }, + "required": [ + "transport" + ] + } + } + } +} +``` + +3. 重启 vscode,可以看到 `chatlog` 已经添加成功 + +![monica-1](https://github.com/user-attachments/assets/8d0a96f2-ed05-48aa-a99a-06648ae1c500) + +4. 测试功能是否正常 + +![monica-2](https://github.com/user-attachments/assets/054e0a30-428a-48a6-9f31-d2596fb8f743) + diff --git a/docs/prompt.md b/docs/prompt.md new file mode 100644 index 0000000..2ff14e2 --- /dev/null +++ b/docs/prompt.md @@ -0,0 +1,70 @@ +# Prompt 指南 + +## 概述 +优秀的 `prompt` 可以极大的提高 `chatlog` 使用体验,收集了部分群友分享的 `prompt`,供大家参考。 +在处理聊天记录时,尽量选择上下文长度足够的 LLM,例如 `Gemini 2.5 Pro`、`Claude 3.5 Sonnet` 等。 +欢迎大家在 [Discussions](https://github.com/sjzar/chatlog/discussions/47) 中分享自己的使用方式,共同进步。 + + +## 群聊总结 +作者:@eyaeya + +```md +你是一个中文的群聊总结的助手,你可以为一个微信的群聊记录,提取并总结每个时间段大家在重点讨论的话题内容。 + +请帮我将 "" 在