From df9c59e21c561bd8d3f8b952e2969b47efe79514 Mon Sep 17 00:00:00 2001 From: Administrator Date: Tue, 23 Jun 2026 20:41:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(y-log):=20=E9=97=AE=E7=AD=94=E6=8C=89=20LL?= =?UTF-8?q?M=20=E8=B0=83=E7=94=A8=E5=90=84=E8=AE=B0=E4=B8=80=E6=9D=A1=20+?= =?UTF-8?q?=20=E5=85=A8=E9=87=8F=E8=A1=A5=E9=BD=90=E5=90=84=20API=20?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E5=9F=8B=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 问答日志从「每轮 onFinish 一条」改为 useThreadStream onLangChainEvent 监听 on_chat_model_end,每次大模型调用结束各上报一条(含子代理) - 广覆盖各 API client 成功路径:查看(view)/查询(search)/新增(create)/ 编辑(edit)/删除(delete)/启停(enabled·disabled)/导入导出,补齐此前 整模块零埋点与漏掉的单条详情 GET;跳过高频 typeahead/轮询/bootstrap Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 2 +- frontend-web/src/core/admin/api.ts | 212 ++++++++++++---- frontend-web/src/core/agents/api.ts | 19 +- frontend-web/src/core/artifacts/api.ts | 10 +- frontend-web/src/core/curator/api.ts | 37 ++- frontend-web/src/core/custom-prompts/index.ts | 33 ++- frontend-web/src/core/knowledge/api.ts | 148 ++++++++++- frontend-web/src/core/llm-metrics/api.ts | 27 +- frontend-web/src/core/mcp/api.ts | 20 +- frontend-web/src/core/memory/api.ts | 70 ++++- frontend-web/src/core/models/api.ts | 10 +- frontend-web/src/core/note/api.ts | 26 +- frontend-web/src/core/notifications/api.ts | 43 +++- frontend-web/src/core/positions/api.ts | 87 ++++++- .../src/core/recommended-questions/api.ts | 55 +++- .../src/core/roundtable-draft-shares/api.ts | 58 ++++- frontend-web/src/core/scheduled-tasks/api.ts | 240 ++++++++++++++++-- frontend-web/src/core/studio/api.ts | 63 ++++- frontend-web/src/core/system-settings/api.ts | 164 ++++++++++-- frontend-web/src/core/tags/api.ts | 48 +++- frontend-web/src/core/thread-shares/api.ts | 58 ++++- frontend-web/src/core/threads/hooks.ts | 101 ++++++++ frontend-web/src/core/tool-metrics/api.ts | 42 ++- .../src/core/user-preferences/index.ts | 11 +- frontend-web/src/core/user-qa/api.ts | 15 +- .../src/roundtable-planning/api/chains.ts | 52 +++- .../api/dashboard-sessions.ts | 21 +- .../src/roundtable-planning/api/drafts.ts | 10 +- .../roundtable-planning/api/multi-agent.ts | 11 +- .../src/roundtable-planning/api/recommend.ts | 21 +- .../api/roundtable-jobs.ts | 10 +- .../roundtable-planning/api/task-drafts.ts | 20 +- .../api/business-mapping.ts | 20 +- .../src/strategy-components/api/light-apps.ts | 10 +- .../api/skill-addresses.ts | 19 +- 35 files changed, 1569 insertions(+), 224 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4f68d459..b011329e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -91,7 +91,7 @@ The frontend (`frontend-web/`) is a React 19 + Vite SPA: - **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}/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 inside a **任务工作区** (resolved via `/api/business-mapping`; `resolveTaskDeeplinkTarget` calls `persistTaskContext({taskId, agentId, openingText})` and navigates to the `…/chats` **index**, which opens the latest task conversation or 新建 — a 1:N task↔对话 workspace, NOT `pendingAgentMessage`); the `-A` suffix (`3qfx-A`/…) → **multi-agent roundtable** with `roundtablePrefill.businessCode` so the page auto-selects the configured 业务链条. **xdfx's deep-link `?taskId=` is actually an 行动id, not a 任务id** — `resolveDeeplinkTaskId(goPath, rawId)` (called in `LoginPage` before everything) detects `xdfx`/`xdfx-A` and resolves it via `GET {action_detail_url}/{actionId}` → `data.taskId` (`fetchTaskIdFromAction`; `action_test`+`mock.action_detail` for offline testing); the resolved 任务id then drives `cop-task-detail`, the purpose tree, and the 1:N binding (other businesses pass the id through unchanged). **xdfx additionally partitions its chat records by 行动id (action_id), NOT by task_id**: `resolveTaskDeeplinkTarget` sets `partitionByAction:true` + keeps the raw action_id as `TaskContext.actionId`, so new threads are tagged `metadata.{taskId,actionId}` (taskId kept only to preserve the backend「任务深链对话全局共享」semantics, which keys off `metadata.taskId`) and the conversation list filters by `actionId` (+`agent_id`) — different actions of the same task get separate lists. xdfx's first-question opening text also appends `行动id(action_id)为…` (see `buildOpeningText` / `taskThreadMetadata`). goPath→businessCode: `3qfx`→`3Q`, `rwfx`→`6BF`, `xdfx`→`7BF`. The roundtable target also threads the URL `taskId` into `roundtablePrefill.taskId` (`resolveTaskDeeplinkTarget(goPath, detail, taskId)`), which turns on two taskId-scoped behaviors: (1) **separate, unpartitioned per-task storage** — taskId 会商聊天记录 live in their **own** store/table (`roundtable_task_drafts`), keyed **only by `task_id`** (one taskId = many records, shared across users, **no user 分权** at all; `created_by` is audit-only), kept entirely out of the per-user personal store (`roundtable_drafts`, now strictly per-user) so the two never mix. Backend router `/api/roundtable-task-drafts` (`GET /by-task/{id}` + `/by-task/{id}/latest`, `POST`, `GET/PUT/DELETE /{id}` — all user-agnostic); frontend client `roundtable-planning/api/task-drafts.ts`, and `useDraftPersistence`/`useRoundtableDrafts` route **all** CRUD to it when a `taskId` is present (else the personal `drafts.ts`). The matching **background jobs** are shared by task too: `roundtable_jobs.task_id` marks a task job; reads/dedup/stream/resume/cancel pass `?task_id=` so any user opening the task sees the same running 研讨 (progress/flowchart), and the executor writes the final report back to the task-draft store. Existing rows are moved over by `scripts/migrate_roundtable_task_drafts.py` (migration `20260618_01`). The history dropdown filters to the task and 新建 keeps the binding; (2) **Step-3 auto structured output** — after the `roundtable-summary` agent writes the Markdown report it automatically runs a second round emitting a `flow-json` flowchart (`useStep3Summary` `autoFlow`+`chainLevels`), whose hierarchy follows the business chain's stage goals (任务 → 目的 → 行为体 → …; coordinatorPrompt-specified hierarchy wins) and whose nodes never carry 阶段编号/阶段名 (the prior bug). **Both targets render embedded under the 任务管理 layout** — nested routes under `task-management//{roundtable,agent/:agent_id/chats}` 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. **All three single-agent businesses share `TaskFxWorkspace`** (`task-components/TaskFxWorkspace.tsx`): left = a「当前任务」card (`readCopTaskDetail()` 任务名称 + 简介, desensitized, 留存 above 新建对话) + per-task 1:N conversation list (`useTaskThreads(taskId, agentId, actionId?)` — filtered by **both** `metadata.taskId` AND `metadata.agent_id` so the three businesses' conversations stay isolated even though they share one taskId; **xdfx** passes a third `actionId` so it filters by `metadata.actionId` too, partitioning chat records per 行动 — `useChatsBase` derives it from `TaskContext.partitionByAction`) + 新建对话; the `…/chats` index (`TaskFxIndexResolver`) opens the latest task thread or 新建. The right `:thread_id` host differs by business: **rwfx** → `TaskFxChat` (`AgentChatPage taskGate`) = full workspace (non-`lite`): 目的树闸门 panel + 清除记录 (placed under 新建对话); **xdfx** → also `TaskFxChat` (same 目的树闸门 query+display, same `purpose_detail_url`) but workspace passed `lite`, **plus its own「清除记录」button** (`ActionClearButton`, under 新建对话) that deletes **by 行动id**: opening its confirm dialog first runs a「行动历史是否存在」pre-check (`fetchActionHistoryExists`; query endpoint `task_deeplink.action_history_url` is **address-TBD** — empty ⇒ unknown, does **not** block the delete; once a definite「无历史」comes back it disables 确认清除), then deletes via the **shared** delete endpoint `task_deeplink.delete_url` (migrated to `dropSixOrSevenData`, used by **both** rwfx + xdfx — Six=6BF/rwfx, Seven=7BF/xdfx): `deleteTaskPurpose(id, {idKind})` sends rwfx `?taskId=` (omits actionId) / xdfx `?actionId=` (omits taskId) — the unused param is **absent from the URL**, not null. `deleteTaskPurpose` + `fetchActionHistoryExists` live in `core/auth/task-purpose.ts`. **3qfx** → `TaskBindChat` (`AgentChatPage taskBind`, `lite`) = 1:N list only, no gate, and 新建对话 **auto-sends** the task opening text (`taskAutoSendText`; gate businesses send it via the gate's 开始问答 instead). **rwfx auto-starts the first Q&A when the 任务目的 query returns empty** (`TaskPurposePanel autoStartWhenEmpty`, threaded from the rwfx `TaskFxChat` only), **but gated by the 3q precheck**: on entering the rwfx workspace `RwfxSqGateProvider` (`task-components/rwfx-sq-gate.tsx`, wraps the workspace, rwfx only) calls `fetchSqReportStatus(taskId)` — `incomplete`(3q 无数据) pops the「3q 未完成,是否前往 3qfx」dialog AND **holds the auto-send**; the question is only sent when the user picks「暂不」(or dismisses — i.e. does NOT go to 3q analysis), while「前往 3qfx」navigates and discards the send. `complete`/`unknown` send immediately (no dialog). The panel's auto-start calls `useRwfxSqGate().gateStart(onStart)` instead of `onStart()`; the manual「开始问答」/覆盖确认 path is unchanged (ungated). This replaced the old standalone `RwfxSqReportGate` (which popped the same dialog but did not gate the auto-send). The 目的树闸门 (`TaskPurposePanel`) defaults **collapsed** and now keeps only 弹框查看 + 开始问答; the former jump buttons (编辑/下一步/3q详情/切换三情/弹框查看) all moved to the **configurable bottom 快捷跳转 bar** (`TaskJumpBar`, red 红底白字 buttons, right-aligned) driven by 按钮管理 — see the 按钮管理 bullet. `TaskPurposeDialog` (exported from `TaskPurposePanel`) backs both the gate zoom and the bottom bar's `purpose`-type button. `chatBasePath` keeps the post-send URL rewrite + new-chat button in the task-management context. Backend config `task_deeplink` (`config.yaml`, surfaced by `GET /api/public/config/task-deeplink`, mirrored in `core/auth/task-deeplink-config.ts`) adds `action_test`/`action_detail_url`/`detail_3q_url` + `mock.action_detail`. Any failure (no consumer URL / fetch error / unmapped goPath / unresolvable action id / no configured agent) silently falls back to the default home. Helpers: `core/auth/task-deeplink.ts`, `core/auth/task-purpose.ts`, `core/auth/task-deeplink-config.ts`, `core/threads/use-task-threads.ts`. - **iframe 嵌入模式 (外部系统用 iframe 套用深链问答/会商)** — 三个**按标签页**开关,全部由深链 URL 参数开启、存进同一份 `sessionStorage` 会话(`embed.session`,键名常量在 `core/embed/embed-session.ts`),跨登录跳转/刷新/页内导航存活,**绝不跨标签污染**同浏览器另开的正常系统页。核心隔离手法:用 `sessionStorage`(按标签页/浏览上下文隔离,iframe 即便同源也有独立一份)而非 `localStorage`(跨标签共享)。开关:(1) **`?embed=1`(可带 `&theme=`)** — 去全局 chrome(`PageLayout` 的 `pageSidebarAllowed && !isEmbedActive()` → 隐藏 Header + PageSidebar)+ 按外部主题色**强制**明/暗:走 next-themes 的 `forcedTheme`(`App.tsx` `AppProviders` 调 `resolveForcedEmbedTheme`,**不写** localStorage `strategy-theme`),离散映射 `dark-blue`/`dark`→深、其余→浅;嵌入侧**绝不**调 `setTheme`/`persistSidebarEnabled`(那俩落 localStorage 跨标签污染,`LoginPage` 据 `readEmbedParam` 跳过)。(2) **`?hideJump=1`** — 隐藏深链底部「快捷跳转」栏 `TaskJumpBar` 的两部分(底部「下一步」等按钮 + 右侧垂直居中悬浮菜单),单智能体问答 + 会商共用该栏,`isJumpBarHidden()` 为真时整条 `return null`。(3) **登录用户隔离** — 嵌入态下鉴权/身份键(`deerflow.auth` token、`userInfo`、`login.*`、`tools_token`、maxkey/userDetail token)全部经 `core/auth/scoped-storage.ts` 的 `authStorage()`(嵌入→sessionStorage / 正常→localStorage)读写,使 iframe 里登录的用户与正常登录用户**完全隔离**、互不覆盖;所有 token 读取都过 `getStoredAuth()`/`getAuthorizationHeaderValue()`(API 客户端 `core/api/*` 全走这俩),改中心函数即让全体调用方自动隔离;跨标签账号变更监听 `AuthSyncWatcher`(`core/auth/sync.tsx`)在嵌入态禁用。嵌入会话在 `main.tsx` 渲染前 `syncEmbedSessionFromCurrentUrl()` 同步启用(早于任何鉴权写入,避免首帧 effect 顺序漏写 localStorage);`AppProviders` 的 effect 再按 SPA 导航同步。`embed=0`/`hideJump=0` 可显式关闭。正常(非嵌入)标签页所有 `isXxx()` 恒 false,行为**零变化**。第三个开关 `?isolate=1`(**独立登录会话**)把同一套**登录用户隔离**单独提供给**普通(带 chrome)标签页**——只切换鉴权存储、不去 chrome / 不强制主题 / 不动跳转栏:`authStorage()` 的条件从「仅嵌入」放宽为 `isEmbedActive() || isIsolatedSession()`,`AuthSyncWatcher` 的跨标签监听对嵌入态**或**独立态都即时忽略(handler 内实时判,运行时开关也即刻生效)。除 URL 参数外,**设置 → 外观 →「登录会话」** 有一个 Switch「独立登录会话(本标签页)」运行时切换(`setIsolatedSession`,`appearance-settings-page.tsx`):**开启时先 `migrateAuthToSession()`**(`scoped-storage.ts`,把 localStorage 的受保护键 `deerflow.auth`/`userInfo`/`userId`/`tools_token` + `login.*` 前缀快照搬进 sessionStorage)再翻标志,故无感不登出;此后本标签页登录态独立——别处登录/换号影响不到它,它换号也影响不到别人,但关掉该标签即结束(需重登)。开启给 toast 提示。 -- **操作日志(Y-Log)上报** — 用户的关键操作(登录登出/查询/查看/增删改/启停/导入导出/问答等)成功后,**前端直连**外部接口 `POST {write_url}`(默认 `https://ch1.b.uat.4.cn/api/y-log/write`)记一条审计日志。核心模块 `src/core/log/`:`y-log.ts`(`writeYLog(params)` + `OptType` 枚举 0/1/3..12;**异步即发即忘、永不 await、永不抛错、失败静默**,自动把完整 url 转相对路径去 ip/host、对象入参/返回自动 `JSON.stringify` + 50k 裁剪、`optResult` 恒 0 只报成功),`y-log-config.ts`(从公开只读 `GET /api/public/config/y-log` 拉 `{enabled, writeUrl}`,模块级缓存 + 去重 + 默认兜底;`App.tsx` `AppProviders` 启动预热 `fetchYLogConfig()`),`y-log-token.ts`(token1 = token 登录的 `?authToken=`,经 [[scoped-storage]] 的 `authStorage*` 存取,嵌入态自动隔离)。**双门控**:仅当①存在 token1(即用 token 登录,`LoginPage` 成功后 `persistYLogToken(authToken)`,登出 `account-settings-page.tsx` 先报后清 token1)且②配置 `enabled && writeUrl` 才上报;否则一律不发。`optDetail` 统一「B1-CM助手-…」(**禁内部框架名**)。埋点集中在各 API client 的成功路径:`strategy-components/api/{sensitive-words,menu-overrides,task-buttons,skill-addresses,light-apps}.ts`、`roundtable-planning/api/{drafts,task-drafts,roundtable-jobs}.ts`、`core/{agents,scheduled-tasks,knowledge}/api.ts`、`core/api/markdown-docx-export.ts`,以及问答完成钩子(`ChatPage`/`AgentChatPage` 的 `useThreadStream` `onFinish`,用最终汇总文本作 `returnParam`,optType 12)。后端仅新增公开只读配置:`deerflow/config/y_log_config.py`(`YLogConfig{enabled,write_url}`)挂到 `AppConfig.y_log`,`app/gateway/routers/public_config.py` 加 `GET /api/public/config/y-log`,地址写在 `config.yaml` 的 `y_log` 段(mtime 热重载,前端无需重建);**不做后端代理**。Tests:`tests/test_y_log_config.py`。 +- **操作日志(Y-Log)上报** — 用户的关键操作(登录登出/查询/查看/增删改/启停/导入导出/问答等)成功后,**前端直连**外部接口 `POST {write_url}`(默认 `https://ch1.b.uat.4.cn/api/y-log/write`)记一条审计日志。核心模块 `src/core/log/`:`y-log.ts`(`writeYLog(params)` + `OptType` 枚举 0/1/3..12;**异步即发即忘、永不 await、永不抛错、失败静默**,自动把完整 url 转相对路径去 ip/host、对象入参/返回自动 `JSON.stringify` + 50k 裁剪、`optResult` 恒 0 只报成功),`y-log-config.ts`(从公开只读 `GET /api/public/config/y-log` 拉 `{enabled, writeUrl}`,模块级缓存 + 去重 + 默认兜底;`App.tsx` `AppProviders` 启动预热 `fetchYLogConfig()`),`y-log-token.ts`(token1 = token 登录的 `?authToken=`,经 [[scoped-storage]] 的 `authStorage*` 存取,嵌入态自动隔离)。**双门控**:仅当①存在 token1(即用 token 登录,`LoginPage` 成功后 `persistYLogToken(authToken)`,登出 `account-settings-page.tsx` 先报后清 token1)且②配置 `enabled && writeUrl` 才上报;否则一律不发。`optDetail` 统一「B1-CM助手-…」(**禁内部框架名**)。埋点**广覆盖**各 API client 的成功路径——原则是「一个用户操作(查看/查询/新增/编辑/删除/启停/导入导出)= 一条日志」,每个 API client 的**单条详情 GET(查看→view 4)/ 列表查询 GET(search 3)/ 新增 POST(create 9)/ 改 PUT·PATCH(edit 5)/ 删 DELETE(delete 6)/ 启停(enabled 11 / disabled 10)/ 导入导出(import 7 / export 8)**都各落一条,仅跳过高频 typeahead/状态轮询/纯 bootstrap config 读。覆盖文件:`strategy-components/api/{sensitive-words,menu-overrides,task-buttons,skill-addresses,light-apps,business-mapping}.ts`、`roundtable-planning/api/{drafts,task-drafts,roundtable-jobs,recommend,chains,multi-agent,dashboard-sessions}.ts`、`core/{agents,scheduled-tasks,knowledge,positions,tags,studio,system-settings,memory,mcp,models,custom-prompts,user-preferences,notifications,recommended-questions,user-qa,curator,admin,thread-shares,roundtable-draft-shares,tool-metrics,llm-metrics,note,artifacts}/api.ts`、`core/api/markdown-docx-export.ts`。**问答(Q&A)改为「一次大模型调用 = 一条日志」**:在共享的 `core/threads/hooks.ts` `useThreadStream` 的 `onLangChainEvent` 里监听 `on_chat_model_end`,每次模型调用结束就上报一条(optType 12,`reqParam` 取本次 input 的最后一条 user 文本 + 模型名,`returnParam` 取本次输出文本)——一个问答轮次里 lead agent / 子代理的**每次** LLM 调用都各记一条,覆盖所有走 `useThreadStream` 的页面(`ChatPage`/`AgentChatPage`/创建智能体/iframe/会商等);`ChatPage`/`AgentChatPage` 的 `onFinish` 仍额外保留一条轮次汇总。后端仅新增公开只读配置:`deerflow/config/y_log_config.py`(`YLogConfig{enabled,write_url}`)挂到 `AppConfig.y_log`,`app/gateway/routers/public_config.py` 加 `GET /api/public/config/y-log`,地址写在 `config.yaml` 的 `y_log` 段(mtime 热重载,前端无需重建);**不做后端代理**。Tests:`tests/test_y_log_config.py`。 ### Configuration diff --git a/frontend-web/src/core/admin/api.ts b/frontend-web/src/core/admin/api.ts index 4e06f4d7..0ab2861c 100644 --- a/frontend-web/src/core/admin/api.ts +++ b/frontend-web/src/core/admin/api.ts @@ -1,5 +1,6 @@ import { apiFetch } from "@/core/api"; import { getBackendBaseURL } from "@/core/config"; +import { OptType, writeYLog } from "@/core/log/y-log"; /** A conversation thread owned by some user (admin view). */ export interface AdminThread { @@ -172,22 +173,32 @@ async function parse(res: Response, what: string): Promise { } export async function listUserThreads(userId: string): Promise { - return parse( - await apiFetch(`${base()}/${encodeURIComponent(userId)}/threads?limit=200`), - "Failed to load threads", - ); + const url = `${base()}/${encodeURIComponent(userId)}/threads?limit=200`; + const result = await parse(await apiFetch(url), "Failed to load threads"); + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-管理后台-查询用户会话列表", + url, + reqParam: { userId }, + returnParam: { count: result.length }, + }); + return result; } export async function listUserThreadMessages( userId: string, threadId: string, ): Promise { - return parse( - await apiFetch( - `${base()}/${encodeURIComponent(userId)}/threads/${encodeURIComponent(threadId)}/messages`, - ), - "Failed to load messages", - ); + const url = `${base()}/${encodeURIComponent(userId)}/threads/${encodeURIComponent(threadId)}/messages`; + const result = await parse(await apiFetch(url), "Failed to load messages"); + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-管理后台-查看会话消息", + url, + reqParam: { userId, threadId }, + returnParam: { count: result.length }, + }); + return result; } export async function listUserQa( @@ -200,19 +211,30 @@ export async function listUserQa( }); const query = (params.q ?? "").trim(); if (query) search.set("q", query); - return parse( - await apiFetch(`${base()}/${encodeURIComponent(userId)}/qa?${search}`), - "Failed to load user Q&A", - ); + const url = `${base()}/${encodeURIComponent(userId)}/qa?${search}`; + const result = await parse(await apiFetch(url), "Failed to load user Q&A"); + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-管理后台-查询用户问答", + url, + reqParam: { userId, ...params }, + returnParam: { total: result.total, count: result.items.length }, + }); + return result; } export async function getUserMemory( userId: string, ): Promise { - return parse( - await apiFetch(`${base()}/${encodeURIComponent(userId)}/memory`), - "Failed to load memory", - ); + const url = `${base()}/${encodeURIComponent(userId)}/memory`; + const result = await parse(await apiFetch(url), "Failed to load memory"); + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-管理后台-查看用户记忆", + url, + reqParam: { userId }, + }); + return result; } export async function getUserLeaderboard( @@ -229,10 +251,15 @@ export async function getUserLeaderboard( until: range.until, limit: String(limit), }); - return parse( - await apiFetch(`${base()}/leaderboard?${params}`), - "Failed to load leaderboard", - ); + const url = `${base()}/leaderboard?${params}`; + const result = await parse(await apiFetch(url), "Failed to load leaderboard"); + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-管理后台-查询用户排行榜", + url, + reqParam: { ...range, limit }, + }); + return result; } /** @@ -253,12 +280,18 @@ export async function refreshTodayLeaderboard( until: range.until, limit: String(limit), }); - return parse( - await apiFetch(`${base()}/leaderboard/refresh-today?${params}`, { - method: "POST", - }), + const url = `${base()}/leaderboard/refresh-today?${params}`; + const result = await parse( + await apiFetch(url, { method: "POST" }), "Failed to refresh today's leaderboard", ); + writeYLog({ + optType: OptType.other, + optDetail: "B1-CM助手-管理后台-刷新今日排行榜", + url, + reqParam: { ...range, limit }, + }); + return result; } export async function getUserAnalytics( @@ -272,10 +305,15 @@ export async function getUserAnalytics( until: range.until, limit: String(limit), }); - return parse( - await apiFetch(`${base()}/${encodeURIComponent(userId)}/analytics?${params}`), - "Failed to load user analytics", - ); + const url = `${base()}/${encodeURIComponent(userId)}/analytics?${params}`; + const result = await parse(await apiFetch(url), "Failed to load user analytics"); + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-管理后台-查看用户分析", + url, + reqParam: { userId, ...range, limit }, + }); + return result; } export async function generateUserLeaderboardReport( @@ -290,12 +328,19 @@ export async function generateUserLeaderboardReport( limit: String(limit), }); if (modelName) params.set("model_name", modelName); - return parse( - await apiFetch(`${base()}/leaderboard/report?${params}`, { - method: "POST", - }), + const url = `${base()}/leaderboard/report?${params}`; + const result = await parse( + await apiFetch(url, { method: "POST" }), "Failed to generate leaderboard report", ); + writeYLog({ + optType: OptType.other, + optDetail: "B1-CM助手-管理后台-生成排行榜报告", + url, + reqParam: { ...range, limit, modelName }, + returnParam: { generated_at: result.generated_at, model_name: result.model_name }, + }); + return result; } function parseSseFrame(frame: string): AdminLeaderboardReportStreamEvent | null { @@ -335,7 +380,8 @@ export async function streamUserLeaderboardReport({ limit: String(limit), }); if (modelName) params.set("model_name", modelName); - const response = await apiFetch(`${base()}/leaderboard/report/stream?${params}`, { + const url = `${base()}/leaderboard/report/stream?${params}`; + const response = await apiFetch(url, { method: "POST", signal, }); @@ -366,6 +412,12 @@ export async function streamUserLeaderboardReport({ const parsed = parseSseFrame(tail); if (parsed) onEvent(parsed); } + writeYLog({ + optType: OptType.other, + optDetail: "B1-CM助手-管理后台-流式生成排行榜报告", + url, + reqParam: { ...range, limit, modelName }, + }); } export async function addAdminMemoryEntry( @@ -373,14 +425,22 @@ export async function addAdminMemoryEntry( content: string, agent: string | null, ): Promise { - return parse( - await apiFetch(`${base()}/${encodeURIComponent(userId)}/memory/entries`, { + const url = `${base()}/${encodeURIComponent(userId)}/memory/entries`; + const result = await parse( + await apiFetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content, agent }), }), "Failed to add memory entry", ); + writeYLog({ + optType: OptType.create, + optDetail: "B1-CM助手-管理后台-新增记忆条目", + url, + reqParam: { userId, content, agent }, + }); + return result; } export async function replaceAdminMemoryEntry( @@ -389,14 +449,22 @@ export async function replaceAdminMemoryEntry( content: string, agent: string | null, ): Promise { - return parse( - await apiFetch(`${base()}/${encodeURIComponent(userId)}/memory/entries`, { + const url = `${base()}/${encodeURIComponent(userId)}/memory/entries`; + const result = await parse( + await apiFetch(url, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ old_text: oldText, content, agent }), }), "Failed to replace memory entry", ); + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-管理后台-编辑记忆条目", + url, + reqParam: { userId, old_text: oldText, content, agent }, + }); + return result; } export async function deleteAdminMemoryEntry( @@ -406,13 +474,18 @@ export async function deleteAdminMemoryEntry( ): Promise { const params = new URLSearchParams({ old_text: oldText }); if (agent) params.set("agent", agent); - return parse( - await apiFetch( - `${base()}/${encodeURIComponent(userId)}/memory/entries?${params}`, - { method: "DELETE" }, - ), + const url = `${base()}/${encodeURIComponent(userId)}/memory/entries?${params}`; + const result = await parse( + await apiFetch(url, { method: "DELETE" }), "Failed to delete memory entry", ); + writeYLog({ + optType: OptType.delete, + optDetail: "B1-CM助手-管理后台-删除记忆条目", + url, + reqParam: { userId, old_text: oldText, agent }, + }); + return result; } // ── V1 Memory CRUD ────────────────────────────────────────────────────────── @@ -424,14 +497,22 @@ export async function updateAdminV1Section( summary: string, agent: string | null, ): Promise { - return parse( - await apiFetch(`${base()}/${encodeURIComponent(userId)}/memory/v1/sections`, { + const url = `${base()}/${encodeURIComponent(userId)}/memory/v1/sections`; + const result = await parse( + await apiFetch(url, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ section_type: sectionType, field, summary, agent }), }), "Failed to update V1 section", ); + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-管理后台-编辑记忆段落", + url, + reqParam: { userId, section_type: sectionType, field, summary, agent }, + }); + return result; } export async function addAdminV1Fact( @@ -441,14 +522,22 @@ export async function addAdminV1Fact( confidence: number, agent: string | null, ): Promise { - return parse( - await apiFetch(`${base()}/${encodeURIComponent(userId)}/memory/v1/facts`, { + const url = `${base()}/${encodeURIComponent(userId)}/memory/v1/facts`; + const result = await parse( + await apiFetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content, category, confidence, agent }), }), "Failed to add V1 fact", ); + writeYLog({ + optType: OptType.create, + optDetail: "B1-CM助手-管理后台-新增记忆事实", + url, + reqParam: { userId, content, category, confidence, agent }, + }); + return result; } export async function updateAdminV1Fact( @@ -457,14 +546,22 @@ export async function updateAdminV1Fact( updates: { content?: string; category?: string; confidence?: number }, agent: string | null, ): Promise { - return parse( - await apiFetch(`${base()}/${encodeURIComponent(userId)}/memory/v1/facts/${encodeURIComponent(factId)}`, { + const url = `${base()}/${encodeURIComponent(userId)}/memory/v1/facts/${encodeURIComponent(factId)}`; + const result = await parse( + await apiFetch(url, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...updates, agent }), }), "Failed to update V1 fact", ); + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-管理后台-编辑记忆事实", + url, + reqParam: { userId, factId, ...updates, agent }, + }); + return result; } export async function deleteAdminV1Fact( @@ -475,11 +572,16 @@ export async function deleteAdminV1Fact( const params = new URLSearchParams(); if (agent) params.set("agent", agent); const qs = params.toString() ? `?${params}` : ""; - return parse( - await apiFetch( - `${base()}/${encodeURIComponent(userId)}/memory/v1/facts/${encodeURIComponent(factId)}${qs}`, - { method: "DELETE" }, - ), + const url = `${base()}/${encodeURIComponent(userId)}/memory/v1/facts/${encodeURIComponent(factId)}${qs}`; + const result = await parse( + await apiFetch(url, { method: "DELETE" }), "Failed to delete V1 fact", ); + writeYLog({ + optType: OptType.delete, + optDetail: "B1-CM助手-管理后台-删除记忆事实", + url, + reqParam: { userId, factId, agent }, + }); + return result; } diff --git a/frontend-web/src/core/agents/api.ts b/frontend-web/src/core/agents/api.ts index 94766bd1..86012665 100644 --- a/frontend-web/src/core/agents/api.ts +++ b/frontend-web/src/core/agents/api.ts @@ -119,7 +119,15 @@ export async function listAllAgentsAdmin(): Promise { export async function getAgent(id: string): Promise { const res = await apiFetch(`${getBackendBaseURL()}/api/agents/${id}`); if (!res.ok) throw new Error(`Agent '${id}' not found`); - return res.json() as Promise; + const agent = (await res.json()) as Agent; + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-智能体-查看", + url: `/api/agents/${id}`, + reqParam: { id }, + returnParam: agent, + }); + return agent; } export async function createAgent(request: CreateAgentRequest): Promise { @@ -183,7 +191,14 @@ export async function deleteAgent(id: string): Promise { export async function getAgentSelector(): Promise { const res = await apiFetch(`${getBackendBaseURL()}/api/agents/selector`); if (!res.ok) throw new Error(`Failed to load agent selector: ${res.statusText}`); - return res.json() as Promise; + const selector = (await res.json()) as AgentSelectorResponse; + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-智能体-选择器", + url: `/api/agents/selector`, + returnParam: selector, + }); + return selector; } export async function favoriteAgent(id: string): Promise { diff --git a/frontend-web/src/core/artifacts/api.ts b/frontend-web/src/core/artifacts/api.ts index 83a57a4c..72c3c1bd 100644 --- a/frontend-web/src/core/artifacts/api.ts +++ b/frontend-web/src/core/artifacts/api.ts @@ -1,4 +1,5 @@ import { apiFetch } from "@/core/api"; +import { OptType, writeYLog } from "@/core/log/y-log"; import { urlOfArtifact } from "./utils"; @@ -26,7 +27,8 @@ export async function updateArtifactContent({ content: string; isMock?: boolean; }) { - const res = await apiFetch(urlOfArtifact({ filepath, threadId, isMock }), { + const url = urlOfArtifact({ filepath, threadId, isMock }); + const res = await apiFetch(url, { method: "PATCH", headers: { "Content-Type": "application/json", @@ -36,5 +38,11 @@ export async function updateArtifactContent({ if (!res.ok) { throw new Error(await errorMessageFromResponse(res)); } + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-产物-编辑产物内容", + url, + reqParam: { filepath, threadId }, + }); return res.text(); } diff --git a/frontend-web/src/core/curator/api.ts b/frontend-web/src/core/curator/api.ts index 0e7dd12c..5d7f556f 100644 --- a/frontend-web/src/core/curator/api.ts +++ b/frontend-web/src/core/curator/api.ts @@ -1,5 +1,6 @@ import { apiFetch } from "@/core/api"; import { getBackendBaseURL } from "@/core/config"; +import { OptType, writeYLog } from "@/core/log/y-log"; import type { CuratorConfig, @@ -27,12 +28,21 @@ export interface CuratorConfigUpdatePayload { export async function updateCuratorConfig( payload: CuratorConfigUpdatePayload, ): Promise { - const response = await apiFetch(`${getBackendBaseURL()}/api/curator/config`, { + const url = `${getBackendBaseURL()}/api/curator/config`; + const response = await apiFetch(url, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); - return response.json() as Promise; + const result = (await response.json()) as CuratorConfig; + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-策展-更新配置", + url, + reqParam: payload, + returnParam: result, + }); + return result; } export interface RunCuratorResult { @@ -42,13 +52,26 @@ export interface RunCuratorResult { } export async function runCurator(): Promise { - const response = await apiFetch(`${getBackendBaseURL()}/api/curator/run`, { - method: "POST", + const url = `${getBackendBaseURL()}/api/curator/run`; + const response = await apiFetch(url, { method: "POST" }); + const result = (await response.json()) as RunCuratorResult; + writeYLog({ + optType: OptType.other, + optDetail: "B1-CM助手-策展-手动触发策展", + url, + returnParam: result, }); - return response.json() as Promise; + return result; } export async function getCuratorLogs(): Promise { - const response = await apiFetch(`${getBackendBaseURL()}/api/curator/logs`); - return response.json() as Promise; + const url = `${getBackendBaseURL()}/api/curator/logs`; + const response = await apiFetch(url); + const result = (await response.json()) as CuratorLogs; + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-策展-查看运行日志", + url, + }); + return result; } diff --git a/frontend-web/src/core/custom-prompts/index.ts b/frontend-web/src/core/custom-prompts/index.ts index 8d087c92..17d7b2fa 100644 --- a/frontend-web/src/core/custom-prompts/index.ts +++ b/frontend-web/src/core/custom-prompts/index.ts @@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { apiFetch } from "@/core/api"; import { getBackendBaseURL } from "@/core/config"; +import { OptType, writeYLog } from "@/core/log/y-log"; /** A user-defined reusable prompt, bound to the account (stored server-side). */ export interface CustomPrompt { @@ -27,6 +28,12 @@ async function listUserPrompts(): Promise { const res = await apiFetch(baseUrl()); if (!res.ok) throw new Error(`Failed to load prompts: ${res.statusText}`); const data = (await res.json()) as { prompts: CustomPrompt[] }; + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-自定义提示词-查询", + url: "/api/user-prompts", + returnParam: data.prompts, + }); return data.prompts; } @@ -44,7 +51,15 @@ async function createUserPrompt(input: { const err = (await res.json().catch(() => ({}))) as { detail?: string }; throw new Error(err.detail ?? `Failed to create prompt: ${res.statusText}`); } - return res.json() as Promise; + const created = (await res.json()) as CustomPrompt; + writeYLog({ + optType: OptType.create, + optDetail: "B1-CM助手-自定义提示词-新增", + url: "/api/user-prompts", + reqParam: input, + returnParam: created, + }); + return created; } async function updateUserPrompt( @@ -60,7 +75,15 @@ async function updateUserPrompt( const err = (await res.json().catch(() => ({}))) as { detail?: string }; throw new Error(err.detail ?? `Failed to update prompt: ${res.statusText}`); } - return res.json() as Promise; + const updated = (await res.json()) as CustomPrompt; + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-自定义提示词-编辑", + url: `/api/user-prompts/${id}`, + reqParam: input, + returnParam: updated, + }); + return updated; } async function deleteUserPrompt(id: string): Promise { @@ -71,6 +94,12 @@ async function deleteUserPrompt(id: string): Promise { const err = (await res.json().catch(() => ({}))) as { detail?: string }; throw new Error(err.detail ?? `Failed to delete prompt: ${res.statusText}`); } + writeYLog({ + optType: OptType.delete, + optDetail: "B1-CM助手-自定义提示词-删除", + url: `/api/user-prompts/${id}`, + reqParam: { id }, + }); } /** Reactive list of the current user's saved prompts (server-backed). */ diff --git a/frontend-web/src/core/knowledge/api.ts b/frontend-web/src/core/knowledge/api.ts index 5439a561..1e79fa28 100644 --- a/frontend-web/src/core/knowledge/api.ts +++ b/frontend-web/src/core/knowledge/api.ts @@ -56,9 +56,17 @@ export async function listKnowledgeNotes(params: KnowledgeListParams = {}) { } export async function getKnowledgeNote(noteId: string) { - return parseResponse( + const result = await parseResponse( await apiFetch(`${baseUrl()}/notes/${encodeURIComponent(noteId)}`), ); + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-知识库-查看", + url: `${baseUrl()}/notes/${encodeURIComponent(noteId)}`, + reqParam: { noteId }, + returnParam: result, + }); + return result; } export async function createKnowledgeNote(data: { @@ -254,9 +262,17 @@ export async function resolveNoteWikilinks(noteId: string) { /** Fetch a raw vault meta/hub Markdown page (no DB note) by its path. */ export async function fetchVaultPage(path: string) { - return parseResponse( + const result = await parseResponse( await apiFetch(`${baseUrl()}/vault-page?path=${encodeURIComponent(path)}`), ); + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-知识库-Vault页面查看", + url: `${baseUrl()}/vault-page?path=${encodeURIComponent(path)}`, + reqParam: { path }, + returnParam: result, + }); + return result; } export async function exportKnowledgeGraph() { @@ -295,6 +311,13 @@ export async function listNoteVersions(noteId: string) { const res = await parseResponse<{ items: KnowledgeVersion[] }>( await apiFetch(`${baseUrl()}/notes/${encodeURIComponent(noteId)}/versions`), ); + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-知识库-历史版本列表", + url: `${baseUrl()}/notes/${encodeURIComponent(noteId)}/versions`, + reqParam: { noteId }, + returnParam: res.items, + }); return res.items; } @@ -307,23 +330,44 @@ export async function listDuplicates() { const res = await parseResponse<{ groups: DuplicateGroup[] }>( await apiFetch(`${baseUrl()}/duplicates`), ); + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-知识库-重复项列表", + url: `${baseUrl()}/duplicates`, + returnParam: res.groups, + }); return res.groups; } export async function mergeKnowledgeNotes(primaryId: string, duplicateIds: string[]) { - return parseResponse<{ note: KnowledgeNote }>( + const result = await parseResponse<{ note: KnowledgeNote }>( await apiFetch(`${baseUrl()}/merge`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ primary_id: primaryId, duplicate_ids: duplicateIds }), }), ); + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-知识库-合并", + url: `${baseUrl()}/merge`, + reqParam: { primary_id: primaryId, duplicate_ids: duplicateIds }, + returnParam: result, + }); + return result; } export async function reindexKnowledge() { - return parseResponse<{ reindexed: number; embedding_enabled: number }>( + const result = await parseResponse<{ reindexed: number; embedding_enabled: number }>( await apiFetch(`${baseUrl()}/reindex`, { method: "POST" }), ); + writeYLog({ + optType: OptType.other, + optDetail: "B1-CM助手-知识库-重建索引", + url: `${baseUrl()}/reindex`, + returnParam: result, + }); + return result; } export interface KnowledgeGraph { @@ -332,7 +376,14 @@ export interface KnowledgeGraph { } export async function getKnowledgeGraph() { - return parseResponse(await apiFetch(`${baseUrl()}/graph`)); + const result = await parseResponse(await apiFetch(`${baseUrl()}/graph`)); + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-知识库-图谱查看", + url: `${baseUrl()}/graph`, + returnParam: result, + }); + return result; } // --- graph blacklist --- @@ -348,26 +399,48 @@ export async function getGraphBlacklist() { const res = await parseResponse<{ items: GraphBlacklistItem[] }>( await apiFetch(`${baseUrl()}/graph/blacklist`), ); + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-知识库-图谱黑名单列表", + url: `${baseUrl()}/graph/blacklist`, + returnParam: res.items, + }); return res.items; } /** Blacklist node labels (entity names / note titles); they disappear from the graph. */ export async function addGraphBlacklist(labels: string[]) { - return parseResponse<{ added: number; items: GraphBlacklistItem[] }>( + const result = await parseResponse<{ added: number; items: GraphBlacklistItem[] }>( await apiFetch(`${baseUrl()}/graph/blacklist`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ labels }), }), ); + writeYLog({ + optType: OptType.create, + optDetail: "B1-CM助手-知识库-图谱黑名单新增", + url: `${baseUrl()}/graph/blacklist`, + reqParam: { labels }, + returnParam: result, + }); + return result; } export async function removeGraphBlacklist(entryId: string) { - return parseResponse<{ ok: boolean }>( + const result = await parseResponse<{ ok: boolean }>( await apiFetch(`${baseUrl()}/graph/blacklist/${encodeURIComponent(entryId)}`, { method: "DELETE", }), ); + writeYLog({ + optType: OptType.delete, + optDetail: "B1-CM助手-知识库-图谱黑名单删除", + url: `${baseUrl()}/graph/blacklist/${encodeURIComponent(entryId)}`, + reqParam: { entryId }, + returnParam: result, + }); + return result; } // --- folders (user-managed directories) --- @@ -376,6 +449,12 @@ export async function listKnowledgeFolders() { const res = await parseResponse<{ items: KnowledgeFolder[] }>( await apiFetch(`${baseUrl()}/folders`), ); + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-知识库-目录列表", + url: `${baseUrl()}/folders`, + returnParam: res.items, + }); return res.items; } @@ -387,6 +466,13 @@ export async function createKnowledgeFolder(path: string) { body: JSON.stringify({ path }), }), ); + writeYLog({ + optType: OptType.create, + optDetail: "B1-CM助手-知识库-目录新建", + url: `${baseUrl()}/folders`, + reqParam: { path }, + returnParam: res.folder, + }); return res.folder; } @@ -398,16 +484,31 @@ export async function renameKnowledgeFolder(folderId: string, path: string) { body: JSON.stringify({ path }), }), ); + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-知识库-目录重命名", + url: `${baseUrl()}/folders/${encodeURIComponent(folderId)}`, + reqParam: { folderId, path }, + returnParam: res.folder, + }); return res.folder; } export async function deleteKnowledgeFolder(folderId: string, reassignTo?: string) { const qs = reassignTo ? `?reassign_to=${encodeURIComponent(reassignTo)}` : ""; - return parseResponse<{ ok: boolean }>( + const result = await parseResponse<{ ok: boolean }>( await apiFetch(`${baseUrl()}/folders/${encodeURIComponent(folderId)}${qs}`, { method: "DELETE", }), ); + writeYLog({ + optType: OptType.delete, + optDetail: "B1-CM助手-知识库-目录删除", + url: `${baseUrl()}/folders/${encodeURIComponent(folderId)}${qs}`, + reqParam: { folderId, reassignTo }, + returnParam: result, + }); + return result; } // --- extraction prompt templates --- @@ -420,6 +521,13 @@ export async function listExtractTemplates(scope?: ExtractTemplateScope, enabled const res = await parseResponse<{ items: KnowledgeExtractTemplate[] }>( await apiFetch(`${baseUrl()}/extract-templates${q ? `?${q}` : ""}`), ); + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-知识库-抽取模板列表", + url: `${baseUrl()}/extract-templates${q ? `?${q}` : ""}`, + reqParam: { scope, enabledOnly }, + returnParam: res.items, + }); return res.items; } @@ -440,6 +548,13 @@ export async function createExtractTemplate(data: ExtractTemplateInput) { body: JSON.stringify(data), }), ); + writeYLog({ + optType: OptType.create, + optDetail: "B1-CM助手-知识库-抽取模板新建", + url: `${baseUrl()}/extract-templates`, + reqParam: data, + returnParam: res.template, + }); return res.template; } @@ -454,13 +569,28 @@ export async function updateExtractTemplate( body: JSON.stringify(data), }), ); + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-知识库-抽取模板修改", + url: `${baseUrl()}/extract-templates/${encodeURIComponent(templateId)}`, + reqParam: { templateId, data }, + returnParam: res.template, + }); return res.template; } export async function deleteExtractTemplate(templateId: string) { - return parseResponse<{ ok: boolean }>( + const result = await parseResponse<{ ok: boolean }>( await apiFetch(`${baseUrl()}/extract-templates/${encodeURIComponent(templateId)}`, { method: "DELETE", }), ); + writeYLog({ + optType: OptType.delete, + optDetail: "B1-CM助手-知识库-抽取模板删除", + url: `${baseUrl()}/extract-templates/${encodeURIComponent(templateId)}`, + reqParam: { templateId }, + returnParam: result, + }); + return result; } diff --git a/frontend-web/src/core/llm-metrics/api.ts b/frontend-web/src/core/llm-metrics/api.ts index 29ba87a0..b3433901 100644 --- a/frontend-web/src/core/llm-metrics/api.ts +++ b/frontend-web/src/core/llm-metrics/api.ts @@ -1,5 +1,6 @@ import { apiFetch } from "@/core/api"; import { getBackendBaseURL } from "@/core/config"; +import { OptType, writeYLog } from "@/core/log/y-log"; export interface LlmMetric { id: string; @@ -54,14 +55,21 @@ function buildQuery(filters: LlmMetricsListQuery): string { export async function loadLlmMetrics( query: LlmMetricsListQuery, ): Promise { - const response = await apiFetch( - `${getBackendBaseURL()}/api/admin/llm-metrics${buildQuery(query)}`, - ); + const url = `${getBackendBaseURL()}/api/admin/llm-metrics${buildQuery(query)}`; + const response = await apiFetch(url); if (!response.ok) { const detail = await response.json().catch(() => ({ detail: response.statusText })); throw new Error(detail.detail ?? "Failed to load LLM metrics"); } - return (await response.json()) as LlmMetricsListResponse; + const result = (await response.json()) as LlmMetricsListResponse; + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-模型指标-查询调用列表", + url, + reqParam: query, + returnParam: { total: result.total, count: result.items.length }, + }); + return result; } /** @@ -71,13 +79,18 @@ export async function loadLlmMetrics( export async function downloadLlmMetricsExcel( filters: LlmMetricsFilters, ): Promise { - const response = await apiFetch( - `${getBackendBaseURL()}/api/admin/llm-metrics/export${buildQuery(filters)}`, - ); + const requestUrl = `${getBackendBaseURL()}/api/admin/llm-metrics/export${buildQuery(filters)}`; + const response = await apiFetch(requestUrl); if (!response.ok) { const detail = await response.json().catch(() => ({ detail: response.statusText })); throw new Error(detail.detail ?? "Failed to export LLM metrics"); } + writeYLog({ + optType: OptType.export, + optDetail: "B1-CM助手-模型指标-导出Excel", + url: requestUrl, + reqParam: filters, + }); const blob = await response.blob(); const url = URL.createObjectURL(blob); const filename = (() => { diff --git a/frontend-web/src/core/mcp/api.ts b/frontend-web/src/core/mcp/api.ts index c3b602a1..10c83f02 100644 --- a/frontend-web/src/core/mcp/api.ts +++ b/frontend-web/src/core/mcp/api.ts @@ -1,11 +1,19 @@ import { getBackendBaseURL } from "@/core/config"; import { apiFetch } from "@/core/api"; +import { OptType, writeYLog } from "@/core/log/y-log"; import type { MCPConfig } from "./types"; export async function loadMCPConfig() { const response = await apiFetch(`${getBackendBaseURL()}/api/mcp/config`); - return response.json() as Promise; + const data = (await response.json()) as MCPConfig; + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-MCP服务-配置查看", + url: "/api/mcp/config", + returnParam: data, + }); + return data; } export async function updateMCPConfig(config: MCPConfig) { @@ -16,5 +24,13 @@ export async function updateMCPConfig(config: MCPConfig) { }, body: JSON.stringify(config), }); - return response.json(); + const data = await response.json(); + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-MCP服务-配置编辑", + url: "/api/mcp/config", + reqParam: config, + returnParam: data, + }); + return data; } diff --git a/frontend-web/src/core/memory/api.ts b/frontend-web/src/core/memory/api.ts index 8e0725ba..5a759744 100644 --- a/frontend-web/src/core/memory/api.ts +++ b/frontend-web/src/core/memory/api.ts @@ -2,6 +2,7 @@ import { apiFetch } from "../api"; import { getBackendBaseURL } from "../config"; +import { OptType, writeYLog } from "../log/y-log"; import type { MemorySearchMode, @@ -54,7 +55,15 @@ export async function loadMemoryV2(agent?: string): Promise { const response = await apiFetch( `${getBackendBaseURL()}/api/memory/v2${agentQuery(agent)}`, ); - return readJson(response, "Failed to fetch memory"); + const data = await readJson(response, "Failed to fetch memory"); + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-记忆-查看", + url: `/api/memory/v2${agentQuery(agent)}`, + reqParam: { agent }, + returnParam: data, + }); + return data; } export async function addMemoryEntry( @@ -70,7 +79,15 @@ export async function addMemoryEntry( body: JSON.stringify({ target, content }), }, ); - return readJson(response, "Failed to add memory entry"); + const data = await readJson(response, "Failed to add memory entry"); + writeYLog({ + optType: OptType.create, + optDetail: "B1-CM助手-记忆-新增", + url: `/api/memory/v2/entries${agentQuery(agent)}`, + reqParam: { target, content, agent }, + returnParam: data, + }); + return data; } export async function replaceMemoryEntry( @@ -87,7 +104,15 @@ export async function replaceMemoryEntry( body: JSON.stringify({ target, old_text: oldText, content }), }, ); - return readJson(response, "Failed to replace memory entry"); + const data = await readJson(response, "Failed to replace memory entry"); + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-记忆-编辑", + url: `/api/memory/v2/entries${agentQuery(agent)}`, + reqParam: { target, old_text: oldText, content, agent }, + returnParam: data, + }); + return data; } export async function deleteMemoryEntry( @@ -103,14 +128,29 @@ export async function deleteMemoryEntry( `${getBackendBaseURL()}/api/memory/v2/entries?${params.toString()}`, { method: "DELETE" }, ); - return readJson(response, "Failed to delete memory entry"); + const data = await readJson(response, "Failed to delete memory entry"); + writeYLog({ + optType: OptType.delete, + optDetail: "B1-CM助手-记忆-删除", + url: `/api/memory/v2/entries?${params.toString()}`, + reqParam: { target, old_text: oldText, agent }, + returnParam: data, + }); + return data; } // --- 配置 --- export async function loadMemoryConfig(): Promise { const response = await apiFetch(`${getBackendBaseURL()}/api/memory/v2/config`); - return readJson(response, "Failed to fetch memory config"); + const data = await readJson(response, "Failed to fetch memory config"); + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-记忆-配置查看", + url: "/api/memory/v2/config", + returnParam: data, + }); + return data; } export async function patchMemoryConfig( @@ -121,7 +161,15 @@ export async function patchMemoryConfig( headers: { "Content-Type": "application/json" }, body: JSON.stringify({ overrides }), }); - return readJson(response, "Failed to update memory config"); + const data = await readJson(response, "Failed to update memory config"); + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-记忆-配置编辑", + url: "/api/memory/v2/config", + reqParam: { overrides }, + returnParam: data, + }); + return data; } // --- Hindsight 检索 --- @@ -139,5 +187,13 @@ export async function searchMemory( body: JSON.stringify({ query, mode }), }, ); - return readJson(response, "Failed to search memory"); + const data = await readJson(response, "Failed to search memory"); + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-记忆-检索", + url: `/api/memory/v2/search${agentQuery(agent)}`, + reqParam: { query, mode, agent }, + returnParam: data, + }); + return data; } diff --git a/frontend-web/src/core/models/api.ts b/frontend-web/src/core/models/api.ts index 02e600fb..63ff64a9 100644 --- a/frontend-web/src/core/models/api.ts +++ b/frontend-web/src/core/models/api.ts @@ -1,13 +1,21 @@ import { getBackendBaseURL } from "../config"; import { apiFetch } from "../api"; +import { OptType, writeYLog } from "../log/y-log"; import type { ModelsResponse } from "./types"; export async function loadModels(): Promise { const res = await apiFetch(`${getBackendBaseURL()}/api/models`); const data = (await res.json()) as Partial; - return { + const result: ModelsResponse = { models: data.models ?? [], token_usage: data.token_usage ?? { enabled: false }, }; + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-模型-查询", + url: "/api/models", + returnParam: result, + }); + return result; } diff --git a/frontend-web/src/core/note/api.ts b/frontend-web/src/core/note/api.ts index 71a1c2cc..f4a5dbd9 100644 --- a/frontend-web/src/core/note/api.ts +++ b/frontend-web/src/core/note/api.ts @@ -1,4 +1,5 @@ import { getNoteApiBaseURL } from "@/core/config"; +import { OptType, writeYLog } from "@/core/log/y-log"; import { ensureNoteAuth, @@ -76,7 +77,15 @@ export async function createSearchSpace( throw new Error(`创建笔记失败: ${response.status} ${response.statusText}`); } - return (await response.json()) as SearchSpace; + const result = (await response.json()) as SearchSpace; + writeYLog({ + optType: OptType.create, + optDetail: "B1-CM助手-笔记-新建笔记空间", + url: "/api/v1/searchspaces", + reqParam: { name: input.name, description: input.description ?? "" }, + returnParam: { id: result.id, name: result.name }, + }); + return result; } export async function listSearchSpaces( @@ -90,14 +99,21 @@ export async function listSearchSpaces( order_by: query.order_by ?? "created_at", }); - const response = await noteApiFetch(`/api/v1/searchspaces?${params.toString()}`, { - method: "GET", - }); + const path = `/api/v1/searchspaces?${params.toString()}`; + const response = await noteApiFetch(path, { method: "GET" }); if (!response.ok) { throw new Error(`获取笔记列表失败: ${response.status} ${response.statusText}`); } const data: unknown = await response.json(); - return Array.isArray(data) ? (data as SearchSpace[]) : []; + const list = Array.isArray(data) ? (data as SearchSpace[]) : []; + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-笔记-查询笔记空间列表", + url: path, + reqParam: query, + returnParam: { count: list.length }, + }); + return list; } diff --git a/frontend-web/src/core/notifications/api.ts b/frontend-web/src/core/notifications/api.ts index 0e854bdb..19c54190 100644 --- a/frontend-web/src/core/notifications/api.ts +++ b/frontend-web/src/core/notifications/api.ts @@ -1,5 +1,6 @@ import { apiFetch } from "@/core/api"; import { getBackendBaseURL } from "@/core/config"; +import { OptType, writeYLog } from "@/core/log/y-log"; import type { NotificationItem, @@ -24,6 +25,13 @@ export async function listNotifications( const res = await apiFetch(url); if (!res.ok) throw await parseDetail(res, "Failed to list notifications"); const data = (await res.json()) as NotificationListResponse; + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-通知-查询通知列表", + url, + reqParam: options, + returnParam: { count: data.items.length }, + }); return data.items; } @@ -35,26 +43,39 @@ export async function getUnreadCount(): Promise { } export async function markNotificationRead(id: string): Promise { - const res = await apiFetch( - `${getBackendBaseURL()}/api/notifications/${encodeURIComponent(id)}/read`, - { method: "POST" }, - ); + const url = `${getBackendBaseURL()}/api/notifications/${encodeURIComponent(id)}/read`; + const res = await apiFetch(url, { method: "POST" }); if (!res.ok) throw await parseDetail(res, "Failed to mark notification read"); + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-通知-标记已读", + url, + reqParam: { id }, + }); } export async function markAllNotificationsRead(): Promise { - const res = await apiFetch(`${getBackendBaseURL()}/api/notifications/read_all`, { - method: "POST", - }); + const url = `${getBackendBaseURL()}/api/notifications/read_all`; + const res = await apiFetch(url, { method: "POST" }); if (!res.ok) throw await parseDetail(res, "Failed to mark all read"); const data = (await res.json()) as { updated: number }; + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-通知-全部标记已读", + url, + returnParam: data, + }); return data.updated; } export async function deleteNotification(id: string): Promise { - const res = await apiFetch( - `${getBackendBaseURL()}/api/notifications/${encodeURIComponent(id)}`, - { method: "DELETE" }, - ); + const url = `${getBackendBaseURL()}/api/notifications/${encodeURIComponent(id)}`; + const res = await apiFetch(url, { method: "DELETE" }); if (!res.ok) throw await parseDetail(res, "Failed to delete notification"); + writeYLog({ + optType: OptType.delete, + optDetail: "B1-CM助手-通知-删除通知", + url, + reqParam: { id }, + }); } diff --git a/frontend-web/src/core/positions/api.ts b/frontend-web/src/core/positions/api.ts index be1a8521..f7106191 100644 --- a/frontend-web/src/core/positions/api.ts +++ b/frontend-web/src/core/positions/api.ts @@ -1,5 +1,6 @@ import { apiFetch } from "@/core/api"; import { getBackendBaseURL } from "@/core/config"; +import { OptType, writeYLog } from "@/core/log/y-log"; import type { Position, @@ -24,6 +25,12 @@ export async function listPositions(): Promise { const res = await apiFetch(base()); if (!res.ok) throw await parseDetail(res, "Failed to list positions"); const data = (await res.json()) as PositionListResponse; + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-岗位-查询", + url: "/api/positions", + returnParam: data.positions, + }); return data.positions; } @@ -34,7 +41,15 @@ export async function createPosition(input: PositionCreateInput): Promise { @@ -44,18 +59,39 @@ export async function updatePosition(id: string, input: PositionUpdateInput): Pr body: JSON.stringify(input), }); if (!res.ok) throw await parseDetail(res, "Failed to update position"); - return (await res.json()) as Position; + const updated = (await res.json()) as Position; + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-岗位-编辑", + url: `/api/positions/${id}`, + reqParam: input, + returnParam: updated, + }); + return updated; } export async function deletePosition(id: string): Promise { const res = await apiFetch(`${base()}/${encodeURIComponent(id)}`, { method: "DELETE" }); if (!res.ok) throw await parseDetail(res, "Failed to delete position"); + writeYLog({ + optType: OptType.delete, + optDetail: "B1-CM助手-岗位-删除", + url: `/api/positions/${id}`, + reqParam: { id }, + }); } export async function getTagBindings(positionId: string): Promise { const res = await apiFetch(`${base()}/${encodeURIComponent(positionId)}/tag-bindings`); if (!res.ok) throw await parseDetail(res, "Failed to load tag bindings"); const data = (await res.json()) as TagBindingsResponse; + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-岗位-标签绑定查看", + url: `/api/positions/${positionId}/tag-bindings`, + reqParam: { positionId }, + returnParam: data.bindings, + }); return data.bindings; } @@ -67,6 +103,13 @@ export async function setTagBindings(positionId: string, bindings: TagBindings): }); if (!res.ok) throw await parseDetail(res, "Failed to save tag bindings"); const data = (await res.json()) as TagBindingsResponse; + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-岗位-标签绑定保存", + url: `/api/positions/${positionId}/tag-bindings`, + reqParam: { positionId, bindings }, + returnParam: data.bindings, + }); return data.bindings; } @@ -74,6 +117,13 @@ export async function listMembers(positionId: string): Promise const res = await apiFetch(`${base()}/${encodeURIComponent(positionId)}/members`); if (!res.ok) throw await parseDetail(res, "Failed to list members"); const data = (await res.json()) as { members: PositionMember[] }; + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-岗位-成员查询", + url: `/api/positions/${positionId}/members`, + reqParam: { positionId }, + returnParam: data.members, + }); return data.members; } @@ -85,6 +135,13 @@ export async function addMembers(positionId: string, userIds: string[]): Promise }); if (!res.ok) throw await parseDetail(res, "Failed to add members"); const data = (await res.json()) as { members: PositionMember[] }; + writeYLog({ + optType: OptType.create, + optDetail: "B1-CM助手-岗位-添加成员", + url: `/api/positions/${positionId}/members`, + reqParam: { positionId, userIds }, + returnParam: data.members, + }); return data.members; } @@ -96,6 +153,13 @@ export async function removeMembers(positionId: string, userIds: string[]): Prom }); if (!res.ok) throw await parseDetail(res, "Failed to remove members"); const data = (await res.json()) as { members: PositionMember[] }; + writeYLog({ + optType: OptType.delete, + optDetail: "B1-CM助手-岗位-移除成员", + url: `/api/positions/${positionId}/members`, + reqParam: { positionId, userIds }, + returnParam: data.members, + }); return data.members; } @@ -103,6 +167,13 @@ export async function resyncPosition(positionId: string): Promise { const res = await apiFetch(`${base()}/${encodeURIComponent(positionId)}/resync`, { method: "POST" }); if (!res.ok) throw await parseDetail(res, "Failed to resync position"); const data = (await res.json()) as { synced: number }; + writeYLog({ + optType: OptType.other, + optDetail: "B1-CM助手-岗位-同步", + url: `/api/positions/${positionId}/resync`, + reqParam: { positionId }, + returnParam: data, + }); return data.synced; } @@ -117,6 +188,12 @@ export async function listPositionUsers(): Promise { const res = await apiFetch(`${base()}/users`); if (!res.ok) throw await parseDetail(res, "Failed to list users"); const data = (await res.json()) as { users: PositionUser[] }; + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-岗位-用户查询", + url: "/api/positions/users", + returnParam: data.users, + }); return data.users; } @@ -127,4 +204,10 @@ export async function setUserPosition(userId: string, positionId: string | null) body: JSON.stringify({ position_id: positionId }), }); if (!res.ok) throw await parseDetail(res, "Failed to set user position"); + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-岗位-设置用户岗位", + url: `/api/positions/users/${userId}`, + reqParam: { userId, positionId }, + }); } diff --git a/frontend-web/src/core/recommended-questions/api.ts b/frontend-web/src/core/recommended-questions/api.ts index ad840f7f..d125872a 100644 --- a/frontend-web/src/core/recommended-questions/api.ts +++ b/frontend-web/src/core/recommended-questions/api.ts @@ -1,5 +1,6 @@ import { apiFetch } from "@/core/api"; import { getBackendBaseURL } from "@/core/config"; +import { OptType, writeYLog } from "@/core/log/y-log"; import type { CreateQuestionRequest, @@ -12,16 +13,25 @@ const BASE = () => `${getBackendBaseURL()}/api/recommended-questions`; export async function listRecommendedQuestions(all = false): Promise { // ``all=true`` is the admin management view: every question, unfiltered by the // caller's 岗位 (position). The chat input uses the default (filtered) view. - const res = await apiFetch(`${BASE()}${all ? "?all=1" : ""}`); + const url = `${BASE()}${all ? "?all=1" : ""}`; + const res = await apiFetch(url); if (!res.ok) throw new Error(`Failed to load questions: ${res.statusText}`); const data = (await res.json()) as { questions: RecommendedQuestion[] }; + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-推荐问题-查询列表", + url, + reqParam: { all }, + returnParam: { count: data.questions.length }, + }); return data.questions; } export async function createRecommendedQuestion( body: CreateQuestionRequest, ): Promise { - const res = await apiFetch(BASE(), { + const url = BASE(); + const res = await apiFetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), @@ -30,14 +40,23 @@ export async function createRecommendedQuestion( const err = (await res.json().catch(() => ({}))) as { detail?: string }; throw new Error(err.detail ?? `Failed to create question: ${res.statusText}`); } - return res.json() as Promise; + const result = (await res.json()) as RecommendedQuestion; + writeYLog({ + optType: OptType.create, + optDetail: "B1-CM助手-推荐问题-新增问题", + url, + reqParam: body, + returnParam: result, + }); + return result; } export async function updateRecommendedQuestion( id: string, body: UpdateQuestionRequest, ): Promise { - const res = await apiFetch(`${BASE()}/${id}`, { + const url = `${BASE()}/${id}`; + const res = await apiFetch(url, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), @@ -46,23 +65,45 @@ export async function updateRecommendedQuestion( const err = (await res.json().catch(() => ({}))) as { detail?: string }; throw new Error(err.detail ?? `Failed to update question: ${res.statusText}`); } - return res.json() as Promise; + const result = (await res.json()) as RecommendedQuestion; + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-推荐问题-编辑问题", + url, + reqParam: { id, ...body }, + returnParam: result, + }); + return result; } export async function deleteRecommendedQuestion(id: string): Promise { - const res = await apiFetch(`${BASE()}/${id}`, { method: "DELETE" }); + const url = `${BASE()}/${id}`; + const res = await apiFetch(url, { method: "DELETE" }); if (!res.ok) throw new Error(`Failed to delete question: ${res.statusText}`); + writeYLog({ + optType: OptType.delete, + optDetail: "B1-CM助手-推荐问题-删除问题", + url, + reqParam: { id }, + }); } export async function reorderRecommendedQuestions( ids: string[], ): Promise { - const res = await apiFetch(`${getBackendBaseURL()}/api/recommended-questions-reorder`, { + const url = `${getBackendBaseURL()}/api/recommended-questions-reorder`; + const res = await apiFetch(url, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ids }), }); if (!res.ok) throw new Error(`Failed to reorder questions: ${res.statusText}`); const data = (await res.json()) as { questions: RecommendedQuestion[] }; + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-推荐问题-调整排序", + url, + reqParam: { ids }, + }); return data.questions; } diff --git a/frontend-web/src/core/roundtable-draft-shares/api.ts b/frontend-web/src/core/roundtable-draft-shares/api.ts index 3535cf1c..cf48e1dc 100644 --- a/frontend-web/src/core/roundtable-draft-shares/api.ts +++ b/frontend-web/src/core/roundtable-draft-shares/api.ts @@ -1,5 +1,6 @@ import { apiFetch } from "@/core/api"; import { getBackendBaseURL } from "@/core/config"; +import { OptType, writeYLog } from "@/core/log/y-log"; // Share codes share the same alphabet/shape as conversation shares, so reuse the // extractor instead of duplicating the regex. export { extractShareCode } from "@/core/thread-shares/api"; @@ -86,7 +87,15 @@ export async function createDraftShare( body: JSON.stringify({ draft_id: draftId, mode }), }); if (!response.ok) throw await parseError(response, "创建分享失败"); - return response.json() as Promise; + const result = (await response.json()) as DraftShare; + writeYLog({ + optType: OptType.create, + optDetail: "B1-CM助手-会商草稿分享-创建分享", + url: baseUrl(), + reqParam: { draft_id: draftId, mode }, + returnParam: { share_code: result.share_code, mode: result.mode }, + }); + return result; } /** List the current user's own roundtable-draft share links. */ @@ -94,34 +103,57 @@ export async function listDraftShares(): Promise { const response = await apiFetch(baseUrl()); if (!response.ok) throw await parseError(response, "加载分享列表失败"); const json = (await response.json()) as { shares: DraftShare[] }; + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-会商草稿分享-查询分享列表", + url: baseUrl(), + returnParam: { count: json.shares.length }, + }); return json.shares; } /** Revoke a share link so it can no longer be previewed, imported, or viewed. */ export async function revokeDraftShare(shareCode: string): Promise { - const response = await apiFetch(`${baseUrl()}/${encodeURIComponent(shareCode)}`, { - method: "DELETE", - }); + const url = `${baseUrl()}/${encodeURIComponent(shareCode)}`; + const response = await apiFetch(url, { method: "DELETE" }); if (!response.ok) throw await parseError(response, "撤销分享失败"); + writeYLog({ + optType: OptType.delete, + optDetail: "B1-CM助手-会商草稿分享-撤销分享", + url, + reqParam: { shareCode }, + }); } /** Preview a share link before importing it. */ export async function previewDraftShare(shareCode: string): Promise { - const response = await apiFetch( - `${baseUrl()}/${encodeURIComponent(shareCode)}/preview`, - ); + const url = `${baseUrl()}/${encodeURIComponent(shareCode)}/preview`; + const response = await apiFetch(url); if (!response.ok) throw await parseError(response, "无法读取该分享"); - return response.json() as Promise; + const result = (await response.json()) as DraftSharePreview; + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-会商草稿分享-预览分享", + url, + reqParam: { shareCode }, + }); + return result; } /** Import the roundtable draft behind a share link into the current account. */ export async function importDraftShare(shareCode: string): Promise { - const response = await apiFetch( - `${baseUrl()}/${encodeURIComponent(shareCode)}/import`, - { method: "POST" }, - ); + const url = `${baseUrl()}/${encodeURIComponent(shareCode)}/import`; + const response = await apiFetch(url, { method: "POST" }); if (!response.ok) throw await parseError(response, "导入失败"); - return response.json() as Promise; + const result = (await response.json()) as DraftImportResult; + writeYLog({ + optType: OptType.import, + optDetail: "B1-CM助手-会商草稿分享-导入分享", + url, + reqParam: { shareCode }, + returnParam: result, + }); + return result; } /** diff --git a/frontend-web/src/core/scheduled-tasks/api.ts b/frontend-web/src/core/scheduled-tasks/api.ts index 6fccbbe8..9b73c043 100644 --- a/frontend-web/src/core/scheduled-tasks/api.ts +++ b/frontend-web/src/core/scheduled-tasks/api.ts @@ -93,9 +93,17 @@ export async function createScheduledTask(data: { } export async function listScheduledTaskRuns(taskId: string) { - return parseResponse( + const result = await parseResponse( await apiFetch(`${baseUrl()}/${encodeURIComponent(taskId)}/runs`), ); + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-定时任务-运行记录列表", + url: `${baseUrl()}/${encodeURIComponent(taskId)}/runs`, + reqParam: { taskId }, + returnParam: result, + }); + return result; } export async function getScheduledTaskRun(runId: string) { @@ -113,18 +121,34 @@ export async function getScheduledTaskRun(runId: string) { } export async function deleteScheduledTaskRun(runId: string) { - return parseResponse<{ success: boolean }>( + const result = await parseResponse<{ success: boolean }>( await apiFetch(`${baseUrl()}/runs/${encodeURIComponent(runId)}`, { method: "DELETE", }), ); + writeYLog({ + optType: OptType.delete, + optDetail: "B1-CM助手-定时任务-删除运行记录", + url: `${baseUrl()}/runs/${encodeURIComponent(runId)}`, + reqParam: { runId }, + returnParam: result, + }); + return result; } /** Agent messages for a run's underlying agent run — used to inspect a stuck/running run. */ export async function getScheduledTaskRunMessages(runId: string) { - return parseResponse( + const result = await parseResponse( await apiFetch(`${baseUrl()}/runs/${encodeURIComponent(runId)}/messages`), ); + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-定时任务-运行消息", + url: `${baseUrl()}/runs/${encodeURIComponent(runId)}/messages`, + reqParam: { runId }, + returnParam: result, + }); + return result; } export function scheduledTaskRunDownloadUrl(runId: string) { @@ -136,9 +160,17 @@ export function scheduledTaskRunFileDownloadUrl(runId: string, fileIndex: number } export async function getScheduledTaskRunFileContent(runId: string, fileIndex: number) { - return parseResponse( + const result = await parseResponse( await apiFetch(`${baseUrl()}/runs/${encodeURIComponent(runId)}/files/${fileIndex}`), ); + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-定时任务-运行文件查看", + url: `${baseUrl()}/runs/${encodeURIComponent(runId)}/files/${fileIndex}`, + reqParam: { runId, fileIndex }, + returnParam: result, + }); + return result; } export async function updateScheduledTaskRunFile( @@ -146,13 +178,21 @@ export async function updateScheduledTaskRunFile( fileIndex: number, content: string, ) { - return parseResponse( + const result = await parseResponse( await apiFetch(`${baseUrl()}/runs/${encodeURIComponent(runId)}/files/${fileIndex}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content }), }), ); + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-定时任务-运行文件编辑", + url: `${baseUrl()}/runs/${encodeURIComponent(runId)}/files/${fileIndex}`, + reqParam: { runId, fileIndex }, + returnParam: result, + }); + return result; } function filenameFromContentDisposition(header: string | null): string | null { @@ -212,6 +252,12 @@ export async function downloadScheduledTaskRun(runId: string) { await apiFetch(scheduledTaskRunDownloadUrl(runId)), "scheduled-task-result.zip", ); + writeYLog({ + optType: OptType.export, + optDetail: "B1-CM助手-定时任务-下载运行结果", + url: scheduledTaskRunDownloadUrl(runId), + reqParam: { runId }, + }); } export async function downloadScheduledTaskRunFile(runId: string, fileIndex: number) { @@ -219,24 +265,45 @@ export async function downloadScheduledTaskRunFile(runId: string, fileIndex: num await apiFetch(scheduledTaskRunFileDownloadUrl(runId, fileIndex)), "scheduled-task-file", ); + writeYLog({ + optType: OptType.export, + optDetail: "B1-CM助手-定时任务-下载运行文件", + url: scheduledTaskRunFileDownloadUrl(runId, fileIndex), + reqParam: { runId, fileIndex }, + }); } export async function getScheduledTaskDeliveryProfile() { - return parseResponse( + const result = await parseResponse( await apiFetch(`${baseUrl()}/delivery-profile`), ); + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-定时任务-投递配置查看", + url: `${baseUrl()}/delivery-profile`, + returnParam: result, + }); + return result; } export async function updateScheduledTaskDeliveryProfile(data: { element_user_id?: string | null; }) { - return parseResponse( + const result = await parseResponse( await apiFetch(`${baseUrl()}/delivery-profile`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }), ); + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-定时任务-投递配置修改", + url: `${baseUrl()}/delivery-profile`, + reqParam: data, + returnParam: result, + }); + return result; } export async function updateScheduledTask( @@ -339,53 +406,101 @@ export async function listPublishedScheduledTasks(params: { limit?: number; offs if (params.limit != null) query.set("limit", String(params.limit)); if (params.offset != null) query.set("offset", String(params.offset)); const suffix = query.toString(); - return parseResponse( + const result = await parseResponse( await apiFetch(`${baseUrl()}/published${suffix ? `?${suffix}` : ""}`), ); + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-定时任务-已发布列表", + url: `${baseUrl()}/published${suffix ? `?${suffix}` : ""}`, + reqParam: params, + returnParam: result, + }); + return result; } export async function publishScheduledTask(taskId: string) { - return parseResponse( + const result = await parseResponse( await apiFetch(`${baseUrl()}/${encodeURIComponent(taskId)}/publish`, { method: "POST", }), ); + writeYLog({ + optType: OptType.export, + optDetail: "B1-CM助手-定时任务-发布", + url: `${baseUrl()}/${encodeURIComponent(taskId)}/publish`, + reqParam: { taskId }, + returnParam: result, + }); + return result; } export async function unpublishScheduledTask(taskId: string) { - return parseResponse( + const result = await parseResponse( await apiFetch(`${baseUrl()}/${encodeURIComponent(taskId)}/unpublish`, { method: "POST", }), ); + writeYLog({ + optType: OptType.other, + optDetail: "B1-CM助手-定时任务-取消发布", + url: `${baseUrl()}/${encodeURIComponent(taskId)}/unpublish`, + reqParam: { taskId }, + returnParam: result, + }); + return result; } export async function subscribeScheduledTask(taskId: string, notifyElement: boolean) { - return parseResponse( + const result = await parseResponse( await apiFetch(`${baseUrl()}/${encodeURIComponent(taskId)}/subscribe`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ notify_element: notifyElement }), }), ); + writeYLog({ + optType: OptType.create, + optDetail: "B1-CM助手-定时任务-订阅", + url: `${baseUrl()}/${encodeURIComponent(taskId)}/subscribe`, + reqParam: { taskId, notify_element: notifyElement }, + returnParam: result, + }); + return result; } export async function updateScheduledTaskSubscription(taskId: string, notifyElement: boolean) { - return parseResponse( + const result = await parseResponse( await apiFetch(`${baseUrl()}/${encodeURIComponent(taskId)}/subscribe`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ notify_element: notifyElement }), }), ); + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-定时任务-订阅修改", + url: `${baseUrl()}/${encodeURIComponent(taskId)}/subscribe`, + reqParam: { taskId, notify_element: notifyElement }, + returnParam: result, + }); + return result; } export async function unsubscribeScheduledTask(taskId: string) { - return parseResponse<{ success: boolean }>( + const result = await parseResponse<{ success: boolean }>( await apiFetch(`${baseUrl()}/${encodeURIComponent(taskId)}/subscribe`, { method: "DELETE", }), ); + writeYLog({ + optType: OptType.delete, + optDetail: "B1-CM助手-定时任务-取消订阅", + url: `${baseUrl()}/${encodeURIComponent(taskId)}/subscribe`, + reqParam: { taskId }, + returnParam: result, + }); + return result; } // --- Admin (cross-user management) --- @@ -395,16 +510,32 @@ export async function listAllScheduledTasks(search?: string) { const qs = new URLSearchParams(); if (search) qs.set("search", search); const query = qs.toString(); - return parseResponse( + const result = await parseResponse( await apiFetch(`${baseUrl()}/admin/all${query ? `?${query}` : ""}`), ); + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-定时任务-全部任务列表", + url: `${baseUrl()}/admin/all${query ? `?${query}` : ""}`, + reqParam: { search }, + returnParam: result, + }); + return result; } /** List runs for any task regardless of owner. Admin-only endpoint. */ export async function adminListScheduledTaskRuns(taskId: string) { - return parseResponse( + const result = await parseResponse( await apiFetch(`${baseUrl()}/admin/${encodeURIComponent(taskId)}/runs`), ); + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-定时任务-运行记录列表", + url: `${baseUrl()}/admin/${encodeURIComponent(taskId)}/runs`, + reqParam: { taskId }, + returnParam: result, + }); + return result; } /** Update any task regardless of owner. Admin-only endpoint. */ @@ -421,52 +552,99 @@ export async function adminUpdateScheduledTask( require_markdown: boolean; }>, ) { - return parseResponse( + const result = await parseResponse( await apiFetch(`${baseUrl()}/admin/${encodeURIComponent(taskId)}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }), ); + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-定时任务-修改", + url: `${baseUrl()}/admin/${encodeURIComponent(taskId)}`, + reqParam: { taskId, data }, + returnParam: result, + }); + return result; } /** Delete any task regardless of owner. Admin-only endpoint. */ export async function adminDeleteScheduledTask(taskId: string) { - return parseResponse<{ success: boolean }>( + const result = await parseResponse<{ success: boolean }>( await apiFetch(`${baseUrl()}/admin/${encodeURIComponent(taskId)}`, { method: "DELETE", }), ); + writeYLog({ + optType: OptType.delete, + optDetail: "B1-CM助手-定时任务-删除", + url: `${baseUrl()}/admin/${encodeURIComponent(taskId)}`, + reqParam: { taskId }, + returnParam: result, + }); + return result; } /** Trigger an immediate run of any task regardless of owner. Admin-only. */ export async function adminTriggerScheduledTask(taskId: string) { - return parseResponse<{ success: boolean; task: ScheduledTask }>( + const result = await parseResponse<{ success: boolean; task: ScheduledTask }>( await apiFetch(`${baseUrl()}/admin/${encodeURIComponent(taskId)}/trigger`, { method: "POST", }), ); + writeYLog({ + optType: OptType.other, + optDetail: "B1-CM助手-定时任务-手动运行", + url: `${baseUrl()}/admin/${encodeURIComponent(taskId)}/trigger`, + reqParam: { taskId }, + returnParam: result, + }); + return result; } // --- HTML page tasks --- /** Fetch the generated HTML document for an ``html_page`` run. */ export async function getScheduledTaskRunHtml(runId: string) { - return parseResponse( + const result = await parseResponse( await apiFetch(`${baseUrl()}/runs/${encodeURIComponent(runId)}/html`), ); + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-定时任务-运行HTML查看", + url: `${baseUrl()}/runs/${encodeURIComponent(runId)}/html`, + reqParam: { runId }, + returnParam: result, + }); + return result; } const favoritesUrl = () => `${getBackendBaseURL()}/api/html-page-favorites`; export async function listHtmlPageFavorites() { - return parseResponse(await apiFetch(favoritesUrl())); + const result = await parseResponse(await apiFetch(favoritesUrl())); + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-HTML收藏-列表", + url: `/api/html-page-favorites`, + returnParam: result, + }); + return result; } export async function getHtmlPageFavorite(favoriteId: string) { - return parseResponse( + const result = await parseResponse( await apiFetch(`${favoritesUrl()}/${encodeURIComponent(favoriteId)}`), ); + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-HTML收藏-查看", + url: `/api/html-page-favorites/${encodeURIComponent(favoriteId)}`, + reqParam: { favoriteId }, + returnParam: result, + }); + return result; } export async function createHtmlPageFavorite(data: { @@ -475,19 +653,35 @@ export async function createHtmlPageFavorite(data: { description?: string | null; tags?: string[]; }) { - return parseResponse( + const result = await parseResponse( await apiFetch(favoritesUrl(), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }), ); + writeYLog({ + optType: OptType.create, + optDetail: "B1-CM助手-HTML收藏-新建", + url: `/api/html-page-favorites`, + reqParam: data, + returnParam: result, + }); + return result; } export async function deleteHtmlPageFavorite(favoriteId: string) { - return parseResponse<{ success: boolean }>( + const result = await parseResponse<{ success: boolean }>( await apiFetch(`${favoritesUrl()}/${encodeURIComponent(favoriteId)}`, { method: "DELETE", }), ); + writeYLog({ + optType: OptType.delete, + optDetail: "B1-CM助手-HTML收藏-删除", + url: `/api/html-page-favorites/${encodeURIComponent(favoriteId)}`, + reqParam: { favoriteId }, + returnParam: result, + }); + return result; } diff --git a/frontend-web/src/core/studio/api.ts b/frontend-web/src/core/studio/api.ts index f2aa636b..d2f31aa6 100644 --- a/frontend-web/src/core/studio/api.ts +++ b/frontend-web/src/core/studio/api.ts @@ -1,4 +1,5 @@ import { getNotebookApiBaseURL } from "@/core/config"; +import { OptType, writeYLog } from "@/core/log/y-log"; import type { SourceItem, StudioNotebook } from "./types"; function base(): string { @@ -30,7 +31,14 @@ export async function fetchStudioNotebooks( headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) throw new Error(`获取空间列表失败:${res.status}`); - return res.json() as Promise; + const notebooks = (await res.json()) as StudioNotebook[]; + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-工作室-空间查询", + url: "/api/v1/searchspaces", + returnParam: notebooks, + }); + return notebooks; } export async function createSearchSpace( @@ -52,6 +60,12 @@ export async function createSearchSpace( const body = await res.json().catch(() => null); throw new Error(body?.detail || `创建失败:${res.status}`); } + writeYLog({ + optType: OptType.create, + optDetail: "B1-CM助手-工作室-新建空间", + url: "/api/v1/searchspaces", + reqParam: { name }, + }); } export async function uploadSourceText( @@ -75,6 +89,12 @@ export async function uploadSourceText( }), }); if (!res.ok) throw new Error(`上传失败:${res.status}`); + writeYLog({ + optType: OptType.import, + optDetail: "B1-CM助手-工作室-上传文字来源", + url: "/api/v1/documents/textupload", + reqParam: { search_space_id: notebookId, title: title || "粘贴的文字" }, + }); } export async function deleteSearchSpace( @@ -88,6 +108,12 @@ export async function deleteSearchSpace( headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) throw new Error(`删除失败:${res.status}`); + writeYLog({ + optType: OptType.delete, + optDetail: "B1-CM助手-工作室-删除空间", + url: `/api/v1/searchspaces/${id}`, + reqParam: { id }, + }); } export async function renameSearchSpace( @@ -106,6 +132,12 @@ export async function renameSearchSpace( body: JSON.stringify({ name }), }); if (!res.ok) throw new Error(`重命名失败:${res.status}`); + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-工作室-重命名空间", + url: `/api/v1/searchspaces/${id}`, + reqParam: { id, name }, + }); } export interface ChunkRead { @@ -134,7 +166,15 @@ export async function fetchDocumentByChunk( headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) throw new Error(`获取文档失败:${res.status}`); - return res.json() as Promise; + const doc = (await res.json()) as DocumentWithChunks; + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-工作室-查看文档", + url: `/api/v1/documents/by-chunk/${chunkId}`, + reqParam: { chunkId }, + returnParam: { id: doc.id, title: doc.title, file_type: doc.file_type }, + }); + return doc; } export async function uploadSourceFiles( @@ -157,6 +197,15 @@ export async function uploadSourceFiles( body: formData, }); if (!res.ok) throw new Error(`上传失败:${res.status}`); + writeYLog({ + optType: OptType.import, + optDetail: "B1-CM助手-工作室-上传文件来源", + url: "/api/v1/documents/fileupload", + reqParam: { + search_space_id: notebookId, + files: files.map((f) => f.name), + }, + }); } export async function fetchNotebookSources( @@ -171,5 +220,13 @@ export async function fetchNotebookSources( { headers: { Authorization: `Bearer ${token}` } }, ); if (!res.ok) throw new Error(`获取来源失败:${res.status}`); - return res.json() as Promise; + const sources = (await res.json()) as SourceItem[]; + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-工作室-来源查询", + url: `/api/v1/source/search-space/${notebookId}?limit=100`, + reqParam: { notebookId }, + returnParam: sources, + }); + return sources; } diff --git a/frontend-web/src/core/system-settings/api.ts b/frontend-web/src/core/system-settings/api.ts index 26d854f6..fa78767d 100644 --- a/frontend-web/src/core/system-settings/api.ts +++ b/frontend-web/src/core/system-settings/api.ts @@ -1,5 +1,6 @@ import { apiFetch } from "@/core/api"; import { getBackendBaseURL } from "@/core/config"; +import { OptType, writeYLog } from "@/core/log/y-log"; export interface PromptPrefixSettings { enabled: boolean; @@ -20,7 +21,14 @@ export async function loadPromptPrefixSettings(): Promise if (!response.ok) { throw new Error(`Failed to load prompt prefix settings: ${response.status}`); } - return (await response.json()) as PromptPrefixSettings; + const data = (await response.json()) as PromptPrefixSettings; + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-系统设置-提示词前缀查看", + url: "/api/system-settings/prompt-prefix", + returnParam: data, + }); + return data; } export async function updatePromptPrefixSettings( @@ -40,7 +48,15 @@ export async function updatePromptPrefixSettings( .catch(() => ({ detail: "Failed to update prompt prefix" })); throw new Error(detail.detail ?? "Failed to update prompt prefix"); } - return (await response.json()) as PromptPrefixSettings; + const data = (await response.json()) as PromptPrefixSettings; + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-系统设置-提示词前缀编辑", + url: "/api/system-settings/prompt-prefix", + reqParam: body, + returnParam: data, + }); + return data; } export interface PrivateSquareStatus { @@ -86,7 +102,14 @@ export async function loadPrivateSquareStatus(): Promise { `Failed to load private square settings: ${response.status}`, ); } - return (await response.json()) as PrivateSquareStatus; + const data = (await response.json()) as PrivateSquareStatus; + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-系统设置-私密广场查看", + url: "/api/system-settings/private-square", + returnParam: data, + }); + return data; } export interface PublishSquare { @@ -112,7 +135,14 @@ export async function loadPublishSquares(): Promise { if (!response.ok) { throw new Error(`Failed to load publish squares: ${response.status}`); } - return (await response.json()) as PublishSquaresSettings; + const data = (await response.json()) as PublishSquaresSettings; + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-系统设置-发布广场查看", + url: "/api/system-settings/publish-squares", + returnParam: data, + }); + return data; } export async function updatePublishSquares( @@ -132,7 +162,15 @@ export async function updatePublishSquares( .catch(() => ({ detail: "Failed to update publish squares" })); throw new Error(detail.detail ?? "Failed to update publish squares"); } - return (await response.json()) as PublishSquaresSettings; + const data = (await response.json()) as PublishSquaresSettings; + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-系统设置-发布广场编辑", + url: "/api/system-settings/publish-squares", + reqParam: body, + returnParam: data, + }); + return data; } export type ThemeMode = "light" | "dark" | "lightblue"; @@ -162,7 +200,14 @@ export async function loadAppearanceSettings(): Promise { `Failed to load appearance settings: ${response.status}`, ); } - return (await response.json()) as AppearanceSettings; + const data = (await response.json()) as AppearanceSettings; + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-系统设置-外观查看", + url: "/api/system-settings/appearance", + returnParam: data, + }); + return data; } export async function updateAppearanceSettings( @@ -182,7 +227,15 @@ export async function updateAppearanceSettings( .catch(() => ({ detail: "Failed to update appearance" })); throw new Error(detail.detail ?? "Failed to update appearance"); } - return (await response.json()) as AppearanceSettings; + const data = (await response.json()) as AppearanceSettings; + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-系统设置-外观编辑", + url: "/api/system-settings/appearance", + reqParam: body, + returnParam: data, + }); + return data; } export async function loadCitationDisplaySettings(): Promise { @@ -194,7 +247,14 @@ export async function loadCitationDisplaySettings(): Promise { @@ -204,7 +264,14 @@ export async function loadLeaderboardSettings(): Promise { if (!response.ok) { throw new Error(`Failed to load leaderboard settings: ${response.status}`); } - return (await response.json()) as LeaderboardSettings; + const data = (await response.json()) as LeaderboardSettings; + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-系统设置-排行榜查看", + url: "/api/system-settings/leaderboard", + returnParam: data, + }); + return data; } export async function updateLeaderboardSettings( @@ -224,7 +291,15 @@ export async function updateLeaderboardSettings( .catch(() => ({ detail: "Failed to update leaderboard settings" })); throw new Error(detail.detail ?? "Failed to update leaderboard settings"); } - return (await response.json()) as LeaderboardSettings; + const data = (await response.json()) as LeaderboardSettings; + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-系统设置-排行榜编辑", + url: "/api/system-settings/leaderboard", + reqParam: body, + returnParam: data, + }); + return data; } export async function updateCitationDisplaySettings( @@ -244,7 +319,15 @@ export async function updateCitationDisplaySettings( .catch(() => ({ detail: "Failed to update citation display" })); throw new Error(detail.detail ?? "Failed to update citation display"); } - return (await response.json()) as CitationDisplaySettings; + const data = (await response.json()) as CitationDisplaySettings; + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-系统设置-引用展示编辑", + url: "/api/system-settings/citation-display", + reqParam: body, + returnParam: data, + }); + return data; } export interface SkillCompressionSettings { @@ -268,7 +351,14 @@ export async function loadSkillCompressionSettings(): Promise ({ detail: "Failed to update skill compression" })); throw new Error(detail.detail ?? "Failed to update skill compression"); } - return (await response.json()) as SkillCompressionSettings; + const data = (await response.json()) as SkillCompressionSettings; + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-系统设置-技能压缩编辑", + url: "/api/system-settings/skill-compression", + reqParam: body, + returnParam: data, + }); + return data; } /** 「按类型聚合」模块(zzJC / zzXD)选中的智能体标签配置(需求 0616 #3/#4)。 */ @@ -306,7 +404,15 @@ export async function loadAgentModuleConfig( if (!response.ok) { throw new Error(`Failed to load agent module config: ${response.status}`); } - return (await response.json()) as AgentModuleConfig; + const data = (await response.json()) as AgentModuleConfig; + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-系统设置-智能体模块查看", + url: `/api/system-settings/agent-modules/${key}`, + reqParam: { key }, + returnParam: data, + }); + return data; } export async function updateAgentModuleConfig( @@ -327,7 +433,15 @@ export async function updateAgentModuleConfig( .catch(() => ({ detail: "Failed to update agent module config" })); throw new Error(detail.detail ?? "Failed to update agent module config"); } - return (await response.json()) as AgentModuleConfig; + const data = (await response.json()) as AgentModuleConfig; + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-系统设置-智能体模块编辑", + url: `/api/system-settings/agent-modules/${key}`, + reqParam: { key, tag_ids: tagIds }, + returnParam: data, + }); + return data; } export interface LogSystemSettings { @@ -343,7 +457,14 @@ export async function loadLogSystemSettings(): Promise { if (!response.ok) { throw new Error(`Failed to load log system settings: ${response.status}`); } - return (await response.json()) as LogSystemSettings; + const data = (await response.json()) as LogSystemSettings; + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-系统设置-日志系统查看", + url: "/api/system-settings/log-system", + returnParam: data, + }); + return data; } export async function updatePrivateSquareSettings( @@ -363,5 +484,14 @@ export async function updatePrivateSquareSettings( .catch(() => ({ detail: "Failed to update private square" })); throw new Error(detail.detail ?? "Failed to update private square"); } - return (await response.json()) as PrivateSquareStatus; + const data = (await response.json()) as PrivateSquareStatus; + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-系统设置-私密广场编辑", + url: "/api/system-settings/private-square", + // omit `password` (secret) from the logged request params + reqParam: { enabled: body.enabled, password_set: body.password != null }, + returnParam: data, + }); + return data; } diff --git a/frontend-web/src/core/tags/api.ts b/frontend-web/src/core/tags/api.ts index 085d8c09..42d7d8d7 100644 --- a/frontend-web/src/core/tags/api.ts +++ b/frontend-web/src/core/tags/api.ts @@ -1,5 +1,6 @@ import { apiFetch } from "@/core/api"; import { getBackendBaseURL } from "@/core/config"; +import { OptType, writeYLog } from "@/core/log/y-log"; import type { Tag, @@ -22,6 +23,13 @@ export async function listTags(tagType?: TagType): Promise { const res = await apiFetch(`${base()}${query}`); if (!res.ok) throw await parseDetail(res, "Failed to list tags"); const data = (await res.json()) as TagListResponse; + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-标签-查询", + url: `/api/tags${query}`, + reqParam: { tagType }, + returnParam: data.tags, + }); return data.tags; } @@ -32,7 +40,15 @@ export async function createTag(input: TagCreateInput): Promise { body: JSON.stringify(input), }); if (!res.ok) throw await parseDetail(res, "Failed to create tag"); - return (await res.json()) as Tag; + const created = (await res.json()) as Tag; + writeYLog({ + optType: OptType.create, + optDetail: "B1-CM助手-标签-新增", + url: "/api/tags", + reqParam: input, + returnParam: created, + }); + return created; } export async function updateTag(id: string, input: TagUpdateInput): Promise { @@ -42,7 +58,15 @@ export async function updateTag(id: string, input: TagUpdateInput): Promise body: JSON.stringify(input), }); if (!res.ok) throw await parseDetail(res, "Failed to update tag"); - return (await res.json()) as Tag; + const updated = (await res.json()) as Tag; + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-标签-编辑", + url: `/api/tags/${id}`, + reqParam: input, + returnParam: updated, + }); + return updated; } export async function deleteTag(id: string): Promise { @@ -50,6 +74,12 @@ export async function deleteTag(id: string): Promise { method: "DELETE", }); if (!res.ok) throw await parseDetail(res, "Failed to delete tag"); + writeYLog({ + optType: OptType.delete, + optDetail: "B1-CM助手-标签-删除", + url: `/api/tags/${id}`, + reqParam: { id }, + }); } export async function getResourceTags( @@ -61,6 +91,13 @@ export async function getResourceTags( ); if (!res.ok) throw await parseDetail(res, "Failed to load resource tags"); const data = (await res.json()) as TagListResponse; + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-标签-资源标签查看", + url: `/api/tags/assignments/${targetType}/${targetId}`, + reqParam: { targetType, targetId }, + returnParam: data.tags, + }); return data.tags; } @@ -79,5 +116,12 @@ export async function setResourceTags( ); if (!res.ok) throw await parseDetail(res, "Failed to update resource tags"); const data = (await res.json()) as TagListResponse; + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-标签-资源标签设置", + url: `/api/tags/assignments/${targetType}/${targetId}`, + reqParam: { targetType, targetId, tagIds }, + returnParam: data.tags, + }); return data.tags; } diff --git a/frontend-web/src/core/thread-shares/api.ts b/frontend-web/src/core/thread-shares/api.ts index 2736fb2b..fa6a7517 100644 --- a/frontend-web/src/core/thread-shares/api.ts +++ b/frontend-web/src/core/thread-shares/api.ts @@ -2,6 +2,7 @@ import type { Message } from "@langchain/langgraph-sdk"; import { apiFetch } from "@/core/api"; import { getBackendBaseURL } from "@/core/config"; +import { OptType, writeYLog } from "@/core/log/y-log"; /** * Share modes: @@ -65,7 +66,15 @@ export async function createShare( body: JSON.stringify({ thread_id: threadId, mode }), }); if (!response.ok) throw await parseError(response, "创建分享失败"); - return response.json() as Promise; + const result = (await response.json()) as ThreadShare; + writeYLog({ + optType: OptType.create, + optDetail: "B1-CM助手-会话分享-创建分享", + url: baseUrl(), + reqParam: { thread_id: threadId, mode }, + returnParam: { share_code: result.share_code, mode: result.mode }, + }); + return result; } /** List the current user's own share links. */ @@ -73,34 +82,57 @@ export async function listShares(): Promise { const response = await apiFetch(baseUrl()); if (!response.ok) throw await parseError(response, "加载分享列表失败"); const json = (await response.json()) as { shares: ThreadShare[] }; + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-会话分享-查询分享列表", + url: baseUrl(), + returnParam: { count: json.shares.length }, + }); return json.shares; } /** Revoke a share link so it can no longer be previewed or imported. */ export async function revokeShare(shareCode: string): Promise { - const response = await apiFetch(`${baseUrl()}/${encodeURIComponent(shareCode)}`, { - method: "DELETE", - }); + const url = `${baseUrl()}/${encodeURIComponent(shareCode)}`; + const response = await apiFetch(url, { method: "DELETE" }); if (!response.ok) throw await parseError(response, "撤销分享失败"); + writeYLog({ + optType: OptType.delete, + optDetail: "B1-CM助手-会话分享-撤销分享", + url, + reqParam: { shareCode }, + }); } /** Preview a share link before importing it. */ export async function previewShare(shareCode: string): Promise { - const response = await apiFetch( - `${baseUrl()}/${encodeURIComponent(shareCode)}/preview`, - ); + const url = `${baseUrl()}/${encodeURIComponent(shareCode)}/preview`; + const response = await apiFetch(url); if (!response.ok) throw await parseError(response, "无法读取该分享"); - return response.json() as Promise; + const result = (await response.json()) as SharePreview; + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-会话分享-预览分享", + url, + reqParam: { shareCode }, + }); + return result; } /** Import every conversation behind a share link into the current account. */ export async function importShare(shareCode: string): Promise { - const response = await apiFetch( - `${baseUrl()}/${encodeURIComponent(shareCode)}/import`, - { method: "POST" }, - ); + const url = `${baseUrl()}/${encodeURIComponent(shareCode)}/import`; + const response = await apiFetch(url, { method: "POST" }); if (!response.ok) throw await parseError(response, "导入失败"); - return response.json() as Promise; + const result = (await response.json()) as ImportResult; + writeYLog({ + optType: OptType.import, + optDetail: "B1-CM助手-会话分享-导入分享", + url, + reqParam: { shareCode }, + returnParam: result, + }); + return result; } /** diff --git a/frontend-web/src/core/threads/hooks.ts b/frontend-web/src/core/threads/hooks.ts index a84e1c32..970bd903 100644 --- a/frontend-web/src/core/threads/hooks.ts +++ b/frontend-web/src/core/threads/hooks.ts @@ -15,6 +15,7 @@ import { getStoredAuth } from "../auth"; import { rememberAskTime } from "../chat-timestamps"; import { getBackendBaseURL, getLangGraphBaseURL } from "../config"; import { useI18n } from "../i18n/hooks"; +import { OptType, writeYLog } from "../log/y-log"; import type { FileInMessage } from "../messages/utils"; import type { LocalSettings } from "../settings"; import { isThreadRagModeLocked, lockThreadRagMode, useLocalSettings } from "../settings"; @@ -231,6 +232,84 @@ function buildCurrentUserLabel(): string { return userId ? userId : "未知用户"; } +/** + * 从 `on_chat_model_end` 事件的 output 里尽力取出模型输出文本。 + * + * output 形态可能是 AIMessage(`{ content: string | parts[] }`),也可能是 + * LangChain 的 `{ generations: [[{ text, message }]] }`。全部容错,取不到返回空串。 + */ +function textFromChatModelOutput(output: unknown): string { + if (output == null) return ""; + if (typeof output === "string") return output; + if (typeof output !== "object") return String(output); + const o = output as Record; + if ("content" in o) { + const c = o.content; + if (typeof c === "string") return c; + if (Array.isArray(c)) { + const parts = c + .map((p) => + p && typeof p === "object" && "text" in p + ? String((p as { text?: unknown }).text ?? "") + : "", + ) + .filter(Boolean); + if (parts.length) return parts.join(""); + } + } + if (Array.isArray(o.generations)) { + const texts: string[] = []; + const collect = (gen: unknown) => { + if (Array.isArray(gen)) { + gen.forEach(collect); + return; + } + if (gen && typeof gen === "object") { + const gg = gen as Record; + if (typeof gg.text === "string" && gg.text) texts.push(gg.text); + else if (gg.message) { + const t = textFromChatModelOutput(gg.message); + if (t) texts.push(t); + } + } + }; + collect(o.generations); + if (texts.length) return texts.join(""); + } + return ""; +} + +/** + * 从 `on_chat_model_*` 事件的 input 里尽力取出最后一条 user/human 文本, + * 作为本次大模型调用的「问题」。input 可能是 `{ messages: BaseMessage[] | BaseMessage[][] }`。 + */ +function lastHumanFromChatModelInput(input: unknown): string { + try { + const raw = + input && typeof input === "object" && "messages" in input + ? (input as { messages?: unknown }).messages + : input; + const flat: unknown[] = []; + const push = (m: unknown) => { + if (Array.isArray(m)) m.forEach(push); + else if (m != null) flat.push(m); + }; + push(raw); + for (let i = flat.length - 1; i >= 0; i--) { + const m = flat[i]; + if (!m || typeof m !== "object") continue; + const type = (m as Record).type ?? (m as Record).role; + if (type === "human" || type === "user") { + const text = textFromChatModelOutput(m); + if (text) return text; + } + } + } catch { + /* 取不到忽略 */ + } + return ""; +} + export function useThreadStream({ threadId, context, @@ -333,6 +412,28 @@ export function useThreadStream({ data: event.data, }); } + // 操作日志(Y-Log):每一次大模型调用结束都各上报一条(一次 LLM 调用 = 一条), + // 让问答日志尽量「多、全」——一个问答轮次里 lead agent / 子代理的每次模型调用 + // 都会落一条。门控与失败静默都在 writeYLog 内部处理,绝不影响问答。 + if (event.event === "on_chat_model_end") { + try { + const data = (event.data ?? {}) as { + input?: unknown; + output?: unknown; + }; + const answer = textFromChatModelOutput(data.output); + const question = lastHumanFromChatModelInput(data.input); + writeYLog({ + optType: OptType.other, + optDetail: "B1-CM助手-智能问答", + url: `/api/threads/${threadIdRef.current ?? ""}/runs/stream`, + reqParam: { model: event.name ?? "", question }, + returnParam: { data: answer }, + }); + } catch { + /* 上报绝不影响问答 */ + } + } }, onUpdateEvent(data) { const updates: Array | null> = Object.values( diff --git a/frontend-web/src/core/tool-metrics/api.ts b/frontend-web/src/core/tool-metrics/api.ts index 3814e940..457905d6 100644 --- a/frontend-web/src/core/tool-metrics/api.ts +++ b/frontend-web/src/core/tool-metrics/api.ts @@ -1,5 +1,6 @@ import { apiFetch } from "@/core/api"; import { getBackendBaseURL } from "@/core/config"; +import { OptType, writeYLog } from "@/core/log/y-log"; /** Coarse failure buckets, mirrored from the backend classifier. */ export const TOOL_ERROR_CATEGORIES = [ @@ -98,39 +99,58 @@ function buildQuery(filters: ToolMetricsListQuery): string { export async function loadToolMetrics( query: ToolMetricsListQuery, ): Promise { - const response = await apiFetch( - `${getBackendBaseURL()}/api/admin/tool-metrics${buildQuery(query)}`, - ); + const url = `${getBackendBaseURL()}/api/admin/tool-metrics${buildQuery(query)}`; + const response = await apiFetch(url); if (!response.ok) { const detail = await response.json().catch(() => ({ detail: response.statusText })); throw new Error(detail.detail ?? "Failed to load tool metrics"); } - return (await response.json()) as ToolMetricsListResponse; + const result = (await response.json()) as ToolMetricsListResponse; + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-工具指标-查询调用列表", + url, + reqParam: query, + returnParam: { total: result.total, count: result.items.length }, + }); + return result; } export async function loadToolMetricsSummary( filters: ToolMetricsFilters, ): Promise { - const response = await apiFetch( - `${getBackendBaseURL()}/api/admin/tool-metrics/summary${buildQuery(filters)}`, - ); + const url = `${getBackendBaseURL()}/api/admin/tool-metrics/summary${buildQuery(filters)}`; + const response = await apiFetch(url); if (!response.ok) { const detail = await response.json().catch(() => ({ detail: response.statusText })); throw new Error(detail.detail ?? "Failed to load tool metrics summary"); } - return (await response.json()) as ToolMetricsSummary; + const result = (await response.json()) as ToolMetricsSummary; + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-工具指标-查询汇总统计", + url, + reqParam: filters, + returnParam: { total: result.total, error_total: result.error_total }, + }); + return result; } export async function downloadToolMetricsExcel( filters: ToolMetricsFilters, ): Promise { - const response = await apiFetch( - `${getBackendBaseURL()}/api/admin/tool-metrics/export${buildQuery(filters)}`, - ); + const requestUrl = `${getBackendBaseURL()}/api/admin/tool-metrics/export${buildQuery(filters)}`; + const response = await apiFetch(requestUrl); if (!response.ok) { const detail = await response.json().catch(() => ({ detail: response.statusText })); throw new Error(detail.detail ?? "Failed to export tool metrics"); } + writeYLog({ + optType: OptType.export, + optDetail: "B1-CM助手-工具指标-导出Excel", + url: requestUrl, + reqParam: filters, + }); const blob = await response.blob(); const url = URL.createObjectURL(blob); const filename = (() => { diff --git a/frontend-web/src/core/user-preferences/index.ts b/frontend-web/src/core/user-preferences/index.ts index 3d2d31c9..7a6a4774 100644 --- a/frontend-web/src/core/user-preferences/index.ts +++ b/frontend-web/src/core/user-preferences/index.ts @@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { apiFetch } from "@/core/api"; import { getBackendBaseURL } from "@/core/config"; +import { OptType, writeYLog } from "@/core/log/y-log"; /** Per-user UI preferences, bound to the account (stored server-side). */ export interface UserPreferences { @@ -36,7 +37,15 @@ async function updateUserPreferences( throw new Error(err.detail ?? `Failed to update preferences: ${res.statusText}`); } const data = (await res.json()) as Partial; - return { hide_admin_prompt_prefix: !!data.hide_admin_prompt_prefix }; + const result = { hide_admin_prompt_prefix: !!data.hide_admin_prompt_prefix }; + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-用户偏好-编辑", + url: "/api/user-preferences", + reqParam: input, + returnParam: result, + }); + return result; } /** Reactive current-user preferences (server-backed, follows the account). */ diff --git a/frontend-web/src/core/user-qa/api.ts b/frontend-web/src/core/user-qa/api.ts index 1ce9bc93..754dbd7f 100644 --- a/frontend-web/src/core/user-qa/api.ts +++ b/frontend-web/src/core/user-qa/api.ts @@ -1,5 +1,6 @@ import { apiFetch } from "@/core/api"; import { getBackendBaseURL } from "@/core/config"; +import { OptType, writeYLog } from "@/core/log/y-log"; export interface UserQaItem { run_id: string; @@ -40,8 +41,14 @@ export async function listMyQa( }); const query = (params.q ?? "").trim(); if (query) search.set("q", query); - return parse( - await apiFetch(`${getBackendBaseURL()}/api/users/me/qa?${search}`), - "Failed to load my Q&A", - ); + const url = `${getBackendBaseURL()}/api/users/me/qa?${search}`; + const result = await parse(await apiFetch(url), "Failed to load my Q&A"); + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-用户问答-查询我的问答", + url, + reqParam: params, + returnParam: { total: result.total, count: result.items.length }, + }); + return result; } diff --git a/frontend-web/src/roundtable-planning/api/chains.ts b/frontend-web/src/roundtable-planning/api/chains.ts index b57bf26d..ae4c3b33 100644 --- a/frontend-web/src/roundtable-planning/api/chains.ts +++ b/frontend-web/src/roundtable-planning/api/chains.ts @@ -12,6 +12,7 @@ import { apiFetch } from "@/core/api/fetch-client"; import { getBackendBaseURL } from "@/core/config"; +import { OptType, writeYLog } from "@/core/log/y-log"; import type { BusinessChain, ChainSeat } from "@/roundtable-planning/lib/business-chain"; import type { SeatMode } from "@/roundtable-planning/lib/seat-mode"; @@ -154,7 +155,15 @@ export async function listChains(scope: ChainScope = "mine"): Promise(res, "list chains"); - return (data.chains ?? []).map(toChain); + const chains = (data.chains ?? []).map(toChain); + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-业务链条-查询", + url, + reqParam: { scope }, + returnParam: { count: chains.length }, + }); + return chains; } /** 发布 / 取消发布一条链条(仅 owner 生效)。返回更新后的链条,404 返回 null。 */ @@ -167,7 +176,15 @@ export async function getChain(id: string): Promise { const res = await apiFetch(`${BASE()}/${encodeURIComponent(id)}`, { method: "GET" }); if (res.status === 404) return null; const data = await readJson(res, "get chain"); - return toChain(data); + const chain = toChain(data); + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-业务链条-查看", + url: `${BASE()}/${id}`, + reqParam: { id }, + returnParam: { id: chain.id, title: chain.title }, + }); + return chain; } export async function createChain(payload: ChainCreatePayload): Promise { @@ -177,7 +194,15 @@ export async function createChain(payload: ChainCreatePayload): Promise(res, "create chain"); - return toChain(data); + const chain = toChain(data); + writeYLog({ + optType: OptType.create, + optDetail: "B1-CM助手-业务链条-新建", + url: BASE(), + reqParam: { title: payload.title, seats: payload.seats?.length ?? 0 }, + returnParam: { id: chain.id, title: chain.title }, + }); + return chain; } export async function updateChain(id: string, patch: ChainUpdatePayload): Promise { @@ -188,7 +213,20 @@ export async function updateChain(id: string, patch: ChainUpdatePayload): Promis }); if (res.status === 404) return null; const data = await readJson(res, "update chain"); - return toChain(data); + const chain = toChain(data); + writeYLog({ + optType: patch.isPublic === true ? OptType.enabled : patch.isPublic === false ? OptType.disabled : OptType.edit, + optDetail: + patch.isPublic === true + ? "B1-CM助手-业务链条-发布" + : patch.isPublic === false + ? "B1-CM助手-业务链条-取消发布" + : "B1-CM助手-业务链条-编辑", + url: `${BASE()}/${id}`, + reqParam: { id, ...patch }, + returnParam: { id: chain.id, title: chain.title }, + }); + return chain; } export async function deleteChain(id: string): Promise { @@ -197,6 +235,12 @@ export async function deleteChain(id: string): Promise { const text = await res.text().catch(() => ""); throw new Error(`delete chain failed (${res.status}): ${text}`); } + writeYLog({ + optType: OptType.delete, + optDetail: "B1-CM助手-业务链条-删除", + url: `${BASE()}/${id}`, + reqParam: { id }, + }); } // ── 提示词优化 ─────────────────────────────────────────────────────────────── diff --git a/frontend-web/src/roundtable-planning/api/dashboard-sessions.ts b/frontend-web/src/roundtable-planning/api/dashboard-sessions.ts index d7ddd688..afbbaf48 100644 --- a/frontend-web/src/roundtable-planning/api/dashboard-sessions.ts +++ b/frontend-web/src/roundtable-planning/api/dashboard-sessions.ts @@ -7,6 +7,7 @@ */ import { apiFetch } from "@/core/api/fetch-client"; import { getBackendBaseURL } from "@/core/config"; +import { OptType, writeYLog } from "@/core/log/y-log"; export interface DashboardSession { taskId: string; @@ -44,7 +45,15 @@ export async function getDashboardSessionByTask(taskId: string): Promise ""); throw new Error(`get dashboard session failed (${res.status}): ${text}`); } - return toClient((await res.json()) as ServerDashboardSession); + const session = toClient((await res.json()) as ServerDashboardSession); + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-大屏会话-查看", + url: `${BASE()}/by-task/${taskId}`, + reqParam: { taskId }, + returnParam: { taskId: session.taskId, updatedAt: session.updatedAt }, + }); + return session; } /** upsert 该 taskId 的会话(只传需要更新的字段)。 */ @@ -64,5 +73,13 @@ export async function putDashboardSessionByTask( const text = await res.text().catch(() => ""); throw new Error(`put dashboard session failed (${res.status}): ${text}`); } - return toClient((await res.json()) as ServerDashboardSession); + const session = toClient((await res.json()) as ServerDashboardSession); + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-大屏会话-保存", + url: `${BASE()}/by-task/${taskId}`, + reqParam: { taskId, hasTranscript: patch.transcript !== undefined, hasReportJson: patch.reportJson !== undefined }, + returnParam: { taskId: session.taskId, updatedAt: session.updatedAt }, + }); + return session; } diff --git a/frontend-web/src/roundtable-planning/api/drafts.ts b/frontend-web/src/roundtable-planning/api/drafts.ts index 3448daf4..98451f41 100644 --- a/frontend-web/src/roundtable-planning/api/drafts.ts +++ b/frontend-web/src/roundtable-planning/api/drafts.ts @@ -337,7 +337,15 @@ export async function getDraft(id: string): Promise { const res = await apiFetch(`${BASE()}/${encodeURIComponent(id)}`, { method: "GET" }); if (res.status === 404) return null; const data = await readJson(res, "get draft"); - return toRecord(data); + const record = toRecord(data); + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-会商-查看草稿", + url: `${BASE()}/${id}`, + reqParam: { id }, + returnParam: { id: record.id, title: record.title }, + }); + return record; } export async function createDraft(payload: DraftCreatePayload): Promise { diff --git a/frontend-web/src/roundtable-planning/api/multi-agent.ts b/frontend-web/src/roundtable-planning/api/multi-agent.ts index b1634be3..0c0d2ed2 100644 --- a/frontend-web/src/roundtable-planning/api/multi-agent.ts +++ b/frontend-web/src/roundtable-planning/api/multi-agent.ts @@ -1,5 +1,6 @@ import { apiFetch } from "@/core/api/fetch-client"; import { getBackendBaseURL } from "@/core/config"; +import { OptType, writeYLog } from "@/core/log/y-log"; import { resolveAllowMultiple } from "@/roundtable-planning/lib/clarification"; import { reportRoundtableError, stageForRoundtableAgent } from "@/roundtable-planning/api/diagnostics-report"; @@ -921,7 +922,7 @@ export async function listSessionArtifacts( modified_at: string; }>; }; - return { + const result = { threadId: json.thread_id, files: (json.files ?? []).map((f) => ({ path: f.path, @@ -931,6 +932,14 @@ export async function listSessionArtifacts( modifiedAt: f.modified_at, })), }; + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-会商产物-查询", + url: `/api/threads/${threadId}/artifacts`, + reqParam: { threadId }, + returnParam: { count: result.files.length }, + }); + return result; } /** diff --git a/frontend-web/src/roundtable-planning/api/recommend.ts b/frontend-web/src/roundtable-planning/api/recommend.ts index 1df1b859..e16ad8dd 100644 --- a/frontend-web/src/roundtable-planning/api/recommend.ts +++ b/frontend-web/src/roundtable-planning/api/recommend.ts @@ -1,5 +1,6 @@ import { apiFetch } from "@/core/api/fetch-client"; import { getBackendBaseURL } from "@/core/config"; +import { OptType, writeYLog } from "@/core/log/y-log"; import type { Agent } from "@/core/agents"; import type { IntentSummary } from "./intent"; @@ -268,7 +269,15 @@ export async function listRecommendHistory(draftId: string): Promise ""); throw new Error(`save recommend history failed (${res.status}): ${text}`); } - return toHistoryItem((await res.json()) as ServerHistoryItem); + const item = toHistoryItem((await res.json()) as ServerHistoryItem); + writeYLog({ + optType: OptType.create, + optDetail: "B1-CM助手-席位推荐-保存历史", + url: `/api/roundtable-drafts/${draftId}/recommendations`, + reqParam: { draftId, status: payload.status, picks: payload.picks?.length ?? 0 }, + returnParam: { id: item.id }, + }); + return item; } /** diff --git a/frontend-web/src/roundtable-planning/api/roundtable-jobs.ts b/frontend-web/src/roundtable-planning/api/roundtable-jobs.ts index 61226f70..027a948c 100644 --- a/frontend-web/src/roundtable-planning/api/roundtable-jobs.ts +++ b/frontend-web/src/roundtable-planning/api/roundtable-jobs.ts @@ -189,7 +189,15 @@ export async function resumeJob(jobId: string, answer: string, taskId?: string | headers: { "Content-Type": "application/json" }, body: JSON.stringify({ answer }), }); - return mapJob(await readJson>(res, "resume job")); + const job = mapJob(await readJson>(res, "resume job")); + writeYLog({ + optType: OptType.other, + optDetail: "B1-CM助手-会商-回答澄清续跑", + url: `${BASE()}/${jobId}/resume`, + reqParam: { jobId, taskId: taskId ?? null }, + returnParam: { id: job.id, status: job.status }, + }); + return job; } /** 取消作业。`taskId` 非空 → task 共享取消(不分权)。 */ diff --git a/frontend-web/src/roundtable-planning/api/task-drafts.ts b/frontend-web/src/roundtable-planning/api/task-drafts.ts index 7eca3882..12963ffc 100644 --- a/frontend-web/src/roundtable-planning/api/task-drafts.ts +++ b/frontend-web/src/roundtable-planning/api/task-drafts.ts @@ -50,7 +50,15 @@ export async function getLatestTaskDraftByTask(taskId: string): Promise(res, "get latest task draft"); - return toRecord(data); + const record = toRecord(data); + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-会商任务-查看最新草稿", + url: `${BASE()}/by-task/${taskId}/latest`, + reqParam: { taskId }, + returnParam: { id: record.id, title: record.title }, + }); + return record; } /** Full task draft by id (不分权). Returns null on 404. */ @@ -58,7 +66,15 @@ export async function getTaskDraft(id: string): Promise { const res = await apiFetch(`${BASE()}/${encodeURIComponent(id)}`, { method: "GET" }); if (res.status === 404) return null; const data = await readJson(res, "get task draft"); - return toRecord(data); + const record = toRecord(data); + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-会商任务-查看草稿", + url: `${BASE()}/${id}`, + reqParam: { id }, + returnParam: { id: record.id, title: record.title }, + }); + return record; } /** Create a task draft. `payload.taskId` is required. */ diff --git a/frontend-web/src/strategy-components/api/business-mapping.ts b/frontend-web/src/strategy-components/api/business-mapping.ts index 858e0a1c..9f90420e 100644 --- a/frontend-web/src/strategy-components/api/business-mapping.ts +++ b/frontend-web/src/strategy-components/api/business-mapping.ts @@ -8,6 +8,7 @@ import { apiFetch } from "@/core/api/fetch-client"; import { getBackendBaseURL } from "@/core/config"; +import { OptType, writeYLog } from "@/core/log/y-log"; export interface BusinessMapping { businessCode: string; @@ -63,7 +64,14 @@ async function readJson(res: Response, ctx: string): Promise { export async function listBusinessMappings(): Promise { const res = await apiFetch(BASE(), { method: "GET" }); const data = await readJson<{ mappings: ServerMapping[] }>(res, "list business mappings"); - return (data.mappings ?? []).map(toMapping); + const mappings = (data.mappings ?? []).map(toMapping); + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-业务映射-列表", + url: "/api/business-mapping", + returnParam: mappings, + }); + return mappings; } /** 更新某业务行的智能体 / 业务链选择。404 返回 null。 */ @@ -83,5 +91,13 @@ export async function updateBusinessMapping( }); if (res.status === 404) return null; const data = await readJson(res, "update business mapping"); - return toMapping(data); + const mapping = toMapping(data); + writeYLog({ + optType: OptType.edit, + optDetail: "B1-CM助手-业务映射-修改", + url: `${BASE()}/${encodeURIComponent(businessCode)}`, + reqParam: { businessCode, patch }, + returnParam: mapping, + }); + return mapping; } diff --git a/frontend-web/src/strategy-components/api/light-apps.ts b/frontend-web/src/strategy-components/api/light-apps.ts index b2a6632a..cce7f4bc 100644 --- a/frontend-web/src/strategy-components/api/light-apps.ts +++ b/frontend-web/src/strategy-components/api/light-apps.ts @@ -170,7 +170,15 @@ export async function listLightApps(params: ListLightAppsParams = {}): Promise { const res = await apiFetch(`${BASE()}/${encodeURIComponent(appId)}`, { method: "GET" }); - return toApp(await readJson(res, "get light app")); + const app = toApp(await readJson(res, "get light app")); + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-轻应用-查看", + url: `${BASE()}/${encodeURIComponent(appId)}`, + reqParam: { appId }, + returnParam: app, + }); + return app; } export async function createLightApp(input: LightAppInput): Promise { diff --git a/frontend-web/src/strategy-components/api/skill-addresses.ts b/frontend-web/src/strategy-components/api/skill-addresses.ts index 1c8b2143..bec36773 100644 --- a/frontend-web/src/strategy-components/api/skill-addresses.ts +++ b/frontend-web/src/strategy-components/api/skill-addresses.ts @@ -242,7 +242,15 @@ export async function previewReplacements(replacements: Replacement[]): Promise< headers: { "Content-Type": "application/json" }, body: JSON.stringify({ replacements }), }); - return toPreviewResult(await readJson(res, "预览")); + const result = toPreviewResult(await readJson(res, "预览")); + writeYLog({ + optType: OptType.view, + optDetail: "B1-CM助手-技能地址管理-预览", + url: `${BASE()}/preview`, + reqParam: { replacements }, + returnParam: result, + }); + return result; } /** 流式应用:逐文件上报进度,返回最终结果。`expectedTotal` 做乐观锁。 */ @@ -280,13 +288,20 @@ export async function applyReplacementsStream( export async function listBackups(): Promise { const res = await apiFetch(`${BASE()}/backups`, { method: "GET" }); const data = await readJson<{ backups: any[] }>(res, "读取备份"); - return (data.backups ?? []).map((b) => ({ + const backups = (data.backups ?? []).map((b) => ({ backupId: b.backup_id, files: b.files ?? 0, createdAt: b.created_at ?? "", replacedTotal: b.replaced_total ?? 0, replacements: (b.replacements ?? []).map((r: any) => ({ from: r.from, to: r.to, kind: r.kind })), })); + writeYLog({ + optType: OptType.search, + optDetail: "B1-CM助手-技能地址管理-修改记录", + url: `${BASE()}/backups`, + returnParam: backups, + }); + return backups; } export async function rollbackBackup(backupId: string): Promise<{ restored: number }> {