xuym-codex-collab 0.4.4 → 0.6.0
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 +23 -2
- package/apps/onboarding/bundle.mjs +1 -6
- package/apps/onboarding/commands.mjs +46 -0
- package/apps/onboarding/guided.mjs +3 -2
- package/apps/onboarding/main.mjs +72 -19
- package/apps/onboarding/manager.mjs +116 -55
- package/apps/onboarding/reset.mjs +3 -1
- package/apps/onboarding/server.mjs +1 -1
- package/apps/onboarding/startup.mjs +61 -0
- package/apps/onboarding/supervisor.mjs +41 -0
- package/apps/onboarding/web/app.js +78 -13
- package/apps/onboarding/web/index.html +17 -17
- package/apps/onboarding/web/style.css +3 -0
- package/apps/onboarding/workspace.mjs +36 -0
- package/apps/peer/desktop.mjs +4 -3
- package/apps/peer/intent.mjs +19 -0
- package/apps/peer/mcp.mjs +1 -1
- package/apps/peer/process.mjs +13 -0
- package/apps/peer/runner.mjs +4 -3
- package/apps/peer/runtime.mjs +6 -1
- package/apps/peer/wecom.mjs +1 -1
- package/bin/codex-collab.mjs +21 -9
- package/docs/desktop-poll.md +2 -2
- package/docs/onboarding-audit.md +12 -2
- package/docs/onboarding.md +37 -9
- package/docs/peer.md +4 -2
- package/docs/releases/0.5.0.md +14 -0
- package/docs/team-registry.md +7 -1
- package/docs/terminal-onboarding.md +3 -3
- package/package.json +1 -1
- package/packages/codex-kit/templates/.agents/skills/team-collab/SKILL.md +2 -2
- package/packages/codex-kit/templates/.agents/skills/team-collab-desktop/SKILL.md +2 -2
- package/packages/peer/config.mjs +2 -0
- package/packages/peer/git.mjs +14 -0
- package/packages/peer/protocol.mjs +4 -2
- package/packages/peer/repository.mjs +67 -0
- package/packages/peer/service.mjs +11 -8
- package/packages/peer/store.mjs +16 -16
- package/schemas/peer-task.schema.json +15 -13
- package/schemas/team-policy.schema.json +12 -1
- package/scripts/peer/cli.mjs +14 -8
- package/scripts/peer/init-agent.mjs +6 -15
- package/scripts/peer/onboarding.mjs +26 -20
- package/scripts/peer/refresh-project.mjs +23 -15
package/README.md
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
10
|
npm install --global xuym-codex-collab
|
|
11
|
-
codex-collab
|
|
11
|
+
codex-collab
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
`setup`
|
|
14
|
+
`setup` 打开本机网页工作台,所有接入操作集中在一个页面。管理员选择“连接我的机器人”,成员选择“接受同事邀请”;群身份验证、等待审批和本机配置会自动衔接,页面显示当前进度。已在使用旧配置的成员选择“导入已有配置”。命令启动后台工作台后退出,可以关闭终端和浏览器。macOS 默认设置登录自启;Linux 尝试配置 systemd 用户服务,失败时明确提示。工作台每 15 秒检查一次已开启的机器人进程,意外退出时恢复并在失败后退避重试;手动停止的连接保持停止。
|
|
15
15
|
|
|
16
16
|
| 谁 | 在工作台做什么 | 接入后需要保留什么 |
|
|
17
17
|
| --- | --- | --- |
|
|
@@ -20,6 +20,8 @@ codex-collab setup
|
|
|
20
20
|
|
|
21
21
|
管理员先在企业微信创建**独立智能机器人**并加入协作群,再填写 Bot ID/Secret。工具通过群验证码发现群 `chatid` 和真人 `userid`,不需要成员手工寻找这些 ID、生成密钥或编辑 JSON。机器人 Secret 通过加密接入包交给指定成员;批量分配自动生成密码,可在“已分配的机器人”查看并单独复制。接入说明不含密码和 Secret。显示名称支持中文,职责可从前端、后端、全栈、测试等预设中选择;内部 ID 和自定义标签放在高级选项中。
|
|
22
22
|
|
|
23
|
+
协作远程与业务基准分支自动识别;不要求 `origin/main`。有多个不同远程或默认分支不明确时,管理员选择一次。成员与新增机器人继承签名团队基准,各自可使用不同远程别名。每个新任务固定创建时的 commit,排队和重试期间不会漂移;管理员可在团队设置里为后续任务修改基准分支。
|
|
24
|
+
|
|
23
25
|
同一个真人可以拥有多个 Agent,每个 Agent 使用独立机器人、签名身份、业务 clone 和 Codex 对话。管理员在项目页点击“给自己增加机器人”,验证同一个企业微信账号后自动接入,无需下载包或填写包密码;给同事分配时仍使用加密接入包,同事可以领取多份。相同 userid 不会合并 Agent 权限,审核通知会指明应使用的机器人。
|
|
24
26
|
|
|
25
27
|
机器人在企业微信后台设为多人可用并加入协作群后,已批准入队的成员可以 `@` 任意团队机器人的自然语言需求;未入队的群成员仍会被工具拒绝。普通成员可提交需求、创建任务和查询状态,审核与取消、完成等控制操作继续受原有身份权限限制。接入完成后,团队成员可直接互相派发,不必再改机器人配置。
|
|
@@ -36,6 +38,25 @@ Skill 完成身份核对、对话绑定和 heartbeat 配置,页面自动观察
|
|
|
36
38
|
|
|
37
39
|
若管理员工作台显示接入包密码为空,通常是旧版本未保存原密码:无法从密文恢复,只能在管理员项目的“已分配的机器人”中重新生成包和密码。详情见[密码遗失恢复步骤](docs/onboarding.md#接入包密码遗失)。
|
|
38
40
|
|
|
41
|
+
## 日常使用与升级
|
|
42
|
+
|
|
43
|
+
在业务项目目录运行以下命令;只有一个接入时,也可以在任意目录运行。多个接入可通过 `codex-collab projects` 查看,再加 `--profile <项目ID>` 或唯一的 Agent ID 选择。原有显式 `peer.json` 路径参数继续支持。
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
codex-collab status
|
|
47
|
+
codex-collab doctor
|
|
48
|
+
codex-collab repair
|
|
49
|
+
codex-collab stop
|
|
50
|
+
codex-collab start
|
|
51
|
+
codex-collab autostart status
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`doctor` 检查项目 MCP/Skill、机器人进程与认证、对话绑定、真实轮询以及 Git 读取和推送权限,并给出下一步。页面也提供“检查整条连接”和“修复本机配置”。修复更新已识别的生成配置,并恢复缺失的 MCP 配置和 Skill;自定义、符号链接或已提交的共享配置需要手动合并。修复后请在 Codex 重新加载项目 MCP,在原对话执行启用指令。
|
|
55
|
+
|
|
56
|
+
升级执行 `npm install -g xuym-codex-collab@latest`,然后重新打开工作台并修复项目配置。已经运行的 Node 进程需要重启才能加载新代码:停止并重新启动机器人;后台工作台可等下次登录,或终止 `codex-collab service-status` 显示的工作台 PID 后重新执行 `codex-collab`。0.6.0 增加任务 v3 规格:团队全员须先升级并重启工作台、Runner 与 MCP,再创建新任务;历史任务保持原规则。0.5.0 迁移时只接管仍在运行的旧连接,已停止的旧连接需要你主动启动。
|
|
57
|
+
|
|
58
|
+
`codex-collab autostart off` 关闭登录自启,`autostart on` 重新设置。`setup --no-startup` 仅跳过本次自启配置,`setup --foreground` 以前台模式运行,便于排查。Windows 支持独立后台工作台,暂未自动配置登录启动。电脑休眠、关机或退出系统期间不会执行群任务;Codex 对话的自动化仍需保持启用。
|
|
59
|
+
|
|
39
60
|
## 一次任务如何接力
|
|
40
61
|
|
|
41
62
|
```mermaid
|
|
@@ -17,9 +17,4 @@ export function decryptBundle(value, password) {
|
|
|
17
17
|
return JSON.parse(Buffer.concat([decipher.update(Buffer.from(value.ciphertext, 'base64')), decipher.final()]).toString('utf8'));
|
|
18
18
|
} catch { throw new SetupError('接入包或密码无效,请确认文件完整且密码正确。'); }
|
|
19
19
|
}
|
|
20
|
-
export
|
|
21
|
-
const scp = remote.match(/^(?:[^@/]+@)?([^:/]+):(.+)$/);
|
|
22
|
-
try { const url = new URL(remote); if (['https:', 'http:', 'ssh:'].includes(url.protocol)) return `${url.hostname.toLowerCase()}/${url.pathname.replace(/^\/+|\/+$/g, '').replace(/\.git$/, '')}`; } catch {}
|
|
23
|
-
if (scp && !remote.startsWith('/')) return `${scp[1].toLowerCase()}/${scp[2].replace(/\/+$/g, '').replace(/\.git$/, '')}`;
|
|
24
|
-
return remote;
|
|
25
|
-
}
|
|
20
|
+
export { remoteIdentity } from '../../packages/peer/repository.mjs';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
2
|
+
import { SetupManager } from './manager.mjs';
|
|
3
|
+
import { selectProfile, workspaceHome, profiles } from './workspace.mjs';
|
|
4
|
+
import { configureStartup, startupStatus } from './startup.mjs';
|
|
5
|
+
import { serviceState } from './main.mjs';
|
|
6
|
+
|
|
7
|
+
// Preserve the original explicit peer.json interface for scripts and advanced users.
|
|
8
|
+
export async function resolveCommand(args, { home = workspaceHome(), cwd = process.cwd() } = {}) {
|
|
9
|
+
const [command, ...input] = args;
|
|
10
|
+
let selector; const rest = [];
|
|
11
|
+
for (let i = 0; i < input.length; i++) {
|
|
12
|
+
if (input[i] === '--profile') { if (!input[i + 1]) throw new Error('--profile 需要项目 ID 或 Agent ID。'); selector = input[++i]; }
|
|
13
|
+
else rest.push(input[i]);
|
|
14
|
+
}
|
|
15
|
+
if (rest[0] && (isAbsolute(rest[0]) || rest[0].endsWith('.json'))) return { command, configPath: resolve(rest.shift()), args: rest };
|
|
16
|
+
const profile = await selectProfile({ home, cwd, selector });
|
|
17
|
+
return { command, configPath: profile.config_path, profile, args: rest };
|
|
18
|
+
}
|
|
19
|
+
export async function workspaceCommand(args, { home = workspaceHome() } = {}) {
|
|
20
|
+
if (args[0] === 'projects') {
|
|
21
|
+
console.log(JSON.stringify((await profiles(home)).map(({ id, agent_id, repo, phase }) => ({ id, agent_id, repo, phase })), null, 2)); return true;
|
|
22
|
+
}
|
|
23
|
+
if (args[0] === 'autostart') {
|
|
24
|
+
if (!['on', 'off', 'status'].includes(args[1])) throw new Error('usage: codex-collab autostart on|off|status');
|
|
25
|
+
console.log(JSON.stringify(args[1] === 'status' ? await startupStatus(home) : await configureStartup({ home, enabled: args[1] === 'on' }), null, 2)); return true;
|
|
26
|
+
}
|
|
27
|
+
if (args[0] === 'service-status') {
|
|
28
|
+
const state = await serviceState(home);
|
|
29
|
+
console.log(JSON.stringify({ online: !!state, pid: state?.pid || null, startup: await startupStatus(home) }, null, 2)); return true;
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
export async function guidedCommand(resolved, { home = workspaceHome() } = {}) {
|
|
34
|
+
if (resolved.command === 'repair' && !resolved.profile) {
|
|
35
|
+
const { refreshProject } = await import('../../scripts/peer/refresh-project.mjs');
|
|
36
|
+
console.log(JSON.stringify(await refreshProject({ configPath: resolved.configPath }), null, 2)); return true;
|
|
37
|
+
}
|
|
38
|
+
if (!['doctor', 'repair'].includes(resolved.command) || !resolved.profile) return false;
|
|
39
|
+
const manager = new SetupManager({ home });
|
|
40
|
+
try {
|
|
41
|
+
const value = resolved.command === 'repair' ? await manager.repair({ id: resolved.profile.id }) : await manager.diagnostics({ id: resolved.profile.id });
|
|
42
|
+
console.log(JSON.stringify(value, null, 2));
|
|
43
|
+
if (resolved.command === 'doctor' && value.checks.some((check) => !check.ok)) process.exitCode = 1;
|
|
44
|
+
} finally { manager.close(); }
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { resolveRemote } from '../../packages/peer/repository.mjs';
|
|
1
2
|
import { randomBytes, randomUUID } from 'node:crypto';
|
|
2
3
|
import { lstat, mkdir, readFile, readdir, realpath, rename, rm, rmdir } from 'node:fs/promises';
|
|
3
4
|
import { homedir } from 'node:os';
|
|
4
5
|
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
5
6
|
import { git } from '../../packages/peer/store.mjs';
|
|
6
|
-
import { remoteIdentity } from './bundle.mjs';
|
|
7
7
|
import { SetupError } from './errors.mjs';
|
|
8
8
|
|
|
9
9
|
export const responsibilities = {
|
|
@@ -55,6 +55,7 @@ export async function recommendTests(repo) {
|
|
|
55
55
|
return commands.map((argv) => ({ argv, command: displayCommand(argv) }));
|
|
56
56
|
}
|
|
57
57
|
export function suggestedRepo(remote, agentId) {
|
|
58
|
+
if (typeof remote !== 'string' || !remote.trim()) throw new SetupError('请填写业务仓库地址。');
|
|
58
59
|
const name = remote.split(/[/:]/).at(-1).replace(/\.git$/, '').replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 80) || 'project';
|
|
59
60
|
return join(homedir(), 'codex-projects', `${name}-${agentId}`);
|
|
60
61
|
}
|
|
@@ -79,7 +80,7 @@ export async function cloneRepository({ remote, destination, privateHome }) {
|
|
|
79
80
|
if (existing.isSymbolicLink()) throw new SetupError('拉取目录不能是符号链接,请选择独立目录。');
|
|
80
81
|
try {
|
|
81
82
|
const actual = await realpath(target);
|
|
82
|
-
if (await git(actual, 'rev-parse', '--show-toplevel') === actual &&
|
|
83
|
+
if (await git(actual, 'rev-parse', '--show-toplevel') === actual && !!await resolveRemote(actual, { expectedRemote: remote })) return actual;
|
|
83
84
|
} catch { /* Reject existing unrelated folders instead of overwriting them. */ }
|
|
84
85
|
throw new SetupError('目标目录已存在,请选择新的目录,或切换到使用已有项目。');
|
|
85
86
|
}
|
package/apps/onboarding/main.mjs
CHANGED
|
@@ -1,32 +1,85 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { mkdir, readFile, rm } from 'node:fs/promises';
|
|
2
|
+
import { chmod, mkdir, open as openFile, readFile, rm } from 'node:fs/promises';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
6
|
import { startSetupServer } from './server.mjs';
|
|
6
|
-
import { SetupManager } from './manager.mjs';
|
|
7
|
+
import { SetupManager, savePrivate } from './manager.mjs';
|
|
8
|
+
import { RunnerSupervisor } from './supervisor.mjs';
|
|
9
|
+
import { workspaceHome } from './workspace.mjs';
|
|
10
|
+
import { configureStartup, startupStatus } from './startup.mjs';
|
|
7
11
|
import { alive } from '../peer/process.mjs';
|
|
12
|
+
const entry = fileURLToPath(import.meta.url);
|
|
8
13
|
function openBrowser(url) {
|
|
9
14
|
const command = process.platform === 'darwin' ? ['open', url] : process.platform === 'win32' ? ['cmd', '/c', 'start', '', url] : ['xdg-open', url];
|
|
10
15
|
const child = spawn(command[0], command.slice(1), { stdio: 'ignore' }); child.on('error', () => {}); child.unref();
|
|
11
16
|
}
|
|
12
|
-
export async function
|
|
13
|
-
|
|
17
|
+
export async function serviceState(home) {
|
|
18
|
+
for (let attempt = 0; attempt < 30; attempt++) {
|
|
19
|
+
try { const state = JSON.parse(await readFile(join(home, 'wizard.json'), 'utf8')); return alive(state.pid) ? state : null; }
|
|
20
|
+
catch (error) { if (error.code === 'ENOENT') return null; if (!(error instanceof SyntaxError)) throw error; }
|
|
21
|
+
await delay(50);
|
|
22
|
+
}
|
|
23
|
+
throw new Error('本机工作台正在初始化,请稍后重试。');
|
|
24
|
+
}
|
|
25
|
+
export async function runService({ home = workspaceHome() } = {}) {
|
|
26
|
+
process.umask(0o077); home = resolve(home); await mkdir(home, { recursive: true, mode: 0o700 });
|
|
14
27
|
const path = join(home, 'wizard.json'); let lock;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
28
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
29
|
+
try { lock = await openFile(path, 'wx', 0o600); break; }
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error.code !== 'EEXIST') throw error;
|
|
32
|
+
if (await serviceState(home)) return null;
|
|
33
|
+
await rm(path, { force: true });
|
|
34
|
+
}
|
|
21
35
|
}
|
|
22
|
-
|
|
36
|
+
if (!lock) throw new Error('无法取得本机工作台锁。');
|
|
37
|
+
let app, timer, stopping = false;
|
|
23
38
|
try {
|
|
24
|
-
|
|
25
|
-
await
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
39
|
+
const manager = new SetupManager({ home });
|
|
40
|
+
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();
|
|
42
|
+
const supervisor = new RunnerSupervisor(manager);
|
|
43
|
+
const tick = async () => {
|
|
44
|
+
try { await manager.exclusive(() => supervisor.tick()); } catch { /* Local diagnostics show missing/invalid profiles without logging secrets. */ }
|
|
45
|
+
if (!stopping) timer = setTimeout(tick, 15000);
|
|
46
|
+
};
|
|
47
|
+
timer = setTimeout(tick, 1000);
|
|
48
|
+
const stop = async () => {
|
|
49
|
+
if (stopping) return; stopping = true; clearTimeout(timer);
|
|
50
|
+
await app.close(); const state = await serviceState(home); if (state?.pid === process.pid) await rm(path, { force: true });
|
|
51
|
+
};
|
|
52
|
+
process.once('SIGINT', () => void stop()); process.once('SIGTERM', () => void stop());
|
|
53
|
+
return { ...app, stop };
|
|
31
54
|
} catch (error) { await lock.close().catch(() => {}); await rm(path, { force: true }); if (app) await app.close(); throw error; }
|
|
32
55
|
}
|
|
56
|
+
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) });
|
|
58
|
+
let state = await serviceState(home);
|
|
59
|
+
if (foreground) {
|
|
60
|
+
if (state) { console.log(`本机工作台已运行:${state.url}`); if (open) openBrowser(state.url); return; }
|
|
61
|
+
const app = await runService({ home }); if (app) { console.log(`Codex Collab 工作台:${app.url}`); if (open) openBrowser(app.url); } return app;
|
|
62
|
+
}
|
|
63
|
+
const previous = await startupStatus(home);
|
|
64
|
+
let registered = startup ? previous : { enabled: false };
|
|
65
|
+
if (startup && (previous.enabled || previous.kind == null && previous.supported && !previous.error)) registered = await configureStartup({ home, start: !state });
|
|
66
|
+
if (!state) {
|
|
67
|
+
state = await serviceState(home);
|
|
68
|
+
if (!state && !registered.enabled) {
|
|
69
|
+
const log = await openFile(join(home, 'service.log'), 'a', 0o600); await chmod(join(home, 'service.log'), 0o600);
|
|
70
|
+
const child = spawn(process.execPath, [entry, '--home', home], { detached: true, stdio: ['ignore', log.fd, log.fd], env: process.env });
|
|
71
|
+
let error; child.on('error', () => { error = true; }); child.unref(); await log.close();
|
|
72
|
+
for (let i = 0; i < 100 && !state && !error; i++) { await delay(100); state = await serviceState(home); }
|
|
73
|
+
} else for (let i = 0; i < 100 && !state; i++) { await delay(100); state = await serviceState(home); }
|
|
74
|
+
}
|
|
75
|
+
if (!state) throw new Error(`本机后台工作台未启动,请检查 ${join(home, 'service.log')},或运行 codex-collab setup --foreground。`);
|
|
76
|
+
console.log(`Codex Collab 工作台:${state.url}\n后台服务已运行,可以关闭终端和浏览器。${registered.enabled ? '登录时将自动恢复已开启的连接。' : '登录自启未启用。'}`);
|
|
77
|
+
if (registered.error) console.log(registered.error);
|
|
78
|
+
if (open) openBrowser(state.url);
|
|
79
|
+
return state;
|
|
80
|
+
}
|
|
81
|
+
if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) {
|
|
82
|
+
const args = process.argv.slice(2);
|
|
83
|
+
if (args.length !== 2 || args[0] !== '--home') throw new Error('Internal service requires --home.');
|
|
84
|
+
runService({ home: args[1] }).catch(() => { console.error('Codex Collab 后台服务启动失败,请运行 codex-collab setup --foreground 排查。'); process.exitCode = 1; });
|
|
85
|
+
}
|