@jesonliu/lark-claudecode-bridge 1.3.7 → 1.4.0
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 +17 -0
- package/assets/web/js/main.js +2 -1
- package/assets/web/js/pages/memory.js +561 -0
- package/config.example.yaml +34 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +133 -1
- package/dist/config.js.map +1 -1
- package/dist/executor/claude-executor.d.ts +26 -3
- package/dist/executor/claude-executor.js +718 -293
- package/dist/executor/claude-executor.js.map +1 -1
- package/dist/executor/notify-server.d.ts +10 -0
- package/dist/executor/notify-server.js +11 -0
- package/dist/executor/notify-server.js.map +1 -1
- package/dist/executor/permission-gate.d.ts +8 -1
- package/dist/executor/permission-gate.js +7 -3
- package/dist/executor/permission-gate.js.map +1 -1
- package/dist/gateway/card-builder.d.ts +15 -3
- package/dist/gateway/card-builder.js +21 -10
- package/dist/gateway/card-builder.js.map +1 -1
- package/dist/gateway/feishu-gateway.d.ts +13 -0
- package/dist/gateway/feishu-gateway.js +34 -2
- package/dist/gateway/feishu-gateway.js.map +1 -1
- package/dist/gateway/progress-card.d.ts +4 -2
- package/dist/gateway/progress-card.js +52 -25
- package/dist/gateway/progress-card.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +225 -32
- package/dist/index.js.map +1 -1
- package/dist/memory-installer.d.ts +174 -0
- package/dist/memory-installer.js +1035 -0
- package/dist/memory-installer.js.map +1 -0
- package/dist/memory.d.ts +88 -0
- package/dist/memory.js +195 -0
- package/dist/memory.js.map +1 -0
- package/dist/security/paths.d.ts +3 -1
- package/dist/security/paths.js +28 -4
- package/dist/security/paths.js.map +1 -1
- package/dist/session/session-store.d.ts +15 -0
- package/dist/session/session-store.js +16 -0
- package/dist/session/session-store.js.map +1 -1
- package/dist/types.d.ts +56 -1
- package/dist/web/config-api.js +32 -0
- package/dist/web/config-api.js.map +1 -1
- package/dist/web/memory-api.d.ts +25 -0
- package/dist/web/memory-api.js +197 -0
- package/dist/web/memory-api.js.map +1 -0
- package/dist/web/server.js +73 -2
- package/dist/web/server.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -154,8 +154,25 @@ concurrency: 3 # 通道间并发上限(未单独配置的 app 沿
|
|
|
154
154
|
# retention_days: 90
|
|
155
155
|
# session: # 会话行为微调
|
|
156
156
|
# context_remind_tokens: 150000 # 上下文超长提醒阈值(tokens);0 = 关闭;改后热生效
|
|
157
|
+
# resident_process: true # 常驻进程池:同通道复用 CLI 子进程,冷启动税摊薄为每会话一次(见下节)
|
|
158
|
+
# resident_idle_minutes: 60 # 常驻进程空闲回收(分钟);0 = 不回收;缺省 60
|
|
159
|
+
# resident_max_processes: 3 # 进程池上限(按机器人,LRU 回收空闲进程);缺省 = concurrency
|
|
160
|
+
# group_idle_reset_hours: 4 # 群聊空闲滚动(小时);0 = 关闭;缺省 4;私聊不受影响
|
|
161
|
+
# group_max_turns: 30 # 群聊轮次滚动(任务数);0 = 关闭;缺省 30
|
|
162
|
+
# group_roll_notify: true # 群聊滚动后首条回复尾部轻注记;缺省 true
|
|
157
163
|
```
|
|
158
164
|
|
|
165
|
+
### 会话生命周期:常驻进程 + 群聊自动滚动
|
|
166
|
+
|
|
167
|
+
**常驻进程池(响应提速)**:同一聊天通道的连续消息复用同一个 Claude Code 子进程——冷启动(拉起全部 MCP / 插件,实测约 30s)从「每条消息付一次」摊薄为「每个会话付一次」,同通道第二条消息即刻开始执行。可靠性语义:
|
|
168
|
+
|
|
169
|
+
- **热生效保真**:人设、`/model`、`/plan`、环境变量、配置页 MCP、插件清单、厂商档案任一改动,下一条消息自动回收旧进程并按新配置重建(配置指纹比对)——「每任务现读热生效」承诺不变;
|
|
170
|
+
- **`/stop` 与硬超时**照旧生效(进程终止,下条消息重新冷启);
|
|
171
|
+
- **空闲回收**:通道闲置超 `resident_idle_minutes`(缺省 60 分钟)自动杀进程释放内存,只影响下次响应速度、不影响会话——指针与对话记录均落盘,追问时冷启动 + 续接无缝接上;进程意外崩溃同样自愈;
|
|
172
|
+
- 不想要此行为:`resident_process: false` 回到每消息冷启(旧行为)。
|
|
173
|
+
|
|
174
|
+
**群聊自动滚动(上下文保鲜)**:群聊会话按「双阈值」自动开新话题——空闲超过 `group_idle_reset_hours`(缺省 4 小时)或累计任务数达到 `group_max_turns`(缺省 30),先到先滚。滚动即 `/new` 的语义(历史保留、`/resume` 可找回),仅在滚动后首条回复尾部带一次轻注记。私聊不滚动(手动 `/new` 管理最优)。
|
|
175
|
+
|
|
159
176
|
### 认证双模式(inherit / managed)
|
|
160
177
|
|
|
161
178
|
|
package/assets/web/js/main.js
CHANGED
|
@@ -14,8 +14,9 @@ import { page as pSlash } from './pages/slash.js';
|
|
|
14
14
|
import { page as pPlugins } from './pages/plugins.js';
|
|
15
15
|
import { page as pSkills } from './pages/skills.js';
|
|
16
16
|
import { page as pMcp } from './pages/mcp.js';
|
|
17
|
+
import { page as pMemory } from './pages/memory.js';
|
|
17
18
|
|
|
18
|
-
const PAGES = [pOverview, pApps, pWorkspaces, pClaude, pPermissions, pSlash, pPlugins, pSkills, pMcp];
|
|
19
|
+
const PAGES = [pOverview, pApps, pWorkspaces, pClaude, pPermissions, pSlash, pPlugins, pSkills, pMcp, pMemory];
|
|
19
20
|
const pageById = new Map(PAGES.map((p) => [p.id, p]));
|
|
20
21
|
const DEFAULT_TAB = 'overview';
|
|
21
22
|
|
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
// ============ 记忆增强(OpenViking 长期记忆,生命周期方案 §6.7/§6.8/§6.9) ============
|
|
2
|
+
// 双视图页面(互斥渲染,消除「未安装却看一堆服务状态行」的错位):
|
|
3
|
+
// - 未安装向导(!serverFound && !healthy && 非远程模式):引导横幅(⚡ 一键安装 + 五步流水线
|
|
4
|
+
// 水平 stepper,模型未保存时安装置灰)+ 模型配置(安装前置)+ 外部服务入口 + 折叠手动指引。
|
|
5
|
+
// - 已安装:总开关卡(OFF 时下方整体置灰仅查看)+ 服务状态卡(deriveState 八态精确状态横幅
|
|
6
|
+
// + 关键信息网格;启停 / 检查修复 / 刷新按钮就在卡标题右侧——状态与操作同屏)+ 模型配置卡。
|
|
7
|
+
// 配置读写走标准 doc 管道(saveDoc → PUT /api/config,secret 自动回填);测试连接走
|
|
8
|
+
// /api/memory/test-connection(表单现值,不落盘);安装/启停走 /api/memory/install|start|stop。
|
|
9
|
+
// 安装回写会改 config.yaml——完成后走 refresh()(重拉 S.doc 再渲染),避免陈旧 doc 覆盖回写。
|
|
10
|
+
import { S, $, esc, api, toast, saveDoc, hooks, refresh } from '../core.js';
|
|
11
|
+
import { confirmDialog } from '../ui.js';
|
|
12
|
+
|
|
13
|
+
/** 服务商预设(§6.8 填表助手:只预填已知值,全部可见可改) */
|
|
14
|
+
const PRESETS = {
|
|
15
|
+
glm: { label: '智谱 GLM', api_base: 'https://open.bigmodel.cn/api/paas/v4', vlm_model: 'glm-4.7', embedding_model: 'embedding-3', key_hint: 'open.bigmodel.cn 的 API Key' },
|
|
16
|
+
volcengine: { label: '火山方舟', api_base: 'https://ark.cn-beijing.volces.com/api/v3', vlm_model: '', embedding_model: '', key_hint: '方舟 API Key;模型名填推理接入点 EP ID' },
|
|
17
|
+
'openai-compatible': { label: 'OpenAI 兼容(中转 / 自建)', api_base: '', vlm_model: '', embedding_model: '', key_hint: '任意 OpenAI 协议端点的 Key(本地端点可留空)' },
|
|
18
|
+
ollama: { label: 'Ollama 本地(零 Key)', api_base: 'http://127.0.0.1:11434/v1', vlm_model: 'qwen3:8b', embedding_model: 'nomic-embed-text', key_hint: '本地服务无需 Key' },
|
|
19
|
+
};
|
|
20
|
+
const DEFAULT_ENDPOINT = 'http://127.0.0.1:1933';
|
|
21
|
+
const DOC_URL = 'https://github.com/volcengine/OpenViking';
|
|
22
|
+
|
|
23
|
+
/** 探活失败分类 → 下一步行动建议(后端 reason 驱动;not-running 由状态机接管,不在此列) */
|
|
24
|
+
const REASON_HINT = {
|
|
25
|
+
auth: '鉴权失败——服务为 api_key 模式时,检查 ov.conf 的 root key 与本页保存的 API Key 是否一致(一键安装会自动对齐)',
|
|
26
|
+
timeout: '服务无响应——进程可能卡死,可「一键检查 / 修复」重拉',
|
|
27
|
+
unreachable: '端点不可达——检查服务端点地址与网络(远程模式常见),或服务是否监听该地址',
|
|
28
|
+
'http-error': '服务返回异常状态码——可「一键检查 / 修复」重拉,或查看服务日志定位',
|
|
29
|
+
unhealthy: '服务进程在但自报异常——可「一键检查 / 修复」重拉,或查看服务日志定位',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** 向导 idle 态的五步预览(真实进度以 /api/memory/install/status 为准) */
|
|
33
|
+
const IDLE_STEPS = [
|
|
34
|
+
{ id: 'runtime', title: '运行时(uv)', status: 'pending' },
|
|
35
|
+
{ id: 'service', title: '安装 OpenViking 服务', status: 'pending' },
|
|
36
|
+
{ id: 'config', title: '生成服务配置(ov.conf)', status: 'pending' },
|
|
37
|
+
{ id: 'start', title: '拉起受管服务', status: 'pending' },
|
|
38
|
+
{ id: 'plugin', title: '接入记忆插件', status: 'pending' },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/** 路径缩短(家目录前缀折叠为 ~) */
|
|
42
|
+
const shortPath = (p) => esc(String(p)
|
|
43
|
+
.replace(/^C:\\Users\\[^\\]+/i, '~')
|
|
44
|
+
.replace(/^\/(home|Users)\/[^/]+/, '~'));
|
|
45
|
+
|
|
46
|
+
/** 受管运行时间人性化(ISO → 「2天 3小时」/「4小时 12分钟」/「35分钟」) */
|
|
47
|
+
const fmtSince = (iso) => {
|
|
48
|
+
const t = Date.parse(iso);
|
|
49
|
+
if (!Number.isFinite(t)) return '—';
|
|
50
|
+
const min = Math.floor((Date.now() - t) / 60000);
|
|
51
|
+
if (min < 1) return '刚刚';
|
|
52
|
+
const h = Math.floor(min / 60), d = Math.floor(h / 24);
|
|
53
|
+
return d > 0 ? `${d}天 ${h % 24}小时` : h > 0 ? `${h}小时 ${min % 60}分钟` : `${min}分钟`;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** 端点是否指向本机(非本机 + mode=remote = 远程模式,不引导本地安装) */
|
|
57
|
+
const isLocalHost = (u) => {
|
|
58
|
+
try {
|
|
59
|
+
const h = new URL(u).hostname;
|
|
60
|
+
return h === '127.0.0.1' || h === 'localhost' || h === '::1' || h === '[::1]' || h === '0.0.0.0';
|
|
61
|
+
} catch { return false; }
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** 信息网格字段图标(Lucide 风格线性 SVG,stroke:currentColor) */
|
|
65
|
+
const cellIcon = (inner) => `<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${inner}</svg>`;
|
|
66
|
+
const ICONS = {
|
|
67
|
+
globe: '<circle cx="12" cy="12" r="9"/><path d="M3 12h18"/><path d="M12 3a14 14 0 0 1 0 18a14 14 0 0 1 0-18"/>',
|
|
68
|
+
box: '<path d="M3 8l9-5 9 5v8l-9 5-9-5z"/><path d="M3 8l9 5 9-5"/><path d="M12 13v8"/>',
|
|
69
|
+
server: '<rect x="3" y="4" width="18" height="7" rx="2"/><rect x="3" y="14" width="18" height="6" rx="2"/><path d="M7 7.5h.01M7 17h.01"/>',
|
|
70
|
+
clock: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 3"/>',
|
|
71
|
+
spark: '<path d="M12 3l1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9z"/>',
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** 关键信息网格单元(图标+标签在上、值在下;值行 flex 防错位换行;span>1 时跨多列) */
|
|
75
|
+
const infoCell = (icon, label, valueHtml, span = 1) =>
|
|
76
|
+
`<div style="min-width:0${span > 1 ? `;grid-column:span ${span}` : ''}">
|
|
77
|
+
<div class="hint" style="display:flex;align-items:center;gap:5px;margin-bottom:4px">${cellIcon(ICONS[icon])}${label}</div>
|
|
78
|
+
<div style="font-size:13px;display:flex;align-items:center;gap:6px;flex-wrap:wrap;line-height:1.5">${valueHtml}</div>
|
|
79
|
+
</div>`;
|
|
80
|
+
|
|
81
|
+
/** 页头说明行(卡片外,两视图共用):一句话定位 + 文档外链 */
|
|
82
|
+
const pageIntro = `<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;margin:0 0 14px">
|
|
83
|
+
<span class="hint" style="font-size:12.5px">OpenViking 长期记忆:每条消息自动召回相关历史、会话归档自动提炼,按 <code>{appId}-{userId}</code> 命名空间隔离(机器人之间互不可见)。</span>
|
|
84
|
+
<!--<a href="${DOC_URL}" target="_blank" rel="noreferrer" style="font-size:12.5px;flex:0 0 auto;margin-left:auto">查看文档 ↗</a>-->
|
|
85
|
+
</div>`;
|
|
86
|
+
|
|
87
|
+
// ============ 服务状态八态(「一概而论异常」→ 精确状态;探活 reason 全量映射) ============
|
|
88
|
+
|
|
89
|
+
function deriveState(st, svcInstalled) {
|
|
90
|
+
const h = st.health ?? {};
|
|
91
|
+
if (h.healthy) return 'running';
|
|
92
|
+
switch (h.reason) {
|
|
93
|
+
case 'not-running': return svcInstalled ? 'stopped' : 'not-installed';
|
|
94
|
+
case 'auth': return 'auth';
|
|
95
|
+
case 'timeout': return 'timeout';
|
|
96
|
+
case 'unreachable': return 'unreachable';
|
|
97
|
+
case 'http-error': case 'unhealthy': return 'unhealthy';
|
|
98
|
+
default: return 'error';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 状态元数据:标签 + tag 色(css 定义 ok/off/err)+ 状态横幅底色 */
|
|
103
|
+
const STATE_META = {
|
|
104
|
+
running: { label: '运行中', cls: 'ok', bg: 'var(--ok-bg)' },
|
|
105
|
+
stopped: { label: '已停止', cls: 'off', bg: 'var(--off-bg)' },
|
|
106
|
+
auth: { label: '鉴权失败', cls: 'err', bg: 'var(--err-bg)' },
|
|
107
|
+
timeout: { label: '无响应', cls: 'err', bg: 'var(--err-bg)' },
|
|
108
|
+
unreachable: { label: '端点不可达', cls: 'err', bg: 'var(--err-bg)' },
|
|
109
|
+
unhealthy: { label: '服务异常', cls: 'err', bg: 'var(--err-bg)' },
|
|
110
|
+
error: { label: '未知错误', cls: 'err', bg: 'var(--err-bg)' },
|
|
111
|
+
'not-installed': { label: '未安装', cls: 'off', bg: 'var(--off-bg)' },
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** 记忆插件加载渠道标注(status.plugin.source):与任务装配实际加载源一一对应 */
|
|
115
|
+
const PLUGIN_SOURCE_LABEL = {
|
|
116
|
+
marketplace: 'CLI 安装',
|
|
117
|
+
explicit: '显式引用',
|
|
118
|
+
managed: 'LCB 自管副本',
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/** 客户端接入状态标注(status.plugin.clientConfState):插件侧启用判定 env 优先、文件兜底——
|
|
122
|
+
* 与 LCB 侧「加载渠道」是两回事(加载渠道 ✓ 不代表启用 ✓)。missing 且记忆开启 = env 通道
|
|
123
|
+
* 启用(LCB 每任务下发 OPENVIKING_MEMORY_ENABLED,无须任何文件) */
|
|
124
|
+
const CLIENT_CONF_LABEL = {
|
|
125
|
+
'lcb-managed': '客户端配置 LCB 托管',
|
|
126
|
+
'user-managed': '客户端配置本机自管',
|
|
127
|
+
external: '外部 ov.conf 接入',
|
|
128
|
+
missing: 'env 通道启用',
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/** 记忆插件 cell 尾部的启用徽标:未启用红字提醒(全绿但 hooks 空转的历史盲区在此现形) */
|
|
132
|
+
function clientConfBadge(p) {
|
|
133
|
+
if (!p) return '';
|
|
134
|
+
if (p.clientEnabled === false) return ' <span class="hint" style="color:var(--warn-tx)">未启用(无 env、无配置文件)</span>';
|
|
135
|
+
const label = p.clientConfState && CLIENT_CONF_LABEL[p.clientConfState];
|
|
136
|
+
return label ? ` <span class="hint" title="插件侧启用判定:env 优先,客户端配置兜底(${esc(p.clientConfPath || '')})">${esc(label)}</span>` : '';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ============ 模型表单(双视图共用:渲染 + 绑定) ============
|
|
140
|
+
|
|
141
|
+
function modelFormHtml(model, endpoint) {
|
|
142
|
+
const secretVal = (v) => (v && typeof v === 'object' && v.secretSet) ? v.secretHint : '';
|
|
143
|
+
return `
|
|
144
|
+
<div class="grid2">
|
|
145
|
+
<div><label>服务商预设(填表助手,值可改)</label>
|
|
146
|
+
<select id="memProvider">
|
|
147
|
+
<option value="">— 选择预设 —</option>
|
|
148
|
+
${Object.entries(PRESETS).map(([k, p]) => `<option value="${k}" ${model.provider === k ? 'selected' : ''}>${p.label}</option>`).join('')}
|
|
149
|
+
</select>
|
|
150
|
+
<div class="hint" style="margin-top:3px">选择后只预填空字段,已填值不覆盖</div></div>
|
|
151
|
+
<div><label>API 端点(api_base)</label><input type="text" id="memApiBase" value="${esc(model.apiBase || '')}" placeholder="https://…/v1">
|
|
152
|
+
<div class="hint" style="margin-top:3px">OpenAI 协议端点,与机器人对话用的厂商档案相互独立</div></div>
|
|
153
|
+
<div><label>API Key ${secretVal(model.apiKey) ? `<span class="hint">已保存 ${esc(secretVal(model.apiKey))},测试连接将使用已保存 Key;更换请输入新值</span>` : ''}</label>
|
|
154
|
+
<input type="password" id="memApiKey" value="" placeholder="${esc(PRESETS[model.provider]?.key_hint || 'sk-…(本地端点可留空)')}"></div>
|
|
155
|
+
<div><label>提炼模型(vlm,会话归档时用)</label><input type="text" id="memVlm" value="${esc(model.vlmModel || '')}" placeholder="glm-4.7">
|
|
156
|
+
<div class="hint" style="margin-top:3px">归档会话时提炼记忆的对话模型,可选更强档位</div></div>
|
|
157
|
+
<div><label>向量模型(embedding)</label><input type="text" id="memEmb" value="${esc(model.embeddingModel || '')}" placeholder="embedding-3">
|
|
158
|
+
<div class="hint" style="margin-top:3px">文本向量化模型,测试连接会真调一次验证</div></div>
|
|
159
|
+
<div><label>服务端点(OpenViking server)</label><input type="text" id="memEndpoint" value="${esc(endpoint || DEFAULT_ENDPOINT)}" placeholder="${DEFAULT_ENDPOINT}">
|
|
160
|
+
<div class="hint" style="margin-top:3px">一般无须修改:一键安装自动回写;远程模式填外部服务地址</div></div>
|
|
161
|
+
</div>`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** 绑定模型表单交互(预设联动 / 测试连接双绿 / 保存)。isWizard 只影响保存后的提示文案 */
|
|
165
|
+
function bindModelForm(isWizard) {
|
|
166
|
+
// 预设联动:只预填空字段(已手填的值不覆盖)
|
|
167
|
+
$('#memProvider').onchange = (e) => {
|
|
168
|
+
const p = PRESETS[e.target.value];
|
|
169
|
+
if (!p) return;
|
|
170
|
+
if (!$('#memApiBase').value.trim()) $('#memApiBase').value = p.api_base;
|
|
171
|
+
if (!$('#memVlm').value.trim()) $('#memVlm').value = p.vlm_model;
|
|
172
|
+
if (!$('#memEmb').value.trim()) $('#memEmb').value = p.embedding_model;
|
|
173
|
+
$('#memApiKey').placeholder = p.key_hint || 'sk-…';
|
|
174
|
+
};
|
|
175
|
+
// 测试连接:表单现值直测(不落盘)——把 Key/端点/模型名错误挡在开启之前
|
|
176
|
+
$('#memTest').onclick = async () => {
|
|
177
|
+
const btn = $('#memTest');
|
|
178
|
+
btn.disabled = true;
|
|
179
|
+
$('#memTestResult').textContent = '测试中(真实调用 embedding + 提炼模型各一次)…';
|
|
180
|
+
$('#memTestReport').style.display = 'none';
|
|
181
|
+
try {
|
|
182
|
+
const r = await api('POST', '/api/memory/test-connection', {
|
|
183
|
+
api_base: $('#memApiBase').value.trim(),
|
|
184
|
+
api_key: $('#memApiKey').value.trim(),
|
|
185
|
+
vlm_model: $('#memVlm').value.trim(),
|
|
186
|
+
embedding_model: $('#memEmb').value.trim(),
|
|
187
|
+
});
|
|
188
|
+
const line = (name, c) => `${c.ok ? '✅' : '❌'} ${name}:${c.detail}`;
|
|
189
|
+
$('#memTestResult').innerHTML = r.ok
|
|
190
|
+
? '<span class="tag ok">双绿通过</span> 可以保存' + (isWizard ? '并一键安装' : '')
|
|
191
|
+
: '<span class="tag err">未通过</span> 请按下方报告修正';
|
|
192
|
+
$('#memTestReport').style.display = '';
|
|
193
|
+
$('#memTestReport').textContent = `${line('向量模型', r.embedding)}\n${line('提炼模型', r.vlm)}`;
|
|
194
|
+
} catch (e) {
|
|
195
|
+
$('#memTestResult').innerHTML = `<span class="tag err">请求失败</span> ${esc(e.message)}`;
|
|
196
|
+
}
|
|
197
|
+
btn.disabled = false;
|
|
198
|
+
};
|
|
199
|
+
// 保存模型配置:写入 S.doc.memory(secret 空值保持脱敏对象 → 后端 applySecrets 回填现值)
|
|
200
|
+
$('#memSaveModel').onclick = async () => {
|
|
201
|
+
if (!S.doc.memory || typeof S.doc.memory !== 'object') S.doc.memory = {};
|
|
202
|
+
const mem = S.doc.memory;
|
|
203
|
+
mem.endpoint = $('#memEndpoint').value.trim() || DEFAULT_ENDPOINT;
|
|
204
|
+
const keep = (mem.model && typeof mem.model === 'object') ? mem.model : {};
|
|
205
|
+
const pv = $('#memProvider').value;
|
|
206
|
+
const next = {
|
|
207
|
+
...keep,
|
|
208
|
+
...(pv ? { provider: pv } : {}),
|
|
209
|
+
api_base: $('#memApiBase').value.trim(),
|
|
210
|
+
vlm_model: $('#memVlm').value.trim(),
|
|
211
|
+
embedding_model: $('#memEmb').value.trim(),
|
|
212
|
+
};
|
|
213
|
+
if (!pv) delete next.provider;
|
|
214
|
+
mem.model = next;
|
|
215
|
+
const rawKey = $('#memApiKey').value.trim();
|
|
216
|
+
if (rawKey) mem.model.api_key = rawKey; // 空值保留脱敏对象 → applySecrets 回填
|
|
217
|
+
if (!await saveDoc()) return;
|
|
218
|
+
if (isWizard) {
|
|
219
|
+
// 保存即解锁「⚡ 一键安装」:重渲染让 modelSaved 重新计算(不让用户对着灰按钮困惑到刷新)
|
|
220
|
+
toast('模型配置已保存——「⚡ 一键安装」已解锁');
|
|
221
|
+
hooks.rerender();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
toast('记忆模型配置已保存(未开启记忆时仅保存,打开开关后生效)');
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ============ 安装流水线(双视图共用:水平 stepper + 轮询 + 进页恢复) ============
|
|
229
|
+
|
|
230
|
+
/** 模块级轮询句柄:rerender 不产生重复 interval(旧闭包继续刷新 DOM,gate 以最新渲染为准) */
|
|
231
|
+
let installTimer = null;
|
|
232
|
+
|
|
233
|
+
function installStepsHtml(steps) {
|
|
234
|
+
const chip = (s, i) => {
|
|
235
|
+
const done = s.status === 'done' || s.status === 'skipped';
|
|
236
|
+
const badge = done
|
|
237
|
+
? '<span style="width:20px;height:20px;border-radius:50%;background:var(--ok-tx);color:#fff;display:inline-flex;align-items:center;justify-content:center;font-size:11px;flex:0 0 auto">✓</span>'
|
|
238
|
+
: s.status === 'failed'
|
|
239
|
+
? '<span style="width:20px;height:20px;border-radius:50%;background:var(--err-tx);color:#fff;display:inline-flex;align-items:center;justify-content:center;font-size:11px;flex:0 0 auto">✗</span>'
|
|
240
|
+
: s.status === 'running'
|
|
241
|
+
? '<span class="br-spin-sm" style="margin:0;flex:0 0 auto"></span>'
|
|
242
|
+
: `<span style="width:20px;height:20px;border-radius:50%;border:1.5px solid var(--muted);color:var(--muted);display:inline-flex;align-items:center;justify-content:center;font-size:11px;flex:0 0 auto">${i + 1}</span>`;
|
|
243
|
+
const fallback = ({ pending: '等待中', running: '执行中…', done: '完成', skipped: '已就绪(跳过)' })[s.status] || '';
|
|
244
|
+
const sub = esc(s.detail || fallback);
|
|
245
|
+
return `<div style="flex:1;min-width:0;background:var(--off-bg);border-radius:10px;padding:9px 11px">
|
|
246
|
+
<div style="display:flex;align-items:center;gap:7px">${badge}<b style="font-size:12px;line-height:1.35">${esc(s.title)}</b></div>
|
|
247
|
+
<div class="hint" style="margin-top:5px;font-size:11px;line-height:1.5;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical" title="${esc(s.detail || '')}">${sub || ' '}</div>
|
|
248
|
+
</div>`;
|
|
249
|
+
};
|
|
250
|
+
return `<div style="display:flex;gap:6px;align-items:stretch;margin-top:12px">${steps.map(chip).join('<span class="hint" style="align-self:center;flex:0 0 auto">→</span>')}</div>`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* 绑定安装按钮 + 轮询 + 进页恢复。gate() = 额外禁用条件(向导:模型未保存时禁装——
|
|
255
|
+
* 首装生成 ov.conf 需要模型三件套;已安装视图的「检查/修复」不受此限)。
|
|
256
|
+
*/
|
|
257
|
+
function bindInstallActions(gate) {
|
|
258
|
+
const renderInstall = (ist) => {
|
|
259
|
+
const box = $('#memInstallSteps');
|
|
260
|
+
if (box) box.innerHTML = installStepsHtml(ist.steps);
|
|
261
|
+
const failed = ist.steps.find((s) => s.status === 'failed');
|
|
262
|
+
const hintEl = $('#memInstallHint');
|
|
263
|
+
const kindTxt = ist.kind === 'start' ? '启动流水线' : '安装流水线';
|
|
264
|
+
if (hintEl) hintEl.innerHTML = ist.running ? `${kindTxt}执行中…`
|
|
265
|
+
: failed ? `<span style="color:var(--warn-tx)">「${esc(failed.title)}」失败:${esc(failed.detail || '')}——修正后可重跑(已就绪步骤自动跳过)</span>`
|
|
266
|
+
: ist.finishedAt ? (ist.kind === 'start'
|
|
267
|
+
? '<span class="tag ok">启动完成</span> 服务已就绪、记忆插件已接入——LCB 接管的完整记忆链路即刻生效'
|
|
268
|
+
: '<span class="tag ok">全部就绪</span> 配置已回写(endpoint / mode=embedded),机器人记忆即刻生效')
|
|
269
|
+
: '';
|
|
270
|
+
const btn = $('#memInstall');
|
|
271
|
+
if (btn) btn.disabled = !!ist.running || gate();
|
|
272
|
+
['memStart', 'memStop', 'memRefresh'].forEach((id) => { const b = $('#' + id); if (b) b.disabled = !!ist.running; });
|
|
273
|
+
};
|
|
274
|
+
const poll = async () => {
|
|
275
|
+
try {
|
|
276
|
+
const ist = await api('GET', '/api/memory/install/status');
|
|
277
|
+
renderInstall(ist);
|
|
278
|
+
if (!ist.running) {
|
|
279
|
+
if (installTimer) { clearInterval(installTimer); installTimer = null; }
|
|
280
|
+
// 回写改了 config.yaml:refresh 重拉 S.doc 再整页渲染(防止陈旧 doc 在下次保存时覆盖回写)
|
|
281
|
+
refresh().catch(() => hooks.rerender());
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
} catch { /* 轮询失败静默重试 */ }
|
|
285
|
+
};
|
|
286
|
+
const startPipeline = (path, runningMsg) => async () => {
|
|
287
|
+
try {
|
|
288
|
+
await api('POST', path, {});
|
|
289
|
+
toast(runningMsg);
|
|
290
|
+
if (!installTimer) installTimer = setInterval(poll, 2000);
|
|
291
|
+
poll();
|
|
292
|
+
} catch (e) { toast(e.message, true); }
|
|
293
|
+
};
|
|
294
|
+
const btn = $('#memInstall');
|
|
295
|
+
if (btn) btn.onclick = startPipeline('/api/memory/install', '安装流水线已启动(后台执行,本页实时显示进度)');
|
|
296
|
+
// 启动服务:LCB 接管式启动流水线(检查 → 拉起探活 → 接入插件),同款 stepper 实时进度——
|
|
297
|
+
// 结束即完整接管态,无须再点「检查修复」补插件
|
|
298
|
+
const startBtn = $('#memStart');
|
|
299
|
+
if (startBtn) startBtn.onclick = startPipeline('/api/memory/start', '启动流水线已开始(检查 → 拉起 → 接入插件,本页实时显示进度)');
|
|
300
|
+
// 进页恢复(切页回来 / 安装刚结束):在跑则继续轮询,刚结束则展示结果
|
|
301
|
+
api('GET', '/api/memory/install/status').then((ist) => {
|
|
302
|
+
if (ist.running || ist.finishedAt) {
|
|
303
|
+
renderInstall(ist);
|
|
304
|
+
if (ist.running && !installTimer) installTimer = setInterval(poll, 2000);
|
|
305
|
+
}
|
|
306
|
+
}).catch(() => {});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ============ 主渲染 ============
|
|
310
|
+
|
|
311
|
+
export async function renderMemory(el) {
|
|
312
|
+
el.innerHTML = '<div class="card"><h3>记忆增强</h3><div class="br-spin-sm"></div><div class="hint">正在读取记忆服务状态…</div></div>';
|
|
313
|
+
let st;
|
|
314
|
+
try {
|
|
315
|
+
st = await api('GET', '/api/memory/status');
|
|
316
|
+
} catch (e) {
|
|
317
|
+
el.innerHTML = `<div class="card"><h3>记忆增强</h3><div class="hint">状态读取失败:${esc(e.message)}</div></div>`;
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const m = st.memory ?? { enabled: false, mode: 'remote', endpoint: DEFAULT_ENDPOINT };
|
|
321
|
+
const model = m.model ?? {};
|
|
322
|
+
const inst = st.install ?? {};
|
|
323
|
+
const svcInstalled = !!inst.serverFound;
|
|
324
|
+
const svcOk = !!st.health?.healthy;
|
|
325
|
+
// 远程模式(endpoint 指向非本机):不引导本地安装,进已安装视图管理外部服务
|
|
326
|
+
const remoteConfigured = m.mode === 'remote' && !!m.endpoint && !isLocalHost(m.endpoint);
|
|
327
|
+
const showWizard = !svcInstalled && !svcOk && !remoteConfigured;
|
|
328
|
+
const state = deriveState(st, svcInstalled);
|
|
329
|
+
const meta = STATE_META[state] ?? STATE_META.error;
|
|
330
|
+
// 首装模型三件套就绪判定(安装流水线生成 ov.conf 的前置)
|
|
331
|
+
const modelSaved = !!(model.apiBase && model.vlmModel && model.embeddingModel);
|
|
332
|
+
|
|
333
|
+
if (showWizard) return renderWizard(el, { m, model, modelSaved });
|
|
334
|
+
return renderInstalled(el, { st, m, model, inst, state, meta, remoteConfigured });
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ============ 未安装向导 ============
|
|
338
|
+
|
|
339
|
+
function renderWizard(el, { m, model, modelSaved }) {
|
|
340
|
+
const epVal = (m.endpoint && m.endpoint !== DEFAULT_ENDPOINT) ? m.endpoint : '';
|
|
341
|
+
el.innerHTML = `
|
|
342
|
+
${pageIntro}
|
|
343
|
+
<div class="card">
|
|
344
|
+
<div style="display:flex;align-items:center;gap:10px">
|
|
345
|
+
<h3 style="margin:0">开始使用记忆增强</h3>
|
|
346
|
+
<span style="flex:1"></span>
|
|
347
|
+
<button class="btn primary sm" id="memInstall" ${modelSaved ? '' : 'disabled'} title="${modelSaved ? '自动完成安装与配置(检测/安装 uv → 服务 → 配置 → 拉起 → 插件)' : '请先在下方「模型配置」卡填写并保存(安装生成服务配置需要模型三件套)'}">⚡ 一键安装</button>
|
|
348
|
+
</div>
|
|
349
|
+
<div style="background:var(--primary-weak);border-radius:10px;padding:14px 16px">
|
|
350
|
+
<b>未安装 OpenViking 服务</b>
|
|
351
|
+
<div class="hint" style="margin-top:4px;line-height:1.7">点右上「⚡ 一键安装」自动完成安装与配置(检测/安装 uv 运行时 → 安装 OpenViking 服务 → 生成配置 → 拉起服务 → 接入记忆插件)。全程无须终端;已就绪部件自动跳过,可重复执行。</div>
|
|
352
|
+
${modelSaved ? '' : `<div class="hint" style="margin-top:8px;color:var(--warn-tx)">⚠ 请先在下方「模型配置」填表并保存——安装需要用模型三件套(API 端点 / 提炼模型 / 向量模型)生成服务配置。</div>`}
|
|
353
|
+
</div>
|
|
354
|
+
<div id="memInstallSteps">${installStepsHtml(IDLE_STEPS)}</div>
|
|
355
|
+
<div id="memInstallHint" class="hint" style="margin-top:8px"></div>
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
<div class="card">
|
|
359
|
+
<div style="display:flex;align-items:center;gap:10px">
|
|
360
|
+
<h3 style="margin:0">模型配置(安装前置)</h3>
|
|
361
|
+
<span style="flex:1"></span>
|
|
362
|
+
<button class="btn sm" id="memTest">测试连接</button>
|
|
363
|
+
<button class="btn sm primary" id="memSaveModel">保存配置</button>
|
|
364
|
+
</div>
|
|
365
|
+
<div class="desc" style="margin-top:6px">记忆的<b>提炼(vlm)与向量化(embedding)</b>走 OpenAI 协议端点,与机器人对话用的厂商档案相互独立。建议先 [测试连接] 双绿,再保存并一键安装。</div>
|
|
366
|
+
${modelFormHtml(model, m.endpoint)}
|
|
367
|
+
<div id="memTestResult" class="hint" style="margin-top:8px"></div>
|
|
368
|
+
<pre class="report" id="memTestReport" style="display:none;margin-top:6px"></pre>
|
|
369
|
+
</div>
|
|
370
|
+
|
|
371
|
+
<div class="card">
|
|
372
|
+
<details><summary class="hint">已有外部 OpenViking 服务?填写远程端点(不在本机安装)</summary>
|
|
373
|
+
<div style="margin-top:10px">
|
|
374
|
+
<div class="row" style="align-items:end">
|
|
375
|
+
<div><label>服务端点(OpenViking server)</label><input type="text" id="memRemoteEp" value="${esc(epVal)}" placeholder="http://192.168.x.x:1933"></div>
|
|
376
|
+
<div class="btn-wrap"><button class="btn" id="memRemoteSave">保存并切换</button></div>
|
|
377
|
+
</div>
|
|
378
|
+
<div class="hint" style="margin-top:6px">保存后本页改为管理该外部服务(探活 + 模型配置,不接管其进程)。</div>
|
|
379
|
+
</div>
|
|
380
|
+
</details>
|
|
381
|
+
<details style="margin-top:10px"><summary class="hint">高级:手动安装指引与常见问题(一键安装失败时的兜底路径)</summary>
|
|
382
|
+
<pre class="report" style="margin-top:8px">① 安装并启动 OpenViking 服务(本机):
|
|
383
|
+
uv tool install openviking
|
|
384
|
+
openviking-server init # 初始化配置(含模型)
|
|
385
|
+
openviking-server start
|
|
386
|
+
|
|
387
|
+
② 安装 Claude Code 记忆插件(一次性):
|
|
388
|
+
claude marketplace add openviking
|
|
389
|
+
claude plugin install openviking-memory@openviking
|
|
390
|
+
|
|
391
|
+
③ 回到本页:保存模型配置 → 一键安装(或直接开启)</pre>
|
|
392
|
+
<div class="hint" style="margin-top:8px;line-height:1.9">常见问题:
|
|
393
|
+
· 启动失败 / 服务异常 → 点「一键检查 / 修复」重跑流水线(已就绪步骤自动跳过,不覆盖现有数据)
|
|
394
|
+
· 鉴权失败 → ov.conf 的 root_api_key 与本页保存的 API Key 需一致(一键安装会自动对齐)
|
|
395
|
+
· 支持哪些模型 → 任意 OpenAI 协议端点(智谱 / 方舟 / 中转 / Ollama 本地)
|
|
396
|
+
· 修改端口 → 模型配置里的「服务端点」;一键安装会自动避开占用端口</div>
|
|
397
|
+
</details>
|
|
398
|
+
</div>`;
|
|
399
|
+
|
|
400
|
+
bindModelForm(true);
|
|
401
|
+
bindInstallActions(() => !modelSaved);
|
|
402
|
+
|
|
403
|
+
// 外部服务入口:保存远程 endpoint → remoteConfigured → 切到已安装(外部)视图
|
|
404
|
+
$('#memRemoteSave').onclick = async () => {
|
|
405
|
+
const val = $('#memRemoteEp').value.trim();
|
|
406
|
+
if (!/^https?:\/\//.test(val)) return toast('服务端点必须以 http(s):// 开头', true);
|
|
407
|
+
if (!S.doc.memory || typeof S.doc.memory !== 'object') S.doc.memory = { mode: 'remote', endpoint: val };
|
|
408
|
+
else { S.doc.memory.endpoint = val; if (!S.doc.memory.mode) S.doc.memory.mode = 'remote'; }
|
|
409
|
+
if (await saveDoc()) toast('已保存远程端点,切换为外部服务管理模式');
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ============ 已安装视图 ============
|
|
414
|
+
|
|
415
|
+
function renderInstalled(el, { st, m, model, inst, state, meta, remoteConfigured }) {
|
|
416
|
+
const ver = st.health?.version;
|
|
417
|
+
const endpoint = m.endpoint || DEFAULT_ENDPOINT;
|
|
418
|
+
const authNote = st.authNote;
|
|
419
|
+
const off = !m.enabled;
|
|
420
|
+
|
|
421
|
+
// 状态行主文案 + 补充说明(running 一行收尾;其余态独立说明行带 detail + 行动建议)
|
|
422
|
+
const statusSub = state === 'running' ? 'OpenViking 服务运行正常,记忆召回与归档已就绪'
|
|
423
|
+
: state === 'stopped' ? '服务已安装,进程未运行——点右上「▶ 启动服务」一键拉起(自动检查配置 → 拉起探活 → 接入插件)'
|
|
424
|
+
: '';
|
|
425
|
+
const statusDetail = state === 'running' || state === 'stopped' ? ''
|
|
426
|
+
: `${esc(st.health?.detail || '探活失败')}${REASON_HINT[st.health?.reason] ? `<br>➜ ${REASON_HINT[st.health.reason]}` : ''}`;
|
|
427
|
+
|
|
428
|
+
// 信息网格:只渲染「有值」的列(隐藏字段不占列位);运行方式用 mode 推导(embedded=LCB 受管安装)
|
|
429
|
+
// 网格固定 3 列(一行 3 项;auto-fit 会在宽屏挤成 6 列);地址列不跨列,长 URL 列内省略号截断防撑破;插件列值整体 nowrap 防拆行
|
|
430
|
+
const cells = [
|
|
431
|
+
infoCell('globe', '服务地址', `<span style="white-space:nowrap;display:inline-flex;align-items:center;gap:6px;min-width:0;max-width:100%"><code style="font-size:11.5px;min-width:0;overflow:hidden;text-overflow:ellipsis">${esc(endpoint)}</code><button class="btn sm" id="memCopy" title="复制服务地址" style="padding:2px 9px;flex:0 0 auto">⧉</button>${remoteConfigured ? '<span class="hint">远程</span>' : ''}</span>`),
|
|
432
|
+
...(state === 'running' && ver ? [infoCell('box', '版本', `v${esc(ver)}`)] : []),
|
|
433
|
+
infoCell('server', '运行方式', m.mode === 'embedded'
|
|
434
|
+
? `LCB 受管${inst.managedRunning ? '' : ' <span class="hint">(未运行)</span>'}`
|
|
435
|
+
: `外部自管 <span class="hint">非 LCB 拉起</span>`),
|
|
436
|
+
...(inst.managedSince ? [infoCell('clock', '运行时间', esc(fmtSince(inst.managedSince)))] : []),
|
|
437
|
+
...(st.health?.authMode ? [infoCell('server', '鉴权模式', `${esc(st.health.authMode)}${authNote ? ` <span class="hint" title="${esc(authNote)}">ⓘ</span>` : ''}`)] : []),
|
|
438
|
+
infoCell('spark', '记忆插件', (st.plugin?.found
|
|
439
|
+
? `<span style="white-space:nowrap;display:inline-flex;align-items:center;gap:6px"><span style="color:var(--ok-tx);font-weight:600">✓</span><code>${esc(st.plugin.name)}</code>${st.plugin.version ? `<span class="hint">v${esc(st.plugin.version)}</span>` : ''}${st.plugin.source && PLUGIN_SOURCE_LABEL[st.plugin.source] ? `<span class="hint" title="实际加载渠道:与机器人任务装配一致">${esc(PLUGIN_SOURCE_LABEL[st.plugin.source])}</span>` : ''}</span>`
|
|
440
|
+
: '<span style="color:var(--warn-tx);font-weight:600">○ 未安装</span><span class="hint">「检查修复」可补装</span>') + clientConfBadge(st.plugin)),
|
|
441
|
+
];
|
|
442
|
+
|
|
443
|
+
// 卡标题右按钮组:主行动随状态切换(启动/停止),刷新与检查修复常驻
|
|
444
|
+
// 检查/修复门禁与向导一键安装同口径:受管 conf 缺失(= 重跑会走首装生成 ov.conf)且模型
|
|
445
|
+
// 三件套未保存时禁用——把「缺模型」挡在点击之前,而不是让用户在「生成服务配置」步骤撞失败
|
|
446
|
+
const needModelFirst = !inst.confFound && !modelSaved;
|
|
447
|
+
const repairBtn = `<button class="btn sm" id="memInstall" title="${needModelFirst
|
|
448
|
+
? '受管服务配置缺失,生成配置需要模型三件套——请先在下方「模型配置」卡填写并保存'
|
|
449
|
+
: '重跑安装流水线(已就绪部件自动跳过)'}">检查修复</button>`;
|
|
450
|
+
const refreshBtn = '<button class="btn sm" id="memRefresh" title="重新探活">↻ 刷新</button>';
|
|
451
|
+
const actionBtns = state === 'stopped'
|
|
452
|
+
? `${repairBtn} ${refreshBtn} <button class="btn sm primary" id="memStart">▶ 启动服务</button>`
|
|
453
|
+
: state === 'running'
|
|
454
|
+
? `${repairBtn} ${refreshBtn}${inst.managedRunning ? ' <button class="btn sm danger" id="memStop" title="停止 LCB 受管服务">⏹ 停止</button>' : ''}`
|
|
455
|
+
: `${repairBtn} ${refreshBtn}`;
|
|
456
|
+
|
|
457
|
+
el.innerHTML = `
|
|
458
|
+
${pageIntro}
|
|
459
|
+
<div class="card">
|
|
460
|
+
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap">
|
|
461
|
+
<label class="switch"><input type="checkbox" id="memEnabled" ${m.enabled ? 'checked' : ''}><span class="track"></span></label>
|
|
462
|
+
<span class="switch-label" style="font-weight:600">启用长期记忆</span>
|
|
463
|
+
<span style="flex:1"></span>
|
|
464
|
+
<span class="tag ${meta.cls}" title="${esc(shortPath(inst.serverBin || ''))}">${meta.label}${state === 'running' && ver ? ` · v${esc(ver)}` : ''}</span>
|
|
465
|
+
</div>
|
|
466
|
+
<div class="desc" style="margin:10px 0 0">关闭后 LCB 行为与未安装完全一致。</div>
|
|
467
|
+
</div>
|
|
468
|
+
${off ? '<div class="hint" style="margin:0 0 12px;padding:9px 14px;background:var(--warn-bg);color:var(--warn-tx);border-radius:10px">⏸ 记忆增强已停用——下方仅供查看,打开顶部开关后可管理服务与配置。</div><div class="block-disabled">' : ''}
|
|
469
|
+
<div class="card">
|
|
470
|
+
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
|
471
|
+
<h3 style="margin:0">服务状态</h3>
|
|
472
|
+
<span style="flex:1"></span>
|
|
473
|
+
${actionBtns}
|
|
474
|
+
</div>
|
|
475
|
+
<div style="background:${meta.bg};border-radius:12px;padding:16px 18px;margin-top:12px">
|
|
476
|
+
<div style="display:flex;align-items:center;gap:9px;flex-wrap:wrap">
|
|
477
|
+
<span style="width:9px;height:9px;border-radius:50%;background:var(--${meta.cls}-tx);flex:0 0 auto"></span>
|
|
478
|
+
<span style="font-size:15px;font-weight:700;color:var(--${meta.cls}-tx)">${meta.label}</span>
|
|
479
|
+
${statusSub ? `<span class="hint">— ${statusSub}</span>` : ''}
|
|
480
|
+
</div>
|
|
481
|
+
${statusDetail ? `<div class="hint" style="margin-top:9px;line-height:1.8">${statusDetail}</div>` : ''}
|
|
482
|
+
<div style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));grid-auto-flow:dense;gap:16px 22px;margin-top:16px">${cells.join('')}</div>
|
|
483
|
+
</div>
|
|
484
|
+
<div id="memInstallSteps"></div>
|
|
485
|
+
<div id="memInstallHint" class="hint" style="margin-top:8px"></div>
|
|
486
|
+
</div>
|
|
487
|
+
<div class="card">
|
|
488
|
+
<div style="display:flex;align-items:center;gap:10px">
|
|
489
|
+
<h3 style="margin:0">模型配置</h3>
|
|
490
|
+
<span style="flex:1"></span>
|
|
491
|
+
<button class="btn sm" id="memTest">测试连接</button>
|
|
492
|
+
<button class="btn sm primary" id="memSaveModel">保存配置</button>
|
|
493
|
+
</div>
|
|
494
|
+
<div class="desc" style="margin-top:6px">记忆的<b>提炼(vlm)与向量化(embedding)</b>走 OpenAI 协议端点,与机器人对话用的厂商档案相互独立(改档案不影响记忆)。建议变更后先 [测试连接] 双绿再保存。</div>
|
|
495
|
+
${modelFormHtml(model, m.endpoint)}
|
|
496
|
+
<div id="memTestResult" class="hint" style="margin-top:8px"></div>
|
|
497
|
+
<pre class="report" id="memTestReport" style="display:none;margin-top:6px"></pre>
|
|
498
|
+
</div>
|
|
499
|
+
${off ? '</div>' : ''}`;
|
|
500
|
+
|
|
501
|
+
// 总开关:即时保存;开启时确认 + 预检(服务/插件状态给出真实提示,指引指向服务状态卡)
|
|
502
|
+
$('#memEnabled').onchange = async (e) => {
|
|
503
|
+
const next = e.target.checked;
|
|
504
|
+
if (next) {
|
|
505
|
+
const blockers = [];
|
|
506
|
+
if (!st.health?.healthy) {
|
|
507
|
+
blockers.push(`OpenViking 服务不可达:${esc(st.health?.detail || '未知原因')}${state === 'stopped' ? '——开启后可在「服务状态」卡点「▶ 启动服务」拉起' : ''}`);
|
|
508
|
+
}
|
|
509
|
+
if (!st.plugin?.found) blockers.push('记忆插件未安装——开启后可在「服务状态」卡点「一键检查 / 修复」补装');
|
|
510
|
+
const warnTxt = blockers.length
|
|
511
|
+
? `开启前请注意:<br>· ${blockers.join('<br>· ')}<br><br>仍可开启(保存配置,日志会在每条消息给出指引),确认继续?`
|
|
512
|
+
: '开启后每个机器人的每条消息将自动:注入记忆召回(hooks)+ 按命名空间隔离读写。确认开启?';
|
|
513
|
+
if (!(await confirmDialog({ title: '启用长期记忆', message: warnTxt, confirmText: '开启' }))) {
|
|
514
|
+
e.target.checked = false;
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
const old = (S.doc.memory && typeof S.doc.memory === 'object') ? S.doc.memory.enabled : undefined;
|
|
519
|
+
if (!S.doc.memory || typeof S.doc.memory !== 'object') {
|
|
520
|
+
S.doc.memory = { enabled: next, mode: 'remote', endpoint: DEFAULT_ENDPOINT };
|
|
521
|
+
} else {
|
|
522
|
+
S.doc.memory.enabled = next;
|
|
523
|
+
}
|
|
524
|
+
if (!await saveDoc()) {
|
|
525
|
+
if (old === undefined) delete S.doc.memory;
|
|
526
|
+
else S.doc.memory.enabled = old;
|
|
527
|
+
e.target.checked = !next;
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
// 刷新:整页重渲染(重新探活)
|
|
532
|
+
const refreshBtnEl = $('#memRefresh');
|
|
533
|
+
if (refreshBtnEl) refreshBtnEl.onclick = () => hooks.rerender();
|
|
534
|
+
|
|
535
|
+
// 复制服务地址
|
|
536
|
+
const copyBtn = $('#memCopy');
|
|
537
|
+
if (copyBtn) copyBtn.onclick = async () => {
|
|
538
|
+
try { await navigator.clipboard.writeText(endpoint); toast('服务地址已复制'); }
|
|
539
|
+
catch { toast('复制失败(浏览器未授权剪贴板)', true); }
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
// 启动服务已统一走 bindInstallActions 的启动流水线(检查 → 拉起探活 → 接入插件,stepper 实时进度)
|
|
543
|
+
|
|
544
|
+
// 停止受管服务(危险确认;外部自管服务不显示此按钮)
|
|
545
|
+
const stopBtn = $('#memStop');
|
|
546
|
+
if (stopBtn) stopBtn.onclick = async () => {
|
|
547
|
+
if (!(await confirmDialog({ title: '停止受管服务', message: '停止 LCB 托管的 OpenViking 服务?记忆功能将不可用(独立手动安装的服务不受影响,可随时重新拉起)。', confirmText: '停止' }))) return;
|
|
548
|
+
try { await api('POST', '/api/memory/stop', {}); toast('已停止'); hooks.rerender(); } catch (e) { toast(e.message, true); }
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
bindModelForm(false);
|
|
552
|
+
// 已安装视图:检查/修复门禁——受管 conf 已存在时放行(沿用现值重跑);缺失时与向导同口径要模型三件套
|
|
553
|
+
bindInstallActions(() => needModelFirst);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export const page = {
|
|
557
|
+
id: 'memory',
|
|
558
|
+
title: '记忆增强',
|
|
559
|
+
icon: '<ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v14a9 3 0 0 0 18 0V5"/><path d="M3 12a9 3 0 0 0 18 0"/>',
|
|
560
|
+
render: renderMemory,
|
|
561
|
+
};
|