@jesonliu/lark-claudecode-bridge 2.0.0 → 2.0.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/assets/web/css/main.css +10 -2
- package/assets/web/js/auth.js +4 -3
- package/assets/web/js/pages/chats.js +178 -143
- package/assets/web/js/pages/memory.js +4 -3
- package/assets/web/js/pages/overview.js +82 -12
- package/assets/web/js/pages/permissions.js +82 -3
- package/dist/access/access-control.d.ts +18 -0
- package/dist/access/access-control.js +29 -0
- package/dist/access/access-control.js.map +1 -1
- package/dist/feishu/name-cache.d.ts +15 -0
- package/dist/feishu/name-cache.js +82 -0
- package/dist/feishu/name-cache.js.map +1 -0
- package/dist/feishu/sibling-names.d.ts +1 -0
- package/dist/feishu/sibling-names.js +63 -0
- package/dist/feishu/sibling-names.js.map +1 -0
- package/dist/gateway/feishu-gateway.d.ts +13 -0
- package/dist/gateway/feishu-gateway.js +127 -0
- package/dist/gateway/feishu-gateway.js.map +1 -1
- package/dist/gateway/gateway-registry.d.ts +2 -0
- package/dist/gateway/gateway-registry.js +4 -0
- package/dist/gateway/gateway-registry.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +49 -9
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/web/chats-api.d.ts +16 -0
- package/dist/web/chats-api.js +175 -47
- package/dist/web/chats-api.js.map +1 -1
- package/dist/web/lifecycle.d.ts +21 -0
- package/dist/web/lifecycle.js +80 -3
- package/dist/web/lifecycle.js.map +1 -1
- package/dist/web/server.js +78 -14
- package/dist/web/server.js.map +1 -1
- package/dist/web/webauth.d.ts +2 -2
- package/dist/web/webauth.js +55 -11
- package/dist/web/webauth.js.map +1 -1
- package/package.json +1 -1
package/assets/web/css/main.css
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
--shadow:0 1px 2px rgba(16,24,40,.05);
|
|
26
26
|
--shadow-2:0 10px 32px rgba(16,24,40,.14);
|
|
27
27
|
--header-bg:rgba(255,255,255,.86);
|
|
28
|
+
--boot-veil:rgba(255,255,255,.82);
|
|
28
29
|
--nav-w:212px;
|
|
29
30
|
/* 旧变量别名:页面内联样式/历史组件仍引用旧名(--primary/--muted/--danger…),统一桥接到新 tokens */
|
|
30
31
|
--primary:var(--pri); --primary-strong:var(--pri-h); --primary-weak:var(--pri-w); --primary-ring:var(--pri-ring);
|
|
@@ -45,6 +46,7 @@
|
|
|
45
46
|
--shadow:0 1px 2px rgba(0,0,0,.3);
|
|
46
47
|
--shadow-2:0 10px 32px rgba(0,0,0,.5);
|
|
47
48
|
--header-bg:rgba(17,21,29,.86);
|
|
49
|
+
--boot-veil:rgba(17,21,29,.82);
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
/* ═══ Base ═══ */
|
|
@@ -629,9 +631,13 @@ details.adv>.adv-body{padding:2px 2px 14px}
|
|
|
629
631
|
.picked-path .hint{font-size:11.5px;color:var(--mut)}
|
|
630
632
|
|
|
631
633
|
/* ═══ 会话页(查询条件 + 会话列表 + 消息流) ═══ */
|
|
632
|
-
.chats-layout{display:grid;grid-template-columns:290px minmax(0,1fr);gap:14px;align-items:start}
|
|
634
|
+
.chats-layout{display:grid;grid-template-columns:290px minmax(0,1fr);gap:14px;align-items:start;position:relative}
|
|
633
635
|
@media (max-width:1080px){.chats-layout{grid-template-columns:1fr}}
|
|
634
|
-
|
|
636
|
+
/* 整页加载层:进页初始链路(树→话题→时间线)期间盖住列表+详情区,完成/出错即被 JS 移除 */
|
|
637
|
+
.chats-boot{position:absolute;inset:0;z-index:5;display:flex;align-items:center;justify-content:center;background:var(--boot-veil);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);animation:fadein .16s ease}
|
|
638
|
+
.chats-boot-box{display:flex;flex-direction:column;align-items:center;color:var(--mut);font-size:12.5px}
|
|
639
|
+
.chat-list-panel{padding:0!important;max-height:calc(100vh - 278px);overflow-y:auto}
|
|
640
|
+
@media (max-width:1080px){.chat-list-panel{max-height:340px}}
|
|
635
641
|
/* 消息流固定高度滚动(主人反馈 #4c:不随内容无限拉高) */
|
|
636
642
|
.chats-scroll{max-height:calc(100vh - 330px);overflow-y:auto;padding:2px 6px 2px 2px}
|
|
637
643
|
/* 任务块(关键词定位的锚点):默认透明,命中高亮时描边 */
|
|
@@ -650,6 +656,8 @@ mark.kw-mark{background:var(--warn-bg);color:var(--warn-tx);border-radius:3px;pa
|
|
|
650
656
|
display:inline-flex;align-items:center;justify-content:center;
|
|
651
657
|
background:var(--pri-w);color:var(--pri);font-size:12.5px;font-weight:650;
|
|
652
658
|
}
|
|
659
|
+
/* 群聊会话头像:中性底与私聊(品牌色)区分(原型 chats-redesign 同款) */
|
|
660
|
+
.chat-obj .c-ava.group{background:var(--off-bg);color:var(--mut)}
|
|
653
661
|
.chat-obj .c-main{flex:1;min-width:0}
|
|
654
662
|
.chat-obj .c-top{display:flex;align-items:center;gap:6px}
|
|
655
663
|
.chat-obj .c-name{font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
package/assets/web/js/auth.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// ============ 配置页密码锁屏(原型设置/锁屏模块的服务端真实现) ============
|
|
2
2
|
// 行为约定:
|
|
3
|
-
// -
|
|
3
|
+
// - 进页即锁:每次进入配置页(刷新/新开标签)都要输入密码,服务端会话放行 API(会话已持久化,
|
|
4
|
+
// 桥接器重启后会话延续,重启轮询/刷新不再被 401 拦截;手动锁屏/登出仍即时吊销)
|
|
4
5
|
// - 默认密码 123456;解锁响应带 isDefault,仍为默认密码时每次登录弹「修改默认密码」引导
|
|
5
|
-
// - 无操作自动锁屏:lockMinutes 分钟(服务端 webauth.json,默认 30
|
|
6
|
+
// - 无操作自动锁屏:lockMinutes 分钟(服务端 webauth.json,默认 30 分钟;0=永不),双端同步过期
|
|
6
7
|
// - 恢复初始密码两条路:本弹窗(验当前密码)/ 本机终端 `lcb reset-password`(忘记密码兜底)
|
|
7
8
|
// - 密文落盘在服务端(webauth.json 只存 salt+md5),前端全程不接触哈希
|
|
8
9
|
import { $, api, toast } from './core.js';
|
|
@@ -12,7 +13,7 @@ export const AUTH = {
|
|
|
12
13
|
unlocked: false,
|
|
13
14
|
everUnlocked: false, // 本次页面生命周期内是否解锁过:锁屏时才需要吊销服务端会话
|
|
14
15
|
isDefault: true,
|
|
15
|
-
lockMinutes:
|
|
16
|
+
lockMinutes: 30,
|
|
16
17
|
lastActive: Date.now(),
|
|
17
18
|
enteredPwd: '', // 本次解锁所输密码(仅内存):引导改密时作为「当前密码」提交,免重复输入
|
|
18
19
|
};
|
|
@@ -1,23 +1,20 @@
|
|
|
1
1
|
// ============ 会话 ============
|
|
2
|
-
// LCB
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// LCB 本地聊天时间线只读查看器(「用户+/new」会话维度):一次 /new = 一个会话条目(sessionId 粒度),
|
|
3
|
+
// 左列表平铺全部会话、按最后活跃时间降序;右内容区的话题/时间/成员由选中会话唯一决定,
|
|
4
|
+
// 只保留关键词定位。后端 /api/chats/*(chats-api.ts):树(含每通道会话条目)+ 任务时间线,
|
|
5
|
+
// 全部只读、缺数据静默容忍。任务 = 一条飞书消息触发的一次 CC 执行。
|
|
5
6
|
// 搜索过滤只切换列表项显隐、不重建 DOM(兼容中文输入法组合输入)。
|
|
6
7
|
import { $, esc, api, toast } from '../core.js';
|
|
7
8
|
import { renderMarkdown } from '../markdown.js';
|
|
8
9
|
|
|
9
10
|
/** 页面态(切页重进即重置;不做跨页缓存——树接口本身就是目录扫描,量级很轻) */
|
|
10
11
|
const ST = {
|
|
11
|
-
bots: [],
|
|
12
|
-
app: '', chat: '',
|
|
13
|
-
|
|
14
|
-
nameByUser: {},
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
date: '', // 日期筛选('' = 最新一天)
|
|
18
|
-
user: '', // 人筛选(''=全部)
|
|
19
|
-
kw: '', // 关键词(定位对话用;''=不检索)
|
|
20
|
-
kwHits: [], kwAt: 0, // 关键词命中的任务索引与当前指向
|
|
12
|
+
bots: [], // /api/chats/tree
|
|
13
|
+
app: '', chat: '', session: '', // 当前选中:机器人 + 通道 + 会话(sessionId)
|
|
14
|
+
sessions: [], // 当前机器人的会话条目(拍平 + updatedAt 降序)
|
|
15
|
+
nameByUser: {}, // userId → 显示名(access 人名 + 飞书解析名,兜底尾号)
|
|
16
|
+
kw: '', // 关键词(定位对话用;''=不检索)
|
|
17
|
+
kwHits: [], kwAt: 0, // 关键词命中的任务索引与当前指向
|
|
21
18
|
tasks: [], truncated: false, partial: false,
|
|
22
19
|
};
|
|
23
20
|
|
|
@@ -25,7 +22,6 @@ const svg = (inner, s = 14) => `<svg viewBox="0 0 24 24" width="${s}" height="${
|
|
|
25
22
|
const IC = {
|
|
26
23
|
search: '<circle cx="11" cy="11" r="7"/><path d="m21 21-3.5-3.5"/>',
|
|
27
24
|
copy: '<rect width="13" height="13" x="9" y="9" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/>',
|
|
28
|
-
group: '<circle cx="9" cy="8" r="3.5"/><circle cx="17" cy="9" r="2.5"/><path d="M2.5 20c0-3.4 2.9-5.2 6.5-5.2s6.5 1.8 6.5 5.2"/><path d="M16 14.6c3 .4 5.5 2 5.5 5.4"/>',
|
|
29
25
|
};
|
|
30
26
|
|
|
31
27
|
const RESULT_BADGE = {
|
|
@@ -34,24 +30,29 @@ const RESULT_BADGE = {
|
|
|
34
30
|
error: '<span class="tag err">出错</span>',
|
|
35
31
|
};
|
|
36
32
|
|
|
33
|
+
/** 整页加载层(仅初始进页存在):盖住列表+详情区,初始链路(树→时间线)走完/出错即移除。
|
|
34
|
+
* 层移除后以下调用自动变 no-op——页内后续「查询/切会话」仍走时间线区行内「加载中…」,不重复盖罩 */
|
|
35
|
+
const bootStage = (t) => { const n = $('#chBootTxt'); if (n) n.textContent = t; };
|
|
36
|
+
const hideBoot = () => $('#chBoot')?.remove();
|
|
37
|
+
|
|
37
38
|
export const page = {
|
|
38
39
|
id: 'chats',
|
|
39
40
|
title: '会话',
|
|
40
|
-
desc: '
|
|
41
|
+
desc: '按机器人检索会话,查看完整消息与任务执行详情',
|
|
41
42
|
legend: false,
|
|
42
43
|
icon: '<path d="M14 9a2 2 0 0 1-2 2H6l-4 4V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2z"/><path d="M18 9h2a2 2 0 0 1 2 2v11l-4-4h-6a2 2 0 0 1-2-2v-1"/>',
|
|
43
44
|
async render(el) {
|
|
44
45
|
el.innerHTML = `
|
|
45
46
|
<div class="panel">
|
|
46
47
|
<header class="panel-head"><h3>查询条件</h3>
|
|
47
|
-
<div class="panel-actions"><span class="dim" style="font-size:11.5px"
|
|
48
|
+
<div class="panel-actions"><span class="dim" style="font-size:11.5px">选择机器人后,左侧列出全部「用户 + /new」会话</span></div></header>
|
|
48
49
|
<div class="panel-body">
|
|
49
50
|
<div style="display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap">
|
|
50
51
|
<div><label class="fld">飞书机器人 *</label>
|
|
51
52
|
<select id="chBot" style="width:220px"></select></div>
|
|
52
53
|
<button class="btn primary" id="chGo">${svg(IC.search, 13)} 查询会话</button>
|
|
53
54
|
</div>
|
|
54
|
-
<div class="mini-note" style="margin-top:8px">记录落盘于 <code>~/.lark-claudecode-bridge/transcripts/</code
|
|
55
|
+
<div class="mini-note" style="margin-top:8px">记录落盘于 <code>~/.lark-claudecode-bridge/transcripts/</code>;列表按会话最后活跃时间降序,点进即看该会话跨天全部消息。</div>
|
|
55
56
|
<div class="mini-note" id="chPermHint" hidden style="margin-top:4px;color:var(--warn-tx)">ⓘ 当前显示为飞书 ID(ou_…):请在飞书开放平台为对应机器人应用开通「获取用户基本信息」(contact:user.base:readonly)权限并发布版本;开通后约 10 分钟内自动恢复真名,无须重启。</div>
|
|
56
57
|
</div>
|
|
57
58
|
</div>
|
|
@@ -59,15 +60,24 @@ export const page = {
|
|
|
59
60
|
<div class="panel" style="margin:0">
|
|
60
61
|
<header class="panel-head"><h3>会话列表</h3>
|
|
61
62
|
<div class="panel-actions"><span class="tag off" id="chCnt"></span></div></header>
|
|
62
|
-
<div style="padding:10px 12px 8px"><input type="search" id="chQ" placeholder="
|
|
63
|
+
<div style="padding:10px 12px 8px"><input type="search" id="chQ" placeholder="搜索会话:标题 / 用户 / 群名"></div>
|
|
63
64
|
<div class="chat-list-panel" id="chList" style="border-top:1px solid var(--border)"></div>
|
|
64
65
|
</div>
|
|
65
66
|
<div class="panel" style="margin:0;min-height:440px" id="chDetail"></div>
|
|
67
|
+
<div class="chats-boot" id="chBoot" role="status" aria-live="polite">
|
|
68
|
+
<div class="chats-boot-box"><span class="br-spin-sm" aria-hidden="true"></span><span id="chBootTxt">正在加载会话目录…</span></div>
|
|
69
|
+
</div>
|
|
66
70
|
</div>`;
|
|
71
|
+
// 列表搜索实时过滤(旧版缺输入监听、输入时不过滤——本次顺手补上);IME 组合期间暂停,选词结束再过滤
|
|
72
|
+
let composing = false;
|
|
73
|
+
$('#chQ').addEventListener('compositionstart', () => { composing = true; });
|
|
74
|
+
$('#chQ').addEventListener('compositionend', () => { composing = false; applyFilter(); });
|
|
75
|
+
$('#chQ').oninput = () => { if (!composing) applyFilter(); };
|
|
67
76
|
try {
|
|
68
77
|
const r = await api('GET', '/api/chats/tree');
|
|
69
78
|
ST.bots = r.bots ?? [];
|
|
70
79
|
} catch (e) {
|
|
80
|
+
hideBoot();
|
|
71
81
|
$('#chDetail').innerHTML = `<div class="empty" style="margin-top:60px">会话树加载失败:${esc(e.message)}</div>`;
|
|
72
82
|
return;
|
|
73
83
|
}
|
|
@@ -79,63 +89,114 @@ export const page = {
|
|
|
79
89
|
const botSel = $('#chBot');
|
|
80
90
|
botSel.innerHTML = ST.bots.map((b) => `<option value="${esc(b.appId)}">${esc(b.name)}(${esc(b.appId)})</option>`).join('');
|
|
81
91
|
if (!ST.bots.length) {
|
|
92
|
+
hideBoot();
|
|
82
93
|
$('#chList').innerHTML = '<div class="empty">暂无会话记录<br><span class="mini-note">还没有任何对话落盘。在飞书里和机器人聊几句后回来看。</span></div>';
|
|
83
|
-
$('#chCnt').textContent = '0
|
|
94
|
+
$('#chCnt').textContent = '0 个会话';
|
|
84
95
|
$('#chGo').disabled = true;
|
|
85
96
|
return;
|
|
86
97
|
}
|
|
87
|
-
$('#chGo').onclick = () => void
|
|
98
|
+
$('#chGo').onclick = () => void openBot(botSel.value);
|
|
88
99
|
botSel.onchange = () => { $('#chGo').click(); };
|
|
89
|
-
//
|
|
90
|
-
const firstBot = ST.bots.find((b) => b.channels.length);
|
|
100
|
+
// 默认选中最近活跃的第一个有会话的机器人,进来即有内容可看
|
|
101
|
+
const firstBot = ST.bots.find((b) => b.channels.some((c) => c.sessions?.length));
|
|
91
102
|
if (firstBot) {
|
|
92
103
|
botSel.value = firstBot.appId;
|
|
93
|
-
|
|
104
|
+
bootStage('正在加载会话详情…');
|
|
105
|
+
try {
|
|
106
|
+
await openBot(firstBot.appId);
|
|
107
|
+
} finally {
|
|
108
|
+
hideBoot(); // 成功/空态/报错任何出口都撤层,加载层绝不残留
|
|
109
|
+
}
|
|
94
110
|
} else {
|
|
95
|
-
|
|
96
|
-
|
|
111
|
+
hideBoot();
|
|
112
|
+
openBot(botSel.value); // 有通道但全部无会话索引(如旧数据):列表空态兜底
|
|
97
113
|
}
|
|
98
114
|
},
|
|
99
115
|
};
|
|
100
116
|
|
|
101
|
-
/**
|
|
102
|
-
function
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
117
|
+
/** 拍平当前机器人的会话条目(跨通道)并按 updatedAt 降序;hay 供列表搜索(标题/用户/群名) */
|
|
118
|
+
function flattenSessions(app) {
|
|
119
|
+
const bot = ST.bots.find((b) => b.appId === app);
|
|
120
|
+
if (!bot) return [];
|
|
121
|
+
const out = [];
|
|
122
|
+
for (const c of bot.channels) {
|
|
123
|
+
for (const s of c.sessions ?? []) {
|
|
124
|
+
out.push({
|
|
125
|
+
app, chatId: c.chatId, chatName: c.displayName, chatType: c.chatType,
|
|
126
|
+
sessionId: s.sessionId, title: s.title, updatedAt: s.updatedAt, users: s.users ?? [],
|
|
127
|
+
hay: `${s.title} ${(s.users ?? []).map((u) => u.name || u.userId).join(' ')} ${c.displayName}`.toLowerCase(),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
107
130
|
}
|
|
108
|
-
return
|
|
131
|
+
return out.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 切机器人:重拍平会话列表并默认选中最近活跃第一条 */
|
|
135
|
+
async function openBot(app) {
|
|
136
|
+
ST.app = app;
|
|
137
|
+
ST.sessions = flattenSessions(app);
|
|
138
|
+
renderList();
|
|
139
|
+
const first = ST.sessions[0];
|
|
140
|
+
if (first) await selectSession(first.chatId, first.sessionId);
|
|
141
|
+
else renderDetailEmpty();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function selectSession(chatId, sessionId) {
|
|
145
|
+
ST.chat = chatId;
|
|
146
|
+
ST.session = sessionId;
|
|
147
|
+
ST.kw = '';
|
|
148
|
+
ST.kwHits = [];
|
|
149
|
+
ST.kwAt = 0;
|
|
150
|
+
ST.nameByUser = {};
|
|
151
|
+
const s = curSession();
|
|
152
|
+
// 真名优先,无真名退 ou 尾号(自指记录按无真名处理,后端同理过滤)
|
|
153
|
+
for (const u of s?.users ?? []) ST.nameByUser[u.userId] = u.name && u.name !== u.userId ? u.name : `${u.userId.slice(0, 10)}…`;
|
|
154
|
+
renderList();
|
|
155
|
+
renderDetailSkeleton();
|
|
156
|
+
await loadTimeline();
|
|
109
157
|
}
|
|
110
158
|
|
|
159
|
+
const curSession = () => ST.sessions.find((s) => s.chatId === ST.chat && s.sessionId === ST.session) ?? null;
|
|
160
|
+
|
|
161
|
+
/** 会话类型徽标三态:group=群聊 / p2p|private=私聊(副行已注明「私聊」,不再重复挂标)/ 空=未知 */
|
|
162
|
+
const typeTag = (t) => t === 'group' ? '<span class="tag info">群聊</span>'
|
|
163
|
+
: (t === 'p2p' || t === 'private') ? ''
|
|
164
|
+
: '<span class="tag off" title="暂无法识别会话类型(飞书群信息拉取失败)">未知</span>';
|
|
165
|
+
/** 成员名串(= 该会话的参与用户):真名优先,无真名退 ou 尾号 */
|
|
166
|
+
const membersOf = (s) => (s?.users || [])
|
|
167
|
+
.map((u) => (u.name && u.name !== u.userId ? u.name : short(u.userId, 8)))
|
|
168
|
+
.join('、');
|
|
169
|
+
|
|
111
170
|
function renderList() {
|
|
112
171
|
const el = $('#chList');
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
<span class="c-ava">${c.chatType === 'group' ? svg(IC.group, 15) : esc((c.displayName || '?')[0])}</span>
|
|
172
|
+
if (!el) return;
|
|
173
|
+
el.innerHTML = ST.sessions.map((s, i) => `
|
|
174
|
+
<div class="chat-obj ${s.chatId === ST.chat && s.sessionId === ST.session ? 'active' : ''}" data-i="${i}" role="button" tabindex="0" title="${esc(s.title)}">
|
|
175
|
+
<span class="c-ava ${s.chatType === 'group' ? 'group' : ''}">${esc(((s.users[0]?.name || s.users[0]?.userId) || '?')[0])}</span>
|
|
118
176
|
<div class="c-main">
|
|
119
|
-
<div class="c-top"><span class="c-name">${esc(
|
|
120
|
-
<div class="c-prev">${
|
|
121
|
-
<div class="c-badges">${
|
|
177
|
+
<div class="c-top"><span class="c-name">${esc(s.title)}</span><span class="c-time">${esc(fmtRel(s.updatedAt))}</span></div>
|
|
178
|
+
<div class="c-prev">${esc(membersOf(s) || '未知用户')} · ${s.chatType === 'group' ? esc(s.chatName) : '私聊'}</div>
|
|
179
|
+
<div class="c-badges">${typeTag(s.chatType)}</div>
|
|
122
180
|
</div>
|
|
123
|
-
</div>`).join('')
|
|
124
|
-
|
|
181
|
+
</div>`).join('')
|
|
182
|
+
|| '<div class="empty">暂无会话记录<br><span class="mini-note">该机器人还没有可展示的会话索引(早期落盘数据不含会话边界)。</span></div>';
|
|
183
|
+
$('#chCnt').textContent = `${ST.sessions.length} 个会话`;
|
|
125
184
|
el.querySelectorAll('.chat-obj').forEach((n) => {
|
|
126
|
-
|
|
127
|
-
n.
|
|
185
|
+
const go = () => void selectSession(ST.sessions[Number(n.dataset.i)].chatId, ST.sessions[Number(n.dataset.i)].sessionId);
|
|
186
|
+
n.onclick = go;
|
|
187
|
+
n.onkeydown = (e) => { if (e.key === 'Enter') go(); };
|
|
128
188
|
});
|
|
129
189
|
applyFilter();
|
|
130
190
|
}
|
|
131
191
|
|
|
132
|
-
/** 搜索过滤:只切换显隐不重建 DOM
|
|
192
|
+
/** 搜索过滤:只切换显隐不重建 DOM(兼容中文输入法组合输入);hay 已含标题/用户/群名 */
|
|
133
193
|
function applyFilter() {
|
|
134
194
|
const inp = $('#chQ');
|
|
135
195
|
if (!inp) return;
|
|
136
196
|
const kw = (inp.value || '').trim().toLowerCase();
|
|
137
197
|
$('#chList').querySelectorAll('.chat-obj').forEach((n) => {
|
|
138
|
-
|
|
198
|
+
const s = ST.sessions[Number(n.dataset.i)];
|
|
199
|
+
n.hidden = !!kw && !(s?.hay ?? n.textContent.toLowerCase()).includes(kw);
|
|
139
200
|
});
|
|
140
201
|
}
|
|
141
202
|
|
|
@@ -146,51 +207,53 @@ const fmtSize = (n) => {
|
|
|
146
207
|
return `${(n / 1024 / 1024).toFixed(1)} MB`;
|
|
147
208
|
};
|
|
148
209
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
210
|
+
/** 相对时间(列表条目右上角):今天 HH:MM / 昨天 / M-D / 跨年 YYYY-M-D */
|
|
211
|
+
const fmtRel = (iso) => {
|
|
212
|
+
const ms = Date.parse(String(iso ?? ''));
|
|
213
|
+
if (!Number.isFinite(ms)) return '';
|
|
214
|
+
const d = new Date(ms);
|
|
215
|
+
const hm = `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
|
216
|
+
const day0 = (x) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();
|
|
217
|
+
const diff = Math.round((day0(new Date()) - day0(d)) / 86400000);
|
|
218
|
+
if (diff === 0) return `今天 ${hm}`;
|
|
219
|
+
if (diff === 1) return '昨天';
|
|
220
|
+
if (d.getFullYear() === new Date().getFullYear()) return `${d.getMonth() + 1}-${d.getDate()}`;
|
|
221
|
+
return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`;
|
|
222
|
+
};
|
|
223
|
+
/** 日期段(信息行时间范围用):今天/昨天/M-D(ts 为北京时间 ISO,直接取日期段避免跨时区换算) */
|
|
224
|
+
const fmtDayRel = (ts) => {
|
|
225
|
+
const s = String(ts ?? '');
|
|
226
|
+
const m = s.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
|
227
|
+
if (!m) return s.slice(0, 10);
|
|
228
|
+
const ms = Date.parse(s);
|
|
229
|
+
if (!Number.isFinite(ms)) return `${+m[2]}-${+m[3]}`;
|
|
230
|
+
const day0 = (x) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();
|
|
231
|
+
const d = new Date(ms);
|
|
232
|
+
const diff = Math.round((day0(new Date()) - day0(d)) / 86400000);
|
|
233
|
+
if (diff === 0) return '今天';
|
|
234
|
+
if (diff === 1) return '昨天';
|
|
235
|
+
return `${+m[2]}-${+m[3]}`;
|
|
236
|
+
};
|
|
170
237
|
|
|
171
238
|
function renderDetailEmpty() {
|
|
172
|
-
$('#chDetail').innerHTML = '<div class="empty" style="margin-top:80px"
|
|
239
|
+
$('#chDetail').innerHTML = '<div class="empty" style="margin-top:80px">选择左侧会话查看会话详情</div>';
|
|
173
240
|
}
|
|
174
241
|
|
|
175
242
|
function renderDetailSkeleton() {
|
|
176
|
-
const
|
|
177
|
-
const
|
|
243
|
+
const s = curSession();
|
|
244
|
+
const bot = ST.bots.find((b) => b.appId === ST.app);
|
|
178
245
|
$('#chDetail').innerHTML = `
|
|
179
246
|
<header class="panel-head" style="padding-bottom:12px;border-bottom:1px solid var(--border)">
|
|
180
|
-
<h3 id="chDName"></h3
|
|
181
|
-
<span class="panel-sub" id="chDSub"></span>
|
|
247
|
+
<h3 id="chDName"></h3>${typeTag(s?.chatType)}
|
|
182
248
|
<div class="panel-actions">
|
|
183
249
|
<button class="btn sm" id="chCopyId">${svg(IC.copy, 13)} 复制会话 ID</button>
|
|
250
|
+
<button class="btn sm" id="chatRefresh" title="重拉会话树与本会话记录">↻</button>
|
|
184
251
|
</div></header>
|
|
185
252
|
<div class="panel-body" style="padding-top:6px">
|
|
253
|
+
<div class="mini-note" id="chDMeta" style="margin:0 0 8px"></div>
|
|
186
254
|
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:6px">
|
|
187
|
-
<
|
|
188
|
-
<select id="chatDate" style="width:auto" title="按天浏览"></select>
|
|
189
|
-
<select id="chatUser" style="width:auto" title="按发信人筛选"></select>
|
|
190
|
-
<input type="search" id="chatKw" placeholder="关键词:定位对话…" style="width:180px;padding:5px 10px;font-size:12.5px">
|
|
255
|
+
<input type="search" id="chatKw" placeholder="关键词:定位对话…" style="width:200px;padding:5px 10px;font-size:12.5px">
|
|
191
256
|
<button class="btn sm primary" id="chatGo">查询</button>
|
|
192
|
-
<button class="btn sm" id="chatReset" title="恢复默认筛选并重新加载">重置</button>
|
|
193
|
-
<button class="btn sm" id="chatRefresh" title="重拉会话树与本通道记录">↻</button>
|
|
194
257
|
<span id="kwNav" hidden style="display:inline-flex;align-items:center;gap:6px">
|
|
195
258
|
<span class="mini-note" id="kwPos"></span>
|
|
196
259
|
<button class="btn sm" id="kwPrev" title="上一处">↑</button>
|
|
@@ -201,89 +264,44 @@ function renderDetailSkeleton() {
|
|
|
201
264
|
</div>
|
|
202
265
|
<div id="chatTl" class="chats-scroll"></div>
|
|
203
266
|
</div>`;
|
|
204
|
-
$('#chDName').
|
|
205
|
-
|
|
267
|
+
$('#chDName').textContent = s?.title || ST.session.slice(0, 8);
|
|
268
|
+
// 只读信息行:聊天对象(群聊场景,私聊对象=成员本人不再重复)· 成员 · 时间范围(时间线渲染后回填)
|
|
269
|
+
$('#chDMeta').innerHTML = `${s?.chatType === 'group' ? `${esc(s?.chatName || ST.chat)} · ` : ''}成员:${esc(membersOf(s) || '未知用户')} · 时间范围:<b id="chDRange">渲染后回填</b>`;
|
|
206
270
|
$('#chCopyId').onclick = async () => {
|
|
207
|
-
try { await navigator.clipboard.writeText(ST.
|
|
271
|
+
try { await navigator.clipboard.writeText(ST.session); toast(`会话 ID ${ST.session} 已复制`); }
|
|
208
272
|
catch { toast('复制失败(浏览器剪贴板权限)', true); }
|
|
209
273
|
};
|
|
210
|
-
//
|
|
211
|
-
$('#
|
|
212
|
-
$('#chatGo').onclick = () => {
|
|
213
|
-
ST.topic = $('#chatTopic').value;
|
|
214
|
-
ST.date = $('#chatDate').value;
|
|
215
|
-
ST.user = $('#chatUser').value;
|
|
216
|
-
ST.kw = $('#chatKw').value.trim();
|
|
217
|
-
void loadTimeline();
|
|
218
|
-
};
|
|
274
|
+
// 查询制:关键词改动不立即生效,回车或点「查询」统一应用(主人反馈:不要选一下就搜)
|
|
275
|
+
$('#chatGo').onclick = () => void loadTimeline();
|
|
219
276
|
$('#chatKw').onkeydown = (e) => { if (e.key === 'Enter') $('#chatGo').click(); };
|
|
220
|
-
$('#chatReset').onclick = () => {
|
|
221
|
-
ST.topic = '';
|
|
222
|
-
ST.user = '';
|
|
223
|
-
ST.kw = '';
|
|
224
|
-
$('#chatKw').value = '';
|
|
225
|
-
ST.date = ST.dates[0] ?? '';
|
|
226
|
-
renderFilters();
|
|
227
|
-
void loadTimeline();
|
|
228
|
-
};
|
|
229
277
|
$('#kwPrev').onclick = () => { ST.kwAt--; gotoKwHit(); };
|
|
230
278
|
$('#kwNext').onclick = () => { ST.kwAt++; gotoKwHit(); };
|
|
231
279
|
$('#kwClear').onclick = () => { ST.kw = ''; $('#chatKw').value = ''; renderTimeline(); };
|
|
232
280
|
$('#chatRefresh').onclick = () => void fullRefresh();
|
|
233
281
|
}
|
|
234
282
|
|
|
235
|
-
/** 刷新 = 重拉树 +
|
|
283
|
+
/** 刷新 = 重拉树 + 重拍平 + 尽量留在当前会话(不在了就落到最新一条) */
|
|
236
284
|
async function fullRefresh() {
|
|
237
285
|
try {
|
|
238
286
|
const r = await api('GET', '/api/chats/tree');
|
|
239
287
|
ST.bots = r.bots ?? [];
|
|
240
288
|
} catch (e) { toast(`刷新失败:${e.message}`, true); }
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if (topic) {
|
|
248
|
-
topic.innerHTML = `<option value="">全部话题(按天浏览)</option>` + ST.topics.map((t) =>
|
|
249
|
-
`<option value="${esc(t.sessionId)}" ${t.sessionId === ST.topic ? 'selected' : ''}>${esc(short(t.title, 42))}</option>`).join('');
|
|
250
|
-
topic.value = ST.topic;
|
|
251
|
-
topic.disabled = ST.topics.length === 0;
|
|
252
|
-
}
|
|
253
|
-
const date = $('#chatDate');
|
|
254
|
-
if (date) {
|
|
255
|
-
date.innerHTML = ST.dates.map((d) => `<option value="${esc(d)}" ${d === ST.date ? 'selected' : ''}>${esc(d)}</option>`).join('');
|
|
256
|
-
date.disabled = !!ST.topic;
|
|
257
|
-
if (!ST.topic && !ST.dates.includes(ST.date)) ST.date = ST.dates[0] ?? '';
|
|
258
|
-
date.value = ST.date;
|
|
259
|
-
}
|
|
260
|
-
const user = $('#chatUser');
|
|
261
|
-
if (user) {
|
|
262
|
-
user.innerHTML = `<option value="">全部成员</option>` + ST.users.map((u) =>
|
|
263
|
-
`<option value="${esc(u.userId)}" ${u.userId === ST.user ? 'selected' : ''}>${esc(u.name || short(u.userId, 14))}</option>`).join('');
|
|
264
|
-
user.value = ST.user;
|
|
265
|
-
user.disabled = ST.users.length === 0;
|
|
266
|
-
}
|
|
289
|
+
ST.sessions = flattenSessions(ST.app);
|
|
290
|
+
const keep = ST.sessions.find((s) => s.chatId === ST.chat && s.sessionId === ST.session);
|
|
291
|
+
renderList();
|
|
292
|
+
if (keep) await selectSession(ST.chat, ST.session);
|
|
293
|
+
else if (ST.sessions[0]) await selectSession(ST.sessions[0].chatId, ST.sessions[0].sessionId);
|
|
294
|
+
else renderDetailEmpty();
|
|
267
295
|
}
|
|
268
296
|
|
|
269
297
|
async function loadTimeline() {
|
|
270
298
|
const tl = $('#chatTl');
|
|
271
|
-
if (!tl) return;
|
|
272
|
-
// 筛选值以 DOM
|
|
273
|
-
ST.topic = $('#chatTopic')?.value ?? '';
|
|
274
|
-
ST.date = $('#chatDate')?.value ?? ST.date;
|
|
275
|
-
ST.user = $('#chatUser')?.value ?? '';
|
|
299
|
+
if (!tl || !ST.session) return;
|
|
300
|
+
// 筛选值以 DOM 现值为准(查询按钮已把关键词写入 DOM;selectSession 流程骨架刚重建)
|
|
276
301
|
ST.kw = ($('#chatKw')?.value || '').trim();
|
|
277
302
|
ST.kwAt = 0;
|
|
278
|
-
if (!ST.dates.length) {
|
|
279
|
-
tl.innerHTML = '<div class="empty">这一天没有记录<br><span class="mini-note">该通道没有可读的落盘文件(可能已被保留期清理)。</span></div>';
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
303
|
tl.innerHTML = '<div class="hint" style="padding:6px 2px">加载中…</div>';
|
|
283
|
-
const p = new URLSearchParams({ app: ST.app, chat: ST.chat });
|
|
284
|
-
if (ST.topic) p.set('session', ST.topic);
|
|
285
|
-
else p.set('date', ST.date || ST.dates[0]);
|
|
286
|
-
if (ST.user) p.set('user', ST.user);
|
|
304
|
+
const p = new URLSearchParams({ app: ST.app, chat: ST.chat, session: ST.session });
|
|
287
305
|
try {
|
|
288
306
|
const r = await api('GET', `/api/chats/timeline?${p.toString()}`);
|
|
289
307
|
ST.tasks = r.tasks ?? [];
|
|
@@ -293,33 +311,50 @@ async function loadTimeline() {
|
|
|
293
311
|
tl.innerHTML = `<div class="empty">时间线加载失败:${esc(e.message)}</div>`;
|
|
294
312
|
return;
|
|
295
313
|
}
|
|
314
|
+
bootStage('正在渲染消息…'); // 仅初始进页有加载层时生效;页内查询时层已移除,自动 no-op
|
|
296
315
|
renderTimeline();
|
|
297
316
|
}
|
|
298
317
|
|
|
318
|
+
/** 信息行时间范围回填:该会话首末任务时间(同天只写一个日期) */
|
|
319
|
+
function fillRange() {
|
|
320
|
+
const el = $('#chDRange');
|
|
321
|
+
if (!el) return;
|
|
322
|
+
if (!ST.tasks.length) { el.textContent = '—'; return; }
|
|
323
|
+
const a = ST.tasks[0];
|
|
324
|
+
const b = ST.tasks[ST.tasks.length - 1];
|
|
325
|
+
const da = String(a.ts).slice(0, 10);
|
|
326
|
+
const db = String(b.ts).slice(0, 10);
|
|
327
|
+
el.textContent = da === db
|
|
328
|
+
? `${fmtDayRel(da)} ${fmtTime(a.ts)} ~ ${fmtTime(b.ts)}`
|
|
329
|
+
: `${fmtDayRel(da)} ${fmtTime(a.ts)} ~ ${fmtDayRel(db)} ${fmtTime(b.ts)}`;
|
|
330
|
+
}
|
|
331
|
+
|
|
299
332
|
function renderTimeline() {
|
|
300
333
|
const tl = $('#chatTl');
|
|
334
|
+
if (!tl) return;
|
|
301
335
|
const notes = [];
|
|
302
|
-
if (ST.topic) notes.push(`话题视图:跨 ${ST.tasks.length ? new Set(ST.tasks.map((t) => t.date)).size : 0} 天聚合`);
|
|
303
336
|
if (ST.truncated) notes.push('已达单次返回上限,仅显示最近一部分');
|
|
304
337
|
if (ST.partial) notes.push('历史文件超量,仅扫描了部分(防呆上限 300 文件 / 150MB)');
|
|
305
338
|
$('#chatScan').textContent = notes.join(' · ');
|
|
306
339
|
if (!ST.tasks.length) {
|
|
307
|
-
tl.innerHTML =
|
|
340
|
+
tl.innerHTML = '<div class="empty">该会话暂无可读消息<br><span class="mini-note">可能早于落盘功能启用,或落盘文件已被保留期清理。</span></div>';
|
|
341
|
+
fillRange();
|
|
308
342
|
return;
|
|
309
343
|
}
|
|
310
|
-
const botName =
|
|
344
|
+
const botName = ST.bots.find((b) => b.appId === ST.app)?.name || '机器人';
|
|
311
345
|
let html = '';
|
|
312
346
|
let lastDate = '';
|
|
313
347
|
let idx = 0;
|
|
314
348
|
for (const t of ST.tasks) {
|
|
315
349
|
if (t.date !== lastDate) {
|
|
316
350
|
lastDate = t.date;
|
|
317
|
-
html += `<div class="cmsg-day">${esc(t.date)}</div>`;
|
|
351
|
+
html += `<div class="cmsg-day">${esc(fmtDayRel(t.date))}</div>`;
|
|
318
352
|
}
|
|
319
353
|
html += taskHtml(t, botName, idx);
|
|
320
354
|
idx++;
|
|
321
355
|
}
|
|
322
356
|
tl.innerHTML = html;
|
|
357
|
+
fillRange();
|
|
323
358
|
// 「查看完整内容」折叠切换(用户消息)
|
|
324
359
|
tl.querySelectorAll('[data-more]').forEach((b) => b.onclick = () => {
|
|
325
360
|
const f = b.nextElementSibling;
|
|
@@ -833,14 +833,15 @@ function renderUsersTab(el, { st, m }) {
|
|
|
833
833
|
|
|
834
834
|
/** 粒度向导卡(方案 §六:默认 bot 级;「升级到 bot×人」开关 = 增量升级意向,不动既有 key 与记忆) */
|
|
835
835
|
const granularityCardHtml = (gran) => `
|
|
836
|
-
<div class="card" style="margin-top:14px">
|
|
836
|
+
<div class="card" style="margin-top:14px;">
|
|
837
837
|
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap">
|
|
838
838
|
<b style="font-size:13.5px">记忆粒度</b>
|
|
839
839
|
<span class="hint">${gran === 'bot-user' ? '已选择:bot × 人(每位聊天者独立空间)' : '当前:每 bot 一把密钥(默认,单机自用零损失)'}</span>
|
|
840
840
|
<span style="flex:1"></span>
|
|
841
841
|
<label style="display:inline-flex;align-items:center;gap:8px;cursor:pointer">
|
|
842
|
-
<span class="hint">升级到 bot
|
|
843
|
-
|
|
842
|
+
<span class="hint">升级到 bot×人(暂不支持升级)</span>
|
|
843
|
+
<!-- 我暂时把这个开关关掉,暂时不开放开启 -->
|
|
844
|
+
<!--<label class="switch"><input type="checkbox" id="memGran" ${gran === 'bot-user' ? 'checked' : ''}><span class="track"></span></label>-->
|
|
844
845
|
</label>
|
|
845
846
|
</div>
|
|
846
847
|
<div class="hint" style="margin-top:8px;line-height:1.8">升级属<b>增量演进</b>:不影响既有密钥与记忆、会话不中断,仅为新聊天者追加独立签发。
|