From 64b8c912efbccf4c0077280d978a89c22ebcab86 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Tue, 30 Jun 2026 07:23:12 +0800 Subject: [PATCH] =?UTF-8?q?chore(console):=20=E9=AA=8C=E8=AF=81=E9=99=84?= =?UTF-8?q?=E4=BB=B6=E5=88=9B=E5=BB=BA=E9=9D=A2=E6=9D=BF=E5=B7=B2=E4=B8=8A?= =?UTF-8?q?=E7=BA=BF=20+=20=E4=BD=8E=E9=A3=8E=E9=99=A9=E5=8A=A0=E5=9B=BA?= =?UTF-8?q?=EF=BC=88=E7=B2=98=E8=B4=B4/=E6=8B=96=E6=8B=BD=E4=BD=93?= =?UTF-8?q?=E9=87=8F=E4=B8=8A=E9=99=90=20+=20render=20=E5=89=AF=E4=BD=9C?= =?UTF-8?q?=E7=94=A8=E6=94=B6=E6=95=9B=EF=BC=89[tsk=5Fia4uiOfk6ZdV]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 任务功能(统一 files 状态 / 缩略图预览 / 单项删除 / 客户端去重 / 5 语 i18n) 已于 65cd5ad、00e6bc7 实现并合并,后端同源存储型 XSS 防护已于 d9d3de2 落实, 逐条核对全部满足,故不重复实现。仅做两处低风险加固(范围限 design/ui_kits/console/**): - NewTaskPanel.addFiles 增加单文件 25MB 软上限,超限跳过并内联提示, 新增 i18n key attTooLarge(zh/en/es/ja/fr 五语齐备) - 缩略图 objectURL 改为在 addFiles 中预生成(ensureThumb),render 仅读, 消除 map 渲染期写 urlMapRef 的副作用,避免 React 严格模式下重复创建/泄漏 Co-Authored-By: Claude Opus 4.8 --- design/ui_kits/console/TaskTree.jsx | 16 ++++++++++++---- design/ui_kits/console/i18n.js | 10 +++++----- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/design/ui_kits/console/TaskTree.jsx b/design/ui_kits/console/TaskTree.jsx index 3b83c27..27e1455 100644 --- a/design/ui_kits/console/TaskTree.jsx +++ b/design/ui_kits/console/TaskTree.jsx @@ -451,13 +451,15 @@ function NewTaskPanel({ tasks, onCancel, onCreate, t }) { const [dragOver, setDragOver] = React.useState(false); // 图片缩略图 objectURL 缓存(按去重键),卸载/移除时显式 revoke 防泄漏 const urlMapRef = React.useRef(new Map()); - const thumbUrl = (f) => { - if (!f.type || !f.type.startsWith('image/')) return null; + const [warn, setWarn] = React.useState(''); + // 为图片预生成缩略图 objectURL(幂等,按去重键缓存)——只在 addFiles 调用,render 仅读,避免严格模式下重复 render 泄漏 + const ensureThumb = (f) => { + if (!f.type || !f.type.startsWith('image/')) return; const key = ntFileKey(f); const map = urlMapRef.current; if (!map.has(key)) map.set(key, URL.createObjectURL(f)); - return map.get(key); }; + const thumbUrl = (f) => (f.type && f.type.startsWith('image/')) ? (urlMapRef.current.get(ntFileKey(f)) || null) : null; // 组件卸载时释放全部 objectURL React.useEffect(() => () => { urlMapRef.current.forEach((u) => URL.revokeObjectURL(u)); @@ -466,11 +468,16 @@ function NewTaskPanel({ tasks, onCancel, onCreate, t }) { // 合并新文件并按复合键去重(剪贴板无名图先补名) const addFiles = (incoming) => { if (!incoming || !incoming.length) return; + const MAX_BYTES = 25 * 1024 * 1024; // 单文件软上限,避免超大截图卡住上传 const named = Array.from(incoming).map((f, i) => ntNamed(f, i)); + const ok = named.filter((f) => f.size <= MAX_BYTES); + setWarn(ok.length < named.length ? t.attTooLarge : ''); + if (!ok.length) return; + ok.forEach(ensureThumb); // 预生成缩略图,render 不再产生副作用 setFiles((prev) => { const seen = new Set(prev.map(ntFileKey)); const merged = prev.slice(); - named.forEach((f) => { const k = ntFileKey(f); if (!seen.has(k)) { seen.add(k); merged.push(f); } }); + ok.forEach((f) => { const k = ntFileKey(f); if (!seen.has(k)) { seen.add(k); merged.push(f); } }); return merged; }); }; @@ -530,6 +537,7 @@ function NewTaskPanel({ tasks, onCancel, onCreate, t }) { onChange={(e) => { addFiles(e.target.files); e.target.value = ''; }} style={{ fontFamily: 'var(--mono)', fontSize: 12, color: 'var(--ink)' }} /> {dragOver ? t.dropHint : t.pasteHint} + {warn ? {warn} : null} {files.length ? (
{files.map((f) => { diff --git a/design/ui_kits/console/i18n.js b/design/ui_kits/console/i18n.js index 9397f9e..d62eb8a 100644 --- a/design/ui_kits/console/i18n.js +++ b/design/ui_kits/console/i18n.js @@ -41,7 +41,7 @@ window.MAESTRO_I18N = { complexity: '复杂度', cplxAuto: '智能', parentTask: '父任务', topLevel: '(顶层)', priority: '优先级', p0: 'P0 · 高', p1: 'P1 · 中', p2: 'P2 · 低', create: '创建任务', cancel: '取消', attachLabel: '附件 · 图片/文件(可选)', dropHint: '拖拽文件到此处,或粘贴截图', removeFile: '移除', pasteHint: '支持 ⌘V 粘贴图片', - attSection: '附件', attDownload: '下载', attDeleteConfirm: '删除附件「{name}」?此操作不可恢复。', attDeleted: '附件已删除', attDeleteFailed: '删除附件失败:', + attSection: '附件', attDownload: '下载', attDeleteConfirm: '删除附件「{name}」?此操作不可恢复。', attDeleted: '附件已删除', attDeleteFailed: '删除附件失败:', attTooLarge: '单个文件超过 25MB,已跳过', depsWait: '待依赖', specLabel: '改动方案(SPEC)', opsLabel: '将执行的操作(OPERATIONS)', pendingDoc: '待 Claude Code 产出(经 MCP 写入并提交评审)', emptyTasks: '该项目暂无任务 —— 点「+ 新建任务」或经 MCP 创建', empty: '空', @@ -93,7 +93,7 @@ window.MAESTRO_I18N = { complexity: 'Complexity', cplxAuto: 'AUTO', parentTask: 'Parent', topLevel: '(top level)', priority: 'Priority', p0: 'P0 · high', p1: 'P1 · medium', p2: 'P2 · low', create: 'Create task', cancel: 'Cancel', attachLabel: 'Attachments · images/files (optional)', dropHint: 'Drop files here, or paste a screenshot', removeFile: 'Remove', pasteHint: '⌘V to paste images', - attSection: 'Attachments', attDownload: 'Download', attDeleteConfirm: 'Delete attachment "{name}"? This cannot be undone.', attDeleted: 'Attachment deleted', attDeleteFailed: 'Failed to delete attachment: ', + attSection: 'Attachments', attDownload: 'Download', attDeleteConfirm: 'Delete attachment "{name}"? This cannot be undone.', attDeleted: 'Attachment deleted', attDeleteFailed: 'Failed to delete attachment: ', attTooLarge: 'File exceeds 25MB and was skipped', depsWait: 'deps', specLabel: 'CHANGE SPEC', opsLabel: 'PLANNED OPERATIONS', pendingDoc: 'Awaiting Claude Code output (written via MCP, then submitted for review)', emptyTasks: 'No tasks in this project — hit "+ New task" or create via MCP', empty: 'EMPTY', @@ -145,7 +145,7 @@ window.MAESTRO_I18N = { complexity: 'Complejidad', cplxAuto: 'AUTO', parentTask: 'Padre', topLevel: '(raíz)', priority: 'Prioridad', p0: 'P0 · alta', p1: 'P1 · media', p2: 'P2 · baja', create: 'Crear tarea', cancel: 'Cancelar', attachLabel: 'Adjuntos · imágenes/archivos (opcional)', dropHint: 'Suelta archivos aquí o pega una captura', removeFile: 'Quitar', pasteHint: '⌘V para pegar imágenes', - attSection: 'Adjuntos', attDownload: 'Descargar', attDeleteConfirm: '¿Eliminar el adjunto «{name}»? Esta acción no se puede deshacer.', attDeleted: 'Adjunto eliminado', attDeleteFailed: 'Error al eliminar el adjunto: ', + attSection: 'Adjuntos', attDownload: 'Descargar', attDeleteConfirm: '¿Eliminar el adjunto «{name}»? Esta acción no se puede deshacer.', attDeleted: 'Adjunto eliminado', attDeleteFailed: 'Error al eliminar el adjunto: ', attTooLarge: 'El archivo supera los 25MB y se omitió', depsWait: 'deps', specLabel: 'SPEC DE CAMBIOS', opsLabel: 'OPERACIONES PREVISTAS', pendingDoc: 'Esperando salida de Claude Code (vía MCP, luego a revisión)', emptyTasks: 'Sin tareas — pulsa "+ Nueva tarea" o crea vía MCP', empty: 'VACÍO', @@ -197,7 +197,7 @@ window.MAESTRO_I18N = { complexity: '複雑度', cplxAuto: '智能', parentTask: '親タスク', topLevel: '(トップ)', priority: '優先度', p0: 'P0 · 高', p1: 'P1 · 中', p2: 'P2 · 低', create: 'タスク作成', cancel: 'キャンセル', attachLabel: '添付 · 画像/ファイル(任意)', dropHint: 'ここにファイルをドロップ、または貼り付け', removeFile: '削除', pasteHint: '⌘Vで画像を貼り付け', - attSection: '添付', attDownload: 'ダウンロード', attDeleteConfirm: '添付「{name}」を削除しますか?この操作は元に戻せません。', attDeleted: '添付を削除しました', attDeleteFailed: '添付の削除に失敗:', + attSection: '添付', attDownload: 'ダウンロード', attDeleteConfirm: '添付「{name}」を削除しますか?この操作は元に戻せません。', attDeleted: '添付を削除しました', attDeleteFailed: '添付の削除に失敗:', attTooLarge: 'ファイルが25MBを超えたためスキップしました', depsWait: '依存待ち', specLabel: 'SPEC · 変更案', opsLabel: 'OPERATIONS · 操作', pendingDoc: 'Claude Code の出力待ち(MCP 経由で書き込み後レビューへ)', emptyTasks: 'タスクなし — 「+ 新規タスク」または MCP で作成', empty: '空', @@ -249,7 +249,7 @@ window.MAESTRO_I18N = { complexity: 'Complexité', cplxAuto: 'AUTO', parentTask: 'Parent', topLevel: '(racine)', priority: 'Priorité', p0: 'P0 · haute', p1: 'P1 · moyenne', p2: 'P2 · basse', create: 'Créer la tâche', cancel: 'Annuler', attachLabel: 'Pièces jointes · images/fichiers (facultatif)', dropHint: 'Déposez des fichiers ici ou collez une capture', removeFile: 'Retirer', pasteHint: '⌘V pour coller des images', - attSection: 'Pièces jointes', attDownload: 'Télécharger', attDeleteConfirm: 'Supprimer la pièce jointe « {name} » ? Action irréversible.', attDeleted: 'Pièce jointe supprimée', attDeleteFailed: 'Échec de la suppression : ', + attSection: 'Pièces jointes', attDownload: 'Télécharger', attDeleteConfirm: 'Supprimer la pièce jointe « {name} » ? Action irréversible.', attDeleted: 'Pièce jointe supprimée', attDeleteFailed: 'Échec de la suppression : ', attTooLarge: 'Le fichier dépasse 25 Mo et a été ignoré', depsWait: 'deps', specLabel: 'SPEC DES CHANGEMENTS', opsLabel: 'OPÉRATIONS PRÉVUES', pendingDoc: 'En attente de la sortie de Claude Code (via MCP, puis revue)', emptyTasks: 'Aucune tâche — « + Nouvelle tâche » ou via MCP', empty: 'VIDE',