灵通问道 (LingTongAsk) 代码审计报告
审计日期: 2026-04-05
审计范围: src/ 全部源码(17个模块,~5500行),scripts/,tests/
审计维度: 业务逻辑 · 安全漏洞 · 代码质量 · 合规性 · 架构风险
执行摘要
| 维度 | 风险等级 | 发现数 | 概述 |
|---|---|---|---|
| 🔴 安全漏洞 | 高 | 6 | API密钥硬编码路径、XML注入、无输入校验、Token明文存储 |
| 🟠 业务逻辑 | 中高 | 8 | 反序列化崩溃、Episode.duration逻辑反转、大量死代码/占位实现 |
| 🟡 代码质量 | 中 | 12 | 4个未定义名称(F821)、125+条ruff警告、大量未使用导入 |
| 🟡 架构风险 | 中 | 7 | 紧耦合LingFlow、无持久层、CLI巨石文件、配置散乱 |
| 🔵 合规性 | 低 | 3 | 个人数据收集无隐私声明、自动回复可能违反平台规则 |
整体评级: ⚠️ 可用于内部原型开发,但不可用于生产环境。 需修复安全问题和核心逻辑缺陷后方可进入下一阶段。
一、安全漏洞(🔴 高风险)
S-1. API密钥通过硬编码路径跨项目读取 ✅ 已修复
- 文件:
src/cli/main.py:890(check_tts命令) - 问题: 代码从
~/zhineng-knowledge-system/.env读取 API key,而非从自身项目的.env读取 - 影响: 违反最小权限原则;如果其他项目被部署到不同环境,密钥将无法找到或泄露
- 修复: 已替换为项目相对
.env加载 (python-dotenv) - 建议: 统一使用
python-dotenv加载项目自身的.env文件
S-2. XML 注入(用户输入未转义) ✅ 已修复
- 文件:
src/publisher/rss.py:44-55,rss.py:67-74 - 问题:
episode.title、episode.description、podcast.name等用户输入直接拼入 XML 字符串,未经转义 - 修复: 已使用
xml.sax.saxutils.escape()转义所有用户输入,published_at添加 None 防护 - 影响: 攻击者可通过构造特殊标题注入任意 XML,破坏 RSS feed 结构
- 建议: 使用
xml.etree.ElementTree或xml.sax.saxutils.escape()转义
S-3. Token 明文存储在本地文件 ⚠️ 部分修复
- 文件:
src/publisher/bilibili.py:189-200,src/publisher/wechat_mp.py:114-124 - 问题: OAuth access_token 和 refresh_token 以明文 JSON 存储
- 修复: 文件权限已加固为
0o600(仅文件所有者可读写) - 剩余风险: Token 仍为明文内容,建议后续使用
keyring库加密存储
S-4. 微信API URL 中拼接 Token
- 文件:
src/publisher/wechat_mp.py:165-167,wechat_mp.py:267,wechat_mp.py:301,wechat_mp.py:339 - 问题: access_token 直接拼接在 URL query string 中
- 影响: Token 可能出现在访问日志、代理服务器日志、浏览器历史中
- 建议: 通过 HTTP Header 或 POST body 传递 token
S-5. 用户输入未校验即传入LLM
- 文件:
src/generator/script.py、src/generator/topic.py - 问题: CLI 中用户提供的 topic 字符串直接拼接到 prompt 模板中,无长度限制和内容过滤
- 影响: Prompt injection 攻击者可操纵 LLM 输出恶意内容或泄露系统提示
- 建议: 添加输入长度上限(如200字)、敏感词过滤、prompt 模板使用参数化
S-6. 文件路径由用户输入构建(Path Traversal风险) ✅ 已修复
- 文件:
src/cli/main.py多处、src/optimizer.py:213-218 - 问题:
episode_id等 CLI 参数直接用于构建文件路径 - 修复: 添加
_validate_episode_id()函数(正则^[a-zA-Z0-9_-]+$),CLI命令和optimizer质量检查均添加校验,JSON二级注入同步校验 - 建议: ~~对 episode_id 进行白名单校验~~ 已实施
二、业务逻辑缺陷(🟠 中高风险)
B-1. Episode JSON 反序列化必定失败 ✅ 已修复
- 文件:
src/cli/main.py:961-968(_load_script函数) - 问题:
Episode(**data)从 JSON 加载,但 Episode 是复杂 dataclass,包含List[Segment]、datetime、EpisodeStatus枚举等字段。JSON dict 无法直接映射到这些类型 - 修复: 已改为手动反序列化,逐字段构造
Segment对象并正确转换类型 - 建议: 实现自定义
from_dict()类方法,或使用dacite/marshmallow库
B-2. Episode.duration 属性逻辑可能错误 ✅ 已修复
- 文件:
src/core/episode.py:54-59 - 问题:
durationproperty 在audio_path为 None 时直接返回 None,丢弃 segment 时长数据 - 修复: 改为始终从 segment 累加计算,
total > 0时返回total,否则返回None
B-3. 脚本模板用重复内容填充字数
- 文件:
src/generator/script.py(_generate_from_template方法) - 问题: 当目标字数未达到时,硬编码循环重复添加 "这需要我们深入理解并实践运用。" 等句子
- 影响: 生成质量低下,内容重复
- 建议: 使用 LingFlow 动态生成补充内容,或调整模板使其达到目标字数
B-4. 7个平台发布器全部是 Stub
- 文件:
src/publisher/platform.py:86-280 - 问题: 所有 7 个
PlatformPublisher子类(微信、B站、喜马拉雅、小宇宙、抖音、快手、小红书)的方法体只做logger.info()然后返回模拟数据 - 影响:
multi-publishCLI 命令始终报告"成功"但实际什么都没做。用户可能以为已发布 - 建议: 至少在
publish()返回前添加logger.warning("当前为模拟模式,未实际上传")提醒用户
B-5. schedule_publish 串行阻塞等待
- 文件:
src/publisher/platform.py:406-407 - 问题:
await asyncio.sleep(wait_seconds)会阻塞整个任务队列。如果第一个任务定时在 3 天后,后续所有任务都要等 3 天 - 建议: 使用
asyncio.create_task+asyncio.wait并发执行
B-6. 脚本生成 ID 可能冲突 ✅ 已修复
- 文件:
src/generator/script.py(_generate_id方法) - 问题: 使用
datetime.now().strftime('%Y%m%d%H%M')生成 ID,并发或同分钟内会冲突 - 修复: 已添加
uuid.uuid4().hex[:6]随机后缀;填充循环添加max_fill=100上限保护
B-7. Podcast.total_episodes 只计算已发布
- 文件:
src/core/episode.py - 问题:
total_episodesproperty 只计数EpisodeStatus.PUBLISHED状态的 episode,而len(podcast.episodes)计数全部。这导致测试中的混淆 - 建议: 文档中明确说明,或重命名为
published_episode_count
B-8. 知识库文件搜索无分页
- 文件:
src/knowledge/base.py:97-109 - 问题:
FileKnowledgeBase.search()使用root_path.rglob("*.md")遍历所有文件并全量读取内容 - 影响: 如果知识库目录有大量文件,搜索会很慢且消耗大量内存
- 建议: 增加分页、使用缓存索引
三、代码质量问题(🟡 中风险)
Q-1. 4个未定义名称错误(运行时崩溃) ✅ 已修复
|| 文件 | 行号 | 未定义名 | 修复方式 |
|------|------|----------|----------|
| src/content/pipeline.py | 128 | edge_tts_make_mp3 | 改用 edge_tts.Communicate(...).save() |
| src/generator/video.py | 214 | Image (PIL) | 添加 TYPE_CHECKING guard |
| src/generator/enhanced_video.py | 504 | Image (PIL) | 添加 TYPE_CHECKING guard |
| src/generator/multimodal_pipeline.py | 468 | create_smart_video_composer | 改用 SmartVideoComposer(...) 构造 |
- 原因: 这些名称在条件导入的
try块内定义,但在except之后的代码路径中被引用 - 建议: 在
except块中将这些名称设为None,并在使用前检查可用性
Q-2. 125+ 条 Ruff 警告
- 分布: 项目级诊断显示 4 个错误 + 125 条警告
- 高频问题:
F401未使用导入 (~40处):asyncio,Path,datetime,Counter,field,hashlib等F541f-string无占位符 (~15处):f"✅ 群发任务提交成功"等F841变量赋值后未使用 (~10处):url,params,config,file_md5,endpoint等- 建议: 运行
ruff check --fix src/自动修复大部分
Q-3. 巨型CLI文件
- 文件:
src/cli/main.py(~987行) - 问题: 所有CLI命令集中在一个文件中,包含 15+ 个 Click 命令
- 建议: 按功能拆分为
cli/generate.py、cli/publish.py、cli/audio.py等
Q-4. 命名不一致
- 同一概念多种命名:
- 主持人:
灵通子/灵通/host/male_name - 嘉宾:
慧心子/慧心/guest/female_name - TTS引擎:
gptsovits/GPTSoVITS/gpt-sovits - 建议: 建立统一术语表,定义常量
Q-5. Python版本声明矛盾
- 文件:
pyproject.toml - 问题:
requires-python = ">=3.8"但 mypy/black 配置为 Python 3.11。代码中使用了|联合类型语法(3.10+特性) - 建议: 统一为
>=3.10或移除新语法
Q-6. 死代码
| 文件 | 说明 |
|---|---|
src/main.py |
导入 LingFlow(注意不是 lingflow),与项目其余部分使用的 lingflow 不一致。此文件从未被 CLI 或其他模块调用 |
src/audio/voice_clone.py |
CLI 工具但未注册到主 CLI 入口 |
src/content/pipeline.py |
独立脚本,不被 CLI 导入 |
src/generator/multimodal_pipeline.py |
导入了 SmartVideoComposer 但在模块级代码中使用未定义的 create_smart_video_composer |
Q-7. 异步模式不一致
FeedbackAnalyzer.analyze_comments()是async但内部全是同步操作KnowledgeBase的search()是async但FileKnowledgeBase的实现全是同步 I/O- 建议: 移除不必要的
async声明,或使用aiofiles做真正的异步 I/O
四、合规性问题(🔵 低风险)
C-1. 粉丝数据收集无隐私声明
- 文件:
src/fan_engagement/collector.py、analyzer.py - 问题: 收集用户评论、私信、用户画像(ID、互动记录、情感分析),但代码中无隐私声明、数据保留策略或用户同意机制
- 影响: 若在中国大陆运营,可能违反《个人信息保护法》(PIPL)
- 建议: 添加隐私政策配置、数据保留期限、用户数据删除接口
C-2. 自动回复可能违反平台规则
- 文件:
src/fan_engagement/responder.py - 问题:
AUTO策略下自动回复用户评论,但大多数平台(B站、微信公众号等)要求机器人回复须标明"自动回复" - 建议: 在自动回复内容中添加
[自动回复]标识
C-3. 知识库内容版权未声明
- 文件:
src/knowledge/base.py - 问题: 知识库搜索结果直接返回给用户,未标注内容来源和版权
- 建议: 在回复模板中添加 "内容来源:灵通问道知识库"
五、架构风险(🟡 中风险)
A-1. 紧耦合 LingFlow 框架
- 分布: 6个模块有
from lingflow import ...调用 - 问题: 每个模块都使用
try/except ImportError做 fallback 到 mock 实现,但这意味着: - 开发环境和生产环境行为完全不同
- Mock 输出的质量(模板填充)与真实 LingFlow 输出差距巨大
- 无法在无 LingFlow 环境下测试核心功能
- 建议: 引入抽象层(已有部分),确保核心逻辑不直接依赖 LingFlow
A-2. 无持久化层
- 问题: Episode 数据存储为 JSON 文件(
episodes/drafts/{id}_data.json),无数据库 - 影响:
- 无法高效查询历史 episode
- 并发写入可能导致数据损坏
- 文件名约定不一致(CLI期望
_data.json,实际目录用script.md) - 建议: 引入 SQLite 或至少统一的文件存储方案
A-3. 配置散乱
- 问题: 配置来源分散在 3 个地方:
- 环境变量(
.env文件) - CLI 参数
- 硬编码默认值(如
ScriptConfig、TopicConfig) - 建议: 统一使用配置管理方案(如
pydantic-settings)
A-4. 双重平台实现
- 问题: 每个平台有两组实现:
publisher/platform.py中的 stub(7个类)publisher/bilibili.py、publisher/wechat_mp.py中的真实实现- 影响: 两套代码不一致,CLI 使用 stub 版本(永远成功但不做任何事),真实实现未被集成
- 建议: 将真实实现注册到
MultiPlatformPublisher.PUBLISHERS映射中
A-5. 无统一错误处理
- 问题: 各模块的异常处理策略不一致:
- 有些模块返回空列表/None(静默失败)
- 有些模块 raise Exception
- 有些模块返回
{"error": "..."}dict - 建议: 定义项目级异常层次(
LingTongAskError基类 + 子类),统一错误报告
A-6. 测试基础设施薄弱
- 问题:
- 测试使用
sys.path.insert而非包导入 - 大部分测试是集成测试而非单元测试
- 无 CI 配置文件
- Mock 模式下测试无法验证真实功能
- 建议: 使用
pytestfixtures 和monkeypatch,添加 GitHub Actions
A-7. 发布调度器无法优雅关闭
- 文件:
src/publisher/platform.py:398-413 - 问题:
schedule_publish()中的await asyncio.sleep(wait_seconds)无法被中断。如果进程被 SIGTERM,正在等待的任务会丢失 - 建议: 使用
asyncio.Event或threading.Event实现可取消的等待
六、风险矩阵与优先级
| 编号 | 问题 | 严重性 | 修复难度 | 优先级 | 状态 |
|---|---|---|---|---|---|
| S-2 | XML注入 | 高 | 低 | P0 | ✅ 已修复 |
| S-6 | Path Traversal | 高 | 低 | P0 | ✅ 已修复 |
| Q-1 | 未定义名称(F821) | 高 | 低 | P0 | ✅ 已修复 |
| B-1 | 反序列化崩溃 | 高 | 中 | P0 | ✅ 已修复 |
| S-1 | 硬编码密钥路径 | 高 | 低 | P1 | ✅ 已修复 |
| S-3 | Token明文存储 | 高 | 中 | P1 | ⚠️ 部分修复 |
| S-5 | 无输入校验 | 高 | 中 | P1 | 未修复 |
| B-4 | 平台发布全stub | 高 | 高 | P1 | 未修复 |
| B-2 | duration逻辑错误 | 中 | 低 | P2 | ✅ 已修复 |
| B-3 | 模板填充 | 中 | 中 | P2 | 未修复 |
| Q-2 | 125+ ruff警告 | 低 | 低 | P2 | 未修复 |
| A-4 | 双重平台实现 | 中 | 中 | P2 | 未修复 |
| A-2 | 无持久化层 | 中 | 高 | P3 | 未修复 |
| A-3 | 配置散乱 | 中 | 中 | P3 | 未修复 |
| C-1 | 隐私合规 | 中 | 中 | P3 | 未修复 |
| A-1 | 紧耦合LingFlow | 中 | 高 | P4 | 未修复 |
七、快速修复清单(可立即执行)
# 1. 自动修复大部分 ruff 警告
ruff check --fix src/
ruff check --fix scripts/
# 2. XML 转义修复(rss.py)
# 将 f-string 拼接改为 xml.sax.saxutils.escape()
# 3. 输入校验(cli/main.py)
# episode_id 参数添加 regex 验证: r'^[a-z0-9_]+$'
# 4. 统一 .env 加载
# 在 cli/main.py 入口添加: from dotenv import load_dotenv; load_dotenv()
# 5. 为 F821 错误添加 fallback
# 在 except ImportError 块中添加: Image = None
# 在使用前添加: if Image is None: raise RuntimeError("需要安装 Pillow")
审计完成。以上发现基于静态分析,未进行动态渗透测试。建议在修复安全问题后,进行一轮完整的功能回归测试。