xuym-codex-collab 0.5.0 → 0.7.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.
@@ -1,11 +1,12 @@
1
1
  const $ = (id) => document.getElementById(id);
2
2
  let currentId, pending, discovery, timer, busy = false, reviewVersion = null, reviewId, ownAdminId, resetPlan, resetDiscovery;
3
+ let repositoryPolicy, currentProjectId, currentProject, projectSignature, ownReturnProject;
3
4
  let viewEpoch = 0, currentView = 'home', completing = false, deliveryTarget;
4
5
  const storageKey = 'codex-collab-onboarding';
5
6
  function remember(value) { try { if (value) sessionStorage.setItem(storageKey, JSON.stringify(value)); else sessionStorage.removeItem(storageKey); } catch {} }
6
7
  function remembered() { try { return JSON.parse(sessionStorage.getItem(storageKey) || 'null'); } catch { return null; } }
7
8
  function notice(text) { $('notice').textContent = text; $('notice').hidden = !text; }
8
- function show(view) { if (view !== 'profile' || currentView === 'profile') { $('delivery-panel').hidden = true; $('delivery-password').value = ''; $('delivery-instructions').textContent = ''; $('reissue-secret').value = ''; $('reissue-form').hidden = true; deliveryTarget = null; } clearInterval(timer); clearTimeout(timer); viewEpoch++; currentView = view; for (const section of document.querySelectorAll('main>section')) section.hidden = section.id !== view; }
9
+ function show(view) { notice(''); const active = ['project', 'profile', 'own', 'reset-view'].includes(view) ? 'home' : view; for (const link of document.querySelectorAll('nav [data-view]')) { if (link.dataset.view === active) link.setAttribute('aria-current', 'page'); else link.removeAttribute('aria-current'); } if (view !== 'profile' || currentView === 'profile') { $('delivery-panel').hidden = true; $('delivery-password').value = ''; $('delivery-instructions').textContent = ''; $('reissue-secret').value = ''; $('reissue-form').hidden = true; deliveryTarget = null; } clearInterval(timer); clearTimeout(timer); viewEpoch++; currentView = view; for (const section of document.querySelectorAll('main>section')) section.hidden = section.id !== view; }
9
10
  async function api(path, body) {
10
11
  const response = await fetch(`/api/${path}`, body ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) } : {});
11
12
  const value = await response.json(); if (!response.ok) throw new Error(value.error); return value;
@@ -35,13 +36,94 @@ async function overview() {
35
36
  const card = document.createElement('div'); card.className = 'card';
36
37
  card.append(text('span', `${draft.display_name || draft.agent_id} · 待完成群验证`), button('继续创建团队', () => openDraft(draft.id))); $('profiles').append(card);
37
38
  }
39
+ $('project-count').textContent = value.projects ? `${value.projects.length} 个项目` : '';
40
+ for (const [index, project] of (value.projects || []).entries()) {
41
+ const card = document.createElement('div'); card.className = 'card project-card'; card.style?.setProperty('--order', Math.min(index, 7));
42
+ const info = document.createElement('div'); info.className = 'project-card-info';
43
+ const icon = text('span', project.name.slice(0, 1).toUpperCase()); icon.className = 'project-icon'; icon.setAttribute?.('aria-hidden', 'true');
44
+ info.append(icon, text('h3', project.name), text('p', project.repository || '本机接入'), text('span', `本机已接入 ${project.local_agent_count} 个机器人`));
45
+ card.append(info, button('打开项目 ↗', () => loadProject(project.id))); $('profiles').append(card);
46
+ }
38
47
  if (!value.profiles.length && !(value.drafts || []).length) $('profiles').append(text('p', '还没有接入项目。选择“连接我的机器人”,或导入同事提供的接入包。'));
39
- for (const profile of value.profiles) {
48
+ for (const profile of value.projects ? [] : value.profiles) {
40
49
  const card = document.createElement('div'); card.className = 'card'; const info = document.createElement('div');
41
50
  info.append(text('strong', `${profile.display_name || profile.agent_id} · ${profile.role === 'admin' ? '管理员' : '成员'}`), text('p', profile.repo));
42
51
  card.append(info, button('打开项目', () => loadProfile(profile.id))); $('profiles').append(card);
43
52
  }
44
53
  }
54
+ function connectionStatus(connection) {
55
+ if (connection.has_error) return '需要检查';
56
+ if (connection.phase !== 'ready') return ({ imported: '待群验证', waiting: '待管理员确认', verified: '正在入队', configuring: '正在配置', initializing: '正在初始化' })[connection.phase] || '待完成接入';
57
+ if (connection.can_receive_tasks) return '已就绪';
58
+ if (!connection.runner_online) return '连接已停止';
59
+ if (!connection.bound) return '待绑定对话';
60
+ if (!connection.poll_recent) return '等待对话轮询';
61
+ return '连接检查中';
62
+ }
63
+ function renderProject(value) {
64
+ currentProject = value;
65
+ $('project-title').textContent = value.name;
66
+ $('project-repository').textContent = value.repository;
67
+ $('project-sync').textContent = value.sync_error || (value.sync_status === 'synced' ? '团队信息已同步。连接状态仅显示本机实际记录。' : value.sync_status === 'cached' ? '正在显示本机已验证的团队记录。' : '接入尚未完成,可以继续配置下面的机器人。');
68
+ $('project-add-own').hidden = !value.admin_profile_id; $('project-settings').hidden = !value.admin_profile_id;
69
+ const signature = JSON.stringify(value);
70
+ if (signature === projectSignature) return;
71
+ projectSignature = signature;
72
+ $('project-summary').replaceChildren();
73
+ for (const [number, label] of [[value.agents.length, '项目机器人'], [value.agents.filter((agent) => agent.mine).length, '属于我'], [value.base_branch || '待确认', '任务基准分支']]) {
74
+ const stat = document.createElement('div'); stat.className = 'summary-item'; stat.append(text('strong', String(number)), text('span', label)); $('project-summary').append(stat);
75
+ }
76
+ $('project-agents').replaceChildren();
77
+ for (const [index, agent] of value.agents.entries()) {
78
+ const card = document.createElement('article'); card.className = `robot-card${agent.mine ? ' is-mine' : ''}`; card.style?.setProperty('--order', Math.min(index, 7));
79
+ const heading = document.createElement('div'); heading.className = 'robot-title';
80
+ const connection = agent.connections[0];
81
+ const display = connection?.display_name;
82
+ heading.append(text('h4', display && display !== agent.agent_id ? `${display}(${agent.agent_id})` : agent.agent_id));
83
+ if (agent.mine) { const mine = text('span', '我的'); mine.className = 'badge mine-badge'; heading.append(mine); }
84
+ card.append(heading, text('p', `${agent.role === 'admin' ? '管理员' : '成员'}${agent.capabilities.length ? ' · ' + agent.capabilities.join(' / ') : ''}`));
85
+ if (agent.identity_conflict) { const warning = text('p', '本机身份与团队记录不一致,请检查原接入。'); warning.className = 'warning-text'; card.append(warning); }
86
+ for (const local of agent.connections) {
87
+ const details = document.createElement('div'); details.className = 'robot-connection';
88
+ const status = text('span', connectionStatus(local)); status.className = `badge${local.can_receive_tasks ? ' ready-badge' : ' pending'}`;
89
+ const path = text('p', local.repo); path.className = 'repository-path';
90
+ details.append(status, path, button(local.phase === 'ready' ? '管理机器人' : '继续接入', () => loadProfile(local.id))); card.append(details);
91
+ }
92
+ if (!agent.connections.length) { const note = text('p', agent.mine ? '我的机器人 · 未在本机接入' : '由同事维护'); note.className = 'muted'; card.append(note); }
93
+ $('project-agents').append(card);
94
+ }
95
+ }
96
+ async function refreshProjectView(id, epoch) {
97
+ const value = await api(`project?id=${encodeURIComponent(id)}`);
98
+ if (epoch === viewEpoch && currentView === 'project') renderProject(value);
99
+ }
100
+ function watchProject(id, epoch, delay = 15000) {
101
+ timer = setTimeout(async () => {
102
+ if (epoch !== viewEpoch || currentView !== 'project') return;
103
+ if (!busy && document.visibilityState !== 'hidden') {
104
+ try { await refreshProjectView(id, epoch); }
105
+ catch (error) { if (epoch === viewEpoch) $('project-sync').textContent = `自动同步暂未成功,将重试。${error.message}`; }
106
+ }
107
+ if (epoch === viewEpoch) watchProject(id, epoch);
108
+ }, delay);
109
+ }
110
+ async function loadProject(id) {
111
+ currentProjectId = id; projectSignature = null; currentProject = null;
112
+ remember({ project_id: id }); show('project'); const epoch = viewEpoch;
113
+ $('project-title').textContent = '正在打开项目'; $('project-repository').textContent = '';
114
+ $('project-summary').replaceChildren(); $('project-agents').replaceChildren();
115
+ $('project-add-own').hidden = true; $('project-settings').hidden = true; $('project-sync').textContent = '';
116
+ try {
117
+ const value = await api(`project?id=${encodeURIComponent(id)}&local=1`);
118
+ if (epoch !== viewEpoch) return;
119
+ renderProject(value); watchProject(id, epoch, 100);
120
+ } catch (error) { if (epoch === viewEpoch) { show('home'); await overview(); throw error; } }
121
+ }
122
+ $('project-back').onclick = () => action(async () => { remember(null); show('home'); await overview(); });
123
+ $('project-refresh').onclick = () => action(() => refreshProjectView(currentProjectId, viewEpoch));
124
+ $('project-settings').onclick = () => action(() => loadProfile(currentProject.admin_profile_id));
125
+ $('project-add-own').onclick = () => action(async () => { ownAdminId = currentProject.admin_profile_id; ownReturnProject = currentProjectId; show('own'); });
126
+ $('profile-back').onclick = () => action(() => loadProject(currentProjectId));
45
127
  async function openDraft(id) {
46
128
  const value = await api(`admin-draft?id=${id}`);
47
129
  if (value.profile) { pending = null; await loadProfile(value.profile.id); return; }
@@ -114,16 +196,19 @@ function watchProfile(id, epoch) {
114
196
  timer = setTimeout(tick, 5000);
115
197
  }
116
198
  async function loadProfile(id) {
199
+ repositoryPolicy = null; $('repository-policy-form').hidden = true;
117
200
  currentId = id; remember({ profile_id: id }); show('profile'); const epoch = viewEpoch; let value;
118
201
  try { value = await api(`profile?id=${id}`); } catch (error) { value = await api(`profile?id=${id}&local=1`); notice(`团队同步暂未成功,本机状态仍可查看。${error.message}`); }
119
202
  if (epoch !== viewEpoch) return;
120
203
  if (value.phase === 'removed') { show('home'); await overview(); return; }
204
+ currentProjectId = value.project_id || null;
205
+ $('profile-back').hidden = !currentProjectId;
121
206
  renderProfile(value); watchProfile(id, epoch); return value;
122
207
  }
123
208
  function renderProfile(value, automatic = false) {
124
209
  const id = value.id; renderSteps(value);
125
210
  $('reset-conversation').hidden = value.phase !== 'ready'; $('reset-robot').hidden = value.phase !== 'ready';
126
- $('profile-title').textContent = `${value.display_name && value.display_name !== value.agent_id ? `${value.display_name}(${value.agent_id})` : value.agent_id} · ${value.role === 'admin' ? '管理员' : '成员'}`; $('profile-repo').textContent = value.repo;
211
+ $('profile-title').textContent = `${value.display_name && value.display_name !== value.agent_id ? `${value.display_name}(${value.agent_id})` : value.agent_id} · ${value.role === 'admin' ? '管理员' : '成员'}`; $('profile-repo').textContent = `${value.repo}${value.base_branch ? ` · 基准:${value.remote_name || '远程'}/${value.base_branch}` : ''}`;
127
212
  $('profile-diagnostics').hidden = !value.config_path; $('config-path').textContent = value.config_path || '';
128
213
  $('checks').replaceChildren(); $('progress').replaceChildren(); $('binding').hidden = value.phase !== 'ready'; $('runner-actions').hidden = value.phase !== 'ready'; $('diagnostic-actions').hidden = value.phase !== 'ready'; $('admin-tools').hidden = value.role !== 'admin' || value.phase !== 'ready';
129
214
  if (value.phase === 'ready') {
@@ -197,25 +282,66 @@ $('review-form').onsubmit = (event) => { event.preventDefault(); action(async ()
197
282
  await api('review-rules', { id: currentId, expected_version: reviewVersion, review_rules });
198
283
  await loadProfile(currentId); notice('审核规则已保存,新任务将使用此规则。');
199
284
  }); };
200
- for (const button of document.querySelectorAll('[data-view]')) button.onclick = () => action(async () => { clearInterval(timer); if (resetPlan) { await api('reset-cancel', { id: resetPlan.id, preview_token: resetPlan.preview_token }); resetPlan = null; resetDiscovery = null; $('reset-secret').value = ''; } remember(null); show(button.dataset.view); if (button.dataset.view === 'home') await overview(); if (button.dataset.view === 'admin' && $('admin-form').elements.repo.value) await checkProject(); });
285
+ for (const link of document.querySelectorAll('[data-view]')) link.onclick = (event) => {
286
+ event.preventDefault();
287
+ return action(async () => {
288
+ if (resetPlan) { await api('reset-cancel', { id: resetPlan.id, preview_token: resetPlan.preview_token }); resetPlan = null; resetDiscovery = null; $('reset-secret').value = ''; }
289
+ remember(null); show(link.dataset.view);
290
+ if (link.dataset.view === 'home') await overview();
291
+ if (link.dataset.view === 'admin' && $('admin-form').elements.repo.value) await checkProject();
292
+ });
293
+ };
201
294
  function repoMode(prefix) {
202
295
  const clone = $(`${prefix}-repo-mode`).value === 'clone'; $(`${prefix}-existing-repo`).hidden = clone; $(`${prefix}-clone-repo`).hidden = !clone;
203
296
  }
204
- for (const prefix of ['admin', 'join', 'own']) $(`${prefix}-repo-mode`).onchange = () => repoMode(prefix);
297
+ for (const prefix of ['admin', 'join', 'own']) $(`${prefix}-repo-mode`).onchange = () => {
298
+ repoMode(prefix);
299
+ if (prefix === 'admin') clearProjectSelection();
300
+ };
301
+ function clearProjectSelection() {
302
+ $('admin-remote-name').value = ''; $('admin-base-branch').value = ''; $('repository-options').hidden = true;
303
+ $('project-recommendation').textContent = '';
304
+ }
205
305
  async function checkProject() {
206
306
  const snapshot = values($('admin-form'));
207
307
  const selected = await api('project-check', snapshot);
208
308
  const current = values($('admin-form'));
209
- if (['repo_mode', 'repo', 'remote', 'destination'].some((key) => current[key] !== snapshot[key])) return;
309
+ if (['repo_mode', 'repo', 'remote', 'destination', 'remote_name', 'base_branch'].some((key) => current[key] !== snapshot[key])) return;
210
310
  const form = $('admin-form'); form.elements.repo.value = selected.repo; $('admin-repo-mode').value = 'existing'; repoMode('admin');
211
- $('project-recommendation').textContent = `项目已检查:${selected.repo}。${selected.tests.length ? '推荐:' + selected.tests.map((test) => test.command).join(' / ') + ',请确认或修改。' : '未找到常见测试入口,请填写适合项目的验证命令。'}`;
311
+ $('repository-options').hidden = false;
312
+ for (const [id, options, chosen, placeholder] of [
313
+ ['admin-remote-name', selected.remotes || [], selected.remote_name, '请选择协作远程'],
314
+ ['admin-base-branch', selected.branches || [], selected.base_branch, '请选择业务基准分支'],
315
+ ]) {
316
+ const select = $(id); select.replaceChildren();
317
+ const empty = text('option', placeholder); empty.value = ''; select.append(empty);
318
+ for (const value of options) { const option = text('option', value); option.value = value; select.append(option); }
319
+ select.value = chosen || '';
320
+ }
321
+ $('project-recommendation').textContent = selected.selection_required ? '请在下方选择协作远程或业务基准分支,再继续。' : `项目已检查:${selected.repo}。基准:${selected.remote_name}/${selected.base_branch}。${selected.tests.length ? '推荐:' + selected.tests.map((test) => test.command).join(' / ') + ',请确认或修改。' : '未找到常见测试入口,请填写适合项目的验证命令。'}`;
212
322
  if (!$('test-command').value && selected.tests.length) $('test-command').value = selected.tests[0].command;
213
323
  }
324
+ $('load-repository-settings').onclick = () => action(async () => {
325
+ const id = currentId, value = await api('repository-settings', { id });
326
+ if (id !== currentId || currentView !== 'profile') return;
327
+ const select = $('team-base-branch'); select.replaceChildren();
328
+ for (const branch of value.branches) { const option = text('option', branch); option.value = branch; select.append(option); }
329
+ select.value = value.branches.includes(value.base_branch) ? value.base_branch : value.default_branch || '';
330
+ repositoryPolicy = { id, expected_version: value.version }; $('repository-policy-form').hidden = false;
331
+ });
332
+ $('repository-policy-form').onsubmit = (event) => { event.preventDefault(); action(async () => {
333
+ if (!repositoryPolicy || repositoryPolicy.id !== currentId) throw new Error('请重新读取团队基准分支。');
334
+ await api('repository-policy', { ...repositoryPolicy, base_branch: $('team-base-branch').value });
335
+ await loadProfile(currentId); notice('团队基准分支已保存,同事和新增机器人会自动继承;已有任务保留原起始提交。');
336
+ }); };
214
337
  $('check-project').onclick = () => action(checkProject);
215
338
  for (const id of ['admin-repo', 'admin-remote', 'admin-destination']) $(id).onchange = () => {
216
339
  const form = $('admin-form');
340
+ clearProjectSelection();
217
341
  if ($('admin-repo-mode').value === 'clone' ? form.elements.remote.value && form.elements.destination.value : form.elements.repo.value) action(checkProject);
218
342
  };
343
+ $('admin-remote-name').onchange = () => { $('admin-base-branch').value = ''; action(checkProject); };
344
+ $('admin-base-branch').onchange = () => action(checkProject);
219
345
  $('admin-form').onsubmit = (event) => { event.preventDefault(); action(async () => {
220
346
  const input = values(event.target);
221
347
  const session = await api('admin-prepare', input); event.target.elements.secret.value = '';
@@ -229,7 +355,7 @@ async function inspectInvite() {
229
355
  const value = await api('inspect-invite', input);
230
356
  if ($('join-form').elements.password.value !== input.password || $('join-form').elements.file.files?.[0] !== file) return;
231
357
  $('invite-info').hidden = false; $('invite-summary').textContent = `${value.display_name || value.agent_id}(${value.agent_id})· 职责:${value.capabilities.join('、')}${value.team_name ? ' · ' + value.team_name : ''}`;
232
- $('invite-remote').textContent = value.remote;
358
+ $('invite-remote').textContent = `${value.remote}${value.base_branch ? ` · 基准分支:${value.base_branch}(自动继承)` : ''}`;
233
359
  if (!$('join-form').elements.destination.value) $('join-form').elements.destination.value = value.suggested_repo;
234
360
  }
235
361
  $('inspect-invite').onclick = () => action(inspectInvite);
@@ -247,8 +373,8 @@ async function connectMember(profile) {
247
373
  try { await verifyView(await api('discovery', { profile_id: profile.id })); }
248
374
  catch (error) { await loadProfile(profile.id); throw error; }
249
375
  }
250
- $('add-own-agent').onclick = () => action(async () => { ownAdminId = currentId; show('own'); });
251
- $('own-cancel').onclick = () => action(() => loadProfile(ownAdminId));
376
+ $('add-own-agent').onclick = () => action(async () => { ownAdminId = currentId; ownReturnProject = null; show('own'); });
377
+ $('own-cancel').onclick = () => action(() => ownReturnProject ? loadProject(ownReturnProject) : loadProfile(ownAdminId));
252
378
  $('own-form').onsubmit = (event) => { event.preventDefault(); action(async () => {
253
379
  const input = values(event.target); input.id = ownAdminId; input.prefer = event.target.elements.prefer.checked;
254
380
  let profile;
@@ -374,7 +500,10 @@ $('hide-delivery').onclick = () => action(async () => { $('delivery-panel').hidd
374
500
  assignmentRow();
375
501
  action(async () => {
376
502
  const token = location.hash.slice(1); if (token) { await api('session', { token }); history.replaceState(null, '', '/'); }
377
- const saved = remembered(); await overview();
378
- try { if (saved?.draft_id) await openDraft(saved.draft_id); else if (saved?.profile_id) { const value = await loadProfile(saved.profile_id); if (value?.phase === 'imported') await connectMember(value); } }
503
+ const saved = remembered(); show('home'); await overview();
504
+ try { if (saved?.draft_id) await openDraft(saved.draft_id); else if (saved?.profile_id) { const value = await loadProfile(saved.profile_id); if (value?.phase === 'imported') await connectMember(value); } else if (saved?.project_id) await loadProject(saved.project_id); }
379
505
  catch (error) { remember(null); show('home'); notice(`上次接入暂时无法恢复,可以从项目列表继续。${error.message}`); }
380
506
  });
507
+
508
+ function updateMotion() { if (document.documentElement) document.documentElement.dataset.pageHidden = String(document.visibilityState === 'hidden'); }
509
+ document.addEventListener?.('visibilitychange', updateMotion); updateMotion();
@@ -1,14 +1,16 @@
1
1
  <!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Codex Collab · 团队接入</title><link rel="stylesheet" href="/style.css"></head><body>
2
- <header><span class="brand">Codex Collab</span><span>本机团队工作台</span></header>
3
- <main><h1>把群里的任务,交给你的 Codex 对话</h1><p class="intro">选一种接入方式,按提示完成群验证,再把启用指令发给你的 Codex 对话。</p><div id="notice" role="status" hidden></div>
2
+ <div class="ambient" aria-hidden="true"><i></i><i></i></div><header><a class="brand" href="/" data-view="home" aria-label="Codex Collab 首页"><span class="brand-mark" aria-hidden="true">C</span>Codex Collab</a><span class="workspace-label"><span class="status-dot" aria-hidden="true"></span>本机协作工作台</span></header>
3
+ <main><div class="hero"><p class="eyebrow">YOUR TEAM, IN SYNC</p><h1>一个项目,<span>一支协作队伍。</span></h1><p class="intro">连接群机器人与 Codex 对话,让任务在团队中有序接力。</p></div><div id="notice" role="status" hidden></div>
4
4
  <nav><button data-view="home">我的项目</button><button data-view="admin">连接我的机器人</button><button data-view="join">接受同事邀请</button><button data-view="existing">导入已有配置</button></nav>
5
- <section id="home"><div class="entry-options"><button data-view="admin">连接我的机器人<small>首次接入,或为团队建立协作项目</small></button><button data-view="join">接受同事邀请<small>已有接入包,自动带入机器人和项目信息</small></button></div><p id="startup-status" class="muted"></p><h2>我的项目</h2><div id="profiles"></div><details id="removed-projects" hidden><summary>已移除的接入</summary><p class="muted">本机身份与恢复记录保留。恢复前请确保原 clone 未绑定其他身份。</p><div id="removed-profiles"></div></details></section>
5
+ <section id="home"><div class="entry-options"><button data-view="admin">连接我的机器人<small>首次接入,或为团队建立协作项目</small></button><button data-view="join">接受同事邀请<small>已有接入包,自动带入机器人和项目信息</small></button></div><p id="startup-status" class="muted"></p><div class="section-heading"><div><p class="eyebrow">PROJECTS</p><h2>我的项目</h2></div><span class="muted" id="project-count"></span></div><div id="profiles" class="project-grid"></div><details id="removed-projects" hidden><summary>已移除的接入</summary><p class="muted">本机身份与恢复记录保留。恢复前请确保原 clone 未绑定其他身份。</p><div id="removed-profiles"></div></details></section>
6
+ <section id="project" hidden><button id="project-back" class="back-button secondary">← 我的项目</button><div class="section-heading"><div><p class="eyebrow">PROJECT WORKSPACE</p><h2 id="project-title"></h2></div><button id="project-refresh" class="secondary">刷新项目</button></div><p id="project-repository" class="repository-path"></p><div id="project-summary" class="project-summary"></div><p id="project-sync" class="muted" role="status"></p><div class="section-heading robot-heading"><h3>项目机器人</h3><div class="manage-actions"><button id="project-add-own" hidden>添加我的机器人</button><button id="project-settings" class="secondary" hidden>团队管理</button></div></div><div id="project-agents" class="robot-grid"></div></section>
6
7
  <section id="admin" hidden><h2>连接我的机器人</h2><p>先在企业微信创建智能机器人并加入协作群。这里会通过群验证码自动获取群和你的 userid。</p>
7
8
  <form id="admin-form">
8
9
  <fieldset><legend>选择业务项目</legend><label>项目来源<select id="admin-repo-mode" name="repo_mode"><option value="existing">使用本机已有项目</option><option value="clone">从 Git 地址拉取新项目</option></select></label>
9
10
  <label id="admin-existing-repo">项目目录<input name="repo" id="admin-repo" placeholder="Git 仓库根目录的完整路径"></label>
10
11
  <div id="admin-clone-repo" hidden><label>Git 仓库地址<input name="remote" id="admin-remote" placeholder="HTTPS 或 SSH 地址,不含 Token"></label><label>保存到新目录(可选)<input name="destination" id="admin-destination" placeholder="留空按项目与名称生成独立目录"></label></div>
11
- <button id="check-project" type="button" class="secondary">检查项目并推荐验证命令</button><p id="project-recommendation" class="muted">自动拉取使用你本机已有的 Git 登录。仓库需要已有 main 分支。</p>
12
+ <button id="check-project" type="button" class="secondary">检查项目并推荐验证命令</button><p id="project-recommendation" class="muted">使用本机已有的 Git 登录,自动识别远程仓库和默认分支;无法确定时只需选择一次。</p>
13
+ <div id="repository-options" hidden><label>协作远程<select name="remote_name" id="admin-remote-name"><option value="">自动识别</option></select></label><label>业务基准分支<select name="base_branch" id="admin-base-branch"><option value="">使用远程默认分支</option></select></label><p class="muted">同事和新增机器人会继承这个基准分支。每个新任务固定起始提交,后续分支更新不会改变已有任务。</p></div>
12
14
  <label>验证命令<input id="test-command" name="test_command" required placeholder="例如 npm test、go test ./...、sh test.sh"></label><p class="muted">请确认这个命令适合项目。工具使用固定程序和参数执行,不运行管道或拼接命令。</p></fieldset>
13
15
  <div class="row"><label>你的显示名称<input name="display_name" required placeholder="例如 Alice 或张三" maxlength="80"></label><label>主要职责<select name="responsibility"><option value="fullstack">全栈开发</option><option value="frontend">前端开发</option><option value="backend">后端开发</option><option value="testing">测试</option><option value="docs">文档</option><option value="general">通用开发</option></select></label></div>
14
16
  <details><summary>更多设置:团队名称、内部身份与职责</summary><label>团队名称(可选)<input name="team_name" placeholder="留空使用项目名称" maxlength="80"></label><label>内部 Agent ID(可选)<input name="agent_id" pattern="[a-zA-Z0-9_-]{1,64}" placeholder="留空自动生成"></label><label>自定义职责标签(可选)<input name="capabilities" placeholder="英文标签,以逗号分隔"></label></details>
@@ -25,9 +27,9 @@
25
27
  <button type="submit">接入我的机器人</button><p class="muted">工具使用本机 Git 登录拉取代码,不会改写已有目录。导入后请按提示在群里确认一次身份,审批和配置会自动继续。</p></form></section>
26
28
  <section id="existing" hidden><h2>导入已有协作项目</h2><p>先停止原 Runner。导入会保留原成员身份、任务和对话绑定,并重新启动机器人。旧团队由管理员先导入。</p><form id="existing-form"><label>已有私有 peer.json 路径<input name="config_path" required placeholder="完整路径"></label><button type="submit">导入已有项目</button></form></section>
27
29
  <section id="verification" hidden><h2>在群里确认身份</h2><p id="connection" role="status"></p><p class="muted">发送消息后页面会自动继续,无需回来确认。刷新页面可恢复当前接入进度。</p><p>请用自己的企业微信账号,<strong>@ 刚刚分配的机器人</strong>,发送下面这条消息:</p><pre id="code"></pre><button id="copy-code">复制消息</button><button id="confirm-code">立即检查</button><button id="retry-code" class="secondary" hidden>重新连接</button></section>
28
- <section id="profile" hidden><div class="row"><h2 id="profile-title"></h2><button id="refresh">刷新项目状态</button></div><p id="profile-repo"></p><details id="profile-diagnostics" hidden><summary>查看本机配置路径</summary><pre id="config-path"></pre><button id="copy-config-path">复制配置路径</button></details><p id="next-step" class="next-step" role="status"></p><ol id="setup-steps" class="setup-steps"></ol><details><summary>连接检查详情</summary><div id="checks"></div></details><div id="progress"></div><div id="binding" hidden><h3>最后一步:在你选定的 Codex 对话启用</h3><p>在 Codex 打开上面的项目,信任项目并加载 MCP(已打开的项目请重新加载 MCP),将下面的指令发到要绑定的对话。Skill 会完成身份检查、对话绑定和轮询启用。完成后本页自动更新实际连接状态。</p><pre id="binding-text"></pre><button id="copy-binding">复制启用指令</button><details><summary>无法识别 Skill 时使用详细指令</summary><pre id="binding-details"></pre><button id="copy-binding-details">复制详细指令</button></details></div><div id="diagnostic-actions" hidden><button id="run-diagnostics" class="secondary">检查整条连接</button><button id="repair-project" class="secondary">修复本机配置</button><div id="diagnostic-result" role="status"></div></div><div id="runner-actions" hidden><button id="start">启动机器人连接</button><button id="stop" class="secondary">停止机器人连接</button><p class="muted">停止连接仅断开群机器人。对话中的轮询自动化需要在 Codex 中单独暂停。</p></div>
30
+ <section id="profile" hidden><button id="profile-back" class="back-button secondary" hidden>← 返回项目</button><div class="row"><h2 id="profile-title"></h2><button id="refresh" class="secondary">刷新机器人状态</button></div><p id="profile-repo"></p><details id="profile-diagnostics" hidden><summary>查看本机配置路径</summary><pre id="config-path"></pre><button id="copy-config-path">复制配置路径</button></details><p id="next-step" class="next-step" role="status"></p><ol id="setup-steps" class="setup-steps"></ol><details><summary>连接检查详情</summary><div id="checks"></div></details><div id="progress"></div><div id="binding" hidden><h3>最后一步:在你选定的 Codex 对话启用</h3><p>在 Codex 打开上面的项目,信任项目并加载 MCP(已打开的项目请重新加载 MCP),将下面的指令发到要绑定的对话。Skill 会完成身份检查、对话绑定和轮询启用。完成后本页自动更新实际连接状态。</p><pre id="binding-text"></pre><button id="copy-binding">复制启用指令</button><details><summary>无法识别 Skill 时使用详细指令</summary><pre id="binding-details"></pre><button id="copy-binding-details">复制详细指令</button></details></div><div id="diagnostic-actions" hidden><button id="run-diagnostics" class="secondary">检查整条连接</button><button id="repair-project" class="secondary">修复本机配置</button><div id="diagnostic-result" role="status"></div></div><div id="runner-actions" hidden><button id="start">启动机器人连接</button><button id="stop" class="secondary">停止机器人连接</button><p class="muted">停止连接仅断开群机器人。对话中的轮询自动化需要在 Codex 中单独暂停。</p></div>
29
31
  <details id="manage-project"><summary>管理与重置</summary><p>选择要调整的配置,先查看影响,再确认。每次只操作当前 Agent 和 clone。</p><div class="manage-actions"><button id="reset-conversation" class="secondary">重新绑定对话</button><button id="reset-robot" class="secondary">更新机器人凭据</button><button id="reset-project" class="danger">移除本机接入</button></div></details>
30
- <div id="admin-tools" hidden><details><summary>团队分工、审核与通知设置</summary><h3>团队 Agent 与分工</h3><div id="members"></div><div id="owners"></div><button id="add-own-agent" class="secondary">给自己增加机器人</button><div id="routes"></div><h3>真人审核与通知</h3><p>按职责设置:阶段结果提交后由谁审核、通知谁。关闭中间审核可直接接力;最终提交仍需真人审核,并由管理员完成任务。关闭审核的职责作为最后阶段时由管理员审核。</p><p id="review-policy-note" class="muted"></p><form id="review-form" hidden><div id="review-rules"></div><button type="submit">保存审核规则</button></form><p class="muted">规则保存后用于新任务。现有任务继续使用创建时的规则。审核通知附带分支、提交、测试和可复制的群命令;企业微信原生 @ 的显示效果需要在实际群中确认。</p></details><h3>批量分配机器人给同事</h3><p>每行分配一个独立机器人。同一个同事可以领取多行,分别绑定独立项目和对话。接入包密码自动生成。</p>
32
+ <div id="admin-tools" hidden><details><summary>团队分工、审核与通知设置</summary><h3>团队 Agent 与分工</h3><div id="members"></div><div id="owners"></div><button id="add-own-agent" class="secondary">给自己增加机器人</button><div id="routes"></div><h3>任务基准分支</h3><button type="button" id="load-repository-settings" class="secondary">读取并调整基准分支</button><form id="repository-policy-form" hidden><label>业务基准分支<select id="team-base-branch" required></select></label><p class="muted">保存后所有成员的新任务使用此分支。已创建任务保留原来的分支、起始提交和审核规则。</p><button type="submit">保存团队基准分支</button></form><h3>真人审核与通知</h3><p>按职责设置:阶段结果提交后由谁审核、通知谁。关闭中间审核可直接接力;最终提交仍需真人审核,并由管理员完成任务。关闭审核的职责作为最后阶段时由管理员审核。</p><p id="review-policy-note" class="muted"></p><form id="review-form" hidden><div id="review-rules"></div><button type="submit">保存审核规则</button></form><p class="muted">规则保存后用于新任务。现有任务继续使用创建时的规则。审核通知附带分支、提交、测试和可复制的群命令;企业微信原生 @ 的显示效果需要在实际群中确认。</p></details><h3>批量分配机器人给同事</h3><p>每行分配一个独立机器人。同一个同事可以领取多行,分别绑定独立项目和对话。接入包密码自动生成。</p>
31
33
  <form id="batch-form"><div id="assignment-rows"></div><div class="row"><button id="add-assignment" type="button" class="secondary">增加一位成员/机器人</button><button type="submit">生成全部接入包</button></div></form>
32
34
  <div id="delivery-panel" hidden><h3 id="delivery-title"></h3><p>接入包与密码请分开传递。下面的接入说明不包含密码或 Secret。</p><pre id="delivery-instructions"></pre><button id="copy-delivery">复制接入说明</button><label id="delivery-password-label">接入包密码<input id="delivery-password" type="password" readonly></label><button id="copy-password" type="button" class="secondary">复制密码</button><p id="delivery-password-note" class="muted" hidden></p><a id="delivery-download" class="secondary">下载这份接入包</a><button id="reissue-invite" type="button" class="secondary" hidden>重新生成接入包与密码</button><form id="reissue-form" hidden><p>旧包和未批准的旧入队申请将失效。保持原机器人和职责;请输入这个机器人的 Secret,自动生成新的密码。</p><label>机器人 Bot ID<input id="reissue-bot-id" readonly></label><label>机器人 Secret<input id="reissue-secret" name="secret" type="password" required autocomplete="off"></label><button type="submit">生成新的接入包与密码</button><button id="reissue-cancel" type="button" class="secondary">取消</button></form><button id="hide-delivery" type="button" class="secondary">收起</button></div>
33
35
  <details><summary>单独分配(兼容入口)</summary><form id="issue-form"><div class="row"><label>Agent ID<input name="agent_id" required pattern="[a-zA-Z0-9_-]{1,64}" placeholder="bob"></label><label>职责标签<input name="capabilities" required placeholder="backend"></label></div><div class="row"><label>机器人 Bot ID<input name="bot_id" required autocomplete="off"></label><label>机器人 Secret<input name="secret" type="password" required autocomplete="off"></label></div><label>自定义密码(可选,留空自动生成)<input name="password" type="password" minlength="12" autocomplete="off"></label><label class="checkbox"><input name="prefer" type="checkbox" checked>设为这些职责的首选执行者</label><label class="checkbox"><input name="commands_enabled" type="checkbox" checked>允许所有者通过此机器人发送群命令</label><button type="submit">生成接入包</button></form></details>
@@ -1,10 +1,14 @@
1
- :root{color-scheme:light;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif;color:#18312b;background:#f4f7f5}*{box-sizing:border-box}body{margin:0}header{background:#fff;border-bottom:1px solid #dbe6df;display:flex;gap:22px;padding:22px max(24px,calc((100vw - 980px)/2));font-size:14px;color:#677d72}.brand{font-weight:750;color:#126744;white-space:nowrap}main{max-width:980px;margin:40px auto;padding:0 24px}h1{font-size:30px;line-height:1.35;letter-spacing:-.5px}h2{font-size:22px;margin-top:0}h3{margin-top:30px;font-size:17px}p{line-height:1.8}.intro,.muted{color:#657970}.muted{font-size:13px}nav{display:flex;gap:10px;margin:25px 0;flex-wrap:wrap}nav button,.secondary{background:#fff;color:#285442;border:1px solid #c9dacf}button{font:inherit;background:#176b4a;color:#fff;border:1px solid transparent;border-radius:8px;padding:10px 16px;cursor:pointer;line-height:1.4}button:hover{filter:brightness(.95)}button:disabled{opacity:.55;cursor:wait}section{background:#fff;border:1px solid #dbe6df;border-radius:14px;padding:28px;box-shadow:0 8px 28px #18312b06}section[hidden],div[hidden]{display:none}form{display:grid;gap:17px;margin:24px 0}label{display:grid;gap:8px;font-size:14px}input{font:inherit;padding:12px;border:1px solid #c9dacf;border-radius:7px;width:100%;color:#18312b;background:#fff}input:focus{outline:2px solid #a4d0bb;outline-offset:1px}.row{display:flex;gap:20px;align-items:center}.row>*{flex:1;min-width:0}.row>button{flex:initial}.checkbox{display:flex;align-items:center}.checkbox input{width:auto}pre{background:#f0f5f2;border:1px solid #dce8df;padding:16px;border-radius:8px;white-space:pre-wrap;overflow-wrap:anywhere;font:13px/1.65 ui-monospace,SFMono-Regular,monospace}.card,.check{padding:15px 0;border-bottom:1px solid #e5ece7;display:flex;gap:12px;justify-content:space-between;align-items:center}.card p{margin:4px 0;font-size:13px;overflow-wrap:anywhere}.badge{font-size:13px;color:#176b4a;background:#e3f2e8;padding:5px 9px;border-radius:6px;white-space:nowrap}.pending{color:#896229;background:#fff3db}#notice{padding:14px 18px;border:1px solid #ebc8aa;background:#fff3e9;border-radius:8px;margin:20px 0;line-height:1.6}footer{text-align:center;font-size:12px;color:#6a7e72;padding:30px}#profile-repo{overflow-wrap:anywhere;font-size:14px;color:#657970}@media(max-width:640px){main{margin:25px auto;padding:0 16px}h1{font-size:24px}section{padding:20px}.row{flex-direction:column;align-items:stretch;gap:17px}.card{align-items:flex-start;flex-wrap:wrap}nav button{font-size:13px;padding:8px 10px}}
2
-
3
- form[hidden]{display:none}fieldset{border:1px solid #dbe6df;border-radius:9px;padding:16px;margin:0 0 16px;display:grid;gap:16px}legend{font-weight:600;padding:0 6px}select{font:inherit;padding:12px;border:1px solid #c9dacf;border-radius:7px;width:100%;color:#18312b;background:#fff}select:focus{outline:2px solid #a4d0bb;outline-offset:1px}
4
-
5
- button[hidden],details[hidden],label[hidden]{display:none}details{margin:24px 0}summary{cursor:pointer;font-weight:600;line-height:1.7}.manage-actions{display:flex;gap:12px;flex-wrap:wrap}.danger{background:#fff;border:1px solid #d9aca4;color:#943c30}#reset-effects{line-height:1.9;padding-left:24px}#reset-target{overflow-wrap:anywhere}#reset-verification{padding:16px;background:#f0f5f2;border-radius:8px}
6
-
7
- #setup-steps{display:flex;flex-wrap:wrap;gap:10px;list-style:none;padding:0;margin:22px 0}#setup-steps li{font-size:13px;border:1px solid #e0e7e2;border-radius:7px;padding:9px 12px;color:#738379}#setup-steps .done{color:#176b4a;background:#eaf5ed;border-color:#cbe4d3}#setup-steps .active{color:#845d26;background:#fff5e2;border-color:#ebd29f}#next-step{padding:14px 16px;background:#edf6f0;border-radius:8px}#delivery-panel{padding:18px;background:#f6f9f7;border:1px solid #dbe6df;border-radius:9px;margin-top:20px}#delivery-panel h3{margin-top:0}#delivery-panel button{margin:6px 6px 6px 0}.assignment details{margin:0}.assignment>button{justify-self:start;background:#fff;color:#657970;border-color:#c9dacf}#assignment-rows{display:grid;gap:14px}#assignment-rows fieldset{margin:0}@media(max-width:640px){#setup-steps{gap:7px}#setup-steps li{flex:1 1 42%}.card>a{display:inline-block}.card{gap:14px}}
8
-
9
- .entry-options{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;margin:12px 0 24px}.entry-options button{text-align:left;padding:24px;font-size:19px}.entry-options small{display:block;margin-top:10px;font-size:13px;font-weight:normal;line-height:1.6}#diagnostic-result p{line-height:1.7}#diagnostic-actions{margin:18px 0}
10
- form,label,fieldset,details,input,select{min-width:0}#invite-remote{overflow-wrap:anywhere}
1
+ :root{color-scheme:dark;--bg:#080e14;--surface:#101923;--surface-raised:#15212c;--line:#243440;--text:#e8f1f4;--muted:#9baeb9;--accent:#7cebc3;--accent-dark:#0a3b30;--radius:18px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif;color:var(--text);background:var(--bg);font-synthesis:none}
2
+ *{box-sizing:border-box}body{margin:0;min-height:100vh;background:radial-gradient(ellipse at 50% 0,#102b313d,transparent 65%)}body::before{content:"";position:fixed;inset:0;pointer-events:none;background-image:linear-gradient(#b6e6df04 1px,transparent 1px),linear-gradient(90deg,#b6e6df04 1px,transparent 1px);background-size:64px 64px;z-index:-1}.ambient{position:fixed;inset:0;overflow:hidden;pointer-events:none;z-index:-1}.ambient i{position:absolute;width:480px;height:480px;border-radius:50%;background:radial-gradient(circle,#25be9e12,transparent 70%);top:-170px;left:8%;animation:ambient-drift 24s ease-in-out infinite alternate}.ambient i+i{width:560px;height:560px;background:radial-gradient(circle,#497bdf0d,transparent 70%);top:10%;left:auto;right:-180px;animation-delay:-12s}
3
+ header{position:relative;border-bottom:1px solid #2434409c;display:flex;justify-content:space-between;align-items:center;gap:20px;padding:20px max(24px,calc((100vw - 1120px)/2));font-size:13px;background:#0c141de8}.brand{display:inline-flex;align-items:center;gap:12px;font-weight:730;letter-spacing:-.4px;color:var(--text);text-decoration:none;font-size:17px;white-space:nowrap}.brand-mark{display:grid;place-items:center;width:32px;height:32px;background:linear-gradient(135deg,#9bf5d5,#50bdb7);color:#0a2521;font-weight:850;border-radius:10px;box-shadow:0 0 26px #65d8ba18}.workspace-label{display:flex;align-items:center;gap:9px;color:var(--muted)}.status-dot{width:6px;height:6px;border-radius:50%;background:var(--accent);box-shadow:0 0 0 4px #7cebc30d}
4
+ main{max-width:1120px;margin:58px auto 0;padding:0 24px}.hero{max-width:760px;animation:enter-view .55s ease-out backwards}.eyebrow{font-size:10px;font-weight:700;letter-spacing:2.6px;color:var(--accent);margin:0 0 14px;line-height:1.6}h1{font-size:clamp(30px,4.5vw,46px);font-weight:720;line-height:1.28;letter-spacing:-1.4px;margin:0 0 18px}h1 span{color:var(--accent)}h2{font-size:23px;letter-spacing:-.5px;line-height:1.4;margin:0}h3{font-size:16px;margin:30px 0 14px;line-height:1.5}h4{font-size:16px;margin:0;line-height:1.6;overflow-wrap:anywhere}p{line-height:1.8}.intro,.muted{color:var(--muted)}.intro{font-size:15px;margin:0}.muted{font-size:13px}.warning-text{color:#f1bd83}
5
+ nav{display:flex;gap:8px;margin:32px 0 26px;flex-wrap:wrap}button,a.secondary{font:inherit;font-size:13px;font-weight:600;background:var(--accent);color:#092c24;border:1px solid transparent;border-radius:9px;padding:11px 17px;cursor:pointer;line-height:1.5;position:relative;transition:transform .18s ease,background-color .18s ease,border-color .18s ease,box-shadow .18s ease;touch-action:manipulation}button:hover{transform:translateY(-2px);background:#a4f5d9;box-shadow:0 5px 20px #7cebc312}button:active{transform:translateY(0) scale(.98)}button:disabled{opacity:.5;cursor:wait;transform:none;box-shadow:none}nav button,.secondary,a.secondary{background:#111c26;color:#c3d3dc;border:1px solid var(--line)}nav button:hover,.secondary:hover{background:#1b2b37;border-color:#456273;box-shadow:none}nav button[aria-current=page]{background:#1c302e;border-color:#497c6c;color:#b9f5df}a{color:var(--accent);text-underline-offset:3px}a.secondary{display:inline-block;text-decoration:none}button:focus-visible,a:focus-visible,summary:focus-visible{outline:2px solid var(--accent);outline-offset:4px}.back-button{margin:0 0 26px;background:transparent;border-color:transparent;color:var(--muted);padding:5px 0}.back-button:hover{background:transparent;border-color:transparent;color:var(--accent);box-shadow:none}
6
+ section{background:linear-gradient(145deg,#111c26,#0e1720);border:1px solid var(--line);border-radius:var(--radius);padding:30px;box-shadow:0 16px 55px #00000012}section:not([hidden]){animation:enter-view .35s cubic-bezier(.2,.7,.2,1) backwards}[hidden]{display:none!important}section>p{font-size:14px;color:var(--muted)}.section-heading{display:flex;justify-content:space-between;align-items:center;gap:16px;margin:0 0 20px}.section-heading .eyebrow{margin:0 0 8px;font-size:9px}.section-heading h3{margin:0}.robot-heading{margin-top:30px}
7
+ .entry-options{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px;margin:0 0 18px}.entry-options button{overflow:hidden;text-align:left;padding:24px 26px;font-size:18px;border-radius:13px;border:1px solid #345b50;background:linear-gradient(120deg,#1b3933,#172a2b);color:#dcfff0}.entry-options button::after{content:"↗";position:absolute;right:22px;top:20px;font-size:20px;color:#87c9b4;transition:transform .2s}.entry-options button:hover::after{transform:translate(3px,-3px)}.entry-options button+button{background:linear-gradient(120deg,#1b2939,#17202f);border-color:#33455e;color:#dfebff}.entry-options button+button::after{color:#9aaed6}.entry-options small{display:block;margin:10px 15px 0 0;font-size:12px;font-weight:400;color:#a9c2b8;line-height:1.8}.entry-options button+button small{color:#a8b5cc}#startup-status{font-size:12px;margin:0 0 30px}
8
+ .project-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,285px),1fr));gap:16px}.project-card{display:flex;flex-direction:column;align-items:stretch;justify-content:space-between;gap:22px;padding:24px;border:1px solid #2a3d49;border-radius:14px;background:linear-gradient(150deg,#172530,#111c26);overflow:hidden;position:relative;transition:border-color .25s,box-shadow .25s}.project-card::after{content:"";position:absolute;inset:0;border-radius:inherit;border-top:1px solid #b1efdd35;pointer-events:none}.project-card:hover{border-color:#4b7f72;box-shadow:0 8px 24px #00000020}.project-card-info{min-width:0}.project-icon{display:grid;place-items:center;width:42px;height:42px;border:1px solid #406659;border-radius:12px;background:#1c3932;color:#a8f2d5;font-size:20px;font-weight:650;margin-bottom:22px}.project-card h3{font-size:19px;line-height:1.5;margin:0 0 7px;overflow-wrap:anywhere}.project-card p{color:var(--muted);font-size:12px;margin:0 0 18px;overflow-wrap:anywhere}.project-card-info>span:last-child{color:#a9bfca;font-size:12px}.project-card>button{align-self:flex-start;background:transparent;color:var(--accent);border-color:#314c46;padding:8px 13px}.project-card>button:hover{background:#7cebc30b;border-color:#608f7f}.project-card,.robot-card{animation:enter-card .4s cubic-bezier(.2,.7,.2,1) backwards;animation-delay:calc(var(--order,0)*45ms)}
9
+ .repository-path,#profile-repo,#reset-target,#invite-remote{overflow-wrap:anywhere;color:var(--muted);font-size:13px}.project-summary{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1px;border:1px solid var(--line);border-radius:12px;background:var(--line);overflow:hidden;margin:25px 0 15px}.summary-item{background:#111e28;padding:19px 22px;display:flex;flex-direction:column;gap:8px;min-width:0}.summary-item strong{font-size:24px;font-weight:650;letter-spacing:-.5px;overflow-wrap:anywhere}.summary-item:last-child strong{font-size:20px;line-height:1.5}.summary-item span{font-size:11px;color:var(--muted)}.robot-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,285px),1fr));gap:15px}.robot-card{border:1px solid var(--line);border-radius:13px;padding:22px;background:#111e28;min-width:0}.robot-card.is-mine{border-color:#365f51;background:linear-gradient(145deg,#162923,#122028)}.robot-title{display:flex;justify-content:space-between;gap:10px;align-items:center}.robot-card>p{margin:9px 0 0;font-size:12px;color:var(--muted)}.robot-connection{border-top:1px solid var(--line);padding-top:18px;margin-top:20px}.robot-connection .repository-path{margin:12px 0 16px;font-size:11px}.robot-connection button{padding:8px 13px;font-size:12px}.badge{display:inline-flex;gap:7px;align-items:center;font-size:11px;font-weight:500;color:var(--accent);background:#1a3b30;padding:5px 9px;border-radius:6px;white-space:nowrap}.mine-badge{border:1px solid #376a54;flex-shrink:0}.pending{color:#f0c27e;background:#3a2e1c}.ready-badge::before{content:"";height:5px;width:5px;border-radius:50%;background:var(--accent);animation:status-pulse 2.8s ease-in-out infinite}
10
+ form{display:grid;gap:18px;margin:25px 0}form,label,fieldset,details,input,select{min-width:0}label{display:grid;gap:9px;font-size:13px;color:#c7d5dd}input,select{font:inherit;font-size:14px;padding:12px 14px;border:1px solid #31424f;border-radius:8px;width:100%;color:var(--text);background:#0c151e;transition:border-color .18s,box-shadow .18s}input::placeholder{color:#708592}input:focus,select:focus{outline:none;border-color:#6dc6a8;box-shadow:0 0 0 3px #7cebc310}input[type=file]{font-size:12px}input[type=file]::file-selector-button{border:1px solid #3a4f5e;border-radius:5px;background:#1d2c38;color:var(--text);padding:6px 10px;margin-right:12px}.row{display:flex;gap:20px;align-items:center}.row>*{flex:1;min-width:0}.row>button{flex:initial}.checkbox{display:flex;align-items:flex-start;line-height:1.7}.checkbox input{width:auto;margin:4px 0 0;accent-color:var(--accent)}fieldset{border:1px solid var(--line);border-radius:11px;padding:20px;margin:0 0 16px;display:grid;gap:16px}legend{font-weight:600;padding:0 8px;color:#d5e4eb}pre{background:#09131b;border:1px solid var(--line);padding:18px;border-radius:10px;white-space:pre-wrap;overflow-wrap:anywhere;color:#c3e5d6;font:12px/1.8 ui-monospace,SFMono-Regular,monospace}details{margin:24px 0}summary{cursor:pointer;font-size:14px;font-weight:550;line-height:1.8;color:#c3d4dd}details[open]>summary{margin-bottom:18px;color:var(--accent)}.card:not(.project-card),.check{padding:17px 0;border-bottom:1px solid var(--line);display:flex;gap:14px;justify-content:space-between;align-items:center}.card p{margin:4px 0;font-size:12px;overflow-wrap:anywhere}.card:not(.project-card)>div{min-width:0}.check{font-size:13px}.check .badge{flex-shrink:0}.manage-actions{display:flex;gap:10px;flex-wrap:wrap}.danger{background:#2a1b20;border:1px solid #6a3a42;color:#f3a7a7}.danger:hover{background:#38212a;border-color:#a6656d}.manage-actions .danger{box-shadow:none}#notice{padding:14px 18px;border:1px solid #725035;background:#30241a;color:#f5cfa9;border-radius:10px;margin:20px 0;font-size:13px;line-height:1.8}#reset-effects{line-height:1.9;padding-left:24px;font-size:14px}#reset-verification{padding:18px;background:#142822;border:1px solid #2a4b3e;border-radius:10px}
11
+ #setup-steps{display:flex;flex-wrap:wrap;gap:9px;list-style:none;padding:0;margin:24px 0}#setup-steps li{font-size:11px;border:1px solid var(--line);border-radius:7px;padding:9px 12px;color:#8fa2ad}#setup-steps .done{color:#99e9bf;background:#153026;border-color:#315d45}#setup-steps .active{color:#f0c27e;background:#30271a;border-color:#6c5330}#next-step{padding:16px 18px;background:#142b26;border:1px solid #2b5144;border-radius:10px;color:#bbe6d3;line-height:1.8;font-size:14px}#binding h3{color:#b0f0d6}#delivery-panel{padding:22px;background:#13241f;border:1px solid #365c4b;border-radius:12px;margin-top:22px}#delivery-panel h3{margin-top:0}#delivery-panel button{margin:6px 6px 6px 0}.assignment details{margin:0}.assignment>button{justify-self:start;background:transparent;color:var(--muted);border-color:var(--line)}#assignment-rows{display:grid;gap:14px}#assignment-rows fieldset{margin:0}#diagnostic-result p{font-size:13px;line-height:1.8}#diagnostic-actions{margin:20px 0}footer{text-align:center;font-size:11px;color:#7e939f;padding:30px 24px 40px;line-height:1.8}
12
+ @keyframes enter-view{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}@keyframes enter-card{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:translateY(0)}}@keyframes ambient-drift{from{transform:translate3d(0,0,0)}to{transform:translate3d(90px,65px,0)}}@keyframes status-pulse{0%,100%{opacity:1}50%{opacity:.4}}html[data-page-hidden=true] *,html[data-page-hidden=true] *::before,html[data-page-hidden=true] *::after{animation-play-state:paused!important}
13
+ @media(max-width:640px){header{padding:17px 20px}.brand{font-size:15px}.workspace-label{font-size:11px;gap:7px}.brand-mark{width:28px;height:28px}main{margin:34px auto 0;padding:0 16px}h1{letter-spacing:-.8px}h1 span{display:block}.hero .eyebrow{font-size:9px}.intro{font-size:13px}nav{gap:7px;margin:25px 0 20px}nav button{font-size:11px;padding:8px 10px}section{padding:22px 18px;border-radius:14px}.entry-options{grid-template-columns:1fr;gap:12px}.entry-options button{font-size:16px;padding:20px}.entry-options small{margin-top:7px}.section-heading{align-items:flex-start;gap:12px}.section-heading h2{font-size:21px}.section-heading>button{font-size:11px;white-space:nowrap}.robot-heading{flex-direction:column;align-items:stretch}.robot-heading .manage-actions{justify-content:flex-start}.project-summary{margin-top:20px}.summary-item{padding:16px 12px}.summary-item strong{font-size:22px}.summary-item:last-child strong{font-size:15px;line-height:1.9}.summary-item span{font-size:10px}.row{flex-direction:column;align-items:stretch;gap:16px}.row>button{align-self:flex-start}.card:not(.project-card){align-items:flex-start;flex-wrap:wrap}.project-card{padding:22px}.project-grid,.robot-grid{grid-template-columns:minmax(0,1fr)}.manage-actions button{font-size:12px}#setup-steps{gap:7px}#setup-steps li{flex:1 1 42%;padding:9px}.ambient i{width:300px;height:300px}.ambient i+i{width:340px;height:340px}fieldset{padding:16px}footer{font-size:10px}}
14
+ @media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}button:hover,button:active{transform:none}.entry-options button:hover::after{transform:none}}
@@ -63,10 +63,11 @@ export class DesktopExecutor {
63
63
  async prepare(job, task) {
64
64
  const refs = task.state.checkpoint;
65
65
  const fetched = `refs/collab-desktop/${this.service.agentId}/${task.spec.id}/${job.token.epoch}`;
66
- await git(this.config.repo, 'fetch', '--no-tags', '--no-write-fetch-head', 'origin', `${refs ? `refs/heads/${refs.branch}` : 'refs/heads/main'}:${fetched}`);
66
+ await git(this.config.repo, 'fetch', '--no-tags', '--no-write-fetch-head', this.service.store.remoteName, `refs/heads/${refs?.branch || task.spec.base_branch}:${fetched}`);
67
67
  const tip = await git(this.config.repo, 'rev-parse', '--verify', `${fetched}^{commit}`);
68
68
  if (refs) await git(this.config.repo, 'merge-base', '--is-ancestor', refs.commit, tip);
69
- const source = refs?.commit || tip;
69
+ const source = refs?.commit || task.spec.base_commit || tip;
70
+ if (!refs && task.spec.base_commit) await git(this.config.repo, 'merge-base', '--is-ancestor', source, tip);
70
71
  await this.service.assertLease(task.spec.id, job.token);
71
72
  const worktree = join(this.config.repo, '.codex-collab-worktrees', `${task.spec.id.toLowerCase()}-${this.service.agentId}-${job.token.epoch}`);
72
73
  const branch = `codex/desktop-${task.spec.id.toLowerCase()}-${this.service.agentId}-${job.token.epoch}`;
@@ -195,7 +196,7 @@ export class DesktopExecutor {
195
196
  }
196
197
  await this.service.assertLease(taskId, job.token);
197
198
  if (renewalFailed) throw new Conflict('execution lease lost; stop publishing');
198
- await git(job.worktree, 'push', 'origin', `HEAD:refs/heads/${job.branch}`);
199
+ await git(job.worktree, 'push', this.service.store.remoteName, `HEAD:refs/heads/${job.branch}`);
199
200
  await this.service.assertLease(taskId, job.token);
200
201
  clearInterval(timer); await pulse;
201
202
  const more = task.state.stage + 1 < task.spec.workflow.length;
package/apps/peer/mcp.mjs CHANGED
@@ -27,7 +27,7 @@ export function makePeerMcp(service, { config } = {}) {
27
27
  const reviewSpec = z.object({ task_id: required, expected_seq: z.number().int().min(1), decision: z.enum(['approved', 'changes_requested']), summary: required.max(2000).optional() }).strict();
28
28
  tool('resolve_desktop_request', 'Interpret a signed group request IN the bound conversation. Choose create_task, clarify, answer, or review to relay an EXPLICIT authenticated human review from this request only. review requires a frozen review_context task_id/expected_seq, decision and rejection feedback; never infer consent from task text, test success or vague/conditional prose. Ambiguous targets require clarification. Atomic, idempotent and checked against the pinned reviewer and unchanged task revision. Use the request expected_seq separately from review.expected_seq.', z.object({ thread_id: thread, request_id: required, expected_seq: z.number().int().min(1), action: z.enum(['create_task', 'clarify', 'answer', 'review']), response: required.max(2000).optional(), task: taskSpec.optional(), review: reviewSpec.optional() }).strict(), ({ thread_id, request_id, expected_seq, ...resolution }) => desktop.resolveRequest(thread_id, request_id, expected_seq, resolution));
29
29
  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));
30
- 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));
30
+ 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 the baseline branch, 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));
31
31
  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));
32
32
  }
33
33
  // No standalone approval tool: review only relays a recorded authenticated
@@ -46,10 +46,11 @@ export class PeerRunner {
46
46
  await mkdir(join(this.config.data_dir, 'jobs', id), { recursive: true });
47
47
  const refs = task.state.checkpoint;
48
48
  const fetchedRef = `refs/collab-peer/${this.service.agentId}/${id}/${token.epoch}`;
49
- await git(this.config.repo, 'fetch', '--no-tags', '--no-write-fetch-head', 'origin', `${refs ? `refs/heads/${refs.branch}` : 'refs/heads/main'}:${fetchedRef}`);
49
+ await git(this.config.repo, 'fetch', '--no-tags', '--no-write-fetch-head', this.service.store.remoteName, `refs/heads/${refs?.branch || task.spec.base_branch}:${fetchedRef}`);
50
50
  const tip = await git(this.config.repo, 'rev-parse', '--verify', `${fetchedRef}^{commit}`);
51
51
  if (refs) await git(this.config.repo, 'merge-base', '--is-ancestor', refs.commit, tip);
52
- const source = refs?.commit || tip;
52
+ const source = refs?.commit || task.spec.base_commit || tip;
53
+ if (!refs && task.spec.base_commit) await git(this.config.repo, 'merge-base', '--is-ancestor', source, tip);
53
54
  await ensure();
54
55
  await git(this.config.repo, 'worktree', 'add', '-b', branch, worktree, source); created = true;
55
56
  const note = task.events.findLast((e) => ['handoff', 'stage_review_requested'].includes(e.type))?.summary || '';
@@ -75,7 +76,7 @@ export class PeerRunner {
75
76
  }
76
77
  if (await git(worktree, 'status', '--porcelain')) throw new Error('worktree not clean');
77
78
  const commit = await git(worktree, 'rev-parse', 'HEAD'); if (commit === source) throw new Error('agent produced no code changes');
78
- await ensure(); await git(worktree, 'push', 'origin', `HEAD:refs/heads/${branch}`); await ensure();
79
+ await ensure(); await git(worktree, 'push', this.service.store.remoteName, `HEAD:refs/heads/${branch}`); await ensure();
79
80
  clearInterval(timer); await pulse; await ensure();
80
81
  const more = task.state.stage + 1 < task.spec.workflow.length;
81
82
  phase = 'protocol';
@@ -1,3 +1,4 @@
1
+ import { resolveRemote } from '../../packages/peer/repository.mjs';
1
2
  import { readFile } from 'node:fs/promises';
2
3
  import { isAbsolute, resolve } from 'node:path';
3
4
  import { makeSigner, readPrivateJson, validateTeam } from '../../packages/peer/config.mjs';
@@ -8,6 +9,8 @@ export async function readPeerConfig(path, { allowDisabled = false } = {}) {
8
9
  const config = await readPrivateJson(resolve(path));
9
10
  if (config.local_disabled === true && !allowDisabled) throw new Error('此项目已移除本机接入,请通过向导恢复后重试。');
10
11
  if (config.schema_version !== 2 || ![config.repo, config.data_dir, config.team_file].every((p) => typeof p === 'string' && isAbsolute(p)) || typeof config.agent_id !== 'string') throw new Error('peer config needs v2 and absolute repo/data_dir/team_file paths');
12
+ if (config.remote_name !== undefined && (typeof config.remote_name !== 'string' || !config.remote_name || config.remote_name.startsWith('-') || /[\s\x00-\x1f\x7f]/.test(config.remote_name))) throw new Error('invalid configured remote name');
13
+ if (config.remote_url !== undefined && (typeof config.remote_url !== 'string' || !config.remote_url || /[\x00-\x1f\x7f]/.test(config.remote_url))) throw new Error('invalid configured remote URL');
11
14
  const team = validateTeam(JSON.parse(await readFile(config.team_file, 'utf8')));
12
15
  if (config.codex_home !== undefined && (typeof config.codex_home !== 'string' || !isAbsolute(config.codex_home))) throw new Error('codex_home must be an absolute path');
13
16
  if (config.auto_execute !== undefined && typeof config.auto_execute !== 'boolean') throw new Error('auto_execute must be a boolean');
@@ -25,6 +28,8 @@ export async function readPeerConfig(path, { allowDisabled = false } = {}) {
25
28
  }
26
29
  export async function loadRuntime(path, options = {}) {
27
30
  const { config, team } = await readPeerConfig(path);
28
- const store = await new PeerGitStore({ registryRoot: config.team_registry_root, repo: config.repo, dataDir: config.data_dir, team, agentId: config.agent_id, privateKey: config.private_key, ...options }).init();
31
+ const selected = await resolveRemote(config.repo, { remoteName: config.remote_name, expectedRemote: config.remote_url, legacyOrigin: !config.remote_name && !config.remote_url });
32
+ config.remote_name = selected.remote_name;
33
+ const store = await new PeerGitStore({ remoteName: selected.remote_name, remoteUrl: selected.remote, registryRoot: config.team_registry_root, repo: config.repo, dataDir: config.data_dir, team, agentId: config.agent_id, privateKey: config.private_key, ...options }).init();
29
34
  return { config, team, store, service: new PeerService(store) };
30
35
  }
@@ -137,7 +137,7 @@ export function formatPeerNotification(task, type, agentId, team) {
137
137
  if (type === 'reviewed') {
138
138
  if (event.decision === 'changes_requested') detail.push(`审核退回,${task.spec.workflow[task.state.stage]} 阶段重新执行。\n修改意见:${notificationText(task.state.review_feedback)}\n从提交 ${task.state.checkpoint.commit} 继续修改。`);
139
139
  else if (task.state.status === 'waiting') detail.push('真人审核已通过。', next());
140
- else detail.push(`最终审核已通过;请管理员 @ 自己的机器人发送:协作 完成 ${task.spec.id}。代码分支尚未合并 main。`);
140
+ else detail.push(`最终审核已通过;请管理员 @ 自己的机器人发送:协作 完成 ${task.spec.id}。代码分支尚未合并 ${task.spec.base_branch}。`);
141
141
  }
142
142
  if (type === 'run_failed') detail.push('本阶段未成功,按 Git 路由交给剩余候选或等待人工处理。');
143
143
  return [base, ...detail].join('\n');
@@ -167,7 +167,7 @@ Runner 收到已验证审核人的群消息时,把本群由该机器人负责
167
167
 
168
168
  对话通过 `resolve_desktop_request` 的 `review` action 提交 `task_id`、冻结的 `expected_seq`、`decision`;退回还需要从真人消息中原文摘录具体意见 `summary`。服务检查当前身份、任务固定的审核策略、所属群、状态、版本和 SHA,再把任务 `reviewed` 事件与 inbox `reviewed` 结果在同一次 Git 提交中发布。重复处理同一消息返回原结果,不会再次推进阶段;原文、候选版本和决策摘要留在团队 Git 中。
169
169
 
170
- 如果提交或任务状态在处理前变化,审核会被拒绝,需要真人查看最新结果后重新发一条消息。收到提交前说的“通过”不能批准未来结果。只有新请求会捕获新的候选,旧消息及其澄清不升级版本。审核只是按原工作流继续;最终通过不自动完成任务或合并 main。
170
+ 如果提交或任务状态在处理前变化,审核会被拒绝,需要真人查看最新结果后重新发一条消息。收到提交前说的“通过”不能批准未来结果。只有新请求会捕获新的候选,旧消息及其澄清不升级版本。审核只是按原工作流继续;最终通过不自动完成任务或合并团队基准分支。
171
171
 
172
172
  ## MCP 与运行约束
173
173
 
@@ -187,7 +187,7 @@ Runner 收到已验证审核人的群消息时,把本群由该机器人负责
187
187
  - 编辑阶段没有无限续租定时器。定时 app heartbeat 只是唤起检查;必须由活跃对话显式调用续租。只有有超时的前台固定测试/发布调用会在调用期间续租。Bot 在线不是对话活着的证据。
188
188
  - 首选不接单,3 分钟后候补可接;执行掉线,15 分钟 lease 过期加 clock skew 后可接管。旧进程/对话应停止,不改写已交接内容。没有签名成功 checkpoint 的未提交进度不会自动恢复给另一台机器。
189
189
  - 本机绑定使用调用参数 thread_id 与私有绑定比对,是工具路由与防误操作措施,**不是由 Codex 宿主认证的调用者身份**。同一台机器上能访问同一私钥/配置的其他对话不构成安全隔离边界;真正成员授权靠独立签名私钥和团队策略。
190
- - MCP 不发完整对话,不自动审核,不 merge main;群通知失败不回滚 Git 事件,适配器按成功回执持久化游标重试。
190
+ - MCP 不发完整对话,不自动审核,不合并团队基准分支;群通知失败不回滚 Git 事件,适配器按成功回执持久化游标重试。
191
191
 
192
192
  ## 停止、状态和重新绑定
193
193
 
@@ -1,6 +1,6 @@
1
1
  # 网页向导功能检查与测试记录
2
2
 
3
- 最近检查日期:2026-10-22。对象是工具仓库中的网页向导源码,初始记录对应 0.3.1 的发布前验证;后面的补充记录覆盖后续源码变更。
3
+ 最近检查日期:2026-10-24。对象是工具仓库中的网页向导源码,初始记录对应 0.3.1 的发布前验证;后面的补充记录覆盖后续源码变更。
4
4
 
5
5
  ## 测试范围
6
6
 
@@ -115,3 +115,14 @@ Skill 校验通过。`npm pack --dry-run` 确认发布包包含 `guided.mjs`、`
115
115
  使用本机 Chrome 实际检查:首页双入口、当前项目与本机名称默认值、验证命令推荐、连接诊断与修复、邀请文件上传和密码输入后自动读取、推荐 clone 目录。桌面与 390px 手机视口检查通过,邀请信息展开后没有横向溢出,页面没有脚本错误。机器人认证仍使用隔离测试连接,未连接真实企业微信群或创建 Codex 自动化。
116
116
 
117
117
  实际 npm tarball 在独立目录安装,命令入口、后台启动、页面资源、带会话鉴权的 API 与服务状态检查均通过。发布包包含 74 个文件,新增运行模块齐全,不包含本机凭据、运行日志或测试目录。Linux 仅验证 systemd 配置生成和失败处理;Windows 登录自启尚不支持。尚未实际退出登录或重启电脑,也未完成跨机器企业微信、真实 Codex 自动化与睡眠恢复的联合验收。
118
+
119
+
120
+ ## 2026-10-24 · 0.7.0 项目工作台与页面动效
121
+
122
+ 完整自动测试通过 203 项,0 失败。新增测试覆盖同仓库不同 clone/HTTPS 与 SSH 地址的聚合、fork 与签名根隔离、统一机器人列表、“我的”归属标记、普通成员只读展示、身份冲突恢复、离线时选择最新的已验证签名记录、真实临时 Git 入队后的聚合、项目 API 会话保护、项目/机器人往返导航、刷新恢复、过期响应隔离及后台同步跳过。API 不返回真人 userid、机器人 Secret 或签名私钥。
123
+
124
+ 本机 Chrome 使用隔离的示例 API 与真实页面资源,检查首页、项目列表、机器人详情、添加本人机器人取消返回、刷新恢复、管理员与成员接入表单。在 1440、768、390、320 像素宽度检查无横向溢出,无页面脚本错误。示例项目截图仅保存于本机临时目录,未加入源码和 npm 包。
125
+
126
+ 持续动画仅有两处背景光晕位移与就绪状态透明度变化;在动画稳定后的 1.8 秒 Chrome Performance 采样中,新增 LayoutCount、RecalcStyleCount 和 ScriptDuration 均为 0。此记录反映本机隔离示例,不代表所有设备上的帧率。一次性入场动画完成后不保留最终变换。系统减少动态效果设置能关闭全部动画;页面隐藏信号能暂停动画,后台项目轮询不发出同步请求。
127
+
128
+ npm tarball 在独立目录安装后,命令入口、带会话鉴权的 API、新项目读取路由和静态页面资源检查通过。包包含 77 个文件,含新增项目聚合模块,不包含测试目录、本机凭据、运行日志或截图。此轮验证未连接真实企业微信群、修改个人业务 clone 或建立 Codex 自动化;任务协议与既有操作授权保持原有行为。