Skip to content

Commit a28fdbb

Browse files
nieaoclaude
andcommitted
feat: ALETHEIA 品牌化 + 决策引擎真跑 + CLI 监控 + Hermes 云端拉通
按 boss 反馈整治多项: ## 品牌化 (3 处) - 左上角加 ALETHEIA 品牌标识 (KnowledgeGraph) - index.html title: ALETHEIA — 逻辑对抗决策引擎 - JoinRoom 标题/品牌 KNOW/CANVAS → ALETHEIA ## 决策引擎真跑 (subagent 完成) - src/services/aletheia/runner.js (220 行) 新建 - runAletheiaCycle 真跑 LLM, 按 personas × scenarios 输出反驳 - 节点逐个长出 (sleep 600ms), 用户能看到动态变化 - LLM 失败兜底 mock (3 persona 各 3 条预写反驳) - 综合后 SynthesisNode + 健康分回写 - AletheiaLayer.jsx (+75 行) banner 加 "⚔ 对画布跑一轮" 按钮 + 进度脉冲条 ## CLI 全流程监控 (boss 调试用) - src/utils/logBus.js (新建) — ring buffer 1000 条 + EventEmitter, 桥接 console.* - src/components/CliMonitor.jsx (新建) — 右下角折叠面板, 等级过滤/暂停/复制/下载/清空 - main.jsx attachConsoleBridge() 启动桥接 - 集成到 KnowledgeGraph 右下角 ## action log 系统 (subagent 完成) - server/action-log-server.js (102 行, 端口 18091) — POST /log 写 logs/actions-YYYYMMDD.jsonl - src/utils/actionLog.js (新建) — logAction(name, payload) 同时 push logBus + POST 后端 - LeftPanel 5 处 logAction (addSource/select/remove) - RightPanel 7 处 logAction (editField/addTag/removeTag/setCategory/addRelation/removeEdge/runLocalTask) ## Hermes 云端拉通 - RightPanel hermes inject URL 改环境感知: dev → http://127.0.0.1:17082/api/orchestra/inject prod → /canvas/api/orchestra/inject (经 caddy 反代) - deploy/Caddyfile.canvas 加 /canvas/api/orchestra/* + /canvas/api/log/* 反代 - 注: VPS 还需起 orchestra-http daemon (npm run orchestra:http) 才能真生效 ## 清空画布按钮 - SaveExportToolbar 加 "清空" 按钮 (二次确认, 红色态) ## 杂项 - .gitignore 加 logs/ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 663ca93 commit a28fdbb

15 files changed

Lines changed: 878 additions & 20 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ e2e/.tmp
1111
server/yjs-data/
1212
server/node_modules/
1313

14+
# action-log-server 输出
15+
logs/
16+
1417
# 临时日志
1518
*.log
1619

deploy/Caddyfile.canvas

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,32 @@ ha2.digitalvio.shop {
2929
}
3030
}
3131

32-
# ↑ 上述两个 handle 块要放在 Hermes 已有的 reverse_proxy / file_server 块之前,
32+
# === Orchestra HTTP API(Hermes 派单 + 任务查询) ===
33+
# 浏览器 fetch /canvas/api/orchestra/inject → 反代到 VPS 本机 17082
34+
# 必须先在 VPS 起 orchestra-http daemon (npm run orchestra:http) 才会真生效
35+
handle_path /canvas/api/orchestra/* {
36+
reverse_proxy 127.0.0.1:17082 {
37+
header_up Host {host}
38+
header_up X-Real-IP {remote_host}
39+
}
40+
}
41+
42+
# === Action Log 服务(左右面板用户动作记录) ===
43+
# 浏览器 POST /canvas/api/log → 反代到 18091, 写 logs/actions-YYYYMMDD.jsonl
44+
handle_path /canvas/api/log {
45+
reverse_proxy 127.0.0.1:18091/log {
46+
header_up Host {host}
47+
header_up X-Real-IP {remote_host}
48+
}
49+
}
50+
handle_path /canvas/api/log/* {
51+
reverse_proxy 127.0.0.1:18091 {
52+
header_up Host {host}
53+
header_up X-Real-IP {remote_host}
54+
}
55+
}
56+
57+
# ↑ 上述 handle 块要放在 Hermes 已有的 reverse_proxy / file_server 块之前,
3358
# 因为 Caddy 按顺序匹配;如有疑问 sudo caddy validate /etc/caddy/Caddyfile 看看
3459
# ↓ 这里保留 Hermes 已有的所有规则,不要删
3560
# reverse_proxy ... (Hermes API)

index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<meta charset="UTF-8" />
55
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7-
<title>Know Canvas - 知识图谱画布</title>
7+
<title>ALETHEIA — 逻辑对抗决策引擎</title>
88
</head>
99
<body>
1010
<div id="root"></div>

server/action-log-server.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/**
2+
* Action Log Server — 用户行为日志收集服务
3+
*
4+
* 接收前端 LeftPanel / RightPanel 的用户动作,按天追加写入 logs/actions-YYYYMMDD.jsonl
5+
* 启动: node server/action-log-server.js
6+
* 端口: 18091(默认,ACTION_LOG_PORT 环境变量可覆盖)
7+
*
8+
* API:
9+
* GET /health → { ok: true, service: 'action-log', logFile }
10+
* POST /log → 追加 jsonl,body: { name, payload, room, user, ts? }
11+
* GET /tail?n=50 → 返回当天最近 N 行
12+
*/
13+
14+
const http = require('http')
15+
const fs = require('fs')
16+
const path = require('path')
17+
const url = require('url')
18+
19+
const PORT = parseInt(process.env.ACTION_LOG_PORT || '18091', 10)
20+
const HOST = process.env.ACTION_LOG_HOST || '127.0.0.1'
21+
const LOG_DIR = path.join(process.cwd(), 'logs')
22+
fs.mkdirSync(LOG_DIR, { recursive: true })
23+
24+
/** 当天 jsonl 文件路径 */
25+
function todayLogFile() {
26+
const d = new Date()
27+
const stamp = `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`
28+
return path.join(LOG_DIR, `actions-${stamp}.jsonl`)
29+
}
30+
31+
/** CORS:允许 localhost / 127.0.0.1 / file:// / 同源 */
32+
function setCors(req, res) {
33+
const origin = req.headers.origin
34+
let allow = '*'
35+
if (origin === 'null') allow = 'null'
36+
else if (origin && /^https?:\/\/(localhost|127\.0\.0\.1)/.test(origin)) allow = origin
37+
res.setHeader('Access-Control-Allow-Origin', allow)
38+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
39+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
40+
}
41+
42+
function readJsonBody(req) {
43+
return new Promise((resolve, reject) => {
44+
let buf = ''
45+
req.setEncoding('utf8')
46+
req.on('data', (c) => { buf += c; if (buf.length > 1024 * 1024) { reject(new Error('payload too large')); req.destroy() } })
47+
req.on('end', () => { try { resolve(JSON.parse(buf || '{}')) } catch (e) { reject(e) } })
48+
req.on('error', reject)
49+
})
50+
}
51+
52+
const server = http.createServer(async (req, res) => {
53+
setCors(req, res)
54+
if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return }
55+
const parsed = url.parse(req.url, true)
56+
const send = (code, obj) => { res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify(obj)) }
57+
58+
if (parsed.pathname === '/health' && req.method === 'GET') {
59+
return send(200, { ok: true, service: 'action-log', logFile: path.relative(process.cwd(), todayLogFile()).replace(/\\/g, '/') })
60+
}
61+
62+
if (parsed.pathname === '/log' && req.method === 'POST') {
63+
try {
64+
const { name, payload, room, user, ts } = (await readJsonBody(req)) || {}
65+
if (!name || typeof name !== 'string') return send(400, { ok: false, error: '缺少 name 字段' })
66+
const record = { ts: ts || new Date().toISOString(), name, room: room || '', user: user || '', payload: payload || {} }
67+
fs.appendFileSync(todayLogFile(), JSON.stringify(record) + '\n', 'utf8')
68+
return send(200, { ok: true })
69+
} catch (err) {
70+
console.error('[action-log] /log 写入失败:', err.message)
71+
return send(500, { ok: false, error: err.message })
72+
}
73+
}
74+
75+
if (parsed.pathname === '/tail' && req.method === 'GET') {
76+
try {
77+
const n = Math.min(parseInt(parsed.query.n || '50', 10) || 50, 1000)
78+
const file = todayLogFile()
79+
const lines = fs.existsSync(file) ? fs.readFileSync(file, 'utf8').split('\n').filter(Boolean).slice(-n) : []
80+
return send(200, { ok: true, count: lines.length, lines: lines.map((l) => { try { return JSON.parse(l) } catch { return { raw: l } } }) })
81+
} catch (err) {
82+
return send(500, { ok: false, error: err.message })
83+
}
84+
}
85+
86+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' })
87+
res.end('Not Found\n')
88+
})
89+
90+
server.listen(PORT, HOST, () => {
91+
console.log(`[action-log] 监听 http://${HOST}:${PORT}`)
92+
console.log(`[action-log] 当天日志: ${path.relative(process.cwd(), todayLogFile())}`)
93+
console.log('[action-log] GET /health POST /log GET /tail?n=50')
94+
})
95+
96+
function shutdown(signal) {
97+
console.log(`\n[action-log] 收到 ${signal},正在关闭...`)
98+
server.close(() => process.exit(0))
99+
setTimeout(() => process.exit(1), 3000)
100+
}
101+
process.on('SIGINT', () => shutdown('SIGINT'))
102+
process.on('SIGTERM', () => shutdown('SIGTERM'))

src/components/CliMonitor.jsx

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
/**
2+
* CliMonitor — 画布右下角 CLI 全流程监控折叠面板
3+
*
4+
* 订阅 logBus 实时显示用户左右面板动作 + Aletheia 进度 + 本地任务 + yjs 状态。
5+
* 折叠时只露一个小条, 展开后是全屏右侧 384px 宽抽屉, 黑底等宽字体。
6+
*
7+
* 操作:
8+
* - 点条头 toggle 展开/折叠
9+
* - 清空 / 复制全部 / 下载 .txt
10+
* - 过滤等级 (info/warn/error/all)
11+
* - 跟随最新 (auto-scroll)
12+
*/
13+
14+
import { useEffect, useRef, useState } from 'react'
15+
import { onAppend, getAll, clear, exportText, pushLog } from '../utils/logBus'
16+
17+
const LEVEL_COLOR = {
18+
debug: '#888',
19+
info: '#9ec3d8',
20+
warn: '#e8d5c0',
21+
error: '#d27b7b',
22+
}
23+
const SOURCE_COLOR = {
24+
action: '#c8a882',
25+
aletheia: '#a78bfa',
26+
task: '#7bc47f',
27+
yjs: '#7c9eb2',
28+
console: '#888',
29+
bus: '#555',
30+
net: '#d27b7b',
31+
misc: '#bbb',
32+
}
33+
34+
export default function CliMonitor() {
35+
const [open, setOpen] = useState(false)
36+
const [logs, setLogs] = useState(() => getAll())
37+
const [filter, setFilter] = useState('all') // all | info | warn | error
38+
const [autoScroll, setAutoScroll] = useState(true)
39+
const [paused, setPaused] = useState(false)
40+
const scrollRef = useRef(null)
41+
42+
// 订阅 logBus
43+
useEffect(() => {
44+
const off = onAppend((e) => {
45+
if (paused) return
46+
setLogs((prev) => {
47+
// ring buffer 同步(最多 1000 条)
48+
const next = prev.length >= 1000 ? prev.slice(-999).concat([e]) : prev.concat([e])
49+
return next
50+
})
51+
})
52+
return off
53+
}, [paused])
54+
55+
// auto-scroll
56+
useEffect(() => {
57+
if (autoScroll && scrollRef.current) {
58+
scrollRef.current.scrollTop = scrollRef.current.scrollHeight
59+
}
60+
}, [logs, autoScroll])
61+
62+
// 过滤后日志
63+
const filtered = filter === 'all' ? logs : logs.filter((e) => e.level === filter)
64+
65+
// 错误数 / 警告数(折叠时显示徽章)
66+
const errCount = logs.filter((e) => e.level === 'error').length
67+
const warnCount = logs.filter((e) => e.level === 'warn').length
68+
69+
const handleClear = () => { clear(); setLogs([]) }
70+
const handleCopy = () => {
71+
navigator.clipboard?.writeText(exportText()).then(
72+
() => pushLog({ level: 'info', source: 'bus', msg: '已复制 ' + logs.length + ' 行到剪贴板' }),
73+
() => pushLog({ level: 'warn', source: 'bus', msg: '复制失败(剪贴板权限被拒)' }),
74+
)
75+
}
76+
const handleDownload = () => {
77+
const blob = new Blob([exportText()], { type: 'text/plain;charset=utf-8' })
78+
const url = URL.createObjectURL(blob)
79+
const a = document.createElement('a')
80+
a.href = url
81+
a.download = `know-canvas-log-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.txt`
82+
a.click()
83+
URL.revokeObjectURL(url)
84+
}
85+
86+
// 折叠态 — 右下角小条
87+
if (!open) {
88+
return (
89+
<button
90+
onClick={() => setOpen(true)}
91+
className="absolute bottom-3 right-3 z-40 flex items-center gap-2 px-3 py-1.5 rounded-md shadow-lg transition-all hover:translate-y-[-2px]"
92+
style={{
93+
background: '#1a1a1a',
94+
color: '#fafafa',
95+
border: '1px solid #2d2d2d',
96+
fontFamily: '"Courier New", monospace',
97+
fontSize: 11,
98+
letterSpacing: '0.05em',
99+
}}
100+
title="展开 CLI 监控面板"
101+
>
102+
<span
103+
className="inline-block w-1.5 h-1.5 rounded-full"
104+
style={{ backgroundColor: errCount > 0 ? '#d27b7b' : warnCount > 0 ? '#e8d5c0' : '#7bc47f' }}
105+
/>
106+
<span>CLI 监控</span>
107+
<span style={{ color: '#888' }}>{logs.length}</span>
108+
{errCount > 0 && <span style={{ color: '#d27b7b' }}>!{errCount}</span>}
109+
</button>
110+
)
111+
}
112+
113+
return (
114+
<div
115+
className="absolute top-0 right-0 bottom-0 z-40 flex flex-col"
116+
style={{
117+
width: 420,
118+
background: '#0f0f0f',
119+
color: '#e0e0e0',
120+
borderLeft: '1px solid #2d2d2d',
121+
fontFamily: '"Courier New", "Consolas", monospace',
122+
fontSize: 11,
123+
}}
124+
>
125+
{/* Header */}
126+
<div
127+
className="flex items-center justify-between px-3 py-2"
128+
style={{ background: '#1a1a1a', borderBottom: '1px solid #2d2d2d' }}
129+
>
130+
<div className="flex items-center gap-2">
131+
<span
132+
className="inline-block w-1.5 h-1.5 rounded-full animate-pulse"
133+
style={{ backgroundColor: paused ? '#888' : '#c8a882' }}
134+
/>
135+
<span style={{ color: '#c8a882', letterSpacing: '0.25em', fontSize: 10 }}>CLI 全流程监控</span>
136+
<span style={{ color: '#666', fontSize: 10 }}>({logs.length}/1000)</span>
137+
</div>
138+
<button
139+
onClick={() => setOpen(false)}
140+
className="px-2 py-0.5 rounded"
141+
style={{ color: '#888', fontSize: 14 }}
142+
title="折叠"
143+
>
144+
145+
</button>
146+
</div>
147+
148+
{/* Toolbar */}
149+
<div
150+
className="flex items-center gap-1 px-2 py-1.5"
151+
style={{ background: '#161616', borderBottom: '1px solid #2d2d2d', fontSize: 10 }}
152+
>
153+
{['all', 'info', 'warn', 'error'].map((lv) => (
154+
<button
155+
key={lv}
156+
onClick={() => setFilter(lv)}
157+
className="px-2 py-0.5 rounded"
158+
style={{
159+
background: filter === lv ? '#2d2d2d' : 'transparent',
160+
color: filter === lv ? '#fafafa' : '#888',
161+
border: '1px solid ' + (filter === lv ? '#3a3a3a' : 'transparent'),
162+
}}
163+
>
164+
{lv}
165+
</button>
166+
))}
167+
<span style={{ color: '#444', margin: '0 4px' }}>|</span>
168+
<button onClick={() => setPaused((p) => !p)} className="px-2 py-0.5 rounded" style={{ color: paused ? '#e8d5c0' : '#888' }}>
169+
{paused ? '▶' : '⏸'}
170+
</button>
171+
<button onClick={() => setAutoScroll((a) => !a)} className="px-2 py-0.5 rounded" style={{ color: autoScroll ? '#7bc47f' : '#888' }}>
172+
{autoScroll ? '↓ 跟随' : '↓ 暂停'}
173+
</button>
174+
<span style={{ flex: 1 }} />
175+
<button onClick={handleCopy} className="px-2 py-0.5 rounded" style={{ color: '#888' }} title="复制全部"></button>
176+
<button onClick={handleDownload} className="px-2 py-0.5 rounded" style={{ color: '#888' }} title="下载 .txt"></button>
177+
<button onClick={handleClear} className="px-2 py-0.5 rounded" style={{ color: '#d27b7b' }} title="清空">×</button>
178+
</div>
179+
180+
{/* Logs */}
181+
<div ref={scrollRef} className="flex-1 overflow-y-auto px-2 py-1" style={{ scrollbarWidth: 'thin' }}>
182+
{filtered.length === 0 ? (
183+
<div style={{ color: '#444', padding: 16, textAlign: 'center', fontSize: 11 }}>
184+
{logs.length === 0 ? '暂无日志…\n左右面板任意操作都会出现在这里' : '<当前过滤无匹配>'}
185+
</div>
186+
) : (
187+
filtered.map((e, i) => {
188+
const t = new Date(e.ts).toISOString().slice(11, 23)
189+
return (
190+
<div key={i} style={{ marginBottom: 2, lineHeight: 1.5, wordBreak: 'break-word' }}>
191+
<span style={{ color: '#555' }}>{t}</span>
192+
{' '}
193+
<span style={{ color: LEVEL_COLOR[e.level] || '#bbb' }}>{e.level.toUpperCase().padEnd(5)}</span>
194+
{' '}
195+
<span style={{ color: SOURCE_COLOR[e.source] || '#888' }}>[{e.source}]</span>
196+
{' '}
197+
<span style={{ color: '#e0e0e0' }}>{e.msg}</span>
198+
{e.data && (
199+
<pre style={{ color: '#7c9eb2', marginLeft: 16, fontSize: 10, whiteSpace: 'pre-wrap', marginTop: 1 }}>
200+
{JSON.stringify(e.data, null, 0)}
201+
</pre>
202+
)}
203+
</div>
204+
)
205+
})
206+
)}
207+
</div>
208+
</div>
209+
)
210+
}

0 commit comments

Comments
 (0)