Compare commits
5 Commits
57043b0084
...
2f0b970060
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f0b970060 | |||
| bd2cf0f56d | |||
| 7fa2715ee3 | |||
| 2482b42771 | |||
| 18b285aca1 |
@ -97,6 +97,7 @@ __all__ = [
|
||||
"CODE_EXECUTION_TIMEOUT",
|
||||
"TERMINAL_COMMAND_TIMEOUT",
|
||||
"SEARCH_MAX_RESULTS",
|
||||
"MAX_MESSAGE_CHARS",
|
||||
"AUTO_FIX_TOOL_CALL",
|
||||
"AUTO_FIX_MAX_ATTEMPTS",
|
||||
"MAX_ITERATIONS_PER_TASK",
|
||||
|
||||
@ -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"))
|
||||
MAX_ACTIVE_USER_CONTAINERS = int(os.environ.get("MAX_ACTIVE_USER_CONTAINERS", "8"))
|
||||
# 每用户同时活跃的容器上限(防单用户多工作区占满全局容器池,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()
|
||||
# 沙箱可写路径的「部署通道」(逗号分隔)。路径授权只有两个来源:
|
||||
# config/host_sandbox_policy.json(前端「路径授权」UI)+ 本变量(真·环境变量),
|
||||
|
||||
@ -158,6 +158,9 @@ class UserContainerManager:
|
||||
handle.touch()
|
||||
return handle
|
||||
|
||||
# 配额检查仅针对 docker 句柄:host 句柄只是会话标识,不占用容器池资源,
|
||||
# 创建 host 句柄时直接跳过全局/每用户两道配额闸门
|
||||
if mode == "docker":
|
||||
if not self._has_capacity(key):
|
||||
raise RuntimeError(tr("container_mgr.quota_exhausted"))
|
||||
|
||||
@ -165,8 +168,9 @@ class UserContainerManager:
|
||||
per_user_limit = MAX_ACTIVE_CONTAINERS_PER_USER
|
||||
if per_user_limit > 0:
|
||||
owned = sum(
|
||||
1 for k in self._containers
|
||||
if k == username_norm or k.startswith(f"{username_norm}::")
|
||||
1 for k, h in self._containers.items()
|
||||
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))
|
||||
@ -193,7 +197,8 @@ class UserContainerManager:
|
||||
return True
|
||||
if self.max_containers <= 0:
|
||||
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]:
|
||||
key = self._normalize_username(container_key)
|
||||
@ -293,8 +298,11 @@ class UserContainerManager:
|
||||
def _has_capacity(self, username: str) -> bool:
|
||||
if self.max_containers <= 0:
|
||||
return True
|
||||
existing = 1 if username in self._containers else 0
|
||||
return (len(self._containers) - existing) < self.max_containers
|
||||
# 全局上限同样只统计 docker 句柄(host 句柄不占用容器池)
|
||||
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:
|
||||
if mode != "docker":
|
||||
|
||||
@ -419,7 +419,9 @@ def build_api_admin_dashboard_snapshot():
|
||||
max_containers = getattr(container_manager, "max_containers", None) or len(handle_map)
|
||||
available_slots = None
|
||||
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()
|
||||
uploads_summary = summarize_upload_events(upload_events, quarantine_total_bytes)
|
||||
|
||||
@ -312,7 +312,8 @@ def collect_container_snapshots(handle_map: Dict[str, Dict[str, Any]]) -> Dict[s
|
||||
"host": active_total - docker_count,
|
||||
"issues": failure_count,
|
||||
"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_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,
|
||||
|
||||
@ -56,9 +56,9 @@
|
||||
<script>
|
||||
/* 演示引导:注册 mock SW,等 controllerchange 后换路由、再加载真实前端 */
|
||||
(function () {
|
||||
// 固定演示呈现状态:经典配色 + 极简模式 + 快捷窗口自动展开,不受访客浏览器缓存影响
|
||||
// 固定演示呈现状态:夜间配色 + 极简模式 + 快捷窗口自动展开,不受访客浏览器缓存影响
|
||||
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_quick_dock_auto_expand', 'true');
|
||||
} catch (e) {}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
54
website-design/exp/mock/file-11.12周会速记-待整理_1.txt
Normal 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 + 双十一最终确认
|
||||
|
||||
哦对差点忘了:会议室投影仪又坏了,行政说报修了第三次了
|
||||
BIN
website-design/exp/mock/files/11.12周会会议纪要.docx
Normal file
BIN
website-design/exp/mock/files/11.12周会会议纪要.pdf
Normal file
1349
website-design/exp/mock/files/index.html
Normal 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`;首页 CTA(MAKE 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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 265 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 265 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 11 KiB |
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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}}
|
||||
@ -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"
|
||||
}
|
||||
10
website-design/exp/mock/tokens-conv_20260825_210916_394.json
Normal 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
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
10
website-design/exp/mock/tokens-conv_20260902_235153_495.json
Normal 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"
|
||||
}
|
||||
10
website-design/exp/mock/tokens-conv_20260903_003950_896.json
Normal 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"
|
||||
}
|
||||
BIN
website-design/exp/static/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
@ -8,16 +8,41 @@
|
||||
*/
|
||||
|
||||
const DEMO_PAGE = '/demo.html';
|
||||
const CACHE = 'astrion-demo-v4';
|
||||
const CACHE = 'astrion-demo-v19';
|
||||
|
||||
/* ── 静态响应体 ── */
|
||||
|
||||
const MODELS = {
|
||||
success: true,
|
||||
items: [
|
||||
{
|
||||
context_window: 256000, description: 'Kimi-K3', fast_only: false,
|
||||
max_output_tokens: 32768, model_key: 'Kimi-K3', multimodal: 'image,video',
|
||||
name: 'Kimi-K3', supports_reasoning_effort: false, supports_thinking: true,
|
||||
context_window: 1048576, description: 'Kimi-K3', fast_only: false,
|
||||
max_output_tokens: 64000, model_key: 'Kimi-K3', multimodal: 'image,video',
|
||||
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
|
||||
}
|
||||
]
|
||||
@ -28,18 +53,26 @@ const SESSION_STATUS = {
|
||||
session: { host_mode: true, logged_in: true, role: 'admin', run_mode: 'thinking', thinking_mode: true, username: 'host' }
|
||||
};
|
||||
|
||||
const WORKSPACES = {
|
||||
// 工作区列表为静态数据;「当前工作区」是会话态——select 接口会改它,
|
||||
// 列表与 select 响应都从它动态组装;否则 current_workspace_id 永远钉死,
|
||||
// 前端切换后 toast/分组高亮会拿错工作区(实测弹窗恒显示「日常使用」)。
|
||||
const WS_LIST = [
|
||||
{ workspace_id: 'ws-daily', label: '日常使用', path: '/Users/demo/daily', running_task_count: 0 },
|
||||
{ workspace_id: 'ws-starraid', label: 'StarRaid', path: '/Users/demo/starraid', running_task_count: 0 },
|
||||
{ workspace_id: 'ws-dev', label: '开发实战', path: '/Users/demo/dev', running_task_count: 0 }
|
||||
];
|
||||
// 初始与 demo 默认落点(弹幕调研对话所属工作区)一致
|
||||
let currentWorkspaceId = 'ws-starraid';
|
||||
function buildWorkspaces() {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
current_workspace_id: 'ws-daily',
|
||||
current_workspace_id: currentWorkspaceId,
|
||||
default_workspace_id: 'ws-daily',
|
||||
workspaces: [
|
||||
{ workspace_id: 'ws-daily', label: '日常使用', path: '/Users/demo/daily', is_current: true, 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 }
|
||||
]
|
||||
workspaces: WS_LIST.map((w) => ({ ...w, is_current: w.workspace_id === currentWorkspaceId }))
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const UI_BLOCKS = {
|
||||
block_compress_conversation: false, block_conversation_review: false,
|
||||
@ -52,24 +85,28 @@ const STATUS = {
|
||||
success: true,
|
||||
version: '2026-08-23',
|
||||
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' }
|
||||
};
|
||||
|
||||
const PERSONALIZATION = {
|
||||
success: true,
|
||||
data: {
|
||||
theme: 'classic',
|
||||
theme: 'dark',
|
||||
tool_intent_enabled: true,
|
||||
group_sidebar_by_workspace: true,
|
||||
block_display_mode: 'minimal',
|
||||
compact_message_display: 'brief',
|
||||
communication_style: 'auto',
|
||||
conversation_continuity: 'medium',
|
||||
agents_md_auto_inject: true,
|
||||
auto_deep_compress_enabled: true,
|
||||
deep_compress_trigger_tokens: 200000,
|
||||
auto_generate_title: true,
|
||||
auto_shallow_compress_enabled: false
|
||||
},
|
||||
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_max_replace_per_round: 10, shallow_trigger_tokens: 1000,
|
||||
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 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 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 直接 return,quotas count 恒为 0、
|
||||
// window_start/reset_at 为 null(get_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_LIST = { success: true, data: [] };
|
||||
const GIT_SUMMARY = { success: true, data: { available: false } };
|
||||
@ -128,11 +172,34 @@ self.addEventListener('fetch', (e) => {
|
||||
|
||||
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_complete,status 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 method = req ? req.method : 'GET';
|
||||
|
||||
// 子智能体活动详情
|
||||
const actMatch = p.match(/^\/api\/sub_agents\/(sub_[\w-]+)\/activity$/);
|
||||
@ -150,21 +217,72 @@ async function routeApi(url) {
|
||||
return json({ success: true, data: [] });
|
||||
}
|
||||
|
||||
// 多智能体活跃实例(演示无)
|
||||
if (p === '/api/multiagent/active_sub_agents') return json({ success: true, agents: [] });
|
||||
// 多智能体活跃实例:猫咪对话(conv_20260902_235153_495)有 4 个实例,其余对话为空。
|
||||
// 这是 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/file/content') {
|
||||
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}`);
|
||||
if (r.ok) return new Response(await r.text(), { headers: { 'Content-Type': 'text/plain; charset=utf-8' } });
|
||||
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
|
||||
const bootMatch = p.match(/^\/api\/conversations\/(conv_[\w-]+)\/bootstrap$/);
|
||||
if (bootMatch) {
|
||||
@ -173,18 +291,41 @@ async function routeApi(url) {
|
||||
return json({ success: false, error: '对话不存在' }, 404);
|
||||
}
|
||||
|
||||
// 对话列表
|
||||
// 版本控制状态:必须显式 mock!前端 fetchVersioningStatus 会用 payload.host_mode
|
||||
// 覆盖全局 versioningHostMode;若落到兜底 EMPTY_OK(data:{}),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') {
|
||||
const ma = url.searchParams.get('multi_agent_mode') === '1';
|
||||
const r = await fetch(`/mock/conversations-${ma ? 'ma' : 'normal'}.json`);
|
||||
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/csrf-token') return json({ success: true, csrf_token: 'demo' });
|
||||
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/personalization') return json(PERSONALIZATION);
|
||||
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/health') return json({ status: 'ok' });
|
||||
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/tool-settings') return json({ success: true, categories: [] });
|
||||
if (p === '/api/effective-policy') return json({ success: true, data: { categories: {} } });
|
||||
|
||||
411
website-design/tools/build-mocks.cjs
Normal 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 成功渲染为 PDF(233KB,3页)——这是最强的验证: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('✅ 全部完成');
|
||||
89
website-design/tools/check-mocks.cjs
Normal 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);
|
||||
52
website-design/tools/rebuild-cat-html.cjs
Normal 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, '字节');
|
||||
33
website-design/tools/serve.cjs
Normal 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})`));
|
||||