luckytri 1.2.3 → 1.2.4

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.
Files changed (30) hide show
  1. package/README.en.md +1 -1
  2. package/README.md +1 -1
  3. package/docs/en/CHANGELOG.md +5 -0
  4. package/docs/zh/CHANGELOG.md +5 -0
  5. package/package.json +1 -1
  6. package/public/app/assets/{ChatsPage-o-exjdFu.js → ChatsPage-C_gKz4mJ.js} +1 -1
  7. package/public/app/assets/{Empty-uSyRBXOT.js → Empty-y1vXDIjQ.js} +1 -1
  8. package/public/app/assets/{Field-Bfa5dCkk.js → Field-CJG5lERx.js} +1 -1
  9. package/public/app/assets/{HeartPage-Q5ts7Ko6.js → HeartPage-5y3avjSy.js} +1 -1
  10. package/public/app/assets/{LifePage-B4AsD_DM.js → LifePage-BtwRMDAM.js} +1 -1
  11. package/public/app/assets/{MemoryPage-ijD6lsxo.js → MemoryPage-54noDhWz.js} +1 -1
  12. package/public/app/assets/{NaturePage-B9-dv8vz.js → NaturePage-DHK5X2Qw.js} +1 -1
  13. package/public/app/assets/{PeoplePage-BG_XPian.js → PeoplePage-ZsA7gyxP.js} +1 -1
  14. package/public/app/assets/{PluginsPage-CPYYMown.js → PluginsPage-BCRRIAhq.js} +1 -1
  15. package/public/app/assets/{SystemPage-B9M9g95D.css → SystemPage-DNTD2_nj.css} +1 -1
  16. package/public/app/assets/SystemPage-DWr1TG49.js +1 -0
  17. package/public/app/assets/{TimePage-DScn8IN-.js → TimePage-6MUmtSOT.js} +1 -1
  18. package/public/app/assets/{index-5FMs9YYQ.js → index-BokqwNXF.js} +3 -3
  19. package/public/app/assets/{knowledge-Ces-Bx70.js → knowledge-BjD9PkXe.js} +1 -1
  20. package/public/app/assets/{media-BsG-i0Lv.js → media-Ciozo_yb.js} +1 -1
  21. package/public/app/assets/{mind-D3mdwFW3.js → mind-KkinyNC6.js} +1 -1
  22. package/public/app/index.html +3 -3
  23. package/public/guide.en.html +3 -3
  24. package/public/guide.html +3 -3
  25. package/server/app.js +1 -0
  26. package/server/i18n/errors.js +1 -0
  27. package/server/plugins/zip.js +32 -4
  28. package/server/stickers/api.js +22 -1
  29. package/server/stickers/zip-import.js +76 -0
  30. package/public/app/assets/SystemPage-CTduZhr3.js +0 -1
@@ -27,7 +27,19 @@ function safeName(name) {
27
27
 
28
28
  // Reads a zip the way a stranger's file has to be read: only stored and
29
29
  // deflate entries, no encryption, no links, no paths that leave the folder.
30
- export function readZip(buffer) {
30
+ //
31
+ // Options let another caller (the sticker library) take larger archives:
32
+ // maxFiles / maxBytes bound the whole archive; keep(name) skips an entry
33
+ // without inflating it; an entry over maxEntryBytes is reported, not read.
34
+ export function readZip(
35
+ buffer,
36
+ {
37
+ maxFiles = MAX_FILES,
38
+ maxBytes = MAX_BYTES,
39
+ maxEntryBytes = Infinity,
40
+ keep = () => true,
41
+ } = {},
42
+ ) {
31
43
  if (!Buffer.isBuffer(buffer) || buffer.length < 22)
32
44
  throw new Error("压缩包无法读取");
33
45
  const eocd = findEocd(buffer);
@@ -35,7 +47,7 @@ export function readZip(buffer) {
35
47
  if (buffer.readUInt16LE(eocd + 4) !== 0)
36
48
  throw new Error("压缩包格式不受支持");
37
49
  const entries = buffer.readUInt16LE(eocd + 10);
38
- if (!entries || entries > MAX_FILES) throw new Error("压缩包文件过多");
50
+ if (!entries || entries > maxFiles) throw new Error("压缩包文件过多");
39
51
  let offset = buffer.readUInt32LE(eocd + 16);
40
52
  if (offset >= buffer.length) throw new Error("压缩包无法读取");
41
53
  const files = [];
@@ -68,6 +80,11 @@ export function readZip(buffer) {
68
80
  if (method !== 0 && method !== 8) throw new Error("压缩包格式不受支持");
69
81
  if (name.endsWith("/") || name.endsWith("\\")) continue;
70
82
  const fileName = safeName(name);
83
+ if (!keep(fileName)) continue;
84
+ if (size > maxEntryBytes) {
85
+ files.push({ name: fileName, skipped: "too-large" });
86
+ continue;
87
+ }
71
88
  if (buffer.readUInt32LE(local) !== 0x04034b50)
72
89
  throw new Error("压缩包无法读取");
73
90
  const dataAt =
@@ -84,16 +101,27 @@ export function readZip(buffer) {
84
101
  method === 0
85
102
  ? Buffer.from(compressed)
86
103
  : inflateRawSync(compressed, {
87
- maxOutputLength: MAX_BYTES - total + 1,
104
+ maxOutputLength: Math.min(maxBytes - total, maxEntryBytes) + 1,
88
105
  });
89
106
  } catch (error) {
107
+ if (
108
+ error?.code === "ERR_BUFFER_TOO_LARGE" &&
109
+ maxEntryBytes < maxBytes - total
110
+ ) {
111
+ files.push({ name: fileName, skipped: "too-large" });
112
+ continue;
113
+ }
90
114
  if (error?.code === "ERR_BUFFER_TOO_LARGE")
91
115
  throw new Error("压缩包解压后过大");
92
116
  throw new Error("压缩包无法读取");
93
117
  }
94
118
  if (size && bytes.length !== size) throw new Error("压缩包内容不完整");
119
+ if (bytes.length > maxEntryBytes) {
120
+ files.push({ name: fileName, skipped: "too-large" });
121
+ continue;
122
+ }
95
123
  total += bytes.length;
96
- if (total > MAX_BYTES) throw new Error("压缩包解压后过大");
124
+ if (total > maxBytes) throw new Error("压缩包解压后过大");
97
125
  files.push({ name: fileName, bytes });
98
126
  }
99
127
  return files;
@@ -1,4 +1,5 @@
1
1
  import express from "express";
2
+ import { importStickerZip, stickerZipLimitMb } from "./zip-import.js";
2
3
 
3
4
  // The owner's sticker library. Uploads arrive as raw bytes, one file per
4
5
  // request; everything else is JSON like the rest of the studio API.
@@ -19,10 +20,28 @@ export function mountStickerUpload(app, system) {
19
20
  }
20
21
  },
21
22
  );
23
+ app.post(
24
+ "/api/core/stickers/upload-zip",
25
+ express.raw({ type: "*/*", limit: `${stickerZipLimitMb()}mb` }),
26
+ async (req, res) => {
27
+ try {
28
+ res.json(
29
+ await importStickerZip(system.stickers.library, req.body, {
30
+ category: String(req.query.category || ""),
31
+ folderCategory: req.query.folderCategory === "1",
32
+ queue: (id) => queueDescribe(system, id),
33
+ }),
34
+ );
35
+ } catch (error) {
36
+ res.status(400).json({ error: error.message });
37
+ }
38
+ },
39
+ );
22
40
  }
23
41
 
24
42
  const queue = [];
25
43
  let running = false;
44
+ let current = null; // the one being analysed right now still counts as pending
26
45
  function queueDescribe(system, id) {
27
46
  queue.push(id);
28
47
  if (running) return;
@@ -30,12 +49,14 @@ function queueDescribe(system, id) {
30
49
  (async () => {
31
50
  while (queue.length) {
32
51
  const next = queue.shift();
52
+ current = next;
33
53
  try {
34
54
  await system.stickers.describe(next, { force: true });
35
55
  } catch {
36
56
  // The item records its own failure; the owner can retry.
37
57
  }
38
58
  }
59
+ current = null;
39
60
  running = false;
40
61
  })();
41
62
  }
@@ -68,7 +89,7 @@ export function mountStickers(app, system) {
68
89
  }),
69
90
  categories: library.categories(),
70
91
  settings: system.stickers.settings(),
71
- pending: queue.length,
92
+ pending: queue.length + (current ? 1 : 0),
72
93
  }),
73
94
  ),
74
95
  );
@@ -0,0 +1,76 @@
1
+ import { readZip, stripRoot } from "../plugins/zip.js";
2
+ import { STICKER_MAX_BYTES } from "./library.js";
3
+
4
+ const MIB = 1024 * 1024;
5
+ const IMAGE = /\.(png|jpe?g|gif|webp)$/i;
6
+ const ARCHIVE_ROOT =
7
+ /^(?:pack(?:age)?|stickers?(?:[-_ ]?pack)?|emojis?|images?|my[-_ ]stickers|我的表情|表情包|表情)$/i;
8
+
9
+ function stripArchiveRoot(files) {
10
+ if (!files.length) return files;
11
+ const roots = new Set(files.map((file) => file.name.split("/")[0]));
12
+ if (roots.size !== 1 || !ARCHIVE_ROOT.test([...roots][0])) return files;
13
+ return stripRoot(files);
14
+ }
15
+
16
+ // Upload size of one sticker archive; STICKER_ZIP_MAX_MB overrides it.
17
+ export function stickerZipLimitMb(env = process.env) {
18
+ const n = Number(env.STICKER_ZIP_MAX_MB);
19
+ return Number.isFinite(n) && n > 0 ? Math.min(n, 2048) : 200;
20
+ }
21
+
22
+ const hidden = (name) =>
23
+ name.split("/").some((part) => part === "__MACOSX" || part.startsWith("."));
24
+
25
+ // Imports every image in an archive, in memory. Each new one joins the same
26
+ // one-time analysis queue as a single upload; nothing is written to disk.
27
+ export async function importStickerZip(
28
+ library,
29
+ buffer,
30
+ { category = "", folderCategory = false, queue = () => {} } = {},
31
+ env = process.env,
32
+ ) {
33
+ const limit = stickerZipLimitMb(env) * MIB;
34
+ if (!Buffer.isBuffer(buffer) || !buffer.length) throw Error("文件为空");
35
+ if (buffer.length > limit) throw Error("压缩包过大");
36
+ const summary = { imported: 0, duplicates: 0, skipped: [], failed: [] };
37
+ const files = stripArchiveRoot(
38
+ readZip(buffer, {
39
+ maxFiles: 5000,
40
+ maxBytes: Math.max(limit * 2, 400 * MIB),
41
+ maxEntryBytes: STICKER_MAX_BYTES,
42
+ keep: (name) => {
43
+ if (hidden(name)) return false;
44
+ if (!IMAGE.test(name)) {
45
+ summary.skipped.push({ name, reason: "不是图片" });
46
+ return false;
47
+ }
48
+ return true;
49
+ },
50
+ }),
51
+ );
52
+ for (const file of files) {
53
+ if (file.skipped) {
54
+ summary.skipped.push({ name: file.name, reason: "超过 2MB" });
55
+ continue;
56
+ }
57
+ const parts = file.name.split("/");
58
+ const base = parts.at(-1).replace(/\.[a-z0-9]+$/i, "");
59
+ // The first folder names the category: 开心/a.png, 开心/more/b.png.
60
+ const folder = parts.length > 1 ? parts[0] : "";
61
+ try {
62
+ const item = await library.add(file.bytes, {
63
+ name: base,
64
+ category: (folderCategory && folder) || category,
65
+ });
66
+ if (item.duplicate) summary.duplicates++;
67
+ else {
68
+ summary.imported++;
69
+ queue(item.id);
70
+ }
71
+ } catch (error) {
72
+ summary.failed.push({ name: file.name, reason: error.message });
73
+ }
74
+ }
75
+ return summary;
76
+ }
@@ -1 +0,0 @@
1
- import{Dt as e,E as t,Et as n,G as r,H as i,I as a,J as o,M as s,N as c,P as l,Q as u,S as d,St as f,T as p,Tt as m,V as h,W as g,X as _,Y as v,Z as y,_t as b,a as x,b as S,bt as C,dt as w,et as T,f as E,ft as D,gt as O,h as k,i as A,it as j,j as M,lt as N,m as P,n as F,o as I,p as L,pt as R,rt as z,s as B,st as V,t as ee,tt as H,ut as U,v as W,w as G,wt as K,xt as te,yt as q,z as J}from"./Empty-uSyRBXOT.js";import{v as ne}from"./mind-D3mdwFW3.js";import{c as Y,f as re,l as X,n as ie,o as ae}from"./index-5FMs9YYQ.js";var oe={class:`connect`},Z={class:`card connection`},Q={class:`card-head`},se={class:`eyebrow`},ce={class:`pill`},le={key:0,class:`notice`,role:`note`},ue={class:`muted`},de={class:`connection-details`},fe={class:`detail`},pe={class:`instructions`},me={class:`muted`},$={class:`muted note`},he={key:0,class:`row`},ge=[`disabled`],_e={class:`muted`},ve=[`disabled`,`placeholder`],ye={key:0,class:`faint`},be=[`disabled`,`placeholder`],xe={key:0,class:`faint`},Se={class:`row`},Ce=[`disabled`],we={key:0,class:`faint`},Te={key:0,class:`connection-details`},Ee={class:`detail`},De={"data-state":``},Oe={key:0,class:`detail`},ke={class:`warn-text`},Ae={key:1,class:`detail`},je={class:`detail`},Me={class:`detail`},Ne={key:1,class:`notice`,role:`note`},Pe={class:`instructions`},Fe={class:`muted notes`},Ie={class:`card checklist`},Le={class:`card-head`},Re={class:`eyebrow`},ze={class:`chip`},Be={class:`checks`},Ve={class:`mark`,"aria-hidden":`true`},He={class:`muted`},Ue=[`data-fix`,`onClick`],We=F(z({__name:`ConnectQQ`,emits:[`open`],setup(n,{emit:s}){let c=s,d=o(()=>B.health.connection||{}),p=o(()=>d.value.channel===`qqbot`?`qqbot`:`onebot`),h=o(()=>!!d.value.locked),_=o(()=>!!d.value.online);o(()=>d.value.onebot||{});let S=o(()=>d.value.qqbot||{}),E=o(()=>String(B.health.settings.qqbotAppId||``)),O=o(()=>!!B.health.settings.hasQqbotSecret),k=f(p.value);b(p,e=>k.value=e);let j=o(()=>`${location.protocol===`https:`?`wss`:`ws`}://${location.host}/onebot/v11/ws`),N=o(()=>[{key:`onebot`,label:a(`OneBot 11`),count:p.value===`onebot`?a(`使用中`):``},{key:`qqbot`,label:a(`QQ 官方机器人`),count:p.value===`qqbot`?a(`使用中`):``}]),F=te({appId:E.value,secret:``}),I=f(!1),L=o(()=>F.appId.trim()!==E.value||!!F.secret.trim());b(L,e=>{B.dirty=e}),b(E,e=>{L.value||(F.appId=e)}),U(()=>{B.dirty=!1});let R=o(()=>!!F.appId.trim()&&(O.value||!!F.secret.trim())),z=l({idle:M(`未启用`),unconfigured:M(`尚未填写 AppID 与 AppSecret`),connecting:M(`正在连接开放平台`),ready:M(`已连接`),closed:M(`连接已断开,正在重连`),error:M(`连接出错`)}),V=l({all:M(`全部群消息`),mentions:M(`只有 @ 她的消息`),unknown:M(`尚未收到群消息`)}),ee=o(()=>z[S.value.state||`idle`]??z.idle),G=o(()=>V[S.value.groupMessages||`unknown`]??V.unknown),q=o(()=>p.value===`qqbot`&&S.value.groupMessages===`mentions`);async function J(e){if(I.value||h.value||e===p.value)return;let n=a(e===`qqbot`?`QQ 官方机器人`:`OneBot 11`);if(!_.value||await t(a(`切换后会断开当前连接。原来方式下的会话仍可查看,但不能再向它们发送消息。`),{title:a(`切换连接方式`),confirmText:a(`切换`),cancelText:a(`先不切换`)})){I.value=!0;try{await P(`/settings`,`PATCH`,{channel:e}),await x(),W(a(`已切换为 {name}`,{name:n}))}catch(e){W(e.message,!0)}finally{I.value=!1}}}async function ne(){if(!I.value){if(!R.value){W(a(`请填写 AppID 和 AppSecret`),!0);return}I.value=!0;try{let e={qqbotAppId:F.appId.trim()};F.secret.trim()&&(e.qqbotSecret=F.secret.trim()),p.value!==`qqbot`&&!h.value&&(e.channel=`qqbot`),await P(`/settings`,`PATCH`,e),F.secret=``,await x(),W(a(`已保存`))}catch(e){W(e.message,!0)}finally{I.value=!1}}}let Y={token:[``,`connect`],credentials:[``,`connect`],qq:[``,`connect`],model:[``,`models`],mode:[``,`runtime`],session:[`chats`,``],persona:[`nature`,``]};function re(e){let[t,n]=Y[e]||[``,`connect`];t?A(t):c(`open`,n)}return(t,n)=>(w(),u(`div`,oe,[v(`section`,Z,[v(`div`,Q,[v(`div`,null,[v(`span`,se,e(K(a)(`QQ`)),1),v(`h2`,null,e(K(a)(`连接 QQ`)),1),v(`p`,null,e(K(a)(`两种连接方式二选一,同一时间只使用一种。她的记忆、心情和行为完全一致,区别只在消息怎么到达。`)),1)]),v(`span`,ce,[v(`span`,{class:m([`dot`,{off:!_.value}])},null,2),T(` `+e(_.value?K(a)(`QQ 已连接`):K(a)(`等待连接`)),1)])]),H(ie,{modelValue:k.value,"onUpdate:modelValue":n[0]||=e=>k.value=e,label:K(a)(`连接方式`),items:N.value},null,8,[`modelValue`,`label`,`items`]),h.value?(w(),u(`p`,le,e(K(a)(`连接方式由环境变量 LUCKYTRI_CHANNEL 指定,在这里不能更改。去掉该变量后即可在此选择。`)),1)):y(``,!0),k.value===`onebot`?(w(),u(r,{key:1},[v(`p`,ue,e(K(a)(`自备接入端,通过反向 WebSocket 连接。能看到全部群消息。`)),1),v(`div`,de,[v(`div`,fe,[v(`span`,null,e(K(a)(`反向 WebSocket 地址`)),1),v(`code`,null,e(j.value),1)])]),v(`ol`,pe,[v(`li`,null,e(K(a)(`同一台电脑上的接入端无需单独配置连接令牌。`)),1),v(`li`,null,e(K(a)(`在接入端启用反向 WebSocket 客户端,填写上方地址,消息格式选择数组。已有的地址配置可以继续使用。`)),1),v(`li`,null,e(K(a)(`在接入端完成 QQ 登录。连接成功后,本页状态会更新;新会话仍需在「对话」中开启参与。`)),1)]),v(`p`,me,e(K(a)(`远程接入时,连接密码使用管理密码;已有的远程连接令牌仍兼容。`)),1),v(`p`,$,e(K(a)(`LuckyTri 只负责接收,不提供、不安装也不管理接入端。`)),1),p.value===`onebot`?y(``,!0):(w(),u(`div`,he,[v(`button`,{class:`primary`,"data-use":`onebot`,disabled:I.value||h.value,onClick:n[1]||=e=>J(`onebot`)},e(K(a)(`改用这种方式`)),9,ge)]))],64)):(w(),u(r,{key:2},[v(`p`,_e,e(K(a)(`只需 AppID 与 AppSecret,无需额外软件。群里默认只收到 @ 她的消息。`)),1),v(`form`,{class:`stack tight`,onSubmit:g(ne,[`prevent`])},[v(`label`,null,[T(e(K(a)(`AppID`))+` `,1),C(v(`input`,{"onUpdate:modelValue":n[2]||=e=>F.appId=e,name:`qqbotAppId`,autocomplete:`off`,inputmode:`numeric`,maxlength:`40`,disabled:I.value||S.value.appIdFromEnv,placeholder:K(a)(`机器人的 AppID`)},null,8,ve),[[i,F.appId]]),S.value.appIdFromEnv?(w(),u(`small`,ye,e(K(a)(`由环境变量指定,在这里不能修改`)),1)):y(``,!0)]),v(`label`,null,[T(e(K(a)(`AppSecret`))+` `,1),C(v(`input`,{"onUpdate:modelValue":n[3]||=e=>F.secret=e,name:`qqbotSecret`,type:`password`,autocomplete:`new-password`,maxlength:`200`,disabled:I.value||S.value.secretFromEnv,placeholder:O.value||S.value.secretFromEnv?K(a)(`已保存,留空保留`):K(a)(`机器人的 AppSecret`)},null,8,be),[[i,F.secret]]),S.value.secretFromEnv?(w(),u(`small`,xe,e(K(a)(`由环境变量指定,在这里不能修改`)),1)):y(``,!0)]),v(`div`,Se,[v(`button`,{class:`primary`,"data-save":`qqbot`,disabled:I.value||S.value.appIdFromEnv&&S.value.secretFromEnv},e(p.value===`qqbot`||h.value?K(a)(`保存`):K(a)(`保存并使用`)),9,Ce),L.value?(w(),u(`span`,we,e(K(a)(`有尚未保存的修改`)),1)):y(``,!0)])],32),p.value===`qqbot`?(w(),u(`div`,Te,[v(`div`,Ee,[v(`span`,null,e(K(a)(`连接状态`)),1),v(`strong`,De,e(ee.value),1)]),S.value.error?(w(),u(`div`,Oe,[n[4]||=v(`span`,null,null,-1),v(`small`,ke,e(S.value.error),1)])):y(``,!0),S.value.botName?(w(),u(`div`,Ae,[v(`span`,null,e(K(a)(`机器人`)),1),v(`strong`,null,e(S.value.botName),1)])):y(``,!0),v(`div`,je,[v(`span`,null,e(K(a)(`最近收到消息`)),1),v(`strong`,null,e(S.value.lastEventAt?K(X)(S.value.lastEventAt):K(a)(`还没有`)),1)]),v(`div`,Me,[v(`span`,null,e(K(a)(`群消息范围`)),1),v(`strong`,null,e(G.value),1)])])):y(``,!0),q.value?(w(),u(`p`,Ne,e(K(a)(`目前只收到 @ 她的群消息。想让她看到群里的全部消息,请在开放平台的机器人设置里开启「接收所有消息」。`)),1)):y(``,!0),v(`ol`,Pe,[v(`li`,null,e(K(a)(`在 QQ 开放平台创建机器人,取得 AppID 与 AppSecret。`)),1),v(`li`,null,e(K(a)(`把运行 LuckyTri 的这台机器的出口 IP 加入机器人的 IP 白名单。`)),1),v(`li`,null,e(K(a)(`可选:开启「接收所有消息」,她才能看到群里没有 @ 她的话;不开启时只会收到 @ 她的消息。`)),1),v(`li`,null,e(K(a)(`填写上方的 AppID 与 AppSecret 并保存。状态变为「已连接」后,在群里 @ 她或私聊她,新会话会出现在「对话」中。`)),1)]),v(`ul`,Fe,[v(`li`,null,e(K(a)(`官方平台不提供群名称:新会话以占位名出现,可以在「对话」中为它改名。`)),1),v(`li`,null,e(K(a)(`官方平台上,同一个人在不同的群里标识不同。像只在不同的房间见过对方一样,平台给出统一身份之前,她不会把两处的人当成同一个人。`)),1),v(`li`,null,e(K(a)(`对方关闭了机器人的主动消息时,她不会去打扰,会等对方来找她。`)),1)])],64))]),v(`section`,Ie,[v(`div`,Le,[v(`div`,null,[v(`span`,Re,e(K(a)(`就绪检查`)),1),v(`h2`,null,e(K(a)(`还差哪一步`)),1)]),v(`span`,ze,e(K(B).health.readiness?.completed??0)+` / `+e(K(B).health.readiness?.total??0),1)]),v(`ol`,Be,[(w(!0),u(r,null,D(K(B).health.readiness?.checks||[],t=>(w(),u(`li`,{key:t.id,class:m({done:t.done})},[v(`span`,Ve,e(t.done?`✓`:``),1),v(`div`,null,[v(`b`,null,e(t.name),1),v(`small`,He,e(t.detail),1)]),v(`button`,{class:`small`,"data-fix":t.id,onClick:e=>re(t.id)},e(t.done?K(a)(`查看`):K(a)(`去处理`)),9,Ue)],2))),128))])])]))}}),[[`__scopeId`,`data-v-6f4553ed`]]);async function Ge(e){return P(`/core/models`,`PUT`,{models:e})}async function Ke(e){return P(`/core/models/`+encodeURIComponent(e),`DELETE`,{})}async function qe(e){return P(`/model/test`,`POST`,{modelId:e})}var Je={class:`card embedding-profile`,"aria-labelledby":`embedding-title`},Ye={id:`embedding-title`},Xe={class:`muted`},Ze={key:0,class:`faint`},Qe={class:`check`},$e={class:`fields`},et=[`required`],tt=[`placeholder`],nt=[`placeholder`],rt={class:`fields`},it={value:`openai`},at={value:`multimodal`},ot={class:`save-bar`},st=[`disabled`],ct=[`disabled`],lt={class:`faint`},ut={key:2,role:`status`},dt=F(z({__name:`EmbeddingProfile`,setup(t){let n=te({enabled:!1,label:a(`知识向量`),baseUrl:``,model:``,apiKey:``,dimensions:0,batchSize:10,timeoutMs:9e4,protocol:`openai`}),r=f(!1),o=f(!1),s=f(!1),c=f(``);async function l(){try{Object.assign(n,await P(`/core/knowledge/embedding`),{apiKey:``}),s.value=!0}catch(e){c.value=e.message}}async function d(){r.value=!0;try{Object.assign(n,await P(`/core/knowledge/embedding`,`PATCH`,n),{apiKey:``}),o.value=!1,W(a(`知识向量模型已保存`))}catch(e){W(e.message,!0)}finally{r.value=!1}}async function p(){r.value=!0;try{let e=await P(`/core/knowledge/embedding/test`,`POST`,{});c.value=a(`连接成功,向量维度 {dimensions}`,{dimensions:e.dimensions})}catch(e){c.value=e.message}finally{r.value=!1}}return N(l),(t,l)=>(w(),u(`section`,Je,[v(`h2`,Ye,e(K(a)(`知识向量模型`)),1),v(`p`,Xe,e(K(a)(`填写任意 OpenAI 兼容向量服务(/embeddings)的请求地址、模型名和密钥。火山方舟地址可以不填模型名,会用 doubao-embedding-vision。维度填 0 表示用模型自己的长度(doubao-embedding-vision 是 2048),写明长度就按写的请求。不启用或连不上时,对话和关键词回忆照常进行。`)),1),s.value&&!n.configured?(w(),u(`p`,Ze,e(K(a)(`当前兼容旧配置,跟随默认模型。保存后改用独立配置。`)),1)):y(``,!0),s.value?(w(),u(`form`,{key:1,autocomplete:`off`,onSubmit:g(d,[`prevent`]),onInput:l[7]||=e=>o.value=!0,onChange:l[8]||=e=>o.value=!0},[v(`label`,Qe,[C(v(`input`,{"onUpdate:modelValue":l[0]||=e=>n.enabled=e,type:`checkbox`,name:`embeddingEnabled`},null,512),[[J,n.enabled]]),T(e(K(a)(`启用知识向量`)),1)]),v(`div`,$e,[v(`label`,null,[T(e(K(a)(`请求地址`)),1),C(v(`input`,{"onUpdate:modelValue":l[1]||=e=>n.baseUrl=e,name:`embeddingBaseUrl`,type:`url`,inputmode:`url`,autocomplete:`off`,spellcheck:`false`,placeholder:`https://example.com/v1`,required:n.enabled},null,8,et),[[i,n.baseUrl]])]),v(`label`,null,[T(e(K(a)(`模型名称`)),1),C(v(`input`,{"onUpdate:modelValue":l[2]||=e=>n.model=e,name:`embeddingModelName`,autocomplete:`off`,spellcheck:`false`,placeholder:K(a)(`火山方舟可留空`)},null,8,tt),[[i,n.model]])]),v(`label`,null,[T(e(K(a)(`向量服务密钥`)),1),C(v(`input`,{"onUpdate:modelValue":l[3]||=e=>n.apiKey=e,name:`embeddingApiKey`,type:`text`,class:`secret`,autocomplete:`off`,spellcheck:`false`,"data-1p-ignore":``,"data-lpignore":`true`,placeholder:n.hasApiKey?K(a)(`已保存,留空保留`):K(a)(`填写向量服务密钥`)},null,8,nt),[[i,n.apiKey]])])]),v(`details`,null,[v(`summary`,null,e(K(a)(`自动选择的细节`)),1),v(`div`,rt,[v(`label`,null,[T(e(K(a)(`向量维度(0 表示自动)`)),1),C(v(`input`,{"onUpdate:modelValue":l[4]||=e=>n.dimensions=e,name:`embeddingDimensions`,type:`number`,min:`0`,max:`65536`},null,512),[[i,n.dimensions,void 0,{number:!0}]])]),v(`label`,null,[T(e(K(a)(`每批段落数`)),1),C(v(`input`,{"onUpdate:modelValue":l[5]||=e=>n.batchSize=e,name:`embeddingBatchSize`,type:`number`,min:`1`,max:`128`},null,512),[[i,n.batchSize,void 0,{number:!0}]])]),v(`label`,null,[T(e(K(a)(`请求格式`)),1),C(v(`select`,{"onUpdate:modelValue":l[6]||=e=>n.protocol=e,name:`embeddingProtocol`},[v(`option`,it,e(K(a)(`OpenAI 兼容(可批量)`)),1),v(`option`,at,e(K(a)(`方舟多模态接口(逐条)`)),1)],512),[[h,n.protocol]])])])]),v(`div`,ot,[v(`button`,{class:`primary`,disabled:r.value},e(K(a)(`保存向量模型`)),9,st),v(`button`,{type:`button`,disabled:r.value||o.value||!n.enabled||!n.configured,onClick:p},e(K(a)(`测试向量连接`)),9,ct),v(`small`,lt,e(o.value?K(a)(`有未保存的修改`):K(a)(`配置已载入`)),1)])],32)):y(``,!0),c.value?(w(),u(`p`,ut,e(c.value),1)):y(``,!0)]))}}),[[`__scopeId`,`data-v-944d9f50`]]),ft={class:`card recall-panel`,"aria-labelledby":`recall-title`},pt={id:`recall-title`},mt={class:`muted`},ht={key:0,class:`stats`},gt={key:0,class:`pill strong`},_t={key:1,class:`pill`},vt={key:2,class:`pill warn`},yt={key:1,class:`waiting`},bt={key:2,class:`waiting`},xt={value:``,disabled:``},St=[`value`],Ct=[`placeholder`],wt=[`disabled`],Tt={key:3,role:`status`},Et={key:4,class:`result`},Dt={class:`faint`},Ot={class:`faint`},kt={key:0,class:`faint`},At=F(z({__name:`RecallPanel`,setup(t){let n=f(null),s=f(``),c=o(()=>B.core?.sessions||[]),l=f(``),d=f(``),p=f(null),m=f(!1),_={fact:a(`事实`),episode:a(`情景`),meeting:a(`相遇`),self:a(`自我线索`),thought:a(`念头`),diary:a(`日记`),period:a(`回顾`),anticipation:a(`约定`),message:a(`原话`)},b={meaning:a(`意思相关`),words:a(`说到了`),person:a(`关于这个人`)},x=o(()=>{let e=n.value?.coverage;return e?.total?`${e.embedded||0} / ${e.total}`:``});async function S(){try{n.value=await P(`/mind/recall`),s.value=``}catch(e){s.value=e.message}}async function E(){if(l.value&&d.value.trim()){m.value=!0;try{p.value=await P(`/mind/recall/probe`,`POST`,{session:l.value,text:d.value}),s.value=``}catch(e){s.value=e.message}finally{m.value=!1}}}return N(S),(t,o)=>(w(),u(`section`,ft,[v(`h2`,pt,e(K(a)(`回忆索引`)),1),v(`p`,mt,e(K(a)(`她的事实、情景、自我线索和近一个月的原话都在同一个索引里,按意思、字词和相关的人一起检索。向量还没覆盖的部分先按字词找。`)),1),n.value?(w(),u(`div`,ht,[(w(!0),u(r,null,D(n.value.items,t=>(w(),u(`span`,{key:t.kind,class:`pill`},e(_[t.kind]||t.kind)+` `+e(t.n),1))),128)),x.value?(w(),u(`span`,gt,e(K(a)(`已有向量`))+` `+e(x.value),1)):y(``,!0),n.value.embedding?.latencyMs?(w(),u(`span`,_t,e(K(a)(`向量延迟`))+` `+e(n.value.embedding.latencyMs.p50)+` ms`,1)):y(``,!0),n.value.embedding?.lastError?(w(),u(`span`,vt,e(n.value.embedding.lastError),1)):y(``,!0)])):y(``,!0),n.value?.waiting?.length?(w(),u(`ul`,yt,[(w(!0),u(r,null,D(n.value.waiting,t=>(w(),u(`li`,{key:t.key},e(K(a)(`等待重试`))+` `+e(t.key)+` × `+e(t.failures)+`:`+e(t.lastError),1))),128))])):y(``,!0),n.value?.unavailableModels?.length?(w(),u(`ul`,bt,[(w(!0),u(r,null,D(n.value.unavailableModels,t=>(w(),u(`li`,{key:t.key},e(K(a)(`暂时停用的备用模型`))+` `+e(t.label)+`:`+e(t.reason),1))),128))])):y(``,!0),v(`form`,{class:`probe`,onSubmit:g(E,[`prevent`])},[C(v(`select`,{"onUpdate:modelValue":o[0]||=e=>l.value=e,name:`recallSession`,required:``},[v(`option`,xt,e(K(a)(`选择会话`)),1),(w(!0),u(r,null,D(c.value,t=>(w(),u(`option`,{key:t.id,value:t.id},e(t.name||t.id),9,St))),128))],512),[[h,l.value]]),C(v(`input`,{"onUpdate:modelValue":o[1]||=e=>d.value=e,name:`recallText`,maxlength:`500`,placeholder:K(a)(`输入一句话,看她会想起什么`)},null,8,Ct),[[i,d.value]]),v(`button`,{disabled:m.value||!l.value||!d.value.trim()},e(K(a)(`试试回忆`)),9,wt)],32),s.value?(w(),u(`p`,Tt,e(s.value),1)):y(``,!0),p.value?(w(),u(`div`,Et,[v(`p`,Dt,e(p.value.mode===`hybrid`?K(a)(`意思与字词一起`):K(a)(`仅字词`))+` · `+e(K(a)(`候选`))+` `+e(p.value.candidates),1),v(`ol`,null,[(w(!0),u(r,null,D(p.value.items,t=>(w(),u(`li`,{key:t.id},[v(`strong`,null,e(_[t.kind]||t.kind),1),T(` `+e(t.text)+` `,1),v(`small`,Ot,[T(e(b[t.why])+` `,1),t.standsOut===null?y(``,!0):(w(),u(r,{key:0},[T(` · `+e(K(a)(`突出度`))+` `+e(t.standsOut),1)],64)),T(` · `+e(K(a)(`共有字词`))+` `+e(t.sharedWords),1)])]))),128))]),p.value.items.length?y(``,!0):(w(),u(`p`,kt,e(K(a)(`没有想起什么`)),1))])):y(``,!0)]))}}),[[`__scopeId`,`data-v-cdde3f37`]]),jt={class:`library`},Mt={class:`model-workspace`},Nt={class:`card shelf`},Pt={class:`shelf-head`},Ft={class:`faint`},It={class:`rows`},Lt={key:0,class:`muted`},Rt=[`open`,`data-saved-vendor`,`onToggle`],zt={class:`vendor-models`},Bt=[`onClick`],Vt={class:`who`},Ht={key:0,class:`card detail`},Ut={class:`card-head`},Wt={class:`eyebrow`},Gt=[`data-tone`],Kt={class:`form-grid`},qt={key:0},Jt=[`placeholder`],Yt={class:`wide`},Xt=[`placeholder`],Zt={class:`faint`},Qt={class:`form-grid`},$t={key:0},en={key:1},tn={class:`advanced`},nn={class:`form-grid`},rn=[`placeholder`],an={class:`faint`},on={class:`caps`},sn=[`onUpdate:modelValue`,`name`],cn={key:0,id:`modelTestResult`,class:`notice`,role:`status`},ln={class:`actions`},un=[`disabled`],dn={class:`faint`},fn={key:0,class:`check`},pn=[`checked`],mn=[`disabled`],hn={key:1,class:`card detail`},gn={class:`card usage-panel`,"aria-labelledby":`token-usage-title`},_n={class:`usage-intro`},vn={class:`eyebrow`},yn={id:`token-usage-title`},bn={class:`usage-total`},xn={class:`usage-detail`},Sn={class:`usage-stat`},Cn={class:`usage-stat`},wn={class:`usage-stat cache-stat`},Tn={class:`usage-stat`},En={class:`usage-foot`},Dn={key:0},On={key:1,class:`usage-error`},kn={key:2},An=[`disabled`],jn={class:`model-picker`},Mn={class:`muted`},Nn={class:`catalog-search`},Pn=[`placeholder`],Fn=[`data-vendor`],In={class:`vendor-summary`},Ln={class:`vendor-name`},Rn={class:`presets`},zn=[`data-preset`,`onClick`],Bn={key:0,class:`muted search-empty`},Vn=F(z({__name:`ModelLibrary`,setup(c){let l=o(()=>B.health?.modelCatalog||[]),d=o(()=>B.health?.effortLabels||{none:a(`关闭`),minimal:a(`极低`),low:a(`低`),medium:a(`中`),high:a(`高`),xhigh:a(`极高`),max:a(`最深`)}),p=f(B.core.models.map(e=>({...e}))),h=f(p.value.length?0:-1),_=f(p.value.length?Q(p.value[0]):``),S=f(!1),E=f(!1),O=f(!1),k=f(``),A=f(``),j=f(null),M=f(!1),F=f(``),I=te(Z());p.value[0]&&Object.assign(I,p.value[0],{apiKey:``});let L=o(()=>h.value>=0&&!!I.id),R=o(()=>{let e=[];for(let t of l.value){if(t.id===`custom`)continue;let n=e.find(e=>e.vendor===t.vendor);n||(n={vendor:t.vendor,models:[]},e.push(n)),n.models.push(t)}return e}),z=o(()=>{let e=k.value.trim().toLocaleLowerCase();return e?R.value.map(t=>({...t,models:t.models.filter(n=>[t.vendor,n.label,n.model,n.summary].filter(Boolean).join(` `).toLocaleLowerCase().includes(e))})).filter(e=>e.models.length):R.value}),V=o(()=>{let e=[];return p.value.forEach((t,n)=>{let r=Q(t),i=e.find(e=>e.vendor===r);i||(i={vendor:r,models:[]},e.push(i)),i.models.push({model:t,index:n})}),e}),U=o(()=>l.value.find(e=>e.id===`custom`)),G=o(()=>l.value.filter(e=>e.vendor&&e.vendor===I.vendor)),ne=o(()=>l.value.find(e=>e.id===I.presetId)||l.value.find(e=>e.model&&e.model===I.model&&e.baseUrl===I.baseUrl)),X=o(()=>{let e=ne.value?.contextWindows||[];if(e.length<2)return[];let t=Number(I.contextWindow);return e.some(e=>e.contextWindow===t)?e:[...e,{label:a(`已保存 {value}`,{value:fe(t)}),contextWindow:t,maxInputTokens:Number(I.maxInputTokens),maxOutputTokens:Number(I.maxOutputTokens)}]}),ie=o(()=>{let e=Array.isArray(I.reasoningEfforts)&&I.reasoningEfforts.length?[...I.reasoningEfforts]:[`none`,`minimal`,`low`,`medium`,`high`,`xhigh`,`max`];return I.reasoningEffort&&!e.includes(I.reasoningEffort)&&e.unshift(I.reasoningEffort),e}),oe=o(()=>B.core.models.some(e=>e.id===I.id));function Z(){return{id:``,presetId:``,label:``,provider:``,vendor:``,baseUrl:``,model:``,apiKey:``,hasApiKey:!1,contextWindow:128e3,maxInputTokens:119808,maxOutputTokens:8192,vision:!1,system:!0,json:!0,tools:!1,embedding:!1,embeddingModel:``,reasoningEffort:`none`,reasoningEfforts:[`none`,`low`,`medium`,`high`],thinkingStyle:`openai`,tokenField:`max_tokens`,omitSampling:!1,temperature:.85,topP:1,timeoutMs:9e4,isDefault:!1,enabled:!0}}function Q(e){let t={"volcengine-coding-plan":a(`火山方舟 Coding Plan`),"opencode-go":`OpenCode Go`,"opencode-zen":`OpenCode Zen`,deepseek:`DeepSeek`,openai:`OpenAI`,bedrock:a(`亚马逊 Bedrock`),qwen:a(`通义千问`),moonshot:`Kimi`,kimi:`Kimi`,zhipu:a(`智谱 GLM`),glm:a(`智谱 GLM`),mimo:a(`小米 MiMo`),siliconflow:`SiliconFlow`,openrouter:`OpenRouter`};return String(e?.vendor||``).trim()||t[String(e?.provider||``).toLowerCase()]||String(e?.provider||a(`自定义供应商`))}function se(e,t){let n=t.currentTarget;_.value=n.open?e:``}function ce(e){let{summary:t,contextWindows:n,...r}=e;return{...r,presetId:e.id}}function le(e){let{contextWindows:t,...n}=e;return n}function ue(e){let t=Number(e.contextWindow),n=Number(e.maxOutputTokens),r=Number(e.maxInputTokens);return(!Number.isFinite(t)||t<2)&&(t=2),(!Number.isFinite(n)||n<1)&&(n=1),n>=t&&(n=t-1),(!Number.isFinite(r)||r<1)&&(r=t-n),r+n>t&&(r=t-n),{contextWindow:t,maxInputTokens:r,maxOutputTokens:n}}function de(e){let t=Number(e.target.value),n=X.value.find(e=>e.contextWindow===t);n&&(I.contextWindow=n.contextWindow,I.maxInputTokens=n.maxInputTokens,I.maxOutputTokens=n.maxOutputTokens,B.dirty=!0)}function fe(e){let t=Number(e)||0;return t>=1e6?(t%1048576==0?t/1048576:Number((t/1e6).toFixed(2)))+`M`:t>=1e3?(t%1e3==0?t/1e3:t%1024==0?t/1024:Math.round(t/1e3))+`K`:String(t)}function pe(e){if(e.id===`openrouter`)return a(`按所选模型填写上下文和输出参数`);if(!e.model)return a(`填写模型 ID 和对应参数`);let t=e.contextWindows||[],n=t.length?t.map(e=>e.label).join(` / `):fe(e.contextWindow);return a(`上下文 {context} · 输出 {v}`,{context:n,v:fe(e.maxOutputTokens)})}function me(e){return[`mimo`,`qwen`,`kimi-toggle`].includes(I.thinkingStyle)?a(e===`none`?`关闭`:`开启`):`${d.value[e]||e} / ${e}`}function $(e){return new Intl.NumberFormat(s()).format(Number(e)||0)}async function he(){if(!M.value){M.value=!0;try{j.value=await P(`/core/usage`),F.value=``}catch(e){F.value=e.message}finally{M.value=!1}}}N(()=>void he()),b(()=>B.tick,()=>void he());async function ge(){return B.dirty?await t(a(`放弃当前模型草稿?`),{title:a(`草稿还没保存`),confirmText:a(`放弃`),cancelText:a(`留下`)})?(B.dirty=!1,!0):!1:!0}async function _e(e){let t=p.value[e];t&&t.id!==I.id&&await ge()&&(p.value=p.value.filter(e=>e.id===t.id||B.core.models.some(t=>t.id===e.id)),h.value=p.value.findIndex(e=>e.id===t.id),_.value=Q(t),Object.assign(I,Z(),t,{apiKey:``}),A.value=``)}async function ve(){if(!await ge())return;p.value=B.core.models.map(e=>({...e})),p.value.length?p.value.some(e=>e.id===I.id)||(h.value=0,Object.assign(I,Z(),p.value[0],{apiKey:``})):(h.value=-1,Object.assign(I,Z()));let e=p.value[h.value]||p.value[0];_.value=e?Q(e):``,k.value=``,O.value=!0}function ye(e){p.value=B.core.models.map(e=>({...e}));let t={...Z(),...ce(e),id:`model-`+Date.now(),apiKey:``,hasApiKey:!1,isDefault:p.value.length===0};p.value.push(t),h.value=p.value.length-1,_.value=Q(t),Object.assign(I,t),A.value=``,O.value=!1,B.dirty=!0}function be(e){let t=e.target.value,n=l.value.find(e=>e.id===t);if(!n)return;Object.assign(I,Z(),ce(n),{id:I.id,apiKey:I.apiKey,hasApiKey:I.hasApiKey,isDefault:I.isDefault});let r=p.value[h.value];r&&(r.label=I.label),B.dirty=!0}function xe(){for(let e of p.value)e.isDefault=e.id===I.id;I.isDefault=!0,B.dirty=!0}async function Se(){let e=ue(I),t=p.value.map((t,n)=>le(n===h.value?{...I,...e,isDefault:!!I.isDefault,enabled:I.isDefault?!0:I.enabled!==!1,timeoutMs:Number(I.timeoutMs),temperature:Number(I.temperature),topP:Number(I.topP)}:{...t,isDefault:!!t.isDefault&&t.id!==I.id}));if(!t.some(e=>e.isDefault)&&t.length&&(t[0].isDefault=!0),!S.value){S.value=!0;try{await Ge(t),B.dirty=!1,await x(),p.value=B.core.models.map(e=>({...e})),Object.assign(I,Z(),p.value[h.value],{apiKey:``}),W(a(`已保存并应用`))}catch(e){W(e.message,!0)}finally{S.value=!1}}}async function Ce(){if(await t(a(`确定删除这个模型?`),{title:a(`删除模型`),confirmText:a(`删除`),danger:!0})){if(B.core.models.some(e=>e.id===I.id)&&await Ke(I.id),B.dirty=!1,await x(),p.value=B.core.models.map(e=>({...e})),A.value=``,!p.value.length){h.value=-1,_.value=``,Object.assign(I,Z());return}h.value=0,_.value=Q(p.value[0]),Object.assign(I,Z(),p.value[0],{apiKey:``})}}async function we(){if(!(!oe.value||B.dirty||E.value)){E.value=!0,A.value=a(`正在测试「{v}」…`,{v:I.label||I.model});try{let e=await qe(I.id);A.value=a(`✓ {v} 连接成功 · {latency} ms`,{v:I.label||e.model,latency:e.latency}),await x()}catch(e){A.value=e.message}finally{E.value=!1}}}return(t,o)=>(w(),u(`div`,jt,[v(`div`,Mt,[v(`aside`,Nt,[v(`div`,Pt,[v(`h2`,null,[T(e(K(a)(`模型库`))+` `,1),v(`small`,null,e(p.value.length),1)]),v(`button`,{id:`addModel`,class:`small primary`,onClick:ve},e(K(a)(`+ 新增模型`)),1)]),v(`p`,Ft,e(K(a)(`群聊、私聊和独处都使用默认模型。其余已启用的模型按这里的顺序作为备用。关掉的模型不会被调用。`)),1),v(`div`,It,[p.value.length?y(``,!0):(w(),u(`p`,Lt,e(K(a)(`还没有模型。`)),1)),(w(!0),u(r,null,D(V.value,t=>(w(),u(`details`,{key:t.vendor,class:`saved-vendor`,open:_.value===t.vendor,"data-saved-vendor":t.vendor,onToggle:e=>se(t.vendor,e)},[v(`summary`,null,[v(`span`,null,e(t.vendor),1),v(`small`,null,e(K(a)(`{length} 个模型`,{length:t.models.length})),1)]),v(`div`,zt,[(w(!0),u(r,null,D(t.models,t=>(w(),u(`button`,{key:t.model.id,class:m([`entity-row`,{selected:h.value===t.index}]),onClick:e=>_e(t.index)},[v(`span`,{class:`badge`,style:n({"--hue":K(re)(t.model.label||t.model.model||t.model.id)})},e(String(t.model.label||t.model.model||`?`).slice(0,1).toUpperCase()),5),v(`span`,Vt,[v(`b`,null,e(t.model.label||t.model.model),1),v(`small`,null,e(t.model.enabled===!1?K(a)(`已关闭`):t.model.isDefault?K(a)(`默认模型`):K(a)(`备用模型`)),1)])],10,Bt))),128))])],40,Rt))),128))])]),L.value?(w(),u(`section`,Ht,[v(`div`,Ut,[v(`div`,null,[v(`span`,Wt,e(K(a)(`模型档案`)),1),v(`h2`,null,e(I.label||K(a)(`新模型`)),1)]),v(`span`,{class:`chip`,"data-tone":I.hasApiKey?`ok`:`warn`},e((I.isDefault?K(a)(`默认模型 · `):``)+(I.hasApiKey?K(a)(`密钥已保存`):K(a)(`待填写密钥`))),9,Gt)]),v(`form`,{id:`modelForm`,class:`stack`,onSubmit:g(Se,[`prevent`]),onInput:o[14]||=e=>K(B).dirty=!0},[v(`fieldset`,null,[v(`legend`,null,e(K(a)(`连接`)),1),v(`div`,Kt,[G.value.length>1?(w(),u(`label`,qt,[T(e(K(a)(`同厂商模型`))+` `,1),H(Y,{"model-value":I.presetId,"aria-label":K(a)(`同厂商模型`),options:G.value.map(e=>({value:e.id,label:e.label})),onChange:be},null,8,[`model-value`,`aria-label`,`options`])])):y(``,!0),v(`label`,null,[T(e(K(a)(`显示名称`)),1),C(v(`input`,{"onUpdate:modelValue":o[0]||=e=>I.label=e,name:`label`,required:``},null,512),[[i,I.label]])]),v(`label`,null,[T(e(K(a)(`供应商`)),1),C(v(`input`,{"onUpdate:modelValue":o[1]||=e=>I.provider=e,name:`provider`,placeholder:K(a)(`例如 deepseek、openai`)},null,8,Jt),[[i,I.provider]])]),v(`label`,null,[T(e(K(a)(`API 地址`)),1),C(v(`input`,{"onUpdate:modelValue":o[2]||=e=>I.baseUrl=e,name:`baseUrl`,type:`url`,required:``},null,512),[[i,I.baseUrl]])]),v(`label`,null,[T(e(K(a)(`模型名称`)),1),C(v(`input`,{"onUpdate:modelValue":o[3]||=e=>I.model=e,name:`model`,required:``},null,512),[[i,I.model]])]),v(`label`,Yt,[o[18]||=T(` API Key `,-1),C(v(`input`,{"onUpdate:modelValue":o[4]||=e=>I.apiKey=e,name:`apiKey`,type:`password`,autocomplete:`new-password`,placeholder:I.hasApiKey?K(a)(`已保存,留空则保留`):K(a)(`填写供应商提供的密钥`)},null,8,Xt),[[i,I.apiKey]])])])]),v(`fieldset`,null,[v(`legend`,null,e(K(a)(`容量与思考`)),1),v(`p`,Zt,e(K(a)(`官方按输入长度分档计价的模型可以在标准和百万之间切换;输出上限默认是官方最大值,思考强度只列出这个模型支持的档位。`)),1),v(`div`,Qt,[X.value.length?(w(),u(`label`,$t,[T(e(K(a)(`上下文容量`))+` `,1),H(Y,{name:`contextWindow`,"model-value":I.contextWindow,options:X.value.map(e=>({value:e.contextWindow,label:e.label})),onChange:de},null,8,[`model-value`,`options`])])):(w(),u(`label`,en,[T(e(K(a)(`上下文容量`)),1),C(v(`input`,{"onUpdate:modelValue":o[5]||=e=>I.contextWindow=e,name:`contextWindow`,type:`number`},null,512),[[i,I.contextWindow,void 0,{number:!0}]])])),v(`label`,null,[T(e(K(a)(`最大输入 Token`)),1),C(v(`input`,{"onUpdate:modelValue":o[6]||=e=>I.maxInputTokens=e,name:`maxInputTokens`,type:`number`},null,512),[[i,I.maxInputTokens,void 0,{number:!0}]])]),v(`label`,null,[T(e(K(a)(`最大输出 Token`)),1),C(v(`input`,{"onUpdate:modelValue":o[7]||=e=>I.maxOutputTokens=e,name:`maxOutputTokens`,type:`number`},null,512),[[i,I.maxOutputTokens,void 0,{number:!0}]])]),v(`label`,null,[T(e(K(a)(`思考强度`))+` `,1),H(Y,{modelValue:I.reasoningEffort,"onUpdate:modelValue":o[8]||=e=>I.reasoningEffort=e,name:`reasoningEffort`,"aria-label":K(a)(`思考强度`),options:ie.value.map(e=>({value:e,label:me(e)}))},null,8,[`modelValue`,`aria-label`,`options`])])])]),v(`details`,tn,[v(`summary`,null,e(K(a)(`高级参数与模型能力`)),1),v(`div`,nn,[v(`label`,null,[T(e(K(a)(`随机程度 Temperature`)),1),C(v(`input`,{"onUpdate:modelValue":o[9]||=e=>I.temperature=e,name:`temperature`,type:`number`,step:`0.05`},null,512),[[i,I.temperature,void 0,{number:!0}]])]),v(`label`,null,[T(e(K(a)(`采样范围 Top P`)),1),C(v(`input`,{"onUpdate:modelValue":o[10]||=e=>I.topP=e,name:`topP`,type:`number`,step:`0.05`},null,512),[[i,I.topP,void 0,{number:!0}]])]),v(`label`,null,[T(e(K(a)(`超时(毫秒)`)),1),C(v(`input`,{"onUpdate:modelValue":o[11]||=e=>I.timeoutMs=e,name:`timeoutMs`,type:`number`},null,512),[[i,I.timeoutMs,void 0,{number:!0}]])]),v(`label`,null,[T(e(K(a)(`知识检索模型`)),1),C(v(`input`,{"onUpdate:modelValue":o[12]||=e=>I.embeddingModel=e,name:`embeddingModel`,placeholder:K(a)(`留空跟随对话模型`)},null,8,rn),[[i,I.embeddingModel]])])]),v(`p`,an,e(K(a)(`打开图片理解后,聊天里的图片会先在本机读取,再连同画面交给这个模型。主模型不能看图时,到会话设置另选一个打开了图片理解的视觉兼容模型。`)),1),v(`div`,on,[(w(!0),u(r,null,D({vision:K(a)(`图片理解`),system:`System Prompt`,json:K(a)(`JSON 输出`),tools:K(a)(`工具调用`),embedding:K(a)(`知识向量`)},(t,n)=>(w(),u(`label`,{key:n,class:`check`},[C(v(`input`,{"onUpdate:modelValue":e=>I[n]=e,name:n,type:`checkbox`},null,8,sn),[[J,I[n]]]),T(e(t),1)]))),128))])]),A.value?(w(),u(`div`,cn,e(A.value),1)):y(``,!0),v(`div`,ln,[v(`button`,{class:`primary`,disabled:S.value},e(S.value?K(a)(`正在保存…`):K(a)(`保存模型`)),9,un),v(`small`,dn,e(K(B).dirty?K(a)(`有未保存的修改`):K(a)(`已保存的模型才会用来测试和回复`)),1),I.isDefault?y(``,!0):(w(),u(`label`,fn,[v(`input`,{type:`checkbox`,checked:I.enabled!==!1,onChange:o[13]||=e=>{I.enabled=e.target.checked,K(B).dirty=!0}},null,40,pn),T(e(K(a)(`启用这个备用模型`)),1)])),I.isDefault?y(``,!0):(w(),u(`button`,{key:1,id:`setDefaultModel`,type:`button`,onClick:xe},e(K(a)(`设为默认模型`)),1)),oe.value?(w(),u(`button`,{key:2,id:`testModel`,type:`button`,disabled:S.value||E.value||K(B).dirty,onClick:we},e(E.value?K(a)(`正在测试…`):K(B).dirty?K(a)(`保存后测试此模型`):K(a)(`测试此模型连接`)),9,mn)):y(``,!0),v(`button`,{id:`deleteModel`,type:`button`,class:`danger push`,onClick:Ce},e(K(a)(`删除模型`)),1)])],32)])):(w(),u(`section`,hn,[H(ee,{title:K(a)(`还没有模型`),text:K(a)(`常见厂商会预填模型参数;OpenRouter 可选 GPT-6 预设,也可用自选模型手动填写模型 ID 和参数。`)},{default:q(()=>[v(`button`,{class:`primary`,onClick:ve},e(K(a)(`+ 新增模型`)),1)]),_:1},8,[`title`,`text`])]))]),H(dt),H(At),v(`section`,gn,[v(`div`,_n,[v(`div`,null,[v(`span`,vn,e(K(a)(`TOKEN LEDGER · 累计用量`)),1),v(`h2`,yn,e(K(a)(`一路聊到现在`)),1),v(`p`,null,e(K(a)(`统计本机账本记录的模型调用;缓存 Token 已包含在输入量里。`)),1)]),v(`div`,bn,[v(`strong`,null,e($(j.value?.total)),1),v(`span`,null,e(K(a)(`累计 Token`)),1)])]),v(`div`,xn,[v(`div`,Sn,[v(`span`,null,e(K(a)(`输入`)),1),v(`b`,null,e($(j.value?.input)),1)]),v(`div`,Cn,[v(`span`,null,e(K(a)(`输出`)),1),v(`b`,null,e($(j.value?.output)),1)]),v(`div`,wn,[v(`span`,null,[T(e(K(a)(`缓存命中`))+` `,1),v(`small`,null,e(K(a)(`(输入子项)`)),1)]),v(`b`,null,e($(j.value?.cached)),1)]),v(`div`,Tn,[v(`span`,null,e(K(a)(`调用次数`)),1),v(`b`,null,e($(j.value?.calls)),1)])]),v(`div`,En,[j.value?.since?(w(),u(`small`,Dn,[T(e(K(a)(`从 {v} 开始记录 · {v2} 为模型返回用量`,{v:new Date(j.value.since).toLocaleDateString(K(s)()),v2:$(j.value.reportedTokens)}))+` `,1),j.value.estimatedCalls?(w(),u(r,{key:0},[T(e(K(a)(`· {v} 次调用按文本估算`,{v:$(j.value.estimatedCalls)})),1)],64)):y(``,!0)])):F.value?(w(),u(`small`,On,e(F.value),1)):(w(),u(`small`,kn,e(K(a)(`还没有模型调用记录。`)),1)),v(`button`,{type:`button`,class:`usage-refresh`,disabled:M.value,onClick:he},e(M.value?K(a)(`更新中…`):K(a)(`刷新统计 ↻`)),9,An)])]),H(ae,{open:O.value,title:K(a)(`选择模型`),eyebrow:`MODELS`,width:`880px`,onClose:o[17]||=e=>O.value=!1},{default:q(()=>[v(`div`,jn,[v(`p`,Mn,e(K(a)(`先选供应商,再挑具体模型;支持搜索。火山方舟 Coding Plan 已预填套餐专用接口与型号参数,请勿换成普通推理 API 地址。`)),1),v(`label`,Nn,[v(`span`,null,e(K(a)(`搜索模型或供应商`)),1),C(v(`input`,{"onUpdate:modelValue":o[15]||=e=>k.value=e,type:`search`,placeholder:K(a)(`例如:火山方舟、Kimi K3、DeepSeek`)},null,8,Pn),[[i,k.value]])]),(w(!0),u(r,null,D(z.value,t=>(w(),u(`details`,{key:t.vendor,class:`vendor`,"data-vendor":t.vendor},[v(`summary`,In,[v(`span`,Ln,[v(`b`,null,e(t.vendor),1),v(`small`,null,e(K(a)(`{length} 个预设`,{length:t.models.length})),1)]),o[19]||=v(`span`,{class:`vendor-chevron`,"aria-hidden":`true`},`⌄`,-1)]),v(`div`,Rn,[(w(!0),u(r,null,D(t.models,t=>(w(),u(`button`,{key:t.id,type:`button`,class:`preset-card`,"data-preset":t.id,onClick:e=>ye(t)},[v(`b`,null,e(t.label),1),v(`small`,null,e(t.model||K(a)(`填写你在 OpenRouter 选择的模型 ID`)),1),v(`small`,null,e(pe(t)),1),v(`small`,null,e(t.summary),1)],8,zn))),128))])],8,Fn))),128)),z.value.length?y(``,!0):(w(),u(`p`,Bn,e(K(a)(`没有找到匹配的模型预设。`)),1)),U.value?(w(),u(`button`,{key:1,type:`button`,class:`preset-card custom`,"data-preset":`custom`,onClick:o[16]||=e=>ye(U.value)},[v(`b`,null,e(U.value.label),1),v(`small`,null,e(U.value.summary),1)])):y(``,!0)])]),_:1},8,[`open`,`title`])]))}}),[[`__scopeId`,`data-v-8a9bd858`]]),Hn={class:`switch`},Un=[`name`,`disabled`],Wn=z({__name:`Toggle`,props:j({name:{},disabled:{type:Boolean}},{modelValue:{type:Boolean,default:!1},modelModifiers:{}}),emits:[`update:modelValue`],setup(e){let t=O(e,`modelValue`);return(n,r)=>(w(),u(`label`,Hn,[C(v(`input`,{"onUpdate:modelValue":r[0]||=e=>t.value=e,type:`checkbox`,name:e.name,disabled:e.disabled},null,8,Un),[[J,t.value]]),v(`span`,null,[R(n.$slots,`default`)])]))}}),Gn={class:`runtime-grid`},Kn={class:`card-head`},qn={class:`eyebrow`},Jn={class:`switches`},Yn={class:`save-bar`},Xn={class:`primary`,type:`submit`},Zn={class:`faint`},Qn={class:`card numbers`},$n=[`title`],er=F(z({__name:`Runtime`,setup(t){let n=f(!!B.health.settings.enabled),r=f(!!B.health.settings.demo),i=o(()=>(B.core.sessions||[]).filter(e=>e.enabled&&!e.archived).length),s=o(()=>{let e=B.health.backup;if(!e)return null;if(!e.enabled)return{value:a(`已关闭`),caption:a(`自动备份`),tone:`warn`,title:``};if(e.lastError)return{value:a(`失败`),caption:a(`自动备份出错,稍后重试`),tone:`danger`,title:e.lastError.message};if(!e.latest)return{value:a(`还没有`),caption:a(`自动备份,运行一阵后会做第一份`),tone:``,title:``};let t=Math.max(1,Math.round(e.latest.bytes/1048576));return{value:X(e.latest.at),caption:a(`上次自动备份 · {mb} MB · 已留 {count}/{keep} 份`,{mb:t,count:e.count,keep:e.keep}),tone:``,title:e.latest.name}});async function c(){try{await L({enabled:n.value,demo:r.value}),B.dirty=!1,B.health=await E(),W(a(`已保存并应用`))}catch(e){W(e.message,!0)}}return(t,o)=>(w(),u(`div`,Gn,[v(`form`,{id:`runtime`,class:`card`,onSubmit:g(c,[`prevent`]),onChange:o[2]||=e=>K(B).dirty=!0},[v(`div`,Kn,[v(`div`,null,[v(`span`,qn,e(K(a)(`运行开关`)),1),v(`h2`,null,e(K(a)(`TA 现在可以说话吗`)),1)])]),v(`div`,Jn,[H(Wn,{modelValue:n.value,"onUpdate:modelValue":o[0]||=e=>n.value=e,name:`enabled`},{default:q(()=>[v(`b`,null,e(K(a)(`允许参与聊天`)),1),v(`small`,null,e(K(a)(`总开关。关掉后 TA 不看也不回任何会话,心智照常保存。`)),1)]),_:1},8,[`modelValue`]),H(Wn,{modelValue:r.value,"onUpdate:modelValue":o[1]||=e=>r.value=e,name:`demo`},{default:q(()=>[v(`b`,null,e(K(a)(`仅模拟运行,不向 QQ 发送`)),1),v(`small`,null,e(K(a)(`模拟模式下,「对话」里可以发模拟消息走完整流程;它们写进模拟会话,不进入真实记忆。`)),1)]),_:1},8,[`modelValue`])]),v(`div`,Yn,[v(`button`,Xn,e(K(a)(`保存运行状态`)),1),v(`small`,Zn,e(K(B).dirty?K(a)(`有未保存的修改`):K(a)(`已保存,立刻生效`)),1)])],32),v(`section`,Qn,[v(`button`,{onClick:o[3]||=e=>K(A)(`chats`)},[v(`b`,null,e(i.value),1),v(`span`,null,e(K(a)(`参与中的会话`)),1)]),v(`button`,{onClick:o[4]||=e=>K(A)(`system`,`models`)},[v(`b`,null,e(K(B).core.models.length),1),v(`span`,null,e(K(a)(`可用模型`)),1)]),v(`button`,{onClick:o[5]||=e=>K(A)(`system`,`connect`)},[v(`b`,null,e(K(B).health.connection?.online?K(a)(`在线`):K(a)(`离线`)),1),v(`span`,null,e(K(a)(`QQ 连接`)),1)]),s.value?(w(),u(`button`,{key:0,class:m([`stat`,s.value.tone]),title:s.value.title,"data-testid":`backup-status`,type:`button`,onClick:o[6]||=e=>K(A)(`system`,`storage`)},[v(`b`,null,e(s.value.value),1),v(`span`,null,e(s.value.caption),1)],10,$n)):y(``,!0)])]))}}),[[`__scopeId`,`data-v-76173390`]]),tr={class:`card search-profile`},nr={class:`eyebrow`},rr={class:`muted`},ir={key:0,class:`faint`},ar={class:`check`},or=[`disabled`],sr={class:`search-fields`},cr=[`disabled`],lr=[`disabled`,`placeholder`],ur=[`disabled`],dr=[`disabled`],fr=[`disabled`],pr={class:`faint`},mr={class:`search-usage`,role:`status`},hr={class:`chip`},gr={class:`faint`},_r={class:`faint`},vr={class:`row`},yr=[`disabled`],br=[`disabled`],xr={key:0,class:`faint`},Sr=[`role`],Cr=F(z({__name:`SearchProfile`,setup(t){let n=[`provider`,`enabled`,`baseUrl`,`timeoutMs`,`maxResults`,`dailyLimit`],r=te({provider:`tavily`,enabled:!0,baseUrl:``,apiKey:``,timeoutMs:2e4,maxResults:5,dailyLimit:12}),s=f(null),c=f(!1),l=f(!1),d=f(``),p=f(!1),h=new Map,_=`tavily`,x=o(()=>l.value&&(!!r.apiKey.trim()||n.some(e=>r[e]!==s.value?.[e]))),S=e=>e===`brave`?`Brave`:`Tavily`,E=o(()=>s.value?.dailyLimit===0?a(`不限`):s.value?.dailyLimit??12);b(x,e=>{B.dirty=e}),b(()=>[B.tick,B.pulse],()=>{l.value&&!c.value&&B.page===`system`&&B.sub===`search`&&D().catch(()=>{})});async function D(){s.value=await P(`/mind/time/search`)}async function O(){try{await D(),Object.assign(r,s.value,{apiKey:``}),_=r.provider,l.value=!0}catch(e){W(e.message,!0)}}async function k(){if(c.value)return;c.value=!0;let e=r.provider;h.set(_,{...r,provider:_});try{Object.assign(r,h.get(e)||{...await P(`/mind/time/search?provider=`+e),apiKey:``}),_=e,d.value=``}catch(e){r.provider=_,W(e.message,!0)}finally{c.value=!1}}function A(){return Object.fromEntries([...n,`apiKey`].map(e=>[e,r[e]]))}async function j(){if(!c.value){c.value=!0;try{let e=await P(`/mind/time/search`,`PATCH`,A());s.value=e,Object.assign(r,e,{apiKey:``}),h.set(r.provider,{...r}),_=r.provider,W(a(`搜索配置已保存并生效`))}catch(e){W(e.message,!0)}finally{c.value=!1}}}async function M(){if(!c.value){c.value=!0,d.value=``,p.value=!1;try{let e=await P(`/mind/time/search/test`,`POST`,A()),t={source:e.profile.mode===`web`?a(`{provider} 联网检索`,{provider:S(e.profile.provider)}):e.profile.materialLabel,count:e.results.length};d.value=x.value?a(`{source}测试通过,返回 {count} 条资料。测试不占用每日查询额度;当前填写的配置尚未保存。`,t):a(`{source}测试通过,返回 {count} 条资料。测试不占用每日查询额度。`,t)}catch(e){p.value=!0,d.value=a(`{error}。本次测试不占用每日查询额度。`,{error:e.message})}finally{try{await D()}catch{}c.value=!1}}}return N(O),U(()=>{B.dirty=!1}),(t,n)=>(w(),u(`section`,tr,[v(`span`,nr,e(K(a)(`SEARCH · 独立搜索`)),1),v(`h2`,null,e(K(a)(`接触世界的资料`)),1),v(`p`,rr,e(K(a)(`配置后优先联网检索。未配置密钥或关闭独立搜索时,使用当前模型整理已有知识。`)),1),l.value?(w(),u(`p`,ir,e(K(a)(`当前生效:{v}。表单修改保存后生效。`,{v:s.value.mode===`web`?K(a)(`{provider} · 联网检索资料`,{provider:S(s.value.provider)}):s.value.materialLabel})),1)):y(``,!0),l.value?(w(),u(`form`,{key:1,onSubmit:g(j,[`prevent`])},[v(`label`,ar,[C(v(`input`,{"onUpdate:modelValue":n[0]||=e=>r.enabled=e,type:`checkbox`,disabled:c.value},null,8,or),[[J,r.enabled]]),T(e(K(a)(`启用独立搜索`)),1)]),v(`div`,sr,[v(`label`,null,[T(e(K(a)(`提供方`)),1),H(Y,{modelValue:r.provider,"onUpdate:modelValue":n[1]||=e=>r.provider=e,"aria-label":K(a)(`搜索提供方`),disabled:c.value,options:[{value:`tavily`,label:`Tavily`},{value:`brave`,label:`Brave`}],onChange:k},null,8,[`modelValue`,`aria-label`,`disabled`])]),v(`label`,null,[T(e(K(a)(`搜索地址`)),1),C(v(`input`,{"onUpdate:modelValue":n[2]||=e=>r.baseUrl=e,type:`url`,required:``,name:`searchUrl`,disabled:c.value},null,8,cr),[[i,r.baseUrl]])]),v(`label`,null,[T(e(K(a)(`独立密钥`)),1),C(v(`input`,{"onUpdate:modelValue":n[3]||=e=>r.apiKey=e,type:`password`,autocomplete:`new-password`,name:`searchKey`,disabled:c.value,placeholder:r.hasApiKey?K(a)(`已保存,留空保留`):K(a)(`填写该提供方的搜索密钥`)},null,8,lr),[[i,r.apiKey]])]),v(`label`,null,[T(e(K(a)(`超时(毫秒)`)),1),C(v(`input`,{"onUpdate:modelValue":n[4]||=e=>r.timeoutMs=e,type:`number`,min:`1000`,max:`120000`,disabled:c.value},null,8,ur),[[i,r.timeoutMs,void 0,{number:!0}]])]),v(`label`,null,[T(e(K(a)(`每次结果数`)),1),C(v(`input`,{"onUpdate:modelValue":n[5]||=e=>r.maxResults=e,type:`number`,min:`1`,max:`20`,disabled:c.value},null,8,dr),[[i,r.maxResults,void 0,{number:!0}]])]),v(`label`,null,[T(e(K(a)(`每日查询上限`)),1),C(v(`input`,{"onUpdate:modelValue":n[6]||=e=>r.dailyLimit=e,name:`searchDailyLimit`,type:`number`,min:`0`,max:`100000`,disabled:c.value},null,8,fr),[[i,r.dailyLimit,void 0,{number:!0}]]),v(`small`,pr,e(K(a)(`可手动调整;0 表示不限。保存后立即生效。`)),1)])]),v(`div`,mr,[v(`span`,hr,e(K(a)(`今日有效查询 {v} / {limitLabel}`,{v:s.value.used||0,limitLabel:E.value})),1),v(`small`,gr,e(K(a)(`{v} 次进行中 · {v2} 次失败 · {v3} 次测试`,{v:s.value.usage?.running||0,v2:s.value.usage?.failed||0,v3:s.value.usage?.diagnostics||0})),1)]),v(`p`,_r,e(K(a)(`测试当前填写的配置,不会自动保存。失败与连接测试不占本地每日额度;提供方的账户额度由提供方管理。`)),1),v(`div`,vr,[v(`button`,{class:`primary`,disabled:c.value},e(K(a)(`保存搜索`)),9,yr),v(`button`,{type:`button`,disabled:c.value,onClick:M},e(c.value?K(a)(`处理中…`):K(a)(`测试连接`)),9,br),x.value?(w(),u(`span`,xr,e(K(a)(`有尚未保存的修改`)),1)):y(``,!0)]),d.value?(w(),u(`p`,{key:0,role:p.value?`alert`:`status`,class:m([`search-result`,{failed:p.value}])},e(d.value),11,Sr)):y(``,!0)],32)):y(``,!0)]))}}),[[`__scopeId`,`data-v-e874d8b6`]]),wr={key:0,class:`card data-transfer`,"aria-labelledby":`data-transfer-title`},Tr={class:`eyebrow`},Er={id:`data-transfer-title`},Dr={class:`transfer-note`},Or={class:`transfer-note`},kr={class:`transfer-actions`},Ar=[`disabled`],jr=[`disabled`],Mr=[`aria-label`],Nr={class:`transfer-note`},Pr={key:0,class:`transfer-progress`,role:`status`},Fr={key:1,class:`transfer-error`,role:`alert`},Ir={key:2,class:`transfer-note`},Lr={key:3,class:`transfer-preview`},Rr={class:`eyebrow`},zr={class:`transfer-note`},Br={class:`transfer-counts`},Vr={class:`transfer-actions`},Hr=[`disabled`],Ur=[`disabled`],Wr=F(z({__name:`DataTransfer`,setup(n){let i=f(null),s=f(null),l=f(),m=f(``),h=f(``),g=!0,_=null,b=o(()=>!!m.value),x=o(()=>m.value===`restarting`?a(`正在保留快照并重启加载,请等待…`):m.value===`uploading`?a(`正在上传数据文件…`):m.value===`exporting`?a(`正在生成完整数据包…`):a(`正在校验数据文件…`)),S=o(()=>{let e=s.value?.summary?.tables||{};return[[a(`聊天记录`),e.core_events||e.messages||0],[a(`记忆`),e.core_memories||e.memories||0],[a(`待办`),e.mind_time_tasks||0],[a(`作品`),e.mind_time_works||0],[a(`日记`),e.mind_diary||0]]}),C=()=>new Promise(e=>setTimeout(e,800));async function T(e,t=!1){for(let n=0;n<4500&&g;n++){try{let n=await P(`/storage/transfer/jobs/`+e);if(n.status===`error`)throw Error(n.error||a(`数据处理失败`));if(n.status===(t?`done`:`ready`))return n;if(n.status===`expired`)throw Error(a(`数据文件已过期,请重新选择`))}catch(e){if(!t||!(e instanceof TypeError)&&e.message!==a(`请求失败`))throw e}await C()}throw Error(a(`等待数据处理超时,请检查实例运行状态`))}async function E(){if(!b.value){m.value=`exporting`,h.value=``;try{let e=await P(`/storage/transfer/export`,`POST`,{});if(await T(e.id),!g)return;let t=document.createElement(`a`);t.href=`/api/storage/transfer/download/`+e.id,t.download=``,document.body.appendChild(t),t.click(),t.remove(),W(a(`数据包已生成,浏览器将开始下载`))}catch(e){g&&(h.value=e.message)}finally{g&&(m.value=``)}}}async function O(e){let t=e.target,n=t.files?.[0];if(t.value=``,n&&!b.value){if(h.value=``,s.value=null,n.size>(i.value?.maxBytes||0)){h.value=a(`数据文件超过导入大小限制`);return}m.value=`uploading`,_=new AbortController;try{await p();let e=await fetch(`/api/storage/transfer/upload`,{method:`POST`,credentials:`same-origin`,headers:{"Content-Type":`application/octet-stream`,"X-LuckyTri-Filename":encodeURIComponent(n.name),"X-LuckyTri-Locale":c.value},body:n,signal:_.signal});if(e.status===401)throw d(),Error(a(`请先登录管理台`));let t=await e.json().catch(()=>({}));if(!e.ok)throw Error(t.error||a(`数据上传失败`));m.value=`checking`;let r=await T(t.id);g&&(s.value=r)}catch(e){g&&(h.value=e.message)}finally{_=null,g&&(m.value=``)}}}async function A(){if(s.value&&!b.value)try{await P(`/storage/transfer/jobs/`+s.value.id,`DELETE`),s.value=null}catch(e){h.value=e.message}}async function j(){if(!s.value||b.value||!await t(a(`用这份数据替换当前数据,并保留导入前快照。实例将重启,导入后自动回复关闭。本机管理密码与 QQ 连接配置保持不变。`),{title:a(`确认导入数据`),confirmText:a(`导入并重启`),danger:!0}))return;m.value=`restarting`,h.value=``;let e=s.value.id;try{if(await P(`/storage/transfer/import`,`POST`,{id:e}),await T(e,!0),!g)return;k(),location.reload()}catch(e){g&&(h.value=e.message)}finally{g&&(m.value=``)}}return N(async()=>{try{i.value=await P(`/storage/transfer`)}catch(e){g&&(h.value=e.message)}}),V(()=>{g=!1,_?.abort()}),(t,n)=>i.value?.available?(w(),u(`section`,wr,[v(`span`,Tr,e(K(a)(`DATA · 导入与导出`)),1),v(`h2`,Er,e(K(a)(`带上记忆,继续生活。`)),1),v(`p`,Dr,e(K(a)(`导出数据库内的聊天、记忆、待办、作品、知识与设置。插件文件、知识库原始文件和本机 .env 不在数据包中。`)),1),v(`p`,Or,e(K(a)(`数据包包含私聊内容和模型设置中的密钥,请存放在你信任的位置。导入前会校验,确认后才替换当前数据。`)),1),v(`div`,kr,[v(`button`,{class:`primary`,type:`button`,disabled:b.value,onClick:E},e(K(a)(`导出数据包`)),9,Ar),v(`button`,{type:`button`,disabled:b.value,onClick:n[0]||=e=>l.value?.click()},e(K(a)(`选择导入文件`)),9,jr),v(`input`,{ref_key:`input`,ref:l,class:`transfer-file`,type:`file`,accept:`.luckytri,.db,.sqlite,.sqlite3`,"aria-label":K(a)(`导入数据文件`),onChange:O},null,40,Mr)]),v(`p`,Nr,e(K(a)(`支持 .luckytri 数据包和已有 SQLite 备份,单个文件最多 {size} GiB。`,{size:(i.value.maxBytes/1073741824).toFixed(0)})),1),b.value?(w(),u(`p`,Pr,e(x.value),1)):y(``,!0),h.value?(w(),u(`p`,Fr,e(h.value),1)):y(``,!0),i.value.lastImport?.status===`done`?(w(),u(`p`,Ir,e(K(a)(`上次数据导入已完成,自动回复在导入时关闭。请检查连接、模型和待办后再开启;插件需重新安装或确认权限。`)),1)):y(``,!0),s.value?(w(),u(`div`,Lr,[v(`span`,Rr,e(K(a)(`校验通过 · 尚未导入`)),1),v(`h3`,null,e(s.value.summary?.name),1),v(`p`,zr,e(s.value.name)+` · `+e(K(a)(`数据库版本 {from} → {to}`,{from:s.value.originalVersion,to:s.value.summary?.schemaVersion})),1),v(`div`,Br,[(w(!0),u(r,null,D(S.value,([t,n])=>(w(),u(`div`,{key:t},[v(`b`,null,e(n),1),v(`span`,null,e(t),1)]))),128))]),v(`div`,Vr,[v(`button`,{class:`primary`,type:`button`,disabled:b.value,onClick:j},e(K(a)(`导入并重启`)),9,Hr),v(`button`,{type:`button`,disabled:b.value,onClick:A},e(K(a)(`取消导入`)),9,Ur)])])):y(``,!0)])):y(``,!0)}}),[[`__scopeId`,`data-v-ea16c0c1`]]),Gr={class:`storage-page`},Kr={class:`card storage-overview`},qr={class:`eyebrow`},Jr={key:0,class:`storage-metrics`},Yr={key:0,class:`storage-error`,role:`alert`},Xr={key:1,class:`storage-grid`},Zr={class:`card-head`},Qr={class:`eyebrow`},$r={class:`storage-check`},ei={class:`storage-fields`},ti=[`aria-label`],ni=[`aria-label`],ri=[`aria-label`],ii={class:`storage-note`},ai={key:0,class:`storage-note`},oi={key:1,class:`storage-error`},si=[`disabled`],ci={class:`card storage-list`},li={class:`card-head`},ui={class:`eyebrow`},di={class:`storage-note`},fi={class:`storage-actions`},pi=[`disabled`],mi=[`disabled`],hi=[`disabled`],gi={class:`storage-scroll`},_i=[`checked`,`disabled`,`aria-label`,`onChange`],vi={class:`storage-item`},yi={key:0,class:`storage-error`,role:`alert`},bi={class:`storage-footer`},xi=[`disabled`],Si=F(z({__name:`BackupManager`,setup(n){let c=f(null),l=f({enabled:!0,time:`04:30`,retainDays:14,keepFull:2}),d=f([]),p=f(!1),h=f(``),_=f([]),b=!0,x=e=>`${e.scope}:${e.name}`,S=o(()=>c.value?.entries.filter(e=>d.value.includes(x(e)))||[]),E=o(()=>S.value.reduce((e,t)=>e+t.bytes,0)),O=e=>e===0?`0 B`:e>=1073741824?`${(e/1073741824).toFixed(2)} GiB`:`${Math.max(.01,e/1048576).toFixed(2)} MiB`,k=e=>new Date(e).toLocaleString(s()),A=e=>({auto:a(`自动备份`),full:a(`完整备份`),migration:a(`迁移快照`),legacy:a(`旧版本备份`),snapshot:a(`升级前快照`),import:a(`导入前快照`)})[e];async function j(e=!1){try{h.value=``;let t=await P(`/storage/cleanup`);if(!b)return;if(c.value=t,e)l.value={...c.value.policy},d.value=[],B.dirty=!1;else{let e=new Set(c.value.entries.filter(e=>!e.protected).map(x));d.value=d.value.filter(t=>e.has(t))}}catch(e){b&&(h.value=e.message)}}function M(){B.dirty=!0}function F(e){let t=x(e);d.value=d.value.includes(t)?d.value.filter(e=>e!==t):[...d.value,t]}function I(){d.value=(c.value?.plan.items||[]).map(x)}function L(){d.value=(c.value?.entries||[]).filter(e=>e.kind===`full`&&!e.protected).map(x)}async function R(){if(!p.value){p.value=!0;try{let e=await P(`/storage/cleanup`,`PATCH`,{enabled:l.value.enabled,time:l.value.time,retainDays:Number(l.value.retainDays),keepFull:Number(l.value.keepFull)});c.value=e,l.value={...e.policy},B.dirty=!1,W(a(`备份整理规则已保存`))}catch(e){W(e.message,!0)}finally{p.value=!1}}}async function z(){if(!S.value.length||p.value)return;let e=S.value.map(({scope:e,name:t,bytes:n,modified:r})=>({scope:e,name:t,bytes:n,modified:r}));if(await t(a(`将清理选中的 {count} 份备份,释放约 {size}。这些历史恢复点清理后无法从本机找回。`,{count:e.length,size:O(E.value)}),{title:a(`清理所选备份`),confirmText:a(`清理所选`),danger:!0})){p.value=!0;try{let t=await P(`/storage/cleanup/manual`,`POST`,{items:e});_.value=t.failures||[];let n=new Set((t.failures||[]).map(e=>`${e.scope}:${e.name}`));d.value=d.value.filter(e=>n.has(e)),await j(),W(a(`已清理 {count} 份备份,释放 {size}`,{count:t.removed.length,size:O(t.bytes)})+(_.value.length?a(`;部分未清理,详情见列表`):``),_.value.length>0)}catch(e){W(e.message,!0),await j()}finally{p.value=!1}}}return N(()=>j(!0)),V(()=>{b=!1}),(t,n)=>(w(),u(`div`,Gr,[v(`section`,Kr,[v(`span`,qr,e(K(a)(`DATA · 数据与备份`)),1),v(`h2`,null,e(K(a)(`留住重要的,整理重复的。`)),1),v(`p`,null,e(K(a)(`这里管理历史备份。聊天、记忆和作品仍在正在运行的数据库里,不会被列入清理。`)),1),c.value?(w(),u(`div`,Jr,[v(`div`,null,[v(`b`,null,e(O(c.value.totalBytes)),1),v(`span`,null,e(K(a)(`可管理的历史备份`)),1)]),v(`div`,null,[v(`b`,null,e(c.value.entries.length),1),v(`span`,null,e(K(a)(`份数据库副本`)),1)]),v(`div`,null,[v(`b`,null,e(O(c.value.plan.bytes)),1),v(`span`,null,e(K(a)(`按当前规则可定时整理`)),1)])])):y(``,!0)]),h.value?(w(),u(`p`,Yr,e(h.value),1)):y(``,!0),H(Wr),c.value?(w(),u(`div`,Xr,[v(`form`,{class:`card storage-policy`,onSubmit:g(R,[`prevent`])},[v(`div`,Zr,[v(`div`,null,[v(`span`,Qr,e(K(a)(`定时整理`)),1),v(`h2`,null,e(K(a)(`每天给备份留一点空间`)),1)])]),v(`label`,$r,[C(v(`input`,{"onUpdate:modelValue":n[0]||=e=>l.value.enabled=e,type:`checkbox`,onChange:M},null,544),[[J,l.value.enabled]]),T(e(K(a)(`启用定时整理`)),1)]),v(`div`,ei,[v(`label`,null,[T(e(K(a)(`每天执行时间`)),1),C(v(`input`,{"onUpdate:modelValue":n[1]||=e=>l.value.time=e,type:`text`,inputmode:`numeric`,maxlength:`5`,placeholder:`04:30`,"aria-label":K(a)(`每天执行时间`),onInput:M},null,40,ti),[[i,l.value.time]])]),v(`label`,null,[T(e(K(a)(`完整备份保留天数`)),1),C(v(`input`,{"onUpdate:modelValue":n[2]||=e=>l.value.retainDays=e,type:`number`,min:`1`,max:`3650`,"aria-label":K(a)(`完整备份保留天数`),onInput:M},null,40,ni),[[i,l.value.retainDays,void 0,{number:!0}]])]),v(`label`,null,[T(e(K(a)(`完整备份最多保留份数`)),1),C(v(`input`,{"onUpdate:modelValue":n[3]||=e=>l.value.keepFull=e,type:`number`,min:`0`,max:`100`,"aria-label":K(a)(`完整备份最多保留份数`),onInput:M},null,40,ri),[[i,l.value.keepFull,void 0,{number:!0}]])])]),v(`p`,ii,e(K(a)(`时间按服务器本地时区 {zone}。完整备份超过保留天数或数量上限便会定时整理;数量填 0 表示不保留完整备份。手动清理可选择完整备份和迁移快照,执行前会校验同一实例的近期自动恢复点。`,{zone:c.value.serverTimeZone})),1),c.value.last?(w(),u(`p`,ai,e(c.value.last.error&&c.value.last.removed===0?K(a)(`上次整理未执行:{error}`,{error:c.value.last.error}):K(a)(`上次整理:{time} · {count} 份 · {size}`,{time:k(c.value.last.at),count:c.value.last.removed,size:O(c.value.last.bytes)})),1)):y(``,!0),c.value.last?.error&&c.value.last.removed>0?(w(),u(`p`,oi,e(c.value.last.error),1)):y(``,!0),v(`button`,{class:`primary`,type:`submit`,disabled:p.value},e(p.value?K(a)(`处理中…`):K(a)(`保存整理规则`)),9,si)],32),v(`section`,ci,[v(`div`,li,[v(`div`,null,[v(`span`,ui,e(K(a)(`手动整理`)),1),v(`h2`,null,e(K(a)(`挑选历史恢复点`)),1)])]),v(`p`,di,e(K(a)(`自动备份已有自己的轮换规则。迁移、旧版本和升级前快照只由你手动挑选;清理前会再次核对文件和恢复点。`)),1),v(`div`,fi,[v(`button`,{type:`button`,disabled:p.value||!c.value.plan.items.length,onClick:I},e(K(a)(`选中规则建议`)),9,pi),v(`button`,{type:`button`,disabled:p.value,onClick:L},e(K(a)(`选中完整备份`)),9,mi),v(`button`,{type:`button`,disabled:p.value||!d.value.length,onClick:n[4]||=e=>d.value=[]},e(K(a)(`取消选择`)),9,hi)]),v(`div`,gi,[(w(!0),u(r,null,D(c.value.entries,t=>(w(),u(`label`,{key:x(t),class:m([`storage-row`,{protected:t.protected}])},[v(`input`,{type:`checkbox`,checked:d.value.includes(x(t)),disabled:p.value||t.protected||c.value.running,"aria-label":t.name,onChange:e=>F(t)},null,40,_i),v(`span`,vi,[v(`strong`,null,e(t.name),1),v(`small`,null,e(A(t.kind))+` · `+e(k(t.at))+` · `+e(t.manifest?K(a)(`有校验清单`):K(a)(`旧备份无清单`))+e(t.protected?` · `+(t.protectionReason||K(a)(`保留恢复点`)):``),1)]),v(`b`,null,e(O(t.bytes)),1)],2))),128))]),_.value.length?(w(),u(`div`,yi,[v(`p`,null,e(K(a)(`部分备份未清理,请刷新列表后重试`)),1),(w(!0),u(r,null,D(_.value,t=>(w(),u(`p`,{key:t.name},e(t.name)+` · `+e(t.error),1))),128))])):y(``,!0),v(`div`,bi,[v(`span`,null,e(K(a)(`已选 {count} 份 · 约 {size}`,{count:S.value.length,size:O(E.value)})),1),v(`button`,{class:`primary`,type:`button`,disabled:p.value||c.value.running||!S.value.length,onClick:z},e(K(a)(`清理所选`)),9,xi)])])])):y(``,!0)]))}}),[[`__scopeId`,`data-v-4593780e`]]),Ci={class:`card password-settings`},wi={class:`card-head`},Ti={class:`row`},Ei=[`disabled`],Di=F(z({__name:`PasswordSettings`,setup(t){let n=f(``),r=f(``),o=f(``),s=f(!1);async function c(){if(!s.value){if(r.value!==o.value){W(a(`两次输入的密码不一致`),!0);return}s.value=!0;try{await S(`password`,{currentPassword:n.value,password:r.value}),n.value=r.value=o.value=``,W(a(`管理密码已更新`))}catch(e){W(e.message,!0)}finally{s.value=!1}}}return(t,l)=>(w(),u(`section`,Ci,[v(`div`,wi,[v(`div`,null,[v(`h2`,null,e(K(a)(`管理密码`)),1),v(`p`,null,e(K(a)(`一个密码管理这个小世界。修改后,其他浏览器需要重新登录。`)),1)])]),v(`form`,{onSubmit:g(c,[`prevent`])},[v(`label`,null,[T(e(K(a)(`当前密码`)),1),C(v(`input`,{"onUpdate:modelValue":l[0]||=e=>n.value=e,name:`currentPassword`,type:`password`,autocomplete:`current-password`,required:``},null,512),[[i,n.value]])]),v(`label`,null,[T(e(K(a)(`新密码`)),1),C(v(`input`,{"onUpdate:modelValue":l[1]||=e=>r.value=e,name:`newPassword`,type:`password`,autocomplete:`new-password`,minlength:`8`,maxlength:`256`,required:``},null,512),[[i,r.value]])]),v(`label`,null,[T(e(K(a)(`确认密码`)),1),C(v(`input`,{"onUpdate:modelValue":l[2]||=e=>o.value=e,name:`confirmNewPassword`,type:`password`,autocomplete:`new-password`,minlength:`8`,maxlength:`256`,required:``},null,512),[[i,o.value]])]),v(`div`,Ti,[v(`button`,{class:`primary`,disabled:s.value},e(K(a)(`修改密码`)),9,Ei),v(`button`,{type:`button`,onClick:l[3]||=(...e)=>K(G)&&K(G)(...e)},e(K(a)(`退出登录`)),1)])],32)]))}}),[[`__scopeId`,`data-v-415fadf7`]]),Oi={class:`card update-settings`,"aria-labelledby":`update-title`},ki={id:`update-title`},Ai=[`disabled`],ji={class:`kv`},Mi={key:0,class:`muted`,role:`alert`},Ni={class:`muted small-text`},Pi={key:1,class:`update-how stack tight`},Fi=[`disabled`],Ii={key:1,role:`status`},Li={key:2,class:`error-text`,role:`alert`},Ri={key:0,class:`error-text`},zi={key:1,class:`error-text`},Bi={key:2,class:`error-text`},Vi={key:4,class:`muted`},Hi={class:`update-links`},Ui=[`href`],Wi=[`href`],Gi=F(z({__name:`UpdateSettings`,setup(n){let i=f(null),o=f(!1),s=f(!1),c,d=!1,p=l({queued:M(`更新已开始`),downloading:M(`正在下载新版本…`),checking:M(`正在检查新程序…`),stopping:M(`正在保存当下并停止实例…`),backup:M(`正在保留更新前的校验备份…`),migrating:M(`正在整理并校验记忆与数据…`),restarting:M(`正在切换版本并重启…`),verifying:M(`正在确认新实例就绪…`),restoring:M(`正在恢复原版本和数据…`),done:M(`更新与重启已完成`),error:M(`更新未完成`)}),h=l({queued:M(`准备更新`),downloading:M(`下载新版本`),checking:M(`检查新程序`),stopping:M(`停止旧实例`),backup:M(`保存校验备份`),migrating:M(`迁移记忆与数据`),restarting:M(`切换并重启`),verifying:M(`检查新实例`),restoring:M(`恢复原版本`)});function g(e){return e&&![`done`,`error`].includes(e.status)}async function _(){if(!d){try{let e=await P(`/system/update`);if(i.value=e,e.operation?.status===`done`){s.value=!1,location.reload();return}s.value=!!g(e.operation)}catch{}s.value&&!d&&(c=setTimeout(_,1800))}}U(()=>{d=!0,clearTimeout(c)});let b=l({unchecked:M(`尚未检查更新`),available:M(`有新版本可用`),current:M(`已是最新发布版本`),ahead:M(`当前版本领先于 npm 发布版本`),unavailable:M(`暂时无法检查更新`)});N(async()=>{try{i.value=await P(`/system/update`),s.value=!!g(i.value.operation),s.value&&(c=setTimeout(_,1800))}catch(e){W(e.message,!0)}});async function x(){if(!o.value){o.value=!0;try{i.value=await P(`/system/update/check`,`POST`,{})}catch(e){W(e.message,!0)}finally{o.value=!1}}}async function S(){if(!(s.value||o.value||i.value?.status!==`available`)&&await t(a(`更新到 {version} 并重启当前实例?先下载校验,再保留数据恢复点;启动失败时尝试恢复原版本。`,{version:i.value.latest}),{title:a(`更新并重启`),confirmText:a(`开始更新`)})){s.value=!0;try{i.value.operation=await P(`/system/update/install`,`POST`,{version:i.value.latest}),c=setTimeout(_,800)}catch(e){s.value=!1,W(e.message,!0)}}}return(t,n)=>(w(),u(`section`,Oi,[v(`header`,null,[v(`div`,null,[n[0]||=v(`span`,{class:`eyebrow`},`LUCKYTRI / VERSION`,-1),v(`h2`,ki,e(K(a)(`版本与更新`)),1)]),v(`button`,{disabled:o.value||s.value||!i.value,onClick:x},e(o.value?K(a)(`正在检查…`):K(a)(`检查更新`)),9,Ai)]),i.value?(w(),u(r,{key:0},[v(`p`,{class:m([`version-state`,{available:i.value.status===`available`}]),role:`status`},e(K(b)[i.value.status]||K(b).unchecked),3),v(`dl`,ji,[v(`dt`,null,e(K(a)(`当前版本`)),1),v(`dd`,null,e(i.value.current),1),v(`dt`,null,e(K(a)(`npm 最新版本`)),1),v(`dd`,null,e(i.value.latest||`—`),1),v(`dt`,null,e(K(a)(`上次成功检查`)),1),v(`dd`,null,e(i.value.checkedAt?K(ne)(i.value.checkedAt):`—`),1)]),i.value.error?(w(),u(`p`,Mi,e(K(a)(`暂时无法连接 npm 或读取版本信息,请稍后重试。`)),1)):y(``,!0),v(`p`,Ni,e(K(a)(`点击时才检查,五分钟内复用结果。检查更新不会自动安装或重启。`)),1),i.value.status===`available`||i.value.operation?(w(),u(`div`,Pi,[v(`p`,null,e(K(a)(`下载期间继续运行,切换前保留数据恢复点。更新完成后此页面会自动重新连接。`)),1),i.value.status===`available`?(w(),u(`button`,{key:0,class:`primary`,disabled:s.value||o.value,onClick:S},e(s.value?K(a)(`正在更新…`):K(a)(`更新并重启`)),9,Fi)):y(``,!0),i.value.operation?(w(),u(`p`,Ii,e(K(p)[i.value.operation.status])+` · `+e(i.value.operation.version),1)):y(``,!0),i.value.operation?.error?(w(),u(`p`,Li,e(i.value.operation.error),1)):y(``,!0),i.value.operation?.status===`error`?(w(),u(r,{key:3},[K(h)[i.value.operation.failedStage]?(w(),u(`p`,Ri,e(K(a)(`失败阶段:{stage}`,{stage:K(h)[i.value.operation.failedStage]})),1)):y(``,!0),i.value.operation.detail?(w(),u(`p`,zi,e(i.value.operation.detail),1)):y(``,!0),i.value.operation.recoveryDetail?(w(),u(`p`,Bi,e(i.value.operation.recoveryDetail),1)):y(``,!0)],64)):y(``,!0),i.value.operation?.restored?(w(),u(`p`,Vi,e(K(a)(`已恢复原版本和更新前的数据。`)),1)):y(``,!0)])):y(``,!0),v(`nav`,Hi,[v(`a`,{href:i.value.npmUrl,target:`_blank`,rel:`noopener noreferrer`},`npm`,8,Ui),v(`a`,{href:i.value.releaseUrl,target:`_blank`,rel:`noopener noreferrer`},e(K(a)(`发布说明`))+` ↗`,9,Wi)])],64)):y(``,!0)]))}}),[[`__scopeId`,`data-v-afcb3dd7`]]),Ki={class:`card stickers`},qi={class:`card-head`},Ji={class:`eyebrow`},Yi={class:`faint`},Xi={class:`toolbar`},Zi=[`placeholder`],Qi={value:``},$i=[`value`],ea=[`placeholder`],ta={key:0,class:`faint`,"data-testid":`sticker-progress`},na={key:1,class:`grid`},ra=[`onClick`],ia=[`src`,`alt`],aa={class:`caption`},oa={key:0,class:`badge`},sa={key:1,class:`badge gif`},ca={key:2,class:`editor card`,role:`dialog`},la={class:`editor-head`},ua=[`src`],da={key:0,class:`danger`},fa={class:`fields`},pa={class:`wide`},ma={class:`wide`},ha={class:`wide`},ga={class:`wide`},_a={class:`check`},va={class:`save-bar`},ya=F(z({__name:`StickerLibrary`,setup(n){let s=f([]),l=f([]),b=f(``),x=f(``),S=f(``),E=f(``),O=f(null),k=f({}),A=f(),j=o(()=>s.value.filter(e=>e.status===`pending`).length);async function M(){let e=new URLSearchParams;b.value.trim()&&e.set(`q`,b.value.trim()),x.value&&e.set(`category`,x.value);let t=await P(`/core/stickers?${e}`);s.value=t.items,l.value=t.categories}let F;function I(){clearTimeout(F),j.value&&(F=setTimeout(()=>M().then(I).catch(()=>{}),3e3))}N(()=>M().then(I).catch(e=>W(e.message,!0))),U(()=>clearTimeout(F));async function L(e){let t=Array.from(e||[]);if(!t.length)return;let n=0,r=0;await p();for(let e of t){E.value=a(`正在上传 {done}/{total}`,{done:n,total:t.length});let i=new URLSearchParams({name:e.name});S.value.trim()&&i.set(`category`,S.value.trim());let o=await fetch(`/api/core/stickers/upload?${i}`,{method:`POST`,credentials:`same-origin`,headers:{"Content-Type":`application/octet-stream`,"X-LuckyTri-Locale":c.value},body:e});if(o.status===401){d();break}let s=await o.json().catch(()=>({}));o.ok?n++:(r++,W(`${e.name}:${s.error||a(`上传失败`)}`,!0))}E.value=``,W(a(`已上传 {done} 张,正在自动识别含义`,{done:n})+(r?a(`,{failed} 张未通过`,{failed:r}):``)),A.value&&(A.value.value=``),await M(),I()}function R(e){e.preventDefault(),L(e.dataTransfer?.files||null)}function z(e){O.value=e,k.value={...e,tags:e.tags.join(`、`)}}async function B(){if(O.value)try{await P(`/core/stickers/${O.value.id}`,`PATCH`,{name:k.value.name,category:k.value.category,tags:k.value.tags,description:k.value.description,words:k.value.words,emotion:k.value.emotion,meaning:k.value.meaning,scenes:k.value.scenes,enabled:k.value.enabled}),O.value=null,W(a(`已保存`)),await M()}catch(e){W(e.message,!0)}}async function V(e){await P(`/core/stickers/${e.id}/describe`,`POST`,{}),W(a(`已重新识别,稍后刷新查看`)),await M(),I()}async function H(e){await t(a(`删除这张表情?它和大家一起用过的记录也会一并删除。`),{title:a(`删除表情`),confirmText:a(`删除`),cancelText:a(`取消`)})&&(await P(`/core/stickers/${e.id}`,`DELETE`),O.value=null,await M())}let G=e=>e.status===`pending`?a(`识别中`):e.status===`failed`?a(`识别失败`):e.status===`manual`?a(`待手动填写`):``;return(t,n)=>(w(),u(`section`,Ki,[v(`div`,qi,[v(`div`,null,[v(`span`,Ji,e(K(a)(`表情包`)),1),v(`h2`,null,e(K(a)(`TA 收藏的表情`)),1),v(`p`,Yi,e(K(a)(`上传后会自动看懂画面、文字、情绪和用法,你可以随时修改。TA 只在觉得合适时才用,也会记得和谁用过、对方怎么回应。`)),1)])]),v(`div`,{class:`drop`,"data-testid":`sticker-drop`,onDragover:n[1]||=g(()=>{},[`prevent`]),onDrop:R,onClick:n[2]||=e=>A.value?.click()},[v(`b`,null,e(E.value||K(a)(`拖入或点击选择图片,可一次选多张`)),1),v(`small`,null,e(K(a)(`PNG、JPEG、GIF、WebP,每张不超过 2MB`)),1),v(`input`,{ref_key:`fileInput`,ref:A,type:`file`,accept:`image/png,image/jpeg,image/gif,image/webp`,multiple:``,hidden:``,onChange:n[0]||=e=>L(e.target.files)},null,544)],32),v(`div`,Xi,[C(v(`input`,{"onUpdate:modelValue":n[3]||=e=>b.value=e,type:`search`,placeholder:K(a)(`搜索描述、标签、情绪…`),onInput:n[4]||=e=>M()},null,40,Zi),[[i,b.value]]),C(v(`select`,{"onUpdate:modelValue":n[5]||=e=>x.value=e,onChange:n[6]||=e=>M()},[v(`option`,Qi,e(K(a)(`全部分类`)),1),(w(!0),u(r,null,D(l.value,t=>(w(),u(`option`,{key:t.category,value:t.category},e(t.category)+` · `+e(t.n),9,$i))),128))],544),[[h,x.value]]),C(v(`input`,{"onUpdate:modelValue":n[7]||=e=>S.value=e,placeholder:K(a)(`上传到分类(可选)`)},null,8,ea),[[i,S.value]]),j.value?(w(),u(`small`,ta,e(K(a)(`{n} 张识别中`,{n:j.value})),1)):y(``,!0)]),s.value.length?(w(),u(`div`,na,[(w(!0),u(r,null,D(s.value,t=>(w(),u(`button`,{key:t.id,class:m([`tile`,{off:!t.enabled}]),type:`button`,onClick:e=>z(t)},[v(`img`,{src:`/api/core/stickers/${t.id}/image`,alt:t.description||t.name,loading:`lazy`},null,8,ia),v(`span`,aa,e(t.meaning||t.description||t.name),1),G(t)?(w(),u(`span`,oa,e(G(t)),1)):y(``,!0),t.animated?(w(),u(`span`,sa,`GIF`)):y(``,!0)],10,ra))),128))])):(w(),_(ee,{key:0,title:K(a)(`还没有表情`),text:K(a)(`上传几张常用的表情,TA 就能在聊天里自己挑着用。`)},null,8,[`title`,`text`])),O.value?(w(),u(`div`,ca,[v(`div`,la,[v(`img`,{src:`/api/core/stickers/${O.value.id}/image`,alt:``},null,8,ua),v(`div`,null,[v(`b`,null,e(O.value.name||K(a)(`未命名`)),1),O.value.error?(w(),u(`small`,da,e(O.value.error),1)):y(``,!0)])]),v(`div`,fa,[v(`label`,null,[T(e(K(a)(`名称`)),1),C(v(`input`,{"onUpdate:modelValue":n[8]||=e=>k.value.name=e,maxlength:`60`},null,512),[[i,k.value.name]])]),v(`label`,null,[T(e(K(a)(`分类`)),1),C(v(`input`,{"onUpdate:modelValue":n[9]||=e=>k.value.category=e,maxlength:`24`},null,512),[[i,k.value.category]])]),v(`label`,pa,[T(e(K(a)(`标签(用顿号或逗号分隔)`)),1),C(v(`input`,{"onUpdate:modelValue":n[10]||=e=>k.value.tags=e},null,512),[[i,k.value.tags]])]),v(`label`,ma,[T(e(K(a)(`画面`)),1),C(v(`textarea`,{"onUpdate:modelValue":n[11]||=e=>k.value.description=e,rows:`2`},null,512),[[i,k.value.description]])]),v(`label`,null,[T(e(K(a)(`图上的字`)),1),C(v(`input`,{"onUpdate:modelValue":n[12]||=e=>k.value.words=e,maxlength:`100`},null,512),[[i,k.value.words]])]),v(`label`,null,[T(e(K(a)(`情绪`)),1),C(v(`input`,{"onUpdate:modelValue":n[13]||=e=>k.value.emotion=e,maxlength:`40`},null,512),[[i,k.value.emotion]])]),v(`label`,ha,[T(e(K(a)(`意思或梗`)),1),C(v(`textarea`,{"onUpdate:modelValue":n[14]||=e=>k.value.meaning=e,rows:`2`},null,512),[[i,k.value.meaning]])]),v(`label`,ga,[T(e(K(a)(`适合的情境`)),1),C(v(`textarea`,{"onUpdate:modelValue":n[15]||=e=>k.value.scenes=e,rows:`2`},null,512),[[i,k.value.scenes]])]),v(`label`,_a,[C(v(`input`,{"onUpdate:modelValue":n[16]||=e=>k.value.enabled=e,type:`checkbox`},null,512),[[J,k.value.enabled]]),T(e(K(a)(`TA 可以使用这张`)),1)])]),v(`div`,va,[v(`button`,{class:`primary`,type:`button`,onClick:B},e(K(a)(`保存`)),1),v(`button`,{type:`button`,onClick:n[17]||=e=>V(O.value)},e(K(a)(`重新识别`)),1),v(`button`,{type:`button`,class:`danger`,onClick:n[18]||=e=>H(O.value)},e(K(a)(`删除`)),1),v(`button`,{type:`button`,onClick:n[19]||=e=>O.value=null},e(K(a)(`关闭`)),1)])])):y(``,!0)]))}}),[[`__scopeId`,`data-v-04cb4590`]]),ba={class:`card-head`},xa={class:`eyebrow`},Sa={class:`fields`},Ca={class:`faint`},wa={class:`faint`},Ta={class:`faint`},Ea={value:``},Da=[`value`],Oa={class:`faint`},ka={class:`switches`},Aa={class:`save-bar`},ja={class:`primary`,type:`submit`},Ma=F(z({__name:`MediaSettings`,setup(t){let n=f(2048),o=f(3),s=f(20),c=f(!0),l=f(``);N(async()=>{try{let e=await P(`/core/media-settings`);n.value=e.visionMaxSide,o.value=e.visionMaxMB,s.value=e.visionSourceMaxMB??20;let t=await P(`/core/stickers`);c.value=t.settings.enabled!==!1,l.value=t.settings.taggingModelId||``}catch(e){W(e.message,!0)}});async function d(){try{await P(`/core/media-settings`,`PUT`,{visionMaxSide:Number(n.value),visionMaxMB:Number(o.value),visionSourceMaxMB:Number(s.value)}),await P(`/core/stickers-settings`,`PUT`,{enabled:c.value,taggingModelId:l.value}),B.dirty=!1,W(a(`已保存并应用`))}catch(e){W(e.message,!0)}}return(t,f)=>(w(),u(`form`,{class:`card media`,"data-testid":`media-settings`,onSubmit:g(d,[`prevent`]),onChange:f[5]||=e=>K(B).dirty=!0},[v(`div`,ba,[v(`div`,null,[v(`span`,xa,e(K(a)(`图片与表情`)),1),v(`h2`,null,e(K(a)(`TA 怎样看图、用表情`)),1)])]),v(`div`,Sa,[v(`label`,null,[T(e(K(a)(`可接收的图片大小上限(MB)`))+` `,1),C(v(`input`,{"onUpdate:modelValue":f[0]||=e=>s.value=e,type:`number`,min:`1`,max:`64`,step:`1`},null,512),[[i,s.value,void 0,{number:!0}]]),v(`small`,Ca,e(K(a)(`聊天里不超过这个大小的图片都会先缩小压缩再看。默认 20。`)),1)]),v(`label`,null,[T(e(K(a)(`图片理解的最长边(像素)`))+` `,1),C(v(`input`,{"onUpdate:modelValue":f[1]||=e=>n.value=e,type:`number`,min:`512`,max:`8192`,step:`64`},null,512),[[i,n.value,void 0,{number:!0}]]),v(`small`,wa,e(K(a)(`更大的图会先缩小再交给视觉模型,避免大图失败或超时。默认 2048。`)),1)]),v(`label`,null,[T(e(K(a)(`图片理解的大小上限(MB)`))+` `,1),C(v(`input`,{"onUpdate:modelValue":f[2]||=e=>o.value=e,type:`number`,min:`0.5`,max:`4`,step:`0.5`},null,512),[[i,o.value,void 0,{number:!0}]]),v(`small`,Ta,e(K(a)(`超过时会重新压缩。部分服务商对单张图片有更严格的限制。`)),1)]),v(`label`,null,[T(e(K(a)(`表情识别用的模型`))+` `,1),C(v(`select`,{"onUpdate:modelValue":f[3]||=e=>l.value=e},[v(`option`,Ea,e(K(a)(`自动选择开启视觉的模型`)),1),(w(!0),u(r,null,D((K(B).core.models||[]).filter(e=>e.vision),t=>(w(),u(`option`,{key:t.id,value:t.id},e(t.label||t.model||t.id),9,Da))),128))],512),[[h,l.value]]),v(`small`,Oa,e(K(a)(`每张表情只识别一次,可选一个便宜的视觉模型。`)),1)])]),v(`div`,ka,[H(Wn,{modelValue:c.value,"onUpdate:modelValue":f[4]||=e=>c.value=e,name:`stickers`},{default:q(()=>[v(`b`,null,e(K(a)(`允许 TA 在聊天里用表情`)),1),v(`small`,null,e(K(a)(`关掉后表情库保留,TA 不再使用。`)),1)]),_:1},8,[`modelValue`])]),v(`div`,Aa,[v(`button`,ja,e(K(a)(`保存`)),1)])],32))}}),[[`__scopeId`,`data-v-48971015`]]),Na={class:`page system`},Pa={class:`system-intro`},Fa={class:`eyebrow`},Ia={class:`system-status`},La=F(z({__name:`SystemPage`,setup(n){let i=[`connect`,`models`,`stickers`,`settings`,`storage`],o=e=>e===`runtime`?`settings`:e===`search`?`models`:e,s=f(i.includes(o(B.sub))?o(B.sub):`connect`);async function c(e){e!==s.value&&(!B.dirty||await t(a(`当前设置还没保存,离开这个分区吗?`),{title:a(`未保存的修改`),confirmText:a(`放弃修改`),cancelText:a(`继续编辑`)}))&&(B.dirty=!1,s.value=e)}return b(s,e=>I(e)),b(()=>B.sub,e=>{i.includes(o(e))&&(s.value=o(e))}),(t,n)=>(w(),u(`div`,Na,[v(`header`,Pa,[v(`div`,null,[v(`span`,Fa,e(K(a)(`THE WORLD AROUND HER · 系统`)),1),v(`h1`,null,[T(e(K(a)(`让她与世界,`)),1),v(`em`,null,e(K(a)(`温柔相连。`)),1)]),v(`p`,null,e(K(a)(`连接、模型与运行方式都在这里。每一处改变,都可以看见结果。`)),1)]),v(`span`,Ia,[v(`i`,{class:m({off:!K(B).health.connection?.online})},null,2),T(` `+e(K(B).health.connection?.online?K(a)(`正在与 QQ 相连`):K(a)(`等待 QQ 连接`)),1)])]),H(ie,{"model-value":s.value,"onUpdate:modelValue":c,label:K(a)(`系统的分区`),items:[{key:`connect`,label:K(a)(`连接 QQ`)},{key:`models`,label:K(a)(`模型库`)},{key:`stickers`,label:K(a)(`表情包`)},{key:`settings`,label:K(a)(`各项设置`)},{key:`storage`,label:K(a)(`数据与备份`)}]},null,8,[`model-value`,`label`,`items`]),s.value===`connect`?(w(),_(We,{key:0,onOpen:c})):s.value===`models`?(w(),u(r,{key:1},[H(Vn),H(Cr)],64)):s.value===`stickers`?(w(),_(ya,{key:2})):s.value===`settings`?(w(),u(r,{key:3},[H(er),H(Ma),H(Gi),H(Di)],64)):(w(),_(Si,{key:4}))]))}}),[[`__scopeId`,`data-v-813b64f3`]]);export{La as default};