Compare commits
No commits in common. "205c2561feefa9a8a54c86b4e90ecbd520e512e6" and "ca94e0328978d031314e2271432bfa1e0baf5489" have entirely different histories.
205c2561fe
...
ca94e03289
File diff suppressed because it is too large
Load Diff
|
|
@ -1,112 +0,0 @@
|
|||
// ==COMMAND EXECUTOR START==
|
||||
/* global GM_xmlhttpRequest */
|
||||
(function () {
|
||||
class CommandExecutor {
|
||||
static async execute(command, sourceElement, label = '') {
|
||||
const log = window.AI_REPO_LOGGER;
|
||||
const cfg = window.AI_REPO_CONFIG;
|
||||
|
||||
try {
|
||||
if (['update_file', 'create_file'].includes(command.action) && !command.commit_message) {
|
||||
command.commit_message = `AI Repo Commander: ${command.path} (${new Date().toISOString()})`;
|
||||
}
|
||||
|
||||
if (!cfg.get('api.enabled')) {
|
||||
log.info('Mock executing', { action: command.action, label });
|
||||
await this.delay(300);
|
||||
return this._success({ status: 200, responseText: JSON.stringify({ success: true, message: 'Mock execution completed' }) }, command, sourceElement, true, label);
|
||||
}
|
||||
|
||||
log.info('Executing via API', { action: command.action, label });
|
||||
const res = await this._api(command);
|
||||
return this._success(res, command, sourceElement, false, label);
|
||||
|
||||
} catch (err) {
|
||||
window.AI_REPO_LOGGER.error('Execution failed', { action: command.action, error: err.message });
|
||||
return this._error(err, command, sourceElement, label);
|
||||
}
|
||||
}
|
||||
|
||||
static _api(command, attempt = 0) {
|
||||
const cfg = window.AI_REPO_CONFIG;
|
||||
const maxRetries = cfg.get('api.maxRetries') ?? 2;
|
||||
const timeout = cfg.get('api.timeout') ?? 60000;
|
||||
const bridgeKey = this._getBridgeKey();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
GM_xmlhttpRequest({
|
||||
method: 'POST',
|
||||
url: command.url,
|
||||
headers: { 'X-Bridge-Key': bridgeKey, 'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(command),
|
||||
timeout,
|
||||
onload: (r) => (r.status >= 200 && r.status < 300) ? resolve(r) : reject(new Error(`API Error ${r.status}: ${r.statusText}`)),
|
||||
onerror: (e) => {
|
||||
if (attempt < maxRetries) {
|
||||
setTimeout(() => this._api(command, attempt + 1).then(resolve).catch(reject), 1000 * (attempt + 1));
|
||||
} else reject(new Error(`Network error after ${attempt + 1} attempts: ${e?.error || 'unknown'}`));
|
||||
},
|
||||
ontimeout: () => reject(new Error(`API timeout after ${timeout}ms`))
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
static _getBridgeKey() {
|
||||
const cfg = window.AI_REPO_CONFIG;
|
||||
let key = cfg.get('api.bridgeKey');
|
||||
if (!key) {
|
||||
key = prompt('[AI Repo Commander] Enter your bridge key for this session:') || '';
|
||||
if (!key) throw new Error('Bridge key required when API is enabled');
|
||||
if (confirm('Save this bridge key to avoid future prompts?')) cfg.set('api.bridgeKey', key);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
static _success(response, command, el, isMock = false, label = '') {
|
||||
let data; try { data = JSON.parse(response.responseText || '{}'); } catch { data = { message: 'Operation completed' }; }
|
||||
this._status(el, isMock ? 'MOCK' : 'SUCCESS', { action: command.action, details: data.message || 'Completed successfully', label });
|
||||
|
||||
if (command.action === 'get_file') this._handleGetFile(data, label);
|
||||
if (command.action === 'list_files') this._handleListFiles(data, label);
|
||||
|
||||
return { success: true, data, isMock };
|
||||
}
|
||||
|
||||
static _error(error, command, el, label = '') {
|
||||
this._status(el, 'ERROR', { action: command.action, details: error.message, label });
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
static _status(el, type, data) {
|
||||
const div = document.createElement('div');
|
||||
div.style.cssText = `
|
||||
padding:8px 12px;margin:8px 0;border-radius:4px;
|
||||
border-left:4px solid ${this._color(type)};
|
||||
background:rgba(255,255,255,.05);font-family:monospace;font-size:13px;white-space:pre-wrap;
|
||||
`;
|
||||
div.textContent = `${data.label || data.action} — ${type}${data.details ? ': ' + data.details : ''}`;
|
||||
el.appendChild(div);
|
||||
}
|
||||
static _color(t){ return ({SUCCESS:'#10B981', ERROR:'#EF4444', MOCK:'#8B5CF6'})[t] || '#6B7280'; }
|
||||
|
||||
static _handleGetFile(data, label) {
|
||||
const content = data?.content?.data ?? data?.content ?? data?.result?.content?.data ?? data?.result?.content;
|
||||
if (!content) return;
|
||||
window.AI_REPO_RESPONSES = window.AI_REPO_RESPONSES || [];
|
||||
window.AI_REPO_RESPONSES.push({ label, content });
|
||||
}
|
||||
|
||||
static _handleListFiles(data, label) {
|
||||
const files = data?.files ?? data?.result?.files;
|
||||
if (!Array.isArray(files)) return;
|
||||
const listing = '```text\n' + files.map(f => (typeof f === 'string' ? f : (f?.path || f?.name || JSON.stringify(f)))).join('\n') + '\n```';
|
||||
window.AI_REPO_RESPONSES = window.AI_REPO_RESPONSES || [];
|
||||
window.AI_REPO_RESPONSES.push({ label, content: listing });
|
||||
}
|
||||
|
||||
static delay(ms) { return new Promise(r => setTimeout(r, ms)); }
|
||||
}
|
||||
|
||||
window.AI_REPO_EXECUTOR = CommandExecutor;
|
||||
})();
|
||||
// ==COMMAND EXECUTOR END==
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
// ==COMMAND PARSER START==
|
||||
(function () {
|
||||
class CommandParser {
|
||||
static REQUIRED = {
|
||||
get_file: ['action', 'repo', 'path'],
|
||||
update_file: ['action', 'repo', 'path', 'content'],
|
||||
create_file: ['action', 'repo', 'path', 'content'],
|
||||
create_repo: ['action', 'repo'],
|
||||
create_branch:['action', 'repo', 'branch'],
|
||||
create_pr: ['action', 'repo', 'title', 'head', 'base'],
|
||||
list_files: ['action', 'repo', 'path']
|
||||
};
|
||||
|
||||
static parse(text) {
|
||||
const block = this.extractBlock(text);
|
||||
if (!block) throw new Error('No complete @bridge@ command found (missing @end@)');
|
||||
const parsed = this.parseKV(block);
|
||||
this.applyDefaults(parsed);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
static extractBlock(text) {
|
||||
const m = /^\s*@bridge@[ \t]*\n([\s\S]*?)\n@end@[ \t]*(?:\n|$)/m.exec(text);
|
||||
return m?.[1]?.trim() || null;
|
||||
}
|
||||
|
||||
// Simple YAML-like parser (supports "key: value" & "key: |" multiline)
|
||||
static parseKV(block) {
|
||||
const out = {};
|
||||
const lines = block.split('\n');
|
||||
let curKey = null, multi = false, buf = [];
|
||||
|
||||
const flush = () => { if (multi && curKey) out[curKey] = buf.join('\n').replace(/\s+$/,''); curKey = null; buf = []; multi = false; };
|
||||
|
||||
for (const raw of lines) {
|
||||
const line = raw.replace(/\r$/, '');
|
||||
|
||||
if (multi) {
|
||||
// End multiline if we see an unindented key pattern
|
||||
if (/^[A-Za-z_][\w\-]*\s*:/.test(line) && !/^\s/.test(line)) {
|
||||
flush();
|
||||
} else {
|
||||
buf.push(line);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const idx = line.indexOf(':');
|
||||
if (idx !== -1) {
|
||||
const key = line.slice(0, idx).trim();
|
||||
let value = line.slice(idx + 1).trim();
|
||||
if (value === '|') {
|
||||
curKey = key; multi = true; buf = [];
|
||||
} else {
|
||||
out[key] = value;
|
||||
curKey = key;
|
||||
}
|
||||
} else if (multi) {
|
||||
buf.push(line);
|
||||
}
|
||||
}
|
||||
flush();
|
||||
return out;
|
||||
}
|
||||
|
||||
static applyDefaults(p) {
|
||||
p.url = p.url || 'https://n8n.brrd.tech/webhook/ai-gitea-bridge';
|
||||
p.owner = p.owner || 'rob';
|
||||
if (p.action === 'create_branch' && !p.source_branch) p.source_branch = 'main';
|
||||
if (typeof p.repo === 'string' && p.repo.includes('/')) {
|
||||
const [owner, repo] = p.repo.split('/', 2);
|
||||
if (!p.owner) p.owner = owner;
|
||||
p.repo = repo;
|
||||
}
|
||||
}
|
||||
|
||||
static validate(p) {
|
||||
const errors = [];
|
||||
|
||||
// explicit example flag
|
||||
if (p.example === true || String(p.example).toLowerCase() === 'true' || String(p.example).toLowerCase() === 'yes') {
|
||||
return { isValid: true, errors: [], example: true };
|
||||
}
|
||||
|
||||
const action = p.action;
|
||||
if (!action) return { isValid: false, errors: ['Missing required field: action'] };
|
||||
const req = this.REQUIRED[action];
|
||||
if (!req) return { isValid: false, errors: [`Unknown action: ${action}`] };
|
||||
|
||||
for (const f of req) if (!(f in p) || p[f] === '') errors.push(`Missing required field: ${f}`);
|
||||
|
||||
return { isValid: errors.length === 0, errors };
|
||||
}
|
||||
}
|
||||
|
||||
window.AI_REPO_PARSER = CommandParser;
|
||||
})();
|
||||
// ==COMMAND PARSER END==
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
// ==CONFIG START==
|
||||
(function () {
|
||||
const STORAGE_KEYS = {
|
||||
history: 'ai_repo_commander_executed',
|
||||
cfg: 'ai_repo_commander_cfg',
|
||||
panel: 'ai_repo_commander_panel_state'
|
||||
};
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
meta: { version: '1.6.2' },
|
||||
|
||||
api: {
|
||||
enabled: true,
|
||||
timeout: 60000,
|
||||
maxRetries: 2,
|
||||
bridgeKey: ''
|
||||
},
|
||||
|
||||
debug: {
|
||||
enabled: true,
|
||||
level: 2, // 0=off, 1=errors, 2=info, 3=verbose, 4=trace
|
||||
maxLines: 400,
|
||||
showPanel: true
|
||||
},
|
||||
|
||||
execution: {
|
||||
debounceDelay: 6500,
|
||||
settleCheckMs: 1300,
|
||||
settlePollMs: 250,
|
||||
requireTerminator: true
|
||||
},
|
||||
|
||||
queue: {
|
||||
minDelayMs: 1500,
|
||||
maxPerMinute: 15,
|
||||
maxPerMessage: 5
|
||||
},
|
||||
|
||||
ui: {
|
||||
autoSubmit: true,
|
||||
appendTrailingNewline: true,
|
||||
postPasteDelayMs: 600,
|
||||
showExecutedMarker: true,
|
||||
processExisting: false // used by main.js
|
||||
},
|
||||
|
||||
// Runtime state (not persisted)
|
||||
runtime: {
|
||||
paused: false
|
||||
}
|
||||
};
|
||||
|
||||
class ConfigManager {
|
||||
constructor() { this.config = this.load(); }
|
||||
load() {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEYS.cfg);
|
||||
if (!raw) return this.deepClone(DEFAULT_CONFIG);
|
||||
const saved = JSON.parse(raw);
|
||||
return this.mergeConfigs(DEFAULT_CONFIG, saved);
|
||||
} catch {
|
||||
return this.deepClone(DEFAULT_CONFIG);
|
||||
}
|
||||
}
|
||||
save() {
|
||||
try {
|
||||
const { runtime, ...persistable } = this.config; // do not persist runtime
|
||||
localStorage.setItem('ai_repo_commander_cfg', JSON.stringify(persistable));
|
||||
} catch (e) { console.warn('Failed to save config:', e); }
|
||||
}
|
||||
get(keyPath) {
|
||||
return keyPath.split('.').reduce((obj, key) => obj?.[key], this.config);
|
||||
}
|
||||
set(keyPath, value) {
|
||||
const keys = keyPath.split('.');
|
||||
const last = keys.pop();
|
||||
const tgt = keys.reduce((o, k) => (o[k] = o[k] || {}), this.config);
|
||||
tgt[last] = value;
|
||||
this.save();
|
||||
}
|
||||
mergeConfigs(defaults, saved) {
|
||||
const out = this.deepClone(defaults);
|
||||
for (const k of Object.keys(saved)) {
|
||||
if (k === 'runtime') continue; // never restore runtime
|
||||
if (typeof out[k] === 'object' && !Array.isArray(out[k])) {
|
||||
out[k] = { ...out[k], ...saved[k] };
|
||||
} else {
|
||||
out[k] = saved[k];
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
deepClone(o) { return JSON.parse(JSON.stringify(o)); }
|
||||
}
|
||||
|
||||
window.AI_REPO_CONFIG = new ConfigManager();
|
||||
window.AI_REPO_STORAGE_KEYS = STORAGE_KEYS;
|
||||
})();
|
||||
// ==CONFIG END==
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
// ==LOGGER START==
|
||||
(function () {
|
||||
class Logger {
|
||||
constructor() {
|
||||
this.config = window.AI_REPO_CONFIG;
|
||||
this.buffer = [];
|
||||
}
|
||||
error(msg, data) { this._log(1, 'ERROR', msg, data); }
|
||||
warn(msg, data) { this._log(2, 'WARN', msg, data); }
|
||||
info(msg, data) { this._log(3, 'INFO', msg, data); }
|
||||
verbose(msg, data) { this._log(4, 'VERBOSE', msg, data); }
|
||||
trace(msg, data) { this._log(5, 'TRACE', msg, data); }
|
||||
|
||||
_log(levelNum, levelName, msg, data) {
|
||||
const enabled = !!this.config.get('debug.enabled');
|
||||
const level = this.config.get('debug.level') ?? 0;
|
||||
if (!enabled || levelNum > level) return;
|
||||
|
||||
const entry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
level: levelName,
|
||||
message: String(msg),
|
||||
data: this._sanitize(data)
|
||||
};
|
||||
this.buffer.push(entry);
|
||||
const maxLines = this.config.get('debug.maxLines') || 400;
|
||||
if (this.buffer.length > maxLines) this.buffer.splice(0, this.buffer.length - maxLines);
|
||||
|
||||
const prefix = `[AI RC ${levelName}]`;
|
||||
entry.data ? console.log(prefix, msg, entry.data) : console.log(prefix, msg);
|
||||
}
|
||||
|
||||
_sanitize(data) {
|
||||
if (!data) return null;
|
||||
if (data instanceof HTMLElement) return `HTMLElement<${data.tagName}>`;
|
||||
if (typeof data === 'string' && data.length > 200) return data.slice(0, 200) + '…';
|
||||
if (typeof data === 'object') {
|
||||
const out = {};
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
out[k] = v instanceof HTMLElement ? `HTMLElement<${v.tagName}>` :
|
||||
(typeof v === 'string' && v.length > 200 ? v.slice(0, 200) + '…' : v);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
getRecentLogs(n = 50) {
|
||||
return this.buffer.slice(-n).map(e =>
|
||||
`${e.timestamp} ${e.level.padEnd(7)} ${e.message}${e.data ? ' ' + JSON.stringify(e.data) : ''}`
|
||||
).join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
window.AI_REPO_LOGGER = new Logger();
|
||||
})();
|
||||
// ==LOGGER END==
|
||||
159
src/main.js
159
src/main.js
|
|
@ -1,159 +0,0 @@
|
|||
// ==MAIN START==
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (!window.AI_REPO_CONFIG || !window.AI_REPO_LOGGER || !window.AI_REPO_HISTORY || !window.AI_REPO_PARSER || !window.AI_REPO_EXECUTOR) {
|
||||
console.error('AI Repo Commander: Core modules not loaded');
|
||||
return;
|
||||
}
|
||||
|
||||
const logger = window.AI_REPO_LOGGER;
|
||||
const config = window.AI_REPO_CONFIG;
|
||||
const history = window.AI_REPO_HISTORY;
|
||||
|
||||
class AIRepoCommander {
|
||||
constructor() {
|
||||
this.isInitialized = false;
|
||||
this.observer = null;
|
||||
this.processed = new WeakSet();
|
||||
this.messageSelectors = [
|
||||
'[data-message-author-role="assistant"]',
|
||||
'.chat-message:not([data-message-author-role="user"])',
|
||||
'.message-content'
|
||||
];
|
||||
}
|
||||
|
||||
initialize() {
|
||||
if (this.isInitialized) return;
|
||||
logger.info('AI Repo Commander initializing', {
|
||||
version: config.get('meta.version'),
|
||||
debugLevel: config.get('debug.level'),
|
||||
apiEnabled: config.get('api.enabled')
|
||||
});
|
||||
|
||||
this.startObserver();
|
||||
if (config.get('ui.processExisting')) this.scanExisting();
|
||||
this.exposeAPI();
|
||||
|
||||
this.isInitialized = true;
|
||||
logger.info('AI Repo Commander initialized');
|
||||
}
|
||||
|
||||
startObserver() {
|
||||
this.observer = new MutationObserver((mutations) => {
|
||||
if (config.get('runtime.paused')) return;
|
||||
for (const m of mutations) {
|
||||
if (m.type !== 'childList') continue;
|
||||
for (const n of m.addedNodes) {
|
||||
if (n.nodeType !== 1) continue;
|
||||
if (this.isAssistantMessage(n)) this.processMessage(n);
|
||||
const inner = n.querySelectorAll?.(this.messageSelectors.join(',')) || [];
|
||||
inner.forEach(el => this.isAssistantMessage(el) && this.processMessage(el));
|
||||
}
|
||||
}
|
||||
});
|
||||
this.observer.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
isAssistantMessage(el) {
|
||||
return this.messageSelectors.some(sel => el.matches?.(sel));
|
||||
}
|
||||
|
||||
processMessage(el) {
|
||||
if (this.processed.has(el)) return;
|
||||
const commands = this.extractCommands(el);
|
||||
if (!commands.length) return;
|
||||
|
||||
this.processed.add(el);
|
||||
commands.slice(0, config.get('queue.maxPerMessage')).forEach((cmdText, idx) => {
|
||||
if (history.isProcessed(el, idx)) {
|
||||
this.addRetryButton(el, cmdText, idx);
|
||||
} else {
|
||||
this.run(el, cmdText, idx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
extractCommands(el) {
|
||||
const text = el.textContent || '';
|
||||
const out = [];
|
||||
const re = /@bridge@[\s\S]*?@end@/g;
|
||||
let m;
|
||||
while ((m = re.exec(text)) !== null) out.push(m[0]);
|
||||
return out;
|
||||
}
|
||||
|
||||
async run(el, commandText, index) {
|
||||
try {
|
||||
history.markProcessed(el, index);
|
||||
|
||||
const parsed = window.AI_REPO_PARSER.parse(commandText);
|
||||
const validation = window.AI_REPO_PARSER.validate(parsed);
|
||||
if (!validation.isValid) {
|
||||
logger.error('Command validation failed', { errors: validation.errors, command: parsed.action });
|
||||
this.addRetryButton(el, commandText, index);
|
||||
return;
|
||||
}
|
||||
if (validation.example) {
|
||||
logger.info('Skipping example command');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.delay(config.get('execution.debounceDelay') || 0);
|
||||
const label = `Command ${index + 1}`;
|
||||
await window.AI_REPO_EXECUTOR.execute(parsed, el, label);
|
||||
|
||||
} catch (e) {
|
||||
logger.error('Command execution failed', { error: e.message, commandIndex: index });
|
||||
this.addRetryButton(el, commandText, index);
|
||||
}
|
||||
}
|
||||
|
||||
addRetryButton(el, commandText, idx) {
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = `Run Again #${idx + 1}`;
|
||||
btn.style.cssText = `
|
||||
padding:4px 8px;margin:4px;border:1px solid #374151;border-radius:4px;
|
||||
background:#1f2937;color:#e5e7eb;cursor:pointer;
|
||||
`;
|
||||
btn.addEventListener('click', () => this.run(el, commandText, idx));
|
||||
el.appendChild(btn);
|
||||
}
|
||||
|
||||
scanExisting() {
|
||||
const nodes = document.querySelectorAll(this.messageSelectors.join(','));
|
||||
nodes.forEach(el => this.isAssistantMessage(el) && this.processMessage(el));
|
||||
}
|
||||
|
||||
exposeAPI() {
|
||||
window.AI_REPO_COMMANDER = {
|
||||
version: config.get('meta.version'),
|
||||
config,
|
||||
logger,
|
||||
history,
|
||||
pause: () => { config.set('runtime.paused', true); logger.info('Paused'); },
|
||||
resume: () => { config.set('runtime.paused', false); logger.info('Resumed'); },
|
||||
clearHistory: () => { history.clear(); logger.info('History cleared'); }
|
||||
};
|
||||
}
|
||||
|
||||
delay(ms) { return new Promise(r => setTimeout(r, ms)); }
|
||||
destroy() {
|
||||
this.observer?.disconnect();
|
||||
this.processed = new WeakSet();
|
||||
this.isInitialized = false;
|
||||
logger.info('AI Repo Commander destroyed');
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.AI_REPO_MAIN = new AIRepoCommander();
|
||||
window.AI_REPO_MAIN.initialize();
|
||||
});
|
||||
} else {
|
||||
window.AI_REPO_MAIN = new AIRepoCommander();
|
||||
window.AI_REPO_MAIN.initialize();
|
||||
}
|
||||
})();
|
||||
// ==MAIN END==
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
// ==STORAGE START==
|
||||
(function () {
|
||||
class ConversationHistory {
|
||||
constructor() {
|
||||
this.conversationId = this._getConversationId();
|
||||
this.key = `ai_rc:conv:${this.conversationId}:processed`;
|
||||
this.cache = this._load();
|
||||
this._cleanupExpired();
|
||||
}
|
||||
|
||||
_getConversationId() {
|
||||
const host = location.hostname.replace('chat.openai.com', 'chatgpt.com');
|
||||
return `${host}:${location.pathname || '/'}`;
|
||||
}
|
||||
|
||||
_load() {
|
||||
try { return JSON.parse(localStorage.getItem(this.key) || '{}'); }
|
||||
catch { return {}; }
|
||||
}
|
||||
_save() {
|
||||
try { localStorage.setItem(this.key, JSON.stringify(this.cache)); }
|
||||
catch (e) { window.AI_REPO_LOGGER?.warn('Failed to save history cache', { error: e.message }); }
|
||||
}
|
||||
|
||||
isProcessed(el, commandIndex = 0) {
|
||||
const fp = this._fingerprint(el, commandIndex);
|
||||
return Object.prototype.hasOwnProperty.call(this.cache, fp);
|
||||
}
|
||||
markProcessed(el, commandIndex = 0) {
|
||||
const fp = this._fingerprint(el, commandIndex);
|
||||
this.cache[fp] = Date.now();
|
||||
this._save();
|
||||
if (window.AI_REPO_CONFIG.get('ui.showExecutedMarker')) this._mark(el);
|
||||
}
|
||||
|
||||
_fingerprint(el, idx) {
|
||||
const text = (el.textContent || '').slice(0, 1000);
|
||||
const list = Array.from(document.querySelectorAll('[data-message-author-role], .chat-message, .message-content'));
|
||||
const pos = list.indexOf(el);
|
||||
return `conv:${this.conversationId}|pos:${pos}|idx:${idx}|hash:${this._hash(text)}`;
|
||||
}
|
||||
_hash(str) {
|
||||
let h = 5381;
|
||||
for (let i = 0; i < Math.min(str.length, 1000); i++) h = ((h << 5) + h) ^ str.charCodeAt(i);
|
||||
return (h >>> 0).toString(36);
|
||||
}
|
||||
_mark(el) { try { el.style.borderLeft = '3px solid #10B981'; } catch {} }
|
||||
|
||||
_cleanupExpired() {
|
||||
const ttl = 30 * 24 * 60 * 60 * 1000;
|
||||
const now = Date.now();
|
||||
let dirty = false;
|
||||
for (const [k, ts] of Object.entries(this.cache)) {
|
||||
if (!ts || now - ts > ttl) { delete this.cache[k]; dirty = true; }
|
||||
}
|
||||
if (dirty) this._save();
|
||||
}
|
||||
|
||||
clear() { this.cache = {}; this._save(); }
|
||||
}
|
||||
|
||||
window.AI_REPO_HISTORY = new ConversationHistory();
|
||||
})();
|
||||
// ==STORAGE END==
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
// ==UserScript==
|
||||
// @name AI Repo Commander (Modular Refactor)
|
||||
// @namespace http://tampermonkey.net/
|
||||
// @version 2.0.0-mod
|
||||
// @description Modularized: config, logger, storage, parser, executor, main
|
||||
// @author You
|
||||
// @match https://chat.openai.com/*
|
||||
// @match https://chatgpt.com/*
|
||||
// @match https://claude.ai/*
|
||||
// @match https://gemini.google.com/*
|
||||
// @grant GM_xmlhttpRequest
|
||||
// @connect n8n.brrd.tech
|
||||
// @connect *
|
||||
// @require https://YOUR_HOST/config.js
|
||||
// @require https://YOUR_HOST/logger.js
|
||||
// @require https://YOUR_HOST/storage.js
|
||||
// @require https://YOUR_HOST/command-parser.js
|
||||
// @require https://YOUR_HOST/command-executor.js
|
||||
// @require https://YOUR_HOST/main.js
|
||||
// ==/UserScript==
|
||||
Loading…
Reference in New Issue