Compare commits

...

5 Commits

Author SHA1 Message Date
2f0b970060 chore(website-demo): 新增 mock 构建/校验/本地预览工具脚本
- build-mocks.cjs:从真实对话导出演示 mock 数据
- check-mocks.cjs:mock 数据完整性校验
- rebuild-cat-html.cjs / serve.cjs:分类页重建与本地预览服务
2026-09-03 21:54:58 +08:00
bd2cf0f56d feat(website-demo): SW mock 接口增强与演示默认深色主题
- 模型列表扩充至 5 个(K3/DeepSeek-V4-Pro/Flash/GLM-5.2/MiniMax-M3),补齐 success 字段
- 工作区「当前选中」改为会话态动态组装,修复切换工作区后 toast/高亮拿错工作区
- 演示默认主题从经典配色改为夜间配色
- 缓存版本 bump 至 v19
2026-09-03 21:54:37 +08:00
7fa2715ee3 feat(website-demo): 更新演示区 mock 对话数据与静态资源
- 新增 4 组真实对话 mock(bootstrap/tokens/activity/files/media)
- 更新 conversations-normal/ma 列表,移除旧版对比对话 mock
- 新增 apple-touch-icon
2026-09-03 21:54:22 +08:00
2482b42771 fix(config): 补导出 MAX_MESSAGE_CHARS,修复创建任务 ImportError
c724451d 新增该常量及两处 from config import 引用,但漏加入
config/limits.py 的 __all__,而 config/__init__.py 以 import * 聚合,
导致 /api/tasks 与 api_v1 发消息时惰性 import 必炸。
2026-09-02 23:58:19 +08:00
18b285aca1 fix(containers): 配额只统计 docker 句柄,修复 host 模式工作区列表 503
- ensure_container 的全局/每用户配额闸门仅在创建 docker 句柄时执行,
  host 句柄(会话标识,不占容器池)不再计入任何配额,修复 host 模式
  下第 4 个工作区起 GET /api/conversations 返回 503 的问题
- _has_capacity / has_capacity 全局上限计数同步只统计 docker 句柄
- MAX_ACTIVE_CONTAINERS_PER_USER 默认值 3 -> 8
- admin 面板 available_slots 展示对齐为 docker 句柄计数
2026-09-02 23:39:42 +08:00
46 changed files with 2454 additions and 50 deletions

View File

@ -97,6 +97,7 @@ __all__ = [
"CODE_EXECUTION_TIMEOUT", "CODE_EXECUTION_TIMEOUT",
"TERMINAL_COMMAND_TIMEOUT", "TERMINAL_COMMAND_TIMEOUT",
"SEARCH_MAX_RESULTS", "SEARCH_MAX_RESULTS",
"MAX_MESSAGE_CHARS",
"AUTO_FIX_TOOL_CALL", "AUTO_FIX_TOOL_CALL",
"AUTO_FIX_MAX_ATTEMPTS", "AUTO_FIX_MAX_ATTEMPTS",
"MAX_ITERATIONS_PER_TASK", "MAX_ITERATIONS_PER_TASK",

View File

@ -57,7 +57,8 @@ LINUX_SAFETY = os.environ.get("LINUX_SAFETY", "0") not in {"0", "false", "False"
TOOLBOX_TERMINAL_IDLE_SECONDS = int(os.environ.get("TOOLBOX_TERMINAL_IDLE_SECONDS", "900")) TOOLBOX_TERMINAL_IDLE_SECONDS = int(os.environ.get("TOOLBOX_TERMINAL_IDLE_SECONDS", "900"))
MAX_ACTIVE_USER_CONTAINERS = int(os.environ.get("MAX_ACTIVE_USER_CONTAINERS", "8")) MAX_ACTIVE_USER_CONTAINERS = int(os.environ.get("MAX_ACTIVE_USER_CONTAINERS", "8"))
# 每用户同时活跃的容器上限防单用户多工作区占满全局容器池2026-09-02 审计新增) # 每用户同时活跃的容器上限防单用户多工作区占满全局容器池2026-09-02 审计新增)
MAX_ACTIVE_CONTAINERS_PER_USER = int(os.environ.get("MAX_ACTIVE_CONTAINERS_PER_USER", "3")) # 仅统计 docker 句柄host 句柄不计入);默认值 8 与全局上限对齐2026-09-02 调整)
MAX_ACTIVE_CONTAINERS_PER_USER = int(os.environ.get("MAX_ACTIVE_CONTAINERS_PER_USER", "8"))
HOST_EXECUTION_MODE_DEFAULT = os.environ.get("HOST_EXECUTION_MODE_DEFAULT", "sandbox").strip().lower() HOST_EXECUTION_MODE_DEFAULT = os.environ.get("HOST_EXECUTION_MODE_DEFAULT", "sandbox").strip().lower()
# 沙箱可写路径的「部署通道」(逗号分隔)。路径授权只有两个来源: # 沙箱可写路径的「部署通道」(逗号分隔)。路径授权只有两个来源:
# config/host_sandbox_policy.json前端「路径授权」UI+ 本变量(真·环境变量), # config/host_sandbox_policy.json前端「路径授权」UI+ 本变量(真·环境变量),

View File

@ -158,18 +158,22 @@ class UserContainerManager:
handle.touch() handle.touch()
return handle return handle
if not self._has_capacity(key): # 配额检查仅针对 docker 句柄host 句柄只是会话标识,不占用容器池资源,
raise RuntimeError(tr("container_mgr.quota_exhausted")) # 创建 host 句柄时直接跳过全局/每用户两道配额闸门
if mode == "docker":
if not self._has_capacity(key):
raise RuntimeError(tr("container_mgr.quota_exhausted"))
# 每用户容器配额:防单用户多工作区占满全局容器池,挤占其他用户 # 每用户容器配额:防单用户多工作区占满全局容器池,挤占其他用户
per_user_limit = MAX_ACTIVE_CONTAINERS_PER_USER per_user_limit = MAX_ACTIVE_CONTAINERS_PER_USER
if per_user_limit > 0: if per_user_limit > 0:
owned = sum( owned = sum(
1 for k in self._containers 1 for k, h in self._containers.items()
if k == username_norm or k.startswith(f"{username_norm}::") if (k == username_norm or k.startswith(f"{username_norm}::"))
) and h.mode == "docker"
if owned >= per_user_limit: )
raise RuntimeError(tr("container_mgr.per_user_quota_exhausted", limit=per_user_limit)) if owned >= per_user_limit:
raise RuntimeError(tr("container_mgr.per_user_quota_exhausted", limit=per_user_limit))
# Important: create container using the cache key so each workspace gets its own container name. # Important: create container using the cache key so each workspace gets its own container name.
handle = self._create_handle(key, workspace, mode) handle = self._create_handle(key, workspace, mode)
@ -193,7 +197,8 @@ class UserContainerManager:
return True return True
if self.max_containers <= 0: if self.max_containers <= 0:
return True return True
return len(self._containers) < self.max_containers docker_total = sum(1 for h in self._containers.values() if h.mode == "docker")
return docker_total < self.max_containers
def get_handle(self, container_key: str) -> Optional[ContainerHandle]: def get_handle(self, container_key: str) -> Optional[ContainerHandle]:
key = self._normalize_username(container_key) key = self._normalize_username(container_key)
@ -293,8 +298,11 @@ class UserContainerManager:
def _has_capacity(self, username: str) -> bool: def _has_capacity(self, username: str) -> bool:
if self.max_containers <= 0: if self.max_containers <= 0:
return True return True
existing = 1 if username in self._containers else 0 # 全局上限同样只统计 docker 句柄host 句柄不占用容器池)
return (len(self._containers) - existing) < self.max_containers docker_total = sum(1 for h in self._containers.values() if h.mode == "docker")
existing_handle = self._containers.get(username)
existing = 1 if (existing_handle and existing_handle.mode == "docker") else 0
return (docker_total - existing) < self.max_containers
def _create_handle(self, username: str, workspace: str, mode: str) -> ContainerHandle: def _create_handle(self, username: str, workspace: str, mode: str) -> ContainerHandle:
if mode != "docker": if mode != "docker":

View File

@ -419,7 +419,9 @@ def build_api_admin_dashboard_snapshot():
max_containers = getattr(container_manager, "max_containers", None) or len(handle_map) max_containers = getattr(container_manager, "max_containers", None) or len(handle_map)
available_slots = None available_slots = None
if max_containers: if max_containers:
available_slots = max(0, max_containers - len(handle_map)) # 可用余量只对 docker 句柄计数host 句柄不占用容器池配额)
docker_total = sum(1 for val in handle_map.values() if val.get("mode") == "docker")
available_slots = max(0, max_containers - docker_total)
upload_events = collect_upload_events() upload_events = collect_upload_events()
uploads_summary = summarize_upload_events(upload_events, quarantine_total_bytes) uploads_summary = summarize_upload_events(upload_events, quarantine_total_bytes)

View File

@ -312,7 +312,8 @@ def collect_container_snapshots(handle_map: Dict[str, Dict[str, Any]]) -> Dict[s
"host": active_total - docker_count, "host": active_total - docker_count,
"issues": failure_count, "issues": failure_count,
"max_containers": container_manager.max_containers, "max_containers": container_manager.max_containers,
"available_slots": max(0, container_manager.max_containers - active_total) if container_manager.max_containers > 0 else None, # 可用余量只对 docker 句柄计数host 句柄不占用容器池配额)
"available_slots": max(0, container_manager.max_containers - docker_count) if container_manager.max_containers > 0 else None,
"avg_cpu_percent": round(sum(cpu_values) / len(cpu_values), 2) if cpu_values else None, "avg_cpu_percent": round(sum(cpu_values) / len(cpu_values), 2) if cpu_values else None,
"avg_mem_percent": round(sum(mem_percent_values) / len(mem_percent_values), 2) if mem_percent_values else None, "avg_mem_percent": round(sum(mem_percent_values) / len(mem_percent_values), 2) if mem_percent_values else None,
"total_mem_used_bytes": total_mem_used, "total_mem_used_bytes": total_mem_used,

View File

@ -56,9 +56,9 @@
<script> <script>
/* 演示引导:注册 mock SW等 controllerchange 后换路由、再加载真实前端 */ /* 演示引导:注册 mock SW等 controllerchange 后换路由、再加载真实前端 */
(function () { (function () {
// 固定演示呈现状态:经典配色 + 极简模式 + 快捷窗口自动展开,不受访客浏览器缓存影响 // 固定演示呈现状态:夜间配色 + 极简模式 + 快捷窗口自动展开,不受访客浏览器缓存影响
try { try {
localStorage.setItem('agents_ui_theme', 'classic'); localStorage.setItem('agents_ui_theme', 'dark');
localStorage.setItem('agents_personalization_experiments', JSON.stringify({ blockDisplayMode: 'minimal' })); localStorage.setItem('agents_personalization_experiments', JSON.stringify({ blockDisplayMode: 'minimal' }));
localStorage.setItem('agents_quick_dock_auto_expand', 'true'); localStorage.setItem('agents_quick_dock_auto_expand', 'true');
} catch (e) {} } catch (e) {}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1,24 @@
{"success": true, "data": {"conversations": [], "has_more": false, "limit": 20, "offset": 0, "total": 0}} {
"success": true,
"data": {
"conversations": [
{
"id": "conv_20260902_235153_495",
"title": "猫咪咖啡店官网demo制作",
"project_path": "/Users/demo/dev",
"project_relative_path": null,
"multi_agent_mode": true,
"thinking_mode": true,
"status": "active",
"total_messages": 185,
"total_tools": 87,
"created_at": "2026-09-02T23:51:53.495751",
"updated_at": "2026-09-03T20:39:59.397333"
}
],
"has_more": false,
"limit": 20,
"offset": 0,
"total": 1
}
}

View File

@ -1 +1,63 @@
{"success": true, "data": {"conversations": [{"id": "conv_20260823_122836_778", "title": "弹幕游戏美术设计调研", "project_path": "/Users/demo/starraid", "project_relative_path": null, "multi_agent_mode": false, "thinking_mode": true, "status": "active", "total_messages": 29, "total_tools": 13, "created_at": "2026-08-23T12:28:36.778945", "updated_at": "2026-08-23T13:32:51.486262"}, {"id": "conv_20260823_133304_218", "title": "尚界Z7与小米SU7外观对比", "project_path": "/Users/demo/daily", "project_relative_path": null, "multi_agent_mode": false, "thinking_mode": true, "status": "active", "total_messages": 11, "total_tools": 6, "created_at": "2026-08-23T13:33:04.218941", "updated_at": "2026-08-23T13:35:36.442941"}], "has_more": false, "limit": 20, "offset": 0, "total": 2}} {
"success": true,
"data": {
"conversations": [
{
"id": "conv_20260823_122836_778",
"title": "弹幕游戏美术设计调研",
"project_path": "/Users/demo/starraid",
"project_relative_path": null,
"multi_agent_mode": false,
"thinking_mode": true,
"status": "active",
"total_messages": 29,
"total_tools": 13,
"created_at": "2026-08-23T12:28:36.778945",
"updated_at": "2026-08-23T13:32:51.486262"
},
{
"id": "conv_20260825_210916_394",
"title": "整理周会速记为 Word",
"project_path": "/Users/demo/daily",
"project_relative_path": null,
"multi_agent_mode": false,
"thinking_mode": true,
"status": "active",
"total_messages": 32,
"total_tools": 15,
"created_at": "2026-08-25T21:09:16.394889",
"updated_at": "2026-09-03T00:28:37.188421"
},
{
"id": "conv_20260825_222209_771",
"title": "像素太空飞机设计稿",
"project_path": "/Users/demo/starraid",
"project_relative_path": null,
"multi_agent_mode": false,
"thinking_mode": true,
"status": "active",
"total_messages": 4,
"total_tools": 1,
"created_at": "2026-08-25T22:22:09.771765",
"updated_at": "2026-08-25T22:38:44.394880"
},
{
"id": "conv_20260903_003950_896",
"title": "尚界Z7与小米SU7外观",
"project_path": "/Users/demo/daily",
"project_relative_path": null,
"multi_agent_mode": false,
"thinking_mode": true,
"status": "active",
"total_messages": 12,
"total_tools": 6,
"created_at": "2026-09-03T00:39:50.896435",
"updated_at": "2026-09-03T00:42:05.089376"
}
],
"has_more": false,
"limit": 20,
"offset": 0,
"total": 4
}
}

View File

@ -0,0 +1,54 @@
11.12 周会速记(很乱,回头整理)
产品+技术+运营 三方对齐会
到场我、老王王磊、Lisa、测试小赵、运营Amanda老板后半程进来待了大概10分钟
3F 蒲公英会议室原定14:00开始实际14:15人才到齐
== 双十一大促 ==
Amanda预算批下来了 80w主推玩法 满300减50 + 前2小时折上折
节奏11.10 晚8点开始预热11.11 0点正式开抢11.12 返场一天(返场力度待定)
注意去年0点秒杀服务挂了12分钟被骂上热搜那次。老王拍胸脯说今年扩容3倍口头的待压测验证
压测 11.5 前必须出报告,负责人王磊,小赵配合
Amanda补充预算里含 12w KOL 投放6个腰部博主名单还没定下周给
这里好像和80w总数有点对不上会上忘了问待确认12w是80w里面的还是额外的
== App 3.2 版本 ==
- 首页改版Lisa 出终稿11.18 出内测包
- 搜索联想词 bug 还没修完用户已经投诉3次了其实10.28就提过一次,优先级要升 P0
- 安卓崩溃率 0.8%,要压到 0.3% 以下。小赵:主要集中在 OPPO 某几个机型,复现中
- iOS 提审风险3.2 的「关联启动」可能被卡,老王说先提审试试,不行再砍
- 内测包 11.18,正式版想赶 11.25,但看压测和崩溃率情况再说
15:02 老板进来了
老板:双十一 GMV 目标 8000w原话比较委婉但意思是完不成年终奖很难看
老板客服话术必须更新现在退费流程太长NPS 掉到 31 了,这个数很难看
老板走了以后大家沉默了大概半分钟
== 数据回顾Amanda 口头报的,回头要邮件确认准确数) ==
10月 DAU 42w环比 +6%
新客成本 38元/人比9月涨了4块
复购率 21%(?这个数我记的可能不准)
客单价 176 元
搜索到下单的转化率 3.2%Lisa 说首页改版主要就是为了拉这个
跑题记录:中午食堂新开的麻辣烫不错,小赵推荐了二楼咖啡,散会要去试试
== 待办(我自己随手记的,不全) ==
1. Lisa 首页终稿 11.15 前
2. 压测报告 11.5,老王
3. OPPO 机型崩溃复现小赵11.8
4. 双11活动页文案 Amanda 11.6
5. 我:整理这份会议纪要发全员
6. 客服话术新版 11.20 前出初稿——负责人是谁来着??会上好像说了又好像没说,待确认
7. KOL 名单 Amanda 下周
8. 搜索联想 bug 修复排期——老王说下版本前修掉,具体日期没定
== 杂事 ==
团建投票:安吉 vs 崇明,目前 7:5行政让周五前投完
小赵申请换显示器,审批走到我这了,提醒我记得点
老王提了一嘴想招一个实习生前端,年后再说
下次会11.19周三下午3点老地方 3F-蒲公英
议程预告:压测报告 review + 首页终稿 review + 双十一最终确认
哦对差点忘了:会议室投影仪又坏了,行政说报修了第三次了

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,57 @@
{
"success": true,
"agents": [
{
"agent_id": 1,
"role_id": "researcher",
"display_name": "Researcher_1",
"task_id": "sub_1_1788364427_985302",
"status": "idle",
"summary": "Researcher作业",
"created_at": 1788364427.560046,
"last_output": "调研素材已充分catmocha.jp 首页全文、% Arabica 官网案例、Blue Bottle 摄影语言、Meow Parlour/てまりのおしろ定位均已核实)。以下为最终汇报。\n\n---\n\n来自 Researcher_1 的任务进度输出\nid: out_task_b8b2a0cdc2\n\n## 猫咖 × 精品咖啡官网设计调研报告\n\n**素材来源**:本次通过实时搜索+页面抽取核实catmocha.jp、CGICA 对 % Arabica 官网案例分析、Blue Bottle 品牌/摄影方向案例等),网络工具可用,结论均基于真实案例。\n\n### 一、典型案例\n\n1. **Cat Caf",
"conversation_id": "conv_20260902_235153_495",
"last_tool": null,
"current_context_tokens": 0
},
{
"agent_id": 2,
"role_id": "researcher",
"display_name": "Researcher_2",
"task_id": "sub_2_1788364429_91688a",
"status": "idle",
"summary": "Researcher作业",
"created_at": 1788364429.548623,
"last_output": "已完成调研。基于 web_search + extract_webpage 的实时信息Nippon Colors 传统色体系、日式网页设计方法论文章、無印良品/中川政七案例),产出以下报告。\n\n---\n\n# 暖色系日式风格视觉调研报告\n\n**核心理念**:暖 ≠ 艳。日式高级感 = 低饱和高明度的大地色 + 大留白(\"八分满\"哲学),原研哉所谓\"隐藏在色彩中真正的白\"。\n\n## 一、配色方案3 套)\n\n**方案 A「茶室陶土」— 主推,最贴合猫咖烘焙感**\n| 角色 | 色名 | Hex | 比例 |\n|---|---|---|---|\n| 主色·背景 | 生成り(未漂麻米白) | `#F",
"conversation_id": "conv_20260902_235153_495",
"last_tool": null,
"current_context_tokens": 0
},
{
"agent_id": 3,
"role_id": "researcher",
"display_name": "Researcher_3",
"task_id": "sub_3_1788364431_74db3e",
"status": "idle",
"summary": "Researcher作业",
"created_at": 1788364431.575275,
"last_output": "调研完成。已核实 Kitty Brew、NEKO、Cat Cafe San Diego、Coffee Cats、Orlando、Tree House 等 6 家真实猫咖官网的栏目与字段设置,以下是结论报告。\n\n---\n\n## 猫咪咖啡店官网内容组织调研报告\n\n### 一、推荐单页板块顺序(首屏→页脚)\n\n**真实案例共性**:导航普遍为 `Home / Visit(预约) / Menu / Our Cats / FAQ`;首页 CTAMAKE A RESERVATION高频复现单页适合按下述顺序\n\n1. **Hero**:店名 + 标语 + 「立即预约」主按钮(可加\"预约建议/无需预约",
"conversation_id": "conv_20260902_235153_495",
"last_tool": null,
"current_context_tokens": 0
},
{
"agent_id": 4,
"role_id": "ui-operator",
"display_name": "UI Operator_1",
"task_id": "sub_4_1788364765_5d6208",
"status": "terminated",
"summary": "UI Operator作业",
"created_at": 1788364765.709567,
"last_output": "汇报已发出。交付物为 `cat-cafe/index.html`73KB 单文件),全部静态验证通过,视觉验收因沙箱限制留给用户确认。任务结束,等待下一步指令。",
"conversation_id": "conv_20260902_235153_495",
"last_tool": null,
"current_context_tokens": 0
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,33 @@
{
"success": true,
"data": [
{
"task_id": "sub_1_1788364427_985302",
"status": "completed",
"summary": "调研素材已充分catmocha.jp 首页全文、% Arabica 官网案例、Blue Bottle 摄影语言、Meo",
"conversation_id": "conv_20260902_235153_495",
"created_at": 1788364.427
},
{
"task_id": "sub_2_1788364429_91688a",
"status": "completed",
"summary": "已完成调研。基于 web_search + extract_webpage 的实时信息Nippon Colors 传统",
"conversation_id": "conv_20260902_235153_495",
"created_at": 1788364.429
},
{
"task_id": "sub_3_1788364431_74db3e",
"status": "completed",
"summary": "调研完成。已核实 Kitty Brew、NEKO、Cat Cafe San Diego、Coffee Cats、Orla",
"conversation_id": "conv_20260902_235153_495",
"created_at": 1788364.431
},
{
"task_id": "sub_4_1788364765_5d6208",
"status": "completed",
"summary": "子智能体已被手动终止",
"conversation_id": "conv_20260902_235153_495",
"created_at": 1788364.765
}
]
}

View File

@ -0,0 +1 @@
{"success":true,"data":{"overview":"调研→汇总→产出猫咪咖啡店官网单页 demo","tasks":[{"index":1,"title":"派 3 个子智能体并行调研:猫咖/精品咖啡官网设计、日式暖色风格、内容组织","status":"done"},{"index":2,"title":"汇总三份调研结论,形成设计简报","status":"done"},{"index":3,"title":"派 UI 子智能体基于简报编写单页 demo","status":"done"},{"index":4,"title":"验证 demo 文件并向用户汇报文件位置","status":"done"}],"status":"active","forced_finish":false,"forced_reason":null}}

View File

@ -0,0 +1,7 @@
{
"total_input_tokens": 413766,
"total_output_tokens": 7268,
"total_tokens": 421034,
"current_context_tokens": 56257,
"updated_at": "2026-08-23T13:32:51.486308"
}

View File

@ -0,0 +1,10 @@
{
"total_input_tokens": 735367,
"total_output_tokens": 15266,
"total_tokens": 750633,
"current_context_tokens": 45832,
"updated_at": "2026-09-03T00:28:37.188478",
"total_cached_input_tokens": 0,
"cache_exempt_input_tokens": 0,
"cache_cold_start_pending": false
}

View File

@ -0,0 +1,7 @@
{
"total_input_tokens": 40143,
"total_output_tokens": 27519,
"total_tokens": 67662,
"current_context_tokens": 21000,
"updated_at": "2026-08-25T22:38:44.368561"
}

View File

@ -0,0 +1,10 @@
{
"total_input_tokens": 4472987,
"total_output_tokens": 23563,
"total_tokens": 4496550,
"total_cached_input_tokens": 4389209,
"cache_exempt_input_tokens": 21249,
"cache_cold_start_pending": false,
"current_context_tokens": 77441,
"updated_at": "2026-09-03T20:39:59.397436"
}

View File

@ -0,0 +1,10 @@
{
"total_input_tokens": 212096,
"total_output_tokens": 5703,
"total_tokens": 217799,
"total_cached_input_tokens": 156416,
"cache_exempt_input_tokens": 24964,
"cache_cold_start_pending": false,
"current_context_tokens": 57569,
"updated_at": "2026-09-03T00:42:05.061150"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -8,16 +8,41 @@
*/ */
const DEMO_PAGE = '/demo.html'; const DEMO_PAGE = '/demo.html';
const CACHE = 'astrion-demo-v4'; const CACHE = 'astrion-demo-v19';
/* ── 静态响应体 ── */ /* ── 静态响应体 ── */
const MODELS = { const MODELS = {
success: true,
items: [ items: [
{ {
context_window: 256000, description: 'Kimi-K3', fast_only: false, context_window: 1048576, description: 'Kimi-K3', fast_only: false,
max_output_tokens: 32768, model_key: 'Kimi-K3', multimodal: 'image,video', max_output_tokens: 64000, model_key: 'Kimi-K3', multimodal: 'image,video',
name: 'Kimi-K3', supports_reasoning_effort: false, supports_thinking: true, name: 'Kimi-K3', supports_reasoning_effort: true, supports_thinking: true,
thinking_only: false, visible: true
},
{
context_window: 1000000, description: 'DeepSeek-V4-Pro', fast_only: false,
max_output_tokens: 384000, model_key: 'DeepSeek-V4-Pro', multimodal: 'none',
name: 'DeepSeek-V4-Pro', supports_reasoning_effort: true, supports_thinking: true,
thinking_only: false, visible: true
},
{
context_window: 1000000, description: 'DeepSeek-V4-Flash', fast_only: false,
max_output_tokens: 384000, model_key: 'DeepSeek-V4-Flash', multimodal: 'none',
name: 'DeepSeek-V4-Flash', supports_reasoning_effort: true, supports_thinking: true,
thinking_only: false, visible: true
},
{
context_window: 262144, description: 'GLM-5.2', fast_only: false,
max_output_tokens: 64072, model_key: 'GLM-5.2', multimodal: 'none',
name: 'GLM-5.2', supports_reasoning_effort: true, supports_thinking: true,
thinking_only: false, visible: true
},
{
context_window: 1000000, description: 'MiniMax-M3', fast_only: false,
max_output_tokens: 131072, model_key: 'MiniMax-M3', multimodal: 'none',
name: 'MiniMax-M3', supports_reasoning_effort: false, supports_thinking: true,
thinking_only: false, visible: true thinking_only: false, visible: true
} }
] ]
@ -28,18 +53,26 @@ const SESSION_STATUS = {
session: { host_mode: true, logged_in: true, role: 'admin', run_mode: 'thinking', thinking_mode: true, username: 'host' } session: { host_mode: true, logged_in: true, role: 'admin', run_mode: 'thinking', thinking_mode: true, username: 'host' }
}; };
const WORKSPACES = { // 工作区列表为静态数据「当前工作区」是会话态——select 接口会改它,
success: true, // 列表与 select 响应都从它动态组装;否则 current_workspace_id 永远钉死,
data: { // 前端切换后 toast/分组高亮会拿错工作区(实测弹窗恒显示「日常使用」)。
current_workspace_id: 'ws-daily', const WS_LIST = [
default_workspace_id: 'ws-daily', { workspace_id: 'ws-daily', label: '日常使用', path: '/Users/demo/daily', running_task_count: 0 },
workspaces: [ { workspace_id: 'ws-starraid', label: 'StarRaid', path: '/Users/demo/starraid', running_task_count: 0 },
{ workspace_id: 'ws-daily', label: '日常使用', path: '/Users/demo/daily', is_current: true, running_task_count: 0 }, { workspace_id: 'ws-dev', label: '开发实战', path: '/Users/demo/dev', running_task_count: 0 }
{ workspace_id: 'ws-starraid', label: 'StarRaid', path: '/Users/demo/starraid', is_current: false, running_task_count: 0 }, ];
{ workspace_id: 'ws-dev', label: '开发实战', path: '/Users/demo/dev', is_current: false, running_task_count: 0 } // 初始与 demo 默认落点(弹幕调研对话所属工作区)一致
] let currentWorkspaceId = 'ws-starraid';
} function buildWorkspaces() {
}; return {
success: true,
data: {
current_workspace_id: currentWorkspaceId,
default_workspace_id: 'ws-daily',
workspaces: WS_LIST.map((w) => ({ ...w, is_current: w.workspace_id === currentWorkspaceId }))
}
};
}
const UI_BLOCKS = { const UI_BLOCKS = {
block_compress_conversation: false, block_conversation_review: false, block_compress_conversation: false, block_conversation_review: false,
@ -52,24 +85,28 @@ const STATUS = {
success: true, success: true,
version: '2026-08-23', version: '2026-08-23',
host_mode: true, host_mode: true,
container: { username: 'demo', mode: 'host', workspace_path: '/Users/demo/daily', running: true },
admin_policy: { disabled_models: [], forced_category_states: {}, ui_blocks: UI_BLOCKS, version: '2026-08-23' } admin_policy: { disabled_models: [], forced_category_states: {}, ui_blocks: UI_BLOCKS, version: '2026-08-23' }
}; };
const PERSONALIZATION = { const PERSONALIZATION = {
success: true, success: true,
data: { data: {
theme: 'classic', theme: 'dark',
tool_intent_enabled: true,
group_sidebar_by_workspace: true,
block_display_mode: 'minimal', block_display_mode: 'minimal',
compact_message_display: 'brief', compact_message_display: 'brief',
communication_style: 'auto', communication_style: 'auto',
conversation_continuity: 'medium', conversation_continuity: 'medium',
agents_md_auto_inject: true, agents_md_auto_inject: true,
auto_deep_compress_enabled: true, auto_deep_compress_enabled: true,
deep_compress_trigger_tokens: 200000,
auto_generate_title: true, auto_generate_title: true,
auto_shallow_compress_enabled: false auto_shallow_compress_enabled: false
}, },
context_compression_settings: { context_compression_settings: {
context_window_tokens: 256000, deep_trigger_tokens: 256000, context_window_tokens: 256000, deep_trigger_tokens: 200000,
shallow_keep_recent_tools: 15, shallow_keep_user_turn_tools: 3, shallow_keep_recent_tools: 15, shallow_keep_user_turn_tools: 3,
shallow_max_replace_per_round: 10, shallow_trigger_tokens: 1000, shallow_max_replace_per_round: 10, shallow_trigger_tokens: 1000,
shallow_trigger_tool_calls_interval: 10 shallow_trigger_tool_calls_interval: 10
@ -81,7 +118,14 @@ const PERMISSION_MODE = { success: true, mode: 'unrestricted', options: ['readon
const EXECUTION_MODE = { success: true, enabled: true, options: ['sandbox', 'direct'], pending_mode: null, state: { default_mode: 'sandbox', mode: 'sandbox' } }; const EXECUTION_MODE = { success: true, enabled: true, options: ['sandbox', 'direct'], pending_mode: null, state: { default_mode: 'sandbox', mode: 'sandbox' } };
const NETWORK_PERMISSION = { success: true, enabled: true, mode: 'restricted', options: ['restricted', 'full', 'none'], pending_mode: null }; const NETWORK_PERMISSION = { success: true, enabled: true, mode: 'restricted', options: ['restricted', 'full', 'none'], pending_mode: null };
const TUTORIAL = { success: true, data: { applicable: false, should_prompt: false, tutorial_completed: true, username: 'host' } }; const TUTORIAL = { success: true, data: { applicable: false, should_prompt: false, tutorial_completed: true, username: 'host' } };
const USAGE = { success: true, data: { quotas: { fast: { count: 0, limit: 200 }, thinking: { count: 0, limit: 200 }, search: { count: 0, limit: 20 } }, role: 'user' } }; // 宿主机模式不统计用量record_model_call/record_search_call 直接 returnquotas count 恒为 0、
// window_start/reset_at 为 nullget_quota_snapshot 行为limit 为配置默认值。勿编造非 0 数字。
const USAGE = { success: true, data: { quotas: { fast: { count: 0, limit: 200, window_start: null, reset_at: null }, thinking: { count: 0, limit: 200, window_start: null, reset_at: null }, search: { count: 0, limit: 20, window_start: null, reset_at: null } }, role: 'user' } };
// ── Token 统计 mock右下角 token 菜单/抽屉数据源)──
// 数据为每条对话的真实 token_statistics由 build-mocks.cjs 导出为 mock/tokens-<id>.json。
// 当前上下文走 /api/conversations/{id}/tokens统计区走 /token-statistics。上限 200k 由
// PERSONALIZATION 的 deep_compress_trigger_tokens 决定(深度压缩阈值)。
const EMPTY_OK = { success: true, data: {} }; const EMPTY_OK = { success: true, data: {} };
const EMPTY_LIST = { success: true, data: [] }; const EMPTY_LIST = { success: true, data: [] };
const GIT_SUMMARY = { success: true, data: { available: false } }; const GIT_SUMMARY = { success: true, data: { available: false } };
@ -128,11 +172,34 @@ self.addEventListener('fetch', (e) => {
if (!url.pathname.startsWith('/api/')) return; // 静态资源放行 if (!url.pathname.startsWith('/api/')) return; // 静态资源放行
e.respondWith(routeApi(url)); e.respondWith(routeApi(url, e.request));
}); });
async function routeApi(url) { // ── 模拟流式回复:演示站发送消息时,返回固定文案「实际功能请部署后体验」逐字流式吐出 ──
// 协议对齐真实后端POST /api/tasks 创建任务GET /api/tasks/{id}?from=N 增量拉事件
// text_start → 逐字 text_chunk → text_end → task_completestatus running→succeeded
// 逐字 100ms用户拍板前端轮询周期 250ms每轮稳定拿 2~3 字,节奏快而匀。
const DEMO_REPLY = '实际功能请部署后体验';
const DEMO_TASK_EVENT_INTERVAL_MS = 100;
let demoTask = null; // { taskId, convId, startTs, events }SW 休眠后丢失,重发即可
function buildDemoReplyEvents() {
const evts = [{ idx: 0, type: 'text_start', data: {} }];
Array.from(DEMO_REPLY).forEach((ch, i) => {
evts.push({ idx: i + 1, type: 'text_chunk', data: { content: ch } });
});
evts.push({ idx: DEMO_REPLY.length + 1, type: 'text_end', data: { full_content: DEMO_REPLY } });
evts.push({
idx: DEMO_REPLY.length + 2,
type: 'task_complete',
data: { has_running_sub_agents: false, has_running_background_commands: false, has_running_multi_agent: false }
});
return evts;
}
async function routeApi(url, req) {
const p = url.pathname; const p = url.pathname;
const method = req ? req.method : 'GET';
// 子智能体活动详情 // 子智能体活动详情
const actMatch = p.match(/^\/api\/sub_agents\/(sub_[\w-]+)\/activity$/); const actMatch = p.match(/^\/api\/sub_agents\/(sub_[\w-]+)\/activity$/);
@ -150,21 +217,72 @@ async function routeApi(url) {
return json({ success: true, data: [] }); return json({ success: true, data: [] });
} }
// 多智能体活跃实例(演示无) // 多智能体活跃实例猫咪对话conv_20260902_235153_495有 4 个实例,其余对话为空。
if (p === '/api/multiagent/active_sub_agents') return json({ success: true, agents: [] }); // 这是 QuickDock 子智能体窗口在多智能体对话下的唯一数据源,钉空会让快捷窗口无法展开。
if (p === '/api/multiagent/active_sub_agents') {
const convId = url.searchParams.get('conversation_id') || '';
const r = await fetch(`/mock/ma-agents-${convId}.json`);
if (r.ok) return new Response(await r.text(), { headers: { 'Content-Type': 'application/json; charset=utf-8' } });
return json({ success: true, agents: [] });
}
// 待办 / 后台命令(演示对话为空) // 待办 / 后台命令(演示对话为空)
if (p === '/api/todo-list') return json({ success: true, data: null }); // 待办列表:有 todo mock 的对话如猫咪返回真实内容否则空——QuickDock 待办窗口数据源
if (p === '/api/todo-list') {
const convId = url.searchParams.get('conversation_id') || '';
if (convId) {
const r = await fetch(`/mock/todo-${convId}.json`);
if (r.ok) return new Response(await r.text(), { headers: { 'Content-Type': 'application/json; charset=utf-8' } });
}
return json({ success: true, data: null });
}
if (p === '/api/background_commands') return json({ success: true, data: [] }); if (p === '/api/background_commands') return json({ success: true, data: [] });
// 文件预览(快捷窗口点文件名):返回真实调研报告内容 // 文件预览/下载(对话内文件卡片、下载链接)
if (p === '/api/file/content') { if (p === '/api/file/content') {
const name = (url.searchParams.get('path') || '').split('/').pop(); const name = (url.searchParams.get('path') || '').split('/').pop();
// 二进制/富文本资源放 mock/files/(按扩展名给 MIME
const ext = (name.split('.').pop() || '').toLowerCase();
const MIME = {
pdf: 'application/pdf',
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
html: 'text/html; charset=utf-8',
png: 'image/png',
jpg: 'image/jpeg',
};
if (MIME[ext]) {
const rb = await fetch(`/mock/files/${name}`);
if (rb.ok) return new Response(await rb.arrayBuffer(), { headers: { 'Content-Type': MIME[ext] } });
}
// 文本资源mock/file-*
const r = await fetch(`/mock/file-${name}`); const r = await fetch(`/mock/file-${name}`);
if (r.ok) return new Response(await r.text(), { headers: { 'Content-Type': 'text/plain; charset=utf-8' } }); if (r.ok) return new Response(await r.text(), { headers: { 'Content-Type': 'text/plain; charset=utf-8' } });
return new Response('演示环境没有该文件', { status: 404 }); return new Response('演示环境没有该文件', { status: 404 });
} }
// 消息内嵌媒体view_image 截图等mock/media/{sha}.png
const mediaMatch = p.match(/^\/api\/conversations\/media\/(.+)$/);
if (mediaMatch) {
const id = decodeURIComponent(mediaMatch[1]).replace(/^sha256:/, '');
const r = await fetch(`/mock/media/${id}.png`);
if (r.ok) return new Response(await r.arrayBuffer(), { headers: { 'Content-Type': 'image/png' } });
return new Response('演示环境没有该媒体', { status: 404 });
}
// 对话 token当前上下文 / 统计区(均读真实导出的 mock/tokens-<id>.json
const tokensMatch = p.match(/^\/api\/conversations\/(conv_[\w-]+)\/tokens$/);
if (tokensMatch) {
const r = await fetch(`/mock/tokens-${tokensMatch[1]}.json`);
const d = r.ok ? await r.json() : null;
return json({ success: true, data: { total_tokens: (d && d.current_context_tokens) || 0 } });
}
const tokenStatsMatch = p.match(/^\/api\/conversations\/(conv_[\w-]+)\/token-statistics$/);
if (tokenStatsMatch) {
const r = await fetch(`/mock/tokens-${tokenStatsMatch[1]}.json`);
if (r.ok) return json({ success: true, data: await r.json() });
return json({ success: true, data: { total_input_tokens: 0, total_output_tokens: 0, total_tokens: 0, total_cached_input_tokens: 0, cache_exempt_input_tokens: 0, current_context_tokens: 0 } });
}
// 对话 bootstrap // 对话 bootstrap
const bootMatch = p.match(/^\/api\/conversations\/(conv_[\w-]+)\/bootstrap$/); const bootMatch = p.match(/^\/api\/conversations\/(conv_[\w-]+)\/bootstrap$/);
if (bootMatch) { if (bootMatch) {
@ -173,18 +291,41 @@ async function routeApi(url) {
return json({ success: false, error: '对话不存在' }, 404); return json({ success: false, error: '对话不存在' }, 404);
} }
// 对话列表 // 版本控制状态:必须显式 mock前端 fetchVersioningStatus 会用 payload.host_mode
// 覆盖全局 versioningHostMode若落到兜底 EMPTY_OKdata:{}host_mode=undefined
// 会把 host 模式打回 false分组视图立刻消失且 persist 进 localStorage 持续作恶。
const versioningMatch = p.match(/^\/api\/conversations\/(conv_[\w-]+)\/versioning$/);
if (versioningMatch) {
return json({ success: true, data: { host_mode: true, enabled: false, mode: 'overwrite' } });
}
// 对话列表(支持 workspace_id 过滤:按 WORKSPACES 的 path 匹配对话 project_path
if (p === '/api/conversations') { if (p === '/api/conversations') {
const ma = url.searchParams.get('multi_agent_mode') === '1'; const ma = url.searchParams.get('multi_agent_mode') === '1';
const r = await fetch(`/mock/conversations-${ma ? 'ma' : 'normal'}.json`); const r = await fetch(`/mock/conversations-${ma ? 'ma' : 'normal'}.json`);
return new Response(await r.text(), { headers: { 'Content-Type': 'application/json; charset=utf-8' } }); const wsId = url.searchParams.get('workspace_id');
if (!wsId) {
return new Response(await r.text(), { headers: { 'Content-Type': 'application/json; charset=utf-8' } });
}
const all = await r.json();
const ws = WS_LIST.find((w) => w.workspace_id === wsId);
const list = (all.data.conversations || []).filter((c) => !ws || c.project_path === ws.path);
return json({ success: true, data: { conversations: list, has_more: false, limit: 50, offset: 0, total: list.length } });
} }
// 各状态端点 // 各状态端点
if (p === '/api/session-status') return json(SESSION_STATUS); if (p === '/api/session-status') return json(SESSION_STATUS);
if (p === '/api/csrf-token') return json({ success: true, csrf_token: 'demo' }); if (p === '/api/csrf-token') return json({ success: true, csrf_token: 'demo' });
if (p === '/api/v1/models') return json(MODELS); if (p === '/api/v1/models') return json(MODELS);
if (p === '/api/host/workspaces') return json(WORKSPACES); if (p === '/api/host/workspaces/select') {
const target = url.searchParams.get('workspace_id') || '';
const ws = WS_LIST.find((w) => w.workspace_id === target);
if (ws) currentWorkspaceId = ws.workspace_id;
// 故意不返回 project_path前端 toast 取值顺序是 project_path || 工作区名 || id
// 演示区路径(/Users/demo/xxx没有展示价值缺省后 toast 显示工作区名称。
return json({ success: true, data: { current_workspace_id: currentWorkspaceId, default_workspace_id: 'ws-daily', workspace_label: ws ? ws.label : '' } });
}
if (p === '/api/host/workspaces') return json(buildWorkspaces());
if (p === '/api/status') return json(STATUS); if (p === '/api/status') return json(STATUS);
if (p === '/api/personalization') return json(PERSONALIZATION); if (p === '/api/personalization') return json(PERSONALIZATION);
if (p === '/api/work-mode') return json(WORK_MODE); if (p === '/api/work-mode') return json(WORK_MODE);
@ -196,6 +337,41 @@ async function routeApi(url) {
if (p === '/api/project/git-summary') return json(GIT_SUMMARY); if (p === '/api/project/git-summary') return json(GIT_SUMMARY);
if (p === '/api/health') return json({ status: 'ok' }); if (p === '/api/health') return json({ status: 'ok' });
if (p === '/api/focused') return json({}); if (p === '/api/focused') return json({});
// 发送消息 → 创建模拟任务(前端随后按 task_id 轮询事件)
if (p === '/api/tasks' && method === 'POST') {
const body = await req.json().catch(() => ({}));
demoTask = {
taskId: `demo_task_${Date.now()}`,
convId: String(body.conversation_id || ''),
startTs: Date.now(),
events: buildDemoReplyEvents(),
};
return json({ success: true, data: { task_id: demoTask.taskId, status: 'running', created_at: new Date().toISOString() } });
}
// 模拟任务轮询:按经过时间逐步放事件,放完后 status=succeeded前端停止轮询
const demoTaskMatch = p.match(/^\/api\/tasks\/(demo_task_\d+)$/);
if (demoTaskMatch) {
if (!demoTask || demoTask.taskId !== demoTaskMatch[1]) {
return json({ success: false, error: '任务不存在或已过期' }, 404);
}
const from = Math.max(0, Number(url.searchParams.get('from') || 0));
const visible = Math.min(demoTask.events.length, Math.floor((Date.now() - demoTask.startTs) / DEMO_TASK_EVENT_INTERVAL_MS));
const done = visible >= demoTask.events.length;
return json({
success: true,
data: {
task_id: demoTask.taskId,
status: done ? 'succeeded' : 'running',
events: demoTask.events.slice(from, visible),
next_offset: visible,
conversation_id: demoTask.convId,
updated_at: new Date().toISOString(),
}
});
}
if (p === '/api/tasks' || p === '/api/terminals') return json(EMPTY_LIST); if (p === '/api/tasks' || p === '/api/terminals') return json(EMPTY_LIST);
if (p === '/api/tool-settings') return json({ success: true, categories: [] }); if (p === '/api/tool-settings') return json({ success: true, categories: [] });
if (p === '/api/effective-policy') return json({ success: true, data: { categories: {} } }); if (p === '/api/effective-policy') return json({ success: true, data: { categories: {} } });

View File

@ -0,0 +1,411 @@
#!/usr/bin/env node
/**
* 官网演示 mock 构建脚本一次性含人工手术逻辑
* 输入用户真实对话 JSON~/.astrion
* 输出website-design/exp/mock/ 下的 bootstrap/列表/activity/媒体 mock
* 手术内容
* - word vlm_analyze失败+ send_qq_file私人工具+ 预览图生成段改标题/首尾文案todo 4 项改 3
* - cat 3 sleep 超时等待含对应思考文案续接其余不动
* - 全量路径改写 /Users/jojo/... /Users/demo/...剥离 edit_summary
*/
const fs = require('fs');
const path = require('path');
const EXP = path.join(__dirname, '..', 'exp');
const MOCK = path.join(EXP, 'mock');
const MEDIA_OUT = path.join(MOCK, 'media');
const MEDIA_STORE = '/Users/jojo/.astrion/astrion/host/data/conversations/media_store';
const SUB_TASKS_DIR = '/Users/jojo/.astrion/astrion/host/data/sub_agent_tasks';
const CONVS = {
car: {
src: '/Users/jojo/.astrion/astrion/host/data/conversations/workspace-6/conv_20260903_003950_896.json',
id: 'conv_20260903_003950_896', oldBase: '/Users/jojo/Desktop/游戏', newBase: '/Users/demo/daily', ma: false,
model: 'DeepSeek-V4-Flash', // 真实模型(演示站修正用户库里的 Flah 笔误)
},
word: {
src: '/Users/jojo/.astrion/astrion/host/data/conversations/workspace-3/conv_20260825_210916_394.json',
id: 'conv_20260825_210916_394', oldBase: '/Users/jojo/Desktop/整理资料', newBase: '/Users/demo/daily', ma: false,
title: '整理周会速记为 Word',
},
plane: {
src: '/Users/jojo/.astrion/astrion/host/data/conversations/workspace-8/conv_20260825_222209_771.json',
id: 'conv_20260825_222209_771', oldBase: '/Users/jojo/Desktop/个人网站', newBase: '/Users/demo/starraid', ma: false,
},
cat: {
src: '/Users/jojo/.astrion/astrion/host/mutiagents/conversations/workspace/conv_20260902_235153_495.json',
id: 'conv_20260902_235153_495', oldBase: '/Users/jojo/Desktop/语音', newBase: '/Users/demo/dev', ma: true,
},
};
// ── 通用工具 ──────────────────────────────────────────────
function makeSanitizer(oldBase, newBase) {
return function sanitize(s) {
if (typeof s !== 'string') return s;
return s
.split(oldBase).join(newBase)
.replace(/\/Users\/jojo/g, '/Users/demo')
.replace(/\bjojo\b/g, 'demo');
};
}
function deepSanitize(v, san) {
if (typeof v === 'string') return san(v);
if (Array.isArray(v)) return v.map((x) => deepSanitize(x, san));
if (v && typeof v === 'object') {
const o = {};
for (const [k, val] of Object.entries(v)) o[k] = deepSanitize(val, san);
return o;
}
return v;
}
const MSG_KEYS = ['role', 'content', 'reasoning_content', 'timestamp', 'message_id', 'metadata', 'tool_calls', 'tool_call_id', 'name'];
const META_KEYS = ['message_source', 'visibility', 'starts_work', 'work_timer', 'model_key', 'tool_payload', 'files', 'hidden', 'source', 'citations', 'runtime_injected', 'inline', 'is_auto_generated', 'auto_message_type', 'multi_agent_message', 'multi_agent_display_name', 'multi_agent_subtype', 'runtime_guidance', 'runtime_guidance_original', 'tool_image_path', 'media_refs'];
// 演示站 MODELS 只列 Kimi-K3消息里的模型名统一归一纯展示字段
const DEMO_MODEL = 'Kimi-K3';
function transformMessage(m, san, demoModel) {
const o = {};
for (const k of MSG_KEYS) if (m[k] !== undefined) o[k] = m[k];
if (o.metadata) {
const meta = {};
for (const k of META_KEYS) if (m.metadata[k] !== undefined) meta[k] = m.metadata[k];
if (meta.model_key) meta.model_key = demoModel;
o.metadata = meta;
}
return deepSanitize(o, san);
}
function replaceOnce(haystack, needle, replacement, label) {
if (!haystack || !haystack.includes(needle)) throw new Error(`手术失败:未找到文本 [${label}]`);
return haystack.split(needle).join(replacement);
}
function replaceOnceRe(haystack, re, replacement, label) {
if (!haystack || !re.test(haystack)) throw new Error(`手术失败:正则未命中 [${label}]`);
return haystack.replace(re, replacement);
}
// ── word 手术 ──────────────────────────────────────────────
function wordSurgery(msgs) {
const cut = new Set();
const idxOf = (pred, label) => {
const i = msgs.findIndex(pred);
if (i < 0) throw new Error('word: 未找到 ' + label);
return i;
};
const cutPair = (iTool, label) => {
if (msgs[iTool].role !== 'tool' || msgs[iTool - 1].role !== 'assistant') throw new Error('word: 配对异常 ' + label);
cut.add(iTool); cut.add(iTool - 1);
};
// Quick Look 缩略图生成对preview png 只服务被剪掉的 vlm
cutPair(idxOf((m) => m.role === 'tool' && typeof m.content === 'string' && m.content.includes('Quick Look thumbnails'), '缩略图结果'), '缩略图');
// ls 预览图对
cutPair(idxOf((m) => m.role === 'tool' && typeof m.content === 'string' && m.content.includes('_preview_page1.png'), '预览图ls'), '预览图ls');
// vlm_analyze 对(失败调用)
cutPair(idxOf((m) => m.role === 'tool' && m.name === 'vlm_analyze', 'vlm结果'), 'vlm');
// send_qq_file 结果(其 assistant 只摘 tool_call保留 todo 调用)
cut.add(idxOf((m) => m.role === 'tool' && m.name === 'send_qq_file', 'qq结果'));
// 收尾 todo 对("进度 4/4"
cutPair(idxOf((m) => m.role === 'tool' && m.name === 'todo_update_task' && String(m.content).includes('进度 4/4'), '收尾todo'), '收尾todo');
const kept = msgs.filter((_, i) => !cut.has(i));
if (msgs.length - kept.length !== 9) throw new Error('word: 应剪 9 条,实际 ' + (msgs.length - kept.length));
// 首条用户消息去掉"然后发给我"
const u0 = kept.find((m) => m.role === 'user' && String(m.content).includes('发给我'));
u0.content = replaceOnce(u0.content, '帮我整理这份文件为word然后发给我', '帮我整理这份文件为word', '首条用户消息');
// todo_create 所在 assistant改思考 + 重写 todo 参数4 项 → 3 项,去发送)
const todoAsst = kept.find((m) => (m.tool_calls || []).some((t) => t.function.name === 'todo_create'));
todoAsst.reasoning_content = replaceOnce(todoAsst.reasoning_content,
'5. 发送给用户(用户说"发给我"——可以用 send_qq_file 发送到 QQ 群,这是用户的习惯;也可以提供下载链接)',
'5. 交付给用户(提供下载链接,并转一份 PDF 用文件卡片直接预览)', '思考-交付方式1');
todoAsst.reasoning_content = replaceOnceRe(todoAsst.reasoning_content,
/用户说"发给我",考虑到有 send_qq_file 工具[\s\S]*?同时提供下载链接。/,
'交付方式:生成 Word 后提供工作区下载链接,并转换一份 PDF 用文件卡片预览,方便直接查看。', '思考-交付方式2');
todoAsst.reasoning_content = replaceOnce(todoAsst.reasoning_content, '5. 发送\n', '5. 交付\n', '思考-交付方式3');
for (const t of todoAsst.tool_calls) {
if (t.function.name !== 'todo_create') continue;
const args = JSON.parse(t.function.arguments);
args.overview = '将周会速记整理为结构化 Word 会议纪要';
args.tasks = args.tasks.filter((x) => !x.title.includes('发送'));
if (args.tasks.length !== 3) throw new Error('word: todo 应为 3 项');
t.function.arguments = JSON.stringify(args);
}
// todo_create 结果
const todoRes = kept.find((m) => m.role === 'tool' && m.name === 'todo_create');
todoRes.content = '已创建 TODO将周会速记整理为结构化 Word 会议纪要(共 3 项)';
// 清理/交付 assistant原 vlm 失败后的那条重写思考与正文rm 命令去掉预览图
const cleanup = kept.find((m) => m.role === 'assistant' && String(m.content).includes('VLM 配置缺失'));
cleanup.reasoning_content = '三层验证已经足够:\n1. docx-js 生成成功\n2. zip 结构完整,所有 XML 可解析\n3. LibreOffice 成功渲染为 PDF233KB3页——这是最强的验证LibreOffice 能正常打开并渲染,说明文档在 Word/WPS 中也能正常打开\n\n中文字体我用了宋体/微软雅黑eastAsia这是标准做法LibreOffice 渲染成功说明字体回退正常。\n\n现在收尾\n1. 删除临时脚本 _gen_minutes.js\n2. 更新待办\n3. 交付:提供下载链接 + 文件预览\n\n先清理临时文件。';
cleanup.content = '三重验证都已通过zip 结构完整、XML 全部可解析、LibreOffice 成功渲染 3 页 PDF文档可用性已确认。清理临时文件';
for (const t of cleanup.tool_calls) {
t.function.arguments = t.function.arguments.replace('rm -f _gen_minutes.js .astrion/user_upload/_preview_page1.png &&', 'rm -f _gen_minutes.js &&');
}
// send_qq_file 所在 assistant摘掉该 tool_call保留 todo_update_task
const deliver = kept.find((m) => (m.tool_calls || []).some((t) => t.function.name === 'send_qq_file'));
deliver.tool_calls = deliver.tool_calls.filter((t) => t.function.name !== 'send_qq_file');
// 其 todo 结果文案3/4 → 全部完成 3/3
const todoRes2 = kept.find((m) => m.role === 'tool' && m.name === 'todo_update_task' && String(m.content).includes('进度 3/4'));
todoRes2.content = '所有任务已完成。;进度 3/3';
// 最终总结:去掉 QQ 发送表述
const final = kept.find((m) => m.role === 'assistant' && String(m.content).startsWith('搞定!'));
final.content = replaceOnce(final.content, '搞定!文件已整理成 Word 并通过 QQ 发送,同时也保留在工作区,可随时下载:',
'搞定!文件已整理成 Word保存在工作区可随时下载', '最终总结正文');
final.reasoning_content = replaceOnce(final.reasoning_content, '文件已发送到 QQ + 提供下载链接。', '提供下载链接与文件预览。', '最终总结思考');
// todo 进度 1/4 → 1/3任务数从 4 改为 3
const todoRes1 = kept.find((m) => m.role === 'tool' && m.name === 'todo_update_task' && String(m.content).includes('进度 1/4'));
if (todoRes1) todoRes1.content = String(todoRes1.content).replace('进度 1/4', '进度 1/3');
// 其余 assistant 思考中的发送/QQ 残留(首条用户消息已改,相关思考同步改)
const r2 = kept.find((m) => m.role === 'assistant' && String(m.reasoning_content).includes('可能是通过 QQ 或下载链接'));
r2.reasoning_content = replaceOnce(r2.reasoning_content, '整理成 Word 文档,然后发给他。', '整理成 Word 文档。', '思考2-开头');
r2.reasoning_content = replaceOnce(r2.reasoning_content, '4. 发送给用户(可能是通过 QQ 或下载链接)', '4. 交付给用户(提供下载链接,并转一份 PDF 预览)', '思考2-交付');
r2.reasoning_content = replaceOnceRe(r2.reasoning_content, /用户说"发给我",工作区里有 send_qq_file 工具[\s\S]*?(如果那是用户习惯的方式)。/, '交付方式:生成 Word 文件后提供下载链接,并转换一份 PDF 用文件卡片预览,方便直接查看。', '思考2-发给我');
const r8 = kept.find((m) => m.role === 'assistant' && String(m.reasoning_content).includes('验证通过后删除脚本'));
r8.reasoning_content = replaceOnce(r8.reasoning_content, '验证通过后删除脚本,然后 send_qq_file 发送 + 提供下载链接。', '验证通过后删除脚本,然后提供下载链接与文件预览。', '思考8-交付');
r8.reasoning_content = replaceOnceRe(r8.reasoning_content, /用户说"整理这份文件为word然后发给我",重点是发给他。/, '用户要的是整理成 Word。', '思考8-发给我');
const r17 = kept.find((m) => m.role === 'assistant' && String(m.reasoning_content).includes('send_qq_file 到 QQ 群'));
r17.reasoning_content = replaceOnceRe(r17.reasoning_content, /然后:\n1\. 删除临时脚本 _gen_minutes\.js\n2\. 发送给用户[\s\S]*?同时提供下载链接。/, '然后:\n1. 删除临时脚本 _gen_minutes.js\n2. 交付给用户:提供下载链接 + 文件预览卡片', '思考17-交付');
// 残留扫描(硬失败,确保清理干净)
const rest = JSON.stringify(kept);
for (const bad of ['send_qq_file', 'vlm_analyze', 'VLM', 'QQ', '发给我', '发给他', '视觉模型']) {
if (rest.includes(bad)) throw new Error('word 残留关键词: ' + bad);
}
return kept;
}
// ── plane 手术:卡片按 620px 基准设计,演示区实际渲染 580px两侧标注会被裁
// 注入等比缩放脚本(卡片内外同为 3:4按宽缩放即满填。仅改 mock不动原对话。 ──
function planeSurgery(msgs) {
const FIT = [
'<' + 'script>',
'(function () {',
" var DESIGN_W = 620;",
" var card = document.querySelector('.ps-card');",
' if (!card) return;',
' function fit() {',
' var w = document.documentElement.clientWidth;',
' var s = Math.min(1, w / DESIGN_W);',
" document.body.style.overflow = 'hidden';",
" card.style.width = DESIGN_W + 'px';",
" card.style.minHeight = (DESIGN_W / 0.75) + 'px';",
" card.style.marginLeft = ((w - DESIGN_W) / 2) + 'px';",
" card.style.transform = 'scale(' + s + ')';",
" card.style.transformOrigin = 'top center';",
" document.body.style.height = (DESIGN_W / 0.75 * s) + 'px';",
' }',
' fit();',
" window.addEventListener('resize', fit);",
'})();',
'</scr' + 'ipt>',
].join('\n');
const m = msgs.find((x) => x.role === 'assistant' && String(x.content).includes('</show_html>'));
if (!m) throw new Error('plane: 未找到 show_html 卡片');
m.content = m.content.replace('</show_html>', '\n' + FIT + '\n</show_html>');
return msgs;
}
// ── cat 手术:只剪超时等待对,续接思考 ──────────────────────
function catSurgery(msgs) {
const cut = new Set();
msgs.forEach((m, i) => {
if (m.role === 'tool' && m.name === 'sleep' && String(m.content).includes('sleep 失败')) {
const a = msgs[i - 1];
if (!a || a.role !== 'assistant' || (a.tool_calls || []).length !== 1 || a.tool_calls[0].function.name !== 'sleep') {
throw new Error('cat: 超时配对异常 @' + i);
}
cut.add(i); cut.add(i - 1);
}
});
if (cut.size !== 6) throw new Error('cat: 应剪 3 对6 条),实际 ' + cut.size);
const kept = msgs.filter((_, i) => !cut.has(i));
// 续接 1状态检查的思考原"超时了"
const s1 = kept.find((m) => m.role === 'assistant' && String(m.content).startsWith('5 分钟没有输出'));
s1.reasoning_content = replaceOnce(s1.reasoning_content, 'UI Operator_1 超时了5分钟无输出。', 'UI Operator_1 有一阵没有新输出了。', 'cat续接1');
// 续接 2第二次状态检查的思考无正文
const s2 = kept.find((m) => m.role === 'assistant' && String(m.reasoning_content).startsWith('又超时了'));
s2.reasoning_content = replaceOnce(s2.reasoning_content, '又超时了。再查一下状态', '还是没有新输出。再查一下状态', 'cat续接2');
// 续接 3直接检查工作区
const s3 = kept.find((m) => m.role === 'assistant' && String(m.content).startsWith('连续超时'));
s3.reasoning_content = replaceOnce(s3.reasoning_content, '连续两次超时了。', '等了挺久还没有新动静。', 'cat续接3思考');
s3.content = replaceOnce(s3.content, '连续超时,我直接检查一下工作区', '等了一阵还没新动静,我直接检查一下工作区', 'cat续接3正文');
const rest = JSON.stringify(kept);
for (const bad of ['sleep 失败', '超时']) {
if (rest.includes(bad)) console.warn('⚠️ cat 残留关键词:', bad);
}
return kept;
}
// ── 媒体导出 ──────────────────────────────────────────────
function exportMedia(keptMsgs, san) {
fs.mkdirSync(MEDIA_OUT, { recursive: true });
const seen = new Map();
for (const m of keptMsgs) {
for (const ref of (m.metadata && m.metadata.media_refs) || []) {
if (ref && ref.sha256 && !seen.has(ref.sha256)) seen.set(ref.sha256, ref);
}
}
let n = 0;
for (const [sha, ref] of seen) {
const ext = ref.mime_type === 'image/png' ? '.png' : ref.mime_type === 'image/jpeg' ? '.jpg' : '';
if (!ext) { console.warn('⚠️ 未知媒体类型', ref.mime_type); continue; }
const src = path.join(MEDIA_STORE, 'blobs', sha.slice(0, 2), sha);
const dst = path.join(MEDIA_OUT, sha + ext);
if (fs.existsSync(src)) fs.copyFileSync(src, dst);
else if (fs.existsSync(src + ext)) fs.copyFileSync(src + ext, dst);
else { console.warn('⚠️ 媒体 blob 缺失:', sha.slice(0, 12)); continue; }
n++;
}
console.log('媒体导出:', n, '个');
}
// ── activity mock猫咪的 4 个子智能体)────────────────────
function truncateArgs(v) {
if (typeof v === 'string') return v.length > 300 ? v.slice(0, 300) + '…' : v;
if (Array.isArray(v)) return v.map(truncateArgs);
if (v && typeof v === 'object') {
const o = {};
for (const [k, val] of Object.entries(v)) o[k] = truncateArgs(val);
return o;
}
return v;
}
function buildActivityMocks(san) {
const taskIds = ['sub_1_1788364427_985302', 'sub_2_1788364429_91688a', 'sub_3_1788364431_74db3e', 'sub_4_1788364765_5d6208'];
// 多智能体实例快照activity 状态与实例列表共用的真值来源
const MA_SNAP = (JSON.parse(fs.readFileSync('/Users/jojo/.astrion/astrion/host/data/sub_agents.json', 'utf8')).multi_agent_states || {})['conv_20260902_235153_495'];
const byTask = {};
for (const inst of Object.values((MA_SNAP && MA_SNAP.agents) || {})) byTask[inst.task_id] = inst;
const list = [];
for (const tid of taskIds) {
const dir = path.join(SUB_TASKS_DIR, tid);
const entries = fs.readFileSync(path.join(dir, 'progress.jsonl'), 'utf8')
.split('\n').filter(Boolean)
.map((l) => JSON.parse(l))
.filter((e) => e.type === 'progress')
.map((e) => deepSanitize({ ...e, args: truncateArgs(e.args || {}) }, san));
// status 必须还原实例真实状态idle/terminated——写死 completed 会让前端在查看
// 详情时把多智能体实例错误显示为「已完成」(单智能体语义),轮询后才跳回。
const instStatus = (byTask[tid] && byTask[tid].status) || 'completed';
fs.writeFileSync(path.join(MOCK, `activity-${tid}.json`), JSON.stringify({ success: true, data: { status: instStatus, entries } }));
const out = JSON.parse(fs.readFileSync(path.join(dir, 'output.json'), 'utf8'));
const lastTool = entries.length ? entries[entries.length - 1].tool : null;
const ts = tid.split('_');
list.push({
task_id: tid, status: 'completed',
summary: san((out.summary || '').slice(0, 60)),
last_tool: lastTool, conversation_id: 'conv_20260902_235153_495',
created_at: Number(ts[2]) / 1000,
});
}
fs.writeFileSync(path.join(MOCK, 'sub-agents-conv_20260902_235153_495.json'), JSON.stringify({ success: true, data: list }, null, 1));
// 多智能体实例列表(/api/multiagent/active_sub_agents猫咪对话 QuickDock 子智能体窗口的数据源。
// 字段对齐 MultiAgentInstance.to_dict() + 后端接口补充的 last_tool/current_context_tokens。
const maAgents = taskIds.map((tid) => {
const inst = byTask[tid] || {};
const base = list.find((x) => x.task_id === tid) || {};
return {
agent_id: inst.agent_id ?? null,
role_id: inst.role_id || '',
display_name: inst.display_name || tid,
task_id: tid,
status: inst.status || 'idle',
summary: san(String(inst.summary || base.summary || '').slice(0, 60)),
created_at: inst.created_at || base.created_at,
last_output: san(String(inst.last_output || '').slice(0, 300)),
conversation_id: 'conv_20260902_235153_495',
last_tool: base.last_tool || null,
current_context_tokens: 0,
};
});
fs.writeFileSync(path.join(MOCK, 'ma-agents-conv_20260902_235153_495.json'), JSON.stringify({ success: true, agents: maAgents }, null, 1));
console.log('activity mock: 4 个子智能体已导出');
}
// ── 主流程 ──────────────────────────────────────────────
const RUNNING = { has_running_background_commands: false, has_running_multi_agent: false, has_running_sub_agents: false, is_main_running: false, is_truly_active: false, main_task_id: null, main_task_type: null };
const listEntries = { normal: [], ma: [] };
for (const [name, cfg] of Object.entries(CONVS)) {
const raw = JSON.parse(fs.readFileSync(cfg.src, 'utf8'));
const san = makeSanitizer(cfg.oldBase, cfg.newBase);
const demoModel = cfg.model || DEMO_MODEL;
let msgs = raw.messages.map((m) => transformMessage(m, san, demoModel));
const before = msgs.length;
if (name === 'word') msgs = wordSurgery(msgs);
if (name === 'plane') msgs = planeSurgery(msgs);
if (name === 'cat') msgs = catSurgery(msgs);
const title = cfg.title || raw.title;
const meta = {
title: san(title),
model_key: demoModel,
thinking_mode: true,
run_mode: 'thinking',
multi_agent_mode: !!cfg.ma,
permission_mode: 'unrestricted',
execution_mode: 'sandbox',
network_permission: '',
messages_count: msgs.length,
};
// 真实 edited_files 随 bootstrap 还原QuickDock 文件记录窗口);真实 todo_list 导出独立
// mock 文件(/api/todo-list 按 conversation_id 读取QuickDock 待办窗口)。此前均钉空/钉 null。
const rawEdited = (raw.metadata && Array.isArray(raw.metadata.edited_files)) ? raw.metadata.edited_files : [];
const editedFiles = rawEdited
.filter((it) => it && typeof it === 'object' && it.path)
.map((it) => ({ path: san(String(it.path)), op: it.op || 'edit', ts: it.ts || '' }));
const boot = { success: true, data: { conversation_id: cfg.id, edited_files: editedFiles, messages: msgs, meta, running: RUNNING } };
fs.writeFileSync(path.join(MOCK, `bootstrap-${cfg.id}.json`), JSON.stringify(boot));
if (raw.todo_list && Array.isArray(raw.todo_list.tasks) && raw.todo_list.tasks.length) {
fs.writeFileSync(path.join(MOCK, `todo-${cfg.id}.json`), JSON.stringify({ success: true, data: deepSanitize(raw.todo_list, san) }));
}
// 真实 token 统计导出独立 mock/tokens 与 /token-statistics 按 conversation_id 读取)。
// 字段名与对话文件一致total_input_tokens 等),前端 resource store 直接消费;
// 老对话无 total_cached_input_tokens/cache_exempt_input_tokens 字段,前端 `|| 0` 兜底。
if (raw.token_statistics && typeof raw.token_statistics === 'object') {
fs.writeFileSync(path.join(MOCK, `tokens-${cfg.id}.json`), JSON.stringify(raw.token_statistics, null, 1));
}
const tools = msgs.filter((m) => m.role === 'tool').length;
console.log(`${name}: ${before}${msgs.length} 条消息,工具消息 ${tools}`);
exportMedia(msgs, san);
listEntries[cfg.ma ? 'ma' : 'normal'].push({
id: cfg.id, title: meta.title, project_path: cfg.newBase, project_relative_path: null,
multi_agent_mode: !!cfg.ma, thinking_mode: true, status: 'active',
total_messages: msgs.length, total_tools: tools,
created_at: raw.created_at, updated_at: raw.updated_at,
});
}
// 保留原有的弹幕调研对话(不动),插入 normal 列表头部
const oldList = JSON.parse(fs.readFileSync(path.join(MOCK, 'conversations-normal.json'), 'utf8'));
const research = oldList.data.conversations.find((c) => c.id === 'conv_20260823_122836_778');
// 弹幕调研对话不在上方循环里,单独导出真实 token 统计(老格式:无 cached/exempt 字段)
const danmakuRaw = JSON.parse(fs.readFileSync('/Users/jojo/.astrion/astrion/host/data/conversations/me/conv_20260823_122836_778.json', 'utf8'));
if (danmakuRaw.token_statistics) {
fs.writeFileSync(path.join(MOCK, 'tokens-conv_20260823_122836_778.json'), JSON.stringify(danmakuRaw.token_statistics, null, 1));
}
const normal = [research, ...listEntries.normal.sort((a, b) => String(a.created_at).localeCompare(String(b.created_at)))];
fs.writeFileSync(path.join(MOCK, 'conversations-normal.json'),
JSON.stringify({ success: true, data: { conversations: normal, has_more: false, limit: 20, offset: 0, total: normal.length } }, null, 1));
fs.writeFileSync(path.join(MOCK, 'conversations-ma.json'),
JSON.stringify({ success: true, data: { conversations: listEntries.ma, has_more: false, limit: 20, offset: 0, total: listEntries.ma.length } }, null, 1));
console.log('列表更新normal', normal.length, '条 / ma', listEntries.ma.length, '条');
buildActivityMocks(makeSanitizer('/Users/jojo/Desktop/语音', '/Users/demo/dev'));
console.log('✅ 全部完成');

View File

@ -0,0 +1,89 @@
#!/usr/bin/env node
/* mock 一致性自检列表↔bootstrap↔资源↔媒体↔activity 全部交叉核对 */
const fs = require('fs');
const path = require('path');
const MOCK = path.join(__dirname, '..', 'exp', 'mock');
let errors = 0;
const bad = (msg) => { console.error('❌', msg); errors++; };
const ok = (msg) => console.log('✓', msg);
// 1. 列表 ↔ bootstrap
const normal = JSON.parse(fs.readFileSync(path.join(MOCK, 'conversations-normal.json'), 'utf8')).data.conversations;
const ma = JSON.parse(fs.readFileSync(path.join(MOCK, 'conversations-ma.json'), 'utf8')).data.conversations;
const all = [...normal, ...ma];
console.log(`列表: normal ${normal.length} 条, ma ${ma.length}`);
for (const c of all) {
const f = path.join(MOCK, `bootstrap-${c.id}.json`);
if (!fs.existsSync(f)) { bad(`缺 bootstrap: ${c.id}`); continue; }
const b = JSON.parse(fs.readFileSync(f, 'utf8'));
if (b.data.messages.length !== c.total_messages) bad(`${c.id} 列表消息数 ${c.total_messages} != bootstrap ${b.data.messages.length}`);
if (b.data.meta.title !== c.title) bad(`${c.id} 标题不一致: ${c.title} vs ${b.data.meta.title}`);
if (b.data.meta.multi_agent_mode !== c.multi_agent_mode) bad(`${c.id} MA 标记不一致`);
ok(`${c.title} (${c.id})`);
}
// 2. bootstrap 内部引用核对
const filesDir = fs.readdirSync(path.join(MOCK, 'files'));
const fileMocks = fs.readdirSync(MOCK).filter((f) => f.startsWith('file-'));
const mediaDir = fs.readdirSync(path.join(MOCK, 'media'));
let mediaRefs = 0, showFiles = 0, downloads = 0;
for (const c of all) {
const f = path.join(MOCK, `bootstrap-${c.id}.json`);
if (!fs.existsSync(f)) continue;
const b = JSON.parse(fs.readFileSync(f, 'utf8'));
for (const m of b.data.messages) {
const text = typeof m.content === 'string' ? m.content : '';
// show_file 卡片
for (const mm of text.matchAll(/<show_file\s+path="([^"]+)"/g)) {
showFiles++;
const name = mm[1].split('/').pop();
if (!filesDir.includes(name) && !fileMocks.includes('file-' + name)) bad(`${c.id} show_file 缺资源: ${name}`);
}
// download:// 链接
for (const mm of text.matchAll(/download:\/\/([^)\s"]+)/g)) {
downloads++;
const name = mm[1].split('/').pop();
if (!filesDir.includes(name) && !fileMocks.includes('file-' + name)) bad(`${c.id} download 缺资源: ${name}`);
}
// user_upload 引用(上传附件)
for (const mm of text.matchAll(/\.astrion\/user_upload\/([^\s"`)]+)/g)) {
const name = mm[1];
if (!fileMocks.includes('file-' + name)) bad(`${c.id} user_upload 缺资源: ${name}`);
}
// media_refs
for (const ref of (m.metadata && m.metadata.media_refs) || []) {
mediaRefs++;
if (!mediaDir.includes(ref.sha256 + '.png')) bad(`${c.id} 缺媒体: ${ref.sha256.slice(0, 12)}`);
}
// tool_calls 与 tool 结果配对
}
// tool_call 配对校验
const callIds = new Set();
for (const m of b.data.messages) for (const t of m.tool_calls || []) callIds.add(t.id);
for (const m of b.data.messages) {
if (m.role === 'tool' && m.tool_call_id && !callIds.has(m.tool_call_id)) bad(`${c.id} 孤儿 tool 消息: ${m.name} (${m.tool_call_id})`);
}
}
ok(`资源引用: show_file ${showFiles}, download ${downloads}, media_refs ${mediaRefs}`);
// 3. 隐私扫描:全 mock 不得出现 /Users/jojo 或裸 jojo
const walk = (d) => fs.readdirSync(d, { withFileTypes: true }).flatMap((e) => e.isDirectory() ? walk(path.join(d, e.name)) : [path.join(d, e.name)]);
for (const f of walk(MOCK)) {
if (!/\.(json|txt|md)$/.test(f)) continue;
const s = fs.readFileSync(f, 'utf8');
if (s.includes('/Users/jojo')) bad(`${path.basename(f)} 残留 /Users/jojo`);
if (/\bjojo\b/.test(s)) bad(`${path.basename(f)} 残留 jojo`);
}
ok('隐私扫描完成');
// 4. activity mock
const subList = JSON.parse(fs.readFileSync(path.join(MOCK, 'sub-agents-conv_20260902_235153_495.json'), 'utf8')).data;
for (const t of subList) {
const f = path.join(MOCK, `activity-${t.task_id}.json`);
if (!fs.existsSync(f)) { bad(`缺 activity: ${t.task_id}`); continue; }
const a = JSON.parse(fs.readFileSync(f, 'utf8'));
ok(`activity ${t.task_id}: ${a.data.entries.length} 条事件`);
}
console.log(errors ? `\n${errors} 个问题` : '\n✅ 全部通过');
process.exit(errors ? 1 : 0);

View File

@ -0,0 +1,52 @@
#!/usr/bin/env node
/**
* 重建猫咪对话的 cat-cafe/index.html
* 子智能体 write_file 全文 + 主对话两次 edit_file太阳修复按钮修复
* 输出到 cache/website-mock-review/assets/cat-cafe/index.html
*/
const fs = require('fs');
const path = require('path');
const SUB = '/Users/jojo/.astrion/astrion/host/data/sub_agent_tasks/sub_4_1788364765_5d6208/conversation.json';
const MAIN = '/Users/jojo/.astrion/astrion/host/mutiagents/conversations/workspace/conv_20260902_235153_495.json';
const OUT = '/Users/jojo/Desktop/agents/正在修复中/agents/cache/website-mock-review/assets/cat-cafe/index.html';
// 1. 取子智能体的 write_file 全文
const sub = JSON.parse(fs.readFileSync(SUB, 'utf8'));
const msgs = sub.messages || sub;
let html = null;
for (const m of msgs) {
for (const tc of m.tool_calls || []) {
if (tc.function.name === 'write_file') {
const args = JSON.parse(tc.function.arguments);
if (args.file_path && args.file_path.includes('cat-cafe')) html = args.content;
}
}
}
if (!html) throw new Error('未找到 cat-cafe 的 write_file');
console.log('write_file 全文:', html.length, '字符');
// 2. 按主对话顺序应用 edit_file
const main = JSON.parse(fs.readFileSync(MAIN, 'utf8'));
let edits = [];
for (const m of main.messages) {
for (const tc of m.tool_calls || []) {
if (tc.function.name === 'edit_file') {
const args = JSON.parse(tc.function.arguments);
if ((args.file_path || '').includes('cat-cafe')) edits.push(args);
}
}
}
console.log('待应用 edit_file:', edits.length, '个');
for (const [i, e] of edits.entries()) {
for (const [j, r] of (e.replacements || []).entries()) {
const cnt = html.split(r.old_string).length - 1;
console.log(` edit ${i + 1}.${j + 1}: 匹配 ${cnt}`);
if (cnt !== 1) throw new Error('匹配数异常,重建不保真');
html = html.split(r.old_string).join(r.new_string);
}
}
fs.mkdirSync(path.dirname(OUT), { recursive: true });
fs.writeFileSync(OUT, html);
console.log('已写出:', OUT, fs.statSync(OUT).size, '字节');

View File

@ -0,0 +1,33 @@
#!/usr/bin/env node
/* 极简静态文件服务器(官网预览用,替代沙箱内不可用的 python3 -m http.server */
const http = require('http');
const fs = require('fs');
const path = require('path');
const ROOT = path.join(__dirname, '..', 'exp');
const PORT = Number(process.argv[2]) || 8898;
const MIME = {
html: 'text/html; charset=utf-8', js: 'text/javascript', mjs: 'text/javascript',
css: 'text/css', json: 'application/json; charset=utf-8', png: 'image/png',
jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', svg: 'image/svg+xml',
webp: 'image/webp', woff: 'font/woff', woff2: 'font/woff2', ttf: 'font/ttf',
pdf: 'application/pdf', docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
txt: 'text/plain; charset=utf-8', md: 'text/plain; charset=utf-8', ico: 'image/x-icon',
mp4: 'video/mp4', webm: 'video/webm',
};
http.createServer((req, res) => {
try {
let p = decodeURIComponent(new URL(req.url, 'http://x').pathname);
if (p.endsWith('/')) p += 'index.html';
const file = path.normalize(path.join(ROOT, p));
if (!file.startsWith(ROOT)) { res.writeHead(403); return res.end(); }
fs.readFile(file, (err, data) => {
if (err) { res.writeHead(404); return res.end('not found'); }
const ext = path.extname(file).slice(1).toLowerCase();
res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' });
res.end(data);
});
} catch (e) { res.writeHead(500); res.end(String(e)); }
}).listen(PORT, '127.0.0.1', () => console.log(`preview: http://127.0.0.1:${PORT}/ (root: ${ROOT})`));