{"id":47973060,"url":"https://github.com/lain39/discorsair","last_synced_at":"2026-04-04T10:49:12.798Z","repository":{"id":346757522,"uuid":"1190587884","full_name":"lain39/discorsair","owner":"lain39","description":"Discourse 自动巡帖工具。","archived":false,"fork":false,"pushed_at":"2026-03-25T19:16:23.000Z","size":253,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-04T10:49:02.659Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lain39.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-24T12:34:32.000Z","updated_at":"2026-03-25T19:16:29.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/lain39/discorsair","commit_stats":null,"previous_names":["lain39/discorsair"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/lain39/discorsair","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lain39%2Fdiscorsair","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lain39%2Fdiscorsair/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lain39%2Fdiscorsair/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lain39%2Fdiscorsair/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lain39","download_url":"https://codeload.github.com/lain39/discorsair/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lain39%2Fdiscorsair/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31397055,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-04T10:20:44.708Z","status":"ssl_error","status_checked_at":"2026-04-04T10:20:06.846Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-04-04T10:49:11.779Z","updated_at":"2026-04-04T10:49:12.783Z","avatar_url":"https://github.com/lain39.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Discorsair\n\nDiscourse 自动巡帖工具。\n\n## 使用边界\n\n- 项目默认面向“本人账号、本人数据、本人控制环境”的个人使用场景，主要用途是保持账号活跃度。\n- 不建议用于商用采集、攻击性压测、权限绕过等其他高风险用途。\n- FlareSolverr、代理、Cookie、数据库、插件和外部通知服务均由使用者自行配置和承担风险。\n\n## 运行方式\n\nCLI 命令名：`discorsair`\n\n## CLI\n\n- 常用命令：`run` / `watch` / `daily` / `like` / `reply` / `export` / `import` / `status` / `notify test` / `init` / `serve`\n- `run` 和 `watch` 当前共用同一套 watch 循环实现与参数\n- `status` / `daily` / `like` / `reply` / `export` / `import` / `notify test` 默认输出 JSON，便于脚本处理\n- 详细命令说明、参数与输出示例见 `docs/cli.md`\n- `status` 输出也包含插件状态快照：后端类型、已启用插件、运行态计数，以及插件持久态摘要（今日计数、once 标记数量、KV key 列表）\n- 插件开发说明见 `docs/plugin-development.md`\n\n## 配置\n\n- 主配置：`config/app.json`\n- 运行时状态：与配置文件同目录同名的 `*.state.json`，例如 `config/app.json -\u003e config/app.state.json`\n- 账号配置：`config/app.json` 内的 `auth`\n- 模板参考：`config/app.json.template`\n- 必填：`site.base_url`\n- `auth.cookie` 需要在 `app.json`、对应的 `*.state.json` 或环境变量里至少提供一处\n- 敏感字段支持环境变量覆盖：`DISCORSAIR_AUTH_COOKIE`、`DISCORSAIR_AUTH_NAME`、`DISCORSAIR_AUTH_KEY`、`DISCORSAIR_NOTIFY_URL`、`DISCORSAIR_POSTGRES_DSN`\n- 存储后端：`storage.backend`（`sqlite` / `postgres`，默认 `sqlite`）\n- SQLite 路径：`storage.path`（默认 `data/discorsair.db`）\n- SQLite 按站点分库：`storage.auto_per_site`\n- PostgreSQL DSN：`storage.postgres.dsn`\n- 站点爬虫锁目录：`storage.lock_dir`\n- 爬取模式下同一 `site` 同一时刻只允许一个进程运行；会基于 `storage.lock_dir` 创建按站点区分的 crawl lock\n- 旧 SQLite schema 不提供迁移脚本；如果命中 schema mismatch，直接删除旧库后重建\n- 抓取：`crawl.enabled`（是否抓取帖子内容）\n- 调试：`debug`（更详细日志）\n- 日志文件：`logging.path`\n- 请求节流：`request.min_interval_secs`（默认 1 秒）\n- 未读优先：`watch.use_unseen`\n- 队列：`queue.maxsize`\n- 通知：`notify.enabled` + `notify.url` + `notify.chat_id`\n- 通知自动已读：`notify.auto_mark_read`（默认关闭；当当前未读通知都已在本地去重状态中时，调用 mark-read 全部标记为已读）\n- 插件：`plugins.dir` + `plugins.items`（示例插件见 `plugins/sample_forum_ops/`；其中回复/点赞代码默认注释）\n- 通知前缀：`notify.prefix` / `notify.error_prefix`\n- 服务：`discorsair serve` 启动 HTTP 控制服务\n- 控制接口超时：`server.action_timeout_secs`（`0` 表示不设超时）\n- HTTP 控制接口返回 `504` 只表示调用方等待超时，不保证动作未生效；底层请求可能仍已发出或完成\n- 服务默认仅监听 `127.0.0.1`\n- 如果 `server.host` 或 `--host` 使用非回环地址，必须配置 `server.api_key`\n- `server.schedule` / `server.interval_secs` / `server.max_posts_per_interval` 仅作用于 `serve` 模式下的 watch 线程；`run/watch` 仍以 CLI 参数为准\n- 启动时按 `app.json -\u003e *.state.json -\u003e 环境变量` 的顺序合并 `auth` 状态\n- 运行时只会写回 `*.state.json` 里的 `auth` 状态，不再修改 `app.json`\n- `*.state.json` 不会在启动时预先生成；首次发生受管 `auth` 状态写入时才会自动生成\n- 运行时写回的 `_t` 必须已经被一次成功交互实际带到服务器；如果响应里刚拿到更新的 `_t`，会先留在内存，等后续成功交互验证后再写回\n- 如果要手工修复运行时状态，直接修改对应的 `*.state.json`，或者删除它后等待后续运行时状态重新写入\n- `serve` 模式下如果遇到登录失效或 unresolved challenge，会停止 watch 并把 watch 标记为 blocked；HTTP 服务继续存活\n- watch 被 `auth_invalid` / `unresolved_challenge` 阻塞后，可用 HTTP `POST /auth/cookie` 更新 `_t`，再用 `POST /watch/start` 恢复，或直接 `POST /watch/start {\"force\": true}` 强制重试\n- `POST /auth/cookie` / `force=true` 的设计目标是“同一账号刷新登录态”，不是“跨账号热切换”；如果要换号，建议重启进程并使用目标账号配置重新启动\n- HTTP `GET /` 和 `GET /healthz` 都是公开的轻量状态端点，不受 `server.api_key` 保护；返回 `{\"ok\":true}`，适合做容器保活和外部探活\n- `queue.maxsize` 只限制 ready/running 的请求；已进入 `429` 冷却等待的 delayed 请求不受这个上限约束\n\n## 容器部署\n\n- 仓库内提供了一个从官方 FlareSolverr 镜像出发的单容器方案：`Dockerfile` + `docker-entrypoint.sh`\n- 容器内会同时启动：\n  - FlareSolverr：`127.0.0.1:8191`\n  - Discorsair `serve`：`0.0.0.0:17880`\n- 只暴露 Discorsair 的 `17880`；FlareSolverr 只供容器内访问\n- 镜像默认不打包你本地的 `config/*.json` / `*.state.json`；需要在运行时挂载或自行派生镜像提供 `config/app.json`\n- 镜像默认使用 SQLite，并把数据目录约定为 `/data`\n\n推荐的容器配置：\n\n```jsonc\n{\n  \"storage\": {\n    \"backend\": \"sqlite\",\n    \"path\": \"/data/discorsair.db\",\n    \"lock_dir\": \"/data/locks\"\n  },\n  \"flaresolverr\": {\n    \"base_url\": \"http://127.0.0.1:8191\",\n    \"in_docker\": false\n  },\n  \"server\": {\n    \"host\": \"0.0.0.0\",\n    \"port\": 17880,\n    \"api_key\": \"\"\n  }\n}\n```\n\n建议把敏感值放环境变量：\n\n- `DISCORSAIR_AUTH_COOKIE`\n- `DISCORSAIR_AUTH_KEY`\n- 可选：`DISCORSAIR_CONFIG`\n- 可选：`DISCORSAIR_SERVER_HOST`\n- 可选：`DISCORSAIR_SERVER_PORT`\n- 可选：`FLARESOLVERR_INTERNAL_URL`\n- 可选：`FLARESOLVERR_STARTUP_TIMEOUT_SECS`\n\n最省事的本地启动方式是直接使用仓库内的 `docker-compose.yml`：\n\n```bash\nDISCORSAIR_AUTH_COOKIE='_t=...' \\\nDISCORSAIR_AUTH_KEY='replace-me' \\\ndocker compose up --build\n```\n\n也可以先把它们写进仓库根目录的 `.env` 再执行 `docker compose up --build`；仓库提供了可提交的 `.env.example`，而本地 `.env` 已被 `.gitignore` 忽略。\n\n它会：\n\n- 构建当前仓库镜像\n- 挂载 `./config/app.json` 到容器内\n- 挂载命名卷 `discorsair-data` 到 `/data`\n- 暴露 `17880`\n\n本地构建：\n\n```bash\ndocker build -t discorsair-flaresolverr .\n```\n\n运行示例：\n\n```bash\ndocker run --rm \\\n  -p 17880:17880 \\\n  -v \"$(pwd)/config/app.json:/app/config/app.json:ro\" \\\n  -v discorsair-data:/data \\\n  -e DISCORSAIR_AUTH_COOKIE='_t=...' \\\n  -e DISCORSAIR_AUTH_KEY='replace-me' \\\n  discorsair-flaresolverr\n```\n\n说明：\n\n- 如果你传了容器命令参数，入口脚本会先启动 FlareSolverr\n- 当参数看起来像 `watch` / `serve` / `run` 这类 Discorsair 子命令时，会自动执行 `discorsair --config \u003cconfig-path\u003e ...`\n- 如果你传的是完整外部命令，比如 `bash`，则按原样执行，不自动补 `discorsair`\n- 如果不挂载 `/data`，SQLite、lock 目录和运行时状态会随容器销毁一起丢失\n- 如果你想把配置直接烘进镜像，可以基于当前 `Dockerfile` 再写一层派生镜像，把你自己的 `config/app.json` 复制进去\n- `FLARESOLVERR_INTERNAL_URL` 用于告诉入口脚本去哪里探测容器内 FlareSolverr 的就绪状态，默认是 `http://127.0.0.1:8191`\n- `FLARESOLVERR_STARTUP_TIMEOUT_SECS` 控制入口脚本等待 FlareSolverr 启动完成的超时时间，默认 `60`\n\n## PostgreSQL\n\n- 先安装可选依赖：`uv sync --extra postgres`\n- 也可以不把 DSN 写进配置文件，改用环境变量：`DISCORSAIR_POSTGRES_DSN`\n- `storage.backend` 设为 `postgres` 后，运行时使用 `storage.postgres.dsn` 连接数据库；`storage.path` / `storage.auto_per_site` 会被忽略\n- PostgreSQL 模式是“单库多站点、多账号共存”；SQLite 仍是按站点分文件\n- 只需要先手动建库，不需要手动建表；首次启动时会自动初始化 schema\n- 爬取模式下的 crawl lock 仍按 `site` 生效，和 SQLite 一样继续使用 `storage.lock_dir`\n- `discorsair status` / HTTP `GET /watch/status` 在 PostgreSQL 下返回的 `storage_path` 是脱敏后的 DSN，不是文件路径\n\n配置示例：\n\n```jsonc\n\"storage\": {\n  \"backend\": \"postgres\",\n  \"path\": \"data/discorsair.db\", // postgres 模式下忽略\n  \"auto_per_site\": true,        // postgres 模式下忽略\n  \"lock_dir\": \"data/locks\",\n  \"postgres\": {\n    \"dsn\": \"postgresql://user:password@127.0.0.1:5432/discorsair\"\n  }\n}\n```\n\n典型流程：\n\n- 新建数据库后，直接执行 `discorsair run` / `watch` / `serve`，程序会自动建表\n- SQLite 导出：`discorsair --config config/sqlite.json export --output ./export`\n- 导入 PostgreSQL：`discorsair --config config/postgres.json import --input ./export`\n- 同样也支持 PostgreSQL -\u003e SQLite、PostgreSQL -\u003e PostgreSQL 的导出/导入\n- 当前导入导出实现会按表整批读入内存，更适合中小规模数据，不适合超大库直接整库搬迁\n\n## 开发\n\n- 安装开发依赖：`uv sync --group dev`\n- 本地检查总入口：`make check`\n- 单独跑静态检查：`make lint`、`make static`\n- 单独跑测试：`make test`\n- 构建与包元数据校验：`make build`\n- 发布前约束校验：`make release-check TAG=v0.1.2`\n- CI 会执行：release guard、`ruff check`、`compileall`、单元测试、构建、`twine check`\n- `plugins/` 默认忽略本地自用插件，只保留 `plugins/sample_forum_ops/` 示例插件；如果你要提交自用插件，需要手动 `git add -f`\n- 如果要使用 PostgreSQL 后端，先安装可选依赖：`uv sync --extra postgres`\n- PostgreSQL 集成测试入口：`DISCORSAIR_PG_TEST_DSN=postgresql://... uv run --extra postgres python -m unittest tests.test_postgres_integration`\n- CI 也会单独跑一条 PostgreSQL 集成测试 job\n- 数据迁移命令：`discorsair export --output ./export`、`discorsair import --input ./export`\n- Schema 规划与表结构见 `docs/schema.md`\n\n## 结构\n\n- `config/` 配置\n- `src/` 源码\n- `docs/` 文档\n- `tests/` 测试\n- 架构说明：`docs/architecture.md`\n\n## 备注\n\n- Cookie 建议新建一个隐私窗口来获取，获取后关闭窗口，以免会话冲突导致 cookie 失效。首次导入时建议只保留 `_t`。\n\n## 许可证\n\n本项目采用 MIT License。\n\n## 免责声明\n\n- 本项目主要用于学习、研究和个人使用。\n- 使用者需自行确认目标站点服务条款、robots 规则及所在地区法律法规，并自行承担使用风险。\n- 作者不对账号受限、限流、封禁、数据缺失、第三方服务异常或由此产生的直接/间接损失负责。\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flain39%2Fdiscorsair","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flain39%2Fdiscorsair","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flain39%2Fdiscorsair/lists"}