Skip to content

feat(extension-agent): 增强 Skills、Sub Agent 与 Computer 管理体验#800

Merged
dingyi222666 merged 21 commits intoChatLunaLab:v1-devfrom
CookSleep:feat/agent-console-skills-subagents
Mar 28, 2026
Merged

feat(extension-agent): 增强 Skills、Sub Agent 与 Computer 管理体验#800
dingyi222666 merged 21 commits intoChatLunaLab:v1-devfrom
CookSleep:feat/agent-console-skills-subagents

Conversation

@CookSleep
Copy link
Copy Markdown
Member

概述

  • 重做 extension-agent 的 Skills、Sub Agent 与 Tool 管理页交互,补充筛选、详情编辑、导入导出与运行轨迹查看能力,并统一移动端体验。
  • 增强技能与子代理权限配置,支持按主 Agent、ChatLuna、Character、Sub Agent 等维度控制可见性与可用性。
  • 修复本地 Computer 提示与路径解析问题,并按当前启用能力动态生成系统提示,避免无效能力注入。

验证

  • yarn fast-build

- 统一侧边栏导航样式,移除硬编码颜色,增加描边以提升深色模式下的辨识度
- 全局重构 el-tag 样式,采用透明背景与柔和描边,视觉更轻量
- 精简所有页面的 toolbar-container,移除毛玻璃与渐变效果,从 sticky 改为 normal 流布局
- 居中 Computer 页面并约束 PC 端最大宽度
- 替换瞎眼的 el-alert 高亮,改用自定义柔和的 warning/info box
- 优化工具详情页 ID 列表交互:添加 ID 拼接提示、复制按钮以及 http 环境下的手动复制弹窗降级
支持 Markdown 新建与导入、Skill 内容编辑、GitHub Token 配置和按项导入,降低 Skills 管理与导入失败成本。

同时补齐 Sub Agent 的 MCP、Computer 权限配置与默认继承行为,让控制台配置更贴近实际使用场景。
为内置工具和长期记忆工具声明默认启用范围,避免主 Agent、ChatLuna 与 Character 首次加载时拿到不合预期的工具集。
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 26, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: aa7effac-b6aa-4eb1-8e6c-8cdd5f9429f4

📥 Commits

Reviewing files that changed from the base of the PR and between b3643c0 and 7a2592c.

⛔ Files ignored due to path filters (1)
  • packages/core/package.json is excluded by !**/*.json
📒 Files selected for processing (32)
  • packages/core/src/llm-core/agent/agent.ts
  • packages/core/src/llm-core/agent/creator.ts
  • packages/core/src/llm-core/agent/executor.ts
  • packages/core/src/llm-core/agent/index.ts
  • packages/core/src/llm-core/agent/legacy-executor.ts
  • packages/core/src/llm-core/agent/service.ts
  • packages/core/src/llm-core/agent/sub-agent.ts
  • packages/core/src/llm-core/agent/types.ts
  • packages/core/src/llm-core/chain/plugin_chat_chain.ts
  • packages/core/src/llm-core/platform/types.ts
  • packages/core/src/services/chat.ts
  • packages/core/src/utils/chatluna.ts
  • packages/extension-agent/src/cli/service.ts
  • packages/extension-agent/src/config/defaults.ts
  • packages/extension-agent/src/config/read.ts
  • packages/extension-agent/src/service/computer.ts
  • packages/extension-agent/src/service/mcp.ts
  • packages/extension-agent/src/service/permissions.ts
  • packages/extension-agent/src/service/sub_agent.ts
  • packages/extension-agent/src/sub-agent/render.ts
  • packages/extension-agent/src/sub-agent/run.ts
  • packages/extension-agent/src/types/tool.ts
  • packages/extension-long-memory/src/plugins/tool.ts
  • packages/extension-tools/src/plugins/command.ts
  • packages/extension-tools/src/plugins/cron.ts
  • packages/extension-tools/src/plugins/file_sender.ts
  • packages/extension-tools/src/plugins/group.ts
  • packages/extension-tools/src/plugins/music.ts
  • packages/extension-tools/src/plugins/request.ts
  • packages/extension-tools/src/plugins/think.ts
  • packages/extension-tools/src/plugins/todos.ts
  • packages/service-search/src/index.ts

Walkthrough

本次变更为工具/技能/子 Agent 引入基于来源(chatluna/character)的细粒度默认可用性与掩码支持,扩展相关类型与默认元数据,调整权限判断与工具掩码计算,并同步大量 Web UI 组件、导入/编辑与样式改动。

Changes

Cohort / File(s) Summary
核心类型与平台元数据
packages/core/src/llm-core/platform/types.ts, packages/extension-agent/src/types/tool.ts, packages/core/src/llm-core/agent/types.ts
新增/扩展 Tool 默认可用性类型(character scope、defaultAvailability),扩展 tool/meta 接口并保留若干已弃用字段。
Agent / 创建与运行时
packages/core/src/llm-core/agent/*.ts, packages/core/src/llm-core/agent/service.ts, packages/core/src/llm-core/agent/creator.ts
添加 createAgent/createAgentTool、AgentRunner/legacy 执行器重构、AgentTask 子任务工具与运行时、以及相关运行时配置项与导出。
服务层与权限逻辑
packages/extension-agent/src/service/permissions.ts, packages/extension-agent/src/service/index.ts, packages/extension-agent/src/service/skills.ts, packages/extension-agent/src/service/sub_agent.ts
权限判断感知 source,新增 hasAuthority/isSessionAllowed/canUseSubAgent 等,工具/技能/子 Agent 的可用性与授权检查重构并新增若干服务 API(setSkillMode、saveSkillContent、preview/save/export SubAgent)。
配置/默认与读取合并
packages/extension-agent/src/config/defaults.ts, packages/extension-agent/src/config/read.ts
新增 createSkillItemConfig、copyRule,扩展默认工具/技能/子 Agent 字段,readConfig 改为按键合并 registry 与 skills 并正规化 defaultAvailability。
类型扩展与控制台接口
packages/extension-agent/src/types/*.ts, packages/core/src/llm-core/agent/types.ts
扩展 Skill/SubAgent/Tool 类型以包含 chatluna/character、group/private 模式与 ID 列表、authority、SkillMode,更新 console/webui 事件类型署名。
Web UI 组件与样式
packages/extension-agent/client/components/...
大量新增/重构 Vue 组件(tool/skills/sub-agent 详情、导入对话、运行追踪、权限编辑等),删除旧对话组件,广泛布局与滚动条样式调整。
导入/预览/扫描与 catalog
packages/extension-agent/src/skills/*.ts, packages/extension-agent/src/skills/import.ts
支持按用户选择导入(selected)、预览包含 importName/exists、导入校验加强;技能启用以 mode 驱动,catalog 使用配置正规化字段填充。
插件工具元数据与注册
packages/extension-tools/src/plugins/*, packages/extension-long-memory/src/plugins/tool.ts, packages/service-search/src/index.ts
为多个插件工具注册添加 meta(source/group/tags/defaultAvailability),并在若干插件中设置默认可用性。
CLI/命令行与搜索链/终端
packages/extension-agent/src/cli/*, packages/service-search/src/chain/browsing_chain.ts, packages/extension-agent/src/computer/backends/open_terminal.ts
CLI 新增设置 chatluna/character/group/private 域的 mutation;搜索链接受 toolMask;OpenTerminal 改进路径解析与响应解包。

Sequence Diagram(s)

sequenceDiagram
    participant Client as Web UI / Client
    participant ChatSvc as ChatLunaAgentService
    participant PermSvc as PermissionService
    participant Registry as Tool Registry
    participant DB as Config Store

    Client->>ChatSvc: 请求 chat/run(含 session, room, source)
    ChatSvc->>PermSvc: createMainToolMask(session, source)
    PermSvc->>Registry: 请求 registry 元数据(含 defaultAvailability)
    Registry-->>PermSvc: 返回工具元数据
    PermSvc->>DB: 读取/合并已保存 ToolItemConfig
    DB-->>PermSvc: 返回保存配置
    PermSvc-->>ChatSvc: 返回经 source + authority + mask 过滤的允许工具列表
    ChatSvc->>Client: 返回允许的工具/能力,并继续生成/执行
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

我是小兔来道喜,掩码细分如织锦;
技能子 Agent 皆分明,UI 与导入都更新。
跳跃代码田野里,祝审阅者心生欢。 🐇

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Warning

Gemini encountered an error creating the review. You can try again by commenting /gemini review.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/extension-agent/client/components/sub-agent/sub-agent-page.vue (1)

571-592: ⚠️ Potential issue | 🟠 Major

详情页编辑的名称/描述/Prompt 还没有真正落盘。

这里虽然开始从 draft 保存 enabled,但 name/description 仍然回写 item,而 promptContent 完全没进 saved。结果就是详情页里改完这些字段后提示“保存成功”,刷新后内容还是旧的。

建议修改
         const saved = {
             enabled: draft.enabled,
-            name: item.name,
-            description: item.description,
+            name: draft.name.trim(),
+            description: draft.description.trim(),
             source: item.source,
             format: item.format,
+            promptContent: draft.promptContent,
             model: draft.model.trim() || undefined,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension-agent/client/components/sub-agent/sub-agent-page.vue`
around lines 571 - 592, The saved payload uses item.name/item.description and
omits promptContent, so edits in the detail page aren't persisted; update the
construction of saved in the save routine (where draft, item, busy,
toRaw(props.config) and saved: SubAgentItemConfig are used) to take name and
description from draft instead of item, and include draft.promptContent (or
draft.promptContent.trim() || undefined) into the saved object (ensuring it
matches the SubAgentItemConfig shape), then keep the rest of the fields
(enabled, model, permissions, etc.) unchanged so the edited
name/description/prompt persist after save.
🧹 Nitpick comments (13)
packages/extension-agent/client/components/sub-agent/preset-dialog.vue (1)

86-99: 移动端响应式逻辑实现良好

正确在 onMounted 中添加监听器并在 onBeforeUnmount 中移除。

可选优化:resize 事件在窗口调整大小时会高频触发,可考虑添加节流(throttle)以优化性能:

♻️ 可选的节流处理
+let resizeTimer: ReturnType<typeof setTimeout> | null = null
+
+function syncMobile() {
+    if (resizeTimer) clearTimeout(resizeTimer)
+    resizeTimer = setTimeout(() => {
+        mobile.value = window.innerWidth <= 768
+    }, 100)
+}
-function syncMobile() {
-    mobile.value = window.innerWidth <= 768
-}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension-agent/client/components/sub-agent/preset-dialog.vue`
around lines 86 - 99, The resize handler is attached directly which can fire
very frequently; wrap syncMobile with a throttling function and use that
throttled function for addEventListener/removeEventListener so the same
reference is removed; update the code around syncMobile, onMounted and
onBeforeUnmount to create a throttledSyncMobile (using your preferred throttle
utility or a simple timestamp-based throttle), call throttledSyncMobile
initially instead of syncMobile where appropriate, and ensure
window.addEventListener('resize', throttledSyncMobile) and
window.removeEventListener('resize', throttledSyncMobile) are used to
register/unregister the listener.
packages/extension-agent/client/components/computer/desktop-panel.vue (1)

290-314: LGTM!

滚动条样式统一且跨浏览器兼容(Firefox + WebKit)。使用 CSS 变量保持主题一致性。

可选优化:此滚动条样式在多个组件中重复出现(desktop-panel、files-panel、terminal-panel、code-editor 等),可考虑提取为共享的 CSS mixin 或 utility class 以减少重复。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension-agent/client/components/computer/desktop-panel.vue` around
lines 290 - 314, The duplicated custom scrollbar CSS used by .desktop-stage (and
similar blocks in files-panel, terminal-panel, code-editor) should be extracted
into a shared utility/mixin to remove repetition; create a single exported CSS
mixin or utility class (e.g., .custom-scrollbar or mixin name) containing the
scrollbar rules (scrollbar-width, scrollbar-color, ::-webkit-scrollbar,
::-webkit-scrollbar-track, ::-webkit-scrollbar-thumb,
::-webkit-scrollbar-thumb:hover) and then replace the repeated blocks in
desktop-panel.vue (and the other components) by importing/applying that shared
mixin or adding the utility class to the root element so all components reuse
the same scrollbar styling.
packages/extension-agent/client/components/mcp/mcp-servers-view.vue (1)

1743-1746: 媒体查询中存在与网格布局冲突的 flex 属性。

在第 1743-1746 行,.card-list 被设置为 flex-direction: column,但在第 1192-1198 行已将其改为 CSS Grid 布局(display: grid)。flex-direction 属性对 grid 布局无效。

建议移除这些 flex 相关的属性,因为网格布局已通过 --card-cols: 1 在第 1727-1730 行正确处理了移动端的单列显示。

建议的修改
 `@media` (max-width: 768px) {
     .card-list,
     .card-list.compact {
         --card-cols: 1;
     }

     .panel-header {
         flex-direction: column;
         align-items: flex-start;
     }

     .dialog-grid > .form-group,
     .dialog-grid > .form-span {
         flex-basis: 100%;
         max-width: none;
     }

-    .card-list {
-        flex-direction: column;
-        align-items: stretch;
-    }
-
     .server-card,
     .tool-card {
         flex-basis: auto;
         width: 100%;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension-agent/client/components/mcp/mcp-servers-view.vue` around
lines 1743 - 1746, The .card-list selector defines flex properties
(flex-direction: column and align-items: stretch) that conflict with the grid
layout already applied earlier; remove the flex-specific declarations from the
.card-list rule in mcp-servers-view.vue so the grid (display: grid and the
--card-cols: 1 mobile behavior) controls layout instead.
packages/extension-agent/client/components/computer/files-panel.vue (3)

1107-1133: .tabs-scroll 存在重复定义。

第 1107-1133 行定义了带滚动条样式的 .tabs-scroll,而第 1295-1303 行又重复定义了相同选择器的基础布局属性。后者会覆盖前者的部分属性,但滚动条相关样式会保留。

建议将两处合并为一处完整的定义。

Also applies to: 1295-1303

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension-agent/client/components/computer/files-panel.vue` around
lines 1107 - 1133, Merge the duplicate .tabs-scroll selector definitions into a
single consolidated rule: locate both .tabs-scroll blocks and combine the layout
properties (display, align-items, gap, flex, min-width, min-height, overflow)
with the scrollbar styling (scrollbar-width, scrollbar-color and the
::-webkit-scrollbar/track/thumb rules) so no layout or scrollbar rules are lost;
remove the later redundant .tabs-scroll block (or replace it with a comment
linking to the consolidated rule) and ensure the final selector preserves the
original order/positioning concerns so specificity and cascade remain unchanged.

1082-1105: 存在重复的 CSS 规则定义。

.list-body 选择器在第 1082-1090 行和第 1209-1214 行都有定义。这两处定义了相同的属性(positionmin-heightmax-heightoverflow),会导致维护困难和潜在的样式冲突。

建议合并这两处定义,只保留一处(包含滚动条样式的那个)。

Also applies to: 1209-1214

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension-agent/client/components/computer/files-panel.vue` around
lines 1082 - 1105, There are duplicate .list-body rules; locate both .list-body
blocks and merge them into a single definition by removing the redundant block
and keeping the one that includes the scrollbar rules (ensure the final
.list-body contains position, min-height, max-height, overflow and the
::-webkit-scrollbar / ::-webkit-scrollbar-track / ::-webkit-scrollbar-thumb
rules). Update only the CSS blocks (the selectors .list-body and its
pseudo-elements) so no properties are lost and no duplicate selectors remain.

1135-1162: .image-stage 存在重复定义。

第 1135-1162 行和第 1376-1384 行都定义了 .image-stage。后者会覆盖前者的 displayalign-itemsjustify-contentmin-heightheightpaddingoverflow 属性,但不包含滚动条样式。

建议删除第 1376-1384 行的重复定义,保留包含滚动条样式的完整版本。

Also applies to: 1376-1384

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension-agent/client/components/computer/files-panel.vue` around
lines 1135 - 1162, Remove the duplicate .image-stage CSS block that lacks
scrollbar rules and is shadowing the full definition; keep the comprehensive
.image-stage declaration that includes scrollbar styling (the block that defines
display, align-items, justify-content, min-height, height, padding, overflow,
scrollbar-width, scrollbar-color and the ::-webkit-scrollbar rules) and delete
the later, partial .image-stage block so the scroll styling is preserved and not
inadvertently overridden.
packages/extension-agent/client/components/sub-agent/sub-agent-detail.vue (1)

233-237: emit 变量未被使用。

defineEmits 的返回值赋给了 emit,但模板中使用的是 $emit。可以移除该变量声明或在模板中统一使用 emit

♻️ 建议移除未使用的变量
-const emit = defineEmits<{
+defineEmits<{
     back: []
     save: []
     remove: []
 }>()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension-agent/client/components/sub-agent/sub-agent-detail.vue`
around lines 233 - 237, 模板中使用的是 $emit 而不是局部变量 emit,导致 const emit =
defineEmits<...>() 未被使用;请删除该未使用的 emit 赋值(defineEmits 调用可以直接保留为
defineEmits<...>() 或直接在 setup 中返回事件接口),或者将模板中所有 $emit 调用改为使用 setup 里导出的
emit(统一使用 emit),以消除未使用变量并保持事件触发方式一致(关注 defineEmits、emit 与模板中的 $emit 引用)。
packages/extension-agent/client/components/sub-agent/sub-agent-runs.vue (1)

205-226: 剪贴板回退逻辑合理,但空 catch 块可记录日志。

navigator.clipboard.writeText 失败时静默捕获异常并回退到 execCommand,逻辑正确。建议在 catch 块中至少添加 console.debug 以便调试。

♻️ 建议添加调试日志
     if (window.isSecureContext && navigator.clipboard) {
         try {
             await navigator.clipboard.writeText(text)
             ElMessage.success(msg)
             return
-        } catch {}
+        } catch (e) {
+            console.debug('clipboard API failed, falling back to execCommand', e)
+        }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension-agent/client/components/sub-agent/sub-agent-runs.vue`
around lines 205 - 226, The async function copy(text: string, msg: string)
silently swallows errors from navigator.clipboard.writeText; update the catch in
copy to log the caught error (e.g., using console.debug or process logger) with
context so failures are visible while still falling back to the
textarea/execCommand path; keep the existing fallback behavior and only add the
logging inside the catch block in the copy function.
packages/extension-agent/src/sub-agent/run.ts (1)

281-303: formatTraceText 仅在单处调用,可考虑内联。

根据编码规范,少于 5 行逻辑且仅单处调用的函数建议内联。不过此函数包含多个分支,保持独立有助于可读性,可保留现状或酌情内联。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension-agent/src/sub-agent/run.ts` around lines 281 - 303,
formatTraceText is only used once and per style you should inline it: replace
the single call to formatTraceText(...) with the function's logic (preserve all
branches: string return, Array.isArray branch mapping items with 'text' property
fallback to JSON.stringify, and final JSON.stringify fallback) at the call site,
then remove the formatTraceText function declaration; if you prefer to keep it
for readability, leave it unchanged but add a short comment above
formatTraceText explaining its purpose and why it remains a separate helper.
packages/extension-agent/client/components/skills/skills-settings-dialog.vue (1)

152-166: watcher 依赖数组包含不必要的项。

watch 的依赖数组同时包含 props.config.dirsprops.config.githubToken,但回调内仅在 visibletrue 时同步这些值。直接 watch props.visible 即可,或将依赖简化。

♻️ 建议简化 watcher
 watch(
-    () => [props.visible, props.config.dirs, props.config.githubToken] as const,
+    () => props.visible,
     (visible) => {
         if (!visible) {
             return
         }

         dirDraft.value = [...(props.config.dirs ?? [])]
         tokenDraft.value = props.config.githubToken ?? ''
-    },
-    {
-        immediate: true,
-        deep: true
-    }
+    },
+    { immediate: true }
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension-agent/client/components/skills/skills-settings-dialog.vue`
around lines 152 - 166, The watcher currently depends on props.visible,
props.config.dirs and props.config.githubToken but the callback only acts when
props.visible is true; simplify by watching only props.visible (i.e. replace the
dependency array with () => props.visible) and inside the callback copy
props.config.dirs into dirDraft and props.config.githubToken into tokenDraft
when visible; alternatively, if you need reactive updates to dirs/token while
visible, create a separate watch on props.config.dirs and
props.config.githubToken that checks visible before updating dirDraft and
tokenDraft. Ensure you update the watcher invocation named watch and the
referenced variables dirDraft and tokenDraft accordingly.
packages/extension-agent/src/service/index.ts (1)

331-334: 使用 ES import 替代 require()

此处使用了 CommonJS 的 require() 语句,违反了 coding guidelines 中关于使用现代 ES 模块的规定。

♻️ 建议改为顶层 import

在文件顶部添加 import:

import { parseAgentFrontmatter } from '../sub-agent/parse'

然后简化方法实现:

 async previewSubAgentImport(data: string) {
-    const { parseAgentFrontmatter } = require('../sub-agent/parse')
     return parseAgentFrontmatter(data, 'preview')
 }

根据 coding guidelines:Use modern ES modules (.ts); target es2022 as configured in tsconfig.json

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension-agent/src/service/index.ts` around lines 331 - 334,
Replace the dynamic CommonJS require in previewSubAgentImport with a top-level
ES import: add "import { parseAgentFrontmatter } from '../sub-agent/parse'" at
the top of the module and simplify the async previewSubAgentImport(data: string)
implementation to directly return parseAgentFrontmatter(data, 'preview'); ensure
the imported symbol name matches parseAgentFrontmatter and remove the inline
require to comply with ES module (ES2022) guidelines.
packages/extension-agent/client/components/tool/tool-page.vue (1)

517-551: createItem 中大量使用 as any 类型转换

此函数同时处理 ToolItemConfigToolInfo 两种类型,由于字段命名差异(如 chatluna vs chatlunaEnabled),导致需要频繁使用 as any 进行类型转换。

虽然当前实现功能正确,但建议考虑:

  1. 统一类型定义中的字段命名
  2. 或使用类型守卫区分处理
♻️ 使用类型守卫减少 any 使用
function isToolInfo(item: Partial<ToolItemConfig> | ToolInfo): item is ToolInfo {
    return 'chatlunaEnabled' in item
}

function createItem(
    item?: Partial<ToolItemConfig> | ToolInfo,
    name?: string
): ToolItemConfig {
    const info = item && isToolInfo(item)
    return {
        enabled: item?.enabled !== false,
        main: item?.main !== false,
        chatluna: info
            ? item.chatlunaEnabled !== false
            : (item?.chatluna !== false),
        // ... 类似处理其他字段
    }
}

根据 coding guidelines:Avoid any where possible。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension-agent/client/components/tool/tool-page.vue` around lines
517 - 551, The createItem function overuses "as any" to handle two shapes
(ToolItemConfig and ToolInfo); add a type guard (e.g., isToolInfo) that detects
ToolInfo (check for a representative property like "chatlunaEnabled") and then
branch inside createItem to read fields from the correct type instead of casting
to any; update uses for chatluna/chatlunaEnabled, character/characterEnabled,
characterGroup/characterGroupEnabled, characterPrivate/characterPrivateEnabled,
characterGroupMode/characterPrivateMode and id arrays
(characterGroupIds/characterPrivateIds) and keep authority defaulting via
defaultAuthority(name). Ensure cloneRule(item?.subAgents) remains unchanged and
return a ToolItemConfig.
packages/extension-agent/client/components/skills/skills-detail.vue (1)

356-373: 空 catch 块静默吞噬错误

copyIds() 中的空 catch 块会静默忽略剪贴板复制失败的错误,这与 tool-detail.vue 中相同功能的实现不一致(后者使用 console.error 记录错误)。

♻️ 建议添加错误日志以保持一致性
 async function copyIds() {
     const text = currentIds.value.join(',')

     if (window.isSecureContext && navigator.clipboard) {
         try {
             await navigator.clipboard.writeText(text)
             ElMessage.success('已复制到剪贴板')
             return
-        } catch {}
+        } catch (e) {
+            console.error('Copy failed', e)
+        }
     }

     copyContent.value = text
     showCopyDialog.value = true
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension-agent/client/components/skills/skills-detail.vue` around
lines 356 - 373, The empty catch in async function copyIds() swallows clipboard
errors; update the catch to accept the error (e.g., "err") and log it (use
console.error or the same logging pattern used in tool-detail.vue) with a clear
message like "Failed to write to clipboard" before falling back to the dialog
flow, ensuring copyIds, navigator.clipboard.writeText, copyContent,
showCopyDialog, copyInputRef and nextTick behavior remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/llm-core/platform/types.ts`:
- Line 74: Change the source property's type from the current union that ends
with plain string to the suggested pattern so literal members retain IDE
autocomplete while still allowing arbitrary strings; specifically update the
declaration that currently reads "source?: 'core' | 'extension' | 'mcp' |
'action' | string" to use "source?: 'core' | 'extension' | 'mcp' | 'action' |
(string & {})" (locate the source property in
packages/core/src/llm-core/platform/types.ts) so known literals keep their type
hints but custom values like 'infinite-context' remain allowed.

In `@packages/extension-agent/client/components/computer/computer-page.vue`:
- Around line 114-153: The Transition wrapper using name="fade-slide" has no
corresponding CSS so it produces no animation; either remove the <Transition
name="fade-slide">...</Transition> wrapper around the panels (e.g., the
Transition in computer-page.vue that encloses the
config/terminal/jobs/files/desktop divs) or add the required CSS transition
classes for "fade-slide" (.fade-slide-enter-active, .fade-slide-leave-active,
.fade-slide-enter-from, .fade-slide-leave-to) in the component's style block or
a shared stylesheet so the enter/leave animations take effect.

In `@packages/extension-agent/client/components/layout/agent-shell.vue`:
- Around line 132-136: The watcher on activeTab calls refreshData() concurrently
allowing multiple refreshConsoleData calls to run and race because refreshData()
only uses a single boolean pending; change refreshData()/refreshConsoleData to
ensure only the latest invocation updates state by serializing or canceling
previous runs—e.g., add a per-call token/id (requestId or AbortController)
checked before clearing pending or applying response data, or queue calls so a
new tab-switch cancels/ignores prior requests; update the
watcher/watch(activeTab, ...) to either debounce rapid changes or pass a cancel
token and have refreshData() respect it so only the final tab switch's response
clears loading and writes data.

In
`@packages/extension-agent/client/components/skills/skills-import-folder-dialog.vue`:
- Around line 331-337: The preview request is sending selected.value which may
be stale when handleSelect triggers preview; update the code to avoid passing
outdated selection by either clearing/resetting selected before calling
send('chatluna-agent/previewSkillImport') or removing the selected property from
the payload entirely. Locate the preview logic (previewing, preview,
send('chatluna-agent/previewSkillImport')) and the selection handler
(handleSelect, selected) and ensure selected is reset (e.g., selected.value = []
or null) or omitted from the request payload so the backend never receives an
old selected value.

In `@packages/extension-agent/client/components/skills/skills-page.vue`:
- Around line 242-244: The click handler passes item.homepage directly to
openLink(item.homepage) and ultimately to window.open, which allows unsafe
schemes; update the openLink function (and any callers) to validate and only
allow http: and https: schemes before opening: parse the string with the URL
constructor (catching errors), check url.protocol is 'http:' or 'https:', and
only then call window.open; if validation fails, abort and optionally
log/fallback. Also ensure item.homepage is not used elsewhere without the same
validation (referenced symbols: item.homepage, openLink, window.open).
- Around line 564-579: The setEnabled function currently forces
draft.value.items[id].mode = 'description' when disabling a skill, which
permanently downgrades any previously 'full' mode; remove that line so disabling
only toggles enabled state and does not mutate mode. In setEnabled (which finds
the skill via skills.value.find and guards with isReadonly), ensure you still
create draft entry with createItem if missing and call scheduleSave(), but do
not reset draft.value.items[id].mode when enabled === false so the original
injection mode is preserved across disable/enable cycles.
- Around line 133-140: The skill card div rendering filteredSkills must be made
keyboard-focusable and accessible: replace the interactive <div
class="skill-card"> with a semantic interactive element (preferably a <button>
or <el-button>) or, if keeping the div, add tabindex="0", role="button" and
keydown handlers for Enter/Space that call the same handler as `@click`; update
the template where v-for="item in filteredSkills" and the click uses
openEditor(item.id) and the readonly check via isReadonly(item) to ensure the
keyboard handlers respect isReadonly(item) and do not run when readonly, and
keep the existing :class bindings (centered, muted, invalid, readonly) intact so
styling continues to apply.

In `@packages/extension-agent/client/components/sub-agent/sub-agent-page.vue`:
- Around line 50-68: The tab items use plain divs with click handlers so they
lack keyboard/ARIA semantics; replace them with semantic, focusable controls
(e.g., button elements or an accessible tab widget) or add role="tab", tabindex,
and `@keydown` handlers to support Enter/Space activation and arrow-key navigation
while keeping the listTab binding (refer to the existing listTab usage for
state); additionally, fix saveSelected() to persist edited fields by reading
from draft (use draft.name, draft.description, and include draft.promptContent)
instead of the original item so user edits are saved.

In `@packages/extension-agent/src/computer/backends/open_terminal.ts`:
- Around line 109-117: readFile uses the raw filePath when calling the backend
(e.g. this.url('/files/read')) while writeFile uses resolvePath(filePath),
causing inconsistent behavior for ~/ and relative paths; fix by resolving the
path once at the start of readFile (call resolvePath(filePath) and assign to a
local variable like resolvedPath) and use that resolvedPath for all subsequent
requests and directory logic (e.g. list/read calls and computing dir), and apply
the same change to the analogous block covering lines 146-153 so both read and
related operations consistently use resolvePath.

In `@packages/extension-agent/src/config/defaults.ts`:
- Around line 219-227: The task config's character flags conflict:
defaultCharacter is false while defaultCharacterGroup and
defaultCharacterPrivate are true, but createMainToolMask() treats the overall
Character switch (defaultCharacter) as a gate so group/private flags never take
effect; fix by either setting task.defaultCharacter to true to match the
group/private defaults OR update createMainToolMask() to treat
defaultCharacterGroup and defaultCharacterPrivate independently of
defaultCharacter (i.e., evaluate group/private flags directly rather than
short-circuiting on defaultCharacter) so the group/private masks can enable
character behavior even when defaultCharacter is false.

In `@packages/extension-agent/src/config/read.ts`:
- Around line 18-23: The merge for skills.items currently overwrites with
cfg.skills?.items ?? {} while skills.dirs falls back to base.skills.dirs; update
the items merge to mirror that fallback behavior by using base.skills.items as
the default (i.e. replace the current items spread to fall back to
base.skills.items when cfg.skills?.items is missing) so that skills.items and
skills.dirs have consistent merge semantics (refer to the skills object
construction where cfg and base are combined).

In `@packages/extension-agent/src/service/skills.ts`:
- Around line 189-203: In saveSkillContent, replace the dynamic
require('fs/promises') usage with the already imported writeFile (used at the
top of the file) to remove redundancy and maintain consistent ES imports; locate
the saveSkillContent method, remove the line const { writeFile } =
require('fs/promises') and call the top-level imported writeFile(skill.path,
content, 'utf-8') instead, then keep the await this.reload() as-is.
- Around line 332-365: The code computes idValue from session but doesn't handle
the case when idValue is undefined; add an early guard in the permission check
function (the block using idValue/session) that returns false when idValue is
null/undefined so neither the 'deny' nor 'allow' branches accidentally grant
access for unknown sessions; update both the direct-path logic (the
session?.isDirect branch that checks
item.characterPrivateEnabled/characterPrivateMode and characterPrivateIds) and
the group-path logic (the checks for
item.characterGroupEnabled/characterGroupMode and characterGroupIds) to rely on
this guard before performing includes(...) checks.

In `@packages/extension-agent/src/service/sub_agent.ts`:
- Around line 357-360: The child subagent's base toolMask currently only uses
options.parentSubagentContext?.toolMask, so when parentSubagentContext is absent
the configurable.runConfig.toolMask is ignored and the child can gain broader
tools; update the codepaths that call RunSubAgentOptions (from runTask) to pass
runConfig?.configurable?.toolMask into RunSubAgentOptions, then change the
earlier logic to compute base by combining parentSubagentContext?.toolMask and
options.runConfig?.configurable?.toolMask (e.g., take the more restrictive
intersection/merge) before calling this.permission.createSubAgentToolMask(info)
and buildSubAgentToolMask; reference symbols: RunSubAgentOptions, runTask,
options.parentSubagentContext, runConfig?.configurable?.toolMask,
this.permission.createSubAgentToolMask, buildSubAgentToolMask.

In `@packages/extension-agent/src/skills/catalog.ts`:
- Around line 75-79: The code normalizes mode to 'off' for disabled skills which
hides missing-but-kept items whose cfg.mode is 'description' or 'full'; change
the mode assignment so it preserves the original cfg.mode instead of forcing
'off' (e.g. use const mode = cfg.mode or const mode = cfg.mode ?? 'off'), and
keep the existing guard if (!cfg.enabled && cfg.mode !== 'description' &&
cfg.mode !== 'full') continue so missing-but-preserved items with cfg.mode ===
'description'|'full' are retained in the catalog; update references to mode
accordingly in createSkillItemConfig / the surrounding block.

In `@packages/extension-agent/src/skills/import.ts`:
- Around line 101-114: The import loop allows multiple picked entries to map to
the same destination (item.importName) causing silent overwrites; before
performing any fs operations, validate the array picked by checking
picked.map(item => item.importName) for duplicates, and if any duplicates exist,
abort the whole import and return an error/validation message to the caller
instead of proceeding; implement this check near the start of the import
function (before the for loop that uses skillsRoot, rm, cp, result.imported and
result.replaced) and ensure no filesystem changes occur when duplicates are
found.
- Around line 514-532: The current matcher wrongly treats any suffix match
(dir.endsWith(`/${clean}`)) as valid; change it so we only accept exact matches
or matches where the path is exactly one leading directory plus the requested
clean path (to account for zipball top-level folder). In the dirs.filter used to
build matched (the callback referencing dir, clean), replace the endsWith check
with logic that either dir === clean OR dir.split('/').length ===
clean.split('/').length + 1 and dir.split('/').slice(1).join('/') === clean
(i.e., strip exactly one leading segment and compare), then return join(root,
matched[0]) as before.

In `@packages/extension-agent/src/skills/scan.ts`:
- Around line 371-374: The code treats missing input.cfg.items[id] as disabled
by default; change the fallback so only an explicit enabled === false maps to
'off' and everything else defaults to 'description'. Update the mode computation
around input.cfg.items[id] (and the similar block later around the other items
at the same pattern) to use a conditional that checks
input.cfg.items[id]?.enabled === false ? 'off' : 'description', keeping enabled
= mode !== 'off' unchanged; apply the same fix to the similar logic in the
429-443 region (same symbols: input.cfg.items[id], mode, enabled,
scanSkillRoot).

In `@packages/extension-agent/src/types.ts`:
- Line 237: The RPC mapping currently uses a loose any return for
'chatluna-agent/previewSubAgentImport'; replace it with a concrete interface
(e.g., PreviewSubAgentImportResult) mirroring the shape used by
previewSkillImport (include fields like state, diagnostics, maybe errors or
metadata) and use that interface as the Promise return type in the mapping
instead of any so it satisfies `@typescript-eslint/no-explicit-any` and provides
precise typing for consumers of previewSubAgentImport.

In `@packages/extension-long-memory/src/plugins/tool.ts`:
- Around line 40-49: The role-default permissions in the plugin meta are
inconsistent: memory_update and memory_delete are enabled by default at the
character level while memory_search is disabled and memory_add doesn't return
IDs, causing update/delete flows to fail; fix by aligning the defaults in the
meta objects for the plugin (the meta blocks that include
defaultMain/defaultChatluna/defaultCharacter/etc.) — either enable memory_search
where memory_update/memory_delete are true (e.g., set the character-level
default for memory_search to true) or disable memory_update/memory_delete to
match memory_search; repeat the same alignment for the other meta blocks
referenced (the other meta objects in this file where default* flags are set).

---

Outside diff comments:
In `@packages/extension-agent/client/components/sub-agent/sub-agent-page.vue`:
- Around line 571-592: The saved payload uses item.name/item.description and
omits promptContent, so edits in the detail page aren't persisted; update the
construction of saved in the save routine (where draft, item, busy,
toRaw(props.config) and saved: SubAgentItemConfig are used) to take name and
description from draft instead of item, and include draft.promptContent (or
draft.promptContent.trim() || undefined) into the saved object (ensuring it
matches the SubAgentItemConfig shape), then keep the rest of the fields
(enabled, model, permissions, etc.) unchanged so the edited
name/description/prompt persist after save.

---

Nitpick comments:
In `@packages/extension-agent/client/components/computer/desktop-panel.vue`:
- Around line 290-314: The duplicated custom scrollbar CSS used by
.desktop-stage (and similar blocks in files-panel, terminal-panel, code-editor)
should be extracted into a shared utility/mixin to remove repetition; create a
single exported CSS mixin or utility class (e.g., .custom-scrollbar or mixin
name) containing the scrollbar rules (scrollbar-width, scrollbar-color,
::-webkit-scrollbar, ::-webkit-scrollbar-track, ::-webkit-scrollbar-thumb,
::-webkit-scrollbar-thumb:hover) and then replace the repeated blocks in
desktop-panel.vue (and the other components) by importing/applying that shared
mixin or adding the utility class to the root element so all components reuse
the same scrollbar styling.

In `@packages/extension-agent/client/components/computer/files-panel.vue`:
- Around line 1107-1133: Merge the duplicate .tabs-scroll selector definitions
into a single consolidated rule: locate both .tabs-scroll blocks and combine the
layout properties (display, align-items, gap, flex, min-width, min-height,
overflow) with the scrollbar styling (scrollbar-width, scrollbar-color and the
::-webkit-scrollbar/track/thumb rules) so no layout or scrollbar rules are lost;
remove the later redundant .tabs-scroll block (or replace it with a comment
linking to the consolidated rule) and ensure the final selector preserves the
original order/positioning concerns so specificity and cascade remain unchanged.
- Around line 1082-1105: There are duplicate .list-body rules; locate both
.list-body blocks and merge them into a single definition by removing the
redundant block and keeping the one that includes the scrollbar rules (ensure
the final .list-body contains position, min-height, max-height, overflow and the
::-webkit-scrollbar / ::-webkit-scrollbar-track / ::-webkit-scrollbar-thumb
rules). Update only the CSS blocks (the selectors .list-body and its
pseudo-elements) so no properties are lost and no duplicate selectors remain.
- Around line 1135-1162: Remove the duplicate .image-stage CSS block that lacks
scrollbar rules and is shadowing the full definition; keep the comprehensive
.image-stage declaration that includes scrollbar styling (the block that defines
display, align-items, justify-content, min-height, height, padding, overflow,
scrollbar-width, scrollbar-color and the ::-webkit-scrollbar rules) and delete
the later, partial .image-stage block so the scroll styling is preserved and not
inadvertently overridden.

In `@packages/extension-agent/client/components/mcp/mcp-servers-view.vue`:
- Around line 1743-1746: The .card-list selector defines flex properties
(flex-direction: column and align-items: stretch) that conflict with the grid
layout already applied earlier; remove the flex-specific declarations from the
.card-list rule in mcp-servers-view.vue so the grid (display: grid and the
--card-cols: 1 mobile behavior) controls layout instead.

In `@packages/extension-agent/client/components/skills/skills-detail.vue`:
- Around line 356-373: The empty catch in async function copyIds() swallows
clipboard errors; update the catch to accept the error (e.g., "err") and log it
(use console.error or the same logging pattern used in tool-detail.vue) with a
clear message like "Failed to write to clipboard" before falling back to the
dialog flow, ensuring copyIds, navigator.clipboard.writeText, copyContent,
showCopyDialog, copyInputRef and nextTick behavior remain unchanged.

In
`@packages/extension-agent/client/components/skills/skills-settings-dialog.vue`:
- Around line 152-166: The watcher currently depends on props.visible,
props.config.dirs and props.config.githubToken but the callback only acts when
props.visible is true; simplify by watching only props.visible (i.e. replace the
dependency array with () => props.visible) and inside the callback copy
props.config.dirs into dirDraft and props.config.githubToken into tokenDraft
when visible; alternatively, if you need reactive updates to dirs/token while
visible, create a separate watch on props.config.dirs and
props.config.githubToken that checks visible before updating dirDraft and
tokenDraft. Ensure you update the watcher invocation named watch and the
referenced variables dirDraft and tokenDraft accordingly.

In `@packages/extension-agent/client/components/sub-agent/preset-dialog.vue`:
- Around line 86-99: The resize handler is attached directly which can fire very
frequently; wrap syncMobile with a throttling function and use that throttled
function for addEventListener/removeEventListener so the same reference is
removed; update the code around syncMobile, onMounted and onBeforeUnmount to
create a throttledSyncMobile (using your preferred throttle utility or a simple
timestamp-based throttle), call throttledSyncMobile initially instead of
syncMobile where appropriate, and ensure window.addEventListener('resize',
throttledSyncMobile) and window.removeEventListener('resize',
throttledSyncMobile) are used to register/unregister the listener.

In `@packages/extension-agent/client/components/sub-agent/sub-agent-detail.vue`:
- Around line 233-237: 模板中使用的是 $emit 而不是局部变量 emit,导致 const emit =
defineEmits<...>() 未被使用;请删除该未使用的 emit 赋值(defineEmits 调用可以直接保留为
defineEmits<...>() 或直接在 setup 中返回事件接口),或者将模板中所有 $emit 调用改为使用 setup 里导出的
emit(统一使用 emit),以消除未使用变量并保持事件触发方式一致(关注 defineEmits、emit 与模板中的 $emit 引用)。

In `@packages/extension-agent/client/components/sub-agent/sub-agent-runs.vue`:
- Around line 205-226: The async function copy(text: string, msg: string)
silently swallows errors from navigator.clipboard.writeText; update the catch in
copy to log the caught error (e.g., using console.debug or process logger) with
context so failures are visible while still falling back to the
textarea/execCommand path; keep the existing fallback behavior and only add the
logging inside the catch block in the copy function.

In `@packages/extension-agent/client/components/tool/tool-page.vue`:
- Around line 517-551: The createItem function overuses "as any" to handle two
shapes (ToolItemConfig and ToolInfo); add a type guard (e.g., isToolInfo) that
detects ToolInfo (check for a representative property like "chatlunaEnabled")
and then branch inside createItem to read fields from the correct type instead
of casting to any; update uses for chatluna/chatlunaEnabled,
character/characterEnabled, characterGroup/characterGroupEnabled,
characterPrivate/characterPrivateEnabled,
characterGroupMode/characterPrivateMode and id arrays
(characterGroupIds/characterPrivateIds) and keep authority defaulting via
defaultAuthority(name). Ensure cloneRule(item?.subAgents) remains unchanged and
return a ToolItemConfig.

In `@packages/extension-agent/src/service/index.ts`:
- Around line 331-334: Replace the dynamic CommonJS require in
previewSubAgentImport with a top-level ES import: add "import {
parseAgentFrontmatter } from '../sub-agent/parse'" at the top of the module and
simplify the async previewSubAgentImport(data: string) implementation to
directly return parseAgentFrontmatter(data, 'preview'); ensure the imported
symbol name matches parseAgentFrontmatter and remove the inline require to
comply with ES module (ES2022) guidelines.

In `@packages/extension-agent/src/sub-agent/run.ts`:
- Around line 281-303: formatTraceText is only used once and per style you
should inline it: replace the single call to formatTraceText(...) with the
function's logic (preserve all branches: string return, Array.isArray branch
mapping items with 'text' property fallback to JSON.stringify, and final
JSON.stringify fallback) at the call site, then remove the formatTraceText
function declaration; if you prefer to keep it for readability, leave it
unchanged but add a short comment above formatTraceText explaining its purpose
and why it remains a separate helper.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ace16abe-c5d3-4d0a-8a95-91bc0abe9739

📥 Commits

Reviewing files that changed from the base of the PR and between 0afb139 and 2f7cb49.

📒 Files selected for processing (67)
  • packages/core/src/llm-core/platform/types.ts
  • packages/core/src/services/chat.ts
  • packages/core/src/services/types.ts
  • packages/extension-agent/client/components/computer/computer-page.vue
  • packages/extension-agent/client/components/computer/config-backends/backend-e2b.vue
  • packages/extension-agent/client/components/computer/config-backends/backend-local.vue
  • packages/extension-agent/client/components/computer/config-backends/backend-open-terminal.vue
  • packages/extension-agent/client/components/computer/configuration-panel.vue
  • packages/extension-agent/client/components/computer/desktop-panel.vue
  • packages/extension-agent/client/components/computer/files-panel.vue
  • packages/extension-agent/client/components/computer/status-panel.vue
  • packages/extension-agent/client/components/computer/terminal-panel.vue
  • packages/extension-agent/client/components/layout/agent-shell.vue
  • packages/extension-agent/client/components/layout/agent-sidebar.vue
  • packages/extension-agent/client/components/mcp/mcp-page.vue
  • packages/extension-agent/client/components/mcp/mcp-servers-view.vue
  • packages/extension-agent/client/components/shared/code-editor.vue
  • packages/extension-agent/client/components/shared/config-record-page.vue
  • packages/extension-agent/client/components/skills/skills-detail.vue
  • packages/extension-agent/client/components/skills/skills-diagnostics-dialog.vue
  • packages/extension-agent/client/components/skills/skills-import-folder-dialog.vue
  • packages/extension-agent/client/components/skills/skills-import-github-dialog.vue
  • packages/extension-agent/client/components/skills/skills-import-markdown-dialog.vue
  • packages/extension-agent/client/components/skills/skills-page.vue
  • packages/extension-agent/client/components/skills/skills-settings-dialog.vue
  • packages/extension-agent/client/components/sub-agent/permission-editor.vue
  • packages/extension-agent/client/components/sub-agent/preset-dialog.vue
  • packages/extension-agent/client/components/sub-agent/sub-agent-availability.vue
  • packages/extension-agent/client/components/sub-agent/sub-agent-catalog.vue
  • packages/extension-agent/client/components/sub-agent/sub-agent-detail.vue
  • packages/extension-agent/client/components/sub-agent/sub-agent-import-markdown-dialog.vue
  • packages/extension-agent/client/components/sub-agent/sub-agent-page.vue
  • packages/extension-agent/client/components/sub-agent/sub-agent-runs.vue
  • packages/extension-agent/client/components/tool/tool-detail.vue
  • packages/extension-agent/client/components/tool/tool-edit-dialog.vue
  • packages/extension-agent/client/components/tool/tool-page.vue
  • packages/extension-agent/src/cli/dispatch.ts
  • packages/extension-agent/src/cli/types.ts
  • packages/extension-agent/src/computer/backends/open_terminal.ts
  • packages/extension-agent/src/config/defaults.ts
  • packages/extension-agent/src/config/read.ts
  • packages/extension-agent/src/service/computer.ts
  • packages/extension-agent/src/service/index.ts
  • packages/extension-agent/src/service/permissions.ts
  • packages/extension-agent/src/service/skills.ts
  • packages/extension-agent/src/service/sub_agent.ts
  • packages/extension-agent/src/skills/catalog.ts
  • packages/extension-agent/src/skills/import.ts
  • packages/extension-agent/src/skills/render.ts
  • packages/extension-agent/src/skills/scan.ts
  • packages/extension-agent/src/sub-agent/parse.ts
  • packages/extension-agent/src/sub-agent/run.ts
  • packages/extension-agent/src/types.ts
  • packages/extension-agent/src/types/skills.ts
  • packages/extension-agent/src/types/sub_agent.ts
  • packages/extension-agent/src/types/tool.ts
  • packages/extension-agent/src/webui/index.ts
  • packages/extension-long-memory/src/plugins/tool.ts
  • packages/extension-tools/src/plugins/command.ts
  • packages/extension-tools/src/plugins/cron.ts
  • packages/extension-tools/src/plugins/file_sender.ts
  • packages/extension-tools/src/plugins/group.ts
  • packages/extension-tools/src/plugins/music.ts
  • packages/extension-tools/src/plugins/request.ts
  • packages/extension-tools/src/plugins/think.ts
  • packages/extension-tools/src/plugins/todos.ts
  • packages/service-search/src/chain/browsing_chain.ts
💤 Files with no reviewable changes (1)
  • packages/extension-agent/client/components/tool/tool-edit-dialog.vue

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/extension-agent/src/sub-agent/parse.ts (1)

161-170: ⚠️ Potential issue | 🟠 Major

computer 的缺省解析还停留在 deny,round-trip 会悄悄降权。

Line 169 只修正了“permissions 对象存在但没写 computer”的分支;如果 frontmatter 完全没有 permissionscreatePermissionConfig() 在 Line 367 仍然先给出 deny。这和 packages/extension-agent/src/config/defaults.ts Line 79 的 inherit 默认值不一致,而 packages/extension-agent/src/sub-agent/markdown.ts 也会在未配置时省略整个 permissions 块,所以手动导出再扫描后 Computer 权限会被错误地收紧。

🧩 建议修改
 function createPermissionConfig(): SubAgentPermissionConfig {
     return {
         skills: createRule(undefined, 'inherit'),
         mcp: createRule(undefined, 'inherit'),
         tools: createRule(undefined, 'inherit'),
-        computer: createRule(undefined, 'deny')
+        computer: createRule(undefined, 'inherit')
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension-agent/src/sub-agent/parse.ts` around lines 161 - 170, The
Computer permission defaults to 'deny' in createPermissionConfig(), causing
silent downgrades when frontmatter omits the permissions block; change
createPermissionConfig() so the default for permissions.computer is 'inherit'
(matching packages/extension-agent/src/config/defaults.ts and the behavior of
markdown.ts which may omit the permissions block), or use createRule(undefined,
'inherit')/equivalent when no explicit value exists; ensure the permissions
object (and specifically the permissions.computer path used alongside
createRule) returns 'inherit' when frontmatter is entirely missing.
♻️ Duplicate comments (3)
packages/extension-agent/src/service/permissions.ts (1)

321-353: ⚠️ Potential issue | 🟠 Major

缺少会话 ID 时,character 权限判断会放行。

id 取不到值时,denyall 分支都可能返回 true,所以缺少 userId / guildId / channelId 的会话会绕过 character 的私聊/群聊限制。这个 helper 现在被 skills、sub-agent 和 tool mask 共用,建议在取完 id 后直接 fail closed。

🛡️ 建议修改
         const id = session?.isDirect
             ? session.userId
             : (session?.guildId ?? session?.channelId)
+
+        if (!id) {
+            return false
+        }
 
         if (session?.isDirect === true) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension-agent/src/service/permissions.ts` around lines 321 - 353,
The permission check currently derives an `id` from `session` and then evaluates
private/group rules, but when `id` is falsy the `'deny'` and default branches
can incorrectly allow access; update the helper (the block using `id`,
`session?.isDirect`, `item.characterPrivateEnabled`,
`item.characterPrivateMode`, `item.characterPrivateIds`,
`item.characterGroupEnabled`, `item.characterGroupMode`,
`item.characterGroupIds`) to immediately "fail closed" if `id` is missing (e.g.,
return false as soon as `const id = ...` yields a falsy value), ensuring all
subsequent `allow`/`deny` checks only run when a valid `id` exists.
packages/extension-agent/src/service/sub_agent.ts (1)

331-341: ⚠️ Potential issue | 🔴 Critical

一级 handoff 仍然会绕过当前会话的 toolMask

runTask() 这里没有把 runConfig.configurable.toolMask 透传进 runSubAgent(),而 runSubAgent() 只看 parentSubagentContext?.toolMask。一级 handoff 时 parentSubagentContext 为空,子代理就会重新拿到比当前会话更宽的工具集合。

🛠️ 建议修改
         try {
             return await this.runSubAgent({
                 agentId: info.id,
                 prompt,
                 session,
                 parentConversationId: conversationId,
                 parentSubagentContext: parent,
+                toolMask: runConfig?.configurable?.toolMask,
                 model: runConfig?.configurable?.model,
                 task: next,
                 background: input.background === true
             })
@@
-        const base = options.parentSubagentContext?.toolMask
+        const base =
+            options.toolMask ?? options.parentSubagentContext?.toolMask

同时别忘了给 RunSubAgentOptions 补上 toolMask 字段。

Also applies to: 375-378

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension-agent/src/service/sub_agent.ts` around lines 331 - 341,
The call site runTask should forward the current session's tool mask into
runSubAgent so sub-agents respect the session's tool restrictions: add
runConfig?.configurable?.toolMask to the options object passed to
this.runSubAgent (both in the shown call and the similar call around the other
occurrence), and update the RunSubAgentOptions type to include a toolMask field
so runSubAgent can read it (in addition to existing
parentSubagentContext?.toolMask).
packages/extension-agent/client/components/sub-agent/sub-agent-page.vue (1)

50-68: ⚠️ Potential issue | 🟠 Major

列表页 tabs 仍然只能靠鼠标切换。

这里还是 div + @click``,缺少语义和键盘处理,键盘用户没法访问“列表 / 运行记录 / 工具可用性”这组主导航。建议至少改成 button,或者直接接入可访问的 tabs 组件。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension-agent/client/components/sub-agent/sub-agent-page.vue`
around lines 50 - 68, The tab items are currently plain divs with `@click` (the
three elements using :class="['tab', { active: listTab === '...'}]" and
`@click`="listTab = '...'", bound to the listTab state) which lack semantic button
roles and keyboard support; replace each tab div with a native interactive
element (e.g., <button>) or wire up proper ARIA tab semantics
(role="tab"/role="tablist") and add keyboard handlers for Enter/Space and
arrow-key navigation to update listTab, ensure focusability (tabindex) and keep
the existing :class binding and active logic so clicking or keyboard activation
sets listTab to 'catalog' / 'runs' / 'availability'.
🧹 Nitpick comments (3)
packages/extension-agent/client/components/skills/skills-detail.vue (1)

21-30: Tab 导航缺少键盘支持(可选改进)。

当前 tab 使用 <div> + @click 实现,缺少 tabindexrole="tab" 和键盘事件处理。建议使用 Element Plus 的 <el-tabs> 组件,或为自定义 tab 添加 ARIA 属性和键盘导航支持。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension-agent/client/components/skills/skills-detail.vue` around
lines 21 - 30, The custom tab list uses clickable <div>s without keyboard/ARIA
support; update the tab rendering (the v-for over tabs and the reactive tab
variable) to either switch to Element Plus <el-tabs>/<el-tab-pane> or, if
keeping the custom markup, add role="tablist" on the container and role="tab",
tabindex="0" and aria-selected bindings on each tab item, and add a `@keydown`
handler on each tab to handle Enter/Space (activate) and ArrowLeft/ArrowRight
(move focus and change the tab reactive variable) so keyboard users can navigate
and activate tabs; ensure the v-for items (tabs array) and the tab assignment
(tab = item.value) are used within the updated handlers to change selection and
manage focus.
packages/extension-agent/client/components/skills/skills-page.vue (1)

622-647: createItem 函数过于复杂,类型断言过多。

该函数混合处理 SkillConfigSkillInfo 两种类型,大量使用 as 类型断言使代码难以维护。建议引入类型守卫或将逻辑拆分为两个独立的映射函数。

重构建议
function isSkillInfo(item: unknown): item is SkillInfo {
    return !!item && typeof (item as SkillInfo).chatlunaEnabled === 'boolean'
}

function createItem(item?: Partial<SkillConfig> | SkillInfo): SkillConfig {
    if (!item) {
        return getDefaultSkillConfig()
    }
    
    if (isSkillInfo(item)) {
        return createItemFromInfo(item)
    }
    
    return createItemFromConfig(item)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension-agent/client/components/skills/skills-page.vue` around
lines 622 - 647, The createItem function mixes SkillConfig and SkillInfo with
many unsafe casts; split the mapping into clear paths by adding a type guard
(e.g., isSkillInfo(item): item is SkillInfo) and implement two dedicated mappers
createItemFromInfo(item: SkillInfo) and createItemFromConfig(item:
Partial<SkillConfig>) plus getDefaultSkillConfig() for undefined input; update
createItem to return getDefaultSkillConfig() when no item, call
createItemFromInfo when isSkillInfo(item) is true, otherwise call
createItemFromConfig, moving the existing property-mapping logic into the two
mapper functions and removing all the (item as ...) assertions.
packages/extension-agent/src/service/index.ts (1)

331-334: parseAgentFrontmatter 改成静态 import

这里在 TS 模块里动态 require(),会继续触发 no-var-requires;如果当前包产物已经是 ESM,运行时也拿不到 require。直接放到文件顶部静态导入更稳,请顺手确认一下当前 tsconfig / package.json 的模块配置。

♻️ 建议修改
 import {
     createSubAgentMarkdown,
     getSubAgentFileName
 } from '../sub-agent/markdown'
+import { parseAgentFrontmatter } from '../sub-agent/parse'
@@
     async previewSubAgentImport(data: string) {
-        const { parseAgentFrontmatter } = require('../sub-agent/parse')
         return parseAgentFrontmatter(data, 'preview')
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension-agent/src/service/index.ts` around lines 331 - 334, The
function previewSubAgentImport uses a runtime require to load
parseAgentFrontmatter which violates no-var-requires and breaks in ESM builds;
replace the dynamic require with a static top-level import of
parseAgentFrontmatter from '../sub-agent/parse' (update the import list at the
top of the file) and remove the require inside previewSubAgentImport, keeping
the implementation returning parseAgentFrontmatter(data, 'preview'); also verify
the package/module settings in tsconfig/package.json to ensure static imports
are supported.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/extension-agent/client/components/skills/skills-page.vue`:
- Around line 649-655: The cloneRule function currently maps PermissionRule.mode
to 'all' whenever it's not 'allow' or 'deny', dropping the 'inherit' value;
update cloneRule to preserve 'inherit' by checking for 'allow'|'deny'|'inherit'
(i.e., use rule.mode when it equals 'allow', 'deny', or 'inherit') and only
default to 'all' when mode is truly undefined/invalid; reference the cloneRule
function and the PermissionRule.mode union in your change, and if the UI
intentionally disallows 'inherit' confirm that behavior rather than silently
converting it.

In `@packages/extension-agent/client/components/sub-agent/sub-agent-detail.vue`:
- Around line 32-41: The tab controls rendered in the template (the div with
class "tabs-underline" that v-for iterates over "tabs" and each ".tab-item"
which sets `@click`="tab = item.value") are non-semantic and not
keyboard-accessible; replace those clickable divs with semantic interactive
elements (e.g., <button> for each tab or use an ARIA tabs widget) or add proper
role="tab", tabindex, and keyboard event handlers (Enter/Space/arrow keys) wired
to the same state update (the "tab" reactive variable) to enable keyboard
navigation and screen reader support; apply the same changes to the similar
block around lines 182-195 (the private/chat switch) so both the main tabs and
the "私聊 / 群聊" toggle use semantic buttons or a fully accessible tabs component.

In `@packages/extension-agent/client/components/sub-agent/sub-agent-page.vue`:
- Around line 456-458: canEditPreview currently enables editing for any
non-remote markdown agent which causes savePreview() to call the generic
addSubAgent API and always write into the data directory (via addSubAgent /
getSubAgentsRootPath) instead of updating the agent's original file; either
restrict editing to agents stored in the data scope or add a new backend
endpoint to save to the agent's original path and call it from savePreview when
previewItem.value.path (or scope !== 'data') is present. Update canEditPreview
or savePreview to check previewItem.value.path/previewItem.value.scope and route
to the new save API (e.g., saveSubAgentAtPath) for external files, otherwise
fall back to addSubAgent for data-scoped agents.

In `@packages/extension-agent/client/components/tool/tool-detail.vue`:
- Around line 22-29: Replace non-interactive elements used for controls with
real interactive controls: change the tab items rendered by the v-for div (the
element that sets tab = item.value) to a <button> or <el-button> with
type="button" and add accessible state (e.g., aria-pressed or aria-current when
tab === item.value); likewise replace the copy/clear/delete el-icon wrappers
with buttons (or wrap icons inside <button>/<el-button>) that call the same
handlers. Ensure each new button is focusable by default, preserves the original
`@click` logic, and also wires keyboard activation by handling Enter/Space (or
relying on native button behavior) and includes appropriate aria-labels for
assistive tech.

In `@packages/extension-agent/src/config/defaults.ts`:
- Around line 105-107: The current assignment for subAgents reuses the
input.subAgents object (subAgents: input.subAgents ??
createPermissionRule('all')), causing drafts to share and mutate the original
permission object (draft.subAgents.mode/allow/deny are edited in
tool-detail.vue); instead, normalize/clone the permission object so mode, allow
and deny are shallow-copied into a new object when building defaults (e.g.,
construct subAgents = input.subAgents ? { mode: input.subAgents.mode, allow:
[...(input.subAgents.allow ?? [])], deny: [...(input.subAgents.deny ?? [])] } :
createPermissionRule('all')), and apply the same cloning fix at the other
occurrence mentioned (lines ~138-140) to avoid in-place mutations of the source
permission object.

In `@packages/extension-agent/src/service/skills.ts`:
- Around line 494-506: The prompt list is still built from this._catalog and
injects full-mode skills even when running a sub-agent, bypassing the sub-agent
permission filter and duplicating skill prompts; instead of filtering directly
with this._catalog.filter(...), call the existing permission.filterSkills(...)
to produce the prompt (reusing the same agent/session/source arguments as used
in sub-agent/run.ts) so subagentContext paths are filtered the same way; update
both occurrences that build prompt (the block using canUseSkill(...) around
mask/hasTool and the similar block at 521-536) to use
permission.filterSkills(...) rather than raw this._catalog.filter(...) so
full-mode skills are only included when allowed.

In `@packages/extension-agent/src/sub-agent/parse.ts`:
- Around line 144-145: The current code uses readNames() to parse
characterGroupIds and characterPrivateIds which causes values to be passed
through mapCompatToolName() (so IDs like "webfetch", "read", "agent(123)" get
rewritten); instead preserve the original strings by replacing
readNames(characterGroupIds) and readNames(characterPrivateIds) with a parser
that returns raw strings (e.g., a readRawStrings / parseStringList or simply
split/trim without name-mapping), ensuring the values written back by the
frontmatter writer in markdown.ts remain identical to the original IDs and are
not transformed by mapCompatToolName().

---

Outside diff comments:
In `@packages/extension-agent/src/sub-agent/parse.ts`:
- Around line 161-170: The Computer permission defaults to 'deny' in
createPermissionConfig(), causing silent downgrades when frontmatter omits the
permissions block; change createPermissionConfig() so the default for
permissions.computer is 'inherit' (matching
packages/extension-agent/src/config/defaults.ts and the behavior of markdown.ts
which may omit the permissions block), or use createRule(undefined,
'inherit')/equivalent when no explicit value exists; ensure the permissions
object (and specifically the permissions.computer path used alongside
createRule) returns 'inherit' when frontmatter is entirely missing.

---

Duplicate comments:
In `@packages/extension-agent/client/components/sub-agent/sub-agent-page.vue`:
- Around line 50-68: The tab items are currently plain divs with `@click` (the
three elements using :class="['tab', { active: listTab === '...'}]" and
`@click`="listTab = '...'", bound to the listTab state) which lack semantic button
roles and keyboard support; replace each tab div with a native interactive
element (e.g., <button>) or wire up proper ARIA tab semantics
(role="tab"/role="tablist") and add keyboard handlers for Enter/Space and
arrow-key navigation to update listTab, ensure focusability (tabindex) and keep
the existing :class binding and active logic so clicking or keyboard activation
sets listTab to 'catalog' / 'runs' / 'availability'.

In `@packages/extension-agent/src/service/permissions.ts`:
- Around line 321-353: The permission check currently derives an `id` from
`session` and then evaluates private/group rules, but when `id` is falsy the
`'deny'` and default branches can incorrectly allow access; update the helper
(the block using `id`, `session?.isDirect`, `item.characterPrivateEnabled`,
`item.characterPrivateMode`, `item.characterPrivateIds`,
`item.characterGroupEnabled`, `item.characterGroupMode`,
`item.characterGroupIds`) to immediately "fail closed" if `id` is missing (e.g.,
return false as soon as `const id = ...` yields a falsy value), ensuring all
subsequent `allow`/`deny` checks only run when a valid `id` exists.

In `@packages/extension-agent/src/service/sub_agent.ts`:
- Around line 331-341: The call site runTask should forward the current
session's tool mask into runSubAgent so sub-agents respect the session's tool
restrictions: add runConfig?.configurable?.toolMask to the options object passed
to this.runSubAgent (both in the shown call and the similar call around the
other occurrence), and update the RunSubAgentOptions type to include a toolMask
field so runSubAgent can read it (in addition to existing
parentSubagentContext?.toolMask).

---

Nitpick comments:
In `@packages/extension-agent/client/components/skills/skills-detail.vue`:
- Around line 21-30: The custom tab list uses clickable <div>s without
keyboard/ARIA support; update the tab rendering (the v-for over tabs and the
reactive tab variable) to either switch to Element Plus <el-tabs>/<el-tab-pane>
or, if keeping the custom markup, add role="tablist" on the container and
role="tab", tabindex="0" and aria-selected bindings on each tab item, and add a
`@keydown` handler on each tab to handle Enter/Space (activate) and
ArrowLeft/ArrowRight (move focus and change the tab reactive variable) so
keyboard users can navigate and activate tabs; ensure the v-for items (tabs
array) and the tab assignment (tab = item.value) are used within the updated
handlers to change selection and manage focus.

In `@packages/extension-agent/client/components/skills/skills-page.vue`:
- Around line 622-647: The createItem function mixes SkillConfig and SkillInfo
with many unsafe casts; split the mapping into clear paths by adding a type
guard (e.g., isSkillInfo(item): item is SkillInfo) and implement two dedicated
mappers createItemFromInfo(item: SkillInfo) and createItemFromConfig(item:
Partial<SkillConfig>) plus getDefaultSkillConfig() for undefined input; update
createItem to return getDefaultSkillConfig() when no item, call
createItemFromInfo when isSkillInfo(item) is true, otherwise call
createItemFromConfig, moving the existing property-mapping logic into the two
mapper functions and removing all the (item as ...) assertions.

In `@packages/extension-agent/src/service/index.ts`:
- Around line 331-334: The function previewSubAgentImport uses a runtime require
to load parseAgentFrontmatter which violates no-var-requires and breaks in ESM
builds; replace the dynamic require with a static top-level import of
parseAgentFrontmatter from '../sub-agent/parse' (update the import list at the
top of the file) and remove the require inside previewSubAgentImport, keeping
the implementation returning parseAgentFrontmatter(data, 'preview'); also verify
the package/module settings in tsconfig/package.json to ensure static imports
are supported.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: fe0cdc10-6fd6-4df0-a343-8014e6ea7d45

📥 Commits

Reviewing files that changed from the base of the PR and between 2f7cb49 and af786b3.

📒 Files selected for processing (19)
  • packages/extension-agent/client/components/skills/skills-detail.vue
  • packages/extension-agent/client/components/skills/skills-page.vue
  • packages/extension-agent/client/components/sub-agent/sub-agent-detail.vue
  • packages/extension-agent/client/components/sub-agent/sub-agent-page.vue
  • packages/extension-agent/client/components/tool/tool-detail.vue
  • packages/extension-agent/src/config/defaults.ts
  • packages/extension-agent/src/service/index.ts
  • packages/extension-agent/src/service/permissions.ts
  • packages/extension-agent/src/service/skills.ts
  • packages/extension-agent/src/service/sub_agent.ts
  • packages/extension-agent/src/skills/catalog.ts
  • packages/extension-agent/src/sub-agent/builtin.ts
  • packages/extension-agent/src/sub-agent/manual.ts
  • packages/extension-agent/src/sub-agent/markdown.ts
  • packages/extension-agent/src/sub-agent/parse.ts
  • packages/extension-agent/src/sub-agent/preset.ts
  • packages/extension-agent/src/sub-agent/scan.ts
  • packages/extension-agent/src/types/skills.ts
  • packages/extension-agent/src/types/sub_agent.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/extension-agent/src/skills/catalog.ts
  • packages/extension-agent/src/types/sub_agent.ts
  • packages/extension-agent/src/types/skills.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/extension-agent/src/sub-agent/parse.ts (1)

165-170: ⚠️ Potential issue | 🟡 Minor

调整 fallback 逻辑后无需额外确认——computer 权限处理现已与其他权限类型一致。

变更将 createRule(item.computer, 'inherit') 作为 frontmatter permissions 中 computer 字段的 fallback(之前为 'deny')。实际上,现存唯一有 permissions 段落的 agent 文件(sub-agent-creator/SKILL.md)在所有 4 个 agent 定义中都显式指定了 computer 字段,因此这个变更不影响现有文件

变更的实际效果是:未来新增的 agent 如果定义了 permissions 段落但省略 computer 字段,将采用 'inherit' 模式而非 'deny'。这与 skillsmcptoolscreatePermissionConfig() 中的行为一致,形成了统一的继承策略。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension-agent/src/sub-agent/parse.ts` around lines 165 - 170, The
change makes `computer` use the same 'inherit' fallback as the other permission
types, so remove any leftover special-case handling that previously defaulted
`computer` to 'deny' and ensure consistency: use createRule(item.computer,
'inherit') where permissions.computer is assigned, align any logic in
createPermissionConfig() and related code that previously treated `computer`
differently, and update/remove tests or comments that assumed the old 'deny'
fallback.
🧹 Nitpick comments (8)
packages/extension-agent/src/service/sub_agent.ts (1)

684-710: buildToolMask 总是返回 'allow' 模式,可能不是最优选择。

packages/extension-agent/src/service/permissions.ts 中的同名函数相比,这里的 buildToolMask 总是返回 mode: 'allow',而不会根据 allow/deny 列表大小选择更紧凑的表示。

对于工具数量较多的场景,始终使用 allow 模式可能会导致较大的 mask 对象。不过考虑到 sub-agent 通常只能访问有限的工具集,这个简化是可以接受的。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension-agent/src/service/sub_agent.ts` around lines 684 - 710,
buildToolMask currently always returns a ToolMask with mode: 'allow', which can
produce a larger mask than necessary; update buildToolMask (used by
buildSubAgentToolMask) to choose between 'allow' and 'deny' modes based on which
list (allow vs computed deny) yields a more compact representation (mirror the
behavior in permissions.ts): compute the full set of known tools or derive deny
= allTools - allow, then return mode:'deny' with deny when deny.length <
allow.length, otherwise return mode:'allow'; ensure the function signature and
return type (ToolMask) remain unchanged and that buildSubAgentToolMask continues
to call buildToolMask(allow).
packages/extension-agent/client/components/sub-agent/sub-agent-catalog.vue (2)

625-649: 合并重复的 .search-input CSS 选择器。

Line 625-649 中的 .search-input 样式与 Line 386-390 重复(静态分析工具也指出了这一点)。建议将移动端样式整合到一个规则块中。

♻️ 建议修改
 `@media` (max-width: 768px) {
-    .search-input {
-        width: 100%;
-    }
-
     .search-row {
         width: 100%;
         min-width: 0;
         flex: none;
         display: grid;
         grid-template-columns: 1fr;
         gap: 10px;
         align-items: stretch;
     }

     .filter-trigger {
         min-width: 0;
         width: 100%;
         flex: none;
     }

     .search-input {
         width: 100% !important;
         min-width: 0;
         flex: none;
     }
     /* 保留一个 .search-input 规则块即可 */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension-agent/client/components/sub-agent/sub-agent-catalog.vue`
around lines 625 - 649, There are duplicate .search-input rules; consolidate
them into a single CSS rule by merging the properties (width:100%, min-width:0,
flex:none and the !important usage) into the original .search-input block (the
earlier occurrence around the existing .search-row/.filter-trigger rules) and
remove the redundant later rule; ensure the merged rule retains any necessary
!important and min-width/flex declarations so layout behavior is unchanged.

240-256: 筛选逻辑使用 AND 语义可能导致意外的空结果。

当前 filters.value.every(...) 实现意味着所有选中的筛选条件必须同时满足。例如,同时选择「启用」和「禁用」将不会显示任何结果,因为一个 agent 不可能同时处于启用和禁用状态。

如果这是预期行为,建议在 UI 上提示用户筛选条件是 AND 关系;否则,对于互斥条件(如 enabled:yes/no)考虑使用 OR 语义。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension-agent/client/components/sub-agent/sub-agent-catalog.vue`
around lines 240 - 256, The current filter check uses filters.value.every(...)
which forces all selected filters to be true simultaneously (AND semantics) and
causes contradictory selections like enabled:yes and enabled:no to yield no
results; update the logic inside the block that currently invokes
filters.value.every to instead group filters by their key (the substring before
':'), apply OR semantics within each group (e.g., for key "enabled" allow item
if any enabled:yes/no in that group matches item.enabled), and keep AND
semantics across different keys (e.g., enabled + source must both match);
operate on the existing symbols (filters.value, the predicate that examines
item.enabled, item.state, item.hidden, item.source) and replace the single
every(...) predicate with grouping+matching logic that checks per-key OR and
cross-key AND.
packages/extension-agent/client/components/skills/skills-detail.vue (1)

621-653: 合并重复的 .inner-tab 样式定义。

同一个选择器被拆成两段后,后面的块会静默覆盖前面的属性,后续改样式时很容易漏掉其中一段。这里合并成一个声明会更稳,也能顺手消掉当前的 duplicate selector 告警。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension-agent/client/components/skills/skills-detail.vue` around
lines 621 - 653, There are two separate .inner-tab CSS blocks that silently
override each other; merge them into a single .inner-tab rule combining all
properties (background, border, font-size, color, cursor, font-weight) and
remove the duplicate block so styling is consolidated and the duplicate selector
warning is resolved; update the .inner-tab declaration in skills-detail.vue
accordingly.
packages/extension-agent/client/components/sub-agent/sub-agent-page.vue (4)

711-726: 函数参数缩进不一致。

options 对象的属性缩进层级不统一,allowKoishiMessageTransform(Line 725)与其他属性的缩进不对齐。建议统一格式化。

♻️ 统一缩进
 async function createPresetAgent(
     name: string,
     preset: string,
-        options: {
-            description: string
-            chatluna?: boolean
-            character?: boolean
-            characterGroup?: boolean
-            characterPrivate?: boolean
-            characterGroupMode?: 'all' | 'allow' | 'deny'
-            characterPrivateMode?: 'all' | 'allow' | 'deny'
-            characterGroupIds?: string[]
-            characterPrivateIds?: string[]
-            authority?: number
-            model: string | undefined
-            maxTurns: number
-            hidden: boolean
-        allowKoishiMessageTransform: boolean
-    }
+    options: {
+        description: string
+        chatluna?: boolean
+        character?: boolean
+        characterGroup?: boolean
+        characterPrivate?: boolean
+        characterGroupMode?: 'all' | 'allow' | 'deny'
+        characterPrivateMode?: 'all' | 'allow' | 'deny'
+        characterGroupIds?: string[]
+        characterPrivateIds?: string[]
+        authority?: number
+        model: string | undefined
+        maxTurns: number
+        hidden: boolean
+        allowKoishiMessageTransform: boolean
+    }
 ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension-agent/client/components/sub-agent/sub-agent-page.vue`
around lines 711 - 726, The options property's field indentation is
inconsistent: align allowKoishiMessageTransform with the other fields inside the
options object so it uses the same indentation level as description, chatluna,
character, etc.; update the options block in sub-agent-page.vue (the options
object definition) so all property lines share the same indent and the closing
brace stays aligned.

526-536: 考虑对深度侦听器进行防抖处理,避免频繁触发网络请求。

当前对 props.status.catalogprops.toolsprops.skills 同时进行深度监听,任何嵌套属性的变化都会触发 loadDynamicData()。如果这些对象频繁更新,可能导致大量冗余的 API 调用。

♻️ 建议使用 debounce 或 watchDebounced
+import { useDebounceFn } from '@vueuse/core'
+
+const debouncedLoadDynamicData = useDebounceFn(loadDynamicData, 300)
+
 watch(
     [
         () => props.status.catalog,
         () => props.tools,
         () => props.skills
     ],
     async () => {
-        await loadDynamicData()
+        await debouncedLoadDynamicData()
     },
     { deep: true }
 )

如果项目未使用 @vueuse/core,也可以使用 lodash 的 debounce 或手动实现。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension-agent/client/components/sub-agent/sub-agent-page.vue`
around lines 526 - 536, The deep watcher on props.status.catalog, props.tools
and props.skills calls loadDynamicData() on any nested change and should be
debounced to avoid redundant API calls; modify the watch usage (watch([...],
async () => await loadDynamicData(), { deep: true })) to wrap the callback with
a debounce (e.g., use watchDebounced from `@vueuse/core` or lodash.debounce) or
debounce loadDynamicData itself, keeping the same watched sources
(props.status.catalog, props.tools, props.skills) and preserving deep: true,
ensuring the debounced function is properly cleaned up on component unmount.

543-545: 可考虑对 resize 事件处理进行节流。

resize 事件在窗口调整大小时会高频触发,直接更新响应式状态可能导致不必要的重渲染。

♻️ 使用 throttle 或 passive listener
+import { useThrottleFn } from '@vueuse/core'
+
+const onResize = useThrottleFn(() => {
+    mobile.value = window.innerWidth <= 768
+}, 100)
+
-function onResize() {
-    mobile.value = window.innerWidth <= 768
-}

或者考虑使用 @vueuse/coreuseMediaQuery 来替代手动监听:

import { useMediaQuery } from '@vueuse/core'
const mobile = useMediaQuery('(max-width: 768px)')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension-agent/client/components/sub-agent/sub-agent-page.vue`
around lines 543 - 545, The onResize handler updates the reactive mobile state
on every window resize causing potentially high-frequency re-renders; replace
the direct listener with a throttled or debounced wrapper (e.g., throttle the
onResize callback) or switch to useMediaQuery from `@vueuse/core` (e.g., const
mobile = useMediaQuery('(max-width: 768px)')) and ensure the window resize
listener (and its cleanup) is updated to use the throttled function if you keep
manual listening; update references to the onResize function and the mobile ref
accordingly.

459-469: 外部 Agent 的名称字段允许编辑但保存时会被阻止,建议在 UI 层面提前限制。

当前逻辑允许编辑非 data 作用域的 markdown Agent,但在 savePreview 中(Line 787-790)会阻止改名操作。用户可能会先修改名称,然后在保存时才发现不允许,体验不佳。

♻️ 建议在 UI 层面禁用外部 Agent 的名称编辑
+const canRenamePreviewed = computed(() => {
+    return previewItem.value?.scope === 'data'
+})
+
 <el-input
     v-model="previewDraft.name"
     placeholder="例如:my-agent"
-    :disabled="!canEditPreview"
+    :disabled="!canEditPreview || !canRenamePreviewed"
 />

或者在名称输入框旁添加提示,说明外部 Agent 不支持改名。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension-agent/client/components/sub-agent/sub-agent-page.vue`
around lines 459 - 469, The UI allows editing the name for markdown Agents that
are not data-scoped but savePreview rejects renaming for external Agents,
causing a bad UX; update the component to disable the name input (or show a
tooltip) when the Agent is external by deriving a new computed (or extend
canEditPreview) based on previewItem.value?.source === 'markdown' &&
!previewItem.value?.remote && previewItem.value?.scope === 'data' (or simply
require previewItem.value.remote === false), and ensure canSavePreview uses the
same rule so the name field cannot be edited or lead to a blocked save;
reference canEditPreview, canSavePreview, previewItem, previewDraft and
savePreview when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/extension-agent/client/components/skills/skills-import-folder-dialog.vue`:
- Around line 203-219: The UI still promises "启用" while the backend
importSkills() now creates items with { enabled: false, mode: 'off' }; update
all user-facing strings in skills-import-folder-dialog.vue (including the footer
message block using canImport/selected/replaceCount, the primary button label
bound where importing/data are used, and any success notification emitted by
importSkills) to say "导入" or "导入(需手动启用)" consistently; also adjust any
conditional messaging that reads "现在可以直接导入并启用" to "现在可以直接导入(需手动启用)" so users
aren’t misled.

In `@packages/extension-agent/src/service/index.ts`:
- Around line 429-433: The current check calls info.promptContent.trim() which
can throw if promptContent is null/undefined; update the guard around the
catalog lookup (the result of this.subAgent.getCatalogSync().find(...), stored
in info) to use optional chaining when checking promptContent (e.g., replace the
condition with a check such as !info || !(info.promptContent?.trim())) so
sub-agents with no body are silently skipped without causing a runtime error.

In `@packages/extension-agent/src/service/permissions.ts`:
- Around line 299-354: The permission check in isSessionAllowed mishandles
source === 'character' when session is undefined (id becomes undefined and
includes checks behave asymmetrically); add an early explicit branch at the top
of isSessionAllowed to handle when source === 'character' && !session (e.g.,
return false to deny by default) so subsequent logic that references session,
id, characterPrivateMode, characterGroupMode, characterPrivateIds and
characterGroupIds never runs with undefined session; update or document callers
if a different policy is desired.

---

Outside diff comments:
In `@packages/extension-agent/src/sub-agent/parse.ts`:
- Around line 165-170: The change makes `computer` use the same 'inherit'
fallback as the other permission types, so remove any leftover special-case
handling that previously defaulted `computer` to 'deny' and ensure consistency:
use createRule(item.computer, 'inherit') where permissions.computer is assigned,
align any logic in createPermissionConfig() and related code that previously
treated `computer` differently, and update/remove tests or comments that assumed
the old 'deny' fallback.

---

Nitpick comments:
In `@packages/extension-agent/client/components/skills/skills-detail.vue`:
- Around line 621-653: There are two separate .inner-tab CSS blocks that
silently override each other; merge them into a single .inner-tab rule combining
all properties (background, border, font-size, color, cursor, font-weight) and
remove the duplicate block so styling is consolidated and the duplicate selector
warning is resolved; update the .inner-tab declaration in skills-detail.vue
accordingly.

In `@packages/extension-agent/client/components/sub-agent/sub-agent-catalog.vue`:
- Around line 625-649: There are duplicate .search-input rules; consolidate them
into a single CSS rule by merging the properties (width:100%, min-width:0,
flex:none and the !important usage) into the original .search-input block (the
earlier occurrence around the existing .search-row/.filter-trigger rules) and
remove the redundant later rule; ensure the merged rule retains any necessary
!important and min-width/flex declarations so layout behavior is unchanged.
- Around line 240-256: The current filter check uses filters.value.every(...)
which forces all selected filters to be true simultaneously (AND semantics) and
causes contradictory selections like enabled:yes and enabled:no to yield no
results; update the logic inside the block that currently invokes
filters.value.every to instead group filters by their key (the substring before
':'), apply OR semantics within each group (e.g., for key "enabled" allow item
if any enabled:yes/no in that group matches item.enabled), and keep AND
semantics across different keys (e.g., enabled + source must both match);
operate on the existing symbols (filters.value, the predicate that examines
item.enabled, item.state, item.hidden, item.source) and replace the single
every(...) predicate with grouping+matching logic that checks per-key OR and
cross-key AND.

In `@packages/extension-agent/client/components/sub-agent/sub-agent-page.vue`:
- Around line 711-726: The options property's field indentation is inconsistent:
align allowKoishiMessageTransform with the other fields inside the options
object so it uses the same indentation level as description, chatluna,
character, etc.; update the options block in sub-agent-page.vue (the options
object definition) so all property lines share the same indent and the closing
brace stays aligned.
- Around line 526-536: The deep watcher on props.status.catalog, props.tools and
props.skills calls loadDynamicData() on any nested change and should be
debounced to avoid redundant API calls; modify the watch usage (watch([...],
async () => await loadDynamicData(), { deep: true })) to wrap the callback with
a debounce (e.g., use watchDebounced from `@vueuse/core` or lodash.debounce) or
debounce loadDynamicData itself, keeping the same watched sources
(props.status.catalog, props.tools, props.skills) and preserving deep: true,
ensuring the debounced function is properly cleaned up on component unmount.
- Around line 543-545: The onResize handler updates the reactive mobile state on
every window resize causing potentially high-frequency re-renders; replace the
direct listener with a throttled or debounced wrapper (e.g., throttle the
onResize callback) or switch to useMediaQuery from `@vueuse/core` (e.g., const
mobile = useMediaQuery('(max-width: 768px)')) and ensure the window resize
listener (and its cleanup) is updated to use the throttled function if you keep
manual listening; update references to the onResize function and the mobile ref
accordingly.
- Around line 459-469: The UI allows editing the name for markdown Agents that
are not data-scoped but savePreview rejects renaming for external Agents,
causing a bad UX; update the component to disable the name input (or show a
tooltip) when the Agent is external by deriving a new computed (or extend
canEditPreview) based on previewItem.value?.source === 'markdown' &&
!previewItem.value?.remote && previewItem.value?.scope === 'data' (or simply
require previewItem.value.remote === false), and ensure canSavePreview uses the
same rule so the name field cannot be edited or lead to a blocked save;
reference canEditPreview, canSavePreview, previewItem, previewDraft and
savePreview when making the change.

In `@packages/extension-agent/src/service/sub_agent.ts`:
- Around line 684-710: buildToolMask currently always returns a ToolMask with
mode: 'allow', which can produce a larger mask than necessary; update
buildToolMask (used by buildSubAgentToolMask) to choose between 'allow' and
'deny' modes based on which list (allow vs computed deny) yields a more compact
representation (mirror the behavior in permissions.ts): compute the full set of
known tools or derive deny = allTools - allow, then return mode:'deny' with deny
when deny.length < allow.length, otherwise return mode:'allow'; ensure the
function signature and return type (ToolMask) remain unchanged and that
buildSubAgentToolMask continues to call buildToolMask(allow).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 32e872f6-8252-474c-9329-f3216fb2cae7

📥 Commits

Reviewing files that changed from the base of the PR and between af786b3 and b3643c0.

📒 Files selected for processing (27)
  • packages/core/src/llm-core/platform/types.ts
  • packages/extension-agent/client/components/computer/computer-page.vue
  • packages/extension-agent/client/components/layout/agent-shell.vue
  • packages/extension-agent/client/components/skills/skills-detail.vue
  • packages/extension-agent/client/components/skills/skills-import-folder-dialog.vue
  • packages/extension-agent/client/components/skills/skills-page.vue
  • packages/extension-agent/client/components/sub-agent/sub-agent-catalog.vue
  • packages/extension-agent/client/components/sub-agent/sub-agent-detail.vue
  • packages/extension-agent/client/components/sub-agent/sub-agent-page.vue
  • packages/extension-agent/client/components/tool/tool-detail.vue
  • packages/extension-agent/src/computer/backends/open_terminal.ts
  • packages/extension-agent/src/config/defaults.ts
  • packages/extension-agent/src/config/read.ts
  • packages/extension-agent/src/service/index.ts
  • packages/extension-agent/src/service/permissions.ts
  • packages/extension-agent/src/service/skills.ts
  • packages/extension-agent/src/service/sub_agent.ts
  • packages/extension-agent/src/skills/catalog.ts
  • packages/extension-agent/src/skills/import.ts
  • packages/extension-agent/src/skills/scan.ts
  • packages/extension-agent/src/sub-agent/parse.ts
  • packages/extension-agent/src/sub-agent/run.ts
  • packages/extension-agent/src/sub-agent/runtime.ts
  • packages/extension-agent/src/types.ts
  • packages/extension-agent/src/types/sub_agent.ts
  • packages/extension-agent/src/webui/index.ts
  • packages/extension-tools/src/plugins/file_sender.ts
✅ Files skipped from review due to trivial changes (4)
  • packages/extension-tools/src/plugins/file_sender.ts
  • packages/extension-agent/src/sub-agent/runtime.ts
  • packages/extension-agent/src/skills/catalog.ts
  • packages/extension-agent/client/components/computer/computer-page.vue
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/extension-agent/src/config/read.ts
  • packages/extension-agent/client/components/layout/agent-shell.vue
  • packages/extension-agent/src/skills/scan.ts
  • packages/extension-agent/src/computer/backends/open_terminal.ts
  • packages/extension-agent/src/types.ts
  • packages/extension-agent/src/sub-agent/run.ts

@dingyi222666 dingyi222666 merged commit cec3f94 into ChatLunaLab:v1-dev Mar 28, 2026
1 of 3 checks passed
@CookSleep CookSleep deleted the feat/agent-console-skills-subagents branch March 28, 2026 22:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants