feat(server, thought-lab): 优化百炼API配置并新增机器人象棋实验室

- 优化服务端百炼API配置,将硬编码的API地址改为通过环境变量读取,支持自定义部署端点
- 新增机器人象棋思想实验室,包含完整对战逻辑、管理接口与前端页面,配套测试用例
- 优化思想实验室路由注册,新增公开路由绑定能力支持非浏览器客户端调用
This commit is contained in:
yangxiangyuan
2026-08-18 13:23:36 +08:00
parent e4f84caa23
commit 3ba267b38a
20 changed files with 2639 additions and 11 deletions
@@ -0,0 +1,140 @@
// ============================================================
// test_bot_chess_api_smoke.js —— bot_chess HTTP 冒烟集成测试
// 运行:node dev_test_scripts/integration/test_bot_chess_api_smoke.js
//
// 临时起一个 express 实例,真实绑定 thought_lab 全部路由(含 bot_chess
// bindPublicRoutes 公开路由),验证:鉴权穿透、建赛、state、走子、
// 非法走子错误码、轮次保护、MD 日志、删除。全程不碰 8976/8977 端口。
// ============================================================
const express = require('express')
const http = require('http')
const thoughtLab = require('../../src/server/thought_lab')
const COOKIE = 'nav_gate=1'
let passed = 0, failed = 0
const app = express()
app.use(express.json())
thoughtLab.bindRoutes(app)
const server = http.createServer(app)
async function req(method, path, body) {
const opts = { method, headers: { Cookie: COOKIE } }
if (body !== undefined) {
opts.headers['Content-Type'] = 'application/json'
opts.body = JSON.stringify(body)
}
const resp = await fetch(`http://127.0.0.1:${server.address().port}${path}`, opts)
let data = null
try { data = await resp.json() } catch (e) { data = null }
return { status: resp.status, data }
}
const ok = (name) => { passed++; console.log(' ✔ ' + name) }
const bad = (name, e) => { failed++; console.log(' ✘ ' + name + ' —— ' + (e || 'fail')) }
const chk = (cond, msg) => { if (!cond) throw new Error(msg) }
const main = async () => {
await new Promise(res => server.listen(0, res))
// 1) lab 列表包含 bot_chess(首页页签渲染)
{
const r = await req('GET', '/api/thought_lab/labs')
chk(r.data && r.data.ok, 'labs 接口不 ok')
const lab = (r.data.labs || []).find(l => l.id === 'bot_chess')
chk(!!lab, '列表无 bot_chess')
ok('lab 列表包含 bot_chess(' + lab.name + ')')
}
// 2) 创建比赛
let matchId = 0, redToken = '', blackToken = ''
{
const r = await req('POST', '/api/thought_lab/bot_chess/matches', {
title: '冒烟测试', red_bot: '红Bot', black_bot: '黑Bot', best_of: 1
})
chk(r.data && r.data.ok, '创建比赛失败:' + JSON.stringify(r.data))
chk(r.data.match_id > 0, '无 match_id')
chk(r.data.red_token && r.data.black_token, '未返回 token')
matchId = r.data.match_id
redToken = r.data.red_token
blackToken = r.data.black_token
ok(`创建比赛 #${matchId}(红/黑 token 已签发)`)
}
// 3) 未开局时走子 → MATCH_NOT_STARTED
{
const r = await req('POST', `/api/thought_lab/bot_chess/matches/${matchId}/moves`, { move: 'h2-e2', side: 'red' })
chk(r.status === 400 && r.data.code === 'MATCH_NOT_STARTED', '未开局应拒绝走子:' + JSON.stringify(r.data))
ok('未开局走子被拒(MATCH_NOT_STARTED)')
}
// 4) 开局
{
const r = await req('POST', `/api/thought_lab/bot_chess/matches/${matchId}/start`)
chk(r.data && r.data.ok, '开局失败')
ok('开局成功')
}
// 5) 红方合法走子 h2-e2
{
const r = await req('POST', `/api/thought_lab/bot_chess/matches/${matchId}/moves`, { move: 'h2e2', side: 'red' })
chk(r.data && r.data.ok && r.data.accepted, '红方 h2e2 应被接受:' + JSON.stringify(r.data))
ok('红方 h2e2 合法走子被接受')
}
// 6) 轮次保护:红方再次走子 → NOT_YOUR_TURN
{
const r = await req('POST', `/api/thought_lab/bot_chess/matches/${matchId}/moves`, { move: 'b0c2', side: 'red' })
chk(r.data && r.data.code === 'NOT_YOUR_TURN', '轮次保护失效:' + JSON.stringify(r.data))
ok('红方连走被拒(NOT_YOUR_TURN)')
}
// 7) 非法走子:黑马斜走 b9c8(马走日,斜线非法)→ 400 + 错误码
{
const r = await req('POST', `/api/thought_lab/bot_chess/matches/${matchId}/moves`, { move: 'b9c8', side: 'black' })
chk(r.status === 400 && r.data.code === 'ILLEGAL_MOVE', '非法走子应 400+code:' + JSON.stringify(r.data))
ok('非法走子返回 400 + ILLEGAL_MOVE')
}
// 8) bot token 鉴权:裸 token 走子/查状态
{
const r = await req('GET', `/api/thought_lab/bot_chess/matches/${matchId}/state`)
chk(r.data && r.data.ok, 'state 失败')
chk(r.data.board && r.data.board.fen.includes(' '), 'state 无 FEN')
chk(r.data.game && (r.data.game.side_to_move === 'red' || r.data.game.side_to_move === 'black'), 'state 无 side_to_move')
ok('state 接口返回局面(FEN/棋盘/turn/side_to_move)')
}
// 9) 黑方合法走子 + 状态推进
{
const r = await req('POST', `/api/thought_lab/bot_chess/matches/${matchId}/moves`, { move: 'h9g7', side: 'black' })
chk(r.data && r.data.ok, '黑方 h9g7 应接受:' + JSON.stringify(r.data))
ok('黑方 h9g7 合法走子被接受')
}
// 10) MD 日志可导出
{
const resp = await fetch(`http://127.0.0.1:${server.address().port}/api/thought_lab/bot_chess/matches/${matchId}/log.md`, { headers: { Cookie: COOKIE } })
const md = await resp.text()
chk(resp.status === 200 && md.includes('机器人象棋 比赛日志'), 'MD 日志异常')
chk(md.includes('h2'), 'MD 日志应包含着法 h2')
ok('MD 日志导出成功(含着法记录)')
}
// 11) 清理
{
const r = await req('DELETE', `/api/thought_lab/bot_chess/matches/${matchId}`)
chk(r.data && r.data.ok, '删除失败')
// token 不应再被识别
const r2 = await req('POST', `/api/thought_lab/bot_chess/matches/${matchId}/moves`, { move: 'h2e2', side: 'red' })
chk(r2.status === 404 || !r2.data.ok, '删除后仍可访问')
ok('比赛删除成功')
}
server.close()
console.log(`\n结果:${passed} 通过 / ${failed} 失败`)
process.exit(failed ? 1 : 0)
}
main().catch(e => { console.error('冒烟测试异常:', e); server && server.close(); process.exit(1) })