feat(y-log): 操作日志上报(前端直连 + 后端公开只读下发地址)

- 后端新增 y_log 公开只读配置:YLogConfig + AppConfig.y_log +
  GET /api/public/config/y-log,地址写在 config.yaml(mtime 热重载)
- 前端核心 src/core/log/:writeYLog(双门控/异步即发即忘/永不抛错/
  去ip-host/对象序列化裁剪/optResult恒0)+ 配置加载 + token1 隔离存取
- 登录(token)持久化 token1 并报登录、登出先报后清;启动预热配置
- 各 API client 成功路径埋点:敏感词/菜单/按钮/技能地址/轻应用、
  会商草稿+任务草稿+后台任务、智能体/定时任务/知识库、Word导出,
  以及问答完成钩子(ChatPage/AgentChatPage onFinish)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
AdministratorandClaude Opus 4.8 committed 2026-06-23 17:46:08 +08:00
1 parent dd942cdd60
commit d2be3b206d
26 files changed
+808 -40

No files matched your search

+127 -8
View File
@@ -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;
+77 -7
View File
@@ -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). */
+68
View File
@@ -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;
}
+34
View File
@@ -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);
}
+109
View File
@@ -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;
}
}
+73 -8
View File
@@ -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");