Metadata-Version: 2.4
Name: baisitong2
Version: 1.0.2
Summary: 民情直通车2 —— 民情诉求智能分析与智能派单系统(AI+社会治理):FastAPI + 双引擎语义分析 + 智能派单 + 工单状态机 + SLA 监管
Author: yangxuan
License: MIT
Project-URL: Homepage, https://pypi.org/project/baisitong2/
Keywords: community,appeal,dispatch,fastapi,ai-agent
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: fastapi==0.115.0
Requires-Dist: uvicorn==0.30.6
Requires-Dist: python-multipart==0.0.12
Requires-Dist: sqlalchemy==2.0.35
Requires-Dist: pymysql==1.1.1
Requires-Dist: cryptography==43.0.3
Requires-Dist: httpx==0.27.2
Requires-Dist: python-dotenv==1.0.1
Provides-Extra: dev
Requires-Dist: pytest==8.3.3; extra == "dev"
Dynamic: license-file

# 🏛️ 民情直通车2(baisitong2)

> 民情诉求智能分析与智能派单系统 · AI + 社会治理
> 双引擎语义分析 + 分类职责矩阵派单 + 工单状态机 + SLA 时限监管,**零配置、可离线运行**

[![npm](https://img.shields.io/badge/npm-baisitong2-blue)](https://www.npmjs.com/package/baisitong2)
[![pypi](https://img.shields.io/badge/pypi-baisitong2-green)](https://pypi.org/project/baisitong2/)
[![tests](https://img.shields.io/badge/tests-137%20passed-brightgreen)](#测试)

---

## 🎯 为什么 AI 是本系统的关键要素(核心演示论点)

以一条真实风格的复合诉求为例:

> **"楼下烧烤店每天晚上油烟很大,我家孩子一直咳嗽"**

| 引擎 | 分析结果 | 派单结果 |
|------|----------|----------|
| 规则引擎(离线兜底) | 只能命中"烧烤/油烟"字面关键词 → 单标签 `['环境卫生']` | 仅派综合执法局 |
| **远程 LLM(智能分析)** | 推理出"孩子一直咳嗽"是油烟引发的**隐含健康风险** → 复合标签 `['环境卫生', '民生保障']` | 主派综合执法局 + **抄送民政局**,儿童健康问题不再漏管 |

关键词规则永远无法从"孩子咳嗽"推出"民生保障"——这**只有大模型的语义推理做得到**。
同时,健康受害的隐含情绪、长期反映的焦灼感,LLM 也能给出更真实的情感分(2 分 vs 规则引擎的 3 分中性),
让风险预警列表第一时间捕捉到这条诉求。这就是"AI 是解决问题的关键要素"的直接证据。

> 该案例已内置为工作台"一键填充"示例与测试锚点(`tests/test_api.py::test_composite_appeal_offline_full_flow`),
> 离线(无 Key)环境下走规则引擎全流程可用,配置 Key 后即可演示 LLM 复合标签的对照效果。

## 一分钟跑起来

**方式 1:npm(任何装了 Python 3.9+ 的电脑)**

```bash
npx baisitong2                 # 自动建 venv、装依赖、启动,打开 http://localhost:8081
npx baisitong2 --port 9000     # 自定义端口
```

**方式 2:pip 安装**

```bash
pip install baisitong2 -i https://mirrors.aliyun.com/pypi/simple/
baisitong2                     # 或 python -m baisitong2
```

**方式 3:源码运行**

```bash
./startup.sh          # Linux / macOS(Windows 用 startup.bat)
# 或手动:
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python -m baisitong2 --port 8081
```

> **零配置即用**:默认 SQLite + 内置规则分析引擎(关键词多标签分类 + 情感词典 + 紧急度规则),
> 无需数据库、无需 API Key、无需联网。配置大模型 Key 后自动切换真模型,见[配置](#配置)。

## 功能特性

| 模块 | 说明 |
|------|------|
| 🤖 **双引擎语义分析** | 远程 LLM(OpenAI 兼容 + Function-Calling 强约束 JSON)↔ 内置规则引擎;超时(15s 可调)/5xx/脏 JSON 自动降级,`analysis_source` 标记来源 |
| 🏷️ **多标签分类** | 固定 9 类体系(环境卫生/噪音扰民/违章建筑/公共设施/物业管理/民生保障/邻里纠纷/交通安全/其他),复合诉求一次打多标 |
| 💗 **情感 + 紧急度** | 情感 1-5 五档(风险预警口径 ≤2)、紧急度 1-3(煤气/漏水/倒塌/伤人等安全词 → 3 级) |
| 📨 **智能派单** | 分类 → 职责矩阵自动匹配主责部门;多标签诉求其余部门自动抄送;支持人工纠偏指定部门 |
| 🔁 **诉求重分析** | `POST /api/appeals/{id}/reanalyze`(可 `force_rule=true` 强制规则引擎);结果变化自动保留历史版本(`analysis_history`),已派单诉求重析后可重新派单 |
| ⏱️ **SLA 时限监管** | 紧急 4h / 一般 24h / 低 72h(环境变量可调),看板临期黄色预警、超时红色告警、剩余 ≤25% 临期提醒 |
| 🔁 **工单状态机** | 待派单→已派单→已接单→已完成→已办结主线 + 拒单回退;非法流转直接 400,全程时间戳落档 |
| 🪙 **Token 管控(AC-4)** | `ANALYSIS_COMPACT` 省流模式(精简 Prompt + 压缩输出,单条 <600 token 预算);API `usage` 精确捕获、本地按 1.6字/token 折算;看板今日/累计 Token 卡片 |
| 📦 **批量分析** | 并发受 `BATCH_CONCURRENCY` 管控(Token 管控),单条失败逐条降级,绝不整体失败 |
| 📊 **治理看板** | 分类分布、近 7 日趋势、部门工单量与平均办结时效、情感风险预警、SLA 按时率 + 临期列表、Token 消耗 |
| 📄 **CSV 导出** | 诉求/工单两份数据按当前筛选导出 UTF-8-BOM CSV(Excel 中文不乱码),标准库实现零依赖 |
| 🌙 **深色模式** | CSS 变量全站主题化,`data-theme="dark"` 整组覆盖;四页右上角一键切换 + localStorage 记忆 + 跟随系统深浅色 + 首帧防闪白;徽章/SLA 色块/看板深色下对比度整组重调 |
| 📱 **移动端适配** | 480px 断点:窄屏顶部导航、筛选器两列换行、列表卡纵排、统计表格横向滚动、输入 16px 防 iOS 缩放;筛选条件 localStorage 记忆,刷新自动还原 |
| ⚡ **性能与并发** | stats 聚合无 N+1(SQL 条数护栏测试锁定);单条规则分析 <10ms、看板聚合 <100ms 基准护栏;**10 线程并发提交+分析+派单零串扰零失败**(工单号取号加锁原子化 + SQLite 引擎多连接化,修复两处真实并发缺陷) |
| 🖥️ **原生前端** | 纯 HTML/CSS/JS 零构建:工作台(示例一键填充)、诉求列表(多维筛选)、工单看板(SLA 倒计时)、统计看板 |

## 系统架构

```
┌──────────────────────────────────────────────────────────────────┐
│            前端 原生 HTML/CSS/JS(无框架、无构建)                  │
│  诉求工作台(提交+AI分析卡) · 诉求列表(筛选/派单)               │
│  工单看板(状态分列+SLA倒计时+流转) · 统计看板(趋势/风险)      │
└───────────────────────────┬──────────────────────────────────────┘
                            │ HTTP JSON
┌───────────────────────────▼──────────────────────────────────────┐
│                        接口层 routers/                            │
│  appeals(提交/批量/筛选/派单) · workorders(列表/流转)         │
│  stats(看板) · health(健康检查)  ← 统一 {code,message} 异常兜底 │
├──────────────────────────────────────────────────────────────────┤
│                        逻辑层 services/                           │
│  analyzer  双引擎:RemoteAnalyzer(Function-Calling JSON)          │
│            ↔ RuleAnalyzer(关键词+情感词典+紧急度规则)           │
│  dispatcher 职责矩阵匹配(主责+抄送) · SLA 计算 · 工单号生成     │
│  state_machine 状态流转校验 + 时间戳 + 诉求状态联动              │
│  stats     每日聚合(DailyStat)+ 看板组装                        │
├──────────────────────────────────────────────────────────────────┤
│                   数据层 models/ + database.py                    │
│  Appeal(诉求+分析JSON) · WorkOrder(状态机+SLA)                 │
│  Department(职责矩阵) · DailyStat(按日聚合)                     │
│  SQLAlchemy ORM → SQLite(默认) / MySQL 8(可切换)              │
└──────────────────────────────────────────────────────────────────┘
        │                          │
   GLM / DeepSeek API        内置规则引擎(离线兜底)
   (配置 Key 后启用)         关键词分类 + 词典打分 + 模板摘要
```

**AI 双引擎设计**:`analyze_appeal()` 统一编排——优先远程 LLM
(Function-Calling 强约束返回 `{categories, key_info, sentiment, urgency, summary}` 严格 JSON),
超时 / 5xx / JSON 解析失败自动降级内置规则引擎,保证**演示永不中断**,
`analysis_source` 字段(llm / rule_fallback)全程标记数据来自哪个引擎。

**派单管线**:`多标签分类 → 职责矩阵匹配(首分类定主责,其余部门抄送) →
SLA 时限(紧急4h/一般24h/低72h) → 工单号 WO-YYYYMMDD-XXXX → 状态机流转 → 看板监管`

## 状态机

```
诉 求: pending → analyzing → analyzed → dispatched → resolved
                                    ▲            (工单办结联动)
                                    └── 拒单回退(可重新派单)

工 单: pending → dispatched → accepted → completed → resolved
              └── dispatch ──┴── reject → rejected(终态)
   动作:dispatch(派单) accept(接单) reject(拒单) complete(办结反馈) resolve(确认办结)
   非法流转(跨级/终态再动)→ BusinessError → 400 {code, message}
```

## API 一览

| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/api/appeals` | 提交诉求,立即分析,返回完整结果(多标签/情感/紧急度/摘要/来源) |
| POST | `/api/appeals/batch` | 批量分析(并发管控,逐条降级,绝不整体失败) |
| GET | `/api/appeals` | 列表:分页 + category/status/source/urgency/sentiment(_lte) 筛选 + `q` 关键词 |
| GET | `/api/appeals/export` | 诉求 CSV 导出(筛选同列表口径,UTF-8-BOM 流式) |
| GET | `/api/appeals/{id}` | 诉求详情(含关联工单、分析历史、Token 消耗) |
| POST | `/api/appeals/{id}/dispatch` | 智能派单(body 可选 `department` 人工纠偏) |
| POST | `/api/appeals/{id}/reanalyze` | 重新分析(body 可选 `force_rule=true` 强制规则引擎;结果变化保留历史) |
| GET | `/api/appeals/departments` | 分类职责矩阵 |
| GET | `/api/workorders` | 工单列表:status/department 筛选 + `q` 工单号搜索 |
| GET | `/api/workorders/export` | 工单 CSV 导出(status/department 筛选,UTF-8-BOM 流式) |
| GET | `/api/workorders/{id}` | 工单详情(含诉求摘要与时间线) |
| POST | `/api/workorders/{id}/transition` | 状态机流转 `{action, note?, handler?}` |
| GET | `/api/stats` | 看板:总览/分类分布/近7日趋势/部门时效/风险预警/SLA(含 `sla_ending` 临期)/Token |
| GET | `/api/health` | 健康检查(运行模式/SLA/Token 管控配置) |

## 配置

复制 `.env.example` 为 `.env`,全部留空即本地模式:

```bash
# LLM(OpenAI 兼容协议,留空 = 内置规则引擎)
LLM_API_KEY=sk-xxx
LLM_BASE_URL=https://open.bigmodel.cn/api/paas/v4   # 或 https://api.deepseek.com/v1
LLM_MODEL=glm-4-flash                                # 或 deepseek-chat

ANALYSIS_TIMEOUT=15      # 单条分析超时(秒),超时自动降级
BATCH_CONCURRENCY=2      # 批量分析并发上限(Token 管控)
ANALYSIS_COMPACT=true    # 省流模式:精简 Prompt + 压缩输出(AC-4 Token 管控)
TOKEN_BUDGET_PER_APPEAL=600  # 单条分析 Token 预算(软约束,超出记告警日志)
SLA_HOURS_URGENT=4       # 紧急件办结时限
SLA_HOURS_NORMAL=24
SLA_HOURS_LOW=72

# 决赛环境切换 MySQL 8:
DATABASE_URL=mysql+pymysql://root:password@localhost:3306/appeal_dispatch
```

v1.0.0 老库(SQLite/MySQL)直接升级:启动时自动 `ALTER TABLE ADD COLUMN` 补齐新列
(appeals.tokens_used / appeals.analysis_history / daily_stats.tokens_used),老数据零丢失。

**离线部署**:无外网机器直接 `./startup.sh` 即可 —— SQLite 零配置 + 规则引擎离线分析,
全部 137 个测试也在离线环境跑通(`tests/conftest.py` 强制弹出 Key 环境变量)。

## 性能(实测)

| 指标 | 实测(M3,TestClient) | 护栏断言 |
|------|----------------------|----------|
| 单条规则分析 | ~0.01ms | <10ms(`test_rule_analysis_under_10ms`) |
| stats 看板聚合(60 诉求/20 工单,含 HTTP) | ~3.9ms | <100ms(`test_stats_endpoint_under_100ms`) |
| 诉求列表组合筛选(分类+关键词) | ~1.8ms | — |
| 10 线程并发提交+分析+派单全链 | ~61ms 全部成功 | 零串扰零失败(`test_10_concurrent_zero_cross_talk`) |
| /api/stats SQL 条数 | 6 条(全量内存聚合) | ≤8 条防 N+1 回归(`test_stats_query_count_bounded`) |

并发正确性:v1.0.2 修复两处真实缺陷 —— ① SQLite 文件库引擎 StaticPool 单连接在并发下互锁,
改为 QueuePool 多连接 + busy timeout;② 工单号"读当日最大序号+1"竞态撞唯一约束,
取号到落库进程内锁原子化 + 冲突重试。热路径(分析/派单/聚合)耗时均落 INFO 日志可现场观测。

## 测试

```bash
pip install -r requirements-dev.txt
pytest -q          # 137 个用例,全程离线
```

覆盖:规则引擎(复合案例/情感边界/紧急度安全词全覆盖/关键信息)、LLM 分析器(假 httpx 响应:正常/
超时/5xx/脏JSON/空参数/多余字段/缺字段/归一化)、职责匹配、SLA 三档与超时统计口径、工单号生成、
状态机(主线全流转/跨级非法/终态冻结/重复接单/拒单重派)、提交→分析→派单→流转→办结全闭环、
批量分析降级与脏数据拦截、组合筛选/分页边界、统计口径、health,以及 **XSS 防御三件套**
(payload 原样 JSON 往返、esc() 引号转义契约、用户字段拼接静态扫描 + JS 语法回归);
v1.0.1 新增:**Token 管控**(折算口径/省流 Prompt 长度与请求体断言/usage 捕获与折算兜底)、
**旧库补列迁移**(升级 + 幂等)、**重分析**(历史保留/强制规则/已派单重派/404/422)、
**CSV 导出**(BOM/表头/行数与筛选一致/部门筛选/422)、**SLA 临期四边界**(恰好 25% 计入、
33% 排除、超时排除、已完成排除)与看板 Token 字段增长;
v1.0.2 新增:**深色模式静态契约**(变量覆盖/切换/记忆/跟随系统/node 执行首帧脚本行为矩阵)、
**移动端断点**(480px/导航/表格滚动)、**筛选记忆契约**、**10 线程并发零串扰**、
**性能基准护栏**(规则分析 <10ms/聚合 <100ms)与 **stats SQL 条数护栏**。

## 项目结构

```
baisitong2/
├── baisitong2/               # Python 包
│   ├── main.py               # 应用装配 + 异常兜底
│   ├── config.py             # 环境变量集中配置
│   ├── database.py           # engine / 会话工厂
│   ├── errors.py             # 业务异常体系
│   ├── models/               # Appeal / WorkOrder / Department / DailyStat
│   ├── schemas/              # Pydantic DTO
│   ├── routers/              # appeals / workorders / stats
│   ├── services/             # analyzer / dispatcher / state_machine / stats / departments
│   └── static/               # 原生前端(4 页面)
├── bin/cli.js                # npm 启动器(探测 Python → ~/.baisitong2/venv)
├── tests/                    # pytest:单元 + API 集成(离线)
├── sql/init.sql              # MySQL 8 DDL + 预置部门
├── docs/项目流程.md/html     # 项目文档
└── startup.sh / startup.bat  # 一键启动
```

## License

MIT © yangxuan
