mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-29 20:30:00 +08:00
发布 0.15.0: - 新增内核版本兼容层、特性门控、低版本阻断和升级提示 - 新增 PATH 中 OpenClaw CLI 冲突检测、隔离与恢复 - 修复 Hermes Gateway loopback 自动拉起与 /v1/runs 诊断 - 修复 standalone 一键安装包在 About/仪表盘显示未知版本 - 同步 OpenClaw 2026.5.6 推荐版本和热更新 minAppVersion - 补齐本地 JS/Rust 测试与发布前检查说明 验证: - npm run build - node --test tests/*.test.js - node --check src/scripts JS 文件 - cargo fmt --all -- --check - cargo check - cargo clippy --all-targets -- -D warnings - cargo test
195 lines
6.4 KiB
JavaScript
195 lines
6.4 KiB
JavaScript
/**
|
||
* 跨内核版本的归一化 API 封装
|
||
*
|
||
* 上游不同版本的 RPC 返回结构可能不同(数组 vs 分页对象、字段拆分等)。
|
||
* 此模块为页面提供 **统一形态** 的 helper:
|
||
* - 老内核返回数组 → wrapper 包成 { items, truncated, cursor }
|
||
* - 新内核返回 { items, truncated, cursor } → 透传
|
||
* - 页面按一致接口处理,不需自己 if/else
|
||
*
|
||
* 仅在需要使用 5.x 新返回字段(truncation、status 拆分等)时引入本模块;
|
||
* 不需要新字段的页面可以继续直接用 wsClient.request(...)。
|
||
*
|
||
* @see .tmp/multi-kernel-compat-design.md §5
|
||
*/
|
||
import { wsClient } from './ws-client.js'
|
||
import { hasFeature } from './kernel.js'
|
||
|
||
/**
|
||
* @typedef {Object} PagedResult
|
||
* @property {any[]} items 数据条目
|
||
* @property {boolean} truncated 是否被服务端截断
|
||
* @property {string|null} cursor 下一页游标,null 表示没有更多
|
||
* @property {number|null} total 服务端如果返回总数则填,否则 null
|
||
*/
|
||
|
||
/**
|
||
* 列出会话,归一化分页字段(兼容 4.x 数组返回 / 5.4+ 截断元数据)
|
||
*
|
||
* @param {Object} [opts]
|
||
* @param {number} [opts.limit=100]
|
||
* @param {string} [opts.cursor]
|
||
* @returns {Promise<PagedResult>}
|
||
*/
|
||
export async function listSessions(opts = {}) {
|
||
const params = { limit: opts.limit ?? 100 }
|
||
if (opts.cursor) params.cursor = opts.cursor
|
||
|
||
// sessions.list 在所有支持的内核版本(>=2026.3.x)都存在;用 requestCompat 仅为兜底
|
||
const raw = await wsClient.requestCompat('sessions.list', params, [])
|
||
|
||
// 老内核:直接返回数组
|
||
if (Array.isArray(raw)) {
|
||
return {
|
||
items: raw,
|
||
truncated: false,
|
||
cursor: null,
|
||
total: raw.length,
|
||
}
|
||
}
|
||
|
||
// 5.4+:返回 { items, truncated, cursor } 或 { sessions, hasMore, nextCursor }
|
||
const items = raw?.items ?? raw?.sessions ?? []
|
||
const truncated = !!(raw?.truncated || raw?.hasMore)
|
||
const cursor = raw?.cursor ?? raw?.nextCursor ?? null
|
||
const total = raw?.total ?? null
|
||
|
||
return { items, truncated, cursor, total }
|
||
}
|
||
|
||
/**
|
||
* @typedef {Object} MemoryStatus
|
||
* @property {boolean} ready 整体就绪
|
||
* @property {{ ready: boolean, reason?: string|null }} vectorStore 向量存储(sqlite-vec)
|
||
* @property {{ ready: boolean, reason?: string|null }} embedding 嵌入提供方
|
||
* @property {string|null} reason 整体失败原因(兼容老内核)
|
||
* @property {Object} raw 原始返回,调试用
|
||
*/
|
||
|
||
/**
|
||
* 获取记忆系统状态,归一化新老内核字段差异。
|
||
*
|
||
* 老内核(< 2026.5.3):返回 { ready, reason },前端无法区分 vector-store / embedding
|
||
* 新内核(>= 2026.5.3):返回 { ready, vectorStore: {...}, embedding: {...} }
|
||
*
|
||
* @param {Object} [opts]
|
||
* @param {boolean} [opts.deep] 仅在新内核生效,老内核忽略此字段
|
||
* @returns {Promise<MemoryStatus>}
|
||
*/
|
||
export async function memoryStatus(opts = {}) {
|
||
const params = {}
|
||
if (opts.deep && hasFeature('memory.statusDeepSplit')) {
|
||
params.deep = true
|
||
}
|
||
// 上游 RPC 名称在不同版本间统一为 doctor.memory.status,老内核可能没有
|
||
const raw = await wsClient.requestCompat('doctor.memory.status', params, null)
|
||
if (!raw) {
|
||
return {
|
||
ready: false,
|
||
vectorStore: { ready: false, reason: null },
|
||
embedding: { ready: false, reason: null },
|
||
reason: 'unsupported',
|
||
raw: null,
|
||
}
|
||
}
|
||
|
||
const overallReady = raw?.ready ?? raw?.healthy ?? false
|
||
const overallReason = raw?.reason ?? raw?.error ?? null
|
||
|
||
return {
|
||
ready: overallReady,
|
||
vectorStore: {
|
||
ready: raw?.vectorStore?.ready ?? overallReady,
|
||
reason: raw?.vectorStore?.reason ?? null,
|
||
},
|
||
embedding: {
|
||
ready: raw?.embedding?.ready ?? overallReady,
|
||
reason: raw?.embedding?.reason ?? null,
|
||
},
|
||
reason: overallReason,
|
||
raw,
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @typedef {Object} ProbeStatus
|
||
* @property {string} model
|
||
* @property {string|null} status 可能值: 'ok' | 'no_model' | 'excluded_by_auth_order' | 'cooling_down'
|
||
* @property {number|null} cooldownUntilMs
|
||
* @property {string|null} reason
|
||
*/
|
||
|
||
/**
|
||
* 模型探测:归一化 5.2+ 的 excluded_by_auth_order / no_model / cooldown 字段。
|
||
*
|
||
* @param {Object} [opts]
|
||
* @returns {Promise<ProbeStatus[]>}
|
||
*/
|
||
export async function modelStatusProbe(opts = {}) {
|
||
// 5.2+ 才有专用 probe RPC,老内核回退到 model.list
|
||
if (hasFeature('models.probeStatus')) {
|
||
const raw = await wsClient.requestCompat('model.status.probe', opts, null)
|
||
if (raw) {
|
||
const list = raw?.results || raw?.probes || raw || []
|
||
return list.map(m => ({
|
||
model: m.model || m.id || '',
|
||
status: m.status || 'ok',
|
||
cooldownUntilMs: m.cooldownUntilMs ?? m.cooldown_until_ms ?? null,
|
||
reason: m.reason ?? null,
|
||
}))
|
||
}
|
||
}
|
||
|
||
// 降级:用 model.list 构造简化 probe 结果
|
||
const raw = await wsClient.requestCompat('model.list', {}, null)
|
||
if (!raw) return []
|
||
const models = raw?.models || raw || []
|
||
return models.map(m => ({
|
||
model: m.id || m.fullId || m.name || '',
|
||
status: 'ok',
|
||
cooldownUntilMs: null,
|
||
reason: null,
|
||
}))
|
||
}
|
||
|
||
/**
|
||
* 列出 Agents,自动剥离/补全 agentRuntime 字段。
|
||
*
|
||
* - 老内核(< 2026.5.2):补 `agentRuntime: { id: 'pi' }` 默认值
|
||
* - 新内核:透传
|
||
*
|
||
* 注意:底层调用的是 Tauri 命令 `list_agents`(封装了 Gateway RPC),不直接走 WS。
|
||
*/
|
||
export async function listAgentsCompat() {
|
||
// 动态导入避免顶层依赖
|
||
const { api } = await import('./tauri-api.js')
|
||
const list = await api.listAgents()
|
||
if (!Array.isArray(list)) return []
|
||
|
||
return list.map(ag => {
|
||
const out = { ...ag }
|
||
if (!out.agentRuntime) {
|
||
out.agentRuntime = { id: 'pi' }
|
||
} else if (typeof out.agentRuntime === 'string') {
|
||
out.agentRuntime = { id: out.agentRuntime }
|
||
}
|
||
return out
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Cron 任务列表,归一化老/新返回结构。
|
||
*
|
||
* - 老内核:可能直接返回数组或 { jobs: [...] }
|
||
* - 新内核(5.4+):每个 job 多了 lastErrorDetail 字段
|
||
*
|
||
* @returns {Promise<{ jobs: any[] }>}
|
||
*/
|
||
export async function cronList(opts = {}) {
|
||
const params = { includeDisabled: opts.includeDisabled ?? true }
|
||
const raw = await wsClient.requestCompat('cron.list', params, null)
|
||
if (!raw) return { jobs: [], raw: null }
|
||
const jobs = Array.isArray(raw) ? raw : (raw?.jobs ?? [])
|
||
return { jobs, raw }
|
||
}
|