上线版: 产品表单统一+form嵌套修复+用户管理+部署+三套叙事

- 产品编辑入口统一走 ProductFormFull(卖点/风格/人群/品牌词全字段);
  修复开任务页 <form> 套 <form> 致"编辑产品"报错、改不了、跳回首个产品
- dashboard 入口卡片对齐实际路由: 系统管理(/config) 与 工作配置(/settings) 分开;
  settings ?tab=products 直达改用挂载后读 URL, 消除 hydration mismatch
- 新增用户管理(users API/admin service/改密页) + alembic 022/023/024
- 上线部署: Dockerfile / docker-compose.prod+https / nginx https / .env.example
- A8 三套正交叙事(痛点/场景/成分背书) + beige 调色去AI化 + 飞轮 text_import 高权重信号

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
yangqianqian
2026-06-30 18:08:13 +08:00
parent a77212781c
commit df1856d793
150 changed files with 8616 additions and 1765 deletions

View File

@@ -30,7 +30,11 @@ async function refreshToken(): Promise<string | null> {
try {
// 走 /auth/refresh二期实现一期降级跳登录
clearToken();
window.location.href = '/login';
// 带提示跳登录,避免“弹窗一闪就被踢、看不清原因”
if (typeof window !== 'undefined') {
const back = encodeURIComponent(window.location.pathname);
window.location.href = `/login?reason=expired&from=${back}`;
}
return null;
} finally {
refreshPromise = null;
@@ -67,7 +71,13 @@ async function request<T>(
}
}
throw err;
const apiError = new Error(err.message || `请求失败(${err.code || res.status})`) as Error & {
code?: number;
status?: number;
};
apiError.code = err.code;
apiError.status = res.status;
throw apiError;
}
// ─── 公开方法 ────────────────────────────────────────────────

View File

@@ -70,11 +70,18 @@ export class SseClient {
// SSE 必须直连后端,绕过 Next dev rewrite 代理——后者会缓冲/吞掉 EventSource 的
// 流式事件(实测:经代理 EventSource 只触发 open零事件直连后端事件正常
// NEXT_PUBLIC_SSE_BASE_URL 缺省时退回 NEXT_PUBLIC_API_BASE_URL再退回相对路径。
const sseBase = (
let sseBase = (
process.env.NEXT_PUBLIC_SSE_BASE_URL ||
process.env.NEXT_PUBLIC_API_BASE_URL ||
''
).replace(/\/$/, '');
if (
typeof window !== 'undefined' &&
process.env.NODE_ENV === 'development' &&
(window.location.hostname === '127.0.0.1' || window.location.hostname === 'localhost')
) {
sseBase = `http://${window.location.hostname}:8000`;
}
const url = `${sseBase}/api/v1/tasks/${this.taskId}/stream?ticket=${ticket}&last_seq=${lastSeq}`;
this.es = new EventSource(url);
this.es.onopen = () => this.options.onStatusChange('connected');