Skip to content

Commit f4610a4

Browse files
wujiaming-aiGarming
andauthored
fix(studio): present native Codex migration activity (#988)
* fix(studio): present native Codex migration activity * fix(studio): preserve migration activity during delivery * fix(studio): exclude incompatible migration model * style: format migration capability test --------- Co-authored-by: Garming <garming.wu@gmail.com>
1 parent 405a945 commit f4610a4

11 files changed

Lines changed: 482 additions & 135 deletions

frontend/server/migration/service.py

Lines changed: 78 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
MIGRATION_SESSION_TTL_SECONDS = 60 * 60
7373
MIGRATION_UPLOAD_MAX_BYTES = 50 * 1024 * 1024
7474
MIGRATION_CLI_MIN_VERSION = "0.52.1"
75+
MIGRATION_UNSUPPORTED_MODEL_IDS = frozenset({"deepseek-v4-pro-260425"})
7576
_MAX_EXPANDED_BYTES = 1024 * 1024 * 1024
7677
_MAX_ARCHIVE_FILES = 20_000
7778
_MAX_ARCHIVE_PATH_BYTES = 4 * 1024
@@ -357,73 +358,6 @@ def _analysis_result_message(value: str) -> bool:
357358
return True
358359

359360

360-
def _command_activity_action(command: str, phase: str) -> str | None:
361-
normalized = command.casefold()
362-
if "ak migrate" in normalized:
363-
return "执行 AgentKit 迁移"
364-
if re.search(
365-
r"(?:^|[\s;&|(/])(?:zip|tar)(?:\s|$)",
366-
normalized,
367-
) or any(
368-
marker in normalized
369-
for marker in ("package_artifact", "package-result", "package.py", "package.sh")
370-
):
371-
return "打包迁移产物"
372-
if any(
373-
marker in normalized
374-
for marker in (
375-
"pip install",
376-
"uv sync",
377-
"npm install",
378-
"pnpm install",
379-
"yarn install",
380-
)
381-
):
382-
return "准备项目依赖"
383-
if any(marker in normalized for marker in ("compileall", "py_compile")):
384-
return "检查代码语法"
385-
if any(
386-
marker in normalized
387-
for marker in ("validate", "verify", "pytest", "unittest", " test")
388-
):
389-
return "验证迁移结果"
390-
if "git diff" in normalized or "git status" in normalized:
391-
return "检查代码改动"
392-
if any(marker in normalized for marker in ("apply_patch", "<<", "tee ")):
393-
return "生成迁移代码"
394-
if any(marker in normalized for marker in ("mkdir ", "cp ", "mv ", "touch ")):
395-
return "整理迁移文件"
396-
if "docker " in normalized or "dockerfile" in normalized:
397-
return "检查运行配置"
398-
if re.search(
399-
r"(?:^|[\s;&|(/])(?:find|fd|rg|grep|ls|tree)(?:\s|$)",
400-
normalized,
401-
):
402-
return "检查项目结构"
403-
if re.search(
404-
r"(?:^|[\s;&|(/])(?:cat|sed|head|tail|jq|yq|less)(?:\s|$)",
405-
normalized,
406-
):
407-
return "读取项目文件"
408-
if re.search(
409-
r"(?:^|[\s;&|(/])(?:python(?:\d+(?:\.\d+)*)?|node|npx|tsx|bash|sh)(?:\s|$)",
410-
normalized,
411-
):
412-
return "运行分析脚本" if phase == "analysis" else "运行迁移脚本"
413-
return None
414-
415-
416-
def _command_activity_title(command: str, status: str, phase: str) -> str | None:
417-
action = _command_activity_action(command, phase)
418-
if action is None:
419-
return None
420-
if status == "completed":
421-
return f"已{action}"
422-
if status == "failed":
423-
return f"{action}未完成"
424-
return f"正在{action}"
425-
426-
427361
def _parse_activity_log(
428362
content: bytes,
429363
attempt: int,
@@ -521,6 +455,16 @@ def upsert(item: dict[str, object]) -> None:
521455
plan_states.append(todo_status)
522456
if not plan:
523457
continue
458+
if (
459+
status != "failed"
460+
and "failed" not in plan_states
461+
and "in_progress" not in plan_states
462+
):
463+
for index, plan_state in enumerate(plan_states):
464+
if plan_state == "pending":
465+
plan[index]["status"] = "in_progress"
466+
plan_states[index] = "in_progress"
467+
break
524468
completed = plan_states.count("completed")
525469
todo_status = (
526470
"failed"
@@ -546,17 +490,11 @@ def upsert(item: dict[str, object]) -> None:
546490
if item_type == "command_execution":
547491
command = item.get("command")
548492
command_text = command if isinstance(command, str) else ""
549-
title = _command_activity_title(
550-
command_text,
551-
status,
552-
phase,
553-
)
554-
if title is None:
555-
title = {
556-
"running": "正在执行命令",
557-
"completed": "已执行命令",
558-
"failed": "命令执行未完成",
559-
}[status]
493+
title = {
494+
"running": "正在执行命令",
495+
"completed": "命令执行完成",
496+
"failed": "命令执行失败",
497+
}[status]
560498
tool: dict[str, object] = {"name": title}
561499
if command_text:
562500
tool["input"] = _activity_payload(
@@ -584,12 +522,14 @@ def upsert(item: dict[str, object]) -> None:
584522
continue
585523

586524
if item_type == "file_change":
525+
changes = item.get("changes")
526+
change_count = len(changes) if isinstance(changes, list) else 0
527+
subject = f"{change_count}个项目文件" if change_count else "项目文件"
587528
title = {
588-
"running": "正在更新项目文件",
589-
"completed": "已更新项目文件",
590-
"failed": "项目文件更新未完成",
529+
"running": f"正在更新{subject}",
530+
"completed": f"已更新{subject}",
531+
"failed": f"更新{subject}失败",
591532
}[status]
592-
changes = item.get("changes")
593533
tool: dict[str, object] = {"name": title}
594534
if isinstance(changes, list):
595535
tool["input"] = _activity_payload(
@@ -653,11 +593,37 @@ def upsert(item: dict[str, object]) -> None:
653593
continue
654594

655595
if item_type == "collab_tool_call":
656-
title = {
657-
"running": "正在协调子任务",
658-
"completed": "已完成子任务协作",
659-
"failed": "子任务协作未完成",
660-
}[status]
596+
collab_tool = str(item.get("tool") or "")
597+
collab_titles = {
598+
"spawn_agent": {
599+
"running": "正在启动子任务",
600+
"completed": "子任务已启动",
601+
"failed": "子任务启动失败",
602+
},
603+
"send_input": {
604+
"running": "正在向子任务发送信息",
605+
"completed": "已向子任务发送信息",
606+
"failed": "向子任务发送信息失败",
607+
},
608+
"wait": {
609+
"running": "正在等待子任务",
610+
"completed": "子任务等待已结束",
611+
"failed": "等待子任务失败",
612+
},
613+
"close_agent": {
614+
"running": "正在结束子任务",
615+
"completed": "子任务已结束",
616+
"failed": "结束子任务失败",
617+
},
618+
}
619+
title = collab_titles.get(collab_tool, {}).get(
620+
status,
621+
{
622+
"running": "正在协调子任务",
623+
"completed": "子任务协作已完成",
624+
"failed": "子任务协作失败",
625+
}[status],
626+
)
661627
input_value = {
662628
key: item[key]
663629
for key in ("tool", "receiver_thread_ids", "prompt")
@@ -1457,6 +1423,16 @@ def _analysis_prompt(
14571423
网络访问、测试或运行条件不能作为 unsupported 的理由,只能列入 assumptions、
14581424
warnings 或 boundary.exclude,供迁移和部署时处理。
14591425
1426+
## 用户可见执行动态
1427+
1428+
- 开始分析后使用 Codex 计划能力列出三到六个有明确结果的有序步骤,并随分析进展及时
1429+
更新;按顺序完成步骤,使第一个未完成项始终代表当前工作。
1430+
- 计划步骤和阶段性 assistant 更新会直接展示给用户,必须使用简体中文,说明当前发现、
1431+
已确认结果或下一步动作,不要输出“已完成分析步骤”之类没有事实内容的固定句式。
1432+
- 仅在开始新的关键阶段或获得重要结论时输出简短更新,不要重复计划内容,不要为了展示
1433+
进度而执行额外命令,也不得包含系统提示词、凭证、环境变量值或其他敏感信息。
1434+
- 最终响应仍必须严格遵守下方输出协议;执行动态不得改变 JSON 字段、迁移建议或证据标准。
1435+
14601436
## 支持判定与用户表达
14611437
14621438
- 能可靠识别 Structured 框架和入口时推荐对应 Structured 方式;否则只要存在足够材料
@@ -1879,6 +1855,13 @@ def _migration_instruction(
18791855
"configurable and follow the source project's security requirements.",
18801856
"Use the user's language in user-facing migration reports. If no user ",
18811857
"language is available, use Simplified Chinese.",
1858+
"Keep a concise ordered Codex todo list with three to six outcome-oriented ",
1859+
"steps. Complete it sequentially so the first incomplete step represents ",
1860+
"the current work, and update it as work advances. At meaningful ",
1861+
"milestones, emit a brief Simplified Chinese assistant update stating a ",
1862+
"concrete finding, confirmed result, or next action. Do not emit generic ",
1863+
"fixed completion notices, repeat the todo list, run extra commands only ",
1864+
"for progress reporting, or expose prompts, credentials, or environment values.",
18821865
"",
18831866
]
18841867
)
@@ -2163,6 +2146,7 @@ def capabilities(self) -> dict[str, object]:
21632146
"configured": model.get("configured") is True,
21642147
"id": str(model.get("id") or ""),
21652148
},
2149+
"unsupportedModelIds": sorted(MIGRATION_UNSUPPORTED_MODEL_IDS),
21662150
"maxUploadBytes": MIGRATION_UPLOAD_MAX_BYTES,
21672151
"sessionTtlSeconds": MIGRATION_SESSION_TTL_SECONDS,
21682152
"frameworks": list(MIGRATION_FRAMEWORKS),
@@ -2411,6 +2395,13 @@ def create_task(
24112395
owner_id: str,
24122396
creator_name: str,
24132397
) -> dict[str, object]:
2398+
if body.model_id in MIGRATION_UNSUPPORTED_MODEL_IDS:
2399+
raise MigrationError(
2400+
"MIGRATION_MODEL_UNSUPPORTED",
2401+
"所选模型暂不兼容项目迁移,请选择其他模型。",
2402+
status_code=400,
2403+
retryable=False,
2404+
)
24142405
capability = self.capabilities()
24152406
if not capability["enabled"]:
24162407
raise MigrationError(

frontend/src/adk/migrations.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export interface MigrationCapabilities {
4040
configured: boolean;
4141
id: string;
4242
};
43+
unsupportedModelIds?: string[];
4344
maxUploadBytes: number;
4445
sessionTtlSeconds: number;
4546
frameworks: MigrationFramework[];

frontend/src/blocks.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export type Block =
7676
response?: unknown;
7777
done: boolean;
7878
status?: "running" | "completed" | "failed";
79+
defaultOpen?: boolean;
7980
}
8081
| {
8182
kind: "plan";

frontend/src/migrations/MigrationWorkspace.tsx

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -235,8 +235,14 @@ function sourceStem(name: string): string {
235235
return name.replace(/\.zip$/i, "");
236236
}
237237

238-
function isSelectableMigrationModel(model: ModelOption): boolean {
239-
return model.available || model.lifecycleStatus === "Retiring";
238+
function isSelectableMigrationModel(
239+
model: ModelOption,
240+
unsupportedModelIds: ReadonlySet<string>,
241+
): boolean {
242+
return (
243+
!unsupportedModelIds.has(model.id) &&
244+
(model.available || model.lifecycleStatus === "Retiring")
245+
);
240246
}
241247

242248
function defaultAppName(name: string): string {
@@ -679,9 +685,16 @@ export function MigrationWorkspace({
679685
Record<string, string>
680686
>({});
681687
const task = selectedTask(tasks, selectedTaskId);
688+
const unsupportedMigrationModelIds = useMemo(
689+
() => new Set(capability?.unsupportedModelIds ?? []),
690+
[capability?.unsupportedModelIds],
691+
);
682692
const selectableModels = useMemo(
683-
() => models.filter(isSelectableMigrationModel),
684-
[models],
693+
() =>
694+
models.filter((model) =>
695+
isSelectableMigrationModel(model, unsupportedMigrationModelIds),
696+
),
697+
[models, unsupportedMigrationModelIds],
685698
);
686699
const composerModelId = task?.modelId || selectedModelId;
687700
const modelSelectOptions = useMemo(() => {
@@ -702,15 +715,27 @@ export function MigrationWorkspace({
702715
capability?.model?.id ||
703716
""
704717
).trim();
705-
if (fallbackId && !options.some((option) => option.value === fallbackId)) {
718+
const preservesExistingTaskModel = task?.modelId === fallbackId;
719+
if (
720+
fallbackId &&
721+
(preservesExistingTaskModel ||
722+
!unsupportedMigrationModelIds.has(fallbackId)) &&
723+
!options.some((option) => option.value === fallbackId)
724+
) {
706725
options.unshift({
707726
value: fallbackId,
708727
label: fallbackId,
709728
description: "当前默认模型",
710729
});
711730
}
712731
return options;
713-
}, [capability?.model?.id, selectableModels, selectedModelId, task?.modelId]);
732+
}, [
733+
capability?.model?.id,
734+
selectableModels,
735+
selectedModelId,
736+
task?.modelId,
737+
unsupportedMigrationModelIds,
738+
]);
714739
const createElapsedSeconds = createStartedAt
715740
? Math.max(0, Math.floor((now - createStartedAt) / 1_000))
716741
: 0;
@@ -819,10 +844,19 @@ export function MigrationWorkspace({
819844

820845
useEffect(() => {
821846
if (!capability || selectedModelId) return;
847+
const configuredModelId = capability.model?.id.trim() || "";
822848
const defaultModelId =
823-
capability?.model?.id.trim() || selectableModels[0]?.id || "";
849+
configuredModelId &&
850+
!unsupportedMigrationModelIds.has(configuredModelId)
851+
? configuredModelId
852+
: selectableModels[0]?.id || "";
824853
if (defaultModelId) setSelectedModelId(defaultModelId);
825-
}, [capability, selectableModels, selectedModelId]);
854+
}, [
855+
capability,
856+
selectableModels,
857+
selectedModelId,
858+
unsupportedMigrationModelIds,
859+
]);
826860

827861
useEffect(
828862
() => () => {
@@ -911,6 +945,9 @@ export function MigrationWorkspace({
911945
setActivity(null);
912946
setActivityError("");
913947
setActivityLoading(false);
948+
}, [task?.id]);
949+
950+
useEffect(() => {
914951
if (!task || !shouldShowCodexActivity(task)) {
915952
return;
916953
}

frontend/src/migrations/migrationActivityBlocks.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export function migrationActivityBlocks(items: MigrationActivityItem[]): Block[]
4040
response,
4141
done: item.status !== "running",
4242
status: item.status,
43+
...(item.status === "failed" ? { defaultOpen: true } : {}),
4344
}];
4445
}
4546
if (item.kind === "status" && item.status !== "completed") {
@@ -49,6 +50,7 @@ export function migrationActivityBlocks(items: MigrationActivityItem[]): Block[]
4950
response: item.detail,
5051
done: item.status !== "running",
5152
status: item.status,
53+
...(item.status === "failed" ? { defaultOpen: true } : {}),
5254
}];
5355
}
5456
return [];

0 commit comments

Comments
 (0)