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

This commit is contained in:
dela
2026-10-09 14:08:31 +08:00
parent 8af63417c8
commit 97668dab7e
148 changed files with 1671 additions and 1672 deletions
+4 -4
View File
@@ -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 ---
+4 -4
View File
@@ -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 기본값으로 정규화되어 통과한다
+4 -4
View File
@@ -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 {
+5 -5
View File
@@ -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()))
}
+14 -14
View File
@@ -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
}
+3 -3
View File
@@ -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)
}
}
+8 -8
View File
@@ -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.
+6 -6
View File
@@ -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))
}
+7 -7
View File
@@ -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,
+6 -6
View File
@@ -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)
})
}
+5 -5
View File
@@ -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 的
+3 -3
View File
@@ -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)
})
}
}
+18 -18
View File
@@ -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) {
+11 -11
View File
@@ -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)
}
}
+9 -9
View File
@@ -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)
}
}
+4 -4
View File
@@ -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)
})
}
}
+6 -6
View File
@@ -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 {
+3 -3
View File
@@ -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)
}
}
+1 -1
View File
@@ -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) {
+4 -4
View File
@@ -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) {
+2 -2
View File
@@ -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)
}
}
+4 -4
View File
@@ -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 {
+2 -2
View File
@@ -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)
}
}
+2 -2
View File
@@ -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())
}
+4 -4
View File
@@ -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)
}
+6 -6
View File
@@ -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) {
+7 -7
View File
@@ -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 节点,
+3 -3
View File
@@ -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)
})
}
}
+2 -2
View File
@@ -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)
}
}
+11 -11
View File
@@ -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 {
+3 -3
View File
@@ -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))
})
}
}
+16 -16
View File
@@ -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)})
+19 -19
View File
@@ -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())
}
}
+1 -1
View File
@@ -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())
}
+4 -4
View File
@@ -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 捕获代理配置已移除")
}
}
+2 -2
View File
@@ -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())
}
+7 -7
View File
@@ -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()
}
+3 -3
View File
@@ -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())
}
}
+8 -8
View File
@@ -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 是漏洞推送的投递引擎。
+4 -4
View File
@@ -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())
}
}
+13 -13
View File
@@ -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 是渠道的对外表述。
+6 -6
View File
@@ -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)
}
+11 -11
View File
@@ -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)
}
+4 -4
View File
@@ -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
+3 -3
View File
@@ -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)
}
+3 -3
View File
@@ -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)
}
+30 -30
View File
@@ -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"})
+5 -5
View File
@@ -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)이 한국어
+44 -44
View File
@@ -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: "서버 데이터 루트 디렉터리(모든 작업/대화 산출물의 루트이며, 각 에이전트는 그 아래 하위 디렉터리에 실제로 기록합니다. 예: <DataDir>/<taskID>)", Example: "/app/data", Source: "runtime"},
{Name: "Now", Description: "服务器当前时间(每次执行都会实时刷新,减去固定的起始时间即可计算经过的时长)", Example: "2026-08-11 14:30:00 CST", Source: "runtime"},
{Name: "DataDir", Description: "服务器数据根目录(所有任务/对话产物的根目录,各 Agent 实际写入其下的子目录。例如: <DataDir>/<taskID>)", Example: "/app/data", Source: "runtime"},
}
// withGlobalVars appends the universal runtime vars onto an agent's own catalog,
+13 -13
View File
@@ -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)
}
}
+12 -12
View File
@@ -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 {
+2 -2
View File
@@ -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)
}
}
+3 -3
View File
@@ -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 {
+7 -7
View File
@@ -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
+10 -10
View File
@@ -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))
}
+16 -16
View File
@@ -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.
+8 -8
View File
@@ -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)
}
+1 -1
View File
@@ -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"`
@@ -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())
}
+19 -19
View File
@@ -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
+2 -2
View File
@@ -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) {
+6 -6
View File
@@ -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 {
+7 -7
View File
@@ -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()))
}
+1 -1
View File
@@ -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())
}
+11 -11
View File
@@ -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
}
+6 -6
View File
@@ -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())
}
+1 -1
View File
@@ -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。
+4 -4
View File
@@ -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
+3 -3
View File
@@ -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))
})
}
}
+6 -6
View File
@@ -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 {
+3 -3
View File
@@ -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)
}
+5 -5
View File
@@ -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
+8 -8
View File
@@ -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()))
}
+3 -3
View File
@@ -10,9 +10,9 @@ import (
// 트리거 설정을 검증할 때 사용자에게 돌려주는 오류 문구.
const (
errTriggerNoCondition = "트리거 조건을 하나 이상 선택하세요(정기 실행/취약점 발견/목표 달성/작업 시간 초과/도구 호출/작업 생성)"
errTriggerToolSetEmpty = "도구 호출 트리거는 도구를 하나 이상 선택하세요"
errTriggerCustomOnly = "트리거는 사용자 지정 에이전트만 지원합니다"
errTriggerNoCondition = "请至少选择一个触发条件(定时执行/发现漏洞/达成目标/任务超时/工具调用/任务创建)"
errTriggerToolSetEmpty = "工具调用触发器请至少选择一个工具"
errTriggerCustomOnly = "触发器仅支持自定义代理"
)
func (s *Server) pgListTriggers(w http.ResponseWriter, r *http.Request) {
+2 -2
View File
@@ -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)
}
}
+7 -7
View File
@@ -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 退出。
+9 -9
View File
@@ -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)
}
+1 -1
View File
@@ -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)
})
}
+1 -1
View File
@@ -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) {
+10 -10
View File
@@ -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
+2 -2
View File
@@ -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)
})
}
}