Spaces:
Running
Running
| import { spawn } from 'node:child_process'; | |
| import fs from 'node:fs/promises'; | |
| import path from 'node:path'; | |
| const args = process.argv.slice(2); | |
| const realCli = process.env.CODEXMOBILE_REAL_LARK_CLI || 'lark-cli'; | |
| const stateDir = process.env.CODEXMOBILE_LARK_GUARD_STATE_DIR || process.cwd(); | |
| const turnId = process.env.CODEXMOBILE_TURN_ID || process.env.CODEXMOBILE_SESSION_ID || 'unknown-turn'; | |
| function argValue(flag) { | |
| const index = args.indexOf(flag); | |
| if (index < 0 || index + 1 >= args.length) { | |
| return ''; | |
| } | |
| return String(args[index + 1] || '').trim(); | |
| } | |
| function isSlidesCreate() { | |
| return args[0] === 'slides' && args[1] === '+create'; | |
| } | |
| function stateKey() { | |
| const title = argValue('--title') || '(untitled)'; | |
| return `${turnId}:${title.toLowerCase()}`; | |
| } | |
| function redacted(value) { | |
| return String(value || '') | |
| .replace(/"appSecret"\s*:\s*"[^"]+"/gi, '"appSecret":"****"') | |
| .replace(/"access[_-]?token"\s*:\s*"[^"]+"/gi, '"accessToken":"****"') | |
| .replace(/"refresh[_-]?token"\s*:\s*"[^"]+"/gi, '"refreshToken":"****"') | |
| .replace(/\b(u|ur|t)-[A-Za-z0-9._-]{20,}\b/g, '$1-[hidden]') | |
| .replace(/sk-[A-Za-z0-9._-]+/g, 'sk-[hidden]'); | |
| } | |
| async function readState(filePath) { | |
| try { | |
| const raw = await fs.readFile(filePath, 'utf8'); | |
| const parsed = JSON.parse(raw); | |
| return parsed && typeof parsed === 'object' ? parsed : {}; | |
| } catch { | |
| return {}; | |
| } | |
| } | |
| async function writeState(filePath, state) { | |
| await fs.mkdir(path.dirname(filePath), { recursive: true }); | |
| await fs.writeFile(filePath, JSON.stringify(state, null, 2), 'utf8'); | |
| } | |
| function isPreCreateValidationFailure(text) { | |
| return /--slides invalid json|invalid json, must be an array|unknown flag|required flag|auth|permission|forbidden|unauthorized|no user logged in|network|econn|timeout/i.test(text); | |
| } | |
| async function maybeBlockDuplicate(stateFile) { | |
| if (!isSlidesCreate()) { | |
| return false; | |
| } | |
| const state = await readState(stateFile); | |
| const key = stateKey(); | |
| const existing = state[key]; | |
| if (!existing?.attemptedAt) { | |
| return false; | |
| } | |
| const title = argValue('--title') || '(untitled)'; | |
| const hint = { | |
| ok: false, | |
| error: { | |
| type: 'codexmobile_duplicate_slides_create_blocked', | |
| message: `This turn already ran slides +create for "${title}". Do not create another PPT. Reuse the previous xml_presentation_id and repair/append pages on the same presentation.`, | |
| previous: { | |
| title, | |
| attemptedAt: existing.attemptedAt, | |
| xmlPresentationId: existing.xmlPresentationId || '', | |
| output: existing.output || '' | |
| }, | |
| nextStep: 'Run lark-cli slides xml_presentations get, then use xml_presentation.slide.create or +replace-slide on the existing PPT.' | |
| } | |
| }; | |
| process.stderr.write(`${JSON.stringify(hint, null, 2)}\n`); | |
| process.exitCode = 24; | |
| return true; | |
| } | |
| function extractPresentationId(text) { | |
| const value = String(text || ''); | |
| const jsonMatch = value.match(/"xml_presentation_id"\s*:\s*"([^"]+)"/i); | |
| if (jsonMatch) { | |
| return jsonMatch[1]; | |
| } | |
| const urlMatch = value.match(/\/slides\/([A-Za-z0-9_-]+)/i); | |
| return urlMatch?.[1] || ''; | |
| } | |
| async function recordCreateAttempt(stateFile, stdout, stderr, code) { | |
| if (!isSlidesCreate()) { | |
| return; | |
| } | |
| const combined = `${stdout}\n${stderr}`; | |
| const xmlPresentationId = extractPresentationId(combined); | |
| if (code !== 0 && !xmlPresentationId && isPreCreateValidationFailure(combined)) { | |
| return; | |
| } | |
| const state = await readState(stateFile); | |
| const key = stateKey(); | |
| state[key] = { | |
| attemptedAt: new Date().toISOString(), | |
| title: argValue('--title') || '(untitled)', | |
| xmlPresentationId, | |
| output: redacted(combined).slice(-1600) | |
| }; | |
| await writeState(stateFile, state); | |
| } | |
| function larkCliSpawnCommand() { | |
| if (process.platform === 'win32' && /\.cmd$|\.bat$/i.test(realCli)) { | |
| const commandLine = ['call', windowsCmdQuote(realCli), ...args.map(windowsCmdQuote)].join(' '); | |
| return { | |
| command: process.env.ComSpec || 'cmd.exe', | |
| args: ['/d', '/c', commandLine], | |
| windowsVerbatimArguments: true | |
| }; | |
| } | |
| return { | |
| command: realCli, | |
| args, | |
| windowsVerbatimArguments: false | |
| }; | |
| } | |
| function windowsCmdQuote(value) { | |
| return `"${String(value || '').replace(/"/g, '""').replace(/\r?\n/g, ' ')}"`; | |
| } | |
| async function main() { | |
| const stateFile = path.join(stateDir, 'lark-slides-create-guard.json'); | |
| if (await maybeBlockDuplicate(stateFile)) { | |
| return; | |
| } | |
| const cli = larkCliSpawnCommand(); | |
| const child = spawn(cli.command, cli.args, { | |
| cwd: process.cwd(), | |
| env: { | |
| ...process.env, | |
| CODEXMOBILE_LARK_GUARD_CHILD: '1' | |
| }, | |
| shell: false, | |
| windowsHide: true, | |
| windowsVerbatimArguments: cli.windowsVerbatimArguments | |
| }); | |
| let stdout = ''; | |
| let stderr = ''; | |
| child.stdout?.on('data', (chunk) => { | |
| const text = chunk.toString('utf8'); | |
| stdout += text; | |
| }); | |
| child.stderr?.on('data', (chunk) => { | |
| const text = chunk.toString('utf8'); | |
| stderr += text; | |
| }); | |
| child.on('error', (error) => { | |
| process.stderr.write(`${error.message}\n`); | |
| process.exitCode = 1; | |
| }); | |
| child.on('close', async (code) => { | |
| if (stdout) { | |
| process.stdout.write(stdout); | |
| } | |
| if (stderr) { | |
| process.stderr.write(stderr); | |
| } | |
| await recordCreateAttempt(stateFile, stdout, stderr, code || 0).catch((error) => { | |
| process.stderr.write(`[codexmobile-lark-guard] failed to record state: ${error.message}\n`); | |
| }); | |
| process.exitCode = code || 0; | |
| }); | |
| } | |
| main().catch((error) => { | |
| process.stderr.write(`${error.message}\n`); | |
| process.exitCode = 1; | |
| }); | |