# Conflicts:
#	CLAUDE.md
This commit is contained in:
Administrator committed 2026-06-23 20:52:39 +08:00
commit 6f0f9fb1fc
180 files changed
+14037 -1445

No files matched your search

+8
View File
@@ -58,6 +58,10 @@ export interface Agent {
/** 入库模板名(外部知识入库接口的 templateName)。配置后该智能体会话中的
* Markdown 产物右上角出现"发布入库"按钮。空/缺省 = 不支持入库。 */
template_name?: string | null;
/** 会商席位「技能识别强化」开关。作为会商席位运行时,是否把本智能体配置的技能
* (名称 + SKILL.md 路径 + 必须先 read_file 的硬指令) 拼进本轮任务以补偿弱模型。
* 与业务链条同名开关取 OR。null/false = 关 (默认)。 */
seat_skill_directive?: boolean | null;
tags?: Tag[];
featured_order?: number | null;
/** Whether the current user has favorited (收藏) this agent. */
@@ -91,6 +95,8 @@ export interface CreateAgentRequest {
tool_groups?: string[] | null;
common_questions?: AgentQuestion[] | null;
template_name?: string | null;
/** 会商席位「技能识别强化」开关;缺省/false = 关。 */
seat_skill_directive?: boolean | null;
}
export interface UpdateAgentRequest {
@@ -106,4 +112,6 @@ export interface UpdateAgentRequest {
common_questions?: AgentQuestion[] | null;
/** 入库模板名;传空字符串表示清除。 */
template_name?: string | null;
/** 会商席位「技能识别强化」开关;缺省/false = 关。 */
seat_skill_directive?: boolean | null;
}
+42 -4
View File
@@ -17,15 +17,17 @@
* sessionStorage、正常态恒为 localStorage;嵌入标志一进程内不翻转,故一致)。
*/
import { isEmbedActive } from "@/core/embed/embed-session";
import { isEmbedActive, isIsolatedSession } from "@/core/embed/embed-session";
/**
* 返回当前应使用的鉴权存储:嵌入态 → sessionStorage(按标签页隔离);否则 → localStorage。
* 非浏览器环境返回 null(调用方各自有 SSR 守卫)。
* 返回当前应使用的鉴权存储:嵌入态(iframe)**或**独立会话态(`?isolate=1`)→ sessionStorage
* (按标签页隔离);否则 → localStorage(跨标签共享、持久)。非浏览器环境返回 null(调用方各自
* 有 SSR 守卫)。两种隔离态等价于「本标签页鉴权独立、绝不污染其它标签页登录态」,区别仅在于嵌入态
* 还会去 chrome / 强制主题,独立会话态只切换存储。
*/
export function authStorage(): Storage | null {
if (typeof window === "undefined") return null;
return isEmbedActive() ? window.sessionStorage : window.localStorage;
return isEmbedActive() || isIsolatedSession() ? window.sessionStorage : window.localStorage;
}
/** 读受保护键(嵌入态 sessionStorage / 正常态 localStorage)。 */
@@ -54,3 +56,39 @@ export function authStorageRemove(key: string): void {
/* noop */
}
}
/**
* 受 `authStorage()` 收口管理的鉴权 / 身份键:四个精确键 + `login.` 前缀(外部登录参数族
* ticket/userId/env/maxkeyOnlineToken/userDetailToken)。用于「开启独立会话」时把当前登录态从
* localStorage 快照搬进 sessionStorage,避免切换存储后被登出。
*/
const PROTECTED_AUTH_EXACT_KEYS = ["deerflow.auth", "userInfo", "userId", "tools_token"];
const PROTECTED_AUTH_PREFIX = "login.";
function isProtectedAuthKey(key: string): boolean {
return PROTECTED_AUTH_EXACT_KEYS.includes(key) || key.startsWith(PROTECTED_AUTH_PREFIX);
}
/**
* 把 localStorage 里的鉴权 / 身份键**快照复制**进 sessionStorage(覆盖式)。
*
* 「外观」设置开启独立会话时调用:此后 `authStorage()` 改读 sessionStorage,若不先搬一份当前账号
* 过去就会瞬间「未登录」。每次开启都重新快照(覆盖旧值),确保拿到的是当前 localStorage 的最新账号。
* 仅搬受保护键,不动 sessionStorage 里既有的本标签页专属态(embed.session / 任务上下文等)。
*/
export function migrateAuthToSession(): void {
if (typeof window === "undefined") return;
try {
const ls = window.localStorage;
const ss = window.sessionStorage;
for (let i = 0; i < ls.length; i++) {
const key = ls.key(i);
if (key && isProtectedAuthKey(key)) {
const value = ls.getItem(key);
if (value !== null) ss.setItem(key, value);
}
}
} catch {
/* noop */
}
}
+9 -4
View File
@@ -4,7 +4,7 @@ import { useEffect, useRef } from "react";
import { toast } from "sonner";
import { getStoredAuth } from "@/core/auth";
import { isEmbedActive } from "@/core/embed/embed-session";
import { isEmbedActive, isIsolatedSession } from "@/core/embed/embed-session";
const AUTH_STORAGE_KEY = "deerflow.auth";
const SETTLE_DELAY_MS = 1200;
@@ -22,9 +22,9 @@ export function AuthSyncWatcher() {
const lastUserIdRef = useRef<string | null>(readUserId());
useEffect(() => {
// 嵌入(iframe)态:鉴权走按标签页的 sessionStorage,本就不与其它标签页共享,且不应被正常
// 标签页的 localStorage 账号变更打扰 —— 直接不挂跨标签监听。
if (isEmbedActive()) return;
// 嵌入(iframe)态 / 独立会话态(?isolate=1):鉴权走按标签页的 sessionStorage,本就不与其它标签页
// 共享,且不应被其它标签页的 localStorage 账号变更打扰 —— 直接不挂跨标签监听。
if (isEmbedActive() || isIsolatedSession()) return;
let settleTimer: ReturnType<typeof setTimeout> | null = null;
@@ -52,6 +52,11 @@ export function AuthSyncWatcher() {
}
function handleStorage(event: StorageEvent) {
// 运行时若已切到独立会话(「外观」开关在挂载后才开启):鉴权改读本标签页 sessionStorage,
// 不应再被其它标签页的 localStorage 账号变更打扰 —— 即时忽略,无需刷新页面。
if (isEmbedActive() || isIsolatedSession()) {
return;
}
// Ignore unrelated keys and localStorage.clear(). Backend session loss is
// handled centrally through 401 responses.
if (event.key !== AUTH_STORAGE_KEY) {
+57 -8
View File
@@ -7,7 +7,8 @@
* 任务深链配置读出 rwfx 的 4 个历史按钮,3qfx/xdfx 为空)——这样真实跳转地址通过实时
* config 进种子,避免写死;一旦保存过,后端即唯一数据源。
*
* 按钮两类(``linkType``):``url`` 外链(``window.open``,可选追加 ``?id=任务id``)/
* 按钮两类(``linkType``):``url``(外链 ``window.open``;**或应用内路由**——target 以 ``/`` 开头
* 时按内部路由处理:``self`` 走 react-router 同窗导航、``blank`` 拼 hash 路由新标签;可选追加 ``?{idParam}=id``)/
* ``business`` 内部切换到另一业务(复用 [[task-deeplink]] 的 ``resolveTaskDeeplinkTarget``)。
*/
@@ -20,6 +21,9 @@ import {
resolveTaskDeeplinkTarget,
} from "@/core/auth/task-deeplink";
/** 「大屏绘制」独立页应用内路由(深链「上一步」默认指向它,跳转追加 ?taskId=)。 */
export const DASHBOARD_AGENT_ROUTE = "/page/strategy/dashboard-agent";
/** 任务工作区业务码(与路由 taskBase 一一对应)。 */
export type TaskBusiness = "rwfx" | "3qfx" | "xdfx";
@@ -59,6 +63,9 @@ export type TaskButtonLinkType = "url" | "business" | "purpose";
/** 追加的 id 取哪种:``task``=任务id(默认)/ ``action``=行动id(仅 xdfx 二者不同)。 */
export type TaskButtonIdKind = "task" | "action";
/** ``url`` 外链打开方式:``blank``=新窗口(默认)/ ``self``=当前窗口(嵌入态即当前 iframe)。 */
export type TaskButtonOpenMode = "blank" | "self";
/** 一个可配置跳转按钮。 */
export interface TaskButton {
id: string;
@@ -76,6 +83,8 @@ export interface TaskButton {
* 实为行动id,任务id 由 action 接口解析。其它业务两者相同。
*/
idKind: TaskButtonIdKind;
/** ``url`` 外链打开方式:``blank``=新窗口(默认)/ ``self``=当前窗口。仅 ``url`` 类型有意义。 */
openMode: TaskButtonOpenMode;
enabled: boolean;
sortOrder: number;
}
@@ -89,7 +98,8 @@ export const TASK_BUSINESS_TARGETS: { code: TaskBusiness; label: string }[] = [
/**
* 代码种子默认按钮(仅在后端整张表为空时使用):
* - rwfx:历史 4 个按钮(编辑/下一步/3q详情,地址取自实时任务深链配置)+ 切换到三情/行动分析;
* - rwfx:历史按钮(编辑/下一步/3q详情,地址取自实时任务深链配置)+「上一步」+ 切换到三情/行动分析;
* 「上一步」紧邻「下一步」左侧(底部栏 prev 组在 next 组前),默认指向应用内大屏绘制页(?taskId=任务id);
* - 3qfx / xdfx:互相切换到另外两个业务。
* 即「三个业务都带切换按钮」。已保存过则用页面「导入默认」把缺失项补进来。
*/
@@ -109,6 +119,7 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] {
appendTaskId: false,
idParam: "id",
idKind: "task",
openMode: "blank",
enabled: true,
sortOrder,
});
@@ -122,6 +133,7 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] {
appendTaskId: false,
idParam: "id",
idKind: "task",
openMode: "blank",
enabled: true,
sortOrder,
});
@@ -136,9 +148,28 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] {
appendTaskId: true,
idParam: "id",
idKind: "task",
openMode: "blank",
enabled: true,
sortOrder: 0,
},
{
// 上一步:放在「下一步」左边(底部快捷跳转栏,prev 组渲染在 next 组之前)。
// 默认指向**应用内**「大屏绘制」页 /page/strategy/dashboard-agent(内部路由,以 / 开头),
// 携带 ?taskId=任务id(大屏页按 taskId 取数)。openMode=self → 同窗 react-router 导航;
// 大屏页路由在 PageLayout 里被**强制显示全局 chrome**(见 PageRoutes 的 forceChromeForDashboardAgent),
// 故即便从去 chrome 的工作区进来也带回顶部导航栏 + 侧边栏(embed=1 外部嵌入态仍隐藏)。
id: "rwfx-prev",
business: "rwfx",
label: "上一步",
linkType: "url",
target: DASHBOARD_AGENT_ROUTE,
appendTaskId: true,
idParam: "taskId",
idKind: "task",
openMode: "self",
enabled: true,
sortOrder: 1,
},
{
id: "rwfx-next",
business: "rwfx",
@@ -148,8 +179,9 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] {
appendTaskId: true,
idParam: "id",
idKind: "task",
openMode: "blank",
enabled: true,
sortOrder: 1,
sortOrder: 2,
},
{
id: "rwfx-detail3q",
@@ -160,12 +192,13 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] {
appendTaskId: true,
idParam: "id",
idKind: "task",
openMode: "blank",
enabled: true,
sortOrder: 2,
sortOrder: 3,
},
sw("rwfx-switch3q", "rwfx", "切换到三情分析", "3qfx", 3),
sw("rwfx-switchxd", "rwfx", "切换到行动分析", "xdfx", 4),
pz("rwfx-purpose", "rwfx", 5),
sw("rwfx-switch3q", "rwfx", "切换到三情分析", "3qfx", 4),
sw("rwfx-switchxd", "rwfx", "切换到行动分析", "xdfx", 5),
pz("rwfx-purpose", "rwfx", 6),
// 3qfx(三情分析):切换到任务/行动分析
sw("3qfx-switchrw", "3qfx", "切换到任务分析", "rwfx", 0),
sw("3qfx-switchxd", "3qfx", "切换到行动分析", "xdfx", 1),
@@ -227,7 +260,23 @@ export async function runTaskButton(btn: TaskButton, ctx: RunTaskButtonCtx): Pro
const id = btn.idKind === "action" ? ctx.actionId || ctx.taskId : ctx.taskId;
const param = (btn.idParam || "id").trim() || "id";
const url = btn.appendTaskId && id ? appendQueryParam(base, param, id) : base;
window.open(url, "_blank", "noopener,noreferrer");
// 应用内部路由(以单个 / 开头,如「上一步」→ 大屏绘制页):
// self → 走 react-router 同窗导航(不刷新、保留 SPA);blank → 拼成当前源的 hash 路由新标签打开。
if (base.startsWith("/") && !base.startsWith("//")) {
if (btn.openMode === "blank") {
const full = `${window.location.origin}${window.location.pathname}#${url}`;
window.open(full, "_blank", "noopener,noreferrer");
} else {
ctx.navigate(url);
}
return;
}
// 外链:self=当前窗口(嵌入态即当前 iframe);否则 blank=新窗口(默认)。
if (btn.openMode === "self") {
window.location.assign(url);
} else {
window.open(url, "_blank", "noopener,noreferrer");
}
return;
}
// business:内部切换到另一业务
+59 -2
View File
@@ -35,6 +35,9 @@ export interface CopTaskDetail {
/** 任务详情整包暂存的 sessionStorage key(目标页/后续可读取)。 */
export const COP_TASK_DETAIL_STORAGE_KEY = "login.copTaskDetail";
/** 行动详情整包暂存的 sessionStorage key(xdfx 行动分析:会商/单智能体读取行动名称/叙事)。 */
export const ACTION_DETAIL_STORAGE_KEY = "login.actionDetail";
/** goPath 基础码 → 业务码(配置管理 businessCode)。 */
const GOPATH_BUSINESS_CODE: Record<string, string> = {
"3qfx": "3Q", // 三情分析
@@ -74,8 +77,28 @@ function consumerBaseUrl(): string {
return typeof value === "string" ? value.replace(/\/+$/, "") : "";
}
/** 拉取任务详情;缺少 consumer 地址 / 返回不合法时返回 null。 */
/** cop-task-detail 是否走本地假数据:开关 `VITE_MOCK_COP_TASK_DETAIL=true`(真实接口未就绪时调试用)。 */
function copTaskDetailMockEnabled(): boolean {
return (import.meta.env as Record<string, unknown>).VITE_MOCK_COP_TASK_DETAIL === "true";
}
/** cop-task-detail 本地假数据(与真实 data 字段同构;mock 开关开启时返回)。 */
function mockCopTaskDetail(taskId: string): CopTaskDetail {
return {
id: taskId || "2055",
overview: "暑期文旅消费高峰期间的舆情态势综合研判与引导任务(本地假数据)。",
taskDirection: "正向引导 + 风险预警",
taskName: "任务01:暑期文旅舆情综合研判",
taskContent:
"围绕暑期文旅消费高峰,对重点城市、重点景区的公众情绪与舆情态势做综合研判,识别情绪拐点与潜在风险,提出正向引导与处置建议。",
};
}
/** 拉取任务详情;``VITE_MOCK_COP_TASK_DETAIL=true`` 走假数据;缺少 consumer 地址 / 返回不合法时返回 null。 */
export async function fetchCopTaskDetail(taskId: string): Promise<CopTaskDetail | null> {
if (copTaskDetailMockEnabled()) {
return mockCopTaskDetail(taskId);
}
const base = consumerBaseUrl();
if (!base || !taskId) return null;
// service 拦截器已统一注入 `Authorization: Bearer admin` 并返回 response.data(整包 body)。
@@ -111,6 +134,25 @@ export function readCopTaskDetail(): CopTaskDetail | null {
}
}
/** 把行动详情整包暂存到 sessionStorage(xdfx 行动分析用;隐私模式/quota 异常时静默失败)。 */
export function persistActionDetail(detail: ActionDetail): void {
try {
sessionStorage.setItem(ACTION_DETAIL_STORAGE_KEY, JSON.stringify(detail));
} catch {
/* noop */
}
}
/** 读取暂存的行动详情(会商左上角「当前行动」、单智能体行动详情卡都可读)。 */
export function readActionDetail(): ActionDetail | null {
try {
const raw = sessionStorage.getItem(ACTION_DETAIL_STORAGE_KEY);
return raw ? (JSON.parse(raw) as ActionDetail) : null;
} catch {
return null;
}
}
/** 行动详情接口 ``{action_detail_url}/{actionId}`` 返回的 ``data`` 字段(xdfx 行动分析用)。 */
export interface ActionDetail {
/** 行动id(深链 ?taskId= 传入的就是它)。 */
@@ -148,7 +190,7 @@ export async function fetchActionDetail(actionId: string): Promise<ActionDetail
const data = body && typeof body === "object" ? (body as { data?: unknown }).data : null;
if (!data || typeof data !== "object") return null;
const d = data as Record<string, unknown>;
return {
const detail: ActionDetail = {
id: d.id != null ? String(d.id) : "",
taskId: d.taskId != null ? String(d.taskId) : "",
purposeId: d.purposeId != null ? String(d.purposeId) : "",
@@ -157,6 +199,10 @@ export async function fetchActionDetail(actionId: string): Promise<ActionDetail
typeof d.actionStatus === "number" || typeof d.actionStatus === "string" ? d.actionStatus : "",
narration: typeof d.narration === "string" ? d.narration : "",
};
// 取到即整包暂存:登录解析任务id 时(resolveDeeplinkTaskId→fetchTaskIdFromAction)就会缓存它,
// 之后会商左上角「当前行动」、单智能体行动详情卡都能直接 readActionDetail(),避免重复请求。
persistActionDetail(detail);
return detail;
}
/**
@@ -232,6 +278,15 @@ export async function resolveTaskDeeplinkTarget(
if (parsed.roundtable) {
// 多智能体会商:嵌进对应业务的「任务管理」布局;businessCode 让会商页自动选用配置的业务链条;
// taskId 让会商页按任务隔离存取草稿(一个 taskId 多条记录),并触发第三步自动结构化输出。
// xdfx(行动分析):根是「行动」——把行动名称随首条消息交给任务理解,左上角展示「当前行动」。
// 行动详情登录解析任务id 时已缓存(fetchActionDetail 内 persistActionDetail);这里优先读缓存。
let actionName = "";
let actionId = "";
if (parsed.businessCode === XDFX_BUSINESS_CODE && rawId) {
actionId = rawId;
const ad = readActionDetail() ?? (await fetchActionDetail(rawId));
actionName = ad?.actionName ?? "";
}
return {
path: `${taskBase}/roundtable`,
state: {
@@ -240,6 +295,8 @@ export async function resolveTaskDeeplinkTarget(
taskGoal: detail.taskContent,
businessCode: parsed.businessCode,
taskId: taskId || undefined,
...(actionId ? { actionId } : {}),
...(actionName ? { actionName } : {}),
autoStart: true,
},
},
+3 -1
View File
@@ -282,12 +282,14 @@ export async function deleteTaskPurpose(
if (!cfg.deleteUrl) return { message: "缺少删除接口地址", success: false };
// 只放要传的那个 id key:另一参数直接不出现在 URL(axios 不会序列化缺省 key)。
const params = idKind === "action" ? { actionId: id } : { taskId: id };
// 该删除接口(dropSixOrSevenData)成功时**什么都不返回**(空响应体),所以只要请求没抛错
// (HTTP 成功)就直接判定为成功;仅当后端偶尔回了 {success:false} 这类显式失败标记才算失败。
const body = await requestCrudService.get(cfg.deleteUrl, params);
const obj =
body && typeof body === "object" ? (body as unknown as Record<string, unknown>) : {};
return {
message: typeof obj.message === "string" ? obj.message : "",
success: obj.success === true,
success: obj.success !== false,
};
}
+46 -5
View File
@@ -1,13 +1,19 @@
/**
* 「外部系统 iframe 套用深链」相关的**按标签页**会话开关。承载两个相互独立的能力,均由 URL
* 路由参数开启、存进同一份 sessionStorage 会话、跨登录跳转/刷新/页内导航存活:
* 「外部系统 iframe 套用深链」相关的**按标签页**会话开关。承载三个相互独立的能力,多由 URL
* 路由参数开启(独立会话 `isolate` 另可由「外观」设置里的开关在运行时切换,见 `setIsolatedSession`),
* 存进同一份 sessionStorage 会话、跨登录跳转/刷新/页内导航存活:
*
* 1. `embed=1`(可带 `&theme=`)—— **嵌入模式**:去掉全局导航栏(Header)+ 侧边栏(PageSidebar),
* 并按外部主题色强制系统明/暗(离散切换)。
* 2. `hideJump=1` —— **隐藏深链底部「快捷跳转」栏**:去掉 `TaskJumpBar` 的两部分——底部的
* 「下一步」等按钮 + 右侧垂直居中的悬浮菜单(单智能体问答 / 多智能体会商深链共用该栏)。
* 3. `isolate=1` —— **独立会话登录**:本标签页的鉴权身份按标签页隔离(见 [[scoped-storage]]
* `authStorage()`,嵌入态与独立态同走 sessionStorage)。用于同一浏览器里**另开一个标签页登
* 录不同账号**而**不覆盖**当前标签页(localStorage)的登录态——`embed` 已隐含此隔离,`isolate`
* 则是把同一套隔离单独提供给**普通(带 chrome)标签页**,不去 chrome、不强制主题、不动跳转栏。
*
* 两者可单独使用,也可叠加:`hideJump` **只**隐藏跳转栏,不动 chrome / 主题;`embed` 不影响跳转栏。
* 三者可单独使用,也可叠加:`hideJump` **只**隐藏跳转栏,不动 chrome / 主题;`embed` 不影响跳转栏;
* `isolate` 只切换鉴权存储,不影响 chrome / 主题 / 跳转栏。
*
* 隔离要点(核心诉求:同一浏览器另开咱们正常系统页,**绝不能**被这些开关影响):
* - 用 **sessionStorage**(按标签页隔离、绝不跨标签)持有这些 flag,而**不用** localStorage
@@ -33,6 +39,8 @@ export interface EmbedSession {
theme: EmbedTheme;
/** 隐藏深链底部「快捷跳转」栏(TaskJumpBar:底部按钮 + 右侧悬浮菜单)。 */
hideJump: boolean;
/** 独立会话:鉴权身份按本标签页隔离(走 sessionStorage),用于同浏览器登录不同账号互不覆盖。 */
isolate: boolean;
}
/** URL 布尔参数的三态:显式开 / 显式关 / 未带。 */
@@ -61,6 +69,11 @@ export function readHideJumpParam(search: string): FlagParam {
return readFlagParam(search, "hideJump");
}
/** 读 URL 里的 `isolate` 三态(独立会话登录)。 */
export function readIsolateParam(search: string): FlagParam {
return readFlagParam(search, "isolate");
}
/**
* 把外部传入的主题色映射成离散明/暗。与 `resolveThemeFromQuery` 同源约定(dark-blue → 深色),
* 另外宽松接受 `dark` / 中文「深色/深蓝」直传;其余(含空)一律浅色。
@@ -86,6 +99,7 @@ export function readEmbedSession(): EmbedSession | null {
embed: p.embed === true,
theme: p.theme === "dark" ? "dark" : "light",
hideJump: p.hideJump === true,
isolate: p.isolate === true,
};
} catch {
return null;
@@ -95,7 +109,7 @@ export function readEmbedSession(): EmbedSession | null {
/** 写会话;无任何开关置位时直接清除(保持 sessionStorage 干净)。隐私模式/quota 异常静默失败。 */
function writeEmbedSession(next: EmbedSession): void {
try {
if (!next.embed && !next.hideJump) {
if (!next.embed && !next.hideJump && !next.isolate) {
sessionStorage.removeItem(EMBED_SESSION_KEY);
return;
}
@@ -115,13 +129,24 @@ export function isJumpBarHidden(): boolean {
return readEmbedSession()?.hideJump === true;
}
/**
* 本标签页是否处于独立会话态:鉴权身份按标签页隔离(走 sessionStorage)。
* `embed` 已隐含同样的隔离,故 `authStorage()` 取「embed || isolate」之并;这里只判 `isolate`,
* 供需要区分「纯独立会话(带 chrome)」与「嵌入态(去 chrome)」的场合使用。
*/
export function isIsolatedSession(): boolean {
return readEmbedSession()?.isolate === true;
}
/**
* 按 URL 参数同步 sessionStorage 会话(AppProviders 在每次 search 变化时调用一次)。
* **合并语义**:on 置位、off 清位、未带则沿用既有值——这样登录后跳到不带参数的干净路由时,
* 之前置位的开关不会被抹掉。
*/
export function syncEmbedSessionFromSearch(search: string): void {
const cur = readEmbedSession() ?? { embed: false, theme: "light" as EmbedTheme, hideJump: false };
const cur =
readEmbedSession() ??
{ embed: false, theme: "light" as EmbedTheme, hideJump: false, isolate: false };
const next: EmbedSession = { ...cur };
const embedParam = readEmbedParam(search);
@@ -136,6 +161,10 @@ export function syncEmbedSessionFromSearch(search: string): void {
if (hideJumpParam === "on") next.hideJump = true;
else if (hideJumpParam === "off") next.hideJump = false;
const isolateParam = readIsolateParam(search);
if (isolateParam === "on") next.isolate = true;
else if (isolateParam === "off") next.isolate = false;
writeEmbedSession(next);
}
@@ -170,3 +199,15 @@ export function resolveForcedEmbedTheme(search: string): EmbedTheme | undefined
const session = readEmbedSession();
return session?.embed ? session.theme : undefined;
}
/**
* 运行时直接置位/清除本标签页的独立会话标志(供「外观」设置里的开关用,区别于 URL 参数驱动)。
* 仅切换标志;登录态的「localStorage 快照搬进 sessionStorage」由 [[scoped-storage]]
* `migrateAuthToSession()` 负责,调用顺序见 appearance 设置页。
*/
export function setIsolatedSession(on: boolean): void {
const cur =
readEmbedSession() ??
{ embed: false, theme: "light" as EmbedTheme, hideJump: false, isolate: false };
writeEmbedSession({ ...cur, isolate: on });
}
+22 -10
View File
@@ -212,21 +212,33 @@ export function extractToolResultPayload(
}
const THINK_TAG_RE = /<think>\s*([\s\S]*?)\s*<\/think>/g;
const THINK_OPEN = "<think>";
function splitInlineReasoning(content: string) {
const reasoningParts: string[] = [];
const cleaned = content
.replace(THINK_TAG_RE, (_, reasoning: string) => {
const normalized = reasoning.trim();
if (normalized) {
reasoningParts.push(normalized);
}
return "";
})
.trim();
// 1) 先抽出所有**已闭合**的 <think>...</think>。
let working = content.replace(THINK_TAG_RE, (_, reasoning: string) => {
const normalized = reasoning.trim();
if (normalized) {
reasoningParts.push(normalized);
}
return "";
});
// 2) 流式中常见**未闭合**的 <think>(还没等到 </think> 就先到达):把开标签之后的内容
// 当作「实时思考」抽到 reasoning、从答案正文里移除。否则模型慢/先思考时,用户既会看到
// 字面 "<think>",又会因答案区长时间空白而以为卡死。这样思考内容会实时流进思考区。
const openIdx = working.indexOf(THINK_OPEN);
if (openIdx !== -1 && !working.slice(openIdx).includes("</think>")) {
const partial = working.slice(openIdx + THINK_OPEN.length).trim();
if (partial) {
reasoningParts.push(partial);
}
working = working.slice(0, openIdx);
}
return {
content: cleaned,
content: working.trim(),
reasoning: reasoningParts.length > 0 ? reasoningParts.join("\n\n") : null,
};
}
@@ -4,6 +4,7 @@ import { OptType, writeYLog } from "@/core/log/y-log";
import type {
HtmlPageFavorite,
HtmlTemplateOption,
PublishedScheduledTask,
ScheduledTask,
ScheduledTaskDeliveryProfile,
@@ -33,6 +34,14 @@ export async function getSchedulerThread() {
return parseResponse<SchedulerThread>(await apiFetch(`${baseUrl()}/thread`));
}
/** 可选的 HTML 模板清单(模板数据转换器填充用)。 */
export async function listHtmlTemplates() {
const data = await parseResponse<{ templates: HtmlTemplateOption[] }>(
await apiFetch(`${baseUrl()}/html-templates`),
);
return data.templates ?? [];
}
export interface ListScheduledTasksParams {
search?: string;
tagIds?: string[];
@@ -16,6 +16,7 @@ import {
deleteScheduledTask,
deleteScheduledTaskRun,
getSchedulerThread,
listHtmlTemplates,
getScheduledTaskDeliveryProfile,
getScheduledTaskRun,
getScheduledTaskRunHtml,
@@ -45,6 +46,17 @@ export function useSchedulerThread() {
});
}
/** 可选的 HTML 模板清单(模板数据转换器填充用)。模板是后端静态注册,长缓存即可。 */
export function useHtmlTemplates(enabled = true) {
return useQuery({
queryKey: ["scheduled-tasks", "html-templates"],
queryFn: listHtmlTemplates,
enabled,
staleTime: 5 * 60_000,
refetchOnWindowFocus: false,
});
}
/** A task is currently executing — used to drive live polling of the task list. */
function taskIsLive(status?: string | null): boolean {
return status === "running" || status === "pending";
@@ -24,8 +24,27 @@ export type HtmlPageConfig = {
model_name?: string | null;
};
/** 选中内置「模板数据转换器」agent 后,要把源 JSON 填充进的 HTML 模板。
* 持久化在 ``execution_context.template_config`` 里(无独立列)。 */
export type TemplateFillConfig = {
template_id?: string | null;
/** 可选:覆盖转换所用模型(留空走 agent.model / 系统默认)。 */
model_name?: string | null;
};
/** ``GET /api/scheduled-tasks/html-templates`` 返回的一条模板选项。 */
export type HtmlTemplateOption = {
id: string;
name: string;
description?: string | null;
/** false = 暂只「直接展示已有数据」,不需要也不会用源 JSON 转换填充。 */
convertible?: boolean;
};
export type ScheduledTaskExecutionContext = {
require_markdown?: boolean;
/** 模板填充模式:选中「模板数据转换器」+ 选模板后填充进固定 HTML 模板。 */
template_config?: TemplateFillConfig;
/**
* The unified "必须生成 HTML" switch. The task form no longer splits "普通任务 /
* HTML 页面" into separate kinds — instead ``require_html`` and