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")
|
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 未加载时使用)──
|
# ── 默认工具定义(在 pipeline-core 未加载时使用)──
|
||||||
|
|
||||||
_BUILTIN_ASK_USER_SCHEMA = {
|
_BUILTIN_ASK_USER_SCHEMA = {
|
||||||
@ -688,7 +671,8 @@ class AgentExecutor:
|
|||||||
# 项目专属工作空间目录(workspace_base 从 appbase params 表读,可动态配置)
|
# 项目专属工作空间目录(workspace_base 从 appbase params 表读,可动态配置)
|
||||||
# 不设置 workspace_dir 会导致 _resolve_workspace 回退到 ~/pipeline_ws/default,
|
# 不设置 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)
|
workspace_dir = os.path.join(workspace_base, str(org_id), name)
|
||||||
os.makedirs(workspace_dir, exist_ok=True)
|
os.makedirs(workspace_dir, exist_ok=True)
|
||||||
|
|
||||||
|
|||||||
@ -500,6 +500,10 @@ def load_pipeline_service():
|
|||||||
# Load built-in interactive step types
|
# Load built-in interactive step types
|
||||||
load_builtin_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)
|
# Register intent classifier + LLM bridge (shared across all pipelines)
|
||||||
from .intent_classifier import intent_classify
|
from .intent_classifier import intent_classify
|
||||||
from .llm_bridge import llm_call
|
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