Remove unused HTML files and hexToRgba function from the web directory
This commit is contained in:
@@ -65,6 +65,93 @@ class _FakeGatewayCapability:
|
||||
return True
|
||||
|
||||
|
||||
class _FakeNapCatQueryService:
|
||||
"""用于驱动 NapCat 入站编解码测试的查询服务替身。"""
|
||||
|
||||
def __init__(self, forward_payloads: Dict[str, Any] | None = None) -> None:
|
||||
"""初始化查询服务替身。
|
||||
|
||||
Args:
|
||||
forward_payloads: 预置的合并转发响应映射。
|
||||
"""
|
||||
self._forward_payloads = forward_payloads or {}
|
||||
|
||||
async def download_binary(self, url: str) -> bytes | None:
|
||||
"""模拟下载远程二进制资源。
|
||||
|
||||
Args:
|
||||
url: 资源地址。
|
||||
|
||||
Returns:
|
||||
bytes | None: 测试中默认不返回二进制内容。
|
||||
"""
|
||||
del url
|
||||
return None
|
||||
|
||||
async def get_message_detail(self, message_id: str) -> Dict[str, Any] | None:
|
||||
"""模拟获取消息详情。
|
||||
|
||||
Args:
|
||||
message_id: 消息 ID。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any] | None: 测试中默认不返回详情。
|
||||
"""
|
||||
del message_id
|
||||
return None
|
||||
|
||||
async def get_forward_message(self, message_id: str) -> Any:
|
||||
"""模拟获取合并转发消息详情。
|
||||
|
||||
Args:
|
||||
message_id: 转发消息 ID。
|
||||
|
||||
Returns:
|
||||
Any: 预置的合并转发消息详情。
|
||||
"""
|
||||
return self._forward_payloads.get(message_id)
|
||||
|
||||
async def get_record_detail(self, file_name: str, file_id: str | None = None) -> Dict[str, Any] | None:
|
||||
"""模拟获取语音详情。
|
||||
|
||||
Args:
|
||||
file_name: 文件名。
|
||||
file_id: 文件 ID。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any] | None: 测试中默认不返回语音详情。
|
||||
"""
|
||||
del file_name
|
||||
del file_id
|
||||
return None
|
||||
|
||||
|
||||
class _FakeNapCatActionService:
|
||||
"""用于驱动 NapCat 查询服务测试的动作服务替身。"""
|
||||
|
||||
def __init__(self, response_data: Any) -> None:
|
||||
"""初始化动作服务替身。
|
||||
|
||||
Args:
|
||||
response_data: 预置的 ``safe_call_action_data`` 返回值。
|
||||
"""
|
||||
self._response_data = response_data
|
||||
|
||||
async def safe_call_action_data(self, action_name: str, params: Dict[str, Any]) -> Any:
|
||||
"""模拟安全调用 OneBot 动作。
|
||||
|
||||
Args:
|
||||
action_name: 动作名称。
|
||||
params: 动作参数。
|
||||
|
||||
Returns:
|
||||
Any: 预置返回值。
|
||||
"""
|
||||
del action_name
|
||||
del params
|
||||
return self._response_data
|
||||
|
||||
|
||||
def _load_napcat_sdk_modules() -> Tuple[Any, Any, Any, Any]:
|
||||
"""动态加载 NapCat 插件测试所需的模块。
|
||||
|
||||
@@ -116,6 +203,28 @@ def _load_napcat_sdk_symbols() -> Tuple[Any, Any, Any, Any]:
|
||||
)
|
||||
|
||||
|
||||
def _load_napcat_inbound_codec_cls() -> Any:
|
||||
"""动态加载 NapCat 入站编解码器类。
|
||||
|
||||
Returns:
|
||||
Any: ``NapCatInboundCodec`` 类对象。
|
||||
"""
|
||||
_load_napcat_sdk_modules()
|
||||
codec_module = import_module(f"{NAPCAT_TEST_MODULE}.codecs.inbound.message_codec")
|
||||
return codec_module.NapCatInboundCodec
|
||||
|
||||
|
||||
def _load_napcat_query_service_cls() -> Any:
|
||||
"""动态加载 NapCat 查询服务类。
|
||||
|
||||
Returns:
|
||||
Any: ``NapCatQueryService`` 类对象。
|
||||
"""
|
||||
_load_napcat_sdk_modules()
|
||||
query_service_module = import_module(f"{NAPCAT_TEST_MODULE}.services.query_service")
|
||||
return query_service_module.NapCatQueryService
|
||||
|
||||
|
||||
def test_napcat_plugin_collects_duplex_message_gateway() -> None:
|
||||
"""NapCat 插件应声明新的双工消息网关组件。"""
|
||||
|
||||
@@ -161,7 +270,7 @@ def test_napcat_plugin_normalizes_legacy_config_values() -> None:
|
||||
|
||||
plugin.set_plugin_config(
|
||||
{
|
||||
"plugin": {"enabled": True, "config_version": ""},
|
||||
"plugin": {"enabled": True, "config_version": constants_module.SUPPORTED_CONFIG_VERSION},
|
||||
"connection": {
|
||||
"access_token": "secret-token",
|
||||
"heartbeat_sec": "45",
|
||||
@@ -220,3 +329,121 @@ async def test_runtime_state_reports_via_gateway_capability() -> None:
|
||||
assert gateway_capability.calls[1]["gateway_name"] == napcat_gateway_name
|
||||
assert gateway_capability.calls[1]["ready"] is False
|
||||
assert gateway_capability.calls[1]["platform"] == "qq"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_inbound_codec_parses_forward_nodes_from_legacy_message_field() -> None:
|
||||
"""入站编解码器应兼容旧版 ``sender + message`` 转发节点结构。"""
|
||||
|
||||
inbound_codec_cls = _load_napcat_inbound_codec_cls()
|
||||
codec = inbound_codec_cls(
|
||||
logger=logging.getLogger("test.napcat_adapter.forward_legacy"),
|
||||
query_service=_FakeNapCatQueryService(
|
||||
forward_payloads={
|
||||
"forward-1": {
|
||||
"messages": [
|
||||
{
|
||||
"sender": {"user_id": "10001", "nickname": "张三", "card": "群名片"},
|
||||
"message_id": "node-1",
|
||||
"message": [{"type": "text", "data": {"text": "第一条转发"}}],
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
segments, is_at = await codec.convert_segments(
|
||||
{"message": [{"type": "forward", "data": {"id": "forward-1"}}]},
|
||||
"",
|
||||
)
|
||||
|
||||
assert is_at is False
|
||||
assert len(segments) == 1
|
||||
assert segments[0]["type"] == "forward"
|
||||
assert segments[0]["data"][0]["user_id"] == "10001"
|
||||
assert segments[0]["data"][0]["user_nickname"] == "张三"
|
||||
assert segments[0]["data"][0]["user_cardname"] == "群名片"
|
||||
assert segments[0]["data"][0]["content"] == [{"type": "text", "data": "第一条转发"}]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_inbound_codec_parses_nested_inline_forward_content() -> None:
|
||||
"""入站编解码器应支持内联 ``content`` 形式的嵌套合并转发。"""
|
||||
|
||||
inbound_codec_cls = _load_napcat_inbound_codec_cls()
|
||||
codec = inbound_codec_cls(
|
||||
logger=logging.getLogger("test.napcat_adapter.forward_nested"),
|
||||
query_service=_FakeNapCatQueryService(
|
||||
forward_payloads={
|
||||
"forward-outer": {
|
||||
"messages": [
|
||||
{
|
||||
"sender": {"user_id": "10001", "nickname": "张三"},
|
||||
"message_id": "node-outer",
|
||||
"message": [
|
||||
{
|
||||
"type": "forward",
|
||||
"data": {
|
||||
"content": [
|
||||
{
|
||||
"sender": {"user_id": "10002", "nickname": "李四"},
|
||||
"message_id": "node-inner",
|
||||
"message": [{"type": "text", "data": {"text": "内层消息"}}],
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
segments, _ = await codec.convert_segments(
|
||||
{"message": [{"type": "forward", "data": {"id": "forward-outer"}}]},
|
||||
"",
|
||||
)
|
||||
|
||||
assert len(segments) == 1
|
||||
assert segments[0]["type"] == "forward"
|
||||
outer_content = segments[0]["data"][0]["content"]
|
||||
assert len(outer_content) == 1
|
||||
assert outer_content[0]["type"] == "forward"
|
||||
nested_nodes = outer_content[0]["data"]
|
||||
assert nested_nodes[0]["user_id"] == "10002"
|
||||
assert nested_nodes[0]["user_nickname"] == "李四"
|
||||
assert nested_nodes[0]["content"] == [{"type": "text", "data": "内层消息"}]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_service_normalizes_forward_payload_list() -> None:
|
||||
"""查询服务应兼容 ``get_forward_msg`` 直接返回节点列表。"""
|
||||
|
||||
query_service_cls = _load_napcat_query_service_cls()
|
||||
query_service = query_service_cls(
|
||||
action_service=_FakeNapCatActionService(
|
||||
[
|
||||
{
|
||||
"sender": {"user_id": "10001", "nickname": "张三"},
|
||||
"message_id": "node-1",
|
||||
"message": [{"type": "text", "data": {"text": "列表返回"}}],
|
||||
}
|
||||
]
|
||||
),
|
||||
logger=logging.getLogger("test.napcat_adapter.query_service"),
|
||||
)
|
||||
|
||||
forward_payload = await query_service.get_forward_message("forward-1")
|
||||
|
||||
assert forward_payload == {
|
||||
"messages": [
|
||||
{
|
||||
"sender": {"user_id": "10001", "nickname": "张三"},
|
||||
"message_id": "node-1",
|
||||
"message": [{"type": "text", "data": {"text": "列表返回"}}],
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,913 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>A_Memorix 导入中心</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0b1220;
|
||||
--panel: #111827;
|
||||
--border: #334155;
|
||||
--text: #e2e8f0;
|
||||
--muted: #94a3b8;
|
||||
--pri: #38bdf8;
|
||||
--ok: #10b981;
|
||||
--warn: #f59e0b;
|
||||
--err: #ef4444;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; color: var(--text); font-family: "Segoe UI", "Microsoft YaHei", sans-serif; background: radial-gradient(circle at 0 0, #0f2a4a, transparent 35%), var(--bg); }
|
||||
.page { width: min(1400px, 96vw); margin: 18px auto 28px; }
|
||||
.top { display: flex; gap: 10px; justify-content: space-between; align-items: end; margin-bottom: 12px; }
|
||||
.top-mid { flex: 1; display: flex; justify-content: center; align-items: end; }
|
||||
.title { font-size: 22px; font-weight: 700; }
|
||||
.sub { color: var(--muted); font-size: 12px; }
|
||||
.token { display: flex; gap: 8px; min-width: 360px; }
|
||||
.grid { display: grid; grid-template-columns: 420px 1fr; gap: 12px; }
|
||||
.card { background: rgba(17, 24, 39, 0.92); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }
|
||||
.hd { padding: 10px 12px; font-size: 13px; color: var(--pri); border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; }
|
||||
.bd { padding: 12px; }
|
||||
.row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 8px; }
|
||||
.row3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; margin-bottom: 8px; }
|
||||
label { display: block; color: var(--muted); font-size: 12px; margin-bottom: 4px; }
|
||||
input, select, textarea, button { font: inherit; }
|
||||
input, select, textarea { width: 100%; border: 1px solid var(--border); border-radius: 8px; padding: 7px 9px; background: #0f172a; color: var(--text); }
|
||||
input[type="checkbox"] { width: 16px; height: 16px; padding: 0; border-radius: 4px; accent-color: var(--pri); flex: 0 0 auto; }
|
||||
textarea { min-height: 120px; resize: vertical; }
|
||||
.checkline { display: flex; align-items: center; gap: 8px; color: var(--text); margin-bottom: 8px; line-height: 1.4; }
|
||||
.checkline:last-child { margin-bottom: 0; }
|
||||
.checkgrid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 14px; margin-bottom: 8px; }
|
||||
.file-pick { display: flex; align-items: center; gap: 8px; min-height: 38px; padding: 4px; border: 1px solid var(--border); border-radius: 8px; background: #0f172a; }
|
||||
.file-pick .tiny { margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.file-pick input[type="file"] { display: none; }
|
||||
.modal-mask { position: fixed; inset: 0; background: rgba(2, 6, 23, 0.72); display: none; align-items: center; justify-content: center; z-index: 120; padding: 20px; }
|
||||
.modal-mask.show { display: flex; }
|
||||
.modal { width: min(980px, 96vw); max-height: 86vh; background: #0b1528; border: 1px solid var(--border); border-radius: 12px; display: flex; flex-direction: column; overflow: hidden; }
|
||||
.modal-hd { display: flex; justify-content: space-between; align-items: center; padding: 10px 12px; border-bottom: 1px solid var(--border); color: var(--pri); }
|
||||
.modal-bd { padding: 12px; overflow: auto; line-height: 1.62; font-size: 14px; }
|
||||
.md h1, .md h2, .md h3 { margin: 14px 0 8px; color: #f8fafc; }
|
||||
.md p { margin: 8px 0; }
|
||||
.md ul { margin: 8px 0 8px 20px; padding: 0; }
|
||||
.md pre { margin: 10px 0; padding: 10px; border: 1px solid var(--border); border-radius: 8px; background: #0f172a; overflow: auto; }
|
||||
.md code { font-family: Consolas, Menlo, Monaco, monospace; font-size: 12px; }
|
||||
.md a { color: #67e8f9; text-decoration: none; }
|
||||
.md a:hover { text-decoration: underline; }
|
||||
.md blockquote { margin: 10px 0; padding: 8px 10px; border-left: 3px solid var(--pri); background: rgba(56, 189, 248, 0.08); color: #cbd5e1; }
|
||||
.tabs { display: flex; gap: 6px; margin-bottom: 8px; flex-wrap: wrap; }
|
||||
.tabs button { flex: 1 1 calc(33.33% - 6px); min-width: 104px; border: 1px solid var(--border); border-radius: 8px; background: #1f2937; color: var(--text); padding: 7px; cursor: pointer; }
|
||||
.tabs button.active { background: linear-gradient(120deg, #0ea5e9, #22d3ee); color: #01243a; font-weight: 700; border: none; }
|
||||
.panel { display: none; }
|
||||
.panel.active { display: block; }
|
||||
.btns { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.btn { border: 1px solid var(--border); border-radius: 8px; padding: 7px 10px; background: #1f2937; color: var(--text); cursor: pointer; }
|
||||
.btn.p { background: linear-gradient(120deg, #0ea5e9, #22d3ee); color: #022f4d; border: none; font-weight: 700; }
|
||||
.btn.warn { border-color: #854d0e; color: #fbbf24; }
|
||||
.btn.err { border-color: #7f1d1d; color: #fca5a5; }
|
||||
.tiny { color: var(--muted); font-size: 12px; }
|
||||
.list { max-height: 150px; overflow: auto; border: 1px dashed var(--border); border-radius: 8px; padding: 6px; }
|
||||
.list-item { display: flex; justify-content: space-between; gap: 8px; padding: 5px; border-radius: 7px; }
|
||||
.list-item:hover { background: #1e293b; }
|
||||
.task-list { max-height: 260px; overflow: auto; display: grid; gap: 7px; }
|
||||
.task { border: 1px solid var(--border); border-radius: 8px; padding: 7px; cursor: pointer; background: #0f172a; }
|
||||
.task.active { border-color: var(--pri); }
|
||||
.badge { font-size: 11px; border: 1px solid var(--border); border-radius: 999px; padding: 1px 7px; }
|
||||
.b-run { color: #67e8f9; } .b-q { color: #c4b5fd; } .b-ok { color: #6ee7b7; } .b-err { color: #fca5a5; } .b-cancel { color: #fcd34d; }
|
||||
.bar { height: 7px; border-radius: 999px; background: #334155; overflow: hidden; margin-top: 5px; }
|
||||
.bar > div { height: 100%; background: linear-gradient(120deg, #0ea5e9, #22d3ee); width: 0; }
|
||||
.mgrid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 7px; margin-top: 8px; }
|
||||
.m { border: 1px solid var(--border); border-radius: 8px; padding: 7px; background: #0f172a; }
|
||||
.m .k { color: var(--muted); font-size: 11px; } .m .v { font-size: 16px; font-weight: 700; margin-top: 3px; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
||||
th, td { text-align: left; border-bottom: 1px solid var(--border); padding: 7px 5px; vertical-align: top; }
|
||||
tr.pick { cursor: pointer; } tr.pick.active { background: #10324f; }
|
||||
.foot { display: flex; justify-content: space-between; align-items: center; margin-top: 8px; }
|
||||
#toast { position: fixed; top: 12px; left: 50%; transform: translateX(-50%); background: #111827; border: 1px solid var(--border); border-radius: 8px; padding: 8px 11px; display: none; z-index: 99; }
|
||||
@media (max-width: 1100px) {
|
||||
.grid { grid-template-columns: 1fr; }
|
||||
.token { min-width: 0; width: 100%; }
|
||||
.top { flex-direction: column; align-items: stretch; }
|
||||
.top-mid { justify-content: flex-start; }
|
||||
.mgrid { grid-template-columns: repeat(2, 1fr); }
|
||||
.checkgrid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="toast"></div>
|
||||
<div id="guide-modal" class="modal-mask" role="dialog" aria-modal="true" aria-labelledby="guide-modal-title">
|
||||
<div class="modal">
|
||||
<div class="modal-hd">
|
||||
<div id="guide-modal-title">导入相关文档</div>
|
||||
<button class="btn" id="guide-close" type="button">关闭</button>
|
||||
</div>
|
||||
<div class="tiny" id="guide-meta" style="padding: 8px 12px 0;"></div>
|
||||
<div class="modal-bd md" id="guide-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page">
|
||||
<div class="top">
|
||||
<div>
|
||||
<div class="title">A_Memorix 导入中心</div>
|
||||
<div class="sub">可控并发、部分文件导入、粘贴导入、分块级进度</div>
|
||||
</div>
|
||||
<div class="top-mid">
|
||||
<button class="btn" id="guide-open" type="button">阅读导入文档</button>
|
||||
<button class="btn" type="button" onclick="window.open('/tuning', '_blank')">检索调优</button>
|
||||
</div>
|
||||
<div class="token">
|
||||
<input id="token-input" type="password" placeholder="可选:X-Memorix-Import-Token" />
|
||||
<button class="btn" id="token-save">保存</button>
|
||||
<button class="btn" id="token-clear">清空</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="hd">创建导入任务</div>
|
||||
<div class="bd">
|
||||
<div class="tabs">
|
||||
<button id="tab-upload" class="active">上传文件</button>
|
||||
<button id="tab-paste">粘贴导入</button>
|
||||
<button id="tab-raw">本地扫描</button>
|
||||
<button id="tab-openie">LPMM OpenIE</button>
|
||||
<button id="tab-convert">LPMM转换</button>
|
||||
<button id="tab-backfill">时序回填</button>
|
||||
<button id="tab-maibot">MaiBot迁移</button>
|
||||
</div>
|
||||
|
||||
<div class="row3">
|
||||
<div>
|
||||
<label>文件并发</label>
|
||||
<input id="file-concurrency" type="number" min="1" max="6" value="2" />
|
||||
</div>
|
||||
<div>
|
||||
<label>分块并发</label>
|
||||
<input id="chunk-concurrency" type="number" min="1" max="12" value="4" />
|
||||
</div>
|
||||
<div>
|
||||
<label>策略覆盖</label>
|
||||
<select id="strategy-override">
|
||||
<option value="auto">自动(auto)</option>
|
||||
<option value="narrative">叙事(narrative)</option>
|
||||
<option value="factual">事实(factual)</option>
|
||||
<option value="quote">引用(quote)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row3">
|
||||
<div>
|
||||
<label>去重策略</label>
|
||||
<select id="dedupe-policy">
|
||||
<option value="content_hash">内容哈希(content_hash)</option>
|
||||
<option value="manifest">导入清单(manifest)</option>
|
||||
<option value="none">不去重(none)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>聊天参考时间(chat_reference_time)</label>
|
||||
<input id="chat-reference-time" placeholder="2026/02/12 10:30" />
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="checkgrid tiny">
|
||||
<label class="checkline">
|
||||
<input id="llm-enabled" type="checkbox" checked />
|
||||
<span>启用 LLM 抽取</span>
|
||||
</label>
|
||||
<label class="checkline">
|
||||
<input id="chat-log" type="checkbox" />
|
||||
<span>按聊天日志抽取时间(chat_log)</span>
|
||||
</label>
|
||||
<label class="checkline">
|
||||
<input id="force-reimport" type="checkbox" />
|
||||
<span>强制重导(force)</span>
|
||||
</label>
|
||||
<label class="checkline">
|
||||
<input id="clear-manifest" type="checkbox" />
|
||||
<span>清理导入清单(clear_manifest)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="panel-upload" class="panel active">
|
||||
<div class="row">
|
||||
<div>
|
||||
<label>文本输入模式</label>
|
||||
<select id="upload-input-mode">
|
||||
<option value="text">文本(text)</option>
|
||||
<option value="json">JSON(json)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>选择文件 (txt/md/json)</label>
|
||||
<div class="file-pick">
|
||||
<button class="btn" id="upload-file-pick" type="button">选择文件</button>
|
||||
<div class="tiny" id="upload-file-hint">未选择文件</div>
|
||||
<input id="upload-file-input" type="file" multiple accept=".txt,.md,.json" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tiny" id="upload-count">已选择 0 个文件</div>
|
||||
<div class="list" id="upload-files"></div>
|
||||
<div class="btns" style="margin-top: 8px;">
|
||||
<button class="btn p" id="upload-submit">提交上传任务</button>
|
||||
<button class="btn" id="upload-clear">清空文件列表</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="panel-paste" class="panel">
|
||||
<div class="row">
|
||||
<div>
|
||||
<label>粘贴模式</label>
|
||||
<select id="paste-input-mode">
|
||||
<option value="text">文本(text)</option>
|
||||
<option value="json">JSON(json)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>名称(可选)</label>
|
||||
<input id="paste-name" placeholder="paste_时间戳" />
|
||||
</div>
|
||||
</div>
|
||||
<label>内容</label>
|
||||
<textarea id="paste-content" placeholder="粘贴 text/json"></textarea>
|
||||
<div class="btns" style="margin-top: 8px;">
|
||||
<button class="btn p" id="paste-submit">提交粘贴任务</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="panel-raw" class="panel">
|
||||
<div class="row">
|
||||
<div>
|
||||
<label>路径别名</label>
|
||||
<select id="raw-alias"></select>
|
||||
</div>
|
||||
<div>
|
||||
<label>相对路径(relative_path)</label>
|
||||
<input id="raw-relative-path" placeholder="raw 子目录(可选)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row3">
|
||||
<div>
|
||||
<label>匹配规则(glob)</label>
|
||||
<input id="raw-glob" value="*" />
|
||||
</div>
|
||||
<div>
|
||||
<label>输入模式</label>
|
||||
<select id="raw-input-mode">
|
||||
<option value="text">文本(text)</option>
|
||||
<option value="json">JSON(json)</option>
|
||||
</select>
|
||||
</div>
|
||||
<label class="checkline tiny" style="margin-top: 20px;">
|
||||
<input id="raw-recursive" type="checkbox" checked />
|
||||
<span>递归扫描</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="btns" style="margin-top: 8px;">
|
||||
<button class="btn p" id="raw-submit">提交本地扫描任务</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="panel-openie" class="panel">
|
||||
<div class="row">
|
||||
<div>
|
||||
<label>路径别名</label>
|
||||
<select id="openie-alias"></select>
|
||||
</div>
|
||||
<div>
|
||||
<label>相对路径(relative_path)</label>
|
||||
<input id="openie-relative-path" placeholder="OpenIE 目录或文件" />
|
||||
</div>
|
||||
</div>
|
||||
<label class="checkline tiny">
|
||||
<input id="openie-include-all" type="checkbox" />
|
||||
<span>找不到 *-openie.json 时回退全部 .json</span>
|
||||
</label>
|
||||
<div class="btns" style="margin-top: 8px;">
|
||||
<button class="btn p" id="openie-submit">提交 OpenIE 任务</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="panel-convert" class="panel">
|
||||
<div class="row">
|
||||
<div>
|
||||
<label>输入别名</label>
|
||||
<select id="convert-alias"></select>
|
||||
</div>
|
||||
<div>
|
||||
<label>输入相对路径(relative_path)</label>
|
||||
<input id="convert-relative-path" placeholder="LPMM 数据目录" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>
|
||||
<label>目标别名</label>
|
||||
<select id="convert-target-alias"></select>
|
||||
</div>
|
||||
<div>
|
||||
<label>目标相对路径(relative_path)</label>
|
||||
<input id="convert-target-relative-path" placeholder="可选:目标子目录" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row3">
|
||||
<div>
|
||||
<label>向量维度(dimension)</label>
|
||||
<input id="convert-dimension" type="number" min="1" placeholder="默认读取配置" />
|
||||
</div>
|
||||
<div>
|
||||
<label>批大小(batch_size)</label>
|
||||
<input id="convert-batch-size" type="number" min="1" value="1024" />
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="tiny">将执行 staging 转换与切换,请确认输入目录正确。</div>
|
||||
<div class="btns" style="margin-top: 8px;">
|
||||
<button class="btn warn" id="convert-submit">提交 LPMM 转换任务</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="panel-backfill" class="panel">
|
||||
<div class="row">
|
||||
<div>
|
||||
<label>路径别名</label>
|
||||
<select id="backfill-alias"></select>
|
||||
</div>
|
||||
<div>
|
||||
<label>相对路径(relative_path)</label>
|
||||
<input id="backfill-relative-path" placeholder="默认插件 data 目录" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row3">
|
||||
<div>
|
||||
<label>处理上限(limit)</label>
|
||||
<input id="backfill-limit" type="number" min="1" value="100000" />
|
||||
</div>
|
||||
<label class="checkline tiny" style="margin-top: 20px;">
|
||||
<input id="backfill-dry-run" type="checkbox" />
|
||||
<span>仅预览(dry-run)</span>
|
||||
</label>
|
||||
<label class="checkline tiny" style="margin-top: 20px;">
|
||||
<input id="backfill-no-created" type="checkbox" />
|
||||
<span>禁用 created 回退(no-created-fallback)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="btns" style="margin-top: 8px;">
|
||||
<button class="btn p" id="backfill-submit">提交时序回填任务</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="panel-maibot" class="panel">
|
||||
<div class="row">
|
||||
<div>
|
||||
<label>源数据库路径</label>
|
||||
<input id="maibot-source-db" placeholder="data/MaiBot.db" />
|
||||
</div>
|
||||
<div>
|
||||
<label>时间范围(可选)</label>
|
||||
<input id="maibot-time-from" placeholder="from: YYYY-MM-DD HH:mm" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>
|
||||
<label>时间范围(可选)</label>
|
||||
<input id="maibot-time-to" placeholder="to: YYYY-MM-DD HH:mm" />
|
||||
</div>
|
||||
<div>
|
||||
<label>ID范围(可选)</label>
|
||||
<input id="maibot-start-id" type="number" min="1" placeholder="start_id" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>
|
||||
<label>ID范围(可选)</label>
|
||||
<input id="maibot-end-id" type="number" min="1" placeholder="end_id" />
|
||||
</div>
|
||||
<div>
|
||||
<label>stream_ids(逗号分隔)</label>
|
||||
<input id="maibot-stream-ids" placeholder="stream1,stream2" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>
|
||||
<label>group_ids(逗号分隔)</label>
|
||||
<input id="maibot-group-ids" placeholder="123456,234567" />
|
||||
</div>
|
||||
<div>
|
||||
<label>user_ids(逗号分隔)</label>
|
||||
<input id="maibot-user-ids" placeholder="10001,10002" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row3">
|
||||
<div>
|
||||
<label>读取批大小(read_batch_size)</label>
|
||||
<input id="maibot-read-batch-size" type="number" min="1" value="2000" />
|
||||
</div>
|
||||
<div>
|
||||
<label>提交窗口行数(commit_window_rows)</label>
|
||||
<input id="maibot-commit-window-rows" type="number" min="1" value="20000" />
|
||||
</div>
|
||||
<div>
|
||||
<label>嵌入工作线程(embed_workers)</label>
|
||||
<input id="maibot-embed-workers" type="number" min="1" placeholder="默认读取配置" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="checkgrid tiny">
|
||||
<label class="checkline">
|
||||
<input id="maibot-no-resume" type="checkbox" />
|
||||
<span>禁用断点续传(--no-resume)</span>
|
||||
</label>
|
||||
<label class="checkline">
|
||||
<input id="maibot-reset-state" type="checkbox" />
|
||||
<span>重置迁移状态(--reset-state)</span>
|
||||
</label>
|
||||
<label class="checkline">
|
||||
<input id="maibot-dry-run" type="checkbox" />
|
||||
<span>仅预览(--dry-run)</span>
|
||||
</label>
|
||||
<label class="checkline">
|
||||
<input id="maibot-verify-only" type="checkbox" />
|
||||
<span>仅校验(--verify-only)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="btns" style="margin-top: 8px;">
|
||||
<button class="btn p" id="maibot-submit">提交 MaiBot 迁移任务</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top: 12px;">
|
||||
<div class="hd"><span>任务队列</span><span class="tiny" id="poll-meta">轮询: 1000ms</span></div>
|
||||
<div class="bd">
|
||||
<div class="btns" style="margin-bottom: 9px;">
|
||||
<button class="btn" id="refresh-btn">立即刷新</button>
|
||||
<button class="btn err" id="cancel-btn">取消任务</button>
|
||||
<button class="btn warn" id="retry-btn">重试失败项(分块优先)</button>
|
||||
</div>
|
||||
<div class="tiny">运行中 / 准备中</div>
|
||||
<div class="task-list" id="tasks-running"></div>
|
||||
<div class="tiny" style="margin-top: 8px;">排队中</div>
|
||||
<div class="task-list" id="tasks-queued"></div>
|
||||
<div class="tiny" style="margin-top: 8px;">最近完成</div>
|
||||
<div class="task-list" id="tasks-recent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="hd">任务详情</div>
|
||||
<div class="bd">
|
||||
<div id="task-empty" class="tiny">请选择任务查看详情</div>
|
||||
<div id="task-body" style="display: none;">
|
||||
<div class="row">
|
||||
<div><div class="tiny">任务 ID</div><div id="d-id" style="font-size: 12px;"></div></div>
|
||||
<div><div class="tiny">状态 / 步骤</div><div id="d-status"></div></div>
|
||||
</div>
|
||||
<div class="bar"><div id="d-progress"></div></div>
|
||||
<div class="tiny" id="d-progress-text" style="margin-top: 5px;"></div>
|
||||
<div class="mgrid">
|
||||
<div class="m"><div class="k">总分块</div><div class="v" id="m-total">0</div></div>
|
||||
<div class="m"><div class="k">完成</div><div class="v" id="m-done">0</div></div>
|
||||
<div class="m"><div class="k">失败</div><div class="v" id="m-fail">0</div></div>
|
||||
<div class="m"><div class="k">取消</div><div class="v" id="m-cancel">0</div></div>
|
||||
</div>
|
||||
<div id="d-error" style="color: #fca5a5; margin-top: 6px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top: 12px;">
|
||||
<div class="hd">文件级状态</div>
|
||||
<div class="bd">
|
||||
<table>
|
||||
<thead><tr><th>文件</th><th>类型</th><th>状态</th><th>步骤</th><th>进度</th><th>统计</th></tr></thead>
|
||||
<tbody id="files-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top: 12px;">
|
||||
<div class="hd">分块级状态</div>
|
||||
<div class="bd">
|
||||
<table>
|
||||
<thead><tr><th>#</th><th>类型</th><th>状态</th><th>步骤</th><th>预览</th><th>错误</th></tr></thead>
|
||||
<tbody id="chunks-body"></tbody>
|
||||
</table>
|
||||
<div class="foot">
|
||||
<div class="tiny" id="chunk-meta">0 / 0</div>
|
||||
<div class="btns">
|
||||
<button class="btn" id="chunk-prev">上一页</button>
|
||||
<button class="btn" id="chunk-next">下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const state = { files: [], tasks: [], task: null, taskId: null, fileId: null, chunkOffset: 0, chunkLimit: 100, chunkTotal: 0, settings: {}, pathAliases: {}, pollMs: 1000, timer: null, pollErrSig: "", pollErrAt: 0 };
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const esc = (s) => String(s ?? "").replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'");
|
||||
const pct = (n) => `${(Math.max(0, Math.min(1, Number(n || 0))) * 100).toFixed(1)}%`;
|
||||
const badgeClass = (s) => ["running","preparing","cancel_requested"].includes(s) ? "b-run" : s==="queued" ? "b-q" : ["completed","completed_with_errors"].includes(s) ? "b-ok" : s==="failed" ? "b-err" : s==="cancelled" ? "b-cancel" : "";
|
||||
const STATUS_ZH = {
|
||||
queued: "排队中",
|
||||
preparing: "准备中",
|
||||
running: "运行中",
|
||||
cancel_requested: "取消中",
|
||||
cancelled: "已取消",
|
||||
completed: "已完成",
|
||||
completed_with_errors: "完成(有错误)",
|
||||
failed: "失败",
|
||||
splitting: "分块中",
|
||||
extracting: "抽取中",
|
||||
writing: "写入中",
|
||||
saving: "保存中",
|
||||
};
|
||||
const STEP_ZH = {
|
||||
queued: "排队中",
|
||||
scanning: "扫描中",
|
||||
preparing: "准备中",
|
||||
splitting: "分块中",
|
||||
extracting: "抽取中",
|
||||
writing: "写入中",
|
||||
saving: "保存中",
|
||||
converting: "转换中",
|
||||
verifying: "校验中",
|
||||
switching: "切换中",
|
||||
backfilling: "回填中",
|
||||
cancel_requested: "取消中",
|
||||
cancelled: "已取消",
|
||||
completed: "已完成",
|
||||
completed_with_errors: "完成(有错误)",
|
||||
failed: "失败",
|
||||
};
|
||||
const STRATEGY_ZH = {
|
||||
auto: "自动",
|
||||
narrative: "叙事",
|
||||
factual: "事实",
|
||||
quote: "引用",
|
||||
json: "JSON",
|
||||
text: "文本",
|
||||
};
|
||||
const TASK_KIND_ZH = {
|
||||
upload: "上传文件",
|
||||
paste: "粘贴导入",
|
||||
raw_scan: "本地扫描",
|
||||
lpmm_openie: "LPMM OpenIE",
|
||||
lpmm_convert: "LPMM 转换",
|
||||
temporal_backfill: "时序回填",
|
||||
maibot_migration: "MaiBot 迁移",
|
||||
};
|
||||
const SCHEMA_ZH = {
|
||||
web_json: "Web JSON",
|
||||
script_json: "脚本 JSON",
|
||||
lpmm_openie: "LPMM OpenIE",
|
||||
plain_text: "纯文本",
|
||||
};
|
||||
const SOURCE_ZH = {
|
||||
upload: "上传文件",
|
||||
paste: "粘贴导入",
|
||||
raw_scan: "本地扫描",
|
||||
lpmm_openie: "LPMM OpenIE",
|
||||
lpmm_convert: "LPMM 转换",
|
||||
temporal_backfill: "时序回填",
|
||||
maibot_migration: "MaiBot 迁移",
|
||||
};
|
||||
const chunkTypeZh = (v) => STRATEGY_ZH[String(v || "").trim()] || String(v || "-");
|
||||
const statusZh = (v) => STATUS_ZH[String(v || "").trim()] || String(v || "-");
|
||||
const stepZh = (v) => STEP_ZH[String(v || "").trim()] || String(v || "-");
|
||||
const taskKindZh = (v) => TASK_KIND_ZH[String(v || "").trim()] || String(v || "-");
|
||||
const schemaZh = (v) => SCHEMA_ZH[String(v || "").trim()] || String(v || "-");
|
||||
const sourceZh = (v) => SOURCE_ZH[String(v || "").trim()] || String(v || "-");
|
||||
|
||||
function toast(msg) { const t=$("toast"); t.textContent=msg; t.style.display="block"; clearTimeout(window._tt); window._tt=setTimeout(()=>t.style.display="none",2200); }
|
||||
function token(){ return String(localStorage.getItem("memorix_import_token") || "").trim(); }
|
||||
function headers(isJson=true){ const h={}; if(isJson) h["Content-Type"]="application/json"; const tk=token(); if(tk) h["X-Memorix-Import-Token"]=tk; return h; }
|
||||
async function req(path,opt={}){ const r=await fetch(path,opt); let b=null; try{b=await r.json();}catch{} if(!r.ok) throw new Error(typeof b?.detail==="string"?b.detail:`请求失败(HTTP ${r.status})`); return b||{}; }
|
||||
const parseCsvList = (v) => String(v || "").split(",").map(x => x.trim()).filter(Boolean);
|
||||
const numOrNull = (v) => { const t = String(v ?? "").trim(); if(!t) return null; const n = Number(t); return Number.isFinite(n) ? n : null; };
|
||||
|
||||
function renderGuideMarkdown(md){
|
||||
const src = String(md || "").replace(/\r\n/g, "\n");
|
||||
const codeBlocks = [];
|
||||
let text = src.replace(/```([\s\S]*?)```/g, (_, code) => {
|
||||
const idx = codeBlocks.length;
|
||||
codeBlocks.push(`<pre><code>${esc(code).trim()}</code></pre>`);
|
||||
return `@@CODE_${idx}@@`;
|
||||
});
|
||||
text = esc(text);
|
||||
text = text.replace(/^###\s+(.+)$/gm, "<h3>$1</h3>");
|
||||
text = text.replace(/^##\s+(.+)$/gm, "<h2>$1</h2>");
|
||||
text = text.replace(/^#\s+(.+)$/gm, "<h1>$1</h1>");
|
||||
text = text.replace(/^>\s+(.+)$/gm, "<blockquote>$1</blockquote>");
|
||||
text = text.replace(/^\s*[-*]\s+(.+)$/gm, "<li>$1</li>");
|
||||
text = text.replace(/(?:<li>[\s\S]*?<\/li>\s*)+/g, (m) => `<ul>${m}</ul>`);
|
||||
text = text.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
||||
text = text.replace(/`([^`]+)`/g, "<code>$1</code>");
|
||||
text = text.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
|
||||
text = text.split(/\n{2,}/).map((blk) => {
|
||||
const b = blk.trim();
|
||||
if(!b) return "";
|
||||
if(/^@@CODE_\d+@@$/.test(b)) return b;
|
||||
if(/^<(h1|h2|h3|ul|pre|blockquote)/.test(b)) return b;
|
||||
return `<p>${b.replace(/\n/g, "<br/>")}</p>`;
|
||||
}).join("");
|
||||
text = text.replace(/@@CODE_(\d+)@@/g, (_, i) => codeBlocks[Number(i)] || "");
|
||||
return text || `<p class="tiny">文档为空</p>`;
|
||||
}
|
||||
|
||||
async function openGuideModal(){
|
||||
$("guide-modal").classList.add("show");
|
||||
$("guide-meta").textContent = "";
|
||||
$("guide-body").innerHTML = `<p class="tiny">正在加载文档...</p>`;
|
||||
try{
|
||||
const d = await req("/api/import/guide", { headers: headers(false) });
|
||||
const srcText = d.source === "remote" ? "远程" : "本地";
|
||||
const sourceDesc = d.source === "remote" ? (d.url || "") : (d.path || "");
|
||||
$("guide-meta").textContent = `来源: ${srcText}${sourceDesc ? ` | ${sourceDesc}` : ""}`;
|
||||
$("guide-body").innerHTML = renderGuideMarkdown(d.content || "");
|
||||
}catch(e){
|
||||
$("guide-meta").textContent = "";
|
||||
$("guide-body").innerHTML = `<p style="color:#fca5a5;">文档加载失败: ${esc(e.message || "")}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function closeGuideModal(){ $("guide-modal").classList.remove("show"); }
|
||||
|
||||
function commonParams(){
|
||||
const mf=Number(state.settings.max_file_concurrency || 6), mc=Number(state.settings.max_chunk_concurrency || 12);
|
||||
const fc=Math.max(1,Math.min(mf,Number($("file-concurrency").value||2)));
|
||||
const cc=Math.max(1,Math.min(mc,Number($("chunk-concurrency").value||4)));
|
||||
$("file-concurrency").value=fc; $("chunk-concurrency").value=cc;
|
||||
return {
|
||||
file_concurrency: fc,
|
||||
chunk_concurrency: cc,
|
||||
llm_enabled: $("llm-enabled").checked,
|
||||
strategy_override: $("strategy-override").value,
|
||||
dedupe_policy: $("dedupe-policy").value,
|
||||
chat_log: !!$("chat-log").checked,
|
||||
chat_reference_time: ($("chat-reference-time").value || "").trim() || null,
|
||||
force: !!$("force-reimport").checked,
|
||||
clear_manifest: !!$("clear-manifest").checked,
|
||||
};
|
||||
}
|
||||
|
||||
function renderUploadFiles(){
|
||||
$("upload-count").textContent=`已选择 ${state.files.length} 个文件`;
|
||||
$("upload-file-hint").textContent = state.files.length ? `已选择 ${state.files.length} 个文件` : "未选择文件";
|
||||
$("upload-files").innerHTML = state.files.length ? state.files.map((f,i)=>`<div class="list-item"><div><div>${esc(f.name)}</div><div class="tiny">${(f.size/1024).toFixed(1)} KB</div></div><button class="btn" data-rm="${i}">移除</button></div>`).join("") : `<div class="tiny">暂无文件</div>`;
|
||||
}
|
||||
|
||||
function populateAliasSelects(){
|
||||
const aliases = state.pathAliases || {};
|
||||
const keys = Object.keys(aliases);
|
||||
const html = keys.length ? keys.map((k)=>`<option value="${esc(k)}">${esc(k)} (${esc(aliases[k])})</option>`).join("") : `<option value="">(无可用路径别名)</option>`;
|
||||
["raw-alias","openie-alias","convert-alias","convert-target-alias","backfill-alias"].forEach((id)=>{
|
||||
const el=$(id); if(!el) return;
|
||||
const old=el.value;
|
||||
el.innerHTML=html;
|
||||
if(old && keys.includes(old)) el.value=old;
|
||||
if(!el.value){
|
||||
if(id==="raw-alias" && keys.includes("raw")) el.value="raw";
|
||||
else if(id==="openie-alias" && keys.includes("lpmm")) el.value="lpmm";
|
||||
else if(id==="convert-alias" && keys.includes("lpmm")) el.value="lpmm";
|
||||
else if(id==="convert-target-alias" && keys.includes("plugin_data")) el.value="plugin_data";
|
||||
else if(id==="backfill-alias" && keys.includes("plugin_data")) el.value="plugin_data";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderTaskLists(){
|
||||
const g={run:[],q:[],r:[]};
|
||||
for(const t of state.tasks){ if(["running","preparing","cancel_requested"].includes(t.status)) g.run.push(t); else if(t.status==="queued") g.q.push(t); else g.r.push(t); }
|
||||
const render = (list) => list.length ? list.map(t=>`<div class="task ${state.taskId===t.task_id?"active":""}" data-tid="${t.task_id}"><div style="display:flex;justify-content:space-between;gap:8px;"><span>${esc(t.task_id.slice(0,12))}</span><span class="badge ${badgeClass(t.status)}">${esc(statusZh(t.status))}</span></div><div class="tiny">来源=${esc(sourceZh(t.source))} | 步骤=${esc(stepZh(t.current_step||"-"))}</div><div class="bar"><div style="width:${pct(t.progress)}"></div></div><div class="tiny">${pct(t.progress)} | ${t.done_chunks}/${t.total_chunks}</div></div>`).join("") : `<div class="tiny">暂无任务</div>`;
|
||||
$("tasks-running").innerHTML=render(g.run); $("tasks-queued").innerHTML=render(g.q); $("tasks-recent").innerHTML=render(g.r);
|
||||
}
|
||||
|
||||
async function loadTasks(){
|
||||
const d=await req("/api/import/tasks?limit=80",{headers:headers(false)});
|
||||
state.tasks=Array.isArray(d.items)?d.items:[]; state.settings=d.settings||state.settings||{};
|
||||
state.pathAliases = state.settings.path_aliases || state.pathAliases || {};
|
||||
const p=Number(state.settings.poll_interval_ms||1000); if(p!==state.pollMs){ state.pollMs=Math.max(200,p); restartPoll(); }
|
||||
$("poll-meta").textContent=`轮询: ${state.pollMs}ms`;
|
||||
if(state.settings.default_file_concurrency && !$("file-concurrency").dataset.touched) $("file-concurrency").value=state.settings.default_file_concurrency;
|
||||
if(state.settings.default_chunk_concurrency && !$("chunk-concurrency").dataset.touched) $("chunk-concurrency").value=state.settings.default_chunk_concurrency;
|
||||
if(state.settings.max_file_concurrency) $("file-concurrency").max=state.settings.max_file_concurrency;
|
||||
if(state.settings.max_chunk_concurrency) $("chunk-concurrency").max=state.settings.max_chunk_concurrency;
|
||||
if(state.settings.maibot_source_db_default && !$("maibot-source-db").dataset.touched) $("maibot-source-db").value=state.settings.maibot_source_db_default;
|
||||
populateAliasSelects();
|
||||
if(!state.taskId && state.tasks.length) state.taskId=state.tasks[0].task_id;
|
||||
if(state.taskId && !state.tasks.some(t=>t.task_id===state.taskId)){ state.taskId=state.tasks.length?state.tasks[0].task_id:null; state.fileId=null; }
|
||||
renderTaskLists();
|
||||
}
|
||||
|
||||
function renderTaskDetail(task){
|
||||
if(!task){ $("task-empty").style.display="block"; $("task-body").style.display="none"; $("files-body").innerHTML=`<tr><td colspan="6" class="tiny">暂无数据</td></tr>`; $("chunks-body").innerHTML=`<tr><td colspan="6" class="tiny">暂无数据</td></tr>`; $("chunk-meta").textContent="0 / 0"; return; }
|
||||
$("task-empty").style.display="none"; $("task-body").style.display="block";
|
||||
$("d-id").textContent=task.task_id; $("d-status").innerHTML=`<span class="badge ${badgeClass(task.status)}">${esc(statusZh(task.status))}</span> <span class="tiny">步骤=${esc(stepZh(task.current_step||"-"))}</span>`;
|
||||
$("d-progress").style.width=pct(task.progress); $("d-progress-text").textContent=`${pct(task.progress)} | 完成=${task.done_chunks} 失败=${task.failed_chunks} 取消=${task.cancelled_chunks} 总计=${task.total_chunks}`;
|
||||
$("m-total").textContent=task.total_chunks||0; $("m-done").textContent=task.done_chunks||0; $("m-fail").textContent=task.failed_chunks||0; $("m-cancel").textContent=task.cancelled_chunks||0;
|
||||
const extraMeta = [];
|
||||
if(task.schema_detected) extraMeta.push(`识别模式=${schemaZh(task.schema_detected)}`);
|
||||
if(task.task_kind) extraMeta.push(`任务类型=${taskKindZh(task.task_kind)}`);
|
||||
if(task.retry_parent_task_id) extraMeta.push(`父任务=${task.retry_parent_task_id}`);
|
||||
if(task.retry_summary && Object.keys(task.retry_summary).length) extraMeta.push(`重试摘要=${JSON.stringify(task.retry_summary)}`);
|
||||
if(task.artifact_paths && Object.keys(task.artifact_paths).length) extraMeta.push(`产物=${JSON.stringify(task.artifact_paths)}`);
|
||||
if(task.rollback_info && Object.keys(task.rollback_info).length) extraMeta.push(`回滚=${JSON.stringify(task.rollback_info)}`);
|
||||
const errText = task.error ? `错误: ${task.error}` : "";
|
||||
$("d-error").textContent = [errText, ...extraMeta].filter(Boolean).join(" | ");
|
||||
const files=Array.isArray(task.files)?task.files:[];
|
||||
$("files-body").innerHTML=files.length?files.map(f=>`<tr class="pick ${state.fileId===f.file_id?"active":""}" data-fid="${f.file_id}"><td>${esc(f.name)}</td><td>${esc(chunkTypeZh(f.detected_strategy_type||"-"))}</td><td><span class="badge ${badgeClass(f.status)}">${esc(statusZh(f.status))}</span></td><td>${esc(stepZh(f.current_step||"-"))}</td><td><div class="bar"><div style="width:${pct(f.progress)}"></div></div><div class="tiny">${pct(f.progress)}</div></td><td>${f.done_chunks}/${f.total_chunks} (失败:${f.failed_chunks} 取消:${f.cancelled_chunks})</td></tr>`).join(""):`<tr><td colspan="6" class="tiny">暂无文件</td></tr>`;
|
||||
}
|
||||
|
||||
async function loadTaskDetail(){
|
||||
if(!state.taskId){ renderTaskDetail(null); return; }
|
||||
const d=await req(`/api/import/tasks/${encodeURIComponent(state.taskId)}`,{headers:headers(false)});
|
||||
state.task=d.task||null; const files=Array.isArray(state.task?.files)?state.task.files:[]; if(!state.fileId || !files.some(x=>x.file_id===state.fileId)){ state.fileId=files.length?files[0].file_id:null; state.chunkOffset=0; }
|
||||
renderTaskDetail(state.task);
|
||||
}
|
||||
|
||||
async function loadChunks(){
|
||||
if(!state.taskId || !state.fileId){ $("chunks-body").innerHTML=`<tr><td colspan="6" class="tiny">请选择文件</td></tr>`; $("chunk-meta").textContent="0 / 0"; return; }
|
||||
const d=await req(`/api/import/tasks/${encodeURIComponent(state.taskId)}/files/${encodeURIComponent(state.fileId)}/chunks?offset=${state.chunkOffset}&limit=${state.chunkLimit}`,{headers:headers(false)});
|
||||
const it=Array.isArray(d.items)?d.items:[]; state.chunkTotal=Number(d.total||0);
|
||||
$("chunks-body").innerHTML=it.length?it.map(c=>`<tr><td>${c.index}</td><td>${esc(chunkTypeZh(c.chunk_type||"-"))}</td><td><span class="badge ${badgeClass(c.status)}">${esc(statusZh(c.status))}</span></td><td>${esc(stepZh(c.step||"-"))}</td><td title="${esc(c.content_preview||"")}">${esc(c.content_preview||"").slice(0,90)}</td><td style="color:#fca5a5">${esc(c.error||"")}</td></tr>`).join(""):`<tr><td colspan="6" class="tiny">暂无分块</td></tr>`;
|
||||
const from=state.chunkTotal?state.chunkOffset+1:0, to=Math.min(state.chunkOffset+state.chunkLimit,state.chunkTotal); $("chunk-meta").textContent=`${from}-${to} / ${state.chunkTotal}`;
|
||||
}
|
||||
|
||||
async function createUploadTask(){
|
||||
if(!state.files.length){ toast("请先选择文件"); return; }
|
||||
const fd=new FormData(); state.files.forEach(f=>fd.append("files[]",f,f.name)); fd.append("payload",JSON.stringify({ ...commonParams(), input_mode:$("upload-input-mode").value }));
|
||||
const d=await req("/api/import/tasks/upload",{method:"POST",headers:headers(false),body:fd}); if(d?.task?.task_id) state.taskId=d.task.task_id; state.files=[]; renderUploadFiles(); toast("上传任务已创建");
|
||||
}
|
||||
|
||||
async function createPasteTask(){
|
||||
const content=$("paste-content").value||""; if(!content.trim()){ toast("粘贴内容不能为空"); return; }
|
||||
const d=await req("/api/import/tasks/paste",{method:"POST",headers:headers(true),body:JSON.stringify({ ...commonParams(), input_mode:$("paste-input-mode").value, content, name:$("paste-name").value||"" })});
|
||||
if(d?.task?.task_id) state.taskId=d.task.task_id; toast("粘贴任务已创建");
|
||||
}
|
||||
|
||||
async function createRawScanTask(){
|
||||
const payload = {
|
||||
...commonParams(),
|
||||
alias: $("raw-alias").value,
|
||||
relative_path: ($("raw-relative-path").value || "").trim(),
|
||||
glob: ($("raw-glob").value || "*").trim() || "*",
|
||||
recursive: !!$("raw-recursive").checked,
|
||||
input_mode: $("raw-input-mode").value,
|
||||
};
|
||||
const d=await req("/api/import/tasks/raw_scan",{method:"POST",headers:headers(true),body:JSON.stringify(payload)});
|
||||
if(d?.task?.task_id) state.taskId=d.task.task_id;
|
||||
toast("本地扫描任务已创建");
|
||||
}
|
||||
|
||||
async function createLpmmOpenieTask(){
|
||||
const payload = {
|
||||
...commonParams(),
|
||||
alias: $("openie-alias").value,
|
||||
relative_path: ($("openie-relative-path").value || "").trim(),
|
||||
include_all_json: !!$("openie-include-all").checked,
|
||||
};
|
||||
const d=await req("/api/import/tasks/lpmm_openie",{method:"POST",headers:headers(true),body:JSON.stringify(payload)});
|
||||
if(d?.task?.task_id) state.taskId=d.task.task_id;
|
||||
toast("LPMM OpenIE 任务已创建");
|
||||
}
|
||||
|
||||
async function createLpmmConvertTask(){
|
||||
if(!confirm("该任务会执行 staging 转换并切换 vectors/graph/metadata,是否继续?")) return;
|
||||
const payload = {
|
||||
alias: $("convert-alias").value,
|
||||
relative_path: ($("convert-relative-path").value || "").trim(),
|
||||
target_alias: $("convert-target-alias").value,
|
||||
target_relative_path: ($("convert-target-relative-path").value || "").trim(),
|
||||
dimension: numOrNull($("convert-dimension").value),
|
||||
batch_size: numOrNull($("convert-batch-size").value),
|
||||
};
|
||||
const d=await req("/api/import/tasks/lpmm_convert",{method:"POST",headers:headers(true),body:JSON.stringify(payload)});
|
||||
if(d?.task?.task_id) state.taskId=d.task.task_id;
|
||||
toast("LPMM 转换任务已创建");
|
||||
}
|
||||
|
||||
async function createTemporalBackfillTask(){
|
||||
const payload = {
|
||||
alias: $("backfill-alias").value,
|
||||
relative_path: ($("backfill-relative-path").value || "").trim(),
|
||||
dry_run: !!$("backfill-dry-run").checked,
|
||||
no_created_fallback: !!$("backfill-no-created").checked,
|
||||
limit: numOrNull($("backfill-limit").value),
|
||||
};
|
||||
const d=await req("/api/import/tasks/temporal_backfill",{method:"POST",headers:headers(true),body:JSON.stringify(payload)});
|
||||
if(d?.task?.task_id) state.taskId=d.task.task_id;
|
||||
toast("时序回填任务已创建");
|
||||
}
|
||||
|
||||
async function createMaibotMigrationTask(){
|
||||
const payload = {
|
||||
source_db: ($("maibot-source-db").value || "").trim() || null,
|
||||
time_from: ($("maibot-time-from").value || "").trim() || null,
|
||||
time_to: ($("maibot-time-to").value || "").trim() || null,
|
||||
stream_ids: parseCsvList($("maibot-stream-ids").value),
|
||||
group_ids: parseCsvList($("maibot-group-ids").value),
|
||||
user_ids: parseCsvList($("maibot-user-ids").value),
|
||||
start_id: numOrNull($("maibot-start-id").value),
|
||||
end_id: numOrNull($("maibot-end-id").value),
|
||||
read_batch_size: numOrNull($("maibot-read-batch-size").value),
|
||||
commit_window_rows: numOrNull($("maibot-commit-window-rows").value),
|
||||
embed_workers: numOrNull($("maibot-embed-workers").value),
|
||||
no_resume: !!$("maibot-no-resume").checked,
|
||||
reset_state: !!$("maibot-reset-state").checked,
|
||||
dry_run: !!$("maibot-dry-run").checked,
|
||||
verify_only: !!$("maibot-verify-only").checked,
|
||||
};
|
||||
const d = await req("/api/import/tasks/maibot_migration", {
|
||||
method: "POST",
|
||||
headers: headers(true),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if(d?.task?.task_id) state.taskId = d.task.task_id;
|
||||
toast("MaiBot 迁移任务已创建");
|
||||
}
|
||||
|
||||
async function cancelTask(){ if(!state.taskId){ toast("请先选择任务"); return; } await req(`/api/import/tasks/${encodeURIComponent(state.taskId)}/cancel`,{method:"POST",headers:headers(false)}); toast("已请求取消"); }
|
||||
async function retryTask(){
|
||||
if(!state.taskId){ toast("请先选择任务"); return; }
|
||||
const d=await req(`/api/import/tasks/${encodeURIComponent(state.taskId)}/retry_failed`,{method:"POST",headers:headers(true),body:JSON.stringify(commonParams())});
|
||||
if(d?.task?.task_id) state.taskId=d.task.task_id;
|
||||
const rs = d?.retry_summary || d?.task?.retry_summary || {};
|
||||
const chunkFiles = Number(rs?.chunk_retry_files || 0);
|
||||
const chunkCount = Number(rs?.chunk_retry_chunks || 0);
|
||||
const fileFallback = Number(rs?.file_fallback_files || 0);
|
||||
toast(`重试任务已创建:分块重试 ${chunkFiles} 文件/${chunkCount} 分块,文件回退 ${fileFallback} 文件`);
|
||||
}
|
||||
|
||||
async function poll(){
|
||||
try{
|
||||
await loadTasks(); await loadTaskDetail(); await loadChunks();
|
||||
state.pollErrSig = "";
|
||||
}catch(e){
|
||||
const sig = String(e.message || "请求失败");
|
||||
const now = Date.now();
|
||||
if(sig !== state.pollErrSig || now - state.pollErrAt > 5000){
|
||||
toast(`请求失败: ${sig}`);
|
||||
state.pollErrSig = sig;
|
||||
state.pollErrAt = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
function restartPoll(){ if(state.timer) clearInterval(state.timer); state.timer=setInterval(poll,state.pollMs); }
|
||||
|
||||
function bind(){
|
||||
$("token-input").value=token();
|
||||
$("token-save").onclick=()=>{ localStorage.setItem("memorix_import_token",String($("token-input").value||"").trim()); toast("Token 已保存"); };
|
||||
$("token-clear").onclick=()=>{ localStorage.removeItem("memorix_import_token"); $("token-input").value=""; toast("Token 已清空"); };
|
||||
$("guide-open").onclick=()=>openGuideModal();
|
||||
$("guide-close").onclick=()=>closeGuideModal();
|
||||
$("guide-modal").onclick=(e)=>{ if(e.target === $("guide-modal")) closeGuideModal(); };
|
||||
window.addEventListener("keydown",(e)=>{ if(e.key==="Escape" && $("guide-modal").classList.contains("show")) closeGuideModal(); });
|
||||
const tabs = ["upload","paste","raw","openie","convert","backfill","maibot"];
|
||||
const activateTab = (name) => {
|
||||
tabs.forEach((t)=>{
|
||||
const tab = $(`tab-${t}`);
|
||||
const panel = $(`panel-${t}`);
|
||||
if(tab) tab.classList.toggle("active", t===name);
|
||||
if(panel) panel.classList.toggle("active", t===name);
|
||||
});
|
||||
if(["raw","openie"].includes(name)){
|
||||
$("dedupe-policy").value = "manifest";
|
||||
}else if(["upload","paste"].includes(name)){
|
||||
$("dedupe-policy").value = "content_hash";
|
||||
}
|
||||
};
|
||||
tabs.forEach((t)=>{ const tab = $(`tab-${t}`); if(tab) tab.onclick=()=>activateTab(t); });
|
||||
$("file-concurrency").oninput=(e)=>e.target.dataset.touched="1"; $("chunk-concurrency").oninput=(e)=>e.target.dataset.touched="1";
|
||||
$("maibot-source-db").oninput=(e)=>e.target.dataset.touched="1";
|
||||
$("upload-file-pick").onclick=()=>$("upload-file-input").click();
|
||||
$("upload-file-input").onchange=(e)=>{ Array.from(e.target.files||[]).forEach(f=>state.files.push(f)); e.target.value=""; renderUploadFiles(); };
|
||||
$("upload-files").onclick=(e)=>{ const i=e.target?.dataset?.rm; if(i===undefined) return; state.files.splice(Number(i),1); renderUploadFiles(); };
|
||||
$("upload-clear").onclick=()=>{ state.files=[]; renderUploadFiles(); };
|
||||
$("upload-submit").onclick=async()=>{ try{ await createUploadTask(); await poll(); }catch(e){ toast(`上传失败: ${e.message}`); } };
|
||||
$("paste-submit").onclick=async()=>{ try{ await createPasteTask(); await poll(); }catch(e){ toast(`粘贴失败: ${e.message}`); } };
|
||||
$("raw-submit").onclick=async()=>{ try{ await createRawScanTask(); await poll(); }catch(e){ toast(`本地扫描失败: ${e.message}`); } };
|
||||
$("openie-submit").onclick=async()=>{ try{ await createLpmmOpenieTask(); await poll(); }catch(e){ toast(`OpenIE 导入失败: ${e.message}`); } };
|
||||
$("convert-submit").onclick=async()=>{ try{ await createLpmmConvertTask(); await poll(); }catch(e){ toast(`LPMM 转换失败: ${e.message}`); } };
|
||||
$("backfill-submit").onclick=async()=>{ try{ await createTemporalBackfillTask(); await poll(); }catch(e){ toast(`回填任务失败: ${e.message}`); } };
|
||||
$("maibot-submit").onclick=async()=>{ try{ await createMaibotMigrationTask(); await poll(); }catch(e){ toast(`迁移任务创建失败: ${e.message}`); } };
|
||||
$("refresh-btn").onclick=async()=>{ await poll(); toast("已刷新"); };
|
||||
$("cancel-btn").onclick=async()=>{ try{ await cancelTask(); await poll(); }catch(e){ toast(`取消失败: ${e.message}`); } };
|
||||
$("retry-btn").onclick=async()=>{ try{ await retryTask(); await poll(); }catch(e){ toast(`重试失败: ${e.message}`); } };
|
||||
["tasks-running","tasks-queued","tasks-recent"].forEach(id=>$(id).onclick=async(e)=>{ const n=e.target.closest("[data-tid]"); if(!n) return; state.taskId=n.dataset.tid; state.fileId=null; state.chunkOffset=0; renderTaskLists(); try{ await loadTaskDetail(); await loadChunks(); }catch(err){ toast(`加载失败: ${err.message}`); } });
|
||||
$("files-body").onclick=async(e)=>{ const n=e.target.closest("[data-fid]"); if(!n) return; state.fileId=n.dataset.fid; state.chunkOffset=0; renderTaskDetail(state.task); try{ await loadChunks(); }catch(err){ toast(`加载分块失败: ${err.message}`); } };
|
||||
$("chunk-prev").onclick=async()=>{ if(state.chunkOffset<=0) return; state.chunkOffset=Math.max(0,state.chunkOffset-state.chunkLimit); try{ await loadChunks(); }catch(e){ toast(`翻页失败: ${e.message}`); } };
|
||||
$("chunk-next").onclick=async()=>{ if(state.chunkOffset+state.chunkLimit>=state.chunkTotal) return; state.chunkOffset+=state.chunkLimit; try{ await loadChunks(); }catch(e){ toast(`翻页失败: ${e.message}`); } };
|
||||
}
|
||||
|
||||
bind(); renderUploadFiles(); poll(); restartPoll();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +0,0 @@
|
||||
function hexToRgba(hex, alpha) {
|
||||
let c;
|
||||
if (/^#([A-Fa-f0-9]{3}){1,2}$/.test(hex)) {
|
||||
c = hex.substring(1).split('');
|
||||
if (c.length === 3) {
|
||||
c = [c[0], c[0], c[1], c[1], c[2], c[2]];
|
||||
}
|
||||
c = '0x' + c.join('');
|
||||
return 'rgba(' + [(c >> 16) & 255, (c >> 8) & 255, c & 255].join(',') + ',' + alpha + ')';
|
||||
}
|
||||
// Fallback if not hex (e.g. already rgba or invalid)
|
||||
return hex;
|
||||
}
|
||||
@@ -1,722 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>A_Memorix 检索调优中心</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #08101f;
|
||||
--panel: #0f1a2f;
|
||||
--border: #28415f;
|
||||
--text: #dbeafe;
|
||||
--muted: #93c5fd;
|
||||
--pri: #22d3ee;
|
||||
--ok: #34d399;
|
||||
--warn: #fbbf24;
|
||||
--err: #f87171;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
background: radial-gradient(circle at 0 0, #12335d, transparent 34%), var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
.page { width: min(1500px, 96vw); margin: 18px auto 28px; }
|
||||
.top { display: flex; justify-content: space-between; gap: 12px; align-items: end; margin-bottom: 12px; }
|
||||
.title { font-size: 24px; font-weight: 700; }
|
||||
.sub { color: var(--muted); font-size: 12px; margin-top: 4px; }
|
||||
.btns { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.grid { display: grid; grid-template-columns: 460px 1fr; gap: 12px; }
|
||||
.card { background: rgba(15, 26, 47, 0.94); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }
|
||||
.hd { display: flex; justify-content: space-between; align-items: center; padding: 10px 12px; border-bottom: 1px solid var(--border); color: var(--pri); font-size: 13px; }
|
||||
.bd { padding: 12px; }
|
||||
label { display: block; color: var(--muted); font-size: 12px; margin-bottom: 4px; }
|
||||
input, select, textarea, button { font: inherit; }
|
||||
input, select, textarea {
|
||||
width: 100%;
|
||||
background: #0a1425;
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
textarea { min-height: 120px; resize: vertical; }
|
||||
.row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 8px; }
|
||||
.row3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; margin-bottom: 8px; }
|
||||
.btn {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: #152841;
|
||||
color: var(--text);
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn.pri {
|
||||
border: none;
|
||||
color: #022f4d;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(120deg, #22d3ee, #67e8f9);
|
||||
}
|
||||
.btn.warn { color: var(--warn); border-color: #854d0e; }
|
||||
.btn.err { color: var(--err); border-color: #7f1d1d; }
|
||||
.tiny { color: var(--muted); font-size: 12px; }
|
||||
.mono {
|
||||
font-family: Consolas, Menlo, Monaco, monospace;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.split { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
||||
.task-list { max-height: 280px; overflow: auto; border: 1px solid var(--border); border-radius: 8px; padding: 6px; }
|
||||
.task { border: 1px solid var(--border); background: #0a1425; border-radius: 8px; padding: 8px; margin-bottom: 7px; cursor: pointer; }
|
||||
.task:last-child { margin-bottom: 0; }
|
||||
.task.active { border-color: var(--pri); }
|
||||
.task .line { display: flex; justify-content: space-between; gap: 8px; margin-bottom: 4px; }
|
||||
.badge { border: 1px solid var(--border); border-radius: 999px; padding: 1px 8px; font-size: 11px; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
||||
th, td { text-align: left; border-bottom: 1px solid var(--border); padding: 7px 4px; vertical-align: top; }
|
||||
#toast { position: fixed; top: 10px; left: 50%; transform: translateX(-50%); z-index: 100; display: none; padding: 8px 12px; border: 1px solid var(--border); border-radius: 8px; background: #0f172a; }
|
||||
.cmp-modal-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(2, 6, 23, 0.72);
|
||||
z-index: 120;
|
||||
padding: 16px;
|
||||
}
|
||||
.cmp-modal-mask.show { display: flex; }
|
||||
.cmp-modal {
|
||||
width: min(1080px, 96vw);
|
||||
max-height: 90vh;
|
||||
background: #0a1425;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.cmp-hd {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
color: var(--pri);
|
||||
font-size: 14px;
|
||||
}
|
||||
.cmp-bd {
|
||||
padding: 12px;
|
||||
overflow: auto;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
.cmp-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
.cmp-metric {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
background: #0f1a2f;
|
||||
}
|
||||
.cmp-metric .k {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
.cmp-metric .v {
|
||||
margin-top: 4px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.cmp-delta-pos { color: var(--ok); }
|
||||
.cmp-delta-neg { color: var(--err); }
|
||||
.cmp-bars {
|
||||
margin-top: 6px;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
.cmp-bar-line {
|
||||
display: grid;
|
||||
grid-template-columns: 54px 1fr;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
.cmp-bar-wrap {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
height: 8px;
|
||||
overflow: hidden;
|
||||
background: #08101f;
|
||||
}
|
||||
.cmp-bar-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(120deg, #22d3ee, #67e8f9);
|
||||
}
|
||||
.cmp-subcard {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
background: #0f1a2f;
|
||||
}
|
||||
@media (max-width: 1150px) {
|
||||
.grid { grid-template-columns: 1fr; }
|
||||
.split { grid-template-columns: 1fr; }
|
||||
.row, .row3 { grid-template-columns: 1fr; }
|
||||
.cmp-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="toast"></div>
|
||||
<div id="cmp-modal-mask" class="cmp-modal-mask">
|
||||
<div class="cmp-modal">
|
||||
<div class="cmp-hd">
|
||||
<span>调优完成对比:Baseline vs Best</span>
|
||||
<button class="btn" id="cmp-close-btn">关闭</button>
|
||||
</div>
|
||||
<div class="cmp-bd">
|
||||
<div class="tiny" id="cmp-summary"></div>
|
||||
<div class="cmp-grid" id="cmp-metrics"></div>
|
||||
<div class="cmp-subcard">
|
||||
<div style="font-weight: 700; margin-bottom: 6px;">分类召回/精度对比</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>分类</th>
|
||||
<th>Baseline 召回</th>
|
||||
<th>Best 召回</th>
|
||||
<th>Δ召回</th>
|
||||
<th>Baseline P@1</th>
|
||||
<th>Best P@1</th>
|
||||
<th>ΔP@1</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="cmp-category-table"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page">
|
||||
<div class="top">
|
||||
<div>
|
||||
<div class="title">A_Memorix 检索调优中心</div>
|
||||
<div class="sub">LLM 辅助 + 多轮调查 + 运行时参数应用(不自动写 config.toml)</div>
|
||||
</div>
|
||||
<div class="btns">
|
||||
<button class="btn" onclick="window.open('/', '_blank')">打开主面板</button>
|
||||
<button class="btn" onclick="window.open('/import', '_blank')">打开导入中心</button>
|
||||
<button class="btn pri" id="refresh-all">刷新</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="hd">当前运行时参数</div>
|
||||
<div class="bd">
|
||||
<div class="tiny" id="settings-tip"></div>
|
||||
<pre id="current-profile" class="mono"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top: 10px;">
|
||||
<div class="hd">手动调参与应用</div>
|
||||
<div class="bd">
|
||||
<label>参数 JSON(支持局部字段)</label>
|
||||
<textarea id="manual-profile" class="mono"></textarea>
|
||||
<div class="btns" style="margin-top: 8px;">
|
||||
<button class="btn pri" id="btn-apply">应用到运行时</button>
|
||||
<button class="btn warn" id="btn-rollback">回滚上次应用</button>
|
||||
<button class="btn" id="btn-export">导出 TOML 片段</button>
|
||||
</div>
|
||||
<label style="margin-top: 10px;">TOML 导出</label>
|
||||
<textarea id="toml-snippet" class="mono" style="min-height: 90px;"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top: 10px;">
|
||||
<div class="hd">创建自动调优任务</div>
|
||||
<div class="bd">
|
||||
<div class="row">
|
||||
<div>
|
||||
<label>目标函数</label>
|
||||
<select id="objective">
|
||||
<option value="precision_priority">precision_priority</option>
|
||||
<option value="balanced">balanced</option>
|
||||
<option value="recall_priority">recall_priority</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>强度</label>
|
||||
<select id="intensity">
|
||||
<option value="standard">standard</option>
|
||||
<option value="quick">quick</option>
|
||||
<option value="deep">deep</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row3">
|
||||
<div>
|
||||
<label>轮次(可选)</label>
|
||||
<input id="rounds" type="number" min="1" max="200" placeholder="留空走强度默认" />
|
||||
</div>
|
||||
<div>
|
||||
<label>样本数</label>
|
||||
<input id="sample-size" type="number" min="4" max="500" value="24" />
|
||||
</div>
|
||||
<div>
|
||||
<label>评估 top_k</label>
|
||||
<input id="top-k-eval" type="number" min="5" max="100" value="20" />
|
||||
</div>
|
||||
</div>
|
||||
<label style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
||||
<input id="llm-enabled" type="checkbox" checked style="width: 16px; height: 16px;" />
|
||||
<span>启用 LLM 问题生成/失败模式建议(不可用自动退化)</span>
|
||||
</label>
|
||||
<div class="btns">
|
||||
<button class="btn pri" id="btn-create-task">创建任务</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="hd">
|
||||
<span>任务队列</span>
|
||||
<span class="tiny" id="task-meta"></span>
|
||||
</div>
|
||||
<div class="bd split">
|
||||
<div>
|
||||
<div class="task-list" id="task-list"></div>
|
||||
</div>
|
||||
<div>
|
||||
<label>任务详情</label>
|
||||
<pre id="task-detail" class="mono" style="min-height: 160px; max-height: 250px; overflow: auto; border: 1px solid var(--border); border-radius: 8px; padding: 8px;"></pre>
|
||||
<div class="btns" style="margin-top: 8px;">
|
||||
<button class="btn warn" id="btn-cancel-task">取消任务</button>
|
||||
<button class="btn pri" id="btn-apply-best">应用最优参数</button>
|
||||
<button class="btn" id="btn-load-report">加载报告</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top: 10px;">
|
||||
<div class="hd">
|
||||
<span>轮次明细</span>
|
||||
<span class="tiny" id="round-meta"></span>
|
||||
</div>
|
||||
<div class="bd">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Round</th>
|
||||
<th>Score</th>
|
||||
<th>P@1</th>
|
||||
<th>P@3</th>
|
||||
<th>MRR</th>
|
||||
<th>Recall@K</th>
|
||||
<th>SPO hit</th>
|
||||
<th>Empty</th>
|
||||
<th>Latency(ms)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="round-table"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top: 10px;">
|
||||
<div class="hd">报告预览</div>
|
||||
<div class="bd">
|
||||
<textarea id="report-content" class="mono" style="min-height: 240px;"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const state = {
|
||||
settings: null,
|
||||
tasks: [],
|
||||
selectedTaskId: null,
|
||||
pollTimer: null,
|
||||
taskStatusMap: {},
|
||||
watchTaskIds: new Set(),
|
||||
completionPopupShown: new Set(),
|
||||
};
|
||||
|
||||
function _num(v, fallback = 0) {
|
||||
const x = Number(v);
|
||||
return Number.isFinite(x) ? x : fallback;
|
||||
}
|
||||
|
||||
function _pct(v) {
|
||||
return `${(_num(v, 0) * 100).toFixed(2)}%`;
|
||||
}
|
||||
|
||||
function _fmt(v, digits = 4) {
|
||||
return _num(v, 0).toFixed(digits);
|
||||
}
|
||||
|
||||
function _deltaClass(delta, reverse = false) {
|
||||
const d = _num(delta, 0);
|
||||
if (Math.abs(d) < 1e-12) return "";
|
||||
const positive = reverse ? d < 0 : d > 0;
|
||||
return positive ? "cmp-delta-pos" : "cmp-delta-neg";
|
||||
}
|
||||
|
||||
function _renderMetricCard({ name, base, best, percent = true, reverse = false }) {
|
||||
const b = _num(base, 0);
|
||||
const n = _num(best, 0);
|
||||
const d = n - b;
|
||||
const bTxt = percent ? _pct(b) : _fmt(b, 3);
|
||||
const nTxt = percent ? _pct(n) : _fmt(n, 3);
|
||||
const dTxt = `${d >= 0 ? "+" : ""}${percent ? _pct(d) : _fmt(d, 3)}`;
|
||||
const dClass = _deltaClass(d, reverse);
|
||||
const bWidth = percent ? Math.max(0, Math.min(100, b * 100)) : 100;
|
||||
const nWidth = percent ? Math.max(0, Math.min(100, n * 100)) : 100;
|
||||
return `
|
||||
<div class="cmp-metric">
|
||||
<div class="k">${name}</div>
|
||||
<div class="v">${bTxt} -> ${nTxt}</div>
|
||||
<div class="${dClass}" style="font-size:12px;">Δ ${dTxt}</div>
|
||||
<div class="cmp-bars">
|
||||
<div class="cmp-bar-line">
|
||||
<div class="tiny">Base</div>
|
||||
<div class="cmp-bar-wrap"><div class="cmp-bar-fill" style="width:${bWidth}%;opacity:0.75;"></div></div>
|
||||
</div>
|
||||
<div class="cmp-bar-line">
|
||||
<div class="tiny">Best</div>
|
||||
<div class="cmp-bar-wrap"><div class="cmp-bar-fill" style="width:${nWidth}%;"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function hideCompletionPopup() {
|
||||
document.getElementById("cmp-modal-mask").classList.remove("show");
|
||||
}
|
||||
|
||||
function showCompletionPopupContent(task) {
|
||||
const baseline = task.baseline_metrics || {};
|
||||
const best = task.best_metrics || {};
|
||||
const roundsDone = Number(task.rounds_done || 0);
|
||||
const roundsTotal = Number(task.rounds_total || 0);
|
||||
const summary = `任务 ${String(task.task_id || "").slice(0, 8)} 已完成,目标=${task.objective || "-"},轮次=${roundsDone}/${roundsTotal},best_score=${_fmt(task.best_score || 0, 6)}`;
|
||||
document.getElementById("cmp-summary").textContent = summary;
|
||||
|
||||
const metricCards = [
|
||||
{ name: "Precision@1", base: baseline.precision_at_1, best: best.precision_at_1, percent: true },
|
||||
{ name: "Precision@3", base: baseline.precision_at_3, best: best.precision_at_3, percent: true },
|
||||
{ name: "Recall@K", base: baseline.recall_at_k, best: best.recall_at_k, percent: true },
|
||||
{ name: "MRR", base: baseline.mrr, best: best.mrr, percent: true },
|
||||
{ name: "SPO Relation Hit", base: baseline.spo_relation_hit_rate, best: best.spo_relation_hit_rate, percent: true },
|
||||
{ name: "Empty Rate", base: baseline.empty_rate, best: best.empty_rate, percent: true, reverse: true },
|
||||
];
|
||||
document.getElementById("cmp-metrics").innerHTML = metricCards.map(_renderMetricCard).join("");
|
||||
|
||||
const baseCat = baseline.category || {};
|
||||
const bestCat = best.category || {};
|
||||
const keys = Array.from(new Set([...Object.keys(baseCat), ...Object.keys(bestCat)])).sort();
|
||||
const rows = [];
|
||||
for (const k of keys) {
|
||||
const b = baseCat[k] || {};
|
||||
const n = bestCat[k] || {};
|
||||
const bTot = Math.max(1, Number(b.total || 0));
|
||||
const nTot = Math.max(1, Number(n.total || 0));
|
||||
const bRecall = Number(b.hit || 0) / bTot;
|
||||
const nRecall = Number(n.hit || 0) / nTot;
|
||||
const bP1 = Number(b.hit_at_1 || 0) / bTot;
|
||||
const nP1 = Number(n.hit_at_1 || 0) / nTot;
|
||||
const dRecall = nRecall - bRecall;
|
||||
const dP1 = nP1 - bP1;
|
||||
rows.push(`
|
||||
<tr>
|
||||
<td>${k}</td>
|
||||
<td>${_pct(bRecall)} (${Number(b.hit || 0)}/${Number(b.total || 0)})</td>
|
||||
<td>${_pct(nRecall)} (${Number(n.hit || 0)}/${Number(n.total || 0)})</td>
|
||||
<td class="${_deltaClass(dRecall)}">${dRecall >= 0 ? "+" : ""}${_pct(dRecall)}</td>
|
||||
<td>${_pct(bP1)}</td>
|
||||
<td>${_pct(nP1)}</td>
|
||||
<td class="${_deltaClass(dP1)}">${dP1 >= 0 ? "+" : ""}${_pct(dP1)}</td>
|
||||
</tr>
|
||||
`);
|
||||
}
|
||||
document.getElementById("cmp-category-table").innerHTML = rows.join("") || '<tr><td colspan="7" class="tiny">无分类指标</td></tr>';
|
||||
document.getElementById("cmp-modal-mask").classList.add("show");
|
||||
}
|
||||
|
||||
async function tryShowCompletionPopup(taskId) {
|
||||
if (!taskId || state.completionPopupShown.has(taskId)) return;
|
||||
const body = await req(`/api/retrieval_tuning/tasks/${taskId}`);
|
||||
const task = body.task || {};
|
||||
if (task.status !== "completed") return;
|
||||
showCompletionPopupContent(task);
|
||||
state.completionPopupShown.add(taskId);
|
||||
}
|
||||
|
||||
function toast(msg, level = "info") {
|
||||
const el = document.getElementById("toast");
|
||||
el.style.display = "block";
|
||||
el.style.borderColor = level === "error" ? "#7f1d1d" : level === "warn" ? "#854d0e" : "#28415f";
|
||||
el.textContent = msg;
|
||||
setTimeout(() => { el.style.display = "none"; }, 2200);
|
||||
}
|
||||
|
||||
async function req(url, options = {}) {
|
||||
const resp = await fetch(url, options);
|
||||
const body = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok || body.success === false) {
|
||||
throw new Error(body.detail || body.error || body.message || `HTTP ${resp.status}`);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
function pretty(obj) {
|
||||
return JSON.stringify(obj, null, 2);
|
||||
}
|
||||
|
||||
async function loadProfile() {
|
||||
const body = await req("/api/retrieval_tuning/profile");
|
||||
state.settings = body.settings || {};
|
||||
document.getElementById("settings-tip").textContent = `默认目标=${state.settings.default_objective},默认强度=${state.settings.default_intensity},轮询=${state.settings.poll_interval_ms}ms`;
|
||||
document.getElementById("current-profile").textContent = pretty(body.profile || {});
|
||||
document.getElementById("manual-profile").value = pretty(body.profile || {});
|
||||
|
||||
if (state.settings.default_objective) document.getElementById("objective").value = state.settings.default_objective;
|
||||
if (state.settings.default_intensity) document.getElementById("intensity").value = state.settings.default_intensity;
|
||||
if (state.settings.default_sample_size) document.getElementById("sample-size").value = state.settings.default_sample_size;
|
||||
if (state.settings.default_top_k_eval) document.getElementById("top-k-eval").value = state.settings.default_top_k_eval;
|
||||
}
|
||||
|
||||
function renderTaskList() {
|
||||
const list = document.getElementById("task-list");
|
||||
list.innerHTML = "";
|
||||
for (const task of state.tasks) {
|
||||
const div = document.createElement("div");
|
||||
div.className = `task${task.task_id === state.selectedTaskId ? " active" : ""}`;
|
||||
div.onclick = () => {
|
||||
state.selectedTaskId = task.task_id;
|
||||
renderTaskList();
|
||||
loadTaskDetail();
|
||||
};
|
||||
div.innerHTML = `
|
||||
<div class="line"><span>${task.task_id.slice(0, 8)}</span><span class="badge">${task.status}</span></div>
|
||||
<div class="line tiny"><span>${task.objective}</span><span>${Math.round((task.progress || 0) * 100)}%</span></div>
|
||||
<div class="tiny">round ${task.rounds_done || 0}/${task.rounds_total || 0}, best=${(task.best_score || 0).toFixed(4)}</div>
|
||||
`;
|
||||
list.appendChild(div);
|
||||
}
|
||||
document.getElementById("task-meta").textContent = `共 ${state.tasks.length} 个任务`;
|
||||
}
|
||||
|
||||
async function loadTasks() {
|
||||
const body = await req("/api/retrieval_tuning/tasks?limit=100");
|
||||
state.tasks = body.items || [];
|
||||
const prevMap = { ...state.taskStatusMap };
|
||||
state.taskStatusMap = {};
|
||||
let toPopupTaskId = null;
|
||||
for (const t of state.tasks) {
|
||||
state.taskStatusMap[t.task_id] = t.status;
|
||||
if (
|
||||
state.watchTaskIds.has(t.task_id) &&
|
||||
t.status === "completed" &&
|
||||
prevMap[t.task_id] &&
|
||||
prevMap[t.task_id] !== "completed" &&
|
||||
!state.completionPopupShown.has(t.task_id)
|
||||
) {
|
||||
toPopupTaskId = t.task_id;
|
||||
}
|
||||
}
|
||||
if (!state.selectedTaskId && state.tasks.length) {
|
||||
state.selectedTaskId = state.tasks[0].task_id;
|
||||
} else if (state.selectedTaskId && !state.tasks.find(x => x.task_id === state.selectedTaskId)) {
|
||||
state.selectedTaskId = state.tasks.length ? state.tasks[0].task_id : null;
|
||||
}
|
||||
renderTaskList();
|
||||
if (toPopupTaskId) {
|
||||
await tryShowCompletionPopup(toPopupTaskId);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTaskDetail() {
|
||||
const taskId = state.selectedTaskId;
|
||||
if (!taskId) {
|
||||
document.getElementById("task-detail").textContent = "";
|
||||
document.getElementById("round-table").innerHTML = "";
|
||||
return;
|
||||
}
|
||||
const body = await req(`/api/retrieval_tuning/tasks/${taskId}`);
|
||||
const task = body.task || {};
|
||||
document.getElementById("task-detail").textContent = pretty(task);
|
||||
|
||||
const rounds = await req(`/api/retrieval_tuning/tasks/${taskId}/rounds?offset=0&limit=400`);
|
||||
const tb = document.getElementById("round-table");
|
||||
tb.innerHTML = "";
|
||||
for (const row of rounds.items || []) {
|
||||
const m = row.metrics || {};
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<td>${row.round_index}</td>
|
||||
<td>${Number(row.score || 0).toFixed(4)}</td>
|
||||
<td>${Number(m.precision_at_1 || 0).toFixed(4)}</td>
|
||||
<td>${Number(m.precision_at_3 || 0).toFixed(4)}</td>
|
||||
<td>${Number(m.mrr || 0).toFixed(4)}</td>
|
||||
<td>${Number(m.recall_at_k || 0).toFixed(4)}</td>
|
||||
<td>${Number(m.spo_relation_hit_rate || 0).toFixed(4)}</td>
|
||||
<td>${Number(m.empty_rate || 0).toFixed(4)}</td>
|
||||
<td>${Number(row.latency_ms || 0).toFixed(2)}</td>
|
||||
`;
|
||||
tb.appendChild(tr);
|
||||
}
|
||||
document.getElementById("round-meta").textContent = `total ${rounds.total || 0}`;
|
||||
}
|
||||
|
||||
async function loadReport() {
|
||||
if (!state.selectedTaskId) return;
|
||||
const body = await req(`/api/retrieval_tuning/tasks/${state.selectedTaskId}/report?format=md`);
|
||||
document.getElementById("report-content").value = body.content || "";
|
||||
}
|
||||
|
||||
async function refreshAll() {
|
||||
try {
|
||||
await loadProfile();
|
||||
await loadTasks();
|
||||
await loadTaskDetail();
|
||||
} catch (e) {
|
||||
toast(e.message || String(e), "error");
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("refresh-all").onclick = refreshAll;
|
||||
|
||||
document.getElementById("btn-apply").onclick = async () => {
|
||||
try {
|
||||
const profile = JSON.parse(document.getElementById("manual-profile").value || "{}");
|
||||
await req("/api/retrieval_tuning/profile/apply", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ profile, reason: "web_manual_apply" }),
|
||||
});
|
||||
toast("参数已应用");
|
||||
await refreshAll();
|
||||
} catch (e) {
|
||||
toast(e.message || String(e), "error");
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById("btn-rollback").onclick = async () => {
|
||||
try {
|
||||
await req("/api/retrieval_tuning/profile/rollback", { method: "POST" });
|
||||
toast("已回滚");
|
||||
await refreshAll();
|
||||
} catch (e) {
|
||||
toast(e.message || String(e), "error");
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById("btn-export").onclick = async () => {
|
||||
try {
|
||||
const body = await req("/api/retrieval_tuning/profile/export_toml");
|
||||
document.getElementById("toml-snippet").value = body.toml || "";
|
||||
toast("已导出 TOML");
|
||||
} catch (e) {
|
||||
toast(e.message || String(e), "error");
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById("btn-create-task").onclick = async () => {
|
||||
try {
|
||||
const rounds = document.getElementById("rounds").value.trim();
|
||||
const payload = {
|
||||
objective: document.getElementById("objective").value,
|
||||
intensity: document.getElementById("intensity").value,
|
||||
sample_size: Number(document.getElementById("sample-size").value || 24),
|
||||
top_k_eval: Number(document.getElementById("top-k-eval").value || 20),
|
||||
llm_enabled: document.getElementById("llm-enabled").checked,
|
||||
};
|
||||
if (rounds) payload.rounds = Number(rounds);
|
||||
const body = await req("/api/retrieval_tuning/tasks", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
toast("任务已创建");
|
||||
const newTaskId = body.task?.task_id || "";
|
||||
if (newTaskId) {
|
||||
state.watchTaskIds.add(newTaskId);
|
||||
state.selectedTaskId = newTaskId;
|
||||
}
|
||||
await refreshAll();
|
||||
} catch (e) {
|
||||
toast(e.message || String(e), "error");
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById("btn-cancel-task").onclick = async () => {
|
||||
if (!state.selectedTaskId) return;
|
||||
try {
|
||||
await req(`/api/retrieval_tuning/tasks/${state.selectedTaskId}/cancel`, { method: "POST" });
|
||||
toast("已发送取消请求", "warn");
|
||||
await refreshAll();
|
||||
} catch (e) {
|
||||
toast(e.message || String(e), "error");
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById("btn-apply-best").onclick = async () => {
|
||||
if (!state.selectedTaskId) return;
|
||||
try {
|
||||
await req(`/api/retrieval_tuning/tasks/${state.selectedTaskId}/apply_best`, { method: "POST" });
|
||||
toast("最优参数已应用");
|
||||
await refreshAll();
|
||||
} catch (e) {
|
||||
toast(e.message || String(e), "error");
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById("btn-load-report").onclick = async () => {
|
||||
try {
|
||||
await loadReport();
|
||||
toast("报告已加载");
|
||||
} catch (e) {
|
||||
toast(e.message || String(e), "error");
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById("cmp-close-btn").onclick = hideCompletionPopup;
|
||||
document.getElementById("cmp-modal-mask").onclick = (e) => {
|
||||
if (e.target && e.target.id === "cmp-modal-mask") {
|
||||
hideCompletionPopup();
|
||||
}
|
||||
};
|
||||
|
||||
function startPolling() {
|
||||
const ms = Number(state.settings?.poll_interval_ms || 1200);
|
||||
if (state.pollTimer) clearInterval(state.pollTimer);
|
||||
state.pollTimer = setInterval(async () => {
|
||||
try {
|
||||
await loadTasks();
|
||||
await loadTaskDetail();
|
||||
} catch (_) {}
|
||||
}, Math.max(400, ms));
|
||||
}
|
||||
|
||||
(async () => {
|
||||
await refreshAll();
|
||||
startPolling();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user