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.
Files changed (35) hide show
  1. knowflow/__init__.py +4 -0
  2. knowflow/__main__.py +3 -0
  3. knowflow/adapters/knowledge/__init__.py +42 -0
  4. knowflow/adapters/knowledge/local.py +43 -0
  5. knowflow/adapters/knowledge/obsidian.py +64 -0
  6. knowflow/adapters/storage/__init__.py +62 -0
  7. knowflow/adapters/storage/git.py +155 -0
  8. knowflow/adapters/storage/local.py +110 -0
  9. knowflow/adapters/storage/quark.py +225 -0
  10. knowflow/adapters/storage/s3.py +118 -0
  11. knowflow/adapters/storage/webdav.py +196 -0
  12. knowflow/cli/__init__.py +265 -0
  13. knowflow/cli/template_manager.py +100 -0
  14. knowflow/core/__init__.py +10 -0
  15. knowflow/core/knowledge.py +85 -0
  16. knowflow/core/pipeline.py +119 -0
  17. knowflow/core/storage.py +167 -0
  18. knowflow/dashboard/__init__.py +355 -0
  19. knowflow/templates/daily-journal/plugin.yaml +21 -0
  20. knowflow/templates/football-prediction/plugin.yaml +22 -0
  21. knowflow/templates/project-wiki/plugin.yaml +20 -0
  22. knowflow/templates/research-notes/plugin.yaml +26 -0
  23. knowflow/templates/stock-analysis/plugin.yaml +22 -0
  24. knowflow/templates/web-monitor/plugin.yaml +20 -0
  25. knowflow/templates/worldcup-tracker/plugin.yaml +18 -0
  26. knowflow/transforms/__init__.py +6 -0
  27. knowflow/transforms/archive.py +87 -0
  28. knowflow/transforms/markdown_export.py +110 -0
  29. knowflow/transforms/profile_index.py +89 -0
  30. knowflow-0.1.0.dist-info/METADATA +264 -0
  31. knowflow-0.1.0.dist-info/RECORD +35 -0
  32. knowflow-0.1.0.dist-info/WHEEL +5 -0
  33. knowflow-0.1.0.dist-info/entry_points.txt +2 -0
  34. knowflow-0.1.0.dist-info/licenses/LICENSE +21 -0
  35. knowflow-0.1.0.dist-info/top_level.txt +1 -0
@@ -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 &copy; {datetime.now().year} &mdash; Agent Knowledge Pipeline &mdash; 数据自动流动
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"
@@ -0,0 +1,6 @@
1
+ """knowflow/transforms/__init__.py"""
2
+ from .markdown_export import MarkdownExport
3
+ from .profile_index import ProfileIndex
4
+ from .archive import ArchiveStage
5
+
6
+ __all__ = ["MarkdownExport", "ProfileIndex", "ArchiveStage"]