feat(client): project plan and task agents
This commit is contained in:
@@ -11,7 +11,9 @@ import type {
|
|||||||
DigitalEmployeeDecision,
|
DigitalEmployeeDecision,
|
||||||
DigitalEmployeeDuty,
|
DigitalEmployeeDuty,
|
||||||
DigitalEmployeeManagedService,
|
DigitalEmployeeManagedService,
|
||||||
|
DigitalEmployeePlanAgentState,
|
||||||
DigitalEmployeeRunDetail,
|
DigitalEmployeeRunDetail,
|
||||||
|
DigitalEmployeeTaskAgentState,
|
||||||
DigitalEmployeeTaskExecutionSummary,
|
DigitalEmployeeTaskExecutionSummary,
|
||||||
DigitalEmployeeWorkEvent,
|
DigitalEmployeeWorkEvent,
|
||||||
DigitalEmployeeWorkdayActionRequest,
|
DigitalEmployeeWorkdayActionRequest,
|
||||||
@@ -829,6 +831,63 @@ function planRecordStatus(plan: QimingclawPlanRecord, tasks: QimingclawTaskRecor
|
|||||||
return plan.status || 'pending';
|
return plan.status || 'pending';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildPlanAgents(snapshot: QimingclawSnapshot | null): DigitalEmployeePlanAgentState[] {
|
||||||
|
return runtimePlans(snapshot).map((plan) => {
|
||||||
|
const tasks = runtimePlanTasks(snapshot, plan.id);
|
||||||
|
const status = planRecordStatus(plan, tasks);
|
||||||
|
const tone = planStatusTone(status);
|
||||||
|
const failedTaskCount = tasks.filter((task) => taskStatusTone(task.status) === 'danger').length;
|
||||||
|
const runningTaskCount = tasks.filter((task) => taskStatusTone(task.status) === 'running').length;
|
||||||
|
return {
|
||||||
|
agent_id: `plan-agent-${slugifyIdPart(plan.id)}`,
|
||||||
|
plan_id: plan.id,
|
||||||
|
title: plan.title,
|
||||||
|
lifecycle: status,
|
||||||
|
status_label: planStatusLabel(status),
|
||||||
|
status_tone: tone,
|
||||||
|
managed_task_count: tasks.length,
|
||||||
|
running_task_count: runningTaskCount,
|
||||||
|
failed_task_count: failedTaskCount,
|
||||||
|
next_action: failedTaskCount > 0 ? 'review_failures' : runningTaskCount > 0 ? 'monitor_runs' : tone === 'success' ? 'archive_results' : 'activate_plan',
|
||||||
|
can_replan: failedTaskCount > 0,
|
||||||
|
can_pause: runningTaskCount > 0,
|
||||||
|
can_resume: status === 'paused',
|
||||||
|
updated_at: latestRuntimeTime(plan, tasks),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTaskAgents(snapshot: QimingclawSnapshot | null): DigitalEmployeeTaskAgentState[] {
|
||||||
|
const runs = runtimeRuns(snapshot);
|
||||||
|
return runtimeTasks(snapshot).map((task) => {
|
||||||
|
const latestRun = latestRunForTask(runs, task.id);
|
||||||
|
const tone = taskStatusTone(task.status);
|
||||||
|
return {
|
||||||
|
agent_id: `task-agent-${slugifyIdPart(task.id)}`,
|
||||||
|
task_id: task.id,
|
||||||
|
plan_id: task.planId ?? null,
|
||||||
|
title: task.title,
|
||||||
|
execution_state: task.status,
|
||||||
|
status_label: taskStatusLabel(task.status),
|
||||||
|
status_tone: tone,
|
||||||
|
assigned_skill_id: task.assignedSkillId ?? null,
|
||||||
|
latest_run_id: latestRun?.id ?? null,
|
||||||
|
can_retry: tone === 'danger',
|
||||||
|
can_skip: tone === 'danger' || tone === 'waiting',
|
||||||
|
can_cancel: tone === 'running',
|
||||||
|
next_action: tone === 'danger' ? 'retry_or_skip' : tone === 'running' ? 'watch_progress' : tone === 'success' ? 'collect_artifacts' : 'start_run',
|
||||||
|
updated_at: latestRun?.updatedAt ?? task.updatedAt,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function latestRunForTask(runs: QimingclawRunRecord[], taskId: string): QimingclawRunRecord | null {
|
||||||
|
const sorted = runs
|
||||||
|
.filter((run) => run.taskId === taskId)
|
||||||
|
.sort((left, right) => (right.updatedAt || right.createdAt).localeCompare(left.updatedAt || left.createdAt));
|
||||||
|
return sorted[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
function runtimePlanToDebugPlan(plan: QimingclawPlanRecord, snapshot: QimingclawSnapshot | null): DebugPlan {
|
function runtimePlanToDebugPlan(plan: QimingclawPlanRecord, snapshot: QimingclawSnapshot | null): DebugPlan {
|
||||||
const tasks = runtimePlanTasks(snapshot, plan.id);
|
const tasks = runtimePlanTasks(snapshot, plan.id);
|
||||||
const status = planRecordStatus(plan, tasks);
|
const status = planRecordStatus(plan, tasks);
|
||||||
@@ -1800,6 +1859,9 @@ function buildWorkday(date: string, snapshot: QimingclawSnapshot | null): Digita
|
|||||||
const managedServices = buildManagedServices(snapshot);
|
const managedServices = buildManagedServices(snapshot);
|
||||||
const managedServiceIssues = managedServices.filter((service) => service.requires_human_action || service.status === 'degraded');
|
const managedServiceIssues = managedServices.filter((service) => service.requires_human_action || service.status === 'degraded');
|
||||||
const runningManagedServiceCount = managedServices.filter((service) => service.running).length;
|
const runningManagedServiceCount = managedServices.filter((service) => service.running).length;
|
||||||
|
const planAgents = buildPlanAgents(snapshot);
|
||||||
|
const taskAgents = buildTaskAgents(snapshot);
|
||||||
|
const actionableTaskAgents = taskAgents.filter((agent) => agent.can_retry || agent.can_skip || agent.can_cancel);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
source: 'live',
|
source: 'live',
|
||||||
@@ -1852,9 +1914,14 @@ function buildWorkday(date: string, snapshot: QimingclawSnapshot | null): Digita
|
|||||||
{ label: '同步异常', value: String(sync?.failed ?? 0) },
|
{ label: '同步异常', value: String(sync?.failed ?? 0) },
|
||||||
{ label: '服务运行', value: `${runningManagedServiceCount}/${managedServices.length}` },
|
{ label: '服务运行', value: `${runningManagedServiceCount}/${managedServices.length}` },
|
||||||
{ label: '需处理服务', value: String(managedServiceIssues.length) },
|
{ label: '需处理服务', value: String(managedServiceIssues.length) },
|
||||||
|
{ label: 'Plan Agent', value: String(planAgents.length) },
|
||||||
|
{ label: 'Task Agent', value: String(taskAgents.length) },
|
||||||
|
{ label: '可治理任务', value: String(actionableTaskAgents.length) },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
managed_services: managedServices,
|
managed_services: managedServices,
|
||||||
|
plan_agents: planAgents,
|
||||||
|
task_agents: taskAgents,
|
||||||
duties,
|
duties,
|
||||||
events,
|
events,
|
||||||
execution_summaries: executionSummaries,
|
execution_summaries: executionSummaries,
|
||||||
@@ -1875,6 +1942,10 @@ function buildWorkday(date: string, snapshot: QimingclawSnapshot | null): Digita
|
|||||||
serviceLine(snapshot, 'computerServer', 'Computer Server'),
|
serviceLine(snapshot, 'computerServer', 'Computer Server'),
|
||||||
managedServiceSummary(managedServices),
|
managedServiceSummary(managedServices),
|
||||||
...managedServiceIssues.slice(0, 3).map(managedServiceLine),
|
...managedServiceIssues.slice(0, 3).map(managedServiceLine),
|
||||||
|
`Plan Agent:${planAgents.length} 个计划代理,Task Agent:${taskAgents.length} 个任务代理。`,
|
||||||
|
actionableTaskAgents[0]
|
||||||
|
? `待治理任务:${actionableTaskAgents[0].title},下一步 ${actionableTaskAgents[0].next_action}。`
|
||||||
|
: 'Task Agent:暂无需要重试、跳过或取消的任务。',
|
||||||
syncLine(snapshot),
|
syncLine(snapshot),
|
||||||
`活跃会话:${snapshot?.sessions.active ?? 0} / ${snapshot?.sessions.total ?? 0}`,
|
`活跃会话:${snapshot?.sessions.active ?? 0} / ${snapshot?.sessions.total ?? 0}`,
|
||||||
],
|
],
|
||||||
@@ -2054,6 +2125,9 @@ async function handleGovernanceCommand(
|
|||||||
const planId = stringFromUnknown(body.context_refs?.plan_id)
|
const planId = stringFromUnknown(body.context_refs?.plan_id)
|
||||||
|| stringFromUnknown(body.payload?.plan_id)
|
|| stringFromUnknown(body.payload?.plan_id)
|
||||||
|| stringFromUnknown(body.payload?.original_plan_id);
|
|| stringFromUnknown(body.payload?.original_plan_id);
|
||||||
|
const taskId = stringFromUnknown(body.context_refs?.task_id)
|
||||||
|
|| stringFromUnknown(body.payload?.task_id)
|
||||||
|
|| stringFromUnknown(body.payload?.original_task_id);
|
||||||
const scheduleId = stringFromUnknown(body.context_refs?.schedule_id)
|
const scheduleId = stringFromUnknown(body.context_refs?.schedule_id)
|
||||||
|| stringFromUnknown(body.payload?.schedule_id);
|
|| stringFromUnknown(body.payload?.schedule_id);
|
||||||
|
|
||||||
@@ -2108,6 +2182,20 @@ async function handleGovernanceCommand(
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (['retry_task', 'skip_task', 'pause_task', 'resume_task', 'provide_task_input', 'cancel_task', 'start_run', 'cancel_run'].includes(body.command_kind)) {
|
||||||
|
const targetTask = taskId || runtimeTasks(snapshot)[0]?.id || 'qimingclaw-task-agent';
|
||||||
|
const response = governanceAccepted(commandId, correlationId, `已接受 ${body.command_kind},并记录到 qimingclaw Task Agent 控制平面。`, {
|
||||||
|
plan_id: planId || runtimeTasks(snapshot).find((task) => task.id === targetTask)?.planId || 'qimingclaw-task-agent-plan',
|
||||||
|
task_id: targetTask,
|
||||||
|
task_agent_id: `task-agent-${slugifyIdPart(targetTask)}`,
|
||||||
|
status: taskGovernanceStatus(body.command_kind),
|
||||||
|
next_action: taskGovernanceNextAction(body.command_kind),
|
||||||
|
source: 'qimingclaw-task-agent',
|
||||||
|
});
|
||||||
|
await recordGovernanceCommand(body, response);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
command_id: commandId,
|
command_id: commandId,
|
||||||
accepted: false,
|
accepted: false,
|
||||||
@@ -2134,6 +2222,46 @@ function governanceAccepted(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function taskGovernanceStatus(commandKind: string): string {
|
||||||
|
switch (commandKind) {
|
||||||
|
case 'retry_task':
|
||||||
|
case 'start_run':
|
||||||
|
case 'resume_task':
|
||||||
|
return 'running';
|
||||||
|
case 'skip_task':
|
||||||
|
return 'skipped';
|
||||||
|
case 'cancel_task':
|
||||||
|
case 'cancel_run':
|
||||||
|
return 'cancelled';
|
||||||
|
case 'pause_task':
|
||||||
|
return 'paused';
|
||||||
|
default:
|
||||||
|
return 'accepted';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function taskGovernanceNextAction(commandKind: string): string {
|
||||||
|
switch (commandKind) {
|
||||||
|
case 'retry_task':
|
||||||
|
return 'start_retry_run';
|
||||||
|
case 'skip_task':
|
||||||
|
return 'continue_next_task';
|
||||||
|
case 'pause_task':
|
||||||
|
return 'await_resume';
|
||||||
|
case 'resume_task':
|
||||||
|
return 'watch_progress';
|
||||||
|
case 'provide_task_input':
|
||||||
|
return 'resume_with_input';
|
||||||
|
case 'cancel_task':
|
||||||
|
case 'cancel_run':
|
||||||
|
return 'close_execution';
|
||||||
|
case 'start_run':
|
||||||
|
return 'watch_progress';
|
||||||
|
default:
|
||||||
|
return 'record_control_event';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function stringFromUnknown(value: unknown): string {
|
function stringFromUnknown(value: unknown): string {
|
||||||
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import type {
|
|||||||
DigitalEmployeeDuty,
|
DigitalEmployeeDuty,
|
||||||
DigitalEmployeeManagedService,
|
DigitalEmployeeManagedService,
|
||||||
DigitalEmployeeRunDetail,
|
DigitalEmployeeRunDetail,
|
||||||
|
DigitalEmployeeTaskAgentState,
|
||||||
DigitalEmployeeTaskExecutionSummary,
|
DigitalEmployeeTaskExecutionSummary,
|
||||||
DigitalEmployeeWorkEvent,
|
DigitalEmployeeWorkEvent,
|
||||||
DigitalEmployeeWorkdayProjection,
|
DigitalEmployeeWorkdayProjection,
|
||||||
@@ -717,6 +718,11 @@ function managedServiceSummary(service: DigitalEmployeeManagedService): string {
|
|||||||
return `${service.name} ${service.status_label}${service.error ? `:${service.error}` : deps}`;
|
return `${service.name} ${service.status_label}${service.error ? `:${service.error}` : deps}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function taskAgentSummary(agent: DigitalEmployeeTaskAgentState): string {
|
||||||
|
const next = agent.next_action === 'retry_or_skip' ? '重试或跳过' : agent.next_action === 'watch_progress' ? '观察进度' : agent.next_action;
|
||||||
|
return `${agent.title} ${agent.status_label},下一步 ${next}`;
|
||||||
|
}
|
||||||
|
|
||||||
function isDecisionOpen(status?: string): boolean {
|
function isDecisionOpen(status?: string): boolean {
|
||||||
return ['pending', 'requested'].includes(String(status || '').toLowerCase());
|
return ['pending', 'requested'].includes(String(status || '').toLowerCase());
|
||||||
}
|
}
|
||||||
@@ -998,6 +1004,8 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
|||||||
const delivery = workdayProjection?.delivery;
|
const delivery = workdayProjection?.delivery;
|
||||||
const managedServices = isLiveProjection ? (workdayProjection?.managed_services ?? []) : [];
|
const managedServices = isLiveProjection ? (workdayProjection?.managed_services ?? []) : [];
|
||||||
const managedServiceIssues = managedServices.filter((service) => service.requires_human_action || service.status === 'degraded');
|
const managedServiceIssues = managedServices.filter((service) => service.requires_human_action || service.status === 'degraded');
|
||||||
|
const taskAgents = isLiveProjection ? (workdayProjection?.task_agents ?? []) : [];
|
||||||
|
const actionableTaskAgents = taskAgents.filter((agent) => agent.can_retry || agent.can_skip || agent.can_cancel);
|
||||||
const dailyReport = isLiveProjection ? workdayProjection?.daily_report : undefined;
|
const dailyReport = isLiveProjection ? workdayProjection?.daily_report : undefined;
|
||||||
const runDetails = isLiveProjection
|
const runDetails = isLiveProjection
|
||||||
? (workdayProjection?.run_details?.length ? workdayProjection.run_details : allEvents.map(runDetailFromEvent))
|
? (workdayProjection?.run_details?.length ? workdayProjection.run_details : allEvents.map(runDetailFromEvent))
|
||||||
@@ -1055,14 +1063,18 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
|||||||
? `已加载 ${totalDuties.length} 项任务,当前选择 ${selectedDuties.length} 项;右侧展示真实执行状态、运行明细和日报。`
|
? `已加载 ${totalDuties.length} 项任务,当前选择 ${selectedDuties.length} 项;右侧展示真实执行状态、运行明细和日报。`
|
||||||
: (workday?.summary ?? '等待同步当天任务计划。');
|
: (workday?.summary ?? '等待同步当天任务计划。');
|
||||||
const currentActionTitle = currentEvent?.task_title
|
const currentActionTitle = currentEvent?.task_title
|
||||||
?? (managedServiceIssues[0] ? managedServiceIssues[0].name : workday?.confirmed ? '等待下一条执行事件' : '等待确认任务设置');
|
?? (actionableTaskAgents[0] ? actionableTaskAgents[0].title : managedServiceIssues[0] ? managedServiceIssues[0].name : workday?.confirmed ? '等待下一条执行事件' : '等待确认任务设置');
|
||||||
const currentActionDetail = currentEvent
|
const currentActionDetail = currentEvent
|
||||||
? `${currentEvent.plan_title}:${eventBusinessResult(currentEvent)}(${eventTimingSummary(currentEvent)})`
|
? `${currentEvent.plan_title}:${eventBusinessResult(currentEvent)}(${eventTimingSummary(currentEvent)})`
|
||||||
|
: actionableTaskAgents[0]
|
||||||
|
? taskAgentSummary(actionableTaskAgents[0])
|
||||||
: managedServiceIssues[0]
|
: managedServiceIssues[0]
|
||||||
? managedServiceSummary(managedServiceIssues[0])
|
? managedServiceSummary(managedServiceIssues[0])
|
||||||
: '任务执行后会显示正在处理的任务、步骤或汇总动作。';
|
: '任务执行后会显示正在处理的任务、步骤或汇总动作。';
|
||||||
const nextStepText = pendingDecisions[0]
|
const nextStepText = pendingDecisions[0]
|
||||||
? `等待人工确认:${pendingDecisions[0].title}`
|
? `等待人工确认:${pendingDecisions[0].title}`
|
||||||
|
: actionableTaskAgents[0]
|
||||||
|
? `Task Agent:${taskAgentSummary(actionableTaskAgents[0])}`
|
||||||
: managedServiceIssues[0]
|
: managedServiceIssues[0]
|
||||||
? `处理服务状态:${managedServiceSummary(managedServiceIssues[0])}`
|
? `处理服务状态:${managedServiceSummary(managedServiceIssues[0])}`
|
||||||
: workday?.confirmed
|
: workday?.confirmed
|
||||||
|
|||||||
@@ -605,6 +605,40 @@ export interface DigitalEmployeeManagedService {
|
|||||||
lease?: Record<string, unknown>;
|
lease?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DigitalEmployeePlanAgentState {
|
||||||
|
agent_id: string;
|
||||||
|
plan_id: string;
|
||||||
|
title: string;
|
||||||
|
lifecycle: string;
|
||||||
|
status_label: string;
|
||||||
|
status_tone: string;
|
||||||
|
managed_task_count: number;
|
||||||
|
running_task_count: number;
|
||||||
|
failed_task_count: number;
|
||||||
|
next_action: string;
|
||||||
|
can_replan: boolean;
|
||||||
|
can_pause: boolean;
|
||||||
|
can_resume: boolean;
|
||||||
|
updated_at?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DigitalEmployeeTaskAgentState {
|
||||||
|
agent_id: string;
|
||||||
|
task_id: string;
|
||||||
|
plan_id?: string | null;
|
||||||
|
title: string;
|
||||||
|
execution_state: string;
|
||||||
|
status_label: string;
|
||||||
|
status_tone: string;
|
||||||
|
assigned_skill_id?: string | null;
|
||||||
|
latest_run_id?: string | null;
|
||||||
|
can_retry: boolean;
|
||||||
|
can_skip: boolean;
|
||||||
|
can_cancel: boolean;
|
||||||
|
next_action: string;
|
||||||
|
updated_at?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DigitalEmployeeWorkEvent {
|
export interface DigitalEmployeeWorkEvent {
|
||||||
id: string;
|
id: string;
|
||||||
time: string;
|
time: string;
|
||||||
@@ -728,6 +762,8 @@ export interface DigitalEmployeeWorkdayProjection {
|
|||||||
workday: DigitalEmployeeWorkdayState;
|
workday: DigitalEmployeeWorkdayState;
|
||||||
overview: DigitalEmployeeOverview;
|
overview: DigitalEmployeeOverview;
|
||||||
managed_services?: DigitalEmployeeManagedService[];
|
managed_services?: DigitalEmployeeManagedService[];
|
||||||
|
plan_agents?: DigitalEmployeePlanAgentState[];
|
||||||
|
task_agents?: DigitalEmployeeTaskAgentState[];
|
||||||
duties: DigitalEmployeeDuty[];
|
duties: DigitalEmployeeDuty[];
|
||||||
events: DigitalEmployeeWorkEvent[];
|
events: DigitalEmployeeWorkEvent[];
|
||||||
execution_summaries?: DigitalEmployeeTaskExecutionSummary[];
|
execution_summaries?: DigitalEmployeeTaskExecutionSummary[];
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -15,7 +15,7 @@
|
|||||||
console.warn("[qimingclaw] digital employee auth bootstrap skipped", error);
|
console.warn("[qimingclaw] digital employee auth bootstrap skipped", error);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script type="module" crossorigin src="./assets/index-DALQvQtr.js"></script>
|
<script type="module" crossorigin src="./assets/index-C9vE8PTK.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-BgRGGB2I.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-BgRGGB2I.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1104,9 +1104,18 @@ User
|
|||||||
-> Digital Employee Projection
|
-> Digital Employee Projection
|
||||||
```
|
```
|
||||||
|
|
||||||
|
当前客户端轻量实现:
|
||||||
|
|
||||||
|
- embedded adapter 从本地 Plan / Task / Run 派生 `plan_agents` 和 `task_agents`。
|
||||||
|
- Plan Agent 包含计划生命周期、托管任务数、失败任务数、下一步动作和是否可重规划。
|
||||||
|
- Task Agent 包含执行状态、绑定 Skill、最新 Run、是否可 retry / skip / cancel,以及下一步动作。
|
||||||
|
- 首页 overview 展示 Plan Agent、Task Agent 和可治理任务数量;播报区域优先提示可治理 Task Agent。
|
||||||
|
- `retry_task`、`skip_task`、`pause_task`、`resume_task`、`provide_task_input`、`cancel_task`、`start_run`、`cancel_run` governance command 会被 qimingclaw adapter 接受并写入正式本地 governance 记录。
|
||||||
|
|
||||||
### 产出
|
### 产出
|
||||||
|
|
||||||
- qimingclaw 从“Agent 客户端”升级为“数字员工任务操作系统”。
|
- qimingclaw 从“Agent 客户端”升级为“数字员工任务操作系统”。
|
||||||
|
- 计划/任务执行开始具备 Plan Agent / Task Agent 控制平面投影。
|
||||||
|
|
||||||
## 推荐实施顺序
|
## 推荐实施顺序
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user