Files
dify/api/services/dify_builder/app_naming.py
T
Charles YaoandClaude Opus 5 65a47018b4 feat(dify-builder): say the app's name once when the build lands
Spec N4 has the build-complete card headline read "<App name> is ready", with
the annotation 名字只在回复句里念一次;记录卡上不出现 -- said once, in that
sentence, and never repeated on the completion receipt. We said it nowhere: the
card was titled "Build plan" and the engine had no app name to say.

DifyBuilderContext now carries app_name, seeded at session creation through a
get_app_name_fn seam alongside the existing revision/authorize ones. Env.rename_app
returns the name the database actually took rather than None, so the refine
records what landed instead of what it proposed -- the service normalizes after
the engine hands off, and the two can differ.

The headline is interpolated, so it gets a strings.py template or it would
survive localization untranslated. That pattern matches almost anything ending
in "is ready", so it sits last and every more specific template gets first
refusal.

The receipt already omitted the name; there is now a test pinning that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 18:09:23 -07:00

67 lines
2.6 KiB
Python

"""Store the Builder's proposed app name (spec N1).
The seam between the model's proposal and the app row. Strictly best-effort: a
build that is going fine must not fail because a cosmetic rename did, so every
failure is logged and swallowed and the app keeps its derived name.
"""
import logging
from sqlalchemy import select
from sqlalchemy.orm import Session, sessionmaker
from core.dify_builder import naming
from extensions.ext_database import db
from models.model import App
from services.app_service import AppService
logger = logging.getLogger(__name__)
def rename_app_from_proposal(*, app_id: str, tenant_id: str, account_id: str, proposed: str) -> str:
"""Store ``proposed`` as the app's name if usable; return what the app is now called.
Returns "" when nothing was stored, so the caller never records a name the
database does not have. Never raises. Opens its own session: this runs in
the Celery advance task, outside any Flask request.
"""
name = naming.normalize_proposed_name(proposed)
if not name:
return ""
try:
factory = sessionmaker(bind=db.engine, expire_on_commit=False)
with factory() as session:
app = _load_app(session, app_id=app_id, tenant_id=tenant_id)
if app is None:
return ""
if app.name == name:
return name
AppService().rename_app(app, name, account_id=account_id, session=session)
return name
except Exception:
# The app keeps its derived name; the card keeps its generic title.
logger.warning("dify_builder: renaming app %s failed; keeping the derived name", app_id, exc_info=True)
return ""
def current_app_name(*, app_id: str, tenant_id: str) -> str:
"""The app's stored name, or "" if it cannot be read. Never raises.
Seeds the Builder context so the build-complete card can name the app
(spec N4) without the engine reaching for the database itself.
"""
try:
factory = sessionmaker(bind=db.engine, expire_on_commit=False)
with factory() as session:
app = _load_app(session, app_id=app_id, tenant_id=tenant_id)
return app.name if app is not None else ""
except Exception:
logger.warning("dify_builder: reading the name of app %s failed", app_id, exc_info=True)
return ""
def _load_app(session: Session, *, app_id: str, tenant_id: str) -> App | None:
# Tenant-scoped: the actor's tenant is the only authority this path carries,
# so a cross-tenant id must find nothing rather than rename.
return session.scalar(select(App).where(App.id == app_id, App.tenant_id == tenant_id))