elect 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4f7352a40c19964176a0d00add7f035879262fd8188804c1da6e1bd43b85b9d6
4
+ data.tar.gz: 46a8cce817616178a6a4c016975e6f0a494e38136c84100bc7fb0c73b2eb638a
5
+ SHA512:
6
+ metadata.gz: 20c1005f70e2e588f547cf32adee6fbe67ddb07c7b9e802ef32051cadda62f1c3f8825b3f40d6e4df10f89209f188bd05c6dbc781fcd6cafa7653881d40c2a50
7
+ data.tar.gz: 6a677ae493ac9819e0797df3bde53761d14818ab61f0fc4c182a4ed730c4d1d217965d5ad233245af2ebe99fa15dba125c8924be38fa59342d85e02881e5951d
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Frampt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,2 @@
1
+ # elect
2
+ a watermark eraser
data/elect.gemspec ADDED
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path("lib", __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require "elect/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "elect"
9
+ spec.version = Elect::VERSION
10
+ spec.authors = ["arc-v"]
11
+ spec.email = ["arc-v@example.com"]
12
+
13
+ spec.summary = "AIGC 水印干掉器 — 从代码/文档中移除 AIGC 水印块"
14
+ spec.description = <<~DESC
15
+ elect 是一个 Ruby gem,用于从代码和文档文件中检测并移除 AIGC 水印块。
16
+ 支持目录/文件级扫描,提供 dry-run、自定义扩展名、目录排除等功能。
17
+ 零运行时依赖,纯标准库实现。
18
+ DESC
19
+ spec.homepage = "https://github.com/arc-v/elect"
20
+ spec.license = "MIT"
21
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
22
+
23
+ spec.metadata["homepage_uri"] = spec.homepage
24
+ spec.metadata["source_code_uri"] = "https://github.com/arc-v/elect"
25
+
26
+ # 文件列表
27
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
28
+ Dir["lib/**/*", "exe/*", "README.md", "LICENSE", "elect.gemspec"]
29
+ end
30
+
31
+ spec.bindir = "exe"
32
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
33
+ spec.require_paths = ["lib"]
34
+
35
+ spec.add_development_dependency "bundler", "~> 2.0"
36
+ spec.add_development_dependency "rake", "~> 13.0"
37
+ spec.add_development_dependency "rspec", "~> 3.0"
38
+ end
data/exe/elect ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/elect"
5
+
6
+ Elect::CLI.run(ARGV)
@@ -0,0 +1,49 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "pattern"
5
+
6
+ module Elect
7
+ # 清理器:接收文本,移除 AIGC 水印块。
8
+ #
9
+ module Cleaner
10
+ # 从文本中移除所有水印块。
11
+ #
12
+ # 策略:
13
+ # 1. 用 Pattern::REGEX 全局替换为空串
14
+ # 2. 清理因移除产生的连续空行(>2 连续空行压缩为 2)
15
+ # 3. 清理文件首尾多余空白
16
+ #
17
+ # 返回 [清理后文本, 移除数量]
18
+ #
19
+ def self.clean(text)
20
+ n = Pattern.count(text)
21
+ return [text, 0] if n.zero?
22
+
23
+ cleaned = text.gsub(Pattern::REGEX, "")
24
+
25
+ # 压缩连续空行(保留最多 2 换行 = 1 空行),兼容 CRLF
26
+ cleaned.gsub!(/(?:\r\n|\n){3,}/, "\n\n")
27
+
28
+ # 移除首尾多余空白
29
+ cleaned.strip!
30
+
31
+ # 确保文件结尾有且仅有一个换行
32
+ cleaned << "\n" unless cleaned.empty?
33
+
34
+ [cleaned, n]
35
+ end
36
+
37
+ # 仅检测不修改,返回水印数量
38
+ #
39
+ def self.detect(text)
40
+ Pattern.count(text)
41
+ end
42
+
43
+ # 判断文本是否含水印
44
+ #
45
+ def self.watermarked?(text)
46
+ Pattern.watermarked?(text)
47
+ end
48
+ end
49
+ end
data/lib/elect/cli.rb ADDED
@@ -0,0 +1,110 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require "optparse"
5
+ require "pathname"
6
+
7
+ module Elect
8
+ # 命令行接口
9
+ #
10
+ # 用法:
11
+ # elect [选项] <文件或目录...>
12
+ #
13
+ # 选项:
14
+ # -d, --dry-run 仅检测,不修改文件
15
+ # -v, --verbose 详细输出
16
+ # -e, --exts x,y,z 自定义扩展名(逗号分隔)
17
+ # -x, --exclude a,b 排除目录名(逗号分隔)
18
+ # -h, --help 显示帮助
19
+ # -V, --version 显示版本
20
+ #
21
+ module CLI
22
+ def self.run(argv = ARGV)
23
+ opts = {
24
+ dry_run: false,
25
+ verbose: false,
26
+ exts: nil,
27
+ exclude: nil,
28
+ }
29
+
30
+ parser = build_parser(opts)
31
+ paths = parser.parse(argv)
32
+
33
+ if paths.empty?
34
+ warn parser
35
+ exit 1
36
+ end
37
+
38
+ all_results = []
39
+ paths.each do |p|
40
+ path = Pathname.new(p)
41
+ if path.directory?
42
+ results = Processor.process_dir(
43
+ path,
44
+ dry_run: opts[:dry_run],
45
+ verbose: opts[:verbose],
46
+ exts: opts[:exts],
47
+ exclude: opts[:exclude],
48
+ )
49
+ all_results.concat(results)
50
+ elsif path.file?
51
+ r = Processor.process_file(path, dry_run: opts[:dry_run], verbose: opts[:verbose])
52
+ all_results << r
53
+ else
54
+ warn "[elect] 路径不存在: #{p}"
55
+ end
56
+ end
57
+
58
+ Processor.report(all_results)
59
+
60
+ # 有错误时退出码非零
61
+ exit 1 if all_results.any? { |r| r.status == :error }
62
+ end
63
+
64
+ def self.build_parser(opts)
65
+ OptionParser.new do |o|
66
+ o.banner = <<~BANNER
67
+ elect — AIGC 水印干掉器
68
+
69
+ 用法:
70
+ elect [选项] <文件或目录...>
71
+
72
+ 示例:
73
+ elect ./src # 清理 src 目录下所有水印
74
+ elect -d ./docs # 仅检测不修改(dry-run)
75
+ elect -v main.py # 清理单个文件,详细输出
76
+ elect -e md,txt ./docs # 只扫描 md 和 txt
77
+ elect -x vendor,tmp ./src # 排除 vendor 和 tmp 目录
78
+
79
+ 选项:
80
+ BANNER
81
+
82
+ o.on("-d", "--dry-run", "仅检测水印,不修改文件") do
83
+ opts[:dry_run] = true
84
+ end
85
+
86
+ o.on("-v", "--verbose", "详细输出") do
87
+ opts[:verbose] = true
88
+ end
89
+
90
+ o.on("-e", "--exts x,y,z", Array, "自定义扫描扩展名(逗号分隔)") do |v|
91
+ opts[:exts] = v
92
+ end
93
+
94
+ o.on("-x", "--exclude a,b", Array, "排除目录名(逗号分隔)") do |v|
95
+ opts[:exclude] = v
96
+ end
97
+
98
+ o.on("-h", "--help", "显示帮助") do
99
+ puts o
100
+ exit
101
+ end
102
+
103
+ o.on("-V", "--version", "显示版本") do
104
+ puts "elect #{Elect::VERSION}"
105
+ exit
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,97 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module Elect
5
+ # 水印模式定义与匹配逻辑。
6
+ #
7
+ # AIGC 水印结构如下(YAML front-matter 风格,用三横线包裹):
8
+ #
9
+ # ---
10
+ # AIGC:
11
+ # ContentProducer: '001191110102xxxx5U9H0F10002'
12
+ # ContentPropagator: '001191110102xxxx5U9H0F10002'
13
+ # Label: '1'
14
+ # ProduceID: '7e123539-xxxx-4617-9fdb-d10422064fe9'
15
+ # PropagateID: '7e123539-xxxx-4617-9fdb-d10422064fe9'
16
+ # ReservedCode1: '718161f2-xxxx-4528-9e76-b071818e46dd'
17
+ # ReservedCode2: '718161f2-xxxx-4528-9e76-b071818e46dd'
18
+ # ---
19
+ #
20
+ # 字段值的内容是动态的,因此以结构模式匹配,而非硬编码值。
21
+ #
22
+ module Pattern
23
+ # 水印块匹配的正则。
24
+ #
25
+ # 约束:
26
+ # - 以 `---` 开头(行首)
27
+ # - 后面跟着 `AIGC:` 键
28
+ # - 包含若干缩进的子字段行
29
+ # - 以 `---` 结尾
30
+ # - 支持前后空白行
31
+ #
32
+ # 允许的子字段名(不限制值内容):
33
+ # ContentProducer / ContentPropagator / Label /
34
+ # ProduceID / PropagateID / ReservedCode1 / ReservedCode2
35
+ #
36
+ # 对非列表中字段同样宽容——只要 AIGC: dict 下有任意子键即匹配,
37
+ # 这样未来新增字段也可被自动识别。
38
+ #
39
+ # 换行兼容:用 \r?\n 同时匹配 LF 和 CRLF。
40
+ #
41
+ REGEX = /
42
+ (?:
43
+ ^---[ \t]*\r?\n
44
+ [ \t]*AIGC:[ \t]*\r?\n
45
+ (?:[ \t]+[A-Za-z_][A-Za-z0-9_]*:[ \t]*[^\r\n]*\r?\n)+
46
+ [ \t]*---[ \t]*\r?\n?
47
+ )
48
+ /x.freeze
49
+
50
+ # 匹配水印块,返回 MatchData 或 nil
51
+ #
52
+ def self.match(text)
53
+ s = sanitize(text)
54
+ REGEX.match(s)
55
+ end
56
+
57
+ # 判断是否包含水印
58
+ #
59
+ def self.watermarked?(text)
60
+ s = sanitize(text)
61
+ REGEX.match?(s)
62
+ end
63
+
64
+ # 统计水印块数量
65
+ #
66
+ def self.count(text)
67
+ s = sanitize(text)
68
+ s.scan(REGEX).size
69
+ end
70
+
71
+ # 提取所有水印块的文本(供调试用)
72
+ #
73
+ def self.extract(text)
74
+ s = sanitize(text)
75
+ s.scan(REGEX)
76
+ end
77
+
78
+ # 对非 UTF-8 合法字节做替换,避免 scan/match 抛 ArgumentError。
79
+ # 如果传入的 String 编码不是 utf-8,先强制转码再清洗非法字节。
80
+ #
81
+ def self.sanitize(text)
82
+ return text if text.encoding == Encoding::UTF_8 && text.valid_encoding?
83
+ if text.encoding != Encoding::UTF_8
84
+ begin
85
+ text = text.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
86
+ return text if text.valid_encoding?
87
+ rescue Encoding::UndefinedConversionError, Encoding::ConverterNotFoundError
88
+ # 走 fallback
89
+ end
90
+ end
91
+ # encoding 是 utf-8 但有非法字节,或转码失败:直接 force + clean
92
+ text.dup.force_encoding(Encoding::UTF_8).encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "?")
93
+ end
94
+
95
+ private_class_method :sanitize
96
+ end
97
+ end
@@ -0,0 +1,140 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "cleaner"
5
+ require "pathname"
6
+
7
+ module Elect
8
+ # 文件与目录扫描处理器。
9
+ #
10
+ # 职责:
11
+ # - 遍历目录树,找出所有文本类文件
12
+ # - 逐文件读取、检测水印、按选项清理或仅报告
13
+ # - 输出统计信息
14
+ #
15
+ module Processor
16
+ # 默认扫描的文件扩展名
17
+ #
18
+ DEFAULT_EXTS = %w[
19
+ .md .txt .py .rb .js .ts .tsx .jsx .java .go .rs
20
+ .c .h .cpp .hpp .cc .cs .php .swift .kt .scala
21
+ .html .css .scss .less .vue .svelte
22
+ .json .yaml .yml .toml .xml .sql .sh .bat .ps1
23
+ .tex .rst .adoc .org .csv .tsv .ini .conf .cfg
24
+ ].freeze
25
+
26
+ # 文件结果
27
+ #
28
+ Result = Struct.new(:path, :removed, :status, keyword_init: true)
29
+
30
+ # 扫描并处理单个文件。
31
+ #
32
+ # 参数:
33
+ # path - 文件路径(String 或 Pathname)
34
+ # dry_run - true 时仅检测不改写文件
35
+ # verbose - true 时输出详细日志
36
+ #
37
+ # 返回: Result
38
+ #
39
+ def self.process_file(path, dry_run: false, verbose: false)
40
+ p = Pathname.new(path)
41
+ result = Result.new(path: p.to_s, removed: 0, status: :skip)
42
+
43
+ unless p.file?
44
+ result.status = :not_found
45
+ return result
46
+ end
47
+
48
+ # 统一走二进制读取 + 编码强制转换,避免非 UTF-8 文件触发 ArgumentError
49
+ raw = begin
50
+ data = File.binread(p.to_s)
51
+ data = data.force_encoding(Encoding::UTF_8)
52
+ unless data.valid_encoding?
53
+ data = data.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "?")
54
+ end
55
+ data
56
+ rescue => e
57
+ warn "[elect] 跳过 #{p}: #{e.message}" if verbose
58
+ result.status = :error
59
+ return result
60
+ end
61
+
62
+ n = Cleaner.detect(raw)
63
+ if n.zero?
64
+ result.status = :clean
65
+ return result
66
+ end
67
+
68
+ if dry_run
69
+ result.removed = n
70
+ result.status = :found
71
+ puts " [DRY] #{p} — 发现 #{n} 处水印"
72
+ return result
73
+ end
74
+
75
+ cleaned, removed = Cleaner.clean(raw)
76
+ p.write(cleaned, encoding: "utf-8")
77
+ result.removed = removed
78
+ result.status = :cleaned
79
+ puts " [OK] #{p} — 移除 #{removed} 处水印" if verbose
80
+ result
81
+ end
82
+
83
+ # 扫描并处理目录。
84
+ #
85
+ # 参数:
86
+ # dir - 目录路径
87
+ # dry_run - true 时仅检测不修改
88
+ # verbose - true 时输出详细日志
89
+ # exts - 自定义扩展名数组(nil 用 DEFAULT_EXTS)
90
+ # exclude - 排除的目录名数组(如 ["node_modules", ".git"])
91
+ #
92
+ # 返回: [Result, ...]
93
+ #
94
+ def self.process_dir(dir, dry_run: false, verbose: false, exts: nil, exclude: nil)
95
+ d = Pathname.new(dir)
96
+ exts = (exts || DEFAULT_EXTS).map { |e| e.start_with?(".") ? e : ".#{e}" }
97
+ exclude_set = (exclude || %w[node_modules .git .svn __pycache__ dist build]).to_set
98
+
99
+ results = []
100
+ d.find do |path|
101
+ next if path == d
102
+ if path.directory?
103
+ next if exclude_set.include?(path.basename.to_s)
104
+ next
105
+ end
106
+
107
+ next unless exts.include?(path.extname.downcase)
108
+
109
+ r = process_file(path, dry_run: dry_run, verbose: verbose)
110
+ results << r
111
+ end
112
+
113
+ results
114
+ end
115
+
116
+ # 打印汇总报告
117
+ #
118
+ def self.report(results)
119
+ total = results.size
120
+ found = results.count { |r| r.status == :found }
121
+ cleaned = results.count { |r| r.status == :cleaned }
122
+ clean = results.count { |r| r.status == :clean }
123
+ skipped = results.count { |r| r.status == :skip }
124
+ errors = results.count { |r| r.status == :error }
125
+ total_rm = results.sum { |r| r.removed }
126
+
127
+ puts
128
+ puts "=" * 50
129
+ puts " 扫描完毕"
130
+ puts "=" * 50
131
+ puts " 扫描文件 : #{total}"
132
+ puts " 发现水印 : #{found + cleaned} 个文件"
133
+ puts " 已清理 : #{cleaned} 个文件 (共移除 #{total_rm} 处水印)"
134
+ puts " 无水印 : #{clean} 个文件"
135
+ puts " 跳过 : #{skipped}"
136
+ puts " 错误 : #{errors}" if errors.positive?
137
+ puts "=" * 50
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,6 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module Elect
5
+ VERSION = "1.1.0"
6
+ end
data/lib/elect.rb ADDED
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "elect/version"
5
+ require_relative "elect/pattern"
6
+ require_relative "elect/cleaner"
7
+ require_relative "elect/processor"
8
+ require_relative "elect/cli"
9
+
10
+ # elect — AIGC 水印干掉器
11
+ #
12
+ # 从代码/文档文件中移除天翼星辰超级智能体等工具注入的 AIGC 水印块。
13
+ #
14
+ # 快速使用:
15
+ #
16
+ # require 'elect'
17
+ #
18
+ # # Ruby 代码中直接调用
19
+ # cleaned, n = Elect::Cleaner.clean(text)
20
+ # puts "移除 #{n} 处水印" if n > 0
21
+ #
22
+ # 命令行:
23
+ #
24
+ # elect ./src # 清理目录
25
+ # elect -d ./docs # 仅检测
26
+ # elect -v main.py # 单文件
27
+ #
28
+ module Elect
29
+ end
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: elect
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.1.0
5
+ platform: ruby
6
+ authors:
7
+ - arc-v
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: bundler
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rspec
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.0'
54
+ description: |
55
+ elect 是一个 Ruby gem,用于从代码和文档文件中检测并移除 AIGC 水印块。
56
+ 支持目录/文件级扫描,提供 dry-run、自定义扩展名、目录排除等功能。
57
+ 零运行时依赖,纯标准库实现。
58
+ email:
59
+ - arc-v@example.com
60
+ executables:
61
+ - elect
62
+ extensions: []
63
+ extra_rdoc_files: []
64
+ files:
65
+ - LICENSE
66
+ - README.md
67
+ - elect.gemspec
68
+ - exe/elect
69
+ - lib/elect.rb
70
+ - lib/elect/cleaner.rb
71
+ - lib/elect/cli.rb
72
+ - lib/elect/pattern.rb
73
+ - lib/elect/processor.rb
74
+ - lib/elect/version.rb
75
+ homepage: https://github.com/arc-v/elect
76
+ licenses:
77
+ - MIT
78
+ metadata:
79
+ homepage_uri: https://github.com/arc-v/elect
80
+ source_code_uri: https://github.com/arc-v/elect
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 2.5.0
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubygems_version: 4.0.19
96
+ specification_version: 4
97
+ summary: AIGC 水印干掉器 — 从代码/文档中移除 AIGC 水印块
98
+ test_files: []