diff --git a/wwwroot/api/work_env.dspy b/wwwroot/api/work_env.dspy
index e032eb7..138a536 100644
--- a/wwwroot/api/work_env.dspy
+++ b/wwwroot/api/work_env.dspy
@@ -107,6 +107,46 @@ elif action == 'ensure_remote_bwrap':
except Exception as e:
return json.dumps({'ok': False, 'error': str(e)}, ensure_ascii=False)
+elif action == 'init_remote_env':
+ # 一键初始化远程环境:bwrap + 技能同步 + 依赖安装(可重跑,幂等)。
+ # 从表单取临时配置(未保存时也可测试);保存后从 DB 取当前配置均可。
+ from pipeline_service.remote_env_init import init_remote_env as _init_remote_env
+ async with get_sor_context(request._run_ns, 'pipeline') as _sor:
+ _adm = await _sor.sqlExe(
+ "SELECT 1 FROM userrole WHERE userid=${u}$ AND roleid='owner.superuser' LIMIT 1",
+ {'u': user_id})
+ if not _adm:
+ return json.dumps({'ok': False, 'error': '只有平台管理员能初始化远程环境'}, ensure_ascii=False)
+ remote_config = {
+ 'remote_host': (params_kw or {}).get('remote_host', ''),
+ 'remote_port': int((params_kw or {}).get('remote_port', 22) or 22),
+ 'remote_user': (params_kw or {}).get('remote_user', ''),
+ 'remote_key_path': (params_kw or {}).get('remote_key_path', ''),
+ 'remote_dir': (params_kw or {}).get('remote_dir', ''),
+ }
+ # 未传 key 时用机构级 key(与 set 一致)
+ if not remote_config['remote_key_path'] and org_id:
+ from pipeline_service.work_env import ensure_org_key
+ remote_config['remote_key_path'] = ensure_org_key(org_id).get('key_path', '')
+ try:
+ async with get_sor_context(request._run_ns, 'pipeline') as sor:
+ r = await _init_remote_env(remote_config, org_id=org_id, sor=sor)
+ await audit_log(sor, user_id, username, 'remote_env_init',
+ target=str(remote_config.get('remote_host', '')),
+ detail='技能同步+依赖安装',
+ result='ok' if r.get('ok') else 'fail', client_ip=client_ip)
+ return json.dumps(r, ensure_ascii=False)
+ except Exception as e:
+ return json.dumps({'ok': False, 'error': str(e)}, ensure_ascii=False)
+
+elif action == 'init_remote_env_status':
+ # 轮询依赖安装后台任务结果(init_remote_env 返回的 deps_task)。
+ from pipeline_service.remote_env_init import get_deps_install_status
+ _task = (params_kw or {}).get('task', '')
+ if not _task:
+ return json.dumps({'ok': False, 'error': '缺少 task 参数'}, ensure_ascii=False)
+ return json.dumps(get_deps_install_status(_task), ensure_ascii=False)
+
elif action == 'get_org_pubkey':
# 获取机构 public key(供 admin 复制到远程主机 authorized_keys)。仅超管。
try:
diff --git a/wwwroot/sd_org_remote/index.ui b/wwwroot/sd_org_remote/index.ui
index f4b5170..5c5a4e2 100644
--- a/wwwroot/sd_org_remote/index.ui
+++ b/wwwroot/sd_org_remote/index.ui
@@ -1,6 +1,11 @@
{
"widgettype": "VBox",
- "options": {"width": "100%", "height": "100%", "padding": "24px", "gap": "16px"},
+ "options": {
+ "width": "100%",
+ "height": "100%",
+ "padding": "24px",
+ "gap": "16px"
+ },
"subwidgets": [
{
"widgettype": "Html",
@@ -12,7 +17,9 @@
},
{
"widgettype": "Title3",
- "options": {"text": "机构远程空间配置"}
+ "options": {
+ "text": "机构远程空间配置"
+ }
},
{
"widgettype": "Text",
@@ -24,7 +31,13 @@
},
{
"widgettype": "VBox",
- "options": {"bgcolor": "#fff", "border": "1px solid #e2e8f0", "borderRadius": "12px", "padding": "20px", "gap": "12px"},
+ "options": {
+ "bgcolor": "#fff",
+ "border": "1px solid #e2e8f0",
+ "borderRadius": "12px",
+ "padding": "20px",
+ "gap": "12px"
+ },
"subwidgets": [
{
"widgettype": "Form",
@@ -39,8 +52,14 @@
"label": "工作模式",
"cwidth": 12,
"data": [
- {"value": "local", "text": "本地(bwrap 沙箱)"},
- {"value": "remote", "text": "远程(SSH + bwrap 沙箱)"}
+ {
+ "value": "local",
+ "text": "本地(bwrap 沙箱)"
+ },
+ {
+ "value": "remote",
+ "text": "远程(SSH + bwrap 沙箱)"
+ }
],
"valueField": "value",
"textField": "text"
@@ -77,41 +96,87 @@
},
{
"widgettype": "HBox",
- "options": {"gap": "12px"},
+ "options": {
+ "gap": "12px"
+ },
"subwidgets": [
{
"widgettype": "Button",
"id": "save_btn",
- "options": {"label": "保存配置"},
- "binds": [{
- "wid": "self", "event": "click", "actiontype": "script", "target": "self",
- "script": "var f=bricks.getWidgetById('remote_form',bricks.app);var v=f?f._getValue():{};var body=new URLSearchParams();body.append('action','set');body.append('owner_type','org');body.append('mode',v.mode||'local');body.append('remote_host',v.remote_host||'');body.append('remote_port',v.remote_port||'22');body.append('remote_user',v.remote_user||'');body.append('remote_dir',v.remote_dir||'');fetch('/pipeline-sdlc/api/work_env.dspy',{method:'POST',body:body}).then(function(r){return r.json()}).then(function(d){if(d.ok){bricks.show_message({title:'成功',message:'远程空间配置已保存'});}else{bricks.show_error({title:'失败',message:d.error||'未知错误'});}});"
- }]
+ "options": {
+ "label": "保存配置"
+ },
+ "binds": [
+ {
+ "wid": "self",
+ "event": "click",
+ "actiontype": "script",
+ "target": "self",
+ "script": "var f=bricks.getWidgetById('remote_form',bricks.app);var v=f?f._getValue():{};var body=new URLSearchParams();body.append('action','set');body.append('owner_type','org');body.append('mode',v.mode||'local');body.append('remote_host',v.remote_host||'');body.append('remote_port',v.remote_port||'22');body.append('remote_user',v.remote_user||'');body.append('remote_dir',v.remote_dir||'');fetch('/pipeline-sdlc/api/work_env.dspy',{method:'POST',body:body}).then(function(r){return r.json()}).then(function(d){if(d.ok){bricks.show_message({title:'成功',message:'远程空间配置已保存'});}else{bricks.show_error({title:'失败',message:d.error||'未知错误'});}});"
+ }
+ ]
},
{
"widgettype": "Button",
"id": "test_btn",
- "options": {"label": "测试远程连接"},
- "binds": [{
- "wid": "self", "event": "click", "actiontype": "script", "target": "self",
- "script": "var f=bricks.getWidgetById('remote_form',bricks.app);var v=f?f._getValue():{};var body=new URLSearchParams();body.append('action','ensure_remote_bwrap');body.append('remote_host',v.remote_host||'');body.append('remote_port',v.remote_port||'22');body.append('remote_user',v.remote_user||'');fetch('/pipeline-sdlc/api/work_env.dspy',{method:'POST',body:body}).then(function(r){return r.json()}).then(function(d){if(d.ok){bricks.show_message({title:'连接成功',message:'远程 bwrap 位置: '+(d.location||'unknown')});}else{bricks.show_error({title:'连接失败',message:d.error||'未知错误'});}});"
- }]
+ "options": {
+ "label": "测试远程连接"
+ },
+ "binds": [
+ {
+ "wid": "self",
+ "event": "click",
+ "actiontype": "script",
+ "target": "self",
+ "script": "var f=bricks.getWidgetById('remote_form',bricks.app);var v=f?f._getValue():{};var body=new URLSearchParams();body.append('action','ensure_remote_bwrap');body.append('remote_host',v.remote_host||'');body.append('remote_port',v.remote_port||'22');body.append('remote_user',v.remote_user||'');fetch('/pipeline-sdlc/api/work_env.dspy',{method:'POST',body:body}).then(function(r){return r.json()}).then(function(d){if(d.ok){bricks.show_message({title:'连接成功',message:'远程 bwrap 位置: '+(d.location||'unknown')});}else{bricks.show_error({title:'连接失败',message:d.error||'未知错误'});}});"
+ }
+ ]
},
{
"widgettype": "Button",
"id": "pubkey_btn",
- "options": {"label": "获取机构公钥"},
- "binds": [{
- "wid": "self", "event": "click", "actiontype": "script", "target": "self",
- "script": "fetch('/pipeline-sdlc/api/work_env.dspy?action=get_org_pubkey').then(function(r){return r.json()}).then(function(d){if(d.ok){var t=bricks.getWidgetById('pubkey_text',bricks.app);if(t)t.set_text('机构公钥(复制到远程主机 ~/.ssh/authorized_keys):\\n'+d.pubkey);}else{bricks.show_error({title:'失败',message:d.error||'未知错误'});}});"
- }]
+ "options": {
+ "label": "获取机构公钥"
+ },
+ "binds": [
+ {
+ "wid": "self",
+ "event": "click",
+ "actiontype": "script",
+ "target": "self",
+ "script": "fetch('/pipeline-sdlc/api/work_env.dspy?action=get_org_pubkey').then(function(r){return r.json()}).then(function(d){if(d.ok){var t=bricks.getWidgetById('pubkey_text',bricks.app);if(t)t.set_text('机构公钥(复制到远程主机 ~/.ssh/authorized_keys):\\\\n'+d.pubkey);}else{bricks.show_error({title:'失败',message:d.error||'未知错误'});}});"
+ }
+ ]
+ },
+ {
+ "widgettype": "Button",
+ "id": "init_btn",
+ "options": {
+ "label": "初始化远程环境",
+ "bgcolor": "#0ea5e9",
+ "color": "#fff"
+ },
+ "binds": [
+ {
+ "wid": "self",
+ "event": "click",
+ "actiontype": "script",
+ "target": "self",
+ "script": "var f=bricks.getWidgetById('remote_form',bricks.app);var v=f?f._getValue():{};if(!v.remote_host||!v.remote_dir){bricks.show_error({title:'缺少配置',message:'请先填写远程主机与远程目录并保存'});return;}var btn=this;btn.disabled=true;var body=new URLSearchParams();body.append('action','init_remote_env');body.append('remote_host',v.remote_host);body.append('remote_port',v.remote_port||'22');body.append('remote_user',v.remote_user||'');body.append('remote_dir',v.remote_dir);fetch('/pipeline-sdlc/api/work_env.dspy',{method:'POST',body:body}).then(function(r){return r.json()}).then(function(d){if(!d.ok){btn.disabled=false;bricks.show_error({title:'初始化失败',message:d.error||'未知错误'});return;}var st=d.steps||{};var sk=st.skills||{};var dp=st.deps_found||{};var msg='bwrap: '+((st.bwrap&&st.bwrap.ok)?'✓':'✓')+'
技能同步: '+(sk.synced?sk.synced.join(', '):'✗')+'
待装依赖: '+(dp.specs?dp.specs.length:0)+' 个包(重量级已跳过 '+(dp.heavy_skipped?dp.heavy_skipped.length:0)+' 个)
依赖安装进行中…';bricks.show_message({title:'远程环境初始化',message:msg});var task=d.deps_task;var tries=0;function poll(){if(tries++>60){btn.disabled=false;bricks.show_error({title:'依赖安装超时',message:'超过10分钟未返回结果,请稍后重新点击初始化查询'});return;}fetch('/pipeline-sdlc/api/work_env.dspy?action=init_remote_env_status&task='+encodeURIComponent(task)).then(function(r){return r.json()}).then(function(s){if(s.status==='running'){setTimeout(poll,10000);return;}btn.disabled=false;if(s.ok){bricks.show_message({title:'远程环境初始化完成',message:'依赖安装成功: '+(s.installed?s.installed.length:0)+' 个包
venv: '+(s.venv||'')});}else{bricks.show_error({title:'依赖安装失败',message:(s.error||'')+'
失败包: '+((s.failed||[]).join('; ')||'无')+((s.tail)?'
日志末尾:
'+s.tail.slice(-300):'')});}}).catch(function(e){btn.disabled=false;bricks.show_error({title:'查询失败',message:String(e)});});}setTimeout(poll,8000);}).catch(function(e){btn.disabled=false;bricks.show_error({title:'请求失败',message:String(e)});});"
+ }
+ ]
}
]
},
{
"widgettype": "Text",
"id": "pubkey_text",
- "options": {"text": "", "whiteSpace": "pre-wrap", "cfontsize": 0.85, "color": "#334155"}
+ "options": {
+ "text": "",
+ "whiteSpace": "pre-wrap",
+ "cfontsize": 0.85,
+ "color": "#334155"
+ }
}
]
}