chattercatcher 0.1.31 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import fs15 from "fs/promises";
8
8
  // package.json
9
9
  var package_default = {
10
10
  name: "chattercatcher",
11
- version: "0.1.31",
11
+ version: "0.2.1",
12
12
  description: "\u672C\u5730\u4F18\u5148\u7684\u98DE\u4E66/Lark \u5BB6\u5EAD\u7FA4\u77E5\u8BC6\u5E93\u673A\u5668\u4EBA",
13
13
  type: "module",
14
14
  main: "dist/index.js",
@@ -4614,11 +4614,23 @@ function extractImageKey(response) {
4614
4614
  }
4615
4615
  throw new Error("\u98DE\u4E66\u56FE\u7247\u4E0A\u4F20\u54CD\u5E94\u7F3A\u5C11 image_key\u3002");
4616
4616
  }
4617
- function isRichTextCompatibilityError(error) {
4617
+ function collectErrorFields(error) {
4618
+ const fields = [error];
4618
4619
  const value = error && typeof error === "object" ? error : {};
4619
- const code = value.code ?? value.errorCode;
4620
- const message = error instanceof Error ? error.message : String(error);
4621
- return code === 230001 || /post|msg_type|content|unsupported|invalid/i.test(message);
4620
+ fields.push(value.code, value.errorCode, value.msg, value.message);
4621
+ const response = value.response && typeof value.response === "object" ? value.response : {};
4622
+ const data2 = response.data && typeof response.data === "object" ? response.data : {};
4623
+ fields.push(data2.code, data2.errorCode, data2.msg, data2.message);
4624
+ return fields;
4625
+ }
4626
+ function isRichTextCompatibilityError(error) {
4627
+ return collectErrorFields(error).some((field) => {
4628
+ if (field === 230001) return true;
4629
+ if (typeof field === "string") {
4630
+ return /post|msg_type|content|unsupported|invalid/i.test(field);
4631
+ }
4632
+ return false;
4633
+ });
4622
4634
  }
4623
4635
  async function sendWithTextFallback(input2) {
4624
4636
  try {
@@ -5926,516 +5938,867 @@ import Fastify from "fastify";
5926
5938
  function buildHtml() {
5927
5939
  return `<!doctype html>
5928
5940
  <html lang="zh-CN">
5929
- <head>
5930
- <meta charset="utf-8" />
5931
- <meta name="viewport" content="width=device-width, initial-scale=1" />
5932
- <title>ChatterCatcher</title>
5933
- <style>
5934
- :root {
5935
- color-scheme: light;
5936
- --bg: #f6f5f0;
5937
- --panel: #ffffff;
5938
- --text: #1f2933;
5939
- --muted: #667085;
5940
- --line: #d9d7cf;
5941
- --accent: #1f7a5a;
5942
- --warn: #9a5b13;
5943
- }
5944
- * { box-sizing: border-box; }
5945
- body {
5946
- margin: 0;
5947
- font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
5948
- background: var(--bg);
5949
- color: var(--text);
5950
- }
5951
- main { max-width: 1120px; margin: 0 auto; padding: 32px 24px 48px; overflow-x: hidden; }
5952
- header {
5953
- display: flex;
5954
- justify-content: space-between;
5955
- gap: 20px;
5956
- align-items: flex-start;
5957
- padding-bottom: 24px;
5958
- border-bottom: 1px solid var(--line);
5959
- }
5960
- h1 { margin: 0; font-size: 30px; line-height: 1.1; letter-spacing: 0; }
5961
- h2 { margin: 0 0 12px; font-size: 18px; letter-spacing: 0; }
5962
- p { margin: 8px 0 0; color: var(--muted); }
5963
- code { background: #eceae2; border-radius: 4px; padding: 2px 6px; }
5964
- button {
5965
- appearance: none;
5966
- border: 1px solid var(--line);
5967
- background: var(--panel);
5968
- color: var(--text);
5969
- border-radius: 6px;
5970
- padding: 8px 12px;
5971
- cursor: pointer;
5972
- }
5973
- button:hover { border-color: var(--accent); }
5974
- .actions { display: flex; gap: 10px; flex-wrap: wrap; justify-content: flex-end; }
5975
- .grid {
5976
- display: grid;
5977
- grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
5978
- gap: 12px;
5979
- margin: 24px 0;
5980
- }
5981
- .metric {
5982
- background: var(--panel);
5983
- border: 1px solid var(--line);
5984
- border-radius: 8px;
5985
- padding: 16px;
5986
- min-height: 112px;
5987
- }
5988
- .label { color: var(--muted); font-size: 13px; }
5989
- .value { margin-top: 10px; font-size: 22px; font-weight: 650; overflow-wrap: anywhere; line-height: 1.18; }
5990
- .note { margin-top: 8px; color: var(--muted); font-size: 13px; line-height: 1.45; }
5991
- .layout {
5992
- display: grid;
5993
- grid-template-columns: minmax(0, 1fr) minmax(280px, 380px);
5994
- gap: 24px;
5995
- }
5996
- .layout > * { min-width: 0; }
5997
- section { padding: 20px 0; border-top: 1px solid var(--line); }
5998
- section:first-child { border-top: 0; }
5999
- .message-list { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
6000
- .message-item { padding: 14px 16px; border-bottom: 1px solid var(--line); }
6001
- .message-item:last-child { border-bottom: 0; }
6002
- .message-meta { display: flex; flex-wrap: wrap; gap: 8px 14px; color: var(--muted); font-size: 13px; line-height: 1.4; }
6003
- .message-body { margin-top: 8px; white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.55; }
6004
- table { width: 100%; table-layout: fixed; border-collapse: collapse; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
6005
- th, td { padding: 12px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; overflow: hidden; text-overflow: ellipsis; }
6006
- th { color: var(--muted); font-size: 13px; font-weight: 600; }
6007
- tr:last-child td { border-bottom: 0; }
6008
- .message { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
6009
- .id-text, .path { display: block; max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--muted); font-size: 13px; }
6010
- .compact-table th:first-child, .compact-table td:first-child { width: 120px; }
6011
- .compact-table th:nth-child(2), .compact-table td:nth-child(2) { width: 180px; }
6012
- .status-ok { color: var(--accent); }
6013
- .status-warn { color: var(--warn); }
6014
- .empty { color: var(--muted); padding: 18px; background: var(--panel); border: 1px dashed var(--line); border-radius: 8px; }
6015
- .status-line { margin-top: 10px; font-size: 13px; color: var(--muted); text-align: right; }
6016
- @media (max-width: 900px) {
6017
- header, .layout { display: block; }
6018
- .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
6019
- header button { margin-top: 16px; }
6020
- }
6021
- @media (max-width: 560px) {
6022
- main { padding: 24px 16px 36px; }
6023
- .grid { grid-template-columns: 1fr; }
6024
- }
6025
- </style>
6026
- </head>
6027
- <body>
6028
- <main>
6029
- <header>
5941
+ <head>
5942
+ <meta charset="utf-8" />
5943
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
5944
+ <meta name="color-scheme" content="dark" />
5945
+ <title>ChatterCatcher</title>
5946
+ <style>
5947
+ :root {
5948
+ --bg-primary: #0a0a0f;
5949
+ --bg-secondary: #12121a;
5950
+ --bg-tertiary: #1a1a28;
5951
+ --glass-bg: rgba(255,255,255,0.05);
5952
+ --glass-border: rgba(255,255,255,0.1);
5953
+ --glass-border-hover: rgba(255,255,255,0.2);
5954
+ --glass-shadow: 0 8px 32px rgba(0,0,0,0.3);
5955
+ --text-primary: #f0f0f5;
5956
+ --text-secondary: #a0a0b0;
5957
+ --text-muted: #6e6e80;
5958
+ --accent: #64d2ff;
5959
+ --accent-hover: #7dd8ff;
5960
+ --success: #30d158;
5961
+ --warning: #ff9f0a;
5962
+ --danger: #ff453a;
5963
+ --radius-sm: 8px;
5964
+ --radius-md: 12px;
5965
+ --radius-lg: 16px;
5966
+ --radius-xl: 24px;
5967
+ --space-xs: 4px;
5968
+ --space-sm: 8px;
5969
+ --space-md: 16px;
5970
+ --space-lg: 24px;
5971
+ --space-xl: 32px;
5972
+ --space-2xl: 48px;
5973
+ --font-sans: -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;
5974
+ --font-mono: "SF Mono","Menlo","Consolas",monospace;
5975
+ }
5976
+ * { box-sizing: border-box; margin: 0; padding: 0; }
5977
+ body {
5978
+ font-family: var(--font-sans);
5979
+ background: var(--bg-primary);
5980
+ color: var(--text-primary);
5981
+ line-height: 1.6;
5982
+ -webkit-font-smoothing: antialiased;
5983
+ overflow-x: hidden;
5984
+ min-height: 100vh;
5985
+ }
5986
+ .glass {
5987
+ background: var(--glass-bg);
5988
+ backdrop-filter: blur(20px) saturate(180%);
5989
+ -webkit-backdrop-filter: blur(20px) saturate(180%);
5990
+ border: 1px solid var(--glass-border);
5991
+ border-radius: var(--radius-lg);
5992
+ box-shadow: var(--glass-shadow);
5993
+ transition: all 0.3s ease;
5994
+ }
5995
+ .glass:hover { border-color: var(--glass-border-hover); box-shadow: 0 12px 40px rgba(0,0,0,0.4); }
5996
+ .gradient-bg {
5997
+ background: linear-gradient(135deg,#0a0a0f 0%,#12121a 50%,#1a1a28 100%);
5998
+ min-height: 100vh;
5999
+ }
6000
+ .sidebar {
6001
+ position: fixed; left: 0; top: 0; width: 260px; height: 100vh;
6002
+ padding: var(--space-lg); display: flex; flex-direction: column; gap: var(--space-md); z-index: 100;
6003
+ background: linear-gradient(180deg,rgba(255,255,255,0.08) 0%,rgba(255,255,255,0.02) 100%);
6004
+ backdrop-filter: blur(40px) saturate(200%);
6005
+ -webkit-backdrop-filter: blur(40px) saturate(200%);
6006
+ border-right: 1px solid var(--glass-border);
6007
+ }
6008
+ .sidebar-logo {
6009
+ display: flex; align-items: center; gap: var(--space-sm);
6010
+ padding: var(--space-md); font-size: 20px; font-weight: 700;
6011
+ color: var(--text-primary); margin-bottom: var(--space-md);
6012
+ }
6013
+ .logo-icon {
6014
+ width: 36px; height: 36px;
6015
+ background: linear-gradient(135deg,var(--accent),#5e60ce);
6016
+ border-radius: var(--radius-md);
6017
+ display: flex; align-items: center; justify-content: center;
6018
+ box-shadow: 0 4px 16px rgba(100,210,255,0.3);
6019
+ }
6020
+ .sidebar-nav { display: flex; flex-direction: column; gap: var(--space-xs); }
6021
+ .nav-item {
6022
+ display: flex; align-items: center; gap: var(--space-sm);
6023
+ padding: var(--space-sm) var(--space-md); border-radius: var(--radius-md);
6024
+ color: var(--text-secondary); text-decoration: none; cursor: pointer;
6025
+ transition: all 0.2s ease; border: none; background: none;
6026
+ font-size: 14px; font-family: inherit; width: 100%; text-align: left;
6027
+ }
6028
+ .nav-item:hover { background: rgba(255,255,255,0.06); color: var(--text-primary); }
6029
+ .nav-item.active {
6030
+ background: rgba(100,210,255,0.15); color: var(--accent);
6031
+ box-shadow: 0 0 20px rgba(100,210,255,0.1);
6032
+ }
6033
+ .nav-icon { width: 20px; height: 20px; flex-shrink: 0; }
6034
+ .main-content { margin-left: 260px; min-height: 100vh; padding: var(--space-xl); }
6035
+ .page-header { margin-bottom: var(--space-xl); }
6036
+ .page-title {
6037
+ font-size: 36px; font-weight: 700; letter-spacing: -0.03em;
6038
+ margin-bottom: var(--space-sm);
6039
+ background: linear-gradient(135deg,var(--text-primary),var(--accent));
6040
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
6041
+ }
6042
+ .page-subtitle { color: var(--text-secondary); font-size: 15px; }
6043
+ .metrics-grid {
6044
+ display: grid; grid-template-columns: repeat(auto-fit,minmax(200px,1fr));
6045
+ gap: var(--space-md); margin-bottom: var(--space-xl);
6046
+ }
6047
+ .metric-card {
6048
+ padding: var(--space-lg); display: flex; flex-direction: column; gap: var(--space-sm);
6049
+ position: relative; overflow: hidden;
6050
+ }
6051
+ .metric-card::before {
6052
+ content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px;
6053
+ background: linear-gradient(90deg,var(--accent),transparent); opacity: 0.5;
6054
+ }
6055
+ .metric-value { font-size: 40px; font-weight: 700; color: var(--text-primary); line-height: 1; font-variant-numeric: tabular-nums; }
6056
+ .metric-label { font-size: 12px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.08em; font-weight: 600; }
6057
+ .metric-note { font-size: 13px; color: var(--text-secondary); margin-top: var(--space-xs); }
6058
+ .content-grid { display: grid; grid-template-columns: 2fr 1fr; gap: var(--space-lg); }
6059
+ .content-panel { padding: var(--space-lg); }
6060
+ .panel-header {
6061
+ display: flex; justify-content: space-between; align-items: center;
6062
+ margin-bottom: var(--space-lg); padding-bottom: var(--space-md);
6063
+ border-bottom: 1px solid var(--glass-border);
6064
+ }
6065
+ .panel-title { font-size: 18px; font-weight: 600; }
6066
+ .message-list { display: flex; flex-direction: column; gap: var(--space-sm); }
6067
+ .message-card {
6068
+ padding: var(--space-md); border-radius: var(--radius-md);
6069
+ background: rgba(255,255,255,0.03); border: 1px solid transparent;
6070
+ transition: all 0.25s ease; cursor: pointer;
6071
+ }
6072
+ .message-card:hover { background: rgba(255,255,255,0.06); border-color: var(--glass-border); transform: translateX(4px); }
6073
+ .message-meta {
6074
+ display: flex; align-items: center; gap: var(--space-md);
6075
+ color: var(--text-muted); font-size: 12px; margin-bottom: var(--space-xs); flex-wrap: wrap;
6076
+ }
6077
+ .message-text { color: var(--text-secondary); font-size: 14px; line-height: 1.6; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
6078
+ .status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
6079
+ .status-dot.online { background: var(--success); box-shadow: 0 0 8px var(--success); }
6080
+ .status-dot.offline { background: var(--danger); }
6081
+ .status-dot.warning { background: var(--warning); box-shadow: 0 0 8px var(--warning); }
6082
+ .status-dot.pending { background: var(--text-muted); }
6083
+ .btn {
6084
+ display: inline-flex; align-items: center; justify-content: center; gap: var(--space-sm);
6085
+ padding: 10px var(--space-md); border-radius: var(--radius-md);
6086
+ border: 1px solid var(--glass-border); background: var(--glass-bg);
6087
+ color: var(--text-primary); font-family: inherit; font-size: 14px;
6088
+ cursor: pointer; transition: all 0.2s ease; text-decoration: none;
6089
+ }
6090
+ .btn:hover { background: rgba(255,255,255,0.1); border-color: var(--glass-border-hover); transform: translateY(-1px); }
6091
+ .btn-primary {
6092
+ background: linear-gradient(135deg,var(--accent),#5e60ce); color: white; border: none;
6093
+ font-weight: 600; box-shadow: 0 4px 16px rgba(100,210,255,0.3);
6094
+ }
6095
+ .btn-primary:hover {
6096
+ background: linear-gradient(135deg,var(--accent-hover),#6b6dd8);
6097
+ box-shadow: 0 6px 20px rgba(100,210,255,0.4); transform: translateY(-1px);
6098
+ }
6099
+ .btn-danger { background: rgba(255,69,58,0.15); color: var(--danger); border-color: rgba(255,69,58,0.3); }
6100
+ .btn-danger:hover { background: rgba(255,69,58,0.25); }
6101
+ .btn-sm { padding: 6px var(--space-sm); font-size: 13px; }
6102
+ .btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
6103
+ .tag {
6104
+ display: inline-flex; align-items: center; padding: 2px 10px;
6105
+ border-radius: 20px; font-size: 12px; font-weight: 500;
6106
+ background: rgba(255,255,255,0.06); color: var(--text-secondary);
6107
+ }
6108
+ .tag-success { background: rgba(48,209,88,0.15); color: var(--success); }
6109
+ .tag-warning { background: rgba(255,159,10,0.15); color: var(--warning); }
6110
+ .tag-error { background: rgba(255,69,58,0.15); color: var(--danger); }
6111
+ .tag-info { background: rgba(100,210,255,0.15); color: var(--accent); }
6112
+ .empty-state { text-align: center; padding: var(--space-2xl); color: var(--text-muted); }
6113
+ .empty-state svg { width: 48px; height: 48px; margin: 0 auto var(--space-md); opacity: 0.3; }
6114
+ .skeleton {
6115
+ background: linear-gradient(90deg,rgba(255,255,255,0.03) 25%,rgba(255,255,255,0.08) 50%,rgba(255,255,255,0.03) 75%);
6116
+ background-size: 200% 100%; animation: shimmer 1.5s infinite; border-radius: var(--radius-sm);
6117
+ }
6118
+ @keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
6119
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
6120
+ @keyframes slideIn { from { opacity: 0; transform: translateX(-10px); } to { opacity: 1; transform: translateX(0); } }
6121
+ @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
6122
+ .view { display: none; animation: fadeIn 0.35s ease; }
6123
+ .view.active { display: block; }
6124
+ .search-box { position: relative; width: 100%; max-width: 400px; }
6125
+ .search-box input {
6126
+ width: 100%; padding: var(--space-sm) var(--space-md) var(--space-sm) 40px;
6127
+ border-radius: var(--radius-md); border: 1px solid var(--glass-border);
6128
+ background: var(--glass-bg); color: var(--text-primary); font-family: inherit;
6129
+ font-size: 14px; outline: none; transition: all 0.2s ease;
6130
+ }
6131
+ .search-box input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(100,210,255,0.1); }
6132
+ .search-box .search-icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--text-muted); }
6133
+ .data-table { width: 100%; border-collapse: collapse; }
6134
+ .data-table th {
6135
+ text-align: left; padding: var(--space-sm) var(--space-md); color: var(--text-muted);
6136
+ font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em;
6137
+ border-bottom: 1px solid var(--glass-border);
6138
+ }
6139
+ .data-table td { padding: var(--space-sm) var(--space-md); color: var(--text-secondary); font-size: 14px; border-bottom: 1px solid rgba(255,255,255,0.03); vertical-align: top; }
6140
+ .data-table tr:hover td { background: rgba(255,255,255,0.02); }
6141
+ .data-table tr:last-child td { border-bottom: none; }
6142
+ .truncate { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
6143
+ .truncate-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
6144
+ .truncate-3 { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
6145
+ .flex { display: flex; } .flex-col { flex-direction: column; }
6146
+ .items-center { align-items: center; } .justify-between { justify-content: space-between; }
6147
+ .gap-sm { gap: var(--space-sm); } .gap-md { gap: var(--space-md); }
6148
+ .mt-md { margin-top: var(--space-md); } .mt-lg { margin-top: var(--space-lg); }
6149
+ .mb-md { margin-bottom: var(--space-md); }
6150
+ .toast {
6151
+ padding: var(--space-md) var(--space-lg); border-radius: var(--radius-md);
6152
+ background: var(--glass-bg); backdrop-filter: blur(20px); border: 1px solid var(--glass-border);
6153
+ box-shadow: 0 8px 32px rgba(0,0,0,0.4); color: var(--text-primary); font-size: 14px;
6154
+ max-width: 400px; animation: slideIn 0.3s ease;
6155
+ display: flex; align-items: center; gap: var(--space-sm);
6156
+ }
6157
+ .toast-success { border-color: rgba(48,209,88,0.3); background: rgba(48,209,88,0.1); }
6158
+ .toast-error { border-color: rgba(255,69,58,0.3); background: rgba(255,69,58,0.1); }
6159
+ .toast-warning { border-color: rgba(255,159,10,0.3); background: rgba(255,159,10,0.1); }
6160
+ .episode-card {
6161
+ padding: var(--space-md); border-radius: var(--radius-md);
6162
+ background: rgba(255,255,255,0.03); border: 1px solid transparent; transition: all 0.25s ease;
6163
+ }
6164
+ .episode-card:hover { background: rgba(255,255,255,0.06); border-color: var(--glass-border); }
6165
+ .qa-card {
6166
+ padding: var(--space-md); border-radius: var(--radius-md);
6167
+ background: rgba(255,255,255,0.03); border-left: 3px solid var(--accent); margin-bottom: var(--space-sm);
6168
+ }
6169
+ .qa-question { font-weight: 600; color: var(--text-primary); margin-bottom: var(--space-xs); font-size: 14px; }
6170
+ .qa-answer { color: var(--text-secondary); font-size: 14px; line-height: 1.6; }
6171
+ .section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--space-lg); }
6172
+ .section-title { font-size: 24px; font-weight: 700; }
6173
+ .tabs {
6174
+ display: flex; gap: var(--space-xs); padding: 4px;
6175
+ background: rgba(255,255,255,0.03); border-radius: var(--radius-md); border: 1px solid var(--glass-border);
6176
+ }
6177
+ .tab { padding: 8px 16px; border-radius: var(--radius-sm); border: none; background: none; color: var(--text-secondary); font-family: inherit; font-size: 14px; cursor: pointer; transition: all 0.2s ease; }
6178
+ .tab:hover { color: var(--text-primary); }
6179
+ .tab.active { background: rgba(255,255,255,0.08); color: var(--text-primary); font-weight: 500; }
6180
+ .file-card {
6181
+ padding: var(--space-md); border-radius: var(--radius-md);
6182
+ background: rgba(255,255,255,0.03); border: 1px solid transparent; transition: all 0.25s ease; cursor: pointer;
6183
+ }
6184
+ .file-card:hover { background: rgba(255,255,255,0.06); border-color: var(--glass-border); }
6185
+ .file-icon {
6186
+ width: 40px; height: 40px; border-radius: var(--radius-sm);
6187
+ background: linear-gradient(135deg,var(--accent),#5e60ce);
6188
+ display: flex; align-items: center; justify-content: center; margin-bottom: var(--space-sm);
6189
+ }
6190
+ .timeline { position: relative; padding-left: 28px; }
6191
+ .timeline::before { content: ''; position: absolute; left: 8px; top: 0; bottom: 0; width: 2px; background: linear-gradient(180deg,var(--accent),transparent); opacity: 0.3; }
6192
+ .timeline-item { position: relative; padding-bottom: var(--space-lg); }
6193
+ .timeline-item::before { content: ''; position: absolute; left: -24px; top: 4px; width: 10px; height: 10px; border-radius: 50%; background: var(--accent); border: 2px solid var(--bg-primary); box-shadow: 0 0 0 2px var(--accent); }
6194
+ .timeline-date { font-size: 12px; color: var(--text-muted); margin-bottom: var(--space-xs); }
6195
+ .timeline-content { color: var(--text-secondary); font-size: 14px; }
6196
+ .status-bar { display: flex; align-items: center; gap: var(--space-md); padding: var(--space-md); margin-bottom: var(--space-lg); }
6197
+ .status-item { display: flex; align-items: center; gap: var(--space-sm); }
6198
+ .status-label { font-size: 13px; color: var(--text-muted); }
6199
+ .status-value { font-size: 14px; font-weight: 600; color: var(--text-primary); }
6200
+ .grid-2 { display: grid; grid-template-columns: repeat(2,1fr); gap: var(--space-md); }
6201
+ .grid-3 { display: grid; grid-template-columns: repeat(3,1fr); gap: var(--space-md); }
6202
+ .settings-group { padding: var(--space-lg); margin-bottom: var(--space-lg); }
6203
+ .settings-item { display: flex; justify-content: space-between; align-items: center; padding: var(--space-md) 0; border-bottom: 1px solid var(--glass-border); }
6204
+ .settings-item:last-child { border-bottom: none; }
6205
+ .settings-label { font-size: 14px; font-weight: 500; color: var(--text-primary); }
6206
+ .settings-value { font-size: 14px; color: var(--text-secondary); font-family: var(--font-mono); }
6207
+ .settings-desc { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
6208
+ .mobile-nav {
6209
+ display: none; position: fixed; bottom: 0; left: 0; right: 0;
6210
+ padding: var(--space-sm); z-index: 100; flex-direction: row; justify-content: space-around;
6211
+ border-top: 1px solid var(--glass-border);
6212
+ background: linear-gradient(180deg,rgba(255,255,255,0.08) 0%,rgba(255,255,255,0.02) 100%);
6213
+ backdrop-filter: blur(40px) saturate(200%);
6214
+ }
6215
+ .mobile-nav-item { display: flex; flex-direction: column; align-items: center; gap: 2px; padding: var(--space-xs); color: var(--text-secondary); text-decoration: none; cursor: pointer; border: none; background: none; font-size: 10px; font-family: inherit; }
6216
+ .mobile-nav-item.active { color: var(--accent); }
6217
+ .pulse { animation: pulse 2s cubic-bezier(0.4,0,0.6,1) infinite; }
6218
+ @media (max-width: 1024px) {
6219
+ .sidebar { width: 72px; padding: var(--space-sm); }
6220
+ .sidebar-logo span, .nav-item span { display: none; }
6221
+ .nav-item { justify-content: center; padding: var(--space-sm); }
6222
+ .main-content { margin-left: 72px; padding: var(--space-lg); }
6223
+ .content-grid { grid-template-columns: 1fr; }
6224
+ .metrics-grid { grid-template-columns: repeat(2,1fr); }
6225
+ }
6226
+ @media (max-width: 768px) {
6227
+ .sidebar { display: none; }
6228
+ .mobile-nav { display: flex; }
6229
+ .main-content { margin-left: 0; margin-bottom: 80px; padding: var(--space-md); }
6230
+ .page-title { font-size: 28px; }
6231
+ .metrics-grid { grid-template-columns: repeat(2,1fr); }
6232
+ .grid-2, .grid-3 { grid-template-columns: 1fr; }
6233
+ .section-header { flex-direction: column; align-items: flex-start; gap: var(--space-sm); }
6234
+ }
6235
+ @media (prefers-reduced-motion: reduce) {
6236
+ *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
6237
+ }
6238
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
6239
+ ::-webkit-scrollbar-track { background: transparent; }
6240
+ ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 4px; }
6241
+ ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.2); }
6242
+ .highlight-text { background: rgba(100,210,255,0.15); padding: 0 4px; border-radius: 3px; color: var(--accent); }
6243
+ </style>
6244
+ </head>
6245
+ <body class="gradient-bg">
6246
+ <aside class="sidebar">
6247
+ <div class="sidebar-logo">
6248
+ <div class="logo-icon">
6249
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
6250
+ </div>
6251
+ <span>ChatterCatcher</span>
6252
+ </div>
6253
+ <nav class="sidebar-nav">
6254
+ <button class="nav-item active" data-view="overview">
6255
+ <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
6256
+ <span>\u6982\u89C8</span>
6257
+ </button>
6258
+ <button class="nav-item" data-view="messages">
6259
+ <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
6260
+ <span>\u6D88\u606F</span>
6261
+ </button>
6262
+ <button class="nav-item" data-view="episodes">
6263
+ <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>
6264
+ <span>\u4F1A\u8BDD\u8BB0\u5FC6</span>
6265
+ </button>
6266
+ <button class="nav-item" data-view="files">
6267
+ <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>
6268
+ <span>\u6587\u4EF6\u5E93</span>
6269
+ </button>
6270
+ <button class="nav-item" data-view="tasks">
6271
+ <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
6272
+ <span>\u4EFB\u52A1</span>
6273
+ </button>
6274
+ <button class="nav-item" data-view="qa-logs">
6275
+ <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
6276
+ <span>\u95EE\u7B54\u65E5\u5FD7</span>
6277
+ </button>
6278
+ <button class="nav-item" data-view="settings">
6279
+ <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.6 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
6280
+ <span>\u8BBE\u7F6E</span>
6281
+ </button>
6282
+ </nav>
6283
+ <div style="margin-top: auto; padding: var(--space-md);">
6284
+ <div style="display: flex; align-items: center; gap: var(--space-sm); font-size: 12px; color: var(--text-muted);">
6285
+ <span class="status-dot online" id="gateway-indicator"></span>
6286
+ <span id="gateway-status-text">Gateway \u8FD0\u884C\u4E2D</span>
6287
+ </div>
6288
+ <div style="font-size: 11px; color: var(--text-muted); margin-top: var(--space-xs); opacity: 0.7;" id="version-text">v0.0.0</div>
6289
+ </div>
6290
+ </aside>
6291
+
6292
+ <nav class="mobile-nav glass">
6293
+ <button class="mobile-nav-item active" data-view="overview">
6294
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
6295
+ <span>\u6982\u89C8</span>
6296
+ </button>
6297
+ <button class="mobile-nav-item" data-view="messages">
6298
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
6299
+ <span>\u6D88\u606F</span>
6300
+ </button>
6301
+ <button class="mobile-nav-item" data-view="files">
6302
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>
6303
+ <span>\u6587\u4EF6</span>
6304
+ </button>
6305
+ <button class="mobile-nav-item" data-view="tasks">
6306
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
6307
+ <span>\u4EFB\u52A1</span>
6308
+ </button>
6309
+ <button class="mobile-nav-item" data-view="settings">
6310
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.6 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
6311
+ <span>\u8BBE\u7F6E</span>
6312
+ </button>
6313
+ </nav>
6314
+
6315
+ <main class="main-content">
6316
+ <div class="view active" id="view-overview">
6317
+ <div class="page-header">
6318
+ <h1 class="page-title">Dashboard</h1>
6319
+ <p class="page-subtitle">\u672C\u5730\u4F18\u5148\u7684\u5BB6\u5EAD\u7FA4\u77E5\u8BC6\u5E93 \xB7 \u95EE\u7B54\u5FC5\u987B\u5148\u68C0\u7D22 RAG \u8BC1\u636E\uFF0C\u4E0D\u5806\u53E0\u5168\u91CF\u4E0A\u4E0B\u6587</p>
6320
+ </div>
6321
+ <div class="metrics-grid" id="metrics"></div>
6322
+ <div class="content-grid">
6030
6323
  <div>
6031
- <h1>ChatterCatcher</h1>
6032
- <p>\u672C\u5730\u4F18\u5148\u7684\u5BB6\u5EAD\u7FA4\u77E5\u8BC6\u5E93\u3002\u95EE\u7B54\u5FC5\u987B\u5148\u68C0\u7D22 RAG \u8BC1\u636E\uFF0C\u4E0D\u5806\u53E0\u5168\u91CF\u4E0A\u4E0B\u6587\u3002</p>
6324
+ <div class="content-panel glass">
6325
+ <div class="panel-header">
6326
+ <h2 class="panel-title">\u6700\u8FD1\u6D88\u606F</h2>
6327
+ <button class="btn btn-sm" onclick="navigateTo('messages')">\u67E5\u770B\u5168\u90E8</button>
6328
+ </div>
6329
+ <div id="recent-messages"></div>
6330
+ </div>
6331
+ <div class="content-panel glass mt-lg">
6332
+ <div class="panel-header">
6333
+ <h2 class="panel-title">\u4F1A\u8BDD\u8BB0\u5FC6</h2>
6334
+ <button class="btn btn-sm" onclick="navigateTo('episodes')">\u67E5\u770B\u5168\u90E8</button>
6335
+ </div>
6336
+ <div id="recent-episodes"></div>
6337
+ </div>
6033
6338
  </div>
6034
6339
  <div>
6035
- <div class="actions">
6036
- <button id="process-messages" type="button">\u7ACB\u5373\u5904\u7406</button>
6340
+ <div class="content-panel glass">
6341
+ <div class="panel-header"><h2 class="panel-title">\u7CFB\u7EDF\u72B6\u6001</h2></div>
6342
+ <div id="system-status"></div>
6343
+ </div>
6344
+ <div class="content-panel glass mt-lg">
6345
+ <div class="panel-header"><h2 class="panel-title">\u5FEB\u6377\u64CD\u4F5C</h2></div>
6346
+ <div style="display: flex; flex-direction: column; gap: var(--space-sm);">
6347
+ <button class="btn btn-primary" id="btn-process-messages" onclick="processNow()">
6348
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>
6349
+ \u7ACB\u5373\u5904\u7406\u6D88\u606F
6350
+ </button>
6351
+ <button class="btn" onclick="navigateTo('settings')">
6352
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.6 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
6353
+ \u7CFB\u7EDF\u8BBE\u7F6E
6354
+ </button>
6355
+ </div>
6356
+ </div>
6357
+ <div class="content-panel glass mt-lg">
6358
+ <div class="panel-header"><h2 class="panel-title">RAG \u68C0\u7D22</h2></div>
6359
+ <div style="font-size: 13px; color: var(--text-secondary); line-height: 1.8;">
6360
+ <div style="display: flex; align-items: center; gap: var(--space-sm); margin-bottom: var(--space-sm);"><span class="tag tag-success">FTS5</span><span>\u5173\u952E\u8BCD\u68C0\u7D22</span></div>
6361
+ <div style="display: flex; align-items: center; gap: var(--space-sm); margin-bottom: var(--space-sm);"><span class="tag tag-info">\u5411\u91CF</span><span>\u8BED\u4E49\u68C0\u7D22</span></div>
6362
+ <div style="display: flex; align-items: center; gap: var(--space-sm);"><span class="tag tag-success">\u6DF7\u5408</span><span>Hybrid RAG</span></div>
6363
+ </div>
6037
6364
  </div>
6038
- <div id="action-status" class="status-line"></div>
6039
6365
  </div>
6040
- </header>
6366
+ </div>
6367
+ </div>
6041
6368
 
6042
- <div class="grid" id="metrics"></div>
6369
+ <div class="view" id="view-messages">
6370
+ <div class="section-header">
6371
+ <div><h1 class="section-title">\u6D88\u606F</h1><p class="page-subtitle">\u7FA4\u804A\u6D88\u606F\u5386\u53F2</p></div>
6372
+ <div class="search-box">
6373
+ <svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
6374
+ <input type="text" id="message-search" placeholder="\u641C\u7D22\u6D88\u606F..." oninput="filterMessages()" />
6375
+ </div>
6376
+ </div>
6377
+ <div class="content-panel glass"><div id="messages-list"></div></div>
6378
+ </div>
6043
6379
 
6044
- <div class="layout">
6045
- <div>
6046
- <section>
6047
- <h2>\u6700\u8FD1\u6D88\u606F</h2>
6048
- <div id="messages" class="empty">\u6B63\u5728\u8BFB\u53D6...</div>
6049
- </section>
6050
- <section>
6051
- <h2>\u4F1A\u8BDD\u8BB0\u5FC6</h2>
6052
- <div id="episodes" class="empty">\u6B63\u5728\u8BFB\u53D6...</div>
6053
- </section>
6054
- <section>
6055
- <h2>\u95EE\u7B54\u65E5\u5FD7</h2>
6056
- <div id="qa-logs" class="empty">\u6B63\u5728\u8BFB\u53D6...</div>
6057
- </section>
6380
+ <div class="view" id="view-episodes">
6381
+ <div class="section-header"><div><h1 class="section-title">\u4F1A\u8BDD\u8BB0\u5FC6</h1><p class="page-subtitle">\u81EA\u52A8\u805A\u5408\u7684\u804A\u5929\u7247\u6BB5</p></div></div>
6382
+ <div class="content-panel glass"><div id="episodes-list"></div></div>
6383
+ </div>
6384
+
6385
+ <div class="view" id="view-files">
6386
+ <div class="section-header"><div><h1 class="section-title">\u6587\u4EF6\u5E93</h1><p class="page-subtitle">\u5DF2\u5BFC\u5165\u7684\u6587\u4EF6\u77E5\u8BC6\u6E90</p></div></div>
6387
+ <div id="files-list"></div>
6388
+ </div>
6389
+
6390
+ <div class="view" id="view-tasks">
6391
+ <div class="section-header"><div><h1 class="section-title">\u4EFB\u52A1</h1><p class="page-subtitle">\u6587\u4EF6\u89E3\u6790\u4E0E\u5B9A\u65F6\u4EFB\u52A1</p></div></div>
6392
+ <div class="tabs" style="margin-bottom: var(--space-lg);">
6393
+ <button class="tab active" data-tab="file-jobs" onclick="switchTab('file-jobs')">\u6587\u4EF6\u89E3\u6790</button>
6394
+ <button class="tab" data-tab="cron-jobs" onclick="switchTab('cron-jobs')">\u5B9A\u65F6\u4EFB\u52A1</button>
6395
+ </div>
6396
+ <div class="content-panel glass" id="tab-file-jobs"><div id="file-jobs-list"></div></div>
6397
+ <div class="content-panel glass" id="tab-cron-jobs" style="display: none;"><div id="cron-jobs-list"></div></div>
6398
+ </div>
6399
+
6400
+ <div class="view" id="view-qa-logs">
6401
+ <div class="section-header"><div><h1 class="section-title">\u95EE\u7B54\u65E5\u5FD7</h1><p class="page-subtitle">\u95EE\u7B54\u5386\u53F2\u8BB0\u5F55</p></div></div>
6402
+ <div class="content-panel glass"><div id="qa-logs-list"></div></div>
6403
+ </div>
6404
+
6405
+ <div class="view" id="view-settings">
6406
+ <div class="section-header"><div><h1 class="section-title">\u8BBE\u7F6E</h1><p class="page-subtitle">\u7CFB\u7EDF\u914D\u7F6E\u4E0E\u64CD\u4F5C</p></div></div>
6407
+ <div class="settings-group glass" id="settings-config"></div>
6408
+ <div class="settings-group glass">
6409
+ <h3 style="font-size: 16px; font-weight: 600; margin-bottom: var(--space-md);">\u64CD\u4F5C</h3>
6410
+ <div style="display: flex; flex-direction: column; gap: var(--space-sm);">
6411
+ <button class="btn btn-primary" onclick="processNow()">
6412
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>
6413
+ \u7ACB\u5373\u5904\u7406\u6D88\u606F\u7D22\u5F15
6414
+ </button>
6415
+ <div style="font-size: 12px; color: var(--text-muted); padding: var(--space-sm); background: rgba(255,255,255,0.03); border-radius: var(--radius-sm);">
6416
+ \u8FD0\u884C CLI \u547D\u4EE4\u8FDB\u884C\u66F4\u591A\u64CD\u4F5C\uFF1A
6417
+ <div style="font-family: var(--font-mono); margin-top: var(--space-xs); line-height: 1.8;">
6418
+ chattercatcher settings<br/>
6419
+ chattercatcher doctor<br/>
6420
+ chattercatcher index rebuild<br/>
6421
+ chattercatcher files add &lt;path...&gt;<br/>
6422
+ chattercatcher export
6423
+ </div>
6424
+ </div>
6058
6425
  </div>
6059
- <aside>
6060
- <section>
6061
- <h2>\u7FA4\u804A</h2>
6062
- <div id="chats" class="empty">\u6B63\u5728\u8BFB\u53D6...</div>
6063
- </section>
6064
- <section>
6065
- <h2>\u6587\u4EF6\u5E93</h2>
6066
- <div id="files" class="empty">\u6B63\u5728\u8BFB\u53D6...</div>
6067
- </section>
6068
- <section>
6069
- <h2>\u89E3\u6790\u4EFB\u52A1</h2>
6070
- <div id="file-jobs" class="empty">\u6B63\u5728\u8BFB\u53D6...</div>
6071
- </section>
6072
- <section>
6073
- <h2>\u5B9A\u65F6\u4EFB\u52A1</h2>
6074
- <div id="cron-jobs" class="empty">\u6B63\u5728\u8BFB\u53D6...</div>
6075
- </section>
6076
- <section>
6077
- <h2>\u672C\u5730\u64CD\u4F5C</h2>
6078
- <p><code>chattercatcher settings</code> \u4FEE\u6539\u914D\u7F6E\u3002</p>
6079
- <p><code>chattercatcher files add &lt;path...&gt;</code> \u5BFC\u5165\u6587\u672C\u3001DOCX \u6216 PDF \u6587\u4EF6\u3002</p>
6080
- <p><code>chattercatcher doctor</code> \u68C0\u67E5\u98DE\u4E66\u3001\u6A21\u578B\u3001RAG \u548C\u672C\u5730\u5B58\u50A8\u3002</p>
6081
- </section>
6082
- </aside>
6083
6426
  </div>
6084
- </main>
6085
- <script>
6086
- const metrics = document.querySelector("#metrics");
6087
- const messages = document.querySelector("#messages");
6088
- const episodes = document.querySelector("#episodes");
6089
- const chats = document.querySelector("#chats");
6090
- const files = document.querySelector("#files");
6091
- const fileJobs = document.querySelector("#file-jobs");
6092
- const cronJobs = document.querySelector("#cron-jobs");
6093
- const qaLogs = document.querySelector("#qa-logs");
6094
- const processMessages = document.querySelector("#process-messages");
6095
- const actionStatus = document.querySelector("#action-status");
6096
-
6097
- let webActionToken = "__WEB_ACTION_TOKEN__";
6098
-
6099
- function fmt(value) {
6100
- return value == null || value === "" ? "-" : String(value);
6101
- }
6427
+ </div>
6428
+ </main>
6102
6429
 
6103
- function escapeHtml(value) {
6104
- return fmt(value)
6105
- .replaceAll("&", "&amp;")
6106
- .replaceAll("<", "&lt;")
6107
- .replaceAll(">", "&gt;")
6108
- .replaceAll('"', "&quot;");
6109
- }
6430
+ <div id="toast-container" style="position: fixed; top: 24px; right: 24px; z-index: 1001; display: flex; flex-direction: column; gap: 12px;"></div>
6110
6431
 
6111
- function isOpaqueId(value) {
6112
- return /^(ou|oc|om|cli|on|un|uid)_?[a-z0-9]+/i.test(fmt(value));
6113
- }
6432
+ <script>
6433
+ let currentView = "overview";
6434
+ let allMessages = [];
6435
+ let allEpisodes = [];
6436
+ let allFiles = [];
6437
+ let allFileJobs = [];
6438
+ let allCronJobs = [];
6439
+ let allQaLogs = [];
6440
+ let statusData = null;
6114
6441
 
6115
- function formatDateTime(value) {
6116
- const date = new Date(value);
6117
- if (Number.isNaN(date.getTime())) return fmt(value);
6118
- const pad = (input) => String(input).padStart(2, "0");
6119
- return [
6120
- date.getFullYear(),
6121
- pad(date.getMonth() + 1),
6122
- pad(date.getDate()),
6123
- ].join("/") + " " + [
6124
- pad(date.getHours()),
6125
- pad(date.getMinutes()),
6126
- pad(date.getSeconds()),
6127
- ].join(":");
6128
- }
6442
+ function fmt(value) { return value == null || value === "" ? "-" : String(value); }
6443
+ function escapeHtml(value) {
6444
+ return fmt(value).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
6445
+ }
6446
+ function isOpaqueId(value) { return /^(ou|oc|om|cli|on|un|uid)_?[a-z0-9]+/i.test(fmt(value)); }
6447
+ function formatDateTime(value) {
6448
+ var date = new Date(value);
6449
+ if (Number.isNaN(date.getTime())) return fmt(value);
6450
+ var pad = function(n) { return String(n).padStart(2, "0"); };
6451
+ return date.getFullYear() + "/" + pad(date.getMonth()+1) + "/" + pad(date.getDate()) + " " + pad(date.getHours()) + ":" + pad(date.getMinutes());
6452
+ }
6453
+ function displaySender(value) { return isOpaqueId(value) ? "\u7FA4\u6210\u5458" : fmt(value); }
6454
+ function displayChatName(value, platform) { return !isOpaqueId(value) ? fmt(value) : (platform === "feishu" ? "\u98DE\u4E66\u7FA4\u804A" : "\u7FA4\u804A"); }
6129
6455
 
6130
- function displaySender(value) {
6131
- return isOpaqueId(value) ? "\u7FA4\u6210\u5458" : fmt(value);
6132
- }
6456
+ function showToast(message, type) {
6457
+ type = type || "info";
6458
+ var container = document.getElementById("toast-container");
6459
+ var toast = document.createElement("div");
6460
+ toast.className = "toast toast-" + type;
6461
+ toast.textContent = message;
6462
+ container.appendChild(toast);
6463
+ setTimeout(function() {
6464
+ toast.style.opacity = "0"; toast.style.transform = "translateX(10px)";
6465
+ setTimeout(function() { toast.remove(); }, 300);
6466
+ }, 3000);
6467
+ }
6133
6468
 
6134
- function displayChatName(value, platform) {
6135
- if (!isOpaqueId(value)) return fmt(value);
6136
- return platform === "feishu" ? "\u98DE\u4E66\u7FA4\u804A" : "\u7FA4\u804A";
6137
- }
6469
+ function navigateTo(view) {
6470
+ document.querySelectorAll(".view").forEach(function(el) { el.classList.remove("active"); });
6471
+ document.querySelectorAll(".nav-item, .mobile-nav-item").forEach(function(el) { el.classList.remove("active"); });
6472
+ document.getElementById("view-" + view).classList.add("active");
6473
+ document.querySelectorAll('[data-view="' + view + '"]').forEach(function(el) { el.classList.add("active"); });
6474
+ currentView = view;
6475
+ window.scrollTo(0, 0);
6476
+ if (view === "messages") renderMessagesView();
6477
+ if (view === "episodes") renderEpisodesView();
6478
+ if (view === "files") renderFilesView();
6479
+ if (view === "tasks") renderTasksView();
6480
+ if (view === "qa-logs") renderQaLogsView();
6481
+ }
6138
6482
 
6139
- function formatGatewayValue(gateway) {
6140
- if (gateway.connection === "running") return "\u8FD0\u884C\u4E2D";
6141
- if (!gateway.configured) return "\u672A\u914D\u7F6E";
6142
- return "\u5F85\u542F\u52A8";
6143
- }
6483
+ document.querySelectorAll(".nav-item, .mobile-nav-item").forEach(function(el) {
6484
+ el.addEventListener("click", function() { navigateTo(el.dataset.view); });
6485
+ });
6144
6486
 
6145
- function formatGatewayNote(gateway) {
6146
- if (gateway.connection === "running" && gateway.pid) return "PID " + gateway.pid;
6147
- return "\u98DE\u4E66\u957F\u8FDE\u63A5";
6148
- }
6487
+ function switchTab(tab) {
6488
+ document.querySelectorAll(".tab").forEach(function(el) { el.classList.remove("active"); });
6489
+ document.querySelector('[data-tab="' + tab + '"]').classList.add("active");
6490
+ document.getElementById("tab-file-jobs").style.display = tab === "file-jobs" ? "block" : "none";
6491
+ document.getElementById("tab-cron-jobs").style.display = tab === "cron-jobs" ? "block" : "none";
6492
+ if (tab === "file-jobs") renderFileJobs();
6493
+ if (tab === "cron-jobs") renderCronJobs();
6494
+ }
6149
6495
 
6150
- function renderMetrics(status) {
6151
- const gatewayClass = status.gateway.configured ? "status-ok" : "status-warn";
6152
- metrics.innerHTML = [
6153
- ["Gateway", formatGatewayValue(status.gateway), formatGatewayNote(status.gateway), gatewayClass],
6154
- ["\u7248\u672C", status.version || "unknown", "\u5F53\u524D\u8FD0\u884C\u7248\u672C", ""],
6155
- ["\u7FA4\u804A", status.data.chats, "\u672C\u5730\u7FA4\u804A\u6570", ""],
6156
- ["\u6D88\u606F", status.data.messages, "\u5DF2\u5165\u5E93\u6D88\u606F", ""],
6157
- ["\u4F1A\u8BDD\u8BB0\u5FC6", status.data.episodes, "\u5DF2\u751F\u6210\u6458\u8981", ""],
6158
- ["\u6587\u4EF6", status.data.files, "\u6587\u4EF6\u77E5\u8BC6\u6E90", ""],
6159
- ].map(([label, value, note, extra]) => \`
6160
- <div class="metric">
6161
- <div class="label">\${escapeHtml(label)}</div>
6162
- <div class="value \${extra}">\${escapeHtml(value)}</div>
6163
- <div class="note">\${escapeHtml(note)}</div>
6164
- </div>
6165
- \`).join("");
6496
+ async function fetchJson(path) {
6497
+ var response = await fetch(path);
6498
+ if (!response.ok) {
6499
+ var body = await response.text();
6500
+ throw new Error(path + " " + response.status + " " + body);
6166
6501
  }
6502
+ return response.json();
6503
+ }
6167
6504
 
6168
- function renderMessages(items) {
6169
- if (items.length === 0) {
6170
- messages.className = "empty";
6171
- messages.textContent = "\u8FD8\u6CA1\u6709\u6D88\u606F\u3002\u542F\u52A8 Gateway \u540E\uFF0C\u7FA4\u804A\u6587\u672C\u4F1A\u8FDB\u5165\u672C\u5730 RAG \u7D22\u5F15\u3002";
6172
- return;
6173
- }
6174
- messages.className = "";
6175
- messages.innerHTML = \`
6176
- <div class="message-list">
6177
- \${items.map((item) => \`
6178
- <article class="message-item">
6179
- <div class="message-meta">
6180
- <span>\${escapeHtml(formatDateTime(item.sentAt))}</span>
6181
- <span>\${escapeHtml(displaySender(item.senderName))}</span>
6182
- <span>\${escapeHtml(displayChatName(item.chatName, item.platform))}</span>
6183
- </div>
6184
- <div class="message-body">\${escapeHtml(item.text)}</div>
6185
- </article>
6186
- \`).join("")}
6187
- </div>
6188
- \`;
6505
+ async function postJson(path, options) {
6506
+ var response = await fetch(path, Object.assign({ method: "POST" }, options || {}));
6507
+ var result = await response.json();
6508
+ if (!response.ok) {
6509
+ throw new Error(result.message || result.reason || "\u8BF7\u6C42\u5931\u8D25");
6189
6510
  }
6511
+ return result;
6512
+ }
6190
6513
 
6191
- function renderEpisodes(items) {
6192
- if (items.length === 0) {
6193
- episodes.className = "empty";
6194
- episodes.textContent = "\u8FD8\u6CA1\u6709\u4F1A\u8BDD\u8BB0\u5FC6\u3002\u9ED8\u8BA4\u5728 10 \u5206\u949F\u7A97\u53E3\u9759\u9ED8 2 \u5206\u949F\u540E\u751F\u6210\uFF0C\u4E5F\u53EF\u4EE5\u8FD0\u884C chattercatcher process episodes \u624B\u52A8\u89E6\u53D1\u3002";
6195
- return;
6196
- }
6197
- episodes.className = "";
6198
- episodes.innerHTML = \`
6199
- <div class="message-list">
6200
- \${items.map((item) => \`
6201
- <article class="message-item">
6202
- <div class="message-meta">
6203
- <span>\${escapeHtml(formatDateTime(item.startedAt))} - \${escapeHtml(formatDateTime(item.endedAt))}</span>
6204
- <span>\${escapeHtml(displayChatName(item.chatName, "feishu"))}</span>
6205
- <span>\${escapeHtml(item.messageCount)} \u6761\u6D88\u606F</span>
6206
- </div>
6207
- <div class="message-body">\${escapeHtml(item.summary)}</div>
6208
- </article>
6209
- \`).join("")}
6210
- </div>
6211
- \`;
6514
+ async function deleteJson(path) {
6515
+ var response = await fetch(path, { method: "DELETE" });
6516
+ var result = await response.json();
6517
+ if (!response.ok) {
6518
+ throw new Error(result.message || result.reason || "\u8BF7\u6C42\u5931\u8D25");
6212
6519
  }
6520
+ return result;
6521
+ }
6213
6522
 
6214
- function renderChats(items) {
6215
- if (items.length === 0) {
6216
- chats.className = "empty";
6217
- chats.textContent = "\u8FD8\u6CA1\u6709\u7FA4\u804A\u8BB0\u5F55\u3002";
6218
- return;
6219
- }
6220
- chats.className = "";
6221
- chats.innerHTML = \`
6222
- <table>
6223
- <thead><tr><th>\u540D\u79F0</th><th>\u5E73\u53F0</th></tr></thead>
6224
- <tbody>
6225
- \${items.map((item) => \`
6226
- <tr>
6227
- <td><span class="id-text" title="\${escapeHtml(item.name)}">\${escapeHtml(displayChatName(item.name, item.platform))}</span></td>
6228
- <td>\${escapeHtml(item.platform)}</td>
6229
- </tr>
6230
- \`).join("")}
6231
- </tbody>
6232
- </table>
6233
- \`;
6234
- }
6523
+ function renderMetrics(status) {
6524
+ var gatewayClass = status.gateway.configured ? "status-dot online" : "status-dot offline";
6525
+ var gatewayText = status.gateway.connection === "running" ? "\u8FD0\u884C\u4E2D" : (!status.gateway.configured ? "\u672A\u914D\u7F6E" : "\u5F85\u542F\u52A8");
6526
+ var metricsHtml = [
6527
+ ["Gateway", gatewayText, "\u98DE\u4E66\u957F\u8FDE\u63A5", gatewayClass],
6528
+ ["\u7248\u672C", status.version || "unknown", "\u5F53\u524D\u8FD0\u884C\u7248\u672C", ""],
6529
+ ["\u7FA4\u804A", status.data.chats, "\u672C\u5730\u7FA4\u804A\u6570", ""],
6530
+ ["\u6D88\u606F", status.data.messages, "\u5DF2\u5165\u5E93\u6D88\u606F", ""],
6531
+ ["\u4F1A\u8BDD\u8BB0\u5FC6", status.data.episodes, "\u5DF2\u751F\u6210\u6458\u8981", ""],
6532
+ ["\u6587\u4EF6", status.data.files, "\u6587\u4EF6\u77E5\u8BC6\u6E90", ""],
6533
+ ["\u95EE\u7B54", status.data.qaLogs, "\u95EE\u7B54\u8BB0\u5F55", ""],
6534
+ ["\u4EFB\u52A1", status.data.cronJobs, "\u5B9A\u65F6\u4EFB\u52A1", ""]
6535
+ ].map(function(item) {
6536
+ var label = item[0], value = item[1], note = item[2], dotClass = item[3];
6537
+ return '<div class="metric-card glass"><div class="metric-label">' + escapeHtml(label) + '</div>' +
6538
+ '<div class="metric-value">' + (dotClass ? '<span class="' + dotClass + '" style="margin-right:8px;"></span>' : '') + escapeHtml(value) + '</div>' +
6539
+ '<div class="metric-note">' + escapeHtml(note) + '</div></div>';
6540
+ }).join("");
6541
+ document.getElementById("metrics").innerHTML = metricsHtml;
6542
+ document.getElementById("gateway-indicator").className = gatewayClass;
6543
+ document.getElementById("gateway-status-text").textContent = "Gateway " + gatewayText;
6544
+ document.getElementById("version-text").textContent = "v" + (status.version || "unknown");
6545
+ }
6235
6546
 
6236
- function renderFiles(items) {
6237
- if (items.length === 0) {
6238
- files.className = "empty";
6239
- files.textContent = "\u8FD8\u6CA1\u6709\u6587\u4EF6\u3002\u53EF\u5148\u8FD0\u884C chattercatcher files add <path...> \u5BFC\u5165\u6587\u672C\u3001DOCX \u6216 PDF \u6587\u4EF6\u3002";
6240
- return;
6241
- }
6242
- files.className = "";
6243
- files.innerHTML = \`
6244
- <table>
6245
- <thead><tr><th>\u6587\u4EF6</th><th>\u89E3\u6790\u5668</th><th>\u5B57\u7B26</th></tr></thead>
6246
- <tbody>
6247
- \${items.map((item) => \`
6248
- <tr>
6249
- <td>
6250
- <div>\${escapeHtml(item.fileName)}</div>
6251
- <div class="path" title="\${escapeHtml(item.storedPath)}">\${escapeHtml(item.storedPath)}</div>
6252
- </td>
6253
- <td>\${escapeHtml(item.parser || "unknown")}</td>
6254
- <td>\${escapeHtml(item.characters)}</td>
6255
- </tr>
6256
- \`).join("")}
6257
- </tbody>
6258
- </table>
6259
- \`;
6547
+ function renderSystemStatus(status) {
6548
+ var gateway = status.gateway;
6549
+ var html = '<div style="display:flex;flex-direction:column;gap:var(--space-md);">';
6550
+ html += '<div class="settings-item"><div><div class="settings-label">Gateway</div></div><div class="settings-value">' + (gateway.connection === "running" ? '<span class="tag tag-success">\u8FD0\u884C\u4E2D</span>' : '<span class="tag tag-warning">\u672A\u8FD0\u884C</span>') + '</div></div>';
6551
+ html += '<div class="settings-item"><div><div class="settings-label">Web UI</div></div><div class="settings-value">' + escapeHtml((status.web && status.web.host ? status.web.host : "127.0.0.1") + ":" + (status.web && status.web.port ? status.web.port : "3878")) + '</div></div>';
6552
+ html += '<div class="settings-item"><div><div class="settings-label">RAG \u6A21\u5F0F</div></div><div class="settings-value"><span class="tag tag-success">\u5F3A\u5236\u68C0\u7D22</span></div></div>';
6553
+ html += '<div class="settings-item"><div><div class="settings-label">\u5173\u952E\u8BCD\u68C0\u7D22</div></div><div class="settings-value">SQLite FTS5</div></div>';
6554
+ html += '<div class="settings-item"><div><div class="settings-label">\u5411\u91CF\u68C0\u7D22</div></div><div class="settings-value">SQLite embedding</div></div>';
6555
+ html += '</div>';
6556
+ document.getElementById("system-status").innerHTML = html;
6557
+ }
6558
+
6559
+ function renderRecentMessages(items) {
6560
+ var el = document.getElementById("recent-messages");
6561
+ if (!items || items.length === 0) {
6562
+ el.innerHTML = '<div class="empty-state">\u8FD8\u6CA1\u6709\u6D88\u606F\u3002\u542F\u52A8 Gateway \u540E\uFF0C\u7FA4\u804A\u6587\u672C\u4F1A\u8FDB\u5165\u672C\u5730 RAG \u7D22\u5F15\u3002</div>';
6563
+ return;
6564
+ }
6565
+ var html = '<div class="message-list">';
6566
+ for (var i = 0; i < Math.min(items.length, 5); i++) {
6567
+ var item = items[i];
6568
+ html += '<div class="message-card"><div class="message-meta">' +
6569
+ '<span>' + escapeHtml(formatDateTime(item.sentAt)) + '</span>' +
6570
+ '<span>' + escapeHtml(displaySender(item.senderName)) + '</span>' +
6571
+ '<span>' + escapeHtml(displayChatName(item.chatName, item.platform)) + '</span>' +
6572
+ '</div><div class="message-text">' + escapeHtml(item.text) + '</div></div>';
6260
6573
  }
6574
+ html += '</div>';
6575
+ el.innerHTML = html;
6576
+ }
6261
6577
 
6262
- function renderFileJobs(items) {
6263
- if (items.length === 0) {
6264
- fileJobs.className = "empty";
6265
- fileJobs.textContent = "\u8FD8\u6CA1\u6709\u6587\u4EF6\u89E3\u6790\u4EFB\u52A1\u3002";
6266
- return;
6267
- }
6268
- fileJobs.className = "";
6269
- fileJobs.innerHTML = \`
6270
- <table>
6271
- <thead><tr><th>\u4EFB\u52A1</th><th>\u72B6\u6001</th></tr></thead>
6272
- <tbody>
6273
- \${items.map((item) => \`
6274
- <tr>
6275
- <td>
6276
- <div>\${escapeHtml(item.fileName)}</div>
6277
- <div class="path" title="\${escapeHtml(item.id)}">ID: \${escapeHtml(item.id)}</div>
6278
- <div class="path" title="\${escapeHtml(item.error || item.storedPath)}">\${escapeHtml(item.error || item.storedPath)}</div>
6279
- </td>
6280
- <td>\${escapeHtml(item.status)}</td>
6281
- </tr>
6282
- \`).join("")}
6283
- </tbody>
6284
- </table>
6285
- \`;
6578
+ function renderRecentEpisodes(items) {
6579
+ var el = document.getElementById("recent-episodes");
6580
+ if (!items || items.length === 0) {
6581
+ el.innerHTML = '<div class="empty-state">\u8FD8\u6CA1\u6709\u4F1A\u8BDD\u8BB0\u5FC6\u3002</div>';
6582
+ return;
6583
+ }
6584
+ var html = '<div class="message-list">';
6585
+ for (var i = 0; i < Math.min(items.length, 3); i++) {
6586
+ var item = items[i];
6587
+ html += '<div class="episode-card"><div class="message-meta">' +
6588
+ '<span>' + escapeHtml(formatDateTime(item.startedAt)) + " - " + escapeHtml(formatDateTime(item.endedAt)) + '</span>' +
6589
+ '<span>' + escapeHtml(item.messageCount) + ' \u6761\u6D88\u606F</span>' +
6590
+ '</div><div class="message-text">' + escapeHtml(item.summary) + '</div></div>';
6286
6591
  }
6592
+ html += '</div>';
6593
+ el.innerHTML = html;
6594
+ }
6287
6595
 
6288
- function renderCronJobs(items) {
6289
- if (items.length === 0) {
6290
- cronJobs.className = "empty";
6291
- cronJobs.textContent = "\u8FD8\u6CA1\u6709\u5B9A\u65F6\u4EFB\u52A1\u3002\u53EF\u5728\u98DE\u4E66\u7FA4\u91CC @ \u673A\u5668\u4EBA\u521B\u5EFA\u3002";
6292
- return;
6293
- }
6294
- cronJobs.className = "";
6295
- cronJobs.innerHTML = \`
6296
- <table>
6297
- <thead><tr><th>\u4EFB\u52A1</th><th>\u72B6\u6001</th></tr></thead>
6298
- <tbody>
6299
- \${items.map((item) => \`
6300
- <tr>
6301
- <td>
6302
- <div>\${escapeHtml(item.schedule)}</div>
6303
- <div class="message" title="\${escapeHtml(item.prompt)}">\${escapeHtml(item.prompt)}</div>
6304
- <div class="path" title="\${escapeHtml(item.id)}">ID: \${escapeHtml(item.id)}</div>
6305
- <div class="path" title="\${escapeHtml(item.chatId)}">\u7FA4: \${escapeHtml(item.chatId)}</div>
6306
- <div class="path">\u4E0B\u6B21: \${escapeHtml(formatDateTime(item.nextRunAt))}</div>
6307
- <div class="path" title="\${escapeHtml(item.lastError || "")}">\${escapeHtml(item.lastError || "")}</div>
6308
- \${item.status === "active" ? \`<button type="button" data-delete-cron-job="\${escapeHtml(item.id)}">\u5220\u9664</button>\` : ""}
6309
- </td>
6310
- <td>\${escapeHtml(item.status)}</td>
6311
- </tr>
6312
- \`).join("")}
6313
- </tbody>
6314
- </table>
6315
- \`;
6596
+ function renderMessagesView() {
6597
+ var el = document.getElementById("messages-list");
6598
+ if (!allMessages || allMessages.length === 0) {
6599
+ el.innerHTML = '<div class="empty-state">\u8FD8\u6CA1\u6709\u6D88\u606F\u3002</div>';
6600
+ return;
6316
6601
  }
6602
+ var searchInput = document.getElementById("message-search");
6603
+ var searchTerm = searchInput ? searchInput.value.toLowerCase() : "";
6604
+ var filtered = searchTerm ? allMessages.filter(function(m) { return (m.text || "").toLowerCase().indexOf(searchTerm) !== -1; }) : allMessages;
6605
+ if (filtered.length === 0) {
6606
+ el.innerHTML = '<div class="empty-state">\u6CA1\u6709\u627E\u5230\u5339\u914D\u7684\u6D88\u606F\u3002</div>';
6607
+ return;
6608
+ }
6609
+ var html = '<div class="message-list">';
6610
+ for (var i = 0; i < Math.min(filtered.length, 50); i++) {
6611
+ var item = filtered[i];
6612
+ html += '<div class="message-card"><div class="message-meta">' +
6613
+ '<span>' + escapeHtml(formatDateTime(item.sentAt)) + '</span>' +
6614
+ '<span>' + escapeHtml(displaySender(item.senderName)) + '</span>' +
6615
+ '<span>' + escapeHtml(displayChatName(item.chatName, item.platform)) + '</span>' +
6616
+ '</div><div class="message-text" style="-webkit-line-clamp:4;">' + escapeHtml(item.text) + '</div></div>';
6617
+ }
6618
+ html += '</div>';
6619
+ if (filtered.length > 50) {
6620
+ html += '<div style="text-align:center;padding:var(--space-md);color:var(--text-muted);font-size:13px;">\u8FD8\u6709 ' + (filtered.length - 50) + ' \u6761\u6D88\u606F...</div>';
6621
+ }
6622
+ el.innerHTML = html;
6623
+ }
6317
6624
 
6318
- function renderQaLogs(items) {
6319
- if (items.length === 0) {
6320
- qaLogs.className = "empty";
6321
- qaLogs.textContent = "\u8FD8\u6CA1\u6709\u95EE\u7B54\u65E5\u5FD7\u3002";
6322
- return;
6323
- }
6324
- qaLogs.className = "";
6325
- const rows = items.map((item) => {
6326
- const citationCount = Array.isArray(item.citations) ? item.citations.length : 0;
6327
- return [
6328
- '<article class="message-item">',
6329
- ' <div class="message-meta">',
6330
- " <span>" + escapeHtml(formatDateTime(item.createdAt)) + "</span>",
6331
- " <span>" + escapeHtml(item.status) + "</span>",
6332
- " <span>" + escapeHtml(citationCount) + " \u6761\u5F15\u7528</span>",
6333
- " </div>",
6334
- " <div class=\\"message-body\\"><strong>\u95EE\uFF1A</strong>" + escapeHtml(item.question) + "</div>",
6335
- " <div class=\\"message-body\\"><strong>\u7B54\uFF1A</strong>" + escapeHtml(item.answer) + "</div>",
6336
- "</article>",
6337
- ].join("\\n");
6338
- });
6339
- qaLogs.innerHTML = [
6340
- '<div class="message-list">',
6341
- rows.join(""),
6342
- "</div>",
6343
- ].join("\\n");
6625
+ function filterMessages() { renderMessagesView(); }
6626
+
6627
+ function renderEpisodesView() {
6628
+ var el = document.getElementById("episodes-list");
6629
+ if (!allEpisodes || allEpisodes.length === 0) {
6630
+ el.innerHTML = '<div class="empty-state">\u8FD8\u6CA1\u6709\u4F1A\u8BDD\u8BB0\u5FC6\u3002</div>';
6631
+ return;
6632
+ }
6633
+ var html = '<div class="timeline">';
6634
+ for (var i = 0; i < allEpisodes.length; i++) {
6635
+ var item = allEpisodes[i];
6636
+ html += '<div class="timeline-item"><div class="timeline-date">' + escapeHtml(formatDateTime(item.startedAt)) + " - " + escapeHtml(formatDateTime(item.endedAt)) + " \xB7 " + escapeHtml(item.messageCount) + ' \u6761\u6D88\u606F</div><div class="timeline-content">' + escapeHtml(item.summary) + '</div></div>';
6344
6637
  }
6638
+ html += '</div>';
6639
+ el.innerHTML = html;
6640
+ }
6345
6641
 
6346
- async function fetchJson(path) {
6347
- const response = await fetch(path);
6348
- if (!response.ok) {
6349
- const body = await response.text();
6350
- throw new Error(path + " " + response.status + " " + body);
6351
- }
6352
- return response.json();
6642
+ function renderFilesView() {
6643
+ var el = document.getElementById("files-list");
6644
+ if (!allFiles || allFiles.length === 0) {
6645
+ el.innerHTML = '<div class="content-panel glass"><div class="empty-state">\u8FD8\u6CA1\u6709\u6587\u4EF6\u3002\u8FD0\u884C <code>chattercatcher files add &lt;path...&gt;</code> \u5BFC\u5165\u6587\u4EF6\u3002</div></div>';
6646
+ return;
6353
6647
  }
6648
+ var html = '<div class="grid-2">';
6649
+ for (var i = 0; i < allFiles.length; i++) {
6650
+ var item = allFiles[i];
6651
+ html += '<div class="file-card glass"><div class="file-icon">' +
6652
+ '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>' +
6653
+ '</div><div style="font-weight:600;margin-bottom:4px;">' + escapeHtml(item.fileName) + '</div>' +
6654
+ '<div style="font-size:13px;color:var(--text-muted);margin-bottom:4px;" class="truncate">' + escapeHtml(item.storedPath) + '</div>' +
6655
+ '<div style="display:flex;gap:var(--space-sm);"><span class="tag">' + escapeHtml(item.parser || "unknown") + '</span><span class="tag">' + escapeHtml(item.characters) + ' \u5B57\u7B26</span></div></div>';
6656
+ }
6657
+ html += '</div>';
6658
+ el.innerHTML = html;
6659
+ }
6660
+
6661
+ function renderTasksView() {
6662
+ var activeTab = document.querySelector(".tab.active");
6663
+ var tab = activeTab ? activeTab.dataset.tab : "file-jobs";
6664
+ if (tab === "file-jobs") renderFileJobs();
6665
+ else renderCronJobs();
6666
+ }
6354
6667
 
6355
- function renderLoadError(element, error) {
6356
- element.className = "empty";
6357
- element.textContent = "\u52A0\u8F7D\u5931\u8D25\uFF1A" + (error instanceof Error ? error.message : String(error));
6668
+ function renderFileJobs() {
6669
+ var el = document.getElementById("file-jobs-list");
6670
+ if (!allFileJobs || allFileJobs.length === 0) {
6671
+ el.innerHTML = '<div class="empty-state">\u8FD8\u6CA1\u6709\u6587\u4EF6\u89E3\u6790\u4EFB\u52A1\u3002</div>';
6672
+ return;
6673
+ }
6674
+ var html = '<table class="data-table"><thead><tr><th>\u6587\u4EF6</th><th>\u72B6\u6001</th><th>\u4FE1\u606F</th></tr></thead><tbody>';
6675
+ for (var i = 0; i < allFileJobs.length; i++) {
6676
+ var item = allFileJobs[i];
6677
+ var tagClass = item.status === 'indexed' ? 'tag-success' : item.status === 'failed' ? 'tag-error' : 'tag-warning';
6678
+ html += '<tr><td><div style="font-weight:500;">' + escapeHtml(item.fileName) + '</div><div style="font-size:12px;color:var(--text-muted);" class="truncate">' + escapeHtml(item.storedPath || item.id) + '</div></td>' +
6679
+ '<td><span class="tag ' + tagClass + '">' + escapeHtml(item.status) + '</span></td>' +
6680
+ '<td style="font-size:13px;color:var(--text-muted);">' + escapeHtml(item.error || "") + '</td></tr>';
6358
6681
  }
6682
+ html += '</tbody></table>';
6683
+ el.innerHTML = html;
6684
+ }
6359
6685
 
6360
- async function loadSection(path, element, render) {
6361
- try {
6362
- render(await fetchJson(path));
6363
- } catch (error) {
6364
- renderLoadError(element, error);
6365
- }
6686
+ function renderCronJobs() {
6687
+ var el = document.getElementById("cron-jobs-list");
6688
+ if (!allCronJobs || allCronJobs.length === 0) {
6689
+ el.innerHTML = '<div class="empty-state">\u8FD8\u6CA1\u6709\u5B9A\u65F6\u4EFB\u52A1\u3002</div>';
6690
+ return;
6691
+ }
6692
+ var html = '<table class="data-table"><thead><tr><th>\u4EFB\u52A1</th><th>\u72B6\u6001</th><th>\u64CD\u4F5C</th></tr></thead><tbody>';
6693
+ for (var i = 0; i < allCronJobs.length; i++) {
6694
+ var item = allCronJobs[i];
6695
+ var tagClass = item.status === 'active' ? 'tag-success' : 'tag-warning';
6696
+ html += '<tr><td><div style="font-weight:500;">' + escapeHtml(item.schedule) + '</div>' +
6697
+ '<div style="font-size:13px;color:var(--text-muted);" class="truncate-2">' + escapeHtml(item.prompt) + '</div>' +
6698
+ '<div style="font-size:12px;color:var(--text-muted);">\u4E0B\u6B21: ' + escapeHtml(formatDateTime(item.nextRunAt)) + '</div>' +
6699
+ (item.lastError ? '<div style="font-size:12px;color:var(--danger);margin-top:4px;">' + escapeHtml(item.lastError) + '</div>' : '') +
6700
+ '</td><td><span class="tag ' + tagClass + '">' + escapeHtml(item.status) + '</span></td><td>' +
6701
+ (item.status === "active" ? '<button class="btn btn-sm btn-danger" data-delete-cron-job="' + escapeHtml(item.id) + '">\u5220\u9664</button>' : '-') +
6702
+ '</td></tr>';
6366
6703
  }
6704
+ html += '</tbody></table>';
6705
+ el.innerHTML = html;
6706
+ }
6367
6707
 
6368
- async function load() {
6369
- await Promise.all([
6370
- loadSection("/api/status", metrics, renderMetrics),
6371
- loadSection("/api/messages/recent?limit=20", messages, (data) => renderMessages(data.items)),
6372
- loadSection("/api/episodes?limit=10", episodes, (data) => renderEpisodes(data.items)),
6373
- loadSection("/api/chats", chats, (data) => renderChats(data.items)),
6374
- loadSection("/api/files", files, (data) => renderFiles(data.items)),
6375
- loadSection("/api/file-jobs", fileJobs, (data) => renderFileJobs(data.items)),
6376
- loadSection("/api/qa-logs?limit=10", qaLogs, (data) => renderQaLogs(data.items)),
6377
- loadSection("/api/cron-jobs", cronJobs, (data) => renderCronJobs(data.items)),
6378
- ]);
6708
+ function renderQaLogsView() {
6709
+ var el = document.getElementById("qa-logs-list");
6710
+ if (!allQaLogs || allQaLogs.length === 0) {
6711
+ el.innerHTML = '<div class="empty-state">\u8FD8\u6CA1\u6709\u95EE\u7B54\u65E5\u5FD7\u3002</div>';
6712
+ return;
6379
6713
  }
6714
+ var html = '';
6715
+ for (var i = 0; i < allQaLogs.length; i++) {
6716
+ var item = allQaLogs[i];
6717
+ var citationCount = Array.isArray(item.citations) ? item.citations.length : 0;
6718
+ var statusClass = item.status === 'success' ? 'tag-success' : 'tag-warning';
6719
+ html += '<div class="qa-card"><div class="message-meta" style="margin-bottom:var(--space-sm);">' +
6720
+ '<span>' + escapeHtml(formatDateTime(item.createdAt)) + '</span>' +
6721
+ '<span class="tag ' + statusClass + '">' + escapeHtml(item.status) + '</span>' +
6722
+ '<span>' + citationCount + ' \u6761\u5F15\u7528</span></div>' +
6723
+ '<div class="qa-question">' + escapeHtml(item.question) + '</div>' +
6724
+ '<div class="qa-answer">' + escapeHtml(item.answer) + '</div></div>';
6725
+ }
6726
+ el.innerHTML = html;
6727
+ }
6380
6728
 
6381
- async function processNow() {
6382
- processMessages.disabled = true;
6383
- actionStatus.textContent = "\u6B63\u5728\u5904\u7406\u6D88\u606F\u7D22\u5F15...";
6384
- try {
6385
- const response = await fetch("/api/process/messages", {
6386
- method: "POST",
6387
- headers: { "x-chattercatcher-web-token": webActionToken },
6388
- });
6389
- const result = await response.json();
6390
- if (!response.ok) {
6391
- actionStatus.textContent = result.message || "\u5904\u7406\u5931\u8D25\u3002";
6392
- return;
6393
- }
6729
+ function renderSettings(status) {
6730
+ var el = document.getElementById("settings-config");
6731
+ var html = '<h3 style="font-size:16px;font-weight:600;margin-bottom:var(--space-md);">\u7CFB\u7EDF\u914D\u7F6E</h3>';
6732
+ html += '<div style="display:flex;flex-direction:column;">';
6733
+ html += '<div class="settings-item"><div><div class="settings-label">Web UI</div><div class="settings-desc">' + escapeHtml((status.web && status.web.host ? status.web.host : "127.0.0.1") + ":" + (status.web && status.web.port ? status.web.port : "3878")) + '</div></div></div>';
6734
+ html += '<div class="settings-item"><div><div class="settings-label">Gateway</div><div class="settings-desc">' + (status.gateway.configured ? "\u5DF2\u914D\u7F6E" : "\u672A\u914D\u7F6E") + '</div></div></div>';
6735
+ html += '<div class="settings-item"><div><div class="settings-label">RAG \u6A21\u5F0F</div><div class="settings-desc">\u5F3A\u5236\u5148\u68C0\u7D22\u8BC1\u636E\uFF0C\u7981\u6B62\u5168\u91CF\u4E0A\u4E0B\u6587\u5806\u53E0</div></div></div>';
6736
+ html += '<div class="settings-item"><div><div class="settings-label">\u6570\u636E\u76EE\u5F55</div><div class="settings-desc">SQLite + \u672C\u5730\u6587\u4EF6</div></div></div>';
6737
+ html += '</div>';
6738
+ el.innerHTML = html;
6739
+ }
6394
6740
 
6395
- if (result.status === "skipped") {
6396
- actionStatus.textContent = result.reason;
6397
- } else {
6398
- actionStatus.textContent = \`\u5904\u7406\u5B8C\u6210\uFF1Achunks=\${result.chunks}, vectors=\${result.vectors}\`;
6399
- }
6400
- await load();
6401
- } catch (error) {
6402
- actionStatus.textContent = error instanceof Error ? error.message : String(error);
6403
- } finally {
6404
- processMessages.disabled = false;
6405
- }
6406
- }
6741
+ async function loadSection(path, setter) {
6742
+ try { setter(await fetchJson(path)); }
6743
+ catch (error) { console.error("\u52A0\u8F7D\u5931\u8D25:", path, error); }
6744
+ }
6407
6745
 
6408
- document.addEventListener("click", async (event) => {
6409
- const target = event.target;
6410
- if (!(target instanceof HTMLElement)) return;
6411
- const id = target.dataset.deleteCronJob;
6412
- if (!id) return;
6413
- target.setAttribute("disabled", "disabled");
6414
- actionStatus.textContent = "\u6B63\u5728\u5220\u9664\u5B9A\u65F6\u4EFB\u52A1...";
6415
- try {
6416
- const response = await fetch(\`/api/cron-jobs/\${encodeURIComponent(id)}\`, {
6417
- method: "DELETE",
6418
- headers: { "x-chattercatcher-web-token": webActionToken },
6419
- });
6420
- const result = await response.json();
6421
- actionStatus.textContent = result.ok ? "\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u5220\u9664\u3002" : result.message || "\u5220\u9664\u5931\u8D25\u3002";
6422
- await load();
6423
- } catch (error) {
6424
- actionStatus.textContent = error instanceof Error ? error.message : String(error);
6425
- } finally {
6426
- target.removeAttribute("disabled");
6427
- }
6746
+ async function load() {
6747
+ await loadSection("/api/status", function(data) {
6748
+ statusData = data;
6749
+ renderMetrics(data);
6750
+ renderSystemStatus(data);
6751
+ renderSettings(data);
6428
6752
  });
6753
+ await loadSection("/api/messages/recent?limit=50", function(data) { allMessages = data.items || []; renderRecentMessages(allMessages); });
6754
+ await loadSection("/api/episodes?limit=20", function(data) { allEpisodes = data.items || []; renderRecentEpisodes(allEpisodes); });
6755
+ await loadSection("/api/files", function(data) { allFiles = data.items || []; });
6756
+ await loadSection("/api/file-jobs", function(data) { allFileJobs = data.items || []; });
6757
+ await loadSection("/api/qa-logs?limit=20", function(data) { allQaLogs = data.items || []; });
6758
+ await loadSection("/api/cron-jobs", function(data) { allCronJobs = data.items || []; });
6759
+ if (currentView === "messages") renderMessagesView();
6760
+ if (currentView === "episodes") renderEpisodesView();
6761
+ if (currentView === "files") renderFilesView();
6762
+ if (currentView === "tasks") renderTasksView();
6763
+ if (currentView === "qa-logs") renderQaLogsView();
6764
+ }
6429
6765
 
6430
- processMessages.addEventListener("click", () => void processNow());
6431
- void load();
6432
- setInterval(() => {
6433
- if (document.visibilityState === "visible") {
6434
- void load();
6435
- }
6436
- }, 5000);
6437
- </script>
6438
- </body>
6766
+ async function processNow() {
6767
+ var btn = document.getElementById("btn-process-messages");
6768
+ if (btn) { btn.disabled = true; }
6769
+ showToast("\u6B63\u5728\u5904\u7406\u6D88\u606F\u7D22\u5F15...", "info");
6770
+ try {
6771
+ var result = await postJson("/api/process/messages");
6772
+ if (result.status === "skipped") { showToast(result.reason, "warning"); }
6773
+ else { showToast("\u5904\u7406\u5B8C\u6210\uFF1Achunks=" + result.chunks + ", vectors=" + result.vectors, "success"); }
6774
+ await load();
6775
+ } catch (error) {
6776
+ showToast(error instanceof Error ? error.message : String(error), "error");
6777
+ } finally {
6778
+ if (btn) { btn.disabled = false; }
6779
+ }
6780
+ }
6781
+
6782
+ document.addEventListener("click", async function(event) {
6783
+ var target = event.target;
6784
+ if (!(target instanceof HTMLElement)) return;
6785
+ var id = target.dataset.deleteCronJob;
6786
+ if (!id) return;
6787
+ target.disabled = true;
6788
+ showToast("\u6B63\u5728\u5220\u9664\u5B9A\u65F6\u4EFB\u52A1...", "info");
6789
+ try {
6790
+ var result = await deleteJson("/api/cron-jobs/" + encodeURIComponent(id));
6791
+ showToast(result.ok ? "\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u5220\u9664" : (result.message || "\u5220\u9664\u5931\u8D25"), result.ok ? "success" : "error");
6792
+ await load();
6793
+ } catch (error) {
6794
+ showToast(error instanceof Error ? error.message : String(error), "error");
6795
+ }
6796
+ });
6797
+
6798
+ void load();
6799
+ setInterval(function() { if (document.visibilityState === "visible") void load(); }, 5000);
6800
+ </script>
6801
+ </body>
6439
6802
  </html>`;
6440
6803
  }
6441
6804
  function parseLimit(value, fallback, max) {
@@ -6445,12 +6808,22 @@ function parseLimit(value, fallback, max) {
6445
6808
  function getWebActionToken(secrets) {
6446
6809
  return secrets.web.actionToken;
6447
6810
  }
6448
- function readHeader(value) {
6449
- return Array.isArray(value) ? value[0] : value;
6811
+ function getWebActionCookie(token) {
6812
+ return `chattercatcher_web_token=${encodeURIComponent(token)}; Path=/; HttpOnly; SameSite=Strict`;
6813
+ }
6814
+ function parseCookies(header) {
6815
+ const value = Array.isArray(header) ? header.join("; ") : header;
6816
+ if (!value) return {};
6817
+ const cookies = {};
6818
+ for (const part of value.split(";")) {
6819
+ const [rawName, ...rawValue] = part.trim().split("=");
6820
+ if (!rawName || rawValue.length === 0) continue;
6821
+ cookies[rawName] = decodeURIComponent(rawValue.join("="));
6822
+ }
6823
+ return cookies;
6450
6824
  }
6451
6825
  function isAuthorizedWebAction(request, token) {
6452
- const provided = readHeader(request.headers["x-chattercatcher-web-token"]);
6453
- return provided === token;
6826
+ return parseCookies(request.headers.cookie).chattercatcher_web_token === token;
6454
6827
  }
6455
6828
  function createWebApp(config, options = {}) {
6456
6829
  const app = Fastify({ logger: false });
@@ -6578,7 +6951,8 @@ function createWebApp(config, options = {}) {
6578
6951
  app.get("/", async (_request, reply) => {
6579
6952
  await tokenReady;
6580
6953
  reply.type("text/html; charset=utf-8");
6581
- return buildHtml().replaceAll("__WEB_ACTION_TOKEN__", webActionToken);
6954
+ reply.header("set-cookie", getWebActionCookie(webActionToken));
6955
+ return buildHtml();
6582
6956
  });
6583
6957
  return app;
6584
6958
  }