build: 打通 FastAPI 与 Vue 最小工程、依赖与数据库连接

后端入口、配置、Redis 与 Alembic 迁移就位;前端 Vite 工程可安装可构建。
This commit is contained in:
WangShen007 committed 2026-09-22 20:15:00 +08:00
1 parent fb0ddca4de
commit 2aee6286d6
167 files changed
+26082

No files matched your search

@@ -0,0 +1,184 @@
"""
Copyright 2024-2026 ChatterMate
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
"""repair agent roles left behind by the hosted signup seeder
Revision ID: repair_agent_role_seed_001
Revises: add_run_connector_status_001
Create Date: 2026-08-03
Two signup paths seed an organization's starter roles, and they disagreed. The
hosted one gave Agent only view_assigned_chats + manage_assigned_chats, and
marked BOTH Admin and Agent is_default=true. b7c4e91a2d38 repaired the orgs
that existed in July 2026; every org created since through hosted signup was
seeded wrong again. The seeders now share DEFAULT_AGENT_ROLE_PERMISSIONS, so
this is the last of it.
Unlike b7c4e91a2d38 this does NOT backfill by capability. That migration
widened every chat-capable role and promised admins the grant was theirs to
revoke:
"an admin who wants an agent kept out can uncheck the permission in Roles"
Running a capability backfill again would break that promise — a role someone
deliberately narrowed would silently get its permissions back. So the repair
matches the bad seed EXACTLY: a role qualifies only if its permission set is
precisely {view_assigned_chats, manage_assigned_chats}, no more and no less.
An edited role has a different set and is left alone. A community-seeded org
has four permissions and never matches, which is what makes this a no-op
outside hosted.
Also does NOT promote a role to is_default where an organization has none.
roles.py refuses to edit or delete a default role, so promoting Agent would
make it permanently uneditable through the API.
Idempotent: every insert is guarded by NOT EXISTS (role_permissions has no
primary key or unique constraint, so that guard is the only thing standing
between a re-run and duplicate rows), and the is_default repair is a no-op
once an org is down to one default.
"""
from alembic import op
# revision identifiers, used by Alembic.
revision = 'repair_agent_role_seed_001'
down_revision = 'add_run_connector_status_001'
branch_labels = None
depends_on = None
# The exact permission set the hosted seeder produced, and what it should have
# been. Sorted, because the match is done against a sorted aggregate.
BAD_SEED = ('manage_assigned_chats', 'view_assigned_chats')
MISSING_FROM_BAD_SEED = ('view_unassigned_chats', 'view_people')
# view_agents and view_knowledge became real permissions in the same release
# that this migration ships with — they were in the catalogue and the role
# editor for months while being enforced nowhere. Anyone holding the manage_*
# twin already passes those checks (they are OR'd), so this grant changes no
# behaviour. It exists so the role editor stops showing an admin role with
# "manage knowledge" ticked and "view knowledge" blank.
IMPLIED_BY_MANAGE = (
('view_agents', 'manage_agents'),
('view_knowledge', 'manage_knowledge'),
)
# Permission rows themselves may be absent on a deployment whose last org was
# created before the name was added to Permission.default_permissions().
REQUIRED_PERMISSIONS = (
('view_unassigned_chats', 'Can view unassigned AI chats'),
('view_people', 'Can view the people directory'),
('view_agents', 'Can view chat agents'),
('view_knowledge', 'Can view knowledge base'),
)
BAD_SEED_SIGNATURE = ",".join(sorted(BAD_SEED))
def _sql_list(values) -> str:
return ", ".join(f"'{value}'" for value in values)
def upgrade() -> None:
for name, description in REQUIRED_PERMISSIONS:
op.execute(
f"""
INSERT INTO permissions (name, description)
SELECT '{name}', '{description}'
WHERE NOT EXISTS (SELECT 1 FROM permissions WHERE name = '{name}')
"""
)
# Roles whose permission set is exactly the bad seed. HAVING on the sorted
# aggregate is what makes it exact — a role with one extra permission, or
# one fewer, produces a different string and is skipped.
#
# Both permissions go in ONE statement, deliberately. Granting them in
# separate statements does not work: the first grant changes the role's
# permission set, so it no longer matches the signature and the second
# statement finds nothing to do. The subquery here is evaluated once,
# against the pre-insert state.
op.execute(
f"""
INSERT INTO role_permissions (role_id, permission_id)
SELECT bad.role_id, target.id
FROM (
SELECT rp.role_id
FROM role_permissions rp
JOIN permissions p ON p.id = rp.permission_id
GROUP BY rp.role_id
HAVING string_agg(p.name, ',' ORDER BY p.name) = '{BAD_SEED_SIGNATURE}'
) bad
CROSS JOIN permissions target
WHERE target.name IN ({_sql_list(MISSING_FROM_BAD_SEED)})
AND NOT EXISTS (
SELECT 1 FROM role_permissions existing
WHERE existing.role_id = bad.role_id
AND existing.permission_id = target.id
)
"""
)
for implied, held in IMPLIED_BY_MANAGE:
op.execute(
f"""
INSERT INTO role_permissions (role_id, permission_id)
SELECT DISTINCT rp.role_id, target.id
FROM role_permissions rp
JOIN permissions holder ON holder.id = rp.permission_id
CROSS JOIN permissions target
WHERE holder.name = '{held}'
AND target.name = '{implied}'
AND NOT EXISTS (
SELECT 1 FROM role_permissions existing
WHERE existing.role_id = rp.role_id
AND existing.permission_id = target.id
)
"""
)
# One default role per organization, same repair as b7c4e91a2d38. Clearing
# Admin is what leaves Agent as the default; clearing Agent instead would
# hand every newly invited user full permissions.
#
# The COUNT(*) > 1 guard means an org whose only default is Admin keeps it
# rather than ending up with no default at all.
op.execute(
"""
UPDATE roles SET is_default = false
WHERE name = 'Admin'
AND is_default = true
AND organization_id IN (
SELECT organization_id FROM roles
WHERE is_default = true
GROUP BY organization_id
HAVING COUNT(*) > 1
)
"""
)
def downgrade() -> None:
"""Deliberately empty.
There is nothing safe to undo. Revoking the four permissions would strip
them from roles that legitimately hold them — this migration cannot tell
the grants it made from the ones the seeder wrote. Deleting the permission
rows, as b7c4e91a2d38's downgrade does, would cascade across every
organization. And the pre-repair is_default state was ambiguous by
definition, which is the whole reason for the repair.
"""