feat:create skill example

This commit is contained in:
aries_ckt
2026-02-28 17:06:05 +08:00
parent 48aa8af1af
commit 11057f4782
9 changed files with 5439 additions and 5234 deletions

View File

@@ -65,7 +65,7 @@ async def execute_skill_script(skill_name: str, script_name: str, args: dict) ->
"\\n示例:"
'\\n- 读取参考文档: {"skill_name": "my-skill", '
'"resource_path": "references/analysis_framework.md"}'
"\\n注意: 执行脚本请使用 execute_skill_script_file 工具"
"\n注意: 执行脚本请使用 shell_interpreter 工具"
)
async def get_skill_resource(
skill_name: str, resource_path: str, args: Optional[dict] = None
@@ -1498,6 +1498,113 @@ print(json.dumps(summary, ensure_ascii=False))
}
)
# ── Safety-net post-processing for skill script execution ──
# If the LLM used shell_interpreter to run a skill script despite
# the prompt requesting execute_skill_script_file, we still capture
# critical side-effects (ratio_data, images) into react_state.
_code_lower = code.strip().lower()
_is_skill_script = "skills/" in _code_lower and ".py" in _code_lower
if _is_skill_script and output_text.strip():
import shutil
from dbgpt.configs.model_config import STATIC_MESSAGE_IMG_PATH
# 1) Capture calculate_ratios.py output as ratio_data
if "calculate_ratios" in _code_lower:
try:
ratio_data = json.loads(output_text.strip())
if isinstance(ratio_data, dict):
react_state["ratio_data"] = ratio_data
logger.info(
"shell_interpreter: captured %d ratio_data keys",
len(ratio_data),
)
except Exception:
pass
# 2) Capture generate_charts.py output — look for image paths
# and copy them to static dir, same as execute_skill_script_file
if "generate_charts" in _code_lower:
try:
os.makedirs(STATIC_MESSAGE_IMG_PATH, exist_ok=True)
IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"}
# Try to parse JSON output for image paths
try:
chart_output = json.loads(output_text.strip())
if isinstance(chart_output, dict):
# Might be {"charts": {...}} or flat dict
chart_map = chart_output.get("charts", chart_output)
for name, abs_path in chart_map.items():
if isinstance(abs_path, str) and os.path.isfile(abs_path):
ext = os.path.splitext(abs_path)[1].lower()
if ext in IMAGE_EXTS:
unique_name = (
f"{uuid.uuid4().hex[:8]}_"
f"{os.path.basename(abs_path)}"
)
dest = os.path.join(
STATIC_MESSAGE_IMG_PATH, unique_name
)
shutil.copy2(abs_path, dest)
img_url = f"/images/{unique_name}"
react_state.setdefault(
"generated_images", []
).append(img_url)
orig_stem = os.path.splitext(
os.path.basename(abs_path)
)[0].lower()
react_state.setdefault(
"image_url_map", {}
)[orig_stem] = img_url
except (json.JSONDecodeError, TypeError):
pass
# Also scan the output dir for any new .png files
cid = react_state.get("conv_id") or "default"
from dbgpt.configs.model_config import PILOT_PATH
out_dir = os.path.join(PILOT_PATH, "tmp", cid)
if os.path.isdir(out_dir):
for fname in os.listdir(out_dir):
ext = os.path.splitext(fname)[1].lower()
if ext in IMAGE_EXTS:
abs_path = os.path.join(out_dir, fname)
orig_stem = os.path.splitext(fname)[0].lower()
if orig_stem not in react_state.get(
"image_url_map", {}
):
unique_name = (
f"{uuid.uuid4().hex[:8]}_{fname}"
)
dest = os.path.join(
STATIC_MESSAGE_IMG_PATH, unique_name
)
shutil.copy2(abs_path, dest)
img_url = f"/images/{unique_name}"
react_state.setdefault(
"generated_images", []
).append(img_url)
react_state.setdefault(
"image_url_map", {}
)[orig_stem] = img_url
# Append image URL summary for LLM reference
all_images = react_state.get("generated_images", [])
if all_images:
img_summary = (
"\u5df2\u751f\u6210\u7684\u56fe\u7247URL\uff08\u5728\u751f\u6210HTML\u62a5\u544a\u65f6\u8bf7\u4f7f\u7528\u8fd9\u4e9bURL\uff09:\n"
+ "\n".join(f" - {url}" for url in all_images)
)
chunks.append(
{"output_type": "text", "content": img_summary}
)
logger.info(
"shell_interpreter: captured %d images for skill script",
len(react_state.get("image_url_map", {})),
)
except Exception as e:
logger.warning(
"shell_interpreter: image post-processing failed: %s", e
)
return json.dumps({"chunks": chunks}, ensure_ascii=False)
@tool(
@@ -2051,7 +2158,7 @@ print(json.dumps(summary, ensure_ascii=False))
1. 严格按照已加载技能的指令执行
2. 每个步骤输出 Thought → Phase → Action → Action Input
3. 等待系统返回 Observation 后,再决定下一步
4. 如果任务需要生成分析报告,流程为:`execute_skill_script_file` 执行 `extract_financials.py` 提取数据 → `execute_skill_script_file` 执行 `calculate_ratios.py` 计算比率(系统自动记录结果) → `execute_skill_script_file` 执行 `generate_charts.py` 生成图表(系统自动合并图片) → 调用 `html_interpreter(template_path=..., data={{...仅包含你写的分析文本}})` 渲染报告(系统自动合并数据和图片) → `terminate` 返回摘要
4. 如果任务需要生成分析报告,流程为:`execute_skill_script_file` 执行 extract_financials.py 提取数据 → `execute_skill_script_file` 执行 calculate_ratios.py 计算比率(系统自动记录结果) → `execute_skill_script_file` 执行 generate_charts.py 生成图表(系统自动合并图片) → 调用 `html_interpreter(template_path=..., data={{...仅包含你写的分析文本}})` 渲染报告(系统自动合并数据和图片) → `terminate` 返回摘要
5. 如果任务不需要生成报告,直接调用 terminate 返回最终结果Action Input 格式必须为 {{"result": "最终回答"}}
{skill_prompt_context}
@@ -2059,26 +2166,25 @@ print(json.dumps(summary, ensure_ascii=False))
## 技能执行规范
### 资源使用
- **需要计算/处理数据** → 使用 `execute_skill_script_file` 执行技能 scripts 目录下的脚本
- **需要执行技能脚本** → 使用 `execute_skill_script_file`,参数为 {{"skill_name": "技能名", "script_file_name": "脚本文件名", "args": {{参数}}}}。此工具会自动处理图片复制和数据记录。
- **需要了解指标定义/分析框架** → 使用 `get_skill_resource` 并指定 `references/xxx.md` 路径读取参考文档
- **遇到图片文件** → 如果模型不支持图片输入,会返回错误提示
- **需要生成报告** → 调用 `html_interpreter`,传入 `template_path`(模板相对路径)和 `data`仅包含你自己撰写的分析文本由于后端会自动合并之前工具生成的30个数据指标和图片URL请绝对不要在 `data` 里写这些数据指标,否则会导致超长截断)。**不要使用 `code_interpreter`,不要使用 `execute_skill_script_file` 生成报告,不需要先用 `get_skill_resource` 读取模板**
- **需要生成报告** → 调用 `html_interpreter`,传入 `template_path`(模板相对路径)和 `data`仅包含你自己撰写的分析文本由于后端会自动合并之前工具生成的30个数据指标和图片URL请绝对不要在 `data` 里写这些数据指标,否则会导致超长截断)。**不要使用 `code_interpreter` 生成报告,不需要先用 `get_skill_resource` 读取模板**
## 可用工具说明
1. **execute_skill_script_file**(用于执行技能自身的脚本): 执行技能 scripts 目录下的脚本文件。
参数: {{"skill_name": "技能名", "script_file_name": "脚本文件名", "args": {{"参数名": "参数值"}}}}
- 示例: {{"skill_name": "{pre_matched_skill.metadata.name if pre_matched_skill else 'skill'}", "script_file_name": "calculate.py", "args": {{"param": "value"}}}}
1. **execute_skill_script_file**推荐用于执行技能脚本): 执行技能 scripts 目录下的脚本文件,自动处理图片复制到静态目录、记录计算结果等后处理
参数: {{"skill_name": "技能名", "script_file_name": "脚本文件名", "args": {{参数}}}}
- 示例: {{"skill_name": "{pre_matched_skill.metadata.name if pre_matched_skill else 'skill'}", "script_file_name": "calculate_ratios.py", "args": {{"input_data": "..."}}}}
- **执行技能脚本时,必须使用此工具**,不要使用 shell_interpreter
2. **get_skill_resource**: 读取技能中的参考文档、配置、模板等非脚本资源文件。
参数: {{"skill_name": "技能名", "resource_path": "资源路径"}}
- 读取参考文档: {{"skill_name": "{pre_matched_skill.metadata.name if pre_matched_skill else 'skill'}", "resource_path": "references/analysis_framework.md"}}
- 注意: 报告模板不需要用此工具读取,直接用 html_interpreter 的 template_path 参数
3. **execute_skill_script**: 执行技能中定义的内联脚本。参数: {{"skill_name": "技能名", "script_name": "脚本名", "args": {{"参数名": "参数值"}}}}
4. **shell_interpreter**: 执行 shell/bash 命令。适用于运行系统命令、执行 Python 脚本等场景
3. **execute_skill_script**: 执行技能中定义的内联脚本(备用)。参数: {{"skill_name": "技能名", "script_name": "脚本名", "args": {{"参数名": "参数值"}}}}
4. **shell_interpreter**: 执行 shell/bash 命令(仅用于非技能脚本的系统命令,如 ls、cat 等)
参数: {{"code": "shell命令"}}
- 示例: {{"code": "python skills/skill-creator/scripts/init_skill.py my-skill --path skills/"}}
- 示例: {{"code": "ls -la skills/my-skill/"}}
- 每次调用独立,不保留状态。如需多步操作,用 `&&` 或 `;` 串联命令。
- **当技能 SKILL.md 指示使用 shell_interpreter 执行脚本时,优先使用此工具**
- **注意:不要用此工具执行技能脚本**,因为它不会自动处理图片和数据记录
5. **html_interpreter**: 将 HTML 模板渲染为网页报告。
推荐用法: {{"template_path": "技能名/templates/模板文件.html", "data": {{"PLACEHOLDER_KEY": "", ...}}, "title": "报告标题"}}
- 后端会自动把先前的财务数据计算结果合并进模板中。你只需要在 `data` 字典中返回诸如 `PROFITABILITY_ANALYSIS` 等你手写的分析段落即可,无需包含 `COMPANY_NAME` 或 `REVENUE` 等。
@@ -2135,7 +2241,7 @@ Action Input: 工具参数的 JSON 格式
加载技能后,仔细阅读 SKILL.md 中的 **核心工作流程** 部分,按步骤顺序执行。如果某个步骤明确说明了跳过条件(如用户意图已明确时),则直接跳到下一步,不要强制执行每一步。优先快速产出结果,迭代优化在后续步骤完成。
### 2. 资源使用时机
- **需要计算/处理数据** → 使用 `execute_skill_script_file` 执行技能 scripts 目录下的脚本
- **需要计算/处理数据** → 使用 `execute_skill_script_file` 执行技能 scripts 目录下的脚本(此工具会自动处理图片和数据记录),参数为 {{"skill_name": "技能名", "script_file_name": "脚本.py", "args": {{参数}}}}
- **需要了解指标定义/分析框架** → 使用 `get_skill_resource` 并指定 `references/xxx.md` 路径读取参考文档
- **遇到图片文件** → 如果模型不支持图片输入,会返回错误提示
@@ -2152,20 +2258,19 @@ Action Input: {{"skill_name": "financial-report-analyzer", "resource_path": "ref
Thought: 现在执行脚本计算比率
Phase: 执行比率计算脚本
Action: execute_skill_script_file
Action Input: {{"skill_name": "financial-report-analyzer", "script_file_name": "calculate_ratios.py", "args": {{...}}}}
Action Input: {{"skill_name": "financial-report-analyzer", "script_file_name": "calculate_ratios.py", "args": {{"input_data": "..."}}}}
```
## 可用工具说明
1. **load_skill**: 加载指定技能的详细说明,参数: {{"skill_name": "技能名", "file_path": "技能文件路径"}}
2. **knowledge_retrieve**: 从知识库中检索相关信息,参数: {{"query": "检索问题"}}
3. **execute_skill_script**: 执行技能中定义的内联脚本,参数: {{"skill_name": "技能名", "script_name": \
3. **execute_skill_script**: 执行技能中定义的内联脚本(备用),参数: {{"skill_name": "技能名", "script_name": \
"脚本名", "args": {{"参数名": "参数值"}}}}
4. **execute_skill_script_file**(推荐用于脚本执行): 执行技能 scripts 目录下的脚本文件,参数: {{"skill_name": "技能名", \
"script_file_name": "脚本文件名如calculate_ratios.py", "args": {{"参数名": "参数值"}}}}
4. **execute_skill_script_file**(推荐用于执行技能脚本: 执行技能 scripts 目录下的脚本文件,自动处理图片复制和数据记录。参数: {{"skill_name": "技能名", "script_file_name": "脚本文件名", "args": {{参数}}}}
5. **get_skill_resource**: 读取技能中的参考文档、配置等非脚本资源文件。
参数: {{"skill_name": "技能名", "resource_path": "资源路径"}}
- 读取参考文档: {{"skill_name": "my-skill", "resource_path": "references/analysis_framework.md"}}
- 注意: 执行脚本请使用 execute_skill_script_file不要用此工具执行脚本
- 注意: 执行脚本请使用 execute_skill_script_file(不是 shell_interpreter,不要用此工具执行脚本
- 图片文件会返回错误提示(模型不支持)
6. **code_interpreter**: 执行 Python 代码进行数据分析和计算。支持 pandas、numpy、matplotlib 等。已预导入 \
pandas(pd)、numpy(np)、json、os。如果用户上传了文件FILE_PATH 变量已预设为文件路径。PLOT_DIR 变量已预设为图片保存目录。参数: {{"code": "python代码"}}

View File

@@ -26,6 +26,10 @@ EXAMPLE_FILES = {
"path": "docker/examples/fin_report/pdf/2020-01-23__浙江海翔药业股份有限公司__002099__海翔药业__2019年__年度报告.pdf",
"name": "2020-01-23__浙江海翔药业股份有限公司__002099__海翔药业__2019年__年度报告.pdf",
},
"create_sql_skill": {
"path": "docker/examples/txt/sql_skill.txt",
"name": "sql_skill.txt",
},
}

View File

@@ -14,70 +14,94 @@ description: 专门用于上市公司财报(如年度报告、季度报告)
- 脚本支持 PDF 文件(通过 pdfplumber 解析)和纯文本文件,返回 JSON 格式的结构化数据。
2. **财务比率计算**
- 将提取的 JSON 数据传递给 `scripts/calculate_ratios.py` 脚本
- 使用 `execute_skill_script_file` 执行 `scripts/calculate_ratios.py`,传入 Step 1 的 JSON 数据
- 自动计算毛利率、净利率、ROE、资产负债率等关键指标输出 30 个模板占位符键值。
- 参考 `references/financial_metrics.md` 确保指标定义的准确性。
- **系统会自动保存返回的 JSON 结果**`react_state["ratio_data"]`),后续 html_interpreter 会自动合并。
3. **图表生成**
- 将提取的 JSON 数据传递给 `scripts/generate_charts.py` 脚本
- 使用 `execute_skill_script_file` 执行 `scripts/generate_charts.py`,传入 Step 1 的 JSON 数据
- 自动生成 3 张可视化图表:
- `financial_overview.png`:核心财务指标对比柱状图
- `profitability.png`:盈利能力指标横向条形图
- `asset_structure.png`:资产结构环形饼图
- 脚本执行后,系统自动扫描生成的图片文件并返回 `/images/xxx.png` 格式的 URL可直接用于 HTML 报告
- **系统自动将图片复制到静态目录并记录 URL 映射**`react_state["image_url_map"]`),后续 html_interpreter 会自动合并
4. **深度分析**
- 遵循 `references/analysis_framework.md` 提供的框架,从盈利质量、偿债风险、营运效率和现金流四个维度进行深度剖析。
- 结合"经营情况讨论与分析"章节,解释业绩变动的核心驱动因素。
- 撰写以下 7 段分析文本:
- `PROFITABILITY_ANALYSIS`:盈利能力分析
- `SOLVENCY_ANALYSIS`:偿债与风险分析
- `EFFICIENCY_ANALYSIS`:营运效率分析
- `CASHFLOW_ANALYSIS`:现金流与利润质量分析
- `ADVANTAGES_LIST`核心优势列表HTML `<li>` 格式)
- `RISKS_LIST`主要风险列表HTML `<li>` 格式)
- `OVERALL_ASSESSMENT`:综合评价
5. **报告生成与展示**
- 调用 `html_interpreter`传入 `template_path` `data` 参数
- `template_path`: `"financial-report-analyzer/templates/report_template.html"`
- `data`: 包含你需要填写的占位符键值的字典,包括:
- LLM 自行撰写的分析文本键:`PROFITABILITY_ANALYSIS`, `SOLVENCY_ANALYSIS`, `EFFICIENCY_ANALYSIS`, `CASHFLOW_ANALYSIS`, `ADVANTAGES_LIST`, `RISKS_LIST`, `OVERALL_ASSESSMENT`
- (注:来自 `calculate_ratios.py` 的 30 个数据键会被系统自动合并到模板中,你不需要在 `data` 里再次传入它们,只需传入你写的分析文本即可)
- 后端自动读取模板文件并替换 `{{占位符}}`,无需手动拼接 HTML。
5. **渲染报告**
- 调用 `html_interpreter`使用 `template_path` 模式
```json
{
"template_path": "financial-report-analyzer/templates/report_template.html",
"data": {
"PROFITABILITY_ANALYSIS": "LLM撰写的盈利能力分析...",
"SOLVENCY_ANALYSIS": "LLM撰写的偿债分析...",
"EFFICIENCY_ANALYSIS": "LLM撰写的营运效率分析...",
"CASHFLOW_ANALYSIS": "LLM撰写的现金流分析...",
"ADVANTAGES_LIST": "<li>优势1</li><li>优势2</li>",
"RISKS_LIST": "<li>风险1</li><li>风险2</li>",
"OVERALL_ASSESSMENT": "LLM撰写的综合评价..."
},
"title": "XX公司 2023年度财报分析报告"
}
```
- **重要**`data` 字典中只需传入你撰写的 7 段分析文本!后端会自动合并:
- Step 2 的 30 个数据指标COMPANY_NAME、REVENUE、NET_PROFIT 等)
- Step 3 的图表 URLCHART_FINANCIAL_OVERVIEW、CHART_PROFITABILITY、CHART_ASSET_STRUCTURE
- **绝对不要**在 `data` 中包含数据指标或图表路径,否则 JSON 过大会导致截断。
6. **完成**
- 调用 `terminate` 返回 1-2 句话的简短摘要。
- **注意**:不需要使用 `code_interpreter`,不需要使用 `execute_skill_script_file` 生成报告,不需要先用 `get_skill_resource` 读取模板。
- **严格保留**模板的完整结构所有章节第1-4章不得删除摘要表格必须保留4列。
- 报告会以卡片形式展示在左侧面板,用户点击卡片即可在右侧面板查看完整报告。
## 完整流程示例
```
Step 1: execute_skill_script_file(skill_name="financial-report-analyzer", script_file_name="extract_financials.py", args={"file_path": "/path/to/report.pdf"})
→ 返回 JSON: {"revenue": 10500000000, "net_profit": 1200000000, ...}
→ 返回 JSON: {"revenue": 10500000000, "net_profit": 1200000000, ...} (记为 raw_data
Step 2: execute_skill_script_file(skill_name="financial-report-analyzer", script_file_name="calculate_ratios.py", args=<Step1的JSON结果>)
→ 返回 30 个模板键值: {"COMPANY_NAME": "XX公司", "REVENUE": "105.00亿元", "GROSS_MARGIN": "28.57%", ...}
Step 2: execute_skill_script_file(skill_name="financial-report-analyzer", script_file_name="calculate_ratios.py", args=<raw_data>)
→ 返回 30 个模板键值,系统自动记录到 react_state["ratio_data"]
Step 3: execute_skill_script_file(skill_name="financial-report-analyzer", script_file_name="generate_charts.py", args=<Step1的JSON结果>)
→ 生成图表,系统自动返回图片URL: /images/xxx_financial_overview.png, /images/xxx_profitability.png, /images/xxx_asset_structure.png
Step 3: execute_skill_script_file(skill_name="financial-report-analyzer", script_file_name="generate_charts.py", args=<raw_data>)
→ 生成图表,系统自动复制到 /images/ 并记录 URL 映射
Step 4: html_interpreter(template_path="financial-report-analyzer/templates/report_template.html", data={
"PROFITABILITY_ANALYSIS": "LLM撰写的盈利能力分析...",
"SOLVENCY_ANALYSIS": "LLM撰写的偿债分析...",
...其他LLM分析文本
})
Step 4: LLM 自行撰写 7 段深度分析文本)
Step 5: terminate(message="简短摘要")
Step 5: html_interpreter(template_path="financial-report-analyzer/templates/report_template.html", data={仅包含 7 段分析文本}, title="报告标题")
→ 后端自动合并数据指标 + 图表 URL + 分析文本,渲染完整报告
Step 6: terminate(result="简短摘要")
```
## 资源使用说明
- **脚本**
- **脚本**(均通过 `execute_skill_script_file` 执行)
- `scripts/extract_financials.py`:接收 `file_path` 参数,读取财报文件(支持 PDF 和文本格式),提取核心财务数据。
- `scripts/calculate_ratios.py`:计算财务比率,输出 30 个模板占位符键值。
- `scripts/generate_charts.py`:生成 3 张可视化图表matplotlib图表 URL 由系统自动扫描返回
- `scripts/calculate_ratios.py`:计算财务比率,输出 30 个模板占位符键值。系统自动记录结果。
- `scripts/generate_charts.py`:生成 3 张可视化图表matplotlib系统自动处理图片复制
- `scripts/fill_template.py`:(备用)接收 `ratio_data`、`chart_paths`、`analysis` 三个参数,读取 HTML 模板并替换所有占位符。正常情况下不需要使用此脚本,因为 html_interpreter 的 template_path 模式会自动完成模板填充。
- **参考**
- `references/financial_metrics.md`:包含公式定义。
- `references/analysis_framework.md`:包含分析逻辑。
- **模板**
- `templates/report_template.html`:最终交付报告的 HTML 模板(**必须严格遵循**,不得删减章节或修改表格结构)。通过 `html_interpreter``template_path` 参数直接引用,后端自动替换 `{{占位符}}`。模板包含 3 处图表占位符(`{{CHART_FINANCIAL_OVERVIEW}}`, `{{CHART_PROFITABILITY}}`, `{{CHART_ASSET_STRUCTURE}}`),需填入 Step 3 返回的图片 URL
- `templates/report_template.html`:最终交付报告的 HTML 模板(**必须严格遵循**,不得删减章节或修改表格结构)。html_interpreter 的 template_path 参数自动读取并填充
- `templates/report_template.md`Markdown 版本,仅供参考结构说明。
## 注意事项
- **必须使用 `execute_skill_script_file`** 执行脚本(不要用 shell_interpreter因为 `execute_skill_script_file` 会自动处理图片复制和数据记录。
- 脚本提取可能受排版影响,建议在计算前人工核对提取的关键数值。
- 始终关注"非经常性损益",以评估公司核心业务的真实盈利能力。
- 对比至少三年的历史数据,以识别趋势。

10224
uv.lock generated

File diff suppressed because one or more lines are too long

View File

@@ -342,7 +342,7 @@ const SkillCompactCard: React.FC<{
onDownload?: () => void;
}> = memo(({ skillName, onClick, onDownload }) => {
const [downloading, setDownloading] = useState(false);
const [isAdded, setIsAdded] = useState(false);
return (
<div
onClick={onClick}
@@ -373,14 +373,30 @@ const SkillCompactCard: React.FC<{
</button>
</Tooltip>
<button
className='flex items-center gap-1.5 bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-900 rounded-lg text-xs font-medium px-4 py-2 hover:opacity-90 transition-opacity'
className={`flex items-center gap-1.5 rounded-lg text-xs font-medium px-4 py-2 transition-all duration-200 ${
isAdded
? 'bg-green-50 text-green-600 border border-green-200 dark:bg-green-900/20 dark:text-green-500 dark:border-green-800'
: 'bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-900 hover:opacity-90'
}`}
onClick={e => {
e.stopPropagation();
message.success(`技能 "${skillName}" 已添加到我的技能`);
if (!isAdded) {
setIsAdded(true);
message.success(`技能 "${skillName}" 已添加到我的技能`);
}
}}
>
<PlusOutlined className='text-[10px]' />
{isAdded ? (
<>
<CheckOutlined className='text-[10px]' />
</>
) : (
<>
<PlusOutlined className='text-[10px]' />
</>
)}
</button>
</div>
</div>

View File

@@ -6,6 +6,7 @@ import {
AppstoreOutlined,
BarChartOutlined,
CheckCircleFilled,
CheckOutlined,
CloseCircleFilled,
CodeOutlined,
ConsoleSqlOutlined,
@@ -1047,14 +1048,14 @@ const parseSkillCreatorOutput = (detail?: string, outputs?: ExecutionOutput[]):
const parsed = JSON.parse(inputMatch[1]);
if (parsed.skill_name) return parsed.skill_name;
if (parsed.name) return parsed.name;
} catch { /* ignore */ }
} catch {
/* ignore */
}
}
}
// Priority 3: Last skills/xxx path (skip skill-creator which is the tool, not the created skill)
const allPaths = [...combined.matchAll(/skills\/([\w-]+)/g)]
.map(m => m[1])
.filter(name => name !== 'skill-creator');
const allPaths = [...combined.matchAll(/skills\/([\w-]+)/g)].map(m => m[1]).filter(name => name !== 'skill-creator');
if (allPaths.length > 0) return allPaths[allPaths.length - 1];
return null;
@@ -1137,7 +1138,7 @@ const SkillCardRenderer: React.FC<{
const [fileContent, setFileContent] = useState<string>('');
const [showDetail, setShowDetail] = useState(true);
const [downloading, setDownloading] = useState(false);
const [isAdded, setIsAdded] = useState(false);
// Fetch skill detail on mount
useEffect(() => {
const fetchDetail = async () => {
@@ -1212,8 +1213,11 @@ const SkillCardRenderer: React.FC<{
}, [skillName]);
const handleAddToSkills = useCallback(() => {
message.success(`技能 "${skillName}" 已添加到我的技能`);
}, [skillName]);
if (!isAdded) {
setIsAdded(true);
message.success(`技能 "${skillName}" 已添加到我的技能`);
}
}, [skillName, isAdded]);
const displayName = detailData?.metadata?.name || detailData?.skill_name || skillName;
const description = detailData?.metadata?.description || '';
@@ -1271,11 +1275,15 @@ const SkillCardRenderer: React.FC<{
<Button
type='primary'
size='small'
className='!bg-gray-900 !border-gray-900 dark:!bg-gray-100 dark:!border-gray-100 dark:!text-gray-900 !text-white !rounded-lg !text-xs !font-medium !px-3'
icon={<PlusOutlined className='text-[10px]' />}
className={`!rounded-lg !text-xs !font-medium !px-3 ${
isAdded
? '!bg-green-50 !text-green-600 !border-green-200 dark:!bg-green-900/20 dark:!text-green-500 dark:!border-green-800'
: '!bg-gray-900 !border-gray-900 dark:!bg-gray-100 dark:!border-gray-100 dark:!text-gray-900 !text-white'
}`}
icon={isAdded ? <CheckOutlined className='text-[10px]' /> : <PlusOutlined className='text-[10px]' />}
onClick={handleAddToSkills}
>
{isAdded ? '已添加' : '添加到我的技能'}
</Button>
</div>
</div>
@@ -1335,11 +1343,15 @@ const SkillCardRenderer: React.FC<{
<Button
type='primary'
size='small'
className='!bg-gray-900 !border-gray-900 dark:!bg-gray-100 dark:!border-gray-100 dark:!text-gray-900 !text-white !rounded-lg !text-xs !font-medium !px-3'
icon={<PlusOutlined className='text-[10px]' />}
className={`!rounded-lg !text-xs !font-medium !px-3 ${
isAdded
? '!bg-green-50 !text-green-600 !border-green-200 dark:!bg-green-900/20 dark:!text-green-500 dark:!border-green-800'
: '!bg-gray-900 !border-gray-900 dark:!bg-gray-100 dark:!border-gray-100 dark:!text-gray-900 !text-white'
}`}
icon={isAdded ? <CheckOutlined className='text-[10px]' /> : <PlusOutlined className='text-[10px]' />}
onClick={handleAddToSkills}
>
{isAdded ? '已添加' : '添加到我的技能'}
</Button>
</div>
</div>
@@ -1355,37 +1367,56 @@ const SkillCardRenderer: React.FC<{
<div className='flex-1 min-w-0 overflow-auto p-4'>
{fileContent ? (
<div className='prose prose-sm dark:prose-invert max-w-none'>
{fileContent.startsWith('---') ?
(() => {
const parts = fileContent.split('---');
if (parts.length >= 3) {
return (
<>
<pre className='text-xs bg-gray-50 dark:bg-[#161719] rounded-lg px-4 py-3 text-gray-600 dark:text-gray-300 font-mono leading-relaxed mb-4 border border-gray-200 dark:border-gray-700'>
{parts[1].trim()}
</pre>
<MarkDownContext>{preprocessLaTeX(parts.slice(2).join('---').trim())}</MarkDownContext>
</>
);
}
return <MarkDownContext>{preprocessLaTeX(fileContent)}</MarkDownContext>;
})()
: (() => {
const ext = selectedFile?.split('.').pop()?.toLowerCase();
const langMap: Record<string, string> = {
py: 'python', sh: 'bash', bash: 'bash', zsh: 'bash',
js: 'javascript', ts: 'typescript', jsx: 'javascript', tsx: 'typescript',
json: 'json', yaml: 'yaml', yml: 'yaml', toml: 'toml',
sql: 'sql', md: 'markdown', html: 'html', css: 'css',
xml: 'xml', java: 'java', go: 'go', rs: 'rust', rb: 'ruby',
c: 'c', cpp: 'cpp', h: 'c', hpp: 'cpp',
};
const lang = ext ? langMap[ext] : undefined;
if (lang) {
return <CodePreview code={fileContent} language={lang} />;
}
return <MarkDownContext>{preprocessLaTeX(fileContent)}</MarkDownContext>;
})()}
{fileContent.startsWith('---')
? (() => {
const parts = fileContent.split('---');
if (parts.length >= 3) {
return (
<>
<pre className='text-xs bg-gray-50 dark:bg-[#161719] rounded-lg px-4 py-3 text-gray-600 dark:text-gray-300 font-mono leading-relaxed mb-4 border border-gray-200 dark:border-gray-700'>
{parts[1].trim()}
</pre>
<MarkDownContext>{preprocessLaTeX(parts.slice(2).join('---').trim())}</MarkDownContext>
</>
);
}
return <MarkDownContext>{preprocessLaTeX(fileContent)}</MarkDownContext>;
})()
: (() => {
const ext = selectedFile?.split('.').pop()?.toLowerCase();
const langMap: Record<string, string> = {
py: 'python',
sh: 'bash',
bash: 'bash',
zsh: 'bash',
js: 'javascript',
ts: 'typescript',
jsx: 'javascript',
tsx: 'typescript',
json: 'json',
yaml: 'yaml',
yml: 'yaml',
toml: 'toml',
sql: 'sql',
md: 'markdown',
html: 'html',
css: 'css',
xml: 'xml',
java: 'java',
go: 'go',
rs: 'rust',
rb: 'ruby',
c: 'c',
cpp: 'cpp',
h: 'c',
hpp: 'cpp',
};
const lang = ext ? langMap[ext] : undefined;
if (lang) {
return <CodePreview code={fileContent} language={lang} />;
}
return <MarkDownContext>{preprocessLaTeX(fileContent)}</MarkDownContext>;
})()}
</div>
) : (
<div className='flex flex-col items-center justify-center py-12 text-gray-400'>

View File

@@ -302,12 +302,16 @@ const ToolPartDisplay: React.FC<ToolPartDisplayProps> = ({ part, defaultOpen = f
<ToolIcon name='brain' size='medium' className='text-indigo-500' />
</div>
<div className='min-w-0 flex-1'>
<div className='text-sm font-semibold text-gray-800 dark:text-gray-200 truncate'>{skillInfo.name}</div>
<div className='text-sm font-semibold text-gray-800 dark:text-gray-200 truncate'>
{skillInfo.name}
</div>
<div className='text-[11px] text-gray-400 dark:text-gray-500'>Skill</div>
</div>
</div>
{skillInfo.description && (
<p className='text-sm text-gray-600 dark:text-gray-400 leading-relaxed mt-2'>{skillInfo.description}</p>
<p className='text-sm text-gray-600 dark:text-gray-400 leading-relaxed mt-2'>
{skillInfo.description}
</p>
)}
</div>
</div>

View File

@@ -45,7 +45,9 @@ const ThinkingProcess: React.FC<ThinkingProcessProps> = ({
if (!content) return null;
return (
<div className={classNames('text-sm text-gray-700 dark:text-gray-300 leading-relaxed whitespace-pre-wrap', className)}>
<div
className={classNames('text-sm text-gray-700 dark:text-gray-300 leading-relaxed whitespace-pre-wrap', className)}
>
{displayedText}
{!isComplete && <span className='inline-block w-1.5 h-4 ml-1 bg-blue-500 animate-pulse' />}
</div>

View File

@@ -485,6 +485,18 @@ const EXAMPLE_CARDS = [
iconBg: 'bg-violet-100 dark:bg-violet-900/40',
skillName: 'financial-report-analyzer',
},
{
id: 'create_sql_skill',
icon: '🛠️',
title: '创建SQL分析技能',
description: '使用skill-creator创建一个实用的SQL数据分析技能',
query:
'请使用 skill-creator 帮我创建一个实用的SQL数据分析技能包含连接数据库、执行SQL查询和数据可视化等核心功能。',
color: 'from-amber-500/10 to-orange-500/10',
borderColor: 'border-amber-200/60 dark:border-amber-800/40',
iconBg: 'bg-amber-100 dark:bg-amber-900/40',
skillName: 'skill-creator',
},
];
const Playground: NextPage = () => {
@@ -1843,36 +1855,43 @@ const Playground: NextPage = () => {
try {
message.loading({ content: '正在加载示例...', key: 'example-loading', duration: 0 });
const res = await axios.post(`${process.env.API_BASE_URL ?? ''}/api/v1/examples/use`, {
example_id: example.id,
});
let filePath: string | null = null;
let fakeFile: File | null = null;
// If example has a file, request it from backend
if (example.fileName) {
const res = await axios.post(`${process.env.API_BASE_URL ?? ''}/api/v1/examples/use`, {
example_id: example.id,
});
if (res?.success && res?.data) {
filePath = res.data;
preloadedFilePathRef.current = filePath;
fakeFile = new File([new ArrayBuffer(example.fileSize || 0)], example.fileName, {
type: example.fileType,
});
setUploadedFile(fakeFile);
} else {
message.destroy('example-loading');
const errMsg = res?.err_msg || 'Unknown error';
message.error('加载示例失败: ' + errMsg);
return;
}
}
message.destroy('example-loading');
if (res?.success && res?.data) {
const filePath = res.data;
preloadedFilePathRef.current = filePath;
const fakeFile = new File([new ArrayBuffer(example.fileSize || 0)], example.fileName, {
type: example.fileType,
});
setUploadedFile(fakeFile);
// Auto-select skill if example specifies one
let exampleSkill: Skill | null = null;
if (example.skillName && skillsList) {
const matched = skillsList.find(s => s.name === example.skillName);
if (matched) {
exampleSkill = matched;
setSelectedSkill(matched);
}
// Auto-select skill if example specifies one
let exampleSkill: Skill | null = null;
if (example.skillName && skillsList) {
const matched = skillsList.find(s => s.name === example.skillName);
if (matched) {
exampleSkill = matched;
setSelectedSkill(matched);
}
handleStart(example.query, fakeFile, exampleSkill);
} else {
const errMsg = res?.err_msg || 'Unknown error';
message.error('加载示例失败: ' + errMsg);
}
handleStart(example.query, fakeFile, exampleSkill);
} catch (err: unknown) {
message.destroy('example-loading');
console.error('Example click error:', err);