model-manager:队列排空写 flush 标记,热重载交 TUI 侧接力,补 setup ctx 形态诊断
This commit is contained in:
+55
-8
@@ -16,7 +16,11 @@
|
||||
// 因此采用「外部子进程执行 DB 写」方案:
|
||||
// 1. OC 内部 daemon 每 5s 轮询 outbox 队列文件(普通 JSON 文件,不碰 DB)
|
||||
// 2. 命中操作 → 把 SQLite 写 fork 给外部 Python 子进程执行(独立连接,安全)
|
||||
// 3. 子进程写完 → OC 调 POST /api/location/reload 热重载(/models 立即生效)
|
||||
// 3. 队列排空 → 写 flush 标记 → 由 TUI 侧 tui.js 执行 location.reload 热重载
|
||||
// (2026-10-19 实测定论:V2 server 插件 ctx 没有 client.location / serverUrl,
|
||||
// 进程内 reload 无通道 —— ai-mcp 2026-09-24 也报过同款「没有可用的 reload 通道」;
|
||||
// 而 TUI 插件 ctx 的 client.location 实测可用,见 oc-session-all.log 的
|
||||
// 「location.reload OK」。所以热重载的正主在 tui.js,本插件只负责写 DB + 发标记)
|
||||
//
|
||||
// ── 操作语义 ──────────────────────────────────────────────────────────────
|
||||
// disable(provider, modelID) = 从 catalog body 里 DELETE provider.models[modelID]
|
||||
@@ -50,6 +54,9 @@ const LOG_FILE = join(homedir(), ".config", "opencode", "model-manager.log")
|
||||
const OUTBOX = join(homedir(), ".config", "opencode", "model-manager.outbox.jsonl")
|
||||
// 出厂备份(reset 用:首次操作前自动把 pristine catalog 存一份)
|
||||
const PRISTINE = join(homedir(), ".config", "opencode", "model-manager.pristine.json")
|
||||
// flush 标记:daemon 把队列排空并写完 DB 后落盘 { at, lastOp, serverReload };
|
||||
// tui.js 轮询到 at >= 自己入队时刻 就执行 location.reload(TUI 侧才有 reload 通道)
|
||||
const FLUSH = join(homedir(), ".config", "opencode", "model-manager.last-flush.json")
|
||||
// 数据库路径
|
||||
const DB_PATH = join(homedir(), ".local", "share", "opencode", "opencode.db")
|
||||
// SQLite 写执行器(Python,OC 外部子进程跑,绕 OC 自身 DB 写锁)
|
||||
@@ -119,10 +126,10 @@ async function execDbWrite(op: string, payload: any): Promise<string> {
|
||||
})
|
||||
}
|
||||
|
||||
// 热重载 OC(让 /models 立即反映 catalog 变更)
|
||||
// 通道整段抄自 ai-mcp 的 doReload(MCP 对调已验证此链路可用):
|
||||
// 热重载(进程内尽力通道,2026-10-19 实测:V2 server 插件 ctx 两条通道都没有,
|
||||
// 保留尝试只为将来宿主补上 client/serverUrl 时能自动生效):
|
||||
// 进程内 SDK client.location.reload 三种调用形态 → HTTP POST ctx.serverUrl /api/location/reload
|
||||
// 关键差异:用 ctx.serverUrl(OC 自己告知插件的服务地址),绝不写死端口
|
||||
// 正常路径见 processOutbox 尾部:写 flush 标记 → tui.js 用 TUI ctx 的 client.location 热重载
|
||||
// V2 上下文可能有 client 属性:优先 ctx.client(若宿主提供),否则回退 ctx 本身(抄 ai-mcp pickClient)
|
||||
function pickClient(ctx: any) {
|
||||
if (!ctx) return ctx
|
||||
@@ -159,11 +166,21 @@ async function doReload(ctx: any): Promise<boolean> {
|
||||
lastErr = e
|
||||
}
|
||||
}
|
||||
const msg = lastErr ? lastErr.message : "没有可用的 reload 通道(ctx 无 location/serverUrl)"
|
||||
fileLog("hot reload 失败(需重启 OC 生效): " + msg)
|
||||
const msg = lastErr ? lastErr.message : "进程内无 reload 通道(V2 server ctx 无 client.location/serverUrl,已实测)"
|
||||
fileLog("进程内 hot reload 未成功: " + msg + " → 已写 flush 标记,交 TUI 侧 tui.js 热重载(看 oc-session-all.log)")
|
||||
return false
|
||||
}
|
||||
|
||||
// 队列深度(只数行,不改文件)
|
||||
function queueDepth(): number {
|
||||
try {
|
||||
if (!existsSync(OUTBOX)) return 0
|
||||
return readFileSync(OUTBOX, "utf-8").split("\n").filter((l) => l.trim()).length
|
||||
} catch {
|
||||
return -1 // 读失败时按「未知」处理,宁可多等一轮
|
||||
}
|
||||
}
|
||||
|
||||
// 从 outbox 消费一条操作并执行
|
||||
async function processOutbox(ctx: any): Promise<void> {
|
||||
if (!existsSync(OUTBOX)) return
|
||||
@@ -199,8 +216,23 @@ async function processOutbox(ctx: any): Promise<void> {
|
||||
}
|
||||
|
||||
const res = await execDbWrite(op, payload)
|
||||
fileLog(`DB 写结果 op=${op}: ${res}`)
|
||||
await doReload(ctx)
|
||||
const depth = queueDepth()
|
||||
fileLog(`DB 写结果 op=${op}: ${res},队列剩余=${depth}`)
|
||||
|
||||
if (depth !== 0) {
|
||||
// 还有排队的操作(整场停用会连着 9 条):先不 reload,等全部写完一次性热重载
|
||||
fileLog("队列未排空,暂不 reload(等最后一条写完再一次性热重载)")
|
||||
return
|
||||
}
|
||||
// 队列已排空:先试进程内通道(V2 server ctx 实测无通道,失败会写日志),
|
||||
// 再落 flush 标记 —— TUI 侧 tui.js 轮询到标记后用它自己的 client.location 热重载
|
||||
const serverReload = await doReload(ctx)
|
||||
try {
|
||||
writeFileSync(FLUSH, JSON.stringify({ at: Date.now(), lastOp: op, serverReload: !!serverReload }))
|
||||
fileLog("flush 标记已写(TUI 侧将据此触发热重载): serverReload=" + !!serverReload)
|
||||
} catch (e: any) {
|
||||
fileLog("flush 标记写入失败: " + (e && e.message ? e.message : String(e)))
|
||||
}
|
||||
}
|
||||
|
||||
// SQLite 写执行器(Python,子进程独立连接)
|
||||
@@ -317,6 +349,21 @@ export default {
|
||||
async setup(ctx: any): Promise<void> {
|
||||
try {
|
||||
fileLog("model-manager setup 启动")
|
||||
// ctx 形态诊断(一次性):回答「为什么进程内 reload 无通道」——
|
||||
// 2026-10-19 实测 keys 里没有 client/serverUrl/location,热重载只能靠 TUI 侧
|
||||
try {
|
||||
const c: any = ctx
|
||||
fileLog(
|
||||
"setup ctx 诊断: keys=" +
|
||||
JSON.stringify(Object.keys(c || {})) +
|
||||
" | client=" + typeof (c && c.client) +
|
||||
" | location=" + typeof (c && c.location) +
|
||||
" | serverUrl=" + typeof (c && c.serverUrl) +
|
||||
" | app=" + typeof (c && c.app),
|
||||
)
|
||||
} catch {
|
||||
/* 诊断失败不影响功能 */
|
||||
}
|
||||
ensureDbExec()
|
||||
|
||||
// 后台 daemon:每 5s 轮询 outbox(普通文件读写,不碰 DB 锁)
|
||||
|
||||
Reference in New Issue
Block a user