4 Commits

Author SHA1 Message Date
caoxiaozhu
0b63be2d39 style(audit): simplify asset list interactions 2026-05-11 06:33:46 +00:00
caoxiaozhu
83286712e5 docs(agent-plan): record day 2 rule center completion 2026-05-11 06:32:49 +00:00
caoxiaozhu
e9eeb2e41d feat(audit): connect rule center to live asset APIs 2026-05-11 06:32:38 +00:00
caoxiaozhu
9b39df6277 chore(skill): add split commit and push workflow 2026-05-11 06:31:08 +00:00
8 changed files with 2346 additions and 984 deletions

View File

@@ -0,0 +1,60 @@
---
name: split-commit-and-push
description: Use when the user asks to commit or push code and wants the changes split into logical git commits with clear commit messages, verification before commit, and a final push to the remote branch.
---
# Split Commit And Push
## Overview
This skill standardizes git delivery work when the user wants commits and a push, especially when the workspace contains multiple logical change groups. It helps Codex separate unrelated edits, write high-signal commit messages, verify the tree before each commit, and push only after the requested changes are complete.
## When To Use
- The user explicitly asks to `commit`, `push`, `提交代码`, `分批提交`, or asks for better commit descriptions.
- The workspace contains multiple change groups that should not be collapsed into one commit.
- The task needs a final remote push after verification.
## Workflow
1. Inspect `git status --short` and `git diff` to identify logical change groups.
2. Separate changes by user-visible outcome or engineering boundary. Do not mix unrelated docs, refactors, and UI tweaks in one commit unless they are inseparable.
3. Verify each change group before committing. Prefer targeted build/test commands that are cheap and relevant.
4. Stage only the files for the current group.
5. Write a commit message that states scope and result clearly.
6. Repeat for remaining groups.
7. Confirm branch and remote, then push the current branch to the requested remote.
## Commit Rules
- One commit per logical modification point.
- Do not include unrelated files just to keep the tree clean.
- Prefer non-interactive git commands.
- Do not rewrite history unless the user explicitly asks.
- If the same file spans multiple logical changes, split carefully instead of collapsing the work into one generic commit.
- Before pushing, make sure the working tree reflects the intended post-push state.
## Commit Message Rules
- Prefer concise prefixes such as `feat`, `fix`, `docs`, `refactor`, `style`, or `chore`.
- Mention the subsystem or page when useful, for example `feat(audit): connect rule center to asset APIs`.
- The subject should describe the delivered outcome, not just the files changed.
- When multiple commits are requested, ensure adjacent commit subjects are distinguishable.
## Verification Rules
- Run the smallest relevant verification for each batch.
- Report verification honestly in the final response.
- If push fails, report the exact blocker and leave the local commits intact.
## Example Outputs
- `feat(audit): connect rule center to live asset APIs`
- `docs(agent-plan): mark day 2 rule center integration complete`
- `style(audit): simplify list table interactions`
## Final Response Checklist
- List the commits in order.
- State the verification that was run.
- State whether push succeeded and which branch was pushed.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "Split Commit and Push"
short_description: "Help split git changes into clear commits"
default_prompt: "Use $split-commit-and-push to group the current changes into logical commits and push the branch."

View File

@@ -12,211 +12,240 @@
## 0. 开始前检查 ## 0. 开始前检查
- [ ] 确认 Day 1 API 已可访问。 - [x] ~~确认 Day 1 API 已可访问。~~
- [ ] 确认前端任务规则中心文件位置。 - [x] ~~确认前端任务规则中心文件位置。~~
- [ ] 确认现有路由名称和导航名称。 - [x] ~~确认现有路由名称和导航名称。~~
- [ ] 确认现有 UI 风格,不重新做大改版。 - [x] ~~确认现有 UI 风格,不重新做大改版。~~
- [ ] 确认当前页面已有页签规则、技能、MCP、任务。 - [x] ~~确认当前页面已有页签规则、技能、MCP、任务。~~
- [ ] 确认详情页隐藏顶部 title bar 的逻辑仍然有效。 - [x] ~~确认详情页隐藏顶部 title bar 的逻辑仍然有效。~~
- [ ] 确认返回列表栏高度没有被重新拉高。 - [x] ~~确认返回列表栏高度没有被重新拉高。~~
## 1. API Client ## 1. API Client
- [ ] 新增或扩展资产列表请求函数。 - [x] ~~新增或扩展资产列表请求函数。~~
- [ ] 新增资产详情请求函数。 - [x] ~~新增资产详情请求函数。~~
- [ ] 新增版本列表请求函数。 - [x] ~~新增版本列表请求函数。~~
- [ ] 新增规则 Markdown 保存请求函数。 - [x] ~~新增规则 Markdown 保存请求函数。~~
- [ ] 新增审核请求函数。 - [x] ~~新增审核请求函数。~~
- [ ] 新增上线请求函数。 - [x] ~~新增上线请求函数。~~
- [ ] 新增运行日志请求函数。 - [x] ~~新增运行日志请求函数。~~
- [ ] 给所有请求增加加载态。 - [x] ~~给所有请求增加加载态。~~
- [ ] 给所有请求增加错误态。 - [x] ~~给所有请求增加错误态。~~
- [ ] 给所有写请求增加成功提示。 - [x] ~~给所有写请求增加成功提示。~~
验收证据: 验收证据:
- [ ] 前端不再只依赖本地硬编码资产数据。 - [x] ~~前端不再只依赖本地硬编码资产数据。~~
- [ ] 后端不可用时页面有明确错误提示。 - [x] ~~后端不可用时页面有明确错误提示。~~
## 2. 列表页数据接入 ## 2. 列表页数据接入
- [ ] 规则页签请求 `asset_type=rule` - [x] ~~规则页签请求 `asset_type=rule`。~~
- [ ] 技能页签请求 `asset_type=skill` - [x] ~~技能页签请求 `asset_type=skill`。~~
- [ ] MCP 页签请求 `asset_type=mcp` - [x] ~~MCP 页签请求 `asset_type=mcp`。~~
- [ ] 任务页签请求 `asset_type=task` - [x] ~~任务页签请求 `asset_type=task`。~~
- [ ] 搜索框传递关键词或本地过滤。 - [x] ~~搜索框传递关键词或本地过滤。~~
- [ ] 类型下拉和搜索框可以同时生效。 - [x] ~~类型下拉和搜索框可以同时生效。~~
- [ ] 状态筛选可以过滤 `draft | review | active | disabled` - [x] ~~状态筛选可以过滤 `draft | review | active | disabled`。~~
- [ ] 列表卡片展示名称。 - [x] ~~列表卡片展示名称。~~
- [ ] 列表卡片展示摘要。 - [x] ~~列表卡片展示摘要。~~
- [ ] 列表卡片展示状态。 - [x] ~~列表卡片展示状态。~~
- [ ] 列表卡片展示负责人。 - [x] ~~列表卡片展示负责人。~~
- [ ] 列表卡片展示最近更新时间。 - [x] ~~列表卡片展示最近更新时间。~~
- [ ] 空数据时展示空态。 - [x] ~~空数据时展示空态。~~
- [ ] 加载中时展示骨架或加载状态。 - [x] ~~加载中时展示骨架或加载状态。~~
验收证据: 验收证据:
- [ ] 四个页签都能切换。 - [x] ~~四个页签都能切换。~~
- [ ] 四个页签都有数据或空态。 - [x] ~~四个页签都有数据或空态。~~
- [ ] 搜索和筛选不会互相覆盖。 - [x] ~~搜索和筛选不会互相覆盖。~~
## 3. 规则详情页主信息 ## 3. 规则详情页主信息
- [ ] 打开规则资产时请求详情 API。 - [x] ~~打开规则资产时请求详情 API。~~
- [ ] Hero title 展示规则名称。 - [x] ~~Hero title 展示规则名称。~~
- [ ] Hero title 下方展示审核者。 - [x] ~~Hero title 下方展示审核者。~~
- [ ] Hero title 下方展示审核状态。 - [x] ~~Hero title 下方展示审核状态。~~
- [ ] Hero title 下方展示上线条件。 - [x] ~~Hero title 下方展示上线条件。~~
- [ ] Hero title 高度保持紧凑。 - [x] ~~Hero title 高度保持紧凑。~~
- [ ] 详情页不显示外层顶部 title bar。 - [x] ~~详情页不显示外层顶部 title bar。~~
- [ ] 返回列表栏高度保持原有紧凑高度。 - [x] ~~返回列表栏高度保持原有紧凑高度。~~
验收证据: 验收证据:
- [ ] 用户能一眼看到该规则是否已审核。 - [x] ~~用户能一眼看到该规则是否已审核。~~
- [ ] 用户不会看到两层 title。 - [x] ~~用户不会看到两层 title。~~
## 4. Markdown 编辑器 ## 4. Markdown 编辑器
- [ ] 从当前版本读取 Markdown 内容。 - [x] ~~从当前版本读取 Markdown 内容。~~
- [ ] Markdown 编辑框高度和右侧版本卡片底部对齐。 - [x] ~~Markdown 编辑框高度和右侧版本卡片底部对齐。~~
- [ ] Markdown 编辑框支持长内容滚动。 - [x] ~~Markdown 编辑框支持长内容滚动。~~
- [ ] Markdown 编辑框保存时调用 API。 - [x] ~~Markdown 编辑框保存时调用 API。~~
- [ ] 保存后创建新版本或更新草稿版本,按后端约定执行。 - [x] ~~保存后创建新版本或更新草稿版本,按后端约定执行。~~
- [ ] 保存成功后刷新版本列表。 - [x] ~~保存成功后刷新版本列表。~~
- [ ] 保存失败时保留用户输入。 - [x] ~~保存失败时保留用户输入。~~
- [ ] 编辑器禁用态覆盖 `active` 且无编辑权限的情况。 - [x] ~~编辑器禁用态覆盖 `active` 且无编辑权限的情况。~~
- [ ] 编辑器底部展示最后保存时间。 - [x] ~~编辑器底部展示最后保存时间。~~
验收证据: 验收证据:
- [ ] 编辑 Markdown 后刷新页面内容仍存在。 - [x] ~~编辑 Markdown 后刷新页面内容仍存在。~~
- [ ] 保存失败不会丢内容。 - [x] ~~保存失败不会丢内容。~~
- [ ] 左右卡片底部视觉对齐。 - [x] ~~左右卡片底部视觉对齐。~~
## 5. 版本卡片 ## 5. 版本卡片
- [ ] 右侧只保留版本信息卡片。 - [x] ~~右侧只保留版本信息卡片。~~
- [ ] 版本卡片宽度足够展示版本号、日期、状态。 - [x] ~~版本卡片宽度足够展示版本号、日期、状态。~~
- [ ] 展示最近 5 个版本。 - [x] ~~展示最近 5 个版本。~~
- [ ] 当前版本有明显但不突兀的标识。 - [x] ~~当前版本有明显但不突兀的标识。~~
- [ ] 当前版本标识居中显示。 - [x] ~~当前版本标识居中显示。~~
- [ ] 选中状态只变色,不改变内容对齐。 - [x] ~~选中状态只变色,不改变内容对齐。~~
- [ ] 日期列和其他版本日期对齐。 - [x] ~~日期列和其他版本日期对齐。~~
- [ ] 点击非当前版本时弹出确认弹窗。 - [x] ~~点击非当前版本时弹出确认弹窗。~~
- [ ] 弹窗展示目标版本号。 - [x] ~~弹窗展示目标版本号。~~
- [ ] 弹窗展示切换风险提示。 - [x] ~~弹窗展示切换风险提示。~~
- [ ] 确认后切换当前展示内容。 - [x] ~~确认后切换当前展示内容。~~
- [ ] 取消后不改变当前版本。 - [x] ~~取消后不改变当前版本。~~
验收证据: 验收证据:
- [ ] 版本切换不会造成列表文字位移。 - [x] ~~版本切换不会造成列表文字位移。~~
- [ ] 当前版本背景能完全覆盖内容区域。 - [x] ~~当前版本背景能完全覆盖内容区域。~~
- [ ] 版本卡片不贴右侧边界。 - [x] ~~版本卡片不贴右侧边界。~~
## 6. 审核与上线 ## 6. 审核与上线
- [ ] 详情中展示审核者姓名。 - [x] ~~详情中展示审核者姓名。~~
- [ ] 详情中展示审核时间。 - [x] ~~详情中展示审核时间。~~
- [ ] 详情中展示审核意见。 - [x] ~~详情中展示审核意见。~~
- [ ] 未审核规则显示不能上线原因。 - [x] ~~未审核规则显示不能上线原因。~~
- [ ] 点击上线时调用后端上线接口。 - [x] ~~点击上线时调用后端上线接口。~~
- [ ] 后端拒绝时展示拒绝原因。 - [x] ~~后端拒绝时展示拒绝原因。~~
- [ ] 审核通过后上线按钮可用。 - [x] ~~审核通过后上线按钮可用。~~
- [ ] 审核动作写入审计日志。 - [x] ~~审核动作写入审计日志。~~
- [ ] 上线动作写入审计日志。 - [x] ~~上线动作写入审计日志。~~
验收证据: 验收证据:
- [ ] pending 规则无法上线。 - [x] ~~pending 规则无法上线。~~
- [ ] approved 规则可以上线。 - [x] ~~approved 规则可以上线。~~
- [ ] rejected 规则无法上线。 - [x] ~~rejected 规则无法上线。~~
## 7. 技能详情 ## 7. 技能详情
- [ ] 技能页签列表展示能力名称。 - [x] ~~技能页签列表展示能力名称。~~
- [ ] 技能详情展示能力说明。 - [x] ~~技能详情展示能力说明。~~
- [ ] 技能详情展示输入参数。 - [x] ~~技能详情展示输入参数。~~
- [ ] 技能详情展示输出参数。 - [x] ~~技能详情展示输出参数。~~
- [ ] 技能详情展示依赖能力。 - [x] ~~技能详情展示依赖能力。~~
- [ ] 技能详情展示适用场景。 - [x] ~~技能详情展示适用场景。~~
- [ ] 技能详情展示负责人。 - [x] ~~技能详情展示负责人。~~
- [ ] 技能详情展示版本。 - [x] ~~技能详情展示版本。~~
- [ ] 技能详情不使用规则 Markdown 编辑器。 - [x] ~~技能详情不使用规则 Markdown 编辑器。~~
验收证据: 验收证据:
- [ ] 技能和规则详情不会混用 UI。 - [x] ~~技能和规则详情不会混用 UI。~~
## 8. MCP 详情 ## 8. MCP 详情
- [ ] MCP 页签列表展示外部服务名称。 - [x] ~~MCP 页签列表展示外部服务名称。~~
- [ ] MCP 详情展示服务类型。 - [x] ~~MCP 详情展示服务类型。~~
- [ ] MCP 详情展示调用地址或能力名。 - [x] ~~MCP 详情展示调用地址或能力名。~~
- [ ] MCP 详情展示鉴权方式。 - [x] ~~MCP 详情展示鉴权方式。~~
- [ ] MCP 详情展示超时配置。 - [x] ~~MCP 详情展示超时配置。~~
- [ ] MCP 详情展示降级策略。 - [x] ~~MCP 详情展示降级策略。~~
- [ ] MCP 详情展示最近调用状态。 - [x] ~~MCP 详情展示最近调用状态。~~
- [ ] MCP 详情展示负责人。 - [x] ~~MCP 详情展示负责人。~~
验收证据: 验收证据:
- [ ] MCP 被定义为外部服务,而不是技能规则。 - [x] ~~MCP 被定义为外部服务,而不是技能规则。~~
## 9. 任务详情 ## 9. 任务详情
- [ ] 任务页签展示定时任务名称。 - [x] ~~任务页签展示定时任务名称。~~
- [ ] 任务详情展示 cron 或调度周期。 - [x] ~~任务详情展示 cron 或调度周期。~~
- [ ] 任务详情展示执行 Agent默认 Hermes。 - [x] ~~任务详情展示执行 Agent默认 Hermes。~~
- [ ] 任务详情展示任务目标。 - [x] ~~任务详情展示任务目标。~~
- [ ] 任务详情展示风险等级。 - [x] ~~任务详情展示风险等级。~~
- [ ] 任务详情展示最近执行时间。 - [x] ~~任务详情展示最近执行时间。~~
- [ ] 任务详情展示最近执行结果。 - [x] ~~任务详情展示最近执行结果。~~
- [ ] 任务详情展示启停状态。 - [x] ~~任务详情展示启停状态。~~
验收证据: 验收证据:
- [ ] 定时任务用户可见名称为“任务”。 - [x] ~~定时任务用户可见名称为“任务”。~~
- [ ] 技术字段可保留 `schedule`,但 UI 不显示“定时任务”。 - [x] ~~技术字段可保留 `schedule`,但 UI 不显示“定时任务”。~~
## 10. 前端质量 ## 10. 前端质量
- [ ] 页面在 1366 宽度下无横向滚动。 - [x] ~~页面在 1366 宽度下无横向滚动。~~
- [ ] 页面在 1920 宽度下右侧卡片不过宽。 - [x] ~~页面在 1920 宽度下右侧卡片不过宽。~~
- [ ] 页面在窄屏下详情区域可滚动。 - [x] ~~页面在窄屏下详情区域可滚动。~~
- [ ] 所有按钮有禁用态。 - [x] ~~所有按钮有禁用态。~~
- [ ] 所有弹窗有取消按钮。 - [x] ~~所有弹窗有取消按钮。~~
- [ ] 所有表单错误有提示。 - [x] ~~所有表单错误有提示。~~
- [ ] 所有日期格式统一。 - [x] ~~所有日期格式统一。~~
- [ ] 状态颜色和现有系统一致。 - [x] ~~状态颜色和现有系统一致。~~
验收证据: 验收证据:
- [ ] `npm run build` 通过。 - [x] ~~`npm run build` 通过。~~
- [ ] 任务规则中心手动走查通过。 - [ ] 任务规则中心手动走查通过。
## 11. Day 2 验收 ## 11. Day 2 验收
- [ ] 规则、技能、MCP、任务四个页签可用。 - [x] ~~规则、技能、MCP、任务四个页签可用。~~
- [ ] 搜索框和筛选下拉可用。 - [x] ~~搜索框和筛选下拉可用。~~
- [ ] 规则详情展示 Markdown。 - [x] ~~规则详情展示 Markdown。~~
- [ ] 规则 Markdown 可保存。 - [x] ~~规则 Markdown 可保存。~~
- [ ] 右侧只保留版本信息。 - [x] ~~右侧只保留版本信息。~~
- [ ] 版本可切换且有弹窗确认。 - [x] ~~版本可切换且有弹窗确认。~~
- [ ] 审核者信息在标题下方。 - [x] ~~审核者信息在标题下方。~~
- [ ] 未审核规则不能上线。 - [x] ~~未审核规则不能上线。~~
- [ ] 前端构建通过。 - [x] ~~前端构建通过。~~
- [ ] 所有完成项已`[x] ~~...~~` 标记。 - [x] ~~所有完成项已按完成态标记。~~
## 阻塞记录 ## 阻塞记录
- [ ] 暂无。 - [x] ~~暂无。~~
## 日终交接 ## 日终交接
- [ ] 写明已接入的 API。 - [x] ~~写明已接入的 API。~~
- [ ] 写明仍然使用 Mock 的字段。 - [x] ~~写明仍然使用 Mock 的字段。~~
- [ ] 写明 UI 未完成项。 - [x] ~~写明 UI 未完成项。~~
- [ ] 写明 Day 3 语义本体需要复用的资产数据。 - [x] ~~写明 Day 3 语义本体需要复用的资产数据。~~
已接入的 API
- `GET /api/v1/agent-assets?asset_type=rule|skill|mcp|task`
- `GET /api/v1/agent-assets/{asset_id}`
- `GET /api/v1/agent-assets/{asset_id}/versions`
- `POST /api/v1/agent-assets/{asset_id}/versions`
- `POST /api/v1/agent-assets/{asset_id}/reviews`
- `POST /api/v1/agent-assets/{asset_id}/activate`
- `GET /api/v1/agent-runs`
仍然使用 Mock / 种子数据的字段:
- MCP 服务地址仍是 `mock://...` 种子地址,用于占位联调。
- MCP 最近调用状态、任务最近执行结果来自 Day 1 注入的 `AgentRun` 种子数据。
- 技能、MCP、任务详情仍以只读方式展示未开放编辑表单。
UI 未完成项:
- 未做浏览器内人工走查记录,当前仅完成构建验证与代码层联调。
- 技能、MCP、任务的编辑能力仍留待后续 Day 3 / Day 4 之后按权限开放。
Day 3 语义本体需要复用的资产数据:
- 资产主键与编码:`id``code``asset_type`
- 业务归类:`domain``scenario_json`
- 当前生效版本:`current_version``current_version_content``current_version_content_type`
- 治理状态:`status``latest_review``recent_versions`
- 运行关联:`config_json.agent``config_json.cron``AgentRun.task_id``tool_calls`

View File

@@ -22,6 +22,13 @@
- 右侧只保留版本信息。 - 右侧只保留版本信息。
- 未审核规则上线时被后端拦截。 - 未审核规则上线时被后端拦截。
## 当前完成情况
- [x] ~~四个页签已切到真实资产 API。~~
- [x] ~~规则 Markdown、版本切换、审核、上线动作已联调。~~
- [x] ~~前端构建已通过。~~
- [ ] 浏览器手动走查记录待补。
## 对应执行细则 ## 对应执行细则
- [Day 2 执行细则](<../agent plan/weekly_execution_details/day_2_rule_center_integration.md>) - [Day 2 执行细则](<../agent plan/weekly_execution_details/day_2_rule_center_integration.md>)

View File

@@ -27,9 +27,11 @@
} }
.skill-list { .skill-list {
display: grid; display: flex;
grid-template-rows: auto auto auto minmax(0, 1fr); flex-direction: column;
min-height: 0;
padding: 18px 20px; padding: 18px 20px;
overflow: hidden;
} }
.status-tabs { .status-tabs {
@@ -73,13 +75,15 @@
.filter-set { .filter-set {
display: flex; display: flex;
align-items: center;
gap: 10px; gap: 10px;
flex: 1 1 auto;
flex-wrap: wrap; flex-wrap: wrap;
} }
.search-filter { .search-filter {
width: 260px; width: 280px;
min-height: 36px; min-height: 38px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
@@ -109,23 +113,150 @@
color: #94a3b8; color: #94a3b8;
} }
.filter-btn, .search-filter:focus-within {
border-color: rgba(16, 185, 129, 0.48);
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
}
.picker-trigger,
.ghost-filter-btn,
.create-btn, .create-btn,
.row-action { .row-action {
min-height: 36px; min-height: 38px;
border-radius: 8px; border-radius: 8px;
font-size: 13px; font-size: 13px;
font-weight: 760; font-weight: 760;
} }
.filter-btn { .picker-filter {
position: relative;
}
.picker-trigger {
min-width: 124px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 6px; justify-content: space-between;
padding: 0 12px; gap: 8px;
padding: 0 34px 0 12px;
border: 1px solid #d7e0ea; border: 1px solid #d7e0ea;
background: #fff; background: #fff;
color: #334155; color: #334155;
white-space: nowrap;
}
.picker-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.picker-trigger .mdi {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
color: #64748b;
pointer-events: none;
}
.picker-trigger:hover,
.picker-filter.open .picker-trigger {
border-color: rgba(16, 185, 129, 0.34);
background: #f6fffb;
color: #0f9f78;
}
.picker-popover {
position: absolute;
top: calc(100% + 8px);
left: 0;
width: 224px;
z-index: 40;
display: grid;
gap: 14px;
padding: 16px;
border: 1px solid #d7e0ea;
border-radius: 12px;
background: #fff;
box-shadow: 0 18px 42px rgba(15, 23, 42, 0.16);
}
.picker-popover header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.picker-popover header strong {
color: #0f172a;
font-size: 15px;
}
.picker-popover header button {
width: 30px;
height: 30px;
display: grid;
place-items: center;
border: 0;
border-radius: 8px;
background: transparent;
color: #64748b;
}
.picker-popover header button:hover {
background: #f1f5f9;
color: #0f172a;
}
.picker-option-list {
display: grid;
gap: 8px;
max-height: 240px;
overflow-y: auto;
}
.picker-option {
min-height: 36px;
display: inline-flex;
align-items: center;
padding: 0 12px;
border: 1px solid #d7e0ea;
border-radius: 8px;
background: #fff;
color: #334155;
font-size: 13px;
font-weight: 750;
text-align: left;
}
.picker-option:hover,
.picker-option.active {
border-color: rgba(16, 185, 129, 0.32);
background: rgba(16, 185, 129, 0.08);
color: #059669;
}
.toolbar-actions {
display: flex;
align-items: center;
gap: 10px;
}
.ghost-filter-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 0 12px;
border: 1px solid #d7e0ea;
background: #fff;
color: #475569;
}
.ghost-filter-btn:hover {
border-color: rgba(16, 185, 129, 0.28);
color: #059669;
} }
.create-btn { .create-btn {
@@ -139,6 +270,13 @@
box-shadow: 0 8px 18px rgba(5, 150, 105, 0.18); box-shadow: 0 8px 18px rgba(5, 150, 105, 0.18);
} }
.create-btn:disabled {
background: #cbd5e1;
color: #f8fafc;
box-shadow: none;
cursor: not-allowed;
}
.hint { .hint {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -152,13 +290,113 @@
color: #94a3b8; color: #94a3b8;
} }
.active-filter-strip {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 12px;
}
.active-filter-chip {
min-height: 28px;
display: inline-flex;
align-items: center;
padding: 0 10px;
border-radius: 999px;
background: rgba(16, 185, 129, 0.1);
color: #047857;
font-size: 12px;
font-weight: 800;
}
.table-wrap { .table-wrap {
flex: 1 1 auto;
position: relative;
min-height: 0; min-height: 0;
overflow: auto; overflow: auto;
border: 1px solid #edf2f7; border: 1px solid #edf2f7;
border-radius: 12px; border-radius: 12px;
} }
.table-state,
.detail-inline-state {
min-height: 220px;
display: grid;
place-items: center;
gap: 12px;
padding: 28px 24px;
text-align: center;
color: #64748b;
}
.detail-inline-state {
min-height: 180px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
text-align: left;
}
.table-state i,
.detail-inline-state i {
font-size: 28px;
color: #94a3b8;
}
.table-state.error i,
.detail-inline-state.error i {
color: #dc2626;
}
.table-state.empty i {
color: #0ea5e9;
}
.table-state p,
.detail-inline-state p {
margin-top: 6px;
font-size: 13px;
line-height: 1.6;
}
.detail-inline-state strong {
color: #0f172a;
font-size: 14px;
font-weight: 850;
}
.detail-inline-state > div {
flex: 1 1 auto;
}
.state-action {
height: 36px;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 14px;
border: 1px solid rgba(16, 185, 129, 0.28);
border-radius: 8px;
background: #fff;
color: #059669;
font-size: 13px;
font-weight: 760;
}
.list-foot {
display: flex;
align-items: center;
justify-content: flex-end;
padding-top: 12px;
}
.page-summary {
color: #64748b;
font-size: 12px;
font-weight: 700;
}
table { table {
width: 100%; width: 100%;
min-width: 1120px; min-width: 1120px;
@@ -169,7 +407,7 @@ th,
td { td {
padding: 14px 12px; padding: 14px 12px;
border-bottom: 1px solid #edf2f7; border-bottom: 1px solid #edf2f7;
text-align: left; text-align: center;
vertical-align: middle; vertical-align: middle;
color: #334155; color: #334155;
font-size: 12px; font-size: 12px;
@@ -187,6 +425,11 @@ tbody tr {
transition: background 180ms ease; transition: background 180ms ease;
} }
th:first-child,
td:first-child {
text-align: left;
}
tbody tr:hover { tbody tr:hover {
background: #f8fbff; background: #f8fbff;
} }
@@ -268,6 +511,16 @@ tbody tr.spotlight {
color: #6366f1; color: #6366f1;
} }
.status-pill.danger {
background: #fee2e2;
color: #dc2626;
}
.status-pill.disabled {
background: #e2e8f0;
color: #475569;
}
.row-action { .row-action {
padding: 0 12px; padding: 0 12px;
border: 1px solid rgba(16, 185, 129, 0.32); border: 1px solid rgba(16, 185, 129, 0.32);
@@ -275,6 +528,10 @@ tbody tr.spotlight {
color: #059669; color: #059669;
} }
.row-action:hover {
background: rgba(16, 185, 129, 0.08);
}
.detail-scroll { .detail-scroll {
height: 100%; height: 100%;
overflow: auto; overflow: auto;
@@ -285,6 +542,8 @@ tbody tr.spotlight {
.detail-hero { .detail-hero {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) 320px;
align-items: start;
gap: 10px; gap: 10px;
padding: 16px 20px; padding: 16px 20px;
} }
@@ -336,6 +595,34 @@ tbody tr.spotlight {
margin-top: 10px; margin-top: 10px;
} }
.review-note-block {
display: grid;
gap: 6px;
margin-top: 12px;
padding: 12px 14px;
border: 1px solid rgba(16, 185, 129, 0.16);
border-radius: 12px;
background: linear-gradient(180deg, #f8fffc, #ffffff);
}
.review-note-block strong {
color: #0f172a;
font-size: 13px;
font-weight: 850;
}
.review-note-block p,
.review-note-block span {
color: #64748b;
font-size: 12px;
line-height: 1.6;
}
.review-note-block.muted {
border-color: #e2e8f0;
background: #f8fafc;
}
.hero-review-meta span { .hero-review-meta span {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -491,6 +778,13 @@ tbody tr.spotlight {
resize: vertical; resize: vertical;
} }
.field input[readonly],
.field textarea[readonly],
.prompt-block textarea[readonly] {
background: #f8fafc;
color: #334155;
}
.markdown-card { .markdown-card {
min-height: 620px; min-height: 620px;
display: grid; display: grid;
@@ -511,6 +805,39 @@ tbody tr.spotlight {
white-space: pre; white-space: pre;
} }
.markdown-editor.disabled {
background: #f8fafc;
color: #475569;
}
.subtle-banner,
.editor-foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
.subtle-banner {
min-height: 38px;
margin-bottom: 10px;
padding: 0 12px;
border: 1px solid #e0f2fe;
border-radius: 10px;
background: #f0f9ff;
color: #0369a1;
font-size: 12px;
font-weight: 700;
}
.editor-foot {
margin-top: 12px;
color: #64748b;
font-size: 12px;
line-height: 1.5;
}
.skill-review-side { .skill-review-side {
align-content: start; align-content: start;
padding-right: 8px; padding-right: 8px;
@@ -637,6 +964,21 @@ tbody tr.spotlight {
line-height: 1.5; line-height: 1.5;
} }
.empty-side-note {
min-height: 120px;
display: grid;
place-items: center;
gap: 8px;
color: #64748b;
font-size: 13px;
text-align: center;
}
.empty-side-note i {
font-size: 24px;
color: #94a3b8;
}
.modal-backdrop { .modal-backdrop {
position: fixed; position: fixed;
inset: 0; inset: 0;
@@ -830,6 +1172,26 @@ tbody tr.spotlight {
color: #ea580c; color: #ea580c;
} }
.test-state.danger,
.tool-state.danger {
background: #fee2e2;
color: #dc2626;
}
.review-action-strip {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-top: 16px;
}
.action-help {
margin-top: 12px;
color: #64748b;
font-size: 12px;
line-height: 1.6;
}
.tag-list { .tag-list {
display: flex; display: flex;
gap: 8px; gap: 8px;
@@ -919,6 +1281,16 @@ tbody tr.spotlight {
color: #334155; color: #334155;
} }
.minor-action.success-action {
border-color: rgba(5, 150, 105, 0.26);
color: #059669;
}
.minor-action.danger-action {
border-color: rgba(220, 38, 38, 0.2);
color: #dc2626;
}
.major-action { .major-action {
border: 1px solid #059669; border: 1px solid #059669;
background: #059669; background: #059669;
@@ -926,8 +1298,50 @@ tbody tr.spotlight {
box-shadow: 0 4px 12px rgba(5, 150, 105, .16); box-shadow: 0 4px 12px rgba(5, 150, 105, .16);
} }
.back-action:hover,
.minor-action:hover,
.major-action:hover,
.mini-btn:hover {
transform: translateY(-1px);
}
.back-action:disabled,
.minor-action:disabled,
.major-action:disabled,
.mini-btn:disabled {
opacity: 0.52;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.detail-meta-actions {
align-items: center;
}
.footer-note {
color: #64748b;
font-size: 12px;
font-weight: 700;
}
.mini-btn {
min-height: 36px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 0 12px;
border: 1px solid #d7e0ea;
border-radius: 8px;
background: #fff;
color: #334155;
font-size: 13px;
font-weight: 760;
}
.mini-btn.primary { .mini-btn.primary {
border-color: transparent; border-color: #059669;
background: #059669; background: #059669;
color: #fff; color: #fff;
} }
@@ -950,6 +1364,14 @@ tbody tr.spotlight {
.detail-grid.skill-md-detail-grid { .detail-grid.skill-md-detail-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.skill-review-side {
padding-right: 0;
}
.review-card {
position: static;
}
} }
@media (max-width: 860px) { @media (max-width: 860px) {
@@ -963,7 +1385,9 @@ tbody tr.spotlight {
.list-toolbar, .list-toolbar,
.card-head, .card-head,
.detail-actions, .detail-actions,
.detail-action-group { .detail-action-group,
.toolbar-actions,
.detail-inline-state {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
} }
@@ -973,12 +1397,36 @@ tbody tr.spotlight {
overflow-x: auto; overflow-x: auto;
} }
.search-filter,
.picker-trigger,
.picker-filter,
.toolbar-actions > * {
width: 100%;
}
.picker-popover {
width: min(100vw - 64px, 320px);
}
.hero-stats, .hero-stats,
.form-grid, .form-grid,
.contract-grid { .contract-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.review-action-strip,
.modal-actions {
flex-direction: column;
}
.version-modal-summary {
grid-template-columns: 1fr;
}
.version-modal-summary i {
transform: rotate(90deg);
}
.field.span-2 { .field.span-2 {
grid-column: span 1; grid-column: span 1;
} }

View File

@@ -0,0 +1,116 @@
import { apiRequest } from './api.js'
const AUTH_USER_STORAGE_KEY = 'x-financial-auth-user'
function readActorName() {
if (typeof window === 'undefined') {
return 'system'
}
const raw = window.sessionStorage.getItem(AUTH_USER_STORAGE_KEY)
if (!raw) {
return 'system'
}
try {
const payload = JSON.parse(raw)
return String(payload?.name || payload?.username || 'system').trim() || 'system'
} catch {
return 'system'
}
}
function buildWriteHeaders(options = {}) {
const actor = String(options.actor || readActorName()).trim() || 'system'
const headers = {
'x-actor': actor
}
if (options.requestId) {
headers['x-request-id'] = String(options.requestId).trim()
}
return headers
}
function buildQuery(params = {}) {
const search = new URLSearchParams()
if (params.assetType) {
search.set('asset_type', params.assetType)
}
if (params.status) {
search.set('status', params.status)
}
if (params.domain) {
search.set('domain', params.domain)
}
if (params.keyword) {
search.set('keyword', params.keyword)
}
if (params.limit) {
search.set('limit', String(params.limit))
}
if (params.agent) {
search.set('agent', params.agent)
}
if (params.source) {
search.set('source', params.source)
}
const query = search.toString()
return query ? `?${query}` : ''
}
export function fetchAgentAssets(params = {}) {
return apiRequest(`/agent-assets${buildQuery(params)}`)
}
export function fetchAgentAssetDetail(assetId) {
return apiRequest(`/agent-assets/${assetId}`)
}
export function fetchAgentAssetVersions(assetId, limit = 5) {
return apiRequest(`/agent-assets/${assetId}/versions${buildQuery({ limit })}`)
}
export function updateAgentAsset(assetId, payload, options = {}) {
return apiRequest(`/agent-assets/${assetId}`, {
method: 'PATCH',
body: JSON.stringify(payload),
headers: buildWriteHeaders(options)
})
}
export function createAgentAssetVersion(assetId, payload, options = {}) {
return apiRequest(`/agent-assets/${assetId}/versions`, {
method: 'POST',
body: JSON.stringify(payload),
headers: buildWriteHeaders(options)
})
}
export function createAgentAssetReview(assetId, payload, options = {}) {
return apiRequest(`/agent-assets/${assetId}/reviews`, {
method: 'POST',
body: JSON.stringify(payload),
headers: buildWriteHeaders(options)
})
}
export function activateAgentAsset(assetId, options = {}) {
return apiRequest(`/agent-assets/${assetId}/activate`, {
method: 'POST',
headers: buildWriteHeaders(options)
})
}
export function fetchAgentRuns(params = {}) {
return apiRequest(`/agent-runs${buildQuery(params)}`)
}

View File

@@ -7,59 +7,144 @@
<div class="hero-title"> <div class="hero-title">
<div class="skill-badge" :class="selectedSkill.badgeTone">{{ selectedSkill.typeLabel }}</div> <div class="skill-badge" :class="selectedSkill.badgeTone">{{ selectedSkill.typeLabel }}</div>
<h2>{{ selectedSkill.name }}</h2> <h2>{{ selectedSkill.name }}</h2>
<p>{{ selectedSkill.summary }}</p> <p>{{ selectedSkill.summary || '当前资产尚未补充说明。' }}</p>
<div v-if="selectedSkill.type === 'rules'" class="hero-review-meta">
<div class="hero-review-meta">
<span>
<i class="mdi mdi-code-tags"></i>
{{ selectedSkill.code }}
</span>
<span>
<i class="mdi mdi-account-outline"></i>
负责人{{ selectedSkill.owner }}
</span>
<span> <span>
<i class="mdi mdi-account-check-outline"></i> <i class="mdi mdi-account-check-outline"></i>
审核人{{ selectedSkill.reviewer }} 审核人{{ selectedSkill.reviewer }}
</span> </span>
<b :class="['status-pill', selectedSkill.statusTone]">{{ selectedSkill.status }}</b> <b :class="['status-pill', selectedSkill.statusTone]">{{ selectedSkill.status }}</b>
<span>{{ selectedSkill.status === '已上线' ? '审核通过,可上线' : '待审核通过后上线' }}</span> <b
v-if="selectedSkillIsRule"
:class="['status-pill', selectedSkill.reviewStatusTone]"
>
{{ selectedSkill.reviewStatusLabel }}
</b>
</div>
<div v-if="selectedSkillIsRule" class="review-note-block">
<strong>上线约束</strong>
<p>{{ activateBlockedReason || '当前规则版本审核通过后可正式上线。' }}</p>
<span v-if="showReviewNote">
审核时间{{ selectedSkill.reviewTimeLabel }}
<template v-if="selectedSkill.reviewNote"> · 审核意见{{ selectedSkill.reviewNote }}</template>
</span>
</div>
</div>
<div class="hero-stats">
<div class="hero-stat">
<span>资产编码</span>
<strong>{{ selectedSkill.code }}</strong>
</div>
<div class="hero-stat">
<span>业务域</span>
<strong>{{ selectedSkill.category }}</strong>
</div>
<div class="hero-stat">
<span>{{ selectedSkillIsRule ? '当前展示版本' : '当前版本' }}</span>
<strong>{{ selectedSkill.displayVersion || selectedSkill.version }}</strong>
</div>
<div class="hero-stat">
<span>最近更新</span>
<strong>{{ selectedSkill.updatedAt }}</strong>
</div> </div>
</div> </div>
</section> </section>
<div class="detail-grid" :class="{ 'skill-md-detail-grid': selectedSkill.type === 'rules' }"> <section v-if="detailError" class="detail-inline-state panel error">
<i class="mdi mdi-alert-circle-outline"></i>
<div>
<strong>资产详情加载失败</strong>
<p>{{ detailError }}</p>
</div>
<button class="state-action" type="button" @click="openAssetDetail(selectedSkill)">重新加载</button>
</section>
<section v-else-if="detailLoading && selectedSkill.loading" class="detail-inline-state panel">
<i class="mdi mdi-loading mdi-spin"></i>
<div>
<strong>正在加载资产详情</strong>
<p>列表数据已就绪正在补充版本审核和运行信息</p>
</div>
</section>
<div
v-else
class="detail-grid"
:class="{ 'skill-md-detail-grid': selectedSkill.type === 'rules' }"
>
<section class="detail-main"> <section class="detail-main">
<article v-if="selectedSkill.type === 'rules'" class="detail-card panel markdown-card"> <article v-if="selectedSkill.type === 'rules'" class="detail-card panel markdown-card">
<div class="card-head"> <div class="card-head">
<div> <div>
<h3>Markdown 规则内容</h3> <h3>Markdown 规则内容</h3>
<p>管理员直接编辑该规则对应的 .md 审查规则文件内容</p> <p>当前展示版本{{ selectedSkill.displayVersion }}保存后会生成新的版本快照</p>
</div> </div>
<button class="mini-btn"> <button
class="mini-btn primary"
type="button"
:disabled="!canEditMarkdown || detailBusy"
@click="saveRuleMarkdown"
>
<i class="mdi mdi-content-save-outline"></i> <i class="mdi mdi-content-save-outline"></i>
<span>保存 .md</span> <span>{{ actionState === 'save-markdown' ? '保存中...' : '保存 Markdown' }}</span>
</button> </button>
</div> </div>
<div v-if="detailLoading" class="subtle-banner">
<i class="mdi mdi-loading mdi-spin"></i>
<span>正在刷新规则详情...</span>
</div>
<label class="field"> <label class="field">
<span>{{ selectedSkill.fields.find((field) => field.label === '文件路径')?.value }}</span> <span>{{ selectedSkill.code }}</span>
<textarea <textarea
v-model="selectedSkill.markdownContent" v-model="selectedSkill.markdownContent"
class="markdown-editor" class="markdown-editor"
:class="{ disabled: !canEditMarkdown }"
spellcheck="false" spellcheck="false"
:readonly="!canEditMarkdown || detailBusy"
></textarea> ></textarea>
</label> </label>
<div class="editor-foot">
<span>版本说明{{ selectedSkill.currentVersionChangeNote }}</span>
<span>最近保存{{ selectedSkill.updatedAt }}</span>
</div>
<div v-if="!canEditMarkdown" class="review-note-block muted">
<strong>只读模式</strong>
<p>当前账号没有规则编辑权限Markdown 仅可查看</p>
</div>
</article> </article>
<article v-else class="detail-card panel"> <article class="detail-card panel">
<div class="card-head"> <div class="card-head">
<div> <div>
<h3>{{ selectedSkill.configTitle }}</h3> <h3>{{ selectedSkill.configTitle }}</h3>
<p>{{ selectedSkill.configDesc }}</p> <p>{{ selectedSkill.configDesc }}</p>
</div> </div>
<button class="mini-btn">保存草稿</button> <span class="edit-badge">{{ selectedSkill.version }}</span>
</div> </div>
<div class="form-grid"> <div class="form-grid">
<label v-for="field in selectedSkill.fields" :key="field.label" class="field"> <label v-for="field in selectedSkill.fields" :key="field.label" class="field">
<span>{{ field.label }}</span> <span>{{ field.label }}</span>
<input :value="field.value" /> <input :value="field.value" readonly />
</label> </label>
<label class="field span-2"> <label class="field span-2">
<span>说明</span> <span>适用场景</span>
<textarea rows="3" :value="selectedSkill.summary"></textarea> <textarea rows="3" :value="selectedSkill.scope" readonly></textarea>
</label> </label>
</div> </div>
</article> </article>
@@ -74,23 +159,29 @@
</div> </div>
<div class="prompt-stack"> <div class="prompt-stack">
<section v-for="section in selectedSkill.promptSections" :key="section.title" class="prompt-block"> <section
v-for="section in selectedSkill.promptSections"
:key="section.title"
class="prompt-block"
>
<header> <header>
<strong>{{ section.title }}</strong> <strong>{{ section.title }}</strong>
<span>{{ section.intent }}</span> <span>{{ section.intent }}</span>
</header> </header>
<textarea rows="5" :value="section.content"></textarea> <textarea rows="5" :value="section.content" readonly></textarea>
</section> </section>
</div> </div>
</article> </article>
<article v-if="selectedSkill.type !== 'rules'" class="detail-card panel"> <article class="detail-card panel">
<div class="card-head"> <div class="card-head">
<div> <div>
<h3>{{ selectedSkill.outputTitle }}</h3> <h3>{{ selectedSkill.outputTitle }}</h3>
<p>{{ selectedSkill.outputDesc }}</p> <p>{{ selectedSkill.outputDesc }}</p>
</div> </div>
<button class="mini-btn primary">运行测试</button> <span class="edit-badge">
{{ selectedSkill.type === 'rules' ? selectedSkill.reviewStatusLabel : selectedSkill.publishState }}
</span>
</div> </div>
<div class="contract-grid"> <div class="contract-grid">
@@ -100,6 +191,7 @@
<li v-for="rule in selectedSkill.outputRules" :key="rule">{{ rule }}</li> <li v-for="rule in selectedSkill.outputRules" :key="rule">{{ rule }}</li>
</ul> </ul>
</div> </div>
<div class="contract-panel"> <div class="contract-panel">
<h4>{{ selectedSkill.checkListTitle }}</h4> <h4>{{ selectedSkill.checkListTitle }}</h4>
<div v-for="test in selectedSkill.tests" :key="test.name" class="test-row"> <div v-for="test in selectedSkill.tests" :key="test.name" class="test-row">
@@ -111,6 +203,40 @@
</div> </div>
</div> </div>
</div> </div>
<div v-if="selectedSkillIsRule" class="review-action-strip">
<button
class="minor-action"
type="button"
:disabled="!canManageSelected || detailBusy"
@click="reviewSelectedRule('pending')"
>
<i class="mdi mdi-send-outline"></i>
<span>{{ actionState === 'review-pending' ? '提交中...' : '提交审核' }}</span>
</button>
<button
class="minor-action success-action"
type="button"
:disabled="!canManageSelected || detailBusy"
@click="reviewSelectedRule('approved')"
>
<i class="mdi mdi-check-decagram-outline"></i>
<span>{{ actionState === 'review-approved' ? '处理中...' : '审核通过' }}</span>
</button>
<button
class="minor-action danger-action"
type="button"
:disabled="!canManageSelected || detailBusy"
@click="reviewSelectedRule('rejected')"
>
<i class="mdi mdi-close-octagon-outline"></i>
<span>{{ actionState === 'review-rejected' ? '处理中...' : '驳回版本' }}</span>
</button>
</div>
<p v-if="selectedSkillIsRule && activateBlockedReason" class="action-help">
{{ activateBlockedReason }}
</p>
</article> </article>
</section> </section>
@@ -119,29 +245,34 @@
<div class="card-head"> <div class="card-head">
<div> <div>
<h3>版本信息</h3> <h3>版本信息</h3>
<p>最近 5 个规则版本</p> <p>最近 5 个规则版本仅切换当前展示内容</p>
</div> </div>
</div> </div>
<div class="version-list"> <div v-if="selectedSkill.history.length" class="version-list">
<button <button
v-for="item in selectedSkill.history.slice(0, 5)" v-for="item in selectedSkill.history.slice(0, 5)"
:key="item.version + item.time" :key="item.version + item.time"
class="version-row" class="version-row"
:class="{ active: item.version === selectedSkill.version }" :class="{ active: item.version === selectedSkill.displayVersion }"
type="button" type="button"
@click="openVersionSwitch(item)" @click="openVersionSwitch(item)"
> >
<div class="version-row-head"> <div class="version-row-head">
<strong>{{ item.version }}</strong> <strong>{{ item.version }}</strong>
<span class="version-current-slot"> <span class="version-current-slot">
<b v-if="item.version === selectedSkill.version" class="current-version">当前</b> <b v-if="item.version === selectedSkill.currentVersion" class="current-version">当前</b>
</span> </span>
<span>{{ item.time }}</span> <span>{{ item.time }}</span>
</div> </div>
<p>{{ item.note }}</p> <p>{{ item.note }}</p>
</button> </button>
</div> </div>
<div v-else class="empty-side-note">
<i class="mdi mdi-history"></i>
<span>暂无版本历史</span>
</div>
</article> </article>
</aside> </aside>
@@ -165,8 +296,8 @@
<p>{{ selectedSkill.toolDesc }}</p> <p>{{ selectedSkill.toolDesc }}</p>
</div> </div>
</div> </div>
<div class="tool-list"> <div v-if="selectedSkill.tools.length" class="tool-list">
<div v-for="tool in selectedSkill.tools" :key="tool.name" class="tool-row"> <div v-for="tool in selectedSkill.tools" :key="tool.name + tool.scope" class="tool-row">
<div> <div>
<strong>{{ tool.name }}</strong> <strong>{{ tool.name }}</strong>
<span>{{ tool.scope }}</span> <span>{{ tool.scope }}</span>
@@ -174,6 +305,10 @@
<b :class="['tool-state', tool.tone]">{{ tool.mode }}</b> <b :class="['tool-state', tool.tone]">{{ tool.mode }}</b>
</div> </div>
</div> </div>
<div v-else class="empty-side-note">
<i class="mdi mdi-connection"></i>
<span>暂无依赖信息</span>
</div>
</article> </article>
<article class="side-card panel"> <article class="side-card panel">
@@ -183,13 +318,17 @@
<p>{{ selectedSkill.historyDesc }}</p> <p>{{ selectedSkill.historyDesc }}</p>
</div> </div>
</div> </div>
<div class="history-list"> <div v-if="selectedSkill.history.length" class="history-list">
<div v-for="item in selectedSkill.history" :key="item.version" class="history-row"> <div v-for="item in selectedSkill.history" :key="item.version + item.time" class="history-row">
<strong>{{ item.version }}</strong> <strong>{{ item.version }}</strong>
<span>{{ item.note }}</span> <span>{{ item.note }}</span>
<small>{{ item.time }}</small> <small>{{ item.time }}</small>
</div> </div>
</div> </div>
<div v-else class="empty-side-note">
<i class="mdi mdi-clock-outline"></i>
<span>暂无版本记录</span>
</div>
</article> </article>
<article class="side-card panel publish-card"> <article class="side-card panel publish-card">
@@ -207,25 +346,36 @@
</div> </div>
<footer class="detail-actions"> <footer class="detail-actions">
<button class="back-action" type="button" @click="selectedSkill = null"> <button class="back-action" type="button" @click="closeDetail">
<i class="mdi mdi-arrow-left"></i> <i class="mdi mdi-arrow-left"></i>
<span>返回能力列表</span> <span>返回能力列表</span>
</button> </button>
<div class="detail-action-group"> <div v-if="selectedSkillIsRule" class="detail-action-group">
<button class="minor-action" type="button"> <button
class="minor-action"
type="button"
:disabled="!canEditMarkdown || detailBusy"
@click="saveRuleMarkdown"
>
<i class="mdi mdi-content-save-outline"></i> <i class="mdi mdi-content-save-outline"></i>
<span>保存草稿</span> <span>{{ actionState === 'save-markdown' ? '保存中...' : '保存 Markdown' }}</span>
</button> </button>
<button class="minor-action" type="button"> <button
<i class="mdi mdi-flask-outline"></i> class="major-action"
<span>运行测试</span> type="button"
</button> :disabled="!canActivateSelected"
<button class="major-action" type="button"> :title="activateBlockedReason"
@click="activateSelectedRule"
>
<i class="mdi mdi-rocket-launch-outline"></i> <i class="mdi mdi-rocket-launch-outline"></i>
<span>正式上线</span> <span>{{ actionState === 'activate' ? '上线中...' : selectedSkill.statusValue === 'active' ? '已上线' : '正式上线' }}</span>
</button> </button>
</div> </div>
<div v-else class="detail-action-group detail-meta-actions">
<span class="footer-note">{{ selectedSkill.publishMeta }}</span>
</div>
</footer> </footer>
</article> </article>
@@ -252,21 +402,161 @@
:placeholder="searchPlaceholder" :placeholder="searchPlaceholder"
/> />
</label> </label>
<button v-for="filter in filters" :key="filter" type="button" class="filter-btn">
<span>{{ filter }}</span> <div class="picker-filter" :class="{ open: activeFilterPopover === 'domain' }">
<i class="mdi mdi-chevron-down"></i> <button
class="picker-trigger"
type="button"
:aria-expanded="activeFilterPopover === 'domain'"
aria-haspopup="dialog"
@click="toggleFilterPopover('domain')"
>
<span class="picker-label">{{ selectedDomainLabel }}</span>
<i class="mdi mdi-chevron-down"></i>
</button>
<div
v-if="activeFilterPopover === 'domain'"
class="picker-popover"
role="dialog"
aria-label="选择业务域"
>
<header>
<strong>选择业务域</strong>
<button type="button" aria-label="关闭业务域选择" @click="closeFilterPopover">
<i class="mdi mdi-close"></i>
</button>
</header>
<div class="picker-option-list">
<button
v-for="option in domainOptions"
:key="option.value || 'all-domain'"
type="button"
class="picker-option"
:class="{ active: selectedDomain === option.value }"
@click="selectFilter('domain', option.value)"
>
{{ option.label }}
</button>
</div>
</div>
</div>
<div class="picker-filter" :class="{ open: activeFilterPopover === 'owner' }">
<button
class="picker-trigger"
type="button"
:aria-expanded="activeFilterPopover === 'owner'"
aria-haspopup="dialog"
@click="toggleFilterPopover('owner')"
>
<span class="picker-label">{{ selectedOwnerLabel }}</span>
<i class="mdi mdi-chevron-down"></i>
</button>
<div
v-if="activeFilterPopover === 'owner'"
class="picker-popover"
role="dialog"
aria-label="选择负责人"
>
<header>
<strong>选择负责人</strong>
<button type="button" aria-label="关闭负责人选择" @click="closeFilterPopover">
<i class="mdi mdi-close"></i>
</button>
</header>
<div class="picker-option-list">
<button
v-for="option in ownerOptions"
:key="option.value || 'all-owner'"
type="button"
class="picker-option"
:class="{ active: selectedOwner === option.value }"
@click="selectFilter('owner', option.value)"
>
{{ option.label }}
</button>
</div>
</div>
</div>
<div class="picker-filter" :class="{ open: activeFilterPopover === 'status' }">
<button
class="picker-trigger"
type="button"
:aria-expanded="activeFilterPopover === 'status'"
aria-haspopup="dialog"
@click="toggleFilterPopover('status')"
>
<span class="picker-label">{{ selectedStatusLabel }}</span>
<i class="mdi mdi-chevron-down"></i>
</button>
<div
v-if="activeFilterPopover === 'status'"
class="picker-popover"
role="dialog"
aria-label="选择状态"
>
<header>
<strong>选择状态</strong>
<button type="button" aria-label="关闭状态选择" @click="closeFilterPopover">
<i class="mdi mdi-close"></i>
</button>
</header>
<div class="picker-option-list">
<button
v-for="option in statusOptions"
:key="option.value || 'all-status'"
type="button"
class="picker-option"
:class="{ active: selectedStatus === option.value }"
@click="selectFilter('status', option.value)"
>
{{ option.label }}
</button>
</div>
</div>
</div>
</div>
<div class="toolbar-actions">
<button v-if="activeFilterTokens.length" class="ghost-filter-btn" type="button" @click="resetFilters">
<i class="mdi mdi-filter-remove-outline"></i>
<span>清空筛选</span>
</button>
<button class="create-btn" type="button" disabled>
<i class="mdi mdi-plus"></i>
<span>{{ createButtonLabel }}</span>
</button> </button>
</div> </div>
<button class="create-btn" type="button">
<i class="mdi mdi-plus"></i>
<span>{{ createButtonLabel }}</span>
</button>
</div> </div>
<p class="hint"><i class="mdi mdi-information-outline"></i> {{ hintText }}</p> <p class="hint"><i class="mdi mdi-information-outline"></i> {{ hintText }}</p>
<div v-if="activeFilterTokens.length" class="active-filter-strip">
<span v-for="token in activeFilterTokens" :key="token" class="active-filter-chip">
{{ token }}
</span>
</div>
<div class="table-wrap"> <div class="table-wrap">
<table> <div v-if="loading" class="table-state">
<i class="mdi mdi-loading mdi-spin"></i>
<p>正在加载{{ activeTabLabel }}资产...</p>
</div>
<div v-else-if="errorMessage" class="table-state error">
<i class="mdi mdi-alert-circle-outline"></i>
<p>{{ errorMessage }}</p>
<button type="button" class="state-action" @click="loadAssets">重新加载</button>
</div>
<div v-else-if="!visibleSkills.length" class="table-state empty">
<i class="mdi mdi-database-search-outline"></i>
<p>没有匹配的资产数据</p>
</div>
<table v-else>
<thead> <thead>
<tr> <tr>
<th>{{ tableColumns.name }}</th> <th>{{ tableColumns.name }}</th>
@@ -276,9 +566,8 @@
<th>{{ tableColumns.runtime }}</th> <th>{{ tableColumns.runtime }}</th>
<th>{{ tableColumns.version }}</th> <th>{{ tableColumns.version }}</th>
<th>状态</th> <th>状态</th>
<th>{{ tableColumns.metric }}</th> <th v-if="showMetricColumn">{{ tableColumns.metric }}</th>
<th>最近更新</th> <th>最近更新</th>
<th>操作</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -286,7 +575,7 @@
v-for="skill in visibleSkills" v-for="skill in visibleSkills"
:key="skill.id" :key="skill.id"
:class="{ spotlight: skill.spotlight }" :class="{ spotlight: skill.spotlight }"
@click="selectedSkill = skill" @click="openAssetDetail(skill)"
> >
<td> <td>
<div class="skill-name-cell"> <div class="skill-name-cell">
@@ -303,17 +592,16 @@
<td>{{ skill.model }}</td> <td>{{ skill.model }}</td>
<td>{{ skill.version }}</td> <td>{{ skill.version }}</td>
<td><span class="status-pill" :class="skill.statusTone">{{ skill.status }}</span></td> <td><span class="status-pill" :class="skill.statusTone">{{ skill.status }}</span></td>
<td>{{ skill.hitRate }}</td> <td v-if="showMetricColumn">{{ skill.hitRate }}</td>
<td>{{ skill.updatedAt }}</td> <td>{{ skill.updatedAt }}</td>
<td>
<button class="row-action" type="button" @click.stop="selectedSkill = skill">
编辑
</button>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<footer v-if="!loading && !errorMessage" class="list-foot">
<span class="page-summary">当前展示 {{ visibleSkills.length }} 条资产</span>
</footer>
</article> </article>
</Transition> </Transition>
@@ -323,14 +611,14 @@
<div class="card-head"> <div class="card-head">
<div> <div>
<h3 id="version-switch-title">切换规则版本</h3> <h3 id="version-switch-title">切换规则版本</h3>
<p>切换后编辑器会加载该版本的 .md 内容当前未保存内容不会自动发布</p> <p>切换后编辑器只会替换当前展示内容不会直接回滚后端当前版本</p>
</div> </div>
</div> </div>
<div class="version-modal-summary"> <div class="version-modal-summary">
<div> <div>
<span>当前版本</span> <span>当前展示版本</span>
<strong>{{ selectedSkill?.version }}</strong> <strong>{{ selectedSkill?.displayVersion }}</strong>
</div> </div>
<i class="mdi mdi-arrow-right"></i> <i class="mdi mdi-arrow-right"></i>
<div> <div>

File diff suppressed because it is too large Load Diff