Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d5a420671 |
@@ -75,4 +75,5 @@ ori_meetingfile/
|
||||
nul
|
||||
.next-dev/
|
||||
deploy/*.env
|
||||
server-deploy.conf
|
||||
ori_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)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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", "跳过现金流检查")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -7,6 +7,7 @@ BACKEND_PORT=8002
|
||||
DEEPSEEK_API_KEY=replace-me
|
||||
DEEPSEEK_MODEL=deepseek-chat
|
||||
DEEPSEEK_BASE_URL=https://api.deepseek.com
|
||||
# The business Agent Loop reuses the same approved official DeepSeek endpoint.
|
||||
# For the gateway tested in the chat repair use disabled; otherwise verify support.
|
||||
CHAT_THINKING_MODE=
|
||||
# Temporary business-workbench role dropdown. Keep disabled for production data.
|
||||
|
||||
@@ -10,6 +10,9 @@ services:
|
||||
DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY:?Set DEEPSEEK_API_KEY}
|
||||
DEEPSEEK_MODEL: ${DEEPSEEK_MODEL:-deepseek-chat}
|
||||
DEEPSEEK_BASE_URL: ${DEEPSEEK_BASE_URL:-https://api.deepseek.com}
|
||||
XINGBANG_LOCAL_MODEL_KEY: ${DEEPSEEK_API_KEY:?Set DEEPSEEK_API_KEY}
|
||||
XINGBANG_LOCAL_MODEL_NAME: ${DEEPSEEK_MODEL:-deepseek-chat}
|
||||
XINGBANG_LOCAL_MODEL_URL: ${DEEPSEEK_BASE_URL:-https://api.deepseek.com}
|
||||
CHAT_THINKING_MODE: ${CHAT_THINKING_MODE:-}
|
||||
XINGBANG_DEMO_ROLE_LOGIN: ${XINGBANG_DEMO_ROLE_LOGIN:-0}
|
||||
XINGBANG_DEMO_PROJECTS: ${XINGBANG_DEMO_PROJECTS:-DEMO-A,DEMO-B,DEMO-C,LAB-001,XB-2026-001}
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,661 @@
|
||||
# mini-openclaw 项目技术架构与数据流说明
|
||||
|
||||
**面向对象:** 无代码基础的业务、产品、实施与管理同事
|
||||
**说明版本:** v1.0
|
||||
**代码基线:** Git 提交 `aec52ec`(2026-09-16)
|
||||
**适用范围:** 本仓库当前代码,不扩展到通用 LangChain、LangGraph 或其他 OpenClaw 实现
|
||||
|
||||
---
|
||||
|
||||
## 1. 这份文件要回答什么
|
||||
|
||||
这份文件只解释当前项目里的真实实现,重点回答六个问题:
|
||||
|
||||
1. 用户输入一句话后,数据先到哪里、再到哪里?
|
||||
2. 原版 mini-openclaw 怎样组织提示词、历史对话、Skills 和工具?
|
||||
3. LangChain 与 LangGraph 在本项目里各自承担什么?
|
||||
4. 大模型一次“回答”为什么可能实际调用多次模型?每次输入和输出是什么?
|
||||
5. 工具怎样被选择、执行和回传?哪些工具有安全边界?
|
||||
6. 短期记忆、长期记忆、知识库、运行留痕和兴邦业务快照有什么区别?
|
||||
|
||||
为了不误导读者,全文使用三个标记:
|
||||
|
||||
- **已实现**:当前代码中存在并能指出具体文件。
|
||||
- **配置约定**:由提示词或配置规定,但不等于代码自动强制完成。
|
||||
- **当前边界**:代码尚未实现、依赖缺失或能力有限的地方。
|
||||
|
||||
## 2. 一页结论:先建立正确的整体认识
|
||||
|
||||
mini-openclaw 不是“一个大模型直接连接数据库然后给答案”。当前项目由两条相互关联但用途不同的数据链组成。
|
||||
|
||||
### 2.1 原版通用对话链
|
||||
|
||||
```text
|
||||
浏览器聊天框
|
||||
→ Next.js 同源代理 /api/core
|
||||
→ FastAPI /api/chat
|
||||
→ 读取会话历史与系统提示词
|
||||
→ LangChain create_agent(底层由 LangGraph 驱动循环)
|
||||
→ 大模型决定直接回答,或请求调用工具
|
||||
→ Python 执行被允许的工具
|
||||
→ 工具结果回到模型
|
||||
→ 模型继续判断,直至形成最终文本
|
||||
→ SSE 流式返回浏览器,并保存会话与追踪日志
|
||||
```
|
||||
|
||||
它适合自由问答、文件读取、受限命令、Python 计算、网页抓取和知识库检索。它的主要输出是自然语言。
|
||||
|
||||
### 2.2 兴邦业务运行链
|
||||
|
||||
```text
|
||||
业务数据库 / 上传材料 / 人工确认
|
||||
→ 数据卡受控 SQL 与字段映射
|
||||
→ 冻结输入快照
|
||||
→ 确定性 Skills 与规则先计算
|
||||
→ 可选的受限大模型语义核查
|
||||
→ 对模型 JSON 做结构、证据和权限校验
|
||||
→ 形成问题、证据、运行记录与报告草稿
|
||||
→ 客户经理提交评审池
|
||||
→ 评审员领取、复核、退回或批准
|
||||
```
|
||||
|
||||
它适合可追溯的对公金租预审与评审。它的主要输出不是聊天文字,而是带编号、版本、证据和状态的业务对象。
|
||||
|
||||
### 2.3 两条链的关系
|
||||
|
||||
- 两者共用同一个 `AgentManager` 基座和 LangChain Agent 循环。
|
||||
- 原生聊天拥有通用工具;业务运行只获得当前任务明确允许的只读工具。
|
||||
- 原生聊天使用会话记忆;业务运行使用冻结快照,不继承聊天记忆,避免把上一项目内容带入下一项目。
|
||||
- 业务快捷助手的多数命令是可审计的固定意图分派,不是把每句话都交给大模型猜。
|
||||
|
||||
## 3. 系统分层:每一层只做一类事
|
||||
|
||||
| 层次 | 当前技术 | 主要职责 | 关键代码 |
|
||||
|---|---|---|---|
|
||||
| 浏览器界面 | Next.js 14、React 18 | 展示聊天、项目、评审池、报告、时效看板 | `src/app/`、`src/components/` |
|
||||
| 同源代理 | Next.js Route Handler | 将浏览器请求转发到 8002 后端;生产环境校验访问身份 | `src/app/api/core/[...path]/route.ts`、`src/app/api/risk-agent/[...path]/route.ts` |
|
||||
| API 层 | FastAPI | 参数校验、权限检查、SSE 输出、业务路由 | `backend/app.py`、`backend/api/` |
|
||||
| Agent 编排层 | LangChain `create_agent`,LangGraph 运行循环 | 组织模型与工具的多轮调用 | `backend/graph/agent.py` |
|
||||
| 模型适配层 | `ChatDeepSeek`、`ChatOpenAI` | 调用通用模型或内网业务模型 | `backend/graph/agent.py`、`backend/services/assistant_model.py` |
|
||||
| 工具层 | LangChain Tools | 终端、Python、网页、文件、知识库或业务只读工具 | `backend/tools/`、`backend/services/assistant_model.py` |
|
||||
| 业务执行层 | Python 确定性执行器、持久 Worker | 数据读取、计算、规则、模型核查、断点恢复 | `backend/services/assistant_worker.py`、`skill_executor.py` |
|
||||
| 持久化层 | JSON/JSONL、SQLite、LlamaIndex 索引 | 会话、追踪、项目对象、任务、证据、向量索引 | `backend/sessions/`、`raw_logs/`、`xingbang_data/`、`storage/` |
|
||||
|
||||
浏览器并不直接连接大模型、数据库或文件系统。所有能力都必须先经过代理和后端接口。
|
||||
|
||||
## 4. 服务启动时发生什么
|
||||
|
||||
FastAPI 的入口是 `backend/app.py`。服务启动时按当前代码执行以下动作:
|
||||
|
||||
1. 扫描 `backend/skills/**/SKILL.md`,生成 `backend/SKILLS_SNAPSHOT.md`。
|
||||
2. 初始化 `AgentManager`,加载模型配置、通用工具和会话管理器。
|
||||
3. 尝试为 `backend/memory/MEMORY.md` 重建或加载向量索引。
|
||||
4. 根据功能开关启动兴邦项目的 `DurableWorker` 后台线程。
|
||||
5. 注册聊天、会话、文件、配置以及兴邦项目、评审、报告等 API 路由。
|
||||
|
||||
这说明 Skills 清单、模型对象和记忆索引不是每次点击按钮才从零创建;部分基础设施在后端启动时准备好。业务组件则在每次运行入队时冻结,保证运行过程中版本不漂移。
|
||||
|
||||
## 5. 原版 mini-openclaw:一条消息的完整旅程
|
||||
|
||||
以下按时间顺序解释用户在原生聊天框输入“读取材料并分析风险”之后发生的事情。
|
||||
|
||||
### 5.1 浏览器发出请求
|
||||
|
||||
前端 `src/lib/api.ts` 将消息、`session_id` 和 `stream=true` 发送到 `/api/core/chat`。
|
||||
|
||||
`/api/core` 是 Next.js 同源代理。它只允许一组白名单路径,例如 `chat`、`sessions`、`files`、`skills`、`tokens`、`config` 和 `xingbang`。生产环境中,通用聊天和文件工具只允许已认证管理员;未配置认证时,只允许本机开发访问。
|
||||
|
||||
代理把请求转发到 FastAPI 的 `/api/chat`,并且对 SSE 响应直接转发流,不先把整个回答读完。
|
||||
|
||||
### 5.2 FastAPI 读取会话
|
||||
|
||||
`backend/api/chat.py` 接到请求后,从 `SessionManager` 读取当前会话。
|
||||
|
||||
会话文件位于 `backend/sessions/{session_id}.json`,主要包括:
|
||||
|
||||
- 会话标题;
|
||||
- 创建和更新时间;
|
||||
- 用户与助手消息;
|
||||
- 压缩后的历史摘要(如果执行过会话压缩)。
|
||||
|
||||
送给 Agent 之前,代码会把连续的助手消息合并,并只把普通的用户/助手历史转成模型消息。工具轨迹有单独的 JSONL 留痕,不会原样全部塞回下一轮上下文。
|
||||
|
||||
### 5.3 构造系统提示词
|
||||
|
||||
`backend/graph/prompt_builder.py` 按固定顺序拼接:
|
||||
|
||||
1. `backend/SKILLS_SNAPSHOT.md`:可用 Skills 的名称、说明和路径;
|
||||
2. `backend/workspace/SOUL.md`:行为风格和原则;
|
||||
3. `backend/workspace/IDENTITY.md`:助手身份;
|
||||
4. `backend/workspace/USER.md`:用户背景;
|
||||
5. `backend/workspace/AGENTS.md`:工作规则;
|
||||
6. `backend/memory/MEMORY.md`:仅在 RAG 关闭时整份加入。
|
||||
|
||||
单个文件最多读取 20,000 个字符。这里要区分:Skills 快照告诉模型“有哪些方法说明”;它本身不会把每个 `SKILL.md` 自动变成 Python 函数。
|
||||
|
||||
### 5.4 构造 Agent
|
||||
|
||||
`backend/graph/agent.py` 调用:
|
||||
|
||||
```python
|
||||
create_agent(model=model, tools=tools, system_prompt=system_prompt)
|
||||
```
|
||||
|
||||
这里的职责分工是:
|
||||
|
||||
- **LangChain** 提供模型适配器、消息类型、工具定义和 `create_agent` 高层接口。
|
||||
- **LangGraph** 在 LangChain v1 的 `create_agent` 内部承担有状态循环:模型节点 → 工具节点 → 再回模型节点,直到不再产生工具调用。
|
||||
- **本项目代码** 决定系统提示词、可用工具、历史消息、流式事件、日志和超时。
|
||||
|
||||
原版聊天没有在仓库中手写一张复杂的 `StateGraph` 图,而是使用 LangChain 封装好的 Agent 图。因此说“本项目使用 LangGraph”是正确的,但不能误解为仓库中每个页面都自己定义了 LangGraph 节点。
|
||||
|
||||
### 5.5 模型第一次收到什么
|
||||
|
||||
第一次模型调用的输入可概括为:
|
||||
|
||||
```text
|
||||
系统提示词
|
||||
+ 已压缩历史摘要(如果有)
|
||||
+ 既往用户/助手消息
|
||||
+ 本轮用户消息
|
||||
+ 本轮 RAG 检索结果(仅 RAG 开启且检索成功时)
|
||||
+ 工具名称、用途与参数结构
|
||||
```
|
||||
|
||||
模型会输出两类结果之一:
|
||||
|
||||
1. **普通文本 token**:表示模型准备直接回答;
|
||||
2. **tool_calls**:表示模型不直接给最终答案,而是请求执行一个或多个工具,并给出工具参数。
|
||||
|
||||
模型只“提出调用请求”,真正执行命令、读取文件或发起 HTTP 请求的是后端 Python 工具代码。
|
||||
|
||||
### 5.6 工具调用循环
|
||||
|
||||
如果模型输出工具调用,例如 `read_file({path: ...})`,流程是:
|
||||
|
||||
1. LangGraph 将控制权转到工具节点;
|
||||
2. 工具节点按参数调用 Python 函数;
|
||||
3. Python 函数完成读取或计算,返回文本/对象;
|
||||
4. 结果包装成 `ToolMessage`;
|
||||
5. `ToolMessage` 与此前上下文一起再次送入模型;
|
||||
6. 模型根据工具结果继续回答,或再次请求工具;
|
||||
7. 当模型只输出文本、不再输出 `tool_calls` 时,本轮结束。
|
||||
|
||||
因此一次用户问题可能包含多次大模型调用。例如:
|
||||
|
||||
```text
|
||||
第1次模型:决定读取文件
|
||||
工具:返回文件内容
|
||||
第2次模型:发现需要计算
|
||||
工具:执行 Python 计算
|
||||
第3次模型:综合文件与计算结果,生成最终回答
|
||||
```
|
||||
|
||||
“三次模型调用”仍然是用户看到的一次对话轮次。
|
||||
|
||||
### 5.7 SSE 怎样让页面实时更新
|
||||
|
||||
后端把运行过程转换为 Server-Sent Events:
|
||||
|
||||
| 事件 | 页面含义 |
|
||||
|---|---|
|
||||
| `retrieval` | 长期记忆检索结果已加入上下文 |
|
||||
| `token` | 新生成的一小段回答文字 |
|
||||
| `tool_start` | 模型请求的工具开始执行 |
|
||||
| `tool_end` | 工具执行结束,返回预览结果 |
|
||||
| `new_response` | 工具调用后,模型开始新一段文字回答 |
|
||||
| `done` | 本轮完整回答结束 |
|
||||
| `title` | 首轮对话自动生成了会话标题 |
|
||||
| `error` | 空回答、超时或调用失败 |
|
||||
|
||||
聊天流空闲超过 75 秒或总运行超过 180 秒会终止。代码不会在超时后自动重放整轮,以免有副作用的工具被执行两次。
|
||||
|
||||
### 5.8 一轮结束后保存什么
|
||||
|
||||
一轮结束后至少有三类记录:
|
||||
|
||||
1. **会话记录**:`backend/sessions/{session_id}.json`,供下次对话继续使用;
|
||||
2. **完整消息轨迹**:`backend/raw_logs/{session_id}.msgs.jsonl`,包含 human、assistant、tool 消息;
|
||||
3. **原始模型追踪**:`backend/raw_logs/{session_id}.jsonl`,包含每次 LLM 请求、响应、工具调用、token 用量、结束原因和耗时。
|
||||
|
||||
页面上的工具结果可能被截短用于展示,但完整 `ToolMessage` 会写入消息轨迹。原始追踪会过滤 `api_key` 字段,但消息正文仍可能包含业务内容,因此该目录应按敏感日志管理。
|
||||
|
||||
首轮对话结束后还可能额外调用一次模型生成短标题;这次调用不属于业务回答本身。
|
||||
|
||||
## 6. 原版通用工具:模型能做什么
|
||||
|
||||
`backend/tools/__init__.py` 当前注册五类通用工具。
|
||||
|
||||
### 6.1 `terminal`
|
||||
|
||||
- 在后端项目目录作为工作目录执行命令;
|
||||
- 30 秒超时;
|
||||
- 返回内容最多约 5,000 字符;
|
||||
- 对若干危险命令字符串做黑名单检查。
|
||||
|
||||
**当前边界:** 这是“工作目录 + 命令黑名单”的约束,不是操作系统级强沙箱。它不能被描述为完全隔离的安全容器。因此生产代理将通用工具限制为管理员使用。
|
||||
|
||||
### 6.2 `python_repl`
|
||||
|
||||
通过 LangChain Experimental 的 Python REPL 执行计算。适合数据计算和快速验证。
|
||||
|
||||
**当前边界:** 包装层没有显示文件系统强隔离,权限仍取决于后端进程账户。
|
||||
|
||||
### 6.3 `fetch_url`
|
||||
|
||||
- 使用 HTTP GET 获取 URL;
|
||||
- 15 秒超时;
|
||||
- JSON 直接返回,HTML 转 Markdown;
|
||||
- 结果最多约 5,000 字符。
|
||||
|
||||
### 6.4 `read_file`
|
||||
|
||||
- 读取后端允许根目录内的文件;
|
||||
- 路径解析后检查是否仍位于根目录;
|
||||
- 最多返回约 10,000 字符。
|
||||
|
||||
### 6.5 `search_knowledge_base`
|
||||
|
||||
- 面向 `backend/knowledge` 的通用知识库;
|
||||
- 通过 LlamaIndex 建立向量索引;
|
||||
- 每次返回相似度最高的 3 个结果。
|
||||
|
||||
**当前边界:** 当前代码实现的是向量检索,不是文档中曾描述的 BM25 + 向量混合检索。仓库当前也没有可见的 `backend/knowledge` 内容时,工具会返回知识库为空。
|
||||
|
||||
## 7. Skills 在本项目里到底是什么
|
||||
|
||||
Skills 容易被误解为“装上去就能自动执行的插件”。当前项目存在两种不同用法。
|
||||
|
||||
### 7.1 原生聊天里的 Skills:方法说明
|
||||
|
||||
`backend/tools/skills_scanner.py` 扫描 `backend/skills/**/SKILL.md`:
|
||||
|
||||
- YAML 头必须包含 `name` 和 `description`;
|
||||
- 单文件最大 64 KB;
|
||||
- 状态为 `archived` 的 Skill 不展示;
|
||||
- 扫描结果写入 `SKILLS_SNAPSHOT.md`,进入系统提示词。
|
||||
|
||||
这类 Skill 主要告诉模型“遇到某类任务应按什么步骤做”。它本身不会自动获得新权限;真正能不能读文件、查网页、执行命令,仍由 `tools` 列表决定。
|
||||
|
||||
### 7.2 兴邦业务里的 Skills:冻结的方法 + 受控执行器
|
||||
|
||||
业务运行还会把 Skills 组件冻结进运行计划:
|
||||
|
||||
- `quality`、`ratio`、`compare`、`required`、`duplicate` 等由 Python 确定性执行;
|
||||
- `model` 类型才调用大模型;
|
||||
- `model` Skill 只能使用发布时声明的 `allowed_tools`;
|
||||
- 被选中的原生 `SKILL.md` 文本在入队时读出并计算哈希,之后文件发生变化也不会影响这次运行。
|
||||
|
||||
这使“方法”与“权限”分离:Skill 可以教模型如何核查租赁物,但不能凭文字自行获得 SQL、终端或写文件权限。
|
||||
|
||||
## 8. 记忆系统:五种容易混淆的存储
|
||||
|
||||
### 8.1 会话短期记忆
|
||||
|
||||
位置:`backend/sessions/{session_id}.json`。
|
||||
|
||||
用途:让同一聊天会话记住前文。下一轮模型会重新获得历史用户/助手消息。
|
||||
|
||||
### 8.2 压缩历史
|
||||
|
||||
当会话过长,可以调用压缩接口:
|
||||
|
||||
- 旧消息归档到 `backend/sessions/archive/`;
|
||||
- 摘要写入会话的 `compressed_context`;
|
||||
- 下次对话把摘要作为第一条助手上下文加入。
|
||||
|
||||
摘要不是原消息逐字回放,因此重要事实仍应进入正式项目数据或长期记忆,而不是只依赖压缩摘要。
|
||||
|
||||
### 8.3 长期记忆 `MEMORY.md`
|
||||
|
||||
位置:`backend/memory/MEMORY.md`。
|
||||
|
||||
当前有两种模式:
|
||||
|
||||
- **RAG 关闭(当前 `backend/config.json` 为 `false`)**:整份 `MEMORY.md` 直接进入系统提示词;
|
||||
- **RAG 开启**:将文件切成约 256 token、重叠 32 token 的片段,使用 `BAAI/bge-small-zh` 嵌入,按问题检索前 3 个片段。
|
||||
|
||||
索引持久化到 `backend/storage/memory_index`,用 MD5 检测文件变化并重建。索引加载失败时会降级,不应把“启动成功”误认为“长期记忆一定可检索”。
|
||||
|
||||
**配置约定:** `workspace/AGENTS.md` 可以要求模型把重要信息写入长期记忆;这是提示词协议。当前没有发现一套独立代码自动把每日聊天全部总结并写入 `MEMORY.md`,所以不能宣传为全自动记忆沉淀。
|
||||
|
||||
### 8.4 通用知识库
|
||||
|
||||
位置:`backend/knowledge` 和 `backend/storage` 下的知识库索引。
|
||||
|
||||
用途:查询制度、材料或知识文档。它与 `MEMORY.md` 是两套独立索引:长期记忆是“助手应该记住的上下文”,知识库是“可以检索的外部资料”。
|
||||
|
||||
### 8.5 兴邦业务快照与经验
|
||||
|
||||
业务项目不会依赖聊天记忆作为审批依据。它使用 SQLite 中的:
|
||||
|
||||
- 项目事实与已选数据来源;
|
||||
- 不可变输入快照和项目快照;
|
||||
- 运行步骤、问题、证据、报告和评审决定;
|
||||
- 已发布经验版本及其适用记录。
|
||||
|
||||
这类存储是业务留痕,不是大模型的自由记忆。只有经过发布、权限和适用条件筛选的经验才会进入某次业务运行。
|
||||
|
||||
## 9. 兴邦扩展:业务数据怎样进入系统
|
||||
|
||||
### 9.1 三类入口
|
||||
|
||||
当前业务事实可以来自:
|
||||
|
||||
1. **受控数据库数据卡**:管理员配置数据源、参数化 SQL 和字段映射;
|
||||
2. **上传材料**:TXT、CSV、DOCX、XLSX、PDF、PNG、JPG;
|
||||
3. **人工确认/补充**:客户经理对解析候选、期间、单位、口径和来源进行确认。
|
||||
|
||||
三类入口最终都转换成标准字段,例如:
|
||||
|
||||
- `facility.amount`:融资金额;
|
||||
- `lease.total_value`:租赁物价值;
|
||||
- `financial.total_liabilities`:财务负债;
|
||||
- `financial.operating_cashflow`:经营现金流;
|
||||
- `credit.overdue_count`:逾期记录数。
|
||||
|
||||
Skills 和规则只依赖标准字段,不直接依赖源系统的中文列名或表结构。
|
||||
|
||||
### 9.2 数据卡怎样读取多张业务表
|
||||
|
||||
演示业务库脚本 `backend/backend/xingbang_data/business_database.sql` 包含客户、项目、融资方案、财务报表、租赁物、保险、增信、征信、诉讼、还款来源和材料等多张表。
|
||||
|
||||
数据卡配置包含:
|
||||
|
||||
- 数据源 ID;
|
||||
- 只读、参数化 SQL,例如使用 `:project_id`;
|
||||
- 源字段到标准字段的映射;
|
||||
- 金额单位、币种、期间和口径。
|
||||
|
||||
`assistant_data.py` 执行数据卡后会:
|
||||
|
||||
1. 检查演示域与生产域不能混用;
|
||||
2. 按项目参数读取行;
|
||||
3. 做字段映射和金额单位换算;
|
||||
4. 为每个字段生成证据引用;
|
||||
5. 发现多来源不一致时将字段置空并记录冲突,而不是擅自选一个值;
|
||||
6. 保存数据集行哈希,便于判断来源是否变化。
|
||||
|
||||
业务模型不会直接拿到数据库连接,也没有 SQL 工具。SQL 由数据卡层执行,模型只能读取已经授权并冻结的标准字段和证据。
|
||||
|
||||
### 9.3 上传材料怎样解析
|
||||
|
||||
`assistant_materials.py` 的约束包括:
|
||||
|
||||
- 单文件最大 10 MB;
|
||||
- 文本总量最大约 30 万字;
|
||||
- DOCX/XLSX 直接读取压缩包内 XML,不执行宏;
|
||||
- XLSX 公式只读取已有缓存值,不在服务端重算公式;
|
||||
- PDF/图片通过抵质押核验模块的页面提取能力读取文本;
|
||||
- 解析不到的页会产生警告;
|
||||
- 每个文本片段生成独立证据 ID。
|
||||
|
||||
解析结果先形成“候选字段”,不会自动覆盖项目事实。只有人工确认候选并说明依据后,项目数据版本才更新。
|
||||
|
||||
## 10. 一次业务 AI 预审怎样执行
|
||||
|
||||
### 10.1 入队,而不是浏览器一直等待
|
||||
|
||||
客户经理点击“发起 AI 预审”或使用业务快捷命令后,`ProjectAssistantService.enqueue` 会:
|
||||
|
||||
1. 检查身份、角色和项目权限;
|
||||
2. 解析选定方案;
|
||||
3. 冻结 Skill、规则、模板、数据卡、内置方法文本及版本哈希;
|
||||
4. 冻结当时已确认的项目事实、来源和材料清单;
|
||||
5. 创建 `run`、`input_snapshot` 和 `assistant_jobs` 记录;
|
||||
6. 使用幂等键避免同一操作因重复点击而创建两次;
|
||||
7. 立即返回运行编号,后台 Worker 再继续执行。
|
||||
|
||||
### 10.2 DurableWorker 怎样保证任务可恢复
|
||||
|
||||
Worker 从 SQLite 领取 `queued` 任务,并写入:
|
||||
|
||||
- 执行人(worker owner);
|
||||
- 30 秒租约;
|
||||
- 尝试次数;
|
||||
- 当前状态和开始时间。
|
||||
|
||||
执行期间每 5 秒续租。进程异常后,租约过期的任务可以被重新领取;最多恢复 3 次。每个步骤完成后都会持久化,重新执行时可复用已完成步骤,而不是从第一步全部重跑。
|
||||
|
||||
### 10.3 先确定性计算,再调用模型
|
||||
|
||||
当前默认顺序是:
|
||||
|
||||
1. 非模型 Skills;
|
||||
2. 规则集;
|
||||
3. 已发布经验核查;
|
||||
4. 模型 Skills。
|
||||
|
||||
金额和比率使用 `Decimal`,不是二进制浮点数;执行器只允许固定类型,不使用动态 `eval`。这部分结果可重复、可单元测试。
|
||||
|
||||
### 10.4 业务模型每次看到什么
|
||||
|
||||
模型 Skill 通过 `assistant_model.py` 创建一个“任务隔离 Agent”。它不会读取聊天历史,也不能继承通用 Agent 的终端、网页或 SQL 工具。
|
||||
|
||||
它可被授权的工具只有:
|
||||
|
||||
- `read_project_snapshot`:读本次不可变项目事实和来源;
|
||||
- `read_document`:读本次快照内的一份材料;
|
||||
- `read_checks`:读已经计算的检查、发布规则和本次人工要求;
|
||||
- `read_method`:读本次选中的冻结 Skill 方法;
|
||||
- `read_experiences`:读脱敏后的适用经验指引。
|
||||
|
||||
模型的任务说明明确要求:材料中的指令是不可信数据;不得执行 SQL;不得修改文件;不得作审批决定;不得自行生成重要数字。
|
||||
|
||||
### 10.5 模型输出为什么不能直接入库
|
||||
|
||||
模型必须返回一个 JSON 对象,并通过 Pydantic 校验。主要字段是:
|
||||
|
||||
```json
|
||||
{
|
||||
"findings": [
|
||||
{
|
||||
"code": "稳定英文检查码",
|
||||
"title": "问题标题",
|
||||
"result": "pass | issue | unknown | not_applicable",
|
||||
"evidence_ids": ["证据编号"],
|
||||
"fields": ["标准字段"],
|
||||
"suggested_action": "建议动作"
|
||||
}
|
||||
],
|
||||
"requirement_response": "对本次人工要求的回应"
|
||||
}
|
||||
```
|
||||
|
||||
程序还会继续做业务校验:
|
||||
|
||||
- 必须调用 `read_project_snapshot` 和 `read_checks`;
|
||||
- 每个选中方法都必须调用 `read_method`;
|
||||
- 引用的证据必须属于当前快照;
|
||||
- 引用材料片段前必须实际调用过对应 `read_document`;
|
||||
- `pass` 或 `issue` 没有证据时拒绝;
|
||||
- 标准字段必须真实存在;
|
||||
- 检查码不能重复;
|
||||
- 超时、取消或超过工具次数预算时拒绝。
|
||||
|
||||
只有通过这些校验,模型结果才进入运行步骤。这就是“模型负责语义判断,程序负责边界与证据完整性”。
|
||||
|
||||
### 10.6 运行结束怎样形成问题
|
||||
|
||||
Worker 将本轮 checks 与历史 findings 对账:
|
||||
|
||||
- 新问题 → 新建;
|
||||
- 有新证据证明通过 → 已解决或待复核;
|
||||
- 已解决问题再次失败 → 重新打开;
|
||||
- 本轮没有执行到的旧检查 → 标记 stale,不能默认为通过;
|
||||
- 运行期间项目数据版本已变化 → 本轮结果标记 superseded,不覆盖当前问题状态。
|
||||
|
||||
最终运行状态可能是 `completed`、`partial`、`failed` 或 `cancelled`。`partial` 明确表示仍有数据错误、步骤未完成或结果已过时,而不是把部分成功包装成全部成功。
|
||||
|
||||
## 11. 报告、评审池和人工决定怎样衔接
|
||||
|
||||
1. 客户经理用某次已完成/部分完成的预审运行生成报告草稿。
|
||||
2. 报告保存所用快照、运行、模板和章节版本;数据变化会把旧草稿标记为 stale。
|
||||
3. 客户经理提交报告后,项目进入评审池,并记录提交时间。
|
||||
4. 评审员领取项目后,才能写评审决定。
|
||||
5. 评审员可以对全链路节点或具体问题选择固定结论,并在需要时填写意见。
|
||||
6. “退回”使项目回到客户经理补件;重新提交保留历史版本。
|
||||
7. “批准”及评审报告不会由 AI 自动决定,最终动作属于人工角色。
|
||||
|
||||
报告导出为 DOCX,但它是结构化业务对象的呈现形式。可追溯链仍是:报告章节 → 运行 → 检查/问题 → 证据 ID → 数据卡字段或材料定位。
|
||||
|
||||
## 12. 业务快捷助手与原生聊天为什么回答方式不同
|
||||
|
||||
`BusinessAssistant.tsx` 有两个选项卡:
|
||||
|
||||
- **业务快捷助手**:调用 `/project-assistant/commands`;
|
||||
- **mini-openclaw**:嵌入原生 `ChatPanel`。
|
||||
|
||||
业务快捷助手当前不是开放式 LLM。后端按关键词识别有限意图,例如:
|
||||
|
||||
- 发起预审/独立评审;
|
||||
- 生成报告;
|
||||
- 提交评审;
|
||||
- 查看进度、问题、缺失材料和下一步;
|
||||
- 显示帮助或要求用户澄清。
|
||||
|
||||
所有变更都调用与页面按钮相同的服务方法,所以权限、幂等和留痕一致。复杂自由分析要切到原生 mini-openclaw。这个设计避免业务人员一句模糊自然语言就触发不可预测的业务修改。
|
||||
|
||||
## 13. 持久化总表:数据究竟放在哪里
|
||||
|
||||
| 数据 | 存储 | 是否进入下一次模型上下文 | 主要用途 |
|
||||
|---|---|---|---|
|
||||
| 聊天会话 | `backend/sessions/*.json` | 是,同一 session | 连续对话 |
|
||||
| 压缩前历史 | `backend/sessions/archive/*.json` | 否,摘要替代 | 审计与节省上下文 |
|
||||
| 长期记忆 | `backend/memory/MEMORY.md` | 是,整份或检索片段 | 跨会话偏好/约定 |
|
||||
| 长期记忆向量索引 | `backend/storage/memory_index` | 通过检索间接进入 | 加速相关片段查找 |
|
||||
| 通用知识库 | `backend/knowledge` + 索引 | 仅工具调用后 | 文档问答 |
|
||||
| 原始 LLM 追踪 | `backend/raw_logs/*.jsonl` | 默认不进入 | 请求/响应/耗时/用量追踪 |
|
||||
| 完整消息轨迹 | `backend/raw_logs/*.msgs.jsonl` | 默认不进入 | 工具与消息审计 |
|
||||
| 兴邦项目对象 | SQLite `assistant_objects` | 业务运行按快照读取 | 项目、运行、证据、报告等 |
|
||||
| 业务事件 | SQLite `assistant_events` | 否 | 操作审计 |
|
||||
| 任务队列 | SQLite `assistant_jobs` | 否 | 可恢复后台执行 |
|
||||
| 幂等记录 | SQLite `assistant_idempotency` | 否 | 防重复提交 |
|
||||
| 时间事件 | SQLite `business_time_events` | 否 | 阶段耗时与看板 |
|
||||
| 演示业务源库 | 多表 SQLite | 先经数据卡映射 | 模拟真实源系统 |
|
||||
|
||||
## 14. 可追溯性:怎样回答“这句话从哪里来”
|
||||
|
||||
### 14.1 原生聊天追溯
|
||||
|
||||
可以查看:
|
||||
|
||||
- 会话消息;
|
||||
- 模型每次请求与响应;
|
||||
- 模型请求了哪个工具及参数;
|
||||
- 工具返回了什么;
|
||||
- token 用量、结束原因和耗时。
|
||||
|
||||
它能解释“模型怎样得到回答”,但自然语言回答本身不一定逐句绑定证据 ID。
|
||||
|
||||
### 14.2 业务预审追溯
|
||||
|
||||
业务链额外固定:
|
||||
|
||||
- 数据快照 ID 和内容哈希;
|
||||
- 数据卡、SQL 配置版本及行哈希;
|
||||
- Skill、规则、模板和经验版本;
|
||||
- 每一步输入哈希、输出和状态;
|
||||
- finding 的 `evidence_ids` 和 `basis_ids`;
|
||||
- 人工确认、退回、批准的操作者与时间;
|
||||
- 报告引用的运行和快照。
|
||||
|
||||
因此业务结论应从项目页面点击证据,而不是去聊天记录里寻找审批依据。
|
||||
|
||||
## 15. 权限、隔离与防重复
|
||||
|
||||
### 15.1 身份与项目范围
|
||||
|
||||
业务 API 根据客户经理、评审员、管理员角色和项目清单授权。Worker 在领取、执行和发布前都会再次检查权限,避免“入队时有权、完成时已无权”仍发布结果。
|
||||
|
||||
### 15.2 演示与生产域
|
||||
|
||||
组件、数据源和方案有 `demo` / `production` 范围,代码阻止混用。凭据不存入组件正文,只允许服务器环境变量引用。
|
||||
|
||||
### 15.3 冻结与版本
|
||||
|
||||
运行开始后使用冻结版本,不自动追随新的 Skill、规则或材料。新数据会增加 `data_revision`,旧运行不能覆盖当前状态。
|
||||
|
||||
### 15.4 幂等与并发
|
||||
|
||||
业务写操作支持幂等键;对象有 revision,旧页面提交会收到冲突并要求刷新。模型并发由信号量限制为 2 个槽位,方案还限制总超时和工具次数。
|
||||
|
||||
## 16. 当前实现边界与需要谨慎表述的地方
|
||||
|
||||
1. **原版 terminal 不是强沙箱。** 它有黑名单和工作目录,但不是容器级隔离。
|
||||
2. **记忆 RAG 当前默认关闭。** `config.json` 中为 `false`,所以当前是整份 `MEMORY.md` 注入。
|
||||
3. **长期记忆并非全自动沉淀。** 提示词可以要求模型写入,但代码没有证明每天自动总结归档。
|
||||
4. **通用知识库当前是向量检索。** 不应声称现状是 BM25 + 向量混合。
|
||||
5. **业务快捷助手不是自由大模型。** 它是有限、可审计的命令识别层。
|
||||
6. **模型不等于审批人。** 业务模型输出核查建议;批准、退回与人工意见由评审员完成。
|
||||
7. **业务模型不能直连数据库。** 它只读冻结后的标准字段和证据。
|
||||
8. **原生聊天与业务快照不是同一套记忆。** 聊天中说过的事实不会自动成为项目事实。
|
||||
9. **RAG 嵌入依赖需实际部署验证。** 代码使用 HuggingFace Embedding,但依赖清单没有明确列出对应 HuggingFace 嵌入包,部署环境需要验证。
|
||||
10. **JSON 会话文件未显示跨进程文件锁。** 多实例部署原版会话层前需要补充一致性方案;兴邦业务状态已使用 SQLite 事务。
|
||||
|
||||
## 17. 用 DEMO-B 串起一条完整演示
|
||||
|
||||
以“恒远设备(DEMO-B)”为例:
|
||||
|
||||
1. 客户经理进入项目,系统读取项目事实和已选来源。
|
||||
2. 数据卡按 `project_id=DEMO-B` 查询业务源库,将财务、租赁物等字段映射到标准字段。
|
||||
3. 客户经理上传设备清单;解析器生成带页/行定位的候选值,不自动覆盖项目。
|
||||
4. 客户经理确认候选的单位、期间和来源,项目 `data_revision` 增加。
|
||||
5. 发起 AI 预审后,系统冻结项目事实、材料、Skills、规则和报告模板,写入任务队列。
|
||||
6. Worker 先做必填、对比、去重和规则计算,再把冻结事实交给受限模型 Agent。
|
||||
7. 模型通过只读工具读取快照、检查和方法,返回结构化 JSON;程序拒绝无证据或越权引用。
|
||||
8. 系统形成问题卡,每个问题可查看数据来源、AI 判断、建议动作、规则/Skill 版本。
|
||||
9. 客户经理补件并重新预审;新运行与旧运行并存,问题状态按证据变化对账。
|
||||
10. 客户经理生成 AI 预审报告并提交,项目进入评审池,记录提交时间和等待时长。
|
||||
11. 评审员领取后使用评审员方案独立复核,逐节点作人工判断。
|
||||
12. 评审员退回时,项目在客户经理工作台重点显示;客户经理补件后重新提交,历史仍保留。
|
||||
13. 评审员批准并生成评审报告;报告可以回到快照、检查和证据定位。
|
||||
|
||||
## 18. 面向小白的术语表
|
||||
|
||||
| 术语 | 在本项目中的通俗解释 |
|
||||
|---|---|
|
||||
| 大模型 / LLM | 根据上下文生成文字或工具调用请求的模型服务 |
|
||||
| Prompt | 送给模型的规则、背景和任务说明 |
|
||||
| Token | 模型读取和生成文字时使用的计量单位,不等于汉字数 |
|
||||
| LangChain | 连接模型、消息与工具的应用开发层 |
|
||||
| LangGraph | 让“模型—工具—模型”循环按状态运行的图执行层 |
|
||||
| Agent | 能根据任务选择是否调用工具、再继续回答的运行体 |
|
||||
| Tool | 由后端真正执行的函数;模型只能请求调用 |
|
||||
| Skill | 方法说明或受控业务能力定义,不天然等于工具权限 |
|
||||
| Rule | 程序按固定条件执行的判断,结果可重复 |
|
||||
| RAG | 先检索相关片段,再把片段加入模型上下文 |
|
||||
| Embedding | 把文字转换为可比较相似度的数值向量 |
|
||||
| Snapshot | 某个时刻冻结的数据副本,后续修改不会改变它 |
|
||||
| Evidence ID | 指向数据卡字段或材料页/行的证据编号 |
|
||||
| SSE | 后端持续向浏览器推送小段事件的流式协议 |
|
||||
| 幂等 | 同一请求重复发送,也只产生一次业务结果 |
|
||||
| Lease | Worker 领取任务的限时占用权,超时可由别的 Worker 恢复 |
|
||||
| Stale | 依赖数据已变化,原结果还可查看但不能当作当前结论 |
|
||||
|
||||
## 19. 阅读代码的推荐顺序
|
||||
|
||||
没有编码经验的同事如果要配合研发核对,可以按以下顺序阅读:
|
||||
|
||||
1. `backend/app.py`:看系统启动了哪些能力;
|
||||
2. `backend/api/chat.py`:看一轮聊天怎样进出;
|
||||
3. `backend/graph/agent.py`:看模型和工具循环;
|
||||
4. `backend/graph/prompt_builder.py`:看模型获得哪些长期规则;
|
||||
5. `backend/tools/`:看原生工具实际做什么;
|
||||
6. `backend/graph/session_manager.py` 与 `memory_indexer.py`:看记忆;
|
||||
7. `backend/services/project_assistant.py`:看业务对象怎样创建;
|
||||
8. `backend/services/assistant_worker.py`:看业务任务怎样逐步执行;
|
||||
9. `backend/services/assistant_model.py`:看业务模型工具与输出校验;
|
||||
10. `backend/services/assistant_store.py`:看 SQLite 对象、事件和任务怎样保存。
|
||||
|
||||
---
|
||||
|
||||
## 20. 最终总结
|
||||
|
||||
这个项目的核心不是“让大模型拥有所有权限”,而是把能力拆成三层:
|
||||
|
||||
1. **模型负责理解和语义判断**;
|
||||
2. **工具与确定性代码负责读取、计算和执行**;
|
||||
3. **快照、证据、版本、权限和人工决定负责把结果变成可审计业务流程**。
|
||||
|
||||
原版 mini-openclaw 提供通用 Agent 基座;兴邦改造没有绕开这个基座,而是在它之上收窄工具、冻结输入、校验输出,并把结果写成可追溯的项目对象。理解这条边界,就能同时理解系统为什么“有 AI 的灵活性”,又为什么在对公评审场景中不能让 AI 自由修改事实或替人审批。
|
||||
@@ -0,0 +1,87 @@
|
||||
# 阶段 ① 需求与规格(Requirement & Spec)
|
||||
|
||||
> **定义**:把用户意图变成一份"实现者(人或 agent)无需猜"的规格文档。规格完备的标志是:实现者不发明任何决策就能把改动做出来。
|
||||
> **输入**:需求描述 / issue / 用户反馈 / 变更请求。
|
||||
> **输出**:规格文档(按下方 7 段结构 + BR/E/AC 编号)。
|
||||
> **进入下一阶段(② 架构设计)**:六道完备性门全部通过。
|
||||
|
||||
---
|
||||
|
||||
## 1. 要求(Requirements)
|
||||
|
||||
### 1.1 进入条件
|
||||
- 需求来源明确(谁提的、要解决什么问题、对哪些角色生效)。
|
||||
- 明确本轮范围 vs 明确不在本轮范围(out-of-scope 与 in-scope 同等重要)。
|
||||
- 单一交付面还是多交付面已确认(web / Electron 主进程 / 硬件网关 / 设置面板……每个交付面都会让规格多一段)。
|
||||
|
||||
### 1.2 完成条件(DoD)
|
||||
- [ ] 七段结构齐全(多交付面则第 3 段重复),每个领域术语都进术语表
|
||||
- [ ] 每条业务规则 `BR-xx` 可测试(能说清"什么输入让它过、什么输入让它挂")
|
||||
- [ ] 每条规则隐含的失败路径都有 `E-xx`,异常"用户可见消息 ↔ 后端错误码"绑定
|
||||
- [ ] 每条 `BR-xx` / `E-xx` 至少被一条 `AC-xx` 引用;验收含正常/异常/边界三类用例
|
||||
- [ ] 无阻塞当前迭代的未决问题(或已把阻塞规则移出范围)
|
||||
- [ ] 决策记录(Decision Record)已写明已定决策与被否掉的替代方案
|
||||
|
||||
---
|
||||
|
||||
## 2. 规则(Rules)
|
||||
|
||||
| 编号 | 规则 |
|
||||
|---|---|
|
||||
| R-1.1 | 行为必须有 id:业务规则 `BR-01`…,异常场景 `E-01`…,验收标准 `AC-01`…。散文式描述不能进入规格。 |
|
||||
| R-1.2 | id 只追加不重排;规则废弃时标记 withdrawn 并保留编号(提交、测试、评审已引用旧 id)。编号按规格独立,不全局递增。 |
|
||||
| R-1.3 | 每条 `BR-*` 只对应**一个**实现点。一条规则要碰三条无关代码路径 = 它其实是三条规则。 |
|
||||
| R-1.4 | 每条 `E-*` 必须绑定"各受影响面展示给用户的消息"与"产生它的后端错误码",前后端不能静默漂移。 |
|
||||
| R-1.5 | 每条 `AC-*` 用 `Given / When / Then` 三元组,且引用它覆盖的 `BR-*`/`E-*` id。无验收引用的规则/异常 = 未验证,阻塞进入实现。 |
|
||||
| R-1.6 | 未决定的事项显式标记(`needs-decision` / open question,带 owner 与期限),禁止用"貌似合理的编造值"填空。 |
|
||||
| R-1.7 | 截图/线框图只表达布局,不替代规则:不得用截图当 spec,除非它同时写清状态转换与权限。 |
|
||||
| R-1.8 | 反模式禁止:只写 happy path(§5/§6 才是实现风险所在)、把字段类型当业务规则(`BR-*` 描述系统**要决定什么**,不是**存什么**)、整段照抄他项目(可复用段清单,内容必须基于本目标事实重写)。 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 输出规范
|
||||
|
||||
### 3.1 规格文档结构(7 段;每多一个交付面,第 3 段重复一次 → 8/9 段)
|
||||
|
||||
```text
|
||||
1. Requirement Overview 业务背景、in-scope / out-of-scope、受影响角色、术语表
|
||||
2. Data Model & Interface 新增/变更的表(列类型、可空性)、服务契约、公开端点(请求/响应形状)、枚举、消息/事件载荷
|
||||
Contract
|
||||
3. Surface Specification 每个交付面:页面骨架、组件粒度、每个有状态视图的状态机、动作列表(含守卫权限)
|
||||
4. Business Rules 编号 BR-01…,每条一条可测试语句
|
||||
5. Exception Scenarios 编号 E-01…,触发条件 + 各面用户可见消息 + 后端错误码
|
||||
6. Acceptance Criteria 编号 AC-01…,按角色分组,Given/When/Then,覆盖正常/异常/边界
|
||||
7. Open Questions & 未决项(owner + needed-by)+ 已定决策与被否方案
|
||||
Decision Record
|
||||
```
|
||||
|
||||
> 参考模板:`vendor/better-harness/references/bootstrap/examples/frontend-web-spec.md`(前端表面)、`backend-service-spec.md`(多交付面)、`mobile-app-spec.md`(离线/权限/版本)。**复用结构,不复用表名/端点/阈值/角色名。**
|
||||
|
||||
### 3.2 六道完备性门(任一不过,禁止进入实现)
|
||||
|
||||
| 门 | 判定 |
|
||||
|---|---|
|
||||
| **Term gate 术语门** | 规则中每个领域术语都在术语表里 |
|
||||
| **Contract gate 契约门** | 表面展示/提交的每个字段都在 §2 里有类型,每个枚举值都列出 |
|
||||
| **Rule gate 规则门** | 每条 `BR-*` 可测试(评审者能说出"输入 X 通过、输入 Y 失败") |
|
||||
| **Exception gate 异常门** | 每条规则隐含的失败路径都有 `E-*`;静默失败是规格缺陷,不是实现细节 |
|
||||
| **Coverage gate 覆盖门** | 每条 `BR-*`/`E-*` 至少被一条 `AC-*` 引用;验收含至少一个边界用例 |
|
||||
| **Decision gate 决策门** | 本轮范围内没有未决问题阻塞规则;要么移出范围,要么解决 |
|
||||
|
||||
### 3.3 交付物清单
|
||||
- 规格文档(本地草稿放 `docs/plans/`,**不入库**,见 `.gitignore`;需入库的规格随 PR 放 `docs/` 对应目录,命名 `<日期>-<主题>-spec.md`)
|
||||
- (可选)给 UI 需求的面:指向 [04-ui-spec.md](04-ui-spec.md) 的表面规格拆分
|
||||
|
||||
---
|
||||
|
||||
## 4. 轻量路径(小改动)
|
||||
|
||||
- 一行修复 / 文案 / 纯内部重构:规格可缩为 PR 描述中的"范围 + BR/AC 各 1-3 条",但 id 与可追溯要求不变。
|
||||
- 无法写清验收标准的改动 = 规格未完成,不得进实现。
|
||||
|
||||
---
|
||||
|
||||
## 5. 相关文档
|
||||
|
||||
- 阶段 ② [02-architecture.md](02-architecture.md)、阶段 ④ [04-ui-spec.md](04-ui-spec.md)
|
||||
- 规格→实现→验证的流程闭环定义见 [README.md](README.md) 全景
|
||||
@@ -0,0 +1,88 @@
|
||||
# 阶段 ② 架构设计(Architecture Design)
|
||||
|
||||
> **定义**:确定系统/改动的分层、模块边界、数据流、依赖方向与关键技术决策,让实现者知道"改哪里、往哪加、不能碰什么"。架构文档回答系统**为什么这样长**;详细设计(阶段 ③)回答**每个零件怎么做**。
|
||||
> **输入**:① 的规格文档(验收标准是架构的约束)。
|
||||
> **输出**:架构文档 + ADR(决策记录)+ 架构图。
|
||||
> **进入下一阶段(③ 详细设计)**:模块边界与关键决策已定案,无未决阻塞项。
|
||||
|
||||
---
|
||||
|
||||
## 1. 要求(Requirements)
|
||||
|
||||
### 1.1 进入条件
|
||||
- 规格已过六道完备性门(见 [01-requirement.md](01-requirement.md) §3.2)。
|
||||
- 涉及跨模块/跨进程/硬件/网关/持久化的改动,必须走完整架构设计;纯单文件小改可走轻量路径。
|
||||
|
||||
### 1.2 完成条件(DoD)
|
||||
- [ ] 分层与依赖方向明确:数据只能往哪个方向流、禁止哪些反向依赖
|
||||
- [ ] 每个模块有唯一 owner 与清晰职责;没有"第二份 owner"(同一概念不复制两处实现)
|
||||
- [ ] 关键决策以 ADR 形式记录(结论 + 被否方案 + 理由),决策号稳定(如 D1/D2)
|
||||
- [ ] 数据流/主链路(含失败路径)有图或文字描述,与现有架构基线(`docs/architecture/architecture-overview.md` 分层图)能对上
|
||||
- [ ] 风险与副作用边界已标注:哪些是高危操作(生产数据、硬件写、外发请求)、默认拒绝还是需审批
|
||||
- [ ] 架构不变量可被机器验证的部分列出(对应 lint/architecture test/契约测试)
|
||||
|
||||
---
|
||||
|
||||
## 2. 规则(Rules)
|
||||
|
||||
| 编号 | 规则 |
|
||||
|---|---|
|
||||
| R-2.1 | **依赖单向**:renderer → preload → IPC handler → domain service → state / Pi runtime;service 不依赖 UI 层与宿主;禁止跨层绕行。本仓现状:`src/main/`(Electron 宿主:index/preload/app-menu/macos/update/host)· `packages/agent-sdk/`(服务主体,零 electron 导入)· `packages/web-ui/`(renderer SPA)· `packages/api/`(dk-api 契约)。 |
|
||||
| R-2.2 | **唯一 owner**:跨进程/跨模块的类型只在共享层定义一次,其余侧只 `import type`;禁止本地镜像同一契约。 |
|
||||
| R-2.3 | **架构不变量写入文档且可验证**:能用测试/静态检查强制的(模块边界、依赖方向、迁移追加、白名单),不许只写文档;验证不了的规则不写进"不可违背"清单。 |
|
||||
| R-2.4 | **决策要留痕**:架构层面的每个取舍写成 ADR(背景/决策/被否方案/影响),不搞口头决定。被否方案是决策记录的一部分。 |
|
||||
| R-2.5 | **分层不做多余抽象**:无第二调用方的接口不抽象;不为想象中的灵活性提前配置化(Karpathy 准则:最小代码解决当前问题)。 |
|
||||
| R-2.6 | **边界即安全**:跨进程 IPC、硬件写、外发请求、文件写入都有明确边界与校验点;高危路径默认拒绝或显式审批(见 [07-delivery.md](07-delivery.md) 高风险审批)。 |
|
||||
| R-2.7 | **架构图遵守作者契约**:用图时按下方 §3.2 的五类图规则(架构/工作流/时序/数据流/生命周期)绘制,只画验证过的事实,不画"可能"的关系。 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 输出规范
|
||||
|
||||
### 3.1 架构文档结构
|
||||
|
||||
```text
|
||||
1. 概览(一屏) 一句话定位 + 分层图 + 本改动在哪个层
|
||||
2. 分层与模块边界 模块清单:职责 / owner / 依赖方向 / 禁止的反向依赖
|
||||
3. 数据模型与持久化 表/文件/状态的归属与迁移策略(追加式迁移,禁改历史)
|
||||
4. 关键流程 主链路 + 失败/恢复路径(任务运行、硬件发现、网关、升级等)
|
||||
5. 接口与集成点 跨进程契约、外部依赖(硬件协议、网关、云服务)、版本约束
|
||||
6. 风险与副作用边界 高危操作清单:触发条件 / 审批点 / 审计点 / 回滚
|
||||
7. 决策记录(ADR) D1/D2…:决策 + 被否方案 + 影响
|
||||
8. 验证方式 架构不变量对应的测试/门禁
|
||||
```
|
||||
|
||||
> 本仓已有基线:`docs/architecture/architecture-overview.md`(分层总览)+ `docs/architecture/codemap.md`(模块地图,头部统计由 `scripts/repo-stats.mjs` 生成)。新架构文档放在 `docs/architecture/`(入库基线)或 `docs/plans/`(本地草稿,不入库,见 `.gitignore`),并保持与基线一致;基线变更要同步。
|
||||
|
||||
### 3.2 架构图规范(五类,摘要)
|
||||
|
||||
| 图类型 | 结构规则 |
|
||||
|---|---|
|
||||
| Architecture | 一条从左到右主干 + 短垂直分支;6–12 个主组件;只按真实归属/信任/进程边界分组;外部角色画在系统边界外 |
|
||||
| Workflow | 泳道=职责/阶段,列=推进;happy path 单调;重试/异常走主走廊外 |
|
||||
| Sequence | 参与者按会话角色排序;消息拥有纵向顺序 |
|
||||
| Dataflow | 阶段=转换/保管;行=并行流;只标注数据契约与跨边界移动 |
|
||||
| Lifecycle | 主相位列 0..4;可恢复失败必须有真实回迁转换 |
|
||||
|
||||
> 构图约定:图以 Mermaid / 文本图为主(见 `docs/architecture/flows.md`、`docs/release-process.md` 既有图例),只画代码检查证实的事实,不画"可能"的关系,并保证 lint/渲染校验可通过。
|
||||
|
||||
### 3.3 交付物清单
|
||||
- 架构文档(含分层/边界/数据流/风险)
|
||||
- ADR(每个决策一个记录,可内嵌架构文档或独立 `docs/adr/`)
|
||||
- 架构图(适用时)
|
||||
- 架构不变量对应的机器验证清单(测试/静态检查名)
|
||||
|
||||
---
|
||||
|
||||
## 4. 轻量路径(小改动)
|
||||
|
||||
- 单模块内、无跨进程/持久化/外部的改动:架构要求并入详细设计开头一小节(边界 + owner + 依赖方向),不单独成文。
|
||||
- 改动触碰 `packages/api/src/contracts.ts`(跨进程契约)、IPC、迁移、安全边界时,**必须**走完整架构设计。
|
||||
|
||||
---
|
||||
|
||||
## 5. 相关文档
|
||||
|
||||
- 输入:阶段 ① [01-requirement.md](01-requirement.md)
|
||||
- 输出到:阶段 ③ [03-detailed-design.md](03-detailed-design.md)
|
||||
- 变更矩阵(架构文档随代码同步):[05-implementation.md](05-implementation.md) §3.3
|
||||
@@ -0,0 +1,79 @@
|
||||
# 阶段 ③ 详细设计(Detailed Design)
|
||||
|
||||
> **定义**:把架构落到可实现的粒度——每个接口的签名与契约、数据结构与迁移、视图状态机、异常处理、边界条件。详细设计完成后,实现者不需要再做任何设计决策,只需照做。
|
||||
> **输入**:① 规格 + ② 架构(模块边界、owner、契约归属)。
|
||||
> **输出**:详细设计文档(接口/数据/状态机/异常/边界)+ 可落地的契约声明(类型定义、表结构草案)。
|
||||
> **进入下一阶段(④ UI Spec / ⑤ 实现)**:接口契约与数据契约已定案,边界与异常已覆盖。
|
||||
|
||||
---
|
||||
|
||||
## 1. 要求(Requirements)
|
||||
|
||||
### 1.1 进入条件
|
||||
- 架构已定案(无阻塞性未决决策)。
|
||||
- 规格的 `BR-*`/`E-*`/`AC-*` 全部可映射到具体接口/数据/状态。
|
||||
|
||||
### 1.2 完成条件(DoD)
|
||||
- [ ] 每个公开接口(函数/方法/事件/IPC 通道/硬件协议命令)有签名、入参校验规则、返回与错误语义
|
||||
- [ ] 每个数据结构(表/文件/状态对象)有字段、类型、可空性、owner;变更走追加式迁移且有守卫
|
||||
- [ ] 每个有状态视图/流程有状态机(状态全集 + 合法转换 + 每个状态的禁用手势)
|
||||
- [ ] 规格中每条 `E-*` 都能落到具体错误码 + 处理路径(重试/降级/回滚)
|
||||
- [ ] 边界条件显式列出:空输入、上限、并发、超时、权限缺失、版本不兼容
|
||||
- [ ] 所有契约与规格 id 可双向追溯(实现点 ↔ BR/E/AC)
|
||||
|
||||
---
|
||||
|
||||
## 2. 规则(Rules)
|
||||
|
||||
| 编号 | 规则 |
|
||||
|---|---|
|
||||
| R-3.1 | **契约先行且单一**:接口与数据契约在详细设计中定案,实现只落契约不发明契约;跨进程契约只放共享层一处(G-1)。 |
|
||||
| R-3.2 | **入参显式校验**:每个对外接口(IPC、命令、硬件写入、外部请求)的入参都有校验(类型/枚举/边界),禁止裸用;"无入参"是唯一例外。通用校验器不得被滥用成"非空字符串检查"。 |
|
||||
| R-3.3 | **状态机完备**:状态全集含加载/空/错误/提交中等"非 happy path"状态;每个状态定义允许的动作与禁用手势。折叠状态机(如把 Loading 与 Submitting 合并成一个布尔)会丢失关键行为,禁止。 |
|
||||
| R-3.4 | **异常必有归属**:每条失败路径有明确处理语义(重试上限、降级、回滚、幂等)与错误码;静默失败是设计缺陷。 |
|
||||
| R-3.5 | **迁移只追加**:持久化结构变更只能追加带守卫的迁移(版本号递增 + 幂等守卫),禁止修改历史迁移;迁移只跑一次。 |
|
||||
| R-3.6 | **边界显式**:空输入、数量上限、并发/竞态、超时、凭据缺失、旧版本兼容等边界条件写成条目,不得留给实现时"顺手处理"。 |
|
||||
| R-3.7 | **单实现点**:一条规则/一个行为只有一个实现点(继承规格 R-1.3);发现需要多处实现,回到规格拆分。 |
|
||||
| R-3.8 | **详细设计可评审**:设计文档要能被实现者之外的人评审出"漏了 X 状态/缺了 Y 错误码",否则说明粒度还不够。 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 输出规范
|
||||
|
||||
### 3.1 详细设计文档结构
|
||||
|
||||
```text
|
||||
1. 范围与本设计覆盖的规格 id BR-xx / E-xx / AC-xx 清单(双向追溯表)
|
||||
2. 接口设计 每个接口:签名 / 入参校验规则 / 返回 / 错误语义 / owner
|
||||
3. 数据设计 表/文件/对象:字段、类型、可空性、owner、迁移草案(只追加)
|
||||
4. 状态机与流程 有状态视图/流程的状态机表(状态 × 转换 × 动作 × 守卫)
|
||||
5. 异常与错误码 错误码表:码 / 触发条件 / 用户可见消息 / 处理路径(重试/降级/回滚/幂等)
|
||||
6. 边界条件清单 空输入 / 上限 / 并发 / 超时 / 权限 / 版本兼容
|
||||
7. 测试映射 每个 AC-xx 对应的测试点(单测/集成/E2E 层级建议)
|
||||
8. 未决项 遗留问题(owner + needed-by),或已解决的决策记录
|
||||
```
|
||||
|
||||
### 3.2 契约声明(可落地的部分)
|
||||
- 类型定义:跨进程契约先写在 `packages/api/src/contracts.ts`(唯一事实源),宿主与各包只 `import type`;包内契约就近在包内共享层声明。
|
||||
- 数据结构草案:DDL/迁移骨架,标注幂等守卫。
|
||||
- 状态机表:Markdown 表格即可,复杂流程可配 sequence 图(见 [02-architecture.md](02-architecture.md) §3.2)。
|
||||
|
||||
### 3.3 交付物清单
|
||||
- 详细设计文档(本地草稿 `docs/plans/`(不入库,见 `.gitignore`),需入库随 PR 放 `docs/` 对应目录)
|
||||
- 契约草案(类型/迁移/错误码表)
|
||||
- 测试映射表(为 ⑥ 验证做准备)
|
||||
|
||||
---
|
||||
|
||||
## 4. 轻量路径(小改动)
|
||||
|
||||
- 单接口小改:契约 + 边界 + 测试映射合并为设计文档中的 2-3 小节。
|
||||
- 触碰共享契约、迁移、状态机、硬件协议:必须完整走详细设计。
|
||||
|
||||
---
|
||||
|
||||
## 5. 相关文档
|
||||
|
||||
- 输入:阶段 ① [01-requirement.md](01-requirement.md)、阶段 ② [02-architecture.md](02-architecture.md)
|
||||
- UI 相关设计拆到:阶段 ④ [04-ui-spec.md](04-ui-spec.md)
|
||||
- 实现时按此设计编码:阶段 ⑤ [05-implementation.md](05-implementation.md)
|
||||
@@ -0,0 +1,116 @@
|
||||
# 阶段 ④ UI Spec — UI 规则定义与约束规范
|
||||
|
||||
> **定义**:定义每个交付面的 UI 行为与视觉约束,让 UI 实现者(人或 agent)不发明任何决策:页面骨架、组件粒度、状态机、动作权限由**表面规格(Surface Specification)** 规定;颜色/字号/间距/组件形态由**设计系统契约(DESIGN.md)** 规定。
|
||||
> **输入**:① 规格 §3(Surface Specification)+ ②/③ 的接口契约(UI 消费的字段)。
|
||||
> **输出**:表面规格 + DESIGN.md(含 token)+ i18n 约束。
|
||||
> **进入下一阶段(⑤ 实现)**:状态机、动作权限、设计 token、i18n 全部定案。
|
||||
|
||||
> 适用判定:本阶段仅当改动拥有用户可见 UI(web 界面、Electron 窗口、设置面板、硬件设备界面)时强制;纯后端/网关/CLI 改动标记 `not applicable`。
|
||||
|
||||
---
|
||||
|
||||
## 1. 要求(Requirements)
|
||||
|
||||
### 1.1 进入条件
|
||||
- 规格 §3 已定义交付面清单与每个面的消费字段(字段有类型、可空性、枚举值)。
|
||||
- 项目已有或需要建立视觉事实源(`DESIGN.md` / 主题变量 / 组件库)。
|
||||
|
||||
### 1.2 完成条件(DoD)
|
||||
- [ ] 每个交付面有:页面骨架 + 组件粒度边界 + 完整状态机 + 动作×权限表
|
||||
- [ ] 每个 UI 提交/展示的字段能在规格 §2 找到(契约门)
|
||||
- [ ] 视觉 token(颜色/字号/间距/圆角/组件)有语义化来源,不出现临时发明的 `blue500` 类裸色
|
||||
- [ ] 全部 UI 文案走 i18n(本仓:`packages/web-ui/src/i18n/messages.ts`,zh-CN/en-US 双语),无硬编码用户可见字符串
|
||||
- [ ] 可访问性约束写明:对比度(WCAG AA)、focus-visible、颜色不单独承载状态、reduced-motion
|
||||
- [ ] 异常态(加载/空/错误/禁用/提交中)有明确视觉与文案定义(对应规格 `E-*`)
|
||||
|
||||
---
|
||||
|
||||
## 2. 规则(Rules)
|
||||
|
||||
### 2.1 表面规格规则(Surface Specification)
|
||||
|
||||
| 编号 | 规则 |
|
||||
|---|---|
|
||||
| R-4.1 | **骨架先行**:每个交付面先定页面骨架(区域划分),再定组件,禁止直接进入像素级布局。 |
|
||||
| R-4.2 | **组件粒度即所有权边界**:明确"哪个组件拥有哪个状态"(如筛选条是查询条件的唯一事实源、表格不持有选择集)。组件不得越界:表格不能清空选择,只能向拥有者发请求。 |
|
||||
| R-4.3 | **状态机完整**:`Idle/Loading/Ready/Empty/Error/Submitting` 等状态全列;`Loading`(骨架屏)与 `Submitting`(保留现有内容+禁用交互)是不同状态,不得合并。 |
|
||||
| R-4.4 | **动作×权限表**:每个动作列角色与禁用条件(如"Auditor 看到页面但没有动作控件,而非禁用按钮")。 |
|
||||
| R-4.5 | **交互约束显式**:URL 持有可分享状态、筛选变更清空选择、选择集不跨页、破坏性操作需确认且写明数量——写成条目,不留给实现。 |
|
||||
| R-4.6 | **截图不是 spec**:图片可表达布局,但必须同时有规则、状态转换、权限(R-1.7 在 UI 层同样生效)。 |
|
||||
|
||||
### 2.2 设计系统规则(DESIGN.md / 视觉 token)
|
||||
|
||||
| 编号 | 规则 |
|
||||
|---|---|
|
||||
| R-4.7 | **视觉事实源唯一**:有 UI 的项目维护唯一的视觉 token 事实源(本仓现状:`packages/web-ui/src/theme/tokens.css`,语义 token + CSS 变量,经 `styles.css` `@import` 消费;若将来建根级 `DESIGN.md` 则与之对齐);禁止在组件里临时发明颜色/间距/字号。 |
|
||||
| R-4.8 | **语义 token 优先**:token 用语义角色(`primary`/`surface`/`danger`),不用调色板名(`teal-500`);组件 token 带状态(如 `button-primary-hover`、`text-input-focused`)。 |
|
||||
| R-4.9 | **token 可导出可防漂移**:token 能导出为 CSS 变量/主题,并用 lint/diff/单测检查漂移(本仓:`tokens.css` 变量 + `styles.test.ts` 等单测守卫;`useAppearance.ts` 归一化主题 id);"文件存在"不等于"被使用"(G-3)。 |
|
||||
| R-4.10 | **可访问性硬约束**:正文与 focus 态满足 WCAG AA 对比度;错误/成功/选中状态不得只靠颜色表达;支持 reduced-motion;表单与错误态有明确指引。 |
|
||||
| R-4.11 | **不强推框架外规范**:后端/CLI/纯数据项目标记 `not applicable`,不套用视觉契约。 |
|
||||
|
||||
### 2.3 i18n 规则
|
||||
|
||||
| 编号 | 规则 |
|
||||
|---|---|
|
||||
| R-4.12 | 所有用户可见文案走 key 化翻译(本仓:`packages/web-ui/src/i18n/messages.ts`,扁平 key + `t(key, params?)`,key 按命名空间组织:`common.*`/`task.*`/`settings.*`/`shell.*`/`inspector.*`/`mode.*`);禁止硬编码中文/英文串。 |
|
||||
| R-4.13 | 多语言文案与 UI 状态同步维护;新增文案必须同时补齐既有语言包。 |
|
||||
| R-4.14 | 设备端/子窗口界面不做 i18n 决策,文案由宿主统一注入(避免每窗口维护语言状态)。 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 输出规范
|
||||
|
||||
### 3.1 表面规格结构(每个交付面一份)
|
||||
|
||||
```text
|
||||
Skeleton 页面骨架:区域划分与信息层级
|
||||
Component 组件粒度:组件清单 + 各自拥有的状态 + 不允许越界的边界
|
||||
Granularity
|
||||
State Machine 状态全集 + 合法转换 + 每个状态的禁用手势(表格或图)
|
||||
Actions & Guards 动作 × 角色 × 禁用条件(含"只读角色无控件而非禁用")
|
||||
Interaction 交互约束:URL 状态、选择集规则、确认对话框、虚拟化/固定行高、截断策略
|
||||
Constraints
|
||||
```
|
||||
|
||||
> 模板参考:`vendor/better-harness/references/bootstrap/examples/frontend-web-spec.md` 的 §3(复用结构,内容必须基于本仓事实)。
|
||||
|
||||
### 3.2 DESIGN.md 结构
|
||||
|
||||
```text
|
||||
frontmatter(机器可读) colors / typography / rounded / spacing / components
|
||||
(顶层组,语义角色 + 组件状态 token,支持 {colors.primary} 引用)
|
||||
Markdown(人读) Overview / Colors / Typography / Layout / Elevation & Depth /
|
||||
Shapes / Components / Do's and Don'ts
|
||||
+ 可访问性(对比度、focus-visible、color-not-alone、reduced-motion)
|
||||
+ 每个 token 的"何时用 / 何时不用 / 防止什么 agent 错误"
|
||||
```
|
||||
|
||||
> 完整示例:`vendor/better-harness/case-studies/project-harness/design-md-complete-example.md`(结构性回退模板,数值必须替换为本仓已核实的来源)。若项目尚无 DESIGN.md:从现有代码/主题推断并标注"推断",未决品牌决策标记 `needs-design-decision`,禁止编造。
|
||||
|
||||
### 3.3 交付物清单
|
||||
- 每个交付面的表面规格
|
||||
- `DESIGN.md`(若建)或其缺失项清单;本仓现状:视觉 token 事实源为 `packages/web-ui/src/theme/tokens.css`(含证据分级:Present / Structured / Token-ready / Agent-ready / Aligned / Backstopped)
|
||||
- i18n key 清单(新增/变更)
|
||||
- UI 验收映射:表面规格的每条交互约束 ↔ 规格 `AC-xx` ↔ [06-validation.md](06-validation.md) 的测试点(组件/E2E)
|
||||
|
||||
---
|
||||
|
||||
## 4. UI 验证约束(预置,详见阶段 ⑥)
|
||||
|
||||
- UI 验证分驱动平面(D0 进程内渲染树 → D1 浏览器自动化协议 → D2 附加运行中浏览器 → D3 OS 输入/无障碍 → D4 物理设备),选满足声明的最便宜平面,失败再升级。
|
||||
- **驱动与观察分离**:同一模型既驱动 UI 又目测自己的截图 = 无独立裁判,不能算 pass(`Self-reported success` 反模式)。截图基线只追加不覆盖。
|
||||
- 定位器禁止跨动作缓存(元素索引/坐标每次交互前重新读取);降级顺序:语义选择器 → 文本/aria → 原始坐标,并记录最终落到哪一级。
|
||||
|
||||
---
|
||||
|
||||
## 5. 轻量路径(小改动)
|
||||
|
||||
- 单组件样式调整:更新 DESIGN.md 对应 token + 表面规格的组件小节,不重写整份规格。
|
||||
- 触碰多交付面、状态机重构、主题切换:必须完整走本阶段。
|
||||
|
||||
---
|
||||
|
||||
## 6. 相关文档
|
||||
|
||||
- 输入:阶段 ① [01-requirement.md](01-requirement.md) §3、阶段 ③ [03-detailed-design.md](03-detailed-design.md)
|
||||
- 验证:阶段 ⑥ [06-validation.md](06-validation.md)(UI 驱动平面与裁判分离约束)
|
||||
@@ -0,0 +1,107 @@
|
||||
# 阶段 ⑤ 实现(Implementation)
|
||||
|
||||
> **定义**:小步、可验证、范围受控地把设计变成代码。实现期的核心不是"写得多",而是**每一步都有验证、每个改动都可追溯到请求、不越界、不留孤儿**。
|
||||
> **输入**:① 规格 + ② 架构 + ③ 详细设计(+ ④ UI Spec)。
|
||||
> **输出**:代码 + colocated 测试 + 变更同步(文档/契约)。
|
||||
> **进入下一阶段(⑥ 验证与质量门)**:编码规则满足、自测通过、变更矩阵已同步。
|
||||
|
||||
---
|
||||
|
||||
## 1. 要求(Requirements)
|
||||
|
||||
### 1.1 进入条件
|
||||
- 规格完备性门、架构/详细设计决策均已定案(小改动走轻量路径也必须有范围 + 验收标准)。
|
||||
- 涉及 UI 的改动已有 ④ 的表面规格与 token。
|
||||
|
||||
### 1.2 完成条件(DoD)
|
||||
- [ ] 每个提交只做一件事(Conventional Commits),变更可逐行追溯到请求范围
|
||||
- [ ] 新行为有 colocated 测试(与源码同目录 `*.test.ts`,vitest;E2E 在 `e2e/`,playwright。本仓测试体系见 `docs/architecture/testing-architecture.md`)
|
||||
- [ ] 无未使用 import/变量/函数(由本次改动产生的孤儿已清理;存量死代码只提不改,G-4)
|
||||
- [ ] 变更矩阵已同步(见 §3.3):契约、文档、i18n、迁移、架构图
|
||||
- [ ] 本阶段自测(lint/typecheck/相关单测)全绿
|
||||
|
||||
---
|
||||
|
||||
## 2. 规则(Rules)
|
||||
|
||||
### 2.1 编码行为
|
||||
|
||||
| 编号 | 规则 |
|
||||
|---|---|
|
||||
| R-5.1 | **想清楚再写**:假设显式化;多种解释并存时先摆出来,不静默选择;不确定就停,说出困惑点,问。 |
|
||||
| R-5.2 | **最小代码**:解决当前问题的代码,不做投机抽象/灵活配置/为不可能场景写错误处理;200 行能解决的不要写 500 行。 |
|
||||
| R-5.3 | **外科手术式改动**:只碰必须碰的;不顺手"改进"相邻代码/注释/格式;不重构没坏的东西;风格跟随现有代码。 |
|
||||
| R-5.4 | **规模红线**:新文件 ≤500 行;既有大文件只减不增;新增依赖必须先评估体积与必要性(依赖红线见仓库约定)。 |
|
||||
| R-5.5 | **状态持久化白名单**:新增受管写入路径须进白名单并说明;禁止绕过既有持久化/校验路径自行写文件。 |
|
||||
| R-5.6 | **小步提交**:一个提交一个意图;变更混杂时先拆分再提交(见 `skills/builtin/dk-commit` 的提交规范)。 |
|
||||
|
||||
### 2.2 安全与副作用(实现期强制)
|
||||
|
||||
| 编号 | 规则 |
|
||||
|---|---|
|
||||
| R-5.7 | 所有副作用入口(文件写、命令执行、外发请求、硬件写、IPC)走统一受控路径,禁止平行执行路径。 |
|
||||
| R-5.8 | 项目目录写入/命令必须有归属校验 + 审计;凭据/配置保护区一律拒绝。 |
|
||||
| R-5.9 | 日志/输出禁止出现凭据任何片段(脱敏);禁止 `nodeIntegration: true`、禁止绕过 preload/安全边界暴露主机能力。 |
|
||||
| R-5.10 | 高危动作(删除、生产数据、外发、发布)默认拒绝或显式审批,先于副作用发生,且留审计(详见 [07-delivery.md](07-delivery.md))。 |
|
||||
|
||||
### 2.3 契约与同步
|
||||
|
||||
| 编号 | 规则 |
|
||||
|---|---|
|
||||
| R-5.11 | 跨进程/跨模块契约只在共享层一处定义(G-1),实现只落契约不发明契约。 |
|
||||
| R-5.12 | 每个对外接口入参显式校验(继承 R-3.2)。 |
|
||||
| R-5.13 | 实现点 ↔ 规格 id(BR/E/AC)双向可追溯:改一个行为必须同步它的验收/异常/契约。 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 输出规范
|
||||
|
||||
### 3.1 交付物
|
||||
- 源码改动(每行可追溯)
|
||||
- colocated 测试(新行为 + 修复回归)
|
||||
- 变更同步产物(契约/迁移/文档/i18n)
|
||||
|
||||
### 3.2 提交规范(本仓沿用 `AGENTS.md` 规则 6 + Conventional Commits;生成建议见 `skills/builtin/dk-commit`)
|
||||
```text
|
||||
<type>(<scope>): <subject> # subject 简洁中文,≤50 字
|
||||
# 正文:英文完整内容 + `---` 分隔 + 中文完整内容
|
||||
# (不复述 diff;主分支是 build)
|
||||
type: feat | fix | docs | refactor | test | chore | build
|
||||
```
|
||||
|
||||
### 3.3 变更矩阵(改了什么,必须同步什么)
|
||||
|
||||
| 变更 | 必须同步 |
|
||||
|---|---|
|
||||
| 跨进程契约 / IPC 通道 / 接口 | `packages/api/src/contracts.ts`(唯一入口)+ `docs/api/ipc-api-reference.md` + preload/handler 对齐(`src/main/ipc-alignment.test.ts` 拦截) |
|
||||
| 模块增删 / 职责变化 | `docs/architecture/codemap.md`(头部统计跑 `node scripts/repo-stats.mjs`,`docs-baseline.test.ts` 拦截)+ 架构文档 |
|
||||
| 数据结构 / 迁移 | 迁移文件(只追加,`packages/agent-sdk/src/state/migrations.ts`)+ `docs/architecture/data-model.md` |
|
||||
| UI 文案 / 状态机 / 组件 | i18n key(`packages/web-ui/src/i18n/messages.ts`)+ 视觉 token(`packages/web-ui/src/theme/tokens.css`)+ 表面规格 |
|
||||
| 主链路 / 安全边界 | `docs/architecture/flows.md` + 相关安全测试(tool-policy/tool-gate/redact) |
|
||||
| E2E 行为 / 运行方式 | E2E 测试与自动化文档(`docs/e2e/e2e-architecture-and-automation.md`)+ playwright |
|
||||
| 宿主形态 / 接入方式 | `docs/architecture/host-modes.md` |
|
||||
|
||||
---
|
||||
|
||||
## 4. 反模式(实现期高频错误,摘自 Karpathy 准则)
|
||||
|
||||
- **先写后想**:不问假设、隐藏困惑、静默选一种解释。
|
||||
- **过度设计**:为单次使用写抽象、为没要求的场景加"灵活性"。
|
||||
- **顺手重构**:改 bug 时顺带"美化"周边代码 → diff 失控、评审失效。
|
||||
- **无验证前进**:写一大片再统一跑一次 → 失败定位成本爆炸。
|
||||
- **留下孤儿**:改动删掉了某函数/变量/import 却不清理由本次改动产生的悬空引用。
|
||||
|
||||
---
|
||||
|
||||
## 5. 轻量路径(小改动)
|
||||
|
||||
- 一行修复/文案:单提交 + 最小测试,直接走本阶段规则不产额外文档。
|
||||
- 但 R-5.2~R-5.13 的安全/契约/同步规则对小改动同样强制。
|
||||
|
||||
---
|
||||
|
||||
## 6. 相关文档
|
||||
|
||||
- 编码前确认设计:阶段 ③ [03-detailed-design.md](03-detailed-design.md)、阶段 ④ [04-ui-spec.md](04-ui-spec.md)
|
||||
- 自测通过后进入:阶段 ⑥ [06-validation.md](06-validation.md)
|
||||
- 提交与审查技能:`skills/builtin/dk-commit`、`skills/builtin/dk-review`
|
||||
@@ -0,0 +1,106 @@
|
||||
# 阶段 ⑥ 验证与质量门(Validation & Quality Gates)
|
||||
|
||||
> **定义**:用**最小且与最终改动相关**的检查验证结果:每个改动映射到覆盖其行为/不变量/风险的检查并实际执行;失败必须可复现、可定位、有因果诊断;修复后**同一检查**复验。验证在真实交付边界之外只算"局部证据"。
|
||||
> **输入**:⑤ 的实现产物(diff + 自测结果)+ ① 的验收标准(AC 映射)。
|
||||
> **输出**:验证记录(检查身份 + argv/cwd + 结果 + 覆盖的不变量)、审查结论。
|
||||
> **进入下一阶段(⑦ 交付与发布)**:全部质量门通过、审查无阻塞项。
|
||||
|
||||
---
|
||||
|
||||
## 1. 要求(Requirements)
|
||||
|
||||
### 1.1 进入条件
|
||||
- 实现已满足 ⑤ 的 DoD(自测全绿、变更矩阵已同步)。
|
||||
|
||||
### 1.2 完成条件(DoD)
|
||||
- [ ] 每个实质改动映射到最小且覆盖它的项目检查(单测/契约/集成/E2E/视觉/迁移/打包之一),并在最后一次编辑后运行
|
||||
- [ ] 验证失败按"复现 → 定位(可归因的诊断)→ 因果假设 → 最小 owner 修复"链处理;重试不加诊断 ≠ 修复证据
|
||||
- [ ] 修复后在最终状态上**重跑同一检查**(或记录等价性理由)
|
||||
- [ ] 评审(dk-review 四维)完成:正确性/一致性/安全/测试,无未解决的阻塞项
|
||||
- [ ] 门禁结果与最终修订绑定(不是"跑过"而是"这个 revision 过了")
|
||||
|
||||
---
|
||||
|
||||
## 2. 规则(Rules)
|
||||
|
||||
### 2.1 验证选择
|
||||
|
||||
| 编号 | 规则 |
|
||||
|---|---|
|
||||
| R-6.1 | **相关验证**:最终改动必须被最小且直接覆盖其行为的检查验证;跑一个无关的大套件或陈旧结果不算(旧结果、全量套件、agent 一句"done"都不是验证证据)。 |
|
||||
| R-6.2 | **分层选择**:按"快/慢、便宜/昂贵"分层(lint/typecheck/单测 → 契约/集成 → E2E → 打包冒烟),agent 能按改动选择正确层;慢 e2e 本身不坏,坏的是它成为唯一反馈。 |
|
||||
| R-6.3 | **失败的顺序链**:`failure → reproduction → diagnosis → bounded repair`,缺一不可;一条重试就过了但没有诊断的修复,不构成修复证据。 |
|
||||
| R-6.4 | **修复后复验**:同一检查(或证明覆盖同一行为与范围的等价检查)在修复后的最终状态重跑;无失败无修复则记 `N/A` 并附相关验证结果。 |
|
||||
| R-6.5 | **验证是机器事实**:命令身份(argv/cwd)、fixture、修订号、结果、覆盖的不变量都要保留;CI 身份与本地等价性记录在案。 |
|
||||
|
||||
### 2.2 质量门(Quality Gates)
|
||||
|
||||
| 编号 | 规则 |
|
||||
|---|---|
|
||||
| R-6.6 | **机制强制优先于文档**:架构/安全/schema/迁移/生成物漂移等规则用测试与静态检查强制(architecture test、契约 diff、迁移 lint、生成物 drift check),只写进文档不算门禁。 |
|
||||
| R-6.7 | **门禁与修订绑定**:必须过的检查(lint/typecheck/单测/关键 E2E/构建)在合并前对最终修订全绿;绕过门禁必须有记录的理由。 |
|
||||
| R-6.8 | **测试断言真实行为**:测试断言行为与不变量,不只是冒烟(跑通≠通过);live/真实设备用例必须有 skip 守卫,禁止依赖本机路径/凭据。 |
|
||||
| R-6.9 | **UI 验证裁判独立**:驱动与观察分离;同一模型驱动又目测自己的截图不算 pass(详见 [04-ui-spec.md](04-ui-spec.md) §4)。 |
|
||||
|
||||
### 2.3 评审
|
||||
|
||||
| 编号 | 规则 |
|
||||
|---|---|
|
||||
| R-6.10 | 评审顺序:正确性(边界/错误处理/并发/资源泄漏)→ 一致性(风格/命名/契约同步)→ 安全(注入面/密钥落日志)→ 测试(新行为有测试锁定)。 |
|
||||
| R-6.11 | 只报有证据的问题,按严重度分组(阻塞/建议/可选),每条带 `文件:行号` 与可执行建议;没有问题的维度明确说"未发现问题"。 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 输出规范
|
||||
|
||||
### 3.1 验证记录模板
|
||||
|
||||
```text
|
||||
改动(diff/修订号)
|
||||
检查清单: 检查身份 | 命令(argv/cwd) | 覆盖的不变量 | 结果 | 时间
|
||||
失败记录: 失败检查 → 复现步骤 → 诊断/日志身份 → 因果 → 修复 diff → 复验结果
|
||||
残余风险: 未覆盖范围、跳过守卫的用例、需人工确认项
|
||||
```
|
||||
|
||||
### 3.2 验证分层建议(本仓)
|
||||
|
||||
| 层 | 内容 | 触发 |
|
||||
|---|---|---|
|
||||
| L0 静态 | typecheck(`npm run typecheck`:tsc --noEmit + 双 tsconfig) | 每次提交 |
|
||||
| L1 单测 | 组件/工具/逻辑(vitest,colocated `*.test.ts`,`npm test`) | 每次提交 |
|
||||
| L2 契约/守卫 | 跨进程契约对齐(`src/main/ipc-alignment.test.ts`)、迁移守卫(`migrations.test.ts`)、架构守卫(`architecture-guardrails.test.ts`)、docs 基线 | 触碰共享层/迁移 |
|
||||
| L3 集成 | 主进程+渲染、宿主装配 | 主链路 |
|
||||
| L4 E2E | `npm run test:e2e`(playwright,需先构建) | 关键用户流/发布前 |
|
||||
| L5 打包冒烟 | `build/ci-local.ps1`(Windows)/ `build/ci-local.sh`(macOS/Linux):干净安装+全量门禁+安装包+启动冒烟 | 发布前 |
|
||||
|
||||
> 本仓测试体系单一事实源:`docs/architecture/testing-architecture.md`;E2E 执行线见 `docs/e2e/e2e-architecture-and-automation.md`。验证要求按此落地,发现矩阵缺口在阶段 ⑧ 反馈。
|
||||
|
||||
### 3.3 交付物清单
|
||||
- 验证记录(含失败→修复→复验链)
|
||||
- 审查结论(dk-review 输出,阻塞项清零)
|
||||
- 测试覆盖增量说明(新增测试对应的 AC/BR/E)
|
||||
|
||||
---
|
||||
|
||||
## 4. 反模式
|
||||
|
||||
- **陈旧/无关验证**:拿旧结果或全量套件当"这次改动"的证据。
|
||||
- **只重试不诊断**:失败重跑过了就提交,没有定位因果。
|
||||
- **修完不复验**:修复可见,但最终修订上没有任何同一检查的重跑。
|
||||
- **把本地测试当交付证据**:真实接受边界(CI/合并/发布)未被打开时,用 `Unobserved` 而非编造缺失的 PR/部署。
|
||||
- **裁判即选手**:模型目测自己生成/操作的 UI。
|
||||
|
||||
---
|
||||
|
||||
## 5. 轻量路径(小改动)
|
||||
|
||||
- 一行修复:对应单测 + 全量 typecheck + 该模块相关测试,记录即可。
|
||||
- 任何改动都不得跳过 R-6.3/R-6.4(诊断与复验)与 R-6.7(门禁)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 相关文档
|
||||
|
||||
- 输入:阶段 ⑤ [05-implementation.md](05-implementation.md)、规格 [01-requirement.md](01-requirement.md) 的 AC
|
||||
- UI 验证细则:阶段 ④ [04-ui-spec.md](04-ui-spec.md) §4
|
||||
- 通过后进入:阶段 ⑦ [07-delivery.md](07-delivery.md)
|
||||
@@ -0,0 +1,143 @@
|
||||
# 阶段 ⑦ 交付与发布(Delivery & Release)
|
||||
|
||||
> **定义**:让当前结果在**真实交付边界**被接受——真实的评审、必需的 CI、合并、发布——且高风险动作有风险匹配的审批、副作用有可用的回滚/恢复路径。本地测试与 agent 一句"done"属于阶段 ⑥,不属于交付。
|
||||
> **输入**:⑥ 已通过的实现(验证记录 + 门禁结果 + 审查结论)。
|
||||
> **输出**:已接受的修订 + 构建/安装包 + 发布记录 + 审计。
|
||||
> **进入下一阶段(⑧ 学习与沉淀)**:交付边界接受完成、发布记录落档。
|
||||
|
||||
---
|
||||
|
||||
## 1. 要求(Requirements)
|
||||
|
||||
### 1.1 进入条件
|
||||
- 阶段 ⑥ 全部质量门通过,无未解决阻塞项。
|
||||
|
||||
### 1.2 完成条件(DoD)
|
||||
- [ ] 真实接受边界(合并/发布/部署)绑定到**当前修订**,有修订级决策证据(不是"跑过 CI"而是"这个 revision 合并了/发布了")
|
||||
- [ ] 每个高风险动作(发布、删除、生产数据、外发、签名)在副作用**之前**获得所需审批并留审计
|
||||
- [ ] 实际副作用有风险匹配的回滚/恢复路径(回滚、恢复、重试、补偿、幂等重放、安全中止之一)且含 owner 与后置条件
|
||||
- [ ] 发布记录(版本、变更、产物、验证)落档
|
||||
|
||||
---
|
||||
|
||||
## 2. 规则(Rules)
|
||||
|
||||
### 2.1 接受边界
|
||||
|
||||
| 编号 | 规则 |
|
||||
|---|---|
|
||||
| R-7.1 | **真实边界证据**:把真实接受决策绑定到当前修订与目标分支/环境。本地测试、自测、agent 完成声明均不是交付证据;外部边界打不开时记 `Unobserved`,不编造缺失的 PR/部署。 |
|
||||
| R-7.2 | **合并路径**:改动经默认分支可比较的 diff 进入(分支/PR 或等价受控路径);直接在默认分支开发、无可评审 base diff 属交付缺陷。 |
|
||||
| R-7.3 | **必需检查绑定**:合并前必需检查对最终修订全绿,且与本地命令一致或有明确差异说明。 |
|
||||
|
||||
### 2.2 高风险审批
|
||||
|
||||
| 编号 | 规则 |
|
||||
|---|---|
|
||||
| R-7.4 | 破坏性/特权/外部/不可逆/共享状态/凭据/发布/生产动作,在效果发生**前**取得所需的人或策略 owner 决策;安全拒绝 + 可用升级路径是正确的控制行为,不是失败。 |
|
||||
| R-7.5 | 高风险动作留审计:谁、何时、什么、批准人/策略、审计引用。 |
|
||||
| R-7.6 | 确认无高风险动作时记 `N/A`(须经检查证明),不允许无理由的 N/A 阻塞结果。 |
|
||||
|
||||
### 2.3 回滚与恢复
|
||||
|
||||
| 编号 | 规则 |
|
||||
|---|---|
|
||||
| R-7.7 | 每个实际副作用有风险比例匹配的回滚/恢复路径,且绑定到受影响资源/入口/owner/后置条件。 |
|
||||
| R-7.8 | 回滚/恢复须可验证(安全模拟、演练、可比的保留任务或实际恢复结果);禁止仅为提高证据强度而执行破坏性回滚。 |
|
||||
| R-7.9 | 恢复路径对失败与超时也要可运行(清理不得只在成功路径执行)。 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 输出规范
|
||||
|
||||
### 3.1 发布流程(本仓)
|
||||
|
||||
> 发布链唯一权威:`docs/release-process.md`(ci-local 门禁 / release.mjs / 四包发布 / pack-audit / install-acceptance / 签名 / OTA);签名细节 `docs/release-signing.md`;本地最短路径 `docs/local-build-quickstart.md`。以下为阶段 ⑦ 在本仓的落地步骤,任一步非零即中止。
|
||||
|
||||
```text
|
||||
Step 0 离线门禁(提交/合并/发布前必过)
|
||||
Windows: build\ci-local.ps1 · macOS/Linux: build/ci-local.sh → run-local-ci.mjs 八步:
|
||||
① 删 release/ → ② npm ci 离线干净安装 → ③ npm run ci:check
|
||||
(build:verify:runtime → vitest 全量 → build)→ ④ playwright E2E(不重构建)
|
||||
→ ⑤ electron-builder --publish never 打安装包 → ⑥ package:audit
|
||||
(asar 禁 dist/mac-*、release/、.map/.d.ts/.ts;体积预算 140 MiB)
|
||||
→ ⑦ verify-packaged-app(extraResources 五件断言 + 真启动 5s 冒烟)
|
||||
→ ⑧ write-build-report(逐产物 SHA-256 → build-report + SHA256SUMS)
|
||||
|
||||
Step 1 版本与变更(SDK 四包发布时)
|
||||
node scripts/release.mjs --registry <内网源URL> [--bump patch|minor|major]
|
||||
[--skip-publish] [--dry-run]
|
||||
九步编排:install → build(拓扑序 api→agent-sdk→account→web-ui)
|
||||
→ sync-web-ui-dist(dist/renderer → packages/web-ui/dist,断言 index.html 字节一致)
|
||||
→ test → pack-audit → bump(四包+根 lockstep,dk-* 互引钉新版,再生 lockfile)
|
||||
→ publish-sdks.mjs 逐包 npm publish 内网源 → install-acceptance
|
||||
→ npm run package
|
||||
日常验证用 --dry-run --skip-publish(shouldPublish 独立纯函数,零副作用有测试断言)
|
||||
|
||||
Step 2 构建安装包 + 产物审计
|
||||
npm run package = build + electron-builder --publish never + package:audit + pack:sdks
|
||||
审计(scripts/pack-audit.mjs):tarball 顶层 ⊆ {dist, bin, README, LICENSE, package.json};
|
||||
零测试产物;dk-account 防误发(registry 锁内网 GitLab);公开三包无 /account/ 路径泄漏
|
||||
安装验收(scripts/install-acceptance.mjs):spawn dk-web → listening banner →
|
||||
GET /health 200 + GET / 200 含 <div id="root"> + 入口 JS 200
|
||||
|
||||
Step 3 签名与公证(对外发布 / CI 打 Git tag 触发)
|
||||
build/sign-local.sh(mac/Linux)/ build/sign-local.ps1(Windows)→ run-signing-build.mjs
|
||||
(仅 darwin/win32;保留 APPLE_*/CSC_* 变量且不设断网)
|
||||
npm ci → build → electron-builder --config.forceCodeSigning=true → package:audit
|
||||
→ 验签:mac codesign --verify --deep --strict;win Get-AuthenticodeSignature 双件 Valid
|
||||
→ 有 APPLE_APP_SPECIFIC_PASSWORD 时 DMG 公证 + stapler 装订 → 冒烟 + 构建报告
|
||||
|
||||
Step 4 OTA 更新(走 OTA 通道时)
|
||||
npm run ota:manifest(scripts/ota/generate-manifest.mjs):扫 release/ 安装包算 SHA-256
|
||||
→ scripts/ota/latest.json;强制单一版本、单平台唯一;缺平台填全 0(运行侧 A6 守卫拒绝)
|
||||
|
||||
Step 5 发布记录落档
|
||||
build-report + SHA256SUMS(产物校验值)· 版本与变更(release.mjs bump)·
|
||||
验证摘要(⑥ 门禁结果绑定修订)· 审批人/审计引用(高风险动作)
|
||||
```
|
||||
|
||||
> 合并/发布边界绑定:改动经默认分支(`build`)可比较的 diff 进入;必需检查(ci-local 全链)对最终修订全绿。本仓无 CHANGELOG/RELEASE_NOTES 文件,版本与变更由 `scripts/release.mjs` 编排。
|
||||
|
||||
### 3.2 交付记录模板
|
||||
|
||||
```text
|
||||
目标边界: 分支 / 环境 / 发布通道
|
||||
修订绑定: 合并/发布的 revision 与决策证据(PR 号 / 发布记录)
|
||||
高风险动作: 动作 | 审批人/策略 | 时间 | 审计引用
|
||||
回滚路径: 受影响资源 | 路径 | owner | 后置条件 | 验证方式
|
||||
验证摘要: ⑥ 的门禁结果绑定
|
||||
残余风险: 未覆盖边界、需人工确认项
|
||||
```
|
||||
|
||||
### 3.3 交付物清单
|
||||
- 合并/发布证据(绑定修订)
|
||||
- 构建产物 + 审计记录(build-report / SHA256SUMS / package:audit 结果)
|
||||
- 发布记录(版本/变更/验证/审批)
|
||||
- 回滚路径文档(高风险动作必填)
|
||||
|
||||
---
|
||||
|
||||
## 4. 反模式
|
||||
|
||||
- **本地验证冒充交付**:只有本地测试支持交付声明。
|
||||
- **先斩后奏的审批**:效果发生了才补审批,或自查自批。
|
||||
- **回滚路径悬空**:有"看起来像回滚"的文件/命令,但连不上实际资源/修订/权限/后置条件(文件名匹配不算证据)。
|
||||
- **只走发布后自动化**:仅在推到默认分支/发布时跑的检查不算接受时验证。
|
||||
- **无边界的高危可达性**:默认路径能触达生产/凭据/破坏性命令而无边界。
|
||||
|
||||
---
|
||||
|
||||
## 5. 轻量路径(小改动)
|
||||
|
||||
- 内部改动:合并路径 + 必需检查即可,无发布/回滚部分(记 N/A 需检查证明)。
|
||||
- 任何触达发布/生产/凭据的动作:审批与审计条款不可省略。
|
||||
|
||||
---
|
||||
|
||||
## 6. 相关文档
|
||||
|
||||
- 输入:阶段 ⑥ [06-validation.md](06-validation.md)
|
||||
- 发布链唯一权威与签名:`docs/release-process.md`、`docs/release-signing.md`(本仓无 CHANGELOG/RELEASE_NOTES 文件,版本与变更由 `scripts/release.mjs` 编排)
|
||||
- 本地/CI 补充:`docs/local-build-quickstart.md`、`docs/ci-runner-setup.md`、`docs/architecture/reproducible-build-baseline.md`
|
||||
- 完成后进入:阶段 ⑧ [08-learning.md](08-learning.md)
|
||||
@@ -0,0 +1,88 @@
|
||||
# 阶段 ⑧ 学习与沉淀(Learning & Capture)
|
||||
|
||||
> **定义**:识别生命周期机会(重复工作、熵增维护、能力缺口),把被支持的改进沉淀为**最小的持久 owner**(文档/技能/记忆/检查),并让沉淀保持准确、长期有效(长期验证)。学习不只是在项目结束时做,而是每个任务闭环后都要做。
|
||||
> **输入**:①–⑦ 全程产生的摩擦、重复、缺口、成功过程。
|
||||
> **输出**:学习捕获(本流程文档/仓库 docs/skills/记忆/门禁)+ 后续验证契约。
|
||||
> **进入下一阶段(回到 ①,或项目收尾)**:捕获落档,owner 明确。
|
||||
|
||||
---
|
||||
|
||||
## 1. 要求(Requirements)
|
||||
|
||||
### 1.1 进入条件
|
||||
- 本任务已交付(⑦ 完成)或中途出现明确重复/熵增信号。
|
||||
|
||||
### 1.2 完成条件(DoD)
|
||||
- [ ] 已区分"一次性问题"与"可复用机会":重复工作(≥2 个可比任务同样意图+同样摩擦/成本)才进循环工程
|
||||
- [ ] 被捕获的改进有:触发方式 / 稳定输入 / owner / 工具与权限 / 产物 / 验证器 / 停止规则
|
||||
- [ ] 覆盖面检查做过:已有能力(观察→内置→已配置→扩展→新建)优先于新建
|
||||
- [ ] 捕获产物同步到正确 owner:流程缺陷 → 本目录;代码事实 → 文档/测试;重复过程 → 技能/脚本
|
||||
- [ ] 长期验证方式已写明(可比后续结果 或 定期维护检查),未验证的效果不作宣称
|
||||
|
||||
---
|
||||
|
||||
## 2. 规则(Rules)
|
||||
|
||||
| 编号 | 规则 |
|
||||
|---|---|
|
||||
| R-8.1 | **重复才算机会**:一次偶发不算;两次可比任务 + 可复现的摩擦/成本/成功过程才构成重复机会。当前一次交接只能产生"当前维度"的建议,不能证明复现。 |
|
||||
| R-8.2 | **最小 owner**:先查覆盖阶梯(已有机制 → 内置 → 已配置 → 扩展 → 新建),选择最小持久 owner;不默认新建 Skill/脚本。 |
|
||||
| R-8.3 | **可运行才算沉淀**:沉淀必须有触发、输入、验证器与停止规则,且被实际使用过(`Exercised`);仅创建未使用 = 未沉淀。 |
|
||||
| R-8.4 | **不空置缺口**:观察到需求但无持久覆盖时,留下显式缺口条目(owner + 证据),不悄悄丢弃。 |
|
||||
| R-8.5 | **效果需长期证据**:只有在可比后续结果中观察到改进(且无护栏回归)才能宣称"有效";同窗口验证只证明修复状态,不证明长期效果。 |
|
||||
| R-8.6 | **维护类机会需可判定**:熵增/过时(依赖、文档、命令、版本)必须有命名的资产/不变量、权威真相、可运行的检查(返回 clean/gap/needs-evidence),文件年龄/提交历史本身只是线索,不是判定。 |
|
||||
| R-8.7 | **流程本身可演进**:本流程文档的缺陷、反模式、新规则,经确认后回写(带版本与变更记录,见 [README.md](README.md) 变更记录)。 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 输出规范
|
||||
|
||||
### 3.1 学习捕获模板
|
||||
|
||||
```text
|
||||
机会类型: 一次性修正 | 重复机会 | 熵增维护 | 能力缺口 | 无候选窗口
|
||||
证据: 可比任务(id/日期/摩擦点)或 资产+不变量+检查
|
||||
覆盖面检查: 观察 → 内置 → 已配置 → 扩展 → 新建(勾选已试层级)
|
||||
最小 owner: 文档/技能/脚本/门禁/记忆(+ 触发/输入/验证器/停止规则)
|
||||
验证方式: 可比后续结果 | 定期维护检查(权威真相 + owner + 频率)
|
||||
状态: 已捕获(Exercised)/ 缺口(证据 + owner)/ 无候选
|
||||
```
|
||||
|
||||
### 3.2 捕获落点
|
||||
|
||||
| 捕获类型 | 落点(本仓) |
|
||||
|---|---|
|
||||
| 流程/阶段缺陷 | 本目录对应阶段文档(带版本变更记录) |
|
||||
| 代码事实/架构决策 | `docs/architecture/`(architecture-overview / codemap / flows / data-model 等基线)、ADR |
|
||||
| 重复过程/命令 | 脚本、`scripts/`、技能(`skills/`) |
|
||||
| 测试缺口 | `docs/architecture/testing-architecture.md`(测试体系单一事实源) |
|
||||
| 长期记忆 | agent 记忆系统 / 项目 memory(本仓:`docs/memory/` 本地工作文档与记忆架构,不入库) |
|
||||
|
||||
### 3.3 交付物清单
|
||||
- 学习捕获记录(模板一份,可并入交付总结)
|
||||
- 实际落地的捕获产物(文档/脚本/技能/门禁 + 使用证据)
|
||||
- 长期验证契约(谁、何时、怎么判 clean/gap)
|
||||
|
||||
---
|
||||
|
||||
## 4. 反模式
|
||||
|
||||
- **一次事故当规律**:单个事件、命令 token、计数、文件名当重复证据。
|
||||
- **跳过覆盖阶梯**:已有机制没试就先新建 Skill/脚本。
|
||||
- **沉淀完就完**:创建后无触发、无验证器、无停止规则,也从没被用过。
|
||||
- **同窗宣称效果**:同一窗口的修复验证冒充"长期有效"。
|
||||
- **文件年龄当结论**:没和权威真相对比就宣布依赖/文档过时。
|
||||
|
||||
---
|
||||
|
||||
## 5. 轻量路径(小改动)
|
||||
|
||||
- 小任务:在 PR 描述或交付总结里加一小节(机会类型 + 落点 + 验证方式)即可。
|
||||
- 重大发布/反复摩擦的项目:必须完整执行本阶段。
|
||||
|
||||
---
|
||||
|
||||
## 6. 相关文档
|
||||
|
||||
- 输入:阶段 ⑦ [07-delivery.md](07-delivery.md) 及全程证据
|
||||
- 流程总览与变更记录:[README.md](README.md)
|
||||
@@ -0,0 +1,84 @@
|
||||
# Project Process — DK Forge(pi-desktop)开发流程规范
|
||||
|
||||
> **本文档是本仓库项目开发的强制流程**。做任何项目需求(新功能、修复、重构、UI 改动、IPC/契约变更、发布)时,都必须按本流程的阶段执行:进入每个阶段先读对应章节,满足**要求**、遵守**规则(Rule)**、按**输出规范**产出交付物,检查清单全过才算完成本阶段。
|
||||
|
||||
- 版本:v1.2 · 2026-09-04
|
||||
- 适用范围:本仓库(`pi-desktop`,发布名 **DK Forge**)全部功能开发,包括 Electron 宿主(`src/main/`)、renderer SPA(`packages/web-ui/`)、服务主体与 SDK 包(`packages/agent-sdk` 等)、E2E 与发布。仓库硬约束以 `AGENTS.md` 为准,本文档是执行流程(不与其冲突)
|
||||
- 来源:本规范由既有工程规则体系沉淀提炼(spec-structure / Agent Work Loop / Software Fluency / DESIGN.md Contract / Harness Engineering,规则参考 `vendor/better-harness/`),并按本仓技术栈(Electron + Node.js + React + TypeScript strict ESM monorepo)与 `AGENTS.md` 既有约定适配(v1.1:清除上游仓库残留引用,对齐本仓目录/契约/测试/发布事实)
|
||||
- 语言约定:流程文档正文用中文;标识符、类型、API、命令、文件路径一律用英文
|
||||
|
||||
---
|
||||
|
||||
## 1. 流程全景(八个阶段)
|
||||
|
||||
```text
|
||||
① 需求与规格 ─→ ② 架构设计 ─→ ③ 详细设计 ─→ ④ UI Spec ─→ ⑤ 实现
|
||||
│
|
||||
⑥ 验证与质量门 ←─────────────────────────────────────┘
|
||||
│
|
||||
⑦ 交付与发布 ─→ ⑧ 学习与沉淀
|
||||
```
|
||||
|
||||
| 阶段 | 文档 | 一句话定义 | 核心输出 |
|
||||
|---|---|---|---|
|
||||
| ① 需求与规格 | [01-requirement.md](01-requirement.md) | 把意图变成一份"实现者无需猜"的规格(scope + 规则 id + 验收标准) | 规格文档(BR/E/AC 编号) |
|
||||
| ② 架构设计 | [02-architecture.md](02-architecture.md) | 定分层、模块边界、数据流与关键技术决策 | 架构文档 + ADR + 架构图 |
|
||||
| ③ 详细设计 | [03-detailed-design.md](03-detailed-design.md) | 把架构落到可实现的接口、数据结构、状态机与异常处理 | 详细设计文档 + 接口/数据契约 |
|
||||
| ④ UI Spec | [04-ui-spec.md](04-ui-spec.md) | 定义每个交付面的 UI 规则与约束(骨架/状态机/动作权限 + 设计系统 token) | DESIGN.md / 表面规格 / i18n 约束 |
|
||||
| ⑤ 实现 | [05-implementation.md](05-implementation.md) | 小步、可验证、范围受控地把设计变成代码 | 代码 + colocated 测试 + 变更同步 |
|
||||
| ⑥ 验证与质量门 | [06-validation.md](06-validation.md) | 用最小且相关的检查验证最终改动,失败可诊断、修复后复验 | 测试/门禁/审查记录 |
|
||||
| ⑦ 交付与发布 | [07-delivery.md](07-delivery.md) | 在真实交付边界被接受,高风险有审批,可回滚 | 构建产物 + 发布记录 + 审计 |
|
||||
| ⑧ 学习与沉淀 | [08-learning.md](08-learning.md) | 识别重复工作与熵增,沉淀可复用改进并长期验证 | 学习捕获(docs/skills/memory) |
|
||||
|
||||
> **规模适配**:小改动(一行修复、文案调整)不必产出全部文档——规格/设计可合并为短说明(见各阶段"轻量路径")。但**要求与规则始终有效**,越级必须有理由并记录。
|
||||
|
||||
---
|
||||
|
||||
## 2. 全局规则(所有阶段适用)
|
||||
|
||||
以下规则跨阶段生效,任何阶段不得违反:
|
||||
|
||||
- **G-1 唯一事实源**:跨进程/跨模块的类型与契约只定义一处(本仓:`packages/api/src/contracts.ts`),宿主与各包只 `import type`,禁止本地镜像。文档不覆盖代码,代码与测试是运行时事实。
|
||||
- **G-2 可追溯**:一切行为、规则、异常、验收标准都有稳定 id(如 `BR-01`、`E-01`、`AC-01`)。id 只追加不重排;废弃时标记 withdrawn,保留编号。
|
||||
- **G-3 配置≠行为**:存在一个文件/命令/规则 ≠ 它被使用。任何"机制存在"的结论必须有打开的证据(被调用、被测试、被门禁执行)。
|
||||
- **G-4 不越界**:只做请求范围内的事;发现无关问题只提不改;改动逐行可追溯到本次请求。越界扩展必须显式批准并记录。
|
||||
- **G-5 小步可回滚**:一次只做可验证的一步;优先狭窄、可回滚的改动;`trash` 优于 `rm`。
|
||||
- **G-6 机械规则优先于文档规则**:能用 lint/typecheck/测试/CI 门禁强制的,不许只写进文档。
|
||||
- **G-7 文档随变更同步**:改了什么,必须同步改对应的架构/接口/流程文档(见 [05-implementation.md](05-implementation.md) 变更矩阵)。
|
||||
- **G-8 提交与评审规范**:Conventional Commits(规范见 `AGENTS.md` 规则 6;commit 生成见 `skills/builtin/dk-commit`);PR 必须带测试,至少 1 人评审,主分支是 `build`。
|
||||
|
||||
---
|
||||
|
||||
## 3. 遵守机制(如何保证被遵守)
|
||||
|
||||
1. **启动即读**:接手本仓的开发 agent(编码执行者)开工前先读本 `README.md` + 目标阶段文档。
|
||||
2. **阶段门禁**:每个阶段的"完成条件(DoD)"检查清单未全过,不得进入下一阶段;上一阶段的输出是本阶段的输入。
|
||||
3. **合并前审查**:PR 评审人按 [06-validation.md](06-validation.md) 的质量门核对,未过门禁的 PR 打回。
|
||||
4. **演进**:本流程本身也在 ⑧ 学习与沉淀 的范围内——流程缺陷、重复摩擦、新反模式,应更新对应文档(带版本号与变更记录)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 文档索引
|
||||
|
||||
| 路径 | 内容 |
|
||||
|---|---|
|
||||
| [01-requirement.md](01-requirement.md) | 需求与规格:7 段结构、BR/E/AC 编号、六道完备性门 |
|
||||
| [02-architecture.md](02-architecture.md) | 架构设计:分层/边界/数据流/决策记录、架构图规范 |
|
||||
| [03-detailed-design.md](03-detailed-design.md) | 详细设计:接口契约/状态机/异常/迁移/边界条件 |
|
||||
| [04-ui-spec.md](04-ui-spec.md) | UI Spec:表面规格(骨架/组件/状态机/权限)+ 设计系统(DESIGN.md) |
|
||||
| [05-implementation.md](05-implementation.md) | 实现:编码规则、规模红线、变更矩阵、自测 |
|
||||
| [06-validation.md](06-validation.md) | 验证与质量门:分层验证、失败诊断与复验、评审 |
|
||||
| [07-delivery.md](07-delivery.md) | 交付与发布:构建/签名/发布记录/回滚 |
|
||||
| [08-learning.md](08-learning.md) | 学习与沉淀:循环工程、知识捕获、长期验证 |
|
||||
| **templates/**(模板库) | 产品/文档模板:**[URD-Template.md](templates/URD-Template.md)**(用户需求文档模板,产品级需求描述,软件项目通用;[HTML 看板](templates/URD-Template.html)) |
|
||||
|
||||
**快速开始**:新需求 → 读 [01-requirement.md](01-requirement.md);涉及 UI → 同时读 [04-ui-spec.md](04-ui-spec.md);改动进入实现前 → 过 [01-requirement.md](01-requirement.md) 的完备性门 + [03-detailed-design.md](03-detailed-design.md) 的契约检查;提交前 → 过 [06-validation.md](06-validation.md) 门禁。
|
||||
|
||||
---
|
||||
|
||||
## 变更记录
|
||||
|
||||
- v1.3(2026-09-05):新增 §4 模板库索引——`templates/URD-Template.md`(URD 模板,含使用说明/编号规则/章节结构)及其 HTML 看板;模板为软件项目产品级需求文档的通用起点(输入到阶段 ①)。
|
||||
- v1.2(2026-09-04):阶段 ⑦ 发布流程按本仓真实编译打包链路重写——离线门禁(ci-local 八步)→ release.mjs 九步编排 → pack-audit/install-acceptance → sign-local 签名公证 → OTA manifest,全部对齐 `docs/release-process.md`。
|
||||
- v1.1(2026-09-04):适配本仓(pi-desktop / DK Forge)事实——清除上游仓库残留(decokee-deck / Vue2 / `src/shared` / CONTRIBUTING 等引用),目录、契约、测试、发布与文档引用对齐 `AGENTS.md`。
|
||||
- v1.0(2026-08-16):初始版本,基于既有工程规则体系建立八阶段流程。
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,305 @@
|
||||
# URD 模板(User Requirements Document)
|
||||
|
||||
> **定位**:URD(用户需求文档,与 PRD 等价)是**产品级**需求描述——定义"产品是什么、为谁服务、用户要完成哪些任务、每个功能如何被用户感知和使用"。它是软件项目**一切后续文档的输入**:阶段 ① 需求规格(BR/E/AC)、② 架构、③ 详细设计、④ UI Spec、测试用例,都必须能追溯到本文档的功能条目。
|
||||
> **适用**:本模板用于 DK Forge 及后续所有软件项目的产品需求文档撰写。
|
||||
> **用法**:复制本文件为 `<产品名>-URD-<版本>-<日期>.md`,按占位符 `<...>` 填写;删除不适用章节并记录删除理由;编号规则见第 0 节,不得破坏。
|
||||
|
||||
---
|
||||
|
||||
## 0. 使用说明与编号规则(写文档前先读;成稿后可删)
|
||||
|
||||
### 0.1 文档关系
|
||||
```text
|
||||
商业/产品设想(PRD 草稿、口头想法)
|
||||
│ 提炼、确认边界
|
||||
▼
|
||||
本 URD —— 用户可感知的功能需求(本文档)
|
||||
│ 拆解为可实现的单点需求
|
||||
▼
|
||||
阶段①规格(BR/E/AC)→ ② 架构 → ③ 详细设计 → ④ UI Spec → 实现 → 测试
|
||||
```
|
||||
- URD 回答 **"用户要什么、为什么、长什么样、做到什么程度算好"**;
|
||||
- 阶段 ① 规格回答 **"这一版改动具体怎么实现、怎么验收"**;
|
||||
- URD 讲用户语言,不写内部实现(不写表结构、类名、IPC 通道);确有底层约束须写"约束与假设"。
|
||||
|
||||
### 0.2 编号规则(只追加、不重排、弃用标记 withdrawn)
|
||||
| 前缀 | 含义 | 示例 |
|
||||
|---|---|---|
|
||||
| `MOD-xx` | 模块编号 | `MOD-03` |
|
||||
| `FR-<模块简码>-<nn>` | 功能点编号(模块简码见 6.1 清单) | `FR-STU-04` |
|
||||
| `EX-<模块简码>-<nn>` | 功能点的异常/边界场景 | `EX-DASH-02` |
|
||||
| `NFR-xx` | 非功能需求 | `NFR-07` |
|
||||
| `SEC-xx` | 安全/权限需求 | `SEC-03` |
|
||||
| `AC-xx` | 端到端验收标准 | `AC-10` |
|
||||
| `OQ-xx` | 待决策/开放问题 | `OQ-04` |
|
||||
|
||||
### 0.3 书写纪律(继承仓库阶段①规则)
|
||||
1. 每条功能点 = **一个用户可感知的行为**,写成"用户可验证"的描述;散文式一段话超过 5 行 → 拆成行为列表。
|
||||
2. 每个功能点必须写 **主流程**(happy path)与 **异常/边界**(用户会遇到的不顺路径),禁止只写 happy path。
|
||||
3. 写不动的先 `TBD` / `OQ-xx`(带 owner 与期限),禁止编造。
|
||||
4. 优先级统一 `P0`(MVP 必做)/ `P1`(近期待做)/ `P2`(远期)。
|
||||
5. 所有状态、权限、阈值是可配置项时写明"默认值 + 可配置范围",不写死。
|
||||
|
||||
---
|
||||
|
||||
## 1. 文档控制
|
||||
|
||||
### 1.1 文档信息
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 文档名称 | `<产品名> 用户需求文档(URD)` |
|
||||
| 文档编号 | `URD-<产品简称>-<版本>` |
|
||||
| 版本 / 日期 | `V<0.x> · <yyyy-mm-dd>` |
|
||||
| 产品名称 / 版本 | `<对外名称> V<目标版本>` |
|
||||
| 文档状态 | 草稿 / 评审中 / 已确认 / 已发布 |
|
||||
| 作者 / 评审人 | `<姓名或角色>` |
|
||||
| 维护位置 | `<仓库路径>` |
|
||||
|
||||
### 1.2 历史记录
|
||||
| 日期 | 版本 | 作者 | 变更记录 |
|
||||
|---|---|---|---|
|
||||
| yyyy-mm-dd | V0.1 | xxx | 首次创建;<列出范围,如"第 1–3 章 + MOD-01" > |
|
||||
|
||||
### 1.3 评审与分发
|
||||
| 角色 | 评审关注点 |
|
||||
|---|---|
|
||||
| 产品负责人 | 范围、优先级、验收标准是否符合商业意图 |
|
||||
| 设计(UI/UX) | 功能点的用户流程与界面要求是否可实现 |
|
||||
| 研发负责人 | 是否存在未定义边界或不可实现的约束 |
|
||||
| 测试负责人 | 每条功能点是否可验证、异常是否齐全 |
|
||||
| 市场/实施 | 话术与演示脚本是否与功能一致 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 目的与范围
|
||||
|
||||
### 2.1 文档目的
|
||||
> 一句话:本文档定义 `<产品名>` 的用户需求——`<它解决谁的什么问题,交付什么结果>`。
|
||||
说明:本文档的读者(产品/设计/研发/测试/市场)、文档如何被使用(培训材料?需求基线?)。
|
||||
|
||||
### 2.2 产品定义
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 一句话结论 | `<产品做什么、为谁做、交付物>` |
|
||||
| 核心价值 | `<用户购买/使用它的理由,1–3 条>` |
|
||||
| 交付结果 | `<安装包/服务/平台等>` |
|
||||
|
||||
### 2.3 目标范围(In Scope)
|
||||
- `<范围一:……>`
|
||||
- `<范围二:……>`
|
||||
|
||||
### 2.4 非目标(Out of Scope)——与范围同等重要,逐条写明排除理由
|
||||
| 排除项 | 理由 | 可能时机 |
|
||||
|---|---|---|
|
||||
| `<明确不做的事>` | `<为什么本期不做>` | 后续版本 / 永不 |
|
||||
| `<例如:开放第三方市场>` | `<例如:首版只支持受控模板>` | V2+ |
|
||||
|
||||
### 2.5 成功标准(产品级,一句话可判)
|
||||
- `<首个版本成功的硬性标志,如:不写代码的业务人员可在 60 分钟内发布一个可用 Agent>`
|
||||
|
||||
---
|
||||
|
||||
## 3. 参考文献
|
||||
| 文档/来源 | 版本/日期 | 与本文档的关系 |
|
||||
|---|---|---|
|
||||
| `<产品定义 PRD>` | V0.1 | 本文档的功能蓝本 |
|
||||
| `<代码功能事实:codewiki / docs 架构文档>` | 生成日期 | 现状能力基线 |
|
||||
| `<竞品/标准/协议>` | | 约束来源 |
|
||||
| `<阶段①规格草稿>` | | 已确认决策的回写来源 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 术语、角色与成功标准
|
||||
|
||||
### 4.1 术语表
|
||||
| 术语 | 定义(用户语言,可配图) |
|
||||
|---|---|
|
||||
| `<领域词,如 Agent / 面板 / Scope>` | `<一句话说清>` |
|
||||
|
||||
### 4.2 用户角色
|
||||
| 角色 | 画像 | 主要任务 | 成功标准 | 常用模块 |
|
||||
|---|---|---|---|---|
|
||||
| `<角色>` | `<谁,技术背景>` | `<日常要完成什么>` | `<做到什么算好>` | MOD-xx |
|
||||
| 例:培训者 | 懂业务不写代码 | 制作并发布一个 Agent | 不改代码完成首版 | MOD-02/05 |
|
||||
|
||||
### 4.3 角色 × 模块 关注矩阵(可选)
|
||||
| | MOD-01 | MOD-02 | MOD-03 | … |
|
||||
|---|---|---|---|---|
|
||||
| 角色 A | 使用 | 使用 | – | |
|
||||
| 角色 B | – | 审核 | 使用 | |
|
||||
|
||||
---
|
||||
|
||||
## 5. 产品概览与核心体验
|
||||
|
||||
### 5.1 产品背景与目标
|
||||
`<为什么做这个产品;本版本要证明什么(如:证明某类用户能独立完成某闭环)。>`
|
||||
|
||||
### 5.2 信息架构与模块关系
|
||||
```text
|
||||
一级入口/模块树(文字树或图)
|
||||
├── <模块一>
|
||||
│ ├── <子功能>
|
||||
└── ...
|
||||
```
|
||||
`<模块之间的依赖与数据走向一句话说明。>`
|
||||
|
||||
### 5.3 核心用户流程(端到端)
|
||||
`<步骤表:用户从进入到完成一次价值闭环的全过程>`(例:安装 → 授权 → 首次运行 → 理解 → 协作)
|
||||
|
||||
### 5.4 体验原则
|
||||
1. `<先给结果,再邀请对话>`(示例原则,按产品改写)
|
||||
2. `<每个结论可解释>`
|
||||
3. `<异常与关键选择集中呈现,普通状态不打扰>`
|
||||
4. `<任何能力可见、可停用、可替换>`
|
||||
5. ...
|
||||
|
||||
### 5.5 交付面与平台约束
|
||||
| 交付面 | 说明 | 优先级 |
|
||||
|---|---|---|
|
||||
| `<桌面 Electron / Web / 移动 / 硬件>` | `<能力边界、离线、多端同步>` | P0/P1 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 功能模块需求(本文档主体)
|
||||
|
||||
### 6.1 模块清单与划分说明
|
||||
> 划分原则:按**用户任务闭环**划分模块(不是按技术层);模块间尽量低耦合、每个模块有明确负责人与验收场景。
|
||||
| 模块 | 模块名 | 一句话职责 | 主要角色 | 简码 | 优先级 |
|
||||
|---|---|---|---|---|---|
|
||||
| MOD-01 | `<名称>` | `<为谁解决什么>` | | `<STU/DASH…>` | P0 |
|
||||
| MOD-02 | ... | | | | |
|
||||
|
||||
> 模块依赖图(可选):`MOD-02 依赖 MOD-01 的发布产物` 等。
|
||||
|
||||
### 6.2 各模块详细需求
|
||||
> 每模块复制下面一节;模板中 MOD-xx 以 MOD-01 为例。
|
||||
|
||||
#### MOD-01 `<模块名>`
|
||||
**模块概述**
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 目的 | `<该模块在用户任务闭环中的位置与作用>` |
|
||||
| 用户 | `<角色>` |
|
||||
| 入口 | `<从哪个一级入口/页面进入>` |
|
||||
| 依赖 | `<依赖哪些其他模块或外部系统>` |
|
||||
| 核心对象 | `<用户在该模块操作的对象:如 Agent、面板、版本>` |
|
||||
|
||||
**功能点总览**
|
||||
| 编号 | 功能名称 | 用户可完成的任务 | 优先级 |
|
||||
|---|---|---|---|
|
||||
| FR-XXX-01 | `<名称>` | `<一句话用户价值>` | P0 |
|
||||
| FR-XXX-02 | ... | | P1 |
|
||||
|
||||
**功能点详述**
|
||||
|
||||
##### FR-XXX-01 `<功能名称>`(P0)
|
||||
- **功能描述**:`<该功能是什么;用户可用它完成什么;关键行为逐条列出,如:>`
|
||||
1. `<行为一>`
|
||||
2. `<行为二>`
|
||||
- **前置条件**:`<进入该功能前必须满足的条件,如已登录、已有数据源授权>`(无则写"无")
|
||||
- **主流程**:
|
||||
| 步骤 | 用户操作 | 系统响应 |
|
||||
|---|---|---|
|
||||
| 1 | `<操作>` | `<响应/界面变化>` |
|
||||
| 2 | ... | |
|
||||
- **行为规则与边界**:
|
||||
- `<规则/边界一:如"默认展示最近 7 天,可配置 1–90 天">`
|
||||
- **异常与边界场景**:
|
||||
| 编号 | 场景 | 用户可见行为 | 后备方案 |
|
||||
|---|---|---|---|
|
||||
| EX-XXX-01 | `<如:数据源超时>` | `<提示什么>` | `<重试/降级显示陈旧数据并标注>` |
|
||||
- **UI 与交互要求**:`<布局要点、状态(空/加载/错误/正常)、交互手势、可访问性>`(详见阶段 ④ UI Spec)
|
||||
- **权限与安全**:`<该功能涉及的权限等级(见第 9 章 L0–L3)、确认点、审计>`(无则写"无额外要求")
|
||||
- **验收要点**:`<该功能可验证的 3–6 条要点,对应第 11 章 AC>`:
|
||||
- [ ] 要点一
|
||||
- [ ] 要点二
|
||||
- **关联与备注**:`<关联功能点/引用文献/开放问题 OQ-xx>`(无则写"—")
|
||||
|
||||
##### FR-XXX-02 …(复制上面小节)
|
||||
|
||||
---
|
||||
|
||||
## 7. 数据与内容需求(若产品以数据/面板/内容为核心)
|
||||
### 7.1 数据类型与口径
|
||||
| 内容 | 用户要定义什么 | 系统如何使用 |
|
||||
|---|---|---|
|
||||
### 7.2 内容/数据生成规则
|
||||
### 7.3 展示与交互要求(数据面)
|
||||
|
||||
---
|
||||
|
||||
## 8. 非功能需求
|
||||
| 编号 | 类别 | 需求 | 目标/阈值 |
|
||||
|---|---|---|---|
|
||||
| NFR-01 | 性能 | `<首屏/操作响应>` | `<如 ≤2s>` |
|
||||
| NFR-02 | 可用性 | `<新用户无培训完成核心任务>` | `<如 ≤60 分钟>` |
|
||||
| NFR-03 | 可靠性与离线 | `<数据源/服务不可用时的降级>` | |
|
||||
| NFR-04 | 兼容性 | `<OS/浏览器/分辨率>` | |
|
||||
| NFR-05 | 可访问性 | `<键盘/读屏/对比度>` | |
|
||||
| NFR-06 | 国际化与本地化 | `<语言/时区/货币/格式>` | |
|
||||
| NFR-07 | 数据与隐私 | `<数据保留/删除/导出>` | |
|
||||
| NFR-08 | 可观测性 | `<日志/审计/用量统计>` | |
|
||||
| NFR-09 | 安装升级 | `<安装包、升级与回滚>` | |
|
||||
> 每行写清"可度量阈值",无阈值 = 不可验收。
|
||||
|
||||
---
|
||||
|
||||
## 9. 安全、权限与信任
|
||||
### 9.1 用户可理解的动作等级
|
||||
| 等级 | 含义 | 产品行为 | 示例 |
|
||||
|---|---|---|---|
|
||||
| L0 查看 | 只读取和展示 | 授权后自动运行,留读取记录 | |
|
||||
| L1 准备 | 生成草稿不对外提交 | 可自动完成,用户可编辑 | |
|
||||
| L2 确认后执行 | 改变外部系统/通知他人 | 必须展示详情由用户确认 | |
|
||||
| L3 强确认 | 金钱/敏感/不可逆 | 二次确认或强身份验证,默认不批量 | |
|
||||
### 9.2 必须由产品层提供的能力(按产品改写)
|
||||
- `Scope 隔离 / 最小权限 / 凭证托管 / 执行确认 / 可撤销与熔断 / 全链路记录 / 发布前安全测试`
|
||||
### 9.3 信任界面要求
|
||||
- `<每个展示面与动作卡应呈现的信任信息:版本、数据时间、来源、权限等级、失败后果>`
|
||||
|
||||
---
|
||||
|
||||
## 10. MVP 范围与版本规划
|
||||
### 10.1 MVP 目标与样板场景
|
||||
| 目标 | 验证重点 | 最低闭环 |
|
||||
|---|---|---|
|
||||
### 10.2 P0 / P1 / P2 范围表
|
||||
| 产品/模块 | P0(MVP) | P1 | P2(明确不做入近期) |
|
||||
|---|---|---|---|
|
||||
### 10.3 推荐交付阶段
|
||||
| 阶段 | 产品结果 | 验收门 |
|
||||
|---|---|---|
|
||||
|
||||
---
|
||||
|
||||
## 11. 验收标准与产品指标
|
||||
### 11.1 端到端验收
|
||||
| 编号 | 验收标准(按角色、可执行) |
|
||||
|---|---|
|
||||
| AC-01 | `<Given/When/Then 或可执行描述>` |
|
||||
### 11.2 核心产品指标
|
||||
| 指标 | 定义 | 目标 |
|
||||
|---|---|---|
|
||||
| `<首次价值时间>` | `<定义>` | `<阈值>` |
|
||||
|
||||
---
|
||||
|
||||
## 12. 假设与待决策事项
|
||||
| 编号 | 事项 | V0.x 默认假设 | 需要的产品决策 | Owner | 期限 |
|
||||
|---|---|---|---|---|---|
|
||||
| OQ-01 | `<如:首端平台>` | `<默认>` | `<待决问题>` | | |
|
||||
> 未决事项不阻止开始原型,但进入正式排期前必须关闭。
|
||||
|
||||
---
|
||||
|
||||
## 附录
|
||||
### A. 功能点索引(全部 FR / EX 编号 → 模块位置,自动核对用)
|
||||
| 编号 | 功能 | 模块 | 优先级 | 状态(草稿/已确认/withdrawn) |
|
||||
|---|---|---|---|---|
|
||||
### B. 产品话术与演示脚本(可选)
|
||||
### C. 变更对照(本版相对上一版的增删)
|
||||
|
||||
---
|
||||
|
||||
> **模板维护**:本模板本身受仓库 ⑧ 学习与沉淀 约束——使用中发现结构缺陷,更新本文档并记入"变更记录"(放文末)。
|
||||
Binary file not shown.
@@ -0,0 +1,527 @@
|
||||
# 兴邦金租对公智能评审平台技术路径与 Agent Loop 说明
|
||||
|
||||
适用对象:没有代码基础的业务、产品、实施、数据和管理同事
|
||||
文档版本:v1.0
|
||||
代码基线:Git b10093f
|
||||
说明范围:只解释当前仓库中的实现,不泛化到其他 LangChain、LangGraph 或 OpenClaw 项目
|
||||
|
||||
## 一 先理解当前系统的结论
|
||||
|
||||
当前平台不是让大模型直接连接业务数据库后自由作答。系统先把业务数据和材料转换成标准字段,冻结本次输入以及数据卡、Skills、规则和模板版本,再执行确定性检查、已发布经验核查和受限模型核查。运行结果经过结构、证据和权限校验后,才形成问题、报告和评审对象。
|
||||
|
||||
业务人员看到的是一个统一业务助手和项目办理页面。底层仍由 mini-openclaw 的 `AgentManager` 提供 LangChain Agent 与工具循环,但业务运行使用独立上下文和受限工具,不继承普通聊天历史,不向模型开放数据库 SQL、终端或文件写入权限。
|
||||
|
||||
当前系统有两种 Agent Loop。
|
||||
|
||||
1. 通用对话循环:模型根据会话历史决定直接回答或调用通用工具,工具结果回到模型,直到模型给出最终文本。
|
||||
2. 兴邦业务循环:服务先执行数据卡、确定性 Skills、规则和经验,再让模型通过只读工具读取冻结快照和方法,最后由程序校验模型 JSON 并写入业务对象。
|
||||
|
||||
第二种循环是兴邦项目的主要技术路径。它的目标不是让模型自由发挥,而是让每次判断都能回答四个问题:使用了什么数据,依据了什么方法,谁在什么时间作了什么决定,后来是否发生了版本变化。
|
||||
|
||||
## 二 系统分层
|
||||
|
||||
| 层次 | 当前技术 | 主要职责 | 关键位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| 浏览器界面 | Next.js 14 与 React 18 | 业务助手、项目工作台、评审池、时效看板、配置治理 | `src/app` 与 `src/components/xingbang` |
|
||||
| 同源代理 | Next.js Route Handler | 把浏览器请求转发到后端并传递身份信息 | `src/app/api/core` 与 `src/app/api/risk-agent` |
|
||||
| API 层 | FastAPI | 参数校验、角色与项目权限、业务路由、SSE 流 | `backend/api` 与 `backend/app.py` |
|
||||
| Agent 基座 | LangChain `create_agent` | 组装模型、工具和系统提示词 | `backend/graph/agent.py` |
|
||||
| Agent 循环 | LangGraph 的模型节点与工具节点 | 模型调用、工具调用、再次模型调用、结束判断 | 由 LangChain `create_agent` 内部驱动 |
|
||||
| 业务编排 | 项目服务与持久任务 Worker | 冻结快照、读取数据卡、执行组件、恢复任务 | `backend/services/project_assistant.py` 与 `assistant_worker.py` |
|
||||
| 执行器 | Python 确定性程序与受限模型 Agent | 必填、对比、比率、重复项、规则和语义核查 | `skill_executor.py` 与 `assistant_model.py` |
|
||||
| 持久化 | SQLite、JSON、JSONL 和向量索引 | 项目对象、任务、审计、会话、原始轨迹和长期记忆 | `backend/xingbang_data`、`sessions`、`raw_logs`、`storage` |
|
||||
|
||||
浏览器不直接连接数据库、模型或文件系统。所有读取和写入都经过后端接口及身份检查。
|
||||
|
||||
## 三 六个核心概念
|
||||
|
||||
### 三一 数据连接
|
||||
|
||||
数据连接描述系统允许连接到哪一个数据源,包括数据源类型、只读边界、表白名单和凭据环境变量引用。当前组件协议支持演示源、SQLite、PostgreSQL 和 MySQL。密码、令牌和 API Key 不允许直接写入组件内容或运行快照。
|
||||
|
||||
### 三二 数据卡
|
||||
|
||||
数据卡说明如何从一个数据连接中读取某一组业务字段。它包含参数化 SQL、源字段到标准字段的映射、单位、币种、期间和口径。数据卡的输出不是一段供人阅读的报表,而是一组标准事实和字段级证据。
|
||||
|
||||
### 三三 Skill
|
||||
|
||||
Skill 表示一种可执行的业务能力或方法。当前业务 Skill 分为确定性 Skill 和模型 Skill。
|
||||
|
||||
- 确定性 Skill 由固定 Python 执行器完成,例如必填检查、两来源比较、比例计算和重复项检查。
|
||||
- 模型 Skill 使用已冻结的 `SKILL.md` 方法文本和只读工具,通过 mini-openclaw Agent Loop 做语义核查。
|
||||
|
||||
Skill 不能仅靠文字获得新权限。它可以声明允许使用哪些只读工具,实际工具集合仍由后端白名单控制。
|
||||
|
||||
### 三四 规则卡
|
||||
|
||||
规则卡适合执行明确的条件判断,例如覆盖率低于阈值、某字段大于上限或在特定条件下必须补充资料。规则卡不调用大模型。阈值和操作符由程序读取,金额使用十进制数计算。
|
||||
|
||||
### 三五 业务方案
|
||||
|
||||
业务方案把数据卡、Skills、规则、报告模板、适用角色、运行类型和工具预算组合在一起。客户经理 AI 预审和评审员独立复核是两张不同方案。它们可以复用基础数据和部分确定性 Skills,但使用不同的模型 Skill、报告模板和预算。
|
||||
|
||||
### 三六 快照与运行
|
||||
|
||||
快照保存某一次运行实际看到的项目事实、材料、来源、数据卡结果和内容哈希。运行保存执行人、身份、方案、组件版本、步骤、问题、证据、开始结束时间和最终状态。快照和已发布组件版本不可被覆盖。
|
||||
|
||||
## 四 从数据进入到业务结果的完整路径
|
||||
|
||||
```text
|
||||
业务数据库或上传材料
|
||||
→ 数据连接和数据卡读取
|
||||
→ 标准字段与字段级来源
|
||||
→ 人工确认材料候选
|
||||
→ 冻结输入快照和组件版本
|
||||
→ 确定性 Skills
|
||||
→ 规则卡
|
||||
→ 已发布经验核查
|
||||
→ 受限模型 Skill 和工具循环
|
||||
→ 结构与证据校验
|
||||
→ 问题与运行结果
|
||||
→ 报告草稿
|
||||
→ 客户经理提交评审池
|
||||
→ 评审员领取和独立评审
|
||||
→ 退回或批准
|
||||
```
|
||||
|
||||
这条路径中,大模型只参与受限模型 Skill。数据卡、金额换算、口径校验、比例计算、阈值判断、任务持久化和人工审批都由普通程序完成。
|
||||
|
||||
## 五 数据卡如何把多张表变成标准事实
|
||||
|
||||
### 五一 为什么需要数据卡
|
||||
|
||||
同一个业务事实在不同系统中可能使用不同表名、列名、金额单位和期间。Skill 如果直接依赖源系统列名,一旦 ERP 或财务系统调整字段,所有检查都会受影响。数据卡把源系统结构与业务标准字段隔开。
|
||||
|
||||
例如,源表中的 `total_liability`、`liab_total` 或中文负债合计,都可以映射到标准字段 `financial.total_liabilities`。后续 Skill 和规则只依赖标准字段。
|
||||
|
||||
### 五二 当前标准字段示例
|
||||
|
||||
| 标准字段 | 业务含义 | 可能来源 |
|
||||
| --- | --- | --- |
|
||||
| `facility.amount` | 融资金额 | 项目融资表或审批申请 |
|
||||
| `lease.total_value` | 租赁物价值 | 租赁物台账或评估材料 |
|
||||
| `financial.total_liabilities` | 财务报表负债 | 财务报表数据卡 |
|
||||
| `due_diligence.liabilities` | 尽调口径负债 | 尽调材料或尽调数据表 |
|
||||
| `financial.operating_cashflow` | 经营现金流 | 现金流量表 |
|
||||
| `lease.duplicate_count` | 重复设备数量 | 设备清单去重结果 |
|
||||
| `credit.overdue_count` | 征信逾期数量 | 征信数据卡 |
|
||||
| `collection.evidence` | 回款依据 | 回款说明或附件 |
|
||||
|
||||
### 五三 SQL 执行边界
|
||||
|
||||
数据卡只允许一条参数化 `SELECT` 或 `WITH SELECT`。项目条件使用服务端参数 `:project_id`,不能通过字符串拼接项目编号。数据连接可以配置允许访问的表,SQL 不能访问白名单之外的业务表或系统表。SQLite 以只读方式打开,并由数据库授权器再次禁止写操作。
|
||||
|
||||
模型不获得 SQL 工具。执行 SQL 的是数据卡层,模型只能看到数据卡已经读取、映射、授权并冻结的结果。
|
||||
|
||||
### 五四 字段映射与口径校验
|
||||
|
||||
`assistant_data.py` 读取数据卡后依次处理以下事项:
|
||||
|
||||
1. 检查演示数据域与生产数据域不能混用。
|
||||
2. 使用项目编号参数读取数据行。
|
||||
3. 按数据卡映射生成标准字段。
|
||||
4. 按字段单位把元、万元等换算为统一量纲。
|
||||
5. 保留期间、口径、单位和币种。
|
||||
6. 为每个字段生成证据编号、来源名称和定位信息。
|
||||
7. 对查询结果计算行哈希,记录本次读到了哪一版数据。
|
||||
|
||||
若同一标准字段来自多个来源且值不一致,程序会把当前值置为空并记录冲突,不会让模型自动选择其中一个。金额和比例计算前会检查币种、单位、口径和期间;不一致时停止交叉计算。
|
||||
|
||||
### 五五 多张表与多张数据卡
|
||||
|
||||
当前演示业务库包含客户、项目、融资方案、财务报表、租赁物、保险、增信、征信、诉讼、还款来源和材料等多张表。管理员可以按业务域拆成多张数据卡,例如项目主数据卡、财务现金流卡、租赁物保险卡、征信涉诉卡和增信还款卡。业务方案选择多张卡后,运行会把它们统一映射到同一份标准字段快照。
|
||||
|
||||
## 六 上传材料如何进入同一条数据链
|
||||
|
||||
材料入口支持 TXT、CSV、DOCX、XLSX、PDF 和图片,单文件上限为 10 MB。解析器会保存原件、文件哈希、版本、文本片段、定位和警告。每个文本片段生成独立证据编号。
|
||||
|
||||
解析结果先形成候选字段。候选字段不会自动覆盖项目事实。客户经理必须确认值、期间、单位、口径和来源后,系统才更新项目数据版本。旧材料和旧字段来源继续保留。
|
||||
|
||||
DOCX 和 XLSX 通过压缩包内的 XML 读取,不执行宏。XLSX 公式只读取文件中已有的缓存值。PDF 和图片通过现有页面提取能力获取文本;无法识别的页产生警告。材料正文被视为不可信数据,其中写入的指令不会变成 Agent 指令。
|
||||
|
||||
## 七 Skills 在当前项目中的两种形态
|
||||
|
||||
### 七一 原生方法 Skill
|
||||
|
||||
`backend/skills` 下的 `SKILL.md` 由 `skills_scanner.py` 扫描。文件头包含名称、说明、分类、输入字段和版本。扫描结果形成 `SKILLS_SNAPSHOT.md`,供通用对话 Agent 了解可用方法。
|
||||
|
||||
兴邦业务运行不会在执行中临时读取变化中的文件。方案冻结时,系统读取选中的 `SKILL.md` 全文并计算内容哈希,保存到 Skill 组件的 `methods` 中。之后即使管理员修改本地文件,本次运行仍使用入队时冻结的内容。
|
||||
|
||||
当前金租方法包括承租人及还款能力、租赁物适格性与权属、售后回租真实性、增信与关联交易集中度、独立评审与报告证据核查。
|
||||
|
||||
### 七二 可执行业务 Skill
|
||||
|
||||
当前允许的执行器如下。
|
||||
|
||||
| 执行器 | 用途 | 是否调用模型 |
|
||||
| --- | --- | --- |
|
||||
| `required` | 检查标准字段是否缺失 | 否 |
|
||||
| `compare` | 比较两个来源值及其期间口径 | 否 |
|
||||
| `ratio` | 用十进制数计算比例并生成输出字段 | 否 |
|
||||
| `duplicate` | 判断重复项数量是否大于零 | 否 |
|
||||
| `quality` | 检查输入字段质量 | 否 |
|
||||
| `overdue` | 处理逾期类输入 | 否 |
|
||||
| `model` | 通过受限 Agent 做语义核查 | 是 |
|
||||
|
||||
执行器集合写死在 `skill_executor.py`。系统不根据上传内容动态导入 Python 模块,也不使用 `eval` 执行表达式。首版 Skill 只支持无外部副作用的读取与判断。
|
||||
|
||||
### 七三 当前预置确定性 Skills
|
||||
|
||||
| Skill | 输入 | 输出或判断 |
|
||||
| --- | --- | --- |
|
||||
| 负债金额来源不一致 | 财报负债、尽调负债 | 两个来源一致、冲突或无法判断 |
|
||||
| 现金流完整性 | 经营现金流 | 已提供或材料不足 |
|
||||
| 设备清单重复项 | 重复设备数量 | 有重复、无重复或缺少数据 |
|
||||
| 租赁物价值覆盖测算 | 租赁物价值、融资金额 | 生成 `metrics.coverage` |
|
||||
|
||||
确定性 Skill 的结果可以重复计算和单元测试。它们不依赖模型是否可用。
|
||||
|
||||
## 八 规则卡怎样使用 Skill 输出
|
||||
|
||||
规则卡由一个或多个条件组成,每个条件包含字段、操作符、阈值、问题标题和建议动作。支持大于、小于、等于、大于等于和小于等于。
|
||||
|
||||
当前演示覆盖规则读取 `metrics.coverage`。这个字段先由比例 Skill 计算,再由规则比较是否低于 1.2。执行顺序由 Skill 依赖和业务方案控制。规则命中时形成问题;字段缺失或无法计算时形成材料不足;条件不适用时形成不适用。
|
||||
|
||||
规则卡中的 1.2 是演示阈值。正式环境必须由兴邦金租确认规则来源、适用范围、例外、版本和生效日期后再发布。
|
||||
|
||||
## 九 业务方案如何冻结一次运行
|
||||
|
||||
`component_registry.py` 维护已发布组件版本和发布指针。运行开始时,`freeze` 按方案解析数据卡、数据连接、Skills、规则和模板,并完成以下检查:
|
||||
|
||||
- 所有组件属于相同的数据域。
|
||||
- Skill 执行器和允许工具在白名单内。
|
||||
- Skill 依赖不存在循环,比例输出字段不冲突。
|
||||
- 规则操作符和阈值有效。
|
||||
- 报告模板章节不为空且编号不重复。
|
||||
- 原生 `SKILL.md` 存在,并把方法全文读入当前组件。
|
||||
|
||||
冻结结果计算整体内容哈希。运行对象还保存每个组件的稳定编号、版本和内容哈希。发布新版本只影响后续运行,不会改变历史运行。
|
||||
|
||||
当前主要方案如下。
|
||||
|
||||
| 方案 | 适用身份 | 主要差异 |
|
||||
| --- | --- | --- |
|
||||
| 客户经理 AI 预审 | 客户经理 | 使用综合核查 Skill 和 AI 预审报告模板 |
|
||||
| 评审员独立复核 | 评审员 | 使用独立评审 Skill、独立评审模板和更高工具预算 |
|
||||
| 多表业务库 AI 预审 | 客户经理 | 使用多张业务数据卡读取模拟数据库 |
|
||||
|
||||
## 十 业务 Agent Loop 的十二个步骤
|
||||
|
||||
### 十一 接收指令与权限检查
|
||||
|
||||
业务助手或页面按钮调用项目服务。服务端检查当前身份、角色、项目授权、方案受众和项目版本。评审员方案还要求当前评审员已经从评审池领取项目。
|
||||
|
||||
### 十二 幂等检查
|
||||
|
||||
前端为发起运行和生成报告提供幂等键。后端把执行人、操作、幂等键、请求哈希和对象编号保存在 `assistant_idempotency`。相同请求重复点击返回原对象;同一幂等键用于不同请求会被拒绝。
|
||||
|
||||
### 十三 冻结输入快照
|
||||
|
||||
系统保存当前项目事实、来源、材料、数据版本和材料片段证据。每个材料片段根据文件、内容哈希、序号和定位生成稳定证据编号。这个快照写入后不可覆盖。
|
||||
|
||||
### 十四 冻结组件
|
||||
|
||||
系统按方案解析当前发布的数据卡、Skills、规则和模板,读取原生 Skill 方法文本并计算哈希。模型配置和功能开关一并写入运行对象。
|
||||
|
||||
### 十五 创建持久任务
|
||||
|
||||
系统创建状态为 `queued` 的运行和 `assistant_jobs` 任务,并立即向页面返回运行编号。浏览器不需要保持长连接。
|
||||
|
||||
### 十六 Worker 领取与续租
|
||||
|
||||
后台 Worker 领取任务,写入执行人、尝试次数和租约。执行期间定时续租。进程异常后,租约过期的任务可以被重新领取。已完成步骤写入运行对象,恢复时可以复用,不必从第一步全部执行。
|
||||
|
||||
### 十七 执行数据卡
|
||||
|
||||
Worker 从输入快照和冻结数据卡读取业务数据,做字段映射、单位换算和冲突检查,生成正式业务快照、数据集行哈希和数据错误。
|
||||
|
||||
### 十八 执行确定性 Skills
|
||||
|
||||
非模型 Skills 先运行。每个步骤保存输入哈希、开始结束时间、输出、问题、缺失输入、错误和状态。比例 Skill 产生的新字段会加入后续步骤使用的 `values`。
|
||||
|
||||
### 十九 执行规则卡
|
||||
|
||||
规则读取当前标准字段和前序 Skill 输出,形成通过、发现问题、材料不足或不适用的检查结果。每项结果包含字段、值、证据编号、规则编号与版本和建议动作。
|
||||
|
||||
### 十十 执行已发布经验
|
||||
|
||||
运行第一次建立业务快照时冻结当时可用的经验版本。经验先检查数据域、项目或团队范围、行业、触发条件、例外条件和有效期。匹配后生成一项可追溯检查,并保存实际应用记录。
|
||||
|
||||
### 十十一 执行模型 Skill
|
||||
|
||||
模型 Skill 通过同一个 `AgentManager` 创建隔离 Agent,但不读取普通聊天历史。模型只能使用方案授权的只读工具。工具结果回到模型,模型可再次选择工具,直到输出最终 JSON。
|
||||
|
||||
### 十十二 校验与发布结果
|
||||
|
||||
程序用 Pydantic 校验模型 JSON,检查问题编号、结果枚举、标准字段、证据编号和材料读取记录。通过后,系统合并所有检查,更新问题生命周期,保存证据、步骤、运行摘要和结束状态。若运行期间项目数据版本已经变化,本次运行标记为已被新版本取代,不覆盖当前问题状态。
|
||||
|
||||
## 十一 模型 Skill 内部的 Agent Loop
|
||||
|
||||
### 十一一 模型第一次收到的内容
|
||||
|
||||
模型不会直接收到整个数据库或服务器目录。初始任务包含选中方法清单、材料编号清单、人工提出的本次关注点和 Skill 指令。系统提示明确要求模型必须使用受限工具读取事实、计算结果和方法,不能执行 SQL、修改文件或作审批决定。
|
||||
|
||||
### 十一二 可用只读工具
|
||||
|
||||
| 工具 | 读取内容 | 关键限制 |
|
||||
| --- | --- | --- |
|
||||
| `read_project_snapshot` | 当前冻结的标准字段与字段级来源 | 只能读本次项目快照 |
|
||||
| `read_document` | 当前快照中的指定材料片段 | 材料编号必须属于本次快照 |
|
||||
| `read_checks` | 确定性检查、已发布规则和本次要求 | 不允许模型自行改规则 |
|
||||
| `read_method` | 被方案选择并冻结的 `SKILL.md` 方法 | 未选择的方法不能读取 |
|
||||
| `read_experiences` | 已授权且适用的已发布经验 | 不提供来源项目专有信息 |
|
||||
|
||||
模型 Skill 声明 `allowed_tools`,后端再从固定工具集合中取交集。Skill 文件中的文字不能创造一个新工具。
|
||||
|
||||
### 十一三 LangChain 与 LangGraph 的职责
|
||||
|
||||
`AgentManager.build_task_agent` 调用 LangChain 的 `create_agent`。LangChain 提供模型适配、消息和工具定义。`create_agent` 内部由 LangGraph 驱动模型节点和工具节点之间的循环。
|
||||
|
||||
```text
|
||||
任务和系统提示词
|
||||
→ 模型节点
|
||||
→ 模型返回普通文本或 tool_calls
|
||||
→ 如有 tool_calls 则进入工具节点
|
||||
→ Python 工具返回 ToolMessage
|
||||
→ ToolMessage 回到模型节点
|
||||
→ 模型继续调用工具或输出最终 JSON
|
||||
→ 没有未完成工具调用时结束
|
||||
```
|
||||
|
||||
一次业务模型核查可能调用模型多次,但对用户仍是一项运行步骤。后端设置递归上限、总超时和最大工具调用次数。客户经理方案默认 120 秒和最多 12 次工具调用,评审员方案默认 180 秒和最多 18 次工具调用。
|
||||
|
||||
### 十一四 强制读取与输出校验
|
||||
|
||||
模型完成前必须至少读取项目快照、已有检查和每个选中的方法。缺少任一必需读取,步骤失败。
|
||||
|
||||
最终输出必须是结构化 JSON,包含 `findings` 和 `requirement_response`。每个判断只能使用四种结果:通过、发现问题、材料不足、不适用。判断为通过或发现问题时必须有证据。引用材料证据前必须实际调用 `read_document` 读取对应材料。引用未知字段、未授权证据或重复检查码都会导致步骤失败。
|
||||
|
||||
这种校验不能保证模型永远正确,但可以防止它把无法追溯的文字直接写入正式问题列表。
|
||||
|
||||
## 十二 确定性检查与模型检查如何合并
|
||||
|
||||
Worker 按以下顺序执行:非模型 Skills、规则卡、已发布经验、模型 Skills。所有检查汇总后,`reconcile` 根据稳定问题编号处理问题生命周期。
|
||||
|
||||
| 场景 | 问题状态处理 |
|
||||
| --- | --- |
|
||||
| 新检查首次发现问题或材料不足 | 创建待处理、待补件或待复核问题 |
|
||||
| 新运行确认此前问题已通过 | 标记已解决,保留历史 |
|
||||
| 已解决问题再次出现 | 重新打开并记录新运行 |
|
||||
| 规则或 Skill 依据版本变化 | 原结果进入待复核,不静默沿用 |
|
||||
| 运行期间项目数据变化 | 本次运行不覆盖当前问题状态 |
|
||||
|
||||
人工可以把问题标记为待补件、待复核或不适用,并填写说明。已解决状态只由有依据的新检查确认,避免业务人员直接把问题点掉后失去证据。
|
||||
|
||||
## 十三 当前记忆系统的整体设计
|
||||
|
||||
当前项目不存在一个把所有内容自动记住的单一记忆库。不同用途使用不同存储,并有不同的可信度和生命周期。
|
||||
|
||||
| 记忆类型 | 存储位置 | 主要用途 | 是否进入业务审批依据 |
|
||||
| --- | --- | --- | --- |
|
||||
| 会话短期记忆 | `backend/sessions/{session_id}.json` | 让通用聊天记住同一会话前文 | 否 |
|
||||
| 压缩会话摘要 | 会话文件的 `compressed_context` 与 `sessions/archive` | 缩短过长聊天上下文 | 否 |
|
||||
| 长期记忆 | `backend/memory/MEMORY.md` | 保存跨会话的重要背景 | 否,除非转成正式业务对象 |
|
||||
| 长期记忆向量索引 | `backend/storage/memory_index` | RAG 模式按问题检索记忆片段 | 否 |
|
||||
| 原始模型轨迹 | `backend/raw_logs` | 调试模型请求、响应和工具调用 | 否,属于敏感技术日志 |
|
||||
| 项目业务记忆 | `project_assistant.sqlite3` | 项目事实、来源、快照、运行、问题、报告和决定 | 是 |
|
||||
| 组织经验记忆 | SQLite 中的经验版本和应用记录 | 在适用条件下复用已审核经验 | 审核发布后可作为检查依据 |
|
||||
| 时间与审计记忆 | `assistant_events` 与 `business_time_events` | 记录操作和阶段耗时 | 是,作为流程留痕 |
|
||||
|
||||
## 十四 通用聊天记忆
|
||||
|
||||
### 十四一 会话短期记忆
|
||||
|
||||
每个会话保存标题、创建和更新时间以及用户与助手消息。下一轮对话前,`SessionManager.load_session_for_agent` 合并连续助手消息,只把用户和助手正文重新放入上下文。工具调用的完整轨迹不直接全部塞回模型,而是单独保存在原始日志。
|
||||
|
||||
### 十四二 会话压缩
|
||||
|
||||
当会话过长时,压缩接口选取前一部分消息,调用模型生成不超过 500 字的摘要。原消息归档到 `sessions/archive`,摘要写入 `compressed_context`。下一轮把摘要作为开头的助手上下文。
|
||||
|
||||
摘要可能遗漏细节,因此重要业务事实不能只保存在聊天摘要中。正式项目数据必须进入项目事实、材料、问题或报告对象。
|
||||
|
||||
### 十四三 长期记忆
|
||||
|
||||
长期记忆文件是 `backend/memory/MEMORY.md`。当前 `backend/config.json` 中 `rag_mode` 为 `false`,因此整个文件作为系统提示词的一部分加载。切换为 RAG 模式后,系统不再把全文放入提示词,而是把文件切分为约 256 token、重叠 32 token 的片段,使用 `BAAI/bge-small-zh` 建立向量索引,并按问题检索前三个片段。
|
||||
|
||||
索引使用 MEMORY 文件的 MD5 判断是否需要重建,保存在 `backend/storage/memory_index`。索引加载失败时返回空检索结果并继续运行。
|
||||
|
||||
当前代码没有一个自动把每天聊天内容总结后写入 `MEMORY.md` 的后台学习流程。工作区规则可以要求 Agent 使用工具维护记忆,但那是受权限控制的操作约定,不应宣传为全自动学习。
|
||||
|
||||
### 十四四 原始模型轨迹
|
||||
|
||||
通用对话把完整 human、assistant 和 tool 消息写入 `{session_id}.msgs.jsonl`,把每次模型请求、响应、工具调用、token 用量和耗时写入另一份 JSONL。页面可以显示截短的工具结果,但完整工具消息保存在轨迹中。
|
||||
|
||||
原始轨迹用于故障排查,不是业务事实库。它可能包含业务文本,必须按敏感日志管理和设置保留周期。
|
||||
|
||||
## 十五 兴邦业务记忆
|
||||
|
||||
### 十五一 为什么业务运行不使用聊天记忆
|
||||
|
||||
业务检查必须可复现。如果模型继承某个用户前一次聊天中的项目编号、判断或材料片段,很容易造成跨项目污染。因此 `AgentManager.invoke_scoped` 为每次模型 Skill 创建隔离 Agent,不传入共享聊天历史。
|
||||
|
||||
### 十五二 SQLite 中的核心表
|
||||
|
||||
| 表 | 内容 |
|
||||
| --- | --- |
|
||||
| `assistant_objects` | 使用种类和编号保存项目、快照、运行、问题、报告、评审决定、组件版本、经验等 JSON 对象 |
|
||||
| `assistant_events` | 记录谁在什么时间对哪个项目执行什么动作 |
|
||||
| `assistant_idempotency` | 防止重复点击创建重复运行或报告 |
|
||||
| `assistant_jobs` | 保存持久任务状态、执行人、租约和尝试次数 |
|
||||
| `business_time_events` | 保存资料收集、预审、排队、评审办理和外部等待等时间区间 |
|
||||
|
||||
`assistant_objects` 采用乐观版本号。更新时可以要求客户端提交预期 revision,若页面数据已经过期,服务返回内容已更新,要求刷新后重试。快照和发布版本使用不可变写入。
|
||||
|
||||
### 十五三 项目事实与来源
|
||||
|
||||
项目对象保存标准字段 `values`、所有字段来源 `sources`、当前选择来源 `selected_sources`、材料编号、数据版本和待确认候选。字段来源包含证据编号、来源名称、定位、期间、口径、单位和币种。
|
||||
|
||||
### 十五四 输入快照与业务快照
|
||||
|
||||
输入快照在运行入队时创建,保存当时的项目对象和材料。业务快照在 Worker 执行数据卡后创建,加入数据集记录、映射结果、数据错误和内容哈希。模型和后续步骤只读取这份业务快照。
|
||||
|
||||
### 十五五 运行步骤和问题
|
||||
|
||||
运行保存方案、组件版本、模型配置、步骤、检查、证据、摘要和状态。每个步骤独立保存输入哈希、输出、错误和技术轨迹。问题保存稳定检查码、当前状态、证据、依据组件以及人工处理历史。
|
||||
|
||||
### 十五六 报告与评审决定
|
||||
|
||||
报告绑定运行、快照、模板和数据版本。系统章节来自冻结快照和运行结果,允许编辑的人工章节保存修改历史、确认和锁定状态。评审决定绑定评审员、时间、节点、意见、运行、快照、证据和依据组件。最终退回或批准还绑定六个节点决定引用。
|
||||
|
||||
## 十六 组织经验记忆
|
||||
|
||||
评审反馈不会直接变成规则。当前流程如下。
|
||||
|
||||
```text
|
||||
评审员记录具体反馈
|
||||
→ 创建经验草稿
|
||||
→ 写清触发条件、核查动作、例外、范围和有效期
|
||||
→ 在隔离留出样本上评测正例和反例
|
||||
→ 独立审核人发布
|
||||
→ 后续运行冻结已发布版本
|
||||
→ 条件匹配时生成检查并记录应用
|
||||
→ 不再适用时停用回滚
|
||||
```
|
||||
|
||||
经验匹配会检查数据域、项目或团队、行业、触发字段、例外字段和有效期。团队经验在发布前检查是否含来源项目编号、客户名称等专有信息。表达、事实或数据纠正不能直接升级为核查经验,必须先修正原对象。
|
||||
|
||||
隔离评测至少包含适用样本、条件不匹配样本、数据域隔离、过期样本、例外样本、无团队权限或其他项目样本,以及经验实际生成缺件检查的验证。评测通过不等于自动发布,还需要独立审核。
|
||||
|
||||
这套机制属于受控知识沉淀,不是模型参数训练。系统不会因为某个评审员点了一次不采纳就自动改变所有项目的规则。
|
||||
|
||||
## 十七 报告生成路径
|
||||
|
||||
报告生成不调用自由聊天记录。服务选择一个已完成或部分完成的业务运行,读取其快照、问题、证据和模板,按章节生成草稿。模板决定章节顺序和依赖字段。
|
||||
|
||||
客户经理使用 AI 预审报告模板,评审员使用独立评审报告或上会摘要模板。报告保存来源快照、运行编号、数据版本、模板版本和证据。项目数据或新运行变化后,旧报告标记为待更新,但不会被自动覆盖。用户选择按最新运行更新时,系统先生成候选内容,人工采纳后才替换正文。
|
||||
|
||||
提交正式评审时,当前报告作为不可覆盖的送审版本写入评审池。重新推送会创建新序号,旧送审版本保留。
|
||||
|
||||
## 十八 评审员独立复核路径
|
||||
|
||||
评审员从评审池领取项目后,系统记录领取时间并关闭排队等待时间。评审员方案重新运行独立复核,不直接继承客户经理的最终判断。
|
||||
|
||||
运行结果按六个业务节点组织:主体信息、财务与现金流、租赁物、增信措施、征信与涉诉、交易结构与还款。每个节点包含数据来源、AI 检查、依据组件和评审员结论。
|
||||
|
||||
评审员可以选择确认无异常、风险可控、要求补件、转现场核查、不采纳 AI 或不适用。需要专业判断的结论必须填写意见。批准前,所有节点必须有结论,且不能存在要求补件、转现场核查等未闭环决定。最终批准或退回由评审员执行,不由模型工具执行。
|
||||
|
||||
## 十九 时间和审计链
|
||||
|
||||
系统使用 `assistant_events` 保存动作事件,使用 `business_time_events` 保存阶段区间。阶段记录包括项目、阶段码、阶段名称、角色、时间类别、开始和结束时间、来源对象和详情。
|
||||
|
||||
客户经理提交报告时开始评审排队;评审员领取时结束排队并开始评审办理;退回或批准时结束评审办理。AI 预审和独立评审运行也有独立时间区间。
|
||||
|
||||
时效看板把时间分为人员处理、排队等待、外部等待和系统执行。该分类用于发现流程瓶颈,不应把所有等待时间都归因于工作人员或模型。
|
||||
|
||||
## 二十 权限与安全边界
|
||||
|
||||
### 二十一 身份和项目权限
|
||||
|
||||
后端在每个项目接口校验身份、角色和项目授权。客户经理只能使用面向客户经理的方案和模板;评审员必须领取项目后才能形成评审决定;配置管理员负责组件配置。演示身份选择器不是生产鉴权。
|
||||
|
||||
### 二十二 模型地址
|
||||
|
||||
业务模型从 `XINGBANG_LOCAL_MODEL_URL` 读取,只接受 HTTP 或 HTTPS 的本机、回环或内网地址,不允许在 URL 中嵌入用户名、密码、查询参数或片段。模型 Key 从环境变量读取。
|
||||
|
||||
### 二十三 工具权限
|
||||
|
||||
通用聊天 Agent 可以配置终端、Python、网页、文件和知识库等工具,因此生产代理只应向授权管理员开放。兴邦业务模型只使用五个只读业务工具,不开放终端、Python、网页、SQL 或文件写入。
|
||||
|
||||
### 二十四 超时和失败
|
||||
|
||||
通用聊天有空闲和总时长限制,超时后不会自动重放可能有副作用的工具。业务任务有方案级超时和工具预算。单个模型步骤失败时,其他确定性步骤保留,运行状态可能为部分完成。部分完成不能被解释为全部通过。
|
||||
|
||||
## 二十一 当前已实现与当前边界
|
||||
|
||||
| 能力 | 当前状态 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 多表 SQLite 演示库与多张数据卡 | 已实现 | 可演示 SQL 读取、字段映射和完整流程 |
|
||||
| 客户经理 AI 预审与报告 | 已实现 | 支持持久运行、问题、报告和送审 |
|
||||
| 评审池、领取、退回和批准 | 已实现 | 记录提交时间、等待时间和办理时间 |
|
||||
| 六节点独立评审 | 已实现 | 每个节点保存证据和人工结论 |
|
||||
| Skills 与规则版本冻结 | 已实现 | 运行保留版本和内容哈希 |
|
||||
| 经验草稿、评测、发布和应用 | 已实现 | 不自动成为正式制度 |
|
||||
| 通用会话记忆与 MEMORY RAG | 已实现 | 当前配置为 RAG 关闭 |
|
||||
| 生产统一身份和单点登录 | 需部署确认 | 演示角色选择器不能替代生产鉴权 |
|
||||
| 正式业务数据字典和阈值 | 需兴邦确认 | 演示字段和 120% 阈值不是制度 |
|
||||
| 正式 OCR 与大模型质量验收 | 需专项验证 | 当前代码不能替代模型效果和 OCR 验收 |
|
||||
| 自动把全部聊天转成企业经验 | 未实现且不建议直接实现 | 经验需要明确条件、评测和审核 |
|
||||
|
||||
## 二十二 如何判断一次运行是否可信
|
||||
|
||||
查看一次运行时,应按以下顺序核对:
|
||||
|
||||
1. 身份和项目是否正确,方案是否适用于当前角色。
|
||||
2. 输入快照是否对应预期数据版本。
|
||||
3. 数据卡是否读取成功,期间、口径、单位和币种是否一致。
|
||||
4. 确定性步骤是否完成,比例和比较是否有证据。
|
||||
5. 模型步骤是否完成必需工具读取,是否超时或超过预算。
|
||||
6. 模型问题是否通过结构和证据校验。
|
||||
7. 运行是否因项目中途更新而被标记为已取代。
|
||||
8. 问题是否经过人工处理,评审节点是否全部形成结论。
|
||||
9. 报告是否绑定正确快照和模板版本。
|
||||
10. 最终批准或退回是否由有权限的评审员完成。
|
||||
|
||||
只看到一段自然语言结论,不足以证明一次运行可信。可信运行必须同时具有数据快照、证据、组件版本、步骤状态和人工决定。
|
||||
|
||||
## 二十三 术语对照
|
||||
|
||||
| 页面用语 | 技术对象 | 解释 |
|
||||
| --- | --- | --- |
|
||||
| 项目数据版本 | `project.data_revision` | 人工确认或补件后递增 |
|
||||
| 本次运行 | `run` | 一次 AI 预审或独立评审任务 |
|
||||
| 来源快照 | `snapshot` | 数据卡读取和材料证据冻结结果 |
|
||||
| 问题 | `finding` | 跨运行维护状态的业务待办 |
|
||||
| 检查 | `check` | 某一步在某一次运行产生的原始判断 |
|
||||
| 数据卡 | `data_card` | SQL、映射、单位和口径配置 |
|
||||
| Skill 卡 | `skill` | 确定性执行器或模型方法 |
|
||||
| 规则卡 | `rule_set` | 阈值和条件判断集合 |
|
||||
| 方案 | `plan` | 数据卡、Skills、规则、模板和预算组合 |
|
||||
| 报告模板 | `template` | 报告章节和字段依赖 |
|
||||
| 经验 | `experience_version` | 经过评测和审核的条件化核查知识 |
|
||||
| 操作凭证 | 对象编号或事件编号 | 用于回到项目中追溯执行结果 |
|
||||
|
||||
## 二十四 代码定位索引
|
||||
|
||||
| 主题 | 主要文件 |
|
||||
| --- | --- |
|
||||
| 通用 Agent 创建与工具循环 | `backend/graph/agent.py` |
|
||||
| 系统提示词拼装 | `backend/graph/prompt_builder.py` |
|
||||
| 会话短期记忆和压缩摘要 | `backend/graph/session_manager.py` 与 `backend/api/compress.py` |
|
||||
| 长期记忆向量索引 | `backend/graph/memory_indexer.py` |
|
||||
| 数据卡读取与字段对齐 | `backend/services/assistant_data.py` |
|
||||
| 确定性 Skill 和规则执行 | `backend/services/skill_executor.py` |
|
||||
| 组件版本、发布与冻结 | `backend/services/component_registry.py` |
|
||||
| 项目、评审池、补件和决定 | `backend/services/project_assistant.py` |
|
||||
| 持久任务和执行顺序 | `backend/services/assistant_worker.py` |
|
||||
| 受限模型工具和输出校验 | `backend/services/assistant_model.py` |
|
||||
| 经验适用、评测和发布 | `backend/services/assistant_experiences.py` |
|
||||
| SQLite 对象、事件和时间区间 | `backend/services/assistant_store.py` |
|
||||
| 业务助手命令路由 | `backend/api/project_assistant.py` |
|
||||
| 客户经理与评审员工作台 | `src/app/xingbang-workbench/page.tsx` |
|
||||
| 业务助手 | `src/components/xingbang/BusinessAssistant.tsx` |
|
||||
|
||||
## 二十五 最终理解
|
||||
|
||||
当前兴邦项目把大模型放在一条受控的业务链中。数据卡解决数据从哪里来和如何对齐,确定性 Skills 解决可以程序化计算的问题,规则卡解决明确阈值和条件,模型 Skill 解决需要阅读方法和材料的语义核查,业务方案决定本次运行使用哪些组件,记忆系统分别保存聊天上下文、业务快照、组织经验和审计时间。
|
||||
|
||||
系统的核心不是让模型记住更多内容,而是让每次运行只读取被授权且已经冻结的内容,让所有输出经过证据和结构校验,再由客户经理和评审员完成业务闭环。
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,415 @@
|
||||
# 兴邦金租对公智能评审平台详细演示手册
|
||||
|
||||
适用对象:售前演示人员、业务负责人、客户经理、评审员、配置管理员和项目验收人员
|
||||
文档版本:v1.0
|
||||
代码基线:Git b10093f
|
||||
建议演示时长:完整演示四十五至六十分钟,精简演示二十五分钟
|
||||
|
||||
## 一 演示目的与结论
|
||||
|
||||
本次演示要让观看人员完整理解一笔对公融资租赁项目如何从材料进入系统,经过客户经理 AI 预审、问题闭环、报告生成和正式提交,再进入评审池,由评审员领取、独立复核、退回或批准,最后形成评审报告和全流程时间记录。
|
||||
|
||||
演示时始终强调三点。第一,AI 输出是可复核的业务建议,审批决定仍由评审员作出。第二,每一项判断都能回到数据字段、材料位置、数据卡、Skill、规则版本和运行快照。第三,普通业务人员只使用业务助手、项目工作台、项目评审池和业务时效四个主要入口,不需要理解底层模型、SQL 或 Agent 编排。
|
||||
|
||||
演示数据、项目名称和百分比阈值均为虚构样例。演示覆盖率阈值 120% 只用于展示规则命中和版本追溯,不代表兴邦金租正式制度。
|
||||
|
||||
## 二 演示角色与各自任务
|
||||
|
||||
| 身份 | 主要任务 | 演示时要证明的能力 | 不应由该身份完成的事项 |
|
||||
| --- | --- | --- | --- |
|
||||
| 客户经理 | 补充材料、确认来源、发起 AI 预审、处理问题、生成预审报告、提交评审 | 少录入、少跳转、提交前自查、问题能闭环 | 配置生产数据连接、修改已发布规则、代替评审员批准 |
|
||||
| 评审员 | 从评审池领取项目、独立复核六个必审领域、填写意见、退回或批准、生成评审报告 | 全链路独立判断、每个结论可追溯、等待和办理时间连续 | 沿用客户经理结论而不复核、让 AI 自动作审批决定 |
|
||||
| 配置管理员 | 管理模型、数据连接、数据卡、Skills、规则、方案和模板 | 数据合规由部门统一控制、组件可版本化发布和回滚 | 代替客户经理补材料、代替评审员形成业务结论 |
|
||||
|
||||
演示环境通过右上角身份选择器切换身份。该选择器仅用于本地或验收演示,不是生产鉴权。正式环境应由统一身份、令牌或单点登录在服务端确定角色和项目权限。
|
||||
|
||||
## 三 演示项目与推荐故事线
|
||||
|
||||
| 项目 | 当前特征 | 推荐用途 |
|
||||
| --- | --- | --- |
|
||||
| DEMO-A 兴华精密制造 | 材料相对完整,处于报告准备阶段 | 快速展示无明显问题时如何生成预审报告 |
|
||||
| DEMO-B 恒远设备 | 融资 5000 万元,租赁物价值 5400 万元,经营现金流缺失,财报与尽调负债不一致,设备清单有重复项 | 完整主线演示,最适合展示四类问题、补件、复跑和追溯 |
|
||||
| DEMO-C 新能服务 | 依赖特定回款但缺少回款依据,已进入独立评审场景 | 展示评审员专用 Skill、经验沉淀和退回流程 |
|
||||
|
||||
完整演示以 DEMO-B 为主线。它初始会触发四类问题:负债金额来源不一致、经营现金流资料缺失、设备清单重复、租赁物价值覆盖率低于演示阈值。覆盖率由 5400 除以 5000 得到 1.0800,因此会命中 1.2 的演示阈值。演示人员可以先保留这些问题展示 AI 预审和证据链,再通过补件减少问题,最后走正式提交和评审流程。
|
||||
|
||||
## 四 演示前准备
|
||||
|
||||
### 四一 环境检查
|
||||
|
||||
1. 使用部署方提供的系统地址进入首页。不要在讲解中固定某一个 IP 地址,避免客户环境变化后手册失效。
|
||||
2. 确认顶部导航能看到业务助手、项目工作台、项目评审池和业务时效。配置管理员还会看到配置治理。
|
||||
3. 确认右上角显示当前身份。完整演示至少需要能够切换客户经理和评审员;配置治理演示需要配置管理员身份。
|
||||
4. 确认 DEMO-A、DEMO-B、DEMO-C 三个项目能正常读取。如果之前做过多次演示,先确认 DEMO-B 当前状态是否仍适合主线。历史运行保留是正常现象,不应为了演示删除审计记录。
|
||||
5. 确认后端和前端健康。页面若提示连接失败,应先检查服务、模型配置和访问凭据,不要把技术失败解释为业务结论。
|
||||
6. 准备一份不含真实客户信息的 TXT、CSV、DOCX、XLSX、PDF 或图片材料,用于展示上传和解析候选。单文件不得超过 10 MB。
|
||||
|
||||
### 四二 演示数据复位原则
|
||||
|
||||
系统保留运行、问题、报告和评审历史。重复演示时不要承诺页面一定与第一次完全相同。开始前应先打开 DEMO-B,记录当前数据版本、未处理问题数、最近运行状态和当前阶段。若项目已经评审通过,可以改用 DEMO-C 展示评审,或明确说明当前记录为上一次演示留下的可追溯历史。
|
||||
|
||||
### 四三 演示窗口准备
|
||||
|
||||
- 浏览器缩放建议保持 100%。
|
||||
- 关闭会遮挡页面的下载栏、开发者工具和无关标签页。
|
||||
- 预先打开系统首页,但不要提前进入深层页面,让客户看到从统一入口开始办理的过程。
|
||||
- 准备一个计时器。完整演示中,每个模块讲解不宜超过八分钟。
|
||||
|
||||
## 五 完整演示路线
|
||||
|
||||
| 阶段 | 建议时长 | 身份 | 页面 | 演示结果 |
|
||||
| --- | ---: | --- | --- | --- |
|
||||
| 统一入口与项目选择 | 5 分钟 | 客户经理 | 业务助手 | 用一句话查询状态并进入 DEMO-B |
|
||||
| 材料与数据来源 | 8 分钟 | 客户经理 | 项目工作台 | 看到材料解析、字段来源和待确认候选 |
|
||||
| AI 预审与问题追溯 | 10 分钟 | 客户经理 | 项目工作台 | 形成四类问题并逐项查看证据和解决建议 |
|
||||
| 补件与再次预审 | 7 分钟 | 客户经理 | 项目工作台 | 修改业务事实后生成新的数据版本和运行记录 |
|
||||
| 报告与送审 | 6 分钟 | 客户经理 | 报告与项目评审池 | 生成 AI 预审报告并提交,看到提交时间与等待时间 |
|
||||
| 独立评审 | 12 分钟 | 评审员 | 项目评审池与全链路评审 | 领取项目,逐节点确认,退回或批准 |
|
||||
| 时效和配置治理 | 8 分钟 | 评审员或管理员 | 业务时效与配置治理 | 解释时间分布和组件版本治理 |
|
||||
|
||||
## 六 客户经理演示步骤
|
||||
|
||||
### 六一 从业务助手开始
|
||||
|
||||
操作:保持客户经理身份,进入业务助手。在正在处理的项目下拉框选择 DEMO-B 恒远设备。
|
||||
|
||||
讲解:业务助手是业务人员的统一入口。它和项目页面按钮使用同一套权限、Skills、规则和留痕。普通业务人员不需要选择底层 mini-openclaw 页面,也不需要知道模型地址或 SQL。
|
||||
|
||||
先点击查看项目状态,或输入以下任一指令:
|
||||
|
||||
```text
|
||||
这个项目到哪一步了
|
||||
我接下来要做什么
|
||||
现在有哪些问题
|
||||
还缺什么材料
|
||||
```
|
||||
|
||||
预期结果:助手返回 DEMO-B 当前阶段、待处理问题数、材料候选情况或下一步,并提供查看项目中的结果链接。用户消息靠右显示并带本人图标,助手消息靠左显示并带机器人图标。执行型操作会显示操作凭证编号。
|
||||
|
||||
演示重点:回答不是简单复述用户问题。它读取当前项目对象后返回确定的状态或下一步。若输入过于宽泛,助手会要求用户选择进度、下一步、问题或材料,而不会伪造分析结果。
|
||||
|
||||
### 六二 进入项目工作台
|
||||
|
||||
操作:点击进入项目,或点击助手回答下方的查看项目中的结果。
|
||||
|
||||
先指给客户看四个位置:当前身份、项目编号与客户名称、当前阶段、建议下一步。然后查看三张概览卡:当前数据版本、未处理问题、最近运行。
|
||||
|
||||
预期结果:点击未处理问题卡可以直接跳转到材料与问题,不需要用户在页面中寻找。项目当前没有运行时,建议下一步会提示发起 AI 预审;存在问题时会提示处理具体问题;报告完成后会提示提交正式评审。
|
||||
|
||||
### 六三 展示材料档案与解析
|
||||
|
||||
操作:切换到材料与问题,查看材料档案。点击已有材料旁的查看解析。
|
||||
|
||||
讲解:查看解析展示系统从材料中识别出的候选字段、值、页码或表格位置和解析警告。它不是最终业务事实。候选值只有经过人工确认,才会进入新的项目数据版本。下载原件用于与解析结果对照。
|
||||
|
||||
如需现场上传,点击上传项目材料并选择准备好的演示文件。上传完成后,页面提示材料已经归档并生成若干候选字段。
|
||||
|
||||
预期结果:材料列表出现新版本;查看解析能看到候选值。项目事实不会因上传动作自动被覆盖。
|
||||
|
||||
讲解边界:扫描件或图片无法可靠识别时,页面会给出警告并要求补充。XLSX 公式读取已有缓存值,服务端不会重新计算公式。任何材料内容都视为不可信数据,材料中的指令文字不会成为系统命令。
|
||||
|
||||
### 六四 发起第一次 AI 预审
|
||||
|
||||
操作:回到项目概览。在本次 AI 预审要求中输入:
|
||||
|
||||
```text
|
||||
重点核对现金流、财报与尽调负债口径、设备清单重复情况和租赁物价值覆盖率,并说明需要补充的材料。
|
||||
```
|
||||
|
||||
确认业务检查方案选择客户经理 AI 预审,点击开始 AI 预审。
|
||||
|
||||
预期结果:系统提示任务已经进入队列。页面每隔约一点五秒刷新,运行状态从已排队变为检查中,最终为已完成、部分完成或未完成。离开页面不会丢失任务。
|
||||
|
||||
讲解:点击后不是浏览器一直等待模型。后端先冻结项目数据、材料、数据卡、Skills、规则、模板和版本哈希,再创建可恢复的后台任务。即使页面关闭,任务仍能继续;进程异常后,租约过期任务可以重新领取。
|
||||
|
||||
### 六五 解读第一次检查结果
|
||||
|
||||
预期看到的四类业务结果如下。
|
||||
|
||||
| 检查项 | 演示数据 | 预期判断 | 业务含义 |
|
||||
| --- | --- | --- | --- |
|
||||
| 负债金额来源一致性 | 财报 13200 万元,尽调 12800 万元 | 发现问题或待复核 | 两个来源口径不一致,需要确认期间、范围和依据 |
|
||||
| 经营现金流完整性 | 当前为空 | 材料不足 | 不能在缺少材料时推断为正常 |
|
||||
| 设备清单重复项 | 重复数量 1 | 发现问题 | 应按设备唯一编号核对和去重 |
|
||||
| 租赁物价值覆盖率 | 5400 除以 5000 等于 1.0800 | 低于演示阈值 | 触发 1.2 的演示规则,仅用于展示 |
|
||||
|
||||
操作:依次展开每个业务步骤。先读页面上的业务判断,再查看问题说明、建议动作和证据来源。最后展开技术追溯信息,指出输入快照编号、组件版本和本次运行编号。
|
||||
|
||||
讲解:确定性检查先执行,模型核查后执行。若内网模型未配置,模型步骤会明确显示未执行或失败,前面的确定性结果仍然保留。部分完成不等于全部通过。
|
||||
|
||||
### 六六 逐项查看问题和解决方式
|
||||
|
||||
操作:点击未处理问题卡,或进入材料与问题。选中一个问题,展示三层信息。
|
||||
|
||||
1. 数据来源:字段名称、当前值、材料名称、数据卡或材料定位。
|
||||
2. AI 辅助判断:通过、发现问题、材料不足、不适用或执行失败。
|
||||
3. 怎么解决:系统给出的补件、核对口径、去重或人工复核建议。
|
||||
|
||||
操作:展开查看规则与技术追溯,指出 Skill 或规则的稳定编号和版本。强调页面先给业务人员看结论和动作,技术编号放在最后。
|
||||
|
||||
### 六七 补件并形成新数据版本
|
||||
|
||||
操作:在人工补充字段中依次选择需要处理的字段。演示时至少完成以下两项:
|
||||
|
||||
- 经营现金流填写 900,处理说明填写已核对 2025 年合并口径现金流材料。
|
||||
- 尽调负债填写 13200,处理说明填写已与财报同期间同口径复核。
|
||||
|
||||
如需完整闭环,再将重复设备数量填写 0,并说明已经按设备唯一编号去重。
|
||||
|
||||
点击保存补件。每保存一次,项目数据版本会增加,原来源和变更说明保留。
|
||||
|
||||
重要说明:融资金额和租赁物价值属于核心方案字段,当前补件表单不允许随意覆盖。演示时不要承诺业务人员可以直接在此修改所有核心字段。
|
||||
|
||||
### 六八 再次 AI 预审并比较变化
|
||||
|
||||
操作:回到项目概览,重新点击开始 AI 预审。
|
||||
|
||||
预期结果:系统生成新的运行编号和输入快照。已经补齐的现金流、负债口径和重复设备问题会减少或进入待复核。覆盖率问题仍可能保留,因为 5400 除以 5000 仍低于演示阈值。
|
||||
|
||||
演示重点:选择历史运行可以对照补件前后的结果。旧运行不会被新结果覆盖;它仍然绑定旧数据版本、旧快照和旧组件版本。
|
||||
|
||||
### 六九 生成 AI 预审报告
|
||||
|
||||
操作:切换到报告。选择 AI 预审意见模板和最新一次已结束运行,点击生成报告。
|
||||
|
||||
预期结果:系统生成报告草稿,显示报告状态、来源快照和章节。项目概况和主要风险等系统章节不可随意改写;客户经理说明等允许编辑的章节可以保存修改。确认内容后可以确认并锁定,必要时解除锁定。
|
||||
|
||||
讲解:报告不是把聊天回答复制到 Word。它按已发布模板组装当前运行、问题、证据和人工说明。报告冻结模板、数据快照、Skills 和规则版本。点击导出 Word 可得到可读取的 DOCX。
|
||||
|
||||
### 六十 提交正式评审
|
||||
|
||||
操作:在报告页点击提交正式评审,确认二次提示。
|
||||
|
||||
预期结果:系统进入项目评审池,并显示刚刚提交。卡片包含客户经理提交时间、当前等待时间、送审版本和当前状态。
|
||||
|
||||
讲解:客户经理提交的是一个不可覆盖的送审版本。项目仍可撤回当前版本;重新推送时创建新的送审版本,旧版本和评审留痕不会丢失。
|
||||
|
||||
### 六十一 展示客户经理的评审池
|
||||
|
||||
操作:保持客户经理身份进入项目评审池。
|
||||
|
||||
展示内容:本人提交的项目、提交时间、已等待多久、当前是否被评审员领取、送审版本、查看项目、撤回或重新推送按钮。
|
||||
|
||||
可选演示:在评审员领取之前点击撤回。页面提示项目已撤回,历史版本保留。再点击重新推送,页面显示新的送审版本。若项目已经被领取,应根据现场状态决定是否演示撤回,不要为了演示破坏正在进行的评审记录。
|
||||
|
||||
## 七 评审员演示步骤
|
||||
|
||||
### 七一 进入项目评审池
|
||||
|
||||
操作:切换为评审员身份,进入项目评审池。
|
||||
|
||||
讲解:评审员不是从全部项目中随意挑选。客户经理正式提交后,项目才进入待评审池。池子按等待时间倒序排列,优先暴露等待时间长的项目。
|
||||
|
||||
卡片应显示客户经理提交时间、已等待多久、送审版本、是否已领取和办理操作。提交时间与客户经理侧使用同一条时间记录。
|
||||
|
||||
### 七二 领取项目并停止排队计时
|
||||
|
||||
操作:找到 DEMO-B,点击领取并开始评审。
|
||||
|
||||
预期结果:系统跳转到项目工作台,全局身份显示评审员独立评审。评审池等待计时停止,评审办理计时开始。再次进入时按钮显示继续办理。
|
||||
|
||||
讲解:领取动作创建评审分配记录,避免不同评审员同时处理同一项目。只有领取该项目的评审员才能保存独立评审结论。
|
||||
|
||||
### 七三 发起评审员独立检查
|
||||
|
||||
操作:在本次独立评审关注点中输入:
|
||||
|
||||
```text
|
||||
独立核对主体信息、财务与现金流、租赁物、增信措施、征信涉诉、交易结构与还款,不沿用客户经理结论。
|
||||
```
|
||||
|
||||
确认方案为评审员独立复核,点击发起独立评审。
|
||||
|
||||
讲解:客户经理预审和评审员复核共用底层 mini-openclaw Agent 基座,但使用不同方案和 Skill。评审员专用方案会加入独立评审与报告证据核查方法,并重新冻结输入和组件版本,不把客户经理结论当成最终事实。
|
||||
|
||||
### 七四 全链路六节点评审
|
||||
|
||||
操作:切换到全链路评审。页面按以下六个节点展示。
|
||||
|
||||
| 节点 | 评审关注点 | 可查看来源示例 |
|
||||
| --- | --- | --- |
|
||||
| 主体信息 | 项目、客户、主体资格和基础信息 | 项目主数据、客户主数据、主体材料 |
|
||||
| 财务与现金流 | 财务报表、尽调口径、现金流和偿债能力 | 财务数据卡、尽调材料、计算结果 |
|
||||
| 租赁物 | 权属、适格性、特定化、估值、重复项和保险 | 租赁物台账、设备清单、保险材料 |
|
||||
| 增信措施 | 保证、抵质押、关联关系和集中度 | 增信数据卡、合同或说明材料 |
|
||||
| 征信与涉诉 | 逾期、征信报告日期、诉讼和执行信息 | 征信与司法数据卡、附件 |
|
||||
| 交易结构与还款 | 融资金额、期限、还款来源和回款依赖 | 项目融资卡、还款来源卡、业务说明 |
|
||||
|
||||
每个节点从左到右看三部分:数据与来源、AI 辅助判断、评审员结论。AI 认为有问题或材料不足的节点用重点颜色标识。评审员可以查看来源,再选择确认无异常、风险可控、要求补件、转现场核查、不采纳 AI 或不适用。需要专业判断的选项必须填写意见。
|
||||
|
||||
演示重点:能选的结论通过按钮完成,尽量减少自由输入。保存后自动记录评审员、时间、数据快照、证据编号、Skill 和规则版本。
|
||||
|
||||
### 七五 演示退回
|
||||
|
||||
操作:在至少一个节点选择要求补件并填写意见,例如请补充最新银行流水并说明经营现金流口径。在页面底部填写退回原因,点击退回补充材料。
|
||||
|
||||
预期结果:项目状态变为退回补件。客户经理再次进入项目列表时,项目卡片使用红色底色重点标识,并显示退回时间和退回原因。
|
||||
|
||||
讲解:退回不是删除或重建项目。客户经理在同一项目中补材料、重新预审、更新报告并重新推送。新送审版本覆盖当前池内版本,历史送审版本和原评审意见继续保留。
|
||||
|
||||
### 七六 客户经理处理退回并重新推送
|
||||
|
||||
操作:切换回客户经理,进入红色退回项目。先查看退回要求,上传或补充材料,确认候选来源,重新发起 AI 预审,生成或更新预审报告,再次提交正式评审。
|
||||
|
||||
预期结果:项目评审池出现更高的送审版本号。评审员可以对比新旧快照和问题变化。
|
||||
|
||||
### 七七 演示批准和生成评审报告
|
||||
|
||||
如时间允许,改用已经满足条件的项目,或在所有六个节点完成可闭环结论后点击批准并生成评审报告。
|
||||
|
||||
批准按钮只有在以下条件同时满足时可用:已经完成独立评审运行;六个必审节点都有结论;不存在要求补件、转现场核查等未闭环结论。
|
||||
|
||||
预期结果:项目进入评审通过,系统生成独立评审报告草稿。评审员可编辑允许编辑的章节,确认并锁定后定稿,也可选择上会审议摘要模板。
|
||||
|
||||
## 八 业务时效演示步骤
|
||||
|
||||
操作:进入业务时效。先看顶部汇总,再查看阶段耗时分布、时间构成和项目时间轴。点击 DEMO-B 返回该项目的完整时间轨迹。
|
||||
|
||||
讲解四类时间:
|
||||
|
||||
| 类别 | 含义 | 业务解释 |
|
||||
| --- | --- | --- |
|
||||
| 人员处理 | 客户经理或评审员实际办理 | 可观察材料整理、报告修改和评审处理耗时 |
|
||||
| 排队等待 | 已提交但尚未被评审员领取 | 反映评审资源和分配效率 |
|
||||
| 外部等待 | 等待客户或外部机构补充材料 | 不应误算为系统运行慢 |
|
||||
| 系统执行 | AI 预审、规则检查和报告自动处理 | 用于衡量自动化执行时间 |
|
||||
|
||||
演示重点:系统自动记录开始和结束时间,不要求业务人员额外打卡。时效看板用于发现时间消耗位置,不用于简单评价个人绩效。演示数据量很小,不应宣称已经完成大规模并发性能验证。
|
||||
|
||||
## 九 配置管理员演示步骤
|
||||
|
||||
### 九一 进入配置治理
|
||||
|
||||
操作:切换为配置管理员,进入配置治理。
|
||||
|
||||
说明:配置管理员负责底层模型和数据合规边界。普通客户经理和评审员不自行配置数据库密码、模型 Key 或生产 SQL。
|
||||
|
||||
### 九二 数据连接与数据卡
|
||||
|
||||
依次展示数据连接和数据卡。说明数据连接保存数据库类型、只读边界、环境变量引用和表白名单;数据卡保存参数化 SQL、源字段到标准字段的映射、期间、口径、币种和单位。
|
||||
|
||||
演示数据卡可以从多张表读取指定字段。SQL 使用服务端参数 `:project_id`,只允许一条 SELECT 或 WITH SELECT。模型没有 SQL 工具,不会自己拼接查询。
|
||||
|
||||
### 九三 Skills 能力库
|
||||
|
||||
展示客户经理与评审员可用的金租对公业务 Skills,包括承租人及还款能力、租赁物适格性与权属、售后回租真实性、增信与集中度、独立评审与报告证据核查。
|
||||
|
||||
如需展示本地导入,选择包含 `SKILL.md` 的文件夹。系统只读取 Skill 说明,不执行文件夹中的 Python 或 Shell 脚本。导入后形成草稿或本机预览版本,正式环境需要独立审核发布。
|
||||
|
||||
### 九四 规则卡与业务方案
|
||||
|
||||
展示演示覆盖规则。解释规则卡适合数值阈值、必填、对比和确定性条件;Skill 适合方法步骤和受控语义核查;业务方案把数据卡、Skills、规则和报告模板组合成一次可运行配置。
|
||||
|
||||
重点展示客户经理 AI 预审和评审员独立复核不是同一张方案。它们可以共用基础数据和确定性能力,但评审员方案增加独立复核方法、不同模板和更高的工具调用预算。
|
||||
|
||||
### 九五 模板与版本治理
|
||||
|
||||
展示 AI 预审报告、独立评审报告和上会审议摘要模板。说明组件先形成草稿,再评测、审核和发布。运行开始后会冻结当前发布版本和内容哈希;后来修改规则不会改写历史结果。发布错误版本时可以回滚发布指针,历史运行仍保留原版本。
|
||||
|
||||
## 十 可追溯性专题演示
|
||||
|
||||
选取一个问题,按以下顺序完成一次从结论到源数据的穿透。
|
||||
|
||||
1. 在项目概览看到问题标题和状态。
|
||||
2. 进入材料与问题查看 AI 判断和解决建议。
|
||||
3. 打开数据来源,确认字段值、材料名称、页码或数据卡字段。
|
||||
4. 打开规则与技术追溯,确认 Skill 或规则编号和版本。
|
||||
5. 回到运行历史,确认运行编号、输入快照和数据版本。
|
||||
6. 打开报告,确认报告绑定相同的快照和运行。
|
||||
7. 在业务历史中查看发起人、操作时间和后续人工处理。
|
||||
|
||||
讲解:可追溯不等于只展示一串 JSON。业务页面先给出人能看懂的判断、来源和建议;技术编号用于审计和复现。
|
||||
|
||||
## 十一 异常场景与现场处理
|
||||
|
||||
| 页面现象 | 含义 | 现场处理方式 |
|
||||
| --- | --- | --- |
|
||||
| 访问令牌无效 | 云端身份凭据缺失或过期 | 刷新身份或重新登录,不要反复提交业务动作 |
|
||||
| AI 预审部分完成 | 部分步骤成功,部分步骤未执行或失败 | 展开步骤看失败原因;已成功的确定性结果仍可查看 |
|
||||
| 模型未配置 | 语义模型步骤不可用 | 说明确定性 Skills 和规则仍能运行;配置管理员检查内网模型 URL 与 Key |
|
||||
| 数据卡读取失败 | 数据连接、SQL、表白名单或字段口径异常 | 到配置治理检查数据卡,不让模型猜测缺失数据 |
|
||||
| 材料解析无候选 | 文件无可识别文本或扫描质量较差 | 下载原件核对,补充清晰文件或人工确认字段 |
|
||||
| 按钮不可点击 | 前置条件未满足 | 查看按钮提示;通常需要先领取、先运行、填写意见或完成六个节点 |
|
||||
| 提交后评审池未出现 | 当前报告不是最新数据版本或请求失败 | 刷新项目和评审池,检查报告状态及页面错误提示 |
|
||||
| 时间轨迹为空 | 该项目还没有对应阶段事件 | 先执行提交、领取或运行;不要手工伪造耗时 |
|
||||
|
||||
## 十二 精简演示方案
|
||||
|
||||
只有二十五分钟时,按以下顺序演示:
|
||||
|
||||
1. 客户经理在业务助手选择 DEMO-B,询问项目状态和下一步。
|
||||
2. 进入项目概览,点击未处理问题,展示四类问题中的两类及证据来源。
|
||||
3. 发起 AI 预审,解释冻结快照和确定性检查优先。
|
||||
4. 生成 AI 预审报告并提交正式评审。
|
||||
5. 切换评审员,在评审池看到提交时间和等待时长,领取项目。
|
||||
6. 展示六节点全链路评审,选择一个节点填写意见并演示退回。
|
||||
7. 切回客户经理,展示红色退回项目卡。
|
||||
8. 打开业务时效,说明人员处理、排队、外部等待和系统执行四类时间。
|
||||
9. 用一分钟说明配置管理员统一管理数据卡、Skills、规则、方案和模板。
|
||||
|
||||
## 十三 建议讲解话术
|
||||
|
||||
### 开场话术
|
||||
|
||||
今天演示的是一笔对公融资租赁项目从客户经理提交前自查到评审员独立评审的完整过程。我们重点看三个结果:业务人员是否少操作,AI 判断是否有来源,客户经理提交和评审员办理是否能在同一条时间线上衔接。
|
||||
|
||||
### AI 预审话术
|
||||
|
||||
点击开始 AI 预审后,系统会冻结当时的项目数据、材料、数据卡、Skills 和规则版本。金额比较和比例计算先由确定性程序完成;需要语义理解时,模型只能通过授权的只读工具读取本项目快照。缺少证据时,系统应显示材料不足,而不是给出通过结论。
|
||||
|
||||
### 评审话术
|
||||
|
||||
评审员会对主体、财务、租赁物、增信、征信涉诉和交易还款六个领域逐项形成结论。AI 高亮需要关注的位置,但批准、退回和专业意见由评审员操作。每个结论都会绑定来源快照、证据和规则版本。
|
||||
|
||||
### 经验沉淀话术
|
||||
|
||||
评审意见可以形成经验草稿,但不会自动变成全公司的规则。经验需要写清适用条件、例外条件、核查动作和有效期,再经过隔离评测与独立审核发布。后续运行只有在项目、数据域、行业、团队和条件匹配时才会使用它。
|
||||
|
||||
## 十四 常见提问与回答
|
||||
|
||||
### AI 会不会直接修改数据库
|
||||
|
||||
不会。数据卡由服务端执行受控只读 SQL,模型没有 SQL 工具。上传材料生成候选字段,必须经过人工确认才更新项目事实。
|
||||
|
||||
### AI 会不会替评审员批准
|
||||
|
||||
不会。AI 输出检查结果和建议。最终批准或退回必须由已领取项目的评审员操作,且批准前六个必审节点必须完成。
|
||||
|
||||
### 每个人写报告不一样怎么处理
|
||||
|
||||
系统通过可配置模板统一报告结构,同时保留允许编辑的人工判断章节。系统生成的事实和风险章节绑定运行快照;人工章节保留编辑、确认、锁定和历史版本。
|
||||
|
||||
### Skills 和规则是否相同
|
||||
|
||||
两者职责不同。规则卡处理阈值、比较和必填等确定条件。Skill 描述业务方法,或由受控执行器和模型完成步骤。业务方案负责把数据卡、Skills、规则和模板组合起来。
|
||||
|
||||
### 客户经理和评审员是否共用同一套能力
|
||||
|
||||
共用底层 Agent 基座、标准字段和部分确定性 Skills,但业务方案不同。评审员使用独立复核 Skill 和评审报告模板,并重新运行检查,不继承客户经理的最终结论。
|
||||
|
||||
### 系统是否会自动学习所有意见
|
||||
|
||||
不会。评审反馈先形成经验草稿,经过适用性评测、独立审核和发布后才可能用于后续项目。表达或事实纠正不会自动升级为规则。
|
||||
|
||||
## 十五 演示验收清单
|
||||
|
||||
演示结束前逐项确认:
|
||||
|
||||
- 业务助手能选择项目,回答状态、下一步、问题和缺失材料。
|
||||
- 用户消息与助手消息左右清楚,执行动作显示操作凭证。
|
||||
- 客户经理能打开项目、上传材料、查看解析候选并人工确认。
|
||||
- AI 预审能创建持久任务,展示已排队、检查中和最终状态。
|
||||
- 问题能看到数据来源、AI 判断、解决建议和技术追溯。
|
||||
- 报告能从指定运行生成,并能导出 Word。
|
||||
- 客户经理提交后,评审池显示提交时间、等待时间和送审版本。
|
||||
- 评审员能领取项目,等待计时停止,办理计时开始。
|
||||
- 六个必审节点都能查看来源、AI 判断和人工结论。
|
||||
- 退回需要原因,客户经理项目卡出现红色重点标识。
|
||||
- 批准需要完成全部节点且没有未闭环事项。
|
||||
- 业务时效能区分人员处理、排队、外部等待和系统执行。
|
||||
- 配置管理员能说明数据连接、数据卡、Skills、规则、方案和模板的版本关系。
|
||||
- 任一历史运行和报告都能回到数据快照、证据编号和组件版本。
|
||||
|
||||
## 十六 演示结束后的交付说明
|
||||
|
||||
演示完成后,应保存本次使用的系统版本、项目编号、运行编号、报告编号和发现的问题。客户确认的是当前版本的业务流程与可追溯能力,不等于演示阈值、演示数据、模型效果或身份切换方式已经成为生产制度。生产上线前仍需完成正式数据字典、规则阈值、报告模板、模型和 OCR 选型、统一身份认证、权限矩阵、日志保留周期及数据脱敏要求的确认。
|
||||
@@ -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())
|
||||
@@ -0,0 +1,10 @@
|
||||
import type {Metadata} from "next";
|
||||
|
||||
export const metadata:Metadata={
|
||||
title:"演示数据复位",
|
||||
robots:{index:false,follow:false,noarchive:true},
|
||||
};
|
||||
|
||||
export default function ResetDataLayout({children}:{children:React.ReactNode}){
|
||||
return children;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import {useState} from "react";
|
||||
import "./resetdata.css";
|
||||
|
||||
const RESET_ENDPOINT="/api/risk-agent/project-assistant/demo/reset";
|
||||
|
||||
export default function ResetDataPage(){
|
||||
const [busy,setBusy]=useState(false);
|
||||
|
||||
async function reset(){
|
||||
if(!window.confirm("确认恢复演示初始数据?DEMO-A、DEMO-B、DEMO-C 当前演示操作将被归档。"))return;
|
||||
setBusy(true);
|
||||
try{
|
||||
const response=await fetch(RESET_ENDPOINT,{
|
||||
method:"POST",
|
||||
headers:{"Content-Type":"application/json","X-Xingbang-Demo-Role":"admin"},
|
||||
body:JSON.stringify({confirmation:"RESET-DEMO-A-B-C"}),
|
||||
});
|
||||
const result=await response.json();
|
||||
if(!response.ok)throw Error(typeof result.detail==="string"?result.detail:"数据恢复失败");
|
||||
window.alert("演示数据已恢复到初始状态。");
|
||||
}catch(error){
|
||||
window.alert(error instanceof Error?error.message:"数据恢复失败,请稍后重试。");
|
||||
}finally{setBusy(false)}
|
||||
}
|
||||
|
||||
return <main className="reset-page">
|
||||
<button type="button" onClick={reset} disabled={busy} aria-label="一键恢复演示数据">
|
||||
{busy?"正在恢复……":"一键恢复演示数据"}
|
||||
</button>
|
||||
</main>;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
.reset-page{
|
||||
min-height:100vh;
|
||||
display:grid;
|
||||
place-items:center;
|
||||
background:#f4f7f6;
|
||||
}
|
||||
|
||||
.reset-page button{
|
||||
min-width:260px;
|
||||
min-height:64px;
|
||||
padding:18px 30px;
|
||||
border:0;
|
||||
border-radius:14px;
|
||||
background:#c83b31;
|
||||
color:#fff;
|
||||
font-size:20px;
|
||||
font-weight:800;
|
||||
cursor:pointer;
|
||||
box-shadow:0 14px 34px rgba(152,35,28,.24);
|
||||
transition:background .15s ease,transform .15s ease,box-shadow .15s ease;
|
||||
}
|
||||
|
||||
.reset-page button:hover:not(:disabled){
|
||||
background:#ad2f27;
|
||||
transform:translateY(-1px);
|
||||
box-shadow:0 17px 38px rgba(152,35,28,.3);
|
||||
}
|
||||
|
||||
.reset-page button:focus-visible{
|
||||
outline:4px solid rgba(200,59,49,.25);
|
||||
outline-offset:4px;
|
||||
}
|
||||
|
||||
.reset-page button:disabled{
|
||||
opacity:.65;
|
||||
cursor:wait;
|
||||
}
|
||||
Reference in New Issue
Block a user