xuym-codex-collab 0.3.0 → 0.4.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 -7
- package/apps/onboarding/manager.mjs +165 -24
- package/apps/onboarding/server.mjs +1 -1
- package/apps/onboarding/web/app.js +44 -18
- package/apps/onboarding/web/index.html +5 -3
- package/apps/peer/desktop.mjs +21 -0
- package/apps/peer/mcp.mjs +4 -2
- package/apps/peer/wecom.mjs +58 -12
- package/bin/codex-collab.mjs +8 -0
- package/docs/desktop-poll.md +32 -8
- package/docs/local-automatic-test.md +1 -1
- package/docs/local-peer-test.md +1 -1
- package/docs/onboarding-audit.md +88 -0
- package/docs/onboarding.md +33 -17
- package/docs/peer.md +11 -2
- package/docs/team-registry.md +13 -3
- package/docs/terminal-onboarding.md +1 -1
- package/package.json +1 -1
- package/packages/codex-kit/templates/.agents/skills/team-collab-desktop/SKILL.md +14 -9
- package/packages/peer/config.mjs +3 -2
- package/packages/peer/inbox.mjs +48 -0
- package/packages/peer/registry.mjs +5 -2
- package/packages/peer/review.mjs +2 -1
- package/packages/peer/service.mjs +70 -1
- package/packages/peer/store.mjs +90 -2
- package/schemas/peer-inbox-event.schema.json +284 -0
- package/schemas/peer-inbox.schema.json +63 -0
- package/schemas/team-invitation.schema.json +23 -1
- package/schemas/team-policy.schema.json +1 -1
- package/scripts/peer/onboarding.mjs +15 -3
- package/scripts/peer/refresh-project.mjs +76 -0
- package/scripts/peer/setup-desktop-project.mjs +1 -1
package/apps/peer/desktop.mjs
CHANGED
|
@@ -82,6 +82,19 @@ export class DesktopExecutor {
|
|
|
82
82
|
}
|
|
83
83
|
Object.assign(job, { source_commit: source, branch, worktree, status: 'prepared' });
|
|
84
84
|
}
|
|
85
|
+
teamContext() {
|
|
86
|
+
return { default_route: this.service.team.default_route, routes: this.service.team.routes,
|
|
87
|
+
default_workflow: this.config.wecom?.default_workflow || [this.service.team.default_route],
|
|
88
|
+
agents: Object.fromEntries(Object.entries(this.service.team.agents).map(([id, agent]) => [id, { role: agent.role, capabilities: agent.capabilities }])) };
|
|
89
|
+
}
|
|
90
|
+
async resolveRequest(threadId, requestId, expectedSeq, resolution) {
|
|
91
|
+
return this.private.locked(async (state) => {
|
|
92
|
+
this.checkBinding(state, threadId);
|
|
93
|
+
const request = (await this.service.listRequests()).find((item) => item.spec.id === requestId);
|
|
94
|
+
if (!request || !this.config.wecom?.chat_ids?.includes(request.spec.chat_id)) throw new Denied("request is outside this bot's configured groups");
|
|
95
|
+
return this.service.resolveRequest(requestId, expectedSeq, resolution, { defaultWorkflow: this.config.wecom?.default_workflow });
|
|
96
|
+
});
|
|
97
|
+
}
|
|
85
98
|
async poll(threadId) {
|
|
86
99
|
return this.private.locked(async (state) => {
|
|
87
100
|
this.checkBinding(state, threadId);
|
|
@@ -96,6 +109,14 @@ export class DesktopExecutor {
|
|
|
96
109
|
if (!current || current.state.status !== 'running' || current.state.assignee !== this.service.agentId || current.state.epoch !== job.token.epoch || this.service.store.clock() >= Date.parse(current.state.lease.expires_at)) job.status = 'superseded';
|
|
97
110
|
}
|
|
98
111
|
if (!task) {
|
|
112
|
+
const requests = await this.service.pendingRequests({ chatIds: this.config.wecom?.chat_ids || [] });
|
|
113
|
+
if (requests.length) {
|
|
114
|
+
await this.private.write(state);
|
|
115
|
+
return { agent_id: this.service.agentId, job: null, notify: true, team_context: this.teamContext(),
|
|
116
|
+
requests: requests.map((request) => ({ id: request.spec.id, expected_seq: request.state.seq, chat_id: request.spec.chat_id,
|
|
117
|
+
messages: [{ kind: 'request', content: request.spec.content }, ...request.events.filter((event) => ['clarified', 'replied'].includes(event.type)).map((event) => ({ kind: event.type === 'clarified' ? 'question' : 'reply', content: event.response || event.content }))] })),
|
|
118
|
+
instructions: 'Interpret requests in this bound conversation. Use resolve_desktop_request to create a structured task, ask a clarification, or answer without creating work. Then poll again; job=null alone does not mean idle.' };
|
|
119
|
+
}
|
|
99
120
|
const next = await this.service.nextJob({ executionMode: 'desktop_poll' });
|
|
100
121
|
if (!next) { await this.private.write(state); return { agent_id: this.service.agentId, job: null, notify: false }; }
|
|
101
122
|
try { task = await this.service.claim(next.spec.id, `desktop-claim:${this.service.agentId}:${next.spec.id}:${next.state.epoch + 1}`, { executionMode: 'desktop_poll' }); }
|
package/apps/peer/mcp.mjs
CHANGED
|
@@ -12,7 +12,7 @@ export function makePeerMcp(service, { config } = {}) {
|
|
|
12
12
|
tool('list_tasks', 'Read signed shared tasks directly from Git; no central HTTP service', z.object({}), () => service.list());
|
|
13
13
|
tool('read_task', 'Read the task, exact handoff commit, lease and workflow stage', z.object({ task_id: required }), ({ task_id }) => service.read(task_id));
|
|
14
14
|
tool('claim_desktop_task', 'Manually reserve the next stage of a waiting desktop-created task for this desktop identity. No automatic wakeup or lease expiry; requires latest seq and configured route eligibility. Other Runners cannot execute it once reserved.', z.object({ task_id: required, expected_seq: z.number().int().min(1), idempotency_key: required.optional() }), ({ task_id, expected_seq, idempotency_key }) => service.claimDesktop(task_id, expected_seq, idempotency_key));
|
|
15
|
-
tool('create_task', 'Create a signed task. By default the current desktop Codex owns workflow[0]; include a second configured role for a teammate. Call handoff_task after pushing code. Use execution_mode=automatic for Runner dispatch, or desktop_poll for the bound existing conversation heartbeat.', z.object({ title: required.max(200), description: z.string().max(4000).default(''), acceptance: z.array(required.max(1000)).min(1).max(32), workflow: z.array(required).min(1).max(16).optional(), execution_mode: z.enum(['desktop', 'automatic', 'desktop_poll']).default('desktop'), chat_id: required.optional(), idempotency_key: required.optional() }), ({ idempotency_key, chat_id, ...input }) => service.create({ ...input, chatId: chat_id }, idempotency_key));
|
|
15
|
+
tool('create_task', 'Create a signed task. By default the current desktop Codex owns workflow[0]; include a second configured role for a teammate. Call handoff_task after pushing code. Use execution_mode=automatic for Runner dispatch, or desktop_poll for the bound existing conversation heartbeat.', z.object({ title: required.max(200), description: z.string().max(4000).default(''), acceptance: z.array(required.max(1000)).min(1).max(32), workflow: z.array(required.max(64)).min(1).max(16).optional(), execution_mode: z.enum(['desktop', 'automatic', 'desktop_poll']).default('desktop'), chat_id: required.optional(), idempotency_key: required.optional() }), ({ idempotency_key, chat_id, ...input }) => service.create({ ...input, chatId: chat_id }, idempotency_key));
|
|
16
16
|
tool('handoff_task', 'Publish a pushed code commit and next-step summary for the next workflow role. Requires the next configured stage; never publishes private conversations.', z.object({ task_id: required, expected_seq: z.number().int().min(1), summary: required.max(4000), branch: required, commit: required, tests: required.max(1000), idempotency_key: required.optional() }), ({ task_id, expected_seq, idempotency_key, summary, branch, commit, tests }) => service.handoff(task_id, { summary, refs: { branch, commit, tests } }, expected_seq, idempotency_key));
|
|
17
17
|
tool('request_review', 'Publish the final stage code and test evidence for explicit human review; this does not approve or complete the task.', z.object({ task_id: required, expected_seq: z.number().int().min(1), summary: required.max(4000), branch: required, commit: required, tests: required.max(1000), idempotency_key: required.optional() }), ({ task_id, expected_seq, idempotency_key, summary, branch, commit, tests }) => service.requestReview(task_id, { summary, refs: { branch, commit, tests } }, expected_seq, idempotency_key));
|
|
18
18
|
if (config) {
|
|
@@ -22,7 +22,9 @@ export function makePeerMcp(service, { config } = {}) {
|
|
|
22
22
|
tool('bind_desktop_thread', 'Bind this agent to the actual current desktop conversation. Call once in the intended Alice/Bob thread, then separately create a heartbeat in THAT thread. Does not create an automation or launch a model.', z.object({ thread_id: thread }), ({ thread_id }) => desktop.bind(thread_id));
|
|
23
23
|
tool('desktop_status', 'Read private local desktop binding and job metadata, not credentials. Binding/thread IDs are never written to the Git task ledger.', z.object({}), () => desktop.status());
|
|
24
24
|
tool('unbind_desktop_thread', 'Explicitly remove the local binding only when no live job is held. Pause/delete the app heartbeat separately.', z.object({ thread_id: thread }), ({ thread_id }) => desktop.unbind(thread_id));
|
|
25
|
-
tool('poll_desktop_task', 'Called by the bound desktop thread heartbeat: atomically claim or resume one desktop_poll task and prepare its isolated worktree. Returns job=null
|
|
25
|
+
tool('poll_desktop_task', 'Called by the bound desktop thread heartbeat: atomically claim or resume one desktop_poll task and prepare its isolated worktree. Returns requests for natural-language intake before claiming new work. Only job=null AND no requests means idle. Execute the stage IN THIS conversation; no SDK session is launched.', z.object({ thread_id: thread }), ({ thread_id }) => desktop.poll(thread_id));
|
|
26
|
+
const taskSpec = z.object({ title: required.max(200), description: z.string().max(4000).default(''), acceptance: z.array(required.max(1000)).min(1).max(32), workflow: z.array(required.max(64)).min(1).max(16).optional() }).strict();
|
|
27
|
+
tool('resolve_desktop_request', 'Interpret a signed group request IN the bound conversation. Choose create_task with a structured task, clarify with a question, or answer without creating work (e.g. status/chatter). Source group and desktop_poll are enforced; creation and resolution are atomic/idempotent. Use the request expected_seq; no human approval tools are provided.', z.object({ thread_id: thread, request_id: required, expected_seq: z.number().int().min(1), action: z.enum(['create_task', 'clarify', 'answer']), response: required.max(2000).optional(), task: taskSpec.optional() }).strict(), ({ thread_id, request_id, expected_seq, ...resolution }) => desktop.resolveRequest(thread_id, request_id, expected_seq, resolution));
|
|
26
28
|
tool('renew_desktop_task', 'Renew a live task only while this bound conversation is actively working. Call before/after long editing or command operations. Bot uptime does not renew the lease.', job, ({ thread_id, task_id }) => desktop.renew(thread_id, task_id));
|
|
27
29
|
tool('finish_desktop_task', 'After explicitly committing code in the assigned worktree, run the trusted fixed test command, verify lease and ancestry, push, and publish a stage review, handoff or final review according to the pinned team policy. Does not approve, merge main, send arbitrary commands, or start the next conversation. Bounded foreground tests renew their lease.', job.extend({ summary: required.max(4000) }), ({ thread_id, task_id, summary }) => desktop.finish(thread_id, task_id, summary));
|
|
28
30
|
tool('fail_desktop_task', 'Report a concise sanitized stage failure; release the lease for configured fallback. Preserves the worktree. Never include private prose, secrets or raw errors.', job.extend({ summary: required.max(4000) }), ({ thread_id, task_id, summary }) => desktop.fail(thread_id, task_id, summary));
|
package/apps/peer/wecom.mjs
CHANGED
|
@@ -27,20 +27,42 @@ export async function dispatchPeerWecom(frame, { service, config, reply }) {
|
|
|
27
27
|
if (config.commands_enabled === false) return false;
|
|
28
28
|
const body = frame?.body;
|
|
29
29
|
if (body?.chattype !== 'group' || body.msgtype !== 'text' || !config.chat_ids.includes(body.chatid) || !body.msgid) return false;
|
|
30
|
-
|
|
30
|
+
if (config.bot_id && body.aibotid && body.aibotid !== config.bot_id) return false;
|
|
31
|
+
const raw = body.text?.content;
|
|
32
|
+
if (typeof raw !== 'string' || !raw.trim()) return false;
|
|
33
|
+
const command = parsePeerCommand(raw);
|
|
34
|
+
const content = raw.trim().replace(/^@[^\s@]{1,40}\s+/, '').trim();
|
|
31
35
|
// Refresh policy before authorization so newly approved admins/owners are visible.
|
|
32
36
|
if (service.store.registryRoot) await service.list();
|
|
33
37
|
const sender = body.from?.userid;
|
|
34
38
|
const owner = !!sender && sender === service.team.agents[service.agentId]?.wecom_userid;
|
|
35
39
|
const admin = config.allow_admin_commands === true && !!sender && Object.values(service.team.agents).some((agent) => agent.role === 'admin' && agent.wecom_userid === sender);
|
|
40
|
+
// Shared owners do not merge agent roles: the receiving bot selects its signer.
|
|
36
41
|
// Admins may dispatch/query via a member bot. Review mutations must be signed by
|
|
37
42
|
// the human reviewer's own identity, never by impersonating another member.
|
|
38
|
-
if (!owner && !(admin && ['create', 'status'].includes(command
|
|
43
|
+
if (!owner && !(admin && ['create', 'status', 'natural'].includes(command?.action || 'natural'))) return false;
|
|
39
44
|
const key = `wecom:${body.chatid}:${body.msgid}`;
|
|
40
45
|
let text;
|
|
41
46
|
try {
|
|
42
47
|
let task;
|
|
43
|
-
if (command
|
|
48
|
+
if (!command) {
|
|
49
|
+
// The authenticated smart-bot callback is addressed to this bot. Do not
|
|
50
|
+
// infer bot mentions from display names; validate aibotid when supplied.
|
|
51
|
+
if (/^协作(?:\s|$)/.test(content)) {
|
|
52
|
+
await reply(frame, '这条协作命令格式不完整,请使用通知中的完整命令;创建任务也可以直接 @我描述需求。'); return true;
|
|
53
|
+
}
|
|
54
|
+
if (!content || content.length > 4000) {
|
|
55
|
+
await reply(frame, '请用不超过 4000 字的文字描述需求。'); return true;
|
|
56
|
+
}
|
|
57
|
+
if (config.execution_mode !== 'desktop_poll') {
|
|
58
|
+
await reply(frame, '自然语言任务需要绑定已有 Codex 对话,并启用 desktop_poll 轮询。'); return true;
|
|
59
|
+
}
|
|
60
|
+
const request = await service.receiveRequest({ chatId: body.chatid, messageId: body.msgid, senderId: sender, content, allowAdmin: config.allow_admin_commands === true });
|
|
61
|
+
text = request.state.task_id ? `该消息已创建任务 ${request.state.task_id},不会重复创建。`
|
|
62
|
+
: request.state.status === 'answered' ? '这条消息已处理。'
|
|
63
|
+
: request.state.status === 'clarification' ? `需求 ${request.spec.id} 等待你补充说明,请查看群里的追问。`
|
|
64
|
+
: `已收到你的需求(${request.spec.id});等待绑定的 Codex 对话轮询理解和处理。`;
|
|
65
|
+
} else if (command.action === 'create') {
|
|
44
66
|
task = await service.create({ ...command, workflow: command.workflow || (config.default_workflow ? config.default_workflow : undefined), id: `T-${digest(key).slice(0, 32).toUpperCase()}`, chatId: body.chatid, execution_mode: config.execution_mode === 'desktop_poll' ? 'desktop_poll' : 'automatic' }, key);
|
|
45
67
|
text = task.spec.execution_mode === 'desktop_poll'
|
|
46
68
|
? `任务 ${task.spec.id} 已创建;按 ${task.spec.workflow.join(' → ')} 等待绑定的 Codex 桌面对话轮询认领。`
|
|
@@ -68,7 +90,7 @@ export async function dispatchPeerWecom(frame, { service, config, reply }) {
|
|
|
68
90
|
if (command.action === 'reopen') task = await service.report(command.id, { type: 'reopened', summary: command.summary }, {}, key);
|
|
69
91
|
text = `任务 ${task.spec.id}:${task.state.status};负责人 ${task.state.assignee || '待认领'};步骤 ${task.state.stage + 1}/${task.spec.workflow.length};轮次 ${task.state.epoch};版本 R-${task.state.seq}。`;
|
|
70
92
|
}
|
|
71
|
-
} catch { text = '协作操作未成功:请核对任务所属群、本人权限和本机 Git 连接。'; }
|
|
93
|
+
} catch (error) { text = error.code === 'AMBIGUOUS_INBOX_REPLY' ? '有多条需求等待补充,请 @我回复时带上对应的 N-需求ID。' : '协作操作未成功:请核对任务所属群、本人权限和本机 Git 连接。'; }
|
|
72
94
|
await reply(frame, text); return true;
|
|
73
95
|
}
|
|
74
96
|
|
|
@@ -92,15 +114,20 @@ export function formatPeerNotification(task, type, agentId, team) {
|
|
|
92
114
|
if (members.length) {
|
|
93
115
|
// WeCom's Markdown extension is best-effort on the smart-bot transport.
|
|
94
116
|
// Encrypted callback userids and actual native @ rendering need group verification.
|
|
95
|
-
|
|
117
|
+
const owners = new Map();
|
|
118
|
+
for (const id of members) {
|
|
96
119
|
const userid = team.agents[id].wecom_userid;
|
|
97
|
-
|
|
98
|
-
}
|
|
120
|
+
const ids = owners.get(userid) || []; ids.push(id); owners.set(userid, ids);
|
|
121
|
+
}
|
|
122
|
+
detail.push(`通知本人:${[...owners].map(([userid, ids]) => {
|
|
123
|
+
const names = ids.join('、');
|
|
124
|
+
return /^[a-zA-Z0-9_-]{1,128}$/.test(userid) ? `${names} <@${userid}>` : names;
|
|
125
|
+
}).join(';')}`);
|
|
99
126
|
}
|
|
100
127
|
if (type === 'handoff') detail.push(next());
|
|
101
128
|
else {
|
|
102
|
-
detail.push(`${type === 'stage_review_requested' ? '本阶段等待真人审核,通过后继续下一阶段。' : '全部阶段已提交,等待真人审核。'}\n审核人:${task.state.assignee};版本:R-${task.state.seq}`);
|
|
103
|
-
detail.push(`请审核人 @
|
|
129
|
+
detail.push(`${type === 'stage_review_requested' ? '本阶段等待真人审核,通过后继续下一阶段。' : '全部阶段已提交,等待真人审核。'}\n审核人:${task.state.assignee} 的所有者;审核身份:${task.state.assignee};版本:R-${task.state.seq}`);
|
|
130
|
+
detail.push(`请审核人 @ ${task.state.assignee} 对应的机器人发送:\n协作 审核 ${task.spec.id} R-${task.state.seq} 通过\n协作 审核 ${task.spec.id} R-${task.state.seq} 退回 | 修改意见`);
|
|
104
131
|
}
|
|
105
132
|
}
|
|
106
133
|
if (type === 'reviewed') {
|
|
@@ -146,7 +173,13 @@ export function startPeerWecom({ service, config, clientFactory = (options) => n
|
|
|
146
173
|
};
|
|
147
174
|
// MCP and Runner use independent processes. Relay committed events from either
|
|
148
175
|
// process; never rely on receiving another bot's group messages for scheduling.
|
|
149
|
-
const
|
|
176
|
+
const sendInbox = async (request, event) => {
|
|
177
|
+
if (!config.chat_ids.includes(request.spec.chat_id)) return;
|
|
178
|
+
const label = event.type === 'clarified' ? '需要补充说明' : event.type === 'task_created' ? '任务与分工确认' : '回复';
|
|
179
|
+
const content = `[Codex Collab · ${service.agentId}] ${request.spec.id} ${label}\n${event.type === 'task_created' ? `任务:${event.task_id}\n` : ''}${notificationText(event.response)}${event.type === 'clarified' ? '\n请 @这个机器人直接补充;有多条追问时附上需求 N-ID。' : ''}`;
|
|
180
|
+
assertReceipt(await client.sendMessage(request.spec.chat_id, { msgtype: 'markdown', markdown: { content } }));
|
|
181
|
+
};
|
|
182
|
+
const relay = new PeerNotificationRelay(service, send, sendInbox);
|
|
150
183
|
service.notify = () => relay.poll();
|
|
151
184
|
client.connect(); return { close: () => { setHealth('disconnected'); client.disconnect(); }, poll: () => relay.poll(), health: { snapshot: () => health, flush: () => pendingHealth } };
|
|
152
185
|
}
|
|
@@ -156,8 +189,8 @@ export function assertReceipt(frame) {
|
|
|
156
189
|
}
|
|
157
190
|
|
|
158
191
|
export class PeerNotificationRelay {
|
|
159
|
-
constructor(service, send) {
|
|
160
|
-
this.service = service; this.send = send;
|
|
192
|
+
constructor(service, send, sendInbox = async () => {}) {
|
|
193
|
+
this.service = service; this.send = send; this.sendInbox = sendInbox;
|
|
161
194
|
this.path = join(service.store.dataDir, 'wecom-notifications.json');
|
|
162
195
|
this.cursors = null; this.pending = Promise.resolve();
|
|
163
196
|
}
|
|
@@ -185,5 +218,18 @@ export class PeerNotificationRelay {
|
|
|
185
218
|
await rename(temp, this.path);
|
|
186
219
|
}
|
|
187
220
|
}
|
|
221
|
+
if (typeof this.service.listRequests === 'function') {
|
|
222
|
+
for (const request of await this.service.listRequests()) {
|
|
223
|
+
if (request.spec.target_agent !== this.service.agentId) continue;
|
|
224
|
+
const cursor = `inbox:${request.spec.id}`;
|
|
225
|
+
for (const event of request.events.slice(this.cursors[cursor] || 0)) {
|
|
226
|
+
if (['clarified', 'answered', 'task_created'].includes(event.type)) await this.sendInbox(request, event);
|
|
227
|
+
this.cursors[cursor] = event.seq;
|
|
228
|
+
const temp = `${this.path}.${process.pid}.tmp`;
|
|
229
|
+
await writeFile(temp, JSON.stringify(this.cursors) + '\n', { mode: 0o600 });
|
|
230
|
+
await rename(temp, this.path);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
188
234
|
}
|
|
189
235
|
}
|
package/bin/codex-collab.mjs
CHANGED
|
@@ -17,6 +17,7 @@ Usage:
|
|
|
17
17
|
codex-collab
|
|
18
18
|
codex-collab identity create <agent-id> [--dir /absolute/private/directory]
|
|
19
19
|
codex-collab wecom discover
|
|
20
|
+
codex-collab refresh-project /absolute/private-peer.json
|
|
20
21
|
codex-collab <doctor|start|stop|status|list|read|review|complete|reopen|...> /abs/private-peer.json [args]
|
|
21
22
|
|
|
22
23
|
setup opens the local web wizard. --terminal keeps the legacy terminal workflow. Never pass a bot Secret as a command argument.`);
|
|
@@ -68,6 +69,13 @@ async function main() {
|
|
|
68
69
|
return;
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
if (args[0] === 'refresh-project') {
|
|
73
|
+
if (args.length !== 2) throw new Error('usage: codex-collab refresh-project /absolute/private-peer.json');
|
|
74
|
+
const { refreshProject } = await import('../scripts/peer/refresh-project.mjs');
|
|
75
|
+
console.log(JSON.stringify(await refreshProject({ configPath: args[1] }), null, 2));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
71
79
|
if (args[0] === 'wecom') {
|
|
72
80
|
if (args.length !== 2 || args[1] !== 'discover') throw new Error('usage: codex-collab wecom discover');
|
|
73
81
|
await discoverWeCom();
|
package/docs/desktop-poll.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 企业微信任务 → 指定 Codex 桌面对话轮询执行
|
|
2
2
|
|
|
3
|
-
日常团队使用 [本机网页接入向导](onboarding.md)
|
|
3
|
+
日常团队使用 [本机网页接入向导](onboarding.md) 自动配置项目、群身份和机器人。本页保留隔离测试与底层轮询机制。已有配置可从网页导入,无需重绑对话。一个真人使用多个机器人建议走向导的“给自己增加机器人”,允许 Alice/Bob 共享 userid 并各自处理命令。本页的旧隔离测试脚本仍默认只给 Alice 设置群命令身份。
|
|
4
4
|
|
|
5
5
|
本模式是 `desktop_poll`,不是后台 SDK 自动模式,也不是旧的手动 `desktop` 保留。企业微信适配器负责入口/通知,Git 保存协议,**指定已有 Codex 对话的 heartbeat**负责调用 MCP 接单并在该对话执行。
|
|
6
6
|
|
|
@@ -77,7 +77,7 @@ cat "$root/bob-bind-prompt.txt"
|
|
|
77
77
|
|
|
78
78
|
提示词要求目标对话:确认 MCP 身份 → 获取当前真实 thread ID → `bind_desktop_thread` → 用应用 automation 工具在**这个已有对话**设置周期 heartbeat → 立即只读/无任务检查。不能仅凭项目目录猜对话 ID;若应用上下文/当前 shell 未暴露 ID,让用户提供目标对话 ID/链接,不读取或修改应用私有数据库。
|
|
79
79
|
|
|
80
|
-
每个 heartbeat 保存的提示词须含该身份、真实 thread_id、skill 路径;每次 `get_identity`、`poll_desktop_task
|
|
80
|
+
每个 heartbeat 保存的提示词须含该身份、真实 thread_id、skill 路径;每次 `get_identity`、`poll_desktop_task`。只有 `job=null` 且 `requests` 为空时保持安静;有需求时先理解并调用 `resolve_desktop_request` 创建任务、追问或回答,有任务时在本对话完成当前阶段并交接。创建前查已有 automation,避免重复。默认尝试一分钟周期,只有工具接受且真实触发后才算启用;否则说明限制。应用工具不可用时只完成绑定,不能声称已自动轮询。
|
|
81
81
|
|
|
82
82
|
绑定和 app heartbeat 是两件事:MCP 返回 `automation_created=false`;只能由应用工具完成定时设置。桌面绑定 ID/执行路径/token 只存本机 `desktop-state.json`,不进 Git。
|
|
83
83
|
|
|
@@ -90,7 +90,7 @@ node scripts/peer/cli.mjs start "$root/alice-peer.json"
|
|
|
90
90
|
node scripts/peer/cli.mjs start "$root/bob-peer.json"
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
日志应显示 `notifications only; automatic code execution disabled` 和两次各自的 `Peer WeCom authenticated
|
|
93
|
+
日志应显示 `notifications only; automatic code execution disabled` 和两次各自的 `Peer WeCom authenticated`。本页旧脚本默认 Alice 接本人的群命令,Bob 只发送通知。这是测试夹具的默认设置;新网页向导支持同一真人拥有两个可接群命令的 Agent,各自用独立机器人和密钥。真实同事应验证各自本人账号。
|
|
94
94
|
|
|
95
95
|
可用 CLI 检查“进程在线”和“企微已鉴权”是否是两个独立状态:
|
|
96
96
|
|
|
@@ -106,14 +106,16 @@ node scripts/peer/cli.mjs status "$root/bob-peer.json"
|
|
|
106
106
|
所有绑定/heartbeat 就绪后,在群里选择真实 Alice @,发**新任务**:
|
|
107
107
|
|
|
108
108
|
```text
|
|
109
|
-
@Alice
|
|
109
|
+
@Alice 给 greet.sh 增加 --upper 选项,默认和姓名参数的行为保持不变。
|
|
110
|
+
你只修改 greet.sh,Bob 接着扩展 test.sh,覆盖默认、姓名、大写默认、大写姓名四种行为。
|
|
111
|
+
sh test.sh 要通过,完成后在群里发总结。
|
|
110
112
|
```
|
|
111
113
|
|
|
112
|
-
|
|
114
|
+
不用填写工作流语法、分隔符或任务 ID。先配置 Alice 的 frontend 职责和 Bob 的 backend 职责;绑定对话根据团队路由解释这条消息,把职责与验收保存在结构化任务中。没有对应同事或分工不清时先在群里追问;不能只凭显示名猜测分配。旧 `协作 创建 [frontend>backend] ... | ...` 命令仍可直接创建任务。
|
|
113
115
|
|
|
114
116
|
验收要看到同一个新任务 ID:
|
|
115
117
|
|
|
116
|
-
1.
|
|
118
|
+
1. 群先回复“已收到你的需求”,给出 `N-需求ID`;下一次 Alice 对话轮询后,群里收到任务与分工确认及 `T-任务ID`。从此检查同一个 T-ID。
|
|
117
119
|
2. 指定 Alice 对话出现任务 ID、当前阶段和实际修改/测试记录;Alice Bot 发 claimed。
|
|
118
120
|
3. Alice `finish_desktop_task` 固定验证通过并 push;群里给出结果、SHA、测试。v3 默认先等待 Alice 本人审核,通过后才显示下一阶段优先 Bob;v2 或关闭中间审核时直接交接。
|
|
119
121
|
4. 指定 Bob 对话从 Alice 的准确 SHA 执行;Bob Bot 发 claimed、review_requested,并给出实际审核人与版本号。
|
|
@@ -129,7 +131,28 @@ node scripts/peer/cli.mjs status "$root/bob-peer.json"
|
|
|
129
131
|
@Alice 协作 完成 T-真实任务ID
|
|
130
132
|
```
|
|
131
133
|
|
|
132
|
-
示例 `R-7` 替换为最新通知版本;v2
|
|
134
|
+
示例 `R-7` 替换为最新通知版本;v2 任务仍兼容省略版本号的旧命令。本页旧夹具中没有群命令身份的 Bob 由 Alice 管理员审核;新向导中用相同本人账号验证的 Bob 默认通过 Bob 机器人审核,按当前通知操作。原生 @ 提醒需实群确认;审核与 Git 接力不依赖原生 @。
|
|
135
|
+
|
|
136
|
+
## 已有项目启用自然语言入口
|
|
137
|
+
|
|
138
|
+
已有绑定不需要重新创建身份。先处理正在执行的任务,再对每个 Agent 用其**项目 MCP 实际使用的私有配置路径**操作:
|
|
139
|
+
|
|
140
|
+
```sh
|
|
141
|
+
npm install --global xuym-codex-collab@0.4.0
|
|
142
|
+
codex-collab stop /absolute/private-peer.json
|
|
143
|
+
codex-collab refresh-project /absolute/private-peer.json
|
|
144
|
+
codex-collab start /absolute/private-peer.json
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
自然语言入口和 `refresh-project` 从 `0.4.0` 开始提供。从源码测试时,可将 `codex-collab` 替换为 `node /absolute/codex-collab/bin/codex-collab.mjs`。
|
|
148
|
+
|
|
149
|
+
`refresh-project` 更新可识别的项目 MCP 和 Skill,保留 provider、其他 MCP、签名身份和本机对话绑定,生成私有备份,并返回 `binding_prompt` 文件路径。重新加载该项目 MCP,把这个文件里的启用指令发送到**原绑定对话**,让它更新原协作 heartbeat 的提示词,再立即轮询一次。升级命令本身不操作应用 heartbeat。
|
|
150
|
+
|
|
151
|
+
网页工作台中继续检查/导入已有配置也会刷新可识别的安装;随后重新复制启用指令到原对话。定制 Skill、MCP、已提交的项目配置或其他身份配置不会被覆盖,提示手动合并后再继续。
|
|
152
|
+
|
|
153
|
+
新一次群消息到达后,Runner 先把需求保存到业务仓库 `agent-collab` 的 `.agent-collab/peer/inbox/N-ID/`,使用签名与消息 ID 去重。MCP 轮询返回原文、追问/回复历史和团队职责,由绑定对话理解;任务与解读结果在同一次 Git 提交中发布。原始群需求和处理结果由团队共享,完整私有 Codex 对话和绑定 ID 不写入 Git。
|
|
154
|
+
|
|
155
|
+
需求不清时机器人会追问:只有一条待补充需求,可直接 @它回复;多条时带上对应 N-ID。澄清等待中不产生执行任务;状态问题使用回答,不创建新任务。自然语言“通过/完成”只作为请求文字处理,审核、完成仍由真人发明确命令。运行中的任务优先恢复,新的需求会等活跃阶段结束后处理。响应时间取决于 heartbeat 实际轮询周期、对话是否可用和 Git 同步。
|
|
133
156
|
|
|
134
157
|
## MCP 与运行约束
|
|
135
158
|
|
|
@@ -137,7 +160,8 @@ node scripts/peer/cli.mjs status "$root/bob-peer.json"
|
|
|
137
160
|
| --- | --- |
|
|
138
161
|
| get_identity | 核对身份与模式 |
|
|
139
162
|
| bind_desktop_thread / desktop_status | 本机绑定与检查;不创建 app automation |
|
|
140
|
-
| poll_desktop_task |
|
|
163
|
+
| poll_desktop_task | 恢复活跃任务;否则返回待理解 requests,或原子认领任务;两者都无时 notify=false |
|
|
164
|
+
| resolve_desktop_request | 绑定对话按最新 expected_seq 创建结构化任务、追问或回答;不批准、完成或取消任务 |
|
|
141
165
|
| renew_desktop_task | 活跃执行时显式续租 |
|
|
142
166
|
| finish_desktop_task | 验证已提交代码、跑固定测试、push、发布下一阶段或待审核 |
|
|
143
167
|
| fail_desktop_task | 简洁脱敏失败;候补接力;保留工作树 |
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
| 企微连接 | Alice 接命令;Bob 发通知 | Alice 接命令并执行;Bob 不接命令但可执行并发通知 |
|
|
18
18
|
| Git | 私有本机 bare remote | 另一个私有本机 bare remote,不推送 GitHub |
|
|
19
19
|
|
|
20
|
-
**Bob 的 `commands_enabled=false` 只禁用群命令入口,不禁用自动执行。** 它不需要收到 Alice Bot 的 @ 消息,直接读取 Git 中的已签名 handoff
|
|
20
|
+
**Bob 的 `commands_enabled=false` 只禁用群命令入口,不禁用自动执行。** 它不需要收到 Alice Bot 的 @ 消息,直接读取 Git 中的已签名 handoff 就会接力。本页旧测试脚本仅给 Alice 绑定 userid,Bob 保持通知模式。新网页向导支持同一个 userid 拥有多个独立 Agent,并同时开启各机器人的群命令;模拟既有 Codex 对话接力请使用 [向导中的本人多机器人流程](onboarding.md#一个人使用多个机器人或模拟两个同事)。
|
|
21
21
|
|
|
22
22
|
## 1. 生成新的自动测试环境
|
|
23
23
|
|
package/docs/local-peer-test.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
| 两个本地 Runner | `auto_execute=false`,只连接机器人和中继通知 |
|
|
19
19
|
| `origin.git` | 本机共享 bare Git remote,不连接 GitHub |
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
本页保留的旧测试脚本只给 Alice 绑定真人 `userid`,Bob 用 `--notify-only` 保持通知模式。新工具允许同一个真人拥有多个独立 Agent,并分别通过自己的机器人处理群命令;建议使用 [网页向导中的“给自己增加机器人”](onboarding.md#一个人使用多个机器人或模拟两个同事),按新流程建立身份绑定,不直接修改旧任务的签名策略。真实同事使用各自的企业微信账号。
|
|
22
22
|
|
|
23
23
|
## 1. 只初始化一次
|
|
24
24
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# 网页向导功能检查与测试记录
|
|
2
|
+
|
|
3
|
+
检查日期:2026-10-13。对象是工具仓库中的网页向导源码,本记录对应 0.3.1 的发布前验证。
|
|
4
|
+
|
|
5
|
+
## 测试范围
|
|
6
|
+
|
|
7
|
+
使用独立的临时 Git 仓库、真实 Git 分支与签名协议、模拟企业微信连接进行验证。浏览器实际操作管理员与成员表单、文件下载/上传、状态与审核配置;自动测试覆盖接口鉴权、异常输入、任务接力、真人审核和失败候补。本次没有修改个人真实机器人凭据,没有停止原有 Alice/Bob Runner。
|
|
8
|
+
|
|
9
|
+
浏览器中的“已认证”由模拟连接提供。测试中的对话绑定和轮询通过执行器 API 产生,不能据此宣称已完成真实企业微信群、桌面对话自动唤醒和多台电脑的联合验收。
|
|
10
|
+
|
|
11
|
+
## 页面功能矩阵
|
|
12
|
+
|
|
13
|
+
| 区域 | 功能 | 测试方式与结果 |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| 启动与首页 | 终端链接换取本机会话、项目列表、打开项目 | 浏览器及 HTTP 测试通过;刷新后会话有效,已有项目保留 |
|
|
16
|
+
| 访问控制 | 错误令牌、错误 Host/Origin、未登录接口、请求格式与体积 | HTTP 测试通过;拒绝未授权操作,返回安全提示 |
|
|
17
|
+
| 管理员初始化 | 仓库目录、成员名称、职责、固定验证命令、机器人凭据 | 浏览器创建新管理员成功;无效 JSON/非字符串数组在连接前拦截 |
|
|
18
|
+
| 仓库检查 | 缺少 origin、远程空仓库、缺少 main | 自动测试通过;给出具体的 origin/main 提示与推送指令 |
|
|
19
|
+
| 群身份验证 | 连接、验证码、复制消息、继续检查 | 浏览器通过;未验证不能继续 |
|
|
20
|
+
| 群身份约束 | 准确验证码、指定群、群消息发送人、连接认证前不得验证 | 自动测试通过;不匹配消息忽略 |
|
|
21
|
+
| 验证恢复 | 连接失败重试、断开、验证码过期 | 失败与重试经浏览器验证;过期经自动测试验证 |
|
|
22
|
+
| 团队初始化 | 生成本机身份、发布团队签名、安装 MCP/Skill、启动连接 | 浏览器与本地 Git 测试通过 |
|
|
23
|
+
| 初始化中断 | 已保存项目重新打开、继续接入、启动失败重试 | 浏览器及自动测试通过;不会因页面恢复再次提交验证码 |
|
|
24
|
+
| 分配机器人 | 独立机器人、职责、首选/候补、是否允许本人群命令 | 浏览器通过;取消两个选项后导出值均为 false |
|
|
25
|
+
| 生成接入包 | 加密、密码、邀请有效期、重复成员/机器人约束 | 自动测试通过;浏览器生成并实际下载文件 |
|
|
26
|
+
| 下载与保密 | 下载加密文件、文件不含明文 Secret | 浏览器下载后解密核验通过;Secret 仅存在加密正文 |
|
|
27
|
+
| 邀请发布中断 | 找回同一份导出、重试发布、未发布邀请不得加入 | 自动测试通过 |
|
|
28
|
+
| 成员导入 | 文件上传、密码、业务 clone 与 remote 一致性 | 浏览器正确文件/密码通过;错误密码能留在表单重试;remote 校验经自动测试覆盖 |
|
|
29
|
+
| 文件错误 | 损坏的 JSON、错误密码、被篡改密文 | 自动测试通过;损坏 JSON 有明确重新下载提示 |
|
|
30
|
+
| 重复导入 | 已验证、待批准、配置中、已就绪成员再次导入 | 自动测试通过;浏览器重复导入 ready 成员直接打开原项目,不再抢机器人连接 |
|
|
31
|
+
| 入队申请 | 群身份自动生成 userid、提交签名申请、等待管理员 | 浏览器通过;成员页面显示待批准状态 |
|
|
32
|
+
| 管理员批准 | 显示申请身份、确认入队、更新成员与职责路由 | 浏览器与协议测试通过;新增职责自动有审核规则 |
|
|
33
|
+
| 成员完成配置 | 批准后继续、安装个人 MCP/Skill、启动机器人 | 浏览器通过 |
|
|
34
|
+
| 旧项目导入 | 旧管理员先导入、保留身份/任务/绑定、重复导入 | 浏览器及自动测试通过;兼容原生成的 desktop MCP 配置路径 |
|
|
35
|
+
| 项目状态 | MCP、Skill、机器人、对话绑定、实际轮询、综合就绪 | 浏览器与自动测试通过;缺失/禁用配置、未绑定或轮询过期不会显示综合就绪 |
|
|
36
|
+
| 绑定指令 | 展示、复制、指定身份和项目 | 浏览器通过;桌面对话仍需执行指令,页面不能代替对话创建 heartbeat |
|
|
37
|
+
| 连接操作 | 启动、停止、启动失败、安全错误保存、成功后清理错误 | 浏览器及自动测试通过 |
|
|
38
|
+
| 团队分工 | 成员/职责列表、首选与候补顺序展示 | 浏览器通过;当前可查看,不能编辑既有顺序 |
|
|
39
|
+
| 本人多机器人 | 管理员添加自己的新 Agent、独立 clone 与 Bot、同一真人验证 | 浏览器完成 Alice → 新 Bob 接入;无需下载接入包或设置接入包密码 |
|
|
40
|
+
| 本人接入恢复 | 发布确认丢失、自动批准中断、首页重新打开已保存配置 | 自动测试通过;浏览器从已保存分配继续并打开 ready 项目,不重复创建身份 |
|
|
41
|
+
| 普通同事多机器人 | 同一 userid 分别导入多个接入包、逐个批准 | 自动测试通过;两个 Agent 均保留群命令权限 |
|
|
42
|
+
| 身份隔离 | 独立签名密钥、独立项目 MCP、错误真人/remote 拒绝 | 自动测试通过;同一所有者不合并 Agent 角色,Bob 仍不能签署管理员完成事件 |
|
|
43
|
+
| 审核与通知 | 同一真人 @ 去重、明确审核 Agent 和对应机器人、错误审核机器人拒绝 | 自动测试通过;按 Alice/Bob 的独立签名身份进行审核和任务接力 |
|
|
44
|
+
| 所有者与路由展示 | 显示同一真人的多个 Agent、首选与候补 | 浏览器确认“你的机器人:alice、bob”和 backend:bob → alice,无控制台错误 |
|
|
45
|
+
| 真人审核规则 | 必须审核开关、审核人、额外通知、指定已验证同事、保存与刷新 | 浏览器及自动测试通过;版本校验拒绝过时配置,新规则只用于新任务 |
|
|
46
|
+
| 协作执行 | 两位执行者接力固定提交、运行验证、发阶段通知、真人审批、完成 | 本地自动测试通过;真实群通知呈现仍需实群验证 |
|
|
47
|
+
| 诊断提示 | 配置路径、失败说明、Git 同步失败本机状态回退 | 浏览器确认配置路径展示;安全错误与 Git 同步回退经自动测试覆盖,不输出 SDK 原始异常 |
|
|
48
|
+
|
|
49
|
+
完整测试命令是 `npm test`,最终结果为 **108 passed, 0 failed**。新增加的页面事件测试运行实际交付的 `app.js`,使用最小 DOM 验证 API 调用顺序与恢复;它不能替代真实浏览器交互。本次已结合两种测试。
|
|
50
|
+
|
|
51
|
+
## 本次修复
|
|
52
|
+
|
|
53
|
+
1. 重复导入已接入成员的接入包时,按已有阶段打开项目,避免重复验证抢占 Runner;首次导入后连接失败也能看到已保存项目和重新连接入口。
|
|
54
|
+
2. 项目状态独立显示 MCP 与 Skill,并识别旧版生成的 desktop 配置。综合就绪要求项目配置、认证连接、绑定和最近三分钟内实际轮询同时满足。显式禁用 MCP 会显示未就绪。
|
|
55
|
+
3. 验证成功后页面加载失败,不再因为清空 pending 而报空对象错误;管理员中断恢复识别本次新建的项目,兼容规范化后的目录路径。
|
|
56
|
+
4. 中断接入与 Runner 启停错误转换成可恢复的安全中文提示;保存错误供刷新查看,成功后清理。
|
|
57
|
+
5. 缺失 Git `info/exclude` 时正常创建父目录与排除文件,失败时恢复原状。
|
|
58
|
+
6. 无效验证命令在连接前拦截;损坏接入包 JSON 有具体文件错误提示。
|
|
59
|
+
|
|
60
|
+
## 本人多机器人补充验证
|
|
61
|
+
|
|
62
|
+
新增 8 项多所有者/多 Agent 自动测试、3 项网页事件测试、1 项企业微信审核通知测试和 1 项网页接口鉴权测试。完整测试通过后,没有修改执行逻辑;本记录补充了验证范围与结果。
|
|
63
|
+
|
|
64
|
+
浏览器使用新建的临时 Git 仓库、模拟机器人连接和模拟 Runner 启停,实际填写第二个 clone、新 Agent 和机器人配置,验证群身份后完成本人入队、安装 MCP/Skill,并从管理员页面继续打开该配置。这里的 ready 表示向导完成配置;模拟 Runner 不构成真实群连接或桌面对话验收。浏览器截图保存于本机临时目录,未加入发布包。
|
|
65
|
+
|
|
66
|
+
本人邀请带有签名保护的可选 `owner_userid`,要求群验证账号与已验证管理员一致;接入记录沿用正常签名申请和管理员批准。邀请与恢复资料保存在仓库外的 0600 私有文件中。普通同事可以通过多份接入包使用同一个真人账号,但仍需分别验证和批准。新客户端接受旧邀请;旧客户端不识别新所有者字段或共享所有者策略,采用此功能前应统一升级并重启 Runner/MCP。
|
|
67
|
+
|
|
68
|
+
## 尚缺少的页面功能
|
|
69
|
+
|
|
70
|
+
| 功能遗漏 | 当前影响 |
|
|
71
|
+
| --- | --- |
|
|
72
|
+
| 更换机器人、轮换 Secret、变更群 | 只能按本机配置与企业微信端操作;向导没有维护入口 |
|
|
73
|
+
| 撤销邀请、禁用/移除成员、轮换签名身份 | 当前没有产品化入口;需要补签名协议、权限与历史兼容,不能仅删本机文件 |
|
|
74
|
+
| 修改既有成员职责、重排首选与候补 | 页面只展示已有分工;生成邀请时可以指定新成员优先级 |
|
|
75
|
+
| 缺失 MCP/Skill 的一键修复 | 当前能识别缺失,但现有 Skill、受版本管理的配置等需要检查;不会自动覆盖 |
|
|
76
|
+
| 确认 heartbeat 持续启用、切换绑定对话 | 页面能观察绑定与最近轮询;不能控制 Codex 自动化,切换需要对话工具解除旧绑定 |
|
|
77
|
+
| 页面内展示日志与详细 Git 诊断 | 当前只有安全错误提示、私有配置路径;详细日志仍需本机查看 |
|
|
78
|
+
| 多管理员配置冲突的页面引导 | 后端按版本拒绝过时保存;页面需要刷新重新设置,不会自动合并 |
|
|
79
|
+
| 页面刷新保留正在进行的管理员验证码步骤 | 管理员保存 profile 前的验证码状态在当前页面内存中;刷新需重新填写和验证;成员已导入的项目可从首页恢复 |
|
|
80
|
+
|
|
81
|
+
## 仍需联合验收的环境场景
|
|
82
|
+
|
|
83
|
+
- 两台不同电脑分别安装 npm 包,对同一远程仓库各自接入、绑定真实 Codex 对话。
|
|
84
|
+
- 真实企业微信群中创建任务、自动轮询启动执行、阶段总结、真人审核、下一位接力。
|
|
85
|
+
- 企业微信原生 @ 提醒效果,以及真实 WebSocket 断线/凭据轮换的提示和恢复。
|
|
86
|
+
- 应用退出、电脑休眠、heartbeat 暂停与网络恢复后,候补接管与旧执行者的租约隔离。
|
|
87
|
+
|
|
88
|
+
页面的“项目 MCP / Skill 已就绪”表示本机文件已配置,并非读取了 Codex 的实际加载状态。近期成功轮询是额外证据,仍不能保证未来自动化持续运行。Git 启动检查与任务执行中的 Git 失败属于不同时间点,初始化通过不代表未来的 push 永远成功。
|
package/docs/onboarding.md
CHANGED
|
@@ -15,18 +15,20 @@ codex-collab setup
|
|
|
15
15
|
|
|
16
16
|
网页自动打开;若未打开,执行 `codex-collab setup --no-open`,复制终端打印的本机链接。保留此终端以使用工作台。团队成员只需 clone 业务仓库,无需 clone 工具仓库。
|
|
17
17
|
|
|
18
|
-
同一台电脑模拟两位成员时,使用同一业务仓库的两个独立 clone、两个机器人、两个 Codex 对话;同一个 clone
|
|
18
|
+
同一台电脑模拟两位成员时,使用同一业务仓库的两个独立 clone、两个机器人、两个 Codex 对话;同一个 clone 只能接入一个身份。同一个企业微信账号可以拥有多个 Agent,每个 Agent 的机器人、签名身份、clone 和对话独立。真正同事使用自己的企业微信账号和 Git 凭据。
|
|
19
19
|
|
|
20
20
|
## 管理员操作
|
|
21
21
|
|
|
22
22
|
1. 在企业微信创建独立智能机器人,获取 Bot ID 和 Secret,拉进协作群。为每位成员分别创建机器人并加入同一个群。
|
|
23
23
|
2. 工作台选择“初始化团队”:输入业务仓库本机目录、自己的成员名称、职责标签、固定验证命令,以及自己的机器人 Bot ID/Secret。成员名称与职责用英文、数字、下划线或连字符,例如 `alice`、`frontend,backend`。
|
|
24
24
|
3. 点击“连接机器人并验证群”,用自己的企业微信账号 @ 此机器人,发送页面显示的 `协作 接入 六位数字`。返回网页点击“已发送,继续检查”。工具自动发现群 `chatid` 与你的 `userid`,创建身份、签名账本和项目 MCP/Skill,并启动本机机器人 Runner。
|
|
25
|
-
4.
|
|
25
|
+
4. 项目页面“分配机器人给同事”:填写成员名称、职责、成员机器人 Bot ID/Secret,以及至少 12 字符的接入包密码。“首选执行者”决定新成员在职责路由中排第一还是候补;未使用的职责会新增路由。
|
|
26
26
|
5. 生成并下载加密 JSON 接入包,交给指定成员;密码通过另一条可信渠道单独提供。包默认 7 天有效,同一成员或机器人不会同时分发多个有效邀请。
|
|
27
27
|
6. 成员完成群验证后,刷新“待确认的成员”,核对成员名称、职责和 userid,点击“确认此成员入队”。新的签名策略通过 Git 同步到其他在线成员,无需重建已有任务。
|
|
28
28
|
7. 项目页“真人审核与通知”按职责设置审核人和额外通知对象,默认需要阶段审核且由执行者本人处理。可选管理员或已经验证本人身份的指定同事。管理员也按下文把自己的 Codex 对话启用。
|
|
29
29
|
|
|
30
|
+
同一位同事可以领取多份接入包,用同一个企业微信账号分别验证不同机器人;每份包选择独立 clone,并绑定不同 Codex 对话,不需要关闭群命令权限。
|
|
31
|
+
|
|
30
32
|
真人同事和机器人都应加入协作群。每位真人通过自己的企业微信账号验证接入,以便工具核对审核权限;额外通知对象不会因此获得审核权限。
|
|
31
33
|
|
|
32
34
|
机器人必须由一个本机 Runner 保持连接。同一个 Bot ID 不要同时运行旧服务、群发现命令和新 Runner。向导会检查可识别的本机重复连接;其他电脑上的连接需由团队自己核实。
|
|
@@ -51,18 +53,10 @@ codex-collab setup
|
|
|
51
53
|
1. 用原配置停止该机器人的 Runner:`codex-collab stop /absolute/path/to/private-peer.json`。有正在执行的任务时先处理其租约/执行状态。
|
|
52
54
|
2. 进入网页“导入已有配置”,填写原私有配置的完整路径。**旧团队由管理员先导入,然后其他成员分别导入**。
|
|
53
55
|
3. 工具从原策略建立签名版本根,保留原身份、私有数据目录、Codex 对话绑定和任务,再通过原配置路径重启 Runner。
|
|
54
|
-
4.
|
|
56
|
+
4. 向导刷新可识别的项目 MCP 和 Skill;重新加载 MCP,再把页面的新启用指令发送到原绑定对话,更新原 heartbeat 的提示词。以后统一在工作台查看状态、添加新成员。
|
|
55
57
|
5. 原 v2 团队不会仅因导入就改变接力行为。所有成员更新工具并重启机器人和 MCP 后,管理员在“真人审核与通知”保存规则,发布 v3 策略。此后创建的新任务应用新规则;正在执行的旧任务继续遵循原策略。
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
```text
|
|
60
|
-
阶段提交返回 stage_review 或 review 时等待指定真人审核。
|
|
61
|
-
审核退回后,按 review_feedback 从工具指定的提交继续修改并重新提交。
|
|
62
|
-
审核与完成命令由真人在企业微信群执行。
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
原对话绑定可以继续使用;定制过的 Skill 由本人检查并更新。
|
|
59
|
+
向导只更新工具生成且可识别的 Skill/MCP;自定义、已提交或不同身份的配置需要本人手动检查。原对话绑定和其他模型/provider 配置保留。仅升级 npm 工具不足以更新已经复制进项目的 Skill 或应用 heartbeat;也可按 [自然语言入口升级步骤](desktop-poll.md#已有项目启用自然语言入口) 使用 `refresh-project`。
|
|
66
60
|
|
|
67
61
|
导入要求原配置是 `desktop_poll` 且包含企微机器人。策略根与本机已经信任的旧策略不一致时会拒绝迁移。旧成员尚未导入签名版本前,不应混用旧客户端处理新增成员的任务。
|
|
68
62
|
|
|
@@ -71,10 +65,13 @@ codex-collab setup
|
|
|
71
65
|
管理员和成员都绑定对话并完成实际轮询后,在群里选中实际机器人发送一条**新任务**:
|
|
72
66
|
|
|
73
67
|
```text
|
|
74
|
-
@Alice
|
|
68
|
+
@Alice 给登录页增加密码显示切换,保持原有登录行为。
|
|
69
|
+
你做前端,Bob 补接口和测试,完成后在群里告诉我。
|
|
75
70
|
```
|
|
76
71
|
|
|
77
|
-
|
|
72
|
+
默认验收顺序:群回复需求 N-ID → Alice 对话理解需求并在群里确认任务 T-ID 与分工 → Alice 指定对话执行 → 群收到阶段总结、代码提交、测试和审核命令 → Alice 本人审核通过 → Bob 指定对话从获准的 SHA 接力 → Bob 本人审核最终结果 → 管理员完成任务。
|
|
73
|
+
|
|
74
|
+
需求不清时机器人先追问,直接 @同一个机器人补充;有多条待补充需求时带上 N-ID。状态问题或闲聊可以只回答,不创建任务。自然语言“通过/完成”不会改变审核状态,真人仍复制群通知中的审核命令。
|
|
78
75
|
|
|
79
76
|
管理员也能通过已经允许群命令的成员机器人创建/查询任务。`@Bob 协作 创建 ...` 默认使用 Bob 接入包中的首个职责;实际领取仍由能力路由、占用和候补决定。审核由当前指定审核人通过自己的机器人发出;完成、取消、重试仍需管理员通过自己的机器人发出。成员机器人不会代签其他人的操作。
|
|
80
77
|
|
|
@@ -92,17 +89,34 @@ codex-collab setup
|
|
|
92
89
|
|
|
93
90
|
群里“任务已创建”和“机器人已认证”不能代替 Codex 已接单证据。最终任务不会自动批准或合并 `main`;管理员需要检查代码、真实测试证据再决定如何合并。消息中的 `npm test 通过` 是验收描述,真正运行的是管理员在本机初始化时固定的 JSON argv。
|
|
94
91
|
|
|
95
|
-
##
|
|
92
|
+
## 一个人使用多个机器人,或模拟两个同事
|
|
96
93
|
|
|
97
|
-
|
|
94
|
+
准备同一业务仓库的两个独立 clone、两个企业微信机器人和两个 Codex 对话。管理员给自己增加 Agent 可以直接在同一工作台完成:
|
|
95
|
+
|
|
96
|
+
1. 以 Alice 初始化团队,完成群验证并进入管理员项目。已有团队可直接打开已就绪的管理员项目。
|
|
97
|
+
2. 点击“给自己增加机器人”,填写第二个 clone 的本机目录、新 Agent 名称 `bob`、职责 `backend`,以及新机器人的 Bot ID/Secret。
|
|
98
|
+
3. 保留“首选执行者”时,Bob 成为 backend 首选;Alice 若也有 backend 能力则是候补。点击“连接我的新机器人”。无需包密码或下载接入包。
|
|
99
|
+
4. 用初始化 Alice 时的**同一个企业微信账号** @Bob,发送页面验证码,再点击“已发送,继续检查”。工具校验本人身份,自动签名批准新 Agent、配置 Bob 项目的 MCP/Skill 并启动连接。Bob 仍是 member,不会继承 Alice 的管理员角色。
|
|
100
|
+
5. 在 Alice clone 和 Bob clone 中分别打开要绑定的已有 Codex 对话,向各自对话粘贴该项目的“启用指令”。每个 Agent 各自创建 heartbeat,确认身份与最近轮询。
|
|
101
|
+
6. 在群里 @Alice 创建包含两种职责的任务,例如 `[frontend>backend]`。默认 Alice 阶段提交后你 @Alice 审核,Bob 阶段提交后你 @Bob 审核,最后 @Alice 完成任务。以每次通知指定的审核身份和版本号为准。
|
|
102
|
+
|
|
103
|
+
两份配置都允许所有者发送群命令。同一个 userid 只表示是同一个真人;收消息的机器人决定签名 Agent,权限不会合并,发给 Bob 的管理员完成命令仍会被拒绝。审核人与通知对象属于同一个真人时,群通知只提及一次,同时列出所对应的 Agent 和必须使用的审核机器人。
|
|
104
|
+
|
|
105
|
+
普通团队成员拥有多个机器人时,由管理员给他分配多份独立接入包;成员用自己的同一个账号验证,管理员逐个确认,其余操作与成员流程一致。“给自己增加机器人”的自动批准入口仅供已经验证、就绪的管理员使用。
|
|
106
|
+
|
|
107
|
+
接入中断时,从管理员项目“已分配的机器人”找到 Bob,点击“继续接入我的机器人”,或从首页打开已经保存的 Bob 项目继续。不要重新生成同名 Agent。若误选 Alice 的 clone、另一个 remote 或已分配的 Bot ID,工具会拒绝。
|
|
108
|
+
|
|
109
|
+
使用这个功能前,所有参与者需运行包含多 Agent 所有权支持的新工具,并重启旧 Runner/MCP。旧客户端会拒绝重复 owner userid 或新邀请的可选字段。旧策略里没有 userid 的通知机器人仍按旧规则由管理员审核;已签名的旧身份绑定不能直接手改成新绑定。
|
|
98
110
|
|
|
99
111
|
## 工作台、停止和恢复
|
|
100
112
|
|
|
101
113
|
默认本机数据位于 `~/.config/codex-collab/workspace`,机器人 Secret、私钥、对话 ID、Runner 日志和接入状态均在业务仓库之外;私有 JSON 使用 `0600`。Git 只同步签名协议、公钥、职责、任务证据和必要结果摘要。
|
|
102
114
|
|
|
115
|
+
- 重复导入同一接入包:已验证或已接入的成员会直接打开原项目,继续原进度;不会重复启动群验证。
|
|
116
|
+
- 项目接单状态:同时检查 MCP、Skill、认证连接、目标对话绑定和最近三分钟内实际轮询。配置文件存在不等于 Codex 已加载,自动化启用状态仍需在绑定对话确认。
|
|
103
117
|
- 群验证断线/过期:点击“重新连接”,使用新的验证码。
|
|
104
118
|
- Git/MCP 配置中断:从“我的项目”打开原项目,再点击“继续接入”;不要反复创建另一身份。
|
|
105
|
-
-
|
|
119
|
+
- 管理员生成邀请时网络失败:回项目页面刷新已生成接入包,再下载;本人机器人点击“继续接入我的机器人”。工具重试发布同一签名邀请。
|
|
106
120
|
- 本机工作台关闭:重新运行 `codex-collab setup`;已启动的机器人 Runner 会继续运行。
|
|
107
121
|
- 页面“停止机器人连接”只断开群机器人。暂停接单需在对应 Codex 对话暂停 heartbeat;已有任务仍需完成、报告失败或按租约恢复处理。
|
|
108
122
|
- CC Switch 与模型账号设置保持原样;项目 MCP 使用本机 Node 的绝对执行路径。
|
|
@@ -117,3 +131,5 @@ codex-collab doctor /absolute/path/to/private-peer.json
|
|
|
117
131
|
Secret 若曾发到聊天或 Git,应在企业微信重新生成后更新本机配置。工具只发布协作任务的阶段总结,不会把整个私有 Codex 对话发到群里;目前尚无任意普通对话完成后自动发群总结的独立工具。
|
|
118
132
|
|
|
119
133
|
旧命令兼容:`codex-collab setup --terminal` 或 `setup --role admin|member` 继续使用 [旧终端工作流](terminal-onboarding.md),不走加密包与动态签名入队。
|
|
134
|
+
|
|
135
|
+
网页向导的逐项覆盖、故障恢复与功能遗漏见 [功能检查与测试记录](onboarding-audit.md)。
|
package/docs/peer.md
CHANGED
|
@@ -88,7 +88,7 @@ unset WECOM_BOT_SECRET
|
|
|
88
88
|
|
|
89
89
|
不要把上面的真实 Secret 放在命令历史、聊天或 Git 中;如果曾经泄露,先在企业微信后台轮换,再使用新 Secret。
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
同一个真人可以拥有多个独立 Agent,并使用同一个已验证 `userid`;每个 Agent 仍需独立机器人、签名身份、clone 和对话。推荐通过 [网页向导](onboarding.md#一个人使用多个机器人或模拟两个同事) 添加。旧隔离测试也可以给第二个 Bot 加 `--notify-only`,让它只发送通知;这属于测试脚本的默认设置。不要手改既有签名任务的所有者绑定,旧客户端需要统一升级后再采用多 Agent 所有权。
|
|
92
92
|
|
|
93
93
|
## 安装当前 Codex 的 Skill 和 MCP
|
|
94
94
|
|
|
@@ -143,7 +143,16 @@ export PATH="$HOME/.cache/codex-runtimes/codex-primary-runtime/dependencies/node
|
|
|
143
143
|
|
|
144
144
|
### C. 企业微信群自动分配
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
网页向导使用 `desktop_poll`,支持有权限的真人向自己的机器人用自然语言描述需求:
|
|
147
|
+
|
|
148
|
+
```text
|
|
149
|
+
@Alice 给登录页增加密码显示切换,保持现有登录行为。
|
|
150
|
+
你负责前端,Bob 补接口和测试,完成后在群里告诉我。
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Runner 保存签名需求,已有 Codex 对话轮询后理解并调用 `resolve_desktop_request` 创建任务、追问或回答。分工必须匹配已配置路由;需求不清时直接 @原机器人补充,多条追问时附 N-ID。确认后沿用原租约、候补和审核流程;详见 [自然语言入口与升级](desktop-poll.md#已有项目启用自然语言入口)。本入口不支持 SDK 自动模式,SDK 仍使用明确命令。
|
|
154
|
+
|
|
155
|
+
以下明确命令同时兼容两种模式;审核/完成由真人执行,自然语言文字不代替它们:
|
|
147
156
|
|
|
148
157
|
```text
|
|
149
158
|
@机器人 协作 创建 [frontend>backend] 增加登录页 | npm test 通过;登录页可访问
|
package/docs/team-registry.md
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
|
|
18
18
|
客户端通过加密接入包或明确导入的旧可信策略 pin 根 hash。本机保存最高版本与对应 hash,拒绝回滚和从已观察版本分叉。后续版本须由上一策略中的管理员签名;已有成员公钥、角色和 userid 不可偷偷替换。
|
|
19
19
|
|
|
20
|
+
Agent ID 与签名公钥保持独立;多个 Agent 可以使用同一个已验证 `wecom_userid`。接收群命令的机器人选择本机 Agent 的签名身份,所有者相同不会合并角色或审核权限。
|
|
21
|
+
|
|
20
22
|
每个新任务的 created 事件 pin 当时的 `team_hash`。重放、claim、续租、审核和候补均使用那个版本,而不是直接把最新路由用于旧任务。因此加入新成员或更新审核规则影响此后创建的任务,旧任务不改变授权或候补。
|
|
21
23
|
|
|
22
24
|
## 真人审核策略 v3
|
|
@@ -38,18 +40,26 @@
|
|
|
38
40
|
|
|
39
41
|
## 邀请、申请、批准
|
|
40
42
|
|
|
41
|
-
公开邀请由管理员签名,包含成员名称、职责、机器人 Bot ID、允许的群 ID、群命令开关、首选/候补、到期时间、邀请 claim 公钥和引用的团队 hash。默认 7
|
|
43
|
+
公开邀请由管理员签名,包含成员名称、职责、机器人 Bot ID、允许的群 ID、群命令开关、首选/候补、到期时间、邀请 claim 公钥和引用的团队 hash。默认 7 天有效。“给自己增加机器人”会额外签名可选 `owner_userid`,限定为签发管理员已验证的 userid,并要求开启群命令;入队申请必须证明同一个真人账号。旧邀请不含此字段也继续有效。它不包含机器人 Secret、claim 私钥或成员私钥。
|
|
42
44
|
|
|
43
45
|
成员的私钥在成员本机生成。通过群验证码得到 userid/chatid 后,申请包括成员公钥和这些公开绑定,并由成员密钥与邀请 claim 密钥分别签名。管理员核对后发布新的团队版本;版本的 enrollment 包含对应 invite_id 和 request_id。相同申请重试幂等,另一个身份复用已消费邀请会被拒绝。已经批准的同一身份恢复本机配置不受后续邀请过期影响。
|
|
44
46
|
|
|
47
|
+
管理员在本人第二个 clone 的接入流程通过群验证码证明相同账号后,自动调用同一套签名入队批准协议;新 Agent 仍为 member。普通成员的多个 Agent 仍分别提交申请并由管理员确认。
|
|
48
|
+
|
|
45
49
|
目前版本链用于追加成员、能力路由、审核规则和本机恢复。机器人轮换、密钥轮换、成员撤销与群迁移没有网页操作;不要手改签名文件实现这些动作。多个管理员同时写入时,经本机锁、Git fast-forward push 与重新校验重试处理。
|
|
46
50
|
|
|
47
51
|
## 加密接入包
|
|
48
52
|
|
|
49
53
|
分发文件 `kind: codex-collab-encrypted-invite`,含 schema_version、随机 salt、随机 iv、GCM tag 和 ciphertext;scrypt 派生密钥,AES-256-GCM 加密内部 JSON。内部包包含公开签名历史、邀请、root_hash、remote、固定 test_argv、专用 claim 私钥及分配的机器人 Secret。密码至少 12 字符,文件与密码分开传递。
|
|
50
54
|
|
|
51
|
-
|
|
55
|
+
给同事分配时,管理员保存可再次下载的密文。成员的签名私钥始终在成员机器上,管理员不会代为生成。包仍是敏感凭据,持有包和密码的人能连接该机器人并申请入队;管理员最终批准用于核对申请身份。
|
|
56
|
+
|
|
57
|
+
管理员本人新增机器人不分发文件。其 Bot Secret 与 claim 私钥保存在工作台私有目录的 `0600` JSON,和各 Agent 私钥一样不进入业务仓库。先保存本机配置再发布邀请,丢失 Git 回执后可重试同一邀请;群验证和管理员签名批准仍然必需。
|
|
52
58
|
|
|
53
59
|
本机向导使用随机打开链接交换 HttpOnly SameSite=Strict cookie;检查 Host、Origin、JSON 内容类型和请求长度,避免其他网站直接操作本机配置。SDK/Git 错误经脱敏,不把异常中的凭据返回网页。
|
|
54
60
|
|
|
55
|
-
Schemas:`team-policy.schema.json`、`team-version.schema.json`、`team-invitation.schema.json`、`team-enrollment.schema.json`、`encrypted-invite.schema.json`。运行时另外校验签名、完整历史链、root/checkpoint
|
|
61
|
+
Schemas:`team-policy.schema.json`、`team-version.schema.json`、`team-invitation.schema.json`、`team-enrollment.schema.json`、`encrypted-invite.schema.json`。运行时另外校验签名、完整历史链、root/checkpoint、Agent ID 与公钥唯一性、邀请时效、群绑定、Git 权限与任务创建时策略。
|
|
62
|
+
|
|
63
|
+
## 多 Agent 所有权兼容
|
|
64
|
+
|
|
65
|
+
邀请 schema 1 增加可选 `owner_userid`;旧邀请仍可由新客户端验证,任务及团队版本的 schema 编号保持不变。旧客户端对字段与 owner 唯一性的校验不支持此功能,会拒绝新邀请或共享 owner 的策略。在增加同一真人的第二个 Agent 前,所有参与者必须升级到支持此功能的工具,并重启 Runner 与 MCP;不能混用旧安装。
|