Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
713232bd62 | ||
|
|
e83e85fe64 | ||
|
|
e5a1656644 | ||
|
|
194dd0035b | ||
|
|
c3629255ea | ||
|
|
345a0db49b | ||
|
|
5276bce060 | ||
|
|
0abbda1160 | ||
|
|
97c74859c0 | ||
|
|
4d5c6459fe | ||
|
|
c835d5c570 | ||
|
|
59872daf52 | ||
|
|
3bd3935042 | ||
|
|
77a6912ade | ||
|
|
4b83aaed61 | ||
|
|
7268188e40 | ||
|
|
069d836dea | ||
|
|
95384a4a92 | ||
|
|
9b82ca663b | ||
|
|
f5633a29cb | ||
|
|
766cd94788 | ||
|
|
b6d287a299 | ||
|
|
4c25236070 | ||
|
|
c1224cb612 | ||
|
|
5036639aa1 | ||
|
|
9593ca0f54 | ||
|
|
b441287d54 | ||
|
|
9b82d29037 | ||
|
|
0f270366aa | ||
|
|
12b8fb34c6 | ||
|
|
774346d503 | ||
|
|
b97e1f4bc6 | ||
|
|
4b2395a8d2 | ||
|
|
1337bcb7de | ||
|
|
b8f08d54c8 | ||
|
|
24399f1d70 | ||
|
|
e6db4de711 | ||
|
|
adff6021fc | ||
|
|
75004375df | ||
|
|
75de616527 | ||
|
|
f5158fd3e4 | ||
|
|
ed4c21a68f | ||
|
|
f35ca1778a | ||
|
|
21b2b57d28 | ||
|
|
67dc45819e | ||
|
|
ff212d594c | ||
|
|
353df30ff8 | ||
|
|
4f8435d0e2 | ||
|
|
389a853560 | ||
|
|
c437be349e | ||
|
|
488966d771 | ||
|
|
00095ae627 | ||
|
|
ffbe78f74a | ||
|
|
7e9a79486e | ||
|
|
4191d4d5f5 | ||
|
|
29c1166eca | ||
|
|
bf78055b69 | ||
|
|
550e3b104c | ||
|
|
cf9ea8a83b | ||
|
|
5efae86fcf | ||
|
|
70c55442f3 | ||
|
|
9288256a5e | ||
|
|
e5bd162a01 | ||
|
|
cef1ad493c | ||
|
|
add16885fe | ||
|
|
ab3b3731b7 | ||
|
|
0c4932d740 | ||
|
|
fe8619d56d | ||
|
|
376ea79ff6 | ||
|
|
dc6d1eb6ac | ||
|
|
532e135406 | ||
|
|
7ae0d63a7f | ||
|
|
c759bcd30e | ||
|
|
0bad419150 | ||
|
|
facc292443 | ||
|
|
7554de7d72 | ||
|
|
1bc66e33fd | ||
|
|
976aac32c5 | ||
|
|
3634f49a48 | ||
|
|
1156362c3c | ||
|
|
60528ffe53 | ||
|
|
f28480ffbe | ||
|
|
8e784d8125 | ||
|
|
3489535750 | ||
|
|
5e268cd8f4 | ||
|
|
454f449c41 | ||
|
|
517e650942 | ||
|
|
21e299942b | ||
|
|
bde3bc537f | ||
|
|
719ffa0a24 | ||
|
|
8fa4dd50e7 | ||
|
|
3462d49516 | ||
|
|
7ef991aa67 | ||
|
|
4d758b2aca | ||
|
|
27f4108cf9 | ||
|
|
4075e52eec | ||
|
|
f7d619292b | ||
|
|
e1f4ec79dd | ||
|
|
76e9e683c4 | ||
|
|
7c8b45a24a | ||
|
|
1c8bbae1f0 | ||
|
|
25b72c3d76 | ||
|
|
f65dc4ac32 | ||
|
|
e59332b514 | ||
|
|
6cbf9af662 | ||
|
|
5acfa14475 | ||
|
|
698251796f | ||
|
|
a007e110ac | ||
|
|
7cf043e3c3 | ||
|
|
949e6bc868 | ||
|
|
62abd872ee | ||
|
|
0d68217dda | ||
|
|
ee5f50db83 | ||
|
|
36e709d7a6 | ||
|
|
b26d4dd1a7 | ||
|
|
8f9a0c44da | ||
|
|
7016ca1237 | ||
|
|
fc42b18f4a | ||
|
|
1e25ae4065 | ||
|
|
d26af6462f | ||
|
|
4ffac08775 | ||
|
|
9dee0bbd3c | ||
|
|
5ff67edaef | ||
|
|
eec6286a58 | ||
|
|
47e0d7cd1e | ||
|
|
fc54c3b946 | ||
|
|
8e5b9790f6 | ||
|
|
a0c8696987 | ||
|
|
77e7e54949 | ||
|
|
ce27829811 | ||
|
|
17e3b51629 | ||
|
|
313d0afff7 | ||
|
|
c83d109a75 | ||
|
|
72d2b17842 | ||
|
|
d93caa3539 | ||
|
|
49d4e17d67 | ||
|
|
c0b710e56f | ||
|
|
d23a83d066 | ||
|
|
10ea43dc9f | ||
|
|
c7f2105495 | ||
|
|
14f3b85865 | ||
|
|
e75e2ebb5e | ||
|
|
316b8c7583 | ||
|
|
c89c108098 | ||
|
|
4058a817dd | ||
|
|
9b7f5114bd | ||
|
|
55f91370be | ||
|
|
412a509b2b | ||
|
|
63a11a334e | ||
|
|
d2402c8cfa | ||
|
|
e34567e58f | ||
|
|
7b07561a7f | ||
|
|
7f3eff8f03 | ||
|
|
cd2fb09e02 | ||
|
|
dcba9496c6 | ||
|
|
af56f19863 | ||
|
|
c1224530c4 | ||
|
|
eb4cb30fb5 | ||
|
|
5018054cfd | ||
|
|
3f019faa38 | ||
|
|
b490dc4098 | ||
|
|
072387562a | ||
|
|
f72a7c8025 | ||
|
|
632af4f409 | ||
|
|
4c765568a2 | ||
|
|
fcba05aff9 | ||
|
|
6b9358b69b | ||
|
|
36a9cffc82 | ||
|
|
e595e240eb | ||
|
|
e62b5ef138 | ||
|
|
f9e1ba9372 | ||
|
|
92d5f5ab47 | ||
|
|
03870ca78b | ||
|
|
7ec0e4f539 | ||
|
|
bf80cbc14d | ||
|
|
cc67b7074f | ||
|
|
1edea7d3cc | ||
|
|
0e96c146d5 | ||
|
|
7904d42442 | ||
|
|
7eefc59a0f | ||
|
|
33c0f15665 | ||
|
|
ac82b50571 | ||
|
|
1d9987181b | ||
|
|
3bef4cecc1 | ||
|
|
2775cc424e | ||
|
|
fb5ebfad68 | ||
|
|
1c6a908499 | ||
|
|
7c4508bf29 | ||
|
|
a6a004e403 | ||
|
|
9789cdb72e | ||
|
|
b5f67c47e5 | ||
|
|
eee84f35df | ||
|
|
4967cff3c0 | ||
|
|
17417aa9d6 | ||
|
|
ce67107128 | ||
|
|
c36590839b | ||
|
|
8a5f3bbd00 | ||
|
|
e001074392 | ||
|
|
7bf79a7424 | ||
|
|
40d9cb3fad | ||
|
|
eaa70dd8c6 | ||
|
|
9e79790d41 | ||
|
|
580c7019d7 | ||
|
|
d4bd7be51c | ||
|
|
f0662a23c2 | ||
|
|
79ed94bc57 | ||
|
|
f07f8aeb6c | ||
|
|
5a53640b6f | ||
|
|
a8737fc6f7 | ||
|
|
87ef43575e | ||
|
|
24227b5df2 | ||
|
|
c9522f71d2 | ||
|
|
0c38e22c09 | ||
|
|
58906da35c | ||
|
|
903e98820a | ||
|
|
7f7ebc8811 | ||
|
|
4ac1dbb87f | ||
|
|
603b790682 | ||
|
|
8ec3237ef9 | ||
|
|
e8cbfaf223 | ||
|
|
d7a2dd3709 | ||
|
|
b779dc9838 | ||
|
|
7d509ae33b | ||
|
|
af03e398e7 | ||
|
|
6df271a173 | ||
|
|
e1a174ade9 | ||
|
|
3c0e5a94cb | ||
|
|
303333dbc3 | ||
|
|
bfc6271af3 | ||
|
|
20ceaf3b8a | ||
|
|
b5c0239b65 | ||
|
|
fb906a02c1 | ||
|
|
488460cd82 | ||
|
|
306c7d99c2 | ||
|
|
0084b92703 | ||
|
|
b7fba40754 | ||
|
|
7f25920965 | ||
|
|
7f6b9a0948 | ||
|
|
1394208421 | ||
|
|
44ec8c4bb9 | ||
|
|
a3fb425061 | ||
|
|
b466f4555e | ||
|
|
0846e432cd | ||
|
|
fd2febc152 | ||
|
|
8083c1c4bd | ||
|
|
bb588b1448 | ||
|
|
4ae5afb888 | ||
|
|
26d027f947 | ||
|
|
864c01b345 | ||
|
|
fa1ccc341e | ||
|
|
39afb0b8c2 | ||
|
|
dabdb61919 | ||
|
|
7238b4a01c | ||
|
|
3c2aa00dc6 | ||
|
|
fd52aa4f5c | ||
|
|
0c1197b7a9 | ||
|
|
7fbd9f3af2 | ||
|
|
e7a4ed738e | ||
|
|
840ad51900 | ||
|
|
6d8ed43f65 | ||
|
|
5f341357d1 | ||
|
|
c21fda590b | ||
|
|
ba9e08d406 | ||
|
|
6070d3b92d | ||
|
|
5cda0c8c18 | ||
|
|
bf41745cbd | ||
|
|
888a708476 | ||
|
|
959a4fefd1 | ||
|
|
7c8aafaf78 | ||
|
|
3a9dbec1f1 | ||
|
|
b15f23d728 | ||
|
|
85d66f2da2 | ||
|
|
a0028fc57d | ||
|
|
24879a2639 | ||
|
|
54ebe01fa3 | ||
|
|
e586c5f571 | ||
|
|
6b20e819aa | ||
|
|
c8be398243 | ||
|
|
18e2b90583 | ||
|
|
e70bddea82 | ||
|
|
1e6bffa6ee | ||
|
|
762af8dd95 | ||
|
|
1d3a2ee181 | ||
|
|
d7c5ff1335 | ||
|
|
1eb96f3d56 | ||
|
|
716597f089 | ||
|
|
65d0d05daa | ||
|
|
85728a47f4 | ||
|
|
606d05aa7a | ||
|
|
8368f7f7f2 | ||
|
|
e67d893458 | ||
|
|
383ace8474 | ||
|
|
8f2d83dead | ||
|
|
52d2e63bc5 | ||
|
|
4fedc8c387 | ||
|
|
295d77f585 | ||
|
|
ae561e6340 | ||
|
|
3e37550ff9 | ||
|
|
359ff23faf | ||
|
|
cedc67c36e | ||
|
|
e4c708cdf0 | ||
|
|
42393e4796 | ||
|
|
e6927d7cad | ||
|
|
767a85ee5f | ||
|
|
04925828d9 | ||
|
|
42c6cce0c9 | ||
|
|
cca5284977 | ||
|
|
3b05bcbe29 | ||
|
|
1ddfe5f87e | ||
|
|
e0e0105e8b | ||
|
|
44bf1bcaef | ||
|
|
a184bb1da1 | ||
|
|
5f92e1d410 | ||
|
|
a12ec593da | ||
|
|
9a7549505f | ||
|
|
bcef842bb3 | ||
|
|
d151f36dd3 | ||
|
|
a3e3f8edbf | ||
|
|
7154921d05 | ||
|
|
e11b7b1aa2 | ||
|
|
bdccead5b4 | ||
|
|
6e6224c017 | ||
|
|
1f84a0a4b9 | ||
|
|
f61482afd8 | ||
|
|
eb36aa20e3 | ||
|
|
c74b99e61a | ||
|
|
d9cb3f676c | ||
|
|
6f3e97e7eb | ||
|
|
13149ecd0d | ||
|
|
e7f0306225 | ||
|
|
8edf36f826 | ||
|
|
2f816eb06a |
@@ -1,8 +1,39 @@
|
|||||||
# WeChat Database Key (Optional, can be entered in UI)
|
# WeChat Database Key (Optional, can be entered in UI)
|
||||||
VITE_DB_KEY=
|
VITE_DB_KEY=
|
||||||
|
|
||||||
# DeepSeek API Key (Optional, can be entered in UI)
|
# Auto login on startup with VITE_DB_KEY or saved key.
|
||||||
VITE_DEEPSEEK_API_KEY=
|
# Set to true/1/yes/on for local development. Default is disabled.
|
||||||
|
VITE_AUTO_LOGIN=false
|
||||||
|
|
||||||
# Message types to filter out (comma separated)
|
# Show the scheduled report debug notification test action. Default is disabled.
|
||||||
VITE_FILTER_MSG_TYPES=分享消息,图片,表情包,视频
|
VITE_SCHEDULED_REPORT_DEBUG=false
|
||||||
|
|
||||||
|
# AI API Configuration (Optional, can be entered in UI)
|
||||||
|
# 注意:发布版本不再自动读取以下环境变量。
|
||||||
|
# 如果你只是本地开发想用默认值,可以在自己机器的 .env.local 里填,
|
||||||
|
# 然后在「设置 → AI 模型」里手动完成"添加供应商"流程。
|
||||||
|
VITE_DEEPSEEK_API_KEY=
|
||||||
|
VITE_AI_BASE_URL=https://api.deepseek.com
|
||||||
|
VITE_AI_MODEL=deepseek-chat
|
||||||
|
|
||||||
|
# Message types to filter out (comma separated). Empty means show all message types.
|
||||||
|
VITE_FILTER_MSG_TYPES=
|
||||||
|
|
||||||
|
# Image Decryption Keys (Optional, for WeChat 4.0+ image decryption)
|
||||||
|
# These are dev fallbacks. End users can fill or auto-fetch them in Settings.
|
||||||
|
# XOR Key: hex format like 0x40, 0x53 etc.
|
||||||
|
# AES Key: 16-character string, derived from wxid and code
|
||||||
|
VITE_IMAGE_XOR_KEY=
|
||||||
|
VITE_IMAGE_AES_KEY=
|
||||||
|
|
||||||
|
# Electron E2E test window close delay in milliseconds.
|
||||||
|
# Local default: 2000 (2 seconds). Set to 0 for immediate close.
|
||||||
|
WXE_E2E_CLOSE_DELAY_MS=2000
|
||||||
|
|
||||||
|
# Experimental self-hosted WeChat share-card service
|
||||||
|
# Copy these placeholders to .env. Never commit real AppSecret or UPLOAD_TOKEN values.
|
||||||
|
WECHAT_SHARE_DOMAIN=share.example.com
|
||||||
|
WECHAT_SHARE_APP_ID=
|
||||||
|
WECHAT_SHARE_APP_SECRET=
|
||||||
|
# Leave empty to let docs/skill/setup-wechat-share-card/scripts/setup.sh generate one.
|
||||||
|
WECHAT_SHARE_UPLOAD_TOKEN=
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
name: Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
desktop-tests:
|
||||||
|
name: ${{ matrix.os }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [windows-latest, macos-latest]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 7.33.7
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Install Playwright Chromium
|
||||||
|
run: pnpm exec playwright install chromium
|
||||||
|
|
||||||
|
- name: Type check
|
||||||
|
run: pnpm typecheck
|
||||||
|
|
||||||
|
- name: Unit tests
|
||||||
|
run: pnpm test:unit
|
||||||
|
|
||||||
|
- name: Component tests
|
||||||
|
run: pnpm test:component
|
||||||
|
|
||||||
|
- name: IPC integration tests
|
||||||
|
run: pnpm test:integration
|
||||||
|
|
||||||
|
- name: Skill installation instruction tests
|
||||||
|
run: pnpm test:skill-install
|
||||||
|
|
||||||
|
- name: Build Electron test application
|
||||||
|
run: pnpm test:e2e:build
|
||||||
|
|
||||||
|
- name: Electron E2E tests
|
||||||
|
run: pnpm exec playwright test --grep-invert="@visual"
|
||||||
|
env:
|
||||||
|
WXE_E2E_CLOSE_DELAY_MS: 0
|
||||||
|
|
||||||
|
- name: Platform visual regression
|
||||||
|
# GitHub-hosted Windows 无法稳定容纳项目统一的 1400×800 Electron 窗口,Windows visual regression 改由可控 Windows 桌面环境执行。
|
||||||
|
if: matrix.os != 'windows-latest'
|
||||||
|
run: pnpm exec playwright test tests/e2e/visual.spec.ts
|
||||||
|
env:
|
||||||
|
WXE_E2E_CLOSE_DELAY_MS: 0
|
||||||
|
|
||||||
|
- name: Upload Playwright diagnostics
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: playwright-${{ matrix.os }}
|
||||||
|
path: |
|
||||||
|
test-results/
|
||||||
|
playwright-report/
|
||||||
|
if-no-files-found: ignore
|
||||||
|
retention-days: 14
|
||||||
@@ -1,7 +1,28 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
*.tsbuildinfo
|
||||||
|
electron.vite.config.[0-9]*.mjs
|
||||||
dist
|
dist
|
||||||
out
|
out
|
||||||
.env
|
.env
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.eslintcache
|
.eslintcache
|
||||||
*.log*
|
*.log*
|
||||||
|
coverage/
|
||||||
|
playwright-report/
|
||||||
|
test-results/
|
||||||
|
resources/runtime/darwin-arm64
|
||||||
|
.native-runtime-source
|
||||||
|
.omc
|
||||||
|
.codex/
|
||||||
|
skills-lock.json
|
||||||
|
*__screenshots__
|
||||||
|
|
||||||
|
/AGENTS.md
|
||||||
|
/CLAUDE.md
|
||||||
|
/.claude/
|
||||||
|
/.workbuddy/
|
||||||
|
/.ai-local/
|
||||||
|
|
||||||
|
findings.md
|
||||||
|
progress.md
|
||||||
|
task_plan.md
|
||||||
|
|||||||
@@ -1 +1,8 @@
|
|||||||
shamefully-hoist=true
|
shamefully-hoist=true
|
||||||
|
electron_mirror=https://npmmirror.com/mirrors/electron/
|
||||||
|
# Keep the Windows x64 sherpa-onnx optional runtime available when packaging
|
||||||
|
# Windows from macOS/Linux hosts.
|
||||||
|
supportedArchitectures.os[]=darwin
|
||||||
|
supportedArchitectures.os[]=win32
|
||||||
|
supportedArchitectures.cpu[]=arm64
|
||||||
|
supportedArchitectures.cpu[]=x64
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ singleQuote: true
|
|||||||
semi: false
|
semi: false
|
||||||
printWidth: 100
|
printWidth: 100
|
||||||
trailingComma: none
|
trailingComma: none
|
||||||
|
endOfLine: auto
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"cwd": "${workspaceRoot}",
|
"cwd": "${workspaceRoot}",
|
||||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
|
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
|
||||||
"windows": {
|
"windows": {
|
||||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd"
|
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.CMD"
|
||||||
},
|
},
|
||||||
"runtimeArgs": ["--sourcemap"],
|
"runtimeArgs": ["--sourcemap"],
|
||||||
"env": {
|
"env": {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"[typescript]": {
|
"[typescript]": {
|
||||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
},
|
},
|
||||||
"[javascript]": {
|
"[javascript]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
},
|
},
|
||||||
"[json]": {
|
"[json]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "vscode.json-language-features"
|
||||||
}
|
},
|
||||||
}
|
"files.eol": "\n"
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# 参与贡献
|
||||||
|
|
||||||
|
> 这份文档同时写给人和 AI Agent。文末有给 Agent 的英文硬性规则。
|
||||||
|
> 如果你只是想把 TraceMemo 跑起来,看[第一次使用](./docs/user-guide/getting-started.md)就够了。
|
||||||
|
|
||||||
|
## 一句话规则
|
||||||
|
|
||||||
|
**从 `develop` 拉分支,把 PR 提给 `develop`。**
|
||||||
|
|
||||||
|
## 为什么不是 main
|
||||||
|
|
||||||
|
| 分支 | 是什么 | 接受 PR 吗 |
|
||||||
|
| --------- | ------------------------------------------------------- | -------------- |
|
||||||
|
| `main` | 稳定版。只在发版时更新,对应 GitHub Releases 里的安装包 | 不接受 |
|
||||||
|
| `develop` | 开发主线。所有改动先进这里,**随下一个版本一起发布** | 唯一的目标分支 |
|
||||||
|
|
||||||
|
指向 `main` 的 PR、或者不是基于 `develop` 拉出来的分支,会被直接关闭。不是不欢迎贡献,而是因为冲突合并 提交落后较多等原因
|
||||||
|
|
||||||
|
## 提 PR 的完整流程
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 基于 develop 拉分支(不要基于 main)
|
||||||
|
git fetch origin
|
||||||
|
git checkout -b feat/your-change origin/develop
|
||||||
|
|
||||||
|
# 2. 改代码,只改与本任务相关的文件
|
||||||
|
|
||||||
|
# 3. 自检
|
||||||
|
pnpm install # 需要 Node 22 与 pnpm 7.33.7
|
||||||
|
pnpm typecheck
|
||||||
|
pnpm test:unit # 再按改动范围补跑 component / integration
|
||||||
|
|
||||||
|
# 4. 提交
|
||||||
|
git commit -m "feat: 一句话说明改了什么"
|
||||||
|
|
||||||
|
# 5. 提 PR,目标分支必须是 develop
|
||||||
|
gh pr create --base develop --head feat/your-change
|
||||||
|
```
|
||||||
|
|
||||||
|
## 提交信息
|
||||||
|
|
||||||
|
默认**只写一行标题**:类型前缀 + 一句话说明。
|
||||||
|
|
||||||
|
```
|
||||||
|
feat: 新增本地查询能力
|
||||||
|
fix: 修复查询工具时间契约
|
||||||
|
docs: 整理开发文档
|
||||||
|
```
|
||||||
|
|
||||||
|
确实包含多个功能点时,标题之后每个功能点各写一行纯文本 —— 不要用列表符号,也不要写
|
||||||
|
「设置侧:」「测试:」这类分节标题:
|
||||||
|
|
||||||
|
```
|
||||||
|
feat: 统一手动发送入口为文字转语音
|
||||||
|
移除普通文本、图片和本地语音的手动发送入口
|
||||||
|
保留文字转语音的生成、试听和发送能力
|
||||||
|
```
|
||||||
|
|
||||||
|
不要在提交信息里堆文件名清单、测试结果或实现过程叙述,那些属于 PR 描述。
|
||||||
|
|
||||||
|
## 分支命名
|
||||||
|
|
||||||
|
`feat/…`、`fix/…`、`docs/…`、`refactor/…`,后面接简短的英文或拼音描述。
|
||||||
|
|
||||||
|
## PR 前自检
|
||||||
|
|
||||||
|
| 你的改动 | 至少跑这些 |
|
||||||
|
| -------------------------- | ----------------------------------------------------------------- |
|
||||||
|
| 一般代码 / 服务 / 工具函数 | `pnpm typecheck` + 相关 `pnpm test:unit` |
|
||||||
|
| 界面 / 交互 | `pnpm typecheck` + 相关 `pnpm test:component`,或针对该功能的 E2E |
|
||||||
|
| preload / IPC 契约 | `pnpm typecheck` + 相关 contract 测试或 `pnpm test:integration` |
|
||||||
|
| 只改文档 | 不需要跑测试 |
|
||||||
|
|
||||||
|
全部测试命令见 `package.json` 的 `scripts`;本地开发环境的说明见[开发概览](./docs/development/overview.md)。
|
||||||
|
|
||||||
|
## 请不要提交这些东西
|
||||||
|
|
||||||
|
- **构建产物**:`out/`、`dist/`、`build/` 下的二进制、`node_modules/`、`test-results/`、`playwright-report/`
|
||||||
|
- **真实数据**:微信聊天内容、真实的 wxid / 群名 / 联系人名、聊天截图
|
||||||
|
- **密钥**:API Key、Token、数据库密钥、图片解密密钥
|
||||||
|
- **本机绝对路径**:`/Users/…`、`C:\Users\…`
|
||||||
|
|
||||||
|
测试用的假数据请用一眼能看出是合成的命名,例如 `fixture-group`、`wxid_fixture_member`。
|
||||||
|
|
||||||
|
## 合入之后
|
||||||
|
|
||||||
|
PR 合进 `develop` 不会立刻出现在下载页,它随**下一个版本**发布。想提前用上,可以自行从
|
||||||
|
`develop` 构建。
|
||||||
|
|
||||||
|
## 如果你是 AI Agent
|
||||||
|
|
||||||
|
请按顺序执行,不要凭直觉选分支:
|
||||||
|
|
||||||
|
1. **基线**:`git fetch origin && git checkout -b <branch> origin/develop`。永远不要基于 `main`。
|
||||||
|
2. **PR 目标**:`gh pr create --base develop`。无法确定时默认 `develop`;任何情况下都不要把
|
||||||
|
`main` 当 PR 目标。
|
||||||
|
3. **改动范围**:只改与本任务相关的文件。不要顺手格式化、重排 import、升级依赖。
|
||||||
|
4. **提交前**:跑 `pnpm typecheck` 和与本次改动相关的测试(见上表)。
|
||||||
|
5. **提交信息**:一行标题,`type: 描述`。不要写文件清单、测试输出或过程叙述。
|
||||||
|
6. **PR 描述**:说明改了什么、为什么改、怎么验证的;关联 Issue 用 `closes #123`。
|
||||||
|
7. **禁止**:真实聊天数据、密钥、Token、本机绝对路径、构建产物。
|
||||||
|
|
||||||
|
### Hard rules for AI agents (English)
|
||||||
|
|
||||||
|
- Base branch: `origin/develop`. Never branch off `main`.
|
||||||
|
- Open pull requests with base branch `develop`. PRs targeting `main` are closed without review.
|
||||||
|
- One PR = one logical change. No drive-by reformatting, import reordering, or dependency upgrades.
|
||||||
|
- Before opening a PR, run `pnpm typecheck` plus the tests relevant to your change.
|
||||||
|
- Commit subject: a single line, `type: summary`. No file lists, no test logs, no process narration.
|
||||||
|
- Never commit build output (`out/`, `dist/`, `test-results/`, `playwright-report/`), real WeChat
|
||||||
|
data, keys, tokens, or absolute local paths.
|
||||||
|
- Changes merged into `develop` ship with the next release.
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2024 WechatExplorer Contributors
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,92 +1,234 @@
|
|||||||
# WechatExplorer
|
# TraceMemo(迹忆)
|
||||||
|
|
||||||
MAC系统 获取微信聊天记录 AI一键生成群聊总结
|
<p align="center">
|
||||||
是一个基于 Electron + React + TypeScript 开发的微信聊天记录查看与分析工具。它支持查看解密后的微信数据库内容,提供聊天记录搜索、导出以及 AI 智能总结功能。
|
<img src="./build/icon.png" width="120" alt="TraceMemo Logo" />
|
||||||
|
</p>
|
||||||
|
|
||||||
## 📸 预览
|
<h2 align="center">把微信里的信息,记住、理解、监控,并在需要时行动</h2>
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-between;">
|
<p align="center">本地优先的微信数据、AI 分析与自动化工作台</p>
|
||||||
<img src="./public/WX20251215-155324.png" alt="预览图1" width="48%" />
|
|
||||||
<img src="./public/WX20251215-155524.png" alt="预览图2" width="48%" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## ✨ 功能特性
|
<p align="center">
|
||||||
|
<img src="https://img.shields.io/github/stars/Wxw-Gu/TraceMemo?style=for-the-badge" alt="GitHub stars" />
|
||||||
|
<img src="https://img.shields.io/github/downloads/Wxw-Gu/TraceMemo/total?style=for-the-badge" alt="GitHub downloads" />
|
||||||
|
<img src="https://img.shields.io/github/v/release/Wxw-Gu/TraceMemo?style=for-the-badge" alt="Latest release" />
|
||||||
|
</p>
|
||||||
|
|
||||||
- **聊天记录查看**: 浏览微信好友和群聊的聊天记录。
|
<p align="center">
|
||||||
- **全局搜索**: 快速搜索聊天内容。
|
<a href="https://github.com/Wxw-Gu/TraceMemo/releases"><b>下载 TraceMemo</b></a>
|
||||||
- **AI 智能总结**: 集成 DeepSeek AI,一键总结群聊精华内容,生成话题报告。
|
·
|
||||||
- **图片生成**: 将 AI 总结的内容生成精美图片,方便分享。
|
<a href="./docs/user-guide/getting-started.md"><b>第一次使用</b></a>
|
||||||
- **数据导出**: 支持导出聊天记录为 CSV 文件(今日、昨日、近7天或全部)。
|
·
|
||||||
- **安全隐私**: 所有数据仅在本地处理,AI 功能需自行配置 API Key。
|
<a href="./docs/README.md"><b>完整文档</b></a>
|
||||||
|
·
|
||||||
|
<a href="./docs/concepts/how-it-works.md"><b>TraceMemo 如何工作</b></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
## 🛠️ 技术栈
|
<p align="center">
|
||||||
|
<img src="./public/日报.png" alt="TraceMemo 日报" />
|
||||||
|
</p>
|
||||||
|
|
||||||
- **Electron**: 跨平台桌面应用框架
|
<p align="center">
|
||||||
- **React**: 用于构建用户界面
|
<img src="./public/自动化.png" alt="TraceMemo 自动化" />
|
||||||
- **TypeScript**: 提供类型安全
|
</p>
|
||||||
- **Vite**: 极速构建工具
|
|
||||||
- **Better-SQLite3**: 高性能 SQLite 数据库操作
|
|
||||||
- **OpenAI SDK**: 用于调用 DeepSeek API
|
|
||||||
|
|
||||||
## 🚀 快速开始
|
|
||||||
|
|
||||||
### 前置要求
|
---
|
||||||
|
|
||||||
- 微信<=4.0 需要获取自己微信本地数据库的密码, 获取方式参考: [Mac 导出微信聊天记录](https://blog.vcvit.me/2024/08/02/mac-export-wechat-chat-records/)
|
## 🎨 社区日报模板
|
||||||
- 如果无法获取本地数据库密码 则无法使用当前项目
|
|
||||||
- Node.js (推荐 v16+)
|
|
||||||
- pnpm (推荐) 或 npm
|
|
||||||
- 解密后的微信数据库文件 (`.db`) 和对应的密钥
|
|
||||||
- [DeepSeek API Key](https://www.deepseek.com/) (用于 AI 总结功能)
|
|
||||||
|
|
||||||
### 安装
|
TraceMemo 日报除了内置版式,也支持从社区模板市场安装更多样式。社区模板与默认日报读取同一份真实日报数据,只改变展示方式,适合手机长图分享、桌面归档、团队复盘等不同场景。
|
||||||
|
|
||||||
```bash
|
<p align="center">
|
||||||
# 克隆项目
|
<a href="https://github.com/Wxw-Gu/TraceMemo-Templates"><b>浏览 TraceMemo 模板社区</b></a>
|
||||||
git clone https://github.com/your-username/WechatExplorer.git
|
·
|
||||||
|
<a href="https://github.com/Wxw-Gu/TraceMemo-Templates/tree/main/skills/tracememo-template-contributor"><b>用 AI 制作并投稿模板</b></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
# 进入目录
|
在 TraceMemo 中打开:
|
||||||
cd WechatExplorer
|
|
||||||
|
|
||||||
# 安装依赖
|
**日报 → 社区模板市场**
|
||||||
pnpm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### 配置
|
即可查看、预览、安装和切换已发布的社区模板。
|
||||||
|
|
||||||
复制 `.env.example` 文件为 `.env`,并填写相关配置(可选,也可以在应用内设置):
|
如果你有一张喜欢的日报长图、网页或前端项目,也可以把它交给 Codex、ChatGPT 或其他能够读取 GitHub 仓库的 AI,并让它读取 [TraceMemo Template Contributor Skill](https://github.com/Wxw-Gu/TraceMemo-Templates/tree/main/skills/tracememo-template-contributor)。AI 可以帮助你完成模板转换、真实预览,并在你确认满意后向 [TraceMemo-Templates](https://github.com/Wxw-Gu/TraceMemo-Templates) 提交 Pull Request。
|
||||||
|
|
||||||
```bash
|
模板通过审核并正式发布后,其他 TraceMemo 用户即可在模板市场中安装使用。
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
### 开发模式运行
|
---
|
||||||
|
|
||||||
```bash
|
## TraceMemo 是什么
|
||||||
pnpm dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### 打包构建
|
TraceMemo(迹忆)原名 **WechatExplorer** 是一款本地优先的微信数据、AI 分析与自动化工作台,把聊天变成可浏览、可搜索、可理解、可追溯的信息。
|
||||||
|
|
||||||
```bash
|
先用档案找原话,再按需要使用 AI Search、日报、监控或 Agent。普通浏览、搜索和导出不需要 AI。
|
||||||
# Windows
|
|
||||||
pnpm build:win
|
|
||||||
|
|
||||||
# macOS
|
## 核心能力
|
||||||
pnpm build:mac
|
|
||||||
|
|
||||||
# Linux
|
- 💬 **聊天档案与搜索**:浏览会话,按关键词、备注、昵称或 wxid 查找消息。
|
||||||
pnpm build:linux
|
- 🔍 **AI Search / 问问微信**:用自然语言找回模糊记忆,并查看来源。
|
||||||
```
|
- 🧠 **本地知识库**:在本机建立索引,让跨会话、跨时间的查询更稳定。
|
||||||
|
- 🖼️ **图片文字索引**:在本机识别微信图片里的文字(截图、公告、报价图),识别结果可以在搜索和「问问微信」里被检索。识别全程不联网,原始图片不会因为本地识别而上传。
|
||||||
|
- 📊 **群聊日报**:生成今日、昨日或近 7 天的群聊总结,可保存为 HTML 与 PNG。
|
||||||
|
- 🗣️ **群发言统计**:统计群成员的发言量和沉默成员,看清一个群里谁在说、谁一直没说。
|
||||||
|
- 👀 **退群监控**:用成员快照对比记录群成员退出事件,支持多群与事件历史。
|
||||||
|
- ⚙️ **自动化**:把上面几步按规则串起来——定时生成并发送日报、成员退群时发送通知;能发到哪里取决于当前的发送能力。
|
||||||
|
- 🔊 **文字转语音**:把文字生成语音,试听后发送到当前会话。
|
||||||
|
- 🤖 **Agent Hub**:在微信里向本机 TraceMemo 提问。
|
||||||
|
- 🔌 **外部 Agent / Local HTTP API**:让 Codex 等外部 Agent 查询本机微信历史。
|
||||||
|
|
||||||
## ⚠️ 免责声明
|
## 💻 平台支持
|
||||||
|
|
||||||
本项目仅供学习和研究使用。请勿用于非法用途。开发者不对使用本项目造成的任何后果负责。请遵守相关法律法规和微信使用协议。
|
TraceMemo 2.5.0 支持:
|
||||||
|
|
||||||
## 📄 许可证
|
- **Windows x64**
|
||||||
|
- **macOS Apple Silicon(M 系列 / arm64)**
|
||||||
|
- **macOS Intel(x64)**
|
||||||
|
|
||||||
[MIT](./LICENSE) License
|
Windows 与 macOS 均支持微信本地数据库连接与数据库 Key 获取。
|
||||||
|
|
||||||
## 🔗 参考
|
### 关于“发送能力”
|
||||||
|
|
||||||
- [WechatMessageExplorer](https://github.com/svcvit/WechatMessageExplorer)
|
浏览、搜索、日报生成、导出、知识库和图片文字索引都不需要额外的发送组件。只有**把内容真正发回微信**这一步——自动发送日报、退群通知、把语音发到会话——依赖本机发送能力:
|
||||||
|
|
||||||
|
发送能力未就绪、未绑定或发送失败时,报告本身仍会正常生成并保存在本机,执行记录会显示为“已生成,但未发送”或“已生成,发送失败”,可以稍后重试。
|
||||||
|
|
||||||
|
## 项目缘起
|
||||||
|
|
||||||
|
<details>
|
||||||
|
|
||||||
|
TraceMemo 最早叫 **WechatExplorer**。
|
||||||
|
|
||||||
|
**2025 年 12 月**,我做出了第一个版本。当时功能很简单:解析微信 3.0 的聊天记录,再用 AI 生成群聊日报。最初只是给自己用,想把散落在微信里的信息重新找出来,也方便看看群里每天聊了什么。
|
||||||
|
|
||||||
|
第一个版本完成后,项目搁置了一段时间。后来重新捡起来,我还是想继续做群聊日报,但微信已经更新到 4.x,原来的微信 3.0 数据解析方案不再适用。
|
||||||
|
|
||||||
|
为了支持微信 4.x,我开始重新研究数据访问。这部分工作最初得到了 **WeFlow** 很大的帮助。早期 TraceMemo 曾参考 WeFlow 历史版本中的实现和思路,借此解决了数据库消息、密钥获取等微信 4.x 数据访问问题。
|
||||||
|
|
||||||
|
随着项目继续发展,我逐步把这部分底层能力从原有实现中抽离,并重新实现了一套独立的数据访问兼容层。目前会继续保持与 WeFlow 历史接口和行为的兼容,以减少上层业务迁移成本。
|
||||||
|
|
||||||
|
也就是说,**WeFlow 是 TraceMemo 进入微信 数据访问领域的重要起点。没有 WeFlow,就没有今天的 TraceMemo。**
|
||||||
|
|
||||||
|
在此基础上,项目陆续加入了:
|
||||||
|
|
||||||
|
- 本地知识库
|
||||||
|
- 消息来源追溯
|
||||||
|
- 群聊日报
|
||||||
|
- 语音消息也参与知识库等问答
|
||||||
|
- 微信机器人
|
||||||
|
- Local HTTP API
|
||||||
|
- Reader Skill
|
||||||
|
- Agent 接入
|
||||||
|
- 多种聊天记录导出能力
|
||||||
|
- 退群监控
|
||||||
|
- 文字转语音
|
||||||
|
- 持续监控自动化能力
|
||||||
|
|
||||||
|
群聊日报后来被一些人看到,项目也开始有了 Star、Fork、使用反馈和功能建议。说实话,我一开始没想到,这个原本只给自己用的小工具,会得到这么多人的关注。
|
||||||
|
|
||||||
|
这些关注和反馈让我决定认真把项目继续做下去。WechatExplorer 就这样一步一步变成了今天的 **TraceMemo(迹忆)**。
|
||||||
|
|
||||||
|
感谢每一位使用、关注和反馈过的人。
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💬 交流与反馈
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="./public/二维码.jpg" alt="TraceMemo 交流与售后群二维码" width="280" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## 从你的任务开始
|
||||||
|
|
||||||
|
| 想做什么 | 使用入口 |
|
||||||
|
| ---------------------------------- | ----------------------------- |
|
||||||
|
| 找记得原文或关键词的消息 | 档案搜索 |
|
||||||
|
| 找记得大意、但不知道在哪聊过的内容 | 问问微信(AI Search) |
|
||||||
|
| 找到截图、公告图里写过的文字 | 问问微信 → 图片文字索引 |
|
||||||
|
| 长期跨群查询历史 | 本地知识库 |
|
||||||
|
| 了解一个群今天或近 7 天聊了什么 | 日报 |
|
||||||
|
| 看群里谁最活跃、谁一直没说话 | 档案 → 群聊 → 群发言统计 |
|
||||||
|
| 持续关注群成员退出 | 退群监控 |
|
||||||
|
| 按计划自动生成并发送群聊日报 | 自动化 |
|
||||||
|
| 成员退群时自动发一条通知 | 自动化 → 退群通知 |
|
||||||
|
| 把文字生成微信语音 | 文字转语音 |
|
||||||
|
| 在微信里向本机 TraceMemo 提问 | Agent Hub |
|
||||||
|
| 让 Codex 等工具查询微信历史 | Reader Skill / Local HTTP API |
|
||||||
|
| 把聊天保存成文件 | 导出 |
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
1. 从 [GitHub Releases](https://github.com/Wxw-Gu/TraceMemo/releases) 下载对应平台的安装包。
|
||||||
|
2. 启动应用,按“第一次使用”页面选择微信数据目录并完成连接。
|
||||||
|
3. 打开“档案”,确认联系人和消息已加载后开始搜索。
|
||||||
|
4. 需要 AI 时,在“设置 → AI 模型”添加并测试 Provider。
|
||||||
|
|
||||||
|
详细步骤见[第一次使用 TraceMemo](./docs/user-guide/getting-started.md)。
|
||||||
|
|
||||||
|
## 文档
|
||||||
|
|
||||||
|
- [用户指南](./docs/README.md#档案与搜索)
|
||||||
|
- [AI / Knowledge](./docs/README.md#ai-与知识库)
|
||||||
|
- [日报与自动化](./docs/README.md#日报与自动化)
|
||||||
|
- [Agent / API](./docs/README.md#agent--api)
|
||||||
|
- [开发文档](./docs/development/overview.md)
|
||||||
|
- [隐私与安全](./docs/user-guide/privacy.md)
|
||||||
|
|
||||||
|
完整目录由[文档首页](./docs/README.md)维护。
|
||||||
|
|
||||||
|
## 支持平台
|
||||||
|
|
||||||
|
| 平台 | 架构 | 微信连接 | 安装包 |
|
||||||
|
| ------- | ------------------------------ | ------------------------------------- | ------------------------------- |
|
||||||
|
| Windows | x64 | 支持微信 4.x | `tracememo-<version>-setup.exe` |
|
||||||
|
| macOS | Apple Silicon(M 系列、arm64) | 自动获取数据库 Key,已适配微信 4.1.13 | `tracememo-<version>-arm64.dmg` |
|
||||||
|
| macOS | Intel(x64) | 自动获取数据库 Key,已适配微信 4.1.13 | `tracememo-<version>-x64.dmg` |
|
||||||
|
|
||||||
|
## 参与贡献
|
||||||
|
|
||||||
|
稳定版在 `main`,只在发版时更新;所有改动都先进 `develop`,随**下一个版本**一起发布。
|
||||||
|
|
||||||
|
**提 PR 请基于 `develop` 拉新分支,并把 PR 的目标分支设为 `develop`** —— 指向 `main` 的 PR 会被直接关闭。
|
||||||
|
|
||||||
|
分支流程、提交信息风格、PR 前自检,以及**给 AI Agent 的硬性规则**,都在[参与贡献指南](./CONTRIBUTING.md)。
|
||||||
|
|
||||||
|
## 致谢
|
||||||
|
|
||||||
|
TraceMemo 的诞生离不开开源社区中许多优秀项目的工作。
|
||||||
|
|
||||||
|
### 特别感谢 WeFlow
|
||||||
|
|
||||||
|
TraceMemo 在早期适配微信 4.x 时,曾参考 **[WeFlow](https://github.com/hicccc77/WeFlow)** 历史版本中的相关实现和思路,包括数据库访问、密钥获取等底层能力。
|
||||||
|
|
||||||
|
特别感谢作者 **[hicccc77](https://github.com/hicccc77)**。项目与 WeFlow 的具体关系见[项目缘起](#项目缘起)。
|
||||||
|
|
||||||
|
### 其他参考项目
|
||||||
|
|
||||||
|
- **[WechatMessageExplorer](https://github.com/svcvit/WechatMessageExplorer)**
|
||||||
|
- 提供了数据解析相关思路。
|
||||||
|
|
||||||
|
- **[chatlog](https://github.com/sjzar/chatlog)**
|
||||||
|
- 提供了数据处理方面的参考。
|
||||||
|
|
||||||
|
- **[wechat_chatter](https://github.com/yincongcyincong/wechat_chatter)**
|
||||||
|
- 提供了发送方面的参考。
|
||||||
|
|
||||||
|
感谢所有开源作者,也感谢所有帮助 TraceMemo 发现问题、提出建议和持续使用它的人。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 最后说两句
|
||||||
|
|
||||||
|
这个项目起初只是一个一时兴起的项目,所以它大概也不会有一份特别严肃的产品路线图。
|
||||||
|
|
||||||
|
我可能会按照自己的兴趣继续折腾,也可能突然加入一些奇奇怪怪、但觉得有意思的功能
|
||||||
|
|
||||||
|
也因此,这个项目随时可能继续折腾,也可能因为其他事情暂时搁置。如果你有想要的功能,可以提Issue;如果觉得现有实现不符合你的需求,也欢迎直接 Fork 后自己改。
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<b>TraceMemo(迹忆)</b>
|
||||||
|
<br />
|
||||||
|
把微信聊过的事,找回来、问清楚、留下来。
|
||||||
|
</p>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
provider: github
|
||||||
|
owner: Wxw-Gu
|
||||||
|
repo: TraceMemo
|
||||||
|
releaseType: release
|
||||||
|
updaterCacheDirName: tracememo-updater
|
||||||
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 27 KiB |
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 获取当前版本号(从 package.json 读取)
|
||||||
|
VERSION=$(jq -r '.version' package.json)
|
||||||
|
|
||||||
|
# 格式化为新的 tag(在版本号前加上 "v")
|
||||||
|
TAG="v${VERSION}"
|
||||||
|
|
||||||
|
# 输出当前生成的 tag
|
||||||
|
echo "生成的 tag: $TAG"
|
||||||
|
|
||||||
|
# 创建并推送 tag 到远程
|
||||||
|
git tag $TAG
|
||||||
|
git push origin $TAG
|
||||||
|
|
||||||
|
echo "Tag $TAG 已经推送到远程仓库!"
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# TraceMemo 文档
|
||||||
|
|
||||||
|
文档按产品任务和使用场景组织。需要集成或开发时,再看 Agent、概念和开发文档。
|
||||||
|
|
||||||
|
## 档案与搜索
|
||||||
|
|
||||||
|
- [第一次使用](./user-guide/getting-started.md):安装、连接微信并完成第一次搜索。
|
||||||
|
- [Intel Mac 获取微信密钥](./user-guide/intel-mac-key.md):按页面检查结果准备环境并获取密钥。
|
||||||
|
- [聊天档案与搜索](./user-guide/chat-archive.md):浏览联系人和群聊,按关键词、备注、昵称、微信号或 wxid 查找消息;也包含档案中的文字转语音入口,以及群聊里的「群发言统计」。
|
||||||
|
|
||||||
|
## AI 与知识库
|
||||||
|
|
||||||
|
- [AI Search / 问问微信](./user-guide/ai-search.md):用自然语言找回记得大意、但不知道在哪个会话里的内容,并查看 Evidence、Citation 和 Search Trace。
|
||||||
|
- [本地知识库](./user-guide/knowledge.md):主动建立本地索引,提升跨会话、跨时间查询的稳定性;也包括在本机识别图片文字、让截图和公告图变得可搜索的「图片文字索引」。
|
||||||
|
- [如何核对 AI 的回答来源](./concepts/answer-sources.md):从来源回到原始消息,检查上下文和覆盖范围。
|
||||||
|
- [从微信数据到回答、日报和导出](./concepts/how-it-works.md):了解哪些步骤在本机完成,哪些 AI 功能可能调用 Provider。
|
||||||
|
|
||||||
|
## 日报与自动化
|
||||||
|
|
||||||
|
- [群聊日报](./user-guide/report.md):手动生成今日、昨日或近 7 天的群聊报告,也可以在「自动化」里创建定时日报。
|
||||||
|
- 「自动化」按三类规则执行:**@我生成日报**、**定时日报**、**退群通知**。定时日报会依次生成报告、保存 Report History,再按当前微信发送能力尝试通知;发送失败时可复用已有 PNG 重试。
|
||||||
|
- 自动发送和监控动作通过统一执行边界,并保留执行记录;简要说明见[产品工作方式](./concepts/how-it-works.md#动作执行与审计)。
|
||||||
|
|
||||||
|
## 退群监控
|
||||||
|
|
||||||
|
退群监控会比较当前成员与上一份有效快照,记录成员退出事件。它支持多群、Last Good Snapshot 和事件历史;监控关闭期间的变化不会在重新开启后补报。成员退出同时是「自动化 → 退群通知」的触发条件。工作方式见[产品工作方式](./concepts/how-it-works.md#退群监控)。
|
||||||
|
|
||||||
|
## 语音能力
|
||||||
|
|
||||||
|
- [语音转文字](./user-guide/voice.md):在本机转写微信语音,结果可用于搜索、Knowledge 和导出。
|
||||||
|
- [聊天档案与搜索](./user-guide/chat-archive.md#文字转语音):把文字生成微信语音,试听后发送到当前联系人或群聊;实际发送依赖本机发送能力。
|
||||||
|
|
||||||
|
## Agent / API
|
||||||
|
|
||||||
|
Agent Hub 让微信机器人调用本机 TraceMemo;Reader Skill / Local HTTP API 让外部 Agent 主动查询历史数据。
|
||||||
|
|
||||||
|
- [Agent 接入概览](./agent/overview.md)
|
||||||
|
- [Agent Hub](./agent/agent-hub.md)
|
||||||
|
- [Reader Skill](./agent/reader-skill.md)
|
||||||
|
- [Local HTTP API](./agent/api.md)
|
||||||
|
- [API 安全](./agent/api-security.md)
|
||||||
|
|
||||||
|
## 导出与隐私
|
||||||
|
|
||||||
|
- [导出聊天](./user-guide/export.md):导出 HTML、Markdown、CSV 或 JSON 档案。
|
||||||
|
- [数据、隐私与安全](./user-guide/privacy.md):本地处理、Provider、媒体和 Token 的数据边界。
|
||||||
|
- [常见问题与排查](./user-guide/troubleshooting.md):按安装、连接、AI、媒体和 Agent 现象排查。
|
||||||
|
|
||||||
|
## 开发文档
|
||||||
|
|
||||||
|
- [开发、测试与构建](./development/overview.md)
|
||||||
|
- [界面开发规范:按钮与主题色](./development/ui-guidelines.md)
|
||||||
|
- [微信系统消息解析与格式兼容](./development/wechat-system-message-parsing.md)
|
||||||
|
- [Query Agent POC(开发测试入口)](./development/query-agent-poc.md)
|
||||||
|
- [本地启动排障](./development/local-startup-troubleshooting.md)
|
||||||
|
- [macOS 数据访问说明](./platform/macos.md)
|
||||||
|
- [关闭 SIP 教程](./mac-disable-sip.md)
|
||||||
|
|
||||||
|
## 实验性功能与第三方
|
||||||
|
|
||||||
|
- [实验性:自托管微信分享卡片](./deployment/experimental-wechat-share-card.md)
|
||||||
|
- [微信分享卡片自动部署 Skill](./skill/setup-wechat-share-card/SKILL.md)
|
||||||
|
- [TraceMemo Reader Skill 文件](./skill/tracememo-reader/SKILL.md)
|
||||||
|
|
||||||
|
## 版本说明
|
||||||
|
|
||||||
|
- [v2.2.0 品牌与安全迁移](./agent/release-notes-v2.2.0.md)
|
||||||
|
- [v2.1.9 API 鉴权迁移](./agent/release-notes-v2.1.9.md)
|
||||||
|
|
||||||
|
文档按当前 develop 已实现的能力维护,不在首页固定写死版本号。版本兼容性、AI Provider 行为和媒体读取结果可能随系统、微信客户端和服务商变化。
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# 在微信里向 TraceMemo 提问(Agent Hub)
|
||||||
|
|
||||||
|
Agent Hub 是 TraceMemo 内置的微信机器人入口,也是应用一级导航中的“Agent”页面。你先扫码登录一个微信机器人账号,再用微信账号向机器人发送文字;本机 Agent Hub 会接收消息、读取已经连接的微信数据,必要时调用已配置的 AI,再把结果回复给发送者。
|
||||||
|
|
||||||
|
普通用户不需要安装 Reader Skill,也不需要配置 API Token。先连接微信数据库,再扫码登录机器人即可开始;需要总结或自然语言理解的任务还要配置 AI Provider。
|
||||||
|
|
||||||
|
它和 Reader Skill 是两条不同的路径:
|
||||||
|
|
||||||
|
- Reader Skill / Local HTTP API:外部 Agent 主动查询历史微信数据;
|
||||||
|
- Agent Hub / 微信机器人:机器人收到实时消息后处理并回复。
|
||||||
|
|
||||||
|
## 连接器和 Agent Hub 是什么关系
|
||||||
|
|
||||||
|
你不需要单独部署这些组件。扫码后,后台的微信连接器负责登录机器人、保持连接、接收微信消息和发送回复;Agent Hub 负责判断消息要做什么、查询 TraceMemo 本地数据、调用 AI 并组织结果。可以把它理解为:连接器负责“和微信通信”,Hub 负责“处理任务”。
|
||||||
|
|
||||||
|
## 你能做什么
|
||||||
|
|
||||||
|
连接 Agent Hub 后,可以在微信中询问:
|
||||||
|
|
||||||
|
- “最近 5 个会话”;
|
||||||
|
- “帮我看看最近跟某人聊了些什么。”
|
||||||
|
- “生成产品交流群今天的群聊总结图片。”
|
||||||
|
|
||||||
|
Hub 把入站文字分成两类处理。
|
||||||
|
|
||||||
|
**确定性的快捷动作**(不经过模型,命中就执行):
|
||||||
|
|
||||||
|
- 查看最近会话:数量限制为 1–20;
|
||||||
|
- 生成群聊总结图片:今天、昨天或近 7 天(需要同时提到“群”和“图片 / 长图 / 日报 / 报告”);
|
||||||
|
- 分析某个群成员的近期发言:可以指定“今天 / 昨天 / 最近 N 天”。
|
||||||
|
|
||||||
|
**其余问题**交给本机的 Query Agent:它和桌面端“问问微信”使用的是同一个实现,可以按需读取联系人、会话和时间范围来回答,必要时调用你在“设置 → AI 模型”里配置的 AI。例如“帮我看看最近跟某人聊了些什么”“上个月讨论过的项目地址在哪里”。
|
||||||
|
|
||||||
|
任务完成后,回复会发送回触发这次请求的微信用户。群聊总结会先发送进度提示,完成后发送图片。
|
||||||
|
|
||||||
|
这些任务会在后台查询联系人、群聊和聊天记录,但当前机器人没有单独的“列出所有联系人”或“列出所有群聊”命令;需要完整浏览或按条件查询时,请使用档案页面或 Reader Skill / Local HTTP API。
|
||||||
|
|
||||||
|
## 连接步骤
|
||||||
|
|
||||||
|
1. 打开应用主导航中的“Agent”;页面标题为“Agent Hub”。
|
||||||
|
2. 确认 Hub 显示“运行中”,数据库状态为“可查询”。
|
||||||
|
3. 点击“扫码登录微信机器人”。
|
||||||
|
4. 用微信扫描二维码;如果页面显示“已扫码,等待手机确认”,在手机上确认。
|
||||||
|
5. 状态变为“在线”后,用另一个微信账号向机器人发送测试问题。
|
||||||
|
|
||||||
|
可以重新扫码登录或断开连接。登录凭证失效时,需要重新扫码。
|
||||||
|
|
||||||
|
## 运行日志
|
||||||
|
|
||||||
|
Agent Hub 页面会记录系统、Agent Hub 和微信连接器日志。日志支持筛选、复制和清空,并会隐藏 Token 和二维码数据,不记录微信密码。
|
||||||
|
|
||||||
|
## 需要满足的条件
|
||||||
|
|
||||||
|
- TraceMemo 的微信数据库已经连接,并且数据 API 可以查询;
|
||||||
|
- 依赖总结或自然语言理解的任务,需要在“设置 → AI 模型”配置可用的 AI 服务;
|
||||||
|
- TraceMemo 和 Agent Hub 需要保持运行,机器人才能接收和回复消息。
|
||||||
|
|
||||||
|
## 安全与边界
|
||||||
|
|
||||||
|
- Hub 在主进程内运行,不开放本地监听端口;它不会把数据库暴露到公网;
|
||||||
|
- 机器人账号和个人微信账号是不同的登录边界,请确认你连接的是正确账号;
|
||||||
|
- 机器人回复会发送给当前发消息的人;开发者 API 另有受保护的测试发送入口,使用前必须确认接收者;
|
||||||
|
- Hub 理解请求或生成总结时,会调用你在“设置 → AI 模型”配置的 Provider;
|
||||||
|
- 当前实时入口只处理文字消息。连接器会归一化收到的消息条目,但 Agent Hub 只把文本条目当作意图处理,尚未为图片、语音、文件和视频提供同等能力;
|
||||||
|
- 当前没有实现群发、广播、定时任务或通用自主操作微信(定时日报属于「自动化」,不是 Agent Hub);
|
||||||
|
- 本页面的“Agent Hub 状态”可以通过 Local HTTP API 查询,但不要把它误认为外部 Agent 的实时消息订阅接口或 MCP Server。
|
||||||
|
|
||||||
|
## 无法连接时
|
||||||
|
|
||||||
|
先检查 Hub、连接器和数据库三项状态,再查看日志。二维码过期、连接器不存在、凭证失效和数据 API 未就绪分别需要重新扫码、修复安装、重新登录或先完成微信数据库连接。
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Local HTTP API 安全
|
||||||
|
|
||||||
|
## 当前安全边界
|
||||||
|
|
||||||
|
TraceMemo 的本地 API 默认监听 `127.0.0.1:6131`。它面向同一台电脑上的 API Center、Reader Skill、CLI 和 Agent,不是公网网关,也不是带用户账户和细粒度权限 Scope 的服务。
|
||||||
|
|
||||||
|
它同时包含**写入型**端点:生成报告并渲染 PNG(`/report`)、通过已连接机器人发送微信消息(`/agent/send`)、创建/修改/删除/启停定时日报任务并触发立即执行(`/scheduled-reports*`)。因此这个 Token 相当于本机敏感凭据,而不是一个只读查询键。
|
||||||
|
|
||||||
|
## Bearer Token
|
||||||
|
|
||||||
|
新 Agent 配置使用 `TRACEMEMO_API_TOKEN`。历史变量名 `WECHATEXPLORER_API_TOKEN` 仍被兼容读取,优先级为新变量高于旧变量;当前没有设定旧变量名的移除时间,新配置不要再使用它。
|
||||||
|
|
||||||
|
- `/api/v1/health` 是公开健康检查;
|
||||||
|
- 其他所有端点都要求 `Authorization: Bearer <TOKEN>`;
|
||||||
|
- Token 由应用生成,使用 32 个随机字节编码;
|
||||||
|
- Token 由 Electron `safeStorage` 加密保存在用户数据目录的 `local-api-token.bin`;
|
||||||
|
- 文件权限设置为 `0600`;
|
||||||
|
- 在“API Center”中可以显示、复制和重新生成;
|
||||||
|
- 重新生成后旧 Token 立即失效;
|
||||||
|
- 服务端只认这个 Token,**不接受用环境变量覆盖**——Agent 一侧的环境变量只是把 Token 交给 Agent 自己的方式,不是鉴权来源。
|
||||||
|
|
||||||
|
应用不会自动把 Token 写入 Codex、Claude Code、OpenClaw 或其他 Agent 配置。请把它放进 Agent 自己的本地 secret/environment,例如:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export TRACEMEMO_API_TOKEN="<TOKEN>"
|
||||||
|
```
|
||||||
|
|
||||||
|
## CORS 与 Origin
|
||||||
|
|
||||||
|
带浏览器 `Origin` 的请求只允许精确的 HTTP loopback Origin:
|
||||||
|
|
||||||
|
- `http://localhost` 及其端口;
|
||||||
|
- `http://127.0.0.1` 及其端口;
|
||||||
|
- `http://[::1]` 及其端口。
|
||||||
|
|
||||||
|
不带 `Origin` 的 curl、Node、本地脚本和 Agent 请求不受浏览器 CORS 规则限制,但仍必须携带 Token(health 除外)。
|
||||||
|
|
||||||
|
## 不要做的事
|
||||||
|
|
||||||
|
- 不要把 Token 放入 URL query、日志、截图、公开 Skill 或 Git;
|
||||||
|
- 不要把服务反向代理到公网;
|
||||||
|
- 不要把“health 能访问”误认为数据端点无需授权;
|
||||||
|
- 不要把 Bearer Token 当成跨用户权限系统;当前服务没有细粒度 Scope;
|
||||||
|
- 不要在共享机器上让不可信进程继承 Token 环境变量。
|
||||||
|
|
||||||
|
## Token 不可用时
|
||||||
|
|
||||||
|
如果系统安全存储不可用,API Token 会无法生成或读取,本地 API 会安全停用。先修复系统钥匙串/凭据服务,再回到 API Center 重试。不要手动编辑 `local-api-token.bin`。
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [Agent 接入概览](./overview.md)
|
||||||
|
- [Reader Skill](./reader-skill.md)
|
||||||
|
- [数据、隐私与安全](../user-guide/privacy.md)
|
||||||
|
- [v2.1.9 鉴权迁移说明](./release-notes-v2.1.9.md)
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
# TraceMemo Local HTTP API
|
||||||
|
|
||||||
|
本文面向需要自己写集成的开发者。普通用户请先阅读[Agent 接入概览](./overview.md)。
|
||||||
|
|
||||||
|
## 基本信息
|
||||||
|
|
||||||
|
- 默认地址:`http://127.0.0.1:6131`
|
||||||
|
- API 前缀:`/api/v1`
|
||||||
|
- 默认只监听 loopback;不要把它当作公网服务。
|
||||||
|
- `/api/v1/health` 无需 Token;其他端点需要 `Authorization: Bearer <TOKEN>`。
|
||||||
|
- 请求体使用 JSON,单个请求体最大 `1 MiB`;超限返回 `413`。
|
||||||
|
- 错误响应包含 `requestId`,响应头包含 `X-Request-Id`。客户端可传入 1-128 位的 `[A-Za-z0-9._:-]` 标识,否则服务端会生成 UUID。
|
||||||
|
- 不支持的 HTTP method 返回 `405` 和 `Allow` 响应头。
|
||||||
|
|
||||||
|
## 最小请求
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 健康检查
|
||||||
|
curl http://127.0.0.1:6131/api/v1/health
|
||||||
|
|
||||||
|
# 读取数据
|
||||||
|
export TRACEMEMO_API_TOKEN="<从 API Center 复制的 Token>"
|
||||||
|
curl -H "Authorization: Bearer $TRACEMEMO_API_TOKEN" \
|
||||||
|
"http://127.0.0.1:6131/api/v1/recent_chat?limit=20"
|
||||||
|
```
|
||||||
|
|
||||||
|
不要把 Token 放入 URL、Skill 文件、仓库或命令历史可被共享的脚本中。
|
||||||
|
|
||||||
|
新配置必须优先使用 `TRACEMEMO_API_TOKEN`。应用生成的安装指令仍会提示:尚未升级的旧配置可以继续读取 `WECHATEXPLORER_API_TOKEN`,但新配置必须使用新变量名;如果两个变量都存在,以新变量为准。当前没有设定旧变量名的移除时间。
|
||||||
|
|
||||||
|
Token 由应用生成并保存在本机,**不接受用环境变量覆盖**:Agent 侧的环境变量只是把 Token 传给 Agent 自己的方式,不是服务端的鉴权来源。
|
||||||
|
|
||||||
|
## 端点
|
||||||
|
|
||||||
|
| 方法 | 路径 | 作用 | 参数/请求体 |
|
||||||
|
| ------ | --------------------------------------------------------------- | -------------------------------------- | --------------------------------------------------------------- |
|
||||||
|
| GET | `/api/v1/health` | 服务与数据库健康状态 | 无 |
|
||||||
|
| GET | `/api/v1/current_time` | 本机时间、时区和 Unix 时间戳 | 无 |
|
||||||
|
| GET | `/api/v1/contact` | 联系人和群聊列表 | `filter`、`type=user\|group` |
|
||||||
|
| GET | `/api/v1/chatroom` | 群聊列表 | `keyword` |
|
||||||
|
| GET | `/api/v1/recent_chat` | 最近会话 | `limit`,默认 50 |
|
||||||
|
| GET | `/api/v1/chatlog` | 指定会话的聊天记录 | 必填 `talker`;可选 `time` 或 `startTime`/`endTime` |
|
||||||
|
| GET | `/api/v1/media/{mediaId}` | 获取图片消息的二进制资源 | 原样使用 `/chatlog` 返回的 `media.url`,不要用消息 `id` 拼接 |
|
||||||
|
| GET | `/api/v1/group_snapshot` | 群成员快照 | 必填 `md5` |
|
||||||
|
| GET | `/api/v1/resolve` | 将昵称、wxid 或 md5 解析为会话 | 必填 `q` |
|
||||||
|
| POST | `/api/v1/report` | 将结构化日报渲染为 HTML 与 PNG | `GroupReportExportRequest` JSON |
|
||||||
|
| GET | `/api/v1/agent/status` | Agent Hub、连接器和数据库状态 | 无 |
|
||||||
|
| POST | `/api/v1/agent/group-report` | 读取群聊并生成总结图片 | `{ "group": "群名或标识", "range": "today\|yesterday\|7days" }` |
|
||||||
|
| POST | `/api/v1/agent/send` | 通过已连接机器人测试发送文字或本地图片 | `{ "to": "接收者", "text": "...", "media_url": "..." }` |
|
||||||
|
| GET | `/api/v1/wechat-personal/send-capability` | 个人微信发送能力状态 | 无 |
|
||||||
|
| GET | `/api/v1/scheduled-reports` | 定时日报任务列表 | 无 |
|
||||||
|
| POST | `/api/v1/scheduled-reports` | 创建定时日报任务 | `ScheduledReportApiCreateRequest` JSON |
|
||||||
|
| GET | `/api/v1/scheduled-reports/{id}` | 查询单个定时日报任务 | 无 |
|
||||||
|
| PATCH | `/api/v1/scheduled-reports/{id}` | 修改定时日报任务 | `ScheduledReportApiUpdateRequest` JSON |
|
||||||
|
| DELETE | `/api/v1/scheduled-reports/{id}` | 删除定时日报任务 | 无 |
|
||||||
|
| POST | `/api/v1/scheduled-reports/{id}/enable` | 启用定时日报任务 | 无 |
|
||||||
|
| POST | `/api/v1/scheduled-reports/{id}/disable` | 暂停定时日报任务 | 无 |
|
||||||
|
| POST | `/api/v1/scheduled-reports/{id}/run` | 立即执行一次并返回 execution | 无 |
|
||||||
|
| GET | `/api/v1/scheduled-reports/{id}/executions` | 查询某个任务的执行记录 | 无 |
|
||||||
|
| POST | `/api/v1/scheduled-reports/executions/{executionId}/retry-send` | 兼容占位路由;当前返回 `501 not_supported` | 无 |
|
||||||
|
| GET | `/api/v1/capabilities` | TraceMemo 应用能力和可用状态 | 无 |
|
||||||
|
| GET | `/api/v1/automations` | 自动化规则列表 | 可选 `type`、`enabled` |
|
||||||
|
| POST | `/api/v1/automations` | 创建默认停用的自动化规则 | Automation draft JSON |
|
||||||
|
| POST | `/api/v1/automations/validate` | 校验规则,不保存、不执行 | Automation draft JSON |
|
||||||
|
| GET | `/api/v1/automations/{id}` | 查询单条自动化规则 | 无 |
|
||||||
|
| PATCH | `/api/v1/automations/{id}` | 更新规则配置 | 可变配置字段 JSON |
|
||||||
|
| DELETE | `/api/v1/automations/{id}` | 删除自定义规则 | 系统内置规则受保护 |
|
||||||
|
| POST | `/api/v1/automations/{id}/enable` | 校验并启用规则 | 无 |
|
||||||
|
| POST | `/api/v1/automations/{id}/disable` | 停用规则 | 无 |
|
||||||
|
| GET | `/api/v1/automations/executions` | 查询自动化执行记录 | `ruleId`、`status`、`since`、`until`、`limit` |
|
||||||
|
| GET | `/api/v1/monitors/group-exits` | 查看退群监控状态 | 无 |
|
||||||
|
| PATCH | `/api/v1/monitors/group-exits` | 配置监控群范围或启停 | `enabled`、`monitoredConversationIds` |
|
||||||
|
| GET | `/api/v1/monitors/group-exits/events` | 查询退群事件历史 | `conversationId`、`since`、`until`、`limit` |
|
||||||
|
| GET | `/api/v1/groups/{conversationId}/member-stats` | 查询群成员活跃统计 | 必填 `conversationId`、`start`、`end` |
|
||||||
|
|
||||||
|
`/api/v1/query/*` 是一组结构化的 Query 端点,见下方[LLM-friendly Query Tool API](#llm-friendly-query-tool-api)。
|
||||||
|
|
||||||
|
## Application Capabilities
|
||||||
|
|
||||||
|
`GET /api/v1/capabilities` 描述 TraceMemo 应用级能力和当前运行环境;`GET /api/v1/query/capabilities` 只描述结构化 Query primitive,两者不是同一份目录。应用能力使用 `supported` 和 `available` 分开表示“代码支持”与“当前可用”;运行时原因使用稳定的简短 code,不返回 Token、数据库路径、微信密钥或 sender 诊断路径。
|
||||||
|
|
||||||
|
响应包含应用版本、数据库 readiness、Query、Automation、退群监控、群统计,以及个人微信/iLink 的能力状态。`groupExitMonitor.operations` 当前声明 `read_state`、`configure_scope`、`enable`、`disable`、`list_events`;`groupStats.operations` 当前声明 `member_stats`。能力声明不会触发监控扫描或群统计查询。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
: "${TRACEMEMO_API_TOKEN:?Set TRACEMEMO_API_TOKEN from API Center}"
|
||||||
|
BASE="http://127.0.0.1:6131/api/v1"
|
||||||
|
AUTH="Authorization: Bearer $TRACEMEMO_API_TOKEN"
|
||||||
|
curl -H "$AUTH" "$BASE/capabilities"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Group Exit Monitor API
|
||||||
|
|
||||||
|
退群监控只负责“监测哪些群、发现了哪些退群事实”。退群后是否通知、通知到哪里以及通知模板,仍由 `leave_notification` Automation singleton 负责;修改监控范围不会隐式修改该 Automation。
|
||||||
|
|
||||||
|
### 查看和配置监控
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "$AUTH" "$BASE/monitors/group-exits"
|
||||||
|
|
||||||
|
curl -X PATCH -H "$AUTH" -H 'Content-Type: application/json' \
|
||||||
|
"$BASE/monitors/group-exits" \
|
||||||
|
-d '{"enabled":true,"monitoredConversationIds":["123@chatroom"]}'
|
||||||
|
```
|
||||||
|
|
||||||
|
`monitoredConversationIds` 只接受当前联系人列表中精确存在的群 `roomId`(例如 `xxx@chatroom`),不接受群名、md5、个人联系人、重复或空 ID。请求至少提供 `enabled` 或 `monitoredConversationIds` 其中一个;传空数组表示清空监控范围。服务会先校验全部群,再执行一次原子配置。PATCH 返回最终完整状态。
|
||||||
|
|
||||||
|
状态中的 `eventCount` 是持久化退群事件总数,`lastCheckedAt`/`lastReadAt` 为空时返回 `null`。GET 不会调用 `checkNow()`,也不会触发通知发送。
|
||||||
|
|
||||||
|
### 查询退群事件
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -G -H "$AUTH" "$BASE/monitors/group-exits/events" \
|
||||||
|
--data-urlencode 'conversationId=123@chatroom' \
|
||||||
|
--data-urlencode 'since=2026-10-01T00:00:00+07:00' \
|
||||||
|
--data-urlencode 'until=2026-10-02T23:59:59+07:00' \
|
||||||
|
--data-urlencode 'limit=50'
|
||||||
|
```
|
||||||
|
|
||||||
|
时间参数必须是带 offset 的 ISO-8601;默认 `limit=50`,最大 200。事件按 `detectedAt` 升序返回。事件 DTO 使用 `eventId`、稳定的 `conversationId` 和 `memberId`,并把时间输出为 ISO-8601;当前整体已读状态不会伪造成 event-level `read` 字段。当前未开放 clear events、markRead 或 checkNow HTTP 路由。
|
||||||
|
|
||||||
|
一个典型 Agent 工作流是:先通过 `/resolve` 或 `/contact` 找到稳定群 ID,再 PATCH monitor scope;如需通知,再单独 PATCH `leave_notification` Automation,调用 `/automations/validate`,最后启用规则。
|
||||||
|
|
||||||
|
## Group Member Stats API
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -G -H "$AUTH" "$BASE/groups/123%40chatroom/member-stats" \
|
||||||
|
--data-urlencode 'start=2026-09-01T00:00:00+07:00' \
|
||||||
|
--data-urlencode 'end=2026-10-01T00:00:00+07:00'
|
||||||
|
```
|
||||||
|
|
||||||
|
`conversationId` 必须是当前联系人列表中精确存在的群 `roomId`;不存在返回 `NOT_FOUND`,个人联系人返回 `NOT_GROUP_CONVERSATION`。`start` 和 `end` 必须同时提供,且使用带 offset 的 ISO-8601,`start` 不能晚于 `end`。HTTP adapter 只负责把稳定群 ID 解析为内部 md5 并调用现有 `GroupStatsService`,不会在 HTTP 层重新统计消息。
|
||||||
|
|
||||||
|
响应中的 `activeMembers` 和 `silentMembers` 都只描述当前成员名单;成员使用 `memberId`,活跃成员的 `lastMessageAt` 和 `range` 时间均为 ISO-8601。`freshness`、`complete`、`limitations` 必须原样保留,`unattributedMessages` 与 `excludedSystemMessages` 用于诊断,`firstMessageAt` 没有消息时为 `null`。当前成员统计不等于完整历史成员统计,`limitations` 表达的退群成员或未归档时段不能从文本中推导成额外的 `formerMembers`,也不会伪造 `totalMessageCount`。
|
||||||
|
|
||||||
|
## Automation API
|
||||||
|
|
||||||
|
`/api/v1/automations*` 是 Automation 的 canonical HTTP API,读写唯一的 `AutomationRuleStore`。它支持当前真实规则类型:`daily_report`、`scheduled_report`、`leave_notification`。本 API 不提供立即执行、重试或清理执行记录。
|
||||||
|
|
||||||
|
旧 `/api/v1/scheduled-reports*` 保持兼容,不设移除日期;它是面向旧 DTO 的受限 compatibility API,不是第二份存储,也不能表示所有新的定时日报目标和配置。新的 Agent 集成应使用 `/automations`。
|
||||||
|
|
||||||
|
创建和校验规则时 `enabled` 只能缺省或为 `false`。创建成功后必须调用 `/automations/{id}/enable` 才会启用。启用会重新校验当前规则;数据库未就绪、目标无法解析或配置无效时不会启用。`PATCH` 只接受规则配置字段,不可改 `ruleType`、`id`、创建/更新时间或 `enabled`;启停必须使用独立 endpoint。未知字段和未知枚举会被拒绝。
|
||||||
|
|
||||||
|
会话范围优先传 `wxid`、`roomId`(如 `xxx@chatroom`)或 canonical conversation ID。唯一匹配的联系人名可被解析为稳定 ID;重名会返回 `ambiguous_contact`,不会猜测。`daily_report.conditions.conversationIds` 在对外 API 中使用稳定 ID,Store 内部仍沿用既有 md5 口径。
|
||||||
|
|
||||||
|
校验请求不落盘、不发消息,也不执行规则。`valid: false` 时查看 `issues`;有效时 `normalized` 是经 ID 解析后的草稿,`effects` 描述启用后的动作,定时日报另外返回按本机时区计算的 `nextRunAt`。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "产品群每日日报",
|
||||||
|
"ruleType": "scheduled_report",
|
||||||
|
"scheduledReport": {
|
||||||
|
"schedule": { "time": "20:00" },
|
||||||
|
"report": {
|
||||||
|
"sourceConversationId": "wxid_product@chatroom",
|
||||||
|
"range": "today",
|
||||||
|
"messageTypes": ["text", "image"],
|
||||||
|
"templateId": "v1",
|
||||||
|
"memberNameMode": "groupNickname",
|
||||||
|
"timeoutSeconds": 300
|
||||||
|
},
|
||||||
|
"target": { "type": "file_transfer" },
|
||||||
|
"postfixText": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
执行历史只读,默认最多返回 50 条,`limit` 范围是 1-200。`since` 和 `until` 接受带时区的 ISO-8601 时间;execution 本身最多留存 200 条。`running` 记录的 `finishedAt` 为 `null`。
|
||||||
|
|
||||||
|
新 Agent API 的错误格式:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"code": "VALIDATION_FAILED",
|
||||||
|
"message": "自动化规则校验失败",
|
||||||
|
"details": []
|
||||||
|
},
|
||||||
|
"requestId": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
常见错误码包括 `UNAUTHORIZED`、`METHOD_NOT_ALLOWED`、`PAYLOAD_TOO_LARGE`、`INVALID_ARGUMENT`、`NOT_FOUND`、`NOT_GROUP_CONVERSATION`、`DATABASE_NOT_READY`、`VALIDATION_FAILED`、`SINGLETON_RULE`、`PROTECTED_RULE` 和 `PERSISTENCE_FAILED`。`leave_notification` 是固定单例:可读取、修改和启停,但不能创建第二条或删除。内置 `@我生成日报` 同样不能通过 HTTP 删除。
|
||||||
|
|
||||||
|
### 这些端点与实时机器人有什么关系
|
||||||
|
|
||||||
|
- `/api/v1/agent/status` 只用于查询 Agent Hub、微信连接器和数据库状态;
|
||||||
|
- `/api/v1/agent/group-report` 由外部 Agent 或脚本主动请求生成群聊总结图片;
|
||||||
|
- `/api/v1/agent/send` 是受 Bearer Token 保护的开发者/测试发送入口,用于通过已经连接的机器人发送文字或本地图片;它不是任意群发能力,也不是实时消息订阅接口;
|
||||||
|
- `/api/v1/scheduled-reports*` 会**写入**应用状态:创建、修改、删除、启停定时日报任务,以及立刻执行一次。加上 `/report` 和 `/agent/send`,这个 API 并非只读接口——拿到 Token 就能改配置、生成报告并发送微信消息,请按本机敏感凭据对待;
|
||||||
|
- `POST /api/v1/scheduled-reports/{id}/run` 与定时触发共用同一条链路:读取群聊 → 生成报告 → 保存 Report History → 尝试发送;
|
||||||
|
- 当前 API 没有对外暴露实时入站 webhook。微信消息由应用内部的 Agent Hub 和微信连接器接收、处理和回复。
|
||||||
|
|
||||||
|
## 时间查询
|
||||||
|
|
||||||
|
`chatlog` 的 `time` 支持:
|
||||||
|
|
||||||
|
- `YYYY-MM-DD`:当天;
|
||||||
|
- `YYYY-MM-DD~YYYY-MM-DD`:日期闭区间;
|
||||||
|
- `YYYY-MM-DD/HH:mm`:从该分钟开始的 60 秒;
|
||||||
|
- 也可以使用 Unix 秒级 `startTime` 和 `endTime`。
|
||||||
|
|
||||||
|
时间按运行 TraceMemo 的本机时区解析。用户说“今天”“昨天”时,先调用 `current_time`,再根据返回的 `localDate` 计算日期,避免使用 Agent 自己的时区。
|
||||||
|
|
||||||
|
## 常用工作流
|
||||||
|
|
||||||
|
### 查找并读取一个会话
|
||||||
|
|
||||||
|
```bash
|
||||||
|
BASE="http://127.0.0.1:6131/api/v1"
|
||||||
|
AUTH="Authorization: Bearer ${TRACEMEMO_API_TOKEN:-$WECHATEXPLORER_API_TOKEN}"
|
||||||
|
|
||||||
|
curl -H "$AUTH" "$BASE/resolve?q=技术交流群"
|
||||||
|
curl -H "$AUTH" "$BASE/chatlog?talker=技术交流群&time=2026-08-07"
|
||||||
|
```
|
||||||
|
|
||||||
|
当标识不确定时,先用 `resolve` 或 `contact`,再调用 `chatlog`。对重要问题,先宽范围定位,再针对关键时间点读取前后文,不要只凭一次粗查回答。
|
||||||
|
|
||||||
|
### 生成群聊总结图片
|
||||||
|
|
||||||
|
优先使用 `/api/v1/agent/group-report`,因为它会读取指定群聊并按 `today`、`yesterday` 或 `7days` 生成总结。`/api/v1/report` 是更底层的渲染接口,要求调用方已经准备好 `report` 和 `metadata` 结构;完整 TypeScript 类型以 `src/shared/group-report.ts` 为准。
|
||||||
|
|
||||||
|
## 响应与错误
|
||||||
|
|
||||||
|
- `200`:请求成功;
|
||||||
|
- `201`:定时日报任务创建成功;
|
||||||
|
- `401`:缺少、错误或已失效的 Bearer Token;
|
||||||
|
- `400`:参数或 JSON 请求体无效;
|
||||||
|
- `422`:媒体标识格式错误,或目标消息不是可读取的图片(`NOT_IMAGE`);
|
||||||
|
- `403`:浏览器 Origin 不在允许的 loopback 列表;
|
||||||
|
- `409`:定时日报任务重复(`error === "duplicate"`,响应里会带回已存在的任务),或群聊名称匹配到多个目标(`ambiguous_contact`);
|
||||||
|
- `404`:端点、会话或群聊不存在;媒体标识未登记、已过期、有歧义,或图片文件不存在(`NOT_FOUND`)。媒体请求遇到此状态时,先重新读取 `/chatlog` 并使用新的 `media.url`;若仍失败,再检查本地图片文件是否存在;
|
||||||
|
- `503`:数据库或 Agent Hub 尚未就绪;
|
||||||
|
- `500`:服务端处理或报告渲染失败。
|
||||||
|
|
||||||
|
成功响应会返回端点对应的 JSON 对象,例如 `chatlog` 包含 `contact`、`query`、`count` 和 `messages`,`contact` 返回 `count` 与 `contacts`。
|
||||||
|
|
||||||
|
图片消息在 `messages` 中保留原有字段,并额外提供 `media`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "图片",
|
||||||
|
"content": "",
|
||||||
|
"media": {
|
||||||
|
"type": "image",
|
||||||
|
"available": true,
|
||||||
|
"url": "/api/v1/media/image%3A0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
当用户要求查看或理解图片时,使用 `media.url` 获取 `image/jpeg`、`image/png` 等真实二进制;不要根据 `[图片]` 猜测内容,也不要向 API 传入本地路径。
|
||||||
|
|
||||||
|
`media.url` 包含当前数据库连接内的独立媒体标识,不等同于消息 `id`。不同会话的消息 `id` 可能重复,调用方应原样使用返回的地址,不自行拼接或解析。重启、重连或切换账号后须重新读取 `/chatlog` 获取新地址;旧的纯消息 ID 地址仅在无歧义时兼容。`available` 只表示消息带有图片定位信息,不保证本地图片文件仍存在或可以解密。
|
||||||
|
|
||||||
|
## 与 MCP 的关系
|
||||||
|
|
||||||
|
当前实现没有把 `6131` 暴露为 MCP Server。需要在 Agent 中使用时,请安装随应用提供的 Reader Skill,并让 Skill 通过普通 HTTP 请求调用本 API。
|
||||||
|
|
||||||
|
## LLM-friendly Query Tool API
|
||||||
|
|
||||||
|
这些端点提供稳定的结构化 Query primitive,不接收自然语言问题,也不会调用 AI。它们与现有 API 共用端口、Bearer Token、loopback 和 CORS 安全策略。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
BASE="http://127.0.0.1:6131/api/v1"
|
||||||
|
AUTH="Authorization: Bearer ${TRACEMEMO_API_TOKEN:-$WECHATEXPLORER_API_TOKEN}"
|
||||||
|
|
||||||
|
# 能力目录
|
||||||
|
curl -H "$AUTH" "$BASE/query/capabilities"
|
||||||
|
|
||||||
|
# BOBO 的第一条真实互动
|
||||||
|
curl -X POST -H "$AUTH" -H 'Content-Type: application/json' "$BASE/query/messages" \
|
||||||
|
-d '{"target":{"query":"BOBO"},"timeRange":{"kind":"all"},"direction":"any","order":"asc","limit":1,"excludeSystem":true}'
|
||||||
|
|
||||||
|
# 上个月 BOBO 发来的文件
|
||||||
|
curl -X POST -H "$AUTH" -H 'Content-Type: application/json' "$BASE/query/messages" \
|
||||||
|
-d '{"target":{"query":"BOBO"},"timeRange":{"kind":"previous_month"},"direction":"from_target","messageTypes":["file"],"order":"desc","limit":1}'
|
||||||
|
|
||||||
|
# 受限语义关键词检索(最多 4 个 variants)
|
||||||
|
curl -X POST -H "$AUTH" -H 'Content-Type: application/json' "$BASE/query/search" \
|
||||||
|
-d '{"target":{"query":"BOBO"},"timeRange":{"kind":"all"},"query":"答应之后给我或者帮我完成某件事情","variants":["我给你","我发你","弄好给你"],"limit":20}'
|
||||||
|
|
||||||
|
# 按会话和时间范围提取可供总结的证据
|
||||||
|
curl -X POST -H "$AUTH" -H 'Content-Type: application/json' "$BASE/query/conversation-overview" \
|
||||||
|
-d '{"target":{"query":"BOBO"},"timeRange":{"kind":"previous_month"}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
`query/messages` 的 `messageRef` 是服务端生成的不透明引用,可直接传给 `query/message-context` 获取前后文;不要自行构造 wxid、md5 或数据库路径。
|
||||||
|
|
||||||
|
每条消息都会返回 `messageType`(`text`、`image`、`voice`、`video`、`file`、`link`、`sticker`、`system` 或 `other`)。非文本消息不会伪造 `text`;可识别的图片、视频、贴纸和文件会返回不含密钥或本地路径的 `attachment` 元数据。
|
||||||
|
|
||||||
|
`conversation-overview` 同时返回 `sourceCoverage` 与 `selection`:前者描述时间范围内源消息是否完整及 `sourceMessageCount`,后者描述从源消息中选出的 Evidence 数量及是否抽样。`evidence` 最终按 `timestamp` 升序返回,`messageRef` 是唯一推荐的消息引用。
|
||||||
|
`conversation-overview` 另有一个 `origin` 字段:`wcdb` 表示这次证据直接来自本机聊天数据库(会话概览的事实来源),`knowledge` 表示来自本地索引。
|
||||||
|
|
||||||
|
### 搜索范围(scope)
|
||||||
|
|
||||||
|
`query/messages`、`query/search`、`query/message-context` 和 `query/conversation-overview` 都接受一个可选的 `scope`,用来把检索限制在一个确定的语料边界内:
|
||||||
|
|
||||||
|
| scope | 含义 |
|
||||||
|
| ----------------------------------------- | -------------------------------------------------------------------- |
|
||||||
|
| `{"kind":"all"}` | 所有可读会话(默认;省略 `scope` 等价于此) |
|
||||||
|
| `{"kind":"groups"}` | 只搜群聊语料,**且包含群成员实际发送的消息**(不是群名称或群元数据) |
|
||||||
|
| `{"kind":"contact","conversationId":"…"}` | 只搜该一对一会话 |
|
||||||
|
| `{"kind":"current","conversationId":"…"}` | 只搜指定的那个会话(单聊或群聊) |
|
||||||
|
|
||||||
|
`conversationId` 是会话标识,可用 `/api/v1/resolve` 或 `/api/v1/contact` 得到。`scope` 一旦给出就是**权威边界**:`target` 落在范围之外会被拒绝(`status: "invalid_tool_arguments"`、`constraint: "target_outside_scope"`),不会静默扩大范围;范围里包含多个会话时,`query/messages` 与 `query/conversation-overview` 必须显式指定 `target`(`constraint: "target_required_for_scope"`)。
|
||||||
|
|
||||||
|
响应会回显实际生效的边界:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "scope": { "kind": "groups", "conversationCount": 243 } }
|
||||||
|
```
|
||||||
|
|
||||||
|
跨会话检索时,`evidence` 的每一项都会带上它所属的会话,便于把结果归属到具体群 / 联系人与具体成员:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"messageRef": "…",
|
||||||
|
"conversationName": "某个群",
|
||||||
|
"conversationType": "group",
|
||||||
|
"sender": "某成员",
|
||||||
|
"timestamp": 1789099069000,
|
||||||
|
"text": "…"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 索引新鲜度(freshness)
|
||||||
|
|
||||||
|
`query/search` 依赖本地索引,而本地索引是异步建立的派生数据,可能落后于聊天数据库。因此它的响应会显式给出覆盖口径:
|
||||||
|
|
||||||
|
| 字段 | 含义 |
|
||||||
|
| ------------------- | ------------------------------------------------------------------- |
|
||||||
|
| `indexLatestAt` | 索引目前覆盖到的源数据时间(epoch ms),`null` 表示无法判定 |
|
||||||
|
| `sourceLatestAt` | 聊天数据库里最新的活跃时间(epoch ms),`null` 表示无法判定 |
|
||||||
|
| `coverage.state` | `complete` 只在索引确实覆盖了所请求的时间范围时出现 |
|
||||||
|
| `freshness.catchUp` | 本次为追赶索引做了什么:`none` / `reused` / `completed` / `pending` |
|
||||||
|
|
||||||
|
调用方**必须**把 `coverage` 当真:`coverage.state` 不是 `complete` 且 `evidence` 为空时,只能说明"这段范围暂时无法确认",**不能**下"没有找到"的结论。索引落后时服务端会自动请求一次追赶同步,但不会让请求无限等待;`freshness.catchUp` 为 `pending` 表示追赶仍在后台进行,稍后重试即可拿到更新的覆盖。
|
||||||
|
|
||||||
|
`query/messages` 与 `query/conversation-overview` 直读聊天数据库,不受索引新鲜度影响。
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# 在微信机器人或外部 Agent 中使用 TraceMemo
|
||||||
|
|
||||||
|
TraceMemo 提供两条不同路径。先按你实际想做的事选择,不需要先理解 Agent、Skill 或 API 等术语。
|
||||||
|
|
||||||
|
| 你想做什么 | 使用方式 | 需要什么 |
|
||||||
|
| ---------------------------------------------------- | ----------------------------- | --------------------------------------------------------- |
|
||||||
|
| 直接在微信里发文字,让本机查询聊天并回复 | 微信机器人(Agent Hub) | 在应用“Agent”页面扫码登录机器人;部分任务需要 AI Provider |
|
||||||
|
| 在 Codex、Claude Code、OpenClaw 等工具里查询微信历史 | Reader Skill + Local HTTP API | 安装 Skill,并配置本机 API Token |
|
||||||
|
|
||||||
|
## 直接在微信里提问
|
||||||
|
|
||||||
|
打开应用一级导航中的“Agent”,进入“Agent Hub”,扫码登录一个微信机器人账号。之后用另一个微信账号向机器人发送文字,它会调用 TraceMemo 的本机数据,必要时使用已配置的 AI,再把结果回复给发送者。
|
||||||
|
|
||||||
|
可以先尝试:
|
||||||
|
|
||||||
|
- “最近 5 个会话”;
|
||||||
|
- “帮我看看最近跟张三聊了些什么”;
|
||||||
|
- “生成产品交流群今天的群聊总结图片”。
|
||||||
|
|
||||||
|
这条路径不要求安装 Reader Skill,也不要求用户配置 API Token。它主要处理文字请求,不支持群发、定时任务或与文字同等的任意媒体理解。
|
||||||
|
|
||||||
|
连接步骤、当前任务清单和安全边界见[Agent Hub](./agent-hub.md)。
|
||||||
|
|
||||||
|
## 在外部 Agent 中查询历史微信
|
||||||
|
|
||||||
|
Reader Skill 是给外部 Agent 的操作说明。安装后,Codex、Claude Code、OpenClaw 或其他本地 Agent 可以通过 TraceMemo Local HTTP API 按需读取联系人、群聊、最近会话、指定时间范围的聊天和群成员信息。
|
||||||
|
|
||||||
|
典型问题包括:
|
||||||
|
|
||||||
|
- “总结今天技术交流群讨论的内容。”
|
||||||
|
- “帮我找上个月讨论过的项目地址。”
|
||||||
|
- “过去一周有没有人提到退款?”
|
||||||
|
|
||||||
|
外部 Agent 不会直接打开微信数据库文件,但它能取得本机 API 返回的聊天内容。Agent 是否继续把结果发送给云端模型,取决于 Agent 自己的模型和工具配置。
|
||||||
|
|
||||||
|
## 外部 Agent 的安装步骤
|
||||||
|
|
||||||
|
1. 启动 TraceMemo 并完成微信数据库连接。
|
||||||
|
2. 打开一级导航“API”(页面为“API Center”),确认本地 API、数据库和 Reader Skill 都可用。
|
||||||
|
3. 选择目标 Agent,点击“复制安装指令”。
|
||||||
|
4. 在 Agent 自己的 Skill/配置目录执行或粘贴指令。
|
||||||
|
5. 在 API Center 复制当前 Token,并在 Agent 运行环境中设置 `TRACEMEMO_API_TOKEN`。
|
||||||
|
6. 先让 Agent 调用 health,再尝试查询最近会话。
|
||||||
|
|
||||||
|
详细说明:[Reader Skill](./reader-skill.md)、[Local HTTP API](./api.md)、[API 安全](./api-security.md)。
|
||||||
|
|
||||||
|
## 不要混淆两条路径
|
||||||
|
|
||||||
|
- Agent Hub:微信机器人收到实时文字后处理并回复;
|
||||||
|
- Reader Skill/API:外部 Agent 主动查询历史数据;
|
||||||
|
- `127.0.0.1:6131` 是 Local HTTP API,不是 MCP Server;
|
||||||
|
- Local HTTP API 当前没有对外提供实时入站消息订阅。
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# Reader Skill:让外部 Agent 读取微信
|
||||||
|
|
||||||
|
## 先理解它能做什么
|
||||||
|
|
||||||
|
Reader Skill 是一份给 Agent 的操作说明。安装后,Codex、Claude Code、OpenClaw 或其他本地 Agent 可以按需调用 TraceMemo,读取联系人、群聊、最近会话、指定时间的聊天和群成员信息。
|
||||||
|
|
||||||
|
它使用的是 TraceMemo Local HTTP API,不是 MCP Server。
|
||||||
|
|
||||||
|
Reader Skill 只负责“外部 Agent 主动查询历史微信数据”。它不负责二维码登录、监听微信实时消息、接收机器人消息或管理 Agent Hub。想让机器人收到微信消息后处理并回复,请阅读[Agent Hub](./agent-hub.md)。
|
||||||
|
|
||||||
|
正式 Reader Skill 名称和目录是 `tracememo-reader`,新安装使用 `TRACEMEMO_API_TOKEN`。已安装的旧 `wechatexplorer-reader` 仍可继续使用旧变量 `WECHATEXPLORER_API_TOKEN`(当前没有设定移除时间),但新安装请使用新名称与新变量名。
|
||||||
|
|
||||||
|
## 推荐安装流程
|
||||||
|
|
||||||
|
1. 启动 TraceMemo 并完成数据库连接。
|
||||||
|
2. 打开“API Center”,确认 API 服务和数据库状态正常。
|
||||||
|
3. 在 Reader Skill 区域选择目标 Agent,点击“复制安装指令”。
|
||||||
|
4. 把指令粘贴到对应 Agent 的 Skill/配置目录;应用会根据本机路径生成适合 Codex、Claude Code、OpenClaw 或通用 Agent 的说明。
|
||||||
|
5. 在 API Center 复制 Token,在 Agent 自己的本地环境设置:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export TRACEMEMO_API_TOKEN="<YOUR_API_TOKEN>"
|
||||||
|
```
|
||||||
|
|
||||||
|
6. 先执行 health 检查,再读取数据端点。
|
||||||
|
|
||||||
|
TraceMemo 不会自动把 Token 写进 Agent 配置。重新生成 Token 后,必须同步更新 Agent 环境。
|
||||||
|
|
||||||
|
## Agent 的读取顺序
|
||||||
|
|
||||||
|
当用户使用“今天”“昨天”“本周”等相对时间时:
|
||||||
|
|
||||||
|
1. 调用 `/api/v1/current_time` 获取本机时区和日期;
|
||||||
|
2. 将相对时间换算为 `chatlog` 支持的 `time` 或时间戳;
|
||||||
|
3. 调用 `/api/v1/resolve`、`contact` 或 `chatroom` 确认会话;
|
||||||
|
4. 调用 `/api/v1/chatlog` 读取目标范围;
|
||||||
|
5. 对重要结论再读取关键消息前后文,不要只凭一次粗查。
|
||||||
|
|
||||||
|
## 最小请求
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://127.0.0.1:6131/api/v1/health
|
||||||
|
|
||||||
|
curl -H "Authorization: Bearer $TRACEMEMO_API_TOKEN" \
|
||||||
|
"http://127.0.0.1:6131/api/v1/recent_chat?limit=20"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 当前能力范围
|
||||||
|
|
||||||
|
Reader Skill 可以指导 Agent 使用:
|
||||||
|
|
||||||
|
- 联系人、群聊、最近会话和会话解析;
|
||||||
|
- 指定会话、日期或时间戳范围的聊天记录;
|
||||||
|
- 群成员快照;
|
||||||
|
- 结构化日报渲染和按群聊生成总结图片;
|
||||||
|
- 定时日报任务的查询、创建、修改、启停、删除、立即执行和执行记录;删除不可逆,Skill 要求先列出唯一任务并取得用户明确确认;
|
||||||
|
- 个人微信发送能力状态查询(`/wechat-personal/send-capability`);
|
||||||
|
- `query/*` 一组结构化 Query 端点:`messages`、`search`、`message-context`、`conversation-overview`;
|
||||||
|
- Agent Hub 状态检查与已连接机器人发送测试。这里的发送接口是开发者/测试用途,不是实时机器人入口,也不会让 Reader Skill 自动监听微信消息。
|
||||||
|
|
||||||
|
注意这个 API 不只是只读的:`/report`、`/agent/send` 和 `/scheduled-reports*` 会写入状态或真的发出微信消息。
|
||||||
|
|
||||||
|
端点、参数、错误码和鉴权细节以[Local HTTP API](./api.md)为准。Skill 文件保持短小,避免在多个文档中复制会变化的完整响应 schema。
|
||||||
|
|
||||||
|
## 隐私边界
|
||||||
|
|
||||||
|
Reader Skill 本身不会把聊天数据自动上传到其他服务器;它只是让 Agent 调用本机 API。Agent 读取结果是否继续发送给云端模型,取决于 Agent 自己的模型和工具配置。请同时阅读[数据、隐私与安全](../user-guide/privacy.md)。
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# TraceMemo 2.1.9:Local HTTP API 鉴权迁移
|
||||||
|
|
||||||
|
2.1.9 为 Local HTTP API 增加 Bearer Token 鉴权。这是一次有意的兼容性变化:除健康检查外,数据接口不再接受裸请求。
|
||||||
|
|
||||||
|
- 历史版本中,`GET /api/v1/contact` 等数据请求可能直接返回内容;
|
||||||
|
- 2.1.9 中,相同请求必须携带 `Authorization: Bearer <TOKEN>`,否则返回 `401`;
|
||||||
|
- `GET /api/v1/health` 保持公开;
|
||||||
|
- 升级后应用会生成并安全保存 Token,原有 API 启用状态、监听地址和端口设置保持不变;
|
||||||
|
- Token 可在 TraceMemo → API Center 中显示、复制和重新生成;
|
||||||
|
- Reader Skill、Codex、Claude Code、OpenClaw 和其他本地 Agent 需要在自己的环境中设置 `WECHATEXPLORER_API_TOKEN`。
|
||||||
|
|
||||||
|
如果旧 Agent 无法访问,请先从 API Center 复制当前 Token,再确认每个非 health 请求都带有 Bearer header。完整规则见[API 安全](./api-security.md)。
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# TraceMemo 2.2.0:正式品牌身份与安全升级迁移
|
||||||
|
|
||||||
|
TraceMemo(迹忆)原名 WechatExplorer。v2.2.0 不只更新用户可见名称,也正式启用新的应用身份、数据目录、Reader Skill 和默认 Agent 环境变量,同时为 v2.1.9 用户提供一次安全迁移路径。
|
||||||
|
|
||||||
|
## 新的产品身份
|
||||||
|
|
||||||
|
- 产品名与 Electron runtime name:`TraceMemo`;
|
||||||
|
- bundle/app identifier:`com.tracememo.app`;
|
||||||
|
- macOS userData:`~/Library/Application Support/TraceMemo`;
|
||||||
|
- macOS 日志:`~/Library/Logs/TraceMemo`;
|
||||||
|
- Reader Skill:`tracememo-reader`;
|
||||||
|
- Agent API Token 环境变量:`TRACEMEMO_API_TOKEN`;
|
||||||
|
- Agent Hub 凭据目录:`~/.tracememo/wechat-connector/accounts`。
|
||||||
|
|
||||||
|
## v2.1.9 升级迁移
|
||||||
|
|
||||||
|
首次启动 TraceMemo 时,如果检测到包含有效用户资产的旧数据目录,应用会询问是否立即迁移:
|
||||||
|
|
||||||
|
- `WechatExplorer`;
|
||||||
|
- v2.1.9 在区分大小写文件系统上可能使用的 `wechatexplorer`。
|
||||||
|
|
||||||
|
两个旧目录都有效时,应用确定性优先选择 `WechatExplorer` 并写入诊断日志,不合并目录。选择“以后迁移”不会删除或修改旧数据,下次启动仍可继续处理。
|
||||||
|
|
||||||
|
迁移遵循以下安全边界:
|
||||||
|
|
||||||
|
- 只复制明确列出的用户资产,不复制整个 Application Support;
|
||||||
|
- TraceMemo 已存在的文件或目录绝不覆盖;
|
||||||
|
- 每一项迁移可重复执行,已完成项会跳过;
|
||||||
|
- 迁移失败只清理本次创建的 staging,旧目录和旧文件始终保留;
|
||||||
|
- 不移动、不删除旧目录,不修改微信数据库或 Knowledge schema。
|
||||||
|
|
||||||
|
## 迁移的用户资产
|
||||||
|
|
||||||
|
- 设置、微信数据库连接路径和 AI Provider 元数据;
|
||||||
|
- Knowledge 本地索引;
|
||||||
|
- 报告历史、防撤回归档、图片理解结果和 Renderer Local Storage;
|
||||||
|
- Local HTTP API Token;
|
||||||
|
- AI Provider Key、微信数据库 Key 和图片解密 Key;
|
||||||
|
- Agent Hub credential 与同步状态。
|
||||||
|
|
||||||
|
Chromium Cache、Code Cache、GPUCache、临时文件、语音模型和其他可重建运行缓存不会为了品牌升级强制复制。
|
||||||
|
|
||||||
|
## Knowledge
|
||||||
|
|
||||||
|
Knowledge 以完整目录为单位复制。每个账号的 `knowledge.sqlite`、`knowledge.sqlite-wal` 和 `knowledge.sqlite-shm` 会一起进入同一个 staging;复制后先核对主库及 companion 文件,再对 staging 数据库执行 SQLite `integrity_check`。只有验证通过后才放入 TraceMemo 数据根。
|
||||||
|
|
||||||
|
迁移过程不会打开、修改或删除真实旧 Knowledge。失败时旧索引仍可用于重新迁移,不要求用户重新建立 2.47GB 级别的索引。
|
||||||
|
|
||||||
|
## Token 与加密 Key
|
||||||
|
|
||||||
|
旧 `safeStorage` 密文不会原样复制到新数据目录。TraceMemo 会启动一个隔离的 legacy helper:macOS 使用旧 `WechatExplorer` identity,helper 只在内存中解密并校验旧 Token/Key,再通过专用进程管道交给主进程重新加密;macOS 主进程使用 TraceMemo identity. 明文不会写入磁盘、环境变量或日志。
|
||||||
|
|
||||||
|
Token 格式、随机熵、加密方式和 rotation 行为没有变化。如果旧 API Token 因系统安全存储限制无法迁移,应用不会静默生成替代 Token,本地 API 会安全停用并提示用户重试迁移或在 API Center 主动重新生成。AI Provider Key、数据库 Key 和图片 Key 失败时也会明确记录为部分迁移,旧密文保持不变。
|
||||||
|
|
||||||
|
## API、Agent 与 Skill 兼容
|
||||||
|
|
||||||
|
Local HTTP API 继续使用 `127.0.0.1:6131` 和 `/api/v1/*`,Bearer Token 格式不变。
|
||||||
|
|
||||||
|
新安装和新文档默认使用 `TRACEMEMO_API_TOKEN`。已安装的旧 Reader Skill 可以在一个兼容版本内继续使用 `WECHATEXPLORER_API_TOKEN`。正式随应用分发的 Skill 已更名为 `tracememo-reader`,资源解析仍可读取旧 `wechatexplorer-reader` 目录作为 fallback。
|
||||||
|
|
||||||
|
Agent Hub 新凭据写入 `~/.tracememo`。如果迁移尚未完成且新目录没有凭据,connector 会只读回退到 `~/.wechatexplorer`;新版本不会清理或删除旧目录。
|
||||||
|
|
||||||
|
## 日志与 Documents
|
||||||
|
|
||||||
|
TraceMemo 新日志写入新的日志目录,“设置 → 关于 → 打开诊断日志目录”会打开当前 TraceMemo 日志。历史 WechatExplorer 日志保持原位置,不搬迁、不重命名、不删除。
|
||||||
|
|
||||||
|
`Documents/TraceMemo` 用于新导出和 Emoji 数据;历史 `Documents/WechatExplorer` 不删除,并继续提供兼容读取。
|
||||||
|
|
||||||
|
更多安全边界见[数据、隐私与安全](../user-guide/privacy.md)和[API 安全](./api-security.md)。
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# 如何核对 AI 的回答来源
|
||||||
|
|
||||||
|
## 先记住一件事
|
||||||
|
|
||||||
|
AI 回答后,你可以继续查看它参考了哪些聊天内容、这些内容来自哪个会话和时间,并跳回原始消息检查上下文。
|
||||||
|
|
||||||
|
这让 TraceMemo 和只给一段摘要的聊天机器人不同:答案不是终点,来源也应该能被你检查。
|
||||||
|
|
||||||
|
## 三类来源信息
|
||||||
|
|
||||||
|
在产品界面和检索详情中,你可能看到这些名称:
|
||||||
|
|
||||||
|
- **Evidence**:AI 回答所依据的原始聊天片段。
|
||||||
|
- **Citation**:回答中某个结论对应的来源标记。
|
||||||
|
- **Search Trace**:本次查找经历了哪些阶段、每一步用了多久、覆盖是否完整。
|
||||||
|
|
||||||
|
普通用户不需要记住英文名。判断一个回答是否可信时,按“来源 → 原消息 → 上下文”检查即可。
|
||||||
|
|
||||||
|
## 推荐的核对顺序
|
||||||
|
|
||||||
|
1. 先看回答是否明确区分事实、推断和不确定信息;
|
||||||
|
2. 打开来源,检查发送者、会话和时间;
|
||||||
|
3. 跳回档案,查看消息前后文,确认是否存在引用、转发或后续修正;
|
||||||
|
4. 检查提示中是否有未转写语音、缺失媒体或只覆盖部分范围;
|
||||||
|
5. 对重要决定、金额、日期和责任人,不要只依据 AI 摘要。
|
||||||
|
|
||||||
|
## 为什么来源可能不完整
|
||||||
|
|
||||||
|
来源覆盖受时间范围、会话范围、索引状态和可读媒体影响。例如:
|
||||||
|
|
||||||
|
- Knowledge 还没追到最新时,跨会话检索只覆盖到索引当前的时间点,答案会标注这个范围;
|
||||||
|
- 语音没有转写时,AI 可能只能看到消息类型;
|
||||||
|
- 图片无法读取或未启用图片理解时,AI 不应声称知道图片内容;
|
||||||
|
- 你只选择了一个群,答案不会自动代表所有聊天。
|
||||||
|
|
||||||
|
Knowledge 在后台同步时**不会**暂停分析:你仍然可以提问,只是答案基于当前已可用的覆盖范围。看到“可能遗漏”或“部分覆盖”时,扩大范围、等同步追上或检查原始媒体后再问。
|
||||||
|
|
||||||
|
## 这不是事实保证
|
||||||
|
|
||||||
|
Evidence 和 Citation 能告诉你“模型看到了什么”,不能保证模型没有误读。最终判断仍应回到原始消息,尤其是涉及隐私、法律、财务、医疗或工作决策时。
|
||||||
|
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# TraceMemo 如何把聊天变成可用的信息
|
||||||
|
|
||||||
|
你可以把一次任务想成下面这条路径:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
WX["本机微信数据"] --> PARSE["读取与解析"]
|
||||||
|
|
||||||
|
PARSE --> ARCHIVE["聊天档案与普通搜索"]
|
||||||
|
PARSE --> EXPORT["聊天导出"]
|
||||||
|
|
||||||
|
PARSE --> IDX["本机索引"]
|
||||||
|
IDX --> TEXTIDX["聊天记录索引"]
|
||||||
|
IDX --> IMGIDX["图片文字索引(本机识别)"]
|
||||||
|
|
||||||
|
TEXTIDX --> UNDERSTAND["Understand:AI Search / 问问微信"]
|
||||||
|
IMGIDX --> UNDERSTAND
|
||||||
|
UNDERSTAND --> PROVIDER["你配置的 AI Provider"]
|
||||||
|
PROVIDER --> ANSWER["回答与可核对来源"]
|
||||||
|
|
||||||
|
PARSE --> REPORTINPUT["整理日报输入"]
|
||||||
|
REPORTINPUT --> PROVIDER
|
||||||
|
PROVIDER --> REPORTFILE["本机保存 HTML 与 PNG"]
|
||||||
|
|
||||||
|
PARSE --> MONITOR["Monitor:退群监控 / 成员快照"]
|
||||||
|
MONITOR --> RULE["自动化规则"]
|
||||||
|
REPORTFILE --> RULE
|
||||||
|
RULE --> POLICY["Policy"]
|
||||||
|
POLICY --> GATEWAY["Action Gateway"]
|
||||||
|
GATEWAY --> CAP["本机发送能力"]
|
||||||
|
CAP --> AUDIT["执行记录与审计"]
|
||||||
|
|
||||||
|
PARSE --> API["Local HTTP API"]
|
||||||
|
API --> EXTAGENT["外部 Agent / Reader Skill"]
|
||||||
|
BOT["微信机器人消息"] --> HUB["Agent Hub"]
|
||||||
|
HUB --> PARSE
|
||||||
|
HUB --> PROVIDER
|
||||||
|
```
|
||||||
|
|
||||||
|
## Remember → 图片文字 → Understand → Monitor → Act
|
||||||
|
|
||||||
|
TraceMemo 的工作方式可以概括为:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Remember → 图片文字 → Understand → Monitor → Act
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Remember**:读取并解析本机微信数据,建立聊天档案、普通搜索和导出。
|
||||||
|
- **图片文字**:在本机识别图片里的文字,把截图、公告、报价图也变成可检索的内容。这一步不联网。
|
||||||
|
- **Understand**:Knowledge、AI Search / 问问微信、群聊日报。需要模型时,只把完成这次任务所需的受控上下文交给 Provider。
|
||||||
|
- **Monitor**:用成员快照对比发现群成员变化,产出成员退出事件。
|
||||||
|
- **Act**:自动化规则把前面的步骤串起来(定时日报、退群通知);动作经过统一执行边界,并留下执行记录。
|
||||||
|
|
||||||
|
回答和动作结果都应能回到来源或记录核对。
|
||||||
|
|
||||||
|
## 退群监控
|
||||||
|
|
||||||
|
退群监控使用成员快照判断变化:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Current Membership → Snapshot Diff → Member Event
|
||||||
|
```
|
||||||
|
|
||||||
|
上一份有效快照(Last Good Snapshot)不会被不完整读取覆盖,因此重启后仍可继续监控通知。监控关闭期间发生的变化,不会在重新开启后补报。
|
||||||
|
|
||||||
|
成员退出事件同时是「自动化」里「退群通知」规则的触发条件。
|
||||||
|
|
||||||
|
## 动作执行与审计
|
||||||
|
|
||||||
|
自动发送和监控动作经过统一边界:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Feature → Policy → Gateway → Capability → Execution → Audit
|
||||||
|
```
|
||||||
|
|
||||||
|
Policy blocked 表示策略不允许,Capability unavailable 表示当前发送能力不可用,Send failed 表示已经尝试但执行失败。Action Audit / Logs 会保留执行结果;定时日报即使发送失败,也会保留已生成的报告记录。
|
||||||
|
|
||||||
|
这些动作统一由「自动化」管理,当前有三类规则:**@我生成日报**、**定时日报**、**退群通知**。发送目标支持当前群聊、文件传输助手、自己、指定好友,不是任意群发。
|
||||||
|
|
||||||
|
## 哪些步骤在本机
|
||||||
|
|
||||||
|
- 微信数据库读取与解析;
|
||||||
|
- 聊天档案浏览和普通搜索;
|
||||||
|
- Knowledge 索引与增量同步;
|
||||||
|
- 图片文字索引:识别图片中的文字完全在本机进行,原始图片不会因为本地识别而上传;
|
||||||
|
- 离线语音转写;
|
||||||
|
- 聊天导出文件、日报 HTML/PNG 和本地历史记录的保存。
|
||||||
|
|
||||||
|
## 哪些步骤可能调用外部服务
|
||||||
|
|
||||||
|
当你主动使用 AI Search、群聊日报或图片理解时,应用会把完成任务所需的受控问题和上下文发送给你配置的 Provider。它不会因为打开软件就自动上传完整数据库,本机 OCR、离线语音转写和普通搜索也不会触发外发。
|
||||||
|
|
||||||
|
Agent Hub 收到微信机器人的文字后,也可能为了理解请求或生成总结调用已配置的 Provider。Reader Skill 调用的是本机 API;外部 Agent 是否把读取结果继续交给云端模型,取决于外部 Agent 自己的配置。
|
||||||
|
|
||||||
|
如果 Provider 是 Ollama 等本机服务,请把它视为本机的另一个进程;如果是云服务,数据处理和留存规则由该服务商决定。
|
||||||
|
|
||||||
|
## 产品名词和用户任务的对应关系
|
||||||
|
|
||||||
|
| 用户想做什么 | 产品中可能看到的名称 |
|
||||||
|
| ------------------------------ | ---------------------------- |
|
||||||
|
| 让 AI 找相关聊天 | AI Search、Retrieval |
|
||||||
|
| 让答案能回到原消息 | Evidence、Citation |
|
||||||
|
| 查看 AI 查找过程 | Search Trace |
|
||||||
|
| 让跨会话查找更稳定 | Knowledge、FTS 索引 |
|
||||||
|
| 搜到截图、公告图里写过的文字 | 图片文字索引、本机 OCR |
|
||||||
|
| 让日报、退群通知按规则自动执行 | 自动化、Policy、执行记录 |
|
||||||
|
| 让外部 Agent 读取聊天 | Reader Skill、Local HTTP API |
|
||||||
|
| 让微信机器人调用本机能力 | Agent Hub |
|
||||||
|
|
||||||
|
先按任务使用,再在需要排查或开发集成时阅读术语。
|
||||||
@@ -0,0 +1,447 @@
|
|||||||
|
# 实验性功能:自托管微信分享卡片
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> **实验性功能**
|
||||||
|
> 该能力需要用户自行准备 Cloudflare、域名和微信公众平台测试号,目前不属于开箱即用的稳定功能。Cloudflare、微信 JS-SDK、测试号权限或微信客户端行为变化,都可能导致分享卡片失效。
|
||||||
|
|
||||||
|
## 新手推荐:直接交给 Agent
|
||||||
|
|
||||||
|
如果你不熟悉 Cloudflare、Wrangler 或命令行,不需要手动照着整篇文档操作。把下面这个 Skill 文件夹交给 Codex、Claude Code 或其他能够操作项目终端的编程 Agent:
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/skill/setup-wechat-share-card/
|
||||||
|
```
|
||||||
|
|
||||||
|
然后对 Agent 说:
|
||||||
|
|
||||||
|
```text
|
||||||
|
请使用 setup-wechat-share-card Skill,帮我部署 TraceMemo 的实验性微信分享卡片服务。尽量自动完成,只在缺少必要信息时一次性问我。
|
||||||
|
```
|
||||||
|
|
||||||
|
Agent 会自动:
|
||||||
|
|
||||||
|
- 检查 Node.js、pnpm 和 Wrangler;
|
||||||
|
- 必要时临时下载 Wrangler;
|
||||||
|
- 打开 Cloudflare 登录并执行 `whoami`;
|
||||||
|
- 自动生成 `UPLOAD_TOKEN`;
|
||||||
|
- 创建或复用私有 R2 Bucket;
|
||||||
|
- 写入 Worker Secret;
|
||||||
|
- 根据你的域名生成本机 Worker 配置;
|
||||||
|
- 部署 Worker并执行健康检查和微信签名检查;
|
||||||
|
- 把上传密钥复制到剪贴板,供你粘贴到 TraceMemo。
|
||||||
|
|
||||||
|
Agent 无法替你创建微信测试号或决定使用哪个域名,因此通常只需要你提供:
|
||||||
|
|
||||||
|
1. 你准备使用的分享域名,例如 `share.example.com`;
|
||||||
|
2. 微信测试号页面中的 AppID;
|
||||||
|
3. 微信测试号页面中的 AppSecret;
|
||||||
|
4. 浏览器弹出 Cloudflare OAuth 页面时完成一次登录授权。
|
||||||
|
|
||||||
|
真实配置保存在被 Git 忽略的本机 `.env` 中,不会写入 `.env.example`。不要把 `.env` 发给别人或提交到仓库。
|
||||||
|
|
||||||
|
TraceMemo 可以把已经生成的群聊日报长图发布为一个临时网页,并在微信中分享成带有标题、描述和缩略图的卡片。
|
||||||
|
|
||||||
|
TraceMemo **不提供公共卡片服务器**。使用该功能前,需要按照本文部署一套属于你自己的卡片服务。日报图片将上传到你自己的 Cloudflare R2,而不是上传到 TraceMemo 作者的服务器。
|
||||||
|
|
||||||
|
## 这个功能解决什么问题
|
||||||
|
|
||||||
|
直接把日报 PNG 发到微信,只会显示为一张普通图片。微信卡片还需要:
|
||||||
|
|
||||||
|
- 一个域名 (未能备案的话 在微信里点击多次 可能会被微信内置窗口提示需要备案);
|
||||||
|
- 卡片标题和描述;
|
||||||
|
- 一张微信可以读取的缩略图;
|
||||||
|
- 微信 JS-SDK 签名;
|
||||||
|
- 一个临时保存日报图片的位置。
|
||||||
|
|
||||||
|
本项目提供的 Cloudflare Worker 负责这些工作。桌面端上传日报后,会得到一个分享链接和二维码。用微信扫码打开链接,再点击右上角菜单分享,即可生成微信卡片。
|
||||||
|
|
||||||
|
## 数据会经过哪里
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
A[TraceMemo 本机日报 PNG] -->|带 UPLOAD_TOKEN 上传| B[你的 Cloudflare Worker]
|
||||||
|
B --> C[你的私有 R2 Bucket]
|
||||||
|
B -->|AppID + AppSecret| D[微信公众平台接口]
|
||||||
|
D -->|access_token 与 jsapi_ticket| B
|
||||||
|
B --> E[临时分享网页]
|
||||||
|
E --> F[微信 JS-SDK]
|
||||||
|
F --> G[微信好友或群聊卡片]
|
||||||
|
```
|
||||||
|
|
||||||
|
与 TraceMemo 的本地浏览能力不同,启用分享卡片后,当前日报长图、缩略图、卡片标题和描述会离开本机,上传到你控制的 Cloudflare 账号。
|
||||||
|
|
||||||
|
## 你需要准备什么
|
||||||
|
|
||||||
|
| 项目 | 用途 | 从哪里获得 |
|
||||||
|
| ------------------------ | -------------------------------------------- | ---------------------------------------- |
|
||||||
|
| Cloudflare 账号 | 运行 Worker 和保存 R2 图片 | 自行注册 Cloudflare |
|
||||||
|
| 托管在 Cloudflare 的域名 | 提供 HTTPS 分享地址 | 使用自己的域名,例如 `share.example.com` |
|
||||||
|
| R2 Bucket | 临时保存日报和缩略图 | 使用 Wrangler 创建 |
|
||||||
|
| `UPLOAD_TOKEN` | 阻止陌生人调用你的上传接口 | **由你自己随机生成** |
|
||||||
|
| 微信测试号 AppID | 标识调用 JS-SDK 的微信应用 | 微信公众平台接口测试号页面 |
|
||||||
|
| 微信测试号 AppSecret | Worker 获取微信接口凭据 | 微信公众平台接口测试号页面 |
|
||||||
|
| JS 接口安全域名 | 告诉微信哪些网页可以使用该 AppID 调用 JS-SDK | 在微信测试号页面填写你的分享域名 |
|
||||||
|
|
||||||
|
微信公众平台接口测试号入口:
|
||||||
|
|
||||||
|
<https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index>
|
||||||
|
|
||||||
|
微信 JS-SDK 官方文档:
|
||||||
|
|
||||||
|
<https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html>
|
||||||
|
|
||||||
|
## 理解三个重要配置
|
||||||
|
|
||||||
|
### `UPLOAD_TOKEN` 从哪里来
|
||||||
|
|
||||||
|
`UPLOAD_TOKEN` **不是从 Cloudflare 或微信后台领取的**,它是卡片服务部署者自己生成的一段随机密码。
|
||||||
|
|
||||||
|
它用于保护 Worker 的上传接口:TraceMemo 上传日报时,会发送:
|
||||||
|
|
||||||
|
```http
|
||||||
|
Authorization: Bearer <UPLOAD_TOKEN>
|
||||||
|
```
|
||||||
|
|
||||||
|
Worker 只有在密钥完全一致时才接受上传。没有它,任何知道接口地址的人都可能向你的 R2 上传文件并消耗资源。
|
||||||
|
|
||||||
|
在 macOS 或 Linux 中生成一枚 64 位十六进制随机密钥:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl rand -hex 32
|
||||||
|
```
|
||||||
|
|
||||||
|
示例输出只用于说明格式,不要直接使用:
|
||||||
|
|
||||||
|
```text
|
||||||
|
8a4d...一共 64 个十六进制字符...72ef
|
||||||
|
```
|
||||||
|
|
||||||
|
生成后,同一个值需要配置到两个地方:
|
||||||
|
|
||||||
|
1. Cloudflare Worker Secret `UPLOAD_TOKEN`;
|
||||||
|
2. TraceMemo“生成微信卡片”弹窗中的“上传密钥”。
|
||||||
|
|
||||||
|
如果两边不一致,卡片服务会返回 HTTP 401 或“未授权”。
|
||||||
|
|
||||||
|
TraceMemo 会使用 Electron `safeStorage` 将服务地址和上传密钥加密保存在本机。不要把密钥提交到 Git,也不要写入 `wrangler.jsonc`。
|
||||||
|
|
||||||
|
### AppID 和 AppSecret 从哪里来
|
||||||
|
|
||||||
|
打开[微信公众平台接口测试号](https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index),使用微信扫码登录。
|
||||||
|
|
||||||
|
页面上方会显示:
|
||||||
|
|
||||||
|
- `appID`;
|
||||||
|
- `appsecret`。
|
||||||
|
|
||||||
|
将它们分别保存为 Worker Secret:
|
||||||
|
|
||||||
|
```text
|
||||||
|
WECHAT_APP_ID
|
||||||
|
WECHAT_APP_SECRET
|
||||||
|
```
|
||||||
|
|
||||||
|
它们的作用不同:
|
||||||
|
|
||||||
|
- AppID 用于标识这个微信测试应用;
|
||||||
|
- AppSecret 是高敏感凭据,Worker 用它向微信服务器获取 `access_token`;
|
||||||
|
- Worker 再使用 `access_token` 获取 `jsapi_ticket`;
|
||||||
|
- 最后使用 `jsapi_ticket`、当前网页 URL、时间戳和随机串生成 JS-SDK 签名。
|
||||||
|
|
||||||
|
AppSecret 只能保存在 Worker Secret 中。不要把它填写到 TraceMemo 的“上传密钥”输入框,不要发送给前端,也不要提交到仓库。怀疑泄露时,应立即在微信后台重置并更新 Worker Secret。
|
||||||
|
|
||||||
|
### JS 接口安全域名是干什么的
|
||||||
|
|
||||||
|
JS 接口安全域名是微信对网页来源的白名单。
|
||||||
|
|
||||||
|
假设你的分享服务地址是:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://share.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
那么测试号页面中的“JS 接口安全域名”应填写:
|
||||||
|
|
||||||
|
```text
|
||||||
|
share.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
填写时:
|
||||||
|
|
||||||
|
- 不带 `https://`;
|
||||||
|
- 不带 `/s/xxx` 等路径;
|
||||||
|
- 不要填写 Cloudflare Worker 名称;
|
||||||
|
- 必须与用户实际打开分享页时的域名一致。
|
||||||
|
|
||||||
|
它不是用来解析 DNS 的。域名仍然需要先在 Cloudflare 中正确绑定到 Worker。安全域名的作用是告诉微信:允许这个域名下的网页使用当前 AppID 请求 JS-SDK 能力。
|
||||||
|
|
||||||
|
如果没有配置、填错域名,或签名 URL 与实际页面 URL 不一致,通常会出现 `invalid signature`、`config:fail` 或分享信息没有生效。
|
||||||
|
|
||||||
|
微信可能要求下载一个 TXT 验证文件,并确保它可以通过下面的地址访问:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://share.example.com/微信提供的文件名.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
项目 Worker 已包含根路径验证文件的实现方式。你需要把自己的文件名和内容加入 `services/share-card-worker/src/index.js` 中的 `WECHAT_DOMAIN_VERIFICATION`,然后重新部署。
|
||||||
|
|
||||||
|
## 自托管部署步骤
|
||||||
|
|
||||||
|
以下命令均在项目根目录执行。
|
||||||
|
|
||||||
|
### 1. 登录 Cloudflare
|
||||||
|
|
||||||
|
项目建议使用本地 Wrangler:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm exec wrangler login
|
||||||
|
pnpm exec wrangler whoami
|
||||||
|
```
|
||||||
|
|
||||||
|
如果本地版本的 OAuth 登录出现 `invalid_scope` 等问题,可临时使用更新版本:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dlx wrangler@latest login
|
||||||
|
pnpm dlx wrangler@latest whoami
|
||||||
|
```
|
||||||
|
|
||||||
|
登录注意事项:
|
||||||
|
|
||||||
|
- 让 Wrangler 自动打开浏览器最稳妥;
|
||||||
|
- 不要复用以前生成的 OAuth 链接;
|
||||||
|
- 不要修改链接中的 `state`、`code_challenge` 或回调地址;
|
||||||
|
- 不建议使用无痕窗口或跨浏览器复制链接;
|
||||||
|
- 默认回调使用 `localhost:8976`,端口被占用时先结束旧的 Wrangler 登录进程;
|
||||||
|
- 浏览器提示授权成功后,仍应通过 `whoami` 核对账号。
|
||||||
|
|
||||||
|
### 2. 修改 Worker 配置
|
||||||
|
|
||||||
|
打开:
|
||||||
|
|
||||||
|
```text
|
||||||
|
services/share-card-worker/wrangler.jsonc
|
||||||
|
```
|
||||||
|
|
||||||
|
至少修改下面两个位置:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"pattern": "share.example.com",
|
||||||
|
"custom_domain": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vars": {
|
||||||
|
"PUBLIC_ORIGIN": "https://share.example.com",
|
||||||
|
"DEFAULT_EXPIRY_DAYS": "7"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`routes[].pattern` 是 Worker 自定义域名,`PUBLIC_ORIGIN` 是生成分享链接和校验签名来源时使用的完整 HTTPS 地址,两者必须一致。
|
||||||
|
|
||||||
|
不要直接照抄仓库维护者的域名。请替换为你自己 Cloudflare 账号中的域名或子域名。
|
||||||
|
|
||||||
|
### 3. 创建私有 R2 Bucket
|
||||||
|
|
||||||
|
默认配置使用 Bucket 名称:
|
||||||
|
|
||||||
|
```text
|
||||||
|
wechatexplorer-share-reports
|
||||||
|
```
|
||||||
|
|
||||||
|
创建:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm exec wrangler r2 bucket create wechatexplorer-share-reports \
|
||||||
|
--config services/share-card-worker/wrangler.jsonc
|
||||||
|
```
|
||||||
|
|
||||||
|
Worker 中的绑定名称是 `REPORTS`。R2 会保存:
|
||||||
|
|
||||||
|
```text
|
||||||
|
cards/<card-id>/card.json
|
||||||
|
cards/<card-id>/report.png
|
||||||
|
cards/<card-id>/thumbnail.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
- `card.json`:标题、描述、创建时间和过期时间;
|
||||||
|
- `report.png`:完整日报长图;
|
||||||
|
- `thumbnail.jpg`:微信卡片缩略图。
|
||||||
|
|
||||||
|
请保持 R2 Bucket 私有,不要启用公开 `r2.dev` 开发 URL。图片应统一通过 Worker 的随机卡片 URL 读取。
|
||||||
|
|
||||||
|
### 4. 生成并配置 `UPLOAD_TOKEN`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl rand -hex 32
|
||||||
|
```
|
||||||
|
|
||||||
|
复制生成结果,然后执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm exec wrangler secret put UPLOAD_TOKEN \
|
||||||
|
--config services/share-card-worker/wrangler.jsonc
|
||||||
|
```
|
||||||
|
|
||||||
|
Wrangler 提示输入时粘贴密钥。终端不会正常显示 Secret 内容。
|
||||||
|
|
||||||
|
### 5. 配置微信 AppID 和 AppSecret
|
||||||
|
|
||||||
|
从[微信公众平台接口测试号](https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index)复制 AppID:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm exec wrangler secret put WECHAT_APP_ID \
|
||||||
|
--config services/share-card-worker/wrangler.jsonc
|
||||||
|
```
|
||||||
|
|
||||||
|
再复制 AppSecret:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm exec wrangler secret put WECHAT_APP_SECRET \
|
||||||
|
--config services/share-card-worker/wrangler.jsonc
|
||||||
|
```
|
||||||
|
|
||||||
|
Secret 不会出现在 `wrangler.jsonc` 中。如果你更换 Cloudflare 账号或重新创建 Worker,需要重新配置全部三个 Secret。
|
||||||
|
|
||||||
|
### 6. 配置微信测试号
|
||||||
|
|
||||||
|
在测试号页面完成:
|
||||||
|
|
||||||
|
1. 使用测试微信关注该测试号;
|
||||||
|
2. 将 `share.example.com` 填入“JS 接口安全域名”;
|
||||||
|
3. 按页面提示完成 TXT 文件域名验证;
|
||||||
|
4. 确认 AppID/AppSecret 与刚才写入 Worker 的值来自同一个测试号。
|
||||||
|
|
||||||
|
测试号只适合开发和验证。正式公众号的接口权限、认证要求和后台菜单可能不同,请以微信公众平台实际规则为准。
|
||||||
|
|
||||||
|
### 7. 部署 Worker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm exec wrangler deploy \
|
||||||
|
--config services/share-card-worker/wrangler.jsonc
|
||||||
|
```
|
||||||
|
|
||||||
|
Cloudflare Custom Domain 要求域名已经位于同一 Cloudflare 账号中。如果该子域名已经存在 A、AAAA 或 CNAME 记录,绑定可能失败。删除冲突记录,或者换一个未使用的子域名,例如 `share2.example.com`。
|
||||||
|
|
||||||
|
更新 Secret 后,如果线上仍提示旧配置,可再执行一次完整部署。
|
||||||
|
|
||||||
|
## 在 TraceMemo 中配置
|
||||||
|
|
||||||
|
生成一份日报后,点击“生成微信卡片(实验性)”。首次使用需要填写:
|
||||||
|
|
||||||
|
```text
|
||||||
|
服务地址:https://share.example.com
|
||||||
|
上传密钥:你自己通过 openssl rand -hex 32 生成的 UPLOAD_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
这里的“上传密钥”绝对不是微信 AppSecret。
|
||||||
|
|
||||||
|
配置保存后,TraceMemo 会上传当前日报和缩略图,返回二维码。使用已经关注测试号的微信扫码,打开页面后再通过右上角菜单分享。
|
||||||
|
|
||||||
|
## 验证部署
|
||||||
|
|
||||||
|
### 健康检查
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsS https://share.example.com/health
|
||||||
|
```
|
||||||
|
|
||||||
|
正常结果类似:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "ok": true, "service": "wechatexplorer-share-card", "storage": "ready" }
|
||||||
|
```
|
||||||
|
|
||||||
|
### JS-SDK 签名检查
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsS \
|
||||||
|
'https://share.example.com/api/wx-signature?url=https%3A%2F%2Fshare.example.com%2Fhealth'
|
||||||
|
```
|
||||||
|
|
||||||
|
正常结果应包含:
|
||||||
|
|
||||||
|
```text
|
||||||
|
appId
|
||||||
|
timestamp
|
||||||
|
nonceStr
|
||||||
|
signature
|
||||||
|
```
|
||||||
|
|
||||||
|
响应中不应包含 AppSecret、`access_token` 或 `jsapi_ticket`。
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### HTTP 401 / 未授权
|
||||||
|
|
||||||
|
TraceMemo 中保存的上传密钥与 Worker 的 `UPLOAD_TOKEN` 不一致。重新生成或重新配置时,必须同步更新两边。
|
||||||
|
|
||||||
|
### “微信 JS-SDK 尚未配置”
|
||||||
|
|
||||||
|
Worker 缺少 `WECHAT_APP_ID` 或 `WECHAT_APP_SECRET`。执行两个 `secret put`,再重新部署。
|
||||||
|
|
||||||
|
### `invalid signature` 或分享信息不生效
|
||||||
|
|
||||||
|
依次检查:
|
||||||
|
|
||||||
|
- `PUBLIC_ORIGIN` 是否与浏览器实际访问的 origin 完全一致;
|
||||||
|
- JS 接口安全域名是否只填写了域名;
|
||||||
|
- AppID/AppSecret 是否属于同一个测试号;
|
||||||
|
- AppSecret 是否已被重置但 Worker 仍保存旧值;
|
||||||
|
- 分享页面是否经过了改变 URL 的代理或重定向;
|
||||||
|
- 测试微信是否已关注测试号。
|
||||||
|
|
||||||
|
### 自定义域名绑定失败
|
||||||
|
|
||||||
|
检查同名 A、AAAA、CNAME 记录是否已经存在,域名是否位于当前 Wrangler 登录的 Cloudflare 账号中。
|
||||||
|
|
||||||
|
### R2 未配置
|
||||||
|
|
||||||
|
确认 Bucket 存在,并且 `wrangler.jsonc` 中的绑定名称为 `REPORTS`、`bucket_name` 与实际 Bucket 一致。
|
||||||
|
|
||||||
|
### 卡片过期或图片消失
|
||||||
|
|
||||||
|
默认有效期为 7 天。Worker 的定时任务会删除过期卡片的元数据、日报和缩略图,这是设计行为。
|
||||||
|
|
||||||
|
## 安全和隐私注意事项
|
||||||
|
|
||||||
|
- 日报可能包含敏感群聊内容。只分享你有权分享的内容。
|
||||||
|
- 获得分享 URL 的人,在过期前可能查看对应日报;当前实现不是按访问者身份授权。
|
||||||
|
- `UPLOAD_TOKEN` 是整个 Worker 的服务级密钥,不是每个用户独立的账号凭据。
|
||||||
|
- 不要把 `UPLOAD_TOKEN`、AppSecret 或 Wrangler 凭据提交到 Git。
|
||||||
|
- R2 保持私有,不要把 Bucket 直接公开。
|
||||||
|
- 建议定期轮换 `UPLOAD_TOKEN`,怀疑泄露时立即轮换。
|
||||||
|
- 微信 AppSecret 泄露时,应在微信后台重置,并立即更新 Worker Secret。
|
||||||
|
- 自托管者自行承担 Cloudflare 用量、域名、数据合规和微信平台规则相关责任。
|
||||||
|
|
||||||
|
## 当前实验性限制
|
||||||
|
|
||||||
|
- 需要用户自己部署,普通用户无法直接开箱使用;
|
||||||
|
- 使用一个共享的 `UPLOAD_TOKEN`,没有多用户账号系统;
|
||||||
|
- 分享链接在有效期内属于“知道链接即可访问”;
|
||||||
|
- 依赖微信 JS-SDK 和测试号能力,微信侧规则变化可能造成失效;
|
||||||
|
- 当前仅上传 PNG 日报和 JPEG 缩略图;
|
||||||
|
- 没有管理后台用于列出、提前删除或审计所有卡片;
|
||||||
|
- 过期清理由定时任务完成,不保证到期瞬间立即删除。
|
||||||
|
|
||||||
|
## 代码入口
|
||||||
|
|
||||||
|
- Worker:`services/share-card-worker/src/index.js`
|
||||||
|
- Worker 配置:`services/share-card-worker/wrangler.jsonc`
|
||||||
|
- Worker 测试:`services/share-card-worker/test/index.test.js`
|
||||||
|
- 桌面端上传:`src/main/wechat-share-card-service.ts`
|
||||||
|
- 本地加密配置:`src/main/wechat-share-config-store.ts`
|
||||||
|
- 分享弹窗:`src/renderer/src/components/reports/WechatShareCardDialog.tsx`
|
||||||
|
- 共享类型:`src/shared/wechat-share-card.ts`
|
||||||
|
|
||||||
|
## 参考资料
|
||||||
|
|
||||||
|
- [微信公众平台接口测试号](https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index)
|
||||||
|
- [微信 JS-SDK 官方文档](https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html)
|
||||||
|
- [Cloudflare Wrangler 命令文档](https://developers.cloudflare.com/workers/wrangler/commands/)
|
||||||
|
- [Cloudflare R2 Wrangler 命令](https://developers.cloudflare.com/workers/wrangler/commands/r2/)
|
||||||
|
- [在 Worker 中绑定和使用 R2](https://developers.cloudflare.com/r2/api/workers/workers-api-usage/)
|
||||||
|
- [Cloudflare Worker Custom Domains](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/)
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
# TraceMemo Local HTTP API / Agent API 能力审计
|
||||||
|
|
||||||
|
本报告基于当前代码、文档、IPC、Renderer 调用和相关测试做静态审计,对应当前发布应用版本 `2.5.0`;不连接真实微信数据库,也不修改生产代码、API Center 或测试。
|
||||||
|
|
||||||
|
主要代码入口:[http-server.ts](../../src/main/http-server.ts)、[automation-rule-store.ts](../../src/main/services/automation-rule-store.ts)、[group-exit-monitor-service.ts](../../src/main/services/group-exit-monitor-service.ts)、[group-stats-service.ts](../../src/main/services/group-stats-service.ts)、[api.md](../agent/api.md)。
|
||||||
|
|
||||||
|
## 1. Executive Summary
|
||||||
|
|
||||||
|
当前 HTTP 层声明了 **30 个 Method + Path 模板**:15 个 GET、12 个 POST、1 个 PATCH、1 个 DELETE、1 个 HEAD。共享 `LOCAL_API_ENDPOINTS` 定义 **14 个测试项**,但 Renderer 的 `API_ENDPOINTS` 实际只展示 **12 项**;个人微信能力和 `GET /scheduled-reports` 虽有共享定义,界面没有展示。界面也没有结构化 Query、媒体读取、定时日报写操作、退群监控、群统计、Automation 通用资源或执行日志查询。
|
||||||
|
|
||||||
|
当前不缺成熟的 Query primitive:结构化读取消息、Knowledge 搜索、消息前后文、会话概览和图片 OCR 搜索已经有 HTTP 契约。真正的能力缺口集中在 **监控配置、自动化规则通用管理、群员统计、执行历史、全局能力发现、发送状态和报告历史**。
|
||||||
|
|
||||||
|
几个影响后续设计的代码事实:
|
||||||
|
|
||||||
|
1. 定时日报已迁入 `AutomationRuleStore`。当前 `/scheduled-reports` 是旧 HTTP 契约的兼容投影,不是第二份活动规则存储;但它只表达“生成后发回来源群”,其他当前合法目标不会出现在该兼容 API 列表中。
|
||||||
|
2. 退群监控只负责监测范围、快照和事件历史;退群后如何通知由唯一的 `leave_notification` Automation 规则负责。二者应分别建资源。
|
||||||
|
3. 群统计 Service 足以提供活跃成员、当前沉默成员、时间范围、新鲜度和限制说明;它没有结构化的 former member 列表,也没有覆盖所有发言者的群总消息数。
|
||||||
|
4. 现有微信 Action Gateway 有策略、审计和幂等骨架,但策略目前主要校验收件人,以及 Automation purpose allowlist;手动用户 purpose 默认可放行。它只发个人微信,现有 `/agent/send` 则走 iLink 的 `WechatSendGateway`。两者不能直接视为统一的 Agent 安全边界。
|
||||||
|
5. HTTP 静态 GET handler 大多没有 method guard;对这些路径发 POST、PATCH 或 DELETE 仍会执行读处理。Automation store 的规则落盘失败会记录日志,但仍将内存中的规则返回为成功。
|
||||||
|
|
||||||
|
建议先做应用级能力发现、退群监控资源、严格校验后的 Automation CRUD/执行查询;发送和“立即执行”放到有 dry-run、明确确认、幂等键和 Action 审计的后续阶段。
|
||||||
|
|
||||||
|
## 2. Current API Inventory
|
||||||
|
|
||||||
|
以下按 `http-server.ts` 的路由分派和动态 route factory 盘点。静态读路由的 `Method` 是当前文档和产品语义的预期方法;实际接受方法的差异见本节末尾。
|
||||||
|
|
||||||
|
| Method | Path | 能力 | Read/Write/Execute | Service | API Center | 文档 | Agent 价值 |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| GET | `/api/v1/health` | HTTP 与数据库 ready 状态;唯一免 Token 路径 | Read | `isReady()` | 是 | 是 | 高:连通性 |
|
||||||
|
| GET | `/api/v1/current_time` | 本机时间、时区、日期 | Read | JavaScript `Date` | 是 | 是 | 中:相对日期换算 |
|
||||||
|
| GET | `/api/v1/contact` | 联系人和群列表;`filter`、`type`;使用异步 hydration | Read | `chat-service.listContactsAsync` | 是 | 是 | 高:标识发现与 resolve 前置 |
|
||||||
|
| GET | `/api/v1/chatroom` | 群聊列表;`keyword`;使用异步 hydration | Read | `chat-service.listContactsAsync` | 是 | 是 | 高:群标识发现 |
|
||||||
|
| GET | `/api/v1/recent_chat` | 最近会话;`limit` 默认 50 | Read | `chat-service.listRecentChat` | 是 | 是 | 高:导航/摘要 |
|
||||||
|
| GET | `/api/v1/chatlog` | 按 talker 和时间读取原始消息;移除 `contentData.aeskey` | Read | `chat-service.listMessages`、`resolveMd5` | 是 | 是 | 高,但旧式、未做结构化分页 |
|
||||||
|
| GET | `/api/v1/group_snapshot` | 群成员快照;必填 `md5` | Read | `chat-service.getGroupSnapshot` | 是 | 是 | 高:成员身份解析 |
|
||||||
|
| GET | `/api/v1/resolve` | 昵称、wxid、md5 解析为会话 | Read | `chat-service.resolveMd5` | 是 | 是 | 高;新资源宜返回稳定 ID 和歧义候选 |
|
||||||
|
| POST | `/api/v1/report` | 接收完整结构化日报并导出 HTML/PNG;当前拒绝外部 `templateRef` | Write:本地文件 | `group-report-service.exportGroupReport` | 是 | 是 | 低/中:低层渲染契约,Agent 须先拼完整结构 |
|
||||||
|
| POST | `/api/v1/agent/group-report` | 读群消息、调用 AI 生成群总结、导出 HTML/PNG | Execute:AI/本地文件 | `agent-group-report-service.generateAgentGroupReport` | 是 | 是 | 高但有模型费用/数据出站;结果不等同于报告历史记录 |
|
||||||
|
| GET | `/api/v1/agent/status` | Agent Hub、connector、数据 API、数据库状态 | Read | `agentHubService.getStatus` | 是 | 是 | 中:只覆盖 Agent Hub,不是应用总能力 |
|
||||||
|
| POST | `/api/v1/agent/send` | 通过 Agent Hub 连接器发送文字或媒体 | Execute:微信发送 | `agentHubService.testSend` → `WechatSendGateway`/iLink | 是 | 是 | 高但 R2;是测试入口,不含统一 Action policy/幂等确认 |
|
||||||
|
| GET | `/api/v1/wechat-personal/send-capability` | 个人微信 text/image/voice 能力状态 | Read | `PersonalWechatCapabilityService`,由 `ScheduledReportApiService` 包装 | 否(共享定义有,界面未展示) | 是 | 高但只代表 personal,不代表 iLink |
|
||||||
|
| GET | `/api/v1/scheduled-reports` | 列出旧 DTO 可表达的定时日报规则 | Read | `ScheduledReportApiService.list` → 规则投影 | 否(共享定义有,界面未展示) | 是 | 高但不完整:仅 `source_chat` 目标 |
|
||||||
|
| POST | `/api/v1/scheduled-reports` | 创建旧型定时日报,来源群即发送目标 | Write:规则配置 | `ScheduledReportApiService.create` → `AutomationRuleStore` | 否 | 是 | 高;功能受旧 DTO 限制 |
|
||||||
|
| GET | `/api/v1/scheduled-reports/{id}` | 单条旧型日报规则投影 | Read | `ScheduledReportApiService.get` → `AutomationRuleStore` | 否 | 是 | 中/高:只支持兼容投影规则 |
|
||||||
|
| PATCH | `/api/v1/scheduled-reports/{id}` | 修改旧型日报规则 | Write:规则配置 | `ScheduledReportApiService.update` → `AutomationRuleStore` | 否 | 是 | 高但只能改旧字段/目标 |
|
||||||
|
| DELETE | `/api/v1/scheduled-reports/{id}` | 删除旧型日报规则 | Write:删除配置 | `ScheduledReportApiService.delete` → `AutomationRuleStore` | 否 | 是 | 中;新 API 应标 R3 并防护系统规则 |
|
||||||
|
| POST | `/api/v1/scheduled-reports/{id}/enable` | 启用规则 | Write:配置/未来执行 | `ScheduledReportApiService.setEnabled` → `AutomationRuleStore` | 否 | 是 | 高;启用后未来可能发送微信 |
|
||||||
|
| POST | `/api/v1/scheduled-reports/{id}/disable` | 暂停规则 | Write:配置 | `ScheduledReportApiService.setEnabled` → `AutomationRuleStore` | 否 | 是 | 高 |
|
||||||
|
| POST | `/api/v1/scheduled-reports/{id}/run` | 手动触发完整日报规则 | Execute:可能调用 AI、保存历史、发微信 | `ScheduledReportService.runScheduledReportNow` → `AutomationService` | 否 | 是 | 高但 R2;当前无 HTTP 幂等键/确认 |
|
||||||
|
| GET | `/api/v1/scheduled-reports/{id}/executions` | 旧型 execution 历史和新 Automation execution 投影 | Read | `ScheduledReportService.listExecutions` | 否 | 是 | 高但旧响应模型/有限留存 |
|
||||||
|
| POST | `/api/v1/scheduled-reports/executions/{executionId}/retry-send` | 旧文档称复用 PNG 重发 | Execute 路由存在但当前固定 `501 not_supported` | `ScheduledReportApiService.retrySend` | 否 | **路径有,语义已失效** | 无:不能重发 |
|
||||||
|
| GET | `/api/v1/media/{mediaId}` | 读取消息关联的图片二进制 | Read | `http-media-service.readImageMedia` | 否 | 是 | 高:图像证据 |
|
||||||
|
| HEAD | `/api/v1/media/{mediaId}` | 图片资源存在性/响应头 | Read | `http-media-service.readImageMedia` | 否 | 否 | 低/中 |
|
||||||
|
| GET | `/api/v1/query/capabilities` | Query Tool 支持的结构化操作、范围和上限 | Read | `LocalQueryApiService.capabilities` | 否 | 是(独立章节) | 高,但不是 TraceMemo 应用能力清单 |
|
||||||
|
| POST | `/api/v1/query/messages` | 单会话、范围、时间、方向、类型等确定性消息读取 | Read | `LocalQueryApiService.messages` | 否 | 是 | 高:推荐 Query primitive |
|
||||||
|
| POST | `/api/v1/query/search` | Knowledge 关键词检索,返回覆盖、新鲜度和 OCR 命中 | Read | `LocalQueryApiService.search` + `KnowledgeSearchService` | 否 | 是 | 高:必须读取 coverage/freshness |
|
||||||
|
| POST | `/api/v1/query/message-context` | 通过 opaque `messageRef` 读取前后文 | Read | `LocalQueryApiService.context` | 否 | 是 | 高:稳定消息引用 |
|
||||||
|
| POST | `/api/v1/query/conversation-overview` | 单会话范围的概览证据和 source coverage | Read | `LocalQueryApiService.overview` | 否 | 是 | 高:broad summary |
|
||||||
|
|
||||||
|
路由来源:[http-server.ts](../../src/main/http-server.ts#L212)、scheduled/query/media route factory(同文件 L404-L670)。总数是代码中声明的业务方法模板,不代表静态 handler 都正确拒绝其他动词:`/health`、`/current_time`、`/contact`、`/chatroom`、`/recent_chat`、`/chatlog`、`/group_snapshot`、`/resolve`、`/agent/status` 没有检查 `req.method`。这些路径携带错误动词仍会走同一 handler;特别是 `POST /health` 也绕过 Token 检查,因为鉴权例外按 pathname 判断。新/旧路由都应 fail closed 并对不支持的方法返回 405。
|
||||||
|
|
||||||
|
全局 `OPTIONS` 在路由和鉴权前处理;媒体额外支持 HEAD。非 health 路径要求 `Authorization: Bearer …`,但 `readBody` 没有大小上限。路由外层目前没有统一请求 schema、统一错误 envelope 或 request id。
|
||||||
|
|
||||||
|
## 3. Internal Capability Inventory
|
||||||
|
|
||||||
|
这里按产品能力追 Service → IPC/Renderer → HTTP → 外部 Agent,而不是按当前 API 名字扩展。
|
||||||
|
|
||||||
|
| 业务域 | 已有能力及实现 | IPC / Renderer | HTTP 现状 | Agent 结论 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Chat / Contact | 联系人、群聊、最近会话、解析、历史消息、群快照、消息周边上下文、媒体定位;`contact-resolution-service` 可精确匹配别名并返回歧义候选 | `db:getContacts`、`db:getGroupSnapshot`、消息查询/around 等,Chat/Contact/档案 UI | 旧 Reader routes + `/query/*` + 图片 `/media/{id}` | 已有 Read API 基础完整。新配置应使用 `m_nsUsrName` 对应的 wxid/roomId 等稳定 ID,不应把昵称当长期键 |
|
||||||
|
| Query / Knowledge | `messages`、`search`、`message-context`、`conversation-overview`;Knowledge 索引覆盖/新鲜度;Image OCR 可进入 Knowledge 搜索和证据 | `knowledge:search/getStatus/startIndex/cancelIndex`、AI Search UI、`LocalQueryToolExecutor` | 四个 Query 操作均已 HTTP 化;`query/capabilities` 只描述 Query Tool | 没有需要重做的基础 Query primitive。缺的是统一应用 capability/status、更多外层筛选和 API Center 展示 |
|
||||||
|
| Group Analytics | 活跃/沉默当前成员、每人 messageCount/lastMessageTime、窗口时间、memberCount、unattributed/system 消息、firstMessageTime、freshness/complete/limitations;索引未新鲜时最多等待 2 秒并如实降级 | `group-stats:getMemberStats`;聊天页群统计 UI | 无 | P1 候选。当前 query 要求群 `userMd5` + epoch 毫秒。former sender 只以限制文案给出数量,没有 formerMembers 数组/结构化计数;没有覆盖 former sender 的群总消息数字段,需扩 DTO 后再承诺 |
|
||||||
|
| Group Exit Monitor | enabled/running/nativeMonitor 状态、监控 roomId 集合、lastChecked、unread、事件历史;每次回传最多 500 条,但完整事件历史 append-only 长期保存;可立即 check、改范围、启停、筛事件、清历史、mark read | `group-exit-monitor:*`;`GroupExitMonitorWorkspace` | 无 | P0。拆成 monitor state/config、events、check。`checkNow` 可能发现事件并触发自动通知,不是纯读操作;重启监控会重建快照基线,暂停期成员变化不会补报 |
|
||||||
|
| Automation | 实际规则类型:`daily_report`、`scheduled_report`、`leave_notification`。`daily_report` 是现有消息触发条件/动作链,不是任意流程引擎;退群通知为固定 ID singleton。规则 CRUD、enable、执行记录读写均已有 Service/Store | `automation:*`;AutomationWorkspace 有规则、日志、定时执行、启停、删除和状态 UI | 没有通用 Automation API;仅 scheduled-report 兼容映射 | P0。使用 typed rule union;不要把内部任意 draft 原样开放。规则创建当前缺省 enabled=true,未知值会被归一化成默认;需 HTTP 严格校验,先 disabled + validate,再显式 enable |
|
||||||
|
| WeChat Send / Action | `WechatSendGateway` 有 personal/iLink transport resolution、text/image/voice/file 统一类型及 Send Log;`WechatActionGateway` 做 capability preflight、Automation purpose allowlist、Action audit、幂等和 Automation 3 秒间隔 | `wechat-personal:send`、`sendGeneratedTtsVoice`、`wechat-action-log:list`、Agent Hub connector 相关 IPC/UI | `/agent/send` 只走 Agent Hub/iLink 测试发送;个人 capability 有独立 GET;两类日志没有 HTTP | 分 transport 公布 capability;R2 send 通过经审计的业务门面,不直接暴露底层 gateway。现有 Action Gateway 还不是普适安全策略:`triggerType=user` 不按 purpose 限制;普通 `/agent/send` 没传 idempotency key,也没有 Action audit |
|
||||||
|
| Agent Hub | status、connector login/reconnect/disconnect、notification recipient/send、logs、conversation list/detail/clear、入站 inbox retry | `agent-hub:*`;Agent Hub UI | 仅 `/agent/status` 和 `/agent/send`;无 conversation/log HTTP | status 有只读价值。对话记录包含完整收发正文;inbox 包含 context token/raw items。Connector 生命周期、登录 QR/验证码、收件箱和通知 recipient 应保持 internal |
|
||||||
|
| Reports / Templates | 手动 report render/export;AI group report;本地 Report History list/save/update template/delete;内置/已安装模板和市场 catalog/install/uninstall | `report:*`、`report-template:*`、`report-template-market:*`;Reports 与 Template Market UI | `/report` 低层 export,`/agent/group-report` AI 生成;没有 history/template API | P1:只读 Report History 元数据/资产可分离设计。现有 `listGeneratedReports` 会读取每张 PNG 为 base64,并返回结构快照和本机绝对路径,不可原样直出。模板目录可读列入 P2;安装/卸载涉及网络与本地包写入,不宜第一批开放 |
|
||||||
|
| Recall Archive | 后台监听撤回变化,最多按会话存归档消息/撤回记录;chat-service 将 archive merge 到历史读结果 | 没有独立 CRUD IPC;设置开关和消息渲染 | 没有独立 archive API;旧 `/chatlog` 可能随底层消息返回 `recalled` 标记;Query DTO 未声明 recalled 字段 | 不开放原始 archive 管理。后续 Query 应明确返回 `recalled`/来源,避免把已撤回归档当普通消息证据 |
|
||||||
|
| OCR / Image Insight | System OCR 本地识别;image-text-index status/count/start/pause/resume/cancel/clear/repair;Image Insight 读/解密图片并可调用 AI Provider | `system-ocr:*`、`image-text-index:*`、`image:*` IPC;Search/Report UI | OCR 派生文本可通过 `/query/search` 得到;索引管理、单图 AI 分析无 HTTP | 已有搜索能力可用。状态可纳入 capability/status;索引删除、key/decoder 配置、任意图像 AI 分析涉及成本、私密图片和索引破坏,不列第一批 |
|
||||||
|
| 系统 / 数据 / 其他 | account discovery、DB key 管理、数据库 connect/root 重开、设置写入、cache summary/clear、app update、voice/TTS、export/import、Reader Skill 本地安装信息 | 多组 IPC;Settings、Cache、Export、Update、Voice UI | 无相应 HTTP API | 只读脱敏运行状态可按需求列 P2;DB key/root、通用 settings patch、cache 清理、任意文件路径、更新安装、TTS synthesis 等保持 internal |
|
||||||
|
|
||||||
|
### A. Chat / Contact 与 ID 语义
|
||||||
|
|
||||||
|
- `/contact`、`/chatroom` 改用 `listContactsAsync`,因为 macOS Session 可能只有原始 wxid/chatroom id,需要 hydrate 显示名;`ScheduledReportApiService` 却使用同步 `listContacts()` 解析群名。稳定 `talker` 可直接解析,但名称输入在需要 hydration 的运行时可能失败/退化。这是可复用 adapter 应统一异步解析的理由。
|
||||||
|
- ID 现在不是一个口径:旧 Query 的 `scope.conversationId`/`target` 解析为 `Contact.md5`;`group-stats` 传 `userMd5`;监控用 `roomId`(`xxx@chatroom`);新的 scheduled automation 用 `sourceConversationId`(wxid/roomId);老 HTTP 路由混用昵称、wxid、md5。保持已有 Reader 契约不动,新 API facade 应统一对外 canonical `conversationId`(底层当前联系人的稳定 username/wxid 或 roomId),并在 main adapter 转为服务所需 md5。名称只做 resolve,不持久化到规则。
|
||||||
|
- `chatlog` 时间边界是 Unix 秒,Query `absolute` 内部也是秒,而 group stats IPC 是 epoch 毫秒;新 Agent 资源建议用带时区 ISO-8601 输入/输出,并在 facade 单点转换。
|
||||||
|
- `/query/messages` 有 200 上限,`messageRef` 是 opaque 稳定引用;图片 OCR 文字和 Coverage 分开呈现。`/chatlog` 则支持旧 talker/time 风格但读取结果没有同等结构边界;作为兼容 Reader 保留,不作为新 Agent 配置/分析的默认接口。
|
||||||
|
|
||||||
|
### B. Group Exit Monitor 与 Leave Notification
|
||||||
|
|
||||||
|
`GroupExitMonitorService` 的真实 IPC 有 `getState`、`setEnabled`、`setGroups`、`checkNow`、`listEvents`、`clearEvents`、`markRead`。事件是群成员差异事实,包含 roomId、member wxid/name、previous/current count、detectedAt;monitor state 中 `events` 只是最近 500 条快照,`totalEventCount` 对应完整内存历史。
|
||||||
|
|
||||||
|
`AutomationService.handleGroupExit` 只处理 `BUILTIN_LEAVE_NOTIFICATION_RULE_ID` 对应的 singleton 规则。规则另有 `notifyScope` / `notifyRoomIds` 二次范围、target、template。Agent 配“监控 A/B/C”需改 monitor 范围;配置通知目标/通知哪些被监控群则另改这条 leave notification automation。两者不能合并为 `/monitors/{id}/notify`。
|
||||||
|
|
||||||
|
### C. Automation 与 Scheduled Report
|
||||||
|
|
||||||
|
`AutomationRuleStore` 的真实方法有 `listRules/getRule/createRule/updateRule/saveLeaveNotificationRule/deleteRule/setRuleEnabled`;`AutomationExecutionLogService` 提供 `list/record/clear/countSince`。执行日志最多留存 200 条,clear 属于破坏性操作。规则在 `{userData}/automation/rules.json` 中 JSON 持久化。
|
||||||
|
|
||||||
|
`scheduled-report-service.ts` 的调度来源是 `automationRuleStore.listRules()`,执行交给 `AutomationService.executeScheduledRule()`,execution 从 Automation Log 投影。迁移后的旧 `tasks.json`/`executions.json` 是只读历史存档。旧 HTTP API 通过 `ScheduledReportApiService` 转换旧 DTO;创建、修改、删除、启停最终也是读写 `AutomationRuleStore`。所以正确方案是保留兼容 facade,并建立 Automation canonical API,不要继续增加第二个 scheduled-report store。
|
||||||
|
|
||||||
|
旧 facade 的限制:只列/操作可投影为 `target.type === 'wechat_group'`、且目标等于来源群的规则。如今 scheduled automation 支持 source_chat/self/file_transfer/contact,故通过新 UI 创建为文件传输助手或联系人目标的规则,会从旧 `/scheduled-reports` 列表隐藏。旧 API 输入 schema 也不能表示完整 scheduled config(成员名、消息类型、模板、timeout、postfix 等)。
|
||||||
|
|
||||||
|
写 API 前还需处理 `AutomationRuleStore` 的归一化和持久化契约:未知 ruleType 会降成 `daily_report`,大部分错误枚举会静默落安全默认;缺省 enabled 是 true;`persist()` catch 写盘错误后只记 warning,Store 仍返回创建/更新后的对象。HTTP adapter 必须先 strict validate,且 Store 需要可观察的持久化结果,不能把内存态冒充成功。
|
||||||
|
|
||||||
|
### D. Group Analytics 确认项
|
||||||
|
|
||||||
|
`GroupStatsService.getMemberStats` 已有可直接复用的核心计算;接口具体有:
|
||||||
|
|
||||||
|
- 当前群成员:`memberCount`、`activeMembers`、`silentMembers`、各活跃成员 `messageCount`/`lastMessageTime`;
|
||||||
|
- 查询窗口:`startTime`、`endTime`(epoch ms)、`firstMessageTime`;
|
||||||
|
- 数据完整性:`freshness = fresh|stale|unknown`、`complete`、`limitations`;
|
||||||
|
- 诊断:`unattributedMessages`、`excludedSystemMessages`。
|
||||||
|
|
||||||
|
成员名单是当前成员集合;知识库统计的 sender 不在当前集合时被排除,只在 `limitations` 中增加“另有 N 位窗口内发言者已不在当前群成员名单”。Service 不返回其身份/每人消息数,也没有 `totalMessageCount`。若 Agent 需要“前成员榜”或全群消息数,需要先扩展 Service/shared type;不能由 API adapter 从 limitation 文案反解析。
|
||||||
|
|
||||||
|
## 4. API / Docs / API Center Drift
|
||||||
|
|
||||||
|
| 项目 | 代码事实 | 漂移/影响 |
|
||||||
|
|---|---|---|
|
||||||
|
| HTTP、共享定义与界面列表 | HTTP 有 30 个 method/path 模板;`LOCAL_API_ENDPOINTS` 定义 14 项,Renderer `API_ENDPOINTS` 实际展示 12 项 | 16 个 HTTP 操作模板没有共享定义;另有 2 个已定义项(个人微信能力、定时日报列表)没有展示。界面仅呈现 12/30 项,不能作为完整 API catalog |
|
||||||
|
| Scheduled Report 展示 | 共享定义只有 `GET /scheduled-reports`;该项本身也未进入 Renderer 列表 | POST 和 task action 不显示,GET 列表也不显示;Agent 在 API Center 里无法发现这组 API |
|
||||||
|
| WeChat Capability 展示 | 共享定义有 `GET /wechat-personal/send-capability`;Renderer 列表未包含它 | API Center 看不到个人微信发送能力状态,用户可能误把 Agent Hub 状态当成完整发送能力 |
|
||||||
|
| Query 展示 | Query 文档在 `api.md` 的独立 LLM-friendly 章节,Service/HTTP 实现完整 | API Center 看不到;用户可能误认为 Reader API 仍只有旧 chatlog |
|
||||||
|
| Media 方法 | `/media/{mediaId}` 支持 GET、HEAD | 文档仅列 GET;API Center 都未列 |
|
||||||
|
| Retry Send | 文档表称 retry-send“复用已有 PNG 重试发送” | `ScheduledReportApiService.retrySend()` 当前无条件抛 `501 not_supported`;integration/unit tests 也未覆盖 retry 路由的这项现状 |
|
||||||
|
| Scheduled Report 完整性 | 旧 facade 只 project `source_chat` | UI 可保存的其他 scheduled target 会从旧 API list/get 隐藏;不是两份存储,但旧 API 不是 Automation API 的完整别名 |
|
||||||
|
| Health 版本 | `/health` 固定返回 `version: "1.0.0"` | 与当前 package version `2.5.0` 不同,Agent 无法据此判断应用版本 |
|
||||||
|
| HTTP 动词 | 九个静态 GET 语义路由无 method guard | POST/PATCH/DELETE 等也可能调用读取逻辑;`/health` 任意 method 均免 Token。测试目前未锁定统一 405 契约 |
|
||||||
|
| 请求/错误 schema | JSON parsing 和错误形状分散:通用 `sendError`、ScheduledReport 专用 error、Query status body、业务自身 result | Agent 要写多套解析逻辑;共享 API schema 和统一错误 code 不存在 |
|
||||||
|
| 命名 | `/contact`、`/chatroom`、`/recent_chat`、`/group_snapshot` 与 `/scheduled-reports`、`/query/*`、`/agent/*`、`/wechat-personal/*` 并存 | snake_case 旧路径、资源路径和“Agent 为业务 owner”的命名混杂;新接口不能继续沿用此漂移 |
|
||||||
|
|
||||||
|
文档 [api.md](../agent/api.md#L35) 基本列出当前 HTTP 路径,Query 在后续单独说明;除 HEAD 外没有发现漏写的当前业务路径,但 retry-send 的成功语义过期。API Center 的来源是单独的 [local-api-test.ts](../../src/shared/local-api-test.ts) 和 [apiEndpoints.ts](../../src/renderer/src/features/api-center/model/apiEndpoints.ts),没有从 HTTP route/schema 派生。测试现有 `local-api-auth` 覆盖鉴权、媒体、部分 Query 和 Agent send;`scheduled-report-api` 覆盖旧生命周期;`local-api-contact-search` 覆盖 hydrate。它们没有自动比对 HTTP route、文档、Catalog 三者,也没有覆盖全部 method guard 和 retry-send。
|
||||||
|
|
||||||
|
## 5. Candidate API Matrix
|
||||||
|
|
||||||
|
风险按本任务口径:R0 只读;R1 本地配置/应用状态修改;R2 微信发送、AI/provider 调用等外部副作用;R3 删除或清空不可轻易恢复的数据。R1 不代表没有后续行为:enable 一条定时规则会武装未来的 R2 执行。
|
||||||
|
|
||||||
|
| Capability | 当前实现 | 当前 API | 建议 | Agent 用例 | Risk | Priority |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 联系人/群/会话 resolve | Chat Service + Contact Resolution | 有旧 routes;Query 内 resolve | 保留旧路由;新 resource 返回稳定 ID、歧义候选 | 查找群并取得 roomId | R0 | P0(复用) |
|
||||||
|
| 结构化消息/搜索/上下文/概览 | LocalQueryApiService + Knowledge | `/query/*` | 保持契约;加 route schema/catalog,后续可升级稳定 ID | 查聊天、关键词/OCR、补上下文 | R0 | P0(复用) |
|
||||||
|
| 应用 capability discovery | 各 Service 能回答局部状态 | 无;`query/capabilities` 仅 Query Tools | 新 `GET /capabilities`,区分 supported/available/reason/operations | 发现自动化、监控、统计、发送 transport | R0 | P0 |
|
||||||
|
| Group Exit Monitor 状态/范围 | GroupExitMonitorService | 仅 IPC | GET state + PATCH enabled/roomIds | 查看监控、监控/停止一个群 | R0/R1 | P0 |
|
||||||
|
| Group Exit events | Monitor JSONL + listEvents | 仅 IPC | GET 带 stable roomId/time/cursor/limit | 最近 7 天谁退群 | R0 | P0 |
|
||||||
|
| 手动检查退群 | checkNow 会扫描并触发事件 handler | 仅 IPC | 有外部通知时按 R2 操作开放,先 validate effects + confirm | 立即检查一次 | R2 | P1 |
|
||||||
|
| Automation 规则 CRUD | AutomationRuleStore | 通用 IPC;HTTP 仅旧 scheduled facade | typed union CRUD;create disabled;validate 再 enable;保护 singleton/system rules | 创建、列出、修改、暂停自动化 | R1/R3(delete) | P0 |
|
||||||
|
| Automation validation/dry-run | 现有编辑器 preview 分散;无通用 validator API | 无 | `POST /automations/validate`;不落盘、不发送 | 确认群、目标、模板、下次运行和能力 | R0 | P0 |
|
||||||
|
| Automation execution history | AutomationExecutionLogService,最多 200 条 | schedule 专属旧投影 | 规范化 Automation execution 读接口;清日志不开放第一批 | 查看失败、按 rule 过滤 | R0/R3(clear) | P0 |
|
||||||
|
| Group member stats | GroupStatsService | 仅 IPC | 按稳定 group ID + ISO window 读统计;先补 former/total 语义 | 近 30 天活跃榜 | R0 | P1 |
|
||||||
|
| WeChat capability | personal capability service;Agent Hub status | personal GET + Agent status | 新全局 capability 含 personal/iLink 和内容能力;旧路由保留 | 检查发送当前是否可用 | R0 | P0 |
|
||||||
|
| 手动微信发送 | WechatSendGateway + Action Gateway | `/agent/send` iLink test send | 新 send command 经受限 Action facade,强制 stable recipient、confirm、idempotency | 文件助手测试消息 | R2 | P1 |
|
||||||
|
| Send Log / Action audit | Send Log 500 条;Action audit 500 条;IPC action-log | 无 HTTP | 分层只读分页,preview 脱敏;按 executionId/requestId 关联 | 查最近发送失败、审计规则动作 | R0 | P1 |
|
||||||
|
| Agent Hub status | AgentHubService.getStatus | `/agent/status` + IPC | 保留 alias,新资源名归 `/agent-hub/status`,与 app capabilities 分开 | 查 Hub/connector online | R0 | P1(复用) |
|
||||||
|
| Agent Hub 对话内容 | Conversation Store,最多 50 会话×500 条 | 仅 IPC | 默认为 Internal;若产品确认需要,另做显式 opt-in、分页/时间过滤 | 查看机器人与某人的对话 | R0(高隐私) | 不建议第一批 |
|
||||||
|
| AI 群日报 | AgentGroupReportService + export | `/agent/group-report` | 保留兼容;未来先 validate model/range/group/data egress,再异步 job | 生成临时总结图片 | R2(provider/本地文件) | P1 |
|
||||||
|
| 日报历史 | ReportHistory Service,IPC CRUD | 无 | 分页 metadata DTO;图片 asset 单独下载;不返回 base64/路径/完整 snapshot | 昨天生成过哪些日报 | R0 | P1 |
|
||||||
|
| 模板列表 | Template Service + market catalog | 仅 IPC | 仅已安装模板只读列表列 P2 | 有哪些日报模板 | R0 | P2 |
|
||||||
|
| 模板安装/删除/历史改版 | Template Service/Market + Report History | 仅 IPC | 不开放通用 HTML/路径写入;将来单独授权且保留校验 | 安装或修改模板 | R1/R3 | Maybe/P2 |
|
||||||
|
| Recall archive 查询 | 内部 archive merge 到历史消息 | 无独立 API | 不单独开放磁盘 Archive;给 Query 增 `recalled` 来源标记 | 找被撤回消息 | R0(敏感/语义风险) | P2 |
|
||||||
|
| Image OCR index 操作 | image-text-index service | IPC(含 clear/repair) | coverage 状态可汇入 capabilities/status;不让 Agent 操作 clear/reset | 查 OCR 覆盖 | R0/R1/R3 | P2 |
|
||||||
|
| AI image insight | ImageInsightService 读/解密图片并调 vision provider | IPC | 不暴露任意 hash/message AI 分析,除非有成本/隐私授权 | 理解群图片 | R2 | 不建议第一批 |
|
||||||
|
| DB key、根目录、settings、cache | 多个设置/DB/cache Service | IPC/UI | 禁止通用 settings patch / 文件路径 / DB key API;只加白名单状态字段 | 修改本机数据库、安全设置 | R1/R3 | 不建议开放 |
|
||||||
|
| Connector 生命周期/inbox | AgentHubService + WechatInboundInbox | 仅 IPC/内部 | connector 登录、验证码、QR、inbox、context token 不对 Agent 暴露 | 重连或直接拿入站 token | R1/R2 | 不建议开放 |
|
||||||
|
|
||||||
|
## 6. P0 Recommendation
|
||||||
|
|
||||||
|
第一批目标是“让 Agent 能配置和核验 TraceMemo,但不意外发消息”。建议只包括:
|
||||||
|
|
||||||
|
1. `GET /api/v1/capabilities`:应用级 capability,不与现有 `/query/capabilities` 合并。返回版本、DB/readiness、supported vs available、不可用原因和依赖;发送分 personal/iLink 与 text/image/voice 能力。
|
||||||
|
2. Group Exit Monitor:读取状态、显式配置 monitored roomIds、读取历史事件。`PATCH` 只接 canonical 群 ID,拒绝不存在/非群 ID;修改范围响应明确显示后台 baseline/check 状态。`check` 先列 P1,因为它可能启动退群通知发送。
|
||||||
|
3. Automation typed CRUD:列/读规则、创建 disabled 规则、更新、显式启停、执行历史读取。Leave Notification 仍使用固定 singleton id,不允许创建重复规则;拒绝未知字段/未知 enum,而不是靠 `normalizeRuleDraft` 静默修正。
|
||||||
|
4. `POST /automations/validate`:验证目标群、稳定通知 recipient、模板变量、report/template 配置、send capability 和 nextRunAt;只返回 plan,不落盘、不发送。
|
||||||
|
5. 运行状态与错误契约:一致的 405、最大 body、请求 ID、错误 envelope;这是任何新 Agent 写接口前的 foundation,不是大规模权限系统。
|
||||||
|
|
||||||
|
Agent 实现示例(概念流程):
|
||||||
|
|
||||||
|
- “监控 A/B/C 退群”:resolve 三个群为 roomId → validate scope → PATCH monitor group IDs。
|
||||||
|
- “A 群有人退出就通知文件助手”:读取 monitor scope 和 singleton leave rule → validate 类型/notify scope/target → 更新规则但保持 disabled → 用户/Agent 明确 enable。监控与 leave-notification 是两份正交配置。
|
||||||
|
- “每天 20:00 生成产品群日报”:resolve sourceConversationId → validate scheduled rule(包含 target/transport/模板/时区/next run)→ 创建 disabled → 显式 enable。旧 `/scheduled-reports` 无法表达所有当前 config,不承担新 Agent CRUD。
|
||||||
|
- “昨天哪些自动化失败”:读 automation executions,按本机 timezone/UTC offset 和 status 查询,不清理日志。
|
||||||
|
|
||||||
|
## 7. Proposed Resource Model
|
||||||
|
|
||||||
|
采用业务 capability 资源,HTTP server 只负责 transport、auth、body、统一错误;每个 domain route 调用独立的 main-process API facade/Service adapter。Facade 复用当前 Service/Store,不把 UI IPC 当 HTTP RPC 转发层。
|
||||||
|
|
||||||
|
| Resource | 职责 | 现有路径的处理 |
|
||||||
|
|---|---|---|
|
||||||
|
| `system` | health、版本、应用级 capabilities、运行状态 | `/health` 保留;新增 `/capabilities`;`query/capabilities` 不改语义 |
|
||||||
|
| `contacts` / `groups` | 稳定 ID 列表、resolve、群成员快照/统计 | `/contact`、`/chatroom`、`/resolve`、`/group_snapshot` 保留兼容 |
|
||||||
|
| `query` | 消息/搜索/上下文/概览证据 | 现有 `/query/*` 保持;query ID 口径升级需兼容 reader skill |
|
||||||
|
| `monitors` | 退群监控范围、启停、事件、显式 check | 新 `/monitors/group-exits`,与通知规则分离 |
|
||||||
|
| `automations` | 规则 typed CRUD、validate、启停和运行 | 新 `/automations` 是 canonical HTTP resource;底层仍由 `AutomationRuleStore` 存储 |
|
||||||
|
| `executions` | 跨 Automation/Action/Send 的只读运行视图 | 新 `/executions` read model,不合并各自写存储 |
|
||||||
|
| `wechat` | transport capabilities、受控 send command、Send Log/Action audit | `/agent/send` 与 `/wechat-personal/send-capability` 保留兼容 |
|
||||||
|
| `reports` | report history 元数据/asset;installed templates read-only | `/report` 与 `/agent/group-report` 保留为不同兼容操作 |
|
||||||
|
| `agent-hub` | Hub/connector 状态;conversation API 默认 internal | `/agent/status` 保留 alias,不复用 app capability |
|
||||||
|
| `developer` | 高级 raw request tester 与诊断 | API Center 的 tester;不作为 Agent 业务 API |
|
||||||
|
|
||||||
|
共享资源原则:新 API 输入以 stable id 为主、名字只用于 resolve;所有写请求 strict validate;时间对新资源用 offset ISO-8601;分页使用 `limit` + `cursor`;成功/失败使用一个 typed envelope;不直接返回 app userData path、token、context token、AES key 或原始 transport payload。
|
||||||
|
|
||||||
|
## 8. Safety Model
|
||||||
|
|
||||||
|
### 当前边界
|
||||||
|
|
||||||
|
- 默认监听 `127.0.0.1:6131`,但 host/port 由设置和 `api:start` 调用传入,API Center 会警告非 loopback。CORS 只允许 loopback Origin,但不带 Origin 的 curl/Agent 请求仍可用 Token;CORS 不是本地进程授权边界。
|
||||||
|
- `/health` 公开,其余 endpoint 共用单一 Bearer Token。Token 为 32 random bytes、safeStorage 加密存储并设 `0600`,没有 read/config/send scope。拿到 token 即可读取聊天,也能建/删/启停规则、立即发送。
|
||||||
|
- HTTP `/agent/send` 的消息进入 `WechatSendGateway`,因此有低层 Send Log;但没有 `WechatActionGateway` 的业务 Action audit/策略决策,也没有调用方 Idempotency-Key。Personal capability 路由只报告个人微信状态。
|
||||||
|
- `WechatActionGateway` 的请求包含 `purpose`、`triggerType`、recipient、content、`idempotencyKey`、`executionId`;Automation trigger 有 purpose allowlist,sender capability 会先检查,审计记录会保存 content preview/hash。当前 `evaluateWechatActionPolicy` 对 `triggerType=user` 不做 purpose allowlist,`shouldUseAiPolicy` 固定 false;幂等只对显式 key 或历史特定 scheduled request 生效。它目前只发个人微信,不能直接替换 iLink send。
|
||||||
|
- Action Audit 和 Send Log 各自上限 500;Automation Execution Log 上限 200。三类日志粒度不同,不是重复的同一事实。
|
||||||
|
|
||||||
|
### 分级建议
|
||||||
|
|
||||||
|
| 风险 | 操作 | 建议控制 |
|
||||||
|
|---|---|---|
|
||||||
|
| R0 | 查询聊天/成员/事件/状态/统计/日志/报告元数据 | 保留本地 Token;响应明确 scope、coverage、freshness、隐私字段 |
|
||||||
|
| R1 | 修改监控群、Automation 配置、启停、模板设置 | typed validation、dry-run plan、显示持久化成功;enable 需确认它武装未来发送 |
|
||||||
|
| R2 | 微信发送、立即执行日报/退群通知 check、调用 AI Provider 生成报告 | 明确 recipient 和内容/规则;`Idempotency-Key` 必填;统一 Action policy + Action audit + Send Log;重复请求回放原结果;返回发送状态 |
|
||||||
|
| R3 | 清理退群事件、删除 report、清空执行/发送/Action 日志、清 cache/index、删规则 | 初始不开放;如以后开放,独立权限、预览计数、可恢复备份/本地确认,不接受批量 wildcard |
|
||||||
|
|
||||||
|
现有 Bearer Token 不足以支撑“查询、配置、发送、清理”都对一个不受信 Agent 开放。近期不必造完整账户系统,但应先修路由 method/body/validation,提供默认只读或 disabled 配置工作流;后续可加入多个 named token + scope(`read`, `configure`, `send`, `destructive`),R2 请求确认和 durable idempotency。风险分类也需承认本地 artifact write(如 `/report` 导出)不是配置本身,可先按 R1 local-write 处理。
|
||||||
|
|
||||||
|
## 9. Compatibility Plan
|
||||||
|
|
||||||
|
1. 不 rename/remove 任何现有 route。Reader Skill 依赖旧 contacts/chatlog/media 路径;新 structured query 已经是更合适的 Agent query,但两者并行。
|
||||||
|
2. 新 `/automations` 读写同一 `AutomationRuleStore`。旧 `/scheduled-reports*` 改为明确标注 Deprecated 的 compatibility adapter;维持现有 request/response shape 和 source_chat 子集,不维护第二个任务存储。响应可加 `Deprecation` header/文档说明,未定 sunset 前不返回 breaking error。
|
||||||
|
3. `/scheduled-reports` 的投影不能假装覆盖所有 scheduled automation。旧 list/get 只反映 source_chat;新 clients 必须迁到 `/automations?type=scheduled_report`。旧 `retry-send` 保留返回 501,文档明确废弃;不能伪造已发送成功。
|
||||||
|
4. `/agent/send` 继续表示现有 iLink/Agent Hub 测试发送。新 `/wechat/send` 必须先明确 transport/recipient schema,再通过能统一 personal+iLink 的受控 Action facade;如果不能保留旧发送语义,就将旧路径作为 adapter 而不是简单 alias。
|
||||||
|
5. `/wechat-personal/send-capability` 保持 personal-only 兼容 view;新全局 capability 返回 transport map。应用能力 `/capabilities` 和 Query Tool 的 `/query/capabilities` 各自有清晰不同的契约。
|
||||||
|
6. 同一 shared contract/catalog 应供 HTTP 验证、API Center、文档和 route contract tests 使用;把实际路由、文档和 API Center 三者 drift 变成测试失败,而不是发布后人工发现。
|
||||||
|
|
||||||
|
## 10. Proposed Phase Plan
|
||||||
|
|
||||||
|
### Phase A — HTTP Contract / Facade
|
||||||
|
|
||||||
|
给现有 routes 加明确 method guard、body size limit、严格 shared request schema、统一错误 envelope/request id;补 `AutomationRuleStore` 写盘成功/失败结果;建立稳定 conversation ID adapter 和 route contract tests。保留 raw Node HTTP,不需要为第一阶段换 web framework。
|
||||||
|
|
||||||
|
### Phase B — Read + Validation P0
|
||||||
|
|
||||||
|
增加应用 `/capabilities`、monitor state/events、automation rules/executions 读取、`/automations/validate`、group stats adapter。monitor `check` 因可能触发 notification 暂留 P1 或先加 side-effect confirmation。把新资源路径、schema、风险元数据接进 EndpointCatalog/生成文档。
|
||||||
|
|
||||||
|
### Phase C — Automation Configuration
|
||||||
|
|
||||||
|
开放 disabled create、PATCH、启停和 DELETE 防护;退群通知专用 singleton upsert 接入同一规则资源;scheduled report 使用真实完整 config;旧 scheduled API 只做 compatibility projection。先做 dry-run 再允许 enable。
|
||||||
|
|
||||||
|
### Phase D — Side Effects / Execution Read Model
|
||||||
|
|
||||||
|
扩展一层统一 `Action` facade 支持 iLink + personal transports,并有 per-purpose policy、recipient allowlist/validation、确认语义、强幂等、Action audit 到 Send Log correlation。再开放 send/check/run;按需增加 `/executions` read projection,不合并底层日志存储。
|
||||||
|
|
||||||
|
### Phase E — API Center
|
||||||
|
|
||||||
|
Overview、Query、Monitors、Automations、WeChat Actions、Reports、Developer 分区;按业务任务做 schema-aware 表单/效果预览,Raw Request Tester 留在 Developer。展示 Token scope/host 范围/transport capability,而不只是固定 URL 测试器。
|
||||||
|
|
||||||
|
## 11. Concrete Endpoint Proposal
|
||||||
|
|
||||||
|
下表是下一阶段建议契约,不表示当前已实现。新写 API 应统一错误:`{"error":{"code":"...","message":"...","details":{...}},"requestId":"..."}`。时间使用带 offset 的 ISO-8601;接口只接受 stable IDs。
|
||||||
|
|
||||||
|
| Method | Path | Request → Response | Risk | Underlying Service |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| GET | `/api/v1/capabilities` | 无 → app version/readiness + `query`,`groups.memberStats`,`groupExitMonitor`,`automations`、每种 `wechat.transport/content` 的 `supported/available/reason` | R0 | 新薄 facade 汇总 LocalQuery、GroupStats、Monitor、AutomationStore、personal capability、AgentHub status |
|
||||||
|
| GET | `/api/v1/monitors/group-exits` | 无 → `{enabled,running,monitoredConversationIds,lastCheckedAt,eventCount}` | R0 | `GroupExitMonitorService.getState` |
|
||||||
|
| PATCH | `/api/v1/monitors/group-exits` | `{enabled?,monitoredConversationIds?}` → 保存后的 state;监控 ID 必须 resolve 到现有群 | R1 | `setEnabled` / `setMonitoredRoomIds` |
|
||||||
|
| GET | `/api/v1/monitors/group-exits/events?conversationId=&since=&until=&limit=&cursor=` | ISO 时间和稳定群 ID → `{events,nextCursor}`,事件显式 `eventId`、group/member、counts、detectedAt | R0 | `GroupExitMonitorService.listEvents`;为无 cursor 的现有 list 加稳定分页 adapter |
|
||||||
|
| POST | `/api/v1/monitors/group-exits/check` | `{confirmSideEffects:true}` + `Idempotency-Key` → checkedAt、新事件数、notification execution refs | R2 | `checkNow`;因检查可发现事件并调用 leave-notification Automation,不应标成纯读 |
|
||||||
|
| GET | `/api/v1/automations?type=&enabled=` | 无 → typed rules page;包括 singleton leave rule | R0 | `AutomationRuleStore.listRules` |
|
||||||
|
| POST | `/api/v1/automations` | typed `AutomationRuleDraft`,create 默认 `enabled:false` → `{rule}` | R1 | `AutomationRuleStore.createRule`(需先强化 strict validation/persist result) |
|
||||||
|
| GET | `/api/v1/automations/{ruleId}` | 无 → `{rule}` | R0 | `AutomationRuleStore.getRule` |
|
||||||
|
| PATCH | `/api/v1/automations/{ruleId}` | typed partial config → `{rule}`;`ruleType` 不可变 | R1 | `AutomationRuleStore.updateRule` |
|
||||||
|
| DELETE | `/api/v1/automations/{ruleId}` | 无 → `{deletedId}`;默认拒绝 builtin/system singleton 删除 | R3 | `AutomationRuleStore.deleteRule` + protected-id policy |
|
||||||
|
| POST | `/api/v1/automations/validate` | typed draft → `{valid,normalizedDraft,issues,effects,resolvedTargets,nextRunAt,capabilities,validationId}`;不保存、不发送 | R0 | 新 validator adapter:contact resolve、template validator、capability services、schedule pure functions |
|
||||||
|
| POST | `/api/v1/automations/{ruleId}/enable` | `{validationId,confirmFutureEffects:true}` → `{rule}`;validation hash 必须匹配当前规则 | R1(武装未来 R2) | `AutomationRuleStore.setRuleEnabled` + validator |
|
||||||
|
| POST | `/api/v1/automations/{ruleId}/disable` | 无 → `{rule}` | R1 | `AutomationRuleStore.setRuleEnabled` |
|
||||||
|
| POST | `/api/v1/automations/{ruleId}/run` | `{confirmSideEffects:true}` + `Idempotency-Key` → `{execution}` | R2 | `AutomationService.executeScheduledRule`;只对具有 `run` 语义的 rule type 开放 |
|
||||||
|
| GET | `/api/v1/automations/executions?ruleId=&status=&trigger=&since=&until=&limit=&cursor=` | 无 → `{executions,nextCursor}` | R0 | `AutomationExecutionLogService.list`;需加 timestamp/filter/cursor adapter |
|
||||||
|
| GET | `/api/v1/groups/{conversationId}/member-stats?start=&end=` | ISO 时间窗口 → active/silent/counts/freshness/complete/limitations;未来要 former members 则先扩 shared result | R0 | `GroupStatsService.getMemberStats`;adapter 把 canonical roomId 转 md5 |
|
||||||
|
| GET | `/api/v1/wechat/capabilities` | 无 → personal/iLink 分 transport 状态与内容能力 | R0 | `PersonalWechatCapabilityService` + `WechatSendGateway.hasIlinkSender`/Agent Hub connector health |
|
||||||
|
| POST | `/api/v1/wechat/send` | `{recipient:{type,id},content:{type:"text",text},confirm:true}` + 必填 `Idempotency-Key` → `{actionId,status,transport,sendLogRef}` | R2 | 扩展后的 `WechatActionGateway`/统一 Action facade;不得裸调 `WechatSendGateway.send` |
|
||||||
|
| GET | `/api/v1/wechat/send-logs?status=&since=&limit=&cursor=` | 无 → 只读、脱敏 Send Log page | R0 | `WechatSendGateway.listSendLog` / `WechatSendLogService.list` |
|
||||||
|
| GET | `/api/v1/wechat/action-logs?executionId=&status=&since=&limit=&cursor=` | 无 → 业务 Action 审计 page;preview 默认截短/可省略 | R0 | `WechatActionLogService.list` / `WechatActionGateway.listAuditRecords` |
|
||||||
|
| GET | `/api/v1/executions?source=&status=&since=&until=&limit=&cursor=` | 无 → 统一只读 projection,每项保留 source/executionId/trigger/action/status/error/times/correlation IDs | R0 | 组合 `AutomationExecutionLogService`、Action audit、Send Log;不合并其存储 |
|
||||||
|
| GET | `/api/v1/reports/history?groupId=&since=&until=&limit=&cursor=` | 无 → 仅元数据分页,不带 `generatedImage`、本地绝对路径或完整 report snapshot | R0 | `listGeneratedReports` 经 summary adapter,最好先让 Service 支持 metadata-only |
|
||||||
|
| GET | `/api/v1/reports/history/{reportId}/image` | 无 → PNG binary | R0 | Report History 的 file resolver;仅按内部 report id 解析,不接收任意路径 |
|
||||||
|
| GET | `/api/v1/report-templates` | 无 → 已安装模板的 stable id/name/version/available 列表 | R0 | `reportTemplateService.list` |
|
||||||
|
| GET | `/api/v1/agent-hub/status` | 无 → hub/connector/dataApi/dbReady 的脱敏状态 | R0 | `AgentHubService.getStatus`;`/api/v1/agent/status` 保留 alias |
|
||||||
|
|
||||||
|
`POST /wechat/send` 建议先只开放 text + 明确目标;image path/url、voice 和 file 类型各自扩大本机文件/外传风险,需独立 validation/permission,不从现有任意 `msg` 输入自动继承。
|
||||||
|
|
||||||
|
## 12. Do Not Expose
|
||||||
|
|
||||||
|
- 任意数据库 key、图片 AES key、微信登录凭据、iLink `context_token`/bot token、inbound inbox raw items;这些是密钥或未处理消息,不是业务 API。
|
||||||
|
- 通用 `settings:set`、任意 `dbRoot`、数据库 disconnect/reopen、cache 全清、Knowledge/OCR 索引 clear/reset;配置或 destructive blast radius 远高于 Agent automation 管理需求。
|
||||||
|
- Agent Hub QR/login/verify-code/reconnect/disconnect/connector lifecycle。它会影响进程状态/账户登录,也会暴露登录材料。
|
||||||
|
- `WechatInboundInbox` pending/clear/complete/recordFailure 等队列控制。它是 at-least-once 消息交付内部机制,外部 ack 会破坏不丢消息语义。
|
||||||
|
- Agent Hub 完整对话正文默认不暴露。若明确产品需要,独立设计用户授权、最小时间窗口、分页、清理和脱敏;现有 Conversation Store 为本机完整收发留档。
|
||||||
|
- 任意本地 file path、path traversal 类导出/报告资产操作;不要把 IPC 的 file chooser、reveal、delete path 形状直接变 HTTP 参数。
|
||||||
|
- 单图 AI insight/任意 Vision analyze、批量 TTS synthesis、AI Provider 配置/测试和 app update download/install;它们具有费用、敏感图片/文本上传或应用安装影响。
|
||||||
|
- 清除退群、Automation、发送/Action、报告历史或批量发送接口作为 P0。清理类不是“管理配置”的必要前提,应使用 R3 独立权限及本地可恢复流程。
|
||||||
|
- 通用“任意 Automation DAG/任意脚本/任意 purpose”的创建。当前实现只有明确的三类规则和固定动作,不是 workflow engine;扩展功能应有显式 ruleType/schema,而不是暴露内存对象。
|
||||||
|
|
||||||
|
## 13. Open Questions
|
||||||
|
|
||||||
|
以下属于产品边界选择,无法只从代码决定:
|
||||||
|
|
||||||
|
1. 新 Agent API 是否允许同时操作个人微信和 Agent Hub/iLink,还是第一版限定一个 transport?现有 capability 与 send endpoint 分属两套连接状态。
|
||||||
|
2. Agent 是否默认只拿只读 scope;配置和 R2 send 是否要求独立 token/用户确认?当前只有单一 Bearer Token。
|
||||||
|
3. 多账号是否属于本阶段?当前 Query/GroupExit 绑定当前活动数据库账号,Agent Hub 有自己的 connector accountId;没有统一 account-scoped API model。
|
||||||
|
4. Agent Hub 完整对话是否要作为 API capability?它含完整消息正文,和从微信数据库按 query 搜历史是不同隐私边界。
|
||||||
|
|
||||||
|
真实微信 runtime 可用性、平台 hydration 和 transport 能力需在后续实现集成测试中验证;静态代码审计无法替代真实数据库/微信连接的运行时验证。
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# 本地启动排障
|
||||||
|
|
||||||
|
本文面向运行源码开发环境的贡献者。常规启动顺序和测试入口请先阅读[开发、测试与构建](./overview.md)。
|
||||||
|
|
||||||
|
## 启动成功的判断标准
|
||||||
|
|
||||||
|
执行 `pnpm dev` 后,以下状态同时满足,说明本地开发环境已经可用:
|
||||||
|
|
||||||
|
- Electron 窗口已打开,或 `http://localhost:5173/` 返回 HTTP `200`;
|
||||||
|
- 控制台显示 Local HTTP API 正在监听 `http://127.0.0.1:6131`。
|
||||||
|
|
||||||
|
`6131` 是应用提供给本机集成使用的 API 端口,不是 Vite 的页面端口。
|
||||||
|
|
||||||
|
## WCDB 消息读取诊断
|
||||||
|
|
||||||
|
`WCDB_DEBUG_LOGS` 默认关闭。需要排查消息读取链路时,可以在启动命令前设置为 `1`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
WCDB_DEBUG_LOGS=1 pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
开启后会输出 `GETMSG-xxx` 请求耗时和 native `WCDB-EXPLAIN` 执行计划,不记录聊天正文。取消该环境变量或设为 `0` 即可关闭。
|
||||||
|
|
||||||
|
## Electron 二进制缺失或下载失败
|
||||||
|
|
||||||
|
`electron-vite dev` 报 `Electron uninstall`,或 Electron 安装器报 `fetch failed`,通常表示 `node_modules/electron/dist` 中的 Electron 二进制缺失或下载未完成。这不是应用业务代码的启动错误。
|
||||||
|
|
||||||
|
**先看根因,别急着删 `node_modules` 重装。** `electron@43` 的 npm 包**不再声明 `postinstall`**(其 `package.json` 里 `scripts` 是空对象),下载改为「首次 `require('electron')` 时的懒加载」。因此:
|
||||||
|
|
||||||
|
- `package.json` 里的 `pnpm.onlyBuiltDependencies: ["electron"]` 对它不起作用——上游没有脚本可执行,pnpm 无从下手;
|
||||||
|
- `pnpm install` 跑完不会有任何二进制被下载,**只重装依赖解决不了这个问题**。
|
||||||
|
|
||||||
|
项目已自动兜住这条路径:`scripts/ensure-electron-binary.cjs` 挂在 `postinstall` 与 `predev` 上,校验 `path.txt` 指向的可执行文件是否真的存在(只有 `path.txt` 而没有 `dist/` 同样算没装好),缺失时就地补下载。它读取 `.npmrc` 的 `electron_mirror`(当前为 `https://npmmirror.com/mirrors/electron/`),失败后再兜底重试一次该镜像。
|
||||||
|
|
||||||
|
正常情况下你不需要做任何事。只有当自动步骤没有执行时(例如安装时带了 `--ignore-scripts`),才需要手动补一次:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node scripts/ensure-electron-binary.cjs
|
||||||
|
```
|
||||||
|
|
||||||
|
要换用别的镜像时,显式设置环境变量(优先于 `.npmrc`)。PowerShell 示例:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:ELECTRON_MIRROR = 'https://your-electron-mirror.example/'
|
||||||
|
node scripts/ensure-electron-binary.cjs
|
||||||
|
```
|
||||||
|
|
||||||
|
该环境变量只影响当前终端,不会改写仓库中的 `.npmrc`。镜像地址必须保留末尾的 `/`,并提供与 Electron 版本对应的目录结构。
|
||||||
|
|
||||||
|
## 页面地址无法通过 IPv4 访问
|
||||||
|
|
||||||
|
Vite 在某些 Windows 环境中只监听 IPv6 本机回环地址 `::1`。这时直接访问 `http://127.0.0.1:5173/` 可能失败,但 `http://localhost:5173/` 仍然正常,Electron 也会使用后者加载页面。
|
||||||
|
|
||||||
|
排查时优先访问 `http://localhost:5173/`;需要显式验证 IPv6 时,使用 `http://[::1]:5173/`。不要因为 IPv4 回环地址不可用就判断 Electron 或 Vite 启动失败。
|
||||||
|
|
||||||
|
## 仍无法启动时
|
||||||
|
|
||||||
|
保留首次错误的完整输出,并同时记录操作系统、Node.js 与 pnpm 版本,以及 `pnpm install --frozen-lockfile` 与 `pnpm dev` 的执行结果。不要提交数据库密钥、AI API Key、微信数据路径或聊天内容。
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# 开发、测试与构建
|
||||||
|
|
||||||
|
本文面向希望参与 TraceMemo 开发、验证文档或维护集成的贡献者。普通用户请从[第一次使用](../user-guide/getting-started.md)开始。
|
||||||
|
|
||||||
|
## 技术基线
|
||||||
|
|
||||||
|
- Electron + React + TypeScript;
|
||||||
|
- pnpm 7+;
|
||||||
|
- 平台对应的 Electron/native 构建环境。
|
||||||
|
|
||||||
|
产品文档的事实来源优先级是:当前源码 → 当前 UI/Renderer → 测试 → package/config → README/docs → 历史资料。功能、API、版本、隐私和兼容性变更时,不要只改 README。
|
||||||
|
|
||||||
|
## 本地开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
本地依赖安装与 Electron 二进制下载异常,请查看[本地启动排障](./local-startup-troubleshooting.md)。
|
||||||
|
|
||||||
|
常用检查:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm typecheck
|
||||||
|
pnpm test:unit
|
||||||
|
pnpm test:component
|
||||||
|
pnpm test:integration
|
||||||
|
pnpm test:e2e:build
|
||||||
|
```
|
||||||
|
|
||||||
|
完整测试入口 `pnpm test` 还会运行 Skill 安装指令、构建和 Playwright 测试;需要对应平台环境。
|
||||||
|
|
||||||
|
## 代码变更对应文档
|
||||||
|
|
||||||
|
| 代码区域 | 需要同步检查的文档 |
|
||||||
|
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------ |
|
||||||
|
| `src/shared/ai-search.ts`、AI Search pipeline | `user-guide/ai-search.md`、`concepts/answer-sources.md` |
|
||||||
|
| `src/shared/knowledge.ts`、`src/main/knowledge/` | `user-guide/knowledge.md`、`concepts/how-it-works.md` |
|
||||||
|
| `src/shared/voice-recognition.ts` | `user-guide/voice.md` |
|
||||||
|
| `src/shared/group-report.ts`、报告 UI | `user-guide/report.md`、API/Agent 文档 |
|
||||||
|
| `src/shared/export.ts`、导出服务/UI | `user-guide/export.md` |
|
||||||
|
| `src/main/services/system-ocr-service.ts`、`image-text-index-service.ts` | `user-guide/knowledge.md`、`concepts/how-it-works.md`、`user-guide/privacy.md` |
|
||||||
|
| `src/main/services/image-insight-service.ts`、AI Provider | `user-guide/report.md`、`user-guide/privacy.md` |
|
||||||
|
| `src/shared/automation.ts`、自动化服务与执行网关 | `user-guide/report.md`、`concepts/how-it-works.md`、`docs/README.md` |
|
||||||
|
| `src/shared/local-api-test.ts`、`src/main/http-server.ts` | `agent/api.md`、`api-security.md`、打包 Skill |
|
||||||
|
| Agent Hub service/UI | `agent/agent-hub.md`、`user-guide/privacy.md` |
|
||||||
|
| 设置导航、连接页面 | `user-guide/getting-started.md`、`docs/README.md` |
|
||||||
|
|
||||||
|
两条容易被写错的边界:
|
||||||
|
|
||||||
|
- **防撤回已下线**(`src/main/services/recall-archive-service.ts` 保留但不再启动):设置入口隐藏,`recallProtectionEnabled` 在所有读写路径上被强制收敛为 `false`。不要把它写回用户指南。
|
||||||
|
- **图片文字索引(本机 OCR)与图片理解(需要 Provider)是两条不同的路径**:前者写入本地索引、能被搜索,且不联网;后者只在日报和设置里的模型检测中使用。改其中一条时不要把另一条的隐私口径带过去。
|
||||||
|
|
||||||
|
## 文档检查
|
||||||
|
|
||||||
|
提交文档变更前至少执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
# 过时版本号、旧品牌名、旧结构叙述、MCP 误解
|
||||||
|
rg -n "v2\.1\.7|2\.4\.0|v2\.2\.0 兼容期|无鉴权|mcpServers" README.md docs --glob '*.md' --glob '!development/overview.md'
|
||||||
|
# 不存在的产品结构(定时日报已并入自动化)
|
||||||
|
rg -n "日报 → 定时日报|Monitor / Automation" README.md docs --glob '*.md'
|
||||||
|
```
|
||||||
|
|
||||||
|
历史迁移说明可以出现旧版本号;正式使用指南不要把过时版本写成当前版本。负向澄清“6131 不是 MCP Server”可以保留,以防用户照抄错误配置。发版前额外确认 `README.md` 里的版本号与 `package.json` 的 `version` 一致。
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# Query Agent POC
|
||||||
|
|
||||||
|
这是独立的开发测试入口,不会修改生产“问问微信”执行链。
|
||||||
|
|
||||||
|
先启动 TraceMemo,并在 API Center 开启 Local HTTP API。然后在仓库根目录运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm poc:query-agent "我和BOBO第一次聊了什么"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 两个入口
|
||||||
|
|
||||||
|
| 命令 | 行为 | 何时用 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `pnpm poc:query-agent "问题"` | 先执行完整构建,再运行 | 首次运行,或刚改过代码 |
|
||||||
|
| `pnpm poc:query-agent:run "问题"` | 直接运行已有构建,**不构建** | 连续迭代测试 |
|
||||||
|
|
||||||
|
`poc:query-agent:run` 在构建产物不存在时会明确提示先运行 `pnpm poc:query-agent`,**不会自动构建**。
|
||||||
|
|
||||||
|
注意:`poc:query-agent` 内部走的是完整 `electron-vite build`(main + preload + renderer),
|
||||||
|
即使 POC 只需要一个 main entry。连续测试请使用 `poc:query-agent:run` 以免每次都重建整个 renderer。
|
||||||
|
|
||||||
|
## 传参
|
||||||
|
|
||||||
|
参数按原样转发给入口,可以被 `--` 分隔(`pnpm run` 惯例):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm poc:query-agent -- "BOBO上个月有没有给我发过文件"
|
||||||
|
pnpm poc:query-agent:run "BOBO上个月有没有给我发过文件"
|
||||||
|
```
|
||||||
|
|
||||||
|
入口只会移除参数列表**开头**的一个独立 `--`;问题正文中的 `--` 会原样保留。
|
||||||
|
|
||||||
|
## Provider
|
||||||
|
|
||||||
|
POC 使用设置页当前默认 AI Provider、模型、Base URL 和安全存储中的 API Key。Local Query API 仍使用现有 Bearer Token;POC 输出不会打印 Token、API Key、数据库路径或内部消息 ID。
|
||||||
|
|
||||||
|
## 输出
|
||||||
|
|
||||||
|
**stdout 是 JSON**(`poc:query-agent` 会在它前面混入构建日志,`poc:query-agent:run` 只多两行 pnpm 横幅)。
|
||||||
|
需要机器解析时用 `--silent` 拿到纯 JSON:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --silent poc:query-agent:run "我和BOBO第一次聊了什么" > result.json
|
||||||
|
```
|
||||||
|
|
||||||
|
JSON 字段:
|
||||||
|
|
||||||
|
- `question`、`provider`、`model`
|
||||||
|
- `modelCallCount`、`toolCallCount`
|
||||||
|
- `modelDurationsMs`(每次模型调用耗时,含失败的那次)
|
||||||
|
- `modelDiagnostics`(每次模型调用的请求级诊断:HTTP status、content-type、是否返回 HTML、是否超时、耗时)
|
||||||
|
- `firstModelMs`、`toolTotalMs`、`finalModelMs`、`totalMs`
|
||||||
|
- 每次工具调用的名称、脱敏参数、耗时、状态和结果数量
|
||||||
|
- 最终 `answer` 或错误信息
|
||||||
|
|
||||||
|
**stderr 是人类可读摘要**(不参与 JSON 解析):
|
||||||
|
|
||||||
|
```text
|
||||||
|
[Timing]
|
||||||
|
Model #1 1315 ms
|
||||||
|
TM Tools(1) 623 ms
|
||||||
|
Model #2 1598 ms
|
||||||
|
------------------------
|
||||||
|
Total 3545 ms
|
||||||
|
Model total 2913 ms (82.2%)
|
||||||
|
TM tool total 623 ms (17.6%)
|
||||||
|
|
||||||
|
[Provider]
|
||||||
|
provider DeepSeek
|
||||||
|
model DeepSeek Chat
|
||||||
|
host api.deepseek.com
|
||||||
|
model calls 2
|
||||||
|
tool calls 1
|
||||||
|
attempt #1 elapsedMs=1298 status=200 contentType=application/json
|
||||||
|
attempt #2 elapsedMs=1571 status=200 contentType=application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
`elapsedMs` 是 TTFB(收到响应头),`modelDurationsMs` 是整次调用(含读 body);502 时两者接近,
|
||||||
|
说明等待发生在上游网关,不是本地读 body 慢。诊断只记录 host,不记录完整 URL 或任何凭据。非 2xx 响应会先记录 status / content-type / elapsedMs,再返回安全错误(例如“模型服务返回了网页而不是 JSON(HTTP 502 Bad Gateway)”),不会把 HTML 正文丢给 JSON 解析器。
|
||||||
|
|
||||||
|
## 约束
|
||||||
|
|
||||||
|
工具调用最多 5 次,只允许 `query_messages`、`search_messages`、`message_context`、`conversation_overview`。未配置 AI Provider、Local Query API 未启动或当前 Provider 协议不支持 tools 时,POC 会直接返回错误,不会回退到另一套模型配置。
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
# 界面开发规范:按钮与主题色
|
||||||
|
|
||||||
|
这份规范回答一件事:**为什么同一个产品里,有的按钮是主题色,有的还是浏览器默认的黑白方角。**
|
||||||
|
|
||||||
|
先看一个真实案例 —— 同一屏里的两组按钮:
|
||||||
|
|
||||||
|
```
|
||||||
|
主界面:「更新图片文字索引」 ← 主题色(正确)
|
||||||
|
弹窗里:「取消」「开始索引」 ← 浏览器默认样式(错误)
|
||||||
|
```
|
||||||
|
|
||||||
|
两者渲染出来完全不同,用户会以为是两个不同的产品。根因不是"设计没定颜色",
|
||||||
|
而是**组件在导出时把样式丢了**。下面写清楚怎么避免。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 永远不要写裸 `<button>`
|
||||||
|
|
||||||
|
任何可点的按钮都必须来自 `components/ui/button`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Button } from '../ui'
|
||||||
|
|
||||||
|
<Button variant="outline" onClick={handleCancel}>取消</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
**唯一的例外**:结构性控件(导航项、Tab、列表行、图标热区)——它们有自己
|
||||||
|
成套的布局样式,用原生 `<button>` 是合理的,但**必须**带 `className`,
|
||||||
|
且样式写在对应的 `.scss` 里,不要在 JSX 里临时拼颜色。
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// 可以:结构性控件,样式来自 .scss
|
||||||
|
<button type="button" role="tab" className={active ? 'active' : ''} onClick={...}>
|
||||||
|
今日日报
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
**判据**:如果这个按钮在别的界面也会以同样形态出现("取消"、"保存"、"删除"),
|
||||||
|
它就该是 `Button`;如果它只在某一个位置有意义(侧栏导航项),才考虑原生。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 三种角色,只有三个默认变体
|
||||||
|
|
||||||
|
`Button` 提供 6 个变体,但**日常只用其中 3 个**:
|
||||||
|
|
||||||
|
| 角色 | `variant` | 长什么样 | 用在哪 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 主要 | `default` | 主题色实底 | 这一步用户唯一该做的事 |
|
||||||
|
| 次要 | `outline` / `ghost` | 描边 / 无底色 | 取消、返回、并列的辅助操作 |
|
||||||
|
| 危险 | `destructive` | 红色实底 | 删除、清空、不可恢复的操作 |
|
||||||
|
|
||||||
|
另外两个(`secondary` / `link`)按需用;`link` 只用于正文里的行内跳转。
|
||||||
|
|
||||||
|
**一条硬约束:同一个界面(或同一个弹窗)里,`default` 最多出现一次。**
|
||||||
|
两个主题色实底按钮并排,等于没有主次。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 弹窗按钮:组件已经带样式了,不要再包一层
|
||||||
|
|
||||||
|
`AlertDialogCancel` 和 `AlertDialogAction` **自带**按钮样式(分别是 `outline`
|
||||||
|
和 `default`),直接写文字即可:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={handleStart}>开始索引</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
```
|
||||||
|
|
||||||
|
**不要**再套一层 `Button`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// 反面写法:外层已经有样式了,再包一层只会产生重复类名
|
||||||
|
<AlertDialogCancel asChild>
|
||||||
|
<Button variant="outline">取消</Button>
|
||||||
|
</AlertDialogCancel>
|
||||||
|
```
|
||||||
|
|
||||||
|
需要危险动作时,用 `className` 覆盖(`cn` 走 tailwind-merge,同族类后者生效):
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<AlertDialogAction className="bg-destructive text-destructive-foreground">
|
||||||
|
删除
|
||||||
|
</AlertDialogAction>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 颜色只能用语义 token,禁止硬编码
|
||||||
|
|
||||||
|
颜色全部走 Tailwind 的语义类,它们背后是 `--tm-*` 变量,换主题时自动跟随:
|
||||||
|
|
||||||
|
```
|
||||||
|
背景 bg-primary / bg-surface / bg-accent / bg-destructive
|
||||||
|
文字 text-foreground / text-primary-foreground / text-muted-foreground
|
||||||
|
描边 border-border / border-border-subtle / border-disabled-border
|
||||||
|
```
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// 对
|
||||||
|
<Button className="bg-primary text-primary-foreground">保存</Button>
|
||||||
|
|
||||||
|
// 错 —— 换主题时这行不会跟着变
|
||||||
|
<Button className="bg-[#247a63] text-white">保存</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
**判据**:JSX 里出现 `#` 开头的颜色、`rgb(...)`、或 Tailwind 的调色板名
|
||||||
|
(`bg-green-600`、`text-slate-500`)—— 都是漏用 semantic token 的信号。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 「默认样式」的三个常见来源
|
||||||
|
|
||||||
|
排查界面里冒出来的黑白方角按钮时,按这个顺序找:
|
||||||
|
|
||||||
|
**① 组件导出时把样式丢了。** 最常见。把 Radix 的 primitive 原样导出:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// 错:渲染出来就是浏览器默认按钮
|
||||||
|
const AlertDialogCancel = AlertDialogPrimitive.Cancel
|
||||||
|
```
|
||||||
|
|
||||||
|
正确做法是 `forwardRef` 包一层,挂上 `buttonVariants`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const AlertDialogCancel = React.forwardRef<...>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Cancel
|
||||||
|
ref={ref}
|
||||||
|
className={cn(buttonVariants({ variant: 'outline' }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
```
|
||||||
|
|
||||||
|
**判据**:`components/ui/` 里凡是导出 Radix primitive 的地方,都要确认它是
|
||||||
|
"样式化的封装"还是"原样透传"。原样透传只对布局容器(`Root` / `Portal` /
|
||||||
|
`Group`)成立,对**可点元素**(`Close` / `Action` / `Cancel` / `Item`)不成立。
|
||||||
|
|
||||||
|
**② `asChild` 里重复包了一层。** 外层已经带样式、子元素又带一次,虽然因为
|
||||||
|
同族类后生效而不会出错,但会产生冗余类名。**能去掉一层就去掉。**
|
||||||
|
|
||||||
|
**③ 原生 `<button>` 忘写 `className`。** 见第 1 节的例外条款 —— 结构性控件也必须
|
||||||
|
有样式来源。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 提交前检查清单
|
||||||
|
|
||||||
|
- [ ] 新增的可点元素来自 `Button`,不是裸 `<button>`
|
||||||
|
- [ ] 同一界面里 `default` 变体不超过一个
|
||||||
|
- [ ] 危险操作走 `destructive`,不是红色硬编码
|
||||||
|
- [ ] 弹窗按钮没有重复包 `Button`
|
||||||
|
- [ ] JSX 里没有 `#` 开头的颜色、没有 Tailwind 调色板名
|
||||||
|
- [ ] `components/ui/` 里新导出的可点 primitive 已经挂上 `buttonVariants`
|
||||||
|
- [ ] 组件测试覆盖到按钮的可见性与点击行为(testid 用 `xxx-yyy` 连字符命名)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 一个反面案例的复盘
|
||||||
|
|
||||||
|
弹窗里的「取消 / 开始索引」显示成浏览器默认样式,原因就是第 5 节第 ① 条:
|
||||||
|
`alert-dialog.tsx` 把 `Cancel` / `Action` 两个 primitive 原样导出了。
|
||||||
|
|
||||||
|
修复是给它们各加一个 `forwardRef` 封装,挂上 `buttonVariants`。**组件本身没坏**,
|
||||||
|
所有调用方一行不用改,样式自动生效 —— 这正是把样式收在 `components/ui/` 里的价值:
|
||||||
|
**修一处,全产品对齐。**
|
||||||
|
|
||||||
|
如果你发现某个地方的按钮"没跟上主题",先别去改那个界面 ——
|
||||||
|
**先看它用的组件是不是漏了样式。**
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# 微信系统消息(sysmsg)解析与格式兼容
|
||||||
|
|
||||||
|
微信的「系统消息」(入群、撤回、成员变动等)以 XML(`<sysmsg>`)存放在消息内容里,
|
||||||
|
但**同一类提示的 XML 结构会随客户端版本变化**。本文说明 TraceMemo 的解析方式,
|
||||||
|
以及在遇到新格式时应当怎么扩展。
|
||||||
|
|
||||||
|
## 两类格式
|
||||||
|
|
||||||
|
### 旧格式:正文直接放在 `<plain>`
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<sysmsg type="delchatroommember">
|
||||||
|
<delchatroommember>
|
||||||
|
<plain><![CDATA["成员昵称"通过扫描你分享的二维码加入群聊]]></plain>
|
||||||
|
<text><![CDATA["成员昵称"通过扫描你分享的二维码加入群聊]]></text>
|
||||||
|
<link>
|
||||||
|
<scene>qrcode</scene>
|
||||||
|
<text><![CDATA[撤销]]></text>
|
||||||
|
</link>
|
||||||
|
</delchatroommember>
|
||||||
|
</sysmsg>
|
||||||
|
```
|
||||||
|
|
||||||
|
解析:命中 `delchatroommember`,直接取 `<plain>`。
|
||||||
|
|
||||||
|
### 新格式:正文在 `<template>`,用 `$名称$` 引用 link
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<sysmsg type="sysmsgtemplate">
|
||||||
|
<sysmsgtemplate>
|
||||||
|
<content_template type="tmpl_type_profilewithrevokeqrcode">
|
||||||
|
<plain><![CDATA[]]></plain>
|
||||||
|
<template><![CDATA["$adder$"通过扫描你分享的二维码加入群聊 $revoke$]]></template>
|
||||||
|
<link_list>
|
||||||
|
<link name="adder" type="link_profile">
|
||||||
|
<memberlist><member>
|
||||||
|
<username><![CDATA[wxid_xxxxxxxx]]></username>
|
||||||
|
<nickname><![CDATA[成员昵称]]></nickname>
|
||||||
|
</member></memberlist>
|
||||||
|
</link>
|
||||||
|
<link name="revoke" type="link_revoke_qrcode" hidden="1">
|
||||||
|
<title><![CDATA[撤销]]></title>
|
||||||
|
</link>
|
||||||
|
</link_list>
|
||||||
|
</content_template>
|
||||||
|
</sysmsgtemplate>
|
||||||
|
</sysmsg>
|
||||||
|
```
|
||||||
|
|
||||||
|
三个要点:
|
||||||
|
|
||||||
|
- `<plain>` 变成**空 CDATA**,正文挪进 `<template>`;
|
||||||
|
- 正文里的 `$名称$` 是占位符,按 `<link_list>` 中 `link[name]` 回填;
|
||||||
|
- `hidden="1"` 的 link 在微信里是**可点击按钮**,纯文本展示时应省略其文案。
|
||||||
|
|
||||||
|
## 解析流程
|
||||||
|
|
||||||
|
`src/main/message-parser.ts` 的 `parseSystemMessage()` 按以下顺序尝试:
|
||||||
|
|
||||||
|
| 顺序 | 分支 | 处理对象 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 1 | `extractRecallMessage` | `<revokemsg>` 撤回通知 |
|
||||||
|
| 2 | `extractSysmsgTemplateText` | `<sysmsgtemplate>` 模板消息 |
|
||||||
|
| 3 | `extractDelChatroomMemberText` | `<delchatroommember>` 成员变动 |
|
||||||
|
| 4 | 通用提取(`plain` → `text` → `title`),再退回 `fallbackSystemText` | 其余未覆盖类型 |
|
||||||
|
|
||||||
|
第 4 步之前会先调用 `stripSysmsgLinkList()` 剥掉 `<link_list>`。
|
||||||
|
|
||||||
|
## 为什么必须显式处理新格式
|
||||||
|
|
||||||
|
通用提取链只在第 1~3 步全部落空时才执行,而新格式恰好让它落空:
|
||||||
|
`<plain>` 是空 CDATA,又没有 `<text>`,于是取到 `<title>` ——
|
||||||
|
那是 `hidden="1"` 按钮的标题。**结果是整条系统消息只剩一个按钮文案**,
|
||||||
|
例如把「某某通过扫描你分享的二维码加入群聊」显示成「撤销」。
|
||||||
|
|
||||||
|
因此三处约束缺一不可:
|
||||||
|
|
||||||
|
1. 模板分支必须排在通用提取之前;
|
||||||
|
2. 占位符回填必须尊重 `hidden="1"`;
|
||||||
|
3. 通用提取前先剥 `<link_list>`,作为未知类型的防护。
|
||||||
|
|
||||||
|
## 新增一类系统消息时
|
||||||
|
|
||||||
|
1. 从真实消息中取出 `content`(`<sysmsg>` 原文),确认 `type` 与承载正文的标签;
|
||||||
|
2. 在 `parseSystemMessage()` 里加一个**早于通用提取**的分支;
|
||||||
|
3. 补 `tests/unit/message-parser.test.ts` 用例,**新旧两版各一条**,防止回归;
|
||||||
|
4. 文档与代码注释只写结构,不粘贴真实会话内容、昵称、wxid 或二维码链接。
|
||||||
|
|
||||||
|
## 相关位置
|
||||||
|
|
||||||
|
- 解析实现:`src/main/message-parser.ts`
|
||||||
|
- 单元测试:`tests/unit/message-parser.test.ts`
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# macOS 关闭 SIP 教程
|
||||||
|
|
||||||
|
SIP(System Integrity Protection,系统完整性保护)是 macOS 的系统安全机制。关闭 SIP 会降低系统安全性,只建议在确实需要读取或调试本地微信数据时临时关闭;操作完成后,建议重新开启。
|
||||||
|
|
||||||
|
> 只在连接页面明确提示需要关闭 SIP 时才处理。首次连接失败时,先确认微信版本、账号目录和登录时机,再按本文操作。关闭 SIP 不是 TraceMemo 的常规安装步骤,也不应长期保持关闭。
|
||||||
|
|
||||||
|
## 准备
|
||||||
|
|
||||||
|
- 一台 Mac 电脑,Intel 芯片和 Apple Silicon 芯片均可。
|
||||||
|
- 需要进入 macOS 恢复模式。
|
||||||
|
- 请先保存正在编辑的文件,并预留一次重启时间。
|
||||||
|
|
||||||
|
## 关闭 SIP
|
||||||
|
|
||||||
|
### Intel Mac
|
||||||
|
|
||||||
|
1. 关机。
|
||||||
|
2. 按下开机键后,立刻按住 `Command + R`。
|
||||||
|
3. 保持按住,直到进入 macOS 恢复模式。
|
||||||
|
|
||||||
|
### Apple Silicon Mac(M1/M2/M3/M4/M5)
|
||||||
|
|
||||||
|
1. 关机。
|
||||||
|
2. 长按开机键不放。
|
||||||
|
3. 直到出现启动选项界面后松开。
|
||||||
|
4. 选择"选项",进入 macOS 恢复模式。
|
||||||
|
|
||||||
|
### 在恢复模式中执行命令
|
||||||
|
|
||||||
|
1. 进入恢复模式后,点击顶部菜单栏的 **Utilities(实用工具)**。
|
||||||
|
2. 选择 **Terminal(终端)**。
|
||||||
|
3. 在终端中输入:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
csrutil disable
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 按回车执行。
|
||||||
|
5. 看到关闭成功提示后,重启电脑。
|
||||||
|
|
||||||
|
## 确认是否生效
|
||||||
|
|
||||||
|
重启回到正常桌面后,打开"终端",执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
csrutil status
|
||||||
|
```
|
||||||
|
|
||||||
|
看到 `System Integrity Protection status: disabled.` 才算关闭成功。
|
||||||
|
|
||||||
|
若仍显示 `enabled`,说明没有生效。常见原因是没在恢复模式里执行,或系统刚做过大版本更新——
|
||||||
|
macOS 大版本更新会把 SIP 重置回开启状态,此前关过也会失效,需要重新按上面的步骤操作。
|
||||||
|
|
||||||
|
## 重新开启 SIP
|
||||||
|
|
||||||
|
拿到数据库密钥后,建议重新进入恢复模式,在终端中执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
csrutil enable
|
||||||
|
```
|
||||||
|
|
||||||
|
然后重启电脑,恢复系统安全设置。
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# macOS 数据访问与系统权限
|
||||||
|
|
||||||
|
## 你什么时候会看到这些提示
|
||||||
|
|
||||||
|
TraceMemo 需要读取微信本地数据。macOS 会根据系统版本、微信状态和安全设置,要求应用完成授权;自动获取数据库密钥时,页面可能提示暂时调整系统安全设置。
|
||||||
|
|
||||||
|
## 推荐步骤
|
||||||
|
|
||||||
|
1. 先启动 TraceMemo,阅读连接页面显示的当前前置条件。
|
||||||
|
2. 确认微信数据目录指向当前账号。
|
||||||
|
3. 只在页面明确要求时处理系统授权或 SIP;关闭 SIP 的具体步骤见[关闭 SIP 教程](../mac-disable-sip.md),按页面提示完成密钥获取后,恢复你平时使用的安全设置。
|
||||||
|
4. 返回应用重新检测账号、数据库和图片资源状态。
|
||||||
|
|
||||||
|
不要直接复制网上针对其他微信版本的命令。系统授权失败时,记录 macOS 版本、微信版本和页面错误,再按[排障文档](../user-guide/troubleshooting.md#连接微信失败)处理。
|
||||||
|
|
||||||
|
## SIP 风险
|
||||||
|
|
||||||
|
关闭 System Integrity Protection 会降低 macOS 对系统文件和进程的保护。它不是日常使用 TraceMemo 的功能开关,也不应长期保持关闭。只有在你理解风险、确认页面要求且完成必要操作时才处理;完成后按 Apple 官方方式重新启用。
|
||||||
|
|
||||||
|
## 应用无法打开
|
||||||
|
|
||||||
|
如果 macOS 阻止未验证的应用,使用系统“隐私与安全性”中的“仍要打开”选项。不要为了绕过提示下载来历不明的补丁或替换应用文件。
|
||||||
|
|
||||||
|
## Intel 与 Apple Silicon
|
||||||
|
|
||||||
|
TraceMemo 同时支持两种 Mac 架构:
|
||||||
|
|
||||||
|
- **Apple Silicon(M 系列、`arm64`)**
|
||||||
|
- **Intel Mac(`x64`)**
|
||||||
|
|
||||||
|
从 Releases 下载与你 Mac 处理器匹配的构建:
|
||||||
|
|
||||||
|
- Apple Silicon:`tracememo-<版本号>-arm64.dmg`
|
||||||
|
- Intel:`tracememo-<版本号>-x64.dmg`
|
||||||
|
|
||||||
|
两种架构都可以通过应用内的连接流程自动获取微信数据库密钥。首次连接时,TraceMemo 会根据当前机器架构进入对应的流程,按连接页面提示操作即可。
|
||||||
|
|
||||||
|
Apple Silicon 和 Intel 已适配微信 macOS `4.1.13`。不同架构、微信版本和系统授权状态可能导致连接结果不同;文档不对所有组合做兼容性保证。
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
---
|
||||||
|
name: setup-wechat-share-card
|
||||||
|
description: 自动配置和部署 TraceMemo 实验性微信分享卡片服务。用户要求启用、部署、修复或迁移微信分享卡片,配置 Cloudflare Worker/R2/Wrangler,设置 UPLOAD_TOKEN、微信测试号 AppID/AppSecret、JS 接口安全域名,或希望由 Codex、Claude Code 等 Agent 代替手工阅读部署文档时使用。
|
||||||
|
---
|
||||||
|
|
||||||
|
# 部署微信分享卡片
|
||||||
|
|
||||||
|
尽量自行发现项目状态并完成部署。只在自动检查后仍缺少必要信息时,一次性询问用户;不要逐项反复确认。
|
||||||
|
|
||||||
|
## 安全边界
|
||||||
|
|
||||||
|
- 不把真实 AppSecret、`UPLOAD_TOKEN`、Cloudflare Token 或微信验证内容写入 Git。
|
||||||
|
- `.env.example` 只保存占位符;真实值写入项目根目录 `.env`,该文件必须被 Git 忽略。
|
||||||
|
- 不在最终回答中回显 Secret。日志中只报告“已配置/缺失”。
|
||||||
|
- `UPLOAD_TOKEN` 默认自动生成,不要求用户提供。
|
||||||
|
- AppID/AppSecret 必须来自用户自己的微信测试号或公众号。无法自动获取时才询问。
|
||||||
|
- 部署、创建 R2 和写 Secret 属于用户明确请求本 Skill 后的正常动作;不要提交、推送或创建 PR,除非用户另外明确要求。
|
||||||
|
|
||||||
|
## 自动工作流
|
||||||
|
|
||||||
|
1. 定位 TraceMemo 仓库根目录。确认存在 `services/share-card-worker/wrangler.jsonc`。
|
||||||
|
2. 运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash docs/skill/setup-wechat-share-card/scripts/setup.sh doctor
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 检查根目录 `.env`。脚本会自动复用已有配置并生成缺失的 `WECHAT_SHARE_UPLOAD_TOKEN`。
|
||||||
|
4. 如果以下值缺失,只向用户发起一次集中询问:
|
||||||
|
- 分享域名,例如 `share.example.com`;
|
||||||
|
- 微信测试号 AppID;
|
||||||
|
- 微信测试号 AppSecret。
|
||||||
|
5. 用户不知道从哪里获取时,告诉他打开:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index
|
||||||
|
```
|
||||||
|
|
||||||
|
使用微信扫码登录后,复制页面上的 `appID` 和 `appsecret`。提醒用户把分享域名填入“JS 接口安全域名”,不带 `https://` 和路径。
|
||||||
|
6. 将缺失值交给交互脚本,不要把 Secret 放进命令行参数:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash docs/skill/setup-wechat-share-card/scripts/setup.sh configure
|
||||||
|
```
|
||||||
|
|
||||||
|
该命令通过终端交互收集缺项,AppSecret 使用隐藏输入。
|
||||||
|
7. 执行完整部署:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash docs/skill/setup-wechat-share-card/scripts/setup.sh deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本会依次:
|
||||||
|
- 检查或临时下载 Wrangler;
|
||||||
|
- 启动 Cloudflare OAuth 登录;
|
||||||
|
- 执行 `whoami`;
|
||||||
|
- 生成本地 `wrangler.local.jsonc`;
|
||||||
|
- 创建或复用 R2 Bucket;
|
||||||
|
- 写入三个 Worker Secret;
|
||||||
|
- 部署 Worker;
|
||||||
|
- 检查 `/health` 和微信签名接口。
|
||||||
|
8. OAuth 页面出现时,让用户只完成浏览器登录/授权;不要改用 API Token,除非用户主动要求。
|
||||||
|
9. 部署后把服务地址告诉用户,并提醒他在 TraceMemo 卡片弹窗粘贴 `.env` 中的 `WECHAT_SHARE_UPLOAD_TOKEN`。优先把 Token 复制到剪贴板,不在聊天中展示:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash docs/skill/setup-wechat-share-card/scripts/setup.sh copy-token
|
||||||
|
```
|
||||||
|
|
||||||
|
10. 如果微信要求 TXT 验证文件,读取 [references/wechat-domain-verification.md](references/wechat-domain-verification.md),取得用户提供的文件后再修改 Worker。
|
||||||
|
|
||||||
|
## 决策规则
|
||||||
|
|
||||||
|
- Wrangler 未安装:优先使用项目依赖;否则通过 `pnpm dlx wrangler@latest` 临时下载,不强制全局安装。
|
||||||
|
- `whoami` 已登录正确账号:不要重复登录。
|
||||||
|
- R2 已存在:继续,不把“已存在”视为失败。
|
||||||
|
- 自定义域名有 A/AAAA/CNAME 冲突:报告准确域名并要求用户选择删除冲突记录或换子域名;不要擅自删除 DNS。
|
||||||
|
- HTTP 401:重新同步 `.env` 中的 `WECHAT_SHARE_UPLOAD_TOKEN` 到 Worker,再让用户更新 TraceMemo。
|
||||||
|
- “微信 JS-SDK 尚未配置”:重新写入 AppID/AppSecret 并部署。
|
||||||
|
- 微信返回 AppID/AppSecret 错误:让用户检查是否来自同一个测试号、AppSecret 是否已重置。
|
||||||
|
- 缺少 JS 接口安全域名或测试号关注:这是微信后台操作,明确告诉用户要填写什么,不要假装已完成。
|
||||||
|
|
||||||
|
## 验证结果
|
||||||
|
|
||||||
|
完成前必须确认:
|
||||||
|
|
||||||
|
- `wrangler whoami` 成功;
|
||||||
|
- Worker 部署成功;
|
||||||
|
- `/health` 返回 `storage: ready`;
|
||||||
|
- `/api/wx-signature` 返回 `appId`、`timestamp`、`nonceStr`、`signature`;
|
||||||
|
- Git 扫描未发现 `.env`、真实 AppSecret、上传密钥或用户域名被暂存。
|
||||||
|
|
||||||
|
详细产品和架构说明见:`docs/deployment/experimental-wechat-share-card.md`。
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "部署微信分享卡片"
|
||||||
|
short_description: "让 Agent 自动完成微信分享卡片服务的配置与部署"
|
||||||
|
default_prompt: "Use $setup-wechat-share-card to configure and deploy my self-hosted WeChat share-card service with minimal questions."
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# 微信域名验证文件
|
||||||
|
|
||||||
|
当微信测试号页面要求下载 TXT 文件时:
|
||||||
|
|
||||||
|
1. 向用户索取 TXT 文件本身,或文件名与完整内容。
|
||||||
|
2. 不把真实验证内容写进公开仓库历史。
|
||||||
|
3. 优先在本地私有配置中注入;若当前 Worker 只能通过源码 Map 返回,则先提醒用户该值会进入工作区,确认仓库发布前必须移除或改造成 Secret/变量。
|
||||||
|
4. 验证目标必须是:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://<分享域名>/<微信提供的文件名>.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
5. 返回内容必须是纯文本且与微信提供内容完全一致,不加空格、HTML 或额外换行。
|
||||||
|
6. 完成验证后再配置 JS 接口安全域名。安全域名只填主机名,不带协议或路径。
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
||||||
|
ENV_FILE="$REPO_ROOT/.env"
|
||||||
|
EXAMPLE_FILE="$REPO_ROOT/.env.example"
|
||||||
|
WORKER_DIR="$REPO_ROOT/services/share-card-worker"
|
||||||
|
BASE_CONFIG="$WORKER_DIR/wrangler.jsonc"
|
||||||
|
LOCAL_CONFIG="$WORKER_DIR/wrangler.local.jsonc"
|
||||||
|
BUCKET_NAME="wechatexplorer-share-reports"
|
||||||
|
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf 'ERROR: %s\n' "$*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
info() {
|
||||||
|
printf '[share-card] %s\n' "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
require_project() {
|
||||||
|
[[ -f "$BASE_CONFIG" ]] || fail "请在 TraceMemo 仓库根目录运行此脚本"
|
||||||
|
[[ -f "$EXAMPLE_FILE" ]] || fail "缺少 .env.example"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_env_file() {
|
||||||
|
if [[ ! -f "$ENV_FILE" ]]; then
|
||||||
|
cp "$EXAMPLE_FILE" "$ENV_FILE"
|
||||||
|
chmod 600 "$ENV_FILE"
|
||||||
|
info "已从 .env.example 创建本机 .env"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
read_env_value() {
|
||||||
|
local key="$1"
|
||||||
|
local line
|
||||||
|
line="$(grep -E "^${key}=" "$ENV_FILE" | tail -n 1 || true)"
|
||||||
|
printf '%s' "${line#*=}"
|
||||||
|
}
|
||||||
|
|
||||||
|
write_env_value() {
|
||||||
|
local key="$1"
|
||||||
|
local value="$2"
|
||||||
|
local escaped
|
||||||
|
escaped="$(printf '%s' "$value" | sed 's/[\\&|]/\\&/g')"
|
||||||
|
if grep -q -E "^${key}=" "$ENV_FILE"; then
|
||||||
|
sed -i.bak -E "s|^${key}=.*$|${key}=${escaped}|" "$ENV_FILE"
|
||||||
|
command rm "$ENV_FILE.bak"
|
||||||
|
else
|
||||||
|
printf '\n%s=%s\n' "$key" "$value" >> "$ENV_FILE"
|
||||||
|
fi
|
||||||
|
chmod 600 "$ENV_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_domain() {
|
||||||
|
local value="$1"
|
||||||
|
value="${value#http://}"
|
||||||
|
value="${value#https://}"
|
||||||
|
value="${value%%/*}"
|
||||||
|
printf '%s' "$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_upload_token() {
|
||||||
|
local token
|
||||||
|
token="$(read_env_value WECHAT_SHARE_UPLOAD_TOKEN)"
|
||||||
|
if [[ ${#token} -lt 32 ]]; then
|
||||||
|
token="$(openssl rand -hex 32)"
|
||||||
|
write_env_value WECHAT_SHARE_UPLOAD_TOKEN "$token"
|
||||||
|
info "已生成新的 UPLOAD_TOKEN 并安全写入 .env"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
wrangler() {
|
||||||
|
if [[ -x "$REPO_ROOT/node_modules/.bin/wrangler" ]]; then
|
||||||
|
"$REPO_ROOT/node_modules/.bin/wrangler" "$@"
|
||||||
|
elif command -v pnpm >/dev/null 2>&1; then
|
||||||
|
pnpm dlx wrangler@latest "$@"
|
||||||
|
elif command -v npx >/dev/null 2>&1; then
|
||||||
|
npx --yes wrangler@latest "$@"
|
||||||
|
else
|
||||||
|
fail "需要 Node.js 以及 pnpm 或 npm 才能运行 Wrangler"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_wrangler_login() {
|
||||||
|
if wrangler whoami >/dev/null 2>&1; then
|
||||||
|
wrangler whoami
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
info "即将打开 Cloudflare OAuth 登录,请在浏览器中完成授权"
|
||||||
|
wrangler login
|
||||||
|
wrangler whoami
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_required_config() {
|
||||||
|
local domain app_id app_secret token
|
||||||
|
domain="$(normalize_domain "$(read_env_value WECHAT_SHARE_DOMAIN)")"
|
||||||
|
app_id="$(read_env_value WECHAT_SHARE_APP_ID)"
|
||||||
|
app_secret="$(read_env_value WECHAT_SHARE_APP_SECRET)"
|
||||||
|
token="$(read_env_value WECHAT_SHARE_UPLOAD_TOKEN)"
|
||||||
|
[[ -n "$domain" && "$domain" != "share.example.com" ]] || fail "缺少真实 WECHAT_SHARE_DOMAIN"
|
||||||
|
[[ -n "$app_id" ]] || fail "缺少 WECHAT_SHARE_APP_ID"
|
||||||
|
[[ -n "$app_secret" ]] || fail "缺少 WECHAT_SHARE_APP_SECRET"
|
||||||
|
[[ ${#token} -ge 32 ]] || fail "WECHAT_SHARE_UPLOAD_TOKEN 长度不足"
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_interactively() {
|
||||||
|
ensure_env_file
|
||||||
|
local domain app_id app_secret
|
||||||
|
domain="$(read_env_value WECHAT_SHARE_DOMAIN)"
|
||||||
|
if [[ -z "$domain" || "$domain" == "share.example.com" ]]; then
|
||||||
|
read -r -p '分享域名(例如 share.example.com,不带 https://):' domain
|
||||||
|
domain="$(normalize_domain "$domain")"
|
||||||
|
[[ -n "$domain" ]] || fail "分享域名不能为空"
|
||||||
|
write_env_value WECHAT_SHARE_DOMAIN "$domain"
|
||||||
|
fi
|
||||||
|
|
||||||
|
app_id="$(read_env_value WECHAT_SHARE_APP_ID)"
|
||||||
|
if [[ -z "$app_id" ]]; then
|
||||||
|
read -r -p '微信测试号 AppID:' app_id
|
||||||
|
[[ -n "$app_id" ]] || fail "AppID 不能为空"
|
||||||
|
write_env_value WECHAT_SHARE_APP_ID "$app_id"
|
||||||
|
fi
|
||||||
|
|
||||||
|
app_secret="$(read_env_value WECHAT_SHARE_APP_SECRET)"
|
||||||
|
if [[ -z "$app_secret" ]]; then
|
||||||
|
read -r -s -p '微信测试号 AppSecret(输入不会显示):' app_secret
|
||||||
|
printf '\n'
|
||||||
|
[[ -n "$app_secret" ]] || fail "AppSecret 不能为空"
|
||||||
|
write_env_value WECHAT_SHARE_APP_SECRET "$app_secret"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ensure_upload_token
|
||||||
|
info "本机配置已准备完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_local_config() {
|
||||||
|
local domain
|
||||||
|
domain="$(normalize_domain "$(read_env_value WECHAT_SHARE_DOMAIN)")"
|
||||||
|
cat > "$LOCAL_CONFIG" <<EOF
|
||||||
|
{
|
||||||
|
"\$schema": "node_modules/wrangler/config-schema.json",
|
||||||
|
"name": "wechatexplorer-share-card",
|
||||||
|
"main": "src/index.js",
|
||||||
|
"compatibility_date": "2026-07-23",
|
||||||
|
"routes": [{ "pattern": "$domain", "custom_domain": true }],
|
||||||
|
"r2_buckets": [{ "binding": "REPORTS", "bucket_name": "$BUCKET_NAME" }],
|
||||||
|
"triggers": { "crons": ["17 3 * * *"] },
|
||||||
|
"vars": {
|
||||||
|
"PUBLIC_ORIGIN": "https://$domain",
|
||||||
|
"DEFAULT_EXPIRY_DAYS": "7"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
info "已生成本机 Worker 配置 services/share-card-worker/wrangler.local.jsonc"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_secrets_not_tracked() {
|
||||||
|
git check-ignore -q .env || fail ".env 未被 Git 忽略,请停止部署并检查 .gitignore"
|
||||||
|
if git ls-files --error-unmatch .env >/dev/null 2>&1; then
|
||||||
|
fail ".env 已被 Git 跟踪,请先从索引移除"
|
||||||
|
fi
|
||||||
|
if git diff --cached --name-only | grep -Eq '(^|/)\.env$|wrangler\.local\.jsonc$'; then
|
||||||
|
fail "敏感本机配置已被暂存,请先取消暂存"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
create_bucket_if_needed() {
|
||||||
|
local output
|
||||||
|
set +e
|
||||||
|
output="$(wrangler r2 bucket create "$BUCKET_NAME" --config "$LOCAL_CONFIG" 2>&1)"
|
||||||
|
local status=$?
|
||||||
|
set -e
|
||||||
|
if [[ $status -eq 0 ]]; then
|
||||||
|
printf '%s\n' "$output"
|
||||||
|
elif printf '%s' "$output" | grep -Eqi 'already exists|already owned|10004'; then
|
||||||
|
info "R2 Bucket 已存在,继续部署"
|
||||||
|
else
|
||||||
|
printf '%s\n' "$output" >&2
|
||||||
|
fail "创建 R2 Bucket 失败"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
put_secrets() {
|
||||||
|
local upload_token app_id app_secret
|
||||||
|
upload_token="$(read_env_value WECHAT_SHARE_UPLOAD_TOKEN)"
|
||||||
|
app_id="$(read_env_value WECHAT_SHARE_APP_ID)"
|
||||||
|
app_secret="$(read_env_value WECHAT_SHARE_APP_SECRET)"
|
||||||
|
printf '%s' "$upload_token" | wrangler secret put UPLOAD_TOKEN --config "$LOCAL_CONFIG"
|
||||||
|
printf '%s' "$app_id" | wrangler secret put WECHAT_APP_ID --config "$LOCAL_CONFIG"
|
||||||
|
printf '%s' "$app_secret" | wrangler secret put WECHAT_APP_SECRET --config "$LOCAL_CONFIG"
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_service() {
|
||||||
|
local domain health signature
|
||||||
|
domain="$(normalize_domain "$(read_env_value WECHAT_SHARE_DOMAIN)")"
|
||||||
|
health="$(curl -fsS --retry 5 --retry-delay 2 "https://$domain/health")"
|
||||||
|
printf '%s' "$health" | grep -q '"storage":"ready"' || fail "健康检查未返回 storage: ready"
|
||||||
|
signature="$(curl -fsS --retry 3 --retry-delay 2 "https://$domain/api/wx-signature?url=https%3A%2F%2F${domain}%2Fhealth")"
|
||||||
|
printf '%s' "$signature" | grep -q '"signature"' || fail "微信 JS-SDK 签名检查失败:$signature"
|
||||||
|
info "服务验证成功:https://$domain"
|
||||||
|
}
|
||||||
|
|
||||||
|
doctor() {
|
||||||
|
require_project
|
||||||
|
ensure_env_file
|
||||||
|
ensure_upload_token
|
||||||
|
assert_secrets_not_tracked
|
||||||
|
info "Node: $(node --version 2>/dev/null || printf '未安装')"
|
||||||
|
info "pnpm: $(pnpm --version 2>/dev/null || printf '未安装')"
|
||||||
|
if wrangler --version >/dev/null 2>&1; then
|
||||||
|
info "Wrangler 可用:$(wrangler --version | tail -n 1)"
|
||||||
|
else
|
||||||
|
fail "Wrangler 无法运行"
|
||||||
|
fi
|
||||||
|
local domain app_id app_secret
|
||||||
|
domain="$(read_env_value WECHAT_SHARE_DOMAIN)"
|
||||||
|
app_id="$(read_env_value WECHAT_SHARE_APP_ID)"
|
||||||
|
app_secret="$(read_env_value WECHAT_SHARE_APP_SECRET)"
|
||||||
|
[[ -n "$domain" && "$domain" != "share.example.com" ]] && info "分享域名:已配置" || info "分享域名:缺失"
|
||||||
|
[[ -n "$app_id" ]] && info "微信 AppID:已配置" || info "微信 AppID:缺失"
|
||||||
|
[[ -n "$app_secret" ]] && info "微信 AppSecret:已配置" || info "微信 AppSecret:缺失"
|
||||||
|
info "UPLOAD_TOKEN:已配置"
|
||||||
|
}
|
||||||
|
|
||||||
|
deploy() {
|
||||||
|
require_project
|
||||||
|
ensure_env_file
|
||||||
|
ensure_upload_token
|
||||||
|
validate_required_config
|
||||||
|
assert_secrets_not_tracked
|
||||||
|
ensure_wrangler_login
|
||||||
|
generate_local_config
|
||||||
|
create_bucket_if_needed
|
||||||
|
put_secrets
|
||||||
|
wrangler deploy --config "$LOCAL_CONFIG"
|
||||||
|
verify_service
|
||||||
|
}
|
||||||
|
|
||||||
|
copy_token() {
|
||||||
|
ensure_env_file
|
||||||
|
ensure_upload_token
|
||||||
|
local token
|
||||||
|
token="$(read_env_value WECHAT_SHARE_UPLOAD_TOKEN)"
|
||||||
|
if command -v pbcopy >/dev/null 2>&1; then
|
||||||
|
printf '%s' "$token" | pbcopy
|
||||||
|
elif command -v wl-copy >/dev/null 2>&1; then
|
||||||
|
printf '%s' "$token" | wl-copy
|
||||||
|
elif command -v xclip >/dev/null 2>&1; then
|
||||||
|
printf '%s' "$token" | xclip -selection clipboard
|
||||||
|
else
|
||||||
|
fail "未找到剪贴板工具;请让用户自行从 .env 读取 WECHAT_SHARE_UPLOAD_TOKEN"
|
||||||
|
fi
|
||||||
|
info "UPLOAD_TOKEN 已复制到剪贴板"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-doctor}" in
|
||||||
|
doctor) doctor ;;
|
||||||
|
configure) configure_interactively ;;
|
||||||
|
deploy) deploy ;;
|
||||||
|
copy-token) copy_token ;;
|
||||||
|
*) fail "用法:$0 {doctor|configure|deploy|copy-token}" ;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
---
|
||||||
|
name: tracememo-reader
|
||||||
|
description: 通过 TraceMemo 本地 HTTP API 按需读取用户有权访问的微信聊天数据和图片媒体,并管理定时日报任务。当用户要求查看微信消息、查找联系人或群聊、总结聊天、查看或理解图片、生成群聊总结、查询或修改定时日报时使用。此 Skill 由本机 TraceMemo 提供数据,不是 MCP Server。
|
||||||
|
---
|
||||||
|
|
||||||
|
# TraceMemo Reader
|
||||||
|
|
||||||
|
你是一个通过本机 TraceMemo 读取微信历史的 Agent。先确认用户已经在 TraceMemo 中完成数据库连接,再按需调用 API;不要假设数据库已就绪,也不要声称读取了没有调用过的消息。
|
||||||
|
|
||||||
|
## 连接信息
|
||||||
|
|
||||||
|
- Base URL 默认是 `http://127.0.0.1:6131/api/v1`。
|
||||||
|
- `GET /health` 不需要 Token。
|
||||||
|
- 其他端点必须带 `Authorization: Bearer $TRACEMEMO_API_TOKEN`。
|
||||||
|
- 新配置优先读取 `TRACEMEMO_API_TOKEN`;为兼容已安装的旧 Reader,可在新变量缺失时回退到 `WECHATEXPLORER_API_TOKEN`。
|
||||||
|
- Token 由用户在 TraceMemo → API Center 显示/复制,并放在 Agent 自己的本地环境中。
|
||||||
|
- 不要把 Token 放到 URL、回答、日志、Skill 文件或仓库。
|
||||||
|
- 6131 是普通 Local HTTP API,不是 MCP Server;不要生成 `mcpServers` 配置。
|
||||||
|
|
||||||
|
## 每次任务前
|
||||||
|
|
||||||
|
1. 调用 `/health`,确认服务和数据库状态。
|
||||||
|
2. 用户说“今天”“昨天”“本周”等相对时间时,先调用 `/current_time`,按返回的本机时区换算日期。
|
||||||
|
3. 用 `/resolve`、`/contact` 或 `/chatroom` 确认会话标识。
|
||||||
|
4. 用 `/chatlog` 读取最小必要的时间范围。
|
||||||
|
5. 对重要结论读取关键消息前后文;不要只凭一次宽范围粗查回答。
|
||||||
|
|
||||||
|
## 端点速查
|
||||||
|
|
||||||
|
| 方法 | 路径 | 用途 |
|
||||||
|
| ------ | ------------------------------------------------------- | ------------------------------------------------- |
|
||||||
|
| GET | `/health` | 健康和数据库状态 |
|
||||||
|
| GET | `/current_time` | 本机时间与时区 |
|
||||||
|
| GET | `/contact` | 联系人/群聊列表;可传 `filter`、`type` |
|
||||||
|
| GET | `/chatroom` | 群聊列表;可传 `keyword` |
|
||||||
|
| GET | `/recent_chat` | 最近会话;可传 `limit` |
|
||||||
|
| GET | `/chatlog` | 会话消息;必填 `talker`,可传 `time` 或时间戳范围 |
|
||||||
|
| GET | `/media/{mediaId}` | 按消息返回的 `media.url` 获取图片二进制资源 |
|
||||||
|
| GET | `/group_snapshot` | 群成员快照;必填 `md5` |
|
||||||
|
| GET | `/resolve` | 昵称、wxid、md5 解析;必填 `q` |
|
||||||
|
| POST | `/query/messages` | 按目标与时间范围取消息(结构化,不调用 AI) |
|
||||||
|
| POST | `/query/search` | 受限语义关键词检索(依赖本地索引,见 freshness) |
|
||||||
|
| POST | `/query/message-context` | 用 `messageRef` 取某条消息的前后文 |
|
||||||
|
| POST | `/query/conversation-overview` | 按会话与时间范围提取可总结的证据 |
|
||||||
|
| GET | `/query/capabilities` | Query 端点能力目录 |
|
||||||
|
| GET | `/wechat-personal/send-capability` | 个人微信发送能力状态(文字 / 图片 / 语音) |
|
||||||
|
| GET | `/scheduled-reports` | 查询全部定时日报任务 |
|
||||||
|
| GET | `/scheduled-reports/:id` | 查询单个定时日报任务 |
|
||||||
|
| POST | `/scheduled-reports` | 创建定时日报任务 |
|
||||||
|
| PATCH | `/scheduled-reports/:id` | 修改定时日报任务 |
|
||||||
|
| DELETE | `/scheduled-reports/:id` | 删除定时日报任务(执行前必须获得用户确认) |
|
||||||
|
| POST | `/scheduled-reports/:id/enable` | 启用定时日报任务 |
|
||||||
|
| POST | `/scheduled-reports/:id/disable` | 暂停定时日报任务 |
|
||||||
|
| POST | `/scheduled-reports/:id/run` | 立即执行一次并返回 execution |
|
||||||
|
| GET | `/scheduled-reports/:id/executions` | 查询执行记录 |
|
||||||
|
| POST | `/scheduled-reports/executions/:executionId/retry-send` | 复用已有 PNG 重试发送 |
|
||||||
|
| POST | `/report` | 将已有日报结构渲染为 HTML/PNG |
|
||||||
|
| GET | `/agent/status` | Agent Hub、连接器和数据库状态 |
|
||||||
|
| POST | `/agent/group-report` | 按群和 `today`/`yesterday`/`7days` 生成总结图片 |
|
||||||
|
| POST | `/agent/send` | 已连接机器人发送测试(文字或本地图片) |
|
||||||
|
|
||||||
|
这个 API **不只是只读的**:`/report` 会渲染并写文件,`/agent/send` 会真的发出微信消息,`/scheduled-reports*` 会创建、修改、删除或立刻执行定时任务。这些调用都要先确认用户意图;`DELETE` 与 `/agent/send` 尤其需要用户明确确认。
|
||||||
|
|
||||||
|
## 定时日报管理
|
||||||
|
|
||||||
|
定时日报由 TraceMemo 自己持久化和调度。Agent 只负责理解自然语言、解析群聊和时间,再调用上述 API;不要创建 cron、维护任务文件、计算下一次执行时间或自行发送微信。
|
||||||
|
|
||||||
|
### 创建任务
|
||||||
|
|
||||||
|
用户提出“每天早上 9 点给技术交流群发昨天的日报”时,按以下顺序执行:
|
||||||
|
|
||||||
|
1. 调用 `/health`,确认 TraceMemo 和数据库可用。
|
||||||
|
2. 调用 `/wechat-personal/send-capability`,只有 `capability.status === "ready"` 且 `capability.capabilities.image === true` 才允许继续。
|
||||||
|
3. 用户使用“今天”“昨天”等相对日期时调用 `/current_time`;日报任务的 `schedule.time` 使用 TraceMemo 本机时区的 `HH:mm`,不要转成 UTC。
|
||||||
|
4. 调用 `/chatroom` 或 `/contact?type=group` 查找群聊。名称匹配多个结果时,必须把候选项展示给用户并要求选择;不能猜测。
|
||||||
|
5. 使用唯一群聊的 `talker` 创建:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "技术交流群 · 每日日报",
|
||||||
|
"group": { "talker": "xxx@chatroom", "name": "技术交流群" },
|
||||||
|
"schedule": { "type": "daily", "time": "09:00" },
|
||||||
|
"reportRange": "yesterday",
|
||||||
|
"target": { "type": "wechat_group", "talker": "xxx@chatroom" },
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
如果 API 返回 `409` 且 `error === "duplicate"`,告诉用户相同任务已经存在,不要再次创建。能力状态不是 `ready` 时(`unsupported`、`unconfigured`、`needs_binding`、`initializing` 或 `error`),直接说明需要先在 TraceMemo 的“设置 → 发送能力”里完成个人微信绑定和能力检测,不要继续创建任务。
|
||||||
|
|
||||||
|
### 查看、修改和执行
|
||||||
|
|
||||||
|
- “我现在有哪些定时日报”调用 `GET /scheduled-reports`,使用返回的 `tasks` 展示任务名称、群聊、每天的时间、范围、目标和启停状态。
|
||||||
|
- 修改前先查询列表并确认唯一任务,再调用 `PATCH /scheduled-reports/:id`。只提交需要修改的字段,例如 `{"schedule":{"type":"daily","time":"10:00"}}`。
|
||||||
|
- 暂停调用 `/scheduled-reports/:id/disable`,恢复调用 `/scheduled-reports/:id/enable`。
|
||||||
|
- “现在执行一次”调用 `/scheduled-reports/:id/run`,不要改用 `/agent/group-report` 后自行发送微信;该接口和定时执行共用同一条链路。
|
||||||
|
- 查询执行结果调用 `/scheduled-reports/:id/executions`,根据 `status`、`startedAt`、`finishedAt`、`message` 和 `error` 向用户解释结果。
|
||||||
|
|
||||||
|
### 删除确认
|
||||||
|
|
||||||
|
删除是不可逆操作。收到删除请求后,先用任务列表找到唯一任务,向用户展示任务名称、时间、日报范围和发送目标并明确询问确认;只有用户明确确认后,才调用 `DELETE /scheduled-reports/:id`。
|
||||||
|
|
||||||
|
## 时间与上下文规则
|
||||||
|
|
||||||
|
`/chatlog` 的 `time` 支持 `YYYY-MM-DD`、日期闭区间和分钟范围;也可以使用 Unix 秒级 `startTime`/`endTime`。时间按 TraceMemo 所在机器的本机时区解释。
|
||||||
|
|
||||||
|
当用户问“某个话题是谁说的、后来结论是什么”时,先定位会话和时间,再读取关键消息前后文。回答时区分:
|
||||||
|
|
||||||
|
- 原消息明确写出的内容;
|
||||||
|
- 根据多条消息整理出的总结;
|
||||||
|
- 没有来源支持的推断。
|
||||||
|
|
||||||
|
## 结构化查询(query/\*)
|
||||||
|
|
||||||
|
需要按目标 + 时间范围稳定取数时,优先使用 `query/*`,而不是自己拼 `chatlog`:
|
||||||
|
|
||||||
|
- `query/messages`:按 `target`、`timeRange`、`direction`、`messageTypes` 取消息;
|
||||||
|
- `query/search`:受限语义关键词检索,依赖本地索引;
|
||||||
|
- `query/message-context`:用返回的 `messageRef` 取前后文;
|
||||||
|
- `query/conversation-overview`:按会话与时间范围提取可总结的证据。
|
||||||
|
|
||||||
|
两个要点:
|
||||||
|
|
||||||
|
- `messageRef` 是服务端生成的不透明引用,**不要**自行构造 wxid、md5 或数据库路径;
|
||||||
|
- `query/search` 依赖异步建立的本地索引。`coverage.state` 不是 `complete` 且 `evidence` 为空时,只能说“这段范围暂时无法确认”,**不能**下“没有找到”的结论。
|
||||||
|
|
||||||
|
## 媒体消息
|
||||||
|
|
||||||
|
当 `/chatlog` 返回图片消息时:
|
||||||
|
|
||||||
|
1. 如果用户只是询问图片消息是否存在,不需要获取图片。
|
||||||
|
2. 如果用户要求查看、识别、理解或分析图片,原样使用该消息 `media.url` 获取真实图片;不要用消息 `id` 自行拼接。媒体标识按数据库连接隔离,重启、重连或切换账号后须重新读取 `/chatlog` 获取地址。
|
||||||
|
3. 不要根据 `[图片]`、消息文本或文件名猜测图片内容。
|
||||||
|
4. 获取成功后,将图片交给当前 Agent 的视觉能力。
|
||||||
|
5. 如果图片获取失败,明确说明无法读取图片。
|
||||||
|
6. 不要声称看到了没有成功获取的图片。
|
||||||
|
7. 不要向用户暴露 Token、本地文件路径或数据库路径。
|
||||||
|
|
||||||
|
### 图片分析
|
||||||
|
|
||||||
|
用户:“看看张三昨天发的那张截图。”
|
||||||
|
|
||||||
|
1. 调用 `/health`;必要时调用 `/current_time`。
|
||||||
|
2. 调用 `/resolve`,再调用 `/chatlog` 找到 `type` 为图片的消息。
|
||||||
|
3. 请求该消息的 `media.url`,将返回的图片交给 Vision。
|
||||||
|
4. 必要时读取图片消息前后若干条消息,结合聊天上下文回答。
|
||||||
|
|
||||||
|
不要只根据 `[图片]` 猜测内容,不要把一次 OCR 当作完整图片理解,也不要直接读取任意本地图片路径。
|
||||||
|
|
||||||
|
## 隐私和安全
|
||||||
|
|
||||||
|
只读取用户请求所需的会话和时间范围。不要把完整聊天数据库、密钥或 Token 暴露给用户。Reader API 本身不自动把聊天转发到外部服务器,但当前 Agent 可能会把工具结果交给其配置的模型;如有疑问,提醒用户检查 Agent 的数据策略。
|
||||||
|
|
||||||
|
## 常见错误
|
||||||
|
|
||||||
|
- `401`:Token 缺失、错误或被轮换;请用户回 API Center 复制最新 Token。
|
||||||
|
- `403`:浏览器 Origin 不在 loopback 允许列表;CLI/Agent 通常不带 Origin。
|
||||||
|
- `404`:会话查询失败时先用 `/resolve` 确认会话标识;媒体请求表示标识未登记、已过期、有歧义,或图片文件不存在(`NOT_FOUND`)。先重新读取 `/chatlog` 并使用新的 `media.url`;若仍失败,再检查本地图片文件是否存在。
|
||||||
|
- `422`:媒体标识格式错误,或消息不是可读取的图片(`NOT_IMAGE`)。
|
||||||
|
- `503`:用户还没有完成数据库连接或对应服务未就绪。
|
||||||
|
- 空结果:缩小/扩大时间范围,确认账号和会话,再检查媒体或语音是否可读。
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# 用 AI 查找你以前聊过的信息
|
||||||
|
|
||||||
|
## AI Search 是什么
|
||||||
|
|
||||||
|
你可以把它理解成“会帮你翻聊天记录的 AI”。
|
||||||
|
|
||||||
|
普通搜索需要你猜关键词;AI Search 更适合这些问题:
|
||||||
|
|
||||||
|
- “我们上个月为什么决定延期?”
|
||||||
|
- “谁提过这个项目,后来结论是什么?”
|
||||||
|
- “过去一周有哪些待跟进事项?”
|
||||||
|
|
||||||
|
它会先在本机查找相关聊天,再把受控范围内的内容交给你选择的 AI Provider 生成回答。它不是凭空记忆,也不是把整库聊天一次性上传。
|
||||||
|
|
||||||
|
## 第一次使用
|
||||||
|
|
||||||
|
1. 进入“设置 → AI 模型”,添加一个 Provider,填写服务地址、模型和认证信息,然后测试连接。
|
||||||
|
2. 打开“问问微信”。
|
||||||
|
3. 选择所有聊天、群聊、单聊或当前会话,并选择今天、近 7 天、近 30 天或不限时间;范围越明确,答案越容易核对。
|
||||||
|
4. 输入问题并开始分析。
|
||||||
|
|
||||||
|
如果知识库尚未建立,页面会提示你建立或同步;你也可以先直接使用当前可用的搜索路径。
|
||||||
|
|
||||||
|
## 怎么提问更容易得到好结果
|
||||||
|
|
||||||
|
把“谁、什么时候、在哪个群、想找什么结果”写出来。例如:
|
||||||
|
|
||||||
|
> “在产品交流群里,查找 2026 年 7 月讨论发布延期的消息,列出结论和待办。”
|
||||||
|
|
||||||
|
尽量避免只写“总结一下”。如果你只记得模糊含义,也可以先提问,再根据来源缩小范围继续追问。
|
||||||
|
|
||||||
|
## AI 回答后先看什么
|
||||||
|
|
||||||
|
不要只看结论。回答区域通常还会展示:
|
||||||
|
|
||||||
|
- 参考了哪些聊天内容;
|
||||||
|
- 来源来自哪个会话、发送者和时间;
|
||||||
|
- 哪一段回答对应哪条来源;
|
||||||
|
- 本次查找经过了哪些阶段、耗时和覆盖情况;
|
||||||
|
- 是否存在未转写语音、媒体不可用或结果不完整的提示。
|
||||||
|
|
||||||
|
你可以点击来源回到档案中的原始消息。产品内部将这些信息称为 Evidence、Citation 和 Search Trace,用户可以把它们理解为“依据、来源标记和查找过程”。详见[如何核对 AI 的回答来源](../concepts/answer-sources.md)。
|
||||||
|
|
||||||
|
## 查找过程和跳回原消息
|
||||||
|
|
||||||
|
查找过程中,界面依次显示真实阶段:**理解问题 → 查找相关聊天 → 整理证据 → 生成回答**。跨会话、大范围检索更慢时,副提示会写明“正在搜索较大范围的聊天记录…”。阶段只在真正进入下一步时前进,不使用定时器或百分比伪造进度。
|
||||||
|
|
||||||
|
查找结束后,界面给出耗时拆解:**总耗时**,以及其中分别花在 **AI 生成** 和 **本地查询** 上的时间。这样你能判断慢在哪——是模型在写答案,还是本机还在翻聊天记录。
|
||||||
|
|
||||||
|
点击来源卡片的 **“跳转到原聊天”** 会真的打开对应会话并定位到那条消息:
|
||||||
|
|
||||||
|
- 群聊来源打开的是那个群,而不是群里某个联系人;
|
||||||
|
- 会加载该消息前后的上下文,并滚动到它、短暂高亮;
|
||||||
|
- 只加载目标消息附近的一段,不会把整个会话历史全部读出来;
|
||||||
|
- 如果这条消息已经不在本地(例如已被删除),界面会明确说明“已打开对应会话,但暂时无法定位原消息”,不会假装跳转成功。
|
||||||
|
|
||||||
|
## 什么时候不要直接相信答案
|
||||||
|
|
||||||
|
- 来源很少,或时间范围与问题不一致;
|
||||||
|
- 回答提到了来源中没有的细节;
|
||||||
|
- 关键内容来自未转写语音、无法读取的图片或转发消息;
|
||||||
|
- 页面提示只覆盖了部分聊天。
|
||||||
|
|
||||||
|
这些情况下,打开原消息,扩大或缩小范围,再重新提问。必要时把问题改成“只列出原文明确说过的内容”。
|
||||||
|
|
||||||
|
## 取消、失败和降级
|
||||||
|
|
||||||
|
分析过程中可以取消当前任务。检索或模型请求失败时,页面可能保留已找到的来源或切换到备用路径;这不代表一定得到了完整答案。请查看提示、检索详情和[排障文档](./troubleshooting.md#ai-没有结果或回答失败)。
|
||||||
|
|
||||||
|
## 数据会发到哪里
|
||||||
|
|
||||||
|
本地解析、索引和候选消息查找在本机完成。只有完成 AI 任务所需的用户问题、受控检索上下文和最终用于总结的来源内容,才可能发送到你配置的 Provider;具体边界见[数据、隐私与安全](./privacy.md)。
|
||||||
|
|
||||||
|
使用远程 Provider 时,当前界面会在本次请求发出前显示接收方和发送范围,等待你确认。当前实现最多发送 8 条最终来源,不会发送完整微信数据库、数据库密钥、绝对文件路径或内部会话/消息引用 ID;这次确认不会自动授权之后的其他请求。
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# 查看和搜索聊天
|
||||||
|
|
||||||
|
“档案”是你直接阅读微信历史的地方。适合查原文、回看上下文、确认 AI 来源,也适合在你已经知道关键词时快速定位。
|
||||||
|
|
||||||
|
## 选择要看的会话
|
||||||
|
|
||||||
|
左侧会话列表可以浏览已读取到的联系人、群聊和公众号。选中会话后,右侧显示消息时间线;滚动到较早位置可以继续加载历史。
|
||||||
|
|
||||||
|
如果你从 AI 回答的来源进入档案,应用会自动切换到对应会话并尽量定位到消息时间。
|
||||||
|
|
||||||
|
## 普通关键词搜索什么时候最好用
|
||||||
|
|
||||||
|
当你记得以下任意信息时,优先使用档案搜索:
|
||||||
|
|
||||||
|
- 一段原话或关键词;
|
||||||
|
- 人名、群名、项目名;
|
||||||
|
- 链接、文件名或订单号;
|
||||||
|
- 大致知道在哪个联系人或群里。
|
||||||
|
|
||||||
|
关键词搜索速度快、结果直观,但它不会理解“意思相近但没有相同词”的问题。
|
||||||
|
|
||||||
|
## 怎么找到联系人
|
||||||
|
|
||||||
|
档案搜索会综合多个身份字段匹配联系人或群聊,包括:
|
||||||
|
|
||||||
|
- 通讯录备注(remark);
|
||||||
|
- 微信昵称;
|
||||||
|
- 当前微信号;
|
||||||
|
- wxid;
|
||||||
|
- 拼音全拼和拼音首字母。
|
||||||
|
|
||||||
|
因此可以直接输入备注、昵称、微信号或拼音查找。搜索联系人和搜索消息是两步:先确认目标会话,再在会话内查关键词;记得大意但不知道原话时,改用[AI Search](./ai-search.md)。
|
||||||
|
|
||||||
|
## 文字转语音
|
||||||
|
|
||||||
|
在档案中选择当前联系人或群聊,输入文字后生成语音,试听确认后发送。这个入口只处理明确的文字转语音动作,不是任意文本、图片或本地语音文件发送器。
|
||||||
|
|
||||||
|
## 消息和媒体
|
||||||
|
|
||||||
|
根据微信数据中实际可用的资源,档案可以展示文本、图片、视频、语音、文件、链接、引用、小程序、表情和系统消息等类型。媒体是否能显示,取决于本机原始资源是否仍然存在、权限是否完整以及当前微信版本的存储方式。
|
||||||
|
|
||||||
|
不要把“消息类型已读取”理解成“所有媒体都一定能解码”。遇到图片或视频空白时,请先检查[媒体与导出排查](./troubleshooting.md#媒体显示或导出异常)。
|
||||||
|
|
||||||
|
如果文字正常但图片无法打开,进入“设置 → 图片解密”查看当前状态。可以尝试自动获取,也可以在已经知道正确密钥时手动配置;原文件已经被微信清理时,仅配置密钥也无法恢复图片。
|
||||||
|
|
||||||
|
联系人和群聊列表会尽量显示头像、备注和昵称。头像或资料缺失时不影响消息读取;这通常表示本机没有对应资源,或微信没有返回完整资料。
|
||||||
|
|
||||||
|
## 保护自己不被误导
|
||||||
|
|
||||||
|
档案中的原始消息是核对 AI 结果的最终依据。看到 AI 的总结、日报或来源时,建议:
|
||||||
|
|
||||||
|
1. 打开来源对应的会话;
|
||||||
|
2. 查看消息前后几条上下文;
|
||||||
|
3. 注意消息时间、发送者和是否存在转发/引用;
|
||||||
|
4. 对未转写的语音、无法读取的图片保持不确定判断。
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### 会话列表为空
|
||||||
|
|
||||||
|
确认数据库连接成功、连接的是正确微信账号,并重新加载数据。若仍为空,查看[连接微信失败](./troubleshooting.md#连接微信失败)。
|
||||||
|
|
||||||
|
### 搜索不到明明存在的消息
|
||||||
|
|
||||||
|
先缩小到正确会话,再尝试更短的关键词或原文片段。对于“以前讨论过什么”这类语义问题,改用[AI Search](./ai-search.md)。
|
||||||
|
|
||||||
|
### 想跨多个会话查找
|
||||||
|
|
||||||
|
使用“问问微信”,并在问题中写清时间范围、人物或群聊范围。需要更稳定的跨会话查找时,先建立[本地知识库](./knowledge.md)。
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# 导出聊天档案
|
||||||
|
|
||||||
|
导出适合把微信里的重要讨论保存成可阅读、可分享或可继续处理的文件。
|
||||||
|
|
||||||
|
## 支持的格式
|
||||||
|
|
||||||
|
| 格式 | 适合什么任务 | 当前边界 |
|
||||||
|
| -------- | ------------------------ | ---------------------------------------------------------- |
|
||||||
|
| HTML | 完整阅读和长期归档 | 可包含媒体、头像和可选语音转写;支持多会话、增量合并和 ZIP |
|
||||||
|
| Markdown | 笔记、版本管理和再次编辑 | 主要保留文本内容,不复制 HTML 资源文件 |
|
||||||
|
| CSV | 表格分析 | 主要保留文本内容,不复制 HTML 资源文件 |
|
||||||
|
| JSON | 程序处理和数据归档 | 主要保留文本内容,不复制 HTML 资源文件 |
|
||||||
|
|
||||||
|
ZIP 是 HTML 资源包的压缩选项,不是第五种内容格式。
|
||||||
|
|
||||||
|
## 导出步骤
|
||||||
|
|
||||||
|
可以打开一级导航“导出”,也可以在“档案”的聊天顶部点击“导出”并选择时间范围。
|
||||||
|
|
||||||
|
1. 选择一个或多个联系人/群聊。
|
||||||
|
2. 选择时间范围和消息类型。
|
||||||
|
3. 选择格式;只有 HTML 可以配置媒体资源、语音转写和 ZIP。
|
||||||
|
4. 按需要设置头像、原图/缩略图和缺失资源处理。
|
||||||
|
5. 设置文件名并开始导出。
|
||||||
|
6. 在导出任务中心查看读取、解析、媒体处理、转写、写入和压缩进度;完成后打开文件位置。
|
||||||
|
|
||||||
|
## 多会话和增量导出
|
||||||
|
|
||||||
|
HTML 支持把最多五个会话合并到一个档案中;选择多个会话后,其他格式会不可用。再次使用相同名称导出 HTML 时,可以把新消息增量合并到已有档案;这不会删除之前已导出的消息。
|
||||||
|
|
||||||
|
## 媒体怎么处理
|
||||||
|
|
||||||
|
原图、缩略图、缺失资源和头像都可能影响导出大小与可读性。想要小文件时关闭媒体或选择缩略图;想要长期保存时,确认原始媒体目录仍可访问,并考虑 ZIP 归档。
|
||||||
|
|
||||||
|
HTML 导出可以选择在任务中执行本地语音转写,并把成功结果显示在语音气泡下方。语音模型不可用或识别失败时,导出不会把失败内容当成已转写文本。
|
||||||
|
|
||||||
|
## 导出和原始数据的关系
|
||||||
|
|
||||||
|
导出是复制/整理结果,不会修改微信原始数据库。删除导出文件也不会影响应用内聊天记录或本地知识库。
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
# 第一次使用 TraceMemo
|
||||||
|
|
||||||
|
如果你刚下载 TraceMemo,只需要完成一条主线:
|
||||||
|
|
||||||
|
> 安装应用 → 连接微信数据 → 确认聊天已加载 → 搜索或提问。
|
||||||
|
|
||||||
|
这篇文档不要求你先学习内部术语;先把第一个问题问出来,之后再按需要深入了解产品名称和进阶功能。
|
||||||
|
|
||||||
|
## 1. 开始前准备
|
||||||
|
|
||||||
|
TraceMemo 需要读取微信本地数据库。首次使用前,请确认已安装受支持的微信客户端,并按照连接页面完成数据库密钥获取。
|
||||||
|
|
||||||
|
| 系统 | 微信客户端 | 首次连接说明 |
|
||||||
|
| ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
|
| macOS | **微信 4.1.13 系列**(推荐 [4.1.13.8](https://github.com/zsbai/wechat-versions/releases/tag/4.1.13.8))<br>备选 [4.1.8.100](https://github.com/zsbai/wechat-versions/releases/tag/4.1.8.100) | 支持 Apple Silicon(M 系列)和 Intel Mac 自动获取数据库密钥。首次连接时,请按照 TraceMemo 页面提示完成系统授权及微信登录操作。 |
|
||||||
|
| Windows | **微信 4.x**(推荐 [4.1.9.57](https://github.com/iibob/wechat-win-archive/releases/tag/v4.1.9.57)) | 支持自动获取数据库密钥。首次使用时请确认微信数据目录正确,无需处理 macOS 的 SIP 设置。 |
|
||||||
|
|
||||||
|
### macOS 用户
|
||||||
|
|
||||||
|
TraceMemo 已支持两种 Mac 架构:
|
||||||
|
|
||||||
|
- **Apple Silicon(M1 / M2 / M3 / M4 等)**
|
||||||
|
- **Intel Mac(x64)**
|
||||||
|
|
||||||
|
两种架构均支持自动获取微信数据库密钥,TraceMemo 会根据当前 Mac 自动选择对应的连接方式。
|
||||||
|
|
||||||
|
Apple Silicon 和 Intel 均已适配微信 macOS `4.1.13` 系列。首次获取密钥时,请让微信停留在登录页面,并按照 TraceMemo 中显示的步骤操作。
|
||||||
|
|
||||||
|
> 不同 Mac 架构的连接流程可能略有区别,请始终以应用内「第一次使用」页面显示的提示为准。
|
||||||
|
|
||||||
|
### 关于微信版本
|
||||||
|
|
||||||
|
- 上表中的版本是当前 TraceMemo 已适配或推荐使用的版本,并不代表只有这些版本可以运行。
|
||||||
|
- TraceMemo 必须取得当前微信账号对应的数据库密钥,才能读取聊天记录。
|
||||||
|
- 你需要有权访问要读取的微信账号和聊天数据。
|
||||||
|
- 如果要使用 AI 问答、群聊日报或图片理解,还需要在应用中配置一个 AI 服务。「图片文字索引」不在此列——它在本机识别图片里的文字,不需要配置 AI。
|
||||||
|
|
||||||
|
当前代码按微信 4.x 数据结构处理。不同微信客户端版本、系统权限和数据迁移状态可能影响自动连接;遇到问题时请查看[常见问题与排查](./troubleshooting.md)。
|
||||||
|
|
||||||
|
## 2. 安装并启动
|
||||||
|
|
||||||
|
安装包统一从 [GitHub Releases](https://github.com/Wxw-Gu/TraceMemo/releases) 下载。
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
1. 从 Releases 下载 Windows x64 的 `tracememo-<版本号>-setup.exe` 安装包。
|
||||||
|
2. 双击安装包,按向导完成安装。
|
||||||
|
3. 启动 TraceMemo。
|
||||||
|
4. 如果安装完成后软件无法启动,请安装 Microsoft Visual C++ x64 运行库:[vc_redist.x64.exe](https://aka.ms/vc14/vc_redist.x64.exe),安装完成后重新启动 TraceMemo。
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
|
||||||
|
1. 从 Releases 下载与你 Mac 处理器架构匹配的 `.dmg`:
|
||||||
|
- Apple Silicon(M 系列、`arm64`):`tracememo-<版本号>-arm64.dmg`
|
||||||
|
- Intel(`x64`):`tracememo-<版本号>-x64.dmg`
|
||||||
|
2. 打开 DMG,将 TraceMemo 拖入“应用程序”文件夹。
|
||||||
|
3. 如果系统提示“无法打开,因为开发者无法验证”,前往“系统设置 → 隐私与安全性”,点击“仍要打开”。
|
||||||
|
4. 如果系统提示应用已损坏,可在终端执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xattr -cr "/Applications/TraceMemo.app"
|
||||||
|
```
|
||||||
|
|
||||||
|
5. 先让微信停在**未登录窗口**(如果微信已经登录,请退出当前账号,**只关闭窗口不算**),再启动 TraceMemo。
|
||||||
|
- 连接时会按提示自动获取数据库密钥;应用会根据当前架构进入对应流程,按连接页面显示的授权要求操作即可。
|
||||||
|
- **等第五步 明确提示可以登录后,再回到微信点击登录。** 在此之前不要先点登录,否则这次密钥获取会失败,需要重新开始。
|
||||||
|
- 只有页面明确提示时才按[关闭 SIP 教程](../mac-disable-sip.md)处理。关闭 SIP 会降低系统安全性,完成密钥配置后应重新开启。
|
||||||
|
|
||||||
|
> **注意事项:取密钥时提示监听超时(`CAPTURE_TIMEOUT`,或相关失败)**
|
||||||
|
>
|
||||||
|
> 这说明「先让微信停在未登录窗口」这一步操作有误——常见原因是微信当时已经登录,或者还没等连接页面提示就先点了登录
|
||||||
|
>
|
||||||
|
> 请先退出微信账号、回到**未登录窗口**,然后按上面的第 5 步**重新来一遍**(等连接页面明确提示可以登录后,再回到微信点击登录)。
|
||||||
|
|
||||||
|
更完整的权限和安全边界见 [macOS 数据访问说明](../platform/macos.md)。
|
||||||
|
|
||||||
|
## 3. 让应用读取微信数据
|
||||||
|
|
||||||
|
首次启动会自动进入“第一次使用”页面。页面会根据当前系统显示连接方式和注意事项:
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="../../public/setup-page.png" alt="第一次使用连接页面" width="820" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
通常按下面三步操作即可:
|
||||||
|
|
||||||
|
1. **确认微信数据目录**:自动识别不准确时,打开微信设置中的缓存/存储管理,复制实际路径并在页面中修改。
|
||||||
|
2. **让微信停在登录页面**:如果微信已经登录,先退出当前微信账号,不只是关闭微信窗口。
|
||||||
|
3. **点击“开始连接”并按提示获取密钥**:软件准备好连接组件后会提示你登录微信;回到微信完成登录,再等待数据库、账号和联系人检查完成。
|
||||||
|
|
||||||
|
只有已经通过其他方式取得当前账号数据库密钥的高级用户,才需要选择“手动连接”。Windows 不需要关闭 SIP;macOS 是否需要额外授权或处理 SIP,以当前连接页面提示为准。
|
||||||
|
|
||||||
|
连接页面会显示微信状态、数据库状态和诊断结果。连接失败时先不要反复删除数据,优先查看[连接问题排查](./troubleshooting.md#连接微信失败)。
|
||||||
|
|
||||||
|
## 4. 确认第一次连接成功
|
||||||
|
|
||||||
|
连接成功后会进入“档案”页面。你可以用下面三个信号确认已经准备好:
|
||||||
|
|
||||||
|
- 左侧出现联系人或群聊列表;
|
||||||
|
- 选中一个会话后,右侧能看到历史消息;
|
||||||
|
- 搜索框可以在当前会话中定位文字。
|
||||||
|
|
||||||
|
如果联系人列表为空,先检查是否连到了正确账号和数据目录,再重新加载会话。
|
||||||
|
|
||||||
|
文字消息正常但图片打不开时,不代表数据库连接失败。打开“设置 → 图片解密”查看状态并尝试自动获取;图片原文件缺失、权限不足或密钥不匹配时,部分图片仍可能无法显示。
|
||||||
|
|
||||||
|
## 5. 完成你的第一个任务
|
||||||
|
|
||||||
|
### 只是想找一句话
|
||||||
|
|
||||||
|
进入“档案”,选择联系人或群聊,在会话内搜索关键词。适合你记得原话、姓名、链接或大致关键词的情况。
|
||||||
|
|
||||||
|
### 想找一个模糊的结论
|
||||||
|
|
||||||
|
先在“设置 → AI 模型”添加并测试一个 Provider,再进入“问问微信”描述问题,例如:
|
||||||
|
|
||||||
|
- “上个月技术群讨论过哪些发布问题?”
|
||||||
|
- “张三之前发过的项目地址在哪里?”
|
||||||
|
- “过去一周有没有人提到退款?”
|
||||||
|
|
||||||
|
这就是 AI Search:它会先帮你从本机聊天中找出相关内容,再让你配置的模型组织答案。你不需要知道关键词在哪,但问题越具体,结果越容易核对。
|
||||||
|
|
||||||
|
### 想让 AI 的答案可核对
|
||||||
|
|
||||||
|
回答生成后,打开来源或检索详情,查看它参考的聊天内容、会话、时间和原始消息。你可以从来源直接跳回“档案”检查上下文。
|
||||||
|
|
||||||
|
产品把这些来源信息分别称为 Evidence、Citation 和 Search Trace;普通使用时只需要记住“答案可以回到原消息核对”即可。详见[如何核对 AI 的回答来源](../concepts/answer-sources.md)。
|
||||||
|
|
||||||
|
## 6. 接下来可以做什么
|
||||||
|
|
||||||
|
- [查看和搜索聊天](./chat-archive.md)
|
||||||
|
- [使用 AI 查找聊天信息](./ai-search.md)
|
||||||
|
- [建立本地知识库,让后续查找更稳定](./knowledge.md)
|
||||||
|
- [生成群聊日报或总结](./report.md)
|
||||||
|
- [转写微信语音](./voice.md)
|
||||||
|
- [导出聊天档案](./export.md)
|
||||||
|
- [让日报、退群通知按规则自动运行](../README.md#日报与自动化)
|
||||||
|
- [在微信里向 TraceMemo 提问](../agent/agent-hub.md)
|
||||||
|
- [让外部 Agent 查询微信历史](../agent/overview.md)
|
||||||
|
|
||||||
|
## 7. 想直接在微信里提问
|
||||||
|
|
||||||
|
如果你希望直接在微信里向 TraceMemo 提问,而不是另外配置 Codex 等外部 Agent,请使用 Agent Hub:
|
||||||
|
|
||||||
|
1. 先完成上面的微信数据库连接,并确认“档案”里能看到聊天。
|
||||||
|
2. 打开应用主导航中的“Agent”;页面标题为“Agent Hub”。
|
||||||
|
3. 确认 Agent Hub 显示“运行中”,数据 API/数据库状态可以查询。
|
||||||
|
4. 点击“扫码登录微信机器人”,用微信扫描二维码,并在手机上确认登录。
|
||||||
|
5. 状态变为“在线”后,向这个机器人发送文字消息。
|
||||||
|
|
||||||
|
可以先试试这些真实支持的请求:
|
||||||
|
|
||||||
|
- “最近 5 个会话”;
|
||||||
|
- “帮我看看最近跟张三聊了些什么”;
|
||||||
|
- “生成产品交流群今天的群聊总结图片”。
|
||||||
|
|
||||||
|
机器人会把处理结果回复给发消息的人。联系人聊天总结、群聊总结和需要理解自然语言的请求依赖“设置 → AI 模型”中已经配置好的 AI 服务。当前实时入口主要处理文字消息;它不是支持任意图片、语音、文件理解、群发或定时任务的通用机器人。机器人账号扫码登录与读取你微信数据库是两条独立流程,都需要分别确认账号和权限。
|
||||||
|
|
||||||
|
Agent Hub 是普通用户可以直接使用的入口,不需要安装 Reader Skill 或配置 API Token。Reader Skill 和 API 只用于让外部 Agent 主动查询历史微信。
|
||||||
|
|
||||||
|
## 8. 需要配置 AI 吗?
|
||||||
|
|
||||||
|
不一定。浏览聊天、普通关键词搜索、建立本地知识库、图片文字识别、离线语音转写和导出都不要求在线 AI 服务。
|
||||||
|
|
||||||
|
使用“问问微信”、群聊日报或图片理解时,需要在“设置 → AI 模型”中添加并测试 AI 服务。你主动开始并确认远程 AI 功能后,完成任务所需的内容才可能发送给该服务;计费、留存和地区规则由对应服务商决定。
|
||||||
|
|
||||||
|
这两种情况容易混淆:**本机识别图片里的文字**(图片文字索引)不联网、不需要 AI 服务;**让模型看图并回答**(图片理解)才需要配置 AI 服务。
|
||||||
|
|
||||||
|
## 9. 数据和隐私的最低须知
|
||||||
|
|
||||||
|
- 微信数据库、聊天解析、本地索引和图片文字识别默认留在本机。
|
||||||
|
- 离线语音转写使用本地模型;它与在线 AI 请求是两条不同的数据路径。
|
||||||
|
- 图片文字索引只在本机识别,原始图片不会因为本地识别而上传;识别出的文字会进入本地索引,供搜索和“问问微信”使用。
|
||||||
|
- 你主动开始并确认 AI 问答或日报后,完成任务所需的受控上下文才可能发送给你选择的 AI 服务;打开应用不会自动上传全部聊天。
|
||||||
|
- 应用内 Local HTTP API 默认只监听 `127.0.0.1:6131`,受保护接口需要 Token。
|
||||||
|
|
||||||
|
完整边界见[数据、隐私与安全](./privacy.md)。
|
||||||
|
|
||||||
|
## 10. 如果你卡住了
|
||||||
|
|
||||||
|
按现象进入[常见问题与排查](./troubleshooting.md):连接失败、聊天为空、AI 没有结果、语音模型不可用、导出失败和 Agent 无法访问分别有不同处理方式。
|
||||||
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 214 KiB |
@@ -0,0 +1,19 @@
|
|||||||
|
# Intel Mac 首次获取密钥
|
||||||
|
|
||||||
|
请先备份微信本地数据。密钥获取和数据库验证都在本机完成,不要把密钥、日志或数据库发给他人。
|
||||||
|
|
||||||
|
## 开始使用
|
||||||
|
|
||||||
|
1. 启动 TraceMemo,在首次连接页面点击“重新检查环境”。
|
||||||
|
2. 如果页面显示需要准备连接环境,点击“准备环境”,按页面提示完成。
|
||||||
|
3. 打开微信,让微信停在未登录页面,不要先点击登录。
|
||||||
|
4. 回到 TraceMemo,选择微信账号,点击“开始获取密钥”。
|
||||||
|
5. 等待页面提示可以登录后,立即回到微信点击登录。
|
||||||
|
6. 等待 TraceMemo 显示“获取成功,验证数据库连接”。
|
||||||
|
|
||||||
|
获取成功后,请保存密钥。如果页面提示需要重新准备,按页面的“查看说明”操作,不要连续重试。
|
||||||
|
|
||||||
|
## 注意
|
||||||
|
|
||||||
|
- 密钥只保存在本机的安全存储中。
|
||||||
|
- 请不要删除微信本地数据目录。
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# 把聊天变成更容易再次找到的本地资料
|
||||||
|
|
||||||
|
## 你为什么需要 Knowledge
|
||||||
|
|
||||||
|
如果你经常查同一批工作群、项目讨论或长期联系人,只靠每次临时翻聊天会越来越慢。Knowledge 会在本机建立一份可重复查找的索引,让“以前聊过什么”这类问题更容易跨会话、跨时间找到相关内容。
|
||||||
|
|
||||||
|
它不是另一个聊天窗口,也不会替你修改微信原始数据库;它是 TraceMemo 为当前账号维护的本地加速资料。
|
||||||
|
|
||||||
|
## 建立和同步
|
||||||
|
|
||||||
|
Knowledge 不会在第一次连接后自动悄悄建立。进入“问问微信”后,在“本地知识库”区域点击:
|
||||||
|
|
||||||
|
- **建立本地知识库**:第一次读取当前账号的可检索聊天;
|
||||||
|
- **同步最新记录**:已有索引时,只补充新增或变化的内容。
|
||||||
|
|
||||||
|
同步在后台运行,**期间仍然可以正常提问和分析**,不会被禁用。索引还没追完时,答案会基于当前已经可用的部分给出,并在界面标注覆盖范围。
|
||||||
|
|
||||||
|
知识库卡片同时显示两组互相独立的信息:
|
||||||
|
|
||||||
|
- **规模**:已索引消息、知识片段、磁盘占用——说明索引有多大;
|
||||||
|
- **状态与本轮进度**:说明索引现在处于什么状态、这一轮同步在做什么(扫了多少、真正新增了多少、处理到第几个会话)。
|
||||||
|
|
||||||
|
`最新索引` 只表示索引已经覆盖到聊天记录的哪个时间点,**不等于**整库已经建完;进度里的计数是**本轮**的数字,不是全部历史的总数。
|
||||||
|
|
||||||
|
### 状态怎么读
|
||||||
|
|
||||||
|
| 状态 | 含义 |
|
||||||
|
| ------------------- | ------------------------------------------------ |
|
||||||
|
| 可用 · 已追至最新 | 索引已覆盖到聊天记录的最新位置,可以直接用 |
|
||||||
|
| 可用 · 正在追新 | 索引可用,正在后台补充最近新增的消息 |
|
||||||
|
| 可用 · 正在补齐历史 | 索引可用,正在后台补齐较早的历史内容 |
|
||||||
|
| 可用 · 同步已取消 | 索引仍然可用;上一轮同步被取消,已建立的部分保留 |
|
||||||
|
| 可用 · 更新失败 | 索引仍然可用;上一轮同步出错,可以稍后重试 |
|
||||||
|
|
||||||
|
只有确实追平、且没有待补齐内容时才会出现“已追至最新”。索引不可查询时不会显示“可用”。
|
||||||
|
|
||||||
|
### 取消和继续
|
||||||
|
|
||||||
|
同步过程中可以点击 **取消同步**(点击后显示“正在取消…”)。取消只结束当前这一轮,不会删除已经建立的索引,也不会回滚已完成的部分;下次同步会从上次停下的位置继续,不需要从头重扫。中断过的索引仍然可以正常搜索。
|
||||||
|
|
||||||
|
## 图片文字索引(另一份索引)
|
||||||
|
|
||||||
|
本地索引其实有两份,彼此独立:
|
||||||
|
|
||||||
|
- **聊天记录索引**(也就是上面说的 Knowledge):索引文字消息,用于跨会话、跨时间查找;
|
||||||
|
- **图片文字索引**:在本机识别微信图片里的文字(截图、公告、报价图等),把识别结果也变成可搜索的文字。
|
||||||
|
|
||||||
|
“独立”的意思是:聊天记录索引建好了,并不代表图片里的文字就搜得到。建立图片文字索引后,可以在“问问微信”里直接搜截图或公告图里写过的词。
|
||||||
|
|
||||||
|
图片文字索引只在本机识别,原始图片不会因为本地识别而上传。它**不等于“图片理解”**:识别文字不联网、不需要 AI 服务;而让模型看图并回答属于图片理解,需要配置 AI 服务,走的是另一条路径。
|
||||||
|
|
||||||
|
识别失败的图片可以单独重试,也有“只重建搜索索引、不重新识别图片”的修复入口——修索引不需要重跑几万张图。
|
||||||
|
|
||||||
|
两个索引都可以在“设置 → 本地索引”里集中查看状态、建立、同步和清理。
|
||||||
|
|
||||||
|
## 账号隔离
|
||||||
|
|
||||||
|
每个微信账号使用独立的本地索引。切换账号时,应用不会把一个账号的索引混入另一个账号的搜索结果。
|
||||||
|
|
||||||
|
## 什么时候值得建立
|
||||||
|
|
||||||
|
- 你要跨多个群查过去几个月的内容;
|
||||||
|
- 你反复查同一个项目、客户或主题;
|
||||||
|
- 你希望 AI 先从更稳定的本地资料中找来源;
|
||||||
|
- 你想减少每次搜索都重新读取大量原始记录的等待。
|
||||||
|
|
||||||
|
只偶尔查一条原话时,直接使用档案搜索通常更快。
|
||||||
|
|
||||||
|
## 清理和重建
|
||||||
|
|
||||||
|
在“设置 → 缓存与清理”中可以清理本地知识库索引、检索记录和导出任务缓存。清理索引不会删除微信原始聊天记录或数据库密钥;之后可以回到“问问微信”重新建立。
|
||||||
|
|
||||||
|
## 产品术语(可选)
|
||||||
|
|
||||||
|
源码和日志中可能出现 SQLite、FTS、Chunk、索引等词。它们描述的是本地存储和检索实现,不是你开始使用 TraceMemo 的前置知识。
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# 数据、隐私与安全
|
||||||
|
|
||||||
|
TraceMemo 的核心路径是本地优先,但“本地优先”不等于所有功能都完全离线。是否有数据离开电脑,取决于你是否启用了对应的 AI、Agent 或机器人能力。
|
||||||
|
|
||||||
|
## 默认留在本机的内容
|
||||||
|
|
||||||
|
以下处理由应用在本机完成:
|
||||||
|
|
||||||
|
- 读取和解析微信数据库;
|
||||||
|
- 聊天档案浏览和普通关键词搜索;
|
||||||
|
- 本地 Knowledge 索引及其账号隔离;
|
||||||
|
- 图片文字索引:识别图片中的文字在本机完成,原始图片不会因为本地识别而上传;
|
||||||
|
- 离线语音转写;
|
||||||
|
- 导出文件生成和本地日报历史。
|
||||||
|
|
||||||
|
应用不会因为你打开 TraceMemo 就自动把整份微信数据库上传。
|
||||||
|
|
||||||
|
## 什么时候会请求外部服务
|
||||||
|
|
||||||
|
当你主动使用 AI Search、群聊日报或图片理解,并配置了远程 Provider 时,完成任务所需的内容可能发送给该 Provider。图片文字索引、离线语音转写、档案浏览和普通搜索不会触发这一步。当前设置页给出的边界是:
|
||||||
|
|
||||||
|
- 当前用户问题;
|
||||||
|
- 受控检索所需的有限上下文;
|
||||||
|
- 最终用于总结的 Evidence。
|
||||||
|
|
||||||
|
不会发送完整微信数据库、全量聊天记录、未选中的聊天范围、数据库密钥、内部索引结构或内部会话/消息引用 ID。Provider 的日志、保留、计费和跨境规则不由 TraceMemo 控制,请查看你所选服务商的政策。
|
||||||
|
|
||||||
|
Ollama 等本机 Provider 可以把模型请求留在本机,但本机服务的日志和配置仍由你负责。
|
||||||
|
|
||||||
|
## 语音和媒体
|
||||||
|
|
||||||
|
离线语音转写在本机进行。
|
||||||
|
|
||||||
|
图片有两条完全不同的路径,不要混为一谈:
|
||||||
|
|
||||||
|
- **图片文字索引**:在本机识别图片里的文字,产出的是本地索引数据;原始图片不会因为这一步被上传,也不需要配置 AI 服务。
|
||||||
|
- **图片理解**:属于 AI 功能。只有你主动启用并使用相关报告/分析路径时,图片才可能按该 Provider 的请求规则被处理。
|
||||||
|
|
||||||
|
无法读取的媒体不会被自动“猜出来”。
|
||||||
|
|
||||||
|
## Local HTTP API
|
||||||
|
|
||||||
|
- 默认监听地址为 `127.0.0.1:6131`,不是公网服务;
|
||||||
|
- `/api/v1/health` 为公开健康检查;
|
||||||
|
- 其他端点需要 `Authorization: Bearer <TOKEN>`;
|
||||||
|
- 浏览器 CORS 只允许 HTTP 的 `localhost`、`127.0.0.1` 和 `[::1]` Origin;
|
||||||
|
- 不带 Origin 的本地 CLI/Agent 请求可以使用 Token 访问;
|
||||||
|
- API 不适合直接转发到公网或绑定到不受信任的网络接口。
|
||||||
|
|
||||||
|
Token 由应用生成,使用 Electron `safeStorage` 加密保存在本机 `local-api-token.bin`,文件权限为仅当前用户可读写。你可以在“API Center”中显示、复制或重新生成 Token;重新生成会立即使旧 Token 失效。具体配置见[API 安全](../agent/api-security.md)。
|
||||||
|
|
||||||
|
## Agent 访问时发生什么
|
||||||
|
|
||||||
|
外部 Agent 通过 Reader Skill 调用本机 API,按需读取联系人、会话或时间范围内的聊天;它不会因此获得数据库文件路径或任意文件系统权限。Agent 是否把读取结果再次发送给模型,取决于 Agent 本身及其配置。
|
||||||
|
|
||||||
|
应用内 Agent Hub 是另一条路径:微信机器人通过本机 Hub 调用 TraceMemo,并且可能使用已配置的 AI 来理解问题。请把机器人账号、发送权限和日志视为独立的安全边界。
|
||||||
|
|
||||||
|
机器人收到的文字会先进入本机 Agent Hub;如果任务需要总结或自然语言理解,受控上下文可能发送给你配置的 AI Provider。机器人账号扫码登录、个人微信数据库连接和外部 Agent/API Token 是不同的边界,使用前请分别确认账号与权限。
|
||||||
|
|
||||||
|
## 你可以主动做的事
|
||||||
|
|
||||||
|
- 不要把 API Token 放进 Git、截图、URL 或公开 Skill 文件;
|
||||||
|
- 只连接你有权访问的微信数据;
|
||||||
|
- 对需要外发的 AI 功能逐项确认 Provider;
|
||||||
|
- 定期在“设置 → 缓存与清理”清理不再需要的检索、导出和索引缓存;
|
||||||
|
- 在共享电脑上退出应用并保护系统账户。
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# 生成群聊日报和总结
|
||||||
|
|
||||||
|
如果你每天在多个群里聊天,晚上不想重新翻几十个群,可以让 TraceMemo 根据一个群的聊天内容整理出一份可阅读、可保存的报告。
|
||||||
|
|
||||||
|
## 报告适合做什么
|
||||||
|
|
||||||
|
典型场景包括:
|
||||||
|
|
||||||
|
- 整理今日工作群的讨论重点;
|
||||||
|
- 回顾昨日错过的决定和资源;
|
||||||
|
- 汇总近 7 天的项目进展、待办和未解决问题;
|
||||||
|
- 把群里的图片、语音统计和重要消息放进一张长图或 HTML 页面。
|
||||||
|
|
||||||
|
## 生成步骤
|
||||||
|
|
||||||
|
你可以从两个入口开始:打开一级导航“日报”后新建报告,或者在“档案”中选中一个群聊并点击“生成 AI 日报”。
|
||||||
|
|
||||||
|
1. 选择一个群聊。当前日报入口只支持群聊,不支持单聊。
|
||||||
|
2. 选择时间范围:今日、昨日或近 7 天。
|
||||||
|
3. 按需要选择参与总结的消息类型,先从文字开始最容易核对。
|
||||||
|
4. 选择报告模板/内容模式并开始生成。
|
||||||
|
5. 等待“整理输入 → AI 生成 → HTML/PNG 导出”完成。
|
||||||
|
|
||||||
|
报告可能包含主题、重要消息、问答、资源、待办、未解决事项、关键词、活跃统计,以及可用媒体的精选内容。具体展示内容会随消息类型、资源可用性和模型能力变化。
|
||||||
|
|
||||||
|
## 如何检查报告
|
||||||
|
|
||||||
|
报告中的重点结论会关联来源消息。对于重要决定、金额、时间和责任人,打开对应原消息核对,不要把 AI 生成的摘要当成新的事实来源。
|
||||||
|
|
||||||
|
图片无法读取时,报告可能只保留消息类型和上下文;模型未通过图片理解验证时,图片精选会被跳过。语音在日报中可参与数量和活跃度统计,但不要把统计当成语音内容已经被完整转写。
|
||||||
|
|
||||||
|
## 保存、查看和删除
|
||||||
|
|
||||||
|
生成成功后会保存本地 HTML 与 PNG,并出现在日报历史中。你可以复制图片、打开文件位置或重新生成。删除历史日报只删除本地生成的报告文件,不会影响微信聊天数据库。
|
||||||
|
|
||||||
|
## 定时日报(在自动化里)
|
||||||
|
|
||||||
|
定时日报现在是「自动化」里的一种规则,不再单独占一个页面:打开一级导航的「自动化」,新建或编辑一条「定时日报」规则,选择群聊、执行时间、日报范围、消息类型和发送目标。日报页顶部的指引条也会直接跳到自动化。
|
||||||
|
|
||||||
|
TraceMemo 会按计划执行:
|
||||||
|
|
||||||
|
```text
|
||||||
|
定时触发 → 读取群聊 → 生成报告 → 保存 Report History → 尝试发送
|
||||||
|
```
|
||||||
|
|
||||||
|
生成和发送是两个阶段。当前微信发送能力不可用、未绑定或发送失败时,报告仍会保存,PNG 和执行记录也会保留;这类结果会显示为“已生成,但未发送”或“已生成,发送失败”。
|
||||||
|
|
||||||
|
执行记录支持查看已生成的日报。对“等待发送”或“发送失败”的记录,可以直接重试发送,重试会复用已经生成的 PNG,不会重新调用 AI 生成整份报告;完整执行状态和发送边界见[如何把聊天变成可用的信息](../concepts/how-it-works.md#动作执行与审计)。
|
||||||
|
|
||||||
|
发送目标当前支持**当前群聊、文件传输助手、自己、指定好友**——还不是任意群发。
|
||||||
|
|
||||||
|
## 让报告更可靠
|
||||||
|
|
||||||
|
- 先选正确的群和时间范围;
|
||||||
|
- 不确定时先只选择文字消息;
|
||||||
|
- 群太活跃时分成“今日”和“近 7 天”两次生成;
|
||||||
|
- 看到待办和结论后回到原消息核对上下文;
|
||||||
|
- AI Provider 不可用时先检查模型配置和网络/本地服务状态。
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# 常见问题与排查
|
||||||
|
|
||||||
|
先按现象定位,不要为了“重置”而直接删除微信数据库或整个应用目录。
|
||||||
|
|
||||||
|
## 安装后软件无法打开
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
1. 确认下载的是 GitHub Releases 中的 Windows x64 `-setup.exe`,并已完成安装。
|
||||||
|
2. 安装 [Microsoft Visual C++ x64 运行库](https://aka.ms/vc14/vc_redist.x64.exe)。
|
||||||
|
3. 安装完成后重新启动 TraceMemo;如果仍无响应,再重新运行安装包进行覆盖安装。
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
|
||||||
|
- 确认下载的构建与 Mac 处理器匹配:Apple Silicon(M 系列)用 `arm64`,Intel 用 `x64`。
|
||||||
|
- 提示“无法打开,因为开发者无法验证”时,前往“系统设置 → 隐私与安全性”并点击“仍要打开”。
|
||||||
|
- 提示应用已损坏时,确认应用位于“应用程序”目录,再执行 `xattr -cr "/Applications/TraceMemo.app"`。
|
||||||
|
|
||||||
|
完整安装步骤见[第一次使用 TraceMemo](./getting-started.md#2-安装并启动)。
|
||||||
|
|
||||||
|
## 连接微信失败
|
||||||
|
|
||||||
|
依次检查:
|
||||||
|
|
||||||
|
1. 数据目录是否指向当前登录账号,而不是旧备份或迁移前目录;
|
||||||
|
2. 微信版本是否属于当前代码面向的 4.x 数据结构;
|
||||||
|
3. 微信是否处于页面要求的登录/退出状态;
|
||||||
|
4. macOS 是否完成页面要求的授权;
|
||||||
|
5. 连接页面的诊断项是否明确指出密钥、账号或数据库问题;
|
||||||
|
6. macOS 上 Apple Silicon 与 Intel 使用不同的连接流程,按连接页面提示操作;两者都可以自动获取数据库密钥。
|
||||||
|
|
||||||
|
重新输入密钥或断开连接不会删除微信原始数据库。macOS 的 SIP 和授权说明见[平台说明](../platform/macos.md)。
|
||||||
|
|
||||||
|
仍然失败时,记录 **macOS 版本、CPU 架构(Apple Silicon / Intel)、微信版本、TraceMemo 版本和页面错误提示**后 扫码 README 文档二维码进群提交消息, 或者提交 Issue。
|
||||||
|
|
||||||
|
## 连接成功但没有联系人或消息
|
||||||
|
|
||||||
|
确认账号身份和数据目录匹配。返回“设置 → 账号与数据库”查看数据库连接状态,重新加载会话后再试。若仍为空,记录系统、微信版本和错误提示后提交 Issue。
|
||||||
|
|
||||||
|
## AI 没有结果或回答失败
|
||||||
|
|
||||||
|
- 先在“设置 → AI 模型”测试 Provider;
|
||||||
|
- 检查问题的时间范围和会话范围是否过窄;
|
||||||
|
- 确认 Knowledge 没有正在同步;
|
||||||
|
- 打开检索详情,查看是本地查找为空、Provider 失败还是来源被过滤;
|
||||||
|
- 把问题改成要求“只根据来源原文回答”。
|
||||||
|
|
||||||
|
AI Search 失败时可能仍保留部分来源;不要把部分结果当成完整覆盖。
|
||||||
|
|
||||||
|
## AI 答案看起来不对
|
||||||
|
|
||||||
|
打开来源和原始消息,检查发送者、时间和上下文。若来源不支持结论,扩大或缩小范围后重问。涉及未转写语音、缺失图片、转发和引用时,优先以原消息为准。
|
||||||
|
|
||||||
|
## Knowledge 一直在同步
|
||||||
|
|
||||||
|
首次建立或增量同步会在后台运行。查看“已索引消息、知识片段、磁盘占用”和同步详情;同步期间暂不能开始新的 AI 分析。若出现错误,旧索引可能仍可用,重启应用或在“缓存与清理”清理后重新建立。
|
||||||
|
|
||||||
|
## 语音转写失败
|
||||||
|
|
||||||
|
检查本地模型是否已准备、磁盘空间是否足够、单条语音是否仍有原始资源。批量任务可能部分成功;先处理失败项,不必重复转写已缓存内容。
|
||||||
|
|
||||||
|
## 媒体显示或导出异常
|
||||||
|
|
||||||
|
原图/缩略图目录缺失、权限不足或微信资源已被清理都会导致图片、视频或语音不可用。导出时可以切换缩略图、关闭媒体或保留缺失项,先确认文本档案是否正常。
|
||||||
|
|
||||||
|
文字正常但图片打不开时,进入“设置 → 图片解密”查看状态并尝试自动获取。密钥正确也不能恢复已经被微信清理的原图文件。
|
||||||
|
|
||||||
|
## 日报生成失败
|
||||||
|
|
||||||
|
日报只支持群聊。确认已选择群聊、时间范围内确实有消息、Provider 可用,并尝试先只选择文字消息。图片理解失败不会自动变成图片内容;报告可能跳过图片精选但仍生成文字日报。
|
||||||
|
|
||||||
|
## Agent 无法读取
|
||||||
|
|
||||||
|
确认:
|
||||||
|
|
||||||
|
1. TraceMemo 正在运行且 API Center 显示本地服务在线;
|
||||||
|
2. Agent 使用的是当前 Reader Skill,而不是旧的 MCP 配置;
|
||||||
|
3. 请求地址为 `http://127.0.0.1:6131`;
|
||||||
|
4. 非 health 请求带有最新 `Authorization: Bearer <TOKEN>`;
|
||||||
|
5. Token 重新生成后,Agent 配置已同步更新。
|
||||||
|
|
||||||
|
详细步骤见[Agent 接入概览](../agent/overview.md)和[API 安全](../agent/api-security.md)。
|
||||||
|
|
||||||
|
## 微信机器人无法连接或不回复
|
||||||
|
|
||||||
|
Agent Hub 和外部 Agent 是两条路径。机器人异常时依次确认:
|
||||||
|
|
||||||
|
1. “Agent”页面中的 Agent Hub、微信连接器和数据库状态是否正常;
|
||||||
|
2. 二维码是否过期,手机是否已经确认登录;
|
||||||
|
3. 是否由另一个微信账号向已登录的机器人账号发送文字;
|
||||||
|
4. 请求是否属于当前支持的最近会话、联系人聊天、近 7 天联系人总结、群聊总结或群成员发言总结;
|
||||||
|
5. 需要总结或自然语言理解时,AI Provider 是否可用。
|
||||||
|
|
||||||
|
当前机器人不支持群发、定时任务或与文字同等的图片、语音、文件和视频理解。详细边界见[Agent Hub](../agent/agent-hub.md)。
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# 语音转文字
|
||||||
|
|
||||||
|
TraceMemo 可以把微信语音转换成可搜索的文字,适合你不想逐条播放、希望把语音内容带入后续查找或导出的场景。
|
||||||
|
|
||||||
|
## 使用前准备
|
||||||
|
|
||||||
|
1. 打开“设置 → 语音识别”。
|
||||||
|
2. 按页面提示准备或下载本地语音模型。
|
||||||
|
3. 等待模型状态显示可用。
|
||||||
|
|
||||||
|
语音识别使用本地 SenseVoice/sherpa-onnx 运行时。首次准备模型可能需要下载文件和占用额外磁盘空间;模型文件可以从设置中删除,之后需要重新准备。
|
||||||
|
|
||||||
|
## 转写单条语音
|
||||||
|
|
||||||
|
在聊天档案中找到语音消息,点击转写入口。完成后,转写文本会与该消息关联,并可用于后续查看或检索。失败时查看消息提示和模型状态。
|
||||||
|
|
||||||
|
## 批量转写
|
||||||
|
|
||||||
|
在语音设置中选择联系人或群聊,再选择范围:
|
||||||
|
|
||||||
|
- 最近 30 天;
|
||||||
|
- 当前年份;
|
||||||
|
- 选择的历史范围。
|
||||||
|
|
||||||
|
开始前页面会显示语音条数、已缓存数量、待处理数量和预计耗时。批量任务支持进度、取消、缓存复用,并可能以“部分失败”结束;部分失败时可以根据列表重新处理未成功内容。
|
||||||
|
|
||||||
|
## 和 AI、知识库、导出的关系
|
||||||
|
|
||||||
|
- 本地转写结果可以参与本地知识库检索;
|
||||||
|
- 导出时可选择是否包含已有语音转写;
|
||||||
|
- AI Search 可能提示某些语音尚未转写,这意味着答案覆盖不完整;
|
||||||
|
- 群聊日报默认会统计语音数量和时长,但不等于已经理解了每条语音的具体内容。
|
||||||
|
|
||||||
|
## 隐私提示
|
||||||
|
|
||||||
|
离线转写本身在本机完成。若你主动把转写结果用于 AI Search、日报或其他 AI 功能,受控文本可能按对应功能的规则发送给你配置的 Provider;详见[数据、隐私与安全](./privacy.md)。
|
||||||
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
extends: ./electron-builder.yml
|
||||||
|
extraResources:
|
||||||
|
- from: build/app-update.yml
|
||||||
|
to: app-update.yml
|
||||||
|
- from: resources
|
||||||
|
to: resources
|
||||||
|
filter:
|
||||||
|
- '**/*'
|
||||||
|
- from: docs/skill/tracememo-reader
|
||||||
|
to: skill/tracememo-reader
|
||||||
|
filter:
|
||||||
|
- '**/*'
|
||||||
|
mac:
|
||||||
|
target:
|
||||||
|
- dmg
|
||||||
|
- zip
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
appId: com.tracememo.app
|
||||||
|
productName: TraceMemo
|
||||||
|
afterPack: scripts/after-pack.cjs
|
||||||
|
directories:
|
||||||
|
buildResources: build
|
||||||
|
files:
|
||||||
|
- out
|
||||||
|
- '!**/.vscode/*'
|
||||||
|
- '!src/*'
|
||||||
|
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
||||||
|
- '!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
|
||||||
|
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
||||||
|
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
||||||
|
extraMetadata:
|
||||||
|
main: out/main/index.js
|
||||||
|
asarUnpack:
|
||||||
|
- resources/**
|
||||||
|
- node_modules/ffmpeg-static/**
|
||||||
|
- node_modules/silk-wasm/**
|
||||||
|
- node_modules/sherpa-onnx-node/**
|
||||||
|
- node_modules/sherpa-onnx-*/**
|
||||||
|
- node_modules/@napi-rs/system-ocr/**
|
||||||
|
- node_modules/@napi-rs/system-ocr-*/**
|
||||||
|
extraResources:
|
||||||
|
# Keep the updater provider in every packaged Windows app. electron-builder also
|
||||||
|
# regenerates this file during publish, using the same release configuration.
|
||||||
|
- from: build/app-update.yml
|
||||||
|
to: app-update.yml
|
||||||
|
# Keep only resources required by the Windows x64 runtime.
|
||||||
|
- from: resources
|
||||||
|
to: resources
|
||||||
|
filter:
|
||||||
|
- icon.png
|
||||||
|
- daily_report_templates.html
|
||||||
|
- mobile_daily_report.html
|
||||||
|
- mobile_daily_report_v1.html
|
||||||
|
- mobile_daily_report_v2.html
|
||||||
|
- key/win32/x64/**
|
||||||
|
- runtime/win32/**
|
||||||
|
- wcdb/win32/x64/**
|
||||||
|
- from: docs/skill/tracememo-reader
|
||||||
|
to: skill/tracememo-reader
|
||||||
|
filter:
|
||||||
|
- '**/*'
|
||||||
|
win:
|
||||||
|
icon: icon.ico
|
||||||
|
executableName: TraceMemo
|
||||||
|
target:
|
||||||
|
- target: nsis
|
||||||
|
arch:
|
||||||
|
- x64
|
||||||
|
nsis:
|
||||||
|
oneClick: false
|
||||||
|
allowToChangeInstallationDirectory: true
|
||||||
|
artifactName: ${name}-${version}-setup.${ext}
|
||||||
|
shortcutName: ${productName}
|
||||||
|
uninstallDisplayName: ${productName}
|
||||||
|
createDesktopShortcut: always
|
||||||
|
npmRebuild: false
|
||||||
|
electronLanguages:
|
||||||
|
- zh-CN
|
||||||
|
- en-US
|
||||||
|
publish:
|
||||||
|
provider: github
|
||||||
|
owner: Wxw-Gu
|
||||||
|
repo: TraceMemo
|
||||||
|
# 一律先上传为草稿 再到 GitHub 上手动 Publish。
|
||||||
|
# 需要预发布时用 `pnpm release:beta`(EP_PRE_RELEASE 会覆盖这里的 draft)。
|
||||||
|
releaseType: draft
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
appId: com.electron.app
|
appId: com.tracememo.app
|
||||||
productName: wechatexplorer
|
productName: TraceMemo
|
||||||
|
afterPack: scripts/after-pack.cjs
|
||||||
directories:
|
directories:
|
||||||
buildResources: build
|
buildResources: build
|
||||||
files:
|
files:
|
||||||
@@ -14,24 +15,54 @@ extraMetadata:
|
|||||||
main: out/main/index.js
|
main: out/main/index.js
|
||||||
asarUnpack:
|
asarUnpack:
|
||||||
- resources/**
|
- resources/**
|
||||||
win:
|
- node_modules/ffmpeg-static/**
|
||||||
executableName: wechatexplorer
|
- node_modules/silk-wasm/**
|
||||||
|
- node_modules/sherpa-onnx-node/**
|
||||||
|
- node_modules/sherpa-onnx-*/**
|
||||||
|
# System OCR(@napi-rs/system-ocr)的 native binding 必须 unpacked,否则
|
||||||
|
# macOS 的系统 OCR 会在运行时 MODULE_NOT_FOUND。平台本机的 binding 由
|
||||||
|
# scripts/after-pack.cjs 校验,外架构的同级包在 afterPack 里被裁掉。
|
||||||
|
- node_modules/@napi-rs/system-ocr/**
|
||||||
|
- node_modules/@napi-rs/system-ocr-*/**
|
||||||
|
extraResources:
|
||||||
|
# Keep the updater provider in every packaged macOS app. electron-builder also
|
||||||
|
# regenerates this file during publish, using the same release configuration.
|
||||||
|
- from: build/app-update.yml
|
||||||
|
to: app-update.yml
|
||||||
|
# Includes the optional WeChat connector binary for the target platform.
|
||||||
|
- from: resources
|
||||||
|
to: resources
|
||||||
|
filter:
|
||||||
|
- '**/*'
|
||||||
|
- '!runtime/darwin-arm64/**'
|
||||||
|
- from: docs/skill/tracememo-reader
|
||||||
|
to: skill/tracememo-reader
|
||||||
|
filter:
|
||||||
|
- '**/*'
|
||||||
nsis:
|
nsis:
|
||||||
|
oneClick: false
|
||||||
|
allowToChangeInstallationDirectory: true
|
||||||
artifactName: ${name}-${version}-setup.${ext}
|
artifactName: ${name}-${version}-setup.${ext}
|
||||||
shortcutName: ${productName}
|
shortcutName: ${productName}
|
||||||
uninstallDisplayName: ${productName}
|
uninstallDisplayName: ${productName}
|
||||||
createDesktopShortcut: always
|
createDesktopShortcut: always
|
||||||
mac:
|
mac:
|
||||||
|
icon: icon.icns
|
||||||
|
target:
|
||||||
|
- dmg
|
||||||
|
- zip
|
||||||
entitlementsInherit: build/entitlements.mac.plist
|
entitlementsInherit: build/entitlements.mac.plist
|
||||||
extendInfo:
|
extendInfo:
|
||||||
- NSCameraUsageDescription: Application requests access to the device's camera.
|
CFBundleDisplayName: TraceMemo
|
||||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
NSCameraUsageDescription: Application requests access to the device's camera.
|
||||||
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||||
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
||||||
|
NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||||
notarize: false
|
notarize: false
|
||||||
dmg:
|
dmg:
|
||||||
artifactName: ${name}-${version}.${ext}
|
artifactName: ${name}-${version}-${arch}.${ext}
|
||||||
linux:
|
linux:
|
||||||
|
icon: icon.png
|
||||||
target:
|
target:
|
||||||
- AppImage
|
- AppImage
|
||||||
- snap
|
- snap
|
||||||
@@ -42,5 +73,9 @@ appImage:
|
|||||||
artifactName: ${name}-${version}.${ext}
|
artifactName: ${name}-${version}.${ext}
|
||||||
npmRebuild: false
|
npmRebuild: false
|
||||||
publish:
|
publish:
|
||||||
provider: generic
|
provider: github
|
||||||
url: https://example.com/auto-updates
|
owner: Wxw-Gu
|
||||||
|
repo: TraceMemo
|
||||||
|
# 一律先上传为草稿 再到 GitHub 上手动 Publish。
|
||||||
|
# 需要预发布时用 `pnpm release:beta`(EP_PRE_RELEASE 会覆盖这里的 draft)。
|
||||||
|
releaseType: draft
|
||||||
|
|||||||
@@ -3,7 +3,23 @@ import { defineConfig } from 'electron-vite'
|
|||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
main: {},
|
main: {
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
index: resolve('src/main/index.ts'),
|
||||||
|
reportTemplateTest: resolve('src/main/report-template-test-entry.ts'),
|
||||||
|
queryAgentPoc: resolve('src/main/query-agent-poc-entry.ts'),
|
||||||
|
voiceRecognitionWorker: resolve('src/main/voice-pipeline/voice-recognition-worker.ts'),
|
||||||
|
knowledgeWorker: resolve('src/main/knowledge/knowledge-worker.ts')
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
entryFileNames: '[name].js'
|
||||||
|
},
|
||||||
|
external: ['koffi', 'sherpa-onnx-node', '@napi-rs/system-ocr']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
preload: {},
|
preload: {},
|
||||||
renderer: {
|
renderer: {
|
||||||
resolve: {
|
resolve: {
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ export default defineConfig(
|
|||||||
'react-refresh': eslintPluginReactRefresh
|
'react-refresh': eslintPluginReactRefresh
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
|
'prettier/prettier': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
endOfLine: 'auto'
|
||||||
|
}
|
||||||
|
],
|
||||||
...eslintPluginReactHooks.configs.recommended.rules,
|
...eslintPluginReactHooks.configs.recommended.rules,
|
||||||
...eslintPluginReactRefresh.configs.vite.rules
|
...eslintPluginReactRefresh.configs.vite.rules
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# TraceMemo 日报模板示例
|
||||||
|
|
||||||
|
此目录是可安装的最小日报模板源文件,使用全部虚构数据进行预览。运行 `node scripts/build-report-template-example.cjs` 会生成 `examples/report-template-basic.zip`。
|
||||||
|
|
||||||
|
模板只能调整已有日报模块的 HTML/CSS 排版。作者不能加入 JavaScript、事件属性、外部网络资源、嵌套页面、数据库访问、AI Prompt 或新的业务分析。
|
||||||
|
|
||||||
|
占位符分为三类:普通文本(会被 HTML 转义)、应用生成的 HTML 片段(只能作为元素内容使用)、受限样式类(只能放进 `class` 属性,并由应用输出合法 token)。`*_MORE_NOTE` 是 HTML 片段,不是样式类。
|
||||||
|
|
||||||
|
允许的标签和属性由 `src/shared/report-template-package.ts` 统一定义;图片资源只能是包内 `assets/` 下的 PNG/JPEG/WebP。缺少可选模块时应用输出空字符串和 `*_EMPTY_CLASS`,模板应允许该模块隐藏。
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"protocolVersion": "1.0",
|
||||||
|
"kind": "daily-report",
|
||||||
|
"id": "community.github.example.basic-feed",
|
||||||
|
"name": "基础信息流",
|
||||||
|
"author": { "name": "example" },
|
||||||
|
"templateVersion": "1.0.0",
|
||||||
|
"interfaceVersion": "1",
|
||||||
|
"entry": "template.html",
|
||||||
|
"preview": "preview.png",
|
||||||
|
"capture": { "width": 430, "maxWidth": 430, "maxHeight": 20000 },
|
||||||
|
"license": { "spdx": "MIT" },
|
||||||
|
"platform": "mobile"
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<title>{{REPORT_TITLE}}</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html, body { margin: 0; width: 430px; background: #f3f5f7; color: #18202a; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
|
||||||
|
.report { padding: 18px 14px 32px; }
|
||||||
|
.hero, .section { margin-top: 10px; padding: 16px; border-radius: 14px; background: #fff; }
|
||||||
|
.hero { margin-top: 0; border-top: 4px solid #1769aa; }
|
||||||
|
h1 { margin: 0 0 8px; font-size: 23px; }
|
||||||
|
.meta, .muted { color: #64748b; font-size: 12px; line-height: 1.5; }
|
||||||
|
.section-title { margin: 0 0 8px; font-size: 14px; }
|
||||||
|
.topics-grid, .important-list { display: grid; gap: 8px; }
|
||||||
|
.topic-card, .important-card { padding: 10px; border: 1px solid #dbe4ee; border-radius: 10px; }
|
||||||
|
.empty-section { display: none !important; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="report {{REPORT_MODE_CLASS}}">
|
||||||
|
<section class="hero {{REPORT_MODE_CLASS}}">
|
||||||
|
<h1>{{REPORT_TITLE}}</h1>
|
||||||
|
<div class="meta">{{REPORT_DATE}} · {{DATE_RANGE}}</div>
|
||||||
|
<p>{{HERO_SUMMARY}}</p>
|
||||||
|
<div class="muted">{{HERO_STATUS_LINE}}</div>
|
||||||
|
</section>
|
||||||
|
<section class="section {{TOPICS_EMPTY_CLASS}}">
|
||||||
|
<h2 class="section-title">今日话题</h2>
|
||||||
|
<div class="topics-grid">{{TOPIC_CARDS}}</div>
|
||||||
|
{{TOPICS_MORE_NOTE}}
|
||||||
|
</section>
|
||||||
|
<section class="section {{MESSAGES_EMPTY_CLASS}}">
|
||||||
|
<h2 class="section-title">重要消息</h2>
|
||||||
|
<div class="important-list">{{IMPORTANT_MESSAGES}}</div>
|
||||||
|
</section>
|
||||||
|
<footer class="muted">{{FOOTER_NOTE}}</footer>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
"use strict";
|
||||||
|
const electron = require("electron");
|
||||||
|
const preload = require("@electron-toolkit/preload");
|
||||||
|
const api = {
|
||||||
|
writeAppLog: (entry) => electron.ipcRenderer.invoke("app-log:write", entry),
|
||||||
|
getAppLogPath: () => electron.ipcRenderer.invoke("app-log:getPath"),
|
||||||
|
revealAppLog: () => electron.ipcRenderer.invoke("app-log:reveal"),
|
||||||
|
getAppUpdateState: () => electron.ipcRenderer.invoke("app-update:getState"),
|
||||||
|
checkAppUpdate: () => electron.ipcRenderer.invoke("app-update:check"),
|
||||||
|
downloadAppUpdate: () => electron.ipcRenderer.invoke("app-update:download"),
|
||||||
|
installAppUpdate: () => electron.ipcRenderer.invoke("app-update:install"),
|
||||||
|
onAppUpdateState: (callback) => {
|
||||||
|
const listener = (_event, state) => callback(state);
|
||||||
|
electron.ipcRenderer.on("app-update:state", listener);
|
||||||
|
return () => electron.ipcRenderer.removeListener("app-update:state", listener);
|
||||||
|
},
|
||||||
|
getCacheSummary: () => electron.ipcRenderer.invoke("cache:getSummary"),
|
||||||
|
clearCache: (scope) => electron.ipcRenderer.invoke("cache:clear", scope),
|
||||||
|
initDb: (key) => electron.ipcRenderer.invoke("db:init", key),
|
||||||
|
getBootstrapCache: () => electron.ipcRenderer.invoke("db:getBootstrapCache"),
|
||||||
|
getStartupCache: () => electron.ipcRenderer.invoke("db:getStartupCache"),
|
||||||
|
getContacts: (filter) => electron.ipcRenderer.invoke("db:getContacts", filter),
|
||||||
|
getContactAvatars: (usernames) => electron.ipcRenderer.invoke("db:getContactAvatars", usernames),
|
||||||
|
getCachedMessages: (userMd5, startTime, endTime) => electron.ipcRenderer.invoke("db:getCachedMessages", userMd5, startTime, endTime),
|
||||||
|
getCachedMessagePage: (userMd5, startTime, endTime) => electron.ipcRenderer.invoke("db:getCachedMessagePage", userMd5, startTime, endTime),
|
||||||
|
getMessages: (userMd5, startTime, endTime, options) => electron.ipcRenderer.invoke("db:getMessages", userMd5, startTime, endTime, options),
|
||||||
|
getGroupSnapshot: (userMd5) => electron.ipcRenderer.invoke("db:getGroupSnapshot", userMd5),
|
||||||
|
search: (keyword) => electron.ipcRenderer.invoke("db:search", keyword),
|
||||||
|
aiChat: (messages, options) => electron.ipcRenderer.invoke("ai:chat", messages, options),
|
||||||
|
listAIProviders: () => electron.ipcRenderer.invoke("ai:listProviders"),
|
||||||
|
getAIRuntimeConfig: () => electron.ipcRenderer.invoke("ai:getRuntimeConfig"),
|
||||||
|
saveAIProvider: (provider) => electron.ipcRenderer.invoke("ai:saveProvider", provider),
|
||||||
|
deleteAIProvider: (providerId) => electron.ipcRenderer.invoke("ai:deleteProvider", providerId),
|
||||||
|
setDefaultAIProvider: (providerId) => electron.ipcRenderer.invoke("ai:setDefaultProvider", providerId),
|
||||||
|
testAIProvider: (providerId) => electron.ipcRenderer.invoke("ai:testProvider", providerId),
|
||||||
|
testAIVision: (request) => electron.ipcRenderer.invoke("ai:testVision", request),
|
||||||
|
migrateLegacyAIConfig: (config) => electron.ipcRenderer.invoke("ai:migrateLegacy", config),
|
||||||
|
copyImage: (base64String) => electron.ipcRenderer.invoke("copy-image", base64String),
|
||||||
|
getVoiceData: (sessionId, localId, createTime, svrId) => electron.ipcRenderer.invoke("db:getVoiceData", sessionId, localId, createTime, svrId),
|
||||||
|
parseMessage: (content, messageType) => electron.ipcRenderer.invoke("db:parseMessage", content, messageType),
|
||||||
|
getImage: (imageMd5, imageDatNameOrThumb, sessionId, options) => electron.ipcRenderer.invoke("db:getImage", imageMd5, imageDatNameOrThumb, sessionId, options),
|
||||||
|
getVideo: (hashes) => electron.ipcRenderer.invoke("db:getVideo", hashes),
|
||||||
|
getSticker: (cdnUrl, md5) => electron.ipcRenderer.invoke("db:getSticker", cdnUrl, md5),
|
||||||
|
startExport: (request) => electron.ipcRenderer.invoke("export:start", request),
|
||||||
|
cancelExport: (jobId) => electron.ipcRenderer.invoke("export:cancel", jobId),
|
||||||
|
revealExport: (path) => electron.ipcRenderer.invoke("export:reveal", path),
|
||||||
|
selectExportDirectory: () => electron.ipcRenderer.invoke("export:selectDirectory"),
|
||||||
|
onExportProgress: (callback) => {
|
||||||
|
const listener = (_event, progress) => callback(progress);
|
||||||
|
electron.ipcRenderer.on("export:progress", listener);
|
||||||
|
return () => electron.ipcRenderer.removeListener("export:progress", listener);
|
||||||
|
},
|
||||||
|
exportGroupReport: (request) => electron.ipcRenderer.invoke("report:export", request),
|
||||||
|
listGeneratedReports: () => electron.ipcRenderer.invoke("report:listGenerated"),
|
||||||
|
saveGeneratedReport: (request) => electron.ipcRenderer.invoke("report:saveGenerated", request),
|
||||||
|
deleteGeneratedReport: (reportId) => electron.ipcRenderer.invoke("report:deleteGenerated", reportId),
|
||||||
|
revealGroupReport: (filePath) => electron.ipcRenderer.invoke("report:reveal", filePath),
|
||||||
|
getSavedDbKey: () => electron.ipcRenderer.invoke("key:getSavedDbKey"),
|
||||||
|
getDatabaseKeyEnvironment: () => electron.ipcRenderer.invoke("key:getEnvironment"),
|
||||||
|
readDatabaseKeyClipboard: () => electron.ipcRenderer.invoke("key:readClipboardDbKey"),
|
||||||
|
autoGetDbKey: (options) => electron.ipcRenderer.invoke("key:autoGetDbKey", options),
|
||||||
|
autoGetImageKey: (options) => electron.ipcRenderer.invoke("key:autoGetImageKey", options),
|
||||||
|
getImageKeyConfig: () => electron.ipcRenderer.invoke("image:getConfig"),
|
||||||
|
getImageDecryptionStatus: () => electron.ipcRenderer.invoke("image:getStatus"),
|
||||||
|
saveImageKeyConfig: (request) => electron.ipcRenderer.invoke("image:saveConfig", request),
|
||||||
|
testImageDecryption: (request) => electron.ipcRenderer.invoke("image:testConfig", request),
|
||||||
|
clearImageKeyConfig: () => electron.ipcRenderer.invoke("image:clearConfig"),
|
||||||
|
pasteAndSaveDbKey: () => electron.ipcRenderer.invoke("key:pasteAndSaveDbKey"),
|
||||||
|
saveDbKey: (key) => electron.ipcRenderer.invoke("key:saveDbKey", key),
|
||||||
|
clearSavedDbKey: () => electron.ipcRenderer.invoke("key:clearSavedDbKey"),
|
||||||
|
onWcdbChange: (callback) => {
|
||||||
|
const listener = (_event, payload) => callback(payload);
|
||||||
|
electron.ipcRenderer.on("wcdb-change", listener);
|
||||||
|
return () => electron.ipcRenderer.removeListener("wcdb-change", listener);
|
||||||
|
},
|
||||||
|
onDbKeyStatus: (callback) => {
|
||||||
|
const listener = (_event, payload) => callback(payload);
|
||||||
|
electron.ipcRenderer.on("key:dbKeyStatus", listener);
|
||||||
|
return () => electron.ipcRenderer.removeListener("key:dbKeyStatus", listener);
|
||||||
|
},
|
||||||
|
onImageKeyStatus: (callback) => {
|
||||||
|
const listener = (_event, payload) => callback(payload);
|
||||||
|
electron.ipcRenderer.on("key:imageKeyStatus", listener);
|
||||||
|
return () => electron.ipcRenderer.removeListener("key:imageKeyStatus", listener);
|
||||||
|
},
|
||||||
|
getSettings: () => electron.ipcRenderer.invoke("settings:get"),
|
||||||
|
setSettings: (patch) => electron.ipcRenderer.invoke("settings:set", patch),
|
||||||
|
getSelf: () => electron.ipcRenderer.invoke("settings:getSelf"),
|
||||||
|
testConnection: (key, accountRoot) => electron.ipcRenderer.invoke("db:testConnection", key, accountRoot),
|
||||||
|
reopenWithRoot: (accountRoot) => electron.ipcRenderer.invoke("db:reopenWithRoot", accountRoot),
|
||||||
|
selectDbRoot: () => electron.ipcRenderer.invoke("settings:selectDbRoot"),
|
||||||
|
openAccountRoot: () => electron.ipcRenderer.invoke("settings:openAccountRoot"),
|
||||||
|
disconnectDb: (options) => electron.ipcRenderer.invoke("db:disconnect", options),
|
||||||
|
apiStatus: () => electron.ipcRenderer.invoke("api:getStatus"),
|
||||||
|
apiStart: (host, port) => electron.ipcRenderer.invoke("api:start", host, port),
|
||||||
|
apiStop: () => electron.ipcRenderer.invoke("api:stop"),
|
||||||
|
apiToggle: (enabled) => electron.ipcRenderer.invoke("api:toggle", enabled),
|
||||||
|
getReaderSkillStatus: () => electron.ipcRenderer.invoke("api:skillStatus"),
|
||||||
|
readReaderSkill: () => electron.ipcRenderer.invoke("api:readSkill"),
|
||||||
|
revealReaderSkill: () => electron.ipcRenderer.invoke("api:revealSkill"),
|
||||||
|
openReaderSkillGithub: () => electron.ipcRenderer.invoke("api:openSkillGithub"),
|
||||||
|
testLocalApiRequest: (request) => electron.ipcRenderer.invoke("api:testLocalRequest", request),
|
||||||
|
copyText: (text) => electron.ipcRenderer.invoke("api:copyText", text),
|
||||||
|
// ============================================================
|
||||||
|
// AI 图片理解基础设施(ImageInsightService)
|
||||||
|
// ============================================================
|
||||||
|
imageListCandidates: (query) => electron.ipcRenderer.invoke("image:listCandidates", query),
|
||||||
|
imageAnalyze: (request) => electron.ipcRenderer.invoke("image:analyze", request),
|
||||||
|
getImageInsight: (imageHash) => electron.ipcRenderer.invoke("image:getInsight", imageHash),
|
||||||
|
listImageInsights: (sessionId, limit) => electron.ipcRenderer.invoke("image:listInsights", sessionId, limit),
|
||||||
|
getAgentHubStatus: () => electron.ipcRenderer.invoke("agent-hub:getStatus"),
|
||||||
|
getAgentHubLogs: () => electron.ipcRenderer.invoke("agent-hub:getLogs"),
|
||||||
|
clearAgentHubLogs: () => electron.ipcRenderer.invoke("agent-hub:clearLogs"),
|
||||||
|
startAgentHubLogin: () => electron.ipcRenderer.invoke("agent-hub:startLogin"),
|
||||||
|
cancelAgentHubLogin: () => electron.ipcRenderer.invoke("agent-hub:cancelLogin"),
|
||||||
|
reconnectAgentHub: () => electron.ipcRenderer.invoke("agent-hub:reconnect"),
|
||||||
|
disconnectAgentHub: () => electron.ipcRenderer.invoke("agent-hub:disconnect"),
|
||||||
|
selectAgentHubTestImage: () => electron.ipcRenderer.invoke("agent-hub:selectTestImage"),
|
||||||
|
onAgentHubStatus: (callback) => {
|
||||||
|
const listener = (_event, status) => callback(status);
|
||||||
|
electron.ipcRenderer.on("agent-hub:status", listener);
|
||||||
|
return () => electron.ipcRenderer.removeListener("agent-hub:status", listener);
|
||||||
|
},
|
||||||
|
onAgentHubLog: (callback) => {
|
||||||
|
const listener = (_event, entry) => callback(entry);
|
||||||
|
electron.ipcRenderer.on("agent-hub:log", listener);
|
||||||
|
return () => electron.ipcRenderer.removeListener("agent-hub:log", listener);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (process.contextIsolated) {
|
||||||
|
try {
|
||||||
|
electron.contextBridge.exposeInMainWorld("electron", preload.electronAPI);
|
||||||
|
electron.contextBridge.exposeInMainWorld("api", api);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
window.electron = preload.electronAPI;
|
||||||
|
window.api = api;
|
||||||
|
}
|
||||||
@@ -1,59 +1,177 @@
|
|||||||
{
|
{
|
||||||
"name": "wechatexplorer",
|
"name": "tracememo",
|
||||||
"version": "1.0.0",
|
"version": "2.5.0",
|
||||||
"description": "mac 版本获取微信聊天记录, AI群聊总结助手",
|
"packageManager": "pnpm@7.33.7",
|
||||||
"keywords": ["wechat", "chat", "mac微信", "微信聊天记录", "AI群聊总结助手"],
|
"description": "TraceMemo(迹忆)是一款本地优先、可追溯的 AI 微信知识与分析工作台。 原名 WechatExplorer,支持聊天记录搜索、知识库、微信群聊总结和 Agent 助手。",
|
||||||
|
"keywords": [
|
||||||
|
"wechat",
|
||||||
|
"wechat chat",
|
||||||
|
"wechat history",
|
||||||
|
"mac微信",
|
||||||
|
"windows微信",
|
||||||
|
"微信聊天记录",
|
||||||
|
"微信聊天记录搜索",
|
||||||
|
"微信群聊总结",
|
||||||
|
"微信AI",
|
||||||
|
"微信机器人",
|
||||||
|
"AI聊天搜索",
|
||||||
|
"AI群聊总结",
|
||||||
|
"本地AI"
|
||||||
|
],
|
||||||
"author": "Qingmao",
|
"author": "Qingmao",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Wxw-Gu/TraceMemo.git"
|
||||||
|
},
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"test": "pnpm typecheck && pnpm test:unit && pnpm test:component && pnpm test:integration && pnpm test:skill-install && pnpm test:e2e:build && playwright test",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"lint": "eslint --cache .",
|
"lint": "eslint --cache .",
|
||||||
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
||||||
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
|
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
|
||||||
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
"typecheck": "npm run typecheck:node && npm run typecheck:web && npm run typecheck:test",
|
||||||
|
"typecheck:test": "node scripts/typecheck-tests.cjs",
|
||||||
|
"test:skill-install": "node scripts/test-skill-install-instruction.cjs",
|
||||||
|
"cp:env": "node scripts/ensure-env.cjs",
|
||||||
|
"prepare:env": "node scripts/ensure-env.cjs",
|
||||||
|
"prepare:ffmpeg:win": "node scripts/prepare-electron-runtime.cjs --platform win32 --arch x64",
|
||||||
|
"prepare:ffmpeg:mac:arm64": "node scripts/prepare-electron-runtime.cjs --platform darwin --arch arm64",
|
||||||
|
"prepare:ffmpeg:mac:x64": "node scripts/prepare-electron-runtime.cjs --platform darwin --arch x64",
|
||||||
|
"prepare:win-runtime": "node scripts/prepare-win-runtime.cjs && npm run prepare:ffmpeg:win",
|
||||||
|
"prepare:wechat-native": "node scripts/prepare-wechat-native-runtime.cjs",
|
||||||
"start": "electron-vite preview",
|
"start": "electron-vite preview",
|
||||||
"dev": "electron-vite dev",
|
"predev": "node scripts/ensure-electron-binary.cjs",
|
||||||
|
"dev": "node scripts/ensure-env.cjs && electron-vite dev",
|
||||||
|
"dev:update": "cross-env TRACEMEMO_UPDATE_SIMULATION=true pnpm dev",
|
||||||
|
"poc:query-agent": "electron-vite build && node scripts/run-query-agent-poc.cjs",
|
||||||
|
"poc:query-agent:run": "node scripts/run-query-agent-poc.cjs",
|
||||||
|
"test:unit": "vitest run --config vitest.unit.config.ts",
|
||||||
|
"test:component": "vitest run --config vitest.component.config.ts",
|
||||||
|
"test:integration": "vitest run --config vitest.integration.config.ts",
|
||||||
|
"benchmark:knowledge": "vitest run --config vitest.knowledge-benchmark.config.ts --reporter=verbose",
|
||||||
|
"benchmark:knowledge:capacity": "cross-env KNOWLEDGE_CAPACITY=1 vitest run --config vitest.knowledge-benchmark.config.ts --reporter=verbose",
|
||||||
|
"test:e2e:build": "electron-vite build",
|
||||||
|
"test:knowledge-worker": "pnpm test:e2e:build && node scripts/test-knowledge-worker.cjs",
|
||||||
|
"test:e2e": "pnpm test:e2e:build && playwright test --grep-invert @visual",
|
||||||
|
"test:visual": "pnpm test:e2e:build && playwright test tests/e2e/visual.spec.ts",
|
||||||
|
"test:smoke": "node --test tests/smoke/native-environment.test.mjs",
|
||||||
"build": "npm run typecheck && electron-vite build",
|
"build": "npm run typecheck && electron-vite build",
|
||||||
"postinstall": "electron-builder install-app-deps",
|
"postinstall": "electron-builder install-app-deps && node scripts/prepare-electron-runtime.cjs && node scripts/ensure-electron-binary.cjs",
|
||||||
"build:unpack": "npm run build && electron-builder --dir",
|
"build:unpack": "npm run build && electron-builder --config electron-builder.yml --dir",
|
||||||
"build:win": "npm run build && electron-builder --win",
|
"build:win": "npm run typecheck && npm run prepare:win-runtime && electron-vite build && electron-builder --config electron-builder.win.yml --win --x64",
|
||||||
"build:mac": "electron-vite build && electron-builder --mac",
|
"build:mac:arm64": "npm run typecheck && npm run prepare:ffmpeg:mac:arm64 && electron-vite build && electron-builder --config electron-builder.yml --mac --arm64",
|
||||||
"build:linux": "electron-vite build && electron-builder --linux"
|
"build:mac:arm64:send-runtime": "npm run typecheck && npm run prepare:ffmpeg:mac:arm64 && electron-vite build && cross-env TM_SEND_RUNTIME_BUILD=1 electron-builder --config electron-builder.send-runtime.yml --mac --arm64",
|
||||||
|
"build:mac:x64": "npm run typecheck && npm run prepare:ffmpeg:mac:x64 && electron-vite build && electron-builder --config electron-builder.yml --mac --x64",
|
||||||
|
"release": "npm run release:mac && npm run release:win",
|
||||||
|
"release:mac": "npm run typecheck && electron-vite build && npm run release:mac:arm64 && npm run release:mac:x64",
|
||||||
|
"release:mac:arm64": "npm run prepare:ffmpeg:mac:arm64 && electron-builder --config electron-builder.yml --mac --arm64 --publish always",
|
||||||
|
"release:mac:x64": "npm run prepare:ffmpeg:mac:x64 && electron-builder --config electron-builder.yml --mac --x64 --publish always",
|
||||||
|
"release:win": "npm run typecheck && npm run prepare:win-runtime && electron-vite build && electron-builder --config electron-builder.win.yml --win --x64 --publish always",
|
||||||
|
"release:beta": "cross-env EP_PRE_RELEASE=true npm run release",
|
||||||
|
"release:stable": "npm run release",
|
||||||
|
"build:linux": "electron-vite build && electron-builder --config electron-builder.yml --linux"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron-toolkit/preload": "^3.0.2",
|
"@electron-toolkit/preload": "^3.0.2",
|
||||||
"@electron-toolkit/utils": "^4.0.0",
|
"@electron-toolkit/utils": "^4.0.0",
|
||||||
"better-sqlite3-multiple-ciphers": "^12.5.0",
|
"@koromix/koffi-darwin-x64": "3.1.0",
|
||||||
|
"@koromix/koffi-win32-x64": "3.1.0",
|
||||||
|
"@napi-rs/system-ocr": "1.2.0",
|
||||||
|
"@napi-rs/system-ocr-darwin-x64": "1.2.0",
|
||||||
|
"@napi-rs/system-ocr-win32-x64-msvc": "1.2.0",
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.1.23",
|
||||||
|
"@radix-ui/react-checkbox": "^1.3.11",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.23",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.24",
|
||||||
|
"@radix-ui/react-popover": "^1.1.23",
|
||||||
|
"@radix-ui/react-progress": "^1.1.16",
|
||||||
|
"@radix-ui/react-radio-group": "^1.4.7",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.18",
|
||||||
|
"@radix-ui/react-select": "^2.3.7",
|
||||||
|
"@radix-ui/react-separator": "^1.1.15",
|
||||||
|
"@radix-ui/react-switch": "^1.3.7",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.21",
|
||||||
|
"@radix-ui/react-toast": "^1.2.23",
|
||||||
|
"@radix-ui/react-tooltip": "^1.2.16",
|
||||||
|
"@tanstack/react-virtual": "^3.14.6",
|
||||||
|
"archiver": "^8.0.0",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"cross-env": "^10.1.0",
|
||||||
|
"css-tree": "^3.0.1",
|
||||||
|
"electron-updater": "^6.6.2",
|
||||||
|
"ffmpeg-static": "5.3.0",
|
||||||
"fs-extra": "^11.3.2",
|
"fs-extra": "^11.3.2",
|
||||||
"html-to-image": "^1.11.13",
|
"fzstd": "^0.1.1",
|
||||||
"openai": "^6.10.0"
|
"jsonrepair": "^3.15.0",
|
||||||
|
"koffi": "^3.1.0",
|
||||||
|
"openai": "^6.10.0",
|
||||||
|
"parse5": "^8.0.0",
|
||||||
|
"pinyin-pro": "^3.26.0",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
|
"sherpa-onnx-darwin-x64": "1.13.3",
|
||||||
|
"sherpa-onnx-node": "1.13.3",
|
||||||
|
"sherpa-onnx-win-x64": "1.13.4",
|
||||||
|
"silk-wasm": "^3.7.1",
|
||||||
|
"tailwind-merge": "^3.6.0",
|
||||||
|
"unzipper": "^0.12.0",
|
||||||
|
"wechat-emojis": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
||||||
"@electron-toolkit/eslint-config-ts": "^3.1.0",
|
"@electron-toolkit/eslint-config-ts": "^3.1.0",
|
||||||
"@electron-toolkit/tsconfig": "^2.0.0",
|
"@electron-toolkit/tsconfig": "^2.0.0",
|
||||||
|
"@playwright/test": "^1.62.1",
|
||||||
|
"@rollup/rollup-darwin-arm64": "^4.62.2",
|
||||||
|
"@testing-library/dom": "^10.4.1",
|
||||||
|
"@testing-library/jest-dom": "^7.0.0",
|
||||||
|
"@testing-library/react": "^16.3.2",
|
||||||
|
"@testing-library/user-event": "^14.6.1",
|
||||||
|
"@types/archiver": "^8.0.0",
|
||||||
"@types/fs-extra": "^11.0.4",
|
"@types/fs-extra": "^11.0.4",
|
||||||
"@types/node": "^22.19.1",
|
"@types/node": "^22.19.1",
|
||||||
|
"@types/qrcode": "^1.5.6",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^5.1.1",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
"electron": "^39.2.6",
|
"@vitest/coverage-v8": "^4.1.10",
|
||||||
|
"autoprefixer": "^10.5.4",
|
||||||
|
"electron": "^43.0.0",
|
||||||
"electron-builder": "^26.0.12",
|
"electron-builder": "^26.0.12",
|
||||||
"electron-vite": "^5.0.0",
|
"electron-vite": "^5.0.0",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.24",
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
|
"jsdom": "^30.0.1",
|
||||||
|
"postcss": "^8.5.26",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.7.4",
|
||||||
"react": "^19.2.1",
|
"react": "^19.2.1",
|
||||||
"react-dom": "^19.2.1",
|
"react-dom": "^19.2.1",
|
||||||
|
"sass": "^1.102.0",
|
||||||
|
"tailwindcss": "3.4.17",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.2.6"
|
"vite": "^7.2.6",
|
||||||
|
"vitest": "^4.1.10",
|
||||||
|
"wrangler": "^4.28.1"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
|
"supportedArchitectures": {
|
||||||
|
"os": [
|
||||||
|
"current",
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"cpu": [
|
||||||
|
"current",
|
||||||
|
"x64"
|
||||||
|
]
|
||||||
|
},
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"electron",
|
"electron",
|
||||||
"esbuild"
|
"esbuild",
|
||||||
|
"ffmpeg-static"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { defineConfig } from '@playwright/test'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './tests/e2e',
|
||||||
|
testMatch: /.*\.spec\.ts/,
|
||||||
|
timeout: 45_000,
|
||||||
|
expect: { timeout: 8_000 },
|
||||||
|
fullyParallel: false,
|
||||||
|
workers: 1,
|
||||||
|
forbidOnly: Boolean(process.env.CI),
|
||||||
|
retries: process.env.CI ? 1 : 0,
|
||||||
|
reporter: process.env.CI
|
||||||
|
? [['line'], ['html', { outputFolder: 'playwright-report', open: 'never' }]]
|
||||||
|
: [['list'], ['html', { outputFolder: 'playwright-report', open: 'never' }]],
|
||||||
|
outputDir: 'test-results',
|
||||||
|
snapshotPathTemplate: 'tests/e2e/__screenshots__/{platform}/{testFilePath}/{arg}{ext}',
|
||||||
|
use: {
|
||||||
|
trace: 'retain-on-failure',
|
||||||
|
screenshot: 'only-on-failure',
|
||||||
|
video: 'retain-on-failure'
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 463 KiB |
|
After Width: | Height: | Size: 373 KiB |
|
After Width: | Height: | Size: 208 KiB |
|
After Width: | Height: | Size: 767 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 680 KiB |
|
After Width: | Height: | Size: 204 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 364 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 238 KiB |
|
After Width: | Height: | Size: 338 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 27 KiB |
@@ -0,0 +1,808 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<title>{{REPORT_TITLE}}</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
width: 430px;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
width: 430px;
|
||||||
|
background: #f3f5f7;
|
||||||
|
color: #1f2933;
|
||||||
|
font-family:
|
||||||
|
-apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||||
|
}
|
||||||
|
.report {
|
||||||
|
width: 430px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px 14px 34px;
|
||||||
|
}
|
||||||
|
.hero,
|
||||||
|
.section,
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 18px;
|
||||||
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
.hero {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.hero-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
.hero-top > div:first-child {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
.hero h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 23px;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
.sub {
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #667085;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
.mode-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #eef8f2;
|
||||||
|
color: #07a352;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
.avatar-grid {
|
||||||
|
width: 58px;
|
||||||
|
height: 58px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 3px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.avatar-grid img,
|
||||||
|
.avatar {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.hero-headline {
|
||||||
|
margin-top: 14px;
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: linear-gradient(135deg, #edf9f1 0%, #f7fbf8 100%);
|
||||||
|
}
|
||||||
|
.hero-headline b {
|
||||||
|
display: block;
|
||||||
|
font-size: 17px;
|
||||||
|
color: #076c39;
|
||||||
|
}
|
||||||
|
.hero-headline p {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.65;
|
||||||
|
color: #1f2933;
|
||||||
|
}
|
||||||
|
.hero-inline-notes {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.hero-note,
|
||||||
|
.hero-status {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.hero-status {
|
||||||
|
margin-top: 10px;
|
||||||
|
background: #f7faf9;
|
||||||
|
color: #076c39;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.hero-note.takeaway {
|
||||||
|
background: #eef8f2;
|
||||||
|
color: #076c39;
|
||||||
|
}
|
||||||
|
.hero-note.pending {
|
||||||
|
background: #fff8e8;
|
||||||
|
color: #8a5a00;
|
||||||
|
}
|
||||||
|
.stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
.stat {
|
||||||
|
background: #f7faf9;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 10px 6px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.stat b {
|
||||||
|
display: block;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #07a352;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
.stat span {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #667085;
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
margin-top: 18px;
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
font-size: 19px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
.section-title::before {
|
||||||
|
content: '';
|
||||||
|
width: 5px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 99px;
|
||||||
|
background: #07c160;
|
||||||
|
}
|
||||||
|
.section-subtitle {
|
||||||
|
margin: 10px 0 6px;
|
||||||
|
color: #667085;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.section-more {
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #98a2b3;
|
||||||
|
font-size: 11px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
padding: 14px;
|
||||||
|
margin-top: 10px;
|
||||||
|
border: 1px solid #edf0f2;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.topic-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.topic-title-row h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.35;
|
||||||
|
font-weight: 850;
|
||||||
|
}
|
||||||
|
.heat,
|
||||||
|
.tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #eef8f2;
|
||||||
|
color: #07a352;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.hot {
|
||||||
|
background: #fff4e5;
|
||||||
|
color: #d46b08;
|
||||||
|
}
|
||||||
|
.blue {
|
||||||
|
background: #eef5ff;
|
||||||
|
color: #1677ff;
|
||||||
|
}
|
||||||
|
.topic-meta {
|
||||||
|
margin-top: 6px;
|
||||||
|
color: #8a94a6;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.card p {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
.topic-conclusions {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.topic-conclusion {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #edf9f1;
|
||||||
|
color: #076c39;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.topic-inline-image {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 76px 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #f7faf9;
|
||||||
|
}
|
||||||
|
.topic-inline-image img {
|
||||||
|
width: 76px;
|
||||||
|
height: 76px;
|
||||||
|
border-radius: 10px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.participants {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.person-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
background: #f6f8fa;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 3px 8px 3px 3px;
|
||||||
|
}
|
||||||
|
.person-chip img {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.person-chip b {
|
||||||
|
max-width: 58px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.keywords {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.keywords span {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #f2f4f7;
|
||||||
|
color: #667085;
|
||||||
|
}
|
||||||
|
.important-card {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
background: #f7faf9;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.important-card > .avatar {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.important-meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #667085;
|
||||||
|
}
|
||||||
|
.important-meta b {
|
||||||
|
color: #1f2933;
|
||||||
|
}
|
||||||
|
.important-text {
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
.important-note {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 7px 9px;
|
||||||
|
border-left: 3px solid #07c160;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #07a352;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
.action-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.action-card {
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
.todo-card {
|
||||||
|
background: #eef5ff;
|
||||||
|
}
|
||||||
|
.unresolved-card {
|
||||||
|
background: #fff8e8;
|
||||||
|
}
|
||||||
|
.action-card b {
|
||||||
|
display: block;
|
||||||
|
color: #1f2933;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.action-card div {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: #485465;
|
||||||
|
}
|
||||||
|
.action-note {
|
||||||
|
color: #667085;
|
||||||
|
}
|
||||||
|
.chat-block {
|
||||||
|
background: #f0f2f5;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.chat-msg {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.chat-avatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.chat-name {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #667085;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.chat-bubble {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px 12px 12px 12px;
|
||||||
|
padding: 9px 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.quote-note {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 9px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff8e1;
|
||||||
|
color: #8a5a00;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.qa-card,
|
||||||
|
.resource {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
.qa-card b,
|
||||||
|
.resource b {
|
||||||
|
display: block;
|
||||||
|
color: #1f2933;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.qa-card div,
|
||||||
|
.resource {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: #485465;
|
||||||
|
}
|
||||||
|
.storyline-card,
|
||||||
|
.chain-card {
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
.storyline-steps {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.storyline-step {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 50px 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.storyline-step span {
|
||||||
|
color: #8a94a6;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.storyline-step b {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.chain-flow {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.chain-flow span {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 9px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #eef8f2;
|
||||||
|
color: #076c39;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.chain-flow i {
|
||||||
|
font-style: normal;
|
||||||
|
color: #98a2b3;
|
||||||
|
}
|
||||||
|
.badge-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
/* AI 图片识别板块 */
|
||||||
|
.vision-card {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 132px 1fr;
|
||||||
|
gap: 14px;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
background: linear-gradient(135deg, #edf9f1 0%, #f7fbf8 100%);
|
||||||
|
border: 1px solid #d6efde;
|
||||||
|
border-radius: 14px;
|
||||||
|
}
|
||||||
|
.vision-image {
|
||||||
|
width: 132px;
|
||||||
|
height: 132px;
|
||||||
|
border-radius: 12px;
|
||||||
|
object-fit: cover;
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
.vision-description {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #1f2933;
|
||||||
|
}
|
||||||
|
.vision-ocr {
|
||||||
|
margin-top: 6px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: #eef5ff;
|
||||||
|
color: #1677ff;
|
||||||
|
font-size: 11px;
|
||||||
|
border-radius: 8px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.vision-tags {
|
||||||
|
margin-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.vision-tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 8px;
|
||||||
|
background: #07c160;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
.vision-label {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #07a352;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.badge-card {
|
||||||
|
background: linear-gradient(180deg, #fdfdfd 0%, #f6fbf8 100%);
|
||||||
|
border: 1px solid #edf0f2;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
.badge-card b {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.badge-card p {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
.data-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.rank {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 9px;
|
||||||
|
padding: 9px 0;
|
||||||
|
border-bottom: 1px solid #eef0f2;
|
||||||
|
}
|
||||||
|
.rank:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.rank img {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.rank b {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.rank span {
|
||||||
|
margin-left: auto;
|
||||||
|
color: #8a94a6;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.cloud-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.cloud-tags span {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #f2f4f7;
|
||||||
|
color: #485465;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
.cloud-tags .xl {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #07a352;
|
||||||
|
background: #e9f8ef;
|
||||||
|
}
|
||||||
|
.cloud-tags .lg {
|
||||||
|
font-size: 17px;
|
||||||
|
color: #1677ff;
|
||||||
|
background: #eef5ff;
|
||||||
|
}
|
||||||
|
.cloud-tags .md {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #d46b08;
|
||||||
|
background: #fff4e5;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
padding: 16px 4px 0;
|
||||||
|
color: #98a2b3;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
.muted {
|
||||||
|
color: #8a94a6;
|
||||||
|
}
|
||||||
|
.empty-section {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.compact .report {
|
||||||
|
padding-top: 18px;
|
||||||
|
}
|
||||||
|
.compact .section {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
.compact .card {
|
||||||
|
padding: 11px;
|
||||||
|
}
|
||||||
|
.compact .important-card,
|
||||||
|
.compact .chat-block {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.compact .section-title {
|
||||||
|
margin-bottom: 9px;
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
.compact .hero-headline p,
|
||||||
|
.compact .card p,
|
||||||
|
.compact .important-text,
|
||||||
|
.compact .chat-bubble {
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.compact .participants,
|
||||||
|
.compact .keywords,
|
||||||
|
.compact .hero-inline-notes {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.compact .topic-conclusions {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.compact .topic-inline-image {
|
||||||
|
grid-template-columns: 64px 1fr;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
.compact .topic-inline-image img {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
.compact .stats {
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
.compact .stat {
|
||||||
|
padding: 8px 6px;
|
||||||
|
}
|
||||||
|
.compact .stat b {
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
@media (max-width: 430px) {
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.report {
|
||||||
|
width: 100%;
|
||||||
|
padding-left: 12px;
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="{{REPORT_MODE_CLASS}}">
|
||||||
|
<main class="report">
|
||||||
|
<header class="hero">
|
||||||
|
<div class="hero-top">
|
||||||
|
<div>
|
||||||
|
<h1>{{GROUP_NAME}}日报</h1>
|
||||||
|
<div class="sub">{{DATE_RANGE}}<br />{{RECORD_NOTE}}</div>
|
||||||
|
<div class="mode-tag">{{REPORT_MODE_LABEL}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="avatar-grid">{{HERO_AVATARS}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="hero-headline">
|
||||||
|
<b>{{HERO_HEADLINE}}</b>
|
||||||
|
<p>{{HERO_SUMMARY}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="hero-status {{HERO_STATUS_EMPTY_CLASS}}">{{HERO_STATUS_LINE}}</div>
|
||||||
|
<div class="hero-inline-notes">
|
||||||
|
<div class="hero-note takeaway {{HERO_TAKEAWAY_EMPTY_CLASS}}">{{HERO_TAKEAWAY}}</div>
|
||||||
|
<div class="hero-note pending {{HERO_PENDING_EMPTY_CLASS}}">{{HERO_PENDING}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat"><b>{{MESSAGE_COUNT}}</b><span>消息数</span></div>
|
||||||
|
<div class="stat"><b>{{ACTIVE_USERS}}</b><span>活跃人数</span></div>
|
||||||
|
<div class="stat"><b>{{TOPIC_COUNT}}</b><span>话题数</span></div>
|
||||||
|
<div class="stat"><b>{{MEDIA_COUNT}}</b><span>媒体消息</span></div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="section {{TOPICS_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">今日讨论热点</div>
|
||||||
|
{{TOPIC_CARDS}}
|
||||||
|
{{TOPICS_MORE_NOTE}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section {{MESSAGES_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">重要消息</div>
|
||||||
|
{{IMPORTANT_MESSAGES}}
|
||||||
|
{{MESSAGES_MORE_NOTE}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section {{ACTIONS_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">待办事项和未解决问题</div>
|
||||||
|
<div class="section-subtitle {{TODO_EMPTY_CLASS}}">待办事项</div>
|
||||||
|
<div class="action-grid {{TODO_EMPTY_CLASS}}">{{TODO_CARDS}}</div>
|
||||||
|
<div class="section-subtitle {{UNRESOLVED_EMPTY_CLASS}}">尚未解决</div>
|
||||||
|
<div class="action-grid {{UNRESOLVED_EMPTY_CLASS}}">{{UNRESOLVED_CARDS}}</div>
|
||||||
|
{{ACTIONS_MORE_NOTE}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section {{QUOTES_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">今日名场面</div>
|
||||||
|
{{QUOTE_BLOCKS}}
|
||||||
|
{{QUOTES_MORE_NOTE}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section {{ANALYTICS_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">今日群数据</div>
|
||||||
|
<div class="data-grid">
|
||||||
|
<div class="card">
|
||||||
|
<div class="muted" style="font-size: 12px; margin-bottom: 6px">话唠榜 TOP5</div>
|
||||||
|
{{RANK_ITEMS}}
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<p><b>最活跃时段:</b>{{ACTIVITY_TIMELINE}}</p>
|
||||||
|
<p><b>今日状态:</b>形成 {{CONCLUSION_COUNT}} 个结论,待办 {{TODO_COUNT}} 项,未解决 {{UNRESOLVED_COUNT}} 项。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section {{KEYWORDS_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">关键词</div>
|
||||||
|
<div class="cloud-tags">{{CLOUD_TAGS}}</div>
|
||||||
|
{{KEYWORDS_MORE_NOTE}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section {{RESOURCES_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">实用信息与资源</div>
|
||||||
|
{{RESOURCE_ITEMS}}
|
||||||
|
{{RESOURCES_MORE_NOTE}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section {{QA_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">问题与解答</div>
|
||||||
|
{{QA_CARDS}}
|
||||||
|
{{QA_MORE_NOTE}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section {{STORYLINES_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">今日剧情时间线</div>
|
||||||
|
{{STORYLINE_CARDS}}
|
||||||
|
{{STORYLINES_MORE_NOTE}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section {{REVERSALS_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">群聊反转现场</div>
|
||||||
|
{{REVERSAL_CARDS}}
|
||||||
|
{{REVERSALS_MORE_NOTE}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section {{VISION_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">{{VISION_TITLE}}</div>
|
||||||
|
{{VISION_CARDS}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
<section class="section {{VOICE_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">语音之最</div>
|
||||||
|
{{VOICE_CARDS}}
|
||||||
|
{{VOICE_MORE_NOTE}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section {{VOICE_RANK_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">语音时长榜</div>
|
||||||
|
<div class="card">{{VOICE_RANK_CARDS}}</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section {{BADGES_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">今日临时人设</div>
|
||||||
|
<div class="badge-grid">{{BADGE_CARDS}}</div>
|
||||||
|
{{BADGES_MORE_NOTE}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section {{CHAINS_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">话题参与链路</div>
|
||||||
|
{{CHAIN_CARDS}}
|
||||||
|
{{CHAINS_MORE_NOTE}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
数据来源:微信群聊记录<br />
|
||||||
|
生成时间:{{GENERATED_AT}}<br />
|
||||||
|
{{FOOTER_NOTE}}
|
||||||
|
</footer>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,611 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<title>{{REPORT_TITLE}}</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
width: 430px;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
width: 430px;
|
||||||
|
background: #f3f5f7;
|
||||||
|
color: #1f2933;
|
||||||
|
font-family:
|
||||||
|
-apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||||
|
}
|
||||||
|
.report {
|
||||||
|
width: 430px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 22px 14px 34px;
|
||||||
|
}
|
||||||
|
.hero,
|
||||||
|
.card,
|
||||||
|
.section {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
.hero {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.hero-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 14px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.hero-top > div:first-child {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
.hero h1 {
|
||||||
|
font-size: 23px;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
.sub {
|
||||||
|
color: #667085;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.record-note {
|
||||||
|
color: #485465;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
.overview {
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* hero 头像簇的几何必须由 contract 变量驱动,不能再写死容器尺寸。
|
||||||
|
*
|
||||||
|
* 生产导出会额外注入 `report-template-fragment-contract.ts`,其中
|
||||||
|
* `img.tm-avatar.tm-avatar--hero` 用 !important 把头像钉在
|
||||||
|
* clamp(28px, var(--tm-avatar-hero-size, 40px), 56px)。
|
||||||
|
* 旧版这里写死 58x58(单头像 28x28),两个权威打架:头像实际 40px,
|
||||||
|
* 2 列 x 40px + 3px gap = 83px 塞不进 58px 的盒子,于是头像向右向下溢出容器,
|
||||||
|
* 视觉上越过卡片内边距、压到卡片边缘之外。
|
||||||
|
*
|
||||||
|
* 现在容器尺寸由内容决定(列宽/行高都取同一个变量):头像数 1..4 都不会溢出,
|
||||||
|
* 主题调整 --tm-avatar-hero-size 时容器与头像也不会分叉。
|
||||||
|
*/
|
||||||
|
.avatar-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, var(--tm-avatar-hero-size, 40px));
|
||||||
|
grid-auto-rows: var(--tm-avatar-hero-size, 40px);
|
||||||
|
gap: 3px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
/* 单头像时不保留空列,簇宽恰好等于一个头像。 */
|
||||||
|
.avatar-grid.avatar-count-1 {
|
||||||
|
grid-template-columns: var(--tm-avatar-hero-size, 40px);
|
||||||
|
}
|
||||||
|
.avatar-grid.empty-section {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.avatar-grid img,
|
||||||
|
.avatar {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
.stat {
|
||||||
|
background: #f7faf9;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 10px 6px;
|
||||||
|
text-align: center;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.stat b {
|
||||||
|
display: block;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #07a352;
|
||||||
|
white-space: nowrap;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
.stat span {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #667085;
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
margin-top: 14px;
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 900;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.section-title::before {
|
||||||
|
content: '';
|
||||||
|
width: 5px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 99px;
|
||||||
|
background: #07c160;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
padding: 14px;
|
||||||
|
margin-top: 10px;
|
||||||
|
box-shadow: none;
|
||||||
|
border: 1px solid #edf0f2;
|
||||||
|
}
|
||||||
|
.topic-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.topic-title-row h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.35;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 850;
|
||||||
|
}
|
||||||
|
.heat,
|
||||||
|
.tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: #eef8f2;
|
||||||
|
color: #07a352;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.hot {
|
||||||
|
background: #fff4e5;
|
||||||
|
color: #d46b08;
|
||||||
|
}
|
||||||
|
.blue {
|
||||||
|
background: #eef5ff;
|
||||||
|
color: #1677ff;
|
||||||
|
}
|
||||||
|
.red {
|
||||||
|
background: #fff1f0;
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
.topic-meta {
|
||||||
|
margin-top: 6px;
|
||||||
|
color: #8a94a6;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.card p {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.65;
|
||||||
|
margin: 10px 0 0;
|
||||||
|
}
|
||||||
|
.participants {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.person-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
min-width: 0;
|
||||||
|
background: #f6f8fa;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 3px 8px 3px 3px;
|
||||||
|
}
|
||||||
|
.person-chip img {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.person-chip b {
|
||||||
|
max-width: 58px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.keywords {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.keywords span {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #f2f4f7;
|
||||||
|
color: #667085;
|
||||||
|
}
|
||||||
|
.resource {
|
||||||
|
padding: 11px 12px;
|
||||||
|
background: #f7f8fa;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
.resource b {
|
||||||
|
color: #1677ff;
|
||||||
|
}
|
||||||
|
.important-card {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
background: #f7faf9;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.important-card > .avatar {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.important-meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #667085;
|
||||||
|
}
|
||||||
|
.important-meta b {
|
||||||
|
color: #1f2933;
|
||||||
|
}
|
||||||
|
.important-text {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.55;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
.important-note {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 7px 9px;
|
||||||
|
border-left: 3px solid #07c160;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #07a352;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
.chat-block {
|
||||||
|
background: #f0f2f5;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.chat-msg {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.chat-avatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.chat-name {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #667085;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.chat-bubble {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px 12px 12px 12px;
|
||||||
|
padding: 9px 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.quote-note {
|
||||||
|
background: #fff8e1;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 9px 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #8a5a00;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.qa-card {
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.qa-card b {
|
||||||
|
display: block;
|
||||||
|
color: #1f2933;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.qa-card div {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: #485465;
|
||||||
|
}
|
||||||
|
.bar-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 82px 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 9px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.bar {
|
||||||
|
height: 10px;
|
||||||
|
background: #edf1f5;
|
||||||
|
border-radius: 999px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.bar i {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
background: #07c160;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
.rank {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 9px;
|
||||||
|
padding: 9px 0;
|
||||||
|
border-bottom: 1px solid #eef0f2;
|
||||||
|
}
|
||||||
|
.rank img {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.rank b {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.rank span {
|
||||||
|
margin-left: auto;
|
||||||
|
color: #8a94a6;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.cloud-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.cloud-tags span {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #f2f4f7;
|
||||||
|
color: #485465;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
.cloud-tags .xl {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #07a352;
|
||||||
|
background: #e9f8ef;
|
||||||
|
}
|
||||||
|
.cloud-tags .lg {
|
||||||
|
font-size: 17px;
|
||||||
|
color: #1677ff;
|
||||||
|
background: #eef5ff;
|
||||||
|
}
|
||||||
|
.cloud-tags .md {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #d46b08;
|
||||||
|
background: #fff4e5;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
padding: 16px 4px 0;
|
||||||
|
color: #98a2b3;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
.muted {
|
||||||
|
color: #8a94a6;
|
||||||
|
}
|
||||||
|
.empty-section {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
@media (max-width: 430px) {
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.report {
|
||||||
|
width: 100%;
|
||||||
|
padding-left: 12px;
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
.stats {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.stat b {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* AI 图片识别板块(v1 模板) */
|
||||||
|
.vision-card {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 132px 1fr;
|
||||||
|
gap: 14px;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
background: linear-gradient(135deg, #edf9f1 0%, #f7fbf8 100%);
|
||||||
|
border: 1px solid #d6efde;
|
||||||
|
border-radius: 14px;
|
||||||
|
}
|
||||||
|
.vision-image {
|
||||||
|
width: 132px;
|
||||||
|
height: 132px;
|
||||||
|
border-radius: 12px;
|
||||||
|
object-fit: cover;
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
.vision-description {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #1f2933;
|
||||||
|
}
|
||||||
|
.vision-ocr {
|
||||||
|
margin-top: 6px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: #eef5ff;
|
||||||
|
color: #1677ff;
|
||||||
|
font-size: 11px;
|
||||||
|
border-radius: 8px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.vision-tags {
|
||||||
|
margin-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.vision-tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 8px;
|
||||||
|
background: #07c160;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
.vision-label {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #07a352;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
/* 热度条形图(v1 模板) */
|
||||||
|
.heat-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 80px 1fr 40px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.heat-name {
|
||||||
|
color: #1f2933;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.heat-bar {
|
||||||
|
background: #f3f5f7;
|
||||||
|
border-radius: 999px;
|
||||||
|
height: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.heat-bar i {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #07c160 0%, #34d399 100%);
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
.heat-val {
|
||||||
|
color: #485465;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 11px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="report">
|
||||||
|
<header class="hero">
|
||||||
|
<div class="hero-top">
|
||||||
|
<div>
|
||||||
|
<h1>{{GROUP_NAME}}日报</h1>
|
||||||
|
<div class="sub">
|
||||||
|
<div>{{DATE_RANGE}}</div>
|
||||||
|
<div class="record-note">{{RECORD_NOTE}}</div>
|
||||||
|
<div class="overview">{{OVERVIEW}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="avatar-grid {{HERO_AVATAR_CLASS}}">{{HERO_AVATARS}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat"><b>{{MESSAGE_COUNT}}</b><span>消息数</span></div>
|
||||||
|
<div class="stat"><b>{{ACTIVE_USERS}}</b><span>活跃人数</span></div>
|
||||||
|
<div class="stat"><b>{{TIME_SPAN}}</b><span>持续时长</span></div>
|
||||||
|
<div class="stat"><b>{{TOPIC_COUNT}}</b><span>主要话题</span></div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="section topics">
|
||||||
|
<div class="section-title">今日讨论热点</div>
|
||||||
|
{{TOPIC_CARDS}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section vision {{VISION_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">{{VISION_TITLE}}</div>
|
||||||
|
{{VISION_CARDS}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section resources {{RESOURCES_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">实用信息与资源</div>
|
||||||
|
{{RESOURCE_ITEMS}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section messages {{MESSAGES_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">重要消息汇总</div>
|
||||||
|
{{IMPORTANT_MESSAGES}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section quotes {{QUOTES_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">有趣对话或金句</div>
|
||||||
|
{{QUOTE_BLOCKS}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section qa {{QA_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">问题与解答</div>
|
||||||
|
{{QA_CARDS}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section analytics">
|
||||||
|
<div class="section-title">群内数据可视化</div>
|
||||||
|
{{HEAT_BARS}}
|
||||||
|
<div class="card">
|
||||||
|
<div class="muted" style="font-size: 12px; margin-bottom: 6px">
|
||||||
|
话唠榜 TOP5(基于已读取记录估算)
|
||||||
|
</div>
|
||||||
|
{{RANK_ITEMS}}
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<p><b>活跃时间线:</b>{{ACTIVITY_TIMELINE}}</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section cloud">
|
||||||
|
<div class="section-title">词云/关键词</div>
|
||||||
|
<div class="cloud-tags">{{CLOUD_TAGS}}</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
数据来源:TraceMemo · 微信群聊记录<br />
|
||||||
|
生成时间:{{GENERATED_AT}}<br />
|
||||||
|
{{FOOTER_NOTE}}
|
||||||
|
</footer>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,816 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<title>{{REPORT_TITLE}}</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
width: 430px;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
width: 430px;
|
||||||
|
background: #f3f5f7;
|
||||||
|
color: #1f2933;
|
||||||
|
font-family:
|
||||||
|
-apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||||
|
}
|
||||||
|
.report {
|
||||||
|
width: 430px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px 14px 34px;
|
||||||
|
}
|
||||||
|
.hero,
|
||||||
|
.section,
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 18px;
|
||||||
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
.hero {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.hero-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
.hero-top > div:first-child {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
.hero h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 23px;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
.sub {
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #667085;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* hero 头像簇的几何必须由 contract 变量驱动,不能再写死容器尺寸。
|
||||||
|
*
|
||||||
|
* 生产导出会额外注入 `report-template-fragment-contract.ts`,其中
|
||||||
|
* `img.tm-avatar.tm-avatar--hero` 用 !important 把头像钉在
|
||||||
|
* clamp(28px, var(--tm-avatar-hero-size, 40px), 56px)。
|
||||||
|
* 旧版这里写死 58x58(单头像 28x28),两个权威打架:头像实际 40px,
|
||||||
|
* 2 列 x 40px + 3px gap = 83px 塞不进 58px 的盒子,于是头像向右向下溢出容器,
|
||||||
|
* 视觉上越过卡片内边距、压到卡片边缘之外。
|
||||||
|
*
|
||||||
|
* 现在容器尺寸由内容决定(列宽/行高都取同一个变量):头像数 1..4 都不会溢出,
|
||||||
|
* 主题调整 --tm-avatar-hero-size 时容器与头像也不会分叉。
|
||||||
|
*/
|
||||||
|
.avatar-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, var(--tm-avatar-hero-size, 40px));
|
||||||
|
grid-auto-rows: var(--tm-avatar-hero-size, 40px);
|
||||||
|
gap: 3px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
/* 单头像时不保留空列,簇宽恰好等于一个头像。 */
|
||||||
|
.avatar-grid.avatar-count-1 {
|
||||||
|
grid-template-columns: var(--tm-avatar-hero-size, 40px);
|
||||||
|
}
|
||||||
|
.avatar-grid.empty-section {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.avatar-grid img,
|
||||||
|
.avatar {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.hero-headline {
|
||||||
|
margin-top: 14px;
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: linear-gradient(135deg, #edf9f1 0%, #f7fbf8 100%);
|
||||||
|
}
|
||||||
|
.hero-headline b {
|
||||||
|
display: block;
|
||||||
|
font-size: 17px;
|
||||||
|
color: #076c39;
|
||||||
|
}
|
||||||
|
.hero-headline p {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.65;
|
||||||
|
color: #1f2933;
|
||||||
|
}
|
||||||
|
.hero-inline-notes {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.hero-note,
|
||||||
|
.hero-status {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.hero-status {
|
||||||
|
margin-top: 10px;
|
||||||
|
background: #f7faf9;
|
||||||
|
color: #076c39;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.hero-note.takeaway {
|
||||||
|
background: #eef8f2;
|
||||||
|
color: #076c39;
|
||||||
|
}
|
||||||
|
.hero-note.pending {
|
||||||
|
background: #fff8e8;
|
||||||
|
color: #8a5a00;
|
||||||
|
}
|
||||||
|
.stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
.stat {
|
||||||
|
background: #f7faf9;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 10px 6px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.stat b {
|
||||||
|
display: block;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #07a352;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
.stat span {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #667085;
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
margin-top: 18px;
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
font-size: 19px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
.section-title::before {
|
||||||
|
content: '';
|
||||||
|
width: 5px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 99px;
|
||||||
|
background: #07c160;
|
||||||
|
}
|
||||||
|
.section-subtitle {
|
||||||
|
margin: 10px 0 6px;
|
||||||
|
color: #667085;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.section-more {
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #98a2b3;
|
||||||
|
font-size: 11px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
padding: 14px;
|
||||||
|
margin-top: 10px;
|
||||||
|
border: 1px solid #edf0f2;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.topic-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.topic-title-row h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.35;
|
||||||
|
font-weight: 850;
|
||||||
|
}
|
||||||
|
.heat,
|
||||||
|
.tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #eef8f2;
|
||||||
|
color: #07a352;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.hot {
|
||||||
|
background: #fff4e5;
|
||||||
|
color: #d46b08;
|
||||||
|
}
|
||||||
|
.blue {
|
||||||
|
background: #eef5ff;
|
||||||
|
color: #1677ff;
|
||||||
|
}
|
||||||
|
.topic-meta {
|
||||||
|
margin-top: 6px;
|
||||||
|
color: #8a94a6;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.card p {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
.topic-conclusions {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.topic-conclusion {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #edf9f1;
|
||||||
|
color: #076c39;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.topic-inline-image {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 76px 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #f7faf9;
|
||||||
|
}
|
||||||
|
.topic-inline-image img {
|
||||||
|
width: 76px;
|
||||||
|
height: 76px;
|
||||||
|
border-radius: 10px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.participants {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.person-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
background: #f6f8fa;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 3px 8px 3px 3px;
|
||||||
|
}
|
||||||
|
.person-chip img {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.person-chip b {
|
||||||
|
max-width: 58px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.keywords {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.keywords span {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #f2f4f7;
|
||||||
|
color: #667085;
|
||||||
|
}
|
||||||
|
.important-card {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
background: #f7faf9;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.important-card > .avatar {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.important-meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #667085;
|
||||||
|
}
|
||||||
|
.important-meta b {
|
||||||
|
color: #1f2933;
|
||||||
|
}
|
||||||
|
.important-text {
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
.important-note {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 7px 9px;
|
||||||
|
border-left: 3px solid #07c160;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #07a352;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
.action-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.action-card {
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
.todo-card {
|
||||||
|
background: #eef5ff;
|
||||||
|
}
|
||||||
|
.unresolved-card {
|
||||||
|
background: #fff8e8;
|
||||||
|
}
|
||||||
|
.action-card b {
|
||||||
|
display: block;
|
||||||
|
color: #1f2933;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.action-card div {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: #485465;
|
||||||
|
}
|
||||||
|
.action-note {
|
||||||
|
color: #667085;
|
||||||
|
}
|
||||||
|
.chat-block {
|
||||||
|
background: #f0f2f5;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.chat-msg {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.chat-avatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.chat-name {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #667085;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.chat-bubble {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px 12px 12px 12px;
|
||||||
|
padding: 9px 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.quote-note {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 9px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff8e1;
|
||||||
|
color: #8a5a00;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.qa-card,
|
||||||
|
.resource {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
.qa-card b,
|
||||||
|
.resource b {
|
||||||
|
display: block;
|
||||||
|
color: #1f2933;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.qa-card div,
|
||||||
|
.resource {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: #485465;
|
||||||
|
}
|
||||||
|
.storyline-card,
|
||||||
|
.chain-card {
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
.storyline-steps {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.storyline-step {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 50px 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.storyline-step span {
|
||||||
|
color: #8a94a6;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.storyline-step b {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.chain-flow {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.chain-flow span {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 9px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #eef8f2;
|
||||||
|
color: #076c39;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.chain-flow i {
|
||||||
|
font-style: normal;
|
||||||
|
color: #98a2b3;
|
||||||
|
}
|
||||||
|
.badge-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
/* AI 图片识别板块 */
|
||||||
|
.vision-card {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 132px 1fr;
|
||||||
|
gap: 14px;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
background: linear-gradient(135deg, #edf9f1 0%, #f7fbf8 100%);
|
||||||
|
border: 1px solid #d6efde;
|
||||||
|
border-radius: 14px;
|
||||||
|
}
|
||||||
|
.vision-image {
|
||||||
|
width: 132px;
|
||||||
|
height: 132px;
|
||||||
|
border-radius: 12px;
|
||||||
|
object-fit: cover;
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
.vision-description {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #1f2933;
|
||||||
|
}
|
||||||
|
.vision-ocr {
|
||||||
|
margin-top: 6px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: #eef5ff;
|
||||||
|
color: #1677ff;
|
||||||
|
font-size: 11px;
|
||||||
|
border-radius: 8px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.vision-tags {
|
||||||
|
margin-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.vision-tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 8px;
|
||||||
|
background: #07c160;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
.vision-label {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #07a352;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.badge-card {
|
||||||
|
background: linear-gradient(180deg, #fdfdfd 0%, #f6fbf8 100%);
|
||||||
|
border: 1px solid #edf0f2;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
.badge-card b {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.badge-card p {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
.data-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.rank {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 9px;
|
||||||
|
padding: 9px 0;
|
||||||
|
border-bottom: 1px solid #eef0f2;
|
||||||
|
}
|
||||||
|
.rank:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.rank img {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.rank b {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.rank span {
|
||||||
|
margin-left: auto;
|
||||||
|
color: #8a94a6;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.cloud-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.cloud-tags span {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #f2f4f7;
|
||||||
|
color: #485465;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
.cloud-tags .xl {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #07a352;
|
||||||
|
background: #e9f8ef;
|
||||||
|
}
|
||||||
|
.cloud-tags .lg {
|
||||||
|
font-size: 17px;
|
||||||
|
color: #1677ff;
|
||||||
|
background: #eef5ff;
|
||||||
|
}
|
||||||
|
.cloud-tags .md {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #d46b08;
|
||||||
|
background: #fff4e5;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
padding: 16px 4px 0;
|
||||||
|
color: #98a2b3;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
.muted {
|
||||||
|
color: #8a94a6;
|
||||||
|
}
|
||||||
|
.empty-section {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.compact .report {
|
||||||
|
padding-top: 18px;
|
||||||
|
}
|
||||||
|
.compact .section {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
.compact .card {
|
||||||
|
padding: 11px;
|
||||||
|
}
|
||||||
|
.compact .important-card,
|
||||||
|
.compact .chat-block {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.compact .section-title {
|
||||||
|
margin-bottom: 9px;
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
.compact .hero-headline p,
|
||||||
|
.compact .card p,
|
||||||
|
.compact .important-text,
|
||||||
|
.compact .chat-bubble {
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.compact .participants,
|
||||||
|
.compact .keywords,
|
||||||
|
.compact .hero-inline-notes {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.compact .topic-conclusions {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.compact .topic-inline-image {
|
||||||
|
grid-template-columns: 64px 1fr;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
.compact .topic-inline-image img {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
.compact .stats {
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
.compact .stat {
|
||||||
|
padding: 8px 6px;
|
||||||
|
}
|
||||||
|
.compact .stat b {
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
@media (max-width: 430px) {
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.report {
|
||||||
|
width: 100%;
|
||||||
|
padding-left: 12px;
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="{{REPORT_MODE_CLASS}}">
|
||||||
|
<main class="report">
|
||||||
|
<header class="hero">
|
||||||
|
<div class="hero-top">
|
||||||
|
<div>
|
||||||
|
<h1>{{GROUP_NAME}}日报</h1>
|
||||||
|
<div class="sub">{{DATE_RANGE}}<br />{{RECORD_NOTE}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="avatar-grid {{HERO_AVATAR_CLASS}}">{{HERO_AVATARS}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="hero-headline">
|
||||||
|
<b>{{HERO_HEADLINE}}</b>
|
||||||
|
<p>{{HERO_SUMMARY}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="hero-status {{HERO_STATUS_EMPTY_CLASS}}">{{HERO_STATUS_LINE}}</div>
|
||||||
|
<div class="hero-inline-notes">
|
||||||
|
<div class="hero-note takeaway {{HERO_TAKEAWAY_EMPTY_CLASS}}">{{HERO_TAKEAWAY}}</div>
|
||||||
|
<div class="hero-note pending {{HERO_PENDING_EMPTY_CLASS}}">{{HERO_PENDING}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat"><b>{{MESSAGE_COUNT}}</b><span>消息数</span></div>
|
||||||
|
<div class="stat"><b>{{ACTIVE_USERS}}</b><span>活跃人数</span></div>
|
||||||
|
<div class="stat"><b>{{TOPIC_COUNT}}</b><span>话题数</span></div>
|
||||||
|
<div class="stat"><b>{{MEDIA_COUNT}}</b><span>媒体消息</span></div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="section {{TOPICS_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">今日讨论热点</div>
|
||||||
|
{{TOPIC_CARDS}}
|
||||||
|
{{TOPICS_MORE_NOTE}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section {{MESSAGES_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">重要消息</div>
|
||||||
|
{{IMPORTANT_MESSAGES}}
|
||||||
|
{{MESSAGES_MORE_NOTE}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section {{ACTIONS_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">待办事项和未解决问题</div>
|
||||||
|
<div class="section-subtitle {{TODO_EMPTY_CLASS}}">待办事项</div>
|
||||||
|
<div class="action-grid {{TODO_EMPTY_CLASS}}">{{TODO_CARDS}}</div>
|
||||||
|
<div class="section-subtitle {{UNRESOLVED_EMPTY_CLASS}}">尚未解决</div>
|
||||||
|
<div class="action-grid {{UNRESOLVED_EMPTY_CLASS}}">{{UNRESOLVED_CARDS}}</div>
|
||||||
|
{{ACTIONS_MORE_NOTE}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section {{QUOTES_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">今日名场面</div>
|
||||||
|
{{QUOTE_BLOCKS}}
|
||||||
|
{{QUOTES_MORE_NOTE}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section {{ANALYTICS_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">今日群数据</div>
|
||||||
|
<div class="data-grid">
|
||||||
|
<div class="card">
|
||||||
|
<div class="muted" style="font-size: 12px; margin-bottom: 6px">话唠榜 TOP5</div>
|
||||||
|
{{RANK_ITEMS}}
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<p><b>最活跃时段:</b>{{ACTIVITY_TIMELINE}}</p>
|
||||||
|
<p><b>今日状态:</b>形成 {{CONCLUSION_COUNT}} 个结论,待办 {{TODO_COUNT}} 项,未解决 {{UNRESOLVED_COUNT}} 项。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section {{KEYWORDS_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">关键词</div>
|
||||||
|
<div class="cloud-tags">{{CLOUD_TAGS}}</div>
|
||||||
|
{{KEYWORDS_MORE_NOTE}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section {{RESOURCES_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">实用信息与资源</div>
|
||||||
|
{{RESOURCE_ITEMS}}
|
||||||
|
{{RESOURCES_MORE_NOTE}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section {{QA_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">问题与解答</div>
|
||||||
|
{{QA_CARDS}}
|
||||||
|
{{QA_MORE_NOTE}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section {{STORYLINES_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">今日剧情时间线</div>
|
||||||
|
{{STORYLINE_CARDS}}
|
||||||
|
{{STORYLINES_MORE_NOTE}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section {{REVERSALS_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">群聊反转现场</div>
|
||||||
|
{{REVERSAL_CARDS}}
|
||||||
|
{{REVERSALS_MORE_NOTE}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section {{VISION_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">{{VISION_TITLE}}</div>
|
||||||
|
{{VISION_CARDS}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
<section class="section {{VOICE_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">语音之最</div>
|
||||||
|
{{VOICE_CARDS}}
|
||||||
|
{{VOICE_MORE_NOTE}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section {{VOICE_RANK_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">语音时长榜</div>
|
||||||
|
<div class="card">{{VOICE_RANK_CARDS}}</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section {{BADGES_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">今日临时人设</div>
|
||||||
|
<div class="badge-grid">{{BADGE_CARDS}}</div>
|
||||||
|
{{BADGES_MORE_NOTE}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section {{CHAINS_EMPTY_CLASS}}">
|
||||||
|
<div class="section-title">话题参与链路</div>
|
||||||
|
{{CHAIN_CARDS}}
|
||||||
|
{{CHAINS_MORE_NOTE}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
数据来源:TraceMemo · 微信群聊记录<br />
|
||||||
|
生成时间:{{GENERATED_AT}}<br />
|
||||||
|
{{FOOTER_NOTE}}
|
||||||
|
</footer>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||