Merge branch 'ji' into master
并入: loop 跨事件循环 DB 访问修复(asyncmy attached to a different loop)、y-log 前缀、全角参考文献角标识别 # Conflicts: # CLAUDE.md
This commit is contained in:
commit
1a87077642
54 files changed
+583
-400
No files matched your search
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -40,7 +40,7 @@ export async function updateArtifactContent({
|
||||
}
|
||||
writeYLog({
|
||||
optType: OptType.edit,
|
||||
optDetail: "B1-CM助手-产物-编辑产物内容",
|
||||
optDetail: "CM助手-全量-产物-编辑产物内容",
|
||||
url,
|
||||
reqParam: { filepath, threadId },
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ export enum OptType {
|
||||
export interface WriteYLogParams {
|
||||
/** 操作类型枚举。 */
|
||||
optType: OptType;
|
||||
/** 操作详情(简短描述),统一「B1-CM助手-xxx」。 */
|
||||
/** 操作详情(简短描述),统一「CM助手-全量-xxx」。 */
|
||||
optDetail: string;
|
||||
/** 操作命中的接口地址(完整或相对,内部自动转相对,去 ip/host)。 */
|
||||
url: string;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 },
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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) : "" },
|
||||
|
||||
@@ -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) : "" },
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
+2
-23
@@ -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()
|
||||
|
||||
+7
-11
@@ -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)
|
||||
|
||||
|
||||
+6
-11
@@ -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
|
||||
|
||||
+2
-26
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user