Compare commits
14 Commits
4b0541610b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 12db08abe0 | |||
| b0ed876cb0 | |||
| 1128d1494e | |||
| fb8ed8037c | |||
| d3f772073f | |||
| d8849c5e8b | |||
| 4ce79da429 | |||
| 618b2ca442 | |||
| c3cc86160b | |||
| cf0fc88a24 | |||
| 0ac347968e | |||
| 4eeebdd935 | |||
| 5dd9fb6f97 | |||
| bcf664e9a0 |
144
README.md
144
README.md
@@ -1,143 +1,7 @@
|
||||
# LittleWhiteBox
|
||||
# LittleWhiteBox
|
||||
|
||||
## 📁 目录结构
|
||||
一个面向 SillyTavern 的多功能扩展,包含剧情总结/记忆系统、变量系统、任务与多种面板能力。集成了画图、流式生成、模板编辑、调试面板等组件,适合用于复杂玩法与长期剧情记录。
|
||||
|
||||
```
|
||||
LittleWhiteBox/
|
||||
├── .editorconfig # 编辑器格式规范
|
||||
├── .eslintignore # ESLint 忽略规则
|
||||
├── .eslintrc.cjs # ESLint 配置
|
||||
├── .gitignore # Git 忽略规则
|
||||
├── index.js # 插件入口:初始化/注册所有模块
|
||||
├── jsconfig.json # JS/TS 编辑器提示
|
||||
├── manifest.json # 插件清单:版本/依赖/入口
|
||||
├── package-lock.json # 依赖锁定
|
||||
├── package.json # 开发依赖/脚本
|
||||
├── README.md # 说明文档
|
||||
├── settings.html # 主设置页:模块开关/UI
|
||||
├── style.css # 全局样式
|
||||
│
|
||||
├── bridges/ # 外部桥接
|
||||
│ ├── call-generate-service.js # 调用生成服务桥接
|
||||
│ ├── worldbook-bridge.js # 世界书桥接
|
||||
│ └── wrapper-iframe.js # iframe 包装桥接
|
||||
│
|
||||
├── core/ # 核心基础设施
|
||||
│ ├── constants.js # 常量/路径定义
|
||||
│ ├── debug-core.js # 日志/缓存注册
|
||||
│ ├── event-manager.js # 统一事件管理
|
||||
│ ├── iframe-messaging.js # postMessage 封装
|
||||
│ ├── server-storage.js # 服务器存储封装
|
||||
│ ├── slash-command.js # 斜杠命令封装
|
||||
│ ├── variable-path.js # 变量路径解析
|
||||
│ └── wrapper-inline.js # iframe 内联脚本
|
||||
│
|
||||
├── docs/ # 文档与许可
|
||||
│ ├── COPYRIGHT # 版权声明
|
||||
│ ├── LICENSE.md # 许可协议
|
||||
│ └── NOTICE # 通知/第三方声明
|
||||
│
|
||||
├── libs/ # 第三方库
|
||||
│ ├── dexie.mjs # IndexedDB 封装库
|
||||
│ ├── js-yaml.mjs # YAML 解析/序列化(ESM)
|
||||
│ ├── minisearch.mjs # 轻量搜索库
|
||||
│ ├── pixi.min.js # PixiJS 渲染库
|
||||
│ └── jieba-wasm/
|
||||
│ ├── jieba_rs_wasm.js # 结巴分词 WASM JS 包装
|
||||
│ ├── jieba_rs_wasm_bg.wasm # 结巴分词 WASM 二进制
|
||||
│ └── jieba_rs_wasm_bg.wasm.d.ts # WASM 类型声明
|
||||
│
|
||||
├── modules/ # 功能模块
|
||||
│ ├── control-audio.js # 音频权限控制
|
||||
│ ├── iframe-renderer.js # iframe 渲染
|
||||
│ ├── immersive-mode.js # 沉浸模式
|
||||
│ ├── message-preview.js # 消息预览/拦截
|
||||
│ ├── streaming-generation.js # 生成相关功能
|
||||
│ │
|
||||
│ ├── debug-panel/ # 调试面板
|
||||
│ │ ├── debug-panel.html # 调试面板 UI
|
||||
│ │ └── debug-panel.js # 调试面板逻辑
|
||||
│ │
|
||||
│ ├── fourth-wall/ # 四次元壁
|
||||
│ │ ├── fourth-wall.html # UI
|
||||
│ │ ├── fourth-wall.js # 主逻辑
|
||||
│ │ ├── fw-image.js # 图像相关增强
|
||||
│ │ ├── fw-message-enhancer.js # 消息增强
|
||||
│ │ ├── fw-prompt.js # 提示词/注入
|
||||
│ │ └── fw-voice.js # 语音相关
|
||||
│ │
|
||||
│ ├── novel-draw/ # 画图模块
|
||||
│ │ ├── cloud-presets.js # 云端预设
|
||||
│ │ ├── floating-panel.js # 浮动面板
|
||||
│ │ ├── gallery-cache.js # 图库缓存
|
||||
│ │ ├── image-live-effect.js # 图像动态效果
|
||||
│ │ ├── llm-service.js # LLM 服务调用
|
||||
│ │ ├── novel-draw.html # UI
|
||||
│ │ ├── novel-draw.js # 主逻辑
|
||||
│ │ └── TAG编写指南.md # TAG 编写指南
|
||||
│ │
|
||||
│ ├── scheduled-tasks/ # 定时任务
|
||||
│ │ ├── embedded-tasks.html # 内嵌任务 UI
|
||||
│ │ ├── scheduled-tasks.html # 主 UI
|
||||
│ │ └── scheduled-tasks.js # 逻辑
|
||||
│ │
|
||||
│ ├── story-outline/ # 故事大纲
|
||||
│ │ ├── story-outline-prompt.js # Prompt 模板
|
||||
│ │ ├── story-outline.html # UI
|
||||
│ │ └── story-outline.js # 逻辑
|
||||
│ │
|
||||
│ ├── story-summary/ # 剧情总结 + 记忆系统
|
||||
│ │ ├── story-summary-ui.js # UI 逻辑
|
||||
│ │ ├── story-summary.css # 样式
|
||||
│ │ ├── story-summary.html # UI(含向量设置)
|
||||
│ │ ├── story-summary.js # 主入口:事件/UI/iframe 通讯
|
||||
│ │ ├── data/
|
||||
│ │ │ ├── config.js # 配置管理
|
||||
│ │ │ ├── db.js # 向量存储:L1/L2 Vectors (Dexie/IndexedDB)
|
||||
│ │ │ └── store.js # 核心存储:L2事件 + L3世界状态
|
||||
│ │ ├── generate/
|
||||
│ │ │ ├── generator.js # 调度器:调用 LLM -> 解析 -> 清洗 -> 合并
|
||||
│ │ │ ├── llm.js # LLM API 与 Prompt 定义
|
||||
│ │ │ └── prompt.js # 注入层:格式化 + 预算装配
|
||||
│ │ └── vector/
|
||||
│ │ ├── chunk-builder.js # L1 切分与构建
|
||||
│ │ ├── chunk-store.js # 向量 CRUD 操作
|
||||
│ │ ├── embedder.js # 向量化服务 (Local/Online)
|
||||
│ │ ├── embedder.worker.js # 本地模型 Worker
|
||||
│ │ ├── entity.js # 召回实体/辅助结构
|
||||
│ │ └── recall.js # 召回引擎:加权Query + 实体加分 + MMR去重
|
||||
│ │
|
||||
│ ├── template-editor/ # 模板编辑器
|
||||
│ │ ├── template-editor.html # UI
|
||||
│ │ └── template-editor.js # 逻辑
|
||||
│ │
|
||||
│ ├── tts/ # TTS
|
||||
│ │ ├── tts-api.js # API 适配
|
||||
│ │ ├── tts-auth-provider.js # 鉴权提供者
|
||||
│ │ ├── tts-cache.js # 缓存
|
||||
│ │ ├── tts-free-provider.js # 免费提供者
|
||||
│ │ ├── tts-overlay.html # Overlay UI
|
||||
│ │ ├── tts-panel.js # 面板逻辑
|
||||
│ │ ├── tts-player.js # 播放器
|
||||
│ │ ├── tts-text.js # 文本处理
|
||||
│ │ ├── tts-voices.js # 语音配置
|
||||
│ │ ├── tts.js # 主入口
|
||||
│ │ ├── 声音复刻.png # 说明图
|
||||
│ │ ├── 开通管理.png # 说明图
|
||||
│ │ └── 获取ID和KEY.png # 说明图
|
||||
│ │
|
||||
│ └── variables/ # 变量系统
|
||||
│ ├── var-commands.js # 变量命令/宏/路径解析
|
||||
│ ├── varevent-editor.js # 变量编辑器/注入处理
|
||||
│ ├── variables-core.js # 变量系统核心
|
||||
│ └── variables-panel.js # 变量面板 UI
|
||||
│
|
||||
└── widgets/ # 通用 UI 组件
|
||||
├── button-collapse.js # 按钮收纳
|
||||
└── message-toolbar.js # 消息工具条
|
||||
## 许可证
|
||||
|
||||
```
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
详见 `docs/LICENSE.md`
|
||||
详见 `docs/LICENSE.md`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EventCenter } from "./event-manager.js";
|
||||
import { EventCenter } from "./event-manager.js";
|
||||
|
||||
const DEFAULT_MAX_LOGS = 200;
|
||||
|
||||
@@ -110,14 +110,14 @@ class LoggerCore {
|
||||
});
|
||||
}
|
||||
|
||||
info(moduleId, message) {
|
||||
this._log("info", moduleId, message, null);
|
||||
info(moduleId, ...args) {
|
||||
const msg = args.map(a => (typeof a === 'string' ? a : safeStringify(a))).join(' ');
|
||||
this._log('info', moduleId, msg, null);
|
||||
}
|
||||
|
||||
warn(moduleId, message) {
|
||||
this._log("warn", moduleId, message, null);
|
||||
warn(moduleId, ...args) {
|
||||
const msg = args.map(a => (typeof a === 'string' ? a : safeStringify(a))).join(' ');
|
||||
this._log('warn', moduleId, msg, null);
|
||||
}
|
||||
|
||||
error(moduleId, message, err) {
|
||||
this._log("error", moduleId, message, err || null);
|
||||
}
|
||||
|
||||
@@ -1,49 +1,33 @@
|
||||
|
||||
let wasm;
|
||||
|
||||
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
||||
let cachedUint8ArrayMemory0 = null;
|
||||
|
||||
cachedTextDecoder.decode();
|
||||
|
||||
let cachegetUint8Memory0 = null;
|
||||
function getUint8Memory0() {
|
||||
if (cachegetUint8Memory0 === null || cachegetUint8Memory0.buffer !== wasm.memory.buffer) {
|
||||
cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer);
|
||||
function getUint8ArrayMemory0() {
|
||||
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
|
||||
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
|
||||
}
|
||||
return cachegetUint8Memory0;
|
||||
return cachedUint8ArrayMemory0;
|
||||
}
|
||||
|
||||
let cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
|
||||
|
||||
if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
|
||||
|
||||
const MAX_SAFARI_DECODE_BYTES = 2146435072;
|
||||
let numBytesDecoded = 0;
|
||||
function decodeText(ptr, len) {
|
||||
numBytesDecoded += len;
|
||||
if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) {
|
||||
cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
|
||||
cachedTextDecoder.decode();
|
||||
numBytesDecoded = len;
|
||||
}
|
||||
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
|
||||
}
|
||||
|
||||
function getStringFromWasm0(ptr, len) {
|
||||
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
|
||||
}
|
||||
|
||||
const heap = new Array(32).fill(undefined);
|
||||
|
||||
heap.push(undefined, null, true, false);
|
||||
|
||||
let heap_next = heap.length;
|
||||
|
||||
function addHeapObject(obj) {
|
||||
if (heap_next === heap.length) heap.push(heap.length + 1);
|
||||
const idx = heap_next;
|
||||
heap_next = heap[idx];
|
||||
|
||||
heap[idx] = obj;
|
||||
return idx;
|
||||
}
|
||||
|
||||
function getObject(idx) { return heap[idx]; }
|
||||
|
||||
function dropObject(idx) {
|
||||
if (idx < 36) return;
|
||||
heap[idx] = heap_next;
|
||||
heap_next = idx;
|
||||
}
|
||||
|
||||
function takeObject(idx) {
|
||||
const ret = getObject(idx);
|
||||
dropObject(idx);
|
||||
return ret;
|
||||
ptr = ptr >>> 0;
|
||||
return decodeText(ptr, len);
|
||||
}
|
||||
|
||||
function debugString(val) {
|
||||
@@ -87,7 +71,7 @@ function debugString(val) {
|
||||
// Test for built-in
|
||||
const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val));
|
||||
let className;
|
||||
if (builtInMatches.length > 1) {
|
||||
if (builtInMatches && builtInMatches.length > 1) {
|
||||
className = builtInMatches[1];
|
||||
} else {
|
||||
// Failed to match the standard '[object ClassName]'
|
||||
@@ -113,7 +97,7 @@ function debugString(val) {
|
||||
|
||||
let WASM_VECTOR_LEN = 0;
|
||||
|
||||
let cachedTextEncoder = new TextEncoder('utf-8');
|
||||
const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } );
|
||||
|
||||
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
|
||||
? function (arg, view) {
|
||||
@@ -132,16 +116,16 @@ function passStringToWasm0(arg, malloc, realloc) {
|
||||
|
||||
if (realloc === undefined) {
|
||||
const buf = cachedTextEncoder.encode(arg);
|
||||
const ptr = malloc(buf.length);
|
||||
getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf);
|
||||
const ptr = malloc(buf.length, 1) >>> 0;
|
||||
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
|
||||
WASM_VECTOR_LEN = buf.length;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
let len = arg.length;
|
||||
let ptr = malloc(len);
|
||||
let ptr = malloc(len, 1) >>> 0;
|
||||
|
||||
const mem = getUint8Memory0();
|
||||
const mem = getUint8ArrayMemory0();
|
||||
|
||||
let offset = 0;
|
||||
|
||||
@@ -155,155 +139,161 @@ function passStringToWasm0(arg, malloc, realloc) {
|
||||
if (offset !== 0) {
|
||||
arg = arg.slice(offset);
|
||||
}
|
||||
ptr = realloc(ptr, len, len = offset + arg.length * 3);
|
||||
const view = getUint8Memory0().subarray(ptr + offset, ptr + len);
|
||||
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
|
||||
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
|
||||
const ret = encodeString(arg, view);
|
||||
|
||||
offset += ret.written;
|
||||
ptr = realloc(ptr, len, offset, 1) >>> 0;
|
||||
}
|
||||
|
||||
WASM_VECTOR_LEN = offset;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
let cachegetInt32Memory0 = null;
|
||||
function getInt32Memory0() {
|
||||
if (cachegetInt32Memory0 === null || cachegetInt32Memory0.buffer !== wasm.memory.buffer) {
|
||||
cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer);
|
||||
}
|
||||
return cachegetInt32Memory0;
|
||||
}
|
||||
let cachedDataViewMemory0 = null;
|
||||
|
||||
let cachegetUint32Memory0 = null;
|
||||
function getUint32Memory0() {
|
||||
if (cachegetUint32Memory0 === null || cachegetUint32Memory0.buffer !== wasm.memory.buffer) {
|
||||
cachegetUint32Memory0 = new Uint32Array(wasm.memory.buffer);
|
||||
}
|
||||
return cachegetUint32Memory0;
|
||||
}
|
||||
|
||||
function getArrayJsValueFromWasm0(ptr, len) {
|
||||
const mem = getUint32Memory0();
|
||||
const slice = mem.subarray(ptr / 4, ptr / 4 + len);
|
||||
const result = [];
|
||||
for (let i = 0; i < slice.length; i++) {
|
||||
result.push(takeObject(slice[i]));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* @param {string} text
|
||||
* @param {boolean} hmm
|
||||
* @returns {any[]}
|
||||
*/
|
||||
export function cut(text, hmm) {
|
||||
try {
|
||||
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
||||
var ptr0 = passStringToWasm0(text, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
var len0 = WASM_VECTOR_LEN;
|
||||
wasm.cut(retptr, ptr0, len0, hmm);
|
||||
var r0 = getInt32Memory0()[retptr / 4 + 0];
|
||||
var r1 = getInt32Memory0()[retptr / 4 + 1];
|
||||
var v1 = getArrayJsValueFromWasm0(r0, r1).slice();
|
||||
wasm.__wbindgen_free(r0, r1 * 4);
|
||||
return v1;
|
||||
} finally {
|
||||
wasm.__wbindgen_add_to_stack_pointer(16);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @returns {any[]}
|
||||
*/
|
||||
export function cut_all(text) {
|
||||
try {
|
||||
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
||||
var ptr0 = passStringToWasm0(text, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
var len0 = WASM_VECTOR_LEN;
|
||||
wasm.cut_all(retptr, ptr0, len0);
|
||||
var r0 = getInt32Memory0()[retptr / 4 + 0];
|
||||
var r1 = getInt32Memory0()[retptr / 4 + 1];
|
||||
var v1 = getArrayJsValueFromWasm0(r0, r1).slice();
|
||||
wasm.__wbindgen_free(r0, r1 * 4);
|
||||
return v1;
|
||||
} finally {
|
||||
wasm.__wbindgen_add_to_stack_pointer(16);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @param {boolean} hmm
|
||||
* @returns {any[]}
|
||||
*/
|
||||
export function cut_for_search(text, hmm) {
|
||||
try {
|
||||
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
||||
var ptr0 = passStringToWasm0(text, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
var len0 = WASM_VECTOR_LEN;
|
||||
wasm.cut_for_search(retptr, ptr0, len0, hmm);
|
||||
var r0 = getInt32Memory0()[retptr / 4 + 0];
|
||||
var r1 = getInt32Memory0()[retptr / 4 + 1];
|
||||
var v1 = getArrayJsValueFromWasm0(r0, r1).slice();
|
||||
wasm.__wbindgen_free(r0, r1 * 4);
|
||||
return v1;
|
||||
} finally {
|
||||
wasm.__wbindgen_add_to_stack_pointer(16);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @param {string} mode
|
||||
* @param {boolean} hmm
|
||||
* @returns {any[]}
|
||||
*/
|
||||
export function tokenize(text, mode, hmm) {
|
||||
try {
|
||||
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
||||
var ptr0 = passStringToWasm0(text, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
var len0 = WASM_VECTOR_LEN;
|
||||
var ptr1 = passStringToWasm0(mode, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
var len1 = WASM_VECTOR_LEN;
|
||||
wasm.tokenize(retptr, ptr0, len0, ptr1, len1, hmm);
|
||||
var r0 = getInt32Memory0()[retptr / 4 + 0];
|
||||
var r1 = getInt32Memory0()[retptr / 4 + 1];
|
||||
var v2 = getArrayJsValueFromWasm0(r0, r1).slice();
|
||||
wasm.__wbindgen_free(r0, r1 * 4);
|
||||
return v2;
|
||||
} finally {
|
||||
wasm.__wbindgen_add_to_stack_pointer(16);
|
||||
function getDataViewMemory0() {
|
||||
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
|
||||
cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
|
||||
}
|
||||
return cachedDataViewMemory0;
|
||||
}
|
||||
|
||||
function isLikeNone(x) {
|
||||
return x === undefined || x === null;
|
||||
}
|
||||
|
||||
function getArrayJsValueFromWasm0(ptr, len) {
|
||||
ptr = ptr >>> 0;
|
||||
const mem = getDataViewMemory0();
|
||||
const result = [];
|
||||
for (let i = ptr; i < ptr + 4 * len; i += 4) {
|
||||
result.push(wasm.__wbindgen_export_2.get(mem.getUint32(i, true)));
|
||||
}
|
||||
wasm.__externref_drop_slice(ptr, len);
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* @param {string} word
|
||||
* @param {number | undefined} freq
|
||||
* @param {string | undefined} tag
|
||||
* @returns {number}
|
||||
*/
|
||||
* @param {string} text
|
||||
* @param {boolean | null} [hmm]
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function cut(text, hmm) {
|
||||
const ptr0 = passStringToWasm0(text, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.cut(ptr0, len0, isLikeNone(hmm) ? 0xFFFFFF : hmm ? 1 : 0);
|
||||
var v2 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
|
||||
return v2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function cut_all(text) {
|
||||
const ptr0 = passStringToWasm0(text, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.cut_all(ptr0, len0);
|
||||
var v2 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
|
||||
return v2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @param {boolean | null} [hmm]
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function cut_for_search(text, hmm) {
|
||||
const ptr0 = passStringToWasm0(text, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.cut_for_search(ptr0, len0, isLikeNone(hmm) ? 0xFFFFFF : hmm ? 1 : 0);
|
||||
var v2 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
|
||||
return v2;
|
||||
}
|
||||
|
||||
function takeFromExternrefTable0(idx) {
|
||||
const value = wasm.__wbindgen_export_2.get(idx);
|
||||
wasm.__externref_table_dealloc(idx);
|
||||
return value;
|
||||
}
|
||||
/**
|
||||
* @param {string} text
|
||||
* @param {string} mode
|
||||
* @param {boolean | null} [hmm]
|
||||
* @returns {Token[]}
|
||||
*/
|
||||
export function tokenize(text, mode, hmm) {
|
||||
const ptr0 = passStringToWasm0(text, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ptr1 = passStringToWasm0(mode, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.tokenize(ptr0, len0, ptr1, len1, isLikeNone(hmm) ? 0xFFFFFF : hmm ? 1 : 0);
|
||||
if (ret[3]) {
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
var v3 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
|
||||
return v3;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} word
|
||||
* @param {number | null} [freq]
|
||||
* @param {string | null} [tag]
|
||||
* @returns {number}
|
||||
*/
|
||||
export function add_word(word, freq, tag) {
|
||||
var ptr0 = passStringToWasm0(word, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
var len0 = WASM_VECTOR_LEN;
|
||||
const ptr0 = passStringToWasm0(word, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
var ptr1 = isLikeNone(tag) ? 0 : passStringToWasm0(tag, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
var len1 = WASM_VECTOR_LEN;
|
||||
var ret = wasm.add_word(ptr0, len0, !isLikeNone(freq), isLikeNone(freq) ? 0 : freq, ptr1, len1);
|
||||
const ret = wasm.add_word(ptr0, len0, isLikeNone(freq) ? 0x100000001 : (freq) >>> 0, ptr1, len1);
|
||||
return ret >>> 0;
|
||||
}
|
||||
|
||||
async function load(module, imports) {
|
||||
/**
|
||||
* @param {string} sentence
|
||||
* @param {boolean | null} [hmm]
|
||||
* @returns {Tag[]}
|
||||
*/
|
||||
export function tag(sentence, hmm) {
|
||||
const ptr0 = passStringToWasm0(sentence, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.tag(ptr0, len0, isLikeNone(hmm) ? 0xFFFFFF : hmm ? 1 : 0);
|
||||
var v2 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
|
||||
return v2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} dict
|
||||
*/
|
||||
export function with_dict(dict) {
|
||||
const ptr0 = passStringToWasm0(dict, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.with_dict(ptr0, len0);
|
||||
if (ret[1]) {
|
||||
throw takeFromExternrefTable0(ret[0]);
|
||||
}
|
||||
}
|
||||
|
||||
const EXPECTED_RESPONSE_TYPES = new Set(['basic', 'cors', 'default']);
|
||||
|
||||
async function __wbg_load(module, imports) {
|
||||
if (typeof Response === 'function' && module instanceof Response) {
|
||||
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
||||
try {
|
||||
return await WebAssembly.instantiateStreaming(module, imports);
|
||||
|
||||
} catch (e) {
|
||||
if (module.headers.get('Content-Type') != 'application/wasm') {
|
||||
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
|
||||
const validResponse = module.ok && EXPECTED_RESPONSE_TYPES.has(module.type);
|
||||
|
||||
if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') {
|
||||
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
|
||||
|
||||
} else {
|
||||
throw e;
|
||||
@@ -326,65 +316,123 @@ async function load(module, imports) {
|
||||
}
|
||||
}
|
||||
|
||||
async function init(input) {
|
||||
if (typeof input === 'undefined') {
|
||||
input = new URL('jieba_rs_wasm_bg.wasm', import.meta.url);
|
||||
}
|
||||
function __wbg_get_imports() {
|
||||
const imports = {};
|
||||
imports.wbg = {};
|
||||
imports.wbg.__wbindgen_string_new = function(arg0, arg1) {
|
||||
var ret = getStringFromWasm0(arg0, arg1);
|
||||
return addHeapObject(ret);
|
||||
imports.wbg.__wbg_Error_0497d5bdba9362e5 = function(arg0, arg1) {
|
||||
const ret = Error(getStringFromWasm0(arg0, arg1));
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
|
||||
takeObject(arg0);
|
||||
imports.wbg.__wbg_new_07b483f72211fd66 = function() {
|
||||
const ret = new Object();
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_new_68adb0d58759a4ed = function() {
|
||||
var ret = new Object();
|
||||
return addHeapObject(ret);
|
||||
imports.wbg.__wbg_set_3f1d0b984ed272ed = function(arg0, arg1, arg2) {
|
||||
arg0[arg1] = arg2;
|
||||
};
|
||||
imports.wbg.__wbindgen_number_new = function(arg0) {
|
||||
var ret = arg0;
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_set_2e79e744454afade = function(arg0, arg1, arg2) {
|
||||
getObject(arg0)[takeObject(arg1)] = takeObject(arg2);
|
||||
};
|
||||
imports.wbg.__wbindgen_object_clone_ref = function(arg0) {
|
||||
var ret = getObject(arg0);
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_new_7031805939a80203 = function(arg0, arg1) {
|
||||
var ret = new Error(getStringFromWasm0(arg0, arg1));
|
||||
return addHeapObject(ret);
|
||||
imports.wbg.__wbindgen_bigint_from_u64 = function(arg0) {
|
||||
const ret = BigInt.asUintN(64, arg0);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_debug_string = function(arg0, arg1) {
|
||||
var ret = debugString(getObject(arg1));
|
||||
var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
var len0 = WASM_VECTOR_LEN;
|
||||
getInt32Memory0()[arg0 / 4 + 1] = len0;
|
||||
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
|
||||
const ret = debugString(arg1);
|
||||
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||
};
|
||||
imports.wbg.__wbindgen_init_externref_table = function() {
|
||||
const table = wasm.__wbindgen_export_2;
|
||||
const offset = table.grow(4);
|
||||
table.set(0, undefined);
|
||||
table.set(offset + 0, undefined);
|
||||
table.set(offset + 1, null);
|
||||
table.set(offset + 2, true);
|
||||
table.set(offset + 3, false);
|
||||
;
|
||||
};
|
||||
imports.wbg.__wbindgen_number_new = function(arg0) {
|
||||
const ret = arg0;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_string_new = function(arg0, arg1) {
|
||||
const ret = getStringFromWasm0(arg0, arg1);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_throw = function(arg0, arg1) {
|
||||
throw new Error(getStringFromWasm0(arg0, arg1));
|
||||
};
|
||||
imports.wbg.__wbindgen_rethrow = function(arg0) {
|
||||
throw takeObject(arg0);
|
||||
};
|
||||
|
||||
if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {
|
||||
input = fetch(input);
|
||||
}
|
||||
return imports;
|
||||
}
|
||||
|
||||
function __wbg_init_memory(imports, memory) {
|
||||
|
||||
}
|
||||
|
||||
const { instance, module } = await load(await input, imports);
|
||||
|
||||
function __wbg_finalize_init(instance, module) {
|
||||
wasm = instance.exports;
|
||||
init.__wbindgen_wasm_module = module;
|
||||
__wbg_init.__wbindgen_wasm_module = module;
|
||||
cachedDataViewMemory0 = null;
|
||||
cachedUint8ArrayMemory0 = null;
|
||||
|
||||
|
||||
wasm.__wbindgen_start();
|
||||
return wasm;
|
||||
}
|
||||
|
||||
export default init;
|
||||
function initSync(module) {
|
||||
if (wasm !== undefined) return wasm;
|
||||
|
||||
|
||||
if (typeof module !== 'undefined') {
|
||||
if (Object.getPrototypeOf(module) === Object.prototype) {
|
||||
({module} = module)
|
||||
} else {
|
||||
console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
|
||||
}
|
||||
}
|
||||
|
||||
const imports = __wbg_get_imports();
|
||||
|
||||
__wbg_init_memory(imports);
|
||||
|
||||
if (!(module instanceof WebAssembly.Module)) {
|
||||
module = new WebAssembly.Module(module);
|
||||
}
|
||||
|
||||
const instance = new WebAssembly.Instance(module, imports);
|
||||
|
||||
return __wbg_finalize_init(instance, module);
|
||||
}
|
||||
|
||||
async function __wbg_init(module_or_path) {
|
||||
if (wasm !== undefined) return wasm;
|
||||
|
||||
|
||||
if (typeof module_or_path !== 'undefined') {
|
||||
if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
|
||||
({module_or_path} = module_or_path)
|
||||
} else {
|
||||
console.warn('using deprecated parameters for the initialization function; pass a single object instead')
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof module_or_path === 'undefined') {
|
||||
module_or_path = new URL('jieba_rs_wasm_bg.wasm', import.meta.url);
|
||||
}
|
||||
const imports = __wbg_get_imports();
|
||||
|
||||
if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
|
||||
module_or_path = fetch(module_or_path);
|
||||
}
|
||||
|
||||
__wbg_init_memory(imports);
|
||||
|
||||
const { instance, module } = await __wbg_load(await module_or_path, imports);
|
||||
|
||||
return __wbg_finalize_init(instance, module);
|
||||
}
|
||||
|
||||
export { initSync };
|
||||
export default __wbg_init;
|
||||
|
||||
372
libs/jieba-wasm/jieba_rs_wasm_bg.js
Normal file
372
libs/jieba-wasm/jieba_rs_wasm_bg.js
Normal file
@@ -0,0 +1,372 @@
|
||||
let wasm;
|
||||
export function __wbg_set_wasm(val) {
|
||||
wasm = val;
|
||||
}
|
||||
|
||||
|
||||
const heap = new Array(128).fill(undefined);
|
||||
|
||||
heap.push(undefined, null, true, false);
|
||||
|
||||
function getObject(idx) { return heap[idx]; }
|
||||
|
||||
let heap_next = heap.length;
|
||||
|
||||
function dropObject(idx) {
|
||||
if (idx < 132) return;
|
||||
heap[idx] = heap_next;
|
||||
heap_next = idx;
|
||||
}
|
||||
|
||||
function takeObject(idx) {
|
||||
const ret = getObject(idx);
|
||||
dropObject(idx);
|
||||
return ret;
|
||||
}
|
||||
|
||||
const lTextDecoder = typeof TextDecoder === 'undefined' ? (0, module.require)('util').TextDecoder : TextDecoder;
|
||||
|
||||
let cachedTextDecoder = new lTextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
||||
|
||||
cachedTextDecoder.decode();
|
||||
|
||||
let cachedUint8ArrayMemory0 = null;
|
||||
|
||||
function getUint8ArrayMemory0() {
|
||||
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
|
||||
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
|
||||
}
|
||||
return cachedUint8ArrayMemory0;
|
||||
}
|
||||
|
||||
function getStringFromWasm0(ptr, len) {
|
||||
ptr = ptr >>> 0;
|
||||
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
|
||||
}
|
||||
|
||||
function addHeapObject(obj) {
|
||||
if (heap_next === heap.length) heap.push(heap.length + 1);
|
||||
const idx = heap_next;
|
||||
heap_next = heap[idx];
|
||||
|
||||
heap[idx] = obj;
|
||||
return idx;
|
||||
}
|
||||
|
||||
function debugString(val) {
|
||||
// primitive types
|
||||
const type = typeof val;
|
||||
if (type == 'number' || type == 'boolean' || val == null) {
|
||||
return `${val}`;
|
||||
}
|
||||
if (type == 'string') {
|
||||
return `"${val}"`;
|
||||
}
|
||||
if (type == 'symbol') {
|
||||
const description = val.description;
|
||||
if (description == null) {
|
||||
return 'Symbol';
|
||||
} else {
|
||||
return `Symbol(${description})`;
|
||||
}
|
||||
}
|
||||
if (type == 'function') {
|
||||
const name = val.name;
|
||||
if (typeof name == 'string' && name.length > 0) {
|
||||
return `Function(${name})`;
|
||||
} else {
|
||||
return 'Function';
|
||||
}
|
||||
}
|
||||
// objects
|
||||
if (Array.isArray(val)) {
|
||||
const length = val.length;
|
||||
let debug = '[';
|
||||
if (length > 0) {
|
||||
debug += debugString(val[0]);
|
||||
}
|
||||
for(let i = 1; i < length; i++) {
|
||||
debug += ', ' + debugString(val[i]);
|
||||
}
|
||||
debug += ']';
|
||||
return debug;
|
||||
}
|
||||
// Test for built-in
|
||||
const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val));
|
||||
let className;
|
||||
if (builtInMatches.length > 1) {
|
||||
className = builtInMatches[1];
|
||||
} else {
|
||||
// Failed to match the standard '[object ClassName]'
|
||||
return toString.call(val);
|
||||
}
|
||||
if (className == 'Object') {
|
||||
// we're a user defined class or Object
|
||||
// JSON.stringify avoids problems with cycles, and is generally much
|
||||
// easier than looping through ownProperties of `val`.
|
||||
try {
|
||||
return 'Object(' + JSON.stringify(val) + ')';
|
||||
} catch (_) {
|
||||
return 'Object';
|
||||
}
|
||||
}
|
||||
// errors
|
||||
if (val instanceof Error) {
|
||||
return `${val.name}: ${val.message}\n${val.stack}`;
|
||||
}
|
||||
// TODO we could test for more things here, like `Set`s and `Map`s.
|
||||
return className;
|
||||
}
|
||||
|
||||
let WASM_VECTOR_LEN = 0;
|
||||
|
||||
const lTextEncoder = typeof TextEncoder === 'undefined' ? (0, module.require)('util').TextEncoder : TextEncoder;
|
||||
|
||||
let cachedTextEncoder = new lTextEncoder('utf-8');
|
||||
|
||||
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
|
||||
? function (arg, view) {
|
||||
return cachedTextEncoder.encodeInto(arg, view);
|
||||
}
|
||||
: function (arg, view) {
|
||||
const buf = cachedTextEncoder.encode(arg);
|
||||
view.set(buf);
|
||||
return {
|
||||
read: arg.length,
|
||||
written: buf.length
|
||||
};
|
||||
});
|
||||
|
||||
function passStringToWasm0(arg, malloc, realloc) {
|
||||
|
||||
if (realloc === undefined) {
|
||||
const buf = cachedTextEncoder.encode(arg);
|
||||
const ptr = malloc(buf.length, 1) >>> 0;
|
||||
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
|
||||
WASM_VECTOR_LEN = buf.length;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
let len = arg.length;
|
||||
let ptr = malloc(len, 1) >>> 0;
|
||||
|
||||
const mem = getUint8ArrayMemory0();
|
||||
|
||||
let offset = 0;
|
||||
|
||||
for (; offset < len; offset++) {
|
||||
const code = arg.charCodeAt(offset);
|
||||
if (code > 0x7F) break;
|
||||
mem[ptr + offset] = code;
|
||||
}
|
||||
|
||||
if (offset !== len) {
|
||||
if (offset !== 0) {
|
||||
arg = arg.slice(offset);
|
||||
}
|
||||
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
|
||||
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
|
||||
const ret = encodeString(arg, view);
|
||||
|
||||
offset += ret.written;
|
||||
ptr = realloc(ptr, len, offset, 1) >>> 0;
|
||||
}
|
||||
|
||||
WASM_VECTOR_LEN = offset;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
let cachedDataViewMemory0 = null;
|
||||
|
||||
function getDataViewMemory0() {
|
||||
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
|
||||
cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
|
||||
}
|
||||
return cachedDataViewMemory0;
|
||||
}
|
||||
|
||||
function isLikeNone(x) {
|
||||
return x === undefined || x === null;
|
||||
}
|
||||
|
||||
function getArrayJsValueFromWasm0(ptr, len) {
|
||||
ptr = ptr >>> 0;
|
||||
const mem = getDataViewMemory0();
|
||||
const result = [];
|
||||
for (let i = ptr; i < ptr + 4 * len; i += 4) {
|
||||
result.push(takeObject(mem.getUint32(i, true)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* @param {string} text
|
||||
* @param {boolean | undefined} [hmm]
|
||||
* @returns {any[]}
|
||||
*/
|
||||
export function cut(text, hmm) {
|
||||
try {
|
||||
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
||||
const ptr0 = passStringToWasm0(text, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
wasm.cut(retptr, ptr0, len0, isLikeNone(hmm) ? 0xFFFFFF : hmm ? 1 : 0);
|
||||
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
||||
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
||||
var v2 = getArrayJsValueFromWasm0(r0, r1).slice();
|
||||
wasm.__wbindgen_free(r0, r1 * 4, 4);
|
||||
return v2;
|
||||
} finally {
|
||||
wasm.__wbindgen_add_to_stack_pointer(16);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @returns {any[]}
|
||||
*/
|
||||
export function cut_all(text) {
|
||||
try {
|
||||
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
||||
const ptr0 = passStringToWasm0(text, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
wasm.cut_all(retptr, ptr0, len0);
|
||||
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
||||
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
||||
var v2 = getArrayJsValueFromWasm0(r0, r1).slice();
|
||||
wasm.__wbindgen_free(r0, r1 * 4, 4);
|
||||
return v2;
|
||||
} finally {
|
||||
wasm.__wbindgen_add_to_stack_pointer(16);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @param {boolean | undefined} [hmm]
|
||||
* @returns {any[]}
|
||||
*/
|
||||
export function cut_for_search(text, hmm) {
|
||||
try {
|
||||
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
||||
const ptr0 = passStringToWasm0(text, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
wasm.cut_for_search(retptr, ptr0, len0, isLikeNone(hmm) ? 0xFFFFFF : hmm ? 1 : 0);
|
||||
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
||||
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
||||
var v2 = getArrayJsValueFromWasm0(r0, r1).slice();
|
||||
wasm.__wbindgen_free(r0, r1 * 4, 4);
|
||||
return v2;
|
||||
} finally {
|
||||
wasm.__wbindgen_add_to_stack_pointer(16);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @param {string} mode
|
||||
* @param {boolean | undefined} [hmm]
|
||||
* @returns {any[]}
|
||||
*/
|
||||
export function tokenize(text, mode, hmm) {
|
||||
try {
|
||||
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
||||
const ptr0 = passStringToWasm0(text, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ptr1 = passStringToWasm0(mode, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
wasm.tokenize(retptr, ptr0, len0, ptr1, len1, isLikeNone(hmm) ? 0xFFFFFF : hmm ? 1 : 0);
|
||||
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
||||
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
||||
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
|
||||
var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true);
|
||||
if (r3) {
|
||||
throw takeObject(r2);
|
||||
}
|
||||
var v3 = getArrayJsValueFromWasm0(r0, r1).slice();
|
||||
wasm.__wbindgen_free(r0, r1 * 4, 4);
|
||||
return v3;
|
||||
} finally {
|
||||
wasm.__wbindgen_add_to_stack_pointer(16);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} word
|
||||
* @param {number | undefined} [freq]
|
||||
* @param {string | undefined} [tag]
|
||||
* @returns {number}
|
||||
*/
|
||||
export function add_word(word, freq, tag) {
|
||||
const ptr0 = passStringToWasm0(word, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
var ptr1 = isLikeNone(tag) ? 0 : passStringToWasm0(tag, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
var len1 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.add_word(ptr0, len0, !isLikeNone(freq), isLikeNone(freq) ? 0 : freq, ptr1, len1);
|
||||
return ret >>> 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} sentence
|
||||
* @param {boolean | undefined} [hmm]
|
||||
* @returns {any[]}
|
||||
*/
|
||||
export function tag(sentence, hmm) {
|
||||
try {
|
||||
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
||||
const ptr0 = passStringToWasm0(sentence, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
wasm.tag(retptr, ptr0, len0, isLikeNone(hmm) ? 0xFFFFFF : hmm ? 1 : 0);
|
||||
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
||||
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
||||
var v2 = getArrayJsValueFromWasm0(r0, r1).slice();
|
||||
wasm.__wbindgen_free(r0, r1 * 4, 4);
|
||||
return v2;
|
||||
} finally {
|
||||
wasm.__wbindgen_add_to_stack_pointer(16);
|
||||
}
|
||||
}
|
||||
|
||||
export function __wbindgen_object_drop_ref(arg0) {
|
||||
takeObject(arg0);
|
||||
};
|
||||
|
||||
export function __wbindgen_string_new(arg0, arg1) {
|
||||
const ret = getStringFromWasm0(arg0, arg1);
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
|
||||
export function __wbindgen_object_clone_ref(arg0) {
|
||||
const ret = getObject(arg0);
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
|
||||
export function __wbg_new_1e7c00339420672b() {
|
||||
const ret = new Object();
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
|
||||
export function __wbindgen_number_new(arg0) {
|
||||
const ret = arg0;
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
|
||||
export function __wbg_set_1754fb90457a8cce(arg0, arg1, arg2) {
|
||||
getObject(arg0)[takeObject(arg1)] = takeObject(arg2);
|
||||
};
|
||||
|
||||
export function __wbg_new_b44ab9ef6060dd36(arg0, arg1) {
|
||||
const ret = new Error(getStringFromWasm0(arg0, arg1));
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
|
||||
export function __wbindgen_debug_string(arg0, arg1) {
|
||||
const ret = debugString(getObject(arg1));
|
||||
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||
};
|
||||
|
||||
export function __wbindgen_throw(arg0, arg1) {
|
||||
throw new Error(getStringFromWasm0(arg0, arg1));
|
||||
};
|
||||
|
||||
Binary file not shown.
31
libs/jieba-wasm/jieba_rs_wasm_bg.wasm.d.ts
vendored
31
libs/jieba-wasm/jieba_rs_wasm_bg.wasm.d.ts
vendored
@@ -1,12 +1,25 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export const memory: WebAssembly.Memory;
|
||||
export function cut(a: number, b: number, c: number, d: number): void;
|
||||
export function cut_all(a: number, b: number, c: number): void;
|
||||
export function cut_for_search(a: number, b: number, c: number, d: number): void;
|
||||
export function tokenize(a: number, b: number, c: number, d: number, e: number, f: number): void;
|
||||
export function add_word(a: number, b: number, c: number, d: number, e: number, f: number): number;
|
||||
export function __wbindgen_malloc(a: number): number;
|
||||
export function __wbindgen_realloc(a: number, b: number, c: number): number;
|
||||
export function __wbindgen_add_to_stack_pointer(a: number): number;
|
||||
export function __wbindgen_free(a: number, b: number): void;
|
||||
export const cut: (a: number, b: number, c: number) => [number, number];
|
||||
export const cut_all: (a: number, b: number) => [number, number];
|
||||
export const cut_for_search: (a: number, b: number, c: number) => [number, number];
|
||||
export const tokenize: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
|
||||
export const add_word: (a: number, b: number, c: number, d: number, e: number) => number;
|
||||
export const tag: (a: number, b: number, c: number) => [number, number];
|
||||
export const with_dict: (a: number, b: number) => [number, number];
|
||||
export const rust_zstd_wasm_shim_qsort: (a: number, b: number, c: number, d: number) => void;
|
||||
export const rust_zstd_wasm_shim_malloc: (a: number) => number;
|
||||
export const rust_zstd_wasm_shim_memcmp: (a: number, b: number, c: number) => number;
|
||||
export const rust_zstd_wasm_shim_calloc: (a: number, b: number) => number;
|
||||
export const rust_zstd_wasm_shim_free: (a: number) => void;
|
||||
export const rust_zstd_wasm_shim_memcpy: (a: number, b: number, c: number) => number;
|
||||
export const rust_zstd_wasm_shim_memmove: (a: number, b: number, c: number) => number;
|
||||
export const rust_zstd_wasm_shim_memset: (a: number, b: number, c: number) => number;
|
||||
export const __wbindgen_malloc: (a: number, b: number) => number;
|
||||
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
export const __wbindgen_export_2: WebAssembly.Table;
|
||||
export const __externref_drop_slice: (a: number, b: number) => void;
|
||||
export const __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||
export const __externref_table_dealloc: (a: number) => void;
|
||||
export const __wbindgen_start: () => void;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,11 +1,12 @@
|
||||
// Story Summary - Store
|
||||
// L2 (events/characters/arcs) + L3 (world) 统一存储
|
||||
// L2 (events/characters/arcs) + L3 (facts) 统一存储
|
||||
|
||||
import { getContext, saveMetadataDebounced } from "../../../../../../extensions.js";
|
||||
import { chat_metadata } from "../../../../../../../script.js";
|
||||
import { EXT_ID } from "../../../core/constants.js";
|
||||
import { xbLog } from "../../../core/debug-core.js";
|
||||
import { clearEventVectors, deleteEventVectorsByIds } from "../vector/chunk-store.js";
|
||||
import { clearEventTextIndex } from '../vector/text-search.js';
|
||||
|
||||
const MODULE_ID = 'summaryStore';
|
||||
|
||||
@@ -19,7 +20,26 @@ export function getSummaryStore() {
|
||||
chat_metadata.extensions ||= {};
|
||||
chat_metadata.extensions[EXT_ID] ||= {};
|
||||
chat_metadata.extensions[EXT_ID].storySummary ||= {};
|
||||
return chat_metadata.extensions[EXT_ID].storySummary;
|
||||
|
||||
const store = chat_metadata.extensions[EXT_ID].storySummary;
|
||||
|
||||
// ★ 自动迁移旧数据
|
||||
if (store.json && !store.json.facts) {
|
||||
const hasOldData = store.json.world?.length || store.json.characters?.relationships?.length;
|
||||
if (hasOldData) {
|
||||
store.json.facts = migrateToFacts(store.json);
|
||||
// 删除旧字段
|
||||
delete store.json.world;
|
||||
if (store.json.characters) {
|
||||
delete store.json.characters.relationships;
|
||||
}
|
||||
store.updatedAt = Date.now();
|
||||
saveSummaryStore();
|
||||
xbLog.info(MODULE_ID, `自动迁移完成: ${store.json.facts.length} 条 facts`);
|
||||
}
|
||||
}
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
export function saveSummaryStore() {
|
||||
@@ -31,7 +51,6 @@ export function getKeepVisibleCount() {
|
||||
return store?.keepVisibleCount ?? 3;
|
||||
}
|
||||
|
||||
// boundary:隐藏边界(由调用方决定语义:LLM总结边界 or 向量边界)
|
||||
export function calcHideRange(boundary) {
|
||||
if (boundary == null || boundary < 0) return null;
|
||||
|
||||
@@ -47,42 +66,176 @@ export function addSummarySnapshot(store, endMesId) {
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// L3 世界状态合并
|
||||
// Fact 工具函数
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export function mergeWorldState(existingList, updates, floor) {
|
||||
/**
|
||||
* 判断是否为关系类 fact
|
||||
*/
|
||||
export function isRelationFact(f) {
|
||||
return /^对.+的/.test(f.p);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// 从 facts 提取关系(供关系图 UI 使用)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export function extractRelationshipsFromFacts(facts) {
|
||||
return (facts || [])
|
||||
.filter(f => !f.retracted && isRelationFact(f))
|
||||
.map(f => {
|
||||
const match = f.p.match(/^对(.+)的/);
|
||||
const to = match ? match[1] : '';
|
||||
if (!to) return null;
|
||||
return {
|
||||
from: f.s,
|
||||
to,
|
||||
label: f.o,
|
||||
trend: f.trend || '陌生',
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 fact 的唯一键(s + p)
|
||||
*/
|
||||
function factKey(f) {
|
||||
return `${f.s}::${f.p}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成下一个 fact ID
|
||||
*/
|
||||
function getNextFactId(existingFacts) {
|
||||
let maxId = 0;
|
||||
for (const f of existingFacts || []) {
|
||||
const match = f.id?.match(/^f-(\d+)$/);
|
||||
if (match) {
|
||||
maxId = Math.max(maxId, parseInt(match[1], 10));
|
||||
}
|
||||
}
|
||||
return maxId + 1;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Facts 合并(KV 覆盖模型)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export function mergeFacts(existingFacts, updates, floor) {
|
||||
const map = new Map();
|
||||
|
||||
(existingList || []).forEach(item => {
|
||||
const key = `${item.category}:${item.topic}`;
|
||||
map.set(key, item);
|
||||
});
|
||||
// 加载现有 facts
|
||||
for (const f of existingFacts || []) {
|
||||
if (!f.retracted) {
|
||||
map.set(factKey(f), f);
|
||||
}
|
||||
}
|
||||
|
||||
(updates || []).forEach(up => {
|
||||
if (!up.category || !up.topic) return;
|
||||
// 获取下一个 ID
|
||||
let nextId = getNextFactId(existingFacts);
|
||||
|
||||
const key = `${up.category}:${up.topic}`;
|
||||
// 应用更新
|
||||
for (const u of updates || []) {
|
||||
if (!u.s || !u.p) continue;
|
||||
|
||||
if (up.cleared === true) {
|
||||
const key = factKey(u);
|
||||
|
||||
// 删除操作
|
||||
if (u.retracted === true) {
|
||||
map.delete(key);
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
const content = up.content?.trim();
|
||||
if (!content) return;
|
||||
// 无 o 则跳过
|
||||
if (!u.o || !String(u.o).trim()) continue;
|
||||
|
||||
map.set(key, {
|
||||
category: up.category,
|
||||
topic: up.topic,
|
||||
content: content,
|
||||
floor: floor,
|
||||
_addedAt: floor,
|
||||
});
|
||||
});
|
||||
// 覆盖或新增
|
||||
const existing = map.get(key);
|
||||
const newFact = {
|
||||
id: existing?.id || `f-${nextId++}`,
|
||||
s: u.s.trim(),
|
||||
p: u.p.trim(),
|
||||
o: String(u.o).trim(),
|
||||
since: floor,
|
||||
};
|
||||
|
||||
// 关系类保留 trend
|
||||
if (isRelationFact(newFact) && u.trend) {
|
||||
newFact.trend = u.trend;
|
||||
}
|
||||
|
||||
// 保留原始 _addedAt(如果是更新)
|
||||
if (existing?._addedAt != null) {
|
||||
newFact._addedAt = existing._addedAt;
|
||||
} else {
|
||||
newFact._addedAt = floor;
|
||||
}
|
||||
|
||||
map.set(key, newFact);
|
||||
}
|
||||
|
||||
return Array.from(map.values());
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// 旧数据迁移
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export function migrateToFacts(json) {
|
||||
if (!json) return [];
|
||||
|
||||
// 已有 facts 则跳过迁移
|
||||
if (json.facts?.length) return json.facts;
|
||||
|
||||
const facts = [];
|
||||
let nextId = 1;
|
||||
|
||||
// 迁移 world(worldUpdate 的持久化结果)
|
||||
for (const w of json.world || []) {
|
||||
if (!w.category || !w.topic || !w.content) continue;
|
||||
|
||||
let s, p;
|
||||
|
||||
// 解析 topic 格式:status/knowledge/relation 用 "::" 分隔
|
||||
if (w.topic.includes('::')) {
|
||||
[s, p] = w.topic.split('::').map(x => x.trim());
|
||||
} else {
|
||||
// inventory/rule 类
|
||||
s = w.topic.trim();
|
||||
p = w.category;
|
||||
}
|
||||
|
||||
if (!s || !p) continue;
|
||||
|
||||
facts.push({
|
||||
id: `f-${nextId++}`,
|
||||
s,
|
||||
p,
|
||||
o: w.content.trim(),
|
||||
since: w.floor ?? w._addedAt ?? 0,
|
||||
_addedAt: w._addedAt ?? w.floor ?? 0,
|
||||
});
|
||||
}
|
||||
|
||||
// 迁移 relationships
|
||||
for (const r of json.characters?.relationships || []) {
|
||||
if (!r.from || !r.to) continue;
|
||||
|
||||
facts.push({
|
||||
id: `f-${nextId++}`,
|
||||
s: r.from,
|
||||
p: `对${r.to}的看法`,
|
||||
o: r.label || '未知',
|
||||
trend: r.trend,
|
||||
since: r._addedAt ?? 0,
|
||||
_addedAt: r._addedAt ?? 0,
|
||||
});
|
||||
}
|
||||
|
||||
return facts;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// 数据合并(L2 + L3)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -95,11 +248,10 @@ export function mergeNewData(oldJson, parsed, endMesId) {
|
||||
merged.events ||= [];
|
||||
merged.characters ||= {};
|
||||
merged.characters.main ||= [];
|
||||
merged.characters.relationships ||= [];
|
||||
merged.arcs ||= [];
|
||||
|
||||
// L3 初始化
|
||||
merged.world ||= [];
|
||||
// L3 初始化(不再迁移,getSummaryStore 已处理)
|
||||
merged.facts ||= [];
|
||||
|
||||
// L2 数据合并
|
||||
if (parsed.keywords?.length) {
|
||||
@@ -111,6 +263,7 @@ export function mergeNewData(oldJson, parsed, endMesId) {
|
||||
merged.events.push(e);
|
||||
});
|
||||
|
||||
// newCharacters
|
||||
const existingMain = new Set(
|
||||
(merged.characters.main || []).map(m => typeof m === 'string' ? m : m.name)
|
||||
);
|
||||
@@ -120,22 +273,7 @@ export function mergeNewData(oldJson, parsed, endMesId) {
|
||||
}
|
||||
});
|
||||
|
||||
const relMap = new Map(
|
||||
(merged.characters.relationships || []).map(r => [`${r.from}->${r.to}`, r])
|
||||
);
|
||||
(parsed.newRelationships || []).forEach(r => {
|
||||
const key = `${r.from}->${r.to}`;
|
||||
const existing = relMap.get(key);
|
||||
if (existing) {
|
||||
existing.label = r.label;
|
||||
existing.trend = r.trend;
|
||||
} else {
|
||||
r._addedAt = endMesId;
|
||||
relMap.set(key, r);
|
||||
}
|
||||
});
|
||||
merged.characters.relationships = Array.from(relMap.values());
|
||||
|
||||
// arcUpdates
|
||||
const arcMap = new Map((merged.arcs || []).map(a => [a.name, a]));
|
||||
(parsed.arcUpdates || []).forEach(update => {
|
||||
const existing = arcMap.get(update.name);
|
||||
@@ -158,12 +296,8 @@ export function mergeNewData(oldJson, parsed, endMesId) {
|
||||
});
|
||||
merged.arcs = Array.from(arcMap.values());
|
||||
|
||||
// L3 世界状态合并
|
||||
merged.world = mergeWorldState(
|
||||
merged.world || [],
|
||||
parsed.worldUpdate || [],
|
||||
endMesId
|
||||
);
|
||||
// L3 factUpdates 合并
|
||||
merged.facts = mergeFacts(merged.facts, parsed.factUpdates || [], endMesId);
|
||||
|
||||
return merged;
|
||||
}
|
||||
@@ -241,13 +375,10 @@ export async function executeRollback(chatId, store, targetEndMesId, currentLeng
|
||||
json.characters.main = (json.characters.main || []).filter(m =>
|
||||
typeof m === 'string' || (m._addedAt ?? 0) <= targetEndMesId
|
||||
);
|
||||
json.characters.relationships = (json.characters.relationships || []).filter(r =>
|
||||
(r._addedAt ?? 0) <= targetEndMesId
|
||||
);
|
||||
}
|
||||
|
||||
// L3 回滚
|
||||
json.world = (json.world || []).filter(w => (w._addedAt ?? 0) <= targetEndMesId);
|
||||
// L3 facts 回滚
|
||||
json.facts = (json.facts || []).filter(f => (f._addedAt ?? 0) <= targetEndMesId);
|
||||
|
||||
store.json = json;
|
||||
store.lastSummarizedMesId = targetEndMesId;
|
||||
@@ -278,14 +409,23 @@ export async function clearSummaryData(chatId) {
|
||||
await clearEventVectors(chatId);
|
||||
}
|
||||
|
||||
clearEventTextIndex();
|
||||
|
||||
xbLog.info(MODULE_ID, '总结数据已清空');
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// L3 数据读取(供 prompt.js 使用)
|
||||
// L3 数据读取(供 prompt.js / recall.js 使用)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export function getWorldSnapshot() {
|
||||
export function getFacts() {
|
||||
const store = getSummaryStore();
|
||||
return store?.json?.world || [];
|
||||
return (store?.json?.facts || []).filter(f => !f.retracted);
|
||||
}
|
||||
|
||||
export function getNewCharacters() {
|
||||
const store = getSummaryStore();
|
||||
return (store?.json?.characters?.main || []).map(m =>
|
||||
typeof m === 'string' ? m : m.name
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { getContext } from "../../../../../../extensions.js";
|
||||
import { xbLog } from "../../../core/debug-core.js";
|
||||
import { getSummaryStore, saveSummaryStore, addSummarySnapshot, mergeNewData } from "../data/store.js";
|
||||
import { getSummaryStore, saveSummaryStore, addSummarySnapshot, mergeNewData, getFacts } from "../data/store.js";
|
||||
import { generateSummary, parseSummaryJson } from "./llm.js";
|
||||
|
||||
const MODULE_ID = 'summaryGenerator';
|
||||
@@ -11,46 +11,56 @@ const SUMMARY_SESSION_ID = 'xb9';
|
||||
const MAX_CAUSED_BY = 2;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// worldUpdate 清洗
|
||||
// factUpdates 清洗
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function sanitizeWorldUpdate(parsed) {
|
||||
function normalizeRelationPredicate(p) {
|
||||
if (/^对.+的看法$/.test(p)) return p;
|
||||
if (/^与.+的关系$/.test(p)) return p;
|
||||
return null;
|
||||
}
|
||||
|
||||
function sanitizeFacts(parsed) {
|
||||
if (!parsed) return;
|
||||
|
||||
const wu = Array.isArray(parsed.worldUpdate) ? parsed.worldUpdate : [];
|
||||
const updates = Array.isArray(parsed.factUpdates) ? parsed.factUpdates : [];
|
||||
const ok = [];
|
||||
|
||||
for (const item of wu) {
|
||||
const category = String(item?.category || '').trim().toLowerCase();
|
||||
const topic = String(item?.topic || '').trim();
|
||||
for (const item of updates) {
|
||||
const s = String(item?.s || '').trim();
|
||||
const pRaw = String(item?.p || '').trim();
|
||||
|
||||
if (!category || !topic) continue;
|
||||
if (!s || !pRaw) continue;
|
||||
|
||||
// status/knowledge/relation 必须包含 "::"
|
||||
if (['status', 'knowledge', 'relation'].includes(category) && !topic.includes('::')) {
|
||||
xbLog.warn(MODULE_ID, `丢弃不合格 worldUpdate: ${category}/${topic}`);
|
||||
// 删除操作
|
||||
if (item.retracted === true) {
|
||||
ok.push({ s, p: pRaw, retracted: true });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.cleared === true) {
|
||||
ok.push({ category, topic, cleared: true });
|
||||
continue;
|
||||
const o = String(item?.o || '').trim();
|
||||
if (!o) continue;
|
||||
|
||||
const relP = normalizeRelationPredicate(pRaw);
|
||||
const isRel = !!relP;
|
||||
const fact = { s, p: isRel ? relP : pRaw, o };
|
||||
|
||||
// 关系类保留 trend
|
||||
if (isRel && item.trend) {
|
||||
const validTrends = ['破裂', '厌恶', '反感', '陌生', '投缘', '亲密', '交融'];
|
||||
if (validTrends.includes(item.trend)) {
|
||||
fact.trend = item.trend;
|
||||
}
|
||||
}
|
||||
|
||||
const content = String(item?.content || '').trim();
|
||||
if (!content) continue;
|
||||
|
||||
ok.push({ category, topic, content });
|
||||
ok.push(fact);
|
||||
}
|
||||
|
||||
parsed.worldUpdate = ok;
|
||||
parsed.factUpdates = ok;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// causedBy 清洗(事件因果边)
|
||||
// - 允许引用:已存在事件 + 本次新输出事件
|
||||
// - 限制长度:0-2
|
||||
// - 去重、剔除非法ID、剔除自引用
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function sanitizeEventsCausality(parsed, existingEventIds) {
|
||||
@@ -61,7 +71,6 @@ function sanitizeEventsCausality(parsed, existingEventIds) {
|
||||
|
||||
const idRe = /^evt-\d+$/;
|
||||
|
||||
// 本次新输出事件ID集合(允许引用)
|
||||
const newIds = new Set(
|
||||
events
|
||||
.map(e => String(e?.id || '').trim())
|
||||
@@ -73,7 +82,6 @@ function sanitizeEventsCausality(parsed, existingEventIds) {
|
||||
for (const e of events) {
|
||||
const selfId = String(e?.id || '').trim();
|
||||
if (!idRe.test(selfId)) {
|
||||
// id 不合格的话,causedBy 直接清空,避免污染
|
||||
e.causedBy = [];
|
||||
continue;
|
||||
}
|
||||
@@ -117,11 +125,6 @@ export function formatExistingSummaryForAI(store) {
|
||||
parts.push(`\n【主要角色】${names.join("、")}`);
|
||||
}
|
||||
|
||||
if (data.characters?.relationships?.length) {
|
||||
parts.push("【人物关系】");
|
||||
data.characters.relationships.forEach(r => parts.push(`- ${r.from} → ${r.to}:${r.label}(${r.trend})`));
|
||||
}
|
||||
|
||||
if (data.arcs?.length) {
|
||||
parts.push("【角色弧光】");
|
||||
data.arcs.forEach(a => parts.push(`- ${a.name}:${a.trajectory}(进度${Math.round(a.progress * 100)}%)`));
|
||||
@@ -187,7 +190,7 @@ export async function runSummaryGeneration(mesId, config, callbacks = {}) {
|
||||
onStatus?.(`正在总结 ${slice.range}(${slice.count}楼新内容)...`);
|
||||
|
||||
const existingSummary = formatExistingSummaryForAI(store);
|
||||
const existingWorld = store?.json?.world || [];
|
||||
const existingFacts = getFacts();
|
||||
const nextEventId = getNextEventId(store);
|
||||
const existingEventCount = store?.json?.events?.length || 0;
|
||||
const useStream = config.trigger?.useStream !== false;
|
||||
@@ -196,7 +199,7 @@ export async function runSummaryGeneration(mesId, config, callbacks = {}) {
|
||||
try {
|
||||
raw = await generateSummary({
|
||||
existingSummary,
|
||||
existingWorld,
|
||||
existingFacts,
|
||||
newHistoryText: slice.text,
|
||||
historyRange: slice.range,
|
||||
nextEventId,
|
||||
@@ -231,7 +234,7 @@ export async function runSummaryGeneration(mesId, config, callbacks = {}) {
|
||||
return { success: false, error: "parse" };
|
||||
}
|
||||
|
||||
sanitizeWorldUpdate(parsed);
|
||||
sanitizeFacts(parsed);
|
||||
const existingEventIds = new Set((store?.json?.events || []).map(e => e?.id).filter(Boolean));
|
||||
sanitizeEventsCausality(parsed, existingEventIds);
|
||||
|
||||
@@ -245,8 +248,8 @@ export async function runSummaryGeneration(mesId, config, callbacks = {}) {
|
||||
|
||||
xbLog.info(MODULE_ID, `总结完成,已更新至 ${slice.endMesId + 1} 楼`);
|
||||
|
||||
if (parsed.worldUpdate?.length) {
|
||||
xbLog.info(MODULE_ID, `世界状态更新: ${parsed.worldUpdate.length} 条`);
|
||||
if (parsed.factUpdates?.length) {
|
||||
xbLog.info(MODULE_ID, `Facts 更新: ${parsed.factUpdates.length} 条`);
|
||||
}
|
||||
|
||||
const newEventIds = (parsed.events || []).map(e => e.id);
|
||||
@@ -255,7 +258,7 @@ export async function runSummaryGeneration(mesId, config, callbacks = {}) {
|
||||
merged,
|
||||
endMesId: slice.endMesId,
|
||||
newEventIds,
|
||||
l3Stats: { worldUpdate: parsed.worldUpdate?.length || 0 },
|
||||
factStats: { updated: parsed.factUpdates?.length || 0 },
|
||||
});
|
||||
|
||||
return { success: true, merged, endMesId: slice.endMesId, newEventIds };
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// LLM Service
|
||||
|
||||
const PROVIDER_MAP = {
|
||||
// ...
|
||||
openai: "openai",
|
||||
google: "gemini",
|
||||
gemini: "gemini",
|
||||
@@ -39,43 +38,37 @@ Incremental_Summary_Requirements:
|
||||
- Causal_Chain: 为每个新事件标注直接前因事件ID(causedBy)。仅在因果关系明确(直接导致/明确动机/承接后果)时填写;不明确时填[]完全正常。0-2个,只填 evt-数字,指向已存在或本次新输出事件。
|
||||
- Character_Dynamics: 识别新角色,追踪关系趋势(破裂/厌恶/反感/陌生/投缘/亲密/交融)
|
||||
- Arc_Tracking: 更新角色弧光轨迹与成长进度(0.0-1.0)
|
||||
- World_State_Tracking: 维护当前世界的硬性约束。解决"什么不能违反"。采用 KV 覆盖模型,追踪生死、物品归属、秘密知情、关系状态、环境规则等不可违背的事实。(覆盖式更新)
|
||||
categories:
|
||||
- status: 角色生死、位置锁定、重大状态
|
||||
- inventory: 重要物品归属
|
||||
- knowledge: 秘密的知情状态
|
||||
- relation: 硬性关系(在一起/决裂)
|
||||
- rule: 环境规则/契约限制
|
||||
- Fact_Tracking: 维护 SPO 三元组知识图谱。追踪生死、物品归属、位置、关系等硬性事实。采用 KV 覆盖模型(s+p 为键)。
|
||||
</task_settings>
|
||||
---
|
||||
Story Analyst:
|
||||
[Responsibility Definition]
|
||||
\`\`\`yaml
|
||||
analysis_task:
|
||||
title: Incremental Story Summarization with World State
|
||||
title: Incremental Story Summarization with Knowledge Graph
|
||||
Story Analyst:
|
||||
role: Antigravity
|
||||
task: >-
|
||||
To analyze provided dialogue content against existing summary state,
|
||||
extract only NEW plot elements, character developments, relationship
|
||||
changes, arc progressions, AND world state changes, outputting
|
||||
changes, arc progressions, AND fact updates, outputting
|
||||
structured JSON for incremental summary database updates.
|
||||
assistant:
|
||||
role: Summary Specialist
|
||||
description: Incremental Story Summary & World State Analyst
|
||||
description: Incremental Story Summary & Knowledge Graph Analyst
|
||||
behavior: >-
|
||||
To compare new dialogue against existing summary, identify genuinely
|
||||
new events and character interactions, classify events by narrative
|
||||
type and weight, track character arc progression with percentage,
|
||||
maintain world state as key-value updates with clear flags,
|
||||
maintain facts as SPO triples with clear semantics,
|
||||
and output structured JSON containing only incremental updates.
|
||||
Must strictly avoid repeating any existing summary content.
|
||||
user:
|
||||
role: Content Provider
|
||||
description: Supplies existing summary state and new dialogue
|
||||
behavior: >-
|
||||
To provide existing summary state (events, characters, relationships,
|
||||
arcs, world state) and new dialogue content for incremental analysis.
|
||||
To provide existing summary state (events, characters, arcs, facts)
|
||||
and new dialogue content for incremental analysis.
|
||||
interaction_mode:
|
||||
type: incremental_analysis
|
||||
output_format: structured_json
|
||||
@@ -84,7 +77,7 @@ execution_context:
|
||||
summary_active: true
|
||||
incremental_only: true
|
||||
memory_album_style: true
|
||||
world_state_tracking: true
|
||||
fact_tracking: true
|
||||
\`\`\`
|
||||
---
|
||||
Summary Specialist:
|
||||
@@ -103,15 +96,18 @@ Acknowledged. Now reviewing the incremental summarization specifications:
|
||||
破裂 ← 厌恶 ← 反感 ← 陌生 → 投缘 → 亲密 → 交融
|
||||
|
||||
[Arc Progress Tracking]
|
||||
├─ trajectory: 完整弧光链描述(30字内)
|
||||
├─ trajectory: 当前阶段描述(15字内)
|
||||
├─ progress: 0.0 to 1.0
|
||||
└─ newMoment: 仅记录本次新增的关键时刻
|
||||
|
||||
[World State Maintenance]
|
||||
├─ 维护方式: Key-Value 覆盖(category + topic 为键)
|
||||
├─ 只输出有变化的条目
|
||||
├─ 清除时使用 cleared: true,不要填 content
|
||||
└─ 不记录情绪、衣着、临时动作
|
||||
[Fact Tracking - SPO Triples]
|
||||
├─ s: 主体(角色名/物品名)
|
||||
├─ p: 谓词(属性名)
|
||||
│ - 关系类只允许:对X的看法 / 与X的关系
|
||||
├─ o: 值(当前状态)
|
||||
├─ trend: 仅关系类填写
|
||||
├─ retracted: 删除标记
|
||||
└─ s+p 为键,相同键会覆盖旧值
|
||||
|
||||
Ready to process incremental summary requests with strict deduplication.`,
|
||||
|
||||
@@ -119,19 +115,19 @@ Ready to process incremental summary requests with strict deduplication.`,
|
||||
Summary Specialist:
|
||||
Specifications internalized. Please provide the existing summary state so I can:
|
||||
1. Index all recorded events to avoid duplication
|
||||
2. Map current character relationships as baseline
|
||||
2. Map current character list as baseline
|
||||
3. Note existing arc progress levels
|
||||
4. Identify established keywords
|
||||
5. Review current world state (category + topic baseline)`,
|
||||
5. Review current facts (SPO triples baseline)`,
|
||||
|
||||
assistantAskContent: `
|
||||
Summary Specialist:
|
||||
Existing summary fully analyzed and indexed. I understand:
|
||||
├─ Recorded events: Indexed for deduplication
|
||||
├─ Character relationships: Baseline mapped
|
||||
├─ Character list: Baseline mapped
|
||||
├─ Arc progress: Levels noted
|
||||
├─ Keywords: Current state acknowledged
|
||||
└─ World state: Baseline loaded
|
||||
└─ Facts: SPO baseline loaded
|
||||
|
||||
I will extract only genuinely NEW elements from the upcoming dialogue.
|
||||
Please provide the new dialogue content requiring incremental analysis.`,
|
||||
@@ -152,7 +148,7 @@ Before generating, observe the USER and analyze carefully:
|
||||
- What NEW characters appeared for the first time?
|
||||
- What relationship CHANGES happened?
|
||||
- What arc PROGRESS was made?
|
||||
- What world state changes occurred? (status/inventory/knowledge/relation/rule)
|
||||
- What facts changed? (status/position/ownership/relationships)
|
||||
|
||||
## Output Format
|
||||
\`\`\`json
|
||||
@@ -160,7 +156,7 @@ Before generating, observe the USER and analyze carefully:
|
||||
"mindful_prelude": {
|
||||
"user_insight": "用户的幻想是什么时空、场景,是否反应出存在严重心理问题需要建议?",
|
||||
"dedup_analysis": "已有X个事件,本次识别Y个新事件",
|
||||
"world_changes": "识别到的世界状态变化概述,仅精选不记录则可能导致吃书的硬状态变化"
|
||||
"fact_changes": "识别到的事实变化概述"
|
||||
},
|
||||
"keywords": [
|
||||
{"text": "综合已有+新内容的全局关键词(5-10个)", "weight": "核心|重要|一般"}
|
||||
@@ -178,45 +174,35 @@ Before generating, observe the USER and analyze carefully:
|
||||
}
|
||||
],
|
||||
"newCharacters": ["仅本次首次出现的角色名"],
|
||||
"newRelationships": [
|
||||
{"from": "A", "to": "B", "label": "基于全局的关系描述", "trend": "破裂|厌恶|反感|陌生|投缘|亲密|交融"}
|
||||
],
|
||||
"arcUpdates": [
|
||||
{"name": "角色名", "trajectory": "完整弧光链(30字内)", "progress": 0.0-1.0, "newMoment": "本次新增的关键时刻"}
|
||||
{"name": "角色名", "trajectory": "当前阶段描述(15字内)", "progress": 0.0-1.0, "newMoment": "本次新增的关键时刻"}
|
||||
],
|
||||
"worldUpdate": [
|
||||
"factUpdates": [
|
||||
{
|
||||
"category": "status|inventory|knowledge|relation|rule",
|
||||
"topic": "主体名称(人/物/关系/规则)",
|
||||
"content": "当前状态描述",
|
||||
"cleared": true
|
||||
"s": "主体(角色名/物品名)",
|
||||
"p": "谓词(属性名/对X的看法)",
|
||||
"o": "当前值",
|
||||
"trend": "破裂|厌恶|反感|陌生|投缘|亲密|交融",
|
||||
"retracted": false
|
||||
}
|
||||
]
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## Field Guidelines
|
||||
|
||||
### worldUpdate(世界状态·硬约束KV表)
|
||||
- category 固定 5 选 1:status / inventory / knowledge / relation / rule
|
||||
- topic 命名规范:
|
||||
- status:「角色名::状态类型」如 张三::生死、李四::位置、王五::伤势
|
||||
- knowledge:「角色名::知情事项」如 张三::知道某秘密、李四::知道真相
|
||||
- relation:「角色A::与角色B关系」如 张三::与李四关系
|
||||
- inventory:物品名称,如 钥匙、信物、武器
|
||||
- rule:规则/契约名称,如 门禁时间、魔法契约、禁令
|
||||
- content:当前状态的简短描述
|
||||
- cleared: true 表示该条目已失效需删除(不填 content)
|
||||
- status/knowledge/relation 的 topic 必须包含「::」分隔符
|
||||
- 硬约束才记录,避免叙事化,确保少、硬、稳定、可覆盖
|
||||
- 动态清理:若发现已有条目中存在不适合作为硬约束的内容(如衣着打扮、临时情绪、琐碎动作),本次输出中用 cleared: true 删除
|
||||
## factUpdates 规则
|
||||
- s+p 为键,相同键会覆盖旧值
|
||||
- 状态类:s=角色名, p=属性(生死/位置/状态等), o=值
|
||||
- 关系类:s=角色A, p="对B的看法" 或 p="与B的关系",trend 仅限关系类
|
||||
- 删除:设置 retracted: true(不需要填 o)
|
||||
- 只输出有变化的条目
|
||||
- 硬约束才记录,避免叙事化,确保少、硬、稳定
|
||||
|
||||
## CRITICAL NOTES
|
||||
- events.id 从 evt-{nextEventId} 开始编号
|
||||
- 仅输出【增量】内容,已有事件绝不重复
|
||||
- keywords 是全局关键词,综合已有+新增
|
||||
- causedBy 仅在因果明确时填写,允许为[],0-2个,详见上方 Causal_Chain 规则
|
||||
- worldUpdate 可为空数组
|
||||
- causedBy 仅在因果明确时填写,允许为[],0-2个
|
||||
- factUpdates 可为空数组
|
||||
- 合法JSON,字符串值内部避免英文双引号
|
||||
- 用朴实、白描、有烟火气的笔触记录,避免比喻和意象
|
||||
</meta_protocol>`,
|
||||
@@ -227,15 +213,14 @@ Before generating, observe the USER and analyze carefully:
|
||||
├─ New dialogue received: ✓ Content parsed
|
||||
├─ Deduplication engine: ✓ Active
|
||||
├─ Event classification: ✓ Ready
|
||||
├─ World state tracking: ✓ Enabled
|
||||
├─ Fact tracking: ✓ Enabled
|
||||
└─ Output format: ✓ JSON specification loaded
|
||||
|
||||
[Material Verification]
|
||||
├─ Existing events: Indexed ({existingEventCount} recorded)
|
||||
├─ Character baseline: Mapped
|
||||
├─ Relationship baseline: Mapped
|
||||
├─ Arc progress baseline: Noted
|
||||
├─ World state: Baseline loaded
|
||||
├─ Facts baseline: Loaded
|
||||
└─ Output specification: ✓ Defined in <meta_protocol>
|
||||
All checks passed. Beginning incremental extraction...
|
||||
{
|
||||
@@ -280,39 +265,23 @@ function waitForStreamingComplete(sessionId, streamingMod, timeout = 120000) {
|
||||
// 提示词构建
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function formatWorldForLLM(worldList) {
|
||||
if (!worldList?.length) {
|
||||
return '(空白,尚无世界状态记录)';
|
||||
function formatFactsForLLM(facts) {
|
||||
if (!facts?.length) {
|
||||
return '(空白,尚无事实记录)';
|
||||
}
|
||||
|
||||
const grouped = { status: [], inventory: [], knowledge: [], relation: [], rule: [] };
|
||||
const labels = {
|
||||
status: '状态(生死/位置锁定)',
|
||||
inventory: '物品归属',
|
||||
knowledge: '秘密/认知',
|
||||
relation: '关系状态',
|
||||
rule: '规则/约束'
|
||||
};
|
||||
|
||||
worldList.forEach(w => {
|
||||
if (grouped[w.category]) {
|
||||
grouped[w.category].push(w);
|
||||
const lines = facts.map(f => {
|
||||
if (f.trend) {
|
||||
return `- ${f.s} | ${f.p} | ${f.o} [${f.trend}]`;
|
||||
}
|
||||
return `- ${f.s} | ${f.p} | ${f.o}`;
|
||||
});
|
||||
|
||||
const parts = [];
|
||||
for (const [cat, items] of Object.entries(grouped)) {
|
||||
if (items.length > 0) {
|
||||
const lines = items.map(w => ` - ${w.topic}: ${w.content}`).join('\n');
|
||||
parts.push(`【${labels[cat]}】\n${lines}`);
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join('\n\n') || '(空白,尚无世界状态记录)';
|
||||
return lines.join('\n') || '(空白,尚无事实记录)';
|
||||
}
|
||||
|
||||
function buildSummaryMessages(existingSummary, existingWorld, newHistoryText, historyRange, nextEventId, existingEventCount) {
|
||||
const worldStateText = formatWorldForLLM(existingWorld);
|
||||
function buildSummaryMessages(existingSummary, existingFacts, newHistoryText, historyRange, nextEventId, existingEventCount) {
|
||||
const factsText = formatFactsForLLM(existingFacts);
|
||||
|
||||
const jsonFormat = LLM_PROMPT_CONFIG.userJsonFormat
|
||||
.replace(/\{nextEventId\}/g, String(nextEventId));
|
||||
@@ -324,7 +293,7 @@ function buildSummaryMessages(existingSummary, existingWorld, newHistoryText, hi
|
||||
{ role: 'system', content: LLM_PROMPT_CONFIG.topSystem },
|
||||
{ role: 'assistant', content: LLM_PROMPT_CONFIG.assistantDoc },
|
||||
{ role: 'assistant', content: LLM_PROMPT_CONFIG.assistantAskSummary },
|
||||
{ role: 'user', content: `<已有总结状态>\n${existingSummary}\n</已有总结状态>\n\n<当前世界状态>\n${worldStateText}\n</当前世界状态>` },
|
||||
{ role: 'user', content: `<已有总结状态>\n${existingSummary}\n</已有总结状态>\n\n<当前事实图谱>\n${factsText}\n</当前事实图谱>` },
|
||||
{ role: 'assistant', content: LLM_PROMPT_CONFIG.assistantAskContent },
|
||||
{ role: 'user', content: `<新对话内容>(${historyRange})\n${newHistoryText}\n</新对话内容>` }
|
||||
];
|
||||
@@ -378,7 +347,7 @@ export function parseSummaryJson(raw) {
|
||||
export async function generateSummary(options) {
|
||||
const {
|
||||
existingSummary,
|
||||
existingWorld,
|
||||
existingFacts,
|
||||
newHistoryText,
|
||||
historyRange,
|
||||
nextEventId,
|
||||
@@ -401,7 +370,7 @@ export async function generateSummary(options) {
|
||||
|
||||
const promptData = buildSummaryMessages(
|
||||
existingSummary,
|
||||
existingWorld,
|
||||
existingFacts,
|
||||
newHistoryText,
|
||||
historyRange,
|
||||
nextEventId,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { getContext } from "../../../../../../extensions.js";
|
||||
import { xbLog } from "../../../core/debug-core.js";
|
||||
import { getSummaryStore } from "../data/store.js";
|
||||
import { getSummaryStore, getFacts, isRelationFact } from "../data/store.js";
|
||||
import { getVectorConfig, getSummaryPanelConfig, getSettings } from "../data/config.js";
|
||||
import { recallMemory, buildQueryText } from "../vector/recall.js";
|
||||
import { getChunksByFloors, getAllChunkVectors, getAllEventVectors, getMeta } from "../vector/chunk-store.js";
|
||||
@@ -91,9 +91,9 @@ function cleanSummary(summary) {
|
||||
|
||||
function buildSystemPreamble() {
|
||||
return [
|
||||
"以上内容为因上下文窗口限制保留的可见历史",
|
||||
"以下【剧情记忆】是对可见与不可见历史的总结:",
|
||||
"• 【世界约束】记录着已确立的事实",
|
||||
"以上是还留在眼前的对话",
|
||||
"以下是脑海里的记忆:",
|
||||
"• [定了的事] 这些是不会变的",
|
||||
"• 其余部分是过往经历的回忆碎片",
|
||||
"",
|
||||
"请内化这些记忆:",
|
||||
@@ -103,7 +103,7 @@ function buildSystemPreamble() {
|
||||
function buildPostscript() {
|
||||
return [
|
||||
"",
|
||||
"——",
|
||||
"这些记忆是真实的,请自然地记住它们。",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
@@ -111,10 +111,18 @@ function buildPostscript() {
|
||||
// 格式化函数
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function formatWorldLines(world) {
|
||||
return [...(world || [])]
|
||||
.sort((a, b) => (b.floor || 0) - (a.floor || 0))
|
||||
.map(w => `- ${w.topic}:${w.content}`);
|
||||
function formatFactsForInjection(facts) {
|
||||
const activeFacts = (facts || []).filter(f => !f.retracted);
|
||||
if (!activeFacts.length) return [];
|
||||
return activeFacts
|
||||
.sort((a, b) => (b.since || 0) - (a.since || 0))
|
||||
.map(f => {
|
||||
const since = f.since ? ` (#${f.since + 1})` : '';
|
||||
if (isRelationFact(f) && f.trend) {
|
||||
return `- ${f.s} ${f.p}: ${f.o} [${f.trend}]${since}`;
|
||||
}
|
||||
return `- ${f.s}的${f.p}: ${f.o}${since}`;
|
||||
});
|
||||
}
|
||||
|
||||
function formatArcLine(a) {
|
||||
@@ -179,85 +187,67 @@ function formatInjectionLog(stats, details, recentOrphanStats = null) {
|
||||
const pct = (n, d) => (d > 0 ? Math.round((n / d) * 100) : 0);
|
||||
|
||||
const lines = [
|
||||
"",
|
||||
"╔══════════════════════════════════════════════════════════════╗",
|
||||
"║ Prompt 装配报告 ║",
|
||||
"╠══════════════════════════════════════════════════════════════╣",
|
||||
`║ 总预算: ${stats.budget.max} tokens`,
|
||||
`║ 已使用: ${stats.budget.used} tokens (${pct(stats.budget.used, stats.budget.max)}%)`,
|
||||
`║ 剩余: ${stats.budget.max - stats.budget.used} tokens`,
|
||||
"╚══════════════════════════════════════════════════════════════╝",
|
||||
"",
|
||||
'',
|
||||
'┌─────────────────────────────────────────────────────────────┐',
|
||||
'│ 【装配统计】 │',
|
||||
'└─────────────────────────────────────────────────────────────┘',
|
||||
` 总预算: ${stats.budget.max} tokens | 已使用: ${stats.budget.used} tokens (${pct(stats.budget.used, stats.budget.max)}%)`,
|
||||
'',
|
||||
];
|
||||
|
||||
// 世界状态
|
||||
lines.push("┌─────────────────────────────────────────────────────────────┐");
|
||||
lines.push("│ [1] 世界约束 (上限 2000) │");
|
||||
lines.push("└─────────────────────────────────────────────────────────────┘");
|
||||
lines.push(` 注入: ${stats.world.count} 条 | ${stats.world.tokens} tokens`);
|
||||
lines.push("");
|
||||
// [1] 世界约束
|
||||
lines.push(` [1] 世界约束 (上限 2000)`);
|
||||
lines.push(` 选入: ${stats.facts.count} 条 | 消耗: ${stats.facts.tokens} tokens`);
|
||||
lines.push('');
|
||||
|
||||
// 核心经历 + 过往背景
|
||||
lines.push("┌─────────────────────────────────────────────────────────────┐");
|
||||
lines.push("│ [2] 核心经历 + 过往背景(含证据) │");
|
||||
lines.push("└─────────────────────────────────────────────────────────────┘");
|
||||
lines.push(` 选入: ${stats.events.selected} 条 | 事件本体: ${stats.events.tokens} tokens`);
|
||||
lines.push(` 挂载证据: ${stats.evidence.attached} 条 | 证据: ${stats.evidence.tokens} tokens`);
|
||||
lines.push(` 核心: ${details.directCount || 0} | 过往: ${details.similarCount || 0}`);
|
||||
if (details.eventList?.length) {
|
||||
lines.push(" ────────────────────────────────────────");
|
||||
details.eventList.slice(0, 20).forEach((ev, i) => {
|
||||
const type = ev.isDirect ? "核心" : "过往";
|
||||
const hasE = ev.hasEvidence ? " +E" : "";
|
||||
const title = (ev.title || "(无标题)").slice(0, 32);
|
||||
lines.push(` ${String(i + 1).padStart(2)}. [${type}${hasE}] ${title} (${ev.tokens}tok)`);
|
||||
});
|
||||
if (details.eventList.length > 20) lines.push(` ... 还有 ${details.eventList.length - 20} 条`);
|
||||
}
|
||||
lines.push("");
|
||||
// [2] 核心经历 + 过往背景
|
||||
lines.push(` [2] 核心经历 + 过往背景`);
|
||||
lines.push(` 事件: ${stats.events.selected} 条 | 消耗: ${stats.events.tokens} tokens`);
|
||||
|
||||
// 远期片段
|
||||
lines.push("┌─────────────────────────────────────────────────────────────┐");
|
||||
lines.push("│ [3] 远期片段(已总结范围) │");
|
||||
lines.push("└─────────────────────────────────────────────────────────────┘");
|
||||
lines.push(` 注入: ${stats.orphans.injected} 条 | ${stats.orphans.tokens} tokens`);
|
||||
lines.push("");
|
||||
// 证据统计(区分 L0 和 L1)
|
||||
const l0EvidenceCount = details.eventList?.filter(e => e.hasL0Evidence)?.length || 0;
|
||||
const l1EvidenceCount = (stats.evidence.attached || 0) - l0EvidenceCount;
|
||||
lines.push(` 证据: ${stats.evidence.attached} 条 (L0: ${l0EvidenceCount}, L1: ${l1EvidenceCount}) | 消耗: ${stats.evidence.tokens} tokens`);
|
||||
lines.push(` 核心: ${details.directCount || 0} 条 | 过往: ${details.similarCount || 0} 条`);
|
||||
lines.push('');
|
||||
|
||||
// 待整理
|
||||
lines.push("┌─────────────────────────────────────────────────────────────┐");
|
||||
lines.push("│ [4] 待整理(未总结范围,独立预算 5000) │");
|
||||
lines.push("└─────────────────────────────────────────────────────────────┘");
|
||||
lines.push(` 注入: ${recentOrphanStats?.injected || 0} 条 | ${recentOrphanStats?.tokens || 0} tokens`);
|
||||
lines.push(` 楼层范围: ${recentOrphanStats?.floorRange || "N/A"}`);
|
||||
lines.push("");
|
||||
// [3] 远期片段
|
||||
const l0OrphanCount = stats.orphans.l0Count || 0;
|
||||
const l1OrphanCount = (stats.orphans.injected || 0) - l0OrphanCount;
|
||||
lines.push(` [3] 远期片段 (已总结范围)`);
|
||||
lines.push(` 选入: ${stats.orphans.injected} 条 (L0: ${l0OrphanCount}, L1: ${l1OrphanCount}) | 消耗: ${stats.orphans.tokens} tokens`);
|
||||
lines.push('');
|
||||
|
||||
lines.push("┌─────────────────────────────────────────────────────────────┐");
|
||||
lines.push("│ [5] 人物弧光(上限 1500) │");
|
||||
lines.push("└─────────────────────────────────────────────────────────────┘");
|
||||
lines.push(` 注入: ${stats.arcs.count} 条 | ${stats.arcs.tokens} tokens`);
|
||||
lines.push("");
|
||||
// [4] 待整理
|
||||
lines.push(` [4] 待整理 (独立预算 5000)`);
|
||||
lines.push(` 选入: ${recentOrphanStats?.injected || 0} 条 | 消耗: ${recentOrphanStats?.tokens || 0} tokens`);
|
||||
lines.push(` 楼层: ${recentOrphanStats?.floorRange || 'N/A'}`);
|
||||
lines.push('');
|
||||
|
||||
// [5] 人物弧光
|
||||
lines.push(` [5] 人物弧光 (上限 1500)`);
|
||||
lines.push(` 选入: ${stats.arcs.count} 条 | 消耗: ${stats.arcs.tokens} tokens`);
|
||||
lines.push('');
|
||||
|
||||
// 预算条形图
|
||||
lines.push("┌─────────────────────────────────────────────────────────────┐");
|
||||
lines.push("│ 【预算分布】 │");
|
||||
lines.push("└─────────────────────────────────────────────────────────────┘");
|
||||
lines.push(' 【预算分布】');
|
||||
const total = stats.budget.max;
|
||||
const bar = (tokens, label) => {
|
||||
const width = Math.round((tokens / total) * 40);
|
||||
const pctStr = pct(tokens, total) + "%";
|
||||
return ` ${label.padEnd(6)} ${"█".repeat(width).padEnd(40)} ${String(tokens).padStart(5)} (${pctStr})`;
|
||||
const width = Math.round((tokens / total) * 30);
|
||||
const pctStr = pct(tokens, total) + '%';
|
||||
return ` ${label.padEnd(6)} ${'█'.repeat(width).padEnd(30)} ${String(tokens).padStart(5)} (${pctStr})`;
|
||||
};
|
||||
lines.push(bar(stats.world.tokens, "约束"));
|
||||
lines.push(bar(stats.events.tokens, "经历"));
|
||||
lines.push(bar(stats.evidence.tokens, "证据"));
|
||||
lines.push(bar(stats.orphans.tokens, "远期"));
|
||||
lines.push(bar(recentOrphanStats?.tokens || 0, "待整理"));
|
||||
lines.push(bar(stats.arcs.tokens, "弧光"));
|
||||
lines.push(bar(stats.budget.max - stats.budget.used, "剩余"));
|
||||
lines.push("");
|
||||
lines.push(bar(stats.facts.tokens, '约束'));
|
||||
lines.push(bar(stats.events.tokens + stats.evidence.tokens, '经历'));
|
||||
lines.push(bar(stats.orphans.tokens, '远期'));
|
||||
lines.push(bar(recentOrphanStats?.tokens || 0, '待整理'));
|
||||
lines.push(bar(stats.arcs.tokens, '弧光'));
|
||||
lines.push(bar(stats.budget.max - stats.budget.used, '剩余'));
|
||||
lines.push('');
|
||||
|
||||
return lines.join("\n");
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// 重写事件文本里的序号前缀:把 “{idx}. ” 或 “{idx}.【...】” 的 idx 替换
|
||||
function renumberEventText(text, newIndex) {
|
||||
const s = String(text || "");
|
||||
@@ -281,9 +271,9 @@ function buildNonVectorPrompt(store) {
|
||||
const data = store.json || {};
|
||||
const sections = [];
|
||||
|
||||
if (data.world?.length) {
|
||||
const lines = formatWorldLines(data.world);
|
||||
sections.push(`[世界约束] 已确立的事实\n${lines.join("\n")}`);
|
||||
const factLines = formatFactsForInjection(getFacts(store));
|
||||
if (factLines.length) {
|
||||
sections.push(`[定了的事] 已确立的事实\n${factLines.join("\n")}`);
|
||||
}
|
||||
|
||||
if (data.events?.length) {
|
||||
@@ -348,7 +338,7 @@ async function buildVectorPrompt(store, recallResult, causalById, queryEntities
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
const assembled = {
|
||||
world: { lines: [], tokens: 0 },
|
||||
facts: { lines: [], tokens: 0 },
|
||||
arcs: { lines: [], tokens: 0 },
|
||||
events: { direct: [], similar: [] },
|
||||
orphans: { lines: [], tokens: 0 },
|
||||
@@ -357,7 +347,7 @@ async function buildVectorPrompt(store, recallResult, causalById, queryEntities
|
||||
|
||||
const injectionStats = {
|
||||
budget: { max: TOTAL_BUDGET_MAX, used: 0 },
|
||||
world: { count: 0, tokens: 0 },
|
||||
facts: { count: 0, tokens: 0 },
|
||||
arcs: { count: 0, tokens: 0 },
|
||||
events: { selected: 0, tokens: 0 },
|
||||
evidence: { attached: 0, tokens: 0 },
|
||||
@@ -378,16 +368,16 @@ async function buildVectorPrompt(store, recallResult, causalById, queryEntities
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// [优先级 1] 世界约束 - 最高优先级
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
const worldLines = formatWorldLines(data.world);
|
||||
if (worldLines.length) {
|
||||
const factLines = formatFactsForInjection(getFacts(store));
|
||||
if (factLines.length) {
|
||||
const l3Budget = { used: 0, max: Math.min(L3_MAX, total.max - total.used) };
|
||||
for (const line of worldLines) {
|
||||
if (!pushWithBudget(assembled.world.lines, line, l3Budget)) break;
|
||||
for (const line of factLines) {
|
||||
if (!pushWithBudget(assembled.facts.lines, line, l3Budget)) break;
|
||||
}
|
||||
assembled.world.tokens = l3Budget.used;
|
||||
assembled.facts.tokens = l3Budget.used;
|
||||
total.used += l3Budget.used;
|
||||
injectionStats.world.count = assembled.world.lines.length;
|
||||
injectionStats.world.tokens = l3Budget.used;
|
||||
injectionStats.facts.count = assembled.facts.lines.length;
|
||||
injectionStats.facts.tokens = l3Budget.used;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
@@ -612,49 +602,36 @@ async function buildVectorPrompt(store, recallResult, causalById, queryEntities
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// 按注入顺序拼接 sections
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// 按注入顺序拼接 sections
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
const sections = [];
|
||||
// 1. 世界约束 → 定了的事
|
||||
if (assembled.facts.lines.length) {
|
||||
sections.push(`[定了的事] 已确立的事实\n${assembled.facts.lines.join("\n")}`);
|
||||
}
|
||||
// 2. 核心经历 → 印象深的事
|
||||
if (assembled.events.direct.length) {
|
||||
sections.push(`[印象深的事] 记得很清楚\n\n${assembled.events.direct.join("\n\n")}`);
|
||||
}
|
||||
// 3. 过往背景 → 好像有关的事
|
||||
if (assembled.events.similar.length) {
|
||||
sections.push(`[好像有关的事] 听说过或有点模糊\n\n${assembled.events.similar.join("\n\n")}`);
|
||||
}
|
||||
// 4. 远期片段 → 更早以前
|
||||
if (assembled.orphans.lines.length) {
|
||||
sections.push(`[更早以前] 记忆里残留的老画面\n${assembled.orphans.lines.join("\n")}`);
|
||||
}
|
||||
// 5. 待整理 → 刚发生的
|
||||
if (assembled.recentOrphans.lines.length) {
|
||||
sections.push(`[刚发生的] 还没来得及想明白\n${assembled.recentOrphans.lines.join("\n")}`);
|
||||
}
|
||||
// 6. 人物弧光 → 这些人
|
||||
if (assembled.arcs.lines.length) {
|
||||
sections.push(`[这些人] 他们现在怎样了\n${assembled.arcs.lines.join("\n")}`);
|
||||
}
|
||||
|
||||
const sections = [];
|
||||
|
||||
// 1. 世界约束
|
||||
if (assembled.world.lines.length) {
|
||||
sections.push(`[世界约束] 已确立的事实\n${assembled.world.lines.join("\n")}`);
|
||||
}
|
||||
|
||||
// 2. 核心经历
|
||||
if (assembled.events.direct.length) {
|
||||
sections.push(`[核心经历] 深刻的记忆\n\n${assembled.events.direct.join("\n\n")}`);
|
||||
}
|
||||
|
||||
// 3. 过往背景
|
||||
if (assembled.events.similar.length) {
|
||||
sections.push(`[过往背景] 听别人说起或比较模糊的往事\n\n${assembled.events.similar.join("\n\n")}`);
|
||||
}
|
||||
|
||||
// 4. 远期片段
|
||||
if (assembled.orphans.lines.length) {
|
||||
sections.push(`[远期片段] 记忆里残留的一些老画面\n${assembled.orphans.lines.join("\n")}`);
|
||||
}
|
||||
|
||||
// 5. 待整理
|
||||
if (assembled.recentOrphans.lines.length) {
|
||||
sections.push(`[待整理] 最近发生但尚未梳理的原始记忆\n${assembled.recentOrphans.lines.join("\n")}`);
|
||||
}
|
||||
|
||||
// 6. 人物弧光(最后注入,但预算已在优先级 2 预留)
|
||||
if (assembled.arcs.lines.length) {
|
||||
sections.push(`[人物弧光]\n${assembled.arcs.lines.join("\n")}`);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// 统计 & 返回
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
// 总预算 = 主装配 + 待整理
|
||||
injectionStats.budget.used = total.used + (recentOrphanStats.tokens || 0);
|
||||
|
||||
if (!sections.length) {
|
||||
if (!sections.length) {
|
||||
return { promptText: "", injectionLogText: "", injectionStats };
|
||||
}
|
||||
|
||||
@@ -663,6 +640,8 @@ async function buildVectorPrompt(store, recallResult, causalById, queryEntities
|
||||
`<剧情记忆>\n\n${sections.join("\n\n")}\n\n</剧情记忆>\n` +
|
||||
`${buildPostscript()}`;
|
||||
|
||||
// ★ 修复:先写回预算统计,再生成日志
|
||||
injectionStats.budget.used = total.used + (assembled.recentOrphans.tokens || 0);
|
||||
const injectionLogText = formatInjectionLog(injectionStats, details, recentOrphanStats);
|
||||
|
||||
return { promptText, injectionLogText, injectionStats };
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
events: { title: '编辑事件时间线', hint: '编辑时,每个事件要素都应完整' },
|
||||
characters: { title: '编辑人物关系', hint: '编辑时,每个要素都应完整' },
|
||||
arcs: { title: '编辑角色弧光', hint: '编辑时,每个要素都应完整' },
|
||||
world: { title: '编辑世界状态', hint: '每行一条:category|topic|content。清除用:category|topic|(留空)或 category|topic|cleared' }
|
||||
facts: { title: '编辑事实图谱', hint: '每行一条:主体|谓词|值|趋势(可选)。删除用:主体|谓词|(留空值)' }
|
||||
};
|
||||
|
||||
const TREND_COLORS = {
|
||||
@@ -116,7 +116,7 @@
|
||||
vector: { enabled: false, engine: 'online', local: { modelId: 'bge-small-zh' }, online: { provider: 'siliconflow', url: '', key: '', model: '' } }
|
||||
};
|
||||
|
||||
let summaryData = { keywords: [], events: [], characters: { main: [], relationships: [] }, arcs: [], world: [] };
|
||||
let summaryData = { keywords: [], events: [], characters: { main: [], relationships: [] }, arcs: [], facts: [] };
|
||||
let localGenerating = false;
|
||||
let vectorGenerating = false;
|
||||
let relationChart = null;
|
||||
@@ -1415,9 +1415,14 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "
|
||||
|
||||
if (section === 'keywords') {
|
||||
ta.value = summaryData.keywords.map(k => `${k.text}|${k.weight || '一般'}`).join('\n');
|
||||
} else if (section === 'world') {
|
||||
ta.value = (summaryData.world || [])
|
||||
.map(w => `${w.category || ''}|${w.topic || ''}|${w.content || ''}`)
|
||||
} else if (section === 'facts') {
|
||||
ta.value = (summaryData.facts || [])
|
||||
.filter(f => !f.retracted)
|
||||
.map(f => {
|
||||
const parts = [f.s, f.p, f.o];
|
||||
if (f.trend) parts.push(f.trend);
|
||||
return parts.join('|');
|
||||
})
|
||||
.join('\n');
|
||||
} else {
|
||||
ta.classList.add('hidden');
|
||||
@@ -1496,21 +1501,32 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "
|
||||
moments
|
||||
}, oldArc);
|
||||
}).filter(a => a.name || a.trajectory || a.moments?.length);
|
||||
} else if (section === 'world') {
|
||||
const oldWorldMap = new Map((summaryData.world || []).map(w => [`${w.category}|${w.topic}`, w]));
|
||||
} else if (section === 'facts') {
|
||||
const oldMap = new Map((summaryData.facts || []).map(f => [`${f.s}::${f.p}`, f]));
|
||||
parsed = ta.value
|
||||
.split('\n')
|
||||
.map(l => l.trim())
|
||||
.filter(Boolean)
|
||||
.map(line => {
|
||||
const parts = line.split('|').map(s => s.trim());
|
||||
const category = parts[0];
|
||||
const topic = parts[1];
|
||||
const content = parts.slice(2).join('|').trim();
|
||||
if (!category || !topic) return null;
|
||||
if (!content || content.toLowerCase() === 'cleared') return null;
|
||||
const key = `${category}|${topic}`;
|
||||
return preserveAddedAt({ category, topic, content }, oldWorldMap.get(key));
|
||||
const s = parts[0];
|
||||
const p = parts[1];
|
||||
const o = parts[2];
|
||||
const trend = parts[3];
|
||||
if (!s || !p) return null;
|
||||
if (!o) return null;
|
||||
const key = `${s}::${p}`;
|
||||
const old = oldMap.get(key);
|
||||
const fact = {
|
||||
id: old?.id || `f-${Date.now()}`,
|
||||
s, p, o,
|
||||
since: old?.since ?? 0,
|
||||
_addedAt: old?._addedAt ?? 0,
|
||||
};
|
||||
if (/^对.+的/.test(p) && trend) {
|
||||
fact.trend = trend;
|
||||
}
|
||||
return fact;
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
@@ -1526,7 +1542,7 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "
|
||||
else if (section === 'events') { renderTimeline(parsed); $('stat-events').textContent = parsed.length; }
|
||||
else if (section === 'characters') renderRelations(parsed);
|
||||
else if (section === 'arcs') renderArcs(parsed);
|
||||
else if (section === 'world') renderWorldState(parsed);
|
||||
else if (section === 'facts') renderFacts(parsed);
|
||||
|
||||
closeEditor();
|
||||
}
|
||||
@@ -1565,7 +1581,7 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "
|
||||
if (p.events) renderTimeline(p.events);
|
||||
if (p.characters) renderRelations(p.characters);
|
||||
if (p.arcs) renderArcs(p.arcs);
|
||||
if (p.world) renderWorldState(p.world);
|
||||
if (p.facts) renderFacts(p.facts);
|
||||
$('stat-events').textContent = p.events?.length || 0;
|
||||
if (p.lastSummarizedMesId != null) $('stat-summarized').textContent = p.lastSummarizedMesId + 1;
|
||||
if (p.stats) updateStats(p.stats);
|
||||
@@ -1582,12 +1598,12 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "
|
||||
$('stat-summarized').textContent = 0;
|
||||
$('stat-pending').textContent = t;
|
||||
$('summarized-count').textContent = 0;
|
||||
summaryData = { keywords: [], events: [], characters: { main: [], relationships: [] }, arcs: [], world: [] };
|
||||
summaryData = { keywords: [], events: [], characters: { main: [], relationships: [] }, arcs: [], facts: [] };
|
||||
renderKeywords([]);
|
||||
renderTimeline([]);
|
||||
renderRelations(null);
|
||||
renderArcs([]);
|
||||
renderWorldState([]);
|
||||
renderFacts([]);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1829,7 +1845,7 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "
|
||||
renderKeywords([]);
|
||||
renderTimeline([]);
|
||||
renderArcs([]);
|
||||
renderWorldState([]);
|
||||
renderFacts([]);
|
||||
|
||||
bindEvents();
|
||||
|
||||
@@ -1845,51 +1861,40 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "
|
||||
}
|
||||
|
||||
|
||||
function renderWorldState(world) {
|
||||
summaryData.world = world || [];
|
||||
function renderFacts(facts) {
|
||||
summaryData.facts = facts || [];
|
||||
|
||||
const container = $('world-state-list');
|
||||
const container = $('facts-list');
|
||||
if (!container) return;
|
||||
|
||||
if (!world?.length) {
|
||||
setHtml(container, '<div class="empty">暂无世界状态</div>');
|
||||
const isRelation = f => /^对.+的/.test(f.p);
|
||||
const stateFacts = (facts || []).filter(f => !f.retracted && !isRelation(f));
|
||||
|
||||
if (!stateFacts.length) {
|
||||
setHtml(container, '<div class="empty">暂无状态记录</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
const labels = {
|
||||
status: '状态',
|
||||
inventory: '物品',
|
||||
knowledge: '认知',
|
||||
relation: '关系',
|
||||
rule: '规则'
|
||||
};
|
||||
const grouped = new Map();
|
||||
for (const f of stateFacts) {
|
||||
if (!grouped.has(f.s)) grouped.set(f.s, []);
|
||||
grouped.get(f.s).push(f);
|
||||
}
|
||||
|
||||
const categoryOrder = ['status', 'inventory', 'relation', 'knowledge', 'rule'];
|
||||
let html = '';
|
||||
for (const [subject, items] of grouped) {
|
||||
html += `<div class="fact-group">
|
||||
<div class="fact-group-title">${h(subject)}</div>
|
||||
${items.map(f => `
|
||||
<div class="fact-item">
|
||||
<span class="fact-predicate">${h(f.p)}</span>
|
||||
<span class="fact-object">${h(f.o)}</span>
|
||||
<span class="fact-since">#${(f.since || 0) + 1}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const grouped = {};
|
||||
world.forEach(w => {
|
||||
const cat = w.category || 'other';
|
||||
if (!grouped[cat]) grouped[cat] = [];
|
||||
grouped[cat].push(w);
|
||||
});
|
||||
|
||||
const html = categoryOrder
|
||||
.filter(cat => grouped[cat]?.length)
|
||||
.map(cat => {
|
||||
const items = grouped[cat].sort((a, b) => (b.floor || 0) - (a.floor || 0));
|
||||
return `
|
||||
<div class="world-group">
|
||||
<div class="world-group-title">${labels[cat] || cat}</div>
|
||||
${items.map(w => `
|
||||
<div class="world-item">
|
||||
<span class="world-topic">${h(w.topic)}</span>
|
||||
<span class="world-content">${h(w.content)}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
setHtml(container, html || '<div class="empty">暂无世界状态</div>');
|
||||
setHtml(container, html);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -6,6 +6,79 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
Facts (替换 World State)
|
||||
═══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
.facts {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.facts-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.fact-group {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.fact-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.fact-group-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--hl);
|
||||
margin-bottom: 6px;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px dashed var(--bdr2);
|
||||
}
|
||||
|
||||
.fact-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
margin-bottom: 4px;
|
||||
background: var(--bg3);
|
||||
border: 1px solid var(--bdr2);
|
||||
border-radius: 4px;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.fact-predicate {
|
||||
color: var(--txt2);
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.fact-predicate::after {
|
||||
content: ':';
|
||||
}
|
||||
|
||||
.fact-object {
|
||||
color: var(--txt);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.fact-since {
|
||||
font-size: 0.625rem;
|
||||
color: var(--txt3);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.facts-list {
|
||||
max-height: 180px;
|
||||
}
|
||||
|
||||
.fact-item {
|
||||
padding: 6px 8px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg: #fafafa;
|
||||
--bg2: #fff;
|
||||
|
||||
@@ -80,13 +80,13 @@
|
||||
</div>
|
||||
|
||||
<div class="right">
|
||||
<!-- World State -->
|
||||
<section class="card world-state">
|
||||
<!-- Facts -->
|
||||
<section class="card facts">
|
||||
<div class="sec-head">
|
||||
<div class="sec-title">世界状态</div>
|
||||
<button class="sec-btn" data-section="world">编辑</button>
|
||||
<div class="sec-title">世界状态</div>
|
||||
<button class="sec-btn" data-section="facts">编辑</button>
|
||||
</div>
|
||||
<div class="world-state-list scroll" id="world-state-list"></div>
|
||||
<div class="facts-list scroll" id="facts-list"></div>
|
||||
</section>
|
||||
|
||||
<!-- Relations -->
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
calcHideRange,
|
||||
rollbackSummaryIfNeeded,
|
||||
clearSummaryData,
|
||||
extractRelationshipsFromFacts,
|
||||
} from "./data/store.js";
|
||||
|
||||
// prompt text builder
|
||||
@@ -109,6 +110,26 @@ let vectorAbortController = null;
|
||||
let lastSentUserMessage = null;
|
||||
let lastSentTimestamp = 0;
|
||||
|
||||
function captureUserInput() {
|
||||
const text = $("#send_textarea").val();
|
||||
if (text?.trim()) {
|
||||
lastSentUserMessage = text.trim();
|
||||
lastSentTimestamp = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
function onSendPointerdown(e) {
|
||||
if (e.target?.closest?.("#send_but")) {
|
||||
captureUserInput();
|
||||
}
|
||||
}
|
||||
|
||||
function onSendKeydown(e) {
|
||||
if (e.key === "Enter" && !e.shiftKey && e.target?.closest?.("#send_textarea")) {
|
||||
captureUserInput();
|
||||
}
|
||||
}
|
||||
|
||||
let hideApplyTimer = null;
|
||||
const HIDE_APPLY_DEBOUNCE_MS = 250;
|
||||
|
||||
@@ -826,24 +847,32 @@ async function sendFrameBaseData(store, totalFloors) {
|
||||
}
|
||||
|
||||
function sendFrameFullData(store, totalFloors) {
|
||||
const lastSummarized = store?.lastSummarizedMesId ?? -1;
|
||||
if (store?.json) {
|
||||
postToFrame({
|
||||
type: "SUMMARY_FULL_DATA",
|
||||
payload: {
|
||||
keywords: store.json.keywords || [],
|
||||
events: store.json.events || [],
|
||||
characters: store.json.characters || { main: [], relationships: [] },
|
||||
arcs: store.json.arcs || [],
|
||||
world: store.json.world || [],
|
||||
lastSummarizedMesId: lastSummarized,
|
||||
},
|
||||
payload: buildFramePayload(store),
|
||||
});
|
||||
} else {
|
||||
postToFrame({ type: "SUMMARY_CLEARED", payload: { totalFloors } });
|
||||
}
|
||||
}
|
||||
|
||||
function buildFramePayload(store) {
|
||||
const json = store?.json || {};
|
||||
const facts = json.facts || [];
|
||||
return {
|
||||
keywords: json.keywords || [],
|
||||
events: json.events || [],
|
||||
characters: {
|
||||
main: json.characters?.main || [],
|
||||
relationships: extractRelationshipsFromFacts(facts),
|
||||
},
|
||||
arcs: json.arcs || [],
|
||||
facts,
|
||||
lastSummarizedMesId: store?.lastSummarizedMesId ?? -1,
|
||||
};
|
||||
}
|
||||
|
||||
function openPanelForMessage(mesId) {
|
||||
createOverlay();
|
||||
showOverlay();
|
||||
@@ -953,20 +982,11 @@ async function autoRunSummaryWithRetry(targetMesId, configForRun) {
|
||||
onStatus: (text) => postToFrame({ type: "SUMMARY_STATUS", statusText: text }),
|
||||
onError: (msg) => postToFrame({ type: "SUMMARY_ERROR", message: msg }),
|
||||
onComplete: async ({ merged, endMesId, newEventIds }) => {
|
||||
postToFrame({
|
||||
type: "SUMMARY_FULL_DATA",
|
||||
payload: {
|
||||
keywords: merged.keywords || [],
|
||||
events: merged.events || [],
|
||||
characters: merged.characters || { main: [], relationships: [] },
|
||||
arcs: merged.arcs || [],
|
||||
world: merged.world || [],
|
||||
lastSummarizedMesId: endMesId,
|
||||
},
|
||||
});
|
||||
const store = getSummaryStore();
|
||||
postToFrame({ type: "SUMMARY_FULL_DATA", payload: buildFramePayload(store) });
|
||||
|
||||
applyHideStateDebounced();
|
||||
updateFrameStatsAfterSummary(endMesId, merged);
|
||||
updateFrameStatsAfterSummary(endMesId, store.json || {});
|
||||
|
||||
// L2 自动增量向量化
|
||||
await autoVectorizeNewEvents(newEventIds);
|
||||
@@ -1253,20 +1273,11 @@ async function handleManualGenerate(mesId, config) {
|
||||
onStatus: (text) => postToFrame({ type: "SUMMARY_STATUS", statusText: text }),
|
||||
onError: (msg) => postToFrame({ type: "SUMMARY_ERROR", message: msg }),
|
||||
onComplete: async ({ merged, endMesId, newEventIds }) => {
|
||||
postToFrame({
|
||||
type: "SUMMARY_FULL_DATA",
|
||||
payload: {
|
||||
keywords: merged.keywords || [],
|
||||
events: merged.events || [],
|
||||
characters: merged.characters || { main: [], relationships: [] },
|
||||
arcs: merged.arcs || [],
|
||||
world: merged.world || [],
|
||||
lastSummarizedMesId: endMesId,
|
||||
},
|
||||
});
|
||||
const store = getSummaryStore();
|
||||
postToFrame({ type: "SUMMARY_FULL_DATA", payload: buildFramePayload(store) });
|
||||
|
||||
applyHideStateDebounced();
|
||||
updateFrameStatsAfterSummary(endMesId, merged);
|
||||
updateFrameStatsAfterSummary(endMesId, store.json || {});
|
||||
|
||||
// L2 自动增量向量化
|
||||
await autoVectorizeNewEvents(newEventIds);
|
||||
@@ -1483,6 +1494,10 @@ function registerEvents() {
|
||||
eventSource.on(event_types.USER_MESSAGE_RENDERED, (data) => setTimeout(() => handleMessageRendered(data), 50));
|
||||
eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, (data) => setTimeout(() => handleMessageRendered(data), 50));
|
||||
|
||||
// 用户输入捕获(原生捕获阶段)
|
||||
document.addEventListener("pointerdown", onSendPointerdown, true);
|
||||
document.addEventListener("keydown", onSendKeydown, true);
|
||||
|
||||
// 注入链路
|
||||
eventSource.on(event_types.GENERATION_STARTED, handleGenerationStarted);
|
||||
eventSource.on(event_types.GENERATION_STOPPED, clearExtensionPrompt);
|
||||
@@ -1497,6 +1512,9 @@ function unregisterEvents() {
|
||||
hideOverlay();
|
||||
|
||||
clearExtensionPrompt();
|
||||
|
||||
document.removeEventListener("pointerdown", onSendPointerdown, true);
|
||||
document.removeEventListener("keydown", onSendKeydown, true);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
173
modules/story-summary/vector/text-search.js
Normal file
173
modules/story-summary/vector/text-search.js
Normal file
@@ -0,0 +1,173 @@
|
||||
// text-search.js - 最终版
|
||||
|
||||
import MiniSearch from '../../../libs/minisearch.mjs';
|
||||
|
||||
const STOP_WORDS = new Set([
|
||||
'的', '了', '是', '在', '和', '与', '或', '但', '而', '却',
|
||||
'这', '那', '他', '她', '它', '我', '你', '们', '着', '过',
|
||||
'把', '被', '给', '让', '向', '就', '都', '也', '还', '又',
|
||||
'很', '太', '更', '最', '只', '才', '已', '正', '会', '能',
|
||||
'要', '可', '得', '地', '之', '所', '以', '为', '于', '有',
|
||||
'不', '去', '来', '上', '下', '里', '说', '看', '吧', '呢',
|
||||
'啊', '吗', '呀', '哦', '嗯', '么',
|
||||
'の', 'に', 'は', 'を', 'が', 'と', 'で', 'へ', 'や', 'か',
|
||||
'も', 'な', 'よ', 'ね', 'わ', 'です', 'ます', 'した', 'ない',
|
||||
'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been',
|
||||
'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would',
|
||||
'to', 'of', 'in', 'on', 'at', 'for', 'with', 'by', 'from',
|
||||
'and', 'or', 'but', 'if', 'that', 'this', 'it', 'its',
|
||||
'i', 'you', 'he', 'she', 'we', 'they', 'my', 'your', 'his',
|
||||
]);
|
||||
|
||||
function tokenize(text) {
|
||||
const s = String(text || '').toLowerCase().trim();
|
||||
if (!s) return [];
|
||||
|
||||
const tokens = new Set();
|
||||
|
||||
// CJK Bigram + Trigram
|
||||
const cjk = s.match(/[\u4e00-\u9fff\u3400-\u4dbf]+/g) || [];
|
||||
for (const seg of cjk) {
|
||||
const chars = [...seg].filter(c => !STOP_WORDS.has(c));
|
||||
for (let i = 0; i < chars.length - 1; i++) {
|
||||
tokens.add(chars[i] + chars[i + 1]);
|
||||
}
|
||||
for (let i = 0; i < chars.length - 2; i++) {
|
||||
tokens.add(chars[i] + chars[i + 1] + chars[i + 2]);
|
||||
}
|
||||
}
|
||||
|
||||
// 日语假名
|
||||
const kana = s.match(/[\u3040-\u309f\u30a0-\u30ff]{2,}/g) || [];
|
||||
for (const k of kana) {
|
||||
if (!STOP_WORDS.has(k)) tokens.add(k);
|
||||
}
|
||||
|
||||
// 英文
|
||||
const en = s.match(/[a-z]{2,}/g) || [];
|
||||
for (const w of en) {
|
||||
if (!STOP_WORDS.has(w)) tokens.add(w);
|
||||
}
|
||||
|
||||
return [...tokens];
|
||||
}
|
||||
|
||||
let idx = null;
|
||||
let lastRevision = null;
|
||||
|
||||
function stripFloorTag(s) {
|
||||
return String(s || '').replace(/\s*\(#\d+(?:-\d+)?\)\s*$/, '').trim();
|
||||
}
|
||||
|
||||
export function ensureEventTextIndex(events, revision) {
|
||||
if (!events?.length) {
|
||||
idx = null;
|
||||
lastRevision = null;
|
||||
return;
|
||||
}
|
||||
if (idx && revision === lastRevision) return;
|
||||
|
||||
try {
|
||||
idx = new MiniSearch({
|
||||
fields: ['title', 'summary', 'participants'],
|
||||
storeFields: ['id'],
|
||||
tokenize,
|
||||
searchOptions: { tokenize },
|
||||
});
|
||||
|
||||
idx.addAll(events.map(e => ({
|
||||
id: e.id,
|
||||
title: e.title || '',
|
||||
summary: stripFloorTag(e.summary),
|
||||
participants: (e.participants || []).join(' '),
|
||||
})));
|
||||
|
||||
lastRevision = revision;
|
||||
} catch (e) {
|
||||
console.error('[text-search] Index build failed:', e);
|
||||
idx = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* BM25 检索,返回 top-K 候选给 RRF
|
||||
*
|
||||
* 设计原则:
|
||||
* - 不做分数过滤(BM25 分数跨查询不可比)
|
||||
* - 不做匹配数过滤(bigram 让一个词产生多个 token)
|
||||
* - 只做 top-K(BM25 排序本身有区分度)
|
||||
* - 质量过滤交给 RRF 后的 hasVector 过滤
|
||||
*/
|
||||
/**
|
||||
* 动态 top-K:累积分数占比法
|
||||
*
|
||||
* 原理:BM25 分数服从幂律分布,少数高分条目贡献大部分总分
|
||||
* 取累积分数达到阈值的最小 K
|
||||
*
|
||||
* 参考:帕累托法则(80/20 法则)在信息检索中的应用
|
||||
*/
|
||||
function dynamicTopK(scores, coverage = 0.90, minK = 15, maxK = 80) {
|
||||
if (!scores.length) return 0;
|
||||
|
||||
const total = scores.reduce((a, b) => a + b, 0);
|
||||
if (total <= 0) return Math.min(minK, scores.length);
|
||||
|
||||
let cumulative = 0;
|
||||
for (let i = 0; i < scores.length; i++) {
|
||||
cumulative += scores[i];
|
||||
if (cumulative / total >= coverage) {
|
||||
return Math.max(minK, Math.min(maxK, i + 1));
|
||||
}
|
||||
}
|
||||
|
||||
return Math.min(maxK, scores.length);
|
||||
}
|
||||
|
||||
export function searchEventsByText(queryText, limit = 80) {
|
||||
if (!idx || !queryText?.trim()) return [];
|
||||
|
||||
try {
|
||||
const results = idx.search(queryText, {
|
||||
boost: { title: 4, participants: 2, summary: 1 },
|
||||
fuzzy: false,
|
||||
prefix: false,
|
||||
});
|
||||
|
||||
if (!results.length) return [];
|
||||
|
||||
const scores = results.map(r => r.score);
|
||||
const k = dynamicTopK(scores, 0.90, 15, limit);
|
||||
|
||||
const output = results.slice(0, k).map((r, i) => ({
|
||||
id: r.id,
|
||||
textRank: i + 1,
|
||||
score: r.score,
|
||||
}));
|
||||
|
||||
const total = scores.reduce((a, b) => a + b, 0);
|
||||
const kCumulative = scores.slice(0, k).reduce((a, b) => a + b, 0);
|
||||
|
||||
output._gapInfo = {
|
||||
total: results.length,
|
||||
returned: k,
|
||||
coverage: ((kCumulative / total) * 100).toFixed(1) + '%',
|
||||
scoreRange: {
|
||||
top: scores[0]?.toFixed(1),
|
||||
cutoff: scores[k - 1]?.toFixed(1),
|
||||
p50: scores[Math.floor(scores.length / 2)]?.toFixed(1),
|
||||
last: scores[scores.length - 1]?.toFixed(1),
|
||||
},
|
||||
};
|
||||
|
||||
return output;
|
||||
|
||||
} catch (e) {
|
||||
console.error('[text-search] Search failed:', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function clearEventTextIndex() {
|
||||
idx = null;
|
||||
lastRevision = null;
|
||||
}
|
||||
287
modules/story-summary/vector/tokenizer.js
Normal file
287
modules/story-summary/vector/tokenizer.js
Normal file
@@ -0,0 +1,287 @@
|
||||
import { xbLog } from '../../../core/debug-core.js';
|
||||
import { extensionFolderPath } from '../../../core/constants.js';
|
||||
|
||||
const MODULE_ID = 'tokenizer';
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// 词性过滤
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// 保留的词性(名词类 + 英文)
|
||||
const KEEP_POS_PREFIXES = ['n', 'eng'];
|
||||
|
||||
function shouldKeepByPos(pos) {
|
||||
return KEEP_POS_PREFIXES.some(prefix => pos.startsWith(prefix));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// 语言检测
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function shouldUseJieba(text) {
|
||||
const zh = (text.match(/[\u4e00-\u9fff]/g) || []).length;
|
||||
return zh >= 5;
|
||||
}
|
||||
|
||||
function detectMainLanguage(text) {
|
||||
const zh = (text.match(/[\u4e00-\u9fff]/g) || []).length;
|
||||
const jp = (text.match(/[\u3040-\u309f\u30a0-\u30ff]/g) || []).length;
|
||||
const en = (text.match(/[a-zA-Z]/g) || []).length;
|
||||
const total = zh + jp + en || 1;
|
||||
|
||||
if (jp / total > 0.2) return 'jp';
|
||||
if (en / total > 0.5) return 'en';
|
||||
return 'zh';
|
||||
}
|
||||
|
||||
// 替换原有的大停用词表
|
||||
const STOP_WORDS = new Set([
|
||||
// 系统词
|
||||
'用户', '角色', '玩家', '旁白', 'user', 'assistant', 'system',
|
||||
// 时间泛词
|
||||
'时候', '现在', '今天', '明天', '昨天', '早上', '晚上',
|
||||
// 方位泛词
|
||||
'这里', '那里', '上面', '下面', '里面', '外面',
|
||||
// 泛化名词
|
||||
'东西', '事情', '事儿', '地方', '样子', '意思', '感觉',
|
||||
'一下', '一些', '一点', '一会', '一次',
|
||||
]);
|
||||
|
||||
// 英文停用词(fallback 用)
|
||||
const EN_STOP_WORDS = new Set([
|
||||
'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been',
|
||||
'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would',
|
||||
'could', 'should', 'may', 'might', 'must', 'can',
|
||||
'to', 'of', 'in', 'on', 'at', 'for', 'with', 'by', 'from',
|
||||
'and', 'or', 'but', 'if', 'that', 'this', 'it', 'its',
|
||||
'i', 'you', 'he', 'she', 'we', 'they',
|
||||
'my', 'your', 'his', 'her', 'our', 'their',
|
||||
'what', 'which', 'who', 'whom', 'where', 'when', 'why', 'how',
|
||||
]);
|
||||
|
||||
let jiebaModule = null;
|
||||
let jiebaReady = false;
|
||||
let jiebaLoading = false;
|
||||
|
||||
async function ensureJieba() {
|
||||
if (jiebaReady) return true;
|
||||
if (jiebaLoading) {
|
||||
for (let i = 0; i < 50; i++) {
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
if (jiebaReady) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
jiebaLoading = true;
|
||||
|
||||
try {
|
||||
const jiebaPath = `/${extensionFolderPath}/libs/jieba-wasm/jieba_rs_wasm.js`;
|
||||
// eslint-disable-next-line no-unsanitized/method
|
||||
jiebaModule = await import(jiebaPath);
|
||||
|
||||
if (jiebaModule.default) {
|
||||
await jiebaModule.default();
|
||||
}
|
||||
|
||||
jiebaReady = true;
|
||||
xbLog.info(MODULE_ID, 'jieba-wasm 加载成功');
|
||||
const keys = Object.getOwnPropertyNames(jiebaModule || {});
|
||||
const dkeys = Object.getOwnPropertyNames(jiebaModule?.default || {});
|
||||
xbLog.info(MODULE_ID, `jieba keys: ${keys.join(',')}`);
|
||||
xbLog.info(MODULE_ID, `jieba default keys: ${dkeys.join(',')}`);
|
||||
xbLog.info(MODULE_ID, `jieba.tag: ${typeof jiebaModule?.tag}`);
|
||||
return true;
|
||||
} catch (e) {
|
||||
xbLog.error(MODULE_ID, 'jieba-wasm 加载失败', e);
|
||||
jiebaLoading = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackTokenize(text) {
|
||||
const tokens = [];
|
||||
const lang = detectMainLanguage(text);
|
||||
|
||||
// 英文
|
||||
const enMatches = text.match(/[a-zA-Z]{2,20}/gi) || [];
|
||||
tokens.push(...enMatches.filter(w => !EN_STOP_WORDS.has(w.toLowerCase())));
|
||||
|
||||
// 日语假名
|
||||
if (lang === 'jp') {
|
||||
const kanaMatches = text.match(/[\u3040-\u309f\u30a0-\u30ff]{2,10}/g) || [];
|
||||
tokens.push(...kanaMatches);
|
||||
}
|
||||
|
||||
// 中文/日语汉字
|
||||
const zhMatches = text.match(/[\u4e00-\u9fff]{2,6}/g) || [];
|
||||
tokens.push(...zhMatches);
|
||||
|
||||
// 数字+汉字组合
|
||||
const numZhMatches = text.match(/\d+[\u4e00-\u9fff]{1,4}/g) || [];
|
||||
tokens.push(...numZhMatches);
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
export async function extractNouns(text, options = {}) {
|
||||
const { minLen = 2, maxCount = 0 } = options;
|
||||
if (!text?.trim()) return [];
|
||||
|
||||
// 中文为主 → 用 jieba
|
||||
if (shouldUseJieba(text)) {
|
||||
const hasJieba = await ensureJieba();
|
||||
|
||||
if (hasJieba && jiebaModule?.tag) {
|
||||
try {
|
||||
const tagged = jiebaModule.tag(text, true);
|
||||
|
||||
const result = [];
|
||||
const seen = new Set();
|
||||
|
||||
const list = Array.isArray(tagged) ? tagged : [];
|
||||
for (const item of list) {
|
||||
let word = '';
|
||||
let pos = '';
|
||||
if (Array.isArray(item)) {
|
||||
[word, pos] = item;
|
||||
} else if (item && typeof item === 'object') {
|
||||
word = item.word || item.w || item.text || item.term || '';
|
||||
pos = item.tag || item.pos || item.p || '';
|
||||
}
|
||||
if (!word || !pos) continue;
|
||||
if (word.length < minLen) continue;
|
||||
if (!shouldKeepByPos(pos)) continue;
|
||||
if (STOP_WORDS.has(word)) continue;
|
||||
if (seen.has(word)) continue;
|
||||
|
||||
seen.add(word);
|
||||
result.push(word);
|
||||
|
||||
if (maxCount > 0 && result.length >= maxCount) break;
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
xbLog.warn(MODULE_ID, 'jieba tag 失败:' + (e && e.message ? e.message : String(e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 非中文 / jieba 失败 → fallback
|
||||
const tokens = fallbackTokenize(text);
|
||||
|
||||
const result = [];
|
||||
const seen = new Set();
|
||||
|
||||
for (const t of tokens) {
|
||||
if (t.length < minLen) continue;
|
||||
if (STOP_WORDS.has(t)) continue;
|
||||
if (seen.has(t)) continue;
|
||||
|
||||
seen.add(t);
|
||||
result.push(t);
|
||||
|
||||
if (maxCount > 0 && result.length >= maxCount) break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function extractRareTerms(text, maxCount = 15) {
|
||||
if (!text?.trim()) return [];
|
||||
|
||||
// 中文为主 → 用 jieba
|
||||
if (shouldUseJieba(text)) {
|
||||
const hasJieba = await ensureJieba();
|
||||
|
||||
if (hasJieba && jiebaModule?.tag) {
|
||||
try {
|
||||
const tagged = jiebaModule.tag(text, true);
|
||||
|
||||
const candidates = [];
|
||||
const seen = new Set();
|
||||
|
||||
const list = Array.isArray(tagged) ? tagged : [];
|
||||
for (const item of list) {
|
||||
let word = '';
|
||||
let pos = '';
|
||||
if (Array.isArray(item)) {
|
||||
[word, pos] = item;
|
||||
} else if (item && typeof item === 'object') {
|
||||
word = item.word || item.w || item.text || item.term || '';
|
||||
pos = item.tag || item.pos || item.p || '';
|
||||
}
|
||||
if (!word || !pos) continue;
|
||||
if (word.length < 2) continue;
|
||||
if (!shouldKeepByPos(pos)) continue;
|
||||
if (STOP_WORDS.has(word)) continue;
|
||||
if (seen.has(word)) continue;
|
||||
|
||||
seen.add(word);
|
||||
|
||||
// 稀有度评分
|
||||
let score = 0;
|
||||
if (word.length >= 4) score += 3;
|
||||
else if (word.length >= 3) score += 1;
|
||||
if (/[a-zA-Z]/.test(word)) score += 2;
|
||||
if (/\d/.test(word)) score += 1;
|
||||
// 专名词性加分
|
||||
if (['nr', 'ns', 'nt', 'nz'].some(p => pos.startsWith(p))) score += 2;
|
||||
|
||||
candidates.push({ term: word, score });
|
||||
}
|
||||
|
||||
candidates.sort((a, b) => b.score - a.score);
|
||||
return candidates.slice(0, maxCount).map(x => x.term);
|
||||
} catch (e) {
|
||||
xbLog.warn(MODULE_ID, 'jieba tag 失败:' + (e && e.message ? e.message : String(e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 非中文 / jieba 失败 → fallback
|
||||
const allNouns = await extractNouns(text, { minLen: 2, maxCount: 0 });
|
||||
|
||||
const scored = allNouns.map(t => {
|
||||
let score = 0;
|
||||
if (t.length >= 4) score += 3;
|
||||
else if (t.length >= 3) score += 1;
|
||||
if (/[a-zA-Z]/.test(t)) score += 2;
|
||||
if (/\d/.test(t)) score += 1;
|
||||
return { term: t, score };
|
||||
});
|
||||
|
||||
scored.sort((a, b) => b.score - a.score);
|
||||
return scored.slice(0, maxCount).map(x => x.term);
|
||||
}
|
||||
|
||||
export async function extractNounsFromFactsO(facts, relevantSubjects, maxCount = 5) {
|
||||
if (!facts?.length || !relevantSubjects?.size) return [];
|
||||
|
||||
const oTexts = [];
|
||||
|
||||
for (const f of facts) {
|
||||
if (f.retracted) continue;
|
||||
|
||||
// 只取相关主体的 facts
|
||||
const s = String(f.s || '').trim();
|
||||
if (!relevantSubjects.has(s)) continue;
|
||||
|
||||
const o = String(f.o || '').trim();
|
||||
if (!o) continue;
|
||||
|
||||
// 跳过太长的 O(可能是完整句子)
|
||||
if (o.length > 30) continue;
|
||||
|
||||
oTexts.push(o);
|
||||
}
|
||||
|
||||
if (!oTexts.length) return [];
|
||||
|
||||
const combined = oTexts.join(' ');
|
||||
return await extractNouns(combined, { minLen: 2, maxCount });
|
||||
}
|
||||
|
||||
export { ensureJieba };
|
||||
|
||||
@@ -1,22 +1,343 @@
|
||||
import { getContext } from '../../../../../../extensions.js';
|
||||
import {
|
||||
lwbResolveVarPath,
|
||||
lwbAssignVarPath,
|
||||
lwbAddVarPath,
|
||||
lwbPushVarPath,
|
||||
lwbDeleteVarPath,
|
||||
lwbRemoveArrayItemByValue,
|
||||
} from '../var-commands.js';
|
||||
import { lwbSplitPathWithBrackets } from '../../../core/variable-path.js';
|
||||
|
||||
import { getLocalVariable, setLocalVariable } from '../../../../../../variables.js';
|
||||
import { extractStateBlocks, computeStateSignature, parseStateBlock } from './parser.js';
|
||||
import { generateSemantic } from './semantic.js';
|
||||
|
||||
const LWB_STATE_APPLIED_KEY = 'LWB_STATE_APPLIED_KEY';
|
||||
import { validate, setRule, loadRulesFromMeta, saveRulesToMeta } from './guard.js';
|
||||
|
||||
/**
|
||||
* chatMetadata 内记录每楼层 signature,防止重复执行
|
||||
* =========================
|
||||
* Path / JSON helpers
|
||||
* =========================
|
||||
*/
|
||||
function splitPath(path) {
|
||||
const s = String(path || '');
|
||||
const segs = [];
|
||||
let buf = '';
|
||||
let i = 0;
|
||||
|
||||
while (i < s.length) {
|
||||
const ch = s[i];
|
||||
if (ch === '.') {
|
||||
if (buf) { segs.push(/^\d+$/.test(buf) ? Number(buf) : buf); buf = ''; }
|
||||
i++;
|
||||
} else if (ch === '[') {
|
||||
if (buf) { segs.push(/^\d+$/.test(buf) ? Number(buf) : buf); buf = ''; }
|
||||
i++;
|
||||
let val = '';
|
||||
if (s[i] === '"' || s[i] === "'") {
|
||||
const q = s[i++];
|
||||
while (i < s.length && s[i] !== q) val += s[i++];
|
||||
i++;
|
||||
} else {
|
||||
while (i < s.length && s[i] !== ']') val += s[i++];
|
||||
}
|
||||
if (s[i] === ']') i++;
|
||||
segs.push(/^\d+$/.test(val.trim()) ? Number(val.trim()) : val.trim());
|
||||
} else {
|
||||
buf += ch;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
if (buf) segs.push(/^\d+$/.test(buf) ? Number(buf) : buf);
|
||||
return segs;
|
||||
}
|
||||
|
||||
function normalizePath(path) {
|
||||
return splitPath(path).map(String).join('.');
|
||||
}
|
||||
|
||||
function safeJSON(v) {
|
||||
try { return JSON.stringify(v); } catch { return ''; }
|
||||
}
|
||||
|
||||
function safeParse(s) {
|
||||
if (s == null || s === '') return undefined;
|
||||
if (typeof s !== 'string') return s;
|
||||
const t = s.trim();
|
||||
if (!t) return undefined;
|
||||
if (t[0] === '{' || t[0] === '[') {
|
||||
try { return JSON.parse(t); } catch { return s; }
|
||||
}
|
||||
if (/^-?\d+(?:\.\d+)?$/.test(t)) return Number(t);
|
||||
if (t === 'true') return true;
|
||||
if (t === 'false') return false;
|
||||
return s;
|
||||
}
|
||||
|
||||
function deepClone(obj) {
|
||||
try { return structuredClone(obj); } catch {
|
||||
try { return JSON.parse(JSON.stringify(obj)); } catch { return obj; }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* =========================
|
||||
* Variable getters/setters (local vars)
|
||||
* =========================
|
||||
*/
|
||||
function getVar(path) {
|
||||
const segs = splitPath(path);
|
||||
if (!segs.length) return undefined;
|
||||
|
||||
const rootRaw = getLocalVariable(String(segs[0]));
|
||||
if (segs.length === 1) return safeParse(rootRaw);
|
||||
|
||||
let obj = safeParse(rootRaw);
|
||||
if (!obj || typeof obj !== 'object') return undefined;
|
||||
|
||||
for (let i = 1; i < segs.length; i++) {
|
||||
obj = obj?.[segs[i]];
|
||||
if (obj === undefined) return undefined;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
function setVar(path, value) {
|
||||
const segs = splitPath(path);
|
||||
if (!segs.length) return;
|
||||
|
||||
const rootName = String(segs[0]);
|
||||
|
||||
if (segs.length === 1) {
|
||||
const toStore = (value && typeof value === 'object') ? safeJSON(value) : String(value ?? '');
|
||||
setLocalVariable(rootName, toStore);
|
||||
return;
|
||||
}
|
||||
|
||||
let root = safeParse(getLocalVariable(rootName));
|
||||
if (!root || typeof root !== 'object') {
|
||||
root = typeof segs[1] === 'number' ? [] : {};
|
||||
}
|
||||
|
||||
let cur = root;
|
||||
for (let i = 1; i < segs.length - 1; i++) {
|
||||
const key = segs[i];
|
||||
const nextKey = segs[i + 1];
|
||||
if (cur[key] == null || typeof cur[key] !== 'object') {
|
||||
cur[key] = typeof nextKey === 'number' ? [] : {};
|
||||
}
|
||||
cur = cur[key];
|
||||
}
|
||||
cur[segs[segs.length - 1]] = value;
|
||||
|
||||
setLocalVariable(rootName, safeJSON(root));
|
||||
}
|
||||
|
||||
function delVar(path) {
|
||||
const segs = splitPath(path);
|
||||
if (!segs.length) return;
|
||||
|
||||
const rootName = String(segs[0]);
|
||||
|
||||
if (segs.length === 1) {
|
||||
setLocalVariable(rootName, '');
|
||||
return;
|
||||
}
|
||||
|
||||
let root = safeParse(getLocalVariable(rootName));
|
||||
if (!root || typeof root !== 'object') return;
|
||||
|
||||
let cur = root;
|
||||
for (let i = 1; i < segs.length - 1; i++) {
|
||||
cur = cur?.[segs[i]];
|
||||
if (!cur || typeof cur !== 'object') return;
|
||||
}
|
||||
|
||||
const lastKey = segs[segs.length - 1];
|
||||
if (Array.isArray(cur) && typeof lastKey === 'number') {
|
||||
cur.splice(lastKey, 1);
|
||||
} else {
|
||||
delete cur[lastKey];
|
||||
}
|
||||
|
||||
setLocalVariable(rootName, safeJSON(root));
|
||||
}
|
||||
|
||||
function pushVar(path, value) {
|
||||
const segs = splitPath(path);
|
||||
if (!segs.length) return { ok: false, reason: 'invalid-path' };
|
||||
|
||||
const rootName = String(segs[0]);
|
||||
|
||||
if (segs.length === 1) {
|
||||
let arr = safeParse(getLocalVariable(rootName));
|
||||
// ✅ 类型检查:必须是数组或不存在
|
||||
if (arr !== undefined && !Array.isArray(arr)) {
|
||||
return { ok: false, reason: 'not-array' };
|
||||
}
|
||||
if (!Array.isArray(arr)) arr = [];
|
||||
const items = Array.isArray(value) ? value : [value];
|
||||
arr.push(...items);
|
||||
setLocalVariable(rootName, safeJSON(arr));
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
let root = safeParse(getLocalVariable(rootName));
|
||||
if (!root || typeof root !== 'object') {
|
||||
root = typeof segs[1] === 'number' ? [] : {};
|
||||
}
|
||||
|
||||
let cur = root;
|
||||
for (let i = 1; i < segs.length - 1; i++) {
|
||||
const key = segs[i];
|
||||
const nextKey = segs[i + 1];
|
||||
if (cur[key] == null || typeof cur[key] !== 'object') {
|
||||
cur[key] = typeof nextKey === 'number' ? [] : {};
|
||||
}
|
||||
cur = cur[key];
|
||||
}
|
||||
|
||||
const lastKey = segs[segs.length - 1];
|
||||
let arr = cur[lastKey];
|
||||
|
||||
// ✅ 类型检查:必须是数组或不存在
|
||||
if (arr !== undefined && !Array.isArray(arr)) {
|
||||
return { ok: false, reason: 'not-array' };
|
||||
}
|
||||
if (!Array.isArray(arr)) arr = [];
|
||||
|
||||
const items = Array.isArray(value) ? value : [value];
|
||||
arr.push(...items);
|
||||
cur[lastKey] = arr;
|
||||
|
||||
setLocalVariable(rootName, safeJSON(root));
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
function popVar(path, value) {
|
||||
const segs = splitPath(path);
|
||||
if (!segs.length) return { ok: false, reason: 'invalid-path' };
|
||||
|
||||
const rootName = String(segs[0]);
|
||||
let root = safeParse(getLocalVariable(rootName));
|
||||
|
||||
if (segs.length === 1) {
|
||||
if (!Array.isArray(root)) {
|
||||
return { ok: false, reason: 'not-array' };
|
||||
}
|
||||
const toRemove = Array.isArray(value) ? value : [value];
|
||||
for (const v of toRemove) {
|
||||
const vStr = safeJSON(v);
|
||||
const idx = root.findIndex(x => safeJSON(x) === vStr);
|
||||
if (idx !== -1) root.splice(idx, 1);
|
||||
}
|
||||
setLocalVariable(rootName, safeJSON(root));
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
if (!root || typeof root !== 'object') {
|
||||
return { ok: false, reason: 'not-array' };
|
||||
}
|
||||
|
||||
let cur = root;
|
||||
for (let i = 1; i < segs.length - 1; i++) {
|
||||
cur = cur?.[segs[i]];
|
||||
if (!cur || typeof cur !== 'object') {
|
||||
return { ok: false, reason: 'path-not-found' };
|
||||
}
|
||||
}
|
||||
|
||||
const lastKey = segs[segs.length - 1];
|
||||
let arr = cur[lastKey];
|
||||
|
||||
if (!Array.isArray(arr)) {
|
||||
return { ok: false, reason: 'not-array' };
|
||||
}
|
||||
|
||||
const toRemove = Array.isArray(value) ? value : [value];
|
||||
for (const v of toRemove) {
|
||||
const vStr = safeJSON(v);
|
||||
const idx = arr.findIndex(x => safeJSON(x) === vStr);
|
||||
if (idx !== -1) arr.splice(idx, 1);
|
||||
}
|
||||
|
||||
setLocalVariable(rootName, safeJSON(root));
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* =========================
|
||||
* Storage (chat_metadata.extensions.LittleWhiteBox)
|
||||
* =========================
|
||||
*/
|
||||
const EXT_ID = 'LittleWhiteBox';
|
||||
const ERR_VAR_NAME = 'LWB_STATE_ERRORS';
|
||||
const LOG_KEY = 'stateLogV2';
|
||||
const CKPT_KEY = 'stateCkptV2';
|
||||
|
||||
|
||||
/**
|
||||
* 写入状态错误到本地变量(覆盖写入)
|
||||
*/
|
||||
function writeStateErrorsToLocalVar(lines) {
|
||||
try {
|
||||
const text = Array.isArray(lines) && lines.length
|
||||
? lines.map(s => `- ${String(s)}`).join('\n')
|
||||
: '';
|
||||
setLocalVariable(ERR_VAR_NAME, text);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function getLwbExtMeta() {
|
||||
const ctx = getContext();
|
||||
const meta = ctx?.chatMetadata || (ctx.chatMetadata = {});
|
||||
meta.extensions ||= {};
|
||||
meta.extensions[EXT_ID] ||= {};
|
||||
return meta.extensions[EXT_ID];
|
||||
}
|
||||
|
||||
function getStateLog() {
|
||||
const ext = getLwbExtMeta();
|
||||
ext[LOG_KEY] ||= { version: 1, floors: {} };
|
||||
return ext[LOG_KEY];
|
||||
}
|
||||
|
||||
function getCheckpointStore() {
|
||||
const ext = getLwbExtMeta();
|
||||
ext[CKPT_KEY] ||= { version: 1, every: 50, points: {} };
|
||||
return ext[CKPT_KEY];
|
||||
}
|
||||
|
||||
function saveWalRecord(floor, signature, rules, ops) {
|
||||
const log = getStateLog();
|
||||
log.floors[String(floor)] = {
|
||||
signature: String(signature || ''),
|
||||
rules: Array.isArray(rules) ? deepClone(rules) : [],
|
||||
ops: Array.isArray(ops) ? deepClone(ops) : [],
|
||||
ts: Date.now(),
|
||||
};
|
||||
getContext()?.saveMetadataDebounced?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* checkpoint = 执行完 floor 后的全量变量+规则
|
||||
*/
|
||||
function saveCheckpointIfNeeded(floor) {
|
||||
const ckpt = getCheckpointStore();
|
||||
const every = Number(ckpt.every) || 50;
|
||||
|
||||
// floor=0 也可以存,但一般没意义;你可按需调整
|
||||
if (floor < 0) return;
|
||||
if (every <= 0) return;
|
||||
if (floor % every !== 0) return;
|
||||
|
||||
const ctx = getContext();
|
||||
const meta = ctx?.chatMetadata || {};
|
||||
const vars = deepClone(meta.variables || {});
|
||||
// 2.0 rules 存在 chatMetadata 里(guard.js 写入的位置)
|
||||
const rules = deepClone(meta.LWB_RULES_V2 || {});
|
||||
|
||||
ckpt.points[String(floor)] = { vars, rules, ts: Date.now() };
|
||||
ctx?.saveMetadataDebounced?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* =========================
|
||||
* Applied signature map (idempotent)
|
||||
* =========================
|
||||
*/
|
||||
const LWB_STATE_APPLIED_KEY = 'LWB_STATE_APPLIED_KEY';
|
||||
|
||||
function getAppliedMap() {
|
||||
const meta = getContext()?.chatMetadata || {};
|
||||
meta[LWB_STATE_APPLIED_KEY] ||= {};
|
||||
@@ -25,8 +346,7 @@ function getAppliedMap() {
|
||||
|
||||
export function clearStateAppliedFor(floor) {
|
||||
try {
|
||||
const map = getAppliedMap();
|
||||
delete map[floor];
|
||||
delete getAppliedMap()[floor];
|
||||
getContext()?.saveMetadataDebounced?.();
|
||||
} catch {}
|
||||
}
|
||||
@@ -35,44 +355,24 @@ export function clearStateAppliedFrom(floorInclusive) {
|
||||
try {
|
||||
const map = getAppliedMap();
|
||||
for (const k of Object.keys(map)) {
|
||||
const id = Number(k);
|
||||
if (!Number.isNaN(id) && id >= floorInclusive) delete map[k];
|
||||
if (Number(k) >= floorInclusive) delete map[k];
|
||||
}
|
||||
getContext()?.saveMetadataDebounced?.();
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function safeParseAny(str) {
|
||||
if (str == null || str === '') return undefined;
|
||||
if (typeof str !== 'string') return str;
|
||||
const t = str.trim();
|
||||
if (!t) return undefined;
|
||||
if (t[0] === '{' || t[0] === '[') {
|
||||
try { return JSON.parse(t); } catch { return str; }
|
||||
}
|
||||
if (/^-?\d+(?:\.\d+)?$/.test(t)) return Number(t);
|
||||
if (t === 'true') return true;
|
||||
if (t === 'false') return false;
|
||||
return str;
|
||||
}
|
||||
|
||||
function isIndexDeleteOp(opItem) {
|
||||
if (!opItem || opItem.op !== 'del') return false;
|
||||
const segs = lwbSplitPathWithBrackets(opItem.path);
|
||||
const segs = splitPath(opItem.path);
|
||||
if (!segs.length) return false;
|
||||
const last = segs[segs.length - 1];
|
||||
return typeof last === 'number' && Number.isFinite(last);
|
||||
}
|
||||
|
||||
function buildParentPathFromSegs(segs) {
|
||||
return segs.reduce((acc, s) => {
|
||||
if (typeof s === 'number') return `${acc}[${s}]`;
|
||||
return acc ? `${acc}.${s}` : String(s);
|
||||
}, '');
|
||||
}
|
||||
|
||||
function buildExecOpsWithIndexDeleteReorder(ops) {
|
||||
const groups = new Map(); // parentPath -> [{ op, idx }]
|
||||
// 同一个数组的 index-del:按 parentPath 分组,组内 index 倒序
|
||||
// 其它操作:保持原顺序
|
||||
const groups = new Map(); // parentPath -> { order, items: [{...opItem, index}] }
|
||||
const groupOrder = new Map();
|
||||
let orderCounter = 0;
|
||||
|
||||
@@ -80,9 +380,12 @@ function buildExecOpsWithIndexDeleteReorder(ops) {
|
||||
|
||||
for (const op of ops) {
|
||||
if (isIndexDeleteOp(op)) {
|
||||
const segs = lwbSplitPathWithBrackets(op.path);
|
||||
const segs = splitPath(op.path);
|
||||
const idx = segs[segs.length - 1];
|
||||
const parentPath = buildParentPathFromSegs(segs.slice(0, -1));
|
||||
const parentPath = segs.slice(0, -1).reduce((acc, s) => {
|
||||
if (typeof s === 'number') return acc + `[${s}]`;
|
||||
return acc ? `${acc}.${s}` : String(s);
|
||||
}, '');
|
||||
|
||||
if (!groups.has(parentPath)) {
|
||||
groups.set(parentPath, []);
|
||||
@@ -94,85 +397,149 @@ function buildExecOpsWithIndexDeleteReorder(ops) {
|
||||
}
|
||||
}
|
||||
|
||||
const orderedParents = Array.from(groups.keys()).sort(
|
||||
(a, b) => (groupOrder.get(a) ?? 0) - (groupOrder.get(b) ?? 0)
|
||||
);
|
||||
// 按“该数组第一次出现的顺序”输出各组(可预测)
|
||||
const orderedParents = Array.from(groups.keys()).sort((a, b) => (groupOrder.get(a) ?? 0) - (groupOrder.get(b) ?? 0));
|
||||
|
||||
const reorderedIndexDeletes = [];
|
||||
for (const parent of orderedParents) {
|
||||
const items = groups.get(parent) || [];
|
||||
// 关键:倒序
|
||||
items.sort((a, b) => b.idx - a.idx);
|
||||
for (const it of items) reorderedIndexDeletes.push(it.op);
|
||||
}
|
||||
|
||||
// ✅ 我们把“索引删除”放在最前面执行:这样它们永远按“原索引”删
|
||||
// (避免在同一轮里先删后 push 导致索引变化)
|
||||
return [...reorderedIndexDeletes, ...normalOps];
|
||||
}
|
||||
|
||||
/**
|
||||
* 变量 2.0:执行单条消息里的 <state>,返回 atoms
|
||||
* =========================
|
||||
* Core: apply one message text (<state>...) => update vars + rules + wal + checkpoint
|
||||
* =========================
|
||||
*/
|
||||
export function applyStateForMessage(messageId, messageContent) {
|
||||
const ctx = getContext();
|
||||
const chatId = ctx?.chatId || '';
|
||||
|
||||
loadRulesFromMeta();
|
||||
|
||||
const text = String(messageContent ?? '');
|
||||
const signature = computeStateSignature(text);
|
||||
|
||||
// 没有 state:清理旧 signature(避免“删掉 state 后仍然认为执行过”)
|
||||
if (!signature) {
|
||||
const blocks = extractStateBlocks(text);
|
||||
// ✅ 统一:只要没有可执行 blocks,就视为本层 state 被移除
|
||||
if (!signature || blocks.length === 0) {
|
||||
clearStateAppliedFor(messageId);
|
||||
writeStateErrorsToLocalVar([]);
|
||||
// delete WAL record
|
||||
try {
|
||||
const ext = getLwbExtMeta();
|
||||
const log = ext[LOG_KEY];
|
||||
if (log?.floors) delete log.floors[String(messageId)];
|
||||
getContext()?.saveMetadataDebounced?.();
|
||||
} catch {}
|
||||
return { atoms: [], errors: [], skipped: false };
|
||||
}
|
||||
|
||||
// 幂等:signature 没变就跳过
|
||||
const appliedMap = getAppliedMap();
|
||||
if (appliedMap[messageId] === signature) {
|
||||
return { atoms: [], errors: [], skipped: true };
|
||||
}
|
||||
|
||||
const blocks = extractStateBlocks(text);
|
||||
const atoms = [];
|
||||
const errors = [];
|
||||
|
||||
let idx = 0;
|
||||
|
||||
const mergedRules = [];
|
||||
const mergedOps = [];
|
||||
|
||||
for (const block of blocks) {
|
||||
const ops = parseStateBlock(block);
|
||||
const execOps = buildExecOpsWithIndexDeleteReorder(ops);
|
||||
const parsed = parseStateBlock(block);
|
||||
mergedRules.push(...(parsed?.rules || []));
|
||||
mergedOps.push(...(parsed?.ops || []));
|
||||
}
|
||||
|
||||
if (blocks.length) {
|
||||
// ✅ WAL:一次写入完整的 rules/ops
|
||||
saveWalRecord(messageId, signature, mergedRules, mergedOps);
|
||||
|
||||
// ✅ rules 一次性注册
|
||||
let rulesTouched = false;
|
||||
for (const { path, rule } of mergedRules) {
|
||||
if (path && rule && Object.keys(rule).length) {
|
||||
setRule(normalizePath(path), rule);
|
||||
rulesTouched = true;
|
||||
}
|
||||
}
|
||||
if (rulesTouched) saveRulesToMeta();
|
||||
|
||||
const execOps = buildExecOpsWithIndexDeleteReorder(mergedOps);
|
||||
|
||||
// 执行操作(用 execOps)
|
||||
for (const opItem of execOps) {
|
||||
const { path, op, value, delta, warning } = opItem;
|
||||
if (!path) continue;
|
||||
if (warning) errors.push(`[${path}] ${warning}`);
|
||||
|
||||
const oldValue = safeParseAny(lwbResolveVarPath(path));
|
||||
const absPath = normalizePath(path);
|
||||
const oldValue = getVar(path);
|
||||
|
||||
const guard = validate(op, absPath, op === 'inc' ? delta : value, oldValue);
|
||||
if (!guard.allow) {
|
||||
errors.push(`${path}: ${guard.reason || '\u88ab\u89c4\u5219\u62d2\u7edd'}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 记录修正信息
|
||||
if (guard.note) {
|
||||
if (op === 'inc') {
|
||||
const raw = Number(delta);
|
||||
const rawTxt = Number.isFinite(raw) ? `${raw >= 0 ? '+' : ''}${raw}` : String(delta ?? '');
|
||||
errors.push(`${path}: ${rawTxt} ${guard.note}`);
|
||||
} else {
|
||||
errors.push(`${path}: ${guard.note}`);
|
||||
}
|
||||
}
|
||||
|
||||
let execOk = true;
|
||||
let execReason = '';
|
||||
|
||||
try {
|
||||
switch (op) {
|
||||
case 'set':
|
||||
lwbAssignVarPath(path, value);
|
||||
setVar(path, guard.value);
|
||||
break;
|
||||
case 'inc':
|
||||
lwbAddVarPath(path, delta);
|
||||
// guard.value 对 inc 是最终 nextValue
|
||||
setVar(path, guard.value);
|
||||
break;
|
||||
case 'push':
|
||||
lwbPushVarPath(path, value);
|
||||
case 'push': {
|
||||
const result = pushVar(path, guard.value);
|
||||
if (!result.ok) { execOk = false; execReason = result.reason; }
|
||||
break;
|
||||
case 'pop':
|
||||
lwbRemoveArrayItemByValue(path, value);
|
||||
}
|
||||
case 'pop': {
|
||||
const result = popVar(path, guard.value);
|
||||
if (!result.ok) { execOk = false; execReason = result.reason; }
|
||||
break;
|
||||
}
|
||||
case 'del':
|
||||
lwbDeleteVarPath(path);
|
||||
delVar(path);
|
||||
break;
|
||||
default:
|
||||
errors.push(`[${path}] 未知 op=${op}`);
|
||||
continue;
|
||||
execOk = false;
|
||||
execReason = `未知 op=${op}`;
|
||||
}
|
||||
} catch (e) {
|
||||
errors.push(`[${path}] 执行失败: ${e?.message || e}`);
|
||||
execOk = false;
|
||||
execReason = e?.message || String(e);
|
||||
}
|
||||
|
||||
if (!execOk) {
|
||||
errors.push(`[${path}] 失败: ${execReason}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const newValue = safeParseAny(lwbResolveVarPath(path));
|
||||
const newValue = getVar(path);
|
||||
|
||||
atoms.push({
|
||||
atomId: `sa-${messageId}-${idx}`,
|
||||
@@ -195,5 +562,185 @@ export function applyStateForMessage(messageId, messageContent) {
|
||||
appliedMap[messageId] = signature;
|
||||
getContext()?.saveMetadataDebounced?.();
|
||||
|
||||
// ✅ checkpoint:执行完该楼后,可选存一次全量
|
||||
saveCheckpointIfNeeded(messageId);
|
||||
|
||||
// Write error list to local variable
|
||||
writeStateErrorsToLocalVar(errors);
|
||||
|
||||
return { atoms, errors, skipped: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* =========================
|
||||
* Restore / Replay (for rollback & rebuild)
|
||||
* =========================
|
||||
*/
|
||||
|
||||
/**
|
||||
* 恢复到 targetFloor 执行完成后的变量状态(含规则)
|
||||
* - 使用最近 checkpoint,然后 replay WAL
|
||||
* - 不依赖消息文本 <state>(避免被正则清掉)
|
||||
*/
|
||||
export async function restoreStateV2ToFloor(targetFloor) {
|
||||
const ctx = getContext();
|
||||
const meta = ctx?.chatMetadata || {};
|
||||
const floor = Number(targetFloor);
|
||||
|
||||
if (!Number.isFinite(floor) || floor < 0) {
|
||||
// floor < 0 => 清空
|
||||
meta.variables = {};
|
||||
meta.LWB_RULES_V2 = {};
|
||||
ctx?.saveMetadataDebounced?.();
|
||||
return { ok: true, usedCheckpoint: null };
|
||||
}
|
||||
|
||||
const log = getStateLog();
|
||||
const ckpt = getCheckpointStore();
|
||||
const points = ckpt.points || {};
|
||||
const available = Object.keys(points)
|
||||
.map(Number)
|
||||
.filter(n => Number.isFinite(n) && n <= floor)
|
||||
.sort((a, b) => b - a);
|
||||
|
||||
const ck = available.length ? available[0] : null;
|
||||
|
||||
// 1) 恢复 checkpoint 或清空基线
|
||||
if (ck != null) {
|
||||
const snap = points[String(ck)];
|
||||
meta.variables = deepClone(snap?.vars || {});
|
||||
meta.LWB_RULES_V2 = deepClone(snap?.rules || {});
|
||||
} else {
|
||||
meta.variables = {};
|
||||
meta.LWB_RULES_V2 = {};
|
||||
}
|
||||
|
||||
ctx?.saveMetadataDebounced?.();
|
||||
|
||||
// 2) 从 meta 载入规则到内存(guard.js 的内存表)
|
||||
loadRulesFromMeta();
|
||||
|
||||
let rulesTouchedAny = false;
|
||||
|
||||
// 3) replay WAL: (ck+1 .. floor)
|
||||
const start = ck == null ? 0 : (ck + 1);
|
||||
for (let f = start; f <= floor; f++) {
|
||||
const rec = log.floors?.[String(f)];
|
||||
if (!rec) continue;
|
||||
|
||||
// 先应用 rules
|
||||
const rules = Array.isArray(rec.rules) ? rec.rules : [];
|
||||
let touched = false;
|
||||
for (const r of rules) {
|
||||
const p = r?.path;
|
||||
const rule = r?.rule;
|
||||
if (p && rule && typeof rule === 'object') {
|
||||
setRule(normalizePath(p), rule);
|
||||
touched = true;
|
||||
}
|
||||
}
|
||||
if (touched) rulesTouchedAny = true;
|
||||
|
||||
// 再应用 ops(不产出 atoms、不写 wal)
|
||||
const ops = Array.isArray(rec.ops) ? rec.ops : [];
|
||||
const execOps = buildExecOpsWithIndexDeleteReorder(ops);
|
||||
for (const opItem of execOps) {
|
||||
const path = opItem?.path;
|
||||
const op = opItem?.op;
|
||||
if (!path || !op) continue;
|
||||
|
||||
const absPath = normalizePath(path);
|
||||
const oldValue = getVar(path);
|
||||
|
||||
const payload = (op === 'inc') ? opItem.delta : opItem.value;
|
||||
const guard = validate(op, absPath, payload, oldValue);
|
||||
if (!guard.allow) continue;
|
||||
|
||||
try {
|
||||
switch (op) {
|
||||
case 'set':
|
||||
setVar(path, guard.value);
|
||||
break;
|
||||
case 'inc':
|
||||
setVar(path, guard.value);
|
||||
break;
|
||||
case 'push': {
|
||||
const result = pushVar(path, guard.value);
|
||||
if (!result.ok) {/* ignore */}
|
||||
break;
|
||||
}
|
||||
case 'pop': {
|
||||
const result = popVar(path, guard.value);
|
||||
if (!result.ok) {/* ignore */}
|
||||
break;
|
||||
}
|
||||
case 'del':
|
||||
delVar(path);
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// ignore replay errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rulesTouchedAny) {
|
||||
saveRulesToMeta();
|
||||
}
|
||||
|
||||
// 4) 清理 applied signature:floor 之后都要重新计算
|
||||
clearStateAppliedFrom(floor + 1);
|
||||
|
||||
ctx?.saveMetadataDebounced?.();
|
||||
return { ok: true, usedCheckpoint: ck };
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 floor >= fromFloor 的 2.0 持久化数据:
|
||||
* - WAL: stateLogV2.floors
|
||||
* - checkpoint: stateCkptV2.points
|
||||
* - applied signature: LWB_STATE_APPLIED_KEY
|
||||
*
|
||||
* 用于 MESSAGE_DELETED 等“物理删除消息”场景,避免 WAL/ckpt 无限膨胀。
|
||||
*/
|
||||
export async function trimStateV2FromFloor(fromFloor) {
|
||||
const start = Number(fromFloor);
|
||||
if (!Number.isFinite(start)) return { ok: false };
|
||||
|
||||
const ctx = getContext();
|
||||
const meta = ctx?.chatMetadata || {};
|
||||
meta.extensions ||= {};
|
||||
meta.extensions[EXT_ID] ||= {};
|
||||
|
||||
const ext = meta.extensions[EXT_ID];
|
||||
|
||||
// 1) WAL
|
||||
const log = ext[LOG_KEY];
|
||||
if (log?.floors && typeof log.floors === 'object') {
|
||||
for (const k of Object.keys(log.floors)) {
|
||||
const f = Number(k);
|
||||
if (Number.isFinite(f) && f >= start) {
|
||||
delete log.floors[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Checkpoints
|
||||
const ckpt = ext[CKPT_KEY];
|
||||
if (ckpt?.points && typeof ckpt.points === 'object') {
|
||||
for (const k of Object.keys(ckpt.points)) {
|
||||
const f = Number(k);
|
||||
if (Number.isFinite(f) && f >= start) {
|
||||
delete ckpt.points[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Applied signatures(floor>=start 都要重新算)
|
||||
try {
|
||||
clearStateAppliedFrom(start);
|
||||
} catch {}
|
||||
|
||||
ctx?.saveMetadataDebounced?.();
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
249
modules/variables/state2/guard.js
Normal file
249
modules/variables/state2/guard.js
Normal file
@@ -0,0 +1,249 @@
|
||||
import { getContext } from '../../../../../../extensions.js';
|
||||
|
||||
const LWB_RULES_V2_KEY = 'LWB_RULES_V2';
|
||||
|
||||
let rulesTable = {};
|
||||
|
||||
export function loadRulesFromMeta() {
|
||||
try {
|
||||
const meta = getContext()?.chatMetadata || {};
|
||||
rulesTable = meta[LWB_RULES_V2_KEY] || {};
|
||||
} catch {
|
||||
rulesTable = {};
|
||||
}
|
||||
}
|
||||
|
||||
export function saveRulesToMeta() {
|
||||
try {
|
||||
const meta = getContext()?.chatMetadata || {};
|
||||
meta[LWB_RULES_V2_KEY] = { ...rulesTable };
|
||||
getContext()?.saveMetadataDebounced?.();
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export function getRuleNode(absPath) {
|
||||
return matchRuleWithWildcard(absPath);
|
||||
}
|
||||
|
||||
export function setRule(path, rule) {
|
||||
rulesTable[path] = { ...(rulesTable[path] || {}), ...rule };
|
||||
}
|
||||
|
||||
export function clearRule(path) {
|
||||
delete rulesTable[path];
|
||||
saveRulesToMeta();
|
||||
}
|
||||
|
||||
export function clearAllRules() {
|
||||
rulesTable = {};
|
||||
saveRulesToMeta();
|
||||
}
|
||||
|
||||
export function getParentPath(absPath) {
|
||||
const parts = String(absPath).split('.').filter(Boolean);
|
||||
if (parts.length <= 1) return '';
|
||||
return parts.slice(0, -1).join('.');
|
||||
}
|
||||
|
||||
/**
|
||||
* 通配符路径匹配
|
||||
* 例如:data.同行者.张三.HP 可以匹配 data.同行者.*.HP
|
||||
*/
|
||||
function matchRuleWithWildcard(absPath) {
|
||||
// 1. 精确匹配
|
||||
if (rulesTable[absPath]) return rulesTable[absPath];
|
||||
|
||||
const segs = String(absPath).split('.').filter(Boolean);
|
||||
const n = segs.length;
|
||||
|
||||
// 2. 尝试各种 * 替换组合(从少到多)
|
||||
for (let starCount = 1; starCount <= n; starCount++) {
|
||||
const patterns = generateStarPatterns(segs, starCount);
|
||||
for (const pattern of patterns) {
|
||||
if (rulesTable[pattern]) return rulesTable[pattern];
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 尝试 [*] 匹配(数组元素模板)
|
||||
for (let i = 0; i < n; i++) {
|
||||
if (/^\d+$/.test(segs[i])) {
|
||||
const trySegs = [...segs];
|
||||
trySegs[i] = '[*]';
|
||||
const tryPath = trySegs.join('.');
|
||||
if (rulesTable[tryPath]) return rulesTable[tryPath];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成恰好有 starCount 个 * 的所有模式
|
||||
*/
|
||||
function generateStarPatterns(segs, starCount) {
|
||||
const n = segs.length;
|
||||
const results = [];
|
||||
|
||||
function backtrack(idx, stars, path) {
|
||||
if (idx === n) {
|
||||
if (stars === starCount) results.push(path.join('.'));
|
||||
return;
|
||||
}
|
||||
// 用原值
|
||||
if (n - idx > starCount - stars) {
|
||||
backtrack(idx + 1, stars, [...path, segs[idx]]);
|
||||
}
|
||||
// 用 *
|
||||
if (stars < starCount) {
|
||||
backtrack(idx + 1, stars + 1, [...path, '*']);
|
||||
}
|
||||
}
|
||||
|
||||
backtrack(0, 0, []);
|
||||
return results;
|
||||
}
|
||||
|
||||
function getValueType(v) {
|
||||
if (Array.isArray(v)) return 'array';
|
||||
if (v === null) return 'null';
|
||||
return typeof v;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证操作
|
||||
* @returns {{ allow: boolean, value?: any, reason?: string, note?: string }}
|
||||
*/
|
||||
export function validate(op, absPath, payload, currentValue) {
|
||||
const node = getRuleNode(absPath);
|
||||
const parentPath = getParentPath(absPath);
|
||||
const parentNode = parentPath ? getRuleNode(parentPath) : null;
|
||||
const isNewKey = currentValue === undefined;
|
||||
|
||||
const lastSeg = String(absPath).split('.').pop() || '';
|
||||
|
||||
// ===== 1. $schema 白名单检查 =====
|
||||
if (parentNode?.allowedKeys && Array.isArray(parentNode.allowedKeys)) {
|
||||
if (isNewKey && (op === 'set' || op === 'push')) {
|
||||
if (!parentNode.allowedKeys.includes(lastSeg)) {
|
||||
return { allow: false, reason: `字段不在结构模板中` };
|
||||
}
|
||||
}
|
||||
if (op === 'del') {
|
||||
if (parentNode.allowedKeys.includes(lastSeg)) {
|
||||
return { allow: false, reason: `模板定义的字段不能删除` };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 2. 父层结构锁定(无 objectExt / 无 allowedKeys / 无 hasWildcard) =====
|
||||
if (parentNode && parentNode.typeLock === 'object') {
|
||||
if (!parentNode.objectExt && !parentNode.allowedKeys && !parentNode.hasWildcard) {
|
||||
if (isNewKey && (op === 'set' || op === 'push')) {
|
||||
return { allow: false, reason: '父层结构已锁定,不允许新增字段' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 3. 类型锁定 =====
|
||||
if (node?.typeLock && op === 'set') {
|
||||
let finalPayload = payload;
|
||||
|
||||
// 宽松:数字字符串 => 数字
|
||||
if (node.typeLock === 'number' && typeof payload === 'string') {
|
||||
if (/^-?\d+(?:\.\d+)?$/.test(payload.trim())) {
|
||||
finalPayload = Number(payload);
|
||||
}
|
||||
}
|
||||
|
||||
const finalType = getValueType(finalPayload);
|
||||
if (node.typeLock !== finalType) {
|
||||
return { allow: false, reason: `类型不匹配,期望 ${node.typeLock},实际 ${finalType}` };
|
||||
}
|
||||
|
||||
payload = finalPayload;
|
||||
}
|
||||
|
||||
// ===== 4. 数组扩展检查 =====
|
||||
if (op === 'push') {
|
||||
if (node && node.typeLock === 'array' && !node.arrayGrow) {
|
||||
return { allow: false, reason: '数组不允许扩展' };
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 5. $ro 只读 =====
|
||||
if (node?.ro && (op === 'set' || op === 'inc')) {
|
||||
return { allow: false, reason: '只读字段' };
|
||||
}
|
||||
|
||||
// ===== 6. set 操作:数值约束 =====
|
||||
if (op === 'set') {
|
||||
const num = Number(payload);
|
||||
|
||||
// range 限制
|
||||
if (Number.isFinite(num) && (node?.min !== undefined || node?.max !== undefined)) {
|
||||
let v = num;
|
||||
const min = node?.min;
|
||||
const max = node?.max;
|
||||
|
||||
if (min !== undefined) v = Math.max(v, min);
|
||||
if (max !== undefined) v = Math.min(v, max);
|
||||
|
||||
const clamped = v !== num;
|
||||
return {
|
||||
allow: true,
|
||||
value: v,
|
||||
note: clamped ? `超出范围,已限制到 ${v}` : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// enum 枚举(不自动修正,直接拒绝)
|
||||
if (node?.enum?.length) {
|
||||
const s = String(payload ?? '');
|
||||
if (!node.enum.includes(s)) {
|
||||
return { allow: false, reason: `枚举不匹配,允许:${node.enum.join(' / ')}` };
|
||||
}
|
||||
}
|
||||
|
||||
return { allow: true, value: payload };
|
||||
}
|
||||
|
||||
// ===== 7. inc 操作:step / range 限制 =====
|
||||
if (op === 'inc') {
|
||||
const delta = Number(payload);
|
||||
if (!Number.isFinite(delta)) return { allow: false, reason: 'delta 不是数字' };
|
||||
|
||||
const cur = Number(currentValue) || 0;
|
||||
let d = delta;
|
||||
const noteParts = [];
|
||||
|
||||
// step 限制
|
||||
if (node?.step !== undefined && node.step >= 0) {
|
||||
const before = d;
|
||||
if (d > node.step) d = node.step;
|
||||
if (d < -node.step) d = -node.step;
|
||||
if (d !== before) {
|
||||
noteParts.push(`超出步长限制,已限制到 ${d >= 0 ? '+' : ''}${d}`);
|
||||
}
|
||||
}
|
||||
|
||||
let next = cur + d;
|
||||
|
||||
// range 限制
|
||||
const beforeClamp = next;
|
||||
if (node?.min !== undefined) next = Math.max(next, node.min);
|
||||
if (node?.max !== undefined) next = Math.min(next, node.max);
|
||||
if (next !== beforeClamp) {
|
||||
noteParts.push(`超出范围,已限制到 ${next}`);
|
||||
}
|
||||
|
||||
return {
|
||||
allow: true,
|
||||
value: next,
|
||||
note: noteParts.length ? noteParts.join(',') : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return { allow: true, value: payload };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
export { applyStateForMessage, clearStateAppliedFor, clearStateAppliedFrom } from './executor.js';
|
||||
export {
|
||||
applyStateForMessage,
|
||||
clearStateAppliedFor,
|
||||
clearStateAppliedFrom,
|
||||
restoreStateV2ToFloor,
|
||||
trimStateV2FromFloor,
|
||||
} from './executor.js';
|
||||
|
||||
export { parseStateBlock, extractStateBlocks, computeStateSignature, parseInlineValue } from './parser.js';
|
||||
export { generateSemantic } from './semantic.js';
|
||||
|
||||
export {
|
||||
validate,
|
||||
setRule,
|
||||
clearRule,
|
||||
clearAllRules,
|
||||
loadRulesFromMeta,
|
||||
saveRulesToMeta,
|
||||
getRuleNode,
|
||||
getParentPath,
|
||||
} from './guard.js';
|
||||
|
||||
@@ -1,15 +1,100 @@
|
||||
import jsyaml from '../../../libs/js-yaml.mjs';
|
||||
|
||||
const STATE_TAG_RE = /<\s*state\b[^>]*>([\s\S]*?)<\s*\/\s*state\s*>/gi;
|
||||
/**
|
||||
* Robust <state> block matcher (no regex)
|
||||
* - Pairs each </state> with the nearest preceding <state ...>
|
||||
* - Ignores unclosed <state>
|
||||
*/
|
||||
|
||||
function isValidOpenTagAt(s, i) {
|
||||
if (s[i] !== '<') return false;
|
||||
|
||||
const head = s.slice(i, i + 6).toLowerCase();
|
||||
if (head !== '<state') return false;
|
||||
|
||||
const next = s[i + 6] ?? '';
|
||||
if (next && !(next === '>' || next === '/' || /\s/.test(next))) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function isValidCloseTagAt(s, i) {
|
||||
if (s[i] !== '<') return false;
|
||||
if (s[i + 1] !== '/') return false;
|
||||
|
||||
const head = s.slice(i, i + 7).toLowerCase();
|
||||
if (head !== '</state') return false;
|
||||
|
||||
let j = i + 7;
|
||||
while (j < s.length && /\s/.test(s[j])) j++;
|
||||
return s[j] === '>';
|
||||
}
|
||||
|
||||
function findTagEnd(s, openIndex) {
|
||||
const end = s.indexOf('>', openIndex);
|
||||
return end === -1 ? -1 : end;
|
||||
}
|
||||
|
||||
function findStateBlockSpans(text) {
|
||||
const s = String(text ?? '');
|
||||
const closes = [];
|
||||
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
if (s[i] !== '<') continue;
|
||||
if (isValidCloseTagAt(s, i)) closes.push(i);
|
||||
}
|
||||
if (!closes.length) return [];
|
||||
|
||||
const spans = [];
|
||||
let searchEnd = s.length;
|
||||
|
||||
for (let cIdx = closes.length - 1; cIdx >= 0; cIdx--) {
|
||||
const closeStart = closes[cIdx];
|
||||
if (closeStart >= searchEnd) continue;
|
||||
|
||||
let closeEnd = closeStart + 7;
|
||||
while (closeEnd < s.length && s[closeEnd] !== '>') closeEnd++;
|
||||
if (s[closeEnd] !== '>') continue;
|
||||
closeEnd += 1;
|
||||
|
||||
let openStart = -1;
|
||||
for (let i = closeStart - 1; i >= 0; i--) {
|
||||
if (s[i] !== '<') continue;
|
||||
if (!isValidOpenTagAt(s, i)) continue;
|
||||
|
||||
const tagEnd = findTagEnd(s, i);
|
||||
if (tagEnd === -1) continue;
|
||||
if (tagEnd >= closeStart) continue;
|
||||
|
||||
openStart = i;
|
||||
break;
|
||||
}
|
||||
|
||||
if (openStart === -1) continue;
|
||||
|
||||
const openTagEnd = findTagEnd(s, openStart);
|
||||
if (openTagEnd === -1) continue;
|
||||
|
||||
spans.push({
|
||||
openStart,
|
||||
openTagEnd: openTagEnd + 1,
|
||||
closeStart,
|
||||
closeEnd,
|
||||
});
|
||||
|
||||
searchEnd = openStart;
|
||||
}
|
||||
|
||||
spans.reverse();
|
||||
return spans;
|
||||
}
|
||||
|
||||
export function extractStateBlocks(text) {
|
||||
const s = String(text ?? '');
|
||||
if (!s || s.toLowerCase().indexOf('<state') === -1) return [];
|
||||
const spans = findStateBlockSpans(s);
|
||||
const out = [];
|
||||
STATE_TAG_RE.lastIndex = 0;
|
||||
let m;
|
||||
while ((m = STATE_TAG_RE.exec(s)) !== null) {
|
||||
const inner = String(m[1] ?? '');
|
||||
for (const sp of spans) {
|
||||
const inner = s.slice(sp.openTagEnd, sp.closeStart);
|
||||
if (inner.trim()) out.push(inner);
|
||||
}
|
||||
return out;
|
||||
@@ -17,36 +102,285 @@ export function extractStateBlocks(text) {
|
||||
|
||||
export function computeStateSignature(text) {
|
||||
const s = String(text ?? '');
|
||||
if (!s || s.toLowerCase().indexOf('<state') === -1) return '';
|
||||
const chunks = [];
|
||||
STATE_TAG_RE.lastIndex = 0;
|
||||
let m;
|
||||
while ((m = STATE_TAG_RE.exec(s)) !== null) chunks.push(String(m[0] ?? '').trim());
|
||||
return chunks.length ? chunks.join('\n---\n') : '';
|
||||
const spans = findStateBlockSpans(s);
|
||||
if (!spans.length) return '';
|
||||
const chunks = spans.map(sp => s.slice(sp.openStart, sp.closeEnd).trim());
|
||||
return chunks.join('\n---\n');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parse $schema block
|
||||
*/
|
||||
function parseSchemaBlock(basePath, schemaLines) {
|
||||
const rules = [];
|
||||
|
||||
const nonEmpty = schemaLines.filter(l => l.trim());
|
||||
if (!nonEmpty.length) return rules;
|
||||
|
||||
const minIndent = Math.min(...nonEmpty.map(l => l.search(/\S/)));
|
||||
const yamlText = schemaLines
|
||||
.map(l => (l.trim() ? l.slice(minIndent) : ''))
|
||||
.join('\n');
|
||||
|
||||
let schemaObj;
|
||||
try {
|
||||
schemaObj = jsyaml.load(yamlText);
|
||||
} catch (e) {
|
||||
console.warn('[parser] $schema YAML parse failed:', e.message);
|
||||
return rules;
|
||||
}
|
||||
|
||||
if (!schemaObj || typeof schemaObj !== 'object') return rules;
|
||||
|
||||
function walk(obj, curPath) {
|
||||
if (obj === null || obj === undefined) return;
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
if (obj.length === 0) {
|
||||
rules.push({
|
||||
path: curPath,
|
||||
rule: { typeLock: 'array', arrayGrow: true },
|
||||
});
|
||||
} else {
|
||||
rules.push({
|
||||
path: curPath,
|
||||
rule: { typeLock: 'array', arrayGrow: true },
|
||||
});
|
||||
walk(obj[0], curPath ? `${curPath}.[*]` : '[*]');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof obj !== 'object') {
|
||||
const t = typeof obj;
|
||||
if (t === 'string' || t === 'number' || t === 'boolean') {
|
||||
rules.push({
|
||||
path: curPath,
|
||||
rule: { typeLock: t },
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const keys = Object.keys(obj);
|
||||
|
||||
if (keys.length === 0) {
|
||||
rules.push({
|
||||
path: curPath,
|
||||
rule: { typeLock: 'object', objectExt: true },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const hasWildcard = keys.includes('*');
|
||||
|
||||
if (hasWildcard) {
|
||||
rules.push({
|
||||
path: curPath,
|
||||
rule: { typeLock: 'object', objectExt: true, hasWildcard: true },
|
||||
});
|
||||
|
||||
const wildcardTemplate = obj['*'];
|
||||
if (wildcardTemplate !== undefined) {
|
||||
walk(wildcardTemplate, curPath ? `${curPath}.*` : '*');
|
||||
}
|
||||
|
||||
for (const k of keys) {
|
||||
if (k === '*') continue;
|
||||
const childPath = curPath ? `${curPath}.${k}` : k;
|
||||
walk(obj[k], childPath);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
rules.push({
|
||||
path: curPath,
|
||||
rule: { typeLock: 'object', allowedKeys: keys },
|
||||
});
|
||||
|
||||
for (const k of keys) {
|
||||
const childPath = curPath ? `${curPath}.${k}` : k;
|
||||
walk(obj[k], childPath);
|
||||
}
|
||||
}
|
||||
|
||||
walk(schemaObj, basePath);
|
||||
return rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 <state> 块内容 -> ops[]
|
||||
* 单行支持运算符,多行只支持覆盖 set(YAML)
|
||||
* Parse rule line ($ro, $range, $step, $enum)
|
||||
*/
|
||||
function parseRuleLine(line) {
|
||||
const tokens = line.trim().split(/\s+/);
|
||||
const directives = [];
|
||||
let pathStart = 0;
|
||||
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
if (tokens[i].startsWith('$')) {
|
||||
directives.push(tokens[i]);
|
||||
pathStart = i + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const path = tokens.slice(pathStart).join(' ').trim();
|
||||
if (!path || !directives.length) return null;
|
||||
|
||||
const rule = {};
|
||||
|
||||
for (const tok of directives) {
|
||||
if (tok === '$ro') { rule.ro = true; continue; }
|
||||
|
||||
const rangeMatch = tok.match(/^\$range=\[\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\]$/);
|
||||
if (rangeMatch) {
|
||||
rule.min = Math.min(Number(rangeMatch[1]), Number(rangeMatch[2]));
|
||||
rule.max = Math.max(Number(rangeMatch[1]), Number(rangeMatch[2]));
|
||||
continue;
|
||||
}
|
||||
|
||||
const stepMatch = tok.match(/^\$step=(\d+(?:\.\d+)?)$/);
|
||||
if (stepMatch) { rule.step = Math.abs(Number(stepMatch[1])); continue; }
|
||||
|
||||
const enumMatch = tok.match(/^\$enum=\{([^}]+)\}$/);
|
||||
if (enumMatch) {
|
||||
rule.enum = enumMatch[1].split(/[,、;]/).map(s => s.trim()).filter(Boolean);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return { path, rule };
|
||||
}
|
||||
|
||||
export function parseStateBlock(content) {
|
||||
const results = [];
|
||||
const lines = String(content ?? '').split(/\r?\n/);
|
||||
|
||||
const rules = [];
|
||||
const dataLines = [];
|
||||
|
||||
let inSchema = false;
|
||||
let schemaPath = '';
|
||||
let schemaLines = [];
|
||||
let schemaBaseIndent = -1;
|
||||
|
||||
const flushSchema = () => {
|
||||
if (schemaLines.length) {
|
||||
const parsed = parseSchemaBlock(schemaPath, schemaLines);
|
||||
rules.push(...parsed);
|
||||
}
|
||||
inSchema = false;
|
||||
schemaPath = '';
|
||||
schemaLines = [];
|
||||
schemaBaseIndent = -1;
|
||||
};
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const raw = lines[i];
|
||||
const trimmed = raw.trim();
|
||||
const indent = raw.search(/\S/);
|
||||
|
||||
if (!trimmed || trimmed.startsWith('#')) {
|
||||
if (inSchema && schemaBaseIndent >= 0) schemaLines.push(raw);
|
||||
continue;
|
||||
}
|
||||
|
||||
// $schema 开始
|
||||
if (trimmed.startsWith('$schema')) {
|
||||
flushSchema();
|
||||
const rest = trimmed.slice(7).trim();
|
||||
schemaPath = rest || '';
|
||||
inSchema = true;
|
||||
schemaBaseIndent = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inSchema) {
|
||||
if (schemaBaseIndent < 0) {
|
||||
schemaBaseIndent = indent;
|
||||
}
|
||||
|
||||
// 缩进回退 => schema 结束
|
||||
if (indent < schemaBaseIndent && indent >= 0 && trimmed) {
|
||||
flushSchema();
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
|
||||
schemaLines.push(raw);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 普通 $rule($ro, $range, $step, $enum)
|
||||
if (trimmed.startsWith('$')) {
|
||||
const parsed = parseRuleLine(trimmed);
|
||||
if (parsed) rules.push(parsed);
|
||||
continue;
|
||||
}
|
||||
|
||||
dataLines.push(raw);
|
||||
}
|
||||
|
||||
flushSchema();
|
||||
|
||||
const ops = parseDataLines(dataLines);
|
||||
return { rules, ops };
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析数据行
|
||||
*/
|
||||
function stripYamlInlineComment(s) {
|
||||
const text = String(s ?? '');
|
||||
if (!text) return '';
|
||||
let inSingle = false;
|
||||
let inDouble = false;
|
||||
let escaped = false;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const ch = text[i];
|
||||
if (inSingle) {
|
||||
if (ch === "'") {
|
||||
if (text[i + 1] === "'") { i++; continue; }
|
||||
inSingle = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (inDouble) {
|
||||
if (escaped) { escaped = false; continue; }
|
||||
if (ch === '\\') { escaped = true; continue; }
|
||||
if (ch === '"') inDouble = false;
|
||||
continue;
|
||||
}
|
||||
if (ch === "'") { inSingle = true; continue; }
|
||||
if (ch === '"') { inDouble = true; continue; }
|
||||
if (ch === '#') {
|
||||
const prev = i > 0 ? text[i - 1] : '';
|
||||
if (i === 0 || /\s/.test(prev)) {
|
||||
return text.slice(0, i).trimEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
return text.trimEnd();
|
||||
}
|
||||
|
||||
function parseDataLines(lines) {
|
||||
const results = [];
|
||||
|
||||
let pendingPath = null;
|
||||
let pendingLines = [];
|
||||
|
||||
const flushPending = () => {
|
||||
if (!pendingPath) return;
|
||||
// 没有任何缩进行:视为 set 空字符串
|
||||
|
||||
if (!pendingLines.length) {
|
||||
results.push({ path: pendingPath, op: 'set', value: '' });
|
||||
pendingPath = null;
|
||||
pendingLines = [];
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 去除公共缩进
|
||||
const nonEmpty = pendingLines.filter(l => l.trim());
|
||||
const minIndent = nonEmpty.length
|
||||
? Math.min(...nonEmpty.map(l => l.search(/\S/)))
|
||||
@@ -78,7 +412,8 @@ export function parseStateBlock(content) {
|
||||
if (colonIdx === -1) continue;
|
||||
|
||||
const path = trimmed.slice(0, colonIdx).trim();
|
||||
const rhs = trimmed.slice(colonIdx + 1).trim();
|
||||
let rhs = trimmed.slice(colonIdx + 1).trim();
|
||||
rhs = stripYamlInlineComment(rhs);
|
||||
if (!path) continue;
|
||||
|
||||
if (!rhs) {
|
||||
@@ -121,62 +456,47 @@ function unescapeString(s) {
|
||||
.replace(/\\\\/g, '\\');
|
||||
}
|
||||
|
||||
/**
|
||||
* 单行内联值解析
|
||||
*/
|
||||
export function parseInlineValue(raw) {
|
||||
const t = String(raw ?? '').trim();
|
||||
|
||||
if (t === 'null') return { op: 'del' };
|
||||
|
||||
// (负数) 用于强制 set -5,而不是 inc -5
|
||||
const parenNum = t.match(/^\((-?\d+(?:\.\d+)?)\)$/);
|
||||
if (parenNum) return { op: 'set', value: Number(parenNum[1]) };
|
||||
|
||||
// +10 / -20
|
||||
if (/^\+\d/.test(t) || /^-\d/.test(t)) {
|
||||
const n = Number(t);
|
||||
if (Number.isFinite(n)) return { op: 'inc', delta: n };
|
||||
}
|
||||
|
||||
// +"str" / +'str'
|
||||
const pushD = t.match(/^\+"((?:[^"\\]|\\.)*)"\s*$/);
|
||||
if (pushD) return { op: 'push', value: unescapeString(pushD[1]) };
|
||||
const pushS = t.match(/^\+'((?:[^'\\]|\\.)*)'\s*$/);
|
||||
if (pushS) return { op: 'push', value: unescapeString(pushS[1]) };
|
||||
|
||||
// +[...]
|
||||
if (t.startsWith('+[')) {
|
||||
try {
|
||||
const arr = JSON.parse(t.slice(1));
|
||||
if (Array.isArray(arr)) return { op: 'push', value: arr };
|
||||
return { op: 'set', value: t, warning: '+[] 不是数组,作为字符串' };
|
||||
} catch {
|
||||
return { op: 'set', value: t, warning: '+[] JSON 解析失败,作为字符串' };
|
||||
}
|
||||
} catch {}
|
||||
return { op: 'set', value: t, warning: '+[] 解析失败' };
|
||||
}
|
||||
|
||||
// -"str" / -'str'
|
||||
const popD = t.match(/^-"((?:[^"\\]|\\.)*)"\s*$/);
|
||||
if (popD) return { op: 'pop', value: unescapeString(popD[1]) };
|
||||
const popS = t.match(/^-'((?:[^'\\]|\\.)*)'\s*$/);
|
||||
if (popS) return { op: 'pop', value: unescapeString(popS[1]) };
|
||||
|
||||
// -[...]
|
||||
if (t.startsWith('-[')) {
|
||||
try {
|
||||
const arr = JSON.parse(t.slice(1));
|
||||
if (Array.isArray(arr)) return { op: 'pop', value: arr };
|
||||
return { op: 'set', value: t, warning: '-[] 不是数组,作为字符串' };
|
||||
} catch {
|
||||
return { op: 'set', value: t, warning: '-[] JSON 解析失败,作为字符串' };
|
||||
}
|
||||
} catch {}
|
||||
return { op: 'set', value: t, warning: '-[] 解析失败' };
|
||||
}
|
||||
|
||||
// 裸数字 set
|
||||
if (/^-?\d+(?:\.\d+)?$/.test(t)) return { op: 'set', value: Number(t) };
|
||||
|
||||
// "str" / 'str'
|
||||
const strD = t.match(/^"((?:[^"\\]|\\.)*)"\s*$/);
|
||||
if (strD) return { op: 'set', value: unescapeString(strD[1]) };
|
||||
const strS = t.match(/^'((?:[^'\\]|\\.)*)'\s*$/);
|
||||
@@ -185,12 +505,10 @@ export function parseInlineValue(raw) {
|
||||
if (t === 'true') return { op: 'set', value: true };
|
||||
if (t === 'false') return { op: 'set', value: false };
|
||||
|
||||
// JSON set
|
||||
if (t.startsWith('{') || t.startsWith('[')) {
|
||||
try { return { op: 'set', value: JSON.parse(t) }; }
|
||||
catch { return { op: 'set', value: t, warning: 'JSON 解析失败,作为字符串' }; }
|
||||
catch { return { op: 'set', value: t, warning: 'JSON 解析失败' }; }
|
||||
}
|
||||
|
||||
// 兜底 set 原文本
|
||||
return { op: 'set', value: t };
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ export function generateSemantic(path, op, oldValue, newValue, delta, operandVal
|
||||
if (v === undefined) return '空';
|
||||
if (v === null) return 'null';
|
||||
try {
|
||||
if (typeof v === 'string') return JSON.stringify(v);
|
||||
return JSON.stringify(v);
|
||||
} catch {
|
||||
return String(v);
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
const MODULE_ID = 'varCommands';
|
||||
const TAG_RE_XBGETVAR = /\{\{xbgetvar::([^}]+)\}\}/gi;
|
||||
const TAG_RE_XBGETVAR_YAML = /\{\{xbgetvar_yaml::([^}]+)\}\}/gi;
|
||||
const TAG_RE_XBGETVAR_YAML_IDX = /\{\{xbgetvar_yaml_idx::([^}]+)\}\}/gi;
|
||||
|
||||
let events = null;
|
||||
let initialized = false;
|
||||
@@ -183,6 +184,121 @@ export function replaceXbGetVarYamlInString(s) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 {{xbgetvar_yaml_idx::路径}} 替换为带索引注释的 YAML
|
||||
*/
|
||||
export function replaceXbGetVarYamlIdxInString(s) {
|
||||
s = String(s ?? '');
|
||||
if (!s || s.indexOf('{{xbgetvar_yaml_idx::') === -1) return s;
|
||||
|
||||
TAG_RE_XBGETVAR_YAML_IDX.lastIndex = 0;
|
||||
return s.replace(TAG_RE_XBGETVAR_YAML_IDX, (_, p) => {
|
||||
const value = lwbResolveVarPath(p);
|
||||
if (!value) return '';
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (typeof parsed === 'object' && parsed !== null) {
|
||||
return formatYamlWithIndex(parsed, 0).trim();
|
||||
}
|
||||
return value;
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function formatYamlWithIndex(obj, indent) {
|
||||
const pad = ' '.repeat(indent);
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
if (obj.length === 0) return `${pad}[]`;
|
||||
|
||||
const lines = [];
|
||||
obj.forEach((item, idx) => {
|
||||
if (item && typeof item === 'object' && !Array.isArray(item)) {
|
||||
const keys = Object.keys(item);
|
||||
if (keys.length === 0) {
|
||||
lines.push(`${pad}- {} # [${idx}]`);
|
||||
} else {
|
||||
const firstKey = keys[0];
|
||||
const firstVal = item[firstKey];
|
||||
const firstFormatted = formatValue(firstVal, indent + 2);
|
||||
|
||||
if (typeof firstVal === 'object' && firstVal !== null) {
|
||||
lines.push(`${pad}- ${firstKey}: # [${idx}]`);
|
||||
lines.push(firstFormatted);
|
||||
} else {
|
||||
lines.push(`${pad}- ${firstKey}: ${firstFormatted} # [${idx}]`);
|
||||
}
|
||||
|
||||
for (let i = 1; i < keys.length; i++) {
|
||||
const k = keys[i];
|
||||
const v = item[k];
|
||||
const vFormatted = formatValue(v, indent + 2);
|
||||
if (typeof v === 'object' && v !== null) {
|
||||
lines.push(`${pad} ${k}:`);
|
||||
lines.push(vFormatted);
|
||||
} else {
|
||||
lines.push(`${pad} ${k}: ${vFormatted}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (Array.isArray(item)) {
|
||||
lines.push(`${pad}- # [${idx}]`);
|
||||
lines.push(formatYamlWithIndex(item, indent + 1));
|
||||
} else {
|
||||
lines.push(`${pad}- ${formatScalar(item)} # [${idx}]`);
|
||||
}
|
||||
});
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
if (obj && typeof obj === 'object') {
|
||||
if (Object.keys(obj).length === 0) return `${pad}{}`;
|
||||
|
||||
const lines = [];
|
||||
for (const [key, val] of Object.entries(obj)) {
|
||||
const vFormatted = formatValue(val, indent + 1);
|
||||
if (typeof val === 'object' && val !== null) {
|
||||
lines.push(`${pad}${key}:`);
|
||||
lines.push(vFormatted);
|
||||
} else {
|
||||
lines.push(`${pad}${key}: ${vFormatted}`);
|
||||
}
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
return `${pad}${formatScalar(obj)}`;
|
||||
}
|
||||
|
||||
function formatValue(val, indent) {
|
||||
if (Array.isArray(val)) return formatYamlWithIndex(val, indent);
|
||||
if (val && typeof val === 'object') return formatYamlWithIndex(val, indent);
|
||||
return formatScalar(val);
|
||||
}
|
||||
|
||||
function formatScalar(v) {
|
||||
if (v === null) return 'null';
|
||||
if (v === undefined) return '';
|
||||
if (typeof v === 'boolean') return String(v);
|
||||
if (typeof v === 'number') return String(v);
|
||||
if (typeof v === 'string') {
|
||||
const needsQuote =
|
||||
v === '' ||
|
||||
/^\s|\s$/.test(v) || // 首尾空格
|
||||
/[:[]\]{}&*!|>'"%@`#,]/.test(v) || // YAML 易歧义字符
|
||||
/^(?:true|false|null)$/i.test(v) || // YAML 关键字
|
||||
/^-?(?:\d+(?:\.\d+)?|\.\d+)$/.test(v); // 纯数字字符串
|
||||
if (needsQuote) {
|
||||
return `"${v.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
return String(v);
|
||||
}
|
||||
|
||||
export function replaceXbGetVarInChat(chat) {
|
||||
if (!Array.isArray(chat)) return;
|
||||
|
||||
@@ -194,10 +310,12 @@ export function replaceXbGetVarInChat(chat) {
|
||||
const old = String(msg[key] ?? '');
|
||||
const hasJson = old.indexOf('{{xbgetvar::') !== -1;
|
||||
const hasYaml = old.indexOf('{{xbgetvar_yaml::') !== -1;
|
||||
if (!hasJson && !hasYaml) continue;
|
||||
const hasYamlIdx = old.indexOf('{{xbgetvar_yaml_idx::') !== -1;
|
||||
if (!hasJson && !hasYaml && !hasYamlIdx) continue;
|
||||
|
||||
let result = hasJson ? replaceXbGetVarInString(old) : old;
|
||||
result = hasYaml ? replaceXbGetVarYamlInString(result) : result;
|
||||
result = hasYamlIdx ? replaceXbGetVarYamlIdxInString(result) : result;
|
||||
msg[key] = result;
|
||||
} catch {}
|
||||
}
|
||||
@@ -215,10 +333,12 @@ export function applyXbGetVarForMessage(messageId, writeback = true) {
|
||||
const old = String(msg[key] ?? '');
|
||||
const hasJson = old.indexOf('{{xbgetvar::') !== -1;
|
||||
const hasYaml = old.indexOf('{{xbgetvar_yaml::') !== -1;
|
||||
if (!hasJson && !hasYaml) return;
|
||||
const hasYamlIdx = old.indexOf('{{xbgetvar_yaml_idx::') !== -1;
|
||||
if (!hasJson && !hasYaml && !hasYamlIdx) return;
|
||||
|
||||
let out = hasJson ? replaceXbGetVarInString(old) : old;
|
||||
out = hasYaml ? replaceXbGetVarYamlInString(out) : out;
|
||||
out = hasYamlIdx ? replaceXbGetVarYamlIdxInString(out) : out;
|
||||
if (writeback && out !== old) {
|
||||
msg[key] = out;
|
||||
}
|
||||
@@ -1111,7 +1231,9 @@ export function cleanupVarCommands() {
|
||||
|
||||
initialized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按值从数组中删除元素(2.0 pop 操作)
|
||||
*/
|
||||
export {
|
||||
MODULE_ID,
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { getContext } from "../../../../../extensions.js";
|
||||
import { getLocalVariable } from "../../../../../variables.js";
|
||||
import { createModuleEvents } from "../../core/event-manager.js";
|
||||
import { replaceXbGetVarInString, replaceXbGetVarYamlInString } from "./var-commands.js";
|
||||
import { replaceXbGetVarInString, replaceXbGetVarYamlInString, replaceXbGetVarYamlIdxInString } from "./var-commands.js";
|
||||
|
||||
const MODULE_ID = 'vareventEditor';
|
||||
const LWB_EXT_ID = 'LittleWhiteBox';
|
||||
@@ -303,6 +303,9 @@ function installWIHiddenTagStripper() {
|
||||
if (msg.content.indexOf('{{xbgetvar_yaml::') !== -1) {
|
||||
msg.content = replaceXbGetVarYamlInString(msg.content);
|
||||
}
|
||||
if (msg.content.indexOf('{{xbgetvar_yaml_idx::') !== -1) {
|
||||
msg.content = replaceXbGetVarYamlIdxInString(msg.content);
|
||||
}
|
||||
}
|
||||
if (Array.isArray(msg?.content)) {
|
||||
for (const part of msg.content) {
|
||||
@@ -321,6 +324,9 @@ function installWIHiddenTagStripper() {
|
||||
if (part.text.indexOf('{{xbgetvar_yaml::') !== -1) {
|
||||
part.text = replaceXbGetVarYamlInString(part.text);
|
||||
}
|
||||
if (part.text.indexOf('{{xbgetvar_yaml_idx::') !== -1) {
|
||||
part.text = replaceXbGetVarYamlIdxInString(part.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -339,6 +345,9 @@ function installWIHiddenTagStripper() {
|
||||
if (msg.mes.indexOf('{{xbgetvar_yaml::') !== -1) {
|
||||
msg.mes = replaceXbGetVarYamlInString(msg.mes);
|
||||
}
|
||||
if (msg.mes.indexOf('{{xbgetvar_yaml_idx::') !== -1) {
|
||||
msg.mes = replaceXbGetVarYamlIdxInString(msg.mes);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
@@ -373,6 +382,9 @@ function installWIHiddenTagStripper() {
|
||||
if (data.prompt.indexOf('{{xbgetvar_yaml::') !== -1) {
|
||||
data.prompt = replaceXbGetVarYamlInString(data.prompt);
|
||||
}
|
||||
if (data.prompt.indexOf('{{xbgetvar_yaml_idx::') !== -1) {
|
||||
data.prompt = replaceXbGetVarYamlIdxInString(data.prompt);
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
applyXbGetVarForMessage,
|
||||
parseValueForSet,
|
||||
} from "./var-commands.js";
|
||||
import { applyStateForMessage, clearStateAppliedFrom } from "./state2/index.js";
|
||||
import { applyStateForMessage } from "./state2/index.js";
|
||||
import {
|
||||
preprocessBumpAliases,
|
||||
executeQueuedVareventJsAfterTurn,
|
||||
@@ -1624,16 +1624,10 @@ function rollbackToPreviousOf(messageId) {
|
||||
const id = Number(messageId);
|
||||
if (Number.isNaN(id)) return;
|
||||
|
||||
clearStateAppliedFrom(id);
|
||||
|
||||
if (typeof globalThis.LWB_StateRollbackHook === 'function') {
|
||||
Promise.resolve(globalThis.LWB_StateRollbackHook(id)).catch((e) => {
|
||||
console.error('[variablesCore] LWB_StateRollbackHook failed:', e);
|
||||
});
|
||||
}
|
||||
const prevId = id - 1;
|
||||
if (prevId < 0) return;
|
||||
|
||||
// ???? 1.0 ???????
|
||||
const snap = getSnapshot(prevId);
|
||||
if (snap) {
|
||||
const normalized = normalizeSnapshotRecord(snap);
|
||||
@@ -1647,12 +1641,52 @@ function rollbackToPreviousOf(messageId) {
|
||||
}
|
||||
}
|
||||
|
||||
function rebuildVariablesFromScratch() {
|
||||
async function rollbackToPreviousOfAsync(messageId) {
|
||||
const id = Number(messageId);
|
||||
if (Number.isNaN(id)) return;
|
||||
|
||||
// ???????? floor>=id ? L0
|
||||
if (typeof globalThis.LWB_StateRollbackHook === 'function') {
|
||||
try {
|
||||
await globalThis.LWB_StateRollbackHook(id);
|
||||
} catch (e) {
|
||||
console.error('[variablesCore] LWB_StateRollbackHook failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
const prevId = id - 1;
|
||||
const mode = getVariablesMode();
|
||||
|
||||
if (mode === '2.0') {
|
||||
try {
|
||||
const mod = await import('./state2/index.js');
|
||||
await mod.restoreStateV2ToFloor(prevId); // prevId<0 ???
|
||||
} catch (e) {
|
||||
console.error('[variablesCore][2.0] restoreStateV2ToFloor failed:', e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// mode === '1.0'
|
||||
rollbackToPreviousOf(id);
|
||||
}
|
||||
|
||||
|
||||
async function rebuildVariablesFromScratch() {
|
||||
try {
|
||||
const mode = getVariablesMode();
|
||||
if (mode === '2.0') {
|
||||
const mod = await import('./state2/index.js');
|
||||
const chat = getContext()?.chat || [];
|
||||
const lastId = chat.length ? chat.length - 1 : -1;
|
||||
await mod.restoreStateV2ToFloor(lastId);
|
||||
return;
|
||||
}
|
||||
// 1.0 旧逻辑
|
||||
setVarDict({});
|
||||
const chat = getContext()?.chat || [];
|
||||
for (let i = 0; i < chat.length; i++) {
|
||||
applyVariablesForMessage(i);
|
||||
await applyVariablesForMessage(i);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
@@ -1842,7 +1876,7 @@ async function applyVariablesForMessage(messageId) {
|
||||
} catch (e) {
|
||||
parseErrors++;
|
||||
if (debugOn) {
|
||||
try { xbLog.error(MODULE_ID, `plot-log è§£æž<EFBFBD>失败:楼å±?${messageId} å<EFBFBD>?${idx + 1} 预览=${preview(b)}`, e); } catch {}
|
||||
try { xbLog.error(MODULE_ID, `plot-log 解析失败:楼<EFBFBD>?${messageId} <20>?${idx + 1} 预览=${preview(b)}`, e); } catch {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1873,7 +1907,7 @@ async function applyVariablesForMessage(messageId) {
|
||||
try {
|
||||
xbLog.warn(
|
||||
MODULE_ID,
|
||||
`plot-log 未产生å<EFBFBD>¯æ‰§è¡ŒæŒ‡ä»¤ï¼šæ¥¼å±?${messageId} å<EFBFBD>—æ•°=${blocks.length} è§£æž<EFBFBD>æ<EFBFBD>¡ç›®=${parsedPartsTotal} è§£æž<EFBFBD>失败=${parseErrors} 预览=${preview(blocks[0])}`
|
||||
`plot-log 未产生可执行指令:楼<EFBFBD>?${messageId} 块数=${blocks.length} 解析条目=${parsedPartsTotal} 解析失败=${parseErrors} 预览=${preview(blocks[0])}`
|
||||
);
|
||||
} catch {}
|
||||
}
|
||||
@@ -2149,7 +2183,7 @@ async function applyVariablesForMessage(messageId) {
|
||||
const denied = guardDenied ? `,被规则拦截=${guardDenied}` : '';
|
||||
xbLog.warn(
|
||||
MODULE_ID,
|
||||
`plot-log 指令执行å<EFBFBD>Žæ— å<EFBFBD>˜åŒ–:楼å±?${messageId} 指令æ•?${ops.length}${denied} 示例=${preview(JSON.stringify(guardDeniedSamples))}`
|
||||
`plot-log 指令执行后无变化:楼<EFBFBD>?${messageId} 指令<EFBFBD>?${ops.length}${denied} 示例=${preview(JSON.stringify(guardDeniedSamples))}`
|
||||
);
|
||||
} catch {}
|
||||
}
|
||||
@@ -2218,7 +2252,7 @@ function bindEvents() {
|
||||
|
||||
events?.on(event_types.MESSAGE_SENT, async () => {
|
||||
try {
|
||||
snapshotCurrentLastFloor();
|
||||
if (getVariablesMode() !== '2.0') snapshotCurrentLastFloor();
|
||||
const chat = getContext()?.chat || [];
|
||||
const id = chat.length ? chat.length - 1 : undefined;
|
||||
if (typeof id === 'number') {
|
||||
@@ -2247,7 +2281,7 @@ function bindEvents() {
|
||||
if (typeof id === 'number') {
|
||||
await applyVarsForMessage(id);
|
||||
applyXbGetVarForMessage(id, true);
|
||||
snapshotForMessageId(id);
|
||||
if (getVariablesMode() !== '2.0') snapshotForMessageId(id);
|
||||
}
|
||||
} catch {}
|
||||
});
|
||||
@@ -2259,7 +2293,7 @@ function bindEvents() {
|
||||
if (typeof id === 'number') {
|
||||
await applyVarsForMessage(id);
|
||||
applyXbGetVarForMessage(id, true);
|
||||
snapshotForMessageId(id);
|
||||
if (getVariablesMode() !== '2.0') snapshotForMessageId(id);
|
||||
}
|
||||
} catch {}
|
||||
});
|
||||
@@ -2283,33 +2317,35 @@ function bindEvents() {
|
||||
events?.on(event_types.MESSAGE_EDITED, async (data) => {
|
||||
try {
|
||||
const id = getMsgIdLoose(data);
|
||||
if (typeof id === 'number') {
|
||||
clearAppliedFor(id);
|
||||
rollbackToPreviousOf(id);
|
||||
if (typeof id !== 'number') return;
|
||||
|
||||
setTimeout(async () => {
|
||||
await applyVarsForMessage(id);
|
||||
applyXbGetVarForMessage(id, true);
|
||||
if (getVariablesMode() !== '2.0') clearAppliedFor(id);
|
||||
|
||||
try {
|
||||
const ctx = getContext();
|
||||
const msg = ctx?.chat?.[id];
|
||||
if (msg) updateMessageBlock(id, msg, { rerenderMessage: true });
|
||||
} catch {}
|
||||
// ? ?? await????? apply ????????????
|
||||
await rollbackToPreviousOfAsync(id);
|
||||
|
||||
try {
|
||||
const ctx = getContext();
|
||||
const es = ctx?.eventSource;
|
||||
const et = ctx?.event_types;
|
||||
if (es?.emit && et?.MESSAGE_UPDATED) {
|
||||
suppressUpdatedOnce.add(id);
|
||||
await es.emit(et.MESSAGE_UPDATED, id);
|
||||
}
|
||||
} catch {}
|
||||
setTimeout(async () => {
|
||||
await applyVarsForMessage(id);
|
||||
applyXbGetVarForMessage(id, true);
|
||||
|
||||
await executeQueuedVareventJsAfterTurn();
|
||||
}, 10);
|
||||
}
|
||||
try {
|
||||
const ctx = getContext();
|
||||
const msg = ctx?.chat?.[id];
|
||||
if (msg) updateMessageBlock(id, msg, { rerenderMessage: true });
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
const ctx = getContext();
|
||||
const es = ctx?.eventSource;
|
||||
const et = ctx?.event_types;
|
||||
if (es?.emit && et?.MESSAGE_UPDATED) {
|
||||
suppressUpdatedOnce.add(id);
|
||||
await es.emit(et.MESSAGE_UPDATED, id);
|
||||
}
|
||||
} catch {}
|
||||
|
||||
await executeQueuedVareventJsAfterTurn();
|
||||
}, 10);
|
||||
} catch {}
|
||||
});
|
||||
|
||||
@@ -2317,28 +2353,44 @@ function bindEvents() {
|
||||
events?.on(event_types.MESSAGE_SWIPED, async (data) => {
|
||||
try {
|
||||
const id = getMsgIdLoose(data);
|
||||
if (typeof id === 'number') {
|
||||
lastSwipedId = id;
|
||||
clearAppliedFor(id);
|
||||
rollbackToPreviousOf(id);
|
||||
if (typeof id !== 'number') return;
|
||||
|
||||
const tId = setTimeout(async () => {
|
||||
pendingSwipeApply.delete(id);
|
||||
await applyVarsForMessage(id);
|
||||
await executeQueuedVareventJsAfterTurn();
|
||||
}, 10);
|
||||
lastSwipedId = id;
|
||||
if (getVariablesMode() !== '2.0') clearAppliedFor(id);
|
||||
|
||||
pendingSwipeApply.set(id, tId);
|
||||
}
|
||||
// ? ?? await???????????????
|
||||
await rollbackToPreviousOfAsync(id);
|
||||
|
||||
const tId = setTimeout(async () => {
|
||||
pendingSwipeApply.delete(id);
|
||||
await applyVarsForMessage(id);
|
||||
await executeQueuedVareventJsAfterTurn();
|
||||
}, 10);
|
||||
|
||||
pendingSwipeApply.set(id, tId);
|
||||
} catch {}
|
||||
});
|
||||
|
||||
// message deleted
|
||||
events?.on(event_types.MESSAGE_DELETED, (data) => {
|
||||
events?.on(event_types.MESSAGE_DELETED, async (data) => {
|
||||
try {
|
||||
const id = getMsgIdStrict(data);
|
||||
if (typeof id === 'number') {
|
||||
rollbackToPreviousOf(id);
|
||||
if (typeof id !== 'number') return;
|
||||
|
||||
// ? ????????await ???????
|
||||
await rollbackToPreviousOfAsync(id);
|
||||
|
||||
// ✅ 2.0:物理删除消息 => 同步清理 WAL/ckpt,避免膨胀
|
||||
if (getVariablesMode() === '2.0') {
|
||||
try {
|
||||
const mod = await import('./state2/index.js');
|
||||
await mod.trimStateV2FromFloor(id);
|
||||
} catch (e) {
|
||||
console.error('[variablesCore][2.0] trimStateV2FromFloor failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
if (getVariablesMode() !== '2.0') {
|
||||
clearSnapshotsFrom(id);
|
||||
clearAppliedFrom(id);
|
||||
}
|
||||
@@ -2349,7 +2401,7 @@ function bindEvents() {
|
||||
|
||||
events?.on(event_types.GENERATION_STARTED, (data) => {
|
||||
try {
|
||||
snapshotPreviousFloor();
|
||||
if (getVariablesMode() !== '2.0') snapshotPreviousFloor();
|
||||
|
||||
// cancel swipe delay
|
||||
const t = (typeof data === 'string' ? data : (data?.type || '')).toLowerCase();
|
||||
@@ -2364,7 +2416,7 @@ function bindEvents() {
|
||||
});
|
||||
|
||||
// chat changed
|
||||
events?.on(event_types.CHAT_CHANGED, () => {
|
||||
events?.on(event_types.CHAT_CHANGED, async () => {
|
||||
try {
|
||||
rulesClearCache();
|
||||
rulesLoadFromMeta();
|
||||
@@ -2372,6 +2424,13 @@ function bindEvents() {
|
||||
const meta = getContext()?.chatMetadata || {};
|
||||
meta[LWB_PLOT_APPLIED_KEY] = {};
|
||||
getContext()?.saveMetadataDebounced?.();
|
||||
|
||||
if (getVariablesMode() === '2.0') {
|
||||
try {
|
||||
const mod = await import('./state2/index.js');
|
||||
mod.clearStateAppliedFrom(0);
|
||||
} catch {}
|
||||
}
|
||||
} catch {}
|
||||
});
|
||||
}
|
||||
@@ -2408,6 +2467,33 @@ export function initVariablesCore() {
|
||||
applyDeltaTable: applyRulesDeltaToTable,
|
||||
save: rulesSaveToMeta,
|
||||
};
|
||||
|
||||
globalThis.LWB_StateV2 = {
|
||||
/**
|
||||
* @param {string} text - 包含 <state>...</state> 的文本
|
||||
* @param {{ floor?: number, silent?: boolean }} [options]
|
||||
* - floor: 指定写入/记录用楼层(默认:最后一楼)
|
||||
* - silent: true 时不触发 stateAtomsGenerated(初始化用)
|
||||
*/
|
||||
applyText: async (text, options = {}) => {
|
||||
const { applyStateForMessage } = await import('./state2/index.js');
|
||||
const ctx = getContext();
|
||||
const floor =
|
||||
Number.isFinite(options.floor)
|
||||
? Number(options.floor)
|
||||
: Math.max(0, (ctx?.chat?.length || 1) - 1);
|
||||
const result = applyStateForMessage(floor, String(text || ''));
|
||||
// ✅ 默认会触发(当作事件)
|
||||
// ✅ 初始化时 silent=true,不触发(当作基线写入)
|
||||
if (!options.silent && result?.atoms?.length) {
|
||||
$(document).trigger('xiaobaix:variables:stateAtomsGenerated', {
|
||||
messageId: floor,
|
||||
atoms: result.atoms,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2429,6 +2515,7 @@ export function cleanupVariablesCore() {
|
||||
|
||||
// clear global hooks
|
||||
delete globalThis.LWB_Guard;
|
||||
delete globalThis.LWB_StateV2;
|
||||
|
||||
// clear guard state
|
||||
guardBypass(false);
|
||||
@@ -2454,4 +2541,4 @@ export {
|
||||
rulesSetTable,
|
||||
rulesLoadFromMeta,
|
||||
rulesSaveToMeta,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -117,38 +117,64 @@ const VT = {
|
||||
global: { getter: getGlobalVariable, setter: setGlobalVariable, storage: ()=> extension_settings.variables?.global || ((extension_settings.variables = { global: {} }).global), save: saveSettingsDebounced },
|
||||
};
|
||||
|
||||
const LWB_RULES_KEY='LWB_RULES';
|
||||
const getRulesTable = () => { try { return getContext()?.chatMetadata?.[LWB_RULES_KEY] || {}; } catch { return {}; } };
|
||||
const EXT_ID = 'LittleWhiteBox';
|
||||
const LWB_RULES_V1_KEY = 'LWB_RULES';
|
||||
const LWB_RULES_V2_KEY = 'LWB_RULES_V2';
|
||||
|
||||
const getRulesTable = () => {
|
||||
try {
|
||||
const ctx = getContext();
|
||||
const mode = extension_settings?.[EXT_ID]?.variablesMode || '1.0';
|
||||
const meta = ctx?.chatMetadata || {};
|
||||
return mode === '2.0'
|
||||
? (meta[LWB_RULES_V2_KEY] || {})
|
||||
: (meta[LWB_RULES_V1_KEY] || {});
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const pathKey = (arr)=>{ try { return (arr||[]).map(String).join('.'); } catch { return ''; } };
|
||||
const getRuleNodeByPath = (arr)=> (pathKey(arr) ? (getRulesTable()||{})[pathKey(arr)] : undefined);
|
||||
const hasAnyRule = (n)=>{
|
||||
if(!n) return false;
|
||||
if(n.ro) return true;
|
||||
if(n.objectPolicy && n.objectPolicy!=='none') return true;
|
||||
if(n.arrayPolicy && n.arrayPolicy!=='lock') return true;
|
||||
const c=n.constraints||{};
|
||||
return ('min'in c)||('max'in c)||('step'in c)||(Array.isArray(c.enum)&&c.enum.length)||(c.regex&&c.regex.source);
|
||||
const hasAnyRule = (n) => {
|
||||
if (!n) return false;
|
||||
if (n.ro) return true;
|
||||
if (n.lock) return true;
|
||||
if (n.min !== undefined || n.max !== undefined) return true;
|
||||
if (n.step !== undefined) return true;
|
||||
if (Array.isArray(n.enum) && n.enum.length) return true;
|
||||
return false;
|
||||
};
|
||||
const ruleTip = (n)=>{
|
||||
if(!n) return '';
|
||||
const lines=[], c=n.constraints||{};
|
||||
if(n.ro) lines.push('只读:$ro');
|
||||
if(n.objectPolicy){ const m={none:'(默认:不可增删键)',ext:'$ext(可增键)',prune:'$prune(可删键)',free:'$free(可增删键)'}; lines.push(`对象策略:${m[n.objectPolicy]||n.objectPolicy}`); }
|
||||
if(n.arrayPolicy){ const m={lock:'(默认:不可增删项)',grow:'$grow(可增项)',shrink:'$shrink(可删项)',list:'$list(可增删项)'}; lines.push(`数组策略:${m[n.arrayPolicy]||n.arrayPolicy}`); }
|
||||
if('min'in c||'max'in c){ if('min'in c&&'max'in c) lines.push(`范围:$range=[${c.min},${c.max}]`); else if('min'in c) lines.push(`下限:$min=${c.min}`); else lines.push(`上限:$max=${c.max}`); }
|
||||
if('step'in c) lines.push(`步长:$step=${c.step}`);
|
||||
if(Array.isArray(c.enum)&&c.enum.length) lines.push(`枚举:$enum={${c.enum.join(';')}}`);
|
||||
if(c.regex&&c.regex.source) lines.push(`正则:$match=/${c.regex.source}/${c.regex.flags||''}`);
|
||||
|
||||
const ruleTip = (n) => {
|
||||
if (!n) return '';
|
||||
const lines = [];
|
||||
if (n.ro) lines.push('只读:$ro');
|
||||
if (n.lock) lines.push('结构锁:$lock(禁止增删该层 key/项)');
|
||||
|
||||
if (n.min !== undefined || n.max !== undefined) {
|
||||
const a = n.min !== undefined ? n.min : '-∞';
|
||||
const b = n.max !== undefined ? n.max : '+∞';
|
||||
lines.push(`范围:$range=[${a},${b}]`);
|
||||
}
|
||||
if (n.step !== undefined) lines.push(`步长:$step=${n.step}`);
|
||||
if (Array.isArray(n.enum) && n.enum.length) lines.push(`枚举:$enum={${n.enum.join(';')}}`);
|
||||
return lines.join('\n');
|
||||
};
|
||||
const badgesHtml = (n)=>{
|
||||
if(!hasAnyRule(n)) return '';
|
||||
const tip=ruleTip(n).replace(/"/g,'"'), out=[];
|
||||
if(n.ro) out.push(`<span class="vm-badge" data-type="ro" title="${tip}"><i class="fa-solid fa-shield-halved"></i></span>`);
|
||||
if((n.objectPolicy&&n.objectPolicy!=='none')||(n.arrayPolicy&&n.arrayPolicy!=='lock')) out.push(`<span class="vm-badge" data-type="struct" title="${tip}"><i class="fa-solid fa-diagram-project"></i></span>`);
|
||||
const c=n.constraints||{}; if(('min'in c)||('max'in c)||('step'in c)||(Array.isArray(c.enum)&&c.enum.length)||(c.regex&&c.regex.source)) out.push(`<span class="vm-badge" data-type="cons" title="${tip}"><i class="fa-solid fa-ruler-vertical"></i></span>`);
|
||||
return out.length?`<span class="vm-badges">${out.join('')}</span>`:'';
|
||||
|
||||
const badgesHtml = (n) => {
|
||||
if (!hasAnyRule(n)) return '';
|
||||
const tip = ruleTip(n).replace(/"/g,'"');
|
||||
|
||||
const out = [];
|
||||
if (n.ro) out.push(`<span class="vm-badge" data-type="ro" title="${tip}"><i class="fa-solid fa-shield-halved"></i></span>`);
|
||||
if (n.lock) out.push(`<span class="vm-badge" data-type="struct" title="${tip}"><i class="fa-solid fa-diagram-project"></i></span>`);
|
||||
if ((n.min !== undefined || n.max !== undefined) || (n.step !== undefined) || (Array.isArray(n.enum) && n.enum.length)) {
|
||||
out.push(`<span class="vm-badge" data-type="cons" title="${tip}"><i class="fa-solid fa-ruler-vertical"></i></span>`);
|
||||
}
|
||||
return out.length ? `<span class="vm-badges">${out.join('')}</span>` : '';
|
||||
};
|
||||
|
||||
const debounce=(fn,ms=200)=>{let t;return(...a)=>{clearTimeout(t);t=setTimeout(()=>fn(...a),ms);}};
|
||||
|
||||
class VariablesPanel {
|
||||
|
||||
Reference in New Issue
Block a user