From c8f18fa4479e87e74d84229c0629f065e41f2dfb Mon Sep 17 00:00:00 2001 From: Serge Alex Date: Sun, 1 Nov 2026 01:44:57 +0300 Subject: [PATCH] feat-chat-logger-main-world-stage1 --- .gitignore | 1 + bridge/server.js | 28 +++++++- content/chat-logger.js | 43 ++++++++++++ injected/chat-logger.js | 183 ++++++++++++++++++++++++++++++++++++++++++++++++ manifest.json | 15 +++- memory/state.json | 2 +- 6 files changed, 268 insertions(+), 4 deletions(-) create mode 100644 content/chat-logger.js create mode 100644 injected/chat-logger.js diff --git a/.gitignore b/.gitignore index a69ecd6..fc76292 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules/ .dsh-pro/ .vscode/ *.log +memory/chat-log/ diff --git a/bridge/server.js b/bridge/server.js index 4ec20df..d13d3b1 100644 --- a/bridge/server.js +++ b/bridge/server.js @@ -225,7 +225,33 @@ async function handle(req, res, pathname, body) { } throw new ToolError('E_ARGS_INVALID', 'GET or PUT only'); } - default: + case '/log/append': { + if (req.method !== 'POST') throw new ToolError('E_ARGS_INVALID', 'only POST'); + const file = body && body.file; + const lines = body && body.lines; + if (typeof file !== 'string') throw new ToolError('E_ARGS_INVALID', 'file required'); + if (file.length < 7 || file.length > 44) throw new ToolError('E_ARGS_INVALID', 'file length'); + if (file.slice(-6) !== '.jsonl') throw new ToolError('E_ARGS_INVALID', 'file ext'); + if (file.indexOf('/') !== -1) throw new ToolError('E_ARGS_INVALID', 'file slash'); + if (file.indexOf(String.fromCharCode(92)) !== -1) throw new ToolError('E_ARGS_INVALID', 'file backslash'); + if (!Array.isArray(lines) || lines.length === 0) throw new ToolError('E_ARGS_INVALID', 'lines required'); + if (lines.length > 200) throw new ToolError('E_ARGS_INVALID', 'too many lines'); + const logDir = path.join(resolveMemoryDir(config, BRIDGE_DIR), 'chat-log'); + fs.mkdirSync(logDir, { recursive: true }); + const target = path.join(logDir, file); + const resolved = path.resolve(target); + const rootResolved = path.resolve(logDir) + path.sep; + if (!resolved.startsWith(rootResolved)) throw new ToolError('E_PATH_ESCAPE', 'escape'); + const buf = lines.map(function (l) { + const s = typeof l === 'string' ? l : JSON.stringify(l); + return s + String.fromCharCode(10); + }).join(''); + const bytes = Buffer.byteLength(buf, 'utf8'); + if (bytes > 524288) throw new ToolError('E_ARGS_INVALID', 'payload too big'); + fs.appendFileSync(resolved, buf, 'utf8'); + return { ok: true, data: { file: file, lines: lines.length, bytes: bytes } }; + } + default: // /handoff/:runId 与 /handoff/:runId/cancel const m = pathname.match(/^\/handoff\/([A-Za-z0-9-]+)(\/cancel)?$/); if (m) { diff --git a/content/chat-logger.js b/content/chat-logger.js new file mode 100644 index 0000000..8c7c1c9 --- /dev/null +++ b/content/chat-logger.js @@ -0,0 +1,43 @@ +'use strict'; +// ISOLATED world: инжект MAIN-world chat-logger и передача конфига через CustomEvent +(function () { + var TOKEN_KEY = 'dshmem-bridge-token' + var ENABLED_KEY = 'dshmem-log-enabled' + var HOOK_ID = 'dshmem-hook' + + function pushConfig() { + try { + chrome.storage.local.get([TOKEN_KEY, ENABLED_KEY], function (v) { + var detail = { + token: v && v[TOKEN_KEY] ? String(v[TOKEN_KEY]) : '', + enabled: !(v && v[ENABLED_KEY] === false) + } + try { + window.dispatchEvent(new CustomEvent('dshmem:log-config', { detail: JSON.stringify(detail) })) + } catch (e) { } + }) + } catch (e) { } + } + + function injectHook() { + if (document.getElementById(HOOK_ID)) return + var script = document.createElement('script') + script.id = HOOK_ID + script.src = chrome.runtime.getURL('injected/chat-logger.js') + script.async = false + script.onload = function () { script.remove() } + ;(document.head || document.documentElement).appendChild(script) + } + + window.addEventListener('dshmem:log-request', pushConfig) + + try { + chrome.storage.onChanged.addListener(function (changes, area) { + if (area !== 'local') return + if (changes[TOKEN_KEY] || changes[ENABLED_KEY]) pushConfig() + }) + } catch (e) { } + + injectHook() + console.log('[chat-log] injected') +})() diff --git a/injected/chat-logger.js b/injected/chat-logger.js new file mode 100644 index 0000000..5f7b980 --- /dev/null +++ b/injected/chat-logger.js @@ -0,0 +1,183 @@ +'use strict'; +(function () { + var TOKEN_KEY = 'dshmem-bridge-token' + var ENABLED_KEY = 'dshmem-log-enabled' + var BASE = 'http' + ':' + '//' + '127.0.0.1' + ':' + '8766' + var CHUNK = 700 + var seen = Object.create(null) + var cfg = null + var SEP = String.fromCharCode(10) + String.fromCharCode(10) + + function hash(s) { + var h1 = 5381 + var h2 = 52711 + for (var i = 0; i !== s.length; i++) { + var c = s.charCodeAt(i) + h1 = (((h1 * 33) ^ c) >>> 0) + h2 = (((h2 * 31) ^ c) >>> 0) + } + return h1.toString(36) + h2.toString(36) + } + + function today() { + var d = new Date() + var m = String(d.getMonth() + 1) + var dd = String(d.getDate()) + if (m.length === 1) m = '0' + m + if (dd.length === 1) dd = '0' + dd + return d.getFullYear() + '-' + m + '-' + dd + } + + function chunk(text) { + var out = [] + var n = Math.ceil(text.length / CHUNK) || 1 + for (var i = 0; i !== n; i++) out.push(text.slice(i * CHUNK, (i + 1) * CHUNK)) + return out + } + + function isInj(text) { + if (text.indexOf('MEMORY_SYSTEM') !== -1) return true + if (text.indexOf('DSM:memory_calls') !== -1) return true + if (text.indexOf('DSM:memory_write') !== -1) return true + if (text.indexOf('dsmemory') !== -1) return true + return false + } + + function linesFor(mid, role, text, ts) { + var cs = chunk(text) + var arr = [] + for (var i = 0; i !== cs.length; i++) { + var obj = { + mid: mid, + part: i, + total: cs.length, + role: role, + ts: ts, + inj: isInj(text), + text: cs[i] + } + arr.push(JSON.stringify(obj)) + } + return arr + } + + function requestConfig() { + return new Promise(function (resolve) { + var done = false + function onCfg(e) { + if (done) return + done = true + try { + cfg = JSON.parse(e.detail) + } catch (err) { + cfg = null + } + window.removeEventListener('dshmem:log-config', onCfg) + resolve(cfg) + } + window.addEventListener('dshmem:log-config', onCfg) + try { + window.dispatchEvent(new CustomEvent('dshmem:log-request')) + } catch (err) { + done = true + resolve(null) + } + setTimeout(function () { + if (done) return + done = true + window.removeEventListener('dshmem:log-config', onCfg) + resolve(null) + }, 2000) + }) + } + + function sendLines(file, lines) { + if (!cfg || !cfg.token) return Promise.resolve(false) + var headers = { 'Content-Type': 'application/json' } + headers.Authorization = 'Bearer ' + cfg.token + return fetch(BASE + '/log/append', { + method: 'POST', + headers: headers, + body: JSON.stringify({ file: file, lines: lines }) + }).then(function (r) { return r.ok }).catch(function () { return false }) + } + + function extractAssistant(sse) { + if (!sse) return '' + var out = [] + var blocks = sse.split(SEP) + for (var i = 0; i !== blocks.length; i++) { + var block = blocks[i] + var idx = block.indexOf('data: ') + if (idx === -1) continue + var json = block.slice(idx + 6).trim() + if (!json) continue + var o = null + try { o = JSON.parse(json) } catch (e) { continue } + if (!o || typeof o !== 'object') continue + if (o.p) continue + if (typeof o.v !== 'string') continue + if (Object.keys(o).length !== 1) continue + out.push(o.v) + } + return out.join('') + } + + function process(entry) { + if (!entry || !entry.url) return + if (entry.url.indexOf('chat/completion') === -1) return + if (!cfg) { + requestConfig().then(function () { process(entry) }) + return + } + if (!cfg.enabled) return + var messages = [] + try { + var req = JSON.parse(entry.body) + if (req && Array.isArray(req.messages)) messages = req.messages + } catch (e) { return } + var assistant = '' + try { assistant = extractAssistant(entry.resp || '') } catch (e) { } + var all = messages.slice() + if (assistant) all.push({ role: 'assistant', content: assistant }) + var file = today() + '.jsonl' + var lines = [] + for (var i = 0; i !== all.length; i++) { + var msg = all[i] + if (!msg || typeof msg.content !== 'string') continue + var text = msg.content + if (!text) continue + var role = msg.role || 'user' + var h = hash(role + ':' + text) + if (seen[h]) continue + seen[h] = true + var ls = linesFor(role + '-' + h, role, text, Date.now()) + for (var j = 0; j !== ls.length; j++) lines.push(ls[j]) + } + if (!lines.length) return + sendLines(file, lines).then(function (ok) { + if (ok) console.log('[chat-log] saved', lines.length) + }) + } + + var origOpen = XMLHttpRequest.prototype.open + var origSend = XMLHttpRequest.prototype.send + XMLHttpRequest.prototype.open = function (m, u) { + this.__chatUrl = String(u || '') + return origOpen.apply(this, arguments) + } + XMLHttpRequest.prototype.send = function (body) { + var self = this + var bodyStr = typeof body === 'string' ? body : '' + this.addEventListener('load', function () { + try { + process({ url: self.__chatUrl, body: bodyStr, resp: self.responseText || '' }) + } catch (e) { + console.warn('[chat-log] error', String(e)) + } + }) + return origSend.apply(this, arguments) + } + + requestConfig() +})() diff --git a/manifest.json b/manifest.json index 8f2277e..6ae69af 100644 --- a/manifest.json +++ b/manifest.json @@ -24,13 +24,24 @@ "content/selectors.js", "content/parser.js", "content/storage-bridge.js", - "content/bridge-client.js", + "content/bridge-client.js", "content/panel.js", - "content/content.js" + "content/content.js", + "content/chat-logger.js" ], "css": [ "content/overlay.css" ] } + ], + "web_accessible_resources": [ + { + "resources": [ + "injected/chat-logger.js" + ], + "matches": [ + "https://chat.deepseek.com/*" + ] + } ] } diff --git a/memory/state.json b/memory/state.json index 46d879c..6fd6d58 100644 --- a/memory/state.json +++ b/memory/state.json @@ -1,6 +1,6 @@ { "version": 1, - "updatedAt": "2026-10-31T03:11:08.923Z", + "updatedAt": "2026-10-31T22:42:50.200Z", "memories": {}, "skills": [] } \ No newline at end of file