@jesonliu/lark-claudecode-bridge 1.4.0 → 1.4.1
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/assets/web/js/pages/memory.js +430 -102
- package/dist/config.js +12 -0
- package/dist/config.js.map +1 -1
- package/dist/gateway/card-builder.js +3 -2
- package/dist/gateway/card-builder.js.map +1 -1
- package/dist/gateway/progress-card.d.ts +6 -2
- package/dist/gateway/progress-card.js +8 -4
- package/dist/gateway/progress-card.js.map +1 -1
- package/dist/index.js +14 -35
- package/dist/index.js.map +1 -1
- package/dist/memory-installer.d.ts +21 -2
- package/dist/memory-installer.js +190 -62
- package/dist/memory-installer.js.map +1 -1
- package/dist/types.d.ts +17 -3
- package/dist/util/file-tracker.d.ts +2 -7
- package/dist/util/file-tracker.js +2 -24
- package/dist/util/file-tracker.js.map +1 -1
- package/dist/web/config-api.js +15 -8
- package/dist/web/config-api.js.map +1 -1
- package/dist/web/memory-api.d.ts +10 -2
- package/dist/web/memory-api.js +21 -4
- package/dist/web/memory-api.js.map +1 -1
- package/dist/web/memory-users-api.d.ts +76 -0
- package/dist/web/memory-users-api.js +461 -0
- package/dist/web/memory-users-api.js.map +1 -0
- package/dist/web/server.js +78 -5
- package/dist/web/server.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
// ============ 记忆增强(OpenViking 长期记忆,生命周期方案 §6.7/§6.8/§6.9) ============
|
|
2
|
-
//
|
|
3
|
-
// -
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
1
|
+
// ============ 记忆增强(OpenViking 长期记忆,生命周期方案 §6.7/§6.8/§6.9 + 最终方案 v1.1) ============
|
|
2
|
+
// 页内双 Tab(最终方案 §六:「服务与配置」|「用户与密钥」,形态同配置页分栏;侧边栏不加一级导航):
|
|
3
|
+
// - Tab1 服务与配置:双视图(未安装向导 / 已安装管理)——总开关 + 服务状态(五步流水线 stepper
|
|
4
|
+
// + 启停/检查修复/刷新)+ 模型配置(P2 双端点:vlm 块与 embedding 块各自独立端点/Key)。
|
|
5
|
+
// P1 保存即生效:模型变更保存后弹「立即生效」确认 → /api/memory/apply 重渲染 ov.conf + 重启服务。
|
|
6
|
+
// - Tab2 用户与密钥:一键初始化 / 用户列表(web / 机器人) / 打开记忆空间(密钥进剪贴板 + 自动
|
|
7
|
+
// 新标签开 1933,Ctrl+V 一次连接) / 复制·重置·删除(重置删除均二次确认,删除先备份)。
|
|
8
|
+
// api_key 模式但 web 用户未建时 Tab 亮红点「待初始化鉴权」(前置化浏览器 401 重定向类问题)。
|
|
7
9
|
// 配置读写走标准 doc 管道(saveDoc → PUT /api/config,secret 自动回填);测试连接走
|
|
8
10
|
// /api/memory/test-connection(表单现值,不落盘);安装/启停走 /api/memory/install|start|stop。
|
|
9
|
-
// 安装回写会改 config.yaml——完成后走 refresh()(重拉 S.doc 再渲染),避免陈旧 doc 覆盖回写。
|
|
10
11
|
import { S, $, esc, api, toast, saveDoc, hooks, refresh } from '../core.js';
|
|
11
12
|
import { confirmDialog } from '../ui.js';
|
|
12
13
|
|
|
13
|
-
/** 服务商预设(§6.8
|
|
14
|
+
/** 服务商预设(§6.8 填表助手:只预填已知值,全部可见可改)。预设只填主块(vlm);
|
|
15
|
+
* 向量块留空沿用主块——跨厂商组合时单独填向量端点/Key(P2 双端点拆分语义) */
|
|
14
16
|
const PRESETS = {
|
|
15
17
|
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
18
|
volcengine: { label: '火山方舟', api_base: 'https://ark.cn-beijing.volces.com/api/v3', vlm_model: '', embedding_model: '', key_hint: '方舟 API Key;模型名填推理接入点 EP ID' },
|
|
@@ -22,7 +24,7 @@ const DOC_URL = 'https://github.com/volcengine/OpenViking';
|
|
|
22
24
|
|
|
23
25
|
/** 探活失败分类 → 下一步行动建议(后端 reason 驱动;not-running 由状态机接管,不在此列) */
|
|
24
26
|
const REASON_HINT = {
|
|
25
|
-
auth: '
|
|
27
|
+
auth: '鉴权失败——api_key 模式下网页/机器人各需一把用户密钥。点「用户与密钥」Tab 一键初始化,或检查 ov.conf 的 root key 与本页保存的 API Key 是否一致',
|
|
26
28
|
timeout: '服务无响应——进程可能卡死,可「一键检查 / 修复」重拉',
|
|
27
29
|
unreachable: '端点不可达——检查服务端点地址与网络(远程模式常见),或服务是否监听该地址',
|
|
28
30
|
'http-error': '服务返回异常状态码——可「一键检查 / 修复」重拉,或查看服务日志定位',
|
|
@@ -78,12 +80,39 @@ const infoCell = (icon, label, valueHtml, span = 1) =>
|
|
|
78
80
|
<div style="font-size:13px;display:flex;align-items:center;gap:6px;flex-wrap:wrap;line-height:1.5">${valueHtml}</div>
|
|
79
81
|
</div>`;
|
|
80
82
|
|
|
81
|
-
/**
|
|
83
|
+
/** 页头说明行(卡片外,双 Tab 共用):一句话定位 + 文档外链 */
|
|
82
84
|
const pageIntro = `<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;margin:0 0 14px">
|
|
83
85
|
<span class="hint" style="font-size:12.5px">OpenViking 长期记忆:每条消息自动召回相关历史、会话归档自动提炼,按 <code>{appId}-{userId}</code> 命名空间隔离(机器人之间互不可见)。</span>
|
|
84
86
|
<!--<a href="${DOC_URL}" target="_blank" rel="noreferrer" style="font-size:12.5px;flex:0 0 auto;margin-left:auto">查看文档 ↗</a>-->
|
|
85
87
|
</div>`;
|
|
86
88
|
|
|
89
|
+
// ============ 页内双 Tab(最终方案 §六:「服务与配置」|「用户与密钥」) ============
|
|
90
|
+
|
|
91
|
+
/** 当前 Tab(模块级:rerender 保持);用户快照(红点判定 + Tab2 渲染数据) */
|
|
92
|
+
let activeTab = 'service';
|
|
93
|
+
let usersSnapshot = null;
|
|
94
|
+
|
|
95
|
+
/** 切 Tab(重渲染整个记忆页——S.doc/状态已在内存,切换零请求) */
|
|
96
|
+
function switchTab(tab) {
|
|
97
|
+
activeTab = tab;
|
|
98
|
+
hooks.rerender();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Tab 条:与「权限」页同款标准 .tabs 组件(下划线高亮,main.css 统一样式);待初始化时 Tab2 亮红点 */
|
|
102
|
+
function tabBarHtml(authInitNeeded) {
|
|
103
|
+
const dot = ' <span style="display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--danger);vertical-align:2px" title="待初始化鉴权:服务为 api_key 模式但网页端用户未创建"></span>';
|
|
104
|
+
return `<div class="tabs">
|
|
105
|
+
<button data-memtab="service" class="${activeTab === 'service' ? 'active' : ''}">服务与配置</button>
|
|
106
|
+
<button data-memtab="users" class="${activeTab === 'users' ? 'active' : ''}">用户与密钥${authInitNeeded ? dot : ''}</button>
|
|
107
|
+
</div>`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function bindTabBar() {
|
|
111
|
+
document.querySelectorAll('[data-memtab]').forEach((b) => {
|
|
112
|
+
b.onclick = () => switchTab(b.dataset.memtab);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
87
116
|
// ============ 服务状态八态(「一概而论异常」→ 精确状态;探活 reason 全量映射) ============
|
|
88
117
|
|
|
89
118
|
function deriveState(st, svcInstalled) {
|
|
@@ -136,34 +165,54 @@ function clientConfBadge(p) {
|
|
|
136
165
|
return label ? ` <span class="hint" title="插件侧启用判定:env 优先,客户端配置兜底(${esc(p.clientConfPath || '')})">${esc(label)}</span>` : '';
|
|
137
166
|
}
|
|
138
167
|
|
|
139
|
-
// ============ 模型表单(双视图共用:渲染 +
|
|
168
|
+
// ============ 模型表单(双视图共用:渲染 + 绑定;P2 双端点拆分) ============
|
|
140
169
|
|
|
141
170
|
function modelFormHtml(model, endpoint) {
|
|
142
171
|
const secretVal = (v) => (v && typeof v === 'object' && v.secretSet) ? v.secretHint : '';
|
|
172
|
+
// 向量块独立凭证展示:own=显式配置(输入框脱敏提示「留空不修改」);未配置=沿用主块(提示现沿用值)
|
|
173
|
+
const embOwn = !!model.embeddingKeyOwn;
|
|
174
|
+
const embKeyHint = secretVal(model.embeddingApiKey);
|
|
175
|
+
const embKeyPlaceholder = embOwn
|
|
176
|
+
? `已保存 ${embKeyHint || '••••'},留空不修改`
|
|
177
|
+
: embKeyHint ? `当前沿用主 Key ${embKeyHint},填写后独立` : '留空沿用主 Key(可单独指定)';
|
|
178
|
+
const embBasePlaceholder = model.embeddingApiBaseResolved || '留空沿用主端点';
|
|
179
|
+
// 左右两栏面板(提炼主模型 | 向量模型):用「权限」页同款 row-2col 响应式栅格,窄屏自动上下叠放
|
|
180
|
+
const panel = (title, hint, inner) => `
|
|
181
|
+
<div style="border:1px solid var(--border);border-radius:10px;padding:14px 16px 4px;background:var(--field);min-width:0">
|
|
182
|
+
<div style="font-size:13px;font-weight:700;margin-bottom:12px">${title}
|
|
183
|
+
<span class="hint" style="font-weight:400;margin-left:6px">${hint}</span></div>
|
|
184
|
+
${inner}
|
|
185
|
+
</div>`;
|
|
186
|
+
const fld = (label, inner, hint) => `
|
|
187
|
+
<div style="margin-bottom:12px"><label>${label}</label>${inner}
|
|
188
|
+
${hint ? `<div class="hint" style="margin-top:3px">${hint}</div>` : ''}</div>`;
|
|
143
189
|
return `
|
|
144
|
-
<div class="
|
|
145
|
-
|
|
146
|
-
|
|
190
|
+
<div class="row-2col" style="gap:16px;align-items:stretch">
|
|
191
|
+
${panel('提炼主模型配置', 'vlm · 会话归档时提炼记忆', `
|
|
192
|
+
${fld('服务商预设(填表助手,值可改)', `<select id="memProvider">
|
|
147
193
|
<option value="">— 选择预设 —</option>
|
|
148
194
|
${Object.entries(PRESETS).map(([k, p]) => `<option value="${k}" ${model.provider === k ? 'selected' : ''}>${p.label}</option>`).join('')}
|
|
149
|
-
</select
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
195
|
+
</select>`, '选择后只预填空字段,已填值不覆盖(同时预填右侧向量模型名)')}
|
|
196
|
+
${fld('API 端点(api_base)', `<input type="text" id="memApiBase" value="${esc(model.apiBase || '')}" placeholder="https://…/v1">`, 'OpenAI 协议端点,与机器人对话用的厂商档案相互独立')}
|
|
197
|
+
${fld(`API Key ${secretVal(model.apiKey) ? `<span class="hint">已保存 ${esc(secretVal(model.apiKey))},留空不修改</span>` : ''}`, `<input type="password" id="memApiKey" value="" placeholder="${esc(PRESETS[model.provider]?.key_hint || 'sk-…(本地端点可留空)')}">`)}
|
|
198
|
+
${fld('提炼模型(vlm)', `<input type="text" id="memVlm" value="${esc(model.vlmModel || '')}" placeholder="glm-4.7">`, '归档会话时提炼记忆的对话模型,可选更强档位')}
|
|
199
|
+
`)}
|
|
200
|
+
${panel('向量模型配置', 'embedding · 向量化入库与召回', `
|
|
201
|
+
${fld('向量模型(embedding)', `<input type="text" id="memEmb" value="${esc(model.embeddingModel || '')}" placeholder="embedding-3">`, '测试连接会真调一次验证')}
|
|
202
|
+
${fld('向量端点(独立)', `<input type="text" id="memEmbBase" value="${esc(model.embeddingApiBase || '')}" placeholder="${esc(embBasePlaceholder)}">`, '跨厂商组合时单独指定(如主模型=glm + 向量=OpenAI 系);留空沿用主端点')}
|
|
203
|
+
${fld('向量 Key(独立)', `<input type="password" id="memEmbKey" value="" placeholder="${esc(embKeyPlaceholder)}">`, '与主 Key 相同时留空即可;两块凭证互不影响(留空 = 不修改)')}
|
|
204
|
+
`)}
|
|
205
|
+
</div>
|
|
206
|
+
<div class="row" style="margin-top:12px;align-items:end">
|
|
207
|
+
<div style="flex:1;min-width:260px"><label>服务端点(OpenViking server)</label><input type="text" id="memEndpoint" value="${esc(endpoint || DEFAULT_ENDPOINT)}" placeholder="${DEFAULT_ENDPOINT}">
|
|
160
208
|
<div class="hint" style="margin-top:3px">一般无须修改:一键安装自动回写;远程模式填外部服务地址</div></div>
|
|
161
209
|
</div>`;
|
|
162
210
|
}
|
|
163
211
|
|
|
164
|
-
/** 绑定模型表单交互(预设联动 / 测试连接双绿 /
|
|
165
|
-
|
|
166
|
-
|
|
212
|
+
/** 绑定模型表单交互(预设联动 / 测试连接双绿 / 保存 + P1 立即生效)。isWizard 只影响保存后的
|
|
213
|
+
* 提示文案;canApply(已安装视图)= 保存后检测到模型变更时提供「立即生效」 */
|
|
214
|
+
function bindModelForm(isWizard, canApply) {
|
|
215
|
+
// 预设联动:只预填主块空字段(已手填的值不覆盖;向量块留空沿用——跨厂商组合才手填)
|
|
167
216
|
$('#memProvider').onchange = (e) => {
|
|
168
217
|
const p = PRESETS[e.target.value];
|
|
169
218
|
if (!p) return;
|
|
@@ -172,7 +221,7 @@ function bindModelForm(isWizard) {
|
|
|
172
221
|
if (!$('#memEmb').value.trim()) $('#memEmb').value = p.embedding_model;
|
|
173
222
|
$('#memApiKey').placeholder = p.key_hint || 'sk-…';
|
|
174
223
|
};
|
|
175
|
-
//
|
|
224
|
+
// 测试连接:表单现值直测(不落盘)——向量块留空回退主块现值,与渲染 ov.conf 同口径
|
|
176
225
|
$('#memTest').onclick = async () => {
|
|
177
226
|
const btn = $('#memTest');
|
|
178
227
|
btn.disabled = true;
|
|
@@ -184,6 +233,8 @@ function bindModelForm(isWizard) {
|
|
|
184
233
|
api_key: $('#memApiKey').value.trim(),
|
|
185
234
|
vlm_model: $('#memVlm').value.trim(),
|
|
186
235
|
embedding_model: $('#memEmb').value.trim(),
|
|
236
|
+
embedding_api_base: $('#memEmbBase').value.trim(),
|
|
237
|
+
embedding_api_key: $('#memEmbKey').value.trim(),
|
|
187
238
|
});
|
|
188
239
|
const line = (name, c) => `${c.ok ? '✅' : '❌'} ${name}:${c.detail}`;
|
|
189
240
|
$('#memTestResult').innerHTML = r.ok
|
|
@@ -196,24 +247,42 @@ function bindModelForm(isWizard) {
|
|
|
196
247
|
}
|
|
197
248
|
btn.disabled = false;
|
|
198
249
|
};
|
|
199
|
-
// 保存模型配置:写入 S.doc.memory(secret 空值保持脱敏对象 → 后端 applySecrets
|
|
250
|
+
// 保存模型配置:写入 S.doc.memory(secret 空值保持脱敏对象 → 后端 applySecrets 按块独立回填)。
|
|
251
|
+
// P1 保存即生效:已安装视图检测到模型实际变更 → 弹「立即生效」确认 → apply 流水线
|
|
252
|
+
//(重渲染 ov.conf + 强制重启受管服务)——消灭「保存后还需手动检查修复」两步坑
|
|
200
253
|
$('#memSaveModel').onclick = async () => {
|
|
201
254
|
if (!S.doc.memory || typeof S.doc.memory !== 'object') S.doc.memory = {};
|
|
202
255
|
const mem = S.doc.memory;
|
|
203
|
-
mem.
|
|
204
|
-
|
|
256
|
+
const prev = (mem.model && typeof mem.model === 'object') ? mem.model : {};
|
|
257
|
+
// 变更检测须在改 S.doc 前取原值(api_key 输入非空视为换 Key)
|
|
258
|
+
const newApiBase = $('#memApiBase').value.trim();
|
|
259
|
+
const newVlm = $('#memVlm').value.trim();
|
|
260
|
+
const newEmb = $('#memEmb').value.trim();
|
|
261
|
+
const newEmbBase = $('#memEmbBase').value.trim();
|
|
262
|
+
const rawKey = $('#memApiKey').value.trim();
|
|
263
|
+
const rawEmbKey = $('#memEmbKey').value.trim();
|
|
205
264
|
const pv = $('#memProvider').value;
|
|
265
|
+
const modelChanged = newApiBase !== (prev.api_base || '')
|
|
266
|
+
|| newVlm !== (prev.vlm_model || '')
|
|
267
|
+
|| newEmb !== (prev.embedding_model || '')
|
|
268
|
+
|| newEmbBase !== (prev.embedding_api_base || '')
|
|
269
|
+
|| (pv || '') !== (prev.provider || '')
|
|
270
|
+
|| !!rawKey || !!rawEmbKey;
|
|
271
|
+
mem.endpoint = $('#memEndpoint').value.trim() || DEFAULT_ENDPOINT;
|
|
206
272
|
const next = {
|
|
207
|
-
...
|
|
273
|
+
...prev,
|
|
208
274
|
...(pv ? { provider: pv } : {}),
|
|
209
|
-
api_base:
|
|
210
|
-
vlm_model:
|
|
211
|
-
embedding_model:
|
|
275
|
+
api_base: newApiBase,
|
|
276
|
+
vlm_model: newVlm,
|
|
277
|
+
embedding_model: newEmb,
|
|
212
278
|
};
|
|
213
279
|
if (!pv) delete next.provider;
|
|
280
|
+
// 向量端点(非凭证):留空 = 清除 → 沿用主端点(与输入框提示一致)
|
|
281
|
+
if (!newEmbBase) delete next.embedding_api_base;
|
|
282
|
+
else next.embedding_api_base = newEmbBase;
|
|
214
283
|
mem.model = next;
|
|
215
|
-
|
|
216
|
-
if (
|
|
284
|
+
if (rawKey) mem.model.api_key = rawKey; // Key 空值保留脱敏对象 → applySecrets 回填主 Key(留空=不修改)
|
|
285
|
+
if (rawEmbKey) mem.model.embedding_api_key = rawEmbKey; // 向量 Key 按块独立,同款回填
|
|
217
286
|
if (!await saveDoc()) return;
|
|
218
287
|
if (isWizard) {
|
|
219
288
|
// 保存即解锁「⚡ 一键安装」:重渲染让 modelSaved 重新计算(不让用户对着灰按钮困惑到刷新)
|
|
@@ -221,14 +290,29 @@ function bindModelForm(isWizard) {
|
|
|
221
290
|
hooks.rerender();
|
|
222
291
|
return;
|
|
223
292
|
}
|
|
293
|
+
if (modelChanged && canApply) {
|
|
294
|
+
const ok = await confirmDialog({
|
|
295
|
+
title: '使新配置立即生效',
|
|
296
|
+
message: '模型配置已保存。<br><br>记忆服务需<b>重新渲染配置并重启</b>才能应用新模型(运行中的进程不会热加载)。<br>立即生效?',
|
|
297
|
+
confirmText: '立即生效',
|
|
298
|
+
});
|
|
299
|
+
if (ok) {
|
|
300
|
+
await startPipeline('/api/memory/apply', '正在应用新模型配置(重渲染 → 重启服务 → 校验插件),本页实时显示进度')();
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
toast('已保存。稍后点「检查修复」或再次保存时选择「立即生效」即可应用');
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
224
306
|
toast('记忆模型配置已保存(未开启记忆时仅保存,打开开关后生效)');
|
|
225
307
|
};
|
|
226
308
|
}
|
|
227
309
|
|
|
228
|
-
// ============
|
|
310
|
+
// ============ 安装流水线(双 Tab1 共用:水平 stepper + 轮询 + 进页恢复) ============
|
|
229
311
|
|
|
230
312
|
/** 模块级轮询句柄:rerender 不产生重复 interval(旧闭包继续刷新 DOM,gate 以最新渲染为准) */
|
|
231
313
|
let installTimer = null;
|
|
314
|
+
/** 已安装视图的门禁(受管 conf 缺失且模型未保存时禁「检查修复」);向导视图 = 模型未保存禁安装 */
|
|
315
|
+
let installGateFn = () => false;
|
|
232
316
|
|
|
233
317
|
function installStepsHtml(steps) {
|
|
234
318
|
const chip = (s, i) => {
|
|
@@ -250,49 +334,58 @@ function installStepsHtml(steps) {
|
|
|
250
334
|
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
335
|
}
|
|
252
336
|
|
|
253
|
-
/**
|
|
254
|
-
*
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
:
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
337
|
+
/** 安装状态渲染(P0:慢启动时「拉起/拉起受管服务」步骤保持 running 并实时显示「已等 N 秒」,
|
|
338
|
+
* 不出现红叉——后端转后台续等,就绪自动续跑后续步骤) */
|
|
339
|
+
function renderInstallState(ist) {
|
|
340
|
+
const box = $('#memInstallSteps');
|
|
341
|
+
if (box) box.innerHTML = installStepsHtml(ist.steps);
|
|
342
|
+
const failed = ist.steps.find((s) => s.status === 'failed');
|
|
343
|
+
const hintEl = $('#memInstallHint');
|
|
344
|
+
const kindTxt = ist.kind === 'start' ? '启动流水线' : ist.steps.some((s) => s.detail && s.detail.includes('重渲染')) ? '应用流水线' : '安装流水线';
|
|
345
|
+
if (hintEl) hintEl.innerHTML = ist.running ? `${kindTxt}执行中…`
|
|
346
|
+
: failed ? `<span style="color:var(--warn-tx)">「${esc(failed.title)}」失败:${esc(failed.detail || '')}——修正后可重跑(已就绪步骤自动跳过)</span>`
|
|
347
|
+
: ist.finishedAt ? (ist.kind === 'start'
|
|
348
|
+
? '<span class="tag ok">启动完成</span> 服务已就绪、记忆插件已接入——LCB 接管的完整记忆链路即刻生效'
|
|
349
|
+
: '<span class="tag ok">全部就绪</span> 配置已回写(endpoint / mode=embedded),机器人记忆即刻生效')
|
|
350
|
+
: '';
|
|
351
|
+
const btn = $('#memInstall');
|
|
352
|
+
if (btn) btn.disabled = !!ist.running || installGateFn();
|
|
353
|
+
['memStart', 'memStop', 'memRefresh'].forEach((id) => { const b = $('#' + id); if (b) b.disabled = !!ist.running; });
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const pollInstall = async () => {
|
|
357
|
+
try {
|
|
358
|
+
const ist = await api('GET', '/api/memory/install/status');
|
|
359
|
+
renderInstallState(ist);
|
|
360
|
+
if (!ist.running) {
|
|
361
|
+
if (installTimer) { clearInterval(installTimer); installTimer = null; }
|
|
362
|
+
// 回写改了 config.yaml:refresh 重拉 S.doc 再整页渲染(防止陈旧 doc 在下次保存时覆盖回写)
|
|
363
|
+
refresh().catch(() => hooks.rerender());
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
} catch { /* 轮询失败静默重试 */ }
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
/** 发起流水线(安装 / 启动 / P1 立即生效共用):POST + toast + 轮询 */
|
|
370
|
+
function startPipeline(path, runningMsg) {
|
|
371
|
+
return async () => {
|
|
287
372
|
try {
|
|
288
373
|
await api('POST', path, {});
|
|
289
374
|
toast(runningMsg);
|
|
290
|
-
if (!installTimer) installTimer = setInterval(
|
|
291
|
-
|
|
375
|
+
if (!installTimer) installTimer = setInterval(pollInstall, 2000);
|
|
376
|
+
pollInstall();
|
|
292
377
|
} catch (e) { toast(e.message, true); }
|
|
293
378
|
};
|
|
294
|
-
|
|
295
|
-
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* 绑定安装按钮 + 轮询 + 进页恢复。gate() = 额外禁用条件(向导:模型未保存时禁装——
|
|
383
|
+
* 首装生成 ov.conf 需要模型三件套;已安装视图的「检查/修复」不受此限)。
|
|
384
|
+
*/
|
|
385
|
+
function bindInstallActions(gate) {
|
|
386
|
+
installGateFn = gate;
|
|
387
|
+
const installBtn = $('#memInstall');
|
|
388
|
+
if (installBtn) installBtn.onclick = startPipeline('/api/memory/install', '安装流水线已启动(后台执行,本页实时显示进度)');
|
|
296
389
|
// 启动服务:LCB 接管式启动流水线(检查 → 拉起探活 → 接入插件),同款 stepper 实时进度——
|
|
297
390
|
// 结束即完整接管态,无须再点「检查修复」补插件
|
|
298
391
|
const startBtn = $('#memStart');
|
|
@@ -300,8 +393,8 @@ function bindInstallActions(gate) {
|
|
|
300
393
|
// 进页恢复(切页回来 / 安装刚结束):在跑则继续轮询,刚结束则展示结果
|
|
301
394
|
api('GET', '/api/memory/install/status').then((ist) => {
|
|
302
395
|
if (ist.running || ist.finishedAt) {
|
|
303
|
-
|
|
304
|
-
if (ist.running && !installTimer) installTimer = setInterval(
|
|
396
|
+
renderInstallState(ist);
|
|
397
|
+
if (ist.running && !installTimer) installTimer = setInterval(pollInstall, 2000);
|
|
305
398
|
}
|
|
306
399
|
}).catch(() => {});
|
|
307
400
|
}
|
|
@@ -317,6 +410,9 @@ export async function renderMemory(el) {
|
|
|
317
410
|
el.innerHTML = `<div class="card"><h3>记忆增强</h3><div class="hint">状态读取失败:${esc(e.message)}</div></div>`;
|
|
318
411
|
return;
|
|
319
412
|
}
|
|
413
|
+
// 用户快照(Tab2 数据 + 红点判定);失败按 null(Tab2 内可重试),不阻塞 Tab1
|
|
414
|
+
try { usersSnapshot = await api('GET', '/api/memory/users'); } catch { usersSnapshot = null; }
|
|
415
|
+
|
|
320
416
|
const m = st.memory ?? { enabled: false, mode: 'remote', endpoint: DEFAULT_ENDPOINT };
|
|
321
417
|
const model = m.model ?? {};
|
|
322
418
|
const inst = st.install ?? {};
|
|
@@ -325,21 +421,21 @@ export async function renderMemory(el) {
|
|
|
325
421
|
// 远程模式(endpoint 指向非本机):不引导本地安装,进已安装视图管理外部服务
|
|
326
422
|
const remoteConfigured = m.mode === 'remote' && !!m.endpoint && !isLocalHost(m.endpoint);
|
|
327
423
|
const showWizard = !svcInstalled && !svcOk && !remoteConfigured;
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
if (
|
|
334
|
-
return
|
|
424
|
+
const authInitNeeded = !!usersSnapshot?.authInitNeeded;
|
|
425
|
+
|
|
426
|
+
el.innerHTML = `${pageIntro}${tabBarHtml(authInitNeeded)}<div id="memTabBody"></div>`;
|
|
427
|
+
bindTabBar();
|
|
428
|
+
const body = $('#memTabBody');
|
|
429
|
+
if (activeTab === 'users') return renderUsersTab(body, { st, m });
|
|
430
|
+
if (showWizard) return renderWizard(body, { m, model, modelSaved: !!(model.apiBase && model.vlmModel && model.embeddingModel) });
|
|
431
|
+
return renderInstalled(body, { st, m, model, inst, state: deriveState(st, svcInstalled), meta: STATE_META[deriveState(st, svcInstalled)] ?? STATE_META.error, remoteConfigured, authInitNeeded });
|
|
335
432
|
}
|
|
336
433
|
|
|
337
|
-
// ============ 未安装向导 ============
|
|
434
|
+
// ============ Tab1 · 未安装向导 ============
|
|
338
435
|
|
|
339
436
|
function renderWizard(el, { m, model, modelSaved }) {
|
|
340
437
|
const epVal = (m.endpoint && m.endpoint !== DEFAULT_ENDPOINT) ? m.endpoint : '';
|
|
341
438
|
el.innerHTML = `
|
|
342
|
-
${pageIntro}
|
|
343
439
|
<div class="card">
|
|
344
440
|
<div style="display:flex;align-items:center;gap:10px">
|
|
345
441
|
<h3 style="margin:0">开始使用记忆增强</h3>
|
|
@@ -348,7 +444,7 @@ function renderWizard(el, { m, model, modelSaved }) {
|
|
|
348
444
|
</div>
|
|
349
445
|
<div style="background:var(--primary-weak);border-radius:10px;padding:14px 16px">
|
|
350
446
|
<b>未安装 OpenViking 服务</b>
|
|
351
|
-
<div class="hint" style="margin-top:4px;line-height:1.7">点右上「⚡ 一键安装」自动完成安装与配置(检测/安装 uv 运行时 → 安装 OpenViking 服务 → 生成配置 → 拉起服务 →
|
|
447
|
+
<div class="hint" style="margin-top:4px;line-height:1.7">点右上「⚡ 一键安装」自动完成安装与配置(检测/安装 uv 运行时 → 安装 OpenViking 服务 → 生成配置 → 拉起服务 → 接入记忆插件)。全程无须终端;已就绪部件自动跳过,可重复执行。安装完成后到「用户与密钥」Tab 一键初始化网页/机器人密钥。</div>
|
|
352
448
|
${modelSaved ? '' : `<div class="hint" style="margin-top:8px;color:var(--warn-tx)">⚠ 请先在下方「模型配置」填表并保存——安装需要用模型三件套(API 端点 / 提炼模型 / 向量模型)生成服务配置。</div>`}
|
|
353
449
|
</div>
|
|
354
450
|
<div id="memInstallSteps">${installStepsHtml(IDLE_STEPS)}</div>
|
|
@@ -362,7 +458,7 @@ function renderWizard(el, { m, model, modelSaved }) {
|
|
|
362
458
|
<button class="btn sm" id="memTest">测试连接</button>
|
|
363
459
|
<button class="btn sm primary" id="memSaveModel">保存配置</button>
|
|
364
460
|
</div>
|
|
365
|
-
<div class="desc" style="margin-top:6px">记忆的<b>提炼(vlm)与向量化(embedding)</b>走 OpenAI
|
|
461
|
+
<div class="desc" style="margin-top:6px">记忆的<b>提炼(vlm)与向量化(embedding)</b>走 OpenAI 协议端点,两块可各自独立端点与 Key(跨厂商组合),默认向量块沿用主块。建议先 [测试连接] 双绿,再保存并一键安装。</div>
|
|
366
462
|
${modelFormHtml(model, m.endpoint)}
|
|
367
463
|
<div id="memTestResult" class="hint" style="margin-top:8px"></div>
|
|
368
464
|
<pre class="report" id="memTestReport" style="display:none;margin-top:6px"></pre>
|
|
@@ -388,16 +484,16 @@ function renderWizard(el, { m, model, modelSaved }) {
|
|
|
388
484
|
claude marketplace add openviking
|
|
389
485
|
claude plugin install openviking-memory@openviking
|
|
390
486
|
|
|
391
|
-
③ 回到本页:保存模型配置 →
|
|
487
|
+
③ 回到本页:保存模型配置 → 一键安装 → 「用户与密钥」一键初始化</pre>
|
|
392
488
|
<div class="hint" style="margin-top:8px;line-height:1.9">常见问题:
|
|
393
|
-
· 启动失败 / 服务异常 → 点「一键检查 /
|
|
394
|
-
· 鉴权失败 →
|
|
395
|
-
· 支持哪些模型 → 任意 OpenAI 协议端点(智谱 / 方舟 / 中转 / Ollama
|
|
489
|
+
· 启动失败 / 服务异常 → 点「一键检查 / 修复」重跑流水线(已就绪步骤自动跳过,不覆盖现有数据);首装冷启动可能超过 1 分钟,流水线会转为后台等待并实时显示秒数,无需反复重试
|
|
490
|
+
· 鉴权失败 → 到「用户与密钥」Tab 一键初始化(api_key 模式下网页/机器人各需一把用户密钥)
|
|
491
|
+
· 支持哪些模型 → 任意 OpenAI 协议端点(智谱 / 方舟 / 中转 / Ollama 本地);提炼与向量可分别指定
|
|
396
492
|
· 修改端口 → 模型配置里的「服务端点」;一键安装会自动避开占用端口</div>
|
|
397
493
|
</details>
|
|
398
494
|
</div>`;
|
|
399
495
|
|
|
400
|
-
bindModelForm(true);
|
|
496
|
+
bindModelForm(true, false);
|
|
401
497
|
bindInstallActions(() => !modelSaved);
|
|
402
498
|
|
|
403
499
|
// 外部服务入口:保存远程 endpoint → remoteConfigured → 切到已安装(外部)视图
|
|
@@ -410,9 +506,9 @@ function renderWizard(el, { m, model, modelSaved }) {
|
|
|
410
506
|
};
|
|
411
507
|
}
|
|
412
508
|
|
|
413
|
-
// ============ 已安装视图 ============
|
|
509
|
+
// ============ Tab1 · 已安装视图 ============
|
|
414
510
|
|
|
415
|
-
function renderInstalled(el, { st, m, model, inst, state, meta, remoteConfigured }) {
|
|
511
|
+
function renderInstalled(el, { st, m, model, inst, state, meta, remoteConfigured, authInitNeeded }) {
|
|
416
512
|
const ver = st.health?.version;
|
|
417
513
|
const endpoint = m.endpoint || DEFAULT_ENDPOINT;
|
|
418
514
|
const authNote = st.authNote;
|
|
@@ -422,8 +518,11 @@ function renderInstalled(el, { st, m, model, inst, state, meta, remoteConfigured
|
|
|
422
518
|
const statusSub = state === 'running' ? 'OpenViking 服务运行正常,记忆召回与归档已就绪'
|
|
423
519
|
: state === 'stopped' ? '服务已安装,进程未运行——点右上「▶ 启动服务」一键拉起(自动检查配置 → 拉起探活 → 接入插件)'
|
|
424
520
|
: '';
|
|
521
|
+
// 鉴权失败态优先引导「用户与密钥」Tab(api_key 模式下 401 的正确解法是初始化用户密钥,而非改模型配置)
|
|
425
522
|
const statusDetail = state === 'running' || state === 'stopped' ? ''
|
|
426
523
|
: `${esc(st.health?.detail || '探活失败')}${REASON_HINT[st.health?.reason] ? `<br>➜ ${REASON_HINT[st.health.reason]}` : ''}`;
|
|
524
|
+
const goUsersBtn = (state === 'auth' || authInitNeeded)
|
|
525
|
+
? '<button class="btn sm" id="memGoUsers" style="margin-top:8px">去 用户与密钥 →</button>' : '';
|
|
427
526
|
|
|
428
527
|
// 信息网格:只渲染「有值」的列(隐藏字段不占列位);运行方式用 mode 推导(embedded=LCB 受管安装)
|
|
429
528
|
// 网格固定 3 列(一行 3 项;auto-fit 会在宽屏挤成 6 列);地址列不跨列,长 URL 列内省略号截断防撑破;插件列值整体 nowrap 防拆行
|
|
@@ -443,7 +542,7 @@ function renderInstalled(el, { st, m, model, inst, state, meta, remoteConfigured
|
|
|
443
542
|
// 卡标题右按钮组:主行动随状态切换(启动/停止),刷新与检查修复常驻
|
|
444
543
|
// 检查/修复门禁与向导一键安装同口径:受管 conf 缺失(= 重跑会走首装生成 ov.conf)且模型
|
|
445
544
|
// 三件套未保存时禁用——把「缺模型」挡在点击之前,而不是让用户在「生成服务配置」步骤撞失败
|
|
446
|
-
const needModelFirst = !inst.confFound && !
|
|
545
|
+
const needModelFirst = !inst.confFound && !modelSavedCheck(model);
|
|
447
546
|
const repairBtn = `<button class="btn sm" id="memInstall" title="${needModelFirst
|
|
448
547
|
? '受管服务配置缺失,生成配置需要模型三件套——请先在下方「模型配置」卡填写并保存'
|
|
449
548
|
: '重跑安装流水线(已就绪部件自动跳过)'}">检查修复</button>`;
|
|
@@ -455,7 +554,6 @@ function renderInstalled(el, { st, m, model, inst, state, meta, remoteConfigured
|
|
|
455
554
|
: `${repairBtn} ${refreshBtn}`;
|
|
456
555
|
|
|
457
556
|
el.innerHTML = `
|
|
458
|
-
${pageIntro}
|
|
459
557
|
<div class="card">
|
|
460
558
|
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap">
|
|
461
559
|
<label class="switch"><input type="checkbox" id="memEnabled" ${m.enabled ? 'checked' : ''}><span class="track"></span></label>
|
|
@@ -479,8 +577,10 @@ function renderInstalled(el, { st, m, model, inst, state, meta, remoteConfigured
|
|
|
479
577
|
${statusSub ? `<span class="hint">— ${statusSub}</span>` : ''}
|
|
480
578
|
</div>
|
|
481
579
|
${statusDetail ? `<div class="hint" style="margin-top:9px;line-height:1.8">${statusDetail}</div>` : ''}
|
|
580
|
+
${goUsersBtn}
|
|
482
581
|
<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
582
|
</div>
|
|
583
|
+
${authInitNeeded ? `<div class="hint" style="margin-top:10px;padding:9px 14px;background:var(--warn-bg);color:var(--warn-tx);border-radius:10px">🔑 待初始化鉴权:服务为 api_key 模式但网页端用户尚未创建——浏览器打开记忆空间会 401。到「用户与密钥」Tab 点「一键初始化」即可。</div>` : ''}
|
|
484
584
|
<div id="memInstallSteps"></div>
|
|
485
585
|
<div id="memInstallHint" class="hint" style="margin-top:8px"></div>
|
|
486
586
|
</div>
|
|
@@ -491,7 +591,7 @@ function renderInstalled(el, { st, m, model, inst, state, meta, remoteConfigured
|
|
|
491
591
|
<button class="btn sm" id="memTest">测试连接</button>
|
|
492
592
|
<button class="btn sm primary" id="memSaveModel">保存配置</button>
|
|
493
593
|
</div>
|
|
494
|
-
<div class="desc" style="margin-top:6px">记忆的<b>提炼(vlm)与向量化(embedding)</b>走 OpenAI
|
|
594
|
+
<div class="desc" style="margin-top:6px">记忆的<b>提炼(vlm)与向量化(embedding)</b>走 OpenAI 协议端点,两块可各自独立端点与 Key(跨厂商组合),默认向量块沿用主块。变更后先 [测试连接] 双绿再保存;保存后可「立即生效」。</div>
|
|
495
595
|
${modelFormHtml(model, m.endpoint)}
|
|
496
596
|
<div id="memTestResult" class="hint" style="margin-top:8px"></div>
|
|
497
597
|
<pre class="report" id="memTestReport" style="display:none;margin-top:6px"></pre>
|
|
@@ -507,6 +607,7 @@ function renderInstalled(el, { st, m, model, inst, state, meta, remoteConfigured
|
|
|
507
607
|
blockers.push(`OpenViking 服务不可达:${esc(st.health?.detail || '未知原因')}${state === 'stopped' ? '——开启后可在「服务状态」卡点「▶ 启动服务」拉起' : ''}`);
|
|
508
608
|
}
|
|
509
609
|
if (!st.plugin?.found) blockers.push('记忆插件未安装——开启后可在「服务状态」卡点「一键检查 / 修复」补装');
|
|
610
|
+
if (authInitNeeded) blockers.push('网页端用户未创建(待初始化鉴权)——开启后到「用户与密钥」Tab 一键初始化');
|
|
510
611
|
const warnTxt = blockers.length
|
|
511
612
|
? `开启前请注意:<br>· ${blockers.join('<br>· ')}<br><br>仍可开启(保存配置,日志会在每条消息给出指引),确认继续?`
|
|
512
613
|
: '开启后每个机器人的每条消息将自动:注入记忆召回(hooks)+ 按命名空间隔离读写。确认开启?';
|
|
@@ -539,7 +640,9 @@ function renderInstalled(el, { st, m, model, inst, state, meta, remoteConfigured
|
|
|
539
640
|
catch { toast('复制失败(浏览器未授权剪贴板)', true); }
|
|
540
641
|
};
|
|
541
642
|
|
|
542
|
-
//
|
|
643
|
+
// 去「用户与密钥」Tab(入口联动:Tab1 只留跳转,不承载密钥操作——权限隔离)
|
|
644
|
+
const goUsers = $('#memGoUsers');
|
|
645
|
+
if (goUsers) goUsers.onclick = () => switchTab('users');
|
|
543
646
|
|
|
544
647
|
// 停止受管服务(危险确认;外部自管服务不显示此按钮)
|
|
545
648
|
const stopBtn = $('#memStop');
|
|
@@ -548,11 +651,236 @@ function renderInstalled(el, { st, m, model, inst, state, meta, remoteConfigured
|
|
|
548
651
|
try { await api('POST', '/api/memory/stop', {}); toast('已停止'); hooks.rerender(); } catch (e) { toast(e.message, true); }
|
|
549
652
|
};
|
|
550
653
|
|
|
551
|
-
bindModelForm(false);
|
|
654
|
+
bindModelForm(false, true);
|
|
552
655
|
// 已安装视图:检查/修复门禁——受管 conf 已存在时放行(沿用现值重跑);缺失时与向导同口径要模型三件套
|
|
553
656
|
bindInstallActions(() => needModelFirst);
|
|
554
657
|
}
|
|
555
658
|
|
|
659
|
+
/** 模型三件套就绪判定(status 数据源:apiBase/vlmModel/embeddingModel) */
|
|
660
|
+
const modelSavedCheck = (model) => !!(model && model.apiBase && model.vlmModel && model.embeddingModel);
|
|
661
|
+
|
|
662
|
+
// ============ Tab2 · 用户与密钥(最终方案 §六) ============
|
|
663
|
+
|
|
664
|
+
/** 体量人性化 */
|
|
665
|
+
const fmtSize = (b) => (b === undefined || b === null) ? '—'
|
|
666
|
+
: b < 1024 ? `${b} B`
|
|
667
|
+
: b < 1048576 ? `${(b / 1024).toFixed(0)} KB`
|
|
668
|
+
: `${(b / 1048576).toFixed(1)} MB`;
|
|
669
|
+
|
|
670
|
+
/** 最近活跃人性化(目录最大 mtime) */
|
|
671
|
+
const fmtActive = (iso) => {
|
|
672
|
+
const t = Date.parse(iso);
|
|
673
|
+
if (!Number.isFinite(t)) return '—';
|
|
674
|
+
const diff = Date.now() - t;
|
|
675
|
+
if (diff < 86400000) return '今天';
|
|
676
|
+
const days = Math.floor(diff / 86400000);
|
|
677
|
+
if (days < 30) return `${days} 天前`;
|
|
678
|
+
return new Date(t).toLocaleDateString();
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
/** 用户行(网页管理 / 机器人 / 其他;升级 bot×人 后机器人组可展开聊天者维度) */
|
|
682
|
+
function userRowHtml(u) {
|
|
683
|
+
const displayName = u.appName || (u.kind === 'web' ? '网页管理' : u.userId);
|
|
684
|
+
const keyState = u.keyAvailable
|
|
685
|
+
? `密钥 <span title="脱敏显示,完整密钥经「复制 key / 打开记忆空间」按需放行">${esc(u.keyHint || '••••')}</span>`
|
|
686
|
+
: '<span style="color:var(--warn-tx)">密钥未在库</span>';
|
|
687
|
+
const meta = [fmtSize(u.sizeBytes), u.lastActive ? `活跃于 ${fmtActive(u.lastActive)}` : '暂无记忆数据'].filter(Boolean).join(' · ');
|
|
688
|
+
return `<div style="display:flex;align-items:center;gap:12px;padding:11px 0;border-bottom:1px solid var(--border);flex-wrap:wrap">
|
|
689
|
+
<span style="width:8px;height:8px;border-radius:50%;background:var(--ok-tx);flex:0 0 auto"></span>
|
|
690
|
+
<div style="min-width:180px;flex:1">
|
|
691
|
+
<div style="font-size:13.5px;font-weight:600;display:flex;align-items:center;gap:8px;flex-wrap:wrap">${esc(displayName)}
|
|
692
|
+
<span class="hint"><code style="font-size:11px">${esc(u.userId)}</code></span>
|
|
693
|
+
<span class="hint">${keyState}</span>
|
|
694
|
+
</div>
|
|
695
|
+
<div class="hint" style="margin-top:3px">${meta}</div>
|
|
696
|
+
</div>
|
|
697
|
+
<button class="btn sm primary" data-open="${esc(u.userId)}" title="密钥进剪贴板 + 自动打开记忆空间(文件系统页),粘贴一次即可连接">打开记忆空间</button>
|
|
698
|
+
<button class="btn sm" data-copy="${esc(u.userId)}" title="复制密钥(脱敏显示,此操作会留痕)">⧉ 复制key</button>
|
|
699
|
+
<button class="btn sm" data-reset="${esc(u.userId)}" title="重签密钥:旧密钥立即失效,所有使用方需同步更新">重置</button>
|
|
700
|
+
<button class="btn sm danger" data-del="${esc(u.userId)}" title="删除用户与记忆(先自动备份本地数据)">删除</button>
|
|
701
|
+
</div>`;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/** Tab2 主渲染:一键初始化 + 用户列表(网页管理 / 机器人分组)+ 粒度向导 */
|
|
705
|
+
function renderUsersTab(el, { st, m }) {
|
|
706
|
+
const u = usersSnapshot;
|
|
707
|
+
const healthy = !!st.health?.healthy;
|
|
708
|
+
const apiKeyMode = st.health?.authMode === 'api_key';
|
|
709
|
+
const gran = (S.doc.memory && typeof S.doc.memory === 'object' && S.doc.memory.granularity) || 'bot';
|
|
710
|
+
|
|
711
|
+
let bodyHtml;
|
|
712
|
+
if (!u) {
|
|
713
|
+
bodyHtml = `<div class="card"><div class="hint">用户列表加载失败(服务未运行或请求异常)。</div>
|
|
714
|
+
<button class="btn sm" id="memUsersRetry" style="margin-top:10px">重试</button></div>`;
|
|
715
|
+
} else if (!healthy) {
|
|
716
|
+
bodyHtml = `<div class="card">
|
|
717
|
+
<div style="background:var(--warn-bg);border-radius:10px;padding:14px 16px">
|
|
718
|
+
<b>服务未运行</b>
|
|
719
|
+
<div class="hint" style="margin-top:4px;line-height:1.7">${esc(u.error || st.health?.detail || 'OpenViking 服务不可达')}——请先到「服务与配置」Tab 拉起服务,再回到本页管理用户与密钥。</div>
|
|
720
|
+
</div></div>`;
|
|
721
|
+
} else if (!apiKeyMode) {
|
|
722
|
+
bodyHtml = `<div class="card">
|
|
723
|
+
<div style="background:var(--off-bg);border-radius:10px;padding:14px 16px">
|
|
724
|
+
<b>当前为 ${esc(u.authMode || 'dev')} 模式:无须用户密钥</b>
|
|
725
|
+
<div class="hint" style="margin-top:4px;line-height:1.7">本机回环免鉴权,机器人身份经命名空间(<code>{appId}-{userId}</code>)隔离——浏览器打开记忆空间可直接访问。部署走向多人共享 / 出现敏感内容隔离需求时,配置 root_api_key 切换到 api_key 模式后,回本页一键初始化网页与机器人密钥。</div>
|
|
726
|
+
</div>
|
|
727
|
+
${granularityCardHtml(gran)}</div>`;
|
|
728
|
+
} else {
|
|
729
|
+
const webUsers = u.users.filter((x) => x.kind === 'web');
|
|
730
|
+
const botUsers = u.users.filter((x) => x.kind === 'bot');
|
|
731
|
+
const otherUsers = u.users.filter((x) => x.kind === 'other');
|
|
732
|
+
// 聊天者归属映射(前端兜底,与后端同口径):历史复合 ID({appId}-{ou_xxx})按前缀归属到
|
|
733
|
+
// 对应飞书应用——后端未重启(旧构建无映射)时本层照样生效,刷新即见
|
|
734
|
+
const bots = st.bots || [];
|
|
735
|
+
otherUsers.forEach((x) => {
|
|
736
|
+
if (x.appName) return;
|
|
737
|
+
const hit = bots.find((b) => x.userId.startsWith(`${b.appId}-`));
|
|
738
|
+
if (hit) x.appName = `${hit.name} · 聊天者`;
|
|
739
|
+
});
|
|
740
|
+
const group = (title, rows, hint) => rows.length === 0 ? '' : `
|
|
741
|
+
<div style="margin-top:14px">
|
|
742
|
+
<div style="font-size:12.5px;font-weight:700;color:var(--muted);letter-spacing:.4px">${esc(title)} <span class="hint" style="font-weight:400">${hint || ''}</span></div>
|
|
743
|
+
<div>${rows.map(userRowHtml).join('')}</div>
|
|
744
|
+
</div>`;
|
|
745
|
+
bodyHtml = `<div class="card">
|
|
746
|
+
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
|
747
|
+
<h3 style="margin:0">用户与密钥</h3>
|
|
748
|
+
<span class="hint">${esc(String(u.users.length))} 个用户 · 合计 ${fmtSize(u.totalBytes)} 记忆数据</span>
|
|
749
|
+
<span style="flex:1"></span>
|
|
750
|
+
${u.authInitNeeded ? '<button class="btn sm primary" id="memUsersInit">⚡ 一键初始化</button>' : '<button class="btn sm" id="memUsersInit">⚡ 一键初始化</button>'}
|
|
751
|
+
<button class="btn sm" id="memUsersRefresh">↻ 刷新</button>
|
|
752
|
+
</div>
|
|
753
|
+
<div class="desc" style="margin-top:6px">api_key 模式:网页与每个机器人各持一把独立密钥(密钥落盘已加密、页面只脱敏显示,查看行为留痕审计)。「打开记忆空间」= 密钥进剪贴板 + 自动新标签打开文件系统页,粘贴一次即可连接。</div>
|
|
754
|
+
<div class="hint" style="margin-top:6px">ⓘ 1933 网页右上角的身份切换显示的是技术 ID(OpenViking 暂不支持显示名)——哪个 ID 对应哪台机器人,以本页对照为准。</div>
|
|
755
|
+
${u.authInitNeeded ? `<div class="hint" style="margin-top:10px;padding:9px 14px;background:var(--warn-bg);color:var(--warn-tx);border-radius:10px">🔑 待初始化鉴权:网页端用户(web)尚未创建——浏览器打开记忆空间会 401。点右上「⚡ 一键初始化」自动创建网页 + 各机器人用户并签发密钥。</div>` : ''}
|
|
756
|
+
${u.users.length === 0 && !u.authInitNeeded ? '<div class="hint" style="margin-top:12px">暂无用户——点「⚡ 一键初始化」创建。</div>' : ''}
|
|
757
|
+
${group('网页管理', webUsers, '浏览器打开记忆空间用')}
|
|
758
|
+
${group('机器人', botUsers, '每个 bot 一把独立密钥(升级 bot×人 后此处可展开聊天者维度)')}
|
|
759
|
+
${group('聊天者与其他', otherUsers, '历史身份({appId}-{ou_xxx})已自动归属到对应机器人')}
|
|
760
|
+
${granularityCardHtml(gran)}
|
|
761
|
+
</div>`;
|
|
762
|
+
}
|
|
763
|
+
el.innerHTML = bodyHtml;
|
|
764
|
+
bindUsersTabActions(el, { m });
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/** 粒度向导卡(方案 §六:默认 bot 级;「升级到 bot×人」开关 = 增量升级意向,不动既有 key 与记忆) */
|
|
768
|
+
const granularityCardHtml = (gran) => `
|
|
769
|
+
<div class="card" style="margin-top:14px">
|
|
770
|
+
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap">
|
|
771
|
+
<b style="font-size:13.5px">记忆粒度</b>
|
|
772
|
+
<span class="hint">${gran === 'bot-user' ? '已选择:bot × 人(每位聊天者独立空间)' : '当前:每 bot 一把密钥(默认,单机自用零损失)'}</span>
|
|
773
|
+
<span style="flex:1"></span>
|
|
774
|
+
<label style="display:inline-flex;align-items:center;gap:8px;cursor:pointer">
|
|
775
|
+
<span class="hint">升级到 bot×人</span>
|
|
776
|
+
<label class="switch"><input type="checkbox" id="memGran" ${gran === 'bot-user' ? 'checked' : ''}><span class="track"></span></label>
|
|
777
|
+
</label>
|
|
778
|
+
</div>
|
|
779
|
+
<div class="hint" style="margin-top:8px;line-height:1.8">升级属<b>增量演进</b>:既有密钥与记忆不受影响、会话不中断,仅为新聊天者追加独立签发。建议触发条件(任一出现即升级):同一 bot 服务 2 人以上且内容敏感 / 出现「记忆是谁写的」追溯需求 / 部署走向团队共享。</div>
|
|
780
|
+
</div>`;
|
|
781
|
+
|
|
782
|
+
function bindUsersTabActions(el, { m }) {
|
|
783
|
+
const endpoint = (m.endpoint || DEFAULT_ENDPOINT).replace(/\/+$/, '');
|
|
784
|
+
|
|
785
|
+
const reload = async () => {
|
|
786
|
+
try { usersSnapshot = await api('GET', '/api/memory/users'); } catch { usersSnapshot = null; }
|
|
787
|
+
hooks.rerender();
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
const retry = $('#memUsersRetry');
|
|
791
|
+
if (retry) retry.onclick = reload;
|
|
792
|
+
|
|
793
|
+
const refreshBtn = $('#memUsersRefresh');
|
|
794
|
+
if (refreshBtn) refreshBtn.onclick = reload;
|
|
795
|
+
|
|
796
|
+
const initBtn = $('#memUsersInit');
|
|
797
|
+
if (initBtn) initBtn.onclick = async () => {
|
|
798
|
+
initBtn.disabled = true;
|
|
799
|
+
initBtn.textContent = '初始化中…';
|
|
800
|
+
try {
|
|
801
|
+
const r = await api('POST', '/api/memory/users/init', {});
|
|
802
|
+
if (r.ok) toast(r.detail || '初始化完成');
|
|
803
|
+
else toast((r.errors && r.errors[0]) || r.detail || '初始化失败', true);
|
|
804
|
+
} catch (e) { toast(e.message, true); }
|
|
805
|
+
await reload();
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
// 打开记忆空间(最终方案 §三 决策 5):密钥进剪贴板 → 自动新标签开 Web Studio 文件系统页
|
|
809
|
+
//(深链 /studio/filesystem 直落记忆目录树,避开默认的 VikingBot 标签——那是 OpenViking
|
|
810
|
+
// 自带的 Agent 对话功能,需 --with-bot 启动,与记忆浏览无关)。未连接的浏览器会被
|
|
811
|
+
// Studio 引导到连接设置页,粘贴一次即可。
|
|
812
|
+
const openSpace = async (uid) => {
|
|
813
|
+
try {
|
|
814
|
+
const r = await api('POST', '/api/memory/users/reveal', { user_id: uid });
|
|
815
|
+
if (!r.ok || !r.key) return toast(r.error || '密钥获取失败', true);
|
|
816
|
+
try { await navigator.clipboard.writeText(r.key); } catch { return toast('密钥已放行但剪贴板不可用——请在浏览器地址栏手动粘贴', true); }
|
|
817
|
+
window.open(`${endpoint}/studio/filesystem`, '_blank', 'noopener');
|
|
818
|
+
toast('密钥已复制到剪贴板——在打开的页面粘贴一次(Ctrl+V)即可连接');
|
|
819
|
+
} catch (e) { toast(e.message, true); }
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
const copyKey = async (uid) => {
|
|
823
|
+
try {
|
|
824
|
+
const r = await api('POST', '/api/memory/users/reveal', { user_id: uid });
|
|
825
|
+
if (!r.ok || !r.key) return toast(r.error || '密钥获取失败', true);
|
|
826
|
+
await navigator.clipboard.writeText(r.key);
|
|
827
|
+
toast(`已复制 ${uid} 的密钥(查看行为已留痕)`);
|
|
828
|
+
} catch (e) { toast(e.message, true); }
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
const resetKey = async (uid) => {
|
|
832
|
+
if (!(await confirmDialog({
|
|
833
|
+
title: '重置密钥', danger: true,
|
|
834
|
+
message: `确定重置 <b>${esc(uid)}</b> 的密钥?<br><br>旧密钥将<b>立即失效</b>——所有使用此密钥的一方(浏览器连接、已打开的记忆空间、机器人会话)需同步更新并重启会话。`,
|
|
835
|
+
confirmText: '重置',
|
|
836
|
+
}))) return;
|
|
837
|
+
try {
|
|
838
|
+
const r = await api('POST', '/api/memory/users/reset', { user_id: uid });
|
|
839
|
+
if (!r.ok || !r.key) return toast(r.error || '重置失败', true);
|
|
840
|
+
try { await navigator.clipboard.writeText(r.key); toast('新密钥已生成并复制到剪贴板(旧密钥已失效)'); }
|
|
841
|
+
catch { toast('新密钥已生成(旧密钥已失效;剪贴板不可用,可稍后复制)'); }
|
|
842
|
+
} catch (e) { toast(e.message, true); }
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
const delUser = async (uid) => {
|
|
846
|
+
if (!(await confirmDialog({
|
|
847
|
+
title: '删除用户', danger: true,
|
|
848
|
+
message: `确定删除 <b>${esc(uid)}</b>?<br><br>该用户的服务端数据将被异步清理;删除前会自动把本地记忆目录打包备份到 <code>openviking/backups/</code>(备份失败会中止删除)。`,
|
|
849
|
+
confirmText: '删除',
|
|
850
|
+
}))) return;
|
|
851
|
+
try {
|
|
852
|
+
const r = await api('POST', '/api/memory/users/delete', { user_id: uid });
|
|
853
|
+
toast(r.ok ? (r.detail || '已删除') : (r.error || r.detail || '删除失败'), !r.ok);
|
|
854
|
+
} catch (e) { toast(e.message, true); }
|
|
855
|
+
await reload();
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
el.addEventListener('click', (e) => {
|
|
859
|
+
const t = e.target.closest('[data-open],[data-copy],[data-reset],[data-del]');
|
|
860
|
+
if (!t) return;
|
|
861
|
+
if (t.dataset.open) return void openSpace(t.dataset.open);
|
|
862
|
+
if (t.dataset.copy) return void copyKey(t.dataset.copy);
|
|
863
|
+
if (t.dataset.reset) return void resetKey(t.dataset.reset);
|
|
864
|
+
if (t.dataset.del) return void delUser(t.dataset.del);
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
const granEl = $('#memGran');
|
|
868
|
+
if (granEl) granEl.onchange = async (e) => {
|
|
869
|
+
const next = e.target.checked ? 'bot-user' : 'bot';
|
|
870
|
+
if (!S.doc.memory || typeof S.doc.memory !== 'object') S.doc.memory = {};
|
|
871
|
+
const old = S.doc.memory.granularity;
|
|
872
|
+
S.doc.memory.granularity = next;
|
|
873
|
+
if (await saveDoc()) {
|
|
874
|
+
toast(next === 'bot-user'
|
|
875
|
+
? '已记录升级意向:bot×人(增量生效——既有密钥与记忆不动,新聊天者独立签发)'
|
|
876
|
+
: '已回到每 bot 一把密钥');
|
|
877
|
+
} else {
|
|
878
|
+
S.doc.memory.granularity = old;
|
|
879
|
+
e.target.checked = !e.target.checked;
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
|
|
556
884
|
export const page = {
|
|
557
885
|
id: 'memory',
|
|
558
886
|
title: '记忆增强',
|