@walkhi/code-relax 0.1.0-beta.33 → 0.1.0-beta.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/codex-remote.mjs +26 -2
- package/dist/build-platform.json +1 -1
- package/dist/src/managed-app-server.mjs +1 -0
- package/dist/src/platform/power-inhibitor.mjs +22 -0
- package/dist/src/platform/windows/keep-awake.ps1 +20 -0
- package/dist/src/server.mjs +8 -0
- package/dist/src/service-doctor.mjs +7 -1
- package/dist/src/service-lifecycle.mjs +1 -0
- package/dist/src/service-power.mjs +58 -0
- package/dist/src/shared-app-server.mjs +41 -6
- package/package.json +4 -4
|
@@ -23,6 +23,7 @@ import { controlRequest } from '../src/service-control.mjs';
|
|
|
23
23
|
import { processAlive } from '../src/background-process.mjs';
|
|
24
24
|
import { prepareNativePackage } from '../src/platform/native-package.mjs';
|
|
25
25
|
import { managedAppServerUrl } from '../src/managed-app-server.mjs';
|
|
26
|
+
import { powerConfigPath, powerEnabled } from '../src/service-power.mjs';
|
|
26
27
|
import { readDotsTasks } from '../src/dots-task-catalog.mjs';
|
|
27
28
|
import { installDesktopHelper, desktopHelperStatus, removeDesktopHelper } from '../src/platform/windows/desktop-helper.mjs';
|
|
28
29
|
|
|
@@ -45,6 +46,8 @@ const help = `Code Relax(Windows / macOS / Linux)
|
|
|
45
46
|
desktop-helper-remove (Windows)移除桌面操作辅助服务,需要工作站 UAC 授权
|
|
46
47
|
doctor 简单检查工作站是否正常
|
|
47
48
|
diag 输出详细诊断报告,供客服或技术人员排查
|
|
49
|
+
keep-awake 默认保持电脑唤醒;查看状态
|
|
50
|
+
keep-awake on|off 开启或关闭保持唤醒,立即生效并记住选择;允许屏幕熄灭
|
|
48
51
|
kernel-restart --confirm 重启 Codex 内核并启用已选更新;等待新内核就绪
|
|
49
52
|
desktop-recover --confirm 重新打开 Desktop 并接入 Codex 内核;不强制重启已有内核
|
|
50
53
|
uninstall 停止并移除固定目录服务和本包 Skill,清理自有 Desktop 环境变量,保留其他配置与附件
|
|
@@ -71,8 +74,10 @@ async function main() {
|
|
|
71
74
|
const longPair = command === 'pair' && options.length === 2 && options[0] === '--expires';
|
|
72
75
|
const revokeInvite = command === 'pair-revoke' && options.length === 1 && /^[\w-]{16,128}$/.test(options[0]);
|
|
73
76
|
const revokeDevice = command === 'device-revoke' && options.length === 2 && /^[\w-]{16,128}$/.test(options[0]) && options[1] === '--confirm';
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
const powerCommand = command === 'keep-awake';
|
|
78
|
+
if (powerCommand && !(options.length === 0 || options.length === 1 && ['on', 'off'].includes(options[0]))) throw new Error('用法:relax keep-awake [on|off]');
|
|
79
|
+
if (!['keep-awake', 'kernel-restart', 'desktop-recover', 'setup', 'doctor', 'diag', 'uninstall', 'start', 'pair', 'pair-list', 'pair-revoke', 'devices', 'device-revoke', 'dots-tasks', 'desktop-helper-install', 'desktop-helper-status', 'desktop-helper-remove'].includes(command)
|
|
80
|
+
|| (!powerCommand && !longPair && !revokeInvite && !revokeDevice && (['pair-revoke', 'device-revoke'].includes(command) || options.some(flag => !((['kernel-restart', 'desktop-recover'].includes(command) && flag === '--confirm') || (command === 'setup' && flag === '--desktop-helper')))))) {
|
|
76
81
|
throw new Error('无效命令或参数。使用 --help 查看用法。');
|
|
77
82
|
}
|
|
78
83
|
const inviteExpires = longPair ? Date.parse(options[1]) : undefined;
|
|
@@ -100,6 +105,25 @@ async function main() {
|
|
|
100
105
|
}
|
|
101
106
|
const paths = runtimePaths();
|
|
102
107
|
const { stateRoot, serviceRoot } = paths;
|
|
108
|
+
if (powerCommand) {
|
|
109
|
+
const session = readRecord(path.join(stateRoot, 'bridge-session.json'));
|
|
110
|
+
let live = false;
|
|
111
|
+
if (session?.controlPort) {
|
|
112
|
+
try { await controlRequest(session); live = true; }
|
|
113
|
+
catch (error) { if (error.code !== 'ECONNREFUSED') throw error; }
|
|
114
|
+
}
|
|
115
|
+
let result;
|
|
116
|
+
if (live) {
|
|
117
|
+
const action = options[0] === 'on' ? 'enable' : options[0] === 'off' ? 'disable' : 'status';
|
|
118
|
+
result = await controlRequest(session, 'POST', `keep-awake-${action}`);
|
|
119
|
+
} else {
|
|
120
|
+
if (options.length) { fs.mkdirSync(stateRoot, { recursive: true }); writeRecord(powerConfigPath(stateRoot), { enabled: options[0] === 'on' }); }
|
|
121
|
+
result = { enabled: powerEnabled(stateRoot), active: false, error: '', serviceStopped: true };
|
|
122
|
+
}
|
|
123
|
+
console.log(json ? JSON.stringify(result) : `保持唤醒:${result.enabled ? '已开启' : '已关闭'};${result.active ? '正在生效,允许屏幕熄灭' : result.serviceStopped ? '服务未运行,下次启动应用此设置' : result.error || '未生效'}`);
|
|
124
|
+
if (result.error) process.exitCode = 1;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
103
127
|
const installedEntry = path.join(serviceRoot, 'dist/bin/codex-remote.mjs');
|
|
104
128
|
const installationPath = path.join(serviceRoot, 'installation.json');
|
|
105
129
|
const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(Number);
|
package/dist/build-platform.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"layout":"npm-platform-packages","version":"0.1.0-beta.
|
|
1
|
+
{"layout":"npm-platform-packages","version":"0.1.0-beta.35"}
|
|
@@ -35,6 +35,7 @@ export class ManagedAppServer extends EventEmitter {
|
|
|
35
35
|
this.recordPath = path.join(stateRoot, 'managed-app-server.json');
|
|
36
36
|
this.client = client;
|
|
37
37
|
this.state = { state: 'absent', ready: false, error: null };
|
|
38
|
+
this.client.diagnosticIdentity = () => ({ kernelPid: this.state.pid ?? null, kernelCreated: this.state.created ?? null });
|
|
38
39
|
this.restartStatus = null;
|
|
39
40
|
this.stopped = true;
|
|
40
41
|
this.restarting = false;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
// stdin is a lifetime lease: even an abrupt service exit releases the assertion.
|
|
6
|
+
export function launchPowerInhibitor() {
|
|
7
|
+
const lease = "process.stdin.resume();process.stdin.on('end',()=>process.exit(0));console.log('ready')";
|
|
8
|
+
let executable, args;
|
|
9
|
+
if (process.platform === 'darwin') {
|
|
10
|
+
executable = '/usr/bin/caffeinate';
|
|
11
|
+
args = ['-i', process.execPath, '-e', lease];
|
|
12
|
+
} else if (process.platform === 'win32') {
|
|
13
|
+
executable = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32/WindowsPowerShell/v1.0/powershell.exe');
|
|
14
|
+
args = ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File',
|
|
15
|
+
fileURLToPath(new URL('./windows/keep-awake.ps1', import.meta.url))];
|
|
16
|
+
} else if (process.platform === 'linux') {
|
|
17
|
+
executable = '/usr/bin/systemd-inhibit';
|
|
18
|
+
args = ['--what=idle', '--mode=block', '--who=Code Relax', '--why=Workstation remote access',
|
|
19
|
+
process.execPath, '-e', lease];
|
|
20
|
+
} else throw new Error(`保持唤醒不支持当前系统:${process.platform}`);
|
|
21
|
+
return spawn(executable, args, { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
22
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
$ErrorActionPreference = 'Stop'
|
|
2
|
+
Import-Module "$env:SystemRoot\System32\WindowsPowerShell\v1.0\Modules\Microsoft.PowerShell.Utility\Microsoft.PowerShell.Utility.psd1"
|
|
3
|
+
Add-Type -TypeDefinition @'
|
|
4
|
+
using System;
|
|
5
|
+
using System.Runtime.InteropServices;
|
|
6
|
+
public static class CodeRelaxPowerLease {
|
|
7
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
8
|
+
static extern uint SetThreadExecutionState(uint flags);
|
|
9
|
+
public static void Hold() {
|
|
10
|
+
if (SetThreadExecutionState(0x80000001u) == 0)
|
|
11
|
+
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
|
|
12
|
+
try {
|
|
13
|
+
Console.WriteLine("ready");
|
|
14
|
+
Console.Out.Flush();
|
|
15
|
+
Console.In.ReadLine();
|
|
16
|
+
} finally { SetThreadExecutionState(0x80000000u); }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
'@
|
|
20
|
+
[CodeRelaxPowerLease]::Hold()
|
package/dist/src/server.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import crypto from 'node:crypto';
|
|
|
2
2
|
import { RustDeskVideoProbe } from './platform/windows/rustdesk-video-probe.mjs';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import http from 'node:http';
|
|
5
|
+
import { createPowerManager } from './service-power.mjs';
|
|
5
6
|
import path from 'node:path';
|
|
6
7
|
import os from 'node:os';
|
|
7
8
|
import { fileURLToPath } from 'node:url';
|
|
@@ -307,6 +308,7 @@ async function handleRequest(request, response) {
|
|
|
307
308
|
const executionReady = executionHealth.state.ready && sharedServer?.ready === true;
|
|
308
309
|
sendJson(response, 200, {
|
|
309
310
|
status: 'ok', resident: true, version: runtimeVersion,
|
|
311
|
+
keepAwake: powerManager.snapshot(),
|
|
310
312
|
execution: { ...executionHealth.state, ready: executionReady, canWrite: executionReady,
|
|
311
313
|
mode: executionMode },
|
|
312
314
|
managedAppServer: managedAppServer.snapshot(),
|
|
@@ -975,6 +977,7 @@ const lanSocket = attachLanSocket(server, { token: options.token, dispatch: hand
|
|
|
975
977
|
});
|
|
976
978
|
|
|
977
979
|
let control;
|
|
980
|
+
const powerManager = createPowerManager(stateRoot);
|
|
978
981
|
void managedAppServer.start();
|
|
979
982
|
executionHealth.start();
|
|
980
983
|
runtimeDiscovery.start();
|
|
@@ -984,10 +987,14 @@ server.on('error', error => {
|
|
|
984
987
|
});
|
|
985
988
|
server.listen(options.port, options.lan ? '0.0.0.0' : '127.0.0.1', async () => {
|
|
986
989
|
try {
|
|
990
|
+
await powerManager.start();
|
|
987
991
|
if (process.env.CODEX_REMOTE_INSTANCE && process.env.CODEX_REMOTE_CONTROL_SECRET && process.env.CODEX_REMOTE_READY_PATH) {
|
|
988
992
|
control = await openControl({ instanceId: process.env.CODEX_REMOTE_INSTANCE,
|
|
989
993
|
secret: process.env.CODEX_REMOTE_CONTROL_SECRET, shutdown: shutDown, actions: {
|
|
990
994
|
'refresh-execution': refreshExecution,
|
|
995
|
+
'keep-awake-status': () => powerManager.snapshot(),
|
|
996
|
+
'keep-awake-enable': () => powerManager.setEnabled(true),
|
|
997
|
+
'keep-awake-disable': () => powerManager.setEnabled(false),
|
|
991
998
|
'relay-start': () => relayManager.run('relay-start'),
|
|
992
999
|
'relay-status': () => relayManager.run('relay-status'),
|
|
993
1000
|
'relay-stop': () => relayManager.run('relay-stop'),
|
|
@@ -1021,6 +1028,7 @@ let shuttingDown = false;
|
|
|
1021
1028
|
async function shutDown() {
|
|
1022
1029
|
if (shuttingDown) return;
|
|
1023
1030
|
shuttingDown = true;
|
|
1031
|
+
await powerManager.close();
|
|
1024
1032
|
desktopHelper?.release();
|
|
1025
1033
|
lanSocket.close();
|
|
1026
1034
|
server.close();
|
|
@@ -38,6 +38,12 @@ export async function doctor({ paths, host, service, skillTarget, ensureAutostar
|
|
|
38
38
|
await check('service', async () => {
|
|
39
39
|
const state = await service.status();
|
|
40
40
|
serviceState = state;
|
|
41
|
+
if (state.keepAwake) {
|
|
42
|
+
const power = state.keepAwake;
|
|
43
|
+
add('keep-awake', power.enabled && !power.active ? 'warning' : 'ok',
|
|
44
|
+
power.active ? '保持唤醒已生效,允许屏幕熄灭。' : !power.enabled ? '保持唤醒已由 CLI 关闭。'
|
|
45
|
+
: `保持唤醒未生效:${power.error || '未知原因'};可执行 relax keep-awake on 重新申请。`);
|
|
46
|
+
}
|
|
41
47
|
add('service', state.running ? 'ok' : 'error', state.running
|
|
42
48
|
? `本机服务正常:${state.mode},端口 ${state.port},运行版本:${state.version || '未知'}` : '服务未就绪;安装后执行 relax start。');
|
|
43
49
|
if (state.running && installedVersion && state.version !== installedVersion) add('service-version', 'warning', state.version
|
|
@@ -78,7 +84,7 @@ export async function doctor({ paths, host, service, skillTarget, ensureAutostar
|
|
|
78
84
|
}
|
|
79
85
|
|
|
80
86
|
export function formatDoctorOutput(result, detail = false) {
|
|
81
|
-
const labels = { installation: '安装文件', 'bridge-runtime': '运行环境', network: '局域网',
|
|
87
|
+
const labels = { 'keep-awake': '保持唤醒', installation: '安装文件', 'bridge-runtime': '运行环境', network: '局域网',
|
|
82
88
|
autostart: '登录自启动', service: '工作站服务', 'managed-app-server': 'Codex 内核',
|
|
83
89
|
'service-version': '服务版本', 'managed-app-server-update': 'Codex 更新',
|
|
84
90
|
'managed-app-server-launcher': '工作站更新', skill: 'Codex Skill' };
|
|
@@ -57,6 +57,7 @@ export function lifecycle({ stateRoot, host }) {
|
|
|
57
57
|
return { running: running && result?.status === 'ok', processRunning: running, pid: session?.pid,
|
|
58
58
|
port: session?.port, mode: result?.transportMode, version: result?.version, execution: result?.execution,
|
|
59
59
|
managedAppServer: result?.managedAppServer, desktop: result?.desktop,
|
|
60
|
+
keepAwake: result?.keepAwake,
|
|
60
61
|
desktopRestartRecommended: result?.desktopRestartRecommended === true,
|
|
61
62
|
resident: result?.resident === true };
|
|
62
63
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readRecord, writeRecord } from './service-lifecycle.mjs';
|
|
3
|
+
import { launchPowerInhibitor } from './platform/power-inhibitor.mjs';
|
|
4
|
+
|
|
5
|
+
export const powerConfigPath = stateRoot => path.join(stateRoot, 'keep-awake.json');
|
|
6
|
+
export const powerEnabled = stateRoot => readRecord(powerConfigPath(stateRoot))?.enabled !== false;
|
|
7
|
+
|
|
8
|
+
export function createPowerManager(stateRoot, { launch = launchPowerInhibitor, report = console.error } = {}) {
|
|
9
|
+
let enabled = powerEnabled(stateRoot), active = false, error = '', child = null, closed, stopping = false;
|
|
10
|
+
let queue = Promise.resolve();
|
|
11
|
+
const snapshot = () => ({ enabled, active, error });
|
|
12
|
+
async function release() {
|
|
13
|
+
if (!child) return;
|
|
14
|
+
stopping = true;
|
|
15
|
+
child.stdin.end();
|
|
16
|
+
await closed;
|
|
17
|
+
}
|
|
18
|
+
async function acquire() {
|
|
19
|
+
if (child || !enabled) return;
|
|
20
|
+
error = '';
|
|
21
|
+
try {
|
|
22
|
+
const process = launch();
|
|
23
|
+
stopping = false;
|
|
24
|
+
child = process;
|
|
25
|
+
let stderr = '', output = '';
|
|
26
|
+
process.stderr.on('data', data => { stderr = (stderr + data).slice(-2048); });
|
|
27
|
+
process.stdin.on('error', cause => { if (cause.code !== 'EPIPE') report(`保持唤醒通道异常:${cause.message}`); });
|
|
28
|
+
closed = new Promise(resolve => process.once('close', code => {
|
|
29
|
+
if (active && !stopping) { error = stderr.trim() || `保持唤醒进程意外退出(${code})`; report(error); }
|
|
30
|
+
child = null; active = false; resolve();
|
|
31
|
+
}));
|
|
32
|
+
await new Promise((resolve, reject) => {
|
|
33
|
+
const timer = setTimeout(() => { process.kill(); reject(new Error('保持唤醒申请超时')); }, 8000);
|
|
34
|
+
const finish = cause => { clearTimeout(timer); cause ? reject(cause) : resolve(); };
|
|
35
|
+
process.once('error', finish);
|
|
36
|
+
process.once('close', code => {
|
|
37
|
+
finish(new Error(stderr.trim() || `保持唤醒进程已退出(${code})`));
|
|
38
|
+
});
|
|
39
|
+
process.stdout.on('data', data => {
|
|
40
|
+
output += data;
|
|
41
|
+
if (output.includes('ready\n') || output.includes('ready\r\n')) { active = true; finish(); }
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
} catch (cause) { error = cause.message; report(`保持唤醒未生效:${error}`); }
|
|
45
|
+
}
|
|
46
|
+
const serial = task => { const result = queue.then(task); queue = result.catch(() => {}); return result; };
|
|
47
|
+
return {
|
|
48
|
+
snapshot,
|
|
49
|
+
start: () => serial(async () => { await acquire(); return snapshot(); }),
|
|
50
|
+
setEnabled: value => serial(async () => {
|
|
51
|
+
writeRecord(powerConfigPath(stateRoot), { enabled: value });
|
|
52
|
+
enabled = value;
|
|
53
|
+
if (enabled) await acquire(); else { await release(); error = ''; }
|
|
54
|
+
return snapshot();
|
|
55
|
+
}),
|
|
56
|
+
close: () => serial(async () => { enabled = false; await release(); }),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -13,7 +13,7 @@ const writerConflictMessages = ['already has an active writer', 'already has a l
|
|
|
13
13
|
|
|
14
14
|
export class ThreadWriterConflictError extends Error {
|
|
15
15
|
constructor(threadId, message) {
|
|
16
|
-
super('
|
|
16
|
+
super('该对话存在写入冲突,请稍后重试。');
|
|
17
17
|
this.name = 'ThreadWriterConflictError';
|
|
18
18
|
this.code = 'thread_writer_owned_elsewhere';
|
|
19
19
|
this.threadId = threadId;
|
|
@@ -37,6 +37,7 @@ export class SharedAppServer extends EventEmitter {
|
|
|
37
37
|
this.serverVersion = '';
|
|
38
38
|
this.epoch = randomUUID();
|
|
39
39
|
this.sequence = 0;
|
|
40
|
+
this.connectionId = null;
|
|
40
41
|
this.pending = new Map();
|
|
41
42
|
this.serverRequests = new serverRequests.Store(randomUUID(), id => {
|
|
42
43
|
this.emit('requests-changed', id);
|
|
@@ -64,6 +65,7 @@ export class SharedAppServer extends EventEmitter {
|
|
|
64
65
|
async open() {
|
|
65
66
|
const socket = await this.openSocket();
|
|
66
67
|
this.socket = socket;
|
|
68
|
+
this.connectionId = randomUUID();
|
|
67
69
|
this.ready = false;
|
|
68
70
|
this.serverVersion = '';
|
|
69
71
|
this.serverRequests.reset(randomUUID());
|
|
@@ -78,12 +80,16 @@ export class SharedAppServer extends EventEmitter {
|
|
|
78
80
|
clearTimeout(pending.timer);
|
|
79
81
|
if (message.error) {
|
|
80
82
|
const text = String(message.error.message || 'app-server 请求失败');
|
|
83
|
+
this.traceLifecycle('error', pending, { rpcCode: message.error.code,
|
|
84
|
+
writerError: writerConflictMessages.find(value => text.toLowerCase().includes(value)) || null });
|
|
81
85
|
pending.reject(writerConflictMessages.some(value => text.toLowerCase().includes(value))
|
|
82
86
|
? new ThreadWriterConflictError(pending.threadId, text)
|
|
83
87
|
: Object.assign(new Error(text), { rpcCode: message.error.code, details: message.error.data,
|
|
84
88
|
...(/^invalid cursor:/i.test(text) ? { code: 'history_cursor_expired' } : {}) }));
|
|
85
89
|
}
|
|
86
90
|
else {
|
|
91
|
+
this.traceLifecycle('complete', pending, { status: message.result?.status,
|
|
92
|
+
resultThreadId: message.result?.thread?.id });
|
|
87
93
|
try { pending.resolve(pending.transform(message.result)); }
|
|
88
94
|
catch (error) { pending.reject(error); }
|
|
89
95
|
}
|
|
@@ -93,6 +99,7 @@ export class SharedAppServer extends EventEmitter {
|
|
|
93
99
|
this.serverVersion = '';
|
|
94
100
|
for (const pending of this.pending.values()) {
|
|
95
101
|
clearTimeout(pending.timer);
|
|
102
|
+
this.traceLifecycle('disconnected', pending);
|
|
96
103
|
pending.reject(new Error('Codex 内核连接断开;发送结果可能未确认,请检查对话后再操作。'));
|
|
97
104
|
}
|
|
98
105
|
this.pending.clear();
|
|
@@ -121,24 +128,44 @@ export class SharedAppServer extends EventEmitter {
|
|
|
121
128
|
|
|
122
129
|
request(method, params, transform = value => value) {
|
|
123
130
|
const id = ++this.sequence;
|
|
131
|
+
const diagnostic = { method, requestId: id, threadId: params?.threadId, connectionId: this.connectionId,
|
|
132
|
+
...(method === 'thread/settings/update' && params?.serviceTier !== undefined ? { serviceTier: params.serviceTier } : {}) };
|
|
133
|
+
this.traceLifecycle('begin', diagnostic);
|
|
124
134
|
return new Promise((resolve, reject) => {
|
|
125
135
|
const timer = setTimeout(() => {
|
|
126
136
|
const error = Object.assign(new Error(`${method} 超时;不会自动重发,请先检查对话。`),
|
|
127
137
|
{ code: 'app_server_request_timeout' });
|
|
138
|
+
this.traceLifecycle('timeout', diagnostic);
|
|
128
139
|
if ((method === 'turn/start' || method === 'turn/steer') && this.pending.has(id)) {
|
|
129
140
|
messageDelivery.retainLateResult(this.pending.get(id), error);
|
|
130
141
|
} else { this.pending.delete(id); reject(error); }
|
|
131
142
|
}, 30000);
|
|
132
|
-
this.pending.set(id, { resolve, reject, timer, transform,
|
|
143
|
+
this.pending.set(id, { resolve, reject, timer, transform, ...diagnostic });
|
|
133
144
|
this.socket.send(JSON.stringify({ id, method, params }), error => {
|
|
134
145
|
const pending = this.pending.get(id);
|
|
135
146
|
if (!error || !pending) return;
|
|
147
|
+
this.traceLifecycle('send-error', diagnostic);
|
|
136
148
|
this.pending.delete(id);
|
|
137
149
|
clearTimeout(timer); pending.reject(error);
|
|
138
150
|
});
|
|
139
151
|
});
|
|
140
152
|
}
|
|
141
153
|
|
|
154
|
+
traceLifecycle(phase, request, extra = {}) {
|
|
155
|
+
if (!['thread/start', 'thread/resume', 'thread/unsubscribe', 'thread/settings/update', 'thread/settings/updated'].includes(request.method)) return;
|
|
156
|
+
const threadId = value => typeof value === 'string' && /^[\w-]{1,80}$/.test(value) ? value : null;
|
|
157
|
+
const identity = this.diagnosticIdentity?.() || {};
|
|
158
|
+
console.info('[thread-lifecycle]', JSON.stringify({ time: new Date().toISOString(), bridgePid: process.pid,
|
|
159
|
+
kernelPid: identity.kernelPid ?? null, kernelCreated: identity.kernelCreated ?? null,
|
|
160
|
+
connectionId: request.connectionId, serverVersion: this.serverVersion, phase,
|
|
161
|
+
method: request.method, requestId: request.requestId, threadId: threadId(request.threadId),
|
|
162
|
+
...([null, 'default', 'priority', 'fast'].includes(request.serviceTier) ? { serviceTier: request.serviceTier } : {}),
|
|
163
|
+
...(extra.resultThreadId ? { resultThreadId: threadId(extra.resultThreadId) } : {}),
|
|
164
|
+
...(typeof extra.status === 'string' && /^[\w-]{1,40}$/.test(extra.status) ? { status: extra.status } : {}),
|
|
165
|
+
...(typeof extra.rpcCode === 'number' ? { rpcCode: extra.rpcCode } : {}),
|
|
166
|
+
...(extra.writerError ? { writerError: extra.writerError } : {}) }));
|
|
167
|
+
}
|
|
168
|
+
|
|
142
169
|
async projects() {
|
|
143
170
|
await this.connect();
|
|
144
171
|
return events.readProjects((method, params) => this.request(method, params));
|
|
@@ -190,6 +217,7 @@ export class SharedAppServer extends EventEmitter {
|
|
|
190
217
|
const id = params.threadId;
|
|
191
218
|
if (method === 'thread/settings/updated') {
|
|
192
219
|
const settings = params.threadSettings;
|
|
220
|
+
this.traceLifecycle('notification', { method, threadId: id, connectionId: this.connectionId, serviceTier: settings.serviceTier ?? null });
|
|
193
221
|
this.contexts.set(id, { ...this.contexts.get(id), model: settings.model, thinking: settings.effort, serviceTier: settings.serviceTier ?? null,
|
|
194
222
|
cwd: settings.cwd, ...events.permissionState(settings) });
|
|
195
223
|
this.settingsNotifications.set(id, settings);
|
|
@@ -315,7 +343,7 @@ export class SharedAppServer extends EventEmitter {
|
|
|
315
343
|
} catch (error) {
|
|
316
344
|
snapshot?.close();
|
|
317
345
|
if (!isThreadWriterConflict(error)) throw error;
|
|
318
|
-
record = await this.readOnlyRecord(threadId, pageOptions, '
|
|
346
|
+
record = await this.readOnlyRecord(threadId, pageOptions, '该对话存在写入冲突,请稍后重试。');
|
|
319
347
|
result = null;
|
|
320
348
|
}
|
|
321
349
|
if (result) try {
|
|
@@ -390,11 +418,18 @@ export class SharedAppServer extends EventEmitter {
|
|
|
390
418
|
slot.tail = slot.tail.then(async () => {
|
|
391
419
|
if (this.subscriptions.get(threadId) !== slot) return;
|
|
392
420
|
if (slot.users || slot.readers || this.threads.get(threadId)?.tokens.size || this.keepThreadLoaded(threadId, this.threads.get(threadId))) return;
|
|
393
|
-
this.threads.delete(threadId); this.contexts.delete(threadId); this.permissionOptions.delete(threadId);
|
|
394
|
-
this.subscriptions.delete(threadId); clearTimeout(slot.expiry);
|
|
395
421
|
if (this.ready && !record?.readOnly) await this.unsubscribeThread(threadId);
|
|
422
|
+
// Keep this queue registered until unsubscribe completes. A returning reader
|
|
423
|
+
// must enqueue its resume behind it, rather than creating a second queue.
|
|
424
|
+
if (this.subscriptions.get(threadId) !== slot) return;
|
|
425
|
+
this.threads.delete(threadId); this.contexts.delete(threadId); this.permissionOptions.delete(threadId);
|
|
426
|
+
if (!slot.users && !slot.readers) this.subscriptions.delete(threadId);
|
|
427
|
+
clearTimeout(slot.expiry);
|
|
396
428
|
this.serverRequests.forget(threadId);
|
|
397
|
-
}).catch(error =>
|
|
429
|
+
}).catch(error => {
|
|
430
|
+
if (this.subscriptions.get(threadId) === slot && this.threads.get(threadId) === record && record) record.needsReload = true;
|
|
431
|
+
this.emit('subscription-error', error);
|
|
432
|
+
});
|
|
398
433
|
}
|
|
399
434
|
|
|
400
435
|
keepThreadLoaded(threadId, record) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@walkhi/code-relax",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.35",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"tag": "beta"
|
|
42
42
|
},
|
|
43
43
|
"optionalDependencies": {
|
|
44
|
-
"@walkhi/code-relax-native-win32-x64": "0.1.0-beta.
|
|
45
|
-
"@walkhi/code-relax-native-darwin-arm64": "0.1.0-beta.
|
|
46
|
-
"@walkhi/code-relax-native-darwin-x64": "0.1.0-beta.
|
|
44
|
+
"@walkhi/code-relax-native-win32-x64": "0.1.0-beta.35",
|
|
45
|
+
"@walkhi/code-relax-native-darwin-arm64": "0.1.0-beta.35",
|
|
46
|
+
"@walkhi/code-relax-native-darwin-x64": "0.1.0-beta.35"
|
|
47
47
|
}
|
|
48
48
|
}
|