Files
QiufengandClaude Opus 4.8 5e51dc3f56 SNAPSHOT W7 已部署稳定态 — 凯迪ERP+OA一体化平台 (MET 73.3%)
恢复点(restore point)。别人改崩后可 git reset --hard 回到此提交。

== 此快照内容 ==
- 后端 oa-backend: 734 控制器 / 711 实体 (Spring Boot 3.2.5 + SQLite, 端口8091)
- 前端 modern-ui/app: Vue3+Vite, 约700页 (构建产物已在 oa-backend/src/main/resources/static)
- 数据库 oa-backend/data/oa.db: 含全部演示数据 (强制入库, 6.6MB)
- 交接文档 go.md + go-code-reference/endpoints/entities/database.md
- 多代理建设脚本 .claude/wf-*.js

== 状态 ==
- 对 凯迪科技ERP_20260507.xlsx 合规 MET ~73.3% (PARTIAL 75: 34可建+6种子/bug+35外部硬天花板)
- 安全: 5轮红队+5轮复检, default-deny分级鉴权, 连续零可利用
- W3~W7 累计补完436缺口; W8末轮(40缺口)为半成品(源码树可编译但未集成)
- 运行: cd oa-backend; java -jar build/libs/oa-backend-0.1.0.jar --server.port=8091; admin/123456

== 排除(gitignore, 可再生) ==
node_modules / oa-backend/build / .jdks / *.log / Backup-ERP-* / 弃用的OFBiz核心(只保留modern-ui)
完整文件夹备份见同目录 Backup-ERP-20260615-191517/ (含上述全部, 仅缺 node_modules)

时间戳: 20260615-191517

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:19:15 +08:00

115 lines
4.5 KiB
Python

#!/usr/bin/env python3
"""极简 CDP 截图器(仅标准库)。
用法: cdp_shot.py <ws_port> <out.png> <width> <height> <mobile:0|1> <path> <token>
流程: 导航到 8090 -> 注入 oa.token 到 localStorage -> 重载进入登录态 -> 设备视口 -> 截图。
"""
import sys, json, socket, base64, hashlib, struct, os, urllib.request, time
def http_json(port, path):
with urllib.request.urlopen(f"http://127.0.0.1:{port}{path}", timeout=10) as r:
return json.loads(r.read().decode())
class WS:
def __init__(self, url):
# ws://127.0.0.1:port/devtools/page/XXXX
assert url.startswith("ws://")
hostport, _, path = url[5:].partition("/")
host, _, port = hostport.partition(":")
self.path = "/" + path
self.sock = socket.create_connection((host, int(port)), timeout=15)
key = base64.b64encode(os.urandom(16)).decode()
req = (f"GET {self.path} HTTP/1.1\r\nHost: {hostport}\r\n"
"Upgrade: websocket\r\nConnection: Upgrade\r\n"
f"Sec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n")
self.sock.sendall(req.encode())
# read handshake response headers
buf = b""
while b"\r\n\r\n" not in buf:
buf += self.sock.recv(4096)
self.rest = buf.split(b"\r\n\r\n", 1)[1]
self._id = 0
def send(self, method, params=None):
self._id += 1
msg = json.dumps({"id": self._id, "method": method, "params": params or {}}).encode()
header = bytearray([0x81]) # FIN + text
mask = os.urandom(4)
n = len(msg)
if n < 126:
header.append(0x80 | n)
elif n < 65536:
header.append(0x80 | 126); header += struct.pack(">H", n)
else:
header.append(0x80 | 127); header += struct.pack(">Q", n)
header += mask
masked = bytes(b ^ mask[i % 4] for i, b in enumerate(msg))
self.sock.sendall(bytes(header) + masked)
return self._id
def _read(self, n):
while len(self.rest) < n:
chunk = self.sock.recv(65536)
if not chunk:
raise IOError("socket closed")
self.rest += chunk
out, self.rest = self.rest[:n], self.rest[n:]
return out
def recv_msg(self):
# assemble one full (possibly fragmented) text message
data = b""
while True:
b0, b1 = self._read(2)
fin = b0 & 0x80
opcode = b0 & 0x0F
length = b1 & 0x7F
if length == 126:
length = struct.unpack(">H", self._read(2))[0]
elif length == 127:
length = struct.unpack(">Q", self._read(8))[0]
payload = self._read(length) if length else b""
if opcode == 0x9: # ping -> pong
continue
if opcode == 0x8: # close
raise IOError("ws closed by peer")
data += payload
if fin:
return json.loads(data.decode("utf-8", "replace"))
def call(self, method, params=None, timeout=30):
mid = self.send(method, params)
t0 = time.time()
while time.time() - t0 < timeout:
m = self.recv_msg()
if m.get("id") == mid:
return m
raise TimeoutError(method)
def main():
port, out, w, h, mobile, path, token = (sys.argv[1], sys.argv[2], int(sys.argv[3]),
int(sys.argv[4]), sys.argv[5] == "1",
sys.argv[6], sys.argv[7])
targets = http_json(port, "/json")
page = next(t for t in targets if t.get("type") == "page")
ws = WS(page["webSocketDebuggerUrl"])
ws.call("Page.enable")
ws.call("Runtime.enable")
ws.call("Emulation.setDeviceMetricsOverride",
{"width": w, "height": h, "deviceScaleFactor": 2, "mobile": mobile})
# 1) load origin so localStorage is writable for it
ws.call("Page.navigate", {"url": "http://127.0.0.1:8090/login"})
time.sleep(2.5)
# 2) inject token + go to target path
ws.call("Runtime.evaluate", {"expression":
f"localStorage.setItem('oa.token','{token}'); 'ok'"})
ws.call("Page.navigate", {"url": f"http://127.0.0.1:8090{path}"})
time.sleep(4.5) # let SPA boot + data load
shot = ws.call("Page.captureScreenshot", {"format": "png", "captureBeyondViewport": False})
img = base64.b64decode(shot["result"]["data"])
with open(out, "wb") as f:
f.write(img)
print(f"OK {out} {len(img)} bytes")
if __name__ == "__main__":
main()