@jesonliu/lark-claudecode-bridge 1.0.2 → 1.0.4
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 +13 -0
- package/assets/web/js/core.js +3 -1
- package/assets/web/js/pages/larkcli-device.js +188 -0
- package/assets/web/js/pages/overview.js +244 -5
- package/assets/web/js/ui.js +66 -2
- package/config.example.yaml +3 -0
- package/dist/bin/lcb.js +10 -0
- package/dist/bin/lcb.js.map +1 -1
- package/dist/executor/notify-server.d.ts +0 -2
- package/dist/executor/notify-server.js +3 -10
- package/dist/executor/notify-server.js.map +1 -1
- package/dist/executor/plugin-manager.d.ts +16 -0
- package/dist/executor/plugin-manager.js +40 -1
- package/dist/executor/plugin-manager.js.map +1 -1
- package/dist/gateway/card-builder.d.ts +6 -5
- package/dist/gateway/card-builder.js +10 -9
- package/dist/gateway/card-builder.js.map +1 -1
- package/dist/gateway/diff-card.d.ts +7 -7
- package/dist/gateway/diff-card.js +14 -8
- package/dist/gateway/diff-card.js.map +1 -1
- package/dist/gateway/feishu-gateway.d.ts +7 -1
- package/dist/gateway/feishu-gateway.js +8 -3
- package/dist/gateway/feishu-gateway.js.map +1 -1
- package/dist/index.js +100 -30
- package/dist/index.js.map +1 -1
- package/dist/lark-cli-manager.d.ts +336 -0
- package/dist/lark-cli-manager.js +985 -0
- package/dist/lark-cli-manager.js.map +1 -0
- package/dist/notify-sop.d.ts +1 -1
- package/dist/notify-sop.js +3 -3
- package/dist/notify-sop.js.map +1 -1
- package/dist/session/commands.d.ts +3 -0
- package/dist/session/commands.js +3 -3
- package/dist/session/commands.js.map +1 -1
- package/dist/session/plugin-command.js +18 -2
- package/dist/session/plugin-command.js.map +1 -1
- package/dist/session/session-store.d.ts +9 -2
- package/dist/session/session-store.js +9 -3
- package/dist/session/session-store.js.map +1 -1
- package/dist/util/file-tracker.d.ts +13 -6
- package/dist/util/file-tracker.js +28 -10
- package/dist/util/file-tracker.js.map +1 -1
- package/dist/util/npm.d.ts +13 -0
- package/dist/util/npm.js +44 -0
- package/dist/util/npm.js.map +1 -0
- package/dist/util/terminal-window.d.ts +111 -0
- package/dist/util/terminal-window.js +425 -0
- package/dist/util/terminal-window.js.map +1 -0
- package/dist/util/workspace-diff.d.ts +4 -7
- package/dist/util/workspace-diff.js +30 -35
- package/dist/util/workspace-diff.js.map +1 -1
- package/dist/web/server.js +94 -4
- package/dist/web/server.js.map +1 -1
- package/dist/web/update.d.ts +1 -5
- package/dist/web/update.js +2 -20
- package/dist/web/update.js.map +1 -1
- package/package.json +1 -1
package/assets/web/css/main.css
CHANGED
|
@@ -509,3 +509,16 @@ tr.empty-row td { text-align: center; padding: 22px 10px; color: var(--muted); }
|
|
|
509
509
|
/* 实心危险按钮(弹窗确认键):与 .btn.danger(描边)区分 */
|
|
510
510
|
.btn.danger-solid { background: var(--danger); border-color: var(--danger); color: #fff; }
|
|
511
511
|
.btn.danger-solid:hover { background: var(--danger); border-color: var(--danger); color: #fff; filter: brightness(1.07); }
|
|
512
|
+
|
|
513
|
+
/* ---------- 扫码授权二维码(设备流) ---------- */
|
|
514
|
+
/* 白底是刻意的:lark-cli 生成的 PNG 本身是白底黑码,容器若跟随暗色主题就会在图片
|
|
515
|
+
周围露出一圈深色,看着像二维码被裁掉。固定白底在任何主题下都与图片本身一致,
|
|
516
|
+
也保证手机扫得出来。 */
|
|
517
|
+
.qr-box {
|
|
518
|
+
display: flex; justify-content: center; align-items: center;
|
|
519
|
+
margin-top: 12px; padding: 12px; background: #fff;
|
|
520
|
+
border: 1px solid var(--border); border-radius: 12px; color-scheme: light;
|
|
521
|
+
}
|
|
522
|
+
.qr-box img { display: block; width: 220px; height: 220px; }
|
|
523
|
+
/* 链接可整段选中复制;长 URL 必须能折行,否则会把弹窗撑爆 */
|
|
524
|
+
.qr-url { margin-top: 8px; line-height: 1.6; word-break: break-all; user-select: all; }
|
package/assets/web/js/core.js
CHANGED
|
@@ -20,7 +20,9 @@ export async function api(method, path, body) {
|
|
|
20
20
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
21
21
|
});
|
|
22
22
|
const data = await res.json().catch(() => ({}));
|
|
23
|
-
|
|
23
|
+
// 把响应体一并挂到 Error 上(message 不变,向后兼容):调用方常需要 error 之外的结构化字段,
|
|
24
|
+
// 如扫码授权的 hint:'install'|'config'——丢了它前端就只能干瞪眼
|
|
25
|
+
if (!res.ok) throw Object.assign(new Error(data.error || `HTTP ${res.status}`), data);
|
|
24
26
|
return data;
|
|
25
27
|
}
|
|
26
28
|
/** doc 深拷贝快照(抽屉取消回滚 / 打开前留存) */
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// ============ 飞书扫码授权(设备流):页面内二维码弹窗 ============
|
|
2
|
+
// 与「拉终端」并列的第二条授权路径——把 lark-cli 的 device flow 搬到配置页里,用户直接
|
|
3
|
+
// 扫码完成,不用切窗口。它不经过终端探测,因此无桌面 / SSH 环境同样可用(那条路上
|
|
4
|
+
// 「去终端授权」原本是无出口的死路)。
|
|
5
|
+
//
|
|
6
|
+
// 三段式(后端编排,见 src/lark-cli-manager.ts 的设备流一节):
|
|
7
|
+
// ① POST /device/start → 拿 verification_url + 二维码(base64 PNG)
|
|
8
|
+
// ② 用户用飞书扫码
|
|
9
|
+
// ③ 后端后台跑 `auth login --device-code` 收尾,前端只轮询零成本的内存态端点
|
|
10
|
+
//
|
|
11
|
+
// **device_code 全程只存后端内存,从不下发到浏览器。**
|
|
12
|
+
//
|
|
13
|
+
// 约束:本模块顶层零 DOM 访问——纯函数要能被单测直接 import(见 tests/web/larkcli-device.test.ts)。
|
|
14
|
+
import { esc, toast, api } from '../core.js';
|
|
15
|
+
import { bindDialogKeys, mountDialog } from '../ui.js';
|
|
16
|
+
|
|
17
|
+
const POLL_MS = 2000; // 状态轮询:后端是纯内存查询,零成本(对比终端弹窗的 5s,那是真跑 auth status)
|
|
18
|
+
const TICK_MS = 1000; // 倒计时重绘
|
|
19
|
+
|
|
20
|
+
/** 纯函数(可测):秒 → mm:ss(负数与脏值都收敛到 00:00) */
|
|
21
|
+
export function fmtRemain(sec) {
|
|
22
|
+
const s = Math.max(0, Math.floor(Number(sec) || 0));
|
|
23
|
+
return `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 纯函数(可测):终态 → 给用户看的一句话 */
|
|
27
|
+
export function deviceStateHint(state, error) {
|
|
28
|
+
if (state === 'done') return '✅ 授权完成,可以关闭本弹窗了。';
|
|
29
|
+
if (state === 'expired') return error || '二维码已过期,请重新生成。';
|
|
30
|
+
if (state === 'failed') return error || '授权未完成,可重新生成二维码再试。';
|
|
31
|
+
// 桥接器重启会冲掉后端的会话内存,但 token 可能已经落盘——提示里要点出这个可能
|
|
32
|
+
if (state === 'none') return '授权会话已失效(桥接器可能已重启),请重新生成二维码。';
|
|
33
|
+
return '';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 打开扫码授权弹窗。
|
|
38
|
+
* @param {object} opts
|
|
39
|
+
* @param {(r:any)=>void} [opts.onChange] 收到最新 /api/lark-cli 结果时回调,用于同步刷新概览行
|
|
40
|
+
* @param {()=>void} [opts.onTerminal] 用户选「改用终端窗口」时回调,接回既有终端链路
|
|
41
|
+
* @param {boolean} [opts.wasAuthorized] 打开时的授权态快照:区分「本来就好」与「本次扫码完成」
|
|
42
|
+
*/
|
|
43
|
+
export function openLarkCliDeviceDialog({ onChange, onTerminal, wasAuthorized = false } = {}) {
|
|
44
|
+
const { mask, unmount } = mountDialog(`
|
|
45
|
+
<div class="modal dlg" role="dialog" aria-modal="true" style="width:min(520px,94vw)">
|
|
46
|
+
<h3 class="dlg-title">飞书授权(扫码完成)</h3>
|
|
47
|
+
<div class="dlg-msg">用飞书 App 扫描下方二维码,按提示确认授权。授权成功后本页会自动更新。</div>
|
|
48
|
+
<div id="lqdBody"><div class="hint">正在向飞书申请授权二维码…</div></div>
|
|
49
|
+
<div class="dlg-foot">
|
|
50
|
+
<button class="btn" data-lqd="terminal">改用终端窗口</button>
|
|
51
|
+
<button class="btn" data-lqd="copy" hidden>复制链接</button>
|
|
52
|
+
<button class="btn" data-lqd="regen" hidden>重新生成二维码</button>
|
|
53
|
+
<button class="btn primary" data-lqd="close">关闭</button>
|
|
54
|
+
</div>
|
|
55
|
+
</div>`);
|
|
56
|
+
|
|
57
|
+
const body = mask.querySelector('#lqdBody');
|
|
58
|
+
const btnCopy = mask.querySelector('[data-lqd="copy"]');
|
|
59
|
+
const btnRegen = mask.querySelector('[data-lqd="regen"]');
|
|
60
|
+
let session = null; // 最近一次 start 的响应(存链接与二维码)
|
|
61
|
+
let baseLeft = 0; // 服务端给的剩余秒数
|
|
62
|
+
let baseAt = 0; // 收到该秒数时的本地时刻
|
|
63
|
+
let poll = null;
|
|
64
|
+
let tick = null;
|
|
65
|
+
let closed = false;
|
|
66
|
+
|
|
67
|
+
const stopTimers = () => {
|
|
68
|
+
if (poll) { clearInterval(poll); poll = null; }
|
|
69
|
+
if (tick) { clearInterval(tick); tick = null; }
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// 用「服务端剩余秒 + 本地流逝」的差分量:直接拿服务端绝对时间戳会被浏览器时钟差搞出
|
|
73
|
+
// 一个一打开就已过期的二维码
|
|
74
|
+
const remainNow = () => Math.max(0, baseLeft - (Date.now() - baseAt) / 1000);
|
|
75
|
+
|
|
76
|
+
const paintReady = (s) => {
|
|
77
|
+
const qr = s.qrDataUrl
|
|
78
|
+
? `<div class="qr-box"><img src="${esc(s.qrDataUrl)}" alt="飞书授权二维码" width="220" height="220"></div>`
|
|
79
|
+
: '<div class="hint" style="margin-top:8px">二维码生成失败,请复制下方链接在飞书中打开。</div>';
|
|
80
|
+
body.innerHTML = qr
|
|
81
|
+
+ `<div class="hint" style="margin-top:8px">二维码 <b id="lqdLeft">${fmtRemain(remainNow())}</b> 后失效`
|
|
82
|
+
+ (s.userCode ? ` · 授权码 <span class="chip">${esc(s.userCode)}</span>` : '')
|
|
83
|
+
+ '</div>'
|
|
84
|
+
+ `<div class="hint qr-url">${esc(s.verificationUrl || '')}</div>`;
|
|
85
|
+
btnCopy.hidden = false;
|
|
86
|
+
btnRegen.hidden = false;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const paintDone = (state, { identity, error } = {}) => {
|
|
90
|
+
const tag = state === 'done' ? 'ok' : 'warn';
|
|
91
|
+
const label = state === 'done' ? '已授权' : state === 'expired' ? '已过期' : '未完成';
|
|
92
|
+
body.innerHTML = `<div><span class="tag ${tag}">${label}</span>`
|
|
93
|
+
+ (identity ? ` <span class="chip">${esc(identity)}</span>` : '')
|
|
94
|
+
+ '</div>'
|
|
95
|
+
+ `<div class="hint" style="margin-top:8px">${esc(deviceStateHint(state, error))}</div>`;
|
|
96
|
+
btnCopy.hidden = true;
|
|
97
|
+
btnRegen.hidden = state === 'done';
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** 拉一次概览数据同步行内状态(拿不到就算了,别让弹窗崩) */
|
|
101
|
+
const syncRow = async () => {
|
|
102
|
+
if (!onChange) return;
|
|
103
|
+
const cur = await api('GET', '/api/lark-cli').catch(() => null);
|
|
104
|
+
if (cur) onChange(cur);
|
|
105
|
+
return cur;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const doPoll = async () => {
|
|
109
|
+
if (closed) return;
|
|
110
|
+
let r;
|
|
111
|
+
try {
|
|
112
|
+
r = await api('GET', '/api/lark-cli/device/status');
|
|
113
|
+
} catch {
|
|
114
|
+
return; // 静默重试:桥接器重启时会短暂失联,弹错反而吓人
|
|
115
|
+
}
|
|
116
|
+
if (closed) return;
|
|
117
|
+
if (r.state === 'pending') {
|
|
118
|
+
// 以服务端时间为准校正倒计时基准(每次轮询都对一次表)
|
|
119
|
+
baseLeft = r.expiresInSec ?? remainNow();
|
|
120
|
+
baseAt = Date.now();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
stopTimers();
|
|
124
|
+
if (r.state === 'none') {
|
|
125
|
+
// 内存态被冲(桥接器重启),但用户可能已经扫完了:回查授权态兜底,
|
|
126
|
+
// 否则会把一次成功的授权报成失败
|
|
127
|
+
const cur = await syncRow();
|
|
128
|
+
const authed = cur?.auth?.state === 'authorized';
|
|
129
|
+
return paintDone(authed && !wasAuthorized ? 'done' : 'none',
|
|
130
|
+
{ identity: cur?.auth?.identity });
|
|
131
|
+
}
|
|
132
|
+
await syncRow();
|
|
133
|
+
paintDone(r.state, { identity: r.identity, error: r.error });
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const doStart = async (regenerate) => {
|
|
137
|
+
stopTimers();
|
|
138
|
+
body.innerHTML = '<div class="hint">正在向飞书申请授权二维码…</div>';
|
|
139
|
+
btnCopy.hidden = true;
|
|
140
|
+
btnRegen.hidden = true;
|
|
141
|
+
try {
|
|
142
|
+
session = await api('POST', '/api/lark-cli/device/start', { regenerate: !!regenerate });
|
|
143
|
+
} catch (e) {
|
|
144
|
+
// 后端把「未安装」「应用未配置」等前置问题写成了可照做的文案,直接透出
|
|
145
|
+
body.innerHTML = `<div><span class="tag warn">无法发起</span></div>`
|
|
146
|
+
+ `<div class="hint" style="margin-top:8px">${esc(e.message)}</div>`;
|
|
147
|
+
toast(e.message, true);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (closed) return;
|
|
151
|
+
baseLeft = session.expiresInSec ?? 0;
|
|
152
|
+
baseAt = Date.now();
|
|
153
|
+
paintReady(session);
|
|
154
|
+
poll = setInterval(doPoll, POLL_MS);
|
|
155
|
+
tick = setInterval(() => {
|
|
156
|
+
const el = body.querySelector('#lqdLeft');
|
|
157
|
+
if (el) el.textContent = fmtRemain(remainNow());
|
|
158
|
+
}, TICK_MS);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const close = () => {
|
|
162
|
+
closed = true;
|
|
163
|
+
stopTimers();
|
|
164
|
+
unbind();
|
|
165
|
+
unmount();
|
|
166
|
+
};
|
|
167
|
+
const unbind = bindDialogKeys((k) => { if (k === 'Escape') close(); });
|
|
168
|
+
|
|
169
|
+
mask.querySelector('[data-lqd="close"]').onclick = close;
|
|
170
|
+
mask.querySelector('[data-lqd="regen"]').onclick = () => void doStart(true);
|
|
171
|
+
// 关弹窗**不动后端会话**(沿用终端弹窗「关弹窗不中断流程」的既有理念):
|
|
172
|
+
// 重新打开时 start 不带 regenerate → 后端幂等复用同一个 device code,二维码原样回来
|
|
173
|
+
mask.querySelector('[data-lqd="terminal"]').onclick = () => { close(); onTerminal?.(); };
|
|
174
|
+
btnCopy.onclick = async () => {
|
|
175
|
+
const url = session?.verificationUrl;
|
|
176
|
+
if (!url) return;
|
|
177
|
+
try {
|
|
178
|
+
await navigator.clipboard.writeText(url);
|
|
179
|
+
toast('授权链接已复制');
|
|
180
|
+
} catch {
|
|
181
|
+
toast('复制失败,请手动选中链接复制', true);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
mask.onclick = (e) => { if (e.target === mask) close(); };
|
|
185
|
+
|
|
186
|
+
void doStart(false);
|
|
187
|
+
return { close };
|
|
188
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// ============ 概览 ============
|
|
2
2
|
// 桥接器启停 / 版本更新(宿主进程管理:embedded 页面随进程生死,独立页面经 PID 跨进程操作)
|
|
3
3
|
import { S, $, esc, toast, api, refresh } from '../core.js';
|
|
4
|
-
import { confirmDialog } from '../ui.js';
|
|
4
|
+
import { bindDialogKeys, confirmDialog, mountDialog, progressDialog } from '../ui.js';
|
|
5
|
+
import { openLarkCliDeviceDialog } from './larkcli-device.js';
|
|
5
6
|
|
|
6
7
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
7
8
|
const UPD = { checkedAt: 0, data: null };
|
|
9
|
+
const LARKCLI = { checkedAt: 0, data: null }; // 飞书官方 CLI 状态缓存(30s 节流,同 UPD)
|
|
8
10
|
|
|
9
11
|
function showMask(inner) {
|
|
10
12
|
let m = $('#brMask');
|
|
@@ -120,12 +122,12 @@ async function doCheckUpdate() {
|
|
|
120
122
|
async function runUpdateFlow() {
|
|
121
123
|
const running = S.status?.embedded || S.status?.bridge?.running;
|
|
122
124
|
const extra = running ? ',完成后自动重启桥接器(页面将短暂失联后自动恢复)' : '(桥接器未在运行,下次启动生效)';
|
|
123
|
-
if (!(await confirmDialog({ title: '一键更新', message: `将通过 npm
|
|
125
|
+
if (!(await confirmDialog({ title: '一键更新', message: `将通过 npm 安装最新版本并同步更新飞书 CLI(@larksuite/cli)${extra},可能需要 1-2 分钟。`, confirmText: '开始更新' }))) return;
|
|
124
126
|
const btn = $('#btnUpdRun');
|
|
125
127
|
if (btn) { btn.disabled = true; btn.textContent = '更新中…'; }
|
|
126
128
|
try {
|
|
127
|
-
await api('POST', '/api/update/run');
|
|
128
|
-
UPD.checkedAt = 0;
|
|
129
|
+
await api('POST', '/api/update/run', { larkCli: true });
|
|
130
|
+
UPD.checkedAt = 0; LARKCLI.checkedAt = 0;
|
|
129
131
|
if (running) await restartFlow();
|
|
130
132
|
else toast('✅ 更新完成,下次启动生效');
|
|
131
133
|
await refresh();
|
|
@@ -135,6 +137,240 @@ async function runUpdateFlow() {
|
|
|
135
137
|
}
|
|
136
138
|
}
|
|
137
139
|
|
|
140
|
+
/** 纯函数(可测):与后端 needsLarkCliConfig 同源——detail 含 not_configured 即「未配置应用」 */
|
|
141
|
+
function larkCliNotConfigured(r) {
|
|
142
|
+
return r?.auth?.state === 'unauthorized' && /not_configured/i.test(r?.auth?.detail || '');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** 纯函数(可测):概览表格行里「飞书 CLI」的状态 HTML(不含操作按钮,按钮由 render 按需挂) */
|
|
146
|
+
export function larkCliStatHtml(r) {
|
|
147
|
+
if (!r) return '<span class="hint">未检查</span>';
|
|
148
|
+
if (!r.installed) return '<span class="tag off">未安装</span>';
|
|
149
|
+
// authorized 之外一律呈现为「未授权」(含探测不出的 unknown):UI 不留没有出口的状态,
|
|
150
|
+
// 两种情况都能点授权按钮。detail 悬浮可见(unknown 现已带诊断线索),便于自查。
|
|
151
|
+
const authTag = r.auth?.state === 'authorized'
|
|
152
|
+
? '<span class="tag ok">已授权</span>'
|
|
153
|
+
: `<span class="tag warn"${r.auth?.detail ? ` title="${esc(r.auth.detail)}"` : ''}>未授权</span>`;
|
|
154
|
+
// skill 字段为旧后端所无(undefined 时不显示),与 auth 解耦展示
|
|
155
|
+
const skillTag = r.skill
|
|
156
|
+
? (r.skill.installed ? '<span class="tag ok">SKILL</span>' : '<span class="tag warn">SKILL 未装</span>')
|
|
157
|
+
: '';
|
|
158
|
+
return `<span class="tag ok">已安装</span> <span class="chip">${r.version ? `v${esc(r.version)}` : '版本未知'}</span>`
|
|
159
|
+
+ (r.hasUpdate ? ' <span class="tag warn">有新版本</span>' : '')
|
|
160
|
+
+ (skillTag ? ` ${skillTag}` : '')
|
|
161
|
+
+ ` ${authTag}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 扫码授权弹窗(授权主路径)。弹窗里的「改用终端窗口」接回既有终端链路——
|
|
166
|
+
* 那条路一行没改,仍是拉不起无桌面环境时的兜底,也保留了在终端里补跑命令的能力。
|
|
167
|
+
*/
|
|
168
|
+
function openDeviceDialog() {
|
|
169
|
+
openLarkCliDeviceDialog({
|
|
170
|
+
wasAuthorized: LARKCLI.data?.auth?.state === 'authorized',
|
|
171
|
+
onChange: (r) => { LARKCLI.data = r; LARKCLI.checkedAt = Date.now(); renderLarkCliResult(); },
|
|
172
|
+
onTerminal: () => void larkCliActionFlow('auth'),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function renderLarkCliResult() {
|
|
177
|
+
const el = $('#larkCliStat');
|
|
178
|
+
const r = LARKCLI.data;
|
|
179
|
+
if (!el) return;
|
|
180
|
+
el.innerHTML = `${larkCliStatHtml(r)} <span id="larkCliArea"></span>`;
|
|
181
|
+
const area = $('#larkCliArea');
|
|
182
|
+
if (!area || !r) return;
|
|
183
|
+
const add = (label, op) => {
|
|
184
|
+
const b = document.createElement('button');
|
|
185
|
+
b.className = 'btn sm';
|
|
186
|
+
b.textContent = label;
|
|
187
|
+
b.onclick = () => (op === 'auth-device' ? openDeviceDialog() : larkCliActionFlow(op));
|
|
188
|
+
area.appendChild(b);
|
|
189
|
+
area.appendChild(document.createTextNode(' '));
|
|
190
|
+
};
|
|
191
|
+
if (!r.installed) { add('安装', 'install'); return; }
|
|
192
|
+
if (r.hasUpdate) add('更新', 'update');
|
|
193
|
+
// 未配置应用(not_configured)优先给「配置应用」向导;其余一律给授权入口——
|
|
194
|
+
// 已授权也给(token 会过期,随时可重走一遍 device flow 刷新)。
|
|
195
|
+
// 授权走**页面内二维码**(主路径,无桌面环境同样可用);终端窗口降级为弹窗里的备选。
|
|
196
|
+
if (larkCliNotConfigured(r)) add('配置应用', 'config');
|
|
197
|
+
else if (r.auth?.state === 'authorized') add('重新授权', 'auth-device');
|
|
198
|
+
else add('扫码授权', 'auth-device');
|
|
199
|
+
if (r.skill && !r.skill.installed) add('装 SKILL', 'skill');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function doCheckLarkCli() {
|
|
203
|
+
const el = $('#larkCliStat'); if (!el) return;
|
|
204
|
+
el.innerHTML = '<span class="hint">检测中…</span>';
|
|
205
|
+
try {
|
|
206
|
+
LARKCLI.data = await api('GET', '/api/lark-cli?check=1');
|
|
207
|
+
LARKCLI.checkedAt = Date.now();
|
|
208
|
+
renderLarkCliResult();
|
|
209
|
+
} catch (e) {
|
|
210
|
+
el.innerHTML = `<span class="hint">检测失败:${esc(e.message)}</span>`;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 终端已拉起后的提示弹窗:终端是 detached 的独立进程,这里的「检测」「关闭」
|
|
216
|
+
* 都只作用于本弹窗,不影响终端里的安装/授权。
|
|
217
|
+
*/
|
|
218
|
+
function openLarkCliTerminalDialog(op) {
|
|
219
|
+
const taskLabel = op === 'auth' ? '授权' : op === 'config' ? '配置应用' : '安装 / 更新';
|
|
220
|
+
const { mask, unmount } = mountDialog(`
|
|
221
|
+
<div class="modal dlg" role="dialog" aria-modal="true" style="width:min(600px,94vw)">
|
|
222
|
+
<h3 class="dlg-title">飞书 CLI ${taskLabel}</h3>
|
|
223
|
+
<div class="dlg-msg">已拉起<b>终端窗口</b>执行命令,请按窗口里的提示完成${op === 'auth' ? '授权' : op === 'config' ? '应用配置(在浏览器完成创建后终端会自动继续)' : '安装与配置、授权'}。</div>
|
|
224
|
+
<div class="hint" style="margin-top:10px">关闭本弹窗<b>不会中断</b>终端里的进程;终端还在跑时请不要关掉它。</div>
|
|
225
|
+
<div id="lktStat" style="margin-top:12px"><span class="hint">检测中…</span></div>
|
|
226
|
+
<div class="dlg-foot">
|
|
227
|
+
<button class="btn" data-lkt="auth" hidden>去终端授权</button>
|
|
228
|
+
<button class="btn" data-lkt="check">检测</button>
|
|
229
|
+
<button class="btn primary" data-lkt="close">关闭</button>
|
|
230
|
+
</div>
|
|
231
|
+
</div>`);
|
|
232
|
+
const statEl = mask.querySelector('#lktStat');
|
|
233
|
+
const authBtn = mask.querySelector('[data-lkt="auth"]');
|
|
234
|
+
const POLL_MS = 5000;
|
|
235
|
+
const POLL_MAX = 120; // 约 10 分钟:安装 + 走完 device flow 通常远快于此
|
|
236
|
+
let timer = null;
|
|
237
|
+
let polls = 0;
|
|
238
|
+
let busy = false; // 去重:手动「检测」与自动轮询不叠加
|
|
239
|
+
let stopped = false;
|
|
240
|
+
// 打开弹窗时是否已授权(快照):重新授权场景靠它区分「本来就好」与「刚跑完」
|
|
241
|
+
const wasAuthorized = LARKCLI.data?.auth?.state === 'authorized';
|
|
242
|
+
|
|
243
|
+
const stop = (note) => {
|
|
244
|
+
stopped = true;
|
|
245
|
+
if (timer) { clearInterval(timer); timer = null; }
|
|
246
|
+
if (note) statEl.insertAdjacentHTML('beforeend', `<div class="hint" style="margin-top:6px">${note}</div>`);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const paint = (r) => {
|
|
250
|
+
const auth = r.auth?.state;
|
|
251
|
+
const tag = !r.installed ? '<span class="tag off">未安装</span>'
|
|
252
|
+
: `<span class="tag ok">已安装</span> <span class="chip">${r.version ? `v${esc(r.version)}` : '版本未知'}</span> `
|
|
253
|
+
+ (auth === 'authorized' ? '<span class="tag ok">已授权</span>' : '<span class="tag warn">未授权</span>');
|
|
254
|
+
statEl.innerHTML = tag + (stopped ? '' : ` <span class="hint">(每 ${POLL_MS / 1000} 秒自动检测一次)</span>`);
|
|
255
|
+
// 已安装即给授权入口:未授权可去授权,已授权可重新授权
|
|
256
|
+
authBtn.hidden = !r.installed;
|
|
257
|
+
authBtn.textContent = auth === 'authorized' ? '重新授权' : '去终端授权';
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const refreshBoth = (r) => {
|
|
261
|
+
LARKCLI.data = r;
|
|
262
|
+
LARKCLI.checkedAt = Date.now();
|
|
263
|
+
renderLarkCliResult();
|
|
264
|
+
paint(r);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const tick = async () => {
|
|
268
|
+
if (busy) return;
|
|
269
|
+
busy = true;
|
|
270
|
+
try {
|
|
271
|
+
// 不带 check=1:纯本地探测(detect + auth status),轮询绝不能把 npm registry 打爆
|
|
272
|
+
refreshBoth(await api('GET', '/api/lark-cli'));
|
|
273
|
+
polls++;
|
|
274
|
+
const ok = LARKCLI.data?.installed && LARKCLI.data?.auth?.state === 'authorized';
|
|
275
|
+
// 重新授权场景(打开弹窗时就已授权)不能一进来就判「完成」——终端的 device flow 还没跑完,
|
|
276
|
+
// 此时只持续刷新状态,由用户看终端结果后自行关闭。
|
|
277
|
+
if (ok && !wasAuthorized) {
|
|
278
|
+
stop(op === 'auth' ? '✅ 授权完成,可以关闭本弹窗了。' : '✅ 安装并授权完成,可以关闭本弹窗了。');
|
|
279
|
+
} else if (polls >= POLL_MAX) {
|
|
280
|
+
stop(ok ? '本次自动检测到此为止,终端流程应已完成。' : '自动检测已停止(超时),可点「检测」手动刷新。');
|
|
281
|
+
}
|
|
282
|
+
} catch {
|
|
283
|
+
// 静默重试:一键更新会重启桥接器,轮询撞上失联窗口属预期,弹错反而吓人
|
|
284
|
+
} finally { busy = false; }
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const doCheck = async () => {
|
|
288
|
+
if (busy) return;
|
|
289
|
+
busy = true;
|
|
290
|
+
statEl.innerHTML = '<span class="hint">检测中…</span>';
|
|
291
|
+
try {
|
|
292
|
+
refreshBoth(await api('GET', '/api/lark-cli'));
|
|
293
|
+
} catch (e) {
|
|
294
|
+
statEl.innerHTML = `<span class="hint">检测失败:${esc(e.message)}</span>`;
|
|
295
|
+
} finally { busy = false; }
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const close = () => {
|
|
299
|
+
stopped = true;
|
|
300
|
+
if (timer) { clearInterval(timer); timer = null; }
|
|
301
|
+
unbind();
|
|
302
|
+
unmount();
|
|
303
|
+
};
|
|
304
|
+
const unbind = bindDialogKeys((k) => { if (k === 'Escape') close(); });
|
|
305
|
+
mask.querySelector('[data-lkt="close"]').onclick = close;
|
|
306
|
+
mask.querySelector('[data-lkt="check"]').onclick = () => void doCheck();
|
|
307
|
+
authBtn.onclick = () => { close(); void larkCliActionFlow('auth'); };
|
|
308
|
+
mask.onclick = (e) => { if (e.target === mask) close(); };
|
|
309
|
+
timer = setInterval(tick, POLL_MS);
|
|
310
|
+
void tick(); // 立即给一次现状:安装 / 配置流程下可直接看到「完成」并停止轮询
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function larkCliActionFlow(op = 'install') {
|
|
314
|
+
const meta = {
|
|
315
|
+
install: {
|
|
316
|
+
what: '安装',
|
|
317
|
+
msg: '将打开一个终端窗口依次执行:<code>npm install -g @larksuite/cli</code>、官方 SKILL 安装、'
|
|
318
|
+
+ '(未配置应用时)<code>lark-cli config init --new</code> 配置向导、<code>lark-cli auth login</code> 授权登录。'
|
|
319
|
+
+ '<br><br>若本机没有可用终端(无桌面 / SSH 会话),会自动降级为后台静默安装。',
|
|
320
|
+
},
|
|
321
|
+
update: {
|
|
322
|
+
what: '更新',
|
|
323
|
+
msg: '将打开一个终端窗口更新 lark-cli 并顺势完成官方 SKILL 安装、(未配置应用时)配置向导与授权登录。'
|
|
324
|
+
+ '<br><br>若本机没有可用终端(无桌面 / SSH 会话),会自动降级为后台静默安装。',
|
|
325
|
+
},
|
|
326
|
+
auth: {
|
|
327
|
+
what: '授权登录',
|
|
328
|
+
msg: '将打开一个终端窗口执行 <code>lark-cli auth login --recommend</code>:按窗口里的提示在浏览器或飞书中确认即可。'
|
|
329
|
+
+ '<br><br>已授权时也可重跑(用于刷新授权、补勾选新权限)。'
|
|
330
|
+
+ '<br><br>若本机没有可用终端(无桌面 / SSH 会话),需在服务器上手动执行该命令。',
|
|
331
|
+
},
|
|
332
|
+
config: {
|
|
333
|
+
what: '配置应用',
|
|
334
|
+
msg: '将打开一个终端窗口执行 <code>lark-cli config init --new</code>:按窗口打印的链接在浏览器完成飞书应用创建,'
|
|
335
|
+
+ '完成后会自动进入授权登录。'
|
|
336
|
+
+ '<br><br>若本机没有可用终端(无桌面 / SSH 会话),需在服务器上手动执行该命令。',
|
|
337
|
+
},
|
|
338
|
+
skill: {
|
|
339
|
+
what: 'SKILL 安装',
|
|
340
|
+
msg: '将后台执行 <code>npx skills add https://open.feishu.cn</code> 安装飞书官方 SKILL(教 AI 工具怎么用 lark-cli)。'
|
|
341
|
+
+ '<br><br>安装完成后需<b>重启桥接器</b>,飞书会话才能看到该 SKILL。',
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
const { what, msg } = meta[op] || meta.install;
|
|
345
|
+
if (!(await confirmDialog({ title: `飞书 CLI ${what}`, message: msg, confirmText: '开始' }))) return;
|
|
346
|
+
const rowBtn = $('#larkCliArea button');
|
|
347
|
+
if (rowBtn) rowBtn.disabled = true;
|
|
348
|
+
// 先弹进度框再发请求:静默降级那条路最长 5 分钟,没有它页面会静默无反馈
|
|
349
|
+
const dlg = progressDialog({ title: `飞书 CLI ${what}`, note: op === 'skill' ? '正在安装 SKILL…' : '正在探测可用终端…' });
|
|
350
|
+
try {
|
|
351
|
+
const r = await api('POST', '/api/lark-cli/action', { op });
|
|
352
|
+
if (r.mode === 'terminal') {
|
|
353
|
+
dlg.unmount(); // 交给终端弹窗,两者不叠加
|
|
354
|
+
openLarkCliTerminalDialog(op);
|
|
355
|
+
toast('已拉起终端窗口,请在终端里完成操作');
|
|
356
|
+
} else {
|
|
357
|
+
dlg.setNote(String(r.output || '').trim().slice(-600) || '完成');
|
|
358
|
+
if (r.reason) dlg.appendLog(`⚠️ 未使用终端:${r.reason}`);
|
|
359
|
+
const done = op === 'skill' ? '✅ SKILL 安装完成,重启桥接器后飞书会话可见' : `✅ 飞书 CLI ${what}完成`;
|
|
360
|
+
dlg.finish(true, done);
|
|
361
|
+
dlg.onClose(() => { void refresh(); });
|
|
362
|
+
toast(done);
|
|
363
|
+
}
|
|
364
|
+
await doCheckLarkCli();
|
|
365
|
+
} catch (e) {
|
|
366
|
+
dlg.appendLog(`❌ ${e.message}`);
|
|
367
|
+
dlg.finish(false, `❌ 飞书 CLI ${what}失败`);
|
|
368
|
+
dlg.onClose(() => renderLarkCliResult());
|
|
369
|
+
} finally {
|
|
370
|
+
if (rowBtn) rowBtn.disabled = false;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
138
374
|
export const page = {
|
|
139
375
|
id: 'overview',
|
|
140
376
|
title: '概览',
|
|
@@ -180,6 +416,7 @@ export const page = {
|
|
|
180
416
|
<table>
|
|
181
417
|
<tr><td>当前版本</td><td><span class="chip">v${esc(st.version)}</span></td></tr>
|
|
182
418
|
<tr><td>最新版本</td><td id="updLatest"><span class="hint">未检查</span></td></tr>
|
|
419
|
+
<tr><td>飞书 CLI(lark-cli)</td><td id="larkCliStat"><span class="hint">检测中…</span></td></tr>
|
|
183
420
|
</table>
|
|
184
421
|
<div style="display:flex; gap:10px; margin-top:12px; align-items:center">
|
|
185
422
|
<button class="btn" id="btnUpdCheck">检查更新</button>
|
|
@@ -205,11 +442,13 @@ export const page = {
|
|
|
205
442
|
<h3>常用指引</h3>
|
|
206
443
|
<div class="desc">飞书端与机器人对话即可使用;<code>/help</code> 查看全部命令。配置修改后:<b>应用 / 认证 / server 段需重启 lcb start</b>,工作区 / 权限 / 触发词下一条消息自动生效。</div>
|
|
207
444
|
</div>`;
|
|
208
|
-
// 启停按钮 +
|
|
445
|
+
// 启停按钮 + 版本更新 + 飞书 CLI 状态;自动检查 30s 节流(切回概览 tab 不重复打 npm)
|
|
209
446
|
const bind = (id, fn) => { const b = $(id); if (b) b.onclick = fn; };
|
|
210
447
|
bind('#btnBrStart', startFlow); bind('#btnBrStop', stopFlow); bind('#btnBrRestart', restartFlow);
|
|
211
448
|
bind('#btnUpdCheck', doCheckUpdate);
|
|
212
449
|
if (UPD.data && Date.now() - UPD.checkedAt < 30000) renderUpdateResult();
|
|
213
450
|
else doCheckUpdate();
|
|
451
|
+
if (LARKCLI.data && Date.now() - LARKCLI.checkedAt < 30000) renderLarkCliResult();
|
|
452
|
+
else doCheckLarkCli();
|
|
214
453
|
},
|
|
215
454
|
};
|
package/assets/web/js/ui.js
CHANGED
|
@@ -4,7 +4,7 @@ import { S, $, esc, toast, api, saveDoc, refresh, hooks } from './core.js';
|
|
|
4
4
|
// ============ 风格化弹窗(替代浏览器原生 confirm / prompt) ============
|
|
5
5
|
let openDialogs = 0; // 打开中的弹窗计数:ESC 只关最上层弹窗,不误触抽屉
|
|
6
6
|
/** 挂载弹窗遮罩(.modal-mask + .dlg);返回卸载函数 */
|
|
7
|
-
function mountDialog(innerHTML) {
|
|
7
|
+
export function mountDialog(innerHTML) {
|
|
8
8
|
const mask = document.createElement('div');
|
|
9
9
|
mask.className = 'modal-mask';
|
|
10
10
|
mask.innerHTML = innerHTML;
|
|
@@ -13,7 +13,7 @@ function mountDialog(innerHTML) {
|
|
|
13
13
|
return { mask, unmount() { openDialogs--; mask.remove(); } };
|
|
14
14
|
}
|
|
15
15
|
/** 捕获阶段拦截 ESC / Enter(先于抽屉的冒泡监听),弹窗关闭后自动解绑 */
|
|
16
|
-
function bindDialogKeys(onKey) {
|
|
16
|
+
export function bindDialogKeys(onKey) {
|
|
17
17
|
const handler = (e) => {
|
|
18
18
|
if (e.key !== 'Escape' && e.key !== 'Enter') return;
|
|
19
19
|
e.stopPropagation();
|
|
@@ -192,3 +192,67 @@ export function initDrawer() {
|
|
|
192
192
|
// ESC 有风格化弹窗打开时归弹窗(其自身在捕获阶段拦截并 stopPropagation,此处计数兜底)
|
|
193
193
|
document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && !openDialogs) dismissDrawer(); });
|
|
194
194
|
}
|
|
195
|
+
|
|
196
|
+
// ============ 长耗时单请求的进度弹窗 ============
|
|
197
|
+
/**
|
|
198
|
+
* 进度弹窗(时间缓动模拟,完成前不可关闭)。
|
|
199
|
+
*
|
|
200
|
+
* 观感与 plugins.js 的 runOpDialog 一致,但那个与插件页的 busyText / report / update-all 的
|
|
201
|
+
* 真实 i/N 推进深度耦合,泛化它等于改一个正在工作的界面(`main.css` 明确要求向后兼容)。
|
|
202
|
+
* 这里单独实现语义单一的版本,重复记为可接受代价。
|
|
203
|
+
*/
|
|
204
|
+
export function progressDialog({ title = '执行中', note = '请稍候…' } = {}) {
|
|
205
|
+
const { mask, unmount } = mountDialog(`
|
|
206
|
+
<div class="modal" role="dialog" aria-modal="true" style="width:min(640px,94vw)">
|
|
207
|
+
<div style="display:flex;align-items:center;gap:10px">
|
|
208
|
+
<div class="br-spin" style="margin:0"></div>
|
|
209
|
+
<b class="pd-title">${esc(title)}</b>
|
|
210
|
+
</div>
|
|
211
|
+
<div class="prog"><div class="prog-bar"></div></div>
|
|
212
|
+
<pre class="report" style="margin-top:12px;max-height:220px"></pre>
|
|
213
|
+
<div class="savebar" style="justify-content:flex-end">
|
|
214
|
+
<button class="btn primary" hidden>关闭</button>
|
|
215
|
+
</div>
|
|
216
|
+
</div>`);
|
|
217
|
+
const bar = mask.querySelector('.prog-bar');
|
|
218
|
+
const log = mask.querySelector('pre.report');
|
|
219
|
+
const spin = mask.querySelector('.br-spin');
|
|
220
|
+
const titleEl = mask.querySelector('.pd-title');
|
|
221
|
+
const closeBtn = mask.querySelector('button');
|
|
222
|
+
log.textContent = note;
|
|
223
|
+
let done = false;
|
|
224
|
+
let onCloseFn = null;
|
|
225
|
+
// 单请求拿不到内部进度:前 2s 线性到 70%,之后渐缓封顶 90%,完成跳 100%(同 runOpDialog 曲线)
|
|
226
|
+
let pct = 0;
|
|
227
|
+
const startedAt = Date.now();
|
|
228
|
+
const timer = setInterval(() => {
|
|
229
|
+
const elapsed = Date.now() - startedAt;
|
|
230
|
+
pct = elapsed < 2000 ? Math.min(70, (elapsed / 2000) * 70) : Math.min(90, pct + 1);
|
|
231
|
+
bar.style.width = `${pct}%`;
|
|
232
|
+
}, 300);
|
|
233
|
+
const close = () => {
|
|
234
|
+
if (!done) return; // 完成前没有关闭途径,避免把进行中的操作关成「不知道跑没跑」
|
|
235
|
+
unbind();
|
|
236
|
+
unmount();
|
|
237
|
+
onCloseFn?.();
|
|
238
|
+
};
|
|
239
|
+
const unbind = bindDialogKeys((k) => { if (k === 'Escape') close(); });
|
|
240
|
+
closeBtn.onclick = close;
|
|
241
|
+
mask.onclick = (e) => { if (e.target === mask) close(); };
|
|
242
|
+
return {
|
|
243
|
+
setNote(t) { log.textContent = t; },
|
|
244
|
+
appendLog(line) { log.textContent += `\n${line}`; log.scrollTop = log.scrollHeight; },
|
|
245
|
+
finish(ok, text) {
|
|
246
|
+
done = true;
|
|
247
|
+
clearInterval(timer);
|
|
248
|
+
bar.style.width = '100%';
|
|
249
|
+
if (ok) bar.classList.add('done');
|
|
250
|
+
spin.style.display = 'none';
|
|
251
|
+
titleEl.textContent = text || (ok ? `✅ ${title}` : `❌ ${title}`);
|
|
252
|
+
closeBtn.hidden = false;
|
|
253
|
+
closeBtn.focus();
|
|
254
|
+
},
|
|
255
|
+
onClose(fn) { onCloseFn = fn; },
|
|
256
|
+
unmount() { clearInterval(timer); unbind(); unmount(); },
|
|
257
|
+
};
|
|
258
|
+
}
|
package/config.example.yaml
CHANGED
|
@@ -94,3 +94,6 @@ concurrency: 3 # 全局并发上限(未单独配置的 app 沿
|
|
|
94
94
|
# session: # 会话行为微调;缺省用内置默认
|
|
95
95
|
# context_remind_tokens: 150000 # 上下文超长提醒阈值(tokens,估算口径 = 最后一次回复的输入+缓存规模);
|
|
96
96
|
# # 超过才提示「建议 /new」;0 = 关闭提醒。改后下一条消息热生效
|
|
97
|
+
# card: # 卡片展示(整块可选);改后下一条消息热生效
|
|
98
|
+
# width: fill # default = 飞书默认宽度(与消息等宽留边);fill = 撑满聊天窗口。
|
|
99
|
+
# # 作用于全部卡片:文本卡/图片卡/进度卡(含确认与问答区)/长回复卡等
|
package/dist/bin/lcb.js
CHANGED
|
@@ -16,6 +16,7 @@ import { VERSION } from '../version.js';
|
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import { ensureRuntimeDirs } from '../util/runtime-dirs.js';
|
|
18
18
|
import { installLogTee, cleanupOldLogs } from '../util/log-tee.js';
|
|
19
|
+
import { ensureLarkCli, AGENT_CONTEXT_ENV_KEYS } from '../lark-cli-manager.js';
|
|
19
20
|
function showPending(access) {
|
|
20
21
|
const pending = access.listPending();
|
|
21
22
|
if (pending.length)
|
|
@@ -57,6 +58,15 @@ async function main() {
|
|
|
57
58
|
}
|
|
58
59
|
// managed 模式:把 claude 段最新认证/模型值合并进自管目录 settings.json(首条消息前就位)
|
|
59
60
|
initManagedClaudeDir(config);
|
|
61
|
+
// Agent 上下文清场:本机若设了 HERMES_HOME / OPENCLAW_HOME / LARK_CHANNEL(其它 AI 工具
|
|
62
|
+
// 留下的),lark-cli 会自动进入「Agent 凭证绑定」分支——拒绝 config init、auth status
|
|
63
|
+
// 报 not bound。bridge 是飞书+ClaudeCode 专用进程,与这些 Agent 无关,进程级剔除后
|
|
64
|
+
// claude 会话(飞书里跑 lark-cli 命令)、web API 探测等所有下游子进程一并干净
|
|
65
|
+
for (const k of AGENT_CONTEXT_ENV_KEYS)
|
|
66
|
+
delete process.env[k];
|
|
67
|
+
// 深度拥抱飞书:后台检测/安装官方 Lark CLI(@larksuite/cli),不 await 不阻塞启动;
|
|
68
|
+
// 失败仅告警(npm 源不可达等不影响桥接器本身)
|
|
69
|
+
void ensureLarkCli().catch((e) => console.warn('[lark-cli] 检测流程异常(不影响启动):', e instanceof Error ? e.message : e));
|
|
60
70
|
await startBridge(CONFIG_PATH);
|
|
61
71
|
// 前台监听 stdin:管理员可直接在运行终端输入配对码批准。
|
|
62
72
|
// 现读现批(每次 load 新实例再 approve):桥运行中 beginPairing 写入的新 pending
|