154 lines
6.3 KiB
Python
Executable File
154 lines
6.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""Generate a deterministic daily knowledge note from structured dashboard_logs.
|
||
|
||
This replaces the old LLM-based whole-conversation replay. It intentionally reads
|
||
only the day's structured operation log rows, so a no-op day costs no model tokens.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import os
|
||
import sys
|
||
from collections import Counter
|
||
from datetime import date, datetime, timedelta
|
||
from pathlib import Path
|
||
|
||
import pymysql
|
||
|
||
ROOT = Path('/home/gxd/蝶雅供应链')
|
||
INDEX = ROOT / '知识库' / '索引.md'
|
||
TOPDIR = ROOT / '知识库' / '工作指南' / '训练记录'
|
||
|
||
|
||
def db_config():
|
||
sys.path.insert(0, '/home/gxd/.openclaw/workspace/dy-dashboard')
|
||
from config import DB_CONFIG
|
||
return DB_CONFIG
|
||
|
||
|
||
def fetch_rows(day: date):
|
||
start = datetime.combine(day, datetime.min.time())
|
||
end = start + timedelta(days=1)
|
||
cfg = db_config()
|
||
conn = pymysql.connect(**cfg, cursorclass=pymysql.cursors.DictCursor)
|
||
try:
|
||
with conn.cursor() as cur:
|
||
cur.execute(
|
||
"""
|
||
SELECT l.created_at, COALESCE(u.real_name, u.username, '系统') AS user_name,
|
||
COALESCE(NULLIF(l.page, ''), '未分类') AS page,
|
||
l.action, COALESCE(l.field, '') AS field,
|
||
l.result_status, l.risk_level,
|
||
COALESCE(l.detail, '') AS detail
|
||
FROM dashboard_logs l
|
||
LEFT JOIN dashboard_users u ON u.id = l.user_id
|
||
WHERE l.created_at >= %s AND l.created_at < %s
|
||
AND l.action <> '登录'
|
||
ORDER BY l.created_at ASC, l.id ASC
|
||
""",
|
||
(start, end),
|
||
)
|
||
return cur.fetchall()
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
def clip(value, limit=220):
|
||
text = ' '.join(str(value or '').split())
|
||
return text if len(text) <= limit else text[: limit - 1] + '…'
|
||
|
||
|
||
def build_content(day: date, rows):
|
||
title = f'{day.isoformat()} 每日训练笔记(结构化操作日志)'
|
||
lines = [
|
||
f'> 来源:蝶雅采购智能体平台结构化操作日志,{day.isoformat()}',
|
||
'> 类型:系统操作摘要',
|
||
'',
|
||
f'## 当日概况',
|
||
f'- 结构化操作记录:{len(rows)} 条',
|
||
]
|
||
if not rows:
|
||
lines.append('- 当日没有可归档的业务操作记录,未回放历史会话。')
|
||
return title, '\n'.join(lines) + '\n'
|
||
|
||
pages = Counter(r['page'] for r in rows)
|
||
actions = Counter(r['action'] for r in rows)
|
||
results = Counter(r['result_status'] for r in rows)
|
||
risks = Counter(r['risk_level'] for r in rows)
|
||
lines += [
|
||
f"- 涉及页面:{len(pages)} 个",
|
||
f"- 成功/失败/拒绝:{results.get('success', 0)}/{results.get('failed', 0)}/{results.get('denied', 0)}",
|
||
f"- 高风险记录:{risks.get('high', 0)} 条",
|
||
'',
|
||
'## 页面与操作汇总',
|
||
'| 页面 | 操作数 |',
|
||
'|------|------:|',
|
||
]
|
||
for page, count in pages.most_common():
|
||
lines.append(f'| {page} | {count} |')
|
||
|
||
lines += ['', '## 操作类型汇总', '| 操作 | 次数 |', '|------|------:|']
|
||
for action, count in actions.most_common(30):
|
||
lines.append(f'| {action} | {count} |')
|
||
|
||
exceptional = [r for r in rows if r['result_status'] != 'success' or r['risk_level'] in ('high', 'medium')]
|
||
if exceptional:
|
||
lines += ['', '## 异常与高风险记录', '| 时间 | 用户 | 页面 | 操作 | 结果 | 说明 |', '|------|------|------|------|------|------|']
|
||
for r in exceptional[:80]:
|
||
ts = r['created_at'].strftime('%H:%M:%S') if hasattr(r['created_at'], 'strftime') else str(r['created_at'])[-8:]
|
||
lines.append(f"| {ts} | {clip(r['user_name'], 24)} | {clip(r['page'], 24)} | {clip(r['action'], 28)} | {r['result_status']} | {clip(r['detail'])} |")
|
||
else:
|
||
lines += ['', '## 异常与高风险记录', '- 无失败、拒绝或中高风险操作记录。']
|
||
|
||
lines += ['', '## 结构化操作明细', '| 时间 | 用户 | 页面 | 操作 | 字段 | 结果 |', '|------|------|------|------|------|------|']
|
||
for r in rows[:300]:
|
||
ts = r['created_at'].strftime('%H:%M:%S') if hasattr(r['created_at'], 'strftime') else str(r['created_at'])[-8:]
|
||
lines.append(f"| {ts} | {clip(r['user_name'], 24)} | {clip(r['page'], 24)} | {clip(r['action'], 28)} | {clip(r['field'], 20)} | {r['result_status']} |")
|
||
if len(rows) > 300:
|
||
lines.append(f'- 明细超过300条,以上仅展示前300条;完整记录保留在数据库 `dashboard_logs`。')
|
||
return title, '\n'.join(lines) + '\n'
|
||
|
||
|
||
def write_note(day: date, title: str, content: str):
|
||
TOPDIR.mkdir(parents=True, exist_ok=True)
|
||
path = TOPDIR / f'{day.isoformat()}-{title}.md'
|
||
full = f'# {title}\n\n{content}\n---\n\n*由结构化操作日志任务自动生成*\n'
|
||
path.write_text(full, encoding='utf-8')
|
||
if INDEX.exists():
|
||
index = INDEX.read_text(encoding='utf-8')
|
||
marker = f'[{title}]'
|
||
if marker not in index:
|
||
row = f'| [{title}](工作指南/训练记录/{path.name}) | 基于 dashboard_logs 的结构化操作摘要,不回放历史会话 | {day.isoformat()} |\n'
|
||
anchor = '| 文章 | 摘要 | 更新 |'
|
||
pos = index.find(anchor, index.find('### 训练记录'))
|
||
if pos >= 0:
|
||
header_end = index.find('\n', pos) + 1
|
||
separator_end = index.find('\n', header_end) + 1
|
||
insert_at = separator_end if separator_end > header_end else header_end
|
||
index = index[:insert_at] + row + index[insert_at:]
|
||
else:
|
||
index += f'\n{row}'
|
||
INDEX.write_text(index, encoding='utf-8')
|
||
return path
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser()
|
||
parser.add_argument('--date', default=date.today().isoformat())
|
||
parser.add_argument('--dry-run', action='store_true')
|
||
args = parser.parse_args()
|
||
day = datetime.strptime(args.date, '%Y-%m-%d').date()
|
||
rows = fetch_rows(day)
|
||
title, content = build_content(day, rows)
|
||
if args.dry_run:
|
||
print(f'{day.isoformat()}: {len(rows)} structured log rows')
|
||
print(content[:1200])
|
||
return 0
|
||
path = write_note(day, title, content)
|
||
print(f'STRUCTURED_NOTE_WRITTEN rows={len(rows)} path={path}')
|
||
return 0
|
||
|
||
|
||
if __name__ == '__main__':
|
||
raise SystemExit(main())
|