repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
public Clawd ADK gateway launch mirror
stars
latest
clone command
git clone gitlawb://did:key:z6Mkq5mY...iFZ5/my-project-publ...git clone gitlawb://did:key:z6Mkq5mY.../my-project-publ...2fa351d6docs: add automaton and perps launch sources16d ago| #1 | import { readJSONSync } from 'fs-extra'; |
| #2 | import { get, merge, set } from 'lodash-es'; |
| #3 | import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; |
| #4 | import { resolve } from 'node:path'; |
| #5 | import pMap from 'p-map'; |
| #6 | |
| #7 | import { agents, agentsDir, config, localesDir } from '../core/constants'; |
| #8 | import { AgentParser } from '../parsers/agent-parser'; |
| #9 | import { generateAgentContentAndCategory } from '../processors/category-processor'; |
| #10 | import { translateJSON } from '../processors/i18n-processor'; |
| #11 | import { checkJSON, deleteEmptyJsonFiles, getLocaleAgentFileName, writeJSON } from '../utils/file'; |
| #12 | import { Logger } from '../utils/logger'; |
| #13 | import { formatAgentJSON, formatPrompt } from '../validators/agent-validator'; |
| #14 | import { validateTranslationLanguage } from '../validators/language-validator'; |
| #15 | |
| #16 | /** |
| #17 | * Agent 格式化器类 |
| #18 | * 负责格式化 Agent 配置文件和生成多语言版本 |
| #19 | */ |
| #20 | class AgentFormatter { |
| #21 | private ignoreFilePath = '.i18nignore'; |
| #22 | private ignoredFiles: Set<string> = new Set(); |
| #23 | |
| #24 | constructor() { |
| #25 | this.loadIgnoreList(); |
| #26 | } |
| #27 | |
| #28 | /** |
| #29 | * 加载忽略文件列表 |
| #30 | */ |
| #31 | private loadIgnoreList = () => { |
| #32 | try { |
| #33 | if (existsSync(this.ignoreFilePath)) { |
| #34 | const content = readFileSync(this.ignoreFilePath, 'utf8'); |
| #35 | const lines = content |
| #36 | .split('\n') |
| #37 | .map((line) => line.trim()) |
| #38 | .filter((line) => line && !line.startsWith('#')); |
| #39 | |
| #40 | this.ignoredFiles = new Set(lines); |
| #41 | if (this.ignoredFiles.size > 0) { |
| #42 | Logger.info(`已加载忽略文件列表`, `${this.ignoredFiles.size} 个文件`); |
| #43 | } |
| #44 | } |
| #45 | } catch (error) { |
| #46 | Logger.warn('加载忽略文件列表失败', error); |
| #47 | } |
| #48 | }; |
| #49 | |
| #50 | /** |
| #51 | * 添加文件到忽略列表 |
| #52 | * @param filePath 要忽略的文件路径 |
| #53 | */ |
| #54 | private addToIgnoreList = (filePath: string) => { |
| #55 | if (this.ignoredFiles.has(filePath)) { |
| #56 | return; // 已存在,不重复添加 |
| #57 | } |
| #58 | |
| #59 | this.ignoredFiles.add(filePath); |
| #60 | |
| #61 | try { |
| #62 | let content = ''; |
| #63 | if (existsSync(this.ignoreFilePath)) { |
| #64 | content = readFileSync(this.ignoreFilePath, 'utf8'); |
| #65 | } else { |
| #66 | content = '# 语言验证忽略文件\n# 此文件中列出的路径将跳过语言验证\n\n'; |
| #67 | } |
| #68 | |
| #69 | // 添加新的忽略文件,确保末尾有换行符 |
| #70 | if (!content.endsWith('\n')) { |
| #71 | content += '\n'; |
| #72 | } |
| #73 | content += `${filePath}\n`; |
| #74 | |
| #75 | writeFileSync(this.ignoreFilePath, content, 'utf8'); |
| #76 | Logger.warn(`已添加到忽略列表`, filePath); |
| #77 | } catch (error) { |
| #78 | Logger.error('添加到忽略列表失败', error); |
| #79 | } |
| #80 | }; |
| #81 | |
| #82 | /** |
| #83 | * 检查文件是否在忽略列表中 |
| #84 | * @param filePath 文件路径 |
| #85 | * @returns 是否被忽略 |
| #86 | */ |
| #87 | private isIgnored = (filePath: string): boolean => { |
| #88 | return this.ignoredFiles.has(filePath); |
| #89 | }; |
| #90 | |
| #91 | /** |
| #92 | * 获取对应的 en-US 兜底文件路径 |
| #93 | * @param agentId Agent ID |
| #94 | * @returns en-US 文件路径 |
| #95 | */ |
| #96 | private getEnUsFallbackPath = (agentId: string): string => { |
| #97 | return resolve(localesDir, `${agentId}/index.json`); |
| #98 | }; |
| #99 | |
| #100 | /** |
| #101 | * 获取 en-US 兜底数据 |
| #102 | * @param agentId Agent ID |
| #103 | * @param dataToTranslate 需要翻译的数据结构 |
| #104 | * @returns en-US 兜底数据,如果不存在则返回 null |
| #105 | */ |
| #106 | private getEnUsFallbackData = (agentId: string, dataToTranslate: any): any | null => { |
| #107 | try { |
| #108 | const enUsPath = this.getEnUsFallbackPath(agentId); |
| #109 | |
| #110 | if (!existsSync(enUsPath)) { |
| #111 | Logger.warn(`en-US 兜底文件不存在`, enUsPath); |
| #112 | return null; |
| #113 | } |
| #114 | |
| #115 | const enUsData = readJSONSync(enUsPath); |
| #116 | |
| #117 | // 从 en-US 数据中提取与 dataToTranslate 相同结构的数据 |
| #118 | const fallbackData = {}; |
| #119 | |
| #120 | for (const key of config.selectors) { |
| #121 | const sourceValue = get(dataToTranslate, key); |
| #122 | const fallbackValue = get(enUsData, key); |
| #123 | |
| #124 | if (sourceValue !== undefined && fallbackValue !== undefined) { |
| #125 | set(fallbackData, key, fallbackValue); |
| #126 | } |
| #127 | } |
| #128 | |
| #129 | return Object.keys(fallbackData).length > 0 ? fallbackData : null; |
| #130 | } catch (error) { |
| #131 | Logger.error(`获取 en-US 兜底数据失败`, error); |
| #132 | return null; |
| #133 | } |
| #134 | }; |
| #135 | |
| #136 | /** |
| #137 | * 从翻译文件中提取可检测的文本内容 |
| #138 | * @param data 翻译数据对象 |
| #139 | * @returns 可检测的文本内容字符串 |
| #140 | */ |
| #141 | private extractDetectableText = (data: any): string => { |
| #142 | const texts: string[] = []; |
| #143 | |
| #144 | // 提取各种文本字段 |
| #145 | if (data.meta?.title) texts.push(data.meta.title); |
| #146 | if (data.meta?.description) texts.push(data.meta.description); |
| #147 | if (data.config?.systemRole) texts.push(data.config.systemRole); |
| #148 | if (data.config?.openingMessage) texts.push(data.config.openingMessage); |
| #149 | |
| #150 | // 提取开启问题数组 |
| #151 | if (data.config?.openingQuestions && Array.isArray(data.config.openingQuestions)) { |
| #152 | texts.push(...data.config.openingQuestions); |
| #153 | } |
| #154 | |
| #155 | // 提取标签(如果是字符串数组) |
| #156 | if (data.meta?.tags && Array.isArray(data.meta.tags)) { |
| #157 | texts.push(...data.meta.tags.filter((tag: any) => typeof tag === 'string')); |
| #158 | } |
| #159 | |
| #160 | // 提取示例内容 |
| #161 | if (data.examples && Array.isArray(data.examples)) { |
| #162 | data.examples.forEach((example: any) => { |
| #163 | if (example.content) texts.push(example.content); |
| #164 | }); |
| #165 | } |
| #166 | |
| #167 | // 提取摘要 |
| #168 | if (data.summary) texts.push(data.summary); |
| #169 | |
| #170 | return texts.join(' ').trim(); |
| #171 | }; |
| #172 | |
| #173 | /** |
| #174 | * 比较源数据和已有翻译数据,找出需要翻译的新增字段 |
| #175 | * @param sourceData 源数据 |
| #176 | * @param existingData 已有翻译数据 |
| #177 | * @returns 需要翻译的数据和是否有更新 |
| #178 | */ |
| #179 | private getIncrementalData = (sourceData: any, existingData: any) => { |
| #180 | const needsTranslation = {}; |
| #181 | let hasUpdates = false; |
| #182 | |
| #183 | for (const key of config.selectors) { |
| #184 | const sourceValue = get(sourceData, key); |
| #185 | const existingValue = get(existingData, key); |
| #186 | |
| #187 | // 如果源数据中有该字段,但翻译数据中没有,则需要翻译 |
| #188 | if (sourceValue && existingValue === undefined) { |
| #189 | set(needsTranslation, key, sourceValue); |
| #190 | hasUpdates = true; |
| #191 | } |
| #192 | } |
| #193 | |
| #194 | return { hasUpdates, needsTranslation }; |
| #195 | }; |
| #196 | |
| #197 | /** |
| #198 | * 格式化单个 JSON 文件 |
| #199 | * @param fileName 文件名 |
| #200 | */ |
| #201 | formatJSON = async (fileName: string) => { |
| #202 | let { content: agent, id, locale: defaultLocale } = AgentParser.parseFile(fileName); |
| #203 | |
| #204 | // 格式化 Agent JSON |
| #205 | agent = await formatAgentJSON(agent, defaultLocale); |
| #206 | |
| #207 | // 检查是否需要生成分类或内容 |
| #208 | const needsCategory = !agent.meta.category; |
| #209 | const needsContentGeneration = |
| #210 | !agent.summary || |
| #211 | !agent.examples || |
| #212 | !agent.config.openingMessage || |
| #213 | !agent.config.openingQuestions; |
| #214 | |
| #215 | // 如果需要生成分类或内容,使用合并的函数一次性生成 |
| #216 | if (needsCategory || needsContentGeneration) { |
| #217 | agent = await generateAgentContentAndCategory(agent); |
| #218 | Logger.success('内容生成和分类完成', id); |
| #219 | } |
| #220 | |
| #221 | // 写入格式化后的文件 |
| #222 | writeJSON(resolve(agentsDir, fileName), agent); |
| #223 | |
| #224 | // 国际化工作流 |
| #225 | let rawData = {}; |
| #226 | |
| #227 | // 提取需要翻译的字段 |
| #228 | for (const key of config.selectors) { |
| #229 | const rawValue = get(agent, key); |
| #230 | if (rawValue) set(rawData, key, rawValue); |
| #231 | } |
| #232 | |
| #233 | if (Object.keys(rawData).length > 0) { |
| #234 | const directoryPath = resolve(localesDir, id); |
| #235 | |
| #236 | // 创建本地化目录 |
| #237 | if (!existsSync(directoryPath)) { |
| #238 | mkdirSync(directoryPath, { recursive: true }); |
| #239 | } |
| #240 | |
| #241 | // 并行生成多语言版本 |
| #242 | await pMap( |
| #243 | config.outputLocales, |
| #244 | async (locale: string) => { |
| #245 | const localeFileName = getLocaleAgentFileName(id, locale); |
| #246 | const localeFilePath = resolve(localesDir, localeFileName); |
| #247 | const relativeFilePath = `locales/${localeFileName}`; |
| #248 | |
| #249 | // 检查是否需要增量翻译 |
| #250 | let dataToTranslate = rawData; |
| #251 | let existingTranslation = {}; |
| #252 | let isIncremental = false; |
| #253 | |
| #254 | if (existsSync(localeFilePath)) { |
| #255 | try { |
| #256 | existingTranslation = readJSONSync(localeFilePath); |
| #257 | const { needsTranslation, hasUpdates } = this.getIncrementalData( |
| #258 | rawData, |
| #259 | existingTranslation, |
| #260 | ); |
| #261 | |
| #262 | if (hasUpdates) { |
| #263 | dataToTranslate = needsTranslation; |
| #264 | isIncremental = true; |
| #265 | Logger.info(`检测到新增字段`, `${id} (${locale})`); |
| #266 | } else { |
| #267 | return; |
| #268 | } |
| #269 | } catch { |
| #270 | Logger.warn(`读取已有翻译文件失败,执行完整翻译`, `${id} (${locale})`); |
| #271 | } |
| #272 | } |
| #273 | |
| #274 | // 检查文件是否在忽略列表中 |
| #275 | if (this.isIgnored(relativeFilePath)) { |
| #276 | Logger.info('跳过被忽略的文件', relativeFilePath); |
| #277 | return; |
| #278 | } |
| #279 | |
| #280 | Logger.translate(id, defaultLocale, locale, 'start'); |
| #281 | |
| #282 | let translateResult; |
| #283 | |
| #284 | // 如果 locale 是 defaultLocale,直接使用原数据不走 AI 翻译 |
| #285 | if (locale === defaultLocale) { |
| #286 | translateResult = dataToTranslate; |
| #287 | Logger.info(`跳过 AI 翻译`, `${id} (${locale}) - 使用默认语言数据`); |
| #288 | } else { |
| #289 | translateResult = await translateJSON( |
| #290 | localeFileName, |
| #291 | dataToTranslate, |
| #292 | locale, |
| #293 | defaultLocale, |
| #294 | ); |
| #295 | } |
| #296 | |
| #297 | if (translateResult) { |
| #298 | let finalResult = translateResult; |
| #299 | |
| #300 | // 如果是增量翻译,合并已有翻译 |
| #301 | if (isIncremental) { |
| #302 | finalResult = merge({}, existingTranslation, translateResult); |
| #303 | Logger.info(`合并增量翻译完成`, `${id} (${locale})`); |
| #304 | } |
| #305 | |
| #306 | // 格式化翻译后的系统角色 |
| #307 | if (finalResult.config?.systemRole) { |
| #308 | finalResult.config.systemRole = await formatPrompt( |
| #309 | finalResult.config.systemRole, |
| #310 | locale, |
| #311 | ); |
| #312 | } |
| #313 | |
| #314 | // 先写入文件 |
| #315 | writeJSON(localeFilePath, finalResult); |
| #316 | |
| #317 | // 验证翻译语言是否匹配 |
| #318 | let validationResult = await validateTranslationLanguage(localeFilePath); |
| #319 | |
| #320 | if (!validationResult.valid) { |
| #321 | Logger.warn( |
| #322 | '语言验证失败,尝试重新翻译', |
| #323 | `${localeFileName}: 期望 ${validationResult.expectedLanguage}, 检测到 ${validationResult.detectedLanguage}`, |
| #324 | ); |
| #325 | |
| #326 | // 重新翻译 |
| #327 | Logger.translate(id, defaultLocale, locale, 'start'); |
| #328 | let retryTranslateResult; |
| #329 | |
| #330 | // 如果 locale 是 defaultLocale,直接使用原数据不走 AI 翻译 |
| #331 | if (locale === defaultLocale) { |
| #332 | retryTranslateResult = dataToTranslate; |
| #333 | Logger.info(`跳过 AI 重新翻译`, `${id} (${locale}) - 使用默认语言数据`); |
| #334 | } else { |
| #335 | retryTranslateResult = await translateJSON( |
| #336 | localeFileName, |
| #337 | dataToTranslate, |
| #338 | locale, |
| #339 | defaultLocale, |
| #340 | ); |
| #341 | } |
| #342 | |
| #343 | if (retryTranslateResult) { |
| #344 | let retryFinalResult = retryTranslateResult; |
| #345 | |
| #346 | // 如果是增量翻译,合并已有翻译 |
| #347 | if (isIncremental) { |
| #348 | retryFinalResult = merge({}, existingTranslation, retryTranslateResult); |
| #349 | } |
| #350 | |
| #351 | // 格式化翻译后的系统角色 |
| #352 | if (retryFinalResult.config?.systemRole) { |
| #353 | retryFinalResult.config.systemRole = await formatPrompt( |
| #354 | retryFinalResult.config.systemRole, |
| #355 | locale, |
| #356 | ); |
| #357 | } |
| #358 | |
| #359 | // 再次验证 |
| #360 | writeJSON(localeFilePath, retryFinalResult); |
| #361 | const retryValidationResult = await validateTranslationLanguage(localeFilePath); |
| #362 | |
| #363 | if (retryValidationResult.valid) { |
| #364 | Logger.success( |
| #365 | '重新翻译验证通过', |
| #366 | `${localeFileName}: 期望 ${retryValidationResult.expectedLanguage}, 检测到 ${retryValidationResult.detectedLanguage}`, |
| #367 | ); |
| #368 | Logger.translate(id, defaultLocale, locale, 'success'); |
| #369 | } else { |
| #370 | // 两次验证都失败,使用 en-US 兜底 |
| #371 | Logger.warn('重新翻译仍验证失败,尝试使用 en-US 兜底', localeFileName); |
| #372 | |
| #373 | const fallbackData = this.getEnUsFallbackData(id, dataToTranslate); |
| #374 | if (fallbackData) { |
| #375 | let fallbackFinalResult = fallbackData; |
| #376 | |
| #377 | // 如果是增量翻译,合并已有翻译 |
| #378 | if (isIncremental) { |
| #379 | fallbackFinalResult = merge({}, existingTranslation, fallbackData); |
| #380 | } |
| #381 | |
| #382 | writeJSON(localeFilePath, fallbackFinalResult); |
| #383 | this.addToIgnoreList(relativeFilePath); |
| #384 | Logger.success('使用 en-US 兜底完成并添加到忽略列表', localeFileName); |
| #385 | Logger.translate(id, defaultLocale, locale, 'success'); |
| #386 | } else { |
| #387 | // en-US 兜底也失败,添加到忽略列表 |
| #388 | Logger.error('en-US 兜底失败,添加到忽略列表', localeFileName); |
| #389 | this.addToIgnoreList(relativeFilePath); |
| #390 | Logger.translate(id, defaultLocale, locale, 'error'); |
| #391 | } |
| #392 | } |
| #393 | } else { |
| #394 | // 重新翻译失败,使用 en-US 兜底 |
| #395 | Logger.warn('重新翻译失败,尝试使用 en-US 兜底', localeFileName); |
| #396 | |
| #397 | const fallbackData = this.getEnUsFallbackData(id, dataToTranslate); |
| #398 | if (fallbackData) { |
| #399 | let fallbackFinalResult = fallbackData; |
| #400 | |
| #401 | // 如果是增量翻译,合并已有翻译 |
| #402 | if (isIncremental) { |
| #403 | fallbackFinalResult = merge({}, existingTranslation, fallbackData); |
| #404 | } |
| #405 | |
| #406 | writeJSON(localeFilePath, fallbackFinalResult); |
| #407 | this.addToIgnoreList(relativeFilePath); |
| #408 | Logger.success('使用 en-US 兜底完成并添加到忽略列表', localeFileName); |
| #409 | Logger.translate(id, defaultLocale, locale, 'success'); |
| #410 | } else { |
| #411 | // en-US 兜底也失败,添加到忽略列表 |
| #412 | Logger.error('en-US 兜底失败,添加到忽略列表', localeFileName); |
| #413 | this.addToIgnoreList(relativeFilePath); |
| #414 | Logger.translate(id, defaultLocale, locale, 'error'); |
| #415 | } |
| #416 | } |
| #417 | } else { |
| #418 | Logger.success( |
| #419 | '语言验证通过', |
| #420 | `${localeFileName}: 期望 ${validationResult.expectedLanguage}, 检测到 ${validationResult.detectedLanguage}`, |
| #421 | ); |
| #422 | Logger.translate(id, defaultLocale, locale, 'success'); |
| #423 | } |
| #424 | } else { |
| #425 | // 初次翻译失败,使用 en-US 兜底 |
| #426 | Logger.warn('翻译失败,尝试使用 en-US 兜底', localeFileName); |
| #427 | |
| #428 | const fallbackData = this.getEnUsFallbackData(id, dataToTranslate); |
| #429 | if (fallbackData) { |
| #430 | let fallbackFinalResult = fallbackData; |
| #431 | |
| #432 | // 如果是增量翻译,合并已有翻译 |
| #433 | if (isIncremental) { |
| #434 | fallbackFinalResult = merge({}, existingTranslation, fallbackData); |
| #435 | } |
| #436 | |
| #437 | writeJSON(localeFilePath, fallbackFinalResult); |
| #438 | this.addToIgnoreList(relativeFilePath); |
| #439 | Logger.success('使用 en-US 兜底完成并添加到忽略列表', localeFileName); |
| #440 | Logger.translate(id, defaultLocale, locale, 'success'); |
| #441 | } else { |
| #442 | // en-US 兜底也失败,添加到忽略列表 |
| #443 | Logger.error('en-US 兜底失败,添加到忽略列表', localeFileName); |
| #444 | this.addToIgnoreList(relativeFilePath); |
| #445 | Logger.translate(id, defaultLocale, locale, 'error'); |
| #446 | } |
| #447 | } |
| #448 | }, |
| #449 | { concurrency: config.concurrency }, // 使用配置中的并发数控制 |
| #450 | ); |
| #451 | } |
| #452 | }; |
| #453 | |
| #454 | /** |
| #455 | * 执行格式化流程 |
| #456 | */ |
| #457 | run = async () => { |
| #458 | Logger.start('开始格式化 JSON 内容'); |
| #459 | |
| #460 | // 清理空的 JSON 文件 |
| #461 | deleteEmptyJsonFiles(localesDir); |
| #462 | Logger.info('清理空的 JSON 文件完成'); |
| #463 | |
| #464 | // 并行处理所有 Agent 文件 |
| #465 | const validFiles = agents.filter((file) => checkJSON(file)); |
| #466 | Logger.info('待处理文件数量', validFiles.length); |
| #467 | |
| #468 | await pMap( |
| #469 | validFiles, |
| #470 | async (file, index) => { |
| #471 | Logger.progress(index + 1, validFiles.length, `格式化 ${file.name}`); |
| #472 | await this.formatJSON(file.name); |
| #473 | }, |
| #474 | { concurrency: config.concurrency }, // 使用配置中的并发数控制 |
| #475 | ); |
| #476 | |
| #477 | Logger.success('格式化完成', '', `处理了 ${validFiles.length} 个文件`); |
| #478 | }; |
| #479 | } |
| #480 | |
| #481 | /** |
| #482 | * 格式化 Agent 配置文件 |
| #483 | */ |
| #484 | export const formatAgents = async () => { |
| #485 | Logger.split('格式化 JSON 内容'); |
| #486 | const startTime = Date.now(); |
| #487 | |
| #488 | await new AgentFormatter().run(); |
| #489 | |
| #490 | const duration = Date.now() - startTime; |
| #491 | Logger.success('格式化流程完成', '', `总耗时 ${duration}ms`); |
| #492 | }; |
| #493 | |
| #494 | |
| #495 |