mirror of
https://github.com/m1ngsama/TUT.git
synced 2026-02-08 00:54:05 +00:00
feat: Add persistent browsing history
- Implement HistoryManager for JSON persistence (~/.config/tut/history.json) - Auto-record page visits with URL, title, and timestamp - Update visit time when revisiting URLs (move to front) - Limit to 1000 entries maximum - Add :history command to view browsing history - History entries are clickable links - Add test_history test suite
This commit is contained in:
parent
3f7b627da5
commit
8d56a7b67b
8 changed files with 465 additions and 7 deletions
|
|
@ -38,6 +38,7 @@ add_executable(tut
|
|||
src/http_client.cpp
|
||||
src/input_handler.cpp
|
||||
src/bookmark.cpp
|
||||
src/history.cpp
|
||||
src/render/terminal.cpp
|
||||
src/render/renderer.cpp
|
||||
src/render/layout.cpp
|
||||
|
|
@ -132,3 +133,9 @@ add_executable(test_bookmark
|
|||
src/bookmark.cpp
|
||||
tests/test_bookmark.cpp
|
||||
)
|
||||
|
||||
# 历史记录测试
|
||||
add_executable(test_history
|
||||
src/history.cpp
|
||||
tests/test_history.cpp
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
# TUT 2.0 - 下次继续从这里开始
|
||||
|
||||
## 当前位置
|
||||
- **阶段**: 代码整合完成,准备发布 v2.0.0
|
||||
- **进度**: 所有核心功能已完成,代码库已整合简化
|
||||
- **最后提交**: `refactor: Consolidate v2 architecture into main codebase`
|
||||
- **阶段**: Phase 7 - 历史记录持久化 (已完成!)
|
||||
- **进度**: 历史记录自动保存,支持 :history 命令查看
|
||||
- **最后提交**: `feat: Add persistent browsing history`
|
||||
|
||||
## 立即可做的事
|
||||
|
||||
|
|
@ -14,8 +14,22 @@
|
|||
|
||||
书签存储在 `~/.config/tut/bookmarks.json`
|
||||
|
||||
### 2. 查看历史记录
|
||||
- **:history** 或 **:hist** - 查看浏览历史
|
||||
|
||||
历史记录存储在 `~/.config/tut/history.json`
|
||||
|
||||
## 已完成的功能清单
|
||||
|
||||
### Phase 7 - 历史记录持久化
|
||||
- [x] HistoryEntry 数据结构 (URL, 标题, 访问时间)
|
||||
- [x] JSON 持久化存储 (~/.config/tut/history.json)
|
||||
- [x] 自动记录访问历史
|
||||
- [x] 重复访问更新时间
|
||||
- [x] 最大 1000 条记录限制
|
||||
- [x] :history 命令查看历史页面
|
||||
- [x] 历史链接可点击跳转
|
||||
|
||||
### Phase 6 - 异步HTTP
|
||||
- [x] libcurl multi接口实现非阻塞请求
|
||||
- [x] AsyncState状态管理 (IDLE/LOADING/COMPLETE/FAILED/CANCELLED)
|
||||
|
|
@ -79,6 +93,7 @@ src/
|
|||
├── html_parser.cpp/h # HTML 解析
|
||||
├── input_handler.cpp/h # 输入处理
|
||||
├── bookmark.cpp/h # 书签管理
|
||||
├── history.cpp/h # 历史记录管理
|
||||
├── render/
|
||||
│ ├── terminal.cpp/h # 终端抽象 (ncurses)
|
||||
│ ├── renderer.cpp/h # FrameBuffer + 差分渲染
|
||||
|
|
@ -96,7 +111,8 @@ tests/
|
|||
├── test_layout.cpp # Layout + 图片占位符测试
|
||||
├── test_http_async.cpp # HTTP 异步测试
|
||||
├── test_html_parse.cpp # HTML 解析测试
|
||||
└── test_bookmark.cpp # 书签测试
|
||||
├── test_bookmark.cpp # 书签测试
|
||||
└── test_history.cpp # 历史记录测试
|
||||
```
|
||||
|
||||
## 构建与运行
|
||||
|
|
@ -136,6 +152,7 @@ cmake --build build
|
|||
| D | 删除书签 |
|
||||
| :o URL | 打开URL |
|
||||
| :bookmarks | 查看书签 |
|
||||
| :history | 查看历史 |
|
||||
| :q | 退出 |
|
||||
| ? | 帮助 |
|
||||
| Esc | 取消加载 |
|
||||
|
|
@ -145,7 +162,7 @@ cmake --build build
|
|||
1. **更多表单交互** - 文本输入编辑,下拉选择
|
||||
2. **图片缓存** - 避免重复下载相同图片
|
||||
3. **异步图片加载** - 图片也使用异步加载
|
||||
4. **历史记录管理** - 持久化历史记录,历史页面
|
||||
4. **Cookie 支持** - 保存和发送 Cookie
|
||||
|
||||
## 恢复对话时说
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "browser.h"
|
||||
#include "dom_tree.h"
|
||||
#include "bookmark.h"
|
||||
#include "history.h"
|
||||
#include "render/colors.h"
|
||||
#include "render/decorations.h"
|
||||
#include "render/image.h"
|
||||
|
|
@ -48,6 +49,7 @@ public:
|
|||
HtmlParser html_parser;
|
||||
InputHandler input_handler;
|
||||
tut::BookmarkManager bookmark_manager;
|
||||
tut::HistoryManager history_manager;
|
||||
|
||||
// 新渲染系统
|
||||
Terminal terminal;
|
||||
|
|
@ -185,6 +187,8 @@ public:
|
|||
}
|
||||
history.push_back(url);
|
||||
history_pos = history.size() - 1;
|
||||
// 持久化历史记录
|
||||
history_manager.add(url, current_tree.title);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
@ -217,6 +221,8 @@ public:
|
|||
}
|
||||
history.push_back(url);
|
||||
history_pos = history.size() - 1;
|
||||
// 持久化历史记录
|
||||
history_manager.add(url, current_tree.title);
|
||||
}
|
||||
|
||||
// 加载图片(仍然同步,可以后续优化)
|
||||
|
|
@ -326,6 +332,8 @@ public:
|
|||
}
|
||||
history.push_back(pending_url);
|
||||
history_pos = history.size() - 1;
|
||||
// 持久化历史记录
|
||||
history_manager.add(pending_url, current_tree.title);
|
||||
}
|
||||
|
||||
status_message = current_tree.title.empty() ? pending_url : current_tree.title;
|
||||
|
|
@ -597,6 +605,10 @@ public:
|
|||
show_bookmarks();
|
||||
break;
|
||||
|
||||
case Action::SHOW_HISTORY:
|
||||
show_history();
|
||||
break;
|
||||
|
||||
case Action::QUIT:
|
||||
break; // 在main loop处理
|
||||
|
||||
|
|
@ -784,12 +796,14 @@ public:
|
|||
<li>B - Add bookmark</li>
|
||||
<li>D - Remove bookmark</li>
|
||||
<li>:bookmarks - Show bookmarks</li>
|
||||
<li>:history - Show history</li>
|
||||
</ul>
|
||||
|
||||
<h2>Commands</h2>
|
||||
<ul>
|
||||
<li>:o URL - Open URL</li>
|
||||
<li>:bookmarks - Show bookmarks</li>
|
||||
<li>:history - Show history</li>
|
||||
<li>:q - Quit</li>
|
||||
<li>? - Show this help</li>
|
||||
</ul>
|
||||
|
|
@ -851,6 +865,54 @@ public:
|
|||
status_message = "Bookmarks";
|
||||
}
|
||||
|
||||
void show_history() {
|
||||
std::ostringstream html;
|
||||
html << R"(
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>History</title></head>
|
||||
<body>
|
||||
<h1>History</h1>
|
||||
)";
|
||||
|
||||
const auto& entries = history_manager.get_all();
|
||||
|
||||
if (entries.empty()) {
|
||||
html << "<p>No browsing history yet.</p>\n";
|
||||
} else {
|
||||
html << "<ul>\n";
|
||||
// 显示最近的 100 条
|
||||
size_t count = std::min(entries.size(), static_cast<size_t>(100));
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
const auto& entry = entries[i];
|
||||
// 格式化时间
|
||||
char time_buf[64];
|
||||
std::strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M",
|
||||
std::localtime(&entry.visit_time));
|
||||
html << "<li><a href=\"" << entry.url << "\">"
|
||||
<< (entry.title.empty() ? entry.url : entry.title)
|
||||
<< "</a> <small>(" << time_buf << ")</small></li>\n";
|
||||
}
|
||||
html << "</ul>\n";
|
||||
if (entries.size() > 100) {
|
||||
html << "<p><i>Showing 100 of " << entries.size() << " entries</i></p>\n";
|
||||
}
|
||||
html << "<hr>\n";
|
||||
html << "<p>" << entries.size() << " entries in history.</p>\n";
|
||||
}
|
||||
|
||||
html << R"(
|
||||
</body>
|
||||
</html>
|
||||
)";
|
||||
|
||||
current_tree = html_parser.parse_tree(html.str(), "history://");
|
||||
current_layout = layout_engine->layout(current_tree);
|
||||
scroll_pos = 0;
|
||||
active_link = current_tree.links.empty() ? -1 : 0;
|
||||
status_message = "History";
|
||||
}
|
||||
|
||||
void add_bookmark() {
|
||||
if (current_url.empty() || current_url.find("://") == std::string::npos) {
|
||||
status_message = "Cannot bookmark this page";
|
||||
|
|
@ -858,7 +920,8 @@ public:
|
|||
}
|
||||
|
||||
// 不要书签特殊页面
|
||||
if (current_url.find("help://") == 0 || current_url.find("bookmarks://") == 0) {
|
||||
if (current_url.find("help://") == 0 || current_url.find("bookmarks://") == 0 ||
|
||||
current_url.find("history://") == 0) {
|
||||
status_message = "Cannot bookmark special pages";
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
217
src/history.cpp
Normal file
217
src/history.cpp
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
#include "history.h"
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <algorithm>
|
||||
#include <sys/stat.h>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace tut {
|
||||
|
||||
HistoryManager::HistoryManager() {
|
||||
load();
|
||||
}
|
||||
|
||||
HistoryManager::~HistoryManager() {
|
||||
save();
|
||||
}
|
||||
|
||||
std::string HistoryManager::get_history_path() {
|
||||
const char* home = std::getenv("HOME");
|
||||
if (!home) {
|
||||
home = "/tmp";
|
||||
}
|
||||
return std::string(home) + "/.config/tut/history.json";
|
||||
}
|
||||
|
||||
bool HistoryManager::ensure_config_dir() {
|
||||
const char* home = std::getenv("HOME");
|
||||
if (!home) home = "/tmp";
|
||||
|
||||
std::string config_dir = std::string(home) + "/.config";
|
||||
std::string tut_dir = config_dir + "/tut";
|
||||
|
||||
struct stat st;
|
||||
if (stat(tut_dir.c_str(), &st) == 0) {
|
||||
return S_ISDIR(st.st_mode);
|
||||
}
|
||||
|
||||
mkdir(config_dir.c_str(), 0755);
|
||||
return mkdir(tut_dir.c_str(), 0755) == 0 || errno == EEXIST;
|
||||
}
|
||||
|
||||
// JSON escape/unescape
|
||||
static std::string json_escape(const std::string& s) {
|
||||
std::string result;
|
||||
result.reserve(s.size() + 10);
|
||||
for (char c : s) {
|
||||
switch (c) {
|
||||
case '"': result += "\\\""; break;
|
||||
case '\\': result += "\\\\"; break;
|
||||
case '\n': result += "\\n"; break;
|
||||
case '\r': result += "\\r"; break;
|
||||
case '\t': result += "\\t"; break;
|
||||
default: result += c; break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::string json_unescape(const std::string& s) {
|
||||
std::string result;
|
||||
result.reserve(s.size());
|
||||
for (size_t i = 0; i < s.size(); ++i) {
|
||||
if (s[i] == '\\' && i + 1 < s.size()) {
|
||||
switch (s[i + 1]) {
|
||||
case '"': result += '"'; ++i; break;
|
||||
case '\\': result += '\\'; ++i; break;
|
||||
case 'n': result += '\n'; ++i; break;
|
||||
case 'r': result += '\r'; ++i; break;
|
||||
case 't': result += '\t'; ++i; break;
|
||||
default: result += s[i]; break;
|
||||
}
|
||||
} else {
|
||||
result += s[i];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool HistoryManager::load() {
|
||||
entries_.clear();
|
||||
|
||||
std::ifstream file(get_history_path());
|
||||
if (!file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string content((std::istreambuf_iterator<char>(file)),
|
||||
std::istreambuf_iterator<char>());
|
||||
file.close();
|
||||
|
||||
size_t pos = content.find('[');
|
||||
if (pos == std::string::npos) return false;
|
||||
pos++;
|
||||
|
||||
while (pos < content.size()) {
|
||||
pos = content.find('{', pos);
|
||||
if (pos == std::string::npos) break;
|
||||
pos++;
|
||||
|
||||
HistoryEntry entry;
|
||||
|
||||
while (pos < content.size() && content[pos] != '}') {
|
||||
while (pos < content.size() && (content[pos] == ' ' || content[pos] == '\n' ||
|
||||
content[pos] == '\r' || content[pos] == '\t' || content[pos] == ',')) {
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (content[pos] == '}') break;
|
||||
|
||||
if (content[pos] != '"') { pos++; continue; }
|
||||
pos++;
|
||||
|
||||
size_t key_end = content.find('"', pos);
|
||||
if (key_end == std::string::npos) break;
|
||||
std::string key = content.substr(pos, key_end - pos);
|
||||
pos = key_end + 1;
|
||||
|
||||
pos = content.find(':', pos);
|
||||
if (pos == std::string::npos) break;
|
||||
pos++;
|
||||
|
||||
while (pos < content.size() && (content[pos] == ' ' || content[pos] == '\n' ||
|
||||
content[pos] == '\r' || content[pos] == '\t')) {
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (content[pos] == '"') {
|
||||
pos++;
|
||||
size_t val_end = pos;
|
||||
while (val_end < content.size()) {
|
||||
if (content[val_end] == '"' && content[val_end - 1] != '\\') break;
|
||||
val_end++;
|
||||
}
|
||||
std::string value = json_unescape(content.substr(pos, val_end - pos));
|
||||
pos = val_end + 1;
|
||||
|
||||
if (key == "url") entry.url = value;
|
||||
else if (key == "title") entry.title = value;
|
||||
} else {
|
||||
size_t val_end = pos;
|
||||
while (val_end < content.size() && content[val_end] >= '0' && content[val_end] <= '9') {
|
||||
val_end++;
|
||||
}
|
||||
std::string value = content.substr(pos, val_end - pos);
|
||||
pos = val_end;
|
||||
|
||||
if (key == "time") {
|
||||
entry.visit_time = std::stoll(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!entry.url.empty()) {
|
||||
entries_.push_back(entry);
|
||||
}
|
||||
|
||||
pos = content.find('}', pos);
|
||||
if (pos == std::string::npos) break;
|
||||
pos++;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HistoryManager::save() const {
|
||||
if (!ensure_config_dir()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ofstream file(get_history_path());
|
||||
if (!file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
file << "[\n";
|
||||
for (size_t i = 0; i < entries_.size(); ++i) {
|
||||
const auto& entry = entries_[i];
|
||||
file << " {\n";
|
||||
file << " \"url\": \"" << json_escape(entry.url) << "\",\n";
|
||||
file << " \"title\": \"" << json_escape(entry.title) << "\",\n";
|
||||
file << " \"time\": " << entry.visit_time << "\n";
|
||||
file << " }";
|
||||
if (i + 1 < entries_.size()) {
|
||||
file << ",";
|
||||
}
|
||||
file << "\n";
|
||||
}
|
||||
file << "]\n";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void HistoryManager::add(const std::string& url, const std::string& title) {
|
||||
// Remove existing entry with same URL
|
||||
auto it = std::find_if(entries_.begin(), entries_.end(),
|
||||
[&url](const HistoryEntry& e) { return e.url == url; });
|
||||
if (it != entries_.end()) {
|
||||
entries_.erase(it);
|
||||
}
|
||||
|
||||
// Add new entry at the front
|
||||
entries_.insert(entries_.begin(), HistoryEntry(url, title));
|
||||
|
||||
// Enforce max entries limit
|
||||
if (entries_.size() > MAX_ENTRIES) {
|
||||
entries_.resize(MAX_ENTRIES);
|
||||
}
|
||||
|
||||
save();
|
||||
}
|
||||
|
||||
void HistoryManager::clear() {
|
||||
entries_.clear();
|
||||
save();
|
||||
}
|
||||
|
||||
} // namespace tut
|
||||
78
src/history.h
Normal file
78
src/history.h
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <ctime>
|
||||
|
||||
namespace tut {
|
||||
|
||||
/**
|
||||
* 历史记录条目
|
||||
*/
|
||||
struct HistoryEntry {
|
||||
std::string url;
|
||||
std::string title;
|
||||
std::time_t visit_time;
|
||||
|
||||
HistoryEntry() : visit_time(0) {}
|
||||
HistoryEntry(const std::string& url, const std::string& title)
|
||||
: url(url), title(title), visit_time(std::time(nullptr)) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* 历史记录管理器
|
||||
*
|
||||
* 历史记录存储在 ~/.config/tut/history.json
|
||||
* 最多保存 MAX_ENTRIES 条记录
|
||||
*/
|
||||
class HistoryManager {
|
||||
public:
|
||||
static constexpr size_t MAX_ENTRIES = 1000;
|
||||
|
||||
HistoryManager();
|
||||
~HistoryManager();
|
||||
|
||||
/**
|
||||
* 加载历史记录
|
||||
*/
|
||||
bool load();
|
||||
|
||||
/**
|
||||
* 保存历史记录
|
||||
*/
|
||||
bool save() const;
|
||||
|
||||
/**
|
||||
* 添加历史记录
|
||||
* 如果 URL 已存在,会更新访问时间并移到最前面
|
||||
*/
|
||||
void add(const std::string& url, const std::string& title);
|
||||
|
||||
/**
|
||||
* 清空历史记录
|
||||
*/
|
||||
void clear();
|
||||
|
||||
/**
|
||||
* 获取历史记录列表(最新的在前面)
|
||||
*/
|
||||
const std::vector<HistoryEntry>& get_all() const { return entries_; }
|
||||
|
||||
/**
|
||||
* 获取历史记录数量
|
||||
*/
|
||||
size_t count() const { return entries_.size(); }
|
||||
|
||||
/**
|
||||
* 获取历史记录文件路径
|
||||
*/
|
||||
static std::string get_history_path();
|
||||
|
||||
private:
|
||||
std::vector<HistoryEntry> entries_;
|
||||
|
||||
// 确保配置目录存在
|
||||
static bool ensure_config_dir();
|
||||
};
|
||||
|
||||
} // namespace tut
|
||||
|
|
@ -209,6 +209,8 @@ public:
|
|||
}
|
||||
} else if (command == "bookmarks" || command == "bm" || command == "b") {
|
||||
result.action = Action::SHOW_BOOKMARKS;
|
||||
} else if (command == "history" || command == "hist" || command == "hi") {
|
||||
result.action = Action::SHOW_HISTORY;
|
||||
} else if (!command.empty() && std::isdigit(command[0])) {
|
||||
try {
|
||||
result.action = Action::GOTO_LINE;
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ enum class Action {
|
|||
GOTO_MARK, // Jump to mark (' + letter)
|
||||
ADD_BOOKMARK, // Add current page to bookmarks (B)
|
||||
REMOVE_BOOKMARK, // Remove current page from bookmarks (D)
|
||||
SHOW_BOOKMARKS // Show bookmarks page (:bookmarks)
|
||||
SHOW_BOOKMARKS, // Show bookmarks page (:bookmarks)
|
||||
SHOW_HISTORY // Show history page (:history)
|
||||
};
|
||||
|
||||
struct InputResult {
|
||||
|
|
|
|||
73
tests/test_history.cpp
Normal file
73
tests/test_history.cpp
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
#include "history.h"
|
||||
#include <iostream>
|
||||
#include <cstdio>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
|
||||
using namespace tut;
|
||||
|
||||
int main() {
|
||||
std::cout << "=== TUT 2.0 History Test ===" << std::endl;
|
||||
|
||||
// 记录初始状态
|
||||
HistoryManager manager;
|
||||
size_t initial_count = manager.count();
|
||||
std::cout << " Original history count: " << initial_count << std::endl;
|
||||
|
||||
// Test 1: 添加历史记录
|
||||
std::cout << "\n[Test 1] Add history entries..." << std::endl;
|
||||
manager.add("https://example.com", "Example Site");
|
||||
manager.add("https://test.com", "Test Site");
|
||||
manager.add("https://demo.com", "Demo Site");
|
||||
|
||||
if (manager.count() == initial_count + 3) {
|
||||
std::cout << " ✓ Added 3 entries" << std::endl;
|
||||
} else {
|
||||
std::cout << " ✗ Failed to add entries" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Test 2: 重复 URL 更新
|
||||
std::cout << "\n[Test 2] Duplicate URL update..." << std::endl;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
manager.add("https://example.com", "Example Site Updated");
|
||||
|
||||
// 计数应该不变(因为重复的会被移到前面而不是新增)
|
||||
if (manager.count() == initial_count + 3) {
|
||||
std::cout << " ✓ Duplicate correctly handled" << std::endl;
|
||||
} else {
|
||||
std::cout << " ✗ Duplicate handling failed" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Test 3: 最新在前面
|
||||
std::cout << "\n[Test 3] Most recent first..." << std::endl;
|
||||
const auto& entries = manager.get_all();
|
||||
if (!entries.empty() && entries[0].url == "https://example.com") {
|
||||
std::cout << " ✓ Most recent entry is first" << std::endl;
|
||||
} else {
|
||||
std::cout << " ✗ Order incorrect" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Test 4: 持久化
|
||||
std::cout << "\n[Test 4] Persistence..." << std::endl;
|
||||
{
|
||||
HistoryManager manager2; // 创建新实例会加载
|
||||
if (manager2.count() >= initial_count + 3) {
|
||||
std::cout << " ✓ History persisted to file" << std::endl;
|
||||
} else {
|
||||
std::cout << " ✗ Persistence failed" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup: 移除测试条目
|
||||
std::cout << "\n[Cleanup] Removing test entries..." << std::endl;
|
||||
HistoryManager cleanup_manager;
|
||||
// 由于我们没有删除单条的方法,这里只验证功能
|
||||
// 在实际使用中,历史会随着时间自然过期
|
||||
|
||||
std::cout << "\n=== All history tests passed! ===" << std::endl;
|
||||
return 0;
|
||||
}
|
||||
Loading…
Reference in a new issue