knowflow 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- knowflow/__init__.py +4 -0
- knowflow/__main__.py +3 -0
- knowflow/adapters/knowledge/__init__.py +42 -0
- knowflow/adapters/knowledge/local.py +43 -0
- knowflow/adapters/knowledge/obsidian.py +64 -0
- knowflow/adapters/storage/__init__.py +62 -0
- knowflow/adapters/storage/git.py +155 -0
- knowflow/adapters/storage/local.py +110 -0
- knowflow/adapters/storage/quark.py +225 -0
- knowflow/adapters/storage/s3.py +118 -0
- knowflow/adapters/storage/webdav.py +196 -0
- knowflow/cli/__init__.py +265 -0
- knowflow/cli/template_manager.py +100 -0
- knowflow/core/__init__.py +10 -0
- knowflow/core/knowledge.py +85 -0
- knowflow/core/pipeline.py +119 -0
- knowflow/core/storage.py +167 -0
- knowflow/dashboard/__init__.py +355 -0
- knowflow/templates/daily-journal/plugin.yaml +21 -0
- knowflow/templates/football-prediction/plugin.yaml +22 -0
- knowflow/templates/project-wiki/plugin.yaml +20 -0
- knowflow/templates/research-notes/plugin.yaml +26 -0
- knowflow/templates/stock-analysis/plugin.yaml +22 -0
- knowflow/templates/web-monitor/plugin.yaml +20 -0
- knowflow/templates/worldcup-tracker/plugin.yaml +18 -0
- knowflow/transforms/__init__.py +6 -0
- knowflow/transforms/archive.py +87 -0
- knowflow/transforms/markdown_export.py +110 -0
- knowflow/transforms/profile_index.py +89 -0
- knowflow-0.1.0.dist-info/METADATA +264 -0
- knowflow-0.1.0.dist-info/RECORD +35 -0
- knowflow-0.1.0.dist-info/WHEEL +5 -0
- knowflow-0.1.0.dist-info/entry_points.txt +2 -0
- knowflow-0.1.0.dist-info/licenses/LICENSE +21 -0
- knowflow-0.1.0.dist-info/top_level.txt +1 -0
knowflow/core/storage.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
knowflow/core/storage.py — Storage Adapter Interface
|
|
5
|
+
=====================================================
|
|
6
|
+
Storage = WHERE data lives physically.
|
|
7
|
+
Every backend implements this interface.
|
|
8
|
+
|
|
9
|
+
Built-in backends:
|
|
10
|
+
- local: Local filesystem (simplest)
|
|
11
|
+
- git: Local + Git versioning (recommended)
|
|
12
|
+
- s3: AWS S3 and compatible (MinIO, R2, etc.)
|
|
13
|
+
- webdav: WebDAV protocol (NextCloud, Synology NAS)
|
|
14
|
+
- quark: 夸克网盘 (Chinese cloud drive)
|
|
15
|
+
- baidu: 百度网盘 (Chinese cloud drive)
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
from knowflow.adapters.storage import create_storage
|
|
19
|
+
store = create_storage("git", path="~/my-data")
|
|
20
|
+
store.write("hello.txt", "world")
|
|
21
|
+
store.commit("added hello")
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from abc import ABC, abstractmethod
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from typing import Optional
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
import json
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class StorageError(Exception): pass
|
|
32
|
+
class StorageNotFoundError(StorageError): pass
|
|
33
|
+
class StorageAuthError(StorageError): pass
|
|
34
|
+
class StorageQuotaError(StorageError): pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class StorageItem:
|
|
39
|
+
"""Metadata about a stored item"""
|
|
40
|
+
path: str # Relative path
|
|
41
|
+
size: int # Bytes
|
|
42
|
+
modified_at: float # Unix timestamp
|
|
43
|
+
is_dir: bool = False
|
|
44
|
+
etag: Optional[str] = None # Optional backend-native identifier
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class StorageAdapter(ABC):
|
|
48
|
+
"""
|
|
49
|
+
Storage Adapter Interface
|
|
50
|
+
|
|
51
|
+
Every storage backend must implement this ABC.
|
|
52
|
+
Backend-specific config goes in config dict, never in the class itself.
|
|
53
|
+
|
|
54
|
+
Naming:
|
|
55
|
+
- All paths use forward slashes, relative to store root
|
|
56
|
+
- No leading slash
|
|
57
|
+
- Never use absolute paths
|
|
58
|
+
|
|
59
|
+
Thread safety: not guaranteed (single-user tool)
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self, config: dict = None):
|
|
63
|
+
self.config = config or {}
|
|
64
|
+
|
|
65
|
+
# ─── Read ──────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
@abstractmethod
|
|
68
|
+
def read(self, path: str) -> str:
|
|
69
|
+
"""Read text file. Raises StorageNotFoundError."""
|
|
70
|
+
...
|
|
71
|
+
|
|
72
|
+
@abstractmethod
|
|
73
|
+
def read_binary(self, path: str) -> bytes:
|
|
74
|
+
"""Read binary file."""
|
|
75
|
+
...
|
|
76
|
+
|
|
77
|
+
def read_json(self, path: str) -> dict:
|
|
78
|
+
"""Convenience: read and parse JSON."""
|
|
79
|
+
return json.loads(self.read(path))
|
|
80
|
+
|
|
81
|
+
# ─── Write ─────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
@abstractmethod
|
|
84
|
+
def write(self, path: str, content: str) -> str:
|
|
85
|
+
"""
|
|
86
|
+
Write text file. Overwrites if exists.
|
|
87
|
+
Returns revision ID (or empty if unsupported).
|
|
88
|
+
"""
|
|
89
|
+
...
|
|
90
|
+
|
|
91
|
+
@abstractmethod
|
|
92
|
+
def write_binary(self, path: str, content: bytes) -> str:
|
|
93
|
+
"""Write binary file."""
|
|
94
|
+
...
|
|
95
|
+
|
|
96
|
+
def write_json(self, path: str, data: dict) -> str:
|
|
97
|
+
"""Convenience: serialize and write JSON."""
|
|
98
|
+
return self.write(path, json.dumps(data, ensure_ascii=False, indent=2, default=str))
|
|
99
|
+
|
|
100
|
+
# ─── Delete ────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
@abstractmethod
|
|
103
|
+
def delete(self, path: str) -> bool:
|
|
104
|
+
"""Delete path. Returns True if it existed."""
|
|
105
|
+
...
|
|
106
|
+
|
|
107
|
+
# ─── Query ─────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
@abstractmethod
|
|
110
|
+
def exists(self, path: str) -> bool:
|
|
111
|
+
...
|
|
112
|
+
|
|
113
|
+
@abstractmethod
|
|
114
|
+
def list(self, prefix: str = "") -> list[StorageItem]:
|
|
115
|
+
"""List immediate children (non-recursive)."""
|
|
116
|
+
...
|
|
117
|
+
|
|
118
|
+
@abstractmethod
|
|
119
|
+
def walk(self, prefix: str = "") -> list[StorageItem]:
|
|
120
|
+
"""Recursively list all items."""
|
|
121
|
+
...
|
|
122
|
+
|
|
123
|
+
# ─── Versioning (optional) ─────────────────────────────
|
|
124
|
+
|
|
125
|
+
def commit(self, message: str = "") -> str:
|
|
126
|
+
"""Save snapshot. Default: no-op."""
|
|
127
|
+
return ""
|
|
128
|
+
|
|
129
|
+
def log(self, max_count: int = 10) -> list[dict]:
|
|
130
|
+
"""Return history. Default: empty."""
|
|
131
|
+
return []
|
|
132
|
+
|
|
133
|
+
def diff(self, rev_a: str, rev_b: str, path: str = "") -> str:
|
|
134
|
+
"""Diff between revisions. Default: empty."""
|
|
135
|
+
return ""
|
|
136
|
+
|
|
137
|
+
# ─── Archive ───────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
def archive(self, source_prefix: str, archive_path: str) -> str:
|
|
140
|
+
"""
|
|
141
|
+
Archive files under prefix into single compressed file.
|
|
142
|
+
Default: tar.gz. Override for backend-native archiving.
|
|
143
|
+
"""
|
|
144
|
+
import tarfile, io
|
|
145
|
+
buf = io.BytesIO()
|
|
146
|
+
with tarfile.open(fileobj=buf, mode='w:gz') as tar:
|
|
147
|
+
for item in self.walk(source_prefix):
|
|
148
|
+
if not item.is_dir:
|
|
149
|
+
content = self.read_binary(item.path)
|
|
150
|
+
info = tarfile.TarInfo(name=item.path)
|
|
151
|
+
info.size = len(content)
|
|
152
|
+
info.mtime = item.modified_at
|
|
153
|
+
tar.addfile(info, io.BytesIO(content))
|
|
154
|
+
self.write_binary(archive_path, buf.getvalue())
|
|
155
|
+
return archive_path
|
|
156
|
+
|
|
157
|
+
def prune(self, prefix: str, older_than_days: int, dry_run: bool = False) -> list[str]:
|
|
158
|
+
"""Delete items older than N days."""
|
|
159
|
+
import time
|
|
160
|
+
cutoff = time.time() - (older_than_days * 86400)
|
|
161
|
+
deleted = []
|
|
162
|
+
for item in self.walk(prefix):
|
|
163
|
+
if not item.is_dir and item.modified_at < cutoff:
|
|
164
|
+
deleted.append(item.path)
|
|
165
|
+
if not dry_run:
|
|
166
|
+
self.delete(item.path)
|
|
167
|
+
return deleted
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"""
|
|
2
|
+
KnowFlow Dashboard Generator
|
|
3
|
+
Generate a beautiful, mobile-friendly HTML dashboard from knowledge data.
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from html import escape
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _read_md(filepath):
|
|
14
|
+
"""Read a markdown file and return its content."""
|
|
15
|
+
try:
|
|
16
|
+
return Path(filepath).read_text(encoding='utf-8')
|
|
17
|
+
except (FileNotFoundError, IOError):
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _parse_table(md_text):
|
|
22
|
+
"""Parse a markdown table into list of dicts."""
|
|
23
|
+
lines = md_text.strip().split('\n')
|
|
24
|
+
if len(lines) < 3:
|
|
25
|
+
return []
|
|
26
|
+
headers = [h.strip() for h in lines[0].split('|')[1:-1]]
|
|
27
|
+
rows = []
|
|
28
|
+
for line in lines[2:]: # skip header and separator
|
|
29
|
+
cols = [c.strip() for c in line.split('|')[1:-1]]
|
|
30
|
+
if len(cols) == len(headers):
|
|
31
|
+
rows.append(dict(zip(headers, cols)))
|
|
32
|
+
return rows
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _parse_key_value(md_text):
|
|
36
|
+
"""Parse key-value pairs from markdown (lines like '- **Key**: Value')."""
|
|
37
|
+
result = {}
|
|
38
|
+
for m in re.finditer(r'-\s+\*\*(.+?)\*\*:\s*(.+)', md_text):
|
|
39
|
+
result[m.group(1).strip()] = m.group(2).strip()
|
|
40
|
+
return result
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class DashboardGenerator:
|
|
44
|
+
"""Generate a static HTML dashboard from knowledge data."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, knowledge_dir, output_dir=None, password=None):
|
|
47
|
+
self.knowledge_dir = Path(knowledge_dir)
|
|
48
|
+
self.output_dir = Path(output_dir or knowledge_dir / 'dashboard')
|
|
49
|
+
self.password = password
|
|
50
|
+
self.data = {}
|
|
51
|
+
|
|
52
|
+
def collect(self):
|
|
53
|
+
"""Collect all data from the knowledge directory."""
|
|
54
|
+
obsidian_dir = self.knowledge_dir / 'obsidian'
|
|
55
|
+
output_dir = self.knowledge_dir / 'output'
|
|
56
|
+
|
|
57
|
+
# Football predictions
|
|
58
|
+
football_dir = output_dir / 'football' / 'predictions'
|
|
59
|
+
football_files = sorted(football_dir.glob('*.md')) if football_dir.exists() else []
|
|
60
|
+
football_data = []
|
|
61
|
+
for f in football_files[-5:]: # last 5
|
|
62
|
+
content = _read_md(f)
|
|
63
|
+
if content:
|
|
64
|
+
kv = _parse_key_value(content)
|
|
65
|
+
tables = re.findall(r'## Matches\n\n(.*?)(?=\n\n|$)', content, re.DOTALL)
|
|
66
|
+
matches = _parse_table(tables[0]) if tables else []
|
|
67
|
+
football_data.append({
|
|
68
|
+
'date': kv.get('Date', f.stem),
|
|
69
|
+
'file': f.name,
|
|
70
|
+
'matches': matches,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
# Football learnings
|
|
74
|
+
learnings_md = _read_md(output_dir / 'football' / 'learnings.md') or \
|
|
75
|
+
_read_md(obsidian_dir / 'football' / 'learnings.md')
|
|
76
|
+
|
|
77
|
+
# Stock analysis
|
|
78
|
+
stock_base = output_dir / 'stock' / 'daily'
|
|
79
|
+
stock_dates = sorted([d for d in stock_base.iterdir() if d.is_dir()]) if stock_base.exists() else []
|
|
80
|
+
stock_data = {}
|
|
81
|
+
if stock_dates:
|
|
82
|
+
latest = stock_dates[-1]
|
|
83
|
+
for f in latest.glob('*.md'):
|
|
84
|
+
content = _read_md(f)
|
|
85
|
+
if content:
|
|
86
|
+
kv = _parse_key_value(content)
|
|
87
|
+
tables = re.findall(r'## (.*?)\n\n(.*?)(?=\n##|\Z)', content, re.DOTALL)
|
|
88
|
+
stock_data[f.stem] = {'meta': kv, 'sections': tables}
|
|
89
|
+
|
|
90
|
+
# Stock portfolio
|
|
91
|
+
portfolio_md = _read_md(output_dir / 'stock' / 'portfolio' / 'portfolio_million.md') or \
|
|
92
|
+
_read_md(output_dir / 'stock' / 'portfolio' / 'simulated_portfolio.md')
|
|
93
|
+
|
|
94
|
+
# Worldcup
|
|
95
|
+
worldcup_md = _read_md(output_dir / 'worldcup' / 'squads.md') or \
|
|
96
|
+
_read_md(obsidian_dir / 'worldcup' / 'squads.md')
|
|
97
|
+
worldcup_teams = []
|
|
98
|
+
if worldcup_md:
|
|
99
|
+
tables = re.findall(r'## Teams\n\n(.*?)(?=\n\n|$)', worldcup_md, re.DOTALL)
|
|
100
|
+
worldcup_teams = _parse_table(tables[0]) if tables else []
|
|
101
|
+
|
|
102
|
+
# Count all files
|
|
103
|
+
total_files = sum(1 for _ in self.knowledge_dir.rglob('*') if _.is_file() and '.git' not in str(_))
|
|
104
|
+
|
|
105
|
+
self.data = {
|
|
106
|
+
'generated_at': datetime.now().strftime('%Y-%m-%d %H:%M'),
|
|
107
|
+
'football': {
|
|
108
|
+
'predictions': football_data,
|
|
109
|
+
'learnings': learnings_md,
|
|
110
|
+
},
|
|
111
|
+
'stock': {
|
|
112
|
+
'latest_date': stock_dates[-1].name if stock_dates else None,
|
|
113
|
+
'files': stock_data,
|
|
114
|
+
},
|
|
115
|
+
'worldcup': {
|
|
116
|
+
'teams': worldcup_teams,
|
|
117
|
+
},
|
|
118
|
+
'stats': {
|
|
119
|
+
'total_files': total_files,
|
|
120
|
+
'football_matches': sum(len(p['matches']) for p in football_data),
|
|
121
|
+
'worldcup_teams': len(worldcup_teams),
|
|
122
|
+
'stock_strategies': len(stock_data),
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return self.data
|
|
126
|
+
|
|
127
|
+
def generate_html(self):
|
|
128
|
+
"""Generate the HTML dashboard."""
|
|
129
|
+
d = self.data
|
|
130
|
+
|
|
131
|
+
# Build football matches table
|
|
132
|
+
football_html = ''
|
|
133
|
+
for pred in d['football']['predictions']:
|
|
134
|
+
if pred['matches']:
|
|
135
|
+
football_html += f'<h4>📅 {escape(pred["date"])}</h4>\n'
|
|
136
|
+
football_html += '<table><thead><tr><th>对阵</th><th>预测</th><th>置信度</th><th>比分</th></tr></thead><tbody>\n'
|
|
137
|
+
for m in pred['matches']:
|
|
138
|
+
teams = escape(m.get('Match', ''))
|
|
139
|
+
prediction = escape(m.get('Prediction', ''))
|
|
140
|
+
confidence = escape(m.get('Confidence', ''))
|
|
141
|
+
score = escape(m.get('Predicted Score', ''))
|
|
142
|
+
football_html += f'<tr><td>{teams}</td><td>{prediction}</td><td>{confidence}%</td><td>{score}</td></tr>\n'
|
|
143
|
+
football_html += '</tbody></table>\n'
|
|
144
|
+
|
|
145
|
+
# Build worldcup table
|
|
146
|
+
wc_html = ''
|
|
147
|
+
if d['worldcup']['teams']:
|
|
148
|
+
wc_html = '<table><thead><tr><th>球队</th><th>分组</th><th>人数</th><th>公布时间</th></tr></thead><tbody>\n'
|
|
149
|
+
for t in d['worldcup']['teams']:
|
|
150
|
+
name = escape(t.get('Name Cn', t.get('Name En', '')))
|
|
151
|
+
group = escape(t.get('Group', ''))
|
|
152
|
+
size = escape(t.get('Squad Size', ''))
|
|
153
|
+
announced = escape(t.get('Announced At', ''))
|
|
154
|
+
wc_html += f'<tr><td>{name}</td><td>{group}</td><td>{size}</td><td>{announced}</td></tr>\n'
|
|
155
|
+
wc_html += '</tbody></table>\n'
|
|
156
|
+
|
|
157
|
+
# Build stock summary
|
|
158
|
+
stock_html = ''
|
|
159
|
+
for fname, fdata in d['stock']['files'].items():
|
|
160
|
+
meta = fdata.get('meta', {})
|
|
161
|
+
idx_px = meta.get('Price', '')
|
|
162
|
+
idx_chg = meta.get('Change Pct', '')
|
|
163
|
+
is_pos = idx_chg and float(idx_chg) >= 0
|
|
164
|
+
stock_html += f'<div class="stock-card">\n'
|
|
165
|
+
stock_html += f' <h4>{escape(fname.replace("_", " ").title())}</h4>\n'
|
|
166
|
+
if idx_px:
|
|
167
|
+
cls = 'pos' if is_pos else 'neg'
|
|
168
|
+
stock_html += f' <p>点位: <strong>{escape(idx_px)}</strong> <span class="{cls}">{escape(idx_chg)}%</span></p>\n'
|
|
169
|
+
stock_html += '</div>\n'
|
|
170
|
+
|
|
171
|
+
# Build the full HTML
|
|
172
|
+
now = d['generated_at']
|
|
173
|
+
total_matches = d['stats']['football_matches']
|
|
174
|
+
total_teams = d['stats']['worldcup_teams']
|
|
175
|
+
total_strats = d['stats']['stock_strategies']
|
|
176
|
+
total_files = d['stats']['total_files']
|
|
177
|
+
|
|
178
|
+
# Password protection
|
|
179
|
+
password_guard = ''
|
|
180
|
+
if self.password:
|
|
181
|
+
password_guard = f'''
|
|
182
|
+
<div id="password-overlay" style="position:fixed;top:0;left:0;right:0;bottom:0;background:#0f1117;z-index:9999;display:flex;align-items:center;justify-content:center;">
|
|
183
|
+
<div style="background:#161b22;border:1px solid #30363d;border-radius:16px;padding:40px;width:90%;max-width:380px;text-align:center;">
|
|
184
|
+
<div style="font-size:3em;margin-bottom:16px;">🔐</div>
|
|
185
|
+
<h2 style="color:#f0f6fc;margin-bottom:8px;">KnowFlow</h2>
|
|
186
|
+
<p style="color:#8b949e;font-size:0.9em;margin-bottom:24px;">请输入密码查看仪表盘</p>
|
|
187
|
+
<input id="pwd-input" type="password" placeholder="密码" style="width:100%;padding:12px 16px;background:#0f1117;border:1px solid #30363d;border-radius:8px;color:#e1e4e8;font-size:1em;outline:none;margin-bottom:12px;box-sizing:border-box;" onkeydown="if(event.key==='Enter')checkPwd()">
|
|
188
|
+
<button onclick="checkPwd()" style="width:100%;padding:12px;background:#238636;border:none;border-radius:8px;color:#fff;font-size:1em;cursor:pointer;font-weight:600;">进入</button>
|
|
189
|
+
<p id="pwd-error" style="color:#f85149;font-size:0.85em;margin-top:12px;display:none;">密码错误,请重试</p>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
<script>
|
|
193
|
+
var _pwd = {json.dumps(self.password)};
|
|
194
|
+
function checkPwd() {{
|
|
195
|
+
var v = document.getElementById('pwd-input').value;
|
|
196
|
+
if (v === _pwd) {{
|
|
197
|
+
document.getElementById('password-overlay').style.display = 'none';
|
|
198
|
+
document.getElementById('dashboard-content').style.display = 'block';
|
|
199
|
+
}} else {{
|
|
200
|
+
document.getElementById('pwd-error').style.display = 'block';
|
|
201
|
+
}}
|
|
202
|
+
}}
|
|
203
|
+
document.addEventListener('DOMContentLoaded', function() {{
|
|
204
|
+
document.getElementById('password-overlay').style.display = 'flex';
|
|
205
|
+
document.getElementById('dashboard-content').style.display = 'none';
|
|
206
|
+
document.getElementById('pwd-input').focus();
|
|
207
|
+
}});
|
|
208
|
+
</script>'''
|
|
209
|
+
|
|
210
|
+
html = f'''<!DOCTYPE html>
|
|
211
|
+
<html lang="zh-CN">
|
|
212
|
+
<head>
|
|
213
|
+
<meta charset="UTF-8">
|
|
214
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
215
|
+
<title>KnowFlow Dashboard</title>
|
|
216
|
+
<style>
|
|
217
|
+
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
|
218
|
+
body {{
|
|
219
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
220
|
+
background: #0f1117;
|
|
221
|
+
color: #e1e4e8;
|
|
222
|
+
min-height: 100vh;
|
|
223
|
+
}}
|
|
224
|
+
.container {{ max-width: 1200px; margin: 0 auto; padding: 20px; }}
|
|
225
|
+
.header {{
|
|
226
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
227
|
+
padding: 20px 0; border-bottom: 1px solid #21262d; margin-bottom: 24px;
|
|
228
|
+
flex-wrap: wrap; gap: 12px;
|
|
229
|
+
}}
|
|
230
|
+
.header h1 {{ font-size: 1.6em; background: linear-gradient(135deg, #58a6ff, #bc8cff); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }}
|
|
231
|
+
.header .sub {{ color: #8b949e; font-size: 0.85em; }}
|
|
232
|
+
.stats-row {{
|
|
233
|
+
display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
234
|
+
gap: 16px; margin-bottom: 24px;
|
|
235
|
+
}}
|
|
236
|
+
.stat-card {{
|
|
237
|
+
background: #161b22; border: 1px solid #30363d; border-radius: 12px;
|
|
238
|
+
padding: 20px; text-align: center;
|
|
239
|
+
}}
|
|
240
|
+
.stat-card .num {{ font-size: 2em; font-weight: 700; color: #58a6ff; }}
|
|
241
|
+
.stat-card .label {{ color: #8b949e; font-size: 0.85em; margin-top: 4px; }}
|
|
242
|
+
.section {{
|
|
243
|
+
background: #161b22; border: 1px solid #30363d; border-radius: 12px;
|
|
244
|
+
padding: 24px; margin-bottom: 20px;
|
|
245
|
+
}}
|
|
246
|
+
.section h2 {{ font-size: 1.2em; margin-bottom: 16px; color: #f0f6fc; }}
|
|
247
|
+
.section h2 .icon {{ margin-right: 8px; }}
|
|
248
|
+
.section h3 {{ font-size: 1em; color: #58a6ff; margin: 16px 0 8px; }}
|
|
249
|
+
.section h4 {{ font-size: 0.9em; color: #c9d1d9; margin: 12px 0 8px; }}
|
|
250
|
+
table {{ width: 100%; border-collapse: collapse; font-size: 0.85em; }}
|
|
251
|
+
th {{ text-align: left; padding: 8px; border-bottom: 2px solid #30363d; color: #8b949e; font-weight: 600; }}
|
|
252
|
+
td {{ padding: 8px; border-bottom: 1px solid #21262d; }}
|
|
253
|
+
tr:hover td {{ background: #1c2128; }}
|
|
254
|
+
.pos {{ color: #3fb950; }}
|
|
255
|
+
.neg {{ color: #f85149; }}
|
|
256
|
+
.stock-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }}
|
|
257
|
+
.stock-card {{
|
|
258
|
+
background: #1c2128; border: 1px solid #30363d; border-radius: 8px;
|
|
259
|
+
padding: 14px; font-size: 0.85em;
|
|
260
|
+
}}
|
|
261
|
+
.stock-card h4 {{ margin-bottom: 8px; }}
|
|
262
|
+
.empty {{ color: #8b949e; font-style: italic; text-align: center; padding: 20px; }}
|
|
263
|
+
.footer {{
|
|
264
|
+
text-align: center; padding: 20px; color: #484f58;
|
|
265
|
+
font-size: 0.8em; border-top: 1px solid #21262d; margin-top: 20px;
|
|
266
|
+
}}
|
|
267
|
+
@media (max-width: 600px) {{
|
|
268
|
+
.container {{ padding: 12px; }}
|
|
269
|
+
.header h1 {{ font-size: 1.2em; }}
|
|
270
|
+
.stat-card .num {{ font-size: 1.4em; }}
|
|
271
|
+
.section {{ padding: 16px; }}
|
|
272
|
+
table {{ font-size: 0.78em; }}
|
|
273
|
+
th, td {{ padding: 5px; }}
|
|
274
|
+
}}
|
|
275
|
+
</style>
|
|
276
|
+
</head>
|
|
277
|
+
{password_guard}
|
|
278
|
+
<div id="dashboard-content">
|
|
279
|
+
<div class="container">
|
|
280
|
+
<div class="header">
|
|
281
|
+
<div>
|
|
282
|
+
<h1>🧠 KnowFlow</h1>
|
|
283
|
+
<div class="sub">Agent Knowledge Dashboard</div>
|
|
284
|
+
</div>
|
|
285
|
+
<div class="sub">🔄 {escape(now)}</div>
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
<div class="stats-row">
|
|
289
|
+
<div class="stat-card"><div class="num">{total_matches}</div><div class="label">⚽ 足球预测</div></div>
|
|
290
|
+
<div class="stat-card"><div class="num">{total_teams}</div><div class="label">🏆 世界杯球队</div></div>
|
|
291
|
+
<div class="stat-card"><div class="num">{total_strats}</div><div class="label">📈 股票策略</div></div>
|
|
292
|
+
<div class="stat-card"><div class="num">{total_files}</div><div class="label">📁 知识库文件</div></div>
|
|
293
|
+
</div>
|
|
294
|
+
'''
|
|
295
|
+
|
|
296
|
+
# Football section
|
|
297
|
+
html += '<div class="section">\n<h2><span class="icon">⚽</span>足球预测</h2>\n'
|
|
298
|
+
if football_html:
|
|
299
|
+
html += football_html
|
|
300
|
+
else:
|
|
301
|
+
html += '<div class="empty">暂无足球预测数据</div>\n'
|
|
302
|
+
html += '</div>\n'
|
|
303
|
+
|
|
304
|
+
# Worldcup section
|
|
305
|
+
html += '<div class="section">\n<h2><span class="icon">🏆</span>世界杯名单</h2>\n'
|
|
306
|
+
if wc_html:
|
|
307
|
+
html += wc_html
|
|
308
|
+
else:
|
|
309
|
+
html += '<div class="empty">暂无球队数据</div>\n'
|
|
310
|
+
html += '</div>\n'
|
|
311
|
+
|
|
312
|
+
# Stock section
|
|
313
|
+
html += '<div class="section">\n<h2><span class="icon">📈</span>股票分析</h2>\n'
|
|
314
|
+
if stock_html:
|
|
315
|
+
stock_date = escape(d['stock']['latest_date'] or '')
|
|
316
|
+
html += f'<h3>📅 {stock_date}</h3>\n<div class="stock-grid">\n{stock_html}</div>\n'
|
|
317
|
+
else:
|
|
318
|
+
html += '<div class="empty">暂无股票分析数据</div>\n'
|
|
319
|
+
html += '</div>\n'
|
|
320
|
+
|
|
321
|
+
# System status
|
|
322
|
+
html += f'''<div class="section">
|
|
323
|
+
<h2><span class="icon">🔧</span>系统状态</h2>
|
|
324
|
+
<table>
|
|
325
|
+
<tr><td>知识库位置</td><td>{escape(str(self.knowledge_dir))}</td></tr>
|
|
326
|
+
<tr><td>文件总数</td><td>{total_files}</td></tr>
|
|
327
|
+
<tr><td>仪表盘生成</td><td>{escape(now)}</td></tr>
|
|
328
|
+
</table>
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
<div class="footer">
|
|
332
|
+
KnowFlow © {datetime.now().year} — Agent Knowledge Pipeline — 数据自动流动
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
</body>
|
|
337
|
+
</html>'''
|
|
338
|
+
return html
|
|
339
|
+
|
|
340
|
+
def generate(self):
|
|
341
|
+
"""Collect data and generate the dashboard HTML file."""
|
|
342
|
+
self.collect()
|
|
343
|
+
html = self.generate_html()
|
|
344
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
345
|
+
out_path = self.output_dir / 'index.html'
|
|
346
|
+
out_path.write_text(html, encoding='utf-8')
|
|
347
|
+
return out_path
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def generate(knowledge_dir, output_dir=None, password=None):
|
|
351
|
+
"""Convenience function to generate a dashboard."""
|
|
352
|
+
gen = DashboardGenerator(knowledge_dir, output_dir, password=password)
|
|
353
|
+
path = gen.generate()
|
|
354
|
+
print(f"✅ Dashboard generated: {path}")
|
|
355
|
+
return path
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# 📝 Daily Journal Template
|
|
2
|
+
# KnowFlow template: daily-journal
|
|
3
|
+
# Personal daily notes with automatic archiving
|
|
4
|
+
|
|
5
|
+
name: journal
|
|
6
|
+
display_name: "📝 每日日记"
|
|
7
|
+
description: 个人日记+自动归档
|
|
8
|
+
|
|
9
|
+
schema:
|
|
10
|
+
entries:
|
|
11
|
+
type: directory
|
|
12
|
+
pattern: "*.json"
|
|
13
|
+
description: 每日日记条目
|
|
14
|
+
|
|
15
|
+
exports:
|
|
16
|
+
entries:
|
|
17
|
+
source: "entries/*.json"
|
|
18
|
+
|
|
19
|
+
retention:
|
|
20
|
+
half_lives:
|
|
21
|
+
entries: 365 # 日记保留一年
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# ⚽ Football Prediction Template
|
|
2
|
+
# KnowFlow template: football-prediction
|
|
3
|
+
# Automates match prediction → tracking → review loop
|
|
4
|
+
|
|
5
|
+
name: football
|
|
6
|
+
display_name: "⚽ 足球预测"
|
|
7
|
+
description: 比赛预测+自进化复盘闭环
|
|
8
|
+
|
|
9
|
+
schema:
|
|
10
|
+
predictions:
|
|
11
|
+
type: directory
|
|
12
|
+
pattern: "*.json"
|
|
13
|
+
description: 每日预测结果
|
|
14
|
+
learnings.json:
|
|
15
|
+
type: json_append_only
|
|
16
|
+
description: 复盘学习记录
|
|
17
|
+
|
|
18
|
+
exports:
|
|
19
|
+
predictions:
|
|
20
|
+
source: "predictions/*.json"
|
|
21
|
+
learnings:
|
|
22
|
+
source: "learnings.json"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# 📚 Project Wiki Template
|
|
2
|
+
# KnowFlow template: project-wiki
|
|
3
|
+
# Auto-document project files
|
|
4
|
+
|
|
5
|
+
name: wiki
|
|
6
|
+
display_name: "📚 项目文档"
|
|
7
|
+
description: 项目文档自动管理
|
|
8
|
+
|
|
9
|
+
schema:
|
|
10
|
+
pages:
|
|
11
|
+
type: directory
|
|
12
|
+
pattern: "*.json"
|
|
13
|
+
|
|
14
|
+
exports:
|
|
15
|
+
pages:
|
|
16
|
+
source: "pages/*.json"
|
|
17
|
+
|
|
18
|
+
retention:
|
|
19
|
+
half_lives:
|
|
20
|
+
pages: 180
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# 🔬 Research Notes Template
|
|
2
|
+
# KnowFlow template: research-notes
|
|
3
|
+
# Academic research paper notes
|
|
4
|
+
|
|
5
|
+
name: research
|
|
6
|
+
display_name: "🔬 研究笔记"
|
|
7
|
+
description: 学术研究笔记管理
|
|
8
|
+
|
|
9
|
+
schema:
|
|
10
|
+
papers:
|
|
11
|
+
type: directory
|
|
12
|
+
pattern: "*.json"
|
|
13
|
+
notes:
|
|
14
|
+
type: directory
|
|
15
|
+
pattern: "*.json"
|
|
16
|
+
|
|
17
|
+
exports:
|
|
18
|
+
papers:
|
|
19
|
+
source: "papers/*.json"
|
|
20
|
+
notes:
|
|
21
|
+
source: "notes/*.json"
|
|
22
|
+
|
|
23
|
+
retention:
|
|
24
|
+
half_lives:
|
|
25
|
+
papers: 365 # 论文长期保留
|
|
26
|
+
notes: 90
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# 📈 Stock Analysis Template
|
|
2
|
+
# KnowFlow template: stock-analysis
|
|
3
|
+
# Multi-strategy A-share analysis → recommendation → review
|
|
4
|
+
|
|
5
|
+
name: stock
|
|
6
|
+
display_name: "📈 股票分析"
|
|
7
|
+
description: 多策略A股分析+推荐+模拟盘
|
|
8
|
+
|
|
9
|
+
schema:
|
|
10
|
+
daily:
|
|
11
|
+
type: directory
|
|
12
|
+
pattern: "*/"
|
|
13
|
+
description: 每日分析数据
|
|
14
|
+
review.json:
|
|
15
|
+
type: json_append_only
|
|
16
|
+
description: 策略权重复盘
|
|
17
|
+
|
|
18
|
+
exports:
|
|
19
|
+
daily:
|
|
20
|
+
source: "daily/*/recommendation.json"
|
|
21
|
+
review:
|
|
22
|
+
source: "review.json"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# 🌐 Web Monitor Template
|
|
2
|
+
# KnowFlow template: web-monitor
|
|
3
|
+
# Archive web pages and track changes
|
|
4
|
+
|
|
5
|
+
name: monitor
|
|
6
|
+
display_name: "🌐 网页监控"
|
|
7
|
+
description: 网页内容追踪和归档
|
|
8
|
+
|
|
9
|
+
schema:
|
|
10
|
+
snapshots:
|
|
11
|
+
type: directory
|
|
12
|
+
pattern: "*.json"
|
|
13
|
+
|
|
14
|
+
exports:
|
|
15
|
+
snapshots:
|
|
16
|
+
source: "snapshots/*.json"
|
|
17
|
+
|
|
18
|
+
retention:
|
|
19
|
+
half_lives:
|
|
20
|
+
snapshots: 14 # 网页快照2周后归档
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# 🏆 World Cup Squad Tracker
|
|
2
|
+
# KnowFlow template: worldcup-tracker
|
|
3
|
+
# Track national team squad announcements
|
|
4
|
+
|
|
5
|
+
name: worldcup
|
|
6
|
+
display_name: "🏆 世界杯追踪"
|
|
7
|
+
description: 国家队名单和赛程追踪
|
|
8
|
+
|
|
9
|
+
schema:
|
|
10
|
+
squads.json:
|
|
11
|
+
description: 各国球队名单
|
|
12
|
+
schedule.json:
|
|
13
|
+
optional: true
|
|
14
|
+
description: 完整赛程
|
|
15
|
+
|
|
16
|
+
exports:
|
|
17
|
+
squads:
|
|
18
|
+
source: "*.json"
|