refactor: workspace 路径解析收敛到 workspace.py,dspy 消除重复
- workspace.py 加 get_workspace_dir/get_workspace_base + load_workspace 注册到 ServerEnv - 8 个 workspace dspy 内联重复的'读 workspace_base + 查 project 算 ws_dir'改为调 get_workspace_dir - _t_create_project 复用 get_workspace_base(删掉死代码 _get_param) - workspace.py 从 untracked 变为已跟踪提交
This commit is contained in:
parent
bd3a9a5abd
commit
d2f3caf8a7
@ -26,23 +26,6 @@ from typing import AsyncGenerator, Dict, List, Optional
|
||||
logger = logging.getLogger("pipeline.agent_executor")
|
||||
|
||||
|
||||
async def _get_param(sor, name: str, default: str = "") -> str:
|
||||
"""从 appbase params 表读参数(params_name → params_value),带默认值兜底。
|
||||
|
||||
params 表由 appbase 模块提供,用于存系统级可配置参数,避免硬编码。
|
||||
默认值仅作兜底,真正值以 params 表为准(可动态维护)。
|
||||
"""
|
||||
try:
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT params_value FROM params WHERE params_name=${n}$ LIMIT 1",
|
||||
{"n": name})
|
||||
if recs:
|
||||
return getattr(recs[0], "params_value", "") or default
|
||||
except Exception:
|
||||
pass
|
||||
return default
|
||||
|
||||
|
||||
# ── 默认工具定义(在 pipeline-core 未加载时使用)──
|
||||
|
||||
_BUILTIN_ASK_USER_SCHEMA = {
|
||||
@ -688,7 +671,8 @@ class AgentExecutor:
|
||||
# 项目专属工作空间目录(workspace_base 从 appbase params 表读,可动态配置)
|
||||
# 不设置 workspace_dir 会导致 _resolve_workspace 回退到 ~/pipeline_ws/default,
|
||||
# 所有项目共用同一目录,设计文档/代码互相覆盖。
|
||||
workspace_base = await _get_param(sor, "workspace_base", "/d/pipeline/workspaces")
|
||||
from .workspace import get_workspace_base
|
||||
workspace_base = await get_workspace_base(sor)
|
||||
workspace_dir = os.path.join(workspace_base, str(org_id), name)
|
||||
os.makedirs(workspace_dir, exist_ok=True)
|
||||
|
||||
|
||||
@ -500,6 +500,10 @@ def load_pipeline_service():
|
||||
# Load built-in interactive step types
|
||||
load_builtin_types()
|
||||
|
||||
# Register workspace file-management functions (shared by workspace_*.dspy)
|
||||
from .workspace import load_workspace
|
||||
load_workspace()
|
||||
|
||||
# Register intent classifier + LLM bridge (shared across all pipelines)
|
||||
from .intent_classifier import intent_classify
|
||||
from .llm_bridge import llm_call
|
||||
|
||||
240
pipeline_service/workspace.py
Normal file
240
pipeline_service/workspace.py
Normal file
@ -0,0 +1,240 @@
|
||||
"""
|
||||
pipeline_service/workspace.py - 工作空间文件管理
|
||||
|
||||
提供项目工作空间的文件浏览、读取、保存功能。
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
WORKSPACE_BASE = '/d/pipeline/workspaces'
|
||||
|
||||
# 可编辑的文本文件扩展名
|
||||
TEXT_EXTENSIONS = {
|
||||
'.md', '.py', '.js', '.html', '.css', '.json', '.txt',
|
||||
'.xml', '.yaml', '.yml', '.toml', '.cfg', '.sh', '.sql',
|
||||
'.dspy', '.ui', '.java', '.go', '.rs', '.ts', '.tsx', '.vue',
|
||||
}
|
||||
|
||||
# 媒体文件扩展名
|
||||
MEDIA_EXTENSIONS = {
|
||||
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.mp4', '.webm', '.mp3', '.wav',
|
||||
}
|
||||
|
||||
def _get_file_icon(ext):
|
||||
"""根据扩展名返回图标"""
|
||||
ext = ext.lower()
|
||||
if ext in TEXT_EXTENSIONS:
|
||||
return "📄"
|
||||
if ext in MEDIA_EXTENSIONS:
|
||||
return "🎬"
|
||||
return "📁"
|
||||
|
||||
|
||||
async def get_workspace_base(sor):
|
||||
"""读 workspace_base 参数(appbase params 表,表不存在时兜底 WORKSPACE_BASE)。"""
|
||||
workspace_base = WORKSPACE_BASE
|
||||
try:
|
||||
_wbr = await sor.sqlExe(
|
||||
"SELECT params_value FROM params WHERE params_name='workspace_base' LIMIT 1", {})
|
||||
if _wbr and getattr(_wbr[0], 'params_value', ''):
|
||||
workspace_base = getattr(_wbr[0], 'params_value', '')
|
||||
except Exception:
|
||||
pass
|
||||
return workspace_base
|
||||
|
||||
|
||||
async def get_workspace_dir(sor, uid):
|
||||
"""读当前项目的 workspace 目录(统一 workspace_*.dspy 的重复逻辑)。
|
||||
|
||||
返回 (ws_dir, workspace_base)。ws_dir 优先用 sd_projects.workspace_dir(绝对路径),
|
||||
否则 base/org/name;无当前项目时 ws_dir 返回空字符串。
|
||||
"""
|
||||
workspace_base = await get_workspace_base(sor)
|
||||
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT current_project_id FROM pipeline_agent_settings WHERE user_id=${u}$",
|
||||
{"u": uid})
|
||||
pid = getattr(recs[0], 'current_project_id', '') if recs else ''
|
||||
if not pid:
|
||||
return '', workspace_base
|
||||
|
||||
proj = await sor.sqlExe(
|
||||
"SELECT name, org_id, workspace_dir FROM sd_projects WHERE id=${p}$",
|
||||
{"p": pid})
|
||||
if not proj:
|
||||
return '', workspace_base
|
||||
|
||||
pname = getattr(proj[0], 'name', '')
|
||||
org_id = getattr(proj[0], 'org_id', '0') or '0'
|
||||
ws = getattr(proj[0], 'workspace_dir', '') or ''
|
||||
if ws.startswith('/'):
|
||||
return ws, workspace_base
|
||||
return workspace_base + '/' + str(org_id) + '/' + pname, workspace_base
|
||||
|
||||
|
||||
async def get_workspace_path(sor, user_id):
|
||||
"""获取用户当前项目的工作空间路径(无项目时返回 None)。"""
|
||||
ws, _ = await get_workspace_dir(sor, user_id)
|
||||
return ws or None
|
||||
|
||||
|
||||
def build_tree_items(ws_dir):
|
||||
"""递归扫描目录,返回 Tree widget 需要的 {id, parentid, label} 列表"""
|
||||
if not ws_dir or not os.path.isdir(ws_dir):
|
||||
return [{"id": "__root__", "parentid": "", "label": "📁 工作空间(不可用)"}]
|
||||
|
||||
root_name = os.path.basename(ws_dir)
|
||||
items = [{"id": "__root__", "parentid": "", "label": "📁 " + root_name}]
|
||||
|
||||
def scan(path, parent_id):
|
||||
try:
|
||||
entries = sorted(os.listdir(path))
|
||||
except Exception:
|
||||
return
|
||||
for name in entries:
|
||||
full = os.path.join(path, name)
|
||||
if name.startswith('.') or name == '__pycache__':
|
||||
continue
|
||||
if os.path.isdir(full):
|
||||
items.append({"id": full, "parentid": parent_id, "label": "📁 " + name})
|
||||
scan(full, full)
|
||||
|
||||
scan(ws_dir, "__root__")
|
||||
return items
|
||||
|
||||
|
||||
def build_file_widgets(folder_path):
|
||||
"""返回指定目录下的文件列表(Bricks widget 定义)"""
|
||||
if not folder_path or not os.path.isdir(folder_path):
|
||||
return {"widgettype": "Text", "options": {"text": "目录不可用", "cfontsize": 0.9}}
|
||||
|
||||
files = []
|
||||
try:
|
||||
entries = sorted(os.listdir(folder_path))
|
||||
except Exception as e:
|
||||
return {"widgettype": "Text", "options": {"text": f"读取错误: {e}", "cfontsize": 0.9, "color": "#f44336"}}
|
||||
|
||||
for name in entries:
|
||||
full = os.path.join(folder_path, name)
|
||||
if name.startswith('.'):
|
||||
continue
|
||||
if not os.path.isfile(full):
|
||||
continue
|
||||
|
||||
size = os.path.getsize(full)
|
||||
ext = os.path.splitext(name)[1].lower()
|
||||
icon = _get_file_icon(ext)
|
||||
is_text = ext in TEXT_EXTENSIONS
|
||||
kb_size = f"{size / 1024:.1f}KB"
|
||||
|
||||
# 为安全起见,在 widget JSON 中不嵌入完整文件路径
|
||||
# 文件路径通过 row_id 传递(base64 编码)
|
||||
import base64
|
||||
path_b64 = base64.b64encode(full.encode()).decode()
|
||||
|
||||
if is_text:
|
||||
row = {
|
||||
"widgettype": "HBox",
|
||||
"options": {
|
||||
"padding": "6px 10px", "alignItems": "center", "gap": "8px",
|
||||
"borderRadius": "4px", "style": {"cursor": "pointer"},
|
||||
},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"text": icon, "cfontsize": 1.2}},
|
||||
{"widgettype": "Text", "options": {"text": name, "cfontsize": 0.9, "color": "#1e293b", "css": "filler"}},
|
||||
{"widgettype": "Text", "options": {"text": kb_size, "cfontsize": 0.75, "color": "#94a3b8"}},
|
||||
],
|
||||
"binds": [{
|
||||
"wid": "self", "event": "click", "actiontype": "script", "target": "self",
|
||||
"script": _build_open_file_script(path_b64, name)
|
||||
}],
|
||||
}
|
||||
else:
|
||||
row = {
|
||||
"widgettype": "HBox",
|
||||
"options": {
|
||||
"padding": "6px 10px", "alignItems": "center", "gap": "8px",
|
||||
"borderRadius": "4px",
|
||||
},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"text": icon, "cfontsize": 1.2}},
|
||||
{"widgettype": "Text", "options": {"text": name, "cfontsize": 0.9, "color": "#94a3b8", "css": "filler"}},
|
||||
{"widgettype": "Text", "options": {"text": kb_size, "cfontsize": 0.75, "color": "#94a3b8"}},
|
||||
],
|
||||
}
|
||||
files.append(row)
|
||||
|
||||
if not files:
|
||||
return {"widgettype": "Text", "options": {"text": "(空目录)", "cfontsize": 0.85, "color": "#94a3b8"}}
|
||||
|
||||
return {
|
||||
"widgettype": "VScrollPanel",
|
||||
"options": {"css": "filler", "padding": "8px", "gap": "4px"},
|
||||
"subwidgets": files,
|
||||
}
|
||||
|
||||
|
||||
def _build_open_file_script(path_b64, name):
|
||||
"""生成文件打开脚本(Wterm 编辑器 + 保存按钮)"""
|
||||
return (
|
||||
"var p=atob('" + path_b64 + "');"
|
||||
"var pw=new bricks.PopupWindow({title:'" + name.replace("'", "\\'") + "',cwidth:70,cheight:32,auto_open:true});"
|
||||
"var wt=new bricks.Wterm({css:'filler',padding:'0'});pw.content_w.add_widget(wt);"
|
||||
"var sb=new bricks.Button({label:'保存'});pw.content_w.add_widget(sb);"
|
||||
"fetch('/pipeline-sdlc/api/workspace_file.dspy?action=read&path='+encodeURIComponent(p))"
|
||||
".then(function(r){return r.json()}).then(function(d){"
|
||||
"if(d.error){wt.write('Error: '+d.error);return;}"
|
||||
"wt.write(d.content||'');"
|
||||
"sb.bind('click',function(){"
|
||||
"var c=wt.get_value();"
|
||||
"fetch('/pipeline-sdlc/api/workspace_file.dspy',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},"
|
||||
"body:'action=save&path='+encodeURIComponent(p)+'&content='+encodeURIComponent(c)})"
|
||||
".then(function(r){return r.json()}).then(function(d){"
|
||||
"if(d.success){sb.set_text('已保存');setTimeout(function(){sb.set_text('保存')},2000);}"
|
||||
"});"
|
||||
"});"
|
||||
"});"
|
||||
)
|
||||
|
||||
|
||||
def read_file_content(filepath):
|
||||
"""读取文件内容"""
|
||||
if not filepath or not os.path.isfile(filepath):
|
||||
return None, "文件不存在"
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
content = f.read(50000)
|
||||
return content, None
|
||||
except Exception as e:
|
||||
return None, str(e)
|
||||
|
||||
|
||||
def save_file_content(filepath, content):
|
||||
"""保存文件内容"""
|
||||
if not filepath:
|
||||
return False, "路径为空"
|
||||
try:
|
||||
dirpath = os.path.dirname(filepath)
|
||||
if dirpath:
|
||||
os.makedirs(dirpath, exist_ok=True)
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
return True, None
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
|
||||
def load_workspace():
|
||||
"""注册工作空间函数到 ServerEnv,供 DSPY 直接调用。"""
|
||||
from ahserver.serverenv import ServerEnv
|
||||
g = ServerEnv()
|
||||
g.get_workspace_base = get_workspace_base
|
||||
g.get_workspace_dir = get_workspace_dir
|
||||
g.get_workspace_path = get_workspace_path
|
||||
g.build_tree_items = build_tree_items
|
||||
g.build_file_widgets = build_file_widgets
|
||||
g.read_file_content = read_file_content
|
||||
g.save_file_content = save_file_content
|
||||
Loading…
x
Reference in New Issue
Block a user