Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,8 @@ tls-*.json
*.pfx
*.p12
/source-manifest.json

# Local assessment and review materials stay outside commits.
.review*/
[Aa][Ss][Ss][Ee][Ss][Ss][Mm][Ee][Nn][Tt]*/
*[Aa][Ss][Ss][Ee][Ss][Ss][Mm][Ee][Nn][Tt]*.md
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# 本对话的版本约定

- 用户于 2026-10-31 更新约定:当前及后续版本固定为 `0.6.0`,本地开发分支为 `codex/0.6.0`;不要自动递增或添加 preview 序号。下方旧版本约定仅作历史记录。
- 用户于 2026-10-31 明确授权本轮将最新源码及用户 README 同步到本地脱敏发布仓库后推送并合入公开 `main`。通过独立公开仓库和正常 PR 检查完成,不推送私有开发历史;此次不安装或发布 Release,之后默认仍只保存本地。
- assessment 和临时 `.review` 审查材料统一保存在被忽略的 `.cache/assessments/`;不要加入 Git 暂存区、提交或公开源码包。用户明确仅移走现有 `.review` 审查目录,`docs` 中的项目评估等正式文档保留。使用明确文件列表暂存,提交前检查列表。
- 当前版本需要构建时使用独立的 `0.6.0` 源码导出目录,保留旧版构建和安装。

- 用户于 2026-10-29 更新约定:后续更改统一记录为 `0.5.1`,在 `preview/0.5.1` 分支开发;不要自动递增版本号或添加 preview 序号。此前 `0.5.0-preview` 已推送并保留。
- 用户于 2026-10-29 本轮明确授权:将 `0.5.1` 源码和用户编写的 README 原样推送并合入公开仓库 `main`;仍通过脱敏独立仓库发布。该次授权不包含安装、切换正在使用的插件或发布 Release;完成后后续改动默认仍只保存在本地,除非收到新指令。
- 当前继续使用的 `0.4.0-preview.1` 是保留版本。不要覆盖其安装目录、已生成的 0.4.0 构建、运行配置、任务数据或 GitHub `main` 分支。
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
# 更新记录

## 0.6.0 — 2026-10-31

- 汇总 0.5.1 发布后的数据目录、首次额度监测、演示窗口隔离、备份恢复及表单修复。
- 「加入我的工作」可选择加入后立即开始监测,默认关闭。
- 退出前记录所属服务进程树,退出后清理残留子进程;核对进程创建时间,避免 PID 复用误杀。
- 当前版本统一记录为 0.6.0;用户编写的 README 原样保留。

## 0.5.1 — 开发中

- 第四轮审查修复:PowerShell 回退监听器显式传递指定数据目录并恢复调用方环境;发送结果不确定时清除旧等待原因,卡片按实际阻塞状态提示人工处理;完整页面保留并按顺序补充自定义轮询周期选项。
- 「加入我的工作」新增「加入后开始监测」选项,默认关闭;勾选时与聊天记录一并保存,启用此聊天的监测,继续遵循后台调度开关、额度耗尽恢复和发送上限。中英文挂件与完整页面均支持;MCP 导入仍保持暂停。
- 第四轮修复与新增选项验证:369 项回归全部通过,包含真实 DOM 的自定义周期、语言切换草稿和监测选项提交、PowerShell 目录传递、HTTP 持久化及不确定发送状态;语法和公开源码检查通过,未安装或发送真实消息。
- 第三轮审查修复:正式与演示模式使用独立的 Electron 配置、挂件偏好和诊断文件;启动及第二实例请求核对模式、服务地址和数据目录,避免演示启动打开正式窗口。
- 自启动启用和停用共用运行时的数据目录解析,避免源码目录因创建 data 文件夹而切换默认数据位置,兼容中文路径。
- 恢复备份后同步设置表单;完整页面和挂件保留正在编辑的字段、同步未编辑字段,保存时只提交用户修改项,避免覆盖恢复后的执行上限。
- 第三轮修复验证:362 项回归全部通过,包含双向启动顺序的原生窗口锁检查、10 组自启动目录组合及两种界面的备份恢复检查;语法和公开源码检查通过。测试使用独立数据和模拟通道。
- 后续版本统一记录为 0.5.1,在 preview/0.5.1 分支开发。
- main 合入检查修正:状态文件的大小检查与读取共用同一文件描述符;HTML 转义回归补充大写及带属性的 SCRIPT 输入,并使用不区分大小写的断言。
- 插件、命令行和桌面共用启动与退出接口;本机配置、固定证书请求和生命周期协调明确分工,统一服务构建及目录核验,保留查询冷启动与完整退出规则。
Expand Down
13 changes: 5 additions & 8 deletions Enable-Autostart.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,11 @@ $continuerName = 'CodexContinuer'
$continuerNode = (Get-Command node -ErrorAction Stop).Source
$continuerRoot = [IO.Path]::GetFullPath($PluginRoot)
$continuerWatcher = Join-Path $continuerRoot 'desktop/watch-codex.ps1'
$continuerRuntime = Join-Path $continuerRoot 'runtime.json'
$continuerData = Join-Path $continuerRoot 'data'
if (Test-Path -LiteralPath $continuerRuntime) {
$continuerConfig = Get-Content -LiteralPath $continuerRuntime -Raw | ConvertFrom-Json
if ($continuerConfig.dataDir) { $continuerData = $continuerConfig.dataDir }
elseif ($continuerConfig.profile -eq 'portable' -or -not (Test-Path -LiteralPath $continuerData)) { $continuerData = Join-Path $env:LOCALAPPDATA 'CodexContinuer' }
}
if ($env:CONTINUER_DATA_DIR) { $continuerData = $env:CONTINUER_DATA_DIR }
# Resolve before creating directories, using the same rules as every runtime.
# Base64 preserves Unicode paths across Windows PowerShell console encodings.
$continuerEncodedData = & $continuerNode '-e' 'process.stdout.write(Buffer.from(require(process.argv[1]).dataDir, ''utf16le'').toString(''base64''))' (Join-Path $continuerRoot 'server/config.cjs')
if ($LASTEXITCODE -ne 0 -or -not $continuerEncodedData) { throw 'Could not resolve the Continuer data directory.' }
$continuerData = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($continuerEncodedData))
New-Item -ItemType Directory -Path $continuerData -Force | Out-Null
$continuerRegistryKey = Get-Item -LiteralPath $continuerRegistry -ErrorAction SilentlyContinue
$continuerExisting = if ($continuerRegistryKey) { $continuerRegistryKey.GetValue($continuerName, $null) } else { $null }
Expand Down
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

监测 Codex 账户额度,在额度恢复后接续指定工作,并通过跟随 Codex 窗口的桌面挂件查看状态。

**开发版本:0.5.1 · Windows x64 预览版 · GPL-3.0-only。**
**开发版本:0.6.0 · Windows x64 预览版 · GPL-3.0-only。**

## 功能

Expand All @@ -22,13 +22,13 @@

## 相对上版本的改动

- 你现在可以随意编辑它在额度耗尽后的发送内容
- 解决了即使当前任务已经结束,仍然会对被设置监测的对话发送约定消息的问题
- 拆分了事件处理器的职责,减轻了核心管理器manager.js的负担
- 明确了不同工作的边界,加入了跨模式校验
- 减轻了重复逻辑的使用,稍微优化了石山
- 给依赖文字和源码形状的实现添加了新的协议
- 给英文版装了新的依赖
- 处理了你更新版本后,挂件与已安装的插件使用不同数据目录导致连接失败的问题
- 解决了首次使用插件或首次对对话执行调度后,可能永久等待额度的问题
- 处理了演示模式复用正式窗口的问题
- 解决了退出continuer后因后台继续而导致重启很麻烦的问题
- 现在在设置工作和选择对话处你可以直接开始监测



## 安装

Expand Down
22 changes: 13 additions & 9 deletions desktop/runtime.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@ function createDesktopRuntime({ load = require, processInfo = process, timing =
const runtimeConfig = load('../server/config.cjs');
const logging = load('../server/logging.cjs');
const localTls = load('../server/local-tls.cjs');
const { widgetState, matchesWidgetIdentity } = load('../server/widget-state.cjs');

app.setName('Codex Continuer');
const dataDir = runtimeConfig.dataDir;
const demo = processInfo.env.CONTINUER_DEMO === '1';
const widget = widgetState(dataDir, runtimeConfig.port, demo);
const widgetInfo = () => ({ ...runtimeConfig.appInfo(), ...widget.identity });
mkdirSync(dataDir, { recursive: true });
logging.installLogs(dataDir, 'widget');
app.setPath('userData', join(dataDir, 'desktop-profile'));
const preferenceFile = join(dataDir, 'widget-preferences.json');
app.setPath('userData', widget.profile);
const preferenceFile = widget.preferences;
let prefs = { follow: true, expanded: false, hidden: false, compactSize: 256, offsets: {}, positions: {} };
try { prefs = { ...prefs, ...JSON.parse(readFileSync(preferenceFile, 'utf8')) }; } catch {}
let preferenceError = null;
Expand All @@ -34,22 +38,22 @@ function createDesktopRuntime({ load = require, processInfo = process, timing =
if (processInfo.argv.includes('--auto')) prefs.hidden = false;
function diagnostics() {
if (!win || win.isDestroyed() || dragging || pointerDrag?.active) return;
const next = JSON.stringify({ ...runtimeConfig.appInfo(), pid: processInfo.pid, ready: isReady, visible: win.isVisible(), ...state(), bounds: win.getBounds() });
if (next !== lastDiagnostic) { lastDiagnostic = next; try { writeFileSync(join(dataDir, 'widget-status.json'), next); } catch (error) { console.error(`挂件诊断未保存:${error.message}`); } }
const next = JSON.stringify({ ...widgetInfo(), pid: processInfo.pid, ready: isReady, visible: win.isVisible(), ...state(), bounds: win.getBounds() });
if (next !== lastDiagnostic) { lastDiagnostic = next; try { writeFileSync(widget.status, next); } catch (error) { console.error(`挂件诊断未保存:${error.message}`); } }
}
const ownsInstance = app.requestSingleInstanceLock();
const ownsInstance = app.requestSingleInstanceLock(widget.identity);
let quitting, shutdownComplete = false;
if (!ownsInstance) { app.quit(); }
else if (processInfo.argv.includes('--quit')) { app.whenReady().then(quitAll); }
else {
app.on('second-instance', (_event, argv) => { if (argv.includes('--quit')) { void quitAll(); return; } if (closing) return; prefs.hidden = false; if (!argv.includes('--auto')) changeExpanded(true); save(); show(!argv.includes('--auto')); });
app.on('second-instance', (_event, argv, _directory, identity) => { if (!matchesWidgetIdentity(identity, widget.identity, processInfo.platform)) return; if (argv.includes('--quit')) { void quitAll(); return; } if (closing) return; prefs.hidden = false; if (!argv.includes('--auto')) changeExpanded(true); save(); show(!argv.includes('--auto')); });
app.whenReady().then(start).catch(error => { dialog.showErrorBox(nativeText("native.continuer_could_not_start"), error.message); app.quit(); });
}
function quitAll() {
if (quitting) return quitting;
closing = true; isReady = false;
pointerDrag?.finish(); cancelTransition(); tracker?.kill(); win?.hide();
try { writeFileSync(join(dataDir, 'widget-status.json'), JSON.stringify({ ...runtimeConfig.appInfo(), pid: processInfo.pid, ready: false, exitReason: 'quitting', updatedAt: Date.now() })); } catch {}
try { writeFileSync(widget.status, JSON.stringify({ ...widgetInfo(), pid: processInfo.pid, ready: false, exitReason: 'quitting', updatedAt: Date.now() })); } catch {}
quitting = (async () => {
try {
const runtime = await import('../plugin/runtime.js');
Expand Down Expand Up @@ -250,7 +254,7 @@ function createDesktopRuntime({ load = require, processInfo = process, timing =
let rendererRestarts = 0;
win.webContents.on('render-process-gone', (_event, detail) => {
pointerDrag.finish(); isReady = false; lastDiagnostic = null; lastSentState = null;
try { writeFileSync(join(dataDir, 'widget-status.json'), JSON.stringify({ ...runtimeConfig.appInfo(), pid: processInfo.pid, ready: false, exitReason: `renderer-${detail.reason}` })); } catch {}
try { writeFileSync(widget.status, JSON.stringify({ ...widgetInfo(), pid: processInfo.pid, ready: false, exitReason: `renderer-${detail.reason}` })); } catch {}
if (!closing && rendererRestarts++ < 3) setTimeout(() => { if (!closing && !win.isDestroyed()) win.reload(); }, 1000 * rendererRestarts);
});
win.webContents.on('did-finish-load', () => { if (isReady) show(); });
Expand Down Expand Up @@ -317,7 +321,7 @@ function createDesktopRuntime({ load = require, processInfo = process, timing =
if (!ownsInstance) return;
if (!shutdownComplete) { event.preventDefault(); void quitAll(); return; }
closing = true; pointerDrag?.finish(); cancelTransition(); tracker?.kill(); globalShortcut.unregisterAll(); tray?.destroy();
try { writeFileSync(join(dataDir, 'widget-status.json'), JSON.stringify({ ...runtimeConfig.appInfo(), pid: processInfo.pid, ready: false, exitReason: 'user-quit', updatedAt: Date.now() })); } catch {}
try { writeFileSync(widget.status, JSON.stringify({ ...widgetInfo(), pid: processInfo.pid, ready: false, exitReason: 'user-quit', updatedAt: Date.now() })); } catch {}
});
app.on('window-all-closed', () => app.quit());
return { changeExpanded, finishTransition, show, savePosition, state, setFollow, get preferences() { return prefs; } };
Expand Down
8 changes: 6 additions & 2 deletions desktop/watch-codex.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,12 @@ try {
if ($continuerSeen) { $continuerSeen = ''; Write-ContinuerStartupStatus 'watching' 'Codex desktop closed; waiting for next launch' }
} elseif ($continuerKey -ne $continuerSeen -and [DateTime]::UtcNow -ge $continuerRetryAt) {
# Run inside this already-hidden console; LASTEXITCODE is reliable in Windows PowerShell 5.1.
& $NodePath $continuerLaunchPath '--auto' 1> (Join-Path $DataDir 'autostart-launch.log') 2> (Join-Path $DataDir 'autostart-error.log')
if ($LASTEXITCODE -ne 0) { throw 'Widget startup failed; see autostart-error.log' }
$continuerPreviousData = $env:CONTINUER_DATA_DIR
try {
$env:CONTINUER_DATA_DIR = $DataDir
& $NodePath $continuerLaunchPath '--auto' 1> (Join-Path $DataDir 'autostart-launch.log') 2> (Join-Path $DataDir 'autostart-error.log')
if ($LASTEXITCODE -ne 0) { throw 'Widget startup failed; see autostart-error.log' }
} finally { $env:CONTINUER_DATA_DIR = $continuerPreviousData }
$continuerSeen = $continuerKey
Write-ContinuerStartupStatus 'launched' 'Widget started for Codex desktop'
}
Expand Down
2 changes: 2 additions & 0 deletions docs/task-models.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

两种模式仍使用现有的 `continuous` 字段以兼容界面和持久化格式,含义分别是“允许继续下一轮”和“开启额度监测”。状态及计数的规则由各自模块维护,调用方不再通过同一段分支处理两类任务的暂停、继续和完成。

通过挂件或完整页面加入原聊天时,可勾选「加入后开始监测」,与新记录一并保存;未勾选和 MCP 导入都保持暂停。开启监测不会改变后台调度开关,也不会因当前额度正常而立即发送消息,仍需确认后续耗尽与恢复。

## 旧数据与恢复

- 读取没有 `mode` 的旧工作记录时补为 `direct`,保留目标和执行次数。明确标为 `direct` 的 writer 等待继续按工作模式重试。
Expand Down
Loading
Loading