@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
|
@@ -35,15 +35,37 @@ function showMask(inner) {
|
|
|
35
35
|
m.style.display = 'flex';
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/** 启停请求的容错:embedded
|
|
38
|
+
/** 启停请求的容错:embedded 停止/重启时进程退出可能先于响应到达,网络断开视为已受理。
|
|
39
|
+
* delivered 区分「服务确认受理」与「网络断开推定受理」——后者有两种截然相反的可能:
|
|
40
|
+
* ①旧进程退出先于响应(正常)②服务根本不在、请求无人受理(2026-10-17 120s 空等根因),
|
|
41
|
+
* 调用方(restartFlow)须用 probeBridgeAlive 二次分流 */
|
|
39
42
|
async function bridgeActionSafe(op) {
|
|
40
|
-
try { return await api('POST', '/api/bridge/action', { op }); }
|
|
43
|
+
try { return { ...await api('POST', '/api/bridge/action', { op }), delivered: true }; }
|
|
41
44
|
catch (e) {
|
|
42
|
-
if (/fetch|network|load failed/i.test(e.message)) return { ok: true, message: '已提交(连接关闭属预期)' };
|
|
45
|
+
if (/fetch|network|load failed/i.test(e.message)) return { ok: true, message: '已提交(连接关闭属预期)', delivered: false };
|
|
43
46
|
throw e;
|
|
44
47
|
}
|
|
45
48
|
}
|
|
46
49
|
|
|
50
|
+
/** 桥接器状态探测:裸 fetch /api/status,任何 HTTP 应答(含 401 锁屏/503)都算「在」;
|
|
51
|
+
* 不用 api():它对非 2xx 会抛错,而锁屏 401 时服务明明活着。本地回环毫秒级应答,3s 兜底防悬挂。
|
|
52
|
+
* 返回结构化信息:401 特批响应自带顶层 pid/version(重启判定用它识别新进程,无需 200 会话)*/
|
|
53
|
+
async function probeStatus() {
|
|
54
|
+
try {
|
|
55
|
+
const res = await Promise.race([
|
|
56
|
+
fetch('/api/status', { cache: 'no-store' }),
|
|
57
|
+
sleep(3000).then(() => { throw new Error('probe-timeout'); }),
|
|
58
|
+
]);
|
|
59
|
+
const body = await res.json().catch(() => ({}));
|
|
60
|
+
return {
|
|
61
|
+
alive: true,
|
|
62
|
+
locked: res.status === 401,
|
|
63
|
+
pid: typeof body.pid === 'number' ? body.pid : null,
|
|
64
|
+
version: typeof body.version === 'string' ? body.version : null,
|
|
65
|
+
};
|
|
66
|
+
} catch { return { alive: false, locked: false, pid: null, version: null }; }
|
|
67
|
+
}
|
|
68
|
+
|
|
47
69
|
/**
|
|
48
70
|
* 重启进度分步弹窗(两种运行模式统一)。步骤① LCB 重启(探测新进程上线),步骤② 记忆增强
|
|
49
71
|
* OpenViking(轮询自举状态/健康探活)——两步状态源不同、节奏不同,旧版单句遮罩 + 45s 超时
|
|
@@ -64,8 +86,20 @@ async function restartFlow() {
|
|
|
64
86
|
dlg.onClose(() => { stopped = true; setTimeout(() => void refresh(), 3000); });
|
|
65
87
|
const paint = (i, patch) => { Object.assign(st[i], patch); dlg.setSteps(st.map((s) => ({ ...s }))); };
|
|
66
88
|
|
|
67
|
-
|
|
89
|
+
let delivered = true;
|
|
90
|
+
try { delivered = (await bridgeActionSafe('restart')).delivered !== false; }
|
|
68
91
|
catch (e) { paint(0, { status: 'failed', detail: e.message }); toast(e.message, true); return; }
|
|
92
|
+
if (!delivered) {
|
|
93
|
+
// 推定受理(网络断开)二次分流:服务确实不在 = 重启请求无人受理,立即明示,不空等 120s
|
|
94
|
+
//(embedded 正常重启也会推定受理,但那时服务很快失联/被新进程接管,探测通过继续走轮询)
|
|
95
|
+
if (!(await probeStatus()).alive) {
|
|
96
|
+
paint(0, { status: 'failed', detail: '桥接器服务未在运行,重启请求未送达。请在终端运行 lcb start 拉起后重试' });
|
|
97
|
+
paint(1, { status: 'skipped', detail: '配置页不可达,无法检测记忆增强(记忆增强不会导致重启失败)' });
|
|
98
|
+
dlg.setHint('<button class="btn sm" id="sdReload">手动刷新页面</button>');
|
|
99
|
+
document.querySelector('#sdReload').onclick = () => location.reload();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
69
103
|
|
|
70
104
|
// ---- 步骤①:等 LCB 回归。embedded:本页随进程失联,连续两次 /api/status 成功才算新进程
|
|
71
105
|
//(排除旧进程残影);独立:本页不失联,等 PID 文件换新(bridge.running 且 pid 变化)。
|
|
@@ -74,25 +108,61 @@ async function restartFlow() {
|
|
|
74
108
|
await sleep(2500); // 留给旧进程退场:立刻探测可能命中尚未退出的旧 server
|
|
75
109
|
let streak = false;
|
|
76
110
|
let back = false;
|
|
111
|
+
/** 200 轮询拿不到时的 401 兜底:新进程已上线但页面会话失效(重启清空/改密吊销/锁屏过期)
|
|
112
|
+
* → 重启本身成功,转为「重新解锁」引导而非空等 120s(401 特批响应自带顶层 pid 可判新旧) */
|
|
113
|
+
const finishWithRelock = (waitedSec) => {
|
|
114
|
+
paint(0, { status: 'done', detail: `LCB 已重启(用时 ${waitedSec} 秒)` });
|
|
115
|
+
paint(1, { status: 'warn', detail: '配置页会话已随重启失效(安全设计),请重新解锁后查看记忆增强状态' });
|
|
116
|
+
dlg.setHint('<button class="btn primary sm" id="sdReload">重新解锁页面</button>');
|
|
117
|
+
document.querySelector('#sdReload').onclick = () => location.reload();
|
|
118
|
+
toast('✅ LCB 已重启,请重新解锁页面');
|
|
119
|
+
};
|
|
77
120
|
while (!stopped && Date.now() - t0 < 120_000) {
|
|
78
121
|
const sec = Math.round((Date.now() - t0) / 1000);
|
|
79
122
|
let s = null;
|
|
80
|
-
try { s = await api('GET', '/api/status'); } catch { /*
|
|
123
|
+
try { s = await api('GET', '/api/status'); } catch { /* 失联窗口 / 401 锁屏属预期 */ }
|
|
124
|
+
if (!s) {
|
|
125
|
+
const probe = await probeStatus();
|
|
126
|
+
if (probe.pid != null && oldPid != null && probe.pid !== oldPid) {
|
|
127
|
+
finishWithRelock(sec);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
81
131
|
if (s) {
|
|
82
132
|
if (embedded) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
133
|
+
// 新进程识别:应答者顶层 pid ≠ 重启前 PID(pid 双方齐备时),否则看 uptimeMs 是否小于
|
|
134
|
+
// 已等待时长(新进程必然更年轻;旧版 LCB 无顶层 pid、或别的进程占着端口应答时兜底)。
|
|
135
|
+
// 识别不过 = 旧进程残影/冒充应答:不算回归,继续等(避免端口被旧进程接管时误报「已重启」)
|
|
136
|
+
const waitedMs = Date.now() - t0;
|
|
137
|
+
const fresh = (s.pid != null && oldPid != null)
|
|
138
|
+
? s.pid !== oldPid
|
|
139
|
+
: (s.uptimeMs != null ? s.uptimeMs < waitedMs : true);
|
|
140
|
+
if (fresh) {
|
|
141
|
+
if (streak) { back = true; break; }
|
|
142
|
+
streak = true;
|
|
143
|
+
await sleep(1000); // 快速二次确认(旧实现同口径)
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
streak = false; // 残影应答:重置连击,继续等
|
|
147
|
+
} else if (s.bridge?.running && (oldPid == null || s.bridge?.pid !== oldPid)) { back = true; break; }
|
|
89
148
|
} else if (embedded) streak = false;
|
|
90
149
|
paint(0, { detail: `已等 ${sec} 秒…(新进程上线即自动恢复)` });
|
|
91
150
|
await sleep(1500);
|
|
92
151
|
}
|
|
93
152
|
if (stopped) return;
|
|
94
153
|
if (!back) {
|
|
95
|
-
|
|
154
|
+
// 超时三分:①新进程其实已上线但会话失效(轮询内兜底漏网的最后时机,如 oldPid 迟到才解析)
|
|
155
|
+
// ②端口有应答但 PID 与重启前相同(旧进程仍占端口)③彻底无人应答(启动失败)
|
|
156
|
+
const probe = await probeStatus();
|
|
157
|
+
if (probe.pid != null && oldPid != null && probe.pid !== oldPid) {
|
|
158
|
+
finishWithRelock(Math.round((Date.now() - t0) / 1000));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
paint(0, { status: 'failed', detail: probe.alive
|
|
162
|
+
? '超过 120 秒未见新进程上线:端口上有服务应答但 PID 未变化(疑似重启前旧进程仍占用端口)——请结束占用进程(lcb start 也会自动清场)后重试'
|
|
163
|
+
: '超过 120 秒未见新进程上线——可能端口占用等启动异常,请查看日志' });
|
|
164
|
+
// 步骤①失败不再静默跳过步骤②:给出灰态说明,避免「记忆增强根本没显示启动」的误解
|
|
165
|
+
paint(1, { status: 'skipped', detail: 'LCB 未恢复,无法检测记忆增强(记忆增强不会导致重启失败)' });
|
|
96
166
|
dlg.setHint('<button class="btn sm" id="sdReload">手动刷新页面</button>');
|
|
97
167
|
document.querySelector('#sdReload').onclick = () => location.reload();
|
|
98
168
|
return;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// ============ 权限(页内一级 tab:免确认工具 /
|
|
1
|
+
// ============ 权限(页内一级 tab:免确认工具 / 危险命令黑名单 / 接入申请) ============
|
|
2
2
|
// 改动即时保存并热生效;「全部工具免确认」缺省开启(显式 false 才算关闭),开启时黑名单是唯一安全网。
|
|
3
|
-
import { S, $, esc, toast, saveDoc } from '../core.js';
|
|
3
|
+
import { S, $, esc, toast, saveDoc, api } from '../core.js';
|
|
4
4
|
import { confirmDialog } from '../ui.js';
|
|
5
5
|
|
|
6
6
|
const svg = (inner, s = 14) => `<svg viewBox="0 0 24 24" width="${s}" height="${s}" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${inner}</svg>`;
|
|
@@ -15,13 +15,92 @@ function renderPermissions(el) {
|
|
|
15
15
|
<div class="tabs">
|
|
16
16
|
<button data-t="allow" class="${permTab === 'allow' ? 'active' : ''}">免确认工具</button>
|
|
17
17
|
<button data-t="danger" class="${permTab === 'danger' ? 'active' : ''}">危险命令黑名单 <span class="tag off" style="margin-left:4px">${p.dangerous_commands.length}</span></button>
|
|
18
|
+
<button data-t="pair" class="${permTab === 'pair' ? 'active' : ''}">接入申请 <span class="tag off" style="margin-left:4px" id="pairCount">…</span></button>
|
|
18
19
|
</div>
|
|
19
20
|
<div id="permPanel"></div>`;
|
|
20
21
|
el.querySelectorAll('.tabs button').forEach((b) =>
|
|
21
22
|
b.onclick = () => { permTab = b.dataset.t; renderPermissions(el); });
|
|
22
23
|
const panel = $('#permPanel');
|
|
23
24
|
if (permTab === 'danger') renderDangerTab(panel, p);
|
|
25
|
+
else if (permTab === 'pair') renderPairTab(panel);
|
|
24
26
|
else renderAllowTab(panel, p);
|
|
27
|
+
if (permTab !== 'pair') void refreshPairCount(); // 徽标数量任何 tab 下都保持新鲜
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 接入申请徽标数量(tab 头);面板不存在时静默退出(页面已切换) */
|
|
31
|
+
async function refreshPairCount() {
|
|
32
|
+
let n = 0;
|
|
33
|
+
try { n = (await api('GET', '/api/access/pending')).pending.length; } catch { /* 失联:保持原样 */ }
|
|
34
|
+
const el = $('#pairCount');
|
|
35
|
+
if (el) { el.textContent = String(n); el.className = `tag ${n > 0 ? 'warn' : 'off'}`; }
|
|
36
|
+
return n;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ============ 接入申请 tab:配对授权(码不再发到来话群,私发管理员 + 此处审批) ============
|
|
40
|
+
let pairPollTimer = null;
|
|
41
|
+
function renderPairTab(panel) {
|
|
42
|
+
if (pairPollTimer) { clearInterval(pairPollTimer); pairPollTimer = null; }
|
|
43
|
+
panel.innerHTML = `
|
|
44
|
+
<div class="panel">
|
|
45
|
+
<header class="panel-head"><h3>接入申请</h3>
|
|
46
|
+
<div class="panel-actions">
|
|
47
|
+
<span class="tip" data-tip="未知用户私聊/群里艾特机器人时,配对码私聊发给管理员并出现在这里;批准后立即生效,也可忽略(15 分钟自动过期)">${svg(IC_INFO)}</span>
|
|
48
|
+
<button class="btn sm" id="pairRefresh">↻ 刷新</button>
|
|
49
|
+
</div></header>
|
|
50
|
+
<div class="panel-body">
|
|
51
|
+
<div id="pairList"><span class="hint">加载中…</span></div>
|
|
52
|
+
</div>
|
|
53
|
+
<footer class="panel-foot">批准 = 该用户获得使用权限(member);不批准可忽略,申请 15 分钟后自动过期。终端 <code>lcb pair <码></code> 双通道并存。配对码私聊发给当前配置的管理员(该机器人最早的使用者),不再发到来话群。</footer>
|
|
54
|
+
</div>`;
|
|
55
|
+
$('#pairRefresh').onclick = () => void loadPairList();
|
|
56
|
+
void loadPairList();
|
|
57
|
+
pairPollTimer = setInterval(() => { if (panel.isConnected) void loadPairList(true); else { clearInterval(pairPollTimer); pairPollTimer = null; } }, 30_000);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function loadPairList(silent = false) {
|
|
61
|
+
const box = $('#pairList');
|
|
62
|
+
if (!box) return;
|
|
63
|
+
try {
|
|
64
|
+
const { pending } = await api('GET', '/api/access/pending');
|
|
65
|
+
await refreshPairCount();
|
|
66
|
+
if (!box.isConnected) return;
|
|
67
|
+
if (!pending.length) {
|
|
68
|
+
box.innerHTML = '<div class="banner ok" style="margin:6px 0"><span>暂无接入申请。未知用户联系机器人时会自动出现在这里(配对码同时私聊发给管理员)。</span></div>';
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
box.innerHTML = `<table><thead><tr><th>申请人</th><th>机器人</th><th>配对码</th><th>剩余有效期</th><th style="width:170px">操作</th></tr></thead>
|
|
72
|
+
<tbody>${pending.map((it) => `
|
|
73
|
+
<tr>
|
|
74
|
+
<td><b>${esc(it.displayName || it.name)}</b>${it.displayName && it.displayName !== it.name ? ` <span class="hint" style="font-size:11px">${esc(it.name)}</span>` : ''}</td>
|
|
75
|
+
<td>${esc(it.appName || it.appId || '—')}</td>
|
|
76
|
+
<td><code>${esc(it.code)}</code></td>
|
|
77
|
+
<td>${it.expiresInSec > 60 ? `${Math.floor(it.expiresInSec / 60)} 分钟` : '<span class="tag warn">即将过期</span>'}</td>
|
|
78
|
+
<td style="display:flex;gap:6px">
|
|
79
|
+
<button class="btn sm primary" data-act="approve" data-code="${esc(it.code)}" data-name="${esc(it.displayName || it.name)}">授权</button>
|
|
80
|
+
<button class="btn sm danger" data-act="reject" data-code="${esc(it.code)}" data-name="${esc(it.displayName || it.name)}">拒绝</button>
|
|
81
|
+
</td>
|
|
82
|
+
</tr>`).join('')}</tbody></table>`;
|
|
83
|
+
box.querySelectorAll('button[data-act]').forEach((b) => {
|
|
84
|
+
b.onclick = async () => {
|
|
85
|
+
const approve = b.dataset.act === 'approve';
|
|
86
|
+
if (!(await confirmDialog({
|
|
87
|
+
title: approve ? '批准接入申请' : '拒绝接入申请',
|
|
88
|
+
message: approve
|
|
89
|
+
? `确定批准 <b>${esc(b.dataset.name)}</b> 使用机器人?批准后对方即可开始对话。`
|
|
90
|
+
: `确定拒绝 <b>${esc(b.dataset.name)}</b> 的申请?对方再次发消息可重新发起申请。`,
|
|
91
|
+
confirmText: approve ? '批准' : '拒绝',
|
|
92
|
+
danger: !approve,
|
|
93
|
+
}))) return;
|
|
94
|
+
try {
|
|
95
|
+
await api('POST', `/api/access/pending/${approve ? 'approve' : 'reject'}`, { code: b.dataset.code });
|
|
96
|
+
toast(approve ? `✅ 已批准 ${b.dataset.name}` : '已拒绝该申请');
|
|
97
|
+
} catch (e) { toast(e.message, true); }
|
|
98
|
+
void loadPairList();
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
} catch (e) {
|
|
102
|
+
if (!silent) box.innerHTML = `<div class="banner err" style="margin:6px 0"><span>加载失败:${esc(e.message)}</span></div>`;
|
|
103
|
+
}
|
|
25
104
|
}
|
|
26
105
|
|
|
27
106
|
function renderAllowTab(panel, p) {
|
|
@@ -172,7 +251,7 @@ function renderDangerTab(panel, p) {
|
|
|
172
251
|
export const page = {
|
|
173
252
|
id: 'permissions',
|
|
174
253
|
title: '权限',
|
|
175
|
-
desc: '
|
|
254
|
+
desc: '工具免确认名单、危险命令黑名单与接入申请审批,改动即时生效',
|
|
176
255
|
icon: '<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1 1 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/>',
|
|
177
256
|
render: renderPermissions,
|
|
178
257
|
};
|
|
@@ -42,11 +42,29 @@ export declare class AccessControl {
|
|
|
42
42
|
addUser(userId: string, name: string, role: 'admin' | 'member', appId?: string): void;
|
|
43
43
|
/** 历史用户记录缺 appId 时按当前来路补登(老版本 access.json 的平滑迁移) */
|
|
44
44
|
ensureAppId(userId: string, appId: string): void;
|
|
45
|
+
/** 待审列表(配置页「接入申请」用):带过期时刻与来源应用,供前端算剩余有效期/按机器人分组 */
|
|
46
|
+
listPendingDetailed(): Array<{
|
|
47
|
+
userId: string;
|
|
48
|
+
name: string;
|
|
49
|
+
code: string;
|
|
50
|
+
expiresAt: number;
|
|
51
|
+
appId?: string;
|
|
52
|
+
}>;
|
|
45
53
|
listPending(): Array<{
|
|
46
54
|
userId: string;
|
|
47
55
|
name: string;
|
|
48
56
|
code: string;
|
|
49
57
|
}>;
|
|
58
|
+
/**
|
|
59
|
+
* 当前配置的管理员(配对码私发对象):该应用**最早**成为 admin 的用户——
|
|
60
|
+
* 即首个使用者(产品语义=应用创建人)。按 appId 过滤为空(老数据缺 appId)时
|
|
61
|
+
* 回退全部 admin 中最早者。只发这一个人,不群发所有 admin(主人拍板 2026-10-17)
|
|
62
|
+
*/
|
|
63
|
+
primaryAdmin(appId?: string): {
|
|
64
|
+
userId: string;
|
|
65
|
+
name: string;
|
|
66
|
+
} | null;
|
|
67
|
+
/** 发起配对:已有未过期申请则复用原码(用户连发消息不换码,管理员侧通知可节流) */
|
|
50
68
|
beginPairing(userId: string, name: string, appId?: string): string;
|
|
51
69
|
approvePairing(code: string): {
|
|
52
70
|
ok: boolean;
|
|
@@ -71,12 +71,41 @@ export class AccessControl {
|
|
|
71
71
|
this.save();
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
+
/** 待审列表(配置页「接入申请」用):带过期时刻与来源应用,供前端算剩余有效期/按机器人分组 */
|
|
75
|
+
listPendingDetailed() {
|
|
76
|
+
this.evict();
|
|
77
|
+
return Object.values(this.data.pending).map(({ userId, name, code, expiresAt, appId }) => ({ userId, name, code, expiresAt, ...(appId ? { appId } : {}) }));
|
|
78
|
+
}
|
|
74
79
|
listPending() {
|
|
75
80
|
this.evict();
|
|
76
81
|
return Object.values(this.data.pending).map(({ userId, name, code }) => ({ userId, name, code }));
|
|
77
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* 当前配置的管理员(配对码私发对象):该应用**最早**成为 admin 的用户——
|
|
85
|
+
* 即首个使用者(产品语义=应用创建人)。按 appId 过滤为空(老数据缺 appId)时
|
|
86
|
+
* 回退全部 admin 中最早者。只发这一个人,不群发所有 admin(主人拍板 2026-10-17)
|
|
87
|
+
*/
|
|
88
|
+
primaryAdmin(appId) {
|
|
89
|
+
const byOldest = (list) => list.sort((a, b) => (a[1].pairedAt || '').localeCompare(b[1].pairedAt || ''));
|
|
90
|
+
const adminsOf = (pred) => byOldest(Object.entries(this.data.users).filter(([, u]) => u.role === 'admin' && pred(u)));
|
|
91
|
+
const pick = adminsOf((u) => appId === undefined || u.appId === appId)[0]
|
|
92
|
+
?? adminsOf(() => true)[0];
|
|
93
|
+
return pick ? { userId: pick[0], name: pick[1].name } : null;
|
|
94
|
+
}
|
|
95
|
+
/** 发起配对:已有未过期申请则复用原码(用户连发消息不换码,管理员侧通知可节流) */
|
|
78
96
|
beginPairing(userId, name, appId) {
|
|
79
97
|
this.evict();
|
|
98
|
+
const existing = this.data.pending[userId];
|
|
99
|
+
if (existing && existing.expiresAt > Date.now()) {
|
|
100
|
+
if (appId && !existing.appId)
|
|
101
|
+
existing.appId = appId;
|
|
102
|
+
// 真名升级:早期记录曾把 open_id 当 name 落库(解析失败降级)——后续解析出真名即覆盖,
|
|
103
|
+
// 配置页/通知随之显示真名;已存真名(用户自填或先到先解析)永不被 ou_ 串回写
|
|
104
|
+
if (name && !name.startsWith('ou_') && existing.name.startsWith('ou_'))
|
|
105
|
+
existing.name = name;
|
|
106
|
+
this.save();
|
|
107
|
+
return existing.code;
|
|
108
|
+
}
|
|
80
109
|
const code = String(randomInt(0, 1_000_000)).padStart(6, '0');
|
|
81
110
|
this.data.pending[userId] = { userId, name, code, expiresAt: Date.now() + TTL_MS, ...(appId ? { appId } : {}) };
|
|
82
111
|
this.save();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"access-control.js","sourceRoot":"","sources":["../../src/access/access-control.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"access-control.js","sourceRoot":"","sources":["../../src/access/access-control.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** 磁盘命中返回真名;未命中/过期返回 undefined(调用方走飞书 API) */
|
|
2
|
+
export declare function getCachedUserName(appId: string, openId: string): string | undefined;
|
|
3
|
+
export declare function putCachedUserName(appId: string, openId: string, name: string): void;
|
|
4
|
+
export declare function getCachedChatMeta(appId: string, chatId: string): {
|
|
5
|
+
name?: string;
|
|
6
|
+
chatType?: string;
|
|
7
|
+
} | undefined;
|
|
8
|
+
export declare function putCachedChatMeta(appId: string, chatId: string, meta: {
|
|
9
|
+
name?: string;
|
|
10
|
+
chatType?: string;
|
|
11
|
+
}): void;
|
|
12
|
+
/** openId 的 union_id(消息事件落盘);没有索引返回 undefined */
|
|
13
|
+
export declare function getCachedUserUnion(appId: string, openId: string): string | undefined;
|
|
14
|
+
/** 记录 openId → unionId(已存同值时零写入,不触落盘节流) */
|
|
15
|
+
export declare function putCachedUserUnion(appId: string, openId: string, unionId: string): void;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// 飞书用户名/群名持久化缓存:open_id / chat_id → 真名,全场景共用一份磁盘库。
|
|
2
|
+
// 背景(主人拍板 2026-10-18):名称翻译此前散落 chats-api 与 gateway 各建内存缓存——重启即空、
|
|
3
|
+
// 同一用户反复打飞书 API、多处逻辑漂移。这里统一为磁盘缓存(CONFIG_DIR/feishu-names.json):
|
|
4
|
+
// 重启不丢、跨场景命中(会话页 / 接入申请 / 配对通知 / access 回填)、TTL 长(用户名 30 天、
|
|
5
|
+
// 群名 3 天——群改名数日内可见)。解析失败态不落盘(只留各调用方内存短缓存,10 分钟自愈)。
|
|
6
|
+
// open_id 是应用维度标识:key 一律带 appId(同一人在不同应用下 open_id 不同,不能串库)。
|
|
7
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { CONFIG_DIR } from '../config.js';
|
|
10
|
+
const STORE_PATH = join(CONFIG_DIR, 'feishu-names.json');
|
|
11
|
+
const USER_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 用户名 30 天(飞书改名低频)
|
|
12
|
+
const CHAT_TTL_MS = 3 * 24 * 60 * 60 * 1000; // 群名 3 天(群改名数日内可见)
|
|
13
|
+
const PERSIST_MIN_MS = 30_000; // 落盘节流:30s 内的写入合并成一次(重启最多丢 30s 增量)
|
|
14
|
+
let store = { v: 1, users: {}, chats: {} };
|
|
15
|
+
let loaded = false;
|
|
16
|
+
let lastPersistAt = 0;
|
|
17
|
+
function load() {
|
|
18
|
+
if (loaded)
|
|
19
|
+
return;
|
|
20
|
+
loaded = true;
|
|
21
|
+
try {
|
|
22
|
+
if (existsSync(STORE_PATH)) {
|
|
23
|
+
const raw = JSON.parse(readFileSync(STORE_PATH, 'utf8'));
|
|
24
|
+
if (raw && typeof raw === 'object') {
|
|
25
|
+
store = { v: 1, users: raw.users ?? {}, chats: raw.chats ?? {}, unions: raw.unions ?? {} };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch { /* 损坏:空库起步,下次写盘自愈 */ }
|
|
30
|
+
}
|
|
31
|
+
function persist() {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
if (now - lastPersistAt < PERSIST_MIN_MS)
|
|
34
|
+
return; // 节流:高频写入合并
|
|
35
|
+
lastPersistAt = now;
|
|
36
|
+
try {
|
|
37
|
+
const tmp = `${STORE_PATH}.tmp-${process.pid}`;
|
|
38
|
+
writeFileSync(tmp, JSON.stringify(store), 'utf8');
|
|
39
|
+
renameSync(tmp, STORE_PATH);
|
|
40
|
+
}
|
|
41
|
+
catch { /* 落盘失败降级内存态,下轮写入再试 */ }
|
|
42
|
+
}
|
|
43
|
+
/** 磁盘命中返回真名;未命中/过期返回 undefined(调用方走飞书 API) */
|
|
44
|
+
export function getCachedUserName(appId, openId) {
|
|
45
|
+
load();
|
|
46
|
+
const e = store.users[`${appId}/${openId}`];
|
|
47
|
+
return e && Date.now() - e.at < USER_TTL_MS ? e.name : undefined;
|
|
48
|
+
}
|
|
49
|
+
export function putCachedUserName(appId, openId, name) {
|
|
50
|
+
load();
|
|
51
|
+
store.users[`${appId}/${openId}`] = { name, at: Date.now() };
|
|
52
|
+
persist();
|
|
53
|
+
}
|
|
54
|
+
export function getCachedChatMeta(appId, chatId) {
|
|
55
|
+
load();
|
|
56
|
+
const e = store.chats[`${appId}/${chatId}`];
|
|
57
|
+
if (!e || Date.now() - e.at >= CHAT_TTL_MS)
|
|
58
|
+
return undefined;
|
|
59
|
+
return { name: e.name, chatType: e.chatType };
|
|
60
|
+
}
|
|
61
|
+
export function putCachedChatMeta(appId, chatId, meta) {
|
|
62
|
+
load();
|
|
63
|
+
store.chats[`${appId}/${chatId}`] = { ...meta, at: Date.now() };
|
|
64
|
+
persist();
|
|
65
|
+
}
|
|
66
|
+
/** openId 的 union_id(消息事件落盘);没有索引返回 undefined */
|
|
67
|
+
export function getCachedUserUnion(appId, openId) {
|
|
68
|
+
load();
|
|
69
|
+
return store.unions?.[`${appId}/${openId}`];
|
|
70
|
+
}
|
|
71
|
+
/** 记录 openId → unionId(已存同值时零写入,不触落盘节流) */
|
|
72
|
+
export function putCachedUserUnion(appId, openId, unionId) {
|
|
73
|
+
load();
|
|
74
|
+
if (!store.unions)
|
|
75
|
+
store.unions = {};
|
|
76
|
+
const key = `${appId}/${openId}`;
|
|
77
|
+
if (store.unions[key] === unionId)
|
|
78
|
+
return;
|
|
79
|
+
store.unions[key] = unionId;
|
|
80
|
+
persist();
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=name-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"name-cache.js","sourceRoot":"","sources":["../../src/feishu/name-cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function resolveNamesByUnionViaSiblings(selfAppId: string, unionByOpenId: Map<string, string>): Promise<Map<string, string>>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// 兄弟应用桥:open_id 是应用维度标识,本应用「通讯录权限范围」外的用户(接入申请场景的
|
|
2
|
+
// 常态——申请人天然未被纳入范围)用 contact/v3 直查只会拿到空 items 或 41050。而消息事件
|
|
3
|
+
// 自带的 sender_id.union_id 在同一开发商的全部应用间通用(同租户自建应用实测 2026-10-18:
|
|
4
|
+
// 主应用查不到的用户,分身应用按 union_id 能查到真名)。
|
|
5
|
+
// 本桥按已知 openId→unionId 映射,遍历兄弟应用的注册请求口(gateway-registry)批量查询,
|
|
6
|
+
// 命中即返回;真名由调用方写入共享磁盘缓存(name-cache,key=本应用 openId),一次解析全场景受益。
|
|
7
|
+
// 无兄弟运行 / 兄弟也无权限或范围不含 → 返回空 Map,调用方按原样降级 ou 尾号展示。
|
|
8
|
+
import { listChatInfoSources } from '../gateway/gateway-registry.js';
|
|
9
|
+
export async function resolveNamesByUnionViaSiblings(selfAppId, unionByOpenId) {
|
|
10
|
+
const out = new Map();
|
|
11
|
+
// 同一 union 可能对应多个 openId 请求(理论上同应用同一人只有一个,稳妥去重)
|
|
12
|
+
const byUnion = new Map();
|
|
13
|
+
for (const [openId, unionId] of unionByOpenId) {
|
|
14
|
+
if (!unionId)
|
|
15
|
+
continue;
|
|
16
|
+
byUnion.set(unionId, [...(byUnion.get(unionId) ?? []), openId]);
|
|
17
|
+
}
|
|
18
|
+
if (!byUnion.size)
|
|
19
|
+
return out;
|
|
20
|
+
const siblings = listChatInfoSources()
|
|
21
|
+
.filter(([appId]) => appId !== selfAppId)
|
|
22
|
+
.map(([, req]) => req);
|
|
23
|
+
const pending = new Set(byUnion.keys());
|
|
24
|
+
for (const req of siblings) {
|
|
25
|
+
if (!pending.size)
|
|
26
|
+
break;
|
|
27
|
+
const qs = [...pending].map((u) => `user_ids=${encodeURIComponent(u)}`).join('&');
|
|
28
|
+
let items;
|
|
29
|
+
try {
|
|
30
|
+
const res = await req.request({
|
|
31
|
+
method: 'GET',
|
|
32
|
+
url: `/open-apis/contact/v3/users/batch?user_id_type=union_id&${qs}`,
|
|
33
|
+
});
|
|
34
|
+
if (res.code !== 0) {
|
|
35
|
+
console.log(`[真名桥] ${selfAppId} 兄弟应用返回 code=${res.code}(缺权限/范围外),换下一个继续`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
items = res.data?.items;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
console.log(`[真名桥] ${selfAppId} 兄弟应用请求异常:${e instanceof Error ? e.message : e}`);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (!Array.isArray(items))
|
|
45
|
+
continue;
|
|
46
|
+
for (const it of items) {
|
|
47
|
+
if (typeof it.union_id === 'string' && byUnion.has(it.union_id)
|
|
48
|
+
&& typeof it.name === 'string' && it.name) {
|
|
49
|
+
for (const openId of byUnion.get(it.union_id))
|
|
50
|
+
out.set(openId, it.name);
|
|
51
|
+
pending.delete(it.union_id);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (out.size) {
|
|
56
|
+
console.log(`[真名桥] ${selfAppId} 命中 ${out.size}/${byUnion.size} 个真名:${[...out.entries()].map(([id, n]) => `${id.slice(0, 12)}…→${n}`).join(',')}`);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
console.log(`[真名桥] ${selfAppId} 未命中(${byUnion.size} 个 union × ${siblings.length} 个兄弟应用均无结果)`);
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=sibling-names.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sibling-names.js","sourceRoot":"","sources":["../../src/feishu/sibling-names.ts"],"names":[],"mappings":"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"}
|
|
@@ -255,6 +255,19 @@ export declare class FeishuGateway {
|
|
|
255
255
|
sendCard(chatId: string, card: unknown): Promise<string>;
|
|
256
256
|
/** 以卡片形态发送 markdown 文本(宽度跟随 cardWidth getter) */
|
|
257
257
|
sendText(chatId: string, markdown: string): Promise<string>;
|
|
258
|
+
/**
|
|
259
|
+
* 按 open_id 主动私聊发文本卡(配对码私发管理员等「机器人发起单聊」场景)。
|
|
260
|
+
* 与 sendText 同一凭证同一接口,仅 receive_id_type 换 open_id——需要机器人与该用户
|
|
261
|
+
* 存在可见关系(同租户即可,无需先成为好友);失败由调用方捕获降级(日志留码)
|
|
262
|
+
*/
|
|
263
|
+
sendTextToUser(openId: string, markdown: string): Promise<string>;
|
|
264
|
+
/**
|
|
265
|
+
* 批量解析 open_id → 飞书姓名(contact batch,需 contact:user.base:readonly)。
|
|
266
|
+
* 成功缓存 24h、失败短缓存 10 分钟(权限补开后十分钟自动恢复真名);未返回的 id 同样落
|
|
267
|
+
* 失败缓存防重打。与 web 层 chats-api 的同名解析相互独立(各自缓存,只读低频不叠加压力)
|
|
268
|
+
*/
|
|
269
|
+
private readonly userNameCache;
|
|
270
|
+
resolveUserNames(openIds: string[]): Promise<Map<string, string>>;
|
|
258
271
|
/** 更新已发送卡片(流式进度刷新;消息卡通道:入口剥离 cardkit 专属属性)。
|
|
259
272
|
* R3:流式正文同样出站,先深度脱敏(chatId 不可得 → 告警通道退化为仅审计+日志,
|
|
260
273
|
* 流式块的金丝雀告警由 wiring 在 finalText/流文本层覆盖) */
|