xuym-codex-collab 0.10.0 → 0.10.1

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/README.md CHANGED
@@ -15,6 +15,8 @@ codex-collab setup
15
15
 
16
16
  安装后,先运行 `codex-collab setup`。已有业务项目时,进入项目目录再运行可自动带入信息;没有本机 clone 时可让向导拉取。以后直接运行 `codex-collab` 也会打开向导。上面的 `--foreground-scripts` 会让 npm 显示安装完成后的下一步提示。安装过程不会自动打开浏览器或修改项目;未使用此参数时 npm 可能隐藏提示,使用 `--ignore-scripts` 时会跳过提示,都可以直接运行 `codex-collab setup` 开始。
17
17
 
18
+ 升级 npm 包后重新运行 `codex-collab setup`,向导会检查后台服务版本,自动切换到本次安装的版本并打开新链接。同版本重复打开会复用现有服务,已有项目配置、对话绑定和独立运行的机器人连接保留。
19
+
18
20
  向导每屏只处理一个动作。管理员选择“接入一个项目”,确认目录和自动识别的分支、验证命令,再填写 Bot ID/Secret;成员选择“接受同事的邀请”,读取接入包后确认项目目录。群身份验证、等待审批和本机配置会自动衔接。最后复制指令到指定 Codex 对话,页面依据实际绑定和轮询记录显示完成。已在使用旧配置的成员可在首页“其他接入方式”中选择“导入已有配置”。命令启动后台工作台后退出,可以关闭终端和浏览器。macOS 默认设置登录自启;Linux 尝试配置 systemd 用户服务,失败时明确提示。工作台每 15 秒检查一次已开启的机器人进程,意外退出时恢复并在失败后退避重试;手动停止的连接保持停止。
19
21
 
20
22
  | 谁 | 在工作台做什么 | 接入后需要保留什么 |
@@ -8,8 +8,11 @@ import { SetupManager, savePrivate } from './manager.mjs';
8
8
  import { RunnerSupervisor } from './supervisor.mjs';
9
9
  import { workspaceHome } from './workspace.mjs';
10
10
  import { configureStartup, startupStatus } from './startup.mjs';
11
- import { alive } from '../peer/process.mjs';
11
+ import { alive, acquireRunnerControl } from '../peer/process.mjs';
12
12
  const entry = fileURLToPath(import.meta.url);
13
+ const { version } = JSON.parse(await readFile(new URL('../../package.json', import.meta.url), 'utf8'));
14
+ const runtime = { version, entry, node: process.execPath };
15
+ const sameRuntime = (state) => Object.entries(runtime).every(([key, value]) => state.runtime?.[key] === value);
13
16
  function openBrowser(url) {
14
17
  const command = process.platform === 'darwin' ? ['open', url] : process.platform === 'win32' ? ['cmd', '/c', 'start', '', url] : ['xdg-open', url];
15
18
  const child = spawn(command[0], command.slice(1), { stdio: 'ignore' }); child.on('error', () => {}); child.unref();
@@ -38,7 +41,7 @@ export async function runService({ home = workspaceHome() } = {}) {
38
41
  try {
39
42
  const manager = new SetupManager({ home });
40
43
  app = await startSetupServer({ manager });
41
- await lock.writeFile(JSON.stringify({ pid: process.pid, url: app.url, started_at: new Date().toISOString() }) + '\n'); await lock.close();
44
+ await lock.writeFile(JSON.stringify({ pid: process.pid, url: app.url, started_at: new Date().toISOString(), runtime }) + '\n'); await lock.close();
42
45
  const supervisor = new RunnerSupervisor(manager);
43
46
  const tick = async () => {
44
47
  try { await manager.exclusive(() => supervisor.tick()); } catch { /* Local diagnostics show missing/invalid profiles without logging secrets. */ }
@@ -53,9 +56,49 @@ export async function runService({ home = workspaceHome() } = {}) {
53
56
  return { ...app, stop };
54
57
  } catch (error) { await lock.close().catch(() => {}); await rm(path, { force: true }); if (app) await app.close(); throw error; }
55
58
  }
59
+ async function stopOutdatedService(home, state) {
60
+ // A private lock can outlive its process. Authenticate the loopback service
61
+ // before signalling a legacy PID; never terminate an unrelated process.
62
+ let verified = false;
63
+ try {
64
+ const url = new URL(state.url);
65
+ if (url.protocol !== 'http:' || url.hostname !== '127.0.0.1' || !url.port || url.username || url.password || !/^#[a-f0-9]{64}$/.test(url.hash)) throw new Error();
66
+ const response = await fetch(`${url.origin}/api/session`, {
67
+ method: 'POST', redirect: 'error', signal: AbortSignal.timeout(5000),
68
+ headers: { Origin: url.origin, 'Content-Type': 'application/json' },
69
+ body: JSON.stringify({ token: url.hash.slice(1) }),
70
+ });
71
+ verified = response.ok && (await response.json()).ok === true;
72
+ } catch { /* Keep private service URLs and network errors out of diagnostics. */ }
73
+ if (!verified) throw new Error('无法确认旧版向导服务的身份,请检查后台服务后重试。');
74
+ const current = await serviceState(home);
75
+ if (!current || current.pid !== state.pid || current.url !== state.url) return;
76
+ try { process.kill(state.pid, 'SIGTERM'); }
77
+ catch (error) { if (error.code !== 'ESRCH') throw error; }
78
+ for (let attempt = 0; attempt < 150; attempt++) {
79
+ if (!alive(state.pid)) return;
80
+ await delay(100);
81
+ }
82
+ throw new Error('旧版向导服务尚未退出,请稍后重新运行 codex-collab setup。');
83
+ }
56
84
  export async function runLocalSetup({ open = true, home = workspaceHome(), cwd = process.cwd(), foreground = false, startup = true } = {}) {
57
- home = resolve(home); await savePrivate(join(home, 'context.json'), { cwd: resolve(cwd) });
85
+ home = resolve(home); await mkdir(home, { recursive: true, mode: 0o700 });
86
+ // Serialize launchers so two setup commands cannot both stop a service that
87
+ // another launcher has just upgraded. Runner processes keep their own locks.
88
+ const release = await acquireRunnerControl(join(home, 'launcher'), entry);
89
+ try {
90
+ await savePrivate(join(home, 'context.json'), { cwd: resolve(cwd) });
91
+ return await launchLocalSetup({ open, home, foreground, startup });
92
+ } finally { await release(); }
93
+ }
94
+ async function launchLocalSetup({ open, home, foreground, startup }) {
58
95
  let state = await serviceState(home);
96
+ if (state && !sameRuntime(state)) {
97
+ await stopOutdatedService(home, state);
98
+ state = await serviceState(home);
99
+ if (state && !sameRuntime(state)) throw new Error('向导服务正在更新,请稍后重新运行 codex-collab setup。');
100
+ console.log('已更新本机向导服务。');
101
+ }
59
102
  if (foreground) {
60
103
  if (state) { console.log(`本机工作台已运行:${state.url}`); if (open) openBrowser(state.url); return; }
61
104
  const app = await runService({ home }); if (app) { console.log(`Codex Collab 工作台:${app.url}`); if (open) openBrowser(app.url); } return app;
@@ -0,0 +1,16 @@
1
+ # 0.10.1
2
+
3
+ 修复升级 npm 包后重新打开向导时,旧后台服务仍被复用,导致星尘资源无法加载、页面样式不完整的问题。旧服务持有旧的路由表,却读取安装目录里更新后的 HTML,新增资源会返回 401 或 404。
4
+
5
+ - 后台服务记录启动时的软件版本、入口和 Node 路径。setup 在复用服务前检查这些信息;旧版本和未记录版本的服务会经过本机令牌验证,再正常退出并由新版替换。
6
+ - 并发执行多个 setup 会串行处理升级,同版本继续复用同一个服务。无法确认旧服务身份或旧服务未及时退出时停止升级,不强制结束进程。
7
+ - 服务切换保留项目配置、Codex 对话绑定、机器人运行意图和独立 Runner;打开向导时使用切换后的链接。
8
+
9
+ 升级后运行:
10
+
11
+ ```sh
12
+ npm install --global xuym-codex-collab@0.10.1 --prefer-online
13
+ codex-collab setup
14
+ ```
15
+
16
+ 回归覆盖旧服务迁移、版本不一致时替换、并发打开、同版本复用,以及无法验证身份时保留原进程;检查升级后的全部星尘与 Three.js 资源可访问,业务 API 仍要求登录。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xuym-codex-collab",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "Git-backed collaboration tools for Codex teams, with MCP, WeCom, and local runners.",
5
5
  "repository": {
6
6
  "type": "git",