1. 扫码确认那一刻页面报「服务器内部错误」:`bot_channel` 的轮询口子直接
`current_setting('app.bot_channel', true)::uuid`,而事务级 set_config 提交后
会把连接上的变量还原成空串而不是 NULL,空串 cast 就 InvalidTextRepresentation。
迁移 0015 改成 nullif(...,'')::uuid,与 app_ws_id() 同一写法。
2. 微信回话只回「已保存任务:…」/「已保存知识草稿:…」,不再带站点链接
(PUBLIC_BASE_URL 是内网地址,手机上打不开)。
3. 栏目名「接入」改名「机器人」;页面标题与提示文案同步。
pytest 151 passed;ruff check/format 干净;tsc 0;vite build 通过;
web/e2e/integrations.mjs 7 项全绿。
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
"""bot_channel 的轮询口子要容忍"空字符串"的 GUC
|
||||
|
||||
`set_config(..., true)` 提交后会把变量还原成上一次的值;连接池里这条连接如果本来没有
|
||||
会话级值,还原出来的是空串 `''` 而不是 NULL。`0014` 的策略直接
|
||||
`current_setting('app.bot_channel', true)::uuid`,空串一 cast 就
|
||||
`InvalidTextRepresentationError: invalid input syntax for type uuid: ""`,
|
||||
于是同一连接上下一个请求(比如扫码确认)500。
|
||||
|
||||
这里把策略重建成和 `app_ws_id()` 一样的写法:先 `nullif(..., '')` 再 cast。
|
||||
|
||||
Revision ID: 0015
|
||||
Revises: 0014
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
|
||||
from app.roles import APP_ROLE
|
||||
|
||||
revision = "0015"
|
||||
down_revision = "0014"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
STATEMENTS: tuple[str, ...] = (
|
||||
"DROP POLICY IF EXISTS bot_channel_worker ON bot_channel",
|
||||
"""
|
||||
CREATE POLICY bot_channel_worker ON bot_channel
|
||||
FOR ALL
|
||||
USING (
|
||||
id = nullif(current_setting('app.bot_channel', true), '')::uuid
|
||||
)
|
||||
WITH CHECK (
|
||||
id = nullif(current_setting('app.bot_channel', true), '')::uuid
|
||||
)
|
||||
""",
|
||||
f"GRANT SELECT, INSERT, UPDATE, DELETE ON bot_channel TO {APP_ROLE}",
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
for statement in STATEMENTS:
|
||||
op.execute(statement)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("DROP POLICY IF EXISTS bot_channel_worker ON bot_channel")
|
||||
op.execute(
|
||||
"""
|
||||
CREATE POLICY bot_channel_worker ON bot_channel
|
||||
FOR ALL
|
||||
USING (id = current_setting('app.bot_channel', true)::uuid)
|
||||
WITH CHECK (id = current_setting('app.bot_channel', true)::uuid)
|
||||
"""
|
||||
)
|
||||
@@ -166,18 +166,15 @@ async def create_knowledge(
|
||||
return item
|
||||
|
||||
|
||||
def item_link(base_url: str, ws_id: uuid.UUID, item: Item) -> str:
|
||||
"""给人点的链接。`base_url` 传站点根(http://host:8200),不带 /api/v1。"""
|
||||
base = base_url.rstrip("/")
|
||||
if item.kind == "knowledge":
|
||||
return f"{base}/w/{ws_id}/knowledge/{item.id}"
|
||||
return f"{base}/w/{ws_id}?open={item.id}"
|
||||
def reply_text(kind: str, item: Item) -> str:
|
||||
"""回话只说"收下了",不带链接。
|
||||
|
||||
|
||||
def reply_text(kind: str, item: Item, link: str) -> str:
|
||||
链接是站点地址,可"站点地址"对手机不一定打得开(内网 IP、端口、还没上 HTTPS 的域名),
|
||||
发过去反而像坏消息。要看详情回网页里看就行。
|
||||
"""
|
||||
if kind == "knowledge":
|
||||
return f"已存成知识草稿:{item.title}\n{link}"
|
||||
return f"已记下任务:{item.title}\n{link}"
|
||||
return f"已保存知识草稿:{item.title}"
|
||||
return f"已保存任务:{item.title}"
|
||||
|
||||
|
||||
async def handle_text(
|
||||
@@ -208,7 +205,7 @@ async def handle_text(
|
||||
return {
|
||||
"action": "knowledge",
|
||||
"item_id": item.id,
|
||||
"reply": reply_text("knowledge", item, item_link(base_url, channel.workspace_id, item)),
|
||||
"reply": reply_text("knowledge", item),
|
||||
}
|
||||
item = await create_task(
|
||||
session,
|
||||
@@ -221,5 +218,5 @@ async def handle_text(
|
||||
return {
|
||||
"action": "task",
|
||||
"item_id": item.id,
|
||||
"reply": reply_text("task", item, item_link(base_url, channel.workspace_id, item)),
|
||||
"reply": reply_text("task", item),
|
||||
}
|
||||
|
||||
@@ -176,7 +176,9 @@ async def test_plain_text_becomes_task(client: AsyncClient) -> None:
|
||||
await session.commit()
|
||||
|
||||
assert result["action"] == "task"
|
||||
assert "已记下任务" in result["reply"]
|
||||
# 回话里只确认"收下了",不带站点链接(站点地址在手机上不一定打得开)
|
||||
assert result["reply"] == "已保存任务:给客户回电话"
|
||||
assert "http" not in result["reply"]
|
||||
async with SessionFactory() as session:
|
||||
await bind_tenant(session, ws_id, uuid.UUID(user["user_id"]))
|
||||
item = await session.get(Item, result["item_id"])
|
||||
@@ -207,6 +209,7 @@ async def test_knowledge_prefix_creates_draft_with_tags(client: AsyncClient) ->
|
||||
await session.commit()
|
||||
|
||||
assert result["action"] == "knowledge"
|
||||
assert result["reply"] == "已保存知识草稿:回滚前先停灰度"
|
||||
async with SessionFactory() as session:
|
||||
await bind_tenant(session, ws_id, uuid.UUID(user["user_id"]))
|
||||
item = await session.get(Item, result["item_id"])
|
||||
@@ -279,7 +282,7 @@ async def test_poller_handles_message_once_and_replies(
|
||||
|
||||
assert len(replies) == 1
|
||||
assert replies[0][0] == "wx-user-9"
|
||||
assert "已记下任务" in replies[0][1]
|
||||
assert replies[0][1] == "已保存任务:整理一下本周排期"
|
||||
|
||||
async with SessionFactory() as session:
|
||||
await session.execute(
|
||||
|
||||
Reference in New Issue
Block a user