redbeacon 0.1.0__tar.gz

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.
Files changed (54) hide show
  1. redbeacon-0.1.0/LICENSE +21 -0
  2. redbeacon-0.1.0/PKG-INFO +162 -0
  3. redbeacon-0.1.0/README.md +137 -0
  4. redbeacon-0.1.0/pyproject.toml +44 -0
  5. redbeacon-0.1.0/setup.cfg +4 -0
  6. redbeacon-0.1.0/src/redbeacon/__init__.py +5 -0
  7. redbeacon-0.1.0/src/redbeacon/cli.py +191 -0
  8. redbeacon-0.1.0/src/redbeacon/config.py +102 -0
  9. redbeacon-0.1.0/src/redbeacon/config_keys.py +55 -0
  10. redbeacon-0.1.0/src/redbeacon/database.py +258 -0
  11. redbeacon-0.1.0/src/redbeacon/render_xhs_v2.py +769 -0
  12. redbeacon-0.1.0/src/redbeacon/routers/__init__.py +0 -0
  13. redbeacon-0.1.0/src/redbeacon/routers/_runtime.py +82 -0
  14. redbeacon-0.1.0/src/redbeacon/routers/accounts.py +92 -0
  15. redbeacon-0.1.0/src/redbeacon/routers/config.py +110 -0
  16. redbeacon-0.1.0/src/redbeacon/routers/content.py +114 -0
  17. redbeacon-0.1.0/src/redbeacon/routers/feishu.py +132 -0
  18. redbeacon-0.1.0/src/redbeacon/routers/generate.py +50 -0
  19. redbeacon-0.1.0/src/redbeacon/routers/login.py +146 -0
  20. redbeacon-0.1.0/src/redbeacon/routers/logs.py +18 -0
  21. redbeacon-0.1.0/src/redbeacon/routers/publish.py +25 -0
  22. redbeacon-0.1.0/src/redbeacon/routers/readiness.py +97 -0
  23. redbeacon-0.1.0/src/redbeacon/routers/status.py +39 -0
  24. redbeacon-0.1.0/src/redbeacon/routers/strategy.py +52 -0
  25. redbeacon-0.1.0/src/redbeacon/routers/topics.py +182 -0
  26. redbeacon-0.1.0/src/redbeacon/schemas.py +304 -0
  27. redbeacon-0.1.0/src/redbeacon/services/__init__.py +0 -0
  28. redbeacon-0.1.0/src/redbeacon/services/feishu_api.py +303 -0
  29. redbeacon-0.1.0/src/redbeacon/services/image_gen.py +206 -0
  30. redbeacon-0.1.0/src/redbeacon/services/mcp_manager.py +74 -0
  31. redbeacon-0.1.0/src/redbeacon/services/proxy_service.py +99 -0
  32. redbeacon-0.1.0/src/redbeacon/services/xhs/__init__.py +17 -0
  33. redbeacon-0.1.0/src/redbeacon/services/xhs/login.py +183 -0
  34. redbeacon-0.1.0/src/redbeacon/services/xhs/publish.py +879 -0
  35. redbeacon-0.1.0/src/redbeacon/services/xhs/session.py +230 -0
  36. redbeacon-0.1.0/src/redbeacon/tasks/__init__.py +0 -0
  37. redbeacon-0.1.0/src/redbeacon/tasks/feishu_sync.py +160 -0
  38. redbeacon-0.1.0/src/redbeacon/tasks/generate.py +772 -0
  39. redbeacon-0.1.0/src/redbeacon/tasks/publish.py +598 -0
  40. redbeacon-0.1.0/src/redbeacon/utils/__init__.py +0 -0
  41. redbeacon-0.1.0/src/redbeacon/utils/crypto.py +28 -0
  42. redbeacon-0.1.0/src/redbeacon/utils/logger.py +38 -0
  43. redbeacon-0.1.0/src/redbeacon/utils/paths.py +22 -0
  44. redbeacon-0.1.0/src/redbeacon.egg-info/PKG-INFO +162 -0
  45. redbeacon-0.1.0/src/redbeacon.egg-info/SOURCES.txt +52 -0
  46. redbeacon-0.1.0/src/redbeacon.egg-info/dependency_links.txt +1 -0
  47. redbeacon-0.1.0/src/redbeacon.egg-info/entry_points.txt +2 -0
  48. redbeacon-0.1.0/src/redbeacon.egg-info/requires.txt +12 -0
  49. redbeacon-0.1.0/src/redbeacon.egg-info/top_level.txt +1 -0
  50. redbeacon-0.1.0/tests/test_config.py +62 -0
  51. redbeacon-0.1.0/tests/test_database.py +44 -0
  52. redbeacon-0.1.0/tests/test_feishu_sync.py +47 -0
  53. redbeacon-0.1.0/tests/test_generate_helpers.py +78 -0
  54. redbeacon-0.1.0/tests/test_mcp_manager.py +9 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 djw
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.
@@ -0,0 +1,162 @@
1
+ Metadata-Version: 2.4
2
+ Name: redbeacon
3
+ Version: 0.1.0
4
+ Summary: RedBeacon — 小红书自动化 CLI(生成 / 审核 / 发布 / 多账号)
5
+ Author: djw
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/jidouqie/redbeacon
8
+ Project-URL: Issues, https://github.com/jidouqie/redbeacon/issues
9
+ Keywords: xiaohongshu,redbook,automation,cli,claude-code,skill
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: pydantic>=2.0.0
14
+ Requires-Dist: cryptography>=42.0.0
15
+ Requires-Dist: requests>=2.31.0
16
+ Requires-Dist: httpx>=0.27.0
17
+ Requires-Dist: openai>=1.30.0
18
+ Requires-Dist: playwright>=1.44.0
19
+ Requires-Dist: cloakbrowser>=0.3.30
20
+ Requires-Dist: markdown>=3.6
21
+ Requires-Dist: pyyaml>=6.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=8.0; extra == "dev"
24
+ Dynamic: license-file
25
+
26
+ # redbeacon — 小红书自动化 CLI
27
+
28
+ 把"选好题"以外的事全部交给 CLI:AI 生成文案 + 渲图 / 出图 → 审核 → 多账号发布 → 排期 → 选题库管理。所有命令都是 JSON in / JSON out,方便接入任何 agent、脚本、CI。
29
+
30
+ 主要被 [`redbeacon-skill`](https://github.com/jidouqie/redbeacon-skill)(Claude Code skill 封装)调用,但完全可以独立用。
31
+
32
+ ---
33
+
34
+ ## 安装
35
+
36
+ ```bash
37
+ # 推荐:pipx 隔离环境
38
+ pipx install redbeacon
39
+
40
+ # 或者直接 pip
41
+ pip install redbeacon
42
+
43
+ # Playwright 渲图引擎(图文卡片用)
44
+ playwright install chromium
45
+
46
+ # 校验
47
+ redbeacon --version
48
+ ```
49
+
50
+ > CloakBrowser 的 stealth Chromium 在首次跑 `redbeacon login start` 时自动下载到 `~/.cloakbrowser/`(约 200MB),不用手动装。
51
+
52
+ 依赖:Python ≥ 3.11。
53
+
54
+ ---
55
+
56
+ ## 30 秒看懂
57
+
58
+ ```bash
59
+ redbeacon readiness # 入口:看现在缺什么配
60
+ redbeacon config set ai_api_key sk-… # 配 AI Key
61
+ redbeacon accounts create # 建第 1 个小红书账号(id=1)
62
+ redbeacon login start --account-id 1 # 阻塞 + 弹浏览器扫码
63
+ redbeacon generate --account-id 1 --image-mode cards # 生成一篇
64
+ redbeacon content list --account-id 1 --status pending_review
65
+ redbeacon content approve --account-id 1 --id 1 # 本地审核
66
+ redbeacon publish --account-id 1 # 发布
67
+ ```
68
+
69
+ 每个命令成功 → exit 0 + stdout 一行 JSON;失败 → exit ≠0 + stderr `{"error", "next"}`。
70
+
71
+ ---
72
+
73
+ ## 命令总览
74
+
75
+ | 子命令 | 说明 |
76
+ |---|---|
77
+ | `redbeacon readiness` | 配置完整度 + 引导下一步 |
78
+ | `redbeacon status` | 账号 / 内容计数 / 路径总览 |
79
+ | `redbeacon accounts <list\|get\|create\|delete\|patch>` | 账号 CRUD |
80
+ | `redbeacon login <start\|verify\|status\|delete>` | 小红书登录(CloakBrowser) |
81
+ | `redbeacon config <list\|get\|set\|models\|test-ai\|test-feishu\|feishu-users>` | 配置管理 |
82
+ | `redbeacon strategy <get\|patch>` | 账号定位 / 调性 / 提示词 |
83
+ | `redbeacon topics <list\|add\|batch\|inspire\|stats\|reset\|types\|types-init>` | 选题库 |
84
+ | `redbeacon content <list\|get\|approve\|reject\|edit\|feishu-push>` | 内容审核 |
85
+ | `redbeacon generate --account-id N [--topic …] [--image-mode cards\|ai\|both]` | AI 生成(阻塞) |
86
+ | `redbeacon publish --account-id N` | 发布(飞书源 / 本地源自动判断,阻塞) |
87
+ | `redbeacon feishu <sync\|setup\|test>` | 飞书多维表格 |
88
+ | `redbeacon logs [--tail 150]` | 看日志 |
89
+
90
+ `redbeacon --help` / `redbeacon <子命令> --help` 看完整参数。
91
+
92
+ ---
93
+
94
+ ## 数据位置
95
+
96
+ | 路径 | 内容 |
97
+ |---|---|
98
+ | `~/.redbeacon/data/redbeacon.db` | SQLite 主库 |
99
+ | `~/.redbeacon/data/cb_profiles/{id}/` | 每账号 CloakBrowser profile(含 cookie) |
100
+ | `~/.redbeacon/data/images/` | 生成的图片 |
101
+ | `~/.redbeacon/logs/redbeacon.log` | 运行日志 |
102
+ | `~/.cloakbrowser/chromium-*/` | stealth Chromium |
103
+
104
+ 环境变量覆盖:`REDBEACON_DATA_DIR` / `REDBEACON_LOG_DIR`。
105
+
106
+ 跑 `redbeacon status` 看当前实际路径。
107
+
108
+ ---
109
+
110
+ ## 输出契约
111
+
112
+ - 默认:`{"key": "value", ...}` 一行 JSON
113
+ - TTY 下自动 indent=2 可读;强制覆盖:`REDBEACON_OUT=pretty` / `REDBEACON_OUT=compact`
114
+ - 错误:stderr `{"error": "...", "next": "redbeacon <修复命令>"}`
115
+
116
+ 完整 schema 定义在 `src/redbeacon/schemas.py`(TypedDict)。
117
+
118
+ ---
119
+
120
+ ## 开发
121
+
122
+ ```bash
123
+ git clone https://github.com/jidouqie/redbeacon-cli.git
124
+ cd redbeacon-cli
125
+
126
+ pip install -e .[dev]
127
+
128
+ # 测试
129
+ pytest tests/
130
+
131
+ # 本地跑
132
+ redbeacon --version
133
+ REDBEACON_DATA_DIR=/tmp/rb_dev redbeacon readiness
134
+ ```
135
+
136
+ 代码结构:
137
+
138
+ ```
139
+ src/redbeacon/
140
+ ├── cli.py # argparse 入口 + dispatch
141
+ ├── config.py # settings 表读写(加密支持)
142
+ ├── config_keys.py # 所有 settings key 集中定义
143
+ ├── database.py # SQLite 初始化 + migrate
144
+ ├── schemas.py # 输出 TypedDict
145
+ ├── render_xhs_v2.py # 图文卡片渲染(Playwright)
146
+ ├── routers/ # 子命令处理(accounts / login / config / publish ...)
147
+ ├── services/
148
+ │ ├── xhs/ # CloakBrowser 集成(session/login/publish)
149
+ │ ├── feishu_api.py
150
+ │ ├── image_gen.py
151
+ │ └── proxy_service.py
152
+ ├── tasks/ # 复杂业务编排(generate / publish / feishu_sync)
153
+ └── utils/ # logger / crypto / paths
154
+ ```
155
+
156
+ 加新命令:在 `routers/` 下加文件 + 在 `cli.py:_build_parser` 和 `_DISPATCH` 注册即可。
157
+
158
+ ---
159
+
160
+ ## License
161
+
162
+ MIT。详见 [LICENSE](LICENSE)。
@@ -0,0 +1,137 @@
1
+ # redbeacon — 小红书自动化 CLI
2
+
3
+ 把"选好题"以外的事全部交给 CLI:AI 生成文案 + 渲图 / 出图 → 审核 → 多账号发布 → 排期 → 选题库管理。所有命令都是 JSON in / JSON out,方便接入任何 agent、脚本、CI。
4
+
5
+ 主要被 [`redbeacon-skill`](https://github.com/jidouqie/redbeacon-skill)(Claude Code skill 封装)调用,但完全可以独立用。
6
+
7
+ ---
8
+
9
+ ## 安装
10
+
11
+ ```bash
12
+ # 推荐:pipx 隔离环境
13
+ pipx install redbeacon
14
+
15
+ # 或者直接 pip
16
+ pip install redbeacon
17
+
18
+ # Playwright 渲图引擎(图文卡片用)
19
+ playwright install chromium
20
+
21
+ # 校验
22
+ redbeacon --version
23
+ ```
24
+
25
+ > CloakBrowser 的 stealth Chromium 在首次跑 `redbeacon login start` 时自动下载到 `~/.cloakbrowser/`(约 200MB),不用手动装。
26
+
27
+ 依赖:Python ≥ 3.11。
28
+
29
+ ---
30
+
31
+ ## 30 秒看懂
32
+
33
+ ```bash
34
+ redbeacon readiness # 入口:看现在缺什么配
35
+ redbeacon config set ai_api_key sk-… # 配 AI Key
36
+ redbeacon accounts create # 建第 1 个小红书账号(id=1)
37
+ redbeacon login start --account-id 1 # 阻塞 + 弹浏览器扫码
38
+ redbeacon generate --account-id 1 --image-mode cards # 生成一篇
39
+ redbeacon content list --account-id 1 --status pending_review
40
+ redbeacon content approve --account-id 1 --id 1 # 本地审核
41
+ redbeacon publish --account-id 1 # 发布
42
+ ```
43
+
44
+ 每个命令成功 → exit 0 + stdout 一行 JSON;失败 → exit ≠0 + stderr `{"error", "next"}`。
45
+
46
+ ---
47
+
48
+ ## 命令总览
49
+
50
+ | 子命令 | 说明 |
51
+ |---|---|
52
+ | `redbeacon readiness` | 配置完整度 + 引导下一步 |
53
+ | `redbeacon status` | 账号 / 内容计数 / 路径总览 |
54
+ | `redbeacon accounts <list\|get\|create\|delete\|patch>` | 账号 CRUD |
55
+ | `redbeacon login <start\|verify\|status\|delete>` | 小红书登录(CloakBrowser) |
56
+ | `redbeacon config <list\|get\|set\|models\|test-ai\|test-feishu\|feishu-users>` | 配置管理 |
57
+ | `redbeacon strategy <get\|patch>` | 账号定位 / 调性 / 提示词 |
58
+ | `redbeacon topics <list\|add\|batch\|inspire\|stats\|reset\|types\|types-init>` | 选题库 |
59
+ | `redbeacon content <list\|get\|approve\|reject\|edit\|feishu-push>` | 内容审核 |
60
+ | `redbeacon generate --account-id N [--topic …] [--image-mode cards\|ai\|both]` | AI 生成(阻塞) |
61
+ | `redbeacon publish --account-id N` | 发布(飞书源 / 本地源自动判断,阻塞) |
62
+ | `redbeacon feishu <sync\|setup\|test>` | 飞书多维表格 |
63
+ | `redbeacon logs [--tail 150]` | 看日志 |
64
+
65
+ `redbeacon --help` / `redbeacon <子命令> --help` 看完整参数。
66
+
67
+ ---
68
+
69
+ ## 数据位置
70
+
71
+ | 路径 | 内容 |
72
+ |---|---|
73
+ | `~/.redbeacon/data/redbeacon.db` | SQLite 主库 |
74
+ | `~/.redbeacon/data/cb_profiles/{id}/` | 每账号 CloakBrowser profile(含 cookie) |
75
+ | `~/.redbeacon/data/images/` | 生成的图片 |
76
+ | `~/.redbeacon/logs/redbeacon.log` | 运行日志 |
77
+ | `~/.cloakbrowser/chromium-*/` | stealth Chromium |
78
+
79
+ 环境变量覆盖:`REDBEACON_DATA_DIR` / `REDBEACON_LOG_DIR`。
80
+
81
+ 跑 `redbeacon status` 看当前实际路径。
82
+
83
+ ---
84
+
85
+ ## 输出契约
86
+
87
+ - 默认:`{"key": "value", ...}` 一行 JSON
88
+ - TTY 下自动 indent=2 可读;强制覆盖:`REDBEACON_OUT=pretty` / `REDBEACON_OUT=compact`
89
+ - 错误:stderr `{"error": "...", "next": "redbeacon <修复命令>"}`
90
+
91
+ 完整 schema 定义在 `src/redbeacon/schemas.py`(TypedDict)。
92
+
93
+ ---
94
+
95
+ ## 开发
96
+
97
+ ```bash
98
+ git clone https://github.com/jidouqie/redbeacon-cli.git
99
+ cd redbeacon-cli
100
+
101
+ pip install -e .[dev]
102
+
103
+ # 测试
104
+ pytest tests/
105
+
106
+ # 本地跑
107
+ redbeacon --version
108
+ REDBEACON_DATA_DIR=/tmp/rb_dev redbeacon readiness
109
+ ```
110
+
111
+ 代码结构:
112
+
113
+ ```
114
+ src/redbeacon/
115
+ ├── cli.py # argparse 入口 + dispatch
116
+ ├── config.py # settings 表读写(加密支持)
117
+ ├── config_keys.py # 所有 settings key 集中定义
118
+ ├── database.py # SQLite 初始化 + migrate
119
+ ├── schemas.py # 输出 TypedDict
120
+ ├── render_xhs_v2.py # 图文卡片渲染(Playwright)
121
+ ├── routers/ # 子命令处理(accounts / login / config / publish ...)
122
+ ├── services/
123
+ │ ├── xhs/ # CloakBrowser 集成(session/login/publish)
124
+ │ ├── feishu_api.py
125
+ │ ├── image_gen.py
126
+ │ └── proxy_service.py
127
+ ├── tasks/ # 复杂业务编排(generate / publish / feishu_sync)
128
+ └── utils/ # logger / crypto / paths
129
+ ```
130
+
131
+ 加新命令:在 `routers/` 下加文件 + 在 `cli.py:_build_parser` 和 `_DISPATCH` 注册即可。
132
+
133
+ ---
134
+
135
+ ## License
136
+
137
+ MIT。详见 [LICENSE](LICENSE)。
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "redbeacon"
7
+ version = "0.1.0"
8
+ description = "RedBeacon — 小红书自动化 CLI(生成 / 审核 / 发布 / 多账号)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "djw" }]
13
+ keywords = ["xiaohongshu", "redbook", "automation", "cli", "claude-code", "skill"]
14
+
15
+ dependencies = [
16
+ "pydantic>=2.0.0",
17
+ "cryptography>=42.0.0",
18
+ "requests>=2.31.0",
19
+ "httpx>=0.27.0",
20
+ "openai>=1.30.0",
21
+ "playwright>=1.44.0",
22
+ "cloakbrowser>=0.3.30",
23
+ "markdown>=3.6",
24
+ "pyyaml>=6.0",
25
+ ]
26
+
27
+ [project.optional-dependencies]
28
+ dev = [
29
+ "pytest>=8.0",
30
+ ]
31
+
32
+ [project.scripts]
33
+ redbeacon = "redbeacon.cli:main"
34
+
35
+ [project.urls]
36
+ Homepage = "https://github.com/jidouqie/redbeacon"
37
+ Issues = "https://github.com/jidouqie/redbeacon/issues"
38
+
39
+ [tool.setuptools.packages.find]
40
+ where = ["src"]
41
+ include = ["redbeacon*"]
42
+
43
+ [tool.setuptools.package-data]
44
+ redbeacon = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """RedBeacon — 小红书自动化 CLI。
2
+
3
+ 入口:`redbeacon` 命令(pyproject.toml 声明),实现在 redbeacon.cli:main。
4
+ """
5
+ __version__ = "0.1.0"
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env python3
2
+ """RedBeacon CLI — skill 入口。
3
+
4
+ 约定:
5
+ - 成功:exit 0 + stdout JSON
6
+ - 失败:exit ≠0 + stderr {"error": str}
7
+ - 输出 shape 见 redbeacon.schemas
8
+ - 子命令实现在 redbeacon.routers.<feature>
9
+
10
+ 新增子命令:在 routers/ 下加文件 + 在 _build_parser/_DISPATCH 注册即可。
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import os
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ from redbeacon import __version__
20
+ from redbeacon.utils.paths import default_data_dir, default_log_dir
21
+
22
+ DATA_DIR = str(os.environ.get("REDBEACON_DATA_DIR", default_data_dir()))
23
+ LOG_DIR = str(os.environ.get("REDBEACON_LOG_DIR", default_log_dir()))
24
+
25
+ from redbeacon.utils.logger import init_logger
26
+ from redbeacon import database
27
+
28
+ init_logger(LOG_DIR)
29
+ database.init_db(DATA_DIR)
30
+
31
+ from redbeacon.routers import (
32
+ accounts as r_accounts,
33
+ login as r_login,
34
+ strategy as r_strategy,
35
+ topics as r_topics,
36
+ content as r_content,
37
+ generate as r_generate,
38
+ publish as r_publish,
39
+ feishu as r_feishu,
40
+ config as r_config,
41
+ status as r_status,
42
+ logs as r_logs,
43
+ readiness as r_readiness,
44
+ )
45
+
46
+
47
+ # ── CLI Parser ────────────────────────────────────────────────────────────────
48
+
49
+ def _build_parser() -> argparse.ArgumentParser:
50
+ p = argparse.ArgumentParser(prog="redbeacon", description="RedBeacon CLI — 小红书自动化")
51
+ p.add_argument("--version", action="version", version=f"redbeacon {__version__}")
52
+ sp = p.add_subparsers(dest="cmd", required=True)
53
+
54
+ # accounts
55
+ acc = sp.add_parser("accounts")
56
+ acc_sp = acc.add_subparsers(dest="sub", required=True)
57
+ acc_sp.add_parser("list")
58
+ _add_account_arg(acc_sp.add_parser("get"))
59
+ acc_sp.add_parser("create")
60
+ _add_account_arg(acc_sp.add_parser("delete"))
61
+ acc_pat = acc_sp.add_parser("patch")
62
+ _add_account_arg(acc_pat)
63
+ acc_pat.add_argument("--data", required=True, help='JSON: {"field": "value"}')
64
+
65
+ # login
66
+ login = sp.add_parser("login")
67
+ login_sp = login.add_subparsers(dest="sub", required=True)
68
+ for sub in ("start", "verify", "status", "delete"):
69
+ _add_account_arg(login_sp.add_parser(sub))
70
+
71
+ # strategy
72
+ strat = sp.add_parser("strategy")
73
+ strat_sp = strat.add_subparsers(dest="sub", required=True)
74
+ _add_account_arg(strat_sp.add_parser("get"))
75
+ sp2 = strat_sp.add_parser("patch")
76
+ _add_account_arg(sp2)
77
+ sp2.add_argument("--data", required=True, help='JSON: {"field": "value"}')
78
+
79
+ # topics
80
+ top = sp.add_parser("topics")
81
+ top_sp = top.add_subparsers(dest="sub", required=True)
82
+ tl = top_sp.add_parser("list")
83
+ _add_account_arg(tl)
84
+ tl.add_argument("--type", default=None)
85
+ tl.add_argument("--used", type=int, default=None)
86
+ tl.add_argument("--limit", type=int, default=100)
87
+ tl.add_argument("--offset", type=int, default=0)
88
+ ta = top_sp.add_parser("add"); _add_account_arg(ta)
89
+ ta.add_argument("--content", required=True)
90
+ ta.add_argument("--type", required=True)
91
+ tb = top_sp.add_parser("batch"); _add_account_arg(tb)
92
+ tb.add_argument("--type", required=True)
93
+ tb.add_argument("--text", default=None, help="换行分隔的选题,不传则从 stdin 读取")
94
+ ti = top_sp.add_parser("inspire"); _add_account_arg(ti)
95
+ ti.add_argument("--text", required=True)
96
+ _add_account_arg(top_sp.add_parser("stats"))
97
+ tr = top_sp.add_parser("reset"); _add_account_arg(tr)
98
+ tr.add_argument("--type", default=None)
99
+ _add_account_arg(top_sp.add_parser("types"))
100
+ _add_account_arg(top_sp.add_parser("types-init"))
101
+
102
+ # content
103
+ cont = sp.add_parser("content")
104
+ cont_sp = cont.add_subparsers(dest="sub", required=True)
105
+ cl = cont_sp.add_parser("list"); _add_account_arg(cl)
106
+ cl.add_argument("--status", default=None)
107
+ cl.add_argument("--limit", type=int, default=20)
108
+ cl.add_argument("--offset", type=int, default=0)
109
+ for sub in ("get", "approve", "reject", "edit"):
110
+ s = cont_sp.add_parser(sub); _add_account_arg(s)
111
+ s.add_argument("--id", type=int, required=True)
112
+ if sub == "reject":
113
+ s.add_argument("--comment", default="")
114
+ elif sub == "edit":
115
+ s.add_argument("--title", default=None)
116
+ s.add_argument("--body", default=None)
117
+ s.add_argument("--tags", default=None, help='JSON array: ["#tag1","#tag2"]')
118
+ cont_sp.add_parser("feishu-push")
119
+
120
+ # generate
121
+ gen = sp.add_parser("generate"); _add_account_arg(gen)
122
+ gen.add_argument("--topic", default=None)
123
+ gen.add_argument("--image-mode", default=None, dest="image_mode",
124
+ choices=["cards", "ai", "both"])
125
+ gen.add_argument("--content-type", default=None, dest="content_type")
126
+ gen.add_argument("--pillar", default=None)
127
+
128
+ # publish
129
+ _add_account_arg(sp.add_parser("publish"))
130
+
131
+ # feishu
132
+ fei = sp.add_parser("feishu")
133
+ fei_sp = fei.add_subparsers(dest="sub", required=True)
134
+ fs = fei_sp.add_parser("sync")
135
+ fs.add_argument("--account-id", type=int, default=None, dest="account_id")
136
+ fsu = fei_sp.add_parser("setup"); _add_account_arg(fsu)
137
+ fsu.add_argument("--app-token", default=None, dest="app_token")
138
+ fsu.add_argument("--table-id", default=None, dest="table_id")
139
+ _add_account_arg(fei_sp.add_parser("test"))
140
+
141
+ # config
142
+ conf = sp.add_parser("config")
143
+ conf_sp = conf.add_subparsers(dest="sub", required=True)
144
+ cg = conf_sp.add_parser("get"); cg.add_argument("key")
145
+ cs = conf_sp.add_parser("set"); cs.add_argument("key"); cs.add_argument("value")
146
+ for sub in ("list", "models", "test-ai", "test-feishu", "feishu-users"):
147
+ conf_sp.add_parser(sub)
148
+
149
+ # readiness / status / logs
150
+ sp.add_parser("readiness")
151
+ sp.add_parser("status")
152
+ log_p = sp.add_parser("logs")
153
+ log_p.add_argument("--tail", type=int, default=150)
154
+
155
+ return p
156
+
157
+
158
+ def _add_account_arg(parser: argparse.ArgumentParser) -> None:
159
+ parser.add_argument("--account-id", type=int, required=True, dest="account_id")
160
+
161
+
162
+ # ── Dispatch ──────────────────────────────────────────────────────────────────
163
+
164
+ _DISPATCH = {
165
+ "accounts": r_accounts.dispatch,
166
+ "login": r_login.dispatch,
167
+ "strategy": r_strategy.dispatch,
168
+ "topics": r_topics.dispatch,
169
+ "content": r_content.dispatch,
170
+ "generate": r_generate.dispatch,
171
+ "publish": r_publish.dispatch,
172
+ "feishu": r_feishu.dispatch,
173
+ "config": r_config.dispatch,
174
+ "status": r_status.dispatch,
175
+ "logs": r_logs.dispatch,
176
+ "readiness": r_readiness.dispatch,
177
+ }
178
+
179
+
180
+ def main():
181
+ args = _build_parser().parse_args()
182
+ fn = _DISPATCH.get(args.cmd)
183
+ if fn is None:
184
+ import json
185
+ print(json.dumps({"error": f"未知命令:{args.cmd}"}, ensure_ascii=False), file=sys.stderr)
186
+ sys.exit(1)
187
+ fn(args)
188
+
189
+
190
+ if __name__ == "__main__":
191
+ main()
@@ -0,0 +1,102 @@
1
+ """全局配置读写(settings 表)。
2
+
3
+ 支持两种 key 形态:
4
+ - 字符串:"ai_api_key"
5
+ - Key 常量:config_keys.CK_AI_API_KEY(推荐,附带默认值、加密标志)
6
+
7
+ 敏感字段(is encrypted=True)写入时自动加密,读取时自动解密。
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from typing import Union
13
+
14
+ from redbeacon.database import conn
15
+ from redbeacon.utils.crypto import encrypt, decrypt
16
+ from redbeacon.config_keys import Key, ENCRYPTED_KEY_NAMES, ALL_KEYS
17
+
18
+ _log = logging.getLogger("config")
19
+
20
+ KeyArg = Union[str, Key]
21
+
22
+
23
+ def _name_and_default(key: KeyArg, default: str) -> tuple[str, str]:
24
+ if isinstance(key, Key):
25
+ return key.name, (default if default else key.default)
26
+ return key, default
27
+
28
+
29
+ def get(key: KeyArg, default: str = "") -> str:
30
+ name, default = _name_and_default(key, default)
31
+ c = conn()
32
+ row = c.execute(
33
+ "SELECT value, is_encrypted FROM settings WHERE key=?", (name,)
34
+ ).fetchone()
35
+ c.close()
36
+ if row is None:
37
+ return default
38
+ value = row["value"]
39
+ if row["is_encrypted"] and value:
40
+ try:
41
+ value = decrypt(value)
42
+ except Exception:
43
+ _log.warning(
44
+ "配置项 '%s' 解密失败(可能是机器迁移导致密钥不匹配),"
45
+ "请重新保存该字段", name,
46
+ )
47
+ c2 = conn()
48
+ c2.execute(
49
+ "UPDATE settings SET value='', updated_at=datetime('now') WHERE key=?",
50
+ (name,),
51
+ )
52
+ c2.commit()
53
+ c2.close()
54
+ return default
55
+ return value
56
+
57
+
58
+ def set(key: KeyArg, value: str) -> None:
59
+ name = key.name if isinstance(key, Key) else key
60
+ is_enc = 1 if name in ENCRYPTED_KEY_NAMES else 0
61
+ stored = encrypt(value) if is_enc and value else value
62
+ c = conn()
63
+ c.execute(
64
+ """INSERT INTO settings (key, value, is_encrypted, updated_at)
65
+ VALUES (?, ?, ?, datetime('now'))
66
+ ON CONFLICT(key) DO UPDATE SET
67
+ value=excluded.value,
68
+ is_encrypted=excluded.is_encrypted,
69
+ updated_at=excluded.updated_at""",
70
+ (name, stored, is_enc),
71
+ )
72
+ c.commit()
73
+ c.close()
74
+
75
+
76
+ _SENTINEL = "__SET__" # 表示"已设置但不回传明文"
77
+
78
+
79
+ def get_all_public() -> dict:
80
+ """返回所有配置;加密字段已设置则返回哨兵值 __SET__,未设置或解密失败返回 ""。"""
81
+ c = conn()
82
+ rows = c.execute("SELECT key, value, is_encrypted FROM settings").fetchall()
83
+ c.close()
84
+ result: dict[str, str] = {}
85
+ for r in rows:
86
+ if r["is_encrypted"]:
87
+ if not r["value"]:
88
+ result[r["key"]] = ""
89
+ else:
90
+ try:
91
+ decrypt(r["value"])
92
+ result[r["key"]] = _SENTINEL
93
+ except Exception:
94
+ result[r["key"]] = ""
95
+ else:
96
+ result[r["key"]] = r["value"]
97
+ return result
98
+
99
+
100
+ def registered_keys() -> tuple[Key, ...]:
101
+ """供 CLI / 文档使用:枚举所有注册的 key。"""
102
+ return ALL_KEYS