feique 1.3.0 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bridge/service.d.ts +21 -1
- package/dist/bridge/service.js +156 -0
- package/dist/bridge/service.js.map +1 -1
- package/dist/cli.js +3 -0
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/bridge/service.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ interface RuntimeControl {
|
|
|
17
17
|
restart?: () => Promise<void>;
|
|
18
18
|
}
|
|
19
19
|
export declare class FeiqueService {
|
|
20
|
-
private
|
|
20
|
+
private config;
|
|
21
21
|
private readonly feishuClient;
|
|
22
22
|
private readonly sessionStore;
|
|
23
23
|
private readonly auditLog;
|
|
@@ -39,11 +39,31 @@ export declare class FeiqueService {
|
|
|
39
39
|
private readonly chatRateWindows;
|
|
40
40
|
private maintenanceTimer?;
|
|
41
41
|
private digestTimer?;
|
|
42
|
+
private configWatcher?;
|
|
42
43
|
private readonly intentClassifier?;
|
|
43
44
|
/** Tracks the current incoming message for @mention in replies. */
|
|
44
45
|
private currentMessageContext?;
|
|
45
46
|
constructor(config: BridgeConfig, feishuClient: FeishuClient, sessionStore: SessionStore, auditLog: AuditLog, logger: Logger, metrics?: MetricsRegistry | undefined, idempotencyStore?: IdempotencyStore, runStateStore?: RunStateStore, memoryStore?: MemoryStore, codexSessionIndex?: CodexSessionIndex, runtimeControl?: RuntimeControl | undefined, adminAuditLog?: AuditLog, configHistoryStore?: ConfigHistoryStore, handoffStore?: HandoffStore, trustStore?: TrustStore);
|
|
46
47
|
recoverRuntimeState(): Promise<RunState[]>;
|
|
48
|
+
/**
|
|
49
|
+
* Reload config from disk with validation, diff, and admin notification.
|
|
50
|
+
*
|
|
51
|
+
* Flow:
|
|
52
|
+
* 1. Parse new config — if invalid, reject and notify admin with error
|
|
53
|
+
* 2. Diff against current config — identify what changed
|
|
54
|
+
* 3. Apply new config to memory
|
|
55
|
+
* 4. Notify admin chat(s) with change summary
|
|
56
|
+
*/
|
|
57
|
+
reloadConfig(configPath: string): Promise<{
|
|
58
|
+
ok: boolean;
|
|
59
|
+
error?: string;
|
|
60
|
+
changes?: string[];
|
|
61
|
+
}>;
|
|
62
|
+
/**
|
|
63
|
+
* Watch config file for changes and auto-reload with validation.
|
|
64
|
+
*/
|
|
65
|
+
startConfigWatcher(configPath: string): void;
|
|
66
|
+
stopConfigWatcher(): void;
|
|
47
67
|
startMaintenanceLoop(): void;
|
|
48
68
|
stopMaintenanceLoop(): void;
|
|
49
69
|
startDigestLoop(): void;
|
package/dist/bridge/service.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
+
import { watch as watchFile } from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { randomUUID } from 'node:crypto';
|
|
4
5
|
import { buildHelpText, buildFullHelpText, isReadOnlyCommand, normalizeIncomingText, parseBridgeCommand, } from './commands.js';
|
|
@@ -65,6 +66,7 @@ export class FeiqueService {
|
|
|
65
66
|
chatRateWindows = new Map();
|
|
66
67
|
maintenanceTimer;
|
|
67
68
|
digestTimer;
|
|
69
|
+
configWatcher;
|
|
68
70
|
intentClassifier;
|
|
69
71
|
/** Tracks the current incoming message for @mention in replies. */
|
|
70
72
|
currentMessageContext;
|
|
@@ -113,6 +115,94 @@ export class FeiqueService {
|
|
|
113
115
|
}
|
|
114
116
|
return recovered;
|
|
115
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Reload config from disk with validation, diff, and admin notification.
|
|
120
|
+
*
|
|
121
|
+
* Flow:
|
|
122
|
+
* 1. Parse new config — if invalid, reject and notify admin with error
|
|
123
|
+
* 2. Diff against current config — identify what changed
|
|
124
|
+
* 3. Apply new config to memory
|
|
125
|
+
* 4. Notify admin chat(s) with change summary
|
|
126
|
+
*/
|
|
127
|
+
async reloadConfig(configPath) {
|
|
128
|
+
let newConfig;
|
|
129
|
+
try {
|
|
130
|
+
const { config } = await loadBridgeConfigFile(configPath);
|
|
131
|
+
newConfig = config;
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
135
|
+
this.logger.error({ configPath, error: msg }, 'Config reload rejected — invalid config');
|
|
136
|
+
// Notify admin about the broken config
|
|
137
|
+
const alertText = `🔴 配置变更被拒绝\n\n文件: ${configPath}\n原因: ${msg}\n\n当前服务继续使用旧配置运行。请修正后重新保存。`;
|
|
138
|
+
for (const chatId of this.config.security.admin_chat_ids) {
|
|
139
|
+
try {
|
|
140
|
+
await this.feishuClient.sendText(chatId, alertText);
|
|
141
|
+
}
|
|
142
|
+
catch { /* best-effort */ }
|
|
143
|
+
}
|
|
144
|
+
await this.auditLog.append({
|
|
145
|
+
type: 'config.reload.rejected',
|
|
146
|
+
config_path: configPath,
|
|
147
|
+
error: msg,
|
|
148
|
+
});
|
|
149
|
+
return { ok: false, error: msg };
|
|
150
|
+
}
|
|
151
|
+
// Diff: what changed?
|
|
152
|
+
const changes = diffConfigs(this.config, newConfig);
|
|
153
|
+
if (changes.length === 0) {
|
|
154
|
+
this.logger.debug({ configPath }, 'Config file changed but no effective differences');
|
|
155
|
+
return { ok: true, changes: [] };
|
|
156
|
+
}
|
|
157
|
+
// Apply
|
|
158
|
+
const oldConfig = this.config;
|
|
159
|
+
this.config = newConfig;
|
|
160
|
+
this.logger.info({ configPath, changeCount: changes.length }, 'Config reloaded');
|
|
161
|
+
// Notify admin
|
|
162
|
+
const changeList = changes.slice(0, 15).map((c) => ` • ${c}`).join('\n');
|
|
163
|
+
const truncated = changes.length > 15 ? `\n …及其他 ${changes.length - 15} 项变更` : '';
|
|
164
|
+
const notifyText = `✅ 配置已热加载\n\n${changes.length} 项变更:\n${changeList}${truncated}`;
|
|
165
|
+
for (const chatId of (oldConfig.security.admin_chat_ids.length > 0 ? oldConfig.security.admin_chat_ids : newConfig.security.admin_chat_ids)) {
|
|
166
|
+
try {
|
|
167
|
+
await this.feishuClient.sendText(chatId, notifyText);
|
|
168
|
+
}
|
|
169
|
+
catch { /* best-effort */ }
|
|
170
|
+
}
|
|
171
|
+
await this.auditLog.append({
|
|
172
|
+
type: 'config.reload.applied',
|
|
173
|
+
config_path: configPath,
|
|
174
|
+
change_count: changes.length,
|
|
175
|
+
changes: changes.slice(0, 20),
|
|
176
|
+
});
|
|
177
|
+
return { ok: true, changes };
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Watch config file for changes and auto-reload with validation.
|
|
181
|
+
*/
|
|
182
|
+
startConfigWatcher(configPath) {
|
|
183
|
+
if (this.configWatcher)
|
|
184
|
+
return;
|
|
185
|
+
try {
|
|
186
|
+
let debounce;
|
|
187
|
+
this.configWatcher = watchFile(configPath, () => {
|
|
188
|
+
if (debounce)
|
|
189
|
+
clearTimeout(debounce);
|
|
190
|
+
debounce = setTimeout(async () => {
|
|
191
|
+
await this.reloadConfig(configPath);
|
|
192
|
+
}, 500);
|
|
193
|
+
debounce.unref?.();
|
|
194
|
+
});
|
|
195
|
+
this.configWatcher.unref?.();
|
|
196
|
+
this.logger.info({ configPath }, 'Config file watcher started');
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
this.logger.warn({ error, configPath }, 'Failed to start config file watcher');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
stopConfigWatcher() {
|
|
203
|
+
this.configWatcher?.close();
|
|
204
|
+
this.configWatcher = undefined;
|
|
205
|
+
}
|
|
116
206
|
startMaintenanceLoop() {
|
|
117
207
|
if (this.maintenanceTimer) {
|
|
118
208
|
return;
|
|
@@ -141,6 +231,7 @@ export class FeiqueService {
|
|
|
141
231
|
clearInterval(this.digestTimer);
|
|
142
232
|
this.digestTimer = undefined;
|
|
143
233
|
}
|
|
234
|
+
this.stopConfigWatcher();
|
|
144
235
|
}
|
|
145
236
|
startDigestLoop() {
|
|
146
237
|
if (this.digestTimer || !this.config.service.team_digest_enabled) {
|
|
@@ -3837,6 +3928,71 @@ function extractFileMarkers(text) {
|
|
|
3837
3928
|
const cleanText = text.replace(FILE_MARKER_RE, '').replace(/\n{3,}/g, '\n\n').trim();
|
|
3838
3929
|
return { cleanText, filePaths };
|
|
3839
3930
|
}
|
|
3931
|
+
/**
|
|
3932
|
+
* Shallow diff two BridgeConfig objects, returning human-readable change descriptions.
|
|
3933
|
+
*/
|
|
3934
|
+
function diffConfigs(oldConfig, newConfig) {
|
|
3935
|
+
const changes = [];
|
|
3936
|
+
// Projects added/removed
|
|
3937
|
+
const oldProjects = new Set(Object.keys(oldConfig.projects));
|
|
3938
|
+
const newProjects = new Set(Object.keys(newConfig.projects));
|
|
3939
|
+
for (const p of newProjects) {
|
|
3940
|
+
if (!oldProjects.has(p))
|
|
3941
|
+
changes.push(`项目新增: ${p}`);
|
|
3942
|
+
}
|
|
3943
|
+
for (const p of oldProjects) {
|
|
3944
|
+
if (!newProjects.has(p))
|
|
3945
|
+
changes.push(`项目移除: ${p}`);
|
|
3946
|
+
}
|
|
3947
|
+
// Project-level changes
|
|
3948
|
+
for (const alias of newProjects) {
|
|
3949
|
+
if (!oldProjects.has(alias))
|
|
3950
|
+
continue;
|
|
3951
|
+
const oldP = oldConfig.projects[alias];
|
|
3952
|
+
const newP = newConfig.projects[alias];
|
|
3953
|
+
if (!oldP || !newP)
|
|
3954
|
+
continue;
|
|
3955
|
+
const fields = ['root', 'backend', 'persona', 'codex_model', 'claude_model', 'mention_required', 'description', 'session_scope'];
|
|
3956
|
+
for (const f of fields) {
|
|
3957
|
+
const ov = String(oldP[f] ?? '');
|
|
3958
|
+
const nv = String(newP[f] ?? '');
|
|
3959
|
+
if (ov !== nv)
|
|
3960
|
+
changes.push(`${alias}.${f}: ${ov || '(空)'} → ${nv || '(空)'}`);
|
|
3961
|
+
}
|
|
3962
|
+
// Array fields
|
|
3963
|
+
const arrayFields = ['skills', 'admin_chat_ids', 'operator_chat_ids', 'notification_chat_ids'];
|
|
3964
|
+
for (const f of arrayFields) {
|
|
3965
|
+
const ov = JSON.stringify(oldP[f] ?? []);
|
|
3966
|
+
const nv = JSON.stringify(newP[f] ?? []);
|
|
3967
|
+
if (ov !== nv)
|
|
3968
|
+
changes.push(`${alias}.${f} 变更`);
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
// Service-level changes
|
|
3972
|
+
const serviceFields = ['default_project', 'reply_mode', 'persona', 'team_digest_enabled', 'intent_classifier_enabled'];
|
|
3973
|
+
for (const f of serviceFields) {
|
|
3974
|
+
const ov = String(oldConfig.service[f] ?? '');
|
|
3975
|
+
const nv = String(newConfig.service[f] ?? '');
|
|
3976
|
+
if (ov !== nv)
|
|
3977
|
+
changes.push(`service.${f}: ${ov || '(空)'} → ${nv || '(空)'}`);
|
|
3978
|
+
}
|
|
3979
|
+
// Backend default
|
|
3980
|
+
if (oldConfig.backend?.default !== newConfig.backend?.default) {
|
|
3981
|
+
changes.push(`backend.default: ${oldConfig.backend?.default ?? 'codex'} → ${newConfig.backend?.default ?? 'codex'}`);
|
|
3982
|
+
}
|
|
3983
|
+
// Security admin changes
|
|
3984
|
+
if (JSON.stringify(oldConfig.security.admin_chat_ids) !== JSON.stringify(newConfig.security.admin_chat_ids)) {
|
|
3985
|
+
changes.push('security.admin_chat_ids 变更');
|
|
3986
|
+
}
|
|
3987
|
+
// Embedding provider
|
|
3988
|
+
if (oldConfig.embedding.provider !== newConfig.embedding.provider) {
|
|
3989
|
+
changes.push(`embedding.provider: ${oldConfig.embedding.provider} → ${newConfig.embedding.provider}`);
|
|
3990
|
+
}
|
|
3991
|
+
if (oldConfig.embedding.ollama_model !== newConfig.embedding.ollama_model) {
|
|
3992
|
+
changes.push(`embedding.ollama_model: ${oldConfig.embedding.ollama_model} → ${newConfig.embedding.ollama_model}`);
|
|
3993
|
+
}
|
|
3994
|
+
return changes;
|
|
3995
|
+
}
|
|
3840
3996
|
function truncateExcerpt(text, limit = 160) {
|
|
3841
3997
|
return text.length > limit ? `${text.slice(0, limit)}...` : text;
|
|
3842
3998
|
}
|