From 8d3192e7cb35d681752d70ee27fbb40fc5618d8e Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Tue, 30 Jun 2026 07:42:46 +0800 Subject: [PATCH] =?UTF-8?q?app=E6=8E=A5=E7=BA=BF:=20=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E5=8E=BB=E9=87=8D=E5=93=8D=E5=BA=94/=E5=B7=B2=E5=AD=98?= =?UTF-8?q?=E9=99=84=E4=BB=B6=E5=88=A0=E9=99=A4/=E7=BC=A9=E7=95=A5?= =?UTF-8?q?=E5=9B=BE=E7=99=BD=E5=90=8D=E5=8D=95=E5=AF=B9=E9=BD=90=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=20(tsk=5FsTHRFtFoHf-G)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - createTask 消费后端去重后的权威 attachments 列表,按入库数报数并提示去重数 - 附件删除 toast 语义由 warn 调整为 ok(attDeleted 为成功语义) - TaskDetail 缩略图 isImg 收敛到 KIT_ATT_SAFE_INLINE 硬白名单(png/jpeg/gif/webp), 与后端 src/api/server.ts SAFE_INLINE 一字不差,svg/html 不走 inline, 钉死同源存储型 XSS - i18n 新增 attUploaded/attDeduped 两个带 {n} 占位的 key,覆盖 zh/en/es/ja/fr Co-Authored-By: Claude Opus 4.8 --- app/src/app.jsx | 11 ++++++++--- design/ui_kits/console/TaskTree.jsx | 6 +++++- design/ui_kits/console/i18n.js | 10 +++++----- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/app/src/app.jsx b/app/src/app.jsx index 1f1950e..3c377d5 100644 --- a/app/src/app.jsx +++ b/app/src/app.jsx @@ -350,8 +350,13 @@ export function App() { try { const task = await api.createTask(currentId, body); if (files && files.length) { - try { await api.uploadAttachments(task.id, files); toast('ok', `附件已上传 · ${files.length} 个`); } - catch (e) { toast('warn', e.message); } + try { + // 消费后端去重后的权威列表:新建任务原本无附件,stored 即本批去重后入库条数 + const { attachments } = await api.uploadAttachments(task.id, files); + const stored = attachments.length; + const deduped = files.length - stored; // >0 表示有同内容文件被去重 + toast('ok', t.attUploaded.replace('{n}', stored) + (deduped > 0 ? t.attDeduped.replace('{n}', deduped) : '')); + } catch (e) { toast('warn', e.message); } } toast('ok', t.toastCreated); loadProject(currentId); loadGlobal(); @@ -389,7 +394,7 @@ export function App() { }, // 删除已建任务的单个附件(name = 磁盘文件名 basename(att.path)),成功后局部刷新 delAttachment: async (taskId, name) => { - try { await api.deleteAttachment(taskId, name); toast('warn', t.attDeleted); loadProject(currentId); } + try { await api.deleteAttachment(taskId, name); toast('ok', t.attDeleted); loadProject(currentId); } catch (e) { toast('warn', t.attDeleteFailed + e.message); } }, }; diff --git a/design/ui_kits/console/TaskTree.jsx b/design/ui_kits/console/TaskTree.jsx index 27e1455..e65e49f 100644 --- a/design/ui_kits/console/TaskTree.jsx +++ b/design/ui_kits/console/TaskTree.jsx @@ -9,6 +9,10 @@ const KIT_STATUS_ORDER = [ ]; const KIT_CPLX_OPTS = [{ value: 'hard', label: 'HARD' }, { value: 'medium', label: 'MED' }, { value: 'easy', label: 'EASY' }]; const KIT_PRIO_OPTS = [{ value: 'P0', label: 'P0' }, { value: 'P1', label: 'P1' }, { value: 'P2', label: 'P2' }]; +// 与后端 src/api/server.ts 的 SAFE_INLINE 一字不差:只有这些类型后端才 inline 回原图, +// 其余(尤其 image/svg+xml、text/html)强制 attachment+octet-stream 下载。前端缩略图必须对齐同一硬白名单, +// 否则对 svg 用 会拿到 octet-stream 出坏图,且策略漂移时埋同源存储型 XSS 隐患。 +const KIT_ATT_SAFE_INLINE = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp']); // 主显示区各区块标题通用折叠按钮(▾ 展开 / ▸ 折叠);暴露到 window 供 Agent/审批闸/归档复用 function KitFoldBtn({ folded, onClick, t }) { @@ -292,7 +296,7 @@ function TaskDetail({ task, t, byId, onJump, onTakeover, taskOps }) {
{attachments.map((a) => { const dn = attDiskName(a); - const isImg = (a.type || '').startsWith('image/'); + const isImg = KIT_ATT_SAFE_INLINE.has(a.type || ''); const url = attUrl(task.id, dn); return (
diff --git a/design/ui_kits/console/i18n.js b/design/ui_kits/console/i18n.js index d62eb8a..6b5d199 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: '删除附件失败:', attTooLarge: '单个文件超过 25MB,已跳过', + attSection: '附件', attDownload: '下载', attDeleteConfirm: '删除附件「{name}」?此操作不可恢复。', attDeleted: '附件已删除', attDeleteFailed: '删除附件失败:', attTooLarge: '单个文件超过 25MB,已跳过', attUploaded: '附件已上传 · {n} 个', attDeduped: '(去重 {n} 个)', 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: ', attTooLarge: 'File exceeds 25MB and was skipped', + 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', attUploaded: 'Attachments uploaded · {n}', attDeduped: ' ({n} duplicates 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: ', attTooLarge: 'El archivo supera los 25MB y se omitió', + 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ó', attUploaded: 'Adjuntos subidos · {n}', attDeduped: ' ({n} duplicados omitidos)', 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: '添付の削除に失敗:', attTooLarge: 'ファイルが25MBを超えたためスキップしました', + attSection: '添付', attDownload: 'ダウンロード', attDeleteConfirm: '添付「{name}」を削除しますか?この操作は元に戻せません。', attDeleted: '添付を削除しました', attDeleteFailed: '添付の削除に失敗:', attTooLarge: 'ファイルが25MBを超えたためスキップしました', attUploaded: '添付をアップロード · {n} 件', attDeduped: '(重複 {n} 件を除外)', 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 : ', attTooLarge: 'Le fichier dépasse 25 Mo et a été ignoré', + 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é', attUploaded: 'Pièces jointes envoyées · {n}', attDeduped: ' ({n} doublons ignorés)', 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',