Merge branch 'ji' into master

并入: loop 跨事件循环 DB 访问修复(asyncmy attached to a different loop)、y-log 前缀、全角参考文献角标识别

# Conflicts:
#	CLAUDE.md
This commit is contained in:
吉鑫升 committed 2026-06-24 17:13:45 +08:00
commit 1a87077642
54 files changed
+583 -400

No files matched your search

+1 -1
View File
@@ -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/<business-path>/{roundtable,agent/:agent_id/chats}` in `StrategyRoutes.tsx` (`<business-path>` = `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 staLine truncated
- **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`。(2.5) **`?hideChrome=1`** — **仅**去全局 chrome(Header 导航栏 + PageSidebar 侧边栏),**不**强制主题、**不**隔离鉴权(区别于 `embed`:只想要一个干净全屏页面,如「大屏绘制」`/page/strategy/dashboard-agent?taskId=…&hideChrome=1`)。新增 `EmbedSession.hideChrome` 字段 + `readHideChromeParam()` + `isChromeHidden()`(取 `embed || hideChrome` 之并),与 `hideJump` 完全对称;`PageRoutes.tsx` 的 `PageLayout` 把 `chromeHidden`(`isChromeHidden() || readHideChromeParam(location.search)==="on"`)并入 `pageSidebarAllowed`,且其**优先级高于** dashboard-agent 路由原本的强制全局 chrome(`forceChromeForDashboardAgent`)。`hideChrome=0` 可显式关闭。(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 的成功路径——原则是「一个用户操作(查看/查询/新增/编辑/删除/启停/导入导出)= 一条日志」,每个 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`。
- **操作日志(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` 统一「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
@@ -45,7 +45,7 @@ export function AccountSettingsPage() {
// 操作日志(Y-Log):在清除 token1 之前上报登出(此时 token1 还在),keepalive 保证发出。
writeYLog({
optType: OptType.logout,
optDetail: "B1-CM助手-登出",
optDetail: "CM助手-全量-登出",
url: "/api/v1/auth/logout",
keepalive: true,
});
+16 -16
View File
@@ -177,7 +177,7 @@ export async function listUserThreads(userId: string): Promise<AdminThread[]> {
const result = await parse<AdminThread[]>(await apiFetch(url), "Failed to load threads");
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-管理后台-查询用户会话列表",
optDetail: "CM助手-全量-管理后台-查询用户会话列表",
url,
reqParam: { userId },
returnParam: { count: result.length },
@@ -193,7 +193,7 @@ export async function listUserThreadMessages(
const result = await parse<AdminMessage[]>(await apiFetch(url), "Failed to load messages");
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-管理后台-查看会话消息",
optDetail: "CM助手-全量-管理后台-查看会话消息",
url,
reqParam: { userId, threadId },
returnParam: { count: result.length },
@@ -215,7 +215,7 @@ export async function listUserQa(
const result = await parse<AdminUserQaPageResponse>(await apiFetch(url), "Failed to load user Q&A");
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-管理后台-查询用户问答",
optDetail: "CM助手-全量-管理后台-查询用户问答",
url,
reqParam: { userId, ...params },
returnParam: { total: result.total, count: result.items.length },
@@ -230,7 +230,7 @@ export async function getUserMemory(
const result = await parse<AdminMemoryResponse>(await apiFetch(url), "Failed to load memory");
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-管理后台-查看用户记忆",
optDetail: "CM助手-全量-管理后台-查看用户记忆",
url,
reqParam: { userId },
});
@@ -255,7 +255,7 @@ export async function getUserLeaderboard(
const result = await parse<AdminLeaderboardResponse>(await apiFetch(url), "Failed to load leaderboard");
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-管理后台-查询用户排行榜",
optDetail: "CM助手-全量-管理后台-查询用户排行榜",
url,
reqParam: { ...range, limit },
});
@@ -287,7 +287,7 @@ export async function refreshTodayLeaderboard(
);
writeYLog({
optType: OptType.other,
optDetail: "B1-CM助手-管理后台-刷新今日排行榜",
optDetail: "CM助手-全量-管理后台-刷新今日排行榜",
url,
reqParam: { ...range, limit },
});
@@ -309,7 +309,7 @@ export async function getUserAnalytics(
const result = await parse<AdminUserAnalyticsResponse>(await apiFetch(url), "Failed to load user analytics");
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-管理后台-查看用户分析",
optDetail: "CM助手-全量-管理后台-查看用户分析",
url,
reqParam: { userId, ...range, limit },
});
@@ -335,7 +335,7 @@ export async function generateUserLeaderboardReport(
);
writeYLog({
optType: OptType.other,
optDetail: "B1-CM助手-管理后台-生成排行榜报告",
optDetail: "CM助手-全量-管理后台-生成排行榜报告",
url,
reqParam: { ...range, limit, modelName },
returnParam: { generated_at: result.generated_at, model_name: result.model_name },
@@ -414,7 +414,7 @@ export async function streamUserLeaderboardReport({
}
writeYLog({
optType: OptType.other,
optDetail: "B1-CM助手-管理后台-流式生成排行榜报告",
optDetail: "CM助手-全量-管理后台-流式生成排行榜报告",
url,
reqParam: { ...range, limit, modelName },
});
@@ -436,7 +436,7 @@ export async function addAdminMemoryEntry(
);
writeYLog({
optType: OptType.create,
optDetail: "B1-CM助手-管理后台-新增记忆条目",
optDetail: "CM助手-全量-管理后台-新增记忆条目",
url,
reqParam: { userId, content, agent },
});
@@ -460,7 +460,7 @@ export async function replaceAdminMemoryEntry(
);
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-管理后台-编辑记忆条目",
optDetail: "CM助手-全量-管理后台-编辑记忆条目",
url,
reqParam: { userId, old_text: oldText, content, agent },
});
@@ -481,7 +481,7 @@ export async function deleteAdminMemoryEntry(
);
writeYLog({
optType: OptType.delete,
optDetail: "B1-CM助手-管理后台-删除记忆条目",
optDetail: "CM助手-全量-管理后台-删除记忆条目",
url,
reqParam: { userId, old_text: oldText, agent },
});
@@ -508,7 +508,7 @@ export async function updateAdminV1Section(
);
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-管理后台-编辑记忆段落",
optDetail: "CM助手-全量-管理后台-编辑记忆段落",
url,
reqParam: { userId, section_type: sectionType, field, summary, agent },
});
@@ -533,7 +533,7 @@ export async function addAdminV1Fact(
);
writeYLog({
optType: OptType.create,
optDetail: "B1-CM助手-管理后台-新增记忆事实",
optDetail: "CM助手-全量-管理后台-新增记忆事实",
url,
reqParam: { userId, content, category, confidence, agent },
});
@@ -557,7 +557,7 @@ export async function updateAdminV1Fact(
);
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-管理后台-编辑记忆事实",
optDetail: "CM助手-全量-管理后台-编辑记忆事实",
url,
reqParam: { userId, factId, ...updates, agent },
});
@@ -579,7 +579,7 @@ export async function deleteAdminV1Fact(
);
writeYLog({
optType: OptType.delete,
optDetail: "B1-CM助手-管理后台-删除记忆事实",
optDetail: "CM助手-全量-管理后台-删除记忆事实",
url,
reqParam: { userId, factId, agent },
});
+19 -19
View File
@@ -61,7 +61,7 @@ export async function listAgents(params: ListAgentsParams = {}): Promise<Agent[]
const data = (await res.json()) as { agents: Agent[] };
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-智能体列表",
optDetail: "CM助手-全量-智能体列表",
url: `/api/agents${query ? `?${query}` : ""}`,
reqParam: params,
returnParam: data.agents,
@@ -92,7 +92,7 @@ export async function listAgentsPaginated(
};
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-智能体列表",
optDetail: "CM助手-全量-智能体列表",
url: `/api/agents${query ? `?${query}` : ""}`,
reqParam: params,
returnParam: result,
@@ -109,7 +109,7 @@ export async function listAllAgentsAdmin(): Promise<Agent[]> {
const data = (await res.json()) as { agents: Agent[] };
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-智能体列表",
optDetail: "CM助手-全量-智能体列表",
url: `/api/agents?admin=true`,
returnParam: data.agents,
});
@@ -122,7 +122,7 @@ export async function getAgent(id: string): Promise<Agent> {
const agent = (await res.json()) as Agent;
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-智能体-查看",
optDetail: "CM助手-全量-智能体-查看",
url: `/api/agents/${id}`,
reqParam: { id },
returnParam: agent,
@@ -143,7 +143,7 @@ export async function createAgent(request: CreateAgentRequest): Promise<Agent> {
const created = (await res.json()) as Agent;
writeYLog({
optType: OptType.create,
optDetail: "B1-CM助手-智能体-新建",
optDetail: "CM助手-全量-智能体-新建",
url: `/api/agents`,
reqParam: request,
returnParam: created,
@@ -167,7 +167,7 @@ export async function updateAgent(
const updated = (await res.json()) as Agent;
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-智能体-修改",
optDetail: "CM助手-全量-智能体-修改",
url: `/api/agents/${id}`,
reqParam: { id, ...request },
returnParam: updated,
@@ -182,7 +182,7 @@ export async function deleteAgent(id: string): Promise<void> {
if (!res.ok) throw new Error(`Failed to delete agent: ${res.statusText}`);
writeYLog({
optType: OptType.delete,
optDetail: "B1-CM助手-智能体-删除",
optDetail: "CM助手-全量-智能体-删除",
url: `/api/agents/${id}`,
reqParam: { id },
});
@@ -194,7 +194,7 @@ export async function getAgentSelector(): Promise<AgentSelectorResponse> {
const selector = (await res.json()) as AgentSelectorResponse;
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-智能体-选择器",
optDetail: "CM助手-全量-智能体-选择器",
url: `/api/agents/selector`,
returnParam: selector,
});
@@ -211,7 +211,7 @@ export async function favoriteAgent(id: string): Promise<void> {
}
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-智能体-收藏",
optDetail: "CM助手-全量-智能体-收藏",
url: `/api/agents/${id}/favorite`,
reqParam: { id },
});
@@ -227,7 +227,7 @@ export async function unfavoriteAgent(id: string): Promise<void> {
}
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-智能体-取消收藏",
optDetail: "CM助手-全量-智能体-取消收藏",
url: `/api/agents/${id}/favorite`,
reqParam: { id },
});
@@ -243,7 +243,7 @@ export async function pinAgent(id: string): Promise<void> {
}
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-智能体-置顶",
optDetail: "CM助手-全量-智能体-置顶",
url: `/api/agents/${id}/pin`,
reqParam: { id },
});
@@ -259,7 +259,7 @@ export async function unpinAgent(id: string): Promise<void> {
}
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-智能体-取消置顶",
optDetail: "CM助手-全量-智能体-取消置顶",
url: `/api/agents/${id}/pin`,
reqParam: { id },
});
@@ -282,7 +282,7 @@ export async function reorderFeaturedAgents(agentIds: string[]): Promise<void> {
}
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-智能体-精选排序",
optDetail: "CM助手-全量-智能体-精选排序",
url: `/api/agents/featured/order`,
reqParam: { agent_ids: agentIds },
});
@@ -306,7 +306,7 @@ export async function listPrivateAgents(password: string): Promise<Agent[]> {
const data = (await res.json()) as { agents: Agent[] };
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-私有广场智能体列表",
optDetail: "CM助手-全量-私有广场智能体列表",
url: `/api/agents/private-square/list`,
returnParam: data.agents,
});
@@ -364,7 +364,7 @@ export async function listPrivateAgentsPaginated(
};
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-私有广场智能体列表",
optDetail: "CM助手-全量-私有广场智能体列表",
url: `/api/agents/private-square/list`,
reqParam: { ...params, password: undefined },
returnParam: result,
@@ -393,7 +393,7 @@ export async function setAgentPrivate(
const updated = (await res.json()) as Agent;
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-智能体-设置私有",
optDetail: "CM助手-全量-智能体-设置私有",
url: `/api/agents/${id}/private`,
reqParam: { id, is_private: isPrivate },
returnParam: updated,
@@ -422,7 +422,7 @@ export async function setAgentFeatured(
const updated = (await res.json()) as Agent;
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-智能体-设置精选",
optDetail: "CM助手-全量-智能体-设置精选",
url: `/api/agents/${id}/featured`,
reqParam: { id, order },
returnParam: updated,
@@ -455,7 +455,7 @@ export async function adminSyncAgentSkills(): Promise<AgentSkillsSyncResult> {
const result = (await res.json()) as AgentSkillsSyncResult;
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-智能体-同步技能",
optDetail: "CM助手-全量-智能体-同步技能",
url: `/api/agents/admin/sync-skills`,
returnParam: result,
});
@@ -500,7 +500,7 @@ export async function publishArtifactToKnowledge(request: {
const result = (await res.json()) as KnowledgeIngestPublishResult;
writeYLog({
optType: OptType.export,
optDetail: "B1-CM助手-智能体-产物发布入库",
optDetail: "CM助手-全量-智能体-产物发布入库",
url: `/api/knowledge-ingest/publish`,
reqParam: request,
returnParam: result,
@@ -82,7 +82,7 @@ export async function downloadMarkdownDocxFromBackend(options: {
await downloadMarkdownAsDocx(markdown, options.title?.trim() || "document", options.title);
writeYLog({
optType: OptType.export,
optDetail: "B1-CM助手-导出-Word",
optDetail: "CM助手-全量-导出-Word",
url: `${getBackendBaseURL()}/api/writing/export/docx`,
reqParam: { title: options.title?.trim() || undefined },
});
@@ -92,7 +92,7 @@ export async function downloadMarkdownDocxFromBackend(options: {
await downloadBlobResponse(response, fallbackFilename);
writeYLog({
optType: OptType.export,
optDetail: "B1-CM助手-导出-Word",
optDetail: "CM助手-全量-导出-Word",
url: `${getBackendBaseURL()}/api/writing/export/docx`,
reqParam: { title: options.title?.trim() || undefined },
});
@@ -101,7 +101,7 @@ export async function downloadMarkdownDocxFromBackend(options: {
await downloadMarkdownAsDocx(markdown, options.title?.trim() || "document", options.title);
writeYLog({
optType: OptType.export,
optDetail: "B1-CM助手-导出-Word",
optDetail: "CM助手-全量-导出-Word",
url: `${getBackendBaseURL()}/api/writing/export/docx`,
reqParam: { title: options.title?.trim() || undefined },
});
+1 -1
View File
@@ -40,7 +40,7 @@ export async function updateArtifactContent({
}
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-产物-编辑产物内容",
optDetail: "CM助手-全量-产物-编辑产物内容",
url,
reqParam: { filepath, threadId },
});
+3 -3
View File
@@ -37,7 +37,7 @@ export async function updateCuratorConfig(
const result = (await response.json()) as CuratorConfig;
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-策展-更新配置",
optDetail: "CM助手-全量-策展-更新配置",
url,
reqParam: payload,
returnParam: result,
@@ -57,7 +57,7 @@ export async function runCurator(): Promise<RunCuratorResult> {
const result = (await response.json()) as RunCuratorResult;
writeYLog({
optType: OptType.other,
optDetail: "B1-CM助手-策展-手动触发策展",
optDetail: "CM助手-全量-策展-手动触发策展",
url,
returnParam: result,
});
@@ -70,7 +70,7 @@ export async function getCuratorLogs(): Promise<CuratorLogs> {
const result = (await response.json()) as CuratorLogs;
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-策展-查看运行日志",
optDetail: "CM助手-全量-策展-查看运行日志",
url,
});
return result;
@@ -30,7 +30,7 @@ async function listUserPrompts(): Promise<CustomPrompt[]> {
const data = (await res.json()) as { prompts: CustomPrompt[] };
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-自定义提示词-查询",
optDetail: "CM助手-全量-自定义提示词-查询",
url: "/api/user-prompts",
returnParam: data.prompts,
});
@@ -54,7 +54,7 @@ async function createUserPrompt(input: {
const created = (await res.json()) as CustomPrompt;
writeYLog({
optType: OptType.create,
optDetail: "B1-CM助手-自定义提示词-新增",
optDetail: "CM助手-全量-自定义提示词-新增",
url: "/api/user-prompts",
reqParam: input,
returnParam: created,
@@ -78,7 +78,7 @@ async function updateUserPrompt(
const updated = (await res.json()) as CustomPrompt;
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-自定义提示词-编辑",
optDetail: "CM助手-全量-自定义提示词-编辑",
url: `/api/user-prompts/${id}`,
reqParam: input,
returnParam: updated,
@@ -96,7 +96,7 @@ async function deleteUserPrompt(id: string): Promise<void> {
}
writeYLog({
optType: OptType.delete,
optDetail: "B1-CM助手-自定义提示词-删除",
optDetail: "CM助手-全量-自定义提示词-删除",
url: `/api/user-prompts/${id}`,
reqParam: { id },
});
+27 -27
View File
@@ -47,7 +47,7 @@ export async function listKnowledgeNotes(params: KnowledgeListParams = {}) {
);
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-知识库-检索",
optDetail: "CM助手-全量-知识库-检索",
url: `${baseUrl()}/notes?${qs.toString()}`,
reqParam: params,
returnParam: result,
@@ -61,7 +61,7 @@ export async function getKnowledgeNote(noteId: string) {
);
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-知识库-查看",
optDetail: "CM助手-全量-知识库-查看",
url: `${baseUrl()}/notes/${encodeURIComponent(noteId)}`,
reqParam: { noteId },
returnParam: result,
@@ -86,7 +86,7 @@ export async function createKnowledgeNote(data: {
);
writeYLog({
optType: OptType.create,
optDetail: "B1-CM助手-知识库-新建",
optDetail: "CM助手-全量-知识库-新建",
url: `${baseUrl()}/notes`,
reqParam: data,
returnParam: res.note,
@@ -114,7 +114,7 @@ export async function updateKnowledgeNote(
);
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-知识库-修改",
optDetail: "CM助手-全量-知识库-修改",
url: `${baseUrl()}/notes/${encodeURIComponent(noteId)}`,
reqParam: { noteId, data },
returnParam: result,
@@ -131,7 +131,7 @@ export async function deleteKnowledgeNote(noteId: string, hard = false) {
);
writeYLog({
optType: OptType.delete,
optDetail: "B1-CM助手-知识库-删除",
optDetail: "CM助手-全量-知识库-删除",
url: `${baseUrl()}/notes/${encodeURIComponent(noteId)}${hard ? "?hard=true" : ""}`,
reqParam: { noteId, hard },
returnParam: result,
@@ -149,7 +149,7 @@ export async function searchKnowledge(query: string, limit = 8) {
);
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-知识库-检索",
optDetail: "CM助手-全量-知识库-检索",
url: `${baseUrl()}/search`,
reqParam: { query, mode: "keyword", limit },
returnParam: res.items,
@@ -171,7 +171,7 @@ export async function ingestThread(threadId: string, payload: IngestThreadPayloa
);
writeYLog({
optType: OptType.import,
optDetail: "B1-CM助手-知识库-导入",
optDetail: "CM助手-全量-知识库-导入",
url: `${baseUrl()}/ingest/thread/${encodeURIComponent(threadId)}`,
reqParam: { threadId, payload },
returnParam: result,
@@ -189,7 +189,7 @@ export async function ingestThreadsBatch(payload: BatchIngestPayload = {}) {
);
writeYLog({
optType: OptType.import,
optDetail: "B1-CM助手-知识库-导入",
optDetail: "CM助手-全量-知识库-导入",
url: `${baseUrl()}/ingest/threads/batch`,
reqParam: payload,
returnParam: result,
@@ -230,7 +230,7 @@ export async function ingestKnowledgeFile(
);
writeYLog({
optType: OptType.import,
optDetail: "B1-CM助手-知识库-导入",
optDetail: "CM助手-全量-知识库-导入",
url: `${baseUrl()}/ingest/file`,
reqParam: { fileName: file.name, ...opts },
returnParam: result,
@@ -267,7 +267,7 @@ export async function fetchVaultPage(path: string) {
);
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-知识库-Vault页面查看",
optDetail: "CM助手-全量-知识库-Vault页面查看",
url: `${baseUrl()}/vault-page?path=${encodeURIComponent(path)}`,
reqParam: { path },
returnParam: result,
@@ -281,7 +281,7 @@ export async function exportKnowledgeGraph() {
);
writeYLog({
optType: OptType.export,
optDetail: "B1-CM助手-知识库-导出",
optDetail: "CM助手-全量-知识库-导出",
url: `${baseUrl()}/export/graph`,
returnParam: result,
});
@@ -313,7 +313,7 @@ export async function listNoteVersions(noteId: string) {
);
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-知识库-历史版本列表",
optDetail: "CM助手-全量-知识库-历史版本列表",
url: `${baseUrl()}/notes/${encodeURIComponent(noteId)}/versions`,
reqParam: { noteId },
returnParam: res.items,
@@ -332,7 +332,7 @@ export async function listDuplicates() {
);
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-知识库-重复项列表",
optDetail: "CM助手-全量-知识库-重复项列表",
url: `${baseUrl()}/duplicates`,
returnParam: res.groups,
});
@@ -349,7 +349,7 @@ export async function mergeKnowledgeNotes(primaryId: string, duplicateIds: strin
);
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-知识库-合并",
optDetail: "CM助手-全量-知识库-合并",
url: `${baseUrl()}/merge`,
reqParam: { primary_id: primaryId, duplicate_ids: duplicateIds },
returnParam: result,
@@ -363,7 +363,7 @@ export async function reindexKnowledge() {
);
writeYLog({
optType: OptType.other,
optDetail: "B1-CM助手-知识库-重建索引",
optDetail: "CM助手-全量-知识库-重建索引",
url: `${baseUrl()}/reindex`,
returnParam: result,
});
@@ -379,7 +379,7 @@ export async function getKnowledgeGraph() {
const result = await parseResponse<KnowledgeGraph>(await apiFetch(`${baseUrl()}/graph`));
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-知识库-图谱查看",
optDetail: "CM助手-全量-知识库-图谱查看",
url: `${baseUrl()}/graph`,
returnParam: result,
});
@@ -401,7 +401,7 @@ export async function getGraphBlacklist() {
);
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-知识库-图谱黑名单列表",
optDetail: "CM助手-全量-知识库-图谱黑名单列表",
url: `${baseUrl()}/graph/blacklist`,
returnParam: res.items,
});
@@ -419,7 +419,7 @@ export async function addGraphBlacklist(labels: string[]) {
);
writeYLog({
optType: OptType.create,
optDetail: "B1-CM助手-知识库-图谱黑名单新增",
optDetail: "CM助手-全量-知识库-图谱黑名单新增",
url: `${baseUrl()}/graph/blacklist`,
reqParam: { labels },
returnParam: result,
@@ -435,7 +435,7 @@ export async function removeGraphBlacklist(entryId: string) {
);
writeYLog({
optType: OptType.delete,
optDetail: "B1-CM助手-知识库-图谱黑名单删除",
optDetail: "CM助手-全量-知识库-图谱黑名单删除",
url: `${baseUrl()}/graph/blacklist/${encodeURIComponent(entryId)}`,
reqParam: { entryId },
returnParam: result,
@@ -451,7 +451,7 @@ export async function listKnowledgeFolders() {
);
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-知识库-目录列表",
optDetail: "CM助手-全量-知识库-目录列表",
url: `${baseUrl()}/folders`,
returnParam: res.items,
});
@@ -468,7 +468,7 @@ export async function createKnowledgeFolder(path: string) {
);
writeYLog({
optType: OptType.create,
optDetail: "B1-CM助手-知识库-目录新建",
optDetail: "CM助手-全量-知识库-目录新建",
url: `${baseUrl()}/folders`,
reqParam: { path },
returnParam: res.folder,
@@ -486,7 +486,7 @@ export async function renameKnowledgeFolder(folderId: string, path: string) {
);
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-知识库-目录重命名",
optDetail: "CM助手-全量-知识库-目录重命名",
url: `${baseUrl()}/folders/${encodeURIComponent(folderId)}`,
reqParam: { folderId, path },
returnParam: res.folder,
@@ -503,7 +503,7 @@ export async function deleteKnowledgeFolder(folderId: string, reassignTo?: strin
);
writeYLog({
optType: OptType.delete,
optDetail: "B1-CM助手-知识库-目录删除",
optDetail: "CM助手-全量-知识库-目录删除",
url: `${baseUrl()}/folders/${encodeURIComponent(folderId)}${qs}`,
reqParam: { folderId, reassignTo },
returnParam: result,
@@ -523,7 +523,7 @@ export async function listExtractTemplates(scope?: ExtractTemplateScope, enabled
);
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-知识库-抽取模板列表",
optDetail: "CM助手-全量-知识库-抽取模板列表",
url: `${baseUrl()}/extract-templates${q ? `?${q}` : ""}`,
reqParam: { scope, enabledOnly },
returnParam: res.items,
@@ -550,7 +550,7 @@ export async function createExtractTemplate(data: ExtractTemplateInput) {
);
writeYLog({
optType: OptType.create,
optDetail: "B1-CM助手-知识库-抽取模板新建",
optDetail: "CM助手-全量-知识库-抽取模板新建",
url: `${baseUrl()}/extract-templates`,
reqParam: data,
returnParam: res.template,
@@ -571,7 +571,7 @@ export async function updateExtractTemplate(
);
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-知识库-抽取模板修改",
optDetail: "CM助手-全量-知识库-抽取模板修改",
url: `${baseUrl()}/extract-templates/${encodeURIComponent(templateId)}`,
reqParam: { templateId, data },
returnParam: res.template,
@@ -587,7 +587,7 @@ export async function deleteExtractTemplate(templateId: string) {
);
writeYLog({
optType: OptType.delete,
optDetail: "B1-CM助手-知识库-抽取模板删除",
optDetail: "CM助手-全量-知识库-抽取模板删除",
url: `${baseUrl()}/extract-templates/${encodeURIComponent(templateId)}`,
reqParam: { templateId },
returnParam: result,
+2 -2
View File
@@ -64,7 +64,7 @@ export async function loadLlmMetrics(
const result = (await response.json()) as LlmMetricsListResponse;
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-模型指标-查询调用列表",
optDetail: "CM助手-全量-模型指标-查询调用列表",
url,
reqParam: query,
returnParam: { total: result.total, count: result.items.length },
@@ -87,7 +87,7 @@ export async function downloadLlmMetricsExcel(
}
writeYLog({
optType: OptType.export,
optDetail: "B1-CM助手-模型指标-导出Excel",
optDetail: "CM助手-全量-模型指标-导出Excel",
url: requestUrl,
reqParam: filters,
});
+1 -1
View File
@@ -35,7 +35,7 @@ export enum OptType {
export interface WriteYLogParams {
/** 操作类型枚举。 */
optType: OptType;
/** 操作详情(简短描述),统一「B1-CM助手-xxx」。 */
/** 操作详情(简短描述),统一「CM助手-全量-xxx」。 */
optDetail: string;
/** 操作命中的接口地址(完整或相对,内部自动转相对,去 ip/host)。 */
url: string;
+36 -36
View File
@@ -1,36 +1,36 @@
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`);
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) {
const response = await apiFetch(`${getBackendBaseURL()}/api/mcp/config`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(config),
});
const data = await response.json();
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-MCP服务-配置编辑",
url: "/api/mcp/config",
reqParam: config,
returnParam: data,
});
return data;
}
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`);
const data = (await response.json()) as MCPConfig;
writeYLog({
optType: OptType.view,
optDetail: "CM助手-全量-MCP服务-配置查看",
url: "/api/mcp/config",
returnParam: data,
});
return data;
}
export async function updateMCPConfig(config: MCPConfig) {
const response = await apiFetch(`${getBackendBaseURL()}/api/mcp/config`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(config),
});
const data = await response.json();
writeYLog({
optType: OptType.edit,
optDetail: "CM助手-全量-MCP服务-配置编辑",
url: "/api/mcp/config",
reqParam: config,
returnParam: data,
});
return data;
}
+7 -7
View File
@@ -58,7 +58,7 @@ export async function loadMemoryV2(agent?: string): Promise<MemoryV2Data> {
const data = await readJson<MemoryV2Data>(response, "Failed to fetch memory");
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-记忆-查看",
optDetail: "CM助手-全量-记忆-查看",
url: `/api/memory/v2${agentQuery(agent)}`,
reqParam: { agent },
returnParam: data,
@@ -82,7 +82,7 @@ export async function addMemoryEntry(
const data = await readJson<MemoryV2Data>(response, "Failed to add memory entry");
writeYLog({
optType: OptType.create,
optDetail: "B1-CM助手-记忆-新增",
optDetail: "CM助手-全量-记忆-新增",
url: `/api/memory/v2/entries${agentQuery(agent)}`,
reqParam: { target, content, agent },
returnParam: data,
@@ -107,7 +107,7 @@ export async function replaceMemoryEntry(
const data = await readJson<MemoryV2Data>(response, "Failed to replace memory entry");
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-记忆-编辑",
optDetail: "CM助手-全量-记忆-编辑",
url: `/api/memory/v2/entries${agentQuery(agent)}`,
reqParam: { target, old_text: oldText, content, agent },
returnParam: data,
@@ -131,7 +131,7 @@ export async function deleteMemoryEntry(
const data = await readJson<MemoryV2Data>(response, "Failed to delete memory entry");
writeYLog({
optType: OptType.delete,
optDetail: "B1-CM助手-记忆-删除",
optDetail: "CM助手-全量-记忆-删除",
url: `/api/memory/v2/entries?${params.toString()}`,
reqParam: { target, old_text: oldText, agent },
returnParam: data,
@@ -146,7 +146,7 @@ export async function loadMemoryConfig(): Promise<MemoryV2ConfigResponse> {
const data = await readJson<MemoryV2ConfigResponse>(response, "Failed to fetch memory config");
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-记忆-配置查看",
optDetail: "CM助手-全量-记忆-配置查看",
url: "/api/memory/v2/config",
returnParam: data,
});
@@ -164,7 +164,7 @@ export async function patchMemoryConfig(
const data = await readJson<MemoryV2ConfigResponse>(response, "Failed to update memory config");
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-记忆-配置编辑",
optDetail: "CM助手-全量-记忆-配置编辑",
url: "/api/memory/v2/config",
reqParam: { overrides },
returnParam: data,
@@ -190,7 +190,7 @@ export async function searchMemory(
const data = await readJson<MemorySearchResult>(response, "Failed to search memory");
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-记忆-检索",
optDetail: "CM助手-全量-记忆-检索",
url: `/api/memory/v2/search${agentQuery(agent)}`,
reqParam: { query, mode, agent },
returnParam: data,
+21 -21
View File
@@ -1,21 +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<ModelsResponse> {
const res = await apiFetch(`${getBackendBaseURL()}/api/models`);
const data = (await res.json()) as Partial<ModelsResponse>;
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;
}
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<ModelsResponse> {
const res = await apiFetch(`${getBackendBaseURL()}/api/models`);
const data = (await res.json()) as Partial<ModelsResponse>;
const result: ModelsResponse = {
models: data.models ?? [],
token_usage: data.token_usage ?? { enabled: false },
};
writeYLog({
optType: OptType.search,
optDetail: "CM助手-全量-模型-查询",
url: "/api/models",
returnParam: result,
});
return result;
}
+2 -2
View File
@@ -80,7 +80,7 @@ export async function createSearchSpace(
const result = (await response.json()) as SearchSpace;
writeYLog({
optType: OptType.create,
optDetail: "B1-CM助手-笔记-新建笔记空间",
optDetail: "CM助手-全量-笔记-新建笔记空间",
url: "/api/v1/searchspaces",
reqParam: { name: input.name, description: input.description ?? "" },
returnParam: { id: result.id, name: result.name },
@@ -110,7 +110,7 @@ export async function listSearchSpaces(
const list = Array.isArray(data) ? (data as SearchSpace[]) : [];
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-笔记-查询笔记空间列表",
optDetail: "CM助手-全量-笔记-查询笔记空间列表",
url: path,
reqParam: query,
returnParam: { count: list.length },
+4 -4
View File
@@ -27,7 +27,7 @@ export async function listNotifications(
const data = (await res.json()) as NotificationListResponse;
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-通知-查询通知列表",
optDetail: "CM助手-全量-通知-查询通知列表",
url,
reqParam: options,
returnParam: { count: data.items.length },
@@ -48,7 +48,7 @@ export async function markNotificationRead(id: string): Promise<void> {
if (!res.ok) throw await parseDetail(res, "Failed to mark notification read");
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-通知-标记已读",
optDetail: "CM助手-全量-通知-标记已读",
url,
reqParam: { id },
});
@@ -61,7 +61,7 @@ export async function markAllNotificationsRead(): Promise<number> {
const data = (await res.json()) as { updated: number };
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-通知-全部标记已读",
optDetail: "CM助手-全量-通知-全部标记已读",
url,
returnParam: data,
});
@@ -74,7 +74,7 @@ export async function deleteNotification(id: string): Promise<void> {
if (!res.ok) throw await parseDetail(res, "Failed to delete notification");
writeYLog({
optType: OptType.delete,
optDetail: "B1-CM助手-通知-删除通知",
optDetail: "CM助手-全量-通知-删除通知",
url,
reqParam: { id },
});
+12 -12
View File
@@ -27,7 +27,7 @@ export async function listPositions(): Promise<Position[]> {
const data = (await res.json()) as PositionListResponse;
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-岗位-查询",
optDetail: "CM助手-全量-岗位-查询",
url: "/api/positions",
returnParam: data.positions,
});
@@ -44,7 +44,7 @@ export async function createPosition(input: PositionCreateInput): Promise<Positi
const created = (await res.json()) as Position;
writeYLog({
optType: OptType.create,
optDetail: "B1-CM助手-岗位-新增",
optDetail: "CM助手-全量-岗位-新增",
url: "/api/positions",
reqParam: input,
returnParam: created,
@@ -62,7 +62,7 @@ export async function updatePosition(id: string, input: PositionUpdateInput): Pr
const updated = (await res.json()) as Position;
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-岗位-编辑",
optDetail: "CM助手-全量-岗位-编辑",
url: `/api/positions/${id}`,
reqParam: input,
returnParam: updated,
@@ -75,7 +75,7 @@ export async function deletePosition(id: string): Promise<void> {
if (!res.ok) throw await parseDetail(res, "Failed to delete position");
writeYLog({
optType: OptType.delete,
optDetail: "B1-CM助手-岗位-删除",
optDetail: "CM助手-全量-岗位-删除",
url: `/api/positions/${id}`,
reqParam: { id },
});
@@ -87,7 +87,7 @@ export async function getTagBindings(positionId: string): Promise<TagBindings> {
const data = (await res.json()) as TagBindingsResponse;
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-岗位-标签绑定查看",
optDetail: "CM助手-全量-岗位-标签绑定查看",
url: `/api/positions/${positionId}/tag-bindings`,
reqParam: { positionId },
returnParam: data.bindings,
@@ -105,7 +105,7 @@ export async function setTagBindings(positionId: string, bindings: TagBindings):
const data = (await res.json()) as TagBindingsResponse;
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-岗位-标签绑定保存",
optDetail: "CM助手-全量-岗位-标签绑定保存",
url: `/api/positions/${positionId}/tag-bindings`,
reqParam: { positionId, bindings },
returnParam: data.bindings,
@@ -119,7 +119,7 @@ export async function listMembers(positionId: string): Promise<PositionMember[]>
const data = (await res.json()) as { members: PositionMember[] };
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-岗位-成员查询",
optDetail: "CM助手-全量-岗位-成员查询",
url: `/api/positions/${positionId}/members`,
reqParam: { positionId },
returnParam: data.members,
@@ -137,7 +137,7 @@ export async function addMembers(positionId: string, userIds: string[]): Promise
const data = (await res.json()) as { members: PositionMember[] };
writeYLog({
optType: OptType.create,
optDetail: "B1-CM助手-岗位-添加成员",
optDetail: "CM助手-全量-岗位-添加成员",
url: `/api/positions/${positionId}/members`,
reqParam: { positionId, userIds },
returnParam: data.members,
@@ -155,7 +155,7 @@ export async function removeMembers(positionId: string, userIds: string[]): Prom
const data = (await res.json()) as { members: PositionMember[] };
writeYLog({
optType: OptType.delete,
optDetail: "B1-CM助手-岗位-移除成员",
optDetail: "CM助手-全量-岗位-移除成员",
url: `/api/positions/${positionId}/members`,
reqParam: { positionId, userIds },
returnParam: data.members,
@@ -169,7 +169,7 @@ export async function resyncPosition(positionId: string): Promise<number> {
const data = (await res.json()) as { synced: number };
writeYLog({
optType: OptType.other,
optDetail: "B1-CM助手-岗位-同步",
optDetail: "CM助手-全量-岗位-同步",
url: `/api/positions/${positionId}/resync`,
reqParam: { positionId },
returnParam: data,
@@ -190,7 +190,7 @@ export async function listPositionUsers(): Promise<PositionUser[]> {
const data = (await res.json()) as { users: PositionUser[] };
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-岗位-用户查询",
optDetail: "CM助手-全量-岗位-用户查询",
url: "/api/positions/users",
returnParam: data.users,
});
@@ -206,7 +206,7 @@ export async function setUserPosition(userId: string, positionId: string | null)
if (!res.ok) throw await parseDetail(res, "Failed to set user position");
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-岗位-设置用户岗位",
optDetail: "CM助手-全量-岗位-设置用户岗位",
url: `/api/positions/users/${userId}`,
reqParam: { userId, positionId },
});
@@ -19,7 +19,7 @@ export async function listRecommendedQuestions(all = false): Promise<Recommended
const data = (await res.json()) as { questions: RecommendedQuestion[] };
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-推荐问题-查询列表",
optDetail: "CM助手-全量-推荐问题-查询列表",
url,
reqParam: { all },
returnParam: { count: data.questions.length },
@@ -43,7 +43,7 @@ export async function createRecommendedQuestion(
const result = (await res.json()) as RecommendedQuestion;
writeYLog({
optType: OptType.create,
optDetail: "B1-CM助手-推荐问题-新增问题",
optDetail: "CM助手-全量-推荐问题-新增问题",
url,
reqParam: body,
returnParam: result,
@@ -68,7 +68,7 @@ export async function updateRecommendedQuestion(
const result = (await res.json()) as RecommendedQuestion;
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-推荐问题-编辑问题",
optDetail: "CM助手-全量-推荐问题-编辑问题",
url,
reqParam: { id, ...body },
returnParam: result,
@@ -82,7 +82,7 @@ export async function deleteRecommendedQuestion(id: string): Promise<void> {
if (!res.ok) throw new Error(`Failed to delete question: ${res.statusText}`);
writeYLog({
optType: OptType.delete,
optDetail: "B1-CM助手-推荐问题-删除问题",
optDetail: "CM助手-全量-推荐问题-删除问题",
url,
reqParam: { id },
});
@@ -101,7 +101,7 @@ export async function reorderRecommendedQuestions(
const data = (await res.json()) as { questions: RecommendedQuestion[] };
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-推荐问题-调整排序",
optDetail: "CM助手-全量-推荐问题-调整排序",
url,
reqParam: { ids },
});
@@ -90,7 +90,7 @@ export async function createDraftShare(
const result = (await response.json()) as DraftShare;
writeYLog({
optType: OptType.create,
optDetail: "B1-CM助手-会商草稿分享-创建分享",
optDetail: "CM助手-全量-会商草稿分享-创建分享",
url: baseUrl(),
reqParam: { draft_id: draftId, mode },
returnParam: { share_code: result.share_code, mode: result.mode },
@@ -105,7 +105,7 @@ export async function listDraftShares(): Promise<DraftShare[]> {
const json = (await response.json()) as { shares: DraftShare[] };
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-会商草稿分享-查询分享列表",
optDetail: "CM助手-全量-会商草稿分享-查询分享列表",
url: baseUrl(),
returnParam: { count: json.shares.length },
});
@@ -119,7 +119,7 @@ export async function revokeDraftShare(shareCode: string): Promise<void> {
if (!response.ok) throw await parseError(response, "撤销分享失败");
writeYLog({
optType: OptType.delete,
optDetail: "B1-CM助手-会商草稿分享-撤销分享",
optDetail: "CM助手-全量-会商草稿分享-撤销分享",
url,
reqParam: { shareCode },
});
@@ -133,7 +133,7 @@ export async function previewDraftShare(shareCode: string): Promise<DraftSharePr
const result = (await response.json()) as DraftSharePreview;
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-会商草稿分享-预览分享",
optDetail: "CM助手-全量-会商草稿分享-预览分享",
url,
reqParam: { shareCode },
});
@@ -148,7 +148,7 @@ export async function importDraftShare(shareCode: string): Promise<DraftImportRe
const result = (await response.json()) as DraftImportResult;
writeYLog({
optType: OptType.import,
optDetail: "B1-CM助手-会商草稿分享-导入分享",
optDetail: "CM助手-全量-会商草稿分享-导入分享",
url,
reqParam: { shareCode },
returnParam: result,
+33 -33
View File
@@ -57,7 +57,7 @@ export async function listScheduledTasks(params: ListScheduledTasksParams = {})
);
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-定时任务-列表",
optDetail: "CM助手-全量-定时任务-列表",
url: `${baseUrl()}${query ? `?${query}` : ""}`,
reqParam: params,
returnParam: result,
@@ -84,7 +84,7 @@ export async function createScheduledTask(data: {
);
writeYLog({
optType: OptType.create,
optDetail: "B1-CM助手-定时任务-新建",
optDetail: "CM助手-全量-定时任务-新建",
url: baseUrl(),
reqParam: data,
returnParam: result,
@@ -98,7 +98,7 @@ export async function listScheduledTaskRuns(taskId: string) {
);
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-定时任务-运行记录列表",
optDetail: "CM助手-全量-定时任务-运行记录列表",
url: `${baseUrl()}/${encodeURIComponent(taskId)}/runs`,
reqParam: { taskId },
returnParam: result,
@@ -112,7 +112,7 @@ export async function getScheduledTaskRun(runId: string) {
);
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-定时任务-运行详情",
optDetail: "CM助手-全量-定时任务-运行详情",
url: `${baseUrl()}/runs/${encodeURIComponent(runId)}`,
reqParam: { runId },
returnParam: result,
@@ -128,7 +128,7 @@ export async function deleteScheduledTaskRun(runId: string) {
);
writeYLog({
optType: OptType.delete,
optDetail: "B1-CM助手-定时任务-删除运行记录",
optDetail: "CM助手-全量-定时任务-删除运行记录",
url: `${baseUrl()}/runs/${encodeURIComponent(runId)}`,
reqParam: { runId },
returnParam: result,
@@ -143,7 +143,7 @@ export async function getScheduledTaskRunMessages(runId: string) {
);
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-定时任务-运行消息",
optDetail: "CM助手-全量-定时任务-运行消息",
url: `${baseUrl()}/runs/${encodeURIComponent(runId)}/messages`,
reqParam: { runId },
returnParam: result,
@@ -165,7 +165,7 @@ export async function getScheduledTaskRunFileContent(runId: string, fileIndex: n
);
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-定时任务-运行文件查看",
optDetail: "CM助手-全量-定时任务-运行文件查看",
url: `${baseUrl()}/runs/${encodeURIComponent(runId)}/files/${fileIndex}`,
reqParam: { runId, fileIndex },
returnParam: result,
@@ -187,7 +187,7 @@ export async function updateScheduledTaskRunFile(
);
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-定时任务-运行文件编辑",
optDetail: "CM助手-全量-定时任务-运行文件编辑",
url: `${baseUrl()}/runs/${encodeURIComponent(runId)}/files/${fileIndex}`,
reqParam: { runId, fileIndex },
returnParam: result,
@@ -254,7 +254,7 @@ export async function downloadScheduledTaskRun(runId: string) {
);
writeYLog({
optType: OptType.export,
optDetail: "B1-CM助手-定时任务-下载运行结果",
optDetail: "CM助手-全量-定时任务-下载运行结果",
url: scheduledTaskRunDownloadUrl(runId),
reqParam: { runId },
});
@@ -267,7 +267,7 @@ export async function downloadScheduledTaskRunFile(runId: string, fileIndex: num
);
writeYLog({
optType: OptType.export,
optDetail: "B1-CM助手-定时任务-下载运行文件",
optDetail: "CM助手-全量-定时任务-下载运行文件",
url: scheduledTaskRunFileDownloadUrl(runId, fileIndex),
reqParam: { runId, fileIndex },
});
@@ -279,7 +279,7 @@ export async function getScheduledTaskDeliveryProfile() {
);
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-定时任务-投递配置查看",
optDetail: "CM助手-全量-定时任务-投递配置查看",
url: `${baseUrl()}/delivery-profile`,
returnParam: result,
});
@@ -298,7 +298,7 @@ export async function updateScheduledTaskDeliveryProfile(data: {
);
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-定时任务-投递配置修改",
optDetail: "CM助手-全量-定时任务-投递配置修改",
url: `${baseUrl()}/delivery-profile`,
reqParam: data,
returnParam: result,
@@ -329,7 +329,7 @@ export async function updateScheduledTask(
);
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-定时任务-修改",
optDetail: "CM助手-全量-定时任务-修改",
url: `${baseUrl()}/${encodeURIComponent(taskId)}`,
reqParam: { taskId, data },
returnParam: result,
@@ -345,7 +345,7 @@ export async function deleteScheduledTask(taskId: string) {
);
writeYLog({
optType: OptType.delete,
optDetail: "B1-CM助手-定时任务-删除",
optDetail: "CM助手-全量-定时任务-删除",
url: `${baseUrl()}/${encodeURIComponent(taskId)}`,
reqParam: { taskId },
returnParam: result,
@@ -361,7 +361,7 @@ export async function pauseScheduledTask(taskId: string) {
);
writeYLog({
optType: OptType.disabled,
optDetail: "B1-CM助手-定时任务-暂停",
optDetail: "CM助手-全量-定时任务-暂停",
url: `${baseUrl()}/${encodeURIComponent(taskId)}/pause`,
reqParam: { taskId },
returnParam: result,
@@ -377,7 +377,7 @@ export async function resumeScheduledTask(taskId: string) {
);
writeYLog({
optType: OptType.enabled,
optDetail: "B1-CM助手-定时任务-恢复",
optDetail: "CM助手-全量-定时任务-恢复",
url: `${baseUrl()}/${encodeURIComponent(taskId)}/resume`,
reqParam: { taskId },
returnParam: result,
@@ -393,7 +393,7 @@ export async function triggerScheduledTask(taskId: string) {
);
writeYLog({
optType: OptType.other,
optDetail: "B1-CM助手-定时任务-手动运行",
optDetail: "CM助手-全量-定时任务-手动运行",
url: `${baseUrl()}/${encodeURIComponent(taskId)}/trigger`,
reqParam: { taskId },
returnParam: result,
@@ -411,7 +411,7 @@ export async function listPublishedScheduledTasks(params: { limit?: number; offs
);
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-定时任务-已发布列表",
optDetail: "CM助手-全量-定时任务-已发布列表",
url: `${baseUrl()}/published${suffix ? `?${suffix}` : ""}`,
reqParam: params,
returnParam: result,
@@ -427,7 +427,7 @@ export async function publishScheduledTask(taskId: string) {
);
writeYLog({
optType: OptType.export,
optDetail: "B1-CM助手-定时任务-发布",
optDetail: "CM助手-全量-定时任务-发布",
url: `${baseUrl()}/${encodeURIComponent(taskId)}/publish`,
reqParam: { taskId },
returnParam: result,
@@ -443,7 +443,7 @@ export async function unpublishScheduledTask(taskId: string) {
);
writeYLog({
optType: OptType.other,
optDetail: "B1-CM助手-定时任务-取消发布",
optDetail: "CM助手-全量-定时任务-取消发布",
url: `${baseUrl()}/${encodeURIComponent(taskId)}/unpublish`,
reqParam: { taskId },
returnParam: result,
@@ -461,7 +461,7 @@ export async function subscribeScheduledTask(taskId: string, notifyElement: bool
);
writeYLog({
optType: OptType.create,
optDetail: "B1-CM助手-定时任务-订阅",
optDetail: "CM助手-全量-定时任务-订阅",
url: `${baseUrl()}/${encodeURIComponent(taskId)}/subscribe`,
reqParam: { taskId, notify_element: notifyElement },
returnParam: result,
@@ -479,7 +479,7 @@ export async function updateScheduledTaskSubscription(taskId: string, notifyElem
);
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-定时任务-订阅修改",
optDetail: "CM助手-全量-定时任务-订阅修改",
url: `${baseUrl()}/${encodeURIComponent(taskId)}/subscribe`,
reqParam: { taskId, notify_element: notifyElement },
returnParam: result,
@@ -495,7 +495,7 @@ export async function unsubscribeScheduledTask(taskId: string) {
);
writeYLog({
optType: OptType.delete,
optDetail: "B1-CM助手-定时任务-取消订阅",
optDetail: "CM助手-全量-定时任务-取消订阅",
url: `${baseUrl()}/${encodeURIComponent(taskId)}/subscribe`,
reqParam: { taskId },
returnParam: result,
@@ -515,7 +515,7 @@ export async function listAllScheduledTasks(search?: string) {
);
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-定时任务-全部任务列表",
optDetail: "CM助手-全量-定时任务-全部任务列表",
url: `${baseUrl()}/admin/all${query ? `?${query}` : ""}`,
reqParam: { search },
returnParam: result,
@@ -530,7 +530,7 @@ export async function adminListScheduledTaskRuns(taskId: string) {
);
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-定时任务-运行记录列表",
optDetail: "CM助手-全量-定时任务-运行记录列表",
url: `${baseUrl()}/admin/${encodeURIComponent(taskId)}/runs`,
reqParam: { taskId },
returnParam: result,
@@ -561,7 +561,7 @@ export async function adminUpdateScheduledTask(
);
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-定时任务-修改",
optDetail: "CM助手-全量-定时任务-修改",
url: `${baseUrl()}/admin/${encodeURIComponent(taskId)}`,
reqParam: { taskId, data },
returnParam: result,
@@ -578,7 +578,7 @@ export async function adminDeleteScheduledTask(taskId: string) {
);
writeYLog({
optType: OptType.delete,
optDetail: "B1-CM助手-定时任务-删除",
optDetail: "CM助手-全量-定时任务-删除",
url: `${baseUrl()}/admin/${encodeURIComponent(taskId)}`,
reqParam: { taskId },
returnParam: result,
@@ -595,7 +595,7 @@ export async function adminTriggerScheduledTask(taskId: string) {
);
writeYLog({
optType: OptType.other,
optDetail: "B1-CM助手-定时任务-手动运行",
optDetail: "CM助手-全量-定时任务-手动运行",
url: `${baseUrl()}/admin/${encodeURIComponent(taskId)}/trigger`,
reqParam: { taskId },
returnParam: result,
@@ -612,7 +612,7 @@ export async function getScheduledTaskRunHtml(runId: string) {
);
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-定时任务-运行HTML查看",
optDetail: "CM助手-全量-定时任务-运行HTML查看",
url: `${baseUrl()}/runs/${encodeURIComponent(runId)}/html`,
reqParam: { runId },
returnParam: result,
@@ -626,7 +626,7 @@ export async function listHtmlPageFavorites() {
const result = await parseResponse<HtmlPageFavorite[]>(await apiFetch(favoritesUrl()));
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-HTML收藏-列表",
optDetail: "CM助手-全量-HTML收藏-列表",
url: `/api/html-page-favorites`,
returnParam: result,
});
@@ -639,7 +639,7 @@ export async function getHtmlPageFavorite(favoriteId: string) {
);
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-HTML收藏-查看",
optDetail: "CM助手-全量-HTML收藏-查看",
url: `/api/html-page-favorites/${encodeURIComponent(favoriteId)}`,
reqParam: { favoriteId },
returnParam: result,
@@ -662,7 +662,7 @@ export async function createHtmlPageFavorite(data: {
);
writeYLog({
optType: OptType.create,
optDetail: "B1-CM助手-HTML收藏-新建",
optDetail: "CM助手-全量-HTML收藏-新建",
url: `/api/html-page-favorites`,
reqParam: data,
returnParam: result,
@@ -678,7 +678,7 @@ export async function deleteHtmlPageFavorite(favoriteId: string) {
);
writeYLog({
optType: OptType.delete,
optDetail: "B1-CM助手-HTML收藏-删除",
optDetail: "CM助手-全量-HTML收藏-删除",
url: `/api/html-page-favorites/${encodeURIComponent(favoriteId)}`,
reqParam: { favoriteId },
returnParam: result,
@@ -488,7 +488,7 @@ export async function downloadMarkdownAsDocx(
const blob = await markdownToDocxBlob(markdown, title);
writeYLog({
optType: OptType.export,
optDetail: "B1-CM助手-定时任务-导出",
optDetail: "CM助手-全量-定时任务-导出",
url: "/scheduled-tasks/export/docx",
reqParam: { fileName, title },
});
@@ -0,0 +1,73 @@
import assert from "node:assert/strict";
import test from "node:test";
import { applyInlineCitations, type RuntimeDisplaySource } from "./runtime";
function makeSources(count: number): RuntimeDisplaySource[] {
return Array.from({ length: count }, (_unused, position) => ({
index: position + 1,
skillName: "knowledge-base-search",
mode: "citation",
title: `参考文献 ${position + 1}`,
type: "result",
snippet: "",
raw: {},
}));
}
function citation(index: number): string {
return `[citation:${index}](#reference-${index})`;
}
test("全角单编号 【1】【2】 解析为引用徽章", () => {
const out = applyInlineCitations("结论见【1】【2】。", makeSources(2));
assert.equal(out.includes(citation(1)), true);
assert.equal(out.includes(citation(2)), true);
assert.equal(out.includes("【1】"), false);
assert.equal(out.includes("【2】"), false);
});
test("全角数字 【1】 也能识别", () => {
const out = applyInlineCitations("参考【1】和【2】。", makeSources(2));
assert.equal(out.includes(citation(1)), true);
assert.equal(out.includes(citation(2)), true);
});
test("全角括号合并编号 【1,2】/【1,2】/【1、2】 拆成多个引用", () => {
for (const marker of ["【1,2】", "【1,2】", "【1、2】"]) {
const out = applyInlineCitations(`见${marker}。`, makeSources(2));
assert.equal(out.includes(citation(1)), true, marker);
assert.equal(out.includes(citation(2)), true, marker);
}
});
test("全角区间编号 【1-3】/【2~5】 展开为连续引用", () => {
const dash = applyInlineCitations("见【1-3】。", makeSources(3));
assert.equal(dash.includes(citation(1)), true);
assert.equal(dash.includes(citation(2)), true);
assert.equal(dash.includes(citation(3)), true);
const tilde = applyInlineCitations("见【2~4】。", makeSources(4));
assert.equal(tilde.includes(citation(2)), true);
assert.equal(tilde.includes(citation(3)), true);
assert.equal(tilde.includes(citation(4)), true);
});
test("超出来源范围的全角编号保持原样,不误判正文", () => {
// 只有 1 条来源,【3】 没有对应来源 → 不应转换
const out = applyInlineCitations("正文【3】不是引用。", makeSources(1));
assert.equal(out.includes("【3】"), true);
assert.equal(out.includes("#reference-3"), false);
});
test("非数字的全角括号不被改写", () => {
const out = applyInlineCitations("见【附录】小节。", makeSources(2));
assert.equal(out.includes("【附录】"), true);
});
test("半角写法仍然正常工作", () => {
const out = applyInlineCitations("见[1][2]和[1,3]。", makeSources(3));
assert.equal(out.includes(citation(1)), true);
assert.equal(out.includes(citation(2)), true);
assert.equal(out.includes(citation(3)), true);
});
+56 -3
View File
@@ -623,9 +623,62 @@ function normalizeLooseCitationTokens(
return indexes.map((index) => `[${index}]`).join("");
};
// 全角括号写法:【1】→ [1]
let next = content.replace(/【\s*(\d{1,3})\s*】/g, (match, rawIndex: string) =>
isValidIndex(Number(rawIndex)) ? `[${rawIndex}]` : match,
// 全角数字转半角:123 → 123(仅用于括号内的编号识别)
const toAsciiDigits = (value: string) =>
value.replace(/[0-9]/g, (digit) =>
String.fromCharCode(digit.charCodeAt(0) - 0xfee0),
);
// 全角括号写法(含全角数字):
// 【1】/【1】 → [1]
// 【1,2】/【1,2】/【1、2】 → [1][2]
// 【1-3】/【2~5】 → [1][2][3]
// 三种形态互斥(单编号内不含分隔符),且都要求编号能对应到参考来源,
// 避免把正文里的【…】误判成引用。转成半角后交给下面的半角逻辑统一处理。
// 全角单编号:【1】/【1】
let next = content.replace(
/【\s*([0-90-9]{1,3})\s*】/g,
(match, rawIndex: string) => {
const index = Number(toAsciiDigits(rawIndex));
return isValidIndex(index) ? `[${index}]` : match;
},
);
// 全角合并编号:【1,2】/【1,2】/【1、2】
next = next.replace(
/【\s*([0-90-9]{1,3}(?:\s*[,,、]\s*[0-90-9]{1,3})+)\s*】/g,
(match, rawList: string) => {
const indexes = rawList
.split(/[,,、]/)
.map((part) => Number(toAsciiDigits(part.trim())));
if (indexes.some((index) => !isValidIndex(index))) {
return match;
}
return indexes.map((index) => `[${index}]`).join("");
},
);
// 全角区间编号:【1-3】/【2~5】
next = next.replace(
/【\s*([0-90-9]{1,3})\s*[-–~~]\s*([0-90-9]{1,3})\s*】/g,
(match, rawStart: string, rawEnd: string) => {
const start = Number(toAsciiDigits(rawStart));
const end = Number(toAsciiDigits(rawEnd));
if (
end <= start ||
end - start > 20 ||
!isValidIndex(start) ||
!isValidIndex(end)
) {
return match;
}
const tokens: string[] = [];
for (let index = start; index <= end; index++) {
tokens.push(`[${index}]`);
}
return tokens.join("");
},
);
// 合并写法:[1,2,3] / [1,2] / [1、2] → [1][2][3]
+8 -8
View File
@@ -34,7 +34,7 @@ export async function fetchStudioNotebooks(
const notebooks = (await res.json()) as StudioNotebook[];
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-工作室-空间查询",
optDetail: "CM助手-全量-工作室-空间查询",
url: "/api/v1/searchspaces",
returnParam: notebooks,
});
@@ -62,7 +62,7 @@ export async function createSearchSpace(
}
writeYLog({
optType: OptType.create,
optDetail: "B1-CM助手-工作室-新建空间",
optDetail: "CM助手-全量-工作室-新建空间",
url: "/api/v1/searchspaces",
reqParam: { name },
});
@@ -91,7 +91,7 @@ export async function uploadSourceText(
if (!res.ok) throw new Error(`上传失败:${res.status}`);
writeYLog({
optType: OptType.import,
optDetail: "B1-CM助手-工作室-上传文字来源",
optDetail: "CM助手-全量-工作室-上传文字来源",
url: "/api/v1/documents/textupload",
reqParam: { search_space_id: notebookId, title: title || "粘贴的文字" },
});
@@ -110,7 +110,7 @@ export async function deleteSearchSpace(
if (!res.ok) throw new Error(`删除失败:${res.status}`);
writeYLog({
optType: OptType.delete,
optDetail: "B1-CM助手-工作室-删除空间",
optDetail: "CM助手-全量-工作室-删除空间",
url: `/api/v1/searchspaces/${id}`,
reqParam: { id },
});
@@ -134,7 +134,7 @@ export async function renameSearchSpace(
if (!res.ok) throw new Error(`重命名失败:${res.status}`);
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-工作室-重命名空间",
optDetail: "CM助手-全量-工作室-重命名空间",
url: `/api/v1/searchspaces/${id}`,
reqParam: { id, name },
});
@@ -169,7 +169,7 @@ export async function fetchDocumentByChunk(
const doc = (await res.json()) as DocumentWithChunks;
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-工作室-查看文档",
optDetail: "CM助手-全量-工作室-查看文档",
url: `/api/v1/documents/by-chunk/${chunkId}`,
reqParam: { chunkId },
returnParam: { id: doc.id, title: doc.title, file_type: doc.file_type },
@@ -199,7 +199,7 @@ export async function uploadSourceFiles(
if (!res.ok) throw new Error(`上传失败:${res.status}`);
writeYLog({
optType: OptType.import,
optDetail: "B1-CM助手-工作室-上传文件来源",
optDetail: "CM助手-全量-工作室-上传文件来源",
url: "/api/v1/documents/fileupload",
reqParam: {
search_space_id: notebookId,
@@ -223,7 +223,7 @@ export async function fetchNotebookSources(
const sources = (await res.json()) as SourceItem[];
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-工作室-来源查询",
optDetail: "CM助手-全量-工作室-来源查询",
url: `/api/v1/source/search-space/${notebookId}?limit=100`,
reqParam: { notebookId },
returnParam: sources,
+17 -17
View File
@@ -24,7 +24,7 @@ export async function loadPromptPrefixSettings(): Promise<PromptPrefixSettings>
const data = (await response.json()) as PromptPrefixSettings;
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-系统设置-提示词前缀查看",
optDetail: "CM助手-全量-系统设置-提示词前缀查看",
url: "/api/system-settings/prompt-prefix",
returnParam: data,
});
@@ -51,7 +51,7 @@ export async function updatePromptPrefixSettings(
const data = (await response.json()) as PromptPrefixSettings;
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-系统设置-提示词前缀编辑",
optDetail: "CM助手-全量-系统设置-提示词前缀编辑",
url: "/api/system-settings/prompt-prefix",
reqParam: body,
returnParam: data,
@@ -105,7 +105,7 @@ export async function loadPrivateSquareStatus(): Promise<PrivateSquareStatus> {
const data = (await response.json()) as PrivateSquareStatus;
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-系统设置-私密广场查看",
optDetail: "CM助手-全量-系统设置-私密广场查看",
url: "/api/system-settings/private-square",
returnParam: data,
});
@@ -138,7 +138,7 @@ export async function loadPublishSquares(): Promise<PublishSquaresSettings> {
const data = (await response.json()) as PublishSquaresSettings;
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-系统设置-发布广场查看",
optDetail: "CM助手-全量-系统设置-发布广场查看",
url: "/api/system-settings/publish-squares",
returnParam: data,
});
@@ -165,7 +165,7 @@ export async function updatePublishSquares(
const data = (await response.json()) as PublishSquaresSettings;
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-系统设置-发布广场编辑",
optDetail: "CM助手-全量-系统设置-发布广场编辑",
url: "/api/system-settings/publish-squares",
reqParam: body,
returnParam: data,
@@ -203,7 +203,7 @@ export async function loadAppearanceSettings(): Promise<AppearanceSettings> {
const data = (await response.json()) as AppearanceSettings;
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-系统设置-外观查看",
optDetail: "CM助手-全量-系统设置-外观查看",
url: "/api/system-settings/appearance",
returnParam: data,
});
@@ -230,7 +230,7 @@ export async function updateAppearanceSettings(
const data = (await response.json()) as AppearanceSettings;
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-系统设置-外观编辑",
optDetail: "CM助手-全量-系统设置-外观编辑",
url: "/api/system-settings/appearance",
reqParam: body,
returnParam: data,
@@ -250,7 +250,7 @@ export async function loadCitationDisplaySettings(): Promise<CitationDisplaySett
const data = (await response.json()) as CitationDisplaySettings;
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-系统设置-引用展示查看",
optDetail: "CM助手-全量-系统设置-引用展示查看",
url: "/api/system-settings/citation-display",
returnParam: data,
});
@@ -267,7 +267,7 @@ export async function loadLeaderboardSettings(): Promise<LeaderboardSettings> {
const data = (await response.json()) as LeaderboardSettings;
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-系统设置-排行榜查看",
optDetail: "CM助手-全量-系统设置-排行榜查看",
url: "/api/system-settings/leaderboard",
returnParam: data,
});
@@ -294,7 +294,7 @@ export async function updateLeaderboardSettings(
const data = (await response.json()) as LeaderboardSettings;
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-系统设置-排行榜编辑",
optDetail: "CM助手-全量-系统设置-排行榜编辑",
url: "/api/system-settings/leaderboard",
reqParam: body,
returnParam: data,
@@ -322,7 +322,7 @@ export async function updateCitationDisplaySettings(
const data = (await response.json()) as CitationDisplaySettings;
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-系统设置-引用展示编辑",
optDetail: "CM助手-全量-系统设置-引用展示编辑",
url: "/api/system-settings/citation-display",
reqParam: body,
returnParam: data,
@@ -354,7 +354,7 @@ export async function loadSkillCompressionSettings(): Promise<SkillCompressionSe
const data = (await response.json()) as SkillCompressionSettings;
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-系统设置-技能压缩查看",
optDetail: "CM助手-全量-系统设置-技能压缩查看",
url: "/api/system-settings/skill-compression",
returnParam: data,
});
@@ -381,7 +381,7 @@ export async function updateSkillCompressionSettings(
const data = (await response.json()) as SkillCompressionSettings;
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-系统设置-技能压缩编辑",
optDetail: "CM助手-全量-系统设置-技能压缩编辑",
url: "/api/system-settings/skill-compression",
reqParam: body,
returnParam: data,
@@ -407,7 +407,7 @@ export async function loadAgentModuleConfig(
const data = (await response.json()) as AgentModuleConfig;
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-系统设置-智能体模块查看",
optDetail: "CM助手-全量-系统设置-智能体模块查看",
url: `/api/system-settings/agent-modules/${key}`,
reqParam: { key },
returnParam: data,
@@ -436,7 +436,7 @@ export async function updateAgentModuleConfig(
const data = (await response.json()) as AgentModuleConfig;
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-系统设置-智能体模块编辑",
optDetail: "CM助手-全量-系统设置-智能体模块编辑",
url: `/api/system-settings/agent-modules/${key}`,
reqParam: { key, tag_ids: tagIds },
returnParam: data,
@@ -460,7 +460,7 @@ export async function loadLogSystemSettings(): Promise<LogSystemSettings> {
const data = (await response.json()) as LogSystemSettings;
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-系统设置-日志系统查看",
optDetail: "CM助手-全量-系统设置-日志系统查看",
url: "/api/system-settings/log-system",
returnParam: data,
});
@@ -487,7 +487,7 @@ export async function updatePrivateSquareSettings(
const data = (await response.json()) as PrivateSquareStatus;
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-系统设置-私密广场编辑",
optDetail: "CM助手-全量-系统设置-私密广场编辑",
url: "/api/system-settings/private-square",
// omit `password` (secret) from the logged request params
reqParam: { enabled: body.enabled, password_set: body.password != null },
+6 -6
View File
@@ -25,7 +25,7 @@ export async function listTags(tagType?: TagType): Promise<Tag[]> {
const data = (await res.json()) as TagListResponse;
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-标签-查询",
optDetail: "CM助手-全量-标签-查询",
url: `/api/tags${query}`,
reqParam: { tagType },
returnParam: data.tags,
@@ -43,7 +43,7 @@ export async function createTag(input: TagCreateInput): Promise<Tag> {
const created = (await res.json()) as Tag;
writeYLog({
optType: OptType.create,
optDetail: "B1-CM助手-标签-新增",
optDetail: "CM助手-全量-标签-新增",
url: "/api/tags",
reqParam: input,
returnParam: created,
@@ -61,7 +61,7 @@ export async function updateTag(id: string, input: TagUpdateInput): Promise<Tag>
const updated = (await res.json()) as Tag;
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-标签-编辑",
optDetail: "CM助手-全量-标签-编辑",
url: `/api/tags/${id}`,
reqParam: input,
returnParam: updated,
@@ -76,7 +76,7 @@ export async function deleteTag(id: string): Promise<void> {
if (!res.ok) throw await parseDetail(res, "Failed to delete tag");
writeYLog({
optType: OptType.delete,
optDetail: "B1-CM助手-标签-删除",
optDetail: "CM助手-全量-标签-删除",
url: `/api/tags/${id}`,
reqParam: { id },
});
@@ -93,7 +93,7 @@ export async function getResourceTags(
const data = (await res.json()) as TagListResponse;
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-标签-资源标签查看",
optDetail: "CM助手-全量-标签-资源标签查看",
url: `/api/tags/assignments/${targetType}/${targetId}`,
reqParam: { targetType, targetId },
returnParam: data.tags,
@@ -118,7 +118,7 @@ export async function setResourceTags(
const data = (await res.json()) as TagListResponse;
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-标签-资源标签设置",
optDetail: "CM助手-全量-标签-资源标签设置",
url: `/api/tags/assignments/${targetType}/${targetId}`,
reqParam: { targetType, targetId, tagIds },
returnParam: data.tags,
+5 -5
View File
@@ -69,7 +69,7 @@ export async function createShare(
const result = (await response.json()) as ThreadShare;
writeYLog({
optType: OptType.create,
optDetail: "B1-CM助手-会话分享-创建分享",
optDetail: "CM助手-全量-会话分享-创建分享",
url: baseUrl(),
reqParam: { thread_id: threadId, mode },
returnParam: { share_code: result.share_code, mode: result.mode },
@@ -84,7 +84,7 @@ export async function listShares(): Promise<ThreadShare[]> {
const json = (await response.json()) as { shares: ThreadShare[] };
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-会话分享-查询分享列表",
optDetail: "CM助手-全量-会话分享-查询分享列表",
url: baseUrl(),
returnParam: { count: json.shares.length },
});
@@ -98,7 +98,7 @@ export async function revokeShare(shareCode: string): Promise<void> {
if (!response.ok) throw await parseError(response, "撤销分享失败");
writeYLog({
optType: OptType.delete,
optDetail: "B1-CM助手-会话分享-撤销分享",
optDetail: "CM助手-全量-会话分享-撤销分享",
url,
reqParam: { shareCode },
});
@@ -112,7 +112,7 @@ export async function previewShare(shareCode: string): Promise<SharePreview> {
const result = (await response.json()) as SharePreview;
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-会话分享-预览分享",
optDetail: "CM助手-全量-会话分享-预览分享",
url,
reqParam: { shareCode },
});
@@ -127,7 +127,7 @@ export async function importShare(shareCode: string): Promise<ImportResult> {
const result = (await response.json()) as ImportResult;
writeYLog({
optType: OptType.import,
optDetail: "B1-CM助手-会话分享-导入分享",
optDetail: "CM助手-全量-会话分享-导入分享",
url,
reqParam: { shareCode },
returnParam: result,
+1 -1
View File
@@ -425,7 +425,7 @@ export function useThreadStream({
const question = lastHumanFromChatModelInput(data.input);
writeYLog({
optType: OptType.other,
optDetail: "B1-CM助手-智能问答",
optDetail: "CM助手-全量-智能问答",
url: `/api/threads/${threadIdRef.current ?? ""}/runs/stream`,
reqParam: { model: event.name ?? "", question },
returnParam: { data: answer },
+3 -3
View File
@@ -108,7 +108,7 @@ export async function loadToolMetrics(
const result = (await response.json()) as ToolMetricsListResponse;
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-工具指标-查询调用列表",
optDetail: "CM助手-全量-工具指标-查询调用列表",
url,
reqParam: query,
returnParam: { total: result.total, count: result.items.length },
@@ -128,7 +128,7 @@ export async function loadToolMetricsSummary(
const result = (await response.json()) as ToolMetricsSummary;
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-工具指标-查询汇总统计",
optDetail: "CM助手-全量-工具指标-查询汇总统计",
url,
reqParam: filters,
returnParam: { total: result.total, error_total: result.error_total },
@@ -147,7 +147,7 @@ export async function downloadToolMetricsExcel(
}
writeYLog({
optType: OptType.export,
optDetail: "B1-CM助手-工具指标-导出Excel",
optDetail: "CM助手-全量-工具指标-导出Excel",
url: requestUrl,
reqParam: filters,
});
@@ -40,7 +40,7 @@ async function updateUserPreferences(
const result = { hide_admin_prompt_prefix: !!data.hide_admin_prompt_prefix };
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-用户偏好-编辑",
optDetail: "CM助手-全量-用户偏好-编辑",
url: "/api/user-preferences",
reqParam: input,
returnParam: result,
+1 -1
View File
@@ -45,7 +45,7 @@ export async function listMyQa(
const result = await parse<UserQaPageResponse>(await apiFetch(url), "Failed to load my Q&A");
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-用户问答-查询我的问答",
optDetail: "CM助手-全量-用户问答-查询我的问答",
url,
reqParam: params,
returnParam: { total: result.total, count: result.items.length },
+1 -1
View File
@@ -165,7 +165,7 @@ export default function AgentChatPage({
const lastHuman = [...msgs].reverse().find((m) => (m as { type?: string }).type === "human");
writeYLog({
optType: OptType.other,
optDetail: "B1-CM助手-专家代理问答",
optDetail: "CM助手-全量-专家代理问答",
url: `/api/threads/${threadId ?? ""}/runs/stream`,
reqParam: { agent_id, question: lastHuman ? textOfMessage(lastHuman) : "" },
returnParam: { data: lastAi ? textOfMessage(lastAi) : "" },
+1 -1
View File
@@ -195,7 +195,7 @@ export default function ChatPage() {
const lastHuman = [...msgs].reverse().find((m) => (m as { type?: string }).type === "human");
writeYLog({
optType: OptType.other,
optDetail: "B1-CM助手-专家代理问答",
optDetail: "CM助手-全量-专家代理问答",
url: `/api/threads/${threadId ?? ""}/runs/stream`,
reqParam: { agent_id: selectedAgentId, question: lastHuman ? textOfMessage(lastHuman) : "" },
returnParam: { data: lastAi ? textOfMessage(lastAi) : "" },
+1 -1
View File
@@ -181,7 +181,7 @@ export default function LoginPage() {
void fetchYLogConfig();
writeYLog({
optType: OptType.login,
optDetail: 'B1-CM助手-登录',
optDetail: 'CM助手-全量-登录',
url: '/api/v1/auth/login/token',
reqParam: { username: loginUserName || auth.user_id },
returnParam: { user_id: auth.user_id },
@@ -158,7 +158,7 @@ export async function listChains(scope: ChainScope = "mine"): Promise<BusinessCh
const chains = (data.chains ?? []).map(toChain);
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-业务链条-查询",
optDetail: "CM助手-全量-业务链条-查询",
url,
reqParam: { scope },
returnParam: { count: chains.length },
@@ -179,7 +179,7 @@ export async function getChain(id: string): Promise<BusinessChain | null> {
const chain = toChain(data);
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-业务链条-查看",
optDetail: "CM助手-全量-业务链条-查看",
url: `${BASE()}/${id}`,
reqParam: { id },
returnParam: { id: chain.id, title: chain.title },
@@ -197,7 +197,7 @@ export async function createChain(payload: ChainCreatePayload): Promise<Business
const chain = toChain(data);
writeYLog({
optType: OptType.create,
optDetail: "B1-CM助手-业务链条-新建",
optDetail: "CM助手-全量-业务链条-新建",
url: BASE(),
reqParam: { title: payload.title, seats: payload.seats?.length ?? 0 },
returnParam: { id: chain.id, title: chain.title },
@@ -218,10 +218,10 @@ export async function updateChain(id: string, patch: ChainUpdatePayload): Promis
optType: patch.isPublic === true ? OptType.enabled : patch.isPublic === false ? OptType.disabled : OptType.edit,
optDetail:
patch.isPublic === true
? "B1-CM助手-业务链条-发布"
? "CM助手-全量-业务链条-发布"
: patch.isPublic === false
? "B1-CM助手-业务链条-取消发布"
: "B1-CM助手-业务链条-编辑",
? "CM助手-全量-业务链条-取消发布"
: "CM助手-全量-业务链条-编辑",
url: `${BASE()}/${id}`,
reqParam: { id, ...patch },
returnParam: { id: chain.id, title: chain.title },
@@ -237,7 +237,7 @@ export async function deleteChain(id: string): Promise<void> {
}
writeYLog({
optType: OptType.delete,
optDetail: "B1-CM助手-业务链条-删除",
optDetail: "CM助手-全量-业务链条-删除",
url: `${BASE()}/${id}`,
reqParam: { id },
});
@@ -48,7 +48,7 @@ export async function getDashboardSessionByTask(taskId: string): Promise<Dashboa
const session = toClient((await res.json()) as ServerDashboardSession);
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-大屏会话-查看",
optDetail: "CM助手-全量-大屏会话-查看",
url: `${BASE()}/by-task/${taskId}`,
reqParam: { taskId },
returnParam: { taskId: session.taskId, updatedAt: session.updatedAt },
@@ -76,7 +76,7 @@ export async function putDashboardSessionByTask(
const session = toClient((await res.json()) as ServerDashboardSession);
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-大屏会话-保存",
optDetail: "CM助手-全量-大屏会话-保存",
url: `${BASE()}/by-task/${taskId}`,
reqParam: { taskId, hasTranscript: patch.transcript !== undefined, hasReportJson: patch.reportJson !== undefined },
returnParam: { taskId: session.taskId, updatedAt: session.updatedAt },
@@ -325,7 +325,7 @@ export async function listDrafts(): Promise<DraftMeta[]> {
const metas = (data.drafts ?? []).map(toMeta);
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-会商-查询草稿",
optDetail: "CM助手-全量-会商-查询草稿",
url: BASE(),
returnParam: { count: metas.length },
});
@@ -340,7 +340,7 @@ export async function getDraft(id: string): Promise<DraftRecord | null> {
const record = toRecord(data);
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-会商-查看草稿",
optDetail: "CM助手-全量-会商-查看草稿",
url: `${BASE()}/${id}`,
reqParam: { id },
returnParam: { id: record.id, title: record.title },
@@ -358,7 +358,7 @@ export async function createDraft(payload: DraftCreatePayload): Promise<DraftRec
const record = toRecord(data);
writeYLog({
optType: OptType.create,
optDetail: "B1-CM助手-会商-新建草稿",
optDetail: "CM助手-全量-会商-新建草稿",
url: BASE(),
reqParam: { title: payload.title, furthestStep: payload.furthestStep, taskId: payload.taskId ?? null },
returnParam: { id: record.id, title: record.title },
@@ -377,7 +377,7 @@ export async function updateDraft(id: string, patch: DraftUpdatePayload): Promis
const record = toRecord(data);
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-会商-更新草稿",
optDetail: "CM助手-全量-会商-更新草稿",
url: `${BASE()}/${id}`,
reqParam: { id, ...patch },
returnParam: { id: record.id, title: record.title },
@@ -393,7 +393,7 @@ export async function deleteDraft(id: string): Promise<void> {
}
writeYLog({
optType: OptType.delete,
optDetail: "B1-CM助手-会商-删除草稿",
optDetail: "CM助手-全量-会商-删除草稿",
url: `${BASE()}/${id}`,
reqParam: { id },
});
@@ -934,7 +934,7 @@ export async function listSessionArtifacts(
};
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-会商产物-查询",
optDetail: "CM助手-全量-会商产物-查询",
url: `/api/threads/${threadId}/artifacts`,
reqParam: { threadId },
returnParam: { count: result.files.length },
@@ -272,7 +272,7 @@ export async function listRecommendHistory(draftId: string): Promise<RecommendHi
const items = (data.history ?? []).map(toHistoryItem);
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-席位推荐-查询历史",
optDetail: "CM助手-全量-席位推荐-查询历史",
url: `/api/roundtable-drafts/${draftId}/recommendations`,
reqParam: { draftId },
returnParam: { count: items.length },
@@ -308,7 +308,7 @@ export async function saveRecommendHistory(
const item = toHistoryItem((await res.json()) as ServerHistoryItem);
writeYLog({
optType: OptType.create,
optDetail: "B1-CM助手-席位推荐-保存历史",
optDetail: "CM助手-全量-席位推荐-保存历史",
url: `/api/roundtable-drafts/${draftId}/recommendations`,
reqParam: { draftId, status: payload.status, picks: payload.picks?.length ?? 0 },
returnParam: { id: item.id },
@@ -133,7 +133,7 @@ export async function startJob(payload: StartJobPayload): Promise<RoundtableJob>
const job = mapJob(await readJson<Record<string, unknown>>(res, "start job"));
writeYLog({
optType: OptType.other,
optDetail: "B1-CM助手-会商-运行",
optDetail: "CM助手-全量-会商-运行",
url: BASE(),
reqParam: { draftId: payload.draftId ?? null, taskId: payload.taskId ?? null, model: payload.model },
returnParam: { id: job.id, status: job.status },
@@ -192,7 +192,7 @@ export async function resumeJob(jobId: string, answer: string, taskId?: string |
const job = mapJob(await readJson<Record<string, unknown>>(res, "resume job"));
writeYLog({
optType: OptType.other,
optDetail: "B1-CM助手-会商-回答澄清续跑",
optDetail: "CM助手-全量-会商-回答澄清续跑",
url: `${BASE()}/${jobId}/resume`,
reqParam: { jobId, taskId: taskId ?? null },
returnParam: { id: job.id, status: job.status },
@@ -206,7 +206,7 @@ export async function cancelJob(jobId: string, taskId?: string | null): Promise<
const job = mapJob(await readJson<Record<string, unknown>>(res, "cancel job"));
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-会商-取消",
optDetail: "CM助手-全量-会商-取消",
url: `${BASE()}/${jobId}/cancel`,
reqParam: { jobId, taskId: taskId ?? null },
returnParam: { id: job.id, status: job.status },
@@ -37,7 +37,7 @@ export async function listTaskDraftsByTask(taskId: string): Promise<DraftMeta[]>
const metas = (data.drafts ?? []).map(toMeta);
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-会商任务-查询草稿",
optDetail: "CM助手-全量-会商任务-查询草稿",
url: `${BASE()}/by-task/${taskId}`,
reqParam: { taskId },
returnParam: { count: metas.length },
@@ -53,7 +53,7 @@ export async function getLatestTaskDraftByTask(taskId: string): Promise<DraftRec
const record = toRecord(data);
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-会商任务-查看最新草稿",
optDetail: "CM助手-全量-会商任务-查看最新草稿",
url: `${BASE()}/by-task/${taskId}/latest`,
reqParam: { taskId },
returnParam: { id: record.id, title: record.title },
@@ -69,7 +69,7 @@ export async function getTaskDraft(id: string): Promise<DraftRecord | null> {
const record = toRecord(data);
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-会商任务-查看草稿",
optDetail: "CM助手-全量-会商任务-查看草稿",
url: `${BASE()}/${id}`,
reqParam: { id },
returnParam: { id: record.id, title: record.title },
@@ -88,7 +88,7 @@ export async function createTaskDraft(payload: DraftCreatePayload): Promise<Draf
const record = toRecord(data);
writeYLog({
optType: OptType.create,
optDetail: "B1-CM助手-会商任务-新建草稿",
optDetail: "CM助手-全量-会商任务-新建草稿",
url: BASE(),
reqParam: { taskId: payload.taskId ?? null, title: payload.title, furthestStep: payload.furthestStep },
returnParam: { id: record.id, title: record.title },
@@ -107,7 +107,7 @@ export async function updateTaskDraft(id: string, patch: DraftUpdatePayload): Pr
const record = toRecord(data);
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-会商任务-更新草稿",
optDetail: "CM助手-全量-会商任务-更新草稿",
url: `${BASE()}/${id}`,
reqParam: { id, ...patch },
returnParam: { id: record.id, title: record.title },
@@ -123,7 +123,7 @@ export async function deleteTaskDraft(id: string): Promise<void> {
}
writeYLog({
optType: OptType.delete,
optDetail: "B1-CM助手-会商任务-删除草稿",
optDetail: "CM助手-全量-会商任务-删除草稿",
url: `${BASE()}/${id}`,
reqParam: { id },
});
@@ -67,7 +67,7 @@ export async function listBusinessMappings(): Promise<BusinessMapping[]> {
const mappings = (data.mappings ?? []).map(toMapping);
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-业务映射-列表",
optDetail: "CM助手-全量-业务映射-列表",
url: "/api/business-mapping",
returnParam: mappings,
});
@@ -94,7 +94,7 @@ export async function updateBusinessMapping(
const mapping = toMapping(data);
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-业务映射-修改",
optDetail: "CM助手-全量-业务映射-修改",
url: `${BASE()}/${encodeURIComponent(businessCode)}`,
reqParam: { businessCode, patch },
returnParam: mapping,
@@ -160,7 +160,7 @@ export async function listLightApps(params: ListLightAppsParams = {}): Promise<L
const apps = (data.apps ?? []).map(toApp);
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-轻应用-列表",
optDetail: "CM助手-全量-轻应用-列表",
url: `${BASE()}${query ? `?${query}` : ""}`,
reqParam: params,
returnParam: apps,
@@ -173,7 +173,7 @@ export async function getLightApp(appId: string): Promise<LightApp> {
const app = toApp(await readJson<ServerApp>(res, "get light app"));
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-轻应用-查看",
optDetail: "CM助手-全量-轻应用-查看",
url: `${BASE()}/${encodeURIComponent(appId)}`,
reqParam: { appId },
returnParam: app,
@@ -190,7 +190,7 @@ export async function createLightApp(input: LightAppInput): Promise<LightApp> {
const app = toApp(await readJson<ServerApp>(res, "create light app"));
writeYLog({
optType: OptType.create,
optDetail: "B1-CM助手-轻应用-注册",
optDetail: "CM助手-全量-轻应用-注册",
url: BASE(),
reqParam: input,
returnParam: app,
@@ -210,7 +210,7 @@ export async function updateLightApp(
const app = toApp(await readJson<ServerApp>(res, "update light app"));
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-轻应用-修改",
optDetail: "CM助手-全量-轻应用-修改",
url: `${BASE()}/${encodeURIComponent(appId)}`,
reqParam: { appId, patch },
returnParam: app,
@@ -223,7 +223,7 @@ export async function deleteLightApp(appId: string): Promise<void> {
await readJson<{ success: boolean }>(res, "delete light app");
writeYLog({
optType: OptType.delete,
optDetail: "B1-CM助手-轻应用-删除",
optDetail: "CM助手-全量-轻应用-删除",
url: `${BASE()}/${encodeURIComponent(appId)}`,
reqParam: { appId },
});
@@ -71,7 +71,7 @@ export async function replaceMenuOverrides(overrides: MenuOverride[]): Promise<M
const saved = (data.overrides ?? []).map(toOverride);
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-菜单管理-保存",
optDetail: "CM助手-全量-菜单管理-保存",
url: "/api/menu-overrides",
reqParam: { overrides },
returnParam: saved,
@@ -75,7 +75,7 @@ export async function listSensitiveWords(): Promise<SensitiveWord[]> {
const words = (data.words ?? []).map(toWord);
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-敏感词管理-列表",
optDetail: "CM助手-全量-敏感词管理-列表",
url: "/api/sensitive-words",
returnParam: words,
});
@@ -92,7 +92,7 @@ export async function replaceSensitiveWords(words: SensitiveWord[]): Promise<Sen
const saved = (data.words ?? []).map(toWord);
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-敏感词管理-保存",
optDetail: "CM助手-全量-敏感词管理-保存",
url: "/api/sensitive-words",
reqParam: { words },
returnParam: saved,
@@ -201,7 +201,7 @@ export async function scanAddresses(kinds?: AddressKind[], skill?: string): Prom
const result = toScanResult(await readJson<any>(res, "扫描"));
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-技能地址管理-扫描",
optDetail: "CM助手-全量-技能地址管理-扫描",
url: `${BASE()}/scan${qs ? `?${qs}` : ""}`,
reqParam: { kinds, skill },
returnParam: result,
@@ -227,7 +227,7 @@ export async function scanAddressesStream(
if (!result) throw new Error("扫描未返回结果");
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-技能地址管理-扫描",
optDetail: "CM助手-全量-技能地址管理-扫描",
url: `${BASE()}/scan/stream${qs ? `?${qs}` : ""}`,
reqParam: { kinds: opts.kinds, skill: opts.skill },
returnParam: result,
@@ -245,7 +245,7 @@ export async function previewReplacements(replacements: Replacement[]): Promise<
const result = toPreviewResult(await readJson<any>(res, "预览"));
writeYLog({
optType: OptType.view,
optDetail: "B1-CM助手-技能地址管理-预览",
optDetail: "CM助手-全量-技能地址管理-预览",
url: `${BASE()}/preview`,
reqParam: { replacements },
returnParam: result,
@@ -277,7 +277,7 @@ export async function applyReplacementsStream(
if (!result) throw new Error("应用未返回结果");
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-技能地址管理-应用替换",
optDetail: "CM助手-全量-技能地址管理-应用替换",
url: `${BASE()}/apply/stream`,
reqParam: body,
returnParam: result,
@@ -297,7 +297,7 @@ export async function listBackups(): Promise<BackupEntry[]> {
}));
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-技能地址管理-修改记录",
optDetail: "CM助手-全量-技能地址管理-修改记录",
url: `${BASE()}/backups`,
returnParam: backups,
});
@@ -310,7 +310,7 @@ export async function rollbackBackup(backupId: string): Promise<{ restored: numb
const result = { restored: (data.restored_files ?? []).length };
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-技能地址管理-回退",
optDetail: "CM助手-全量-技能地址管理-回退",
url: `${BASE()}/rollback/${encodeURIComponent(backupId)}`,
reqParam: { backupId },
returnParam: result,
@@ -95,7 +95,7 @@ export async function replaceTaskButtons(buttons: TaskButton[]): Promise<TaskBut
const saved = (data.buttons ?? []).map(toButton);
writeYLog({
optType: OptType.edit,
optDetail: "B1-CM助手-按钮管理-保存",
optDetail: "CM助手-全量-按钮管理-保存",
url: "/api/task-buttons",
reqParam: { buttons },
returnParam: saved,
@@ -5,7 +5,9 @@
入库目标接口的字段名不固定:标题字段可能叫「名称」也可能叫「标题」,因此先按
「名称」发送一次,若判定失败则用 **同一个 nodeName** 把标题字段换成「标题」再
发一次。nodeName 是入库记录的唯一标识,取「md 标题 + 时间戳」。
发一次。nodeName 是入库记录的唯一标识,**只由 md 标题决定**(md5(标题)),不含
时间戳——同一篇 md(同标题)每次入库得到的 nodeName 都一致,避免重复入库时产生
多条记录。
"""
from __future__ import annotations
@@ -56,7 +58,7 @@ class KnowledgeIngestAttempt(BaseModel):
class KnowledgeIngestPublishResponse(BaseModel):
success: bool
template_name: str
node_name: str = Field(..., description="入库唯一标识(md 标题 + 时间戳),两次尝试共用")
node_name: str = Field(..., description="入库唯一标识(md5(标题),同标题每次入库一致),两次尝试共用")
batch_number: str
attempts: list[KnowledgeIngestAttempt]
@@ -99,10 +101,14 @@ def _build_payload(template_name: str, batch_number: str, node_name: str, title_
}
def _build_node_name(title: str, timestamp: str) -> str:
"""Build a stable node name as md5(title) + timestamp + .md."""
def _build_node_name(title: str) -> str:
"""Build a stable node name = md5(title) + .md.
nodeName 只由标题决定(不含时间戳),所以同一篇 md(同标题)每次入库都得到
完全相同的 nodeName —— 入库接口据此把重复上传当作同一条记录,而不是每次新增。
"""
title_md5 = hashlib.md5(title.encode("utf-8")).hexdigest()
return f"{title_md5}-{timestamp}.md"
return f"{title_md5}.md"
def _resolve_ingest_url() -> str:
@@ -132,10 +138,10 @@ async def publish_to_knowledge_base(request: KnowledgeIngestPublishRequest) -> K
raise HTTPException(status_code=400, detail=f"智能体 '{request.agent_id}' 未配置入库模板名(template_name),无法发布")
now = datetime.now()
# nodeName 是入库记录的唯一标识:md 标题 + 时间戳。两次尝试必须共用同一个
# nodeName,避免第一次实际已入库时第二次产生重复记录。
timestamp = now.strftime("%Y%m%d%H%M%S")
node_name = _build_node_name(title, timestamp)
# nodeName 是入库记录的唯一标识:只取 md5(标题),不含时间戳。同一篇 md(同标题)
# 每次入库得到一致的 nodeName —— 既保证两次尝试(名称/标题字段)共用同一个,也
# 保证重复上传同一篇 md 不会产生多条记录。
node_name = _build_node_name(title)
batch_number = f"md{now.strftime('%Y%m%d')}"
ingest_url = _resolve_ingest_url()
@@ -86,27 +86,6 @@ def _drop_archived_skills(skills: list[Skill]) -> list[Skill]:
return skills
def _run_coro_blocking(coro):
"""Run an async coroutine to completion from sync code.
Mirrors ``skill_tools._run_coro_blocking``: when already inside a running
event loop (e.g. prompt built during an async agent run), hand the coroutine
to a fresh loop in a worker thread; otherwise run it directly.
"""
try:
asyncio.get_running_loop()
in_loop = True
except RuntimeError:
in_loop = False
if in_loop:
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
return pool.submit(asyncio.run, coro).result()
return asyncio.run(coro)
def _load_always_on_names() -> set[str]:
"""Names marked 常驻(免压缩), read from the **DB** (skills table).
@@ -114,11 +93,11 @@ def _load_always_on_names() -> set[str]:
simply treats every skill as compressible (safe default).
"""
try:
from deerflow.persistence.engine import get_session_factory
from deerflow.persistence.engine import get_session_factory, run_db_blocking
from deerflow.persistence.skills import make_skill_store
store = make_skill_store(get_session_factory())
return set(_run_coro_blocking(store.list_always_on_names()))
return set(run_db_blocking(store.list_always_on_names()))
except Exception:
logger.debug("Failed to load always_on skill names from DB", exc_info=True)
return set()
@@ -20,7 +20,6 @@ not the retry-wrapped successful response.
from __future__ import annotations
import asyncio
import logging
import time
import uuid
@@ -38,7 +37,7 @@ from langchain.agents.middleware.types import (
from langchain_core.messages import AIMessage
from langgraph.errors import GraphBubbleUp
from deerflow.persistence.engine import get_session_factory
from deerflow.persistence.engine import fire_and_forget_db_write, get_session_factory
from deerflow.persistence.llm_metrics import LlmMetricRecord, make_llm_metrics_store
from deerflow.persistence.llm_metrics.base import LlmMetricsStore
from deerflow.runtime.user_context import get_effective_user_id
@@ -280,16 +279,13 @@ class LlmMetricsMiddleware(AgentMiddleware[AgentState]):
store = self._store()
if store is None:
return
# Route the best-effort write onto the engine's home loop. Doing
# ``asyncio.run(store.record(...))`` here (when the model call ran in a worker
# thread) would reuse the global engine's pooled connections on a fresh loop and
# raise "got Future attached to a different loop" — harmless (caught below) but
# log-spammy and pool-churning under concurrency. See ``fire_and_forget_db_write``.
try:
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = None
if loop is not None and loop.is_running():
# Fire-and-forget — never let metric IO block the request path.
loop.create_task(store.record(record))
else:
asyncio.run(store.record(record))
fire_and_forget_db_write(store.record(record))
except Exception:
logger.warning("LlmMetricsMiddleware: failed to flush metric %s", record.id, exc_info=True)
@@ -25,7 +25,6 @@ agent runtime keeps running.
from __future__ import annotations
import asyncio
import logging
import re
import time
@@ -41,7 +40,7 @@ from langgraph.errors import GraphBubbleUp
from langgraph.prebuilt.tool_node import ToolCallRequest
from langgraph.types import Command
from deerflow.persistence.engine import get_session_factory
from deerflow.persistence.engine import fire_and_forget_db_write, get_session_factory
from deerflow.persistence.tool_metrics import ToolMetricRecord, make_tool_metrics_store
from deerflow.persistence.tool_metrics.base import ToolMetricsStore
from deerflow.runtime.user_context import get_effective_user_id
@@ -262,16 +261,12 @@ class ToolMetricsMiddleware(AgentMiddleware[AgentState]):
store = self._store()
if store is None:
return
# Route the best-effort write onto the engine's home loop (a tool call that ran
# in a worker thread would otherwise do ``asyncio.run(store.record(...))`` and reuse
# the global engine's pooled connections cross-loop → "got Future attached to a
# different loop"). See ``fire_and_forget_db_write``.
try:
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = None
if loop is not None and loop.is_running():
# Fire-and-forget — never let metric IO block the tool path.
loop.create_task(store.record(record))
else:
asyncio.run(store.record(record))
fire_and_forget_db_write(store.record(record))
except Exception:
logger.warning("ToolMetricsMiddleware: failed to flush metric %s", record.id, exc_info=True)
@@ -32,6 +32,13 @@ logger = logging.getLogger(__name__)
_engine: AsyncEngine | None = None
_session_factory: async_sessionmaker[AsyncSession] | None = None
# The event loop on which the async engine was created. asyncmy/asyncpg connection
# pools bind their connections to the loop that opened them; awaiting a write on a
# *different* loop reuses those connections cross-loop and raises
# "got Future attached to a different loop". We capture the engine's home loop here so
# best-effort writes coming from worker threads / other loops can be routed back to it
# (see ``fire_and_forget_db_write``).
_engine_loop: asyncio.AbstractEventLoop | None = None
def _add_runtime_vendor_path() -> None:
@@ -293,12 +300,20 @@ async def init_engine(
pool_timeout: Seconds to wait for a free pooled connection before erroring (postgres/mysql).
sqlite_dir: Directory to create for SQLite (ensured to exist).
"""
global _engine, _session_factory
global _engine, _session_factory, _engine_loop
if backend == "memory":
logger.info("Persistence backend=memory -- ORM engine not initialized")
return
# Remember the loop that owns the engine's connection pool, so off-loop
# best-effort writes (metrics from worker threads, etc.) can be routed here
# instead of reusing pooled connections cross-loop.
try:
_engine_loop = asyncio.get_running_loop()
except RuntimeError:
_engine_loop = None
if backend == "postgres":
try:
import asyncpg # noqa: F401
@@ -459,11 +474,121 @@ def get_engine() -> AsyncEngine | None:
return _engine
def get_engine_loop() -> asyncio.AbstractEventLoop | None:
"""Return the event loop that owns the engine's connection pool (or None)."""
return _engine_loop
def fire_and_forget_db_write(coro) -> None:
"""Schedule a best-effort DB write on the engine's home loop, from any loop/thread.
The async engine's connection pool is bound to the loop that created it
(``init_engine``, normally the Gateway's main loop). Running a write on a
*different* loop — e.g. a model/tool call that LangGraph executed in a worker
thread, which then does ``asyncio.run(store.record(...))`` — reuses those pooled
connections cross-loop and raises ``got Future attached to a different loop``.
Under concurrency this happens constantly (many calls land in worker threads at
once), spamming the log and churning the pool; single-threaded it almost never does.
This routes every such write back onto the engine's home loop:
- already on that loop -> ``create_task`` (in-loop fire-and-forget);
- on another loop/thread -> ``run_coroutine_threadsafe`` to the home loop.
The write itself stays best-effort: if no engine loop is captured (memory backend
/ not initialized) the coroutine is closed (avoids a "never awaited" warning) and
the call is a no-op. Never raises.
"""
loop = _engine_loop
if loop is None or loop.is_closed():
close = getattr(coro, "close", None)
if callable(close):
close()
return
try:
running = asyncio.get_running_loop()
except RuntimeError:
running = None
try:
if running is loop:
loop.create_task(coro)
else:
asyncio.run_coroutine_threadsafe(coro, loop)
except Exception: # noqa: BLE001 — best-effort; scheduling must never break the caller
close = getattr(coro, "close", None)
if callable(close):
close()
logger.debug("fire_and_forget_db_write: failed to schedule DB write", exc_info=True)
def _run_coro_on_throwaway_loop(coro):
"""Last-resort: run *coro* to completion on a fresh event loop.
Only reached when there is no usable engine home loop, or when the caller is
*synchronously* inside the home loop (routing back would deadlock). This
touches the shared connection pool from a throwaway loop, so it can still leak
a dead-loop-bound connection — but the common worker-thread path no longer
gets here (see ``run_db_blocking``).
"""
try:
asyncio.get_running_loop()
in_loop = True
except RuntimeError:
in_loop = False
if in_loop:
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
return pool.submit(asyncio.run, coro).result()
return asyncio.run(coro)
def run_db_blocking(coro, *, timeout: float | None = 30.0):
"""Run an awaited DB coroutine from sync code, safe w.r.t. the engine's home loop.
The blocking-read sibling of :func:`fire_and_forget_db_write`: where that one
drops the result, this returns it — used by the sync→async bridges that must
read a row before proceeding (thread-path owner resolution, always-on skills,
skill visibility).
The async engine's connection pool is bound to the loop that created it
(``init_engine``). Running a DB coroutine on a *throwaway* loop
(``asyncio.run``) checks out pooled connections cross-loop and **leaks** them:
the connection ends up bound to a loop that is then closed, and when the pool
later recycles/disposes it, asyncmy's ``_terminate_graceful_close`` fires on the
wrong loop and raises ``got Future <...> attached to a different loop``. Under
concurrency (scheduler / subagent / sandbox-tool worker threads each spinning a
fresh loop) this floods the logs.
So when we are **off** the home loop — the common case, a worker thread with no
running loop — route the coroutine onto the home loop and block the *caller*
thread for its result. The pool is only ever touched on its own loop, so nothing
leaks. Only when there is no home loop, or we are synchronously inside it (where
blocking would deadlock), do we fall back to a throwaway loop.
Never swallows the coroutine's own exception — the sync bridges wrap this call in
their own try/except and degrade gracefully.
"""
loop = _engine_loop
try:
running = asyncio.get_running_loop()
except RuntimeError:
running = None
if loop is not None and not loop.is_closed() and running is not loop:
# Off the home loop (worker thread, or a different loop): execute the coro
# on the engine's loop and block this thread for the result. asyncmy's
# pooled connections stay on their own loop, so none leak cross-loop.
return asyncio.run_coroutine_threadsafe(coro, loop).result(timeout)
# No home loop, or we're already synchronously inside it: last resort.
return _run_coro_on_throwaway_loop(coro)
async def close_engine() -> None:
"""Dispose the engine, release all connections."""
global _engine, _session_factory
global _engine, _session_factory, _engine_loop
if _engine is not None:
await _engine.dispose()
logger.info("Persistence engine closed")
_engine = None
_session_factory = None
_engine_loop = None
@@ -28,7 +28,6 @@ safely to the pre-fix semantics.
from __future__ import annotations
import asyncio
import logging
import threading
@@ -106,27 +105,6 @@ def _is_creator_shared_thread(metadata: dict) -> bool:
return bool(metadata.get("taskId")) or metadata.get("thread_type") == "roundtable"
def _run_coro_blocking(coro):
"""Run an async coroutine to completion from sync code.
Mirrors ``lead_agent.prompt._run_coro_blocking``: when already inside a
running event loop, hand the coroutine to a fresh loop in a worker thread;
otherwise run it directly.
"""
try:
asyncio.get_running_loop()
in_loop = True
except RuntimeError:
in_loop = False
if in_loop:
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
return pool.submit(asyncio.run, coro).result()
return asyncio.run(coro)
def resolve_path_user_id(thread_id: str | None) -> str:
"""Canonical user bucket for *thread_id*'s files (sync).
@@ -140,7 +118,9 @@ def resolve_path_user_id(thread_id: str | None) -> str:
if cached is not None:
return cached
try:
owner = _run_coro_blocking(_fetch_task_owner(thread_id))
from deerflow.persistence.engine import run_db_blocking
owner = run_db_blocking(_fetch_task_owner(thread_id))
except Exception:
logger.debug("Failed to resolve task-shared owner for thread %s", thread_id, exc_info=True)
owner = None
@@ -75,30 +75,6 @@ def _resolve_effective_user_id() -> str | None:
return None
def _run_coro_blocking(coro):
"""在同步工具里把一个协程跑到完成。
``skill_list`` 是同步 ``@tool``:LangChain 通常在线程池里执行它(无运行中的
事件循环),但 curator 评审会在已有事件循环的线程里调用它。两种情况都要能
拿到 ``SkillStore`` 的异步查询结果 —— 检测到运行中的事件循环就转交独立线程
执行 ``asyncio.run``,否则直接跑。
"""
import asyncio
try:
asyncio.get_running_loop()
in_loop = True
except RuntimeError:
in_loop = False
if in_loop:
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
return pool.submit(asyncio.run, coro).result()
return asyncio.run(coro)
def _load_owned_skill_names(user_id: str | None) -> set[str] | None:
"""返回 ``owner_user_id == user_id`` 的自定义技能名集合。
@@ -110,14 +86,14 @@ def _load_owned_skill_names(user_id: str | None) -> set[str] | None:
if not user_id:
return None
try:
from deerflow.persistence.engine import get_session_factory
from deerflow.persistence.engine import get_session_factory, run_db_blocking
from deerflow.persistence.skills import make_skill_store
factory = get_session_factory()
if factory is None:
return None
store = make_skill_store(factory)
records = _run_coro_blocking(store.list_visible(user_id))
records = run_db_blocking(store.list_visible(user_id))
return {r["name"] for r in records if r.get("owner_user_id") == user_id}
except Exception:
logger.debug("Failed to load owned skill names for skill_list filter", exc_info=True)