diff --git a/.waylog/history/2026-07-07_12-55Z-__Files_mentioned_by_the_user______AUDIT_REPORT_20.md b/.waylog/history/2026-07-07_12-55Z-__Files_mentioned_by_the_user______AUDIT_REPORT_20.md new file mode 100644 index 0000000..d4b5fa0 --- /dev/null +++ b/.waylog/history/2026-07-07_12-55Z-__Files_mentioned_by_the_user______AUDIT_REPORT_20.md @@ -0,0 +1,486 @@ +# # Files mentioned by the user: + +## AUDIT_REPORT_20 +_Exported on 07/07/2026 at 22:07:15 GMT+8 from OpenAI Codex via WayLog_ + + +**OpenAI Codex** + + +Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `danger-full-access`: No filesystem sandboxing - all commands are permitted. Network access is enabled. +Approval policy is currently never. Do not provide the `sandbox_permissions` for any reason, commands will be rejected. + +# Codex desktop context +- You are running inside the Codex (desktop) app, which allows some additional features not available in the CLI alone: + +### Images/Visuals/Files +- In the app, the model can display images and videos using standard Markdown image syntax: ![alt](url) +- When sending or referencing a local image or video, always use an absolute filesystem path in the Markdown image tag (e.g., ![alt](/absolute/path.png)); relative paths and plain text will not render the media. +- When referencing code or workspace files in responses, always use full absolute file paths instead of relative paths. +- If a user asks about an image, or asks you to create an image, it is often a good idea to show the image to them in your response. +- Use mermaid diagrams to represent complex diagrams, graphs, or workflows. Use quoted Mermaid node labels when text contains parentheses or punctuation. +- Return web URLs as Markdown links (e.g., [label](https://example.com)). + +### Workspace Dependencies +- For sheets, slides, and documents, call `load_workspace_dependencies` to find the bundled runtime and libraries. + +### Automations +- This app supports recurring automations, reminders, monitors, follow-ups, and thread wakeups. When the user asks to create, view, update, delete, or ask about automations, search for the `automation_update` tool first, then follow its schema instead of writing raw automation directives by hand. +- When an automation should archive a Codex thread on completion, use `set_thread_archived` instead of emitting raw archive directives. + +### Thread Coordination +- When the user asks to create, fork, inspect, continue, hand off, pin, archive, rename, or otherwise manage Codex threads, search for the relevant thread tool first: `create_thread`, `fork_thread`, `list_threads`, `read_thread`, `send_message_to_thread`, `handoff_thread`, `set_thread_pinned`, `set_thread_archived`, or `set_thread_title`. +- Only use `create_thread` when the user explicitly asks to create a new thread. Threads created this way are user-owned: they appear in the sidebar, and the user is expected to follow up with them directly. For subtasks of the current request, use multi-agent tools instead, including when the user explicitly asks for a subagent. +- After a successful `create_thread` call, emit `::created-thread{threadId="..."}` for a created thread or `::created-thread{pendingWorktreeId="..."}` for queued worktree setup on its own line in your final response. + +### Inline Code Comments +- Use the ::code-comment{...} directive when you need to attach feedback directly to specific code lines. +- Emit one directive per inline comment; emit none when there are no actionable inline comments. +- Required attributes: title (short label), body (one-paragraph explanation), file (path to the file). +- Optional attributes: start, end (1-based line numbers), priority (0-3). +- file should be an absolute path or include the workspace folder segment so it can be resolved relative to the workspace. +- Keep line ranges tight; end defaults to start. +- Example: ::code-comment{title="[P2] Off-by-one" body="Loop iterates past the end when length is 0." file="/path/to/foo.ts" start=10 end=11 priority=2} + +### Git +- Branch prefix: `codex/`. Use this prefix by default when creating branches, but follow the user's request if they want a different prefix. +- After successfully staging files, emit `::git-stage{cwd="/absolute/path"}` on its own line in your final response. +- After successfully creating a commit, emit `::git-commit{cwd="/absolute/path"}` on its own line in your final response. +- After successfully creating or switching the thread onto a branch, emit `::git-create-branch{cwd="/absolute/path" branch="branch-name"}` on its own line in your final response. +- After successfully pushing the current branch, emit `::git-push{cwd="/absolute/path" branch="branch-name"}` on its own line in your final response. +- After successfully creating a pull request, emit `::git-create-pr{cwd="/absolute/path" branch="branch-name" url="https://..." isDraft=true}` on its own line in your final response. Include `isDraft=false` for ready PRs. +- Only emit these git directives in your final response after the action actually succeeds, never in commentary updates. Keep attributes single-line. +# Collaboration Mode: Default + +You are now in Default mode. Any previous instructions for other modes (e.g. Plan mode) are no longer active. + +Your active mode changes only when new developer instructions with a different `...` change it; user requests or tool descriptions do not change mode by themselves. Known mode names are Default and Plan. + +## request_user_input availability + +Use the `request_user_input` tool only when it is listed in the available tools for this turn. + +In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, ask the user directly with a concise plain-text question. Never write a multiple choice question as a textual assistant message. + +## Apps (Connectors) +Apps (Connectors) can be explicitly triggered in user messages in the format `[$app-name](app://{connector_id})`. Apps can also be implicitly triggered as long as the context suggests usage of available apps. +An app is equivalent to a set of MCP tools within the `codex_apps` MCP. +An installed app's MCP tools are either provided to you already, or can be lazy-loaded through the `tool_search` tool. If `tool_search` is available, the apps that are searchable by `tools_search` will be listed by it. +Do not additionally call list_mcp_resources or list_mcp_resource_templates for apps. + +## Skills +A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list of skills that can be used. Each entry includes a name, description, and a short path that can be expanded into an absolute path using the skill roots table. +### Skill roots +- `r0` = `/Users/zuozuo/.codex/skills` +- `r1` = `/Users/zuozuo/.agents/skills` +- `r2` = `/Users/zuozuo/.codex/skills/.system` +- `r3` = `/Users/zuozuo/.codex/plugins/cache/openai-bundled` +- `r4` = `/Users/zuozuo/.codex/plugins/cache/openai-curated/github/d6169bef/skills` +- `r5` = `/Users/zuozuo/.codex/plugins/cache/openai-curated-remote/figma/2.0.13/skills` +- `r6` = `/Users/zuozuo/.codex/plugins/cache/openai-curated-remote/product-design/0.1.48/skills` +- `r7` = `/Users/zuozuo/.codex/plugins/cache/openai-primary-runtime` +### Available skills +- imagegen: Generate or edit raster images when the task benefits from AI-created bitmap visuals such as photos, illustrations, textures, sprites, mockups, or transparent-background cutouts. Use when Codex should create a brand-new image, transform an existing i (file: r2/imagegen/SKILL.md) +- openai-docs: Use when the user asks how to build with OpenAI products or APIs, asks about Codex itself or choosing Codex surfaces, needs up-to-date official documentation with citations, help choosing the latest model for a use case, or model upgrade and prompt (file: r2/openai-docs/SKILL.md) +- plugin-creator: Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, valid manifest defaults, and personal-marketplace entries by default. Use when Codex needs to create a new personal plugin, a (file: r2/plugin-creator/SKILL.md) +- skill-creator: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Codex's capabilities with specialized knowledge, workflows, or tool integrations. (file: r2/skill-creator/SKILL.md) +- skill-installer: Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos). (file: r2/skill-installer/SKILL.md) +- browser:control-in-app-browser: Control the in-app Browser. Use to open, navigate, inspect, test, click, type, screenshot, or verify local targets such as localhost, 127.0.0.1, ::1, file://, the current in-app browser tab, and websites shown side by side inside Codex. (file: r3/browser/26.623.141536/skills/control-in-app-browser/SKILL.md) +- documents:documents: Create, edit, redline, and comment on `.docx`, Word, and Google Docs-targeted document artifacts inside the container, with a strict render-and-verify workflow. Use `render_docx.py` to generate page PNGs (and optional PDF) for visual QA, then itera (file: r7/documents/26.630.12135/skills/documents/SKILL.md) +- dws: 管理钉钉产品能力(AI表格/AI搜问/日历/通讯录/群聊与机器人/待办/审批/考勤/日志/DING消息/开放平台文档/钉钉文档/钉钉云盘/AI听记/邮箱/在线电子表格/知识库等)。当用户需要操作表格数据、管理日程会议、模糊找人/查谁负责某事项、查询通讯录、管理群聊、机器人发消息、创建待办、提交审批、查看考勤、提交日报周报(钉钉日志模版)、读写钉钉文档、上传下载云盘文件、查询听记纪要、收发邮件、读写在线电子表格(axls)、管理钉钉知识库时使用。 (file: r1/dws/SKILL.md) +- dws: 管理钉钉产品能力(AI表格/AI搜问/日历/通讯录/群聊与机器人/待办/审批/考勤/日志/DING消息/开放平台文档/钉钉文档/钉钉云盘/AI听记/邮箱/在线电子表格/知识库等)。当用户需要操作表格数据、管理日程会议、模糊找人/查谁负责某事项、查询通讯录、管理群聊、机器人发消息、创建待办、提交审批、查看考勤、提交日报周报(钉钉日志模版)、读写钉钉文档、上传下载云盘文件、查询听记纪要、收发邮件、读写在线电子表格(axls)、管理钉钉知识库时使用。 (file: r0/dws/SKILL.md) +- figma:figma-code-connect: Creates and maintains Figma Code Connect template files that map Figma components to code snippets. Use when the user mentions Code Connect, Figma component mapping, design-to-code translation, or asks to create/update .figma.ts or .figma.js files. (file: r5/figma-code-connect/SKILL.md) +- figma:figma-create-new-file: **MANDATORY prerequisite** — you MUST invoke this skill BEFORE every `create_new_file` tool call. NEVER call `create_new_file` directly without loading this skill first. Trigger whenever the user wants a new blank Figma file — a new design, FigJam, o (file: r5/figma-create-new-file/SKILL.md) +- figma:figma-generate-design: Use this skill alongside figma-use when the task involves translating an application page, view, or multi-section layout into Figma. Triggers: 'write to Figma', 'create in Figma from code', 'push page to Figma', 'take this app/page and build it in Fi (file: r5/figma-generate-design/SKILL.md) +- figma:figma-generate-diagram: MANDATORY prerequisite — load this skill BEFORE every `generate_diagram` tool call. NEVER call `generate_diagram` directly without loading this skill first. Trigger whenever the user asks to create, generate, draw, render, sketch, or build a diagram (file: r5/figma-generate-diagram/SKILL.md) +- figma:figma-generate-library: Build or update a professional-grade design system in Figma from a codebase. Use when the user wants to create variables/tokens, build component libraries, create individual components with proper variant sets and variable bindings, set up theming (file: r5/figma-generate-library/SKILL.md) +- figma:figma-implement-motion: Translates Figma motion and animations into production-ready application code. Use when implementing animation/motion from a Figma design — user mentions "implement this motion", "add animation from Figma", "animate this component", provides a Figma (file: r5/figma-implement-motion/SKILL.md) +- figma:figma-swiftui: SwiftUI ↔ Figma translation. Use whenever the user mentions Swift, SwiftUI, iOS, iPhone, or iPad — in EITHER direction — translating a Figma design into SwiftUI (design → code), or pushing SwiftUI views / screens / tokens back into a Figma file (code (file: r5/figma-swiftui/SKILL.md) +- figma:figma-use: **MANDATORY prerequisite** — you MUST invoke this skill BEFORE every `use_figma` tool call. NEVER call `use_figma` directly without loading this skill first. Skipping it causes common, hard-to-debug failures. Trigger whenever the user wants to perf (file: r5/figma-use/SKILL.md) +- figma:figma-use-figjam: This skill helps agents use Figma's use_figma MCP tool in the FigJam context. Can be used alongside figma-use which has foundational context for using the use_figma tool. (file: r5/figma-use-figjam/SKILL.md) +- figma:figma-use-motion: Motion / animation context for the `use_figma` MCP tool — animating Figma nodes via manual keyframes, animation styles, easing, and timeline duration. Load alongside figma-use whenever a task involves adding, editing, or inspecting animation on a nod (file: r5/figma-use-motion/SKILL.md) +- figma:figma-use-slides: This skill helps agents use Figma's use_figma MCP tool in the Slides context. Can be used alongside figma-use which has foundational context for using the use_figma tool. (file: r5/figma-use-slides/SKILL.md) +- github:gh-address-comments: Address actionable GitHub pull request review feedback. Use when the user wants to inspect unresolved review threads, requested changes, or inline review comments on a PR, then implement selected fixes. Use the GitHub app for PR metadata and flat co (file: r4/gh-address-comments/SKILL.md) +- github:gh-fix-ci: Use when a user asks to debug or fix failing GitHub PR checks that run in GitHub Actions. Use the GitHub app from this plugin for PR metadata and patch context, and use `gh` for Actions check and log inspection before implementing any approved fix. (file: r4/gh-fix-ci/SKILL.md) +- github:github: Triage and orient GitHub repository, pull request, and issue work through the connected GitHub app. Use when the user asks for general GitHub help, wants PR or issue summaries, or needs repository context before choosing a more specific GitHub workflo (file: r4/github/SKILL.md) +- github:yeet: Publish local changes to GitHub by confirming scope, committing intentionally, pushing the branch, and opening a draft PR through the GitHub app from this plugin, with `gh` used only as a fallback where connector coverage is insufficient. (file: r4/yeet/SKILL.md) +- lark-approval: 飞书审批:当前用户审批的查询与全部处理操作,覆盖待本人审批的任务与本人发起的实例。审批待办不是飞书任务(任务类待办走 lark-task);不负责创建审批定义和发起新审批。 (file: r1/lark-approval/SKILL.md) +- lark-apps: 妙搭(Spark/Miaoda)应用开发与托管:应用创建、HTML静态站点发布、本地全栈开发、云端生成迭代。当用户要开发/新建一个系统·工具·平台·应用,或要本地开发 / 云端开发 / 修改 / 部署 / 发布 / 上线 / 拿可分享链接,或用 HTML 做页面·网站给人看,或提到妙搭/Spark/Miaoda、应用数据库、可见范围时使用。不负责普通云盘文件上传(lark-drive)、飞书文档编辑(lark-doc)、原生幻灯片创建(lark-slides)。 (file: r1/lark-apps/SKILL.md) +- lark-attendance: 飞书考勤打卡:查询自己的考勤打卡记录 (file: r1/lark-attendance/SKILL.md) +- lark-base: 飞书多维表格(Base)操作:建表、字段、记录、视图、统计、公式/lookup、表单、仪表盘、workflow、角色权限;遇到 Base/多维表格/bitable 或 /base/ 链接时使用。文件导入转 lark-drive,认证/授权转 lark-shared。 (file: r1/lark-base/SKILL.md) +- lark-calendar: 飞书日历:管理日历日程和会议室。查看/搜索日程、创建/更新日程、管理参会人、查询忙闲和推荐时段、预定会议室。当用户需要查看日程安排、创建/修改会议、查询/预定会议室时使用。不负责:查询过去的视频会议记录(走 lark-vc)、待办任务(走 lark-task)。 (file: r1/lark-calendar/SKILL.md) +- lark-contact: 飞书 / Lark 通讯录:按姓名 / 邮箱解析成 open_id,或按 open_id 反查姓名 / 部门 / 邮箱 / 联系方式 / 个人状态 / 签名。当用户提到某人姓名要下一步发消息 / 排日程,或拿到 open_id 想查具体信息时使用。不负责部门树遍历、按部门列员工、组织架构图,这类需求走原生 OpenAPI。 (file: r1/lark-contact/SKILL.md) +- lark-doc: 飞书云文档(Docx / Wiki 文档,v2 API):读取和编辑飞书文档内容。当用户给出文档 URL 或 token,或需要查看、创建、编辑文档、插入或下载文档图片附件时使用。文档中嵌入的电子表格、多维表格、画板,先用本 skill 提取 token 再切到对应 skill。当用户给出 doubao.com 的 /docx/ 或 /wiki/ URL/token 时,也应直接使用本 skill;路由依据是 URL 路径模式和 token,而不是域名。不负责文档评论管理,也不负责表格或 Bas (file: r1/lark-doc/SKILL.md) +- lark-drive: 飞书云空间(云盘/云存储):管理云空间(云盘/云存储)中的文件和文件夹。上传和下载文件、创建文件夹、复制/移动/删除文件、查看文件元数据、管理文档评论、管理文档权限、订阅用户评论变更事件、修改文件标题(docx、sheet、bitable、file、folder、wiki);也负责把本地 Word/Markdown/Excel/CSV/PPTX 以及 Base 快照(.base)导入为飞书在线云文档(docx、sheet、bitable、slides)。当用户需要上传或下载文件、整理云空间( (file: r1/lark-drive/SKILL.md) +- lark-event: Lark/Feishu real-time event listening / subscribing / consuming: stream events as NDJSON via `lark-cli event consume ` (covers IM messages/reactions/chat changes, VC meeting ended, Minutes generated, Whiteboard updated, etc.). Use for Lark (file: r1/lark-event/SKILL.md) +- lark-im: 飞书即时通讯:收发消息和管理群聊。发送和回复消息、搜索聊天记录、管理群聊成员、上传下载图片和文件(支持大文件分片下载)、管理表情回复、发送应用内/短信/电话加急。当用户需要发消息、查看或搜索聊天记录、下载聊天中的文件、查看群成员、搜索群、创建群聊或话题群、管理标记数据、管理 Feed 置顶(添加/移除/查询置顶会话)、管理标签数据时使用。 (file: r1/lark-im/SKILL.md) +- lark-mail: 飞书邮箱 — draft, compose, send, reply, forward, read, and search emails; manage drafts, folders, labels, contacts, attachments, and mail rules. Use when user mentions 起草邮件, 写一封邮件, 拟邮件, 草稿, 发通知邮件, 发送邮件, 发邮件, 回复邮件, 转发邮件, 查看邮件, 看邮件, 读邮件, 搜索邮件, 查邮件, 收件箱, (file: r1/lark-mail/SKILL.md) +- lark-markdown: 飞书 Markdown:查看、创建、上传、编辑和比较 Markdown 文件。当用户需要创建或编辑 Markdown 文件、读取、修改、局部 patch 或比较差异时使用。不负责将 Markdown 导入为飞书在线文档,也不负责文件搜索、权限、评论、移动、删除等云空间管理操作。 (file: r1/lark-markdown/SKILL.md) +- lark-minutes: 飞书妙记:搜索妙记列表、查看妙记基础信息、下载妙记音视频文件、上传音视频生成妙记、更新妙记标题、替换说话人。当需要获取、操作或者生成妙记时使用。也支持将本地音视频文件转成纪要和逐字稿(优先使用本 skill,不要用 ffmpeg/whisper 本地转写)。不负责:获取会议关联妙记、纪要/逐字稿内容获取走 lark-vc (file: r1/lark-minutes/SKILL.md) +- lark-okr: 飞书 OKR:管理目标与关键结果。查看和编辑 OKR 周期、目标(Objective)、关键结果(Key Result)、对齐关系、量化指标和进展记录。当用户需要查看或创建 OKR、管理目标和关键结果、查看对齐关系时使用。 (file: r1/lark-okr/SKILL.md) +- lark-openapi-explorer: 飞书/Lark 原生 OpenAPI 探索:从官方文档库中挖掘未经 CLI 封装的原生 OpenAPI 接口。当用户的需求无法被现有 lark-* skill 或 lark-cli 已注册命令满足,需要查找并调用原生飞书 OpenAPI 时使用。 (file: r1/lark-openapi-explorer/SKILL.md) +- lark-shared: Use when first setting up lark-cli, running auth login, switching user/bot identity (--as), handling permission denied or scope errors, needing to update lark-cli, or seeing _notice in JSON output. (file: r1/lark-shared/SKILL.md) +- lark-sheets: 飞书电子表格:创建和操作电子表格。支持创建表格、管理工作表与行列结构(增删/合并/调整尺寸/隐藏/冻结)、读写单元格(值/公式/样式/批注/单元格图片)、查找替换、多操作原子批量更新,以及图表、透视表、条件格式、筛选器、迷你图、浮动图片等对象的创建与维护。当用户需要创建电子表格、管理工作表、批量读写或编辑数据、统计汇总与可视化、表格美化、公式计算(含 Excel 公式迁移)等任务时使用。若用户是想按名称或关键词搜索云空间(云盘/云存储)里的表格文件,请改用 lark-drive 的 drive (file: r1/lark-sheets/SKILL.md) +- lark-skill-maker: 创建 lark-cli 的自定义 Skill。当用户需要把飞书 API 操作封装成可复用的 Skill(包装原子 API 或编排多步流程)时使用。 (file: r1/lark-skill-maker/SKILL.md) +- lark-slides: 飞书幻灯片:创建和编辑幻灯片。创建演示文稿、读取幻灯片内容、管理幻灯片页面(创建、删除、读取、局部替换)。当用户需要创建或编辑幻灯片、读取或修改单个页面时使用。当用户给出 doubao.com 的 /slides/ URL/token 时,也应直接使用本 skill,不要因为域名不是飞书而回退到 WebFetch;路由依据是 URL 路径模式和 token,而不是域名。不负责:云文档内容编辑(走 lark-doc)、云文档里的独立画板对象(走 lark-whiteboard,注意 slide (file: r1/lark-slides/SKILL.md) +- lark-task: 飞书任务:管理任务、清单和任务智能体。创建待办任务、查看和更新任务状态、拆分子任务、组织任务清单、分配协作成员、上传任务附件、注册或注销任务智能体、更新任务智能体的主页数据、写入智能体任务记录。当用户需要创建待办事项、查看任务列表、跟踪任务进度、管理项目清单或给他人分配任务、为任务上传附件文件、注册注销任务智能体、更新智能体主页数据、写入任务记录时使用。 (file: r1/lark-task/SKILL.md) +- lark-vc: 飞书视频会议:搜索历史会议记录、查询会议纪要(总结/待办/章节/逐字稿)、查询参会人快照。当用户查询已结束的会议、获取会议产物(纪要/妙记)、查看参会人时使用;查询未来日程走 lark-calendar。不负责:Agent 真实入会/离会、会中实时事件(走 lark-vc-agent)。 (file: r1/lark-vc/SKILL.md) +- lark-vc-agent: 飞书视频会议:让机器人代当前用户加入/离开正在进行的会议,并读取会议期间的实时事件(参会人加入与离开、发言、聊天、屏幕共享等)。1. 用户提供 9 位会议号、要求代为入会或离会时使用 +meeting-join / +meeting-leave——会真实产生入会/离会记录。2. 会议进行中用户想知道“谁加入了”“谁离开了”“谁在发言”“有人共享屏幕吗”等会中动态时,机器人入会后用 +meeting-events 读取事件时间线。3. 典型场景:参会机器人、会中助手、代为旁听、代为参会。前提: (file: r1/lark-vc-agent/SKILL.md) +- lark-whiteboard: 飞书画板:查询和编辑飞书云文档中的画板。支持导出画板为预览图片、导出原始节点结构、使用 DSL(转成 OpenAPI 格式)、PlantUML/Mermaid 格式更新画板内容。 当用户需要查看画板内容、导出画板图片、编辑画板,或是需要可视化表达架构、流程、组织关系、时间线、因果、对比等结构化信息时使用此 skill,无论是否提及"画板"。 ⚠️ 原 `lark-whiteboard-cli` skill 已合并至本 skill,若 skill 列表中同时存在 `lark-whiteboard- (file: r1/lark-whiteboard/SKILL.md) +- lark-wiki: 飞书知识库:管理知识空间、空间成员和文档节点。创建和查询知识空间、查看和管理空间成员、管理节点层级结构、在知识库中组织文档和快捷方式。当用户需要在知识库中查找或创建文档、浏览知识空间结构、查看或管理空间成员、移动或复制节点时使用。当用户给出 doubao.com 的 /wiki/ URL/token 时,也应直接使用本 skill,不要因为域名不是飞书而回退到 WebFetch;路由依据是 URL 路径模式和 token,而不是域名。不负责:上传文件到知识库节点下(走 lark-drive) (file: r1/lark-wiki/SKILL.md) +- lark-workflow-meeting-summary: 会议纪要整理工作流:汇总指定时间范围内的会议纪要并生成结构化报告。当用户需要整理会议纪要、生成会议周报、回顾一段时间内的会议内容时使用。 (file: r1/lark-workflow-meeting-summary/SKILL.md) +- lark-workflow-standup-report: 日程待办摘要:编排 calendar +agenda 和 task +get-my-tasks,生成指定日期的日程与未完成任务摘要。适用于了解今天/明天/本周的安排。 (file: r1/lark-workflow-standup-report/SKILL.md) +- pdf:pdf: Read, create, inspect, render, and verify PDF files where visual layout matters. Use Poppler rendering plus Python tools such as reportlab, pdfplumber, and pypdf for generation and extraction. (file: r7/pdf/26.630.12135/skills/pdf/SKILL.md) +- presentations:Presentations: Create or edit PowerPoint or Google Slides decks (file: r7/presentations/26.630.12135/skills/presentations/SKILL.md) +- product-design:audit: Audit or critique a product flow, journey, workflow, funnel, onboarding path, checkout path, settings path, screen, or multi-step product experience by capturing screenshots first, placing them in Figma or a local folder, then reporting UX, design, (file: r6/audit/SKILL.md) +- product-design:get-context: Mandatory design-brief gate for Product Design build and design workflows. Use before ideation, prototyping, image-to-code builds, redesigns, or product UI work to clarify missing product, visual, and interactivity context or play back the supplied (file: r6/get-context/SKILL.md) +- product-design:ideate: Generate image-based visual alternatives, remixes, or concept directions after Product Design get-context has confirmed the design brief. Use when the user asks for design variants, visual exploration, remixes, or image-generated approaches from provi (file: r6/ideate/SKILL.md) +- product-design:image-to-code: Implement a selected image, screenshot, mockup, or Image Gen reference as a faithful responsive frontend after Product Design get-context has confirmed the design brief. (file: r6/image-to-code/SKILL.md) +- product-design:index: Use to discover specific skills for the Product Design plugin, when it is at-mentioned directly, or for any mentions of potentially relevant work, including: UX research; product, screen, or flow audits; visual ideation; app or interface design, red (file: r6/index/SKILL.md) +- spreadsheets:Spreadsheets: Use this skill when a user requests to create, modify, analyze, visualize, or work with spreadsheet files (`.xlsx`, `.xls`, `.csv`, `.tsv`) or Google Sheets-targeted spreadsheet artifacts with formulas, formatting, charts, tables, and recalculation (file: r7/spreadsheets/26.630.12135/skills/spreadsheets/SKILL.md) +- template-creator:template-creator: Create or update a reusable personal Codex artifact-template skill. Use when the user invokes $template-creator or asks in natural language to create a template using, from, or based on an attached Word document, PowerPoint presentation, or Excel w (file: r7/template-creator/26.630.12135/skills/template-creator/SKILL.md) +### How to use skills +- Discovery: The list above is the skills available in this session (name + description + short path). Skill bodies live on disk at the listed paths after expanding the matching alias from `### Skill roots`. +- Trigger rules: If the user names a skill (with `$SkillName` or plain text) OR the task clearly matches a skill's description shown above, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned. +- Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback. +- How to use a skill (progressive disclosure): + 1) After deciding to use a skill, the main agent must expand the listed short `path` with the matching alias from `### Skill roots`, then open and read its `SKILL.md` completely before taking task actions. If a read is truncated or paginated, continue until EOF. + 2) When `SKILL.md` references relative paths (e.g., `scripts/foo.py`), resolve them relative to the directory containing that expanded `SKILL.md` first, and only consider other paths if needed. + 3) If `SKILL.md` points to extra folders such as `references/`, use its routing instructions to identify the files required for the task. The main agent must read each required instruction or reference file itself before acting on it. Do not delegate reading, summarizing, or interpreting skill instructions to a subagent. Subagents may still perform task work when the selected skill allows it. + 4) If `scripts/` exist, prefer running or patching them instead of retyping large code blocks. + 5) If `assets/` or templates exist, reuse them instead of recreating from scratch. +- Coordination and sequencing: + - If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them. + - Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why. +- Context hygiene: + - Progressive disclosure applies to selecting relevant files, not partially reading a selected instruction file. Do not load unrelated references, scripts, or assets. + - Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked. + - When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice. +- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue. + +## Plugins +A plugin is a local bundle of skills, MCP servers, and apps. +### How to use plugins +- Skill naming: If a plugin contributes skills, those skill entries are prefixed with `plugin_name:` in the Skills list. +- MCP naming: Plugin-provided MCP tools keep standard MCP identifiers such as `mcp__server__tool`; use tool provenance to tell which plugin they come from. +- Trigger rules: If the user explicitly names a plugin, prefer capabilities associated with that plugin for that turn. +- Relationship to capabilities: Plugins are not invoked directly. Use their underlying skills, MCP tools, and app tools to help solve the task. +- Relevance: Determine what a plugin can help with from explicit user mention or from the plugin-associated skills, MCP tools, and apps exposed elsewhere in this turn. +- Missing/blocked: If the user requests a plugin that does not have relevant callable capabilities for the task, say so briefly and continue with the best fallback. + + +--- + + +**User** + +# Files mentioned by the user: + +## AUDIT_REPORT_2026-07-04.md: /Users/zuozuo/Documents/AUDIT_REPORT_2026-07-04.md + +## My request for Codex: +这个目前这个app的问题, 我现在想要去尝试修复一部分性能问题,我该怎么快速出发,然后一点一点把这个腐朽的项目给复活 + +--- + + +**OpenAI Codex** + +我先看一下审计报告和项目结构,再给你一套“马上能开干”的修复路线。 + +--- + + +**OpenAI Codex** + +审计报告只有 48 行,信息比较聚焦。我再快速扫一下当前仓库的热点文件、支付/SVGA/WebView 位置和 analyze 基线,确保建议不是拍脑袋。 + +--- + + +**OpenAI Codex** + +我会再抓关键文件的精确片段和当前 `flutter analyze` 概况,方便把“第一刀”定准。 + +--- + + +**OpenAI Codex** + +`flutter analyze` 会先 resolve 依赖,它刚才把锁文件环境刷新了。我检查一下有没有改动,避免把建议和副作用混在一起。 + +--- + + +**OpenAI Codex** + +我不会保留这次分析命令产生的 `pubspec.lock` 源地址/版本刷新副作用,先还原。 + +--- + + +**OpenAI Codex** + +我再看一下入口 Provider 注册方式,这会影响“第一批止血改动”的落点。 + +--- + + +**OpenAI Codex** + +建议你不要从“大重构”开始。这个项目现在要复活,最快路径是: + +## 先定原则 + +1. **先止血,再优化,再重构** + - 先修会掉单、崩溃、内存无限涨的问题。 + - 再做可量化性能优化。 + - 最后慢慢拆 God Class。 + +2. **不要试图一次性清理 1000+ analyze 问题** + - 我刚跑了全量 `flutter analyze`,现在是 **1141 issues**,大量来自 `local_packages` 和失效测试。 + - 这不是第一优先级。 + - 第一阶段只要求:**改到的文件不能引入新问题**。 + +3. **每次只做一个小 PR** + - 一个 PR 只解决一类问题。 + - 不夹带 UI 文案、样式重构、命名洁癖。 + +--- + +## 我建议的启动顺序 + +### 第 0 步:建立可跑的基线 + +先不要让 `flutter analyze` 改 `pubspec.lock`,用: + +```bash +git checkout -b codex/perf-revival-start + +flutter analyze --no-pub lib +flutter test --no-pub +``` + +如果全量测试太烂,就先只跑改动相关测试。后面逐步补。 + +--- + +## 第一批先修这 4 个点 + +### PR1:修 SVGA 缓存失控,这是最贴近性能的第一刀 + +重点文件: + +- `/Users/zuozuo/Documents/Project/haiyi/chatapp3-flutter/lib/shared/tools/sc_gift_vap_svga_manager.dart` +- `/Users/zuozuo/Documents/Project/haiyi/chatapp3-flutter/lib/ui_kit/widgets/headdress/headdress_widget.dart` +- `/Users/zuozuo/Documents/Project/haiyi/chatapp3-flutter/lib/ui_kit/widgets/room/anim/room_entrance_widget.dart` +- `/Users/zuozuo/Documents/Project/haiyi/chatapp3-flutter/lib/ui_kit/widgets/svga/sc_network_svga_widget.dart` +- `/Users/zuozuo/Documents/Project/haiyi/chatapp3-flutter/lib/modules/user/profile/profile_gift_wall_detail_page.dart` + +现在的问题是:`SCGiftVapSvgaManager` 里面已经有缓存和淘汰逻辑,但外部组件还在直接写: + +```dart +SCGiftVapSvgaManager().videoItemCache[resource] = videoItem; +``` + +第一刀就是: + +- `videoItemCache` 改私有。 +- 暴露统一方法,例如 `loadSvgaEntity()` / `removeSvgaEntity()`。 +- 所有外部组件都走统一入口。 +- 加引用计数,避免 `clearMemoryCache()` 把正在播放的 SVGA dispose 掉。 + +这是最容易看到收益的一刀:房间挂久了内存不再无上限涨。 + +--- + +### PR2:修支付上下文问题,别再让页面 context 决定是否入账 + +重点文件: + +- `/Users/zuozuo/Documents/Project/haiyi/chatapp3-flutter/lib/services/payment/google_payment_manager.dart` +- `/Users/zuozuo/Documents/Project/haiyi/chatapp3-flutter/lib/services/payment/apple_payment_manager.dart` + +现在两个支付管理器都有: + +```dart +late BuildContext context; +``` + +这很危险。支付回调回来时页面可能已经关了,`Provider.of(context)` 会炸,服务端核销就没发出去。 + +修法: + +- 支付核销逻辑不依赖页面 `BuildContext`。 +- UI 刷新、toast、loading 全部降级为可选。 +- 核心链路顺序固定为: + +```text +收到购买结果 +-> 校验凭证 +-> 服务端入账成功 +-> completePurchase +-> 刷新余额 UI +``` + +不要反过来。 + +--- + +### PR3:修 iOS 掉单 + +重点文件: + +- `/Users/zuozuo/Documents/Project/haiyi/chatapp3-flutter/lib/services/payment/apple_payment_manager.dart` +- `/Users/zuozuo/Documents/Project/haiyi/chatapp3-flutter/lib/main.dart` + +现在 iOS 的问题更严重:验证失败后也可能 `completePurchase`,这会让 StoreKit 不再重放,变成人工补单。 + +修法: + +- 只有服务端确认成功,或者确认订单已存在,才 `completePurchase`。 +- iOS 也加启动补偿,类似 Android 的 `reconcilePendingGooglePurchases()`。 +- `purchaseStream` 不要只在充值页注册,要在 app 启动阶段全局注册。 + +--- + +### PR4:WebView token 注入加白名单 + +重点文件: + +- `/Users/zuozuo/Documents/Project/haiyi/chatapp3-flutter/lib/modules/webview/webview_page.dart` + +现在任意 H5 都能拿到: + +```dart +Authorization: Bearer xxx +``` + +第一版不要搞复杂权限系统,先做硬白名单: + +```text +只有可信业务域名允许注入 getAuth() +其他 URL 不注入 token +``` + +这不是性能问题,但这是账号级风险,应该早修。 + +--- + +## 第二批才开始真正做性能优化 + +### 1. 房间页减少整体重建 + +优先看这些大文件: + +- `/Users/zuozuo/Documents/Project/haiyi/chatapp3-flutter/lib/services/audio/rtc_manager.dart`:7078 行 +- `/Users/zuozuo/Documents/Project/haiyi/chatapp3-flutter/lib/services/audio/rtm_manager.dart`:5997 行 +- `/Users/zuozuo/Documents/Project/haiyi/chatapp3-flutter/lib/modules/room/voice_room_page.dart`:1437 行 +- `/Users/zuozuo/Documents/Project/haiyi/chatapp3-flutter/lib/ui_kit/widgets/room/room_user_info_card.dart`:2383 行 +- `/Users/zuozuo/Documents/Project/haiyi/chatapp3-flutter/lib/ui_kit/widgets/room/room_msg_item.dart`:2234 行 +- `/Users/zuozuo/Documents/Project/haiyi/chatapp3-flutter/lib/modules/gift/gift_page.dart`:2421 行 + +目标不是马上拆完,而是先把热点 Consumer 改成: + +```dart +Selector +context.select(...) +``` + +原则:谁只关心一个字段,就不要监听整个 manager。 + +--- + +### 2. 大 JSON 解析移出主线程 + +重点文件: + +- `/Users/zuozuo/Documents/Project/haiyi/chatapp3-flutter/lib/shared/data_sources/sources/remote/net/api.dart` + +项目里 `jsonDecode` 很多,网络大响应主线程解析会卡 UI。 + +可以先做一个统一入口: + +```dart +Future parseJson(String response) async { + if (response.length < 50 * 1024) { + return jsonDecode(response); + } + return compute(_parseAndDecode, response); +} +``` + +先替换网络层统一 decode,不要到处散改。 + +--- + +### 3. 图片资产瘦身 + +报告里说 `sc_images` 有 84MB,39 张超过 500KB。 + +第一步先列出来: + +```bash +find sc_images -type f -size +500k -print +``` + +然后按收益排序: + +- 大 PNG 转 WebP。 +- 不常用的大图改远程加载。 +- 不在首屏用的图不要提前 decode。 +- 房间常驻页面的图优先处理。 + +--- + +## 第三批才做架构复活 + +不要一上来拆 `rtc_manager.dart` / `rtm_manager.dart`,会很容易炸。 + +正确方式是: + +1. 新功能禁止再往这两个 God Class 里塞。 +2. 每次改一个功能,就顺手抽一个小 controller/view model。 +3. 房间域慢慢分成: + - room session + - mic seat + - gift animation + - message stream + - music + - rocket/red packet +4. 每抽一块,补最小测试。 + +--- + +## 最快能落地的今日计划 + +如果今天就开干,我建议顺序是: + +1. 建分支。 +2. 修 `SCGiftVapSvgaManager` 缓存外部裸写。 +3. 改 4 个 SVGA 调用点。 +4. 跑: + +```bash +flutter analyze --no-pub lib/shared/tools/sc_gift_vap_svga_manager.dart \ + lib/ui_kit/widgets/headdress/headdress_widget.dart \ + lib/ui_kit/widgets/room/anim/room_entrance_widget.dart \ + lib/ui_kit/widgets/svga/sc_network_svga_widget.dart \ + lib/modules/user/profile/profile_gift_wall_detail_page.dart +``` + +5. 真机进房间压测: + - 连续进出房间 + - 连续播放头饰/进场/礼物墙 SVGA + - 切后台再回来 + - 观察内存是否持续上涨 + +--- + +## 一句话路线 + +**第一周别重构,先做“止血 PR”:SVGA 缓存、支付掉单、WebView token。第二周开始用 Selector 拆房间页重建和主线程 JSON。第三周以后再慢慢拆 RTC/RTM God Class。** + +如果你要我直接动手,我建议第一刀就从 **SVGA 缓存失控修复** 开始。 + +--- diff --git a/.waylog/history/2026-07-07_13-00Z-建议你不要从_大重构_开始_这个项目现在要复活_最快路径是__先定原则_先止血_再优化_再重构_先修.md b/.waylog/history/2026-07-07_13-00Z-建议你不要从_大重构_开始_这个项目现在要复活_最快路径是__先定原则_先止血_再优化_再重构_先修.md new file mode 100644 index 0000000..5100e81 --- /dev/null +++ b/.waylog/history/2026-07-07_13-00Z-建议你不要从_大重构_开始_这个项目现在要复活_最快路径是__先定原则_先止血_再优化_再重构_先修.md @@ -0,0 +1,546 @@ +# 建议你不要从“大重构”开始。这个项目现在要复活,最快路径是: +先定原则 +先止血,再优化,再重构 +先修 +_Exported on 07/07/2026 at 22:07:15 GMT+8 from OpenAI Codex via WayLog_ + + +**OpenAI Codex** + + +Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `danger-full-access`: No filesystem sandboxing - all commands are permitted. Network access is enabled. +Approval policy is currently never. Do not provide the `sandbox_permissions` for any reason, commands will be rejected. + +# Codex desktop context +- You are running inside the Codex (desktop) app, which allows some additional features not available in the CLI alone: + +### Images/Visuals/Files +- In the app, the model can display images and videos using standard Markdown image syntax: ![alt](url) +- When sending or referencing a local image or video, always use an absolute filesystem path in the Markdown image tag (e.g., ![alt](/absolute/path.png)); relative paths and plain text will not render the media. +- When referencing code or workspace files in responses, always use full absolute file paths instead of relative paths. +- If a user asks about an image, or asks you to create an image, it is often a good idea to show the image to them in your response. +- Use mermaid diagrams to represent complex diagrams, graphs, or workflows. Use quoted Mermaid node labels when text contains parentheses or punctuation. +- Return web URLs as Markdown links (e.g., [label](https://example.com)). + +### Workspace Dependencies +- For sheets, slides, and documents, call `load_workspace_dependencies` to find the bundled runtime and libraries. + +### Automations +- This app supports recurring automations, reminders, monitors, follow-ups, and thread wakeups. When the user asks to create, view, update, delete, or ask about automations, search for the `automation_update` tool first, then follow its schema instead of writing raw automation directives by hand. +- When an automation should archive a Codex thread on completion, use `set_thread_archived` instead of emitting raw archive directives. + +### Thread Coordination +- When the user asks to create, fork, inspect, continue, hand off, pin, archive, rename, or otherwise manage Codex threads, search for the relevant thread tool first: `create_thread`, `fork_thread`, `list_threads`, `read_thread`, `send_message_to_thread`, `handoff_thread`, `set_thread_pinned`, `set_thread_archived`, or `set_thread_title`. +- Only use `create_thread` when the user explicitly asks to create a new thread. Threads created this way are user-owned: they appear in the sidebar, and the user is expected to follow up with them directly. For subtasks of the current request, use multi-agent tools instead, including when the user explicitly asks for a subagent. +- After a successful `create_thread` call, emit `::created-thread{threadId="..."}` for a created thread or `::created-thread{pendingWorktreeId="..."}` for queued worktree setup on its own line in your final response. + +### Inline Code Comments +- Use the ::code-comment{...} directive when you need to attach feedback directly to specific code lines. +- Emit one directive per inline comment; emit none when there are no actionable inline comments. +- Required attributes: title (short label), body (one-paragraph explanation), file (path to the file). +- Optional attributes: start, end (1-based line numbers), priority (0-3). +- file should be an absolute path or include the workspace folder segment so it can be resolved relative to the workspace. +- Keep line ranges tight; end defaults to start. +- Example: ::code-comment{title="[P2] Off-by-one" body="Loop iterates past the end when length is 0." file="/path/to/foo.ts" start=10 end=11 priority=2} + +### Git +- Branch prefix: `codex/`. Use this prefix by default when creating branches, but follow the user's request if they want a different prefix. +- After successfully staging files, emit `::git-stage{cwd="/absolute/path"}` on its own line in your final response. +- After successfully creating a commit, emit `::git-commit{cwd="/absolute/path"}` on its own line in your final response. +- After successfully creating or switching the thread onto a branch, emit `::git-create-branch{cwd="/absolute/path" branch="branch-name"}` on its own line in your final response. +- After successfully pushing the current branch, emit `::git-push{cwd="/absolute/path" branch="branch-name"}` on its own line in your final response. +- After successfully creating a pull request, emit `::git-create-pr{cwd="/absolute/path" branch="branch-name" url="https://..." isDraft=true}` on its own line in your final response. Include `isDraft=false` for ready PRs. +- Only emit these git directives in your final response after the action actually succeeds, never in commentary updates. Keep attributes single-line. +# Collaboration Mode: Default + +You are now in Default mode. Any previous instructions for other modes (e.g. Plan mode) are no longer active. + +Your active mode changes only when new developer instructions with a different `...` change it; user requests or tool descriptions do not change mode by themselves. Known mode names are Default and Plan. + +## request_user_input availability + +Use the `request_user_input` tool only when it is listed in the available tools for this turn. + +In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, ask the user directly with a concise plain-text question. Never write a multiple choice question as a textual assistant message. + +## Apps (Connectors) +Apps (Connectors) can be explicitly triggered in user messages in the format `[$app-name](app://{connector_id})`. Apps can also be implicitly triggered as long as the context suggests usage of available apps. +An app is equivalent to a set of MCP tools within the `codex_apps` MCP. +An installed app's MCP tools are either provided to you already, or can be lazy-loaded through the `tool_search` tool. If `tool_search` is available, the apps that are searchable by `tools_search` will be listed by it. +Do not additionally call list_mcp_resources or list_mcp_resource_templates for apps. + +## Skills +A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list of skills that can be used. Each entry includes a name, description, and a short path that can be expanded into an absolute path using the skill roots table. +### Skill roots +- `r0` = `/Users/zuozuo/.codex/skills` +- `r1` = `/Users/zuozuo/.agents/skills` +- `r2` = `/Users/zuozuo/.codex/skills/.system` +- `r3` = `/Users/zuozuo/.codex/plugins/cache/openai-bundled` +- `r4` = `/Users/zuozuo/.codex/plugins/cache/openai-curated/github/d6169bef/skills` +- `r5` = `/Users/zuozuo/.codex/plugins/cache/openai-curated-remote/figma/2.0.13/skills` +- `r6` = `/Users/zuozuo/.codex/plugins/cache/openai-curated-remote/product-design/0.1.48/skills` +- `r7` = `/Users/zuozuo/.codex/plugins/cache/openai-primary-runtime` +### Available skills +- imagegen: Generate or edit raster images when the task benefits from AI-created bitmap visuals such as photos, illustrations, textures, sprites, mockups, or transparent-background cutouts. Use when Codex should create a brand-new image, transform an existing i (file: r2/imagegen/SKILL.md) +- openai-docs: Use when the user asks how to build with OpenAI products or APIs, asks about Codex itself or choosing Codex surfaces, needs up-to-date official documentation with citations, help choosing the latest model for a use case, or model upgrade and prompt (file: r2/openai-docs/SKILL.md) +- plugin-creator: Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, valid manifest defaults, and personal-marketplace entries by default. Use when Codex needs to create a new personal plugin, a (file: r2/plugin-creator/SKILL.md) +- skill-creator: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Codex's capabilities with specialized knowledge, workflows, or tool integrations. (file: r2/skill-creator/SKILL.md) +- skill-installer: Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos). (file: r2/skill-installer/SKILL.md) +- browser:control-in-app-browser: Control the in-app Browser. Use to open, navigate, inspect, test, click, type, screenshot, or verify local targets such as localhost, 127.0.0.1, ::1, file://, the current in-app browser tab, and websites shown side by side inside Codex. (file: r3/browser/26.623.141536/skills/control-in-app-browser/SKILL.md) +- documents:documents: Create, edit, redline, and comment on `.docx`, Word, and Google Docs-targeted document artifacts inside the container, with a strict render-and-verify workflow. Use `render_docx.py` to generate page PNGs (and optional PDF) for visual QA, then itera (file: r7/documents/26.630.12135/skills/documents/SKILL.md) +- dws: 管理钉钉产品能力(AI表格/AI搜问/日历/通讯录/群聊与机器人/待办/审批/考勤/日志/DING消息/开放平台文档/钉钉文档/钉钉云盘/AI听记/邮箱/在线电子表格/知识库等)。当用户需要操作表格数据、管理日程会议、模糊找人/查谁负责某事项、查询通讯录、管理群聊、机器人发消息、创建待办、提交审批、查看考勤、提交日报周报(钉钉日志模版)、读写钉钉文档、上传下载云盘文件、查询听记纪要、收发邮件、读写在线电子表格(axls)、管理钉钉知识库时使用。 (file: r1/dws/SKILL.md) +- dws: 管理钉钉产品能力(AI表格/AI搜问/日历/通讯录/群聊与机器人/待办/审批/考勤/日志/DING消息/开放平台文档/钉钉文档/钉钉云盘/AI听记/邮箱/在线电子表格/知识库等)。当用户需要操作表格数据、管理日程会议、模糊找人/查谁负责某事项、查询通讯录、管理群聊、机器人发消息、创建待办、提交审批、查看考勤、提交日报周报(钉钉日志模版)、读写钉钉文档、上传下载云盘文件、查询听记纪要、收发邮件、读写在线电子表格(axls)、管理钉钉知识库时使用。 (file: r0/dws/SKILL.md) +- figma:figma-code-connect: Creates and maintains Figma Code Connect template files that map Figma components to code snippets. Use when the user mentions Code Connect, Figma component mapping, design-to-code translation, or asks to create/update .figma.ts or .figma.js files. (file: r5/figma-code-connect/SKILL.md) +- figma:figma-create-new-file: **MANDATORY prerequisite** — you MUST invoke this skill BEFORE every `create_new_file` tool call. NEVER call `create_new_file` directly without loading this skill first. Trigger whenever the user wants a new blank Figma file — a new design, FigJam, o (file: r5/figma-create-new-file/SKILL.md) +- figma:figma-generate-design: Use this skill alongside figma-use when the task involves translating an application page, view, or multi-section layout into Figma. Triggers: 'write to Figma', 'create in Figma from code', 'push page to Figma', 'take this app/page and build it in Fi (file: r5/figma-generate-design/SKILL.md) +- figma:figma-generate-diagram: MANDATORY prerequisite — load this skill BEFORE every `generate_diagram` tool call. NEVER call `generate_diagram` directly without loading this skill first. Trigger whenever the user asks to create, generate, draw, render, sketch, or build a diagram (file: r5/figma-generate-diagram/SKILL.md) +- figma:figma-generate-library: Build or update a professional-grade design system in Figma from a codebase. Use when the user wants to create variables/tokens, build component libraries, create individual components with proper variant sets and variable bindings, set up theming (file: r5/figma-generate-library/SKILL.md) +- figma:figma-implement-motion: Translates Figma motion and animations into production-ready application code. Use when implementing animation/motion from a Figma design — user mentions "implement this motion", "add animation from Figma", "animate this component", provides a Figma (file: r5/figma-implement-motion/SKILL.md) +- figma:figma-swiftui: SwiftUI ↔ Figma translation. Use whenever the user mentions Swift, SwiftUI, iOS, iPhone, or iPad — in EITHER direction — translating a Figma design into SwiftUI (design → code), or pushing SwiftUI views / screens / tokens back into a Figma file (code (file: r5/figma-swiftui/SKILL.md) +- figma:figma-use: **MANDATORY prerequisite** — you MUST invoke this skill BEFORE every `use_figma` tool call. NEVER call `use_figma` directly without loading this skill first. Skipping it causes common, hard-to-debug failures. Trigger whenever the user wants to perf (file: r5/figma-use/SKILL.md) +- figma:figma-use-figjam: This skill helps agents use Figma's use_figma MCP tool in the FigJam context. Can be used alongside figma-use which has foundational context for using the use_figma tool. (file: r5/figma-use-figjam/SKILL.md) +- figma:figma-use-motion: Motion / animation context for the `use_figma` MCP tool — animating Figma nodes via manual keyframes, animation styles, easing, and timeline duration. Load alongside figma-use whenever a task involves adding, editing, or inspecting animation on a nod (file: r5/figma-use-motion/SKILL.md) +- figma:figma-use-slides: This skill helps agents use Figma's use_figma MCP tool in the Slides context. Can be used alongside figma-use which has foundational context for using the use_figma tool. (file: r5/figma-use-slides/SKILL.md) +- github:gh-address-comments: Address actionable GitHub pull request review feedback. Use when the user wants to inspect unresolved review threads, requested changes, or inline review comments on a PR, then implement selected fixes. Use the GitHub app for PR metadata and flat co (file: r4/gh-address-comments/SKILL.md) +- github:gh-fix-ci: Use when a user asks to debug or fix failing GitHub PR checks that run in GitHub Actions. Use the GitHub app from this plugin for PR metadata and patch context, and use `gh` for Actions check and log inspection before implementing any approved fix. (file: r4/gh-fix-ci/SKILL.md) +- github:github: Triage and orient GitHub repository, pull request, and issue work through the connected GitHub app. Use when the user asks for general GitHub help, wants PR or issue summaries, or needs repository context before choosing a more specific GitHub workflo (file: r4/github/SKILL.md) +- github:yeet: Publish local changes to GitHub by confirming scope, committing intentionally, pushing the branch, and opening a draft PR through the GitHub app from this plugin, with `gh` used only as a fallback where connector coverage is insufficient. (file: r4/yeet/SKILL.md) +- lark-approval: 飞书审批:当前用户审批的查询与全部处理操作,覆盖待本人审批的任务与本人发起的实例。审批待办不是飞书任务(任务类待办走 lark-task);不负责创建审批定义和发起新审批。 (file: r1/lark-approval/SKILL.md) +- lark-apps: 妙搭(Spark/Miaoda)应用开发与托管:应用创建、HTML静态站点发布、本地全栈开发、云端生成迭代。当用户要开发/新建一个系统·工具·平台·应用,或要本地开发 / 云端开发 / 修改 / 部署 / 发布 / 上线 / 拿可分享链接,或用 HTML 做页面·网站给人看,或提到妙搭/Spark/Miaoda、应用数据库、可见范围时使用。不负责普通云盘文件上传(lark-drive)、飞书文档编辑(lark-doc)、原生幻灯片创建(lark-slides)。 (file: r1/lark-apps/SKILL.md) +- lark-attendance: 飞书考勤打卡:查询自己的考勤打卡记录 (file: r1/lark-attendance/SKILL.md) +- lark-base: 飞书多维表格(Base)操作:建表、字段、记录、视图、统计、公式/lookup、表单、仪表盘、workflow、角色权限;遇到 Base/多维表格/bitable 或 /base/ 链接时使用。文件导入转 lark-drive,认证/授权转 lark-shared。 (file: r1/lark-base/SKILL.md) +- lark-calendar: 飞书日历:管理日历日程和会议室。查看/搜索日程、创建/更新日程、管理参会人、查询忙闲和推荐时段、预定会议室。当用户需要查看日程安排、创建/修改会议、查询/预定会议室时使用。不负责:查询过去的视频会议记录(走 lark-vc)、待办任务(走 lark-task)。 (file: r1/lark-calendar/SKILL.md) +- lark-contact: 飞书 / Lark 通讯录:按姓名 / 邮箱解析成 open_id,或按 open_id 反查姓名 / 部门 / 邮箱 / 联系方式 / 个人状态 / 签名。当用户提到某人姓名要下一步发消息 / 排日程,或拿到 open_id 想查具体信息时使用。不负责部门树遍历、按部门列员工、组织架构图,这类需求走原生 OpenAPI。 (file: r1/lark-contact/SKILL.md) +- lark-doc: 飞书云文档(Docx / Wiki 文档,v2 API):读取和编辑飞书文档内容。当用户给出文档 URL 或 token,或需要查看、创建、编辑文档、插入或下载文档图片附件时使用。文档中嵌入的电子表格、多维表格、画板,先用本 skill 提取 token 再切到对应 skill。当用户给出 doubao.com 的 /docx/ 或 /wiki/ URL/token 时,也应直接使用本 skill;路由依据是 URL 路径模式和 token,而不是域名。不负责文档评论管理,也不负责表格或 Bas (file: r1/lark-doc/SKILL.md) +- lark-drive: 飞书云空间(云盘/云存储):管理云空间(云盘/云存储)中的文件和文件夹。上传和下载文件、创建文件夹、复制/移动/删除文件、查看文件元数据、管理文档评论、管理文档权限、订阅用户评论变更事件、修改文件标题(docx、sheet、bitable、file、folder、wiki);也负责把本地 Word/Markdown/Excel/CSV/PPTX 以及 Base 快照(.base)导入为飞书在线云文档(docx、sheet、bitable、slides)。当用户需要上传或下载文件、整理云空间( (file: r1/lark-drive/SKILL.md) +- lark-event: Lark/Feishu real-time event listening / subscribing / consuming: stream events as NDJSON via `lark-cli event consume ` (covers IM messages/reactions/chat changes, VC meeting ended, Minutes generated, Whiteboard updated, etc.). Use for Lark (file: r1/lark-event/SKILL.md) +- lark-im: 飞书即时通讯:收发消息和管理群聊。发送和回复消息、搜索聊天记录、管理群聊成员、上传下载图片和文件(支持大文件分片下载)、管理表情回复、发送应用内/短信/电话加急。当用户需要发消息、查看或搜索聊天记录、下载聊天中的文件、查看群成员、搜索群、创建群聊或话题群、管理标记数据、管理 Feed 置顶(添加/移除/查询置顶会话)、管理标签数据时使用。 (file: r1/lark-im/SKILL.md) +- lark-mail: 飞书邮箱 — draft, compose, send, reply, forward, read, and search emails; manage drafts, folders, labels, contacts, attachments, and mail rules. Use when user mentions 起草邮件, 写一封邮件, 拟邮件, 草稿, 发通知邮件, 发送邮件, 发邮件, 回复邮件, 转发邮件, 查看邮件, 看邮件, 读邮件, 搜索邮件, 查邮件, 收件箱, (file: r1/lark-mail/SKILL.md) +- lark-markdown: 飞书 Markdown:查看、创建、上传、编辑和比较 Markdown 文件。当用户需要创建或编辑 Markdown 文件、读取、修改、局部 patch 或比较差异时使用。不负责将 Markdown 导入为飞书在线文档,也不负责文件搜索、权限、评论、移动、删除等云空间管理操作。 (file: r1/lark-markdown/SKILL.md) +- lark-minutes: 飞书妙记:搜索妙记列表、查看妙记基础信息、下载妙记音视频文件、上传音视频生成妙记、更新妙记标题、替换说话人。当需要获取、操作或者生成妙记时使用。也支持将本地音视频文件转成纪要和逐字稿(优先使用本 skill,不要用 ffmpeg/whisper 本地转写)。不负责:获取会议关联妙记、纪要/逐字稿内容获取走 lark-vc (file: r1/lark-minutes/SKILL.md) +- lark-okr: 飞书 OKR:管理目标与关键结果。查看和编辑 OKR 周期、目标(Objective)、关键结果(Key Result)、对齐关系、量化指标和进展记录。当用户需要查看或创建 OKR、管理目标和关键结果、查看对齐关系时使用。 (file: r1/lark-okr/SKILL.md) +- lark-openapi-explorer: 飞书/Lark 原生 OpenAPI 探索:从官方文档库中挖掘未经 CLI 封装的原生 OpenAPI 接口。当用户的需求无法被现有 lark-* skill 或 lark-cli 已注册命令满足,需要查找并调用原生飞书 OpenAPI 时使用。 (file: r1/lark-openapi-explorer/SKILL.md) +- lark-shared: Use when first setting up lark-cli, running auth login, switching user/bot identity (--as), handling permission denied or scope errors, needing to update lark-cli, or seeing _notice in JSON output. (file: r1/lark-shared/SKILL.md) +- lark-sheets: 飞书电子表格:创建和操作电子表格。支持创建表格、管理工作表与行列结构(增删/合并/调整尺寸/隐藏/冻结)、读写单元格(值/公式/样式/批注/单元格图片)、查找替换、多操作原子批量更新,以及图表、透视表、条件格式、筛选器、迷你图、浮动图片等对象的创建与维护。当用户需要创建电子表格、管理工作表、批量读写或编辑数据、统计汇总与可视化、表格美化、公式计算(含 Excel 公式迁移)等任务时使用。若用户是想按名称或关键词搜索云空间(云盘/云存储)里的表格文件,请改用 lark-drive 的 drive (file: r1/lark-sheets/SKILL.md) +- lark-skill-maker: 创建 lark-cli 的自定义 Skill。当用户需要把飞书 API 操作封装成可复用的 Skill(包装原子 API 或编排多步流程)时使用。 (file: r1/lark-skill-maker/SKILL.md) +- lark-slides: 飞书幻灯片:创建和编辑幻灯片。创建演示文稿、读取幻灯片内容、管理幻灯片页面(创建、删除、读取、局部替换)。当用户需要创建或编辑幻灯片、读取或修改单个页面时使用。当用户给出 doubao.com 的 /slides/ URL/token 时,也应直接使用本 skill,不要因为域名不是飞书而回退到 WebFetch;路由依据是 URL 路径模式和 token,而不是域名。不负责:云文档内容编辑(走 lark-doc)、云文档里的独立画板对象(走 lark-whiteboard,注意 slide (file: r1/lark-slides/SKILL.md) +- lark-task: 飞书任务:管理任务、清单和任务智能体。创建待办任务、查看和更新任务状态、拆分子任务、组织任务清单、分配协作成员、上传任务附件、注册或注销任务智能体、更新任务智能体的主页数据、写入智能体任务记录。当用户需要创建待办事项、查看任务列表、跟踪任务进度、管理项目清单或给他人分配任务、为任务上传附件文件、注册注销任务智能体、更新智能体主页数据、写入任务记录时使用。 (file: r1/lark-task/SKILL.md) +- lark-vc: 飞书视频会议:搜索历史会议记录、查询会议纪要(总结/待办/章节/逐字稿)、查询参会人快照。当用户查询已结束的会议、获取会议产物(纪要/妙记)、查看参会人时使用;查询未来日程走 lark-calendar。不负责:Agent 真实入会/离会、会中实时事件(走 lark-vc-agent)。 (file: r1/lark-vc/SKILL.md) +- lark-vc-agent: 飞书视频会议:让机器人代当前用户加入/离开正在进行的会议,并读取会议期间的实时事件(参会人加入与离开、发言、聊天、屏幕共享等)。1. 用户提供 9 位会议号、要求代为入会或离会时使用 +meeting-join / +meeting-leave——会真实产生入会/离会记录。2. 会议进行中用户想知道“谁加入了”“谁离开了”“谁在发言”“有人共享屏幕吗”等会中动态时,机器人入会后用 +meeting-events 读取事件时间线。3. 典型场景:参会机器人、会中助手、代为旁听、代为参会。前提: (file: r1/lark-vc-agent/SKILL.md) +- lark-whiteboard: 飞书画板:查询和编辑飞书云文档中的画板。支持导出画板为预览图片、导出原始节点结构、使用 DSL(转成 OpenAPI 格式)、PlantUML/Mermaid 格式更新画板内容。 当用户需要查看画板内容、导出画板图片、编辑画板,或是需要可视化表达架构、流程、组织关系、时间线、因果、对比等结构化信息时使用此 skill,无论是否提及"画板"。 ⚠️ 原 `lark-whiteboard-cli` skill 已合并至本 skill,若 skill 列表中同时存在 `lark-whiteboard- (file: r1/lark-whiteboard/SKILL.md) +- lark-wiki: 飞书知识库:管理知识空间、空间成员和文档节点。创建和查询知识空间、查看和管理空间成员、管理节点层级结构、在知识库中组织文档和快捷方式。当用户需要在知识库中查找或创建文档、浏览知识空间结构、查看或管理空间成员、移动或复制节点时使用。当用户给出 doubao.com 的 /wiki/ URL/token 时,也应直接使用本 skill,不要因为域名不是飞书而回退到 WebFetch;路由依据是 URL 路径模式和 token,而不是域名。不负责:上传文件到知识库节点下(走 lark-drive) (file: r1/lark-wiki/SKILL.md) +- lark-workflow-meeting-summary: 会议纪要整理工作流:汇总指定时间范围内的会议纪要并生成结构化报告。当用户需要整理会议纪要、生成会议周报、回顾一段时间内的会议内容时使用。 (file: r1/lark-workflow-meeting-summary/SKILL.md) +- lark-workflow-standup-report: 日程待办摘要:编排 calendar +agenda 和 task +get-my-tasks,生成指定日期的日程与未完成任务摘要。适用于了解今天/明天/本周的安排。 (file: r1/lark-workflow-standup-report/SKILL.md) +- pdf:pdf: Read, create, inspect, render, and verify PDF files where visual layout matters. Use Poppler rendering plus Python tools such as reportlab, pdfplumber, and pypdf for generation and extraction. (file: r7/pdf/26.630.12135/skills/pdf/SKILL.md) +- presentations:Presentations: Create or edit PowerPoint or Google Slides decks (file: r7/presentations/26.630.12135/skills/presentations/SKILL.md) +- product-design:audit: Audit or critique a product flow, journey, workflow, funnel, onboarding path, checkout path, settings path, screen, or multi-step product experience by capturing screenshots first, placing them in Figma or a local folder, then reporting UX, design, (file: r6/audit/SKILL.md) +- product-design:get-context: Mandatory design-brief gate for Product Design build and design workflows. Use before ideation, prototyping, image-to-code builds, redesigns, or product UI work to clarify missing product, visual, and interactivity context or play back the supplied (file: r6/get-context/SKILL.md) +- product-design:ideate: Generate image-based visual alternatives, remixes, or concept directions after Product Design get-context has confirmed the design brief. Use when the user asks for design variants, visual exploration, remixes, or image-generated approaches from provi (file: r6/ideate/SKILL.md) +- product-design:image-to-code: Implement a selected image, screenshot, mockup, or Image Gen reference as a faithful responsive frontend after Product Design get-context has confirmed the design brief. (file: r6/image-to-code/SKILL.md) +- product-design:index: Use to discover specific skills for the Product Design plugin, when it is at-mentioned directly, or for any mentions of potentially relevant work, including: UX research; product, screen, or flow audits; visual ideation; app or interface design, red (file: r6/index/SKILL.md) +- spreadsheets:Spreadsheets: Use this skill when a user requests to create, modify, analyze, visualize, or work with spreadsheet files (`.xlsx`, `.xls`, `.csv`, `.tsv`) or Google Sheets-targeted spreadsheet artifacts with formulas, formatting, charts, tables, and recalculation (file: r7/spreadsheets/26.630.12135/skills/spreadsheets/SKILL.md) +- template-creator:template-creator: Create or update a reusable personal Codex artifact-template skill. Use when the user invokes $template-creator or asks in natural language to create a template using, from, or based on an attached Word document, PowerPoint presentation, or Excel w (file: r7/template-creator/26.630.12135/skills/template-creator/SKILL.md) +### How to use skills +- Discovery: The list above is the skills available in this session (name + description + short path). Skill bodies live on disk at the listed paths after expanding the matching alias from `### Skill roots`. +- Trigger rules: If the user names a skill (with `$SkillName` or plain text) OR the task clearly matches a skill's description shown above, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned. +- Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback. +- How to use a skill (progressive disclosure): + 1) After deciding to use a skill, the main agent must expand the listed short `path` with the matching alias from `### Skill roots`, then open and read its `SKILL.md` completely before taking task actions. If a read is truncated or paginated, continue until EOF. + 2) When `SKILL.md` references relative paths (e.g., `scripts/foo.py`), resolve them relative to the directory containing that expanded `SKILL.md` first, and only consider other paths if needed. + 3) If `SKILL.md` points to extra folders such as `references/`, use its routing instructions to identify the files required for the task. The main agent must read each required instruction or reference file itself before acting on it. Do not delegate reading, summarizing, or interpreting skill instructions to a subagent. Subagents may still perform task work when the selected skill allows it. + 4) If `scripts/` exist, prefer running or patching them instead of retyping large code blocks. + 5) If `assets/` or templates exist, reuse them instead of recreating from scratch. +- Coordination and sequencing: + - If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them. + - Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why. +- Context hygiene: + - Progressive disclosure applies to selecting relevant files, not partially reading a selected instruction file. Do not load unrelated references, scripts, or assets. + - Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked. + - When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice. +- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue. + +## Plugins +A plugin is a local bundle of skills, MCP servers, and apps. +### How to use plugins +- Skill naming: If a plugin contributes skills, those skill entries are prefixed with `plugin_name:` in the Skills list. +- MCP naming: Plugin-provided MCP tools keep standard MCP identifiers such as `mcp__server__tool`; use tool provenance to tell which plugin they come from. +- Trigger rules: If the user explicitly names a plugin, prefer capabilities associated with that plugin for that turn. +- Relationship to capabilities: Plugins are not invoked directly. Use their underlying skills, MCP tools, and app tools to help solve the task. +- Relevance: Determine what a plugin can help with from explicit user mention or from the plugin-associated skills, MCP tools, and apps exposed elsewhere in this turn. +- Missing/blocked: If the user requests a plugin that does not have relevant callable capabilities for the task, say so briefly and continue with the best fallback. + + +--- + + +**User** + +建议你不要从“大重构”开始。这个项目现在要复活,最快路径是: +先定原则 +先止血,再优化,再重构 +先修会掉单、崩溃、内存无限涨的问题。 +再做可量化性能优化。 +最后慢慢拆 God Class。 + +不要试图一次性清理 1000+ analyze 问题 +我刚跑了全量 flutter analyze,现在是 1141 issues,大量来自 local_packages 和失效测试。 +这不是第一优先级。 +第一阶段只要求:改到的文件不能引入新问题。 + +每次只做一个小 PR +一个 PR 只解决一类问题。 +不夹带 UI 文案、样式重构、命名洁癖。 + +我建议的启动顺序 +第 0 步:建立可跑的基线 +先不要让 flutter analyze 改 pubspec.lock,用: + +git checkout -b codex/perf-revival-start + +--- + + +**OpenAI Codex** + +已创建并切换到基线分支: + +`codex/perf-revival-start` + +未运行 `flutter analyze`,未改动 `pubspec.lock`。 + +::git-create-branch{cwd="/Users/zuozuo/Documents/Project/haiyi/chatapp3-flutter" branch="codex/perf-revival-start"} + +--- + + +**User** + +第一批先修这 4 个点 +PR1:修 SVGA 缓存失控,这是最贴近性能的第一刀 +重点文件: +/Users/zuozuo/Documents/Project/haiyi/chatapp3-flutter/lib/shared/tools/sc_gift_vap_svga_manager.dart +/Users/zuozuo/Documents/Project/haiyi/chatapp3-flutter/lib/ui_kit/widgets/headdress/headdress_widget.dart +/Users/zuozuo/Documents/Project/haiyi/chatapp3-flutter/lib/ui_kit/widgets/room/anim/room_entrance_widget.dart +/Users/zuozuo/Documents/Project/haiyi/chatapp3-flutter/lib/ui_kit/widgets/svga/sc_network_svga_widget.dart +/Users/zuozuo/Documents/Project/haiyi/chatapp3-flutter/lib/modules/user/profile/profile_gift_wall_detail_page.dart +现在的问题是:SCGiftVapSvgaManager 里面已经有缓存和淘汰逻辑,但外部组件还在直接写: +SCGiftVapSvgaManager().videoItemCache[resource] = videoItem; +第一刀就是: +videoItemCache 改私有。 +暴露统一方法,例如 loadSvgaEntity() / removeSvgaEntity()。 +所有外部组件都走统一入口。 +加引用计数,避免 clearMemoryCache() 把正在播放的 SVGA dispose 掉。 +这是最容易看到收益的一刀:房间挂久了内存不再无上限涨。 + +--- + + +**OpenAI Codex** + +我会先只处理 PR1 的 SVGA 缓存入口与引用计数,不碰其它 analyze 问题或 UI 文案。 + +--- + + +**OpenAI Codex** + +已完成 PR1 的代码改动,未动 `pubspec.lock`,未做大重构。 + +改动点: +- `/Users/zuozuo/Documents/Project/haiyi/chatapp3-flutter/lib/shared/tools/sc_gift_vap_svga_manager.dart` + - `videoItemCache` 改为私有 `_videoItemCache` + - 新增统一入口 `loadSvgaEntity()` / `removeSvgaEntity()` + - 新增 SVGA 引用计数与 pending eviction,`clearMemoryCache()` 不再 dispose 正在使用的 SVGA + - 管理器自身播放 SVGA 时也持有/释放引用 + +- 已迁移外部组件直接缓存访问: + - `/Users/zuozuo/Documents/Project/haiyi/chatapp3-flutter/lib/ui_kit/widgets/headdress/headdress_widget.dart` + - `/Users/zuozuo/Documents/Project/haiyi/chatapp3-flutter/lib/ui_kit/widgets/room/anim/room_entrance_widget.dart` + - `/Users/zuozuo/Documents/Project/haiyi/chatapp3-flutter/lib/ui_kit/widgets/svga/sc_network_svga_widget.dart` + - `/Users/zuozuo/Documents/Project/haiyi/chatapp3-flutter/lib/modules/user/profile/profile_gift_wall_detail_page.dart` + +校验: +- `flutter analyze` 针对 manager/network/profile 通过:No issues found +- 5 个文件一起 analyze 仍有 21 个既有 lint/info,集中在 headdress/room_entrance 的历史问题,未在本次顺手清理 +- `git diff --check` 按仓库现有 CRLF 文件风格校验通过 + +当前改动未 stage、未 commit。 + +--- + + +**User** + +启动到真机debug线上包, 我需要能看到内存的大小 + +--- + + +**OpenAI Codex** + +我先查项目的 flavor/启动脚本和当前连接的真机,然后用对应的线上环境 debug 参数启动,不改业务代码。 + +--- + + +**OpenAI Codex** + +已确认真机 `PJV110` 已连接;线上接口参数用项目注释里的 `API_HOST=https://jvapi.haiyihy.com/`。现在启动 debug 包到真机。 + +--- + + +**OpenAI Codex** + +第一次构建卡在阿里云 Maven 镜像 502,失败点是下载 `com.google.gms:google-services:4.4.3`。我不改业务代码,先用本机 Gradle 缓存/官方仓库绕过这个镜像问题后继续启动。 + +--- + + +**OpenAI Codex** + +构建失败原因是仓库镜像顺序导致先命中阿里云 502。我会临时把 Android Gradle 仓库顺序调整为官方 `google()/mavenCentral()` 优先,启动成功后把这个临时改动还原,不混进 PR。 + +--- + + +**OpenAI Codex** + +临时仓库顺序已调整,继续启动到真机。 + +--- + + +**OpenAI Codex** + +Gradle 还在首次 assembleDebug,继续等构建结果。 + +--- + + +**OpenAI Codex** + +构建进程还在跑(`assembleDebug`,Flutter 默认 `-q` 静默,所以中间没有输出),继续等待一次。 + +--- + + +**OpenAI Codex** + +`assembleDebug` 已经静默跑了几分钟,没有新输出。我检查一下 Gradle daemon 日志确认是否卡在依赖解析/锁。 + +--- + + +**OpenAI Codex** + +APK 已构建成功,正在安装到真机。首次安装阶段还在等设备返回。 + +--- + + +**OpenAI Codex** + +APK 比较大(270MB),`adb install` 仍在执行。设备端已经能看到包名,继续等安装命令返回。 + +--- + + +**OpenAI Codex** + +安装包已经写入设备(`lastUpdateTime=2026-07-07 13:27:29`),但 `adb install` 进程没有返回。接下来我停止卡住的安装命令,手动拉起已安装的 debug 包,然后用 `flutter attach` 接上 VM Service/DevTools。 + +--- + + +**OpenAI Codex** + +临时 Gradle 仓库顺序已还原,工作区只剩 PR1 的 5 个文件改动。现在启动 attach,再拉起 App。 + +--- + + +**OpenAI Codex** + +App 已 attach 到 VM Service。接下来我打开 DevTools,并抓一次当前进程内存数,方便你马上确认。 + +--- + + +**OpenAI Codex** + +DevTools 已打开;当前 Android 进程内存也抓到了:`TOTAL PSS 430945 KB`,约 `421 MB`。 + +--- + + +**OpenAI Codex** + +已启动到真机 `PJV110`: + +- 模式:debug +- 线上接口:`API_HOST=https://jvapi.haiyihy.com/` +- 包名:`com.org.yumiparty` +- Flutter attach 已连接,保持当前 attach 进程别关 + +内存查看: + +[打开 DevTools](http://127.0.0.1:60501/N2gykmNP-hc=/devtools/?uri=ws://127.0.0.1:60501/N2gykmNP-hc=/ws) + +进去后点 **Memory** tab 就能看实时内存曲线。 + +当前抓到的设备内存: + +- `TOTAL PSS`: `430945 KB`,约 `421 MB` +- `TOTAL RSS`: `587200 KB`,约 `573 MB` +- `Native Heap`: `54720 KB`,约 `53 MB` +- `Graphics`: `55984 KB`,约 `55 MB` + +中途为绕过阿里云 Maven 502 临时改过 Gradle 仓库顺序,启动后已还原;当前工作区仍只保留 PR1 那 5 个业务文件改动。 + +--- + + +**User** + +目前整体app一进去什么都没干就特别卡顿,这个是为啥呢 + +--- + + +**OpenAI Codex** + +我先按“不要猜”查一下当前真机的实际证据:CPU/帧耗时/启动日志/首帧后初始化链路。debug 包本身会比 release/profile 卡很多,但先看是不是还有明显的启动期主线程/CPU/内存压力。 + +--- + + +**OpenAI Codex** + +现在有几个硬数据了,不是体感:当前进程 CPU 约 `206%`,`TOTAL PSS` 已涨到 `1.28GB`,`Graphics` `~507MB`,且 `dumpsys gfxinfo` 显示 `41%` janky frames,主要是 `Slow UI thread`。我再看是哪类线程/页面在烧 CPU。 + +--- + + +**OpenAI Codex** + +我看了真机数据,结论不是“感觉卡”,是现在启动后实际已经很重了。 + +主要原因有 3 个: + +1. **这不是一个真正空闲的首页状态** + 日志里一启动就看到在跑房间相关逻辑: + + - TRTC / liteav 音频线程在工作 + - remote user audio state 在刷 + - `RoomRocketApi` 持续请求: + - `/go/app/voice-room/rocket/status` + - `roomId=2058558819167956994` + - 响应里还有一堆 `.pag`、图片资源 + + 所以你看到“什么都没干”,但 App 内部已经自动恢复/进入了语音房间状态,RTC、房间轮询、动画资源都在跑。 + +2. **真机上 CPU 和帧率已经爆了** + 我抓到的真机数据: + + - App CPU:约 **180% ~ 300%** + - 主线程单独能到 **80%+** + - `gfxinfo`: + - 总帧数:2468 + - 卡顿帧:1013 + - **Janky frames:41.05%** + - Slow UI thread:855 次 + - 99 分位帧耗时:81ms + + 这说明 UI 线程已经长期超时,不是偶发卡顿。 + +3. **内存也不是冷启动正常水平** + 后面抓到: + + - TOTAL PSS:约 **1.23 GB** + - TOTAL RSS:约 **1.36 GB** + - Graphics:约 **507 MB** + - Native Heap:约 **212 MB** + + 这个对“刚进去什么都没干”的状态来说明显过高。当前第一刀修 SVGA 缓存失控是对的,但它更偏向解决“房间挂久了内存无限涨”,不是唯一解释冷启动卡顿的点。 + +我的判断: +**当前一进去就卡,核心不是首页本身,而是 App 启动后自动恢复了房间/RTC/RoomRocket 这套重业务链路;debug 模式又进一步放大了卡顿。** + +下一步建议按这个顺序验证: + +1. 用 **profile 包**再跑一次,排除 debug 本身影响。 +2. 清掉自动恢复状态,看纯冷启动是否还卡。 +3. 如果清状态后不卡,下一刀就修: + - 冷启动不要自动恢复重房间链路 + - 非房间可见时不要启动 RTC / RoomRocket polling + - RoomRocket 轮询加可见性和生命周期控制 + - PAG/SVGA 资源延迟到真正展示时再加载 + +我建议 PR2 不做大改,就做一个小止血: +**房间不可见/后台/冷启动未确认进入房间前,禁止 RoomRocket 轮询和重动画预加载。** + +--- + + +**User** + +用 profile 包再跑一次 + +--- + + +**OpenAI Codex** + +profile 包已经跑起来了。 + +- 设备:`PJV110` +- 包:`com.org.yumiparty` +- 版本:`1.6.3 / 1631` +- APK:`/Users/zuozuo/Documents/Project/haiyi/chatapp3-flutter/build/app/outputs/flutter-apk/app-profile.apk` +- DevTools:`http://127.0.0.1:61322/zU99BdX0_1s=/devtools/?uri=ws://127.0.0.1:61322/zU99BdX0_1s=/ws` + +我采了一轮 profile 数据,当前不是空闲首页,而是已经进了房间。 + +### profile 数据 + +稳定后: + +- CPU:约 **149% ~ 227%** +- TOTAL PSS:约 **706 MB** +- TOTAL RSS:约 **850 MB** +- Graphics:约 **351 MB** +- Native Heap:约 **137 MB** + +帧率数据比 debug 好很多: + +- Janky frames:**6.67%** +- 50 分位:10ms +- 90 分位:15ms +- 99 分位:38ms + +但是 CPU 仍然很高。 + +### 现在的结论 + +profile 下 UI 帧率明显比 debug 好,说明 debug 确实放大了卡顿。 + +但 profile 仍然有问题: +**App 启动后自动进房间,TRTC 音频、房间业务、动画/礼物资源都在跑,所以不是“什么都没干”的空闲状态。** + +日志里已经确认: + +- `TRTCCloudImpl switchRole audience` +- remote audio subscribed +- audio playout started +- 已进入语音房间 +- 当前页面就是房间页 + +所以接下来要查的不是“首页为什么卡”,而是: + +> 冷启动为什么会自动恢复/进入房间,并启动 RTC、房间轮询和重资源加载。 + +建议下一刀 PR2 就做这个止血: +**冷启动/房间不可见时,不允许启动 RTC、RoomRocket 轮询、礼物/PAG/SVGA 重资源预加载。** + +--- diff --git a/lib/modules/user/profile/profile_gift_wall_detail_page.dart b/lib/modules/user/profile/profile_gift_wall_detail_page.dart index d24dcfb..73dd1c4 100644 --- a/lib/modules/user/profile/profile_gift_wall_detail_page.dart +++ b/lib/modules/user/profile/profile_gift_wall_detail_page.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_svga/flutter_svga.dart'; @@ -566,6 +564,7 @@ class _GiftWallSvgaEffectOverlay extends StatefulWidget { class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay> with SingleTickerProviderStateMixin { late final SVGAAnimationController _controller; + String? _retainedSourceUrl; bool _loaded = false; bool _finished = false; @@ -600,25 +599,21 @@ class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay> } Future _loadMovieEntity(String sourceUrl) async { - final cache = SCGiftVapSvgaManager().videoItemCache; - final cached = cache[sourceUrl]; - if (cached != null) return cached; - - final pathType = SCPathUtils.getPathType(sourceUrl); - late final MovieEntity movieEntity; - if (pathType == PathType.network) { - movieEntity = await SVGAParser.shared.decodeFromURL(sourceUrl); - } else if (pathType == PathType.asset) { - movieEntity = await SVGAParser.shared.decodeFromAssets(sourceUrl); - } else if (pathType == PathType.file) { - final bytes = await File(sourceUrl).readAsBytes(); - movieEntity = await SVGAParser.shared.decodeFromBuffer(bytes); - } else { - throw Exception("Unsupported SVGA source: $sourceUrl"); + final retainedForLoad = _retainedSourceUrl != sourceUrl; + if (retainedForLoad) { + _retainedSourceUrl = sourceUrl; + } + try { + return await SCGiftVapSvgaManager().loadSvgaEntity( + sourceUrl, + retain: retainedForLoad, + ); + } catch (_) { + if (retainedForLoad && _retainedSourceUrl == sourceUrl) { + _retainedSourceUrl = null; + } + rethrow; } - movieEntity.autorelease = false; - cache[sourceUrl] = movieEntity; - return movieEntity; } void _finish() { @@ -630,6 +625,13 @@ class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay> @override void dispose() { _controller.removeStatusListener(_handleStatusChanged); + _controller.stop(); + _controller.videoItem = null; + final retainedSourceUrl = _retainedSourceUrl; + if (retainedSourceUrl != null) { + _retainedSourceUrl = null; + SCGiftVapSvgaManager().removeSvgaEntity(retainedSourceUrl); + } _controller.dispose(); super.dispose(); } diff --git a/lib/shared/tools/sc_gift_vap_svga_manager.dart b/lib/shared/tools/sc_gift_vap_svga_manager.dart index 526bc98..492ce4f 100644 --- a/lib/shared/tools/sc_gift_vap_svga_manager.dart +++ b/lib/shared/tools/sc_gift_vap_svga_manager.dart @@ -23,12 +23,14 @@ class SCGiftVapSvgaManager { static const int _maxConsecutiveEntryTasksBeforeGift = 2; static const Duration _taskWatchdogTimeout = Duration(seconds: 20); static const Duration _entryTaskTtl = Duration(seconds: 6); - Map videoItemCache = {}; static SCGiftVapSvgaManager? _inst; static const int _maxPreloadConcurrency = 1; static const int _maxPreloadQueueLength = 12; static const int _maxSvgaCacheEntries = 8; static const int _maxPlayablePathCacheEntries = 24; + final Map _videoItemCache = {}; + final Map _svgaEntityRefCounts = {}; + final Set _pendingSvgaEvictions = {}; SCGiftVapSvgaManager._internal(); @@ -45,6 +47,8 @@ class SCGiftVapSvgaManager { bool _play = false; bool _dis = false; SCVapTask? _currentTask; + String? _currentSvgaRetainPath; + MovieEntity? _currentSvgaRetainEntity; Timer? _currentTaskWatchdogTimer; final Queue _preloadQueue = Queue(); final Set _queuedPreloadPaths = {}; @@ -127,7 +131,7 @@ class SCGiftVapSvgaManager { bool _isPreloadedOrLoading(String path) { if (_needsSvgaController(path)) { - return videoItemCache.containsKey(path) || + return _videoItemCache.containsKey(path) || _svgaLoadTasks.containsKey(path); } final pathType = SCPathUtils.getPathType(path); @@ -207,6 +211,39 @@ class SCGiftVapSvgaManager { await _ensurePlayableFilePath(path); } + Future loadSvgaEntity(String path, {bool retain = true}) async { + final resource = path.trim(); + if (resource.isEmpty || !_needsSvgaController(resource)) { + throw Exception('Unsupported SVGA path: $path'); + } + if (retain) { + _retainSvgaEntity(resource); + } + try { + return await _loadSvgaEntity(resource); + } catch (_) { + if (retain) { + _releaseSvgaEntity(resource); + } + rethrow; + } + } + + void removeSvgaEntity(String path, {bool evict = false}) { + final resource = path.trim(); + if (resource.isEmpty) { + return; + } + if (evict) { + _pendingSvgaEvictions.add(resource); + } + _releaseSvgaEntity(resource); + if (evict || _pendingSvgaEvictions.contains(resource) || _dis) { + _evictCachedSvgaEntityIfUnused(resource); + } + _trimSvgaCache(); + } + Future _loadSvgaEntity(String path) async { final cached = _touchCachedSvgaEntity(path); if (cached != null) { @@ -230,7 +267,7 @@ class SCGiftVapSvgaManager { throw Exception('Unsupported SVGA path: $path'); } entity.autorelease = false; - if (!_dis) { + if (!_dis || _hasSvgaEntityReferences(path)) { _cacheSvgaEntity(path, entity); } return entity; @@ -274,22 +311,39 @@ class SCGiftVapSvgaManager { } MovieEntity? _touchCachedSvgaEntity(String path) { - final cached = videoItemCache.remove(path); + final cached = _videoItemCache.remove(path); if (cached != null) { - videoItemCache[path] = cached; + _videoItemCache[path] = cached; } return cached; } void _cacheSvgaEntity(String path, MovieEntity entity) { - final previous = videoItemCache.remove(path); + final previous = _videoItemCache.remove(path); if (previous != null && !identical(previous, entity)) { - _disposeSvgaEntityIfUnused(previous); + _disposeSvgaEntityIfUnused(previous, path: path); } - videoItemCache[path] = entity; + _videoItemCache[path] = entity; + _pendingSvgaEvictions.remove(path); _trimSvgaCache(); } + void _retainSvgaEntity(String path) { + _svgaEntityRefCounts[path] = (_svgaEntityRefCounts[path] ?? 0) + 1; + } + + void _releaseSvgaEntity(String path) { + final currentCount = _svgaEntityRefCounts[path]; + if (currentCount == null) { + return; + } + if (currentCount <= 1) { + _svgaEntityRefCounts.remove(path); + return; + } + _svgaEntityRefCounts[path] = currentCount - 1; + } + String? _touchCachedPlayablePath(String path) { final cachedPath = _playablePathCache.remove(path); if (cachedPath != null) { @@ -305,17 +359,18 @@ class SCGiftVapSvgaManager { } void _trimSvgaCache() { - while (videoItemCache.length > _maxSvgaCacheEntries) { - final removableEntry = videoItemCache.entries.firstWhere( - (entry) => !_isSvgaEntityInUse(entry.value), - orElse: () => videoItemCache.entries.first, + while (_videoItemCache.length > _maxSvgaCacheEntries) { + final removableEntry = _videoItemCache.entries.firstWhere( + (entry) => !_isSvgaEntityInUse(entry.value, path: entry.key), + orElse: () => _videoItemCache.entries.first, ); - if (_isSvgaEntityInUse(removableEntry.value)) { - videoItemCache.remove(removableEntry.key); - videoItemCache[removableEntry.key] = removableEntry.value; + if (_isSvgaEntityInUse(removableEntry.value, path: removableEntry.key)) { + _videoItemCache.remove(removableEntry.key); + _videoItemCache[removableEntry.key] = removableEntry.value; break; } - videoItemCache.remove(removableEntry.key); + _videoItemCache.remove(removableEntry.key); + _pendingSvgaEvictions.remove(removableEntry.key); _disposeSvgaEntity(removableEntry.value); } } @@ -438,6 +493,12 @@ class SCGiftVapSvgaManager { return; } _cancelCurrentTaskWatchdog(); + final currentSvgaPath = _currentSvgaRetainPath; + _releaseCurrentSvgaEntityReference( + clearController: + currentSvgaPath != null && + _pendingSvgaEvictions.contains(currentSvgaPath), + ); SCRoomEffectScheduler().completeHighCostTask( debugLabel: finishedTask?.path, ); @@ -451,6 +512,31 @@ class SCGiftVapSvgaManager { } } + void _trackCurrentSvgaEntity(String path, MovieEntity entity) { + if (_currentSvgaRetainPath == path && + identical(_currentSvgaRetainEntity, entity)) { + return; + } + _releaseCurrentSvgaEntityReference(); + _currentSvgaRetainPath = path; + _currentSvgaRetainEntity = entity; + } + + void _releaseCurrentSvgaEntityReference({bool clearController = false}) { + final path = _currentSvgaRetainPath; + final entity = _currentSvgaRetainEntity; + _currentSvgaRetainPath = null; + _currentSvgaRetainEntity = null; + if (clearController && + entity != null && + identical(_rsc?.videoItem, entity)) { + _rsc?.videoItem = null; + } + if (path != null) { + removeSvgaEntity(path); + } + } + // 绑定控制器 void bindMp4Ctrl(SCGiftMp4Controller mp4Controller) { _mute = DataPersistence.getPlayGiftMusic(); @@ -612,10 +698,12 @@ class SCGiftVapSvgaManager { if (_dis) return; if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") { _log('play asset svga path=${task.path}'); - final entity = await _loadSvgaEntity(task.path); + final entity = await loadSvgaEntity(task.path); if (!_isCurrentTask(task)) { + removeSvgaEntity(task.path); return; } + _trackCurrentSvgaEntity(task.path, entity); _log('use prepared asset svga path=${task.path}'); _rsc?.muted = _mute; _rsc?.videoItem = entity; @@ -635,10 +723,12 @@ class SCGiftVapSvgaManager { } final resolvedPath = playablePath ?? task.path; if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") { - final entity = await _loadSvgaEntity(resolvedPath); + final entity = await loadSvgaEntity(resolvedPath); if (!_isCurrentTask(task)) { + removeSvgaEntity(resolvedPath); return; } + _trackCurrentSvgaEntity(resolvedPath, entity); _log('play local svga file path=$resolvedPath source=${task.path}'); _rsc?.muted = _mute; _rsc?.videoItem = entity; @@ -674,14 +764,16 @@ class SCGiftVapSvgaManager { _log('play network svga path=${task.path}'); late final MovieEntity entity; try { - entity = await _loadSvgaEntity(task.path); + entity = await loadSvgaEntity(task.path); } catch (e) { _finishCurrentTask(); return; } if (!_isCurrentTask(task)) { + removeSvgaEntity(task.path); return; } + _trackCurrentSvgaEntity(task.path, entity); _log('use prepared network svga path=${task.path}'); _rsc?.muted = _mute; _rsc?.videoItem = entity; @@ -762,6 +854,7 @@ class SCGiftVapSvgaManager { _rsc?.stop(); _rsc?.reset(); _rsc?.videoItem = null; + _releaseCurrentSvgaEntityReference(); SCRoomEffectScheduler().clearHighCostTasks(reason: 'stop_playback'); } @@ -769,29 +862,62 @@ class SCGiftVapSvgaManager { _preloadQueue.clear(); _queuedPreloadPaths.clear(); _activePreloadCount = 0; - final entries = videoItemCache.entries.toList(growable: false); + final entries = _videoItemCache.entries.toList(growable: false); for (final entry in entries) { final entity = entry.value; - if (!includeCurrent && _isSvgaEntityInUse(entity)) { + if (_hasSvgaEntityReferences(entry.key) || + (!includeCurrent && _isSvgaEntityInUse(entity, path: entry.key))) { + _pendingSvgaEvictions.add(entry.key); continue; } - videoItemCache.remove(entry.key); + _videoItemCache.remove(entry.key); + _pendingSvgaEvictions.remove(entry.key); _disposeSvgaEntity(entity); } _playablePathCache.clear(); } - bool _isSvgaEntityInUse(MovieEntity entity) { - return identical(_rsc?.videoItem, entity); + bool _hasSvgaEntityReferences(String path) { + return (_svgaEntityRefCounts[path] ?? 0) > 0; } - void _disposeSvgaEntityIfUnused(MovieEntity entity) { - if (_isSvgaEntityInUse(entity)) { + bool _isSvgaEntityInUse(MovieEntity entity, {String? path}) { + if (identical(_rsc?.videoItem, entity)) { + return true; + } + if (path != null && _hasSvgaEntityReferences(path)) { + return true; + } + for (final entry in _videoItemCache.entries) { + if (identical(entry.value, entity) && + _hasSvgaEntityReferences(entry.key)) { + return true; + } + } + return false; + } + + void _disposeSvgaEntityIfUnused(MovieEntity entity, {String? path}) { + if (_isSvgaEntityInUse(entity, path: path)) { return; } _disposeSvgaEntity(entity); } + void _evictCachedSvgaEntityIfUnused(String path) { + final entity = _videoItemCache[path]; + if (entity == null) { + _pendingSvgaEvictions.remove(path); + return; + } + if (_isSvgaEntityInUse(entity, path: path)) { + return; + } + _videoItemCache.remove(path); + _pendingSvgaEvictions.remove(path); + _disposeSvgaEntity(entity); + } + void _disposeSvgaEntity(MovieEntity entity) { try { entity.dispose(); diff --git a/lib/ui_kit/widgets/headdress/headdress_widget.dart b/lib/ui_kit/widgets/headdress/headdress_widget.dart index e5f0868..e67ebdf 100644 --- a/lib/ui_kit/widgets/headdress/headdress_widget.dart +++ b/lib/ui_kit/widgets/headdress/headdress_widget.dart @@ -48,14 +48,13 @@ class SVGAHeadwearWidget extends StatefulWidget { class _SVGAHeadwearWidgetState extends State with SingleTickerProviderStateMixin { SVGAAnimationController? _animationController; + final Set _retainedSvgaResources = {}; bool _isLoading = true; - bool _isNetworkResource = false; bool _hasError = false; @override void initState() { super.initState(); - _isNetworkResource = widget.resource.startsWith('http'); _animationController = SVGAAnimationController(vsync: this); if (widget.autoPlay) { @@ -70,6 +69,15 @@ class _SVGAHeadwearWidgetState extends State }); } + @override + void didUpdateWidget(covariant SVGAHeadwearWidget oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.resource != widget.resource || + oldWidget.useCache != widget.useCache) { + _loadAnimation(); + } + } + // 加载动画(带缓存) Future _loadAnimation() async { if (widget.onStartLoading != null) { @@ -81,31 +89,21 @@ class _SVGAHeadwearWidgetState extends State _hasError = false; }); + final resource = widget.resource.trim(); + var retainedForLoad = false; try { - // 检查缓存 - MovieEntity? videoItem; - if (widget.useCache) { - videoItem = SCGiftVapSvgaManager().videoItemCache[widget.resource]; - } + retainedForLoad = _retainSvgaResourceIfNeeded(resource); + final videoItem = await SCGiftVapSvgaManager().loadSvgaEntity( + resource, + retain: retainedForLoad, + ); - // 如果没有缓存,则进行解析 - if (videoItem == null) { - videoItem = - _isNetworkResource - ? await SVGAParser.shared.decodeFromURL(widget.resource) - : await SVGAParser.shared.decodeFromAssets(widget.resource); - videoItem.autorelease = false; - // 存入缓存 - if (widget.useCache) { - SCGiftVapSvgaManager().videoItemCache[widget.resource] = videoItem; - } - } - - if (mounted) { + if (mounted && widget.resource.trim() == resource) { setState(() { _animationController?.videoItem = videoItem; _isLoading = false; }); + _releaseRetainedSvgaResourcesExcept(resource); // 根据循环次数设置播放方式 if (widget.loops == 0) { @@ -117,8 +115,13 @@ class _SVGAHeadwearWidgetState extends State if (widget.onFinishLoading != null) { widget.onFinishLoading!(); } + } else if (retainedForLoad) { + _releaseSvgaResource(resource); } } catch (e) { + if (retainedForLoad) { + _releaseSvgaResource(resource); + } if (mounted) { setState(() { _isLoading = false; @@ -133,6 +136,33 @@ class _SVGAHeadwearWidgetState extends State } } + bool _retainSvgaResourceIfNeeded(String resource) { + if (_retainedSvgaResources.contains(resource)) { + return false; + } + _retainedSvgaResources.add(resource); + return true; + } + + void _releaseSvgaResource(String resource, {bool evict = false}) { + final shouldRelease = _retainedSvgaResources.remove(resource); + if (shouldRelease || evict) { + SCGiftVapSvgaManager().removeSvgaEntity(resource, evict: evict); + } + } + + void _releaseRetainedSvgaResourcesExcept( + String? activeResource, { + bool evict = false, + }) { + final resources = _retainedSvgaResources.toList(growable: false); + for (final resource in resources) { + if (resource != activeResource) { + _releaseSvgaResource(resource, evict: evict); + } + } + } + // 播放动画 void play() { if (_animationController?.videoItem != null) { @@ -156,22 +186,26 @@ class _SVGAHeadwearWidgetState extends State void stop() { _animationController?.stop(); if (widget.clearsAfterStop) { + final resource = widget.resource.trim(); _animationController?.videoItem = null; + _releaseSvgaResource(resource, evict: !widget.useCache); } } // 重新加载动画 void reload() { - if (widget.useCache) { - // 清除缓存项 - SCGiftVapSvgaManager().videoItemCache.remove(widget.resource); - } + final resource = widget.resource.trim(); + _animationController?.stop(); + _animationController?.videoItem = null; + _releaseSvgaResource(resource, evict: true); _loadAnimation(); } @override void dispose() { - stop(); + _animationController?.stop(); + _animationController?.videoItem = null; + _releaseRetainedSvgaResourcesExcept(null, evict: !widget.useCache); _animationController?.dispose(); super.dispose(); } diff --git a/lib/ui_kit/widgets/room/anim/room_entrance_widget.dart b/lib/ui_kit/widgets/room/anim/room_entrance_widget.dart index 11b57c0..bc55e56 100644 --- a/lib/ui_kit/widgets/room/anim/room_entrance_widget.dart +++ b/lib/ui_kit/widgets/room/anim/room_entrance_widget.dart @@ -267,6 +267,7 @@ class RoomEntranceWidget extends StatefulWidget { class _RoomEntranceWidgetState extends State with SingleTickerProviderStateMixin { SVGAAnimationController? _animationController; + final Set _retainedSvgaResources = {}; bool _isLoading = false; bool _hasError = false; String? _currentResource; @@ -342,6 +343,7 @@ class _RoomEntranceWidgetState extends State DynamicData? dynamicData, bool isFromQueue = false, }) async { + _currentResource = resource; if (widget.onStartLoading != null) { widget.onStartLoading!(); } @@ -351,34 +353,29 @@ class _RoomEntranceWidgetState extends State _hasError = false; }); + var retainedForLoad = false; try { - final isNetworkResource = resource.startsWith('http'); - MovieEntity? videoItem; + retainedForLoad = _retainSvgaResourceIfNeeded(resource); + final videoItem = await SCGiftVapSvgaManager().loadSvgaEntity( + resource, + retain: retainedForLoad, + ); - if (widget.useCache) { - videoItem = SCGiftVapSvgaManager().videoItemCache[resource]; - } - - if (videoItem == null) { - videoItem = - isNetworkResource - ? await SVGAParser.shared.decodeFromURL(resource) - : await SVGAParser.shared.decodeFromAssets(resource); - videoItem.autorelease = false; - - if (widget.useCache) { - SCGiftVapSvgaManager().videoItemCache[resource] = videoItem; - } - } - - if (mounted) { + if (mounted && _currentResource == resource) { // 应用动态数据(文本、图片替换) await _applyDynamicData(videoItem, dynamicData); + if (!mounted || _currentResource != resource) { + if (retainedForLoad) { + _releaseSvgaResource(resource); + } + return; + } setState(() { _animationController?.videoItem = videoItem; _isLoading = false; }); + _releaseRetainedSvgaResourcesExcept(resource); // 开始播放动画 _startAnimation(); @@ -386,8 +383,13 @@ class _RoomEntranceWidgetState extends State if (widget.onFinishLoading != null) { widget.onFinishLoading!(); } + } else if (retainedForLoad) { + _releaseSvgaResource(resource); } } catch (e) { + if (retainedForLoad) { + _releaseSvgaResource(resource); + } if (mounted) { setState(() { _isLoading = false; @@ -407,6 +409,33 @@ class _RoomEntranceWidgetState extends State } } + bool _retainSvgaResourceIfNeeded(String resource) { + if (_retainedSvgaResources.contains(resource)) { + return false; + } + _retainedSvgaResources.add(resource); + return true; + } + + void _releaseSvgaResource(String resource, {bool evict = false}) { + final shouldRelease = _retainedSvgaResources.remove(resource); + if (shouldRelease || evict) { + SCGiftVapSvgaManager().removeSvgaEntity(resource, evict: evict); + } + } + + void _releaseRetainedSvgaResourcesExcept( + String? activeResource, { + bool evict = false, + }) { + final resources = _retainedSvgaResources.toList(growable: false); + for (final resource in resources) { + if (resource != activeResource) { + _releaseSvgaResource(resource, evict: evict); + } + } + } + // 应用动态数据(文本、图片替换) Future _applyDynamicData( MovieEntity videoItem, @@ -542,7 +571,11 @@ class _RoomEntranceWidgetState extends State void stop() { _animationController?.stop(); if (widget.clearsAfterStop) { + final resource = _currentResource; _animationController?.videoItem = null; + if (resource != null) { + _releaseSvgaResource(resource, evict: !widget.useCache); + } setState(() { _currentResource = null; }); @@ -552,12 +585,13 @@ class _RoomEntranceWidgetState extends State // 重新加载当前动画 void reload() { if (_currentResource != null) { - if (widget.useCache) { - SCGiftVapSvgaManager().videoItemCache.remove(_currentResource); - } + final resource = _currentResource!; + _animationController?.stop(); + _animationController?.videoItem = null; + _releaseSvgaResource(resource, evict: true); _loadAnimation( - resource: _currentResource!, + resource: resource, dynamicData: _currentDynamicData, isFromQueue: false, ); @@ -566,7 +600,9 @@ class _RoomEntranceWidgetState extends State @override void dispose() { - stop(); + _animationController?.stop(); + _animationController?.videoItem = null; + _releaseRetainedSvgaResourcesExcept(null, evict: !widget.useCache); _animationController?.dispose(); // 从队列管理器中移除当前播放器 _queueManager.clearCurrentPlayer(); diff --git a/lib/ui_kit/widgets/svga/sc_network_svga_widget.dart b/lib/ui_kit/widgets/svga/sc_network_svga_widget.dart index a693f6b..1127fd0 100644 --- a/lib/ui_kit/widgets/svga/sc_network_svga_widget.dart +++ b/lib/ui_kit/widgets/svga/sc_network_svga_widget.dart @@ -51,6 +51,7 @@ class SCNetworkSvgaWidget extends StatefulWidget { class _SCNetworkSvgaWidgetState extends State with SingleTickerProviderStateMixin { late final SVGAAnimationController _controller; + final Set _retainedSvgaResources = {}; String? _loadedResource; bool _hasError = false; @@ -77,10 +78,11 @@ class _SCNetworkSvgaWidgetState extends State Future _loadResource() async { final resource = widget.resource.trim(); if (resource.isEmpty || !SCNetworkSvgaWidget.isSvga(resource)) { + _controller.videoItem = null; + _releaseRetainedSvgaResourcesExcept(null); setState(() { _hasError = true; _loadedResource = null; - _controller.videoItem = null; }); return; } @@ -89,30 +91,33 @@ class _SCNetworkSvgaWidgetState extends State _hasError = false; }); + var retainedForLoad = false; try { - final cache = SCGiftVapSvgaManager().videoItemCache; - var movieEntity = cache[resource]; - if (movieEntity == null) { - movieEntity = - resource.startsWith("http") - ? await SVGAParser.shared.decodeFromURL(resource) - : await SVGAParser.shared.decodeFromAssets(resource); - movieEntity.autorelease = false; - cache[resource] = movieEntity; - } + retainedForLoad = _retainSvgaResourceIfNeeded(resource); + final movieEntity = await SCGiftVapSvgaManager().loadSvgaEntity( + resource, + retain: retainedForLoad, + ); if (widget.movieConfigurer != null) { movieEntity.dynamicItem.reset(); await widget.movieConfigurer!(movieEntity); } if (!mounted || widget.resource.trim() != resource) { + if (retainedForLoad) { + _releaseSvgaResource(resource); + } return; } setState(() { _loadedResource = resource; _controller.videoItem = movieEntity; }); + _releaseRetainedSvgaResourcesExcept(resource); _syncPlayback(restartIfActive: true); } catch (error) { + if (retainedForLoad) { + _releaseSvgaResource(resource); + } if (!mounted || widget.resource.trim() != resource) { return; } @@ -121,6 +126,31 @@ class _SCNetworkSvgaWidgetState extends State _loadedResource = null; _controller.videoItem = null; }); + _releaseRetainedSvgaResourcesExcept(null); + } + } + + bool _retainSvgaResourceIfNeeded(String resource) { + if (_retainedSvgaResources.contains(resource)) { + return false; + } + _retainedSvgaResources.add(resource); + return true; + } + + void _releaseSvgaResource(String resource, {bool evict = false}) { + final shouldRelease = _retainedSvgaResources.remove(resource); + if (shouldRelease || evict) { + SCGiftVapSvgaManager().removeSvgaEntity(resource, evict: evict); + } + } + + void _releaseRetainedSvgaResourcesExcept(String? activeResource) { + final resources = _retainedSvgaResources.toList(growable: false); + for (final resource in resources) { + if (resource != activeResource) { + _releaseSvgaResource(resource); + } } } @@ -144,6 +174,9 @@ class _SCNetworkSvgaWidgetState extends State @override void dispose() { + _controller.stop(); + _controller.videoItem = null; + _releaseRetainedSvgaResourcesExcept(null); _controller.dispose(); super.dispose(); }