feat: 2026-07-11 全量更新 - 代办集成+会议室预定+知识迭代修复+UI统一+Bug修复

== 已部署上线 (9项) ==
- 代办事项真实数据源集成 (企微审批API 8bug修复链)
- H5/坐席端 Logo样式统一+绿色背景
- 视频引导页修复 (localStorage key v2)
- 坐席端 v9 Vue版本修复 (ElMessage._context)
- 截图按钮 v10 修复 (getDisplayMedia user gesture)
- 扫码样式恢复+H5扫码登录跳转修复
- H5截图快捷键提示

== 代码完成待部署 (3项) ==
- 知识迭代3Bug修复 (#8 POST端点/#7 MERGE幂等/#6 过期检查)
- 会议室预定-小鱼易联终端 (40文件, 40/40测试通过)
- IT资产升级审批推送 (asset_service.py)

== 需求文档 (2项) ==
- 坐席端AI辅助消息框-PRD (4项新功能确认)
- 坐席端布局优化建议 v2.0 (7天计划)

== 新增文档 ==
- 日报-2026-07-11.md
- 知识迭代Bug修复报告-20260711.md
- 会议室预定-部署指南.md
- CHANGELOG.md 更新

== 测试 ==
- test_todo_integration.py: 40/40
- test_meetingroom.py: 40/40
- test_bugfix_ki_suggestions.py: 21/21
This commit is contained in:
Simon
2026-07-11 23:13:10 +08:00
parent 3d152fc8eb
commit bea288e414
928 changed files with 85169 additions and 54205 deletions
@@ -13,7 +13,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '041_message_server_timestamp'
down_revision: Union[str, None] = '027_audit_logs'
down_revision: Union[str, None] = '028_merge_heads'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
@@ -0,0 +1,69 @@
"""add login_logs table for unified auth
新建 login_logs 表,记录所有登录尝试(成功/失败),用于安全审计和故障排查。
三端认证重构:统一认证后,所有登录方式(oauth/qrcode/bind)都记录到此表。
关联设计文档:技术方案-认证模块重构
Revision ID: 046_add_login_logs
Revises: 045_add_graph_confidence_audience
Create Date: 2026-07-10
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '046_add_login_logs'
down_revision: Union[str, None] = '045_graph_confidence_audience'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# --------------------------------------------------------------------------
# 新建 login_logs 表
# --------------------------------------------------------------------------
op.create_table(
'login_logs',
sa.Column('id', sa.String(36), primary_key=True, comment='登录日志唯一标识'),
sa.Column('employee_id', sa.String(64), nullable=True, comment='企微员工UserID'),
sa.Column('corp_id', sa.String(64), nullable=False, comment='企业微信企业ID'),
sa.Column('login_method', sa.String(20), nullable=False,
comment='登录方式: oauth/qrcode/bind'),
sa.Column('login_source', sa.String(20), nullable=False,
comment='登录来源: h5/agent/admin'),
sa.Column('ip_address', sa.String(45), nullable=True, comment='客户端IP地址'),
sa.Column('user_agent', sa.String(512), nullable=True, comment='客户端User-Agent'),
sa.Column('status', sa.String(20), nullable=False,
comment='登录状态: success/failed/cancelled'),
sa.Column('fail_reason', sa.String(256), nullable=True, comment='失败原因'),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False,
server_default=sa.func.now(), comment='登录时间'),
)
# --------------------------------------------------------------------------
# 创建索引
# --------------------------------------------------------------------------
op.create_index('idx_login_logs_employee_id', 'login_logs', ['employee_id'])
op.create_index('idx_login_logs_corp_id', 'login_logs', ['corp_id'])
op.create_index('idx_login_logs_created_at', 'login_logs', ['created_at'])
op.create_index('idx_login_logs_status', 'login_logs', ['status'])
def downgrade() -> None:
# --------------------------------------------------------------------------
# 删除索引
# --------------------------------------------------------------------------
op.drop_index('idx_login_logs_status', table_name='login_logs')
op.drop_index('idx_login_logs_created_at', table_name='login_logs')
op.drop_index('idx_login_logs_corp_id', table_name='login_logs')
op.drop_index('idx_login_logs_employee_id', table_name='login_logs')
# --------------------------------------------------------------------------
# 删除表
# --------------------------------------------------------------------------
op.drop_table('login_logs')
@@ -0,0 +1,194 @@
"""add business_contacts and routing_events tables
新建 business_contacts 表(业务联系人)和 routing_events 表(路由命中统计),
并预置初始联系人数据(行政/HR/财务/法务/物业 各1-2人)。
关联设计文档:docs/03-技术架构/业务路由推荐-架构设计.md
Revision ID: 047_business_contacts
Revises: 046_add_login_logs
Create Date: 2026-07-15
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '047_business_contacts'
down_revision: Union[str, None] = '046_add_login_logs'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# --------------------------------------------------------------------------
# 新建 business_contacts 表
# --------------------------------------------------------------------------
op.create_table(
'business_contacts',
sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True,
comment='主键'),
sa.Column('name', sa.String(50), nullable=False, comment='联系人姓名'),
sa.Column('gender', sa.String(10), nullable=False, server_default='male',
comment='性别(male/female'),
sa.Column('department', sa.String(100), nullable=False, comment='部门名称'),
sa.Column('position', sa.String(100), nullable=False, comment='岗位'),
sa.Column('responsibility', sa.String(500), nullable=False, comment='负责业务描述'),
sa.Column('extension', sa.String(20), nullable=True, comment='分机号'),
sa.Column('service_area', sa.String(200), nullable=True, comment='服务区域/办公地点'),
sa.Column('wecom_userid', sa.String(100), nullable=False, comment='企微用户ID'),
sa.Column('avatar_url', sa.String(500), nullable=True, comment='头像URL'),
sa.Column('business_category', sa.String(50), nullable=False,
comment='业务类别(行政/人力资源/财务/法务/行政-物业)'),
sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.text('true'),
comment='是否启用'),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False,
server_default=sa.func.now(), comment='创建时间'),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False,
server_default=sa.func.now(), comment='更新时间'),
)
# 索引:按业务类别 + 是否启用查询
op.create_index(
'idx_business_contacts_category',
'business_contacts',
['business_category', 'is_active'],
)
# --------------------------------------------------------------------------
# 新建 routing_events 表(P1
# --------------------------------------------------------------------------
op.create_table(
'routing_events',
sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True,
comment='主键'),
sa.Column('conversation_id', sa.String(36),
sa.ForeignKey('conversations.id', ondelete='CASCADE'),
nullable=False, comment='会话ID'),
sa.Column('employee_id', sa.String(64), nullable=False, comment='员工ID'),
sa.Column('message_content', sa.String(500), nullable=False,
comment='触发路由的员工消息(截断)'),
sa.Column('business_category', sa.String(50), nullable=False,
comment='识别的业务类别'),
sa.Column('routing_confidence', sa.Float(), nullable=False, comment='路由置信度'),
sa.Column('contact_id', sa.Integer(),
sa.ForeignKey('business_contacts.id', ondelete='SET NULL'),
nullable=True, comment='推荐的联系人ID'),
sa.Column('contact_name', sa.String(50), nullable=False,
comment='联系人姓名(冗余)'),
sa.Column('is_clicked', sa.Boolean(), nullable=False, server_default=sa.text('false'),
comment='是否点击了联系TA'),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False,
server_default=sa.func.now(), comment='创建时间'),
)
# 索引
op.create_index('idx_routing_events_category', 'routing_events', ['business_category'])
op.create_index('idx_routing_events_conv', 'routing_events', ['conversation_id'])
# --------------------------------------------------------------------------
# 预置初始联系人数据
# --------------------------------------------------------------------------
# 说明:wecom_userid 使用占位符,上线前需替换为真实的企微通讯录 userid
# --------------------------------------------------------------------------
contacts_data = [
# 行政(2人)
{
'name': '王芳', 'gender': 'female', 'department': '行政部',
'position': '设备管理岗', 'responsibility': '打印机/复印机/扫描仪',
'extension': '8002', 'service_area': '滨江园区 3-5楼',
'wecom_userid': 'WangFang', 'avatar_url': '',
'business_category': '行政', 'is_active': True,
},
{
'name': '陈伟', 'gender': 'male', 'department': '行政部',
'position': '行政事务岗', 'responsibility': '办公用品/名片印刷/保洁服务',
'extension': '8003', 'service_area': '滨江园区 1-2楼',
'wecom_userid': 'ChenWei', 'avatar_url': '',
'business_category': '行政', 'is_active': True,
},
# 人力资源(2人)
{
'name': '李娜', 'gender': 'female', 'department': '人力资源部',
'position': '员工服务岗', 'responsibility': '工牌补办/考勤异常/入职手续',
'extension': '8005', 'service_area': '滨江园区 A栋3楼',
'wecom_userid': 'LiNa', 'avatar_url': '',
'business_category': '人力资源', 'is_active': True,
},
{
'name': '张磊', 'gender': 'male', 'department': '人力资源部',
'position': '薪酬福利岗', 'responsibility': '社保/公积金/离职手续',
'extension': '8006', 'service_area': '滨江园区 A栋3楼',
'wecom_userid': 'ZhangLei', 'avatar_url': '',
'business_category': '人力资源', 'is_active': True,
},
# 财务(1人)
{
'name': '刘洋', 'gender': 'male', 'department': '财务部',
'position': '费用报销岗', 'responsibility': '报销/发票/借款/工资条',
'extension': '8010', 'service_area': '滨江园区 B栋4楼',
'wecom_userid': 'LiuYang', 'avatar_url': '',
'business_category': '财务', 'is_active': True,
},
# 法务(1人)
{
'name': '赵敏', 'gender': 'female', 'department': '法务部',
'position': '法务顾问岗', 'responsibility': '合同/法律咨询/知识产权',
'extension': '8015', 'service_area': '滨江园区 C栋5楼',
'wecom_userid': 'ZhaoMin', 'avatar_url': '',
'business_category': '法务', 'is_active': True,
},
# 行政-物业(1人)
{
'name': '孙强', 'gender': 'male', 'department': '行政部',
'position': '物业管理岗', 'responsibility': '空调/电梯/门禁/停车',
'extension': '8008', 'service_area': '滨江园区 全园区',
'wecom_userid': 'SunQiang', 'avatar_url': '',
'business_category': '行政-物业', 'is_active': True,
},
]
# 批量插入初始数据
op.bulk_insert(
sa.table(
'business_contacts',
sa.column('name', sa.String),
sa.column('gender', sa.String),
sa.column('department', sa.String),
sa.column('position', sa.String),
sa.column('responsibility', sa.String),
sa.column('extension', sa.String),
sa.column('service_area', sa.String),
sa.column('wecom_userid', sa.String),
sa.column('avatar_url', sa.String),
sa.column('business_category', sa.String),
sa.column('is_active', sa.Boolean),
sa.column('created_at', sa.DateTime),
sa.column('updated_at', sa.DateTime),
),
[
{
**c,
'avatar_url': c['avatar_url'] or None,
'extension': c.get('extension') or None,
'service_area': c.get('service_area') or None,
'created_at': sa.func.now(),
'updated_at': sa.func.now(),
}
for c in contacts_data
],
)
def downgrade() -> None:
# 删除索引
op.drop_index('idx_routing_events_conv', table_name='routing_events')
op.drop_index('idx_routing_events_category', table_name='routing_events')
op.drop_index('idx_business_contacts_category', table_name='business_contacts')
# 删除表
op.drop_table('routing_events')
op.drop_table('business_contacts')
@@ -0,0 +1,70 @@
"""add information_items table and paused_at column (复杂场景重构第一阶段)
新建 auto_information_items 表(信息项管理),并在 auto_sessions 表新增
paused_at 字段(暂停时间戳,用于超时计算)。
关联设计文档:docs/03-技术架构/复杂场景重构第一阶段-架构设计.md
Revision ID: 048_add_information_items
Revises: 047_business_contacts
Create Date: 2026-07-20
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '048_add_information_items'
down_revision: Union[str, None] = '047_business_contacts'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# --------------------------------------------------------------------------
# 1. 新建 auto_information_items 表
# --------------------------------------------------------------------------
op.create_table(
'auto_information_items',
sa.Column('id', sa.String(36), primary_key=True),
sa.Column('session_id', sa.String(36), nullable=False, index=True,
comment='关联会话ID(弱关联,不建外键)'),
sa.Column('name', sa.String(128), nullable=False, comment='信息项名称'),
sa.Column('value', sa.Text(), nullable=False, server_default='', comment='当前值'),
sa.Column('modifiers', sa.JSON(), nullable=False, server_default='[]',
comment='修饰符列表,如 ["固定","必需"]'),
sa.Column('is_filled', sa.Boolean(), nullable=False, server_default=sa.false(),
comment='是否已填写'),
sa.Column('is_locked', sa.Boolean(), nullable=False, server_default=sa.false(),
comment='是否已锁定(固定修饰符 + 关联动作已执行 → True)'),
sa.Column('version', sa.Integer(), nullable=False, server_default='1', comment='版本号'),
sa.Column('update_history', sa.JSON(), nullable=False, server_default='[]',
comment='变更历史数组'),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False,
server_default=sa.text('NOW()'), comment='创建时间'),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False,
server_default=sa.text('NOW()'), comment='最后更新时间'),
)
op.create_index('idx_info_items_session_id', 'auto_information_items', ['session_id'])
op.create_index('idx_info_items_session_name', 'auto_information_items', ['session_id', 'name'])
# --------------------------------------------------------------------------
# 2. auto_sessions 表新增 paused_at 字段
# --------------------------------------------------------------------------
op.add_column(
'auto_sessions',
sa.Column('paused_at', sa.DateTime(timezone=True), nullable=True,
comment='暂停时间戳,用于超时计算;恢复后置NULL'),
)
def downgrade() -> None:
# 回滚 auto_sessions 新增字段
op.drop_column('auto_sessions', 'paused_at')
# 回滚 auto_information_items 表
op.drop_index('idx_info_items_session_name', table_name='auto_information_items')
op.drop_index('idx_info_items_session_id', table_name='auto_information_items')
op.drop_table('auto_information_items')
@@ -0,0 +1,63 @@
"""add p2 context compression and p3 snapshot tables
Revision ID: 049_add_p2_p3_tables
Revises: 048_add_information_items
Create Date: 2025-07-11
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision: str = "049_add_p2_p3_tables"
down_revision: Union[str, None] = "048_add_information_items"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# 1. 新增 auto_context_compressions 表
op.create_table(
"auto_context_compressions",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("session_id", sa.String(36), nullable=False),
sa.Column("tokens_before", sa.Integer(), nullable=False),
sa.Column("tokens_after", sa.Integer(), nullable=False),
sa.Column("compression_ratio", sa.Numeric(5, 2), nullable=False),
sa.Column("task_node", sa.String(128), nullable=True),
sa.Column("duration_ms", sa.Integer(), nullable=False),
sa.Column("compression_level", sa.SmallInteger(), nullable=False, server_default="1"),
sa.Column("summary", sa.Text(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("ix_auto_context_compressions_session_id", "auto_context_compressions", ["session_id"])
# 2. 新增 auto_information_snapshots 表
op.create_table(
"auto_information_snapshots",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("session_id", sa.String(36), nullable=False),
sa.Column("trigger_item_key", sa.String(64), nullable=False),
sa.Column("snapshot_data", postgresql.JSONB(), nullable=False),
sa.Column("correction_ids", postgresql.JSONB(), nullable=False, server_default="[]"),
sa.Column("is_undone", sa.Boolean(), nullable=False, server_default="false"),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("ix_auto_information_snapshots_session_id", "auto_information_snapshots", ["session_id"])
# 3. auto_information_items 表新增列
op.add_column("auto_information_items", sa.Column("derived_from", postgresql.JSONB(), nullable=True))
op.add_column("auto_information_items", sa.Column("correction_reason", sa.String(200), nullable=True))
def downgrade() -> None:
op.drop_column("auto_information_items", "correction_reason")
op.drop_column("auto_information_items", "derived_from")
op.drop_index("ix_auto_information_snapshots_session_id", table_name="auto_information_snapshots")
op.drop_table("auto_information_snapshots")
op.drop_index("ix_auto_context_compressions_session_id", table_name="auto_context_compressions")
op.drop_table("auto_context_compressions")
@@ -0,0 +1,74 @@
"""add meetingroom terminal binding and booking snapshot tables
Revision ID: 050_add_meetingroom_tables
Revises: 049_add_p2_p3_tables
Create Date: 2026-07-15
新增表:
- terminal_room_binding: 终端-会议室绑定关系(小鱼易联终端SN ↔ 企微会议室ID)
- meetingroom_booking_snapshot: 会议室预定记录快照(P2统计用)
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# Alembic 修订标识符
revision: str = "050_add_meetingroom_tables"
down_revision: Union[str, None] = "049_add_p2_p3_tables"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""升级:创建 terminal_room_binding 和 meetingroom_booking_snapshot 表。"""
# 1. 终端-会议室绑定表
op.create_table(
"terminal_room_binding",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("terminal_sn", sa.String(64), nullable=False),
sa.Column("terminal_name", sa.String(100), nullable=False, server_default=""),
sa.Column("meetingroom_id", sa.Integer(), nullable=False),
sa.Column("meetingroom_name", sa.String(100), nullable=False, server_default=""),
sa.Column("location", sa.String(200), nullable=False, server_default=""),
sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.text("true")),
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
# terminal_sn 唯一索引(一个终端只能绑定一个会议室)
op.create_index("ix_terminal_room_binding_terminal_sn", "terminal_room_binding", ["terminal_sn"], unique=True)
# meetingroom_id 普通索引(一个会议室可被多个终端绑定)
op.create_index("ix_terminal_room_binding_meetingroom_id", "terminal_room_binding", ["meetingroom_id"])
# 2. 会议室预定记录快照表(P2统计用)
op.create_table(
"meetingroom_booking_snapshot",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("meetingroom_id", sa.Integer(), nullable=False),
sa.Column("booking_id", sa.String(64), nullable=False),
sa.Column("subject", sa.String(200), nullable=False, server_default=""),
sa.Column("booker", sa.String(64), nullable=False, server_default=""),
sa.Column("start_time", sa.DateTime(), nullable=False),
sa.Column("end_time", sa.DateTime(), nullable=False),
sa.Column("status", sa.Integer(), nullable=False, server_default="0"),
sa.Column("snapshot_date", sa.Date(), nullable=False),
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("ix_meetingroom_booking_snapshot_meetingroom_id", "meetingroom_booking_snapshot", ["meetingroom_id"])
op.create_index("ix_meetingroom_booking_snapshot_booking_id", "meetingroom_booking_snapshot", ["booking_id"])
op.create_index("ix_meetingroom_booking_snapshot_snapshot_date", "meetingroom_booking_snapshot", ["snapshot_date"])
def downgrade() -> None:
"""回滚:删除 meetingroom_booking_snapshot 和 terminal_room_binding 表。"""
op.drop_index("ix_meetingroom_booking_snapshot_snapshot_date", table_name="meetingroom_booking_snapshot")
op.drop_index("ix_meetingroom_booking_snapshot_booking_id", table_name="meetingroom_booking_snapshot")
op.drop_index("ix_meetingroom_booking_snapshot_meetingroom_id", table_name="meetingroom_booking_snapshot")
op.drop_table("meetingroom_booking_snapshot")
op.drop_index("ix_terminal_room_binding_meetingroom_id", table_name="terminal_room_binding")
op.drop_index("ix_terminal_room_binding_terminal_sn", table_name="terminal_room_binding")
op.drop_table("terminal_room_binding")