Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27fdb9eac3 | ||
|
|
b866d6eddd |
@@ -149,9 +149,9 @@ GET /api/v1/chatlog?time=2023-01-01&talker=wxid_xxx
|
|||||||
参数说明:
|
参数说明:
|
||||||
- `time`: 时间范围,格式为 `YYYY-MM-DD` 或 `YYYY-MM-DD~YYYY-MM-DD`
|
- `time`: 时间范围,格式为 `YYYY-MM-DD` 或 `YYYY-MM-DD~YYYY-MM-DD`
|
||||||
- `talker`: 聊天对象标识(支持 wxid、群聊 ID、备注名、昵称等)
|
- `talker`: 聊天对象标识(支持 wxid、群聊 ID、备注名、昵称等)
|
||||||
- `limit`: 返回记录数量(默认 100)
|
- `limit`: 返回记录数量
|
||||||
- `offset`: 分页偏移量(默认 0)
|
- `offset`: 分页偏移量
|
||||||
- `format`: 输出格式,支持 `json`、`csv` 或纯文本(默认 纯文本)
|
- `format`: 输出格式,支持 `json`、`csv` 或纯文本
|
||||||
|
|
||||||
### 其他 API 接口
|
### 其他 API 接口
|
||||||
|
|
||||||
|
|||||||
43
cmd/chatlog/cmd_server.go
Normal file
43
cmd/chatlog/cmd_server.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package chatlog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/sjzar/chatlog/internal/chatlog"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(serverCmd)
|
||||||
|
serverCmd.Flags().StringVarP(&serverAddr, "addr", "a", "127.0.0.1:5030", "server address")
|
||||||
|
serverCmd.Flags().StringVarP(&serverDataDir, "data-dir", "d", "", "data dir")
|
||||||
|
serverCmd.Flags().StringVarP(&serverWorkDir, "work-dir", "w", "", "work dir")
|
||||||
|
serverCmd.Flags().StringVarP(&serverPlatform, "platform", "p", runtime.GOOS, "platform")
|
||||||
|
serverCmd.Flags().IntVarP(&serverVer, "version", "v", 3, "version")
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
serverAddr string
|
||||||
|
serverDataDir string
|
||||||
|
serverWorkDir string
|
||||||
|
serverPlatform string
|
||||||
|
serverVer int
|
||||||
|
)
|
||||||
|
|
||||||
|
var serverCmd = &cobra.Command{
|
||||||
|
Use: "server",
|
||||||
|
Short: "Start HTTP server",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
m, err := chatlog.New("")
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Msg("failed to create chatlog instance")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := m.CommandHTTPServer(serverAddr, serverDataDir, serverWorkDir, serverPlatform, serverVer); err != nil {
|
||||||
|
log.Err(err).Msg("failed to start server")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,156 +1,733 @@
|
|||||||
|
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Chatlog</title>
|
<title>Chatlog</title>
|
||||||
<style>
|
<style>
|
||||||
.random-paragraph {
|
:root {
|
||||||
display: none;
|
--primary-color: #3498db;
|
||||||
|
--primary-dark: #2980b9;
|
||||||
|
--success-color: #2ecc71;
|
||||||
|
--success-dark: #27ae60;
|
||||||
|
--error-color: #e74c3c;
|
||||||
|
--bg-light: #f5f5f5;
|
||||||
|
--bg-white: #ffffff;
|
||||||
|
--text-color: #333333;
|
||||||
|
--border-color: #dddddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||||
|
Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--text-color);
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-text {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-section {
|
||||||
|
background-color: var(--bg-light);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 25px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 850px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: var(--primary-color);
|
||||||
|
margin-top: 20px;
|
||||||
|
border-bottom: 2px solid var(--primary-color);
|
||||||
|
padding-bottom: 8px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-top: 20px;
|
||||||
|
color: #34495e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-link {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: var(--primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-tester {
|
||||||
|
background-color: var(--bg-white);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 25px;
|
||||||
|
margin-top: 20px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #34495e;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus,
|
||||||
|
select:focus,
|
||||||
|
textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder,
|
||||||
|
textarea::placeholder {
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 18px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: var(--primary-dark);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo,
|
||||||
|
monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-url {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo,
|
||||||
|
monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
word-break: break-all;
|
||||||
|
border: 1px dashed #ccc;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-text {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-url-button {
|
||||||
|
background-color: #9b59b6;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading::after {
|
||||||
|
content: "...";
|
||||||
|
animation: dots 1.5s steps(5, end) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dots {
|
||||||
|
0%,
|
||||||
|
20% {
|
||||||
|
content: ".";
|
||||||
}
|
}
|
||||||
|
40% {
|
||||||
|
content: "..";
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
content: "...";
|
||||||
|
}
|
||||||
|
80%,
|
||||||
|
100% {
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-container {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: 12px 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 5px;
|
||||||
|
border-radius: 6px 6px 0 0;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-bottom: none;
|
||||||
|
position: relative;
|
||||||
|
bottom: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:hover {
|
||||||
|
background-color: #f0f8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
background-color: var(--bg-white);
|
||||||
|
border-color: #e0e0e0;
|
||||||
|
color: var(--primary-color);
|
||||||
|
border-bottom: 1px solid white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
display: none;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content.active {
|
||||||
|
display: block;
|
||||||
|
animation: fadeIn 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-button {
|
||||||
|
background-color: var(--success-color);
|
||||||
|
padding: 8px 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-button:hover {
|
||||||
|
background-color: var(--success-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: var(--error-color);
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: rgba(231, 76, 60, 0.1);
|
||||||
|
border-left: 4px solid var(--error-color);
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-description {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-left: 8px;
|
||||||
|
background-color: rgba(52, 152, 219, 0.1);
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.optional-param {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #777;
|
||||||
|
margin-left: 5px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required-field {
|
||||||
|
color: var(--error-color);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="paragraphContainer">
|
<div class="container">
|
||||||
<pre style="float:left;white-space:pre-wrap;" class="random-paragraph">
|
<div class="welcome-text">
|
||||||
('-. .-. ('-. .-') _
|
<h1>🎉 恭喜!Chatlog 服务已成功启动</h1>
|
||||||
( OO ) / ( OO ).-. ( OO) )
|
<p>
|
||||||
.-----. ,--. ,--. / . --. / / '._ ,--. .-'),-----. ,----.
|
Chatlog 是一个帮助你轻松使用自己聊天数据的工具,现在你可以通过 HTTP
|
||||||
' .--./ | | | | | \-. \ |'--...__) | |.-') ( OO' .-. ' ' .-./-')
|
API 访问你的聊天记录、联系人和群聊信息。
|
||||||
| |('-. | .| | .-'-' | | '--. .--' | | OO ) / | | | | | |_( O- )
|
</p>
|
||||||
/_) |OO ) | | \| |_.' | | | | |`-' | \_) | |\| | | | .--, \
|
</div>
|
||||||
|| |`-'| | .-. | | .-. | | | (| '---.' \ | | | |(| | '. (_/
|
|
||||||
(_' '--'\ | | | | | | | | | | | | `' '-' ' | '--' |
|
<div class="api-section">
|
||||||
`-----' `--' `--' `--' `--' `--' `------' `-----' `------'
|
<h2>🔍 API 接口与调试</h2>
|
||||||
</pre>
|
|
||||||
<pre style="float:left;white-space:pre-wrap;" class="random-paragraph">
|
<div class="api-tester">
|
||||||
_____ _ _ _
|
<div class="tab-container">
|
||||||
/ __ \| | | | | |
|
<div class="tab active" data-tab="session">最近会话</div>
|
||||||
| / \/| |__ __ _ | |_ | | ___ __ _
|
<div class="tab" data-tab="chatroom">群聊</div>
|
||||||
| | | '_ \ / _` || __|| | / _ \ / _` |
|
<div class="tab" data-tab="contact">联系人</div>
|
||||||
| \__/\| | | || (_| || |_ | || (_) || (_| |
|
<div class="tab" data-tab="chatlog">聊天记录</div>
|
||||||
\____/|_| |_| \__,_| \__||_| \___/ \__, |
|
</div>
|
||||||
__/ |
|
|
||||||
|___/
|
<!-- 会话查询表单 -->
|
||||||
</pre>
|
<div class="tab-content active" id="session-tab">
|
||||||
<pre style="float:left;white-space:pre-wrap;" class="random-paragraph">
|
<div class="api-description">
|
||||||
|
<p>
|
||||||
,-----. ,--. ,--. ,--.
|
查询最近会话列表。<span class="badge">GET /api/v1/session</span>
|
||||||
' .--./ | ,---. ,--,--. ,-' '-. | | ,---. ,---.
|
</p>
|
||||||
| | | .-. | ' ,-. | '-. .-' | | | .-. | | .-. |
|
</div>
|
||||||
' '--'\ | | | | \ '-' | | | | | ' '-' ' ' '-' '
|
<div class="form-group">
|
||||||
`-----' `--' `--' `--`--' `--' `--' `---' .`- /
|
<label for="session-format"
|
||||||
`---'
|
>输出格式:<span class="optional-param">可选</span></label
|
||||||
</pre>
|
>
|
||||||
<pre style="float:left;white-space:pre-wrap;" class="random-paragraph">
|
<select id="session-format">
|
||||||
____ _ _ _
|
<option value="">默认</option>
|
||||||
/ ___| | |__ __ _ | |_ | | ___ __ _
|
<option value="json">JSON</option>
|
||||||
| | | '_ \ / _` | | __| | | / _ \ / _` |
|
<option value="text">纯文本</option>
|
||||||
| |___ | | | | | (_| | | |_ | | | (_) | | (_| |
|
</select>
|
||||||
\____| |_| |_| \__,_| \__| |_| \___/ \__, |
|
</div>
|
||||||
|___/
|
</div>
|
||||||
</pre>
|
|
||||||
<pre style="float:left;white-space:pre-wrap;" class="random-paragraph">
|
<!-- 群聊查询表单 -->
|
||||||
_____ _____ _____ _____ _____ _______ _____
|
<div class="tab-content" id="chatroom-tab">
|
||||||
/\ \ /\ \ /\ \ /\ \ /\ \ /::\ \ /\ \
|
<div class="api-description">
|
||||||
/::\ \ /::\____\ /::\ \ /::\ \ /::\____\ /::::\ \ /::\ \
|
<p>
|
||||||
/::::\ \ /:::/ / /::::\ \ \:::\ \ /:::/ / /::::::\ \ /::::\ \
|
查询群聊列表,可选择性地按关键词搜索。<span class="badge"
|
||||||
/::::::\ \ /:::/ / /::::::\ \ \:::\ \ /:::/ / /::::::::\ \ /::::::\ \
|
>GET /api/v1/chatroom</span
|
||||||
/:::/\:::\ \ /:::/ / /:::/\:::\ \ \:::\ \ /:::/ / /:::/~~\:::\ \ /:::/\:::\ \
|
>
|
||||||
/:::/ \:::\ \ /:::/____/ /:::/__\:::\ \ \:::\ \ /:::/ / /:::/ \:::\ \ /:::/ \:::\ \
|
</p>
|
||||||
/:::/ \:::\ \ /::::\ \ /::::\ \:::\ \ /::::\ \ /:::/ / /:::/ / \:::\ \ /:::/ \:::\ \
|
</div>
|
||||||
/:::/ / \:::\ \ /::::::\ \ _____ /::::::\ \:::\ \ /::::::\ \ /:::/ / /:::/____/ \:::\____\ /:::/ / \:::\ \
|
<div class="form-group">
|
||||||
/:::/ / \:::\ \ /:::/\:::\ \ /\ \ /:::/\:::\ \:::\ \ /:::/\:::\ \ /:::/ / |:::| | |:::| | /:::/ / \:::\ ___\
|
<label for="chatroom-query"
|
||||||
/:::/____/ \:::\____\/:::/ \:::\ /::\____\/:::/ \:::\ \:::\____\ /:::/ \:::\____\/:::/____/ |:::|____| |:::| |/:::/____/ ___\:::| |
|
>搜索群聊:<span class="optional-param">可选</span></label
|
||||||
\:::\ \ \::/ /\::/ \:::\ /:::/ /\::/ \:::\ /:::/ / /:::/ \::/ /\:::\ \ \:::\ \ /:::/ / \:::\ \ /\ /:::|____|
|
>
|
||||||
\:::\ \ \/____/ \/____/ \:::\/:::/ / \/____/ \:::\/:::/ / /:::/ / \/____/ \:::\ \ \:::\ \ /:::/ / \:::\ /::\ \::/ /
|
<input
|
||||||
\:::\ \ \::::::/ / \::::::/ / /:::/ / \:::\ \ \:::\ /:::/ / \:::\ \:::\ \/____/
|
type="text"
|
||||||
\:::\ \ \::::/ / \::::/ / /:::/ / \:::\ \ \:::\__/:::/ / \:::\ \:::\____\
|
id="chatroom-query"
|
||||||
\:::\ \ /:::/ / /:::/ / \::/ / \:::\ \ \::::::::/ / \:::\ /:::/ /
|
placeholder="输入关键词搜索群聊"
|
||||||
\:::\ \ /:::/ / /:::/ / \/____/ \:::\ \ \::::::/ / \:::\/:::/ /
|
/>
|
||||||
\:::\ \ /:::/ / /:::/ / \:::\ \ \::::/ / \::::::/ /
|
</div>
|
||||||
\:::\____\ /:::/ / /:::/ / \:::\____\ \::/____/ \::::/ /
|
<div class="form-group">
|
||||||
\::/ / \::/ / \::/ / \::/ / ~~ \::/____/
|
<label for="chatroom-format"
|
||||||
\/____/ \/____/ \/____/ \/____/
|
>输出格式:<span class="optional-param">可选</span></label
|
||||||
|
>
|
||||||
</pre>
|
<select id="chatroom-format">
|
||||||
<pre style="float:left;white-space:pre-wrap;" class="random-paragraph">
|
<option value="">默认</option>
|
||||||
___ _ _ __ ____ __ _____ ___
|
<option value="json">JSON</option>
|
||||||
/ __)( )_( ) /__\ (_ _)( ) ( _ ) / __)
|
<option value="text">纯文本</option>
|
||||||
( (__ ) _ ( /(__)\ )( )(__ )(_)( ( (_-.
|
</select>
|
||||||
\___)(_) (_)(__)(__) (__) (____)(_____) \___/
|
</div>
|
||||||
</pre>
|
</div>
|
||||||
<pre style="float:left;white-space:pre-wrap;" class="random-paragraph">
|
|
||||||
________ ___ ___ ________ _________ ___ ________ ________
|
<!-- 联系人查询表单 -->
|
||||||
|\ ____\ |\ \|\ \ |\ __ \ |\___ ___\ |\ \ |\ __ \ |\ ____\
|
<div class="tab-content" id="contact-tab">
|
||||||
\ \ \___| \ \ \\\ \ \ \ \|\ \ \|___ \ \_| \ \ \ \ \ \|\ \ \ \ \___|
|
<div class="api-description">
|
||||||
\ \ \ \ \ __ \ \ \ __ \ \ \ \ \ \ \ \ \ \\\ \ \ \ \ ___
|
<p>
|
||||||
\ \ \____ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \____ \ \ \\\ \ \ \ \|\ \
|
查询联系人列表,可选择性地按关键词搜索。<span class="badge"
|
||||||
\ \_______\ \ \__\ \__\ \ \__\ \__\ \ \__\ \ \_______\ \ \_______\ \ \_______\
|
>GET /api/v1/contact</span
|
||||||
\|_______| \|__|\|__| \|__|\|__| \|__| \|_______| \|_______| \|_______|
|
>
|
||||||
</pre>
|
</p>
|
||||||
<pre style="float:left;white-space:pre-wrap;" class="random-paragraph">
|
</div>
|
||||||
╔═╗┬ ┬┌─┐┌┬┐┬ ┌─┐┌─┐
|
<div class="form-group">
|
||||||
║ ├─┤├─┤ │ │ │ ││ ┬
|
<label for="contact-query"
|
||||||
╚═╝┴ ┴┴ ┴ ┴ ┴─┘└─┘└─┘
|
>搜索联系人:<span class="optional-param">可选</span></label
|
||||||
</pre>
|
>
|
||||||
<pre style="float:left;white-space:pre-wrap;" class="random-paragraph">
|
<input
|
||||||
▄▄· ▄ .▄ ▄▄▄· ▄▄▄▄▄▄▄▌ ▄▄ •
|
type="text"
|
||||||
▐█ ▌▪██▪▐█▐█ ▀█ •██ ██• ▪ ▐█ ▀ ▪
|
id="contact-query"
|
||||||
██ ▄▄██▀▐█▄█▀▀█ ▐█.▪██▪ ▄█▀▄ ▄█ ▀█▄
|
placeholder="输入关键词搜索联系人"
|
||||||
▐███▌██▌▐▀▐█ ▪▐▌ ▐█▌·▐█▌▐▌▐█▌.▐▌▐█▄▪▐█
|
/>
|
||||||
·▀▀▀ ▀▀▀ · ▀ ▀ ▀▀▀ .▀▀▀ ▀█▄▀▪·▀▀▀▀
|
</div>
|
||||||
</pre>
|
<div class="form-group">
|
||||||
<pre style="float:left;white-space:pre-wrap;" class="random-paragraph">
|
<label for="contact-format"
|
||||||
,. - ., .·¨'`; ,.·´¨;\ ,., ' , . ., ° ,. ' , ·. ,.-·~·., ‘ ,.-·^*ª'` ·,
|
>输出格式:<span class="optional-param">可选</span></label
|
||||||
,·'´ ,. - , ';\ '; ;'\ '; ;::\ ;´ '· ., ;'´ , ., _';\' / ';\ / ·'´,.-·-., `,'‚ .·´ ,·'´:¯'`·, '\‘
|
>
|
||||||
,·´ .'´\:::::;' ;:'\ ' ; ;::'\ ,' ;::'; .´ .-, ';\ \:´¨¯:;' `;::'\:'\ ,' ,'::'\ / .'´\:::::::'\ '\ ° ,´ ,'\:::::::::\,.·\'
|
<select id="contact-format">
|
||||||
/ ,'´::::'\;:-/ ,' ::; ' ; ;::_';,. ,.' ;:::';° / /:\:'; ;:'\' \::::; ,'::_'\;' ,' ;:::';' ,·' ,'::::\:;:-·-:'; ';\‚ / /:::\;·'´¯'`·;\:::\°
|
<option value="">默认</option>
|
||||||
,' ;':::::;'´ '; /\::;' ' .' ,. -·~-·, ;:::'; ' ,' ,'::::'\'; ;::'; ,' ,'::;' ‘ '; ,':::;' ;. ';:::;´ ,' ,':'\‚ ; ;:::;' '\;:·´
|
<option value="json">JSON</option>
|
||||||
; ;:::::; '\*'´\::\' ° '; ;'\::::::::; '/::::; ,.-·' '·~^*'´¨, ';::; ; ;:::; ° ; ,':::;' ' '; ;::; ,'´ .'´\::';‚ '; ;::/ ,·´¯'; °
|
<option value="text">纯文本</option>
|
||||||
'; ';::::'; '\::'\/.' ; ';:;\;::-··; ;::::; ':, ,·:²*´¨¯'`; ;::'; ; ;::;' ‘ ,' ,'::;' '; ':;: ,.·´,.·´::::\;'° '; '·;' ,.·´, ;'\
|
</select>
|
||||||
\ '·:;:'_ ,. -·'´.·´\‘ ':,.·´\;' ;' ,' :::/ ' ,' / \::::::::'; ;::'; ; ;::;'‚ ; ';_:,.-·´';\‘ \·, `*´,.·'´::::::;·´ \'·. `'´,.·:´'; ;::\'
|
</div>
|
||||||
'\:` · .,. -·:´::::::\' \:::::\ \·.'::::; ,' ,'::::\·²*'´¨¯':,'\:; ',.'\::;'‚ ', _,.-·'´:\:\‘ \\:¯::\:::::::;:·´ '\::\¯::::::::'; ;::'; ‘
|
</div>
|
||||||
\:::::::\:::::::;:·'´' \;:·´ \:\::'; \`¨\:::/ \::\' \::\:;'‚ \¨:::::::::::\'; `\:::::\;::·'´ ° `·:\:::;:·´';.·´\::;'
|
|
||||||
`· :;::\;::-·´ `·\;' '\::\;' '\;' ' \;:' ‘ '\;::_;:-·'´‘ ¯ ¯ \::::\;'‚
|
<!-- 聊天记录查询表单 -->
|
||||||
' `¨' ° '¨ ‘ '\:·´'
|
<div class="tab-content" id="chatlog-tab">
|
||||||
</pre>
|
<div class="api-description">
|
||||||
<pre style="float:left;white-space:pre-wrap;" class="random-paragraph">
|
<p>
|
||||||
▄████▄ ██░ ██ ▄▄▄ ▄▄▄█████▓ ██▓ ▒█████ ▄████
|
查询指定时间范围内与特定联系人或群聊的聊天记录。<span
|
||||||
▒██▀ ▀█ ▓██░ ██▒▒████▄ ▓ ██▒ ▓▒▓██▒ ▒██▒ ██▒ ██▒ ▀█▒
|
class="badge"
|
||||||
▒▓█ ▄ ▒██▀▀██░▒██ ▀█▄ ▒ ▓██░ ▒░▒██░ ▒██░ ██▒▒██░▄▄▄░
|
>GET /api/v1/chatlog</span
|
||||||
▒▓▓▄ ▄██▒░▓█ ░██ ░██▄▄▄▄██ ░ ▓██▓ ░ ▒██░ ▒██ ██░░▓█ ██▓
|
>
|
||||||
▒ ▓███▀ ░░▓█▒░██▓ ▓█ ▓██▒ ▒██▒ ░ ░██████▒░ ████▓▒░░▒▓███▀▒
|
</p>
|
||||||
░ ░▒ ▒ ░ ▒ ░░▒░▒ ▒▒ ▓▒█░ ▒ ░░ ░ ▒░▓ ░░ ▒░▒░▒░ ░▒ ▒
|
</div>
|
||||||
░ ▒ ▒ ░▒░ ░ ▒ ▒▒ ░ ░ ░ ░ ▒ ░ ░ ▒ ▒░ ░ ░
|
<div class="form-group">
|
||||||
░ ░ ░░ ░ ░ ▒ ░ ░ ░ ░ ░ ░ ▒ ░ ░ ░
|
<label for="time"
|
||||||
░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
|
>时间范围:<span class="required-field">*</span></label
|
||||||
░
|
>
|
||||||
</pre>
|
<input
|
||||||
<pre style="float:left;white-space:pre-wrap;" class="random-paragraph">
|
type="text"
|
||||||
▄█▄ ▄ █ ██ ▄▄▄▄▀ █ ████▄ ▄▀
|
id="time"
|
||||||
█▀ ▀▄ █ █ █ █ ▀▀▀ █ █ █ █ ▄▀
|
placeholder="例如:2023-01-01 或 2023-01-01~2023-01-31"
|
||||||
█ ▀ ██▀▀█ █▄▄█ █ █ █ █ █ ▀▄
|
/>
|
||||||
█▄ ▄▀ █ █ █ █ █ ███▄ ▀████ █ █
|
</div>
|
||||||
▀███▀ █ █ ▀ ▀ ███
|
<div class="form-group">
|
||||||
▀ █
|
<label for="talker"
|
||||||
▀
|
>聊天对象:<span class="required-field">*</span></label
|
||||||
</pre>
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="talker"
|
||||||
|
placeholder="wxid、群ID、备注名或昵称"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="limit"
|
||||||
|
>返回数量:<span class="optional-param">可选</span></label
|
||||||
|
>
|
||||||
|
<input type="number" id="limit" placeholder="默认不做限制" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="offset"
|
||||||
|
>偏移量:<span class="optional-param">可选</span></label
|
||||||
|
>
|
||||||
|
<input type="number" id="offset" placeholder="默认 0" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="format"
|
||||||
|
>输出格式:<span class="optional-param">可选</span></label
|
||||||
|
>
|
||||||
|
<select id="format">
|
||||||
|
<option value="">默认</option>
|
||||||
|
<option value="text">纯文本</option>
|
||||||
|
<option value="json">JSON</option>
|
||||||
|
<option value="csv">CSV</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="test-api">执行查询</button>
|
||||||
|
|
||||||
|
<div id="result-wrapper" style="display: none; margin-top: 20px">
|
||||||
|
<div class="request-url" id="request-url-container">
|
||||||
|
<span class="url-text" id="request-url"></span>
|
||||||
|
<button class="copy-button copy-url-button" id="copy-url">
|
||||||
|
复制请求URL
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="result-container" id="api-result">
|
||||||
|
<p>查询结果将显示在这里...</p>
|
||||||
|
</div>
|
||||||
|
<div class="button-group">
|
||||||
|
<button class="copy-button" id="copy-result">复制结果</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="error-message" id="error-message"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="api-section">
|
||||||
|
<h2>🤖 MCP 集成</h2>
|
||||||
|
<p>
|
||||||
|
Chatlog 支持 MCP (Model Context Protocol) SSE 协议,可与支持 MCP 的 AI
|
||||||
|
助手无缝集成。
|
||||||
|
</p>
|
||||||
|
<p>SSE 端点:<strong>/sse</strong></p>
|
||||||
|
<p>
|
||||||
|
详细集成指南请参考
|
||||||
|
<a
|
||||||
|
href="https://github.com/sjzar/chatlog/blob/main/docs/mcp.md"
|
||||||
|
class="docs-link"
|
||||||
|
target="_blank"
|
||||||
|
>MCP 集成指南</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="api-section">
|
||||||
|
<h2>📚 更多资源</h2>
|
||||||
|
<p>
|
||||||
|
查看
|
||||||
|
<a
|
||||||
|
href="https://github.com/sjzar/chatlog"
|
||||||
|
class="docs-link"
|
||||||
|
target="_blank"
|
||||||
|
>GitHub 项目</a
|
||||||
|
>
|
||||||
|
获取完整文档和使用指南。
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
如果你有任何问题或建议,欢迎通过
|
||||||
|
<a
|
||||||
|
href="https://github.com/sjzar/chatlog/discussions"
|
||||||
|
class="docs-link"
|
||||||
|
target="_blank"
|
||||||
|
>Discussions</a
|
||||||
|
>
|
||||||
|
进行交流。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.onload = function() {
|
// 标签切换功能
|
||||||
showRandomParagraph();
|
document.querySelectorAll(".tab").forEach((tab) => {
|
||||||
};
|
tab.addEventListener("click", function () {
|
||||||
function showRandomParagraph() {
|
// 移除所有标签的活动状态
|
||||||
const paragraphs = document.getElementsByClassName("random-paragraph");
|
document
|
||||||
for (let i = 0; i < paragraphs.length; i++) {
|
.querySelectorAll(".tab")
|
||||||
paragraphs[i].style.display = "none";
|
.forEach((t) => t.classList.remove("active"));
|
||||||
|
// 设置当前标签为活动状态
|
||||||
|
this.classList.add("active");
|
||||||
|
|
||||||
|
// 隐藏所有内容区域
|
||||||
|
document.querySelectorAll(".tab-content").forEach((content) => {
|
||||||
|
content.classList.remove("active");
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示当前标签对应的内容
|
||||||
|
const tabId = this.getAttribute("data-tab") + "-tab";
|
||||||
|
document.getElementById(tabId).classList.add("active");
|
||||||
|
|
||||||
|
// 清空结果区域
|
||||||
|
document.getElementById("result-wrapper").style.display = "none";
|
||||||
|
document.getElementById("api-result").innerHTML =
|
||||||
|
"<p>查询结果将显示在这里...</p>";
|
||||||
|
document.getElementById("request-url").textContent = "";
|
||||||
|
document.getElementById("error-message").style.display = "none";
|
||||||
|
document.getElementById("error-message").textContent = "";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// API 测试功能
|
||||||
|
document
|
||||||
|
.getElementById("test-api")
|
||||||
|
.addEventListener("click", async function () {
|
||||||
|
const resultContainer = document.getElementById("api-result");
|
||||||
|
const requestUrlContainer = document.getElementById("request-url");
|
||||||
|
const errorMessage = document.getElementById("error-message");
|
||||||
|
const resultWrapper = document.getElementById("result-wrapper");
|
||||||
|
|
||||||
|
errorMessage.style.display = "none";
|
||||||
|
errorMessage.textContent = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取当前活动的标签
|
||||||
|
const activeTab = document
|
||||||
|
.querySelector(".tab.active")
|
||||||
|
.getAttribute("data-tab");
|
||||||
|
let url = "/api/v1/";
|
||||||
|
let params = new URLSearchParams();
|
||||||
|
|
||||||
|
// 根据不同的标签构建不同的请求
|
||||||
|
switch (activeTab) {
|
||||||
|
case "chatlog":
|
||||||
|
url += "chatlog";
|
||||||
|
const time = document.getElementById("time").value;
|
||||||
|
const talker = document.getElementById("talker").value;
|
||||||
|
const limit = document.getElementById("limit").value;
|
||||||
|
const offset = document.getElementById("offset").value;
|
||||||
|
const format = document.getElementById("format").value;
|
||||||
|
|
||||||
|
// 验证必填项
|
||||||
|
if (!time || !talker) {
|
||||||
|
errorMessage.textContent =
|
||||||
|
"错误: 时间范围和聊天对象为必填项!";
|
||||||
|
errorMessage.style.display = "block";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (time) params.append("time", time);
|
||||||
|
if (talker) params.append("talker", talker);
|
||||||
|
if (limit) params.append("limit", limit);
|
||||||
|
if (offset) params.append("offset", offset);
|
||||||
|
if (format) params.append("format", format);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "contact":
|
||||||
|
url += "contact";
|
||||||
|
const contactQuery =
|
||||||
|
document.getElementById("contact-query").value;
|
||||||
|
const contactFormat =
|
||||||
|
document.getElementById("contact-format").value;
|
||||||
|
|
||||||
|
if (contactQuery) params.append("query", contactQuery);
|
||||||
|
if (contactFormat) params.append("format", contactFormat);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "chatroom":
|
||||||
|
url += "chatroom";
|
||||||
|
const chatroomQuery =
|
||||||
|
document.getElementById("chatroom-query").value;
|
||||||
|
const chatroomFormat =
|
||||||
|
document.getElementById("chatroom-format").value;
|
||||||
|
|
||||||
|
if (chatroomQuery) params.append("query", chatroomQuery);
|
||||||
|
if (chatroomFormat) params.append("format", chatroomFormat);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "session":
|
||||||
|
url += "session";
|
||||||
|
const sessionFormat =
|
||||||
|
document.getElementById("session-format").value;
|
||||||
|
|
||||||
|
if (sessionFormat) params.append("format", sessionFormat);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
const randomIndex = Math.floor(Math.random() * paragraphs.length);
|
|
||||||
paragraphs[randomIndex].style.display = "block";
|
// 添加参数到URL
|
||||||
}
|
const apiUrl = params.toString()
|
||||||
|
? `${url}?${params.toString()}`
|
||||||
|
: url;
|
||||||
|
|
||||||
|
// 获取完整URL(包含域名部分)
|
||||||
|
const fullUrl = window.location.origin + apiUrl;
|
||||||
|
|
||||||
|
// 显示完整请求URL
|
||||||
|
requestUrlContainer.textContent = fullUrl;
|
||||||
|
resultWrapper.style.display = "block";
|
||||||
|
|
||||||
|
// 显示加载中
|
||||||
|
resultContainer.innerHTML = '<div class="loading">加载中</div>';
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
const response = await fetch(apiUrl);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取响应内容类型
|
||||||
|
const contentType = response.headers.get("content-type");
|
||||||
|
let result;
|
||||||
|
|
||||||
|
if (contentType && contentType.includes("application/json")) {
|
||||||
|
// 如果是JSON,格式化显示
|
||||||
|
result = await response.json();
|
||||||
|
resultContainer.innerHTML = JSON.stringify(result, null, 2);
|
||||||
|
} else {
|
||||||
|
// 其他格式直接显示文本
|
||||||
|
result = await response.text();
|
||||||
|
resultContainer.innerHTML = result;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
resultContainer.innerHTML = "";
|
||||||
|
errorMessage.textContent = `查询出错: ${error.message}`;
|
||||||
|
errorMessage.style.display = "block";
|
||||||
|
console.error("API查询出错:", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 复制结果功能
|
||||||
|
document
|
||||||
|
.getElementById("copy-result")
|
||||||
|
.addEventListener("click", function () {
|
||||||
|
const resultText = document.getElementById("api-result").innerText;
|
||||||
|
copyToClipboard(resultText, this, "已复制结果!");
|
||||||
|
});
|
||||||
|
|
||||||
|
// 复制URL功能
|
||||||
|
document
|
||||||
|
.getElementById("copy-url")
|
||||||
|
.addEventListener("click", function () {
|
||||||
|
// 获取完整URL(包含域名部分)
|
||||||
|
const urlText = document.getElementById("request-url").innerText;
|
||||||
|
copyToClipboard(urlText, this, "已复制URL!");
|
||||||
|
});
|
||||||
|
|
||||||
|
// 通用复制功能
|
||||||
|
function copyToClipboard(text, button, successMessage) {
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(text)
|
||||||
|
.then(() => {
|
||||||
|
const originalText = button.textContent;
|
||||||
|
button.textContent = successMessage;
|
||||||
|
setTimeout(() => {
|
||||||
|
button.textContent = originalText;
|
||||||
|
}, 2000);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("复制失败:", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const (
|
|||||||
var V3KeyPatterns = []KeyPatternInfo{
|
var V3KeyPatterns = []KeyPatternInfo{
|
||||||
{
|
{
|
||||||
Pattern: []byte{0x72, 0x74, 0x72, 0x65, 0x65, 0x5f, 0x69, 0x33, 0x32},
|
Pattern: []byte{0x72, 0x74, 0x72, 0x65, 0x65, 0x5f, 0x69, 0x33, 0x32},
|
||||||
Offset: 24,
|
Offsets: []int{24},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,16 +122,73 @@ func (e *V3Extractor) findMemory(ctx context.Context, pid uint32, memoryChannel
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msgf("Read memory region, size: %d bytes", len(memory))
|
totalSize := len(memory)
|
||||||
|
log.Debug().Msgf("Read memory region, size: %d bytes", totalSize)
|
||||||
|
|
||||||
// Send memory data to channel for processing
|
// If memory is small enough, process it as a single chunk
|
||||||
select {
|
if totalSize <= MinChunkSize {
|
||||||
case memoryChannel <- memory:
|
select {
|
||||||
log.Debug().Msg("Memory region sent for analysis")
|
case memoryChannel <- memory:
|
||||||
case <-ctx.Done():
|
log.Debug().Msg("Memory sent as a single chunk for analysis")
|
||||||
return ctx.Err()
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
chunkCount := MaxWorkers * ChunkMultiplier
|
||||||
|
|
||||||
|
// Calculate chunk size based on fixed chunk count
|
||||||
|
chunkSize := totalSize / chunkCount
|
||||||
|
if chunkSize < MinChunkSize {
|
||||||
|
// Reduce number of chunks if each would be too small
|
||||||
|
chunkCount = totalSize / MinChunkSize
|
||||||
|
if chunkCount == 0 {
|
||||||
|
chunkCount = 1
|
||||||
|
}
|
||||||
|
chunkSize = totalSize / chunkCount
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process memory in chunks from end to beginning
|
||||||
|
for i := chunkCount - 1; i >= 0; i-- {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
// Calculate start and end positions for this chunk
|
||||||
|
start := i * chunkSize
|
||||||
|
end := (i + 1) * chunkSize
|
||||||
|
|
||||||
|
// Ensure the last chunk includes all remaining memory
|
||||||
|
if i == chunkCount-1 {
|
||||||
|
end = totalSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add overlap area to catch patterns at chunk boundaries
|
||||||
|
if i > 0 {
|
||||||
|
start -= ChunkOverlapBytes
|
||||||
|
if start < 0 {
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk := memory[start:end]
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Int("chunk_index", i+1).
|
||||||
|
Int("total_chunks", chunkCount).
|
||||||
|
Int("chunk_size", len(chunk)).
|
||||||
|
Int("start_offset", start).
|
||||||
|
Int("end_offset", end).
|
||||||
|
Msg("Processing memory chunk")
|
||||||
|
|
||||||
|
select {
|
||||||
|
case memoryChannel <- chunk:
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,24 +230,26 @@ func (e *V3Extractor) SearchKey(ctx context.Context, memory []byte) (string, boo
|
|||||||
break // No more matches found
|
break // No more matches found
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we have enough space for the key
|
// Try each offset for this pattern
|
||||||
keyOffset := index + keyPattern.Offset
|
for _, offset := range keyPattern.Offsets {
|
||||||
if keyOffset < 0 || keyOffset+32 > len(memory) {
|
// Check if we have enough space for the key
|
||||||
index -= 1
|
keyOffset := index + offset
|
||||||
continue
|
if keyOffset < 0 || keyOffset+32 > len(memory) {
|
||||||
}
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Extract the key data, which is 32 bytes long
|
// Extract the key data, which is at the offset position and 32 bytes long
|
||||||
keyData := memory[keyOffset : keyOffset+32]
|
keyData := memory[keyOffset : keyOffset+32]
|
||||||
|
|
||||||
// Validate key against database header
|
// Validate key against database header
|
||||||
if e.validator.Validate(keyData) {
|
if e.validator.Validate(keyData) {
|
||||||
log.Debug().
|
log.Debug().
|
||||||
Str("pattern", hex.EncodeToString(keyPattern.Pattern)).
|
Str("pattern", hex.EncodeToString(keyPattern.Pattern)).
|
||||||
Int("offset", keyPattern.Offset).
|
Int("offset", offset).
|
||||||
Str("key", hex.EncodeToString(keyData)).
|
Str("key", hex.EncodeToString(keyData)).
|
||||||
Msg("Key found")
|
Msg("Key found")
|
||||||
return hex.EncodeToString(keyData), true
|
return hex.EncodeToString(keyData), true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
index -= 1
|
index -= 1
|
||||||
|
|||||||
@@ -16,17 +16,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MaxWorkers = 8
|
MaxWorkers = 8
|
||||||
|
MinChunkSize = 1 * 1024 * 1024 // 1MB
|
||||||
|
ChunkOverlapBytes = 1024 // Greater than all offsets
|
||||||
|
ChunkMultiplier = 2 // Number of chunks = MaxWorkers * ChunkMultiplier
|
||||||
)
|
)
|
||||||
|
|
||||||
var V4KeyPatterns = []KeyPatternInfo{
|
var V4KeyPatterns = []KeyPatternInfo{
|
||||||
{
|
{
|
||||||
Pattern: []byte{0x20, 0x66, 0x74, 0x73, 0x35, 0x28, 0x25, 0x00},
|
Pattern: []byte{0x20, 0x66, 0x74, 0x73, 0x35, 0x28, 0x25, 0x00},
|
||||||
Offset: 16,
|
Offsets: []int{16, -80, 64},
|
||||||
},
|
|
||||||
{
|
|
||||||
Pattern: []byte{0x20, 0x66, 0x74, 0x73, 0x35, 0x28, 0x25, 0x00},
|
|
||||||
Offset: -80,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,14 +125,72 @@ func (e *V4Extractor) findMemory(ctx context.Context, pid uint32, memoryChannel
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msgf("Read memory region, size: %d bytes", len(memory))
|
totalSize := len(memory)
|
||||||
|
log.Debug().Msgf("Read memory region, size: %d bytes", totalSize)
|
||||||
|
|
||||||
// Send memory data to channel for processing
|
// If memory is small enough, process it as a single chunk
|
||||||
select {
|
if totalSize <= MinChunkSize {
|
||||||
case memoryChannel <- memory:
|
select {
|
||||||
log.Debug().Msg("Memory region sent for analysis")
|
case memoryChannel <- memory:
|
||||||
case <-ctx.Done():
|
log.Debug().Msg("Memory sent as a single chunk for analysis")
|
||||||
return ctx.Err()
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
chunkCount := MaxWorkers * ChunkMultiplier
|
||||||
|
|
||||||
|
// Calculate chunk size based on fixed chunk count
|
||||||
|
chunkSize := totalSize / chunkCount
|
||||||
|
if chunkSize < MinChunkSize {
|
||||||
|
// Reduce number of chunks if each would be too small
|
||||||
|
chunkCount = totalSize / MinChunkSize
|
||||||
|
if chunkCount == 0 {
|
||||||
|
chunkCount = 1
|
||||||
|
}
|
||||||
|
chunkSize = totalSize / chunkCount
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process memory in chunks from end to beginning
|
||||||
|
for i := chunkCount - 1; i >= 0; i-- {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
// Calculate start and end positions for this chunk
|
||||||
|
start := i * chunkSize
|
||||||
|
end := (i + 1) * chunkSize
|
||||||
|
|
||||||
|
// Ensure the last chunk includes all remaining memory
|
||||||
|
if i == chunkCount-1 {
|
||||||
|
end = totalSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add overlap area to catch patterns at chunk boundaries
|
||||||
|
if i > 0 {
|
||||||
|
start -= ChunkOverlapBytes
|
||||||
|
if start < 0 {
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk := memory[start:end]
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Int("chunk_index", i+1).
|
||||||
|
Int("total_chunks", chunkCount).
|
||||||
|
Int("chunk_size", len(chunk)).
|
||||||
|
Int("start_offset", start).
|
||||||
|
Int("end_offset", end).
|
||||||
|
Msg("Processing memory chunk")
|
||||||
|
|
||||||
|
select {
|
||||||
|
case memoryChannel <- chunk:
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -177,24 +234,26 @@ func (e *V4Extractor) SearchKey(ctx context.Context, memory []byte) (string, boo
|
|||||||
break // No more matches found
|
break // No more matches found
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we have enough space for the key
|
// Try each offset for this pattern
|
||||||
keyOffset := index + keyPattern.Offset
|
for _, offset := range keyPattern.Offsets {
|
||||||
if keyOffset < 0 || keyOffset+32 > len(memory) {
|
// Check if we have enough space for the key
|
||||||
index -= 1
|
keyOffset := index + offset
|
||||||
continue
|
if keyOffset < 0 || keyOffset+32 > len(memory) {
|
||||||
}
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Extract the key data, which is 16 bytes after the pattern and 32 bytes long
|
// Extract the key data, which is at the offset position and 32 bytes long
|
||||||
keyData := memory[keyOffset : keyOffset+32]
|
keyData := memory[keyOffset : keyOffset+32]
|
||||||
|
|
||||||
// Validate key against database header
|
// Validate key against database header
|
||||||
if e.validator.Validate(keyData) {
|
if keyData, ok := e.validate(ctx, keyData); ok {
|
||||||
log.Debug().
|
log.Debug().
|
||||||
Str("pattern", hex.EncodeToString(keyPattern.Pattern)).
|
Str("pattern", hex.EncodeToString(keyPattern.Pattern)).
|
||||||
Int("offset", keyPattern.Offset).
|
Int("offset", offset).
|
||||||
Str("key", hex.EncodeToString(keyData)).
|
Str("key", hex.EncodeToString(keyData)).
|
||||||
Msg("Key found")
|
Msg("Key found")
|
||||||
return hex.EncodeToString(keyData), true
|
return hex.EncodeToString(keyData), true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
index -= 1
|
index -= 1
|
||||||
@@ -204,11 +263,19 @@ func (e *V4Extractor) SearchKey(ctx context.Context, memory []byte) (string, boo
|
|||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *V4Extractor) validate(ctx context.Context, keyDate []byte) ([]byte, bool) {
|
||||||
|
if e.validator.Validate(keyDate) {
|
||||||
|
return keyDate, true
|
||||||
|
}
|
||||||
|
// Try to find a valid key by ***
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
func (e *V4Extractor) SetValidate(validator *decrypt.Validator) {
|
func (e *V4Extractor) SetValidate(validator *decrypt.Validator) {
|
||||||
e.validator = validator
|
e.validator = validator
|
||||||
}
|
}
|
||||||
|
|
||||||
type KeyPatternInfo struct {
|
type KeyPatternInfo struct {
|
||||||
Pattern []byte
|
Pattern []byte
|
||||||
Offset int
|
Offsets []int
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const (
|
|||||||
Media = "media"
|
Media = "media"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Groups = []dbm.Group{
|
var Groups = []*dbm.Group{
|
||||||
{
|
{
|
||||||
Name: Message,
|
Name: Message,
|
||||||
Pattern: `^msg_([0-9]?[0-9])?\.db$`,
|
Pattern: `^msg_([0-9]?[0-9])?\.db$`,
|
||||||
@@ -114,6 +114,10 @@ func (ds *DataSource) initMessageDbs() error {
|
|||||||
|
|
||||||
dbPaths, err := ds.dbm.GetDBPath(Message)
|
dbPaths, err := ds.dbm.GetDBPath(Message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "db file not found") {
|
||||||
|
ds.talkerDBMap = make(map[string]string)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// 处理每个数据库文件
|
// 处理每个数据库文件
|
||||||
@@ -155,6 +159,10 @@ func (ds *DataSource) initMessageDbs() error {
|
|||||||
func (ds *DataSource) initChatRoomDb() error {
|
func (ds *DataSource) initChatRoomDb() error {
|
||||||
db, err := ds.dbm.GetDB(ChatRoom)
|
db, err := ds.dbm.GetDB(ChatRoom)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "db file not found") {
|
||||||
|
ds.user2DisplayName = make(map[string]string)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func NewDBManager(path string) *DBManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DBManager) AddGroup(g Group) error {
|
func (d *DBManager) AddGroup(g *Group) error {
|
||||||
fg, err := filemonitor.NewFileGroup(g.Name, d.path, g.Pattern, g.BlackList)
|
fg, err := filemonitor.NewFileGroup(g.Name, d.path, g.Pattern, g.BlackList)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
func TestXxx(t *testing.T) {
|
func TestXxx(t *testing.T) {
|
||||||
path := "/Users/sarv/Documents/chatlog/bigjun_9e7a"
|
path := "/Users/sarv/Documents/chatlog/bigjun_9e7a"
|
||||||
|
|
||||||
g := Group{
|
g := &Group{
|
||||||
Name: "session",
|
Name: "session",
|
||||||
Pattern: `session\.db$`,
|
Pattern: `session\.db$`,
|
||||||
BlackList: []string{},
|
BlackList: []string{},
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const (
|
|||||||
Voice = "voice"
|
Voice = "voice"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Groups = []dbm.Group{
|
var Groups = []*dbm.Group{
|
||||||
{
|
{
|
||||||
Name: Message,
|
Name: Message,
|
||||||
Pattern: `^message_([0-9]?[0-9])?\.db$`,
|
Pattern: `^message_([0-9]?[0-9])?\.db$`,
|
||||||
@@ -113,6 +113,10 @@ func (ds *DataSource) SetCallback(name string, callback func(event fsnotify.Even
|
|||||||
func (ds *DataSource) initMessageDbs() error {
|
func (ds *DataSource) initMessageDbs() error {
|
||||||
dbPaths, err := ds.dbm.GetDBPath(Message)
|
dbPaths, err := ds.dbm.GetDBPath(Message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "db file not found") {
|
||||||
|
ds.messageInfos = make([]MessageDBInfo, 0)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const (
|
|||||||
Voice = "voice"
|
Voice = "voice"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Groups = []dbm.Group{
|
var Groups = []*dbm.Group{
|
||||||
{
|
{
|
||||||
Name: Message,
|
Name: Message,
|
||||||
Pattern: `^MSG([0-9]?[0-9])?\.db$`,
|
Pattern: `^MSG([0-9]?[0-9])?\.db$`,
|
||||||
@@ -35,7 +35,7 @@ var Groups = []dbm.Group{
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: Contact,
|
Name: Contact,
|
||||||
Pattern: `^MicroMsg.db$`,
|
Pattern: `^MicroMsg\.db$`,
|
||||||
BlackList: []string{},
|
BlackList: []string{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -122,6 +122,10 @@ func (ds *DataSource) initMessageDbs() error {
|
|||||||
// 获取所有消息数据库文件路径
|
// 获取所有消息数据库文件路径
|
||||||
dbPaths, err := ds.dbm.GetDBPath(Message)
|
dbPaths, err := ds.dbm.GetDBPath(Message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "db file not found") {
|
||||||
|
ds.messageInfos = make([]MessageDBInfo, 0)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user