@bd7pil/reeve 0.1.3 → 0.1.5
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 +7 -3
- package/default-fallback.json +49 -0
- package/package.json +11 -6
- package/provision.json +29 -23
- package/src/constants.js +1 -1
- package/src/gate/classify.js +77 -0
- package/src/gate/config.js +172 -0
- package/src/gate/gate.js +446 -0
- package/src/gate/lanes.js +194 -0
- package/src/gate/state.js +129 -0
- package/src/index.js +77 -85
- package/src/models.js +11 -0
- package/src/snapshot.js +2 -24
- package/src/stats.js +142 -179
- package/default-chain.json +0 -251
- package/src/chain.js +0 -358
- package/src/proxy.js +0 -587
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// 会话状态机(GATE 里唯一的一份回退状态)。
|
|
2
|
+
//
|
|
3
|
+
// 语义(复刻自 v1 时代那套 fallback 的行为规格):
|
|
4
|
+
// · **单向推进**:候选指针只增不减(attemptCount/fallbackIndex 语义);
|
|
5
|
+
// · **粘性**:切换成功后,同一 session 的后续请求从切换到的那个候选起(不回头);
|
|
6
|
+
// · **冷却**:失败的候选取记时间戳,冷却期(默认 60s)内不再被选;
|
|
7
|
+
// · **可选恢复**:`restore_primary_after_cooldown` 为真且所有失败记录的冷却都过期 → 指针归零;
|
|
8
|
+
// · **并发安全**:同一 session 的两次失败只推进一次(`advance` 带去重键);
|
|
9
|
+
// · **有界内存**:按 TTL 驱逐 + 条数上限。
|
|
10
|
+
//
|
|
11
|
+
// 时间是注入的(测试用假时钟,不 sleep)。
|
|
12
|
+
export function createSessionStore({ now = Date.now, policy = {}, maxSessions = 10_000 } = {}) {
|
|
13
|
+
// policy 允许传函数(配置热更后策略即时生效,状态不必重建)
|
|
14
|
+
const readPolicy = () => (typeof policy === 'function' ? policy() ?? {} : policy)
|
|
15
|
+
const ttlMs = () => Math.max(1, Number(readPolicy().session_ttl_seconds ?? 1800)) * 1000
|
|
16
|
+
const cooldownMs = () => Math.max(0, Number(readPolicy().cooldown_seconds ?? 60)) * 1000
|
|
17
|
+
/** @type {Map<string, {agent: string|null, touchedAt: number, chains: Map<string, {index: number, failed: Map<string, number>, lastNext: {key: string, model: string|null}|null}>}>} */
|
|
18
|
+
const sessions = new Map()
|
|
19
|
+
|
|
20
|
+
const record = (sessionID) => {
|
|
21
|
+
let rec = sessions.get(sessionID)
|
|
22
|
+
if (!rec) {
|
|
23
|
+
rec = { agent: null, touchedAt: now(), chains: new Map() }
|
|
24
|
+
sessions.set(sessionID, rec)
|
|
25
|
+
}
|
|
26
|
+
rec.touchedAt = now()
|
|
27
|
+
return rec
|
|
28
|
+
}
|
|
29
|
+
const chainRec = (sessionID, chainName) => {
|
|
30
|
+
const rec = record(sessionID)
|
|
31
|
+
let c = rec.chains.get(chainName)
|
|
32
|
+
if (!c) {
|
|
33
|
+
c = { index: 0, failed: new Map(), lastNext: null }
|
|
34
|
+
rec.chains.set(chainName, c)
|
|
35
|
+
}
|
|
36
|
+
return c
|
|
37
|
+
}
|
|
38
|
+
const sweep = () => {
|
|
39
|
+
const cutoff = now() - ttlMs()
|
|
40
|
+
for (const [id, rec] of sessions) if (rec.touchedAt < cutoff) sessions.delete(id)
|
|
41
|
+
if (sessions.size <= maxSessions) return
|
|
42
|
+
// 超上限:按 touchedAt 从旧到新驱逐
|
|
43
|
+
const ordered = [...sessions.entries()].sort((a, b) => a[1].touchedAt - b[1].touchedAt)
|
|
44
|
+
for (let i = 0; i < ordered.length - maxSessions; i++) sessions.delete(ordered[i][0])
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const cooling = (rec, key) => {
|
|
48
|
+
const failedAt = rec.failed.get(key)
|
|
49
|
+
if (failedAt === undefined) return false
|
|
50
|
+
if (cooldownMs() === 0) return false
|
|
51
|
+
const alive = now() - failedAt < cooldownMs()
|
|
52
|
+
if (!alive) rec.failed.delete(key) // 过期即清(顺带把恢复判断简化)
|
|
53
|
+
return alive
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
now,
|
|
58
|
+
cooldownMs,
|
|
59
|
+
setAgent(sessionID, agent) {
|
|
60
|
+
if (typeof sessionID !== 'string' || sessionID.length === 0) return
|
|
61
|
+
record(sessionID).agent = typeof agent === 'string' && agent.length > 0 ? agent : null
|
|
62
|
+
sweep()
|
|
63
|
+
},
|
|
64
|
+
agentOf(sessionID) {
|
|
65
|
+
const rec = sessions.get(sessionID)
|
|
66
|
+
if (!rec) return null
|
|
67
|
+
if (now() - rec.touchedAt >= ttlMs()) {
|
|
68
|
+
sessions.delete(sessionID)
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
return rec.agent
|
|
72
|
+
},
|
|
73
|
+
/** 该 session 在这条链上的起点索引(粘性 + 冷却后恢复)。 */
|
|
74
|
+
startIndex(sessionID, chainName, keys) {
|
|
75
|
+
const rec = chainRec(sessionID, chainName)
|
|
76
|
+
if (rec.index > 0 && readPolicy().restore_primary_after_cooldown === true) {
|
|
77
|
+
const anyCooling = keys.some((k) => cooling(rec, k))
|
|
78
|
+
if (!anyCooling) rec.index = 0
|
|
79
|
+
}
|
|
80
|
+
return Math.min(rec.index, Math.max(0, keys.length - 1))
|
|
81
|
+
},
|
|
82
|
+
/** 某个候选此刻是不是在冷却里。 */
|
|
83
|
+
isCooling(sessionID, chainName, key) {
|
|
84
|
+
return cooling(chainRec(sessionID, chainName), key)
|
|
85
|
+
},
|
|
86
|
+
/** 失败:记时间戳(冷却起点)。**不**推进指针(推进由 noteSuccess/advance 决定)。 */
|
|
87
|
+
noteFailure(sessionID, chainName, key) {
|
|
88
|
+
const rec = chainRec(sessionID, chainName)
|
|
89
|
+
rec.failed.set(key, now())
|
|
90
|
+
sweep()
|
|
91
|
+
},
|
|
92
|
+
/** 成功:把指针钉在这个候选上(粘性)。 */
|
|
93
|
+
noteSuccess(sessionID, chainName, key, keys) {
|
|
94
|
+
const rec = chainRec(sessionID, chainName)
|
|
95
|
+
const at = keys.indexOf(key)
|
|
96
|
+
if (at >= 0) rec.index = at
|
|
97
|
+
rec.failed.delete(key)
|
|
98
|
+
sweep()
|
|
99
|
+
},
|
|
100
|
+
/**
|
|
101
|
+
* 插件侧要"下一个候选"时用(流已开始/沉默这两类层内换不了的情形)。
|
|
102
|
+
* `dedupeKey` 保证同一次失败被两条事件各触发一次时**只推进一次**。
|
|
103
|
+
* @returns {{ model: string|null, index: number }} model 为 null = 没有下一个了
|
|
104
|
+
*/
|
|
105
|
+
advance(sessionID, chainName, keys, { dedupeKey = null } = {}) {
|
|
106
|
+
const rec = chainRec(sessionID, chainName)
|
|
107
|
+
if (dedupeKey !== null && rec.lastNext?.key === dedupeKey) return { model: rec.lastNext.model, index: rec.index }
|
|
108
|
+
const from = Math.max(rec.index, 0)
|
|
109
|
+
let nextIndex = -1
|
|
110
|
+
for (let i = from + 1; i < keys.length; i++) {
|
|
111
|
+
if (cooling(rec, keys[i])) continue
|
|
112
|
+
nextIndex = i
|
|
113
|
+
break
|
|
114
|
+
}
|
|
115
|
+
const model = nextIndex >= 0 ? keys[nextIndex] : null
|
|
116
|
+
if (nextIndex >= 0) rec.index = nextIndex
|
|
117
|
+
rec.lastNext = { key: dedupeKey, model }
|
|
118
|
+
sweep()
|
|
119
|
+
return { model, index: rec.index }
|
|
120
|
+
},
|
|
121
|
+
snapshot() {
|
|
122
|
+
return {
|
|
123
|
+
sessions: sessions.size,
|
|
124
|
+
cooling: [...sessions.values()].reduce((n, r) => n + [...r.chains.values()].reduce((m, c) => m + [...c.failed.keys()].filter((k) => cooling(c, k)).length, 0), 0),
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
size: () => sessions.size,
|
|
128
|
+
}
|
|
129
|
+
}
|
package/src/index.js
CHANGED
|
@@ -33,15 +33,16 @@ import { Supervisor } from './supervisor.js'
|
|
|
33
33
|
import { reconcileParent } from './reconcile.js'
|
|
34
34
|
import { SnapshotWriter, buildSnapshot, projectAgentTable } from './snapshot.js'
|
|
35
35
|
import { SUBAGENT_ROSTER, withRoster } from './roster.js'
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
36
|
+
import { probeGate, startGate } from './gate/gate.js'
|
|
37
|
+
import { defaultFallbackConfigPath } from './gate/config.js'
|
|
38
|
+
import { REEVE_TIERS, BUILTINS_TO_HIDE, mergeTierModels, pickDefaultAgent, readEndpoint, resolveGateEndpoint } from './models.js'
|
|
39
|
+
import { STATS_DEFAULTS, aggregateGateRows, aggregateLedger, buildStats, fetchGateModels, mergeModelCandidates, readLedgerTail } from './stats.js'
|
|
39
40
|
import { loadAgents, loadSkills } from './register.js'
|
|
40
41
|
import { ensureProviderBlock, reeveProviderBlock } from './config-write.js'
|
|
41
42
|
|
|
42
43
|
const SUBAGENT_TOOL = 'subagent'
|
|
43
|
-
/**
|
|
44
|
-
const
|
|
44
|
+
/** GATE(唯一代理层)的默认端口(与写进配置的端点一致)。 */
|
|
45
|
+
const GATE_DEFAULT_PORT = 8080
|
|
45
46
|
|
|
46
47
|
export default {
|
|
47
48
|
id: PLUGIN_ID,
|
|
@@ -73,10 +74,8 @@ export default {
|
|
|
73
74
|
// 名册注入的证据:注入成功过一次就记下来(进快照)——"藏起来不削弱委派"这条设计
|
|
74
75
|
// 靠它可核对,而不是靠"模型应该看得见"。
|
|
75
76
|
let roster = null
|
|
76
|
-
/**
|
|
77
|
-
let
|
|
78
|
-
/** 链文件的状态(代理那边在维护,这里只留引用给快照)。必须在快照块之前声明。 */
|
|
79
|
-
let chainRef = null
|
|
77
|
+
/** GATE 的运行态(进快照:面板可显示、上机可核对)。必须在快照块之前声明。 */
|
|
78
|
+
let gateState = null
|
|
80
79
|
/** 性能统计(进快照)。必须在快照块之前声明。 */
|
|
81
80
|
let statsState = null
|
|
82
81
|
/** 模型候选(面板的链编辑选择器用)。必须在快照块之前声明。 */
|
|
@@ -130,12 +129,11 @@ export default {
|
|
|
130
129
|
version: PLUGIN_VERSION,
|
|
131
130
|
agents: agentTable,
|
|
132
131
|
roster,
|
|
133
|
-
proxy: proxyState,
|
|
134
|
-
chain: chainRef,
|
|
135
132
|
models: modelsState,
|
|
136
133
|
registered: { agents: registeredAgents, skills: registeredSkills },
|
|
137
134
|
endpoint: endpointState,
|
|
138
135
|
stats: statsState,
|
|
136
|
+
gate: gateState,
|
|
139
137
|
})
|
|
140
138
|
snapshotFlush = () => snapshot.schedule(build)
|
|
141
139
|
ledger.onChange = () => snapshot.schedule(build)
|
|
@@ -149,7 +147,7 @@ export default {
|
|
|
149
147
|
// (只加不改 / 备份 / 写完自校验 / 保留注释)。
|
|
150
148
|
let endpointWrite = null
|
|
151
149
|
if (options.ownEndpoint !== false) {
|
|
152
|
-
const wantPort =
|
|
150
|
+
const wantPort = resolveGateEndpoint({ gate: options.gate === true ? null : options.gate, proxy: options.proxy === true ? null : options.proxy, fallbackPort: GATE_DEFAULT_PORT }).port
|
|
153
151
|
endpointWrite = await ensureProviderBlock({
|
|
154
152
|
configDir: options.engineConfigDir ?? null,
|
|
155
153
|
block: reeveProviderBlock({ port: wantPort }),
|
|
@@ -159,60 +157,72 @@ export default {
|
|
|
159
157
|
endpointState = { status: endpointWrite.status, path: endpointWrite.path, detail: endpointWrite.detail }
|
|
160
158
|
}
|
|
161
159
|
|
|
162
|
-
// ----
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
160
|
+
// ---- GATE(代理层 —— 唯一入口;opt-out:`gate: false`)----
|
|
161
|
+
// 引擎认的是 `providers.reeve` 那个 baseURL,GATE 就占那个端口;路由/回退/执行/台账都在
|
|
162
|
+
// 它里面(唯一代理层,见 docs/GATE.md)。失败只记日志:引擎照常跑,模型会吵地报错。
|
|
163
|
+
//
|
|
164
|
+
// **复用优先**:引擎是**按工作区**加载插件的 —— 同一个引擎里可能有多个插件实例(每个工作区一个)。
|
|
165
|
+
// 第一个实例把 GATE 拉到端点口上,后面的实例**用健康检查接上**(`external: true`),
|
|
166
|
+
// 而不是去抢同一个口(抢必失败 → 快照里会出现假的"GATE 未就绪",面板跟着误报)。
|
|
167
|
+
// 自己起的才自己关:接上别人的实例在卸载时**不动**那台 GATE。
|
|
168
|
+
let gateHandle = null
|
|
169
|
+
if (options.gate !== false && options.gate?.enabled !== false) {
|
|
170
|
+
const endpoint = await readEndpoint({})
|
|
171
|
+
const { port: gatePort, host: gateHost } = resolveGateEndpoint({
|
|
172
|
+
gate: options.gate === true ? null : options.gate,
|
|
173
|
+
proxy: options.proxy === true ? null : options.proxy,
|
|
174
|
+
endpoint,
|
|
175
|
+
fallbackPort: GATE_DEFAULT_PORT,
|
|
176
|
+
})
|
|
177
|
+
const configPath = options.gate?.configPath ?? options.fallbackConfigPath ?? defaultFallbackConfigPath()
|
|
178
|
+
const probe = await probeGate({ port: gatePort, host: gateHost })
|
|
179
|
+
if (probe.ok) {
|
|
180
|
+
gateState = { ...probe.snapshot, reused: true }
|
|
181
|
+
logger.debug(`[reeve] GATE 已在 :${gatePort} 上跑着(复用;链 ${(probe.snapshot?.chains ?? []).join('/')})`)
|
|
182
|
+
} else {
|
|
183
|
+
try {
|
|
184
|
+
gateHandle = await startGate({
|
|
185
|
+
port: gatePort,
|
|
186
|
+
host: gateHost,
|
|
187
|
+
configPath,
|
|
188
|
+
ledgerPath: options.gate?.ledgerPath ?? undefined,
|
|
189
|
+
logger,
|
|
190
|
+
})
|
|
191
|
+
gateState = gateHandle.snapshot()
|
|
192
|
+
logger.debug(`[reeve] GATE::${gateHandle.port}(链 ${gateState.chains.join('/')})`)
|
|
193
|
+
} catch (err) {
|
|
194
|
+
gateState = { error: String(err).slice(0, 200) }
|
|
195
|
+
logger.debug(`[reeve] GATE 起不来(忽略,引擎照常):${gateState.error}`)
|
|
176
196
|
}
|
|
177
197
|
}
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
// 链的状态对象由代理那边持续更新(热更、收敛复核都会写它)—— 这里只留引用,快照时再取
|
|
182
|
-
if (st.chain) chainRef = st.chain
|
|
183
|
-
snapshotFlush()
|
|
184
|
-
}
|
|
185
|
-
proxyHandle = await startOwnedProxy({ opts, logger, onState })
|
|
186
|
-
onState(proxyHandle)
|
|
187
|
-
logger.debug(`[reeve] 代理层自持:${proxyHandle.ready ? '就绪' : '未就绪'} —— ${proxyHandle.detail ?? '?'}`)
|
|
198
|
+
// 状态一变就刷快照 —— 尤其"起来了"或"起不来(如实报错)"这一刻。
|
|
199
|
+
// 少了这一次,消费方只能等下一次台账变化才看到 gate 段(起不来时会一直显示 null)。
|
|
200
|
+
snapshotFlush()
|
|
188
201
|
}
|
|
189
202
|
|
|
190
203
|
const admission = acquireAdmissionRuntime(ctx.location?.directory, { limits })
|
|
191
204
|
|
|
192
|
-
// ---- ⑦ 性能统计(
|
|
193
|
-
// 为什么在插件里做而不是面板里做:面板跑在 openchamber 的 iframe
|
|
194
|
-
// 只有宿主给的 readFile 与 writeFile
|
|
195
|
-
//
|
|
205
|
+
// ---- ⑦ 性能统计(GATE 台账聚合 + 编排台账聚合 → 快照)----
|
|
206
|
+
// 为什么在插件里做而不是面板里做:面板跑在 openchamber 的 iframe 里,**没有网络/文件能力**,
|
|
207
|
+
// 只有宿主给的 readFile 与 writeFile。台账是引擎进程能读到的本地文件;模型目录是本机回环
|
|
208
|
+
// 的 HTTP(GATE 的 `/v1/models`)—— 只有插件够得着。所以插件取数 + 聚合,面板只读快照。
|
|
196
209
|
const statsOpts = typeof options.stats === 'object' && options.stats !== null ? options.stats : {}
|
|
197
210
|
const statsIntervalMs = Number(statsOpts.intervalMs ?? STATS_DEFAULTS.intervalMs)
|
|
198
211
|
const statsLimit = Number(statsOpts.limit ?? STATS_DEFAULTS.limit)
|
|
199
212
|
const statsWindowMs = Number(statsOpts.windowMs ?? STATS_DEFAULTS.windowMs)
|
|
200
|
-
// 模型目录多久刷一次。**别跟统计同频**:`/v1/models`
|
|
201
|
-
//
|
|
202
|
-
// 用户自己的归因日志灌满探测噪声。模型目录本身变化很慢,半小时足够。
|
|
213
|
+
// 模型目录多久刷一次。**别跟统计同频**:`/v1/models` 会真去上游探测池(9router 那条),
|
|
214
|
+
// 一分钟一次没必要。模型目录本身变化很慢,半小时足够。
|
|
203
215
|
const modelsIntervalMs = Number(statsOpts.modelsIntervalMs ?? STATS_DEFAULTS.modelsIntervalMs)
|
|
204
216
|
let statsTimer = null
|
|
205
|
-
let statsSignature = null
|
|
206
217
|
let statsInFlight = false
|
|
207
218
|
let lastModelsAt = 0
|
|
208
219
|
|
|
209
220
|
/** 链上已经在用的模型(面板选择器的候选之一:发现不全时至少这些能选)。 */
|
|
210
221
|
const chainedRefs = () => {
|
|
211
222
|
const out = []
|
|
212
|
-
for (const
|
|
213
|
-
for (const
|
|
214
|
-
|
|
215
|
-
const s = String(f ?? '')
|
|
223
|
+
for (const targets of Object.values(gateState?.chain_targets ?? {})) {
|
|
224
|
+
for (const ref of targets ?? []) {
|
|
225
|
+
const s = String(ref ?? '')
|
|
216
226
|
if (s && !s.endsWith('/') && s.includes('/')) out.push(s)
|
|
217
227
|
}
|
|
218
228
|
}
|
|
@@ -221,53 +231,35 @@ export default {
|
|
|
221
231
|
const engineModels = () => REEVE_TIERS.map((t) => `reeve/${t.id}`)
|
|
222
232
|
|
|
223
233
|
const refreshStats = async () => {
|
|
224
|
-
if (!
|
|
234
|
+
if (!gateHandle || statsInFlight) return
|
|
225
235
|
statsInFlight = true
|
|
226
236
|
const now = Date.now()
|
|
227
|
-
const
|
|
237
|
+
const nowMs = now
|
|
228
238
|
try {
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
snapshotFlush()
|
|
233
|
-
return
|
|
234
|
-
}
|
|
235
|
-
// 先探一行:`total_count` + 最新 id 没变就说明没有新调用 —— 省掉一次上百行的抓取
|
|
236
|
-
const probe = await fetchBifrostLogs({ port, host, token, limit: 1 })
|
|
237
|
-
if (probe.error) {
|
|
238
|
-
statsState = buildStats({ now, windowMs: statsWindowMs, source: 'unavailable', error: `读 /api/logs 失败:${probe.error}`, checkedAt: now })
|
|
239
|
-
snapshotFlush()
|
|
240
|
-
return
|
|
241
|
-
}
|
|
242
|
-
if (probe.signature && probe.signature === statsSignature && statsState?.bifrost) {
|
|
243
|
-
statsState = { ...statsState, checkedAt: now }
|
|
244
|
-
snapshotFlush()
|
|
245
|
-
return
|
|
246
|
-
}
|
|
247
|
-
const full = await fetchBifrostLogs({ port, host, token, limit: statsLimit })
|
|
248
|
-
statsSignature = full.signature
|
|
249
|
-
const bifrost = full.error ? null : aggregateBifrostRows(full.rows, { now, windowMs: statsWindowMs })
|
|
250
|
-
const ledgerAgg = aggregateLedger(ledger.all(), { now })
|
|
239
|
+
const tail = await readLedgerTail({ path: gateHandle.ledgerPath, limit: statsLimit })
|
|
240
|
+
const gateAgg = tail.error ? null : aggregateGateRows(tail.rows, { now: nowMs, windowMs: statsWindowMs })
|
|
241
|
+
const ledgerAgg = aggregateLedger(ledger.all(), { now: nowMs })
|
|
251
242
|
statsState = buildStats({
|
|
252
|
-
now,
|
|
253
|
-
checkedAt:
|
|
243
|
+
now: nowMs,
|
|
244
|
+
checkedAt: nowMs,
|
|
254
245
|
windowMs: statsWindowMs,
|
|
255
|
-
source:
|
|
256
|
-
error:
|
|
257
|
-
|
|
246
|
+
source: gateAgg ? 'gate.ledger' : 'unavailable',
|
|
247
|
+
error: tail.error ? `读台账失败:${tail.error}` : null,
|
|
248
|
+
gate: gateAgg,
|
|
258
249
|
orchestrate: {
|
|
259
250
|
...ledgerAgg,
|
|
260
251
|
queue: { active: admission.activeCount, waiting: admission.waitingCount },
|
|
261
252
|
},
|
|
262
253
|
})
|
|
254
|
+
gateState = gateHandle.snapshot()
|
|
263
255
|
// 顺手把模型候选也刷一遍(半小时一次;链编辑器的选择器用它)
|
|
264
|
-
if (
|
|
265
|
-
const discovered = await
|
|
266
|
-
lastModelsAt =
|
|
256
|
+
if (nowMs - lastModelsAt >= modelsIntervalMs) {
|
|
257
|
+
const discovered = await fetchGateModels({ port: gateHandle.port, host: gateHandle.host })
|
|
258
|
+
lastModelsAt = nowMs
|
|
267
259
|
const candidates = mergeModelCandidates({ discovered: discovered.models, chained: chainedRefs(), engine: engineModels() })
|
|
268
260
|
modelsState = {
|
|
269
|
-
generatedAt:
|
|
270
|
-
source: discovered.error ? 'chain+engine' : '
|
|
261
|
+
generatedAt: nowMs,
|
|
262
|
+
source: discovered.error ? 'chain+engine' : 'gate',
|
|
271
263
|
error: discovered.error,
|
|
272
264
|
discovered: discovered.models.length,
|
|
273
265
|
candidates,
|
|
@@ -281,11 +273,11 @@ export default {
|
|
|
281
273
|
}
|
|
282
274
|
}
|
|
283
275
|
|
|
284
|
-
if (statsIntervalMs > 0 &&
|
|
276
|
+
if (statsIntervalMs > 0 && gateHandle) {
|
|
285
277
|
void refreshStats()
|
|
286
278
|
statsTimer = setInterval(() => void refreshStats(), statsIntervalMs)
|
|
287
279
|
statsTimer.unref?.()
|
|
288
|
-
logger.debug(`[reeve] 性能统计:每 ${Math.round(statsIntervalMs / 1000)}s
|
|
280
|
+
logger.debug(`[reeve] 性能统计:每 ${Math.round(statsIntervalMs / 1000)}s 刷一次(台账尾 ${statsLimit} 行 / ${Math.round(statsWindowMs / 3600000)}h 窗口)`)
|
|
289
281
|
}
|
|
290
282
|
const supervisor = new Supervisor({
|
|
291
283
|
ledger,
|
|
@@ -571,7 +563,7 @@ export default {
|
|
|
571
563
|
snapshot.cancel()
|
|
572
564
|
void snapshot.flush().catch(() => {})
|
|
573
565
|
}
|
|
574
|
-
if (
|
|
566
|
+
if (gateHandle) void gateHandle.dispose().catch(() => {})
|
|
575
567
|
logger.debug('[reeve] 已卸载')
|
|
576
568
|
}
|
|
577
569
|
},
|
package/src/models.js
CHANGED
|
@@ -112,3 +112,14 @@ export function pickDefaultAgent(editor, id = 'foreman') {
|
|
|
112
112
|
}
|
|
113
113
|
return false
|
|
114
114
|
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* GATE 该在哪个 `host:port` 上服务(纯函数,四级回落:显式 gate → 旧 proxy 键 → 配置里的端点 → 默认)。
|
|
118
|
+
* 抽出来是为了**可测**:真实回落链要碰机器上的真实配置(和真实的 18080),不该在单测里跑。
|
|
119
|
+
*/
|
|
120
|
+
export function resolveGateEndpoint({ gate = null, proxy = null, endpoint = null, fallbackPort = 8080, fallbackHost = '127.0.0.1' } = {}) {
|
|
121
|
+
return {
|
|
122
|
+
port: Number(gate?.port ?? proxy?.port ?? endpoint?.port ?? fallbackPort),
|
|
123
|
+
host: gate?.host ?? endpoint?.host ?? fallbackHost,
|
|
124
|
+
}
|
|
125
|
+
}
|
package/src/snapshot.js
CHANGED
|
@@ -92,7 +92,7 @@ export class SnapshotWriter {
|
|
|
92
92
|
*/
|
|
93
93
|
export function buildSnapshot(
|
|
94
94
|
records,
|
|
95
|
-
{ now = Date.now(), version = null, agents = null, roster = null,
|
|
95
|
+
{ now = Date.now(), version = null, agents = null, roster = null, registered = null, endpoint = null, models = null, stats = null, gate = null } = {},
|
|
96
96
|
) {
|
|
97
97
|
const tasks = records
|
|
98
98
|
.map((r) => ({
|
|
@@ -123,8 +123,7 @@ export function buildSnapshot(
|
|
|
123
123
|
version,
|
|
124
124
|
agentTable: agents,
|
|
125
125
|
roster,
|
|
126
|
-
|
|
127
|
-
chain: chain == null ? null : projectChain(chain),
|
|
126
|
+
gate,
|
|
128
127
|
models,
|
|
129
128
|
registered,
|
|
130
129
|
endpoint,
|
|
@@ -134,27 +133,6 @@ export function buildSnapshot(
|
|
|
134
133
|
}
|
|
135
134
|
}
|
|
136
135
|
|
|
137
|
-
/**
|
|
138
|
-
* 链的状态 → 面板要的那一小块。
|
|
139
|
-
* `tiers` 就是 `~/.reeve/chain.json` 里那份形状(面板编辑的就是它,写回也是它)。
|
|
140
|
-
*/
|
|
141
|
-
export function projectChain(chain) {
|
|
142
|
-
if (!chain) return null
|
|
143
|
-
return {
|
|
144
|
-
tiersPath: chain.tiersPath ?? null,
|
|
145
|
-
source: chain.source ?? null,
|
|
146
|
-
from: chain.from ?? null,
|
|
147
|
-
derived: chain.derived === true,
|
|
148
|
-
tiers: chain.tiers ?? null,
|
|
149
|
-
problems: chain.problems ?? [],
|
|
150
|
-
error: chain.error ?? null,
|
|
151
|
-
changedAt: chain.changedAt ?? null,
|
|
152
|
-
appliedAt: chain.appliedAt ?? null,
|
|
153
|
-
drift: chain.drift ?? null,
|
|
154
|
-
lastError: chain.lastError ?? null,
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
136
|
/**
|
|
159
137
|
* 从 `ctx.agent.list()` 的返回里抽出"谁可见"的最小形状。
|
|
160
138
|
* 读不到就返回 null —— 快照里没有这一节,不假装知道。
|