@jesonliu/lark-claudecode-bridge 1.3.7 → 1.4.0
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/README.md +17 -0
- package/assets/web/js/main.js +2 -1
- package/assets/web/js/pages/memory.js +561 -0
- package/config.example.yaml +34 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +133 -1
- package/dist/config.js.map +1 -1
- package/dist/executor/claude-executor.d.ts +26 -3
- package/dist/executor/claude-executor.js +718 -293
- package/dist/executor/claude-executor.js.map +1 -1
- package/dist/executor/notify-server.d.ts +10 -0
- package/dist/executor/notify-server.js +11 -0
- package/dist/executor/notify-server.js.map +1 -1
- package/dist/executor/permission-gate.d.ts +8 -1
- package/dist/executor/permission-gate.js +7 -3
- package/dist/executor/permission-gate.js.map +1 -1
- package/dist/gateway/card-builder.d.ts +15 -3
- package/dist/gateway/card-builder.js +21 -10
- package/dist/gateway/card-builder.js.map +1 -1
- package/dist/gateway/feishu-gateway.d.ts +13 -0
- package/dist/gateway/feishu-gateway.js +34 -2
- package/dist/gateway/feishu-gateway.js.map +1 -1
- package/dist/gateway/progress-card.d.ts +4 -2
- package/dist/gateway/progress-card.js +52 -25
- package/dist/gateway/progress-card.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +225 -32
- package/dist/index.js.map +1 -1
- package/dist/memory-installer.d.ts +174 -0
- package/dist/memory-installer.js +1035 -0
- package/dist/memory-installer.js.map +1 -0
- package/dist/memory.d.ts +88 -0
- package/dist/memory.js +195 -0
- package/dist/memory.js.map +1 -0
- package/dist/security/paths.d.ts +3 -1
- package/dist/security/paths.js +28 -4
- package/dist/security/paths.js.map +1 -1
- package/dist/session/session-store.d.ts +15 -0
- package/dist/session/session-store.js +16 -0
- package/dist/session/session-store.js.map +1 -1
- package/dist/types.d.ts +56 -1
- package/dist/web/config-api.js +32 -0
- package/dist/web/config-api.js.map +1 -1
- package/dist/web/memory-api.d.ts +25 -0
- package/dist/web/memory-api.js +197 -0
- package/dist/web/memory-api.js.map +1 -0
- package/dist/web/server.js +73 -2
- package/dist/web/server.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { basename, join, relative, resolve } from 'node:path';
|
|
5
|
-
import { CONFIG_DIR, CONFIG_PATH, DEFAULT_CONTEXT_REMIND_TOKENS, loadConfig, sameApps } from './config.js';
|
|
5
|
+
import { CONFIG_DIR, CONFIG_PATH, DEFAULT_CONTEXT_REMIND_TOKENS, INBOX_DIR, loadConfig, sameApps } from './config.js';
|
|
6
6
|
import { DEFAULT_CLAUDE_DIR, initManagedClaudeDir, resolveClaudeDir } from './claude-config.js';
|
|
7
7
|
import { buildTaskEnv } from './task-env.js';
|
|
8
8
|
import { warnIfNoClaudeAuth } from './auth-precheck.js';
|
|
@@ -12,17 +12,19 @@ import { clearPidFile, writePidFile, stopExistingBridgeAndWait } from './web/lif
|
|
|
12
12
|
import { AccessControl } from './access/access-control.js';
|
|
13
13
|
import { FeishuGateway } from './gateway/feishu-gateway.js';
|
|
14
14
|
import { ProgressCard, feishuErrorBody } from './gateway/progress-card.js';
|
|
15
|
-
import { DECISION_TEXT, buildResultTailCard, buildWorkspaceSwitchedCard, resultChunkHeader, } from './gateway/card-builder.js';
|
|
15
|
+
import { DECISION_TEXT, buildPlanTailCard, buildResultTailCard, buildWorkspaceSwitchedCard, planChunkHeader, resultChunkHeader, } from './gateway/card-builder.js';
|
|
16
16
|
import { chunkText } from './util/chunk-text.js';
|
|
17
|
-
import { runTask } from './executor/claude-executor.js';
|
|
17
|
+
import { runTask, killResidentSession, killAllResidentSessions } from './executor/claude-executor.js';
|
|
18
18
|
import { buildFileDiffCards } from './gateway/diff-card.js';
|
|
19
19
|
import { collectFileDiff } from './util/workspace-diff.js';
|
|
20
20
|
import { PermissionGate, DEFAULT_ALLOW_TOOLS_LIST, DEFAULT_DANGEROUS_COMMANDS, DEFAULT_ALLOW_ALL_TOOLS, } from './executor/permission-gate.js';
|
|
21
21
|
import { discoverPlugins, resolvePluginPaths } from './executor/plugin-discovery.js';
|
|
22
|
-
import { createGatewaySender, createNotifyServer, NOTIFY_SERVER_NAME } from './executor/notify-server.js';
|
|
22
|
+
import { createGatewaySender, createNotifyServer, createNotifySenderSlot, NOTIFY_SERVER_NAME } from './executor/notify-server.js';
|
|
23
23
|
import { BRIDGE_MCP_JSON, readMcpServersFromJsonFile } from './web/skills-mcp-api.js';
|
|
24
24
|
import { FEISHU_NOTIFY_SOP_PROMPT, TOOL_HINT_PROMPT } from './notify-sop.js';
|
|
25
25
|
import { ensureProfileSettingsFile, resolveAppProfile, resolveAppProfileModel } from './app-profile.js';
|
|
26
|
+
import { MEMORY_PLUGIN_NAME, createMemoryProvider, ensureMemoryUserKeys, memoryEnvFor, } from './memory.js';
|
|
27
|
+
import { cleanupClientCliConf, ensureClientCliConf, stopManagedServer } from './memory-installer.js';
|
|
26
28
|
import { isImageFile } from './util/file-types.js';
|
|
27
29
|
import { FileTracker } from './util/file-tracker.js';
|
|
28
30
|
import { SessionStore, migrateLegacySessions } from './session/session-store.js';
|
|
@@ -61,7 +63,7 @@ const PLAN_DECISION_TEXT = {
|
|
|
61
63
|
'plan-approve': '✅ 计划已批准,开始执行(后续写操作免确认)',
|
|
62
64
|
'plan-revise': '📝 修改意见已提交',
|
|
63
65
|
'plan-reject': '❌ 计划已放弃',
|
|
64
|
-
// plan-view-file 不消费挂起项,仅 toast
|
|
66
|
+
// plan-view-file 不消费挂起项,仅 toast「正在重新发送计划内容卡片…」——文案不在此出现,留口供编译期完整性
|
|
65
67
|
'plan-view-file': '',
|
|
66
68
|
};
|
|
67
69
|
/**
|
|
@@ -133,6 +135,23 @@ deps, opts = {}) {
|
|
|
133
135
|
// plan 确认挂起项:requestId → 等待中的计划决策(plan-approve/revise/reject)。
|
|
134
136
|
// 嵌入主卡片后回调侧无 cardId(主卡 messageId 由 ProgressCard 内部管理),改带 channelKey 定位活跃进度卡
|
|
135
137
|
const planPending = new Map();
|
|
138
|
+
// 计划正文两段式多卡直发(与下方「结果回传两段式」完全同构):~3000 字/块,非尾卡走文本
|
|
139
|
+
// 消息、尾卡走卡片,连发间隔 300ms 防飞书限流。planAsk 与 plan-view-file 回调(存量卡片
|
|
140
|
+
// 残留按钮,重发即「再看一遍」)共用
|
|
141
|
+
async function sendPlanContentCards(chatId, req) {
|
|
142
|
+
const chunks = chunkText(req.plan);
|
|
143
|
+
const total = chunks.length;
|
|
144
|
+
for (let i = 0; i < total; i++) {
|
|
145
|
+
if (i > 0)
|
|
146
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
147
|
+
if (i === total - 1) {
|
|
148
|
+
await deps.gateway.sendCardTo(chatId, buildPlanTailCard(req, chunks[i], { index: total, total }, config.card?.width ?? 'default'));
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
await deps.gateway.sendTextTo(chatId, `${planChunkHeader(req, i + 1, total)}\n\n${chunks[i]}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
136
155
|
// 提问挂起项:requestId → 等待中的问题答案(qa-pick 逐项收集,qa-submit 一次性 resolve)。
|
|
137
156
|
// 嵌入主卡片后无 cardId,改带 channelKey 定位活跃进度卡
|
|
138
157
|
const qaPending = new Map();
|
|
@@ -147,6 +166,10 @@ deps, opts = {}) {
|
|
|
147
166
|
// 当前存活 query 的 MCP 状态拉取句柄(executor onQuery 注入、任务 finally 删除):
|
|
148
167
|
// /mcp 命令在任务运行中经此实时拉取;句柄随 query 结束失效,调用侧须 catch
|
|
149
168
|
const activeQueries = new Map();
|
|
169
|
+
// 通道级 notify sender 槽(生命周期方案 A2):常驻进程复用 spawn 时的 lcb-notify server
|
|
170
|
+
// 实例,发送目标(chatId 绑定 + sentPaths)经槽每任务换绑——同通道 FIFO 串行保证换绑
|
|
171
|
+
// 总在上一轮发送完成后发生
|
|
172
|
+
const notifySlots = new Map();
|
|
150
173
|
// 触发词两段式(ask_detail)等待补充的挂起项:channelKey → 命中规则与原始消息。
|
|
151
174
|
// 命中「需补充」规则时先追问不入队;同用户下一条非 / 消息视作补充内容,与 rewrite 合并后入队。
|
|
152
175
|
// 超时(SUPPLEMENT_WAIT_MS)自动失效走原流程;任意本地命令(cmd.handled)即取消等待——
|
|
@@ -440,11 +463,14 @@ deps, opts = {}) {
|
|
|
440
463
|
await deps.gateway.sendTextTo(msg.chatId, cmd.reply);
|
|
441
464
|
}
|
|
442
465
|
// 指针变更代际 bump:/new、/resume <n>、/ws use 都改写了 currentSessionId——
|
|
443
|
-
// 运行中/收尾窗口内任务的迟到归档据 pointerGen 放弃回写指针(archive
|
|
466
|
+
// 运行中/收尾窗口内任务的迟到归档据 pointerGen 放弃回写指针(archive 闭包比对)。
|
|
467
|
+
// 同时作废该通道的常驻进程:换了续接目标,旧进程的会话上下文不再适用(忙进程不动,
|
|
468
|
+
// 运行中任务的中止由 /new 的 abort 负责,abort 会连带杀进程)
|
|
444
469
|
if (cmd.pointerTouched) {
|
|
445
470
|
const rt = runtimes.get(key);
|
|
446
471
|
if (rt)
|
|
447
472
|
rt.pointerGen = (rt.pointerGen ?? 0) + 1;
|
|
473
|
+
killResidentSession(`${app.appId}:${key}`);
|
|
448
474
|
}
|
|
449
475
|
// /new 开启新会话:
|
|
450
476
|
// 1) 清空旧代际排队消息(epoch 自增,旧任务入 executeTask 即 no-op)
|
|
@@ -542,6 +568,9 @@ deps, opts = {}) {
|
|
|
542
568
|
const securityGuard = app.role === 'deputy'
|
|
543
569
|
? {
|
|
544
570
|
cwd: workspacePath(wsName),
|
|
571
|
+
// 入站附件豁免根:与 gateway 下载目录共用单源常量(config.ts INBOX_DIR)——用户发给 bot 的
|
|
572
|
+
// 图片/文件必须可读可处理,属产品功能正确性;R1/Bash 探测豁免,R2 写边界不豁免
|
|
573
|
+
inboxRoot: INBOX_DIR,
|
|
545
574
|
redzone: () => config.security?.redzone?.read ?? DEFAULT_REDZONE_READ_SOURCES,
|
|
546
575
|
// 未配置工作区白名单 = 权限轴全给:写根 = 全部已配置工作区(R2 红线仍在:工作区外只读)
|
|
547
576
|
writeRoots: () => (app.allowedWorkspaces?.length ? app.allowedWorkspaces : config.workspaces.map((w) => w.name))
|
|
@@ -603,19 +632,24 @@ deps, opts = {}) {
|
|
|
603
632
|
});
|
|
604
633
|
});
|
|
605
634
|
},
|
|
606
|
-
// plan 确认(#3+#6
|
|
607
|
-
//
|
|
608
|
-
//
|
|
609
|
-
//
|
|
610
|
-
// 仍在主卡片的内嵌 form 内。超时由本闭包自管:到点 clearPlan + resolve reject
|
|
635
|
+
// plan 确认(#3+#6):计划正文与完整回复同款两段式多卡直发(sendPlanContentCards),
|
|
636
|
+
// 不搞「点按钮收文件」特例;原文另落盘 claude/plans/ 归档(slug + 时间戳,方便 review
|
|
637
|
+
// 与 SOP 要求的本地归档,不再有回调消费)。批准/放弃/按意见修改三按钮与意见输入框仍在
|
|
638
|
+
// 主卡片的内嵌 form 内。超时由本闭包自管:到点 clearPlan + resolve reject
|
|
611
639
|
planAsk: async (req) => {
|
|
612
640
|
const planReq = { requestId: randomUUID(), plan: req.plan, workspaceName: req.workspaceName };
|
|
613
|
-
//
|
|
614
|
-
const planFilePath = writeRuntimeMarkdown(join('claude', 'plans'), req.workspaceName || 'plan', `# 执行计划 · ${req.workspaceName} · ${new Date().toLocaleString('zh-CN', { hour12: false })}\n\n${req.plan}\n`, `${tag}[
|
|
641
|
+
// 落盘归档:文件名加时间戳避免并发冲突;失败仅影响归档,不影响直发与审批
|
|
642
|
+
const planFilePath = writeRuntimeMarkdown(join('claude', 'plans'), req.workspaceName || 'plan', `# 执行计划 · ${req.workspaceName} · ${new Date().toLocaleString('zh-CN', { hour12: false })}\n\n${req.plan}\n`, `${tag}[计划归档落盘失败](不影响流程):`);
|
|
643
|
+
if (planFilePath)
|
|
644
|
+
console.log(tag, `[计划归档] ${planFilePath}`);
|
|
615
645
|
const progress = activeProgress.get(key);
|
|
616
646
|
if (!progress)
|
|
617
647
|
return { action: 'reject' }; // 进度卡不在 = 任务已结束/被 abort,按放弃处理
|
|
618
|
-
progress.setPlan({ requestId: planReq.requestId, plan: req.plan, workspaceName: req.workspaceName
|
|
648
|
+
progress.setPlan({ requestId: planReq.requestId, plan: req.plan, workspaceName: req.workspaceName });
|
|
649
|
+
// 正文直发放挂起开始前:raceFallback 兜底 WS 偶发挂起(此刻超时定时器尚未 arm,裸 await
|
|
650
|
+
// 挂起即永不超时——同 ask 发卡约束);raceFallback 永不 reject,失败/超时仅内部落日志,
|
|
651
|
+
// 审批表单照常可用
|
|
652
|
+
await raceFallback(sendPlanContentCards(msg.chatId, planReq), CARD_SEND_TIMEOUT_MS, undefined);
|
|
619
653
|
// 不再 sinkToBottom:plan 区嵌入主进度卡,flush 自然 PATCH,删旧卡重发只会让用户看到计划区跳一下
|
|
620
654
|
return new Promise((resolve) => {
|
|
621
655
|
const gc = setTimeout(() => {
|
|
@@ -634,7 +668,6 @@ deps, opts = {}) {
|
|
|
634
668
|
ownerId: msg.userId,
|
|
635
669
|
channelKey: key,
|
|
636
670
|
chatId: msg.chatId,
|
|
637
|
-
planFilePath,
|
|
638
671
|
});
|
|
639
672
|
});
|
|
640
673
|
},
|
|
@@ -690,7 +723,14 @@ deps, opts = {}) {
|
|
|
690
723
|
const toolHintEnabled = config.session?.toolHint !== false;
|
|
691
724
|
const sentPaths = new Set();
|
|
692
725
|
const sink = async () => { await activeProgress.get(key)?.sinkToBottom(); };
|
|
693
|
-
|
|
726
|
+
// 槽式换绑(A2):本任务的 sender(chatId 硬绑定 + sentPaths 去重集)挂进通道级槽——
|
|
727
|
+
// 常驻进程复用 spawn 时的 lcb-notify server 实例,经 delegate 路由到槽内当前 sender
|
|
728
|
+
let notifySlot = notifySlots.get(key);
|
|
729
|
+
if (!notifySlot) {
|
|
730
|
+
notifySlot = createNotifySenderSlot();
|
|
731
|
+
notifySlots.set(key, notifySlot);
|
|
732
|
+
}
|
|
733
|
+
notifySlot.setSender(createGatewaySender({
|
|
694
734
|
chatId: msg.chatId,
|
|
695
735
|
sentPaths,
|
|
696
736
|
sendText: async (c, md) => {
|
|
@@ -709,7 +749,7 @@ deps, opts = {}) {
|
|
|
709
749
|
await deps.gateway.uploadAndSendFile(c, p);
|
|
710
750
|
await sink();
|
|
711
751
|
},
|
|
712
|
-
});
|
|
752
|
+
}));
|
|
713
753
|
activeProgress.set(key, progress);
|
|
714
754
|
// 指针代际快照:任务运行期间 /new、/resume <n>、/ws use 都会 bump pointerGen,本任务
|
|
715
755
|
// 收尾归档据此放弃回写指针(只入历史不覆盖用户选择)。定义在 try 外:catch 路径同样调用
|
|
@@ -724,10 +764,40 @@ deps, opts = {}) {
|
|
|
724
764
|
try {
|
|
725
765
|
await progress.start();
|
|
726
766
|
const state = deps.store.getChannelState(key);
|
|
767
|
+
// A3 群聊会话自动滚动(双阈值,先到先滚,业务无感):空闲超阈值(话题切换天然伴随
|
|
768
|
+
// 时间间隔)或轮次满阈值(兜底单话题超长)→ 指针置 null——即 /new 的现成一行语义,
|
|
769
|
+
// 本条消息就是新话题的首条。私聊不滚(手动 /new 管理最优)。滚动同样 bump epoch
|
|
770
|
+
// 对齐 /new 语义:旧代际排队消息按「新建会话」丢弃(指针写保护已有 pointerGen 兜底)。
|
|
771
|
+
// 有排队消息被丢弃时显式告知(静默吞用户消息不可接受);滚动本身不发独立提示,
|
|
772
|
+
// 仅在本条回复尾部带一次轻注记(见下方成功路径,可配置关闭)
|
|
773
|
+
let rolled = false;
|
|
774
|
+
if (msg.chatType === 'group') {
|
|
775
|
+
const idleHours = config.session?.groupIdleResetHours ?? 4;
|
|
776
|
+
const maxTurns = config.session?.groupMaxTurns ?? 30;
|
|
777
|
+
const lastActiveMs = state?.lastActiveAt ? Date.parse(state.lastActiveAt) : NaN;
|
|
778
|
+
const idleHit = idleHours > 0 && Number.isFinite(lastActiveMs) && Date.now() - lastActiveMs > idleHours * 3_600_000;
|
|
779
|
+
const turnsHit = maxTurns > 0 && (state?.turnCount ?? 0) >= maxTurns;
|
|
780
|
+
if (idleHit || turnsHit) {
|
|
781
|
+
deps.store.setCurrentSession(key, null, state?.workspaceName || wsName); // turnCount 随之清零
|
|
782
|
+
const rtRoll = runtimes.get(key);
|
|
783
|
+
const dropped = rtRoll?.queuedCount ?? 0;
|
|
784
|
+
if (rtRoll) {
|
|
785
|
+
rtRoll.epoch = (rtRoll.epoch ?? 0) + 1; // 本条已过代际检查,继续执行;后续旧代际排队消息入口 no-op
|
|
786
|
+
rtRoll.queuedCount = 0;
|
|
787
|
+
}
|
|
788
|
+
rolled = true;
|
|
789
|
+
console.log(tag, `[滚动] ${idleHit ? `空闲超 ${idleHours}h` : `轮次满 ${maxTurns}`},群聊自动开启新话题:${key}`);
|
|
790
|
+
if (dropped > 0) {
|
|
791
|
+
await deps.gateway.sendTextTo(msg.chatId, `🗑️ 已开启新话题,${dropped} 条排队消息按旧话题取消(/resume 可找回此前讨论)`)
|
|
792
|
+
.catch(() => { });
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
727
796
|
// 续接指针三态:undefined=旧版存量数据 → 回退 sessions[0](旧语义 sessions[0] 即当前);
|
|
728
797
|
// null=/new 或 /ws use 已清指针 → 新开会话;字符串 → resume 该会话。
|
|
729
|
-
// 不能用 ??:null 会被 ?? 当成空值回退 sessions[0],/new
|
|
730
|
-
|
|
798
|
+
// 不能用 ??:null 会被 ?? 当成空值回退 sessions[0],/new 的意图就被吞掉了。
|
|
799
|
+
// 滚动刚把指针置 null:本条直接开新会话(rolled 分支不用再读盘)
|
|
800
|
+
const pointer = rolled ? null : state?.currentSessionId;
|
|
731
801
|
const resumeId = pointer === undefined ? state?.sessions?.[0]?.sessionId : pointer || undefined;
|
|
732
802
|
const now = () => nowBeijingISO();
|
|
733
803
|
// 落盘 · user:任务起点(prompt 全文 + resume 链上一轮 sessionId)
|
|
@@ -737,6 +807,15 @@ deps, opts = {}) {
|
|
|
737
807
|
});
|
|
738
808
|
// 任务级 env 现读构造(热生效):apps[].env / claude.env 改动后下一条消息即用新值
|
|
739
809
|
const taskEnv = buildTaskEnv(config, app, claudeDir);
|
|
810
|
+
// 长期记忆 ns env(§6.4/§6.6):memory.enabled 时每条消息拼装下发——OPENVIKING_USER 按
|
|
811
|
+
// 消息级 userId 隔离(群聊同事话语不混入主人记忆;deputy 的 appId 天然不同即分身永不读
|
|
812
|
+
// 主人记忆)。?? 保留既有值:apps[].env 手配 OPENVIKING_* = 专家覆盖口。关段 = 零注入,
|
|
813
|
+
// 行为与无此段完全一致(可插拔验收);常驻进程经配置指纹回收(env 变 → 指纹变 → 重建)
|
|
814
|
+
if (config.memory?.enabled) {
|
|
815
|
+
for (const [k, v] of Object.entries(memoryEnvFor(config.memory, app.appId, msg.userId, deps.memory?.userKeys()))) {
|
|
816
|
+
taskEnv[k] ??= v;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
740
819
|
// 打点只列键名不列值(env 常含凭证,防日志泄漏);值含 ${ 提示误配——env 值不做变量
|
|
741
820
|
// 展开,${VAR} 会原样传给 CLI(变量展开只在 MCP 配置一侧,展开源正是这里注入的环境)
|
|
742
821
|
const envKeys = [...new Set([...Object.keys(app.env ?? {}), ...Object.keys(config.claude?.env ?? {})])];
|
|
@@ -793,15 +872,28 @@ deps, opts = {}) {
|
|
|
793
872
|
// 机器人档案认证 settings 文件(--settings):仅含认证键,命令行层压制生效目录 env
|
|
794
873
|
...(profileSettingsPath ? { extraArgs: { settings: profileSettingsPath } } : {}),
|
|
795
874
|
// 进程内通知工具(send_text/send_image/send_file):中间产物实时推给当前聊天。
|
|
796
|
-
// server
|
|
875
|
+
// server 实例按任务构造(挂通道槽 delegate,见上方槽式换绑),chatId 在 sender
|
|
876
|
+
// 闭包内硬绑定——模型无法选择接收者。
|
|
797
877
|
// 配置页 MCP(#12)一并注入:bridge 自管 servers.json 每任务现读(热生效),notify 后展开保名
|
|
798
|
-
mcpServers: { ...loadBridgeMcpServers(), [NOTIFY_SERVER_NAME]: createNotifyServer(
|
|
878
|
+
mcpServers: { ...loadBridgeMcpServers(), [NOTIFY_SERVER_NAME]: createNotifyServer(notifySlot.delegate) },
|
|
879
|
+
// A2 常驻进程池:同通道亲和复用 CLI 子进程(冷启动税摊薄为每会话一次)。
|
|
880
|
+
// 指纹 = 本任务现读的全部 spawn 输入(人设/模型/plan/env/MCP/插件/档案 settings/
|
|
881
|
+
// cwd/续接指针),任一变化 → executor 回收重建,保「每任务现读热生效」语义;
|
|
882
|
+
// 空闲回收与池上限策略同样每任务现读(热改配置下一条消息生效)
|
|
883
|
+
affinityKey: `${app.appId}:${key}`,
|
|
884
|
+
resident: config.session?.residentProcess !== false,
|
|
885
|
+
residentIdleMs: (config.session?.residentIdleMinutes ?? 60) * 60_000,
|
|
886
|
+
residentMax: config.session?.residentMaxProcesses ?? (app.concurrency ?? config.concurrency),
|
|
887
|
+
residentScope: app.appId,
|
|
799
888
|
// 插件:config.yaml 显式配置(开发期指源码目录)+ Claude 配置目录已启用的 marketplace
|
|
800
889
|
// 插件自动发现,同名显式优先;SDK 对无效路径静默跳过,实际加载以 init 清单(/plugins 命令)为准。
|
|
801
890
|
// managed 模式合并发现两处目录(自管 + 本机 ~/.claude):按绝对 installPath 传 SDK,
|
|
802
891
|
// 用户本机已装插件无须重装;resolvePluginPaths 按 name 去重,当前生效目录优先。
|
|
803
892
|
// 分身角色按 allowed_plugins 白名单过滤(显式 app.plugins 始终保留——写在分身配置里
|
|
804
|
-
// 的就是要给的);未配 allowed_plugins =
|
|
893
|
+
// 的就是要给的);未配 allowed_plugins = 权限轴「全给」,自动发现插件全部加载。
|
|
894
|
+
// 记忆插件自动注入(§6.9):memory.enabled 时 openviking-memory 以宿主基础设施身份
|
|
895
|
+
// 加入加载集,不受 deputy 白名单过滤——记忆是宿主给每个 bot 的恒定能力(同 notify
|
|
896
|
+
// server),不是主人分配给分身的业务插件。B0 实测教训:不注入则白名单分身静默金鱼记忆。
|
|
805
897
|
...(() => {
|
|
806
898
|
const discovered = claudeDir === DEFAULT_CLAUDE_DIR
|
|
807
899
|
? discoverPlugins(claudeDir)
|
|
@@ -810,6 +902,27 @@ deps, opts = {}) {
|
|
|
810
902
|
if (app.role === 'deputy' && app.allowedPlugins !== undefined) {
|
|
811
903
|
merged = merged.filter((p) => app.plugins?.some((e) => e.name === p.name) || app.allowedPlugins.includes(p.name));
|
|
812
904
|
}
|
|
905
|
+
// 装配校验告警 ①(§6.9 L2):deputy 白名单零命中——配了白名单但与发现集零交集,
|
|
906
|
+
// 「以为在收权,实际连一个插件都没给」几乎必为配置错误(B0 实测:二号 bot 配
|
|
907
|
+
// [library] 而该插件未启用 → 实际加载 0 插件,全程零提示)。在记忆注入前判定,
|
|
908
|
+
// 避免注入的记忆插件掩盖零命中事实
|
|
909
|
+
if (app.role === 'deputy' && app.allowedPlugins?.length && merged.length === 0) {
|
|
910
|
+
console.warn(tag, `[memory] 分身 "${app.name}" 配置了 allowed_plugins(${app.allowedPlugins.join('、')})`
|
|
911
|
+
+ ' 但与已发现插件零命中:自动发现插件实际加载 0 个。请检查插件名拼写,或确认插件已安装且在 '
|
|
912
|
+
+ 'settings.json enabledPlugins 中启用(/plugins 可查实际加载清单;方案 §6.9)');
|
|
913
|
+
}
|
|
914
|
+
// 记忆插件注入(§6.9 L1):幂等合流——已显式配置或已过白名单时同名去重不重复加载;
|
|
915
|
+
// 未发现(未安装/未启用)则注入无从谈起,落告警 ② 提示安装,不静默
|
|
916
|
+
if (config.memory?.enabled && !merged.some((p) => p.name === MEMORY_PLUGIN_NAME)) {
|
|
917
|
+
const found = discovered.find((p) => p.name === MEMORY_PLUGIN_NAME);
|
|
918
|
+
if (found)
|
|
919
|
+
merged.push({ name: found.name, path: found.path, source: 'discovered' });
|
|
920
|
+
else {
|
|
921
|
+
console.warn(tag, `[memory] memory.enabled=true 但插件 ${MEMORY_PLUGIN_NAME} 未被发现(未安装或 enabledPlugins 未启用):`
|
|
922
|
+
+ '记忆召回/沉淀 hooks 不会生效。安装:claude plugin install openviking-memory@openviking'
|
|
923
|
+
+ '(或经配置页插件安装;详见 docs 方案 §6.9)');
|
|
924
|
+
}
|
|
925
|
+
}
|
|
813
926
|
return merged.length ? { plugins: merged.map((p) => ({ path: p.path })) } : {};
|
|
814
927
|
})(),
|
|
815
928
|
// 分身技能白名单:透传 SDK Options.skills(官方上下文过滤器——未列出技能对模型不可见
|
|
@@ -910,6 +1023,15 @@ deps, opts = {}) {
|
|
|
910
1023
|
if (finalRedaction.canary)
|
|
911
1024
|
onSecurityEvent({ kind: 'canary', source: 'final' });
|
|
912
1025
|
const finalText = finalRedaction.text;
|
|
1026
|
+
// A3 滚动轻注记:新话题首条回复尾部带一次(仅此一条,可配置关闭)——不发独立提示
|
|
1027
|
+
// 消息(群聊里机器人自言自语就是噪音),失忆困惑由注记 + /resume 兜底;记忆层上线后
|
|
1028
|
+
// 此注记可弱化/去除。注记只进回传文本,不进 transcript(传输层装饰,非模型产出)
|
|
1029
|
+
const rollNote = rolled && config.session?.groupRollNotify !== false
|
|
1030
|
+
? '🍃 已开启新话题(此前讨论 /resume 可找回)'
|
|
1031
|
+
: '';
|
|
1032
|
+
const replyText = finalText
|
|
1033
|
+
? (rollNote ? `${finalText}\n\n---\n${rollNote}` : finalText)
|
|
1034
|
+
: rollNote;
|
|
913
1035
|
await progress.finish('✅ 完成', finalText.length || undefined);
|
|
914
1036
|
archive(outcome.sessionId);
|
|
915
1037
|
deps.transcript?.result({
|
|
@@ -933,8 +1055,8 @@ deps, opts = {}) {
|
|
|
933
1055
|
// 消息限流)——不论长短都不再进进度卡正文(主卡只留提示行),彻底消除「折叠看不到全文」
|
|
934
1056
|
// 与「卡片/消息两边重复」(旧三段式 ≤400 终态自载的边界问题)。尾卡纯展示,想继续对话/
|
|
935
1057
|
// 提意见直接回复消息即可
|
|
936
|
-
if (
|
|
937
|
-
const chunks = chunkText(
|
|
1058
|
+
if (replyText) {
|
|
1059
|
+
const chunks = chunkText(replyText);
|
|
938
1060
|
const total = chunks.length;
|
|
939
1061
|
const resultReq = {
|
|
940
1062
|
charCount: finalText.length, workspaceName: wsName,
|
|
@@ -1027,6 +1149,9 @@ deps, opts = {}) {
|
|
|
1027
1149
|
activeQueries.delete(key);
|
|
1028
1150
|
rt.abort = undefined;
|
|
1029
1151
|
rt.busy = false;
|
|
1152
|
+
// A3 滚动计数:任务收尾即一轮活跃(成功/出错/停止都消耗了上下文,均向阈值累积);
|
|
1153
|
+
// 指针与历史归档已在上文完成,markActivity 只记时间戳与轮次
|
|
1154
|
+
deps.store.markActivity(key);
|
|
1030
1155
|
release();
|
|
1031
1156
|
}
|
|
1032
1157
|
}
|
|
@@ -1046,12 +1171,12 @@ deps, opts = {}) {
|
|
|
1046
1171
|
return { toast: { type: 'info', content: '仅任务发起人可操作计划' } };
|
|
1047
1172
|
}
|
|
1048
1173
|
const decision = action.value.decision;
|
|
1049
|
-
//
|
|
1174
|
+
// plan-view-file(存量卡片残留按钮):计划正文已改为卡片直发(planAsk),此处等价重发
|
|
1175
|
+
// 一遍内容卡片组——不消耗挂起项,可反复点击
|
|
1050
1176
|
if (decision === 'plan-view-file') {
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
return { toast: { type: 'info', content: '正在发送方案文件…' } };
|
|
1177
|
+
void sendPlanContentCards(plan.chatId, plan.req)
|
|
1178
|
+
.catch((e) => console.error('[计划内容重发失败]', action.value.requestId, e));
|
|
1179
|
+
return { toast: { type: 'info', content: '正在重新发送计划内容卡片…' } };
|
|
1055
1180
|
}
|
|
1056
1181
|
if (decision !== 'plan-approve' && decision !== 'plan-revise' && decision !== 'plan-reject') {
|
|
1057
1182
|
return { toast: { type: 'info', content: '该计划确认已被处理或已过期' } };
|
|
@@ -1172,10 +1297,12 @@ deps, opts = {}) {
|
|
|
1172
1297
|
return { toast: { type: 'info', content: '该工作区已是当前工作区' } };
|
|
1173
1298
|
}
|
|
1174
1299
|
applyWorkspaceSwitch(deps.store, wsc.channelKey, target);
|
|
1175
|
-
// 等价 cmd.pointerTouched
|
|
1300
|
+
// 等价 cmd.pointerTouched:切换清了续接指针,运行中/收尾窗口内任务的迟到归档不得回写;
|
|
1301
|
+
// 常驻进程同步作废(cwd 已随工作区变化,旧进程上下文不再适用)
|
|
1176
1302
|
const wsRt = runtimes.get(wsc.channelKey);
|
|
1177
1303
|
if (wsRt)
|
|
1178
1304
|
wsRt.pointerGen = (wsRt.pointerGen ?? 0) + 1;
|
|
1305
|
+
killResidentSession(`${app.appId}:${wsc.channelKey}`);
|
|
1179
1306
|
// 同通道其余工作区卡置 stale:旧卡显示的当前项已过时,继续可点会诱导误切回旧工作区
|
|
1180
1307
|
for (const [id, e] of wsPending) {
|
|
1181
1308
|
if (e.channelKey === wsc.channelKey && id !== action.value.requestId)
|
|
@@ -1246,7 +1373,7 @@ deps, opts = {}) {
|
|
|
1246
1373
|
* 读失败(文件被写坏的中间态等)沿用旧值不崩,并返回用户可读的错误消息(成功返回 null)——
|
|
1247
1374
|
* 调用方(processMessage)据此在聊天里显式提示「本次沿用旧配置」,不再静默。
|
|
1248
1375
|
*/
|
|
1249
|
-
export function createConfigReloader(config, configPath) {
|
|
1376
|
+
export function createConfigReloader(config, configPath, opts = {}) {
|
|
1250
1377
|
return () => {
|
|
1251
1378
|
let fresh;
|
|
1252
1379
|
try {
|
|
@@ -1291,6 +1418,13 @@ export function createConfigReloader(config, configPath) {
|
|
|
1291
1418
|
// 卡片宽度热应用:gateway 的 cardWidth getter 与各处发卡传参都现读 config.card,
|
|
1292
1419
|
// 原地替换后下一条消息的所有卡片即用新宽度(0.x 遗漏导致改宽度须重启的修复)
|
|
1293
1420
|
config.card = fresh.card;
|
|
1421
|
+
// 长期记忆段热应用(§6.6):OPENVIKING_* env 下发与记忆插件注入均每任务现读 config.memory,
|
|
1422
|
+
// 原地替换后下一条消息即生效;常驻进程经配置指纹回收(env/plugins 变化 → 指纹变 → 重建)。
|
|
1423
|
+
// 段变化时通知调用方(startBridge 据此后台重签 user key——account/root key 改动会使旧 key 失配)
|
|
1424
|
+
const memoryChanged = JSON.stringify(fresh.memory ?? null) !== JSON.stringify(config.memory ?? null);
|
|
1425
|
+
config.memory = fresh.memory;
|
|
1426
|
+
if (memoryChanged)
|
|
1427
|
+
opts.onMemoryChanged?.(fresh.memory);
|
|
1294
1428
|
// managed 模式 claude.env 热生效:inherit 由每任务 buildTaskEnv 并入子进程 env 即可;
|
|
1295
1429
|
// managed 走托管 settings.json 通道(CLI 侧 settings.json env 优先于进程 env),
|
|
1296
1430
|
// 此处即时重写盘保证两通道同值——不写盘则手改 env 会被盘上旧值覆盖而静默失效
|
|
@@ -1338,8 +1472,62 @@ export async function startBridge(configPath = CONFIG_PATH) {
|
|
|
1338
1472
|
if (removed > 0)
|
|
1339
1473
|
console.log(`[transcripts] 已清理 ${removed} 个过期落盘文件`);
|
|
1340
1474
|
}
|
|
1475
|
+
// 长期记忆(§6.6 B1):启动时 Admin API 签发 per-bot user key——api_key 模式才实际签发,
|
|
1476
|
+
// dev 模式服务端禁用 Admin API(身份经 OPENVIKING_USER → X-OpenViking-User header 隔离,
|
|
1477
|
+
// 跳过即正解);memory 段热重载变化时后台重签(account/root key 改动会使旧 key 失配)。
|
|
1478
|
+
// key 只进内存缓存与子进程 env,永不落日志/配置页
|
|
1479
|
+
const memoryUserKeys = new Map();
|
|
1480
|
+
const refreshMemoryKeys = async () => {
|
|
1481
|
+
if (!config.memory?.enabled) {
|
|
1482
|
+
memoryUserKeys.clear();
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
const outcomes = await ensureMemoryUserKeys(config.memory, config.apps.map((a) => a.appId));
|
|
1486
|
+
memoryUserKeys.clear();
|
|
1487
|
+
for (const o of outcomes.values()) {
|
|
1488
|
+
if (o.userKey) {
|
|
1489
|
+
memoryUserKeys.set(o.appId, o.userKey);
|
|
1490
|
+
console.log(`[memory] bot ${o.appId}:user key 已签发(api_key 模式)`);
|
|
1491
|
+
}
|
|
1492
|
+
else if (o.error)
|
|
1493
|
+
console.warn(`[memory] bot ${o.appId}:user key ${o.error}`);
|
|
1494
|
+
else
|
|
1495
|
+
console.log(`[memory] bot ${o.appId}:${o.skipped}`);
|
|
1496
|
+
}
|
|
1497
|
+
};
|
|
1498
|
+
if (config.memory?.enabled) {
|
|
1499
|
+
await refreshMemoryKeys();
|
|
1500
|
+
// 客户端接入配置兜底(§6.7 第 4 步):外部自管域自动 skipped-external;LCB 骨架已存在
|
|
1501
|
+
// kept 零写。失败只 warn——本机直连 claude 的插件启用受影响,LCB 会话有 env 通道不受影响
|
|
1502
|
+
try {
|
|
1503
|
+
const w = ensureClientCliConf(config.memory);
|
|
1504
|
+
if (w === 'written' || w === 'updated')
|
|
1505
|
+
console.log(`[memory] 客户端配置 ovcli.conf 已${w === 'written' ? '写入' : '刷新'}(${w})`);
|
|
1506
|
+
}
|
|
1507
|
+
catch (e) {
|
|
1508
|
+
console.warn('[memory] ovcli.conf 写入失败:', e instanceof Error ? e.message : e);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
const memoryProvider = createMemoryProvider(config, memoryUserKeys);
|
|
1341
1512
|
// reloader 共享:所有 bridge 持有同一 config 对象引用,mutate 一处全员可见
|
|
1342
|
-
const reloadConfig = createConfigReloader(config, configPath
|
|
1513
|
+
const reloadConfig = createConfigReloader(config, configPath, {
|
|
1514
|
+
onMemoryChanged: (memory) => {
|
|
1515
|
+
if (memory?.enabled) {
|
|
1516
|
+
void refreshMemoryKeys();
|
|
1517
|
+
try {
|
|
1518
|
+
ensureClientCliConf(memory);
|
|
1519
|
+
}
|
|
1520
|
+
catch { /* 同启动口径,容错不阻断 */ } // 'updated' 自愈端点漂移
|
|
1521
|
+
}
|
|
1522
|
+
else {
|
|
1523
|
+
memoryUserKeys.clear();
|
|
1524
|
+
try {
|
|
1525
|
+
cleanupClientCliConf();
|
|
1526
|
+
}
|
|
1527
|
+
catch { /* 清理失败不影响停用:只删 LCB 骨架形状,手配/外部文件保留 */ }
|
|
1528
|
+
}
|
|
1529
|
+
},
|
|
1530
|
+
});
|
|
1343
1531
|
// 旧版(≤0.3.x)缺省独立目录的一次性迁移提示:目录还在则其中历史会话不再可 /resume
|
|
1344
1532
|
//(本版起所有机器人统一 ~/.claude,会话无法跨数据目录 resume)。不做自动迁移(双目录合并无安全做法)
|
|
1345
1533
|
for (const app of config.apps) {
|
|
@@ -1388,6 +1576,7 @@ export async function startBridge(configPath = CONFIG_PATH) {
|
|
|
1388
1576
|
executor: runTask,
|
|
1389
1577
|
transcript: new TranscriptWriter(join(CONFIG_DIR, 'transcripts', app.appId)),
|
|
1390
1578
|
audit: securityAudit,
|
|
1579
|
+
memory: memoryProvider,
|
|
1391
1580
|
};
|
|
1392
1581
|
return { app, gateway, bridge: createBridge(config, app, deps, { reloadConfig }) };
|
|
1393
1582
|
});
|
|
@@ -1413,6 +1602,10 @@ export async function startBridge(configPath = CONFIG_PATH) {
|
|
|
1413
1602
|
webHandle?.close();
|
|
1414
1603
|
for (const r of runners)
|
|
1415
1604
|
r.gateway.close();
|
|
1605
|
+
// 常驻 CLI 子进程一并回收(优雅退出,不赖 stdio 断连兜底)
|
|
1606
|
+
killAllResidentSessions();
|
|
1607
|
+
// B3 受管 OpenViking 服务一并关停(embedded 模式随 LCB 生命周期;独立手动服务不受影响)
|
|
1608
|
+
stopManagedServer();
|
|
1416
1609
|
clearPidFile();
|
|
1417
1610
|
process.exit(0);
|
|
1418
1611
|
};
|