Merge branch 'ji' into master (操作日志 Y-Log 上报)
# Conflicts: # CLAUDE.md
This commit is contained in:
commit
053d3e300e
35 files changed
+1182
-125
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import { getBackendBaseURL } from "@/core/config";
|
||||
import { apiFetch } from "@/core/api";
|
||||
import { OptType, writeYLog } from "@/core/log/y-log";
|
||||
|
||||
import type {
|
||||
Agent,
|
||||
@@ -58,6 +59,13 @@ export async function listAgents(params: ListAgentsParams = {}): Promise<Agent[]
|
||||
);
|
||||
if (!res.ok) throw new Error(`Failed to load agents: ${res.statusText}`);
|
||||
const data = (await res.json()) as { agents: Agent[] };
|
||||
writeYLog({
|
||||
optType: OptType.search,
|
||||
optDetail: "B1-CM助手-智能体列表",
|
||||
url: `/api/agents${query ? `?${query}` : ""}`,
|
||||
reqParam: params,
|
||||
returnParam: data.agents,
|
||||
});
|
||||
return data.agents;
|
||||
}
|
||||
|
||||
@@ -76,12 +84,20 @@ export async function listAgentsPaginated(
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
};
|
||||
return {
|
||||
const result: PaginatedAgents = {
|
||||
agents: data.agents,
|
||||
total: data.total ?? data.agents.length,
|
||||
page: data.page ?? params.page ?? 1,
|
||||
pageSize: data.page_size ?? params.pageSize ?? data.agents.length,
|
||||
};
|
||||
writeYLog({
|
||||
optType: OptType.search,
|
||||
optDetail: "B1-CM助手-智能体列表",
|
||||
url: `/api/agents${query ? `?${query}` : ""}`,
|
||||
reqParam: params,
|
||||
returnParam: result,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function listAllAgentsAdmin(): Promise<Agent[]> {
|
||||
@@ -91,6 +107,12 @@ export async function listAllAgentsAdmin(): Promise<Agent[]> {
|
||||
throw new Error(err.detail ?? `Failed to load agents: ${res.statusText}`);
|
||||
}
|
||||
const data = (await res.json()) as { agents: Agent[] };
|
||||
writeYLog({
|
||||
optType: OptType.search,
|
||||
optDetail: "B1-CM助手-智能体列表",
|
||||
url: `/api/agents?admin=true`,
|
||||
returnParam: data.agents,
|
||||
});
|
||||
return data.agents;
|
||||
}
|
||||
|
||||
@@ -110,7 +132,15 @@ export async function createAgent(request: CreateAgentRequest): Promise<Agent> {
|
||||
const err = (await res.json().catch(() => ({}))) as { detail?: string };
|
||||
throw new Error(err.detail ?? `Failed to create agent: ${res.statusText}`);
|
||||
}
|
||||
return res.json() as Promise<Agent>;
|
||||
const created = (await res.json()) as Agent;
|
||||
writeYLog({
|
||||
optType: OptType.create,
|
||||
optDetail: "B1-CM助手-智能体-新建",
|
||||
url: `/api/agents`,
|
||||
reqParam: request,
|
||||
returnParam: created,
|
||||
});
|
||||
return created;
|
||||
}
|
||||
|
||||
export async function updateAgent(
|
||||
@@ -126,7 +156,15 @@ export async function updateAgent(
|
||||
const err = (await res.json().catch(() => ({}))) as { detail?: string };
|
||||
throw new Error(err.detail ?? `Failed to update agent: ${res.statusText}`);
|
||||
}
|
||||
return res.json() as Promise<Agent>;
|
||||
const updated = (await res.json()) as Agent;
|
||||
writeYLog({
|
||||
optType: OptType.edit,
|
||||
optDetail: "B1-CM助手-智能体-修改",
|
||||
url: `/api/agents/${id}`,
|
||||
reqParam: { id, ...request },
|
||||
returnParam: updated,
|
||||
});
|
||||
return updated;
|
||||
}
|
||||
|
||||
export async function deleteAgent(id: string): Promise<void> {
|
||||
@@ -134,6 +172,12 @@ export async function deleteAgent(id: string): Promise<void> {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (!res.ok) throw new Error(`Failed to delete agent: ${res.statusText}`);
|
||||
writeYLog({
|
||||
optType: OptType.delete,
|
||||
optDetail: "B1-CM助手-智能体-删除",
|
||||
url: `/api/agents/${id}`,
|
||||
reqParam: { id },
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAgentSelector(): Promise<AgentSelectorResponse> {
|
||||
@@ -150,6 +194,12 @@ export async function favoriteAgent(id: string): Promise<void> {
|
||||
const err = (await res.json().catch(() => ({}))) as { detail?: string };
|
||||
throw new Error(err.detail ?? `Failed to favorite agent: ${res.statusText}`);
|
||||
}
|
||||
writeYLog({
|
||||
optType: OptType.edit,
|
||||
optDetail: "B1-CM助手-智能体-收藏",
|
||||
url: `/api/agents/${id}/favorite`,
|
||||
reqParam: { id },
|
||||
});
|
||||
}
|
||||
|
||||
export async function unfavoriteAgent(id: string): Promise<void> {
|
||||
@@ -160,6 +210,12 @@ export async function unfavoriteAgent(id: string): Promise<void> {
|
||||
const err = (await res.json().catch(() => ({}))) as { detail?: string };
|
||||
throw new Error(err.detail ?? `Failed to unfavorite agent: ${res.statusText}`);
|
||||
}
|
||||
writeYLog({
|
||||
optType: OptType.edit,
|
||||
optDetail: "B1-CM助手-智能体-取消收藏",
|
||||
url: `/api/agents/${id}/favorite`,
|
||||
reqParam: { id },
|
||||
});
|
||||
}
|
||||
|
||||
export async function pinAgent(id: string): Promise<void> {
|
||||
@@ -170,6 +226,12 @@ export async function pinAgent(id: string): Promise<void> {
|
||||
const err = (await res.json().catch(() => ({}))) as { detail?: string };
|
||||
throw new Error(err.detail ?? `Failed to pin agent: ${res.statusText}`);
|
||||
}
|
||||
writeYLog({
|
||||
optType: OptType.edit,
|
||||
optDetail: "B1-CM助手-智能体-置顶",
|
||||
url: `/api/agents/${id}/pin`,
|
||||
reqParam: { id },
|
||||
});
|
||||
}
|
||||
|
||||
export async function unpinAgent(id: string): Promise<void> {
|
||||
@@ -180,6 +242,12 @@ export async function unpinAgent(id: string): Promise<void> {
|
||||
const err = (await res.json().catch(() => ({}))) as { detail?: string };
|
||||
throw new Error(err.detail ?? `Failed to unpin agent: ${res.statusText}`);
|
||||
}
|
||||
writeYLog({
|
||||
optType: OptType.edit,
|
||||
optDetail: "B1-CM助手-智能体-取消置顶",
|
||||
url: `/api/agents/${id}/pin`,
|
||||
reqParam: { id },
|
||||
});
|
||||
}
|
||||
|
||||
export async function reorderFeaturedAgents(agentIds: string[]): Promise<void> {
|
||||
@@ -197,6 +265,12 @@ export async function reorderFeaturedAgents(agentIds: string[]): Promise<void> {
|
||||
err.detail ?? `Failed to reorder featured agents: ${res.statusText}`,
|
||||
);
|
||||
}
|
||||
writeYLog({
|
||||
optType: OptType.edit,
|
||||
optDetail: "B1-CM助手-智能体-精选排序",
|
||||
url: `/api/agents/featured/order`,
|
||||
reqParam: { agent_ids: agentIds },
|
||||
});
|
||||
}
|
||||
|
||||
export async function listPrivateAgents(password: string): Promise<Agent[]> {
|
||||
@@ -215,6 +289,12 @@ export async function listPrivateAgents(password: string): Promise<Agent[]> {
|
||||
);
|
||||
}
|
||||
const data = (await res.json()) as { agents: Agent[] };
|
||||
writeYLog({
|
||||
optType: OptType.search,
|
||||
optDetail: "B1-CM助手-私有广场智能体列表",
|
||||
url: `/api/agents/private-square/list`,
|
||||
returnParam: data.agents,
|
||||
});
|
||||
return data.agents;
|
||||
}
|
||||
|
||||
@@ -261,12 +341,20 @@ export async function listPrivateAgentsPaginated(
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
};
|
||||
return {
|
||||
const result: PaginatedAgents = {
|
||||
agents: data.agents,
|
||||
total: data.total ?? data.agents.length,
|
||||
page: data.page ?? params.page ?? 1,
|
||||
pageSize: data.page_size ?? params.pageSize ?? data.agents.length,
|
||||
};
|
||||
writeYLog({
|
||||
optType: OptType.search,
|
||||
optDetail: "B1-CM助手-私有广场智能体列表",
|
||||
url: `/api/agents/private-square/list`,
|
||||
reqParam: { ...params, password: undefined },
|
||||
returnParam: result,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function setAgentPrivate(
|
||||
@@ -287,7 +375,15 @@ export async function setAgentPrivate(
|
||||
err.detail ?? `Failed to update agent privacy: ${res.statusText}`,
|
||||
);
|
||||
}
|
||||
return res.json() as Promise<Agent>;
|
||||
const updated = (await res.json()) as Agent;
|
||||
writeYLog({
|
||||
optType: OptType.edit,
|
||||
optDetail: "B1-CM助手-智能体-设置私有",
|
||||
url: `/api/agents/${id}/private`,
|
||||
reqParam: { id, is_private: isPrivate },
|
||||
returnParam: updated,
|
||||
});
|
||||
return updated;
|
||||
}
|
||||
|
||||
export async function setAgentFeatured(
|
||||
@@ -308,7 +404,15 @@ export async function setAgentFeatured(
|
||||
err.detail ?? `Failed to set featured: ${res.statusText}`,
|
||||
);
|
||||
}
|
||||
return res.json() as Promise<Agent>;
|
||||
const updated = (await res.json()) as Agent;
|
||||
writeYLog({
|
||||
optType: OptType.edit,
|
||||
optDetail: "B1-CM助手-智能体-设置精选",
|
||||
url: `/api/agents/${id}/featured`,
|
||||
reqParam: { id, order },
|
||||
returnParam: updated,
|
||||
});
|
||||
return updated;
|
||||
}
|
||||
|
||||
export interface AgentSkillsSyncRemoval {
|
||||
@@ -333,7 +437,14 @@ export async function adminSyncAgentSkills(): Promise<AgentSkillsSyncResult> {
|
||||
const err = (await res.json().catch(() => ({}))) as { detail?: string };
|
||||
throw new Error(err.detail ?? `Failed to sync agent skills: ${res.statusText}`);
|
||||
}
|
||||
return res.json() as Promise<AgentSkillsSyncResult>;
|
||||
const result = (await res.json()) as AgentSkillsSyncResult;
|
||||
writeYLog({
|
||||
optType: OptType.edit,
|
||||
optDetail: "B1-CM助手-智能体-同步技能",
|
||||
url: `/api/agents/admin/sync-skills`,
|
||||
returnParam: result,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export interface KnowledgeIngestAttempt {
|
||||
@@ -371,7 +482,15 @@ export async function publishArtifactToKnowledge(request: {
|
||||
const err = (await res.json().catch(() => ({}))) as { detail?: string };
|
||||
throw new Error(err.detail ?? `发布入库失败: ${res.statusText}`);
|
||||
}
|
||||
return res.json() as Promise<KnowledgeIngestPublishResult>;
|
||||
const result = (await res.json()) as KnowledgeIngestPublishResult;
|
||||
writeYLog({
|
||||
optType: OptType.export,
|
||||
optDetail: "B1-CM助手-智能体-产物发布入库",
|
||||
url: `/api/knowledge-ingest/publish`,
|
||||
reqParam: request,
|
||||
returnParam: result,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function checkAgentName(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { apiFetch } from "@/core/api/fetch-client";
|
||||
import { getBackendBaseURL } from "@/core/config";
|
||||
import { OptType, writeYLog } from "@/core/log/y-log";
|
||||
import { downloadMarkdownAsDocx } from "@/core/scheduled-tasks/markdown-docx";
|
||||
|
||||
function filenameFromContentDisposition(header: string | null): string | null {
|
||||
@@ -79,13 +80,31 @@ export async function downloadMarkdownDocxFromBackend(options: {
|
||||
|
||||
if (response.status === 404) {
|
||||
await downloadMarkdownAsDocx(markdown, options.title?.trim() || "document", options.title);
|
||||
writeYLog({
|
||||
optType: OptType.export,
|
||||
optDetail: "B1-CM助手-导出-Word",
|
||||
url: `${getBackendBaseURL()}/api/writing/export/docx`,
|
||||
reqParam: { title: options.title?.trim() || undefined },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await downloadBlobResponse(response, fallbackFilename);
|
||||
writeYLog({
|
||||
optType: OptType.export,
|
||||
optDetail: "B1-CM助手-导出-Word",
|
||||
url: `${getBackendBaseURL()}/api/writing/export/docx`,
|
||||
reqParam: { title: options.title?.trim() || undefined },
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof TypeError) {
|
||||
await downloadMarkdownAsDocx(markdown, options.title?.trim() || "document", options.title);
|
||||
writeYLog({
|
||||
optType: OptType.export,
|
||||
optDetail: "B1-CM助手-导出-Word",
|
||||
url: `${getBackendBaseURL()}/api/writing/export/docx`,
|
||||
reqParam: { title: options.title?.trim() || undefined },
|
||||
});
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
|
||||
@@ -23,8 +23,10 @@ export interface TaskDeeplinkConfig {
|
||||
actionTest: boolean;
|
||||
/** 目的树查询完整 URL(调用追加 ?taskId=)。 */
|
||||
purposeDetailUrl: string;
|
||||
/** 删除完整 URL(调用追加 ?taskId=)。 */
|
||||
/** 删除完整 URL(rwfx 与 xdfx 共用同一接口):rwfx 追加 ?taskId=、xdfx 追加 ?actionId=(另一参数不传)。 */
|
||||
deleteUrl: string;
|
||||
/** xdfx「行动历史是否存在」查询完整 URL(调用追加 ?actionId=);留空 → 前端视作无历史、不阻断删除(地址待提供)。 */
|
||||
actionHistoryUrl: string;
|
||||
/** B 系统树形完整编辑页 URL(跳转追加 ?id=)。 */
|
||||
treemapEditUrl: string;
|
||||
/** B 系统树形下一步页 URL(跳转追加 ?id=)。 */
|
||||
@@ -50,10 +52,12 @@ export const DEFAULT_TASK_DEEPLINK_CONFIG: TaskDeeplinkConfig = {
|
||||
deleteTest: false,
|
||||
actionTest: false,
|
||||
purposeDetailUrl: "https://ch1.b.uat.4.cn/consumer/taskAnalyseSearch/getTaskPurposeDetail",
|
||||
deleteUrl: "https://ch1.b.uat.4.cn/consumer/taskAnalyseSearch/delete",
|
||||
deleteUrl: "https://ch1.b.uat.4.cn/bwjt/dropSixOrSevenData",
|
||||
treemapEditUrl: "https://ch1.b.uat.4.cn/web/#/task/item-aicoh/step-mode-treemap-full-edit",
|
||||
treemapNextUrl: "https://ch1.b.uat.4.cn/web/#/task/item-aicoh/step-mode-treemap-full-next",
|
||||
actionDetailUrl: "https://ch1.b.uat.4.cn/consumer/plan/action",
|
||||
// 行动历史查询地址待提供:空字符串 → 前端不查询、视作无历史(不阻断删除)。
|
||||
actionHistoryUrl: "",
|
||||
detail3qUrl: "",
|
||||
mock: {},
|
||||
};
|
||||
@@ -74,6 +78,8 @@ function normalize(body: unknown): TaskDeeplinkConfig {
|
||||
treemapEditUrl: str(o.treemap_edit_url, d.treemapEditUrl),
|
||||
treemapNextUrl: str(o.treemap_next_url, d.treemapNextUrl),
|
||||
actionDetailUrl: str(o.action_detail_url, d.actionDetailUrl),
|
||||
// action_history_url 允许为空(地址待提供):空 → 前端不查询、视作无历史。不回退默认。
|
||||
actionHistoryUrl: typeof o.action_history_url === "string" ? o.action_history_url : d.actionHistoryUrl,
|
||||
// detail_3q_url 允许为空(地址待定),不回退默认;只在配了非空时才生效。
|
||||
detail3qUrl: typeof o.detail_3q_url === "string" ? o.detail_3q_url : d.detail3qUrl,
|
||||
mock: {
|
||||
|
||||
@@ -254,20 +254,35 @@ export interface DeleteTaskPurposeResult {
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除任务目的树(覆盖前清空 / 手动清除任务记录都走这里)。``delete_test=true`` 时
|
||||
* 用后端配置里的假数据(接口未开发完时用);否则打真实接口。
|
||||
* 删除记录的「按哪种 id 删」:
|
||||
* - ``task``(默认,rwfx):传 ``?taskId=``,不传 actionId;
|
||||
* - ``action``(xdfx 行动分析):传 ``?actionId=``,不传 taskId。
|
||||
* rwfx 与 xdfx 共用同一个删除接口(``delete_url`` = dropSixOrSevenData);
|
||||
* 「不传」= 该参数**根本不出现在 URL 里**(只把要传的那个 key 放进 params,axios 自然省略另一个),
|
||||
* 不是传空值/None。
|
||||
*/
|
||||
export type DeleteIdKind = "task" | "action";
|
||||
|
||||
/**
|
||||
* 删除任务/行动记录(rwfx 覆盖前清空 / 手动清除 / xdfx 清除行动记录都走这里)。
|
||||
* ``delete_test=true`` 时用后端配置里的假数据(接口未开发完时用);否则打真实接口
|
||||
* (rwfx 传 ``taskId``、xdfx 传 ``actionId``,见 ``DeleteIdKind``)。
|
||||
*/
|
||||
export async function deleteTaskPurpose(
|
||||
taskId: string,
|
||||
id: string,
|
||||
opts?: { idKind?: DeleteIdKind },
|
||||
): Promise<DeleteTaskPurposeResult> {
|
||||
if (!taskId) return { message: "缺少 taskId", success: false };
|
||||
const idKind = opts?.idKind ?? "task";
|
||||
if (!id) return { message: idKind === "action" ? "缺少 actionId" : "缺少 taskId", success: false };
|
||||
const cfg = await fetchTaskDeeplinkConfig();
|
||||
if (cfg.deleteTest) {
|
||||
const m = cfg.mock.delete ?? {};
|
||||
return { message: typeof m.message === "string" ? m.message : "", success: m.success === true };
|
||||
}
|
||||
if (!cfg.deleteUrl) return { message: "缺少删除接口地址", success: false };
|
||||
const body = await requestCrudService.get(cfg.deleteUrl, { taskId });
|
||||
// 只放要传的那个 id key:另一参数直接不出现在 URL(axios 不会序列化缺省 key)。
|
||||
const params = idKind === "action" ? { actionId: id } : { taskId: id };
|
||||
const body = await requestCrudService.get(cfg.deleteUrl, params);
|
||||
const obj =
|
||||
body && typeof body === "object" ? (body as unknown as Record<string, unknown>) : {};
|
||||
return {
|
||||
@@ -276,6 +291,36 @@ export async function deleteTaskPurpose(
|
||||
};
|
||||
}
|
||||
|
||||
export interface ActionHistoryResult {
|
||||
/** 是否存在可删除的「行动历史」记录。 */
|
||||
exists: boolean;
|
||||
/** 是否成功判定。接口未配置 / 请求失败 → false(调用方据此**不**做「无历史」拦截,仍允许删除)。 */
|
||||
known: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 xdfx「行动历史是否存在」(清除行动记录前的预检),按「行动id」调用。
|
||||
*
|
||||
* 查询接口(``action_history_url``)地址**目前待提供**:为空时直接返回
|
||||
* ``{ exists: false, known: false }``——视作空白、不查询,也不阻断删除;后端配上真实地址后自动生效。
|
||||
*
|
||||
* ⚠️ 真实返回结构待定:拿到接口后按实际字段调整「是否存在」的判定(当前暂按 ``data`` 非空兜底)。
|
||||
*/
|
||||
export async function fetchActionHistoryExists(actionId: string): Promise<ActionHistoryResult> {
|
||||
if (!actionId) return { exists: false, known: false };
|
||||
const cfg = await fetchTaskDeeplinkConfig();
|
||||
if (!cfg.actionHistoryUrl) return { exists: false, known: false };
|
||||
try {
|
||||
const body = await requestCrudService.get(cfg.actionHistoryUrl, { actionId });
|
||||
// TODO(接口就绪):按真实返回结构判定。暂按整包 body.data 非空作「存在历史」。
|
||||
const data = body && typeof body === "object" ? (body as { data?: unknown }).data : null;
|
||||
const exists = Array.isArray(data) ? data.length > 0 : data != null && data !== "";
|
||||
return { exists, known: true };
|
||||
} catch {
|
||||
return { exists: false, known: false };
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------- 跳转地址 ----------------------------- */
|
||||
|
||||
/** B 系统「树形完整编辑」页地址(问答后「编辑」按钮,新标签打开)。用同步缓存配置。 */
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { apiFetch } from "@/core/api";
|
||||
import { getBackendBaseURL } from "@/core/config";
|
||||
import { OptType, writeYLog } from "@/core/log/y-log";
|
||||
|
||||
import type {
|
||||
BatchIngestPayload,
|
||||
@@ -41,9 +42,17 @@ export async function listKnowledgeNotes(params: KnowledgeListParams = {}) {
|
||||
if (params.folder !== undefined) qs.set("folder", params.folder);
|
||||
qs.set("limit", String(params.limit ?? 20));
|
||||
qs.set("offset", String(params.offset ?? 0));
|
||||
return parseResponse<KnowledgeListResponse>(
|
||||
const result = await parseResponse<KnowledgeListResponse>(
|
||||
await apiFetch(`${baseUrl()}/notes?${qs.toString()}`),
|
||||
);
|
||||
writeYLog({
|
||||
optType: OptType.search,
|
||||
optDetail: "B1-CM助手-知识库-检索",
|
||||
url: `${baseUrl()}/notes?${qs.toString()}`,
|
||||
reqParam: params,
|
||||
returnParam: result,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getKnowledgeNote(noteId: string) {
|
||||
@@ -67,6 +76,13 @@ export async function createKnowledgeNote(data: {
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
);
|
||||
writeYLog({
|
||||
optType: OptType.create,
|
||||
optDetail: "B1-CM助手-知识库-新建",
|
||||
url: `${baseUrl()}/notes`,
|
||||
reqParam: data,
|
||||
returnParam: res.note,
|
||||
});
|
||||
return res.note;
|
||||
}
|
||||
|
||||
@@ -81,22 +97,38 @@ export async function updateKnowledgeNote(
|
||||
folder: string | null;
|
||||
}>,
|
||||
) {
|
||||
return parseResponse<KnowledgeNote>(
|
||||
const result = await parseResponse<KnowledgeNote>(
|
||||
await apiFetch(`${baseUrl()}/notes/${encodeURIComponent(noteId)}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
);
|
||||
writeYLog({
|
||||
optType: OptType.edit,
|
||||
optDetail: "B1-CM助手-知识库-修改",
|
||||
url: `${baseUrl()}/notes/${encodeURIComponent(noteId)}`,
|
||||
reqParam: { noteId, data },
|
||||
returnParam: result,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function deleteKnowledgeNote(noteId: string, hard = false) {
|
||||
return parseResponse<{ ok: boolean; note_id: string }>(
|
||||
const result = await parseResponse<{ ok: boolean; note_id: string }>(
|
||||
await apiFetch(
|
||||
`${baseUrl()}/notes/${encodeURIComponent(noteId)}${hard ? "?hard=true" : ""}`,
|
||||
{ method: "DELETE" },
|
||||
),
|
||||
);
|
||||
writeYLog({
|
||||
optType: OptType.delete,
|
||||
optDetail: "B1-CM助手-知识库-删除",
|
||||
url: `${baseUrl()}/notes/${encodeURIComponent(noteId)}${hard ? "?hard=true" : ""}`,
|
||||
reqParam: { noteId, hard },
|
||||
returnParam: result,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function searchKnowledge(query: string, limit = 8) {
|
||||
@@ -107,11 +139,18 @@ export async function searchKnowledge(query: string, limit = 8) {
|
||||
body: JSON.stringify({ query, mode: "keyword", limit }),
|
||||
}),
|
||||
);
|
||||
writeYLog({
|
||||
optType: OptType.search,
|
||||
optDetail: "B1-CM助手-知识库-检索",
|
||||
url: `${baseUrl()}/search`,
|
||||
reqParam: { query, mode: "keyword", limit },
|
||||
returnParam: res.items,
|
||||
});
|
||||
return res.items;
|
||||
}
|
||||
|
||||
export async function ingestThread(threadId: string, payload: IngestThreadPayload = {}) {
|
||||
return parseResponse<{
|
||||
const result = await parseResponse<{
|
||||
note?: KnowledgeNote;
|
||||
created?: boolean;
|
||||
queued?: boolean;
|
||||
@@ -122,16 +161,32 @@ export async function ingestThread(threadId: string, payload: IngestThreadPayloa
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
);
|
||||
writeYLog({
|
||||
optType: OptType.import,
|
||||
optDetail: "B1-CM助手-知识库-导入",
|
||||
url: `${baseUrl()}/ingest/thread/${encodeURIComponent(threadId)}`,
|
||||
reqParam: { threadId, payload },
|
||||
returnParam: result,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function ingestThreadsBatch(payload: BatchIngestPayload = {}) {
|
||||
return parseResponse<BatchIngestResult>(
|
||||
const result = await parseResponse<BatchIngestResult>(
|
||||
await apiFetch(`${baseUrl()}/ingest/threads/batch`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
);
|
||||
writeYLog({
|
||||
optType: OptType.import,
|
||||
optDetail: "B1-CM助手-知识库-导入",
|
||||
url: `${baseUrl()}/ingest/threads/batch`,
|
||||
reqParam: payload,
|
||||
returnParam: result,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export interface IngestFileResult {
|
||||
@@ -162,9 +217,17 @@ export async function ingestKnowledgeFile(
|
||||
if (opts.template_id) form.append("template_id", opts.template_id);
|
||||
if (opts.folder) form.append("folder", opts.folder);
|
||||
// No Content-Type header — the browser sets the multipart boundary itself.
|
||||
return parseResponse<IngestFileResult>(
|
||||
const result = await parseResponse<IngestFileResult>(
|
||||
await apiFetch(`${baseUrl()}/ingest/file`, { method: "POST", body: form }),
|
||||
);
|
||||
writeYLog({
|
||||
optType: OptType.import,
|
||||
optDetail: "B1-CM助手-知识库-导入",
|
||||
url: `${baseUrl()}/ingest/file`,
|
||||
reqParam: { fileName: file.name, ...opts },
|
||||
returnParam: result,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Resolve `[[wikilink]]` targets to note ids / vault pages. */
|
||||
@@ -197,9 +260,16 @@ export async function fetchVaultPage(path: string) {
|
||||
}
|
||||
|
||||
export async function exportKnowledgeGraph() {
|
||||
return parseResponse<GraphExportResult>(
|
||||
const result = await parseResponse<GraphExportResult>(
|
||||
await apiFetch(`${baseUrl()}/export/graph`, { method: "POST" }),
|
||||
);
|
||||
writeYLog({
|
||||
optType: OptType.export,
|
||||
optDetail: "B1-CM助手-知识库-导出",
|
||||
url: `${baseUrl()}/export/graph`,
|
||||
returnParam: result,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Absolute URL of a served wiki-export file (for iframe / download). */
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 操作日志(Y-Log)上报的运行时配置:从后端公开只读接口
|
||||
* ``GET /api/public/config/y-log`` 读取(地址 + 开关在后端 ``config.yaml`` 的
|
||||
* ``y_log`` 段,改完即时生效,前端无需重建)。
|
||||
*
|
||||
* 仿 [[task-deeplink-config]]:
|
||||
* - ``fetchYLogConfig()`` 拉取并模块级缓存(启动早期预热一次即可)。
|
||||
* - ``getCachedYLogConfig()`` 同步读缓存(``writeYLog`` 即发即忘,必须同步可用)。
|
||||
*
|
||||
* 拉取失败 → 用内置默认值兜底(``enabled:false``,但保留默认地址),绝不抛错。
|
||||
*/
|
||||
|
||||
import { apiFetch } from "@/core/api/fetch-client";
|
||||
import { getBackendBaseURL } from "@/core/config";
|
||||
|
||||
export interface YLogConfig {
|
||||
/** 是否启用上报。 */
|
||||
enabled: boolean;
|
||||
/** 上报外部接口完整地址(POST)。 */
|
||||
writeUrl: string;
|
||||
}
|
||||
|
||||
/** 内置默认地址(后端拿不到 / 未配置时的兜底地址;enabled 默认 false 不上报)。 */
|
||||
const DEFAULT_WRITE_URL = "https://ch1.b.uat.4.cn/api/y-log/write";
|
||||
|
||||
/** 内置默认配置(拉取失败兜底;默认不上报)。 */
|
||||
export const DEFAULT_Y_LOG_CONFIG: YLogConfig = {
|
||||
enabled: false,
|
||||
writeUrl: DEFAULT_WRITE_URL,
|
||||
};
|
||||
|
||||
/** 后端 snake_case 响应 → 前端 camelCase;缺字段回退默认值。 */
|
||||
function normalize(body: unknown): YLogConfig {
|
||||
const o = body && typeof body === "object" ? (body as Record<string, unknown>) : {};
|
||||
const writeUrl =
|
||||
typeof o.write_url === "string" && o.write_url.trim() ? o.write_url.trim() : DEFAULT_WRITE_URL;
|
||||
return {
|
||||
enabled: o.enabled === true,
|
||||
writeUrl,
|
||||
};
|
||||
}
|
||||
|
||||
let cached: YLogConfig | null = null;
|
||||
let inflight: Promise<YLogConfig> | null = null;
|
||||
|
||||
/** 拉取(带模块级缓存 + 并发去重);失败回退默认值。 */
|
||||
export async function fetchYLogConfig(): Promise<YLogConfig> {
|
||||
if (cached) return cached;
|
||||
if (inflight) return inflight;
|
||||
inflight = (async () => {
|
||||
try {
|
||||
const res = await apiFetch(`${getBackendBaseURL()}/api/public/config/y-log`);
|
||||
if (!res.ok) throw new Error(`y-log config ${res.status}`);
|
||||
cached = normalize(await res.json());
|
||||
} catch {
|
||||
cached = { ...DEFAULT_Y_LOG_CONFIG };
|
||||
} finally {
|
||||
inflight = null;
|
||||
}
|
||||
return cached;
|
||||
})();
|
||||
return inflight;
|
||||
}
|
||||
|
||||
/** 同步读缓存(未拉取过则返回默认值,默认 enabled:false 不上报)。 */
|
||||
export function getCachedYLogConfig(): YLogConfig {
|
||||
return cached ?? DEFAULT_Y_LOG_CONFIG;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* token1(token 登录用的上游 token,即 URL 参数 ``?authToken=``)的存取。
|
||||
*
|
||||
* 复用 [[scoped-storage]] 的 ``authStorage*``:嵌入(iframe)态自动落 sessionStorage、
|
||||
* 正常态落 localStorage,使 iframe 里登录的用户 token1 与正常标签页**完全隔离**、
|
||||
* 互不污染(与其它鉴权键同一隔离语义)。
|
||||
*
|
||||
* 只有用 token 登录(存在 token1)才上报操作日志;非 token 登录不写 token1,
|
||||
* 该会话从此不上报(见 [[y-log]])。
|
||||
*/
|
||||
|
||||
import {
|
||||
authStorageGet,
|
||||
authStorageRemove,
|
||||
authStorageSet,
|
||||
} from "@/core/auth/scoped-storage";
|
||||
|
||||
const Y_LOG_TOKEN_KEY = "ylog.token1";
|
||||
|
||||
/** 持久化 token1(token 登录成功后调用;空值忽略)。 */
|
||||
export function persistYLogToken(token: string | null | undefined): void {
|
||||
const t = token?.trim();
|
||||
if (t) authStorageSet(Y_LOG_TOKEN_KEY, t);
|
||||
}
|
||||
|
||||
/** 读 token1(无则返回 null → 不上报)。 */
|
||||
export function readYLogToken(): string | null {
|
||||
return authStorageGet(Y_LOG_TOKEN_KEY);
|
||||
}
|
||||
|
||||
/** 清除 token1(登出时调用)。 */
|
||||
export function clearYLogToken(): void {
|
||||
authStorageRemove(Y_LOG_TOKEN_KEY);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* 操作日志(Y-Log)上报主函数。
|
||||
*
|
||||
* 把用户的关键操作(点击/查询/查看/增删改/启停/导入导出/登录登出/问答等)上报到
|
||||
* 外部日志接口(见 [[y-log-config]] 的 ``writeUrl``)。**前端直连**该接口,不经后端代理。
|
||||
*
|
||||
* 硬性约束(务必满足):
|
||||
* ① **token 门控**:无 token1(非 token 登录)→ 直接 return,不发请求。
|
||||
* ② **配置门控**:``enabled=false`` 或 ``writeUrl`` 为空 → 不发请求。
|
||||
* ③ **异步即发即忘**:``void fetch(...)`` 不被 ``await``,绝不阻塞业务/UI。
|
||||
* ④ **永不抛错**:整体包在 ``try/catch`` 中,任何异常(取 token/读配置/序列化/网络)静默吞掉。
|
||||
* ⑤ **只报成功**:埋点只放在操作成功路径,``optResult`` 恒为 ``0``。
|
||||
* ⑥ **失败静默**:``fetch`` 的 ``.catch`` 吞掉网络/跨域失败。
|
||||
*/
|
||||
|
||||
import { getCachedYLogConfig } from "@/core/log/y-log-config";
|
||||
import { readYLogToken } from "@/core/log/y-log-token";
|
||||
|
||||
/** 操作类型,数值与外部系统约定一致,不可改。 */
|
||||
export enum OptType {
|
||||
login = 0,
|
||||
logout = 1,
|
||||
search = 3,
|
||||
view = 4,
|
||||
edit = 5,
|
||||
delete = 6,
|
||||
import = 7,
|
||||
export = 8,
|
||||
create = 9,
|
||||
disabled = 10,
|
||||
enabled = 11,
|
||||
other = 12,
|
||||
}
|
||||
|
||||
export interface WriteYLogParams {
|
||||
/** 操作类型枚举。 */
|
||||
optType: OptType;
|
||||
/** 操作详情(简短描述),统一「B1-CM助手-xxx」。 */
|
||||
optDetail: string;
|
||||
/** 操作命中的接口地址(完整或相对,内部自动转相对,去 ip/host)。 */
|
||||
url: string;
|
||||
/** 入参(对象自动 JSON 序列化)。 */
|
||||
reqParam?: unknown;
|
||||
/** 返回参数(对象自动 JSON 序列化)。 */
|
||||
returnParam?: unknown;
|
||||
/** 登出 / 页面卸载等场景置 true,保证请求在卸载时仍能发出。 */
|
||||
keepalive?: boolean;
|
||||
}
|
||||
|
||||
/** 入参/返回体积上限,超出裁剪。 */
|
||||
const MAX_PARAM_LEN = 50_000;
|
||||
|
||||
/** 上报一条操作日志(异步即发即忘,永不抛错)。 */
|
||||
export function writeYLog(params: WriteYLogParams): void {
|
||||
try {
|
||||
const token = readYLogToken();
|
||||
if (!token) return; // ① 无 token1 不上报
|
||||
const { enabled, writeUrl } = getCachedYLogConfig();
|
||||
if (!enabled || !writeUrl) return; // ② 配置门控
|
||||
if (typeof fetch === "undefined") return;
|
||||
|
||||
const body = {
|
||||
optType: params.optType,
|
||||
optDetail: params.optDetail ?? "",
|
||||
url: toRelativeUrl(params.url ?? ""), // ③ 去 ip/host
|
||||
reqParam: toParamString(params.reqParam), // ④ 对象→字符串+裁剪
|
||||
returnParam: toParamString(params.returnParam),
|
||||
optResult: 0, // ⑤ 恒为 0(只报成功)
|
||||
};
|
||||
|
||||
void fetch(writeUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
keepalive: params.keepalive ?? false,
|
||||
}).catch(() => undefined); // ⑥ 失败静默
|
||||
} catch {
|
||||
/* 绝不影响业务 */
|
||||
}
|
||||
}
|
||||
|
||||
function toParamString(v: unknown): string {
|
||||
if (v == null) return "";
|
||||
const s = typeof v === "string" ? v : safeStringify(v);
|
||||
return s.length > MAX_PARAM_LEN ? `${s.slice(0, MAX_PARAM_LEN)}…[truncated]` : s;
|
||||
}
|
||||
|
||||
function safeStringify(v: unknown): string {
|
||||
try {
|
||||
return JSON.stringify(v) ?? "";
|
||||
} catch {
|
||||
return String(v);
|
||||
}
|
||||
}
|
||||
|
||||
/** 去掉 ip/host,只留 path + query。 */
|
||||
function toRelativeUrl(url: string): string {
|
||||
if (!url) return "";
|
||||
try {
|
||||
const base = typeof window !== "undefined" ? window.location.origin : "http://localhost";
|
||||
const u = new URL(url, base);
|
||||
return `${u.pathname}${u.search}`;
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { apiFetch } from "@/core/api";
|
||||
import { getBackendBaseURL } from "@/core/config";
|
||||
import { OptType, writeYLog } from "@/core/log/y-log";
|
||||
|
||||
import type {
|
||||
HtmlPageFavorite,
|
||||
@@ -42,9 +43,17 @@ export async function listScheduledTasks(params: ListScheduledTasksParams = {})
|
||||
if (params.search) qs.set("search", params.search);
|
||||
for (const id of params.tagIds ?? []) qs.append("tag_ids", id);
|
||||
const query = qs.toString();
|
||||
return parseResponse<ScheduledTask[]>(
|
||||
const result = await parseResponse<ScheduledTask[]>(
|
||||
await apiFetch(`${baseUrl()}${query ? `?${query}` : ""}`),
|
||||
);
|
||||
writeYLog({
|
||||
optType: OptType.search,
|
||||
optDetail: "B1-CM助手-定时任务-列表",
|
||||
url: `${baseUrl()}${query ? `?${query}` : ""}`,
|
||||
reqParam: params,
|
||||
returnParam: result,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function createScheduledTask(data: {
|
||||
@@ -57,13 +66,21 @@ export async function createScheduledTask(data: {
|
||||
require_markdown?: boolean;
|
||||
execution_context?: ScheduledTaskExecutionContext | null;
|
||||
}) {
|
||||
return parseResponse<ScheduledTask>(
|
||||
const result = await parseResponse<ScheduledTask>(
|
||||
await apiFetch(baseUrl(), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
);
|
||||
writeYLog({
|
||||
optType: OptType.create,
|
||||
optDetail: "B1-CM助手-定时任务-新建",
|
||||
url: baseUrl(),
|
||||
reqParam: data,
|
||||
returnParam: result,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function listScheduledTaskRuns(taskId: string) {
|
||||
@@ -73,9 +90,17 @@ export async function listScheduledTaskRuns(taskId: string) {
|
||||
}
|
||||
|
||||
export async function getScheduledTaskRun(runId: string) {
|
||||
return parseResponse<ScheduledTaskRun>(
|
||||
const result = await parseResponse<ScheduledTaskRun>(
|
||||
await apiFetch(`${baseUrl()}/runs/${encodeURIComponent(runId)}`),
|
||||
);
|
||||
writeYLog({
|
||||
optType: OptType.view,
|
||||
optDetail: "B1-CM助手-定时任务-运行详情",
|
||||
url: `${baseUrl()}/runs/${encodeURIComponent(runId)}`,
|
||||
reqParam: { runId },
|
||||
returnParam: result,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function deleteScheduledTaskRun(runId: string) {
|
||||
@@ -219,45 +244,85 @@ export async function updateScheduledTask(
|
||||
execution_context: ScheduledTaskExecutionContext | null;
|
||||
}>,
|
||||
) {
|
||||
return parseResponse<ScheduledTask>(
|
||||
const result = await parseResponse<ScheduledTask>(
|
||||
await apiFetch(`${baseUrl()}/${encodeURIComponent(taskId)}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
);
|
||||
writeYLog({
|
||||
optType: OptType.edit,
|
||||
optDetail: "B1-CM助手-定时任务-修改",
|
||||
url: `${baseUrl()}/${encodeURIComponent(taskId)}`,
|
||||
reqParam: { taskId, data },
|
||||
returnParam: result,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function deleteScheduledTask(taskId: string) {
|
||||
return parseResponse<{ success: boolean }>(
|
||||
const result = await parseResponse<{ success: boolean }>(
|
||||
await apiFetch(`${baseUrl()}/${encodeURIComponent(taskId)}`, {
|
||||
method: "DELETE",
|
||||
}),
|
||||
);
|
||||
writeYLog({
|
||||
optType: OptType.delete,
|
||||
optDetail: "B1-CM助手-定时任务-删除",
|
||||
url: `${baseUrl()}/${encodeURIComponent(taskId)}`,
|
||||
reqParam: { taskId },
|
||||
returnParam: result,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function pauseScheduledTask(taskId: string) {
|
||||
return parseResponse<ScheduledTask>(
|
||||
const result = await parseResponse<ScheduledTask>(
|
||||
await apiFetch(`${baseUrl()}/${encodeURIComponent(taskId)}/pause`, {
|
||||
method: "POST",
|
||||
}),
|
||||
);
|
||||
writeYLog({
|
||||
optType: OptType.disabled,
|
||||
optDetail: "B1-CM助手-定时任务-暂停",
|
||||
url: `${baseUrl()}/${encodeURIComponent(taskId)}/pause`,
|
||||
reqParam: { taskId },
|
||||
returnParam: result,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function resumeScheduledTask(taskId: string) {
|
||||
return parseResponse<ScheduledTask>(
|
||||
const result = await parseResponse<ScheduledTask>(
|
||||
await apiFetch(`${baseUrl()}/${encodeURIComponent(taskId)}/resume`, {
|
||||
method: "POST",
|
||||
}),
|
||||
);
|
||||
writeYLog({
|
||||
optType: OptType.enabled,
|
||||
optDetail: "B1-CM助手-定时任务-恢复",
|
||||
url: `${baseUrl()}/${encodeURIComponent(taskId)}/resume`,
|
||||
reqParam: { taskId },
|
||||
returnParam: result,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function triggerScheduledTask(taskId: string) {
|
||||
return parseResponse<{ success: boolean; task: ScheduledTask }>(
|
||||
const result = await parseResponse<{ success: boolean; task: ScheduledTask }>(
|
||||
await apiFetch(`${baseUrl()}/${encodeURIComponent(taskId)}/trigger`, {
|
||||
method: "POST",
|
||||
}),
|
||||
);
|
||||
writeYLog({
|
||||
optType: OptType.other,
|
||||
optDetail: "B1-CM助手-定时任务-手动运行",
|
||||
url: `${baseUrl()}/${encodeURIComponent(taskId)}/trigger`,
|
||||
reqParam: { taskId },
|
||||
returnParam: result,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function listPublishedScheduledTasks(params: { limit?: number; offset?: number } = {}) {
|
||||
|
||||
@@ -42,6 +42,8 @@ import {
|
||||
type IRunOptions,
|
||||
} from "docx";
|
||||
|
||||
import { OptType, writeYLog } from "@/core/log/y-log";
|
||||
|
||||
const BLACK = "000000";
|
||||
const BODY_FONT = { ascii: "Calibri", eastAsia: "Microsoft YaHei" };
|
||||
const MONO_FONT = { ascii: "Consolas", eastAsia: "Microsoft YaHei" };
|
||||
@@ -484,6 +486,12 @@ export async function downloadMarkdownAsDocx(
|
||||
title?: string,
|
||||
): Promise<void> {
|
||||
const blob = await markdownToDocxBlob(markdown, title);
|
||||
writeYLog({
|
||||
optType: OptType.export,
|
||||
optDetail: "B1-CM助手-定时任务-导出",
|
||||
url: "/scheduled-tasks/export/docx",
|
||||
reqParam: { fileName, title },
|
||||
});
|
||||
const safeName = fileName.replace(/\.(md|markdown)$/i, "") || "document";
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement("a");
|
||||
|
||||
Reference in new issue
Block a user