chore(backup): 保存新版预审开发前的代码与文档基线
verify / test-and-build (push) Canceled after 0s

This commit is contained in:
2026-09-28 16:34:10 +08:00
parent b10093f9d8
commit 4d5a420671
37 changed files with 3890 additions and 10 deletions
+4 -3
View File
@@ -553,13 +553,14 @@ def local_model(skill, values, evidence, system_prompt=None):
endpoint=os.getenv("XINGBANG_LOCAL_MODEL_URL", "")
if not endpoint: raise ValueError("未配置 XINGBANG_LOCAL_MODEL_URL")
url=urlparse(endpoint)
allowed=url.hostname=="localhost"
official_deepseek=url.scheme=="https" and url.hostname=="api.deepseek.com" and url.port in (None,443)
allowed=url.hostname=="localhost" or official_deepseek
try:
address=ipaddress.ip_address(url.hostname or "")
allowed=address.is_loopback or any(address in ipaddress.ip_network(net) for net in ("10.0.0.0/8","172.16.0.0/12","192.168.0.0/16") if address.version==4)
except ValueError: pass
if url.scheme not in ("http","https") or not allowed or url.username or url.password:
raise ValueError("模型地址必须使用 localhost 或内网 IP,禁止外部模型地址")
if url.scheme not in ("http","https") or not allowed or url.username or url.password or url.query or url.fragment:
raise ValueError("模型地址必须使用 localhost、内网 IP 或 DeepSeek 官方 HTTPS 地址")
from graph.risk_task import invoke_risk_task
return invoke_risk_task(skill, values, evidence, endpoint, system_prompt=system_prompt)
+20 -5
View File
@@ -10,7 +10,7 @@ from typing import Literal
import httpx
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, ConfigDict, Field
from pydantic import BaseModel, ConfigDict, Field, field_validator
from graph.agent import agent_manager
@@ -31,6 +31,15 @@ class ModelResult(BaseModel):
requirement_response: str = Field(min_length=1, max_length=5000)
experience_proposal: "ExperienceProposal | None" = None
@field_validator("requirement_response", mode="before")
@classmethod
def normalize_requirement_response(cls, value):
if isinstance(value, dict):
return ";".join(f"{key}:{item}" for key, item in value.items())
if isinstance(value, list):
return ";".join(str(item) for item in value)
return value
class ExperienceProposal(BaseModel):
model_config = ConfigDict(extra="forbid")
@@ -49,16 +58,21 @@ ModelResult.model_rebuild()
def configuration():
endpoint = os.getenv("XINGBANG_LOCAL_MODEL_URL", "")
if not endpoint:
return {"available": False, "reason": "未配置内网模型,语义核查未执行"}
return {"available": False, "reason": "未配置受信模型,语义核查未执行"}
url = urlparse(endpoint)
allowed = url.hostname == "localhost"
official_deepseek = (
url.scheme == "https"
and url.hostname == "api.deepseek.com"
and url.port in (None, 443)
)
allowed = url.hostname == "localhost" or official_deepseek
try:
address = ipaddress.ip_address(url.hostname or "")
allowed = address.is_loopback or any(address in ipaddress.ip_network(net) for net in ("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16") if address.version == 4)
except ValueError:
pass
if url.scheme not in ("http", "https") or not allowed or url.username or url.password or url.query or url.fragment:
raise ValueError("模型须为已批准的内网地址,不允许凭据或参数嵌入 URL")
raise ValueError("模型须为已批准的内网地址或 DeepSeek 官方 HTTPS 地址,不允许凭据或参数嵌入 URL")
return {"available": True, "endpoint": endpoint, "name": os.getenv("XINGBANG_LOCAL_MODEL_NAME", "local-model")}
@@ -69,7 +83,8 @@ PROMPT = """你是兴邦项目核查助手,必须通过受限工具读取本
输出一个JSON对象,只含 findings 和 requirement_response。findings 每项字段为 code(稳定英文码)、title、result
(pass/issue/unknown/not_applicable)、evidence_ids(真实证据ID数组)、fields(标准字段数组)、suggested_action。
没有依据不得判定pass/issue。缺失资料标unknown,证据只能引用冻结快照。不得引用其他项目经验中的客户信息。
requirement_response逐项说明是否完成用户要求,无法验证要明确说明。不要Markdown围栏。"""
requirement_response必须是一个普通字符串,不得输出对象或数组;逐项说明是否完成用户要求,无法验证要明确说明。
示例顶层结构:{"findings": [], "requirement_response": "已读取授权事实并完成核查。"}。不要Markdown围栏。"""
def invoke(skill, snapshot, checks, rules, requirement, experiences, model_config, budget, cancelled=lambda: False, feedback=None):
+9 -2
View File
@@ -602,10 +602,17 @@ class ProjectAssistantService(DocumentsMixin, ExperiencesMixin):
if current["data_domain"] != "demo":
raise ValueError("禁止重置正式项目")
records = [dict(r) for r in db.execute("SELECT * FROM assistant_objects WHERE project_id=? OR (kind='project' AND id=?)", (item["id"], item["id"]))]
put(db, "reset_archive", {"id": uid("reset"), "source_project_id": item["id"], "records": records, "actor": actor, "created_at": now()}, immutable=True)
events = [dict(r) for r in db.execute("SELECT * FROM assistant_events WHERE project_id=? ORDER BY seq", (item["id"],))]
time_events = [dict(r) for r in db.execute("SELECT * FROM business_time_events WHERE project_id=? ORDER BY started_at", (item["id"],))]
put(db, "reset_archive", {"id": uid("reset"), "source_project_id": item["id"], "records": records,
"events": events, "time_events": time_events, "actor": actor, "created_at": now()}, immutable=True)
db.execute("DELETE FROM assistant_jobs WHERE project_id=?", (item["id"],))
db.execute("DELETE FROM assistant_idempotency WHERE object_id IN (SELECT id FROM assistant_objects WHERE project_id=?)", (item["id"],))
db.execute("DELETE FROM assistant_objects WHERE project_id=? OR (kind='project' AND id=?)", (item["id"], item["id"]))
db.execute("DELETE FROM assistant_events WHERE project_id=?", (item["id"],))
db.execute("DELETE FROM business_time_events WHERE project_id=?", (item["id"],))
self._seed_project(db, item, "demo")
self._seed_timeline(db, item)
event(db, item["id"], "demo.reset", actor, {"recoverable": True})
return {"status": "reset", "data_domain": "demo", "projects": len(fixtures["projects"]), "recoverable": True}
return {"status": "reset", "data_domain": "demo", "projects": len(fixtures["projects"]),
"recoverable": True, "history_reset": True}
+20
View File
@@ -9,6 +9,7 @@ import test_project_assistant as fixtures
from services.assistant_store import get, put, listing
from services.component_registry import draft, release, ordered, normalize
from services.component_evaluation import evaluate
from services.assistant_model import ModelResult, configuration
class StructuredToolModel(BaseChatModel):
@@ -44,6 +45,25 @@ class StructuredToolModel(BaseChatModel):
class ComponentTests(fixtures.ProjectAssistantTests):
# Reuse only the fixture helpers, not inherited test execution (filtered below).
def test_model_endpoint_policy_allows_only_private_or_official_deepseek(self):
with patch.dict("os.environ", {"XINGBANG_LOCAL_MODEL_URL": "https://api.deepseek.com", "XINGBANG_LOCAL_MODEL_NAME": "deepseek-chat"}):
config = configuration()
self.assertTrue(config["available"])
self.assertEqual(config["name"], "deepseek-chat")
with patch.dict("os.environ", {"XINGBANG_LOCAL_MODEL_URL": "http://api.deepseek.com"}):
with self.assertRaises(ValueError):
configuration()
def test_requirement_response_object_is_normalized_to_readable_text(self):
result = ModelResult.model_validate({
"findings": [],
"requirement_response": {"数据读取": "完成", "待核实": "现金流缺失"},
})
self.assertEqual(result.requirement_response, "数据读取:完成;待核实:现金流缺失")
with patch.dict("os.environ", {"XINGBANG_LOCAL_MODEL_URL": "https://example.com/v1"}):
with self.assertRaises(ValueError):
configuration()
def test_real_agent_manager_and_invalid_evidence(self):
with patch.dict("os.environ", {"XINGBANG_LOCAL_MODEL_URL": "http://127.0.0.1:11434/v1"}), patch("services.assistant_model.ChatOpenAI", return_value=StructuredToolModel()):
a = self.run_project("DEMO-A", "跳过现金流检查")
+5
View File
@@ -211,10 +211,15 @@ class ProjectAssistantTests(unittest.TestCase):
self.assertEqual(self.post("/demo/reset").status_code, 422)
result = self.post("/demo/reset", {"confirmation": "RESET-DEMO-A-B-C"})
self.assertEqual(result.status_code, 200, result.text)
self.assertTrue(result.json()["history_reset"])
self.assertEqual(self.project()["revision"], 1)
with self.service.connect() as db:
self.assertEqual(len(listing(db, "reset_archive")), 3)
self.assertTrue(listing(db, "component_version"))
project = self.project("DEMO-B")
self.assertEqual(project["stage"], "补件中")
self.assertEqual(len(project["timeline"]), 3)
self.assertEqual([item["action"] for item in project["events"]], ["demo.reset"])
def test_mock_and_readonly_sqlite_adapter_agree(self):
import sqlite3
+5
View File
@@ -67,6 +67,11 @@ class RiskAgentTests(unittest.TestCase):
def test_model_never_calls_external_url(self):
with patch.dict("os.environ",{"XINGBANG_LOCAL_MODEL_URL":"https://example.com/v1"}):
with self.assertRaises(ValueError):engine.local_model({}, {}, {})
def test_official_deepseek_model_url_is_allowed(self):
with patch.dict("os.environ",{"XINGBANG_LOCAL_MODEL_URL":"https://api.deepseek.com"}), patch("graph.risk_task.invoke_risk_task",return_value={"text":"ok"}) as invoke:
result=engine.local_model({}, {}, {})
self.assertEqual(result,{"text":"ok"})
self.assertEqual(invoke.call_args.args[3],"https://api.deepseek.com")
def test_crud_custom_skill(self):
response=self.client.post(self.base+"/skills",json={"name":"新检查","data":{"engine":"quality","inputs":["*"],"output":"quality"}})
self.assertEqual(response.status_code,200,response.text)