diff --git a/wwwroot/api/cockpit_chat.dspy b/wwwroot/api/cockpit_chat.dspy index 385ac21..2e142be 100644 --- a/wwwroot/api/cockpit_chat.dspy +++ b/wwwroot/api/cockpit_chat.dspy @@ -878,29 +878,60 @@ if action == 'send_message': elif intent_type == 'devops': # 从自然语言中提取实际命令:剥前缀 + 识别 git URL 自动构造 git clone cmd = message_text.strip() - import re + import re, os cmd = re.sub(r'^(执行命令[::]|运行[::]|帮我\s*|克隆仓库\s*|从.*克隆\s*)', '', cmd).strip() # 如果包含 git@ 或 https://...git 但没有 git clone 前缀,自动补全 + repo_url = None + repo_target = None if re.search(r'(git@[\w.]+:[\w./-]+\.git|https?://[\w./-]+\.git)', cmd): if not cmd.startswith('git '): - # 提取 URL 和可选目标路径 m = re.search(r'(git@[\w.]+:[\w./-]+\.git|https?://[\w./-]+\.git)', cmd) repo_url = m.group(0) rest = cmd[m.end():].strip() target = rest.split()[0] if rest else '' if target and not target.startswith('-'): + repo_target = target cmd = f'git clone {repo_url} {target}' else: + repo_target = repo_url.rstrip('/').split('/')[-1].replace('.git', '') cmd = f'git clone {repo_url}' # 特殊处理:包含"到本地""到工作区"→只保留 git clone url cmd = re.sub(r'(\s+(到本地|到工作区|到workspace).*)', '', cmd).strip() workdir = ctx.get('workspace_dir', '') or '/d/pipeline/workspaces' result = await shell_exec(cmd, workdir=workdir, timeout=120 if 'git clone' in cmd else 60) + # git clone already exists → git pull + if result['rc'] != 0 and 'already exists' in result.get('stderr', '') and repo_target and cmd.startswith('git clone'): + pull_dir = os.path.join(workdir, repo_target) if not os.path.isabs(repo_target) else repo_target + debug(f'devops: clone already exists, pulling {pull_dir}') + result = await shell_exec(f'git -C {pull_dir} pull', workdir=workdir, timeout=60) if result['rc'] == 0: out = result['stdout'].strip() agent_reply = f"执行成功。{'输出:' + out[:500] if out else '(无输出)'}" else: agent_reply = f"执行失败(rc={result['rc']}):{result['stderr'][:500] or result['stdout'][:500]}" + # 多步任务:如果 git clone 成功且消息中含有"技能""skill""安装",自动触发 skill_import + if result['rc'] == 0 and repo_target and re.search(r'(技能|skill|安装技能|导入技能)', message_text): + clone_dir = os.path.join(workdir, repo_target) if not os.path.isabs(repo_target) else repo_target + if os.path.isdir(clone_dir): + try: + import_result = await skill_import_git('', skills_dir='skills') + # skill_import_git 的 repo_url 为空时不 clone,改用已有目录 + from pipeline_service.init import _resolve_workdir + full_skills = os.path.join(clone_dir, 'skills') + if os.path.isdir(full_skills): + found = [] + for entry in sorted(os.listdir(full_skills)): + entry_path = os.path.join(full_skills, entry) + if os.path.isdir(entry_path): + found.append(entry) + if found: + agent_reply += f'\n\n✅ 发现 {len(found)} 个技能:' + ', '.join(found[:15]) + else: + agent_reply += '\n\n⚠️ skills 目录下未找到技能子目录' + else: + agent_reply += '\n\n⚠️ 仓库中无 skills 目录' + except Exception as e2: + debug(f'skill scan error: {e2}') else: # chat: general conversation messages = await _build_context(sor, iteration_id or ctx['iteration_id'], '',