This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
"""List or remove stopped deployment rollback containers; never touches volumes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import shlex
|
||||
from pathlib import Path
|
||||
|
||||
import paramiko
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
NAME = re.compile(r"^mini-openclaw-rollback-\d{14}$")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--apply", action="store_true")
|
||||
args = parser.parse_args()
|
||||
values = {}
|
||||
for raw in (ROOT / "server-deploy.conf").read_text(encoding="utf-8-sig").splitlines():
|
||||
line = raw.strip()
|
||||
if line and not line.startswith("#") and "=" in line:
|
||||
key, value = line.split("=", 1)
|
||||
values[key.strip()] = value.strip()
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
options = {"hostname": values["SERVER_HOST"], "port": int(values.get("SERVER_PORT", "22")), "username": values["SERVER_USER"], "timeout": 15}
|
||||
options.update({"key_filename": values["SERVER_KEYFILE"]} if values.get("SERVER_KEYFILE") else {"password": values["SERVER_PASSWORD"]})
|
||||
client.connect(**options)
|
||||
_, stdout, _ = client.exec_command("docker ps -a --format '{{.Names}}|{{.State}}'", timeout=30)
|
||||
lines = stdout.read().decode("utf-8", errors="replace").splitlines()
|
||||
status = stdout.channel.recv_exit_status()
|
||||
if status:
|
||||
client.close()
|
||||
raise RuntimeError("Unable to list containers")
|
||||
targets = []
|
||||
for line in lines:
|
||||
name, _, state = line.partition("|")
|
||||
if NAME.fullmatch(name) and state != "running":
|
||||
targets.append(name)
|
||||
print("rollback_targets=" + (",".join(targets) if targets else "none"))
|
||||
if args.apply and targets:
|
||||
command = "docker rm " + " ".join(shlex.quote(name) for name in targets)
|
||||
_, stdout, stderr = client.exec_command(command, timeout=60)
|
||||
output = stdout.read().decode("utf-8", errors="replace")
|
||||
error = stderr.read().decode("utf-8", errors="replace")
|
||||
code = stdout.channel.recv_exit_status()
|
||||
if code:
|
||||
client.close()
|
||||
raise RuntimeError(error.strip() or "Unable to remove rollback containers")
|
||||
print("removed=" + ",".join(output.splitlines()))
|
||||
client.close()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,237 @@
|
||||
"""Deploy the official DeepSeek configuration without exposing credentials."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import posixpath
|
||||
import shlex
|
||||
import stat
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import paramiko
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def load_config(path: Path) -> dict[str, str]:
|
||||
values: dict[str, str] = {}
|
||||
for raw in path.read_text(encoding="utf-8-sig").splitlines():
|
||||
line = raw.strip()
|
||||
if line and not line.startswith("#") and "=" in line:
|
||||
key, value = line.split("=", 1)
|
||||
values[key.strip()] = value.strip()
|
||||
return values
|
||||
|
||||
|
||||
def connect(config: dict[str, str]) -> paramiko.SSHClient:
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
options = {
|
||||
"hostname": config["SERVER_HOST"],
|
||||
"port": int(config.get("SERVER_PORT", "22")),
|
||||
"username": config["SERVER_USER"],
|
||||
"timeout": 20,
|
||||
"banner_timeout": 20,
|
||||
"auth_timeout": 20,
|
||||
}
|
||||
if config.get("SERVER_KEYFILE"):
|
||||
options["key_filename"] = config["SERVER_KEYFILE"]
|
||||
else:
|
||||
options["password"] = config["SERVER_PASSWORD"]
|
||||
client.connect(**options)
|
||||
return client
|
||||
|
||||
|
||||
def run(client: paramiko.SSHClient, command: str, timeout: int = 300) -> str:
|
||||
_, stdout, stderr = client.exec_command(command, timeout=timeout)
|
||||
output = stdout.read().decode("utf-8", errors="replace")
|
||||
error = stderr.read().decode("utf-8", errors="replace")
|
||||
code = stdout.channel.recv_exit_status()
|
||||
if code:
|
||||
raise RuntimeError((error or output or f"remote exit {code}").strip())
|
||||
return output.strip()
|
||||
|
||||
|
||||
def put_text(sftp: paramiko.SFTPClient, path: str, content: str, mode: int = 0o644) -> None:
|
||||
temporary = path + ".tmp"
|
||||
with sftp.file(temporary, "w") as handle:
|
||||
handle.write(content)
|
||||
handle.flush()
|
||||
sftp.chmod(temporary, mode)
|
||||
sftp.posix_rename(temporary, path)
|
||||
|
||||
|
||||
def update_env(content: str, updates: dict[str, str]) -> str:
|
||||
remaining = dict(updates)
|
||||
result: list[str] = []
|
||||
for line in content.splitlines():
|
||||
if "=" in line and not line.lstrip().startswith("#"):
|
||||
key = line.split("=", 1)[0].strip()
|
||||
if key in remaining:
|
||||
result.append(f"{key}={remaining.pop(key)}")
|
||||
continue
|
||||
result.append(line)
|
||||
result.extend(f"{key}={value}" for key, value in remaining.items())
|
||||
return "\n".join(result) + "\n"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
config = load_config(ROOT / "server-deploy.conf")
|
||||
secret = config.get("DEEPSEEK_API_KEY", "")
|
||||
if not secret.startswith("sk-") or len(secret) < 20:
|
||||
raise RuntimeError("DEEPSEEK_API_KEY is missing from server-deploy.conf")
|
||||
|
||||
base = config["SERVER_DEPLOY_PATH"].rstrip("/")
|
||||
port = config.get("PORT", "3200")
|
||||
stamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
||||
client = connect(config)
|
||||
sftp = client.open_sftp()
|
||||
try:
|
||||
current_image = run(client, "docker inspect -f '{{.Config.Image}}' mini-openclaw")
|
||||
if not current_image.startswith("mini-openclaw:"):
|
||||
raise RuntimeError("Unexpected current image; deployment was not changed")
|
||||
new_image = f"mini-openclaw:deepseek-official-{stamp}"
|
||||
rollback_container = f"mini-openclaw-rollback-{stamp}"
|
||||
|
||||
local_files = (
|
||||
"backend/services/assistant_model.py",
|
||||
"backend/services/project_assistant.py",
|
||||
"backend/api/risk_agent_config.py",
|
||||
"backend/test_assistant_components.py",
|
||||
"backend/test_project_assistant.py",
|
||||
"backend/test_risk_agent_config.py",
|
||||
"deploy/docker-compose.aliyun.yml",
|
||||
"deploy/aliyun.env.example",
|
||||
"src/components/xingbang/AgentNav.tsx",
|
||||
"src/components/xingbang/BusinessAssistant.tsx",
|
||||
"src/app/resetdata/layout.tsx",
|
||||
"src/app/resetdata/page.tsx",
|
||||
"src/app/resetdata/resetdata.css",
|
||||
"scripts/test_resetdata_ui.cjs",
|
||||
)
|
||||
for relative in local_files:
|
||||
remote = posixpath.join(base, relative.replace("\\", "/"))
|
||||
try:
|
||||
sftp.stat(remote)
|
||||
run(client, f"cp -p {shlex.quote(remote)} {shlex.quote(remote + '.pre-deepseek-' + stamp)}")
|
||||
except FileNotFoundError:
|
||||
run(client, f"mkdir -p {shlex.quote(posixpath.dirname(remote))}")
|
||||
sftp.put(str(ROOT / relative), remote)
|
||||
|
||||
env_path = posixpath.join(base, ".runtime.env")
|
||||
with sftp.file(env_path, "r") as handle:
|
||||
runtime_env = handle.read().decode("utf-8")
|
||||
original_runtime_env = runtime_env
|
||||
run(client, f"cp -p {shlex.quote(env_path)} {shlex.quote(env_path + '.pre-deepseek-' + stamp)}")
|
||||
runtime_env = update_env(
|
||||
runtime_env,
|
||||
{
|
||||
"DEEPSEEK_API_KEY": secret,
|
||||
"DEEPSEEK_BASE_URL": "https://api.deepseek.com",
|
||||
"DEEPSEEK_MODEL": "deepseek-chat",
|
||||
"XINGBANG_LOCAL_MODEL_KEY": secret,
|
||||
"XINGBANG_LOCAL_MODEL_URL": "https://api.deepseek.com",
|
||||
"XINGBANG_LOCAL_MODEL_NAME": "deepseek-chat",
|
||||
},
|
||||
)
|
||||
put_text(sftp, env_path, runtime_env, 0o600)
|
||||
|
||||
probe = r'''
|
||||
import os
|
||||
import sys
|
||||
import httpx
|
||||
response = httpx.post(
|
||||
"https://api.deepseek.com/chat/completions",
|
||||
headers={"Authorization": "Bearer " + os.environ["DEEPSEEK_API_KEY"]},
|
||||
json={"model": "deepseek-chat", "messages": [{"role": "user", "content": "Reply with OK."}], "max_tokens": 2, "temperature": 0},
|
||||
timeout=30,
|
||||
trust_env=False,
|
||||
)
|
||||
print("deepseek_official_status=" + str(response.status_code))
|
||||
sys.exit(0 if response.status_code == 200 else 1)
|
||||
'''
|
||||
encoded_probe = probe.encode("utf-8").hex()
|
||||
probe_command = (
|
||||
f"cd {shlex.quote(base)} && docker run --rm --env-file .runtime.env "
|
||||
f"--entrypoint python {shlex.quote(current_image)} -c "
|
||||
+ shlex.quote("exec(bytes.fromhex(" + repr(encoded_probe) + ").decode('utf-8'))")
|
||||
)
|
||||
print(run(client, probe_command, timeout=90))
|
||||
|
||||
dockerfile = (
|
||||
"FROM node:18-bookworm-slim AS frontend-builder\n"
|
||||
"WORKDIR /app/frontend\n"
|
||||
"COPY package*.json ./\n"
|
||||
"RUN npm ci\n"
|
||||
"COPY src ./src\n"
|
||||
"COPY next-env.d.ts next.config.mjs postcss.config.mjs tailwind.config.ts tsconfig.json ./\n"
|
||||
"RUN mkdir -p public && npm run build\n"
|
||||
f"FROM {current_image}\n"
|
||||
"WORKDIR /app\n"
|
||||
"COPY backend/services/assistant_model.py /app/backend/services/assistant_model.py\n"
|
||||
"COPY backend/services/project_assistant.py /app/backend/services/project_assistant.py\n"
|
||||
"COPY backend/api/risk_agent_config.py /app/backend/api/risk_agent_config.py\n"
|
||||
"RUN rm -rf /app/frontend/.next /app/frontend/public\n"
|
||||
"COPY --from=frontend-builder /app/frontend/.next /app/frontend/.next\n"
|
||||
"COPY --from=frontend-builder /app/frontend/public /app/frontend/public\n"
|
||||
)
|
||||
dockerfile_path = posixpath.join(base, "Dockerfile.deepseek-official")
|
||||
put_text(sftp, dockerfile_path, dockerfile)
|
||||
print(run(client, f"cd {shlex.quote(base)} && docker build -f Dockerfile.deepseek-official -t {shlex.quote(new_image)} .", timeout=600))
|
||||
|
||||
state = posixpath.join(base, "state")
|
||||
mounts = (
|
||||
("workspace", "/app/backend/workspace"),
|
||||
("config.json", "/app/backend/config.json"),
|
||||
("tokenizer-cache", "/tmp/data-gym-cache"),
|
||||
("xingbang_data", "/app/backend/xingbang_data"),
|
||||
("logs", "/app/backend/logs"),
|
||||
("sessions", "/app/backend/sessions"),
|
||||
("raw_logs", "/app/backend/raw_logs"),
|
||||
)
|
||||
mount_args = " ".join(
|
||||
"-v " + shlex.quote(posixpath.join(state, source) + ":" + target)
|
||||
for source, target in mounts
|
||||
)
|
||||
launch = (
|
||||
f"cd {shlex.quote(base)} && "
|
||||
"docker stop mini-openclaw >/dev/null && "
|
||||
f"docker rename mini-openclaw {shlex.quote(rollback_container)} && "
|
||||
f"docker run -d --name mini-openclaw --restart unless-stopped --env-file .runtime.env "
|
||||
f"-p {shlex.quote(port)}:3000 {mount_args} {shlex.quote(new_image)}"
|
||||
)
|
||||
run(client, launch, timeout=120)
|
||||
|
||||
healthy = False
|
||||
last = "unknown"
|
||||
for _ in range(45):
|
||||
last = run(client, "docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' mini-openclaw")
|
||||
if last == "healthy":
|
||||
healthy = True
|
||||
break
|
||||
if last in {"unhealthy", "exited", "dead"}:
|
||||
break
|
||||
time.sleep(4)
|
||||
if not healthy:
|
||||
run(
|
||||
client,
|
||||
"docker rm -f mini-openclaw >/dev/null 2>&1 || true; "
|
||||
f"docker rename {shlex.quote(rollback_container)} mini-openclaw; "
|
||||
"docker start mini-openclaw >/dev/null",
|
||||
)
|
||||
put_text(sftp, env_path, original_runtime_env, 0o600)
|
||||
raise RuntimeError(f"New container health is {last}; previous container was restored")
|
||||
|
||||
put_text(sftp, posixpath.join(base, ".deployed-version"), new_image + "\n")
|
||||
print(json.dumps({"status": "deployed", "image": new_image, "health": "healthy", "rollback_container": rollback_container}))
|
||||
return 0
|
||||
finally:
|
||||
sftp.close()
|
||||
client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,73 @@
|
||||
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")]
|
||||
param(
|
||||
[string]$BaseUrl = "http://8.163.107.248:3200",
|
||||
[string]$AccessToken = "",
|
||||
[switch]$CheckOnly,
|
||||
[string]$Confirmation = ""
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$apiRoot = $BaseUrl.TrimEnd("/") + "/api/risk-agent/project-assistant"
|
||||
$headers = @{ Accept = "application/json" }
|
||||
if ($AccessToken) {
|
||||
$headers.Authorization = "Bearer $AccessToken"
|
||||
} else {
|
||||
$headers["X-Xingbang-Demo-Role"] = "admin"
|
||||
}
|
||||
|
||||
function Get-DemoState {
|
||||
$response = Invoke-RestMethod -Method Get -Uri "$apiRoot/projects" -Headers $headers -TimeoutSec 20
|
||||
return @($response.projects | Where-Object { $_.id -in @("DEMO-A", "DEMO-B", "DEMO-C") } | Sort-Object id)
|
||||
}
|
||||
|
||||
function Show-DemoState([string]$Title, $Projects) {
|
||||
Write-Host ""
|
||||
Write-Host $Title -ForegroundColor Cyan
|
||||
$Projects | Select-Object id, customer, stage, workflow_state, revision, data_revision, open_findings, next_action | Format-Table -AutoSize
|
||||
}
|
||||
|
||||
$before = Get-DemoState
|
||||
if ($before.Count -ne 3) {
|
||||
throw "Demo projects are incomplete: expected DEMO-A, DEMO-B and DEMO-C; received $($before.Count)."
|
||||
}
|
||||
Show-DemoState "Current Alibaba Cloud demo state" $before
|
||||
|
||||
if ($CheckOnly) {
|
||||
Write-Host "Read-only check completed. No data was changed." -ForegroundColor Green
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Warning "This restores DEMO-A, DEMO-B and DEMO-C facts, materials, runs, findings, reports and review decisions to the built-in baseline."
|
||||
Write-Warning "Formal projects and published cards, skills, rules and templates are not reset. Demo audit and timing events return to the baseline and the prior state is archived."
|
||||
if (-not $Confirmation) {
|
||||
$Confirmation = Read-Host "Type RESET-DEMO-A-B-C to continue"
|
||||
}
|
||||
if ($Confirmation -ne "RESET-DEMO-A-B-C") {
|
||||
throw "Confirmation text did not match. No reset was performed."
|
||||
}
|
||||
|
||||
$target = "$apiRoot/demo/reset"
|
||||
if (-not $PSCmdlet.ShouldProcess($target, "Reset the three demo projects")) {
|
||||
Write-Host "Cancelled. No data was changed."
|
||||
exit 0
|
||||
}
|
||||
|
||||
$result = Invoke-RestMethod -Method Post -Uri $target -Headers ($headers + @{ "Content-Type" = "application/json" }) -Body (@{ confirmation = $Confirmation } | ConvertTo-Json -Compress) -TimeoutSec 30
|
||||
if ($result.status -ne "reset" -or $result.projects -ne 3) {
|
||||
throw "The server did not return the expected reset response: $($result | ConvertTo-Json -Compress)"
|
||||
}
|
||||
|
||||
$after = Get-DemoState
|
||||
Show-DemoState "Demo state after reset" $after
|
||||
|
||||
$expected = @{
|
||||
"DEMO-A" = "report_ready"
|
||||
"DEMO-B" = "supplementing"
|
||||
"DEMO-C" = "under_review"
|
||||
}
|
||||
$unexpected = @($after | Where-Object { $expected[$_.id] -ne $_.workflow_state })
|
||||
if ($unexpected.Count) {
|
||||
throw "The reset API succeeded, but the project state does not match the baseline. Stop and investigate before the demo."
|
||||
}
|
||||
|
||||
Write-Host "Reset completed. Refresh the browser. DEMO-B should be supplementing and retain the four demo risk conditions." -ForegroundColor Green
|
||||
@@ -0,0 +1,39 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ConfigPath = (Join-Path (Split-Path -Parent $PSScriptRoot) "server-deploy.conf")
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$secret = Read-Host "DeepSeek official API Key" -AsSecureString
|
||||
$pointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secret)
|
||||
try {
|
||||
$plain = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($pointer)
|
||||
if (-not $plain.StartsWith("sk-") -or $plain.Length -lt 20) {
|
||||
throw "The value does not look like a DeepSeek API key. Nothing was changed."
|
||||
}
|
||||
$lines = if (Test-Path -LiteralPath $ConfigPath) {
|
||||
@(Get-Content -LiteralPath $ConfigPath)
|
||||
} else {
|
||||
@()
|
||||
}
|
||||
$replacement = "DEEPSEEK_API_KEY=$plain"
|
||||
$found = $false
|
||||
$updated = @($lines | ForEach-Object {
|
||||
if ($_ -match '^DEEPSEEK_API_KEY=') {
|
||||
$found = $true
|
||||
$replacement
|
||||
} else {
|
||||
$_
|
||||
}
|
||||
})
|
||||
if (-not $found) {
|
||||
$updated += $replacement
|
||||
}
|
||||
Set-Content -LiteralPath $ConfigPath -Value $updated -Encoding UTF8
|
||||
Write-Host "DeepSeek key saved to the ignored local deployment config. The key was not printed." -ForegroundColor Green
|
||||
} finally {
|
||||
if ($pointer -ne [IntPtr]::Zero) {
|
||||
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($pointer)
|
||||
}
|
||||
$plain = $null
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
"""Run a non-persistent Agent Loop test with synthetic business data."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shlex
|
||||
from pathlib import Path
|
||||
|
||||
import paramiko
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def config() -> dict[str, str]:
|
||||
result: dict[str, str] = {}
|
||||
for raw in (ROOT / "server-deploy.conf").read_text(encoding="utf-8-sig").splitlines():
|
||||
line = raw.strip()
|
||||
if line and not line.startswith("#") and "=" in line:
|
||||
key, value = line.split("=", 1)
|
||||
result[key.strip()] = value.strip()
|
||||
return result
|
||||
|
||||
|
||||
def main() -> int:
|
||||
settings = config()
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
options = {
|
||||
"hostname": settings["SERVER_HOST"],
|
||||
"port": int(settings.get("SERVER_PORT", "22")),
|
||||
"username": settings["SERVER_USER"],
|
||||
"timeout": 20,
|
||||
"banner_timeout": 20,
|
||||
"auth_timeout": 20,
|
||||
}
|
||||
if settings.get("SERVER_KEYFILE"):
|
||||
options["key_filename"] = settings["SERVER_KEYFILE"]
|
||||
else:
|
||||
options["password"] = settings["SERVER_PASSWORD"]
|
||||
client.connect(**options)
|
||||
|
||||
synthetic_test = r'''
|
||||
import json
|
||||
from services import assistant_model
|
||||
|
||||
model = assistant_model.configuration()
|
||||
skill = {
|
||||
"instructions": "对虚构测试项目执行证据约束核查,只验证工具调用链。",
|
||||
"methods": {
|
||||
"synthetic-financial-leasing/SKILL.md": "读取数据卡证据,检查负债口径、现金流是否齐备及租赁物覆盖率;不得作审批决定。"
|
||||
},
|
||||
"allowed_tools": ["read_project_snapshot", "read_checks", "read_method"],
|
||||
}
|
||||
snapshot = {
|
||||
"id": "snapshot-synthetic-safe",
|
||||
"project_id": "SAFE-SYNTHETIC-TEST",
|
||||
"values": {
|
||||
"financial.total_liabilities": 100,
|
||||
"due_diligence.liabilities": 90,
|
||||
"lease.total_value": 120,
|
||||
"facility.amount": 100,
|
||||
"cashflow.net_operating": None,
|
||||
},
|
||||
"sources": {
|
||||
"financial.total_liabilities": [{"id": "ev-safe-1", "source": "synthetic_finance", "value": 100}],
|
||||
"due_diligence.liabilities": [{"id": "ev-safe-2", "source": "synthetic_due_diligence", "value": 90}],
|
||||
"lease.total_value": [{"id": "ev-safe-3", "source": "synthetic_asset", "value": 120}],
|
||||
"facility.amount": [{"id": "ev-safe-4", "source": "synthetic_facility", "value": 100}],
|
||||
"cashflow.net_operating": [{"id": "ev-safe-5", "source": "synthetic_cashflow", "value": None}],
|
||||
},
|
||||
"documents": [],
|
||||
}
|
||||
checks = [
|
||||
{"code": "synthetic.liability.consistency", "title": "负债口径差异", "result": "issue", "evidence_ids": ["ev-safe-1", "ev-safe-2"], "fields": ["financial.total_liabilities", "due_diligence.liabilities"], "suggested_action": "核实负债差异"},
|
||||
{"code": "synthetic.cashflow.missing", "title": "现金流缺失", "result": "unknown", "evidence_ids": ["ev-safe-5"], "fields": ["cashflow.net_operating"], "suggested_action": "补充现金流数据"},
|
||||
]
|
||||
rules = [{"id": "rule-synthetic", "name": "虚构测试规则", "version": "1.0.0", "natural_language": "现金流缺失时提示补充,不作审批结论"}]
|
||||
result, trace = assistant_model.invoke(
|
||||
skill,
|
||||
snapshot,
|
||||
checks,
|
||||
rules,
|
||||
"读取全部授权事实、检查与方法,返回可追溯建议。",
|
||||
[],
|
||||
model,
|
||||
{"timeout_seconds": 120, "max_tool_calls": 12},
|
||||
)
|
||||
print(json.dumps({
|
||||
"status": "passed",
|
||||
"endpoint": model["endpoint"],
|
||||
"model": trace["model"],
|
||||
"engine": trace["engine"],
|
||||
"tool_calls": trace["tool_calls"],
|
||||
"tools_read": trace["tools_read"],
|
||||
"finding_count": len(result["findings"]),
|
||||
"evidence_bound": all(item["evidence_ids"] for item in result["findings"] if item["result"] in ("pass", "issue")),
|
||||
"requirement_response_present": bool(result["requirement_response"]),
|
||||
}, ensure_ascii=True))
|
||||
'''
|
||||
encoded = synthetic_test.encode("utf-8").hex()
|
||||
code = "exec(bytes.fromhex(" + repr(encoded) + ").decode('utf-8'))"
|
||||
command = "docker exec mini-openclaw python -c " + shlex.quote(code)
|
||||
_, stdout, stderr = client.exec_command(command, timeout=180)
|
||||
output = stdout.read().decode("utf-8", errors="replace")
|
||||
error = stderr.read().decode("utf-8", errors="replace")
|
||||
status = stdout.channel.recv_exit_status()
|
||||
client.close()
|
||||
if status:
|
||||
raise RuntimeError(error.strip() or output.strip() or f"remote exit {status}")
|
||||
print(output.strip())
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,13 @@
|
||||
const fs=require("fs"),assert=require("assert");
|
||||
const page=fs.readFileSync("src/app/resetdata/page.tsx","utf8");
|
||||
const nav=fs.readFileSync("src/components/xingbang/AgentNav.tsx","utf8");
|
||||
const layout=fs.readFileSync("src/app/resetdata/layout.tsx","utf8");
|
||||
assert(page.includes('/demo/reset'),"reset page must call the scoped demo reset API");
|
||||
assert(page.includes('RESET-DEMO-A-B-C'),"reset confirmation must remain explicit");
|
||||
assert(page.includes('window.confirm'),"reset must have a human confirmation step");
|
||||
assert(page.includes('window.alert'),"reset result must be visible without adding page controls");
|
||||
assert((page.match(/<button/g)||[]).length===1,"reset page must contain exactly one button");
|
||||
assert(!page.includes('AgentNav'),"reset page must not expose application navigation");
|
||||
assert(!nav.includes('/resetdata')&&!nav.includes('演示复位'),"navigation must not expose reset page");
|
||||
assert(layout.includes('index:false')&&layout.includes('follow:false'),"reset page must opt out of search indexing");
|
||||
console.log("resetdata UI checks passed");
|
||||
@@ -0,0 +1,148 @@
|
||||
"""Read-only verification of the Alibaba Cloud demo runtime.
|
||||
|
||||
Connection secrets are read from the ignored server-deploy.conf file. Model
|
||||
credentials are read inside the running container and are never returned.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import shlex
|
||||
from pathlib import Path
|
||||
|
||||
import paramiko
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def load_config(path: Path) -> dict[str, str]:
|
||||
values: dict[str, str] = {}
|
||||
for raw in path.read_text(encoding="utf-8-sig").splitlines():
|
||||
line = raw.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, value = line.split("=", 1)
|
||||
values[key.strip()] = value.strip()
|
||||
return values
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--config", type=Path, default=ROOT / "server-deploy.conf")
|
||||
args = parser.parse_args()
|
||||
config = load_config(args.config)
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
connect = {
|
||||
"hostname": config["SERVER_HOST"],
|
||||
"port": int(config.get("SERVER_PORT", "22")),
|
||||
"username": config["SERVER_USER"],
|
||||
"timeout": 15,
|
||||
"banner_timeout": 15,
|
||||
"auth_timeout": 15,
|
||||
}
|
||||
if config.get("SERVER_KEYFILE"):
|
||||
connect["key_filename"] = config["SERVER_KEYFILE"]
|
||||
else:
|
||||
connect["password"] = config["SERVER_PASSWORD"]
|
||||
client.connect(**connect)
|
||||
|
||||
container_script = r'''
|
||||
import json
|
||||
import os
|
||||
|
||||
import httpx
|
||||
|
||||
base = os.getenv("DEEPSEEK_BASE_URL", "").rstrip("/")
|
||||
key = os.getenv("DEEPSEEK_API_KEY", "")
|
||||
model = os.getenv("DEEPSEEK_MODEL", "")
|
||||
local_url = os.getenv("XINGBANG_LOCAL_MODEL_URL", "").rstrip("/")
|
||||
local_key = os.getenv("XINGBANG_LOCAL_MODEL_KEY", "")
|
||||
local_model = os.getenv("XINGBANG_LOCAL_MODEL_NAME", "")
|
||||
result = {
|
||||
"deepseek_base_url": base,
|
||||
"official_url": base == "https://api.deepseek.com",
|
||||
"deepseek_model": model,
|
||||
"deepseek_key_configured": bool(key),
|
||||
"business_model_url": local_url,
|
||||
"business_model_name": local_model,
|
||||
"business_model_key_configured": bool(local_key),
|
||||
"configured_api_status": None,
|
||||
"deepseek_official_api_status": None,
|
||||
}
|
||||
if base and key:
|
||||
try:
|
||||
response = httpx.post(
|
||||
base + "/chat/completions",
|
||||
headers={"Authorization": "Bearer " + key},
|
||||
json={
|
||||
"model": model or "deepseek-chat",
|
||||
"messages": [{"role": "user", "content": "Reply with OK."}],
|
||||
"max_tokens": 2,
|
||||
"temperature": 0,
|
||||
},
|
||||
timeout=30,
|
||||
trust_env=False,
|
||||
)
|
||||
result["configured_api_status"] = response.status_code
|
||||
except Exception as exc:
|
||||
result["configured_api_status"] = "connection_error:" + type(exc).__name__
|
||||
try:
|
||||
response = httpx.post(
|
||||
"https://api.deepseek.com/chat/completions",
|
||||
headers={"Authorization": "Bearer " + key},
|
||||
json={
|
||||
"model": "deepseek-chat",
|
||||
"messages": [{"role": "user", "content": "Reply with OK."}],
|
||||
"max_tokens": 2,
|
||||
"temperature": 0,
|
||||
},
|
||||
timeout=30,
|
||||
trust_env=False,
|
||||
)
|
||||
result["deepseek_official_api_status"] = response.status_code
|
||||
except Exception as exc:
|
||||
result["deepseek_official_api_status"] = "connection_error:" + type(exc).__name__
|
||||
print(json.dumps(result, ensure_ascii=True))
|
||||
'''
|
||||
encoded = container_script.encode("utf-8").hex()
|
||||
remote_python = (
|
||||
"import os;exec(bytes.fromhex(" + repr(encoded) + ").decode('utf-8'))"
|
||||
)
|
||||
command = (
|
||||
"set -eu; "
|
||||
"printf 'container='; docker inspect -f '{{.State.Status}}' mini-openclaw; "
|
||||
"printf 'health='; docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' mini-openclaw; "
|
||||
"printf 'image='; docker inspect -f '{{.Config.Image}}' mini-openclaw; "
|
||||
"printf 'restart_policy='; docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' mini-openclaw; "
|
||||
"printf 'port_bindings='; docker inspect -f '{{json .HostConfig.PortBindings}}' mini-openclaw; "
|
||||
"printf 'mounts='; docker inspect -f '{{range .Mounts}}{{if .Name}}{{.Name}}{{else}}{{.Source}}{{end}}:{{.Destination}};{{end}}' mini-openclaw; "
|
||||
"printf 'networks='; docker inspect -f '{{range $name, $value := .NetworkSettings.Networks}}{{$name}};{{end}}' mini-openclaw; "
|
||||
"printf 'deploy_path='; test -d " + shlex.quote(config.get("SERVER_DEPLOY_PATH", "")) + " && echo present || echo missing; "
|
||||
"printf 'compose_files='; docker inspect -f '{{index .Config.Labels \"com.docker.compose.project.config_files\"}}' mini-openclaw 2>/dev/null || true; "
|
||||
"printf 'working_dir='; docker inspect -f '{{index .Config.Labels \"com.docker.compose.project.working_dir\"}}' mini-openclaw 2>/dev/null || true; "
|
||||
"cd " + shlex.quote(config.get("SERVER_DEPLOY_PATH", "/")) + "; "
|
||||
"if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then "
|
||||
"printf 'git_head='; git rev-parse --short HEAD; "
|
||||
"printf 'git_branch='; git branch --show-current; "
|
||||
"printf 'git_dirty_files='; git status --porcelain | wc -l; "
|
||||
"else echo 'git_head=not-a-repository'; fi; "
|
||||
"echo 'deploy_files_begin'; find . -maxdepth 2 -type f -printf '%P\\n' | sort | head -80; echo 'deploy_files_end'; "
|
||||
"docker exec mini-openclaw python -c " + shlex.quote(remote_python)
|
||||
)
|
||||
_, stdout, stderr = client.exec_command(command, timeout=60)
|
||||
output = stdout.read().decode("utf-8", errors="replace")
|
||||
error = stderr.read().decode("utf-8", errors="replace")
|
||||
code = stdout.channel.recv_exit_status()
|
||||
client.close()
|
||||
if code:
|
||||
raise RuntimeError(error.strip() or f"Remote verification failed with exit code {code}")
|
||||
print(output.strip())
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user