diff --git a/lib/host.js b/lib/host.js index 80141fb..77d792f 100644 --- a/lib/host.js +++ b/lib/host.js @@ -55,7 +55,25 @@ function short(v, n = 120) { } export function apply(ctx) { - const log = (...a) => { try { (ctx.logger?.info || console.log)('[float-window]', ...a) } catch { } } + // 运行状态与自检日志落盘:插件跑在 DSH 内部,控制台不一定看得到, + // 有了这个文件(和 /health)才能在用户机器上远程排障。 + const notes = [] + const statusFile0 = path.join(dshHome(), 'float-window', 'plugin-status.json') + const status = { version: '0.1.0', pid: process.pid, startedAt: new Date().toISOString(), tool: false, settings: false, notes } + const writeStatus = () => { + try { + status.notes = notes.slice(-40) + fs.mkdirSync(path.dirname(statusFile0), { recursive: true }) + fs.writeFileSync(statusFile0, JSON.stringify(status, null, 2)) + } catch { } + } + const log = (...a) => { + const line = a.map((x) => (typeof x === 'string' ? x : short(x, 200))).join(' ') + notes.push(`${new Date().toISOString().slice(11, 19)} ${line}`) + if (notes.length > 80) notes.shift() + try { (ctx.logger?.info || console.log)('[float-window]', ...a) } catch { } + writeStatus() + } // ---------------------------------------------------------------- endpoint const endpointFile = path.join(dshHome(), 'float-window', 'endpoint.json') @@ -78,12 +96,30 @@ export function apply(ctx) { // ---------------------------------------------------------------- 会话镜像缓冲 const MAX_ROWS = 120 const rows = new Map() // sessionId -> row[] + const counts = new Map() // sessionId -> 事件计数兜底(没有 sessionProjection 也能出数字) let lastSessionId = null + function counter(id) { + let c = counts.get(id) + if (!c) { c = { turns: 0, steps: 0, outTokens: 0, decodeMs: 0, stepStart: 0, lastTps: null }; counts.set(id, c) } + return c + } + ctx.on('session/event', (session, event) => { try { const id = session?.id || session?.sessionId || 'unknown' lastSessionId = id + const c = counter(id) + const type = String(event?.type || '') + if (type === 'turn/end') c.turns++ + else if (type === 'step/start') c.stepStart = Date.now() + else if (type === 'step/end') c.steps++ + else if (type === 'assistant/message') { + const usage = event?.usage || event?.message?.usage || {} + const out = usage.outputTokens ?? usage.output_tokens ?? usage.completionTokens + const ms = c.stepStart ? Date.now() - c.stepStart : 0 + if (out && ms > 0) { c.outTokens += out; c.decodeMs += ms; c.lastTps = out / (ms / 1000) } + } const row = toRow(event) if (!row) return const list = rows.get(id) || [] @@ -96,33 +132,48 @@ export function apply(ctx) { }) // ---------------------------------------------------------------- 指标 + let meterKeysLogged = false function metricsFor(session) { - const out = { turns: null, steps: null, tps: null, tokens: null, cacheHit: null, contextUsed: null } + const id = session?.id || lastSessionId || 'unknown' + const c = counts.get(id) || { turns: 0, steps: 0, outTokens: 0, decodeMs: 0, lastTps: null } + const out = { + turns: c.turns || null, + steps: c.steps || null, + tps: c.lastTps, + tokens: null, + cacheHit: null, + contextUsed: null, + } + // 有 session-projection 就用它(更权威),否则用事件计数兜底 try { const proj = ctx.get('sessionProjection') - const stats = proj?.get?.(session, 'sessionStats') || proj?.peek?.(session, 'sessionStats') - if (stats) { - out.turns = stats.turns ?? null - out.steps = stats.steps ?? null - if (stats.decodeTokens && stats.decodeMs) out.tps = (stats.decodeTokens / (stats.decodeMs / 1000)) + const st = proj?.get?.(session, 'sessionStats') || proj?.peek?.(session, 'sessionStats') + if (st) { + if (st.turns != null) out.turns = st.turns + if (st.steps != null) out.steps = st.steps + if (st.decodeTokens && st.decodeMs) out.tps = st.decodeTokens / (st.decodeMs / 1000) } } catch { } try { const meter = ctx.get('tokenMeter') const m = meter?.measure?.(session) if (m) { + // 第一次把真实字段名记进状态文件,便于在用户机器上核对映射 + if (!meterKeysLogged) { meterKeysLogged = true; log('tokenMeter keys: ' + Object.keys(m).join(',')) } out.tokens = m.totalTokens ?? m.surfaceTokens ?? null - const u = m.usage || m.providerUsage + const u = m.usage || m.providerUsage || m.usageBuckets if (u) { - const read = u.cacheRead ?? u.cache_read ?? 0 - const input = (u.input ?? 0) + read - if (input > 0) out.cacheHit = read / input + const read = u.cacheRead ?? u.cache_read ?? u.cacheReadTokens ?? 0 + const input = u.input ?? u.inputTokens ?? 0 + const denom = input + read + if (denom > 0) out.cacheHit = read / denom } - const capacity = ctx.get('llm')?.resolveModelInfo?.()?.context + let capacity = null + try { capacity = ctx.get('llm')?.resolveModelInfo?.()?.context ?? null } catch { } const used = m.surfaceTokens ?? m.totalTokens if (capacity && used) out.contextUsed = Math.min(1, used / capacity) } - } catch { } + } catch (e) { if (!meterKeysLogged) { meterKeysLogged = true; log('tokenMeter measure failed: ' + String(e?.message || e)) } } return out } @@ -228,6 +279,9 @@ export function apply(ctx) { } // ---------------------------------------------------------------- Agent 工具 + status.shell = shellPath() + writeStatus() + try { const tools = ctx.get('tools') if (tools?.register) { @@ -249,11 +303,13 @@ export function apply(ctx) { return r.ok ? `float-window: ${action} ok (${r.via})` : `float-window: ${action} failed: ${r.error}` }, }) + status.tool = true log('tool float_window registered') } else { log('tools service not available; agent trigger disabled') } } catch (e) { log('tool registration failed:', String(e?.message || e)) } + writeStatus() const broadcast = (msg) => { const payload = `data: ${JSON.stringify(msg)}\n\n` @@ -274,12 +330,35 @@ export function apply(ctx) { // 注册一个设置命名空间,DSH 的「设置 → 插件」会把它渲染成一张卡片。 // schemastery 是从 profile 的 node_modules 解析的;link: 安装的插件解析不到时 // 只跳过这一步(浮窗自己的设置面板照常可用)。 + log('services: ' + ['settings', 'sessions', 'tools', 'tokenMeter', 'sessionProjection', 'llm', 'shell', 'sandboxPolicy', 'webServer'] + .map((n) => `${n}=${(() => { try { return ctx.get(n) ? 'y' : 'n' } catch { return '?' } })()}`).join(' ')) ; (async () => { try { const settings = ctx.get('settings') if (!settings?.register) { log('settings service absent; skip settings entry'); return } let z = null + // link: 安装时插件自己的目录里没有 node_modules,裸导入会失败; + // 依次尝试:常规解析 -> 以宿主入口为基准解析 -> profile 目录解析 try { z = (await import('@deepseek-ai/schemastery')).default } catch { } + if (!z) { + try { + const { createRequire } = await import('node:module') + const req = createRequire(process.argv[1] || path.join(dshHome(), 'index.js')) + const m = req('@deepseek-ai/schemastery') + z = m?.default || m + } catch { } + } + if (!z) { + try { + const { createRequire } = await import('node:module') + for (const prof of ['desktop', 'web']) { + const req = createRequire(path.join(dshHome(), 'profiles', prof, 'package.json')) + const m = req('@deepseek-ai/schemastery') + z = m?.default || m + if (z) break + } + } catch { } + } if (!z) { log('schemastery not resolvable; skip settings entry'); return } const schema = z.object({ topmost: z.boolean().default(true).description('始终置顶'), @@ -299,6 +378,8 @@ export function apply(ctx) { return () => { try { stop?.() } catch { } } }) log('settings namespace registered') + status.settings = true + writeStatus() } catch (e) { log('settings registration failed:', String(e?.message || e)) } })()