From d5d0494928f1f9fe5ec74789f41b3c9643b2d510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Tue, 16 Jun 2026 20:20:27 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat(task):=20=E4=BB=BB=E5=8A=A1=E8=AF=A6?= =?UTF-8?q?=E6=83=85(cop-task-detail)=E7=99=BB=E5=BD=95=E6=B7=B1=E9=93=BE?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=9C=AC=E5=9C=B0=E5=81=87=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BC=80=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 VITE_MOCK_COP_TASK_DETAIL 开关(默认 false 走真实接口;true 时该接口走本地假数据, 供 consumer 外网不可达时调试),与 VITE_USE_REAL_DATA 独立 - utils/request.ts 按开关把 cop-task-detail 请求切到本地 mock adapter - task-mock-data.ts 增加 cop-task-detail 假数据 + matcher/adapter - 各 .env(development/network/production/sit) + .env.example 补充该开关说明 Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend-web/.env.development | 3 + frontend-web/.env.example | 5 + frontend-web/.env.network | 3 + frontend-web/.env.production | 3 + frontend-web/.env.sit | 3 + .../task-components/task-mock-data.ts | 96 +++++++++++++++++++ frontend-web/src/utils/request.ts | 16 +++- 7 files changed, 128 insertions(+), 1 deletion(-) diff --git a/frontend-web/.env.development b/frontend-web/.env.development index 5ea0eb44..22563825 100644 --- a/frontend-web/.env.development +++ b/frontend-web/.env.development @@ -4,6 +4,9 @@ # 任务看板数据源:true=真实接口(默认);改成 false 可让任务分析/三情分析两页走本地假数据。 VITE_USE_REAL_DATA=true +# 任务详情(登录深链 cop-task-detail)数据源:默认真实接口;改成 true 走本地假数据(consumer 外网不可达时调试用)。 +VITE_MOCK_COP_TASK_DETAIL=false + # Gateway / DeerFlow API VITE_BACKEND_BASE_URL=http://127.0.0.1:8001 VITE_LANGGRAPH_BASE_URL=http://127.0.0.1:8001/api diff --git a/frontend-web/.env.example b/frontend-web/.env.example index e254e6c8..1b3d80bb 100644 --- a/frontend-web/.env.example +++ b/frontend-web/.env.example @@ -33,6 +33,11 @@ # 仅本机 .env 默认 false(假数据)。改后需重启 dev / 重新打包生效。 # VITE_USE_REAL_DATA=true +# --- 任务详情(登录深链 cop-task-detail)数据源开关 --- +# 默认真实接口(未设/false);改成 true 时该接口走本地假数据(consumer 外网不可达时调试用)。 +# 与 VITE_USE_REAL_DATA 独立。改后需重启 dev / 重新打包生效。 +# VITE_MOCK_COP_TASK_DETAIL=false + # MaxKey 鉴权根地址(无界 onlineTicket 换 token) # VITE_AUTH_BASE_URL=https://auth.y.sit.4.cn # VITE_AIPI_CONSUMER_URL=https://ch1.b.uat.4.cn/consumer diff --git a/frontend-web/.env.network b/frontend-web/.env.network index 569b8422..471b6289 100644 --- a/frontend-web/.env.network +++ b/frontend-web/.env.network @@ -4,6 +4,9 @@ # 任务看板数据源:true=真实接口(默认);改成 false 可让任务分析/三情分析两页走本地假数据。 VITE_USE_REAL_DATA=true +# 任务详情(登录深链 cop-task-detail)数据源:默认真实接口;改成 true 走本地假数据(调试用)。 +VITE_MOCK_COP_TASK_DETAIL=false + # Gateway / DeerFlow API VITE_BACKEND_BASE_URL=http://47.88.25.99:7001 VITE_LANGGRAPH_BASE_URL=http://47.88.25.99:7001/api diff --git a/frontend-web/.env.production b/frontend-web/.env.production index 08bf2a70..97415742 100644 --- a/frontend-web/.env.production +++ b/frontend-web/.env.production @@ -4,6 +4,9 @@ # 任务看板数据源:true=真实接口(默认);改成 false 可让任务分析/三情分析两页走本地假数据。 VITE_USE_REAL_DATA=true +# 任务详情(登录深链 cop-task-detail)数据源:默认真实接口;改成 true 走本地假数据(调试用)。 +VITE_MOCK_COP_TASK_DETAIL=false + # Gateway / DeerFlow API VITE_BACKEND_BASE_URL=https://ch1.b.uat.4.cn/deerflow VITE_LANGGRAPH_BASE_URL=https://ch1.b.uat.4.cn/deerflow/api diff --git a/frontend-web/.env.sit b/frontend-web/.env.sit index d4a4af7b..91ba650f 100644 --- a/frontend-web/.env.sit +++ b/frontend-web/.env.sit @@ -4,6 +4,9 @@ # 任务看板数据源:true=真实接口(默认);改成 false 可让任务分析/三情分析两页走本地假数据。 VITE_USE_REAL_DATA=true +# 任务详情(登录深链 cop-task-detail)数据源:默认真实接口;改成 true 走本地假数据(调试用)。 +VITE_MOCK_COP_TASK_DETAIL=false + # Gateway / DeerFlow API VITE_BACKEND_BASE_URL=https://ch1.b.sit.4.cn/deerflow VITE_LANGGRAPH_BASE_URL=https://ch1.b.sit.4.cn/deerflow/api diff --git a/frontend-web/src/strategy-components/components/task-components/task-mock-data.ts b/frontend-web/src/strategy-components/components/task-components/task-mock-data.ts index dfad039b..9f6c5fc7 100644 --- a/frontend-web/src/strategy-components/components/task-components/task-mock-data.ts +++ b/frontend-web/src/strategy-components/components/task-components/task-mock-data.ts @@ -399,6 +399,102 @@ const MOCK_ROUTES: Array<{ match: string; dataset: MockDataset }> = [ { match: '/task/cop-tasks', dataset: 'task' }, ]; +// ───────────── 任务详情假数据(cop-task-detail,给登录任务深链用) ───────────── +// +// consumer 接口 `{VITE_AIPI_CONSUMER_URL}/taskAnalysSearch/cop-task-detail` 外网可能不可达, +// 可用假数据模拟登录深链(?taskId=&goPath=)效果。返回结构与真实接口一致: +// `{ code, message, data: { id, overview, taskDirection, taskName, taskContent } }` +// +// 启停由 `VITE_MOCK_COP_TASK_DETAIL` 控制(默认真实数据,=true 时走假数据), +// 见 `utils/request.ts` 的请求拦截器。 + +/** cop-task-detail 命中路径。 */ +const COP_TASK_DETAIL_ROUTE = '/taskAnalysSearch/cop-task-detail'; + +interface CopTaskDetailSeed { + taskName: string; + taskContent: string; + overview: string; + taskDirection: string; +} + +/** 任务详情假数据池(按 taskId 取模选一条,让不同 taskId 呈现不同任务)。 */ +const COP_TASK_DETAIL_SEEDS: CopTaskDetailSeed[] = [ + { + taskName: '暑期文旅消费季舆情倾向分析', + taskContent: + '围绕"暑期文旅消费季"全网讨论,分析公众情绪走向、核心关切与潜在争议点,识别需重点引导的群体,并给出传播策略建议。', + overview: '暑期文旅消费季相关讨论的情绪与倾向分析。', + taskDirection: '王磊', + }, + { + taskName: '居民阶梯电价调整公众情绪研判', + taskContent: + '针对居民阶梯电价调整方案,梳理公众主要担忧(用电成本、公平性),判断情绪峰值节点与高敏群体,形成分群体沟通要点。', + overview: '阶梯电价调整的公众情绪研判。', + taskDirection: '李婷', + }, + { + taskName: '城市更新片区舆论引导任务', + taskContent: + '对老城区改造涉及的拆迁、安置等敏感议题进行情绪与诉求拆解,识别居民、商户等不同立场,制定差异化回应策略。', + overview: '城市更新片区敏感议题的舆论引导分析。', + taskDirection: '张伟', + }, +]; + +/** 从 taskId 反推一个稳定下标(同一 taskId 每次返回同一条)。 */ +function pickSeedIndex(taskId: string): number { + const digits = (taskId.match(/\d+/g) ?? []).join(''); + const n = digits ? Number(digits.slice(-6)) : 0; + return Number.isFinite(n) ? n % COP_TASK_DETAIL_SEEDS.length : 0; +} + +/** 模拟 cop-task-detail 的 body。 */ +function buildCopTaskDetailBody(taskId: string) { + const seed = COP_TASK_DETAIL_SEEDS[pickSeedIndex(taskId)]; + return { + code: 0, + message: 'ok', + data: { + id: taskId, + overview: seed.overview, + taskDirection: seed.taskDirection, + taskName: seed.taskName, + taskContent: seed.taskContent, + }, + }; +} + +/** 判断某请求 url 是否是 cop-task-detail。 */ +export function matchCopTaskDetailMock(url?: string): boolean { + return !!url && url.includes(COP_TASK_DETAIL_ROUTE); +} + +/** 从 axios config 取 taskId(优先 params,其次 url query)。 */ +function readTaskIdFromConfig(config: { url?: string; params?: Record }): string { + const fromParams = config.params?.taskId; + if (fromParams != null && fromParams !== '') return String(fromParams); + if (config.url) { + const m = config.url.match(/[?&]taskId=([^&]+)/); + if (m) return decodeURIComponent(m[1]); + } + return ''; +} + +/** 临时假数据 axios adapter:命中 cop-task-detail 时直接返回 mock body,不发真实请求。 */ +export function copTaskDetailMockAdapter(config: { + url?: string; + params?: Record; +}): Promise<{ data: unknown; status: number; statusText: string; headers: Record; config: unknown }> { + const body = buildCopTaskDetailBody(readTaskIdFromConfig(config)); + return new Promise((resolve) => { + setTimeout(() => { + resolve({ data: body, status: 200, statusText: 'OK', headers: {}, config }); + }, 200); + }); +} + /** 判断某个请求 url 是否应走假数据。 */ export function matchTaskBoardMock(url?: string): MockDataset | null { if (!url) return null; diff --git a/frontend-web/src/utils/request.ts b/frontend-web/src/utils/request.ts index 6666aa95..9162dae1 100644 --- a/frontend-web/src/utils/request.ts +++ b/frontend-web/src/utils/request.ts @@ -1,5 +1,10 @@ import axios from 'axios' -import { matchTaskBoardMock, taskBoardMockAdapter } from '@/strategy-components/components/task-components/task-mock-data' +import { + matchTaskBoardMock, + taskBoardMockAdapter, + matchCopTaskDetailMock, + copTaskDetailMockAdapter, +} from '@/strategy-components/components/task-components/task-mock-data' // 任务看板(任务分析 / 三情分析)数据源开关: // VITE_USE_REAL_DATA=true → 走真实接口;其它(含未设置)→ 这两个接口走本地假数据。 @@ -7,6 +12,11 @@ import { matchTaskBoardMock, taskBoardMockAdapter } from '@/strategy-components/ // 仅本机 .env 默认 false(假数据)。改 env 后需重启 dev server 生效。 const USE_REAL_DATA = import.meta.env.VITE_USE_REAL_DATA === 'true' +// 任务详情(cop-task-detail,登录深链用)数据源开关: +// 默认真实数据;仅当 VITE_MOCK_COP_TASK_DETAIL=true 时走本地假数据(consumer 外网不可达时调试用)。 +// 与 USE_REAL_DATA 独立。改 env 后需重启 dev server 生效。 +const MOCK_COP_TASK_DETAIL = import.meta.env.VITE_MOCK_COP_TASK_DETAIL === 'true' + // axios.interceptors const service = axios.create({ // baseURL: '', // url = base url + request url @@ -43,6 +53,10 @@ service.interceptors.request.use( if (!USE_REAL_DATA && matchTaskBoardMock(config.url)) { config.adapter = taskBoardMockAdapter as any; } + // 任务详情(登录深链):默认真实接口;仅 VITE_MOCK_COP_TASK_DETAIL=true 时走本地假数据。 + if (MOCK_COP_TASK_DETAIL && matchCopTaskDetailMock(config.url)) { + config.adapter = copTaskDetailMockAdapter as any; + } // let token: any = localStorage.getItem('token'); config.headers['Authorization'] = `Bearer admin`; // if (token) { From 2fce5e61c95f09e6dbb83ca78217cd3fadc63c0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Tue, 16 Jun 2026 20:56:40 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat(ui):=20=E5=85=A8=E9=87=8F=E7=89=88?= =?UTF-8?q?=E9=80=9A=E7=94=A8=E9=97=AE=E7=AD=94=E4=BE=A7=E6=A0=8F=E6=B7=B1?= =?UTF-8?q?=E8=93=9D=E4=B8=BB=E9=A2=98=E9=80=82=E9=85=8D=20+=20=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E6=B7=B1=E9=93=BE=E6=8E=A5=E5=8F=A3=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - general-qa-sidebar:侧栏底色 bg-white → bg-sidebar(随主题,深蓝主题=深蓝); 「新对话」按钮底色 color-mix 混 transparent 而非 white;智能体图标底色改用 《深蓝 UI 规范》§1.1.7/§1.1.8 的蓝-青系(替换原高彩度玫瑰/琥珀/翠绿渐变) - task-deeplink:cop-task-detail 接口路径修正 taskAnalysSearch → taskAnalyseSearch - 同步 mock-data 路径 + CLAUDE.md 文案 Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 2 +- .../workspace/general-qa-sidebar.tsx | 24 +++++++++++-------- frontend-web/src/core/auth/task-deeplink.ts | 4 ++-- .../task-components/task-mock-data.ts | 4 ++-- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 24bf01d3..54243962 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -84,7 +84,7 @@ The frontend (`frontend-web/`) is a React 19 + Vite SPA: - **LLM streaming**: `@langchain/langgraph-sdk` streams messages from the backend via SSE - **UI**: Radix UI primitives + TailwindCSS v4 + shadcn-style components in `src/components/ui/` - **Shims**: `src/shims/` provides `next/link` and `next/navigation` polyfills (original code was Next.js) -- **Login URL params** (`LoginPage.tsx`, frontend-only — the backend never sees these): besides the auth params (`ticket`/`userId`/`env`/`theme`/`yUserId`/`token`/`access_token`/`authToken`/`password`), the page supports a **task deep-link** via `?taskId=&goPath=`. After login it calls the consumer API `GET {VITE_AIPI_CONSUMER_URL}/taskAnalysSearch/cop-task-detail?taskId=` (header `Authorization: Bearer admin`, auto-injected by `utils/request.ts`), stashes the full `data` (`id/overview/taskDirection/taskName/taskContent`) into `sessionStorage["login.copTaskDetail"]`, then routes by `goPath`: base codes `3qfx`/`rwfx`/`xdfx` → that business's configured **single agent** Q&A (resolved via `/api/business-mapping`, opening message in `sessionStorage.pendingAgentMessage`); the `-A` suffix (`3qfx-A`/…) → **multi-agent roundtable** with `roundtablePrefill.businessCode` so the page auto-selects the configured 业务链条. goPath→businessCode: `3qfx`→`3Q`, `rwfx`→`6BF`, `xdfx`→`7BF`. **Both targets render embedded under the 任务管理 layout** — nested routes under `task-management//{roundtable,agent/:agent_id/chats/:thread_id}` in `StrategyRoutes.tsx` (`` = `plan-task-sentiment` / `plan-task/plan-task-fx` / `plan-task/plan-task-action`, matching the COH中任务 menu paths in `sidebar-menu.ts`) so the global sidebar keeps 任务管理 → COH中任务 → the matching business highlighted. The agent chat is rendered via `TaskEmbeddedAgentChat`, which passes `AgentChatPage`'s `chatBasePath` prop so the post-send URL rewrite + new-chat button stay in the task-management context (instead of the standalone `/agents/...` route). Any failure (no consumer URL / fetch error / unmapped goPath / no configured agent) silently falls back to the default home. Helpers: `core/auth/task-deeplink.ts`. +- **Login URL params** (`LoginPage.tsx`, frontend-only — the backend never sees these): besides the auth params (`ticket`/`userId`/`env`/`theme`/`yUserId`/`token`/`access_token`/`authToken`/`password`), the page supports a **task deep-link** via `?taskId=&goPath=`. After login it calls the consumer API `GET {VITE_AIPI_CONSUMER_URL}/taskAnalyseSearch/cop-task-detail?taskId=` (header `Authorization: Bearer admin`, auto-injected by `utils/request.ts`), stashes the full `data` (`id/overview/taskDirection/taskName/taskContent`) into `sessionStorage["login.copTaskDetail"]`, then routes by `goPath`: base codes `3qfx`/`rwfx`/`xdfx` → that business's configured **single agent** Q&A (resolved via `/api/business-mapping`, opening message in `sessionStorage.pendingAgentMessage`); the `-A` suffix (`3qfx-A`/…) → **multi-agent roundtable** with `roundtablePrefill.businessCode` so the page auto-selects the configured 业务链条. goPath→businessCode: `3qfx`→`3Q`, `rwfx`→`6BF`, `xdfx`→`7BF`. **Both targets render embedded under the 任务管理 layout** — nested routes under `task-management//{roundtable,agent/:agent_id/chats/:thread_id}` in `StrategyRoutes.tsx` (`` = `plan-task-sentiment` / `plan-task/plan-task-fx` / `plan-task/plan-task-action`, matching the COH中任务 menu paths in `sidebar-menu.ts`) so the global sidebar keeps 任务管理 → COH中任务 → the matching business highlighted. The agent chat is rendered via `TaskEmbeddedAgentChat`, which passes `AgentChatPage`'s `chatBasePath` prop so the post-send URL rewrite + new-chat button stay in the task-management context (instead of the standalone `/agents/...` route). Any failure (no consumer URL / fetch error / unmapped goPath / no configured agent) silently falls back to the default home. Helpers: `core/auth/task-deeplink.ts`. ### Configuration diff --git a/frontend-web/src/components/workspace/general-qa-sidebar.tsx b/frontend-web/src/components/workspace/general-qa-sidebar.tsx index 8eb784ca..c935ea42 100644 --- a/frontend-web/src/components/workspace/general-qa-sidebar.tsx +++ b/frontend-web/src/components/workspace/general-qa-sidebar.tsx @@ -95,13 +95,15 @@ const MORE_CHATS_HREF = "/page/workspace/chats"; /** 常用智能体最多展示个数。 */ const MAX_FREQUENT = 4; -// 智能体图标统一用 Bot 机器人图标;底色按 id 稳定散列到一组主题色,复刻参考图的彩色方块。 +// 智能体图标统一用 Bot 机器人图标;底色按 id 稳定散列到一组**深蓝 UI 规范**的色值 +// (§1.1.7 装饰色 / §1.1.8 小面积色序,docs/ui规范-深蓝),克制的蓝/青系,深蓝主题下不刺眼, +// 浅主题也成立。图标做「主色→深一档」的轻渐变保留层次,不再用原来高彩度的玫瑰/琥珀/翠绿。 const AGENT_COLORS = [ - { from: "#fb7185", to: "#ef4444" }, - { from: "#60a5fa", to: "#3b82f6" }, - { from: "#fbbf24", to: "#f59e0b" }, - { from: "#34d399", to: "#10b981" }, - { from: "#a78bfa", to: "#8b5cf6" }, + { from: "#4096FF", to: "#1677FF" }, // 蓝(链接蓝 → 主色) + { from: "#36CFC9", to: "#13C2C2" }, // 青(辅色) + { from: "#6388FF", to: "#0958D9" }, // 紫蓝(装饰紫 → 主色深一档) + { from: "#5CDBD3", to: "#08979C" }, // 浅青 → 深青 + { from: "#69B1FF", to: "#003EB3" }, // 浅蓝 → 主色按下 ] as const; function agentColor(id: string) { @@ -309,12 +311,14 @@ export function GeneralQaSidebar() { const isStatic = env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"; return ( -
+ // 背景走 sidebar token(浅主题=白、深蓝主题=深蓝),不要写死 bg-white,否则全量版深蓝主题下侧栏仍是白底。 +
{/* ── 顶部:新对话胶囊 + 收起按钮 ── */}
{/* 新对话按钮:完全自定义样式(不复用 Button variant)。borderColor / color 走 - inline style,避免被全局 border-color(--border)覆盖;无阴影。浅主色底 + 主色描边 - + 5px 圆角,全部走 --primary token,自动适配红/蓝/深蓝主题。 */} + inline style,避免被全局 border-color(--border)覆盖;无阴影。主色描边 + 5px 圆角。 + 底色用「主色混入透明」=主色低透明度叠在当前侧栏底色上,浅/红/蓝/深蓝主题都自适应 + (不要混 white,否则深蓝主题下会变成浅色块)。 */} diff --git a/frontend-web/src/core/auth/task-deeplink.ts b/frontend-web/src/core/auth/task-deeplink.ts index f375ea01..603e75c4 100644 --- a/frontend-web/src/core/auth/task-deeplink.ts +++ b/frontend-web/src/core/auth/task-deeplink.ts @@ -3,7 +3,7 @@ * 拉取任务详情并据 goPath 把用户直接送到对应的智能体问答 / 多智能体会商页。 * * 流程: - * 1. 调 consumer 接口 ``GET {VITE_AIPI_CONSUMER_URL}/taskAnalysSearch/cop-task-detail?taskId=``, + * 1. 调 consumer 接口 ``GET {VITE_AIPI_CONSUMER_URL}/taskAnalyseSearch/cop-task-detail?taskId=``, * 请求头 ``Authorization: Bearer admin``(由 service 拦截器统一注入)。 * 2. 把返回的 data(id/overview/taskDirection/taskName/taskContent)整包存到 sessionStorage。 * 3. 按 goPath 解析出业务码 + 模式: @@ -74,7 +74,7 @@ export async function fetchCopTaskDetail(taskId: string): Promise; diff --git a/frontend-web/src/strategy-components/components/task-components/task-mock-data.ts b/frontend-web/src/strategy-components/components/task-components/task-mock-data.ts index 9f6c5fc7..da45f0b0 100644 --- a/frontend-web/src/strategy-components/components/task-components/task-mock-data.ts +++ b/frontend-web/src/strategy-components/components/task-components/task-mock-data.ts @@ -401,7 +401,7 @@ const MOCK_ROUTES: Array<{ match: string; dataset: MockDataset }> = [ // ───────────── 任务详情假数据(cop-task-detail,给登录任务深链用) ───────────── // -// consumer 接口 `{VITE_AIPI_CONSUMER_URL}/taskAnalysSearch/cop-task-detail` 外网可能不可达, +// consumer 接口 `{VITE_AIPI_CONSUMER_URL}/taskAnalyseSearch/cop-task-detail` 外网可能不可达, // 可用假数据模拟登录深链(?taskId=&goPath=)效果。返回结构与真实接口一致: // `{ code, message, data: { id, overview, taskDirection, taskName, taskContent } }` // @@ -409,7 +409,7 @@ const MOCK_ROUTES: Array<{ match: string; dataset: MockDataset }> = [ // 见 `utils/request.ts` 的请求拦截器。 /** cop-task-detail 命中路径。 */ -const COP_TASK_DETAIL_ROUTE = '/taskAnalysSearch/cop-task-detail'; +const COP_TASK_DETAIL_ROUTE = '/taskAnalyseSearch/cop-task-detail'; interface CopTaskDetailSeed { taskName: string; From 1ece2c5a4f80d227c0c358cdc38e9e429209b7c6 Mon Sep 17 00:00:00 2001 From: fengxiangdebaozi <10845145+fengxiangdebaozi@user.noreply.gitee.com> Date: Tue, 16 Jun 2026 20:58:46 +0800 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=85=A8=E9=87=8F?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E7=BB=BC=E5=90=88=E7=AE=A1=E7=90=86=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E6=8E=92=E8=A1=8C=E6=A6=9C=E6=B3=84=E6=BC=8F=E5=88=B0?= =?UTF-8?q?=E5=85=B6=E4=BB=96=E4=BE=A7=E8=BE=B9=E6=A0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 综合管理菜单存在两个 id 相同的「用户排行榜」子项,侧边栏以 key={item.id} 渲染导致 React keyed reconciliation 失效,离开综合 管理切换到其他一级菜单时重复 key 的节点无法正确卸载,残留到新的 侧边栏。删除冗余的 adminOnly 重复项(保留的项已通过 userPath 同时 覆盖 admin/非 admin)。 Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend-web/src/core/page-layout/sidebar-menu.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/frontend-web/src/core/page-layout/sidebar-menu.ts b/frontend-web/src/core/page-layout/sidebar-menu.ts index c343a91b..ca1c76ff 100644 --- a/frontend-web/src/core/page-layout/sidebar-menu.ts +++ b/frontend-web/src/core/page-layout/sidebar-menu.ts @@ -249,13 +249,6 @@ export const sidebarMenuItems: MenuItem[] = [ path: "/page/strategy/admin/positions", adminOnly: true, }, - { - id: "mgmt-user-leaderboard", - label: "用户排行榜", - icon: Trophy, - path: "/page/strategy/admin/user-leaderboard", - adminOnly: true, - }, { id: "mgmt-admin-scheduled-tasks", label: "定时任务管理", From 44cb118f95c7bc0799e2f42102a92cb0863f0b1d Mon Sep 17 00:00:00 2001 From: fengxiangdebaozi <10845145+fengxiangdebaozi@user.noreply.gitee.com> Date: Tue, 16 Jun 2026 20:59:44 +0800 Subject: [PATCH 4/5] =?UTF-8?q?bug=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai-writing/WritingSetupChat.tsx | 26 +- .../components/ai-writing/WritingWelcome.tsx | 2 +- .../pages/ResearchTemplateEditorPage.tsx | 300 ++++++++++++++++++ .../pages/ResearchTemplatesConfigPage.tsx | 246 +------------- .../open-canvas/routes/OpenCanvasRoutes.tsx | 3 + 5 files changed, 333 insertions(+), 244 deletions(-) create mode 100644 frontend-web/src/open-canvas/pages/ResearchTemplateEditorPage.tsx diff --git a/frontend-web/src/open-canvas/components/ai-writing/WritingSetupChat.tsx b/frontend-web/src/open-canvas/components/ai-writing/WritingSetupChat.tsx index b1207024..e620b0e2 100644 --- a/frontend-web/src/open-canvas/components/ai-writing/WritingSetupChat.tsx +++ b/frontend-web/src/open-canvas/components/ai-writing/WritingSetupChat.tsx @@ -28,7 +28,7 @@ import { uuid } from '@/core/utils/uuid'; import { cn } from '@/lib/utils'; import { useArticleTypes } from '../../hooks/useArticleTypes'; -import { useWritingForm } from '../../contexts/WritingFormContext'; +import { useWritingFormOptional } from '../../contexts/WritingFormContext'; import { WritingWelcome } from './WritingWelcome'; /** @@ -42,13 +42,25 @@ import { WritingWelcome } from './WritingWelcome'; * * 这条线程只负责「帮用户把表单聊清」,与真正写作的 ai_writing graph 完全解耦。 */ -export function WritingSetupChat() { +export function WritingSetupChat({ + onApprove: onApproveProp, + title = '写作助手', + hint = '描述你想写什么,我来帮你填好右侧配置', +}: { + /** 审批卡「应用」回调;缺省回填写作表单(需 WritingFormProvider)。传入则脱离表单上下文, + * 用于复用到「新建研究模板」等场景,把建议字段写进调用方的表单。 */ + onApprove?: (payload: SetupWritingApprovalPayload) => void; + title?: string; + hint?: string; +} = {}) { const threadId = useMemo(() => uuid(), []); const [threadPrepared, setThreadPrepared] = useState(false); const [chatModel, setChatModel] = useState(''); const { models, isLoading: modelsLoading } = useModels(); const { articleTypes } = useArticleTypes(); - const { applyProposal } = useWritingForm(); + // 缺省回填写作表单;onApprove 传入时脱离 WritingFormProvider(如模板创建页)。 + const formCtx = useWritingFormOptional(); + const applyProposal = onApproveProp ?? formCtx?.applyProposal; // 首个模型可用时默认选中 useEffect(() => { @@ -98,8 +110,8 @@ export function WritingSetupChat() { const handleApprove = useCallback( async (payload: SetupWritingApprovalPayload) => { - applyProposal(payload); - toast.success('已应用到左侧表单,可继续编辑或开始写作'); + applyProposal?.(payload); + toast.success('已应用到表单,可继续编辑'); }, [applyProposal], ); @@ -119,8 +131,8 @@ export function WritingSetupChat() { {!showWelcome && (
-

写作助手

- 描述你想写什么,我来帮你填好右侧配置 +

{title}

+ {hint}
)} diff --git a/frontend-web/src/open-canvas/components/ai-writing/WritingWelcome.tsx b/frontend-web/src/open-canvas/components/ai-writing/WritingWelcome.tsx index 30110ee4..6c0cd0e5 100644 --- a/frontend-web/src/open-canvas/components/ai-writing/WritingWelcome.tsx +++ b/frontend-web/src/open-canvas/components/ai-writing/WritingWelcome.tsx @@ -39,7 +39,7 @@ export function WritingWelcome() {

- 欢迎使用「妙笔 AI」智力打字协作工坊 + 欢迎使用「ai写作」智力打字协作工坊

这是一个专业的自动化写作系统。你只需在下方对话框中 diff --git a/frontend-web/src/open-canvas/pages/ResearchTemplateEditorPage.tsx b/frontend-web/src/open-canvas/pages/ResearchTemplateEditorPage.tsx new file mode 100644 index 00000000..adcdf976 --- /dev/null +++ b/frontend-web/src/open-canvas/pages/ResearchTemplateEditorPage.tsx @@ -0,0 +1,300 @@ +import { useEffect, useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { ArrowLeft, Loader2 } from 'lucide-react'; + +import { Button } from '@/components/ui/button'; +import type { SetupWritingApprovalPayload } from '@/components/workspace/messages'; + +import { useResearchTemplates } from '../hooks/useResearchTemplates'; +import { useArticleTypes } from '../hooks/useArticleTypes'; +import { WritingSetupChat } from '../components/ai-writing/WritingSetupChat'; +import type { + CreateResearchTemplateRequest, + ResearchTemplate, +} from '../api/ai-writing-research-templates'; + +const LIST_PATH = '/page/canvas/research-templates'; + +const INPUT_CLASS = + 'flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm ' + + 'placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50'; +const TEXTAREA_CLASS = + 'flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm ' + + 'placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring resize-none'; +const LABEL_CLASS = 'mb-1 block text-xs font-medium text-muted-foreground'; + +interface FormState { + name: string; + description: string; + article_type_key: string; + outline: string; + material_source: 'general' | 'notebook'; + word_count: string; + audience: string; + strict_mode: boolean; + topic: string; + sort_order: string; +} + +const EMPTY_FORM: FormState = { + name: '', + description: '', + article_type_key: '', + outline: '', + material_source: 'general', + word_count: '800', + audience: '', + strict_mode: false, + topic: '', + sort_order: '0', +}; + +function toForm(t: ResearchTemplate): FormState { + return { + name: t.name, + description: t.description ?? '', + article_type_key: t.article_type_key ?? '', + outline: t.outline ?? '', + material_source: t.material_source, + word_count: String(t.word_count), + audience: t.audience ?? '', + strict_mode: t.strict_mode, + topic: t.topic ?? '', + sort_order: String(t.sort_order), + }; +} + +function toPayload(form: FormState): CreateResearchTemplateRequest { + return { + name: form.name.trim(), + description: form.description.trim() || null, + article_type_key: form.article_type_key || null, + outline: form.outline.trim() || null, + material_source: form.material_source, + word_count: Math.max(100, parseInt(form.word_count, 10) || 800), + audience: form.audience.trim() || null, + strict_mode: form.strict_mode, + topic: form.topic.trim() || null, + sort_order: parseInt(form.sort_order, 10) || 0, + }; +} + +export default function ResearchTemplateEditorPage() { + const navigate = useNavigate(); + const { id } = useParams<{ id: string }>(); + const editing = !!id; + + const { templates, refresh, create, update } = useResearchTemplates(); + const { articleTypes, refresh: refreshTypes } = useArticleTypes(); + + const [form, setForm] = useState(EMPTY_FORM); + // 新建即就绪;编辑需等列表加载后回填一次。 + const [initialized, setInitialized] = useState(!editing); + const [formError, setFormError] = useState(null); + const [saving, setSaving] = useState(false); + + useEffect(() => { + void refresh(); + void refreshTypes(); + }, [refresh, refreshTypes]); + + useEffect(() => { + if (!editing || initialized) return; + const t = templates.find((x) => x.id === id); + if (t) { + setForm(toForm(t)); + setInitialized(true); + } + }, [editing, initialized, templates, id]); + + const set = (key: K, value: FormState[K]) => + setForm((f) => ({ ...f, [key]: value })); + + // 问答助手「应用」→ 把 setup_writing 建议映射进模板表单字段(与 AI 写作同款机制)。 + const applyProposalToForm = (p: SetupWritingApprovalPayload) => + setForm((f) => ({ + ...f, + name: f.name || (p.user_intent?.trim() ? p.user_intent.trim().slice(0, 20) : f.name), + topic: p.user_intent?.trim() || f.topic, + article_type_key: p.article_type?.trim() || f.article_type_key, + outline: p.outline?.trim() || f.outline, + word_count: + typeof p.word_count_target === 'number' && p.word_count_target > 0 + ? String(p.word_count_target) + : f.word_count, + audience: p.target_audience?.trim() || f.audience, + strict_mode: + p.writing_mode === 'strict' ? true : p.writing_mode === 'loose' ? false : f.strict_mode, + material_source: p.material_source + ? p.material_source === 'notebook' + ? 'notebook' + : 'general' + : f.material_source, + })); + + const handleSave = async () => { + if (!form.name.trim()) { + setFormError('请填写模板名称'); + return; + } + setSaving(true); + setFormError(null); + try { + const payload = toPayload(form); + if (editing && id) await update(id, payload); + else await create(payload); + navigate(LIST_PATH); + } catch (e) { + setFormError(e instanceof Error ? e.message : '保存失败'); + setSaving(false); + } + }; + + return ( +

+
+
+ +
+

+ {editing ? '编辑研究模板' : '新建研究模板'} +

+

+ 左侧用问答描述需求,自动填好右侧字段;也可手动编辑。 +

+
+
+
+ {formError && {formError}} + + +
+
+ +
+ {/* 左:模板助手问答(同 AI 写作 setup 机制)→ 应用到右侧表单。问答区更宽(约 7:3)。 */} +
+ +
+ + {/* 右:模板表单 */} +
+
+ + set('name', e.target.value)} + placeholder="如:深度舆情分析" + /> +
+
+ + set('description', e.target.value)} + placeholder="模板用途简述" + /> +
+
+ + +
+
+ + +
+
+ + set('word_count', e.target.value)} + /> +
+
+ + set('audience', e.target.value)} + placeholder="如:决策者" + /> +
+
+ +