Compare commits
6 Commits
bcee17f5de
...
c6c8bf2d0c
| Author | SHA1 | Date | |
|---|---|---|---|
| c6c8bf2d0c | |||
| 9d3784bd90 | |||
| 80284f1732 | |||
| d3622ce438 | |||
| 0563588c74 | |||
| da176ba4c7 |
@ -410,7 +410,7 @@ AI 执行以下流程时,每一步都要向用户说明在做什么:
|
|||||||
|
|
||||||
### 11.2 子智能体执行机制
|
### 11.2 子智能体执行机制
|
||||||
|
|
||||||
- 子智能体在主进程内 `asyncio.Task`,跑在独立后台事件循环线程里(避开 Flask-SocketIO threading 冲突)。工具调用复用主进程沙箱/容器链路,网络调用走 `utils.api_client.DeepSeekClient`。
|
- 子智能体在主进程内 `asyncio.Task`,跑在独立后台事件循环线程里(避开 Flask-SocketIO threading 冲突)。工具调用复用主进程沙箱/容器链路,网络调用走 `utils.api_client.APIClient`。
|
||||||
- 子智能体在多智能体模式下:
|
- 子智能体在多智能体模式下:
|
||||||
- `create_sub_agent` 强制 `run_in_background=False`,不触发 `sub_agent_waiting` 事件,不阻塞前端输入区。
|
- `create_sub_agent` 强制 `run_in_background=False`,不触发 `sub_agent_waiting` 事件,不阻塞前端输入区。
|
||||||
- 子智能体自然的 assistant 输出结束(无 tool_calls)即本轮任务结束,进入 `idle`,上下文保留,不算 failed。
|
- 子智能体自然的 assistant 输出结束(无 tool_calls)即本轮任务结束,进入 `idle`,上下文保留,不算 failed。
|
||||||
|
|||||||
@ -54,7 +54,7 @@
|
|||||||
- 一次展示优先只输出一个完整标签块(开标签 + 内容 + 闭标签),不要拆段输出
|
- 一次展示优先只输出一个完整标签块(开标签 + 内容 + 闭标签),不要拆段输出
|
||||||
- 若还需要解释说明,把说明写在标签之外,且不要再次写标签字面量
|
- 若还需要解释说明,把说明写在标签之外,且不要再次写标签字面量
|
||||||
|
|
||||||
如需在界面直接展示图片卡片,使用 `<show_image src="路径" alt="描述" />`,支持本地路径或网络链接。禁止使用 Markdown 图片语法。
|
如需在界面直接展示图片卡片,使用 `<show_image src="路径" alt="描述" />`。本地图片写工作区相对路径(如 `output/chart.png`;host/宿主机模式也可用任意绝对路径),网络图片写 http(s) 链接。禁止使用 Markdown 图片语法。
|
||||||
|
|
||||||
如需在界面直接渲染 HTML 卡片,使用 `<show_html ratio="比例" js="off|on">...</show_html>`。
|
如需在界面直接渲染 HTML 卡片,使用 `<show_html ratio="比例" js="off|on">...</show_html>`。
|
||||||
- `ratio` 只能从以下五种里选:`1:1`、`16:9`、`9:16`、`4:3`、`3:4`
|
- `ratio` 只能从以下五种里选:`1:1`、`16:9`、`9:16`、`4:3`、`3:4`
|
||||||
|
|||||||
@ -54,7 +54,7 @@
|
|||||||
- 一次展示优先只输出一个完整标签块(开标签 + 内容 + 闭标签),不要拆段输出
|
- 一次展示优先只输出一个完整标签块(开标签 + 内容 + 闭标签),不要拆段输出
|
||||||
- 若还需要解释说明,把说明写在标签之外,且不要再次写标签字面量
|
- 若还需要解释说明,把说明写在标签之外,且不要再次写标签字面量
|
||||||
|
|
||||||
如需在界面直接展示图片卡片,使用 `<show_image src="路径" alt="描述" />`,支持本地路径或网络链接。禁止使用 Markdown 图片语法。
|
如需在界面直接展示图片卡片,使用 `<show_image src="路径" alt="描述" />`。本地图片写工作区相对路径(如 `output/chart.png`;host/宿主机模式也可用任意绝对路径),网络图片写 http(s) 链接。禁止使用 Markdown 图片语法。
|
||||||
|
|
||||||
如需在界面直接渲染 HTML 卡片,使用 `<show_html ratio="比例" js="off|on">...</show_html>`。
|
如需在界面直接渲染 HTML 卡片,使用 `<show_html ratio="比例" js="off|on">...</show_html>`。
|
||||||
- `ratio` 只能从以下五种里选:`1:1`、`16:9`、`9:16`、`4:3`、`3:4`
|
- `ratio` 只能从以下五种里选:`1:1`、`16:9`、`9:16`、`4:3`、`3:4`
|
||||||
|
|||||||
@ -226,7 +226,7 @@ id: ask_fse_001
|
|||||||
- 一次展示优先只输出一个完整标签块(开标签 + 内容 + 闭标签),不要拆段输出
|
- 一次展示优先只输出一个完整标签块(开标签 + 内容 + 闭标签),不要拆段输出
|
||||||
- 若还需要解释说明,把说明写在标签之外,且不要再次写标签字面量
|
- 若还需要解释说明,把说明写在标签之外,且不要再次写标签字面量
|
||||||
|
|
||||||
如需在界面直接展示图片卡片,使用 `<show_image src="路径" alt="描述" />`,支持本地路径或网络链接。禁止使用 Markdown 图片语法。
|
如需在界面直接展示图片卡片,使用 `<show_image src="路径" alt="描述" />`。本地图片写工作区相对路径(如 `output/chart.png`;host/宿主机模式也可用任意绝对路径),网络图片写 http(s) 链接。禁止使用 Markdown 图片语法。
|
||||||
|
|
||||||
如需在界面直接渲染 HTML 卡片,使用 `<show_html ratio="比例" js="off|on">...</show_html>`。
|
如需在界面直接渲染 HTML 卡片,使用 `<show_html ratio="比例" js="off|on">...</show_html>`。
|
||||||
- `ratio` 只能从以下五种里选:`1:1`、`16:9`、`9:16`、`4:3`、`3:4`
|
- `ratio` 只能从以下五种里选:`1:1`、`16:9`、`9:16`、`4:3`、`3:4`
|
||||||
|
|||||||
@ -296,12 +296,11 @@ def _build_inject_guide_message(
|
|||||||
current_record: Dict[str, Any],
|
current_record: Dict[str, Any],
|
||||||
previous_records: List[Dict[str, Any]],
|
previous_records: List[Dict[str, Any]],
|
||||||
user_inputs: List[str],
|
user_inputs: List[str],
|
||||||
latest_user_input: str,
|
|
||||||
runtime_state_lines: Optional[List[str]] = None,
|
runtime_state_lines: Optional[List[str]] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""生成直接注入模式的引导语:把历次压缩总结、用户输入按顺序拼入正文。"""
|
"""生成直接注入模式的引导语:把历次压缩总结、用户输入按顺序拼入正文。"""
|
||||||
lines: List[str] = [
|
lines: List[str] = [
|
||||||
f"当前对话已被第{compression_index}次压缩。以下为按时间顺序汇总的用户输入、历次压缩总结以及最近一次输入,请据此继续工作。",
|
f"当前对话已被第{compression_index}次压缩。以下为按时间顺序汇总的用户输入、历次压缩总结,请据此继续工作。",
|
||||||
"",
|
"",
|
||||||
"用户的所有输入",
|
"用户的所有输入",
|
||||||
]
|
]
|
||||||
@ -315,12 +314,6 @@ def _build_inject_guide_message(
|
|||||||
compression_index,
|
compression_index,
|
||||||
)
|
)
|
||||||
lines.extend(summary_lines)
|
lines.extend(summary_lines)
|
||||||
lines.append("")
|
|
||||||
lines.append("用户的最近一次输入:")
|
|
||||||
if (latest_user_input or "").strip():
|
|
||||||
lines.append(latest_user_input.strip())
|
|
||||||
else:
|
|
||||||
lines.append("(无)")
|
|
||||||
if runtime_state_lines:
|
if runtime_state_lines:
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("当前运行时状态")
|
lines.append("当前运行时状态")
|
||||||
@ -398,7 +391,6 @@ def _write_compact_file(
|
|||||||
compression_index: int,
|
compression_index: int,
|
||||||
summary_text: str,
|
summary_text: str,
|
||||||
user_inputs: List[str],
|
user_inputs: List[str],
|
||||||
latest_user_input: str,
|
|
||||||
previous_records: List[Dict[str, Any]],
|
previous_records: List[Dict[str, Any]],
|
||||||
runtime_state_lines: Optional[List[str]] = None,
|
runtime_state_lines: Optional[List[str]] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
@ -422,12 +414,6 @@ def _write_compact_file(
|
|||||||
compression_index,
|
compression_index,
|
||||||
)
|
)
|
||||||
lines.extend(summary_lines)
|
lines.extend(summary_lines)
|
||||||
lines.append("")
|
|
||||||
lines.append("## 用户最新的一次输入")
|
|
||||||
if (latest_user_input or "").strip():
|
|
||||||
lines.append(latest_user_input.strip())
|
|
||||||
else:
|
|
||||||
lines.append("(无)")
|
|
||||||
if runtime_state_lines:
|
if runtime_state_lines:
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("## 当前运行时状态")
|
lines.append("## 当前运行时状态")
|
||||||
@ -566,7 +552,6 @@ async def run_deep_compression(
|
|||||||
compression_index=target_count,
|
compression_index=target_count,
|
||||||
summary_text=summary_text,
|
summary_text=summary_text,
|
||||||
user_inputs=user_inputs,
|
user_inputs=user_inputs,
|
||||||
latest_user_input=latest_user_input,
|
|
||||||
previous_records=previous_records,
|
previous_records=previous_records,
|
||||||
runtime_state_lines=runtime_state_lines,
|
runtime_state_lines=runtime_state_lines,
|
||||||
)
|
)
|
||||||
@ -622,7 +607,6 @@ async def run_deep_compression(
|
|||||||
current_record=current_record,
|
current_record=current_record,
|
||||||
previous_records=previous_records,
|
previous_records=previous_records,
|
||||||
user_inputs=user_inputs,
|
user_inputs=user_inputs,
|
||||||
latest_user_input=latest_user_input,
|
|
||||||
runtime_state_lines=runtime_state_lines,
|
runtime_state_lines=runtime_state_lines,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -143,20 +143,61 @@ function collectShowImageOrder(root: ParentNode | null = document) {
|
|||||||
return tokens.slice(0, 200);
|
return tokens.slice(0, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* host/docker 模式缓存读取:键名与 app/state.ts 的 WORKSPACE_MODE_STORAGE_KEY 一致。
|
||||||
|
模式是部署级属性几乎不变,socket 连接后会校正回写 localStorage,这里直接读缓存即可。 */
|
||||||
|
function getShowImageWorkspaceMode(): 'host' | 'docker' {
|
||||||
|
try {
|
||||||
|
return window.localStorage.getItem('agents_workspace_mode') === 'host' ? 'host' : 'docker';
|
||||||
|
} catch {
|
||||||
|
return 'docker';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 归一化 show_image 的 src:
|
||||||
|
* - http(s) 网络链接、/user_upload/ 上传文件:原样使用;
|
||||||
|
* - 其余一律视为本地文件路径,统一走 /api/file/content 通道(inline 预览,MIME 白名单由服务端控制):
|
||||||
|
* - docker/web 模式:仅工作区内相对路径,越界(../、项目外绝对路径)由后端 _validate_path 拒绝;
|
||||||
|
* - host 模式:工作区相对路径或任意绝对路径均可(后端 _validate_path host 分支全放行)。
|
||||||
|
* 无法识别时返回空串,由调用方渲染可见的错误占位(不再静默吞掉)。
|
||||||
|
*/
|
||||||
function normalizeShowImageSrc(src: string) {
|
function normalizeShowImageSrc(src: string) {
|
||||||
if (!src) return '';
|
if (!src) return '';
|
||||||
const trimmed = src.trim();
|
let trimmed = src.trim();
|
||||||
|
if (!trimmed) return '';
|
||||||
if (/^https?:\/\//i.test(trimmed)) return trimmed;
|
if (/^https?:\/\//i.test(trimmed)) return trimmed;
|
||||||
|
// file:// 前缀剥离按本地路径处理(浏览器禁止 http 页面直接加载 file:// 资源)
|
||||||
|
if (/^file:\/\//i.test(trimmed)) {
|
||||||
|
trimmed = trimmed.replace(/^file:\/\//i, '');
|
||||||
|
// Windows file:///C:/... → C:/...
|
||||||
|
trimmed = trimmed.replace(/^\/(?=[A-Za-z]:\/)/, '');
|
||||||
|
}
|
||||||
if (trimmed.startsWith('/user_upload/')) return trimmed;
|
if (trimmed.startsWith('/user_upload/')) return trimmed;
|
||||||
// 兼容容器内部路径:/workspace/.../user_upload/xxx.png 或 /workspace/user_upload/xxx
|
// 兼容容器内部路径:/workspace/.../user_upload/xxx.png 或 /workspace/user_upload/xxx
|
||||||
const idx = trimmed.toLowerCase().indexOf('/user_upload/');
|
const idx = trimmed.toLowerCase().indexOf('/user_upload/');
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
return '/user_upload/' + trimmed.slice(idx + '/user_upload/'.length);
|
return '/user_upload/' + trimmed.slice(idx + '/user_upload/'.length);
|
||||||
}
|
}
|
||||||
if (trimmed.startsWith('/') || trimmed.startsWith('./') || trimmed.startsWith('../')) {
|
// /workspace 前缀是容器内绝对路径写法,剥掉后按工作区相对路径处理
|
||||||
return trimmed;
|
if (trimmed === '/workspace') return '';
|
||||||
|
if (trimmed.startsWith('/workspace/')) {
|
||||||
|
trimmed = trimmed.slice('/workspace/'.length);
|
||||||
}
|
}
|
||||||
return '';
|
// 前导 ./ 语义等同工作区相对路径,剥掉保持整洁
|
||||||
|
if (trimmed.startsWith('./')) {
|
||||||
|
trimmed = trimmed.slice(2);
|
||||||
|
}
|
||||||
|
if (!trimmed) return '';
|
||||||
|
if (trimmed.startsWith('/')) {
|
||||||
|
if (getShowImageWorkspaceMode() === 'host') {
|
||||||
|
// host 模式保留绝对路径语义(如 /Users/xxx/img.png),后端全放行
|
||||||
|
return `/api/file/content?path=${encodeURIComponent(trimmed)}`;
|
||||||
|
}
|
||||||
|
// docker 模式下前导 / 视为工作区根相对路径(真绝对路径会被后端拒绝)
|
||||||
|
trimmed = trimmed.replace(/^\/+/, '');
|
||||||
|
if (!trimmed) return '';
|
||||||
|
}
|
||||||
|
return `/api/file/content?path=${encodeURIComponent(trimmed)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parsePixelSize(raw: string | null, fallback: number) {
|
function parsePixelSize(raw: string | null, fallback: number) {
|
||||||
@ -555,6 +596,17 @@ function renderShowImages(root: ParentNode | null = document) {
|
|||||||
const rawSrc = node.getAttribute('src') || '';
|
const rawSrc = node.getAttribute('src') || '';
|
||||||
const mappedSrc = normalizeShowImageSrc(rawSrc);
|
const mappedSrc = normalizeShowImageSrc(rawSrc);
|
||||||
if (!mappedSrc) {
|
if (!mappedSrc) {
|
||||||
|
// 不再静默吞掉:渲染可见错误占位,方便定位路径问题
|
||||||
|
const figure = document.createElement('figure');
|
||||||
|
figure.className =
|
||||||
|
'chat-inline-card chat-inline-card--image chat-inline-image chat-inline-card--error chat-inline-image--error';
|
||||||
|
const tip = document.createElement('div');
|
||||||
|
tip.className = 'chat-inline-card__error chat-inline-image__error';
|
||||||
|
tip.textContent = rawSrc
|
||||||
|
? `无法显示图片:不支持的图片路径 ${rawSrc}`
|
||||||
|
: '无法显示图片:缺少图片路径';
|
||||||
|
figure.appendChild(tip);
|
||||||
|
node.replaceChildren(figure);
|
||||||
node.setAttribute('data-rendered', '1');
|
node.setAttribute('data-rendered', '1');
|
||||||
node.setAttribute('data-rendered-error', 'invalid-src');
|
node.setAttribute('data-rendered-error', 'invalid-src');
|
||||||
if (verbose) debugShowImageLog('render:invalid-src', { renderId, rawSrc });
|
if (verbose) debugShowImageLog('render:invalid-src', { renderId, rawSrc });
|
||||||
|
|||||||
@ -4,7 +4,8 @@ import { debugLog } from '../common';
|
|||||||
import { useModelStore } from '../../../stores/model';
|
import { useModelStore } from '../../../stores/model';
|
||||||
|
|
||||||
// 兼容 Windows 反斜杠路径:skills 目录与 SKILL.md 前的分隔符同时接受 / 和 \
|
// 兼容 Windows 反斜杠路径:skills 目录与 SKILL.md 前的分隔符同时接受 / 和 \
|
||||||
export const SKILL_MARKDOWN_LINK_RE = /\[\$([^\]\n]+)\]\(([^)\n]*[\\/]\.astrion[\\/]skills[\\/][^)\n]+[\\/]SKILL\.md)\)/g;
|
// 路径前缀可选:后端自 cedef87d 起返回相对工作区路径(.astrion/skills/...),历史消息仍可能是绝对路径
|
||||||
|
export const SKILL_MARKDOWN_LINK_RE = /\[\$([^\]\n]+)\]\(((?:[^)\n]*[\\/])?\.astrion[\\/]skills[\\/][^)\n]+[\\/]SKILL\.md)\)/g;
|
||||||
|
|
||||||
export function extractSkillRefsFromMessage(message = '') {
|
export function extractSkillRefsFromMessage(message = '') {
|
||||||
const refs = [];
|
const refs = [];
|
||||||
|
|||||||
@ -1069,7 +1069,8 @@ const escapeUserHtml = (value: string): string =>
|
|||||||
.replace(/'/g, ''');
|
.replace(/'/g, ''');
|
||||||
|
|
||||||
// 兼容 Windows 反斜杠路径:skills 目录与 SKILL.md 前的分隔符同时接受 / 和 \
|
// 兼容 Windows 反斜杠路径:skills 目录与 SKILL.md 前的分隔符同时接受 / 和 \
|
||||||
const USER_SKILL_LINK_RE = /\[\$([^\]\n]+)\]\(([^)\n]*[\\/]\.astrion[\\/]skills[\\/][^)\n]+[\\/]SKILL\.md)\)/g;
|
// 路径前缀可选:后端自 cedef87d 起返回相对工作区路径(.astrion/skills/...),历史消息仍可能是绝对路径
|
||||||
|
const USER_SKILL_LINK_RE = /\[\$([^\]\n]+)\]\(((?:[^)\n]*[\\/])?\.astrion[\\/]skills[\\/][^)\n]+[\\/]SKILL\.md)\)/g;
|
||||||
const USER_FILE_LINK_RE = /\[([^\]\n]+)\]\(file:\/\/([^)\n]+)\)/g;
|
const USER_FILE_LINK_RE = /\[([^\]\n]+)\]\(file:\/\/([^)\n]+)\)/g;
|
||||||
const SUB_AGENT_DONE_LABEL_RE = /^子智能体\d+\s*任务完成$/;
|
const SUB_AGENT_DONE_LABEL_RE = /^子智能体\d+\s*任务完成$/;
|
||||||
const SUB_AGENT_DONE_PREFIX_RE = /^(?:✅\s*)?子智能体\s*#?\s*(\d+)\s*任务摘要[::]/;
|
const SUB_AGENT_DONE_PREFIX_RE = /^(?:✅\s*)?子智能体\s*#?\s*(\d+)\s*任务摘要[::]/;
|
||||||
|
|||||||
@ -1257,6 +1257,29 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== 明亮主题:摘要行降为三级文字色,与正文(text-primary)拉开层级 ===== */
|
||||||
|
/* hover 反馈不做特化:与经典/深色保持一致(基础 hover 底色在三主题下均等于底色,均无可见反馈) */
|
||||||
|
|
||||||
|
body[data-theme='light'] .summary-content-wrapper,
|
||||||
|
body[data-theme='light'] .summary-tool-reel-item {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme='light'] .summary-line-text.running .summary-char {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
animation-name: summaryPassLight;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes summaryPassLight {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* 步骤容器 */
|
/* 步骤容器 */
|
||||||
.steps-container {
|
.steps-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@ -247,7 +247,8 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1;
|
/* 不能压成 1:瘦高字体的下伸部(g/p/y 等)会超出行盒被 overflow 裁掉 */
|
||||||
|
line-height: 1.4;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -1161,7 +1162,8 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1;
|
/* 同 .qd-todo-text:line-height 压 1 会裁掉下伸字母 */
|
||||||
|
line-height: 1.4;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@ -1597,7 +1597,7 @@ const rootSlashMenuItems = computed<SlashMenuItem[]>(() => {
|
|||||||
{
|
{
|
||||||
id: 'run-mode',
|
id: 'run-mode',
|
||||||
label: '思考模式',
|
label: '思考模式',
|
||||||
description: '切换 fast / thinking / deep',
|
description: '切换 fast / thinking',
|
||||||
disabled: !props.isConnected || props.streamingMessage,
|
disabled: !props.isConnected || props.streamingMessage,
|
||||||
mode: 'run-mode'
|
mode: 'run-mode'
|
||||||
},
|
},
|
||||||
@ -3841,6 +3841,15 @@ onBeforeUnmount(() => {
|
|||||||
box-shadow: 0 1px 2px var(--shadow-color);
|
box-shadow: 0 1px 2px var(--shadow-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 经典主题:git 状态栏及子菜单与输入栏壳体对齐为纯白 --surface-raised
|
||||||
|
(基础 --theme-surface-soft 在经典为米色 #f5f0e8,与白色输入栏不一致) */
|
||||||
|
:global(html[data-theme='classic'] .floating-project-status),
|
||||||
|
:global(body[data-theme='classic'] .floating-project-status),
|
||||||
|
:global(html[data-theme='classic'] .floating-project-status__submenu),
|
||||||
|
:global(body[data-theme='classic'] .floating-project-status__submenu) {
|
||||||
|
background: var(--surface-raised);
|
||||||
|
}
|
||||||
|
|
||||||
.floating-status-motion-enter-active,
|
.floating-status-motion-enter-active,
|
||||||
.floating-status-motion-leave-active {
|
.floating-status-motion-leave-active {
|
||||||
transition:
|
transition:
|
||||||
|
|||||||
@ -2523,3 +2523,41 @@ body[data-theme='dark'] {
|
|||||||
background: var(--surface-muted) !important;
|
background: var(--surface-muted) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ========== 明亮主题变体(对话区组件) ========== */
|
||||||
|
/* 背景:浅色主题表面 token 阶梯近乎纯平(base/card/raised 全为 #ffffff),
|
||||||
|
基础样式中代码块白底落白底、表格 #f9f9f9 落白底均不可见;
|
||||||
|
参照上方深色覆盖块的做法,按用户确认的方向做组件级补偿:
|
||||||
|
代码块仅头部着色、代码区与对话底保持同色;表格白体+浅灰表头+可见边框。 */
|
||||||
|
body[data-theme='light'] {
|
||||||
|
/* 代码块 - 头部浅灰、代码区与对话底同为白色,外框加强到可见 */
|
||||||
|
.code-block-wrapper {
|
||||||
|
border-color: var(--border-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block-header {
|
||||||
|
background: var(--surface-muted);
|
||||||
|
border-bottom-color: var(--border-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格 - 白体 + 浅灰表头 + 可见边框 */
|
||||||
|
.text-output .text-content table {
|
||||||
|
background: var(--surface-raised);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-output .text-content thead {
|
||||||
|
background: var(--surface-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-output .text-content th,
|
||||||
|
.text-output .text-content td {
|
||||||
|
border-color: var(--border-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-output .text-content .md-table-scroll,
|
||||||
|
.text-output .text-content [data-md-table-scroll='1'] {
|
||||||
|
border-color: var(--border-strong);
|
||||||
|
background: var(--surface-raised);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -396,9 +396,16 @@ body[data-theme='light'] {
|
|||||||
color: var(--state-info);
|
color: var(--state-info);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 发送按钮 - 黑底白三角,浅阴影做层次 */
|
/* 输入栏壳体 - 与 git 状态栏同色的浅灰(基础 --surface-raised 在本主题为纯白,与对话底同色无层次;无法比白更浅,反向用略浅灰让壳体浮起) */
|
||||||
|
.stadium-shell,
|
||||||
|
.stadium-shell.is-focused,
|
||||||
|
.stadium-shell.has-text {
|
||||||
|
background: var(--surface-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 发送按钮 - 柔化黑底白三角(78% 黑),hover 加深为实黑,浅阴影做层次 */
|
||||||
.stadium-btn.send-btn {
|
.stadium-btn.send-btn {
|
||||||
background: var(--text-primary);
|
background: color-mix(in srgb, var(--text-primary) 78%, transparent);
|
||||||
color: var(--surface-raised);
|
color: var(--surface-raised);
|
||||||
box-shadow: 0 1px 3px var(--shadow-color);
|
box-shadow: 0 1px 3px var(--shadow-color);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import json
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import tempfile
|
import tempfile
|
||||||
import threading
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional, Any
|
from typing import Dict, List, Optional, Any
|
||||||
@ -24,78 +23,10 @@ try:
|
|||||||
except Exception:
|
except Exception:
|
||||||
perf_log = None
|
perf_log = None
|
||||||
|
|
||||||
try:
|
|
||||||
from config import LOGS_DIR as _CONV_SAVE_LOG_DIR
|
|
||||||
except Exception:
|
|
||||||
_CONV_SAVE_LOG_DIR = None
|
|
||||||
|
|
||||||
|
|
||||||
# save_conversation 专用哨兵:区分「不更新该字段」与「显式清除为 None」
|
# save_conversation 专用哨兵:区分「不更新该字段」与「显式清除为 None」
|
||||||
_KEEP = object()
|
_KEEP = object()
|
||||||
|
|
||||||
|
|
||||||
def _debug_conversation_save_trace(conversation_id: str, file_path, data: Dict):
|
|
||||||
"""[临时调试] 记录每次对话文件写入,重点捕获「非空被空覆盖」的调用栈。
|
|
||||||
|
|
||||||
输出: logs/conversation_save_debug.log (JSONL)
|
|
||||||
排查完「对话消息被清空」问题后应移除。
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
import traceback
|
|
||||||
new_msgs = data.get("messages") if isinstance(data, dict) else None
|
|
||||||
new_len = len(new_msgs) if isinstance(new_msgs, list) else -1
|
|
||||||
old_len = None
|
|
||||||
try:
|
|
||||||
p = Path(file_path)
|
|
||||||
if p.exists():
|
|
||||||
old_data = json.loads(p.read_text(encoding="utf-8"))
|
|
||||||
old_msgs = old_data.get("messages")
|
|
||||||
old_len = len(old_msgs) if isinstance(old_msgs, list) else None
|
|
||||||
except Exception:
|
|
||||||
old_len = None
|
|
||||||
wipe_suspect = bool(old_len and new_len == 0)
|
|
||||||
shrink_suspect = bool(old_len and 0 < new_len < old_len)
|
|
||||||
# 只保留项目内调用帧,排除标准库/第三方
|
|
||||||
try:
|
|
||||||
repo_root = Path(__file__).resolve().parents[2]
|
|
||||||
except Exception:
|
|
||||||
repo_root = None
|
|
||||||
frames = []
|
|
||||||
for fr in traceback.extract_stack()[:-2]:
|
|
||||||
fn = str(fr.filename)
|
|
||||||
if repo_root and not fn.startswith(str(repo_root)):
|
|
||||||
continue
|
|
||||||
# Windows 下调用帧文件名用反斜杠,统一归一化再匹配
|
|
||||||
fn_norm = fn.replace("\\", "/")
|
|
||||||
if "/.astrion/" in fn_norm or "site-packages" in fn_norm:
|
|
||||||
continue
|
|
||||||
rel = fn[len(str(repo_root)) + 1:] if repo_root and fn.startswith(str(repo_root)) else fn
|
|
||||||
frames.append(f"{rel}:{fr.lineno}:{fr.name}")
|
|
||||||
record = {
|
|
||||||
"ts": datetime.now().isoformat(),
|
|
||||||
"event": "conversation_save",
|
|
||||||
"conversation_id": conversation_id,
|
|
||||||
"old_msg_len": old_len,
|
|
||||||
"new_msg_len": new_len,
|
|
||||||
"wipe_suspect": wipe_suspect,
|
|
||||||
"shrink_suspect": shrink_suspect,
|
|
||||||
"title": (data.get("title") if isinstance(data, dict) else None),
|
|
||||||
"thread": threading.current_thread().name,
|
|
||||||
"stack_tail": frames[-14:],
|
|
||||||
}
|
|
||||||
if _CONV_SAVE_LOG_DIR:
|
|
||||||
log_dir = Path(_CONV_SAVE_LOG_DIR)
|
|
||||||
else:
|
|
||||||
log_dir = Path(DATA_DIR).parent / "logs"
|
|
||||||
log_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
with open(log_dir / "conversation_save_debug.log", "a", encoding="utf-8") as f:
|
|
||||||
f.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
||||||
if wipe_suspect or shrink_suspect:
|
|
||||||
kind = "空列表覆盖" if wipe_suspect else f"缩减 {old_len}->{new_len}"
|
|
||||||
print(f"🚨 [ConvSaveDebug] 对话 {conversation_id} 消息将被{kind}!栈: {frames[-14:]}")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ConversationMetadata:
|
class ConversationMetadata:
|
||||||
"""对话元数据"""
|
"""对话元数据"""
|
||||||
@ -266,7 +197,6 @@ class CrudMixin:
|
|||||||
try:
|
try:
|
||||||
# 确保Token统计数据有效
|
# 确保Token统计数据有效
|
||||||
data = self._validate_token_statistics(data)
|
data = self._validate_token_statistics(data)
|
||||||
_debug_conversation_save_trace(conversation_id, file_path, data)
|
|
||||||
with self._io_lock:
|
with self._io_lock:
|
||||||
self._atomic_write_json(file_path, data)
|
self._atomic_write_json(file_path, data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user