diff --git a/docs/复刻待办清单-2026-10-17.md b/docs/复刻待办清单-2026-10-17.md index bf5902a..0a5c80d 100644 --- a/docs/复刻待办清单-2026-10-17.md +++ b/docs/复刻待办清单-2026-10-17.md @@ -452,11 +452,32 @@ ### 15. 智能体页面:团队管理 -- [ ] 拆解原版智能体设置页面,本轮仅对接“智能体团队管理”。 -- [ ] 根据 REA 证据梳理团队列表、详情及管理操作,将需要的真实存储/配置链路接入。 -- [ ] 验收:仅展示已确定范围;管理操作实际生效、重启后可恢复;团队管理与真实子代理调度的完成度分别记录。 +- [x] 拆解原版智能体设置页面,本轮仅对接“智能体团队管理”。 +- [x] 根据 REA 证据确认本页只有三个设置区块,没有团队列表、创建、详情或成员管理;接入“开启 / 禁用”偏好的真实存储与重启恢复链路。 +- [x] 验收:仅展示原版已确定的三个区块;团队管理偏好实际持久化、重启后可恢复;团队管理与真实子代理调度分别记录。 -状态:待 REA 拆解;范围限定为团队管理。 +状态:已实现并通过真实窗口专项验收。团队管理偏好已持久化;多智能体自动拆分执行器未接入。 + +原版证据:WorkBuddy 5.8.3,设置窗口 900×680。REA Evidence `ev_bd0f4b5dac7d4e2b89b1b050db1896c65e03b4fd1c3d3d986a8958fd9c1a9883`(浅色布局、DOM、computed style 与 CSS 源)、`ev_3384aa03af8a1ccdffb8e025dbd4a354636d8d6b0c207abd21e12fc5eb2ae719`(深色)、`ev_01940b7925326ee8e7f7e4aba661be2a17a2f876d634d4d370d01b6616a4a7fd`(团队管理下拉只含“开启 / 禁用”)、`ev_1cc896e04c0cc9ceab39b0a252f4982c019305609195ad44804ae9b5fb7379a8`(最终浅色顶部)、`ev_9542a063514b1a980665e504a89126c7a17f00b42f0c6dff9cead085a929f5c7`(三枚浏览器扩展图标媒体字节)。页面根 `638×503 @232,40`,三个区块依次为智能体团队管理、经济模式、浏览器操控(Beta);浏览器卡片 `638×261`,扩展列表 `606×193`,三行高 `68/68/56`。经济模式开启轨道浅色 `rgb(0,194,154)`、深色 `rgb(34,203,168)`。独立懒加载 CSS 为 `ExtensionsSettingsPanel-BnsXh3oa.css`,sha256 `cbd4b37c6061381943d96ca454176b2f083e49eb8e04c34cb5d31eef3dbcb8f5`。浏览器图标分别为 Chrome SVG `bde4af5a…`、Edge SVG `968cf9d8…`、QQ PNG `186c8282…`。观察范围限当前设置窗口;本页没有团队列表、团队详情、创建或成员管理,未据此推断其他页面。 + +实现: + +- `src/shared/types.ts`、`src/shared/ipc.ts`、`src/preload/index.ts`:新增 `AgentSettings { teamManagement }` 与 `window.ycode.agents.get/update` 契约。 +- `src/main/config.ts`、`src/main/agent-settings.ts`、`src/main/ipc.ts`:默认开启,写入 `config.json.agentSettings.teamManagement`;`agents:get/update` 返回归一化后的完整视图。 +- `src/renderer/src/components/AgentsSettingsPanel.tsx` + `source-agents-panels.css`:复刻三区块布局、团队下拉、经济模式 34×20 开关、Beta 徽标和三行浏览器扩展列表;团队管理下拉真实持久化,控件 title 明确“仅保存偏好;多智能体协作执行尚未接入”。 +- `source-agents-panels-recovered.css` + `scripts/rea/recover-agents-panels.mjs`:仅从 REA Evidence 回收并投影原版 `ExtensionsSettingsPanel-BnsXh3oa.css` 与引用闭包内的主题 token。 +- `scripts/rea/probe-todo15-browser-media.json` + `scripts/rea/recover-agents-browser-icons.mjs`:通过 REA 已加载 IMG 媒体选择器导出三枚真实浏览器图标,校验 Evidence 身份、MIME、SHA-256、可见性与资源字节后写入 `components/agents-assets/`;Chrome/Edge 为 SVG,QQ 为 128×128 PNG。 +- REA 媒体扩展新增已加载 SVG 的被动读取与签名校验,回归覆盖 SVG 导出、非法 MIME、字节预算和 Evidence 序列化;`test:rea-css` 已纳入 `media-assets-regression.mjs`。 +- `SettingsDialog.tsx`、`scripts/verify-settings-nav.cjs`:启用设置导航中的“智能体”并渲染面板。 + +验证:`scripts/verify-agents-settings.cjs` 在真实隔离 Electron + 原生设置窗口(900×680、真实 preload/IPC/文件系统)中 17 项断言全部通过:导航启用与选中;面板、三张卡片、主 section、下拉、开关、Beta、浏览器列表及按钮精确几何;原版文案;三枚图标均为已加载 `IMG`、36×36、自然尺寸有效且无占位标题;下拉仅“开启 / 禁用”并持久化为 `false`;关闭重开及 `config.json` 均恢复“禁用”;经济模式禁用且标注“暂未接入”,浅色/深色保持原版品牌绿轨道;浏览器安装入口禁用且标注“尚未配置”、无 ``;浅色/深色 surface 与 token;无渲染器错误。浅色和深色实测截图随报告保存在 `.tmp/agents-settings-OHo1QC/ycode-agents-light.png` / `ycode-agents-dark.png`,报告 `.tmp/agents-settings-OHo1QC/report.json`。回归串行执行 `npm run typecheck`、`npm run build`、`npm run test:rea-css`、`npm run test:chat`、`npm run test:conversation`、`npm run test:conversation-ipc`、`npm run test:personalization`、`node scripts/verify-settings-nav.cjs`、`node scripts/verify-shortcuts-settings.cjs`、`node scripts/verify-appearance-settings.cjs`、`node scripts/verify-general-settings.cjs`、`node scripts/verify-personalization-settings.cjs`、`node scripts/verify-about-help-settings.cjs`、`node scripts/verify-desktop-appearance.cjs`、`node scripts/verify-account-menu.cjs` 全部通过。 + +遗留: + +1. 团队管理开关目前只保存偏好,复杂任务自动拆分与多智能体协作执行尚未接入;按用户要求,本轮不处理子代理调度和云端服务。 +2. 经济模式只还原布局和禁用态,上下文自动压缩未接入,界面标注“暂未接入”。 +3. 浏览器操控的“安装 / 前往安装”只还原布局和禁用态,Chrome、Edge、QQ 扩展安装与外跳未接入,界面标注“尚未配置”。 +4. 视觉验收只覆盖当前 900×680 设置窗口;未覆盖其他窗口尺寸、扩展安装后的状态、真实团队列表或其他云端团队页面。 ### 16. 移除助理与设备连接 @@ -589,7 +610,7 @@ ## 推进顺序 1. 对话与侧栏:01 → 02 → 03 → 04 → 05 → 06 → 07 → 08 → 09 → 21(均已实现并留下验收记录,未完成验收点在各条目内标注)。 -2. 设置:16、17 的入口范围与 10(通用)均已落实并验证;接着依次推进 11 → 12 → 13 → 15 → 19。 +2. 设置:16、17 的入口范围与 10(通用)均已落实并验证;11(外观)、12(快捷键)、13(个性化)、19(关于与获取帮助)、15(智能体)均已实现并完成专项验收。 3. 14、18、20 保留为待拆解,后续单独展开。 ## 每项交付记录模板 diff --git a/package.json b/package.json index 1a9c636..4845f09 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "rea:css:prepare": "node scripts/rea/prepare-css-runtime.mjs", "rea:css:capture": "node scripts/rea/capture-css.mjs", "recover:css-reference": "node scripts/rea/export-css-reference.mjs", - "test:rea-css": "node scripts/rea/css-capture-regression.mjs && node scripts/rea/css-runtime-contract-regression.mjs && node scripts/rea/visual-capture-regression.mjs && node scripts/rea/visual-probe-regression.mjs && node scripts/rea/visual-probe-budget-regression.mjs && node scripts/rea/selected-artifact-text-regression.mjs && node scripts/rea/css-mcp-regression.mjs && node scripts/rea/css-runtime-integrity.mjs", + "test:rea-css": "node scripts/rea/css-capture-regression.mjs && node scripts/rea/css-runtime-contract-regression.mjs && node scripts/rea/visual-capture-regression.mjs && node scripts/rea/visual-probe-regression.mjs && node scripts/rea/visual-probe-budget-regression.mjs && node scripts/rea/media-assets-regression.mjs && node scripts/rea/selected-artifact-text-regression.mjs && node scripts/rea/css-mcp-regression.mjs && node scripts/rea/css-runtime-integrity.mjs", "test:rea-visual-live": "node scripts/rea/visual-probe-live-regression.mjs", "test:visual-pixels": "node scripts/rea/compare-visual-pixels.mjs --self-test", "test:font-platform": "node scripts/rea/capture-ycode-font-platform-fixture.mjs && node scripts/rea/verify-font-platform-parity.mjs .tmp/ycode-font-platform-selftest-reference.json .tmp/ycode-font-platform-selftest-result.json", diff --git a/scripts/rea/media-assets-regression.mjs b/scripts/rea/media-assets-regression.mjs index e4093c7..5be15ba 100644 --- a/scripts/rea/media-assets-regression.mjs +++ b/scripts/rea/media-assets-regression.mjs @@ -19,6 +19,8 @@ try { const main={frame_id:'main',file_path:await fs.realpath(html)},frame={id:'main',url:page} const png='iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jKXcAAAAASUVORK5CYII=' const bytes=Buffer.from(png,'base64'),digest=crypto.createHash('sha256').update(bytes).digest('hex') + const svgBytes=Buffer.from('') + const svgContent=svgBytes.toString('base64'),svgDigest=crypto.createHash('sha256').update(svgBytes).digest('hex') const makeSnapshot=()=>({strings:[page,'main','HTML','IMG','class','selected','src','fallback.png',resourceUrl,'contenteditable','true','14px'],documents:[{ documentURL:0,frameId:1,scrollOffsetX:0,scrollOffsetY:0, nodes:{nodeName:[2,3],parentIndex:[-1,0],backendNodeId:[10,20],attributes:[[],[4,5,6,7]],currentSourceURL:{index:[1],value:[8]}}, @@ -34,10 +36,10 @@ try { if(method==='DOM.describeNode')return {node:{nodeName:mode==='not-img'?'DIV':'IMG',backendNodeId:mode==='foreign-backend'?999:20}} if(method==='Page.getLayoutMetrics')return {cssLayoutViewport:{clientWidth:100,clientHeight:80,pageX:0,pageY:0}} if(method==='DOM.getNodeForLocation')return {backendNodeId:mode==='covered'?999:mode==='partly-covered'&¶ms.y>20?999:20} - if(method==='Page.getResourceTree')return {frameTree:{frame,resources:mode==='not-loaded'?[]:[{url:selectedResourceUrl,type:'Image',mimeType:mode==='svg'?'image/svg+xml':'image/png',...(mode==='declared-oversize'?{contentSize:2097153}:{})}],childFrames:[{frame:{id:'foreign',url:'https://foreign.invalid'},resources:[{url:resourceUrl,type:'Image',mimeType:'image/png'}]}]}} + if(method==='Page.getResourceTree')return {frameTree:{frame,resources:mode==='not-loaded'?[]:[{url:selectedResourceUrl,type:'Image',mimeType:mode==='svg'?'image/svg+xml':mode==='unsupported'?'image/bmp':'image/png',...(mode==='declared-oversize'?{contentSize:2097153}:{})}],childFrames:[{frame:{id:'foreign',url:'https://foreign.invalid'},resources:[{url:resourceUrl,type:'Image',mimeType:'image/png'}]}]}} if(method==='Page.getResourceContent'){ if(mode==='content-unavailable')throw Error('private-url-or-cookie-must-not-leak') - return {base64Encoded:mode!=='not-binary',content:mode==='invalid-base64'?'not-base64':mode==='signature'?'YWJj':mode==='oversize'?Buffer.alloc(2097153).toString('base64'):png} + return {base64Encoded:mode!=='not-binary',content:mode==='invalid-base64'?'not-base64':mode==='signature'?'YWJj':mode==='oversize'?Buffer.alloc(2097153).toString('base64'):mode==='svg'?svgContent:png} } if(method==='DOMSnapshot.captureSnapshot'){const copy=structuredClone(snapshot);for(const doc of copy.documents)doc.layout.styles=doc.layout.nodeIndex.map(()=>params.computedStyles.map(()=>11));return copy} if(method==='Page.captureScreenshot')return {data:png} @@ -58,12 +60,17 @@ try { assert.equal(result.total_bytes,bytes.length) assert.equal(calls.find(c=>c.method==='Page.getResourceContent').params.url,resourceUrl,'currentSrc wins over the fallback src attribute') assert.equal(result.items[0].backend_node_id,20) + mode='svg';result=await capture() + assert.equal(result.items[0].status,'exported') + assert.equal(result.items[0].artifact.mime_type,'image/svg+xml') + assert.equal(result.items[0].artifact.sha256,svgDigest) + assert.equal(result.items[0].artifact.data_base64,svgContent) mode='covered';result=await capture();assert.equal(result.items[0].status,'exported');assert.equal(result.items[0].visibility.status,'covered');assert.equal(result.items[0].selected_node.current_source_url,resourceUrl); mode='partly-covered';assert.equal((await capture()).items[0].status,'exported','partial occlusion may still export actual loaded full image bytes') - for(const [test,reason] of [['absent','selector_absent'],['ambiguous','selector_ambiguous'],['not-img','selected_node_not_img'],['foreign-backend','snapshot_identity_unavailable'],['not-loaded','selected_source_not_loaded_in_main_frame'],['svg','unsupported_image_type'],['declared-oversize','asset_byte_budget_exceeded'],['oversize','asset_byte_budget_exceeded'],['signature','image_signature_mismatch'],['invalid-base64','invalid_base64'],['not-binary','binary_content_unavailable'],['content-unavailable','protocol_resource_unavailable'],['navigation-after-content','frame_changed']]){ + for(const [test,reason] of [['absent','selector_absent'],['ambiguous','selector_ambiguous'],['not-img','selected_node_not_img'],['foreign-backend','snapshot_identity_unavailable'],['not-loaded','selected_source_not_loaded_in_main_frame'],['unsupported','unsupported_image_type'],['declared-oversize','asset_byte_budget_exceeded'],['oversize','asset_byte_budget_exceeded'],['signature','image_signature_mismatch'],['invalid-base64','invalid_base64'],['not-binary','binary_content_unavailable'],['content-unavailable','protocol_resource_unavailable'],['navigation-after-content','frame_changed']]){ mode=test;result=await capture();assert.equal(result.items[0].status,'unknown',test);assert.equal(result.items[0].reason,reason,test);assert.equal(result.total_bytes,0,test) assert.equal(result.items[0].artifact,undefined,test) - if(['not-loaded','svg','declared-oversize'].includes(test))assert.equal(calls.some(c=>c.method==='Page.getResourceContent'),false,test+' must reject before the read') + if(['not-loaded','unsupported','declared-oversize'].includes(test))assert.equal(calls.some(c=>c.method==='Page.getResourceContent'),false,test+' must reject before the read') assert.equal(JSON.stringify(result).includes('private-url-or-cookie'),false) } mode='ok';snapshot=makeSnapshot();snapshot.documents[0].nodes.attributes[0]=[9,10] @@ -98,5 +105,5 @@ try { assert.equal(optedOut.visuals.media_assets,undefined) assert.equal(JSON.stringify(optedOut.visuals).includes('fallback.png'),false) assert.equal(listener,undefined) - console.log('REA selected IMG resource regression passed: opt-in/schema/Evidence, snapshot currentSrc/backend attribution, covered bytes with separate visibility, scope, binary budgets/signatures, URL credential redaction, unknowns and default URL omission') + console.log('REA selected IMG resource regression passed: opt-in/schema/Evidence, snapshot currentSrc/backend attribution, raster/SVG export, covered bytes with separate visibility, scope, binary budgets/signatures, URL credential redaction, unknowns and default URL omission') }finally{await fs.rm(temporary,{recursive:true,force:true})} diff --git a/scripts/rea/patches/CdpElectronMediaAssets.js b/scripts/rea/patches/CdpElectronMediaAssets.js index d7dda00..481e11c 100644 --- a/scripts/rea/patches/CdpElectronMediaAssets.js +++ b/scripts/rea/patches/CdpElectronMediaAssets.js @@ -4,13 +4,15 @@ import { decodeCanonicalBase64 } from '../domain/webScreenshot.js'; const MAX_ASSETS=4, MAX_ASSET_BYTES=2*1024*1024, MAX_TOTAL_BYTES=8*1024*1024; const sha=bytes=>createHash('sha256').update(bytes).digest('hex'); -const types=new Set(['image/png','image/jpeg','image/gif','image/webp']); +const types=new Set(['image/png','image/jpeg','image/gif','image/webp','image/svg+xml']); const mime=value=>value==='image/jpg'?'image/jpeg':value; function sniff(bytes){ if(bytes.length>=24&&bytes.subarray(0,8).equals(Buffer.from([137,80,78,71,13,10,26,10])))return 'image/png'; if(bytes.length>=3&&bytes[0]===255&&bytes[1]===216&&bytes[2]===255)return 'image/jpeg'; if(bytes.length>=10&&/^GIF8[79]a$/.test(bytes.toString('ascii',0,6)))return 'image/gif'; if(bytes.length>=12&&bytes.toString('ascii',0,4)==='RIFF'&&bytes.toString('ascii',8,12)==='WEBP')return 'image/webp'; + const text=bytes.toString('utf8',0,Math.min(bytes.length,4096)).replace(/^\uFEFF/,'').trimStart(); + if(/^(?:<\?xml[\s\S]*?\?>\s*)?)/i.test(text))return 'image/svg+xml'; } function safeSourceUrl(value){ try{const u=new URL(value);if(!['file:','http:','https:'].includes(u.protocol))return null;u.username='';u.password='';u.search='';u.hash='';const result=u.href;return result.length<=2048?result:null;}catch{return null;} @@ -22,7 +24,7 @@ export async function captureElectronMediaAssets(snapshot,frames,selectors,send) const result={items:[],total_bytes:0,limits:{max_assets:MAX_ASSETS,max_asset_bytes:MAX_ASSET_BYTES,max_total_bytes:MAX_TOTAL_BYTES},limitations:[ 'Opt-in IMG resources only: exact unique selectors, authorized main file frame and snapshot backend identity. Visibility is reported separately and does not authorize new requests.', 'Only Page.getResourceTree resources already loaded by the selected frame are read with Page.getResourceContent. No HTTP fetch, cookies, headers, input values or renderer JavaScript are used.', - 'PNG/JPEG/GIF/WebP only; SVG, canvas, video, CSS backgrounds and resources missing from the loaded frame tree remain unknown. Covered loaded images can be exported; bytes do not establish that this image is the currently visible layer.', + 'PNG/JPEG/GIF/WebP/SVG only; canvas, video, CSS backgrounds and resources missing from the loaded frame tree remain unknown. Covered loaded images can be exported; bytes do not establish that this image is the currently visible layer.', 'Resource URLs in metadata omit credentials, query strings and fragments; URL SHA-256 identifies the exact selected URL. DOM and resource observations are sequential.' ]}; const main=frames[0],strings=snapshot.strings??[],str=index=>strings[index]??''; diff --git a/scripts/rea/patches/ElectronVisualProbeSchema.js b/scripts/rea/patches/ElectronVisualProbeSchema.js index bda142d..009e13a 100644 --- a/scripts/rea/patches/ElectronVisualProbeSchema.js +++ b/scripts/rea/patches/ElectronVisualProbeSchema.js @@ -32,7 +32,7 @@ const mediaIdentity = { }; export const electronMediaAssetsSchema = z.object({ items:z.array(z.discriminatedUnion('status',[ - z.object({...mediaIdentity,status:z.literal('exported'),artifact:z.object({mime_type:z.enum(['image/png','image/jpeg','image/gif','image/webp']),bytes:z.number().int().positive().max(2097152),sha256:z.string().regex(/^[a-f0-9]{64}$/),data_base64:z.string().max(2796204)})}), + z.object({...mediaIdentity,status:z.literal('exported'),artifact:z.object({mime_type:z.enum(['image/png','image/jpeg','image/gif','image/webp','image/svg+xml']),bytes:z.number().int().positive().max(2097152),sha256:z.string().regex(/^[a-f0-9]{64}$/),data_base64:z.string().max(2796204)})}), z.object({...mediaIdentity,status:z.literal('unknown'),reason:z.string()}) ])).max(4), total_bytes:z.number().int().min(0).max(8388608), diff --git a/scripts/rea/prepare-css-runtime.mjs b/scripts/rea/prepare-css-runtime.mjs index 2c3b24e..ad69f0c 100644 --- a/scripts/rea/prepare-css-runtime.mjs +++ b/scripts/rea/prepare-css-runtime.mjs @@ -135,7 +135,7 @@ await patch('dist/cli/electronCommands.js', text => once(text, ' include_script_sources: options.includeScriptSources,\n include_style_sources: options.includeStyleSources,\n include_visuals: options.includeVisuals,\n ...(options.visualProbe ? {visual_probe: JSON.parse(options.visualProbe)} : {}),')) await patch('dist/contracts/javascript/electronToolContracts.js', text => once(text, 'Returns frames, DOM structure, resource paths, and scripts without evaluating renderer JavaScript or invoking Electron APIs.', - 'Returns frames, DOM structure, resource paths, scripts and optional attributed CSS sources without evaluating renderer JavaScript or invoking Electron APIs. Default inspection is passive. include_style_sources selects CSS text; include_visuals selects viewport PNG and layout. Optional visual_probe declares native hover/click/scroll/key/viewport actions, sequential PNG samples, and actual platform fonts; UI actions can change application state and restoration is limited. visual_probe.media_selectors explicitly selects at most four unique main-frame IMG nodes and their already loaded PNG/JPEG/GIF/WebP resources, with byte budgets, SHA/base64 and visibility reported separately; no HTTP fetch is performed. These are local REA extensions; inspect coverage, budgets, restoration and limitations.')) + 'Returns frames, DOM structure, resource paths, scripts and optional attributed CSS sources without evaluating renderer JavaScript or invoking Electron APIs. Default inspection is passive. include_style_sources selects CSS text; include_visuals selects viewport PNG and layout. Optional visual_probe declares native hover/click/scroll/key/viewport actions, sequential PNG samples, and actual platform fonts; UI actions can change application state and restoration is limited. visual_probe.media_selectors explicitly selects at most four unique main-frame IMG nodes and their already loaded PNG/JPEG/GIF/WebP/SVG resources, with byte budgets, SHA/base64 and visibility reported separately; no HTTP fetch is performed. These are local REA extensions; inspect coverage, budgets, restoration and limitations.')) const styleModule = await fs.readFile(path.join(patchRoot, 'CdpElectronStyles.js'), 'utf8') await fs.writeFile(path.join(output, 'dist/browser/CdpElectronStyles.js'), styleModule) diff --git a/scripts/rea/probe-todo15-browser-media.json b/scripts/rea/probe-todo15-browser-media.json new file mode 100644 index 0000000..4a00719 --- /dev/null +++ b/scripts/rea/probe-todo15-browser-media.json @@ -0,0 +1,12 @@ +{ + "include_fonts": false, + "include_message_content": false, + "media_selectors": [ + "#settings-browser-extension-chrome img", + "#settings-browser-extension-edge img", + "#settings-browser-extension-qq-browser img" + ], + "actions": [ + { "kind": "wait", "step_id": "observe-loaded-browser-extension-images", "duration_ms": 250, "frames": 1 } + ] +} diff --git a/scripts/rea/probe-todo15-navigation.json b/scripts/rea/probe-todo15-navigation.json new file mode 100644 index 0000000..4734d45 --- /dev/null +++ b/scripts/rea/probe-todo15-navigation.json @@ -0,0 +1,14 @@ +{ + "include_fonts": false, + "include_message_content": false, + "track_selectors": [ + ".settings-navigation", + ".settings-navigation__item", + ".settings-navigation__item--active", + ".settings-modal__panel" + ], + "actions": [ + { "kind": "scroll", "step_id": "reveal-function-group-navigation-labels", "selector": ".settings-navigation", "delta_y": 500, "frames": 1 }, + { "kind": "wait", "step_id": "confirm-agents-navigation-label-and-order", "duration_ms": 400, "frames": 1 } + ] +} diff --git a/scripts/rea/probe-todo15-open-agents-dark.json b/scripts/rea/probe-todo15-open-agents-dark.json new file mode 100644 index 0000000..651a902 --- /dev/null +++ b/scripts/rea/probe-todo15-open-agents-dark.json @@ -0,0 +1,14 @@ +{ + "include_fonts": false, + "include_message_content": false, + "track_selectors": [ + ".settings-navigation__item--active", + ".settings-modal__panel", + ".settings-navigation", + ".settings-navigation__item" + ], + "actions": [ + { "kind": "click", "step_id": "open-agents-by-observed-nav-index-7-in-dark-theme", "selector": ".settings-navigation__item", "index": 7, "frames": 1 }, + { "kind": "wait", "step_id": "verify-agents-dark-page-stable", "duration_ms": 800, "frames": 2 } + ] +} diff --git a/scripts/rea/probe-todo15-open-agents-light.json b/scripts/rea/probe-todo15-open-agents-light.json new file mode 100644 index 0000000..e26c3aa --- /dev/null +++ b/scripts/rea/probe-todo15-open-agents-light.json @@ -0,0 +1,14 @@ +{ + "include_fonts": false, + "include_message_content": false, + "track_selectors": [ + ".settings-navigation__item--active", + ".settings-modal__panel", + ".settings-navigation", + ".settings-navigation__item" + ], + "actions": [ + { "kind": "click", "step_id": "open-agents-by-observed-nav-index-7", "selector": ".settings-navigation__item", "index": 7, "frames": 1 }, + { "kind": "wait", "step_id": "verify-agents-page-stable", "duration_ms": 800, "frames": 4 } + ] +} diff --git a/scripts/rea/probe-todo15-open-appearance-dark.json b/scripts/rea/probe-todo15-open-appearance-dark.json new file mode 100644 index 0000000..76b2322 --- /dev/null +++ b/scripts/rea/probe-todo15-open-appearance-dark.json @@ -0,0 +1,14 @@ +{ + "include_fonts": false, + "include_message_content": false, + "track_selectors": [ + ".settings-modal__panel", + ".settings-navigation__item--active", + ".appearance-card--light", + ".appearance-card--dark" + ], + "actions": [ + { "kind": "click", "step_id": "open-appearance-by-observed-nav-index-3-in-dark-theme", "selector": ".settings-navigation__item", "index": 3, "frames": 1 }, + { "kind": "wait", "step_id": "verify-dark-appearance-before-restoring-light", "duration_ms": 500, "frames": 1 } + ] +} diff --git a/scripts/rea/probe-todo15-open-appearance-light.json b/scripts/rea/probe-todo15-open-appearance-light.json new file mode 100644 index 0000000..5a05e20 --- /dev/null +++ b/scripts/rea/probe-todo15-open-appearance-light.json @@ -0,0 +1,14 @@ +{ + "include_fonts": false, + "include_message_content": false, + "track_selectors": [ + ".settings-modal__panel", + ".settings-navigation__item--active", + ".appearance-card--light", + ".appearance-card--dark" + ], + "actions": [ + { "kind": "click", "step_id": "open-appearance-by-observed-nav-index-3", "selector": ".settings-navigation__item", "index": 3, "frames": 1 }, + { "kind": "wait", "step_id": "confirm-light-appearance-state", "duration_ms": 500, "frames": 1 } + ] +} diff --git a/scripts/rea/probe-todo15-restore-light.json b/scripts/rea/probe-todo15-restore-light.json new file mode 100644 index 0000000..ac5ffd7 --- /dev/null +++ b/scripts/rea/probe-todo15-restore-light.json @@ -0,0 +1,13 @@ +{ + "include_fonts": false, + "include_message_content": false, + "track_selectors": [ + ".settings-modal__panel", + ".appearance-card--light", + ".appearance-card--dark" + ], + "actions": [ + { "kind": "click", "step_id": "restore-light-theme-after-agent-settings-sample", "selector": ".appearance-card--light", "index": 0, "frames": 1 }, + { "kind": "wait", "step_id": "wait-for-light-theme-render", "duration_ms": 800, "frames": 1 } + ] +} diff --git a/scripts/rea/probe-todo15-scroll-and-return-top.json b/scripts/rea/probe-todo15-scroll-and-return-top.json new file mode 100644 index 0000000..7c0aaf9 --- /dev/null +++ b/scripts/rea/probe-todo15-scroll-and-return-top.json @@ -0,0 +1,16 @@ +{ + "include_fonts": false, + "include_message_content": false, + "track_selectors": [ + ".settings-modal__panel", + ".general-settings-panel.extensions-settings-panel", + "#settings-agent-teams", + "#settings-browser-control" + ], + "actions": [ + { "kind": "scroll", "step_id": "check-for-lower-agent-settings-content", "selector": ".settings-modal__panel", "delta_y": 700, "frames": 1 }, + { "kind": "wait", "step_id": "confirm-panel-content-end", "duration_ms": 250, "frames": 1 }, + { "kind": "scroll", "step_id": "return-agent-settings-to-top", "selector": ".settings-modal__panel", "delta_y": -700, "frames": 1 }, + { "kind": "wait", "step_id": "confirm-agent-settings-top", "duration_ms": 250, "frames": 1 } + ] +} diff --git a/scripts/rea/probe-todo15-select-dark.json b/scripts/rea/probe-todo15-select-dark.json new file mode 100644 index 0000000..db9c3c3 --- /dev/null +++ b/scripts/rea/probe-todo15-select-dark.json @@ -0,0 +1,14 @@ +{ + "include_fonts": false, + "include_message_content": false, + "track_selectors": [ + ".settings-modal__panel", + ".settings-navigation__item--active", + ".appearance-card--light", + ".appearance-card--dark" + ], + "actions": [ + { "kind": "click", "step_id": "select-dark-theme-for-agent-settings-sample", "selector": ".appearance-card--dark", "index": 0, "frames": 1 }, + { "kind": "wait", "step_id": "wait-for-dark-theme-render", "duration_ms": 800, "frames": 1 } + ] +} diff --git a/scripts/rea/probe-todo15-team-management-menu.json b/scripts/rea/probe-todo15-team-management-menu.json new file mode 100644 index 0000000..11bbb05 --- /dev/null +++ b/scripts/rea/probe-todo15-team-management-menu.json @@ -0,0 +1,15 @@ +{ + "include_fonts": false, + "include_message_content": false, + "track_selectors": [ + ".settings-modal__panel", + ".wb-select", + ".wb-select__value", + ".wb-select__option" + ], + "actions": [ + { "kind": "click", "step_id": "open-agent-team-setting-options-without-changing-value", "selector": ".wb-select", "index": 0, "frames": 1 }, + { "kind": "wait", "step_id": "observe-agent-team-setting-options", "duration_ms": 250, "frames": 1 }, + { "kind": "key", "step_id": "close-agent-team-setting-options-with-escape", "key": "Escape", "frames": 1 } + ] +} diff --git a/scripts/rea/recover-agents-browser-icons.mjs b/scripts/rea/recover-agents-browser-icons.mjs new file mode 100644 index 0000000..7e1e521 --- /dev/null +++ b/scripts/rea/recover-agents-browser-icons.mjs @@ -0,0 +1,73 @@ +// Recover only the three browser-extension images retained by REA Evidence. +// This script reads the archived Evidence JSON and never accesses the WorkBuddy install. +import { createHash } from 'node:crypto' +import fs from 'node:fs/promises' +import path from 'node:path' + +const evidenceId = 'ev_9542a063514b1a980665e504a89126c7a17f00b42f0c6dff9cead085a929f5c7' +const evidencePath = path.resolve(`.tmp/rea-css-evidence/${evidenceId}.json`) +const outputDirectory = path.resolve('src/renderer/src/components/agents-assets') +const expected = [ + { + selector: '#settings-browser-extension-chrome img', + mimeType: 'image/svg+xml', + sha256: 'bde4af5a5d1150d3ec3108c29a5d746a00892196f3eaf1db4c4dc99aac7ba266', + output: 'workbuddy-browser-chrome.svg' + }, + { + selector: '#settings-browser-extension-edge img', + mimeType: 'image/svg+xml', + sha256: '968cf9d827bfeaf47e4e53da55ec144bbc9565e5cb7a65cdb8dd0284fb3a5d11', + output: 'workbuddy-browser-edge.svg' + }, + { + selector: '#settings-browser-extension-qq-browser img', + mimeType: 'image/png', + sha256: '186c82828c04b5bfe300c201ecd75cefb7984d97ea01bd491e0cfbdeb5df15a1', + output: 'workbuddy-browser-qq.png' + } +] + +const digest = (bytes) => createHash('sha256').update(bytes).digest('hex') +const evidence = JSON.parse(await fs.readFile(evidencePath, 'utf8')) +if (evidence.evidence_id !== evidenceId) throw new Error(`REA identity mismatch: ${evidence.evidence_id}`) +if (evidence.completeness?.truncated_sections?.length) throw new Error('REA Evidence is truncated') +if (!/renderer[\\/]index\.html$/i.test(evidence.normalized_result?.target?.file_path ?? '')) { + throw new Error('REA Evidence target is not the authorized WorkBuddy renderer') +} + +const media = evidence.normalized_result?.visuals?.media_assets +if (!media?.items || media.items.length !== expected.length) throw new Error('Unexpected REA media item count') +const decoded = [] +for (const item of expected) { + const matches = media.items.filter((candidate) => candidate.selector === item.selector) + if (matches.length !== 1) throw new Error(`Expected one REA item for ${item.selector}`) + const asset = matches[0] + if (asset.status !== 'exported' || asset.visibility?.status !== 'observed') { + throw new Error(`REA did not export a visible image for ${item.selector}: ${asset.reason}`) + } + if (asset.artifact?.mime_type !== item.mimeType || asset.artifact.sha256 !== item.sha256) { + throw new Error(`REA artifact identity changed for ${item.selector}`) + } + const bytes = Buffer.from(asset.artifact.data_base64, 'base64') + if (bytes.length !== asset.artifact.bytes || digest(bytes) !== item.sha256) { + throw new Error(`REA artifact bytes changed for ${item.selector}`) + } + if (item.mimeType === 'image/svg+xml') { + const source = bytes.toString('utf8') + if (!/^ ({ + output, sha256, bytes: bytes.length +})) }, null, 2)) diff --git a/scripts/rea/recover-agents-panels.mjs b/scripts/rea/recover-agents-panels.mjs new file mode 100644 index 0000000..ac5e1a3 --- /dev/null +++ b/scripts/rea/recover-agents-panels.mjs @@ -0,0 +1,123 @@ +// Recover only retained REA CSS; never access the installed application here. +// Sources: REA light/dark Evidence for WorkBuddy 5.8.3 Settings -> Agents. +// The lazy module contains the browser-extension status rules for this page. +import fs from 'node:fs/promises' +import { createHash } from 'node:crypto' +import postcss from 'postcss' + +const sha = value => createHash('sha256').update(value).digest('hex') +const evidenceIds = { + light: 'ev_1cc896e04c0cc9ceab39b0a252f4982c019305609195ad44804ae9b5fb7379a8', + dark: 'ev_3384aa03af8a1ccdffb8e025dbd4a354636d8d6b0c207abd21e12fc5eb2ae719' +} +const expectedModuleSha = 'cbd4b37c6061381943d96ca454176b2f083e49eb8e04c34cb5d31eef3dbcb8f5' +const expectedTokensSha = '13f2bf82c96e5167a7d7098e36950c6c22c475b3337dbb8bb29ad2fe79cde696' +const moduleName = 'ExtensionsSettingsPanel-BnsXh3oa.css' + +const loadModule = async evidenceId => { + const evidence = JSON.parse(await fs.readFile(`.tmp/rea-css-evidence/${evidenceId}.json`, 'utf8')) + if (evidence.evidence_id !== evidenceId) throw new Error(`REA identity mismatch: ${evidenceId}`) + const module = evidence.normalized_result.styles.items.find(item => item.file_path.endsWith(moduleName)) + if (!module) throw new Error(`Agent settings module CSS is absent from ${evidenceId}`) + const moduleSha = sha(module.source.artifact.text) + if (moduleSha !== expectedModuleSha || module.source.artifact.sha256 !== expectedModuleSha) { + throw new Error(`Agent settings module CSS changed in ${evidenceId}`) + } + return { filePath: module.file_path, css: module.source.artifact.text, sha: moduleSha } +} + +const lightModule = await loadModule(evidenceIds.light) +const darkModule = await loadModule(evidenceIds.dark) +if (lightModule.filePath !== darkModule.filePath) throw new Error('Agent settings module path changed across captures') + +const tokens = await fs.readFile('.tmp/rea-css-recovered/tokens-Df-i-zsO.css', 'utf8') +const tokensSha = sha(tokens) +if (tokensSha !== expectedTokensSha) throw new Error('Tokens CSS changed') + +const lightTokens = new Map() +const darkTokens = new Map() +for (const rule of postcss.parse(tokens).nodes) { + if (rule.type !== 'rule') continue + const selectors = rule.selectors ?? [] + const dark = selectors.some(selector => selector.includes('[data-theme=dark]')) + const light = selectors.some(selector => selector.trim() === ':root') + if (!dark && !light) continue + for (const node of rule.nodes ?? []) { + if (node.type !== 'decl' || !node.prop.startsWith('--')) continue + if (dark) darkTokens.set(node.prop, node.value) + if (light) lightTokens.set(node.prop, node.value) + } +} + +const relatedSelector = /\.extensions-settings-panel__browser-extension-status(?:[\w-]*)?(?![\w-])/i +const darkScope = /^\s*(?:\[data-theme=["']?dark["']?\]|body\[data-vscode-theme-name=["']IDE Night["']\]|body\.vscode-dark|\.vscode-dark|body\.dark|\.dark)(?=\s|$)/i +function project(node, mode = 'all') { + if (node.type === 'rule') { + const selectors = [] + for (const selector of node.selectors) { + if (!relatedSelector.test(selector)) continue + const isDark = darkScope.test(selector) + if (mode === 'shared' && isDark) continue + if (mode === 'dark' && !isDark) continue + const mapped = isDark ? selector.replace(darkScope, '[data-theme=dark]') : selector + if (!selectors.includes(mapped)) selectors.push(mapped) + } + return selectors.length ? node.clone({ selector: selectors.join(',') }) : null + } + if (node.type === 'atrule' && node.nodes) { + const nodes = node.nodes.map(child => project(child, mode)).filter(Boolean) + return nodes.length ? node.clone({ nodes }) : null + } + return null +} + +const sharedRoot = css => postcss.root({ nodes: postcss.parse(css).nodes.map(node => project(node, 'shared')).filter(Boolean) }) +const lightRoot = sharedRoot(lightModule.css) +const darkSharedRoot = sharedRoot(darkModule.css) +const darkSpecificRoot = postcss.root({ nodes: postcss.parse(darkModule.css).nodes.map(node => project(node, 'dark')).filter(Boolean) }) +const normalizeRules = root => { + const rules = [] + root.walkRules(rule => { + const selectors = rule.selectors.map(selector => selector.replace(/\s+/g, ' ').trim()).join(',') + const declarations = (rule.nodes ?? []) + .filter(node => node.type === 'decl') + .map(node => `${node.prop}:${node.value.replace(/\b0px\b/g, '0').replace(/\s+/g, ' ').trim()}`) + .join(';') + rules.push(`${selectors}{${declarations}}`) + }) + return rules.join('\n') +} +if (normalizeRules(lightRoot) !== normalizeRules(darkSharedRoot)) throw new Error('Light/dark agent module rules differ beyond serialization') + +const root = postcss.root({ nodes: [...lightRoot.nodes, ...darkSpecificRoot.nodes] }) +const referenced = new Set() +const collect = text => { + for (const match of text.matchAll(/var\(\s*(--[\w-]+)/g)) referenced.add(match[1]) +} +collect(root.toString()) +for (let depth = 0; depth < 8; depth++) { + const before = referenced.size + for (const name of [...referenced]) { + for (const value of [lightTokens.get(name), darkTokens.get(name)]) { + if (typeof value === 'string') collect(value) + } + } + if (referenced.size === before) break +} + +const missingLight = [...referenced].filter(name => !lightTokens.has(name)) +if (missingLight.length) throw new Error(`Tokens missing from light root scope: ${missingLight.join(', ')}`) +const pick = map => new Map([...referenced].map(name => [name, map.get(name) ?? lightTokens.get(name)])) +const scope = (selector, map) => `${selector}{${[...map].map(([property, value]) => `${property}:${value}`).join(';')}}` +const scopedTokens = `${scope('.extensions-settings-panel', pick(lightTokens))}\n${scope('[data-theme=dark] .extensions-settings-panel', pick(darkTokens))}\n` +const evidenceList = Object.entries(evidenceIds).map(([name, evidenceId]) => `${name}=${evidenceId}`).join('; ') +const css = `/* REA ${evidenceList}; ${moduleName} sha256=${lightModule.sha}; tokens sha256=${tokensSha}. */\n${root.toString()}\n${scopedTokens}` +await fs.writeFile('src/renderer/src/components/source-agents-panels-recovered.css', css) + +const rules = [] +root.walkRules(rule => rules.push(rule.selector)) +const darkRules = [] +darkSpecificRoot.walkRules(rule => darkRules.push(rule.selector)) +console.log(`Recovered ${rules.length} agent settings rules (${Buffer.byteLength(css)} bytes); ${darkRules.length} dark-specific module rules; scoped ${pick(lightTokens).size} light / ${pick(darkTokens).size} dark tokens`) +console.log(rules.join('\n')) +if (rules.length !== 4) throw new Error(`Expected 4 agent settings module rules, found ${rules.length}`) diff --git a/scripts/verify-agents-settings.cjs b/scripts/verify-agents-settings.cjs new file mode 100644 index 0000000..8880e61 --- /dev/null +++ b/scripts/verify-agents-settings.cjs @@ -0,0 +1,243 @@ +// TODO 15 验收:设置 → 智能体的 1:1 布局 + 团队管理偏好真实持久化 + 未接入项标注 + 深浅主题。 +// 真实隔离 YCode(真实 preload/IPC/渲染进程/文件系统)+ 原生设置窗口,视口 900×680(与原版一致)。 +// +// 原版依据(REA ev_bd0f4b…/ev_3384aa…/ev_01940b…/ev_1cc896…): +// 页面根 [232,40,638,503];团队管理卡 638×90(section 606×66、下拉 120×32 @734,69); +// 经济模式卡 638×90(开关 34×20 @820,181);浏览器操控标签 638×22 + Beta 39.391×20 @314,253; +// 浏览器卡片 638×261(主 section 606×44、安装 68×32 @786,294、列表 606×193、行 68/68/56、按钮 102×32 @752)。 +const fs = require('node:fs/promises') +const path = require('node:path') +const assert = require('node:assert/strict') + +if (!process.versions.electron) { + ;(async () => { + const directory = await fs.mkdtemp(path.resolve('.tmp/agents-settings-')) + const data = path.join(directory, 'data'), home = path.join(directory, 'home') + await fs.mkdir(data, { recursive: true }) + await fs.mkdir(home, { recursive: true }) + const env = { ...process.env, YCODE_QA_DIRECTORY: directory, YCODE_USER_DATA_DIR: data, YCODE_OMP_HOME: home, + YCODE_DISABLE_GPU: '1', YCODE_DISABLE_SYSTEM_INTEGRATION: '1' } + delete env.ELECTRON_RUN_AS_NODE; delete env.ELECTRON_RENDERER_URL + const child = require('node:child_process').spawn(require('electron'), + ['--no-sandbox', '--disable-gpu-sandbox', __filename], { env, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] }) + let output = '' + child.stdout.on('data', bytes => { output += bytes; if (bytes.toString().includes('AGENTS_SETTINGS_')) process.stdout.write(bytes) }) + child.stderr.on('data', bytes => { output += bytes }) + const timer = setTimeout(() => child.kill(), 300000) + child.once('exit', async code => { + clearTimeout(timer) + await fs.writeFile(path.join(directory, 'process.log'), output) + console.log(JSON.stringify({ directory, exitCode: code })) + if (code) console.error(output.slice(-4000)) + process.exitCode = code ? 1 : 0 + }) + })().catch(error => { console.error(error); process.exitCode = 1 }) +} else { + const { app, BrowserWindow } = require('electron') + const { pathToFileURL } = require('node:url') + const directory = process.env.YCODE_QA_DIRECTORY + app.setPath('home', path.join(directory, 'home')) + const checks = [], snapshots = {}, rendererErrors = [] + let main, settings, phase = 'boot' + const pause = ms => new Promise(resolve => setTimeout(resolve, ms)) + async function read(source, win = main) { + let timer + try { + return await Promise.race([win.webContents.executeJavaScript(source), new Promise((_, reject) => { + timer = setTimeout(() => reject(Error('Renderer timeout: ' + source.slice(0, 120))), 15000) + })]) + } finally { clearTimeout(timer) } + } + async function wait(source, label, win = main) { + for (let index = 0; index < 300; index++) { const value = await read(source, win); if (value) return value; await pause(40) } + throw Error('Timeout: ' + label) + } + async function step(label) { phase = label; await fs.writeFile(path.join(directory, 'phase.txt'), label) } + function check(label, actual, expected) { + const pass = typeof expected === 'function' ? expected(actual) : JSON.stringify(actual) === JSON.stringify(expected) + checks.push({ label, pass, actual, expected: typeof expected === 'function' ? expected.toString() : expected }) + } + const near = (value, target, tolerance = 1) => Math.abs(value - target) <= tolerance + function observe(win) { + win.webContents.on('console-message', event => { if (event.level === 'error' || event.level >= 3) rendererErrors.push({ windowId: win.id, message: event.message }) }) + } + async function openSettings(section) { + await read(`window.ycode.app.openSettings(${JSON.stringify(section)})`) + for (let index = 0; index < 200; index++) { + settings = BrowserWindow.getAllWindows().find(win => win !== main) + if (settings && !settings.webContents.isLoading()) break + await pause(25) + } + assert.ok(settings, 'settings window') + await wait('!!document.querySelector(".settings-window-root")', 'settings root', settings) + await pause(400) + return settings + } + + const layoutSource = `(()=>{ + const round = value => Math.round(value * 1000) / 1000 + const rect = node => { if (!node) return null; const r = node.getBoundingClientRect() + return { x: round(r.x), y: round(r.y), width: round(r.width), height: round(r.height) } } + const panel = document.querySelector('.extensions-settings-panel') + const cards = [...document.querySelectorAll('.extensions-settings-panel > .general-settings-panel__group-card')] + const sections = [...document.querySelectorAll('.extensions-settings-panel .general-settings-panel__group-card > .general-settings-panel__section')] + const buttons = [...document.querySelectorAll('.extensions-settings-panel__action-button')] + const rows = [...document.querySelectorAll('.extensions-settings-panel__browser-row')] + const select = document.querySelector('.extensions-settings-panel__team-dropdown .wb-select') + const switchInput = document.querySelector('#agents-economy-switch, .extensions-settings-panel .wb-switch__input') + return { + panel: rect(panel), + cards: cards.map(card => rect(card)), + sections: sections.map(section => ({ + rect: rect(section), + title: section.querySelector('.general-settings-panel__section-title')?.textContent?.trim(), + desc: section.querySelector('.general-settings-panel__section-desc')?.textContent?.trim() + })), + select: select ? { rect: rect(select), value: select.querySelector('.wb-select__value')?.textContent?.trim(), title: select.getAttribute('title') } : null, + switch: switchInput ? (() => { const track = switchInput.closest('.wb-switch')?.querySelector('.wb-switch__track') + return { rect: rect(track), trackBackground: track ? getComputedStyle(track).backgroundColor : null, + trackOpacity: track ? getComputedStyle(track).opacity : null, thumb: rect(track?.querySelector('.wb-switch__thumb')), + checked: switchInput.checked, disabled: switchInput.disabled, + unavailable: switchInput.closest('.extensions-settings-panel__unavailable-control')?.getAttribute('title') } })() : null, + browserLabel: (() => { const node = document.querySelector('.extensions-settings-panel__browser-label'); return node ? { ...rect(node), text: node.firstChild?.textContent?.trim() } : null })(), + beta: (() => { const node = document.querySelector('.extensions-settings-panel__beta'); return node ? { ...rect(node), text: node.textContent.trim() } : null })(), + browserCard: rect(document.querySelector('.extensions-settings-panel__browser-card')), + browserList: rect(document.querySelector('.extensions-settings-panel__browser-list')), + rows: rows.map(row => ({ + rect: rect(row), + title: row.querySelector('.extensions-settings-panel__browser-title')?.textContent?.trim(), + desc: row.querySelector('.extensions-settings-panel__browser-desc')?.textContent?.trim(), + icon: (() => { const node = row.querySelector('.extensions-settings-panel__browser-icon') + return node ? { ...rect(node), tagName: node.tagName, src: node.getAttribute('src'), placeholderTitle: node.getAttribute('title'), + complete: node.complete === true, naturalWidth: node.naturalWidth, naturalHeight: node.naturalHeight } : null })(), + button: (() => { const node = row.querySelector('.extensions-settings-panel__action-button') + return node ? { rect: rect(node), text: node.textContent.trim(), ariaDisabled: node.getAttribute('aria-disabled'), title: node.getAttribute('title'), arrow: !!node.querySelector('.wb-button__icon svg') } : null })() + })), + buttons: buttons.map(button => ({ text: button.textContent.trim(), rect: rect(button), ariaDisabled: button.getAttribute('aria-disabled'), title: button.getAttribute('title') })), + anchors: [...document.querySelectorAll('a')].map(anchor => anchor.getAttribute('href')).filter(Boolean), + cardBackground: cards[0] ? getComputedStyle(cards[0]).backgroundColor : null, + titleColor: (() => { const node = document.querySelector('.extensions-settings-panel .general-settings-panel__section-title'); return node ? getComputedStyle(node).color : null })(), + descColor: (() => { const node = document.querySelector('.extensions-settings-panel .general-settings-panel__section-desc'); return node ? getComputedStyle(node).color : null })(), + selectBackground: select ? getComputedStyle(select).backgroundColor : null, + selectBorder: select ? getComputedStyle(select).borderTopColor : null, + buttonBackground: buttons[0] ? getComputedStyle(buttons[0]).backgroundColor : null, + buttonBorder: buttons[0] ? getComputedStyle(buttons[0]).borderTopColor : null, + betaBackground: (() => { const node = document.querySelector('.extensions-settings-panel__beta'); return node ? getComputedStyle(node).backgroundColor : null })(), + theme: document.documentElement.dataset.theme ?? 'light' + }})()` + + ;(async () => { + await import(pathToFileURL(path.resolve('out/main/index.mjs')).href); await app.whenReady() + for (let index = 0; index < 200; index++) { + main = BrowserWindow.getAllWindows().find(win => win.getTitle() === 'YCode') + if (main && !main.webContents.isLoading()) break + await pause(25) + } + assert.ok(main, 'main window'); observe(main) + await wait('!!window.ycode', 'preload api') + + await step('agents-settings') + await openSettings('extensions'); observe(settings) + await wait('!!document.querySelector(".extensions-settings-panel")', 'agents panel', settings) + const nav = await read(`(()=>{const item=document.querySelector('[data-settings-tab="extensions"]') + return { exists: !!item, disabled: item?.getAttribute('aria-disabled'), active: document.querySelector('.settings-navigation__item--active')?.dataset.settingsTab }})()`, settings) + check('智能体 nav entry is enabled and active', [nav.exists, nav.disabled !== 'true', nav.active], [true, true, 'extensions']) + + const light = await read(layoutSource, settings) + snapshots.light = light + await fs.writeFile(path.join(directory, 'ycode-agents-light.png'), (await settings.webContents.capturePage()).toPNG()) + check('agents panel keeps the recovered 638x503 box at 232,40', + [near(light.panel.x, 232), near(light.panel.y, 40), near(light.panel.width, 638), near(light.panel.height, 503)], [true, true, true, true]) + check('team-management and economy cards keep 638x90 with 606x66 sections', + [light.cards.slice(0, 2).map(card => [near(card.width, 638), near(card.height, 90)].every(Boolean)), + light.sections.slice(0, 2).map(section => [near(section.rect.width, 606), near(section.rect.height, 66)].every(Boolean)), + near(light.cards[0].y, 40), near(light.cards[1].y, 146)], [[true, true], [true, true], true, true]) + check('team-management keeps the recovered title, description and 120x32 select', + [light.sections[0].title, light.sections[0].desc, + near(light.select.rect.x, 734), near(light.select.rect.y, 69), near(light.select.rect.width, 120), near(light.select.rect.height, 32), + light.select.value, light.select.title], + ['智能体团队管理', '开启后,复杂任务会自动拆分多个智能体协作完成。专家团队任务不受此设置影响。', + true, true, true, true, '开启', '仅保存偏好;多智能体协作执行尚未接入']) + check('economy mode keeps the recovered copy, 34x20 switch and disabled annotation', + [light.sections[1].title, light.sections[1].desc, + near(light.switch.rect.x, 820), near(light.switch.rect.y, 181), near(light.switch.rect.width, 34), near(light.switch.rect.height, 20), + light.switch.trackBackground, light.switch.checked, light.switch.disabled, light.switch.unavailable], + ['经济模式', '自动在合适时机触发上下文压缩,减少长对话中重复传输的上下文,降低积分消耗', + true, true, true, true, 'rgb(0, 194, 154)', true, true, '暂未接入']) + check('browser group keeps the recovered label 638x22 and Beta badge', + [near(light.browserLabel.x, 232), near(light.browserLabel.y, 252), near(light.browserLabel.width, 638), near(light.browserLabel.height, 22), + light.browserLabel.text, near(light.beta.x, 314), near(light.beta.y, 253), near(light.beta.width, 39.391), near(light.beta.height, 20), light.beta.text], + [true, true, true, true, '浏览器操控', true, true, true, true, 'Beta']) + check('browser card keeps 638x261 with 606x44 main section and 68x32 install button', + [near(light.browserCard.y, 282), near(light.browserCard.width, 638), near(light.browserCard.height, 261), + near(light.sections[2].rect.y, 294), near(light.sections[2].rect.width, 606), near(light.sections[2].rect.height, 44), + light.sections[2].title, near(light.buttons[0].rect.x, 786), near(light.buttons[0].rect.width, 68), near(light.buttons[0].rect.height, 32), + light.buttons[0].ariaDisabled, light.buttons[0].title], + [true, true, true, true, true, true, '让 YCode 控制浏览器', true, true, true, 'true', '尚未配置']) + check('browser list keeps the recovered 606x193 with 68/68/56 rows', + [near(light.browserList.y, 338), near(light.browserList.width, 606), near(light.browserList.height, 193), + light.rows.map(row => near(row.rect.height, row.title.includes('QQ') ? 56 : 68)), + light.rows.every(row => row.icon && row.icon.tagName === 'IMG' && near(row.icon.width, 36) && near(row.icon.height, 36) + && row.icon.complete && row.icon.naturalWidth > 0 && row.icon.naturalHeight > 0 && row.icon.src && row.icon.placeholderTitle === null)], + [true, true, true, [true, true, true], true]) + check('browser rows keep the recovered titles, descriptions and disabled 102x32 actions', + [light.rows.map(row => row.title), + light.rows.every(row => row.button && near(row.button.rect.width, 102) && near(row.button.rect.height, 32) && row.button.text === '前往安装' && row.button.ariaDisabled === 'true' && row.button.title === '尚未配置' && row.button.arrow)], + [['YCode Chrome 扩展程序', 'YCode Edge 扩展程序', 'YCode QQ 浏览器扩展程序'], true]) + check('agents page contains no anchor links', light.anchors, []) + + await step('agents-team-toggle') + await read(`document.querySelector('.extensions-settings-panel__team-dropdown .wb-select').click()`, settings) + const popover = await wait(`(()=>{const node=document.querySelector('.extensions-settings-panel__team-popover') + return node?{options:[...node.querySelectorAll('[role=option]')].map(option=>option.textContent.trim())}:null})()`, 'team popover', settings) + check('team-management dropdown offers exactly 开启 / 禁用', popover.options.map(text => text.replace(/\s+/g, '')), ['开启', '禁用']) + await read(`[...document.querySelectorAll('.extensions-settings-panel__team-popover [role=option]')].find(option=>option.textContent.trim()==='禁用').click()`, settings) + await wait(`document.querySelector('.extensions-settings-panel__team-dropdown .wb-select__value')?.textContent?.trim()==='禁用'`, 'team management disabled', settings) + const stored = await read(`window.ycode.agents.get().then(view => view.teamManagement)`, settings) + const config = JSON.parse(await fs.readFile(path.join(directory, 'data', 'config.json'), 'utf8')) + snapshots.config = config.agentSettings + check('selecting 禁用 persists the team-management preference', [stored, config.agentSettings?.teamManagement], [false, false]) + + await step('agents-persistence') + settings.close() + await pause(500) + await openSettings('extensions') + const reopened = await read(layoutSource, settings) + check('reopened settings restores the 禁用 preference', reopened.select.value, '禁用') + + await step('agents-dark') + await openSettings('appearance') + await wait('!!document.querySelector(".appearance-card")', 'appearance panel', settings) + await read(`document.querySelector('[aria-label="深色"].appearance-card').click()`, settings) + await wait(`document.documentElement.dataset.theme==='dark'`, 'dark theme', settings) + await openSettings('extensions') + await wait(`document.documentElement.dataset.theme==='dark'&&!!document.querySelector('.extensions-settings-panel')`, 'dark agents panel', settings) + const dark = await read(layoutSource, settings) + snapshots.dark = dark + await fs.writeFile(path.join(directory, 'ycode-agents-dark.png'), (await settings.webContents.capturePage()).toPNG()) + check('dark theme keeps the recovered card, text, select and button surfaces', + [dark.cardBackground, dark.titleColor, dark.descColor, dark.selectBackground, dark.selectBorder, dark.buttonBackground, dark.buttonBorder, dark.betaBackground], + ['rgb(31, 31, 31)', 'rgb(242, 243, 245)', 'rgba(255, 255, 255, 0.5)', 'rgb(31, 31, 31)', 'rgb(61, 61, 61)', 'rgb(38, 38, 38)', 'rgb(61, 61, 61)', 'rgb(31, 31, 31)']) + check('dark theme keeps every recovered geometry', + [near(dark.panel.width, 638), near(dark.panel.height, 503), near(dark.cards[0].height, 90), + near(dark.browserCard.height, 261), near(dark.browserList.height, 193)], [true, true, true, true, true]) + check('dark economy switch keeps the recovered enabled brand track', dark.switch.trackBackground, 'rgb(34, 203, 168)') + await openSettings('appearance') + await wait('!!document.querySelector(".appearance-card")', 'appearance panel again', settings) + await read(`document.querySelector('[aria-label="浅色"].appearance-card').click()`, settings) + await wait(`document.documentElement.dataset.theme==='light'`, 'light restored', settings) + + await step('agents-errors') + check('no renderer error logs', rendererErrors, []) + + const result = checks.every(item => item.pass) ? 'PASS' : 'FAIL' + await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify({ result, phase, checks, snapshots }, null, 2)) + console.log('AGENTS_SETTINGS_' + result + ' ' + directory) + for (const failure of checks.filter(item => !item.pass)) console.log('AGENTS_SETTINGS_FAILURE ' + JSON.stringify(failure)) + app.exit(result === 'PASS' ? 0 : 1) + })().catch(async error => { + console.error(error) + try { await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify({ result: 'ERROR', phase, checks, snapshots, error: String(error?.stack ?? error) }, null, 2)) } catch { /* ignore */ } + app.exit(1) + }) +} diff --git a/scripts/verify-settings-nav.cjs b/scripts/verify-settings-nav.cjs index 6cad184..73493f4 100644 --- a/scripts/verify-settings-nav.cjs +++ b/scripts/verify-settings-nav.cjs @@ -125,10 +125,10 @@ if (!process.versions.electron) { // 原版 4 组;about 组按组件约定渲染为分隔线而不是标题。 check('nav keeps the four recovered groups', [nav.groups.length, nav.dividers], [3, 1]) // 通用(settings) 在 TODO 10 接入后同样可用。 - const expectedDisabled = EXPECTED_TABS.filter(tab => !['settings', 'models', 'appearance', 'keyboardShortcuts', 'personalization', 'helpFeedback', 'getHelp'].includes(tab)) + const expectedDisabled = EXPECTED_TABS.filter(tab => !['settings', 'models', 'appearance', 'keyboardShortcuts', 'personalization', 'extensions', 'helpFeedback', 'getHelp'].includes(tab)) check('unwired entries stay disabled', nav.tabs.filter((tab, index) => nav.disabled[index] === 'true'), expectedDisabled) check('unwired entries keep the 暂未接入 hint', nav.titles.filter(title => (title ?? '').includes('暂未接入')).length, expectedDisabled.length) - check('settings, appearance, shortcuts, personalization, models, about and help stay enabled', nav.tabs.filter((tab, index) => !nav.disabled[index]), ['settings', 'appearance', 'keyboardShortcuts', 'personalization', 'models', 'helpFeedback', 'getHelp']) + check('settings, appearance, shortcuts, personalization, agents, models, about and help stay enabled', nav.tabs.filter((tab, index) => !nav.disabled[index]), ['settings', 'appearance', 'keyboardShortcuts', 'personalization', 'extensions', 'models', 'helpFeedback', 'getHelp']) await step('settings-search-excludes-removed') await read(`(()=>{const input=document.querySelector('.settings-navigation input[type="search"]'); diff --git a/src/main/agent-settings.ts b/src/main/agent-settings.ts new file mode 100644 index 0000000..7037ae0 --- /dev/null +++ b/src/main/agent-settings.ts @@ -0,0 +1,30 @@ +/** + * 设置 → 智能体(TODO 15):只持久化「智能体团队管理」偏好。 + * + * REA 依据(WorkBuddy 5.8.3,设置窗口 900×680): + * - ev_bd0f4b5dac7d4e2b89b1b050db1896c65e03b4fd1c3d3d986a8958fd9c1a9883(浅色顶部 DOM/layout/computed style + CSS 源) + * - ev_3384aa03af8a1ccdffb8e025dbd4a354636d8d6b0c207abd21e12fc5eb2ae719(深色同页) + * - ev_01940b7925326ee8e7f7e4aba661be2a17a2f876d634d4d370d01b6616a4a7fd(团队管理下拉展开:开启 / 禁用) + * - ev_1cc896e04c0cc9ceab39b0a252f4982c019305609195ad44804ae9b5fb7379a8(最终浅色顶部) + * + * 用户限定:云端服务与子代理调度本轮不处理。因此这里只保存偏好(默认开启,与原版一致), + * 多智能体自动拆分的执行器尚未接入;面板用 title 明确说明,不把偏好伪装成已生效能力。 + */ +import type { AgentSettings, AgentSettingsPatch } from '@shared/types' +import { getStoredAgentSettings, setStoredAgentSettings } from './config' + +export const DEFAULT_AGENT_SETTINGS: AgentSettings = { teamManagement: true } + +export function normalizeAgentSettings(value: Partial | null | undefined): AgentSettings { + return { teamManagement: value?.teamManagement !== false } +} + +export async function getAgentSettings(): Promise { + return normalizeAgentSettings(await getStoredAgentSettings()) +} + +export async function updateAgentSettings(patch: AgentSettingsPatch): Promise { + const next = normalizeAgentSettings({ ...await getAgentSettings(), ...patch }) + await setStoredAgentSettings(next) + return next +} diff --git a/src/main/config.ts b/src/main/config.ts index c6f77f4..8c4fffb 100644 --- a/src/main/config.ts +++ b/src/main/config.ts @@ -10,7 +10,7 @@ import { app, safeStorage } from 'electron' import { promises as fs } from 'node:fs' import { join } from 'node:path' -import type { GatewayConfig, GatewayConfigView, PersonalizationSettings } from '@shared/types' +import type { AgentSettings, GatewayConfig, GatewayConfigView, PersonalizationSettings } from '@shared/types' interface StoredConfig { speechBaseUrl?: string @@ -28,6 +28,8 @@ interface StoredConfig { autoLaunch?: boolean /** 设置 → 个性化(身份、回复风格、指令、人设、长期记忆)。 */ personalization?: Partial + /** 设置 → 智能体(智能体团队管理偏好)。 */ + agentSettings?: Partial } const DEFAULTS: StoredConfig = { @@ -131,6 +133,16 @@ export async function setStoredPersonalization(value: Partial> { + const stored = await load() + return stored.agentSettings ?? {} +} + +export async function setStoredAgentSettings(value: Partial): Promise { + const stored = await load() + await persist({ ...stored, agentSettings: { ...(stored.agentSettings ?? {}), ...value } }) +} + /** 供渲染进程使用:脱敏视图 */ export async function getConfigView(): Promise { const stored = await load() diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 1236aa0..7408e44 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -11,7 +11,7 @@ import { BrowserWindow, app, dialog, ipcMain, shell, type OpenDialogOptions } fr import { randomUUID } from 'node:crypto' import { unlink, writeFile } from 'node:fs/promises' import { join } from 'node:path' -import type { ChatEvent, ChatMessage, GitBranchInfo, PersonalizationPatch, WindowState } from '@shared/types' +import type { AgentSettingsPatch, ChatEvent, ChatMessage, GitBranchInfo, PersonalizationPatch, WindowState } from '@shared/types' import { freezeAssistantModelMetadata } from '@shared/assistant-model' import type { ConfigPatchLike, EditAction, SendPayload, WorkspaceAttachResult } from '@shared/ipc' import { applyAssistantEvent } from '@shared/message-parts' @@ -35,6 +35,7 @@ import * as models from './models' import * as workspaces from './workspaces' import * as generalSettings from './general-settings' import * as personalization from './personalization' +import * as agentSettings from './agent-settings' import { prepareOmpModel } from './omp-model' import type { CustomModelInput, ResolvedModel } from '@shared/models' import type { SessionContext } from '@shared/workspaces' @@ -411,6 +412,8 @@ export function registerIpc(getWindow: () => BrowserWindow | null): void { ipcMain.handle('general:get', (_event, options?: { refreshUsage?: boolean }) => generalView(options?.refreshUsage === true)) ipcMain.handle('personalization:get', () => personalization.getPersonalization()) ipcMain.handle('personalization:update', (_event, patch: PersonalizationPatch) => personalization.updatePersonalization(patch)) + ipcMain.handle('agents:get', () => agentSettings.getAgentSettings()) + ipcMain.handle('agents:update', (_event, patch: AgentSettingsPatch) => agentSettings.updateAgentSettings(patch)) ipcMain.handle('general:update', async (_event, patch: { autoLaunch?: boolean; preventSleep?: boolean }) => { if (patch.preventSleep !== undefined) { await config.setPreventSleep(patch.preventSleep) diff --git a/src/preload/index.ts b/src/preload/index.ts index f090986..0ea2fe8 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -117,6 +117,10 @@ const api: YCodeApi = { get: () => ipcRenderer.invoke('personalization:get'), update: (patch) => ipcRenderer.invoke('personalization:update', patch) }, + agents: { + get: () => ipcRenderer.invoke('agents:get'), + update: (patch) => ipcRenderer.invoke('agents:update', patch) + }, workspace: { get: () => ipcRenderer.invoke('workspace:get'), choose: () => ipcRenderer.invoke('workspace:choose'), diff --git a/src/renderer/src/components/AgentsSettingsPanel.tsx b/src/renderer/src/components/AgentsSettingsPanel.tsx new file mode 100644 index 0000000..1917677 --- /dev/null +++ b/src/renderer/src/components/AgentsSettingsPanel.tsx @@ -0,0 +1,182 @@ +/** + * 设置 → 智能体:WorkBuddy 5.8.3「智能体」页的 1:1 复刻(TODO 15)。 + * + * REA 依据(设置窗口 900×680): + * - ev_bd0f4b5dac7d4e2b89b1b050db1896c65e03b4fd1c3d3d986a8958fd9c1a9883(浅色顶部 DOM/layout/computed style + CSS 源) + * - ev_3384aa03af8a1ccdffb8e025dbd4a354636d8d6b0c207abd21e12fc5eb2ae719(深色同页) + * - ev_01940b7925326ee8e7f7e4aba661be2a17a2f876d634d4d370d01b6616a4a7fd(团队管理下拉:开启 / 禁用) + * - ev_1cc896e04c0cc9ceab39b0a252f4982c019305609195ad44804ae9b5fb7379a8(最终浅色顶部) + * + * 页面只有三个区块:智能体团队管理(下拉)、经济模式(开关)、浏览器操控(Beta,三行扩展安装)。 + * 用户限定:云端服务与子代理调度暂不处理。本轮只把「智能体团队管理」做成真实持久化偏好, + * 并在控件 title 说明多智能体执行器尚未接入;经济模式与浏览器操控保持原版布局、禁用并标注。 + */ +import { useEffect, useRef, useState } from 'react' +import type { ReactNode } from 'react' +import type { AgentSettings } from '@shared/types' +import { IconSelectCaret } from './general-settings-icons' +import browserChromeIcon from './agents-assets/workbuddy-browser-chrome.svg' +import browserEdgeIcon from './agents-assets/workbuddy-browser-edge.svg' +import browserQqIcon from './agents-assets/workbuddy-browser-qq.png' +import './source-agents-panels.css' + +const UNAVAILABLE_HINT = '暂未接入' +const UNSET_HINT = '尚未配置' +const BROWSER_ROWS: ReadonlyArray<{ key: string; icon: string; title: string; desc: string }> = [ + { key: 'chrome', icon: browserChromeIcon, title: 'YCode Chrome 扩展程序', desc: '在 Chrome 中安装此扩展程序,将 YCode 连接到你的浏览器。' }, + { key: 'edge', icon: browserEdgeIcon, title: 'YCode Edge 扩展程序', desc: '在 Edge 中安装此扩展程序,将 YCode 连接到你的浏览器。' }, + { key: 'qq', icon: browserQqIcon, title: 'YCode QQ 浏览器扩展程序', desc: '在 QQ 浏览器中安装此扩展程序,将 YCode 连接到你的浏览器。' } +] + +function classNames(...values: Array): string { + return values.filter(Boolean).join(' ') +} + +function ExternalArrow(): React.JSX.Element { + return +} + +function Section({ title, desc, action, stacked }: { title: string; desc?: ReactNode; action: ReactNode; stacked?: boolean }): React.JSX.Element { + return ( +
+
+
+

{title}

+ {desc ?

{desc}

: null} +
+
{action}
+
+
+ ) +} + +function Switch({ checked }: { checked: boolean }): React.JSX.Element { + return ( +
+ +
+ ) +} + +function UnavailableButton({ label, width, arrow = false }: { label: string; width: number; arrow?: boolean }): React.JSX.Element { + return ( + + ) +} + +function TeamSelect({ value, open, onToggle, onChoose }: { + value: boolean + open: boolean + onToggle(): void + onChoose(value: boolean): void +}): React.JSX.Element { + return ( +
+ + {open ? ( +
+ {[true, false].map((option) => ( + + ))} +
+ ) : null} +
+ ) +} + +export default function AgentsSettingsPanel(): React.JSX.Element { + const [view, setView] = useState(null) + const [error, setError] = useState('') + const [teamOpen, setTeamOpen] = useState(false) + const root = useRef(null) + + useEffect(() => { + let active = true + void window.ycode.agents.get() + .then((next) => { if (active) setView(next) }) + .catch((reason) => { if (active) setError(String((reason as Error)?.message ?? reason)) }) + return () => { active = false } + }, []) + + useEffect(() => { + if (!teamOpen) return + const outside = (event: PointerEvent): void => { if (!root.current?.contains(event.target as Node)) setTeamOpen(false) } + const escape = (event: KeyboardEvent): void => { + if (event.key !== 'Escape') return + event.preventDefault(); event.stopPropagation(); setTeamOpen(false) + } + document.addEventListener('pointerdown', outside, true) + document.addEventListener('keydown', escape, true) + return () => { document.removeEventListener('pointerdown', outside, true); document.removeEventListener('keydown', escape, true) } + }, [teamOpen]) + + const update = async (teamManagement: boolean): Promise => { + try { + setView(await window.ycode.agents.update({ teamManagement })) + setError('') + } catch (reason) { + setError(String((reason as Error)?.message ?? reason)) + } + } + + return ( +
+ {error ?

{error}

: null} + +
+
setTeamOpen((open) => !open)} + onChoose={(teamManagement) => { setTeamOpen(false); void update(teamManagement) }} />} /> +
+ +
+
} /> +
+ +
+

浏览器操控Beta

+
+
} /> +
+ {BROWSER_ROWS.map((row) => ( +
+ +
+
{row.title}
+
{row.desc}
+
+ +
+ ))} +
+
+
+
+ ) +} diff --git a/src/renderer/src/components/SettingsDialog.tsx b/src/renderer/src/components/SettingsDialog.tsx index 56eed6c..e6594dd 100644 --- a/src/renderer/src/components/SettingsDialog.tsx +++ b/src/renderer/src/components/SettingsDialog.tsx @@ -6,6 +6,7 @@ import AppearanceSettingsPanel from './AppearanceSettingsPanel' import ShortcutsSettingsPanel from './ShortcutsSettingsPanel' import PersonalizationSettingsPanel from './PersonalizationSettingsPanel' import AboutHelpSettingsPanel from './AboutHelpSettingsPanel' +import AgentsSettingsPanel from './AgentsSettingsPanel' import SourceSettingsNavigation from './SourceSettingsNavigation' import { useSharedTheme } from '../useSharedTheme' import { SOURCE_WINDOWS_SETTINGS_TABS } from './SourceSettingsNavigation' @@ -56,7 +57,7 @@ export default function SettingsDialog({ theme, onThemeChange, onClose, nativeWi return () => window.removeEventListener('keydown', escape) }, [onClose]) const modal =
-
{ setError(''); setSection(id === 'models' ? 'model' : id) }} onUnavailable={() => setError('该设置尚未接入')} />
{error &&

{error}

} @@ -67,6 +68,7 @@ export default function SettingsDialog({ theme, onThemeChange, onClose, nativeWi {section === 'keyboardShortcuts' && } {section === 'personalization' && } {(section === 'helpFeedback' || section === 'getHelp') && } + {section === 'extensions' && } {section === 'about' &&

YCode

{settings.appInfo?.version}

}
diff --git a/src/renderer/src/components/agents-assets/workbuddy-browser-chrome.svg b/src/renderer/src/components/agents-assets/workbuddy-browser-chrome.svg new file mode 100644 index 0000000..8600890 --- /dev/null +++ b/src/renderer/src/components/agents-assets/workbuddy-browser-chrome.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/renderer/src/components/agents-assets/workbuddy-browser-edge.svg b/src/renderer/src/components/agents-assets/workbuddy-browser-edge.svg new file mode 100644 index 0000000..a34c34a --- /dev/null +++ b/src/renderer/src/components/agents-assets/workbuddy-browser-edge.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/renderer/src/components/agents-assets/workbuddy-browser-qq.png b/src/renderer/src/components/agents-assets/workbuddy-browser-qq.png new file mode 100644 index 0000000..6758eae Binary files /dev/null and b/src/renderer/src/components/agents-assets/workbuddy-browser-qq.png differ diff --git a/src/renderer/src/components/source-agents-panels-recovered.css b/src/renderer/src/components/source-agents-panels-recovered.css new file mode 100644 index 0000000..864dae9 --- /dev/null +++ b/src/renderer/src/components/source-agents-panels-recovered.css @@ -0,0 +1,7 @@ +/* REA light=ev_1cc896e04c0cc9ceab39b0a252f4982c019305609195ad44804ae9b5fb7379a8; dark=ev_3384aa03af8a1ccdffb8e025dbd4a354636d8d6b0c207abd21e12fc5eb2ae719; ExtensionsSettingsPanel-BnsXh3oa.css sha256=cbd4b37c6061381943d96ca454176b2f083e49eb8e04c34cb5d31eef3dbcb8f5; tokens sha256=13f2bf82c96e5167a7d7098e36950c6c22c475b3337dbb8bb29ad2fe79cde696. */ +.extensions-settings-panel__browser-extension-status { box-sizing: border-box; min-width: 84px; height: 32px; padding: 0 var(--wb-spacing-4); color: var(--wb-color-text-secondary); font-size: var(--wb-font-body-size); font-weight: 400; line-height: var(--wb-font-body-line-height); white-space: nowrap; cursor: default; align-items: center; display: inline-grid; position: relative; } +.extensions-settings-panel__browser-extension-status::before { content: attr(data-install-label); visibility: hidden; padding-right: 20px; font-weight: 500; } +.extensions-settings-panel__browser-extension-status-content { justify-content: center; align-items: center; gap: var(--wb-spacing-3); display: flex; position: absolute; inset: 0px; } +.extensions-settings-panel__browser-extension-status-dot { background: var(--wb-status-success); border-radius: 50%; flex: 0 0 6px; width: 6px; height: 6px; } +.extensions-settings-panel{--wb-spacing-4:12px;--wb-color-text-secondary:var(--wb-palette-black-70);--wb-font-body-size:var(--wb-font-size-5);--wb-font-body-line-height:var(--wb-font-line-height-5-2);--wb-spacing-3:8px;--wb-status-success:var(--wb-palette-green-8);--wb-palette-black-70:#000000b3;--wb-font-size-5:14px;--wb-font-line-height-5-2:22px;--wb-palette-green-8:#0cbf5b;--wb-color-text-success-default:var(--wb-palette-green-8);--wb-palette-green-6:#47cf84} +[data-theme=dark] .extensions-settings-panel{--wb-spacing-4:12px;--wb-color-text-secondary:#ffffffa6;--wb-font-body-size:var(--wb-font-size-5);--wb-font-body-line-height:var(--wb-font-line-height-5-2);--wb-spacing-3:8px;--wb-status-success:var(--wb-color-text-success-default);--wb-palette-black-70:#000000b3;--wb-font-size-5:14px;--wb-font-line-height-5-2:22px;--wb-palette-green-8:#43f08f;--wb-color-text-success-default:var(--wb-palette-green-6);--wb-palette-green-6:#24c46c} diff --git a/src/renderer/src/components/source-agents-panels.css b/src/renderer/src/components/source-agents-panels.css new file mode 100644 index 0000000..2fa94ca --- /dev/null +++ b/src/renderer/src/components/source-agents-panels.css @@ -0,0 +1,181 @@ +/* + * 设置 → 智能体(TODO 15)。 + * - source-agents-panels-recovered.css:REA 回收的 ExtensionsSettingsPanel-BnsXh3oa.css 投影。 + * - 卡片/section/按钮/开关所需的通用 UI kit 由 GeneralSettingsPanel 已加载的 source-general-settings.css 提供; + * 这里不再重复 @import,避免同一份通用样式表被打包多次改变层叠顺序。 + */ +@import './source-agents-panels-recovered.css'; + +/* + * REA ev_3384aa…(深色):正文 color 为 rgb(242,243,245);原版设置窗口把 --wb-color-text-primary + * 覆盖为 #f2f3f5,比全局 tokens 的 #fff 更具体,这里按同一优先级还原。 + */ +:root[data-theme=dark] .general-settings-panel.extensions-settings-panel { + --wb-color-text-primary: #f2f3f5; +} + +/* ---- 智能体团队管理下拉 ---- */ +.extensions-settings-panel__team-dropdown { + position: relative; + flex-shrink: 0; +} +.extensions-settings-panel__team-dropdown .wb-select { + width: 120px; + min-width: 120px; + height: 32px; + padding: 0 8px; + justify-content: space-between; + gap: 2px; + color: var(--wb-color-text-primary); + background: var(--wb-bg-primary); + border: 1px solid var(--wb-color-border-primary); + border-radius: 8px; + font-family: Arial, system-ui, sans-serif; + font-size: 14px; + line-height: 22px; + cursor: pointer; + display: inline-flex; + align-items: center; +} +:root[data-theme=dark] .extensions-settings-panel__team-dropdown .wb-select { + border-color: var(--wb-color-border-solid); +} +.extensions-settings-panel__team-popover { + position: absolute; + top: calc(100% + 8px); + right: 0; + z-index: 30; + box-sizing: border-box; + width: 120px; + padding: 4px; + background: var(--wb-bg-primary); + border: 1px solid var(--wb-border-default); + border-radius: 12px; + box-shadow: color(srgb 0 0 0 / 0.12) 0 8px 48px -4px, color(srgb 0 0 0 / 0.06) 0 6px 12px -4px; +} +.extensions-settings-panel__team-popover .wb-select__option { + box-sizing: border-box; + width: 100%; + height: 32px; + padding: 0 8px; + color: var(--wb-color-text-primary); + font: inherit; + font-size: 14px; + text-align: left; + background: none; + border: none; + border-radius: 6px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} +.extensions-settings-panel__team-popover .wb-select__option:hover { + background: var(--wb-bg-hover); +} +.extensions-settings-panel__team-popover .wb-select__check { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; +} + +/* ---- 浏览器操控(Beta) ---- */ +.extensions-settings-panel__browser-label { + display: flex; + align-items: center; + gap: 4px; +} +.extensions-settings-panel__beta { + box-sizing: border-box; + padding: 0 8px; + color: var(--wb-color-text-tertiary); + background: var(--wb-bg-primary); + border: 1px solid var(--wb-border-default); + border-radius: 4px; + font-size: 11px; + line-height: 18px; + font-weight: 400; +} +.extensions-settings-panel__browser-card { + display: flex; + flex-direction: column; +} +.extensions-settings-panel__browser-card > .general-settings-panel__section .extensions-settings-panel__action-button { + width: 68px; + min-width: 68px; +} +.extensions-settings-panel__action-button { + flex-shrink: 0; +} +.extensions-settings-panel__browser-list { + box-sizing: border-box; + width: 100%; + height: 193px; + padding-top: 1px; + overflow: hidden; + border-radius: 8px; + box-shadow: inset 0 0 0 1px var(--wb-border-subtle); +} +.extensions-settings-panel__browser-row { + box-sizing: border-box; + width: 100%; + height: 68px; + display: flex; + align-items: center; + gap: 12px; +} +.extensions-settings-panel__browser-row:not(:last-child) { + border-bottom: 1px solid var(--wb-border-subtle); +} +.extensions-settings-panel__browser-row--qq { + height: 56px; + align-items: flex-start; + padding-top: 12px; +} +.extensions-settings-panel__browser-row--qq .extensions-settings-panel__action-button { + margin-top: 6px; +} +.extensions-settings-panel__browser-icon { + width: 36px; + height: 36px; + flex-shrink: 0; + display: block; + object-fit: contain; +} +.extensions-settings-panel__browser-copy { + flex: 1; + min-width: 0; +} +.extensions-settings-panel__browser-title { + color: var(--wb-color-text-primary); + font-size: 14px; + line-height: 22px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.extensions-settings-panel__browser-desc { + color: var(--wb-color-text-secondary); + font-size: 14px; + line-height: 22px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* 未接入行为保持原版形态但禁用(与通用页约定一致)。 */ +.extensions-settings-panel [data-settings-unavailable=true] { cursor: default } +.extensions-settings-panel [data-settings-unavailable=true] .wb-switch { cursor: default } +.general-settings-panel.extensions-settings-panel .extensions-settings-panel__unavailable-control + .wb-switch:has(.wb-switch__input:checked:disabled) .wb-switch__track { + background-color: #00c29a; + border-color: #00c29a; +} +:root[data-theme=dark] .general-settings-panel.extensions-settings-panel .extensions-settings-panel__unavailable-control + .wb-switch:has(.wb-switch__input:checked:disabled) .wb-switch__track { + background-color: #22cba8; + border-color: #22cba8; +} diff --git a/src/shared/ipc.ts b/src/shared/ipc.ts index 17c9eb7..593c9a2 100644 --- a/src/shared/ipc.ts +++ b/src/shared/ipc.ts @@ -15,7 +15,9 @@ import type { GeneralSettingsPatch, GeneralSettingsView, PersonalizationPatch, - PersonalizationSettings + PersonalizationSettings, + AgentSettings, + AgentSettingsPatch } from './types' import type { DownloadPayload, DownloadResult, ResourceEntry, ResourceRange, WorkspaceResource } from './resources' import type { ChatAttachment, InteractionResponse, MessageQuote, PermissionMode } from './conversation' @@ -120,6 +122,11 @@ export interface YCodeApi { get(): Ipc update(patch: PersonalizationPatch): Ipc } + /** 设置 → 智能体:智能体团队管理偏好。 */ + agents: { + get(): Ipc + update(patch: AgentSettingsPatch): Ipc + } workspace: { get(): Ipc choose(): Ipc diff --git a/src/shared/types.ts b/src/shared/types.ts index 9cb8a26..ffaf8f1 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -172,6 +172,17 @@ export interface PersonalizationSettings { export type PersonalizationPatch = Partial +/** + * 设置 → 智能体(TODO 15)。 + * 本轮只接入「智能体团队管理」这一个偏好;经济模式与浏览器操控仅按原版布局呈现并明确标注未接入。 + */ +export interface AgentSettings { + /** 开启后复杂任务自动拆分多个智能体协作完成(原版默认开启)。 */ + teamManagement: boolean +} + +export type AgentSettingsPatch = Partial + /** 主进程 → 渲染进程的流式事件 */ export type ChatEvent = { sessionId?: string; turnDurationMs?: number } & ( | { runId: string; type: 'started'; messageId: string }